Skip to main content
 首页 » 编程设计

javascript之计算 fitBounds() 为给定边界和 map 大小返回的视口(viewport)

2025年05月04日27三少

我有一个包含服务器端和客户端组件的 Web 应用程序。我使用 Google Maps HTTP API 和 Google Maps Javascript API 在双方执行相同的地理编码请求。

对于给定的查询,这两个 API 都使用相同的视口(viewport)进行响应,这一切都很好。

我的问题是我在客户端的给定视口(viewport)上使用方法 map.fitBounds() 来缩放和平移 map 以包含视口(viewport)。问题是我想在服务器端执行相同的操作,以便尽快为客户端提供适当的数据。

目前的做法是在客户端渲染 map ,然后将渲染 map 的实际视口(viewport)发送到服务器端进行计算。我的目标是删除此步骤并在客户端开始渲染 map 之前执行计算。

This JSBin ,在 Bounds 1 和 Bounds 3 中显示我在说什么。当调用 fitBounds() 时, map 会缩放到仍然包含所有矩形的最高级别(根据我的研究,边界周围还有 45 像素的边距)。

此实际视口(viewport) 的地理坐标正是我要查找的内容。

在此SO answer , John-S给出了一种计算给定维度中 map 的最高可能缩放级别的方法。

我认为这应该可以修改以满足我的需要,但我不确定如何修改,我的墨卡托投影技能很遗憾地缺乏。

该函数应采用边界(sw,ne 坐标对)和 map div 的大小,并返回 map.fitBounds< 的地理边界 会返回。

请您参考如下方法:

我终于根据链接的答案得出了这段代码,它提供了计算缩放级别和适当视口(viewport)的功能。

from math import radians, cos, sin, atan2, sqrt, pow, pi, log, floor, tan, atan, exp 
 
 
class Coordinate(object): 
    def __init__(self, lat, lng): 
        """ 
        Represents a coordinate with decimal latitude and longitude in degrees 
        :type lat: float 
        :type lng: float 
        """ 
        self.lat = lat 
        self.lng = lng 
 
    def in_rectangle(self, box): 
        """ 
        :param box: Box 
        :return: True or False 
        """ 
        return (box.south_west.lat <= self.lat <= box.north_east.lat) and \ 
               (box.south_west.lng <= self.lng <= box.north_east.lng) 
 
    def distance_to_coordinate(self, another_coordinate): 
        # convert decimal degrees to radians 
        """ 
        Calculates the distance between the coordinate and the supplied coordinate. Based on the Haversine formula. 
        Returns the value in meters. 
        :param another_coordinate: Coordinate 
        :return: float 
        """ 
        lon1, lat1, lon2, lat2 = map(radians, [self.lng, self.lat, another_coordinate.lng, another_coordinate.lat]) 
        # haversine formula 
        delta_lon = lon2 - lon1 
        delta_lat = lat2 - lat1 
        a = sin(delta_lat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(delta_lon / 2) ** 2 
        c = 2 * atan2(sqrt(a), sqrt(1-a)) 
        km = 6378100 * c 
        return km 
 
 
class Box(object): 
    def __init__(self, south_west, north_east): 
        """ 
        Represents a rectangle on the sphere with the south west and north east corners. 
        :type north_east: Coordinate 
        :type south_west: Coordinate 
        """ 
        self.north_east = north_east 
        self.south_west = south_west 
 
    def center(self): 
        """ 
        Calculates the center point of the box 
        :rtype : Coordinate 
        """ 
        lat_center = self.south_west.lat + (self.north_east.lat-self.south_west.lat)/2 
        lng_center = self.south_west.lng + (self.north_east.lng-self.south_west.lng)/2 
        return Coordinate(lat_center, lng_center) 
 
 
def max_zoom_level_to_fit_box_in_element(box, element_width, element_height): 
    """ 
    Calculates the maximum zoom level that would fit box in a map element with the specified width and height 
    :type box: Box 
    :type element_height: float 
    :type element_width: float 
    """ 
    WORLD_HEIGHT = 256 
    WORLD_WIDTH = 256 
    ZOOM_MAX = 21 
    ne = box.north_east 
    sw = box.south_west 
 
    lat_fraction = (_lat_to_rad(ne.lat) - _lat_to_rad(sw.lat)) / pi 
    lng_delta = ne.lng - sw.lng 
    lng_fraction = ((lng_delta+360) if lng_delta < 0 else lng_delta)/360 
 
    lat_zoom = _zoom(element_height, WORLD_HEIGHT, lat_fraction) 
    lng_zoom = _zoom(element_width, WORLD_WIDTH, lng_fraction) 
 
    return min(lat_zoom, lng_zoom, ZOOM_MAX) 
 
 
def viewport_for_box_in_element(box, element_width, element_height, zoom): 
    """ 
    Calculates the viewport, rectangle, which will fit box within it for a given map size and zoom level. 
    The return value is a box, with coordinates on the sphere. 
    :param box: Box 
    :param element_width: float 
    :param element_height: float 
    :param zoom: float 
    :return: Box 
    """ 
    center_x, center_y = _lat_and_lng_to_x_and_y_for_zoom(box.center().lat, box.center().lng, zoom) 
 
    viewport_sw_x, viewport_sw_y = center_x - element_width/2, center_y+element_height/2 
    viewport_ne_x, viewport_ne_y = center_x + element_width/2, center_y-element_height/2 
 
    viewport_sw = _x_and_y_to_lat_and_lng_for_zoom(viewport_sw_x, viewport_sw_y, zoom) 
    viewport_ne = _x_and_y_to_lat_and_lng_for_zoom(viewport_ne_x, viewport_ne_y, zoom) 
 
    return Box(viewport_sw, viewport_ne) 
 
 
def _lat_and_lng_to_x_and_y_for_zoom(lat, lng, zoom): 
    """ 
    Converts decimal degree coordinates on the sphere to x and y points on the flat Web Mercator projection, for a 
    given zoom level. 
    :param lat: float 
    :param lng: float 
    :param zoom: float 
    :return: x,y: float 
    """ 
    lat_rad = lat * (pi/180) 
    lng_rad = lng * (pi/180) 
    x = (128/pi)*pow(2, zoom)*(lng_rad + pi) 
    y = (128/pi)*pow(2, zoom)*(pi-log(tan(pi/4+lat_rad/2))) 
    return x, y 
 
def _x_and_y_to_lat_and_lng_for_zoom(x, y, zoom): 
    """ 
    Converts x and y points on the flat Web Mercator projection, for a given zoom level, to decimal degrees on the 
    spehere. 
    :param x: float 
    :param y: float 
    :param zoom: float 
    :return: Coordinate 
    """ 
    lng_rad = (x*pi)/(pow(2, zoom)*128)-pi 
    lat_rad = 2*atan(exp(pi-((y*pi)/(pow(2, zoom)*128))))-pi/2 
    return Coordinate(lat_rad*(180/pi), lng_rad*(180/pi)) 
 
def _lat_to_rad(lat): 
    """ 
    Converts a decimal degree latitude to radians, given the fact how Web Mercator projection wraps on the latitude. 
    :param lat: float 
    :return: float 
    """ 
    _sin = sin(lat * pi / 180) 
    rad_x2 = log((1 + _sin) / (1 - _sin)) / 2 
    return max(min(rad_x2, pi), -pi) / 2 
 
def _zoom(map_px, world_px, fraction): 
    """ 
    Calculates a zoom level 
    :param map_px: float 
    :param world_px: float 
    :param fraction: float 
    :return: 
    """ 
    ln_2 = log(2) 
    return floor(log(map_px / world_px / fraction) / ln_2)