Mapy Google V3 - Jak obliczyć poziom powiększenia dla zadanych granic

138

Szukam sposobu na obliczenie poziomu powiększenia dla zadanych granic za pomocą Google Maps V3 API, podobnie jak getBoundsZoomLevel()w V2 API.

Oto, co chcę zrobić:

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

Niestety nie mogę użyć tej fitBounds()metody w moim konkretnym przypadku użycia. Działa dobrze przy dopasowywaniu znaczników na mapie, ale nie sprawdza się dobrze przy ustawianiu dokładnych granic. Oto przykład, dlaczego nie mogę użyć tej fitBounds()metody.

map.fitBounds(map.getBounds()); // not what you expect
Nick Clark
źródło
4
Ostatni przykład jest doskonały i bardzo obrazowy! +1. Mam ten sam problem .
TMS,
Przepraszamy, połączone złe pytanie, to jest poprawny link .
TMS,
To pytanie nie jest powtórzeniem drugiego pytania . Odpowiedzią na drugie pytanie jest użycie fitBounds(). To pytanie dotyczy tego, co zrobić, gdy fitBounds()jest niewystarczające - albo dlatego, że powiększa lub nie chcesz powiększać (tj. Chcesz tylko poziom powiększenia).
John S
@Nick Clark: Skąd wiesz, jakie granice należy wyznaczyć? Jak je wcześniej uchwyciłeś?
varaprakash

Odpowiedzi:

108

Podobne pytanie zostało zadane na grupie Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Poziomy powiększenia są dyskretne, a skala podwaja się w każdym kroku. Więc ogólnie nie możesz dokładnie dopasować granic, które chcesz (chyba że masz szczęście z określonym rozmiarem mapy).

Inną kwestią jest stosunek długości boków, np. Nie można dopasować granic dokładnie do cienkiego prostokąta wewnątrz kwadratowej mapy.

Nie ma łatwej odpowiedzi na to, jak dopasować dokładne granice, ponieważ nawet jeśli chcesz zmienić rozmiar div mapy, musisz wybrać rozmiar i odpowiedni poziom powiększenia, na który się zmieniasz (z grubsza mówiąc, czy zwiększysz czy zmniejszysz niż jest obecnie?).

Jeśli naprawdę potrzebujesz obliczyć powiększenie, zamiast go przechowywać, powinno to załatwić sprawę:

Odwzorowanie Mercatora powoduje wypaczenie szerokości geograficznej, ale każda różnica długości geograficznej zawsze reprezentuje ten sam ułamek szerokości mapy (różnica kątów w stopniach / 360). Przy zerowym powiększeniu cała mapa świata ma 256x256 pikseli, a powiększanie każdego poziomu podwaja szerokość i wysokość. Więc po małej algebrze możemy obliczyć powiększenie w następujący sposób, pod warunkiem, że znamy szerokość mapy w pikselach. Zwróć uwagę, że ponieważ długość geograficzna zawija się wokół, musimy upewnić się, że kąt jest dodatni.

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
źródło
1
Czy nie musiałbyś powtórzyć tego dla łaty, a następnie wybrać min z wyniku 2? Nie sądzę, żeby to działało na wysokie wąskie granice .....
whiteatom
3
U mnie działa świetnie ze zmianą Math.round na Math.floor. Stukrotne dzięki.
Pete
3
Jak to możliwe, jeśli nie uwzględnia szerokości geograficznej? W pobliżu równika powinno być dobrze, ale skala mapy przy danym poziomie powiększenia zmienia się w zależności od szerokości geograficznej!
Eyal
1
@Pete Dobra uwaga, ogólnie rzecz biorąc, prawdopodobnie chciałbyś zaokrąglić w dół poziom powiększenia, aby zmieścić na mapie nieco więcej, niż jest to pożądane, a nie trochę mniej. Użyłem Math.round, ponieważ w sytuacji OP wartość przed zaokrągleniem powinna być w przybliżeniu całkowa.
Giles Gardam,
20
jaka jest wartość pixelWidth
albert Jegani
279

Dziękuję Gilesowi Gardamowi za jego odpowiedź, ale dotyczy tylko długości geograficznej, a nie szerokości geograficznej. Kompletne rozwiązanie powinno obliczyć poziom powiększenia wymagany dla szerokości geograficznej i poziom powiększenia wymagany dla długości geograficznej, a następnie wziąć mniejszy (dalej) z tych dwóch.

Oto funkcja, która używa zarówno szerokości, jak i długości geograficznej:

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

Parametry:

Wartość parametru „bounds” powinna wynosić a google.maps.LatLngBounds obiektem.

Wartość parametru „mapDim” powinna być obiektem z właściwościami „height” i „width”, które reprezentują wysokość i szerokość elementu DOM wyświetlającego mapę. Możesz chcieć zmniejszyć te wartości, jeśli chcesz zapewnić dopełnienie. Oznacza to, że możesz nie chcieć, aby znaczniki mapy w granicach znajdowały się zbyt blisko krawędzi mapy.

Jeśli korzystasz z biblioteki jQuery, mapDimwartość można uzyskać w następujący sposób:

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

Jeśli używasz biblioteki Prototype, wartość mapDim można uzyskać w następujący sposób:

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

Wartość zwracana:

Wartość zwracana to maksymalny poziom powiększenia, który nadal będzie wyświetlał całe granice. Ta wartość będzie wynosić między0 od maksymalnego poziomu powiększenia włącznie.

Maksymalny poziom powiększenia to 21. (myślę, że było to tylko 19 dla Google Maps API v2.)


Wyjaśnienie:

Mapy Google używają odwzorowania Mercator. W rzucie Mercatora linie długości geograficznej są równomiernie rozmieszczone, ale linie szerokości geograficznej nie. Odległość między liniami szerokości geograficznej zwiększa się w miarę przechodzenia od równika do biegunów. W rzeczywistości odległość zbliża się do nieskończoności, gdy dociera do biegunów. Mapa Google Maps nie pokazuje jednak szerokości geograficznych powyżej około 85 stopni na północ ani poniżej około -85 stopni na południe. ( odniesienie ) (Obliczam faktyczną wartość odcięcia przy +/- 85,05112877980658 stopni.)

To sprawia, że ​​obliczanie ułamków dla granic jest bardziej skomplikowane dla szerokości geograficznej niż dla długości geograficznej. Użyłem wzoru z Wikipedii, aby obliczyć ułamek szerokości geograficznej. Zakładam, że jest to zgodne z projekcją używaną przez Google Maps. W końcu strona dokumentacji Google Maps, do której odsyłam powyżej, zawiera link do tej samej strony Wikipedii.

Inne notatki:

  1. Poziomy powiększenia mieszczą się w zakresie od 0 do maksymalnego poziomu powiększenia. Poziom powiększenia 0 to mapa w pełni pomniejszona. Wyższe poziomy powodują dalsze powiększanie mapy. ( odniesienie )
  2. Na poziomie powiększenia 0 cały świat może być wyświetlany w obszarze o wielkości 256 x 256 pikseli. ( odniesienie )
  3. Dla każdego wyższego poziomu powiększenia liczba pikseli potrzebnych do wyświetlenia tego samego obszaru podwaja się zarówno pod względem szerokości, jak i wysokości. ( odniesienie )
  4. Mapy zawijają się w kierunku podłużnym, ale nie w kierunku równoleżnikowym.
John S.
źródło
6
doskonała odpowiedź, to powinno być najczęściej głosowane, ponieważ uwzględnia zarówno długość, jak i szerokość geograficzną. Jak dotąd działało idealnie.
Peter Wooster
1
@John S - To fantastyczne rozwiązanie i rozważam użycie tego w natywnej metodzie fitBounds map Google, która jest dla mnie dostępna. Zauważyłem, że funkcja fitBounds jest czasami o jeden poziom powiększenia wstecz (pomniejszona), ale zakładam, że wynika to z dopełnienia, które dodaje. Czy jedyna różnica między tą metodą a metodą fitBounds, to tylko ilość dopełnienia, którą chcesz dodać, która odpowiada za zmianę poziomu powiększenia między nimi?
johntrepreneur
1
@johntrepreneur - Zaleta nr 1: Możesz użyć tej metody jeszcze przed utworzeniem mapy, dzięki czemu możesz podać jej wynik do początkowych ustawień mapy. W programie fitBoundsmusisz utworzyć mapę, a następnie poczekać na zdarzenie „bounds_changed”.
John S,
1
@ MarianPaździoch - To działa w tych granicach, patrz tutaj . Czy spodziewasz się możliwości powiększenia, aby te punkty znajdowały się dokładnie w rogach mapy? Nie jest to możliwe, ponieważ poziomy powiększenia są wartościami całkowitymi. Funkcja zwraca najwyższy poziom powiększenia, który nadal będzie obejmował całe granice mapy.
John S,
1
@CarlMeyer - nie wspominam o tym w mojej odpowiedzi, ale w komentarzu powyżej stwierdzam, że jedną z zalet tej funkcji jest „Możesz użyć tej metody jeszcze przed utworzeniem mapy”. Użycie map.getProjection()wyeliminowałoby część matematyki (i założenie dotyczące odwzorowania), ale oznaczałoby to, że funkcja nie mogłaby zostać wywołana przed utworzeniem mapy i wywołaniem zdarzenia „projection_changed”.
John S
39

W przypadku wersji 3 interfejsu API jest to proste i działa:

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

i kilka opcjonalnych sztuczek:

//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
źródło
1
dlaczego nie ustawić minimalnego poziomu powiększenia podczas inicjalizacji mapy, na przykład: var mapOptions = {maxZoom: 15,};
Kush
2
@Kush, dobra uwaga. ale maxZoomuniemożliwi użytkownikowi ręczne powiększanie. Mój przykład zmienia tylko DefaultZoom i tylko wtedy, gdy jest to konieczne.
d.raev
1
kiedy wykonujesz fitBounds, po prostu przeskakuje, aby dopasować się do granic, zamiast animować je z bieżącego widoku. wspaniałym rozwiązaniem jest użycie już wspomnianego getBoundsZoomLevel. w ten sposób wywołanie setZoom powoduje animację do żądanego poziomu powiększenia. stamtąd nie ma problemu z zrobieniem panTo i kończy się piękną animacją mapy, która pasuje do granic
user151496
animacja nie jest omawiana w pytaniu ani w mojej odpowiedzi. Jeśli masz przydatny przykład na ten temat, po prostu utwórz konstruktywną odpowiedź z przykładem i jak i kiedy można jej użyć.
d.raev
Z jakiegoś powodu mapa Google nie powiększa się podczas wywoływania setZoom () bezpośrednio po wywołaniu map.fitBounds (). (obecnie gmaps jest w wersji 3.25)
kashiraja
4

Tutaj wersja Kotlin funkcji:

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
źródło
1

Dzięki, to bardzo pomogło mi w znalezieniu najbardziej odpowiedniego współczynnika powiększenia, aby poprawnie wyświetlić polilinię. Maksymalne i minimalne współrzędne znajduję wśród punktów, które muszę śledzić i na wypadek, gdyby ścieżka była bardzo „pionowa”, dodałem tylko kilka wierszy kodu:

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

W rzeczywistości idealnym współczynnikiem powiększenia jest współczynnik zoom-1.

Valerio Giacosa
źródło
Lubiłem var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Wciąż bardzo pomocny.
Mojowen
1

Ponieważ wszystkie inne odpowiedzi wydają się mieć dla mnie problemy z jednym lub innym zestawem okoliczności (szerokość / wysokość mapy, szerokość / wysokość granic itp.), Pomyślałem, że umieściłem swoją odpowiedź tutaj ...

Znajdował się tutaj bardzo przydatny plik javascript: http://www.polyarc.us/adjust.js

Użyłem tego jako podstawy do tego:

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

};

Z pewnością możesz to wyczyścić lub zminimalizować w razie potrzeby, ale zachowałem nazwy zmiennych przez długi czas, aby ułatwić zrozumienie.

Jeśli zastanawiasz się, skąd się wziął OFFSET, najwyraźniej 268435456 to połowa obwodu Ziemi w pikselach na poziomie powiększenia 21 (zgodnie z http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google -mapy ).

Człowiek cień
źródło
0

Valerio ma prawie rację ze swoim rozwiązaniem, ale jest jakiś logiczny błąd.

musisz najpierw sprawdzić, czy kąt2 jest większy niż kąt, zanim dodasz 360 na minusie.

w przeciwnym razie zawsze masz większą wartość niż kąt

Tak więc prawidłowe rozwiązanie to:

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 jest tam, ponieważ mam większą szerokość niż wysokość.

Lukas Olsen
źródło
0

map.getBounds()nie jest operacją chwilową, więc używam w podobnym przypadku obsługi zdarzeń. Oto mój przykład w Coffeescript

@map.fitBounds(@bounds)
google.maps.event.addListenerOnce @map, 'bounds_changed', =>
  @map.setZoom(12) if @map.getZoom() > 12
MikDiet
źródło
0

Przykład pracy, aby znaleźć średnie domyślne centrum z reaktywnymi mapami google na 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
źródło
0

Obliczanie poziomu powiększenia dla długości geograficznych Gilesa Gardama działa dobrze dla mnie. Jeśli chcesz obliczyć współczynnik powiększenia dla szerokości geograficznej, jest to łatwe rozwiązanie, które działa dobrze:

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
źródło