Zwei Modi: Aktuelle Gegend (budget-getrieben, Gassi) + Bereich auswählen (Mehrtagestour) — Karten-Viewport/Rechteck/Routen-Korridor, Größen-Vorschau vor Download (PMTiles-Directory aufsummieren), Liste gespeicherter Gebiete. Budget gilt im Bereichs-Modus nicht (bewusste Wahl).
116 lines
8.1 KiB
Markdown
116 lines
8.1 KiB
Markdown
# Offline-Karten (GL/Vektor) — Feature-Plan
|
||
|
||
**Status:** geplant (Umsetzung nach Tile-Build/Produktions-Rollout der GL-Karten).
|
||
**Stand:** 2026-06-05. Autor: René + Claude (Design).
|
||
|
||
## Ziel
|
||
GL-Vektorkarten offline-tauglich machen — Kernszenario **Gassi/Wandern im Funkloch**.
|
||
Selbst-zielend (cacht wo nötig, nicht überall), speichersparsam, ohne Nutzeraufwand.
|
||
|
||
## Problem (warum GL aktuell NICHT offline geht)
|
||
- PMTiles lädt per **HTTP-Range (206 Partial Content)**. Die **Cache-API kann 206 nicht speichern**
|
||
(`cache.put()` wirft) → Basemap-Kacheln landen nie im Offline-Cache.
|
||
- SW hat **keine Regel für `/tiles`** (nur für `tile.openstreetmap.org` = altes Raster).
|
||
- `offline-indicator.js` prefetcht weiterhin **OSM-Raster** (a.tile.openstreetmap.org), das die GL-Karte
|
||
gar nicht nutzt → doppelter Regress: Raster gecacht das niemand zeigt, GL-Karte offline trotzdem leer.
|
||
- Folge offline heute: App + Daten da, aber GL-Karte = Routenlinie/Marker auf **leerem Hintergrund**.
|
||
|
||
## Gemessene Speicher-Fakten (an echter dach.pmtiles, maxzoom=14 + Overzoom bis ~16)
|
||
**Referenz-Radius = 5 km** (René, 2026-06-05: „5 km genügen"). Messungen:
|
||
| Gebiet | Fläche | Tiles | Größe |
|
||
|---|---|---|---|
|
||
| **München (48,1/11,5), dicht — 5 km** | 10×10 km | 82 | **6,4 MB** |
|
||
| **Bayerischer Wald, ländlich — 5 km** | 10×10 km | 99 | **2,6 MB** |
|
||
| (Kontext) München 10 km | 20×20 km | 252 | 15 MB |
|
||
| (Kontext) Bayerischer Wald 10 km | 20×20 km | 285 | 4,4 MB |
|
||
| (Kontext) Bayerischer Wald ~25 km | 50×50 km | 1.595 | 20 MB |
|
||
|
||
→ Vektor ist ~10× sparsamer als Raster (Raster 5 km ≈ 40–120 MB). Stadt-Tiles ~2,5× dicker als Land.
|
||
→ 5 km ist NICHT 1/4 von 10 km (6,4 vs 15 MB) — die unteren Zoomstufen (Übersicht) sind immer dabei,
|
||
unabhängig von der bbox-Größe.
|
||
→ **Budget ≈ 7 MB** (5 km dichte Stadt + Glyphs). Budget-getrieben deckt das in der Stadt ~5 km, auf dem
|
||
**Land ~8–10 km** Radius ab (mehr Reichweite genau dort wo die Funklöcher sind). Glyphs (~1–2 MB) +
|
||
Style (winzig) → ~8 MB pro Gegend. Sehr sparsam → viele Gegenden problemlos (10 ≈ 80 MB).
|
||
→ Messmethode (reproduzierbar): `docker run --rm -v /tmp/pmt:/out protomaps/go-pmtiles:latest extract
|
||
https://staging.banyaro.app/tiles/dach.pmtiles /out/x.pmtiles --bbox=W,S,E,N` → Dateigröße ablesen.
|
||
|
||
## Architektur
|
||
### Region-Extract (budget-getrieben, NICHT fester Radius)
|
||
- PMTiles-**Directory enthält pro Tile die Byte-Länge** → Server kann die Größe einer Region
|
||
**aufsummieren OHNE die Tiles zu laden**.
|
||
- Endpoint `GET /tiles/region?lat=&lon=&budget=7` (MB): wächst die bbox um die Position, bis die
|
||
summierte Tile-Länge ≈ Budget erreicht (Stadt → kleiner Radius, Land → großer Radius), extrahiert dann
|
||
genau diese Region als `region.pmtiles` (**ein 200er**, ~7 MB). `pmtiles extract` (go-pmtiles) oder
|
||
python-pmtiles im Container.
|
||
- Client lädt die Datei einmal → **IndexedDB** (Blob; 200er, anders als die 206-Ranges cachebar).
|
||
- MapLibre liest **offline** aus dem lokalen Blob via `pmtiles://` (pmtiles.js kann aus ArrayBuffer lesen);
|
||
**online** weiter remote `dach.pmtiles` (immer aktuell, ganz DACH+Anrainer). Source je nach Verbindung wählen.
|
||
- Glyphs (`/fonts/*.pbf`, Open Sans Regular+Semibold) mit cachen (200er, cachebar).
|
||
|
||
### Adaptive Strategie (der eigentliche Clou — lernt von selbst)
|
||
1. **Rollendes Vorausladen beim Aufzeichnen:** Solange GPS aktiv UND Empfang da, fortlaufend Tiles um die
|
||
**aktuelle Position** cachen. Deckt den echten Weg + die Anfahrt automatisch ab — auch beim ersten Mal,
|
||
bevor man ins Funkloch läuft.
|
||
2. **Funkloch-Gedächtnis:** Wo echte Requests **scheitern** (Timeout/Fehler während aktivem GPS — NICHT
|
||
`navigator.onLine`, das lügt bei Captive-Portal/Schwachempfang), den Bereich als „Offline nötig"
|
||
markieren → priorisiert behalten, beim nächsten Online-Durchgang großzügiger nachladen.
|
||
Caveat: im Funkloch selbst kann nicht geladen werden → greift ab dem 2. Besuch (Gassi = repetitiv → ok).
|
||
3. **Manuelles Vorab-Laden** („Offline-Inhalte laden"-Button) — zwei Modi:
|
||
- **Aktuelle Gegend** (Default, Gassi): budget-getrieben um die Position (~7 MB), ein Tipp.
|
||
- **Bereich auswählen** (mehrtägige Wanderung — Auto-5km reicht da nicht): Nutzer wählt ein größeres
|
||
Gebiet, das ganz heruntergeladen wird. Auswahl-Optionen:
|
||
- **Karten-Ausschnitt:** aktuellen Karten-Viewport (durch Zoomen/Verschieben gewählter bbox) als
|
||
Download-Gebiet nehmen — simpel, kein Zeichnen nötig.
|
||
- **Rechteck ziehen** auf der Karte (präziser).
|
||
- **Routen-Korridor:** entlang einer geplanten/gewählten Route ± Puffer (ideal für Touren, die einer
|
||
Strecke folgen — viel sparsamer als eine große bbox).
|
||
- **Größen-Vorschau VOR dem Download:** die Tile-Byte-Längen aus der PMTiles-Directory aufsummieren
|
||
(kein Tile-Download nötig) → „~45 MB" anzeigen, Nutzer bestätigt. Schützt vor versehentlichem Riesen-Download.
|
||
- Fortschritt + „X MB gespeichert" + Liste gespeicherter Gebiete (umbenennen/löschen/aktualisieren).
|
||
- Im Bereichs-Modus gilt das ~7-MB-Budget NICHT (Nutzer entscheidet bewusst), aber eine sinnvolle
|
||
Obergrenze + der globale Speicher-Cap greifen.
|
||
|
||
### Drumherum
|
||
- **Budget-Cap + LRU:** Gesamtspeicher gedeckelt; selten besuchte Funkloch-Caches fallen raus.
|
||
- **Privatsphäre:** „Wo verliere ich Netz" = Aufenthaltsorte → **komplett lokal (IndexedDB), nie hochgeladen.**
|
||
- **Aktualität:** Offline-Region beim nächsten Online-Sein neu ziehbar (Basemap-Updates / pmtiles-Refresh).
|
||
- **Aufräumen:** Den alten OSM-Raster-Prefetch in `offline-indicator.js` ablösen/abschalten (cacht ungenutztes Raster).
|
||
- **Pfoten-Offline-Indikator (Welten-FAB) anpassen:** `offline-indicator.js` füllt 5 Pfoten-Segmente;
|
||
**Segment 5 „Karten-Kacheln"** prüft aktuell `CACHE_TILES` auf ≥ N **OSM-Raster**-Tiles (TILE_PREFETCH
|
||
z14/z13). Die GL-Karte nutzt dieses Raster NICHT → grünes Segment = falsches „Karte offline bereit".
|
||
→ Segment 5 umdefinieren: prüfen ob für die aktuelle Gegend eine **`region.pmtiles` + Glyphs** lokal
|
||
vorliegen (statt Raster-Tile-Count). `offline-fill-btn` („Offline-Inhalte laden") soll dann den
|
||
Region-Download anstoßen statt den Raster-Prefetch. (Bei Variante 1 „Raster-Fallback" bliebe Segment 5
|
||
wie es ist — Entscheidung hängt an der Offline-Strategie oben.)
|
||
|
||
## Offene Entscheidungen / Defaults
|
||
- Budget-Default **~7 MB** (Referenz 5 km Stadt; René 2026-06-05). Stadt ~5 km / Land ~8–10 km.
|
||
Optional Stufe „Groß ~16 MB" (Stadt ~10 km / Land ~18–22 km) für Wandertage.
|
||
- Zoom z0–14 (Overzoom liefert Straßenebene gratis).
|
||
- Detektionssignal = echte Fetch-Timeouts bei aktivem GPS (nicht `navigator.onLine`).
|
||
- Speicher = IndexedDB (Blobs); MapLibre-Source-Umschaltung online/offline.
|
||
|
||
## Abhängigkeiten
|
||
- GL-Tiles in **Produktion** (dach.pmtiles + fonts auf Prod-Volume) — Voraussetzung.
|
||
- pmtiles-Directory-Byte-Summierung (Server) + pmtiles.js Blob-Source (Client).
|
||
- WebGL-Kontext-Disziplin beachten (siehe Skill/Memory: jede GL-Karte beim Schließen `remove()`).
|
||
|
||
Siehe `docs/TILE_SERVER_HANDOVER.md` (Tile-Pipeline) + Memory `project_tile_server_maintenance`.
|
||
|
||
---
|
||
|
||
# Weitere Karten-To-Dos (nicht offline-spezifisch)
|
||
|
||
## Wetter-Chip: Niederschlag „nächste 3 Std" statt ganzer Tag
|
||
**Ist:** Der Karten-Chip unten zeigt die Regenwahrscheinlichkeit als **Tages-Maximum**
|
||
(`backend/weather.py:98` → `precip = daily['precipitation_probability_max'][0]`; angezeigt in
|
||
`pages/map.js:~2598` als `💧 {w.precip_prob}%`). Über den ganzen Tag gemittelt/maximiert = wenig
|
||
aussagekräftig für „soll ich JETZT raus".
|
||
|
||
**Soll:** Den **höchsten Wert der nächsten 3 Stunden** (ab aktueller Stunde) zeigen.
|
||
- Die stündlichen Daten werden in `weather.py` bereits geladen (`&hourly=precipitation_probability`,
|
||
Array `h_precip` ab Zeile ~116) — kein neuer API-Call nötig.
|
||
- Ändern: `precip_prob = max(h_precip[now_idx : now_idx+3])` (aktuellen Stundenindex bestimmen wie bei
|
||
der bestehenden `next_rain_time`-Logik). `next_rain_time`/Warnungen können bleiben.
|
||
- Optional Chip-Text klarstellen, dass sich der Wert auf die nächsten 3 h bezieht (z.B. `💧 {x}% (3h)`).
|
||
|