Google Maps V3 - Belirli bir sınır için yakınlaştırma düzeyi nasıl hesaplanır


138

getBoundsZoomLevel()V2 API benzer Google Maps V3 API kullanarak belirli bir sınır için yakınlaştırma düzeyini hesaplamak için bir yol arıyorum .

İşte yapmak istediğim şey:

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

Ne yazık ki, fitBounds()yöntemi özel kullanım durumum için kullanamıyorum. İşaretleyicileri haritaya yerleştirmek için iyi çalışır, ancak kesin sınırları ayarlamak için iyi çalışmaz. İşte fitBounds()yöntemi neden kullanamıyorum bir örnek .

map.fitBounds(map.getBounds()); // not what you expect

4
Son örnek mükemmel ve çok açıklayıcı! +1. Ben aynı sorun .
TMS

Üzgünüm, bağlantılı yanlış soru, bu doğru bağlantı .
TMS

Bu soru diğer sorunun kopyası değildir . Diğer sorunun cevabı kullanmaktır fitBounds(). Bu soru fitBounds()yetersiz olduğunda ne yapılacağını sorar - ya zumlar üzerinde ya da zum yapmak istemediğiniz için (yani, sadece zum seviyesini istiyorsunuz).
John S

@Nick Clark: Hangi sınırın belirleneceğini nereden biliyorsunuz? Onları daha önce nasıl yakaladın?
varaprakash

Yanıtlar:


108

Google grubunda da benzer bir soru soruldu: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Zoom seviyeleri ayrıktır ve her adımda ölçek iki katına çıkar. Bu nedenle, genel olarak istediğiniz sınırlara tam olarak sığamazsınız (belirli harita boyutuyla çok şanslı değilseniz).

Başka bir sorun, kenar uzunlukları arasındaki orantır, örneğin sınırları kare bir haritanın içindeki ince bir dikdörtgene tam olarak sığdıramazsınız.

Tam sınırların nasıl sığacağına dair kolay bir cevap yoktur, çünkü harita bölmesinin boyutunu değiştirmek isteseniz bile, hangi boyutu ve karşılık gelen yakınlaştırma seviyesini değiştireceğinizi seçmeniz gerekir (kabaca konuşursak, büyütür mü küçür mü şu anda olduğundan daha mı?).

Yakınlaştırmayı depolamak yerine gerçekten hesaplamanız gerekiyorsa, bu hile yapmalıdır:

Mercator projeksiyonu enlemi çözer, ancak boylamdaki herhangi bir fark her zaman haritanın genişliğinin aynı kısmını temsil eder (derece / 360 olarak açı farkı). Sıfır zoomda, tüm dünya haritası 256x256 pikseldir ve her seviyeye yakınlaştırma hem genişliği hem de yüksekliği iki katına çıkarır. Biraz cebirden sonra, haritanın genişliğini piksel cinsinden bilmemiz koşuluyla, yakınlaştırmayı aşağıdaki gibi hesaplayabiliriz. Boylam sarıldığından, açının pozitif olduğundan emin olmamız gerektiğini unutmayın.

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

1
Bunu lat için tekrarlamanız ve ardından 2 sonucunun min'ini seçmeniz gerekmez mi? Bunun uzun dar sınırlar için işe yarayacağını sanmıyorum .....
Mart'ta whiteatom 25:12

3
Math.round'un Math.floor olarak değiştirilmesiyle benim için harika çalışıyor. Milyonlarca kez teşekkürler.
Pete

3
Enlem dikkate alınmazsa bu nasıl doğru olabilir? Ekvatorun yakınında iyi olmalı, ancak belirli bir zum düzeyinde haritanın ölçeği enlemine bağlı olarak değişir!
Eyal

1
@ İyi bir noktaya gelin, genel olarak harita üzerinde istenenden biraz daha fazla yerine biraz daha fazla sığacak şekilde yakınlaştırma seviyesini düşürmek isteyebilirsiniz. Math.round'u kullandım çünkü OP'nin durumunda yuvarlamadan önceki değer yaklaşık olarak integral olmalıdır.
Giles Gardam

20
pixelWidth için değer nedir
albert Jegani

279

Cevabı için Giles Gardam'a teşekkürler, ancak enlem değil sadece boylamı ele alıyor. Tam bir çözüm, enlem için gereken yakınlaştırma seviyesini ve boylam için gerekli yakınlaştırma düzeyini hesaplamalı ve sonra ikisinden daha küçük olanı (daha fazla) almalıdır.

Hem enlem hem de boylam kullanan bir işlev:

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

Parametreler:

"Bounds" parametre değeri bir google.maps.LatLngBounds nesne .

"MapDim" parametre değeri, haritayı görüntüleyen DOM öğesinin yüksekliğini ve genişliğini temsil eden "height" ve "width" özelliklerine sahip bir nesne olmalıdır. Dolguyu sağlamak istiyorsanız bu değerleri azaltmak isteyebilirsiniz. Yani, sınırlar içindeki harita işaretleyicilerinin haritanın kenarına çok yakın olmasını istemeyebilirsiniz.

JQuery kitaplığını kullanıyorsanız, mapDimdeğer aşağıdaki gibi elde edilebilir:

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

Prototip kitaplığı kullanıyorsanız, mapDim değeri aşağıdaki gibi elde edilebilir:

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

Geri dönüş değeri:

Dönüş değeri, tüm sınırları görüntüleyecek maksimum zum seviyesidir. Bu değer arasında 0ve maksimum zum düzeyi dahil (dahil) olacaktır.

Maksimum yakınlaştırma düzeyi 21'dir. (Google Maps API v2 için yalnızca 19 olduğunu düşünüyorum.)


Açıklama:

Google Haritalar bir Mercator projeksiyonu kullanır. Bir Mercator projeksiyonunda, boylam çizgileri eşit aralıklıdır, ancak enlem çizgileri değildir. Enlem çizgileri arasındaki mesafe, ekvatordan kutuplara gittikçe artar. Aslında mesafe, kutuplara ulaştıkça sonsuzluğa doğru yönelir. Bununla birlikte, bir Google Haritalar haritası yaklaşık 85 derecenin kuzeyinden veya yaklaşık -85 derecenin güneyinden enlemler göstermez. ( referans ) (Gerçek kesimi +/- 85.05112877980658 derecede hesaplıyorum.)

Bu, sınırlar için kesirlerin, enlem için boylamdan daha karmaşık olmasını sağlar. Enlem oranını hesaplamak için Wikipedia'dan bir formül kullandım . Bunun Google Haritalar tarafından kullanılan projeksiyonla eşleştiğini düşünüyorum. Sonuçta, yukarıda bağlantı verdiğim Google Haritalar dokümantasyon sayfası aynı Wikipedia sayfasına bir bağlantı içeriyor.

Diğer notlar:

  1. Zoom seviyeleri 0 ile maksimum zoom seviyesi arasında değişir. Zoom seviyesi 0, haritanın tamamen uzaklaştırılmış halidir. Daha yüksek seviyeler haritayı daha da yakınlaştırır. ( başvuru )
  2. 0 zum seviyesinde tüm dünya 256 x 256 piksel olan bir alanda görüntülenebilir. ( başvuru )
  3. Her yüksek yakınlaştırma seviyesi için, aynı alanı görüntülemek için gereken piksel sayısı hem genişlik hem de yükseklik açısından iki katına çıkar. ( başvuru )
  4. Haritalar boylamsal yönde sarılır, ancak enlemsel yönde sarılmaz.

6
mükemmel cevap, bu hem boylam hem de enlemi açıkladığı için en yüksek oyu almalıdır. Şimdiye kadar mükemmel çalıştı.
Peter Wooster

1
@John S - Bu harika bir çözüm ve ben de mevcut yerel google haritalar fitBounds yöntemi üzerinde kullanmayı düşünüyorum. FitBounds bazen bir yakınlaştırma düzeyi (uzaklaştırılmış) olduğunu fark ettim, ama bunun eklediği dolgudan olduğunu varsayıyorum. Bu ve fitBounds yöntemi arasındaki tek fark, sadece ikisi arasındaki yakınlaştırma düzeyindeki değişiklik için hangi hesapları eklemek istediğiniz dolgu miktarıdır?
johntrepreneur

1
@johntrepreneur - Avantaj # 1: Haritayı bile oluşturmadan önce bu yöntemi kullanabilirsiniz, böylece sonucu ilk harita ayarlarına sağlayabilirsiniz. İle fitBoundsharitayı oluşturmanız ve ardından "bounds_changed" etkinliğini beklemeniz gerekir.
John S

1
@ MarianPaździoch - Bu sınırlar için işe yarıyor, buraya bakın . Bu noktalar haritanın tam köşelerinde olacak şekilde zum yapabilmeyi bekliyor musunuz? Yakınlaştırma seviyeleri tamsayı değerler olduğu için bu mümkün değildir. İşlev, haritadaki sınırların tümünü yine de içerecek en yüksek zum düzeyini döndürür.
John S

1
@CarlMeyer - Cevabımda bahsetmiyorum, ancak yukarıdaki bir yorumda bu işlevin bir avantajının "Haritayı bile oluşturmadan önce bu yöntemi kullanabilirsiniz" olduğunu belirtiyorum. Kullanmak map.getProjection(), matematiğin bir kısmını (ve projeksiyonla ilgili varsayımı) ortadan kaldıracaktır, ancak bu, harita oluşturulduktan ve "projection_changed" olayı tetiklenene kadar işlevin çağrılamayacağı anlamına gelir.
John S

39

API'nın 3. sürümü için bu basit ve işe yarar:

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

ve bazı isteğe bağlı hileler:

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

1
haritayı başlatırken neden minimum zoom seviyesini ayarlamıyorsunuz: var mapOptions = {maxZoom: 15,};
Kush

2
@Kush, iyi bir nokta. ancak maxZoomkullanıcının manuel zum yapmasını önler . Örneğim yalnızca DefaultZoom'u ve yalnızca gerekliyse değiştirir.
d.raev

1
fitBounds yaptığınızda, geçerli görünümden orada animasyon yapmak yerine yalnızca sınırlara uyacak şekilde atlar. müthiş çözüm, daha önce bahsedilen kullanarak getBoundsZoomLevel. bu şekilde, setZoom'u çağırdığınızda istediğiniz yakınlaştırma düzeyine hareket eder. oradan panTo yapmak bir sorun değildir ve sınırlara uyan güzel bir harita animasyonu ile
sonuçlanır

Animasyon soruda veya cevabımda tartışılan bir yer değildir. Konuyla ilgili faydalı bir örneğiniz varsa, örnek ve nasıl ve ne zaman kullanılabileceği ile yapıcı bir cevap oluşturmanız yeterlidir.
d.raev

Herhangi bir nedenle, map.fitBounds () çağrısından hemen sonra setZoom () çağrılırken Google haritası zum yapmaz. (gmaps şu anda v3.25'tir)
kashiraja

4

İşte işlevin Kotlin versiyonu:

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

1

Teşekkürler, bu bir çoklu çizgiyi doğru bir şekilde görüntülemek için en uygun zoom faktörünü bulmamda bana çok yardımcı oldu. İzlemem gereken noktalar arasındaki maksimum ve minimum koordinatları buluyorum ve yolun çok "dikey" olması durumunda, birkaç kod satırı ekledim:

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

Aslında ideal zoom faktörü zoomfactor-1'dir.


Sevdim var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Yine de, çok yararlı.
Mojowen

1

Diğer tüm cevapların benim için bir veya daha fazla koşulla (harita genişliği / yüksekliği, sınır genişliği / yüksekliği, vb.) Sorunları olduğu için cevabımı buraya koyacağımı düşündüm ...

Burada çok kullanışlı bir javascript dosyası vardı: http://www.polyarc.us/adjust.js

Bunun için bir üs olarak kullandım:

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

};

Bunu kesinlikle temizleyebilir veya gerekirse küçültebilirsiniz, ancak anlaşılmasını kolaylaştırmak için değişken adlarını uzun süre sakladım.

OFFSET'in nereden geldiğini merak ediyorsanız, görünüşe göre 268435456, yakınlaştırma düzeyi 21'de piksel cinsinden dünya çevresinin yarısıdır ( http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google -maps ).


0

Valerio çözümü konusunda neredeyse haklı, ancak mantıklı bir hata var.

negatif olarak 360 eklemeden önce, ilk olarak angle2'nin açıdan daha büyük olup olmadığını kontrol etmelisiniz.

aksi takdirde daima açıdan daha büyük bir değere sahip olursunuz

Yani doğru çözüm:

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 orada, çünkü yükseklikten daha büyük bir genişliğe sahibim.


0

map.getBounds()anlık işlem değildir, bu yüzden benzer bir olay olay işleyicisi kullanıyorum. İşte benim Coffeescript'teki örneğim

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

0

Tepki-google-maps ile ortalama varsayılan merkezi bulmak için çalışma örneği 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>

0

Giles Gardam'ın boylamları için zoom seviyesinin hesaplanması benim için iyi çalışıyor. Enlem için yakınlaştırma faktörünü hesaplamak istiyorsanız, bu iyi çalışan kolay bir çözümdür:

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);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.