Google Maps V3 - Comment calculer le niveau de zoom pour une limite donnée

138

Je cherche un moyen de calculer le niveau de zoom pour une limite donnée à l'aide de l'API Google Maps V3, similaire à getBoundsZoomLevel()l'API V2.

Voici ce que je veux faire:

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

Malheureusement, je ne peux pas utiliser la fitBounds()méthode pour mon cas d'utilisation particulier. Cela fonctionne bien pour ajuster les marqueurs sur la carte, mais cela ne fonctionne pas bien pour définir des limites exactes. Voici un exemple des raisons pour lesquelles je ne peux pas utiliser la fitBounds()méthode.

map.fitBounds(map.getBounds()); // not what you expect
Nick Clark
la source
4
Le dernier exemple est excellent et très illustratif! +1. J'ai le même problème .
TMS
Désolé, mauvaise question liée, c'est le bon lien .
TMS
Cette question n'est pas une duplication de l'autre question . La réponse à l'autre question est d'utiliser fitBounds(). Cette question demande ce qu'il faut faire lorsque cela fitBounds()est insuffisant - soit parce que vous effectuez un zoom avant ou que vous ne voulez pas zoomer (c'est-à-dire que vous voulez juste le niveau de zoom).
John S
@Nick Clark: Comment connaissez-vous les limites sw, ne à définir? Comment les avez-vous capturés avant?
varaprakash

Réponses:

108

Une question similaire a été posée sur le groupe Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Les niveaux de zoom sont discrets, l'échelle doublant à chaque étape. Donc, en général, vous ne pouvez pas ajuster les limites que vous voulez exactement (sauf si vous êtes très chanceux avec la taille de la carte particulière).

Un autre problème est le rapport entre les longueurs de côté, par exemple vous ne pouvez pas ajuster les limites exactement à un rectangle fin à l'intérieur d'une carte carrée.

Il n'y a pas de réponse facile pour savoir comment ajuster les limites exactes, car même si vous êtes prêt à modifier la taille du div de la carte, vous devez choisir la taille et le niveau de zoom correspondant auquel vous changez (grosso modo, l'agrandissez-vous ou le réduisez-vous qu'il ne l'est actuellement?).

Si vous avez vraiment besoin de calculer le zoom, plutôt que de le stocker, cela devrait faire l'affaire:

La projection Mercator déforme la latitude, mais toute différence de longitude représente toujours la même fraction de la largeur de la carte (la différence d'angle en degrés / 360). Au zoom zéro, la carte du monde entier mesure 256x256 pixels et le zoom à chaque niveau double à la fois la largeur et la hauteur. Ainsi, après un peu d'algèbre, nous pouvons calculer le zoom comme suit, à condition de connaître la largeur de la carte en pixels. Notez que parce que la longitude s'enroule, nous devons nous assurer que l'angle est 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
la source
1
N'auriez-vous pas à répéter cela pour la lat et ensuite choisir le min du résultat du 2? Je ne pense pas que cela fonctionnerait pour une grande limite étroite .....
whiteatom
3
Fonctionne très bien pour moi avec un changement de Math.round à Math.floor. Mille mercis.
Pete
3
Comment cela peut-il être juste si cela ne tient pas compte de la latitude? Près de l'équateur, ça devrait aller mais l'échelle de la carte à un niveau de zoom donné change en fonction de la latitude!
Eyal
1
@Pete Bon point, en général, vous voudrez probablement arrondir le niveau de zoom pour que vous teniez un peu plus que souhaité dans la carte, plutôt qu'un peu moins. J'ai utilisé Math.round car dans la situation de l'OP, la valeur avant arrondi doit être approximativement intégrale.
Giles Gardam
20
quelle est la valeur de pixelWidth
albert Jegani
279

Merci à Giles Gardam pour sa réponse, mais elle ne concerne que la longitude et non la latitude. Une solution complète devrait calculer le niveau de zoom nécessaire pour la latitude et le niveau de zoom nécessaire pour la longitude, puis prendre le plus petit (le plus éloigné) des deux.

Voici une fonction qui utilise à la fois la latitude et la longitude:

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

Paramètres:

La valeur du paramètre «limites» doit être un google.maps.LatLngBoundsobjet.

La valeur du paramètre "mapDim" doit être un objet avec des propriétés "height" et "width" qui représentent la hauteur et la largeur de l'élément DOM qui affiche la carte. Vous souhaiterez peut-être diminuer ces valeurs si vous souhaitez garantir un remplissage. Autrement dit, vous ne voudrez peut-être pas que les marqueurs de carte dans les limites soient trop proches du bord de la carte.

Si vous utilisez la bibliothèque jQuery, la mapDimvaleur peut être obtenue comme suit:

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

Si vous utilisez la bibliothèque Prototype, la valeur mapDim peut être obtenue comme suit:

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

Valeur de retour:

La valeur de retour est le niveau de zoom maximum qui affichera toujours toutes les limites. Cette valeur sera comprise entre 0et le niveau de zoom maximum, inclus.

Le niveau de zoom maximal est de 21. (je crois qu'il n'était que de 19 pour l'API Google Maps v2.)


Explication:

Google Maps utilise une projection Mercator. Dans une projection Mercator, les lignes de longitude sont également espacées, mais les lignes de latitude ne le sont pas. La distance entre les lignes de latitude augmente au fur et à mesure qu'elles vont de l'équateur aux pôles. En fait, la distance tend vers l'infini lorsqu'elle atteint les pôles. Une carte Google Maps, cependant, n'indique pas les latitudes au-dessus d'environ 85 degrés nord ou en dessous d'environ -85 degrés sud. ( référence ) (Je calcule le seuil réel à +/- 85,05112877980658 degrés.)

Cela rend le calcul des fractions pour les limites plus compliqué pour la latitude que pour la longitude. J'ai utilisé une formule de Wikipedia pour calculer la fraction de latitude. Je suppose que cela correspond à la projection utilisée par Google Maps. Après tout, la page de documentation Google Maps vers laquelle je renvoie ci-dessus contient un lien vers la même page Wikipédia.

Autres notes:

  1. Les niveaux de zoom vont de 0 au niveau de zoom maximum. Le niveau de zoom 0 est la carte entièrement agrandie. Des niveaux plus élevés agrandissent davantage la carte. ( référence )
  2. Au niveau de zoom 0, le monde entier peut être affiché dans une zone de 256 x 256 pixels. ( référence )
  3. Pour chaque niveau de zoom supérieur, le nombre de pixels nécessaires pour afficher la même zone double à la fois en largeur et en hauteur. ( référence )
  4. Les cartes s'enroulent dans le sens longitudinal, mais pas dans le sens latitudinal.
John S
la source
6
excellente réponse, cela devrait être le plus voté car il tient compte à la fois de la longitude et de la latitude. A parfaitement fonctionné jusqu'à présent.
Peter Wooster
1
@John S - C'est une solution fantastique et j'envisage de l'utiliser sur la méthode native fitBounds de google maps à ma disposition également. J'ai remarqué que fitBounds est parfois un niveau de zoom arrière (zoom arrière), mais je suppose que cela provient du rembourrage qu'il ajoute. La seule différence entre cette méthode et la méthode fitBounds est-elle simplement la quantité de remplissage que vous souhaitez ajouter qui explique le changement de niveau de zoom entre les deux?
johntrepreneur
1
@johntrepreneur - Avantage n ° 1: Vous pouvez utiliser cette méthode avant même de créer la carte, afin de pouvoir fournir son résultat aux paramètres de carte initiaux. Avec fitBounds, vous devez créer la carte, puis attendre l'événement "bounds_changed".
John S
1
@ MarianPaździoch - Cela fonctionne pour ces limites, voir ici . Vous attendez-vous à pouvoir zoomer pour que ces points soient exactement aux coins de la carte? Cela n'est pas possible car les niveaux de zoom sont des valeurs entières. La fonction renvoie le niveau de zoom le plus élevé qui inclura toujours toutes les limites de la carte.
John S
1
@CarlMeyer - Je ne le mentionne pas dans ma réponse, mais dans un commentaire ci-dessus, j'affirme qu'un avantage de cette fonction est "Vous pouvez utiliser cette méthode avant même de créer la carte." Utiliser map.getProjection()éliminerait une partie du calcul (et l'hypothèse sur la projection), mais cela signifierait que la fonction ne pourrait pas être appelée tant que la carte n'a pas été créée et que l'événement "projection_changed" s'est déclenché.
John S
39

Pour la version 3 de l'API, c'est simple et fonctionnel:

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

et quelques astuces optionnelles:

//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
la source
1
pourquoi ne pas définir un niveau de zoom minimum lors de l'initialisation de la carte, quelque chose comme: var mapOptions = {maxZoom: 15,};
Kush
2
@Kush, bon point. mais maxZoomempêchera l'utilisateur de zoomer manuellement . Mon exemple ne modifie le DefaultZoom que si cela est nécessaire.
d.raev
1
lorsque vous effectuez fitBounds, il saute simplement pour s'adapter aux limites au lieu de l'animer à partir de la vue actuelle. la solution géniale consiste à utiliser déjà mentionné getBoundsZoomLevel. de cette façon, lorsque vous appelez setZoom, il s'anime au niveau de zoom souhaité. à partir de là, ce n'est pas un problème de faire le panoramique et vous vous retrouvez avec une belle animation cartographique qui correspond aux limites
user151496
l'animation n'est pas discutée dans la question ni dans ma réponse. Si vous avez un exemple utile sur le sujet, créez simplement une réponse constructive, avec un exemple et comment et quand il peut être utilisé.
d.raev
Pour une raison quelconque, Google map ne fait pas de zoom lors de l'appel de setZoom () immédiatement après l'appel de map.fitBounds (). (gmaps est actuellement v3.25)
kashiraja
4

Voici une version Kotlin de la fonction:

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)
    }
photograve
la source
1

Merci, cela m'a beaucoup aidé à trouver le facteur de zoom le plus approprié pour afficher correctement une polyligne. Je trouve les coordonnées maximum et minimum parmi les points que je dois suivre et, dans le cas où le chemin est très "vertical", je viens d'ajouter quelques lignes de code:

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

En fait, le facteur de zoom idéal est le zoomfactor-1.

Valerio Giacosa
la source
J'ai bien aimé var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Encore, très utile.
Mojowen
1

Étant donné que toutes les autres réponses semblent avoir des problèmes pour moi avec l'une ou l'autre des circonstances (largeur / hauteur de la carte, largeur / hauteur des limites, etc.), j'ai pensé que je mettrais ma réponse ici ...

Il y avait un fichier javascript très utile ici: http://www.polyarc.us/adjust.js

Je l'ai utilisé comme base pour ceci:

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

};

Vous pouvez certainement le nettoyer ou le réduire si nécessaire, mais j'ai gardé les noms de variables longtemps pour essayer de le rendre plus facile à comprendre.

Si vous vous demandez d'où vient OFFSET, apparemment 268435456 est la moitié de la circonférence de la Terre en pixels au niveau de zoom 21 (selon http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google -maps ).

Homme de l'ombre
la source
0

Valerio a presque raison avec sa solution, mais il y a une erreur logique.

vous devez d'abord vérifier si l'angle2 est plus grand que l'angle, avant d'ajouter 360 à un négatif.

sinon vous avez toujours une valeur plus grande que l'angle

La bonne solution est donc:

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 est là, parce que j'ai une largeur plus grande que la hauteur.

Lukas Olsen
la source
0

map.getBounds()n'est pas une opération momentanée, donc j'utilise dans le même cas un gestionnaire d'événements. Voici mon exemple dans Coffeescript

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

Exemple de travail pour trouver un centre par défaut moyen avec react-google-maps sur 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
la source
0

Le calcul du niveau de zoom pour les longitudes de Giles Gardam fonctionne très bien pour moi. Si vous souhaitez calculer le facteur de zoom pour la latitude, c'est une solution simple qui fonctionne bien:

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);
André
la source