Offline-Karten Runde 6: Standort-Grundversorgung — aktuelles Gebiet bleibt immer geladen
Renes Modell Punkt 1 war zu eng interpretiert (nur Funkloecher): das Gebiet um die aktuelle Position gehoert zur Grundversorgung. - ensureHomeArea(lat,lon): Zentrums-Kachel-Check -> bei Luecke Budget-Download (type 'standort', Cap-gated) - Start-Check: Standort raw pruefen (ohne GL-Stack) und bei Bedarf laden - 'Alles loeschen': Standort-Gebiet wird SOFORT neu geladen (+ Toast) — vorher war die Offline-Funktionalitaet genau am wichtigsten Ort weg - Pfote Segment 5: Standort-Kachel da UND Zonen im 50-km-Umkreis gefuellt (ferne Zonen zaehlen nicht mehr — sie laden erst vor Ort) - Tests r6 + Regression r1/r3/r4/r5 gruen Bump v1234
This commit is contained in:
parent
97f0518c54
commit
94a6ce49ba
11 changed files with 185 additions and 26 deletions
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
|||
1233
|
||||
1234
|
||||
|
|
@ -86,14 +86,14 @@
|
|||
<title>Ban Yaro</title>
|
||||
|
||||
<!-- Theme + theme-color Statusleiste vor CSS setzen -->
|
||||
<script src="/js/boot-early.js?v=1233"></script>
|
||||
<script src="/js/boot-early.js?v=1234"></script>
|
||||
|
||||
<!-- CSS: Reihenfolge ist wichtig — ?v= zwingt Browser zur Neuladung -->
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1233">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1233">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1233">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1233">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1233">
|
||||
<link rel="stylesheet" href="/css/design-system.css?v=1234">
|
||||
<link rel="stylesheet" href="/css/layout.css?v=1234">
|
||||
<link rel="stylesheet" href="/css/components.css?v=1234">
|
||||
<link rel="stylesheet" href="/css/utilities.css?v=1234">
|
||||
<link rel="stylesheet" href="/css/lists.css?v=1234">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
@ -612,11 +612,11 @@
|
|||
<div id="modal-container"></div>
|
||||
|
||||
<!-- JS: Reihenfolge ist wichtig — erst Basis, dann Features -->
|
||||
<script src="/js/api.js?v=1233"></script>
|
||||
<script src="/js/ui.js?v=1233"></script>
|
||||
<script src="/js/app.js?v=1233"></script>
|
||||
<script src="/js/worlds.js?v=1233"></script>
|
||||
<script src="/js/offline-indicator.js?v=1233"></script>
|
||||
<script src="/js/api.js?v=1234"></script>
|
||||
<script src="/js/ui.js?v=1234"></script>
|
||||
<script src="/js/app.js?v=1234"></script>
|
||||
<script src="/js/worlds.js?v=1234"></script>
|
||||
<script src="/js/offline-indicator.js?v=1234"></script>
|
||||
|
||||
<!-- Feature-Seiten werden lazy geladen -->
|
||||
|
||||
|
|
@ -626,7 +626,7 @@
|
|||
|
||||
|
||||
<!-- Boot: Offline-Banner + SW-Registration (extrahiert für CSP) -->
|
||||
<script src="/js/boot.js?v=1233"></script>
|
||||
<script src="/js/boot.js?v=1234"></script>
|
||||
|
||||
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '1233'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '1234'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
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_VERSION = APP_VERSION;
|
||||
|
|
|
|||
|
|
@ -600,6 +600,21 @@ window.MapOffline = (function () {
|
|||
.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
|
||||
|
|
@ -753,7 +768,7 @@ window.MapOffline = (function () {
|
|||
|
||||
return {
|
||||
registerProtocol: registerProtocol, downloadAround: downloadAround, downloadCorridor: downloadCorridor,
|
||||
downloadBbox: downloadBbox, ensureRouteCorridors: ensureRouteCorridors,
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -25,9 +25,27 @@ window.OfflineIndicator = (() => {
|
|||
function _offlineTilesMode() {
|
||||
try { return !!(window.BY && BY.offlineTiles()); } catch (e) { return false; }
|
||||
}
|
||||
// Slippy-Kachel-Koordinaten (z14, wie map-offline.js MAXZOOM) — für raw IDB-Checks
|
||||
// ohne den GL-Stack zu laden.
|
||||
function _tileKey14(lat, lon) {
|
||||
const n = Math.pow(2, 14);
|
||||
const x = Math.floor((lon + 180) / 360 * n);
|
||||
const rad = lat * Math.PI / 180;
|
||||
const y = Math.floor((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2 * n);
|
||||
return `14/${x}/${y}`;
|
||||
}
|
||||
function _lsPos() {
|
||||
try {
|
||||
const raw = localStorage.getItem(LS_LAST_POS);
|
||||
if (raw) { const p = JSON.parse(raw); if (p?.lat != null) return { lat: p.lat, lon: p.lon }; }
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Offline-Ready (Pfote Segment 5) — ohne MapOffline/GL-Stack zu laden.
|
||||
// Semantik (Modell René 2026-06-08): Gibt es bekannte FUNKLOCH-Zonen, zählt deren
|
||||
// Füllstand (alle gefüllt = grün); ohne bekannte Zonen wie bisher: irgendein Gebiet da.
|
||||
// Semantik (Modell René 2026-06-08): Standort-Gebiet gespeichert UND alle bekannten
|
||||
// Funkloch-Zonen IM UMKREIS (50 km, ferne zählen nicht — sie laden erst vor Ort) gefüllt.
|
||||
// Ohne Position: irgendein Gebiet da.
|
||||
// WICHTIG: dasselbe Schema/Version wie map-offline.js anlegen — sonst legt ein versionsloses
|
||||
// open() die DB leer an und MapOffline kann seine Stores nicht mehr erstellen.
|
||||
function _offlineRegionStored() {
|
||||
|
|
@ -44,12 +62,25 @@ window.OfflineIndicator = (() => {
|
|||
if (!db.objectStoreNames.contains('tiles') || !db.objectStoreNames.contains('meta')) {
|
||||
db.close(); return res(false);
|
||||
}
|
||||
const pos = _lsPos();
|
||||
const mz = db.transaction('meta', 'readonly').objectStore('meta').get('deadzones');
|
||||
mz.onsuccess = () => {
|
||||
const zones = mz.result || [];
|
||||
if (zones.length) { res(zones.every(z => z.filled)); db.close(); return; }
|
||||
const zones = (mz.result || []).filter(z => {
|
||||
if (!pos) return true;
|
||||
const dLat = (z.lat - pos.lat) * 111,
|
||||
dLon = (z.lon - pos.lon) * 111 * Math.cos(pos.lat * Math.PI / 180);
|
||||
return Math.sqrt(dLat * dLat + dLon * dLon) <= 50;
|
||||
});
|
||||
const zonesOk = zones.every(z => z.filled);
|
||||
if (pos) {
|
||||
// Standort-Kachel vorhanden? (Grundversorgung)
|
||||
const tq = db.transaction('tiles', 'readonly').objectStore('tiles').get(_tileKey14(pos.lat, pos.lon));
|
||||
tq.onsuccess = () => { res(!!tq.result && zonesOk); db.close(); };
|
||||
tq.onerror = () => { res(false); db.close(); };
|
||||
return;
|
||||
}
|
||||
const cnt = db.transaction('tiles', 'readonly').objectStore('tiles').count();
|
||||
cnt.onsuccess = () => { res(cnt.result > 0); db.close(); };
|
||||
cnt.onsuccess = () => { res(cnt.result > 0 && zonesOk); db.close(); };
|
||||
cnt.onerror = () => { res(false); db.close(); };
|
||||
};
|
||||
mz.onerror = () => { res(false); db.close(); };
|
||||
|
|
@ -59,6 +90,28 @@ window.OfflineIndicator = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
// Fehlt die Standort-Kachel? (raw, ohne GL-Stack) — Trigger für die Grundversorgung.
|
||||
function _homeAreaMissing(pos) {
|
||||
return new Promise(res => {
|
||||
try {
|
||||
const r = indexedDB.open('by-offline-tiles', 1);
|
||||
r.onupgradeneeded = () => {
|
||||
const d = r.result;
|
||||
if (!d.objectStoreNames.contains('tiles')) d.createObjectStore('tiles');
|
||||
if (!d.objectStoreNames.contains('meta')) d.createObjectStore('meta');
|
||||
};
|
||||
r.onsuccess = () => {
|
||||
const db = r.result;
|
||||
if (!db.objectStoreNames.contains('tiles')) { db.close(); return res(true); }
|
||||
const tq = db.transaction('tiles', 'readonly').objectStore('tiles').get(_tileKey14(pos.lat, pos.lon));
|
||||
tq.onsuccess = () => { res(!tq.result); db.close(); };
|
||||
tq.onerror = () => { res(true); db.close(); };
|
||||
};
|
||||
r.onerror = () => res(true);
|
||||
} catch (e) { res(true); }
|
||||
});
|
||||
}
|
||||
|
||||
const CHECKS = [
|
||||
{ step: 1, title: 'App-Grundgerüst',
|
||||
detail: 'CSS, Layout und Hauptmodule — die Basis',
|
||||
|
|
@ -323,18 +376,22 @@ window.OfflineIndicator = (() => {
|
|||
_autoFillTimer = setTimeout(async () => {
|
||||
if (!navigator.onLine) return;
|
||||
try {
|
||||
const pos = await _lastKnownPos();
|
||||
const hasZones = await _anyDeadZonesStored();
|
||||
const homeMissing = pos ? await _homeAreaMissing(pos) : false;
|
||||
let routes = [];
|
||||
try { routes = (await API.routes.list()) || []; } catch {}
|
||||
routes = routes.filter(r => (r.preview_track || []).length >= 2);
|
||||
if (!hasZones && !routes.length) return; // nichts zu tun → GL-Stack nicht laden
|
||||
const pos = await _lastKnownPos();
|
||||
if (!hasZones && !routes.length && !homeMissing) return; // nichts zu tun → GL-Stack nicht laden
|
||||
const o = pos ? { lat: pos.lat, lon: pos.lon } : {};
|
||||
await UI.loadMapLibreUI();
|
||||
// Standort-Grundversorgung zuerst (René: das Gebiet am aktuellen Standort ist IMMER da)
|
||||
const h = (homeMissing && pos) ? (await window.MapOffline?.ensureHomeArea?.(pos.lat, pos.lon) || 0) : 0;
|
||||
const n = await window.MapOffline?.autoFillDeadZones?.(o) || 0;
|
||||
const k = routes.length ? (await window.MapOffline?.ensureRouteCorridors?.(routes, o) || 0) : 0;
|
||||
if (n || k) {
|
||||
if (h || n || k) {
|
||||
const parts = [];
|
||||
if (h) parts.push('Standort-Gebiet');
|
||||
if (n) parts.push(`${n} Funkloch-${n === 1 ? 'Gebiet' : 'Gebiete'}`);
|
||||
if (k) parts.push(`${k} Routen-${k === 1 ? 'Korridor' : 'Korridore'}`);
|
||||
UI.toast?.info(`${parts.join(' + ')} automatisch offline gespeichert.`);
|
||||
|
|
|
|||
|
|
@ -2311,7 +2311,16 @@ window.Page_map = (() => {
|
|||
await MapOffline.clear().catch(() => {});
|
||||
_setCoverage(false);
|
||||
UI.modal.close();
|
||||
UI.toast.success('Offline-Karten gelöscht. Bekannte Funkloch-Gebiete werden beim nächsten Start automatisch neu geladen.');
|
||||
UI.toast.success('Offline-Karten gelöscht. Funkloch-Gebiete werden beim nächsten Start automatisch neu geladen.');
|
||||
// Standort-Grundversorgung sofort wiederherstellen (René 2026-06-08: das Gebiet am
|
||||
// aktuellen Standort muss bleiben — es würde sonst nicht automatisch vorgeladen
|
||||
// und die Offline-Funktionalität wäre genau hier weg).
|
||||
if (_userPos && navigator.onLine) {
|
||||
try {
|
||||
const r = await MapOffline.ensureHomeArea(_userPos.lat, _userPos.lon);
|
||||
if (r) UI.toast.info('Dein Standort-Gebiet wurde neu geladen — offline weiter verfügbar.');
|
||||
} catch (e) {}
|
||||
}
|
||||
window.OfflineIndicator?.refresh();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<script src="/js/landing-init.js?v=1233"></script>
|
||||
<script src="/js/landing-init.js?v=1234"></script>
|
||||
<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="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
|
||||
const VER = '1233';
|
||||
const VER = '1234';
|
||||
const CACHE_VERSION = `by-v${VER}`;
|
||||
const CACHE_STATIC = `${CACHE_VERSION}-static`;
|
||||
const CACHE_TILES = 'ban-yaro-tiles-v1'; // bleibt über SW-Updates erhalten
|
||||
|
|
|
|||
|
|
@ -119,6 +119,15 @@ die Kacheln sind jederzeit neu ableitbarer Cache.
|
|||
offline nutzbar, auch nach „Alles löschen"/Eviction. Region-Dedupe per Typ+Name.
|
||||
- Stub-Tests jetzt im Repo: `tests/js/test-map-offline-r*.js` (s. tests/js/README.md).
|
||||
|
||||
**✅ Runde 6 — Standort-Grundversorgung (2026-06-08):**
|
||||
Renés Original-Modell Punkt 1 („die App holt sich am Standort die Karten- und Markerdaten bis
|
||||
5 MB") war zu eng als nur-Funkloch interpretiert: Das Gebiet um die AKTUELLE POSITION ist jetzt
|
||||
IMMER geladen — `ensureHomeArea(lat, lon)` (Zentrums-Kachel-Check → bei Lücke Budget-Download,
|
||||
type 'standort', Cap-gated). Greift: (a) im Start-Check (raw IDB-Check ohne GL-Stack-Load),
|
||||
(b) SOFORT nach „Alles löschen" (Standort wird direkt neu geladen + Toast). Pfote Segment 5 =
|
||||
Standort-Kachel da UND Zonen im 50-km-Umkreis gefüllt (ferne Zonen zählen nicht mehr — sie
|
||||
laden erst vor Ort). Tests: tests/js/test-map-offline-r6.js.
|
||||
|
||||
**🔲 Offen (Backlog):**
|
||||
- Echte LRU-Eviction (Refcounting/Region-Zuordnung der Kacheln), wenn Nutzer real ans Cap kommen.
|
||||
- Rechteck-Zeichnen als präzisere Bereichsauswahl (Viewport-Variante deckt den Hauptfall ab).
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ for f in tests/js/test-map-offline*.js; do node "$f" backend/static/js/map-offli
|
|||
- r3: downloadBbox, Zu-groß-Schutz, totalBytes, Prefetch-Throttle, Cap-Guard, persist()
|
||||
- r4: Minimal-Speicher-Modell (Prune, Netz-Probe, clear behält Zonen, Nähe/Verify, Färbung)
|
||||
- r5: Bbox-Replace (aufgehobene Warnungen), 24h-Alert-Refresh, removeDeadZone, ensureRouteCorridors
|
||||
- r6: Standort-Grundversorgung (ensureHomeArea: lädt/skippt/Cap, überlebt clear)
|
||||
|
||||
⚠️ Node 21+: eingebautes `navigator`-Global — Stubs via `Object.defineProperty(globalThis, 'navigator', …)`,
|
||||
ein einfaches `global.navigator =` wird still verschluckt.
|
||||
|
|
|
|||
68
tests/js/test-map-offline-r6.js
Normal file
68
tests/js/test-map-offline-r6.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Runde-6-Tests: Standort-Grundversorgung (ensureHomeArea)
|
||||
const fs = require('fs');
|
||||
const stores = { tiles: new Map(), meta: new Map() };
|
||||
function mkReq(result) { return { result }; }
|
||||
global.indexedDB = { open() {
|
||||
const req = {};
|
||||
setTimeout(() => {
|
||||
const db = {
|
||||
objectStoreNames: { contains: n => !!stores[n] },
|
||||
transaction(name) {
|
||||
const os = {
|
||||
get: k => mkReq(stores[name].get(k)),
|
||||
put: (v, k) => { stores[name].set(k, v); return mkReq(undefined); },
|
||||
delete: k => { stores[name].delete(k); return mkReq(undefined); },
|
||||
clear: () => { stores[name].clear(); return mkReq(undefined); },
|
||||
count: () => mkReq(stores[name].size),
|
||||
getAllKeys: () => mkReq([...stores[name].keys()]),
|
||||
};
|
||||
const tx = { objectStore: () => os };
|
||||
setTimeout(() => tx.oncomplete && tx.oncomplete());
|
||||
return tx;
|
||||
},
|
||||
close() {},
|
||||
};
|
||||
req.result = db; req.onsuccess && req.onsuccess();
|
||||
});
|
||||
return req;
|
||||
} };
|
||||
global.window = {};
|
||||
Object.defineProperty(globalThis, 'navigator', { value: { onLine: true, storage: { persist: () => Promise.resolve(true) } }, configurable: true });
|
||||
global.pmtiles = { PMTiles: class { getZxy() { return Promise.resolve({ data: new Uint8Array(100).buffer }); } } };
|
||||
global.MapGLStyle = { tilesUrl: () => 'http://t/d.pmtiles' };
|
||||
global.fetch = () => Promise.resolve({ ok: true, arrayBuffer: () => Promise.resolve(new Uint8Array(50).buffer), json: () => Promise.resolve([]) });
|
||||
eval(fs.readFileSync(process.argv[2], 'utf8'));
|
||||
const MO = global.window.MapOffline;
|
||||
|
||||
(async () => {
|
||||
// 1. Leerer Speicher → Standort wird geladen (type 'standort')
|
||||
const r1 = await MO.ensureHomeArea(48.07, 11.96);
|
||||
console.log('ensureHomeArea (leer):', r1, '— Kacheln:', stores.tiles.size);
|
||||
if (r1 !== 1 || stores.tiles.size === 0) throw new Error('Grundversorgung lädt nicht');
|
||||
const reg = stores.meta.get('regions').find(r => r.type === 'standort');
|
||||
if (!reg) throw new Error('standort-Region fehlt');
|
||||
|
||||
// 2. Bestand vorhanden → kein Doppel-Download
|
||||
const before = stores.tiles.size;
|
||||
const r2 = await MO.ensureHomeArea(48.07, 11.96);
|
||||
console.log('ensureHomeArea (vorhanden):', r2);
|
||||
if (r2 !== 0 || stores.tiles.size !== before) throw new Error('Doppel-Download trotz Bestand');
|
||||
|
||||
// 3. clear() → Zonen bleiben, Standort weg → ensureHomeArea lädt neu
|
||||
await MO.markDeadZone(48.07, 11.96);
|
||||
await MO.clear();
|
||||
if (stores.tiles.size !== 0) throw new Error('clear unvollständig');
|
||||
const r3 = await MO.ensureHomeArea(48.07, 11.96);
|
||||
console.log('Nach clear neu geladen:', r3, '— Zonen erhalten:', (stores.meta.get('deadzones') || []).length);
|
||||
if (r3 !== 1) throw new Error('Reload nach clear fehlt');
|
||||
if ((stores.meta.get('deadzones') || []).length !== 1) throw new Error('Zonen weg');
|
||||
|
||||
// 4. Über Cap → Auto-Pfad lädt nicht
|
||||
stores.meta.set('totalBytes', 300 * 1048576);
|
||||
stores.tiles.clear();
|
||||
const r4 = await MO.ensureHomeArea(48.07, 11.96);
|
||||
console.log('Über Cap:', r4);
|
||||
if (r4 !== 0) throw new Error('Cap-Guard fehlt');
|
||||
|
||||
console.log('\nALLE RUNDE-6-TESTS BESTANDEN');
|
||||
})().catch(e => { console.error('FEHLER:', e.message); process.exit(1); });
|
||||
Loading…
Add table
Add a link
Reference in a new issue