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