Source code for hydro_topo_features.visualization.static

"""Functions for creating static maps of hydro-topological features."""

import os
import logging
from pathlib import Path
from typing import Dict, Optional, Union
import numpy as np
import matplotlib.pyplot as plt
import rasterio
import geopandas as gpd
import cartopy.crs as ccrs
from cartopy.mpl.gridliner import LONGITUDE_FORMATTER, LATITUDE_FORMATTER
import matplotlib.ticker as mticker
from geemap import cartoee
from .. import config

logger = logging.getLogger(__name__)

[docs] def plot_static_map( site_id: str, raster_path: str, aoi_path: str = None, cmap: str = 'terrain', vmin: float = None, vmax: float = None, Name: str = None, Unit: str = None, aoi_color: str = None, aoi_linestyle: str = None, aoi_linewidth: int = None, show_grid: bool = None, show_lon_lat: bool = None, show_scale_bar: bool = None, scale_bar_length: int = None, scale_bar_color: str = None, scale_bar_unit: str = None, bbox_buffer: float = None, figsize: tuple = (18, 6), dpi: int = None, output_dirs: Optional[Dict[str, Path]] = None ) -> str: """ Create a static map visualization of a raster dataset. Args: site_id: Unique identifier for the site raster_path: Path to the raster file to plot aoi_path: Path to the AOI shapefile/geopackage cmap: Matplotlib colormap name vmin: Minimum value for colormap vmax: Maximum value for colormap Name: Name of the feature for the title Unit: Unit for the colorbar label aoi_color: Color for AOI boundary aoi_linestyle: Line style for AOI boundary aoi_linewidth: Line width for AOI boundary show_grid: Whether to show grid lines show_lon_lat: Whether to show lon/lat labels show_scale_bar: Whether to show scale bar scale_bar_length: Length of scale bar scale_bar_color: Color of scale bar scale_bar_unit: Unit for scale bar bbox_buffer: Buffer around data extent figsize: Figure size in inches dpi: Resolution for saved figure output_dirs: Dictionary of output directories Returns: Path to saved figure """ logger.info(f"Creating static map for {Name if Name else 'raster'}") # Set default output directories if not provided if output_dirs is None: site_dir = Path(config.DEFAULT_OUTPUT_DIR) / site_id figures_dir = site_dir / config.DIRECTORY_STRUCTURE["FIGURES"] / config.DIRECTORY_STRUCTURE["STATIC"] os.makedirs(figures_dir, exist_ok=True) else: figures_dir = output_dirs["static_figures"] # Output path feature_name = Name.lower() if Name else Path(raster_path).stem output_path = figures_dir / f"{feature_name}_map.svg" # Set defaults from config if not provided vis_config = config.STATIC_VIS aoi_color = aoi_color or vis_config['aoi_color'] aoi_linestyle = aoi_linestyle or vis_config['aoi_linestyle'] aoi_linewidth = aoi_linewidth or vis_config['aoi_linewidth'] show_grid = show_grid if show_grid is not None else vis_config['show_grid'] show_lon_lat = show_lon_lat if show_lon_lat is not None else vis_config['show_lon_lat'] show_scale_bar = show_scale_bar if show_scale_bar is not None else vis_config['show_scale_bar'] scale_bar_length = scale_bar_length or vis_config['scale_bar_length'] scale_bar_color = scale_bar_color or vis_config['scale_bar_color'] scale_bar_unit = scale_bar_unit or vis_config['scale_bar_unit'] bbox_buffer = bbox_buffer or vis_config['bbox_buffer'] dpi = dpi or vis_config['dpi'] # Set font properties plt.rcParams['font.family'] = vis_config['font'] # Create figure and axis fig, ax = plt.subplots(figsize=figsize, subplot_kw={'projection': ccrs.PlateCarree()}) # Read and plot raster with rasterio.open(raster_path) as src: data = src.read(1) extent = [src.bounds.left, src.bounds.right, src.bounds.bottom, src.bounds.top] if vmin is None: vmin = np.nanmin(data) if vmax is None: vmax = np.nanmax(data) im = ax.imshow( data, extent=extent, origin='upper', cmap=cmap, vmin=vmin, vmax=vmax ) # Add AOI if provided if aoi_path: aoi = gpd.read_file(aoi_path) if aoi.crs != 'EPSG:4326': aoi = aoi.to_crs('EPSG:4326') aoi.boundary.plot( ax=ax, color=aoi_color, linestyle=aoi_linestyle, linewidth=aoi_linewidth, label='AOI' ) # Set extent from AOI bounds = aoi.total_bounds ax.set_extent([ bounds[0] - bbox_buffer, bounds[2] + bbox_buffer, bounds[1] - bbox_buffer, bounds[3] + bbox_buffer ]) # Add gridlines if requested if show_grid: gl = ax.gridlines( draw_labels=show_lon_lat, linewidth=0.5, color='gray', alpha=0.5, linestyle='--' ) if show_lon_lat: gl.top_labels = False gl.right_labels = False gl.xformatter = LONGITUDE_FORMATTER gl.yformatter = LATITUDE_FORMATTER gl.xlabel_style = {'size': vis_config['fontsize_axes']} gl.ylabel_style = {'size': vis_config['fontsize_axes']} gl.xlocator = mticker.MaxNLocator(nbins=3) gl.ylocator = mticker.MaxNLocator(nbins=3) # Add colorbar cbar = fig.colorbar( im, ax=ax, orientation='vertical', pad=0.02, fraction=vis_config['colorbar_width'], shrink=vis_config['colorbar_height'] ) # Ensure a tick at the max value if vmin is not None and vmax is not None: # Create tick locations with first tick at vmin and last tick at vmax tick_count = 5 ticks = np.linspace(vmin, vmax, tick_count) cbar.set_ticks(ticks) # Optional: Format tick labels if needed # cbar.set_ticklabels([f"{t:.2f}" for t in ticks]) if Unit: cbar.set_label( f"{Name} ({Unit})" if Name else Unit, size=vis_config['fontsize_colorbar'] ) cbar.ax.tick_params(labelsize=vis_config['fontsize_colorbar']) # Add scale bar if requested if show_scale_bar: cartoee.add_scale_bar_lite( ax, length=scale_bar_length, xy=(0.8, 0.05), linewidth=2, fontsize=vis_config['fontsize_axes'], color=scale_bar_color, unit=scale_bar_unit ) # Add title if provided if Name: ax.set_title(Name, fontsize=vis_config['fontsize_title']) # Remove spines for spine in ax.spines.values(): spine.set_visible(False) # Save figure fig.savefig(output_path, bbox_inches='tight', dpi=dpi, transparent=True) plt.close() logger.info(f"Static map saved to: {output_path}") return str(output_path)