【问题标题】:Google Maps V3 - How to calculate the zoom level for a given boundsGoogle Maps V3 - 如何计算给定边界的缩放级别
【发布时间】:2011-08-28 06:49:40
【问题描述】:

我正在寻找一种方法来使用 Google Maps V3 API 计算给定边界的缩放级别,类似于 V2 API 中的getBoundsZoomLevel()

这是我想做的:

// 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。我有same problem
  • 对不起,链接错误的问题,this is the correct link
  • 这个问题不是the other question的重复问题。另一个问题的答案是使用fitBounds()。这个问题询问当 fitBounds() 不足时该怎么办 - 因为它过度缩放或者您不想缩放(即,您只想要缩放级别)。
  • @Nick Clark:你怎么知道要设置的 sw、ne 边界?您之前是如何捕获它们的?

标签: google-maps google-maps-api-3


【解决方案1】:

这里是函数的 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) / 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)
    }

【讨论】:

    【解决方案2】:

    高度赞成的答案都没有对我有用。他们抛出了各种未定义的错误,最终计算了角度的 inf/nan。我怀疑 LatLngBounds 的行为可能随着时间而改变。无论如何,我发现这段代码可以满足我的需要,也许它可以帮助某人:

    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 getZoom(lat_a, lng_a, lat_b, lng_b) {
    
          let latDif = Math.abs(latRad(lat_a) - latRad(lat_b))
          let lngDif = Math.abs(lng_a - lng_b)
    
          let latFrac = latDif / Math.PI 
          let lngFrac = lngDif / 360 
    
          let lngZoom = Math.log(1/latFrac) / Math.log(2)
          let latZoom = Math.log(1/lngFrac) / Math.log(2)
    
          return Math.min(lngZoom, latZoom)
    
    }
    

    【讨论】:

      【解决方案3】:

      飞镖版本:

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

      【讨论】:

      • 谢谢,我在找这个。
      【解决方案4】:

      快速版本

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

      【讨论】:

        【解决方案5】:

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

        【讨论】:

          【解决方案6】:

          对于 API 的第 3 版,这很简单且有效:

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

          【讨论】:

          • 为什么不在初始化地图时设置最小缩放级别,例如: var mapOptions = { maxZoom: 15, };
          • @Kush,说得好。但maxZoom 会阻止用户手动缩放。我的示例仅更改 DefaultZoom 并且仅在必要时更改。
          • 当您执行 fitBounds 时,它只是跳转以适应边界,而不是从当前视图中进行动画处理。很棒的解决方案是使用已经提到的getBoundsZoomLevel。这样,当您调用 setZoom 时,它会动画到所需的缩放级别。从那里开始进行 panTo 不是问题,您最终会得到一个适合边界的漂亮地图动画
          • animation 在问题和我的回答中都没有讨论。如果您有关于该主题的有用示例,只需创建一个建设性的答案,包括示例以及如何以及何时可以使用它。
          • 由于某种原因,在 map.fitBounds() 调用后立即调用 setZoom() 时,Google 地图不会缩放。 (gmaps 目前是 v3.25)
          【解决方案7】:

          ES6 上使用 react-google-maps 查找平均默认中心的工作示例:

          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>
          

          【讨论】:

            【解决方案8】:

            感谢 Giles Gardam 的回答,但它只涉及经度而不是纬度。一个完整的解决方案应该计算纬度所需的缩放级别和经度所需的缩放级别,然后取两者中较小的(更远的)。

            这是一个同时使用纬度和经度的函数:

            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”参数值应该是一个具有“height”和“width”属性的对象,这些属性表示显示地图的 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 地图使用墨卡托投影。在墨卡托投影中,经线等距分布,但纬线不等距。纬线之间的距离随着它们从赤道到两极而增加。事实上,当它到达两极时,距离趋于无穷大。然而,谷歌地图不显示纬度高于约北 85 度或低于约 -85 度南。 (reference) (我计算的实际截止值为 +/-85.05112877980658 度。)

            这使得纬度边界分数的计算比经度更复杂。我使用formula from Wikipedia 来计算纬度分数。我假设这与谷歌地图使用的投影相匹配。毕竟,我在上面链接到的 Google 地图文档页面包含指向同一 Wikipedia 页面的链接。

            其他说明:

            1. 缩放级别范围从 0 到最大缩放级别。缩放级别 0 是地图完全缩小。更高的级别进一步放大地图。 (reference)
            2. 在缩放级别 0 下,整个世界可以显示在 256 x 256 像素的区域中。 (reference)
            3. 对于每个更高的缩放级别,显示相同区域所需的像素数在宽度和高度上都会翻倍。 (reference)
            4. 地图沿纵向环绕,但不会沿纬度方向环绕。

            【讨论】:

            • 很好的答案,这应该是投票最高的,因为它同时考虑了经度和纬度。到目前为止工作完美。
            • @John S - 这是一个很棒的解决方案,我正在考虑在我也可以使用的原生谷歌地图 fitBounds 方法上使用它。我注意到 fitBounds 有时是一个缩放级别(缩小),但我认为这是它添加的填充。那么这个方法和 fitBounds 方法之间的唯一区别是你想要添加的填充量,它解释了两者之间缩放级别的变化吗?
            • @johntrepreneur - 优势#1:您甚至可以在创建地图之前使用此方法,因此您可以将其结果提供给初始地图设置。使用fitBounds,您需要创建地图,然后等待“bounds_changed”事件。
            • @MarianPaździoch - 它确实适用于该范围,see here。您是否希望能够缩放以使这些点位于地图的确切角落?这是不可能的,因为缩放级别是整数值。该函数返回仍将包括地图上整个边界的最高缩放级别。
            • @CarlMeyer - 我在回答中没有提到它,但在上面的评论中,我声明此功能的一个优点是“您甚至可以在创建地图之前使用此方法。”使用map.getProjection() 会消除一些数学运算(以及关于投影的假设),但这意味着在创建地图并触发“projection_changed”事件之前无法调用该函数。
            【解决方案9】:

            由于所有其他答案似乎对我来说在一种或另一种情况下(地图宽度/高度、边界宽度/高度等)都有问题。我想我会把我的答案放在这里...

            这里有一个非常有用的 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-maps)。

            【讨论】:

              【解决方案10】:

              map.getBounds() 不是瞬时操作,所以我在类似的情况下使用事件处理程序。这是我在 Coffeescript 中的示例

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

              【讨论】:

                【解决方案11】:

                Valerio 的解决方案几乎是正确的,但存在一些逻辑错误。

                你必须先检查 angle2 是否大于 angle,然后在负数处添加 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 在那里,因为我的宽度大于高度。

                【讨论】:

                  【解决方案12】:

                  谢谢,这对我找到最合适的缩放系数以正确显示折线帮助很大。 我找到了我必须跟踪的点之间的最大和最小坐标,如果路径非常“垂直”,我只添加了几行代码:

                  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;。不过,非常有帮助。
                  【解决方案13】:

                  有人在 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);
                  

                  【讨论】:

                  • 您不必对 lat 重复此操作,然后选择 2 结果的最小值吗?我认为这不适用于高窄的界限.....
                  • 将 Math.round 更改为 Math.floor 对我来说非常有用。谢谢一百万。
                  • 如果不考虑纬度,这怎么可能是正确的?在赤道附近应该没问题,但是地图在给定缩放级别下的比例会根据纬度而变化!
                  • @Pete 好点,一般来说,您可能希望缩小缩放级别,以便您在地图中的适应程度比预期的多一点,而不是少一点。我使用 Math.round 是因为在 OP 的情况下,舍入前的值应该是近似整数。
                  • pixelWidth 的值是多少
                  猜你喜欢
                  • 2011-04-16
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-04-14
                  • 1970-01-01
                  • 2014-03-18
                  • 2011-09-12
                  • 2015-11-03
                  • 2013-04-30
                  相关资源
                  最近更新 更多