How can I show a km ruler on a cartopy / matplotlib plot?

后端 未结 5 1109
温柔的废话
温柔的废话 2020-12-25 14:52

How can I show a km ruler for a zoomed in section of a map, either inset in the image or as rulers on the side of the plot?

E.g. something like the 50 km bar on the

5条回答
  •  南方客
    南方客 (楼主)
    2020-12-25 15:36

    based on the previous examples provided above, and from here, I have developed an alternative for drawing scalebars using cartopy.

    The approach was validated with the cartopy.crs.PlateCarree() projection. Nevertheless, the algorithm did not work correctly for other projections.

    Here is an example:


    # importing main libraries
    
    import cartopy
    import cartopy.crs as ccrs
    from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
    import matplotlib.pyplot as plt
    import numpy as np
    
    from matplotlib import font_manager as mfonts
    import matplotlib.ticker as mticker
    import matplotlib.patches as patches
    import geopandas as gpd
    
    import pandas as pd
    
    
    
    def get_standard_gdf():
        """ basic function for getting some geographical data in geopandas GeoDataFrame python's instance:
            An example data can be downloaded from Brazilian IBGE:
            ref: ftp://geoftp.ibge.gov.br/organizacao_do_territorio/malhas_territoriais/malhas_municipais/municipio_2017/Brasil/BR/br_municipios.zip    
        """
        gdf_path = r'C:\path_to_shp\shapefile.shp'
    
        return gpd.read_file(gdf_path)
    
    
    ----------
    # defining functions for scalebar
    
    
    def _crs_coord_project(crs_target, xcoords, ycoords, crs_source):
        """ metric coordinates (x, y) from cartopy.crs_source"""
        
        axes_coords = crs_target.transform_points(crs_source, xcoords, ycoords)
        
        return axes_coords
    
    
    def _add_bbox(ax, list_of_patches, paddings={}, bbox_kwargs={}):
        
        '''
        Description:
            This helper function adds a box behind the scalebar:
                Code inspired by: https://stackoverflow.com/questions/17086847/box-around-text-in-matplotlib
        
        
        '''
        
        zorder = list_of_patches[0].get_zorder() - 1
        
        xmin = min([t.get_window_extent().xmin for t in list_of_patches])
        xmax = max([t.get_window_extent().xmax for t in list_of_patches])
        ymin = min([t.get_window_extent().ymin for t in list_of_patches])
        ymax = max([t.get_window_extent().ymax for t in list_of_patches])
        
    
        xmin, ymin = ax.transData.inverted().transform((xmin, ymin))
        xmax, ymax = ax.transData.inverted().transform((xmax, ymax))
    
        
        xmin = xmin - ( (xmax-xmin) * paddings['xmin'])
        ymin = ymin - ( (ymax-ymin) * paddings['ymin'])
        
        xmax = xmax + ( (xmax-xmin) * paddings['xmax'])
        ymax = ymax + ( (ymax-ymin) * paddings['ymax'])
        
        width = (xmax-xmin)
        height = (ymax-ymin)
        
        # Setting xmin according to height
        
        
        rect = patches.Rectangle((xmin,ymin),
                                  width,
                                  height, 
                                  facecolor=bbox_kwargs['facecolor'], 
                                  edgecolor =bbox_kwargs['edgecolor'],
                                  alpha=bbox_kwargs['alpha'], 
                                  transform=ax.projection,
                                  fill=True,
                                  clip_on=False,
                                  zorder=zorder)
    
        ax.add_patch(rect)
        return ax
    
    
    
    def add_scalebar(ax, metric_distance=100, 
                     at_x=(0.1, 0.4), 
                     at_y=(0.05, 0.075), 
                     max_stripes=5,
                     ytick_label_margins = 0.25,
                     fontsize= 8,
                     font_weight='bold',
                     rotation = 45,
                     zorder=999,
                     paddings = {'xmin':0.3,
                                 'xmax':0.3,
                                 'ymin':0.3,
                                 'ymax':0.3},
        
                     bbox_kwargs = {'facecolor':'w',
                                    'edgecolor':'k',
                                    'alpha':0.7}
                    ):
        """
        Add a scalebar to a GeoAxes of type cartopy.crs.OSGB (only).
    
        Args:
        * at_x : (float, float)
            target axes X coordinates (0..1) of box (= left, right)
        * at_y : (float, float)
            axes Y coordinates (0..1) of box (= lower, upper)
        * max_stripes
            typical/maximum number of black+white regions
        """
        old_proj = ax.projection
        ax.projection = ccrs.PlateCarree()
        # Set a planar (metric) projection for the centroid of a given axes projection:
        # First get centroid lon and lat coordinates:
        
        lon_0, lon_1, lat_0, lat_1 = ax.get_extent(ax.projection.as_geodetic())
        
        central_lon = np.mean([lon_0, lon_1])
        central_lat = np.mean([lat_0, lat_1])
        
        # Second: set the planar (metric) projection centered in the centroid of the axes;
            # Centroid coordinates must be in lon/lat.
        proj=ccrs.EquidistantConic(central_longitude=central_lon, central_latitude=central_lat)
        
        # fetch axes coordinates in meters
        x0, x1, y0, y1 = ax.get_extent(proj)
        ymean = np.mean([y0, y1])
        
        # set target rectangle in-visible-area (aka 'Axes') coordinates
        axfrac_ini, axfrac_final = at_x
        ayfrac_ini, ayfrac_final = at_y
        
        # choose exact X points as sensible grid ticks with Axis 'ticker' helper
        xcoords = []
        ycoords = []
        xlabels = []
        for i in range(0 , 1+ max_stripes):
            dx = (metric_distance * i) + x0
            xlabels.append(dx - x0)
            
            xcoords.append(dx)
            ycoords.append(ymean)
        
        # Convertin to arrays:
    
        xcoords = np.asanyarray(xcoords)
        ycoords = np.asanyarray(ycoords)
        
        # Ensuring that the coordinate projection is in degrees:
    
        x_targets, y_targets, z_targets = _crs_coord_project(ax.projection, xcoords, ycoords, proj).T
        x_targets = [x + (axfrac_ini * (lon_1 - lon_0)) for x in  x_targets]
    
    
        
        # Checking x_ticks in axes projection coordinates
        #print('x_targets', x_targets)
        
        
        #Setting transform for plotting
        
        
        transform = ax.projection
        
        
        
        # grab min+max for limits
        xl0, xl1 = x_targets[0], x_targets[-1]
        
        
        # calculate Axes Y coordinates of box top+bottom
        yl0, yl1 = [lat_0 + ay_frac * (lat_1 - lat_0) for ay_frac in [ayfrac_ini, ayfrac_final]]
    
        
        # calculate Axes Y distance of ticks + label margins
        y_margin = (yl1-yl0)*ytick_label_margins
        
        
        
        # fill black/white 'stripes' and draw their boundaries
        fill_colors = ['black', 'white']
        i_color = 0
        
        filled_boxs = []
        for xi0, xi1 in zip(x_targets[:-1],x_targets[1:]):
            # fill region
            filled_box = plt.fill(
                                  (xi0, xi1, xi1, xi0, xi0), 
                                  (yl0, yl0, yl1, yl1, yl0),
                     
                                  fill_colors[i_color],
                                  transform=transform,
                                  clip_on=False,
                                  zorder=zorder
                                )
            
            filled_boxs.append(filled_box[0])
            
            # draw boundary
            plt.plot((xi0, xi1, xi1, xi0, xi0), 
                     (yl0, yl0, yl1, yl1, yl0),
                     'black',
                     clip_on=False,
                    transform=transform,
                    zorder=zorder)
            
            i_color = 1 - i_color
        
        # adding boxes
        
        
        _add_bbox(ax, 
                 filled_boxs,
                 bbox_kwargs = bbox_kwargs ,
                 paddings =paddings)
        
        
        
        # add short tick lines
        for x in x_targets:
            plt.plot((x, x), (yl0, yl0-y_margin), 'black', 
                     transform=transform,
                     zorder=zorder,
                     clip_on=False)
        
        
        
        # add a scale legend 'Km'
        font_props = mfonts.FontProperties(size=fontsize, 
                                           weight=font_weight)
        
        plt.text(
            0.5 * (xl0 + xl1),
            yl1 + y_margin,
            'Km',
            color='k',
            verticalalignment='bottom',
            horizontalalignment='center',
            fontproperties=font_props,
            transform=transform,
            clip_on=False,
            zorder=zorder)
    
        # add numeric labels
        for x, xlabel in zip(x_targets, xlabels):
            print('Label set in: ', x, yl0 - 2 * y_margin)
            plt.text(x,
                     yl0 - 2 * y_margin,
                     '{:g}'.format((xlabel) * 0.001),
                     verticalalignment='top',
                     horizontalalignment='center',
                     fontproperties=font_props,
                     transform=transform,
                     rotation=rotation,
                     clip_on=False,
                     zorder=zorder+1,
                    #bbox=dict(facecolor='red', alpha=0.5) # this would add a box only around the xticks
                    )
        
        
        # Adjusting figure borders to ensure that the scalebar is within its limits
        ax.projection = old_proj
        ax.get_figure().canvas.draw()
        fig.tight_layout() 
    
    
    ----------
    

    Defining some helper functions for styling the axes #plotting

    def format_ax(ax, projection):
    
        xlim = ax.get_xlim()
        ylim = ax.get_ylim()
        ax.set_global()
        ax.coastlines()
        
        ax.set_xlim(xlim)
        ax.set_ylim(ylim)
        
        
    def add_grider(ax, nticks=5):
        
        
        if isinstance(ax.projection, ccrs.PlateCarree):
            
    
    
    
            Grider = ax.gridlines(draw_labels=True)
            Grider.xformatter = LONGITUDE_FORMATTER
            Grider.yformatter = LATITUDE_FORMATTER
            Grider.xlabels_top  = False
            Grider.ylabels_right  = False
    
            Grider.xlocator = mticker.MaxNLocator(nticks)
            Grider.ylocator = mticker.MaxNLocator(nticks)
            
        
        else:
            xmin, xmax, ymin, ymax = ax.get_extent()
    
            ax.set_xticks(np.arange(xmin, xmax, nticks))
    
            ax.set_yticks(np.arange(ymin, ymax, nticks))
            ax.grid(True)
    
    ----------
    # Defining a main helper function for plotting:
    
    
    
    def main(projection = ccrs.PlateCarree(central_longitude=0),
            nticks=4):
        
        
        fig, ax1 = plt.subplots( figsize=(8, 10), subplot_kw={'projection':projection})
    
        # Label axes of a Plate Carree projection with a central longitude of 180:
        
        #for enum, proj in enumerate(['Mercator, PlateCarree']):
        
        gdf = get_standard_gdf()
        
    
        if gdf.crs.is_projected:
            epsg = gdf.crs.to_epsg()
    
            crs_epsg = ccrs.epsg(epsg)
    
        else:
            crs_epsg = ccrs.PlateCarree()
    
    
        gdf.plot(ax=ax1, transform=projection)
        
        
        format_ax(ax1, projection)
        
        
        add_grider(ax1, nticks)
        
    
        ax1.set_title('Projection {0}'.format(ax1.projection.__class__.__name__))
        plt.draw()
        return fig, fig.get_axes()
    
    
    ----------
    # Example of the case
    
    
    
    length = 1000
    
    fig, axes = main(ccrs.PlateCarree())
    
    for ax in axes:
    
        add_scalebar(ax, 
                     metric_distance=200_000  , 
                     at_x=(1.1, 1.3), 
                     at_y=(0.08, 0.11), 
                     max_stripes=4,
                     paddings = {'xmin':0.1,
                                'xmax':0.1,
                                'ymin':2.8,
                                'ymax':0.5},
                     fontsize=9,
                     font_weight='bold',
                     bbox_kwargs = {'facecolor':'w',
                                    'edgecolor':'k',
                                   'alpha':0.7})
    
    fig.show()
    

    Here are two figures of the same region (State of Pará - Brazil), with different settings in the "add_scalebar" function. The Fig 1 is derived exactly from the setting presented above. The Fig 2 uses a variant:

    add_scalebar(ax, 
                     metric_distance=200_000  , 
                     at_x=(0.55, 0.3), 
                     at_y=(0.08, 0.11), 
                     max_stripes=4,
                     paddings = {'xmin':0.05,
                                'xmax':0.05,
                                'ymin':2.2,
                                'ymax':0.5},
                     fontsize=7,
                     font_weight='bold',
                     bbox_kwargs = {'facecolor':'w',
                                    'edgecolor':'k',
                                   'alpha':0.7})
    


    The only issue is that this proposed solution still needs to be extended to other cartopy projections (beside PlateCarree).

提交回复
热议问题