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
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):
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.