Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Google Tile based on extent with Matplotlib FuncAnimation #2420

Open
augusts-bit opened this issue Jul 28, 2024 · 3 comments
Open

Update Google Tile based on extent with Matplotlib FuncAnimation #2420

augusts-bit opened this issue Jul 28, 2024 · 3 comments

Comments

@augusts-bit
Copy link

Description

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 for the entire globe. To visualise my problem, view the images attached.

initial_frame
later_frame

Code to reproduce

Function I am using:

# 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")

Without success, I have tried the following:

  • Starting the first frame(s) with large extent, hoping the imagery loads for the entire globe and then to zoom in again at later frames
  • Creating a new ax at every update and/or returning the ax at the update function
  • Creating a list of axes with an ax for every point, and then setting axes visible one by one

My question therefore is: how do I correctly update the basemap image so that it loads when the point is moving across the globe? Is this possible, or am I misunderstanding something?

@rcomer
Copy link
Member

rcomer commented Jul 28, 2024

Looks like the image is only worked out at the first draw, and there is a comment in the code that suggests there was an intention to change that at some point:

# XXX This interface needs a tidy up:
# image drawing on pan/zoom;
# caching the resulting image;
# buffering the result by 10%...;
if not self._done_img_factory:
for factory, factory_args, factory_kwargs in self.img_factories:
img, extent, origin = factory.image_for_domain(
self._get_extent_geom(factory.crs), factory_args[0])
self.imshow(img, extent=extent, origin=origin,
transform=factory.crs, *factory_args[1:],
**factory_kwargs)
self._done_img_factory = True

@greglucas
Copy link
Contributor

Note that we do have an add_raster() method which uses SlippyImageArtist

def add_raster(self, raster_source, **slippy_image_kwargs):

But, it doesn't look like GoogleTiles inherit from the RasterSource to be used with that method.

This would be nice to add some examples explaining how to use these capabilities and it probably needs some updates to the various artist classes to allow for that dynamic image grabbing from more sources.

@rcomer
Copy link
Member

rcomer commented Aug 2, 2024

I see someone has posted a workaround on StackOverflow: https://stackoverflow.com/a/78804739/3501128

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants