Rene: Funkloecher + Routen waren nach 'Alles loeschen' weiter weg.
- Funkloch-Regionen jetzt im Keep-Set (geloescht wird NUR Manuelles);
Zonen behalten ihren Fuellstatus (Komplett-Wipe setzt weiter zurueck)
- Korridor-Migration beim Loeschen: keepTracks=[{name,track}] schreibt
Tracks in Alt-Eintraege ohne r.track (Bestand vor v1236) bzw. legt
fehlende Korridor-Regionen an — kein Warten auf Self-Healing
- clear() liefert Summary; Toast zeigt 'behalten: Standort, X Routen,
Y Funkloch-Gebiete' — Diagnose-Sichtbarkeit fuer Geraetetests
Bump v1237
894 lines
44 KiB
JavaScript
894 lines
44 KiB
JavaScript
/* ============================================================
|
||
BAN YARO — Offline-Vektorkacheln
|
||
Lädt einen Bereich aus der Remote-PMTiles (dach.pmtiles) als einzelne MVT-Tiles
|
||
in IndexedDB und bedient MapLibre offline daraus über das `byt://`-Protokoll.
|
||
Plan/Architektur: docs/OFFLINE_MAPS_PLAN.md
|
||
============================================================ */
|
||
window.MapOffline = (function () {
|
||
'use strict';
|
||
|
||
var DB_NAME = 'by-offline-tiles', STORE = 'tiles', META = 'meta', DB_VER = 1;
|
||
var MAXZOOM = 14; // unsere pmtiles enden bei z14 (Overzoom darüber)
|
||
var _db = null, _pm = null;
|
||
// Hinweis: pmtiles.getZxy() liefert die Tiles BEREITS dekomprimiert (rohe MVT-Protobufs) →
|
||
// wir speichern/servieren sie direkt, kein gunzip. Dadurch ist die IndexedDB-Größe ~2,5× die
|
||
// komprimierte pmtiles-Extract-Größe (5 km ≈ ~16 MB statt 6,4 MB) — fürs Handy unkritisch.
|
||
|
||
// ---- IndexedDB ----
|
||
function _open() {
|
||
if (_db) return Promise.resolve(_db);
|
||
return new Promise(function (res, rej) {
|
||
var r = indexedDB.open(DB_NAME, DB_VER);
|
||
r.onupgradeneeded = function () {
|
||
var d = r.result;
|
||
if (!d.objectStoreNames.contains(STORE)) d.createObjectStore(STORE);
|
||
if (!d.objectStoreNames.contains(META)) d.createObjectStore(META);
|
||
};
|
||
r.onsuccess = function () { _db = r.result; res(_db); };
|
||
r.onerror = function () { rej(r.error); };
|
||
});
|
||
}
|
||
function _req(store, mode, make) {
|
||
return _open().then(function (d) { return new Promise(function (res, rej) {
|
||
var tx = d.transaction(store, mode), rq = make(tx.objectStore(store));
|
||
tx.oncomplete = function () { res(rq ? rq.result : undefined); };
|
||
tx.onerror = function () { rej(tx.error); };
|
||
}); });
|
||
}
|
||
var _get = function (k) { return _req(STORE, 'readonly', function (os) { return os.get(k); }); };
|
||
var _put = function (k, v) { return _req(STORE, 'readwrite', function (os) { os.put(v, k); }); };
|
||
var _del = function (k) { return _req(STORE, 'readwrite', function (os) { os.delete(k); }); };
|
||
var _count = function () { return _req(STORE, 'readonly', function (os) { return os.count(); }); };
|
||
// Viele Keys in EINER Transaktion löschen (einzelne _del-Transaktionen wären zu langsam).
|
||
function _delMany(keys) {
|
||
if (!keys.length) return Promise.resolve();
|
||
return _open().then(function (d) { return new Promise(function (res, rej) {
|
||
var tx = d.transaction(STORE, 'readwrite'), os = tx.objectStore(STORE);
|
||
keys.forEach(function (k) { os.delete(k); });
|
||
tx.oncomplete = function () { res(); };
|
||
tx.onerror = function () { rej(tx.error); };
|
||
}); });
|
||
}
|
||
var _metaGet = function (k) { return _req(META, 'readonly', function (os) { return os.get(k); }); };
|
||
var _metaPut = function (k, v) { return _req(META, 'readwrite', function (os) { os.put(v, k); }); };
|
||
|
||
// ---- Remote-PMTiles ----
|
||
function _pmInst() { if (!_pm) _pm = new pmtiles.PMTiles(MapGLStyle.tilesUrl()); return _pm; }
|
||
|
||
// MVT-Bytes (Uint8Array) für z/x/y — IndexedDB zuerst, sonst remote (online), sonst null.
|
||
function tile(z, x, y) {
|
||
return _get(z + '/' + x + '/' + y).then(function (hit) {
|
||
if (hit) return hit instanceof Uint8Array ? hit : new Uint8Array(hit);
|
||
return _pmInst().getZxy(z, x, y).then(function (r) {
|
||
return (r && r.data) ? new Uint8Array(r.data) : null; // getZxy ist bereits dekomprimiert
|
||
}).catch(function () {
|
||
_noteRemoteMiss(); // Funkloch-Signal: echter Fetch-Fehler bei aktivem GPS
|
||
return null; // offline + nicht gespeichert → leeres Tile
|
||
});
|
||
});
|
||
}
|
||
|
||
// MapLibre-Protokolle registrieren (idempotent):
|
||
// byt://t/{z}/{x}/{y} → Vektorkachel (MVT)
|
||
// byt://f/{fontstack}/{range} → Glyph-PBF (fontstack ggf. URL-encodiert, je nach MapLibre-Version)
|
||
function registerProtocol() {
|
||
if (registerProtocol._done || typeof maplibregl === 'undefined') return;
|
||
registerProtocol._done = true;
|
||
maplibregl.addProtocol('byt', function (params) {
|
||
var ret = function (u) {
|
||
if (!u) return { data: new ArrayBuffer(0) };
|
||
return { data: u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength) };
|
||
};
|
||
var t = /byt:\/\/t\/(\d+)\/(\d+)\/(\d+)/.exec(params.url);
|
||
if (t) return tile(+t[1], +t[2], +t[3]).then(ret);
|
||
var f = /byt:\/\/f\/([^/]+)\/(\d+-\d+)/.exec(params.url);
|
||
if (f) return glyph(decodeURIComponent(f[1]), f[2]).then(ret);
|
||
return Promise.resolve({ data: new ArrayBuffer(0) });
|
||
});
|
||
}
|
||
|
||
// ---- Slippy-Tile-Mathe ----
|
||
function _x(lon, z) { return Math.floor((lon + 180) / 360 * Math.pow(2, z)); }
|
||
function _y(lat, z) {
|
||
var r = lat * Math.PI / 180;
|
||
return Math.floor((1 - Math.log(Math.tan(r) + 1 / Math.cos(r)) / Math.PI) / 2 * Math.pow(2, z));
|
||
}
|
||
function _bboxAround(lat, lon, radiusKm) {
|
||
var dLat = radiusKm / 111, dLon = radiusKm / (111 * Math.cos(lat * Math.PI / 180));
|
||
return { south: lat - dLat, west: lon - dLon, north: lat + dLat, east: lon + dLon };
|
||
}
|
||
|
||
// Glyphs (Open Sans Regular/Semibold, Latin + Latin-Extended) in IndexedDB persistieren
|
||
// (Key-Präfix 'f/' im Tiles-Store, kein Schema-Bump) — überlebt App-Updates, anders als der
|
||
// SW-Cache, der beim Update gepurged wird. Offline serviert übers byt://f/-Protokoll.
|
||
// KRITISCH: ohne Glyphs lässt MapLibre offline die GANZE Kachel fallen (nicht nur die Labels) → leer.
|
||
// 0-255 + 256-511 deckt DE/FR/PL/CZ/IT-Sonderzeichen ab.
|
||
var FONTS = ['Open Sans Regular', 'Open Sans Semibold'], RANGES = ['0-255', '256-511'];
|
||
function _glyphUrl(font, range) { return '/fonts/' + encodeURIComponent(font) + '/' + range + '.pbf'; }
|
||
function _cacheGlyphs() {
|
||
var bytes = 0, jobs = [];
|
||
FONTS.forEach(function (f) { RANGES.forEach(function (rg) {
|
||
jobs.push(fetch(_glyphUrl(f, rg))
|
||
.then(function (r) { return r.ok ? r.arrayBuffer() : null; })
|
||
.then(function (b) {
|
||
if (!b) return;
|
||
bytes += b.byteLength;
|
||
return _put('f/' + f + '/' + rg, new Uint8Array(b));
|
||
})
|
||
.catch(function () {}));
|
||
}); });
|
||
return Promise.all(jobs).then(function () { return bytes; });
|
||
}
|
||
|
||
// Glyph-Bytes für fontstack/range — IndexedDB zuerst, sonst remote (online), sonst null.
|
||
function glyph(font, range) {
|
||
return _get('f/' + font + '/' + range).then(function (hit) {
|
||
if (hit) return hit instanceof Uint8Array ? hit : new Uint8Array(hit);
|
||
return fetch(_glyphUrl(font, range))
|
||
.then(function (r) { return r.ok ? r.arrayBuffer() : null; })
|
||
.then(function (b) { return b ? new Uint8Array(b) : null; })
|
||
.catch(function () { return null; });
|
||
});
|
||
}
|
||
|
||
// POI-Marker der Region mitspeichern (Key-Präfix 'p/<type>' im Tiles-Store) — sonst steht die
|
||
// Offline-Karte ohne Marker da (Gerätetest 2026-06-06). Quelle: /api/osm/pois (liest die lokale
|
||
// osm_pois-DB, fast=true). Typen = Werte von OSM_LAYER_MAP in pages/map.js (synchron halten).
|
||
var POI_TYPES = ['waste_basket', 'dog_park', 'drinking_water', 'tierarzt', 'hundesalon', 'shop',
|
||
'restaurant', 'bank', 'giftkoeder', 'kotbeutel', 'gefahr', 'parkplatz',
|
||
'treffpunkt', 'sonstiges', 'hotel'];
|
||
|
||
// Frische Liste mit Bestand mergen (per id) — eine zweite Region (Urlaubsort) darf die
|
||
// erste nicht löschen. WICHTIG für Warnungen (René 2026-06-08): Die Server-Antwort ist
|
||
// für die geladene Bbox AUTORITATIV — Bestands-Einträge INNERHALB der Bbox, die nicht
|
||
// mehr geliefert werden (aufgehobener Giftköder, gefundener Hund, wegen Meldungen
|
||
// ausgeblendeter Marker), werden ENTFERNT. fresh == null (Fetch fehlgeschlagen) merged
|
||
// NIE — sonst würde ein Offline-Refresh den Bestand wegputzen.
|
||
function _mergeStore(key, fresh, bbox) {
|
||
if (fresh == null) return Promise.resolve(0);
|
||
return _get(key).then(function (old) {
|
||
var seen = {};
|
||
fresh.forEach(function (p) { seen[p.id] = true; });
|
||
var keep = (old || []).filter(function (p) {
|
||
if (seen[p.id]) return false;
|
||
if (bbox && p.lat >= bbox.south && p.lat <= bbox.north &&
|
||
p.lon >= bbox.west && p.lon <= bbox.east) return false; // in Bbox, nicht mehr da → weg
|
||
return true;
|
||
});
|
||
var merged = fresh.concat(keep);
|
||
if (!merged.length && !(old && old.length)) return 0;
|
||
return _put(key, merged).then(function () { return fresh.length; });
|
||
});
|
||
}
|
||
|
||
function _cachePois(bbox) {
|
||
var total = 0;
|
||
var jobs = POI_TYPES.map(function (type) {
|
||
var params = new URLSearchParams({ type: type, fast: 'true',
|
||
south: bbox.south, west: bbox.west, north: bbox.north, east: bbox.east });
|
||
return fetch('/api/osm/pois?' + params)
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (fresh) { return _mergeStore('p/' + type, fresh, bbox); })
|
||
.then(function (n) { total += n; })
|
||
.catch(function () {});
|
||
});
|
||
|
||
// Sicherheitsdaten MÜSSEN offline da sein (René 2026-06-07): Giftköder-Alarme
|
||
// (/api/poison, Radius in m) + vermisste Hunde (/api/lost, Radius in km) — beide anonym.
|
||
// Fetch-Radius ×√2: der Kreis muss die Bbox-ECKEN abdecken, sonst räumt der
|
||
// Bbox-Replace in _mergeStore dort fälschlich Bestand weg.
|
||
var midLat = (bbox.south + bbox.north) / 2, midLon = (bbox.west + bbox.east) / 2;
|
||
var radiusKm = Math.max(
|
||
(bbox.north - bbox.south) * 111 / 2,
|
||
(bbox.east - bbox.west) * 111 * Math.cos(midLat * Math.PI / 180) / 2, 1);
|
||
jobs.push(fetch('/api/poison?lat=' + midLat + '&lon=' + midLon + '&radius=' + Math.round(radiusKm * 1415))
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (fresh) { return _mergeStore('p/_poison', fresh, bbox); })
|
||
.then(function (n) { total += n; })
|
||
.catch(function () {}));
|
||
jobs.push(fetch('/api/lost?lat=' + midLat + '&lon=' + midLon + '&radius_km=' + Math.ceil(radiusKm * 1.42))
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (fresh) { return _mergeStore('p/_lost', fresh, bbox); })
|
||
.then(function (n) { total += n; })
|
||
.catch(function () {}));
|
||
|
||
return Promise.all(jobs).then(function () { return total; });
|
||
}
|
||
|
||
// Warnungen aktuell halten (René 2026-06-08: Giftköder können aufgehoben, Hunde gefunden
|
||
// sein): max. alle 24 h den 50-km-Umkreis der Position frisch laden — der Bbox-Replace
|
||
// in _mergeStore räumt Aufgehobenes weg. Bbox (~35 km Halbseite) liegt IM Fetch-Kreis.
|
||
function _refreshAlerts(lat, lon) {
|
||
return _metaGet('alertsTs').then(function (ts) {
|
||
if (ts && Date.now() - ts < 86400000) return;
|
||
var bb = { south: lat - 0.31, north: lat + 0.31, west: lon - 0.47, east: lon + 0.47 };
|
||
return Promise.all([
|
||
fetch('/api/poison?lat=' + lat + '&lon=' + lon + '&radius=50000')
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (f) { return _mergeStore('p/_poison', f, bb); })
|
||
.catch(function () {}),
|
||
fetch('/api/lost?lat=' + lat + '&lon=' + lon + '&radius_km=50')
|
||
.then(function (r) { return r.ok ? r.json() : null; })
|
||
.then(function (f) { return _mergeStore('p/_lost', f, bb); })
|
||
.catch(function () {}),
|
||
]).then(function () { return _metaPut('alertsTs', Date.now()); });
|
||
}).catch(function () {});
|
||
}
|
||
|
||
// Gespeicherte Sicherheits-Alarme ('poison' | 'lost') im Bbox-Ausschnitt — Offline-Fallback.
|
||
function alerts(kind, bbox) {
|
||
return _get('p/_' + kind).then(function (list) {
|
||
if (!list || !list.length) return [];
|
||
return list.filter(function (p) {
|
||
return p.lat >= bbox.south && p.lat <= bbox.north && p.lon >= bbox.west && p.lon <= bbox.east;
|
||
});
|
||
}).catch(function () { return []; });
|
||
}
|
||
|
||
// Gespeicherte POIs eines Typs im Bbox-Ausschnitt — Offline-Fallback für die Karten-Marker.
|
||
function pois(type, bbox) {
|
||
return _get('p/' + type).then(function (list) {
|
||
if (!list || !list.length) return [];
|
||
return list.filter(function (p) {
|
||
return p.lat >= bbox.south && p.lat <= bbox.north && p.lon >= bbox.west && p.lon <= bbox.east;
|
||
});
|
||
}).catch(function () { return []; });
|
||
}
|
||
|
||
// Kachelbreite (km) auf Zoom z bei lat — für Ring↔Radius-Umrechnung.
|
||
function _tileKm(z, lat) { return 40075 * Math.cos(lat * Math.PI / 180) / Math.pow(2, z); }
|
||
|
||
// Kachel-Liste laden (Concurrency 6) — addiert auf state {done,bytes,stored}, ruft onTick alle 8 Tiles.
|
||
function _fetchTiles(list, state, onTick) {
|
||
var i = 0, CONC = 6;
|
||
function next() {
|
||
if (i >= list.length) return Promise.resolve();
|
||
var t = list[i++], key = t[0] + '/' + t[1] + '/' + t[2];
|
||
return _pmInst().getZxy(t[0], t[1], t[2]).then(function (r) {
|
||
if (r && r.data) { var u = new Uint8Array(r.data); state.bytes += u.byteLength; state.stored++; return _put(key, u); }
|
||
}).catch(function () {}).then(function () {
|
||
state.done++;
|
||
if (onTick && state.done % 8 === 0) onTick();
|
||
return next();
|
||
});
|
||
}
|
||
var w = []; for (var k = 0; k < CONC; k++) w.push(next());
|
||
return Promise.all(w);
|
||
}
|
||
|
||
// Gespeichertes Gebiet in der Regions-Liste verbuchen ('region' = letztes, Back-Compat).
|
||
// Gleicher Typ + Name (z.B. Korridor derselben Route erneut geladen) ersetzt den Alt-Eintrag.
|
||
function _addRegion(region) {
|
||
return _metaGet('regions').then(function (list) {
|
||
list = (list || []).filter(function (r) {
|
||
return !(region.name && r.type === region.type && r.name === region.name);
|
||
});
|
||
list.push(region);
|
||
if (list.length > 30) list = list.slice(-30);
|
||
return _metaPut('regions', list);
|
||
}).then(function () { return _metaPut('region', region); });
|
||
}
|
||
|
||
// Gebiet um lat/lon BUDGET-getrieben laden (Modell René 2026-06-07): z14-Ringe um den
|
||
// Standort expandieren (+ Eltern-Kacheln z10–13), bis budgetMB GESPEICHERTE Bytes erreicht
|
||
// sind — deckt in der Stadt einen kleineren, auf dem Land einen größeren Bereich ab
|
||
// (dort sind die Funklöcher). Basis-Zooms 0–9 sind immer dabei (winzig).
|
||
// opts {budgetMB:5, maxRadiusKm:25, onProgress({bytes,budget,done,radiusKm})}.
|
||
// Liefert {tiles, bytes, pois, radiusKm}.
|
||
function downloadAround(lat, lon, opts) {
|
||
if (typeof opts === 'number') opts = {}; // alte Signatur (lat, lon, radiusKm) → Default-Budget
|
||
opts = opts || {};
|
||
_persistStorage();
|
||
var budget = (opts.budgetMB || 5) * 1048576;
|
||
var maxKm = opts.maxRadiusKm || 25;
|
||
var cx = _x(lon, MAXZOOM), cy = _y(lat, MAXZOOM);
|
||
var kmPerTile = _tileKm(MAXZOOM, lat);
|
||
var maxRing = Math.max(1, Math.ceil(maxKm / kmPerTile));
|
||
var seen = {}, state = { done: 0, bytes: 0, stored: 0 };
|
||
var tick = function (radiusKm) {
|
||
if (opts.onProgress) opts.onProgress({ bytes: state.bytes, budget: budget, done: state.done, radiusKm: radiusKm });
|
||
};
|
||
|
||
// Basis-Zooms 0–9 über die maximale Bbox.
|
||
var base = [], bb = _bboxAround(lat, lon, maxKm);
|
||
for (var z = 0; z <= 9; z++) {
|
||
var x0 = _x(bb.west, z), x1 = _x(bb.east, z), y0 = _y(bb.north, z), y1 = _y(bb.south, z);
|
||
for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) {
|
||
var k = z + '/' + x + '/' + y;
|
||
if (!seen[k]) { seen[k] = 1; base.push([z, x, y]); }
|
||
}
|
||
}
|
||
|
||
// Ring r um die Zentrums-Kachel (z14) + zugehörige Eltern z10–13.
|
||
function ringTiles(r) {
|
||
var list = [];
|
||
var push = function (z2, x2, y2) {
|
||
if (x2 < 0 || y2 < 0 || x2 >= Math.pow(2, z2) || y2 >= Math.pow(2, z2)) return;
|
||
var k2 = z2 + '/' + x2 + '/' + y2;
|
||
if (!seen[k2]) { seen[k2] = 1; list.push([z2, x2, y2]); }
|
||
};
|
||
for (var x2 = cx - r; x2 <= cx + r; x2++) for (var y2 = cy - r; y2 <= cy + r; y2++) {
|
||
if (Math.max(Math.abs(x2 - cx), Math.abs(y2 - cy)) !== r) continue;
|
||
push(MAXZOOM, x2, y2);
|
||
for (var pz = 13; pz >= 10; pz--) push(pz, x2 >> (MAXZOOM - pz), y2 >> (MAXZOOM - pz));
|
||
}
|
||
return list;
|
||
}
|
||
|
||
var coveredRing = 0;
|
||
function nextRing(r) {
|
||
if (r > maxRing || state.bytes >= budget) return Promise.resolve();
|
||
return _fetchTiles(ringTiles(r), state, function () { tick(r * kmPerTile); }).then(function () {
|
||
coveredRing = r;
|
||
tick(r * kmPerTile);
|
||
return nextRing(r + 1);
|
||
});
|
||
}
|
||
|
||
var radiusKm = 0, poiCount = 0;
|
||
return _fetchTiles(base, state, function () { tick(0); })
|
||
.then(function () { return nextRing(0); })
|
||
.then(function () {
|
||
radiusKm = Math.max(1, Math.round(coveredRing * kmPerTile * 10) / 10);
|
||
return _cacheGlyphs(); // Glyphs mitcachen (sonst offline kein Render)
|
||
})
|
||
.then(function (gb) { state.bytes += gb; return _cachePois(_bboxAround(lat, lon, radiusKm)); })
|
||
.then(function (pc) {
|
||
poiCount = pc;
|
||
return _addRegion({ type: opts.type || 'gebiet', lat: lat, lon: lon, radiusKm: radiusKm,
|
||
tiles: state.stored, bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
|
||
})
|
||
.then(function () { return _bumpTotal(state.bytes); })
|
||
.then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, radiusKm: radiusKm }; });
|
||
}
|
||
|
||
// ---- Funkloch-Gedächtnis ----------------------------------------------------
|
||
// „Wo verliere ich Netz" = Aufenthaltsorte → bleibt KOMPLETT LOKAL (IndexedDB),
|
||
// wird nie hochgeladen. Signal = echte Tile-Fetch-Fehler bei aktivem GPS
|
||
// (NICHT navigator.onLine — das lügt bei Captive-Portal/Schwachempfang).
|
||
var _gps = null, _lastZoneNote = 0;
|
||
|
||
// ---- Speicher-Cap (Soft-Guard für die AUTOMATISCHEN Pfade) -------------------
|
||
// Manuelle Downloads bleiben immer möglich; Vorausladen + Funkloch-Autofill stoppen
|
||
// über dem Cap. totalBytes wird bei jedem Download mitgezählt; clear() setzt zurück.
|
||
var CAP_MB = 250;
|
||
function _bumpTotal(bytes) {
|
||
if (!bytes) return Promise.resolve();
|
||
return _metaGet('totalBytes')
|
||
.then(function (t) { return _metaPut('totalBytes', Math.max(0, (t || 0) + bytes)); })
|
||
.catch(function () {});
|
||
}
|
||
function _overCap() {
|
||
return _metaGet('totalBytes')
|
||
.then(function (t) { return (t || 0) > CAP_MB * 1048576; })
|
||
.catch(function () { return false; });
|
||
}
|
||
|
||
// Persistenten Speicher anfragen (best-effort, idempotent) — härtet IndexedDB gegen
|
||
// Eviction bei Speicherdruck. Safari/iOS ignoriert es teils, schadet aber nicht.
|
||
function _persistStorage() {
|
||
if (_persistStorage._done) return;
|
||
_persistStorage._done = true;
|
||
try {
|
||
if (navigator.storage && navigator.storage.persist) navigator.storage.persist().catch(function () {});
|
||
} catch (e) {}
|
||
}
|
||
|
||
// {lat,lon} während aktiver Aufzeichnung, sonst null. Nebeneffekte (Runde 3):
|
||
// 1. ROLLENDES VORAUSLADEN — solange Empfang da ist, alle ~400 m die fehlenden Kacheln
|
||
// um die aktuelle Position still mitnehmen. EPHEMER: Hatte die Runde KEIN Funkloch,
|
||
// wird das Vorausgeladene am Ende wieder gelöscht — gespeichert bleibt nur, was
|
||
// wegen fehlenden Netzes nötig ist + manuell Hinzugefügtes (Modell René 2026-06-08).
|
||
// 2. NETZ-PROBE — erkennt Funklöcher auch in BEREITS GESPEICHERTEN Gebieten (dort
|
||
// kommen Kacheln aus IndexedDB → kein Remote-Miss als Signal).
|
||
var _lastPre = null, _preActive = false;
|
||
var _preKeys = [], _preBytes = 0, _recHadDeadzone = false;
|
||
function setGps(pos) {
|
||
if (!pos) { // Aufzeichnung beendet
|
||
_gps = null; _lastPre = null;
|
||
_prunePrefetch();
|
||
return;
|
||
}
|
||
_gps = pos;
|
||
_probeNet(pos);
|
||
if (_preActive || !navigator.onLine) return;
|
||
if (_lastPre && _distKm(_lastPre.lat, _lastPre.lon, pos.lat, pos.lon) < 0.4) return;
|
||
_preActive = true;
|
||
var p = { lat: pos.lat, lon: pos.lon };
|
||
_overCap().then(function (over) {
|
||
if (over) return;
|
||
return _prefetchRing(p.lat, p.lon, 2).then(function () { _lastPre = p; });
|
||
}).catch(function () {})
|
||
.then(function () { _preActive = false; });
|
||
}
|
||
|
||
// Aktive Netz-Probe (alle ~2 Min bei Aufzeichnung): kleiner Request mit Timeout.
|
||
// Fehlschlag = Funkloch an dieser Position — unabhängig davon, ob die Kacheln
|
||
// hier längst lokal liegen. (navigator.onLine lügt bei Schwachempfang.)
|
||
var _lastProbe = 0;
|
||
function _probeNet(pos) {
|
||
var now = Date.now();
|
||
if (now - _lastProbe < 120000) return;
|
||
_lastProbe = now;
|
||
var p = { lat: pos.lat, lon: pos.lon };
|
||
var ctrl = (typeof AbortController !== 'undefined') ? new AbortController() : null;
|
||
var timer = ctrl && setTimeout(function () { try { ctrl.abort(); } catch (e) {} }, 6000);
|
||
fetch('/api/version', { cache: 'no-store', signal: ctrl ? ctrl.signal : undefined })
|
||
.then(function (r) { if (!r.ok) throw new Error('probe ' + r.status); })
|
||
.catch(function () {
|
||
_recHadDeadzone = true;
|
||
markDeadZone(p.lat, p.lon);
|
||
})
|
||
.then(function () { if (timer) clearTimeout(timer); });
|
||
}
|
||
|
||
// Vorausgeladenes wieder entfernen, wenn die Runde KEIN Funkloch hatte.
|
||
function _prunePrefetch() {
|
||
var keys = _preKeys, bytes = _preBytes, had = _recHadDeadzone;
|
||
_preKeys = []; _preBytes = 0; _recHadDeadzone = false;
|
||
if (had || !keys.length) return Promise.resolve();
|
||
var chain = Promise.resolve();
|
||
keys.forEach(function (k) { chain = chain.then(function () { return _del(k).catch(function () {}); }); });
|
||
return chain.then(function () { return _bumpTotal(-bytes); });
|
||
}
|
||
|
||
// z14-Kacheln ±n um lat/lon (+ Eltern z10–13) — NUR fehlende, still, ohne Region-Eintrag.
|
||
function _prefetchRing(lat, lon, n) {
|
||
var cx = _x(lon, MAXZOOM), cy = _y(lat, MAXZOOM), seen = {}, list = [];
|
||
for (var x = cx - n; x <= cx + n; x++) for (var y = cy - n; y <= cy + n; y++) {
|
||
list.push([MAXZOOM, x, y]);
|
||
for (var pz = 13; pz >= 10; pz--) {
|
||
var px = x >> (MAXZOOM - pz), py = y >> (MAXZOOM - pz), k = pz + '/' + px + '/' + py;
|
||
if (!seen[k]) { seen[k] = 1; list.push([pz, px, py]); }
|
||
}
|
||
}
|
||
var missing = [], chain = Promise.resolve();
|
||
list.forEach(function (t) {
|
||
chain = chain.then(function () {
|
||
return _get(t[0] + '/' + t[1] + '/' + t[2]).then(function (hit) { if (!hit) missing.push(t); });
|
||
});
|
||
});
|
||
var state = { done: 0, bytes: 0, stored: 0 };
|
||
return chain
|
||
.then(function () { return missing.length ? _fetchTiles(missing, state, null) : null; })
|
||
.then(function () {
|
||
// Nur NEU gespeicherte Keys fürs Prune merken — Bestand bleibt unangetastet.
|
||
missing.forEach(function (t) { _preKeys.push(t[0] + '/' + t[1] + '/' + t[2]); });
|
||
_preBytes += state.bytes;
|
||
return _bumpTotal(state.bytes);
|
||
});
|
||
}
|
||
|
||
function _distKm(aLat, aLon, bLat, bLon) {
|
||
var dLat = (bLat - aLat) * 111, dLon = (bLon - aLon) * 111 * Math.cos(aLat * Math.PI / 180);
|
||
return Math.sqrt(dLat * dLat + dLon * dLon);
|
||
}
|
||
|
||
function _noteRemoteMiss() {
|
||
if (!_gps) return;
|
||
_recHadDeadzone = true; // Vorausgeladenes dieser Runde behalten
|
||
var now = Date.now();
|
||
if (now - _lastZoneNote < 120000) return; // max. 1 Eintrag / 2 Min
|
||
_lastZoneNote = now;
|
||
markDeadZone(_gps.lat, _gps.lon);
|
||
}
|
||
|
||
// Zone manuell „ent-funklochen" (✕ im Offline-Modal): wird nicht mehr automatisch
|
||
// geladen; bereits geladene Kacheln bleiben bis „Alles löschen"/Eviction.
|
||
function removeDeadZone(ts) {
|
||
return _metaGet('deadzones').then(function (zones) {
|
||
zones = (zones || []).filter(function (z) { return z.ts !== ts; });
|
||
return _metaPut('deadzones', zones).then(function () { return zones.length; });
|
||
});
|
||
}
|
||
|
||
// Funkloch merken (Dedupe: keine zweite Zone im Umkreis von 2 km, Cap 50).
|
||
function markDeadZone(lat, lon) {
|
||
return _metaGet('deadzones').then(function (zones) {
|
||
zones = zones || [];
|
||
if (zones.some(function (z) { return _distKm(z.lat, z.lon, lat, lon) < 2; })) return;
|
||
zones.push({ lat: lat, lon: lon, ts: Date.now(), filled: false });
|
||
if (zones.length > 50) zones = zones.slice(-50);
|
||
return _metaPut('deadzones', zones);
|
||
}).catch(function () {});
|
||
}
|
||
|
||
// Funkloch-Zonen nachladen (nur online sinnvoll). Dort wo Netz verfügbar ist, braucht
|
||
// man keine Offline-Karten — gespeichert wird gezielt da, wo es ausfiel.
|
||
// opts {lat, lon, maxKm:50}: mit Position werden nur Zonen im Umkreis betrachtet
|
||
// (Speicher minimal halten) und nächstgelegene zuerst geladen. Gefüllte Zonen werden
|
||
// VERIFIZIERT (Zentrums-Kachel noch da?) — fängt „Alles löschen" und iOS-Eviction ab:
|
||
// die Gebiete kommen beim nächsten Start automatisch wieder (Modell René 2026-06-08).
|
||
// Liefert die Anzahl (neu) gefüllter Zonen.
|
||
var _autofillActive = false;
|
||
function autoFillDeadZones(opts) {
|
||
opts = opts || {};
|
||
var maxKm = opts.maxKm || 50;
|
||
if (_autofillActive || !navigator.onLine) return Promise.resolve(0);
|
||
_autofillActive = true;
|
||
var filled = 0, all = null;
|
||
return _overCap().then(function (over) {
|
||
if (over) return null; // Speicher-Cap erreicht → kein automatisches Nachladen mehr
|
||
return _metaGet('deadzones');
|
||
}).then(function (zones) {
|
||
if (zones === null) return 0; // expliziter Cap-Abbruch (s.o.)
|
||
zones = zones || []; // keine Zonen → trotzdem Warnungs-Refresh unten
|
||
all = zones;
|
||
var cand = opts.lat == null ? zones.slice() : zones.filter(function (z) {
|
||
return _distKm(z.lat, z.lon, opts.lat, opts.lon) <= maxKm;
|
||
});
|
||
if (!cand.length) {
|
||
// Keine Zonen in der Nähe — Warnungen trotzdem frisch halten (24-h-Takt).
|
||
if (opts.lat != null) return _refreshAlerts(opts.lat, opts.lon).then(function () { return 0; });
|
||
return 0;
|
||
}
|
||
// Verify: als gefüllt markierte Zonen ohne lokale Zentrums-Kachel wieder öffnen.
|
||
var verify = Promise.resolve();
|
||
cand.forEach(function (z) {
|
||
verify = verify.then(function () {
|
||
if (!z.filled) return;
|
||
return _get(MAXZOOM + '/' + _x(z.lon, MAXZOOM) + '/' + _y(z.lat, MAXZOOM))
|
||
.then(function (hit) { if (!hit) z.filled = false; });
|
||
});
|
||
});
|
||
return verify.then(function () {
|
||
var open = cand.filter(function (z) { return !z.filled; });
|
||
if (opts.lat != null) open.sort(function (a, b) {
|
||
return _distKm(a.lat, a.lon, opts.lat, opts.lon) - _distKm(b.lat, b.lon, opts.lat, opts.lon);
|
||
});
|
||
var chain = Promise.resolve();
|
||
open.forEach(function (z) {
|
||
chain = chain.then(function () {
|
||
return downloadAround(z.lat, z.lon, { budgetMB: 5, type: 'funkloch' }).then(function (res) {
|
||
if (res.bytes > 0) { z.filled = true; filled++; }
|
||
}).catch(function () {});
|
||
});
|
||
});
|
||
return chain.then(function () { return _metaPut('deadzones', all); })
|
||
.then(function () {
|
||
// Warnungen im Umkreis frisch halten (24-h-Takt, Bbox-Replace).
|
||
if (opts.lat != null) return _refreshAlerts(opts.lat, opts.lon);
|
||
})
|
||
.then(function () { return filled; });
|
||
});
|
||
}).then(function (n) { _autofillActive = false; return n; },
|
||
function () { _autofillActive = false; return 0; });
|
||
}
|
||
|
||
// ---- Routen-Korridor ---------------------------------------------------------
|
||
// Kacheln ±bufferKm um den Track (z14 + Eltern z10–13 + Basis 0–9) + Marker der
|
||
// Korridor-Bbox — für mehrtägige Unternehmungen entlang einer Route.
|
||
// track = [{lat,lon},...]; opts {bufferKm:1, capMB:50, name, onProgress({bytes,done,total})}.
|
||
function downloadCorridor(track, opts) {
|
||
opts = opts || {};
|
||
if (!track || track.length < 2) return Promise.reject(new Error('Kein GPS-Track'));
|
||
_persistStorage();
|
||
var buffer = opts.bufferKm || 1, cap = (opts.capMB || 50) * 1048576;
|
||
var seen = {}, list = [];
|
||
var push = function (z, x, y) {
|
||
if (x < 0 || y < 0 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) return;
|
||
var k = z + '/' + x + '/' + y;
|
||
if (!seen[k]) { seen[k] = 1; list.push([z, x, y]); }
|
||
};
|
||
var s = 90, w = 180, n = -90, e = -180;
|
||
track.forEach(function (p) {
|
||
if (p.lat < s) s = p.lat; if (p.lat > n) n = p.lat;
|
||
if (p.lon < w) w = p.lon; if (p.lon > e) e = p.lon;
|
||
var d = Math.ceil(buffer / _tileKm(MAXZOOM, p.lat));
|
||
var cx = _x(p.lon, MAXZOOM), cy = _y(p.lat, MAXZOOM);
|
||
for (var x = cx - d; x <= cx + d; x++) for (var y = cy - d; y <= cy + d; y++) {
|
||
push(MAXZOOM, x, y);
|
||
for (var pz = 13; pz >= 10; pz--) push(pz, x >> (MAXZOOM - pz), y >> (MAXZOOM - pz));
|
||
}
|
||
});
|
||
var latMid = (s + n) / 2;
|
||
var bb = { south: s - buffer / 111, north: n + buffer / 111,
|
||
west: w - buffer / (111 * Math.cos(latMid * Math.PI / 180)),
|
||
east: e + buffer / (111 * Math.cos(latMid * Math.PI / 180)) };
|
||
for (var z2 = 0; z2 <= 9; z2++) {
|
||
var x0 = _x(bb.west, z2), x1 = _x(bb.east, z2), y0 = _y(bb.north, z2), y1 = _y(bb.south, z2);
|
||
for (var x2 = x0; x2 <= x1; x2++) for (var y2 = y0; y2 <= y1; y2++) push(z2, x2, y2);
|
||
}
|
||
|
||
var state = { done: 0, bytes: 0, stored: 0 }, total = list.length, poiCount = 0;
|
||
// In 64er-Blöcken laden → zwischen den Blöcken greift der Speicher-Cap.
|
||
function chunkLoop(idx) {
|
||
if (idx >= list.length || state.bytes >= cap) return Promise.resolve();
|
||
return _fetchTiles(list.slice(idx, idx + 64), state, function () {
|
||
if (opts.onProgress) opts.onProgress({ bytes: state.bytes, done: state.done, total: total });
|
||
}).then(function () { return chunkLoop(idx + 64); });
|
||
}
|
||
// Vereinfachten Track (≤60 Punkte) in der Region-Meta ablegen: damit kann clear()
|
||
// die Korridor-Keep-Keys SELBST aus IndexedDB bauen — ohne API/Login/GPS
|
||
// (Gerätetest René 2026-06-08: leeres Keep-Set = Komplett-Wipe trotz Routen).
|
||
var step = Math.max(1, Math.ceil(track.length / 60));
|
||
var slim = track.filter(function (p, i) { return i % step === 0; });
|
||
if (slim[slim.length - 1] !== track[track.length - 1]) slim.push(track[track.length - 1]);
|
||
slim = slim.map(function (p) { return { lat: p.lat, lon: p.lon }; });
|
||
|
||
return chunkLoop(0)
|
||
.then(function () { return _cacheGlyphs(); })
|
||
.then(function (gb) { state.bytes += gb; return _cachePois(bb); })
|
||
.then(function (pc) {
|
||
poiCount = pc;
|
||
return _addRegion({ type: 'korridor', name: opts.name || null, lat: track[0].lat, lon: track[0].lon,
|
||
track: slim, tiles: state.stored, bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
|
||
})
|
||
.then(function () { return _bumpTotal(state.bytes); })
|
||
.then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, capped: state.bytes >= cap }; });
|
||
}
|
||
|
||
// STANDORT-GRUNDVERSORGUNG (Renés Modell, Punkt 1): das Gebiet um die aktuelle Position
|
||
// ist IMMER geladen (~5 MB) — auch nach „Alles löschen", sonst ist die Offline-
|
||
// Funktionalität am wichtigsten Ort (hier!) weg. Zentrums-Kachel da → nichts zu tun.
|
||
function ensureHomeArea(lat, lon) {
|
||
return _get(MAXZOOM + '/' + _x(lon, MAXZOOM) + '/' + _y(lat, MAXZOOM)).then(function (hit) {
|
||
if (hit) return 0;
|
||
return _overCap().then(function (over) {
|
||
if (over) return 0;
|
||
return downloadAround(lat, lon, { budgetMB: 5, type: 'standort' })
|
||
.then(function (res) { return res.bytes > 0 ? 1 : 0; })
|
||
.catch(function () { return 0; });
|
||
});
|
||
}).catch(function () { return 0; });
|
||
}
|
||
|
||
// Gespeicherte Routen offline nutzbar halten (René 2026-06-08): beim Start die Korridore
|
||
// der eigenen Routen in Positionsnähe sicherstellen — Stichproben-Kacheln prüfen, bei
|
||
// Lücken Korridor (neu) laden. Deckt „Alles löschen" + Eviction ab; preview_track
|
||
// (~40 Punkte) reicht, der ±1-km-Puffer schluckt die Vereinfachung. Automatischer Pfad → Cap.
|
||
var _ensureActive = false;
|
||
function ensureRouteCorridors(routes, opts) {
|
||
opts = opts || {};
|
||
var maxKm = opts.maxKm || 50;
|
||
if (_ensureActive || !navigator.onLine) return Promise.resolve(0);
|
||
_ensureActive = true;
|
||
var done = 0;
|
||
return _overCap().then(function (over) {
|
||
if (over) return 0;
|
||
var cand = (routes || []).filter(function (r) {
|
||
var t = r.preview_track || r.gps_track;
|
||
if (!t || t.length < 2) return false;
|
||
if (opts.lat == null) return true;
|
||
return _distKm(t[0].lat, t[0].lon, opts.lat, opts.lon) <= maxKm;
|
||
});
|
||
var chain = Promise.resolve();
|
||
cand.forEach(function (r) {
|
||
chain = chain.then(function () {
|
||
var t = r.preview_track || r.gps_track;
|
||
var idxs = [0, Math.floor(t.length / 4), Math.floor(t.length / 2),
|
||
Math.floor(3 * t.length / 4), t.length - 1];
|
||
var missing = false, v = Promise.resolve();
|
||
idxs.forEach(function (i) {
|
||
v = v.then(function () {
|
||
if (missing) return;
|
||
var p = t[i];
|
||
return _get(MAXZOOM + '/' + _x(p.lon, MAXZOOM) + '/' + _y(p.lat, MAXZOOM))
|
||
.then(function (hit) { if (!hit) missing = true; });
|
||
});
|
||
});
|
||
return v.then(function () {
|
||
if (!missing) return;
|
||
return downloadCorridor(t, { bufferKm: 1, name: r.name || ('route-' + r.id) })
|
||
.then(function (res) { if (res.bytes > 0) done++; })
|
||
.catch(function () {});
|
||
});
|
||
});
|
||
});
|
||
return chain.then(function () { return done; });
|
||
}).then(function (n) { _ensureActive = false; return n; },
|
||
function () { _ensureActive = false; return 0; });
|
||
}
|
||
|
||
// ---- Bereichsauswahl: sichtbaren Karten-Ausschnitt komplett speichern ---------
|
||
// bbox = {south,west,north,east} (z.B. aktueller Viewport). Zu-groß-Schutz über
|
||
// Kachelzahl, Abbruch-Cap über capMB. opts {capMB:40, name, onProgress({bytes,done,total})}.
|
||
function downloadBbox(bbox, opts) {
|
||
opts = opts || {};
|
||
_persistStorage();
|
||
var cap = (opts.capMB || 40) * 1048576;
|
||
var seen = {}, list = [];
|
||
var push = function (z, x, y) {
|
||
if (x < 0 || y < 0 || x >= Math.pow(2, z) || y >= Math.pow(2, z)) return;
|
||
var k = z + '/' + x + '/' + y;
|
||
if (!seen[k]) { seen[k] = 1; list.push([z, x, y]); }
|
||
};
|
||
for (var z = 0; z <= MAXZOOM; z++) {
|
||
var x0 = _x(bbox.west, z), x1 = _x(bbox.east, z), y0 = _y(bbox.north, z), y1 = _y(bbox.south, z);
|
||
if (z === MAXZOOM && (x1 - x0 + 1) * (y1 - y0 + 1) > 4000) {
|
||
return Promise.reject(new Error('Bereich zu groß — bitte weiter reinzoomen.'));
|
||
}
|
||
for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) push(z, x, y);
|
||
}
|
||
var state = { done: 0, bytes: 0, stored: 0 }, total = list.length, poiCount = 0;
|
||
function chunkLoop(idx) {
|
||
if (idx >= list.length || state.bytes >= cap) return Promise.resolve();
|
||
return _fetchTiles(list.slice(idx, idx + 64), state, function () {
|
||
if (opts.onProgress) opts.onProgress({ bytes: state.bytes, done: state.done, total: total });
|
||
}).then(function () { return chunkLoop(idx + 64); });
|
||
}
|
||
var midLat = (bbox.south + bbox.north) / 2, midLon = (bbox.west + bbox.east) / 2;
|
||
return chunkLoop(0)
|
||
.then(function () { return _cacheGlyphs(); })
|
||
.then(function (gb) { state.bytes += gb; return _cachePois(bbox); })
|
||
.then(function (pc) {
|
||
poiCount = pc;
|
||
return _addRegion({ type: 'ausschnitt', name: opts.name || null, lat: midLat, lon: midLon,
|
||
tiles: state.stored, bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
|
||
})
|
||
.then(function () { return _bumpTotal(state.bytes); })
|
||
.then(function () { return { tiles: state.stored, bytes: state.bytes, pois: poiCount, capped: state.bytes >= cap }; });
|
||
}
|
||
|
||
// ---- Coverage-Layer ----------------------------------------------------------
|
||
// GeoJSON der gespeicherten z14-Kacheln — zeigt auf der Karte, welche Bereiche offline
|
||
// da sind. properties.kind: 'funkloch' (Kachel liegt in einer Funkloch-Region, wird
|
||
// anders eingefärbt — Wunsch René 2026-06-08) oder 'manuell'.
|
||
function _tile2lat(y, z) {
|
||
var m = Math.PI - 2 * Math.PI * y / Math.pow(2, z);
|
||
return 180 / Math.PI * Math.atan(0.5 * (Math.exp(m) - Math.exp(-m)));
|
||
}
|
||
function coverage() {
|
||
var re = new RegExp('^' + MAXZOOM + '/(\\d+)/(\\d+)$');
|
||
return _metaGet('regions').then(function (regions) {
|
||
var fz = (regions || []).filter(function (r) { return r.type === 'funkloch'; });
|
||
return _req(STORE, 'readonly', function (os) { return os.getAllKeys(); }).then(function (keys) {
|
||
var feats = [];
|
||
(keys || []).forEach(function (k) {
|
||
var m = re.exec(k);
|
||
if (!m) return;
|
||
var x = +m[1], y = +m[2], n2 = Math.pow(2, MAXZOOM);
|
||
var west = x / n2 * 360 - 180, east = (x + 1) / n2 * 360 - 180;
|
||
var north = _tile2lat(y, MAXZOOM), south = _tile2lat(y + 1, MAXZOOM);
|
||
var cLat = (north + south) / 2, cLon = (west + east) / 2;
|
||
var isFz = fz.some(function (r) {
|
||
return _distKm(r.lat, r.lon, cLat, cLon) <= (r.radiusKm || 5) + 0.8;
|
||
});
|
||
feats.push({ type: 'Feature', properties: { kind: isFz ? 'funkloch' : 'manuell' },
|
||
geometry: { type: 'Polygon',
|
||
coordinates: [[[west, south], [east, south], [east, north], [west, north], [west, south]]] } });
|
||
});
|
||
return { type: 'FeatureCollection', features: feats };
|
||
});
|
||
});
|
||
}
|
||
|
||
function stats() {
|
||
return _count().then(function (count) {
|
||
return _metaGet('regions').then(function (regions) {
|
||
return _metaGet('totalBytes').then(function (totalBytes) {
|
||
return _metaGet('deadzones').then(function (deadzones) {
|
||
return _metaGet('region').then(function (meta) {
|
||
return { count: count, meta: meta || null, regions: regions || [],
|
||
totalBytes: totalBytes || 0, deadzones: deadzones || [] };
|
||
});
|
||
});
|
||
});
|
||
});
|
||
});
|
||
}
|
||
function hasRegion() { return stats().then(function (s) { return s.count > 0; }).catch(function () { return false; }); }
|
||
// Kachel-Keys eines Umkreises (alle Zooms) ins Keep-Set legen.
|
||
function _keepRegionKeys(lat, lon, radiusKm, keep) {
|
||
var bb = _bboxAround(lat, lon, radiusKm);
|
||
for (var z = 10; z <= MAXZOOM; z++) {
|
||
var x0 = _x(bb.west, z), x1 = _x(bb.east, z), y0 = _y(bb.north, z), y1 = _y(bb.south, z);
|
||
for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) keep[z + '/' + x + '/' + y] = 1;
|
||
}
|
||
}
|
||
// Korridor-Keys eines Tracks (±bufferKm, z10–14) ins Keep-Set legen.
|
||
function _keepCorridorKeys(track, bufferKm, keep) {
|
||
track.forEach(function (p) {
|
||
var d = Math.ceil(bufferKm / _tileKm(MAXZOOM, p.lat));
|
||
var cx = _x(p.lon, MAXZOOM), cy = _y(p.lat, MAXZOOM);
|
||
for (var x = cx - d; x <= cx + d; x++) for (var y = cy - d; y <= cy + d; y++) {
|
||
keep[MAXZOOM + '/' + x + '/' + y] = 1;
|
||
for (var pz = 13; pz >= 10; pz--) {
|
||
keep[pz + '/' + (x >> (MAXZOOM - pz)) + '/' + (y >> (MAXZOOM - pz))] = 1;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// „Alles löschen" — SELEKTIV (René 2026-06-08, spart Vorladezeit):
|
||
// BLEIBEN: Standort-Gebiete, FUNKLOCH-Gebiete (automatisch gelernt — wären sonst sofort
|
||
// wieder vorzuladen), Korridore der Routen (aus Region-Meta r.track ODER opts.keepTracks),
|
||
// der Umkreis von opts.center (5 km), Basis-Zooms 0–9, Marker/Warnungen ('p/') + Glyphs ('f/').
|
||
// GEHEN: nur manuelle Gebiete ('gebiet') + Ausschnitte ('ausschnitt').
|
||
// opts.keepTracks: [{name, track}] oder [track] — Tracks werden in die Korridor-Meta
|
||
// MIGRIERT (Bestandsdaten vor v1236 hatten keinen track im Eintrag).
|
||
// Ohne Keep-Kandidaten (alte Signatur/Tests): kompletter Wipe inkl. Basis-Zooms.
|
||
// Liefert {standort, funkloch, korridore} (Anzahl behaltener Gebiete) für den Toast.
|
||
function clear(opts) {
|
||
opts = opts || {};
|
||
var keep = {}, summary = { standort: 0, funkloch: 0, korridore: 0 };
|
||
return _metaGet('regions').then(function (regions) {
|
||
regions = regions || [];
|
||
var keptRegions = regions.filter(function (r) {
|
||
return r.type === 'standort' || r.type === 'korridor' || r.type === 'funkloch';
|
||
});
|
||
// Tracks normalisieren: [{name, track}] oder [track]
|
||
var tracks = (opts.keepTracks || []).map(function (t) {
|
||
return Array.isArray(t) ? { name: null, track: t } : (t || {});
|
||
}).filter(function (o) { return o.track && o.track.length >= 2; });
|
||
|
||
keptRegions.forEach(function (r) {
|
||
if ((r.type === 'standort' || r.type === 'funkloch') && r.radiusKm) {
|
||
_keepRegionKeys(r.lat, r.lon, r.radiusKm, keep);
|
||
summary[r.type]++;
|
||
}
|
||
// Korridor-Keep direkt aus der Region-Meta (r.track) — unabhängig von API/Login.
|
||
if (r.type === 'korridor') {
|
||
if (!(r.track && r.track.length >= 2)) {
|
||
// Migration: Track aus keepTracks in den Alt-Eintrag übernehmen (Match per Name,
|
||
// sonst erster track-loser Kandidat).
|
||
var m = tracks.find(function (o) { return o.name && o.name === r.name; }) || tracks[0];
|
||
if (m) { r.track = m.track; }
|
||
}
|
||
if (r.track && r.track.length >= 2) { _keepCorridorKeys(r.track, 1, keep); summary.korridore++; }
|
||
}
|
||
});
|
||
// keepTracks ohne passenden Meta-Eintrag → trotzdem behalten + Eintrag anlegen.
|
||
tracks.forEach(function (o) {
|
||
var known = keptRegions.some(function (r) {
|
||
return r.type === 'korridor' && r.track && r.track.length &&
|
||
_distKm(r.track[0].lat, r.track[0].lon, o.track[0].lat, o.track[0].lon) < 0.3;
|
||
});
|
||
if (known) return;
|
||
_keepCorridorKeys(o.track, 1, keep);
|
||
summary.korridore++;
|
||
keptRegions.push({ type: 'korridor', name: o.name || null, lat: o.track[0].lat, lon: o.track[0].lon,
|
||
track: o.track, tiles: 0, bytes: 0, pois: 0, savedAt: Date.now() });
|
||
});
|
||
if (opts.center) {
|
||
_keepRegionKeys(opts.center.lat, opts.center.lon, 5, keep);
|
||
// Standort-ADOPTION: Bestandsdaten haben evtl. keine standort-Region (Gebiet wurde
|
||
// vor Runde 6 geladen) — Eintrag synthetisch anlegen, damit Folge-Läufe ihn kennen.
|
||
if (!keptRegions.some(function (r) { return r.type === 'standort'; })) {
|
||
keptRegions.push({ type: 'standort', lat: opts.center.lat, lon: opts.center.lon,
|
||
radiusKm: 5, tiles: 0, bytes: 0, pois: 0, savedAt: Date.now() });
|
||
}
|
||
summary.standort = Math.max(summary.standort, 1);
|
||
}
|
||
var keepBase = Object.keys(keep).length > 0;
|
||
if (!keepBase) keptRegions = []; // nichts zu behalten → echter Komplett-Wipe
|
||
|
||
return _req(STORE, 'readonly', function (os) { return os.getAllKeys(); }).then(function (keys) {
|
||
// Komplett-Wipe (nichts zu behalten): alles inkl. Marker/Glyphs (altes Verhalten).
|
||
if (!keepBase) return _delMany((keys || []).slice());
|
||
var doomed = (keys || []).filter(function (k) {
|
||
var m = /^(\d+)\//.exec(k);
|
||
if (!m) return false; // 'p/' + 'f/' bleiben
|
||
if (+m[1] <= 9) return false; // Basis-Zooms behalten
|
||
return !keep[k];
|
||
});
|
||
return _delMany(doomed);
|
||
}).then(function () { return _metaGet('deadzones'); })
|
||
.then(function (zones) {
|
||
return _req(META, 'readwrite', function (os) { os.clear(); }).then(function () {
|
||
var jobs = [];
|
||
if (zones && zones.length) {
|
||
// Selektiv: Funkloch-Kacheln bleiben → Füllstatus behalten (Start-Check
|
||
// verifiziert ohnehin). Komplett-Wipe: alles ungefüllt → Neu-Laden.
|
||
if (!keepBase) zones.forEach(function (z) { z.filled = false; });
|
||
jobs.push(_metaPut('deadzones', zones));
|
||
}
|
||
if (keptRegions.length) {
|
||
jobs.push(_metaPut('regions', keptRegions));
|
||
jobs.push(_metaPut('region', keptRegions[keptRegions.length - 1]));
|
||
jobs.push(_metaPut('totalBytes', keptRegions.reduce(function (a, r) { return a + (r.bytes || 0); }, 0)));
|
||
}
|
||
return Promise.all(jobs);
|
||
});
|
||
}).then(function () { return summary; });
|
||
});
|
||
}
|
||
|
||
return {
|
||
registerProtocol: registerProtocol, downloadAround: downloadAround, downloadCorridor: downloadCorridor,
|
||
downloadBbox: downloadBbox, ensureRouteCorridors: ensureRouteCorridors, ensureHomeArea: ensureHomeArea,
|
||
tile: tile, glyph: glyph, pois: pois, alerts: alerts, coverage: coverage,
|
||
setGps: setGps, markDeadZone: markDeadZone, removeDeadZone: removeDeadZone, autoFillDeadZones: autoFillDeadZones,
|
||
stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
|
||
};
|
||
})();
|