banyaro/backend/static/js/map-offline.js
rene 8f13f4d38d Offline-Karten: Kern implementiert (Region-Download → IndexedDB → Offline-Render)
map-offline.js (window.MapOffline): lädt Vektorkacheln eines Bereichs via pmtiles.getZxy
in IndexedDB + cacht die Glyphs mit (KRITISCH: ohne Glyphs lässt MapLibre offline die
ganze Kachel fallen). byt://-Protokoll bedient MapLibre IndexedDB-first, remote-Fallback.
- map-gl-style.js: build({offline}) nutzt byt-Source statt pmtiles:// (Flag by_offline_tiles,
  Default AUS bis gerätegetestet); glyphs bleiben /fonts (SW-gecacht)
- ui.js + map.js: map-offline.js mitladen + byt-Protokoll registrieren
- getZxy liefert bereits dekomprimierte MVT (kein gunzip) → ~15 MB/5km in IndexedDB

Headless bewiesen: Download 97 Tiles (5km München) → Netz AUS → 1903 Features gerendert,
nicht geladene Gegend (Paris) korrekt leer. Offen: Download-Button/FAB-Segment-5-Verdrahtung,
adaptives Lernen, Bereichsauswahl/Routen-Korridor (siehe docs/OFFLINE_MAPS_PLAN.md).
2026-06-05 19:46:18 +02:00

141 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ============================================================
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 _count = function () { return _req(STORE, 'readonly', function (os) { return os.count(); }); };
// ---- 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 () { return null; }); // offline + nicht gespeichert → leeres Tile
});
}
// MapLibre-Protokoll `byt://t/{z}/{x}/{y}` registrieren (idempotent).
function registerProtocol() {
if (registerProtocol._done || typeof maplibregl === 'undefined') return;
registerProtocol._done = true;
maplibregl.addProtocol('byt', function (params) {
var m = /byt:\/\/t\/(\d+)\/(\d+)\/(\d+)/.exec(params.url);
if (!m) return Promise.resolve({ data: new ArrayBuffer(0) });
return tile(+m[1], +m[2], +m[3]).then(function (u) {
if (!u) return { data: new ArrayBuffer(0) };
return { data: u.buffer.slice(u.byteOffset, u.byteOffset + u.byteLength) };
});
});
}
// ---- 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 _tileList(lat, lon, radiusKm) {
var dLat = radiusKm / 111, dLon = radiusKm / (111 * Math.cos(lat * Math.PI / 180));
var w = lon - dLon, e = lon + dLon, s = lat - dLat, n = lat + dLat, list = [];
for (var z = 0; z <= MAXZOOM; z++) {
var x0 = _x(w, z), x1 = _x(e, z), y0 = _y(n, z), y1 = _y(s, z);
for (var x = x0; x <= x1; x++) for (var y = y0; y <= y1; y++) list.push([z, x, y]);
}
return list;
}
// Glyphs (Open Sans Regular/Semibold, Latin + Latin-Extended) holen, damit der Service-Worker sie cacht.
// 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. (Persistenz über App-Updates = Follow-up.)
var FONTS = ['Open Sans Regular', 'Open Sans Semibold'], RANGES = ['0-255', '256-511'];
function _cacheGlyphs() {
var bytes = 0, jobs = [];
FONTS.forEach(function (f) { RANGES.forEach(function (rg) {
jobs.push(fetch('/fonts/' + encodeURIComponent(f) + '/' + rg + '.pbf')
.then(function (r) { return r.ok ? r.arrayBuffer() : null; })
.then(function (b) { if (b) bytes += b.byteLength; })
.catch(function () {}));
}); });
return Promise.all(jobs).then(function () { return bytes; });
}
// Bereich um lat/lon (radiusKm, Default 5) herunterladen + in IndexedDB ablegen.
// onProgress({done,total,bytes}). Liefert {tiles,bytes}.
function downloadAround(lat, lon, radiusKm, onProgress) {
radiusKm = radiusKm || 5;
var list = _tileList(lat, lon, radiusKm), total = list.length, done = 0, bytes = 0, stored = 0, i = 0, CONC = 6;
function next() {
if (i >= total) 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); bytes += u.byteLength; stored++; return _put(key, u); }
}).catch(function () {}).then(function () {
done++;
if (onProgress && (done % 8 === 0 || done === total)) onProgress({ done: done, total: total, bytes: bytes });
return next();
});
}
var w = []; for (var k = 0; k < CONC; k++) w.push(next());
return Promise.all(w)
.then(function () { return _cacheGlyphs(); }) // Glyphs mitcachen (sonst offline kein Render)
.then(function (gb) { bytes += gb; return _req(META, 'readwrite', function (os) {
os.put({ lat: lat, lon: lon, radiusKm: radiusKm, tiles: stored, bytes: bytes, savedAt: Date.now() }, 'region');
}); })
.then(function () { return { tiles: stored, bytes: bytes }; });
}
function stats() {
return _count().then(function (count) {
return _req(META, 'readonly', function (os) { return os.get('region'); })
.then(function (meta) { return { count: count, meta: meta || null }; });
});
}
function hasRegion() { return stats().then(function (s) { return s.count > 0; }).catch(function () { return false; }); }
function clear() {
return _req(STORE, 'readwrite', function (os) { os.clear(); })
.then(function () { return _req(META, 'readwrite', function (os) { os.clear(); }); });
}
return {
registerProtocol: registerProtocol, downloadAround: downloadAround, tile: tile,
stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
};
})();