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.umd.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
// IndexedDB Configuration
|
|
31
31
|
const DB_NAME = 'offline-map-db';
|
|
32
|
-
const DB_VERSION =
|
|
32
|
+
const DB_VERSION = 4;
|
|
33
33
|
// Store Names (regions are stored inside styles.regions[], not as a separate store)
|
|
34
34
|
const STORE_NAMES = {
|
|
35
35
|
TILES: 'tiles',
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
SPRITES: 'sprites',
|
|
38
38
|
GLYPHS: 'glyphs',
|
|
39
39
|
FONTS: 'fonts',
|
|
40
|
+
MODELS: 'models',
|
|
40
41
|
};
|
|
41
42
|
// Download Configuration
|
|
42
43
|
const DOWNLOAD_DEFAULTS = {
|
|
@@ -244,7 +245,7 @@
|
|
|
244
245
|
* Called during initial database creation or when stores are missing.
|
|
245
246
|
*/
|
|
246
247
|
function createStores(db) {
|
|
247
|
-
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts'];
|
|
248
|
+
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts', 'models'];
|
|
248
249
|
for (const storeName of stores) {
|
|
249
250
|
if (!db.objectStoreNames.contains(storeName)) {
|
|
250
251
|
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
@@ -326,6 +327,7 @@
|
|
|
326
327
|
* - sprites: Sprite images and JSON
|
|
327
328
|
* - glyphs: Font glyph data
|
|
328
329
|
* - fonts: Font files
|
|
330
|
+
* - models: 3D model files (.glb) for Mapbox Standard tree/turbine layers
|
|
329
331
|
* - regions: (deprecated) Legacy region storage, migrated to styles.regions[]
|
|
330
332
|
*
|
|
331
333
|
* @example
|
|
@@ -345,6 +347,9 @@
|
|
|
345
347
|
if (oldVersion > 0 && oldVersion < 3) {
|
|
346
348
|
migrateRegionsToStyles(transaction);
|
|
347
349
|
}
|
|
350
|
+
// Migration: v3 -> v4
|
|
351
|
+
// Adds the `models` store for Mapbox Standard 3D model assets.
|
|
352
|
+
// No data migration needed — createStores above handles it.
|
|
348
353
|
},
|
|
349
354
|
});
|
|
350
355
|
}
|
|
@@ -1179,6 +1184,22 @@
|
|
|
1179
1184
|
}
|
|
1180
1185
|
return { styleId, sourceId, z, x, y, ext };
|
|
1181
1186
|
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Extract the extension (the last dotted segment before `?`, `#`, or end) from
|
|
1189
|
+
* a tile URL or tile URL template. Defaults to `"pbf"` when no extension can
|
|
1190
|
+
* be parsed. For multi-extension URLs like Mapbox v4's `{y}.vector.pbf` this
|
|
1191
|
+
* returns `"pbf"`, matching the key used when the tile is stored.
|
|
1192
|
+
*
|
|
1193
|
+
* Keeping extraction logic in one place ensures patchStyleForOffline (which
|
|
1194
|
+
* rewrites tile URLs to `idb://` at load time) derives the same extension
|
|
1195
|
+
* that tileService.extractExtension used at store time — otherwise the
|
|
1196
|
+
* first-try lookup in idbFetchHandler misses and has to fall through its
|
|
1197
|
+
* pbf/mvt/png/jpg/webp fallback loop.
|
|
1198
|
+
*/
|
|
1199
|
+
function extractTileExtensionFromUrl(url) {
|
|
1200
|
+
const match = url.match(/\.([\w]+)(?:[?#]|$)/i);
|
|
1201
|
+
return match ? match[1] : 'pbf';
|
|
1202
|
+
}
|
|
1182
1203
|
/**
|
|
1183
1204
|
* Derive tile extension from tile URL templates
|
|
1184
1205
|
*/
|
|
@@ -1186,21 +1207,217 @@
|
|
|
1186
1207
|
if (Array.isArray(tiles) && tiles.length > 0) {
|
|
1187
1208
|
const firstTile = tiles[0];
|
|
1188
1209
|
if (typeof firstTile === 'string') {
|
|
1189
|
-
|
|
1190
|
-
if (match) {
|
|
1191
|
-
return match[1];
|
|
1192
|
-
}
|
|
1210
|
+
return extractTileExtensionFromUrl(firstTile);
|
|
1193
1211
|
}
|
|
1194
1212
|
}
|
|
1195
1213
|
return 'pbf';
|
|
1196
1214
|
}
|
|
1197
1215
|
|
|
1216
|
+
/**
|
|
1217
|
+
* Pure helpers shared between the main-thread offline fetch handler
|
|
1218
|
+
* (`src/utils/idbFetchHandler.ts`) and the offline Service Worker
|
|
1219
|
+
* (`src/sw/offline-sw.ts`, compiled to `public/idb-offline-sw.js`).
|
|
1220
|
+
*
|
|
1221
|
+
* Keeping these in one place means the SW and the main-thread handler
|
|
1222
|
+
* can't drift — adding a new `model` handler, changing the fallback
|
|
1223
|
+
* order, or tweaking the tilejson-source matcher happens once.
|
|
1224
|
+
*
|
|
1225
|
+
* Nothing in here touches IndexedDB directly. Each helper takes already-
|
|
1226
|
+
* resolved inputs and returns the list of candidate keys (or the
|
|
1227
|
+
* resolved output) that the caller feeds into its own IDB lookup.
|
|
1228
|
+
*
|
|
1229
|
+
* The corresponding IDB access layer is:
|
|
1230
|
+
* - main thread: `idb` library via `dbPromise`
|
|
1231
|
+
* - service worker: raw `indexedDB.open` (see `offline-sw.ts`)
|
|
1232
|
+
*
|
|
1233
|
+
* They have different shapes so cannot be shared; the key computation
|
|
1234
|
+
* can be and is.
|
|
1235
|
+
*/
|
|
1236
|
+
/**
|
|
1237
|
+
* Extensions to try in order when the requested extension misses. `glb` is
|
|
1238
|
+
* last so batched-model sources (Mapbox Standard 3D buildings) resolve when
|
|
1239
|
+
* their source URL template ended in `.vector` or similar and the actual
|
|
1240
|
+
* tile body was stored as glb.
|
|
1241
|
+
*/
|
|
1242
|
+
const TILE_FALLBACK_EXTENSIONS = ['pbf', 'mvt', 'png', 'jpg', 'webp', 'glb'];
|
|
1243
|
+
/** Extensions minus the one the caller already tried. */
|
|
1244
|
+
function tileFallbackExtensions(requested) {
|
|
1245
|
+
return TILE_FALLBACK_EXTENSIONS.filter(e => e !== requested);
|
|
1246
|
+
}
|
|
1247
|
+
// ---------------------------------------------------------------------------
|
|
1248
|
+
// Region → style lookup
|
|
1249
|
+
// ---------------------------------------------------------------------------
|
|
1250
|
+
/**
|
|
1251
|
+
* Given an already-fetched list of style entries, find the first one whose
|
|
1252
|
+
* `regions` array contains the given ID. Pure — the caller is responsible for
|
|
1253
|
+
* loading the entries and for caching. Used by both `findStyleByRegionId`
|
|
1254
|
+
* implementations to keep the match rule identical.
|
|
1255
|
+
*/
|
|
1256
|
+
function findStyleByRegionIdIn(styles, regionId) {
|
|
1257
|
+
for (const entry of styles) {
|
|
1258
|
+
const regions = entry.regions;
|
|
1259
|
+
if (!Array.isArray(regions))
|
|
1260
|
+
continue;
|
|
1261
|
+
for (const r of regions) {
|
|
1262
|
+
if (r?.regionId === regionId || r?.id === regionId) {
|
|
1263
|
+
return entry;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return null;
|
|
1268
|
+
}
|
|
1269
|
+
// ---------------------------------------------------------------------------
|
|
1270
|
+
// Glyph candidate keys
|
|
1271
|
+
// ---------------------------------------------------------------------------
|
|
1272
|
+
/**
|
|
1273
|
+
* Parse `FontA,FontB,FontC/0-255.pbf` into (fontstacks, rangePart). Mapbox
|
|
1274
|
+
* requests a comma-joined font-family fallback chain; each glyph is stored
|
|
1275
|
+
* individually, so the caller tries each fontstack in order.
|
|
1276
|
+
*/
|
|
1277
|
+
function parseGlyphPath(decodedPath) {
|
|
1278
|
+
const pathParts = decodedPath.split('/');
|
|
1279
|
+
const fontstackPart = pathParts[0] ?? '';
|
|
1280
|
+
const rangePart = pathParts[1] || '0-255.pbf';
|
|
1281
|
+
const fontstacks = fontstackPart
|
|
1282
|
+
.split(',')
|
|
1283
|
+
.map(f => f.trim())
|
|
1284
|
+
.filter(Boolean);
|
|
1285
|
+
return { fontstacks, rangePart };
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Build the list of keys to try for a single (fontstack, range) pair.
|
|
1289
|
+
* Order: actualStyleId variants first (most common), then downloadId,
|
|
1290
|
+
* then the bare path. Normalized and raw `.pbf`-less forms are both tried
|
|
1291
|
+
* to cover stored-key variants from older versions.
|
|
1292
|
+
*/
|
|
1293
|
+
function glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart) {
|
|
1294
|
+
const glyphPath = `${fontstack}/${rangePart}`;
|
|
1295
|
+
const normalizedPath = glyphPath.endsWith('.pbf') ? glyphPath : `${glyphPath}.pbf`;
|
|
1296
|
+
return dedupe([
|
|
1297
|
+
`${actualStyleId}::${normalizedPath}`,
|
|
1298
|
+
`${actualStyleId}::${glyphPath}`,
|
|
1299
|
+
`${downloadId}::${normalizedPath}`,
|
|
1300
|
+
`${downloadId}::${glyphPath}`,
|
|
1301
|
+
normalizedPath,
|
|
1302
|
+
glyphPath,
|
|
1303
|
+
]);
|
|
1304
|
+
}
|
|
1305
|
+
// ---------------------------------------------------------------------------
|
|
1306
|
+
// Sprite candidate keys
|
|
1307
|
+
// ---------------------------------------------------------------------------
|
|
1308
|
+
/**
|
|
1309
|
+
* Sprite keys have historically used both `::` and `:` as the separator, and
|
|
1310
|
+
* both the full filename (`sprite.json`) and the bare name (`sprite`). Return
|
|
1311
|
+
* every variant in priority order; the caller stops at the first hit.
|
|
1312
|
+
*/
|
|
1313
|
+
function spriteCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1314
|
+
const stripExt = decodedPath.replace(/\.(json|png)$/i, '');
|
|
1315
|
+
return dedupe([
|
|
1316
|
+
`${actualStyleId}::${decodedPath}`,
|
|
1317
|
+
`${actualStyleId}:${decodedPath}`,
|
|
1318
|
+
`${actualStyleId}::${stripExt}`,
|
|
1319
|
+
`${actualStyleId}:${stripExt}`,
|
|
1320
|
+
`${downloadId}::${decodedPath}`,
|
|
1321
|
+
`${downloadId}:${decodedPath}`,
|
|
1322
|
+
`${downloadId}::${stripExt}`,
|
|
1323
|
+
`${downloadId}:${stripExt}`,
|
|
1324
|
+
decodedPath,
|
|
1325
|
+
]);
|
|
1326
|
+
}
|
|
1327
|
+
// ---------------------------------------------------------------------------
|
|
1328
|
+
// Model candidate keys
|
|
1329
|
+
// ---------------------------------------------------------------------------
|
|
1330
|
+
/**
|
|
1331
|
+
* Model keys are `{styleId}::model::{name}`. Try the resolved style id first,
|
|
1332
|
+
* then the bare downloadId in case the request came through the region-scoped
|
|
1333
|
+
* URL form (`idb://{regionId}/model/{name}`).
|
|
1334
|
+
*/
|
|
1335
|
+
function modelCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1336
|
+
return dedupe([
|
|
1337
|
+
`${actualStyleId}::model::${decodedPath}`,
|
|
1338
|
+
`${downloadId}::model::${decodedPath}`,
|
|
1339
|
+
]);
|
|
1340
|
+
}
|
|
1341
|
+
// ---------------------------------------------------------------------------
|
|
1342
|
+
// TileJSON source matching
|
|
1343
|
+
// ---------------------------------------------------------------------------
|
|
1344
|
+
/**
|
|
1345
|
+
* Mapbox GL requests tilejson via `idb://{downloadId}/tilesjson/{path}` where
|
|
1346
|
+
* `{path}` may be the source id, the original TileJSON URL, or the URL we
|
|
1347
|
+
* stashed under `__originalTilesetUrl` when patching for offline. Try all
|
|
1348
|
+
* three; return the matching source id + its config, or null.
|
|
1349
|
+
*/
|
|
1350
|
+
function matchTileJsonSource(sources, decodedPath) {
|
|
1351
|
+
const asConfig = (v) => v && typeof v === 'object' ? v : null;
|
|
1352
|
+
if (decodedPath in sources) {
|
|
1353
|
+
const config = asConfig(sources[decodedPath]);
|
|
1354
|
+
if (config)
|
|
1355
|
+
return { sourceId: decodedPath, config };
|
|
1356
|
+
}
|
|
1357
|
+
for (const [sourceId, raw] of Object.entries(sources)) {
|
|
1358
|
+
const config = asConfig(raw);
|
|
1359
|
+
if (!config)
|
|
1360
|
+
continue;
|
|
1361
|
+
const url = typeof config.url === 'string' ? config.url : undefined;
|
|
1362
|
+
const original = typeof config.__originalTilesetUrl === 'string'
|
|
1363
|
+
? config.__originalTilesetUrl
|
|
1364
|
+
: undefined;
|
|
1365
|
+
if (url === decodedPath || original === decodedPath) {
|
|
1366
|
+
return { sourceId, config };
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return null;
|
|
1370
|
+
}
|
|
1371
|
+
/**
|
|
1372
|
+
* Build the offline TileJSON payload that replaces the one Mapbox would
|
|
1373
|
+
* have fetched from the network. `tiles` is rewritten to serve from the SW
|
|
1374
|
+
* (the caller supplies the scheme via `tileUrlScheme`); copyable TileJSON
|
|
1375
|
+
* fields are preserved.
|
|
1376
|
+
*/
|
|
1377
|
+
function buildOfflineTileJson(sourceConfig, downloadId, sourceId, extension, tileUrlScheme, origin) {
|
|
1378
|
+
const base = `idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`
|
|
1379
|
+
;
|
|
1380
|
+
const tileJson = {
|
|
1381
|
+
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1382
|
+
name: sourceConfig.name ?? sourceId,
|
|
1383
|
+
tiles: [base],
|
|
1384
|
+
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1385
|
+
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1386
|
+
};
|
|
1387
|
+
const copyable = [
|
|
1388
|
+
'bounds',
|
|
1389
|
+
'center',
|
|
1390
|
+
'vector_layers',
|
|
1391
|
+
'scheme',
|
|
1392
|
+
'attribution',
|
|
1393
|
+
'encoding',
|
|
1394
|
+
'format',
|
|
1395
|
+
'grids',
|
|
1396
|
+
'data',
|
|
1397
|
+
'template',
|
|
1398
|
+
'version',
|
|
1399
|
+
];
|
|
1400
|
+
for (const field of copyable) {
|
|
1401
|
+
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1402
|
+
tileJson[field] = sourceConfig[field];
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return tileJson;
|
|
1406
|
+
}
|
|
1407
|
+
// ---------------------------------------------------------------------------
|
|
1408
|
+
// Internal helpers
|
|
1409
|
+
// ---------------------------------------------------------------------------
|
|
1410
|
+
function dedupe(values) {
|
|
1411
|
+
return Array.from(new Set(values));
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1198
1414
|
// idbFetchHandler.ts
|
|
1199
1415
|
// Intercepts idb:// URLs and serves resources from IndexedDB for MapLibre GL offline mode
|
|
1200
1416
|
const idbLogger = logger.scope('IDBFetch');
|
|
1201
1417
|
// idb://{downloadId}/tile/{sourceKey}/{url}
|
|
1202
1418
|
// idb://{downloadId}/glyph/{fontstack}/{range}.pbf
|
|
1203
1419
|
// idb://{downloadId}/sprite/{spriteName}
|
|
1420
|
+
// idb://{styleId}/model/{modelName}
|
|
1204
1421
|
// idb://{downloadId}/tilesjson/{url}
|
|
1205
1422
|
// Cache for region ID to style mapping to avoid repeated DB queries
|
|
1206
1423
|
const regionToStyleCache = new Map();
|
|
@@ -1250,16 +1467,11 @@
|
|
|
1250
1467
|
}
|
|
1251
1468
|
try {
|
|
1252
1469
|
const allStyles = await db.getAll('styles');
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
// Cache the result
|
|
1259
|
-
regionToStyleCache.set(regionId, { styleEntry, timestamp: Date.now() });
|
|
1260
|
-
return styleEntry;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1470
|
+
const hit = findStyleByRegionIdIn(allStyles, regionId);
|
|
1471
|
+
if (hit) {
|
|
1472
|
+
idbLogger.debug(`Found style "${hit.key}" containing region: ${regionId}`);
|
|
1473
|
+
regionToStyleCache.set(regionId, { styleEntry: hit, timestamp: Date.now() });
|
|
1474
|
+
return hit;
|
|
1263
1475
|
}
|
|
1264
1476
|
idbLogger.debug(`No style found containing region: ${regionId}`);
|
|
1265
1477
|
// Don't cache negative results — the region may be stored moments later
|
|
@@ -1271,36 +1483,6 @@
|
|
|
1271
1483
|
return null;
|
|
1272
1484
|
}
|
|
1273
1485
|
}
|
|
1274
|
-
function buildOfflineTileJson(sourceConfig, downloadId, sourceId) {
|
|
1275
|
-
const extension = deriveTileExtension(sourceConfig.tiles);
|
|
1276
|
-
const offlineTiles = [`idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`];
|
|
1277
|
-
const tileJson = {
|
|
1278
|
-
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1279
|
-
name: sourceConfig.name ?? sourceId,
|
|
1280
|
-
tiles: offlineTiles,
|
|
1281
|
-
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1282
|
-
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1283
|
-
};
|
|
1284
|
-
const fieldsToCopy = [
|
|
1285
|
-
'bounds',
|
|
1286
|
-
'center',
|
|
1287
|
-
'vector_layers',
|
|
1288
|
-
'scheme',
|
|
1289
|
-
'attribution',
|
|
1290
|
-
'encoding',
|
|
1291
|
-
'format',
|
|
1292
|
-
'grids',
|
|
1293
|
-
'data',
|
|
1294
|
-
'template',
|
|
1295
|
-
'version',
|
|
1296
|
-
];
|
|
1297
|
-
for (const field of fieldsToCopy) {
|
|
1298
|
-
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1299
|
-
tileJson[field] = sourceConfig[field];
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
return tileJson;
|
|
1303
|
-
}
|
|
1304
1486
|
async function createTileResponse(resource) {
|
|
1305
1487
|
const headers = {};
|
|
1306
1488
|
// Set proper content type for vector tiles (PBF/MVT format)
|
|
@@ -1395,7 +1577,7 @@
|
|
|
1395
1577
|
// but tiles are stored with integer zoom levels, so floor the value
|
|
1396
1578
|
const z = Math.floor(parseFloat(pathParts[pathParts.length - 3]));
|
|
1397
1579
|
const sourceKey = pathParts.slice(0, pathParts.length - 3).join('/');
|
|
1398
|
-
const yMatch = yExt.match(
|
|
1580
|
+
const yMatch = yExt.match(/^(\d+)\.(\w+)$/);
|
|
1399
1581
|
if (yMatch) {
|
|
1400
1582
|
const y = parseInt(yMatch[1]);
|
|
1401
1583
|
const requestedExt = yMatch[2]; // Extension from URL (for logging only)
|
|
@@ -1410,7 +1592,7 @@
|
|
|
1410
1592
|
}
|
|
1411
1593
|
idbLogger.debug(`Tile not found: ${tileKey}`);
|
|
1412
1594
|
// Fallback: try common alternative extensions
|
|
1413
|
-
const fallbackExtensions =
|
|
1595
|
+
const fallbackExtensions = tileFallbackExtensions(requestedExt);
|
|
1414
1596
|
for (const fallbackExt of fallbackExtensions) {
|
|
1415
1597
|
const fallbackKey = createTileKey(x, y, z, actualStyleId, sourceKey, fallbackExt);
|
|
1416
1598
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1452,7 +1634,7 @@
|
|
|
1452
1634
|
return await createTileResponse(resource);
|
|
1453
1635
|
}
|
|
1454
1636
|
// Try alternative extensions
|
|
1455
|
-
const fallbackExts =
|
|
1637
|
+
const fallbackExts = tileFallbackExtensions(ext);
|
|
1456
1638
|
for (const fallbackExt of fallbackExts) {
|
|
1457
1639
|
const fallbackKey = createTileKey(parseInt(x), parseInt(y), parseInt(z), actualStyleId, fallbackSourceKey, fallbackExt);
|
|
1458
1640
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1472,46 +1654,19 @@
|
|
|
1472
1654
|
}
|
|
1473
1655
|
case 'glyph': {
|
|
1474
1656
|
idbLogger.debug(`Looking for glyph with key: ${key}`);
|
|
1475
|
-
// Find which style this region belongs to
|
|
1476
1657
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1477
1658
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1478
|
-
|
|
1479
|
-
idbLogger.debug(`Region "${downloadId}" belongs to style "${actualStyleId}", searching with style key`);
|
|
1480
|
-
}
|
|
1481
|
-
// Parse the resource path: "FontA,FontB,FontC/0-255.pbf"
|
|
1482
|
-
// MapLibre requests glyphs with comma-separated fallback fonts
|
|
1483
|
-
// but glyphs are stored individually per font
|
|
1484
|
-
const pathParts = decodedResourcePath.split('/');
|
|
1485
|
-
const fontstackPart = pathParts[0]; // "FontA,FontB,FontC"
|
|
1486
|
-
const rangePart = pathParts[1] || '0-255.pbf'; // "0-255.pbf"
|
|
1487
|
-
// Split comma-separated fonts
|
|
1488
|
-
const fontstacks = fontstackPart.split(',').map(f => f.trim());
|
|
1659
|
+
const { fontstacks, rangePart } = parseGlyphPath(decodedResourcePath);
|
|
1489
1660
|
idbLogger.debug(`Trying ${fontstacks.length} fonts in fallback order: ${fontstacks.join(', ')}`);
|
|
1490
|
-
// Try each font in order (this is how font fallbacks work)
|
|
1491
1661
|
for (const fontstack of fontstacks) {
|
|
1492
|
-
const
|
|
1493
|
-
const
|
|
1494
|
-
const glyphCandidateKeys = [
|
|
1495
|
-
// Try with actual style ID first
|
|
1496
|
-
`${actualStyleId}::${normalizedPath}`,
|
|
1497
|
-
`${actualStyleId}::${glyphPath}`,
|
|
1498
|
-
// Then try with download ID
|
|
1499
|
-
`${downloadId}::${normalizedPath}`,
|
|
1500
|
-
`${downloadId}::${glyphPath}`,
|
|
1501
|
-
// Just paths
|
|
1502
|
-
normalizedPath,
|
|
1503
|
-
glyphPath,
|
|
1504
|
-
];
|
|
1505
|
-
idbLogger.debug(`Trying keys for font "${fontstack}":`, glyphCandidateKeys);
|
|
1506
|
-
for (const candidateKey of glyphCandidateKeys) {
|
|
1662
|
+
const candidateKeys = glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart);
|
|
1663
|
+
for (const candidateKey of candidateKeys) {
|
|
1507
1664
|
const resource = await db.get('glyphs', candidateKey);
|
|
1508
1665
|
if (resource?.data) {
|
|
1509
1666
|
idbLogger.debug(`Found glyph using key: ${candidateKey} (font: ${fontstack})`);
|
|
1510
1667
|
return new Response(resource.data, {
|
|
1511
1668
|
status: 200,
|
|
1512
|
-
headers: {
|
|
1513
|
-
'Content-Type': 'application/x-protobuf',
|
|
1514
|
-
},
|
|
1669
|
+
headers: { 'Content-Type': 'application/x-protobuf' },
|
|
1515
1670
|
});
|
|
1516
1671
|
}
|
|
1517
1672
|
}
|
|
@@ -1521,33 +1676,11 @@
|
|
|
1521
1676
|
}
|
|
1522
1677
|
case 'sprite': {
|
|
1523
1678
|
idbLogger.debug(`Looking for sprite with key: ${key}`);
|
|
1524
|
-
// Find which style this region belongs to
|
|
1525
1679
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1526
1680
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
// The sprite service stores sprites with keys like: "voyager::sprite.json", "voyager::sprite@2x.json"
|
|
1531
|
-
// MapLibre requests sprites as: "idb://region_XXX/sprite/sprite@2x.json"
|
|
1532
|
-
// So we need to map the region ID to the style ID
|
|
1533
|
-
const spriteCandidateKeys = Array.from(new Set([
|
|
1534
|
-
// Try with actual style ID first (most likely to work)
|
|
1535
|
-
`${actualStyleId}::${decodedResourcePath}`,
|
|
1536
|
-
`${actualStyleId}:${decodedResourcePath}`,
|
|
1537
|
-
`${actualStyleId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1538
|
-
`${actualStyleId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1539
|
-
// Then try with download ID (in case it's a direct style download)
|
|
1540
|
-
`${downloadId}::${decodedResourcePath}`,
|
|
1541
|
-
`${downloadId}:${decodedResourcePath}`,
|
|
1542
|
-
`${downloadId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1543
|
-
`${downloadId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1544
|
-
// Just the path itself
|
|
1545
|
-
decodedResourcePath,
|
|
1546
|
-
// Original key format
|
|
1547
|
-
key,
|
|
1548
|
-
]));
|
|
1549
|
-
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, spriteCandidateKeys);
|
|
1550
|
-
for (const candidateKey of spriteCandidateKeys) {
|
|
1681
|
+
const candidates = spriteCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1682
|
+
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, candidates);
|
|
1683
|
+
for (const candidateKey of candidates) {
|
|
1551
1684
|
const resource = await db.get('sprites', candidateKey);
|
|
1552
1685
|
if (resource?.data) {
|
|
1553
1686
|
idbLogger.debug(`Found sprite using key: ${candidateKey}`);
|
|
@@ -1557,7 +1690,7 @@
|
|
|
1557
1690
|
});
|
|
1558
1691
|
}
|
|
1559
1692
|
}
|
|
1560
|
-
idbLogger.warn(`Sprite not found, tried keys: ${
|
|
1693
|
+
idbLogger.warn(`Sprite not found, tried keys: ${candidates.join(', ')}`);
|
|
1561
1694
|
break;
|
|
1562
1695
|
}
|
|
1563
1696
|
case 'font': {
|
|
@@ -1572,11 +1705,32 @@
|
|
|
1572
1705
|
}
|
|
1573
1706
|
break;
|
|
1574
1707
|
}
|
|
1708
|
+
case 'model': {
|
|
1709
|
+
// Model URLs are rewritten by patchStyleForOffline to
|
|
1710
|
+
// idb://{styleId}/model/{modelName}
|
|
1711
|
+
// Models are keyed by {styleId}::model::{modelName} in the store.
|
|
1712
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1713
|
+
const actualStyleId = styleEntry?.key || downloadId;
|
|
1714
|
+
const candidates = modelCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1715
|
+
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1716
|
+
for (const candidateKey of candidates) {
|
|
1717
|
+
const resource = await db.get('models', candidateKey);
|
|
1718
|
+
if (resource?.data) {
|
|
1719
|
+
idbLogger.debug(`Found model using key: ${candidateKey}`);
|
|
1720
|
+
return new Response(resource.data, {
|
|
1721
|
+
status: 200,
|
|
1722
|
+
headers: { 'Content-Type': resource.contentType || 'model/gltf-binary' },
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
idbLogger.warn(`Model not found, tried keys: ${candidates.join(', ')}`);
|
|
1727
|
+
break;
|
|
1728
|
+
}
|
|
1575
1729
|
case 'tilesjson': {
|
|
1576
1730
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1577
|
-
// First try direct lookup (for style-level downloads)
|
|
1731
|
+
// First try direct lookup (for style-level downloads), then fall back
|
|
1732
|
+
// to searching by region ID (for region-level downloads).
|
|
1578
1733
|
let styleEntry = await db.get('styles', downloadId);
|
|
1579
|
-
// If not found, search by region ID (for region-level downloads)
|
|
1580
1734
|
if (!styleEntry || !styleEntry.style?.sources) {
|
|
1581
1735
|
idbLogger.debug(`Style not found with key "${downloadId}", searching by region ID...`);
|
|
1582
1736
|
const foundStyle = await findStyleByRegionId(db, downloadId);
|
|
@@ -1584,41 +1738,23 @@
|
|
|
1584
1738
|
styleEntry = foundStyle;
|
|
1585
1739
|
}
|
|
1586
1740
|
}
|
|
1587
|
-
if (styleEntry?.style?.sources) {
|
|
1588
|
-
const sources = styleEntry.style.sources;
|
|
1589
|
-
let matchedSourceId;
|
|
1590
|
-
let matchedSourceConfig;
|
|
1591
|
-
if (decodedResourcePath in sources) {
|
|
1592
|
-
matchedSourceId = decodedResourcePath;
|
|
1593
|
-
matchedSourceConfig = sources[decodedResourcePath];
|
|
1594
|
-
}
|
|
1595
|
-
else {
|
|
1596
|
-
for (const [sourceId, sourceValue] of Object.entries(sources)) {
|
|
1597
|
-
const sourceUrl = typeof sourceValue.url === 'string' ? sourceValue.url : undefined;
|
|
1598
|
-
const originalUrl = typeof sourceValue.__originalTilesetUrl === 'string'
|
|
1599
|
-
? sourceValue.__originalTilesetUrl
|
|
1600
|
-
: undefined;
|
|
1601
|
-
if (sourceUrl === decodedResourcePath || originalUrl === decodedResourcePath) {
|
|
1602
|
-
matchedSourceId = sourceId;
|
|
1603
|
-
matchedSourceConfig = sourceValue;
|
|
1604
|
-
break;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
if (matchedSourceId && matchedSourceConfig) {
|
|
1609
|
-
const tileJson = buildOfflineTileJson(matchedSourceConfig, downloadId, matchedSourceId);
|
|
1610
|
-
idbLogger.debug(`Serving offline tilejson for source: ${matchedSourceId}`);
|
|
1611
|
-
return new Response(JSON.stringify(tileJson), {
|
|
1612
|
-
status: 200,
|
|
1613
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1617
|
-
}
|
|
1618
|
-
else {
|
|
1741
|
+
if (!styleEntry?.style?.sources) {
|
|
1619
1742
|
idbLogger.warn(`Style not found or missing sources for downloadId: ${downloadId}`);
|
|
1743
|
+
break;
|
|
1620
1744
|
}
|
|
1621
|
-
|
|
1745
|
+
const sources = styleEntry.style.sources;
|
|
1746
|
+
const matched = matchTileJsonSource(sources, decodedResourcePath);
|
|
1747
|
+
if (!matched) {
|
|
1748
|
+
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1749
|
+
break;
|
|
1750
|
+
}
|
|
1751
|
+
const extension = deriveTileExtension(matched.config.tiles);
|
|
1752
|
+
const tileJson = buildOfflineTileJson(matched.config, downloadId, matched.sourceId, extension, 'idb');
|
|
1753
|
+
idbLogger.debug(`Serving offline tilejson for source: ${matched.sourceId}`);
|
|
1754
|
+
return new Response(JSON.stringify(tileJson), {
|
|
1755
|
+
status: 200,
|
|
1756
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1757
|
+
});
|
|
1622
1758
|
}
|
|
1623
1759
|
default:
|
|
1624
1760
|
idbLogger.warn(`Unknown resource type: ${type}`);
|
|
@@ -1642,6 +1778,37 @@
|
|
|
1642
1778
|
function isMapboxProtocol(url) {
|
|
1643
1779
|
return url.startsWith(MAPBOX_API.PROTOCOL);
|
|
1644
1780
|
}
|
|
1781
|
+
/**
|
|
1782
|
+
* Parse a URL and return its hostname, or null if the URL is malformed.
|
|
1783
|
+
* Accepts relative URLs when `base` is provided.
|
|
1784
|
+
*/
|
|
1785
|
+
function getUrlHostname(url, base) {
|
|
1786
|
+
try {
|
|
1787
|
+
return new URL(url, base).hostname.toLowerCase();
|
|
1788
|
+
}
|
|
1789
|
+
catch {
|
|
1790
|
+
return null;
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
/**
|
|
1794
|
+
* True if `url`'s hostname equals `host` or is a subdomain of `host`.
|
|
1795
|
+
* Uses URL parsing (not substring matching) to avoid false positives like
|
|
1796
|
+
* `https://evil.com/?x=mapbox.com` matching `mapbox.com`.
|
|
1797
|
+
*/
|
|
1798
|
+
function hostMatches(url, host, base) {
|
|
1799
|
+
const hostname = getUrlHostname(url, base);
|
|
1800
|
+
if (hostname === null)
|
|
1801
|
+
return false;
|
|
1802
|
+
const target = host.toLowerCase();
|
|
1803
|
+
return hostname === target || hostname.endsWith('.' + target);
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* True for any host under the mapbox.com domain (including api.mapbox.com,
|
|
1807
|
+
* *.tiles.mapbox.com, etc.). Used by provider detection.
|
|
1808
|
+
*/
|
|
1809
|
+
function isMapboxHost(url, base) {
|
|
1810
|
+
return hostMatches(url, 'mapbox.com', base);
|
|
1811
|
+
}
|
|
1645
1812
|
/**
|
|
1646
1813
|
* Resolve a mapbox:// URL to its HTTPS API equivalent
|
|
1647
1814
|
*
|
|
@@ -1715,9 +1882,7 @@
|
|
|
1715
1882
|
*/
|
|
1716
1883
|
function detectStyleProvider(styleUrl, style) {
|
|
1717
1884
|
// Check URL patterns
|
|
1718
|
-
if (isMapboxProtocol(styleUrl) ||
|
|
1719
|
-
styleUrl.includes('mapbox.com') ||
|
|
1720
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
1885
|
+
if (isMapboxProtocol(styleUrl) || isMapboxHost(styleUrl)) {
|
|
1721
1886
|
return 'mapbox';
|
|
1722
1887
|
}
|
|
1723
1888
|
if (styleUrl.includes('maplibre') ||
|
|
@@ -1736,7 +1901,7 @@
|
|
|
1736
1901
|
const sources = style.sources || {};
|
|
1737
1902
|
for (const [, sourceConfig] of Object.entries(sources)) {
|
|
1738
1903
|
const source = sourceConfig;
|
|
1739
|
-
if (source.url && (source.url
|
|
1904
|
+
if (source.url && (isMapboxProtocol(source.url) || isMapboxHost(source.url))) {
|
|
1740
1905
|
return 'mapbox';
|
|
1741
1906
|
}
|
|
1742
1907
|
}
|
|
@@ -1828,7 +1993,7 @@
|
|
|
1828
1993
|
if (isMapboxProtocol(tileUrl) && accessToken) {
|
|
1829
1994
|
return resolveMapboxUrl(tileUrl, accessToken);
|
|
1830
1995
|
}
|
|
1831
|
-
if (provider === 'mapbox' && accessToken && tileUrl
|
|
1996
|
+
if (provider === 'mapbox' && accessToken && isMapboxHost(tileUrl)) {
|
|
1832
1997
|
return normalizeStyleUrl(tileUrl, accessToken);
|
|
1833
1998
|
}
|
|
1834
1999
|
return tileUrl;
|
|
@@ -1843,7 +2008,7 @@
|
|
|
1843
2008
|
if (isMapboxProtocol(processedStyle.sprite)) {
|
|
1844
2009
|
processedStyle.sprite = resolveMapboxUrl(processedStyle.sprite, accessToken);
|
|
1845
2010
|
}
|
|
1846
|
-
else if (provider === 'mapbox' && processedStyle.sprite
|
|
2011
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.sprite)) {
|
|
1847
2012
|
processedStyle.sprite = normalizeStyleUrl(processedStyle.sprite, accessToken);
|
|
1848
2013
|
}
|
|
1849
2014
|
}
|
|
@@ -1854,7 +2019,7 @@
|
|
|
1854
2019
|
if (isMapboxProtocol(entry.url)) {
|
|
1855
2020
|
return { ...entry, url: resolveMapboxUrl(entry.url, accessToken) };
|
|
1856
2021
|
}
|
|
1857
|
-
else if (provider === 'mapbox' && entry.url
|
|
2022
|
+
else if (provider === 'mapbox' && isMapboxHost(entry.url)) {
|
|
1858
2023
|
return { ...entry, url: normalizeStyleUrl(entry.url, accessToken) };
|
|
1859
2024
|
}
|
|
1860
2025
|
}
|
|
@@ -1867,7 +2032,7 @@
|
|
|
1867
2032
|
if (isMapboxProtocol(processedStyle.glyphs)) {
|
|
1868
2033
|
processedStyle.glyphs = resolveMapboxUrl(processedStyle.glyphs, accessToken);
|
|
1869
2034
|
}
|
|
1870
|
-
else if (provider === 'mapbox' && processedStyle.glyphs
|
|
2035
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.glyphs)) {
|
|
1871
2036
|
processedStyle.glyphs = normalizeStyleUrl(processedStyle.glyphs, accessToken);
|
|
1872
2037
|
}
|
|
1873
2038
|
}
|
|
@@ -1907,13 +2072,20 @@
|
|
|
1907
2072
|
// Check for Mapbox-specific requirements
|
|
1908
2073
|
const hasMapboxSources = Object.values(style.sources || {}).some((source) => {
|
|
1909
2074
|
const s = source;
|
|
1910
|
-
return s.url && s.url
|
|
2075
|
+
return !!s.url && isMapboxHost(s.url);
|
|
1911
2076
|
});
|
|
1912
2077
|
if (hasMapboxSources) {
|
|
1913
2078
|
// Check if access token might be needed
|
|
1914
2079
|
const hasAccessToken = Object.values(style.sources || {}).some((source) => {
|
|
1915
2080
|
const s = source;
|
|
1916
|
-
|
|
2081
|
+
if (!s.url)
|
|
2082
|
+
return false;
|
|
2083
|
+
try {
|
|
2084
|
+
return new URL(s.url).searchParams.has('access_token');
|
|
2085
|
+
}
|
|
2086
|
+
catch {
|
|
2087
|
+
return false;
|
|
2088
|
+
}
|
|
1917
2089
|
});
|
|
1918
2090
|
if (!hasAccessToken) {
|
|
1919
2091
|
warnings.push('Mapbox sources detected but no access token found - authentication may be required');
|
|
@@ -1947,14 +2119,15 @@
|
|
|
1947
2119
|
styleLogger.debug(`Patching source: ${sourceKey}`, source);
|
|
1948
2120
|
if (source.tiles) {
|
|
1949
2121
|
const originalTiles = [...source.tiles];
|
|
1950
|
-
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext
|
|
2122
|
+
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext.
|
|
2123
|
+
// Extension extraction goes through the shared extractTileExtensionFromUrl
|
|
2124
|
+
// helper so the patched URL's extension matches what tileService used when
|
|
2125
|
+
// storing — otherwise Mapbox v4 tile URLs (`{y}.vector.pbf`) produced a
|
|
2126
|
+
// stored key under `.pbf` but a patched URL with `.vector`, forcing
|
|
2127
|
+
// idbFetchHandler to fall through its pbf/mvt/png/jpg/webp fallback loop
|
|
2128
|
+
// on every tile.
|
|
1951
2129
|
source.tiles = source.tiles.map((url) => {
|
|
1952
|
-
|
|
1953
|
-
let ext = tileExtension;
|
|
1954
|
-
if (!ext) {
|
|
1955
|
-
const extMatch = url.match(/\{z\}\/\{x\}\/\{y\}\.(\w+)/);
|
|
1956
|
-
ext = extMatch ? extMatch[1] : 'pbf';
|
|
1957
|
-
}
|
|
2130
|
+
const ext = tileExtension ?? extractTileExtensionFromUrl(url);
|
|
1958
2131
|
return `idb://${downloadId}/tile/${sourceKey}/{z}/{x}/{y}.${ext}`;
|
|
1959
2132
|
});
|
|
1960
2133
|
styleLogger.debug(`Patched tiles for ${sourceKey} with extension .${tileExtension || 'pbf'}:`, {
|
|
@@ -2027,14 +2200,29 @@
|
|
|
2027
2200
|
});
|
|
2028
2201
|
}
|
|
2029
2202
|
}
|
|
2030
|
-
// Patch top-level models (Mapbox Standard 3D
|
|
2203
|
+
// Patch top-level models (Mapbox Standard 3D trees / wind turbines).
|
|
2204
|
+
// Two shapes exist in the wild:
|
|
2205
|
+
// - Mapbox Standard: `{ "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb" }` (string values)
|
|
2206
|
+
// - Older/generic: `{ "name": { "uri": "mapbox://..." } }` (object values)
|
|
2207
|
+
// Models are keyed on the style ID (like sprites) so they can be shared
|
|
2208
|
+
// across regions.
|
|
2031
2209
|
if (style.models) {
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2210
|
+
const modelBaseId = styleId || downloadId;
|
|
2211
|
+
const models = style.models;
|
|
2212
|
+
let patchedCount = 0;
|
|
2213
|
+
for (const [modelId, value] of Object.entries(models)) {
|
|
2214
|
+
if (typeof value === 'string') {
|
|
2215
|
+
models[modelId] = `idb://${modelBaseId}/model/${modelId}`;
|
|
2216
|
+
patchedCount++;
|
|
2217
|
+
}
|
|
2218
|
+
else if (value && typeof value === 'object' && 'uri' in value && value.uri) {
|
|
2219
|
+
value.uri = `idb://${modelBaseId}/model/${modelId}`;
|
|
2220
|
+
patchedCount++;
|
|
2035
2221
|
}
|
|
2036
2222
|
}
|
|
2037
|
-
|
|
2223
|
+
if (patchedCount > 0) {
|
|
2224
|
+
styleLogger.debug(`Patched ${patchedCount} model URIs (styleId: ${modelBaseId})`);
|
|
2225
|
+
}
|
|
2038
2226
|
}
|
|
2039
2227
|
styleLogger.debug(`Final patched style:`, style);
|
|
2040
2228
|
return style;
|
|
@@ -2602,12 +2790,22 @@
|
|
|
2602
2790
|
});
|
|
2603
2791
|
}
|
|
2604
2792
|
}
|
|
2605
|
-
// Convert models
|
|
2793
|
+
// Convert models. Two shapes in the wild:
|
|
2794
|
+
// - Mapbox Standard: `{ name: "idb://..." }` (string value)
|
|
2795
|
+
// - Older/generic: `{ name: { uri: "idb://..." } }` (object value)
|
|
2606
2796
|
if (converted.models) {
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2797
|
+
const models = converted.models;
|
|
2798
|
+
for (const modelKey of Object.keys(models)) {
|
|
2799
|
+
const value = models[modelKey];
|
|
2800
|
+
if (typeof value === 'string') {
|
|
2801
|
+
if (value.startsWith('idb://')) {
|
|
2802
|
+
models[modelKey] = replace(value);
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
else if (value && typeof value === 'object') {
|
|
2806
|
+
if (typeof value.uri === 'string' && value.uri.startsWith('idb://')) {
|
|
2807
|
+
value.uri = replace(value.uri);
|
|
2808
|
+
}
|
|
2611
2809
|
}
|
|
2612
2810
|
}
|
|
2613
2811
|
}
|
|
@@ -2807,10 +3005,8 @@
|
|
|
2807
3005
|
if (typeof prefixedLayer.source === 'string') {
|
|
2808
3006
|
prefixedLayer.source = `${importId}/${prefixedLayer.source}`;
|
|
2809
3007
|
}
|
|
2810
|
-
// Resolve ["config", "key"] expressions using schema defaults and import overrides
|
|
2811
|
-
|
|
2812
|
-
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2813
|
-
}
|
|
3008
|
+
// Resolve ["config", "key"] expressions using schema defaults and import overrides.
|
|
3009
|
+
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2814
3010
|
flattenedLayers.push(prefixedLayer);
|
|
2815
3011
|
}
|
|
2816
3012
|
}
|
|
@@ -2848,8 +3044,48 @@
|
|
|
2848
3044
|
if (!style.models && importedModels) {
|
|
2849
3045
|
style.models = importedModels;
|
|
2850
3046
|
}
|
|
3047
|
+
// Rewrite indoor-only expressions so the flattened style validates without
|
|
3048
|
+
// the `imports` wrapper at render time — see sanitizeIndoorExpressions.
|
|
3049
|
+
sanitizeIndoorExpressions(style);
|
|
2851
3050
|
return style;
|
|
2852
3051
|
}
|
|
3052
|
+
/**
|
|
3053
|
+
* Rewrite indoor-only expressions in a style's layers to their outdoor no-op
|
|
3054
|
+
* constants. See the in-line comment in `resolveValue` for why this is needed
|
|
3055
|
+
* for Mapbox Standard when the `imports` wrapper is stripped.
|
|
3056
|
+
*
|
|
3057
|
+
* Safe to call multiple times and on already-downloaded stored styles — the
|
|
3058
|
+
* rewrites are idempotent (after the first pass there are no more
|
|
3059
|
+
* `is-active-floor` / `floor-level` expressions to rewrite).
|
|
3060
|
+
*/
|
|
3061
|
+
function sanitizeIndoorExpressions(style) {
|
|
3062
|
+
const layers = style.layers;
|
|
3063
|
+
if (!Array.isArray(layers))
|
|
3064
|
+
return;
|
|
3065
|
+
for (const layer of layers) {
|
|
3066
|
+
if (layer && typeof layer === 'object') {
|
|
3067
|
+
rewriteIndoor(layer);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
}
|
|
3071
|
+
function rewriteIndoor(obj) {
|
|
3072
|
+
for (const key of Object.keys(obj)) {
|
|
3073
|
+
obj[key] = rewriteIndoorValue(obj[key]);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
function rewriteIndoorValue(value) {
|
|
3077
|
+
if (!Array.isArray(value)) {
|
|
3078
|
+
if (value && typeof value === 'object' && !ArrayBuffer.isView(value)) {
|
|
3079
|
+
rewriteIndoor(value);
|
|
3080
|
+
}
|
|
3081
|
+
return value;
|
|
3082
|
+
}
|
|
3083
|
+
if (value[0] === 'is-active-floor')
|
|
3084
|
+
return false;
|
|
3085
|
+
if (value[0] === 'floor-level' && value.length === 1)
|
|
3086
|
+
return 0;
|
|
3087
|
+
return value.map(rewriteIndoorValue);
|
|
3088
|
+
}
|
|
2853
3089
|
/**
|
|
2854
3090
|
* Deep clone a plain object/array (JSON-safe values only).
|
|
2855
3091
|
*/
|
|
@@ -3015,6 +3251,40 @@
|
|
|
3015
3251
|
return result;
|
|
3016
3252
|
}
|
|
3017
3253
|
|
|
3254
|
+
const DEFAULT_WASM_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.14.1/dist/';
|
|
3255
|
+
let currentConfig = {};
|
|
3256
|
+
let sqlJsPromise = null;
|
|
3257
|
+
/**
|
|
3258
|
+
* Override how `sql.js` loads its WebAssembly. Call once before any MBTiles
|
|
3259
|
+
* import/export is invoked. Resets any cached init.
|
|
3260
|
+
*/
|
|
3261
|
+
function configureSqlJs(config) {
|
|
3262
|
+
currentConfig = { ...config };
|
|
3263
|
+
sqlJsPromise = null;
|
|
3264
|
+
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Lazily initialise `sql.js`. The underlying module is loaded via dynamic
|
|
3267
|
+
* `import()` so it only ships with bundles that actually call MBTiles code.
|
|
3268
|
+
*/
|
|
3269
|
+
async function getSqlJs() {
|
|
3270
|
+
if (sqlJsPromise)
|
|
3271
|
+
return sqlJsPromise;
|
|
3272
|
+
sqlJsPromise = (async () => {
|
|
3273
|
+
const mod = (await import('sql.js'));
|
|
3274
|
+
const initSqlJs = mod.default;
|
|
3275
|
+
const options = {};
|
|
3276
|
+
if (currentConfig.wasmBinary) {
|
|
3277
|
+
options.wasmBinary = currentConfig.wasmBinary;
|
|
3278
|
+
}
|
|
3279
|
+
else {
|
|
3280
|
+
const base = currentConfig.wasmUrl ?? DEFAULT_WASM_URL;
|
|
3281
|
+
options.locateFile = (file) => base.endsWith('/') ? `${base}${file}` : `${base}/${file}`;
|
|
3282
|
+
}
|
|
3283
|
+
return initSqlJs(options);
|
|
3284
|
+
})();
|
|
3285
|
+
return sqlJsPromise;
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3018
3288
|
const fontLogger = logger.scope('FontService');
|
|
3019
3289
|
class FontService {
|
|
3020
3290
|
db = dbPromise;
|
|
@@ -3198,15 +3468,23 @@
|
|
|
3198
3468
|
},
|
|
3199
3469
|
};
|
|
3200
3470
|
}
|
|
3201
|
-
|
|
3471
|
+
/**
|
|
3472
|
+
* Delete fonts older than `maxAge` days. When `options.styleId` is
|
|
3473
|
+
* provided, only fonts belonging to that style (per the delimiter-aware
|
|
3474
|
+
* `resourceKeyBelongsToStyle` match) are eligible — callers relying on
|
|
3475
|
+
* a styleId filter previously got a silent full-store wipe.
|
|
3476
|
+
*/
|
|
3477
|
+
async cleanupOldFonts(maxAge = 30, options = {}) {
|
|
3202
3478
|
const db = await this.db;
|
|
3203
3479
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3480
|
+
const { styleId } = options;
|
|
3204
3481
|
const tx = db.transaction(['fonts'], 'readwrite');
|
|
3205
3482
|
let deletedCount = 0;
|
|
3206
3483
|
let cursor = await tx.objectStore('fonts').openCursor();
|
|
3207
3484
|
while (cursor) {
|
|
3208
3485
|
const fontEntry = cursor.value;
|
|
3209
|
-
|
|
3486
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(fontEntry.key, styleId);
|
|
3487
|
+
if (belongs && fontEntry.lastModified < cutoffTime) {
|
|
3210
3488
|
await cursor.delete();
|
|
3211
3489
|
deletedCount++;
|
|
3212
3490
|
}
|
|
@@ -3372,7 +3650,7 @@
|
|
|
3372
3650
|
const downloadFonts = (fontUrls, styleName, options) => fontService.downloadFonts(fontUrls, styleName, options);
|
|
3373
3651
|
const getFontStats = () => fontService.getFontStats();
|
|
3374
3652
|
const getFontAnalytics = () => fontService.getFontAnalytics();
|
|
3375
|
-
const cleanupOldFonts = (maxAge) => fontService.cleanupOldFonts(maxAge);
|
|
3653
|
+
const cleanupOldFonts = (maxAge, options) => fontService.cleanupOldFonts(maxAge, options);
|
|
3376
3654
|
const verifyAndRepairFonts = () => fontService.verifyAndRepairFonts();
|
|
3377
3655
|
|
|
3378
3656
|
const spriteLogger = logger.scope('SpriteService');
|
|
@@ -3683,19 +3961,24 @@
|
|
|
3683
3961
|
};
|
|
3684
3962
|
}
|
|
3685
3963
|
/**
|
|
3686
|
-
*
|
|
3964
|
+
* Remove sprites older than the specified age. When `options.styleId` is
|
|
3965
|
+
* provided, only sprites belonging to that style (per
|
|
3966
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
3687
3967
|
* @param maxAge - Maximum age in days (default: 30)
|
|
3968
|
+
* @param options.styleId - Optional style filter; omit to scan all styles
|
|
3688
3969
|
* @returns Promise resolving to number of deleted sprites
|
|
3689
3970
|
*/
|
|
3690
|
-
async cleanupOldSprites(maxAge = 30) {
|
|
3971
|
+
async cleanupOldSprites(maxAge = 30, options = {}) {
|
|
3691
3972
|
const db = await this.db;
|
|
3692
3973
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3974
|
+
const { styleId } = options;
|
|
3693
3975
|
const tx = db.transaction(['sprites'], 'readwrite');
|
|
3694
3976
|
let deletedCount = 0;
|
|
3695
3977
|
let cursor = await tx.objectStore('sprites').openCursor();
|
|
3696
3978
|
while (cursor) {
|
|
3697
3979
|
const spriteEntry = cursor.value;
|
|
3698
|
-
|
|
3980
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(spriteEntry.key, styleId);
|
|
3981
|
+
if (belongs && spriteEntry.lastModified < cutoffTime) {
|
|
3699
3982
|
await cursor.delete();
|
|
3700
3983
|
deletedCount++;
|
|
3701
3984
|
}
|
|
@@ -3849,7 +4132,7 @@
|
|
|
3849
4132
|
const downloadSprites = (spriteUrls, styleName, options) => spriteService.downloadSprites(spriteUrls, styleName, options);
|
|
3850
4133
|
const getSpriteStats = () => spriteService.getSpriteStats();
|
|
3851
4134
|
const getSpriteAnalytics = () => spriteService.getSpriteAnalytics();
|
|
3852
|
-
const cleanupOldSprites = (maxAge) => spriteService.cleanupOldSprites(maxAge);
|
|
4135
|
+
const cleanupOldSprites = (maxAge, options) => spriteService.cleanupOldSprites(maxAge, options);
|
|
3853
4136
|
const verifyAndRepairSprites = () => spriteService.verifyAndRepairSprites();
|
|
3854
4137
|
|
|
3855
4138
|
var spriteService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -4928,7 +5211,15 @@
|
|
|
4928
5211
|
deletedSprites++;
|
|
4929
5212
|
}
|
|
4930
5213
|
}
|
|
4931
|
-
|
|
5214
|
+
let deletedModels = 0;
|
|
5215
|
+
const modelTx = db.transaction('models', 'readwrite');
|
|
5216
|
+
for await (const cursor of modelTx.store) {
|
|
5217
|
+
if (resourceKeyBelongsToStyle(cursor.value.key, styleId)) {
|
|
5218
|
+
await cursor.delete();
|
|
5219
|
+
deletedModels++;
|
|
5220
|
+
}
|
|
5221
|
+
}
|
|
5222
|
+
regionLogger$1.info(`Deleted style resources: ${deletedFonts} fonts, ${deletedGlyphs} glyphs, ${deletedSprites} sprites, ${deletedModels} models`);
|
|
4932
5223
|
}
|
|
4933
5224
|
/**
|
|
4934
5225
|
* Delete all tiles for a style
|
|
@@ -5036,7 +5327,7 @@
|
|
|
5036
5327
|
if (!region.styleUrl) {
|
|
5037
5328
|
throw new Error('Region must have a styleUrl');
|
|
5038
5329
|
}
|
|
5039
|
-
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, glyphRanges, tileOptions, } = options;
|
|
5330
|
+
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, skipModels = false, glyphRanges, tileOptions, } = options;
|
|
5040
5331
|
const emit = (phase, completed, total, message) => {
|
|
5041
5332
|
if (!onProgress)
|
|
5042
5333
|
return;
|
|
@@ -5091,8 +5382,13 @@
|
|
|
5091
5382
|
const spriteSources = normalizeSpriteProperty(originalSpriteUrl);
|
|
5092
5383
|
if (spriteSources.length > 0) {
|
|
5093
5384
|
const { downloadSprites } = await Promise.resolve().then(function () { return spriteService$1; });
|
|
5094
|
-
|
|
5095
|
-
|
|
5385
|
+
// Standard four sprite variants. For Mapbox Standard, an `iconset.pbf`
|
|
5386
|
+
// sibling is also served under the same /styles/v1/.../<hash>/ path
|
|
5387
|
+
// — we detect that case per-source below and append it to the list.
|
|
5388
|
+
const baseSuffixes = ['.json', '.png', '@2x.json', '@2x.png'];
|
|
5389
|
+
// Estimate total files assuming iconset is always included (the actual
|
|
5390
|
+
// number may be smaller; the emit helper clamps progress to total).
|
|
5391
|
+
const totalFiles = spriteSources.length * (baseSuffixes.length + 1);
|
|
5096
5392
|
let completed = 0;
|
|
5097
5393
|
emit('sprites', 0, totalFiles, 'Downloading sprites');
|
|
5098
5394
|
for (const source of spriteSources) {
|
|
@@ -5101,9 +5397,37 @@
|
|
|
5101
5397
|
spriteBase = resolveMapboxUrl(spriteBase, effectiveAccessToken);
|
|
5102
5398
|
}
|
|
5103
5399
|
const qIndex = spriteBase.indexOf('?');
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5106
|
-
|
|
5400
|
+
const suffixes = [...baseSuffixes];
|
|
5401
|
+
// Mapbox Standard serves an iconset.pbf alongside the sprite under
|
|
5402
|
+
// /styles/v1/{owner}/{style}/{hash}/sprite → the sibling file is
|
|
5403
|
+
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5404
|
+
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5405
|
+
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5406
|
+
let isMapboxStandardSprite = false;
|
|
5407
|
+
try {
|
|
5408
|
+
const parsed = new URL(pathWithoutQuery);
|
|
5409
|
+
isMapboxStandardSprite =
|
|
5410
|
+
parsed.hostname === 'api.mapbox.com' &&
|
|
5411
|
+
parsed.pathname.startsWith('/styles/v1/') &&
|
|
5412
|
+
parsed.pathname.endsWith('/sprite');
|
|
5413
|
+
}
|
|
5414
|
+
catch {
|
|
5415
|
+
// Non-URL sprite base (e.g. relative); not a Mapbox Standard sprite.
|
|
5416
|
+
}
|
|
5417
|
+
if (isMapboxStandardSprite) {
|
|
5418
|
+
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5419
|
+
suffixes.push('__ICONSET__');
|
|
5420
|
+
}
|
|
5421
|
+
const spriteUrls = suffixes.map(suffix => {
|
|
5422
|
+
if (suffix === '__ICONSET__') {
|
|
5423
|
+
// Replace trailing `sprite` with `iconset.pbf`, preserving query.
|
|
5424
|
+
const base = pathWithoutQuery.replace(/sprite$/, 'iconset.pbf');
|
|
5425
|
+
return qIndex !== -1 ? base + spriteBase.slice(qIndex) : base;
|
|
5426
|
+
}
|
|
5427
|
+
return qIndex !== -1
|
|
5428
|
+
? spriteBase.slice(0, qIndex) + suffix + spriteBase.slice(qIndex)
|
|
5429
|
+
: spriteBase + suffix;
|
|
5430
|
+
});
|
|
5107
5431
|
try {
|
|
5108
5432
|
const result = await downloadSprites(spriteUrls, styleId, {
|
|
5109
5433
|
enableValidation: true,
|
|
@@ -5149,7 +5473,42 @@
|
|
|
5149
5473
|
}
|
|
5150
5474
|
}
|
|
5151
5475
|
}
|
|
5152
|
-
// 4.
|
|
5476
|
+
// 4. Models — Mapbox Standard's `style.models` references 3D tree /
|
|
5477
|
+
// turbine .glb assets. Two value shapes exist in the wild
|
|
5478
|
+
// (plain string or `{ uri }`) — we accept both.
|
|
5479
|
+
let modelResult;
|
|
5480
|
+
if (!skipModels && storedStyle.models) {
|
|
5481
|
+
const rawModels = storedStyle.models;
|
|
5482
|
+
const resolved = {};
|
|
5483
|
+
for (const [name, value] of Object.entries(rawModels)) {
|
|
5484
|
+
const uri = typeof value === 'string' ? value : value?.uri;
|
|
5485
|
+
if (!uri)
|
|
5486
|
+
continue;
|
|
5487
|
+
if (uri.startsWith('idb://'))
|
|
5488
|
+
continue; // already patched
|
|
5489
|
+
const httpUrl = isMapboxProtocol(uri) && effectiveAccessToken
|
|
5490
|
+
? resolveMapboxUrl(uri, effectiveAccessToken)
|
|
5491
|
+
: uri;
|
|
5492
|
+
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
|
|
5493
|
+
resolved[name] = httpUrl;
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
if (Object.keys(resolved).length > 0) {
|
|
5497
|
+
const { downloadModels } = await Promise.resolve().then(function () { return modelService$1; });
|
|
5498
|
+
emit('models', 0, Object.keys(resolved).length, 'Downloading 3D models');
|
|
5499
|
+
try {
|
|
5500
|
+
modelResult = await downloadModels(resolved, styleId, {
|
|
5501
|
+
onProgress: (progress) => {
|
|
5502
|
+
emit('models', progress.completed, progress.total, 'Downloading 3D models');
|
|
5503
|
+
},
|
|
5504
|
+
});
|
|
5505
|
+
}
|
|
5506
|
+
catch (error) {
|
|
5507
|
+
regionLogger$1.warn('Model download failed (non-fatal):', error);
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
// 5. Tiles — use the stored (source-embedded) style, which still has HTTP tile URLs
|
|
5153
5512
|
const { downloadTiles } = await Promise.resolve().then(function () { return tileService$1; });
|
|
5154
5513
|
const regionForTiles = { ...region, styleId };
|
|
5155
5514
|
emit('tiles', 0, 100, 'Downloading tiles');
|
|
@@ -5162,7 +5521,7 @@
|
|
|
5162
5521
|
tileOptions?.onProgress?.(progress);
|
|
5163
5522
|
},
|
|
5164
5523
|
});
|
|
5165
|
-
//
|
|
5524
|
+
// 6. Metadata — must run last, since addRegion patches style URLs to idb://.
|
|
5166
5525
|
// Do NOT auto-fill tileExtension from tileResult: that's only the first
|
|
5167
5526
|
// source's extension, and addRegion feeds it to patchStyleForOffline which
|
|
5168
5527
|
// would override ALL sources — breaking mixed raster+vector styles. The
|
|
@@ -5178,6 +5537,7 @@
|
|
|
5178
5537
|
styleResult,
|
|
5179
5538
|
spriteResults,
|
|
5180
5539
|
glyphResult,
|
|
5540
|
+
modelResult,
|
|
5181
5541
|
tileResult,
|
|
5182
5542
|
};
|
|
5183
5543
|
}
|
|
@@ -6316,10 +6676,15 @@
|
|
|
6316
6676
|
tilesLength: config.tiles ? config.tiles.length : 0,
|
|
6317
6677
|
url: config.url,
|
|
6318
6678
|
});
|
|
6319
|
-
// Handle tile-based sources (vector, raster, raster-dem, batched-model
|
|
6679
|
+
// Handle tile-based sources (vector, raster, raster-dem, batched-model,
|
|
6680
|
+
// raster-array). `raster-array` is used by Mapbox Standard for layers
|
|
6681
|
+
// like `mapbox-landmarks` (mapbox.mapbox-landmark-icons-v1) — the tiles
|
|
6682
|
+
// are fetched from the same /v4/ endpoint as other tilesets, so the
|
|
6683
|
+
// TileJSON resolution path below handles them uniformly.
|
|
6320
6684
|
if (config.type === 'vector' ||
|
|
6321
6685
|
config.type === 'raster' ||
|
|
6322
6686
|
config.type === 'raster-dem' ||
|
|
6687
|
+
config.type === 'raster-array' ||
|
|
6323
6688
|
config.type === 'batched-model') {
|
|
6324
6689
|
// Handle direct tile URLs in the source config
|
|
6325
6690
|
if (config.tiles && Array.isArray(config.tiles) && config.tiles.length > 0) {
|
|
@@ -6544,8 +6909,7 @@
|
|
|
6544
6909
|
}
|
|
6545
6910
|
}
|
|
6546
6911
|
extractExtension(template) {
|
|
6547
|
-
|
|
6548
|
-
return extMatch ? extMatch[1] : 'pbf';
|
|
6912
|
+
return extractTileExtensionFromUrl(template);
|
|
6549
6913
|
}
|
|
6550
6914
|
selectTileTemplate(templates, coord) {
|
|
6551
6915
|
if (templates.length === 1) {
|
|
@@ -6927,14 +7291,21 @@
|
|
|
6927
7291
|
},
|
|
6928
7292
|
};
|
|
6929
7293
|
}
|
|
6930
|
-
|
|
7294
|
+
/**
|
|
7295
|
+
* Remove glyphs older than the specified age. When `options.styleId` is
|
|
7296
|
+
* provided, only glyphs belonging to that style (per
|
|
7297
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
7298
|
+
*/
|
|
7299
|
+
async cleanupOldGlyphs(maxAge = 30, options = {}) {
|
|
6931
7300
|
const db = await this.db;
|
|
6932
7301
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7302
|
+
const { styleId } = options;
|
|
6933
7303
|
let deletedCount = 0;
|
|
6934
7304
|
const tx = db.transaction('glyphs', 'readwrite');
|
|
6935
7305
|
for await (const cursor of tx.store) {
|
|
6936
7306
|
const glyphEntry = cursor.value;
|
|
6937
|
-
|
|
7307
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(glyphEntry.key, styleId);
|
|
7308
|
+
if (belongs && glyphEntry.lastModified < cutoffTime) {
|
|
6938
7309
|
await cursor.delete();
|
|
6939
7310
|
deletedCount++;
|
|
6940
7311
|
}
|
|
@@ -7037,7 +7408,7 @@
|
|
|
7037
7408
|
const loadGlyphs = (fontstack, ranges, styleName) => glyphService.loadGlyphs(fontstack, ranges, styleName);
|
|
7038
7409
|
const getGlyphStats = () => glyphService.getGlyphStats();
|
|
7039
7410
|
const getGlyphAnalytics = () => glyphService.getGlyphAnalytics();
|
|
7040
|
-
const cleanupOldGlyphs = (maxAge) => glyphService.cleanupOldGlyphs(maxAge);
|
|
7411
|
+
const cleanupOldGlyphs = (maxAge, options) => glyphService.cleanupOldGlyphs(maxAge, options);
|
|
7041
7412
|
const verifyAndRepairGlyphs = () => glyphService.verifyAndRepairGlyphs();
|
|
7042
7413
|
|
|
7043
7414
|
var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -7052,12 +7423,207 @@
|
|
|
7052
7423
|
verifyAndRepairGlyphs: verifyAndRepairGlyphs
|
|
7053
7424
|
});
|
|
7054
7425
|
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7426
|
+
const modelLogger = logger.scope('ModelService');
|
|
7427
|
+
/**
|
|
7428
|
+
* Build the storage key for a model. Kept consistent with sprite/glyph
|
|
7429
|
+
* conventions: `{styleId}::model::{modelName}`.
|
|
7430
|
+
*/
|
|
7431
|
+
function modelKey(styleId, modelName) {
|
|
7432
|
+
return `${styleId}::model::${modelName}`;
|
|
7433
|
+
}
|
|
7434
|
+
/** True when the given key belongs to the given styleId's model store prefix. */
|
|
7435
|
+
function modelKeyBelongsToStyle(key, styleId) {
|
|
7436
|
+
return key.startsWith(`${styleId}::model::`);
|
|
7437
|
+
}
|
|
7438
|
+
/**
|
|
7439
|
+
* Service for downloading, storing, and serving Mapbox 3D model (.glb) files.
|
|
7440
|
+
*
|
|
7441
|
+
* Mapbox Standard declares 32 models at the top of `style.models`:
|
|
7442
|
+
*
|
|
7443
|
+
* ```json
|
|
7444
|
+
* {
|
|
7445
|
+
* "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb",
|
|
7446
|
+
* ...
|
|
7447
|
+
* }
|
|
7448
|
+
* ```
|
|
7449
|
+
*
|
|
7450
|
+
* `model` layers (e.g. `trees`, `wind-turbine-towers`) pick one by name at
|
|
7451
|
+
* render time. For offline use each referenced URL is fetched and stored
|
|
7452
|
+
* here, and `patchStyleForOffline` rewrites the dictionary entries to
|
|
7453
|
+
* `idb://{styleId}/model/{name}` URLs.
|
|
7454
|
+
*/
|
|
7455
|
+
class ModelService {
|
|
7456
|
+
db = dbPromise;
|
|
7457
|
+
/**
|
|
7458
|
+
* Download the set of models referenced by `style.models` for one style.
|
|
7459
|
+
*
|
|
7460
|
+
* @param models `{ modelName: resolvedHttpUrl }` — URLs must already be
|
|
7461
|
+
* resolved (mapbox:// URLs should be resolved by the caller).
|
|
7462
|
+
* @param styleId The owning style's key.
|
|
7463
|
+
*/
|
|
7464
|
+
async downloadModels(models, styleId, options = {}) {
|
|
7465
|
+
const db = await this.db;
|
|
7466
|
+
const { onProgress, batchSize = 4, maxRetries = 3, skipExisting = true, timeoutMs = 30000, } = options;
|
|
7467
|
+
const entries = Object.entries(models);
|
|
7468
|
+
const progressTracker = createProgressTracker(entries.length);
|
|
7469
|
+
const result = {
|
|
7470
|
+
totalModels: entries.length,
|
|
7471
|
+
downloadedModels: 0,
|
|
7472
|
+
skippedModels: 0,
|
|
7473
|
+
failedModels: 0,
|
|
7474
|
+
totalSize: 0,
|
|
7475
|
+
errors: [],
|
|
7476
|
+
};
|
|
7477
|
+
const emit = () => onProgress?.(progressTracker.getProgress());
|
|
7478
|
+
emit();
|
|
7479
|
+
if (entries.length === 0)
|
|
7480
|
+
return result;
|
|
7481
|
+
// Pre-compute existing keys for skipExisting
|
|
7482
|
+
const existingKeys = new Set();
|
|
7483
|
+
if (skipExisting) {
|
|
7484
|
+
const tx = db.transaction('models', 'readonly');
|
|
7485
|
+
for await (const cursor of tx.store) {
|
|
7486
|
+
existingKeys.add(cursor.value.key);
|
|
7487
|
+
}
|
|
7488
|
+
}
|
|
7489
|
+
await processBatch(entries, async ([modelName, url]) => {
|
|
7490
|
+
const key = modelKey(styleId, modelName);
|
|
7491
|
+
const label = `${styleId}::${modelName}`;
|
|
7492
|
+
if (skipExisting && existingKeys.has(key)) {
|
|
7493
|
+
result.skippedModels++;
|
|
7494
|
+
progressTracker.update(1, label);
|
|
7495
|
+
emit();
|
|
7496
|
+
return;
|
|
7497
|
+
}
|
|
7498
|
+
try {
|
|
7499
|
+
const response = await fetchResourceWithRetry(url, {
|
|
7500
|
+
retries: maxRetries,
|
|
7501
|
+
timeout: timeoutMs,
|
|
7502
|
+
proxyType: 'tiles',
|
|
7503
|
+
});
|
|
7504
|
+
if (response.type === 'json') {
|
|
7505
|
+
throw new Error('Unexpected JSON response for model');
|
|
7506
|
+
}
|
|
7507
|
+
const data = response.data;
|
|
7508
|
+
const contentType = ('contentType' in response && response.contentType) || 'model/gltf-binary';
|
|
7509
|
+
const entry = {
|
|
7510
|
+
key,
|
|
7511
|
+
data,
|
|
7512
|
+
contentType,
|
|
7513
|
+
size: data.byteLength,
|
|
7514
|
+
url,
|
|
7515
|
+
styleId,
|
|
7516
|
+
modelName,
|
|
7517
|
+
lastModified: Date.now(),
|
|
7518
|
+
downloadedAt: new Date().toISOString(),
|
|
7519
|
+
expires: response.expires,
|
|
7520
|
+
};
|
|
7521
|
+
await db.put('models', entry);
|
|
7522
|
+
result.downloadedModels++;
|
|
7523
|
+
result.totalSize += data.byteLength;
|
|
7524
|
+
progressTracker.update(1, label);
|
|
7525
|
+
}
|
|
7526
|
+
catch (err) {
|
|
7527
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7528
|
+
result.failedModels++;
|
|
7529
|
+
result.errors.push({ url, error: message });
|
|
7530
|
+
modelLogger.warn(`Failed to download model "${modelName}" from ${url}:`, err);
|
|
7531
|
+
progressTracker.update(1, label, message);
|
|
7532
|
+
}
|
|
7533
|
+
emit();
|
|
7534
|
+
}, { batchSize });
|
|
7535
|
+
modelLogger.info(`Models downloaded for style ${styleId}: ${result.downloadedModels} new, ${result.skippedModels} skipped, ${result.failedModels} failed`);
|
|
7536
|
+
return result;
|
|
7059
7537
|
}
|
|
7060
|
-
|
|
7538
|
+
/** Retrieve a single model by `{styleId, modelName}`. */
|
|
7539
|
+
async getModel(styleId, modelName) {
|
|
7540
|
+
const db = await this.db;
|
|
7541
|
+
return db.get('models', modelKey(styleId, modelName));
|
|
7542
|
+
}
|
|
7543
|
+
/** Aggregate stats across all stored models. */
|
|
7544
|
+
async getModelStats() {
|
|
7545
|
+
const db = await this.db;
|
|
7546
|
+
const stats = {
|
|
7547
|
+
count: 0,
|
|
7548
|
+
totalSize: 0,
|
|
7549
|
+
averageSize: 0,
|
|
7550
|
+
models: [],
|
|
7551
|
+
modelsByStyle: {},
|
|
7552
|
+
};
|
|
7553
|
+
const tx = db.transaction('models', 'readonly');
|
|
7554
|
+
for await (const cursor of tx.store) {
|
|
7555
|
+
const m = cursor.value;
|
|
7556
|
+
stats.count++;
|
|
7557
|
+
stats.totalSize += m.size;
|
|
7558
|
+
stats.models.push({ name: m.modelName, size: m.size, lastModified: m.lastModified });
|
|
7559
|
+
stats.modelsByStyle[m.styleId] = (stats.modelsByStyle[m.styleId] ?? 0) + 1;
|
|
7560
|
+
}
|
|
7561
|
+
stats.averageSize = stats.count > 0 ? stats.totalSize / stats.count : 0;
|
|
7562
|
+
return stats;
|
|
7563
|
+
}
|
|
7564
|
+
/**
|
|
7565
|
+
* Delete models older than `maxAge` days. Defaults to 30.
|
|
7566
|
+
*/
|
|
7567
|
+
async cleanupOldModels(maxAge = 30) {
|
|
7568
|
+
const db = await this.db;
|
|
7569
|
+
const cutoff = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7570
|
+
let deleted = 0;
|
|
7571
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7572
|
+
for await (const cursor of tx.store) {
|
|
7573
|
+
if (cursor.value.lastModified < cutoff) {
|
|
7574
|
+
await cursor.delete();
|
|
7575
|
+
deleted++;
|
|
7576
|
+
}
|
|
7577
|
+
}
|
|
7578
|
+
return deleted;
|
|
7579
|
+
}
|
|
7580
|
+
/**
|
|
7581
|
+
* Basic integrity check: remove entries with empty/missing data.
|
|
7582
|
+
*/
|
|
7583
|
+
async verifyAndRepairModels() {
|
|
7584
|
+
const db = await this.db;
|
|
7585
|
+
let verified = 0;
|
|
7586
|
+
let removed = 0;
|
|
7587
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7588
|
+
for await (const cursor of tx.store) {
|
|
7589
|
+
const m = cursor.value;
|
|
7590
|
+
if (!m.data || m.data.byteLength === 0) {
|
|
7591
|
+
await cursor.delete();
|
|
7592
|
+
removed++;
|
|
7593
|
+
}
|
|
7594
|
+
else {
|
|
7595
|
+
verified++;
|
|
7596
|
+
}
|
|
7597
|
+
}
|
|
7598
|
+
return { verified, repaired: 0, removed };
|
|
7599
|
+
}
|
|
7600
|
+
}
|
|
7601
|
+
// Singleton + convenience exports, matching other service modules.
|
|
7602
|
+
const modelService = new ModelService();
|
|
7603
|
+
const downloadModels = (models, styleId, options) => modelService.downloadModels(models, styleId, options);
|
|
7604
|
+
const getModel = (styleId, modelName) => modelService.getModel(styleId, modelName);
|
|
7605
|
+
const getModelStats = () => modelService.getModelStats();
|
|
7606
|
+
const cleanupOldModels = (maxAge) => modelService.cleanupOldModels(maxAge);
|
|
7607
|
+
const verifyAndRepairModels = () => modelService.verifyAndRepairModels();
|
|
7608
|
+
|
|
7609
|
+
var modelService$1 = /*#__PURE__*/Object.freeze({
|
|
7610
|
+
__proto__: null,
|
|
7611
|
+
ModelService: ModelService,
|
|
7612
|
+
cleanupOldModels: cleanupOldModels,
|
|
7613
|
+
downloadModels: downloadModels,
|
|
7614
|
+
getModel: getModel,
|
|
7615
|
+
getModelStats: getModelStats,
|
|
7616
|
+
modelKeyBelongsToStyle: modelKeyBelongsToStyle,
|
|
7617
|
+
modelService: modelService,
|
|
7618
|
+
verifyAndRepairModels: verifyAndRepairModels
|
|
7619
|
+
});
|
|
7620
|
+
|
|
7621
|
+
class ResourceService {
|
|
7622
|
+
// Tile Management Methods
|
|
7623
|
+
async downloadTilesWithOptions(region, style, styleId, options = {}) {
|
|
7624
|
+
return downloadTiles(region, style, styleId, options);
|
|
7625
|
+
}
|
|
7626
|
+
async getTileStats(styleId) {
|
|
7061
7627
|
return getTileStats(styleId);
|
|
7062
7628
|
}
|
|
7063
7629
|
async getTileAnalytics(styleId) {
|
|
@@ -7077,8 +7643,7 @@
|
|
|
7077
7643
|
return getFontAnalytics();
|
|
7078
7644
|
}
|
|
7079
7645
|
async cleanupOldFonts(styleId, options) {
|
|
7080
|
-
|
|
7081
|
-
return cleanupOldFonts(maxAge);
|
|
7646
|
+
return cleanupOldFonts(options?.maxAge, { styleId });
|
|
7082
7647
|
}
|
|
7083
7648
|
async verifyAndRepairFonts() {
|
|
7084
7649
|
return verifyAndRepairFonts();
|
|
@@ -7094,8 +7659,7 @@
|
|
|
7094
7659
|
return getSpriteAnalytics();
|
|
7095
7660
|
}
|
|
7096
7661
|
async cleanupOldSprites(styleId, options) {
|
|
7097
|
-
|
|
7098
|
-
return cleanupOldSprites(maxAge);
|
|
7662
|
+
return cleanupOldSprites(options?.maxAge, { styleId });
|
|
7099
7663
|
}
|
|
7100
7664
|
async verifyAndRepairSprites() {
|
|
7101
7665
|
return verifyAndRepairSprites();
|
|
@@ -7114,12 +7678,24 @@
|
|
|
7114
7678
|
return loadGlyphs(fontstack, ranges, styleId);
|
|
7115
7679
|
}
|
|
7116
7680
|
async cleanupOldGlyphs(styleId, options) {
|
|
7117
|
-
|
|
7118
|
-
return cleanupOldGlyphs(maxAge);
|
|
7681
|
+
return cleanupOldGlyphs(options?.maxAge, { styleId });
|
|
7119
7682
|
}
|
|
7120
7683
|
async verifyAndRepairGlyphs() {
|
|
7121
7684
|
return verifyAndRepairGlyphs();
|
|
7122
7685
|
}
|
|
7686
|
+
// 3D Model Management Methods
|
|
7687
|
+
async downloadModelsWithOptions(models, styleId, options = {}) {
|
|
7688
|
+
return downloadModels(models, styleId, options);
|
|
7689
|
+
}
|
|
7690
|
+
async getModelStats() {
|
|
7691
|
+
return getModelStats();
|
|
7692
|
+
}
|
|
7693
|
+
async cleanupOldModels(options) {
|
|
7694
|
+
return cleanupOldModels(options?.maxAge);
|
|
7695
|
+
}
|
|
7696
|
+
async verifyAndRepairModels() {
|
|
7697
|
+
return verifyAndRepairModels();
|
|
7698
|
+
}
|
|
7123
7699
|
}
|
|
7124
7700
|
|
|
7125
7701
|
// The underlying stats functions already iterate every entry in their
|
|
@@ -7217,6 +7793,88 @@
|
|
|
7217
7793
|
}
|
|
7218
7794
|
}
|
|
7219
7795
|
|
|
7796
|
+
/**
|
|
7797
|
+
* MBTiles uses TMS tile_row ordering; our storage uses XYZ y. Flip across
|
|
7798
|
+
* either direction with the same formula.
|
|
7799
|
+
*/
|
|
7800
|
+
function flipY(y, z) {
|
|
7801
|
+
return (1 << z) - 1 - y;
|
|
7802
|
+
}
|
|
7803
|
+
/** Vector tile formats that downstream consumers (QGIS, maplibre-native) expect gzipped. */
|
|
7804
|
+
const VECTOR_FORMATS = new Set(['pbf', 'mvt']);
|
|
7805
|
+
function hasGzipMagic(bytes) {
|
|
7806
|
+
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
7807
|
+
}
|
|
7808
|
+
async function drainReadable(readable) {
|
|
7809
|
+
const reader = readable.getReader();
|
|
7810
|
+
const chunks = [];
|
|
7811
|
+
let total = 0;
|
|
7812
|
+
while (true) {
|
|
7813
|
+
const { done, value } = await reader.read();
|
|
7814
|
+
if (done)
|
|
7815
|
+
break;
|
|
7816
|
+
if (value) {
|
|
7817
|
+
chunks.push(value);
|
|
7818
|
+
total += value.byteLength;
|
|
7819
|
+
}
|
|
7820
|
+
}
|
|
7821
|
+
const out = new Uint8Array(total);
|
|
7822
|
+
let offset = 0;
|
|
7823
|
+
for (const chunk of chunks) {
|
|
7824
|
+
out.set(chunk, offset);
|
|
7825
|
+
offset += chunk.byteLength;
|
|
7826
|
+
}
|
|
7827
|
+
return out;
|
|
7828
|
+
}
|
|
7829
|
+
async function transformBytes(bytes, transform) {
|
|
7830
|
+
const writer = transform.writable.getWriter();
|
|
7831
|
+
// Don't await — the read loop below drives the pipe and we only want
|
|
7832
|
+
// the final bytes, not back-pressure handling for a single chunk.
|
|
7833
|
+
void writer.write(bytes);
|
|
7834
|
+
void writer.close();
|
|
7835
|
+
return drainReadable(transform.readable);
|
|
7836
|
+
}
|
|
7837
|
+
async function gzipBytes(bytes) {
|
|
7838
|
+
return transformBytes(bytes, new CompressionStream('gzip'));
|
|
7839
|
+
}
|
|
7840
|
+
async function gunzipBytes(bytes) {
|
|
7841
|
+
return transformBytes(bytes, new DecompressionStream('gzip'));
|
|
7842
|
+
}
|
|
7843
|
+
/**
|
|
7844
|
+
* Build the MBTiles `json` metadata payload. For vector tiles this is
|
|
7845
|
+
* mandatory for tippecanoe/QGIS/maplibre-native to render — they read
|
|
7846
|
+
* `vector_layers` from here.
|
|
7847
|
+
*
|
|
7848
|
+
* `vector_layers` is inferred from the offline style's vector sources
|
|
7849
|
+
* (populated by the TileJSON expansion step in styleService). Multiple
|
|
7850
|
+
* vector sources are merged; duplicates de-duped by id, first wins.
|
|
7851
|
+
*/
|
|
7852
|
+
function buildVectorJsonMetadata(style, sourceIds) {
|
|
7853
|
+
if (!style || typeof style !== 'object')
|
|
7854
|
+
return null;
|
|
7855
|
+
const sources = style.sources;
|
|
7856
|
+
if (!sources)
|
|
7857
|
+
return null;
|
|
7858
|
+
const merged = [];
|
|
7859
|
+
const seen = new Set();
|
|
7860
|
+
for (const [id, src] of Object.entries(sources)) {
|
|
7861
|
+
if (sourceIds.size > 0 && !sourceIds.has(id))
|
|
7862
|
+
continue;
|
|
7863
|
+
const layers = src?.vector_layers;
|
|
7864
|
+
if (!Array.isArray(layers))
|
|
7865
|
+
continue;
|
|
7866
|
+
for (const layer of layers) {
|
|
7867
|
+
const layerId = typeof layer?.id === 'string' ? layer.id : null;
|
|
7868
|
+
if (!layerId || seen.has(layerId))
|
|
7869
|
+
continue;
|
|
7870
|
+
seen.add(layerId);
|
|
7871
|
+
merged.push(layer);
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
7874
|
+
if (merged.length === 0)
|
|
7875
|
+
return null;
|
|
7876
|
+
return JSON.stringify({ vector_layers: merged });
|
|
7877
|
+
}
|
|
7220
7878
|
const serviceLogger = logger.scope('ImportExportService');
|
|
7221
7879
|
class ImportExportService {
|
|
7222
7880
|
db = dbPromise;
|
|
@@ -7224,270 +7882,173 @@
|
|
|
7224
7882
|
// No need for initialization since dbPromise is already available
|
|
7225
7883
|
}
|
|
7226
7884
|
/**
|
|
7227
|
-
* Export a
|
|
7885
|
+
* Export region as a real binary MBTiles SQLite file.
|
|
7886
|
+
*
|
|
7887
|
+
* Produces a v1.3-compliant MBTiles archive: `metadata` + `tiles` tables,
|
|
7888
|
+
* with `tile_row` flipped to TMS ordering. The resulting blob can be read
|
|
7889
|
+
* by tippecanoe, QGIS, maplibre-native, etc.
|
|
7228
7890
|
*/
|
|
7229
|
-
async
|
|
7891
|
+
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7230
7892
|
const onProgress = options.onProgress || (() => { });
|
|
7231
7893
|
try {
|
|
7232
7894
|
onProgress({
|
|
7233
7895
|
stage: 'preparing',
|
|
7234
7896
|
percentage: 0,
|
|
7235
|
-
message: 'Preparing export...',
|
|
7897
|
+
message: 'Preparing MBTiles export...',
|
|
7236
7898
|
});
|
|
7237
|
-
// Get region metadata
|
|
7238
7899
|
const region = await this.getRegionMetadata(regionId);
|
|
7239
7900
|
if (!region) {
|
|
7240
7901
|
throw new Error(`Region ${regionId} not found`);
|
|
7241
7902
|
}
|
|
7903
|
+
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7904
|
+
// Pick format: caller override → region.tileExtension → default pbf.
|
|
7905
|
+
// Drives both the metadata row and whether tile bytes get gzipped.
|
|
7906
|
+
const format = String(options.format || region.tileExtension || 'pbf').toLowerCase();
|
|
7907
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
7242
7908
|
onProgress({
|
|
7243
|
-
stage: '
|
|
7244
|
-
percentage:
|
|
7245
|
-
message: '
|
|
7909
|
+
stage: 'processing',
|
|
7910
|
+
percentage: 75,
|
|
7911
|
+
message: isVector ? 'Compressing vector tiles...' : 'Packing SQLite database...',
|
|
7246
7912
|
});
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
createdAt: region.created, // StoredRegion uses 'created' not 'createdAt'
|
|
7257
|
-
exportedAt: Date.now(),
|
|
7258
|
-
version: '1.0.0',
|
|
7259
|
-
format: 'json',
|
|
7260
|
-
},
|
|
7261
|
-
style: {},
|
|
7262
|
-
tiles: [],
|
|
7263
|
-
sprites: [],
|
|
7264
|
-
fonts: [],
|
|
7265
|
-
};
|
|
7266
|
-
// Export style if requested
|
|
7267
|
-
if (options.includeStyle !== false) {
|
|
7268
|
-
onProgress({
|
|
7269
|
-
stage: 'exporting',
|
|
7270
|
-
percentage: 20,
|
|
7271
|
-
message: 'Exporting style data...',
|
|
7272
|
-
});
|
|
7273
|
-
exportData.style = await this.exportStyle(regionId);
|
|
7913
|
+
// Gzip vector tiles. Idempotent: skip tiles already gzipped (downloaded
|
|
7914
|
+
// with their original gzip wrapper intact).
|
|
7915
|
+
const packedTiles = [];
|
|
7916
|
+
for (const tile of tiles) {
|
|
7917
|
+
const raw = tile.data instanceof ArrayBuffer
|
|
7918
|
+
? new Uint8Array(tile.data)
|
|
7919
|
+
: new Uint8Array(tile.data);
|
|
7920
|
+
const data = isVector && !hasGzipMagic(raw) ? await gzipBytes(raw) : raw;
|
|
7921
|
+
packedTiles.push({ z: tile.z, x: tile.x, y: tile.y, data });
|
|
7274
7922
|
}
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7280
|
-
|
|
7923
|
+
onProgress({
|
|
7924
|
+
stage: 'processing',
|
|
7925
|
+
percentage: 85,
|
|
7926
|
+
message: 'Packing SQLite database...',
|
|
7927
|
+
});
|
|
7928
|
+
const SQL = await getSqlJs();
|
|
7929
|
+
const db = new SQL.Database();
|
|
7930
|
+
try {
|
|
7931
|
+
db.run(`
|
|
7932
|
+
CREATE TABLE metadata (name TEXT, value TEXT);
|
|
7933
|
+
CREATE TABLE tiles (
|
|
7934
|
+
zoom_level INTEGER NOT NULL,
|
|
7935
|
+
tile_column INTEGER NOT NULL,
|
|
7936
|
+
tile_row INTEGER NOT NULL,
|
|
7937
|
+
tile_data BLOB
|
|
7938
|
+
);
|
|
7939
|
+
CREATE UNIQUE INDEX tile_index ON tiles (zoom_level, tile_column, tile_row);
|
|
7940
|
+
CREATE UNIQUE INDEX name ON metadata (name);
|
|
7941
|
+
`);
|
|
7942
|
+
const [[west, south], [east, north]] = region.bounds;
|
|
7943
|
+
const centerLon = (west + east) / 2;
|
|
7944
|
+
const centerLat = (south + north) / 2;
|
|
7945
|
+
const centerZoom = Math.max(region.minZoom, Math.min(region.maxZoom, Math.round((region.minZoom + region.maxZoom) / 2)));
|
|
7946
|
+
const metadataRows = {
|
|
7947
|
+
name: region.name || region.id,
|
|
7948
|
+
// MBTiles 1.3 type: 'overlay' or 'baselayer'. Baselayer matches how
|
|
7949
|
+
// QGIS treats the dataset (full-coverage map rather than overlay).
|
|
7950
|
+
type: isVector ? 'baselayer' : 'overlay',
|
|
7951
|
+
version: '1.0',
|
|
7952
|
+
description: region.name || region.id,
|
|
7953
|
+
format,
|
|
7954
|
+
bounds: `${west},${south},${east},${north}`,
|
|
7955
|
+
center: `${centerLon},${centerLat},${centerZoom}`,
|
|
7956
|
+
minzoom: String(region.minZoom),
|
|
7957
|
+
maxzoom: String(region.maxZoom),
|
|
7958
|
+
};
|
|
7959
|
+
// For vector tiles, the `json` field with `vector_layers` is required
|
|
7960
|
+
// by the MBTiles 1.3 spec and by every vector tile consumer worth
|
|
7961
|
+
// opening the file in. Derive it from the offline style.
|
|
7962
|
+
if (isVector) {
|
|
7963
|
+
const style = await this.exportStyle(regionId);
|
|
7964
|
+
const sourceIds = new Set(tiles.map(t => t.sourceId).filter(Boolean));
|
|
7965
|
+
const json = buildVectorJsonMetadata(style.style ?? style, sourceIds);
|
|
7966
|
+
if (json)
|
|
7967
|
+
metadataRows.json = json;
|
|
7968
|
+
}
|
|
7969
|
+
for (const [k, v] of Object.entries(options.metadata || {})) {
|
|
7970
|
+
metadataRows[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
|
7971
|
+
}
|
|
7972
|
+
const insertMeta = db.prepare(`INSERT INTO metadata (name, value) VALUES (?, ?)`);
|
|
7973
|
+
try {
|
|
7974
|
+
for (const [name, value] of Object.entries(metadataRows)) {
|
|
7975
|
+
insertMeta.run([name, value]);
|
|
7976
|
+
}
|
|
7977
|
+
}
|
|
7978
|
+
finally {
|
|
7979
|
+
insertMeta.free();
|
|
7980
|
+
}
|
|
7981
|
+
const insertTile = db.prepare(`INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data)
|
|
7982
|
+
VALUES (?, ?, ?, ?)`);
|
|
7983
|
+
try {
|
|
7984
|
+
db.run('BEGIN');
|
|
7985
|
+
for (const tile of packedTiles) {
|
|
7986
|
+
insertTile.run([tile.z, tile.x, flipY(tile.y, tile.z), tile.data]);
|
|
7987
|
+
}
|
|
7988
|
+
db.run('COMMIT');
|
|
7989
|
+
}
|
|
7990
|
+
finally {
|
|
7991
|
+
insertTile.free();
|
|
7992
|
+
}
|
|
7993
|
+
const binary = db.export();
|
|
7994
|
+
const blob = new Blob([binary.buffer], {
|
|
7995
|
+
type: 'application/x-sqlite3',
|
|
7281
7996
|
});
|
|
7282
|
-
exportData.tiles = await this.exportTiles(regionId, onProgress);
|
|
7283
|
-
}
|
|
7284
|
-
// Export sprites if requested
|
|
7285
|
-
if (options.includeSprites !== false) {
|
|
7286
7997
|
onProgress({
|
|
7287
|
-
stage: '
|
|
7288
|
-
percentage:
|
|
7289
|
-
message: '
|
|
7998
|
+
stage: 'complete',
|
|
7999
|
+
percentage: 100,
|
|
8000
|
+
message: 'MBTiles export complete!',
|
|
7290
8001
|
});
|
|
7291
|
-
|
|
8002
|
+
return {
|
|
8003
|
+
success: true,
|
|
8004
|
+
format: 'mbtiles',
|
|
8005
|
+
filename: `${region.name || region.id}.mbtiles`,
|
|
8006
|
+
blob,
|
|
8007
|
+
size: blob.size,
|
|
8008
|
+
statistics: {
|
|
8009
|
+
tilesExported: tiles.length,
|
|
8010
|
+
spritesExported: 0,
|
|
8011
|
+
fontsExported: 0,
|
|
8012
|
+
},
|
|
8013
|
+
};
|
|
7292
8014
|
}
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
onProgress({
|
|
7296
|
-
stage: 'exporting',
|
|
7297
|
-
percentage: 85,
|
|
7298
|
-
message: 'Exporting fonts...',
|
|
7299
|
-
});
|
|
7300
|
-
exportData.fonts = await this.exportFonts(regionId);
|
|
8015
|
+
finally {
|
|
8016
|
+
db.close();
|
|
7301
8017
|
}
|
|
7302
|
-
onProgress({
|
|
7303
|
-
stage: 'processing',
|
|
7304
|
-
percentage: 95,
|
|
7305
|
-
message: 'Creating export file...',
|
|
7306
|
-
});
|
|
7307
|
-
// Create JSON blob
|
|
7308
|
-
const jsonString = JSON.stringify(exportData, null, 2);
|
|
7309
|
-
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
7310
|
-
onProgress({
|
|
7311
|
-
stage: 'complete',
|
|
7312
|
-
percentage: 100,
|
|
7313
|
-
message: 'Export complete!',
|
|
7314
|
-
});
|
|
7315
|
-
return {
|
|
7316
|
-
success: true,
|
|
7317
|
-
format: 'json',
|
|
7318
|
-
filename: `${region.name || region.id}_export.json`,
|
|
7319
|
-
blob,
|
|
7320
|
-
size: blob.size,
|
|
7321
|
-
statistics: {
|
|
7322
|
-
tilesExported: exportData.tiles.length,
|
|
7323
|
-
spritesExported: exportData.sprites.length,
|
|
7324
|
-
fontsExported: exportData.fonts.length,
|
|
7325
|
-
},
|
|
7326
|
-
};
|
|
7327
8018
|
}
|
|
7328
8019
|
catch (error) {
|
|
7329
8020
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7330
|
-
throw new Error(`
|
|
8021
|
+
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7331
8022
|
}
|
|
7332
8023
|
}
|
|
7333
8024
|
/**
|
|
7334
|
-
*
|
|
8025
|
+
* Import region from a binary MBTiles (SQLite) file.
|
|
7335
8026
|
*/
|
|
7336
|
-
async
|
|
7337
|
-
const onProgress =
|
|
8027
|
+
async importRegion(importData) {
|
|
8028
|
+
const onProgress = importData.onProgress || (() => { });
|
|
7338
8029
|
try {
|
|
7339
8030
|
onProgress({
|
|
7340
8031
|
stage: 'preparing',
|
|
7341
8032
|
percentage: 0,
|
|
7342
|
-
message: '
|
|
8033
|
+
message: 'Reading file...',
|
|
7343
8034
|
});
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
// to create a proper PMTiles file format
|
|
7347
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7348
|
-
if (!region) {
|
|
7349
|
-
throw new Error(`Region ${regionId} not found`);
|
|
8035
|
+
if (importData.format !== 'mbtiles') {
|
|
8036
|
+
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7350
8037
|
}
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
const pmtilesData = {
|
|
7355
|
-
header: {
|
|
7356
|
-
version: 3,
|
|
7357
|
-
type: 'mvt',
|
|
7358
|
-
compression: options.compression || 'gzip',
|
|
7359
|
-
bounds: region.bounds,
|
|
7360
|
-
minZoom: region.minZoom,
|
|
7361
|
-
maxZoom: region.maxZoom,
|
|
7362
|
-
metadata: {
|
|
7363
|
-
name: region.name,
|
|
7364
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7365
|
-
...options.metadata,
|
|
7366
|
-
},
|
|
7367
|
-
},
|
|
7368
|
-
tiles: tiles,
|
|
7369
|
-
};
|
|
7370
|
-
// Convert to binary format (simplified)
|
|
7371
|
-
const jsonString = JSON.stringify(pmtilesData);
|
|
7372
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
7373
|
-
onProgress({
|
|
7374
|
-
stage: 'complete',
|
|
7375
|
-
percentage: 100,
|
|
7376
|
-
message: 'PMTiles export complete!',
|
|
7377
|
-
});
|
|
7378
|
-
return {
|
|
7379
|
-
success: true,
|
|
7380
|
-
format: 'pmtiles',
|
|
7381
|
-
filename: `${region.name || region.id}.pmtiles`,
|
|
7382
|
-
blob,
|
|
7383
|
-
size: blob.size,
|
|
7384
|
-
statistics: {
|
|
7385
|
-
tilesExported: tiles.length,
|
|
7386
|
-
spritesExported: 0,
|
|
7387
|
-
fontsExported: 0,
|
|
7388
|
-
},
|
|
7389
|
-
};
|
|
7390
|
-
}
|
|
7391
|
-
catch (error) {
|
|
7392
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7393
|
-
throw new Error(`PMTiles export failed: ${errorMessage}`);
|
|
7394
|
-
}
|
|
7395
|
-
}
|
|
7396
|
-
/**
|
|
7397
|
-
* Export region as MBTiles format
|
|
7398
|
-
*/
|
|
7399
|
-
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7400
|
-
const onProgress = options.onProgress || (() => { });
|
|
7401
|
-
try {
|
|
8038
|
+
const buffer = await this.readFileAsArrayBuffer(importData.file);
|
|
8039
|
+
onProgress({ stage: 'importing', percentage: 40, message: 'Parsing MBTiles...' });
|
|
8040
|
+
const regionData = await this.parseMBTiles(buffer);
|
|
7402
8041
|
onProgress({
|
|
7403
|
-
stage: '
|
|
7404
|
-
percentage:
|
|
7405
|
-
message:
|
|
8042
|
+
stage: 'importing',
|
|
8043
|
+
percentage: 70,
|
|
8044
|
+
message: `Importing ${regionData.tiles?.length ?? 0} tiles...`,
|
|
7406
8045
|
});
|
|
7407
|
-
|
|
7408
|
-
// In a real implementation, you would use SQLite/SQL.js
|
|
7409
|
-
// to create a proper MBTiles SQLite database
|
|
7410
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7411
|
-
if (!region) {
|
|
7412
|
-
throw new Error(`Region ${regionId} not found`);
|
|
7413
|
-
}
|
|
7414
|
-
// Get tiles data
|
|
7415
|
-
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7416
|
-
// Create MBTiles structure (simplified as JSON for now)
|
|
7417
|
-
const mbtilesData = {
|
|
7418
|
-
metadata: {
|
|
7419
|
-
name: region.name,
|
|
7420
|
-
type: 'overlay',
|
|
7421
|
-
version: '1.0',
|
|
7422
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7423
|
-
format: options.format || 'pbf',
|
|
7424
|
-
bounds: region.bounds.flat().join(','),
|
|
7425
|
-
minzoom: region.minZoom,
|
|
7426
|
-
maxzoom: region.maxZoom,
|
|
7427
|
-
...options.metadata,
|
|
7428
|
-
},
|
|
7429
|
-
tiles: tiles.map(tile => ({
|
|
7430
|
-
zoom_level: tile.z,
|
|
7431
|
-
tile_column: tile.x,
|
|
7432
|
-
tile_row: tile.y,
|
|
7433
|
-
tile_data: tile.data,
|
|
7434
|
-
})),
|
|
7435
|
-
};
|
|
7436
|
-
// Convert to binary format (simplified)
|
|
7437
|
-
const jsonString = JSON.stringify(mbtilesData);
|
|
7438
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
8046
|
+
const result = await this.importRegionData(regionData, importData);
|
|
7439
8047
|
onProgress({
|
|
7440
8048
|
stage: 'complete',
|
|
7441
8049
|
percentage: 100,
|
|
7442
|
-
message: '
|
|
8050
|
+
message: result.success ? 'Import complete!' : result.message,
|
|
7443
8051
|
});
|
|
7444
|
-
return {
|
|
7445
|
-
success: true,
|
|
7446
|
-
format: 'mbtiles',
|
|
7447
|
-
filename: `${region.name || region.id}.mbtiles`,
|
|
7448
|
-
blob,
|
|
7449
|
-
size: blob.size,
|
|
7450
|
-
statistics: {
|
|
7451
|
-
tilesExported: tiles.length,
|
|
7452
|
-
spritesExported: 0,
|
|
7453
|
-
fontsExported: 0,
|
|
7454
|
-
},
|
|
7455
|
-
};
|
|
7456
|
-
}
|
|
7457
|
-
catch (error) {
|
|
7458
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7459
|
-
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7460
|
-
}
|
|
7461
|
-
}
|
|
7462
|
-
/**
|
|
7463
|
-
* Import region from file
|
|
7464
|
-
*/
|
|
7465
|
-
async importRegion(importData) {
|
|
7466
|
-
try {
|
|
7467
|
-
let regionData;
|
|
7468
|
-
switch (importData.format) {
|
|
7469
|
-
case 'json': {
|
|
7470
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7471
|
-
regionData = JSON.parse(textContent);
|
|
7472
|
-
break;
|
|
7473
|
-
}
|
|
7474
|
-
case 'pmtiles': {
|
|
7475
|
-
// PMTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7476
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7477
|
-
regionData = await this.parsePMTiles(textContent);
|
|
7478
|
-
break;
|
|
7479
|
-
}
|
|
7480
|
-
case 'mbtiles': {
|
|
7481
|
-
// MBTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7482
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7483
|
-
regionData = await this.parseMBTiles(textContent);
|
|
7484
|
-
break;
|
|
7485
|
-
}
|
|
7486
|
-
default:
|
|
7487
|
-
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7488
|
-
}
|
|
7489
|
-
// Import the region data
|
|
7490
|
-
const result = await this.importRegionData(regionData, importData);
|
|
7491
8052
|
return result;
|
|
7492
8053
|
}
|
|
7493
8054
|
catch (error) {
|
|
@@ -7611,151 +8172,113 @@
|
|
|
7611
8172
|
}
|
|
7612
8173
|
}
|
|
7613
8174
|
/**
|
|
7614
|
-
*
|
|
7615
|
-
*/
|
|
7616
|
-
async exportSprites(_regionId) {
|
|
7617
|
-
const db = await this.db;
|
|
7618
|
-
const transaction = db.transaction(['sprites'], 'readonly');
|
|
7619
|
-
const store = transaction.objectStore('sprites');
|
|
7620
|
-
const sprites = [];
|
|
7621
|
-
try {
|
|
7622
|
-
let cursor = await store.openCursor();
|
|
7623
|
-
while (cursor) {
|
|
7624
|
-
const sprite = cursor.value;
|
|
7625
|
-
// Include sprites that match the styleId, or all sprites if keys don't contain styleId
|
|
7626
|
-
// (sprite keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7627
|
-
sprites.push({
|
|
7628
|
-
url: sprite.url,
|
|
7629
|
-
data: sprite.data,
|
|
7630
|
-
type: sprite.url.endsWith('.json') ? 'json' : 'png',
|
|
7631
|
-
resolution: sprite.url.includes('@2x') ? '2x' : '1x',
|
|
7632
|
-
});
|
|
7633
|
-
cursor = await cursor.continue();
|
|
7634
|
-
}
|
|
7635
|
-
return sprites;
|
|
7636
|
-
}
|
|
7637
|
-
catch (error) {
|
|
7638
|
-
serviceLogger.error('Error exporting sprites:', error);
|
|
7639
|
-
return [];
|
|
7640
|
-
}
|
|
7641
|
-
}
|
|
7642
|
-
/**
|
|
7643
|
-
* Export fonts data
|
|
8175
|
+
* Read file content as ArrayBuffer (for the binary MBTiles file).
|
|
7644
8176
|
*/
|
|
7645
|
-
async
|
|
7646
|
-
const db = await this.db;
|
|
7647
|
-
const transaction = db.transaction(['fonts'], 'readonly');
|
|
7648
|
-
const store = transaction.objectStore('fonts');
|
|
7649
|
-
const fonts = [];
|
|
7650
|
-
try {
|
|
7651
|
-
let cursor = await store.openCursor();
|
|
7652
|
-
while (cursor) {
|
|
7653
|
-
const font = cursor.value;
|
|
7654
|
-
// Include fonts that match the styleId, or all fonts if keys don't contain styleId
|
|
7655
|
-
// (font keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7656
|
-
fonts.push({
|
|
7657
|
-
fontStack: font.key, // Use key as fontstack identifier
|
|
7658
|
-
range: '0-255', // Default range since FontEntry doesn't store this
|
|
7659
|
-
data: font.data,
|
|
7660
|
-
});
|
|
7661
|
-
cursor = await cursor.continue();
|
|
7662
|
-
}
|
|
7663
|
-
return fonts;
|
|
7664
|
-
}
|
|
7665
|
-
catch (error) {
|
|
7666
|
-
serviceLogger.error('Error exporting fonts:', error);
|
|
7667
|
-
return [];
|
|
7668
|
-
}
|
|
7669
|
-
}
|
|
7670
|
-
/**
|
|
7671
|
-
* Read file content as text (for JSON files)
|
|
7672
|
-
*/
|
|
7673
|
-
async readFileAsText(file) {
|
|
8177
|
+
async readFileAsArrayBuffer(file) {
|
|
7674
8178
|
return new Promise((resolve, reject) => {
|
|
7675
8179
|
const reader = new FileReader();
|
|
7676
8180
|
reader.onload = () => resolve(reader.result);
|
|
7677
8181
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
7678
|
-
reader.
|
|
8182
|
+
reader.readAsArrayBuffer(file);
|
|
7679
8183
|
});
|
|
7680
8184
|
}
|
|
7681
8185
|
/**
|
|
7682
|
-
* Parse
|
|
8186
|
+
* Parse a real binary MBTiles (SQLite) file into our import-data shape.
|
|
8187
|
+
* Un-flips the TMS tile_row back to XYZ y.
|
|
7683
8188
|
*/
|
|
7684
|
-
async
|
|
7685
|
-
|
|
7686
|
-
//
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
8189
|
+
async parseMBTiles(buffer) {
|
|
8190
|
+
const bytes = new Uint8Array(buffer);
|
|
8191
|
+
// SQLite header: "SQLite format 3\0" (16 bytes). Validate up front so
|
|
8192
|
+
// non-MBTiles files (e.g. a JSON renamed to .mbtiles) surface a clear
|
|
8193
|
+
// error instead of the opaque "file is not a database" from sql.js.
|
|
8194
|
+
if (bytes.byteLength < 16) {
|
|
8195
|
+
throw new Error('Not a valid MBTiles file: file is too small');
|
|
8196
|
+
}
|
|
8197
|
+
const magic = String.fromCharCode(...bytes.slice(0, 15));
|
|
8198
|
+
if (magic !== 'SQLite format 3') {
|
|
8199
|
+
throw new Error('Not a valid MBTiles file: missing SQLite header');
|
|
8200
|
+
}
|
|
8201
|
+
const SQL = await getSqlJs();
|
|
8202
|
+
const db = new SQL.Database(bytes);
|
|
8203
|
+
try {
|
|
8204
|
+
const tablesResult = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('metadata', 'tiles')");
|
|
8205
|
+
const tableNames = (tablesResult[0]?.values || []).map(r => r[0]);
|
|
8206
|
+
if (!tableNames.includes('metadata') || !tableNames.includes('tiles')) {
|
|
8207
|
+
throw new Error('Not a valid MBTiles file: missing required metadata/tiles tables');
|
|
8208
|
+
}
|
|
8209
|
+
const metadata = {};
|
|
8210
|
+
const metaStmt = db.prepare('SELECT name, value FROM metadata');
|
|
8211
|
+
try {
|
|
8212
|
+
while (metaStmt.step()) {
|
|
8213
|
+
const row = metaStmt.get();
|
|
8214
|
+
metadata[row[0]] = row[1];
|
|
8215
|
+
}
|
|
8216
|
+
}
|
|
8217
|
+
finally {
|
|
8218
|
+
metaStmt.free();
|
|
8219
|
+
}
|
|
8220
|
+
const rawBounds = metadata.bounds ? metadata.bounds.split(',').map(Number) : [0, 0, 0, 0];
|
|
8221
|
+
const bounds = [
|
|
8222
|
+
isFinite(rawBounds[0]) ? rawBounds[0] : 0,
|
|
8223
|
+
isFinite(rawBounds[1]) ? rawBounds[1] : 0,
|
|
8224
|
+
isFinite(rawBounds[2]) ? rawBounds[2] : 0,
|
|
8225
|
+
isFinite(rawBounds[3]) ? rawBounds[3] : 0,
|
|
8226
|
+
];
|
|
8227
|
+
const format = (metadata.format || 'pbf');
|
|
8228
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
8229
|
+
const tiles = [];
|
|
8230
|
+
const tilesStmt = db.prepare('SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles');
|
|
8231
|
+
try {
|
|
8232
|
+
while (tilesStmt.step()) {
|
|
8233
|
+
const row = tilesStmt.get();
|
|
8234
|
+
const [z, x, tmsRow, data] = row;
|
|
8235
|
+
// Sliced copy so the buffer is detached from sql.js's heap.
|
|
8236
|
+
const copy = new Uint8Array(data.byteLength);
|
|
8237
|
+
copy.set(data);
|
|
8238
|
+
// Our IndexedDB stores vector tiles decompressed (tileService
|
|
8239
|
+
// inflates on download). MBTiles vector tiles are gzipped by
|
|
8240
|
+
// convention — un-gzip on the way in so the stored tile matches
|
|
8241
|
+
// what the fetch handler expects to serve.
|
|
8242
|
+
const storedBytes = isVector && hasGzipMagic(copy) ? await gunzipBytes(copy) : copy;
|
|
8243
|
+
tiles.push({
|
|
8244
|
+
z,
|
|
8245
|
+
x,
|
|
8246
|
+
y: flipY(tmsRow, z),
|
|
8247
|
+
data: storedBytes.buffer,
|
|
8248
|
+
format,
|
|
8249
|
+
sourceId: 'imported',
|
|
8250
|
+
});
|
|
8251
|
+
}
|
|
8252
|
+
}
|
|
8253
|
+
finally {
|
|
8254
|
+
tilesStmt.free();
|
|
8255
|
+
}
|
|
8256
|
+
const minZoom = metadata.minzoom !== undefined ? Number(metadata.minzoom) : 0;
|
|
8257
|
+
const maxZoom = metadata.maxzoom !== undefined ? Number(metadata.maxzoom) : 14;
|
|
8258
|
+
return {
|
|
8259
|
+
metadata: {
|
|
8260
|
+
id: metadata.name || 'imported-region',
|
|
8261
|
+
name: metadata.name || 'Imported Region',
|
|
8262
|
+
description: metadata.description,
|
|
8263
|
+
bounds: [
|
|
8264
|
+
[bounds[0], bounds[1]],
|
|
8265
|
+
[bounds[2], bounds[3]],
|
|
8266
|
+
],
|
|
8267
|
+
minZoom,
|
|
8268
|
+
maxZoom,
|
|
8269
|
+
styleUrl: '',
|
|
8270
|
+
createdAt: Date.now(),
|
|
8271
|
+
exportedAt: Date.now(),
|
|
8272
|
+
version: '1.0.0',
|
|
8273
|
+
format: 'mbtiles',
|
|
8274
|
+
},
|
|
8275
|
+
style: {},
|
|
8276
|
+
tiles,
|
|
8277
|
+
};
|
|
8278
|
+
}
|
|
8279
|
+
finally {
|
|
8280
|
+
db.close();
|
|
8281
|
+
}
|
|
7759
8282
|
}
|
|
7760
8283
|
/**
|
|
7761
8284
|
* Import region data to database
|
|
@@ -7820,16 +8343,15 @@
|
|
|
7820
8343
|
});
|
|
7821
8344
|
}
|
|
7822
8345
|
}
|
|
7823
|
-
// Import sprites and fonts similarly...
|
|
7824
8346
|
return {
|
|
7825
8347
|
success: true,
|
|
7826
8348
|
regionId,
|
|
7827
8349
|
message: 'Region imported successfully',
|
|
7828
8350
|
statistics: {
|
|
7829
8351
|
tilesImported: regionData.tiles?.length || 0,
|
|
7830
|
-
spritesImported:
|
|
7831
|
-
fontsImported:
|
|
7832
|
-
totalSize: 0,
|
|
8352
|
+
spritesImported: 0,
|
|
8353
|
+
fontsImported: 0,
|
|
8354
|
+
totalSize: 0,
|
|
7833
8355
|
},
|
|
7834
8356
|
};
|
|
7835
8357
|
}
|
|
@@ -7914,6 +8436,10 @@
|
|
|
7914
8436
|
loadGlyphsForStyle: (...args) => services.resourceService.loadGlyphsForStyle(...args),
|
|
7915
8437
|
cleanupOldGlyphs: (...args) => services.resourceService.cleanupOldGlyphs(...args),
|
|
7916
8438
|
verifyAndRepairGlyphs: (...args) => services.resourceService.verifyAndRepairGlyphs(...args),
|
|
8439
|
+
downloadModelsWithOptions: (...args) => services.resourceService.downloadModelsWithOptions(...args),
|
|
8440
|
+
getModelStats: (...args) => services.resourceService.getModelStats(...args),
|
|
8441
|
+
cleanupOldModels: (...args) => services.resourceService.cleanupOldModels(...args),
|
|
8442
|
+
verifyAndRepairModels: (...args) => services.resourceService.verifyAndRepairModels(...args),
|
|
7917
8443
|
});
|
|
7918
8444
|
|
|
7919
8445
|
const createAnalyticsManagement = (services, deps) => ({
|
|
@@ -8054,8 +8580,6 @@
|
|
|
8054
8580
|
};
|
|
8055
8581
|
|
|
8056
8582
|
const createImportExportManagement = (services) => ({
|
|
8057
|
-
exportRegionAsJSON: async (regionId, options = {}) => services.importExportService.exportRegionAsJSON(regionId, options),
|
|
8058
|
-
exportRegionAsPMTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsPMTiles(regionId, options),
|
|
8059
8583
|
exportRegionAsMBTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsMBTiles(regionId, options),
|
|
8060
8584
|
importRegion: async (importData) => services.importExportService.importRegion(importData),
|
|
8061
8585
|
downloadExportedRegion: (exportResult) => {
|
|
@@ -8411,10 +8935,6 @@
|
|
|
8411
8935
|
'styleSelection.title': 'Select Offline Style',
|
|
8412
8936
|
'styleSelection.message': 'Choose which offline style to load:',
|
|
8413
8937
|
'styleSelection.sources': 'sources',
|
|
8414
|
-
// Import/Export
|
|
8415
|
-
'importExport.title': 'Import/Export',
|
|
8416
|
-
'importExport.export': 'Export',
|
|
8417
|
-
'importExport.import': 'Import',
|
|
8418
8938
|
// Errors
|
|
8419
8939
|
'error.loadingContent': 'Error loading content',
|
|
8420
8940
|
'error.tryAgain': 'Please try again',
|
|
@@ -8439,41 +8959,30 @@
|
|
|
8439
8959
|
'regionDetails.bounds': 'Bounds',
|
|
8440
8960
|
'regionDetails.zoomRange': 'Zoom Range',
|
|
8441
8961
|
'regionDetails.created': 'Created',
|
|
8442
|
-
//
|
|
8443
|
-
'
|
|
8444
|
-
'
|
|
8445
|
-
'
|
|
8446
|
-
'
|
|
8447
|
-
'
|
|
8448
|
-
'
|
|
8449
|
-
'
|
|
8450
|
-
'
|
|
8451
|
-
'
|
|
8452
|
-
'
|
|
8453
|
-
'
|
|
8454
|
-
'
|
|
8455
|
-
'
|
|
8456
|
-
'
|
|
8457
|
-
'
|
|
8458
|
-
'
|
|
8459
|
-
'
|
|
8460
|
-
'
|
|
8461
|
-
'
|
|
8462
|
-
'
|
|
8463
|
-
'
|
|
8464
|
-
'
|
|
8465
|
-
'
|
|
8466
|
-
'importExport.fileFormatsHint': 'Supports JSON, PMTiles, and MBTiles formats',
|
|
8467
|
-
'importExport.newRegionName': 'New Region Name (Optional)',
|
|
8468
|
-
'importExport.newRegionNamePlaceholder': 'Leave empty to use original name',
|
|
8469
|
-
'importExport.overwriteIfExists': 'Overwrite if region exists',
|
|
8470
|
-
'importExport.preparingImport': 'Preparing import...',
|
|
8471
|
-
'importExport.importComplete': 'Import complete!',
|
|
8472
|
-
'importExport.importFailed': 'Import failed. Please try again.',
|
|
8473
|
-
'importExport.formatGuide': 'Format Guide',
|
|
8474
|
-
'importExport.jsonDesc': 'Complete data, human-readable, best for development',
|
|
8475
|
-
'importExport.pmtilesDesc': 'Web-optimized, efficient serving, cloud-friendly',
|
|
8476
|
-
'importExport.mbtilesDesc': 'Industry standard, SQLite-based, cross-platform',
|
|
8962
|
+
// MBTiles Modal
|
|
8963
|
+
'mbtiles.title': 'MBTiles Import / Export',
|
|
8964
|
+
'mbtiles.regionInfo': 'Region Information',
|
|
8965
|
+
'mbtiles.id': 'ID',
|
|
8966
|
+
'mbtiles.name': 'Name',
|
|
8967
|
+
'mbtiles.unnamed': 'Unnamed',
|
|
8968
|
+
'mbtiles.zoom': 'Zoom',
|
|
8969
|
+
'mbtiles.created': 'Created',
|
|
8970
|
+
'mbtiles.exportTitle': 'Export as MBTiles',
|
|
8971
|
+
'mbtiles.exportHint': 'Package the tiles in this region into a standard SQLite MBTiles archive that opens in QGIS, tippecanoe, and other tools.',
|
|
8972
|
+
'mbtiles.exportButton': 'Download .mbtiles',
|
|
8973
|
+
'mbtiles.preparingExport': 'Preparing export...',
|
|
8974
|
+
'mbtiles.exportComplete': 'Export complete!',
|
|
8975
|
+
'mbtiles.exportFailed': 'Export failed. Please try again.',
|
|
8976
|
+
'mbtiles.importTitle': 'Import from MBTiles',
|
|
8977
|
+
'mbtiles.selectFile': 'Select an .mbtiles file',
|
|
8978
|
+
'mbtiles.fileHint': 'Only SQLite-format .mbtiles files are supported.',
|
|
8979
|
+
'mbtiles.newRegionName': 'New Region Name (optional)',
|
|
8980
|
+
'mbtiles.newRegionNamePlaceholder': 'Leave empty to use the name from the file',
|
|
8981
|
+
'mbtiles.overwriteIfExists': 'Overwrite if a region with the same id exists',
|
|
8982
|
+
'mbtiles.importButton': 'Import .mbtiles',
|
|
8983
|
+
'mbtiles.preparingImport': 'Preparing import...',
|
|
8984
|
+
'mbtiles.importComplete': 'Import complete!',
|
|
8985
|
+
'mbtiles.importFailed': 'Import failed. Please try again.',
|
|
8477
8986
|
// Active Downloads
|
|
8478
8987
|
'download.activeCount': 'Active Downloads ({{count}})',
|
|
8479
8988
|
// Panel Manager additional strings
|
|
@@ -8634,10 +9143,6 @@
|
|
|
8634
9143
|
'styleSelection.title': 'اختر نمط غير متصل',
|
|
8635
9144
|
'styleSelection.message': 'اختر النمط غير المتصل الذي تريد تحميله:',
|
|
8636
9145
|
'styleSelection.sources': 'مصادر',
|
|
8637
|
-
// Import/Export - استيراد/تصدير
|
|
8638
|
-
'importExport.title': 'استيراد/تصدير',
|
|
8639
|
-
'importExport.export': 'تصدير',
|
|
8640
|
-
'importExport.import': 'استيراد',
|
|
8641
9146
|
// Errors - الأخطاء
|
|
8642
9147
|
'error.loadingContent': 'خطأ في تحميل المحتوى',
|
|
8643
9148
|
'error.tryAgain': 'يرجى المحاولة مرة أخرى',
|
|
@@ -8662,41 +9167,30 @@
|
|
|
8662
9167
|
'regionDetails.bounds': 'الحدود',
|
|
8663
9168
|
'regionDetails.zoomRange': 'نطاق التكبير',
|
|
8664
9169
|
'regionDetails.created': 'تاريخ الإنشاء',
|
|
8665
|
-
//
|
|
8666
|
-
'
|
|
8667
|
-
'
|
|
8668
|
-
'
|
|
8669
|
-
'
|
|
8670
|
-
'
|
|
8671
|
-
'
|
|
8672
|
-
'
|
|
8673
|
-
'
|
|
8674
|
-
'
|
|
8675
|
-
'
|
|
8676
|
-
'
|
|
8677
|
-
'
|
|
8678
|
-
'
|
|
8679
|
-
'
|
|
8680
|
-
'
|
|
8681
|
-
'
|
|
8682
|
-
'
|
|
8683
|
-
'
|
|
8684
|
-
'
|
|
8685
|
-
'
|
|
8686
|
-
'
|
|
8687
|
-
'
|
|
8688
|
-
'
|
|
8689
|
-
'importExport.fileFormatsHint': 'يدعم تنسيقات JSON و PMTiles و MBTiles',
|
|
8690
|
-
'importExport.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
8691
|
-
'importExport.newRegionNamePlaceholder': 'اتركه فارغاً لاستخدام الاسم الأصلي',
|
|
8692
|
-
'importExport.overwriteIfExists': 'الكتابة فوق المنطقة الموجودة',
|
|
8693
|
-
'importExport.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
8694
|
-
'importExport.importComplete': 'اكتمل الاستيراد!',
|
|
8695
|
-
'importExport.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
8696
|
-
'importExport.formatGuide': 'دليل التنسيقات',
|
|
8697
|
-
'importExport.jsonDesc': 'بيانات كاملة، قابلة للقراءة، الأفضل للتطوير',
|
|
8698
|
-
'importExport.pmtilesDesc': 'محسن للويب، خدمة فعالة، متوافق مع السحابة',
|
|
8699
|
-
'importExport.mbtilesDesc': 'معيار الصناعة، قائم على SQLite، متعدد المنصات',
|
|
9170
|
+
// MBTiles Modal - نافذة MBTiles
|
|
9171
|
+
'mbtiles.title': 'استيراد / تصدير MBTiles',
|
|
9172
|
+
'mbtiles.regionInfo': 'معلومات المنطقة',
|
|
9173
|
+
'mbtiles.id': 'المعرف',
|
|
9174
|
+
'mbtiles.name': 'الاسم',
|
|
9175
|
+
'mbtiles.unnamed': 'بدون اسم',
|
|
9176
|
+
'mbtiles.zoom': 'التكبير',
|
|
9177
|
+
'mbtiles.created': 'تاريخ الإنشاء',
|
|
9178
|
+
'mbtiles.exportTitle': 'التصدير كـ MBTiles',
|
|
9179
|
+
'mbtiles.exportHint': 'احزم بلاطات هذه المنطقة داخل أرشيف MBTiles (SQLite) يمكن فتحه في QGIS و tippecanoe وأدوات أخرى.',
|
|
9180
|
+
'mbtiles.exportButton': 'تنزيل ملف mbtiles.',
|
|
9181
|
+
'mbtiles.preparingExport': 'جاري تجهيز التصدير...',
|
|
9182
|
+
'mbtiles.exportComplete': 'اكتمل التصدير!',
|
|
9183
|
+
'mbtiles.exportFailed': 'فشل التصدير. يرجى المحاولة مرة أخرى.',
|
|
9184
|
+
'mbtiles.importTitle': 'الاستيراد من MBTiles',
|
|
9185
|
+
'mbtiles.selectFile': 'اختر ملف mbtiles.',
|
|
9186
|
+
'mbtiles.fileHint': 'تُدعم ملفات mbtiles. بتنسيق SQLite فقط.',
|
|
9187
|
+
'mbtiles.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
9188
|
+
'mbtiles.newRegionNamePlaceholder': 'اتركه فارغًا لاستخدام الاسم من الملف',
|
|
9189
|
+
'mbtiles.overwriteIfExists': 'الكتابة فوق المنطقة إذا كان المعرف موجودًا',
|
|
9190
|
+
'mbtiles.importButton': 'استيراد ملف mbtiles.',
|
|
9191
|
+
'mbtiles.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
9192
|
+
'mbtiles.importComplete': 'اكتمل الاستيراد!',
|
|
9193
|
+
'mbtiles.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
8700
9194
|
// Active Downloads - التحميلات النشطة
|
|
8701
9195
|
'download.activeCount': 'التحميلات النشطة ({{count}})',
|
|
8702
9196
|
// Panel Manager additional strings - سلاسل إضافية للوحة
|
|
@@ -9635,49 +10129,42 @@
|
|
|
9635
10129
|
}
|
|
9636
10130
|
|
|
9637
10131
|
/**
|
|
9638
|
-
* Import/Export Modal
|
|
9639
|
-
*
|
|
9640
|
-
*
|
|
10132
|
+
* MBTiles Import/Export Modal
|
|
10133
|
+
*
|
|
10134
|
+
* Focused modal for exchanging regions as binary SQLite MBTiles archives.
|
|
10135
|
+
* Replaces the previous multi-format import/export modal.
|
|
9641
10136
|
*/
|
|
9642
|
-
const modalLogger = logger.scope('
|
|
9643
|
-
class
|
|
10137
|
+
const modalLogger = logger.scope('MBTilesModal');
|
|
10138
|
+
class MBTilesModal {
|
|
9644
10139
|
modal;
|
|
9645
10140
|
options;
|
|
9646
10141
|
isExporting = false;
|
|
9647
10142
|
isImporting = false;
|
|
9648
|
-
// Form elements
|
|
9649
|
-
exportFormatSelect;
|
|
9650
|
-
includeStyleCheckbox;
|
|
9651
|
-
includeTilesCheckbox;
|
|
9652
|
-
includeSpritesCheckbox;
|
|
9653
|
-
includeFontsCheckbox;
|
|
9654
10143
|
exportProgressBar;
|
|
9655
10144
|
exportProgressText;
|
|
10145
|
+
exportProgressContainer;
|
|
9656
10146
|
exportButton;
|
|
9657
10147
|
importFileInput;
|
|
9658
10148
|
importNameInput;
|
|
9659
10149
|
importOverwriteCheckbox;
|
|
9660
10150
|
importProgressBar;
|
|
9661
10151
|
importProgressText;
|
|
10152
|
+
importProgressContainer;
|
|
9662
10153
|
importButton;
|
|
9663
10154
|
constructor(options) {
|
|
9664
10155
|
this.options = options;
|
|
9665
10156
|
}
|
|
9666
10157
|
show() {
|
|
9667
10158
|
const modalConfig = {
|
|
9668
|
-
title: t('
|
|
10159
|
+
title: t('mbtiles.title'),
|
|
9669
10160
|
subtitle: this.options.region.name || this.options.region.id,
|
|
9670
10161
|
size: 'md',
|
|
9671
10162
|
closable: true,
|
|
9672
10163
|
onClose: () => this.hide(),
|
|
9673
10164
|
};
|
|
9674
10165
|
this.modal = new Modal(modalConfig);
|
|
9675
|
-
|
|
9676
|
-
|
|
9677
|
-
this.modal.setContent(content);
|
|
9678
|
-
// Create footer with close button
|
|
9679
|
-
const footer = this.createFooter();
|
|
9680
|
-
this.modal.setFooter(footer);
|
|
10166
|
+
this.modal.setContent(this.createContent());
|
|
10167
|
+
this.modal.setFooter(this.createFooter());
|
|
9681
10168
|
this.modal.show();
|
|
9682
10169
|
this.attachEventListeners();
|
|
9683
10170
|
return this.modal.getElement();
|
|
@@ -9686,217 +10173,96 @@
|
|
|
9686
10173
|
this.modal?.hide();
|
|
9687
10174
|
this.options.onClose();
|
|
9688
10175
|
}
|
|
10176
|
+
destroy() {
|
|
10177
|
+
this.modal?.destroy();
|
|
10178
|
+
}
|
|
9689
10179
|
createContent() {
|
|
9690
10180
|
const content = document.createElement('div');
|
|
9691
|
-
content.className = 'flex flex-col gap-6';
|
|
10181
|
+
content.className = 'flex flex-col gap-6 py-2';
|
|
9692
10182
|
if (i18n.isRTL()) {
|
|
9693
10183
|
content.setAttribute('dir', 'rtl');
|
|
9694
10184
|
}
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
content.appendChild(
|
|
9698
|
-
// Export/Import Grid
|
|
9699
|
-
const gridContainer = document.createElement('div');
|
|
9700
|
-
gridContainer.className = 'grid grid-cols-1 gap-6';
|
|
9701
|
-
// Export Section
|
|
9702
|
-
const exportSection = this.createExportSection();
|
|
9703
|
-
gridContainer.appendChild(exportSection);
|
|
9704
|
-
// Import Section
|
|
9705
|
-
const importSection = this.createImportSection();
|
|
9706
|
-
gridContainer.appendChild(importSection);
|
|
9707
|
-
content.appendChild(gridContainer);
|
|
9708
|
-
// Format Guide
|
|
9709
|
-
const formatGuide = this.createFormatGuide();
|
|
9710
|
-
content.appendChild(formatGuide);
|
|
10185
|
+
content.appendChild(this.createRegionInfoLine());
|
|
10186
|
+
content.appendChild(this.createExportSection());
|
|
10187
|
+
content.appendChild(this.createImportSection());
|
|
9711
10188
|
return content;
|
|
9712
10189
|
}
|
|
9713
|
-
|
|
9714
|
-
const
|
|
9715
|
-
|
|
9716
|
-
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
|
|
9722
|
-
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
</div>
|
|
9726
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9727
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.name')}</span>
|
|
9728
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${escapeHtml$1(this.options.region.name || t('importExport.unnamed'))}</div>
|
|
9729
|
-
</div>
|
|
9730
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9731
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.zoom')}</span>
|
|
9732
|
-
<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>
|
|
9733
|
-
</div>
|
|
9734
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
9735
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.created')}</span>
|
|
9736
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${new Date(this.options.region.created).toLocaleDateString()}</div>
|
|
9737
|
-
</div>
|
|
9738
|
-
</div>
|
|
10190
|
+
createRegionInfoLine() {
|
|
10191
|
+
const { region } = this.options;
|
|
10192
|
+
const line = document.createElement('div');
|
|
10193
|
+
line.className =
|
|
10194
|
+
'flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400';
|
|
10195
|
+
line.innerHTML = `
|
|
10196
|
+
<span class="flex items-center gap-1">
|
|
10197
|
+
${icons.mapPin({ size: 12, color: 'currentColor' })}
|
|
10198
|
+
<span class="font-mono">${escapeHtml$1(region.id)}</span>
|
|
10199
|
+
</span>
|
|
10200
|
+
<span>Z${escapeHtml$1(region.minZoom)}-${escapeHtml$1(region.maxZoom)}</span>
|
|
10201
|
+
<span>${new Date(region.created).toLocaleDateString()}</span>
|
|
9739
10202
|
`;
|
|
9740
|
-
return
|
|
10203
|
+
return line;
|
|
9741
10204
|
}
|
|
9742
10205
|
createExportSection() {
|
|
9743
|
-
const section =
|
|
9744
|
-
|
|
9745
|
-
|
|
9746
|
-
|
|
9747
|
-
|
|
9748
|
-
|
|
9749
|
-
|
|
9750
|
-
|
|
9751
|
-
|
|
9752
|
-
|
|
9753
|
-
|
|
9754
|
-
|
|
9755
|
-
|
|
9756
|
-
|
|
9757
|
-
|
|
9758
|
-
`;
|
|
9759
|
-
section.appendChild(header);
|
|
9760
|
-
const formContainer = document.createElement('div');
|
|
9761
|
-
formContainer.className = 'space-y-5';
|
|
9762
|
-
// Format Selection
|
|
9763
|
-
const formatGroup = document.createElement('div');
|
|
9764
|
-
const formatLabel = document.createElement('label');
|
|
9765
|
-
formatLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9766
|
-
formatLabel.textContent = t('importExport.exportFormat');
|
|
9767
|
-
this.exportFormatSelect = document.createElement('select');
|
|
9768
|
-
this.exportFormatSelect.className =
|
|
9769
|
-
'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';
|
|
9770
|
-
this.exportFormatSelect.innerHTML = `
|
|
9771
|
-
<option value="json">${t('importExport.formatJson')}</option>
|
|
9772
|
-
<option value="pmtiles">${t('importExport.formatPmtiles')}</option>
|
|
9773
|
-
<option value="mbtiles">${t('importExport.formatMbtiles')}</option>
|
|
9774
|
-
`;
|
|
9775
|
-
const formatHint = document.createElement('p');
|
|
9776
|
-
formatHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
9777
|
-
formatHint.textContent = t('importExport.formatHint');
|
|
9778
|
-
formatGroup.appendChild(formatLabel);
|
|
9779
|
-
formatGroup.appendChild(this.exportFormatSelect);
|
|
9780
|
-
formatGroup.appendChild(formatHint);
|
|
9781
|
-
formContainer.appendChild(formatGroup);
|
|
9782
|
-
// Export Options
|
|
9783
|
-
const optionsGroup = document.createElement('div');
|
|
9784
|
-
const optionsLabel = document.createElement('label');
|
|
9785
|
-
optionsLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3';
|
|
9786
|
-
optionsLabel.textContent = t('importExport.includeComponents');
|
|
9787
|
-
const checkboxContainer = document.createElement('div');
|
|
9788
|
-
checkboxContainer.className = 'grid grid-cols-1 sm:grid-cols-2 gap-3';
|
|
9789
|
-
const createCheckbox = (text, checked = true) => {
|
|
9790
|
-
const label = document.createElement('label');
|
|
9791
|
-
label.className =
|
|
9792
|
-
'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';
|
|
9793
|
-
const input = document.createElement('input');
|
|
9794
|
-
input.type = 'checkbox';
|
|
9795
|
-
input.checked = checked;
|
|
9796
|
-
input.className =
|
|
9797
|
-
'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';
|
|
9798
|
-
const span = document.createElement('span');
|
|
9799
|
-
span.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
9800
|
-
span.textContent = text;
|
|
9801
|
-
label.appendChild(input);
|
|
9802
|
-
label.appendChild(span);
|
|
9803
|
-
return { label, input };
|
|
9804
|
-
};
|
|
9805
|
-
const styleCheck = createCheckbox(t('importExport.styleConfig'));
|
|
9806
|
-
this.includeStyleCheckbox = styleCheck.input;
|
|
9807
|
-
checkboxContainer.appendChild(styleCheck.label);
|
|
9808
|
-
const tilesCheck = createCheckbox(t('importExport.mapTiles'));
|
|
9809
|
-
this.includeTilesCheckbox = tilesCheck.input;
|
|
9810
|
-
checkboxContainer.appendChild(tilesCheck.label);
|
|
9811
|
-
const spritesCheck = createCheckbox(t('importExport.spritesIcons'));
|
|
9812
|
-
this.includeSpritesCheckbox = spritesCheck.input;
|
|
9813
|
-
checkboxContainer.appendChild(spritesCheck.label);
|
|
9814
|
-
const fontsCheck = createCheckbox(t('importExport.fontsGlyphs'));
|
|
9815
|
-
this.includeFontsCheckbox = fontsCheck.input;
|
|
9816
|
-
checkboxContainer.appendChild(fontsCheck.label);
|
|
9817
|
-
optionsGroup.appendChild(optionsLabel);
|
|
9818
|
-
optionsGroup.appendChild(checkboxContainer);
|
|
9819
|
-
formContainer.appendChild(optionsGroup);
|
|
9820
|
-
// Export Progress (hidden by default)
|
|
9821
|
-
const progressContainer = document.createElement('div');
|
|
9822
|
-
progressContainer.className = 'hidden';
|
|
9823
|
-
const progressBarContainer = document.createElement('div');
|
|
9824
|
-
progressBarContainer.className =
|
|
9825
|
-
'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
9826
|
-
this.exportProgressBar = document.createElement('div');
|
|
9827
|
-
this.exportProgressBar.className = 'bg-blue-600 h-2 rounded-full transition-all duration-300';
|
|
9828
|
-
this.exportProgressBar.style.width = '0%';
|
|
9829
|
-
progressBarContainer.appendChild(this.exportProgressBar);
|
|
9830
|
-
this.exportProgressText = document.createElement('p');
|
|
9831
|
-
this.exportProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
9832
|
-
this.exportProgressText.textContent = t('importExport.preparingExport');
|
|
9833
|
-
progressContainer.appendChild(progressBarContainer);
|
|
9834
|
-
progressContainer.appendChild(this.exportProgressText);
|
|
9835
|
-
formContainer.appendChild(progressContainer);
|
|
9836
|
-
// Export Button
|
|
9837
|
-
const exportButton = new Button({
|
|
9838
|
-
text: t('importExport.exportRegion'),
|
|
10206
|
+
const section = this.createSection(t('mbtiles.exportTitle'), 'blue', icons.download({ size: 20, color: 'currentColor' }));
|
|
10207
|
+
const form = document.createElement('div');
|
|
10208
|
+
form.className = 'space-y-5';
|
|
10209
|
+
const hint = document.createElement('p');
|
|
10210
|
+
hint.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10211
|
+
hint.textContent = t('mbtiles.exportHint');
|
|
10212
|
+
form.appendChild(hint);
|
|
10213
|
+
// Progress (hidden by default)
|
|
10214
|
+
const progress = this.createProgressBlock('blue', t('mbtiles.preparingExport'));
|
|
10215
|
+
this.exportProgressContainer = progress.container;
|
|
10216
|
+
this.exportProgressBar = progress.bar;
|
|
10217
|
+
this.exportProgressText = progress.text;
|
|
10218
|
+
form.appendChild(progress.container);
|
|
10219
|
+
const exportBtn = new Button({
|
|
10220
|
+
text: t('mbtiles.exportButton'),
|
|
9839
10221
|
variant: 'primary',
|
|
9840
10222
|
icon: icons.download({ size: 16, color: 'white' }),
|
|
9841
|
-
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
10223
|
+
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
9842
10224
|
onClick: () => this.handleExport(),
|
|
9843
10225
|
});
|
|
9844
|
-
this.exportButton =
|
|
9845
|
-
|
|
9846
|
-
section.appendChild(
|
|
10226
|
+
this.exportButton = exportBtn.getElement();
|
|
10227
|
+
form.appendChild(this.exportButton);
|
|
10228
|
+
section.appendChild(form);
|
|
9847
10229
|
return section;
|
|
9848
10230
|
}
|
|
9849
10231
|
createImportSection() {
|
|
9850
|
-
const section =
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
//
|
|
9854
|
-
const accent = document.createElement('div');
|
|
9855
|
-
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-green-500 opacity-50`;
|
|
9856
|
-
section.appendChild(accent);
|
|
9857
|
-
const header = document.createElement('h3');
|
|
9858
|
-
header.className =
|
|
9859
|
-
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
9860
|
-
header.innerHTML = `
|
|
9861
|
-
<div class="p-2 bg-green-500/10 rounded-lg text-green-600 dark:text-green-400">
|
|
9862
|
-
${icons.upload({ size: 20, color: 'currentColor' })}
|
|
9863
|
-
</div>
|
|
9864
|
-
${t('importExport.importRegion')}
|
|
9865
|
-
`;
|
|
9866
|
-
section.appendChild(header);
|
|
9867
|
-
const formContainer = document.createElement('div');
|
|
9868
|
-
formContainer.className = 'space-y-5';
|
|
9869
|
-
// File Selection
|
|
10232
|
+
const section = this.createSection(t('mbtiles.importTitle'), 'green', icons.upload({ size: 20, color: 'currentColor' }));
|
|
10233
|
+
const form = document.createElement('div');
|
|
10234
|
+
form.className = 'space-y-5';
|
|
10235
|
+
// File input
|
|
9870
10236
|
const fileGroup = document.createElement('div');
|
|
9871
10237
|
const fileLabel = document.createElement('label');
|
|
9872
10238
|
fileLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9873
|
-
fileLabel.textContent = t('
|
|
10239
|
+
fileLabel.textContent = t('mbtiles.selectFile');
|
|
9874
10240
|
this.importFileInput = document.createElement('input');
|
|
9875
10241
|
this.importFileInput.type = 'file';
|
|
9876
|
-
this.importFileInput.accept = '.
|
|
10242
|
+
this.importFileInput.accept = '.mbtiles,application/vnd.sqlite3,application/x-sqlite3';
|
|
9877
10243
|
this.importFileInput.className =
|
|
9878
10244
|
'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';
|
|
9879
10245
|
const fileHint = document.createElement('p');
|
|
9880
10246
|
fileHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
9881
|
-
fileHint.textContent = t('
|
|
10247
|
+
fileHint.textContent = t('mbtiles.fileHint');
|
|
9882
10248
|
fileGroup.appendChild(fileLabel);
|
|
9883
10249
|
fileGroup.appendChild(this.importFileInput);
|
|
9884
10250
|
fileGroup.appendChild(fileHint);
|
|
9885
|
-
|
|
9886
|
-
// New
|
|
10251
|
+
form.appendChild(fileGroup);
|
|
10252
|
+
// New region name
|
|
9887
10253
|
const nameGroup = document.createElement('div');
|
|
9888
10254
|
const nameLabel = document.createElement('label');
|
|
9889
10255
|
nameLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
9890
|
-
nameLabel.textContent = t('
|
|
10256
|
+
nameLabel.textContent = t('mbtiles.newRegionName');
|
|
9891
10257
|
this.importNameInput = document.createElement('input');
|
|
9892
10258
|
this.importNameInput.type = 'text';
|
|
9893
|
-
this.importNameInput.placeholder = t('
|
|
10259
|
+
this.importNameInput.placeholder = t('mbtiles.newRegionNamePlaceholder');
|
|
9894
10260
|
this.importNameInput.className =
|
|
9895
10261
|
'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';
|
|
9896
10262
|
nameGroup.appendChild(nameLabel);
|
|
9897
10263
|
nameGroup.appendChild(this.importNameInput);
|
|
9898
|
-
|
|
9899
|
-
//
|
|
10264
|
+
form.appendChild(nameGroup);
|
|
10265
|
+
// Overwrite toggle
|
|
9900
10266
|
const overwriteLabel = document.createElement('label');
|
|
9901
10267
|
overwriteLabel.className =
|
|
9902
10268
|
'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';
|
|
@@ -9906,79 +10272,85 @@
|
|
|
9906
10272
|
'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';
|
|
9907
10273
|
const overwriteSpan = document.createElement('span');
|
|
9908
10274
|
overwriteSpan.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
9909
|
-
overwriteSpan.textContent = t('
|
|
10275
|
+
overwriteSpan.textContent = t('mbtiles.overwriteIfExists');
|
|
9910
10276
|
overwriteLabel.appendChild(this.importOverwriteCheckbox);
|
|
9911
10277
|
overwriteLabel.appendChild(overwriteSpan);
|
|
9912
|
-
|
|
9913
|
-
//
|
|
9914
|
-
const
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
this.importProgressText = document.createElement('p');
|
|
9924
|
-
this.importProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
9925
|
-
this.importProgressText.textContent = t('importExport.preparingImport');
|
|
9926
|
-
progressContainer.appendChild(progressBarContainer);
|
|
9927
|
-
progressContainer.appendChild(this.importProgressText);
|
|
9928
|
-
formContainer.appendChild(progressContainer);
|
|
9929
|
-
// Import Button
|
|
9930
|
-
const importButton = new Button({
|
|
9931
|
-
text: t('importExport.importRegion'),
|
|
9932
|
-
variant: 'success', // Assuming 'success' variant exists in Button component, if not might need style adjustment. Assuming it works based on previous code.
|
|
10278
|
+
form.appendChild(overwriteLabel);
|
|
10279
|
+
// Progress
|
|
10280
|
+
const progress = this.createProgressBlock('green', t('mbtiles.preparingImport'));
|
|
10281
|
+
this.importProgressContainer = progress.container;
|
|
10282
|
+
this.importProgressBar = progress.bar;
|
|
10283
|
+
this.importProgressText = progress.text;
|
|
10284
|
+
form.appendChild(progress.container);
|
|
10285
|
+
// Import button (disabled until a file is selected)
|
|
10286
|
+
const importBtn = new Button({
|
|
10287
|
+
text: t('mbtiles.importButton'),
|
|
10288
|
+
variant: 'success',
|
|
9933
10289
|
icon: icons.upload({ size: 16, color: 'white' }),
|
|
9934
10290
|
className: 'w-full py-2.5 text-base shadow-lg shadow-green-500/20',
|
|
9935
10291
|
disabled: true,
|
|
9936
10292
|
onClick: () => this.handleImport(),
|
|
9937
10293
|
});
|
|
9938
|
-
this.importButton =
|
|
9939
|
-
|
|
9940
|
-
section.appendChild(
|
|
10294
|
+
this.importButton = importBtn.getElement();
|
|
10295
|
+
form.appendChild(this.importButton);
|
|
10296
|
+
section.appendChild(form);
|
|
9941
10297
|
return section;
|
|
9942
10298
|
}
|
|
9943
|
-
|
|
9944
|
-
const
|
|
9945
|
-
|
|
9946
|
-
|
|
9947
|
-
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
9955
|
-
|
|
9956
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
9957
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">PMTiles</div>
|
|
9958
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.pmtilesDesc')}</div>
|
|
9959
|
-
</div>
|
|
9960
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
9961
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">MBTiles</div>
|
|
9962
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.mbtilesDesc')}</div>
|
|
9963
|
-
</div>
|
|
10299
|
+
createSection(title, accentColor, iconHtml) {
|
|
10300
|
+
const section = document.createElement('div');
|
|
10301
|
+
section.className =
|
|
10302
|
+
'glass-input p-6 rounded-xl border-0 bg-white/40 dark:bg-gray-800/40 relative overflow-hidden';
|
|
10303
|
+
const accent = document.createElement('div');
|
|
10304
|
+
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-${accentColor}-500 opacity-50`;
|
|
10305
|
+
section.appendChild(accent);
|
|
10306
|
+
const header = document.createElement('h3');
|
|
10307
|
+
header.className =
|
|
10308
|
+
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
10309
|
+
header.innerHTML = `
|
|
10310
|
+
<div class="p-2 bg-${accentColor}-500/10 rounded-lg text-${accentColor}-600 dark:text-${accentColor}-400">
|
|
10311
|
+
${iconHtml}
|
|
9964
10312
|
</div>
|
|
10313
|
+
${title}
|
|
9965
10314
|
`;
|
|
9966
|
-
|
|
10315
|
+
section.appendChild(header);
|
|
10316
|
+
return section;
|
|
10317
|
+
}
|
|
10318
|
+
createProgressBlock(accentColor, initialText) {
|
|
10319
|
+
const container = document.createElement('div');
|
|
10320
|
+
container.className = 'hidden';
|
|
10321
|
+
const barWrap = document.createElement('div');
|
|
10322
|
+
barWrap.className = 'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
10323
|
+
const bar = document.createElement('div');
|
|
10324
|
+
bar.className = `bg-${accentColor}-600 h-2 rounded-full transition-all duration-300`;
|
|
10325
|
+
bar.style.width = '0%';
|
|
10326
|
+
barWrap.appendChild(bar);
|
|
10327
|
+
const text = document.createElement('p');
|
|
10328
|
+
text.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10329
|
+
text.textContent = initialText;
|
|
10330
|
+
container.appendChild(barWrap);
|
|
10331
|
+
container.appendChild(text);
|
|
10332
|
+
return { container, bar, text };
|
|
10333
|
+
}
|
|
10334
|
+
createFooter() {
|
|
10335
|
+
const footer = document.createElement('div');
|
|
10336
|
+
footer.className = 'flex gap-3 justify-end';
|
|
10337
|
+
if (i18n.isRTL()) {
|
|
10338
|
+
footer.setAttribute('dir', 'rtl');
|
|
10339
|
+
}
|
|
10340
|
+
const close = new Button({
|
|
10341
|
+
text: t('app.close'),
|
|
10342
|
+
variant: 'secondary',
|
|
10343
|
+
onClick: () => this.hide(),
|
|
10344
|
+
});
|
|
10345
|
+
footer.appendChild(close.getElement());
|
|
10346
|
+
return footer;
|
|
9967
10347
|
}
|
|
9968
10348
|
attachEventListeners() {
|
|
9969
|
-
// Enable import button when file is selected
|
|
9970
10349
|
if (this.importFileInput && this.importButton) {
|
|
9971
10350
|
this.importFileInput.addEventListener('change', () => {
|
|
9972
|
-
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
}
|
|
9976
|
-
}
|
|
9977
|
-
else {
|
|
9978
|
-
if (this.importButton) {
|
|
9979
|
-
this.importButton.disabled = true;
|
|
9980
|
-
}
|
|
9981
|
-
}
|
|
10351
|
+
const hasFile = !!(this.importFileInput?.files && this.importFileInput.files.length > 0);
|
|
10352
|
+
if (this.importButton)
|
|
10353
|
+
this.importButton.disabled = !hasFile;
|
|
9982
10354
|
});
|
|
9983
10355
|
}
|
|
9984
10356
|
}
|
|
@@ -9988,34 +10360,27 @@
|
|
|
9988
10360
|
this.isExporting = true;
|
|
9989
10361
|
if (this.exportButton)
|
|
9990
10362
|
this.exportButton.disabled = true;
|
|
10363
|
+
this.exportProgressContainer?.classList.remove('hidden');
|
|
9991
10364
|
try {
|
|
9992
|
-
const
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
|
|
9997
|
-
|
|
9998
|
-
|
|
9999
|
-
|
|
10000
|
-
|
|
10001
|
-
if (progressContainer) {
|
|
10002
|
-
progressContainer.classList.remove('hidden');
|
|
10003
|
-
}
|
|
10004
|
-
const result = await this.options.exportRegion(this.options.region.id, format, options);
|
|
10005
|
-
if (this.exportProgressBar) {
|
|
10365
|
+
const result = await this.options.exportRegion(this.options.region.id, {
|
|
10366
|
+
onProgress: p => {
|
|
10367
|
+
if (this.exportProgressBar)
|
|
10368
|
+
this.exportProgressBar.style.width = `${p.percentage}%`;
|
|
10369
|
+
if (this.exportProgressText)
|
|
10370
|
+
this.exportProgressText.textContent = p.message;
|
|
10371
|
+
},
|
|
10372
|
+
});
|
|
10373
|
+
if (this.exportProgressBar)
|
|
10006
10374
|
this.exportProgressBar.style.width = '100%';
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
this.exportProgressText.textContent = t('importExport.exportComplete');
|
|
10010
|
-
}
|
|
10375
|
+
if (this.exportProgressText)
|
|
10376
|
+
this.exportProgressText.textContent = t('mbtiles.exportComplete');
|
|
10011
10377
|
this.options.onExport?.(result);
|
|
10012
|
-
|
|
10013
|
-
setTimeout(() => this.hide(), 1500);
|
|
10378
|
+
setTimeout(() => this.hide(), 1200);
|
|
10014
10379
|
}
|
|
10015
10380
|
catch (error) {
|
|
10016
10381
|
modalLogger.error('Export error:', error instanceof Error ? error.message : String(error));
|
|
10017
10382
|
if (this.exportProgressText) {
|
|
10018
|
-
this.exportProgressText.textContent = t('
|
|
10383
|
+
this.exportProgressText.textContent = t('mbtiles.exportFailed');
|
|
10019
10384
|
this.exportProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10020
10385
|
}
|
|
10021
10386
|
}
|
|
@@ -10026,45 +10391,47 @@
|
|
|
10026
10391
|
}
|
|
10027
10392
|
}
|
|
10028
10393
|
async handleImport() {
|
|
10029
|
-
if (this.isImporting || !this.options.importRegion
|
|
10394
|
+
if (this.isImporting || !this.options.importRegion)
|
|
10395
|
+
return;
|
|
10396
|
+
const file = this.importFileInput?.files?.[0];
|
|
10397
|
+
if (!file)
|
|
10030
10398
|
return;
|
|
10031
10399
|
this.isImporting = true;
|
|
10032
10400
|
if (this.importButton)
|
|
10033
10401
|
this.importButton.disabled = true;
|
|
10402
|
+
this.importProgressContainer?.classList.remove('hidden');
|
|
10034
10403
|
try {
|
|
10035
|
-
const file = this.importFileInput.files[0];
|
|
10036
|
-
const overwrite = this.importOverwriteCheckbox?.checked ?? false;
|
|
10037
|
-
// Show progress
|
|
10038
|
-
const progressContainer = this.importProgressBar?.parentElement?.parentElement;
|
|
10039
|
-
if (progressContainer) {
|
|
10040
|
-
progressContainer.classList.remove('hidden');
|
|
10041
|
-
}
|
|
10042
|
-
// Determine format from file extension
|
|
10043
|
-
const format = file.name.endsWith('.pmtiles')
|
|
10044
|
-
? 'pmtiles'
|
|
10045
|
-
: file.name.endsWith('.mbtiles')
|
|
10046
|
-
? 'mbtiles'
|
|
10047
|
-
: 'json';
|
|
10048
10404
|
const data = {
|
|
10049
10405
|
file,
|
|
10050
|
-
format,
|
|
10051
|
-
overwrite,
|
|
10406
|
+
format: 'mbtiles',
|
|
10407
|
+
overwrite: this.importOverwriteCheckbox?.checked ?? false,
|
|
10408
|
+
newRegionName: this.importNameInput?.value.trim() || undefined,
|
|
10409
|
+
onProgress: p => {
|
|
10410
|
+
if (this.importProgressBar)
|
|
10411
|
+
this.importProgressBar.style.width = `${p.percentage}%`;
|
|
10412
|
+
if (this.importProgressText)
|
|
10413
|
+
this.importProgressText.textContent = p.message;
|
|
10414
|
+
},
|
|
10052
10415
|
};
|
|
10053
10416
|
const result = await this.options.importRegion(data);
|
|
10054
|
-
if (this.importProgressBar)
|
|
10417
|
+
if (this.importProgressBar)
|
|
10055
10418
|
this.importProgressBar.style.width = '100%';
|
|
10056
|
-
}
|
|
10057
10419
|
if (this.importProgressText) {
|
|
10058
|
-
this.importProgressText.textContent =
|
|
10420
|
+
this.importProgressText.textContent = result.success
|
|
10421
|
+
? t('mbtiles.importComplete')
|
|
10422
|
+
: result.message;
|
|
10423
|
+
if (!result.success) {
|
|
10424
|
+
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10425
|
+
}
|
|
10059
10426
|
}
|
|
10060
10427
|
this.options.onImport?.(result);
|
|
10061
|
-
|
|
10062
|
-
|
|
10428
|
+
if (result.success)
|
|
10429
|
+
setTimeout(() => this.hide(), 1200);
|
|
10063
10430
|
}
|
|
10064
10431
|
catch (error) {
|
|
10065
10432
|
modalLogger.error('Import error:', error instanceof Error ? error.message : String(error));
|
|
10066
10433
|
if (this.importProgressText) {
|
|
10067
|
-
this.importProgressText.textContent = t('
|
|
10434
|
+
this.importProgressText.textContent = t('mbtiles.importFailed');
|
|
10068
10435
|
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10069
10436
|
}
|
|
10070
10437
|
}
|
|
@@ -10074,23 +10441,6 @@
|
|
|
10074
10441
|
this.importButton.disabled = false;
|
|
10075
10442
|
}
|
|
10076
10443
|
}
|
|
10077
|
-
createFooter() {
|
|
10078
|
-
const footer = document.createElement('div');
|
|
10079
|
-
footer.className = 'flex gap-3 justify-end';
|
|
10080
|
-
if (i18n.isRTL()) {
|
|
10081
|
-
footer.setAttribute('dir', 'rtl');
|
|
10082
|
-
}
|
|
10083
|
-
const closeButton = new Button({
|
|
10084
|
-
text: t('app.close'),
|
|
10085
|
-
variant: 'secondary',
|
|
10086
|
-
onClick: () => this.hide(),
|
|
10087
|
-
});
|
|
10088
|
-
footer.appendChild(closeButton.getElement());
|
|
10089
|
-
return footer;
|
|
10090
|
-
}
|
|
10091
|
-
destroy() {
|
|
10092
|
-
this.modal?.destroy();
|
|
10093
|
-
}
|
|
10094
10444
|
}
|
|
10095
10445
|
|
|
10096
10446
|
/**
|
|
@@ -10804,9 +11154,9 @@
|
|
|
10804
11154
|
<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')}">
|
|
10805
11155
|
${icons.download({ size: 14, color: 'currentColor' })}
|
|
10806
11156
|
</button>
|
|
10807
|
-
|
|
11157
|
+
<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')}">
|
|
10808
11158
|
${icons.deviceFloppy({ size: 14, color: 'currentColor' })}
|
|
10809
|
-
</button>
|
|
11159
|
+
</button>
|
|
10810
11160
|
<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')}">
|
|
10811
11161
|
${icons.trash({ size: 14, color: 'currentColor' })}
|
|
10812
11162
|
</button>
|
|
@@ -11068,7 +11418,7 @@
|
|
|
11068
11418
|
}
|
|
11069
11419
|
}
|
|
11070
11420
|
/**
|
|
11071
|
-
*
|
|
11421
|
+
* Show the MBTiles import/export modal for a region.
|
|
11072
11422
|
*/
|
|
11073
11423
|
async handleImportExport(regionId, _regionData) {
|
|
11074
11424
|
try {
|
|
@@ -11076,44 +11426,26 @@
|
|
|
11076
11426
|
const region = regions.find((r) => r.id === regionId);
|
|
11077
11427
|
if (!region)
|
|
11078
11428
|
return;
|
|
11079
|
-
const
|
|
11429
|
+
const mbtilesModal = new MBTilesModal({
|
|
11080
11430
|
region,
|
|
11081
|
-
onClose: () =>
|
|
11082
|
-
this.modalManager.close();
|
|
11083
|
-
},
|
|
11431
|
+
onClose: () => this.modalManager.close(),
|
|
11084
11432
|
onExport: result => {
|
|
11085
|
-
panelLogger.debug('
|
|
11086
|
-
// Handle export result - could show success message
|
|
11433
|
+
panelLogger.debug('MBTiles export completed:', result);
|
|
11087
11434
|
this.offlineManager.downloadExportedRegion(result);
|
|
11088
11435
|
},
|
|
11089
11436
|
onImport: result => {
|
|
11090
|
-
panelLogger.debug('
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
},
|
|
11094
|
-
exportRegion: async (regionId, format, options) => {
|
|
11095
|
-
// Delegate to offline manager's export functionality
|
|
11096
|
-
switch (format) {
|
|
11097
|
-
case 'json':
|
|
11098
|
-
return await this.offlineManager.exportRegionAsJSON(regionId, options);
|
|
11099
|
-
case 'pmtiles':
|
|
11100
|
-
return await this.offlineManager.exportRegionAsPMTiles(regionId, options);
|
|
11101
|
-
case 'mbtiles':
|
|
11102
|
-
return await this.offlineManager.exportRegionAsMBTiles(regionId, options);
|
|
11103
|
-
default:
|
|
11104
|
-
throw new Error(`Unsupported export format: ${format}`);
|
|
11105
|
-
}
|
|
11106
|
-
},
|
|
11107
|
-
importRegion: async (data) => {
|
|
11108
|
-
// Delegate to offline manager's import functionality
|
|
11109
|
-
return await this.offlineManager.importRegion(data);
|
|
11437
|
+
panelLogger.debug('MBTiles import completed:', result);
|
|
11438
|
+
if (result.success)
|
|
11439
|
+
this.refresh();
|
|
11110
11440
|
},
|
|
11441
|
+
exportRegion: (id, options) => this.offlineManager.exportRegionAsMBTiles(id, options),
|
|
11442
|
+
importRegion: data => this.offlineManager.importRegion(data),
|
|
11111
11443
|
});
|
|
11112
|
-
const modal =
|
|
11444
|
+
const modal = mbtilesModal.show();
|
|
11113
11445
|
this.modalManager.show(modal);
|
|
11114
11446
|
}
|
|
11115
11447
|
catch (error) {
|
|
11116
|
-
panelLogger.error('Error showing
|
|
11448
|
+
panelLogger.error('Error showing MBTiles modal:', error);
|
|
11117
11449
|
}
|
|
11118
11450
|
}
|
|
11119
11451
|
/**
|
|
@@ -11512,6 +11844,9 @@
|
|
|
11512
11844
|
delete patchedStyle.imports;
|
|
11513
11845
|
panelLogger.debug('Stripped imports from offline style (already flattened)');
|
|
11514
11846
|
}
|
|
11847
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
11848
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
11849
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
11515
11850
|
// Enforce maxzoom for all tile sources to prevent requesting non-existent tiles
|
|
11516
11851
|
// Find the maximum zoom level from all regions using this style
|
|
11517
11852
|
let maxZoom = 14; // Default fallback
|
|
@@ -11560,10 +11895,12 @@
|
|
|
11560
11895
|
panelLogger.debug(`Fixed legacy source ${sourceId}: added tiles, removed url`);
|
|
11561
11896
|
}
|
|
11562
11897
|
}
|
|
11563
|
-
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11898
|
+
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11899
|
+
// buildings and raster-array for Mapbox Standard landmark icons).
|
|
11564
11900
|
if (src.type === 'vector' ||
|
|
11565
11901
|
src.type === 'raster' ||
|
|
11566
11902
|
src.type === 'raster-dem' ||
|
|
11903
|
+
src.type === 'raster-array' ||
|
|
11567
11904
|
src.type === 'batched-model') {
|
|
11568
11905
|
const originalMaxzoom = src.maxzoom;
|
|
11569
11906
|
// Use the lower of region maxZoom and source's original maxzoom so we
|
|
@@ -12251,9 +12588,7 @@
|
|
|
12251
12588
|
detectProviderFromUrl() {
|
|
12252
12589
|
const styleUrl = this.styleUrlInput?.value || '';
|
|
12253
12590
|
// Simple detection logic
|
|
12254
|
-
if (styleUrl.startsWith('mapbox://') ||
|
|
12255
|
-
styleUrl.includes('mapbox.com') ||
|
|
12256
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
12591
|
+
if (styleUrl.startsWith('mapbox://') || isMapboxHost(styleUrl)) {
|
|
12257
12592
|
if (this.providerSelect)
|
|
12258
12593
|
this.providerSelect.value = 'mapbox';
|
|
12259
12594
|
this.toggleAccessTokenVisibility(true);
|
|
@@ -13017,39 +13352,39 @@
|
|
|
13017
13352
|
}
|
|
13018
13353
|
// Development proxy for CORS issues (when running on localhost)
|
|
13019
13354
|
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
if (isTileRequest && url.includes('tiles-a.basemaps.cartocdn.com')) {
|
|
13024
|
-
const proxyUrl = url.replace('https://tiles-a.basemaps.cartocdn.com', '/tiles/carto-a');
|
|
13025
|
-
return originalFetch(proxyUrl, init);
|
|
13026
|
-
}
|
|
13027
|
-
if (isTileRequest && url.includes('tiles-b.basemaps.cartocdn.com')) {
|
|
13028
|
-
const proxyUrl = url.replace('https://tiles-b.basemaps.cartocdn.com', '/tiles/carto-b');
|
|
13029
|
-
return originalFetch(proxyUrl, init);
|
|
13355
|
+
let parsed = null;
|
|
13356
|
+
try {
|
|
13357
|
+
parsed = new URL(url, location.origin);
|
|
13030
13358
|
}
|
|
13031
|
-
|
|
13032
|
-
|
|
13033
|
-
return originalFetch(proxyUrl, init);
|
|
13359
|
+
catch {
|
|
13360
|
+
parsed = null;
|
|
13034
13361
|
}
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13362
|
+
const hostname = parsed?.hostname ?? '';
|
|
13363
|
+
const pathAndQuery = parsed ? parsed.pathname + parsed.search : '';
|
|
13364
|
+
// Proxy Carto tile requests (tiles and TileJSON)
|
|
13365
|
+
const isTileRequest = /\/\d+\/\d+\/\d+\.(pbf|mvt|png|jpg|jpeg|webp)/.test(parsed?.pathname ?? '');
|
|
13366
|
+
const isTileJsonRequest = (parsed?.pathname.endsWith('.json') ?? false) &&
|
|
13367
|
+
(hostname === 'basemaps.cartocdn.com' || hostname.endsWith('.basemaps.cartocdn.com'));
|
|
13368
|
+
const cartoSubdomainProxy = {
|
|
13369
|
+
'tiles-a.basemaps.cartocdn.com': '/tiles/carto-a',
|
|
13370
|
+
'tiles-b.basemaps.cartocdn.com': '/tiles/carto-b',
|
|
13371
|
+
'tiles-c.basemaps.cartocdn.com': '/tiles/carto-c',
|
|
13372
|
+
'tiles-d.basemaps.cartocdn.com': '/tiles/carto-d',
|
|
13373
|
+
};
|
|
13374
|
+
if (isTileRequest && cartoSubdomainProxy[hostname]) {
|
|
13375
|
+
return originalFetch(cartoSubdomainProxy[hostname] + pathAndQuery, init);
|
|
13038
13376
|
}
|
|
13039
13377
|
// Proxy TileJSON requests from tiles.basemaps.cartocdn.com
|
|
13040
|
-
if (isTileJsonRequest &&
|
|
13041
|
-
|
|
13042
|
-
return originalFetch(proxyUrl, init);
|
|
13378
|
+
if (isTileJsonRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13379
|
+
return originalFetch('/carto-api' + pathAndQuery, init);
|
|
13043
13380
|
}
|
|
13044
13381
|
// Fallback for old format (tiles without subdomain)
|
|
13045
|
-
if (isTileRequest &&
|
|
13046
|
-
|
|
13047
|
-
return originalFetch(proxyUrl, init);
|
|
13382
|
+
if (isTileRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13383
|
+
return originalFetch('/tiles/carto-a' + pathAndQuery, init);
|
|
13048
13384
|
}
|
|
13049
13385
|
// Proxy OpenStreetMap tile requests
|
|
13050
|
-
if (
|
|
13051
|
-
|
|
13052
|
-
return originalFetch(proxyUrl, init);
|
|
13386
|
+
if (hostname === 'tile.openstreetmap.org') {
|
|
13387
|
+
return originalFetch('/tiles/osm' + pathAndQuery, init);
|
|
13053
13388
|
}
|
|
13054
13389
|
}
|
|
13055
13390
|
return originalFetch(input, init);
|
|
@@ -13482,6 +13817,9 @@
|
|
|
13482
13817
|
if (patchedStyle.imports) {
|
|
13483
13818
|
delete patchedStyle.imports;
|
|
13484
13819
|
}
|
|
13820
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
13821
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
13822
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
13485
13823
|
// If using Service Worker (Mapbox GL JS), convert idb:// to /__offline__/ URLs
|
|
13486
13824
|
if (this.useServiceWorker) {
|
|
13487
13825
|
if (this.swReadyPromise) {
|
|
@@ -13644,6 +13982,7 @@
|
|
|
13644
13982
|
exports.MAPBOX_CLASSIC_STYLES = MAPBOX_CLASSIC_STYLES;
|
|
13645
13983
|
exports.MAP_PROVIDERS = MAP_PROVIDERS;
|
|
13646
13984
|
exports.MaintenanceService = MaintenanceService;
|
|
13985
|
+
exports.ModelService = ModelService;
|
|
13647
13986
|
exports.OfflineManagerControl = OfflineManagerControl;
|
|
13648
13987
|
exports.OfflineMapDBVersionError = OfflineMapDBVersionError;
|
|
13649
13988
|
exports.OfflineMapManager = OfflineMapManager;
|
|
@@ -13666,6 +14005,7 @@
|
|
|
13666
14005
|
exports.cleanupExpiredTiles = cleanupExpiredTiles;
|
|
13667
14006
|
exports.cleanupOldFonts = cleanupOldFonts;
|
|
13668
14007
|
exports.cleanupOldGlyphs = cleanupOldGlyphs;
|
|
14008
|
+
exports.cleanupOldModels = cleanupOldModels;
|
|
13669
14009
|
exports.cleanupOldSprites = cleanupOldSprites;
|
|
13670
14010
|
exports.cleanupOldStyles = cleanupOldStyles;
|
|
13671
14011
|
exports.cleanupOldTiles = cleanupOldTiles;
|
|
@@ -13673,6 +14013,7 @@
|
|
|
13673
14013
|
exports.clearAllCaches = clearAllCaches;
|
|
13674
14014
|
exports.configureLogger = configureLogger;
|
|
13675
14015
|
exports.configureProxy = configureProxy;
|
|
14016
|
+
exports.configureSqlJs = configureSqlJs;
|
|
13676
14017
|
exports.convertStyleForServiceWorker = convertStyleForServiceWorker;
|
|
13677
14018
|
exports.countCompressedTiles = countCompressedTiles;
|
|
13678
14019
|
exports.createProgressTracker = createProgressTracker;
|
|
@@ -13686,6 +14027,7 @@
|
|
|
13686
14027
|
exports.detectStyleProvider = detectStyleProvider;
|
|
13687
14028
|
exports.downloadFonts = downloadFonts;
|
|
13688
14029
|
exports.downloadGlyphs = downloadGlyphs;
|
|
14030
|
+
exports.downloadModels = downloadModels;
|
|
13689
14031
|
exports.downloadSprites = downloadSprites;
|
|
13690
14032
|
exports.downloadStyleWithProvider = downloadStyleWithProvider;
|
|
13691
14033
|
exports.downloadStyles = downloadStyles;
|
|
@@ -13694,6 +14036,7 @@
|
|
|
13694
14036
|
exports.extractAccessToken = extractAccessToken;
|
|
13695
14037
|
exports.extractAllFontNames = extractAllFontNames;
|
|
13696
14038
|
exports.extractFontNamesFromTextField = extractFontNamesFromTextField;
|
|
14039
|
+
exports.extractTileExtensionFromUrl = extractTileExtensionFromUrl;
|
|
13697
14040
|
exports.fetchResourceWithRetry = fetchResourceWithRetry;
|
|
13698
14041
|
exports.fetchWithRetry = fetchWithRetry;
|
|
13699
14042
|
exports.fontService = fontService;
|
|
@@ -13706,18 +14049,24 @@
|
|
|
13706
14049
|
exports.getGlyphAnalytics = getGlyphAnalytics;
|
|
13707
14050
|
exports.getGlyphStats = getGlyphStats;
|
|
13708
14051
|
exports.getIcon = getIcon;
|
|
14052
|
+
exports.getModel = getModel;
|
|
14053
|
+
exports.getModelStats = getModelStats;
|
|
13709
14054
|
exports.getRegionAnalytics = getRegionAnalytics;
|
|
13710
14055
|
exports.getSpriteAnalytics = getSpriteAnalytics;
|
|
13711
14056
|
exports.getSpriteStats = getSpriteStats;
|
|
14057
|
+
exports.getSqlJs = getSqlJs;
|
|
13712
14058
|
exports.getStyleStats = getStyleStats;
|
|
13713
14059
|
exports.getTileAnalytics = getTileAnalytics;
|
|
13714
14060
|
exports.getTileStats = getTileStats;
|
|
14061
|
+
exports.getUrlHostname = getUrlHostname;
|
|
13715
14062
|
exports.getUserErrorMessage = getUserErrorMessage;
|
|
13716
14063
|
exports.glyphService = glyphService;
|
|
13717
14064
|
exports.hasImports = hasImports;
|
|
14065
|
+
exports.hostMatches = hostMatches;
|
|
13718
14066
|
exports.i18n = i18n;
|
|
13719
14067
|
exports.icons = icons;
|
|
13720
14068
|
exports.idbFetchHandler = idbFetchHandler;
|
|
14069
|
+
exports.isMapboxHost = isMapboxHost;
|
|
13721
14070
|
exports.isMapboxProtocol = isMapboxProtocol;
|
|
13722
14071
|
exports.isStyleDownloaded = isStyleDownloaded;
|
|
13723
14072
|
exports.loadAllStoredRegions = loadAllStoredRegions;
|
|
@@ -13725,6 +14074,8 @@
|
|
|
13725
14074
|
exports.loadStyleById = loadStyleById;
|
|
13726
14075
|
exports.loadStyles = loadStyles;
|
|
13727
14076
|
exports.logger = logger;
|
|
14077
|
+
exports.modelKeyBelongsToStyle = modelKeyBelongsToStyle;
|
|
14078
|
+
exports.modelService = modelService;
|
|
13728
14079
|
exports.normalizeSpriteProperty = normalizeSpriteProperty;
|
|
13729
14080
|
exports.normalizeStyleUrl = normalizeStyleUrl;
|
|
13730
14081
|
exports.optimizeStorage = optimizeStorage;
|
|
@@ -13741,6 +14092,7 @@
|
|
|
13741
14092
|
exports.resourceKeyBelongsToStyle = resourceKeyBelongsToStyle;
|
|
13742
14093
|
exports.rewriteMapboxCdnTileUrl = rewriteMapboxCdnTileUrl;
|
|
13743
14094
|
exports.safeExecute = safeExecute;
|
|
14095
|
+
exports.sanitizeIndoorExpressions = sanitizeIndoorExpressions;
|
|
13744
14096
|
exports.setupAutoCleanup = setupAutoCleanup;
|
|
13745
14097
|
exports.spriteService = spriteService;
|
|
13746
14098
|
exports.stopAutoCleanup = stopAutoCleanup;
|
|
@@ -13754,6 +14106,7 @@
|
|
|
13754
14106
|
exports.validateZoomLevels = validateZoomLevels;
|
|
13755
14107
|
exports.verifyAndRepairFonts = verifyAndRepairFonts;
|
|
13756
14108
|
exports.verifyAndRepairGlyphs = verifyAndRepairGlyphs;
|
|
14109
|
+
exports.verifyAndRepairModels = verifyAndRepairModels;
|
|
13757
14110
|
exports.verifyAndRepairSprites = verifyAndRepairSprites;
|
|
13758
14111
|
|
|
13759
14112
|
Object.defineProperty(exports, '__esModule', { value: true });
|