Animate a point moving along path between two points

前提是你 提交于 2019-12-03 21:18:28

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

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

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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!