Compare commits
2 commits
ea2cdd4f89
...
72ee339860
| Author | SHA1 | Date | |
|---|---|---|---|
| 72ee339860 | |||
| ac187dc740 |
8 changed files with 87 additions and 24 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
1217
|
1219
|
||||||
|
|
@ -3053,17 +3053,18 @@ html.modal-open {
|
||||||
.map-legend::-webkit-scrollbar { display: none; }
|
.map-legend::-webkit-scrollbar { display: none; }
|
||||||
|
|
||||||
/* Regenradar-Zeitleiste (RainViewer: ~2h Vergangenheit + ~30min Nowcast, Play/Pause + Slider) */
|
/* Regenradar-Zeitleiste (RainViewer: ~2h Vergangenheit + ~30min Nowcast, Play/Pause + Slider) */
|
||||||
/* Optik wie die Status-Pill darunter (hell/cremefarben, Blur, Border), direkt darüber gesetzt. */
|
/* Optik + Maße wie die Status-Pill darunter: gleiche linke Kante (var(--space-3)); die Breite wird
|
||||||
|
per JS an die Pill angeglichen (gleiche rechte Kante). Höhe wie die Pill. */
|
||||||
.map-radar-timeline {
|
.map-radar-timeline {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: var(--space-3);
|
left: var(--space-3);
|
||||||
right: 88px; /* Platz für die Ecken-FABs (Speed-Dial / Zurück) rechts */
|
width: min(320px, calc(100% - 100px)); /* Fallback; JS setzt = Pill-Breite */
|
||||||
bottom: calc(var(--space-3) + 40px); /* unmittelbar über der Status-Pill */
|
bottom: calc(var(--space-3) + 34px); /* unmittelbar über der Status-Pill */
|
||||||
z-index: 900;
|
z-index: 900;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items:center;
|
align-items:center;
|
||||||
gap: 8px;
|
gap: 7px;
|
||||||
padding: 4px 12px 4px 5px;
|
padding: 3px 12px 3px 4px;
|
||||||
border-radius: var(--radius-full);
|
border-radius: var(--radius-full);
|
||||||
background: rgba(255, 255, 255, 0.88);
|
background: rgba(255, 255, 255, 0.88);
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur(4px);
|
||||||
|
|
@ -3071,6 +3072,7 @@ html.modal-open {
|
||||||
border: 1px solid var(--c-border-light);
|
border: 1px solid var(--c-border-light);
|
||||||
color: var(--c-text);
|
color: var(--c-text);
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
:root[data-theme="dark"] .map-radar-timeline {
|
:root[data-theme="dark"] .map-radar-timeline {
|
||||||
background: rgba(24, 20, 16, 0.92);
|
background: rgba(24, 20, 16, 0.92);
|
||||||
|
|
@ -3078,19 +3080,20 @@ html.modal-open {
|
||||||
}
|
}
|
||||||
.rdr-play {
|
.rdr-play {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 30px; height: 30px;
|
width: 24px; height: 24px;
|
||||||
border: none; border-radius: 50%;
|
border: none; border-radius: 50%;
|
||||||
background: var(--c-surface-2);
|
background: var(--c-surface-2);
|
||||||
color: var(--c-text); cursor: pointer;
|
color: var(--c-text); cursor: pointer;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
}
|
}
|
||||||
|
.rdr-play svg { width: 14px; height: 14px; }
|
||||||
.rdr-play:active { background: var(--c-border); }
|
.rdr-play:active { background: var(--c-border); }
|
||||||
.rdr-slider { flex: 1; min-width: 0; height: 4px; accent-color: var(--c-primary); cursor: pointer; }
|
.rdr-slider { flex: 1; min-width: 0; height: 4px; accent-color: var(--c-primary); cursor: pointer; }
|
||||||
.rdr-time {
|
.rdr-time {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
font-size: 11px; font-weight: 600;
|
font-size: 11px; font-weight: 600;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
min-width: 78px; text-align: right; color: var(--c-text-secondary);
|
min-width: 74px; text-align: right; color: var(--c-text-secondary);
|
||||||
}
|
}
|
||||||
.rdr-time.is-forecast { color: var(--c-primary); } /* Nowcast/Vorhersage-Frames hervorgehoben */
|
.rdr-time.is-forecast { color: var(--c-primary); } /* Nowcast/Vorhersage-Frames hervorgehoben */
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,14 +86,14 @@
|
||||||
<title>Ban Yaro</title>
|
<title>Ban Yaro</title>
|
||||||
|
|
||||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||||
<script src="/js/boot-early.js?v=1217"></script>
|
<script src="/js/boot-early.js?v=1219"></script>
|
||||||
|
|
||||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||||
<link rel="stylesheet" href="/css/design-system.css?v=1217">
|
<link rel="stylesheet" href="/css/design-system.css?v=1219">
|
||||||
<link rel="stylesheet" href="/css/layout.css?v=1217">
|
<link rel="stylesheet" href="/css/layout.css?v=1219">
|
||||||
<link rel="stylesheet" href="/css/components.css?v=1217">
|
<link rel="stylesheet" href="/css/components.css?v=1219">
|
||||||
<link rel="stylesheet" href="/css/utilities.css?v=1217">
|
<link rel="stylesheet" href="/css/utilities.css?v=1219">
|
||||||
<link rel="stylesheet" href="/css/lists.css?v=1217">
|
<link rel="stylesheet" href="/css/lists.css?v=1219">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -617,11 +617,11 @@
|
||||||
<div id="modal-container"></div>
|
<div id="modal-container"></div>
|
||||||
|
|
||||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||||
<script src="/js/api.js?v=1217"></script>
|
<script src="/js/api.js?v=1219"></script>
|
||||||
<script src="/js/ui.js?v=1217"></script>
|
<script src="/js/ui.js?v=1219"></script>
|
||||||
<script src="/js/app.js?v=1217"></script>
|
<script src="/js/app.js?v=1219"></script>
|
||||||
<script src="/js/worlds.js?v=1217"></script>
|
<script src="/js/worlds.js?v=1219"></script>
|
||||||
<script src="/js/offline-indicator.js?v=1217"></script>
|
<script src="/js/offline-indicator.js?v=1219"></script>
|
||||||
|
|
||||||
<!-- Feature-Seiten werden lazy geladen -->
|
<!-- Feature-Seiten werden lazy geladen -->
|
||||||
|
|
||||||
|
|
@ -631,7 +631,7 @@
|
||||||
|
|
||||||
|
|
||||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||||
<script src="/js/boot.js?v=1217"></script>
|
<script src="/js/boot.js?v=1219"></script>
|
||||||
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Router, State-Management, Navigation, Initialisierung.
|
Router, State-Management, Navigation, Initialisierung.
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
const APP_VER = '1217'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
const APP_VER = '1219'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||||
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
const APP_VERSION = '1.6.0'; // ← semantische Version, wird bei make release gesetzt
|
||||||
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
|
window.APP_VER = APP_VER; // global verfügbar für andere Module (z.B. offline-indicator)
|
||||||
window.APP_VERSION = APP_VERSION;
|
window.APP_VERSION = APP_VERSION;
|
||||||
|
|
|
||||||
|
|
@ -621,12 +621,16 @@ window.Page_map = (() => {
|
||||||
document.getElementById('central-map')?.appendChild(el);
|
document.getElementById('central-map')?.appendChild(el);
|
||||||
el.querySelector('#rdr-play').addEventListener('click', _toggleRadarPlay);
|
el.querySelector('#rdr-play').addEventListener('click', _toggleRadarPlay);
|
||||||
el.querySelector('#rdr-slider').addEventListener('input', e => {
|
el.querySelector('#rdr-slider').addEventListener('input', e => {
|
||||||
|
const idx = parseInt(e.target.value, 10); // ZUERST lesen: _radarPause() setzt slider.value zurück
|
||||||
_radarPause();
|
_radarPause();
|
||||||
_showRadarFrame(parseInt(e.target.value, 10));
|
_showRadarFrame(idx);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
el.querySelector('#rdr-slider').max = _radarFrames.length - 1;
|
el.querySelector('#rdr-slider').max = _radarFrames.length - 1;
|
||||||
}
|
}
|
||||||
|
// Breite an die Status-Pill angleichen → gleiche linke + rechte Kante.
|
||||||
|
const pill = document.querySelector('.map-statusbar');
|
||||||
|
if (pill && pill.offsetWidth > 60) el.style.width = pill.offsetWidth + 'px';
|
||||||
_updateRadarTimelineUI();
|
_updateRadarTimelineUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<script src="/js/landing-init.js?v=1217"></script>
|
<script src="/js/landing-init.js?v=1219"></script>
|
||||||
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
<title>Ban Yaro — Die Hunde-App für Deutschland, Österreich & Schweiz</title>
|
||||||
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
|
<meta name="description" content="Ban Yaro: Die kostenlose All-in-One Hunde-App für DACH. Tagebuch, Giftköder-Alarm, Training mit KI, Forum, Wurfbörse, Stammbaum, Inzucht-Check — DSGVO-konform, offline-fähig, ohne App Store.">
|
||||||
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
<meta name="keywords" content="Hunde App, Hunde Community, Wurfbörse, Züchter, Welpen kaufen, Stammbaum Hund, Inzuchtkoeffizient, Hundezucht, Impfpass Hund, Giftköder Alarm, Gassi Community, Hundetraining App, Hunde Forum, Hunde KI, Hundefilm Datenbank, Welpen Marktplatz">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
// ← EINZIGE Stelle für die Version — STATIC_ASSETS und CACHE_VERSION leiten sich ab
|
||||||
const VER = '1217';
|
const VER = '1219';
|
||||||
const CACHE_VERSION = `by-v${VER}`;
|
const CACHE_VERSION = `by-v${VER}`;
|
||||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||||
|
|
|
||||||
56
docs/DWD_RAIN_FORECAST_PLAN.md
Normal file
56
docs/DWD_RAIN_FORECAST_PLAN.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# DWD Regen-Vorhersage (Radar-Nowcast) — Scoping-Plan
|
||||||
|
|
||||||
|
**Status:** gescoppt + Datenformat verifiziert (2026-06-05). Umsetzung offen.
|
||||||
|
**Ziel:** Verlässliche, längere Regen-**Vorhersage** als animiertes Karten-Overlay (bis +2 h) statt RainViewers
|
||||||
|
unzuverlässigem 30-Min-Nowcast (der oft leer ist). Self-hosted wie die Basemap — passt zur Tile-Server-Philosophie.
|
||||||
|
|
||||||
|
## Quelle: DWD RV (Composite RV) — kostenlos, kein API-Key
|
||||||
|
- `https://opendata.dwd.de/weather/radar/composite/rv/DE1200_RV<YYMMDDHHMM>.tar.bz2`
|
||||||
|
- **Alle 5 Min** publiziert. Jedes Archiv = ein Vorhersage-Lauf mit **25 Frames** `_000`…`_120`
|
||||||
|
(**0 bis +120 Min**, 5-Min-Schritte), je ~2,5 MB unkomprimiert (~1 MB als .tar.bz2).
|
||||||
|
- **Format (verifiziert):** RADOLAN-Binär. 194-Byte-ASCII-Header bis `ETX (0x03)`, dann **1200×1100 uint16
|
||||||
|
little-endian** (= 2.640.000 Byte). Header-Felder: `PR E-02` (0,01 mm), `INT 5` (5-Min-Summe),
|
||||||
|
`GP1200x1100`, `VV<lead>` (Lead-Time). **Wert = `raw & 0x0FFF` × 0,01 mm/5min; `raw & 0x2000` = kein Daten.**
|
||||||
|
→ Decode trivial, **kein wradlib nötig** (PoC: 1,32 Mio Zellen geparst, Regen korrekt erkannt).
|
||||||
|
- **Gitter/Projektion:** DE1200 (1 km), polar-stereografisch, fest georeferenziert (Eckkoordinaten dokumentiert;
|
||||||
|
wradlib `get_radolan_grid` ODER GDAL mit dem bekannten RADOLAN-PROJ-String).
|
||||||
|
- **Abdeckung:** Deutschland + Randbereiche (reicht etwas nach AT/CH/Nachbarn, aber DE-zentriert).
|
||||||
|
Voll-AT/CH bräuchte ACG/MeteoSwiss → out of scope.
|
||||||
|
|
||||||
|
## Pipeline (Server-seitig, Cron alle 5 Min — analog zum OSM-POI-Job)
|
||||||
|
1. **Fetch** neueste `DE1200_RV<time>.tar.bz2` (Verzeichnis-Listing → letzte Datei).
|
||||||
|
2. **Entpacken** → 25 RADOLAN-Grids.
|
||||||
|
3. **Decode** je Grid → 2D-Niederschlags-Array (eigener ~15-Z.-Parser, PoC-bewiesen).
|
||||||
|
4. **Kolorieren** → RGBA (transparent bei 0/kein-Regen; Radar-Farbskala wie das aktuelle Overlay).
|
||||||
|
5. **Reprojektion** DE1200 → EPSG:3857 + **Kacheln z0–9** (Radar ist grob 1 km → höher zoomen bringt nichts).
|
||||||
|
Tooling: **GDAL** (gdalwarp + gdal2tiles) oder rasterio/rio-tiler.
|
||||||
|
6. **Output:** 25 Frame-Tilesets — je eine kleine **PMTiles** pro Lead-Time (`rv_000.pmtiles`…`rv_120.pmtiles`)
|
||||||
|
ODER XYZ-PNG. Nur neuesten Lauf behalten (atomarer Swap wie dach.pmtiles). Plus **`rv_manifest.json`**
|
||||||
|
(Lauf-Zeit, Lead-Times, Tile-URL-Muster).
|
||||||
|
7. **Ausliefern** von der DS (wie `/tiles`).
|
||||||
|
|
||||||
|
## Frontend (bestehende Radar-Timeline erweitern, map.js)
|
||||||
|
- Manifest laden → DWD-Vorhersage-Frames (0…+120 Min) **rechts von „jetzt"** in die Timeline einhängen
|
||||||
|
(die Forecast-Markierung `is-forecast` + Scrub/Play gibt es schon).
|
||||||
|
- **Vergangenheit:** weiter RainViewer (einfach) ODER DWD RADOLAN-RY (5-Min-Analyse) für all-DWD-Konsistenz.
|
||||||
|
- Quelle pro Frame: Vergangenheit = RainViewer-Tiles, Vorhersage = DWD-PMTiles (byt-/raster-Source).
|
||||||
|
|
||||||
|
## Aufwand & offene Entscheidungen
|
||||||
|
- **Decode:** trivial (verifiziert). **Projektion:** der einzige Knackpunkt — DE1200-Georeferenzierung korrekt
|
||||||
|
nach 3857 (wradlib nimmt's ab, oder bekannter PROJ-String + Eckkoordinaten). **PoC nötig:** 1 Frame →
|
||||||
|
Tiles → über MapLibre rendern und gegen RainViewer/echten Regen gegenchecken (Passgenauigkeit).
|
||||||
|
- **Tiling alle 5 Min × 25 Frames:** z0–9 für DE ist schnell (Sekunden–~1 Min auf der DS). Last beachten
|
||||||
|
(läuft neben Immich & Co.); ggf. nur jeden 2. Lead-Time tilen (10-Min-Schritte) zum Sparen.
|
||||||
|
- **Speicher:** Radar-Tiles sind dünn/transparent → wenige MB pro Lauf.
|
||||||
|
- **Cron alle 5 Min:** neuer Container/Job (analog `docker-compose.osm.yml`); ⚠️ `--remove-orphans`-Falle.
|
||||||
|
- **Entscheidungen:** (a) Vergangenheit RainViewer vs. DWD-RY; (b) PMTiles-pro-Frame vs. XYZ; (c) Farbskala;
|
||||||
|
(d) Zoom-Range (z0–9) + Lead-Schrittweite (5 vs 10 Min); (e) Container-Stack (GDAL/Python) auf der DS.
|
||||||
|
- **Abhängigkeit:** Docker auf der DS.
|
||||||
|
|
||||||
|
## Nächste Schritte
|
||||||
|
1. **PoC**: 1 RV-Archiv → 1 Frame decode → kolorieren → reprojizieren → 1 PMTiles → headless über MapLibre
|
||||||
|
rendern. **Kernfrage: stimmt die Georeferenzierung?** (Wenn ja, ist der Rest Fleißarbeit.)
|
||||||
|
2. Pipeline-Skript (fetch→decode→tile→deploy) + Cron-Job + Manifest.
|
||||||
|
3. Frontend: Manifest in die Timeline einhängen (Vorhersage-Frames rechts von „jetzt").
|
||||||
|
|
||||||
|
Siehe `docs/TILE_SERVER_HANDOVER.md` (Tile-Infra), Memory `project_tile_server_maintenance`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue