Google Maps V3 - So berechnen Sie die Zoomstufe für bestimmte Grenzen

138

Ich suche nach einer Möglichkeit, die Zoomstufe für bestimmte Grenzen mithilfe der Google Maps V3-API zu berechnen, ähnlich wie getBoundsZoomLevel()in der V2-API.

Folgendes möchte ich tun:

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

Leider kann ich die fitBounds()Methode für meinen speziellen Anwendungsfall nicht verwenden . Es eignet sich gut zum Anpassen von Markierungen auf der Karte, aber nicht zum Festlegen exakter Grenzen. Hier ist ein Beispiel, warum ich die fitBounds()Methode nicht verwenden kann.

map.fitBounds(map.getBounds()); // not what you expect
Nick Clark
quelle
4
Das letzte Beispiel ist ausgezeichnet und sehr anschaulich! +1. Ich habe das gleiche Problem .
TMS
Entschuldigung, falsche Frage verlinkt, dies ist der richtige Link .
TMS
Diese Frage ist kein Duplikat der anderen Frage . Die Antwort auf die andere Frage ist zu verwenden fitBounds(). In dieser Frage wird gefragt, was zu tun ist, wenn fitBounds()dies nicht ausreicht - entweder weil es überzoomt oder Sie nicht zoomen möchten (dh Sie möchten nur die Zoomstufe).
John S
@ Nick Clark: Woher weißt du, welche Grenzen gesetzt werden müssen? Wie haben Sie sie zuvor aufgenommen?
Varaprakash

Antworten:

108

Eine ähnliche Frage wurde in der Google-Gruppe gestellt: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Die Zoomstufen sind diskret, wobei sich die Skalierung in jedem Schritt verdoppelt. Im Allgemeinen können Sie die gewünschten Grenzen also nicht genau anpassen (es sei denn, Sie haben großes Glück mit der jeweiligen Kartengröße).

Ein weiteres Problem ist das Verhältnis zwischen den Seitenlängen, z. B. können Sie die Grenzen nicht genau an ein dünnes Rechteck innerhalb einer quadratischen Karte anpassen.

Es gibt keine einfache Antwort darauf, wie exakte Grenzen angepasst werden sollen, denn selbst wenn Sie bereit sind, die Größe des Map Div zu ändern, müssen Sie auswählen, auf welche Größe und welche Zoomstufe Sie sich ändern möchten (grob gesagt, machen Sie es größer oder kleiner) als es derzeit ist?).

Wenn Sie den Zoom wirklich berechnen müssen, anstatt ihn zu speichern, sollte dies den Trick tun:

Die Mercator-Projektion verzerrt den Breitengrad, aber jeder Längenunterschied entspricht immer dem gleichen Bruchteil der Breite der Karte (der Winkeldifferenz in Grad / 360). Bei Zoom Null beträgt die gesamte Weltkarte 256 x 256 Pixel, und durch Zoomen jeder Ebene werden sowohl Breite als auch Höhe verdoppelt. Nach einer kleinen Algebra können wir den Zoom wie folgt berechnen, vorausgesetzt, wir kennen die Breite der Karte in Pixel. Beachten Sie, dass wir sicherstellen müssen, dass der Winkel positiv ist, da sich der Längengrad dreht.

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
quelle
1
Müssten Sie dies nicht für den Lat wiederholen und dann das Min des Ergebnisses der 2 wählen? Ich glaube nicht, dass dies für eine große, schmale Grenze funktionieren würde ...
Whiteatom
3
Funktioniert hervorragend für mich mit einem Wechsel von Math.round zu Math.floor. Tausend Dank.
Pete
3
Wie kann das richtig sein, wenn der Spielraum nicht berücksichtigt wird? In der Nähe des Äquators sollte es in Ordnung sein, aber der Maßstab der Karte bei einer bestimmten Zoomstufe ändert sich je nach Breitengrad!
Eyal
1
@Pete guter Punkt, im Allgemeinen möchten Sie wahrscheinlich die Zoomstufe abrunden, damit Sie ein bisschen mehr als gewünscht in die Karte passen, anstatt ein bisschen weniger. Ich habe Math.round verwendet, weil in der Situation des OP der Wert vor dem Runden ungefähr ganzzahlig sein sollte.
Giles Gardam
20
Was ist der Wert für PixelWidth
Albert Jegani
279

Vielen Dank an Giles Gardam für seine Antwort, die jedoch nur den Längen- und nicht den Breitengrad anspricht. Eine vollständige Lösung sollte die für den Breitengrad erforderliche Zoomstufe und die für den Längengrad erforderliche Zoomstufe berechnen und dann die kleinere (weiter außen liegende) der beiden herausnehmen.

Hier ist eine Funktion, die sowohl Breiten- als auch Längengrade verwendet:

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

Parameter:

Der Parameterwert "Grenzen" sollte ein google.maps.LatLngBoundsObjekt sein.

Der Parameterwert "mapDim" sollte ein Objekt mit den Eigenschaften "height" und "width" sein, die die Höhe und Breite des DOM-Elements darstellen, das die Karte anzeigt. Sie können diese Werte verringern, wenn Sie das Auffüllen sicherstellen möchten. Das heißt, Sie möchten möglicherweise nicht, dass Kartenmarkierungen innerhalb der Grenzen zu nahe am Rand der Karte liegen.

Wenn Sie die jQuery-Bibliothek verwenden, kann der mapDimWert wie folgt abgerufen werden:

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

Wenn Sie die Prototype-Bibliothek verwenden, kann der mapDim-Wert wie folgt abgerufen werden:

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

Rückgabewert:

Der Rückgabewert ist die maximale Zoomstufe, bei der weiterhin die gesamten Grenzen angezeigt werden. Dieser Wert liegt zwischen 0und einschließlich der maximalen Zoomstufe.

Die maximale Zoomstufe beträgt 21. (Ich glaube, es waren nur 19 für Google Maps API v2.)


Erläuterung:

Google Maps verwendet eine Mercator-Projektion. In einer Mercator-Projektion sind die Längengrade gleich beabstandet, die Breitengrade jedoch nicht. Der Abstand zwischen den Breitengraden nimmt zu, wenn sie vom Äquator zu den Polen verlaufen. Tatsächlich tendiert die Entfernung gegen unendlich, wenn sie die Pole erreicht. Eine Google Maps-Karte zeigt jedoch keine Breiten über ungefähr 85 Grad Nord oder unter ungefähr -85 Grad Süd. ( Referenz ) (Ich berechne den tatsächlichen Grenzwert bei +/- 85.05112877980658 Grad.)

Dies macht die Berechnung der Brüche für die Grenzen für den Breitengrad komplizierter als für den Längengrad. Ich habe eine Formel aus Wikipedia verwendet , um den Breitengradanteil zu berechnen. Ich gehe davon aus, dass dies mit der von Google Maps verwendeten Projektion übereinstimmt. Schließlich enthält die oben verlinkte Google Maps-Dokumentationsseite einen Link zu derselben Wikipedia-Seite.

Weitere Hinweise:

  1. Die Zoomstufen reichen von 0 bis zur maximalen Zoomstufe. Zoomstufe 0 ist die Karte, die vollständig herausgezoomt ist. Höhere Ebenen vergrößern die Karte weiter. ( Referenz )
  2. Bei Zoomstufe 0 kann die gesamte Welt in einem Bereich von 256 x 256 Pixel angezeigt werden. ( Referenz )
  3. Für jede höhere Zoomstufe verdoppelt sich die Anzahl der Pixel, die zur Anzeige des gleichen Bereichs benötigt werden, sowohl in der Breite als auch in der Höhe. ( Referenz )
  4. Karten werden in Längsrichtung, jedoch nicht in Breitenrichtung gewickelt.
John S.
quelle
6
Hervorragende Antwort, dies sollte die am besten gewählte sein, da es sowohl Längen- als auch Breitengrade berücksichtigt. Hat bisher perfekt funktioniert.
Peter Wooster
1
@ John S - Dies ist eine fantastische Lösung, und ich denke darüber nach, diese über die native Google Maps FitBounds-Methode zu verwenden, die mir ebenfalls zur Verfügung steht. Ich habe festgestellt, dass fitBounds manchmal eine Zoomstufe zurück ist (herausgezoomt), aber ich gehe davon aus, dass dies von der Polsterung herrührt, die hinzugefügt wird. Ist der einzige Unterschied zwischen dieser und der fitBounds-Methode nur die Menge an Polsterung, die Sie hinzufügen möchten, die für die Änderung der Zoomstufe zwischen den beiden verantwortlich ist?
johntrepreneur
1
@johntrepreneur - Vorteil Nr. 1: Sie können diese Methode verwenden, bevor Sie die Karte erstellen, damit Sie das Ergebnis für die anfänglichen Karteneinstellungen bereitstellen können. Mit fitBoundsmüssen Sie die Karte erstellen und dann auf das Ereignis "bounds_changed" warten.
John S
1
@ MarianPaździoch - Es funktioniert für diese Grenzen, siehe hier . Erwarten Sie, dass Sie zoomen können, damit sich diese Punkte genau an den Ecken der Karte befinden? Dies ist nicht möglich, da Zoomstufen ganzzahlige Werte sind. Die Funktion gibt die höchste Zoomstufe zurück, die noch die gesamten Grenzen auf der Karte enthält.
John S
1
@CarlMeyer - Ich erwähne es in meiner Antwort nicht, aber in einem Kommentar oben stelle ich fest, dass ein Vorteil dieser Funktion darin besteht, dass Sie diese Methode verwenden können, bevor Sie die Karte überhaupt erstellen. Die Verwendung map.getProjection()würde einen Teil der Mathematik (und die Annahme über die Projektion) eliminieren, aber dies würde bedeuten, dass die Funktion erst aufgerufen werden könnte, nachdem die Karte erstellt und das Ereignis "projection_changed" ausgelöst wurde.
John S
39

Für Version 3 der API ist dies einfach und funktioniert:

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

und einige optionale Tricks:

//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
quelle
1
Warum nicht beim Initialisieren der Karte eine minimale Zoomstufe festlegen? Var mapOptions = {maxZoom: 15,};
Kush
2
@ Kush, guter Punkt. maxZoomverhindert jedoch , dass der Benutzer manuell zoomt. Mein Beispiel ändert den DefaultZoom nur und nur, wenn es notwendig ist.
d.raev
1
Wenn Sie fitBounds ausführen, wird nur gesprungen, um die Grenzen anzupassen, anstatt dort aus der aktuellen Ansicht zu animieren. Die großartige Lösung ist die Verwendung der bereits erwähnten getBoundsZoomLevel. Auf diese Weise wird beim Aufrufen von setZoom die gewünschte Zoomstufe animiert. Von dort aus ist es kein Problem, das panTo zu machen, und Sie erhalten eine wunderschöne Kartenanimation, die den Grenzen entspricht
user151496
Animation wird weder in der Frage noch in meiner Antwort diskutiert. Wenn Sie ein nützliches Beispiel zu diesem Thema haben, erstellen Sie einfach eine konstruktive Antwort mit einem Beispiel und wie und wann es verwendet werden kann.
d.raev
Aus irgendeinem Grund zoomt Google Map nicht, wenn setZoom () unmittelbar nach dem Aufruf von map.fitBounds () aufgerufen wird. (gmaps ist v3.25 derzeit)
kashiraja
4

Hier eine Kotlin-Version der Funktion:

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)
    }
Fotograve
quelle
1

Vielen Dank, das hat mir sehr geholfen, den am besten geeigneten Zoomfaktor für die korrekte Anzeige einer Polylinie zu finden. Ich finde die maximalen und minimalen Koordinaten unter den Punkten, die ich verfolgen muss, und falls der Pfad sehr "vertikal" ist, habe ich nur einige Codezeilen hinzugefügt:

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

Tatsächlich ist der ideale Zoomfaktor Zoomfaktor 1.

Valerio Giacosa
quelle
Ich mochte var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Trotzdem sehr hilfreich.
Mojowen
1

Da alle anderen Antworten Probleme mit den einen oder anderen Umständen (Kartenbreite / -höhe, Grenzbreite / -höhe usw.) für mich zu haben scheinen, dachte ich, ich würde meine Antwort hier einfügen ...

Hier gab es eine sehr nützliche Javascript-Datei: http://www.polyarc.us/adjust.js

Ich habe das als Basis dafür verwendet:

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

};

Sie können dies sicherlich bereinigen oder bei Bedarf minimieren, aber ich habe die Variablennamen lange beibehalten, um das Verständnis zu erleichtern.

Wenn Sie sich fragen, woher OFFSET stammt, ist 268435456 anscheinend die Hälfte des Erdumfangs in Pixel bei Zoomstufe 21 (laut http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google) -Karten ).

Schattenmann
quelle
0

Valerio hat mit seiner Lösung fast recht, aber es gibt einen logischen Fehler.

Sie müssen zuerst prüfen, ob Winkel2 größer als Winkel ist, bevor Sie 360 ​​bei einem Negativ hinzufügen.

Andernfalls haben Sie immer einen größeren Wert als den Winkel

Die richtige Lösung lautet also:

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 ist da, weil ich eine größere Breite als Höhe habe.

Lukas Olsen
quelle
0

map.getBounds()ist keine momentane Operation, daher verwende ich in einem ähnlichen Fall Event Handler. Hier ist mein Beispiel in Coffeescript

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

Arbeitsbeispiel zum Finden eines durchschnittlichen Standardcenters mit React-Google-Maps auf 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
quelle
0

Die Berechnung der Zoomstufe für die Längen von Giles Gardam funktioniert für mich gut. Wenn Sie den Zoomfaktor für den Breitengrad berechnen möchten, ist dies eine einfache Lösung, die einwandfrei funktioniert:

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
quelle