banyaro/docs/OFFLINE_MAPS_PLAN.md
rene f38301a391 Offline-Plan: 'Offline-Inhalte laden' mit Bereichsauswahl für Mehrtages-Wanderungen
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).
2026-06-05 18:00:59 +02:00

8.1 KiB
Raw Blame History

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 ≈ 40120 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 ~810 km Radius ab (mehr Reichweite genau dort wo die Funklöcher sind). Glyphs (~12 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 ~810 km. Optional Stufe „Groß ~16 MB" (Stadt ~10 km / Land ~1822 km) für Wandertage.
  • Zoom z014 (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:98precip = 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)).