Google Maps V3 - Як обчислити рівень збільшення для заданих меж


138

Я шукаю спосіб обчислити рівень масштабування для заданих меж за допомогою API Карт Google V3, подібний до getBoundsZoomLevel()API V2.

Ось що я хочу зробити:

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

На жаль, я не можу використовувати fitBounds()метод для мого конкретного випадку використання. Він добре працює для встановлення маркерів на карті, але він не працює добре для встановлення точних меж. Ось приклад того, чому я не можу використовувати fitBounds()метод.

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

4
Останній приклад відмінний і дуже показовий! +1. У мене така ж проблема .
TMS

Вибачте, пов'язане неправильне запитання, це правильне посилання .
TMS

Це запитання не є дублікатом іншого питання . Відповідь на інше питання - використовувати fitBounds(). Це запитання задає питання, що робити, коли fitBounds()недостатньо - або через масштабування, або ви не хочете масштабувати (тобто ви просто хочете рівень масштабування).
Джон С

@ Нік Кларк: Як ти знаєш межі sw, ne, які потрібно встановити? Як ви їх захоплювали раніше?
варапракаш

Відповіді:


108

Аналогічне запитання було задано і групі Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

Рівні масштабування дискретні, масштаб подвоюється на кожному кроці. Тож загалом ви не можете точно відповідати бажаним рамкам (якщо тільки вам не пощастить з певним розміром карти).

Іншим питанням є співвідношення між бічними довжинами, наприклад, ви не можете точно встановити межі в тонкий прямокутник всередині квадратної карти.

Немає простої відповіді про те, як підібрати точні межі, тому що навіть якщо ви готові змінити розмір поділки на карті, ви повинні вибрати, на який розмір та відповідний рівень збільшення ви змінюєтесь (грубо кажучи, чи збільшуєте ви його чи менший ніж це зараз?).

Якщо вам дійсно потрібно обчислити масштаб, а не зберігати його, це слід зробити:

Проекція Меркатора викривляє широту, але будь-яка різниця у довготі завжди представляє однакову частку ширини карти (різниця кутів у градусах / 360). При нульовому масштабі вся карта світу становить 256х256 пікселів, а масштабування кожного рівня подвоюється як по ширині, так і по висоті. Тож після невеликої алгебри ми можемо обчислити масштабування наступним чином, якщо ми знаємо ширину карти в пікселях. Зауважте, що оскільки довгота обертається навколо, ми повинні переконатися, що кут позитивний.

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
Чи не доведеться вам повторювати це за лат, а потім вибирати хв результат 2? Я не думаю, що це спрацювало б для високих вузьких меж .....
whiteatom

3
Для мене чудово працює зі зміною Math.round на Math.floor. Завдяки мільйонів.
Піт

3
Як це може бути правильним, якщо він не враховує широту? Біля екватора це повинно бути добре, але масштаб карти на заданому рівні збільшення змінюється залежно від широти!
Еял

1
@ Добре бачте, загалом ви, мабуть, захочете округлити рівень масштабування, щоб ви помістили трохи більше, ніж бажано на карті, а не трохи менше. Я використовував Math.round, оскільки в ситуації з ОП значення перед округленням повинно бути приблизно цільним.
Giles Gardam

20
яке значення для pixelWidth
albert Jegani

279

Дякую Джайлсу Гардаму за свою відповідь, але він стосується лише довготи, а не широти. Повне рішення повинно розраховувати рівень масштабу, необхідний для широти, і рівень масштабу, необхідний для довготи, а потім брати менший (далі) з двох.

Ось функція, яка використовує і широту, і довготу:

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

Параметри:

Значення параметра "межа" має бути google.maps.LatLngBoundsоб'єктом.

Значення параметра "mapDim" має бути об'єктом із властивостями "висота" та "ширина", які представляють висоту та ширину елемента DOM, який відображає карту. Ви можете зменшити ці значення, якщо хочете забезпечити прокладку. Тобто, можливо, ви не хочете, щоб маркери карт в межах були занадто близькими до краю карти.

Якщо ви використовуєте бібліотеку jQuery, mapDimзначення можна отримати таким чином:

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

Якщо ви використовуєте бібліотеку прототипів, значення mapDim можна отримати таким чином:

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

Повернене значення:

Повернене значення - це максимальний рівень масштабування, який все одно відображатиме цілі межі. Це значення буде між 0і максимальним рівнем збільшення, включно.

Максимальний рівень масштабування - 21. (Я вважаю, що для API Карт Google v2 було лише 19).


Пояснення:

Карти Google використовують проекцію Меркатора. У проекції Меркатора лінії довготи однаково розташовані, але лінії широти - ні. Відстань між лініями широти збільшується, коли вони йдуть від екватора до полюсів. Насправді відстань тяжіє до нескінченності, коли воно досягає полюсів. Однак на карті Карт Google не відображаються широти вище приблизно 85 градусів на північ або нижче приблизно -85 градусів на південь. ( довідка ) (я обчислюю фактичне скорочення при +/- 85.05112877980658 градусів.)

Це робить обчислення дробів для меж складнішими для широти, ніж для довготи. Я використовував формулу з Вікіпедії для обчислення частки широти. Я припускаю, що це відповідає проекції, яку використовує Google Maps. Зрештою, сторінка документації Google Maps, на яку я посилаюсь вище, містить посилання на ту саму сторінку Вікіпедії.

Інші примітки:

  1. Рівні масштабу варіюються від 0 до максимального рівня збільшення. Рівень масштабу 0 - це карта, повністю зменшена. Більш високі рівні збільшують масштаб карти. ( довідник )
  2. При рівні масштабу 0 весь світ може відображатися в області, що становить 256 х 256 пікселів. ( довідник )
  3. Для кожного більшого рівня збільшення кількість пікселів, необхідних для відображення однієї і тієї ж області, подвоюється як по ширині, так і по висоті. ( довідник )
  4. Карти загортаються в поздовжньому напрямку, але не в широтному напрямку.

6
відмінна відповідь, це повинно бути голосом, який проголосується, оскільки припадає на довготу та широту. Працював ідеально поки що.
Пітер Вустер

1
@John S - Це фантастичне рішення, і я задумуюсь скористатися цим методом у власних картах google Maps fitBounds, доступним і мені. Я помітив, що fitBounds - це іноді один масштаб назад (зменшений), але я припускаю, що це з додавання, яке додає. Єдина відмінність цього методу від fitBounds - лише кількість прокладки, яку ви хочете додати, який пояснює зміну рівня збільшення між ними?
Johntrepreneur

1
@johntrepreneur - Перевага №1: Ви можете використовувати цей метод перед тим, як навіть створити карту, щоб ви могли забезпечити його результат початковими налаштуваннями карти. З fitBounds, вам потрібно створити карту, а потім дочекатися події "bounds_changed".
Джон С

1
@ MarianPaździoch - Це дійсно працює за ці межі, дивіться тут . Очікуєте, що вам вдасться збільшити масштаб, щоб ці точки опинились у точних кутах карти? Це неможливо, оскільки рівні масштабування є цілими значеннями. Функція повертає найвищий рівень масштабування, який буде включати цілі межі на карті.
Джон С

1
@CarlMeyer - я не згадую це у своїй відповіді, але в коментарі вище я зазначаю, що однією з переваг цієї функції є "Ви можете використовувати цей метод, перш ніж навіть створити карту". Використання map.getProjection()дозволить виключити частину математики (і припущення про проекцію), але це означало б, що функцію не можна було викликати, поки після створення карти і події "projection_changed" не буде запущено.
Джон С

39

Для версії 3 API це просто і працює:

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

і кілька необов’язкових прийомів:

//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
чому б не встановити мінімальний рівень збільшення під час ініціалізації карти, щось на зразок: var mapOptions = {maxZoom: 15,};
Куш

2
@ Куш, хороший момент. але maxZoomперешкоджає користувачеві вручну масштабувати. Мій приклад змінює лише DefaultZoom і лише за необхідності.
d.raev

1
коли ви робите fitBounds, він просто стрибає, щоб підходити до меж, а не анімувати там із поточного подання. дивним рішенням є використання вже згаданих getBoundsZoomLevel. таким чином, коли ви викликаєте setZoom, він анімує бажаний рівень масштабування. звідти це не проблема зробити panTo, і ви закінчите прекрасну анімаційну карту, яка підходить до меж
user151496

анімація не там, де обговорюється в питанні, ні в моїй відповіді. Якщо у вас є корисний приклад по темі, просто створіть конструктивну відповідь із прикладом, як і коли це можна використовувати.
d.raev

Чомусь карта Google не збільшує масштаб, коли викликає setZoom () відразу після дзвінка map.fitBounds (). (gmaps зараз v3.25)
kashiraja

4

Ось варіант функції Котліна:

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

Дякую, що мені дуже допомогло у пошуку найбільш підходящого коефіцієнта збільшення для правильного відображення полілінії. Я знаходжу максимальну і мінімальну координати серед точок, які мені потрібно відстежувати, і, якщо шлях дуже "вертикальний", я лише додав кілька рядків коду:

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

Власне, ідеальний коефіцієнт збільшення - зумфактор-1.


Мені сподобалось var zoomfactor = Math.floor(Math.log(960 * 360 / angle / GLOBE_WIDTH) / Math.LN2)-1;. Все-таки дуже корисно.
Mojowen

1

Оскільки всі інші відповіді, мабуть, мають для мене проблеми з тими чи іншими наборами обставин (ширина / висота карти, ширина / висота меж тощо), я зрозумів, що я поставлю свою відповідь тут ...

Тут був дуже корисний файл JavaScript: http://www.polyarc.us/adjust.js

Я використовував це як основу для цього:

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

};

Ви, звичайно, можете це очистити чи мінімізувати, якщо це потрібно, але я тривалий час зберігав імена змінних, щоб спростити розуміння.

Якщо вам цікаво, звідки взявся OFFSET, очевидно, 268435456 - це половина окружності Землі в пікселях при рівні масштабу 21 (згідно з http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google -карти ).


0

Валеріо майже прав у своєму рішенні, але є якась логічна помилка.

спершу потрібно перевірити, чи кут2 більший за кут, перед тим як додати 360 у від’ємник.

інакше у вас завжди більше значення, ніж кут

Тож правильне рішення:

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;

Дельта є, тому що у мене більша ширина, ніж висота.


0

map.getBounds()це не миттєва операція, тому я використовую в подібному випадку обробник подій. Ось мій приклад у Coffeescript

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

0

Приклад роботи для пошуку середнього центру за замовчуванням за допомогою карт react-google на 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 добре працює для мене. Якщо ви хочете обчислити коефіцієнт збільшення для широти, це просте рішення, яке добре працює:

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);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.