"""Projection utilities for nereus plotting.
This module provides convenient aliases and configuration for Cartopy projections.
"""
from __future__ import annotations
from typing import Any
import cartopy.crs as ccrs
# Projection aliases mapping short names to Cartopy projection classes and defaults
PROJECTION_ALIASES: dict[str, dict[str, Any]] = {
# PlateCarree
"pc": {"class": ccrs.PlateCarree, "kwargs": {}},
"platecarree": {"class": ccrs.PlateCarree, "kwargs": {}},
# Robinson
"rob": {"class": ccrs.Robinson, "kwargs": {}, "global": True},
"robinson": {"class": ccrs.Robinson, "kwargs": {}, "global": True},
# Mercator
"merc": {"class": ccrs.Mercator, "kwargs": {}},
"mercator": {"class": ccrs.Mercator, "kwargs": {}},
# Mollweide
"moll": {"class": ccrs.Mollweide, "kwargs": {}, "global": True},
"mollweide": {"class": ccrs.Mollweide, "kwargs": {}, "global": True},
# North Polar Stereographic
"np": {
"class": ccrs.NorthPolarStereo,
"kwargs": {},
"polar": "north",
},
"npstere": {
"class": ccrs.NorthPolarStereo,
"kwargs": {},
"polar": "north",
},
"northpolarstereo": {
"class": ccrs.NorthPolarStereo,
"kwargs": {},
"polar": "north",
},
# South Polar Stereographic
"sp": {
"class": ccrs.SouthPolarStereo,
"kwargs": {},
"polar": "south",
},
"spstere": {
"class": ccrs.SouthPolarStereo,
"kwargs": {},
"polar": "south",
},
"southpolarstereo": {
"class": ccrs.SouthPolarStereo,
"kwargs": {},
"polar": "south",
},
# Orthographic
"ortho": {"class": ccrs.Orthographic, "kwargs": {"central_longitude": 0, "central_latitude": 0}},
"orthographic": {"class": ccrs.Orthographic, "kwargs": {"central_longitude": 0, "central_latitude": 0}},
# Lambert Conformal
"lcc": {
"class": ccrs.LambertConformal,
"kwargs": {"central_longitude": 0, "central_latitude": 45},
},
"lambertconformal": {
"class": ccrs.LambertConformal,
"kwargs": {"central_longitude": 0, "central_latitude": 45},
},
}
[docs]
def get_projection(
name: str | ccrs.Projection,
**kwargs: Any,
) -> ccrs.Projection:
"""Get a Cartopy projection from name or alias.
Parameters
----------
name : str or Projection
Projection name/alias or an existing Cartopy Projection.
**kwargs
Additional keyword arguments passed to the projection constructor.
Returns
-------
Projection
A Cartopy projection instance.
Raises
------
ValueError
If the projection name is not recognized.
Examples
--------
>>> proj = get_projection("pc")
>>> proj = get_projection("npstere")
>>> proj = get_projection("ortho", central_longitude=10, central_latitude=50)
"""
if isinstance(name, ccrs.Projection):
return name
name_lower = name.lower()
if name_lower not in PROJECTION_ALIASES:
valid = ", ".join(sorted(set(PROJECTION_ALIASES.keys())))
raise ValueError(
f"Unknown projection '{name}'. Valid options: {valid}"
)
config = PROJECTION_ALIASES[name_lower]
proj_kwargs = {**config["kwargs"], **kwargs}
return config["class"](**proj_kwargs)
[docs]
def is_global_projection(name: str | ccrs.Projection) -> bool:
"""Check if a projection should use set_global().
Parameters
----------
name : str or Projection
Projection name/alias or Cartopy Projection.
Returns
-------
bool
True if the projection is global.
"""
if isinstance(name, ccrs.Projection):
# Check by type
return isinstance(name, (ccrs.Robinson, ccrs.Mollweide))
name_lower = name.lower()
if name_lower in PROJECTION_ALIASES:
return PROJECTION_ALIASES[name_lower].get("global", False)
return False
[docs]
def is_polar_projection(name: str | ccrs.Projection) -> str | None:
"""Check if a projection is polar.
Parameters
----------
name : str or Projection
Projection name/alias or Cartopy Projection.
Returns
-------
str or None
"north" for north polar, "south" for south polar, None otherwise.
"""
if isinstance(name, ccrs.Projection):
if isinstance(name, ccrs.NorthPolarStereo):
return "north"
elif isinstance(name, ccrs.SouthPolarStereo):
return "south"
return None
name_lower = name.lower()
if name_lower in PROJECTION_ALIASES:
return PROJECTION_ALIASES[name_lower].get("polar")
return None
[docs]
def get_data_bounds_for_projection(
projection: str | ccrs.Projection,
extent: tuple[float, float, float, float] | None = None,
) -> tuple[tuple[float, float], tuple[float, float]]:
"""Get appropriate data bounds for a projection.
For polar projections, expands the bounds to fill the circular plot area.
Parameters
----------
projection : str or Projection
The projection being used.
extent : tuple of float, optional
Desired extent (lon_min, lon_max, lat_min, lat_max).
Returns
-------
lon_bounds, lat_bounds : tuple of tuples
Longitude and latitude bounds for data fetching.
"""
polar = is_polar_projection(projection)
if polar == "north":
# North polar: full longitude, high latitudes
lon_bounds = (-180.0, 180.0)
if extent:
lat_bounds = (max(0.0, extent[2] - 20), 90.0)
else:
lat_bounds = (0.0, 90.0)
elif polar == "south":
# South polar: full longitude, low latitudes
lon_bounds = (-180.0, 180.0)
if extent:
lat_bounds = (-90.0, min(0.0, extent[3] + 20))
else:
lat_bounds = (-90.0, 0.0)
elif extent:
# Use provided extent with small buffer
buffer = 5.0
lon_bounds = (
max(-180.0, extent[0] - buffer),
min(180.0, extent[1] + buffer),
)
lat_bounds = (
max(-90.0, extent[2] - buffer),
min(90.0, extent[3] + buffer),
)
else:
# Default: global
lon_bounds = (-180.0, 180.0)
lat_bounds = (-90.0, 90.0)
return lon_bounds, lat_bounds