Google Maps V3 - Como calcular o nível de zoom para um determinado limite

138

Estou procurando uma maneira de calcular o nível de zoom para um determinado limite usando a API V3 do Google Maps, semelhante à getBoundsZoomLevel()API V2.

Aqui está o que eu quero fazer:

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

Infelizmente, não posso usar o fitBounds()método para meu caso de uso específico. Ele funciona bem para ajustar marcadores no mapa, mas não funciona bem para definir limites exatos. Aqui está um exemplo de por que não posso usar o fitBounds()método

map.fitBounds(map.getBounds()); // not what you expect
Nick Clark
fonte
4
O último exemplo é excelente e muito ilustrativo! +1. Eu tenho o mesmo problema .
TMS
Desculpe, questão incorreta vinculada, este é o link correto .
TMS
Esta pergunta não é uma duplicata da outra pergunta . A resposta para a outra pergunta é usar fitBounds(). Esta pergunta pergunta o que fazer quando fitBounds()é insuficiente - ou porque o zoom é excessivo ou você não deseja aumentar o zoom (ou seja, você deseja apenas o nível de zoom).
John S
@ Nick Clark: Como você sabe os limites do sw, ne a serem definidos? Como você os capturou antes?
Varprakash 30/05

Respostas:

108

Uma pergunta semelhante foi feita no grupo do Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Os níveis de zoom são discretos, com a escala dobrando em cada etapa. Portanto, em geral, você não pode ajustar exatamente os limites que deseja (a menos que tenha muita sorte com o tamanho do mapa).

Outra questão é a relação entre os comprimentos laterais, por exemplo, você não pode ajustar os limites exatamente a um retângulo fino dentro de um mapa quadrado.

Não há uma resposta fácil sobre como ajustar limites exatos, porque, mesmo se você estiver disposto a alterar o tamanho da div do mapa, terá que escolher para qual tamanho e nível de zoom correspondente será alterado (grosso modo, você o torna maior ou menor) do que é atualmente?).

Se você realmente precisar calcular o zoom, em vez de armazená-lo, faça o seguinte:

A projeção de Mercator distorce a latitude, mas qualquer diferença de longitude sempre representa a mesma fração da largura do mapa (a diferença de ângulo em graus / 360). No zoom zero, o mapa do mundo inteiro tem 256x256 pixels e o zoom de cada nível dobra a largura e a altura. Então, depois de um pouco de álgebra, podemos calcular o zoom da seguinte forma, desde que saibamos a largura do mapa em pixels. Observe que, como a longitude gira em torno, precisamos garantir que o ângulo seja 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
fonte
1
Você não precisaria repetir isso para o lat e depois escolher o mínimo do resultado do 2? Eu não acho que isso funcionaria para limites altos e estreitos .....
whiteatom
3
Funciona muito bem para mim com uma mudança de Math.round para Math.floor. Graças um milhão.
Pete
3
Como isso pode estar certo se não leva em conta a latitude? Perto do equador, deve estar bem, mas a escala do mapa em um determinado nível de zoom muda dependendo da latitude!
Eyal
1
@Pete good point, em geral, você provavelmente desejaria arredondar o nível de zoom para caber um pouco mais do que o desejado no mapa, em vez de um pouco menos. Usei Math.round porque, na situação do OP, o valor antes do arredondamento deve ser aproximadamente integral.
Giles Gardam
20
o que é o valor para pixelWidth
albert Jegani
279

Agradeço a Giles Gardam por sua resposta, mas trata apenas da longitude e não da latitude. Uma solução completa deve calcular o nível de zoom necessário para a latitude e o nível de zoom necessário para a longitude e, em seguida, tirar o menor (mais distante) dos dois.

Aqui está uma função que usa latitude e 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

Parâmetros:

O valor do parâmetro "bounds" deve ser um google.maps.LatLngBoundsobjeto.

O valor do parâmetro "mapDim" deve ser um objeto com as propriedades "height" e "width" que representam a altura e a largura do elemento DOM que exibe o mapa. Você pode diminuir esses valores se quiser garantir o preenchimento. Ou seja, talvez você não queira que os marcadores de mapa dentro dos limites fiquem muito perto da borda do mapa.

Se você estiver usando a biblioteca jQuery, o mapDimvalor poderá ser obtido da seguinte maneira:

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

Se você estiver usando a biblioteca Prototype, o valor mapDim pode ser obtido da seguinte maneira:

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

Valor de retorno:

O valor de retorno é o nível máximo de zoom que ainda exibirá os limites inteiros. Este valor estará entre 0e o nível máximo de zoom, inclusive.

O nível máximo de zoom é 21. (Creio que eram apenas 19 para a API do Google Maps v2.)


Explicação:

O Google Maps usa uma projeção Mercator. Em uma projeção de Mercator, as linhas de longitude são igualmente espaçadas, mas as linhas de latitude não. A distância entre as linhas de latitude aumenta à medida que vão do equador aos pólos. De fato, a distância tende ao infinito quando atinge os pólos. Um mapa do Google Maps, no entanto, não mostra latitudes acima de aproximadamente 85 graus norte ou abaixo de -85 graus sul. ( referência ) (eu calculo o ponto de corte real em +/- 85,05112877980658 graus.)

Isso torna o cálculo das frações para os limites mais complicado para latitude do que para longitude. Eu usei uma fórmula da Wikipedia para calcular a fração de latitude. Suponho que isso corresponda à projeção usada pelo Google Maps. Afinal, a página de documentação do Google Maps que eu vinculo acima contém um link para a mesma página da Wikipedia.

Outras notas:

  1. Os níveis de zoom variam de 0 ao nível máximo de zoom. O nível de zoom 0 é o mapa totalmente diminuído. Níveis mais altos aumentam o zoom do mapa. ( referência )
  2. No nível de zoom 0, o mundo inteiro pode ser exibido em uma área com 256 x 256 pixels. ( referência )
  3. Para cada nível de zoom mais alto, o número de pixels necessários para exibir a mesma área dobra em largura e altura. ( referência )
  4. Mapas quebram na direção longitudinal, mas não na direção latitudinal.
John S
fonte
6
excelente resposta, esse deve ser o principal votado, pois representa longitude e latitude. Funcionou perfeitamente até agora.
Peter Wooster
1
@ John S - Esta é uma solução fantástica e estou pensando em usá-la no método nativo do google maps fitBounds disponível para mim também. Notei que o fitBounds às vezes está com um nível de zoom de volta (menos zoom), mas presumo que seja do preenchimento que está sendo adicionado. Existe a única diferença entre esse método e o fitBounds, apenas a quantidade de preenchimento que você deseja adicionar, responsável pela alteração no nível de zoom entre os dois?
johntrepreneur
1
@johntrepreneur - Vantagem # 1: você pode usar esse método antes mesmo de criar o mapa, para fornecer seu resultado às configurações iniciais do mapa. Com fitBounds, você precisa criar o mapa e aguardar o evento "bounds_changed".
John S
1
@ MarianPaździoch - Funciona para esses limites, veja aqui . Você espera poder aumentar o zoom para que esses pontos estejam nos cantos exatos do mapa? Isso não é possível porque os níveis de zoom são valores inteiros. A função retorna o nível mais alto de zoom que ainda incluirá os limites inteiros no mapa.
John S
1
@CarlMeyer - Eu não o mencionei na minha resposta, mas em um comentário acima, afirmo que uma vantagem dessa função é "Você pode usar esse método antes mesmo de criar o mapa". O uso map.getProjection()eliminaria parte da matemática (e a suposição sobre a projeção), mas significaria que a função não poderia ser chamada até que o mapa fosse criado e o evento "projection_changed" fosse acionado.
John S
39

Para a versão 3 da API, isso é simples e funcional:

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 alguns truques opcionais:

//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
fonte
1
por que não definir um nível mínimo de zoom durante a inicialização do mapa, algo como: var mapOptions = {maxZoom: 15,};
Kush
2
@ Kush, bom ponto. mas maxZoomimpedirá que o usuário faça zoom manual . Meu exemplo altera apenas o DefaultZoom e somente se for necessário.
precisa saber é
1
quando você fitBounds, ele apenas salta para caber nos limites, em vez de animar a partir da exibição atual. a solução impressionante é usando o já mencionado getBoundsZoomLevel. Dessa forma, quando você chama setZoom, ele anima para o nível de zoom desejado. De lá, ele não é um problema para fazer o panTo e você acaba com uma bela animação mapa que se encaixa nos limites
user151496
a animação não é discutida na pergunta nem na minha resposta. Se você tiver um exemplo útil sobre o tópico, basta criar uma resposta construtiva, com exemplo, como e quando ela pode ser usada.
d.raev
Por algum motivo, o mapa do Google não aumenta o zoom ao chamar setZoom () imediatamente após a chamada map.fitBounds (). (gmaps é v3.25 atualmente)
kashiraja
4

Aqui está uma versão da função 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)
    }
photograve
fonte
1

Obrigado, isso me ajudou muito a encontrar o fator de zoom mais adequado para exibir corretamente uma polilinha. Encontro as coordenadas máxima e mínima entre os pontos que tenho que rastrear e, caso o caminho seja muito "vertical", apenas adicionei algumas linhas 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);

Na verdade, o fator de zoom ideal é o fator de zoom-1.

Valerio Giacosa
fonte
Eu gostei var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Ainda assim, muito útil.
Mojowen 29/06/12
1

Como todas as outras respostas parecem ter problemas para mim com um ou outro conjunto de circunstâncias (largura / altura do mapa, largura / altura dos limites etc.), imaginei que colocaria minha resposta aqui ...

Havia um arquivo javascript muito útil aqui: http://www.polyarc.us/adjust.js

Eu usei isso como base para isso:

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

};

Você pode certamente limpar ou reduzi-lo, se necessário, mas mantive os nomes das variáveis ​​por muito tempo, na tentativa de facilitar o entendimento.

Se você está se perguntando de onde veio o OFFSET, aparentemente 268435456 é metade da circunferência da Terra em pixels no nível de zoom 21 (de acordo com http://www.appelsiini.net/2008/11/11/introduction-to-marker-clustering-with-google -maps ).

Homem sombra
fonte
0

Valério está quase certo com sua solução, mas há algum erro lógico.

você deve primeiro verificar se o ângulo2 é maior que o ângulo, antes de adicionar 360 em negativo.

caso contrário, você sempre terá um valor maior que o ângulo

Portanto, a solução correta é:

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á, porque eu tenho uma largura maior que a altura.

Lukas Olsen
fonte
0

map.getBounds()não é uma operação momentânea, então eu uso em um manipulador de eventos de caso semelhante. Aqui está o meu exemplo no Coffeescript

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

Exemplo de trabalho para encontrar o centro padrão médio com react-google-maps em 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
fonte
0

O cálculo do nível de zoom para as longitudes de Giles Gardam funciona bem para mim. Se você deseja calcular o fator de zoom para latitude, esta é uma solução fácil que funciona bem:

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
fonte