DWD-Regenvorhersage: Pipeline + /radar-Route + Timeline-Integration + Settings-Toggle
PoC BESTANDEN (tools/dwd-radar/poc): Anker (9E,51N) = Pixel-Mitte (470/600),
Ecken decken sich mit der DWD-DE1200-Spec — Georeferenzierung bewiesen.
- tools/dwd-radar: RV-Komposit (25 Frames, 0-120min) -> kolorierte RGBA-
PMTiles z4-7 je Frame (MapLibre overzoomt darueber) + manifest.json,
atomarer Swap, KEEP_RUNS-Aufraeumen; 25 Frames in ~14s lokal
- docker-compose.dwd.yml (DSM-Cron alle 5 min, NIE --remove-orphans)
- main.py: /radar/manifest.json (no-store) + /radar/{run}/{file} (Range/206,
immutable — Run-Id im Pfad); sw.js: /radar/ pass-through
- map.js: Radar-Frames heterogen ({url,time,dwd}) — DWD ersetzt RainViewer-
Nowcast (0-120min, 5-min-Schritte) wenn Toggle an + GL + Karte in DE +
Manifest frisch (<30min); sonst RainViewer-Fallback; Label '+X Min - DWD'
- settings.js: Toggle 'DWD-Regenvorhersage' (by_dwd_radar, Default AN)
- pytest 39 passed
Bump v1240
This commit is contained in:
parent
6a06c9be7e
commit
5330681059
17 changed files with 4685 additions and 23 deletions
|
|
@ -387,6 +387,60 @@ _FONTS_DIR = os.getenv("FONTS_DIR", os.path.join(_TILES_DIR, "fonts"))
|
|||
if os.path.isdir(_FONTS_DIR):
|
||||
app.mount("/fonts", StaticFiles(directory=_FONTS_DIR), name="fonts")
|
||||
|
||||
# DWD-Regenvorhersage (tools/dwd-radar, Cron alle 5 Min): run-<id>/rv_*.pmtiles + manifest.json.
|
||||
_RADAR_DIR = os.getenv("RADAR_DIR", "/data/radar")
|
||||
|
||||
|
||||
@app.get("/radar/manifest.json")
|
||||
async def radar_manifest():
|
||||
path = os.path.join(_RADAR_DIR, "manifest.json")
|
||||
if not os.path.isfile(path):
|
||||
return Response(status_code=404)
|
||||
# Immer frisch: der Inhalt wechselt alle 5 Min bei GLEICHER URL
|
||||
return FileResponse(path, media_type="application/json",
|
||||
headers={"Cache-Control": "no-store"})
|
||||
|
||||
|
||||
@app.api_route("/radar/{run}/{filename}", methods=["GET", "HEAD"])
|
||||
async def serve_radar(run: str, filename: str, request: Request):
|
||||
# Kein Path-Traversal; Run-Verzeichnis ist content-stabil (Run-Id im Pfad) → lange cachebar.
|
||||
for part in (run, filename):
|
||||
if "/" in part or "\\" in part or ".." in part:
|
||||
return Response(status_code=404)
|
||||
path = os.path.join(_RADAR_DIR, run, filename)
|
||||
if not os.path.isfile(path):
|
||||
return Response(status_code=404)
|
||||
file_size = os.path.getsize(path)
|
||||
_etag = f'"{file_size:x}-{int(os.path.getmtime(path)):x}"'
|
||||
base_headers = {"Accept-Ranges": "bytes", "ETag": _etag,
|
||||
"Cache-Control": "public, max-age=3600, immutable"}
|
||||
if request.method == "HEAD":
|
||||
return Response(status_code=200, media_type="application/octet-stream",
|
||||
headers={**base_headers, "Content-Length": str(file_size)})
|
||||
range_header = request.headers.get("range")
|
||||
if range_header and range_header.startswith("bytes="):
|
||||
rng = range_header[6:].split(",")[0]
|
||||
start_s, _, end_s = rng.partition("-")
|
||||
try:
|
||||
if start_s == "":
|
||||
length = int(end_s)
|
||||
start = max(0, file_size - length)
|
||||
end = file_size - 1
|
||||
else:
|
||||
start = int(start_s)
|
||||
end = int(end_s) if end_s else file_size - 1
|
||||
except ValueError:
|
||||
return Response(status_code=416, headers={**base_headers, "Content-Range": f"bytes */{file_size}"})
|
||||
end = min(end, file_size - 1)
|
||||
if start > end or start >= file_size:
|
||||
return Response(status_code=416, headers={**base_headers, "Content-Range": f"bytes */{file_size}"})
|
||||
with open(path, "rb") as f:
|
||||
f.seek(start)
|
||||
data = f.read(end - start + 1)
|
||||
return Response(data, status_code=206, media_type="application/octet-stream",
|
||||
headers={**base_headers, "Content-Range": f"bytes {start}-{end}/{file_size}"})
|
||||
return FileResponse(path, media_type="application/octet-stream", headers=base_headers)
|
||||
|
||||
@app.api_route("/tiles/{filename}", methods=["GET", "HEAD"])
|
||||
async def serve_tile(filename: str, request: Request):
|
||||
# Kein Path-Traversal
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue