Google Maps V3 - Come calcolare il livello di zoom per un dato limite


138

Sto cercando un modo per calcolare il livello di zoom per un determinato limite utilizzando l'API V3 di Google Maps, simile a getBoundsZoomLevel()quella dell'API V2.

Ecco cosa voglio fare:

// 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

Sfortunatamente, non posso usare il fitBounds()metodo per il mio caso d'uso particolare. Funziona bene per adattare gli indicatori sulla mappa, ma non funziona bene per impostare limiti esatti. Ecco un esempio del perché non riesco a utilizzare il fitBounds()metodo.

map.fitBounds(map.getBounds()); // not what you expect

4
L'ultimo esempio è eccellente e molto illustrativo! +1. Ho la stesso problema .
TMS

Siamo spiacenti, la domanda sbagliata è collegata, questo è il collegamento corretto .
TMS

Questa domanda non è un duplicato dell'altra domanda . La risposta all'altra domanda è usare fitBounds(). Questa domanda chiede cosa fare quando fitBounds()è insufficiente, sia perché si ingrandisce o non si desidera eseguire lo zoom (ovvero, si desidera solo il livello di zoom).
John S,

@ Nick Clark: come fai a sapere i limiti di sw, ne da impostare? Come li hai catturati prima?
varaprakash,

Risposte:


108

Una domanda simile è stata posta sul gruppo Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

I livelli di zoom sono discreti, con la scala che raddoppia in ogni passaggio. Quindi, in generale, non puoi adattare esattamente i limiti che vuoi (a meno che tu non sia molto fortunato con la dimensione della mappa particolare).

Un altro problema è il rapporto tra le lunghezze laterali, ad esempio non è possibile adattare i limiti esattamente a un rettangolo sottile all'interno di una mappa quadrata.

Non c'è una risposta facile su come adattare i limiti esatti, perché anche se sei disposto a cambiare la dimensione del div della mappa, devi scegliere a quale dimensione e livello di zoom corrispondente cambi (approssimativamente parlando, la ingrandisci o riduci) di quanto non sia attualmente?).

Se hai davvero bisogno di calcolare lo zoom, piuttosto che memorizzarlo, questo dovrebbe fare il trucco:

La proiezione di Mercatore deforma la latitudine, ma qualsiasi differenza di longitudine rappresenta sempre la stessa frazione della larghezza della mappa (la differenza di angolo in gradi / 360). A zoom zero, l'intera mappa del mondo è 256x256 pixel e lo zoom di ogni livello raddoppia sia la larghezza che l'altezza. Quindi dopo una piccola algebra possiamo calcolare lo zoom come segue, purché conosciamo la larghezza della mappa in pixel. Si noti che poiché la longitudine si avvolge, dobbiamo assicurarci che l'angolo sia positivo.

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);

1
Non dovresti ripetere questo per il lat e quindi scegliere il minimo del risultato del 2? Non penso che funzionerebbe per limiti alti e stretti .....
con il

3
Funziona benissimo per me con un cambio di Math.round in Math.floor. Grazie mille.
Pete,

3
Come può essere giusto se non tiene conto della latitudine? Vicino all'equatore dovrebbe andare bene, ma la scala della mappa ad un dato livello di zoom cambia a seconda della latitudine!
Eyal,

1
@Pete un buon punto, in generale probabilmente vorrai arrotondare il livello di zoom in modo da adattarti un po 'più di quanto desiderato nella mappa, piuttosto che un po' meno. Ho usato Math.round perché nella situazione del PO il valore prima dell'arrotondamento dovrebbe essere approssimativamente integrale.
Giles Gardam,

20
qual è il valore di pixelWidth
albert Jegani,

279

Grazie a Giles Gardam per la sua risposta, ma si rivolge solo alla longitudine e non alla latitudine. Una soluzione completa dovrebbe calcolare il livello di zoom necessario per la latitudine e il livello di zoom necessario per la longitudine, quindi prendere il più piccolo (più lontano) dei due.

Ecco una funzione che utilizza sia la latitudine che la longitudine:

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

parametri:

Il valore del parametro "limiti" dovrebbe essere un google.maps.LatLngBoundsoggetto.

Il valore del parametro "mapDim" dovrebbe essere un oggetto con proprietà "height" e "width" che rappresentano l'altezza e la larghezza dell'elemento DOM che visualizza la mappa. È possibile che si desideri ridurre questi valori se si desidera garantire il riempimento. Cioè, potresti non voler che gli indicatori di mappa all'interno dei limiti siano troppo vicini al bordo della mappa.

Se si utilizza la libreria jQuery, il mapDimvalore può essere ottenuto come segue:

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

Se si utilizza la libreria Prototype, il valore mapDim può essere ottenuto come segue:

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

Valore di ritorno:

Il valore di ritorno è il livello massimo di zoom che mostrerà comunque tutti i limiti. Questo valore sarà compreso tra 0e il livello massimo di zoom, incluso.

Il livello massimo di zoom è 21. (credo che fosse solo 19 per l'API di Google Maps v2.)


Spiegazione:

Google Maps utilizza una proiezione Mercator. In una proiezione di Mercatore le linee di longitudine sono equidistanti, ma non le linee di latitudine. La distanza tra le linee di latitudine aumenta man mano che si passa dall'equatore ai poli. In effetti la distanza tende all'infinito quando raggiunge i poli. Una mappa di Google Maps, tuttavia, non mostra latitudini superiori a circa 85 gradi a nord o inferiori a circa -85 gradi a sud. ( riferimento ) (calcolo il taglio effettivo a +/- 85,05112877980658 gradi.)

Questo rende il calcolo delle frazioni per i limiti più complicato per la latitudine che per la longitudine. Ho usato una formula di Wikipedia per calcolare la frazione di latitudine. Suppongo che corrisponda alla proiezione utilizzata da Google Maps. Dopotutto, la pagina della documentazione di Google Maps che collego sopra contiene un collegamento alla stessa pagina di Wikipedia.

Altre note:

  1. I livelli di zoom vanno da 0 al livello massimo di zoom. Il livello di zoom 0 è la mappa completamente ridotta. Livelli più alti ingrandiscono ulteriormente la mappa. ( riferimento )
  2. A livello di zoom 0 l'intero mondo può essere visualizzato in un'area di 256 x 256 pixel. ( riferimento )
  3. Per ogni livello di zoom più elevato il numero di pixel necessari per visualizzare la stessa area raddoppia sia in larghezza che in altezza. ( riferimento )
  4. Le mappe si avvolgono nella direzione longitudinale, ma non nella direzione latitudinale.

6
risposta eccellente, questo dovrebbe essere il massimo votato in quanto rappresenta sia la longitudine che la latitudine. Ha funzionato perfettamente finora.
Peter Wooster,

1
@John S - Questa è una soluzione fantastica e sto pensando di usarla sul metodo fitBounds nativo di google maps disponibile anche per me. Ho notato che fitBounds a volte ha un livello di zoom indietro (ingrandito), ma suppongo che sia dall'imbottitura che sta aggiungendo. L'unica differenza è quindi tra questo e il metodo fitBounds, solo la quantità di riempimento che si desidera aggiungere quali conti per la modifica del livello di zoom tra i due?
johntrepreneur,

1
@johntrepreneur - Vantaggio n. 1: è possibile utilizzare questo metodo prima ancora di creare la mappa, in modo da poter fornire il risultato alle impostazioni iniziali della mappa. Con fitBounds, è necessario creare la mappa e quindi attendere l'evento "bounds_changed".
John S,

1
@ MarianPaździoch - Funziona per quei limiti, vedi qui . Ti aspetti di poter eseguire lo zoom in modo che quei punti si trovino negli angoli esatti della mappa? Ciò non è possibile perché i livelli di zoom sono valori interi. La funzione restituisce il livello di zoom più alto che includerà comunque tutti i limiti sulla mappa.
John S,

1
@CarlMeyer - Non lo menziono nella mia risposta, ma in un commento sopra dichiaro che un vantaggio di questa funzione è "Puoi usare questo metodo prima ancora di creare la mappa". L'uso map.getProjection()eliminerebbe parte della matematica (e il presupposto sulla proiezione), ma significherebbe che la funzione non può essere chiamata fino a quando la mappa non è stata creata e l'evento "projection_changed" è stato attivato.
John S,

39

Per la versione 3 dell'API, questo è semplice e funzionante:

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);

e alcuni trucchi opzionali:

//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);
}

1
perché non impostare un livello di zoom minimo durante l'inizializzazione della mappa, qualcosa del tipo: var mapOptions = {maxZoom: 15,};
Kush,

2
@Kush, buon punto. ma maxZoomimpedirà all'utente di eseguire lo zoom manuale . Il mio esempio cambia solo DefaultZoom e solo se è necessario.
d.raev,

1
quando si esegue fitBounds, salta semplicemente per adattarsi ai limiti invece di animare lì dalla vista corrente. la soluzione fantastica è usando già menzionato getBoundsZoomLevel. in questo modo, quando chiami setZoom si anima al livello di zoom desiderato. da lì non è un problema fare il panTo e si finisce con una bella animazione della mappa che si adatta ai limiti
user151496

l'animazione non è discussa nella domanda né nella mia risposta. Se hai un esempio utile sull'argomento, basta creare una risposta costruttiva, con esempio e come e quando può essere utilizzata.
d.raev,

Per qualche motivo, Google map non esegue lo zoom quando si chiama setZoom () immediatamente dopo la chiamata map.fitBounds (). (gmaps è attualmente la v3.25)
kashiraja,

4

Ecco una versione di Kotlin della funzione:

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)
    }

1

Grazie, ciò mi ha aiutato molto a trovare il fattore di zoom più adatto per visualizzare correttamente una polilinea. Trovo le coordinate massima e minima tra i punti che devo tracciare e, nel caso in cui il percorso sia molto "verticale", ho appena aggiunto alcune righe di codice:

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);

In realtà, il fattore di zoom ideale è zoomfactor-1.


Mi è piaciuto var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Comunque, molto utile.
Mojowen,

1

Dal momento che tutte le altre risposte sembrano avere problemi per me con una o un'altra serie di circostanze (larghezza / altezza della mappa, larghezza / altezza dei limiti, ecc.) Ho pensato di mettere la mia risposta qui ...

C'era un file javascript molto utile qui: http://www.polyarc.us/adjust.js

L'ho usato come base per questo:

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;
   }

};

Puoi certamente ripulirlo o minimizzarlo se necessario, ma ho mantenuto a lungo i nomi delle variabili nel tentativo di renderlo più facile da capire.

Se ti stai chiedendo da dove proviene OFFSET, apparentemente 268435456 è metà della circonferenza terrestre in pixel al livello di zoom 21 (secondo http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google -maps ).


0

Valerio ha quasi ragione con la sua soluzione, ma c'è qualche errore logico.

devi prima verificare che l'angolo 2 sia maggiore dell'angolo, prima di aggiungere 360 ​​in negativo.

altrimenti hai sempre un valore maggiore dell'angolo

Quindi la soluzione corretta è:

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 è lì, perché ho una larghezza maggiore dell'altezza.


0

map.getBounds()non è un'operazione momentanea, quindi uso in un simile gestore di eventi caso. Ecco il mio esempio in Coffeescript

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

0

Esempio di lavoro per trovare il centro predefinito medio con reattivo-google-maps su 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>

0

Il calcolo del livello di zoom per le lunghezze di Giles Gardam funziona bene per me. Se vuoi calcolare il fattore di zoom per la latitudine, questa è una soluzione facile che funziona bene:

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);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.