I want to animate a point moving along a path from one location to another on the map.
For example, I drawn a path from New York to New Delhi, using Geodetic transform. Eg. taken from docs Adding data to the map
plt.plot([ny_lon, delhi_lon], [ny_lat, delhi_lat],
color='blue', linewidth=2, marker='o',
transform=ccrs.Geodetic(),
)
Now i want to move a point along this path.
My idea was to somehow get some (say 50) points, along the path and plot a marker on each point for each frame. But I am not able to find a way to get the points on the path.
I found a function transform_points
under classCRS
, but I am unable to use this, as this gives me the same number of points i have, not the points in between.
Thanks in advance!
There are a couple of approaches to this.
I'll start with perhaps the most basic if you are familiar with matplotlib, but this approach suffers from indirectly using cartopy's functionality, and is therefore harder to configure/extend.
There is a private _get_transformed_path
method on a Line2D object (the thing that is returned from plt.plot
). The resulting TransformedPath object has a get_transformed_path_and_affine
method, which basically will give us the projected line (in the coordinate system of the Axes being drawn).
In [1]: import cartopy.crs as ccrs
In [3]: import matplotlib.pyplot as plt
In [4]: ax = plt.axes(projection=ccrs.Robinson())
In [6]: ny_lon, ny_lat = -75, 43
In [7]: delhi_lon, delhi_lat = 77.23, 28.61
In [8]: [line] = plt.plot([ny_lon, delhi_lon], [ny_lat, delhi_lat],
...: color='blue', linewidth=2, marker='o',
...: transform=ccrs.Geodetic(),
...: )
In [9]: t_path = line._get_transformed_path()
In [10]: path_in_data_coords, _ = t_path.get_transformed_path_and_affine()
In [11]: path_in_data_coords.vertices
Out[11]:
array([[-6425061.82215208, 4594257.92617961],
[-5808923.84969279, 5250795.00604155],
[-5206753.88613758, 5777772.51828996],
[-4554622.94040482, 6244967.03723341],
[-3887558.58343227, 6627927.97123701],
[-3200922.19194864, 6932398.19937816],
[-2480001.76507805, 7165675.95095855],
[-1702269.5101901 , 7332885.72276795],
[ -859899.12295981, 7431215.78426759],
[ 23837.23431173, 7453455.61302756],
[ 889905.10635756, 7397128.77301289],
[ 1695586.66856764, 7268519.87627204],
[ 2434052.81300274, 7073912.54130764],
[ 3122221.22299409, 6812894.40443648],
[ 3782033.80448001, 6478364.28561403],
[ 4425266.18173684, 6062312.15662039],
[ 5049148.25986903, 5563097.6328901 ],
[ 5616318.74912886, 5008293.21452795],
[ 6213232.98764984, 4307186.23400115],
[ 6720608.93929235, 3584542.06839575],
[ 7034261.06659143, 3059873.62740856]])
We can pull this together with matplotlib's animation functionality to do as requested:
import cartopy.crs as ccrs
import matplotlib.animation as animation
import matplotlib.pyplot as plt
ax = plt.axes(projection=ccrs.Robinson())
ax.stock_img()
ny_lon, ny_lat = -75, 43
delhi_lon, delhi_lat = 77.23, 28.61
[line] = plt.plot([ny_lon, delhi_lon], [ny_lat, delhi_lat],
color='blue', linewidth=2, marker='o',
transform=ccrs.Geodetic(),
)
t_path = line._get_transformed_path()
path_in_data_coords, _ = t_path.get_transformed_path_and_affine()
# Draw the point that we want to animate.
[point] = plt.plot(ny_lon, ny_lat, marker='o', transform=ax.projection)
def animate_point(i):
verts = path_in_data_coords.vertices
i = i % verts.shape[0]
# Set the coordinates of the line to the coordinate of the path.
point.set_data(verts[i, 0], verts[i, 1])
ani = animation.FuncAnimation(
ax.figure, animate_point,
frames= path_in_data_coords.vertices.shape[0],
interval=125, repeat=True)
ani.save('point_ani.gif', writer='imagemagick')
plt.show()
Under the hood, cartopy's matplotlib implementation (as used above), is calling the project_geometry method. We may as well make use of this directly as it is often more convenient to be using Shapely geometries than it is matplotlib Paths.
With this approach, we simply define a shapely geometry, and then construct the source and target coordinate reference systems that we want to convert the geometry from/to:
target_cs.project_geometry(geometry, source_cs)
The only thing we have to watch out for is that the result can be a MultiLineString (or more generally, any Multi- geometry type). However, in our simple case, we don't need to deal with that (incidentally, the same was true of the simple Path returned in the first example).
The code to produce a similar plot to above:
import cartopy.crs as ccrs
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np
import shapely.geometry as sgeom
ax = plt.axes(projection=ccrs.Robinson())
ax.stock_img()
ny_lon, ny_lat = -75, 43
delhi_lon, delhi_lat = 77.23, 28.61
line = sgeom.LineString([[ny_lon, ny_lat], [delhi_lon, delhi_lat]])
projected_line = ccrs.PlateCarree().project_geometry(line, ccrs.Geodetic())
# We only animate along one of the projected lines.
if isinstance(projected_line, sgeom.MultiLineString):
projected_line = projected_line.geoms[0]
ax.add_geometries(
[projected_line], ccrs.PlateCarree(),
edgecolor='blue', facecolor='none')
[point] = plt.plot(ny_lon, ny_lat, marker='o', transform=ccrs.PlateCarree())
def animate_point(i):
verts = np.array(projected_line.coords)
i = i % verts.shape[0]
# Set the coordinates of the line to the coordinate of the path.
point.set_data(verts[i, 0], verts[i, 1])
ani = animation.FuncAnimation(
ax.figure, animate_point,
frames=len(projected_line.coords),
interval=125, repeat=True)
ani.save('projected_line_ani.gif', writer='imagemagick')
plt.show()
The approach naturally generalises to animating any type of matplotlib Arrrrtist.... in this case, I took a bit more control over the great circle resolution, and I animated an image along the great circle:
import cartopy.crs as ccrs
import matplotlib.animation as animation
import matplotlib.pyplot as plt
import numpy as np
import shapely.geometry as sgeom
ax = plt.axes(projection=ccrs.Mercator())
ax.stock_img()
line = sgeom.LineString([[-5.9845, 37.3891], [-82.3666, 23.1136]])
# Higher resolution version of Mercator. Same workaround as found in
# https://github.com/SciTools/cartopy/issues/8#issuecomment-326987465.
class HighRes(ax.projection.__class__):
@property
def threshold(self):
return super(HighRes, self).threshold / 100
projected_line = HighRes().project_geometry(line, ccrs.Geodetic())
# We only animate along one of the projected lines.
if isinstance(projected_line, sgeom.MultiLineString):
projected_line = projected_line.geoms[0]
# Add the projected line to the map.
ax.add_geometries(
[projected_line], ax.projection,
edgecolor='blue', facecolor='none')
def ll_to_extent(x, y, ax_size=(4000000, 4000000)):
"""
Return an image extent in centered on the given
point with the given width and height.
"""
return [x - ax_size[0] / 2, x + ax_size[0] / 2,
y - ax_size[1] / 2, y + ax_size[1] / 2]
# Image from https://pixabay.com/en/sailing-ship-boat-sail-pirate-28930/.
pirate = plt.imread('pirates.png')
img = ax.imshow(pirate, extent=ll_to_extent(0, 0), transform=ax.projection, origin='upper')
ax.set_global()
def animate_ship(i):
verts = np.array(projected_line.coords)
i = i % verts.shape[0]
# Set the extent of the image to the coordinate of the path.
img.set_extent(ll_to_extent(verts[i, 0], verts[i, 1]))
ani = animation.FuncAnimation(
ax.figure, animate_ship,
frames=len(projected_line.coords),
interval=125, repeat=False)
ani.save('arrrr.gif', writer='imagemagick')
plt.show()
All code and images for this answer can be found at https://gist.github.com/pelson/618a5f4ca003e56f06d43815b21848f6.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With