from __future__ import annotations from typing import Any from app.cache import cache_get, cache_set from app.config import CACHE_TTL_PRECIP_MAP, OPEN_METEO_BASE_URL from app.http_client import fetch_json BATCH_SIZE = 80 DEFAULT_SPAN_DEG = 3.0 DEFAULT_STEP_DEG = 0.25 def _coord_key(lat: float, lon: float) -> tuple[float, float]: return round(lat, 2), round(lon, 2) def _build_grid(center_lat: float, center_lon: float, span_deg: float, step_deg: float) -> list[tuple[float, float]]: half = span_deg / 2 points: list[tuple[float, float]] = [] lat = center_lat - half while lat <= center_lat + half + 1e-9: lon = center_lon - half while lon <= center_lon + half + 1e-9: points.append((round(lat, 2), round(lon, 2))) lon += step_deg lat += step_deg return points async def _fetch_batch(lats: list[float], lons: list[float], hours: int) -> list[dict[str, Any]]: data = await fetch_json( f"{OPEN_METEO_BASE_URL}/forecast", params={ "latitude": ",".join(str(v) for v in lats), "longitude": ",".join(str(v) for v in lons), "hourly": "precipitation,precipitation_probability", "forecast_hours": hours, "timezone": "Europe/Berlin", }, timeout=30.0, ) if isinstance(data, list): return data return [data] async def get_precipitation_map( lat: float, lon: float, hours: int = 24, span_deg: float = DEFAULT_SPAN_DEG, step_deg: float = DEFAULT_STEP_DEG, ) -> dict[str, Any]: hours = max(6, min(hours, 24)) span_deg = max(1.5, min(span_deg, 6.0)) step_deg = max(0.15, min(step_deg, 0.5)) lat_k, lon_k = _coord_key(lat, lon) cache_key = f"precip_map:{lat_k}:{lon_k}:{hours}:{span_deg}:{step_deg}" cached = await cache_get(cache_key) if cached: return {**cached, "cached": True} grid = _build_grid(lat, lon, span_deg, step_deg) cells: list[dict[str, Any]] = [] times: list[str] | None = None for offset in range(0, len(grid), BATCH_SIZE): batch = grid[offset : offset + BATCH_SIZE] lats = [point[0] for point in batch] lons = [point[1] for point in batch] results = await _fetch_batch(lats, lons, hours) for (point_lat, point_lon), item in zip(batch, results, strict=False): hourly = item.get("hourly") or {} cell_times = hourly.get("time") or [] if times is None: times = cell_times cells.append( { "lat": point_lat, "lon": point_lon, "precipitation": hourly.get("precipitation") or [0.0] * len(cell_times), "probability": hourly.get("precipitation_probability") or [0] * len(cell_times), } ) payload = { "source": OPEN_METEO_BASE_URL, "model": "Open-Meteo ICON/DWD", "center": {"lat": lat, "lon": lon}, "span_deg": span_deg, "step_deg": step_deg, "hours": hours, "times": times or [], "cells": cells, "cell_count": len(cells), "note": "Modellvorhersage, keine Radarmessung. Auflösung grob (~25 km).", "cached": False, } await cache_set(cache_key, payload, CACHE_TTL_PRECIP_MAP) return payload