acgc.map_scalebar API documentation

# acgc.map_scalebar

Scale bar for display on maps

From StackOverflow https://stackoverflow.com/questions/32333870/how-can-i-show-a-km-ruler-on-a-cartopy-matplotlib-plot

View Source

      1#!/usr/bin/env python3
      2""" Scale bar for display on maps
      3
      4From StackOverflow
      5https://stackoverflow.com/questions/32333870/how-can-i-show-a-km-ruler-on-a-cartopy-matplotlib-plot
      6"""
      7
      8import numpy as np
      9import cartopy.crs as ccrs
     10import cartopy.geodesic as cgeo
     11
     12
     13def _axes_to_lonlat(ax, coords):
     14    """(lon, lat) from axes coordinates."""
     15    display = ax.transAxes.transform(coords)
     16    data = ax.transData.inverted().transform(display)
     17    lonlat = ccrs.PlateCarree().transform_point(*data, ax.projection)
     18
     19    return lonlat
     20
     21
     22def _upper_bound(start, direction, distance, dist_func):
     23    """A point farther than distance from start, in the given direction.
     24
     25    It doesn't matter which coordinate system start is given in, as long
     26    as dist_func takes points in that coordinate system.
     27
     28    Args:
     29        start:     Starting point for the line.
     30        direction  Nonzero (2, 1)-shaped array, a direction vector.
     31        distance:  Positive distance to go past.
     32        dist_func: A two-argument function which returns distance.
     33
     34    Returns:
     35        Coordinates of a point (a (2, 1)-shaped NumPy array).
     36    """
     37    if distance <= 0:
     38        raise ValueError(f"Minimum distance is not positive: {distance}")
     39
     40    if np.linalg.norm(direction) == 0:
     41        raise ValueError("Direction vector must not be zero.")
     42
     43    # Exponential search until the distance between start and end is
     44    # greater than the given limit.
     45    length = 0.1
     46    end = start + length * direction
     47
     48    while dist_func(start, end) < distance:
     49        length *= 2
     50        end = start + length * direction
     51
     52    return end
     53
     54
     55def _distance_along_line(start, end, distance, dist_func, tol):
     56    """Point at a distance from start on the segment  from start to end.
     57
     58    It doesn't matter which coordinate system start is given in, as long
     59    as dist_func takes points in that coordinate system.
     60
     61    Args:
     62        start:     Starting point for the line.
     63        end:       Outer bound on point's location.
     64        distance:  Positive distance to travel.
     65        dist_func: Two-argument function which returns distance.
     66        tol:       Relative error in distance to allow.
     67
     68    Returns:
     69        Coordinates of a point (a (2, 1)-shaped NumPy array).
     70    """
     71    initial_distance = dist_func(start, end)
     72    if initial_distance < distance:
     73        raise ValueError(f"End is closer to start ({initial_distance}) than "
     74                         f"given distance ({distance}).")
     75
     76    if tol <= 0:
     77        raise ValueError(f"Tolerance is not positive: {tol}")
     78
     79    # Binary search for a point at the given distance.
     80    left = start
     81    right = end
     82
     83    while not np.isclose(dist_func(start, right), distance, rtol=tol):
     84        midpoint = (left + right) / 2
     85
     86        # If midpoint is too close, search in second half.
     87        if dist_func(start, midpoint) < distance:
     88            left = midpoint
     89        # Otherwise the midpoint is too far, so search in first half.
     90        else:
     91            right = midpoint
     92
     93    return right
     94
     95
     96def _point_along_line(ax, start, distance, angle=0, tol=0.01):
     97    """Point at a given distance from start at a given angle.
     98
     99    Args:
    100        ax:       CartoPy axes.
    101        start:    Starting point for the line in axes coordinates.
    102        distance: Positive physical distance to travel.
    103        angle:    Anti-clockwise angle for the bar, in radians. Default: 0
    104        tol:      Relative error in distance to allow. Default: 0.01
    105
    106    Returns:
    107        Coordinates of a point (a (2, 1)-shaped NumPy array).
    108    """
    109    # Direction vector of the line in axes coordinates.
    110    direction = np.array([np.cos(angle), np.sin(angle)])
    111
    112    geodesic = cgeo.Geodesic()
    113
    114    # Physical distance between points.
    115    def dist_func(a_axes, b_axes):
    116        a_phys = _axes_to_lonlat(ax, a_axes)
    117        b_phys = _axes_to_lonlat(ax, b_axes)
    118
    119        # Geodesic().inverse returns a NumPy MemoryView like [[distance,
    120        # start azimuth, end azimuth]].
    121        return geodesic.inverse(a_phys, b_phys).base[0, 0]
    122
    123    end = _upper_bound(start, direction, distance, dist_func)
    124
    125    return _distance_along_line(start, end, distance, dist_func, tol)
    126
    127
    128def scale_bar(ax, location, length, metres_per_unit=1000, unit_name='km',
    129              tol=0.01, angle=0, color='black', linewidth=3, text_offset=0.005,
    130              ha='center', va='bottom', plot_kwargs=None, text_kwargs=None,
    131              **kwargs):
    132    """Add a scale bar to CartoPy axes.
    133
    134    For angles between 0 and 90 the text and line may be plotted at
    135    slightly different angles for unknown reasons. To work around this,
    136    override the 'rotation' keyword argument with text_kwargs.
    137
    138    Parameters
    139    ----------
    140    ax:
    141        CartoPy axes
    142    location:
    143        Position of left-side of bar in axes coordinates.
    144    length:
    145        Geodesic length of the scale bar.
    146    metres_per_unit: default=1000
    147        Number of metres in the given unit.
    148    unit_name: str, default='km'
    149        Name of the given unit.
    150    tol: float, default=0.01
    151        Allowed relative error in length of bar
    152    angle: float
    153        Anti-clockwise rotation of the bar.
    154    color: str, default='black'
    155        Color of the bar and text.
    156    linewidth: float
    157        Same argument as for plot.
    158    text_offset: float, default=0.005
    159        Perpendicular offset for text in axes coordinates.
    160    ha: str or float [0-1], default='center'
    161        Horizontal alignment.
    162    va: str or float [0-1], default='bottom'
    163        Vertical alignment.
    164    plot_kwargs: dict
    165        Keyword arguments for plot, overridden by **kwargs.
    166    text_kwargs: dict
    167        Keyword arguments for text, overridden by **kwargs.
    168    **kwargs:
    169        Keyword arguments for both plot and text.
    170    """
    171    # Setup kwargs, update plot_kwargs and text_kwargs.
    172    if plot_kwargs is None:
    173        plot_kwargs = {}
    174    if text_kwargs is None:
    175        text_kwargs = {}
    176
    177    plot_kwargs = {'linewidth': linewidth, 'color': color, **plot_kwargs,
    178                   **kwargs}
    179    text_kwargs = {'ha': ha, 'va': va, 'rotation': angle, 'color': color,
    180                   **text_kwargs, **kwargs}
    181
    182    # Convert all units and types.
    183    location = np.asarray(location)  # For vector addition.
    184    length_metres = length * metres_per_unit
    185    angle_rad = angle * np.pi / 180
    186
    187    # End-point of bar.
    188    end = _point_along_line(ax, location, length_metres, angle=angle_rad,
    189                            tol=tol)
    190
    191    # Coordinates are currently in axes coordinates, so use transAxes to
    192    # put into data coordinates. *zip(a, b) produces a list of x-coords,
    193    # then a list of y-coords.
    194    ax.plot(*zip(location, end), transform=ax.transAxes, **plot_kwargs)
    195
    196    # Push text away from bar in the perpendicular direction.
    197    midpoint = (location + end) / 2
    198    offset = text_offset * np.array([-np.sin(angle_rad), np.cos(angle_rad)])
    199    text_location = midpoint + offset
    200
    201    # 'rotation' keyword argument is in text_kwargs.
    202    ax.text(*text_location, f"{length} {unit_name}", rotation_mode='anchor',
    203            transform=ax.transAxes, **text_kwargs)

def scale_bar( ax, location, length, metres_per_unit=1000, unit_name='km', tol=0.01, angle=0, color='black', linewidth=3, text_offset=0.005, ha='center', va='bottom', plot_kwargs=None, text_kwargs=None, **kwargs): View Source

    129def scale_bar(ax, location, length, metres_per_unit=1000, unit_name='km',
    130              tol=0.01, angle=0, color='black', linewidth=3, text_offset=0.005,
    131              ha='center', va='bottom', plot_kwargs=None, text_kwargs=None,
    132              **kwargs):
    133    """Add a scale bar to CartoPy axes.
    134
    135    For angles between 0 and 90 the text and line may be plotted at
    136    slightly different angles for unknown reasons. To work around this,
    137    override the 'rotation' keyword argument with text_kwargs.
    138
    139    Parameters
    140    ----------
    141    ax:
    142        CartoPy axes
    143    location:
    144        Position of left-side of bar in axes coordinates.
    145    length:
    146        Geodesic length of the scale bar.
    147    metres_per_unit: default=1000
    148        Number of metres in the given unit.
    149    unit_name: str, default='km'
    150        Name of the given unit.
    151    tol: float, default=0.01
    152        Allowed relative error in length of bar
    153    angle: float
    154        Anti-clockwise rotation of the bar.
    155    color: str, default='black'
    156        Color of the bar and text.
    157    linewidth: float
    158        Same argument as for plot.
    159    text_offset: float, default=0.005
    160        Perpendicular offset for text in axes coordinates.
    161    ha: str or float [0-1], default='center'
    162        Horizontal alignment.
    163    va: str or float [0-1], default='bottom'
    164        Vertical alignment.
    165    plot_kwargs: dict
    166        Keyword arguments for plot, overridden by **kwargs.
    167    text_kwargs: dict
    168        Keyword arguments for text, overridden by **kwargs.
    169    **kwargs:
    170        Keyword arguments for both plot and text.
    171    """
    172    # Setup kwargs, update plot_kwargs and text_kwargs.
    173    if plot_kwargs is None:
    174        plot_kwargs = {}
    175    if text_kwargs is None:
    176        text_kwargs = {}
    177
    178    plot_kwargs = {'linewidth': linewidth, 'color': color, **plot_kwargs,
    179                   **kwargs}
    180    text_kwargs = {'ha': ha, 'va': va, 'rotation': angle, 'color': color,
    181                   **text_kwargs, **kwargs}
    182
    183    # Convert all units and types.
    184    location = np.asarray(location)  # For vector addition.
    185    length_metres = length * metres_per_unit
    186    angle_rad = angle * np.pi / 180
    187
    188    # End-point of bar.
    189    end = _point_along_line(ax, location, length_metres, angle=angle_rad,
    190                            tol=tol)
    191
    192    # Coordinates are currently in axes coordinates, so use transAxes to
    193    # put into data coordinates. *zip(a, b) produces a list of x-coords,
    194    # then a list of y-coords.
    195    ax.plot(*zip(location, end), transform=ax.transAxes, **plot_kwargs)
    196
    197    # Push text away from bar in the perpendicular direction.
    198    midpoint = (location + end) / 2
    199    offset = text_offset * np.array([-np.sin(angle_rad), np.cos(angle_rad)])
    200    text_location = midpoint + offset
    201
    202    # 'rotation' keyword argument is in text_kwargs.
    203    ax.text(*text_location, f"{length} {unit_name}", rotation_mode='anchor',
    204            transform=ax.transAxes, **text_kwargs)

Add a scale bar to CartoPy axes.

For angles between 0 and 90 the text and line may be plotted at slightly different angles for unknown reasons. To work around this, override the 'rotation' keyword argument with text_kwargs.

###### Parameters

- ax (): CartoPy axes
- location (): Position of left-side of bar in axes coordinates.
- length (): Geodesic length of the scale bar.
- metres_per_unit (default=1000): Number of metres in the given unit.
- unit_name (str, default='km'): Name of the given unit.
- tol (float, default=0.01): Allowed relative error in length of bar
- angle (float): Anti-clockwise rotation of the bar.
- color (str, default='black'): Color of the bar and text.
- linewidth (float): Same argument as for plot.
- text_offset (float, default=0.005): Perpendicular offset for text in axes coordinates.
- ha (str or float [0-1], default='center'): Horizontal alignment.
- va (str or float [0-1], default='bottom'): Vertical alignment.
- plot_kwargs (dict): Keyword arguments for plot, overridden by **kwargs.
- text_kwargs (dict): Keyword arguments for text, overridden by **kwargs.
- **kwargs (): Keyword arguments for both plot and text.