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:
rene 2026-06-06 18:08:57 +02:00
parent 6a06c9be7e
commit 5330681059
17 changed files with 4685 additions and 23 deletions

View file

@ -584,17 +584,57 @@ window.Page_map = (() => {
document.getElementById('central-map')?.appendChild(el);
}
// DWD-Regenvorhersage (Settings-Toggle, Default AN) — nur im GL-Modus sinnvoll
// (PMTiles-Protokoll) und innerhalb der DE1200-Abdeckung.
function _dwdEnabled() {
try { return localStorage.getItem('by_dwd_radar') !== '0'; } catch (e) { return true; }
}
function _mapInDwdCoverage() {
try {
const c = _map.getCenter(); // beide Engines: {lat, lng}
return c.lng >= 1.5 && c.lng <= 18.7 && c.lat >= 45.7 && c.lat <= 56.2;
} catch (e) { return false; }
}
async function _loadRadar() {
if (!_radarActive || !_map) return;
try {
const resp = await fetch('https://api.rainviewer.com/public/weather-maps.json', { cache: 'no-store' });
const data = await resp.json();
const past = data.radar?.past || [], nowcast = data.radar?.nowcast || [];
const frames = [...past, ...nowcast];
if (!frames.length) return;
_radarHost = data.host || _radarHost;
_radarFrames = frames.map(f => ({ path: f.path, time: f.time }));
_radarNowIdx = Math.max(0, past.length - 1); // "jetzt" = letzter Vergangenheits-Frame
if (!past.length && !nowcast.length) return;
_radarHost = data.host || _radarHost;
const rvUrl = f => `${_radarHost}${f.path}/256/{z}/{x}/{y}/4/1_1.png`;
// Default: RainViewer komplett (~2h Vergangenheit + ~30 min Nowcast)
let frames = [...past, ...nowcast].map(f => ({ url: rvUrl(f), time: f.time }));
let nowIdx = Math.max(0, past.length - 1); // "jetzt" = letzter Vergangenheits-Frame
// DWD-Vorhersage (0120 min, 5-Min-Schritte): ersetzt den RainViewer-Nowcast,
// Vergangenheit bleibt RainViewer (docs/DWD_RAIN_FORECAST_PLAN.md).
if (_dwdEnabled() && _engineGL && _mapInDwdCoverage()) {
try {
const r = await fetch('/radar/manifest.json', { cache: 'no-store' });
if (r.ok) {
const man = await r.json();
const runT = Math.floor(Date.parse(man.run_time_utc) / 1000);
// Nur wenn der Lauf frisch ist (< 30 min) — sonst RainViewer-Fallback
if (man.frames?.length && (Date.now() / 1000 - runT) < 1800) {
const pastRv = past.filter(f => f.time <= runT).map(f => ({ url: rvUrl(f), time: f.time }));
const dwd = man.frames.map(fr => ({
url: `pmtiles://${location.origin}/radar/${man.path}/${fr.file}/{z}/{x}/{y}`,
time: runT + fr.lead_min * 60,
dwd: true,
}));
frames = [...pastRv, ...dwd];
nowIdx = pastRv.length; // DWD lead 0 = "jetzt"
}
}
} catch (e) { /* offline/kein Manifest → RainViewer-Fallback */ }
}
_radarFrames = frames;
_radarNowIdx = nowIdx;
if (_radarIdx == null || _radarIdx >= _radarFrames.length) _radarIdx = _radarNowIdx;
_showRadarFrame(_radarIdx);
_buildRadarTimeline();
@ -602,7 +642,7 @@ window.Page_map = (() => {
}
function _radarUrl(idx) {
return `${_radarHost}${_radarFrames[idx].path}/256/{z}/{x}/{y}/4/1_1.png`;
return _radarFrames[idx].url;
}
// Frame anzeigen — wenn möglich smooth via setTiles (kein Flackern), sonst Layer neu.
@ -661,7 +701,7 @@ window.Page_map = (() => {
const hhmm = `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
const diffMin = Math.round((f.time - _radarFrames[_radarNowIdx].time) / 60);
const rel = diffMin === 0 ? 'jetzt' : (diffMin > 0 ? `+${diffMin} Min` : `${diffMin} Min`);
timeEl.textContent = `${hhmm} · ${rel}`;
timeEl.textContent = `${hhmm} · ${rel}${f.dwd && diffMin > 0 ? ' · DWD' : ''}`;
timeEl.classList.toggle('is-forecast', diffMin > 0);
}
}

View file

@ -825,6 +825,30 @@ window.Page_settings = (() => {
</div>`;
})()}
<!-- DWD-Regenvorhersage (Deutschland) speist die Karten-Radar-Timeline -->
<div class="settings-toggle-row">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#cloud-rain"></use></svg>
<div class="settings-toggle-label">
<div style="font-weight:500">DWD-Regenvorhersage</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);margin-top:2px">
2-Stunden-Vorhersage vom Deutschen Wetterdienst im Regenradar (nur Deutschland)
</div>
</div>
<label style="position:relative;display:inline-block;width:44px;height:24px;flex-shrink:0">
<input type="checkbox" id="toggle-dwd-radar"
style="opacity:0;width:0;height:0;position:absolute"
${localStorage.getItem('by_dwd_radar') !== '0' ? 'checked' : ''}>
<span style="position:absolute;cursor:pointer;inset:0;border-radius:12px;
background:${localStorage.getItem('by_dwd_radar') !== '0' ? 'var(--c-primary)' : 'var(--c-border)'};transition:.2s"
id="toggle-dwd-radar-track"></span>
<span id="toggle-dwd-radar-thumb"
style="position:absolute;top:2px;left:${localStorage.getItem('by_dwd_radar') !== '0' ? '22px' : '2px'};
width:20px;height:20px;border-radius:50%;
background:#fff;transition:.2s;
box-shadow:0 1px 3px rgba(0,0,0,.3)"></span>
</label>
</div>
<!-- Goldene Gassi-Stunde -->
<div class="settings-toggle-row" style="border-bottom:none">
<svg class="ph-icon" aria-hidden="true" style="width:1.25rem;height:1.25rem"><use href="/icons/phosphor.svg#paw-print"></use></svg>
@ -1583,6 +1607,19 @@ window.Page_settings = (() => {
: 'Pocket-Modus deaktiviert.');
});
// DWD-Regenvorhersage (Default AN) — ausgewertet in pages/map.js _dwdEnabled()
document.getElementById('toggle-dwd-radar')?.addEventListener('change', e => {
const on = e.target.checked;
localStorage.setItem('by_dwd_radar', on ? '1' : '0');
const track = document.getElementById('toggle-dwd-radar-track');
const thumb = document.getElementById('toggle-dwd-radar-thumb');
if (track) track.style.background = on ? 'var(--c-primary)' : 'var(--c-border)';
if (thumb) thumb.style.left = on ? '22px' : '2px';
UI.toast.info(on
? 'DWD-Regenvorhersage aktiviert — 2h-Vorhersage im Regenradar (Deutschland).'
: 'DWD-Regenvorhersage deaktiviert — Regenradar nutzt RainViewer.');
});
document.getElementById('toggle-notes-ki')?.addEventListener('change', async e => {
const enabled = e.target.checked;
const track = document.getElementById('toggle-notes-ki-track');