map-gl-offline 0.7.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/idb-offline-sw.js +313 -360
- package/dist/index.esm.js +996 -1002
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1002 -1001
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +1002 -1001
- package/dist/index.umd.js.map +1 -1
- package/dist/managers/offlineMapManager/importExportManagement.d.ts +5 -3
- package/dist/services/fontService.d.ts +12 -2
- package/dist/services/glyphService.d.ts +11 -2
- package/dist/services/importExportService.d.ts +11 -26
- package/dist/services/spriteService.d.ts +10 -3
- package/dist/style.css +1 -1
- package/dist/sw/offline-sw.d.ts +17 -0
- package/dist/sw/shared.d.ts +108 -0
- package/dist/types/import-export.d.ts +7 -28
- package/dist/ui/components/shared/PanelContent.d.ts +0 -1
- package/dist/ui/managers/PanelManager.d.ts +1 -1
- package/dist/ui/modals/{importExportModal.d.ts → mbtilesModal.d.ts} +18 -17
- package/dist/ui/translations/ar.d.ts +23 -37
- package/dist/ui/translations/en.d.ts +23 -37
- package/dist/utils/importResolver.d.ts +10 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/sqlJsLoader.d.ts +17 -0
- package/dist/utils/styleProviderUtils.d.ts +16 -0
- package/dist/utils/tileKey.d.ts +13 -0
- package/package.json +7 -4
package/dist/idb-offline-sw.js
CHANGED
|
@@ -1,396 +1,349 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Service Worker for offline map tile serving.
|
|
2
|
+
* Offline Service Worker for offline map tile serving.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* via window.fetch override alone.
|
|
8
|
-
*
|
|
9
|
-
* Self-contained: uses raw IndexedDB API (no imports).
|
|
10
|
-
*
|
|
11
|
-
* DB: 'offline-map-db' v3
|
|
12
|
-
* Stores: tiles, styles, sprites, glyphs
|
|
13
|
-
* Tile key format: {styleId}:{sourceId}:{z}:{x}:{y}.{ext}
|
|
4
|
+
* Built from src/sw/offline-sw.ts by scripts/build-sw.mjs.
|
|
5
|
+
* DO NOT EDIT THIS FILE BY HAND — changes will be overwritten on next build.
|
|
6
|
+
* Edit src/sw/offline-sw.ts or src/sw/shared.ts instead.
|
|
14
7
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// -----------------------------------------------------------
|
|
24
|
-
// Install / Activate
|
|
25
|
-
// -----------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
self.addEventListener('install', () => {
|
|
28
|
-
self.skipWaiting();
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
self.addEventListener('activate', (event) => {
|
|
32
|
-
event.waitUntil(self.clients.claim());
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
// -----------------------------------------------------------
|
|
36
|
-
// IndexedDB helpers (raw API, no idb library)
|
|
37
|
-
// -----------------------------------------------------------
|
|
38
|
-
|
|
39
|
-
function openDatabase() {
|
|
40
|
-
return new Promise((resolve, reject) => {
|
|
41
|
-
// Open without specifying a version so we never trigger onupgradeneeded.
|
|
42
|
-
// The main application is responsible for creating/upgrading the DB schema.
|
|
43
|
-
// If the DB doesn't exist yet, this will create it at version 1 with no stores,
|
|
44
|
-
// but the SW should only run after the main app has initialised the DB.
|
|
45
|
-
const request = indexedDB.open(DB_NAME);
|
|
46
|
-
request.onsuccess = () => resolve(request.result);
|
|
47
|
-
request.onerror = () => reject(request.error);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function idbGet(db, storeName, key) {
|
|
52
|
-
return new Promise((resolve, reject) => {
|
|
53
|
-
const tx = db.transaction(storeName, 'readonly');
|
|
54
|
-
const store = tx.objectStore(storeName);
|
|
55
|
-
const req = store.get(key);
|
|
56
|
-
req.onsuccess = () => resolve(req.result || null);
|
|
57
|
-
req.onerror = () => reject(req.error);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function idbGetAll(db, storeName) {
|
|
62
|
-
return new Promise((resolve, reject) => {
|
|
63
|
-
const tx = db.transaction(storeName, 'readonly');
|
|
64
|
-
const store = tx.objectStore(storeName);
|
|
65
|
-
const req = store.getAll();
|
|
66
|
-
req.onsuccess = () => resolve(req.result || []);
|
|
67
|
-
req.onerror = () => reject(req.error);
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// -----------------------------------------------------------
|
|
72
|
-
// findStyleByRegionId - search all styles for matching region
|
|
73
|
-
// -----------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
async function findStyleByRegionId(db, regionId) {
|
|
76
|
-
// Check cache
|
|
77
|
-
const cached = regionToStyleCache.get(regionId);
|
|
78
|
-
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
79
|
-
return cached.styleEntry;
|
|
8
|
+
"use strict";
|
|
9
|
+
(() => {
|
|
10
|
+
// src/sw/shared.ts
|
|
11
|
+
var OFFLINE_PREFIX = "/__offline__/";
|
|
12
|
+
var DB_NAME = "offline-map-db";
|
|
13
|
+
function makeTileKey(x, y, z, styleId, sourceId, ext) {
|
|
14
|
+
return `${styleId}:${sourceId}:${z}:${x}:${y}.${ext}`;
|
|
80
15
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
16
|
+
var TILE_FALLBACK_EXTENSIONS = ["pbf", "mvt", "png", "jpg", "webp", "glb"];
|
|
17
|
+
function tileFallbackExtensions(requested) {
|
|
18
|
+
return TILE_FALLBACK_EXTENSIONS.filter((e) => e !== requested);
|
|
19
|
+
}
|
|
20
|
+
function parseTileYExt(yExt) {
|
|
21
|
+
const match = yExt.match(/^(\d+)\.([\w.]+?)(?:[?#]|$)/);
|
|
22
|
+
if (!match) return null;
|
|
23
|
+
const y = parseInt(match[1], 10);
|
|
24
|
+
if (Number.isNaN(y)) return null;
|
|
25
|
+
return { y, ext: match[2] };
|
|
26
|
+
}
|
|
27
|
+
function findStyleByRegionIdIn(styles, regionId) {
|
|
28
|
+
for (const entry of styles) {
|
|
29
|
+
const regions = entry.regions;
|
|
30
|
+
if (!Array.isArray(regions)) continue;
|
|
31
|
+
for (const r of regions) {
|
|
32
|
+
if (r?.regionId === regionId || r?.id === regionId) {
|
|
33
|
+
return entry;
|
|
92
34
|
}
|
|
93
35
|
}
|
|
94
36
|
}
|
|
95
|
-
// Cache negative result
|
|
96
|
-
regionToStyleCache.set(regionId, { styleEntry: null, timestamp: Date.now() });
|
|
97
|
-
return null;
|
|
98
|
-
} catch (err) {
|
|
99
|
-
console.warn('[SW] Error searching for style by region ID:', regionId, err);
|
|
100
37
|
return null;
|
|
101
38
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
function createTileKey(x, y, z, styleId, sourceId, ext) {
|
|
109
|
-
return `${styleId}:${sourceId}:${z}:${x}:${y}.${ext}`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function decompressGzip(data) {
|
|
113
|
-
try {
|
|
114
|
-
const ds = new DecompressionStream('gzip');
|
|
115
|
-
const stream = new Response(data).body.pipeThrough(ds);
|
|
116
|
-
return await new Response(stream).arrayBuffer();
|
|
117
|
-
} catch (err) {
|
|
118
|
-
console.warn('[SW] Gzip decompression failed:', err);
|
|
119
|
-
return data;
|
|
39
|
+
function parseGlyphPath(decodedPath) {
|
|
40
|
+
const pathParts = decodedPath.split("/");
|
|
41
|
+
const fontstackPart = pathParts[0] ?? "";
|
|
42
|
+
const rangePart = pathParts[1] || "0-255.pbf";
|
|
43
|
+
const fontstacks = fontstackPart.split(",").map((f) => f.trim()).filter(Boolean);
|
|
44
|
+
return { fontstacks, rangePart };
|
|
120
45
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
headers['Content-Type'] = 'application/vnd.mapbox-vector-tile';
|
|
46
|
+
function glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart) {
|
|
47
|
+
const glyphPath = `${fontstack}/${rangePart}`;
|
|
48
|
+
const normalizedPath = glyphPath.endsWith(".pbf") ? glyphPath : `${glyphPath}.pbf`;
|
|
49
|
+
return dedupe([
|
|
50
|
+
`${actualStyleId}::${normalizedPath}`,
|
|
51
|
+
`${actualStyleId}::${glyphPath}`,
|
|
52
|
+
`${downloadId}::${normalizedPath}`,
|
|
53
|
+
`${downloadId}::${glyphPath}`,
|
|
54
|
+
normalizedPath,
|
|
55
|
+
glyphPath
|
|
56
|
+
]);
|
|
133
57
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
58
|
+
function spriteCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
59
|
+
const stripExt = decodedPath.replace(/\.(json|png)$/i, "");
|
|
60
|
+
return dedupe([
|
|
61
|
+
`${actualStyleId}::${decodedPath}`,
|
|
62
|
+
`${actualStyleId}:${decodedPath}`,
|
|
63
|
+
`${actualStyleId}::${stripExt}`,
|
|
64
|
+
`${actualStyleId}:${stripExt}`,
|
|
65
|
+
`${downloadId}::${decodedPath}`,
|
|
66
|
+
`${downloadId}:${decodedPath}`,
|
|
67
|
+
`${downloadId}::${stripExt}`,
|
|
68
|
+
`${downloadId}:${stripExt}`,
|
|
69
|
+
decodedPath
|
|
70
|
+
]);
|
|
143
71
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
72
|
+
function modelCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
73
|
+
return dedupe([
|
|
74
|
+
`${actualStyleId}::model::${decodedPath}`,
|
|
75
|
+
`${downloadId}::model::${decodedPath}`
|
|
76
|
+
]);
|
|
147
77
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
|
|
78
|
+
function matchTileJsonSource(sources, decodedPath) {
|
|
79
|
+
const asConfig = (v) => v && typeof v === "object" ? v : null;
|
|
80
|
+
if (decodedPath in sources) {
|
|
81
|
+
const config = asConfig(sources[decodedPath]);
|
|
82
|
+
if (config) return { sourceId: decodedPath, config };
|
|
83
|
+
}
|
|
84
|
+
for (const [sourceId, raw] of Object.entries(sources)) {
|
|
85
|
+
const config = asConfig(raw);
|
|
86
|
+
if (!config) continue;
|
|
87
|
+
const url = typeof config.url === "string" ? config.url : void 0;
|
|
88
|
+
const original = typeof config.__originalTilesetUrl === "string" ? config.__originalTilesetUrl : void 0;
|
|
89
|
+
if (url === decodedPath || original === decodedPath) {
|
|
90
|
+
return { sourceId, config };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
156
94
|
}
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
95
|
+
function buildOfflineTileJson(sourceConfig, downloadId, sourceId, extension, tileUrlScheme, origin) {
|
|
96
|
+
const base = tileUrlScheme === "idb" ? `idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}` : `${origin ?? ""}${OFFLINE_PREFIX}${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`;
|
|
97
|
+
const tileJson = {
|
|
98
|
+
tilejson: typeof sourceConfig.tilejson === "string" ? sourceConfig.tilejson : "2.2.0",
|
|
99
|
+
name: sourceConfig.name ?? sourceId,
|
|
100
|
+
tiles: [base],
|
|
101
|
+
minzoom: typeof sourceConfig.minzoom === "number" ? sourceConfig.minzoom : 0,
|
|
102
|
+
maxzoom: typeof sourceConfig.maxzoom === "number" ? sourceConfig.maxzoom : 22
|
|
103
|
+
};
|
|
104
|
+
const copyable = [
|
|
105
|
+
"bounds",
|
|
106
|
+
"center",
|
|
107
|
+
"vector_layers",
|
|
108
|
+
"scheme",
|
|
109
|
+
"attribution",
|
|
110
|
+
"encoding",
|
|
111
|
+
"format",
|
|
112
|
+
"grids",
|
|
113
|
+
"data",
|
|
114
|
+
"template",
|
|
115
|
+
"version"
|
|
116
|
+
];
|
|
117
|
+
for (const field of copyable) {
|
|
118
|
+
if (field in sourceConfig && sourceConfig[field] !== void 0) {
|
|
119
|
+
tileJson[field] = sourceConfig[field];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return tileJson;
|
|
171
123
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!yMatch) {
|
|
180
|
-
return new Response('Invalid tile coordinates', { status: 400 });
|
|
124
|
+
function deriveTileExtensionFromTiles(tiles) {
|
|
125
|
+
if (Array.isArray(tiles) && tiles.length > 0 && typeof tiles[0] === "string") {
|
|
126
|
+
const match = tiles[0].match(/\.([\w]+)(?:[?#]|$)/i);
|
|
127
|
+
if (match) return match[1];
|
|
128
|
+
}
|
|
129
|
+
return "pbf";
|
|
181
130
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
// Try primary key
|
|
187
|
-
const tileKey = createTileKey(x, y, z, actualStyleId, sourceKey, requestedExt);
|
|
188
|
-
let resource = await idbGet(db, 'tiles', tileKey);
|
|
189
|
-
if (resource && resource.data) {
|
|
190
|
-
return buildTileResponse(resource);
|
|
131
|
+
function isGzipped(buffer) {
|
|
132
|
+
if (buffer.byteLength < 2) return false;
|
|
133
|
+
const view = new Uint8Array(buffer, 0, 2);
|
|
134
|
+
return view[0] === 31 && view[1] === 139;
|
|
191
135
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const fallbacks = ['pbf', 'mvt', 'png', 'jpg', 'webp'].filter((e) => e !== requestedExt);
|
|
195
|
-
for (const ext of fallbacks) {
|
|
196
|
-
const key = createTileKey(x, y, z, actualStyleId, sourceKey, ext);
|
|
197
|
-
resource = await idbGet(db, 'tiles', key);
|
|
198
|
-
if (resource && resource.data) {
|
|
199
|
-
return buildTileResponse(resource);
|
|
200
|
-
}
|
|
136
|
+
function dedupe(values) {
|
|
137
|
+
return Array.from(new Set(values));
|
|
201
138
|
}
|
|
202
139
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
140
|
+
// src/sw/offline-sw.ts
|
|
141
|
+
self.addEventListener("install", () => {
|
|
142
|
+
self.skipWaiting();
|
|
143
|
+
});
|
|
144
|
+
self.addEventListener("activate", (event) => {
|
|
145
|
+
event.waitUntil(self.clients.claim());
|
|
146
|
+
});
|
|
147
|
+
function openDatabase() {
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const req = indexedDB.open(DB_NAME);
|
|
150
|
+
req.onsuccess = () => resolve(req.result);
|
|
151
|
+
req.onerror = () => reject(req.error);
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
function idbGet(db, store, key) {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
const tx = db.transaction(store, "readonly");
|
|
157
|
+
const req = tx.objectStore(store).get(key);
|
|
158
|
+
req.onsuccess = () => resolve(req.result ?? null);
|
|
159
|
+
req.onerror = () => reject(req.error);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function idbGetAll(db, store) {
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const tx = db.transaction(store, "readonly");
|
|
165
|
+
const req = tx.objectStore(store).getAll();
|
|
166
|
+
req.onsuccess = () => resolve(req.result ?? []);
|
|
167
|
+
req.onerror = () => reject(req.error);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
var CACHE_TTL_MS = 6e4;
|
|
171
|
+
var regionToStyleCache = /* @__PURE__ */ new Map();
|
|
172
|
+
async function findStyleByRegionId(db, regionId) {
|
|
173
|
+
const cached = regionToStyleCache.get(regionId);
|
|
174
|
+
if (cached && Date.now() - cached.ts < CACHE_TTL_MS) {
|
|
175
|
+
return cached.styleEntry;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const all = await idbGetAll(db, "styles");
|
|
179
|
+
const hit = findStyleByRegionIdIn(all, regionId);
|
|
180
|
+
if (hit) {
|
|
181
|
+
regionToStyleCache.set(regionId, { styleEntry: hit, ts: Date.now() });
|
|
239
182
|
}
|
|
183
|
+
return hit;
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.warn("[SW] findStyleByRegionId failed for", regionId, err);
|
|
186
|
+
return null;
|
|
240
187
|
}
|
|
241
188
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const decodedResourcePath = decodeURIComponent(rest.join('/'));
|
|
251
|
-
|
|
252
|
-
const candidateKeys = [
|
|
253
|
-
`${actualStyleId}::${decodedResourcePath}`,
|
|
254
|
-
`${actualStyleId}:${decodedResourcePath}`,
|
|
255
|
-
`${actualStyleId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
256
|
-
`${actualStyleId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
257
|
-
`${downloadId}::${decodedResourcePath}`,
|
|
258
|
-
`${downloadId}:${decodedResourcePath}`,
|
|
259
|
-
`${downloadId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
260
|
-
`${downloadId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
261
|
-
decodedResourcePath,
|
|
262
|
-
`${downloadId}::${decodedResourcePath}`,
|
|
263
|
-
];
|
|
264
|
-
|
|
265
|
-
// Deduplicate
|
|
266
|
-
const uniqueKeys = [...new Set(candidateKeys)];
|
|
267
|
-
|
|
268
|
-
for (const key of uniqueKeys) {
|
|
269
|
-
const resource = await idbGet(db, 'sprites', key);
|
|
270
|
-
if (resource && resource.data) {
|
|
271
|
-
return new Response(resource.data, {
|
|
272
|
-
status: 200,
|
|
273
|
-
headers: resource.contentType ? { 'Content-Type': resource.contentType } : {},
|
|
274
|
-
});
|
|
189
|
+
async function decompressGzip(data) {
|
|
190
|
+
try {
|
|
191
|
+
const stream = new Response(data).body?.pipeThrough(new DecompressionStream("gzip"));
|
|
192
|
+
if (!stream) return data;
|
|
193
|
+
return await new Response(stream).arrayBuffer();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.warn("[SW] Gzip decompression failed:", err);
|
|
196
|
+
return data;
|
|
275
197
|
}
|
|
276
198
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
if (foundStyle) {
|
|
291
|
-
styleEntry = foundStyle;
|
|
199
|
+
async function buildTileResponse(resource) {
|
|
200
|
+
const headers = {
|
|
201
|
+
"Access-Control-Allow-Origin": "*",
|
|
202
|
+
"Cache-Control": "public, max-age=31536000"
|
|
203
|
+
};
|
|
204
|
+
if (resource.contentType) {
|
|
205
|
+
headers["Content-Type"] = resource.contentType;
|
|
206
|
+
} else if (resource.type === "vector") {
|
|
207
|
+
headers["Content-Type"] = "application/vnd.mapbox-vector-tile";
|
|
208
|
+
}
|
|
209
|
+
let finalData = resource.data;
|
|
210
|
+
if (isGzipped(resource.data) && resource.type === "vector") {
|
|
211
|
+
finalData = await decompressGzip(resource.data);
|
|
292
212
|
}
|
|
213
|
+
if (resource.contentEncoding && resource.contentEncoding !== "gzip") {
|
|
214
|
+
headers["Content-Encoding"] = resource.contentEncoding;
|
|
215
|
+
}
|
|
216
|
+
return new Response(finalData, { status: 200, statusText: "OK", headers });
|
|
293
217
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
218
|
+
async function handleTile(db, downloadId, rest) {
|
|
219
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
220
|
+
const styleId = styleEntry?.key ?? downloadId;
|
|
221
|
+
if (rest.length < 4) {
|
|
222
|
+
return new Response("Invalid tile path", { status: 400 });
|
|
223
|
+
}
|
|
224
|
+
const yExt = rest[rest.length - 1];
|
|
225
|
+
const x = parseInt(rest[rest.length - 2], 10);
|
|
226
|
+
const z = Math.floor(parseFloat(rest[rest.length - 3]));
|
|
227
|
+
const sourceKey = rest.slice(0, rest.length - 3).join("/");
|
|
228
|
+
const parsed = parseTileYExt(yExt);
|
|
229
|
+
if (!parsed || Number.isNaN(x) || Number.isNaN(z)) {
|
|
230
|
+
return new Response("Invalid tile coordinates", { status: 400 });
|
|
231
|
+
}
|
|
232
|
+
const primary = makeTileKey(x, parsed.y, z, styleId, sourceKey, parsed.ext);
|
|
233
|
+
let resource = await idbGet(db, "tiles", primary);
|
|
234
|
+
if (resource?.data) return buildTileResponse(resource);
|
|
235
|
+
for (const ext of tileFallbackExtensions(parsed.ext)) {
|
|
236
|
+
const key = makeTileKey(x, parsed.y, z, styleId, sourceKey, ext);
|
|
237
|
+
resource = await idbGet(db, "tiles", key);
|
|
238
|
+
if (resource?.data) return buildTileResponse(resource);
|
|
239
|
+
}
|
|
240
|
+
return new Response("Tile not found", { status: 404 });
|
|
297
241
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
? sourceValue.__originalTilesetUrl
|
|
312
|
-
: undefined;
|
|
313
|
-
if (sourceUrl === decodedResourcePath || originalUrl === decodedResourcePath) {
|
|
314
|
-
matchedSourceId = sourceId;
|
|
315
|
-
matchedSourceConfig = sourceValue;
|
|
316
|
-
break;
|
|
242
|
+
async function handleGlyph(db, downloadId, rest) {
|
|
243
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
244
|
+
const styleId = styleEntry?.key ?? downloadId;
|
|
245
|
+
const { fontstacks, rangePart } = parseGlyphPath(decodeURIComponent(rest.join("/")));
|
|
246
|
+
for (const fontstack of fontstacks) {
|
|
247
|
+
for (const key of glyphCandidateKeys(styleId, downloadId, fontstack, rangePart)) {
|
|
248
|
+
const resource = await idbGet(db, "glyphs", key);
|
|
249
|
+
if (resource?.data) {
|
|
250
|
+
return new Response(resource.data, {
|
|
251
|
+
status: 200,
|
|
252
|
+
headers: { "Content-Type": "application/x-protobuf" }
|
|
253
|
+
});
|
|
254
|
+
}
|
|
317
255
|
}
|
|
318
256
|
}
|
|
257
|
+
return new Response("Glyph not found", { status: 404 });
|
|
319
258
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
259
|
+
async function handleSprite(db, downloadId, rest) {
|
|
260
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
261
|
+
const styleId = styleEntry?.key ?? downloadId;
|
|
262
|
+
const path = decodeURIComponent(rest.join("/"));
|
|
263
|
+
for (const key of spriteCandidateKeys(styleId, downloadId, path)) {
|
|
264
|
+
const resource = await idbGet(db, "sprites", key);
|
|
265
|
+
if (resource?.data) {
|
|
266
|
+
const headers = {};
|
|
267
|
+
if (resource.contentType) headers["Content-Type"] = resource.contentType;
|
|
268
|
+
return new Response(resource.data, { status: 200, headers });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return new Response("Sprite not found", { status: 404 });
|
|
323
272
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
maxzoom: typeof matchedSourceConfig.maxzoom === 'number' ? matchedSourceConfig.maxzoom : 22,
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
// Copy additional fields
|
|
340
|
-
const fieldsToCopy = [
|
|
341
|
-
'bounds', 'center', 'vector_layers', 'scheme', 'attribution',
|
|
342
|
-
'encoding', 'format', 'grids', 'data', 'template', 'version',
|
|
343
|
-
];
|
|
344
|
-
for (const field of fieldsToCopy) {
|
|
345
|
-
if (field in matchedSourceConfig && matchedSourceConfig[field] !== undefined) {
|
|
346
|
-
tileJson[field] = matchedSourceConfig[field];
|
|
273
|
+
async function handleModel(db, downloadId, rest) {
|
|
274
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
275
|
+
const styleId = styleEntry?.key ?? downloadId;
|
|
276
|
+
const path = decodeURIComponent(rest.join("/"));
|
|
277
|
+
for (const key of modelCandidateKeys(styleId, downloadId, path)) {
|
|
278
|
+
const resource = await idbGet(db, "models", key);
|
|
279
|
+
if (resource?.data) {
|
|
280
|
+
return new Response(resource.data, {
|
|
281
|
+
status: 200,
|
|
282
|
+
headers: { "Content-Type": resource.contentType || "model/gltf-binary" }
|
|
283
|
+
});
|
|
284
|
+
}
|
|
347
285
|
}
|
|
286
|
+
return new Response("Model not found", { status: 404 });
|
|
348
287
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// -----------------------------------------------------------
|
|
357
|
-
// Fetch event listener
|
|
358
|
-
// -----------------------------------------------------------
|
|
359
|
-
|
|
360
|
-
self.addEventListener('fetch', (event) => {
|
|
361
|
-
const url = event.request.url;
|
|
362
|
-
const idx = url.indexOf(OFFLINE_PREFIX);
|
|
363
|
-
if (idx === -1) return; // Not an offline request, let it pass through
|
|
364
|
-
|
|
365
|
-
event.respondWith(handleOfflineRequest(url, idx));
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
async function handleOfflineRequest(url, prefixIndex) {
|
|
369
|
-
try {
|
|
370
|
-
const path = url.substring(prefixIndex + OFFLINE_PREFIX.length);
|
|
371
|
-
const parts = path.split('/');
|
|
372
|
-
const [downloadId, type, ...rest] = parts;
|
|
373
|
-
|
|
374
|
-
if (!downloadId || !type) {
|
|
375
|
-
return new Response('Invalid offline URL', { status: 400 });
|
|
288
|
+
async function handleTileJSON(db, downloadId, rest) {
|
|
289
|
+
const path = decodeURIComponent(rest.join("/"));
|
|
290
|
+
let styleEntry = await idbGet(db, "styles", downloadId);
|
|
291
|
+
if (!styleEntry?.style?.sources) {
|
|
292
|
+
styleEntry = await findStyleByRegionId(db, downloadId);
|
|
376
293
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
294
|
+
if (!styleEntry?.style?.sources) {
|
|
295
|
+
return new Response("Style not found for TileJSON", { status: 404 });
|
|
296
|
+
}
|
|
297
|
+
const match = matchTileJsonSource(styleEntry.style.sources, path);
|
|
298
|
+
if (!match) {
|
|
299
|
+
return new Response("Source not found for TileJSON", { status: 404 });
|
|
300
|
+
}
|
|
301
|
+
const ext = deriveTileExtensionFromTiles(match.config.tiles);
|
|
302
|
+
const tileJson = buildOfflineTileJson(
|
|
303
|
+
match.config,
|
|
304
|
+
downloadId,
|
|
305
|
+
match.sourceId,
|
|
306
|
+
ext,
|
|
307
|
+
"offline",
|
|
308
|
+
self.location.origin
|
|
309
|
+
);
|
|
310
|
+
return new Response(JSON.stringify(tileJson), {
|
|
311
|
+
status: 200,
|
|
312
|
+
headers: { "Content-Type": "application/json" }
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
self.addEventListener("fetch", (event) => {
|
|
316
|
+
const url = event.request.url;
|
|
317
|
+
const idx = url.indexOf(OFFLINE_PREFIX);
|
|
318
|
+
if (idx === -1) return;
|
|
319
|
+
event.respondWith(handleOfflineRequest(url, idx));
|
|
320
|
+
});
|
|
321
|
+
async function handleOfflineRequest(url, prefixIndex) {
|
|
322
|
+
try {
|
|
323
|
+
const path = url.substring(prefixIndex + OFFLINE_PREFIX.length);
|
|
324
|
+
const parts = path.split("/");
|
|
325
|
+
const [downloadId, type, ...rest] = parts;
|
|
326
|
+
if (!downloadId || !type) {
|
|
327
|
+
return new Response("Invalid offline URL", { status: 400 });
|
|
328
|
+
}
|
|
329
|
+
const db = await openDatabase();
|
|
330
|
+
switch (type) {
|
|
331
|
+
case "tile":
|
|
332
|
+
return await handleTile(db, downloadId, rest);
|
|
333
|
+
case "glyph":
|
|
334
|
+
return await handleGlyph(db, downloadId, rest);
|
|
335
|
+
case "sprite":
|
|
336
|
+
return await handleSprite(db, downloadId, rest);
|
|
337
|
+
case "model":
|
|
338
|
+
return await handleModel(db, downloadId, rest);
|
|
339
|
+
case "tilesjson":
|
|
340
|
+
return await handleTileJSON(db, downloadId, rest);
|
|
341
|
+
default:
|
|
342
|
+
return new Response(`Unknown resource type: ${type}`, { status: 400 });
|
|
343
|
+
}
|
|
344
|
+
} catch (err) {
|
|
345
|
+
console.error("[SW] Error handling offline request:", err);
|
|
346
|
+
return new Response("Service Worker error", { status: 500 });
|
|
391
347
|
}
|
|
392
|
-
} catch (err) {
|
|
393
|
-
console.error('[SW] Error handling offline request:', err);
|
|
394
|
-
return new Response('Service Worker error', { status: 500 });
|
|
395
348
|
}
|
|
396
|
-
}
|
|
349
|
+
})();
|