map-gl-offline 0.6.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 +3 -1
- package/dist/idb-offline-sw.js +313 -360
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +1359 -1021
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1373 -1020
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +1373 -1020
- package/dist/index.umd.js.map +1 -1
- package/dist/managers/offlineMapManager/importExportManagement.d.ts +5 -3
- package/dist/managers/offlineMapManager/resourceManagement.d.ts +4 -0
- 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/modelService.d.ts +57 -0
- package/dist/services/resourceService.d.ts +11 -1
- 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/database.d.ts +9 -0
- package/dist/types/import-export.d.ts +7 -28
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model.d.ts +62 -0
- package/dist/types/region.d.ts +11 -1
- package/dist/types/style.d.ts +11 -2
- package/dist/ui/components/shared/PanelContent.d.ts +0 -1
- package/dist/ui/managers/PanelManager.d.ts +1 -1
- package/dist/ui/managers/downloadManager.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/constants.d.ts +2 -1
- 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/index.esm.js
CHANGED
|
@@ -12,7 +12,7 @@ import { polygon, convertArea, featureCollection } from '@turf/helpers';
|
|
|
12
12
|
*/
|
|
13
13
|
// IndexedDB Configuration
|
|
14
14
|
const DB_NAME = 'offline-map-db';
|
|
15
|
-
const DB_VERSION =
|
|
15
|
+
const DB_VERSION = 4;
|
|
16
16
|
// Store Names (regions are stored inside styles.regions[], not as a separate store)
|
|
17
17
|
const STORE_NAMES = {
|
|
18
18
|
TILES: 'tiles',
|
|
@@ -20,6 +20,7 @@ const STORE_NAMES = {
|
|
|
20
20
|
SPRITES: 'sprites',
|
|
21
21
|
GLYPHS: 'glyphs',
|
|
22
22
|
FONTS: 'fonts',
|
|
23
|
+
MODELS: 'models',
|
|
23
24
|
};
|
|
24
25
|
// Download Configuration
|
|
25
26
|
const DOWNLOAD_DEFAULTS = {
|
|
@@ -227,7 +228,7 @@ async function resetOfflineMapDB() {
|
|
|
227
228
|
* Called during initial database creation or when stores are missing.
|
|
228
229
|
*/
|
|
229
230
|
function createStores(db) {
|
|
230
|
-
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts'];
|
|
231
|
+
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts', 'models'];
|
|
231
232
|
for (const storeName of stores) {
|
|
232
233
|
if (!db.objectStoreNames.contains(storeName)) {
|
|
233
234
|
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
@@ -309,6 +310,7 @@ function migrateRegionsToStyles(transaction) {
|
|
|
309
310
|
* - sprites: Sprite images and JSON
|
|
310
311
|
* - glyphs: Font glyph data
|
|
311
312
|
* - fonts: Font files
|
|
313
|
+
* - models: 3D model files (.glb) for Mapbox Standard tree/turbine layers
|
|
312
314
|
* - regions: (deprecated) Legacy region storage, migrated to styles.regions[]
|
|
313
315
|
*
|
|
314
316
|
* @example
|
|
@@ -328,6 +330,9 @@ async function openOfflineMapDB() {
|
|
|
328
330
|
if (oldVersion > 0 && oldVersion < 3) {
|
|
329
331
|
migrateRegionsToStyles(transaction);
|
|
330
332
|
}
|
|
333
|
+
// Migration: v3 -> v4
|
|
334
|
+
// Adds the `models` store for Mapbox Standard 3D model assets.
|
|
335
|
+
// No data migration needed — createStores above handles it.
|
|
331
336
|
},
|
|
332
337
|
});
|
|
333
338
|
}
|
|
@@ -1162,6 +1167,22 @@ function parseTileKey(key) {
|
|
|
1162
1167
|
}
|
|
1163
1168
|
return { styleId, sourceId, z, x, y, ext };
|
|
1164
1169
|
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Extract the extension (the last dotted segment before `?`, `#`, or end) from
|
|
1172
|
+
* a tile URL or tile URL template. Defaults to `"pbf"` when no extension can
|
|
1173
|
+
* be parsed. For multi-extension URLs like Mapbox v4's `{y}.vector.pbf` this
|
|
1174
|
+
* returns `"pbf"`, matching the key used when the tile is stored.
|
|
1175
|
+
*
|
|
1176
|
+
* Keeping extraction logic in one place ensures patchStyleForOffline (which
|
|
1177
|
+
* rewrites tile URLs to `idb://` at load time) derives the same extension
|
|
1178
|
+
* that tileService.extractExtension used at store time — otherwise the
|
|
1179
|
+
* first-try lookup in idbFetchHandler misses and has to fall through its
|
|
1180
|
+
* pbf/mvt/png/jpg/webp fallback loop.
|
|
1181
|
+
*/
|
|
1182
|
+
function extractTileExtensionFromUrl(url) {
|
|
1183
|
+
const match = url.match(/\.([\w]+)(?:[?#]|$)/i);
|
|
1184
|
+
return match ? match[1] : 'pbf';
|
|
1185
|
+
}
|
|
1165
1186
|
/**
|
|
1166
1187
|
* Derive tile extension from tile URL templates
|
|
1167
1188
|
*/
|
|
@@ -1169,21 +1190,217 @@ function deriveTileExtension(tiles) {
|
|
|
1169
1190
|
if (Array.isArray(tiles) && tiles.length > 0) {
|
|
1170
1191
|
const firstTile = tiles[0];
|
|
1171
1192
|
if (typeof firstTile === 'string') {
|
|
1172
|
-
|
|
1173
|
-
if (match) {
|
|
1174
|
-
return match[1];
|
|
1175
|
-
}
|
|
1193
|
+
return extractTileExtensionFromUrl(firstTile);
|
|
1176
1194
|
}
|
|
1177
1195
|
}
|
|
1178
1196
|
return 'pbf';
|
|
1179
1197
|
}
|
|
1180
1198
|
|
|
1199
|
+
/**
|
|
1200
|
+
* Pure helpers shared between the main-thread offline fetch handler
|
|
1201
|
+
* (`src/utils/idbFetchHandler.ts`) and the offline Service Worker
|
|
1202
|
+
* (`src/sw/offline-sw.ts`, compiled to `public/idb-offline-sw.js`).
|
|
1203
|
+
*
|
|
1204
|
+
* Keeping these in one place means the SW and the main-thread handler
|
|
1205
|
+
* can't drift — adding a new `model` handler, changing the fallback
|
|
1206
|
+
* order, or tweaking the tilejson-source matcher happens once.
|
|
1207
|
+
*
|
|
1208
|
+
* Nothing in here touches IndexedDB directly. Each helper takes already-
|
|
1209
|
+
* resolved inputs and returns the list of candidate keys (or the
|
|
1210
|
+
* resolved output) that the caller feeds into its own IDB lookup.
|
|
1211
|
+
*
|
|
1212
|
+
* The corresponding IDB access layer is:
|
|
1213
|
+
* - main thread: `idb` library via `dbPromise`
|
|
1214
|
+
* - service worker: raw `indexedDB.open` (see `offline-sw.ts`)
|
|
1215
|
+
*
|
|
1216
|
+
* They have different shapes so cannot be shared; the key computation
|
|
1217
|
+
* can be and is.
|
|
1218
|
+
*/
|
|
1219
|
+
/**
|
|
1220
|
+
* Extensions to try in order when the requested extension misses. `glb` is
|
|
1221
|
+
* last so batched-model sources (Mapbox Standard 3D buildings) resolve when
|
|
1222
|
+
* their source URL template ended in `.vector` or similar and the actual
|
|
1223
|
+
* tile body was stored as glb.
|
|
1224
|
+
*/
|
|
1225
|
+
const TILE_FALLBACK_EXTENSIONS = ['pbf', 'mvt', 'png', 'jpg', 'webp', 'glb'];
|
|
1226
|
+
/** Extensions minus the one the caller already tried. */
|
|
1227
|
+
function tileFallbackExtensions(requested) {
|
|
1228
|
+
return TILE_FALLBACK_EXTENSIONS.filter(e => e !== requested);
|
|
1229
|
+
}
|
|
1230
|
+
// ---------------------------------------------------------------------------
|
|
1231
|
+
// Region → style lookup
|
|
1232
|
+
// ---------------------------------------------------------------------------
|
|
1233
|
+
/**
|
|
1234
|
+
* Given an already-fetched list of style entries, find the first one whose
|
|
1235
|
+
* `regions` array contains the given ID. Pure — the caller is responsible for
|
|
1236
|
+
* loading the entries and for caching. Used by both `findStyleByRegionId`
|
|
1237
|
+
* implementations to keep the match rule identical.
|
|
1238
|
+
*/
|
|
1239
|
+
function findStyleByRegionIdIn(styles, regionId) {
|
|
1240
|
+
for (const entry of styles) {
|
|
1241
|
+
const regions = entry.regions;
|
|
1242
|
+
if (!Array.isArray(regions))
|
|
1243
|
+
continue;
|
|
1244
|
+
for (const r of regions) {
|
|
1245
|
+
if (r?.regionId === regionId || r?.id === regionId) {
|
|
1246
|
+
return entry;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
// ---------------------------------------------------------------------------
|
|
1253
|
+
// Glyph candidate keys
|
|
1254
|
+
// ---------------------------------------------------------------------------
|
|
1255
|
+
/**
|
|
1256
|
+
* Parse `FontA,FontB,FontC/0-255.pbf` into (fontstacks, rangePart). Mapbox
|
|
1257
|
+
* requests a comma-joined font-family fallback chain; each glyph is stored
|
|
1258
|
+
* individually, so the caller tries each fontstack in order.
|
|
1259
|
+
*/
|
|
1260
|
+
function parseGlyphPath(decodedPath) {
|
|
1261
|
+
const pathParts = decodedPath.split('/');
|
|
1262
|
+
const fontstackPart = pathParts[0] ?? '';
|
|
1263
|
+
const rangePart = pathParts[1] || '0-255.pbf';
|
|
1264
|
+
const fontstacks = fontstackPart
|
|
1265
|
+
.split(',')
|
|
1266
|
+
.map(f => f.trim())
|
|
1267
|
+
.filter(Boolean);
|
|
1268
|
+
return { fontstacks, rangePart };
|
|
1269
|
+
}
|
|
1270
|
+
/**
|
|
1271
|
+
* Build the list of keys to try for a single (fontstack, range) pair.
|
|
1272
|
+
* Order: actualStyleId variants first (most common), then downloadId,
|
|
1273
|
+
* then the bare path. Normalized and raw `.pbf`-less forms are both tried
|
|
1274
|
+
* to cover stored-key variants from older versions.
|
|
1275
|
+
*/
|
|
1276
|
+
function glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart) {
|
|
1277
|
+
const glyphPath = `${fontstack}/${rangePart}`;
|
|
1278
|
+
const normalizedPath = glyphPath.endsWith('.pbf') ? glyphPath : `${glyphPath}.pbf`;
|
|
1279
|
+
return dedupe([
|
|
1280
|
+
`${actualStyleId}::${normalizedPath}`,
|
|
1281
|
+
`${actualStyleId}::${glyphPath}`,
|
|
1282
|
+
`${downloadId}::${normalizedPath}`,
|
|
1283
|
+
`${downloadId}::${glyphPath}`,
|
|
1284
|
+
normalizedPath,
|
|
1285
|
+
glyphPath,
|
|
1286
|
+
]);
|
|
1287
|
+
}
|
|
1288
|
+
// ---------------------------------------------------------------------------
|
|
1289
|
+
// Sprite candidate keys
|
|
1290
|
+
// ---------------------------------------------------------------------------
|
|
1291
|
+
/**
|
|
1292
|
+
* Sprite keys have historically used both `::` and `:` as the separator, and
|
|
1293
|
+
* both the full filename (`sprite.json`) and the bare name (`sprite`). Return
|
|
1294
|
+
* every variant in priority order; the caller stops at the first hit.
|
|
1295
|
+
*/
|
|
1296
|
+
function spriteCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1297
|
+
const stripExt = decodedPath.replace(/\.(json|png)$/i, '');
|
|
1298
|
+
return dedupe([
|
|
1299
|
+
`${actualStyleId}::${decodedPath}`,
|
|
1300
|
+
`${actualStyleId}:${decodedPath}`,
|
|
1301
|
+
`${actualStyleId}::${stripExt}`,
|
|
1302
|
+
`${actualStyleId}:${stripExt}`,
|
|
1303
|
+
`${downloadId}::${decodedPath}`,
|
|
1304
|
+
`${downloadId}:${decodedPath}`,
|
|
1305
|
+
`${downloadId}::${stripExt}`,
|
|
1306
|
+
`${downloadId}:${stripExt}`,
|
|
1307
|
+
decodedPath,
|
|
1308
|
+
]);
|
|
1309
|
+
}
|
|
1310
|
+
// ---------------------------------------------------------------------------
|
|
1311
|
+
// Model candidate keys
|
|
1312
|
+
// ---------------------------------------------------------------------------
|
|
1313
|
+
/**
|
|
1314
|
+
* Model keys are `{styleId}::model::{name}`. Try the resolved style id first,
|
|
1315
|
+
* then the bare downloadId in case the request came through the region-scoped
|
|
1316
|
+
* URL form (`idb://{regionId}/model/{name}`).
|
|
1317
|
+
*/
|
|
1318
|
+
function modelCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1319
|
+
return dedupe([
|
|
1320
|
+
`${actualStyleId}::model::${decodedPath}`,
|
|
1321
|
+
`${downloadId}::model::${decodedPath}`,
|
|
1322
|
+
]);
|
|
1323
|
+
}
|
|
1324
|
+
// ---------------------------------------------------------------------------
|
|
1325
|
+
// TileJSON source matching
|
|
1326
|
+
// ---------------------------------------------------------------------------
|
|
1327
|
+
/**
|
|
1328
|
+
* Mapbox GL requests tilejson via `idb://{downloadId}/tilesjson/{path}` where
|
|
1329
|
+
* `{path}` may be the source id, the original TileJSON URL, or the URL we
|
|
1330
|
+
* stashed under `__originalTilesetUrl` when patching for offline. Try all
|
|
1331
|
+
* three; return the matching source id + its config, or null.
|
|
1332
|
+
*/
|
|
1333
|
+
function matchTileJsonSource(sources, decodedPath) {
|
|
1334
|
+
const asConfig = (v) => v && typeof v === 'object' ? v : null;
|
|
1335
|
+
if (decodedPath in sources) {
|
|
1336
|
+
const config = asConfig(sources[decodedPath]);
|
|
1337
|
+
if (config)
|
|
1338
|
+
return { sourceId: decodedPath, config };
|
|
1339
|
+
}
|
|
1340
|
+
for (const [sourceId, raw] of Object.entries(sources)) {
|
|
1341
|
+
const config = asConfig(raw);
|
|
1342
|
+
if (!config)
|
|
1343
|
+
continue;
|
|
1344
|
+
const url = typeof config.url === 'string' ? config.url : undefined;
|
|
1345
|
+
const original = typeof config.__originalTilesetUrl === 'string'
|
|
1346
|
+
? config.__originalTilesetUrl
|
|
1347
|
+
: undefined;
|
|
1348
|
+
if (url === decodedPath || original === decodedPath) {
|
|
1349
|
+
return { sourceId, config };
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
/**
|
|
1355
|
+
* Build the offline TileJSON payload that replaces the one Mapbox would
|
|
1356
|
+
* have fetched from the network. `tiles` is rewritten to serve from the SW
|
|
1357
|
+
* (the caller supplies the scheme via `tileUrlScheme`); copyable TileJSON
|
|
1358
|
+
* fields are preserved.
|
|
1359
|
+
*/
|
|
1360
|
+
function buildOfflineTileJson(sourceConfig, downloadId, sourceId, extension, tileUrlScheme, origin) {
|
|
1361
|
+
const base = `idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`
|
|
1362
|
+
;
|
|
1363
|
+
const tileJson = {
|
|
1364
|
+
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1365
|
+
name: sourceConfig.name ?? sourceId,
|
|
1366
|
+
tiles: [base],
|
|
1367
|
+
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1368
|
+
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1369
|
+
};
|
|
1370
|
+
const copyable = [
|
|
1371
|
+
'bounds',
|
|
1372
|
+
'center',
|
|
1373
|
+
'vector_layers',
|
|
1374
|
+
'scheme',
|
|
1375
|
+
'attribution',
|
|
1376
|
+
'encoding',
|
|
1377
|
+
'format',
|
|
1378
|
+
'grids',
|
|
1379
|
+
'data',
|
|
1380
|
+
'template',
|
|
1381
|
+
'version',
|
|
1382
|
+
];
|
|
1383
|
+
for (const field of copyable) {
|
|
1384
|
+
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1385
|
+
tileJson[field] = sourceConfig[field];
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return tileJson;
|
|
1389
|
+
}
|
|
1390
|
+
// ---------------------------------------------------------------------------
|
|
1391
|
+
// Internal helpers
|
|
1392
|
+
// ---------------------------------------------------------------------------
|
|
1393
|
+
function dedupe(values) {
|
|
1394
|
+
return Array.from(new Set(values));
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1181
1397
|
// idbFetchHandler.ts
|
|
1182
1398
|
// Intercepts idb:// URLs and serves resources from IndexedDB for MapLibre GL offline mode
|
|
1183
1399
|
const idbLogger = logger.scope('IDBFetch');
|
|
1184
1400
|
// idb://{downloadId}/tile/{sourceKey}/{url}
|
|
1185
1401
|
// idb://{downloadId}/glyph/{fontstack}/{range}.pbf
|
|
1186
1402
|
// idb://{downloadId}/sprite/{spriteName}
|
|
1403
|
+
// idb://{styleId}/model/{modelName}
|
|
1187
1404
|
// idb://{downloadId}/tilesjson/{url}
|
|
1188
1405
|
// Cache for region ID to style mapping to avoid repeated DB queries
|
|
1189
1406
|
const regionToStyleCache = new Map();
|
|
@@ -1233,16 +1450,11 @@ async function findStyleByRegionId(db, regionId) {
|
|
|
1233
1450
|
}
|
|
1234
1451
|
try {
|
|
1235
1452
|
const allStyles = await db.getAll('styles');
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
// Cache the result
|
|
1242
|
-
regionToStyleCache.set(regionId, { styleEntry, timestamp: Date.now() });
|
|
1243
|
-
return styleEntry;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1453
|
+
const hit = findStyleByRegionIdIn(allStyles, regionId);
|
|
1454
|
+
if (hit) {
|
|
1455
|
+
idbLogger.debug(`Found style "${hit.key}" containing region: ${regionId}`);
|
|
1456
|
+
regionToStyleCache.set(regionId, { styleEntry: hit, timestamp: Date.now() });
|
|
1457
|
+
return hit;
|
|
1246
1458
|
}
|
|
1247
1459
|
idbLogger.debug(`No style found containing region: ${regionId}`);
|
|
1248
1460
|
// Don't cache negative results — the region may be stored moments later
|
|
@@ -1254,36 +1466,6 @@ async function findStyleByRegionId(db, regionId) {
|
|
|
1254
1466
|
return null;
|
|
1255
1467
|
}
|
|
1256
1468
|
}
|
|
1257
|
-
function buildOfflineTileJson(sourceConfig, downloadId, sourceId) {
|
|
1258
|
-
const extension = deriveTileExtension(sourceConfig.tiles);
|
|
1259
|
-
const offlineTiles = [`idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`];
|
|
1260
|
-
const tileJson = {
|
|
1261
|
-
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1262
|
-
name: sourceConfig.name ?? sourceId,
|
|
1263
|
-
tiles: offlineTiles,
|
|
1264
|
-
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1265
|
-
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1266
|
-
};
|
|
1267
|
-
const fieldsToCopy = [
|
|
1268
|
-
'bounds',
|
|
1269
|
-
'center',
|
|
1270
|
-
'vector_layers',
|
|
1271
|
-
'scheme',
|
|
1272
|
-
'attribution',
|
|
1273
|
-
'encoding',
|
|
1274
|
-
'format',
|
|
1275
|
-
'grids',
|
|
1276
|
-
'data',
|
|
1277
|
-
'template',
|
|
1278
|
-
'version',
|
|
1279
|
-
];
|
|
1280
|
-
for (const field of fieldsToCopy) {
|
|
1281
|
-
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1282
|
-
tileJson[field] = sourceConfig[field];
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
return tileJson;
|
|
1286
|
-
}
|
|
1287
1469
|
async function createTileResponse(resource) {
|
|
1288
1470
|
const headers = {};
|
|
1289
1471
|
// Set proper content type for vector tiles (PBF/MVT format)
|
|
@@ -1378,7 +1560,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1378
1560
|
// but tiles are stored with integer zoom levels, so floor the value
|
|
1379
1561
|
const z = Math.floor(parseFloat(pathParts[pathParts.length - 3]));
|
|
1380
1562
|
const sourceKey = pathParts.slice(0, pathParts.length - 3).join('/');
|
|
1381
|
-
const yMatch = yExt.match(
|
|
1563
|
+
const yMatch = yExt.match(/^(\d+)\.(\w+)$/);
|
|
1382
1564
|
if (yMatch) {
|
|
1383
1565
|
const y = parseInt(yMatch[1]);
|
|
1384
1566
|
const requestedExt = yMatch[2]; // Extension from URL (for logging only)
|
|
@@ -1393,7 +1575,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1393
1575
|
}
|
|
1394
1576
|
idbLogger.debug(`Tile not found: ${tileKey}`);
|
|
1395
1577
|
// Fallback: try common alternative extensions
|
|
1396
|
-
const fallbackExtensions =
|
|
1578
|
+
const fallbackExtensions = tileFallbackExtensions(requestedExt);
|
|
1397
1579
|
for (const fallbackExt of fallbackExtensions) {
|
|
1398
1580
|
const fallbackKey = createTileKey(x, y, z, actualStyleId, sourceKey, fallbackExt);
|
|
1399
1581
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1435,7 +1617,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1435
1617
|
return await createTileResponse(resource);
|
|
1436
1618
|
}
|
|
1437
1619
|
// Try alternative extensions
|
|
1438
|
-
const fallbackExts =
|
|
1620
|
+
const fallbackExts = tileFallbackExtensions(ext);
|
|
1439
1621
|
for (const fallbackExt of fallbackExts) {
|
|
1440
1622
|
const fallbackKey = createTileKey(parseInt(x), parseInt(y), parseInt(z), actualStyleId, fallbackSourceKey, fallbackExt);
|
|
1441
1623
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1455,46 +1637,19 @@ async function idbFetchHandler(url, init) {
|
|
|
1455
1637
|
}
|
|
1456
1638
|
case 'glyph': {
|
|
1457
1639
|
idbLogger.debug(`Looking for glyph with key: ${key}`);
|
|
1458
|
-
// Find which style this region belongs to
|
|
1459
1640
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1460
1641
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1461
|
-
|
|
1462
|
-
idbLogger.debug(`Region "${downloadId}" belongs to style "${actualStyleId}", searching with style key`);
|
|
1463
|
-
}
|
|
1464
|
-
// Parse the resource path: "FontA,FontB,FontC/0-255.pbf"
|
|
1465
|
-
// MapLibre requests glyphs with comma-separated fallback fonts
|
|
1466
|
-
// but glyphs are stored individually per font
|
|
1467
|
-
const pathParts = decodedResourcePath.split('/');
|
|
1468
|
-
const fontstackPart = pathParts[0]; // "FontA,FontB,FontC"
|
|
1469
|
-
const rangePart = pathParts[1] || '0-255.pbf'; // "0-255.pbf"
|
|
1470
|
-
// Split comma-separated fonts
|
|
1471
|
-
const fontstacks = fontstackPart.split(',').map(f => f.trim());
|
|
1642
|
+
const { fontstacks, rangePart } = parseGlyphPath(decodedResourcePath);
|
|
1472
1643
|
idbLogger.debug(`Trying ${fontstacks.length} fonts in fallback order: ${fontstacks.join(', ')}`);
|
|
1473
|
-
// Try each font in order (this is how font fallbacks work)
|
|
1474
1644
|
for (const fontstack of fontstacks) {
|
|
1475
|
-
const
|
|
1476
|
-
const
|
|
1477
|
-
const glyphCandidateKeys = [
|
|
1478
|
-
// Try with actual style ID first
|
|
1479
|
-
`${actualStyleId}::${normalizedPath}`,
|
|
1480
|
-
`${actualStyleId}::${glyphPath}`,
|
|
1481
|
-
// Then try with download ID
|
|
1482
|
-
`${downloadId}::${normalizedPath}`,
|
|
1483
|
-
`${downloadId}::${glyphPath}`,
|
|
1484
|
-
// Just paths
|
|
1485
|
-
normalizedPath,
|
|
1486
|
-
glyphPath,
|
|
1487
|
-
];
|
|
1488
|
-
idbLogger.debug(`Trying keys for font "${fontstack}":`, glyphCandidateKeys);
|
|
1489
|
-
for (const candidateKey of glyphCandidateKeys) {
|
|
1645
|
+
const candidateKeys = glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart);
|
|
1646
|
+
for (const candidateKey of candidateKeys) {
|
|
1490
1647
|
const resource = await db.get('glyphs', candidateKey);
|
|
1491
1648
|
if (resource?.data) {
|
|
1492
1649
|
idbLogger.debug(`Found glyph using key: ${candidateKey} (font: ${fontstack})`);
|
|
1493
1650
|
return new Response(resource.data, {
|
|
1494
1651
|
status: 200,
|
|
1495
|
-
headers: {
|
|
1496
|
-
'Content-Type': 'application/x-protobuf',
|
|
1497
|
-
},
|
|
1652
|
+
headers: { 'Content-Type': 'application/x-protobuf' },
|
|
1498
1653
|
});
|
|
1499
1654
|
}
|
|
1500
1655
|
}
|
|
@@ -1504,33 +1659,11 @@ async function idbFetchHandler(url, init) {
|
|
|
1504
1659
|
}
|
|
1505
1660
|
case 'sprite': {
|
|
1506
1661
|
idbLogger.debug(`Looking for sprite with key: ${key}`);
|
|
1507
|
-
// Find which style this region belongs to
|
|
1508
1662
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1509
1663
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
// The sprite service stores sprites with keys like: "voyager::sprite.json", "voyager::sprite@2x.json"
|
|
1514
|
-
// MapLibre requests sprites as: "idb://region_XXX/sprite/sprite@2x.json"
|
|
1515
|
-
// So we need to map the region ID to the style ID
|
|
1516
|
-
const spriteCandidateKeys = Array.from(new Set([
|
|
1517
|
-
// Try with actual style ID first (most likely to work)
|
|
1518
|
-
`${actualStyleId}::${decodedResourcePath}`,
|
|
1519
|
-
`${actualStyleId}:${decodedResourcePath}`,
|
|
1520
|
-
`${actualStyleId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1521
|
-
`${actualStyleId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1522
|
-
// Then try with download ID (in case it's a direct style download)
|
|
1523
|
-
`${downloadId}::${decodedResourcePath}`,
|
|
1524
|
-
`${downloadId}:${decodedResourcePath}`,
|
|
1525
|
-
`${downloadId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1526
|
-
`${downloadId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1527
|
-
// Just the path itself
|
|
1528
|
-
decodedResourcePath,
|
|
1529
|
-
// Original key format
|
|
1530
|
-
key,
|
|
1531
|
-
]));
|
|
1532
|
-
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, spriteCandidateKeys);
|
|
1533
|
-
for (const candidateKey of spriteCandidateKeys) {
|
|
1664
|
+
const candidates = spriteCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1665
|
+
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, candidates);
|
|
1666
|
+
for (const candidateKey of candidates) {
|
|
1534
1667
|
const resource = await db.get('sprites', candidateKey);
|
|
1535
1668
|
if (resource?.data) {
|
|
1536
1669
|
idbLogger.debug(`Found sprite using key: ${candidateKey}`);
|
|
@@ -1540,7 +1673,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1540
1673
|
});
|
|
1541
1674
|
}
|
|
1542
1675
|
}
|
|
1543
|
-
idbLogger.warn(`Sprite not found, tried keys: ${
|
|
1676
|
+
idbLogger.warn(`Sprite not found, tried keys: ${candidates.join(', ')}`);
|
|
1544
1677
|
break;
|
|
1545
1678
|
}
|
|
1546
1679
|
case 'font': {
|
|
@@ -1555,11 +1688,32 @@ async function idbFetchHandler(url, init) {
|
|
|
1555
1688
|
}
|
|
1556
1689
|
break;
|
|
1557
1690
|
}
|
|
1691
|
+
case 'model': {
|
|
1692
|
+
// Model URLs are rewritten by patchStyleForOffline to
|
|
1693
|
+
// idb://{styleId}/model/{modelName}
|
|
1694
|
+
// Models are keyed by {styleId}::model::{modelName} in the store.
|
|
1695
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1696
|
+
const actualStyleId = styleEntry?.key || downloadId;
|
|
1697
|
+
const candidates = modelCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1698
|
+
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1699
|
+
for (const candidateKey of candidates) {
|
|
1700
|
+
const resource = await db.get('models', candidateKey);
|
|
1701
|
+
if (resource?.data) {
|
|
1702
|
+
idbLogger.debug(`Found model using key: ${candidateKey}`);
|
|
1703
|
+
return new Response(resource.data, {
|
|
1704
|
+
status: 200,
|
|
1705
|
+
headers: { 'Content-Type': resource.contentType || 'model/gltf-binary' },
|
|
1706
|
+
});
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
idbLogger.warn(`Model not found, tried keys: ${candidates.join(', ')}`);
|
|
1710
|
+
break;
|
|
1711
|
+
}
|
|
1558
1712
|
case 'tilesjson': {
|
|
1559
1713
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1560
|
-
// First try direct lookup (for style-level downloads)
|
|
1714
|
+
// First try direct lookup (for style-level downloads), then fall back
|
|
1715
|
+
// to searching by region ID (for region-level downloads).
|
|
1561
1716
|
let styleEntry = await db.get('styles', downloadId);
|
|
1562
|
-
// If not found, search by region ID (for region-level downloads)
|
|
1563
1717
|
if (!styleEntry || !styleEntry.style?.sources) {
|
|
1564
1718
|
idbLogger.debug(`Style not found with key "${downloadId}", searching by region ID...`);
|
|
1565
1719
|
const foundStyle = await findStyleByRegionId(db, downloadId);
|
|
@@ -1567,41 +1721,23 @@ async function idbFetchHandler(url, init) {
|
|
|
1567
1721
|
styleEntry = foundStyle;
|
|
1568
1722
|
}
|
|
1569
1723
|
}
|
|
1570
|
-
if (styleEntry?.style?.sources) {
|
|
1571
|
-
const sources = styleEntry.style.sources;
|
|
1572
|
-
let matchedSourceId;
|
|
1573
|
-
let matchedSourceConfig;
|
|
1574
|
-
if (decodedResourcePath in sources) {
|
|
1575
|
-
matchedSourceId = decodedResourcePath;
|
|
1576
|
-
matchedSourceConfig = sources[decodedResourcePath];
|
|
1577
|
-
}
|
|
1578
|
-
else {
|
|
1579
|
-
for (const [sourceId, sourceValue] of Object.entries(sources)) {
|
|
1580
|
-
const sourceUrl = typeof sourceValue.url === 'string' ? sourceValue.url : undefined;
|
|
1581
|
-
const originalUrl = typeof sourceValue.__originalTilesetUrl === 'string'
|
|
1582
|
-
? sourceValue.__originalTilesetUrl
|
|
1583
|
-
: undefined;
|
|
1584
|
-
if (sourceUrl === decodedResourcePath || originalUrl === decodedResourcePath) {
|
|
1585
|
-
matchedSourceId = sourceId;
|
|
1586
|
-
matchedSourceConfig = sourceValue;
|
|
1587
|
-
break;
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
if (matchedSourceId && matchedSourceConfig) {
|
|
1592
|
-
const tileJson = buildOfflineTileJson(matchedSourceConfig, downloadId, matchedSourceId);
|
|
1593
|
-
idbLogger.debug(`Serving offline tilejson for source: ${matchedSourceId}`);
|
|
1594
|
-
return new Response(JSON.stringify(tileJson), {
|
|
1595
|
-
status: 200,
|
|
1596
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1600
|
-
}
|
|
1601
|
-
else {
|
|
1724
|
+
if (!styleEntry?.style?.sources) {
|
|
1602
1725
|
idbLogger.warn(`Style not found or missing sources for downloadId: ${downloadId}`);
|
|
1726
|
+
break;
|
|
1603
1727
|
}
|
|
1604
|
-
|
|
1728
|
+
const sources = styleEntry.style.sources;
|
|
1729
|
+
const matched = matchTileJsonSource(sources, decodedResourcePath);
|
|
1730
|
+
if (!matched) {
|
|
1731
|
+
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
const extension = deriveTileExtension(matched.config.tiles);
|
|
1735
|
+
const tileJson = buildOfflineTileJson(matched.config, downloadId, matched.sourceId, extension, 'idb');
|
|
1736
|
+
idbLogger.debug(`Serving offline tilejson for source: ${matched.sourceId}`);
|
|
1737
|
+
return new Response(JSON.stringify(tileJson), {
|
|
1738
|
+
status: 200,
|
|
1739
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1740
|
+
});
|
|
1605
1741
|
}
|
|
1606
1742
|
default:
|
|
1607
1743
|
idbLogger.warn(`Unknown resource type: ${type}`);
|
|
@@ -1625,6 +1761,37 @@ async function idbFetchHandler(url, init) {
|
|
|
1625
1761
|
function isMapboxProtocol(url) {
|
|
1626
1762
|
return url.startsWith(MAPBOX_API.PROTOCOL);
|
|
1627
1763
|
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Parse a URL and return its hostname, or null if the URL is malformed.
|
|
1766
|
+
* Accepts relative URLs when `base` is provided.
|
|
1767
|
+
*/
|
|
1768
|
+
function getUrlHostname(url, base) {
|
|
1769
|
+
try {
|
|
1770
|
+
return new URL(url, base).hostname.toLowerCase();
|
|
1771
|
+
}
|
|
1772
|
+
catch {
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* True if `url`'s hostname equals `host` or is a subdomain of `host`.
|
|
1778
|
+
* Uses URL parsing (not substring matching) to avoid false positives like
|
|
1779
|
+
* `https://evil.com/?x=mapbox.com` matching `mapbox.com`.
|
|
1780
|
+
*/
|
|
1781
|
+
function hostMatches(url, host, base) {
|
|
1782
|
+
const hostname = getUrlHostname(url, base);
|
|
1783
|
+
if (hostname === null)
|
|
1784
|
+
return false;
|
|
1785
|
+
const target = host.toLowerCase();
|
|
1786
|
+
return hostname === target || hostname.endsWith('.' + target);
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* True for any host under the mapbox.com domain (including api.mapbox.com,
|
|
1790
|
+
* *.tiles.mapbox.com, etc.). Used by provider detection.
|
|
1791
|
+
*/
|
|
1792
|
+
function isMapboxHost(url, base) {
|
|
1793
|
+
return hostMatches(url, 'mapbox.com', base);
|
|
1794
|
+
}
|
|
1628
1795
|
/**
|
|
1629
1796
|
* Resolve a mapbox:// URL to its HTTPS API equivalent
|
|
1630
1797
|
*
|
|
@@ -1698,9 +1865,7 @@ function rewriteMapboxCdnTileUrl(tileUrl) {
|
|
|
1698
1865
|
*/
|
|
1699
1866
|
function detectStyleProvider(styleUrl, style) {
|
|
1700
1867
|
// Check URL patterns
|
|
1701
|
-
if (isMapboxProtocol(styleUrl) ||
|
|
1702
|
-
styleUrl.includes('mapbox.com') ||
|
|
1703
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
1868
|
+
if (isMapboxProtocol(styleUrl) || isMapboxHost(styleUrl)) {
|
|
1704
1869
|
return 'mapbox';
|
|
1705
1870
|
}
|
|
1706
1871
|
if (styleUrl.includes('maplibre') ||
|
|
@@ -1719,7 +1884,7 @@ function detectStyleProvider(styleUrl, style) {
|
|
|
1719
1884
|
const sources = style.sources || {};
|
|
1720
1885
|
for (const [, sourceConfig] of Object.entries(sources)) {
|
|
1721
1886
|
const source = sourceConfig;
|
|
1722
|
-
if (source.url && (source.url
|
|
1887
|
+
if (source.url && (isMapboxProtocol(source.url) || isMapboxHost(source.url))) {
|
|
1723
1888
|
return 'mapbox';
|
|
1724
1889
|
}
|
|
1725
1890
|
}
|
|
@@ -1811,7 +1976,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1811
1976
|
if (isMapboxProtocol(tileUrl) && accessToken) {
|
|
1812
1977
|
return resolveMapboxUrl(tileUrl, accessToken);
|
|
1813
1978
|
}
|
|
1814
|
-
if (provider === 'mapbox' && accessToken && tileUrl
|
|
1979
|
+
if (provider === 'mapbox' && accessToken && isMapboxHost(tileUrl)) {
|
|
1815
1980
|
return normalizeStyleUrl(tileUrl, accessToken);
|
|
1816
1981
|
}
|
|
1817
1982
|
return tileUrl;
|
|
@@ -1826,7 +1991,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1826
1991
|
if (isMapboxProtocol(processedStyle.sprite)) {
|
|
1827
1992
|
processedStyle.sprite = resolveMapboxUrl(processedStyle.sprite, accessToken);
|
|
1828
1993
|
}
|
|
1829
|
-
else if (provider === 'mapbox' && processedStyle.sprite
|
|
1994
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.sprite)) {
|
|
1830
1995
|
processedStyle.sprite = normalizeStyleUrl(processedStyle.sprite, accessToken);
|
|
1831
1996
|
}
|
|
1832
1997
|
}
|
|
@@ -1837,7 +2002,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1837
2002
|
if (isMapboxProtocol(entry.url)) {
|
|
1838
2003
|
return { ...entry, url: resolveMapboxUrl(entry.url, accessToken) };
|
|
1839
2004
|
}
|
|
1840
|
-
else if (provider === 'mapbox' && entry.url
|
|
2005
|
+
else if (provider === 'mapbox' && isMapboxHost(entry.url)) {
|
|
1841
2006
|
return { ...entry, url: normalizeStyleUrl(entry.url, accessToken) };
|
|
1842
2007
|
}
|
|
1843
2008
|
}
|
|
@@ -1850,7 +2015,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1850
2015
|
if (isMapboxProtocol(processedStyle.glyphs)) {
|
|
1851
2016
|
processedStyle.glyphs = resolveMapboxUrl(processedStyle.glyphs, accessToken);
|
|
1852
2017
|
}
|
|
1853
|
-
else if (provider === 'mapbox' && processedStyle.glyphs
|
|
2018
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.glyphs)) {
|
|
1854
2019
|
processedStyle.glyphs = normalizeStyleUrl(processedStyle.glyphs, accessToken);
|
|
1855
2020
|
}
|
|
1856
2021
|
}
|
|
@@ -1890,13 +2055,20 @@ function validateStyleForProvider(style, provider) {
|
|
|
1890
2055
|
// Check for Mapbox-specific requirements
|
|
1891
2056
|
const hasMapboxSources = Object.values(style.sources || {}).some((source) => {
|
|
1892
2057
|
const s = source;
|
|
1893
|
-
return s.url && s.url
|
|
2058
|
+
return !!s.url && isMapboxHost(s.url);
|
|
1894
2059
|
});
|
|
1895
2060
|
if (hasMapboxSources) {
|
|
1896
2061
|
// Check if access token might be needed
|
|
1897
2062
|
const hasAccessToken = Object.values(style.sources || {}).some((source) => {
|
|
1898
2063
|
const s = source;
|
|
1899
|
-
|
|
2064
|
+
if (!s.url)
|
|
2065
|
+
return false;
|
|
2066
|
+
try {
|
|
2067
|
+
return new URL(s.url).searchParams.has('access_token');
|
|
2068
|
+
}
|
|
2069
|
+
catch {
|
|
2070
|
+
return false;
|
|
2071
|
+
}
|
|
1900
2072
|
});
|
|
1901
2073
|
if (!hasAccessToken) {
|
|
1902
2074
|
warnings.push('Mapbox sources detected but no access token found - authentication may be required');
|
|
@@ -1930,14 +2102,15 @@ function patchStyleForOffline(style, downloadId, maxZoom, tileExtension, styleId
|
|
|
1930
2102
|
styleLogger.debug(`Patching source: ${sourceKey}`, source);
|
|
1931
2103
|
if (source.tiles) {
|
|
1932
2104
|
const originalTiles = [...source.tiles];
|
|
1933
|
-
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext
|
|
2105
|
+
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext.
|
|
2106
|
+
// Extension extraction goes through the shared extractTileExtensionFromUrl
|
|
2107
|
+
// helper so the patched URL's extension matches what tileService used when
|
|
2108
|
+
// storing — otherwise Mapbox v4 tile URLs (`{y}.vector.pbf`) produced a
|
|
2109
|
+
// stored key under `.pbf` but a patched URL with `.vector`, forcing
|
|
2110
|
+
// idbFetchHandler to fall through its pbf/mvt/png/jpg/webp fallback loop
|
|
2111
|
+
// on every tile.
|
|
1934
2112
|
source.tiles = source.tiles.map((url) => {
|
|
1935
|
-
|
|
1936
|
-
let ext = tileExtension;
|
|
1937
|
-
if (!ext) {
|
|
1938
|
-
const extMatch = url.match(/\{z\}\/\{x\}\/\{y\}\.(\w+)/);
|
|
1939
|
-
ext = extMatch ? extMatch[1] : 'pbf';
|
|
1940
|
-
}
|
|
2113
|
+
const ext = tileExtension ?? extractTileExtensionFromUrl(url);
|
|
1941
2114
|
return `idb://${downloadId}/tile/${sourceKey}/{z}/{x}/{y}.${ext}`;
|
|
1942
2115
|
});
|
|
1943
2116
|
styleLogger.debug(`Patched tiles for ${sourceKey} with extension .${tileExtension || 'pbf'}:`, {
|
|
@@ -2010,14 +2183,29 @@ function patchStyleForOffline(style, downloadId, maxZoom, tileExtension, styleId
|
|
|
2010
2183
|
});
|
|
2011
2184
|
}
|
|
2012
2185
|
}
|
|
2013
|
-
// Patch top-level models (Mapbox Standard 3D
|
|
2186
|
+
// Patch top-level models (Mapbox Standard 3D trees / wind turbines).
|
|
2187
|
+
// Two shapes exist in the wild:
|
|
2188
|
+
// - Mapbox Standard: `{ "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb" }` (string values)
|
|
2189
|
+
// - Older/generic: `{ "name": { "uri": "mapbox://..." } }` (object values)
|
|
2190
|
+
// Models are keyed on the style ID (like sprites) so they can be shared
|
|
2191
|
+
// across regions.
|
|
2014
2192
|
if (style.models) {
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2193
|
+
const modelBaseId = styleId || downloadId;
|
|
2194
|
+
const models = style.models;
|
|
2195
|
+
let patchedCount = 0;
|
|
2196
|
+
for (const [modelId, value] of Object.entries(models)) {
|
|
2197
|
+
if (typeof value === 'string') {
|
|
2198
|
+
models[modelId] = `idb://${modelBaseId}/model/${modelId}`;
|
|
2199
|
+
patchedCount++;
|
|
2200
|
+
}
|
|
2201
|
+
else if (value && typeof value === 'object' && 'uri' in value && value.uri) {
|
|
2202
|
+
value.uri = `idb://${modelBaseId}/model/${modelId}`;
|
|
2203
|
+
patchedCount++;
|
|
2018
2204
|
}
|
|
2019
2205
|
}
|
|
2020
|
-
|
|
2206
|
+
if (patchedCount > 0) {
|
|
2207
|
+
styleLogger.debug(`Patched ${patchedCount} model URIs (styleId: ${modelBaseId})`);
|
|
2208
|
+
}
|
|
2021
2209
|
}
|
|
2022
2210
|
styleLogger.debug(`Final patched style:`, style);
|
|
2023
2211
|
return style;
|
|
@@ -2585,12 +2773,22 @@ function convertStyleForServiceWorker(style) {
|
|
|
2585
2773
|
});
|
|
2586
2774
|
}
|
|
2587
2775
|
}
|
|
2588
|
-
// Convert models
|
|
2776
|
+
// Convert models. Two shapes in the wild:
|
|
2777
|
+
// - Mapbox Standard: `{ name: "idb://..." }` (string value)
|
|
2778
|
+
// - Older/generic: `{ name: { uri: "idb://..." } }` (object value)
|
|
2589
2779
|
if (converted.models) {
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2780
|
+
const models = converted.models;
|
|
2781
|
+
for (const modelKey of Object.keys(models)) {
|
|
2782
|
+
const value = models[modelKey];
|
|
2783
|
+
if (typeof value === 'string') {
|
|
2784
|
+
if (value.startsWith('idb://')) {
|
|
2785
|
+
models[modelKey] = replace(value);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
else if (value && typeof value === 'object') {
|
|
2789
|
+
if (typeof value.uri === 'string' && value.uri.startsWith('idb://')) {
|
|
2790
|
+
value.uri = replace(value.uri);
|
|
2791
|
+
}
|
|
2594
2792
|
}
|
|
2595
2793
|
}
|
|
2596
2794
|
}
|
|
@@ -2790,10 +2988,8 @@ async function resolveImportsRecursive(style, accessToken, visited, depth, maxRe
|
|
|
2790
2988
|
if (typeof prefixedLayer.source === 'string') {
|
|
2791
2989
|
prefixedLayer.source = `${importId}/${prefixedLayer.source}`;
|
|
2792
2990
|
}
|
|
2793
|
-
// Resolve ["config", "key"] expressions using schema defaults and import overrides
|
|
2794
|
-
|
|
2795
|
-
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2796
|
-
}
|
|
2991
|
+
// Resolve ["config", "key"] expressions using schema defaults and import overrides.
|
|
2992
|
+
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2797
2993
|
flattenedLayers.push(prefixedLayer);
|
|
2798
2994
|
}
|
|
2799
2995
|
}
|
|
@@ -2831,8 +3027,48 @@ async function resolveImportsRecursive(style, accessToken, visited, depth, maxRe
|
|
|
2831
3027
|
if (!style.models && importedModels) {
|
|
2832
3028
|
style.models = importedModels;
|
|
2833
3029
|
}
|
|
3030
|
+
// Rewrite indoor-only expressions so the flattened style validates without
|
|
3031
|
+
// the `imports` wrapper at render time — see sanitizeIndoorExpressions.
|
|
3032
|
+
sanitizeIndoorExpressions(style);
|
|
2834
3033
|
return style;
|
|
2835
3034
|
}
|
|
3035
|
+
/**
|
|
3036
|
+
* Rewrite indoor-only expressions in a style's layers to their outdoor no-op
|
|
3037
|
+
* constants. See the in-line comment in `resolveValue` for why this is needed
|
|
3038
|
+
* for Mapbox Standard when the `imports` wrapper is stripped.
|
|
3039
|
+
*
|
|
3040
|
+
* Safe to call multiple times and on already-downloaded stored styles — the
|
|
3041
|
+
* rewrites are idempotent (after the first pass there are no more
|
|
3042
|
+
* `is-active-floor` / `floor-level` expressions to rewrite).
|
|
3043
|
+
*/
|
|
3044
|
+
function sanitizeIndoorExpressions(style) {
|
|
3045
|
+
const layers = style.layers;
|
|
3046
|
+
if (!Array.isArray(layers))
|
|
3047
|
+
return;
|
|
3048
|
+
for (const layer of layers) {
|
|
3049
|
+
if (layer && typeof layer === 'object') {
|
|
3050
|
+
rewriteIndoor(layer);
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
function rewriteIndoor(obj) {
|
|
3055
|
+
for (const key of Object.keys(obj)) {
|
|
3056
|
+
obj[key] = rewriteIndoorValue(obj[key]);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
function rewriteIndoorValue(value) {
|
|
3060
|
+
if (!Array.isArray(value)) {
|
|
3061
|
+
if (value && typeof value === 'object' && !ArrayBuffer.isView(value)) {
|
|
3062
|
+
rewriteIndoor(value);
|
|
3063
|
+
}
|
|
3064
|
+
return value;
|
|
3065
|
+
}
|
|
3066
|
+
if (value[0] === 'is-active-floor')
|
|
3067
|
+
return false;
|
|
3068
|
+
if (value[0] === 'floor-level' && value.length === 1)
|
|
3069
|
+
return 0;
|
|
3070
|
+
return value.map(rewriteIndoorValue);
|
|
3071
|
+
}
|
|
2836
3072
|
/**
|
|
2837
3073
|
* Deep clone a plain object/array (JSON-safe values only).
|
|
2838
3074
|
*/
|
|
@@ -2998,6 +3234,40 @@ function mergeSprites(outer, imported) {
|
|
|
2998
3234
|
return result;
|
|
2999
3235
|
}
|
|
3000
3236
|
|
|
3237
|
+
const DEFAULT_WASM_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.14.1/dist/';
|
|
3238
|
+
let currentConfig = {};
|
|
3239
|
+
let sqlJsPromise = null;
|
|
3240
|
+
/**
|
|
3241
|
+
* Override how `sql.js` loads its WebAssembly. Call once before any MBTiles
|
|
3242
|
+
* import/export is invoked. Resets any cached init.
|
|
3243
|
+
*/
|
|
3244
|
+
function configureSqlJs(config) {
|
|
3245
|
+
currentConfig = { ...config };
|
|
3246
|
+
sqlJsPromise = null;
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Lazily initialise `sql.js`. The underlying module is loaded via dynamic
|
|
3250
|
+
* `import()` so it only ships with bundles that actually call MBTiles code.
|
|
3251
|
+
*/
|
|
3252
|
+
async function getSqlJs() {
|
|
3253
|
+
if (sqlJsPromise)
|
|
3254
|
+
return sqlJsPromise;
|
|
3255
|
+
sqlJsPromise = (async () => {
|
|
3256
|
+
const mod = (await import('sql.js'));
|
|
3257
|
+
const initSqlJs = mod.default;
|
|
3258
|
+
const options = {};
|
|
3259
|
+
if (currentConfig.wasmBinary) {
|
|
3260
|
+
options.wasmBinary = currentConfig.wasmBinary;
|
|
3261
|
+
}
|
|
3262
|
+
else {
|
|
3263
|
+
const base = currentConfig.wasmUrl ?? DEFAULT_WASM_URL;
|
|
3264
|
+
options.locateFile = (file) => base.endsWith('/') ? `${base}${file}` : `${base}/${file}`;
|
|
3265
|
+
}
|
|
3266
|
+
return initSqlJs(options);
|
|
3267
|
+
})();
|
|
3268
|
+
return sqlJsPromise;
|
|
3269
|
+
}
|
|
3270
|
+
|
|
3001
3271
|
const fontLogger = logger.scope('FontService');
|
|
3002
3272
|
class FontService {
|
|
3003
3273
|
db = dbPromise;
|
|
@@ -3181,15 +3451,23 @@ class FontService {
|
|
|
3181
3451
|
},
|
|
3182
3452
|
};
|
|
3183
3453
|
}
|
|
3184
|
-
|
|
3454
|
+
/**
|
|
3455
|
+
* Delete fonts older than `maxAge` days. When `options.styleId` is
|
|
3456
|
+
* provided, only fonts belonging to that style (per the delimiter-aware
|
|
3457
|
+
* `resourceKeyBelongsToStyle` match) are eligible — callers relying on
|
|
3458
|
+
* a styleId filter previously got a silent full-store wipe.
|
|
3459
|
+
*/
|
|
3460
|
+
async cleanupOldFonts(maxAge = 30, options = {}) {
|
|
3185
3461
|
const db = await this.db;
|
|
3186
3462
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3463
|
+
const { styleId } = options;
|
|
3187
3464
|
const tx = db.transaction(['fonts'], 'readwrite');
|
|
3188
3465
|
let deletedCount = 0;
|
|
3189
3466
|
let cursor = await tx.objectStore('fonts').openCursor();
|
|
3190
3467
|
while (cursor) {
|
|
3191
3468
|
const fontEntry = cursor.value;
|
|
3192
|
-
|
|
3469
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(fontEntry.key, styleId);
|
|
3470
|
+
if (belongs && fontEntry.lastModified < cutoffTime) {
|
|
3193
3471
|
await cursor.delete();
|
|
3194
3472
|
deletedCount++;
|
|
3195
3473
|
}
|
|
@@ -3355,7 +3633,7 @@ const fontService = new FontService();
|
|
|
3355
3633
|
const downloadFonts = (fontUrls, styleName, options) => fontService.downloadFonts(fontUrls, styleName, options);
|
|
3356
3634
|
const getFontStats = () => fontService.getFontStats();
|
|
3357
3635
|
const getFontAnalytics = () => fontService.getFontAnalytics();
|
|
3358
|
-
const cleanupOldFonts = (maxAge) => fontService.cleanupOldFonts(maxAge);
|
|
3636
|
+
const cleanupOldFonts = (maxAge, options) => fontService.cleanupOldFonts(maxAge, options);
|
|
3359
3637
|
const verifyAndRepairFonts = () => fontService.verifyAndRepairFonts();
|
|
3360
3638
|
|
|
3361
3639
|
const spriteLogger = logger.scope('SpriteService');
|
|
@@ -3666,19 +3944,24 @@ class SpriteService {
|
|
|
3666
3944
|
};
|
|
3667
3945
|
}
|
|
3668
3946
|
/**
|
|
3669
|
-
*
|
|
3947
|
+
* Remove sprites older than the specified age. When `options.styleId` is
|
|
3948
|
+
* provided, only sprites belonging to that style (per
|
|
3949
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
3670
3950
|
* @param maxAge - Maximum age in days (default: 30)
|
|
3951
|
+
* @param options.styleId - Optional style filter; omit to scan all styles
|
|
3671
3952
|
* @returns Promise resolving to number of deleted sprites
|
|
3672
3953
|
*/
|
|
3673
|
-
async cleanupOldSprites(maxAge = 30) {
|
|
3954
|
+
async cleanupOldSprites(maxAge = 30, options = {}) {
|
|
3674
3955
|
const db = await this.db;
|
|
3675
3956
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3957
|
+
const { styleId } = options;
|
|
3676
3958
|
const tx = db.transaction(['sprites'], 'readwrite');
|
|
3677
3959
|
let deletedCount = 0;
|
|
3678
3960
|
let cursor = await tx.objectStore('sprites').openCursor();
|
|
3679
3961
|
while (cursor) {
|
|
3680
3962
|
const spriteEntry = cursor.value;
|
|
3681
|
-
|
|
3963
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(spriteEntry.key, styleId);
|
|
3964
|
+
if (belongs && spriteEntry.lastModified < cutoffTime) {
|
|
3682
3965
|
await cursor.delete();
|
|
3683
3966
|
deletedCount++;
|
|
3684
3967
|
}
|
|
@@ -3832,7 +4115,7 @@ const spriteService = new SpriteService();
|
|
|
3832
4115
|
const downloadSprites = (spriteUrls, styleName, options) => spriteService.downloadSprites(spriteUrls, styleName, options);
|
|
3833
4116
|
const getSpriteStats = () => spriteService.getSpriteStats();
|
|
3834
4117
|
const getSpriteAnalytics = () => spriteService.getSpriteAnalytics();
|
|
3835
|
-
const cleanupOldSprites = (maxAge) => spriteService.cleanupOldSprites(maxAge);
|
|
4118
|
+
const cleanupOldSprites = (maxAge, options) => spriteService.cleanupOldSprites(maxAge, options);
|
|
3836
4119
|
const verifyAndRepairSprites = () => spriteService.verifyAndRepairSprites();
|
|
3837
4120
|
|
|
3838
4121
|
var spriteService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -4911,7 +5194,15 @@ class RegionService {
|
|
|
4911
5194
|
deletedSprites++;
|
|
4912
5195
|
}
|
|
4913
5196
|
}
|
|
4914
|
-
|
|
5197
|
+
let deletedModels = 0;
|
|
5198
|
+
const modelTx = db.transaction('models', 'readwrite');
|
|
5199
|
+
for await (const cursor of modelTx.store) {
|
|
5200
|
+
if (resourceKeyBelongsToStyle(cursor.value.key, styleId)) {
|
|
5201
|
+
await cursor.delete();
|
|
5202
|
+
deletedModels++;
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5205
|
+
regionLogger$1.info(`Deleted style resources: ${deletedFonts} fonts, ${deletedGlyphs} glyphs, ${deletedSprites} sprites, ${deletedModels} models`);
|
|
4915
5206
|
}
|
|
4916
5207
|
/**
|
|
4917
5208
|
* Delete all tiles for a style
|
|
@@ -5019,7 +5310,7 @@ class RegionService {
|
|
|
5019
5310
|
if (!region.styleUrl) {
|
|
5020
5311
|
throw new Error('Region must have a styleUrl');
|
|
5021
5312
|
}
|
|
5022
|
-
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, glyphRanges, tileOptions, } = options;
|
|
5313
|
+
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, skipModels = false, glyphRanges, tileOptions, } = options;
|
|
5023
5314
|
const emit = (phase, completed, total, message) => {
|
|
5024
5315
|
if (!onProgress)
|
|
5025
5316
|
return;
|
|
@@ -5074,8 +5365,13 @@ class RegionService {
|
|
|
5074
5365
|
const spriteSources = normalizeSpriteProperty(originalSpriteUrl);
|
|
5075
5366
|
if (spriteSources.length > 0) {
|
|
5076
5367
|
const { downloadSprites } = await Promise.resolve().then(function () { return spriteService$1; });
|
|
5077
|
-
|
|
5078
|
-
|
|
5368
|
+
// Standard four sprite variants. For Mapbox Standard, an `iconset.pbf`
|
|
5369
|
+
// sibling is also served under the same /styles/v1/.../<hash>/ path
|
|
5370
|
+
// — we detect that case per-source below and append it to the list.
|
|
5371
|
+
const baseSuffixes = ['.json', '.png', '@2x.json', '@2x.png'];
|
|
5372
|
+
// Estimate total files assuming iconset is always included (the actual
|
|
5373
|
+
// number may be smaller; the emit helper clamps progress to total).
|
|
5374
|
+
const totalFiles = spriteSources.length * (baseSuffixes.length + 1);
|
|
5079
5375
|
let completed = 0;
|
|
5080
5376
|
emit('sprites', 0, totalFiles, 'Downloading sprites');
|
|
5081
5377
|
for (const source of spriteSources) {
|
|
@@ -5084,9 +5380,37 @@ class RegionService {
|
|
|
5084
5380
|
spriteBase = resolveMapboxUrl(spriteBase, effectiveAccessToken);
|
|
5085
5381
|
}
|
|
5086
5382
|
const qIndex = spriteBase.indexOf('?');
|
|
5087
|
-
const
|
|
5088
|
-
|
|
5089
|
-
|
|
5383
|
+
const suffixes = [...baseSuffixes];
|
|
5384
|
+
// Mapbox Standard serves an iconset.pbf alongside the sprite under
|
|
5385
|
+
// /styles/v1/{owner}/{style}/{hash}/sprite → the sibling file is
|
|
5386
|
+
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5387
|
+
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5388
|
+
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5389
|
+
let isMapboxStandardSprite = false;
|
|
5390
|
+
try {
|
|
5391
|
+
const parsed = new URL(pathWithoutQuery);
|
|
5392
|
+
isMapboxStandardSprite =
|
|
5393
|
+
parsed.hostname === 'api.mapbox.com' &&
|
|
5394
|
+
parsed.pathname.startsWith('/styles/v1/') &&
|
|
5395
|
+
parsed.pathname.endsWith('/sprite');
|
|
5396
|
+
}
|
|
5397
|
+
catch {
|
|
5398
|
+
// Non-URL sprite base (e.g. relative); not a Mapbox Standard sprite.
|
|
5399
|
+
}
|
|
5400
|
+
if (isMapboxStandardSprite) {
|
|
5401
|
+
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5402
|
+
suffixes.push('__ICONSET__');
|
|
5403
|
+
}
|
|
5404
|
+
const spriteUrls = suffixes.map(suffix => {
|
|
5405
|
+
if (suffix === '__ICONSET__') {
|
|
5406
|
+
// Replace trailing `sprite` with `iconset.pbf`, preserving query.
|
|
5407
|
+
const base = pathWithoutQuery.replace(/sprite$/, 'iconset.pbf');
|
|
5408
|
+
return qIndex !== -1 ? base + spriteBase.slice(qIndex) : base;
|
|
5409
|
+
}
|
|
5410
|
+
return qIndex !== -1
|
|
5411
|
+
? spriteBase.slice(0, qIndex) + suffix + spriteBase.slice(qIndex)
|
|
5412
|
+
: spriteBase + suffix;
|
|
5413
|
+
});
|
|
5090
5414
|
try {
|
|
5091
5415
|
const result = await downloadSprites(spriteUrls, styleId, {
|
|
5092
5416
|
enableValidation: true,
|
|
@@ -5132,7 +5456,42 @@ class RegionService {
|
|
|
5132
5456
|
}
|
|
5133
5457
|
}
|
|
5134
5458
|
}
|
|
5135
|
-
// 4.
|
|
5459
|
+
// 4. Models — Mapbox Standard's `style.models` references 3D tree /
|
|
5460
|
+
// turbine .glb assets. Two value shapes exist in the wild
|
|
5461
|
+
// (plain string or `{ uri }`) — we accept both.
|
|
5462
|
+
let modelResult;
|
|
5463
|
+
if (!skipModels && storedStyle.models) {
|
|
5464
|
+
const rawModels = storedStyle.models;
|
|
5465
|
+
const resolved = {};
|
|
5466
|
+
for (const [name, value] of Object.entries(rawModels)) {
|
|
5467
|
+
const uri = typeof value === 'string' ? value : value?.uri;
|
|
5468
|
+
if (!uri)
|
|
5469
|
+
continue;
|
|
5470
|
+
if (uri.startsWith('idb://'))
|
|
5471
|
+
continue; // already patched
|
|
5472
|
+
const httpUrl = isMapboxProtocol(uri) && effectiveAccessToken
|
|
5473
|
+
? resolveMapboxUrl(uri, effectiveAccessToken)
|
|
5474
|
+
: uri;
|
|
5475
|
+
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
|
|
5476
|
+
resolved[name] = httpUrl;
|
|
5477
|
+
}
|
|
5478
|
+
}
|
|
5479
|
+
if (Object.keys(resolved).length > 0) {
|
|
5480
|
+
const { downloadModels } = await Promise.resolve().then(function () { return modelService$1; });
|
|
5481
|
+
emit('models', 0, Object.keys(resolved).length, 'Downloading 3D models');
|
|
5482
|
+
try {
|
|
5483
|
+
modelResult = await downloadModels(resolved, styleId, {
|
|
5484
|
+
onProgress: (progress) => {
|
|
5485
|
+
emit('models', progress.completed, progress.total, 'Downloading 3D models');
|
|
5486
|
+
},
|
|
5487
|
+
});
|
|
5488
|
+
}
|
|
5489
|
+
catch (error) {
|
|
5490
|
+
regionLogger$1.warn('Model download failed (non-fatal):', error);
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
}
|
|
5494
|
+
// 5. Tiles — use the stored (source-embedded) style, which still has HTTP tile URLs
|
|
5136
5495
|
const { downloadTiles } = await Promise.resolve().then(function () { return tileService$1; });
|
|
5137
5496
|
const regionForTiles = { ...region, styleId };
|
|
5138
5497
|
emit('tiles', 0, 100, 'Downloading tiles');
|
|
@@ -5145,7 +5504,7 @@ class RegionService {
|
|
|
5145
5504
|
tileOptions?.onProgress?.(progress);
|
|
5146
5505
|
},
|
|
5147
5506
|
});
|
|
5148
|
-
//
|
|
5507
|
+
// 6. Metadata — must run last, since addRegion patches style URLs to idb://.
|
|
5149
5508
|
// Do NOT auto-fill tileExtension from tileResult: that's only the first
|
|
5150
5509
|
// source's extension, and addRegion feeds it to patchStyleForOffline which
|
|
5151
5510
|
// would override ALL sources — breaking mixed raster+vector styles. The
|
|
@@ -5161,6 +5520,7 @@ class RegionService {
|
|
|
5161
5520
|
styleResult,
|
|
5162
5521
|
spriteResults,
|
|
5163
5522
|
glyphResult,
|
|
5523
|
+
modelResult,
|
|
5164
5524
|
tileResult,
|
|
5165
5525
|
};
|
|
5166
5526
|
}
|
|
@@ -6299,10 +6659,15 @@ class TileService {
|
|
|
6299
6659
|
tilesLength: config.tiles ? config.tiles.length : 0,
|
|
6300
6660
|
url: config.url,
|
|
6301
6661
|
});
|
|
6302
|
-
// Handle tile-based sources (vector, raster, raster-dem, batched-model
|
|
6662
|
+
// Handle tile-based sources (vector, raster, raster-dem, batched-model,
|
|
6663
|
+
// raster-array). `raster-array` is used by Mapbox Standard for layers
|
|
6664
|
+
// like `mapbox-landmarks` (mapbox.mapbox-landmark-icons-v1) — the tiles
|
|
6665
|
+
// are fetched from the same /v4/ endpoint as other tilesets, so the
|
|
6666
|
+
// TileJSON resolution path below handles them uniformly.
|
|
6303
6667
|
if (config.type === 'vector' ||
|
|
6304
6668
|
config.type === 'raster' ||
|
|
6305
6669
|
config.type === 'raster-dem' ||
|
|
6670
|
+
config.type === 'raster-array' ||
|
|
6306
6671
|
config.type === 'batched-model') {
|
|
6307
6672
|
// Handle direct tile URLs in the source config
|
|
6308
6673
|
if (config.tiles && Array.isArray(config.tiles) && config.tiles.length > 0) {
|
|
@@ -6527,8 +6892,7 @@ class TileService {
|
|
|
6527
6892
|
}
|
|
6528
6893
|
}
|
|
6529
6894
|
extractExtension(template) {
|
|
6530
|
-
|
|
6531
|
-
return extMatch ? extMatch[1] : 'pbf';
|
|
6895
|
+
return extractTileExtensionFromUrl(template);
|
|
6532
6896
|
}
|
|
6533
6897
|
selectTileTemplate(templates, coord) {
|
|
6534
6898
|
if (templates.length === 1) {
|
|
@@ -6910,14 +7274,21 @@ class GlyphService {
|
|
|
6910
7274
|
},
|
|
6911
7275
|
};
|
|
6912
7276
|
}
|
|
6913
|
-
|
|
7277
|
+
/**
|
|
7278
|
+
* Remove glyphs older than the specified age. When `options.styleId` is
|
|
7279
|
+
* provided, only glyphs belonging to that style (per
|
|
7280
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
7281
|
+
*/
|
|
7282
|
+
async cleanupOldGlyphs(maxAge = 30, options = {}) {
|
|
6914
7283
|
const db = await this.db;
|
|
6915
7284
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7285
|
+
const { styleId } = options;
|
|
6916
7286
|
let deletedCount = 0;
|
|
6917
7287
|
const tx = db.transaction('glyphs', 'readwrite');
|
|
6918
7288
|
for await (const cursor of tx.store) {
|
|
6919
7289
|
const glyphEntry = cursor.value;
|
|
6920
|
-
|
|
7290
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(glyphEntry.key, styleId);
|
|
7291
|
+
if (belongs && glyphEntry.lastModified < cutoffTime) {
|
|
6921
7292
|
await cursor.delete();
|
|
6922
7293
|
deletedCount++;
|
|
6923
7294
|
}
|
|
@@ -7020,7 +7391,7 @@ const downloadGlyphs = (glyphUrl, fontstacks, styleName, ranges, options) => gly
|
|
|
7020
7391
|
const loadGlyphs = (fontstack, ranges, styleName) => glyphService.loadGlyphs(fontstack, ranges, styleName);
|
|
7021
7392
|
const getGlyphStats = () => glyphService.getGlyphStats();
|
|
7022
7393
|
const getGlyphAnalytics = () => glyphService.getGlyphAnalytics();
|
|
7023
|
-
const cleanupOldGlyphs = (maxAge) => glyphService.cleanupOldGlyphs(maxAge);
|
|
7394
|
+
const cleanupOldGlyphs = (maxAge, options) => glyphService.cleanupOldGlyphs(maxAge, options);
|
|
7024
7395
|
const verifyAndRepairGlyphs = () => glyphService.verifyAndRepairGlyphs();
|
|
7025
7396
|
|
|
7026
7397
|
var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -7035,12 +7406,207 @@ var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
|
7035
7406
|
verifyAndRepairGlyphs: verifyAndRepairGlyphs
|
|
7036
7407
|
});
|
|
7037
7408
|
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7409
|
+
const modelLogger = logger.scope('ModelService');
|
|
7410
|
+
/**
|
|
7411
|
+
* Build the storage key for a model. Kept consistent with sprite/glyph
|
|
7412
|
+
* conventions: `{styleId}::model::{modelName}`.
|
|
7413
|
+
*/
|
|
7414
|
+
function modelKey(styleId, modelName) {
|
|
7415
|
+
return `${styleId}::model::${modelName}`;
|
|
7416
|
+
}
|
|
7417
|
+
/** True when the given key belongs to the given styleId's model store prefix. */
|
|
7418
|
+
function modelKeyBelongsToStyle(key, styleId) {
|
|
7419
|
+
return key.startsWith(`${styleId}::model::`);
|
|
7420
|
+
}
|
|
7421
|
+
/**
|
|
7422
|
+
* Service for downloading, storing, and serving Mapbox 3D model (.glb) files.
|
|
7423
|
+
*
|
|
7424
|
+
* Mapbox Standard declares 32 models at the top of `style.models`:
|
|
7425
|
+
*
|
|
7426
|
+
* ```json
|
|
7427
|
+
* {
|
|
7428
|
+
* "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb",
|
|
7429
|
+
* ...
|
|
7430
|
+
* }
|
|
7431
|
+
* ```
|
|
7432
|
+
*
|
|
7433
|
+
* `model` layers (e.g. `trees`, `wind-turbine-towers`) pick one by name at
|
|
7434
|
+
* render time. For offline use each referenced URL is fetched and stored
|
|
7435
|
+
* here, and `patchStyleForOffline` rewrites the dictionary entries to
|
|
7436
|
+
* `idb://{styleId}/model/{name}` URLs.
|
|
7437
|
+
*/
|
|
7438
|
+
class ModelService {
|
|
7439
|
+
db = dbPromise;
|
|
7440
|
+
/**
|
|
7441
|
+
* Download the set of models referenced by `style.models` for one style.
|
|
7442
|
+
*
|
|
7443
|
+
* @param models `{ modelName: resolvedHttpUrl }` — URLs must already be
|
|
7444
|
+
* resolved (mapbox:// URLs should be resolved by the caller).
|
|
7445
|
+
* @param styleId The owning style's key.
|
|
7446
|
+
*/
|
|
7447
|
+
async downloadModels(models, styleId, options = {}) {
|
|
7448
|
+
const db = await this.db;
|
|
7449
|
+
const { onProgress, batchSize = 4, maxRetries = 3, skipExisting = true, timeoutMs = 30000, } = options;
|
|
7450
|
+
const entries = Object.entries(models);
|
|
7451
|
+
const progressTracker = createProgressTracker(entries.length);
|
|
7452
|
+
const result = {
|
|
7453
|
+
totalModels: entries.length,
|
|
7454
|
+
downloadedModels: 0,
|
|
7455
|
+
skippedModels: 0,
|
|
7456
|
+
failedModels: 0,
|
|
7457
|
+
totalSize: 0,
|
|
7458
|
+
errors: [],
|
|
7459
|
+
};
|
|
7460
|
+
const emit = () => onProgress?.(progressTracker.getProgress());
|
|
7461
|
+
emit();
|
|
7462
|
+
if (entries.length === 0)
|
|
7463
|
+
return result;
|
|
7464
|
+
// Pre-compute existing keys for skipExisting
|
|
7465
|
+
const existingKeys = new Set();
|
|
7466
|
+
if (skipExisting) {
|
|
7467
|
+
const tx = db.transaction('models', 'readonly');
|
|
7468
|
+
for await (const cursor of tx.store) {
|
|
7469
|
+
existingKeys.add(cursor.value.key);
|
|
7470
|
+
}
|
|
7471
|
+
}
|
|
7472
|
+
await processBatch(entries, async ([modelName, url]) => {
|
|
7473
|
+
const key = modelKey(styleId, modelName);
|
|
7474
|
+
const label = `${styleId}::${modelName}`;
|
|
7475
|
+
if (skipExisting && existingKeys.has(key)) {
|
|
7476
|
+
result.skippedModels++;
|
|
7477
|
+
progressTracker.update(1, label);
|
|
7478
|
+
emit();
|
|
7479
|
+
return;
|
|
7480
|
+
}
|
|
7481
|
+
try {
|
|
7482
|
+
const response = await fetchResourceWithRetry(url, {
|
|
7483
|
+
retries: maxRetries,
|
|
7484
|
+
timeout: timeoutMs,
|
|
7485
|
+
proxyType: 'tiles',
|
|
7486
|
+
});
|
|
7487
|
+
if (response.type === 'json') {
|
|
7488
|
+
throw new Error('Unexpected JSON response for model');
|
|
7489
|
+
}
|
|
7490
|
+
const data = response.data;
|
|
7491
|
+
const contentType = ('contentType' in response && response.contentType) || 'model/gltf-binary';
|
|
7492
|
+
const entry = {
|
|
7493
|
+
key,
|
|
7494
|
+
data,
|
|
7495
|
+
contentType,
|
|
7496
|
+
size: data.byteLength,
|
|
7497
|
+
url,
|
|
7498
|
+
styleId,
|
|
7499
|
+
modelName,
|
|
7500
|
+
lastModified: Date.now(),
|
|
7501
|
+
downloadedAt: new Date().toISOString(),
|
|
7502
|
+
expires: response.expires,
|
|
7503
|
+
};
|
|
7504
|
+
await db.put('models', entry);
|
|
7505
|
+
result.downloadedModels++;
|
|
7506
|
+
result.totalSize += data.byteLength;
|
|
7507
|
+
progressTracker.update(1, label);
|
|
7508
|
+
}
|
|
7509
|
+
catch (err) {
|
|
7510
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7511
|
+
result.failedModels++;
|
|
7512
|
+
result.errors.push({ url, error: message });
|
|
7513
|
+
modelLogger.warn(`Failed to download model "${modelName}" from ${url}:`, err);
|
|
7514
|
+
progressTracker.update(1, label, message);
|
|
7515
|
+
}
|
|
7516
|
+
emit();
|
|
7517
|
+
}, { batchSize });
|
|
7518
|
+
modelLogger.info(`Models downloaded for style ${styleId}: ${result.downloadedModels} new, ${result.skippedModels} skipped, ${result.failedModels} failed`);
|
|
7519
|
+
return result;
|
|
7042
7520
|
}
|
|
7043
|
-
|
|
7521
|
+
/** Retrieve a single model by `{styleId, modelName}`. */
|
|
7522
|
+
async getModel(styleId, modelName) {
|
|
7523
|
+
const db = await this.db;
|
|
7524
|
+
return db.get('models', modelKey(styleId, modelName));
|
|
7525
|
+
}
|
|
7526
|
+
/** Aggregate stats across all stored models. */
|
|
7527
|
+
async getModelStats() {
|
|
7528
|
+
const db = await this.db;
|
|
7529
|
+
const stats = {
|
|
7530
|
+
count: 0,
|
|
7531
|
+
totalSize: 0,
|
|
7532
|
+
averageSize: 0,
|
|
7533
|
+
models: [],
|
|
7534
|
+
modelsByStyle: {},
|
|
7535
|
+
};
|
|
7536
|
+
const tx = db.transaction('models', 'readonly');
|
|
7537
|
+
for await (const cursor of tx.store) {
|
|
7538
|
+
const m = cursor.value;
|
|
7539
|
+
stats.count++;
|
|
7540
|
+
stats.totalSize += m.size;
|
|
7541
|
+
stats.models.push({ name: m.modelName, size: m.size, lastModified: m.lastModified });
|
|
7542
|
+
stats.modelsByStyle[m.styleId] = (stats.modelsByStyle[m.styleId] ?? 0) + 1;
|
|
7543
|
+
}
|
|
7544
|
+
stats.averageSize = stats.count > 0 ? stats.totalSize / stats.count : 0;
|
|
7545
|
+
return stats;
|
|
7546
|
+
}
|
|
7547
|
+
/**
|
|
7548
|
+
* Delete models older than `maxAge` days. Defaults to 30.
|
|
7549
|
+
*/
|
|
7550
|
+
async cleanupOldModels(maxAge = 30) {
|
|
7551
|
+
const db = await this.db;
|
|
7552
|
+
const cutoff = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7553
|
+
let deleted = 0;
|
|
7554
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7555
|
+
for await (const cursor of tx.store) {
|
|
7556
|
+
if (cursor.value.lastModified < cutoff) {
|
|
7557
|
+
await cursor.delete();
|
|
7558
|
+
deleted++;
|
|
7559
|
+
}
|
|
7560
|
+
}
|
|
7561
|
+
return deleted;
|
|
7562
|
+
}
|
|
7563
|
+
/**
|
|
7564
|
+
* Basic integrity check: remove entries with empty/missing data.
|
|
7565
|
+
*/
|
|
7566
|
+
async verifyAndRepairModels() {
|
|
7567
|
+
const db = await this.db;
|
|
7568
|
+
let verified = 0;
|
|
7569
|
+
let removed = 0;
|
|
7570
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7571
|
+
for await (const cursor of tx.store) {
|
|
7572
|
+
const m = cursor.value;
|
|
7573
|
+
if (!m.data || m.data.byteLength === 0) {
|
|
7574
|
+
await cursor.delete();
|
|
7575
|
+
removed++;
|
|
7576
|
+
}
|
|
7577
|
+
else {
|
|
7578
|
+
verified++;
|
|
7579
|
+
}
|
|
7580
|
+
}
|
|
7581
|
+
return { verified, repaired: 0, removed };
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
// Singleton + convenience exports, matching other service modules.
|
|
7585
|
+
const modelService = new ModelService();
|
|
7586
|
+
const downloadModels = (models, styleId, options) => modelService.downloadModels(models, styleId, options);
|
|
7587
|
+
const getModel = (styleId, modelName) => modelService.getModel(styleId, modelName);
|
|
7588
|
+
const getModelStats = () => modelService.getModelStats();
|
|
7589
|
+
const cleanupOldModels = (maxAge) => modelService.cleanupOldModels(maxAge);
|
|
7590
|
+
const verifyAndRepairModels = () => modelService.verifyAndRepairModels();
|
|
7591
|
+
|
|
7592
|
+
var modelService$1 = /*#__PURE__*/Object.freeze({
|
|
7593
|
+
__proto__: null,
|
|
7594
|
+
ModelService: ModelService,
|
|
7595
|
+
cleanupOldModels: cleanupOldModels,
|
|
7596
|
+
downloadModels: downloadModels,
|
|
7597
|
+
getModel: getModel,
|
|
7598
|
+
getModelStats: getModelStats,
|
|
7599
|
+
modelKeyBelongsToStyle: modelKeyBelongsToStyle,
|
|
7600
|
+
modelService: modelService,
|
|
7601
|
+
verifyAndRepairModels: verifyAndRepairModels
|
|
7602
|
+
});
|
|
7603
|
+
|
|
7604
|
+
class ResourceService {
|
|
7605
|
+
// Tile Management Methods
|
|
7606
|
+
async downloadTilesWithOptions(region, style, styleId, options = {}) {
|
|
7607
|
+
return downloadTiles(region, style, styleId, options);
|
|
7608
|
+
}
|
|
7609
|
+
async getTileStats(styleId) {
|
|
7044
7610
|
return getTileStats(styleId);
|
|
7045
7611
|
}
|
|
7046
7612
|
async getTileAnalytics(styleId) {
|
|
@@ -7060,8 +7626,7 @@ class ResourceService {
|
|
|
7060
7626
|
return getFontAnalytics();
|
|
7061
7627
|
}
|
|
7062
7628
|
async cleanupOldFonts(styleId, options) {
|
|
7063
|
-
|
|
7064
|
-
return cleanupOldFonts(maxAge);
|
|
7629
|
+
return cleanupOldFonts(options?.maxAge, { styleId });
|
|
7065
7630
|
}
|
|
7066
7631
|
async verifyAndRepairFonts() {
|
|
7067
7632
|
return verifyAndRepairFonts();
|
|
@@ -7077,8 +7642,7 @@ class ResourceService {
|
|
|
7077
7642
|
return getSpriteAnalytics();
|
|
7078
7643
|
}
|
|
7079
7644
|
async cleanupOldSprites(styleId, options) {
|
|
7080
|
-
|
|
7081
|
-
return cleanupOldSprites(maxAge);
|
|
7645
|
+
return cleanupOldSprites(options?.maxAge, { styleId });
|
|
7082
7646
|
}
|
|
7083
7647
|
async verifyAndRepairSprites() {
|
|
7084
7648
|
return verifyAndRepairSprites();
|
|
@@ -7097,12 +7661,24 @@ class ResourceService {
|
|
|
7097
7661
|
return loadGlyphs(fontstack, ranges, styleId);
|
|
7098
7662
|
}
|
|
7099
7663
|
async cleanupOldGlyphs(styleId, options) {
|
|
7100
|
-
|
|
7101
|
-
return cleanupOldGlyphs(maxAge);
|
|
7664
|
+
return cleanupOldGlyphs(options?.maxAge, { styleId });
|
|
7102
7665
|
}
|
|
7103
7666
|
async verifyAndRepairGlyphs() {
|
|
7104
7667
|
return verifyAndRepairGlyphs();
|
|
7105
7668
|
}
|
|
7669
|
+
// 3D Model Management Methods
|
|
7670
|
+
async downloadModelsWithOptions(models, styleId, options = {}) {
|
|
7671
|
+
return downloadModels(models, styleId, options);
|
|
7672
|
+
}
|
|
7673
|
+
async getModelStats() {
|
|
7674
|
+
return getModelStats();
|
|
7675
|
+
}
|
|
7676
|
+
async cleanupOldModels(options) {
|
|
7677
|
+
return cleanupOldModels(options?.maxAge);
|
|
7678
|
+
}
|
|
7679
|
+
async verifyAndRepairModels() {
|
|
7680
|
+
return verifyAndRepairModels();
|
|
7681
|
+
}
|
|
7106
7682
|
}
|
|
7107
7683
|
|
|
7108
7684
|
// The underlying stats functions already iterate every entry in their
|
|
@@ -7200,6 +7776,88 @@ class AnalyticsService {
|
|
|
7200
7776
|
}
|
|
7201
7777
|
}
|
|
7202
7778
|
|
|
7779
|
+
/**
|
|
7780
|
+
* MBTiles uses TMS tile_row ordering; our storage uses XYZ y. Flip across
|
|
7781
|
+
* either direction with the same formula.
|
|
7782
|
+
*/
|
|
7783
|
+
function flipY(y, z) {
|
|
7784
|
+
return (1 << z) - 1 - y;
|
|
7785
|
+
}
|
|
7786
|
+
/** Vector tile formats that downstream consumers (QGIS, maplibre-native) expect gzipped. */
|
|
7787
|
+
const VECTOR_FORMATS = new Set(['pbf', 'mvt']);
|
|
7788
|
+
function hasGzipMagic(bytes) {
|
|
7789
|
+
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
7790
|
+
}
|
|
7791
|
+
async function drainReadable(readable) {
|
|
7792
|
+
const reader = readable.getReader();
|
|
7793
|
+
const chunks = [];
|
|
7794
|
+
let total = 0;
|
|
7795
|
+
while (true) {
|
|
7796
|
+
const { done, value } = await reader.read();
|
|
7797
|
+
if (done)
|
|
7798
|
+
break;
|
|
7799
|
+
if (value) {
|
|
7800
|
+
chunks.push(value);
|
|
7801
|
+
total += value.byteLength;
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
7804
|
+
const out = new Uint8Array(total);
|
|
7805
|
+
let offset = 0;
|
|
7806
|
+
for (const chunk of chunks) {
|
|
7807
|
+
out.set(chunk, offset);
|
|
7808
|
+
offset += chunk.byteLength;
|
|
7809
|
+
}
|
|
7810
|
+
return out;
|
|
7811
|
+
}
|
|
7812
|
+
async function transformBytes(bytes, transform) {
|
|
7813
|
+
const writer = transform.writable.getWriter();
|
|
7814
|
+
// Don't await — the read loop below drives the pipe and we only want
|
|
7815
|
+
// the final bytes, not back-pressure handling for a single chunk.
|
|
7816
|
+
void writer.write(bytes);
|
|
7817
|
+
void writer.close();
|
|
7818
|
+
return drainReadable(transform.readable);
|
|
7819
|
+
}
|
|
7820
|
+
async function gzipBytes(bytes) {
|
|
7821
|
+
return transformBytes(bytes, new CompressionStream('gzip'));
|
|
7822
|
+
}
|
|
7823
|
+
async function gunzipBytes(bytes) {
|
|
7824
|
+
return transformBytes(bytes, new DecompressionStream('gzip'));
|
|
7825
|
+
}
|
|
7826
|
+
/**
|
|
7827
|
+
* Build the MBTiles `json` metadata payload. For vector tiles this is
|
|
7828
|
+
* mandatory for tippecanoe/QGIS/maplibre-native to render — they read
|
|
7829
|
+
* `vector_layers` from here.
|
|
7830
|
+
*
|
|
7831
|
+
* `vector_layers` is inferred from the offline style's vector sources
|
|
7832
|
+
* (populated by the TileJSON expansion step in styleService). Multiple
|
|
7833
|
+
* vector sources are merged; duplicates de-duped by id, first wins.
|
|
7834
|
+
*/
|
|
7835
|
+
function buildVectorJsonMetadata(style, sourceIds) {
|
|
7836
|
+
if (!style || typeof style !== 'object')
|
|
7837
|
+
return null;
|
|
7838
|
+
const sources = style.sources;
|
|
7839
|
+
if (!sources)
|
|
7840
|
+
return null;
|
|
7841
|
+
const merged = [];
|
|
7842
|
+
const seen = new Set();
|
|
7843
|
+
for (const [id, src] of Object.entries(sources)) {
|
|
7844
|
+
if (sourceIds.size > 0 && !sourceIds.has(id))
|
|
7845
|
+
continue;
|
|
7846
|
+
const layers = src?.vector_layers;
|
|
7847
|
+
if (!Array.isArray(layers))
|
|
7848
|
+
continue;
|
|
7849
|
+
for (const layer of layers) {
|
|
7850
|
+
const layerId = typeof layer?.id === 'string' ? layer.id : null;
|
|
7851
|
+
if (!layerId || seen.has(layerId))
|
|
7852
|
+
continue;
|
|
7853
|
+
seen.add(layerId);
|
|
7854
|
+
merged.push(layer);
|
|
7855
|
+
}
|
|
7856
|
+
}
|
|
7857
|
+
if (merged.length === 0)
|
|
7858
|
+
return null;
|
|
7859
|
+
return JSON.stringify({ vector_layers: merged });
|
|
7860
|
+
}
|
|
7203
7861
|
const serviceLogger = logger.scope('ImportExportService');
|
|
7204
7862
|
class ImportExportService {
|
|
7205
7863
|
db = dbPromise;
|
|
@@ -7207,270 +7865,173 @@ class ImportExportService {
|
|
|
7207
7865
|
// No need for initialization since dbPromise is already available
|
|
7208
7866
|
}
|
|
7209
7867
|
/**
|
|
7210
|
-
* Export a
|
|
7868
|
+
* Export region as a real binary MBTiles SQLite file.
|
|
7869
|
+
*
|
|
7870
|
+
* Produces a v1.3-compliant MBTiles archive: `metadata` + `tiles` tables,
|
|
7871
|
+
* with `tile_row` flipped to TMS ordering. The resulting blob can be read
|
|
7872
|
+
* by tippecanoe, QGIS, maplibre-native, etc.
|
|
7211
7873
|
*/
|
|
7212
|
-
async
|
|
7874
|
+
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7213
7875
|
const onProgress = options.onProgress || (() => { });
|
|
7214
7876
|
try {
|
|
7215
7877
|
onProgress({
|
|
7216
7878
|
stage: 'preparing',
|
|
7217
7879
|
percentage: 0,
|
|
7218
|
-
message: 'Preparing export...',
|
|
7880
|
+
message: 'Preparing MBTiles export...',
|
|
7219
7881
|
});
|
|
7220
|
-
// Get region metadata
|
|
7221
7882
|
const region = await this.getRegionMetadata(regionId);
|
|
7222
7883
|
if (!region) {
|
|
7223
7884
|
throw new Error(`Region ${regionId} not found`);
|
|
7224
7885
|
}
|
|
7886
|
+
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7887
|
+
// Pick format: caller override → region.tileExtension → default pbf.
|
|
7888
|
+
// Drives both the metadata row and whether tile bytes get gzipped.
|
|
7889
|
+
const format = String(options.format || region.tileExtension || 'pbf').toLowerCase();
|
|
7890
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
7225
7891
|
onProgress({
|
|
7226
|
-
stage: '
|
|
7227
|
-
percentage:
|
|
7228
|
-
message: '
|
|
7892
|
+
stage: 'processing',
|
|
7893
|
+
percentage: 75,
|
|
7894
|
+
message: isVector ? 'Compressing vector tiles...' : 'Packing SQLite database...',
|
|
7229
7895
|
});
|
|
7230
|
-
|
|
7231
|
-
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
createdAt: region.created, // StoredRegion uses 'created' not 'createdAt'
|
|
7240
|
-
exportedAt: Date.now(),
|
|
7241
|
-
version: '1.0.0',
|
|
7242
|
-
format: 'json',
|
|
7243
|
-
},
|
|
7244
|
-
style: {},
|
|
7245
|
-
tiles: [],
|
|
7246
|
-
sprites: [],
|
|
7247
|
-
fonts: [],
|
|
7248
|
-
};
|
|
7249
|
-
// Export style if requested
|
|
7250
|
-
if (options.includeStyle !== false) {
|
|
7251
|
-
onProgress({
|
|
7252
|
-
stage: 'exporting',
|
|
7253
|
-
percentage: 20,
|
|
7254
|
-
message: 'Exporting style data...',
|
|
7255
|
-
});
|
|
7256
|
-
exportData.style = await this.exportStyle(regionId);
|
|
7896
|
+
// Gzip vector tiles. Idempotent: skip tiles already gzipped (downloaded
|
|
7897
|
+
// with their original gzip wrapper intact).
|
|
7898
|
+
const packedTiles = [];
|
|
7899
|
+
for (const tile of tiles) {
|
|
7900
|
+
const raw = tile.data instanceof ArrayBuffer
|
|
7901
|
+
? new Uint8Array(tile.data)
|
|
7902
|
+
: new Uint8Array(tile.data);
|
|
7903
|
+
const data = isVector && !hasGzipMagic(raw) ? await gzipBytes(raw) : raw;
|
|
7904
|
+
packedTiles.push({ z: tile.z, x: tile.x, y: tile.y, data });
|
|
7257
7905
|
}
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
|
|
7263
|
-
|
|
7906
|
+
onProgress({
|
|
7907
|
+
stage: 'processing',
|
|
7908
|
+
percentage: 85,
|
|
7909
|
+
message: 'Packing SQLite database...',
|
|
7910
|
+
});
|
|
7911
|
+
const SQL = await getSqlJs();
|
|
7912
|
+
const db = new SQL.Database();
|
|
7913
|
+
try {
|
|
7914
|
+
db.run(`
|
|
7915
|
+
CREATE TABLE metadata (name TEXT, value TEXT);
|
|
7916
|
+
CREATE TABLE tiles (
|
|
7917
|
+
zoom_level INTEGER NOT NULL,
|
|
7918
|
+
tile_column INTEGER NOT NULL,
|
|
7919
|
+
tile_row INTEGER NOT NULL,
|
|
7920
|
+
tile_data BLOB
|
|
7921
|
+
);
|
|
7922
|
+
CREATE UNIQUE INDEX tile_index ON tiles (zoom_level, tile_column, tile_row);
|
|
7923
|
+
CREATE UNIQUE INDEX name ON metadata (name);
|
|
7924
|
+
`);
|
|
7925
|
+
const [[west, south], [east, north]] = region.bounds;
|
|
7926
|
+
const centerLon = (west + east) / 2;
|
|
7927
|
+
const centerLat = (south + north) / 2;
|
|
7928
|
+
const centerZoom = Math.max(region.minZoom, Math.min(region.maxZoom, Math.round((region.minZoom + region.maxZoom) / 2)));
|
|
7929
|
+
const metadataRows = {
|
|
7930
|
+
name: region.name || region.id,
|
|
7931
|
+
// MBTiles 1.3 type: 'overlay' or 'baselayer'. Baselayer matches how
|
|
7932
|
+
// QGIS treats the dataset (full-coverage map rather than overlay).
|
|
7933
|
+
type: isVector ? 'baselayer' : 'overlay',
|
|
7934
|
+
version: '1.0',
|
|
7935
|
+
description: region.name || region.id,
|
|
7936
|
+
format,
|
|
7937
|
+
bounds: `${west},${south},${east},${north}`,
|
|
7938
|
+
center: `${centerLon},${centerLat},${centerZoom}`,
|
|
7939
|
+
minzoom: String(region.minZoom),
|
|
7940
|
+
maxzoom: String(region.maxZoom),
|
|
7941
|
+
};
|
|
7942
|
+
// For vector tiles, the `json` field with `vector_layers` is required
|
|
7943
|
+
// by the MBTiles 1.3 spec and by every vector tile consumer worth
|
|
7944
|
+
// opening the file in. Derive it from the offline style.
|
|
7945
|
+
if (isVector) {
|
|
7946
|
+
const style = await this.exportStyle(regionId);
|
|
7947
|
+
const sourceIds = new Set(tiles.map(t => t.sourceId).filter(Boolean));
|
|
7948
|
+
const json = buildVectorJsonMetadata(style.style ?? style, sourceIds);
|
|
7949
|
+
if (json)
|
|
7950
|
+
metadataRows.json = json;
|
|
7951
|
+
}
|
|
7952
|
+
for (const [k, v] of Object.entries(options.metadata || {})) {
|
|
7953
|
+
metadataRows[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
|
7954
|
+
}
|
|
7955
|
+
const insertMeta = db.prepare(`INSERT INTO metadata (name, value) VALUES (?, ?)`);
|
|
7956
|
+
try {
|
|
7957
|
+
for (const [name, value] of Object.entries(metadataRows)) {
|
|
7958
|
+
insertMeta.run([name, value]);
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
finally {
|
|
7962
|
+
insertMeta.free();
|
|
7963
|
+
}
|
|
7964
|
+
const insertTile = db.prepare(`INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data)
|
|
7965
|
+
VALUES (?, ?, ?, ?)`);
|
|
7966
|
+
try {
|
|
7967
|
+
db.run('BEGIN');
|
|
7968
|
+
for (const tile of packedTiles) {
|
|
7969
|
+
insertTile.run([tile.z, tile.x, flipY(tile.y, tile.z), tile.data]);
|
|
7970
|
+
}
|
|
7971
|
+
db.run('COMMIT');
|
|
7972
|
+
}
|
|
7973
|
+
finally {
|
|
7974
|
+
insertTile.free();
|
|
7975
|
+
}
|
|
7976
|
+
const binary = db.export();
|
|
7977
|
+
const blob = new Blob([binary.buffer], {
|
|
7978
|
+
type: 'application/x-sqlite3',
|
|
7264
7979
|
});
|
|
7265
|
-
exportData.tiles = await this.exportTiles(regionId, onProgress);
|
|
7266
|
-
}
|
|
7267
|
-
// Export sprites if requested
|
|
7268
|
-
if (options.includeSprites !== false) {
|
|
7269
7980
|
onProgress({
|
|
7270
|
-
stage: '
|
|
7271
|
-
percentage:
|
|
7272
|
-
message: '
|
|
7981
|
+
stage: 'complete',
|
|
7982
|
+
percentage: 100,
|
|
7983
|
+
message: 'MBTiles export complete!',
|
|
7273
7984
|
});
|
|
7274
|
-
|
|
7985
|
+
return {
|
|
7986
|
+
success: true,
|
|
7987
|
+
format: 'mbtiles',
|
|
7988
|
+
filename: `${region.name || region.id}.mbtiles`,
|
|
7989
|
+
blob,
|
|
7990
|
+
size: blob.size,
|
|
7991
|
+
statistics: {
|
|
7992
|
+
tilesExported: tiles.length,
|
|
7993
|
+
spritesExported: 0,
|
|
7994
|
+
fontsExported: 0,
|
|
7995
|
+
},
|
|
7996
|
+
};
|
|
7275
7997
|
}
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
onProgress({
|
|
7279
|
-
stage: 'exporting',
|
|
7280
|
-
percentage: 85,
|
|
7281
|
-
message: 'Exporting fonts...',
|
|
7282
|
-
});
|
|
7283
|
-
exportData.fonts = await this.exportFonts(regionId);
|
|
7998
|
+
finally {
|
|
7999
|
+
db.close();
|
|
7284
8000
|
}
|
|
7285
|
-
onProgress({
|
|
7286
|
-
stage: 'processing',
|
|
7287
|
-
percentage: 95,
|
|
7288
|
-
message: 'Creating export file...',
|
|
7289
|
-
});
|
|
7290
|
-
// Create JSON blob
|
|
7291
|
-
const jsonString = JSON.stringify(exportData, null, 2);
|
|
7292
|
-
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
7293
|
-
onProgress({
|
|
7294
|
-
stage: 'complete',
|
|
7295
|
-
percentage: 100,
|
|
7296
|
-
message: 'Export complete!',
|
|
7297
|
-
});
|
|
7298
|
-
return {
|
|
7299
|
-
success: true,
|
|
7300
|
-
format: 'json',
|
|
7301
|
-
filename: `${region.name || region.id}_export.json`,
|
|
7302
|
-
blob,
|
|
7303
|
-
size: blob.size,
|
|
7304
|
-
statistics: {
|
|
7305
|
-
tilesExported: exportData.tiles.length,
|
|
7306
|
-
spritesExported: exportData.sprites.length,
|
|
7307
|
-
fontsExported: exportData.fonts.length,
|
|
7308
|
-
},
|
|
7309
|
-
};
|
|
7310
8001
|
}
|
|
7311
8002
|
catch (error) {
|
|
7312
8003
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7313
|
-
throw new Error(`
|
|
8004
|
+
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7314
8005
|
}
|
|
7315
8006
|
}
|
|
7316
8007
|
/**
|
|
7317
|
-
*
|
|
8008
|
+
* Import region from a binary MBTiles (SQLite) file.
|
|
7318
8009
|
*/
|
|
7319
|
-
async
|
|
7320
|
-
const onProgress =
|
|
8010
|
+
async importRegion(importData) {
|
|
8011
|
+
const onProgress = importData.onProgress || (() => { });
|
|
7321
8012
|
try {
|
|
7322
8013
|
onProgress({
|
|
7323
8014
|
stage: 'preparing',
|
|
7324
8015
|
percentage: 0,
|
|
7325
|
-
message: '
|
|
8016
|
+
message: 'Reading file...',
|
|
7326
8017
|
});
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
// to create a proper PMTiles file format
|
|
7330
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7331
|
-
if (!region) {
|
|
7332
|
-
throw new Error(`Region ${regionId} not found`);
|
|
8018
|
+
if (importData.format !== 'mbtiles') {
|
|
8019
|
+
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7333
8020
|
}
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
const pmtilesData = {
|
|
7338
|
-
header: {
|
|
7339
|
-
version: 3,
|
|
7340
|
-
type: 'mvt',
|
|
7341
|
-
compression: options.compression || 'gzip',
|
|
7342
|
-
bounds: region.bounds,
|
|
7343
|
-
minZoom: region.minZoom,
|
|
7344
|
-
maxZoom: region.maxZoom,
|
|
7345
|
-
metadata: {
|
|
7346
|
-
name: region.name,
|
|
7347
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7348
|
-
...options.metadata,
|
|
7349
|
-
},
|
|
7350
|
-
},
|
|
7351
|
-
tiles: tiles,
|
|
7352
|
-
};
|
|
7353
|
-
// Convert to binary format (simplified)
|
|
7354
|
-
const jsonString = JSON.stringify(pmtilesData);
|
|
7355
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
7356
|
-
onProgress({
|
|
7357
|
-
stage: 'complete',
|
|
7358
|
-
percentage: 100,
|
|
7359
|
-
message: 'PMTiles export complete!',
|
|
7360
|
-
});
|
|
7361
|
-
return {
|
|
7362
|
-
success: true,
|
|
7363
|
-
format: 'pmtiles',
|
|
7364
|
-
filename: `${region.name || region.id}.pmtiles`,
|
|
7365
|
-
blob,
|
|
7366
|
-
size: blob.size,
|
|
7367
|
-
statistics: {
|
|
7368
|
-
tilesExported: tiles.length,
|
|
7369
|
-
spritesExported: 0,
|
|
7370
|
-
fontsExported: 0,
|
|
7371
|
-
},
|
|
7372
|
-
};
|
|
7373
|
-
}
|
|
7374
|
-
catch (error) {
|
|
7375
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7376
|
-
throw new Error(`PMTiles export failed: ${errorMessage}`);
|
|
7377
|
-
}
|
|
7378
|
-
}
|
|
7379
|
-
/**
|
|
7380
|
-
* Export region as MBTiles format
|
|
7381
|
-
*/
|
|
7382
|
-
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7383
|
-
const onProgress = options.onProgress || (() => { });
|
|
7384
|
-
try {
|
|
8021
|
+
const buffer = await this.readFileAsArrayBuffer(importData.file);
|
|
8022
|
+
onProgress({ stage: 'importing', percentage: 40, message: 'Parsing MBTiles...' });
|
|
8023
|
+
const regionData = await this.parseMBTiles(buffer);
|
|
7385
8024
|
onProgress({
|
|
7386
|
-
stage: '
|
|
7387
|
-
percentage:
|
|
7388
|
-
message:
|
|
8025
|
+
stage: 'importing',
|
|
8026
|
+
percentage: 70,
|
|
8027
|
+
message: `Importing ${regionData.tiles?.length ?? 0} tiles...`,
|
|
7389
8028
|
});
|
|
7390
|
-
|
|
7391
|
-
// In a real implementation, you would use SQLite/SQL.js
|
|
7392
|
-
// to create a proper MBTiles SQLite database
|
|
7393
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7394
|
-
if (!region) {
|
|
7395
|
-
throw new Error(`Region ${regionId} not found`);
|
|
7396
|
-
}
|
|
7397
|
-
// Get tiles data
|
|
7398
|
-
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7399
|
-
// Create MBTiles structure (simplified as JSON for now)
|
|
7400
|
-
const mbtilesData = {
|
|
7401
|
-
metadata: {
|
|
7402
|
-
name: region.name,
|
|
7403
|
-
type: 'overlay',
|
|
7404
|
-
version: '1.0',
|
|
7405
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7406
|
-
format: options.format || 'pbf',
|
|
7407
|
-
bounds: region.bounds.flat().join(','),
|
|
7408
|
-
minzoom: region.minZoom,
|
|
7409
|
-
maxzoom: region.maxZoom,
|
|
7410
|
-
...options.metadata,
|
|
7411
|
-
},
|
|
7412
|
-
tiles: tiles.map(tile => ({
|
|
7413
|
-
zoom_level: tile.z,
|
|
7414
|
-
tile_column: tile.x,
|
|
7415
|
-
tile_row: tile.y,
|
|
7416
|
-
tile_data: tile.data,
|
|
7417
|
-
})),
|
|
7418
|
-
};
|
|
7419
|
-
// Convert to binary format (simplified)
|
|
7420
|
-
const jsonString = JSON.stringify(mbtilesData);
|
|
7421
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
8029
|
+
const result = await this.importRegionData(regionData, importData);
|
|
7422
8030
|
onProgress({
|
|
7423
8031
|
stage: 'complete',
|
|
7424
8032
|
percentage: 100,
|
|
7425
|
-
message: '
|
|
8033
|
+
message: result.success ? 'Import complete!' : result.message,
|
|
7426
8034
|
});
|
|
7427
|
-
return {
|
|
7428
|
-
success: true,
|
|
7429
|
-
format: 'mbtiles',
|
|
7430
|
-
filename: `${region.name || region.id}.mbtiles`,
|
|
7431
|
-
blob,
|
|
7432
|
-
size: blob.size,
|
|
7433
|
-
statistics: {
|
|
7434
|
-
tilesExported: tiles.length,
|
|
7435
|
-
spritesExported: 0,
|
|
7436
|
-
fontsExported: 0,
|
|
7437
|
-
},
|
|
7438
|
-
};
|
|
7439
|
-
}
|
|
7440
|
-
catch (error) {
|
|
7441
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7442
|
-
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7443
|
-
}
|
|
7444
|
-
}
|
|
7445
|
-
/**
|
|
7446
|
-
* Import region from file
|
|
7447
|
-
*/
|
|
7448
|
-
async importRegion(importData) {
|
|
7449
|
-
try {
|
|
7450
|
-
let regionData;
|
|
7451
|
-
switch (importData.format) {
|
|
7452
|
-
case 'json': {
|
|
7453
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7454
|
-
regionData = JSON.parse(textContent);
|
|
7455
|
-
break;
|
|
7456
|
-
}
|
|
7457
|
-
case 'pmtiles': {
|
|
7458
|
-
// PMTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7459
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7460
|
-
regionData = await this.parsePMTiles(textContent);
|
|
7461
|
-
break;
|
|
7462
|
-
}
|
|
7463
|
-
case 'mbtiles': {
|
|
7464
|
-
// MBTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7465
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7466
|
-
regionData = await this.parseMBTiles(textContent);
|
|
7467
|
-
break;
|
|
7468
|
-
}
|
|
7469
|
-
default:
|
|
7470
|
-
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7471
|
-
}
|
|
7472
|
-
// Import the region data
|
|
7473
|
-
const result = await this.importRegionData(regionData, importData);
|
|
7474
8035
|
return result;
|
|
7475
8036
|
}
|
|
7476
8037
|
catch (error) {
|
|
@@ -7594,151 +8155,113 @@ class ImportExportService {
|
|
|
7594
8155
|
}
|
|
7595
8156
|
}
|
|
7596
8157
|
/**
|
|
7597
|
-
*
|
|
7598
|
-
*/
|
|
7599
|
-
async exportSprites(_regionId) {
|
|
7600
|
-
const db = await this.db;
|
|
7601
|
-
const transaction = db.transaction(['sprites'], 'readonly');
|
|
7602
|
-
const store = transaction.objectStore('sprites');
|
|
7603
|
-
const sprites = [];
|
|
7604
|
-
try {
|
|
7605
|
-
let cursor = await store.openCursor();
|
|
7606
|
-
while (cursor) {
|
|
7607
|
-
const sprite = cursor.value;
|
|
7608
|
-
// Include sprites that match the styleId, or all sprites if keys don't contain styleId
|
|
7609
|
-
// (sprite keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7610
|
-
sprites.push({
|
|
7611
|
-
url: sprite.url,
|
|
7612
|
-
data: sprite.data,
|
|
7613
|
-
type: sprite.url.endsWith('.json') ? 'json' : 'png',
|
|
7614
|
-
resolution: sprite.url.includes('@2x') ? '2x' : '1x',
|
|
7615
|
-
});
|
|
7616
|
-
cursor = await cursor.continue();
|
|
7617
|
-
}
|
|
7618
|
-
return sprites;
|
|
7619
|
-
}
|
|
7620
|
-
catch (error) {
|
|
7621
|
-
serviceLogger.error('Error exporting sprites:', error);
|
|
7622
|
-
return [];
|
|
7623
|
-
}
|
|
7624
|
-
}
|
|
7625
|
-
/**
|
|
7626
|
-
* Export fonts data
|
|
8158
|
+
* Read file content as ArrayBuffer (for the binary MBTiles file).
|
|
7627
8159
|
*/
|
|
7628
|
-
async
|
|
7629
|
-
const db = await this.db;
|
|
7630
|
-
const transaction = db.transaction(['fonts'], 'readonly');
|
|
7631
|
-
const store = transaction.objectStore('fonts');
|
|
7632
|
-
const fonts = [];
|
|
7633
|
-
try {
|
|
7634
|
-
let cursor = await store.openCursor();
|
|
7635
|
-
while (cursor) {
|
|
7636
|
-
const font = cursor.value;
|
|
7637
|
-
// Include fonts that match the styleId, or all fonts if keys don't contain styleId
|
|
7638
|
-
// (font keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7639
|
-
fonts.push({
|
|
7640
|
-
fontStack: font.key, // Use key as fontstack identifier
|
|
7641
|
-
range: '0-255', // Default range since FontEntry doesn't store this
|
|
7642
|
-
data: font.data,
|
|
7643
|
-
});
|
|
7644
|
-
cursor = await cursor.continue();
|
|
7645
|
-
}
|
|
7646
|
-
return fonts;
|
|
7647
|
-
}
|
|
7648
|
-
catch (error) {
|
|
7649
|
-
serviceLogger.error('Error exporting fonts:', error);
|
|
7650
|
-
return [];
|
|
7651
|
-
}
|
|
7652
|
-
}
|
|
7653
|
-
/**
|
|
7654
|
-
* Read file content as text (for JSON files)
|
|
7655
|
-
*/
|
|
7656
|
-
async readFileAsText(file) {
|
|
8160
|
+
async readFileAsArrayBuffer(file) {
|
|
7657
8161
|
return new Promise((resolve, reject) => {
|
|
7658
8162
|
const reader = new FileReader();
|
|
7659
8163
|
reader.onload = () => resolve(reader.result);
|
|
7660
8164
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
7661
|
-
reader.
|
|
8165
|
+
reader.readAsArrayBuffer(file);
|
|
7662
8166
|
});
|
|
7663
8167
|
}
|
|
7664
8168
|
/**
|
|
7665
|
-
* Parse
|
|
8169
|
+
* Parse a real binary MBTiles (SQLite) file into our import-data shape.
|
|
8170
|
+
* Un-flips the TMS tile_row back to XYZ y.
|
|
7666
8171
|
*/
|
|
7667
|
-
async
|
|
7668
|
-
|
|
7669
|
-
//
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
8172
|
+
async parseMBTiles(buffer) {
|
|
8173
|
+
const bytes = new Uint8Array(buffer);
|
|
8174
|
+
// SQLite header: "SQLite format 3\0" (16 bytes). Validate up front so
|
|
8175
|
+
// non-MBTiles files (e.g. a JSON renamed to .mbtiles) surface a clear
|
|
8176
|
+
// error instead of the opaque "file is not a database" from sql.js.
|
|
8177
|
+
if (bytes.byteLength < 16) {
|
|
8178
|
+
throw new Error('Not a valid MBTiles file: file is too small');
|
|
8179
|
+
}
|
|
8180
|
+
const magic = String.fromCharCode(...bytes.slice(0, 15));
|
|
8181
|
+
if (magic !== 'SQLite format 3') {
|
|
8182
|
+
throw new Error('Not a valid MBTiles file: missing SQLite header');
|
|
8183
|
+
}
|
|
8184
|
+
const SQL = await getSqlJs();
|
|
8185
|
+
const db = new SQL.Database(bytes);
|
|
8186
|
+
try {
|
|
8187
|
+
const tablesResult = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('metadata', 'tiles')");
|
|
8188
|
+
const tableNames = (tablesResult[0]?.values || []).map(r => r[0]);
|
|
8189
|
+
if (!tableNames.includes('metadata') || !tableNames.includes('tiles')) {
|
|
8190
|
+
throw new Error('Not a valid MBTiles file: missing required metadata/tiles tables');
|
|
8191
|
+
}
|
|
8192
|
+
const metadata = {};
|
|
8193
|
+
const metaStmt = db.prepare('SELECT name, value FROM metadata');
|
|
8194
|
+
try {
|
|
8195
|
+
while (metaStmt.step()) {
|
|
8196
|
+
const row = metaStmt.get();
|
|
8197
|
+
metadata[row[0]] = row[1];
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
finally {
|
|
8201
|
+
metaStmt.free();
|
|
8202
|
+
}
|
|
8203
|
+
const rawBounds = metadata.bounds ? metadata.bounds.split(',').map(Number) : [0, 0, 0, 0];
|
|
8204
|
+
const bounds = [
|
|
8205
|
+
isFinite(rawBounds[0]) ? rawBounds[0] : 0,
|
|
8206
|
+
isFinite(rawBounds[1]) ? rawBounds[1] : 0,
|
|
8207
|
+
isFinite(rawBounds[2]) ? rawBounds[2] : 0,
|
|
8208
|
+
isFinite(rawBounds[3]) ? rawBounds[3] : 0,
|
|
8209
|
+
];
|
|
8210
|
+
const format = (metadata.format || 'pbf');
|
|
8211
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
8212
|
+
const tiles = [];
|
|
8213
|
+
const tilesStmt = db.prepare('SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles');
|
|
8214
|
+
try {
|
|
8215
|
+
while (tilesStmt.step()) {
|
|
8216
|
+
const row = tilesStmt.get();
|
|
8217
|
+
const [z, x, tmsRow, data] = row;
|
|
8218
|
+
// Sliced copy so the buffer is detached from sql.js's heap.
|
|
8219
|
+
const copy = new Uint8Array(data.byteLength);
|
|
8220
|
+
copy.set(data);
|
|
8221
|
+
// Our IndexedDB stores vector tiles decompressed (tileService
|
|
8222
|
+
// inflates on download). MBTiles vector tiles are gzipped by
|
|
8223
|
+
// convention — un-gzip on the way in so the stored tile matches
|
|
8224
|
+
// what the fetch handler expects to serve.
|
|
8225
|
+
const storedBytes = isVector && hasGzipMagic(copy) ? await gunzipBytes(copy) : copy;
|
|
8226
|
+
tiles.push({
|
|
8227
|
+
z,
|
|
8228
|
+
x,
|
|
8229
|
+
y: flipY(tmsRow, z),
|
|
8230
|
+
data: storedBytes.buffer,
|
|
8231
|
+
format,
|
|
8232
|
+
sourceId: 'imported',
|
|
8233
|
+
});
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
finally {
|
|
8237
|
+
tilesStmt.free();
|
|
8238
|
+
}
|
|
8239
|
+
const minZoom = metadata.minzoom !== undefined ? Number(metadata.minzoom) : 0;
|
|
8240
|
+
const maxZoom = metadata.maxzoom !== undefined ? Number(metadata.maxzoom) : 14;
|
|
8241
|
+
return {
|
|
8242
|
+
metadata: {
|
|
8243
|
+
id: metadata.name || 'imported-region',
|
|
8244
|
+
name: metadata.name || 'Imported Region',
|
|
8245
|
+
description: metadata.description,
|
|
8246
|
+
bounds: [
|
|
8247
|
+
[bounds[0], bounds[1]],
|
|
8248
|
+
[bounds[2], bounds[3]],
|
|
8249
|
+
],
|
|
8250
|
+
minZoom,
|
|
8251
|
+
maxZoom,
|
|
8252
|
+
styleUrl: '',
|
|
8253
|
+
createdAt: Date.now(),
|
|
8254
|
+
exportedAt: Date.now(),
|
|
8255
|
+
version: '1.0.0',
|
|
8256
|
+
format: 'mbtiles',
|
|
8257
|
+
},
|
|
8258
|
+
style: {},
|
|
8259
|
+
tiles,
|
|
8260
|
+
};
|
|
8261
|
+
}
|
|
8262
|
+
finally {
|
|
8263
|
+
db.close();
|
|
8264
|
+
}
|
|
7742
8265
|
}
|
|
7743
8266
|
/**
|
|
7744
8267
|
* Import region data to database
|
|
@@ -7803,16 +8326,15 @@ class ImportExportService {
|
|
|
7803
8326
|
});
|
|
7804
8327
|
}
|
|
7805
8328
|
}
|
|
7806
|
-
// Import sprites and fonts similarly...
|
|
7807
8329
|
return {
|
|
7808
8330
|
success: true,
|
|
7809
8331
|
regionId,
|
|
7810
8332
|
message: 'Region imported successfully',
|
|
7811
8333
|
statistics: {
|
|
7812
8334
|
tilesImported: regionData.tiles?.length || 0,
|
|
7813
|
-
spritesImported:
|
|
7814
|
-
fontsImported:
|
|
7815
|
-
totalSize: 0,
|
|
8335
|
+
spritesImported: 0,
|
|
8336
|
+
fontsImported: 0,
|
|
8337
|
+
totalSize: 0,
|
|
7816
8338
|
},
|
|
7817
8339
|
};
|
|
7818
8340
|
}
|
|
@@ -7897,6 +8419,10 @@ const createResourceManagement = (services) => ({
|
|
|
7897
8419
|
loadGlyphsForStyle: (...args) => services.resourceService.loadGlyphsForStyle(...args),
|
|
7898
8420
|
cleanupOldGlyphs: (...args) => services.resourceService.cleanupOldGlyphs(...args),
|
|
7899
8421
|
verifyAndRepairGlyphs: (...args) => services.resourceService.verifyAndRepairGlyphs(...args),
|
|
8422
|
+
downloadModelsWithOptions: (...args) => services.resourceService.downloadModelsWithOptions(...args),
|
|
8423
|
+
getModelStats: (...args) => services.resourceService.getModelStats(...args),
|
|
8424
|
+
cleanupOldModels: (...args) => services.resourceService.cleanupOldModels(...args),
|
|
8425
|
+
verifyAndRepairModels: (...args) => services.resourceService.verifyAndRepairModels(...args),
|
|
7900
8426
|
});
|
|
7901
8427
|
|
|
7902
8428
|
const createAnalyticsManagement = (services, deps) => ({
|
|
@@ -8037,8 +8563,6 @@ const createMaintenanceManagement = (services, deps) => {
|
|
|
8037
8563
|
};
|
|
8038
8564
|
|
|
8039
8565
|
const createImportExportManagement = (services) => ({
|
|
8040
|
-
exportRegionAsJSON: async (regionId, options = {}) => services.importExportService.exportRegionAsJSON(regionId, options),
|
|
8041
|
-
exportRegionAsPMTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsPMTiles(regionId, options),
|
|
8042
8566
|
exportRegionAsMBTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsMBTiles(regionId, options),
|
|
8043
8567
|
importRegion: async (importData) => services.importExportService.importRegion(importData),
|
|
8044
8568
|
downloadExportedRegion: (exportResult) => {
|
|
@@ -8394,10 +8918,6 @@ const en = {
|
|
|
8394
8918
|
'styleSelection.title': 'Select Offline Style',
|
|
8395
8919
|
'styleSelection.message': 'Choose which offline style to load:',
|
|
8396
8920
|
'styleSelection.sources': 'sources',
|
|
8397
|
-
// Import/Export
|
|
8398
|
-
'importExport.title': 'Import/Export',
|
|
8399
|
-
'importExport.export': 'Export',
|
|
8400
|
-
'importExport.import': 'Import',
|
|
8401
8921
|
// Errors
|
|
8402
8922
|
'error.loadingContent': 'Error loading content',
|
|
8403
8923
|
'error.tryAgain': 'Please try again',
|
|
@@ -8422,41 +8942,30 @@ const en = {
|
|
|
8422
8942
|
'regionDetails.bounds': 'Bounds',
|
|
8423
8943
|
'regionDetails.zoomRange': 'Zoom Range',
|
|
8424
8944
|
'regionDetails.created': 'Created',
|
|
8425
|
-
//
|
|
8426
|
-
'
|
|
8427
|
-
'
|
|
8428
|
-
'
|
|
8429
|
-
'
|
|
8430
|
-
'
|
|
8431
|
-
'
|
|
8432
|
-
'
|
|
8433
|
-
'
|
|
8434
|
-
'
|
|
8435
|
-
'
|
|
8436
|
-
'
|
|
8437
|
-
'
|
|
8438
|
-
'
|
|
8439
|
-
'
|
|
8440
|
-
'
|
|
8441
|
-
'
|
|
8442
|
-
'
|
|
8443
|
-
'
|
|
8444
|
-
'
|
|
8445
|
-
'
|
|
8446
|
-
'
|
|
8447
|
-
'
|
|
8448
|
-
'
|
|
8449
|
-
'importExport.fileFormatsHint': 'Supports JSON, PMTiles, and MBTiles formats',
|
|
8450
|
-
'importExport.newRegionName': 'New Region Name (Optional)',
|
|
8451
|
-
'importExport.newRegionNamePlaceholder': 'Leave empty to use original name',
|
|
8452
|
-
'importExport.overwriteIfExists': 'Overwrite if region exists',
|
|
8453
|
-
'importExport.preparingImport': 'Preparing import...',
|
|
8454
|
-
'importExport.importComplete': 'Import complete!',
|
|
8455
|
-
'importExport.importFailed': 'Import failed. Please try again.',
|
|
8456
|
-
'importExport.formatGuide': 'Format Guide',
|
|
8457
|
-
'importExport.jsonDesc': 'Complete data, human-readable, best for development',
|
|
8458
|
-
'importExport.pmtilesDesc': 'Web-optimized, efficient serving, cloud-friendly',
|
|
8459
|
-
'importExport.mbtilesDesc': 'Industry standard, SQLite-based, cross-platform',
|
|
8945
|
+
// MBTiles Modal
|
|
8946
|
+
'mbtiles.title': 'MBTiles Import / Export',
|
|
8947
|
+
'mbtiles.regionInfo': 'Region Information',
|
|
8948
|
+
'mbtiles.id': 'ID',
|
|
8949
|
+
'mbtiles.name': 'Name',
|
|
8950
|
+
'mbtiles.unnamed': 'Unnamed',
|
|
8951
|
+
'mbtiles.zoom': 'Zoom',
|
|
8952
|
+
'mbtiles.created': 'Created',
|
|
8953
|
+
'mbtiles.exportTitle': 'Export as MBTiles',
|
|
8954
|
+
'mbtiles.exportHint': 'Package the tiles in this region into a standard SQLite MBTiles archive that opens in QGIS, tippecanoe, and other tools.',
|
|
8955
|
+
'mbtiles.exportButton': 'Download .mbtiles',
|
|
8956
|
+
'mbtiles.preparingExport': 'Preparing export...',
|
|
8957
|
+
'mbtiles.exportComplete': 'Export complete!',
|
|
8958
|
+
'mbtiles.exportFailed': 'Export failed. Please try again.',
|
|
8959
|
+
'mbtiles.importTitle': 'Import from MBTiles',
|
|
8960
|
+
'mbtiles.selectFile': 'Select an .mbtiles file',
|
|
8961
|
+
'mbtiles.fileHint': 'Only SQLite-format .mbtiles files are supported.',
|
|
8962
|
+
'mbtiles.newRegionName': 'New Region Name (optional)',
|
|
8963
|
+
'mbtiles.newRegionNamePlaceholder': 'Leave empty to use the name from the file',
|
|
8964
|
+
'mbtiles.overwriteIfExists': 'Overwrite if a region with the same id exists',
|
|
8965
|
+
'mbtiles.importButton': 'Import .mbtiles',
|
|
8966
|
+
'mbtiles.preparingImport': 'Preparing import...',
|
|
8967
|
+
'mbtiles.importComplete': 'Import complete!',
|
|
8968
|
+
'mbtiles.importFailed': 'Import failed. Please try again.',
|
|
8460
8969
|
// Active Downloads
|
|
8461
8970
|
'download.activeCount': 'Active Downloads ({{count}})',
|
|
8462
8971
|
// Panel Manager additional strings
|
|
@@ -8617,10 +9126,6 @@ const ar = {
|
|
|
8617
9126
|
'styleSelection.title': 'اختر نمط غير متصل',
|
|
8618
9127
|
'styleSelection.message': 'اختر النمط غير المتصل الذي تريد تحميله:',
|
|
8619
9128
|
'styleSelection.sources': 'مصادر',
|
|
8620
|
-
// Import/Export - استيراد/تصدير
|
|
8621
|
-
'importExport.title': 'استيراد/تصدير',
|
|
8622
|
-
'importExport.export': 'تصدير',
|
|
8623
|
-
'importExport.import': 'استيراد',
|
|
8624
9129
|
// Errors - الأخطاء
|
|
8625
9130
|
'error.loadingContent': 'خطأ في تحميل المحتوى',
|
|
8626
9131
|
'error.tryAgain': 'يرجى المحاولة مرة أخرى',
|
|
@@ -8645,41 +9150,30 @@ const ar = {
|
|
|
8645
9150
|
'regionDetails.bounds': 'الحدود',
|
|
8646
9151
|
'regionDetails.zoomRange': 'نطاق التكبير',
|
|
8647
9152
|
'regionDetails.created': 'تاريخ الإنشاء',
|
|
8648
|
-
//
|
|
8649
|
-
'
|
|
8650
|
-
'
|
|
8651
|
-
'
|
|
8652
|
-
'
|
|
8653
|
-
'
|
|
8654
|
-
'
|
|
8655
|
-
'
|
|
8656
|
-
'
|
|
8657
|
-
'
|
|
8658
|
-
'
|
|
8659
|
-
'
|
|
8660
|
-
'
|
|
8661
|
-
'
|
|
8662
|
-
'
|
|
8663
|
-
'
|
|
8664
|
-
'
|
|
8665
|
-
'
|
|
8666
|
-
'
|
|
8667
|
-
'
|
|
8668
|
-
'
|
|
8669
|
-
'
|
|
8670
|
-
'
|
|
8671
|
-
'
|
|
8672
|
-
'importExport.fileFormatsHint': 'يدعم تنسيقات JSON و PMTiles و MBTiles',
|
|
8673
|
-
'importExport.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
8674
|
-
'importExport.newRegionNamePlaceholder': 'اتركه فارغاً لاستخدام الاسم الأصلي',
|
|
8675
|
-
'importExport.overwriteIfExists': 'الكتابة فوق المنطقة الموجودة',
|
|
8676
|
-
'importExport.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
8677
|
-
'importExport.importComplete': 'اكتمل الاستيراد!',
|
|
8678
|
-
'importExport.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
8679
|
-
'importExport.formatGuide': 'دليل التنسيقات',
|
|
8680
|
-
'importExport.jsonDesc': 'بيانات كاملة، قابلة للقراءة، الأفضل للتطوير',
|
|
8681
|
-
'importExport.pmtilesDesc': 'محسن للويب، خدمة فعالة، متوافق مع السحابة',
|
|
8682
|
-
'importExport.mbtilesDesc': 'معيار الصناعة، قائم على SQLite، متعدد المنصات',
|
|
9153
|
+
// MBTiles Modal - نافذة MBTiles
|
|
9154
|
+
'mbtiles.title': 'استيراد / تصدير MBTiles',
|
|
9155
|
+
'mbtiles.regionInfo': 'معلومات المنطقة',
|
|
9156
|
+
'mbtiles.id': 'المعرف',
|
|
9157
|
+
'mbtiles.name': 'الاسم',
|
|
9158
|
+
'mbtiles.unnamed': 'بدون اسم',
|
|
9159
|
+
'mbtiles.zoom': 'التكبير',
|
|
9160
|
+
'mbtiles.created': 'تاريخ الإنشاء',
|
|
9161
|
+
'mbtiles.exportTitle': 'التصدير كـ MBTiles',
|
|
9162
|
+
'mbtiles.exportHint': 'احزم بلاطات هذه المنطقة داخل أرشيف MBTiles (SQLite) يمكن فتحه في QGIS و tippecanoe وأدوات أخرى.',
|
|
9163
|
+
'mbtiles.exportButton': 'تنزيل ملف mbtiles.',
|
|
9164
|
+
'mbtiles.preparingExport': 'جاري تجهيز التصدير...',
|
|
9165
|
+
'mbtiles.exportComplete': 'اكتمل التصدير!',
|
|
9166
|
+
'mbtiles.exportFailed': 'فشل التصدير. يرجى المحاولة مرة أخرى.',
|
|
9167
|
+
'mbtiles.importTitle': 'الاستيراد من MBTiles',
|
|
9168
|
+
'mbtiles.selectFile': 'اختر ملف mbtiles.',
|
|
9169
|
+
'mbtiles.fileHint': 'تُدعم ملفات mbtiles. بتنسيق SQLite فقط.',
|
|
9170
|
+
'mbtiles.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
9171
|
+
'mbtiles.newRegionNamePlaceholder': 'اتركه فارغًا لاستخدام الاسم من الملف',
|
|
9172
|
+
'mbtiles.overwriteIfExists': 'الكتابة فوق المنطقة إذا كان المعرف موجودًا',
|
|
9173
|
+
'mbtiles.importButton': 'استيراد ملف mbtiles.',
|
|
9174
|
+
'mbtiles.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
9175
|
+
'mbtiles.importComplete': 'اكتمل الاستيراد!',
|
|
9176
|
+
'mbtiles.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
8683
9177
|
// Active Downloads - التحميلات النشطة
|
|
8684
9178
|
'download.activeCount': 'التحميلات النشطة ({{count}})',
|
|
8685
9179
|
// Panel Manager additional strings - سلاسل إضافية للوحة
|
|
@@ -9618,49 +10112,42 @@ class ConfirmationModal {
|
|
|
9618
10112
|
}
|
|
9619
10113
|
|
|
9620
10114
|
/**
|
|
9621
|
-
* Import/Export Modal
|
|
9622
|
-
*
|
|
9623
|
-
*
|
|
10115
|
+
* MBTiles Import/Export Modal
|
|
10116
|
+
*
|
|
10117
|
+
* Focused modal for exchanging regions as binary SQLite MBTiles archives.
|
|
10118
|
+
* Replaces the previous multi-format import/export modal.
|
|
9624
10119
|
*/
|
|
9625
|
-
const modalLogger = logger.scope('
|
|
9626
|
-
class
|
|
10120
|
+
const modalLogger = logger.scope('MBTilesModal');
|
|
10121
|
+
class MBTilesModal {
|
|
9627
10122
|
modal;
|
|
9628
10123
|
options;
|
|
9629
10124
|
isExporting = false;
|
|
9630
10125
|
isImporting = false;
|
|
9631
|
-
// Form elements
|
|
9632
|
-
exportFormatSelect;
|
|
9633
|
-
includeStyleCheckbox;
|
|
9634
|
-
includeTilesCheckbox;
|
|
9635
|
-
includeSpritesCheckbox;
|
|
9636
|
-
includeFontsCheckbox;
|
|
9637
10126
|
exportProgressBar;
|
|
9638
10127
|
exportProgressText;
|
|
10128
|
+
exportProgressContainer;
|
|
9639
10129
|
exportButton;
|
|
9640
10130
|
importFileInput;
|
|
9641
10131
|
importNameInput;
|
|
9642
10132
|
importOverwriteCheckbox;
|
|
9643
10133
|
importProgressBar;
|
|
9644
10134
|
importProgressText;
|
|
10135
|
+
importProgressContainer;
|
|
9645
10136
|
importButton;
|
|
9646
10137
|
constructor(options) {
|
|
9647
10138
|
this.options = options;
|
|
9648
10139
|
}
|
|
9649
10140
|
show() {
|
|
9650
10141
|
const modalConfig = {
|
|
9651
|
-
title: t('
|
|
10142
|
+
title: t('mbtiles.title'),
|
|
9652
10143
|
subtitle: this.options.region.name || this.options.region.id,
|
|
9653
10144
|
size: 'md',
|
|
9654
10145
|
closable: true,
|
|
9655
10146
|
onClose: () => this.hide(),
|
|
9656
10147
|
};
|
|
9657
10148
|
this.modal = new Modal(modalConfig);
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
this.modal.setContent(content);
|
|
9661
|
-
// Create footer with close button
|
|
9662
|
-
const footer = this.createFooter();
|
|
9663
|
-
this.modal.setFooter(footer);
|
|
10149
|
+
this.modal.setContent(this.createContent());
|
|
10150
|
+
this.modal.setFooter(this.createFooter());
|
|
9664
10151
|
this.modal.show();
|
|
9665
10152
|
this.attachEventListeners();
|
|
9666
10153
|
return this.modal.getElement();
|
|
@@ -9669,217 +10156,96 @@ class ImportExportModal {
|
|
|
9669
10156
|
this.modal?.hide();
|
|
9670
10157
|
this.options.onClose();
|
|
9671
10158
|
}
|
|
10159
|
+
destroy() {
|
|
10160
|
+
this.modal?.destroy();
|
|
10161
|
+
}
|
|
9672
10162
|
createContent() {
|
|
9673
10163
|
const content = document.createElement('div');
|
|
9674
|
-
content.className = 'flex flex-col gap-6';
|
|
10164
|
+
content.className = 'flex flex-col gap-6 py-2';
|
|
9675
10165
|
if (i18n.isRTL()) {
|
|
9676
10166
|
content.setAttribute('dir', 'rtl');
|
|
9677
10167
|
}
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
content.appendChild(
|
|
9681
|
-
// Export/Import Grid
|
|
9682
|
-
const gridContainer = document.createElement('div');
|
|
9683
|
-
gridContainer.className = 'grid grid-cols-1 gap-6';
|
|
9684
|
-
// Export Section
|
|
9685
|
-
const exportSection = this.createExportSection();
|
|
9686
|
-
gridContainer.appendChild(exportSection);
|
|
9687
|
-
// Import Section
|
|
9688
|
-
const importSection = this.createImportSection();
|
|
9689
|
-
gridContainer.appendChild(importSection);
|
|
9690
|
-
content.appendChild(gridContainer);
|
|
9691
|
-
// Format Guide
|
|
9692
|
-
const formatGuide = this.createFormatGuide();
|
|
9693
|
-
content.appendChild(formatGuide);
|
|
10168
|
+
content.appendChild(this.createRegionInfoLine());
|
|
10169
|
+
content.appendChild(this.createExportSection());
|
|
10170
|
+
content.appendChild(this.createImportSection());
|
|
9694
10171
|
return content;
|
|
9695
10172
|
}
|
|
9696
|
-
|
|
9697
|
-
const
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
</div>
|
|
9709
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9710
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.name')}</span>
|
|
9711
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${escapeHtml$1(this.options.region.name || t('importExport.unnamed'))}</div>
|
|
9712
|
-
</div>
|
|
9713
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9714
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.zoom')}</span>
|
|
9715
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">Z${escapeHtml$1(this.options.region.minZoom)}-${escapeHtml$1(this.options.region.maxZoom)}</div>
|
|
9716
|
-
</div>
|
|
9717
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9718
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.created')}</span>
|
|
9719
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${new Date(this.options.region.created).toLocaleDateString()}</div>
|
|
9720
|
-
</div>
|
|
9721
|
-
</div>
|
|
10173
|
+
createRegionInfoLine() {
|
|
10174
|
+
const { region } = this.options;
|
|
10175
|
+
const line = document.createElement('div');
|
|
10176
|
+
line.className =
|
|
10177
|
+
'flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400';
|
|
10178
|
+
line.innerHTML = `
|
|
10179
|
+
<span class="flex items-center gap-1">
|
|
10180
|
+
${icons.mapPin({ size: 12, color: 'currentColor' })}
|
|
10181
|
+
<span class="font-mono">${escapeHtml$1(region.id)}</span>
|
|
10182
|
+
</span>
|
|
10183
|
+
<span>Z${escapeHtml$1(region.minZoom)}-${escapeHtml$1(region.maxZoom)}</span>
|
|
10184
|
+
<span>${new Date(region.created).toLocaleDateString()}</span>
|
|
9722
10185
|
`;
|
|
9723
|
-
return
|
|
10186
|
+
return line;
|
|
9724
10187
|
}
|
|
9725
10188
|
createExportSection() {
|
|
9726
|
-
const section =
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
|
|
9735
|
-
|
|
9736
|
-
|
|
9737
|
-
|
|
9738
|
-
|
|
9739
|
-
|
|
9740
|
-
|
|
9741
|
-
`;
|
|
9742
|
-
section.appendChild(header);
|
|
9743
|
-
const formContainer = document.createElement('div');
|
|
9744
|
-
formContainer.className = 'space-y-5';
|
|
9745
|
-
// Format Selection
|
|
9746
|
-
const formatGroup = document.createElement('div');
|
|
9747
|
-
const formatLabel = document.createElement('label');
|
|
9748
|
-
formatLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9749
|
-
formatLabel.textContent = t('importExport.exportFormat');
|
|
9750
|
-
this.exportFormatSelect = document.createElement('select');
|
|
9751
|
-
this.exportFormatSelect.className =
|
|
9752
|
-
'w-full px-4 py-3 rounded-xl text-sm glass-input text-gray-900 dark:text-white bg-white/50 dark:bg-gray-700/50 focus:outline-none focus:ring-2 focus:ring-blue-500/50 transition-all';
|
|
9753
|
-
this.exportFormatSelect.innerHTML = `
|
|
9754
|
-
<option value="json">${t('importExport.formatJson')}</option>
|
|
9755
|
-
<option value="pmtiles">${t('importExport.formatPmtiles')}</option>
|
|
9756
|
-
<option value="mbtiles">${t('importExport.formatMbtiles')}</option>
|
|
9757
|
-
`;
|
|
9758
|
-
const formatHint = document.createElement('p');
|
|
9759
|
-
formatHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
9760
|
-
formatHint.textContent = t('importExport.formatHint');
|
|
9761
|
-
formatGroup.appendChild(formatLabel);
|
|
9762
|
-
formatGroup.appendChild(this.exportFormatSelect);
|
|
9763
|
-
formatGroup.appendChild(formatHint);
|
|
9764
|
-
formContainer.appendChild(formatGroup);
|
|
9765
|
-
// Export Options
|
|
9766
|
-
const optionsGroup = document.createElement('div');
|
|
9767
|
-
const optionsLabel = document.createElement('label');
|
|
9768
|
-
optionsLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3';
|
|
9769
|
-
optionsLabel.textContent = t('importExport.includeComponents');
|
|
9770
|
-
const checkboxContainer = document.createElement('div');
|
|
9771
|
-
checkboxContainer.className = 'grid grid-cols-1 sm:grid-cols-2 gap-3';
|
|
9772
|
-
const createCheckbox = (text, checked = true) => {
|
|
9773
|
-
const label = document.createElement('label');
|
|
9774
|
-
label.className =
|
|
9775
|
-
'flex items-center gap-3 p-3 rounded-lg bg-gray-50/50 dark:bg-gray-800/50 cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer';
|
|
9776
|
-
const input = document.createElement('input');
|
|
9777
|
-
input.type = 'checkbox';
|
|
9778
|
-
input.checked = checked;
|
|
9779
|
-
input.className =
|
|
9780
|
-
'w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 focus:ring-2 dark:bg-gray-700';
|
|
9781
|
-
const span = document.createElement('span');
|
|
9782
|
-
span.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
9783
|
-
span.textContent = text;
|
|
9784
|
-
label.appendChild(input);
|
|
9785
|
-
label.appendChild(span);
|
|
9786
|
-
return { label, input };
|
|
9787
|
-
};
|
|
9788
|
-
const styleCheck = createCheckbox(t('importExport.styleConfig'));
|
|
9789
|
-
this.includeStyleCheckbox = styleCheck.input;
|
|
9790
|
-
checkboxContainer.appendChild(styleCheck.label);
|
|
9791
|
-
const tilesCheck = createCheckbox(t('importExport.mapTiles'));
|
|
9792
|
-
this.includeTilesCheckbox = tilesCheck.input;
|
|
9793
|
-
checkboxContainer.appendChild(tilesCheck.label);
|
|
9794
|
-
const spritesCheck = createCheckbox(t('importExport.spritesIcons'));
|
|
9795
|
-
this.includeSpritesCheckbox = spritesCheck.input;
|
|
9796
|
-
checkboxContainer.appendChild(spritesCheck.label);
|
|
9797
|
-
const fontsCheck = createCheckbox(t('importExport.fontsGlyphs'));
|
|
9798
|
-
this.includeFontsCheckbox = fontsCheck.input;
|
|
9799
|
-
checkboxContainer.appendChild(fontsCheck.label);
|
|
9800
|
-
optionsGroup.appendChild(optionsLabel);
|
|
9801
|
-
optionsGroup.appendChild(checkboxContainer);
|
|
9802
|
-
formContainer.appendChild(optionsGroup);
|
|
9803
|
-
// Export Progress (hidden by default)
|
|
9804
|
-
const progressContainer = document.createElement('div');
|
|
9805
|
-
progressContainer.className = 'hidden';
|
|
9806
|
-
const progressBarContainer = document.createElement('div');
|
|
9807
|
-
progressBarContainer.className =
|
|
9808
|
-
'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
9809
|
-
this.exportProgressBar = document.createElement('div');
|
|
9810
|
-
this.exportProgressBar.className = 'bg-blue-600 h-2 rounded-full transition-all duration-300';
|
|
9811
|
-
this.exportProgressBar.style.width = '0%';
|
|
9812
|
-
progressBarContainer.appendChild(this.exportProgressBar);
|
|
9813
|
-
this.exportProgressText = document.createElement('p');
|
|
9814
|
-
this.exportProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
9815
|
-
this.exportProgressText.textContent = t('importExport.preparingExport');
|
|
9816
|
-
progressContainer.appendChild(progressBarContainer);
|
|
9817
|
-
progressContainer.appendChild(this.exportProgressText);
|
|
9818
|
-
formContainer.appendChild(progressContainer);
|
|
9819
|
-
// Export Button
|
|
9820
|
-
const exportButton = new Button({
|
|
9821
|
-
text: t('importExport.exportRegion'),
|
|
10189
|
+
const section = this.createSection(t('mbtiles.exportTitle'), 'blue', icons.download({ size: 20, color: 'currentColor' }));
|
|
10190
|
+
const form = document.createElement('div');
|
|
10191
|
+
form.className = 'space-y-5';
|
|
10192
|
+
const hint = document.createElement('p');
|
|
10193
|
+
hint.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10194
|
+
hint.textContent = t('mbtiles.exportHint');
|
|
10195
|
+
form.appendChild(hint);
|
|
10196
|
+
// Progress (hidden by default)
|
|
10197
|
+
const progress = this.createProgressBlock('blue', t('mbtiles.preparingExport'));
|
|
10198
|
+
this.exportProgressContainer = progress.container;
|
|
10199
|
+
this.exportProgressBar = progress.bar;
|
|
10200
|
+
this.exportProgressText = progress.text;
|
|
10201
|
+
form.appendChild(progress.container);
|
|
10202
|
+
const exportBtn = new Button({
|
|
10203
|
+
text: t('mbtiles.exportButton'),
|
|
9822
10204
|
variant: 'primary',
|
|
9823
10205
|
icon: icons.download({ size: 16, color: 'white' }),
|
|
9824
|
-
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
10206
|
+
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
9825
10207
|
onClick: () => this.handleExport(),
|
|
9826
10208
|
});
|
|
9827
|
-
this.exportButton =
|
|
9828
|
-
|
|
9829
|
-
section.appendChild(
|
|
10209
|
+
this.exportButton = exportBtn.getElement();
|
|
10210
|
+
form.appendChild(this.exportButton);
|
|
10211
|
+
section.appendChild(form);
|
|
9830
10212
|
return section;
|
|
9831
10213
|
}
|
|
9832
10214
|
createImportSection() {
|
|
9833
|
-
const section =
|
|
9834
|
-
|
|
9835
|
-
|
|
9836
|
-
//
|
|
9837
|
-
const accent = document.createElement('div');
|
|
9838
|
-
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-green-500 opacity-50`;
|
|
9839
|
-
section.appendChild(accent);
|
|
9840
|
-
const header = document.createElement('h3');
|
|
9841
|
-
header.className =
|
|
9842
|
-
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
9843
|
-
header.innerHTML = `
|
|
9844
|
-
<div class="p-2 bg-green-500/10 rounded-lg text-green-600 dark:text-green-400">
|
|
9845
|
-
${icons.upload({ size: 20, color: 'currentColor' })}
|
|
9846
|
-
</div>
|
|
9847
|
-
${t('importExport.importRegion')}
|
|
9848
|
-
`;
|
|
9849
|
-
section.appendChild(header);
|
|
9850
|
-
const formContainer = document.createElement('div');
|
|
9851
|
-
formContainer.className = 'space-y-5';
|
|
9852
|
-
// File Selection
|
|
10215
|
+
const section = this.createSection(t('mbtiles.importTitle'), 'green', icons.upload({ size: 20, color: 'currentColor' }));
|
|
10216
|
+
const form = document.createElement('div');
|
|
10217
|
+
form.className = 'space-y-5';
|
|
10218
|
+
// File input
|
|
9853
10219
|
const fileGroup = document.createElement('div');
|
|
9854
10220
|
const fileLabel = document.createElement('label');
|
|
9855
10221
|
fileLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9856
|
-
fileLabel.textContent = t('
|
|
10222
|
+
fileLabel.textContent = t('mbtiles.selectFile');
|
|
9857
10223
|
this.importFileInput = document.createElement('input');
|
|
9858
10224
|
this.importFileInput.type = 'file';
|
|
9859
|
-
this.importFileInput.accept = '.
|
|
10225
|
+
this.importFileInput.accept = '.mbtiles,application/vnd.sqlite3,application/x-sqlite3';
|
|
9860
10226
|
this.importFileInput.className =
|
|
9861
10227
|
'w-full text-sm text-gray-500 file:mr-4 file:py-2.5 file:px-4 file:rounded-xl file:border-0 file:text-sm file:font-semibold file:bg-primary-50 file:text-primary-700 hover:file:bg-primary-100 dark:file:bg-primary-900/20 dark:file:text-primary-400 glass-input transition-all cursor-pointer';
|
|
9862
10228
|
const fileHint = document.createElement('p');
|
|
9863
10229
|
fileHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
9864
|
-
fileHint.textContent = t('
|
|
10230
|
+
fileHint.textContent = t('mbtiles.fileHint');
|
|
9865
10231
|
fileGroup.appendChild(fileLabel);
|
|
9866
10232
|
fileGroup.appendChild(this.importFileInput);
|
|
9867
10233
|
fileGroup.appendChild(fileHint);
|
|
9868
|
-
|
|
9869
|
-
// New
|
|
10234
|
+
form.appendChild(fileGroup);
|
|
10235
|
+
// New region name
|
|
9870
10236
|
const nameGroup = document.createElement('div');
|
|
9871
10237
|
const nameLabel = document.createElement('label');
|
|
9872
10238
|
nameLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9873
|
-
nameLabel.textContent = t('
|
|
10239
|
+
nameLabel.textContent = t('mbtiles.newRegionName');
|
|
9874
10240
|
this.importNameInput = document.createElement('input');
|
|
9875
10241
|
this.importNameInput.type = 'text';
|
|
9876
|
-
this.importNameInput.placeholder = t('
|
|
10242
|
+
this.importNameInput.placeholder = t('mbtiles.newRegionNamePlaceholder');
|
|
9877
10243
|
this.importNameInput.className =
|
|
9878
10244
|
'w-full px-4 py-3 rounded-xl text-sm glass-input text-gray-900 dark:text-white bg-white/50 dark:bg-gray-700/50 focus:outline-none focus:ring-2 focus:ring-green-500/50 transition-all';
|
|
9879
10245
|
nameGroup.appendChild(nameLabel);
|
|
9880
10246
|
nameGroup.appendChild(this.importNameInput);
|
|
9881
|
-
|
|
9882
|
-
//
|
|
10247
|
+
form.appendChild(nameGroup);
|
|
10248
|
+
// Overwrite toggle
|
|
9883
10249
|
const overwriteLabel = document.createElement('label');
|
|
9884
10250
|
overwriteLabel.className =
|
|
9885
10251
|
'flex items-center gap-3 p-3 rounded-lg bg-gray-50/50 dark:bg-gray-800/50 cursor-pointer hover:bg-gray-100/50 dark:hover:bg-gray-700/50 transition-colors';
|
|
@@ -9889,79 +10255,85 @@ class ImportExportModal {
|
|
|
9889
10255
|
'w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-green-600 focus:ring-green-500 focus:ring-2 dark:bg-gray-700';
|
|
9890
10256
|
const overwriteSpan = document.createElement('span');
|
|
9891
10257
|
overwriteSpan.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
9892
|
-
overwriteSpan.textContent = t('
|
|
10258
|
+
overwriteSpan.textContent = t('mbtiles.overwriteIfExists');
|
|
9893
10259
|
overwriteLabel.appendChild(this.importOverwriteCheckbox);
|
|
9894
10260
|
overwriteLabel.appendChild(overwriteSpan);
|
|
9895
|
-
|
|
9896
|
-
//
|
|
9897
|
-
const
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
this.importProgressText = document.createElement('p');
|
|
9907
|
-
this.importProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
9908
|
-
this.importProgressText.textContent = t('importExport.preparingImport');
|
|
9909
|
-
progressContainer.appendChild(progressBarContainer);
|
|
9910
|
-
progressContainer.appendChild(this.importProgressText);
|
|
9911
|
-
formContainer.appendChild(progressContainer);
|
|
9912
|
-
// Import Button
|
|
9913
|
-
const importButton = new Button({
|
|
9914
|
-
text: t('importExport.importRegion'),
|
|
9915
|
-
variant: 'success', // Assuming 'success' variant exists in Button component, if not might need style adjustment. Assuming it works based on previous code.
|
|
10261
|
+
form.appendChild(overwriteLabel);
|
|
10262
|
+
// Progress
|
|
10263
|
+
const progress = this.createProgressBlock('green', t('mbtiles.preparingImport'));
|
|
10264
|
+
this.importProgressContainer = progress.container;
|
|
10265
|
+
this.importProgressBar = progress.bar;
|
|
10266
|
+
this.importProgressText = progress.text;
|
|
10267
|
+
form.appendChild(progress.container);
|
|
10268
|
+
// Import button (disabled until a file is selected)
|
|
10269
|
+
const importBtn = new Button({
|
|
10270
|
+
text: t('mbtiles.importButton'),
|
|
10271
|
+
variant: 'success',
|
|
9916
10272
|
icon: icons.upload({ size: 16, color: 'white' }),
|
|
9917
10273
|
className: 'w-full py-2.5 text-base shadow-lg shadow-green-500/20',
|
|
9918
10274
|
disabled: true,
|
|
9919
10275
|
onClick: () => this.handleImport(),
|
|
9920
10276
|
});
|
|
9921
|
-
this.importButton =
|
|
9922
|
-
|
|
9923
|
-
section.appendChild(
|
|
10277
|
+
this.importButton = importBtn.getElement();
|
|
10278
|
+
form.appendChild(this.importButton);
|
|
10279
|
+
section.appendChild(form);
|
|
9924
10280
|
return section;
|
|
9925
10281
|
}
|
|
9926
|
-
|
|
9927
|
-
const
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
${
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
9940
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">PMTiles</div>
|
|
9941
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.pmtilesDesc')}</div>
|
|
9942
|
-
</div>
|
|
9943
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
9944
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">MBTiles</div>
|
|
9945
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.mbtilesDesc')}</div>
|
|
9946
|
-
</div>
|
|
10282
|
+
createSection(title, accentColor, iconHtml) {
|
|
10283
|
+
const section = document.createElement('div');
|
|
10284
|
+
section.className =
|
|
10285
|
+
'glass-input p-6 rounded-xl border-0 bg-white/40 dark:bg-gray-800/40 relative overflow-hidden';
|
|
10286
|
+
const accent = document.createElement('div');
|
|
10287
|
+
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-${accentColor}-500 opacity-50`;
|
|
10288
|
+
section.appendChild(accent);
|
|
10289
|
+
const header = document.createElement('h3');
|
|
10290
|
+
header.className =
|
|
10291
|
+
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
10292
|
+
header.innerHTML = `
|
|
10293
|
+
<div class="p-2 bg-${accentColor}-500/10 rounded-lg text-${accentColor}-600 dark:text-${accentColor}-400">
|
|
10294
|
+
${iconHtml}
|
|
9947
10295
|
</div>
|
|
10296
|
+
${title}
|
|
9948
10297
|
`;
|
|
9949
|
-
|
|
10298
|
+
section.appendChild(header);
|
|
10299
|
+
return section;
|
|
10300
|
+
}
|
|
10301
|
+
createProgressBlock(accentColor, initialText) {
|
|
10302
|
+
const container = document.createElement('div');
|
|
10303
|
+
container.className = 'hidden';
|
|
10304
|
+
const barWrap = document.createElement('div');
|
|
10305
|
+
barWrap.className = 'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
10306
|
+
const bar = document.createElement('div');
|
|
10307
|
+
bar.className = `bg-${accentColor}-600 h-2 rounded-full transition-all duration-300`;
|
|
10308
|
+
bar.style.width = '0%';
|
|
10309
|
+
barWrap.appendChild(bar);
|
|
10310
|
+
const text = document.createElement('p');
|
|
10311
|
+
text.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10312
|
+
text.textContent = initialText;
|
|
10313
|
+
container.appendChild(barWrap);
|
|
10314
|
+
container.appendChild(text);
|
|
10315
|
+
return { container, bar, text };
|
|
10316
|
+
}
|
|
10317
|
+
createFooter() {
|
|
10318
|
+
const footer = document.createElement('div');
|
|
10319
|
+
footer.className = 'flex gap-3 justify-end';
|
|
10320
|
+
if (i18n.isRTL()) {
|
|
10321
|
+
footer.setAttribute('dir', 'rtl');
|
|
10322
|
+
}
|
|
10323
|
+
const close = new Button({
|
|
10324
|
+
text: t('app.close'),
|
|
10325
|
+
variant: 'secondary',
|
|
10326
|
+
onClick: () => this.hide(),
|
|
10327
|
+
});
|
|
10328
|
+
footer.appendChild(close.getElement());
|
|
10329
|
+
return footer;
|
|
9950
10330
|
}
|
|
9951
10331
|
attachEventListeners() {
|
|
9952
|
-
// Enable import button when file is selected
|
|
9953
10332
|
if (this.importFileInput && this.importButton) {
|
|
9954
10333
|
this.importFileInput.addEventListener('change', () => {
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
}
|
|
9959
|
-
}
|
|
9960
|
-
else {
|
|
9961
|
-
if (this.importButton) {
|
|
9962
|
-
this.importButton.disabled = true;
|
|
9963
|
-
}
|
|
9964
|
-
}
|
|
10334
|
+
const hasFile = !!(this.importFileInput?.files && this.importFileInput.files.length > 0);
|
|
10335
|
+
if (this.importButton)
|
|
10336
|
+
this.importButton.disabled = !hasFile;
|
|
9965
10337
|
});
|
|
9966
10338
|
}
|
|
9967
10339
|
}
|
|
@@ -9971,34 +10343,27 @@ class ImportExportModal {
|
|
|
9971
10343
|
this.isExporting = true;
|
|
9972
10344
|
if (this.exportButton)
|
|
9973
10345
|
this.exportButton.disabled = true;
|
|
10346
|
+
this.exportProgressContainer?.classList.remove('hidden');
|
|
9974
10347
|
try {
|
|
9975
|
-
const
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
9984
|
-
if (progressContainer) {
|
|
9985
|
-
progressContainer.classList.remove('hidden');
|
|
9986
|
-
}
|
|
9987
|
-
const result = await this.options.exportRegion(this.options.region.id, format, options);
|
|
9988
|
-
if (this.exportProgressBar) {
|
|
10348
|
+
const result = await this.options.exportRegion(this.options.region.id, {
|
|
10349
|
+
onProgress: p => {
|
|
10350
|
+
if (this.exportProgressBar)
|
|
10351
|
+
this.exportProgressBar.style.width = `${p.percentage}%`;
|
|
10352
|
+
if (this.exportProgressText)
|
|
10353
|
+
this.exportProgressText.textContent = p.message;
|
|
10354
|
+
},
|
|
10355
|
+
});
|
|
10356
|
+
if (this.exportProgressBar)
|
|
9989
10357
|
this.exportProgressBar.style.width = '100%';
|
|
9990
|
-
|
|
9991
|
-
|
|
9992
|
-
this.exportProgressText.textContent = t('importExport.exportComplete');
|
|
9993
|
-
}
|
|
10358
|
+
if (this.exportProgressText)
|
|
10359
|
+
this.exportProgressText.textContent = t('mbtiles.exportComplete');
|
|
9994
10360
|
this.options.onExport?.(result);
|
|
9995
|
-
|
|
9996
|
-
setTimeout(() => this.hide(), 1500);
|
|
10361
|
+
setTimeout(() => this.hide(), 1200);
|
|
9997
10362
|
}
|
|
9998
10363
|
catch (error) {
|
|
9999
10364
|
modalLogger.error('Export error:', error instanceof Error ? error.message : String(error));
|
|
10000
10365
|
if (this.exportProgressText) {
|
|
10001
|
-
this.exportProgressText.textContent = t('
|
|
10366
|
+
this.exportProgressText.textContent = t('mbtiles.exportFailed');
|
|
10002
10367
|
this.exportProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10003
10368
|
}
|
|
10004
10369
|
}
|
|
@@ -10009,45 +10374,47 @@ class ImportExportModal {
|
|
|
10009
10374
|
}
|
|
10010
10375
|
}
|
|
10011
10376
|
async handleImport() {
|
|
10012
|
-
if (this.isImporting || !this.options.importRegion
|
|
10377
|
+
if (this.isImporting || !this.options.importRegion)
|
|
10378
|
+
return;
|
|
10379
|
+
const file = this.importFileInput?.files?.[0];
|
|
10380
|
+
if (!file)
|
|
10013
10381
|
return;
|
|
10014
10382
|
this.isImporting = true;
|
|
10015
10383
|
if (this.importButton)
|
|
10016
10384
|
this.importButton.disabled = true;
|
|
10385
|
+
this.importProgressContainer?.classList.remove('hidden');
|
|
10017
10386
|
try {
|
|
10018
|
-
const file = this.importFileInput.files[0];
|
|
10019
|
-
const overwrite = this.importOverwriteCheckbox?.checked ?? false;
|
|
10020
|
-
// Show progress
|
|
10021
|
-
const progressContainer = this.importProgressBar?.parentElement?.parentElement;
|
|
10022
|
-
if (progressContainer) {
|
|
10023
|
-
progressContainer.classList.remove('hidden');
|
|
10024
|
-
}
|
|
10025
|
-
// Determine format from file extension
|
|
10026
|
-
const format = file.name.endsWith('.pmtiles')
|
|
10027
|
-
? 'pmtiles'
|
|
10028
|
-
: file.name.endsWith('.mbtiles')
|
|
10029
|
-
? 'mbtiles'
|
|
10030
|
-
: 'json';
|
|
10031
10387
|
const data = {
|
|
10032
10388
|
file,
|
|
10033
|
-
format,
|
|
10034
|
-
overwrite,
|
|
10389
|
+
format: 'mbtiles',
|
|
10390
|
+
overwrite: this.importOverwriteCheckbox?.checked ?? false,
|
|
10391
|
+
newRegionName: this.importNameInput?.value.trim() || undefined,
|
|
10392
|
+
onProgress: p => {
|
|
10393
|
+
if (this.importProgressBar)
|
|
10394
|
+
this.importProgressBar.style.width = `${p.percentage}%`;
|
|
10395
|
+
if (this.importProgressText)
|
|
10396
|
+
this.importProgressText.textContent = p.message;
|
|
10397
|
+
},
|
|
10035
10398
|
};
|
|
10036
10399
|
const result = await this.options.importRegion(data);
|
|
10037
|
-
if (this.importProgressBar)
|
|
10400
|
+
if (this.importProgressBar)
|
|
10038
10401
|
this.importProgressBar.style.width = '100%';
|
|
10039
|
-
}
|
|
10040
10402
|
if (this.importProgressText) {
|
|
10041
|
-
this.importProgressText.textContent =
|
|
10403
|
+
this.importProgressText.textContent = result.success
|
|
10404
|
+
? t('mbtiles.importComplete')
|
|
10405
|
+
: result.message;
|
|
10406
|
+
if (!result.success) {
|
|
10407
|
+
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10408
|
+
}
|
|
10042
10409
|
}
|
|
10043
10410
|
this.options.onImport?.(result);
|
|
10044
|
-
|
|
10045
|
-
|
|
10411
|
+
if (result.success)
|
|
10412
|
+
setTimeout(() => this.hide(), 1200);
|
|
10046
10413
|
}
|
|
10047
10414
|
catch (error) {
|
|
10048
10415
|
modalLogger.error('Import error:', error instanceof Error ? error.message : String(error));
|
|
10049
10416
|
if (this.importProgressText) {
|
|
10050
|
-
this.importProgressText.textContent = t('
|
|
10417
|
+
this.importProgressText.textContent = t('mbtiles.importFailed');
|
|
10051
10418
|
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10052
10419
|
}
|
|
10053
10420
|
}
|
|
@@ -10057,23 +10424,6 @@ class ImportExportModal {
|
|
|
10057
10424
|
this.importButton.disabled = false;
|
|
10058
10425
|
}
|
|
10059
10426
|
}
|
|
10060
|
-
createFooter() {
|
|
10061
|
-
const footer = document.createElement('div');
|
|
10062
|
-
footer.className = 'flex gap-3 justify-end';
|
|
10063
|
-
if (i18n.isRTL()) {
|
|
10064
|
-
footer.setAttribute('dir', 'rtl');
|
|
10065
|
-
}
|
|
10066
|
-
const closeButton = new Button({
|
|
10067
|
-
text: t('app.close'),
|
|
10068
|
-
variant: 'secondary',
|
|
10069
|
-
onClick: () => this.hide(),
|
|
10070
|
-
});
|
|
10071
|
-
footer.appendChild(closeButton.getElement());
|
|
10072
|
-
return footer;
|
|
10073
|
-
}
|
|
10074
|
-
destroy() {
|
|
10075
|
-
this.modal?.destroy();
|
|
10076
|
-
}
|
|
10077
10427
|
}
|
|
10078
10428
|
|
|
10079
10429
|
/**
|
|
@@ -10787,9 +11137,9 @@ class PanelRenderer extends BaseComponent {
|
|
|
10787
11137
|
<button class="region-action-btn p-1.5 rounded-md cursor-pointer hover:bg-blue-100 dark:hover:bg-blue-900/50 text-blue-600 dark:text-blue-400 transition-colors duration-150" data-action="redownload-region" data-region-id="${escapeHtml$1(region.id)}" title="${t('actions.redownload')}">
|
|
10788
11138
|
${icons.download({ size: 14, color: 'currentColor' })}
|
|
10789
11139
|
</button>
|
|
10790
|
-
|
|
11140
|
+
<button class="region-action-btn p-1.5 rounded-md cursor-pointer hover:bg-purple-100 dark:hover:bg-purple-900/50 text-purple-600 dark:text-purple-400 transition-colors duration-150" data-action="import-export" data-region-id="${escapeHtml$1(region.id)}" title="${t('actions.importExport')}">
|
|
10791
11141
|
${icons.deviceFloppy({ size: 14, color: 'currentColor' })}
|
|
10792
|
-
</button>
|
|
11142
|
+
</button>
|
|
10793
11143
|
<button class="region-action-btn p-1.5 rounded-md cursor-pointer hover:bg-red-100 dark:hover:bg-red-900/50 text-red-600 dark:text-red-400 transition-colors duration-150" data-action="delete-region" data-region-id="${escapeHtml$1(region.id)}" title="${t('app.delete')}">
|
|
10794
11144
|
${icons.trash({ size: 14, color: 'currentColor' })}
|
|
10795
11145
|
</button>
|
|
@@ -11051,7 +11401,7 @@ class PanelRenderer extends BaseComponent {
|
|
|
11051
11401
|
}
|
|
11052
11402
|
}
|
|
11053
11403
|
/**
|
|
11054
|
-
*
|
|
11404
|
+
* Show the MBTiles import/export modal for a region.
|
|
11055
11405
|
*/
|
|
11056
11406
|
async handleImportExport(regionId, _regionData) {
|
|
11057
11407
|
try {
|
|
@@ -11059,44 +11409,26 @@ class PanelRenderer extends BaseComponent {
|
|
|
11059
11409
|
const region = regions.find((r) => r.id === regionId);
|
|
11060
11410
|
if (!region)
|
|
11061
11411
|
return;
|
|
11062
|
-
const
|
|
11412
|
+
const mbtilesModal = new MBTilesModal({
|
|
11063
11413
|
region,
|
|
11064
|
-
onClose: () =>
|
|
11065
|
-
this.modalManager.close();
|
|
11066
|
-
},
|
|
11414
|
+
onClose: () => this.modalManager.close(),
|
|
11067
11415
|
onExport: result => {
|
|
11068
|
-
panelLogger.debug('
|
|
11069
|
-
// Handle export result - could show success message
|
|
11416
|
+
panelLogger.debug('MBTiles export completed:', result);
|
|
11070
11417
|
this.offlineManager.downloadExportedRegion(result);
|
|
11071
11418
|
},
|
|
11072
11419
|
onImport: result => {
|
|
11073
|
-
panelLogger.debug('
|
|
11074
|
-
|
|
11075
|
-
|
|
11076
|
-
},
|
|
11077
|
-
exportRegion: async (regionId, format, options) => {
|
|
11078
|
-
// Delegate to offline manager's export functionality
|
|
11079
|
-
switch (format) {
|
|
11080
|
-
case 'json':
|
|
11081
|
-
return await this.offlineManager.exportRegionAsJSON(regionId, options);
|
|
11082
|
-
case 'pmtiles':
|
|
11083
|
-
return await this.offlineManager.exportRegionAsPMTiles(regionId, options);
|
|
11084
|
-
case 'mbtiles':
|
|
11085
|
-
return await this.offlineManager.exportRegionAsMBTiles(regionId, options);
|
|
11086
|
-
default:
|
|
11087
|
-
throw new Error(`Unsupported export format: ${format}`);
|
|
11088
|
-
}
|
|
11089
|
-
},
|
|
11090
|
-
importRegion: async (data) => {
|
|
11091
|
-
// Delegate to offline manager's import functionality
|
|
11092
|
-
return await this.offlineManager.importRegion(data);
|
|
11420
|
+
panelLogger.debug('MBTiles import completed:', result);
|
|
11421
|
+
if (result.success)
|
|
11422
|
+
this.refresh();
|
|
11093
11423
|
},
|
|
11424
|
+
exportRegion: (id, options) => this.offlineManager.exportRegionAsMBTiles(id, options),
|
|
11425
|
+
importRegion: data => this.offlineManager.importRegion(data),
|
|
11094
11426
|
});
|
|
11095
|
-
const modal =
|
|
11427
|
+
const modal = mbtilesModal.show();
|
|
11096
11428
|
this.modalManager.show(modal);
|
|
11097
11429
|
}
|
|
11098
11430
|
catch (error) {
|
|
11099
|
-
panelLogger.error('Error showing
|
|
11431
|
+
panelLogger.error('Error showing MBTiles modal:', error);
|
|
11100
11432
|
}
|
|
11101
11433
|
}
|
|
11102
11434
|
/**
|
|
@@ -11495,6 +11827,9 @@ class PanelRenderer extends BaseComponent {
|
|
|
11495
11827
|
delete patchedStyle.imports;
|
|
11496
11828
|
panelLogger.debug('Stripped imports from offline style (already flattened)');
|
|
11497
11829
|
}
|
|
11830
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
11831
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
11832
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
11498
11833
|
// Enforce maxzoom for all tile sources to prevent requesting non-existent tiles
|
|
11499
11834
|
// Find the maximum zoom level from all regions using this style
|
|
11500
11835
|
let maxZoom = 14; // Default fallback
|
|
@@ -11543,10 +11878,12 @@ class PanelRenderer extends BaseComponent {
|
|
|
11543
11878
|
panelLogger.debug(`Fixed legacy source ${sourceId}: added tiles, removed url`);
|
|
11544
11879
|
}
|
|
11545
11880
|
}
|
|
11546
|
-
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11881
|
+
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11882
|
+
// buildings and raster-array for Mapbox Standard landmark icons).
|
|
11547
11883
|
if (src.type === 'vector' ||
|
|
11548
11884
|
src.type === 'raster' ||
|
|
11549
11885
|
src.type === 'raster-dem' ||
|
|
11886
|
+
src.type === 'raster-array' ||
|
|
11550
11887
|
src.type === 'batched-model') {
|
|
11551
11888
|
const originalMaxzoom = src.maxzoom;
|
|
11552
11889
|
// Use the lower of region maxZoom and source's original maxzoom so we
|
|
@@ -12234,9 +12571,7 @@ class RegionFormModal {
|
|
|
12234
12571
|
detectProviderFromUrl() {
|
|
12235
12572
|
const styleUrl = this.styleUrlInput?.value || '';
|
|
12236
12573
|
// Simple detection logic
|
|
12237
|
-
if (styleUrl.startsWith('mapbox://') ||
|
|
12238
|
-
styleUrl.includes('mapbox.com') ||
|
|
12239
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
12574
|
+
if (styleUrl.startsWith('mapbox://') || isMapboxHost(styleUrl)) {
|
|
12240
12575
|
if (this.providerSelect)
|
|
12241
12576
|
this.providerSelect.value = 'mapbox';
|
|
12242
12577
|
this.toggleAccessTokenVisibility(true);
|
|
@@ -13000,39 +13335,39 @@ class OfflineManagerControl {
|
|
|
13000
13335
|
}
|
|
13001
13336
|
// Development proxy for CORS issues (when running on localhost)
|
|
13002
13337
|
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
if (isTileRequest && url.includes('tiles-a.basemaps.cartocdn.com')) {
|
|
13007
|
-
const proxyUrl = url.replace('https://tiles-a.basemaps.cartocdn.com', '/tiles/carto-a');
|
|
13008
|
-
return originalFetch(proxyUrl, init);
|
|
13009
|
-
}
|
|
13010
|
-
if (isTileRequest && url.includes('tiles-b.basemaps.cartocdn.com')) {
|
|
13011
|
-
const proxyUrl = url.replace('https://tiles-b.basemaps.cartocdn.com', '/tiles/carto-b');
|
|
13012
|
-
return originalFetch(proxyUrl, init);
|
|
13338
|
+
let parsed = null;
|
|
13339
|
+
try {
|
|
13340
|
+
parsed = new URL(url, location.origin);
|
|
13013
13341
|
}
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
return originalFetch(proxyUrl, init);
|
|
13342
|
+
catch {
|
|
13343
|
+
parsed = null;
|
|
13017
13344
|
}
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13345
|
+
const hostname = parsed?.hostname ?? '';
|
|
13346
|
+
const pathAndQuery = parsed ? parsed.pathname + parsed.search : '';
|
|
13347
|
+
// Proxy Carto tile requests (tiles and TileJSON)
|
|
13348
|
+
const isTileRequest = /\/\d+\/\d+\/\d+\.(pbf|mvt|png|jpg|jpeg|webp)/.test(parsed?.pathname ?? '');
|
|
13349
|
+
const isTileJsonRequest = (parsed?.pathname.endsWith('.json') ?? false) &&
|
|
13350
|
+
(hostname === 'basemaps.cartocdn.com' || hostname.endsWith('.basemaps.cartocdn.com'));
|
|
13351
|
+
const cartoSubdomainProxy = {
|
|
13352
|
+
'tiles-a.basemaps.cartocdn.com': '/tiles/carto-a',
|
|
13353
|
+
'tiles-b.basemaps.cartocdn.com': '/tiles/carto-b',
|
|
13354
|
+
'tiles-c.basemaps.cartocdn.com': '/tiles/carto-c',
|
|
13355
|
+
'tiles-d.basemaps.cartocdn.com': '/tiles/carto-d',
|
|
13356
|
+
};
|
|
13357
|
+
if (isTileRequest && cartoSubdomainProxy[hostname]) {
|
|
13358
|
+
return originalFetch(cartoSubdomainProxy[hostname] + pathAndQuery, init);
|
|
13021
13359
|
}
|
|
13022
13360
|
// Proxy TileJSON requests from tiles.basemaps.cartocdn.com
|
|
13023
|
-
if (isTileJsonRequest &&
|
|
13024
|
-
|
|
13025
|
-
return originalFetch(proxyUrl, init);
|
|
13361
|
+
if (isTileJsonRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13362
|
+
return originalFetch('/carto-api' + pathAndQuery, init);
|
|
13026
13363
|
}
|
|
13027
13364
|
// Fallback for old format (tiles without subdomain)
|
|
13028
|
-
if (isTileRequest &&
|
|
13029
|
-
|
|
13030
|
-
return originalFetch(proxyUrl, init);
|
|
13365
|
+
if (isTileRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13366
|
+
return originalFetch('/tiles/carto-a' + pathAndQuery, init);
|
|
13031
13367
|
}
|
|
13032
13368
|
// Proxy OpenStreetMap tile requests
|
|
13033
|
-
if (
|
|
13034
|
-
|
|
13035
|
-
return originalFetch(proxyUrl, init);
|
|
13369
|
+
if (hostname === 'tile.openstreetmap.org') {
|
|
13370
|
+
return originalFetch('/tiles/osm' + pathAndQuery, init);
|
|
13036
13371
|
}
|
|
13037
13372
|
}
|
|
13038
13373
|
return originalFetch(input, init);
|
|
@@ -13465,6 +13800,9 @@ class OfflineManagerControl {
|
|
|
13465
13800
|
if (patchedStyle.imports) {
|
|
13466
13801
|
delete patchedStyle.imports;
|
|
13467
13802
|
}
|
|
13803
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
13804
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
13805
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
13468
13806
|
// If using Service Worker (Mapbox GL JS), convert idb:// to /__offline__/ URLs
|
|
13469
13807
|
if (this.useServiceWorker) {
|
|
13470
13808
|
if (this.swReadyPromise) {
|
|
@@ -13609,5 +13947,5 @@ class OfflineManagerControl {
|
|
|
13609
13947
|
}
|
|
13610
13948
|
}
|
|
13611
13949
|
|
|
13612
|
-
export { AnalyticsService, CONTENT_TYPES, CategorizedError, CleanupService, DB_NAME, DB_VERSION, DOWNLOAD_DEFAULTS, ERROR_MESSAGES, ErrorType, FontService, GLYPH_CONFIG, GZIP_MAGIC_BYTES, GlyphService, ImportExportService, LogLevel, MAPBOX_API, MAPBOX_CACHE_TTL, MAPBOX_CLASSIC_STYLES, MAP_PROVIDERS, MaintenanceService, OfflineManagerControl, OfflineMapDBVersionError, OfflineMapManager, RESOURCE_TYPES, RegionService, ResourceService, STORAGE_CONFIG, STORE_NAMES, STYLE_CONFIG, SUCCESS_MESSAGES, ScopedLogger, SpriteService, TILE_CONFIG, TileService, URL_SCHEMES, VALIDATION_PATTERNS, applyProxy, categorizeError, cleanupCompressedTiles, cleanupExpiredTiles, cleanupOldFonts, cleanupOldGlyphs, cleanupOldSprites, cleanupOldStyles, cleanupOldTiles, cleanupService, clearAllCaches, configureLogger, configureProxy, convertStyleForServiceWorker, countCompressedTiles, createProgressTracker, createTileKey, dbPromise, OfflineMapManager as default, deleteStyleById, deleteStyles, deriveTileExtension, detectCssPrefix, detectStyleProvider, downloadFonts, downloadGlyphs, downloadSprites, downloadStyleWithProvider, downloadStyles, downloadTiles, escapeHtml$1 as escapeHtml, extractAccessToken, extractAllFontNames, extractFontNamesFromTextField, fetchResourceWithRetry, fetchWithRetry, fontService, formatBytes, formatDate, generateGlyphUrlsFromStyle, getExpiredResourceCount, getFontAnalytics, getFontStats, getGlyphAnalytics, getGlyphStats, getIcon, getRegionAnalytics, getSpriteAnalytics, getSpriteStats, getStyleStats, getTileAnalytics, getTileStats, getUserErrorMessage, glyphService, hasImports, i18n, icons, idbFetchHandler, isMapboxProtocol, isStyleDownloaded, loadAllStoredRegions, loadGlyphs, loadStyleById, loadStyles, logger, normalizeSpriteProperty, normalizeStyleUrl, optimizeStorage, parseCacheExpiry, parseTileKey, patchStyleForOffline, performCleanup, processBatch, processStyleSources, registerOfflineServiceWorker, resetOfflineMapDB, resolveImports, resolveMapboxUrl, resourceKeyBelongsToStyle, rewriteMapboxCdnTileUrl, safeExecute, setupAutoCleanup, spriteService, stopAutoCleanup, t, tileService, unregisterOfflineServiceWorker, validateBounds, validateRegionOptions, validateResource, validateStyleForProvider, validateZoomLevels, verifyAndRepairFonts, verifyAndRepairGlyphs, verifyAndRepairSprites };
|
|
13950
|
+
export { AnalyticsService, CONTENT_TYPES, CategorizedError, CleanupService, DB_NAME, DB_VERSION, DOWNLOAD_DEFAULTS, ERROR_MESSAGES, ErrorType, FontService, GLYPH_CONFIG, GZIP_MAGIC_BYTES, GlyphService, ImportExportService, LogLevel, MAPBOX_API, MAPBOX_CACHE_TTL, MAPBOX_CLASSIC_STYLES, MAP_PROVIDERS, MaintenanceService, ModelService, OfflineManagerControl, OfflineMapDBVersionError, OfflineMapManager, RESOURCE_TYPES, RegionService, ResourceService, STORAGE_CONFIG, STORE_NAMES, STYLE_CONFIG, SUCCESS_MESSAGES, ScopedLogger, SpriteService, TILE_CONFIG, TileService, URL_SCHEMES, VALIDATION_PATTERNS, applyProxy, categorizeError, cleanupCompressedTiles, cleanupExpiredTiles, cleanupOldFonts, cleanupOldGlyphs, cleanupOldModels, cleanupOldSprites, cleanupOldStyles, cleanupOldTiles, cleanupService, clearAllCaches, configureLogger, configureProxy, configureSqlJs, convertStyleForServiceWorker, countCompressedTiles, createProgressTracker, createTileKey, dbPromise, OfflineMapManager as default, deleteStyleById, deleteStyles, deriveTileExtension, detectCssPrefix, detectStyleProvider, downloadFonts, downloadGlyphs, downloadModels, downloadSprites, downloadStyleWithProvider, downloadStyles, downloadTiles, escapeHtml$1 as escapeHtml, extractAccessToken, extractAllFontNames, extractFontNamesFromTextField, extractTileExtensionFromUrl, fetchResourceWithRetry, fetchWithRetry, fontService, formatBytes, formatDate, generateGlyphUrlsFromStyle, getExpiredResourceCount, getFontAnalytics, getFontStats, getGlyphAnalytics, getGlyphStats, getIcon, getModel, getModelStats, getRegionAnalytics, getSpriteAnalytics, getSpriteStats, getSqlJs, getStyleStats, getTileAnalytics, getTileStats, getUrlHostname, getUserErrorMessage, glyphService, hasImports, hostMatches, i18n, icons, idbFetchHandler, isMapboxHost, isMapboxProtocol, isStyleDownloaded, loadAllStoredRegions, loadGlyphs, loadStyleById, loadStyles, logger, modelKeyBelongsToStyle, modelService, normalizeSpriteProperty, normalizeStyleUrl, optimizeStorage, parseCacheExpiry, parseTileKey, patchStyleForOffline, performCleanup, processBatch, processStyleSources, registerOfflineServiceWorker, resetOfflineMapDB, resolveImports, resolveMapboxUrl, resourceKeyBelongsToStyle, rewriteMapboxCdnTileUrl, safeExecute, sanitizeIndoorExpressions, setupAutoCleanup, spriteService, stopAutoCleanup, t, tileService, unregisterOfflineServiceWorker, validateBounds, validateRegionOptions, validateResource, validateStyleForProvider, validateZoomLevels, verifyAndRepairFonts, verifyAndRepairGlyphs, verifyAndRepairModels, verifyAndRepairSprites };
|
|
13613
13951
|
//# sourceMappingURL=index.esm.js.map
|