banyaro/backend/static/js/map-offline.js
rene a600ca1dec Offline: Giftkoeder + vermisste Hunde offline sichtbar, Korridor auf Detailkarte
Geraetetest-Befunde Runde 2:
- Giftkoeder verschwanden offline: /api/places kam aus dem SW-Cache (feste URL)
  und verhinderte den allFailed-Fallback, waehrend /api/poison?lat=... (Bbox-URL)
  scheiterte -> jetzt faellt jede Quelle EINZELN auf den letzten guten Stand
  zurueck (localStorage) + Merge aus dem Offline-Region-Snapshot
- Region-Download speichert jetzt auch /api/poison + /api/lost der Gegend
  (p/_poison, p/_lost, anonym; MapOffline.alerts(kind,bbox) als Reader) —
  Sicherheitsdaten muessen auch am vorab gespeicherten Urlaubsort da sein
- lost.js Offline-Pfad merged den Region-Snapshot in Cache- und Leer-Fall
- Routen-Korridor war 'unsichtbar' (lag im schon gespeicherten Gebiet):
  nach dem Speichern werden die gespeicherten Bereiche blau auf der
  Detailkarte eingeblendet; Logik per Node-Stub-Test verifiziert
Bump v1227
2026-06-06 12:15:34 +02:00

453 lines
22 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(); }); };
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. Liefert die Anzahl frischer Einträge.
function _mergeStore(key, fresh) {
if (!fresh || !fresh.length) return Promise.resolve(0);
return _get(key).then(function (old) {
var merged = fresh;
if (old && old.length) {
var seen = {};
fresh.forEach(function (p) { seen[p.id] = true; });
merged = fresh.concat(old.filter(function (p) { return !seen[p.id]; }));
}
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); })
.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.
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 * 1000))
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (fresh) { return _mergeStore('p/_poison', fresh); })
.then(function (n) { total += n; })
.catch(function () {}));
jobs.push(fetch('/api/lost?lat=' + midLat + '&lon=' + midLon + '&radius_km=' + Math.ceil(radiusKm))
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (fresh) { return _mergeStore('p/_lost', fresh); })
.then(function (n) { total += n; })
.catch(function () {}));
return Promise.all(jobs).then(function () { return total; });
}
// 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).
function _addRegion(region) {
return _metaGet('regions').then(function (list) {
list = list || [];
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 z1013), 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 09 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 || {};
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 09 ü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 z1013.
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({ lat: lat, lon: lon, radiusKm: radiusKm, tiles: state.stored,
bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
})
.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;
function setGps(pos) { _gps = pos; } // {lat,lon} während aktiver Aufzeichnung, sonst null
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;
var now = Date.now();
if (now - _lastZoneNote < 120000) return; // max. 1 Eintrag / 2 Min
_lastZoneNote = now;
markDeadZone(_gps.lat, _gps.lon);
}
// 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 () {});
}
// Offene Funkloch-Zonen budget-getrieben nachladen (nur online sinnvoll).
// Dort wo Netz verfügbar ist, braucht man keine Offline-Karten — gespeichert
// wird gezielt da, wo es ausfiel. Liefert die Anzahl gefüllter Zonen.
var _autofillActive = false;
function autoFillDeadZones() {
if (_autofillActive || !navigator.onLine) return Promise.resolve(0);
_autofillActive = true;
var filled = 0;
return _metaGet('deadzones').then(function (zones) {
zones = zones || [];
var open = zones.filter(function (z) { return !z.filled; });
if (!open.length) return 0;
var chain = Promise.resolve();
open.forEach(function (z) {
chain = chain.then(function () {
return downloadAround(z.lat, z.lon, { budgetMB: 5 }).then(function (res) {
if (res.bytes > 0) { z.filled = true; filled++; }
}).catch(function () {});
});
});
return chain.then(function () { return _metaPut('deadzones', zones); })
.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 z1013 + Basis 09) + 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'));
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); });
}
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,
tiles: state.stored, bytes: state.bytes, pois: poiCount, savedAt: Date.now() });
})
.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.
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 _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);
feats.push({ type: 'Feature', properties: {}, 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('region').then(function (meta) {
return { count: count, meta: meta || null, regions: regions || [] };
});
});
});
}
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, downloadCorridor: downloadCorridor,
tile: tile, glyph: glyph, pois: pois, alerts: alerts, coverage: coverage,
setGps: setGps, markDeadZone: markDeadZone, autoFillDeadZones: autoFillDeadZones,
stats: stats, hasRegion: hasRegion, clear: clear, MAXZOOM: MAXZOOM,
};
})();