# SPDX-License-Identifier: Apache-2.0
"""Basemap helpers for Cartopy/Matplotlib renderers.
Functions here are intentionally lightweight and avoid hard dependencies
on optional tile providers. Tile support is stubbed and only enabled if
the dependency is available at runtime.
"""
from __future__ import annotations
from typing import Iterable, Optional
[docs]
def add_basemap_cartopy(
ax,
extent: Optional[Iterable[float]] = None,
*,
image_path: Optional[str] = None,
features: Optional[Iterable[str]] = None,
alpha: float = 1.0,
):
"""Add a simple basemap to a Cartopy axis.
Parameters
----------
ax : cartopy.mpl.geoaxes.GeoAxesSubplot
Target axes with a geographic projection (PlateCarree recommended).
extent : iterable of float, optional
[west, east, south, north] in PlateCarree coordinates.
image_path : str, optional
Path to a background image to draw via ``imshow``.
features : iterable of str, optional
Feature names to add: any of {"coastline", "borders", "gridlines"}.
alpha : float, default=1.0
Opacity for the background image.
"""
# Lazy imports to avoid heavy deps at import-time
import cartopy.crs as ccrs
import cartopy.feature as cfeature
import matplotlib.pyplot as plt
if extent is not None:
try:
ax.set_extent(extent, crs=ccrs.PlateCarree())
except Exception:
pass
if image_path:
try:
img = plt.imread(image_path)
ax.imshow(
img,
origin="upper",
extent=extent or [-180, 180, -90, 90],
transform=ccrs.PlateCarree(),
alpha=alpha,
)
except Exception:
# Swallow image read failures; caller may still draw data
pass
if features:
for feat in features:
f = (feat or "").lower()
if f == "coastline":
ax.add_feature(cfeature.COASTLINE, linewidth=0.5, edgecolor="#333333CC")
elif f == "borders":
ax.add_feature(cfeature.BORDERS, linewidth=0.5, edgecolor="#33333380")
elif f == "gridlines":
try:
gl = ax.gridlines(
draw_labels=False, linewidth=0.2, color="#00000033"
)
gl.xlocator = None # let Cartopy choose
gl.ylocator = None
except Exception:
pass
[docs]
def add_basemap_tile(
ax,
extent: Optional[Iterable[float]] = None,
*,
tile_source: str | None = None,
zoom: int = 3,
):
"""Add a tile basemap using contextily, if available.
Notes
-----
- This is a best-effort helper. If ``contextily`` is not installed
or tiles cannot be fetched (e.g., no network), the function returns
without raising.
- The axis is expected to use PlateCarree.
"""
try:
import cartopy.crs as ccrs
import contextily as cx # type: ignore
except Exception:
return # graceful no-op
if extent is not None:
try:
ax.set_extent(extent, crs=ccrs.PlateCarree())
except Exception:
pass
try:
# contextily expects Web Mercator typically; let it handle reprojection.
# We draw into the Matplotlib axis backing the Cartopy GeoAxes.
if tile_source is None:
cx.add_basemap(ax, zoom=zoom)
else:
cx.add_basemap(ax, source=tile_source, zoom=zoom)
except Exception:
# Network or provider errors are ignored.
return