Search code examples
pythonmatplotlibanimationcartopy

Update tile/image based on extent using Cartopy and Matplotlib FuncAnimation


I want to create an animation of a trajectory using Cartopy and FuncAnimation. I have the trajectory stored in a GeoDataFrame with latitude, longitude and date time columns, which I want to visualise.

The trajectory moves across the globe, but I want to zoom in and therefore aim to have the basemap to be updated depending on the extent. I want to use satellite imagery as basemap, and I am using GoogleTiles from cartopy.io.img_tiles.

However, the image seems to only load at the initial extent, and is not updated in the new frames. The same happens when using 'stock_img()'. Interestingly, features such as coastlines and borders do load. To visualise my problem, view the images attached.

initial frame with initial extent later frame with different extent, the imagery doesn't cover this extent anymore

My function:

# Function to animate with moving basemaps
def plot_animation_moving(gdf, zoom_level = 8, extent_margin = 4, tail_length = 50, frames_between_points = 10, interval=100):

   tiler = GoogleTiles(style="satellite")
   mercator = tiler.crs

   # Set up the figure and axis
   fig, ax = plt.subplots(figsize=(12, 12), subplot_kw={'projection': mercator})
   
   # Initial extent
   initial_extent = [gdf.iloc[0]['lon']-extent_margin, gdf.iloc[0]['lon']+extent_margin, gdf.iloc[0]['lat']-extent_margin, gdf.iloc[0]['lat']+extent_margin]
   ax.set_extent(initial_extent)

   # Add background and features
   ax.add_image(tiler, zoom_level)
   # ax.stock_img()
   ax.add_feature(cfeature.BORDERS, linestyle=':')
   ax.coastlines(resolution='10m')
   ax.add_feature(cfeature.LAND)
   ax.add_feature(cfeature.OCEAN)
   ax.add_feature(cfeature.LAKES, alpha=0.5)
   ax.add_feature(cfeature.RIVERS)

   # Loop through each pair of points and create points in between
   lats_list = []
   lons_list = []
   dates_list = []
   for i in range(len(gdf)):
       
       # create lat, lon and date sequences
       ...
       lats_list.append(lats)
       lons_list.append(lons)
       dates_list.append(dates)

   # Concatenate points
   lats = np.concatenate(lats_list)
   lons = np.concatenate(lons_list)
   dates = np.concatenate(dates_list)

   # Create a point with tail object on the Basemap
   point, = ...
   tail, = ...
   date_text = ...

   # Update the point position
   def update(frame):

       # Update point, tail and date
       point.set_data([lons[frame]], [lats[frame]])
       current_date = pd.to_datetime(dates[frame])
       date_text.set_text(f'{current_date.strftime("%Y-%m-%d %H:00")}')

       # Update the map extent to follow the line
       ax.set_extent([lons[frame] - extent_margin, lons[frame] + extent_margin, lats[frame] - extent_margin, lats[frame] + extent_margin])
       
       return point, tail, date_text
   
   # Create the animation
   ani = FuncAnimation(fig, update, frames=tqdm.tqdm(range(len(lats)), file=sys.stdout), blit=False, interval=interval),
   ax.add_image(tiler, zoom_level) # --> doesn't help

   ani.save("example.mp4")

In the update function, I have tried adding the image again using ax.add_image(tiler, zoom_level) but it doesn't seem to help.

My question therefore is: how do I correctly update the basemap image so that it loads when the point is moving across the globe?


Solution

  • You could be sneaky and go one level deeper by passing the tiler to imshow at each frame :

    def add_image(factory, *factory_args, **factory_kwargs):
        img, extent, origin = factory.image_for_domain(
            ax._get_extent_geom(factory.crs),
            factory_args[0],
        )
        ax.imshow(
            img,
            extent=extent,
            origin=origin,
            transform=factory.crs,
            *factory_args[1:],
            **factory_kwargs
        )
    

    NB: You need to place add_image(tiler, zoom_level) right before update's return.

    enter image description here