HexaWetter v1.3.0: Add precipitation map feature, update README, and enhance UI elements
This commit is contained in:
102
api/app/services/precipitation_map.py
Normal file
102
api/app/services/precipitation_map.py
Normal file
@@ -0,0 +1,102 @@
|
||||
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
|
||||
Reference in New Issue
Block a user