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.js
CHANGED
|
@@ -35,7 +35,7 @@ var tilebelt__namespace = /*#__PURE__*/_interopNamespaceDefault(tilebelt);
|
|
|
35
35
|
*/
|
|
36
36
|
// IndexedDB Configuration
|
|
37
37
|
const DB_NAME = 'offline-map-db';
|
|
38
|
-
const DB_VERSION =
|
|
38
|
+
const DB_VERSION = 4;
|
|
39
39
|
// Store Names (regions are stored inside styles.regions[], not as a separate store)
|
|
40
40
|
const STORE_NAMES = {
|
|
41
41
|
TILES: 'tiles',
|
|
@@ -43,6 +43,7 @@ const STORE_NAMES = {
|
|
|
43
43
|
SPRITES: 'sprites',
|
|
44
44
|
GLYPHS: 'glyphs',
|
|
45
45
|
FONTS: 'fonts',
|
|
46
|
+
MODELS: 'models',
|
|
46
47
|
};
|
|
47
48
|
// Download Configuration
|
|
48
49
|
const DOWNLOAD_DEFAULTS = {
|
|
@@ -250,7 +251,7 @@ async function resetOfflineMapDB() {
|
|
|
250
251
|
* Called during initial database creation or when stores are missing.
|
|
251
252
|
*/
|
|
252
253
|
function createStores(db) {
|
|
253
|
-
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts'];
|
|
254
|
+
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts', 'models'];
|
|
254
255
|
for (const storeName of stores) {
|
|
255
256
|
if (!db.objectStoreNames.contains(storeName)) {
|
|
256
257
|
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
@@ -332,6 +333,7 @@ function migrateRegionsToStyles(transaction) {
|
|
|
332
333
|
* - sprites: Sprite images and JSON
|
|
333
334
|
* - glyphs: Font glyph data
|
|
334
335
|
* - fonts: Font files
|
|
336
|
+
* - models: 3D model files (.glb) for Mapbox Standard tree/turbine layers
|
|
335
337
|
* - regions: (deprecated) Legacy region storage, migrated to styles.regions[]
|
|
336
338
|
*
|
|
337
339
|
* @example
|
|
@@ -351,6 +353,9 @@ async function openOfflineMapDB() {
|
|
|
351
353
|
if (oldVersion > 0 && oldVersion < 3) {
|
|
352
354
|
migrateRegionsToStyles(transaction);
|
|
353
355
|
}
|
|
356
|
+
// Migration: v3 -> v4
|
|
357
|
+
// Adds the `models` store for Mapbox Standard 3D model assets.
|
|
358
|
+
// No data migration needed — createStores above handles it.
|
|
354
359
|
},
|
|
355
360
|
});
|
|
356
361
|
}
|
|
@@ -1185,6 +1190,22 @@ function parseTileKey(key) {
|
|
|
1185
1190
|
}
|
|
1186
1191
|
return { styleId, sourceId, z, x, y, ext };
|
|
1187
1192
|
}
|
|
1193
|
+
/**
|
|
1194
|
+
* Extract the extension (the last dotted segment before `?`, `#`, or end) from
|
|
1195
|
+
* a tile URL or tile URL template. Defaults to `"pbf"` when no extension can
|
|
1196
|
+
* be parsed. For multi-extension URLs like Mapbox v4's `{y}.vector.pbf` this
|
|
1197
|
+
* returns `"pbf"`, matching the key used when the tile is stored.
|
|
1198
|
+
*
|
|
1199
|
+
* Keeping extraction logic in one place ensures patchStyleForOffline (which
|
|
1200
|
+
* rewrites tile URLs to `idb://` at load time) derives the same extension
|
|
1201
|
+
* that tileService.extractExtension used at store time — otherwise the
|
|
1202
|
+
* first-try lookup in idbFetchHandler misses and has to fall through its
|
|
1203
|
+
* pbf/mvt/png/jpg/webp fallback loop.
|
|
1204
|
+
*/
|
|
1205
|
+
function extractTileExtensionFromUrl(url) {
|
|
1206
|
+
const match = url.match(/\.([\w]+)(?:[?#]|$)/i);
|
|
1207
|
+
return match ? match[1] : 'pbf';
|
|
1208
|
+
}
|
|
1188
1209
|
/**
|
|
1189
1210
|
* Derive tile extension from tile URL templates
|
|
1190
1211
|
*/
|
|
@@ -1192,21 +1213,217 @@ function deriveTileExtension(tiles) {
|
|
|
1192
1213
|
if (Array.isArray(tiles) && tiles.length > 0) {
|
|
1193
1214
|
const firstTile = tiles[0];
|
|
1194
1215
|
if (typeof firstTile === 'string') {
|
|
1195
|
-
|
|
1196
|
-
if (match) {
|
|
1197
|
-
return match[1];
|
|
1198
|
-
}
|
|
1216
|
+
return extractTileExtensionFromUrl(firstTile);
|
|
1199
1217
|
}
|
|
1200
1218
|
}
|
|
1201
1219
|
return 'pbf';
|
|
1202
1220
|
}
|
|
1203
1221
|
|
|
1222
|
+
/**
|
|
1223
|
+
* Pure helpers shared between the main-thread offline fetch handler
|
|
1224
|
+
* (`src/utils/idbFetchHandler.ts`) and the offline Service Worker
|
|
1225
|
+
* (`src/sw/offline-sw.ts`, compiled to `public/idb-offline-sw.js`).
|
|
1226
|
+
*
|
|
1227
|
+
* Keeping these in one place means the SW and the main-thread handler
|
|
1228
|
+
* can't drift — adding a new `model` handler, changing the fallback
|
|
1229
|
+
* order, or tweaking the tilejson-source matcher happens once.
|
|
1230
|
+
*
|
|
1231
|
+
* Nothing in here touches IndexedDB directly. Each helper takes already-
|
|
1232
|
+
* resolved inputs and returns the list of candidate keys (or the
|
|
1233
|
+
* resolved output) that the caller feeds into its own IDB lookup.
|
|
1234
|
+
*
|
|
1235
|
+
* The corresponding IDB access layer is:
|
|
1236
|
+
* - main thread: `idb` library via `dbPromise`
|
|
1237
|
+
* - service worker: raw `indexedDB.open` (see `offline-sw.ts`)
|
|
1238
|
+
*
|
|
1239
|
+
* They have different shapes so cannot be shared; the key computation
|
|
1240
|
+
* can be and is.
|
|
1241
|
+
*/
|
|
1242
|
+
/**
|
|
1243
|
+
* Extensions to try in order when the requested extension misses. `glb` is
|
|
1244
|
+
* last so batched-model sources (Mapbox Standard 3D buildings) resolve when
|
|
1245
|
+
* their source URL template ended in `.vector` or similar and the actual
|
|
1246
|
+
* tile body was stored as glb.
|
|
1247
|
+
*/
|
|
1248
|
+
const TILE_FALLBACK_EXTENSIONS = ['pbf', 'mvt', 'png', 'jpg', 'webp', 'glb'];
|
|
1249
|
+
/** Extensions minus the one the caller already tried. */
|
|
1250
|
+
function tileFallbackExtensions(requested) {
|
|
1251
|
+
return TILE_FALLBACK_EXTENSIONS.filter(e => e !== requested);
|
|
1252
|
+
}
|
|
1253
|
+
// ---------------------------------------------------------------------------
|
|
1254
|
+
// Region → style lookup
|
|
1255
|
+
// ---------------------------------------------------------------------------
|
|
1256
|
+
/**
|
|
1257
|
+
* Given an already-fetched list of style entries, find the first one whose
|
|
1258
|
+
* `regions` array contains the given ID. Pure — the caller is responsible for
|
|
1259
|
+
* loading the entries and for caching. Used by both `findStyleByRegionId`
|
|
1260
|
+
* implementations to keep the match rule identical.
|
|
1261
|
+
*/
|
|
1262
|
+
function findStyleByRegionIdIn(styles, regionId) {
|
|
1263
|
+
for (const entry of styles) {
|
|
1264
|
+
const regions = entry.regions;
|
|
1265
|
+
if (!Array.isArray(regions))
|
|
1266
|
+
continue;
|
|
1267
|
+
for (const r of regions) {
|
|
1268
|
+
if (r?.regionId === regionId || r?.id === regionId) {
|
|
1269
|
+
return entry;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
// ---------------------------------------------------------------------------
|
|
1276
|
+
// Glyph candidate keys
|
|
1277
|
+
// ---------------------------------------------------------------------------
|
|
1278
|
+
/**
|
|
1279
|
+
* Parse `FontA,FontB,FontC/0-255.pbf` into (fontstacks, rangePart). Mapbox
|
|
1280
|
+
* requests a comma-joined font-family fallback chain; each glyph is stored
|
|
1281
|
+
* individually, so the caller tries each fontstack in order.
|
|
1282
|
+
*/
|
|
1283
|
+
function parseGlyphPath(decodedPath) {
|
|
1284
|
+
const pathParts = decodedPath.split('/');
|
|
1285
|
+
const fontstackPart = pathParts[0] ?? '';
|
|
1286
|
+
const rangePart = pathParts[1] || '0-255.pbf';
|
|
1287
|
+
const fontstacks = fontstackPart
|
|
1288
|
+
.split(',')
|
|
1289
|
+
.map(f => f.trim())
|
|
1290
|
+
.filter(Boolean);
|
|
1291
|
+
return { fontstacks, rangePart };
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Build the list of keys to try for a single (fontstack, range) pair.
|
|
1295
|
+
* Order: actualStyleId variants first (most common), then downloadId,
|
|
1296
|
+
* then the bare path. Normalized and raw `.pbf`-less forms are both tried
|
|
1297
|
+
* to cover stored-key variants from older versions.
|
|
1298
|
+
*/
|
|
1299
|
+
function glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart) {
|
|
1300
|
+
const glyphPath = `${fontstack}/${rangePart}`;
|
|
1301
|
+
const normalizedPath = glyphPath.endsWith('.pbf') ? glyphPath : `${glyphPath}.pbf`;
|
|
1302
|
+
return dedupe([
|
|
1303
|
+
`${actualStyleId}::${normalizedPath}`,
|
|
1304
|
+
`${actualStyleId}::${glyphPath}`,
|
|
1305
|
+
`${downloadId}::${normalizedPath}`,
|
|
1306
|
+
`${downloadId}::${glyphPath}`,
|
|
1307
|
+
normalizedPath,
|
|
1308
|
+
glyphPath,
|
|
1309
|
+
]);
|
|
1310
|
+
}
|
|
1311
|
+
// ---------------------------------------------------------------------------
|
|
1312
|
+
// Sprite candidate keys
|
|
1313
|
+
// ---------------------------------------------------------------------------
|
|
1314
|
+
/**
|
|
1315
|
+
* Sprite keys have historically used both `::` and `:` as the separator, and
|
|
1316
|
+
* both the full filename (`sprite.json`) and the bare name (`sprite`). Return
|
|
1317
|
+
* every variant in priority order; the caller stops at the first hit.
|
|
1318
|
+
*/
|
|
1319
|
+
function spriteCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1320
|
+
const stripExt = decodedPath.replace(/\.(json|png)$/i, '');
|
|
1321
|
+
return dedupe([
|
|
1322
|
+
`${actualStyleId}::${decodedPath}`,
|
|
1323
|
+
`${actualStyleId}:${decodedPath}`,
|
|
1324
|
+
`${actualStyleId}::${stripExt}`,
|
|
1325
|
+
`${actualStyleId}:${stripExt}`,
|
|
1326
|
+
`${downloadId}::${decodedPath}`,
|
|
1327
|
+
`${downloadId}:${decodedPath}`,
|
|
1328
|
+
`${downloadId}::${stripExt}`,
|
|
1329
|
+
`${downloadId}:${stripExt}`,
|
|
1330
|
+
decodedPath,
|
|
1331
|
+
]);
|
|
1332
|
+
}
|
|
1333
|
+
// ---------------------------------------------------------------------------
|
|
1334
|
+
// Model candidate keys
|
|
1335
|
+
// ---------------------------------------------------------------------------
|
|
1336
|
+
/**
|
|
1337
|
+
* Model keys are `{styleId}::model::{name}`. Try the resolved style id first,
|
|
1338
|
+
* then the bare downloadId in case the request came through the region-scoped
|
|
1339
|
+
* URL form (`idb://{regionId}/model/{name}`).
|
|
1340
|
+
*/
|
|
1341
|
+
function modelCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1342
|
+
return dedupe([
|
|
1343
|
+
`${actualStyleId}::model::${decodedPath}`,
|
|
1344
|
+
`${downloadId}::model::${decodedPath}`,
|
|
1345
|
+
]);
|
|
1346
|
+
}
|
|
1347
|
+
// ---------------------------------------------------------------------------
|
|
1348
|
+
// TileJSON source matching
|
|
1349
|
+
// ---------------------------------------------------------------------------
|
|
1350
|
+
/**
|
|
1351
|
+
* Mapbox GL requests tilejson via `idb://{downloadId}/tilesjson/{path}` where
|
|
1352
|
+
* `{path}` may be the source id, the original TileJSON URL, or the URL we
|
|
1353
|
+
* stashed under `__originalTilesetUrl` when patching for offline. Try all
|
|
1354
|
+
* three; return the matching source id + its config, or null.
|
|
1355
|
+
*/
|
|
1356
|
+
function matchTileJsonSource(sources, decodedPath) {
|
|
1357
|
+
const asConfig = (v) => v && typeof v === 'object' ? v : null;
|
|
1358
|
+
if (decodedPath in sources) {
|
|
1359
|
+
const config = asConfig(sources[decodedPath]);
|
|
1360
|
+
if (config)
|
|
1361
|
+
return { sourceId: decodedPath, config };
|
|
1362
|
+
}
|
|
1363
|
+
for (const [sourceId, raw] of Object.entries(sources)) {
|
|
1364
|
+
const config = asConfig(raw);
|
|
1365
|
+
if (!config)
|
|
1366
|
+
continue;
|
|
1367
|
+
const url = typeof config.url === 'string' ? config.url : undefined;
|
|
1368
|
+
const original = typeof config.__originalTilesetUrl === 'string'
|
|
1369
|
+
? config.__originalTilesetUrl
|
|
1370
|
+
: undefined;
|
|
1371
|
+
if (url === decodedPath || original === decodedPath) {
|
|
1372
|
+
return { sourceId, config };
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Build the offline TileJSON payload that replaces the one Mapbox would
|
|
1379
|
+
* have fetched from the network. `tiles` is rewritten to serve from the SW
|
|
1380
|
+
* (the caller supplies the scheme via `tileUrlScheme`); copyable TileJSON
|
|
1381
|
+
* fields are preserved.
|
|
1382
|
+
*/
|
|
1383
|
+
function buildOfflineTileJson(sourceConfig, downloadId, sourceId, extension, tileUrlScheme, origin) {
|
|
1384
|
+
const base = `idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`
|
|
1385
|
+
;
|
|
1386
|
+
const tileJson = {
|
|
1387
|
+
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1388
|
+
name: sourceConfig.name ?? sourceId,
|
|
1389
|
+
tiles: [base],
|
|
1390
|
+
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1391
|
+
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1392
|
+
};
|
|
1393
|
+
const copyable = [
|
|
1394
|
+
'bounds',
|
|
1395
|
+
'center',
|
|
1396
|
+
'vector_layers',
|
|
1397
|
+
'scheme',
|
|
1398
|
+
'attribution',
|
|
1399
|
+
'encoding',
|
|
1400
|
+
'format',
|
|
1401
|
+
'grids',
|
|
1402
|
+
'data',
|
|
1403
|
+
'template',
|
|
1404
|
+
'version',
|
|
1405
|
+
];
|
|
1406
|
+
for (const field of copyable) {
|
|
1407
|
+
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1408
|
+
tileJson[field] = sourceConfig[field];
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return tileJson;
|
|
1412
|
+
}
|
|
1413
|
+
// ---------------------------------------------------------------------------
|
|
1414
|
+
// Internal helpers
|
|
1415
|
+
// ---------------------------------------------------------------------------
|
|
1416
|
+
function dedupe(values) {
|
|
1417
|
+
return Array.from(new Set(values));
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1204
1420
|
// idbFetchHandler.ts
|
|
1205
1421
|
// Intercepts idb:// URLs and serves resources from IndexedDB for MapLibre GL offline mode
|
|
1206
1422
|
const idbLogger = logger.scope('IDBFetch');
|
|
1207
1423
|
// idb://{downloadId}/tile/{sourceKey}/{url}
|
|
1208
1424
|
// idb://{downloadId}/glyph/{fontstack}/{range}.pbf
|
|
1209
1425
|
// idb://{downloadId}/sprite/{spriteName}
|
|
1426
|
+
// idb://{styleId}/model/{modelName}
|
|
1210
1427
|
// idb://{downloadId}/tilesjson/{url}
|
|
1211
1428
|
// Cache for region ID to style mapping to avoid repeated DB queries
|
|
1212
1429
|
const regionToStyleCache = new Map();
|
|
@@ -1256,16 +1473,11 @@ async function findStyleByRegionId(db, regionId) {
|
|
|
1256
1473
|
}
|
|
1257
1474
|
try {
|
|
1258
1475
|
const allStyles = await db.getAll('styles');
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
// Cache the result
|
|
1265
|
-
regionToStyleCache.set(regionId, { styleEntry, timestamp: Date.now() });
|
|
1266
|
-
return styleEntry;
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1476
|
+
const hit = findStyleByRegionIdIn(allStyles, regionId);
|
|
1477
|
+
if (hit) {
|
|
1478
|
+
idbLogger.debug(`Found style "${hit.key}" containing region: ${regionId}`);
|
|
1479
|
+
regionToStyleCache.set(regionId, { styleEntry: hit, timestamp: Date.now() });
|
|
1480
|
+
return hit;
|
|
1269
1481
|
}
|
|
1270
1482
|
idbLogger.debug(`No style found containing region: ${regionId}`);
|
|
1271
1483
|
// Don't cache negative results — the region may be stored moments later
|
|
@@ -1277,36 +1489,6 @@ async function findStyleByRegionId(db, regionId) {
|
|
|
1277
1489
|
return null;
|
|
1278
1490
|
}
|
|
1279
1491
|
}
|
|
1280
|
-
function buildOfflineTileJson(sourceConfig, downloadId, sourceId) {
|
|
1281
|
-
const extension = deriveTileExtension(sourceConfig.tiles);
|
|
1282
|
-
const offlineTiles = [`idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`];
|
|
1283
|
-
const tileJson = {
|
|
1284
|
-
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1285
|
-
name: sourceConfig.name ?? sourceId,
|
|
1286
|
-
tiles: offlineTiles,
|
|
1287
|
-
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1288
|
-
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1289
|
-
};
|
|
1290
|
-
const fieldsToCopy = [
|
|
1291
|
-
'bounds',
|
|
1292
|
-
'center',
|
|
1293
|
-
'vector_layers',
|
|
1294
|
-
'scheme',
|
|
1295
|
-
'attribution',
|
|
1296
|
-
'encoding',
|
|
1297
|
-
'format',
|
|
1298
|
-
'grids',
|
|
1299
|
-
'data',
|
|
1300
|
-
'template',
|
|
1301
|
-
'version',
|
|
1302
|
-
];
|
|
1303
|
-
for (const field of fieldsToCopy) {
|
|
1304
|
-
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1305
|
-
tileJson[field] = sourceConfig[field];
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
return tileJson;
|
|
1309
|
-
}
|
|
1310
1492
|
async function createTileResponse(resource) {
|
|
1311
1493
|
const headers = {};
|
|
1312
1494
|
// Set proper content type for vector tiles (PBF/MVT format)
|
|
@@ -1401,7 +1583,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1401
1583
|
// but tiles are stored with integer zoom levels, so floor the value
|
|
1402
1584
|
const z = Math.floor(parseFloat(pathParts[pathParts.length - 3]));
|
|
1403
1585
|
const sourceKey = pathParts.slice(0, pathParts.length - 3).join('/');
|
|
1404
|
-
const yMatch = yExt.match(
|
|
1586
|
+
const yMatch = yExt.match(/^(\d+)\.(\w+)$/);
|
|
1405
1587
|
if (yMatch) {
|
|
1406
1588
|
const y = parseInt(yMatch[1]);
|
|
1407
1589
|
const requestedExt = yMatch[2]; // Extension from URL (for logging only)
|
|
@@ -1416,7 +1598,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1416
1598
|
}
|
|
1417
1599
|
idbLogger.debug(`Tile not found: ${tileKey}`);
|
|
1418
1600
|
// Fallback: try common alternative extensions
|
|
1419
|
-
const fallbackExtensions =
|
|
1601
|
+
const fallbackExtensions = tileFallbackExtensions(requestedExt);
|
|
1420
1602
|
for (const fallbackExt of fallbackExtensions) {
|
|
1421
1603
|
const fallbackKey = createTileKey(x, y, z, actualStyleId, sourceKey, fallbackExt);
|
|
1422
1604
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1458,7 +1640,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1458
1640
|
return await createTileResponse(resource);
|
|
1459
1641
|
}
|
|
1460
1642
|
// Try alternative extensions
|
|
1461
|
-
const fallbackExts =
|
|
1643
|
+
const fallbackExts = tileFallbackExtensions(ext);
|
|
1462
1644
|
for (const fallbackExt of fallbackExts) {
|
|
1463
1645
|
const fallbackKey = createTileKey(parseInt(x), parseInt(y), parseInt(z), actualStyleId, fallbackSourceKey, fallbackExt);
|
|
1464
1646
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1478,46 +1660,19 @@ async function idbFetchHandler(url, init) {
|
|
|
1478
1660
|
}
|
|
1479
1661
|
case 'glyph': {
|
|
1480
1662
|
idbLogger.debug(`Looking for glyph with key: ${key}`);
|
|
1481
|
-
// Find which style this region belongs to
|
|
1482
1663
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1483
1664
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1484
|
-
|
|
1485
|
-
idbLogger.debug(`Region "${downloadId}" belongs to style "${actualStyleId}", searching with style key`);
|
|
1486
|
-
}
|
|
1487
|
-
// Parse the resource path: "FontA,FontB,FontC/0-255.pbf"
|
|
1488
|
-
// MapLibre requests glyphs with comma-separated fallback fonts
|
|
1489
|
-
// but glyphs are stored individually per font
|
|
1490
|
-
const pathParts = decodedResourcePath.split('/');
|
|
1491
|
-
const fontstackPart = pathParts[0]; // "FontA,FontB,FontC"
|
|
1492
|
-
const rangePart = pathParts[1] || '0-255.pbf'; // "0-255.pbf"
|
|
1493
|
-
// Split comma-separated fonts
|
|
1494
|
-
const fontstacks = fontstackPart.split(',').map(f => f.trim());
|
|
1665
|
+
const { fontstacks, rangePart } = parseGlyphPath(decodedResourcePath);
|
|
1495
1666
|
idbLogger.debug(`Trying ${fontstacks.length} fonts in fallback order: ${fontstacks.join(', ')}`);
|
|
1496
|
-
// Try each font in order (this is how font fallbacks work)
|
|
1497
1667
|
for (const fontstack of fontstacks) {
|
|
1498
|
-
const
|
|
1499
|
-
const
|
|
1500
|
-
const glyphCandidateKeys = [
|
|
1501
|
-
// Try with actual style ID first
|
|
1502
|
-
`${actualStyleId}::${normalizedPath}`,
|
|
1503
|
-
`${actualStyleId}::${glyphPath}`,
|
|
1504
|
-
// Then try with download ID
|
|
1505
|
-
`${downloadId}::${normalizedPath}`,
|
|
1506
|
-
`${downloadId}::${glyphPath}`,
|
|
1507
|
-
// Just paths
|
|
1508
|
-
normalizedPath,
|
|
1509
|
-
glyphPath,
|
|
1510
|
-
];
|
|
1511
|
-
idbLogger.debug(`Trying keys for font "${fontstack}":`, glyphCandidateKeys);
|
|
1512
|
-
for (const candidateKey of glyphCandidateKeys) {
|
|
1668
|
+
const candidateKeys = glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart);
|
|
1669
|
+
for (const candidateKey of candidateKeys) {
|
|
1513
1670
|
const resource = await db.get('glyphs', candidateKey);
|
|
1514
1671
|
if (resource?.data) {
|
|
1515
1672
|
idbLogger.debug(`Found glyph using key: ${candidateKey} (font: ${fontstack})`);
|
|
1516
1673
|
return new Response(resource.data, {
|
|
1517
1674
|
status: 200,
|
|
1518
|
-
headers: {
|
|
1519
|
-
'Content-Type': 'application/x-protobuf',
|
|
1520
|
-
},
|
|
1675
|
+
headers: { 'Content-Type': 'application/x-protobuf' },
|
|
1521
1676
|
});
|
|
1522
1677
|
}
|
|
1523
1678
|
}
|
|
@@ -1527,33 +1682,11 @@ async function idbFetchHandler(url, init) {
|
|
|
1527
1682
|
}
|
|
1528
1683
|
case 'sprite': {
|
|
1529
1684
|
idbLogger.debug(`Looking for sprite with key: ${key}`);
|
|
1530
|
-
// Find which style this region belongs to
|
|
1531
1685
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1532
1686
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
// The sprite service stores sprites with keys like: "voyager::sprite.json", "voyager::sprite@2x.json"
|
|
1537
|
-
// MapLibre requests sprites as: "idb://region_XXX/sprite/sprite@2x.json"
|
|
1538
|
-
// So we need to map the region ID to the style ID
|
|
1539
|
-
const spriteCandidateKeys = Array.from(new Set([
|
|
1540
|
-
// Try with actual style ID first (most likely to work)
|
|
1541
|
-
`${actualStyleId}::${decodedResourcePath}`,
|
|
1542
|
-
`${actualStyleId}:${decodedResourcePath}`,
|
|
1543
|
-
`${actualStyleId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1544
|
-
`${actualStyleId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1545
|
-
// Then try with download ID (in case it's a direct style download)
|
|
1546
|
-
`${downloadId}::${decodedResourcePath}`,
|
|
1547
|
-
`${downloadId}:${decodedResourcePath}`,
|
|
1548
|
-
`${downloadId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1549
|
-
`${downloadId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1550
|
-
// Just the path itself
|
|
1551
|
-
decodedResourcePath,
|
|
1552
|
-
// Original key format
|
|
1553
|
-
key,
|
|
1554
|
-
]));
|
|
1555
|
-
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, spriteCandidateKeys);
|
|
1556
|
-
for (const candidateKey of spriteCandidateKeys) {
|
|
1687
|
+
const candidates = spriteCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1688
|
+
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, candidates);
|
|
1689
|
+
for (const candidateKey of candidates) {
|
|
1557
1690
|
const resource = await db.get('sprites', candidateKey);
|
|
1558
1691
|
if (resource?.data) {
|
|
1559
1692
|
idbLogger.debug(`Found sprite using key: ${candidateKey}`);
|
|
@@ -1563,7 +1696,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1563
1696
|
});
|
|
1564
1697
|
}
|
|
1565
1698
|
}
|
|
1566
|
-
idbLogger.warn(`Sprite not found, tried keys: ${
|
|
1699
|
+
idbLogger.warn(`Sprite not found, tried keys: ${candidates.join(', ')}`);
|
|
1567
1700
|
break;
|
|
1568
1701
|
}
|
|
1569
1702
|
case 'font': {
|
|
@@ -1578,11 +1711,32 @@ async function idbFetchHandler(url, init) {
|
|
|
1578
1711
|
}
|
|
1579
1712
|
break;
|
|
1580
1713
|
}
|
|
1714
|
+
case 'model': {
|
|
1715
|
+
// Model URLs are rewritten by patchStyleForOffline to
|
|
1716
|
+
// idb://{styleId}/model/{modelName}
|
|
1717
|
+
// Models are keyed by {styleId}::model::{modelName} in the store.
|
|
1718
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1719
|
+
const actualStyleId = styleEntry?.key || downloadId;
|
|
1720
|
+
const candidates = modelCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1721
|
+
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1722
|
+
for (const candidateKey of candidates) {
|
|
1723
|
+
const resource = await db.get('models', candidateKey);
|
|
1724
|
+
if (resource?.data) {
|
|
1725
|
+
idbLogger.debug(`Found model using key: ${candidateKey}`);
|
|
1726
|
+
return new Response(resource.data, {
|
|
1727
|
+
status: 200,
|
|
1728
|
+
headers: { 'Content-Type': resource.contentType || 'model/gltf-binary' },
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
idbLogger.warn(`Model not found, tried keys: ${candidates.join(', ')}`);
|
|
1733
|
+
break;
|
|
1734
|
+
}
|
|
1581
1735
|
case 'tilesjson': {
|
|
1582
1736
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1583
|
-
// First try direct lookup (for style-level downloads)
|
|
1737
|
+
// First try direct lookup (for style-level downloads), then fall back
|
|
1738
|
+
// to searching by region ID (for region-level downloads).
|
|
1584
1739
|
let styleEntry = await db.get('styles', downloadId);
|
|
1585
|
-
// If not found, search by region ID (for region-level downloads)
|
|
1586
1740
|
if (!styleEntry || !styleEntry.style?.sources) {
|
|
1587
1741
|
idbLogger.debug(`Style not found with key "${downloadId}", searching by region ID...`);
|
|
1588
1742
|
const foundStyle = await findStyleByRegionId(db, downloadId);
|
|
@@ -1590,41 +1744,23 @@ async function idbFetchHandler(url, init) {
|
|
|
1590
1744
|
styleEntry = foundStyle;
|
|
1591
1745
|
}
|
|
1592
1746
|
}
|
|
1593
|
-
if (styleEntry?.style?.sources) {
|
|
1594
|
-
const sources = styleEntry.style.sources;
|
|
1595
|
-
let matchedSourceId;
|
|
1596
|
-
let matchedSourceConfig;
|
|
1597
|
-
if (decodedResourcePath in sources) {
|
|
1598
|
-
matchedSourceId = decodedResourcePath;
|
|
1599
|
-
matchedSourceConfig = sources[decodedResourcePath];
|
|
1600
|
-
}
|
|
1601
|
-
else {
|
|
1602
|
-
for (const [sourceId, sourceValue] of Object.entries(sources)) {
|
|
1603
|
-
const sourceUrl = typeof sourceValue.url === 'string' ? sourceValue.url : undefined;
|
|
1604
|
-
const originalUrl = typeof sourceValue.__originalTilesetUrl === 'string'
|
|
1605
|
-
? sourceValue.__originalTilesetUrl
|
|
1606
|
-
: undefined;
|
|
1607
|
-
if (sourceUrl === decodedResourcePath || originalUrl === decodedResourcePath) {
|
|
1608
|
-
matchedSourceId = sourceId;
|
|
1609
|
-
matchedSourceConfig = sourceValue;
|
|
1610
|
-
break;
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
if (matchedSourceId && matchedSourceConfig) {
|
|
1615
|
-
const tileJson = buildOfflineTileJson(matchedSourceConfig, downloadId, matchedSourceId);
|
|
1616
|
-
idbLogger.debug(`Serving offline tilejson for source: ${matchedSourceId}`);
|
|
1617
|
-
return new Response(JSON.stringify(tileJson), {
|
|
1618
|
-
status: 200,
|
|
1619
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1623
|
-
}
|
|
1624
|
-
else {
|
|
1747
|
+
if (!styleEntry?.style?.sources) {
|
|
1625
1748
|
idbLogger.warn(`Style not found or missing sources for downloadId: ${downloadId}`);
|
|
1749
|
+
break;
|
|
1626
1750
|
}
|
|
1627
|
-
|
|
1751
|
+
const sources = styleEntry.style.sources;
|
|
1752
|
+
const matched = matchTileJsonSource(sources, decodedResourcePath);
|
|
1753
|
+
if (!matched) {
|
|
1754
|
+
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1755
|
+
break;
|
|
1756
|
+
}
|
|
1757
|
+
const extension = deriveTileExtension(matched.config.tiles);
|
|
1758
|
+
const tileJson = buildOfflineTileJson(matched.config, downloadId, matched.sourceId, extension, 'idb');
|
|
1759
|
+
idbLogger.debug(`Serving offline tilejson for source: ${matched.sourceId}`);
|
|
1760
|
+
return new Response(JSON.stringify(tileJson), {
|
|
1761
|
+
status: 200,
|
|
1762
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1763
|
+
});
|
|
1628
1764
|
}
|
|
1629
1765
|
default:
|
|
1630
1766
|
idbLogger.warn(`Unknown resource type: ${type}`);
|
|
@@ -1648,6 +1784,37 @@ async function idbFetchHandler(url, init) {
|
|
|
1648
1784
|
function isMapboxProtocol(url) {
|
|
1649
1785
|
return url.startsWith(MAPBOX_API.PROTOCOL);
|
|
1650
1786
|
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Parse a URL and return its hostname, or null if the URL is malformed.
|
|
1789
|
+
* Accepts relative URLs when `base` is provided.
|
|
1790
|
+
*/
|
|
1791
|
+
function getUrlHostname(url, base) {
|
|
1792
|
+
try {
|
|
1793
|
+
return new URL(url, base).hostname.toLowerCase();
|
|
1794
|
+
}
|
|
1795
|
+
catch {
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
/**
|
|
1800
|
+
* True if `url`'s hostname equals `host` or is a subdomain of `host`.
|
|
1801
|
+
* Uses URL parsing (not substring matching) to avoid false positives like
|
|
1802
|
+
* `https://evil.com/?x=mapbox.com` matching `mapbox.com`.
|
|
1803
|
+
*/
|
|
1804
|
+
function hostMatches(url, host, base) {
|
|
1805
|
+
const hostname = getUrlHostname(url, base);
|
|
1806
|
+
if (hostname === null)
|
|
1807
|
+
return false;
|
|
1808
|
+
const target = host.toLowerCase();
|
|
1809
|
+
return hostname === target || hostname.endsWith('.' + target);
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* True for any host under the mapbox.com domain (including api.mapbox.com,
|
|
1813
|
+
* *.tiles.mapbox.com, etc.). Used by provider detection.
|
|
1814
|
+
*/
|
|
1815
|
+
function isMapboxHost(url, base) {
|
|
1816
|
+
return hostMatches(url, 'mapbox.com', base);
|
|
1817
|
+
}
|
|
1651
1818
|
/**
|
|
1652
1819
|
* Resolve a mapbox:// URL to its HTTPS API equivalent
|
|
1653
1820
|
*
|
|
@@ -1721,9 +1888,7 @@ function rewriteMapboxCdnTileUrl(tileUrl) {
|
|
|
1721
1888
|
*/
|
|
1722
1889
|
function detectStyleProvider(styleUrl, style) {
|
|
1723
1890
|
// Check URL patterns
|
|
1724
|
-
if (isMapboxProtocol(styleUrl) ||
|
|
1725
|
-
styleUrl.includes('mapbox.com') ||
|
|
1726
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
1891
|
+
if (isMapboxProtocol(styleUrl) || isMapboxHost(styleUrl)) {
|
|
1727
1892
|
return 'mapbox';
|
|
1728
1893
|
}
|
|
1729
1894
|
if (styleUrl.includes('maplibre') ||
|
|
@@ -1742,7 +1907,7 @@ function detectStyleProvider(styleUrl, style) {
|
|
|
1742
1907
|
const sources = style.sources || {};
|
|
1743
1908
|
for (const [, sourceConfig] of Object.entries(sources)) {
|
|
1744
1909
|
const source = sourceConfig;
|
|
1745
|
-
if (source.url && (source.url
|
|
1910
|
+
if (source.url && (isMapboxProtocol(source.url) || isMapboxHost(source.url))) {
|
|
1746
1911
|
return 'mapbox';
|
|
1747
1912
|
}
|
|
1748
1913
|
}
|
|
@@ -1834,7 +1999,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1834
1999
|
if (isMapboxProtocol(tileUrl) && accessToken) {
|
|
1835
2000
|
return resolveMapboxUrl(tileUrl, accessToken);
|
|
1836
2001
|
}
|
|
1837
|
-
if (provider === 'mapbox' && accessToken && tileUrl
|
|
2002
|
+
if (provider === 'mapbox' && accessToken && isMapboxHost(tileUrl)) {
|
|
1838
2003
|
return normalizeStyleUrl(tileUrl, accessToken);
|
|
1839
2004
|
}
|
|
1840
2005
|
return tileUrl;
|
|
@@ -1849,7 +2014,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1849
2014
|
if (isMapboxProtocol(processedStyle.sprite)) {
|
|
1850
2015
|
processedStyle.sprite = resolveMapboxUrl(processedStyle.sprite, accessToken);
|
|
1851
2016
|
}
|
|
1852
|
-
else if (provider === 'mapbox' && processedStyle.sprite
|
|
2017
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.sprite)) {
|
|
1853
2018
|
processedStyle.sprite = normalizeStyleUrl(processedStyle.sprite, accessToken);
|
|
1854
2019
|
}
|
|
1855
2020
|
}
|
|
@@ -1860,7 +2025,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1860
2025
|
if (isMapboxProtocol(entry.url)) {
|
|
1861
2026
|
return { ...entry, url: resolveMapboxUrl(entry.url, accessToken) };
|
|
1862
2027
|
}
|
|
1863
|
-
else if (provider === 'mapbox' && entry.url
|
|
2028
|
+
else if (provider === 'mapbox' && isMapboxHost(entry.url)) {
|
|
1864
2029
|
return { ...entry, url: normalizeStyleUrl(entry.url, accessToken) };
|
|
1865
2030
|
}
|
|
1866
2031
|
}
|
|
@@ -1873,7 +2038,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1873
2038
|
if (isMapboxProtocol(processedStyle.glyphs)) {
|
|
1874
2039
|
processedStyle.glyphs = resolveMapboxUrl(processedStyle.glyphs, accessToken);
|
|
1875
2040
|
}
|
|
1876
|
-
else if (provider === 'mapbox' && processedStyle.glyphs
|
|
2041
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.glyphs)) {
|
|
1877
2042
|
processedStyle.glyphs = normalizeStyleUrl(processedStyle.glyphs, accessToken);
|
|
1878
2043
|
}
|
|
1879
2044
|
}
|
|
@@ -1913,13 +2078,20 @@ function validateStyleForProvider(style, provider) {
|
|
|
1913
2078
|
// Check for Mapbox-specific requirements
|
|
1914
2079
|
const hasMapboxSources = Object.values(style.sources || {}).some((source) => {
|
|
1915
2080
|
const s = source;
|
|
1916
|
-
return s.url && s.url
|
|
2081
|
+
return !!s.url && isMapboxHost(s.url);
|
|
1917
2082
|
});
|
|
1918
2083
|
if (hasMapboxSources) {
|
|
1919
2084
|
// Check if access token might be needed
|
|
1920
2085
|
const hasAccessToken = Object.values(style.sources || {}).some((source) => {
|
|
1921
2086
|
const s = source;
|
|
1922
|
-
|
|
2087
|
+
if (!s.url)
|
|
2088
|
+
return false;
|
|
2089
|
+
try {
|
|
2090
|
+
return new URL(s.url).searchParams.has('access_token');
|
|
2091
|
+
}
|
|
2092
|
+
catch {
|
|
2093
|
+
return false;
|
|
2094
|
+
}
|
|
1923
2095
|
});
|
|
1924
2096
|
if (!hasAccessToken) {
|
|
1925
2097
|
warnings.push('Mapbox sources detected but no access token found - authentication may be required');
|
|
@@ -1953,14 +2125,15 @@ function patchStyleForOffline(style, downloadId, maxZoom, tileExtension, styleId
|
|
|
1953
2125
|
styleLogger.debug(`Patching source: ${sourceKey}`, source);
|
|
1954
2126
|
if (source.tiles) {
|
|
1955
2127
|
const originalTiles = [...source.tiles];
|
|
1956
|
-
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext
|
|
2128
|
+
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext.
|
|
2129
|
+
// Extension extraction goes through the shared extractTileExtensionFromUrl
|
|
2130
|
+
// helper so the patched URL's extension matches what tileService used when
|
|
2131
|
+
// storing — otherwise Mapbox v4 tile URLs (`{y}.vector.pbf`) produced a
|
|
2132
|
+
// stored key under `.pbf` but a patched URL with `.vector`, forcing
|
|
2133
|
+
// idbFetchHandler to fall through its pbf/mvt/png/jpg/webp fallback loop
|
|
2134
|
+
// on every tile.
|
|
1957
2135
|
source.tiles = source.tiles.map((url) => {
|
|
1958
|
-
|
|
1959
|
-
let ext = tileExtension;
|
|
1960
|
-
if (!ext) {
|
|
1961
|
-
const extMatch = url.match(/\{z\}\/\{x\}\/\{y\}\.(\w+)/);
|
|
1962
|
-
ext = extMatch ? extMatch[1] : 'pbf';
|
|
1963
|
-
}
|
|
2136
|
+
const ext = tileExtension ?? extractTileExtensionFromUrl(url);
|
|
1964
2137
|
return `idb://${downloadId}/tile/${sourceKey}/{z}/{x}/{y}.${ext}`;
|
|
1965
2138
|
});
|
|
1966
2139
|
styleLogger.debug(`Patched tiles for ${sourceKey} with extension .${tileExtension || 'pbf'}:`, {
|
|
@@ -2033,14 +2206,29 @@ function patchStyleForOffline(style, downloadId, maxZoom, tileExtension, styleId
|
|
|
2033
2206
|
});
|
|
2034
2207
|
}
|
|
2035
2208
|
}
|
|
2036
|
-
// Patch top-level models (Mapbox Standard 3D
|
|
2209
|
+
// Patch top-level models (Mapbox Standard 3D trees / wind turbines).
|
|
2210
|
+
// Two shapes exist in the wild:
|
|
2211
|
+
// - Mapbox Standard: `{ "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb" }` (string values)
|
|
2212
|
+
// - Older/generic: `{ "name": { "uri": "mapbox://..." } }` (object values)
|
|
2213
|
+
// Models are keyed on the style ID (like sprites) so they can be shared
|
|
2214
|
+
// across regions.
|
|
2037
2215
|
if (style.models) {
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2216
|
+
const modelBaseId = styleId || downloadId;
|
|
2217
|
+
const models = style.models;
|
|
2218
|
+
let patchedCount = 0;
|
|
2219
|
+
for (const [modelId, value] of Object.entries(models)) {
|
|
2220
|
+
if (typeof value === 'string') {
|
|
2221
|
+
models[modelId] = `idb://${modelBaseId}/model/${modelId}`;
|
|
2222
|
+
patchedCount++;
|
|
2223
|
+
}
|
|
2224
|
+
else if (value && typeof value === 'object' && 'uri' in value && value.uri) {
|
|
2225
|
+
value.uri = `idb://${modelBaseId}/model/${modelId}`;
|
|
2226
|
+
patchedCount++;
|
|
2041
2227
|
}
|
|
2042
2228
|
}
|
|
2043
|
-
|
|
2229
|
+
if (patchedCount > 0) {
|
|
2230
|
+
styleLogger.debug(`Patched ${patchedCount} model URIs (styleId: ${modelBaseId})`);
|
|
2231
|
+
}
|
|
2044
2232
|
}
|
|
2045
2233
|
styleLogger.debug(`Final patched style:`, style);
|
|
2046
2234
|
return style;
|
|
@@ -2608,12 +2796,22 @@ function convertStyleForServiceWorker(style) {
|
|
|
2608
2796
|
});
|
|
2609
2797
|
}
|
|
2610
2798
|
}
|
|
2611
|
-
// Convert models
|
|
2799
|
+
// Convert models. Two shapes in the wild:
|
|
2800
|
+
// - Mapbox Standard: `{ name: "idb://..." }` (string value)
|
|
2801
|
+
// - Older/generic: `{ name: { uri: "idb://..." } }` (object value)
|
|
2612
2802
|
if (converted.models) {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2803
|
+
const models = converted.models;
|
|
2804
|
+
for (const modelKey of Object.keys(models)) {
|
|
2805
|
+
const value = models[modelKey];
|
|
2806
|
+
if (typeof value === 'string') {
|
|
2807
|
+
if (value.startsWith('idb://')) {
|
|
2808
|
+
models[modelKey] = replace(value);
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
else if (value && typeof value === 'object') {
|
|
2812
|
+
if (typeof value.uri === 'string' && value.uri.startsWith('idb://')) {
|
|
2813
|
+
value.uri = replace(value.uri);
|
|
2814
|
+
}
|
|
2617
2815
|
}
|
|
2618
2816
|
}
|
|
2619
2817
|
}
|
|
@@ -2813,10 +3011,8 @@ async function resolveImportsRecursive(style, accessToken, visited, depth, maxRe
|
|
|
2813
3011
|
if (typeof prefixedLayer.source === 'string') {
|
|
2814
3012
|
prefixedLayer.source = `${importId}/${prefixedLayer.source}`;
|
|
2815
3013
|
}
|
|
2816
|
-
// Resolve ["config", "key"] expressions using schema defaults and import overrides
|
|
2817
|
-
|
|
2818
|
-
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2819
|
-
}
|
|
3014
|
+
// Resolve ["config", "key"] expressions using schema defaults and import overrides.
|
|
3015
|
+
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2820
3016
|
flattenedLayers.push(prefixedLayer);
|
|
2821
3017
|
}
|
|
2822
3018
|
}
|
|
@@ -2854,8 +3050,48 @@ async function resolveImportsRecursive(style, accessToken, visited, depth, maxRe
|
|
|
2854
3050
|
if (!style.models && importedModels) {
|
|
2855
3051
|
style.models = importedModels;
|
|
2856
3052
|
}
|
|
3053
|
+
// Rewrite indoor-only expressions so the flattened style validates without
|
|
3054
|
+
// the `imports` wrapper at render time — see sanitizeIndoorExpressions.
|
|
3055
|
+
sanitizeIndoorExpressions(style);
|
|
2857
3056
|
return style;
|
|
2858
3057
|
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Rewrite indoor-only expressions in a style's layers to their outdoor no-op
|
|
3060
|
+
* constants. See the in-line comment in `resolveValue` for why this is needed
|
|
3061
|
+
* for Mapbox Standard when the `imports` wrapper is stripped.
|
|
3062
|
+
*
|
|
3063
|
+
* Safe to call multiple times and on already-downloaded stored styles — the
|
|
3064
|
+
* rewrites are idempotent (after the first pass there are no more
|
|
3065
|
+
* `is-active-floor` / `floor-level` expressions to rewrite).
|
|
3066
|
+
*/
|
|
3067
|
+
function sanitizeIndoorExpressions(style) {
|
|
3068
|
+
const layers = style.layers;
|
|
3069
|
+
if (!Array.isArray(layers))
|
|
3070
|
+
return;
|
|
3071
|
+
for (const layer of layers) {
|
|
3072
|
+
if (layer && typeof layer === 'object') {
|
|
3073
|
+
rewriteIndoor(layer);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
function rewriteIndoor(obj) {
|
|
3078
|
+
for (const key of Object.keys(obj)) {
|
|
3079
|
+
obj[key] = rewriteIndoorValue(obj[key]);
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
function rewriteIndoorValue(value) {
|
|
3083
|
+
if (!Array.isArray(value)) {
|
|
3084
|
+
if (value && typeof value === 'object' && !ArrayBuffer.isView(value)) {
|
|
3085
|
+
rewriteIndoor(value);
|
|
3086
|
+
}
|
|
3087
|
+
return value;
|
|
3088
|
+
}
|
|
3089
|
+
if (value[0] === 'is-active-floor')
|
|
3090
|
+
return false;
|
|
3091
|
+
if (value[0] === 'floor-level' && value.length === 1)
|
|
3092
|
+
return 0;
|
|
3093
|
+
return value.map(rewriteIndoorValue);
|
|
3094
|
+
}
|
|
2859
3095
|
/**
|
|
2860
3096
|
* Deep clone a plain object/array (JSON-safe values only).
|
|
2861
3097
|
*/
|
|
@@ -3021,6 +3257,40 @@ function mergeSprites(outer, imported) {
|
|
|
3021
3257
|
return result;
|
|
3022
3258
|
}
|
|
3023
3259
|
|
|
3260
|
+
const DEFAULT_WASM_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.14.1/dist/';
|
|
3261
|
+
let currentConfig = {};
|
|
3262
|
+
let sqlJsPromise = null;
|
|
3263
|
+
/**
|
|
3264
|
+
* Override how `sql.js` loads its WebAssembly. Call once before any MBTiles
|
|
3265
|
+
* import/export is invoked. Resets any cached init.
|
|
3266
|
+
*/
|
|
3267
|
+
function configureSqlJs(config) {
|
|
3268
|
+
currentConfig = { ...config };
|
|
3269
|
+
sqlJsPromise = null;
|
|
3270
|
+
}
|
|
3271
|
+
/**
|
|
3272
|
+
* Lazily initialise `sql.js`. The underlying module is loaded via dynamic
|
|
3273
|
+
* `import()` so it only ships with bundles that actually call MBTiles code.
|
|
3274
|
+
*/
|
|
3275
|
+
async function getSqlJs() {
|
|
3276
|
+
if (sqlJsPromise)
|
|
3277
|
+
return sqlJsPromise;
|
|
3278
|
+
sqlJsPromise = (async () => {
|
|
3279
|
+
const mod = (await import('sql.js'));
|
|
3280
|
+
const initSqlJs = mod.default;
|
|
3281
|
+
const options = {};
|
|
3282
|
+
if (currentConfig.wasmBinary) {
|
|
3283
|
+
options.wasmBinary = currentConfig.wasmBinary;
|
|
3284
|
+
}
|
|
3285
|
+
else {
|
|
3286
|
+
const base = currentConfig.wasmUrl ?? DEFAULT_WASM_URL;
|
|
3287
|
+
options.locateFile = (file) => base.endsWith('/') ? `${base}${file}` : `${base}/${file}`;
|
|
3288
|
+
}
|
|
3289
|
+
return initSqlJs(options);
|
|
3290
|
+
})();
|
|
3291
|
+
return sqlJsPromise;
|
|
3292
|
+
}
|
|
3293
|
+
|
|
3024
3294
|
const fontLogger = logger.scope('FontService');
|
|
3025
3295
|
class FontService {
|
|
3026
3296
|
db = dbPromise;
|
|
@@ -3204,15 +3474,23 @@ class FontService {
|
|
|
3204
3474
|
},
|
|
3205
3475
|
};
|
|
3206
3476
|
}
|
|
3207
|
-
|
|
3477
|
+
/**
|
|
3478
|
+
* Delete fonts older than `maxAge` days. When `options.styleId` is
|
|
3479
|
+
* provided, only fonts belonging to that style (per the delimiter-aware
|
|
3480
|
+
* `resourceKeyBelongsToStyle` match) are eligible — callers relying on
|
|
3481
|
+
* a styleId filter previously got a silent full-store wipe.
|
|
3482
|
+
*/
|
|
3483
|
+
async cleanupOldFonts(maxAge = 30, options = {}) {
|
|
3208
3484
|
const db = await this.db;
|
|
3209
3485
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3486
|
+
const { styleId } = options;
|
|
3210
3487
|
const tx = db.transaction(['fonts'], 'readwrite');
|
|
3211
3488
|
let deletedCount = 0;
|
|
3212
3489
|
let cursor = await tx.objectStore('fonts').openCursor();
|
|
3213
3490
|
while (cursor) {
|
|
3214
3491
|
const fontEntry = cursor.value;
|
|
3215
|
-
|
|
3492
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(fontEntry.key, styleId);
|
|
3493
|
+
if (belongs && fontEntry.lastModified < cutoffTime) {
|
|
3216
3494
|
await cursor.delete();
|
|
3217
3495
|
deletedCount++;
|
|
3218
3496
|
}
|
|
@@ -3378,7 +3656,7 @@ const fontService = new FontService();
|
|
|
3378
3656
|
const downloadFonts = (fontUrls, styleName, options) => fontService.downloadFonts(fontUrls, styleName, options);
|
|
3379
3657
|
const getFontStats = () => fontService.getFontStats();
|
|
3380
3658
|
const getFontAnalytics = () => fontService.getFontAnalytics();
|
|
3381
|
-
const cleanupOldFonts = (maxAge) => fontService.cleanupOldFonts(maxAge);
|
|
3659
|
+
const cleanupOldFonts = (maxAge, options) => fontService.cleanupOldFonts(maxAge, options);
|
|
3382
3660
|
const verifyAndRepairFonts = () => fontService.verifyAndRepairFonts();
|
|
3383
3661
|
|
|
3384
3662
|
const spriteLogger = logger.scope('SpriteService');
|
|
@@ -3689,19 +3967,24 @@ class SpriteService {
|
|
|
3689
3967
|
};
|
|
3690
3968
|
}
|
|
3691
3969
|
/**
|
|
3692
|
-
*
|
|
3970
|
+
* Remove sprites older than the specified age. When `options.styleId` is
|
|
3971
|
+
* provided, only sprites belonging to that style (per
|
|
3972
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
3693
3973
|
* @param maxAge - Maximum age in days (default: 30)
|
|
3974
|
+
* @param options.styleId - Optional style filter; omit to scan all styles
|
|
3694
3975
|
* @returns Promise resolving to number of deleted sprites
|
|
3695
3976
|
*/
|
|
3696
|
-
async cleanupOldSprites(maxAge = 30) {
|
|
3977
|
+
async cleanupOldSprites(maxAge = 30, options = {}) {
|
|
3697
3978
|
const db = await this.db;
|
|
3698
3979
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3980
|
+
const { styleId } = options;
|
|
3699
3981
|
const tx = db.transaction(['sprites'], 'readwrite');
|
|
3700
3982
|
let deletedCount = 0;
|
|
3701
3983
|
let cursor = await tx.objectStore('sprites').openCursor();
|
|
3702
3984
|
while (cursor) {
|
|
3703
3985
|
const spriteEntry = cursor.value;
|
|
3704
|
-
|
|
3986
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(spriteEntry.key, styleId);
|
|
3987
|
+
if (belongs && spriteEntry.lastModified < cutoffTime) {
|
|
3705
3988
|
await cursor.delete();
|
|
3706
3989
|
deletedCount++;
|
|
3707
3990
|
}
|
|
@@ -3855,7 +4138,7 @@ const spriteService = new SpriteService();
|
|
|
3855
4138
|
const downloadSprites = (spriteUrls, styleName, options) => spriteService.downloadSprites(spriteUrls, styleName, options);
|
|
3856
4139
|
const getSpriteStats = () => spriteService.getSpriteStats();
|
|
3857
4140
|
const getSpriteAnalytics = () => spriteService.getSpriteAnalytics();
|
|
3858
|
-
const cleanupOldSprites = (maxAge) => spriteService.cleanupOldSprites(maxAge);
|
|
4141
|
+
const cleanupOldSprites = (maxAge, options) => spriteService.cleanupOldSprites(maxAge, options);
|
|
3859
4142
|
const verifyAndRepairSprites = () => spriteService.verifyAndRepairSprites();
|
|
3860
4143
|
|
|
3861
4144
|
var spriteService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -4934,7 +5217,15 @@ class RegionService {
|
|
|
4934
5217
|
deletedSprites++;
|
|
4935
5218
|
}
|
|
4936
5219
|
}
|
|
4937
|
-
|
|
5220
|
+
let deletedModels = 0;
|
|
5221
|
+
const modelTx = db.transaction('models', 'readwrite');
|
|
5222
|
+
for await (const cursor of modelTx.store) {
|
|
5223
|
+
if (resourceKeyBelongsToStyle(cursor.value.key, styleId)) {
|
|
5224
|
+
await cursor.delete();
|
|
5225
|
+
deletedModels++;
|
|
5226
|
+
}
|
|
5227
|
+
}
|
|
5228
|
+
regionLogger$1.info(`Deleted style resources: ${deletedFonts} fonts, ${deletedGlyphs} glyphs, ${deletedSprites} sprites, ${deletedModels} models`);
|
|
4938
5229
|
}
|
|
4939
5230
|
/**
|
|
4940
5231
|
* Delete all tiles for a style
|
|
@@ -5042,7 +5333,7 @@ class RegionService {
|
|
|
5042
5333
|
if (!region.styleUrl) {
|
|
5043
5334
|
throw new Error('Region must have a styleUrl');
|
|
5044
5335
|
}
|
|
5045
|
-
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, glyphRanges, tileOptions, } = options;
|
|
5336
|
+
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, skipModels = false, glyphRanges, tileOptions, } = options;
|
|
5046
5337
|
const emit = (phase, completed, total, message) => {
|
|
5047
5338
|
if (!onProgress)
|
|
5048
5339
|
return;
|
|
@@ -5097,8 +5388,13 @@ class RegionService {
|
|
|
5097
5388
|
const spriteSources = normalizeSpriteProperty(originalSpriteUrl);
|
|
5098
5389
|
if (spriteSources.length > 0) {
|
|
5099
5390
|
const { downloadSprites } = await Promise.resolve().then(function () { return spriteService$1; });
|
|
5100
|
-
|
|
5101
|
-
|
|
5391
|
+
// Standard four sprite variants. For Mapbox Standard, an `iconset.pbf`
|
|
5392
|
+
// sibling is also served under the same /styles/v1/.../<hash>/ path
|
|
5393
|
+
// — we detect that case per-source below and append it to the list.
|
|
5394
|
+
const baseSuffixes = ['.json', '.png', '@2x.json', '@2x.png'];
|
|
5395
|
+
// Estimate total files assuming iconset is always included (the actual
|
|
5396
|
+
// number may be smaller; the emit helper clamps progress to total).
|
|
5397
|
+
const totalFiles = spriteSources.length * (baseSuffixes.length + 1);
|
|
5102
5398
|
let completed = 0;
|
|
5103
5399
|
emit('sprites', 0, totalFiles, 'Downloading sprites');
|
|
5104
5400
|
for (const source of spriteSources) {
|
|
@@ -5107,9 +5403,37 @@ class RegionService {
|
|
|
5107
5403
|
spriteBase = resolveMapboxUrl(spriteBase, effectiveAccessToken);
|
|
5108
5404
|
}
|
|
5109
5405
|
const qIndex = spriteBase.indexOf('?');
|
|
5110
|
-
const
|
|
5111
|
-
|
|
5112
|
-
|
|
5406
|
+
const suffixes = [...baseSuffixes];
|
|
5407
|
+
// Mapbox Standard serves an iconset.pbf alongside the sprite under
|
|
5408
|
+
// /styles/v1/{owner}/{style}/{hash}/sprite → the sibling file is
|
|
5409
|
+
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5410
|
+
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5411
|
+
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5412
|
+
let isMapboxStandardSprite = false;
|
|
5413
|
+
try {
|
|
5414
|
+
const parsed = new URL(pathWithoutQuery);
|
|
5415
|
+
isMapboxStandardSprite =
|
|
5416
|
+
parsed.hostname === 'api.mapbox.com' &&
|
|
5417
|
+
parsed.pathname.startsWith('/styles/v1/') &&
|
|
5418
|
+
parsed.pathname.endsWith('/sprite');
|
|
5419
|
+
}
|
|
5420
|
+
catch {
|
|
5421
|
+
// Non-URL sprite base (e.g. relative); not a Mapbox Standard sprite.
|
|
5422
|
+
}
|
|
5423
|
+
if (isMapboxStandardSprite) {
|
|
5424
|
+
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5425
|
+
suffixes.push('__ICONSET__');
|
|
5426
|
+
}
|
|
5427
|
+
const spriteUrls = suffixes.map(suffix => {
|
|
5428
|
+
if (suffix === '__ICONSET__') {
|
|
5429
|
+
// Replace trailing `sprite` with `iconset.pbf`, preserving query.
|
|
5430
|
+
const base = pathWithoutQuery.replace(/sprite$/, 'iconset.pbf');
|
|
5431
|
+
return qIndex !== -1 ? base + spriteBase.slice(qIndex) : base;
|
|
5432
|
+
}
|
|
5433
|
+
return qIndex !== -1
|
|
5434
|
+
? spriteBase.slice(0, qIndex) + suffix + spriteBase.slice(qIndex)
|
|
5435
|
+
: spriteBase + suffix;
|
|
5436
|
+
});
|
|
5113
5437
|
try {
|
|
5114
5438
|
const result = await downloadSprites(spriteUrls, styleId, {
|
|
5115
5439
|
enableValidation: true,
|
|
@@ -5155,7 +5479,42 @@ class RegionService {
|
|
|
5155
5479
|
}
|
|
5156
5480
|
}
|
|
5157
5481
|
}
|
|
5158
|
-
// 4.
|
|
5482
|
+
// 4. Models — Mapbox Standard's `style.models` references 3D tree /
|
|
5483
|
+
// turbine .glb assets. Two value shapes exist in the wild
|
|
5484
|
+
// (plain string or `{ uri }`) — we accept both.
|
|
5485
|
+
let modelResult;
|
|
5486
|
+
if (!skipModels && storedStyle.models) {
|
|
5487
|
+
const rawModels = storedStyle.models;
|
|
5488
|
+
const resolved = {};
|
|
5489
|
+
for (const [name, value] of Object.entries(rawModels)) {
|
|
5490
|
+
const uri = typeof value === 'string' ? value : value?.uri;
|
|
5491
|
+
if (!uri)
|
|
5492
|
+
continue;
|
|
5493
|
+
if (uri.startsWith('idb://'))
|
|
5494
|
+
continue; // already patched
|
|
5495
|
+
const httpUrl = isMapboxProtocol(uri) && effectiveAccessToken
|
|
5496
|
+
? resolveMapboxUrl(uri, effectiveAccessToken)
|
|
5497
|
+
: uri;
|
|
5498
|
+
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
|
|
5499
|
+
resolved[name] = httpUrl;
|
|
5500
|
+
}
|
|
5501
|
+
}
|
|
5502
|
+
if (Object.keys(resolved).length > 0) {
|
|
5503
|
+
const { downloadModels } = await Promise.resolve().then(function () { return modelService$1; });
|
|
5504
|
+
emit('models', 0, Object.keys(resolved).length, 'Downloading 3D models');
|
|
5505
|
+
try {
|
|
5506
|
+
modelResult = await downloadModels(resolved, styleId, {
|
|
5507
|
+
onProgress: (progress) => {
|
|
5508
|
+
emit('models', progress.completed, progress.total, 'Downloading 3D models');
|
|
5509
|
+
},
|
|
5510
|
+
});
|
|
5511
|
+
}
|
|
5512
|
+
catch (error) {
|
|
5513
|
+
regionLogger$1.warn('Model download failed (non-fatal):', error);
|
|
5514
|
+
}
|
|
5515
|
+
}
|
|
5516
|
+
}
|
|
5517
|
+
// 5. Tiles — use the stored (source-embedded) style, which still has HTTP tile URLs
|
|
5159
5518
|
const { downloadTiles } = await Promise.resolve().then(function () { return tileService$1; });
|
|
5160
5519
|
const regionForTiles = { ...region, styleId };
|
|
5161
5520
|
emit('tiles', 0, 100, 'Downloading tiles');
|
|
@@ -5168,7 +5527,7 @@ class RegionService {
|
|
|
5168
5527
|
tileOptions?.onProgress?.(progress);
|
|
5169
5528
|
},
|
|
5170
5529
|
});
|
|
5171
|
-
//
|
|
5530
|
+
// 6. Metadata — must run last, since addRegion patches style URLs to idb://.
|
|
5172
5531
|
// Do NOT auto-fill tileExtension from tileResult: that's only the first
|
|
5173
5532
|
// source's extension, and addRegion feeds it to patchStyleForOffline which
|
|
5174
5533
|
// would override ALL sources — breaking mixed raster+vector styles. The
|
|
@@ -5184,6 +5543,7 @@ class RegionService {
|
|
|
5184
5543
|
styleResult,
|
|
5185
5544
|
spriteResults,
|
|
5186
5545
|
glyphResult,
|
|
5546
|
+
modelResult,
|
|
5187
5547
|
tileResult,
|
|
5188
5548
|
};
|
|
5189
5549
|
}
|
|
@@ -6322,10 +6682,15 @@ class TileService {
|
|
|
6322
6682
|
tilesLength: config.tiles ? config.tiles.length : 0,
|
|
6323
6683
|
url: config.url,
|
|
6324
6684
|
});
|
|
6325
|
-
// Handle tile-based sources (vector, raster, raster-dem, batched-model
|
|
6685
|
+
// Handle tile-based sources (vector, raster, raster-dem, batched-model,
|
|
6686
|
+
// raster-array). `raster-array` is used by Mapbox Standard for layers
|
|
6687
|
+
// like `mapbox-landmarks` (mapbox.mapbox-landmark-icons-v1) — the tiles
|
|
6688
|
+
// are fetched from the same /v4/ endpoint as other tilesets, so the
|
|
6689
|
+
// TileJSON resolution path below handles them uniformly.
|
|
6326
6690
|
if (config.type === 'vector' ||
|
|
6327
6691
|
config.type === 'raster' ||
|
|
6328
6692
|
config.type === 'raster-dem' ||
|
|
6693
|
+
config.type === 'raster-array' ||
|
|
6329
6694
|
config.type === 'batched-model') {
|
|
6330
6695
|
// Handle direct tile URLs in the source config
|
|
6331
6696
|
if (config.tiles && Array.isArray(config.tiles) && config.tiles.length > 0) {
|
|
@@ -6550,8 +6915,7 @@ class TileService {
|
|
|
6550
6915
|
}
|
|
6551
6916
|
}
|
|
6552
6917
|
extractExtension(template) {
|
|
6553
|
-
|
|
6554
|
-
return extMatch ? extMatch[1] : 'pbf';
|
|
6918
|
+
return extractTileExtensionFromUrl(template);
|
|
6555
6919
|
}
|
|
6556
6920
|
selectTileTemplate(templates, coord) {
|
|
6557
6921
|
if (templates.length === 1) {
|
|
@@ -6933,14 +7297,21 @@ class GlyphService {
|
|
|
6933
7297
|
},
|
|
6934
7298
|
};
|
|
6935
7299
|
}
|
|
6936
|
-
|
|
7300
|
+
/**
|
|
7301
|
+
* Remove glyphs older than the specified age. When `options.styleId` is
|
|
7302
|
+
* provided, only glyphs belonging to that style (per
|
|
7303
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
7304
|
+
*/
|
|
7305
|
+
async cleanupOldGlyphs(maxAge = 30, options = {}) {
|
|
6937
7306
|
const db = await this.db;
|
|
6938
7307
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7308
|
+
const { styleId } = options;
|
|
6939
7309
|
let deletedCount = 0;
|
|
6940
7310
|
const tx = db.transaction('glyphs', 'readwrite');
|
|
6941
7311
|
for await (const cursor of tx.store) {
|
|
6942
7312
|
const glyphEntry = cursor.value;
|
|
6943
|
-
|
|
7313
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(glyphEntry.key, styleId);
|
|
7314
|
+
if (belongs && glyphEntry.lastModified < cutoffTime) {
|
|
6944
7315
|
await cursor.delete();
|
|
6945
7316
|
deletedCount++;
|
|
6946
7317
|
}
|
|
@@ -7043,7 +7414,7 @@ const downloadGlyphs = (glyphUrl, fontstacks, styleName, ranges, options) => gly
|
|
|
7043
7414
|
const loadGlyphs = (fontstack, ranges, styleName) => glyphService.loadGlyphs(fontstack, ranges, styleName);
|
|
7044
7415
|
const getGlyphStats = () => glyphService.getGlyphStats();
|
|
7045
7416
|
const getGlyphAnalytics = () => glyphService.getGlyphAnalytics();
|
|
7046
|
-
const cleanupOldGlyphs = (maxAge) => glyphService.cleanupOldGlyphs(maxAge);
|
|
7417
|
+
const cleanupOldGlyphs = (maxAge, options) => glyphService.cleanupOldGlyphs(maxAge, options);
|
|
7047
7418
|
const verifyAndRepairGlyphs = () => glyphService.verifyAndRepairGlyphs();
|
|
7048
7419
|
|
|
7049
7420
|
var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -7058,12 +7429,207 @@ var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
|
7058
7429
|
verifyAndRepairGlyphs: verifyAndRepairGlyphs
|
|
7059
7430
|
});
|
|
7060
7431
|
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7432
|
+
const modelLogger = logger.scope('ModelService');
|
|
7433
|
+
/**
|
|
7434
|
+
* Build the storage key for a model. Kept consistent with sprite/glyph
|
|
7435
|
+
* conventions: `{styleId}::model::{modelName}`.
|
|
7436
|
+
*/
|
|
7437
|
+
function modelKey(styleId, modelName) {
|
|
7438
|
+
return `${styleId}::model::${modelName}`;
|
|
7439
|
+
}
|
|
7440
|
+
/** True when the given key belongs to the given styleId's model store prefix. */
|
|
7441
|
+
function modelKeyBelongsToStyle(key, styleId) {
|
|
7442
|
+
return key.startsWith(`${styleId}::model::`);
|
|
7443
|
+
}
|
|
7444
|
+
/**
|
|
7445
|
+
* Service for downloading, storing, and serving Mapbox 3D model (.glb) files.
|
|
7446
|
+
*
|
|
7447
|
+
* Mapbox Standard declares 32 models at the top of `style.models`:
|
|
7448
|
+
*
|
|
7449
|
+
* ```json
|
|
7450
|
+
* {
|
|
7451
|
+
* "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb",
|
|
7452
|
+
* ...
|
|
7453
|
+
* }
|
|
7454
|
+
* ```
|
|
7455
|
+
*
|
|
7456
|
+
* `model` layers (e.g. `trees`, `wind-turbine-towers`) pick one by name at
|
|
7457
|
+
* render time. For offline use each referenced URL is fetched and stored
|
|
7458
|
+
* here, and `patchStyleForOffline` rewrites the dictionary entries to
|
|
7459
|
+
* `idb://{styleId}/model/{name}` URLs.
|
|
7460
|
+
*/
|
|
7461
|
+
class ModelService {
|
|
7462
|
+
db = dbPromise;
|
|
7463
|
+
/**
|
|
7464
|
+
* Download the set of models referenced by `style.models` for one style.
|
|
7465
|
+
*
|
|
7466
|
+
* @param models `{ modelName: resolvedHttpUrl }` — URLs must already be
|
|
7467
|
+
* resolved (mapbox:// URLs should be resolved by the caller).
|
|
7468
|
+
* @param styleId The owning style's key.
|
|
7469
|
+
*/
|
|
7470
|
+
async downloadModels(models, styleId, options = {}) {
|
|
7471
|
+
const db = await this.db;
|
|
7472
|
+
const { onProgress, batchSize = 4, maxRetries = 3, skipExisting = true, timeoutMs = 30000, } = options;
|
|
7473
|
+
const entries = Object.entries(models);
|
|
7474
|
+
const progressTracker = createProgressTracker(entries.length);
|
|
7475
|
+
const result = {
|
|
7476
|
+
totalModels: entries.length,
|
|
7477
|
+
downloadedModels: 0,
|
|
7478
|
+
skippedModels: 0,
|
|
7479
|
+
failedModels: 0,
|
|
7480
|
+
totalSize: 0,
|
|
7481
|
+
errors: [],
|
|
7482
|
+
};
|
|
7483
|
+
const emit = () => onProgress?.(progressTracker.getProgress());
|
|
7484
|
+
emit();
|
|
7485
|
+
if (entries.length === 0)
|
|
7486
|
+
return result;
|
|
7487
|
+
// Pre-compute existing keys for skipExisting
|
|
7488
|
+
const existingKeys = new Set();
|
|
7489
|
+
if (skipExisting) {
|
|
7490
|
+
const tx = db.transaction('models', 'readonly');
|
|
7491
|
+
for await (const cursor of tx.store) {
|
|
7492
|
+
existingKeys.add(cursor.value.key);
|
|
7493
|
+
}
|
|
7494
|
+
}
|
|
7495
|
+
await processBatch(entries, async ([modelName, url]) => {
|
|
7496
|
+
const key = modelKey(styleId, modelName);
|
|
7497
|
+
const label = `${styleId}::${modelName}`;
|
|
7498
|
+
if (skipExisting && existingKeys.has(key)) {
|
|
7499
|
+
result.skippedModels++;
|
|
7500
|
+
progressTracker.update(1, label);
|
|
7501
|
+
emit();
|
|
7502
|
+
return;
|
|
7503
|
+
}
|
|
7504
|
+
try {
|
|
7505
|
+
const response = await fetchResourceWithRetry(url, {
|
|
7506
|
+
retries: maxRetries,
|
|
7507
|
+
timeout: timeoutMs,
|
|
7508
|
+
proxyType: 'tiles',
|
|
7509
|
+
});
|
|
7510
|
+
if (response.type === 'json') {
|
|
7511
|
+
throw new Error('Unexpected JSON response for model');
|
|
7512
|
+
}
|
|
7513
|
+
const data = response.data;
|
|
7514
|
+
const contentType = ('contentType' in response && response.contentType) || 'model/gltf-binary';
|
|
7515
|
+
const entry = {
|
|
7516
|
+
key,
|
|
7517
|
+
data,
|
|
7518
|
+
contentType,
|
|
7519
|
+
size: data.byteLength,
|
|
7520
|
+
url,
|
|
7521
|
+
styleId,
|
|
7522
|
+
modelName,
|
|
7523
|
+
lastModified: Date.now(),
|
|
7524
|
+
downloadedAt: new Date().toISOString(),
|
|
7525
|
+
expires: response.expires,
|
|
7526
|
+
};
|
|
7527
|
+
await db.put('models', entry);
|
|
7528
|
+
result.downloadedModels++;
|
|
7529
|
+
result.totalSize += data.byteLength;
|
|
7530
|
+
progressTracker.update(1, label);
|
|
7531
|
+
}
|
|
7532
|
+
catch (err) {
|
|
7533
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7534
|
+
result.failedModels++;
|
|
7535
|
+
result.errors.push({ url, error: message });
|
|
7536
|
+
modelLogger.warn(`Failed to download model "${modelName}" from ${url}:`, err);
|
|
7537
|
+
progressTracker.update(1, label, message);
|
|
7538
|
+
}
|
|
7539
|
+
emit();
|
|
7540
|
+
}, { batchSize });
|
|
7541
|
+
modelLogger.info(`Models downloaded for style ${styleId}: ${result.downloadedModels} new, ${result.skippedModels} skipped, ${result.failedModels} failed`);
|
|
7542
|
+
return result;
|
|
7065
7543
|
}
|
|
7066
|
-
|
|
7544
|
+
/** Retrieve a single model by `{styleId, modelName}`. */
|
|
7545
|
+
async getModel(styleId, modelName) {
|
|
7546
|
+
const db = await this.db;
|
|
7547
|
+
return db.get('models', modelKey(styleId, modelName));
|
|
7548
|
+
}
|
|
7549
|
+
/** Aggregate stats across all stored models. */
|
|
7550
|
+
async getModelStats() {
|
|
7551
|
+
const db = await this.db;
|
|
7552
|
+
const stats = {
|
|
7553
|
+
count: 0,
|
|
7554
|
+
totalSize: 0,
|
|
7555
|
+
averageSize: 0,
|
|
7556
|
+
models: [],
|
|
7557
|
+
modelsByStyle: {},
|
|
7558
|
+
};
|
|
7559
|
+
const tx = db.transaction('models', 'readonly');
|
|
7560
|
+
for await (const cursor of tx.store) {
|
|
7561
|
+
const m = cursor.value;
|
|
7562
|
+
stats.count++;
|
|
7563
|
+
stats.totalSize += m.size;
|
|
7564
|
+
stats.models.push({ name: m.modelName, size: m.size, lastModified: m.lastModified });
|
|
7565
|
+
stats.modelsByStyle[m.styleId] = (stats.modelsByStyle[m.styleId] ?? 0) + 1;
|
|
7566
|
+
}
|
|
7567
|
+
stats.averageSize = stats.count > 0 ? stats.totalSize / stats.count : 0;
|
|
7568
|
+
return stats;
|
|
7569
|
+
}
|
|
7570
|
+
/**
|
|
7571
|
+
* Delete models older than `maxAge` days. Defaults to 30.
|
|
7572
|
+
*/
|
|
7573
|
+
async cleanupOldModels(maxAge = 30) {
|
|
7574
|
+
const db = await this.db;
|
|
7575
|
+
const cutoff = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7576
|
+
let deleted = 0;
|
|
7577
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7578
|
+
for await (const cursor of tx.store) {
|
|
7579
|
+
if (cursor.value.lastModified < cutoff) {
|
|
7580
|
+
await cursor.delete();
|
|
7581
|
+
deleted++;
|
|
7582
|
+
}
|
|
7583
|
+
}
|
|
7584
|
+
return deleted;
|
|
7585
|
+
}
|
|
7586
|
+
/**
|
|
7587
|
+
* Basic integrity check: remove entries with empty/missing data.
|
|
7588
|
+
*/
|
|
7589
|
+
async verifyAndRepairModels() {
|
|
7590
|
+
const db = await this.db;
|
|
7591
|
+
let verified = 0;
|
|
7592
|
+
let removed = 0;
|
|
7593
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7594
|
+
for await (const cursor of tx.store) {
|
|
7595
|
+
const m = cursor.value;
|
|
7596
|
+
if (!m.data || m.data.byteLength === 0) {
|
|
7597
|
+
await cursor.delete();
|
|
7598
|
+
removed++;
|
|
7599
|
+
}
|
|
7600
|
+
else {
|
|
7601
|
+
verified++;
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
return { verified, repaired: 0, removed };
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
// Singleton + convenience exports, matching other service modules.
|
|
7608
|
+
const modelService = new ModelService();
|
|
7609
|
+
const downloadModels = (models, styleId, options) => modelService.downloadModels(models, styleId, options);
|
|
7610
|
+
const getModel = (styleId, modelName) => modelService.getModel(styleId, modelName);
|
|
7611
|
+
const getModelStats = () => modelService.getModelStats();
|
|
7612
|
+
const cleanupOldModels = (maxAge) => modelService.cleanupOldModels(maxAge);
|
|
7613
|
+
const verifyAndRepairModels = () => modelService.verifyAndRepairModels();
|
|
7614
|
+
|
|
7615
|
+
var modelService$1 = /*#__PURE__*/Object.freeze({
|
|
7616
|
+
__proto__: null,
|
|
7617
|
+
ModelService: ModelService,
|
|
7618
|
+
cleanupOldModels: cleanupOldModels,
|
|
7619
|
+
downloadModels: downloadModels,
|
|
7620
|
+
getModel: getModel,
|
|
7621
|
+
getModelStats: getModelStats,
|
|
7622
|
+
modelKeyBelongsToStyle: modelKeyBelongsToStyle,
|
|
7623
|
+
modelService: modelService,
|
|
7624
|
+
verifyAndRepairModels: verifyAndRepairModels
|
|
7625
|
+
});
|
|
7626
|
+
|
|
7627
|
+
class ResourceService {
|
|
7628
|
+
// Tile Management Methods
|
|
7629
|
+
async downloadTilesWithOptions(region, style, styleId, options = {}) {
|
|
7630
|
+
return downloadTiles(region, style, styleId, options);
|
|
7631
|
+
}
|
|
7632
|
+
async getTileStats(styleId) {
|
|
7067
7633
|
return getTileStats(styleId);
|
|
7068
7634
|
}
|
|
7069
7635
|
async getTileAnalytics(styleId) {
|
|
@@ -7083,8 +7649,7 @@ class ResourceService {
|
|
|
7083
7649
|
return getFontAnalytics();
|
|
7084
7650
|
}
|
|
7085
7651
|
async cleanupOldFonts(styleId, options) {
|
|
7086
|
-
|
|
7087
|
-
return cleanupOldFonts(maxAge);
|
|
7652
|
+
return cleanupOldFonts(options?.maxAge, { styleId });
|
|
7088
7653
|
}
|
|
7089
7654
|
async verifyAndRepairFonts() {
|
|
7090
7655
|
return verifyAndRepairFonts();
|
|
@@ -7100,8 +7665,7 @@ class ResourceService {
|
|
|
7100
7665
|
return getSpriteAnalytics();
|
|
7101
7666
|
}
|
|
7102
7667
|
async cleanupOldSprites(styleId, options) {
|
|
7103
|
-
|
|
7104
|
-
return cleanupOldSprites(maxAge);
|
|
7668
|
+
return cleanupOldSprites(options?.maxAge, { styleId });
|
|
7105
7669
|
}
|
|
7106
7670
|
async verifyAndRepairSprites() {
|
|
7107
7671
|
return verifyAndRepairSprites();
|
|
@@ -7120,12 +7684,24 @@ class ResourceService {
|
|
|
7120
7684
|
return loadGlyphs(fontstack, ranges, styleId);
|
|
7121
7685
|
}
|
|
7122
7686
|
async cleanupOldGlyphs(styleId, options) {
|
|
7123
|
-
|
|
7124
|
-
return cleanupOldGlyphs(maxAge);
|
|
7687
|
+
return cleanupOldGlyphs(options?.maxAge, { styleId });
|
|
7125
7688
|
}
|
|
7126
7689
|
async verifyAndRepairGlyphs() {
|
|
7127
7690
|
return verifyAndRepairGlyphs();
|
|
7128
7691
|
}
|
|
7692
|
+
// 3D Model Management Methods
|
|
7693
|
+
async downloadModelsWithOptions(models, styleId, options = {}) {
|
|
7694
|
+
return downloadModels(models, styleId, options);
|
|
7695
|
+
}
|
|
7696
|
+
async getModelStats() {
|
|
7697
|
+
return getModelStats();
|
|
7698
|
+
}
|
|
7699
|
+
async cleanupOldModels(options) {
|
|
7700
|
+
return cleanupOldModels(options?.maxAge);
|
|
7701
|
+
}
|
|
7702
|
+
async verifyAndRepairModels() {
|
|
7703
|
+
return verifyAndRepairModels();
|
|
7704
|
+
}
|
|
7129
7705
|
}
|
|
7130
7706
|
|
|
7131
7707
|
// The underlying stats functions already iterate every entry in their
|
|
@@ -7223,6 +7799,88 @@ class AnalyticsService {
|
|
|
7223
7799
|
}
|
|
7224
7800
|
}
|
|
7225
7801
|
|
|
7802
|
+
/**
|
|
7803
|
+
* MBTiles uses TMS tile_row ordering; our storage uses XYZ y. Flip across
|
|
7804
|
+
* either direction with the same formula.
|
|
7805
|
+
*/
|
|
7806
|
+
function flipY(y, z) {
|
|
7807
|
+
return (1 << z) - 1 - y;
|
|
7808
|
+
}
|
|
7809
|
+
/** Vector tile formats that downstream consumers (QGIS, maplibre-native) expect gzipped. */
|
|
7810
|
+
const VECTOR_FORMATS = new Set(['pbf', 'mvt']);
|
|
7811
|
+
function hasGzipMagic(bytes) {
|
|
7812
|
+
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
7813
|
+
}
|
|
7814
|
+
async function drainReadable(readable) {
|
|
7815
|
+
const reader = readable.getReader();
|
|
7816
|
+
const chunks = [];
|
|
7817
|
+
let total = 0;
|
|
7818
|
+
while (true) {
|
|
7819
|
+
const { done, value } = await reader.read();
|
|
7820
|
+
if (done)
|
|
7821
|
+
break;
|
|
7822
|
+
if (value) {
|
|
7823
|
+
chunks.push(value);
|
|
7824
|
+
total += value.byteLength;
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
const out = new Uint8Array(total);
|
|
7828
|
+
let offset = 0;
|
|
7829
|
+
for (const chunk of chunks) {
|
|
7830
|
+
out.set(chunk, offset);
|
|
7831
|
+
offset += chunk.byteLength;
|
|
7832
|
+
}
|
|
7833
|
+
return out;
|
|
7834
|
+
}
|
|
7835
|
+
async function transformBytes(bytes, transform) {
|
|
7836
|
+
const writer = transform.writable.getWriter();
|
|
7837
|
+
// Don't await — the read loop below drives the pipe and we only want
|
|
7838
|
+
// the final bytes, not back-pressure handling for a single chunk.
|
|
7839
|
+
void writer.write(bytes);
|
|
7840
|
+
void writer.close();
|
|
7841
|
+
return drainReadable(transform.readable);
|
|
7842
|
+
}
|
|
7843
|
+
async function gzipBytes(bytes) {
|
|
7844
|
+
return transformBytes(bytes, new CompressionStream('gzip'));
|
|
7845
|
+
}
|
|
7846
|
+
async function gunzipBytes(bytes) {
|
|
7847
|
+
return transformBytes(bytes, new DecompressionStream('gzip'));
|
|
7848
|
+
}
|
|
7849
|
+
/**
|
|
7850
|
+
* Build the MBTiles `json` metadata payload. For vector tiles this is
|
|
7851
|
+
* mandatory for tippecanoe/QGIS/maplibre-native to render — they read
|
|
7852
|
+
* `vector_layers` from here.
|
|
7853
|
+
*
|
|
7854
|
+
* `vector_layers` is inferred from the offline style's vector sources
|
|
7855
|
+
* (populated by the TileJSON expansion step in styleService). Multiple
|
|
7856
|
+
* vector sources are merged; duplicates de-duped by id, first wins.
|
|
7857
|
+
*/
|
|
7858
|
+
function buildVectorJsonMetadata(style, sourceIds) {
|
|
7859
|
+
if (!style || typeof style !== 'object')
|
|
7860
|
+
return null;
|
|
7861
|
+
const sources = style.sources;
|
|
7862
|
+
if (!sources)
|
|
7863
|
+
return null;
|
|
7864
|
+
const merged = [];
|
|
7865
|
+
const seen = new Set();
|
|
7866
|
+
for (const [id, src] of Object.entries(sources)) {
|
|
7867
|
+
if (sourceIds.size > 0 && !sourceIds.has(id))
|
|
7868
|
+
continue;
|
|
7869
|
+
const layers = src?.vector_layers;
|
|
7870
|
+
if (!Array.isArray(layers))
|
|
7871
|
+
continue;
|
|
7872
|
+
for (const layer of layers) {
|
|
7873
|
+
const layerId = typeof layer?.id === 'string' ? layer.id : null;
|
|
7874
|
+
if (!layerId || seen.has(layerId))
|
|
7875
|
+
continue;
|
|
7876
|
+
seen.add(layerId);
|
|
7877
|
+
merged.push(layer);
|
|
7878
|
+
}
|
|
7879
|
+
}
|
|
7880
|
+
if (merged.length === 0)
|
|
7881
|
+
return null;
|
|
7882
|
+
return JSON.stringify({ vector_layers: merged });
|
|
7883
|
+
}
|
|
7226
7884
|
const serviceLogger = logger.scope('ImportExportService');
|
|
7227
7885
|
class ImportExportService {
|
|
7228
7886
|
db = dbPromise;
|
|
@@ -7230,270 +7888,173 @@ class ImportExportService {
|
|
|
7230
7888
|
// No need for initialization since dbPromise is already available
|
|
7231
7889
|
}
|
|
7232
7890
|
/**
|
|
7233
|
-
* Export a
|
|
7891
|
+
* Export region as a real binary MBTiles SQLite file.
|
|
7892
|
+
*
|
|
7893
|
+
* Produces a v1.3-compliant MBTiles archive: `metadata` + `tiles` tables,
|
|
7894
|
+
* with `tile_row` flipped to TMS ordering. The resulting blob can be read
|
|
7895
|
+
* by tippecanoe, QGIS, maplibre-native, etc.
|
|
7234
7896
|
*/
|
|
7235
|
-
async
|
|
7897
|
+
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7236
7898
|
const onProgress = options.onProgress || (() => { });
|
|
7237
7899
|
try {
|
|
7238
7900
|
onProgress({
|
|
7239
7901
|
stage: 'preparing',
|
|
7240
7902
|
percentage: 0,
|
|
7241
|
-
message: 'Preparing export...',
|
|
7903
|
+
message: 'Preparing MBTiles export...',
|
|
7242
7904
|
});
|
|
7243
|
-
// Get region metadata
|
|
7244
7905
|
const region = await this.getRegionMetadata(regionId);
|
|
7245
7906
|
if (!region) {
|
|
7246
7907
|
throw new Error(`Region ${regionId} not found`);
|
|
7247
7908
|
}
|
|
7909
|
+
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7910
|
+
// Pick format: caller override → region.tileExtension → default pbf.
|
|
7911
|
+
// Drives both the metadata row and whether tile bytes get gzipped.
|
|
7912
|
+
const format = String(options.format || region.tileExtension || 'pbf').toLowerCase();
|
|
7913
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
7248
7914
|
onProgress({
|
|
7249
|
-
stage: '
|
|
7250
|
-
percentage:
|
|
7251
|
-
message: '
|
|
7915
|
+
stage: 'processing',
|
|
7916
|
+
percentage: 75,
|
|
7917
|
+
message: isVector ? 'Compressing vector tiles...' : 'Packing SQLite database...',
|
|
7252
7918
|
});
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
|
|
7262
|
-
createdAt: region.created, // StoredRegion uses 'created' not 'createdAt'
|
|
7263
|
-
exportedAt: Date.now(),
|
|
7264
|
-
version: '1.0.0',
|
|
7265
|
-
format: 'json',
|
|
7266
|
-
},
|
|
7267
|
-
style: {},
|
|
7268
|
-
tiles: [],
|
|
7269
|
-
sprites: [],
|
|
7270
|
-
fonts: [],
|
|
7271
|
-
};
|
|
7272
|
-
// Export style if requested
|
|
7273
|
-
if (options.includeStyle !== false) {
|
|
7274
|
-
onProgress({
|
|
7275
|
-
stage: 'exporting',
|
|
7276
|
-
percentage: 20,
|
|
7277
|
-
message: 'Exporting style data...',
|
|
7278
|
-
});
|
|
7279
|
-
exportData.style = await this.exportStyle(regionId);
|
|
7919
|
+
// Gzip vector tiles. Idempotent: skip tiles already gzipped (downloaded
|
|
7920
|
+
// with their original gzip wrapper intact).
|
|
7921
|
+
const packedTiles = [];
|
|
7922
|
+
for (const tile of tiles) {
|
|
7923
|
+
const raw = tile.data instanceof ArrayBuffer
|
|
7924
|
+
? new Uint8Array(tile.data)
|
|
7925
|
+
: new Uint8Array(tile.data);
|
|
7926
|
+
const data = isVector && !hasGzipMagic(raw) ? await gzipBytes(raw) : raw;
|
|
7927
|
+
packedTiles.push({ z: tile.z, x: tile.x, y: tile.y, data });
|
|
7280
7928
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7284
|
-
|
|
7285
|
-
|
|
7286
|
-
|
|
7929
|
+
onProgress({
|
|
7930
|
+
stage: 'processing',
|
|
7931
|
+
percentage: 85,
|
|
7932
|
+
message: 'Packing SQLite database...',
|
|
7933
|
+
});
|
|
7934
|
+
const SQL = await getSqlJs();
|
|
7935
|
+
const db = new SQL.Database();
|
|
7936
|
+
try {
|
|
7937
|
+
db.run(`
|
|
7938
|
+
CREATE TABLE metadata (name TEXT, value TEXT);
|
|
7939
|
+
CREATE TABLE tiles (
|
|
7940
|
+
zoom_level INTEGER NOT NULL,
|
|
7941
|
+
tile_column INTEGER NOT NULL,
|
|
7942
|
+
tile_row INTEGER NOT NULL,
|
|
7943
|
+
tile_data BLOB
|
|
7944
|
+
);
|
|
7945
|
+
CREATE UNIQUE INDEX tile_index ON tiles (zoom_level, tile_column, tile_row);
|
|
7946
|
+
CREATE UNIQUE INDEX name ON metadata (name);
|
|
7947
|
+
`);
|
|
7948
|
+
const [[west, south], [east, north]] = region.bounds;
|
|
7949
|
+
const centerLon = (west + east) / 2;
|
|
7950
|
+
const centerLat = (south + north) / 2;
|
|
7951
|
+
const centerZoom = Math.max(region.minZoom, Math.min(region.maxZoom, Math.round((region.minZoom + region.maxZoom) / 2)));
|
|
7952
|
+
const metadataRows = {
|
|
7953
|
+
name: region.name || region.id,
|
|
7954
|
+
// MBTiles 1.3 type: 'overlay' or 'baselayer'. Baselayer matches how
|
|
7955
|
+
// QGIS treats the dataset (full-coverage map rather than overlay).
|
|
7956
|
+
type: isVector ? 'baselayer' : 'overlay',
|
|
7957
|
+
version: '1.0',
|
|
7958
|
+
description: region.name || region.id,
|
|
7959
|
+
format,
|
|
7960
|
+
bounds: `${west},${south},${east},${north}`,
|
|
7961
|
+
center: `${centerLon},${centerLat},${centerZoom}`,
|
|
7962
|
+
minzoom: String(region.minZoom),
|
|
7963
|
+
maxzoom: String(region.maxZoom),
|
|
7964
|
+
};
|
|
7965
|
+
// For vector tiles, the `json` field with `vector_layers` is required
|
|
7966
|
+
// by the MBTiles 1.3 spec and by every vector tile consumer worth
|
|
7967
|
+
// opening the file in. Derive it from the offline style.
|
|
7968
|
+
if (isVector) {
|
|
7969
|
+
const style = await this.exportStyle(regionId);
|
|
7970
|
+
const sourceIds = new Set(tiles.map(t => t.sourceId).filter(Boolean));
|
|
7971
|
+
const json = buildVectorJsonMetadata(style.style ?? style, sourceIds);
|
|
7972
|
+
if (json)
|
|
7973
|
+
metadataRows.json = json;
|
|
7974
|
+
}
|
|
7975
|
+
for (const [k, v] of Object.entries(options.metadata || {})) {
|
|
7976
|
+
metadataRows[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
|
7977
|
+
}
|
|
7978
|
+
const insertMeta = db.prepare(`INSERT INTO metadata (name, value) VALUES (?, ?)`);
|
|
7979
|
+
try {
|
|
7980
|
+
for (const [name, value] of Object.entries(metadataRows)) {
|
|
7981
|
+
insertMeta.run([name, value]);
|
|
7982
|
+
}
|
|
7983
|
+
}
|
|
7984
|
+
finally {
|
|
7985
|
+
insertMeta.free();
|
|
7986
|
+
}
|
|
7987
|
+
const insertTile = db.prepare(`INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data)
|
|
7988
|
+
VALUES (?, ?, ?, ?)`);
|
|
7989
|
+
try {
|
|
7990
|
+
db.run('BEGIN');
|
|
7991
|
+
for (const tile of packedTiles) {
|
|
7992
|
+
insertTile.run([tile.z, tile.x, flipY(tile.y, tile.z), tile.data]);
|
|
7993
|
+
}
|
|
7994
|
+
db.run('COMMIT');
|
|
7995
|
+
}
|
|
7996
|
+
finally {
|
|
7997
|
+
insertTile.free();
|
|
7998
|
+
}
|
|
7999
|
+
const binary = db.export();
|
|
8000
|
+
const blob = new Blob([binary.buffer], {
|
|
8001
|
+
type: 'application/x-sqlite3',
|
|
7287
8002
|
});
|
|
7288
|
-
exportData.tiles = await this.exportTiles(regionId, onProgress);
|
|
7289
|
-
}
|
|
7290
|
-
// Export sprites if requested
|
|
7291
|
-
if (options.includeSprites !== false) {
|
|
7292
8003
|
onProgress({
|
|
7293
|
-
stage: '
|
|
7294
|
-
percentage:
|
|
7295
|
-
message: '
|
|
8004
|
+
stage: 'complete',
|
|
8005
|
+
percentage: 100,
|
|
8006
|
+
message: 'MBTiles export complete!',
|
|
7296
8007
|
});
|
|
7297
|
-
|
|
8008
|
+
return {
|
|
8009
|
+
success: true,
|
|
8010
|
+
format: 'mbtiles',
|
|
8011
|
+
filename: `${region.name || region.id}.mbtiles`,
|
|
8012
|
+
blob,
|
|
8013
|
+
size: blob.size,
|
|
8014
|
+
statistics: {
|
|
8015
|
+
tilesExported: tiles.length,
|
|
8016
|
+
spritesExported: 0,
|
|
8017
|
+
fontsExported: 0,
|
|
8018
|
+
},
|
|
8019
|
+
};
|
|
7298
8020
|
}
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
onProgress({
|
|
7302
|
-
stage: 'exporting',
|
|
7303
|
-
percentage: 85,
|
|
7304
|
-
message: 'Exporting fonts...',
|
|
7305
|
-
});
|
|
7306
|
-
exportData.fonts = await this.exportFonts(regionId);
|
|
8021
|
+
finally {
|
|
8022
|
+
db.close();
|
|
7307
8023
|
}
|
|
7308
|
-
onProgress({
|
|
7309
|
-
stage: 'processing',
|
|
7310
|
-
percentage: 95,
|
|
7311
|
-
message: 'Creating export file...',
|
|
7312
|
-
});
|
|
7313
|
-
// Create JSON blob
|
|
7314
|
-
const jsonString = JSON.stringify(exportData, null, 2);
|
|
7315
|
-
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
7316
|
-
onProgress({
|
|
7317
|
-
stage: 'complete',
|
|
7318
|
-
percentage: 100,
|
|
7319
|
-
message: 'Export complete!',
|
|
7320
|
-
});
|
|
7321
|
-
return {
|
|
7322
|
-
success: true,
|
|
7323
|
-
format: 'json',
|
|
7324
|
-
filename: `${region.name || region.id}_export.json`,
|
|
7325
|
-
blob,
|
|
7326
|
-
size: blob.size,
|
|
7327
|
-
statistics: {
|
|
7328
|
-
tilesExported: exportData.tiles.length,
|
|
7329
|
-
spritesExported: exportData.sprites.length,
|
|
7330
|
-
fontsExported: exportData.fonts.length,
|
|
7331
|
-
},
|
|
7332
|
-
};
|
|
7333
8024
|
}
|
|
7334
8025
|
catch (error) {
|
|
7335
8026
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7336
|
-
throw new Error(`
|
|
8027
|
+
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7337
8028
|
}
|
|
7338
8029
|
}
|
|
7339
8030
|
/**
|
|
7340
|
-
*
|
|
8031
|
+
* Import region from a binary MBTiles (SQLite) file.
|
|
7341
8032
|
*/
|
|
7342
|
-
async
|
|
7343
|
-
const onProgress =
|
|
8033
|
+
async importRegion(importData) {
|
|
8034
|
+
const onProgress = importData.onProgress || (() => { });
|
|
7344
8035
|
try {
|
|
7345
8036
|
onProgress({
|
|
7346
8037
|
stage: 'preparing',
|
|
7347
8038
|
percentage: 0,
|
|
7348
|
-
message: '
|
|
8039
|
+
message: 'Reading file...',
|
|
7349
8040
|
});
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
// to create a proper PMTiles file format
|
|
7353
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7354
|
-
if (!region) {
|
|
7355
|
-
throw new Error(`Region ${regionId} not found`);
|
|
8041
|
+
if (importData.format !== 'mbtiles') {
|
|
8042
|
+
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7356
8043
|
}
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7360
|
-
const pmtilesData = {
|
|
7361
|
-
header: {
|
|
7362
|
-
version: 3,
|
|
7363
|
-
type: 'mvt',
|
|
7364
|
-
compression: options.compression || 'gzip',
|
|
7365
|
-
bounds: region.bounds,
|
|
7366
|
-
minZoom: region.minZoom,
|
|
7367
|
-
maxZoom: region.maxZoom,
|
|
7368
|
-
metadata: {
|
|
7369
|
-
name: region.name,
|
|
7370
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7371
|
-
...options.metadata,
|
|
7372
|
-
},
|
|
7373
|
-
},
|
|
7374
|
-
tiles: tiles,
|
|
7375
|
-
};
|
|
7376
|
-
// Convert to binary format (simplified)
|
|
7377
|
-
const jsonString = JSON.stringify(pmtilesData);
|
|
7378
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
7379
|
-
onProgress({
|
|
7380
|
-
stage: 'complete',
|
|
7381
|
-
percentage: 100,
|
|
7382
|
-
message: 'PMTiles export complete!',
|
|
7383
|
-
});
|
|
7384
|
-
return {
|
|
7385
|
-
success: true,
|
|
7386
|
-
format: 'pmtiles',
|
|
7387
|
-
filename: `${region.name || region.id}.pmtiles`,
|
|
7388
|
-
blob,
|
|
7389
|
-
size: blob.size,
|
|
7390
|
-
statistics: {
|
|
7391
|
-
tilesExported: tiles.length,
|
|
7392
|
-
spritesExported: 0,
|
|
7393
|
-
fontsExported: 0,
|
|
7394
|
-
},
|
|
7395
|
-
};
|
|
7396
|
-
}
|
|
7397
|
-
catch (error) {
|
|
7398
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7399
|
-
throw new Error(`PMTiles export failed: ${errorMessage}`);
|
|
7400
|
-
}
|
|
7401
|
-
}
|
|
7402
|
-
/**
|
|
7403
|
-
* Export region as MBTiles format
|
|
7404
|
-
*/
|
|
7405
|
-
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7406
|
-
const onProgress = options.onProgress || (() => { });
|
|
7407
|
-
try {
|
|
8044
|
+
const buffer = await this.readFileAsArrayBuffer(importData.file);
|
|
8045
|
+
onProgress({ stage: 'importing', percentage: 40, message: 'Parsing MBTiles...' });
|
|
8046
|
+
const regionData = await this.parseMBTiles(buffer);
|
|
7408
8047
|
onProgress({
|
|
7409
|
-
stage: '
|
|
7410
|
-
percentage:
|
|
7411
|
-
message:
|
|
8048
|
+
stage: 'importing',
|
|
8049
|
+
percentage: 70,
|
|
8050
|
+
message: `Importing ${regionData.tiles?.length ?? 0} tiles...`,
|
|
7412
8051
|
});
|
|
7413
|
-
|
|
7414
|
-
// In a real implementation, you would use SQLite/SQL.js
|
|
7415
|
-
// to create a proper MBTiles SQLite database
|
|
7416
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7417
|
-
if (!region) {
|
|
7418
|
-
throw new Error(`Region ${regionId} not found`);
|
|
7419
|
-
}
|
|
7420
|
-
// Get tiles data
|
|
7421
|
-
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7422
|
-
// Create MBTiles structure (simplified as JSON for now)
|
|
7423
|
-
const mbtilesData = {
|
|
7424
|
-
metadata: {
|
|
7425
|
-
name: region.name,
|
|
7426
|
-
type: 'overlay',
|
|
7427
|
-
version: '1.0',
|
|
7428
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7429
|
-
format: options.format || 'pbf',
|
|
7430
|
-
bounds: region.bounds.flat().join(','),
|
|
7431
|
-
minzoom: region.minZoom,
|
|
7432
|
-
maxzoom: region.maxZoom,
|
|
7433
|
-
...options.metadata,
|
|
7434
|
-
},
|
|
7435
|
-
tiles: tiles.map(tile => ({
|
|
7436
|
-
zoom_level: tile.z,
|
|
7437
|
-
tile_column: tile.x,
|
|
7438
|
-
tile_row: tile.y,
|
|
7439
|
-
tile_data: tile.data,
|
|
7440
|
-
})),
|
|
7441
|
-
};
|
|
7442
|
-
// Convert to binary format (simplified)
|
|
7443
|
-
const jsonString = JSON.stringify(mbtilesData);
|
|
7444
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
8052
|
+
const result = await this.importRegionData(regionData, importData);
|
|
7445
8053
|
onProgress({
|
|
7446
8054
|
stage: 'complete',
|
|
7447
8055
|
percentage: 100,
|
|
7448
|
-
message: '
|
|
8056
|
+
message: result.success ? 'Import complete!' : result.message,
|
|
7449
8057
|
});
|
|
7450
|
-
return {
|
|
7451
|
-
success: true,
|
|
7452
|
-
format: 'mbtiles',
|
|
7453
|
-
filename: `${region.name || region.id}.mbtiles`,
|
|
7454
|
-
blob,
|
|
7455
|
-
size: blob.size,
|
|
7456
|
-
statistics: {
|
|
7457
|
-
tilesExported: tiles.length,
|
|
7458
|
-
spritesExported: 0,
|
|
7459
|
-
fontsExported: 0,
|
|
7460
|
-
},
|
|
7461
|
-
};
|
|
7462
|
-
}
|
|
7463
|
-
catch (error) {
|
|
7464
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7465
|
-
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7466
|
-
}
|
|
7467
|
-
}
|
|
7468
|
-
/**
|
|
7469
|
-
* Import region from file
|
|
7470
|
-
*/
|
|
7471
|
-
async importRegion(importData) {
|
|
7472
|
-
try {
|
|
7473
|
-
let regionData;
|
|
7474
|
-
switch (importData.format) {
|
|
7475
|
-
case 'json': {
|
|
7476
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7477
|
-
regionData = JSON.parse(textContent);
|
|
7478
|
-
break;
|
|
7479
|
-
}
|
|
7480
|
-
case 'pmtiles': {
|
|
7481
|
-
// PMTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7482
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7483
|
-
regionData = await this.parsePMTiles(textContent);
|
|
7484
|
-
break;
|
|
7485
|
-
}
|
|
7486
|
-
case 'mbtiles': {
|
|
7487
|
-
// MBTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7488
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7489
|
-
regionData = await this.parseMBTiles(textContent);
|
|
7490
|
-
break;
|
|
7491
|
-
}
|
|
7492
|
-
default:
|
|
7493
|
-
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7494
|
-
}
|
|
7495
|
-
// Import the region data
|
|
7496
|
-
const result = await this.importRegionData(regionData, importData);
|
|
7497
8058
|
return result;
|
|
7498
8059
|
}
|
|
7499
8060
|
catch (error) {
|
|
@@ -7617,151 +8178,113 @@ class ImportExportService {
|
|
|
7617
8178
|
}
|
|
7618
8179
|
}
|
|
7619
8180
|
/**
|
|
7620
|
-
*
|
|
7621
|
-
*/
|
|
7622
|
-
async exportSprites(_regionId) {
|
|
7623
|
-
const db = await this.db;
|
|
7624
|
-
const transaction = db.transaction(['sprites'], 'readonly');
|
|
7625
|
-
const store = transaction.objectStore('sprites');
|
|
7626
|
-
const sprites = [];
|
|
7627
|
-
try {
|
|
7628
|
-
let cursor = await store.openCursor();
|
|
7629
|
-
while (cursor) {
|
|
7630
|
-
const sprite = cursor.value;
|
|
7631
|
-
// Include sprites that match the styleId, or all sprites if keys don't contain styleId
|
|
7632
|
-
// (sprite keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7633
|
-
sprites.push({
|
|
7634
|
-
url: sprite.url,
|
|
7635
|
-
data: sprite.data,
|
|
7636
|
-
type: sprite.url.endsWith('.json') ? 'json' : 'png',
|
|
7637
|
-
resolution: sprite.url.includes('@2x') ? '2x' : '1x',
|
|
7638
|
-
});
|
|
7639
|
-
cursor = await cursor.continue();
|
|
7640
|
-
}
|
|
7641
|
-
return sprites;
|
|
7642
|
-
}
|
|
7643
|
-
catch (error) {
|
|
7644
|
-
serviceLogger.error('Error exporting sprites:', error);
|
|
7645
|
-
return [];
|
|
7646
|
-
}
|
|
7647
|
-
}
|
|
7648
|
-
/**
|
|
7649
|
-
* Export fonts data
|
|
8181
|
+
* Read file content as ArrayBuffer (for the binary MBTiles file).
|
|
7650
8182
|
*/
|
|
7651
|
-
async
|
|
7652
|
-
const db = await this.db;
|
|
7653
|
-
const transaction = db.transaction(['fonts'], 'readonly');
|
|
7654
|
-
const store = transaction.objectStore('fonts');
|
|
7655
|
-
const fonts = [];
|
|
7656
|
-
try {
|
|
7657
|
-
let cursor = await store.openCursor();
|
|
7658
|
-
while (cursor) {
|
|
7659
|
-
const font = cursor.value;
|
|
7660
|
-
// Include fonts that match the styleId, or all fonts if keys don't contain styleId
|
|
7661
|
-
// (font keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7662
|
-
fonts.push({
|
|
7663
|
-
fontStack: font.key, // Use key as fontstack identifier
|
|
7664
|
-
range: '0-255', // Default range since FontEntry doesn't store this
|
|
7665
|
-
data: font.data,
|
|
7666
|
-
});
|
|
7667
|
-
cursor = await cursor.continue();
|
|
7668
|
-
}
|
|
7669
|
-
return fonts;
|
|
7670
|
-
}
|
|
7671
|
-
catch (error) {
|
|
7672
|
-
serviceLogger.error('Error exporting fonts:', error);
|
|
7673
|
-
return [];
|
|
7674
|
-
}
|
|
7675
|
-
}
|
|
7676
|
-
/**
|
|
7677
|
-
* Read file content as text (for JSON files)
|
|
7678
|
-
*/
|
|
7679
|
-
async readFileAsText(file) {
|
|
8183
|
+
async readFileAsArrayBuffer(file) {
|
|
7680
8184
|
return new Promise((resolve, reject) => {
|
|
7681
8185
|
const reader = new FileReader();
|
|
7682
8186
|
reader.onload = () => resolve(reader.result);
|
|
7683
8187
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
7684
|
-
reader.
|
|
8188
|
+
reader.readAsArrayBuffer(file);
|
|
7685
8189
|
});
|
|
7686
8190
|
}
|
|
7687
8191
|
/**
|
|
7688
|
-
* Parse
|
|
8192
|
+
* Parse a real binary MBTiles (SQLite) file into our import-data shape.
|
|
8193
|
+
* Un-flips the TMS tile_row back to XYZ y.
|
|
7689
8194
|
*/
|
|
7690
|
-
async
|
|
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
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
8195
|
+
async parseMBTiles(buffer) {
|
|
8196
|
+
const bytes = new Uint8Array(buffer);
|
|
8197
|
+
// SQLite header: "SQLite format 3\0" (16 bytes). Validate up front so
|
|
8198
|
+
// non-MBTiles files (e.g. a JSON renamed to .mbtiles) surface a clear
|
|
8199
|
+
// error instead of the opaque "file is not a database" from sql.js.
|
|
8200
|
+
if (bytes.byteLength < 16) {
|
|
8201
|
+
throw new Error('Not a valid MBTiles file: file is too small');
|
|
8202
|
+
}
|
|
8203
|
+
const magic = String.fromCharCode(...bytes.slice(0, 15));
|
|
8204
|
+
if (magic !== 'SQLite format 3') {
|
|
8205
|
+
throw new Error('Not a valid MBTiles file: missing SQLite header');
|
|
8206
|
+
}
|
|
8207
|
+
const SQL = await getSqlJs();
|
|
8208
|
+
const db = new SQL.Database(bytes);
|
|
8209
|
+
try {
|
|
8210
|
+
const tablesResult = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('metadata', 'tiles')");
|
|
8211
|
+
const tableNames = (tablesResult[0]?.values || []).map(r => r[0]);
|
|
8212
|
+
if (!tableNames.includes('metadata') || !tableNames.includes('tiles')) {
|
|
8213
|
+
throw new Error('Not a valid MBTiles file: missing required metadata/tiles tables');
|
|
8214
|
+
}
|
|
8215
|
+
const metadata = {};
|
|
8216
|
+
const metaStmt = db.prepare('SELECT name, value FROM metadata');
|
|
8217
|
+
try {
|
|
8218
|
+
while (metaStmt.step()) {
|
|
8219
|
+
const row = metaStmt.get();
|
|
8220
|
+
metadata[row[0]] = row[1];
|
|
8221
|
+
}
|
|
8222
|
+
}
|
|
8223
|
+
finally {
|
|
8224
|
+
metaStmt.free();
|
|
8225
|
+
}
|
|
8226
|
+
const rawBounds = metadata.bounds ? metadata.bounds.split(',').map(Number) : [0, 0, 0, 0];
|
|
8227
|
+
const bounds = [
|
|
8228
|
+
isFinite(rawBounds[0]) ? rawBounds[0] : 0,
|
|
8229
|
+
isFinite(rawBounds[1]) ? rawBounds[1] : 0,
|
|
8230
|
+
isFinite(rawBounds[2]) ? rawBounds[2] : 0,
|
|
8231
|
+
isFinite(rawBounds[3]) ? rawBounds[3] : 0,
|
|
8232
|
+
];
|
|
8233
|
+
const format = (metadata.format || 'pbf');
|
|
8234
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
8235
|
+
const tiles = [];
|
|
8236
|
+
const tilesStmt = db.prepare('SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles');
|
|
8237
|
+
try {
|
|
8238
|
+
while (tilesStmt.step()) {
|
|
8239
|
+
const row = tilesStmt.get();
|
|
8240
|
+
const [z, x, tmsRow, data] = row;
|
|
8241
|
+
// Sliced copy so the buffer is detached from sql.js's heap.
|
|
8242
|
+
const copy = new Uint8Array(data.byteLength);
|
|
8243
|
+
copy.set(data);
|
|
8244
|
+
// Our IndexedDB stores vector tiles decompressed (tileService
|
|
8245
|
+
// inflates on download). MBTiles vector tiles are gzipped by
|
|
8246
|
+
// convention — un-gzip on the way in so the stored tile matches
|
|
8247
|
+
// what the fetch handler expects to serve.
|
|
8248
|
+
const storedBytes = isVector && hasGzipMagic(copy) ? await gunzipBytes(copy) : copy;
|
|
8249
|
+
tiles.push({
|
|
8250
|
+
z,
|
|
8251
|
+
x,
|
|
8252
|
+
y: flipY(tmsRow, z),
|
|
8253
|
+
data: storedBytes.buffer,
|
|
8254
|
+
format,
|
|
8255
|
+
sourceId: 'imported',
|
|
8256
|
+
});
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8259
|
+
finally {
|
|
8260
|
+
tilesStmt.free();
|
|
8261
|
+
}
|
|
8262
|
+
const minZoom = metadata.minzoom !== undefined ? Number(metadata.minzoom) : 0;
|
|
8263
|
+
const maxZoom = metadata.maxzoom !== undefined ? Number(metadata.maxzoom) : 14;
|
|
8264
|
+
return {
|
|
8265
|
+
metadata: {
|
|
8266
|
+
id: metadata.name || 'imported-region',
|
|
8267
|
+
name: metadata.name || 'Imported Region',
|
|
8268
|
+
description: metadata.description,
|
|
8269
|
+
bounds: [
|
|
8270
|
+
[bounds[0], bounds[1]],
|
|
8271
|
+
[bounds[2], bounds[3]],
|
|
8272
|
+
],
|
|
8273
|
+
minZoom,
|
|
8274
|
+
maxZoom,
|
|
8275
|
+
styleUrl: '',
|
|
8276
|
+
createdAt: Date.now(),
|
|
8277
|
+
exportedAt: Date.now(),
|
|
8278
|
+
version: '1.0.0',
|
|
8279
|
+
format: 'mbtiles',
|
|
8280
|
+
},
|
|
8281
|
+
style: {},
|
|
8282
|
+
tiles,
|
|
8283
|
+
};
|
|
8284
|
+
}
|
|
8285
|
+
finally {
|
|
8286
|
+
db.close();
|
|
8287
|
+
}
|
|
7765
8288
|
}
|
|
7766
8289
|
/**
|
|
7767
8290
|
* Import region data to database
|
|
@@ -7826,16 +8349,15 @@ class ImportExportService {
|
|
|
7826
8349
|
});
|
|
7827
8350
|
}
|
|
7828
8351
|
}
|
|
7829
|
-
// Import sprites and fonts similarly...
|
|
7830
8352
|
return {
|
|
7831
8353
|
success: true,
|
|
7832
8354
|
regionId,
|
|
7833
8355
|
message: 'Region imported successfully',
|
|
7834
8356
|
statistics: {
|
|
7835
8357
|
tilesImported: regionData.tiles?.length || 0,
|
|
7836
|
-
spritesImported:
|
|
7837
|
-
fontsImported:
|
|
7838
|
-
totalSize: 0,
|
|
8358
|
+
spritesImported: 0,
|
|
8359
|
+
fontsImported: 0,
|
|
8360
|
+
totalSize: 0,
|
|
7839
8361
|
},
|
|
7840
8362
|
};
|
|
7841
8363
|
}
|
|
@@ -7920,6 +8442,10 @@ const createResourceManagement = (services) => ({
|
|
|
7920
8442
|
loadGlyphsForStyle: (...args) => services.resourceService.loadGlyphsForStyle(...args),
|
|
7921
8443
|
cleanupOldGlyphs: (...args) => services.resourceService.cleanupOldGlyphs(...args),
|
|
7922
8444
|
verifyAndRepairGlyphs: (...args) => services.resourceService.verifyAndRepairGlyphs(...args),
|
|
8445
|
+
downloadModelsWithOptions: (...args) => services.resourceService.downloadModelsWithOptions(...args),
|
|
8446
|
+
getModelStats: (...args) => services.resourceService.getModelStats(...args),
|
|
8447
|
+
cleanupOldModels: (...args) => services.resourceService.cleanupOldModels(...args),
|
|
8448
|
+
verifyAndRepairModels: (...args) => services.resourceService.verifyAndRepairModels(...args),
|
|
7923
8449
|
});
|
|
7924
8450
|
|
|
7925
8451
|
const createAnalyticsManagement = (services, deps) => ({
|
|
@@ -8060,8 +8586,6 @@ const createMaintenanceManagement = (services, deps) => {
|
|
|
8060
8586
|
};
|
|
8061
8587
|
|
|
8062
8588
|
const createImportExportManagement = (services) => ({
|
|
8063
|
-
exportRegionAsJSON: async (regionId, options = {}) => services.importExportService.exportRegionAsJSON(regionId, options),
|
|
8064
|
-
exportRegionAsPMTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsPMTiles(regionId, options),
|
|
8065
8589
|
exportRegionAsMBTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsMBTiles(regionId, options),
|
|
8066
8590
|
importRegion: async (importData) => services.importExportService.importRegion(importData),
|
|
8067
8591
|
downloadExportedRegion: (exportResult) => {
|
|
@@ -8417,10 +8941,6 @@ const en = {
|
|
|
8417
8941
|
'styleSelection.title': 'Select Offline Style',
|
|
8418
8942
|
'styleSelection.message': 'Choose which offline style to load:',
|
|
8419
8943
|
'styleSelection.sources': 'sources',
|
|
8420
|
-
// Import/Export
|
|
8421
|
-
'importExport.title': 'Import/Export',
|
|
8422
|
-
'importExport.export': 'Export',
|
|
8423
|
-
'importExport.import': 'Import',
|
|
8424
8944
|
// Errors
|
|
8425
8945
|
'error.loadingContent': 'Error loading content',
|
|
8426
8946
|
'error.tryAgain': 'Please try again',
|
|
@@ -8445,41 +8965,30 @@ const en = {
|
|
|
8445
8965
|
'regionDetails.bounds': 'Bounds',
|
|
8446
8966
|
'regionDetails.zoomRange': 'Zoom Range',
|
|
8447
8967
|
'regionDetails.created': 'Created',
|
|
8448
|
-
//
|
|
8449
|
-
'
|
|
8450
|
-
'
|
|
8451
|
-
'
|
|
8452
|
-
'
|
|
8453
|
-
'
|
|
8454
|
-
'
|
|
8455
|
-
'
|
|
8456
|
-
'
|
|
8457
|
-
'
|
|
8458
|
-
'
|
|
8459
|
-
'
|
|
8460
|
-
'
|
|
8461
|
-
'
|
|
8462
|
-
'
|
|
8463
|
-
'
|
|
8464
|
-
'
|
|
8465
|
-
'
|
|
8466
|
-
'
|
|
8467
|
-
'
|
|
8468
|
-
'
|
|
8469
|
-
'
|
|
8470
|
-
'
|
|
8471
|
-
'
|
|
8472
|
-
'importExport.fileFormatsHint': 'Supports JSON, PMTiles, and MBTiles formats',
|
|
8473
|
-
'importExport.newRegionName': 'New Region Name (Optional)',
|
|
8474
|
-
'importExport.newRegionNamePlaceholder': 'Leave empty to use original name',
|
|
8475
|
-
'importExport.overwriteIfExists': 'Overwrite if region exists',
|
|
8476
|
-
'importExport.preparingImport': 'Preparing import...',
|
|
8477
|
-
'importExport.importComplete': 'Import complete!',
|
|
8478
|
-
'importExport.importFailed': 'Import failed. Please try again.',
|
|
8479
|
-
'importExport.formatGuide': 'Format Guide',
|
|
8480
|
-
'importExport.jsonDesc': 'Complete data, human-readable, best for development',
|
|
8481
|
-
'importExport.pmtilesDesc': 'Web-optimized, efficient serving, cloud-friendly',
|
|
8482
|
-
'importExport.mbtilesDesc': 'Industry standard, SQLite-based, cross-platform',
|
|
8968
|
+
// MBTiles Modal
|
|
8969
|
+
'mbtiles.title': 'MBTiles Import / Export',
|
|
8970
|
+
'mbtiles.regionInfo': 'Region Information',
|
|
8971
|
+
'mbtiles.id': 'ID',
|
|
8972
|
+
'mbtiles.name': 'Name',
|
|
8973
|
+
'mbtiles.unnamed': 'Unnamed',
|
|
8974
|
+
'mbtiles.zoom': 'Zoom',
|
|
8975
|
+
'mbtiles.created': 'Created',
|
|
8976
|
+
'mbtiles.exportTitle': 'Export as MBTiles',
|
|
8977
|
+
'mbtiles.exportHint': 'Package the tiles in this region into a standard SQLite MBTiles archive that opens in QGIS, tippecanoe, and other tools.',
|
|
8978
|
+
'mbtiles.exportButton': 'Download .mbtiles',
|
|
8979
|
+
'mbtiles.preparingExport': 'Preparing export...',
|
|
8980
|
+
'mbtiles.exportComplete': 'Export complete!',
|
|
8981
|
+
'mbtiles.exportFailed': 'Export failed. Please try again.',
|
|
8982
|
+
'mbtiles.importTitle': 'Import from MBTiles',
|
|
8983
|
+
'mbtiles.selectFile': 'Select an .mbtiles file',
|
|
8984
|
+
'mbtiles.fileHint': 'Only SQLite-format .mbtiles files are supported.',
|
|
8985
|
+
'mbtiles.newRegionName': 'New Region Name (optional)',
|
|
8986
|
+
'mbtiles.newRegionNamePlaceholder': 'Leave empty to use the name from the file',
|
|
8987
|
+
'mbtiles.overwriteIfExists': 'Overwrite if a region with the same id exists',
|
|
8988
|
+
'mbtiles.importButton': 'Import .mbtiles',
|
|
8989
|
+
'mbtiles.preparingImport': 'Preparing import...',
|
|
8990
|
+
'mbtiles.importComplete': 'Import complete!',
|
|
8991
|
+
'mbtiles.importFailed': 'Import failed. Please try again.',
|
|
8483
8992
|
// Active Downloads
|
|
8484
8993
|
'download.activeCount': 'Active Downloads ({{count}})',
|
|
8485
8994
|
// Panel Manager additional strings
|
|
@@ -8640,10 +9149,6 @@ const ar = {
|
|
|
8640
9149
|
'styleSelection.title': 'اختر نمط غير متصل',
|
|
8641
9150
|
'styleSelection.message': 'اختر النمط غير المتصل الذي تريد تحميله:',
|
|
8642
9151
|
'styleSelection.sources': 'مصادر',
|
|
8643
|
-
// Import/Export - استيراد/تصدير
|
|
8644
|
-
'importExport.title': 'استيراد/تصدير',
|
|
8645
|
-
'importExport.export': 'تصدير',
|
|
8646
|
-
'importExport.import': 'استيراد',
|
|
8647
9152
|
// Errors - الأخطاء
|
|
8648
9153
|
'error.loadingContent': 'خطأ في تحميل المحتوى',
|
|
8649
9154
|
'error.tryAgain': 'يرجى المحاولة مرة أخرى',
|
|
@@ -8668,41 +9173,30 @@ const ar = {
|
|
|
8668
9173
|
'regionDetails.bounds': 'الحدود',
|
|
8669
9174
|
'regionDetails.zoomRange': 'نطاق التكبير',
|
|
8670
9175
|
'regionDetails.created': 'تاريخ الإنشاء',
|
|
8671
|
-
//
|
|
8672
|
-
'
|
|
8673
|
-
'
|
|
8674
|
-
'
|
|
8675
|
-
'
|
|
8676
|
-
'
|
|
8677
|
-
'
|
|
8678
|
-
'
|
|
8679
|
-
'
|
|
8680
|
-
'
|
|
8681
|
-
'
|
|
8682
|
-
'
|
|
8683
|
-
'
|
|
8684
|
-
'
|
|
8685
|
-
'
|
|
8686
|
-
'
|
|
8687
|
-
'
|
|
8688
|
-
'
|
|
8689
|
-
'
|
|
8690
|
-
'
|
|
8691
|
-
'
|
|
8692
|
-
'
|
|
8693
|
-
'
|
|
8694
|
-
'
|
|
8695
|
-
'importExport.fileFormatsHint': 'يدعم تنسيقات JSON و PMTiles و MBTiles',
|
|
8696
|
-
'importExport.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
8697
|
-
'importExport.newRegionNamePlaceholder': 'اتركه فارغاً لاستخدام الاسم الأصلي',
|
|
8698
|
-
'importExport.overwriteIfExists': 'الكتابة فوق المنطقة الموجودة',
|
|
8699
|
-
'importExport.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
8700
|
-
'importExport.importComplete': 'اكتمل الاستيراد!',
|
|
8701
|
-
'importExport.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
8702
|
-
'importExport.formatGuide': 'دليل التنسيقات',
|
|
8703
|
-
'importExport.jsonDesc': 'بيانات كاملة، قابلة للقراءة، الأفضل للتطوير',
|
|
8704
|
-
'importExport.pmtilesDesc': 'محسن للويب، خدمة فعالة، متوافق مع السحابة',
|
|
8705
|
-
'importExport.mbtilesDesc': 'معيار الصناعة، قائم على SQLite، متعدد المنصات',
|
|
9176
|
+
// MBTiles Modal - نافذة MBTiles
|
|
9177
|
+
'mbtiles.title': 'استيراد / تصدير MBTiles',
|
|
9178
|
+
'mbtiles.regionInfo': 'معلومات المنطقة',
|
|
9179
|
+
'mbtiles.id': 'المعرف',
|
|
9180
|
+
'mbtiles.name': 'الاسم',
|
|
9181
|
+
'mbtiles.unnamed': 'بدون اسم',
|
|
9182
|
+
'mbtiles.zoom': 'التكبير',
|
|
9183
|
+
'mbtiles.created': 'تاريخ الإنشاء',
|
|
9184
|
+
'mbtiles.exportTitle': 'التصدير كـ MBTiles',
|
|
9185
|
+
'mbtiles.exportHint': 'احزم بلاطات هذه المنطقة داخل أرشيف MBTiles (SQLite) يمكن فتحه في QGIS و tippecanoe وأدوات أخرى.',
|
|
9186
|
+
'mbtiles.exportButton': 'تنزيل ملف mbtiles.',
|
|
9187
|
+
'mbtiles.preparingExport': 'جاري تجهيز التصدير...',
|
|
9188
|
+
'mbtiles.exportComplete': 'اكتمل التصدير!',
|
|
9189
|
+
'mbtiles.exportFailed': 'فشل التصدير. يرجى المحاولة مرة أخرى.',
|
|
9190
|
+
'mbtiles.importTitle': 'الاستيراد من MBTiles',
|
|
9191
|
+
'mbtiles.selectFile': 'اختر ملف mbtiles.',
|
|
9192
|
+
'mbtiles.fileHint': 'تُدعم ملفات mbtiles. بتنسيق SQLite فقط.',
|
|
9193
|
+
'mbtiles.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
9194
|
+
'mbtiles.newRegionNamePlaceholder': 'اتركه فارغًا لاستخدام الاسم من الملف',
|
|
9195
|
+
'mbtiles.overwriteIfExists': 'الكتابة فوق المنطقة إذا كان المعرف موجودًا',
|
|
9196
|
+
'mbtiles.importButton': 'استيراد ملف mbtiles.',
|
|
9197
|
+
'mbtiles.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
9198
|
+
'mbtiles.importComplete': 'اكتمل الاستيراد!',
|
|
9199
|
+
'mbtiles.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
8706
9200
|
// Active Downloads - التحميلات النشطة
|
|
8707
9201
|
'download.activeCount': 'التحميلات النشطة ({{count}})',
|
|
8708
9202
|
// Panel Manager additional strings - سلاسل إضافية للوحة
|
|
@@ -9641,49 +10135,42 @@ class ConfirmationModal {
|
|
|
9641
10135
|
}
|
|
9642
10136
|
|
|
9643
10137
|
/**
|
|
9644
|
-
* Import/Export Modal
|
|
9645
|
-
*
|
|
9646
|
-
*
|
|
10138
|
+
* MBTiles Import/Export Modal
|
|
10139
|
+
*
|
|
10140
|
+
* Focused modal for exchanging regions as binary SQLite MBTiles archives.
|
|
10141
|
+
* Replaces the previous multi-format import/export modal.
|
|
9647
10142
|
*/
|
|
9648
|
-
const modalLogger = logger.scope('
|
|
9649
|
-
class
|
|
10143
|
+
const modalLogger = logger.scope('MBTilesModal');
|
|
10144
|
+
class MBTilesModal {
|
|
9650
10145
|
modal;
|
|
9651
10146
|
options;
|
|
9652
10147
|
isExporting = false;
|
|
9653
10148
|
isImporting = false;
|
|
9654
|
-
// Form elements
|
|
9655
|
-
exportFormatSelect;
|
|
9656
|
-
includeStyleCheckbox;
|
|
9657
|
-
includeTilesCheckbox;
|
|
9658
|
-
includeSpritesCheckbox;
|
|
9659
|
-
includeFontsCheckbox;
|
|
9660
10149
|
exportProgressBar;
|
|
9661
10150
|
exportProgressText;
|
|
10151
|
+
exportProgressContainer;
|
|
9662
10152
|
exportButton;
|
|
9663
10153
|
importFileInput;
|
|
9664
10154
|
importNameInput;
|
|
9665
10155
|
importOverwriteCheckbox;
|
|
9666
10156
|
importProgressBar;
|
|
9667
10157
|
importProgressText;
|
|
10158
|
+
importProgressContainer;
|
|
9668
10159
|
importButton;
|
|
9669
10160
|
constructor(options) {
|
|
9670
10161
|
this.options = options;
|
|
9671
10162
|
}
|
|
9672
10163
|
show() {
|
|
9673
10164
|
const modalConfig = {
|
|
9674
|
-
title: t('
|
|
10165
|
+
title: t('mbtiles.title'),
|
|
9675
10166
|
subtitle: this.options.region.name || this.options.region.id,
|
|
9676
10167
|
size: 'md',
|
|
9677
10168
|
closable: true,
|
|
9678
10169
|
onClose: () => this.hide(),
|
|
9679
10170
|
};
|
|
9680
10171
|
this.modal = new Modal(modalConfig);
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
this.modal.setContent(content);
|
|
9684
|
-
// Create footer with close button
|
|
9685
|
-
const footer = this.createFooter();
|
|
9686
|
-
this.modal.setFooter(footer);
|
|
10172
|
+
this.modal.setContent(this.createContent());
|
|
10173
|
+
this.modal.setFooter(this.createFooter());
|
|
9687
10174
|
this.modal.show();
|
|
9688
10175
|
this.attachEventListeners();
|
|
9689
10176
|
return this.modal.getElement();
|
|
@@ -9692,217 +10179,96 @@ class ImportExportModal {
|
|
|
9692
10179
|
this.modal?.hide();
|
|
9693
10180
|
this.options.onClose();
|
|
9694
10181
|
}
|
|
10182
|
+
destroy() {
|
|
10183
|
+
this.modal?.destroy();
|
|
10184
|
+
}
|
|
9695
10185
|
createContent() {
|
|
9696
10186
|
const content = document.createElement('div');
|
|
9697
|
-
content.className = 'flex flex-col gap-6';
|
|
10187
|
+
content.className = 'flex flex-col gap-6 py-2';
|
|
9698
10188
|
if (i18n.isRTL()) {
|
|
9699
10189
|
content.setAttribute('dir', 'rtl');
|
|
9700
10190
|
}
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
content.appendChild(
|
|
9704
|
-
// Export/Import Grid
|
|
9705
|
-
const gridContainer = document.createElement('div');
|
|
9706
|
-
gridContainer.className = 'grid grid-cols-1 gap-6';
|
|
9707
|
-
// Export Section
|
|
9708
|
-
const exportSection = this.createExportSection();
|
|
9709
|
-
gridContainer.appendChild(exportSection);
|
|
9710
|
-
// Import Section
|
|
9711
|
-
const importSection = this.createImportSection();
|
|
9712
|
-
gridContainer.appendChild(importSection);
|
|
9713
|
-
content.appendChild(gridContainer);
|
|
9714
|
-
// Format Guide
|
|
9715
|
-
const formatGuide = this.createFormatGuide();
|
|
9716
|
-
content.appendChild(formatGuide);
|
|
10191
|
+
content.appendChild(this.createRegionInfoLine());
|
|
10192
|
+
content.appendChild(this.createExportSection());
|
|
10193
|
+
content.appendChild(this.createImportSection());
|
|
9717
10194
|
return content;
|
|
9718
10195
|
}
|
|
9719
|
-
|
|
9720
|
-
const
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
9730
|
-
|
|
9731
|
-
</div>
|
|
9732
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9733
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.name')}</span>
|
|
9734
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${escapeHtml$1(this.options.region.name || t('importExport.unnamed'))}</div>
|
|
9735
|
-
</div>
|
|
9736
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9737
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.zoom')}</span>
|
|
9738
|
-
<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>
|
|
9739
|
-
</div>
|
|
9740
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9741
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.created')}</span>
|
|
9742
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${new Date(this.options.region.created).toLocaleDateString()}</div>
|
|
9743
|
-
</div>
|
|
9744
|
-
</div>
|
|
10196
|
+
createRegionInfoLine() {
|
|
10197
|
+
const { region } = this.options;
|
|
10198
|
+
const line = document.createElement('div');
|
|
10199
|
+
line.className =
|
|
10200
|
+
'flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400';
|
|
10201
|
+
line.innerHTML = `
|
|
10202
|
+
<span class="flex items-center gap-1">
|
|
10203
|
+
${icons.mapPin({ size: 12, color: 'currentColor' })}
|
|
10204
|
+
<span class="font-mono">${escapeHtml$1(region.id)}</span>
|
|
10205
|
+
</span>
|
|
10206
|
+
<span>Z${escapeHtml$1(region.minZoom)}-${escapeHtml$1(region.maxZoom)}</span>
|
|
10207
|
+
<span>${new Date(region.created).toLocaleDateString()}</span>
|
|
9745
10208
|
`;
|
|
9746
|
-
return
|
|
10209
|
+
return line;
|
|
9747
10210
|
}
|
|
9748
10211
|
createExportSection() {
|
|
9749
|
-
const section =
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
|
|
9759
|
-
|
|
9760
|
-
|
|
9761
|
-
|
|
9762
|
-
|
|
9763
|
-
|
|
9764
|
-
`;
|
|
9765
|
-
section.appendChild(header);
|
|
9766
|
-
const formContainer = document.createElement('div');
|
|
9767
|
-
formContainer.className = 'space-y-5';
|
|
9768
|
-
// Format Selection
|
|
9769
|
-
const formatGroup = document.createElement('div');
|
|
9770
|
-
const formatLabel = document.createElement('label');
|
|
9771
|
-
formatLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9772
|
-
formatLabel.textContent = t('importExport.exportFormat');
|
|
9773
|
-
this.exportFormatSelect = document.createElement('select');
|
|
9774
|
-
this.exportFormatSelect.className =
|
|
9775
|
-
'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';
|
|
9776
|
-
this.exportFormatSelect.innerHTML = `
|
|
9777
|
-
<option value="json">${t('importExport.formatJson')}</option>
|
|
9778
|
-
<option value="pmtiles">${t('importExport.formatPmtiles')}</option>
|
|
9779
|
-
<option value="mbtiles">${t('importExport.formatMbtiles')}</option>
|
|
9780
|
-
`;
|
|
9781
|
-
const formatHint = document.createElement('p');
|
|
9782
|
-
formatHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
9783
|
-
formatHint.textContent = t('importExport.formatHint');
|
|
9784
|
-
formatGroup.appendChild(formatLabel);
|
|
9785
|
-
formatGroup.appendChild(this.exportFormatSelect);
|
|
9786
|
-
formatGroup.appendChild(formatHint);
|
|
9787
|
-
formContainer.appendChild(formatGroup);
|
|
9788
|
-
// Export Options
|
|
9789
|
-
const optionsGroup = document.createElement('div');
|
|
9790
|
-
const optionsLabel = document.createElement('label');
|
|
9791
|
-
optionsLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3';
|
|
9792
|
-
optionsLabel.textContent = t('importExport.includeComponents');
|
|
9793
|
-
const checkboxContainer = document.createElement('div');
|
|
9794
|
-
checkboxContainer.className = 'grid grid-cols-1 sm:grid-cols-2 gap-3';
|
|
9795
|
-
const createCheckbox = (text, checked = true) => {
|
|
9796
|
-
const label = document.createElement('label');
|
|
9797
|
-
label.className =
|
|
9798
|
-
'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';
|
|
9799
|
-
const input = document.createElement('input');
|
|
9800
|
-
input.type = 'checkbox';
|
|
9801
|
-
input.checked = checked;
|
|
9802
|
-
input.className =
|
|
9803
|
-
'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';
|
|
9804
|
-
const span = document.createElement('span');
|
|
9805
|
-
span.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
9806
|
-
span.textContent = text;
|
|
9807
|
-
label.appendChild(input);
|
|
9808
|
-
label.appendChild(span);
|
|
9809
|
-
return { label, input };
|
|
9810
|
-
};
|
|
9811
|
-
const styleCheck = createCheckbox(t('importExport.styleConfig'));
|
|
9812
|
-
this.includeStyleCheckbox = styleCheck.input;
|
|
9813
|
-
checkboxContainer.appendChild(styleCheck.label);
|
|
9814
|
-
const tilesCheck = createCheckbox(t('importExport.mapTiles'));
|
|
9815
|
-
this.includeTilesCheckbox = tilesCheck.input;
|
|
9816
|
-
checkboxContainer.appendChild(tilesCheck.label);
|
|
9817
|
-
const spritesCheck = createCheckbox(t('importExport.spritesIcons'));
|
|
9818
|
-
this.includeSpritesCheckbox = spritesCheck.input;
|
|
9819
|
-
checkboxContainer.appendChild(spritesCheck.label);
|
|
9820
|
-
const fontsCheck = createCheckbox(t('importExport.fontsGlyphs'));
|
|
9821
|
-
this.includeFontsCheckbox = fontsCheck.input;
|
|
9822
|
-
checkboxContainer.appendChild(fontsCheck.label);
|
|
9823
|
-
optionsGroup.appendChild(optionsLabel);
|
|
9824
|
-
optionsGroup.appendChild(checkboxContainer);
|
|
9825
|
-
formContainer.appendChild(optionsGroup);
|
|
9826
|
-
// Export Progress (hidden by default)
|
|
9827
|
-
const progressContainer = document.createElement('div');
|
|
9828
|
-
progressContainer.className = 'hidden';
|
|
9829
|
-
const progressBarContainer = document.createElement('div');
|
|
9830
|
-
progressBarContainer.className =
|
|
9831
|
-
'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
9832
|
-
this.exportProgressBar = document.createElement('div');
|
|
9833
|
-
this.exportProgressBar.className = 'bg-blue-600 h-2 rounded-full transition-all duration-300';
|
|
9834
|
-
this.exportProgressBar.style.width = '0%';
|
|
9835
|
-
progressBarContainer.appendChild(this.exportProgressBar);
|
|
9836
|
-
this.exportProgressText = document.createElement('p');
|
|
9837
|
-
this.exportProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
9838
|
-
this.exportProgressText.textContent = t('importExport.preparingExport');
|
|
9839
|
-
progressContainer.appendChild(progressBarContainer);
|
|
9840
|
-
progressContainer.appendChild(this.exportProgressText);
|
|
9841
|
-
formContainer.appendChild(progressContainer);
|
|
9842
|
-
// Export Button
|
|
9843
|
-
const exportButton = new Button({
|
|
9844
|
-
text: t('importExport.exportRegion'),
|
|
10212
|
+
const section = this.createSection(t('mbtiles.exportTitle'), 'blue', icons.download({ size: 20, color: 'currentColor' }));
|
|
10213
|
+
const form = document.createElement('div');
|
|
10214
|
+
form.className = 'space-y-5';
|
|
10215
|
+
const hint = document.createElement('p');
|
|
10216
|
+
hint.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10217
|
+
hint.textContent = t('mbtiles.exportHint');
|
|
10218
|
+
form.appendChild(hint);
|
|
10219
|
+
// Progress (hidden by default)
|
|
10220
|
+
const progress = this.createProgressBlock('blue', t('mbtiles.preparingExport'));
|
|
10221
|
+
this.exportProgressContainer = progress.container;
|
|
10222
|
+
this.exportProgressBar = progress.bar;
|
|
10223
|
+
this.exportProgressText = progress.text;
|
|
10224
|
+
form.appendChild(progress.container);
|
|
10225
|
+
const exportBtn = new Button({
|
|
10226
|
+
text: t('mbtiles.exportButton'),
|
|
9845
10227
|
variant: 'primary',
|
|
9846
10228
|
icon: icons.download({ size: 16, color: 'white' }),
|
|
9847
|
-
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
10229
|
+
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
9848
10230
|
onClick: () => this.handleExport(),
|
|
9849
10231
|
});
|
|
9850
|
-
this.exportButton =
|
|
9851
|
-
|
|
9852
|
-
section.appendChild(
|
|
10232
|
+
this.exportButton = exportBtn.getElement();
|
|
10233
|
+
form.appendChild(this.exportButton);
|
|
10234
|
+
section.appendChild(form);
|
|
9853
10235
|
return section;
|
|
9854
10236
|
}
|
|
9855
10237
|
createImportSection() {
|
|
9856
|
-
const section =
|
|
9857
|
-
|
|
9858
|
-
|
|
9859
|
-
//
|
|
9860
|
-
const accent = document.createElement('div');
|
|
9861
|
-
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-green-500 opacity-50`;
|
|
9862
|
-
section.appendChild(accent);
|
|
9863
|
-
const header = document.createElement('h3');
|
|
9864
|
-
header.className =
|
|
9865
|
-
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
9866
|
-
header.innerHTML = `
|
|
9867
|
-
<div class="p-2 bg-green-500/10 rounded-lg text-green-600 dark:text-green-400">
|
|
9868
|
-
${icons.upload({ size: 20, color: 'currentColor' })}
|
|
9869
|
-
</div>
|
|
9870
|
-
${t('importExport.importRegion')}
|
|
9871
|
-
`;
|
|
9872
|
-
section.appendChild(header);
|
|
9873
|
-
const formContainer = document.createElement('div');
|
|
9874
|
-
formContainer.className = 'space-y-5';
|
|
9875
|
-
// File Selection
|
|
10238
|
+
const section = this.createSection(t('mbtiles.importTitle'), 'green', icons.upload({ size: 20, color: 'currentColor' }));
|
|
10239
|
+
const form = document.createElement('div');
|
|
10240
|
+
form.className = 'space-y-5';
|
|
10241
|
+
// File input
|
|
9876
10242
|
const fileGroup = document.createElement('div');
|
|
9877
10243
|
const fileLabel = document.createElement('label');
|
|
9878
10244
|
fileLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9879
|
-
fileLabel.textContent = t('
|
|
10245
|
+
fileLabel.textContent = t('mbtiles.selectFile');
|
|
9880
10246
|
this.importFileInput = document.createElement('input');
|
|
9881
10247
|
this.importFileInput.type = 'file';
|
|
9882
|
-
this.importFileInput.accept = '.
|
|
10248
|
+
this.importFileInput.accept = '.mbtiles,application/vnd.sqlite3,application/x-sqlite3';
|
|
9883
10249
|
this.importFileInput.className =
|
|
9884
10250
|
'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';
|
|
9885
10251
|
const fileHint = document.createElement('p');
|
|
9886
10252
|
fileHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
9887
|
-
fileHint.textContent = t('
|
|
10253
|
+
fileHint.textContent = t('mbtiles.fileHint');
|
|
9888
10254
|
fileGroup.appendChild(fileLabel);
|
|
9889
10255
|
fileGroup.appendChild(this.importFileInput);
|
|
9890
10256
|
fileGroup.appendChild(fileHint);
|
|
9891
|
-
|
|
9892
|
-
// New
|
|
10257
|
+
form.appendChild(fileGroup);
|
|
10258
|
+
// New region name
|
|
9893
10259
|
const nameGroup = document.createElement('div');
|
|
9894
10260
|
const nameLabel = document.createElement('label');
|
|
9895
10261
|
nameLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9896
|
-
nameLabel.textContent = t('
|
|
10262
|
+
nameLabel.textContent = t('mbtiles.newRegionName');
|
|
9897
10263
|
this.importNameInput = document.createElement('input');
|
|
9898
10264
|
this.importNameInput.type = 'text';
|
|
9899
|
-
this.importNameInput.placeholder = t('
|
|
10265
|
+
this.importNameInput.placeholder = t('mbtiles.newRegionNamePlaceholder');
|
|
9900
10266
|
this.importNameInput.className =
|
|
9901
10267
|
'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';
|
|
9902
10268
|
nameGroup.appendChild(nameLabel);
|
|
9903
10269
|
nameGroup.appendChild(this.importNameInput);
|
|
9904
|
-
|
|
9905
|
-
//
|
|
10270
|
+
form.appendChild(nameGroup);
|
|
10271
|
+
// Overwrite toggle
|
|
9906
10272
|
const overwriteLabel = document.createElement('label');
|
|
9907
10273
|
overwriteLabel.className =
|
|
9908
10274
|
'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';
|
|
@@ -9912,79 +10278,85 @@ class ImportExportModal {
|
|
|
9912
10278
|
'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';
|
|
9913
10279
|
const overwriteSpan = document.createElement('span');
|
|
9914
10280
|
overwriteSpan.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
9915
|
-
overwriteSpan.textContent = t('
|
|
10281
|
+
overwriteSpan.textContent = t('mbtiles.overwriteIfExists');
|
|
9916
10282
|
overwriteLabel.appendChild(this.importOverwriteCheckbox);
|
|
9917
10283
|
overwriteLabel.appendChild(overwriteSpan);
|
|
9918
|
-
|
|
9919
|
-
//
|
|
9920
|
-
const
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
this.importProgressText = document.createElement('p');
|
|
9930
|
-
this.importProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
9931
|
-
this.importProgressText.textContent = t('importExport.preparingImport');
|
|
9932
|
-
progressContainer.appendChild(progressBarContainer);
|
|
9933
|
-
progressContainer.appendChild(this.importProgressText);
|
|
9934
|
-
formContainer.appendChild(progressContainer);
|
|
9935
|
-
// Import Button
|
|
9936
|
-
const importButton = new Button({
|
|
9937
|
-
text: t('importExport.importRegion'),
|
|
9938
|
-
variant: 'success', // Assuming 'success' variant exists in Button component, if not might need style adjustment. Assuming it works based on previous code.
|
|
10284
|
+
form.appendChild(overwriteLabel);
|
|
10285
|
+
// Progress
|
|
10286
|
+
const progress = this.createProgressBlock('green', t('mbtiles.preparingImport'));
|
|
10287
|
+
this.importProgressContainer = progress.container;
|
|
10288
|
+
this.importProgressBar = progress.bar;
|
|
10289
|
+
this.importProgressText = progress.text;
|
|
10290
|
+
form.appendChild(progress.container);
|
|
10291
|
+
// Import button (disabled until a file is selected)
|
|
10292
|
+
const importBtn = new Button({
|
|
10293
|
+
text: t('mbtiles.importButton'),
|
|
10294
|
+
variant: 'success',
|
|
9939
10295
|
icon: icons.upload({ size: 16, color: 'white' }),
|
|
9940
10296
|
className: 'w-full py-2.5 text-base shadow-lg shadow-green-500/20',
|
|
9941
10297
|
disabled: true,
|
|
9942
10298
|
onClick: () => this.handleImport(),
|
|
9943
10299
|
});
|
|
9944
|
-
this.importButton =
|
|
9945
|
-
|
|
9946
|
-
section.appendChild(
|
|
10300
|
+
this.importButton = importBtn.getElement();
|
|
10301
|
+
form.appendChild(this.importButton);
|
|
10302
|
+
section.appendChild(form);
|
|
9947
10303
|
return section;
|
|
9948
10304
|
}
|
|
9949
|
-
|
|
9950
|
-
const
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
${
|
|
9955
|
-
|
|
9956
|
-
|
|
9957
|
-
|
|
9958
|
-
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
9963
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">PMTiles</div>
|
|
9964
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.pmtilesDesc')}</div>
|
|
9965
|
-
</div>
|
|
9966
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
9967
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">MBTiles</div>
|
|
9968
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.mbtilesDesc')}</div>
|
|
9969
|
-
</div>
|
|
10305
|
+
createSection(title, accentColor, iconHtml) {
|
|
10306
|
+
const section = document.createElement('div');
|
|
10307
|
+
section.className =
|
|
10308
|
+
'glass-input p-6 rounded-xl border-0 bg-white/40 dark:bg-gray-800/40 relative overflow-hidden';
|
|
10309
|
+
const accent = document.createElement('div');
|
|
10310
|
+
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-${accentColor}-500 opacity-50`;
|
|
10311
|
+
section.appendChild(accent);
|
|
10312
|
+
const header = document.createElement('h3');
|
|
10313
|
+
header.className =
|
|
10314
|
+
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
10315
|
+
header.innerHTML = `
|
|
10316
|
+
<div class="p-2 bg-${accentColor}-500/10 rounded-lg text-${accentColor}-600 dark:text-${accentColor}-400">
|
|
10317
|
+
${iconHtml}
|
|
9970
10318
|
</div>
|
|
10319
|
+
${title}
|
|
9971
10320
|
`;
|
|
9972
|
-
|
|
10321
|
+
section.appendChild(header);
|
|
10322
|
+
return section;
|
|
10323
|
+
}
|
|
10324
|
+
createProgressBlock(accentColor, initialText) {
|
|
10325
|
+
const container = document.createElement('div');
|
|
10326
|
+
container.className = 'hidden';
|
|
10327
|
+
const barWrap = document.createElement('div');
|
|
10328
|
+
barWrap.className = 'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
10329
|
+
const bar = document.createElement('div');
|
|
10330
|
+
bar.className = `bg-${accentColor}-600 h-2 rounded-full transition-all duration-300`;
|
|
10331
|
+
bar.style.width = '0%';
|
|
10332
|
+
barWrap.appendChild(bar);
|
|
10333
|
+
const text = document.createElement('p');
|
|
10334
|
+
text.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10335
|
+
text.textContent = initialText;
|
|
10336
|
+
container.appendChild(barWrap);
|
|
10337
|
+
container.appendChild(text);
|
|
10338
|
+
return { container, bar, text };
|
|
10339
|
+
}
|
|
10340
|
+
createFooter() {
|
|
10341
|
+
const footer = document.createElement('div');
|
|
10342
|
+
footer.className = 'flex gap-3 justify-end';
|
|
10343
|
+
if (i18n.isRTL()) {
|
|
10344
|
+
footer.setAttribute('dir', 'rtl');
|
|
10345
|
+
}
|
|
10346
|
+
const close = new Button({
|
|
10347
|
+
text: t('app.close'),
|
|
10348
|
+
variant: 'secondary',
|
|
10349
|
+
onClick: () => this.hide(),
|
|
10350
|
+
});
|
|
10351
|
+
footer.appendChild(close.getElement());
|
|
10352
|
+
return footer;
|
|
9973
10353
|
}
|
|
9974
10354
|
attachEventListeners() {
|
|
9975
|
-
// Enable import button when file is selected
|
|
9976
10355
|
if (this.importFileInput && this.importButton) {
|
|
9977
10356
|
this.importFileInput.addEventListener('change', () => {
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
}
|
|
9982
|
-
}
|
|
9983
|
-
else {
|
|
9984
|
-
if (this.importButton) {
|
|
9985
|
-
this.importButton.disabled = true;
|
|
9986
|
-
}
|
|
9987
|
-
}
|
|
10357
|
+
const hasFile = !!(this.importFileInput?.files && this.importFileInput.files.length > 0);
|
|
10358
|
+
if (this.importButton)
|
|
10359
|
+
this.importButton.disabled = !hasFile;
|
|
9988
10360
|
});
|
|
9989
10361
|
}
|
|
9990
10362
|
}
|
|
@@ -9994,34 +10366,27 @@ class ImportExportModal {
|
|
|
9994
10366
|
this.isExporting = true;
|
|
9995
10367
|
if (this.exportButton)
|
|
9996
10368
|
this.exportButton.disabled = true;
|
|
10369
|
+
this.exportProgressContainer?.classList.remove('hidden');
|
|
9997
10370
|
try {
|
|
9998
|
-
const
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
if (progressContainer) {
|
|
10008
|
-
progressContainer.classList.remove('hidden');
|
|
10009
|
-
}
|
|
10010
|
-
const result = await this.options.exportRegion(this.options.region.id, format, options);
|
|
10011
|
-
if (this.exportProgressBar) {
|
|
10371
|
+
const result = await this.options.exportRegion(this.options.region.id, {
|
|
10372
|
+
onProgress: p => {
|
|
10373
|
+
if (this.exportProgressBar)
|
|
10374
|
+
this.exportProgressBar.style.width = `${p.percentage}%`;
|
|
10375
|
+
if (this.exportProgressText)
|
|
10376
|
+
this.exportProgressText.textContent = p.message;
|
|
10377
|
+
},
|
|
10378
|
+
});
|
|
10379
|
+
if (this.exportProgressBar)
|
|
10012
10380
|
this.exportProgressBar.style.width = '100%';
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
this.exportProgressText.textContent = t('importExport.exportComplete');
|
|
10016
|
-
}
|
|
10381
|
+
if (this.exportProgressText)
|
|
10382
|
+
this.exportProgressText.textContent = t('mbtiles.exportComplete');
|
|
10017
10383
|
this.options.onExport?.(result);
|
|
10018
|
-
|
|
10019
|
-
setTimeout(() => this.hide(), 1500);
|
|
10384
|
+
setTimeout(() => this.hide(), 1200);
|
|
10020
10385
|
}
|
|
10021
10386
|
catch (error) {
|
|
10022
10387
|
modalLogger.error('Export error:', error instanceof Error ? error.message : String(error));
|
|
10023
10388
|
if (this.exportProgressText) {
|
|
10024
|
-
this.exportProgressText.textContent = t('
|
|
10389
|
+
this.exportProgressText.textContent = t('mbtiles.exportFailed');
|
|
10025
10390
|
this.exportProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10026
10391
|
}
|
|
10027
10392
|
}
|
|
@@ -10032,45 +10397,47 @@ class ImportExportModal {
|
|
|
10032
10397
|
}
|
|
10033
10398
|
}
|
|
10034
10399
|
async handleImport() {
|
|
10035
|
-
if (this.isImporting || !this.options.importRegion
|
|
10400
|
+
if (this.isImporting || !this.options.importRegion)
|
|
10401
|
+
return;
|
|
10402
|
+
const file = this.importFileInput?.files?.[0];
|
|
10403
|
+
if (!file)
|
|
10036
10404
|
return;
|
|
10037
10405
|
this.isImporting = true;
|
|
10038
10406
|
if (this.importButton)
|
|
10039
10407
|
this.importButton.disabled = true;
|
|
10408
|
+
this.importProgressContainer?.classList.remove('hidden');
|
|
10040
10409
|
try {
|
|
10041
|
-
const file = this.importFileInput.files[0];
|
|
10042
|
-
const overwrite = this.importOverwriteCheckbox?.checked ?? false;
|
|
10043
|
-
// Show progress
|
|
10044
|
-
const progressContainer = this.importProgressBar?.parentElement?.parentElement;
|
|
10045
|
-
if (progressContainer) {
|
|
10046
|
-
progressContainer.classList.remove('hidden');
|
|
10047
|
-
}
|
|
10048
|
-
// Determine format from file extension
|
|
10049
|
-
const format = file.name.endsWith('.pmtiles')
|
|
10050
|
-
? 'pmtiles'
|
|
10051
|
-
: file.name.endsWith('.mbtiles')
|
|
10052
|
-
? 'mbtiles'
|
|
10053
|
-
: 'json';
|
|
10054
10410
|
const data = {
|
|
10055
10411
|
file,
|
|
10056
|
-
format,
|
|
10057
|
-
overwrite,
|
|
10412
|
+
format: 'mbtiles',
|
|
10413
|
+
overwrite: this.importOverwriteCheckbox?.checked ?? false,
|
|
10414
|
+
newRegionName: this.importNameInput?.value.trim() || undefined,
|
|
10415
|
+
onProgress: p => {
|
|
10416
|
+
if (this.importProgressBar)
|
|
10417
|
+
this.importProgressBar.style.width = `${p.percentage}%`;
|
|
10418
|
+
if (this.importProgressText)
|
|
10419
|
+
this.importProgressText.textContent = p.message;
|
|
10420
|
+
},
|
|
10058
10421
|
};
|
|
10059
10422
|
const result = await this.options.importRegion(data);
|
|
10060
|
-
if (this.importProgressBar)
|
|
10423
|
+
if (this.importProgressBar)
|
|
10061
10424
|
this.importProgressBar.style.width = '100%';
|
|
10062
|
-
}
|
|
10063
10425
|
if (this.importProgressText) {
|
|
10064
|
-
this.importProgressText.textContent =
|
|
10426
|
+
this.importProgressText.textContent = result.success
|
|
10427
|
+
? t('mbtiles.importComplete')
|
|
10428
|
+
: result.message;
|
|
10429
|
+
if (!result.success) {
|
|
10430
|
+
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10431
|
+
}
|
|
10065
10432
|
}
|
|
10066
10433
|
this.options.onImport?.(result);
|
|
10067
|
-
|
|
10068
|
-
|
|
10434
|
+
if (result.success)
|
|
10435
|
+
setTimeout(() => this.hide(), 1200);
|
|
10069
10436
|
}
|
|
10070
10437
|
catch (error) {
|
|
10071
10438
|
modalLogger.error('Import error:', error instanceof Error ? error.message : String(error));
|
|
10072
10439
|
if (this.importProgressText) {
|
|
10073
|
-
this.importProgressText.textContent = t('
|
|
10440
|
+
this.importProgressText.textContent = t('mbtiles.importFailed');
|
|
10074
10441
|
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10075
10442
|
}
|
|
10076
10443
|
}
|
|
@@ -10080,23 +10447,6 @@ class ImportExportModal {
|
|
|
10080
10447
|
this.importButton.disabled = false;
|
|
10081
10448
|
}
|
|
10082
10449
|
}
|
|
10083
|
-
createFooter() {
|
|
10084
|
-
const footer = document.createElement('div');
|
|
10085
|
-
footer.className = 'flex gap-3 justify-end';
|
|
10086
|
-
if (i18n.isRTL()) {
|
|
10087
|
-
footer.setAttribute('dir', 'rtl');
|
|
10088
|
-
}
|
|
10089
|
-
const closeButton = new Button({
|
|
10090
|
-
text: t('app.close'),
|
|
10091
|
-
variant: 'secondary',
|
|
10092
|
-
onClick: () => this.hide(),
|
|
10093
|
-
});
|
|
10094
|
-
footer.appendChild(closeButton.getElement());
|
|
10095
|
-
return footer;
|
|
10096
|
-
}
|
|
10097
|
-
destroy() {
|
|
10098
|
-
this.modal?.destroy();
|
|
10099
|
-
}
|
|
10100
10450
|
}
|
|
10101
10451
|
|
|
10102
10452
|
/**
|
|
@@ -10810,9 +11160,9 @@ class PanelRenderer extends BaseComponent {
|
|
|
10810
11160
|
<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')}">
|
|
10811
11161
|
${icons.download({ size: 14, color: 'currentColor' })}
|
|
10812
11162
|
</button>
|
|
10813
|
-
|
|
11163
|
+
<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')}">
|
|
10814
11164
|
${icons.deviceFloppy({ size: 14, color: 'currentColor' })}
|
|
10815
|
-
</button>
|
|
11165
|
+
</button>
|
|
10816
11166
|
<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')}">
|
|
10817
11167
|
${icons.trash({ size: 14, color: 'currentColor' })}
|
|
10818
11168
|
</button>
|
|
@@ -11074,7 +11424,7 @@ class PanelRenderer extends BaseComponent {
|
|
|
11074
11424
|
}
|
|
11075
11425
|
}
|
|
11076
11426
|
/**
|
|
11077
|
-
*
|
|
11427
|
+
* Show the MBTiles import/export modal for a region.
|
|
11078
11428
|
*/
|
|
11079
11429
|
async handleImportExport(regionId, _regionData) {
|
|
11080
11430
|
try {
|
|
@@ -11082,44 +11432,26 @@ class PanelRenderer extends BaseComponent {
|
|
|
11082
11432
|
const region = regions.find((r) => r.id === regionId);
|
|
11083
11433
|
if (!region)
|
|
11084
11434
|
return;
|
|
11085
|
-
const
|
|
11435
|
+
const mbtilesModal = new MBTilesModal({
|
|
11086
11436
|
region,
|
|
11087
|
-
onClose: () =>
|
|
11088
|
-
this.modalManager.close();
|
|
11089
|
-
},
|
|
11437
|
+
onClose: () => this.modalManager.close(),
|
|
11090
11438
|
onExport: result => {
|
|
11091
|
-
panelLogger.debug('
|
|
11092
|
-
// Handle export result - could show success message
|
|
11439
|
+
panelLogger.debug('MBTiles export completed:', result);
|
|
11093
11440
|
this.offlineManager.downloadExportedRegion(result);
|
|
11094
11441
|
},
|
|
11095
11442
|
onImport: result => {
|
|
11096
|
-
panelLogger.debug('
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
},
|
|
11100
|
-
exportRegion: async (regionId, format, options) => {
|
|
11101
|
-
// Delegate to offline manager's export functionality
|
|
11102
|
-
switch (format) {
|
|
11103
|
-
case 'json':
|
|
11104
|
-
return await this.offlineManager.exportRegionAsJSON(regionId, options);
|
|
11105
|
-
case 'pmtiles':
|
|
11106
|
-
return await this.offlineManager.exportRegionAsPMTiles(regionId, options);
|
|
11107
|
-
case 'mbtiles':
|
|
11108
|
-
return await this.offlineManager.exportRegionAsMBTiles(regionId, options);
|
|
11109
|
-
default:
|
|
11110
|
-
throw new Error(`Unsupported export format: ${format}`);
|
|
11111
|
-
}
|
|
11112
|
-
},
|
|
11113
|
-
importRegion: async (data) => {
|
|
11114
|
-
// Delegate to offline manager's import functionality
|
|
11115
|
-
return await this.offlineManager.importRegion(data);
|
|
11443
|
+
panelLogger.debug('MBTiles import completed:', result);
|
|
11444
|
+
if (result.success)
|
|
11445
|
+
this.refresh();
|
|
11116
11446
|
},
|
|
11447
|
+
exportRegion: (id, options) => this.offlineManager.exportRegionAsMBTiles(id, options),
|
|
11448
|
+
importRegion: data => this.offlineManager.importRegion(data),
|
|
11117
11449
|
});
|
|
11118
|
-
const modal =
|
|
11450
|
+
const modal = mbtilesModal.show();
|
|
11119
11451
|
this.modalManager.show(modal);
|
|
11120
11452
|
}
|
|
11121
11453
|
catch (error) {
|
|
11122
|
-
panelLogger.error('Error showing
|
|
11454
|
+
panelLogger.error('Error showing MBTiles modal:', error);
|
|
11123
11455
|
}
|
|
11124
11456
|
}
|
|
11125
11457
|
/**
|
|
@@ -11518,6 +11850,9 @@ class PanelRenderer extends BaseComponent {
|
|
|
11518
11850
|
delete patchedStyle.imports;
|
|
11519
11851
|
panelLogger.debug('Stripped imports from offline style (already flattened)');
|
|
11520
11852
|
}
|
|
11853
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
11854
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
11855
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
11521
11856
|
// Enforce maxzoom for all tile sources to prevent requesting non-existent tiles
|
|
11522
11857
|
// Find the maximum zoom level from all regions using this style
|
|
11523
11858
|
let maxZoom = 14; // Default fallback
|
|
@@ -11566,10 +11901,12 @@ class PanelRenderer extends BaseComponent {
|
|
|
11566
11901
|
panelLogger.debug(`Fixed legacy source ${sourceId}: added tiles, removed url`);
|
|
11567
11902
|
}
|
|
11568
11903
|
}
|
|
11569
|
-
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11904
|
+
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11905
|
+
// buildings and raster-array for Mapbox Standard landmark icons).
|
|
11570
11906
|
if (src.type === 'vector' ||
|
|
11571
11907
|
src.type === 'raster' ||
|
|
11572
11908
|
src.type === 'raster-dem' ||
|
|
11909
|
+
src.type === 'raster-array' ||
|
|
11573
11910
|
src.type === 'batched-model') {
|
|
11574
11911
|
const originalMaxzoom = src.maxzoom;
|
|
11575
11912
|
// Use the lower of region maxZoom and source's original maxzoom so we
|
|
@@ -12257,9 +12594,7 @@ class RegionFormModal {
|
|
|
12257
12594
|
detectProviderFromUrl() {
|
|
12258
12595
|
const styleUrl = this.styleUrlInput?.value || '';
|
|
12259
12596
|
// Simple detection logic
|
|
12260
|
-
if (styleUrl.startsWith('mapbox://') ||
|
|
12261
|
-
styleUrl.includes('mapbox.com') ||
|
|
12262
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
12597
|
+
if (styleUrl.startsWith('mapbox://') || isMapboxHost(styleUrl)) {
|
|
12263
12598
|
if (this.providerSelect)
|
|
12264
12599
|
this.providerSelect.value = 'mapbox';
|
|
12265
12600
|
this.toggleAccessTokenVisibility(true);
|
|
@@ -13023,39 +13358,39 @@ class OfflineManagerControl {
|
|
|
13023
13358
|
}
|
|
13024
13359
|
// Development proxy for CORS issues (when running on localhost)
|
|
13025
13360
|
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
13029
|
-
if (isTileRequest && url.includes('tiles-a.basemaps.cartocdn.com')) {
|
|
13030
|
-
const proxyUrl = url.replace('https://tiles-a.basemaps.cartocdn.com', '/tiles/carto-a');
|
|
13031
|
-
return originalFetch(proxyUrl, init);
|
|
13032
|
-
}
|
|
13033
|
-
if (isTileRequest && url.includes('tiles-b.basemaps.cartocdn.com')) {
|
|
13034
|
-
const proxyUrl = url.replace('https://tiles-b.basemaps.cartocdn.com', '/tiles/carto-b');
|
|
13035
|
-
return originalFetch(proxyUrl, init);
|
|
13361
|
+
let parsed = null;
|
|
13362
|
+
try {
|
|
13363
|
+
parsed = new URL(url, location.origin);
|
|
13036
13364
|
}
|
|
13037
|
-
|
|
13038
|
-
|
|
13039
|
-
return originalFetch(proxyUrl, init);
|
|
13365
|
+
catch {
|
|
13366
|
+
parsed = null;
|
|
13040
13367
|
}
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13368
|
+
const hostname = parsed?.hostname ?? '';
|
|
13369
|
+
const pathAndQuery = parsed ? parsed.pathname + parsed.search : '';
|
|
13370
|
+
// Proxy Carto tile requests (tiles and TileJSON)
|
|
13371
|
+
const isTileRequest = /\/\d+\/\d+\/\d+\.(pbf|mvt|png|jpg|jpeg|webp)/.test(parsed?.pathname ?? '');
|
|
13372
|
+
const isTileJsonRequest = (parsed?.pathname.endsWith('.json') ?? false) &&
|
|
13373
|
+
(hostname === 'basemaps.cartocdn.com' || hostname.endsWith('.basemaps.cartocdn.com'));
|
|
13374
|
+
const cartoSubdomainProxy = {
|
|
13375
|
+
'tiles-a.basemaps.cartocdn.com': '/tiles/carto-a',
|
|
13376
|
+
'tiles-b.basemaps.cartocdn.com': '/tiles/carto-b',
|
|
13377
|
+
'tiles-c.basemaps.cartocdn.com': '/tiles/carto-c',
|
|
13378
|
+
'tiles-d.basemaps.cartocdn.com': '/tiles/carto-d',
|
|
13379
|
+
};
|
|
13380
|
+
if (isTileRequest && cartoSubdomainProxy[hostname]) {
|
|
13381
|
+
return originalFetch(cartoSubdomainProxy[hostname] + pathAndQuery, init);
|
|
13044
13382
|
}
|
|
13045
13383
|
// Proxy TileJSON requests from tiles.basemaps.cartocdn.com
|
|
13046
|
-
if (isTileJsonRequest &&
|
|
13047
|
-
|
|
13048
|
-
return originalFetch(proxyUrl, init);
|
|
13384
|
+
if (isTileJsonRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13385
|
+
return originalFetch('/carto-api' + pathAndQuery, init);
|
|
13049
13386
|
}
|
|
13050
13387
|
// Fallback for old format (tiles without subdomain)
|
|
13051
|
-
if (isTileRequest &&
|
|
13052
|
-
|
|
13053
|
-
return originalFetch(proxyUrl, init);
|
|
13388
|
+
if (isTileRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13389
|
+
return originalFetch('/tiles/carto-a' + pathAndQuery, init);
|
|
13054
13390
|
}
|
|
13055
13391
|
// Proxy OpenStreetMap tile requests
|
|
13056
|
-
if (
|
|
13057
|
-
|
|
13058
|
-
return originalFetch(proxyUrl, init);
|
|
13392
|
+
if (hostname === 'tile.openstreetmap.org') {
|
|
13393
|
+
return originalFetch('/tiles/osm' + pathAndQuery, init);
|
|
13059
13394
|
}
|
|
13060
13395
|
}
|
|
13061
13396
|
return originalFetch(input, init);
|
|
@@ -13488,6 +13823,9 @@ class OfflineManagerControl {
|
|
|
13488
13823
|
if (patchedStyle.imports) {
|
|
13489
13824
|
delete patchedStyle.imports;
|
|
13490
13825
|
}
|
|
13826
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
13827
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
13828
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
13491
13829
|
// If using Service Worker (Mapbox GL JS), convert idb:// to /__offline__/ URLs
|
|
13492
13830
|
if (this.useServiceWorker) {
|
|
13493
13831
|
if (this.swReadyPromise) {
|
|
@@ -13650,6 +13988,7 @@ exports.MAPBOX_CACHE_TTL = MAPBOX_CACHE_TTL;
|
|
|
13650
13988
|
exports.MAPBOX_CLASSIC_STYLES = MAPBOX_CLASSIC_STYLES;
|
|
13651
13989
|
exports.MAP_PROVIDERS = MAP_PROVIDERS;
|
|
13652
13990
|
exports.MaintenanceService = MaintenanceService;
|
|
13991
|
+
exports.ModelService = ModelService;
|
|
13653
13992
|
exports.OfflineManagerControl = OfflineManagerControl;
|
|
13654
13993
|
exports.OfflineMapDBVersionError = OfflineMapDBVersionError;
|
|
13655
13994
|
exports.OfflineMapManager = OfflineMapManager;
|
|
@@ -13672,6 +14011,7 @@ exports.cleanupCompressedTiles = cleanupCompressedTiles;
|
|
|
13672
14011
|
exports.cleanupExpiredTiles = cleanupExpiredTiles;
|
|
13673
14012
|
exports.cleanupOldFonts = cleanupOldFonts;
|
|
13674
14013
|
exports.cleanupOldGlyphs = cleanupOldGlyphs;
|
|
14014
|
+
exports.cleanupOldModels = cleanupOldModels;
|
|
13675
14015
|
exports.cleanupOldSprites = cleanupOldSprites;
|
|
13676
14016
|
exports.cleanupOldStyles = cleanupOldStyles;
|
|
13677
14017
|
exports.cleanupOldTiles = cleanupOldTiles;
|
|
@@ -13679,6 +14019,7 @@ exports.cleanupService = cleanupService;
|
|
|
13679
14019
|
exports.clearAllCaches = clearAllCaches;
|
|
13680
14020
|
exports.configureLogger = configureLogger;
|
|
13681
14021
|
exports.configureProxy = configureProxy;
|
|
14022
|
+
exports.configureSqlJs = configureSqlJs;
|
|
13682
14023
|
exports.convertStyleForServiceWorker = convertStyleForServiceWorker;
|
|
13683
14024
|
exports.countCompressedTiles = countCompressedTiles;
|
|
13684
14025
|
exports.createProgressTracker = createProgressTracker;
|
|
@@ -13692,6 +14033,7 @@ exports.detectCssPrefix = detectCssPrefix;
|
|
|
13692
14033
|
exports.detectStyleProvider = detectStyleProvider;
|
|
13693
14034
|
exports.downloadFonts = downloadFonts;
|
|
13694
14035
|
exports.downloadGlyphs = downloadGlyphs;
|
|
14036
|
+
exports.downloadModels = downloadModels;
|
|
13695
14037
|
exports.downloadSprites = downloadSprites;
|
|
13696
14038
|
exports.downloadStyleWithProvider = downloadStyleWithProvider;
|
|
13697
14039
|
exports.downloadStyles = downloadStyles;
|
|
@@ -13700,6 +14042,7 @@ exports.escapeHtml = escapeHtml$1;
|
|
|
13700
14042
|
exports.extractAccessToken = extractAccessToken;
|
|
13701
14043
|
exports.extractAllFontNames = extractAllFontNames;
|
|
13702
14044
|
exports.extractFontNamesFromTextField = extractFontNamesFromTextField;
|
|
14045
|
+
exports.extractTileExtensionFromUrl = extractTileExtensionFromUrl;
|
|
13703
14046
|
exports.fetchResourceWithRetry = fetchResourceWithRetry;
|
|
13704
14047
|
exports.fetchWithRetry = fetchWithRetry;
|
|
13705
14048
|
exports.fontService = fontService;
|
|
@@ -13712,18 +14055,24 @@ exports.getFontStats = getFontStats;
|
|
|
13712
14055
|
exports.getGlyphAnalytics = getGlyphAnalytics;
|
|
13713
14056
|
exports.getGlyphStats = getGlyphStats;
|
|
13714
14057
|
exports.getIcon = getIcon;
|
|
14058
|
+
exports.getModel = getModel;
|
|
14059
|
+
exports.getModelStats = getModelStats;
|
|
13715
14060
|
exports.getRegionAnalytics = getRegionAnalytics;
|
|
13716
14061
|
exports.getSpriteAnalytics = getSpriteAnalytics;
|
|
13717
14062
|
exports.getSpriteStats = getSpriteStats;
|
|
14063
|
+
exports.getSqlJs = getSqlJs;
|
|
13718
14064
|
exports.getStyleStats = getStyleStats;
|
|
13719
14065
|
exports.getTileAnalytics = getTileAnalytics;
|
|
13720
14066
|
exports.getTileStats = getTileStats;
|
|
14067
|
+
exports.getUrlHostname = getUrlHostname;
|
|
13721
14068
|
exports.getUserErrorMessage = getUserErrorMessage;
|
|
13722
14069
|
exports.glyphService = glyphService;
|
|
13723
14070
|
exports.hasImports = hasImports;
|
|
14071
|
+
exports.hostMatches = hostMatches;
|
|
13724
14072
|
exports.i18n = i18n;
|
|
13725
14073
|
exports.icons = icons;
|
|
13726
14074
|
exports.idbFetchHandler = idbFetchHandler;
|
|
14075
|
+
exports.isMapboxHost = isMapboxHost;
|
|
13727
14076
|
exports.isMapboxProtocol = isMapboxProtocol;
|
|
13728
14077
|
exports.isStyleDownloaded = isStyleDownloaded;
|
|
13729
14078
|
exports.loadAllStoredRegions = loadAllStoredRegions;
|
|
@@ -13731,6 +14080,8 @@ exports.loadGlyphs = loadGlyphs;
|
|
|
13731
14080
|
exports.loadStyleById = loadStyleById;
|
|
13732
14081
|
exports.loadStyles = loadStyles;
|
|
13733
14082
|
exports.logger = logger;
|
|
14083
|
+
exports.modelKeyBelongsToStyle = modelKeyBelongsToStyle;
|
|
14084
|
+
exports.modelService = modelService;
|
|
13734
14085
|
exports.normalizeSpriteProperty = normalizeSpriteProperty;
|
|
13735
14086
|
exports.normalizeStyleUrl = normalizeStyleUrl;
|
|
13736
14087
|
exports.optimizeStorage = optimizeStorage;
|
|
@@ -13747,6 +14098,7 @@ exports.resolveMapboxUrl = resolveMapboxUrl;
|
|
|
13747
14098
|
exports.resourceKeyBelongsToStyle = resourceKeyBelongsToStyle;
|
|
13748
14099
|
exports.rewriteMapboxCdnTileUrl = rewriteMapboxCdnTileUrl;
|
|
13749
14100
|
exports.safeExecute = safeExecute;
|
|
14101
|
+
exports.sanitizeIndoorExpressions = sanitizeIndoorExpressions;
|
|
13750
14102
|
exports.setupAutoCleanup = setupAutoCleanup;
|
|
13751
14103
|
exports.spriteService = spriteService;
|
|
13752
14104
|
exports.stopAutoCleanup = stopAutoCleanup;
|
|
@@ -13760,5 +14112,6 @@ exports.validateStyleForProvider = validateStyleForProvider;
|
|
|
13760
14112
|
exports.validateZoomLevels = validateZoomLevels;
|
|
13761
14113
|
exports.verifyAndRepairFonts = verifyAndRepairFonts;
|
|
13762
14114
|
exports.verifyAndRepairGlyphs = verifyAndRepairGlyphs;
|
|
14115
|
+
exports.verifyAndRepairModels = verifyAndRepairModels;
|
|
13763
14116
|
exports.verifyAndRepairSprites = verifyAndRepairSprites;
|
|
13764
14117
|
//# sourceMappingURL=index.js.map
|