Google Maps V3 - Как рассчитать уровень масштабирования для заданных границ

Я ищу способ рассчитать уровень масштабирования для заданных границ с помощью API Google Maps 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

Ответ 1

Аналогичный вопрос задан в группе Google: http://groups.google.com/group/google-maps-js-api-v3/browse_thread/thread/e6448fc197c3c892

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

Другой проблемой является отношение длины сторон, например. вы не можете точно соответствовать границам тонкому прямоугольнику внутри квадратной карты.

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

Если вам действительно нужно рассчитать масштаб, а не сохранить его, это должно сделать трюк:

Проекция Меркатора делит широту, но любая разница в долготе всегда представляет собой ту же долю ширины карты (разность углов в градусах /360). При масштабировании нуля карта всего мира имеет размер 256x256 пикселей, а масштабирование каждого уровня удваивает ширину и высоту. Поэтому после небольшой алгебры мы можем вычислить масштаб следующим образом, если мы знаем ширину карты в пикселях. Обратите внимание, что, поскольку долгота обертывается, мы должны убедиться, что угол положительный.

var GLOBE_WIDTH = 256; // a constant in Google 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);

Ответ 2

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

Вот функция, которая использует как широту, так и долготу:

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

Демо на jsfiddle

Параметры:

Значение параметра "bounds" должно быть google.maps.LatLngBounds.

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

Если вы используете библиотеку jQuery, значение mapDim можно получить следующим образом:

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

Если вы используете библиотеку Prototype, значение mapDim может быть получено следующим образом:

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

Возвращаемое значение:

Возвращаемое значение - это максимальный уровень масштабирования, который будет отображать все границы. Это значение будет находиться между 0 и максимальным уровнем масштабирования включительно.

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


Пояснение:

Google Maps использует проекцию Меркатора. В проекции Меркатора линии долготы одинаково разнесены, но линий широты нет. Расстояние между линиями широты возрастает по мере того, как они идут от экватора к полюсам. Фактически расстояние стремится к бесконечности, когда оно достигает полюсов. Однако карта Google Maps не показывает широты выше 85 градусов северного или ниже -85 градусов южнее. (ссылка) (я вычисляю фактическое обрезание на +/- 85.05112877980658 градусов.)

Это делает вычисления фракций для границ более сложными для широты, чем для долготы. Я использовал формулу из Википедии для вычисления широты. Я предполагаю, что это соответствует прогнозу, используемому Картами Google. В конце концов, на странице документации Google Maps, на которую я ссылаюсь выше, есть ссылка на ту же страницу Википедии.

Другие примечания:

  • Уровни масштабирования варьируются от 0 до максимального уровня масштабирования. Уровень масштабирования 0 - это карта, полностью уменьшенная. Более высокие уровни приближают карту. (ссылка)
  • При уровне масштабирования 0 весь мир может отображаться в области размером 256 х 256 пикселей. (ссылка)
  • Для каждого более высокого уровня увеличения количество пикселей, необходимых для отображения одной и той же области, удваивается как по ширине, так и по высоте. (ссылка)
  • Карты переносятся в продольном направлении, но не в широтном направлении.

Ответ 3

Для версии 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);
}

Ответ 4

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

var GLOBE_WIDTH = 256; // a constant in Google 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);

На самом деле идеальным коэффициентом масштабирования является zoomfactor-1.

Ответ 5

Поскольку у всех остальных ответов есть проблемы для меня с тем или иным набором обстоятельств (ширина/высота карты, ширина/высота границ и т.д.), я подумал, что поставил бы здесь свой ответ...

Здесь был очень полезный файл 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;
   }

};

Вы можете, конечно, очистить или минимизировать его, если это необходимо, но я долго сохранил имена переменных, чтобы упростить их понимание.

Если вам интересно, откуда пришел СМЕЩЕНИЕ, по-видимому, 268435456 - половина окружности Земли в пикселях на уровне масштабирования 21 (согласно http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps).

Ответ 6

Валерио почти прав с его решением, но есть некоторая логическая ошибка.

вы должны сначала проверить, что угол угла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;

Delta есть, потому что у меня большая ширина, чем высота.

Ответ 7

map.getBounds() не является мгновенной операцией, поэтому я использую в подобном обработчике событий case. Вот мой пример в Coffeescript

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