#!/usr/bin/env python3 # -*- coding: utf-8 -*- """Basemap tiles for matplotlib maps (road weather, ice, snow radar). Prefer tile sources that do **not** require an API key: - OpenStreetMap Mapnik — OK with an identifying User-Agent (contextily's default UA gets "403 Access blocked" tiles) - Esri WorldStreetMap — free for reasonable use, good road detail - OpenTopoMap — terrain fallback CARTO basemaps are avoided: they now overlay "API KEY REQUIRED". Compatible with contextily 1.6.x (loogle-casa) and 1.7.x (loogle-bot): in 1.6, ``headers=`` is forwarded to ``imshow`` and breaks the call, so we set ``contextily.tile.USER_AGENT`` instead. """ from __future__ import annotations import inspect import logging from typing import Any, Optional LOGGER = logging.getLogger("basemap_tiles") BASEMAP_USER_AGENT = "LoogleCasa/1.0 (https://casa.loogle.it; meteo maps)" BASEMAP_HEADERS = {"User-Agent": BASEMAP_USER_AGENT} def _configure_user_agent() -> None: try: import contextily.tile as tile_mod tile_mod.USER_AGENT = BASEMAP_USER_AGENT except Exception as exc: # noqa: BLE001 LOGGER.debug("Impossibile impostare USER_AGENT contextily: %s", exc) def _providers(ctx) -> list: """Ordered list of free (no API key) basemap providers.""" out = [] candidates = [ ("OpenStreetMap", "Mapnik"), ("Esri", "WorldStreetMap"), ("OpenTopoMap",), ("OpenStreetMap", "DE"), ("OpenStreetMap", "HOT"), ] for path in candidates: try: node = ctx.providers for part in path: node = getattr(node, part) # skip leaf namespaces that aren't a provider dict if hasattr(node, "url") or (isinstance(node, dict) and "url" in node): out.append(node) elif path == ("OpenTopoMap",): # xyzservices LeafMap style out.append(node) except AttributeError: continue return out def add_basemap( ax: Any, *, alpha: float = 0.85, attribution_size: int = 6, crs: str = "EPSG:4326", ) -> bool: """Add a web basemap under *ax*. Returns True on success.""" try: import contextily as ctx except ImportError: LOGGER.warning("contextily non disponibile: mappa senza basemap") return False _configure_user_agent() providers = _providers(ctx) if not providers: LOGGER.warning("Nessun provider basemap trovato in contextily") return False call_kwargs: dict = { "crs": crs, "alpha": alpha, "attribution_size": attribution_size, } try: if "headers" in inspect.signature(ctx.add_basemap).parameters: call_kwargs["headers"] = BASEMAP_HEADERS except (TypeError, ValueError): pass last_err: Optional[BaseException] = None for source in providers: name = getattr(source, "name", None) or ( source.get("name") if isinstance(source, dict) else str(source) ) try: ctx.add_basemap(ax, source=source, **call_kwargs) LOGGER.info("Basemap OK: %s", name) return True except Exception as exc: # noqa: BLE001 last_err = exc LOGGER.warning("Basemap %s fallita: %s", name, exc) if last_err: LOGGER.warning("Nessuna basemap disponibile: %s", last_err) return False