Plotly自动缩放"Mapbox maps"。

11
在Plotly网站上,描述了如何在Python中自动缩放“Geo map”的《地图配置与样式》。
import plotly.express as px

fig = px.line_geo(lat=[0,15,20,35], lon=[5,10,25,30])            # Creates a "Geo map" figure
fig.update_geos(fitbounds="locations")                           # Automatic Zooming !!!!
fig.show()

这样做是可行的,而且如果我在“Mapbox地图”上尝试同样的操作,它不会应用自动缩放:

fig = px.scatter_mapbox(filtered_df, lat="latitude", lon="longitude", color="ID")  # Creates a "Mapbox map" figure
fig.update_layout(mapbox_style="open-street-map")
fig.update_geos(fitbounds="locations")                                             # Automatic Zooming not working!!!

Python中的Mapbox地图层中没有如何执行此操作的信息。

为了使您的代码可重现,您能否包含 filtered_df - Derek O
4个回答

9

Mapbox API文档显示缩放级别基本上是对数比例尺。因此,经过一些试验,以下函数适用于我:

max_bound = max(abs(x1-x2), abs(y1-y2)) * 111
zoom = 11.5 - np.log(max_bound)

注意:

  • 在这个例子中,xy(经/纬度)坐标是以十进制度为单位的。
  • 111是将十进制度转换为公里的常数。
  • 11.5的值适合我所需的缩放/裁剪级别,但我先尝试了10-12之间的值。

在小规模(地图约20公里宽)上工作,我使用15而不是11.5。 很好的解决方法!谢谢。 - Alex Poca

7

我写了一个函数和其他 geojson 兼容函数一起,放在 rv_geojson.py 文件中。

该函数接受位置列表并查找矩形绑定框的几何高度和宽度,适用于使用墨卡托投影。 它返回缩放比例和中心点。

def zoom_center(lons: tuple=None, lats: tuple=None, lonlats: tuple=None,
        format: str='lonlat', projection: str='mercator',
        width_to_height: float=2.0) -> (float, dict):
    """Finds optimal zoom and centering for a plotly mapbox.
    Must be passed (lons & lats) or lonlats.
    Temporary solution awaiting official implementation, see:
    https://github.com/plotly/plotly.js/issues/3434
    
    Parameters
    --------
    lons: tuple, optional, longitude component of each location
    lats: tuple, optional, latitude component of each location
    lonlats: tuple, optional, gps locations
    format: str, specifying the order of longitud and latitude dimensions,
        expected values: 'lonlat' or 'latlon', only used if passed lonlats
    projection: str, only accepting 'mercator' at the moment,
        raises `NotImplementedError` if other is passed
    width_to_height: float, expected ratio of final graph's with to height,
        used to select the constrained axis.
    
    Returns
    --------
    zoom: float, from 1 to 20
    center: dict, gps position with 'lon' and 'lat' keys

    >>> print(zoom_center((-109.031387, -103.385460),
    ...     (25.587101, 31.784620)))
    (5.75, {'lon': -106.208423, 'lat': 28.685861})
    """
    if lons is None and lats is None:
        if isinstance(lonlats, tuple):
            lons, lats = zip(*lonlats)
        else:
            raise ValueError(
                'Must pass lons & lats or lonlats'
            )
    
    maxlon, minlon = max(lons), min(lons)
    maxlat, minlat = max(lats), min(lats)
    center = {
        'lon': round((maxlon + minlon) / 2, 6),
        'lat': round((maxlat + minlat) / 2, 6)
    }
    
    # longitudinal range by zoom level (20 to 1)
    # in degrees, if centered at equator
    lon_zoom_range = np.array([
        0.0007, 0.0014, 0.003, 0.006, 0.012, 0.024, 0.048, 0.096,
        0.192, 0.3712, 0.768, 1.536, 3.072, 6.144, 11.8784, 23.7568,
        47.5136, 98.304, 190.0544, 360.0
    ])
    
    if projection == 'mercator':
        margin = 1.2
        height = (maxlat - minlat) * margin * width_to_height
        width = (maxlon - minlon) * margin
        lon_zoom = np.interp(width , lon_zoom_range, range(20, 0, -1))
        lat_zoom = np.interp(height, lon_zoom_range, range(20, 0, -1))
        zoom = round(min(lon_zoom, lat_zoom), 2)
    else:
        raise NotImplementedError(
            f'{projection} projection is not implemented'
        )
    
    return zoom, center

将其用作

zoom, center = zoom_center(
    lons=[5, 10, 25, 30],
    lats=[0, 15, 20, 35]
)
fig = px.scatter_mapbox(
    filtered_df, lat="latitude", lon="longitude", color="ID",
    zoom=zoom, center=center
)  # Creates a "Mapbox map" figure


0
这些解决方案仅在某种程度上有效,问题在于像素大小会随纬度变化。因此,如果您需要通用解决方案(且无法与mapbox交流),我使用了opencv在“测试图像”上找到最大和最小的纬度和经度点,然后从最大缩放级别(有时为22或直接跳过到20)到最小缩放级别0计算y轴上的像素长度和x轴上的像素长度。
这是使用粉色和橙色标出的最大和最小纬度和经度点生成的示例“测试图像”: enter image description here 这是一个带注释地图的图像: enter image description here
    def get_marker_location(self, color_min, color_max, hsv):
        # cv2.imshow("hsv", hsv)
        # cv2.waitKey(0)
        mask = cv2.inRange(hsv, color_min, color_max)
        # cv2.imshow("Image", mask)
        # cv2.waitKey(0)
        if cv2.countNonZero(mask) == 0:
            return False, None, None
        cnts, hierarchies = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

        for i in cnts:
            M = cv2.moments(i)
            if M['m00'] != 0:
                cX = int(M["m10"] / M["m00"])
                cY = int(M["m01"] / M["m00"])

                return True, cX, cY


    def get_zoom(self, lons, lats, height, width):
        fig = go.Figure()

        # Get max min long lat per owner
        maxlon, minlon = np.amax(lons), np.amin(lons)
        maxlat, minlat = np.amax(lats), np.amin(lats)

        center = {
            'lon': round((maxlon + minlon) / 2, 6),
            'lat': round((maxlat + minlat) / 2, 6)
        }

        # Place each on map in different colors
        # Pink ff00ff hsv(300,100,100)
        pink_min = (147,219,208)
        pink_max = (153,255,255)
        fig.add_trace(go.Scattermapbox(lon=[minlon], lat=[minlat], 
                                        mode='markers', 
                                        marker=dict(color='#FF00FF')))
        # Oraange ff6000 hsv(23,100,100)
        orange_min = np.array([10,230,236])
        orange_max = np.array([12,255,255])
        fig.add_trace(go.Scattermapbox(lon=[maxlon], lat=[maxlat], 
                                        mode='markers', 
                                        marker=dict(color='#FF6000')))

        # Change start number for small area, skip if longer
        start_zoom = 20
        # extra_zoom_threshold = 0.00762 # 25ft in km
        extra_zoom_threshold = 0.01524 # 50ft in km
        max_min_dist = gpd.geodesic(gp.Point(minlat, minlon),gp.Point(maxlat, maxlon)).km
        print(f'max_min_dist: {max_min_dist}')
        print(f'extra_zoom_threshold: {extra_zoom_threshold}')
        if extra_zoom_threshold > max_min_dist:
            start_zoom = 22

        for i in range(start_zoom, -1, -1):
            # Make images
            fig.update_layout(template='plotly_white', 
                                showlegend=False, height=height, width=width, 
                                margin={'l': 0,'r': 0,'b': 0,'t': 0,},
                                mapbox={'style':'carto-positron', 
                                        'zoom':i,
                                        'center': {'lon': center['lon'], 'lat': center['lat']}})
            fig.write_image("check_zoom.png")

            # Check for colors
            img = cv2.imread('check_zoom.png')
            hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
            pink_loc = dict()
            orange_loc = dict()
            # min
            pink_loc['in_image'], pink_loc['cX'], pink_loc['cY'] = self.get_marker_location(pink_min, pink_max, hsv)
            # max
            orange_loc['in_image'], orange_loc['cX'], orange_loc['cY'] = self.get_marker_location(orange_min, orange_max, hsv)
            # x = 1/0

            if (pink_loc['in_image'] and orange_loc['in_image']) or i == 0:
                # Compute Long and Lat pixel lengths
                # Calc lon lat distance / number of pixels
                pixel_len_lonx = gpd.geodesic(gp.Point(center['lat'], minlon),gp.Point(center['lat'], maxlon)).km / abs(orange_loc['cX'] - pink_loc['cX'])
                pixel_len_laty = gpd.geodesic(gp.Point(minlat, center['lon']),gp.Point(maxlat, center['lon'])).km / abs(orange_loc['cY'] - pink_loc['cY'])
                
                dy = (height/2)*pixel_len_laty # km
                dx = (width/2)*pixel_len_lonx # km
                
                max_lat = gpd.geodesic(kilometers=dy).destination(gp.Point(center['lat'], center['lon']), 0)[0]
                min_lat = gpd.geodesic(kilometers=dy).destination(gp.Point(center['lat'], center['lon']), 180)[0]
                max_lon = gpd.geodesic(kilometers=dx).destination(gp.Point(center['lat'], center['lon']), 90)[1]
                min_lon = gpd.geodesic(kilometers=dx).destination(gp.Point(center['lat'], center['lon']), 270)[1]

                return i, center, max_lat, min_lat, max_lon, min_lon

    def get_mapbox_site_map(self):
        # generate site map
        fig = go.Figure()

        height = 800
        width = 800
        annotation_counter = 0

        if len(self.lines_df):
            l_lat = np.concatenate(self.lines_df['lines'].apply(lambda g: [c[0] for c in g] + [None]).values)
            l_lon = np.concatenate(self.lines_df['lines'].apply(lambda g: [c[1] for c in g] + [None]).values)
        if len(self.gates_df):
            g_lat = np.concatenate(self.gates_df['gates'].apply(lambda g: [c[0] for c in g] + [None]).values)
            g_lon = np.concatenate(self.gates_df['gates'].apply(lambda g: [c[1] for c in g] + [None]).values)
            
        if len(self.gates_df) and len(self.lines_df):
            lg_lat = np.concatenate((l_lat, g_lat))
            lg_lon = np.concatenate((l_lon, g_lon))

            # Get zoom level => Get lat pixel distance, get long pixel distance => Annotate
            zoom, center, max_lat, min_lat, max_lon, min_lon = self.get_zoom(lg_lon[lg_lon != None], lg_lat[lg_lat != None], height, width)
        elif len(self.lines_df):
            # Get zoom level => Get lat pixel distance, get long pixel distance => Annotate
            zoom, center, max_lat, min_lat, max_lon, min_lon = self.get_zoom(l_lon[l_lon != None], l_lat[l_lat != None], height, width)
        elif len(self.gates_df):
            # Get zoom level => Get lat pixel distance, get long pixel distance => Annotate
            zoom, center, max_lat, min_lat, max_lon, min_lon = self.get_zoom(g_lon[g_lat != None], g_lat[g_lat != None], height, width)

        for index in self.lines_df.index:
            line = self.lines_df.at[index, 'lines']

            # 1 is lng x, 0 is lat y
            # x.append(line[0][1])
            # y.append(line[0][0])

            pt1 = line[0]
            pt2 = line[1]
            x_annotation = min(pt1[1], pt2[1]) + (abs(pt2[1] - pt1[1]) / 2)
            y_annotation = min(pt1[0], pt2[0]) + (abs(pt2[0] - pt1[0]) / 2)
            x_annotation = (x_annotation - min_lon)/(max_lon - min_lon)
            y_annotation = (y_annotation - min_lat)/(max_lat - min_lat)
            # print(f'zoom {zoom} x {x_annotation} y {y_annotation}')

            if 2 * (abs(pt2[1] - pt1[1]) / 2) <= (abs(pt2[0] - pt1[0]) / 2):
                xanchor = 'left'
            else:
                xanchor = 'center'

            str_i = str(index).replace('Line ', 'L')
            fig.add_annotation(x=x_annotation,
                                xanchor=xanchor,
                                y=y_annotation,
                                xref='paper',yref='paper',
                                yanchor='bottom',
                                text=f'<b>{str_i}: {str(self.lines_df.at[index, "distance"])}ft</b>',
                                font=dict(size=15, color="darkgreen"),
                                showarrow=False)

            annotation_counter += 1

        if len(self.lines_df):
            fig.add_trace(go.Scattermapbox(lon=l_lon, lat=l_lat, 
                                            mode='markers+lines', 
                                            marker=dict(color='#00B900'), 
                                            line=dict(color='#00B900')))

        annotation_counter = 0

        for index in self.gates_df.index:
            gate = self.gates_df.at[index, 'gates']

            # 1 is lng, 0 is lat
            # x.append(gate[0][1])
            # y.append(gate[0][0])

            pt1 = gate[0]
            pt2 = gate[1]
            x_annotation = min(pt1[1], pt2[1]) + (abs(pt2[1] - pt1[1]) / 2)
            y_annotation = min(pt1[0], pt2[0]) + (abs(pt2[0] - pt1[0]) / 2)
            x_annotation = (x_annotation - min_lon)/(max_lon - min_lon)
            y_annotation = (y_annotation - min_lat)/(max_lat - min_lat)

            if 2 * (abs(pt2[1] - pt1[1]) / 2) <= (abs(pt2[0] - pt1[0]) / 2):
                xanchor = 'left'
            else:
                xanchor = 'center'

            str_i = str(index).replace('Gate ', 'G')
            fig.add_annotation(x=x_annotation,
                                xanchor=xanchor,
                                y=y_annotation,
                                xref='paper',yref='paper',
                                yanchor='bottom',
                                text=f'<b>{str_i}: {str(self.gates_df.at[index, "distance"])}ft</b>',
                                font=dict(size=15, color="#361c00"),
                                showarrow=False)

            annotation_counter += 1

        if len(self.gates_df):
            fig.add_trace(go.Scattermapbox(lon=g_lon, lat=g_lat, 
                                            mode='markers+lines', 
                                            marker=dict(color='#8B4513'), 
                                            line=dict(color='#8B4513')))

        fig.update_yaxes(showticklabels=False)
        fig.update_layout(template='plotly_white', 
                            showlegend=False, height=height, width=width, 
                            margin={'l': 0,'r': 0,'b': 0,'t': 0,},
                            mapbox={'style':'carto-positron', 
                                    'zoom':zoom,
                                    'center': {'lon': center['lon'], 'lat': center['lat']}})

0

基于plotly.com上的问题下面的第一个版本的函数,我想出了以下最终解决方案:

def get_plotting_zoom_level_and_center_coordinates_from_lonlat_tuples(
        longitudes=None, latitudes=None, lonlat_pairs=None):
    """Function documentation:\n
    Basic framework adopted from Krichardson under the following thread:
    https://community.plotly.com/t/dynamic-zoom-for-mapbox/32658/6

    # NOTE:
    # THIS IS A TEMPORARY SOLUTION UNTIL THE DASH TEAM IMPLEMENTS DYNAMIC ZOOM
    # in their plotly-functions associated with mapbox, such as go.Densitymapbox() etc.

    Returns the appropriate zoom-level for these plotly-mapbox-graphics along with
    the center coordinate tuple of all provided coordinate tuples.
    """

    # Check whether the list hasn't already be prepared outside this function
    if lonlat_pairs is None:
        # Check whether both latitudes and longitudes have been passed,
        # or if the list lenghts don't match
        if ((latitudes is None or longitudes is None)
                or (len(latitudes) != len(longitudes))):
            # Otherwise, return the default values of 0 zoom and the coordinate origin as center point
            return 0, (0, 0)

        # Instantiate collator list for all coordinate-tuples
        lonlat_pairs = [(longitudes[i], latitudes[i]) for i in range(len(longitudes))]

    # Get the boundary-box via the planar-module
    b_box = planar.BoundingBox(lonlat_pairs)

    # In case the resulting b_box is empty, return the default 0-values as well
    if b_box.is_empty:
        return 0, (0, 0)

    # Otherwise, get the area of the bounding box in order to calculate a zoom-level
    area = b_box.height * b_box.width

    # * 1D-linear interpolation with numpy:
    # - Pass the area as the only x-value and not as a list, in order to return a scalar as well
    # - The x-points "xp" should be in parts in comparable order of magnitude of the given area
    # - The zoom-levels are adapted to the areas, i.e. start with the smallest area possible of 0
    # which leads to the highest possible zoom value 20, and so forth decreasing with increasing areas
    # as these variables are antiproportional
    zoom = np.interp(x=area,
                     xp=[0, 5**-10, 4**-10, 3**-10, 2**-10, 1**-10, 1**-5],
                     fp=[20, 17, 16, 15, 14, 7, 5])

    # Finally, return the zoom level and the associated boundary-box center coordinates
    return zoom, b_box.center

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接