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


141

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

4
Последний пример отличный и очень наглядный! +1. У меня такая же проблема .
TMS

Извините, связан неправильный вопрос, это правильная ссылка .
TMS

Этот вопрос не дублирует другой вопрос . Ответ на другой вопрос - использовать fitBounds(). В этом вопросе спрашивается, что делать, когда этого fitBounds()недостаточно - либо из-за чрезмерного увеличения, либо из-за того, что вы не хотите увеличивать масштаб (т.е. вам просто нужен уровень масштабирования).
John S

@ Ник Кларк: Откуда вы знаете, что границы sw, ne должны быть установлены? Как вы их раньше ловили?
varaprakash

Ответы:


111

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

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

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

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

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

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

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
@Pete хороший замечание, в общем, вы, вероятно, захотите округлить уровень масштабирования, чтобы вы поместили на карту немного больше, чем нужно, а не немного меньше. Я использовал Math.round, потому что в ситуации OP значение перед округлением должно быть приблизительно целым.
Giles Gardam

23
каково значение pixelWidth
Альберт Джегани,

285

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

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

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

Параметры:

Значение параметра "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. (я считаю, что он был только 19 для Google Maps API v2.)


Пояснение:

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

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

Прочие примечания:

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

7
отличный ответ, это должно быть верхнее голосование, так как оно учитывает как долготу, так и широту. Пока работает отлично.
Питер Вустер

1
@John S - это фантастическое решение, и я подумываю использовать его поверх доступного мне собственного метода fitBounds карт Google. Я заметил, что fitBounds иногда возвращается на один уровень масштабирования (уменьшено), но я предполагаю, что это связано с добавлением отступа. Является ли единственная разница между этим методом и методом fitBounds, просто количество отступов, которое вы хотите добавить, которое учитывает изменение уровня масштабирования между ними?
johntrepreneur

1
@johntrepreneur - Преимущество №1: вы можете использовать этот метод еще до того, как создадите карту, чтобы вы могли предоставить его результат в исходные настройки карты. С помощью fitBoundsвам нужно создать карту, а затем дождаться события «bounds_changed».
John S

1
@ MarianPaździoch - Это действительно работает в этих пределах, см. Здесь . Ожидаете ли вы, что сможете увеличивать масштаб так, чтобы эти точки находились в точных углах карты? Это невозможно, поскольку уровни масштабирования являются целыми числами. Функция возвращает наивысший уровень масштабирования, который по-прежнему будет включать все границы карты.
John S

1
@CarlMeyer - я не упоминаю об этом в своем ответе, но в комментарии выше я заявляю, что одним из преимуществ этой функции является «Вы можете использовать этот метод еще до того, как создадите карту». Использование map.getProjection()устранило бы часть математики (и предположение о проекции), но это означало бы, что функция не может быть вызвана до тех пор, пока карта не будет создана и не сработает событие "projection_changed".
John S

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
@Kush, хорошее замечание. но maxZoomне позволит пользователю изменять масштаб вручную . Мой пример изменяет только DefaultZoom и только если это необходимо.
d.raev

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

Анимация не обсуждается ни в вопросе, ни в моем ответе. Если у вас есть полезный пример по теме, просто дайте конструктивный ответ с примером и указанием того, как и когда его можно использовать.
d.raev

По какой-то причине карта Google не масштабируется при вызове setZoom () сразу после вызова map.fitBounds (). (на данный момент gmaps - v3.25)
kashiraja 07

4

Вот версия функции Kotlin:

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

Фактически, идеальный коэффициент масштабирования - zoomfactor-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;
   }

};

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

Если вам интересно, откуда взялось смещение, очевидно, 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-maps 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

Расчет уровня масштабирования для долготы Джайлза Гардама у меня отлично работает. Если вы хотите рассчитать коэффициент масштабирования для широты, это простое решение, которое отлично работает:

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

0

Для быстрой версии

func getBoundsZoomLevel(bounds: GMSCoordinateBounds, mapDim: CGSize) -> Double {
        var bounds = bounds
        let WORLD_DIM = CGSize(width: 256, height: 256)
        let ZOOM_MAX: Double = 21.0
        func latRad(_ lat: Double) -> Double {
            let sin2 = sin(lat * .pi / 180)
            let radX2 = log10((1 + sin2) / (1 - sin2)) / 2
            return max(min(radX2, .pi), -.pi) / 2
        }
        func zoom(_ mapPx: CGFloat,_ worldPx: CGFloat,_ fraction: Double) -> Double {
            return floor(log10(Double(mapPx) / Double(worldPx) / fraction / log10(2.0)))
        }
        let ne = bounds.northEast
        let sw = bounds.southWest
        let latFraction = (latRad(ne.latitude) - latRad(sw.latitude)) / .pi
        let lngDiff = ne.longitude - sw.longitude
        let lngFraction = lngDiff < 0 ? (lngDiff + 360) : (lngDiff / 360)
        let latZoom = zoom(mapDim.height, WORLD_DIM.height, latFraction);
        let lngZoom = zoom(mapDim.width, WORLD_DIM.width, lngFraction);
        return min(latZoom, lngZoom, ZOOM_MAX)
    }

0

Версия Дротика:

  double latRad(double lat) {
    final double sin = math.sin(lat * math.pi / 180);
    final double radX2 = math.log((1 + sin) / (1 - sin)) / 2;
    return math.max(math.min(radX2, math.pi), -math.pi) / 2;
  }

  double getMapBoundZoom(LatLngBounds bounds, double mapWidth, double mapHeight) {
    final LatLng northEast = bounds.northEast;
    final LatLng southWest = bounds.southWest;

    final double latFraction = (latRad(northEast.latitude) - latRad(southWest.latitude)) / math.pi;

    final double lngDiff = northEast.longitude - southWest.longitude;
    final double lngFraction = ((lngDiff < 0) ? (lngDiff + 360) : lngDiff) / 360;

    final double latZoom = (math.log(mapHeight / 256 / latFraction) / math.ln2).floorToDouble();
    final double lngZoom = (math.log(mapWidth / 256 / lngFraction) / math.ln2).floorToDouble();

    return math.min(latZoom, lngZoom);
  }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.