最佳答案
加上 geodesic module在 CartoPy 0.15 中,我们现在可以相当轻松地计算 map 上的精确长度。弄清楚如何在 map 上的一条直线上找到球体上正确的距离的两个点有点棘手。指定 map 上的方向后,我将执行指数搜索以找到足够远的点。然后,我执行二进制搜索以找到足够接近所需距离的点。
scale_bar
函数很简单,但它有很多选项。基本签名是 scale_bar(ax, location, length)
。 ax
是任何 CartoPy 轴,location
是条形左侧在轴坐标中的位置(因此每个坐标从 0 到 1), length
是以千米为单位的柱体长度。支持其他长度,例如 metres_per_unit
和 unit_name
关键字参数。
额外的关键字参数(如 color
)被简单地传递给 text
和 plot
。但是,特定于 text
或 plot
的关键字参数(如 family
或 path_effects
)必须作为字典传入text_kwargs
和 plot_kwargs
。
我已经包含了我认为是常见用例的示例。
请分享任何问题、评论或批评。
比例尺.py
import numpy as np
import cartopy.crs as ccrs
import cartopy.geodesic as cgeo
def _axes_to_lonlat(ax, coords):
"""(lon, lat) from axes coordinates."""
display = ax.transAxes.transform(coords)
data = ax.transData.inverted().transform(display)
lonlat = ccrs.PlateCarree().transform_point(*data, ax.projection)
return lonlat
def _upper_bound(start, direction, distance, dist_func):
"""A point farther than distance from start, in the given direction.
It doesn't matter which coordinate system start is given in, as long
as dist_func takes points in that coordinate system.
Args:
start: Starting point for the line.
direction Nonzero (2, 1)-shaped array, a direction vector.
distance: Positive distance to go past.
dist_func: A two-argument function which returns distance.
Returns:
Coordinates of a point (a (2, 1)-shaped NumPy array).
"""
if distance <= 0:
raise ValueError(f"Minimum distance is not positive: {distance}")
if np.linalg.norm(direction) == 0:
raise ValueError("Direction vector must not be zero.")
# Exponential search until the distance between start and end is
# greater than the given limit.
length = 0.1
end = start + length * direction
while dist_func(start, end) < distance:
length *= 2
end = start + length * direction
return end
def _distance_along_line(start, end, distance, dist_func, tol):
"""Point at a distance from start on the segment from start to end.
It doesn't matter which coordinate system start is given in, as long
as dist_func takes points in that coordinate system.
Args:
start: Starting point for the line.
end: Outer bound on point's location.
distance: Positive distance to travel.
dist_func: Two-argument function which returns distance.
tol: Relative error in distance to allow.
Returns:
Coordinates of a point (a (2, 1)-shaped NumPy array).
"""
initial_distance = dist_func(start, end)
if initial_distance < distance:
raise ValueError(f"End is closer to start ({initial_distance}) than "
f"given distance ({distance}).")
if tol <= 0:
raise ValueError(f"Tolerance is not positive: {tol}")
# Binary search for a point at the given distance.
left = start
right = end
while not np.isclose(dist_func(start, right), distance, rtol=tol):
midpoint = (left + right) / 2
# If midpoint is too close, search in second half.
if dist_func(start, midpoint) < distance:
left = midpoint
# Otherwise the midpoint is too far, so search in first half.
else:
right = midpoint
return right
def _point_along_line(ax, start, distance, angle=0, tol=0.01):
"""Point at a given distance from start at a given angle.
Args:
ax: CartoPy axes.
start: Starting point for the line in axes coordinates.
distance: Positive physical distance to travel.
angle: Anti-clockwise angle for the bar, in radians. Default: 0
tol: Relative error in distance to allow. Default: 0.01
Returns:
Coordinates of a point (a (2, 1)-shaped NumPy array).
"""
# Direction vector of the line in axes coordinates.
direction = np.array([np.cos(angle), np.sin(angle)])
geodesic = cgeo.Geodesic()
# Physical distance between points.
def dist_func(a_axes, b_axes):
a_phys = _axes_to_lonlat(ax, a_axes)
b_phys = _axes_to_lonlat(ax, b_axes)
# Geodesic().inverse returns a NumPy MemoryView like [[distance,
# start azimuth, end azimuth]].
return geodesic.inverse(a_phys, b_phys).base[0, 0]
end = _upper_bound(start, direction, distance, dist_func)
return _distance_along_line(start, end, distance, dist_func, tol)
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):
"""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.
Args:
ax: CartoPy axes.
location: Position of left-side of bar in axes coordinates.
length: Geodesic length of the scale bar.
metres_per_unit: Number of metres in the given unit. Default: 1000
unit_name: Name of the given unit. Default: 'km'
tol: Allowed relative error in length of bar. Default: 0.01
angle: Anti-clockwise rotation of the bar.
color: Color of the bar and text. Default: 'black'
linewidth: Same argument as for plot.
text_offset: Perpendicular offset for text in axes coordinates.
Default: 0.005
ha: Horizontal alignment. Default: 'center'
va: Vertical alignment. Default: 'bottom'
**plot_kwargs: Keyword arguments for plot, overridden by **kwargs.
**text_kwargs: Keyword arguments for text, overridden by **kwargs.
**kwargs: Keyword arguments for both plot and text.
"""
# Setup kwargs, update plot_kwargs and text_kwargs.
if plot_kwargs is None:
plot_kwargs = {}
if text_kwargs is None:
text_kwargs = {}
plot_kwargs = {'linewidth': linewidth, 'color': color, **plot_kwargs,
**kwargs}
text_kwargs = {'ha': ha, 'va': va, 'rotation': angle, 'color': color,
**text_kwargs, **kwargs}
# Convert all units and types.
location = np.asarray(location) # For vector addition.
length_metres = length * metres_per_unit
angle_rad = angle * np.pi / 180
# End-point of bar.
end = _point_along_line(ax, location, length_metres, angle=angle_rad,
tol=tol)
# Coordinates are currently in axes coordinates, so use transAxes to
# put into data coordinates. *zip(a, b) produces a list of x-coords,
# then a list of y-coords.
ax.plot(*zip(location, end), transform=ax.transAxes, **plot_kwargs)
# Push text away from bar in the perpendicular direction.
midpoint = (location + end) / 2
offset = text_offset * np.array([-np.sin(angle_rad), np.cos(angle_rad)])
text_location = midpoint + offset
# 'rotation' keyword argument is in text_kwargs.
ax.text(*text_location, f"{length} {unit_name}", rotation_mode='anchor',
transform=ax.transAxes, **text_kwargs)
演示.py
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
from scalebar import scale_bar
fig = plt.figure(1, figsize=(10, 10))
ax = fig.add_subplot(111, projection=ccrs.Mercator())
ax.set_extent([-180, 180, -85, 85])
ax.coastlines(facecolor='black')
ax.add_feature(cfeature.LAND)
# Standard 6,000 km scale bar.
scale_bar(ax, (0.65, 0.4), 6_000)
# Length of the bar reflects its position on the map.
scale_bar(ax, (0.55, 0.7), 6_000, color='green')
# Bar can be placed at any angle. Any units can be used.
scale_bar(ax, (0.4, 0.4), 3_000, metres_per_unit=1609, angle=-90,
unit_name='mi', color='red')
# Text and line can be styled separately. Keywords are simply passed to
# text or plot.
text_kwargs = dict(family='serif', size='xx-large', color='red')
plot_kwargs = dict(linestyle='dashed', color='blue')
scale_bar(ax, (0.05, 0.3), 6_000, text_kwargs=text_kwargs,
plot_kwargs=plot_kwargs)
# Angles between 0 and 90 may result in the text and line plotted at
# slightly different angles for an unknown reason.
scale_bar(ax, (0.45, 0.15), 5_000, color='purple', angle=45, text_offset=0)
# To get around this override the text's angle and fiddle manually.
scale_bar(ax, (0.55, 0.15), 5_000, color='orange', angle=45, text_offset=0,
text_kwargs={'rotation': 41})
plt.show()
关于python - 如何在 cartopy/matplotlib 图上显示公里标尺?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32333870/