Google Maps V3 - Cara menghitung tingkat zoom untuk batas yang diberikan

138

Saya mencari cara untuk menghitung tingkat zoom untuk batas tertentu menggunakan Google Maps V3 API, mirip dengan getBoundsZoomLevel()di V2 API.

Inilah yang ingin saya lakukan:

// These are exact bounds previously captured from the map object
var sw = new google.maps.LatLng(42.763479, -84.338918);
var ne = new google.maps.LatLng(42.679488, -84.524313);
var bounds = new google.maps.LatLngBounds(sw, ne);
var zoom = // do some magic to calculate the zoom level

// Set the map to these exact bounds
map.setCenter(bounds.getCenter());
map.setZoom(zoom);

// NOTE: fitBounds() will not work

Sayangnya, saya tidak dapat menggunakan fitBounds()metode ini untuk kasus penggunaan khusus saya. Ini bekerja dengan baik untuk memasang penanda pada peta, tetapi tidak berfungsi dengan baik untuk menetapkan batas yang tepat. Ini adalah contoh mengapa saya tidak bisa menggunakan fitBounds()metode ini.

map.fitBounds(map.getBounds()); // not what you expect
Nick Clark
sumber
4
Contoh terakhir sangat bagus dan sangat ilustratif! +1. Saya memiliki masalah yang sama .
TMS
Maaf, terkait pertanyaan yang salah, ini tautan yang benar .
TMS
Pertanyaan ini bukan duplikat dari pertanyaan lain . Jawaban untuk pertanyaan lain adalah menggunakan fitBounds(). Pertanyaan ini menanyakan apa yang harus dilakukan ketika fitBounds()tidak mencukupi - baik karena terlalu besar atau Anda tidak ingin memperbesar (yaitu, Anda hanya ingin tingkat zoom).
John S
@Nick Clark: Bagaimana Anda tahu batas, batas yang harus ditetapkan? Bagaimana Anda menangkap mereka sebelumnya?
varaprakash

Jawaban:

108

Pertanyaan serupa telah diajukan di grup Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Level zoom terpisah, dengan skala berlipat ganda di setiap langkah. Jadi secara umum Anda tidak dapat memenuhi batas yang Anda inginkan dengan tepat (kecuali jika Anda sangat beruntung dengan ukuran peta tertentu).

Masalah lain adalah rasio antara panjang sisi misalnya Anda tidak dapat memasukkan batas tepat ke persegi panjang tipis di dalam peta persegi.

Tidak ada jawaban yang mudah untuk bagaimana menyesuaikan batas yang tepat, karena meskipun Anda bersedia mengubah ukuran div peta, Anda harus memilih ukuran dan tingkat zoom terkait yang Anda ubah (secara kasar, apakah Anda membuatnya lebih besar atau lebih kecil) daripada saat ini?).

Jika Anda benar-benar perlu menghitung zoom, alih-alih menyimpannya, ini harus dilakukan caranya:

Proyeksi Mercator melengkungkan lintang, tetapi setiap perbedaan dalam bujur selalu mewakili fraksi yang sama dari lebar peta (perbedaan sudut dalam derajat / 360). Pada zoom nol, seluruh peta dunia adalah 256x256 piksel, dan memperbesar setiap level menggandakan lebar dan tinggi. Jadi setelah aljabar kecil kita dapat menghitung zoom sebagai berikut, asalkan kita tahu lebar peta dalam piksel. Perhatikan bahwa karena garis bujur membungkus, kita harus memastikan sudutnya positif.

var GLOBE_WIDTH = 256; // a constant in Google's map projection
var west = sw.lng();
var east = ne.lng();
var angle = east - west;
if (angle < 0) {
  angle += 360;
}
var zoom = Math.round(Math.log(pixelWidth * 360 / angle / GLOBE_WIDTH) / Math.LN2);
Giles Gardam
sumber
1
Tidakkah Anda harus mengulang ini untuk lat dan kemudian memilih min dari hasil 2? Saya tidak berpikir ini akan berhasil untuk batas sempit yang tinggi .....
whiteatom
3
Berfungsi bagus untuk saya dengan perubahan Math.round ke Math.floor. Terima kasih banyak.
Pete
3
Bagaimana ini bisa benar jika tidak memperhitungkan lintang? Di dekat garis khatulistiwa seharusnya baik-baik saja tetapi skala peta pada tingkat zoom yang diberikan berubah tergantung pada garis lintang!
Eyal
1
@Pete poin bagus, secara umum Anda mungkin ingin membulatkan tingkat zoom sehingga Anda pas sedikit lebih dari yang diinginkan di peta, daripada sedikit kurang. Saya menggunakan Math.round karena dalam situasi OP nilai sebelum pembulatan harus kira-kira tidak terpisahkan.
Giles Gardam
20
berapa nilai untuk pixelWidth
albert Jegani
279

Terima kasih kepada Giles Gardam untuk jawabannya, tetapi hanya membahas garis bujur dan bukan garis lintang. Solusi lengkap harus menghitung level zoom yang diperlukan untuk garis lintang dan tingkat zoom yang diperlukan untuk garis bujur, dan kemudian mengambil yang lebih kecil (lebih jauh) dari keduanya.

Berikut adalah fungsi yang menggunakan lintang dan bujur:

function getBoundsZoomLevel(bounds, mapDim) {
    var WORLD_DIM = { height: 256, width: 256 };
    var ZOOM_MAX = 21;

    function latRad(lat) {
        var sin = Math.sin(lat * Math.PI / 180);
        var radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
        return Math.max(Math.min(radX2, Math.PI), -Math.PI) / 2;
    }

    function zoom(mapPx, worldPx, fraction) {
        return Math.floor(Math.log(mapPx / worldPx / fraction) / Math.LN2);
    }

    var ne = bounds.getNorthEast();
    var sw = bounds.getSouthWest();

    var latFraction = (latRad(ne.lat()) - latRad(sw.lat())) / Math.PI;

    var lngDiff = ne.lng() - sw.lng();
    var lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;

    var latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
    var lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);

    return Math.min(latZoom, lngZoom, ZOOM_MAX);
}

Demo on jsfiddle

Parameter:

Nilai parameter "batas" harus menjadi google.maps.LatLngBoundsobjek.

Nilai parameter "mapDim" harus berupa objek dengan properti "height" dan "width" yang mewakili tinggi dan lebar elemen DOM yang menampilkan peta. Anda mungkin ingin mengurangi nilai-nilai ini jika Anda ingin memastikan bantalan. Artinya, Anda mungkin tidak ingin penanda peta di dalam batas terlalu dekat dengan tepi peta.

Jika Anda menggunakan perpustakaan jQuery, mapDimnilainya dapat diperoleh sebagai berikut:

var $mapDiv = $('#mapElementId');
var mapDim = { height: $mapDiv.height(), width: $mapDiv.width() };

Jika Anda menggunakan pustaka Prototipe, nilai mapDim dapat diperoleh sebagai berikut:

var mapDim = $('mapElementId').getDimensions();

Nilai Pengembalian:

Nilai kembali adalah tingkat pembesaran maksimum yang masih akan menampilkan seluruh batas. Nilai ini akan berada di antara 0dan tingkat zoom maksimum, inklusif.

Level zoom maksimum adalah 21. (Saya yakin itu hanya 19 untuk Google Maps API v2.)


Penjelasan:

Google Maps menggunakan proyeksi Mercator. Dalam proyeksi Mercator, garis bujur sama spasi, tetapi garis lintang tidak. Jarak antara garis lintang meningkat saat mereka bergerak dari garis khatulistiwa ke kutub. Bahkan jaraknya cenderung menuju infinity saat mencapai kutub. Namun, peta Google Maps tidak menunjukkan garis lintang di atas sekitar 85 derajat Utara atau di bawah sekitar -85 derajat Selatan. ( referensi ) (Saya menghitung cutoff aktual pada +/- 85.05112877980658 derajat.)

Ini membuat perhitungan fraksi untuk batas lebih rumit untuk lintang daripada untuk bujur. Saya menggunakan rumus dari Wikipedia untuk menghitung fraksi lintang. Saya menganggap ini cocok dengan proyeksi yang digunakan oleh Google Maps. Lagi pula, halaman dokumentasi Google Maps yang saya tautkan di atas berisi tautan ke halaman Wikipedia yang sama.

Catatan lain:

  1. Tingkat zoom berkisar dari 0 hingga tingkat zoom maksimum. Level zoom 0 adalah peta yang sepenuhnya diperbesar. Level yang lebih tinggi memperbesar peta lebih jauh. ( referensi )
  2. Pada tingkat zoom 0 seluruh dunia dapat ditampilkan di area yang 256 x 256 piksel. ( referensi )
  3. Untuk setiap tingkat zoom yang lebih tinggi, jumlah piksel yang diperlukan untuk menampilkan area yang sama menggandakan lebar dan tinggi. ( referensi )
  4. Peta membungkus dalam arah longitudinal, tetapi tidak dalam arah latitudinal.
John S
sumber
6
jawaban yang sangat baik, ini harus menjadi yang teratas karena memberikan kontribusi untuk bujur dan lintang. Bekerja dengan sangat baik sejauh ini.
Peter Wooster
1
@ John S - Ini adalah solusi yang fantastis dan saya sedang mempertimbangkan untuk menggunakan metode fitBounds google maps asli yang tersedia untuk saya juga. Saya perhatikan fitBounds kadang-kadang satu tingkat zoom kembali (diperbesar), tapi saya menganggap itu dari padding yang ditambahkan. Apakah satu-satunya perbedaan antara metode ini dan fitBounds, hanya jumlah bantalan yang ingin Anda tambahkan akun mana untuk perubahan tingkat zoom di antara keduanya?
johntrepreneur
1
@johntrepreneur - Keuntungan # 1: Anda dapat menggunakan metode ini bahkan sebelum Anda membuat peta, sehingga Anda dapat memberikan hasilnya ke pengaturan peta awal. Dengan fitBounds, Anda harus membuat peta dan kemudian menunggu acara "bounds_changed".
John S
1
@ MarianPaździoch - Tidak berfungsi untuk batasan itu, lihat di sini . Apakah Anda berharap dapat memperbesar sehingga titik-titik itu berada di sudut yang tepat dari peta? Itu tidak mungkin karena level zoom adalah nilai integer. Fungsi mengembalikan tingkat zoom tertinggi yang masih akan mencakup seluruh batas pada peta.
John S
1
@CarlMeyer - Saya tidak menyebutkannya dalam jawaban saya, tetapi dalam komentar di atas saya menyatakan bahwa satu keuntungan dari fungsi ini adalah "Anda dapat menggunakan metode ini bahkan sebelum Anda membuat peta." Menggunakan map.getProjection()akan menghilangkan beberapa matematika (dan asumsi tentang proyeksi), tetapi itu berarti fungsi tidak dapat dipanggil sampai setelah peta dibuat dan acara "projection_changed" telah diaktifkan.
John S
39

Untuk versi 3 API, ini sederhana dan berfungsi:

var latlngList = [];
latlngList.push(new google.maps.LatLng(lat, lng));

var bounds = new google.maps.LatLngBounds();
latlngList.each(function(n) {
    bounds.extend(n);
});

map.setCenter(bounds.getCenter()); //or use custom center
map.fitBounds(bounds);

dan beberapa trik opsional:

//remove one zoom level to ensure no marker is on the edge.
map.setZoom(map.getZoom() - 1); 

// set a minimum zoom 
// if you got only 1 marker or all markers are on the same address map will be zoomed too much.
if(map.getZoom() > 15){
    map.setZoom(15);
}
d.raev
sumber
1
mengapa tidak menetapkan level zoom minimum saat menginisialisasi peta, sesuatu seperti: var mapOptions = {maxZoom: 15,};
Kush
2
@ Kus, bagus. tetapi maxZoomakan mencegah pengguna melakukan pembesaran manual . Contoh saya hanya mengubah DefaultZoom dan hanya jika perlu.
d.raev
1
ketika Anda melakukan fitBounds, itu hanya melompat agar sesuai dengan batas daripada menjiwai di sana dari tampilan saat ini. solusi yang mengagumkan adalah dengan menggunakan yang sudah disebutkan getBoundsZoomLevel. dengan begitu, ketika Anda memanggil setZoom, ia akan bergerak ke level zoom yang diinginkan. dari sana itu tidak masalah untuk melakukan panTo dan Anda berakhir dengan animasi peta yang indah yang sesuai dengan batas
user151496
animasi tidak dibahas di mana dalam pertanyaan atau di jawaban saya. Jika Anda memiliki contoh yang bermanfaat tentang topik tersebut, buat saja jawaban yang konstruktif, dengan contoh dan bagaimana serta kapan dapat digunakan.
d.raev
Untuk beberapa alasan Google map tidak memperbesar saat memanggil setZoom () segera setelah panggilan map.fitBounds (). (gmaps adalah v3.25 saat ini)
kashiraja
4

Berikut versi fungsi Kotlin:

fun getBoundsZoomLevel(bounds: LatLngBounds, mapDim: Size): Double {
        val WORLD_DIM = Size(256, 256)
        val ZOOM_MAX = 21.toDouble();

        fun latRad(lat: Double): Double {
            val sin = Math.sin(lat * Math.PI / 180);
            val radX2 = Math.log((1 + sin) / (1 - sin)) / 2;
            return max(min(radX2, Math.PI), -Math.PI) /2
        }

        fun zoom(mapPx: Int, worldPx: Int, fraction: Double): Double {
            return floor(Math.log(mapPx / worldPx / fraction) / Math.log(2.0))
        }

        val ne = bounds.northeast;
        val sw = bounds.southwest;

        val latFraction = (latRad(ne.latitude) - latRad(sw.latitude)) / Math.PI;

        val lngDiff = ne.longitude - sw.longitude;
        val lngFraction = if (lngDiff < 0) { (lngDiff + 360) } else { (lngDiff / 360) }

        val latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
        val lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);

        return minOf(latZoom, lngZoom, ZOOM_MAX)
    }
fotografi
sumber
1

Terima kasih, itu banyak membantu saya dalam menemukan faktor zoom yang paling cocok untuk menampilkan polyline dengan benar. Saya menemukan koordinat maksimum dan minimum di antara titik-titik yang harus saya lacak dan, jika jalurnya sangat "vertikal", saya hanya menambahkan beberapa baris kode:

var GLOBE_WIDTH = 256; // a constant in Google's map projection
var west = <?php echo $minLng; ?>;
var east = <?php echo $maxLng; ?>;
*var north = <?php echo $maxLat; ?>;*
*var south = <?php echo $minLat; ?>;*
var angle = east - west;
if (angle < 0) {
    angle += 360;
}
*var angle2 = north - south;*
*if (angle2 > angle) angle = angle2;*
var zoomfactor = Math.round(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2);

Sebenarnya, faktor zoom yang ideal adalah zoomfactor-1.

Valerio Giacosa
sumber
Saya suka var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Tetap saja, sangat membantu.
Mojowen
1

Karena semua jawaban lain tampaknya memiliki masalah bagi saya dengan satu atau beberapa keadaan (lebar peta / tinggi, lebar batas / tinggi, dll.) Saya pikir saya akan meletakkan jawaban saya di sini ...

Ada file javascript yang sangat berguna di sini: http://www.polyarc.us/adjust.js

Saya menggunakan itu sebagai dasar untuk ini:

var com = com || {};
com.local = com.local || {};
com.local.gmaps3 = com.local.gmaps3 || {};

com.local.gmaps3.CoordinateUtils = new function() {

   var OFFSET = 268435456;
   var RADIUS = OFFSET / Math.PI;

   /**
    * Gets the minimum zoom level that entirely contains the Lat/Lon bounding rectangle given.
    *
    * @param {google.maps.LatLngBounds} boundary the Lat/Lon bounding rectangle to be contained
    * @param {number} mapWidth the width of the map in pixels
    * @param {number} mapHeight the height of the map in pixels
    * @return {number} the minimum zoom level that entirely contains the given Lat/Lon rectangle boundary
    */
   this.getMinimumZoomLevelContainingBounds = function ( boundary, mapWidth, mapHeight ) {
      var zoomIndependentSouthWestPoint = latLonToZoomLevelIndependentPoint( boundary.getSouthWest() );
      var zoomIndependentNorthEastPoint = latLonToZoomLevelIndependentPoint( boundary.getNorthEast() );
      var zoomIndependentNorthWestPoint = { x: zoomIndependentSouthWestPoint.x, y: zoomIndependentNorthEastPoint.y };
      var zoomIndependentSouthEastPoint = { x: zoomIndependentNorthEastPoint.x, y: zoomIndependentSouthWestPoint.y };
      var zoomLevelDependentSouthEast, zoomLevelDependentNorthWest, zoomLevelWidth, zoomLevelHeight;
      for( var zoom = 21; zoom >= 0; --zoom ) {
         zoomLevelDependentSouthEast = zoomLevelIndependentPointToMapCanvasPoint( zoomIndependentSouthEastPoint, zoom );
         zoomLevelDependentNorthWest = zoomLevelIndependentPointToMapCanvasPoint( zoomIndependentNorthWestPoint, zoom );
         zoomLevelWidth = zoomLevelDependentSouthEast.x - zoomLevelDependentNorthWest.x;
         zoomLevelHeight = zoomLevelDependentSouthEast.y - zoomLevelDependentNorthWest.y;
         if( zoomLevelWidth <= mapWidth && zoomLevelHeight <= mapHeight )
            return zoom;
      }
      return 0;
   };

   function latLonToZoomLevelIndependentPoint ( latLon ) {
      return { x: lonToX( latLon.lng() ), y: latToY( latLon.lat() ) };
   }

   function zoomLevelIndependentPointToMapCanvasPoint ( point, zoomLevel ) {
      return {
         x: zoomLevelIndependentCoordinateToMapCanvasCoordinate( point.x, zoomLevel ),
         y: zoomLevelIndependentCoordinateToMapCanvasCoordinate( point.y, zoomLevel )
      };
   }

   function zoomLevelIndependentCoordinateToMapCanvasCoordinate ( coordinate, zoomLevel ) {
      return coordinate >> ( 21 - zoomLevel );
   }

   function latToY ( lat ) {
      return OFFSET - RADIUS * Math.log( ( 1 + Math.sin( lat * Math.PI / 180 ) ) / ( 1 - Math.sin( lat * Math.PI / 180 ) ) ) / 2;
   }

   function lonToX ( lon ) {
      return OFFSET + RADIUS * lon * Math.PI / 180;
   }

};

Anda tentu dapat membersihkan ini atau memperkecil jika diperlukan, tapi saya menyimpan nama variabel lama dalam upaya untuk membuatnya lebih mudah untuk dipahami.

Jika Anda bertanya-tanya dari mana OFFSET berasal, tampaknya 268435456 adalah setengah dari keliling bumi dalam piksel pada level zoom 21 (menurut http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google -maps ).

Manusia bayangan
sumber
0

Valerio hampir benar dengan solusinya, tetapi ada beberapa kesalahan logis.

Anda harus terlebih dahulu memeriksa apakah sudut2 lebih besar dari sudut, sebelum menambahkan 360 pada negatif.

jika tidak, Anda selalu memiliki nilai lebih besar dari sudut

Jadi solusi yang benar adalah:

var west = calculateMin(data.longitudes);
var east = calculateMax(data.longitudes);
var angle = east - west;
var north = calculateMax(data.latitudes);
var south = calculateMin(data.latitudes);
var angle2 = north - south;
var zoomfactor;
var delta = 0;
var horizontal = false;

if(angle2 > angle) {
    angle = angle2;
    delta = 3;
}

if (angle < 0) {
    angle += 360;
}

zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2) - 2 - delta;

Delta ada di sana, karena saya memiliki lebar lebih besar daripada tinggi.

Lukas Olsen
sumber
0

map.getBounds()bukan operasi sesaat, jadi saya gunakan dalam event handler kasus serupa. Ini contoh saya di Coffeescript

@map.fitBounds(@bounds)
google.maps.event.addListenerOnce @map, 'bounds_changed', =>
  @map.setZoom(12) if @map.getZoom() > 12
MikDiet
sumber
0

Contoh kerja untuk menemukan pusat default rata-rata dengan react-google-maps di ES6:

const bounds = new google.maps.LatLngBounds();
paths.map((latLng) => bounds.extend(new google.maps.LatLng(latLng)));
const defaultCenter = bounds.getCenter();
<GoogleMap
 defaultZoom={paths.length ? 12 : 4}
 defaultCenter={defaultCenter}
>
 <Marker position={{ lat, lng }} />
</GoogleMap>
Gapur Kassym
sumber
0

Perhitungan tingkat zoom untuk garis bujur Giles Gardam bekerja dengan baik untuk saya. Jika Anda ingin menghitung faktor zoom untuk garis lintang, ini adalah solusi mudah yang berfungsi dengan baik:

double minLat = ...;
double maxLat = ...;
double midAngle = (maxLat+minLat)/2;
//alpha is the non-negative angle distance of alpha and beta to midangle
double alpha  = maxLat-midAngle;
//Projection screen is orthogonal to vector with angle midAngle
//portion of horizontal scale:
double yPortion = Math.sin(alpha*Math.pi/180) / 2;
double latZoom = Math.log(mapSize.height / GLOBE_WIDTH / yPortion) / Math.ln2;

//return min (max zoom) of both zoom levels
double zoom = Math.min(lngZoom, latZoom);
Andre
sumber