Offline-Karten Runde 2: adaptives Modell (Budget, Funkloch-Gedaechtnis, Korridor, Coverage)

Design Rene 2026-06-06:
- Budget-Download: z14-Ringe um den Standort bis 5 MB gespeicherte Bytes
  (Stadt klein, Land gross — passend zur Funknetzdichte); client-seitig,
  Server-Region-Extract entfaellt
- Funkloch-Gedaechtnis: Tile-Miss bei aktivem GPS-Recording -> Zone gemerkt
  (lokal, nie hochgeladen); Auto-Download offener Zonen sobald online
- Routen-Korridor: 'Offline'-Button im Routen-Detail, Kacheln +-1km um den
  Track + Marker (Cap 50 MB) — fuer mehrtaegige Unternehmungen
- Coverage-Layer: gespeicherte Bereiche als blauer Layer; Offline-Button
  oeffnet Verwaltungs-Modal (Stats, speichern, anzeigen, loeschen)
- Flag-Logik zentral in boot.js BY.offlineTiles() (war 3x dupliziert)
Bump v1226
This commit is contained in:
rene 2026-06-06 12:00:43 +02:00
parent 45534aa8ee
commit 42a04ec405
12 changed files with 466 additions and 91 deletions

View file

@ -375,7 +375,7 @@ window.Page_map = (() => {
});
document.getElementById('map-offline-btn')?.addEventListener('click', () => {
_sdEl?.classList.remove('open');
if (_engineGL) _downloadVectorRegion(); // GL: Vektorkacheln → IndexedDB (byt://)
if (_engineGL) _openOfflineModal(); // GL: Verwaltung (speichern/anzeigen/löschen)
else _cacheTiles(); // Leaflet: OSM-Raster → SW-Cache
});
document.getElementById('map-radar-btn')?.addEventListener('click', () => {
@ -773,16 +773,10 @@ window.Page_map = (() => {
} catch (e) { return false; }
}
// Offline-Vektorkacheln-Flag — gleiche Default-Logik wie map-gl-style.js _offlineEnabled()
// (dupliziert, weil map-gl-style.js erst mit der GL-Karte lazy lädt, das Markup aber sofort rendert).
// Offline-Vektorkacheln-Flag — zentrale Logik in boot.js BY.offlineTiles().
// Steuert nur die Button-Sichtbarkeit: im GL-Modus ohne byt://-Quelle wäre der Download nutzlos.
function _offlineTilesEnabled() {
try {
const flag = localStorage.getItem('by_offline_tiles');
if (flag === '1') return true;
if (flag === '0') return false;
return location.hostname === 'staging.banyaro.app';
} catch (e) { return false; }
try { return !!(window.BY && BY.offlineTiles()); } catch (e) { return false; }
}
function loadMapLibre() {
@ -885,6 +879,7 @@ window.Page_map = (() => {
const el = document.getElementById('central-map');
if (!el || !window.maplibregl || _map) return;
_engineGL = true;
_covOn = false; // Bereiche-Layer-Status gehört zur Karten-Instanz
const center = _userPos ? [_userPos.lon, _userPos.lat] : [8.6821, 50.1109]; // Frankfurt [lng,lat]
const zoom = _userPos ? 14 : 10;
@ -2158,23 +2153,24 @@ window.Page_map = (() => {
return urls;
}
// GL-Modus: Vektorkacheln (5 km um die Kartenmitte) + Glyphs → IndexedDB (byt://).
// Gegenstück zum Welten-FAB-Download (GPS-Position) — hier zählt die KARTENMITTE,
// damit man eine entfernte Gegend (Urlaubsort) vorab speichern kann. docs/OFFLINE_MAPS_PLAN.md
// GL-Modus: Gebiet um die KARTENMITTE budget-getrieben (~5 MB) speichern — Stadt klein,
// Land groß (Ring-Wachstum in MapOffline). Kartenmitte statt GPS, damit man eine entfernte
// Gegend (Urlaubsort) vorab speichern kann. docs/OFFLINE_MAPS_PLAN.md
async function _downloadVectorRegion() {
if (!_map || !window.MapOffline) return;
const btn = document.getElementById('map-offline-btn');
if (btn?.classList.contains('loading')) return; // läuft bereits
const c = _map.getCenter();
btn?.classList.add('loading');
_setOsmStatus('Offline: 0 %…');
_setOsmStatus('Offline: 0 MB…');
try {
const res = await MapOffline.downloadAround(c.lat, c.lng, 5, p => {
_setOsmStatus(`Offline: ${Math.round(p.done / p.total * 100)} %`);
});
const res = await MapOffline.downloadAround(c.lat, c.lng, { budgetMB: 5, onProgress: p => {
_setOsmStatus(`Offline: ${(p.bytes / 1048576).toFixed(1)} / ${Math.round(p.budget / 1048576)} MB`);
} });
_setOsmStatus('');
UI.toast.success(`Gegend offline gespeichert — ${res.tiles} Kacheln, ${res.pois || 0} Marker, ${(res.bytes / 1048576).toFixed(1)} MB.`);
UI.toast.success(`Gegend offline gespeichert — ~${res.radiusKm} km Umkreis, ${res.pois || 0} Marker, ${(res.bytes / 1048576).toFixed(1)} MB.`);
window.OfflineIndicator?.refresh(); // Pfoten-Segment 5 sofort grün
if (_covOn) _setCoverage(true); // Bereiche-Layer aktualisieren
} catch (e) {
_setOsmStatus('');
UI.toast.error('Offline-Download fehlgeschlagen — bitte erneut versuchen.');
@ -2183,6 +2179,77 @@ window.Page_map = (() => {
}
}
// ----------------------------------------------------------
// Offline-Bereiche-Layer (gespeicherte z14-Kacheln) + Verwaltungs-Modal
// ----------------------------------------------------------
let _covOn = false;
async function _setCoverage(on) {
if (!_engineGL || !_map || !window.MapOffline) return false;
if (!on) {
try {
if (_map.getLayer('by-off-cov-line')) _map.removeLayer('by-off-cov-line');
if (_map.getLayer('by-off-cov')) _map.removeLayer('by-off-cov');
if (_map.getSource('by-off-cov')) _map.removeSource('by-off-cov');
} catch (e) {}
_covOn = false;
return false;
}
const gj = await MapOffline.coverage().catch(() => null);
if (!gj || !gj.features.length) { UI.toast.info('Noch keine Offline-Bereiche gespeichert.'); return false; }
if (_map.getSource('by-off-cov')) {
_map.getSource('by-off-cov').setData(gj);
} else {
_map.addSource('by-off-cov', { type: 'geojson', data: gj });
_map.addLayer({ id: 'by-off-cov', type: 'fill', source: 'by-off-cov',
paint: { 'fill-color': '#3b82f6', 'fill-opacity': 0.15 } });
_map.addLayer({ id: 'by-off-cov-line', type: 'line', source: 'by-off-cov',
paint: { 'line-color': '#3b82f6', 'line-opacity': 0.35, 'line-width': 0.5 } });
}
_covOn = true;
return true;
}
// Verwaltungs-Modal am Offline-Button: Stats + Gebiet speichern / Bereiche anzeigen / Löschen.
async function _openOfflineModal() {
if (!window.MapOffline) return;
let s = { regions: [] };
try { s = await MapOffline.stats(); } catch (e) {}
const regions = s.regions || [];
const totalBytes = regions.reduce((a, r) => a + (r.bytes || 0), 0);
const totalPois = regions.reduce((a, r) => a + (r.pois || 0), 0);
UI.modal.open({
title: '🗺️ Offline-Karten',
body: `
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3)">
${regions.length
? `${regions.length} ${regions.length === 1 ? 'Gebiet' : 'Gebiete'} gespeichert — ~${(totalBytes / 1048576).toFixed(1)} MB, ${totalPois} Marker.`
: 'Noch kein Gebiet gespeichert. Karte und Marker bleiben damit auch im Funkloch verfügbar.'}
</p>
<div class="flex flex-col gap-2">
<button class="btn btn-primary" id="off-dl">${UI.icon('download-simple')} Dieses Gebiet speichern (~5 MB)</button>
<button class="btn btn-secondary" id="off-cov">${UI.icon('stack')} Gespeicherte Bereiche ${_covOn ? 'ausblenden' : 'anzeigen'}</button>
${regions.length ? `<button class="btn btn-secondary" id="off-clear" style="color:var(--c-danger)">${UI.icon('trash')} Alles löschen</button>` : ''}
</div>
`,
footer: `<button class="btn btn-secondary" data-modal-close style="width:100%">Schließen</button>`,
});
document.getElementById('off-dl')?.addEventListener('click', () => { UI.modal.close(); _downloadVectorRegion(); });
document.getElementById('off-cov')?.addEventListener('click', async () => { UI.modal.close(); await _setCoverage(!_covOn); });
document.getElementById('off-clear')?.addEventListener('click', async e => {
const btn = e.currentTarget;
if (btn.dataset.confirm !== '1') { // Zweiklick statt confirm-Modal im Modal
btn.dataset.confirm = '1';
btn.innerHTML = `${UI.icon('trash')} Wirklich alles löschen?`;
return;
}
await MapOffline.clear().catch(() => {});
_setCoverage(false);
UI.modal.close();
UI.toast.success('Offline-Karten gelöscht.');
window.OfflineIndicator?.refresh();
});
}
async function _cacheTiles() {
if (!_map) return;
if (!('serviceWorker' in navigator) || !navigator.serviceWorker.controller) {
@ -2411,6 +2478,9 @@ window.Page_map = (() => {
_recDistKm += d / 1000;
}
_recTrack.push({ lat, lon });
// Funkloch-Gedächtnis: Position melden — Tile-Fetch-Fehler bei aktivem GPS
// markieren die Gegend als „Offline nötig" (lokal, map-offline.js).
window.MapOffline?.setGps({ lat, lon });
_persistRec();
_updateRecMap(lat, lon);
_updateRecStatus();
@ -2557,6 +2627,7 @@ window.Page_map = (() => {
if (_recWatchId !== null) { navigator.geolocation.clearWatch(_recWatchId); _recWatchId = null; }
if (_recTimerInt) { clearInterval(_recTimerInt); _recTimerInt = null; }
_recActive = false;
window.MapOffline?.setGps(null); // Funkloch-Erkennung nur bei aktiver Aufzeichnung
_releaseWakeLock();
_hidePocketOverlay();
document.removeEventListener('visibilitychange', _onVisibilityChange);