map-gl-offline 0.7.0 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/idb-offline-sw.js +313 -360
- package/dist/index.esm.js +1048 -1038
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +1056 -1037
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +1056 -1037
- package/dist/index.umd.js.map +1 -1
- package/dist/managers/offlineMapManager/importExportManagement.d.ts +5 -3
- package/dist/services/fontService.d.ts +12 -2
- package/dist/services/glyphService.d.ts +11 -2
- package/dist/services/importExportService.d.ts +11 -26
- package/dist/services/spriteService.d.ts +10 -3
- package/dist/style.css +1 -1
- package/dist/sw/offline-sw.d.ts +17 -0
- package/dist/sw/shared.d.ts +108 -0
- package/dist/types/import-export.d.ts +7 -28
- package/dist/ui/components/shared/PanelContent.d.ts +0 -1
- package/dist/ui/managers/PanelManager.d.ts +1 -1
- package/dist/ui/modals/{importExportModal.d.ts → mbtilesModal.d.ts} +18 -17
- package/dist/ui/offlineManagerControl.d.ts +18 -3
- package/dist/ui/translations/ar.d.ts +23 -37
- package/dist/ui/translations/en.d.ts +23 -37
- package/dist/utils/constants.d.ts +3 -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 +8 -5
package/dist/index.umd.js
CHANGED
|
@@ -55,45 +55,61 @@
|
|
|
55
55
|
SUPPORTED_EXTENSIONS: ['pbf', 'mvt', 'png', 'jpg', 'jpeg', 'webp', 'glb'],
|
|
56
56
|
};
|
|
57
57
|
// Glyph Configuration
|
|
58
|
+
//
|
|
59
|
+
// Glyph servers (MapTiler, Mapbox, OpenFreeMap, ...) serve glyphs in fixed
|
|
60
|
+
// 256-codepoint blocks aligned to a multiple of 256: every request must be
|
|
61
|
+
// `${k * 256}-${k * 256 + 255}`. Strict servers (e.g. MapTiler) reject any
|
|
62
|
+
// other range with HTTP 400 "Invalid glyph range"; lenient ones silently
|
|
63
|
+
// accept them, which is how malformed ranges went unnoticed. See issue #37.
|
|
64
|
+
const GLYPH_BLOCK_SIZE = 256;
|
|
65
|
+
const MAX_GLYPH_CODEPOINT = 65535;
|
|
66
|
+
/**
|
|
67
|
+
* Expand an inclusive Unicode codepoint span into the aligned 256-codepoint
|
|
68
|
+
* glyph blocks that cover it, formatted as `"start-end"` request ranges.
|
|
69
|
+
* The span need not be block-aligned — it is snapped out to whole blocks.
|
|
70
|
+
*/
|
|
71
|
+
function glyphBlocksForSpan(start, end) {
|
|
72
|
+
const firstBlock = Math.floor(start / GLYPH_BLOCK_SIZE);
|
|
73
|
+
const lastBlock = Math.floor(Math.min(end, MAX_GLYPH_CODEPOINT) / GLYPH_BLOCK_SIZE);
|
|
74
|
+
const blocks = [];
|
|
75
|
+
for (let block = firstBlock; block <= lastBlock; block++) {
|
|
76
|
+
const blockStart = block * GLYPH_BLOCK_SIZE;
|
|
77
|
+
blocks.push(`${blockStart}-${blockStart + GLYPH_BLOCK_SIZE - 1}`);
|
|
78
|
+
}
|
|
79
|
+
return blocks;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Unicode codepoint spans the comprehensive glyph download aims to cover.
|
|
83
|
+
* Each span is snapped to whole 256-codepoint glyph blocks below, so the
|
|
84
|
+
* resulting request ranges are always server-valid regardless of where the
|
|
85
|
+
* underlying Unicode blocks happen to start or end. To extend coverage, add
|
|
86
|
+
* a span here — never hand-write raw `"start-end"` ranges.
|
|
87
|
+
*/
|
|
88
|
+
const GLYPH_COVERAGE_SPANS = [
|
|
89
|
+
[0x0000, 0x12ff], // Latin, Greek, Cyrillic, Hebrew, Arabic, Indic, SE Asian, Georgian, Ethiopic, Cherokee
|
|
90
|
+
[0x1e00, 0x21ff], // Latin Extended Additional, punctuation, symbols, arrows
|
|
91
|
+
[0x2e00, 0x31ff], // CJK radicals, Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo
|
|
92
|
+
[0x4e00, 0x4fff], // CJK Unified Ideographs (common subset)
|
|
93
|
+
[0xa000, 0xa4ff], // Yi Syllables and Radicals
|
|
94
|
+
[0xac00, 0xd7ff], // Hangul Syllables (Korean)
|
|
95
|
+
[0xf900, 0xfbff], // CJK Compatibility Ideographs, Alphabetic Presentation Forms
|
|
96
|
+
[0xfe00, 0xfeff], // Variation Selectors
|
|
97
|
+
[0xff00, 0xffff], // Halfwidth and Fullwidth Forms
|
|
98
|
+
];
|
|
99
|
+
/** Build the deduped, codepoint-ascending list of comprehensive glyph ranges. */
|
|
100
|
+
function buildComprehensiveRanges() {
|
|
101
|
+
const ranges = new Set();
|
|
102
|
+
for (const [start, end] of GLYPH_COVERAGE_SPANS) {
|
|
103
|
+
for (const range of glyphBlocksForSpan(start, end)) {
|
|
104
|
+
ranges.add(range);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return Array.from(ranges).sort((a, b) => Number(a.split('-')[0]) - Number(b.split('-')[0]));
|
|
108
|
+
}
|
|
58
109
|
const GLYPH_CONFIG = {
|
|
59
110
|
DEFAULT_URL: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
|
|
60
111
|
DEFAULT_RANGES: ['0-255'],
|
|
61
|
-
COMPREHENSIVE_RANGES:
|
|
62
|
-
'0-255', // Basic Latin + Latin-1 Supplement
|
|
63
|
-
'256-511', // Latin Extended-A + Latin Extended-B
|
|
64
|
-
'512-767', // IPA Extensions + Spacing Modifier Letters
|
|
65
|
-
'768-1023', // Combining Diacritical Marks + Greek and Coptic
|
|
66
|
-
'1024-1279', // Cyrillic + Cyrillic Supplement
|
|
67
|
-
'1280-1535', // Armenian + Hebrew
|
|
68
|
-
'1536-1791', // Arabic
|
|
69
|
-
'1792-2047', // Syriac + Arabic Supplement + Thaana
|
|
70
|
-
'2048-2303', // NKo + Samaritan + Mandaic
|
|
71
|
-
'2304-2559', // Devanagari + Bengali
|
|
72
|
-
'2560-2815', // Gurmukhi + Gujarati
|
|
73
|
-
'2816-3071', // Oriya + Tamil
|
|
74
|
-
'3072-3327', // Telugu + Kannada
|
|
75
|
-
'3328-3583', // Malayalam + Sinhala
|
|
76
|
-
'3584-3839', // Thai + Lao
|
|
77
|
-
'3840-4095', // Tibetan + Myanmar
|
|
78
|
-
'4096-4351', // Georgian + Hangul Jamo
|
|
79
|
-
'4352-4607', // Ethiopic
|
|
80
|
-
'4608-4863', // Cherokee + Canadian Aboriginal
|
|
81
|
-
'7680-7935', // Latin Extended Additional
|
|
82
|
-
'8192-8447', // General Punctuation, Superscripts/Subscripts, Currency Symbols
|
|
83
|
-
'8448-8703', // Letterlike Symbols, Number Forms, Arrows
|
|
84
|
-
'11904-12031', // CJK Radicals Supplement
|
|
85
|
-
'12032-12255', // Kangxi Radicals + CJK Symbols
|
|
86
|
-
'12288-12543', // Hiragana + Katakana
|
|
87
|
-
'12544-12799', // Bopomofo + Hangul Compatibility Jamo
|
|
88
|
-
'19968-20223', // CJK Unified Ideographs (first block)
|
|
89
|
-
'20224-20479', // CJK Unified Ideographs
|
|
90
|
-
'40960-42127', // Yi Syllables + Yi Radicals
|
|
91
|
-
'44032-55203', // Hangul Syllables (Korean)
|
|
92
|
-
'63744-64255', // CJK Compatibility Ideographs
|
|
93
|
-
'64256-64511', // Alphabetic Presentation Forms
|
|
94
|
-
'65024-65279', // Variation Selectors
|
|
95
|
-
'65280-65535', // Halfwidth and Fullwidth Forms
|
|
96
|
-
],
|
|
112
|
+
COMPREHENSIVE_RANGES: buildComprehensiveRanges(),
|
|
97
113
|
};
|
|
98
114
|
// Style Configuration
|
|
99
115
|
const STYLE_CONFIG = {
|
|
@@ -1184,6 +1200,22 @@
|
|
|
1184
1200
|
}
|
|
1185
1201
|
return { styleId, sourceId, z, x, y, ext };
|
|
1186
1202
|
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Extract the extension (the last dotted segment before `?`, `#`, or end) from
|
|
1205
|
+
* a tile URL or tile URL template. Defaults to `"pbf"` when no extension can
|
|
1206
|
+
* be parsed. For multi-extension URLs like Mapbox v4's `{y}.vector.pbf` this
|
|
1207
|
+
* returns `"pbf"`, matching the key used when the tile is stored.
|
|
1208
|
+
*
|
|
1209
|
+
* Keeping extraction logic in one place ensures patchStyleForOffline (which
|
|
1210
|
+
* rewrites tile URLs to `idb://` at load time) derives the same extension
|
|
1211
|
+
* that tileService.extractExtension used at store time — otherwise the
|
|
1212
|
+
* first-try lookup in idbFetchHandler misses and has to fall through its
|
|
1213
|
+
* pbf/mvt/png/jpg/webp fallback loop.
|
|
1214
|
+
*/
|
|
1215
|
+
function extractTileExtensionFromUrl(url) {
|
|
1216
|
+
const match = url.match(/\.([\w]+)(?:[?#]|$)/i);
|
|
1217
|
+
return match ? match[1] : 'pbf';
|
|
1218
|
+
}
|
|
1187
1219
|
/**
|
|
1188
1220
|
* Derive tile extension from tile URL templates
|
|
1189
1221
|
*/
|
|
@@ -1191,15 +1223,210 @@
|
|
|
1191
1223
|
if (Array.isArray(tiles) && tiles.length > 0) {
|
|
1192
1224
|
const firstTile = tiles[0];
|
|
1193
1225
|
if (typeof firstTile === 'string') {
|
|
1194
|
-
|
|
1195
|
-
if (match) {
|
|
1196
|
-
return match[1];
|
|
1197
|
-
}
|
|
1226
|
+
return extractTileExtensionFromUrl(firstTile);
|
|
1198
1227
|
}
|
|
1199
1228
|
}
|
|
1200
1229
|
return 'pbf';
|
|
1201
1230
|
}
|
|
1202
1231
|
|
|
1232
|
+
/**
|
|
1233
|
+
* Pure helpers shared between the main-thread offline fetch handler
|
|
1234
|
+
* (`src/utils/idbFetchHandler.ts`) and the offline Service Worker
|
|
1235
|
+
* (`src/sw/offline-sw.ts`, compiled to `public/idb-offline-sw.js`).
|
|
1236
|
+
*
|
|
1237
|
+
* Keeping these in one place means the SW and the main-thread handler
|
|
1238
|
+
* can't drift — adding a new `model` handler, changing the fallback
|
|
1239
|
+
* order, or tweaking the tilejson-source matcher happens once.
|
|
1240
|
+
*
|
|
1241
|
+
* Nothing in here touches IndexedDB directly. Each helper takes already-
|
|
1242
|
+
* resolved inputs and returns the list of candidate keys (or the
|
|
1243
|
+
* resolved output) that the caller feeds into its own IDB lookup.
|
|
1244
|
+
*
|
|
1245
|
+
* The corresponding IDB access layer is:
|
|
1246
|
+
* - main thread: `idb` library via `dbPromise`
|
|
1247
|
+
* - service worker: raw `indexedDB.open` (see `offline-sw.ts`)
|
|
1248
|
+
*
|
|
1249
|
+
* They have different shapes so cannot be shared; the key computation
|
|
1250
|
+
* can be and is.
|
|
1251
|
+
*/
|
|
1252
|
+
/**
|
|
1253
|
+
* Extensions to try in order when the requested extension misses. `glb` is
|
|
1254
|
+
* last so batched-model sources (Mapbox Standard 3D buildings) resolve when
|
|
1255
|
+
* their source URL template ended in `.vector` or similar and the actual
|
|
1256
|
+
* tile body was stored as glb.
|
|
1257
|
+
*/
|
|
1258
|
+
const TILE_FALLBACK_EXTENSIONS = ['pbf', 'mvt', 'png', 'jpg', 'webp', 'glb'];
|
|
1259
|
+
/** Extensions minus the one the caller already tried. */
|
|
1260
|
+
function tileFallbackExtensions(requested) {
|
|
1261
|
+
return TILE_FALLBACK_EXTENSIONS.filter(e => e !== requested);
|
|
1262
|
+
}
|
|
1263
|
+
// ---------------------------------------------------------------------------
|
|
1264
|
+
// Region → style lookup
|
|
1265
|
+
// ---------------------------------------------------------------------------
|
|
1266
|
+
/**
|
|
1267
|
+
* Given an already-fetched list of style entries, find the first one whose
|
|
1268
|
+
* `regions` array contains the given ID. Pure — the caller is responsible for
|
|
1269
|
+
* loading the entries and for caching. Used by both `findStyleByRegionId`
|
|
1270
|
+
* implementations to keep the match rule identical.
|
|
1271
|
+
*/
|
|
1272
|
+
function findStyleByRegionIdIn(styles, regionId) {
|
|
1273
|
+
for (const entry of styles) {
|
|
1274
|
+
const regions = entry.regions;
|
|
1275
|
+
if (!Array.isArray(regions))
|
|
1276
|
+
continue;
|
|
1277
|
+
for (const r of regions) {
|
|
1278
|
+
if (r?.regionId === regionId || r?.id === regionId) {
|
|
1279
|
+
return entry;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
// ---------------------------------------------------------------------------
|
|
1286
|
+
// Glyph candidate keys
|
|
1287
|
+
// ---------------------------------------------------------------------------
|
|
1288
|
+
/**
|
|
1289
|
+
* Parse `FontA,FontB,FontC/0-255.pbf` into (fontstacks, rangePart). Mapbox
|
|
1290
|
+
* requests a comma-joined font-family fallback chain; each glyph is stored
|
|
1291
|
+
* individually, so the caller tries each fontstack in order.
|
|
1292
|
+
*/
|
|
1293
|
+
function parseGlyphPath(decodedPath) {
|
|
1294
|
+
const pathParts = decodedPath.split('/');
|
|
1295
|
+
const fontstackPart = pathParts[0] ?? '';
|
|
1296
|
+
const rangePart = pathParts[1] || '0-255.pbf';
|
|
1297
|
+
const fontstacks = fontstackPart
|
|
1298
|
+
.split(',')
|
|
1299
|
+
.map(f => f.trim())
|
|
1300
|
+
.filter(Boolean);
|
|
1301
|
+
return { fontstacks, rangePart };
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Build the list of keys to try for a single (fontstack, range) pair.
|
|
1305
|
+
* Order: actualStyleId variants first (most common), then downloadId,
|
|
1306
|
+
* then the bare path. Normalized and raw `.pbf`-less forms are both tried
|
|
1307
|
+
* to cover stored-key variants from older versions.
|
|
1308
|
+
*/
|
|
1309
|
+
function glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart) {
|
|
1310
|
+
const glyphPath = `${fontstack}/${rangePart}`;
|
|
1311
|
+
const normalizedPath = glyphPath.endsWith('.pbf') ? glyphPath : `${glyphPath}.pbf`;
|
|
1312
|
+
return dedupe([
|
|
1313
|
+
`${actualStyleId}::${normalizedPath}`,
|
|
1314
|
+
`${actualStyleId}::${glyphPath}`,
|
|
1315
|
+
`${downloadId}::${normalizedPath}`,
|
|
1316
|
+
`${downloadId}::${glyphPath}`,
|
|
1317
|
+
normalizedPath,
|
|
1318
|
+
glyphPath,
|
|
1319
|
+
]);
|
|
1320
|
+
}
|
|
1321
|
+
// ---------------------------------------------------------------------------
|
|
1322
|
+
// Sprite candidate keys
|
|
1323
|
+
// ---------------------------------------------------------------------------
|
|
1324
|
+
/**
|
|
1325
|
+
* Sprite keys have historically used both `::` and `:` as the separator, and
|
|
1326
|
+
* both the full filename (`sprite.json`) and the bare name (`sprite`). Return
|
|
1327
|
+
* every variant in priority order; the caller stops at the first hit.
|
|
1328
|
+
*/
|
|
1329
|
+
function spriteCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1330
|
+
const stripExt = decodedPath.replace(/\.(json|png)$/i, '');
|
|
1331
|
+
return dedupe([
|
|
1332
|
+
`${actualStyleId}::${decodedPath}`,
|
|
1333
|
+
`${actualStyleId}:${decodedPath}`,
|
|
1334
|
+
`${actualStyleId}::${stripExt}`,
|
|
1335
|
+
`${actualStyleId}:${stripExt}`,
|
|
1336
|
+
`${downloadId}::${decodedPath}`,
|
|
1337
|
+
`${downloadId}:${decodedPath}`,
|
|
1338
|
+
`${downloadId}::${stripExt}`,
|
|
1339
|
+
`${downloadId}:${stripExt}`,
|
|
1340
|
+
decodedPath,
|
|
1341
|
+
]);
|
|
1342
|
+
}
|
|
1343
|
+
// ---------------------------------------------------------------------------
|
|
1344
|
+
// Model candidate keys
|
|
1345
|
+
// ---------------------------------------------------------------------------
|
|
1346
|
+
/**
|
|
1347
|
+
* Model keys are `{styleId}::model::{name}`. Try the resolved style id first,
|
|
1348
|
+
* then the bare downloadId in case the request came through the region-scoped
|
|
1349
|
+
* URL form (`idb://{regionId}/model/{name}`).
|
|
1350
|
+
*/
|
|
1351
|
+
function modelCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1352
|
+
return dedupe([
|
|
1353
|
+
`${actualStyleId}::model::${decodedPath}`,
|
|
1354
|
+
`${downloadId}::model::${decodedPath}`,
|
|
1355
|
+
]);
|
|
1356
|
+
}
|
|
1357
|
+
// ---------------------------------------------------------------------------
|
|
1358
|
+
// TileJSON source matching
|
|
1359
|
+
// ---------------------------------------------------------------------------
|
|
1360
|
+
/**
|
|
1361
|
+
* Mapbox GL requests tilejson via `idb://{downloadId}/tilesjson/{path}` where
|
|
1362
|
+
* `{path}` may be the source id, the original TileJSON URL, or the URL we
|
|
1363
|
+
* stashed under `__originalTilesetUrl` when patching for offline. Try all
|
|
1364
|
+
* three; return the matching source id + its config, or null.
|
|
1365
|
+
*/
|
|
1366
|
+
function matchTileJsonSource(sources, decodedPath) {
|
|
1367
|
+
const asConfig = (v) => v && typeof v === 'object' ? v : null;
|
|
1368
|
+
if (decodedPath in sources) {
|
|
1369
|
+
const config = asConfig(sources[decodedPath]);
|
|
1370
|
+
if (config)
|
|
1371
|
+
return { sourceId: decodedPath, config };
|
|
1372
|
+
}
|
|
1373
|
+
for (const [sourceId, raw] of Object.entries(sources)) {
|
|
1374
|
+
const config = asConfig(raw);
|
|
1375
|
+
if (!config)
|
|
1376
|
+
continue;
|
|
1377
|
+
const url = typeof config.url === 'string' ? config.url : undefined;
|
|
1378
|
+
const original = typeof config.__originalTilesetUrl === 'string'
|
|
1379
|
+
? config.__originalTilesetUrl
|
|
1380
|
+
: undefined;
|
|
1381
|
+
if (url === decodedPath || original === decodedPath) {
|
|
1382
|
+
return { sourceId, config };
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Build the offline TileJSON payload that replaces the one Mapbox would
|
|
1389
|
+
* have fetched from the network. `tiles` is rewritten to serve from the SW
|
|
1390
|
+
* (the caller supplies the scheme via `tileUrlScheme`); copyable TileJSON
|
|
1391
|
+
* fields are preserved.
|
|
1392
|
+
*/
|
|
1393
|
+
function buildOfflineTileJson(sourceConfig, downloadId, sourceId, extension, tileUrlScheme, origin) {
|
|
1394
|
+
const base = `idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`
|
|
1395
|
+
;
|
|
1396
|
+
const tileJson = {
|
|
1397
|
+
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1398
|
+
name: sourceConfig.name ?? sourceId,
|
|
1399
|
+
tiles: [base],
|
|
1400
|
+
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1401
|
+
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1402
|
+
};
|
|
1403
|
+
const copyable = [
|
|
1404
|
+
'bounds',
|
|
1405
|
+
'center',
|
|
1406
|
+
'vector_layers',
|
|
1407
|
+
'scheme',
|
|
1408
|
+
'attribution',
|
|
1409
|
+
'encoding',
|
|
1410
|
+
'format',
|
|
1411
|
+
'grids',
|
|
1412
|
+
'data',
|
|
1413
|
+
'template',
|
|
1414
|
+
'version',
|
|
1415
|
+
];
|
|
1416
|
+
for (const field of copyable) {
|
|
1417
|
+
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1418
|
+
tileJson[field] = sourceConfig[field];
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
return tileJson;
|
|
1422
|
+
}
|
|
1423
|
+
// ---------------------------------------------------------------------------
|
|
1424
|
+
// Internal helpers
|
|
1425
|
+
// ---------------------------------------------------------------------------
|
|
1426
|
+
function dedupe(values) {
|
|
1427
|
+
return Array.from(new Set(values));
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1203
1430
|
// idbFetchHandler.ts
|
|
1204
1431
|
// Intercepts idb:// URLs and serves resources from IndexedDB for MapLibre GL offline mode
|
|
1205
1432
|
const idbLogger = logger.scope('IDBFetch');
|
|
@@ -1256,16 +1483,11 @@
|
|
|
1256
1483
|
}
|
|
1257
1484
|
try {
|
|
1258
1485
|
const allStyles = await db.getAll('styles');
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
// Cache the result
|
|
1265
|
-
regionToStyleCache.set(regionId, { styleEntry, timestamp: Date.now() });
|
|
1266
|
-
return styleEntry;
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1486
|
+
const hit = findStyleByRegionIdIn(allStyles, regionId);
|
|
1487
|
+
if (hit) {
|
|
1488
|
+
idbLogger.debug(`Found style "${hit.key}" containing region: ${regionId}`);
|
|
1489
|
+
regionToStyleCache.set(regionId, { styleEntry: hit, timestamp: Date.now() });
|
|
1490
|
+
return hit;
|
|
1269
1491
|
}
|
|
1270
1492
|
idbLogger.debug(`No style found containing region: ${regionId}`);
|
|
1271
1493
|
// Don't cache negative results — the region may be stored moments later
|
|
@@ -1277,36 +1499,6 @@
|
|
|
1277
1499
|
return null;
|
|
1278
1500
|
}
|
|
1279
1501
|
}
|
|
1280
|
-
function buildOfflineTileJson(sourceConfig, downloadId, sourceId) {
|
|
1281
|
-
const extension = deriveTileExtension(sourceConfig.tiles);
|
|
1282
|
-
const offlineTiles = [`idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`];
|
|
1283
|
-
const tileJson = {
|
|
1284
|
-
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1285
|
-
name: sourceConfig.name ?? sourceId,
|
|
1286
|
-
tiles: offlineTiles,
|
|
1287
|
-
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1288
|
-
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1289
|
-
};
|
|
1290
|
-
const fieldsToCopy = [
|
|
1291
|
-
'bounds',
|
|
1292
|
-
'center',
|
|
1293
|
-
'vector_layers',
|
|
1294
|
-
'scheme',
|
|
1295
|
-
'attribution',
|
|
1296
|
-
'encoding',
|
|
1297
|
-
'format',
|
|
1298
|
-
'grids',
|
|
1299
|
-
'data',
|
|
1300
|
-
'template',
|
|
1301
|
-
'version',
|
|
1302
|
-
];
|
|
1303
|
-
for (const field of fieldsToCopy) {
|
|
1304
|
-
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1305
|
-
tileJson[field] = sourceConfig[field];
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
return tileJson;
|
|
1309
|
-
}
|
|
1310
1502
|
async function createTileResponse(resource) {
|
|
1311
1503
|
const headers = {};
|
|
1312
1504
|
// Set proper content type for vector tiles (PBF/MVT format)
|
|
@@ -1401,7 +1593,7 @@
|
|
|
1401
1593
|
// but tiles are stored with integer zoom levels, so floor the value
|
|
1402
1594
|
const z = Math.floor(parseFloat(pathParts[pathParts.length - 3]));
|
|
1403
1595
|
const sourceKey = pathParts.slice(0, pathParts.length - 3).join('/');
|
|
1404
|
-
const yMatch = yExt.match(
|
|
1596
|
+
const yMatch = yExt.match(/^(\d+)\.(\w+)$/);
|
|
1405
1597
|
if (yMatch) {
|
|
1406
1598
|
const y = parseInt(yMatch[1]);
|
|
1407
1599
|
const requestedExt = yMatch[2]; // Extension from URL (for logging only)
|
|
@@ -1416,7 +1608,7 @@
|
|
|
1416
1608
|
}
|
|
1417
1609
|
idbLogger.debug(`Tile not found: ${tileKey}`);
|
|
1418
1610
|
// Fallback: try common alternative extensions
|
|
1419
|
-
const fallbackExtensions =
|
|
1611
|
+
const fallbackExtensions = tileFallbackExtensions(requestedExt);
|
|
1420
1612
|
for (const fallbackExt of fallbackExtensions) {
|
|
1421
1613
|
const fallbackKey = createTileKey(x, y, z, actualStyleId, sourceKey, fallbackExt);
|
|
1422
1614
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1458,7 +1650,7 @@
|
|
|
1458
1650
|
return await createTileResponse(resource);
|
|
1459
1651
|
}
|
|
1460
1652
|
// Try alternative extensions
|
|
1461
|
-
const fallbackExts =
|
|
1653
|
+
const fallbackExts = tileFallbackExtensions(ext);
|
|
1462
1654
|
for (const fallbackExt of fallbackExts) {
|
|
1463
1655
|
const fallbackKey = createTileKey(parseInt(x), parseInt(y), parseInt(z), actualStyleId, fallbackSourceKey, fallbackExt);
|
|
1464
1656
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1478,46 +1670,19 @@
|
|
|
1478
1670
|
}
|
|
1479
1671
|
case 'glyph': {
|
|
1480
1672
|
idbLogger.debug(`Looking for glyph with key: ${key}`);
|
|
1481
|
-
// Find which style this region belongs to
|
|
1482
1673
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1483
1674
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1484
|
-
|
|
1485
|
-
idbLogger.debug(`Region "${downloadId}" belongs to style "${actualStyleId}", searching with style key`);
|
|
1486
|
-
}
|
|
1487
|
-
// Parse the resource path: "FontA,FontB,FontC/0-255.pbf"
|
|
1488
|
-
// MapLibre requests glyphs with comma-separated fallback fonts
|
|
1489
|
-
// but glyphs are stored individually per font
|
|
1490
|
-
const pathParts = decodedResourcePath.split('/');
|
|
1491
|
-
const fontstackPart = pathParts[0]; // "FontA,FontB,FontC"
|
|
1492
|
-
const rangePart = pathParts[1] || '0-255.pbf'; // "0-255.pbf"
|
|
1493
|
-
// Split comma-separated fonts
|
|
1494
|
-
const fontstacks = fontstackPart.split(',').map(f => f.trim());
|
|
1675
|
+
const { fontstacks, rangePart } = parseGlyphPath(decodedResourcePath);
|
|
1495
1676
|
idbLogger.debug(`Trying ${fontstacks.length} fonts in fallback order: ${fontstacks.join(', ')}`);
|
|
1496
|
-
// Try each font in order (this is how font fallbacks work)
|
|
1497
1677
|
for (const fontstack of fontstacks) {
|
|
1498
|
-
const
|
|
1499
|
-
const
|
|
1500
|
-
const glyphCandidateKeys = [
|
|
1501
|
-
// Try with actual style ID first
|
|
1502
|
-
`${actualStyleId}::${normalizedPath}`,
|
|
1503
|
-
`${actualStyleId}::${glyphPath}`,
|
|
1504
|
-
// Then try with download ID
|
|
1505
|
-
`${downloadId}::${normalizedPath}`,
|
|
1506
|
-
`${downloadId}::${glyphPath}`,
|
|
1507
|
-
// Just paths
|
|
1508
|
-
normalizedPath,
|
|
1509
|
-
glyphPath,
|
|
1510
|
-
];
|
|
1511
|
-
idbLogger.debug(`Trying keys for font "${fontstack}":`, glyphCandidateKeys);
|
|
1512
|
-
for (const candidateKey of glyphCandidateKeys) {
|
|
1678
|
+
const candidateKeys = glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart);
|
|
1679
|
+
for (const candidateKey of candidateKeys) {
|
|
1513
1680
|
const resource = await db.get('glyphs', candidateKey);
|
|
1514
1681
|
if (resource?.data) {
|
|
1515
1682
|
idbLogger.debug(`Found glyph using key: ${candidateKey} (font: ${fontstack})`);
|
|
1516
1683
|
return new Response(resource.data, {
|
|
1517
1684
|
status: 200,
|
|
1518
|
-
headers: {
|
|
1519
|
-
'Content-Type': 'application/x-protobuf',
|
|
1520
|
-
},
|
|
1685
|
+
headers: { 'Content-Type': 'application/x-protobuf' },
|
|
1521
1686
|
});
|
|
1522
1687
|
}
|
|
1523
1688
|
}
|
|
@@ -1527,33 +1692,11 @@
|
|
|
1527
1692
|
}
|
|
1528
1693
|
case 'sprite': {
|
|
1529
1694
|
idbLogger.debug(`Looking for sprite with key: ${key}`);
|
|
1530
|
-
// Find which style this region belongs to
|
|
1531
1695
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1532
1696
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
// The sprite service stores sprites with keys like: "voyager::sprite.json", "voyager::sprite@2x.json"
|
|
1537
|
-
// MapLibre requests sprites as: "idb://region_XXX/sprite/sprite@2x.json"
|
|
1538
|
-
// So we need to map the region ID to the style ID
|
|
1539
|
-
const spriteCandidateKeys = Array.from(new Set([
|
|
1540
|
-
// Try with actual style ID first (most likely to work)
|
|
1541
|
-
`${actualStyleId}::${decodedResourcePath}`,
|
|
1542
|
-
`${actualStyleId}:${decodedResourcePath}`,
|
|
1543
|
-
`${actualStyleId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1544
|
-
`${actualStyleId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1545
|
-
// Then try with download ID (in case it's a direct style download)
|
|
1546
|
-
`${downloadId}::${decodedResourcePath}`,
|
|
1547
|
-
`${downloadId}:${decodedResourcePath}`,
|
|
1548
|
-
`${downloadId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1549
|
-
`${downloadId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1550
|
-
// Just the path itself
|
|
1551
|
-
decodedResourcePath,
|
|
1552
|
-
// Original key format
|
|
1553
|
-
key,
|
|
1554
|
-
]));
|
|
1555
|
-
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, spriteCandidateKeys);
|
|
1556
|
-
for (const candidateKey of spriteCandidateKeys) {
|
|
1697
|
+
const candidates = spriteCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1698
|
+
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, candidates);
|
|
1699
|
+
for (const candidateKey of candidates) {
|
|
1557
1700
|
const resource = await db.get('sprites', candidateKey);
|
|
1558
1701
|
if (resource?.data) {
|
|
1559
1702
|
idbLogger.debug(`Found sprite using key: ${candidateKey}`);
|
|
@@ -1563,7 +1706,7 @@
|
|
|
1563
1706
|
});
|
|
1564
1707
|
}
|
|
1565
1708
|
}
|
|
1566
|
-
idbLogger.warn(`Sprite not found, tried keys: ${
|
|
1709
|
+
idbLogger.warn(`Sprite not found, tried keys: ${candidates.join(', ')}`);
|
|
1567
1710
|
break;
|
|
1568
1711
|
}
|
|
1569
1712
|
case 'font': {
|
|
@@ -1579,18 +1722,12 @@
|
|
|
1579
1722
|
break;
|
|
1580
1723
|
}
|
|
1581
1724
|
case 'model': {
|
|
1582
|
-
// Model URLs are rewritten by patchStyleForOffline to
|
|
1725
|
+
// Model URLs are rewritten by patchStyleForOffline to
|
|
1583
1726
|
// idb://{styleId}/model/{modelName}
|
|
1584
1727
|
// Models are keyed by {styleId}::model::{modelName} in the store.
|
|
1585
|
-
// Mirror the sprite resolution fallback: try the style ID first,
|
|
1586
|
-
// then the download/region ID (in case the request came through a
|
|
1587
|
-
// region-scoped URL).
|
|
1588
1728
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1589
1729
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1590
|
-
const candidates =
|
|
1591
|
-
`${actualStyleId}::model::${decodedResourcePath}`,
|
|
1592
|
-
`${downloadId}::model::${decodedResourcePath}`,
|
|
1593
|
-
]));
|
|
1730
|
+
const candidates = modelCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1594
1731
|
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1595
1732
|
for (const candidateKey of candidates) {
|
|
1596
1733
|
const resource = await db.get('models', candidateKey);
|
|
@@ -1607,9 +1744,9 @@
|
|
|
1607
1744
|
}
|
|
1608
1745
|
case 'tilesjson': {
|
|
1609
1746
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1610
|
-
// First try direct lookup (for style-level downloads)
|
|
1747
|
+
// First try direct lookup (for style-level downloads), then fall back
|
|
1748
|
+
// to searching by region ID (for region-level downloads).
|
|
1611
1749
|
let styleEntry = await db.get('styles', downloadId);
|
|
1612
|
-
// If not found, search by region ID (for region-level downloads)
|
|
1613
1750
|
if (!styleEntry || !styleEntry.style?.sources) {
|
|
1614
1751
|
idbLogger.debug(`Style not found with key "${downloadId}", searching by region ID...`);
|
|
1615
1752
|
const foundStyle = await findStyleByRegionId(db, downloadId);
|
|
@@ -1617,41 +1754,23 @@
|
|
|
1617
1754
|
styleEntry = foundStyle;
|
|
1618
1755
|
}
|
|
1619
1756
|
}
|
|
1620
|
-
if (styleEntry?.style?.sources) {
|
|
1621
|
-
const sources = styleEntry.style.sources;
|
|
1622
|
-
let matchedSourceId;
|
|
1623
|
-
let matchedSourceConfig;
|
|
1624
|
-
if (decodedResourcePath in sources) {
|
|
1625
|
-
matchedSourceId = decodedResourcePath;
|
|
1626
|
-
matchedSourceConfig = sources[decodedResourcePath];
|
|
1627
|
-
}
|
|
1628
|
-
else {
|
|
1629
|
-
for (const [sourceId, sourceValue] of Object.entries(sources)) {
|
|
1630
|
-
const sourceUrl = typeof sourceValue.url === 'string' ? sourceValue.url : undefined;
|
|
1631
|
-
const originalUrl = typeof sourceValue.__originalTilesetUrl === 'string'
|
|
1632
|
-
? sourceValue.__originalTilesetUrl
|
|
1633
|
-
: undefined;
|
|
1634
|
-
if (sourceUrl === decodedResourcePath || originalUrl === decodedResourcePath) {
|
|
1635
|
-
matchedSourceId = sourceId;
|
|
1636
|
-
matchedSourceConfig = sourceValue;
|
|
1637
|
-
break;
|
|
1638
|
-
}
|
|
1639
|
-
}
|
|
1640
|
-
}
|
|
1641
|
-
if (matchedSourceId && matchedSourceConfig) {
|
|
1642
|
-
const tileJson = buildOfflineTileJson(matchedSourceConfig, downloadId, matchedSourceId);
|
|
1643
|
-
idbLogger.debug(`Serving offline tilejson for source: ${matchedSourceId}`);
|
|
1644
|
-
return new Response(JSON.stringify(tileJson), {
|
|
1645
|
-
status: 200,
|
|
1646
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1647
|
-
});
|
|
1648
|
-
}
|
|
1649
|
-
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1650
|
-
}
|
|
1651
|
-
else {
|
|
1757
|
+
if (!styleEntry?.style?.sources) {
|
|
1652
1758
|
idbLogger.warn(`Style not found or missing sources for downloadId: ${downloadId}`);
|
|
1759
|
+
break;
|
|
1653
1760
|
}
|
|
1654
|
-
|
|
1761
|
+
const sources = styleEntry.style.sources;
|
|
1762
|
+
const matched = matchTileJsonSource(sources, decodedResourcePath);
|
|
1763
|
+
if (!matched) {
|
|
1764
|
+
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1765
|
+
break;
|
|
1766
|
+
}
|
|
1767
|
+
const extension = deriveTileExtension(matched.config.tiles);
|
|
1768
|
+
const tileJson = buildOfflineTileJson(matched.config, downloadId, matched.sourceId, extension, 'idb');
|
|
1769
|
+
idbLogger.debug(`Serving offline tilejson for source: ${matched.sourceId}`);
|
|
1770
|
+
return new Response(JSON.stringify(tileJson), {
|
|
1771
|
+
status: 200,
|
|
1772
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1773
|
+
});
|
|
1655
1774
|
}
|
|
1656
1775
|
default:
|
|
1657
1776
|
idbLogger.warn(`Unknown resource type: ${type}`);
|
|
@@ -1675,6 +1794,37 @@
|
|
|
1675
1794
|
function isMapboxProtocol(url) {
|
|
1676
1795
|
return url.startsWith(MAPBOX_API.PROTOCOL);
|
|
1677
1796
|
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Parse a URL and return its hostname, or null if the URL is malformed.
|
|
1799
|
+
* Accepts relative URLs when `base` is provided.
|
|
1800
|
+
*/
|
|
1801
|
+
function getUrlHostname(url, base) {
|
|
1802
|
+
try {
|
|
1803
|
+
return new URL(url, base).hostname.toLowerCase();
|
|
1804
|
+
}
|
|
1805
|
+
catch {
|
|
1806
|
+
return null;
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* True if `url`'s hostname equals `host` or is a subdomain of `host`.
|
|
1811
|
+
* Uses URL parsing (not substring matching) to avoid false positives like
|
|
1812
|
+
* `https://evil.com/?x=mapbox.com` matching `mapbox.com`.
|
|
1813
|
+
*/
|
|
1814
|
+
function hostMatches(url, host, base) {
|
|
1815
|
+
const hostname = getUrlHostname(url, base);
|
|
1816
|
+
if (hostname === null)
|
|
1817
|
+
return false;
|
|
1818
|
+
const target = host.toLowerCase();
|
|
1819
|
+
return hostname === target || hostname.endsWith('.' + target);
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* True for any host under the mapbox.com domain (including api.mapbox.com,
|
|
1823
|
+
* *.tiles.mapbox.com, etc.). Used by provider detection.
|
|
1824
|
+
*/
|
|
1825
|
+
function isMapboxHost(url, base) {
|
|
1826
|
+
return hostMatches(url, 'mapbox.com', base);
|
|
1827
|
+
}
|
|
1678
1828
|
/**
|
|
1679
1829
|
* Resolve a mapbox:// URL to its HTTPS API equivalent
|
|
1680
1830
|
*
|
|
@@ -1748,9 +1898,7 @@
|
|
|
1748
1898
|
*/
|
|
1749
1899
|
function detectStyleProvider(styleUrl, style) {
|
|
1750
1900
|
// Check URL patterns
|
|
1751
|
-
if (isMapboxProtocol(styleUrl) ||
|
|
1752
|
-
styleUrl.includes('mapbox.com') ||
|
|
1753
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
1901
|
+
if (isMapboxProtocol(styleUrl) || isMapboxHost(styleUrl)) {
|
|
1754
1902
|
return 'mapbox';
|
|
1755
1903
|
}
|
|
1756
1904
|
if (styleUrl.includes('maplibre') ||
|
|
@@ -1769,7 +1917,7 @@
|
|
|
1769
1917
|
const sources = style.sources || {};
|
|
1770
1918
|
for (const [, sourceConfig] of Object.entries(sources)) {
|
|
1771
1919
|
const source = sourceConfig;
|
|
1772
|
-
if (source.url && (source.url
|
|
1920
|
+
if (source.url && (isMapboxProtocol(source.url) || isMapboxHost(source.url))) {
|
|
1773
1921
|
return 'mapbox';
|
|
1774
1922
|
}
|
|
1775
1923
|
}
|
|
@@ -1861,7 +2009,7 @@
|
|
|
1861
2009
|
if (isMapboxProtocol(tileUrl) && accessToken) {
|
|
1862
2010
|
return resolveMapboxUrl(tileUrl, accessToken);
|
|
1863
2011
|
}
|
|
1864
|
-
if (provider === 'mapbox' && accessToken && tileUrl
|
|
2012
|
+
if (provider === 'mapbox' && accessToken && isMapboxHost(tileUrl)) {
|
|
1865
2013
|
return normalizeStyleUrl(tileUrl, accessToken);
|
|
1866
2014
|
}
|
|
1867
2015
|
return tileUrl;
|
|
@@ -1876,7 +2024,7 @@
|
|
|
1876
2024
|
if (isMapboxProtocol(processedStyle.sprite)) {
|
|
1877
2025
|
processedStyle.sprite = resolveMapboxUrl(processedStyle.sprite, accessToken);
|
|
1878
2026
|
}
|
|
1879
|
-
else if (provider === 'mapbox' && processedStyle.sprite
|
|
2027
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.sprite)) {
|
|
1880
2028
|
processedStyle.sprite = normalizeStyleUrl(processedStyle.sprite, accessToken);
|
|
1881
2029
|
}
|
|
1882
2030
|
}
|
|
@@ -1887,7 +2035,7 @@
|
|
|
1887
2035
|
if (isMapboxProtocol(entry.url)) {
|
|
1888
2036
|
return { ...entry, url: resolveMapboxUrl(entry.url, accessToken) };
|
|
1889
2037
|
}
|
|
1890
|
-
else if (provider === 'mapbox' && entry.url
|
|
2038
|
+
else if (provider === 'mapbox' && isMapboxHost(entry.url)) {
|
|
1891
2039
|
return { ...entry, url: normalizeStyleUrl(entry.url, accessToken) };
|
|
1892
2040
|
}
|
|
1893
2041
|
}
|
|
@@ -1900,7 +2048,7 @@
|
|
|
1900
2048
|
if (isMapboxProtocol(processedStyle.glyphs)) {
|
|
1901
2049
|
processedStyle.glyphs = resolveMapboxUrl(processedStyle.glyphs, accessToken);
|
|
1902
2050
|
}
|
|
1903
|
-
else if (provider === 'mapbox' && processedStyle.glyphs
|
|
2051
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.glyphs)) {
|
|
1904
2052
|
processedStyle.glyphs = normalizeStyleUrl(processedStyle.glyphs, accessToken);
|
|
1905
2053
|
}
|
|
1906
2054
|
}
|
|
@@ -1940,13 +2088,20 @@
|
|
|
1940
2088
|
// Check for Mapbox-specific requirements
|
|
1941
2089
|
const hasMapboxSources = Object.values(style.sources || {}).some((source) => {
|
|
1942
2090
|
const s = source;
|
|
1943
|
-
return s.url && s.url
|
|
2091
|
+
return !!s.url && isMapboxHost(s.url);
|
|
1944
2092
|
});
|
|
1945
2093
|
if (hasMapboxSources) {
|
|
1946
2094
|
// Check if access token might be needed
|
|
1947
2095
|
const hasAccessToken = Object.values(style.sources || {}).some((source) => {
|
|
1948
2096
|
const s = source;
|
|
1949
|
-
|
|
2097
|
+
if (!s.url)
|
|
2098
|
+
return false;
|
|
2099
|
+
try {
|
|
2100
|
+
return new URL(s.url).searchParams.has('access_token');
|
|
2101
|
+
}
|
|
2102
|
+
catch {
|
|
2103
|
+
return false;
|
|
2104
|
+
}
|
|
1950
2105
|
});
|
|
1951
2106
|
if (!hasAccessToken) {
|
|
1952
2107
|
warnings.push('Mapbox sources detected but no access token found - authentication may be required');
|
|
@@ -1980,14 +2135,15 @@
|
|
|
1980
2135
|
styleLogger.debug(`Patching source: ${sourceKey}`, source);
|
|
1981
2136
|
if (source.tiles) {
|
|
1982
2137
|
const originalTiles = [...source.tiles];
|
|
1983
|
-
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext
|
|
2138
|
+
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext.
|
|
2139
|
+
// Extension extraction goes through the shared extractTileExtensionFromUrl
|
|
2140
|
+
// helper so the patched URL's extension matches what tileService used when
|
|
2141
|
+
// storing — otherwise Mapbox v4 tile URLs (`{y}.vector.pbf`) produced a
|
|
2142
|
+
// stored key under `.pbf` but a patched URL with `.vector`, forcing
|
|
2143
|
+
// idbFetchHandler to fall through its pbf/mvt/png/jpg/webp fallback loop
|
|
2144
|
+
// on every tile.
|
|
1984
2145
|
source.tiles = source.tiles.map((url) => {
|
|
1985
|
-
|
|
1986
|
-
let ext = tileExtension;
|
|
1987
|
-
if (!ext) {
|
|
1988
|
-
const extMatch = url.match(/\{z\}\/\{x\}\/\{y\}\.(\w+)/);
|
|
1989
|
-
ext = extMatch ? extMatch[1] : 'pbf';
|
|
1990
|
-
}
|
|
2146
|
+
const ext = tileExtension ?? extractTileExtensionFromUrl(url);
|
|
1991
2147
|
return `idb://${downloadId}/tile/${sourceKey}/{z}/{x}/{y}.${ext}`;
|
|
1992
2148
|
});
|
|
1993
2149
|
styleLogger.debug(`Patched tiles for ${sourceKey} with extension .${tileExtension || 'pbf'}:`, {
|
|
@@ -2865,10 +3021,8 @@
|
|
|
2865
3021
|
if (typeof prefixedLayer.source === 'string') {
|
|
2866
3022
|
prefixedLayer.source = `${importId}/${prefixedLayer.source}`;
|
|
2867
3023
|
}
|
|
2868
|
-
// Resolve ["config", "key"] expressions using schema defaults and import overrides
|
|
2869
|
-
|
|
2870
|
-
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2871
|
-
}
|
|
3024
|
+
// Resolve ["config", "key"] expressions using schema defaults and import overrides.
|
|
3025
|
+
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2872
3026
|
flattenedLayers.push(prefixedLayer);
|
|
2873
3027
|
}
|
|
2874
3028
|
}
|
|
@@ -2906,8 +3060,48 @@
|
|
|
2906
3060
|
if (!style.models && importedModels) {
|
|
2907
3061
|
style.models = importedModels;
|
|
2908
3062
|
}
|
|
3063
|
+
// Rewrite indoor-only expressions so the flattened style validates without
|
|
3064
|
+
// the `imports` wrapper at render time — see sanitizeIndoorExpressions.
|
|
3065
|
+
sanitizeIndoorExpressions(style);
|
|
2909
3066
|
return style;
|
|
2910
3067
|
}
|
|
3068
|
+
/**
|
|
3069
|
+
* Rewrite indoor-only expressions in a style's layers to their outdoor no-op
|
|
3070
|
+
* constants. See the in-line comment in `resolveValue` for why this is needed
|
|
3071
|
+
* for Mapbox Standard when the `imports` wrapper is stripped.
|
|
3072
|
+
*
|
|
3073
|
+
* Safe to call multiple times and on already-downloaded stored styles — the
|
|
3074
|
+
* rewrites are idempotent (after the first pass there are no more
|
|
3075
|
+
* `is-active-floor` / `floor-level` expressions to rewrite).
|
|
3076
|
+
*/
|
|
3077
|
+
function sanitizeIndoorExpressions(style) {
|
|
3078
|
+
const layers = style.layers;
|
|
3079
|
+
if (!Array.isArray(layers))
|
|
3080
|
+
return;
|
|
3081
|
+
for (const layer of layers) {
|
|
3082
|
+
if (layer && typeof layer === 'object') {
|
|
3083
|
+
rewriteIndoor(layer);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
function rewriteIndoor(obj) {
|
|
3088
|
+
for (const key of Object.keys(obj)) {
|
|
3089
|
+
obj[key] = rewriteIndoorValue(obj[key]);
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
function rewriteIndoorValue(value) {
|
|
3093
|
+
if (!Array.isArray(value)) {
|
|
3094
|
+
if (value && typeof value === 'object' && !ArrayBuffer.isView(value)) {
|
|
3095
|
+
rewriteIndoor(value);
|
|
3096
|
+
}
|
|
3097
|
+
return value;
|
|
3098
|
+
}
|
|
3099
|
+
if (value[0] === 'is-active-floor')
|
|
3100
|
+
return false;
|
|
3101
|
+
if (value[0] === 'floor-level' && value.length === 1)
|
|
3102
|
+
return 0;
|
|
3103
|
+
return value.map(rewriteIndoorValue);
|
|
3104
|
+
}
|
|
2911
3105
|
/**
|
|
2912
3106
|
* Deep clone a plain object/array (JSON-safe values only).
|
|
2913
3107
|
*/
|
|
@@ -3073,6 +3267,40 @@
|
|
|
3073
3267
|
return result;
|
|
3074
3268
|
}
|
|
3075
3269
|
|
|
3270
|
+
const DEFAULT_WASM_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.14.1/dist/';
|
|
3271
|
+
let currentConfig = {};
|
|
3272
|
+
let sqlJsPromise = null;
|
|
3273
|
+
/**
|
|
3274
|
+
* Override how `sql.js` loads its WebAssembly. Call once before any MBTiles
|
|
3275
|
+
* import/export is invoked. Resets any cached init.
|
|
3276
|
+
*/
|
|
3277
|
+
function configureSqlJs(config) {
|
|
3278
|
+
currentConfig = { ...config };
|
|
3279
|
+
sqlJsPromise = null;
|
|
3280
|
+
}
|
|
3281
|
+
/**
|
|
3282
|
+
* Lazily initialise `sql.js`. The underlying module is loaded via dynamic
|
|
3283
|
+
* `import()` so it only ships with bundles that actually call MBTiles code.
|
|
3284
|
+
*/
|
|
3285
|
+
async function getSqlJs() {
|
|
3286
|
+
if (sqlJsPromise)
|
|
3287
|
+
return sqlJsPromise;
|
|
3288
|
+
sqlJsPromise = (async () => {
|
|
3289
|
+
const mod = (await import('sql.js'));
|
|
3290
|
+
const initSqlJs = mod.default;
|
|
3291
|
+
const options = {};
|
|
3292
|
+
if (currentConfig.wasmBinary) {
|
|
3293
|
+
options.wasmBinary = currentConfig.wasmBinary;
|
|
3294
|
+
}
|
|
3295
|
+
else {
|
|
3296
|
+
const base = currentConfig.wasmUrl ?? DEFAULT_WASM_URL;
|
|
3297
|
+
options.locateFile = (file) => base.endsWith('/') ? `${base}${file}` : `${base}/${file}`;
|
|
3298
|
+
}
|
|
3299
|
+
return initSqlJs(options);
|
|
3300
|
+
})();
|
|
3301
|
+
return sqlJsPromise;
|
|
3302
|
+
}
|
|
3303
|
+
|
|
3076
3304
|
const fontLogger = logger.scope('FontService');
|
|
3077
3305
|
class FontService {
|
|
3078
3306
|
db = dbPromise;
|
|
@@ -3256,15 +3484,23 @@
|
|
|
3256
3484
|
},
|
|
3257
3485
|
};
|
|
3258
3486
|
}
|
|
3259
|
-
|
|
3487
|
+
/**
|
|
3488
|
+
* Delete fonts older than `maxAge` days. When `options.styleId` is
|
|
3489
|
+
* provided, only fonts belonging to that style (per the delimiter-aware
|
|
3490
|
+
* `resourceKeyBelongsToStyle` match) are eligible — callers relying on
|
|
3491
|
+
* a styleId filter previously got a silent full-store wipe.
|
|
3492
|
+
*/
|
|
3493
|
+
async cleanupOldFonts(maxAge = 30, options = {}) {
|
|
3260
3494
|
const db = await this.db;
|
|
3261
3495
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3496
|
+
const { styleId } = options;
|
|
3262
3497
|
const tx = db.transaction(['fonts'], 'readwrite');
|
|
3263
3498
|
let deletedCount = 0;
|
|
3264
3499
|
let cursor = await tx.objectStore('fonts').openCursor();
|
|
3265
3500
|
while (cursor) {
|
|
3266
3501
|
const fontEntry = cursor.value;
|
|
3267
|
-
|
|
3502
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(fontEntry.key, styleId);
|
|
3503
|
+
if (belongs && fontEntry.lastModified < cutoffTime) {
|
|
3268
3504
|
await cursor.delete();
|
|
3269
3505
|
deletedCount++;
|
|
3270
3506
|
}
|
|
@@ -3430,7 +3666,7 @@
|
|
|
3430
3666
|
const downloadFonts = (fontUrls, styleName, options) => fontService.downloadFonts(fontUrls, styleName, options);
|
|
3431
3667
|
const getFontStats = () => fontService.getFontStats();
|
|
3432
3668
|
const getFontAnalytics = () => fontService.getFontAnalytics();
|
|
3433
|
-
const cleanupOldFonts = (maxAge) => fontService.cleanupOldFonts(maxAge);
|
|
3669
|
+
const cleanupOldFonts = (maxAge, options) => fontService.cleanupOldFonts(maxAge, options);
|
|
3434
3670
|
const verifyAndRepairFonts = () => fontService.verifyAndRepairFonts();
|
|
3435
3671
|
|
|
3436
3672
|
const spriteLogger = logger.scope('SpriteService');
|
|
@@ -3741,19 +3977,24 @@
|
|
|
3741
3977
|
};
|
|
3742
3978
|
}
|
|
3743
3979
|
/**
|
|
3744
|
-
*
|
|
3980
|
+
* Remove sprites older than the specified age. When `options.styleId` is
|
|
3981
|
+
* provided, only sprites belonging to that style (per
|
|
3982
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
3745
3983
|
* @param maxAge - Maximum age in days (default: 30)
|
|
3984
|
+
* @param options.styleId - Optional style filter; omit to scan all styles
|
|
3746
3985
|
* @returns Promise resolving to number of deleted sprites
|
|
3747
3986
|
*/
|
|
3748
|
-
async cleanupOldSprites(maxAge = 30) {
|
|
3987
|
+
async cleanupOldSprites(maxAge = 30, options = {}) {
|
|
3749
3988
|
const db = await this.db;
|
|
3750
3989
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3990
|
+
const { styleId } = options;
|
|
3751
3991
|
const tx = db.transaction(['sprites'], 'readwrite');
|
|
3752
3992
|
let deletedCount = 0;
|
|
3753
3993
|
let cursor = await tx.objectStore('sprites').openCursor();
|
|
3754
3994
|
while (cursor) {
|
|
3755
3995
|
const spriteEntry = cursor.value;
|
|
3756
|
-
|
|
3996
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(spriteEntry.key, styleId);
|
|
3997
|
+
if (belongs && spriteEntry.lastModified < cutoffTime) {
|
|
3757
3998
|
await cursor.delete();
|
|
3758
3999
|
deletedCount++;
|
|
3759
4000
|
}
|
|
@@ -3907,7 +4148,7 @@
|
|
|
3907
4148
|
const downloadSprites = (spriteUrls, styleName, options) => spriteService.downloadSprites(spriteUrls, styleName, options);
|
|
3908
4149
|
const getSpriteStats = () => spriteService.getSpriteStats();
|
|
3909
4150
|
const getSpriteAnalytics = () => spriteService.getSpriteAnalytics();
|
|
3910
|
-
const cleanupOldSprites = (maxAge) => spriteService.cleanupOldSprites(maxAge);
|
|
4151
|
+
const cleanupOldSprites = (maxAge, options) => spriteService.cleanupOldSprites(maxAge, options);
|
|
3911
4152
|
const verifyAndRepairSprites = () => spriteService.verifyAndRepairSprites();
|
|
3912
4153
|
|
|
3913
4154
|
var spriteService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -5178,7 +5419,17 @@
|
|
|
5178
5419
|
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5179
5420
|
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5180
5421
|
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5181
|
-
|
|
5422
|
+
let isMapboxStandardSprite = false;
|
|
5423
|
+
try {
|
|
5424
|
+
const parsed = new URL(pathWithoutQuery);
|
|
5425
|
+
isMapboxStandardSprite =
|
|
5426
|
+
parsed.hostname === 'api.mapbox.com' &&
|
|
5427
|
+
parsed.pathname.startsWith('/styles/v1/') &&
|
|
5428
|
+
parsed.pathname.endsWith('/sprite');
|
|
5429
|
+
}
|
|
5430
|
+
catch {
|
|
5431
|
+
// Non-URL sprite base (e.g. relative); not a Mapbox Standard sprite.
|
|
5432
|
+
}
|
|
5182
5433
|
if (isMapboxStandardSprite) {
|
|
5183
5434
|
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5184
5435
|
suffixes.push('__ICONSET__');
|
|
@@ -6674,8 +6925,7 @@
|
|
|
6674
6925
|
}
|
|
6675
6926
|
}
|
|
6676
6927
|
extractExtension(template) {
|
|
6677
|
-
|
|
6678
|
-
return extMatch ? extMatch[1] : 'pbf';
|
|
6928
|
+
return extractTileExtensionFromUrl(template);
|
|
6679
6929
|
}
|
|
6680
6930
|
selectTileTemplate(templates, coord) {
|
|
6681
6931
|
if (templates.length === 1) {
|
|
@@ -7057,14 +7307,21 @@
|
|
|
7057
7307
|
},
|
|
7058
7308
|
};
|
|
7059
7309
|
}
|
|
7060
|
-
|
|
7310
|
+
/**
|
|
7311
|
+
* Remove glyphs older than the specified age. When `options.styleId` is
|
|
7312
|
+
* provided, only glyphs belonging to that style (per
|
|
7313
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
7314
|
+
*/
|
|
7315
|
+
async cleanupOldGlyphs(maxAge = 30, options = {}) {
|
|
7061
7316
|
const db = await this.db;
|
|
7062
7317
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7318
|
+
const { styleId } = options;
|
|
7063
7319
|
let deletedCount = 0;
|
|
7064
7320
|
const tx = db.transaction('glyphs', 'readwrite');
|
|
7065
7321
|
for await (const cursor of tx.store) {
|
|
7066
7322
|
const glyphEntry = cursor.value;
|
|
7067
|
-
|
|
7323
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(glyphEntry.key, styleId);
|
|
7324
|
+
if (belongs && glyphEntry.lastModified < cutoffTime) {
|
|
7068
7325
|
await cursor.delete();
|
|
7069
7326
|
deletedCount++;
|
|
7070
7327
|
}
|
|
@@ -7167,7 +7424,7 @@
|
|
|
7167
7424
|
const loadGlyphs = (fontstack, ranges, styleName) => glyphService.loadGlyphs(fontstack, ranges, styleName);
|
|
7168
7425
|
const getGlyphStats = () => glyphService.getGlyphStats();
|
|
7169
7426
|
const getGlyphAnalytics = () => glyphService.getGlyphAnalytics();
|
|
7170
|
-
const cleanupOldGlyphs = (maxAge) => glyphService.cleanupOldGlyphs(maxAge);
|
|
7427
|
+
const cleanupOldGlyphs = (maxAge, options) => glyphService.cleanupOldGlyphs(maxAge, options);
|
|
7171
7428
|
const verifyAndRepairGlyphs = () => glyphService.verifyAndRepairGlyphs();
|
|
7172
7429
|
|
|
7173
7430
|
var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -7402,8 +7659,7 @@
|
|
|
7402
7659
|
return getFontAnalytics();
|
|
7403
7660
|
}
|
|
7404
7661
|
async cleanupOldFonts(styleId, options) {
|
|
7405
|
-
|
|
7406
|
-
return cleanupOldFonts(maxAge);
|
|
7662
|
+
return cleanupOldFonts(options?.maxAge, { styleId });
|
|
7407
7663
|
}
|
|
7408
7664
|
async verifyAndRepairFonts() {
|
|
7409
7665
|
return verifyAndRepairFonts();
|
|
@@ -7419,8 +7675,7 @@
|
|
|
7419
7675
|
return getSpriteAnalytics();
|
|
7420
7676
|
}
|
|
7421
7677
|
async cleanupOldSprites(styleId, options) {
|
|
7422
|
-
|
|
7423
|
-
return cleanupOldSprites(maxAge);
|
|
7678
|
+
return cleanupOldSprites(options?.maxAge, { styleId });
|
|
7424
7679
|
}
|
|
7425
7680
|
async verifyAndRepairSprites() {
|
|
7426
7681
|
return verifyAndRepairSprites();
|
|
@@ -7439,8 +7694,7 @@
|
|
|
7439
7694
|
return loadGlyphs(fontstack, ranges, styleId);
|
|
7440
7695
|
}
|
|
7441
7696
|
async cleanupOldGlyphs(styleId, options) {
|
|
7442
|
-
|
|
7443
|
-
return cleanupOldGlyphs(maxAge);
|
|
7697
|
+
return cleanupOldGlyphs(options?.maxAge, { styleId });
|
|
7444
7698
|
}
|
|
7445
7699
|
async verifyAndRepairGlyphs() {
|
|
7446
7700
|
return verifyAndRepairGlyphs();
|
|
@@ -7555,6 +7809,88 @@
|
|
|
7555
7809
|
}
|
|
7556
7810
|
}
|
|
7557
7811
|
|
|
7812
|
+
/**
|
|
7813
|
+
* MBTiles uses TMS tile_row ordering; our storage uses XYZ y. Flip across
|
|
7814
|
+
* either direction with the same formula.
|
|
7815
|
+
*/
|
|
7816
|
+
function flipY(y, z) {
|
|
7817
|
+
return (1 << z) - 1 - y;
|
|
7818
|
+
}
|
|
7819
|
+
/** Vector tile formats that downstream consumers (QGIS, maplibre-native) expect gzipped. */
|
|
7820
|
+
const VECTOR_FORMATS = new Set(['pbf', 'mvt']);
|
|
7821
|
+
function hasGzipMagic(bytes) {
|
|
7822
|
+
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
7823
|
+
}
|
|
7824
|
+
async function drainReadable(readable) {
|
|
7825
|
+
const reader = readable.getReader();
|
|
7826
|
+
const chunks = [];
|
|
7827
|
+
let total = 0;
|
|
7828
|
+
while (true) {
|
|
7829
|
+
const { done, value } = await reader.read();
|
|
7830
|
+
if (done)
|
|
7831
|
+
break;
|
|
7832
|
+
if (value) {
|
|
7833
|
+
chunks.push(value);
|
|
7834
|
+
total += value.byteLength;
|
|
7835
|
+
}
|
|
7836
|
+
}
|
|
7837
|
+
const out = new Uint8Array(total);
|
|
7838
|
+
let offset = 0;
|
|
7839
|
+
for (const chunk of chunks) {
|
|
7840
|
+
out.set(chunk, offset);
|
|
7841
|
+
offset += chunk.byteLength;
|
|
7842
|
+
}
|
|
7843
|
+
return out;
|
|
7844
|
+
}
|
|
7845
|
+
async function transformBytes(bytes, transform) {
|
|
7846
|
+
const writer = transform.writable.getWriter();
|
|
7847
|
+
// Don't await — the read loop below drives the pipe and we only want
|
|
7848
|
+
// the final bytes, not back-pressure handling for a single chunk.
|
|
7849
|
+
void writer.write(bytes);
|
|
7850
|
+
void writer.close();
|
|
7851
|
+
return drainReadable(transform.readable);
|
|
7852
|
+
}
|
|
7853
|
+
async function gzipBytes(bytes) {
|
|
7854
|
+
return transformBytes(bytes, new CompressionStream('gzip'));
|
|
7855
|
+
}
|
|
7856
|
+
async function gunzipBytes(bytes) {
|
|
7857
|
+
return transformBytes(bytes, new DecompressionStream('gzip'));
|
|
7858
|
+
}
|
|
7859
|
+
/**
|
|
7860
|
+
* Build the MBTiles `json` metadata payload. For vector tiles this is
|
|
7861
|
+
* mandatory for tippecanoe/QGIS/maplibre-native to render — they read
|
|
7862
|
+
* `vector_layers` from here.
|
|
7863
|
+
*
|
|
7864
|
+
* `vector_layers` is inferred from the offline style's vector sources
|
|
7865
|
+
* (populated by the TileJSON expansion step in styleService). Multiple
|
|
7866
|
+
* vector sources are merged; duplicates de-duped by id, first wins.
|
|
7867
|
+
*/
|
|
7868
|
+
function buildVectorJsonMetadata(style, sourceIds) {
|
|
7869
|
+
if (!style || typeof style !== 'object')
|
|
7870
|
+
return null;
|
|
7871
|
+
const sources = style.sources;
|
|
7872
|
+
if (!sources)
|
|
7873
|
+
return null;
|
|
7874
|
+
const merged = [];
|
|
7875
|
+
const seen = new Set();
|
|
7876
|
+
for (const [id, src] of Object.entries(sources)) {
|
|
7877
|
+
if (sourceIds.size > 0 && !sourceIds.has(id))
|
|
7878
|
+
continue;
|
|
7879
|
+
const layers = src?.vector_layers;
|
|
7880
|
+
if (!Array.isArray(layers))
|
|
7881
|
+
continue;
|
|
7882
|
+
for (const layer of layers) {
|
|
7883
|
+
const layerId = typeof layer?.id === 'string' ? layer.id : null;
|
|
7884
|
+
if (!layerId || seen.has(layerId))
|
|
7885
|
+
continue;
|
|
7886
|
+
seen.add(layerId);
|
|
7887
|
+
merged.push(layer);
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
if (merged.length === 0)
|
|
7891
|
+
return null;
|
|
7892
|
+
return JSON.stringify({ vector_layers: merged });
|
|
7893
|
+
}
|
|
7558
7894
|
const serviceLogger = logger.scope('ImportExportService');
|
|
7559
7895
|
class ImportExportService {
|
|
7560
7896
|
db = dbPromise;
|
|
@@ -7562,270 +7898,173 @@
|
|
|
7562
7898
|
// No need for initialization since dbPromise is already available
|
|
7563
7899
|
}
|
|
7564
7900
|
/**
|
|
7565
|
-
* Export a
|
|
7901
|
+
* Export region as a real binary MBTiles SQLite file.
|
|
7902
|
+
*
|
|
7903
|
+
* Produces a v1.3-compliant MBTiles archive: `metadata` + `tiles` tables,
|
|
7904
|
+
* with `tile_row` flipped to TMS ordering. The resulting blob can be read
|
|
7905
|
+
* by tippecanoe, QGIS, maplibre-native, etc.
|
|
7566
7906
|
*/
|
|
7567
|
-
async
|
|
7907
|
+
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7568
7908
|
const onProgress = options.onProgress || (() => { });
|
|
7569
7909
|
try {
|
|
7570
7910
|
onProgress({
|
|
7571
7911
|
stage: 'preparing',
|
|
7572
7912
|
percentage: 0,
|
|
7573
|
-
message: 'Preparing export...',
|
|
7913
|
+
message: 'Preparing MBTiles export...',
|
|
7574
7914
|
});
|
|
7575
|
-
// Get region metadata
|
|
7576
7915
|
const region = await this.getRegionMetadata(regionId);
|
|
7577
7916
|
if (!region) {
|
|
7578
7917
|
throw new Error(`Region ${regionId} not found`);
|
|
7579
7918
|
}
|
|
7919
|
+
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7920
|
+
// Pick format: caller override → region.tileExtension → default pbf.
|
|
7921
|
+
// Drives both the metadata row and whether tile bytes get gzipped.
|
|
7922
|
+
const format = String(options.format || region.tileExtension || 'pbf').toLowerCase();
|
|
7923
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
7580
7924
|
onProgress({
|
|
7581
|
-
stage: '
|
|
7582
|
-
percentage:
|
|
7583
|
-
message: '
|
|
7925
|
+
stage: 'processing',
|
|
7926
|
+
percentage: 75,
|
|
7927
|
+
message: isVector ? 'Compressing vector tiles...' : 'Packing SQLite database...',
|
|
7584
7928
|
});
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
createdAt: region.created, // StoredRegion uses 'created' not 'createdAt'
|
|
7595
|
-
exportedAt: Date.now(),
|
|
7596
|
-
version: '1.0.0',
|
|
7597
|
-
format: 'json',
|
|
7598
|
-
},
|
|
7599
|
-
style: {},
|
|
7600
|
-
tiles: [],
|
|
7601
|
-
sprites: [],
|
|
7602
|
-
fonts: [],
|
|
7603
|
-
};
|
|
7604
|
-
// Export style if requested
|
|
7605
|
-
if (options.includeStyle !== false) {
|
|
7606
|
-
onProgress({
|
|
7607
|
-
stage: 'exporting',
|
|
7608
|
-
percentage: 20,
|
|
7609
|
-
message: 'Exporting style data...',
|
|
7610
|
-
});
|
|
7611
|
-
exportData.style = await this.exportStyle(regionId);
|
|
7929
|
+
// Gzip vector tiles. Idempotent: skip tiles already gzipped (downloaded
|
|
7930
|
+
// with their original gzip wrapper intact).
|
|
7931
|
+
const packedTiles = [];
|
|
7932
|
+
for (const tile of tiles) {
|
|
7933
|
+
const raw = tile.data instanceof ArrayBuffer
|
|
7934
|
+
? new Uint8Array(tile.data)
|
|
7935
|
+
: new Uint8Array(tile.data);
|
|
7936
|
+
const data = isVector && !hasGzipMagic(raw) ? await gzipBytes(raw) : raw;
|
|
7937
|
+
packedTiles.push({ z: tile.z, x: tile.x, y: tile.y, data });
|
|
7612
7938
|
}
|
|
7613
|
-
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
|
|
7617
|
-
|
|
7618
|
-
|
|
7939
|
+
onProgress({
|
|
7940
|
+
stage: 'processing',
|
|
7941
|
+
percentage: 85,
|
|
7942
|
+
message: 'Packing SQLite database...',
|
|
7943
|
+
});
|
|
7944
|
+
const SQL = await getSqlJs();
|
|
7945
|
+
const db = new SQL.Database();
|
|
7946
|
+
try {
|
|
7947
|
+
db.run(`
|
|
7948
|
+
CREATE TABLE metadata (name TEXT, value TEXT);
|
|
7949
|
+
CREATE TABLE tiles (
|
|
7950
|
+
zoom_level INTEGER NOT NULL,
|
|
7951
|
+
tile_column INTEGER NOT NULL,
|
|
7952
|
+
tile_row INTEGER NOT NULL,
|
|
7953
|
+
tile_data BLOB
|
|
7954
|
+
);
|
|
7955
|
+
CREATE UNIQUE INDEX tile_index ON tiles (zoom_level, tile_column, tile_row);
|
|
7956
|
+
CREATE UNIQUE INDEX name ON metadata (name);
|
|
7957
|
+
`);
|
|
7958
|
+
const [[west, south], [east, north]] = region.bounds;
|
|
7959
|
+
const centerLon = (west + east) / 2;
|
|
7960
|
+
const centerLat = (south + north) / 2;
|
|
7961
|
+
const centerZoom = Math.max(region.minZoom, Math.min(region.maxZoom, Math.round((region.minZoom + region.maxZoom) / 2)));
|
|
7962
|
+
const metadataRows = {
|
|
7963
|
+
name: region.name || region.id,
|
|
7964
|
+
// MBTiles 1.3 type: 'overlay' or 'baselayer'. Baselayer matches how
|
|
7965
|
+
// QGIS treats the dataset (full-coverage map rather than overlay).
|
|
7966
|
+
type: isVector ? 'baselayer' : 'overlay',
|
|
7967
|
+
version: '1.0',
|
|
7968
|
+
description: region.name || region.id,
|
|
7969
|
+
format,
|
|
7970
|
+
bounds: `${west},${south},${east},${north}`,
|
|
7971
|
+
center: `${centerLon},${centerLat},${centerZoom}`,
|
|
7972
|
+
minzoom: String(region.minZoom),
|
|
7973
|
+
maxzoom: String(region.maxZoom),
|
|
7974
|
+
};
|
|
7975
|
+
// For vector tiles, the `json` field with `vector_layers` is required
|
|
7976
|
+
// by the MBTiles 1.3 spec and by every vector tile consumer worth
|
|
7977
|
+
// opening the file in. Derive it from the offline style.
|
|
7978
|
+
if (isVector) {
|
|
7979
|
+
const style = await this.exportStyle(regionId);
|
|
7980
|
+
const sourceIds = new Set(tiles.map(t => t.sourceId).filter(Boolean));
|
|
7981
|
+
const json = buildVectorJsonMetadata(style.style ?? style, sourceIds);
|
|
7982
|
+
if (json)
|
|
7983
|
+
metadataRows.json = json;
|
|
7984
|
+
}
|
|
7985
|
+
for (const [k, v] of Object.entries(options.metadata || {})) {
|
|
7986
|
+
metadataRows[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
|
7987
|
+
}
|
|
7988
|
+
const insertMeta = db.prepare(`INSERT INTO metadata (name, value) VALUES (?, ?)`);
|
|
7989
|
+
try {
|
|
7990
|
+
for (const [name, value] of Object.entries(metadataRows)) {
|
|
7991
|
+
insertMeta.run([name, value]);
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
finally {
|
|
7995
|
+
insertMeta.free();
|
|
7996
|
+
}
|
|
7997
|
+
const insertTile = db.prepare(`INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data)
|
|
7998
|
+
VALUES (?, ?, ?, ?)`);
|
|
7999
|
+
try {
|
|
8000
|
+
db.run('BEGIN');
|
|
8001
|
+
for (const tile of packedTiles) {
|
|
8002
|
+
insertTile.run([tile.z, tile.x, flipY(tile.y, tile.z), tile.data]);
|
|
8003
|
+
}
|
|
8004
|
+
db.run('COMMIT');
|
|
8005
|
+
}
|
|
8006
|
+
finally {
|
|
8007
|
+
insertTile.free();
|
|
8008
|
+
}
|
|
8009
|
+
const binary = db.export();
|
|
8010
|
+
const blob = new Blob([binary.buffer], {
|
|
8011
|
+
type: 'application/x-sqlite3',
|
|
7619
8012
|
});
|
|
7620
|
-
exportData.tiles = await this.exportTiles(regionId, onProgress);
|
|
7621
|
-
}
|
|
7622
|
-
// Export sprites if requested
|
|
7623
|
-
if (options.includeSprites !== false) {
|
|
7624
8013
|
onProgress({
|
|
7625
|
-
stage: '
|
|
7626
|
-
percentage:
|
|
7627
|
-
message: '
|
|
8014
|
+
stage: 'complete',
|
|
8015
|
+
percentage: 100,
|
|
8016
|
+
message: 'MBTiles export complete!',
|
|
7628
8017
|
});
|
|
7629
|
-
|
|
8018
|
+
return {
|
|
8019
|
+
success: true,
|
|
8020
|
+
format: 'mbtiles',
|
|
8021
|
+
filename: `${region.name || region.id}.mbtiles`,
|
|
8022
|
+
blob,
|
|
8023
|
+
size: blob.size,
|
|
8024
|
+
statistics: {
|
|
8025
|
+
tilesExported: tiles.length,
|
|
8026
|
+
spritesExported: 0,
|
|
8027
|
+
fontsExported: 0,
|
|
8028
|
+
},
|
|
8029
|
+
};
|
|
7630
8030
|
}
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
onProgress({
|
|
7634
|
-
stage: 'exporting',
|
|
7635
|
-
percentage: 85,
|
|
7636
|
-
message: 'Exporting fonts...',
|
|
7637
|
-
});
|
|
7638
|
-
exportData.fonts = await this.exportFonts(regionId);
|
|
8031
|
+
finally {
|
|
8032
|
+
db.close();
|
|
7639
8033
|
}
|
|
7640
|
-
onProgress({
|
|
7641
|
-
stage: 'processing',
|
|
7642
|
-
percentage: 95,
|
|
7643
|
-
message: 'Creating export file...',
|
|
7644
|
-
});
|
|
7645
|
-
// Create JSON blob
|
|
7646
|
-
const jsonString = JSON.stringify(exportData, null, 2);
|
|
7647
|
-
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
7648
|
-
onProgress({
|
|
7649
|
-
stage: 'complete',
|
|
7650
|
-
percentage: 100,
|
|
7651
|
-
message: 'Export complete!',
|
|
7652
|
-
});
|
|
7653
|
-
return {
|
|
7654
|
-
success: true,
|
|
7655
|
-
format: 'json',
|
|
7656
|
-
filename: `${region.name || region.id}_export.json`,
|
|
7657
|
-
blob,
|
|
7658
|
-
size: blob.size,
|
|
7659
|
-
statistics: {
|
|
7660
|
-
tilesExported: exportData.tiles.length,
|
|
7661
|
-
spritesExported: exportData.sprites.length,
|
|
7662
|
-
fontsExported: exportData.fonts.length,
|
|
7663
|
-
},
|
|
7664
|
-
};
|
|
7665
8034
|
}
|
|
7666
8035
|
catch (error) {
|
|
7667
8036
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7668
|
-
throw new Error(`
|
|
8037
|
+
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7669
8038
|
}
|
|
7670
8039
|
}
|
|
7671
8040
|
/**
|
|
7672
|
-
*
|
|
8041
|
+
* Import region from a binary MBTiles (SQLite) file.
|
|
7673
8042
|
*/
|
|
7674
|
-
async
|
|
7675
|
-
const onProgress =
|
|
8043
|
+
async importRegion(importData) {
|
|
8044
|
+
const onProgress = importData.onProgress || (() => { });
|
|
7676
8045
|
try {
|
|
7677
8046
|
onProgress({
|
|
7678
8047
|
stage: 'preparing',
|
|
7679
8048
|
percentage: 0,
|
|
7680
|
-
message: '
|
|
8049
|
+
message: 'Reading file...',
|
|
7681
8050
|
});
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
// to create a proper PMTiles file format
|
|
7685
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7686
|
-
if (!region) {
|
|
7687
|
-
throw new Error(`Region ${regionId} not found`);
|
|
8051
|
+
if (importData.format !== 'mbtiles') {
|
|
8052
|
+
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7688
8053
|
}
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
const pmtilesData = {
|
|
7693
|
-
header: {
|
|
7694
|
-
version: 3,
|
|
7695
|
-
type: 'mvt',
|
|
7696
|
-
compression: options.compression || 'gzip',
|
|
7697
|
-
bounds: region.bounds,
|
|
7698
|
-
minZoom: region.minZoom,
|
|
7699
|
-
maxZoom: region.maxZoom,
|
|
7700
|
-
metadata: {
|
|
7701
|
-
name: region.name,
|
|
7702
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7703
|
-
...options.metadata,
|
|
7704
|
-
},
|
|
7705
|
-
},
|
|
7706
|
-
tiles: tiles,
|
|
7707
|
-
};
|
|
7708
|
-
// Convert to binary format (simplified)
|
|
7709
|
-
const jsonString = JSON.stringify(pmtilesData);
|
|
7710
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
8054
|
+
const buffer = await this.readFileAsArrayBuffer(importData.file);
|
|
8055
|
+
onProgress({ stage: 'importing', percentage: 40, message: 'Parsing MBTiles...' });
|
|
8056
|
+
const regionData = await this.parseMBTiles(buffer);
|
|
7711
8057
|
onProgress({
|
|
7712
|
-
stage: '
|
|
7713
|
-
percentage:
|
|
7714
|
-
message:
|
|
7715
|
-
});
|
|
7716
|
-
return {
|
|
7717
|
-
success: true,
|
|
7718
|
-
format: 'pmtiles',
|
|
7719
|
-
filename: `${region.name || region.id}.pmtiles`,
|
|
7720
|
-
blob,
|
|
7721
|
-
size: blob.size,
|
|
7722
|
-
statistics: {
|
|
7723
|
-
tilesExported: tiles.length,
|
|
7724
|
-
spritesExported: 0,
|
|
7725
|
-
fontsExported: 0,
|
|
7726
|
-
},
|
|
7727
|
-
};
|
|
7728
|
-
}
|
|
7729
|
-
catch (error) {
|
|
7730
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7731
|
-
throw new Error(`PMTiles export failed: ${errorMessage}`);
|
|
7732
|
-
}
|
|
7733
|
-
}
|
|
7734
|
-
/**
|
|
7735
|
-
* Export region as MBTiles format
|
|
7736
|
-
*/
|
|
7737
|
-
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7738
|
-
const onProgress = options.onProgress || (() => { });
|
|
7739
|
-
try {
|
|
7740
|
-
onProgress({
|
|
7741
|
-
stage: 'preparing',
|
|
7742
|
-
percentage: 0,
|
|
7743
|
-
message: 'Preparing MBTiles export...',
|
|
8058
|
+
stage: 'importing',
|
|
8059
|
+
percentage: 70,
|
|
8060
|
+
message: `Importing ${regionData.tiles?.length ?? 0} tiles...`,
|
|
7744
8061
|
});
|
|
7745
|
-
|
|
7746
|
-
// In a real implementation, you would use SQLite/SQL.js
|
|
7747
|
-
// to create a proper MBTiles SQLite database
|
|
7748
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7749
|
-
if (!region) {
|
|
7750
|
-
throw new Error(`Region ${regionId} not found`);
|
|
7751
|
-
}
|
|
7752
|
-
// Get tiles data
|
|
7753
|
-
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7754
|
-
// Create MBTiles structure (simplified as JSON for now)
|
|
7755
|
-
const mbtilesData = {
|
|
7756
|
-
metadata: {
|
|
7757
|
-
name: region.name,
|
|
7758
|
-
type: 'overlay',
|
|
7759
|
-
version: '1.0',
|
|
7760
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7761
|
-
format: options.format || 'pbf',
|
|
7762
|
-
bounds: region.bounds.flat().join(','),
|
|
7763
|
-
minzoom: region.minZoom,
|
|
7764
|
-
maxzoom: region.maxZoom,
|
|
7765
|
-
...options.metadata,
|
|
7766
|
-
},
|
|
7767
|
-
tiles: tiles.map(tile => ({
|
|
7768
|
-
zoom_level: tile.z,
|
|
7769
|
-
tile_column: tile.x,
|
|
7770
|
-
tile_row: tile.y,
|
|
7771
|
-
tile_data: tile.data,
|
|
7772
|
-
})),
|
|
7773
|
-
};
|
|
7774
|
-
// Convert to binary format (simplified)
|
|
7775
|
-
const jsonString = JSON.stringify(mbtilesData);
|
|
7776
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
8062
|
+
const result = await this.importRegionData(regionData, importData);
|
|
7777
8063
|
onProgress({
|
|
7778
8064
|
stage: 'complete',
|
|
7779
8065
|
percentage: 100,
|
|
7780
|
-
message: '
|
|
8066
|
+
message: result.success ? 'Import complete!' : result.message,
|
|
7781
8067
|
});
|
|
7782
|
-
return {
|
|
7783
|
-
success: true,
|
|
7784
|
-
format: 'mbtiles',
|
|
7785
|
-
filename: `${region.name || region.id}.mbtiles`,
|
|
7786
|
-
blob,
|
|
7787
|
-
size: blob.size,
|
|
7788
|
-
statistics: {
|
|
7789
|
-
tilesExported: tiles.length,
|
|
7790
|
-
spritesExported: 0,
|
|
7791
|
-
fontsExported: 0,
|
|
7792
|
-
},
|
|
7793
|
-
};
|
|
7794
|
-
}
|
|
7795
|
-
catch (error) {
|
|
7796
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7797
|
-
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7798
|
-
}
|
|
7799
|
-
}
|
|
7800
|
-
/**
|
|
7801
|
-
* Import region from file
|
|
7802
|
-
*/
|
|
7803
|
-
async importRegion(importData) {
|
|
7804
|
-
try {
|
|
7805
|
-
let regionData;
|
|
7806
|
-
switch (importData.format) {
|
|
7807
|
-
case 'json': {
|
|
7808
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7809
|
-
regionData = JSON.parse(textContent);
|
|
7810
|
-
break;
|
|
7811
|
-
}
|
|
7812
|
-
case 'pmtiles': {
|
|
7813
|
-
// PMTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7814
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7815
|
-
regionData = await this.parsePMTiles(textContent);
|
|
7816
|
-
break;
|
|
7817
|
-
}
|
|
7818
|
-
case 'mbtiles': {
|
|
7819
|
-
// MBTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7820
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7821
|
-
regionData = await this.parseMBTiles(textContent);
|
|
7822
|
-
break;
|
|
7823
|
-
}
|
|
7824
|
-
default:
|
|
7825
|
-
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7826
|
-
}
|
|
7827
|
-
// Import the region data
|
|
7828
|
-
const result = await this.importRegionData(regionData, importData);
|
|
7829
8068
|
return result;
|
|
7830
8069
|
}
|
|
7831
8070
|
catch (error) {
|
|
@@ -7949,151 +8188,113 @@
|
|
|
7949
8188
|
}
|
|
7950
8189
|
}
|
|
7951
8190
|
/**
|
|
7952
|
-
*
|
|
8191
|
+
* Read file content as ArrayBuffer (for the binary MBTiles file).
|
|
7953
8192
|
*/
|
|
7954
|
-
async
|
|
7955
|
-
const db = await this.db;
|
|
7956
|
-
const transaction = db.transaction(['sprites'], 'readonly');
|
|
7957
|
-
const store = transaction.objectStore('sprites');
|
|
7958
|
-
const sprites = [];
|
|
7959
|
-
try {
|
|
7960
|
-
let cursor = await store.openCursor();
|
|
7961
|
-
while (cursor) {
|
|
7962
|
-
const sprite = cursor.value;
|
|
7963
|
-
// Include sprites that match the styleId, or all sprites if keys don't contain styleId
|
|
7964
|
-
// (sprite keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7965
|
-
sprites.push({
|
|
7966
|
-
url: sprite.url,
|
|
7967
|
-
data: sprite.data,
|
|
7968
|
-
type: sprite.url.endsWith('.json') ? 'json' : 'png',
|
|
7969
|
-
resolution: sprite.url.includes('@2x') ? '2x' : '1x',
|
|
7970
|
-
});
|
|
7971
|
-
cursor = await cursor.continue();
|
|
7972
|
-
}
|
|
7973
|
-
return sprites;
|
|
7974
|
-
}
|
|
7975
|
-
catch (error) {
|
|
7976
|
-
serviceLogger.error('Error exporting sprites:', error);
|
|
7977
|
-
return [];
|
|
7978
|
-
}
|
|
7979
|
-
}
|
|
7980
|
-
/**
|
|
7981
|
-
* Export fonts data
|
|
7982
|
-
*/
|
|
7983
|
-
async exportFonts(_regionId) {
|
|
7984
|
-
const db = await this.db;
|
|
7985
|
-
const transaction = db.transaction(['fonts'], 'readonly');
|
|
7986
|
-
const store = transaction.objectStore('fonts');
|
|
7987
|
-
const fonts = [];
|
|
7988
|
-
try {
|
|
7989
|
-
let cursor = await store.openCursor();
|
|
7990
|
-
while (cursor) {
|
|
7991
|
-
const font = cursor.value;
|
|
7992
|
-
// Include fonts that match the styleId, or all fonts if keys don't contain styleId
|
|
7993
|
-
// (font keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7994
|
-
fonts.push({
|
|
7995
|
-
fontStack: font.key, // Use key as fontstack identifier
|
|
7996
|
-
range: '0-255', // Default range since FontEntry doesn't store this
|
|
7997
|
-
data: font.data,
|
|
7998
|
-
});
|
|
7999
|
-
cursor = await cursor.continue();
|
|
8000
|
-
}
|
|
8001
|
-
return fonts;
|
|
8002
|
-
}
|
|
8003
|
-
catch (error) {
|
|
8004
|
-
serviceLogger.error('Error exporting fonts:', error);
|
|
8005
|
-
return [];
|
|
8006
|
-
}
|
|
8007
|
-
}
|
|
8008
|
-
/**
|
|
8009
|
-
* Read file content as text (for JSON files)
|
|
8010
|
-
*/
|
|
8011
|
-
async readFileAsText(file) {
|
|
8193
|
+
async readFileAsArrayBuffer(file) {
|
|
8012
8194
|
return new Promise((resolve, reject) => {
|
|
8013
8195
|
const reader = new FileReader();
|
|
8014
8196
|
reader.onload = () => resolve(reader.result);
|
|
8015
8197
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
8016
|
-
reader.
|
|
8198
|
+
reader.readAsArrayBuffer(file);
|
|
8017
8199
|
});
|
|
8018
8200
|
}
|
|
8019
8201
|
/**
|
|
8020
|
-
* Parse
|
|
8202
|
+
* Parse a real binary MBTiles (SQLite) file into our import-data shape.
|
|
8203
|
+
* Un-flips the TMS tile_row back to XYZ y.
|
|
8021
8204
|
*/
|
|
8022
|
-
async
|
|
8023
|
-
|
|
8024
|
-
//
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8205
|
+
async parseMBTiles(buffer) {
|
|
8206
|
+
const bytes = new Uint8Array(buffer);
|
|
8207
|
+
// SQLite header: "SQLite format 3\0" (16 bytes). Validate up front so
|
|
8208
|
+
// non-MBTiles files (e.g. a JSON renamed to .mbtiles) surface a clear
|
|
8209
|
+
// error instead of the opaque "file is not a database" from sql.js.
|
|
8210
|
+
if (bytes.byteLength < 16) {
|
|
8211
|
+
throw new Error('Not a valid MBTiles file: file is too small');
|
|
8212
|
+
}
|
|
8213
|
+
const magic = String.fromCharCode(...bytes.slice(0, 15));
|
|
8214
|
+
if (magic !== 'SQLite format 3') {
|
|
8215
|
+
throw new Error('Not a valid MBTiles file: missing SQLite header');
|
|
8216
|
+
}
|
|
8217
|
+
const SQL = await getSqlJs();
|
|
8218
|
+
const db = new SQL.Database(bytes);
|
|
8219
|
+
try {
|
|
8220
|
+
const tablesResult = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('metadata', 'tiles')");
|
|
8221
|
+
const tableNames = (tablesResult[0]?.values || []).map(r => r[0]);
|
|
8222
|
+
if (!tableNames.includes('metadata') || !tableNames.includes('tiles')) {
|
|
8223
|
+
throw new Error('Not a valid MBTiles file: missing required metadata/tiles tables');
|
|
8224
|
+
}
|
|
8225
|
+
const metadata = {};
|
|
8226
|
+
const metaStmt = db.prepare('SELECT name, value FROM metadata');
|
|
8227
|
+
try {
|
|
8228
|
+
while (metaStmt.step()) {
|
|
8229
|
+
const row = metaStmt.get();
|
|
8230
|
+
metadata[row[0]] = row[1];
|
|
8231
|
+
}
|
|
8232
|
+
}
|
|
8233
|
+
finally {
|
|
8234
|
+
metaStmt.free();
|
|
8235
|
+
}
|
|
8236
|
+
const rawBounds = metadata.bounds ? metadata.bounds.split(',').map(Number) : [0, 0, 0, 0];
|
|
8237
|
+
const bounds = [
|
|
8238
|
+
isFinite(rawBounds[0]) ? rawBounds[0] : 0,
|
|
8239
|
+
isFinite(rawBounds[1]) ? rawBounds[1] : 0,
|
|
8240
|
+
isFinite(rawBounds[2]) ? rawBounds[2] : 0,
|
|
8241
|
+
isFinite(rawBounds[3]) ? rawBounds[3] : 0,
|
|
8242
|
+
];
|
|
8243
|
+
const format = (metadata.format || 'pbf');
|
|
8244
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
8245
|
+
const tiles = [];
|
|
8246
|
+
const tilesStmt = db.prepare('SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles');
|
|
8247
|
+
try {
|
|
8248
|
+
while (tilesStmt.step()) {
|
|
8249
|
+
const row = tilesStmt.get();
|
|
8250
|
+
const [z, x, tmsRow, data] = row;
|
|
8251
|
+
// Sliced copy so the buffer is detached from sql.js's heap.
|
|
8252
|
+
const copy = new Uint8Array(data.byteLength);
|
|
8253
|
+
copy.set(data);
|
|
8254
|
+
// Our IndexedDB stores vector tiles decompressed (tileService
|
|
8255
|
+
// inflates on download). MBTiles vector tiles are gzipped by
|
|
8256
|
+
// convention — un-gzip on the way in so the stored tile matches
|
|
8257
|
+
// what the fetch handler expects to serve.
|
|
8258
|
+
const storedBytes = isVector && hasGzipMagic(copy) ? await gunzipBytes(copy) : copy;
|
|
8259
|
+
tiles.push({
|
|
8260
|
+
z,
|
|
8261
|
+
x,
|
|
8262
|
+
y: flipY(tmsRow, z),
|
|
8263
|
+
data: storedBytes.buffer,
|
|
8264
|
+
format,
|
|
8265
|
+
sourceId: 'imported',
|
|
8266
|
+
});
|
|
8267
|
+
}
|
|
8268
|
+
}
|
|
8269
|
+
finally {
|
|
8270
|
+
tilesStmt.free();
|
|
8271
|
+
}
|
|
8272
|
+
const minZoom = metadata.minzoom !== undefined ? Number(metadata.minzoom) : 0;
|
|
8273
|
+
const maxZoom = metadata.maxzoom !== undefined ? Number(metadata.maxzoom) : 14;
|
|
8274
|
+
return {
|
|
8275
|
+
metadata: {
|
|
8276
|
+
id: metadata.name || 'imported-region',
|
|
8277
|
+
name: metadata.name || 'Imported Region',
|
|
8278
|
+
description: metadata.description,
|
|
8279
|
+
bounds: [
|
|
8280
|
+
[bounds[0], bounds[1]],
|
|
8281
|
+
[bounds[2], bounds[3]],
|
|
8282
|
+
],
|
|
8283
|
+
minZoom,
|
|
8284
|
+
maxZoom,
|
|
8285
|
+
styleUrl: '',
|
|
8286
|
+
createdAt: Date.now(),
|
|
8287
|
+
exportedAt: Date.now(),
|
|
8288
|
+
version: '1.0.0',
|
|
8289
|
+
format: 'mbtiles',
|
|
8290
|
+
},
|
|
8291
|
+
style: {},
|
|
8292
|
+
tiles,
|
|
8293
|
+
};
|
|
8294
|
+
}
|
|
8295
|
+
finally {
|
|
8296
|
+
db.close();
|
|
8297
|
+
}
|
|
8097
8298
|
}
|
|
8098
8299
|
/**
|
|
8099
8300
|
* Import region data to database
|
|
@@ -8158,16 +8359,15 @@
|
|
|
8158
8359
|
});
|
|
8159
8360
|
}
|
|
8160
8361
|
}
|
|
8161
|
-
// Import sprites and fonts similarly...
|
|
8162
8362
|
return {
|
|
8163
8363
|
success: true,
|
|
8164
8364
|
regionId,
|
|
8165
8365
|
message: 'Region imported successfully',
|
|
8166
8366
|
statistics: {
|
|
8167
8367
|
tilesImported: regionData.tiles?.length || 0,
|
|
8168
|
-
spritesImported:
|
|
8169
|
-
fontsImported:
|
|
8170
|
-
totalSize: 0,
|
|
8368
|
+
spritesImported: 0,
|
|
8369
|
+
fontsImported: 0,
|
|
8370
|
+
totalSize: 0,
|
|
8171
8371
|
},
|
|
8172
8372
|
};
|
|
8173
8373
|
}
|
|
@@ -8396,8 +8596,6 @@
|
|
|
8396
8596
|
};
|
|
8397
8597
|
|
|
8398
8598
|
const createImportExportManagement = (services) => ({
|
|
8399
|
-
exportRegionAsJSON: async (regionId, options = {}) => services.importExportService.exportRegionAsJSON(regionId, options),
|
|
8400
|
-
exportRegionAsPMTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsPMTiles(regionId, options),
|
|
8401
8599
|
exportRegionAsMBTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsMBTiles(regionId, options),
|
|
8402
8600
|
importRegion: async (importData) => services.importExportService.importRegion(importData),
|
|
8403
8601
|
downloadExportedRegion: (exportResult) => {
|
|
@@ -8753,10 +8951,6 @@
|
|
|
8753
8951
|
'styleSelection.title': 'Select Offline Style',
|
|
8754
8952
|
'styleSelection.message': 'Choose which offline style to load:',
|
|
8755
8953
|
'styleSelection.sources': 'sources',
|
|
8756
|
-
// Import/Export
|
|
8757
|
-
'importExport.title': 'Import/Export',
|
|
8758
|
-
'importExport.export': 'Export',
|
|
8759
|
-
'importExport.import': 'Import',
|
|
8760
8954
|
// Errors
|
|
8761
8955
|
'error.loadingContent': 'Error loading content',
|
|
8762
8956
|
'error.tryAgain': 'Please try again',
|
|
@@ -8781,41 +8975,30 @@
|
|
|
8781
8975
|
'regionDetails.bounds': 'Bounds',
|
|
8782
8976
|
'regionDetails.zoomRange': 'Zoom Range',
|
|
8783
8977
|
'regionDetails.created': 'Created',
|
|
8784
|
-
//
|
|
8785
|
-
'
|
|
8786
|
-
'
|
|
8787
|
-
'
|
|
8788
|
-
'
|
|
8789
|
-
'
|
|
8790
|
-
'
|
|
8791
|
-
'
|
|
8792
|
-
'
|
|
8793
|
-
'
|
|
8794
|
-
'
|
|
8795
|
-
'
|
|
8796
|
-
'
|
|
8797
|
-
'
|
|
8798
|
-
'
|
|
8799
|
-
'
|
|
8800
|
-
'
|
|
8801
|
-
'
|
|
8802
|
-
'
|
|
8803
|
-
'
|
|
8804
|
-
'
|
|
8805
|
-
'
|
|
8806
|
-
'
|
|
8807
|
-
'
|
|
8808
|
-
'importExport.fileFormatsHint': 'Supports JSON, PMTiles, and MBTiles formats',
|
|
8809
|
-
'importExport.newRegionName': 'New Region Name (Optional)',
|
|
8810
|
-
'importExport.newRegionNamePlaceholder': 'Leave empty to use original name',
|
|
8811
|
-
'importExport.overwriteIfExists': 'Overwrite if region exists',
|
|
8812
|
-
'importExport.preparingImport': 'Preparing import...',
|
|
8813
|
-
'importExport.importComplete': 'Import complete!',
|
|
8814
|
-
'importExport.importFailed': 'Import failed. Please try again.',
|
|
8815
|
-
'importExport.formatGuide': 'Format Guide',
|
|
8816
|
-
'importExport.jsonDesc': 'Complete data, human-readable, best for development',
|
|
8817
|
-
'importExport.pmtilesDesc': 'Web-optimized, efficient serving, cloud-friendly',
|
|
8818
|
-
'importExport.mbtilesDesc': 'Industry standard, SQLite-based, cross-platform',
|
|
8978
|
+
// MBTiles Modal
|
|
8979
|
+
'mbtiles.title': 'MBTiles Import / Export',
|
|
8980
|
+
'mbtiles.regionInfo': 'Region Information',
|
|
8981
|
+
'mbtiles.id': 'ID',
|
|
8982
|
+
'mbtiles.name': 'Name',
|
|
8983
|
+
'mbtiles.unnamed': 'Unnamed',
|
|
8984
|
+
'mbtiles.zoom': 'Zoom',
|
|
8985
|
+
'mbtiles.created': 'Created',
|
|
8986
|
+
'mbtiles.exportTitle': 'Export as MBTiles',
|
|
8987
|
+
'mbtiles.exportHint': 'Package the tiles in this region into a standard SQLite MBTiles archive that opens in QGIS, tippecanoe, and other tools.',
|
|
8988
|
+
'mbtiles.exportButton': 'Download .mbtiles',
|
|
8989
|
+
'mbtiles.preparingExport': 'Preparing export...',
|
|
8990
|
+
'mbtiles.exportComplete': 'Export complete!',
|
|
8991
|
+
'mbtiles.exportFailed': 'Export failed. Please try again.',
|
|
8992
|
+
'mbtiles.importTitle': 'Import from MBTiles',
|
|
8993
|
+
'mbtiles.selectFile': 'Select an .mbtiles file',
|
|
8994
|
+
'mbtiles.fileHint': 'Only SQLite-format .mbtiles files are supported.',
|
|
8995
|
+
'mbtiles.newRegionName': 'New Region Name (optional)',
|
|
8996
|
+
'mbtiles.newRegionNamePlaceholder': 'Leave empty to use the name from the file',
|
|
8997
|
+
'mbtiles.overwriteIfExists': 'Overwrite if a region with the same id exists',
|
|
8998
|
+
'mbtiles.importButton': 'Import .mbtiles',
|
|
8999
|
+
'mbtiles.preparingImport': 'Preparing import...',
|
|
9000
|
+
'mbtiles.importComplete': 'Import complete!',
|
|
9001
|
+
'mbtiles.importFailed': 'Import failed. Please try again.',
|
|
8819
9002
|
// Active Downloads
|
|
8820
9003
|
'download.activeCount': 'Active Downloads ({{count}})',
|
|
8821
9004
|
// Panel Manager additional strings
|
|
@@ -8976,10 +9159,6 @@
|
|
|
8976
9159
|
'styleSelection.title': 'اختر نمط غير متصل',
|
|
8977
9160
|
'styleSelection.message': 'اختر النمط غير المتصل الذي تريد تحميله:',
|
|
8978
9161
|
'styleSelection.sources': 'مصادر',
|
|
8979
|
-
// Import/Export - استيراد/تصدير
|
|
8980
|
-
'importExport.title': 'استيراد/تصدير',
|
|
8981
|
-
'importExport.export': 'تصدير',
|
|
8982
|
-
'importExport.import': 'استيراد',
|
|
8983
9162
|
// Errors - الأخطاء
|
|
8984
9163
|
'error.loadingContent': 'خطأ في تحميل المحتوى',
|
|
8985
9164
|
'error.tryAgain': 'يرجى المحاولة مرة أخرى',
|
|
@@ -9004,41 +9183,30 @@
|
|
|
9004
9183
|
'regionDetails.bounds': 'الحدود',
|
|
9005
9184
|
'regionDetails.zoomRange': 'نطاق التكبير',
|
|
9006
9185
|
'regionDetails.created': 'تاريخ الإنشاء',
|
|
9007
|
-
//
|
|
9008
|
-
'
|
|
9009
|
-
'
|
|
9010
|
-
'
|
|
9011
|
-
'
|
|
9012
|
-
'
|
|
9013
|
-
'
|
|
9014
|
-
'
|
|
9015
|
-
'
|
|
9016
|
-
'
|
|
9017
|
-
'
|
|
9018
|
-
'
|
|
9019
|
-
'
|
|
9020
|
-
'
|
|
9021
|
-
'
|
|
9022
|
-
'
|
|
9023
|
-
'
|
|
9024
|
-
'
|
|
9025
|
-
'
|
|
9026
|
-
'
|
|
9027
|
-
'
|
|
9028
|
-
'
|
|
9029
|
-
'
|
|
9030
|
-
'
|
|
9031
|
-
'importExport.fileFormatsHint': 'يدعم تنسيقات JSON و PMTiles و MBTiles',
|
|
9032
|
-
'importExport.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
9033
|
-
'importExport.newRegionNamePlaceholder': 'اتركه فارغاً لاستخدام الاسم الأصلي',
|
|
9034
|
-
'importExport.overwriteIfExists': 'الكتابة فوق المنطقة الموجودة',
|
|
9035
|
-
'importExport.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
9036
|
-
'importExport.importComplete': 'اكتمل الاستيراد!',
|
|
9037
|
-
'importExport.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
9038
|
-
'importExport.formatGuide': 'دليل التنسيقات',
|
|
9039
|
-
'importExport.jsonDesc': 'بيانات كاملة، قابلة للقراءة، الأفضل للتطوير',
|
|
9040
|
-
'importExport.pmtilesDesc': 'محسن للويب، خدمة فعالة، متوافق مع السحابة',
|
|
9041
|
-
'importExport.mbtilesDesc': 'معيار الصناعة، قائم على SQLite، متعدد المنصات',
|
|
9186
|
+
// MBTiles Modal - نافذة MBTiles
|
|
9187
|
+
'mbtiles.title': 'استيراد / تصدير MBTiles',
|
|
9188
|
+
'mbtiles.regionInfo': 'معلومات المنطقة',
|
|
9189
|
+
'mbtiles.id': 'المعرف',
|
|
9190
|
+
'mbtiles.name': 'الاسم',
|
|
9191
|
+
'mbtiles.unnamed': 'بدون اسم',
|
|
9192
|
+
'mbtiles.zoom': 'التكبير',
|
|
9193
|
+
'mbtiles.created': 'تاريخ الإنشاء',
|
|
9194
|
+
'mbtiles.exportTitle': 'التصدير كـ MBTiles',
|
|
9195
|
+
'mbtiles.exportHint': 'احزم بلاطات هذه المنطقة داخل أرشيف MBTiles (SQLite) يمكن فتحه في QGIS و tippecanoe وأدوات أخرى.',
|
|
9196
|
+
'mbtiles.exportButton': 'تنزيل ملف mbtiles.',
|
|
9197
|
+
'mbtiles.preparingExport': 'جاري تجهيز التصدير...',
|
|
9198
|
+
'mbtiles.exportComplete': 'اكتمل التصدير!',
|
|
9199
|
+
'mbtiles.exportFailed': 'فشل التصدير. يرجى المحاولة مرة أخرى.',
|
|
9200
|
+
'mbtiles.importTitle': 'الاستيراد من MBTiles',
|
|
9201
|
+
'mbtiles.selectFile': 'اختر ملف mbtiles.',
|
|
9202
|
+
'mbtiles.fileHint': 'تُدعم ملفات mbtiles. بتنسيق SQLite فقط.',
|
|
9203
|
+
'mbtiles.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
9204
|
+
'mbtiles.newRegionNamePlaceholder': 'اتركه فارغًا لاستخدام الاسم من الملف',
|
|
9205
|
+
'mbtiles.overwriteIfExists': 'الكتابة فوق المنطقة إذا كان المعرف موجودًا',
|
|
9206
|
+
'mbtiles.importButton': 'استيراد ملف mbtiles.',
|
|
9207
|
+
'mbtiles.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
9208
|
+
'mbtiles.importComplete': 'اكتمل الاستيراد!',
|
|
9209
|
+
'mbtiles.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
9042
9210
|
// Active Downloads - التحميلات النشطة
|
|
9043
9211
|
'download.activeCount': 'التحميلات النشطة ({{count}})',
|
|
9044
9212
|
// Panel Manager additional strings - سلاسل إضافية للوحة
|
|
@@ -9977,49 +10145,42 @@
|
|
|
9977
10145
|
}
|
|
9978
10146
|
|
|
9979
10147
|
/**
|
|
9980
|
-
* Import/Export Modal
|
|
9981
|
-
*
|
|
9982
|
-
*
|
|
10148
|
+
* MBTiles Import/Export Modal
|
|
10149
|
+
*
|
|
10150
|
+
* Focused modal for exchanging regions as binary SQLite MBTiles archives.
|
|
10151
|
+
* Replaces the previous multi-format import/export modal.
|
|
9983
10152
|
*/
|
|
9984
|
-
const modalLogger = logger.scope('
|
|
9985
|
-
class
|
|
10153
|
+
const modalLogger = logger.scope('MBTilesModal');
|
|
10154
|
+
class MBTilesModal {
|
|
9986
10155
|
modal;
|
|
9987
10156
|
options;
|
|
9988
10157
|
isExporting = false;
|
|
9989
10158
|
isImporting = false;
|
|
9990
|
-
// Form elements
|
|
9991
|
-
exportFormatSelect;
|
|
9992
|
-
includeStyleCheckbox;
|
|
9993
|
-
includeTilesCheckbox;
|
|
9994
|
-
includeSpritesCheckbox;
|
|
9995
|
-
includeFontsCheckbox;
|
|
9996
10159
|
exportProgressBar;
|
|
9997
10160
|
exportProgressText;
|
|
10161
|
+
exportProgressContainer;
|
|
9998
10162
|
exportButton;
|
|
9999
10163
|
importFileInput;
|
|
10000
10164
|
importNameInput;
|
|
10001
10165
|
importOverwriteCheckbox;
|
|
10002
10166
|
importProgressBar;
|
|
10003
10167
|
importProgressText;
|
|
10168
|
+
importProgressContainer;
|
|
10004
10169
|
importButton;
|
|
10005
10170
|
constructor(options) {
|
|
10006
10171
|
this.options = options;
|
|
10007
10172
|
}
|
|
10008
10173
|
show() {
|
|
10009
10174
|
const modalConfig = {
|
|
10010
|
-
title: t('
|
|
10175
|
+
title: t('mbtiles.title'),
|
|
10011
10176
|
subtitle: this.options.region.name || this.options.region.id,
|
|
10012
10177
|
size: 'md',
|
|
10013
10178
|
closable: true,
|
|
10014
10179
|
onClose: () => this.hide(),
|
|
10015
10180
|
};
|
|
10016
10181
|
this.modal = new Modal(modalConfig);
|
|
10017
|
-
|
|
10018
|
-
|
|
10019
|
-
this.modal.setContent(content);
|
|
10020
|
-
// Create footer with close button
|
|
10021
|
-
const footer = this.createFooter();
|
|
10022
|
-
this.modal.setFooter(footer);
|
|
10182
|
+
this.modal.setContent(this.createContent());
|
|
10183
|
+
this.modal.setFooter(this.createFooter());
|
|
10023
10184
|
this.modal.show();
|
|
10024
10185
|
this.attachEventListeners();
|
|
10025
10186
|
return this.modal.getElement();
|
|
@@ -10028,217 +10189,96 @@
|
|
|
10028
10189
|
this.modal?.hide();
|
|
10029
10190
|
this.options.onClose();
|
|
10030
10191
|
}
|
|
10192
|
+
destroy() {
|
|
10193
|
+
this.modal?.destroy();
|
|
10194
|
+
}
|
|
10031
10195
|
createContent() {
|
|
10032
10196
|
const content = document.createElement('div');
|
|
10033
|
-
content.className = 'flex flex-col gap-6';
|
|
10197
|
+
content.className = 'flex flex-col gap-6 py-2';
|
|
10034
10198
|
if (i18n.isRTL()) {
|
|
10035
10199
|
content.setAttribute('dir', 'rtl');
|
|
10036
10200
|
}
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
content.appendChild(
|
|
10040
|
-
// Export/Import Grid
|
|
10041
|
-
const gridContainer = document.createElement('div');
|
|
10042
|
-
gridContainer.className = 'grid grid-cols-1 gap-6';
|
|
10043
|
-
// Export Section
|
|
10044
|
-
const exportSection = this.createExportSection();
|
|
10045
|
-
gridContainer.appendChild(exportSection);
|
|
10046
|
-
// Import Section
|
|
10047
|
-
const importSection = this.createImportSection();
|
|
10048
|
-
gridContainer.appendChild(importSection);
|
|
10049
|
-
content.appendChild(gridContainer);
|
|
10050
|
-
// Format Guide
|
|
10051
|
-
const formatGuide = this.createFormatGuide();
|
|
10052
|
-
content.appendChild(formatGuide);
|
|
10201
|
+
content.appendChild(this.createRegionInfoLine());
|
|
10202
|
+
content.appendChild(this.createExportSection());
|
|
10203
|
+
content.appendChild(this.createImportSection());
|
|
10053
10204
|
return content;
|
|
10054
10205
|
}
|
|
10055
|
-
|
|
10056
|
-
const
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10066
|
-
|
|
10067
|
-
</div>
|
|
10068
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
10069
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.name')}</span>
|
|
10070
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${escapeHtml$1(this.options.region.name || t('importExport.unnamed'))}</div>
|
|
10071
|
-
</div>
|
|
10072
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
10073
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.zoom')}</span>
|
|
10074
|
-
<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>
|
|
10075
|
-
</div>
|
|
10076
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
10077
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.created')}</span>
|
|
10078
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${new Date(this.options.region.created).toLocaleDateString()}</div>
|
|
10079
|
-
</div>
|
|
10080
|
-
</div>
|
|
10206
|
+
createRegionInfoLine() {
|
|
10207
|
+
const { region } = this.options;
|
|
10208
|
+
const line = document.createElement('div');
|
|
10209
|
+
line.className =
|
|
10210
|
+
'flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400';
|
|
10211
|
+
line.innerHTML = `
|
|
10212
|
+
<span class="flex items-center gap-1">
|
|
10213
|
+
${icons.mapPin({ size: 12, color: 'currentColor' })}
|
|
10214
|
+
<span class="font-mono">${escapeHtml$1(region.id)}</span>
|
|
10215
|
+
</span>
|
|
10216
|
+
<span>Z${escapeHtml$1(region.minZoom)}-${escapeHtml$1(region.maxZoom)}</span>
|
|
10217
|
+
<span>${new Date(region.created).toLocaleDateString()}</span>
|
|
10081
10218
|
`;
|
|
10082
|
-
return
|
|
10219
|
+
return line;
|
|
10083
10220
|
}
|
|
10084
10221
|
createExportSection() {
|
|
10085
|
-
const section =
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
`;
|
|
10101
|
-
section.appendChild(header);
|
|
10102
|
-
const formContainer = document.createElement('div');
|
|
10103
|
-
formContainer.className = 'space-y-5';
|
|
10104
|
-
// Format Selection
|
|
10105
|
-
const formatGroup = document.createElement('div');
|
|
10106
|
-
const formatLabel = document.createElement('label');
|
|
10107
|
-
formatLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
10108
|
-
formatLabel.textContent = t('importExport.exportFormat');
|
|
10109
|
-
this.exportFormatSelect = document.createElement('select');
|
|
10110
|
-
this.exportFormatSelect.className =
|
|
10111
|
-
'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';
|
|
10112
|
-
this.exportFormatSelect.innerHTML = `
|
|
10113
|
-
<option value="json">${t('importExport.formatJson')}</option>
|
|
10114
|
-
<option value="pmtiles">${t('importExport.formatPmtiles')}</option>
|
|
10115
|
-
<option value="mbtiles">${t('importExport.formatMbtiles')}</option>
|
|
10116
|
-
`;
|
|
10117
|
-
const formatHint = document.createElement('p');
|
|
10118
|
-
formatHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
10119
|
-
formatHint.textContent = t('importExport.formatHint');
|
|
10120
|
-
formatGroup.appendChild(formatLabel);
|
|
10121
|
-
formatGroup.appendChild(this.exportFormatSelect);
|
|
10122
|
-
formatGroup.appendChild(formatHint);
|
|
10123
|
-
formContainer.appendChild(formatGroup);
|
|
10124
|
-
// Export Options
|
|
10125
|
-
const optionsGroup = document.createElement('div');
|
|
10126
|
-
const optionsLabel = document.createElement('label');
|
|
10127
|
-
optionsLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3';
|
|
10128
|
-
optionsLabel.textContent = t('importExport.includeComponents');
|
|
10129
|
-
const checkboxContainer = document.createElement('div');
|
|
10130
|
-
checkboxContainer.className = 'grid grid-cols-1 sm:grid-cols-2 gap-3';
|
|
10131
|
-
const createCheckbox = (text, checked = true) => {
|
|
10132
|
-
const label = document.createElement('label');
|
|
10133
|
-
label.className =
|
|
10134
|
-
'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';
|
|
10135
|
-
const input = document.createElement('input');
|
|
10136
|
-
input.type = 'checkbox';
|
|
10137
|
-
input.checked = checked;
|
|
10138
|
-
input.className =
|
|
10139
|
-
'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';
|
|
10140
|
-
const span = document.createElement('span');
|
|
10141
|
-
span.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
10142
|
-
span.textContent = text;
|
|
10143
|
-
label.appendChild(input);
|
|
10144
|
-
label.appendChild(span);
|
|
10145
|
-
return { label, input };
|
|
10146
|
-
};
|
|
10147
|
-
const styleCheck = createCheckbox(t('importExport.styleConfig'));
|
|
10148
|
-
this.includeStyleCheckbox = styleCheck.input;
|
|
10149
|
-
checkboxContainer.appendChild(styleCheck.label);
|
|
10150
|
-
const tilesCheck = createCheckbox(t('importExport.mapTiles'));
|
|
10151
|
-
this.includeTilesCheckbox = tilesCheck.input;
|
|
10152
|
-
checkboxContainer.appendChild(tilesCheck.label);
|
|
10153
|
-
const spritesCheck = createCheckbox(t('importExport.spritesIcons'));
|
|
10154
|
-
this.includeSpritesCheckbox = spritesCheck.input;
|
|
10155
|
-
checkboxContainer.appendChild(spritesCheck.label);
|
|
10156
|
-
const fontsCheck = createCheckbox(t('importExport.fontsGlyphs'));
|
|
10157
|
-
this.includeFontsCheckbox = fontsCheck.input;
|
|
10158
|
-
checkboxContainer.appendChild(fontsCheck.label);
|
|
10159
|
-
optionsGroup.appendChild(optionsLabel);
|
|
10160
|
-
optionsGroup.appendChild(checkboxContainer);
|
|
10161
|
-
formContainer.appendChild(optionsGroup);
|
|
10162
|
-
// Export Progress (hidden by default)
|
|
10163
|
-
const progressContainer = document.createElement('div');
|
|
10164
|
-
progressContainer.className = 'hidden';
|
|
10165
|
-
const progressBarContainer = document.createElement('div');
|
|
10166
|
-
progressBarContainer.className =
|
|
10167
|
-
'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
10168
|
-
this.exportProgressBar = document.createElement('div');
|
|
10169
|
-
this.exportProgressBar.className = 'bg-blue-600 h-2 rounded-full transition-all duration-300';
|
|
10170
|
-
this.exportProgressBar.style.width = '0%';
|
|
10171
|
-
progressBarContainer.appendChild(this.exportProgressBar);
|
|
10172
|
-
this.exportProgressText = document.createElement('p');
|
|
10173
|
-
this.exportProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10174
|
-
this.exportProgressText.textContent = t('importExport.preparingExport');
|
|
10175
|
-
progressContainer.appendChild(progressBarContainer);
|
|
10176
|
-
progressContainer.appendChild(this.exportProgressText);
|
|
10177
|
-
formContainer.appendChild(progressContainer);
|
|
10178
|
-
// Export Button
|
|
10179
|
-
const exportButton = new Button({
|
|
10180
|
-
text: t('importExport.exportRegion'),
|
|
10222
|
+
const section = this.createSection(t('mbtiles.exportTitle'), 'blue', icons.download({ size: 20, color: 'currentColor' }));
|
|
10223
|
+
const form = document.createElement('div');
|
|
10224
|
+
form.className = 'space-y-5';
|
|
10225
|
+
const hint = document.createElement('p');
|
|
10226
|
+
hint.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10227
|
+
hint.textContent = t('mbtiles.exportHint');
|
|
10228
|
+
form.appendChild(hint);
|
|
10229
|
+
// Progress (hidden by default)
|
|
10230
|
+
const progress = this.createProgressBlock('blue', t('mbtiles.preparingExport'));
|
|
10231
|
+
this.exportProgressContainer = progress.container;
|
|
10232
|
+
this.exportProgressBar = progress.bar;
|
|
10233
|
+
this.exportProgressText = progress.text;
|
|
10234
|
+
form.appendChild(progress.container);
|
|
10235
|
+
const exportBtn = new Button({
|
|
10236
|
+
text: t('mbtiles.exportButton'),
|
|
10181
10237
|
variant: 'primary',
|
|
10182
10238
|
icon: icons.download({ size: 16, color: 'white' }),
|
|
10183
|
-
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
10239
|
+
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
10184
10240
|
onClick: () => this.handleExport(),
|
|
10185
10241
|
});
|
|
10186
|
-
this.exportButton =
|
|
10187
|
-
|
|
10188
|
-
section.appendChild(
|
|
10242
|
+
this.exportButton = exportBtn.getElement();
|
|
10243
|
+
form.appendChild(this.exportButton);
|
|
10244
|
+
section.appendChild(form);
|
|
10189
10245
|
return section;
|
|
10190
10246
|
}
|
|
10191
10247
|
createImportSection() {
|
|
10192
|
-
const section =
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
//
|
|
10196
|
-
const accent = document.createElement('div');
|
|
10197
|
-
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-green-500 opacity-50`;
|
|
10198
|
-
section.appendChild(accent);
|
|
10199
|
-
const header = document.createElement('h3');
|
|
10200
|
-
header.className =
|
|
10201
|
-
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
10202
|
-
header.innerHTML = `
|
|
10203
|
-
<div class="p-2 bg-green-500/10 rounded-lg text-green-600 dark:text-green-400">
|
|
10204
|
-
${icons.upload({ size: 20, color: 'currentColor' })}
|
|
10205
|
-
</div>
|
|
10206
|
-
${t('importExport.importRegion')}
|
|
10207
|
-
`;
|
|
10208
|
-
section.appendChild(header);
|
|
10209
|
-
const formContainer = document.createElement('div');
|
|
10210
|
-
formContainer.className = 'space-y-5';
|
|
10211
|
-
// File Selection
|
|
10248
|
+
const section = this.createSection(t('mbtiles.importTitle'), 'green', icons.upload({ size: 20, color: 'currentColor' }));
|
|
10249
|
+
const form = document.createElement('div');
|
|
10250
|
+
form.className = 'space-y-5';
|
|
10251
|
+
// File input
|
|
10212
10252
|
const fileGroup = document.createElement('div');
|
|
10213
10253
|
const fileLabel = document.createElement('label');
|
|
10214
10254
|
fileLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
10215
|
-
fileLabel.textContent = t('
|
|
10255
|
+
fileLabel.textContent = t('mbtiles.selectFile');
|
|
10216
10256
|
this.importFileInput = document.createElement('input');
|
|
10217
10257
|
this.importFileInput.type = 'file';
|
|
10218
|
-
this.importFileInput.accept = '.
|
|
10258
|
+
this.importFileInput.accept = '.mbtiles,application/vnd.sqlite3,application/x-sqlite3';
|
|
10219
10259
|
this.importFileInput.className =
|
|
10220
10260
|
'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';
|
|
10221
10261
|
const fileHint = document.createElement('p');
|
|
10222
10262
|
fileHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
10223
|
-
fileHint.textContent = t('
|
|
10263
|
+
fileHint.textContent = t('mbtiles.fileHint');
|
|
10224
10264
|
fileGroup.appendChild(fileLabel);
|
|
10225
10265
|
fileGroup.appendChild(this.importFileInput);
|
|
10226
10266
|
fileGroup.appendChild(fileHint);
|
|
10227
|
-
|
|
10228
|
-
// New
|
|
10267
|
+
form.appendChild(fileGroup);
|
|
10268
|
+
// New region name
|
|
10229
10269
|
const nameGroup = document.createElement('div');
|
|
10230
10270
|
const nameLabel = document.createElement('label');
|
|
10231
10271
|
nameLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
10232
|
-
nameLabel.textContent = t('
|
|
10272
|
+
nameLabel.textContent = t('mbtiles.newRegionName');
|
|
10233
10273
|
this.importNameInput = document.createElement('input');
|
|
10234
10274
|
this.importNameInput.type = 'text';
|
|
10235
|
-
this.importNameInput.placeholder = t('
|
|
10275
|
+
this.importNameInput.placeholder = t('mbtiles.newRegionNamePlaceholder');
|
|
10236
10276
|
this.importNameInput.className =
|
|
10237
10277
|
'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';
|
|
10238
10278
|
nameGroup.appendChild(nameLabel);
|
|
10239
10279
|
nameGroup.appendChild(this.importNameInput);
|
|
10240
|
-
|
|
10241
|
-
//
|
|
10280
|
+
form.appendChild(nameGroup);
|
|
10281
|
+
// Overwrite toggle
|
|
10242
10282
|
const overwriteLabel = document.createElement('label');
|
|
10243
10283
|
overwriteLabel.className =
|
|
10244
10284
|
'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';
|
|
@@ -10248,79 +10288,85 @@
|
|
|
10248
10288
|
'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';
|
|
10249
10289
|
const overwriteSpan = document.createElement('span');
|
|
10250
10290
|
overwriteSpan.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
10251
|
-
overwriteSpan.textContent = t('
|
|
10291
|
+
overwriteSpan.textContent = t('mbtiles.overwriteIfExists');
|
|
10252
10292
|
overwriteLabel.appendChild(this.importOverwriteCheckbox);
|
|
10253
10293
|
overwriteLabel.appendChild(overwriteSpan);
|
|
10254
|
-
|
|
10255
|
-
//
|
|
10256
|
-
const
|
|
10257
|
-
|
|
10258
|
-
|
|
10259
|
-
|
|
10260
|
-
|
|
10261
|
-
|
|
10262
|
-
|
|
10263
|
-
|
|
10264
|
-
|
|
10265
|
-
this.importProgressText = document.createElement('p');
|
|
10266
|
-
this.importProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10267
|
-
this.importProgressText.textContent = t('importExport.preparingImport');
|
|
10268
|
-
progressContainer.appendChild(progressBarContainer);
|
|
10269
|
-
progressContainer.appendChild(this.importProgressText);
|
|
10270
|
-
formContainer.appendChild(progressContainer);
|
|
10271
|
-
// Import Button
|
|
10272
|
-
const importButton = new Button({
|
|
10273
|
-
text: t('importExport.importRegion'),
|
|
10274
|
-
variant: 'success', // Assuming 'success' variant exists in Button component, if not might need style adjustment. Assuming it works based on previous code.
|
|
10294
|
+
form.appendChild(overwriteLabel);
|
|
10295
|
+
// Progress
|
|
10296
|
+
const progress = this.createProgressBlock('green', t('mbtiles.preparingImport'));
|
|
10297
|
+
this.importProgressContainer = progress.container;
|
|
10298
|
+
this.importProgressBar = progress.bar;
|
|
10299
|
+
this.importProgressText = progress.text;
|
|
10300
|
+
form.appendChild(progress.container);
|
|
10301
|
+
// Import button (disabled until a file is selected)
|
|
10302
|
+
const importBtn = new Button({
|
|
10303
|
+
text: t('mbtiles.importButton'),
|
|
10304
|
+
variant: 'success',
|
|
10275
10305
|
icon: icons.upload({ size: 16, color: 'white' }),
|
|
10276
10306
|
className: 'w-full py-2.5 text-base shadow-lg shadow-green-500/20',
|
|
10277
10307
|
disabled: true,
|
|
10278
10308
|
onClick: () => this.handleImport(),
|
|
10279
10309
|
});
|
|
10280
|
-
this.importButton =
|
|
10281
|
-
|
|
10282
|
-
section.appendChild(
|
|
10310
|
+
this.importButton = importBtn.getElement();
|
|
10311
|
+
form.appendChild(this.importButton);
|
|
10312
|
+
section.appendChild(form);
|
|
10283
10313
|
return section;
|
|
10284
10314
|
}
|
|
10285
|
-
|
|
10286
|
-
const
|
|
10287
|
-
|
|
10288
|
-
|
|
10289
|
-
|
|
10290
|
-
|
|
10291
|
-
|
|
10292
|
-
|
|
10293
|
-
|
|
10294
|
-
|
|
10295
|
-
|
|
10296
|
-
|
|
10297
|
-
|
|
10298
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
10299
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">PMTiles</div>
|
|
10300
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.pmtilesDesc')}</div>
|
|
10301
|
-
</div>
|
|
10302
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
10303
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">MBTiles</div>
|
|
10304
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.mbtilesDesc')}</div>
|
|
10305
|
-
</div>
|
|
10315
|
+
createSection(title, accentColor, iconHtml) {
|
|
10316
|
+
const section = document.createElement('div');
|
|
10317
|
+
section.className =
|
|
10318
|
+
'glass-input p-6 rounded-xl border-0 bg-white/40 dark:bg-gray-800/40 relative overflow-hidden';
|
|
10319
|
+
const accent = document.createElement('div');
|
|
10320
|
+
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-${accentColor}-500 opacity-50`;
|
|
10321
|
+
section.appendChild(accent);
|
|
10322
|
+
const header = document.createElement('h3');
|
|
10323
|
+
header.className =
|
|
10324
|
+
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
10325
|
+
header.innerHTML = `
|
|
10326
|
+
<div class="p-2 bg-${accentColor}-500/10 rounded-lg text-${accentColor}-600 dark:text-${accentColor}-400">
|
|
10327
|
+
${iconHtml}
|
|
10306
10328
|
</div>
|
|
10329
|
+
${title}
|
|
10307
10330
|
`;
|
|
10308
|
-
|
|
10331
|
+
section.appendChild(header);
|
|
10332
|
+
return section;
|
|
10333
|
+
}
|
|
10334
|
+
createProgressBlock(accentColor, initialText) {
|
|
10335
|
+
const container = document.createElement('div');
|
|
10336
|
+
container.className = 'hidden';
|
|
10337
|
+
const barWrap = document.createElement('div');
|
|
10338
|
+
barWrap.className = 'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
10339
|
+
const bar = document.createElement('div');
|
|
10340
|
+
bar.className = `bg-${accentColor}-600 h-2 rounded-full transition-all duration-300`;
|
|
10341
|
+
bar.style.width = '0%';
|
|
10342
|
+
barWrap.appendChild(bar);
|
|
10343
|
+
const text = document.createElement('p');
|
|
10344
|
+
text.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10345
|
+
text.textContent = initialText;
|
|
10346
|
+
container.appendChild(barWrap);
|
|
10347
|
+
container.appendChild(text);
|
|
10348
|
+
return { container, bar, text };
|
|
10349
|
+
}
|
|
10350
|
+
createFooter() {
|
|
10351
|
+
const footer = document.createElement('div');
|
|
10352
|
+
footer.className = 'flex gap-3 justify-end';
|
|
10353
|
+
if (i18n.isRTL()) {
|
|
10354
|
+
footer.setAttribute('dir', 'rtl');
|
|
10355
|
+
}
|
|
10356
|
+
const close = new Button({
|
|
10357
|
+
text: t('app.close'),
|
|
10358
|
+
variant: 'secondary',
|
|
10359
|
+
onClick: () => this.hide(),
|
|
10360
|
+
});
|
|
10361
|
+
footer.appendChild(close.getElement());
|
|
10362
|
+
return footer;
|
|
10309
10363
|
}
|
|
10310
10364
|
attachEventListeners() {
|
|
10311
|
-
// Enable import button when file is selected
|
|
10312
10365
|
if (this.importFileInput && this.importButton) {
|
|
10313
10366
|
this.importFileInput.addEventListener('change', () => {
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
|
|
10317
|
-
}
|
|
10318
|
-
}
|
|
10319
|
-
else {
|
|
10320
|
-
if (this.importButton) {
|
|
10321
|
-
this.importButton.disabled = true;
|
|
10322
|
-
}
|
|
10323
|
-
}
|
|
10367
|
+
const hasFile = !!(this.importFileInput?.files && this.importFileInput.files.length > 0);
|
|
10368
|
+
if (this.importButton)
|
|
10369
|
+
this.importButton.disabled = !hasFile;
|
|
10324
10370
|
});
|
|
10325
10371
|
}
|
|
10326
10372
|
}
|
|
@@ -10330,34 +10376,27 @@
|
|
|
10330
10376
|
this.isExporting = true;
|
|
10331
10377
|
if (this.exportButton)
|
|
10332
10378
|
this.exportButton.disabled = true;
|
|
10379
|
+
this.exportProgressContainer?.classList.remove('hidden');
|
|
10333
10380
|
try {
|
|
10334
|
-
const
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
if (progressContainer) {
|
|
10344
|
-
progressContainer.classList.remove('hidden');
|
|
10345
|
-
}
|
|
10346
|
-
const result = await this.options.exportRegion(this.options.region.id, format, options);
|
|
10347
|
-
if (this.exportProgressBar) {
|
|
10381
|
+
const result = await this.options.exportRegion(this.options.region.id, {
|
|
10382
|
+
onProgress: p => {
|
|
10383
|
+
if (this.exportProgressBar)
|
|
10384
|
+
this.exportProgressBar.style.width = `${p.percentage}%`;
|
|
10385
|
+
if (this.exportProgressText)
|
|
10386
|
+
this.exportProgressText.textContent = p.message;
|
|
10387
|
+
},
|
|
10388
|
+
});
|
|
10389
|
+
if (this.exportProgressBar)
|
|
10348
10390
|
this.exportProgressBar.style.width = '100%';
|
|
10349
|
-
|
|
10350
|
-
|
|
10351
|
-
this.exportProgressText.textContent = t('importExport.exportComplete');
|
|
10352
|
-
}
|
|
10391
|
+
if (this.exportProgressText)
|
|
10392
|
+
this.exportProgressText.textContent = t('mbtiles.exportComplete');
|
|
10353
10393
|
this.options.onExport?.(result);
|
|
10354
|
-
|
|
10355
|
-
setTimeout(() => this.hide(), 1500);
|
|
10394
|
+
setTimeout(() => this.hide(), 1200);
|
|
10356
10395
|
}
|
|
10357
10396
|
catch (error) {
|
|
10358
10397
|
modalLogger.error('Export error:', error instanceof Error ? error.message : String(error));
|
|
10359
10398
|
if (this.exportProgressText) {
|
|
10360
|
-
this.exportProgressText.textContent = t('
|
|
10399
|
+
this.exportProgressText.textContent = t('mbtiles.exportFailed');
|
|
10361
10400
|
this.exportProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10362
10401
|
}
|
|
10363
10402
|
}
|
|
@@ -10368,45 +10407,47 @@
|
|
|
10368
10407
|
}
|
|
10369
10408
|
}
|
|
10370
10409
|
async handleImport() {
|
|
10371
|
-
if (this.isImporting || !this.options.importRegion
|
|
10410
|
+
if (this.isImporting || !this.options.importRegion)
|
|
10411
|
+
return;
|
|
10412
|
+
const file = this.importFileInput?.files?.[0];
|
|
10413
|
+
if (!file)
|
|
10372
10414
|
return;
|
|
10373
10415
|
this.isImporting = true;
|
|
10374
10416
|
if (this.importButton)
|
|
10375
10417
|
this.importButton.disabled = true;
|
|
10418
|
+
this.importProgressContainer?.classList.remove('hidden');
|
|
10376
10419
|
try {
|
|
10377
|
-
const file = this.importFileInput.files[0];
|
|
10378
|
-
const overwrite = this.importOverwriteCheckbox?.checked ?? false;
|
|
10379
|
-
// Show progress
|
|
10380
|
-
const progressContainer = this.importProgressBar?.parentElement?.parentElement;
|
|
10381
|
-
if (progressContainer) {
|
|
10382
|
-
progressContainer.classList.remove('hidden');
|
|
10383
|
-
}
|
|
10384
|
-
// Determine format from file extension
|
|
10385
|
-
const format = file.name.endsWith('.pmtiles')
|
|
10386
|
-
? 'pmtiles'
|
|
10387
|
-
: file.name.endsWith('.mbtiles')
|
|
10388
|
-
? 'mbtiles'
|
|
10389
|
-
: 'json';
|
|
10390
10420
|
const data = {
|
|
10391
10421
|
file,
|
|
10392
|
-
format,
|
|
10393
|
-
overwrite,
|
|
10422
|
+
format: 'mbtiles',
|
|
10423
|
+
overwrite: this.importOverwriteCheckbox?.checked ?? false,
|
|
10424
|
+
newRegionName: this.importNameInput?.value.trim() || undefined,
|
|
10425
|
+
onProgress: p => {
|
|
10426
|
+
if (this.importProgressBar)
|
|
10427
|
+
this.importProgressBar.style.width = `${p.percentage}%`;
|
|
10428
|
+
if (this.importProgressText)
|
|
10429
|
+
this.importProgressText.textContent = p.message;
|
|
10430
|
+
},
|
|
10394
10431
|
};
|
|
10395
10432
|
const result = await this.options.importRegion(data);
|
|
10396
|
-
if (this.importProgressBar)
|
|
10433
|
+
if (this.importProgressBar)
|
|
10397
10434
|
this.importProgressBar.style.width = '100%';
|
|
10398
|
-
}
|
|
10399
10435
|
if (this.importProgressText) {
|
|
10400
|
-
this.importProgressText.textContent =
|
|
10436
|
+
this.importProgressText.textContent = result.success
|
|
10437
|
+
? t('mbtiles.importComplete')
|
|
10438
|
+
: result.message;
|
|
10439
|
+
if (!result.success) {
|
|
10440
|
+
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10441
|
+
}
|
|
10401
10442
|
}
|
|
10402
10443
|
this.options.onImport?.(result);
|
|
10403
|
-
|
|
10404
|
-
|
|
10444
|
+
if (result.success)
|
|
10445
|
+
setTimeout(() => this.hide(), 1200);
|
|
10405
10446
|
}
|
|
10406
10447
|
catch (error) {
|
|
10407
10448
|
modalLogger.error('Import error:', error instanceof Error ? error.message : String(error));
|
|
10408
10449
|
if (this.importProgressText) {
|
|
10409
|
-
this.importProgressText.textContent = t('
|
|
10450
|
+
this.importProgressText.textContent = t('mbtiles.importFailed');
|
|
10410
10451
|
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10411
10452
|
}
|
|
10412
10453
|
}
|
|
@@ -10416,23 +10457,6 @@
|
|
|
10416
10457
|
this.importButton.disabled = false;
|
|
10417
10458
|
}
|
|
10418
10459
|
}
|
|
10419
|
-
createFooter() {
|
|
10420
|
-
const footer = document.createElement('div');
|
|
10421
|
-
footer.className = 'flex gap-3 justify-end';
|
|
10422
|
-
if (i18n.isRTL()) {
|
|
10423
|
-
footer.setAttribute('dir', 'rtl');
|
|
10424
|
-
}
|
|
10425
|
-
const closeButton = new Button({
|
|
10426
|
-
text: t('app.close'),
|
|
10427
|
-
variant: 'secondary',
|
|
10428
|
-
onClick: () => this.hide(),
|
|
10429
|
-
});
|
|
10430
|
-
footer.appendChild(closeButton.getElement());
|
|
10431
|
-
return footer;
|
|
10432
|
-
}
|
|
10433
|
-
destroy() {
|
|
10434
|
-
this.modal?.destroy();
|
|
10435
|
-
}
|
|
10436
10460
|
}
|
|
10437
10461
|
|
|
10438
10462
|
/**
|
|
@@ -11146,9 +11170,9 @@
|
|
|
11146
11170
|
<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')}">
|
|
11147
11171
|
${icons.download({ size: 14, color: 'currentColor' })}
|
|
11148
11172
|
</button>
|
|
11149
|
-
|
|
11173
|
+
<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')}">
|
|
11150
11174
|
${icons.deviceFloppy({ size: 14, color: 'currentColor' })}
|
|
11151
|
-
</button>
|
|
11175
|
+
</button>
|
|
11152
11176
|
<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')}">
|
|
11153
11177
|
${icons.trash({ size: 14, color: 'currentColor' })}
|
|
11154
11178
|
</button>
|
|
@@ -11410,7 +11434,7 @@
|
|
|
11410
11434
|
}
|
|
11411
11435
|
}
|
|
11412
11436
|
/**
|
|
11413
|
-
*
|
|
11437
|
+
* Show the MBTiles import/export modal for a region.
|
|
11414
11438
|
*/
|
|
11415
11439
|
async handleImportExport(regionId, _regionData) {
|
|
11416
11440
|
try {
|
|
@@ -11418,44 +11442,26 @@
|
|
|
11418
11442
|
const region = regions.find((r) => r.id === regionId);
|
|
11419
11443
|
if (!region)
|
|
11420
11444
|
return;
|
|
11421
|
-
const
|
|
11445
|
+
const mbtilesModal = new MBTilesModal({
|
|
11422
11446
|
region,
|
|
11423
|
-
onClose: () =>
|
|
11424
|
-
this.modalManager.close();
|
|
11425
|
-
},
|
|
11447
|
+
onClose: () => this.modalManager.close(),
|
|
11426
11448
|
onExport: result => {
|
|
11427
|
-
panelLogger.debug('
|
|
11428
|
-
// Handle export result - could show success message
|
|
11449
|
+
panelLogger.debug('MBTiles export completed:', result);
|
|
11429
11450
|
this.offlineManager.downloadExportedRegion(result);
|
|
11430
11451
|
},
|
|
11431
11452
|
onImport: result => {
|
|
11432
|
-
panelLogger.debug('
|
|
11433
|
-
|
|
11434
|
-
|
|
11435
|
-
},
|
|
11436
|
-
exportRegion: async (regionId, format, options) => {
|
|
11437
|
-
// Delegate to offline manager's export functionality
|
|
11438
|
-
switch (format) {
|
|
11439
|
-
case 'json':
|
|
11440
|
-
return await this.offlineManager.exportRegionAsJSON(regionId, options);
|
|
11441
|
-
case 'pmtiles':
|
|
11442
|
-
return await this.offlineManager.exportRegionAsPMTiles(regionId, options);
|
|
11443
|
-
case 'mbtiles':
|
|
11444
|
-
return await this.offlineManager.exportRegionAsMBTiles(regionId, options);
|
|
11445
|
-
default:
|
|
11446
|
-
throw new Error(`Unsupported export format: ${format}`);
|
|
11447
|
-
}
|
|
11448
|
-
},
|
|
11449
|
-
importRegion: async (data) => {
|
|
11450
|
-
// Delegate to offline manager's import functionality
|
|
11451
|
-
return await this.offlineManager.importRegion(data);
|
|
11453
|
+
panelLogger.debug('MBTiles import completed:', result);
|
|
11454
|
+
if (result.success)
|
|
11455
|
+
this.refresh();
|
|
11452
11456
|
},
|
|
11457
|
+
exportRegion: (id, options) => this.offlineManager.exportRegionAsMBTiles(id, options),
|
|
11458
|
+
importRegion: data => this.offlineManager.importRegion(data),
|
|
11453
11459
|
});
|
|
11454
|
-
const modal =
|
|
11460
|
+
const modal = mbtilesModal.show();
|
|
11455
11461
|
this.modalManager.show(modal);
|
|
11456
11462
|
}
|
|
11457
11463
|
catch (error) {
|
|
11458
|
-
panelLogger.error('Error showing
|
|
11464
|
+
panelLogger.error('Error showing MBTiles modal:', error);
|
|
11459
11465
|
}
|
|
11460
11466
|
}
|
|
11461
11467
|
/**
|
|
@@ -11854,6 +11860,9 @@
|
|
|
11854
11860
|
delete patchedStyle.imports;
|
|
11855
11861
|
panelLogger.debug('Stripped imports from offline style (already flattened)');
|
|
11856
11862
|
}
|
|
11863
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
11864
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
11865
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
11857
11866
|
// Enforce maxzoom for all tile sources to prevent requesting non-existent tiles
|
|
11858
11867
|
// Find the maximum zoom level from all regions using this style
|
|
11859
11868
|
let maxZoom = 14; // Default fallback
|
|
@@ -12595,9 +12604,7 @@
|
|
|
12595
12604
|
detectProviderFromUrl() {
|
|
12596
12605
|
const styleUrl = this.styleUrlInput?.value || '';
|
|
12597
12606
|
// Simple detection logic
|
|
12598
|
-
if (styleUrl.startsWith('mapbox://') ||
|
|
12599
|
-
styleUrl.includes('mapbox.com') ||
|
|
12600
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
12607
|
+
if (styleUrl.startsWith('mapbox://') || isMapboxHost(styleUrl)) {
|
|
12601
12608
|
if (this.providerSelect)
|
|
12602
12609
|
this.providerSelect.value = 'mapbox';
|
|
12603
12610
|
this.toggleAccessTokenVisibility(true);
|
|
@@ -13361,39 +13368,39 @@
|
|
|
13361
13368
|
}
|
|
13362
13369
|
// Development proxy for CORS issues (when running on localhost)
|
|
13363
13370
|
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
13364
|
-
|
|
13365
|
-
|
|
13366
|
-
|
|
13367
|
-
if (isTileRequest && url.includes('tiles-a.basemaps.cartocdn.com')) {
|
|
13368
|
-
const proxyUrl = url.replace('https://tiles-a.basemaps.cartocdn.com', '/tiles/carto-a');
|
|
13369
|
-
return originalFetch(proxyUrl, init);
|
|
13370
|
-
}
|
|
13371
|
-
if (isTileRequest && url.includes('tiles-b.basemaps.cartocdn.com')) {
|
|
13372
|
-
const proxyUrl = url.replace('https://tiles-b.basemaps.cartocdn.com', '/tiles/carto-b');
|
|
13373
|
-
return originalFetch(proxyUrl, init);
|
|
13371
|
+
let parsed = null;
|
|
13372
|
+
try {
|
|
13373
|
+
parsed = new URL(url, location.origin);
|
|
13374
13374
|
}
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
return originalFetch(proxyUrl, init);
|
|
13375
|
+
catch {
|
|
13376
|
+
parsed = null;
|
|
13378
13377
|
}
|
|
13379
|
-
|
|
13380
|
-
|
|
13381
|
-
|
|
13378
|
+
const hostname = parsed?.hostname ?? '';
|
|
13379
|
+
const pathAndQuery = parsed ? parsed.pathname + parsed.search : '';
|
|
13380
|
+
// Proxy Carto tile requests (tiles and TileJSON)
|
|
13381
|
+
const isTileRequest = /\/\d+\/\d+\/\d+\.(pbf|mvt|png|jpg|jpeg|webp)/.test(parsed?.pathname ?? '');
|
|
13382
|
+
const isTileJsonRequest = (parsed?.pathname.endsWith('.json') ?? false) &&
|
|
13383
|
+
(hostname === 'basemaps.cartocdn.com' || hostname.endsWith('.basemaps.cartocdn.com'));
|
|
13384
|
+
const cartoSubdomainProxy = {
|
|
13385
|
+
'tiles-a.basemaps.cartocdn.com': '/tiles/carto-a',
|
|
13386
|
+
'tiles-b.basemaps.cartocdn.com': '/tiles/carto-b',
|
|
13387
|
+
'tiles-c.basemaps.cartocdn.com': '/tiles/carto-c',
|
|
13388
|
+
'tiles-d.basemaps.cartocdn.com': '/tiles/carto-d',
|
|
13389
|
+
};
|
|
13390
|
+
if (isTileRequest && cartoSubdomainProxy[hostname]) {
|
|
13391
|
+
return originalFetch(cartoSubdomainProxy[hostname] + pathAndQuery, init);
|
|
13382
13392
|
}
|
|
13383
13393
|
// Proxy TileJSON requests from tiles.basemaps.cartocdn.com
|
|
13384
|
-
if (isTileJsonRequest &&
|
|
13385
|
-
|
|
13386
|
-
return originalFetch(proxyUrl, init);
|
|
13394
|
+
if (isTileJsonRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13395
|
+
return originalFetch('/carto-api' + pathAndQuery, init);
|
|
13387
13396
|
}
|
|
13388
13397
|
// Fallback for old format (tiles without subdomain)
|
|
13389
|
-
if (isTileRequest &&
|
|
13390
|
-
|
|
13391
|
-
return originalFetch(proxyUrl, init);
|
|
13398
|
+
if (isTileRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13399
|
+
return originalFetch('/tiles/carto-a' + pathAndQuery, init);
|
|
13392
13400
|
}
|
|
13393
13401
|
// Proxy OpenStreetMap tile requests
|
|
13394
|
-
if (
|
|
13395
|
-
|
|
13396
|
-
return originalFetch(proxyUrl, init);
|
|
13402
|
+
if (hostname === 'tile.openstreetmap.org') {
|
|
13403
|
+
return originalFetch('/tiles/osm' + pathAndQuery, init);
|
|
13397
13404
|
}
|
|
13398
13405
|
}
|
|
13399
13406
|
return originalFetch(input, init);
|
|
@@ -13826,6 +13833,9 @@
|
|
|
13826
13833
|
if (patchedStyle.imports) {
|
|
13827
13834
|
delete patchedStyle.imports;
|
|
13828
13835
|
}
|
|
13836
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
13837
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
13838
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
13829
13839
|
// If using Service Worker (Mapbox GL JS), convert idb:// to /__offline__/ URLs
|
|
13830
13840
|
if (this.useServiceWorker) {
|
|
13831
13841
|
if (this.swReadyPromise) {
|
|
@@ -13979,6 +13989,7 @@
|
|
|
13979
13989
|
exports.DOWNLOAD_DEFAULTS = DOWNLOAD_DEFAULTS;
|
|
13980
13990
|
exports.ERROR_MESSAGES = ERROR_MESSAGES;
|
|
13981
13991
|
exports.FontService = FontService;
|
|
13992
|
+
exports.GLYPH_BLOCK_SIZE = GLYPH_BLOCK_SIZE;
|
|
13982
13993
|
exports.GLYPH_CONFIG = GLYPH_CONFIG;
|
|
13983
13994
|
exports.GZIP_MAGIC_BYTES = GZIP_MAGIC_BYTES;
|
|
13984
13995
|
exports.GlyphService = GlyphService;
|
|
@@ -13987,6 +13998,7 @@
|
|
|
13987
13998
|
exports.MAPBOX_CACHE_TTL = MAPBOX_CACHE_TTL;
|
|
13988
13999
|
exports.MAPBOX_CLASSIC_STYLES = MAPBOX_CLASSIC_STYLES;
|
|
13989
14000
|
exports.MAP_PROVIDERS = MAP_PROVIDERS;
|
|
14001
|
+
exports.MAX_GLYPH_CODEPOINT = MAX_GLYPH_CODEPOINT;
|
|
13990
14002
|
exports.MaintenanceService = MaintenanceService;
|
|
13991
14003
|
exports.ModelService = ModelService;
|
|
13992
14004
|
exports.OfflineManagerControl = OfflineManagerControl;
|
|
@@ -14019,6 +14031,7 @@
|
|
|
14019
14031
|
exports.clearAllCaches = clearAllCaches;
|
|
14020
14032
|
exports.configureLogger = configureLogger;
|
|
14021
14033
|
exports.configureProxy = configureProxy;
|
|
14034
|
+
exports.configureSqlJs = configureSqlJs;
|
|
14022
14035
|
exports.convertStyleForServiceWorker = convertStyleForServiceWorker;
|
|
14023
14036
|
exports.countCompressedTiles = countCompressedTiles;
|
|
14024
14037
|
exports.createProgressTracker = createProgressTracker;
|
|
@@ -14041,6 +14054,7 @@
|
|
|
14041
14054
|
exports.extractAccessToken = extractAccessToken;
|
|
14042
14055
|
exports.extractAllFontNames = extractAllFontNames;
|
|
14043
14056
|
exports.extractFontNamesFromTextField = extractFontNamesFromTextField;
|
|
14057
|
+
exports.extractTileExtensionFromUrl = extractTileExtensionFromUrl;
|
|
14044
14058
|
exports.fetchResourceWithRetry = fetchResourceWithRetry;
|
|
14045
14059
|
exports.fetchWithRetry = fetchWithRetry;
|
|
14046
14060
|
exports.fontService = fontService;
|
|
@@ -14058,15 +14072,19 @@
|
|
|
14058
14072
|
exports.getRegionAnalytics = getRegionAnalytics;
|
|
14059
14073
|
exports.getSpriteAnalytics = getSpriteAnalytics;
|
|
14060
14074
|
exports.getSpriteStats = getSpriteStats;
|
|
14075
|
+
exports.getSqlJs = getSqlJs;
|
|
14061
14076
|
exports.getStyleStats = getStyleStats;
|
|
14062
14077
|
exports.getTileAnalytics = getTileAnalytics;
|
|
14063
14078
|
exports.getTileStats = getTileStats;
|
|
14079
|
+
exports.getUrlHostname = getUrlHostname;
|
|
14064
14080
|
exports.getUserErrorMessage = getUserErrorMessage;
|
|
14065
14081
|
exports.glyphService = glyphService;
|
|
14066
14082
|
exports.hasImports = hasImports;
|
|
14083
|
+
exports.hostMatches = hostMatches;
|
|
14067
14084
|
exports.i18n = i18n;
|
|
14068
14085
|
exports.icons = icons;
|
|
14069
14086
|
exports.idbFetchHandler = idbFetchHandler;
|
|
14087
|
+
exports.isMapboxHost = isMapboxHost;
|
|
14070
14088
|
exports.isMapboxProtocol = isMapboxProtocol;
|
|
14071
14089
|
exports.isStyleDownloaded = isStyleDownloaded;
|
|
14072
14090
|
exports.loadAllStoredRegions = loadAllStoredRegions;
|
|
@@ -14092,6 +14110,7 @@
|
|
|
14092
14110
|
exports.resourceKeyBelongsToStyle = resourceKeyBelongsToStyle;
|
|
14093
14111
|
exports.rewriteMapboxCdnTileUrl = rewriteMapboxCdnTileUrl;
|
|
14094
14112
|
exports.safeExecute = safeExecute;
|
|
14113
|
+
exports.sanitizeIndoorExpressions = sanitizeIndoorExpressions;
|
|
14095
14114
|
exports.setupAutoCleanup = setupAutoCleanup;
|
|
14096
14115
|
exports.spriteService = spriteService;
|
|
14097
14116
|
exports.stopAutoCleanup = stopAutoCleanup;
|