Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animate a point moving along path between two points

Tags:

cartopy

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!

like image 240
Suraj Yadav Avatar asked Jan 02 '23 02:01

Suraj Yadav


1 Answers

There are a couple of approaches to this.

The matplotlib approach

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()

The matplotlib way

The cartopy approach

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 cartopy way

Final remaaaaarrrrrrks....

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()

Arrrr, here be pirates!

All code and images for this answer can be found at https://gist.github.com/pelson/618a5f4ca003e56f06d43815b21848f6.

like image 57
pelson Avatar answered Feb 19 '23 08:02

pelson