Google Maps V3: cómo calcular el nivel de zoom para un límite determinado

138

Estoy buscando una manera de calcular el nivel de zoom para un límite determinado utilizando la API de Google Maps V3, similar a getBoundsZoomLevel()la API de V2.

Esto es lo que quiero hacer:

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

Desafortunadamente, no puedo usar el fitBounds()método para mi caso de uso particular. Funciona bien para ajustar marcadores en el mapa, pero no funciona bien para establecer límites exactos. Aquí hay un ejemplo de por qué no puedo usar el fitBounds()método.

map.fitBounds(map.getBounds()); // not what you expect
Nick Clark
fuente
44
¡El último ejemplo es excelente y muy ilustrativo! +1. Tengo el mismo problema .
TMS
Lo siento, pregunta incorrecta vinculada, este es el enlace correcto .
TMS
Esta pregunta no es un duplicado de la otra pregunta . La respuesta a la otra pregunta es usar fitBounds(). Esta pregunta pregunta qué hacer cuando fitBounds()es insuficiente, ya sea porque se sobrepasa o no desea hacer zoom (es decir, solo quiere el nivel de zoom).
John S
@Nick Clark: ¿Cómo sabes los límites que se establecerán? ¿Cómo los capturaste antes?
varaprakash

Respuestas:

108

Se ha hecho una pregunta similar en el grupo de Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Los niveles de zoom son discretos, y la escala se duplica en cada paso. Por lo tanto, en general, no puede ajustarse exactamente a los límites que desea (a menos que tenga mucha suerte con el tamaño del mapa en particular).

Otro problema es la relación entre las longitudes de los lados, por ejemplo, no puede ajustar los límites exactamente a un rectángulo delgado dentro de un mapa cuadrado.

No hay una respuesta fácil sobre cómo ajustar los límites exactos, porque incluso si está dispuesto a cambiar el tamaño de la división del mapa, debe elegir a qué tamaño y nivel de zoom correspondiente cambiará (en términos generales, ¿lo hace más grande o más pequeño? de lo que es actualmente?

Si realmente necesita calcular el zoom, en lugar de almacenarlo, esto debería ser el truco:

La proyección de Mercator deforma la latitud, pero cualquier diferencia en longitud siempre representa la misma fracción del ancho del mapa (la diferencia de ángulo en grados / 360). En el zoom cero, el mapa mundial completo es de 256x256 píxeles, y el zoom de cada nivel duplica tanto el ancho como la altura. Entonces, después de un poco de álgebra, podemos calcular el zoom de la siguiente manera, siempre que conozcamos el ancho del mapa en píxeles. Tenga en cuenta que debido a que la longitud se envuelve, debemos asegurarnos de que el ángulo sea 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);
Giles Gardam
fuente
1
¿No tendrías que repetir esto para el lat y luego elegir el mínimo del resultado del 2? No creo que esto iba a funcionar durante unos estrechos límites altos .....
whiteatom
3
Funciona muy bien para mí con un cambio de Math.round a Math.floor. Un millón de gracias.
Pete
3
¿Cómo puede ser esto correcto si no tiene en cuenta la latitud? ¡Cerca del ecuador debería estar bien, pero la escala del mapa en un nivel de zoom dado cambia según la latitud!
Eyal
1
@Pete buen punto, en general probablemente desee redondear el nivel de zoom para que se ajuste un poco más de lo deseado en el mapa, en lugar de un poco menos. Usé Math.round porque en la situación del OP el valor antes del redondeo debería ser aproximadamente integral.
Giles Gardam
20
¿Cuál es el valor de pixelWidth?
Albert Jegani
279

Gracias a Giles Gardam por su respuesta, pero solo aborda la longitud y no la latitud. Una solución completa debe calcular el nivel de zoom necesario para la latitud y el nivel de zoom necesario para la longitud, y luego tomar el más pequeño (más alejado) de los dos.

Aquí hay una función que usa tanto latitud como longitud:

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

Parámetros:

El valor del parámetro "límites" debe ser un google.maps.LatLngBoundsobjeto.

El valor del parámetro "mapDim" debe ser un objeto con propiedades de "altura" y "ancho" que representen la altura y el ancho del elemento DOM que muestra el mapa. Es posible que desee disminuir estos valores si desea garantizar el relleno. Es decir, es posible que no desee que los marcadores de mapa dentro de los límites estén demasiado cerca del borde del mapa.

Si está utilizando la biblioteca jQuery, el mapDimvalor se puede obtener de la siguiente manera:

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

Si está utilizando la biblioteca Prototype, el valor de mapDim se puede obtener de la siguiente manera:

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

Valor de retorno:

El valor de retorno es el nivel de zoom máximo que aún mostrará los límites completos. Este valor estará entre 0y el nivel máximo de zoom, inclusive.

El nivel máximo de zoom es 21. (Creo que fue solo 19 para la API de Google Maps v2).


Explicación:

Google Maps utiliza una proyección de Mercator. En una proyección de Mercator, las líneas de longitud están igualmente espaciadas, pero las líneas de latitud no. La distancia entre las líneas de latitud aumenta a medida que van desde el ecuador hasta los polos. De hecho, la distancia tiende hacia el infinito cuando llega a los polos. Sin embargo, un mapa de Google Maps no muestra latitudes superiores a aproximadamente 85 grados norte o inferiores a aproximadamente -85 grados sur. ( referencia ) (Calculo el límite real a +/- 85.05112877980658 grados).

Esto hace que el cálculo de las fracciones para los límites sea más complicado para la latitud que para la longitud. Usé una fórmula de Wikipedia para calcular la fracción de latitud. Supongo que esto coincide con la proyección utilizada por Google Maps. Después de todo, la página de documentación de Google Maps que enlazo arriba contiene un enlace a la misma página de Wikipedia.

Otras notas:

  1. Los niveles de zoom van desde 0 hasta el nivel máximo de zoom. El nivel de zoom 0 es el mapa totalmente alejado. Los niveles más altos amplían el mapa aún más. ( referencia )
  2. En el nivel de zoom 0, el mundo entero se puede mostrar en un área de 256 x 256 píxeles. ( referencia )
  3. Para cada nivel de zoom más alto, el número de píxeles necesarios para mostrar la misma área se duplica tanto en ancho como en alto. ( referencia )
  4. Los mapas se envuelven en la dirección longitudinal, pero no en la dirección latitudinal.
John S
fuente
66
excelente respuesta, esta debería ser la mejor votada, ya que representa tanto la longitud como la latitud. Funcionó perfectamente hasta ahora.
Peter Wooster,
1
@John S: esta es una solución fantástica y estoy contemplando usar esto sobre el método nativo de Google Maps fitBounds disponible para mí también. Noté que fitBounds a veces tiene un nivel de zoom hacia atrás (alejado), pero supongo que es por el relleno que está agregando. ¿Es la única diferencia entre este y el método fitBounds, solo la cantidad de relleno que desea agregar que explica el cambio en el nivel de zoom entre los dos?
johntrepreneur
1
@johntrepreneur - Ventaja # 1: puede usar este método incluso antes de crear el mapa, para que pueda proporcionar su resultado a la configuración inicial del mapa. Con fitBounds, debe crear el mapa y luego esperar el evento "limits_changed".
John S
1
@ MarianPaździoch - Funciona para esos límites, mira aquí . ¿Espera poder hacer zoom para que esos puntos estén en las esquinas exactas del mapa? Eso no es posible porque los niveles de zoom son valores enteros. La función devuelve el nivel de zoom más alto que aún incluirá todos los límites en el mapa.
John S
1
@CarlMeyer: no lo menciono en mi respuesta, pero en un comentario anterior afirmo que una ventaja de esta función es "Puede usar este método incluso antes de crear el mapa". El uso map.getProjection()eliminaría algunas de las matemáticas (y la suposición sobre la proyección), pero significaría que la función no se podría invocar hasta después de que se haya creado el mapa y se haya activado el evento "projection_changed".
John S
39

Para la versión 3 de la API, esto es simple y funciona:

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

y algunos trucos opcionales:

//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
fuente
1
¿por qué no establecer un nivel de zoom mínimo al inicializar el mapa, algo así como: var mapOptions = {maxZoom: 15,};
Kush
2
@ Kush, buen punto. pero maxZoomevitará que el usuario haga zoom manual . Mi ejemplo solo cambia el DefaultZoom y solo si es necesario.
d.raev
1
cuando haces fitBounds, simplemente salta para ajustarse a los límites en lugar de animar allí desde la vista actual. La solución impresionante es mediante el uso ya mencionado getBoundsZoomLevel. de esa manera, cuando llamas a setZoom, se anima al nivel de zoom deseado. de allí que no es un problema para hacer el panTo y se termina con un mapa animación hermosa que se ajuste a los límites
user151496
La animación no se discute en la pregunta ni en mi respuesta. Si tiene un ejemplo útil sobre el tema, simplemente cree una respuesta constructiva, con un ejemplo y cómo y cuándo se puede usar.
d.raev
Por alguna razón, el mapa de Google no se amplía cuando se llama a setZoom () inmediatamente después de la llamada a map.fitBounds (). (Gmaps es v3.25 actualmente)
kashiraja
4

Aquí una versión de Kotlin de la función:

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)
    }
fotograbado
fuente
1

Gracias, eso me ayudó mucho a encontrar el factor de zoom más adecuado para mostrar correctamente una polilínea. Encuentro las coordenadas máximas y mínimas entre los puntos que tengo que rastrear y, en caso de que la ruta sea muy "vertical", solo agregué algunas líneas de código:

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 realidad, el factor de zoom ideal es zoomfactor-1.

Valerio Giacosa
fuente
Me ha gustado var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Aún así, muy útil.
Mojowen
1

Como todas las otras respuestas parecen tener problemas para mí con una u otra serie de circunstancias (ancho / alto del mapa, ancho / alto de los límites, etc.), pensé que pondría mi respuesta aquí ...

Había un archivo javascript muy útil aquí: http://www.polyarc.us/adjust.js

Lo usé como base para esto:

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

};

Ciertamente puede limpiar esto o minimizarlo si es necesario, pero mantuve los nombres de las variables por mucho tiempo en un intento de que sea más fácil de entender.

Si se pregunta de dónde proviene OFFSET, aparentemente 268435456 es la mitad de la circunferencia de la Tierra en píxeles en el nivel de zoom 21 (según http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google -mapas ).

Hombre de la sombra
fuente
0

Valerio tiene casi razón con su solución, pero hay un error lógico.

primero debe verificar si el ángulo2 es mayor que el ángulo, antes de agregar 360 en negativo.

de lo contrario, siempre tiene un valor mayor que el ángulo

Entonces la solución correcta es:

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á allí, porque tengo un ancho mayor que la altura.

Lukas Olsen
fuente
0

map.getBounds()No es una operación momentánea, por lo que utilizo un controlador de eventos de caso similar. Aquí está mi ejemplo en Coffeescript

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

Ejemplo de trabajo para encontrar el centro predeterminado promedio con react-google-maps en 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
fuente
0

El cálculo del nivel de zoom para las longitudes de Giles Gardam funciona bien para mí. Si desea calcular el factor de zoom para la latitud, esta es una solución fácil que funciona 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);
Andre
fuente