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.esm.js
CHANGED
|
@@ -38,45 +38,61 @@ const TILE_CONFIG = {
|
|
|
38
38
|
SUPPORTED_EXTENSIONS: ['pbf', 'mvt', 'png', 'jpg', 'jpeg', 'webp', 'glb'],
|
|
39
39
|
};
|
|
40
40
|
// Glyph Configuration
|
|
41
|
+
//
|
|
42
|
+
// Glyph servers (MapTiler, Mapbox, OpenFreeMap, ...) serve glyphs in fixed
|
|
43
|
+
// 256-codepoint blocks aligned to a multiple of 256: every request must be
|
|
44
|
+
// `${k * 256}-${k * 256 + 255}`. Strict servers (e.g. MapTiler) reject any
|
|
45
|
+
// other range with HTTP 400 "Invalid glyph range"; lenient ones silently
|
|
46
|
+
// accept them, which is how malformed ranges went unnoticed. See issue #37.
|
|
47
|
+
const GLYPH_BLOCK_SIZE = 256;
|
|
48
|
+
const MAX_GLYPH_CODEPOINT = 65535;
|
|
49
|
+
/**
|
|
50
|
+
* Expand an inclusive Unicode codepoint span into the aligned 256-codepoint
|
|
51
|
+
* glyph blocks that cover it, formatted as `"start-end"` request ranges.
|
|
52
|
+
* The span need not be block-aligned — it is snapped out to whole blocks.
|
|
53
|
+
*/
|
|
54
|
+
function glyphBlocksForSpan(start, end) {
|
|
55
|
+
const firstBlock = Math.floor(start / GLYPH_BLOCK_SIZE);
|
|
56
|
+
const lastBlock = Math.floor(Math.min(end, MAX_GLYPH_CODEPOINT) / GLYPH_BLOCK_SIZE);
|
|
57
|
+
const blocks = [];
|
|
58
|
+
for (let block = firstBlock; block <= lastBlock; block++) {
|
|
59
|
+
const blockStart = block * GLYPH_BLOCK_SIZE;
|
|
60
|
+
blocks.push(`${blockStart}-${blockStart + GLYPH_BLOCK_SIZE - 1}`);
|
|
61
|
+
}
|
|
62
|
+
return blocks;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Unicode codepoint spans the comprehensive glyph download aims to cover.
|
|
66
|
+
* Each span is snapped to whole 256-codepoint glyph blocks below, so the
|
|
67
|
+
* resulting request ranges are always server-valid regardless of where the
|
|
68
|
+
* underlying Unicode blocks happen to start or end. To extend coverage, add
|
|
69
|
+
* a span here — never hand-write raw `"start-end"` ranges.
|
|
70
|
+
*/
|
|
71
|
+
const GLYPH_COVERAGE_SPANS = [
|
|
72
|
+
[0x0000, 0x12ff], // Latin, Greek, Cyrillic, Hebrew, Arabic, Indic, SE Asian, Georgian, Ethiopic, Cherokee
|
|
73
|
+
[0x1e00, 0x21ff], // Latin Extended Additional, punctuation, symbols, arrows
|
|
74
|
+
[0x2e00, 0x31ff], // CJK radicals, Hiragana, Katakana, Bopomofo, Hangul Compatibility Jamo
|
|
75
|
+
[0x4e00, 0x4fff], // CJK Unified Ideographs (common subset)
|
|
76
|
+
[0xa000, 0xa4ff], // Yi Syllables and Radicals
|
|
77
|
+
[0xac00, 0xd7ff], // Hangul Syllables (Korean)
|
|
78
|
+
[0xf900, 0xfbff], // CJK Compatibility Ideographs, Alphabetic Presentation Forms
|
|
79
|
+
[0xfe00, 0xfeff], // Variation Selectors
|
|
80
|
+
[0xff00, 0xffff], // Halfwidth and Fullwidth Forms
|
|
81
|
+
];
|
|
82
|
+
/** Build the deduped, codepoint-ascending list of comprehensive glyph ranges. */
|
|
83
|
+
function buildComprehensiveRanges() {
|
|
84
|
+
const ranges = new Set();
|
|
85
|
+
for (const [start, end] of GLYPH_COVERAGE_SPANS) {
|
|
86
|
+
for (const range of glyphBlocksForSpan(start, end)) {
|
|
87
|
+
ranges.add(range);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return Array.from(ranges).sort((a, b) => Number(a.split('-')[0]) - Number(b.split('-')[0]));
|
|
91
|
+
}
|
|
41
92
|
const GLYPH_CONFIG = {
|
|
42
93
|
DEFAULT_URL: 'https://tiles.openfreemap.org/fonts/{fontstack}/{range}.pbf',
|
|
43
94
|
DEFAULT_RANGES: ['0-255'],
|
|
44
|
-
COMPREHENSIVE_RANGES:
|
|
45
|
-
'0-255', // Basic Latin + Latin-1 Supplement
|
|
46
|
-
'256-511', // Latin Extended-A + Latin Extended-B
|
|
47
|
-
'512-767', // IPA Extensions + Spacing Modifier Letters
|
|
48
|
-
'768-1023', // Combining Diacritical Marks + Greek and Coptic
|
|
49
|
-
'1024-1279', // Cyrillic + Cyrillic Supplement
|
|
50
|
-
'1280-1535', // Armenian + Hebrew
|
|
51
|
-
'1536-1791', // Arabic
|
|
52
|
-
'1792-2047', // Syriac + Arabic Supplement + Thaana
|
|
53
|
-
'2048-2303', // NKo + Samaritan + Mandaic
|
|
54
|
-
'2304-2559', // Devanagari + Bengali
|
|
55
|
-
'2560-2815', // Gurmukhi + Gujarati
|
|
56
|
-
'2816-3071', // Oriya + Tamil
|
|
57
|
-
'3072-3327', // Telugu + Kannada
|
|
58
|
-
'3328-3583', // Malayalam + Sinhala
|
|
59
|
-
'3584-3839', // Thai + Lao
|
|
60
|
-
'3840-4095', // Tibetan + Myanmar
|
|
61
|
-
'4096-4351', // Georgian + Hangul Jamo
|
|
62
|
-
'4352-4607', // Ethiopic
|
|
63
|
-
'4608-4863', // Cherokee + Canadian Aboriginal
|
|
64
|
-
'7680-7935', // Latin Extended Additional
|
|
65
|
-
'8192-8447', // General Punctuation, Superscripts/Subscripts, Currency Symbols
|
|
66
|
-
'8448-8703', // Letterlike Symbols, Number Forms, Arrows
|
|
67
|
-
'11904-12031', // CJK Radicals Supplement
|
|
68
|
-
'12032-12255', // Kangxi Radicals + CJK Symbols
|
|
69
|
-
'12288-12543', // Hiragana + Katakana
|
|
70
|
-
'12544-12799', // Bopomofo + Hangul Compatibility Jamo
|
|
71
|
-
'19968-20223', // CJK Unified Ideographs (first block)
|
|
72
|
-
'20224-20479', // CJK Unified Ideographs
|
|
73
|
-
'40960-42127', // Yi Syllables + Yi Radicals
|
|
74
|
-
'44032-55203', // Hangul Syllables (Korean)
|
|
75
|
-
'63744-64255', // CJK Compatibility Ideographs
|
|
76
|
-
'64256-64511', // Alphabetic Presentation Forms
|
|
77
|
-
'65024-65279', // Variation Selectors
|
|
78
|
-
'65280-65535', // Halfwidth and Fullwidth Forms
|
|
79
|
-
],
|
|
95
|
+
COMPREHENSIVE_RANGES: buildComprehensiveRanges(),
|
|
80
96
|
};
|
|
81
97
|
// Style Configuration
|
|
82
98
|
const STYLE_CONFIG = {
|
|
@@ -1167,6 +1183,22 @@ function parseTileKey(key) {
|
|
|
1167
1183
|
}
|
|
1168
1184
|
return { styleId, sourceId, z, x, y, ext };
|
|
1169
1185
|
}
|
|
1186
|
+
/**
|
|
1187
|
+
* Extract the extension (the last dotted segment before `?`, `#`, or end) from
|
|
1188
|
+
* a tile URL or tile URL template. Defaults to `"pbf"` when no extension can
|
|
1189
|
+
* be parsed. For multi-extension URLs like Mapbox v4's `{y}.vector.pbf` this
|
|
1190
|
+
* returns `"pbf"`, matching the key used when the tile is stored.
|
|
1191
|
+
*
|
|
1192
|
+
* Keeping extraction logic in one place ensures patchStyleForOffline (which
|
|
1193
|
+
* rewrites tile URLs to `idb://` at load time) derives the same extension
|
|
1194
|
+
* that tileService.extractExtension used at store time — otherwise the
|
|
1195
|
+
* first-try lookup in idbFetchHandler misses and has to fall through its
|
|
1196
|
+
* pbf/mvt/png/jpg/webp fallback loop.
|
|
1197
|
+
*/
|
|
1198
|
+
function extractTileExtensionFromUrl(url) {
|
|
1199
|
+
const match = url.match(/\.([\w]+)(?:[?#]|$)/i);
|
|
1200
|
+
return match ? match[1] : 'pbf';
|
|
1201
|
+
}
|
|
1170
1202
|
/**
|
|
1171
1203
|
* Derive tile extension from tile URL templates
|
|
1172
1204
|
*/
|
|
@@ -1174,15 +1206,210 @@ function deriveTileExtension(tiles) {
|
|
|
1174
1206
|
if (Array.isArray(tiles) && tiles.length > 0) {
|
|
1175
1207
|
const firstTile = tiles[0];
|
|
1176
1208
|
if (typeof firstTile === 'string') {
|
|
1177
|
-
|
|
1178
|
-
if (match) {
|
|
1179
|
-
return match[1];
|
|
1180
|
-
}
|
|
1209
|
+
return extractTileExtensionFromUrl(firstTile);
|
|
1181
1210
|
}
|
|
1182
1211
|
}
|
|
1183
1212
|
return 'pbf';
|
|
1184
1213
|
}
|
|
1185
1214
|
|
|
1215
|
+
/**
|
|
1216
|
+
* Pure helpers shared between the main-thread offline fetch handler
|
|
1217
|
+
* (`src/utils/idbFetchHandler.ts`) and the offline Service Worker
|
|
1218
|
+
* (`src/sw/offline-sw.ts`, compiled to `public/idb-offline-sw.js`).
|
|
1219
|
+
*
|
|
1220
|
+
* Keeping these in one place means the SW and the main-thread handler
|
|
1221
|
+
* can't drift — adding a new `model` handler, changing the fallback
|
|
1222
|
+
* order, or tweaking the tilejson-source matcher happens once.
|
|
1223
|
+
*
|
|
1224
|
+
* Nothing in here touches IndexedDB directly. Each helper takes already-
|
|
1225
|
+
* resolved inputs and returns the list of candidate keys (or the
|
|
1226
|
+
* resolved output) that the caller feeds into its own IDB lookup.
|
|
1227
|
+
*
|
|
1228
|
+
* The corresponding IDB access layer is:
|
|
1229
|
+
* - main thread: `idb` library via `dbPromise`
|
|
1230
|
+
* - service worker: raw `indexedDB.open` (see `offline-sw.ts`)
|
|
1231
|
+
*
|
|
1232
|
+
* They have different shapes so cannot be shared; the key computation
|
|
1233
|
+
* can be and is.
|
|
1234
|
+
*/
|
|
1235
|
+
/**
|
|
1236
|
+
* Extensions to try in order when the requested extension misses. `glb` is
|
|
1237
|
+
* last so batched-model sources (Mapbox Standard 3D buildings) resolve when
|
|
1238
|
+
* their source URL template ended in `.vector` or similar and the actual
|
|
1239
|
+
* tile body was stored as glb.
|
|
1240
|
+
*/
|
|
1241
|
+
const TILE_FALLBACK_EXTENSIONS = ['pbf', 'mvt', 'png', 'jpg', 'webp', 'glb'];
|
|
1242
|
+
/** Extensions minus the one the caller already tried. */
|
|
1243
|
+
function tileFallbackExtensions(requested) {
|
|
1244
|
+
return TILE_FALLBACK_EXTENSIONS.filter(e => e !== requested);
|
|
1245
|
+
}
|
|
1246
|
+
// ---------------------------------------------------------------------------
|
|
1247
|
+
// Region → style lookup
|
|
1248
|
+
// ---------------------------------------------------------------------------
|
|
1249
|
+
/**
|
|
1250
|
+
* Given an already-fetched list of style entries, find the first one whose
|
|
1251
|
+
* `regions` array contains the given ID. Pure — the caller is responsible for
|
|
1252
|
+
* loading the entries and for caching. Used by both `findStyleByRegionId`
|
|
1253
|
+
* implementations to keep the match rule identical.
|
|
1254
|
+
*/
|
|
1255
|
+
function findStyleByRegionIdIn(styles, regionId) {
|
|
1256
|
+
for (const entry of styles) {
|
|
1257
|
+
const regions = entry.regions;
|
|
1258
|
+
if (!Array.isArray(regions))
|
|
1259
|
+
continue;
|
|
1260
|
+
for (const r of regions) {
|
|
1261
|
+
if (r?.regionId === regionId || r?.id === regionId) {
|
|
1262
|
+
return entry;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
// ---------------------------------------------------------------------------
|
|
1269
|
+
// Glyph candidate keys
|
|
1270
|
+
// ---------------------------------------------------------------------------
|
|
1271
|
+
/**
|
|
1272
|
+
* Parse `FontA,FontB,FontC/0-255.pbf` into (fontstacks, rangePart). Mapbox
|
|
1273
|
+
* requests a comma-joined font-family fallback chain; each glyph is stored
|
|
1274
|
+
* individually, so the caller tries each fontstack in order.
|
|
1275
|
+
*/
|
|
1276
|
+
function parseGlyphPath(decodedPath) {
|
|
1277
|
+
const pathParts = decodedPath.split('/');
|
|
1278
|
+
const fontstackPart = pathParts[0] ?? '';
|
|
1279
|
+
const rangePart = pathParts[1] || '0-255.pbf';
|
|
1280
|
+
const fontstacks = fontstackPart
|
|
1281
|
+
.split(',')
|
|
1282
|
+
.map(f => f.trim())
|
|
1283
|
+
.filter(Boolean);
|
|
1284
|
+
return { fontstacks, rangePart };
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Build the list of keys to try for a single (fontstack, range) pair.
|
|
1288
|
+
* Order: actualStyleId variants first (most common), then downloadId,
|
|
1289
|
+
* then the bare path. Normalized and raw `.pbf`-less forms are both tried
|
|
1290
|
+
* to cover stored-key variants from older versions.
|
|
1291
|
+
*/
|
|
1292
|
+
function glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart) {
|
|
1293
|
+
const glyphPath = `${fontstack}/${rangePart}`;
|
|
1294
|
+
const normalizedPath = glyphPath.endsWith('.pbf') ? glyphPath : `${glyphPath}.pbf`;
|
|
1295
|
+
return dedupe([
|
|
1296
|
+
`${actualStyleId}::${normalizedPath}`,
|
|
1297
|
+
`${actualStyleId}::${glyphPath}`,
|
|
1298
|
+
`${downloadId}::${normalizedPath}`,
|
|
1299
|
+
`${downloadId}::${glyphPath}`,
|
|
1300
|
+
normalizedPath,
|
|
1301
|
+
glyphPath,
|
|
1302
|
+
]);
|
|
1303
|
+
}
|
|
1304
|
+
// ---------------------------------------------------------------------------
|
|
1305
|
+
// Sprite candidate keys
|
|
1306
|
+
// ---------------------------------------------------------------------------
|
|
1307
|
+
/**
|
|
1308
|
+
* Sprite keys have historically used both `::` and `:` as the separator, and
|
|
1309
|
+
* both the full filename (`sprite.json`) and the bare name (`sprite`). Return
|
|
1310
|
+
* every variant in priority order; the caller stops at the first hit.
|
|
1311
|
+
*/
|
|
1312
|
+
function spriteCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1313
|
+
const stripExt = decodedPath.replace(/\.(json|png)$/i, '');
|
|
1314
|
+
return dedupe([
|
|
1315
|
+
`${actualStyleId}::${decodedPath}`,
|
|
1316
|
+
`${actualStyleId}:${decodedPath}`,
|
|
1317
|
+
`${actualStyleId}::${stripExt}`,
|
|
1318
|
+
`${actualStyleId}:${stripExt}`,
|
|
1319
|
+
`${downloadId}::${decodedPath}`,
|
|
1320
|
+
`${downloadId}:${decodedPath}`,
|
|
1321
|
+
`${downloadId}::${stripExt}`,
|
|
1322
|
+
`${downloadId}:${stripExt}`,
|
|
1323
|
+
decodedPath,
|
|
1324
|
+
]);
|
|
1325
|
+
}
|
|
1326
|
+
// ---------------------------------------------------------------------------
|
|
1327
|
+
// Model candidate keys
|
|
1328
|
+
// ---------------------------------------------------------------------------
|
|
1329
|
+
/**
|
|
1330
|
+
* Model keys are `{styleId}::model::{name}`. Try the resolved style id first,
|
|
1331
|
+
* then the bare downloadId in case the request came through the region-scoped
|
|
1332
|
+
* URL form (`idb://{regionId}/model/{name}`).
|
|
1333
|
+
*/
|
|
1334
|
+
function modelCandidateKeys(actualStyleId, downloadId, decodedPath) {
|
|
1335
|
+
return dedupe([
|
|
1336
|
+
`${actualStyleId}::model::${decodedPath}`,
|
|
1337
|
+
`${downloadId}::model::${decodedPath}`,
|
|
1338
|
+
]);
|
|
1339
|
+
}
|
|
1340
|
+
// ---------------------------------------------------------------------------
|
|
1341
|
+
// TileJSON source matching
|
|
1342
|
+
// ---------------------------------------------------------------------------
|
|
1343
|
+
/**
|
|
1344
|
+
* Mapbox GL requests tilejson via `idb://{downloadId}/tilesjson/{path}` where
|
|
1345
|
+
* `{path}` may be the source id, the original TileJSON URL, or the URL we
|
|
1346
|
+
* stashed under `__originalTilesetUrl` when patching for offline. Try all
|
|
1347
|
+
* three; return the matching source id + its config, or null.
|
|
1348
|
+
*/
|
|
1349
|
+
function matchTileJsonSource(sources, decodedPath) {
|
|
1350
|
+
const asConfig = (v) => v && typeof v === 'object' ? v : null;
|
|
1351
|
+
if (decodedPath in sources) {
|
|
1352
|
+
const config = asConfig(sources[decodedPath]);
|
|
1353
|
+
if (config)
|
|
1354
|
+
return { sourceId: decodedPath, config };
|
|
1355
|
+
}
|
|
1356
|
+
for (const [sourceId, raw] of Object.entries(sources)) {
|
|
1357
|
+
const config = asConfig(raw);
|
|
1358
|
+
if (!config)
|
|
1359
|
+
continue;
|
|
1360
|
+
const url = typeof config.url === 'string' ? config.url : undefined;
|
|
1361
|
+
const original = typeof config.__originalTilesetUrl === 'string'
|
|
1362
|
+
? config.__originalTilesetUrl
|
|
1363
|
+
: undefined;
|
|
1364
|
+
if (url === decodedPath || original === decodedPath) {
|
|
1365
|
+
return { sourceId, config };
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Build the offline TileJSON payload that replaces the one Mapbox would
|
|
1372
|
+
* have fetched from the network. `tiles` is rewritten to serve from the SW
|
|
1373
|
+
* (the caller supplies the scheme via `tileUrlScheme`); copyable TileJSON
|
|
1374
|
+
* fields are preserved.
|
|
1375
|
+
*/
|
|
1376
|
+
function buildOfflineTileJson(sourceConfig, downloadId, sourceId, extension, tileUrlScheme, origin) {
|
|
1377
|
+
const base = `idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`
|
|
1378
|
+
;
|
|
1379
|
+
const tileJson = {
|
|
1380
|
+
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1381
|
+
name: sourceConfig.name ?? sourceId,
|
|
1382
|
+
tiles: [base],
|
|
1383
|
+
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1384
|
+
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1385
|
+
};
|
|
1386
|
+
const copyable = [
|
|
1387
|
+
'bounds',
|
|
1388
|
+
'center',
|
|
1389
|
+
'vector_layers',
|
|
1390
|
+
'scheme',
|
|
1391
|
+
'attribution',
|
|
1392
|
+
'encoding',
|
|
1393
|
+
'format',
|
|
1394
|
+
'grids',
|
|
1395
|
+
'data',
|
|
1396
|
+
'template',
|
|
1397
|
+
'version',
|
|
1398
|
+
];
|
|
1399
|
+
for (const field of copyable) {
|
|
1400
|
+
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1401
|
+
tileJson[field] = sourceConfig[field];
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return tileJson;
|
|
1405
|
+
}
|
|
1406
|
+
// ---------------------------------------------------------------------------
|
|
1407
|
+
// Internal helpers
|
|
1408
|
+
// ---------------------------------------------------------------------------
|
|
1409
|
+
function dedupe(values) {
|
|
1410
|
+
return Array.from(new Set(values));
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1186
1413
|
// idbFetchHandler.ts
|
|
1187
1414
|
// Intercepts idb:// URLs and serves resources from IndexedDB for MapLibre GL offline mode
|
|
1188
1415
|
const idbLogger = logger.scope('IDBFetch');
|
|
@@ -1239,16 +1466,11 @@ async function findStyleByRegionId(db, regionId) {
|
|
|
1239
1466
|
}
|
|
1240
1467
|
try {
|
|
1241
1468
|
const allStyles = await db.getAll('styles');
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
// Cache the result
|
|
1248
|
-
regionToStyleCache.set(regionId, { styleEntry, timestamp: Date.now() });
|
|
1249
|
-
return styleEntry;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1469
|
+
const hit = findStyleByRegionIdIn(allStyles, regionId);
|
|
1470
|
+
if (hit) {
|
|
1471
|
+
idbLogger.debug(`Found style "${hit.key}" containing region: ${regionId}`);
|
|
1472
|
+
regionToStyleCache.set(regionId, { styleEntry: hit, timestamp: Date.now() });
|
|
1473
|
+
return hit;
|
|
1252
1474
|
}
|
|
1253
1475
|
idbLogger.debug(`No style found containing region: ${regionId}`);
|
|
1254
1476
|
// Don't cache negative results — the region may be stored moments later
|
|
@@ -1260,36 +1482,6 @@ async function findStyleByRegionId(db, regionId) {
|
|
|
1260
1482
|
return null;
|
|
1261
1483
|
}
|
|
1262
1484
|
}
|
|
1263
|
-
function buildOfflineTileJson(sourceConfig, downloadId, sourceId) {
|
|
1264
|
-
const extension = deriveTileExtension(sourceConfig.tiles);
|
|
1265
|
-
const offlineTiles = [`idb://${downloadId}/tile/${sourceId}/{z}/{x}/{y}.${extension}`];
|
|
1266
|
-
const tileJson = {
|
|
1267
|
-
tilejson: typeof sourceConfig.tilejson === 'string' ? sourceConfig.tilejson : '2.2.0',
|
|
1268
|
-
name: sourceConfig.name ?? sourceId,
|
|
1269
|
-
tiles: offlineTiles,
|
|
1270
|
-
minzoom: typeof sourceConfig.minzoom === 'number' ? sourceConfig.minzoom : 0,
|
|
1271
|
-
maxzoom: typeof sourceConfig.maxzoom === 'number' ? sourceConfig.maxzoom : 22,
|
|
1272
|
-
};
|
|
1273
|
-
const fieldsToCopy = [
|
|
1274
|
-
'bounds',
|
|
1275
|
-
'center',
|
|
1276
|
-
'vector_layers',
|
|
1277
|
-
'scheme',
|
|
1278
|
-
'attribution',
|
|
1279
|
-
'encoding',
|
|
1280
|
-
'format',
|
|
1281
|
-
'grids',
|
|
1282
|
-
'data',
|
|
1283
|
-
'template',
|
|
1284
|
-
'version',
|
|
1285
|
-
];
|
|
1286
|
-
for (const field of fieldsToCopy) {
|
|
1287
|
-
if (field in sourceConfig && sourceConfig[field] !== undefined) {
|
|
1288
|
-
tileJson[field] = sourceConfig[field];
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
return tileJson;
|
|
1292
|
-
}
|
|
1293
1485
|
async function createTileResponse(resource) {
|
|
1294
1486
|
const headers = {};
|
|
1295
1487
|
// Set proper content type for vector tiles (PBF/MVT format)
|
|
@@ -1384,7 +1576,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1384
1576
|
// but tiles are stored with integer zoom levels, so floor the value
|
|
1385
1577
|
const z = Math.floor(parseFloat(pathParts[pathParts.length - 3]));
|
|
1386
1578
|
const sourceKey = pathParts.slice(0, pathParts.length - 3).join('/');
|
|
1387
|
-
const yMatch = yExt.match(
|
|
1579
|
+
const yMatch = yExt.match(/^(\d+)\.(\w+)$/);
|
|
1388
1580
|
if (yMatch) {
|
|
1389
1581
|
const y = parseInt(yMatch[1]);
|
|
1390
1582
|
const requestedExt = yMatch[2]; // Extension from URL (for logging only)
|
|
@@ -1399,7 +1591,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1399
1591
|
}
|
|
1400
1592
|
idbLogger.debug(`Tile not found: ${tileKey}`);
|
|
1401
1593
|
// Fallback: try common alternative extensions
|
|
1402
|
-
const fallbackExtensions =
|
|
1594
|
+
const fallbackExtensions = tileFallbackExtensions(requestedExt);
|
|
1403
1595
|
for (const fallbackExt of fallbackExtensions) {
|
|
1404
1596
|
const fallbackKey = createTileKey(x, y, z, actualStyleId, sourceKey, fallbackExt);
|
|
1405
1597
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1441,7 +1633,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1441
1633
|
return await createTileResponse(resource);
|
|
1442
1634
|
}
|
|
1443
1635
|
// Try alternative extensions
|
|
1444
|
-
const fallbackExts =
|
|
1636
|
+
const fallbackExts = tileFallbackExtensions(ext);
|
|
1445
1637
|
for (const fallbackExt of fallbackExts) {
|
|
1446
1638
|
const fallbackKey = createTileKey(parseInt(x), parseInt(y), parseInt(z), actualStyleId, fallbackSourceKey, fallbackExt);
|
|
1447
1639
|
const fallbackResource = await db.get('tiles', fallbackKey);
|
|
@@ -1461,46 +1653,19 @@ async function idbFetchHandler(url, init) {
|
|
|
1461
1653
|
}
|
|
1462
1654
|
case 'glyph': {
|
|
1463
1655
|
idbLogger.debug(`Looking for glyph with key: ${key}`);
|
|
1464
|
-
// Find which style this region belongs to
|
|
1465
1656
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1466
1657
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1467
|
-
|
|
1468
|
-
idbLogger.debug(`Region "${downloadId}" belongs to style "${actualStyleId}", searching with style key`);
|
|
1469
|
-
}
|
|
1470
|
-
// Parse the resource path: "FontA,FontB,FontC/0-255.pbf"
|
|
1471
|
-
// MapLibre requests glyphs with comma-separated fallback fonts
|
|
1472
|
-
// but glyphs are stored individually per font
|
|
1473
|
-
const pathParts = decodedResourcePath.split('/');
|
|
1474
|
-
const fontstackPart = pathParts[0]; // "FontA,FontB,FontC"
|
|
1475
|
-
const rangePart = pathParts[1] || '0-255.pbf'; // "0-255.pbf"
|
|
1476
|
-
// Split comma-separated fonts
|
|
1477
|
-
const fontstacks = fontstackPart.split(',').map(f => f.trim());
|
|
1658
|
+
const { fontstacks, rangePart } = parseGlyphPath(decodedResourcePath);
|
|
1478
1659
|
idbLogger.debug(`Trying ${fontstacks.length} fonts in fallback order: ${fontstacks.join(', ')}`);
|
|
1479
|
-
// Try each font in order (this is how font fallbacks work)
|
|
1480
1660
|
for (const fontstack of fontstacks) {
|
|
1481
|
-
const
|
|
1482
|
-
const
|
|
1483
|
-
const glyphCandidateKeys = [
|
|
1484
|
-
// Try with actual style ID first
|
|
1485
|
-
`${actualStyleId}::${normalizedPath}`,
|
|
1486
|
-
`${actualStyleId}::${glyphPath}`,
|
|
1487
|
-
// Then try with download ID
|
|
1488
|
-
`${downloadId}::${normalizedPath}`,
|
|
1489
|
-
`${downloadId}::${glyphPath}`,
|
|
1490
|
-
// Just paths
|
|
1491
|
-
normalizedPath,
|
|
1492
|
-
glyphPath,
|
|
1493
|
-
];
|
|
1494
|
-
idbLogger.debug(`Trying keys for font "${fontstack}":`, glyphCandidateKeys);
|
|
1495
|
-
for (const candidateKey of glyphCandidateKeys) {
|
|
1661
|
+
const candidateKeys = glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart);
|
|
1662
|
+
for (const candidateKey of candidateKeys) {
|
|
1496
1663
|
const resource = await db.get('glyphs', candidateKey);
|
|
1497
1664
|
if (resource?.data) {
|
|
1498
1665
|
idbLogger.debug(`Found glyph using key: ${candidateKey} (font: ${fontstack})`);
|
|
1499
1666
|
return new Response(resource.data, {
|
|
1500
1667
|
status: 200,
|
|
1501
|
-
headers: {
|
|
1502
|
-
'Content-Type': 'application/x-protobuf',
|
|
1503
|
-
},
|
|
1668
|
+
headers: { 'Content-Type': 'application/x-protobuf' },
|
|
1504
1669
|
});
|
|
1505
1670
|
}
|
|
1506
1671
|
}
|
|
@@ -1510,33 +1675,11 @@ async function idbFetchHandler(url, init) {
|
|
|
1510
1675
|
}
|
|
1511
1676
|
case 'sprite': {
|
|
1512
1677
|
idbLogger.debug(`Looking for sprite with key: ${key}`);
|
|
1513
|
-
// Find which style this region belongs to
|
|
1514
1678
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1515
1679
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
// The sprite service stores sprites with keys like: "voyager::sprite.json", "voyager::sprite@2x.json"
|
|
1520
|
-
// MapLibre requests sprites as: "idb://region_XXX/sprite/sprite@2x.json"
|
|
1521
|
-
// So we need to map the region ID to the style ID
|
|
1522
|
-
const spriteCandidateKeys = Array.from(new Set([
|
|
1523
|
-
// Try with actual style ID first (most likely to work)
|
|
1524
|
-
`${actualStyleId}::${decodedResourcePath}`,
|
|
1525
|
-
`${actualStyleId}:${decodedResourcePath}`,
|
|
1526
|
-
`${actualStyleId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1527
|
-
`${actualStyleId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1528
|
-
// Then try with download ID (in case it's a direct style download)
|
|
1529
|
-
`${downloadId}::${decodedResourcePath}`,
|
|
1530
|
-
`${downloadId}:${decodedResourcePath}`,
|
|
1531
|
-
`${downloadId}::${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1532
|
-
`${downloadId}:${decodedResourcePath.replace(/\.(json|png)$/i, '')}`,
|
|
1533
|
-
// Just the path itself
|
|
1534
|
-
decodedResourcePath,
|
|
1535
|
-
// Original key format
|
|
1536
|
-
key,
|
|
1537
|
-
]));
|
|
1538
|
-
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, spriteCandidateKeys);
|
|
1539
|
-
for (const candidateKey of spriteCandidateKeys) {
|
|
1680
|
+
const candidates = spriteCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1681
|
+
idbLogger.debug(`Sprite candidates for "${decodedResourcePath}":`, candidates);
|
|
1682
|
+
for (const candidateKey of candidates) {
|
|
1540
1683
|
const resource = await db.get('sprites', candidateKey);
|
|
1541
1684
|
if (resource?.data) {
|
|
1542
1685
|
idbLogger.debug(`Found sprite using key: ${candidateKey}`);
|
|
@@ -1546,7 +1689,7 @@ async function idbFetchHandler(url, init) {
|
|
|
1546
1689
|
});
|
|
1547
1690
|
}
|
|
1548
1691
|
}
|
|
1549
|
-
idbLogger.warn(`Sprite not found, tried keys: ${
|
|
1692
|
+
idbLogger.warn(`Sprite not found, tried keys: ${candidates.join(', ')}`);
|
|
1550
1693
|
break;
|
|
1551
1694
|
}
|
|
1552
1695
|
case 'font': {
|
|
@@ -1562,18 +1705,12 @@ async function idbFetchHandler(url, init) {
|
|
|
1562
1705
|
break;
|
|
1563
1706
|
}
|
|
1564
1707
|
case 'model': {
|
|
1565
|
-
// Model URLs are rewritten by patchStyleForOffline to
|
|
1708
|
+
// Model URLs are rewritten by patchStyleForOffline to
|
|
1566
1709
|
// idb://{styleId}/model/{modelName}
|
|
1567
1710
|
// Models are keyed by {styleId}::model::{modelName} in the store.
|
|
1568
|
-
// Mirror the sprite resolution fallback: try the style ID first,
|
|
1569
|
-
// then the download/region ID (in case the request came through a
|
|
1570
|
-
// region-scoped URL).
|
|
1571
1711
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1572
1712
|
const actualStyleId = styleEntry?.key || downloadId;
|
|
1573
|
-
const candidates =
|
|
1574
|
-
`${actualStyleId}::model::${decodedResourcePath}`,
|
|
1575
|
-
`${downloadId}::model::${decodedResourcePath}`,
|
|
1576
|
-
]));
|
|
1713
|
+
const candidates = modelCandidateKeys(actualStyleId, downloadId, decodedResourcePath);
|
|
1577
1714
|
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1578
1715
|
for (const candidateKey of candidates) {
|
|
1579
1716
|
const resource = await db.get('models', candidateKey);
|
|
@@ -1590,9 +1727,9 @@ async function idbFetchHandler(url, init) {
|
|
|
1590
1727
|
}
|
|
1591
1728
|
case 'tilesjson': {
|
|
1592
1729
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1593
|
-
// First try direct lookup (for style-level downloads)
|
|
1730
|
+
// First try direct lookup (for style-level downloads), then fall back
|
|
1731
|
+
// to searching by region ID (for region-level downloads).
|
|
1594
1732
|
let styleEntry = await db.get('styles', downloadId);
|
|
1595
|
-
// If not found, search by region ID (for region-level downloads)
|
|
1596
1733
|
if (!styleEntry || !styleEntry.style?.sources) {
|
|
1597
1734
|
idbLogger.debug(`Style not found with key "${downloadId}", searching by region ID...`);
|
|
1598
1735
|
const foundStyle = await findStyleByRegionId(db, downloadId);
|
|
@@ -1600,41 +1737,23 @@ async function idbFetchHandler(url, init) {
|
|
|
1600
1737
|
styleEntry = foundStyle;
|
|
1601
1738
|
}
|
|
1602
1739
|
}
|
|
1603
|
-
if (styleEntry?.style?.sources) {
|
|
1604
|
-
const sources = styleEntry.style.sources;
|
|
1605
|
-
let matchedSourceId;
|
|
1606
|
-
let matchedSourceConfig;
|
|
1607
|
-
if (decodedResourcePath in sources) {
|
|
1608
|
-
matchedSourceId = decodedResourcePath;
|
|
1609
|
-
matchedSourceConfig = sources[decodedResourcePath];
|
|
1610
|
-
}
|
|
1611
|
-
else {
|
|
1612
|
-
for (const [sourceId, sourceValue] of Object.entries(sources)) {
|
|
1613
|
-
const sourceUrl = typeof sourceValue.url === 'string' ? sourceValue.url : undefined;
|
|
1614
|
-
const originalUrl = typeof sourceValue.__originalTilesetUrl === 'string'
|
|
1615
|
-
? sourceValue.__originalTilesetUrl
|
|
1616
|
-
: undefined;
|
|
1617
|
-
if (sourceUrl === decodedResourcePath || originalUrl === decodedResourcePath) {
|
|
1618
|
-
matchedSourceId = sourceId;
|
|
1619
|
-
matchedSourceConfig = sourceValue;
|
|
1620
|
-
break;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
}
|
|
1624
|
-
if (matchedSourceId && matchedSourceConfig) {
|
|
1625
|
-
const tileJson = buildOfflineTileJson(matchedSourceConfig, downloadId, matchedSourceId);
|
|
1626
|
-
idbLogger.debug(`Serving offline tilejson for source: ${matchedSourceId}`);
|
|
1627
|
-
return new Response(JSON.stringify(tileJson), {
|
|
1628
|
-
status: 200,
|
|
1629
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1633
|
-
}
|
|
1634
|
-
else {
|
|
1740
|
+
if (!styleEntry?.style?.sources) {
|
|
1635
1741
|
idbLogger.warn(`Style not found or missing sources for downloadId: ${downloadId}`);
|
|
1742
|
+
break;
|
|
1636
1743
|
}
|
|
1637
|
-
|
|
1744
|
+
const sources = styleEntry.style.sources;
|
|
1745
|
+
const matched = matchTileJsonSource(sources, decodedResourcePath);
|
|
1746
|
+
if (!matched) {
|
|
1747
|
+
idbLogger.warn(`No matching source found for tilejson: ${decodedResourcePath}`);
|
|
1748
|
+
break;
|
|
1749
|
+
}
|
|
1750
|
+
const extension = deriveTileExtension(matched.config.tiles);
|
|
1751
|
+
const tileJson = buildOfflineTileJson(matched.config, downloadId, matched.sourceId, extension, 'idb');
|
|
1752
|
+
idbLogger.debug(`Serving offline tilejson for source: ${matched.sourceId}`);
|
|
1753
|
+
return new Response(JSON.stringify(tileJson), {
|
|
1754
|
+
status: 200,
|
|
1755
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1756
|
+
});
|
|
1638
1757
|
}
|
|
1639
1758
|
default:
|
|
1640
1759
|
idbLogger.warn(`Unknown resource type: ${type}`);
|
|
@@ -1658,6 +1777,37 @@ async function idbFetchHandler(url, init) {
|
|
|
1658
1777
|
function isMapboxProtocol(url) {
|
|
1659
1778
|
return url.startsWith(MAPBOX_API.PROTOCOL);
|
|
1660
1779
|
}
|
|
1780
|
+
/**
|
|
1781
|
+
* Parse a URL and return its hostname, or null if the URL is malformed.
|
|
1782
|
+
* Accepts relative URLs when `base` is provided.
|
|
1783
|
+
*/
|
|
1784
|
+
function getUrlHostname(url, base) {
|
|
1785
|
+
try {
|
|
1786
|
+
return new URL(url, base).hostname.toLowerCase();
|
|
1787
|
+
}
|
|
1788
|
+
catch {
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* True if `url`'s hostname equals `host` or is a subdomain of `host`.
|
|
1794
|
+
* Uses URL parsing (not substring matching) to avoid false positives like
|
|
1795
|
+
* `https://evil.com/?x=mapbox.com` matching `mapbox.com`.
|
|
1796
|
+
*/
|
|
1797
|
+
function hostMatches(url, host, base) {
|
|
1798
|
+
const hostname = getUrlHostname(url, base);
|
|
1799
|
+
if (hostname === null)
|
|
1800
|
+
return false;
|
|
1801
|
+
const target = host.toLowerCase();
|
|
1802
|
+
return hostname === target || hostname.endsWith('.' + target);
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* True for any host under the mapbox.com domain (including api.mapbox.com,
|
|
1806
|
+
* *.tiles.mapbox.com, etc.). Used by provider detection.
|
|
1807
|
+
*/
|
|
1808
|
+
function isMapboxHost(url, base) {
|
|
1809
|
+
return hostMatches(url, 'mapbox.com', base);
|
|
1810
|
+
}
|
|
1661
1811
|
/**
|
|
1662
1812
|
* Resolve a mapbox:// URL to its HTTPS API equivalent
|
|
1663
1813
|
*
|
|
@@ -1731,9 +1881,7 @@ function rewriteMapboxCdnTileUrl(tileUrl) {
|
|
|
1731
1881
|
*/
|
|
1732
1882
|
function detectStyleProvider(styleUrl, style) {
|
|
1733
1883
|
// Check URL patterns
|
|
1734
|
-
if (isMapboxProtocol(styleUrl) ||
|
|
1735
|
-
styleUrl.includes('mapbox.com') ||
|
|
1736
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
1884
|
+
if (isMapboxProtocol(styleUrl) || isMapboxHost(styleUrl)) {
|
|
1737
1885
|
return 'mapbox';
|
|
1738
1886
|
}
|
|
1739
1887
|
if (styleUrl.includes('maplibre') ||
|
|
@@ -1752,7 +1900,7 @@ function detectStyleProvider(styleUrl, style) {
|
|
|
1752
1900
|
const sources = style.sources || {};
|
|
1753
1901
|
for (const [, sourceConfig] of Object.entries(sources)) {
|
|
1754
1902
|
const source = sourceConfig;
|
|
1755
|
-
if (source.url && (source.url
|
|
1903
|
+
if (source.url && (isMapboxProtocol(source.url) || isMapboxHost(source.url))) {
|
|
1756
1904
|
return 'mapbox';
|
|
1757
1905
|
}
|
|
1758
1906
|
}
|
|
@@ -1844,7 +1992,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1844
1992
|
if (isMapboxProtocol(tileUrl) && accessToken) {
|
|
1845
1993
|
return resolveMapboxUrl(tileUrl, accessToken);
|
|
1846
1994
|
}
|
|
1847
|
-
if (provider === 'mapbox' && accessToken && tileUrl
|
|
1995
|
+
if (provider === 'mapbox' && accessToken && isMapboxHost(tileUrl)) {
|
|
1848
1996
|
return normalizeStyleUrl(tileUrl, accessToken);
|
|
1849
1997
|
}
|
|
1850
1998
|
return tileUrl;
|
|
@@ -1859,7 +2007,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1859
2007
|
if (isMapboxProtocol(processedStyle.sprite)) {
|
|
1860
2008
|
processedStyle.sprite = resolveMapboxUrl(processedStyle.sprite, accessToken);
|
|
1861
2009
|
}
|
|
1862
|
-
else if (provider === 'mapbox' && processedStyle.sprite
|
|
2010
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.sprite)) {
|
|
1863
2011
|
processedStyle.sprite = normalizeStyleUrl(processedStyle.sprite, accessToken);
|
|
1864
2012
|
}
|
|
1865
2013
|
}
|
|
@@ -1870,7 +2018,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1870
2018
|
if (isMapboxProtocol(entry.url)) {
|
|
1871
2019
|
return { ...entry, url: resolveMapboxUrl(entry.url, accessToken) };
|
|
1872
2020
|
}
|
|
1873
|
-
else if (provider === 'mapbox' && entry.url
|
|
2021
|
+
else if (provider === 'mapbox' && isMapboxHost(entry.url)) {
|
|
1874
2022
|
return { ...entry, url: normalizeStyleUrl(entry.url, accessToken) };
|
|
1875
2023
|
}
|
|
1876
2024
|
}
|
|
@@ -1883,7 +2031,7 @@ function processStyleSources(style, provider, accessToken) {
|
|
|
1883
2031
|
if (isMapboxProtocol(processedStyle.glyphs)) {
|
|
1884
2032
|
processedStyle.glyphs = resolveMapboxUrl(processedStyle.glyphs, accessToken);
|
|
1885
2033
|
}
|
|
1886
|
-
else if (provider === 'mapbox' && processedStyle.glyphs
|
|
2034
|
+
else if (provider === 'mapbox' && isMapboxHost(processedStyle.glyphs)) {
|
|
1887
2035
|
processedStyle.glyphs = normalizeStyleUrl(processedStyle.glyphs, accessToken);
|
|
1888
2036
|
}
|
|
1889
2037
|
}
|
|
@@ -1923,13 +2071,20 @@ function validateStyleForProvider(style, provider) {
|
|
|
1923
2071
|
// Check for Mapbox-specific requirements
|
|
1924
2072
|
const hasMapboxSources = Object.values(style.sources || {}).some((source) => {
|
|
1925
2073
|
const s = source;
|
|
1926
|
-
return s.url && s.url
|
|
2074
|
+
return !!s.url && isMapboxHost(s.url);
|
|
1927
2075
|
});
|
|
1928
2076
|
if (hasMapboxSources) {
|
|
1929
2077
|
// Check if access token might be needed
|
|
1930
2078
|
const hasAccessToken = Object.values(style.sources || {}).some((source) => {
|
|
1931
2079
|
const s = source;
|
|
1932
|
-
|
|
2080
|
+
if (!s.url)
|
|
2081
|
+
return false;
|
|
2082
|
+
try {
|
|
2083
|
+
return new URL(s.url).searchParams.has('access_token');
|
|
2084
|
+
}
|
|
2085
|
+
catch {
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
1933
2088
|
});
|
|
1934
2089
|
if (!hasAccessToken) {
|
|
1935
2090
|
warnings.push('Mapbox sources detected but no access token found - authentication may be required');
|
|
@@ -1963,14 +2118,15 @@ function patchStyleForOffline(style, downloadId, maxZoom, tileExtension, styleId
|
|
|
1963
2118
|
styleLogger.debug(`Patching source: ${sourceKey}`, source);
|
|
1964
2119
|
if (source.tiles) {
|
|
1965
2120
|
const originalTiles = [...source.tiles];
|
|
1966
|
-
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext
|
|
2121
|
+
// Patch to idb://{downloadId}/tile/{sourceKey}/{z}/{x}/{y}.ext.
|
|
2122
|
+
// Extension extraction goes through the shared extractTileExtensionFromUrl
|
|
2123
|
+
// helper so the patched URL's extension matches what tileService used when
|
|
2124
|
+
// storing — otherwise Mapbox v4 tile URLs (`{y}.vector.pbf`) produced a
|
|
2125
|
+
// stored key under `.pbf` but a patched URL with `.vector`, forcing
|
|
2126
|
+
// idbFetchHandler to fall through its pbf/mvt/png/jpg/webp fallback loop
|
|
2127
|
+
// on every tile.
|
|
1967
2128
|
source.tiles = source.tiles.map((url) => {
|
|
1968
|
-
|
|
1969
|
-
let ext = tileExtension;
|
|
1970
|
-
if (!ext) {
|
|
1971
|
-
const extMatch = url.match(/\{z\}\/\{x\}\/\{y\}\.(\w+)/);
|
|
1972
|
-
ext = extMatch ? extMatch[1] : 'pbf';
|
|
1973
|
-
}
|
|
2129
|
+
const ext = tileExtension ?? extractTileExtensionFromUrl(url);
|
|
1974
2130
|
return `idb://${downloadId}/tile/${sourceKey}/{z}/{x}/{y}.${ext}`;
|
|
1975
2131
|
});
|
|
1976
2132
|
styleLogger.debug(`Patched tiles for ${sourceKey} with extension .${tileExtension || 'pbf'}:`, {
|
|
@@ -2848,10 +3004,8 @@ async function resolveImportsRecursive(style, accessToken, visited, depth, maxRe
|
|
|
2848
3004
|
if (typeof prefixedLayer.source === 'string') {
|
|
2849
3005
|
prefixedLayer.source = `${importId}/${prefixedLayer.source}`;
|
|
2850
3006
|
}
|
|
2851
|
-
// Resolve ["config", "key"] expressions using schema defaults and import overrides
|
|
2852
|
-
|
|
2853
|
-
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2854
|
-
}
|
|
3007
|
+
// Resolve ["config", "key"] expressions using schema defaults and import overrides.
|
|
3008
|
+
resolveConfigExpressions(prefixedLayer, configValues);
|
|
2855
3009
|
flattenedLayers.push(prefixedLayer);
|
|
2856
3010
|
}
|
|
2857
3011
|
}
|
|
@@ -2889,8 +3043,48 @@ async function resolveImportsRecursive(style, accessToken, visited, depth, maxRe
|
|
|
2889
3043
|
if (!style.models && importedModels) {
|
|
2890
3044
|
style.models = importedModels;
|
|
2891
3045
|
}
|
|
3046
|
+
// Rewrite indoor-only expressions so the flattened style validates without
|
|
3047
|
+
// the `imports` wrapper at render time — see sanitizeIndoorExpressions.
|
|
3048
|
+
sanitizeIndoorExpressions(style);
|
|
2892
3049
|
return style;
|
|
2893
3050
|
}
|
|
3051
|
+
/**
|
|
3052
|
+
* Rewrite indoor-only expressions in a style's layers to their outdoor no-op
|
|
3053
|
+
* constants. See the in-line comment in `resolveValue` for why this is needed
|
|
3054
|
+
* for Mapbox Standard when the `imports` wrapper is stripped.
|
|
3055
|
+
*
|
|
3056
|
+
* Safe to call multiple times and on already-downloaded stored styles — the
|
|
3057
|
+
* rewrites are idempotent (after the first pass there are no more
|
|
3058
|
+
* `is-active-floor` / `floor-level` expressions to rewrite).
|
|
3059
|
+
*/
|
|
3060
|
+
function sanitizeIndoorExpressions(style) {
|
|
3061
|
+
const layers = style.layers;
|
|
3062
|
+
if (!Array.isArray(layers))
|
|
3063
|
+
return;
|
|
3064
|
+
for (const layer of layers) {
|
|
3065
|
+
if (layer && typeof layer === 'object') {
|
|
3066
|
+
rewriteIndoor(layer);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
function rewriteIndoor(obj) {
|
|
3071
|
+
for (const key of Object.keys(obj)) {
|
|
3072
|
+
obj[key] = rewriteIndoorValue(obj[key]);
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function rewriteIndoorValue(value) {
|
|
3076
|
+
if (!Array.isArray(value)) {
|
|
3077
|
+
if (value && typeof value === 'object' && !ArrayBuffer.isView(value)) {
|
|
3078
|
+
rewriteIndoor(value);
|
|
3079
|
+
}
|
|
3080
|
+
return value;
|
|
3081
|
+
}
|
|
3082
|
+
if (value[0] === 'is-active-floor')
|
|
3083
|
+
return false;
|
|
3084
|
+
if (value[0] === 'floor-level' && value.length === 1)
|
|
3085
|
+
return 0;
|
|
3086
|
+
return value.map(rewriteIndoorValue);
|
|
3087
|
+
}
|
|
2894
3088
|
/**
|
|
2895
3089
|
* Deep clone a plain object/array (JSON-safe values only).
|
|
2896
3090
|
*/
|
|
@@ -3056,6 +3250,40 @@ function mergeSprites(outer, imported) {
|
|
|
3056
3250
|
return result;
|
|
3057
3251
|
}
|
|
3058
3252
|
|
|
3253
|
+
const DEFAULT_WASM_URL = 'https://cdn.jsdelivr.net/npm/sql.js@1.14.1/dist/';
|
|
3254
|
+
let currentConfig = {};
|
|
3255
|
+
let sqlJsPromise = null;
|
|
3256
|
+
/**
|
|
3257
|
+
* Override how `sql.js` loads its WebAssembly. Call once before any MBTiles
|
|
3258
|
+
* import/export is invoked. Resets any cached init.
|
|
3259
|
+
*/
|
|
3260
|
+
function configureSqlJs(config) {
|
|
3261
|
+
currentConfig = { ...config };
|
|
3262
|
+
sqlJsPromise = null;
|
|
3263
|
+
}
|
|
3264
|
+
/**
|
|
3265
|
+
* Lazily initialise `sql.js`. The underlying module is loaded via dynamic
|
|
3266
|
+
* `import()` so it only ships with bundles that actually call MBTiles code.
|
|
3267
|
+
*/
|
|
3268
|
+
async function getSqlJs() {
|
|
3269
|
+
if (sqlJsPromise)
|
|
3270
|
+
return sqlJsPromise;
|
|
3271
|
+
sqlJsPromise = (async () => {
|
|
3272
|
+
const mod = (await import('sql.js'));
|
|
3273
|
+
const initSqlJs = mod.default;
|
|
3274
|
+
const options = {};
|
|
3275
|
+
if (currentConfig.wasmBinary) {
|
|
3276
|
+
options.wasmBinary = currentConfig.wasmBinary;
|
|
3277
|
+
}
|
|
3278
|
+
else {
|
|
3279
|
+
const base = currentConfig.wasmUrl ?? DEFAULT_WASM_URL;
|
|
3280
|
+
options.locateFile = (file) => base.endsWith('/') ? `${base}${file}` : `${base}/${file}`;
|
|
3281
|
+
}
|
|
3282
|
+
return initSqlJs(options);
|
|
3283
|
+
})();
|
|
3284
|
+
return sqlJsPromise;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3059
3287
|
const fontLogger = logger.scope('FontService');
|
|
3060
3288
|
class FontService {
|
|
3061
3289
|
db = dbPromise;
|
|
@@ -3239,15 +3467,23 @@ class FontService {
|
|
|
3239
3467
|
},
|
|
3240
3468
|
};
|
|
3241
3469
|
}
|
|
3242
|
-
|
|
3470
|
+
/**
|
|
3471
|
+
* Delete fonts older than `maxAge` days. When `options.styleId` is
|
|
3472
|
+
* provided, only fonts belonging to that style (per the delimiter-aware
|
|
3473
|
+
* `resourceKeyBelongsToStyle` match) are eligible — callers relying on
|
|
3474
|
+
* a styleId filter previously got a silent full-store wipe.
|
|
3475
|
+
*/
|
|
3476
|
+
async cleanupOldFonts(maxAge = 30, options = {}) {
|
|
3243
3477
|
const db = await this.db;
|
|
3244
3478
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3479
|
+
const { styleId } = options;
|
|
3245
3480
|
const tx = db.transaction(['fonts'], 'readwrite');
|
|
3246
3481
|
let deletedCount = 0;
|
|
3247
3482
|
let cursor = await tx.objectStore('fonts').openCursor();
|
|
3248
3483
|
while (cursor) {
|
|
3249
3484
|
const fontEntry = cursor.value;
|
|
3250
|
-
|
|
3485
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(fontEntry.key, styleId);
|
|
3486
|
+
if (belongs && fontEntry.lastModified < cutoffTime) {
|
|
3251
3487
|
await cursor.delete();
|
|
3252
3488
|
deletedCount++;
|
|
3253
3489
|
}
|
|
@@ -3413,7 +3649,7 @@ const fontService = new FontService();
|
|
|
3413
3649
|
const downloadFonts = (fontUrls, styleName, options) => fontService.downloadFonts(fontUrls, styleName, options);
|
|
3414
3650
|
const getFontStats = () => fontService.getFontStats();
|
|
3415
3651
|
const getFontAnalytics = () => fontService.getFontAnalytics();
|
|
3416
|
-
const cleanupOldFonts = (maxAge) => fontService.cleanupOldFonts(maxAge);
|
|
3652
|
+
const cleanupOldFonts = (maxAge, options) => fontService.cleanupOldFonts(maxAge, options);
|
|
3417
3653
|
const verifyAndRepairFonts = () => fontService.verifyAndRepairFonts();
|
|
3418
3654
|
|
|
3419
3655
|
const spriteLogger = logger.scope('SpriteService');
|
|
@@ -3724,19 +3960,24 @@ class SpriteService {
|
|
|
3724
3960
|
};
|
|
3725
3961
|
}
|
|
3726
3962
|
/**
|
|
3727
|
-
*
|
|
3963
|
+
* Remove sprites older than the specified age. When `options.styleId` is
|
|
3964
|
+
* provided, only sprites belonging to that style (per
|
|
3965
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
3728
3966
|
* @param maxAge - Maximum age in days (default: 30)
|
|
3967
|
+
* @param options.styleId - Optional style filter; omit to scan all styles
|
|
3729
3968
|
* @returns Promise resolving to number of deleted sprites
|
|
3730
3969
|
*/
|
|
3731
|
-
async cleanupOldSprites(maxAge = 30) {
|
|
3970
|
+
async cleanupOldSprites(maxAge = 30, options = {}) {
|
|
3732
3971
|
const db = await this.db;
|
|
3733
3972
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
3973
|
+
const { styleId } = options;
|
|
3734
3974
|
const tx = db.transaction(['sprites'], 'readwrite');
|
|
3735
3975
|
let deletedCount = 0;
|
|
3736
3976
|
let cursor = await tx.objectStore('sprites').openCursor();
|
|
3737
3977
|
while (cursor) {
|
|
3738
3978
|
const spriteEntry = cursor.value;
|
|
3739
|
-
|
|
3979
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(spriteEntry.key, styleId);
|
|
3980
|
+
if (belongs && spriteEntry.lastModified < cutoffTime) {
|
|
3740
3981
|
await cursor.delete();
|
|
3741
3982
|
deletedCount++;
|
|
3742
3983
|
}
|
|
@@ -3890,7 +4131,7 @@ const spriteService = new SpriteService();
|
|
|
3890
4131
|
const downloadSprites = (spriteUrls, styleName, options) => spriteService.downloadSprites(spriteUrls, styleName, options);
|
|
3891
4132
|
const getSpriteStats = () => spriteService.getSpriteStats();
|
|
3892
4133
|
const getSpriteAnalytics = () => spriteService.getSpriteAnalytics();
|
|
3893
|
-
const cleanupOldSprites = (maxAge) => spriteService.cleanupOldSprites(maxAge);
|
|
4134
|
+
const cleanupOldSprites = (maxAge, options) => spriteService.cleanupOldSprites(maxAge, options);
|
|
3894
4135
|
const verifyAndRepairSprites = () => spriteService.verifyAndRepairSprites();
|
|
3895
4136
|
|
|
3896
4137
|
var spriteService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -5161,7 +5402,17 @@ class RegionService {
|
|
|
5161
5402
|
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5162
5403
|
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5163
5404
|
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5164
|
-
|
|
5405
|
+
let isMapboxStandardSprite = false;
|
|
5406
|
+
try {
|
|
5407
|
+
const parsed = new URL(pathWithoutQuery);
|
|
5408
|
+
isMapboxStandardSprite =
|
|
5409
|
+
parsed.hostname === 'api.mapbox.com' &&
|
|
5410
|
+
parsed.pathname.startsWith('/styles/v1/') &&
|
|
5411
|
+
parsed.pathname.endsWith('/sprite');
|
|
5412
|
+
}
|
|
5413
|
+
catch {
|
|
5414
|
+
// Non-URL sprite base (e.g. relative); not a Mapbox Standard sprite.
|
|
5415
|
+
}
|
|
5165
5416
|
if (isMapboxStandardSprite) {
|
|
5166
5417
|
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5167
5418
|
suffixes.push('__ICONSET__');
|
|
@@ -6657,8 +6908,7 @@ class TileService {
|
|
|
6657
6908
|
}
|
|
6658
6909
|
}
|
|
6659
6910
|
extractExtension(template) {
|
|
6660
|
-
|
|
6661
|
-
return extMatch ? extMatch[1] : 'pbf';
|
|
6911
|
+
return extractTileExtensionFromUrl(template);
|
|
6662
6912
|
}
|
|
6663
6913
|
selectTileTemplate(templates, coord) {
|
|
6664
6914
|
if (templates.length === 1) {
|
|
@@ -7040,14 +7290,21 @@ class GlyphService {
|
|
|
7040
7290
|
},
|
|
7041
7291
|
};
|
|
7042
7292
|
}
|
|
7043
|
-
|
|
7293
|
+
/**
|
|
7294
|
+
* Remove glyphs older than the specified age. When `options.styleId` is
|
|
7295
|
+
* provided, only glyphs belonging to that style (per
|
|
7296
|
+
* `resourceKeyBelongsToStyle`) are eligible.
|
|
7297
|
+
*/
|
|
7298
|
+
async cleanupOldGlyphs(maxAge = 30, options = {}) {
|
|
7044
7299
|
const db = await this.db;
|
|
7045
7300
|
const cutoffTime = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7301
|
+
const { styleId } = options;
|
|
7046
7302
|
let deletedCount = 0;
|
|
7047
7303
|
const tx = db.transaction('glyphs', 'readwrite');
|
|
7048
7304
|
for await (const cursor of tx.store) {
|
|
7049
7305
|
const glyphEntry = cursor.value;
|
|
7050
|
-
|
|
7306
|
+
const belongs = !styleId || resourceKeyBelongsToStyle(glyphEntry.key, styleId);
|
|
7307
|
+
if (belongs && glyphEntry.lastModified < cutoffTime) {
|
|
7051
7308
|
await cursor.delete();
|
|
7052
7309
|
deletedCount++;
|
|
7053
7310
|
}
|
|
@@ -7150,7 +7407,7 @@ const downloadGlyphs = (glyphUrl, fontstacks, styleName, ranges, options) => gly
|
|
|
7150
7407
|
const loadGlyphs = (fontstack, ranges, styleName) => glyphService.loadGlyphs(fontstack, ranges, styleName);
|
|
7151
7408
|
const getGlyphStats = () => glyphService.getGlyphStats();
|
|
7152
7409
|
const getGlyphAnalytics = () => glyphService.getGlyphAnalytics();
|
|
7153
|
-
const cleanupOldGlyphs = (maxAge) => glyphService.cleanupOldGlyphs(maxAge);
|
|
7410
|
+
const cleanupOldGlyphs = (maxAge, options) => glyphService.cleanupOldGlyphs(maxAge, options);
|
|
7154
7411
|
const verifyAndRepairGlyphs = () => glyphService.verifyAndRepairGlyphs();
|
|
7155
7412
|
|
|
7156
7413
|
var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
@@ -7385,8 +7642,7 @@ class ResourceService {
|
|
|
7385
7642
|
return getFontAnalytics();
|
|
7386
7643
|
}
|
|
7387
7644
|
async cleanupOldFonts(styleId, options) {
|
|
7388
|
-
|
|
7389
|
-
return cleanupOldFonts(maxAge);
|
|
7645
|
+
return cleanupOldFonts(options?.maxAge, { styleId });
|
|
7390
7646
|
}
|
|
7391
7647
|
async verifyAndRepairFonts() {
|
|
7392
7648
|
return verifyAndRepairFonts();
|
|
@@ -7402,8 +7658,7 @@ class ResourceService {
|
|
|
7402
7658
|
return getSpriteAnalytics();
|
|
7403
7659
|
}
|
|
7404
7660
|
async cleanupOldSprites(styleId, options) {
|
|
7405
|
-
|
|
7406
|
-
return cleanupOldSprites(maxAge);
|
|
7661
|
+
return cleanupOldSprites(options?.maxAge, { styleId });
|
|
7407
7662
|
}
|
|
7408
7663
|
async verifyAndRepairSprites() {
|
|
7409
7664
|
return verifyAndRepairSprites();
|
|
@@ -7422,8 +7677,7 @@ class ResourceService {
|
|
|
7422
7677
|
return loadGlyphs(fontstack, ranges, styleId);
|
|
7423
7678
|
}
|
|
7424
7679
|
async cleanupOldGlyphs(styleId, options) {
|
|
7425
|
-
|
|
7426
|
-
return cleanupOldGlyphs(maxAge);
|
|
7680
|
+
return cleanupOldGlyphs(options?.maxAge, { styleId });
|
|
7427
7681
|
}
|
|
7428
7682
|
async verifyAndRepairGlyphs() {
|
|
7429
7683
|
return verifyAndRepairGlyphs();
|
|
@@ -7538,6 +7792,88 @@ class AnalyticsService {
|
|
|
7538
7792
|
}
|
|
7539
7793
|
}
|
|
7540
7794
|
|
|
7795
|
+
/**
|
|
7796
|
+
* MBTiles uses TMS tile_row ordering; our storage uses XYZ y. Flip across
|
|
7797
|
+
* either direction with the same formula.
|
|
7798
|
+
*/
|
|
7799
|
+
function flipY(y, z) {
|
|
7800
|
+
return (1 << z) - 1 - y;
|
|
7801
|
+
}
|
|
7802
|
+
/** Vector tile formats that downstream consumers (QGIS, maplibre-native) expect gzipped. */
|
|
7803
|
+
const VECTOR_FORMATS = new Set(['pbf', 'mvt']);
|
|
7804
|
+
function hasGzipMagic(bytes) {
|
|
7805
|
+
return bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b;
|
|
7806
|
+
}
|
|
7807
|
+
async function drainReadable(readable) {
|
|
7808
|
+
const reader = readable.getReader();
|
|
7809
|
+
const chunks = [];
|
|
7810
|
+
let total = 0;
|
|
7811
|
+
while (true) {
|
|
7812
|
+
const { done, value } = await reader.read();
|
|
7813
|
+
if (done)
|
|
7814
|
+
break;
|
|
7815
|
+
if (value) {
|
|
7816
|
+
chunks.push(value);
|
|
7817
|
+
total += value.byteLength;
|
|
7818
|
+
}
|
|
7819
|
+
}
|
|
7820
|
+
const out = new Uint8Array(total);
|
|
7821
|
+
let offset = 0;
|
|
7822
|
+
for (const chunk of chunks) {
|
|
7823
|
+
out.set(chunk, offset);
|
|
7824
|
+
offset += chunk.byteLength;
|
|
7825
|
+
}
|
|
7826
|
+
return out;
|
|
7827
|
+
}
|
|
7828
|
+
async function transformBytes(bytes, transform) {
|
|
7829
|
+
const writer = transform.writable.getWriter();
|
|
7830
|
+
// Don't await — the read loop below drives the pipe and we only want
|
|
7831
|
+
// the final bytes, not back-pressure handling for a single chunk.
|
|
7832
|
+
void writer.write(bytes);
|
|
7833
|
+
void writer.close();
|
|
7834
|
+
return drainReadable(transform.readable);
|
|
7835
|
+
}
|
|
7836
|
+
async function gzipBytes(bytes) {
|
|
7837
|
+
return transformBytes(bytes, new CompressionStream('gzip'));
|
|
7838
|
+
}
|
|
7839
|
+
async function gunzipBytes(bytes) {
|
|
7840
|
+
return transformBytes(bytes, new DecompressionStream('gzip'));
|
|
7841
|
+
}
|
|
7842
|
+
/**
|
|
7843
|
+
* Build the MBTiles `json` metadata payload. For vector tiles this is
|
|
7844
|
+
* mandatory for tippecanoe/QGIS/maplibre-native to render — they read
|
|
7845
|
+
* `vector_layers` from here.
|
|
7846
|
+
*
|
|
7847
|
+
* `vector_layers` is inferred from the offline style's vector sources
|
|
7848
|
+
* (populated by the TileJSON expansion step in styleService). Multiple
|
|
7849
|
+
* vector sources are merged; duplicates de-duped by id, first wins.
|
|
7850
|
+
*/
|
|
7851
|
+
function buildVectorJsonMetadata(style, sourceIds) {
|
|
7852
|
+
if (!style || typeof style !== 'object')
|
|
7853
|
+
return null;
|
|
7854
|
+
const sources = style.sources;
|
|
7855
|
+
if (!sources)
|
|
7856
|
+
return null;
|
|
7857
|
+
const merged = [];
|
|
7858
|
+
const seen = new Set();
|
|
7859
|
+
for (const [id, src] of Object.entries(sources)) {
|
|
7860
|
+
if (sourceIds.size > 0 && !sourceIds.has(id))
|
|
7861
|
+
continue;
|
|
7862
|
+
const layers = src?.vector_layers;
|
|
7863
|
+
if (!Array.isArray(layers))
|
|
7864
|
+
continue;
|
|
7865
|
+
for (const layer of layers) {
|
|
7866
|
+
const layerId = typeof layer?.id === 'string' ? layer.id : null;
|
|
7867
|
+
if (!layerId || seen.has(layerId))
|
|
7868
|
+
continue;
|
|
7869
|
+
seen.add(layerId);
|
|
7870
|
+
merged.push(layer);
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
7873
|
+
if (merged.length === 0)
|
|
7874
|
+
return null;
|
|
7875
|
+
return JSON.stringify({ vector_layers: merged });
|
|
7876
|
+
}
|
|
7541
7877
|
const serviceLogger = logger.scope('ImportExportService');
|
|
7542
7878
|
class ImportExportService {
|
|
7543
7879
|
db = dbPromise;
|
|
@@ -7545,270 +7881,173 @@ class ImportExportService {
|
|
|
7545
7881
|
// No need for initialization since dbPromise is already available
|
|
7546
7882
|
}
|
|
7547
7883
|
/**
|
|
7548
|
-
* Export a
|
|
7884
|
+
* Export region as a real binary MBTiles SQLite file.
|
|
7885
|
+
*
|
|
7886
|
+
* Produces a v1.3-compliant MBTiles archive: `metadata` + `tiles` tables,
|
|
7887
|
+
* with `tile_row` flipped to TMS ordering. The resulting blob can be read
|
|
7888
|
+
* by tippecanoe, QGIS, maplibre-native, etc.
|
|
7549
7889
|
*/
|
|
7550
|
-
async
|
|
7890
|
+
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7551
7891
|
const onProgress = options.onProgress || (() => { });
|
|
7552
7892
|
try {
|
|
7553
7893
|
onProgress({
|
|
7554
7894
|
stage: 'preparing',
|
|
7555
7895
|
percentage: 0,
|
|
7556
|
-
message: 'Preparing export...',
|
|
7896
|
+
message: 'Preparing MBTiles export...',
|
|
7557
7897
|
});
|
|
7558
|
-
// Get region metadata
|
|
7559
7898
|
const region = await this.getRegionMetadata(regionId);
|
|
7560
7899
|
if (!region) {
|
|
7561
7900
|
throw new Error(`Region ${regionId} not found`);
|
|
7562
7901
|
}
|
|
7902
|
+
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7903
|
+
// Pick format: caller override → region.tileExtension → default pbf.
|
|
7904
|
+
// Drives both the metadata row and whether tile bytes get gzipped.
|
|
7905
|
+
const format = String(options.format || region.tileExtension || 'pbf').toLowerCase();
|
|
7906
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
7563
7907
|
onProgress({
|
|
7564
|
-
stage: '
|
|
7565
|
-
percentage:
|
|
7566
|
-
message: '
|
|
7908
|
+
stage: 'processing',
|
|
7909
|
+
percentage: 75,
|
|
7910
|
+
message: isVector ? 'Compressing vector tiles...' : 'Packing SQLite database...',
|
|
7567
7911
|
});
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
createdAt: region.created, // StoredRegion uses 'created' not 'createdAt'
|
|
7578
|
-
exportedAt: Date.now(),
|
|
7579
|
-
version: '1.0.0',
|
|
7580
|
-
format: 'json',
|
|
7581
|
-
},
|
|
7582
|
-
style: {},
|
|
7583
|
-
tiles: [],
|
|
7584
|
-
sprites: [],
|
|
7585
|
-
fonts: [],
|
|
7586
|
-
};
|
|
7587
|
-
// Export style if requested
|
|
7588
|
-
if (options.includeStyle !== false) {
|
|
7589
|
-
onProgress({
|
|
7590
|
-
stage: 'exporting',
|
|
7591
|
-
percentage: 20,
|
|
7592
|
-
message: 'Exporting style data...',
|
|
7593
|
-
});
|
|
7594
|
-
exportData.style = await this.exportStyle(regionId);
|
|
7912
|
+
// Gzip vector tiles. Idempotent: skip tiles already gzipped (downloaded
|
|
7913
|
+
// with their original gzip wrapper intact).
|
|
7914
|
+
const packedTiles = [];
|
|
7915
|
+
for (const tile of tiles) {
|
|
7916
|
+
const raw = tile.data instanceof ArrayBuffer
|
|
7917
|
+
? new Uint8Array(tile.data)
|
|
7918
|
+
: new Uint8Array(tile.data);
|
|
7919
|
+
const data = isVector && !hasGzipMagic(raw) ? await gzipBytes(raw) : raw;
|
|
7920
|
+
packedTiles.push({ z: tile.z, x: tile.x, y: tile.y, data });
|
|
7595
7921
|
}
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7922
|
+
onProgress({
|
|
7923
|
+
stage: 'processing',
|
|
7924
|
+
percentage: 85,
|
|
7925
|
+
message: 'Packing SQLite database...',
|
|
7926
|
+
});
|
|
7927
|
+
const SQL = await getSqlJs();
|
|
7928
|
+
const db = new SQL.Database();
|
|
7929
|
+
try {
|
|
7930
|
+
db.run(`
|
|
7931
|
+
CREATE TABLE metadata (name TEXT, value TEXT);
|
|
7932
|
+
CREATE TABLE tiles (
|
|
7933
|
+
zoom_level INTEGER NOT NULL,
|
|
7934
|
+
tile_column INTEGER NOT NULL,
|
|
7935
|
+
tile_row INTEGER NOT NULL,
|
|
7936
|
+
tile_data BLOB
|
|
7937
|
+
);
|
|
7938
|
+
CREATE UNIQUE INDEX tile_index ON tiles (zoom_level, tile_column, tile_row);
|
|
7939
|
+
CREATE UNIQUE INDEX name ON metadata (name);
|
|
7940
|
+
`);
|
|
7941
|
+
const [[west, south], [east, north]] = region.bounds;
|
|
7942
|
+
const centerLon = (west + east) / 2;
|
|
7943
|
+
const centerLat = (south + north) / 2;
|
|
7944
|
+
const centerZoom = Math.max(region.minZoom, Math.min(region.maxZoom, Math.round((region.minZoom + region.maxZoom) / 2)));
|
|
7945
|
+
const metadataRows = {
|
|
7946
|
+
name: region.name || region.id,
|
|
7947
|
+
// MBTiles 1.3 type: 'overlay' or 'baselayer'. Baselayer matches how
|
|
7948
|
+
// QGIS treats the dataset (full-coverage map rather than overlay).
|
|
7949
|
+
type: isVector ? 'baselayer' : 'overlay',
|
|
7950
|
+
version: '1.0',
|
|
7951
|
+
description: region.name || region.id,
|
|
7952
|
+
format,
|
|
7953
|
+
bounds: `${west},${south},${east},${north}`,
|
|
7954
|
+
center: `${centerLon},${centerLat},${centerZoom}`,
|
|
7955
|
+
minzoom: String(region.minZoom),
|
|
7956
|
+
maxzoom: String(region.maxZoom),
|
|
7957
|
+
};
|
|
7958
|
+
// For vector tiles, the `json` field with `vector_layers` is required
|
|
7959
|
+
// by the MBTiles 1.3 spec and by every vector tile consumer worth
|
|
7960
|
+
// opening the file in. Derive it from the offline style.
|
|
7961
|
+
if (isVector) {
|
|
7962
|
+
const style = await this.exportStyle(regionId);
|
|
7963
|
+
const sourceIds = new Set(tiles.map(t => t.sourceId).filter(Boolean));
|
|
7964
|
+
const json = buildVectorJsonMetadata(style.style ?? style, sourceIds);
|
|
7965
|
+
if (json)
|
|
7966
|
+
metadataRows.json = json;
|
|
7967
|
+
}
|
|
7968
|
+
for (const [k, v] of Object.entries(options.metadata || {})) {
|
|
7969
|
+
metadataRows[k] = typeof v === 'string' ? v : JSON.stringify(v);
|
|
7970
|
+
}
|
|
7971
|
+
const insertMeta = db.prepare(`INSERT INTO metadata (name, value) VALUES (?, ?)`);
|
|
7972
|
+
try {
|
|
7973
|
+
for (const [name, value] of Object.entries(metadataRows)) {
|
|
7974
|
+
insertMeta.run([name, value]);
|
|
7975
|
+
}
|
|
7976
|
+
}
|
|
7977
|
+
finally {
|
|
7978
|
+
insertMeta.free();
|
|
7979
|
+
}
|
|
7980
|
+
const insertTile = db.prepare(`INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data)
|
|
7981
|
+
VALUES (?, ?, ?, ?)`);
|
|
7982
|
+
try {
|
|
7983
|
+
db.run('BEGIN');
|
|
7984
|
+
for (const tile of packedTiles) {
|
|
7985
|
+
insertTile.run([tile.z, tile.x, flipY(tile.y, tile.z), tile.data]);
|
|
7986
|
+
}
|
|
7987
|
+
db.run('COMMIT');
|
|
7988
|
+
}
|
|
7989
|
+
finally {
|
|
7990
|
+
insertTile.free();
|
|
7991
|
+
}
|
|
7992
|
+
const binary = db.export();
|
|
7993
|
+
const blob = new Blob([binary.buffer], {
|
|
7994
|
+
type: 'application/x-sqlite3',
|
|
7602
7995
|
});
|
|
7603
|
-
exportData.tiles = await this.exportTiles(regionId, onProgress);
|
|
7604
|
-
}
|
|
7605
|
-
// Export sprites if requested
|
|
7606
|
-
if (options.includeSprites !== false) {
|
|
7607
7996
|
onProgress({
|
|
7608
|
-
stage: '
|
|
7609
|
-
percentage:
|
|
7610
|
-
message: '
|
|
7997
|
+
stage: 'complete',
|
|
7998
|
+
percentage: 100,
|
|
7999
|
+
message: 'MBTiles export complete!',
|
|
7611
8000
|
});
|
|
7612
|
-
|
|
8001
|
+
return {
|
|
8002
|
+
success: true,
|
|
8003
|
+
format: 'mbtiles',
|
|
8004
|
+
filename: `${region.name || region.id}.mbtiles`,
|
|
8005
|
+
blob,
|
|
8006
|
+
size: blob.size,
|
|
8007
|
+
statistics: {
|
|
8008
|
+
tilesExported: tiles.length,
|
|
8009
|
+
spritesExported: 0,
|
|
8010
|
+
fontsExported: 0,
|
|
8011
|
+
},
|
|
8012
|
+
};
|
|
7613
8013
|
}
|
|
7614
|
-
|
|
7615
|
-
|
|
7616
|
-
onProgress({
|
|
7617
|
-
stage: 'exporting',
|
|
7618
|
-
percentage: 85,
|
|
7619
|
-
message: 'Exporting fonts...',
|
|
7620
|
-
});
|
|
7621
|
-
exportData.fonts = await this.exportFonts(regionId);
|
|
8014
|
+
finally {
|
|
8015
|
+
db.close();
|
|
7622
8016
|
}
|
|
7623
|
-
onProgress({
|
|
7624
|
-
stage: 'processing',
|
|
7625
|
-
percentage: 95,
|
|
7626
|
-
message: 'Creating export file...',
|
|
7627
|
-
});
|
|
7628
|
-
// Create JSON blob
|
|
7629
|
-
const jsonString = JSON.stringify(exportData, null, 2);
|
|
7630
|
-
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
7631
|
-
onProgress({
|
|
7632
|
-
stage: 'complete',
|
|
7633
|
-
percentage: 100,
|
|
7634
|
-
message: 'Export complete!',
|
|
7635
|
-
});
|
|
7636
|
-
return {
|
|
7637
|
-
success: true,
|
|
7638
|
-
format: 'json',
|
|
7639
|
-
filename: `${region.name || region.id}_export.json`,
|
|
7640
|
-
blob,
|
|
7641
|
-
size: blob.size,
|
|
7642
|
-
statistics: {
|
|
7643
|
-
tilesExported: exportData.tiles.length,
|
|
7644
|
-
spritesExported: exportData.sprites.length,
|
|
7645
|
-
fontsExported: exportData.fonts.length,
|
|
7646
|
-
},
|
|
7647
|
-
};
|
|
7648
8017
|
}
|
|
7649
8018
|
catch (error) {
|
|
7650
8019
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7651
|
-
throw new Error(`
|
|
8020
|
+
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7652
8021
|
}
|
|
7653
8022
|
}
|
|
7654
8023
|
/**
|
|
7655
|
-
*
|
|
8024
|
+
* Import region from a binary MBTiles (SQLite) file.
|
|
7656
8025
|
*/
|
|
7657
|
-
async
|
|
7658
|
-
const onProgress =
|
|
8026
|
+
async importRegion(importData) {
|
|
8027
|
+
const onProgress = importData.onProgress || (() => { });
|
|
7659
8028
|
try {
|
|
7660
8029
|
onProgress({
|
|
7661
8030
|
stage: 'preparing',
|
|
7662
8031
|
percentage: 0,
|
|
7663
|
-
message: '
|
|
8032
|
+
message: 'Reading file...',
|
|
7664
8033
|
});
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
// to create a proper PMTiles file format
|
|
7668
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7669
|
-
if (!region) {
|
|
7670
|
-
throw new Error(`Region ${regionId} not found`);
|
|
8034
|
+
if (importData.format !== 'mbtiles') {
|
|
8035
|
+
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7671
8036
|
}
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
const pmtilesData = {
|
|
7676
|
-
header: {
|
|
7677
|
-
version: 3,
|
|
7678
|
-
type: 'mvt',
|
|
7679
|
-
compression: options.compression || 'gzip',
|
|
7680
|
-
bounds: region.bounds,
|
|
7681
|
-
minZoom: region.minZoom,
|
|
7682
|
-
maxZoom: region.maxZoom,
|
|
7683
|
-
metadata: {
|
|
7684
|
-
name: region.name,
|
|
7685
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7686
|
-
...options.metadata,
|
|
7687
|
-
},
|
|
7688
|
-
},
|
|
7689
|
-
tiles: tiles,
|
|
7690
|
-
};
|
|
7691
|
-
// Convert to binary format (simplified)
|
|
7692
|
-
const jsonString = JSON.stringify(pmtilesData);
|
|
7693
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
8037
|
+
const buffer = await this.readFileAsArrayBuffer(importData.file);
|
|
8038
|
+
onProgress({ stage: 'importing', percentage: 40, message: 'Parsing MBTiles...' });
|
|
8039
|
+
const regionData = await this.parseMBTiles(buffer);
|
|
7694
8040
|
onProgress({
|
|
7695
|
-
stage: '
|
|
7696
|
-
percentage:
|
|
7697
|
-
message:
|
|
7698
|
-
});
|
|
7699
|
-
return {
|
|
7700
|
-
success: true,
|
|
7701
|
-
format: 'pmtiles',
|
|
7702
|
-
filename: `${region.name || region.id}.pmtiles`,
|
|
7703
|
-
blob,
|
|
7704
|
-
size: blob.size,
|
|
7705
|
-
statistics: {
|
|
7706
|
-
tilesExported: tiles.length,
|
|
7707
|
-
spritesExported: 0,
|
|
7708
|
-
fontsExported: 0,
|
|
7709
|
-
},
|
|
7710
|
-
};
|
|
7711
|
-
}
|
|
7712
|
-
catch (error) {
|
|
7713
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7714
|
-
throw new Error(`PMTiles export failed: ${errorMessage}`);
|
|
7715
|
-
}
|
|
7716
|
-
}
|
|
7717
|
-
/**
|
|
7718
|
-
* Export region as MBTiles format
|
|
7719
|
-
*/
|
|
7720
|
-
async exportRegionAsMBTiles(regionId, options = {}) {
|
|
7721
|
-
const onProgress = options.onProgress || (() => { });
|
|
7722
|
-
try {
|
|
7723
|
-
onProgress({
|
|
7724
|
-
stage: 'preparing',
|
|
7725
|
-
percentage: 0,
|
|
7726
|
-
message: 'Preparing MBTiles export...',
|
|
8041
|
+
stage: 'importing',
|
|
8042
|
+
percentage: 70,
|
|
8043
|
+
message: `Importing ${regionData.tiles?.length ?? 0} tiles...`,
|
|
7727
8044
|
});
|
|
7728
|
-
|
|
7729
|
-
// In a real implementation, you would use SQLite/SQL.js
|
|
7730
|
-
// to create a proper MBTiles SQLite database
|
|
7731
|
-
const region = await this.getRegionMetadata(regionId);
|
|
7732
|
-
if (!region) {
|
|
7733
|
-
throw new Error(`Region ${regionId} not found`);
|
|
7734
|
-
}
|
|
7735
|
-
// Get tiles data
|
|
7736
|
-
const tiles = await this.exportTiles(regionId, onProgress);
|
|
7737
|
-
// Create MBTiles structure (simplified as JSON for now)
|
|
7738
|
-
const mbtilesData = {
|
|
7739
|
-
metadata: {
|
|
7740
|
-
name: region.name,
|
|
7741
|
-
type: 'overlay',
|
|
7742
|
-
version: '1.0',
|
|
7743
|
-
description: region.name || region.id, // StoredRegion doesn't have description, use name instead
|
|
7744
|
-
format: options.format || 'pbf',
|
|
7745
|
-
bounds: region.bounds.flat().join(','),
|
|
7746
|
-
minzoom: region.minZoom,
|
|
7747
|
-
maxzoom: region.maxZoom,
|
|
7748
|
-
...options.metadata,
|
|
7749
|
-
},
|
|
7750
|
-
tiles: tiles.map(tile => ({
|
|
7751
|
-
zoom_level: tile.z,
|
|
7752
|
-
tile_column: tile.x,
|
|
7753
|
-
tile_row: tile.y,
|
|
7754
|
-
tile_data: tile.data,
|
|
7755
|
-
})),
|
|
7756
|
-
};
|
|
7757
|
-
// Convert to binary format (simplified)
|
|
7758
|
-
const jsonString = JSON.stringify(mbtilesData);
|
|
7759
|
-
const blob = new Blob([jsonString], { type: 'application/octet-stream' });
|
|
8045
|
+
const result = await this.importRegionData(regionData, importData);
|
|
7760
8046
|
onProgress({
|
|
7761
8047
|
stage: 'complete',
|
|
7762
8048
|
percentage: 100,
|
|
7763
|
-
message: '
|
|
8049
|
+
message: result.success ? 'Import complete!' : result.message,
|
|
7764
8050
|
});
|
|
7765
|
-
return {
|
|
7766
|
-
success: true,
|
|
7767
|
-
format: 'mbtiles',
|
|
7768
|
-
filename: `${region.name || region.id}.mbtiles`,
|
|
7769
|
-
blob,
|
|
7770
|
-
size: blob.size,
|
|
7771
|
-
statistics: {
|
|
7772
|
-
tilesExported: tiles.length,
|
|
7773
|
-
spritesExported: 0,
|
|
7774
|
-
fontsExported: 0,
|
|
7775
|
-
},
|
|
7776
|
-
};
|
|
7777
|
-
}
|
|
7778
|
-
catch (error) {
|
|
7779
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
7780
|
-
throw new Error(`MBTiles export failed: ${errorMessage}`);
|
|
7781
|
-
}
|
|
7782
|
-
}
|
|
7783
|
-
/**
|
|
7784
|
-
* Import region from file
|
|
7785
|
-
*/
|
|
7786
|
-
async importRegion(importData) {
|
|
7787
|
-
try {
|
|
7788
|
-
let regionData;
|
|
7789
|
-
switch (importData.format) {
|
|
7790
|
-
case 'json': {
|
|
7791
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7792
|
-
regionData = JSON.parse(textContent);
|
|
7793
|
-
break;
|
|
7794
|
-
}
|
|
7795
|
-
case 'pmtiles': {
|
|
7796
|
-
// PMTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7797
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7798
|
-
regionData = await this.parsePMTiles(textContent);
|
|
7799
|
-
break;
|
|
7800
|
-
}
|
|
7801
|
-
case 'mbtiles': {
|
|
7802
|
-
// MBTiles is a binary format; currently parsed as JSON (simplified impl)
|
|
7803
|
-
const textContent = await this.readFileAsText(importData.file);
|
|
7804
|
-
regionData = await this.parseMBTiles(textContent);
|
|
7805
|
-
break;
|
|
7806
|
-
}
|
|
7807
|
-
default:
|
|
7808
|
-
throw new Error(`Unsupported format: ${importData.format}`);
|
|
7809
|
-
}
|
|
7810
|
-
// Import the region data
|
|
7811
|
-
const result = await this.importRegionData(regionData, importData);
|
|
7812
8051
|
return result;
|
|
7813
8052
|
}
|
|
7814
8053
|
catch (error) {
|
|
@@ -7932,151 +8171,113 @@ class ImportExportService {
|
|
|
7932
8171
|
}
|
|
7933
8172
|
}
|
|
7934
8173
|
/**
|
|
7935
|
-
*
|
|
8174
|
+
* Read file content as ArrayBuffer (for the binary MBTiles file).
|
|
7936
8175
|
*/
|
|
7937
|
-
async
|
|
7938
|
-
const db = await this.db;
|
|
7939
|
-
const transaction = db.transaction(['sprites'], 'readonly');
|
|
7940
|
-
const store = transaction.objectStore('sprites');
|
|
7941
|
-
const sprites = [];
|
|
7942
|
-
try {
|
|
7943
|
-
let cursor = await store.openCursor();
|
|
7944
|
-
while (cursor) {
|
|
7945
|
-
const sprite = cursor.value;
|
|
7946
|
-
// Include sprites that match the styleId, or all sprites if keys don't contain styleId
|
|
7947
|
-
// (sprite keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7948
|
-
sprites.push({
|
|
7949
|
-
url: sprite.url,
|
|
7950
|
-
data: sprite.data,
|
|
7951
|
-
type: sprite.url.endsWith('.json') ? 'json' : 'png',
|
|
7952
|
-
resolution: sprite.url.includes('@2x') ? '2x' : '1x',
|
|
7953
|
-
});
|
|
7954
|
-
cursor = await cursor.continue();
|
|
7955
|
-
}
|
|
7956
|
-
return sprites;
|
|
7957
|
-
}
|
|
7958
|
-
catch (error) {
|
|
7959
|
-
serviceLogger.error('Error exporting sprites:', error);
|
|
7960
|
-
return [];
|
|
7961
|
-
}
|
|
7962
|
-
}
|
|
7963
|
-
/**
|
|
7964
|
-
* Export fonts data
|
|
7965
|
-
*/
|
|
7966
|
-
async exportFonts(_regionId) {
|
|
7967
|
-
const db = await this.db;
|
|
7968
|
-
const transaction = db.transaction(['fonts'], 'readonly');
|
|
7969
|
-
const store = transaction.objectStore('fonts');
|
|
7970
|
-
const fonts = [];
|
|
7971
|
-
try {
|
|
7972
|
-
let cursor = await store.openCursor();
|
|
7973
|
-
while (cursor) {
|
|
7974
|
-
const font = cursor.value;
|
|
7975
|
-
// Include fonts that match the styleId, or all fonts if keys don't contain styleId
|
|
7976
|
-
// (font keys may or may not be prefixed with styleId depending on how they were stored)
|
|
7977
|
-
fonts.push({
|
|
7978
|
-
fontStack: font.key, // Use key as fontstack identifier
|
|
7979
|
-
range: '0-255', // Default range since FontEntry doesn't store this
|
|
7980
|
-
data: font.data,
|
|
7981
|
-
});
|
|
7982
|
-
cursor = await cursor.continue();
|
|
7983
|
-
}
|
|
7984
|
-
return fonts;
|
|
7985
|
-
}
|
|
7986
|
-
catch (error) {
|
|
7987
|
-
serviceLogger.error('Error exporting fonts:', error);
|
|
7988
|
-
return [];
|
|
7989
|
-
}
|
|
7990
|
-
}
|
|
7991
|
-
/**
|
|
7992
|
-
* Read file content as text (for JSON files)
|
|
7993
|
-
*/
|
|
7994
|
-
async readFileAsText(file) {
|
|
8176
|
+
async readFileAsArrayBuffer(file) {
|
|
7995
8177
|
return new Promise((resolve, reject) => {
|
|
7996
8178
|
const reader = new FileReader();
|
|
7997
8179
|
reader.onload = () => resolve(reader.result);
|
|
7998
8180
|
reader.onerror = () => reject(new Error('Failed to read file'));
|
|
7999
|
-
reader.
|
|
8181
|
+
reader.readAsArrayBuffer(file);
|
|
8000
8182
|
});
|
|
8001
8183
|
}
|
|
8002
8184
|
/**
|
|
8003
|
-
* Parse
|
|
8185
|
+
* Parse a real binary MBTiles (SQLite) file into our import-data shape.
|
|
8186
|
+
* Un-flips the TMS tile_row back to XYZ y.
|
|
8004
8187
|
*/
|
|
8005
|
-
async
|
|
8006
|
-
|
|
8007
|
-
//
|
|
8008
|
-
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8012
|
-
|
|
8013
|
-
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
|
|
8018
|
-
|
|
8019
|
-
|
|
8020
|
-
|
|
8021
|
-
|
|
8022
|
-
|
|
8023
|
-
|
|
8024
|
-
|
|
8025
|
-
|
|
8026
|
-
|
|
8027
|
-
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8049
|
-
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8054
|
-
|
|
8055
|
-
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8188
|
+
async parseMBTiles(buffer) {
|
|
8189
|
+
const bytes = new Uint8Array(buffer);
|
|
8190
|
+
// SQLite header: "SQLite format 3\0" (16 bytes). Validate up front so
|
|
8191
|
+
// non-MBTiles files (e.g. a JSON renamed to .mbtiles) surface a clear
|
|
8192
|
+
// error instead of the opaque "file is not a database" from sql.js.
|
|
8193
|
+
if (bytes.byteLength < 16) {
|
|
8194
|
+
throw new Error('Not a valid MBTiles file: file is too small');
|
|
8195
|
+
}
|
|
8196
|
+
const magic = String.fromCharCode(...bytes.slice(0, 15));
|
|
8197
|
+
if (magic !== 'SQLite format 3') {
|
|
8198
|
+
throw new Error('Not a valid MBTiles file: missing SQLite header');
|
|
8199
|
+
}
|
|
8200
|
+
const SQL = await getSqlJs();
|
|
8201
|
+
const db = new SQL.Database(bytes);
|
|
8202
|
+
try {
|
|
8203
|
+
const tablesResult = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name IN ('metadata', 'tiles')");
|
|
8204
|
+
const tableNames = (tablesResult[0]?.values || []).map(r => r[0]);
|
|
8205
|
+
if (!tableNames.includes('metadata') || !tableNames.includes('tiles')) {
|
|
8206
|
+
throw new Error('Not a valid MBTiles file: missing required metadata/tiles tables');
|
|
8207
|
+
}
|
|
8208
|
+
const metadata = {};
|
|
8209
|
+
const metaStmt = db.prepare('SELECT name, value FROM metadata');
|
|
8210
|
+
try {
|
|
8211
|
+
while (metaStmt.step()) {
|
|
8212
|
+
const row = metaStmt.get();
|
|
8213
|
+
metadata[row[0]] = row[1];
|
|
8214
|
+
}
|
|
8215
|
+
}
|
|
8216
|
+
finally {
|
|
8217
|
+
metaStmt.free();
|
|
8218
|
+
}
|
|
8219
|
+
const rawBounds = metadata.bounds ? metadata.bounds.split(',').map(Number) : [0, 0, 0, 0];
|
|
8220
|
+
const bounds = [
|
|
8221
|
+
isFinite(rawBounds[0]) ? rawBounds[0] : 0,
|
|
8222
|
+
isFinite(rawBounds[1]) ? rawBounds[1] : 0,
|
|
8223
|
+
isFinite(rawBounds[2]) ? rawBounds[2] : 0,
|
|
8224
|
+
isFinite(rawBounds[3]) ? rawBounds[3] : 0,
|
|
8225
|
+
];
|
|
8226
|
+
const format = (metadata.format || 'pbf');
|
|
8227
|
+
const isVector = VECTOR_FORMATS.has(format);
|
|
8228
|
+
const tiles = [];
|
|
8229
|
+
const tilesStmt = db.prepare('SELECT zoom_level, tile_column, tile_row, tile_data FROM tiles');
|
|
8230
|
+
try {
|
|
8231
|
+
while (tilesStmt.step()) {
|
|
8232
|
+
const row = tilesStmt.get();
|
|
8233
|
+
const [z, x, tmsRow, data] = row;
|
|
8234
|
+
// Sliced copy so the buffer is detached from sql.js's heap.
|
|
8235
|
+
const copy = new Uint8Array(data.byteLength);
|
|
8236
|
+
copy.set(data);
|
|
8237
|
+
// Our IndexedDB stores vector tiles decompressed (tileService
|
|
8238
|
+
// inflates on download). MBTiles vector tiles are gzipped by
|
|
8239
|
+
// convention — un-gzip on the way in so the stored tile matches
|
|
8240
|
+
// what the fetch handler expects to serve.
|
|
8241
|
+
const storedBytes = isVector && hasGzipMagic(copy) ? await gunzipBytes(copy) : copy;
|
|
8242
|
+
tiles.push({
|
|
8243
|
+
z,
|
|
8244
|
+
x,
|
|
8245
|
+
y: flipY(tmsRow, z),
|
|
8246
|
+
data: storedBytes.buffer,
|
|
8247
|
+
format,
|
|
8248
|
+
sourceId: 'imported',
|
|
8249
|
+
});
|
|
8250
|
+
}
|
|
8251
|
+
}
|
|
8252
|
+
finally {
|
|
8253
|
+
tilesStmt.free();
|
|
8254
|
+
}
|
|
8255
|
+
const minZoom = metadata.minzoom !== undefined ? Number(metadata.minzoom) : 0;
|
|
8256
|
+
const maxZoom = metadata.maxzoom !== undefined ? Number(metadata.maxzoom) : 14;
|
|
8257
|
+
return {
|
|
8258
|
+
metadata: {
|
|
8259
|
+
id: metadata.name || 'imported-region',
|
|
8260
|
+
name: metadata.name || 'Imported Region',
|
|
8261
|
+
description: metadata.description,
|
|
8262
|
+
bounds: [
|
|
8263
|
+
[bounds[0], bounds[1]],
|
|
8264
|
+
[bounds[2], bounds[3]],
|
|
8265
|
+
],
|
|
8266
|
+
minZoom,
|
|
8267
|
+
maxZoom,
|
|
8268
|
+
styleUrl: '',
|
|
8269
|
+
createdAt: Date.now(),
|
|
8270
|
+
exportedAt: Date.now(),
|
|
8271
|
+
version: '1.0.0',
|
|
8272
|
+
format: 'mbtiles',
|
|
8273
|
+
},
|
|
8274
|
+
style: {},
|
|
8275
|
+
tiles,
|
|
8276
|
+
};
|
|
8277
|
+
}
|
|
8278
|
+
finally {
|
|
8279
|
+
db.close();
|
|
8280
|
+
}
|
|
8080
8281
|
}
|
|
8081
8282
|
/**
|
|
8082
8283
|
* Import region data to database
|
|
@@ -8141,16 +8342,15 @@ class ImportExportService {
|
|
|
8141
8342
|
});
|
|
8142
8343
|
}
|
|
8143
8344
|
}
|
|
8144
|
-
// Import sprites and fonts similarly...
|
|
8145
8345
|
return {
|
|
8146
8346
|
success: true,
|
|
8147
8347
|
regionId,
|
|
8148
8348
|
message: 'Region imported successfully',
|
|
8149
8349
|
statistics: {
|
|
8150
8350
|
tilesImported: regionData.tiles?.length || 0,
|
|
8151
|
-
spritesImported:
|
|
8152
|
-
fontsImported:
|
|
8153
|
-
totalSize: 0,
|
|
8351
|
+
spritesImported: 0,
|
|
8352
|
+
fontsImported: 0,
|
|
8353
|
+
totalSize: 0,
|
|
8154
8354
|
},
|
|
8155
8355
|
};
|
|
8156
8356
|
}
|
|
@@ -8379,8 +8579,6 @@ const createMaintenanceManagement = (services, deps) => {
|
|
|
8379
8579
|
};
|
|
8380
8580
|
|
|
8381
8581
|
const createImportExportManagement = (services) => ({
|
|
8382
|
-
exportRegionAsJSON: async (regionId, options = {}) => services.importExportService.exportRegionAsJSON(regionId, options),
|
|
8383
|
-
exportRegionAsPMTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsPMTiles(regionId, options),
|
|
8384
8582
|
exportRegionAsMBTiles: async (regionId, options = {}) => services.importExportService.exportRegionAsMBTiles(regionId, options),
|
|
8385
8583
|
importRegion: async (importData) => services.importExportService.importRegion(importData),
|
|
8386
8584
|
downloadExportedRegion: (exportResult) => {
|
|
@@ -8736,10 +8934,6 @@ const en = {
|
|
|
8736
8934
|
'styleSelection.title': 'Select Offline Style',
|
|
8737
8935
|
'styleSelection.message': 'Choose which offline style to load:',
|
|
8738
8936
|
'styleSelection.sources': 'sources',
|
|
8739
|
-
// Import/Export
|
|
8740
|
-
'importExport.title': 'Import/Export',
|
|
8741
|
-
'importExport.export': 'Export',
|
|
8742
|
-
'importExport.import': 'Import',
|
|
8743
8937
|
// Errors
|
|
8744
8938
|
'error.loadingContent': 'Error loading content',
|
|
8745
8939
|
'error.tryAgain': 'Please try again',
|
|
@@ -8764,41 +8958,30 @@ const en = {
|
|
|
8764
8958
|
'regionDetails.bounds': 'Bounds',
|
|
8765
8959
|
'regionDetails.zoomRange': 'Zoom Range',
|
|
8766
8960
|
'regionDetails.created': 'Created',
|
|
8767
|
-
//
|
|
8768
|
-
'
|
|
8769
|
-
'
|
|
8770
|
-
'
|
|
8771
|
-
'
|
|
8772
|
-
'
|
|
8773
|
-
'
|
|
8774
|
-
'
|
|
8775
|
-
'
|
|
8776
|
-
'
|
|
8777
|
-
'
|
|
8778
|
-
'
|
|
8779
|
-
'
|
|
8780
|
-
'
|
|
8781
|
-
'
|
|
8782
|
-
'
|
|
8783
|
-
'
|
|
8784
|
-
'
|
|
8785
|
-
'
|
|
8786
|
-
'
|
|
8787
|
-
'
|
|
8788
|
-
'
|
|
8789
|
-
'
|
|
8790
|
-
'
|
|
8791
|
-
'importExport.fileFormatsHint': 'Supports JSON, PMTiles, and MBTiles formats',
|
|
8792
|
-
'importExport.newRegionName': 'New Region Name (Optional)',
|
|
8793
|
-
'importExport.newRegionNamePlaceholder': 'Leave empty to use original name',
|
|
8794
|
-
'importExport.overwriteIfExists': 'Overwrite if region exists',
|
|
8795
|
-
'importExport.preparingImport': 'Preparing import...',
|
|
8796
|
-
'importExport.importComplete': 'Import complete!',
|
|
8797
|
-
'importExport.importFailed': 'Import failed. Please try again.',
|
|
8798
|
-
'importExport.formatGuide': 'Format Guide',
|
|
8799
|
-
'importExport.jsonDesc': 'Complete data, human-readable, best for development',
|
|
8800
|
-
'importExport.pmtilesDesc': 'Web-optimized, efficient serving, cloud-friendly',
|
|
8801
|
-
'importExport.mbtilesDesc': 'Industry standard, SQLite-based, cross-platform',
|
|
8961
|
+
// MBTiles Modal
|
|
8962
|
+
'mbtiles.title': 'MBTiles Import / Export',
|
|
8963
|
+
'mbtiles.regionInfo': 'Region Information',
|
|
8964
|
+
'mbtiles.id': 'ID',
|
|
8965
|
+
'mbtiles.name': 'Name',
|
|
8966
|
+
'mbtiles.unnamed': 'Unnamed',
|
|
8967
|
+
'mbtiles.zoom': 'Zoom',
|
|
8968
|
+
'mbtiles.created': 'Created',
|
|
8969
|
+
'mbtiles.exportTitle': 'Export as MBTiles',
|
|
8970
|
+
'mbtiles.exportHint': 'Package the tiles in this region into a standard SQLite MBTiles archive that opens in QGIS, tippecanoe, and other tools.',
|
|
8971
|
+
'mbtiles.exportButton': 'Download .mbtiles',
|
|
8972
|
+
'mbtiles.preparingExport': 'Preparing export...',
|
|
8973
|
+
'mbtiles.exportComplete': 'Export complete!',
|
|
8974
|
+
'mbtiles.exportFailed': 'Export failed. Please try again.',
|
|
8975
|
+
'mbtiles.importTitle': 'Import from MBTiles',
|
|
8976
|
+
'mbtiles.selectFile': 'Select an .mbtiles file',
|
|
8977
|
+
'mbtiles.fileHint': 'Only SQLite-format .mbtiles files are supported.',
|
|
8978
|
+
'mbtiles.newRegionName': 'New Region Name (optional)',
|
|
8979
|
+
'mbtiles.newRegionNamePlaceholder': 'Leave empty to use the name from the file',
|
|
8980
|
+
'mbtiles.overwriteIfExists': 'Overwrite if a region with the same id exists',
|
|
8981
|
+
'mbtiles.importButton': 'Import .mbtiles',
|
|
8982
|
+
'mbtiles.preparingImport': 'Preparing import...',
|
|
8983
|
+
'mbtiles.importComplete': 'Import complete!',
|
|
8984
|
+
'mbtiles.importFailed': 'Import failed. Please try again.',
|
|
8802
8985
|
// Active Downloads
|
|
8803
8986
|
'download.activeCount': 'Active Downloads ({{count}})',
|
|
8804
8987
|
// Panel Manager additional strings
|
|
@@ -8959,10 +9142,6 @@ const ar = {
|
|
|
8959
9142
|
'styleSelection.title': 'اختر نمط غير متصل',
|
|
8960
9143
|
'styleSelection.message': 'اختر النمط غير المتصل الذي تريد تحميله:',
|
|
8961
9144
|
'styleSelection.sources': 'مصادر',
|
|
8962
|
-
// Import/Export - استيراد/تصدير
|
|
8963
|
-
'importExport.title': 'استيراد/تصدير',
|
|
8964
|
-
'importExport.export': 'تصدير',
|
|
8965
|
-
'importExport.import': 'استيراد',
|
|
8966
9145
|
// Errors - الأخطاء
|
|
8967
9146
|
'error.loadingContent': 'خطأ في تحميل المحتوى',
|
|
8968
9147
|
'error.tryAgain': 'يرجى المحاولة مرة أخرى',
|
|
@@ -8987,41 +9166,30 @@ const ar = {
|
|
|
8987
9166
|
'regionDetails.bounds': 'الحدود',
|
|
8988
9167
|
'regionDetails.zoomRange': 'نطاق التكبير',
|
|
8989
9168
|
'regionDetails.created': 'تاريخ الإنشاء',
|
|
8990
|
-
//
|
|
8991
|
-
'
|
|
8992
|
-
'
|
|
8993
|
-
'
|
|
8994
|
-
'
|
|
8995
|
-
'
|
|
8996
|
-
'
|
|
8997
|
-
'
|
|
8998
|
-
'
|
|
8999
|
-
'
|
|
9000
|
-
'
|
|
9001
|
-
'
|
|
9002
|
-
'
|
|
9003
|
-
'
|
|
9004
|
-
'
|
|
9005
|
-
'
|
|
9006
|
-
'
|
|
9007
|
-
'
|
|
9008
|
-
'
|
|
9009
|
-
'
|
|
9010
|
-
'
|
|
9011
|
-
'
|
|
9012
|
-
'
|
|
9013
|
-
'
|
|
9014
|
-
'importExport.fileFormatsHint': 'يدعم تنسيقات JSON و PMTiles و MBTiles',
|
|
9015
|
-
'importExport.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
9016
|
-
'importExport.newRegionNamePlaceholder': 'اتركه فارغاً لاستخدام الاسم الأصلي',
|
|
9017
|
-
'importExport.overwriteIfExists': 'الكتابة فوق المنطقة الموجودة',
|
|
9018
|
-
'importExport.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
9019
|
-
'importExport.importComplete': 'اكتمل الاستيراد!',
|
|
9020
|
-
'importExport.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
9021
|
-
'importExport.formatGuide': 'دليل التنسيقات',
|
|
9022
|
-
'importExport.jsonDesc': 'بيانات كاملة، قابلة للقراءة، الأفضل للتطوير',
|
|
9023
|
-
'importExport.pmtilesDesc': 'محسن للويب، خدمة فعالة، متوافق مع السحابة',
|
|
9024
|
-
'importExport.mbtilesDesc': 'معيار الصناعة، قائم على SQLite، متعدد المنصات',
|
|
9169
|
+
// MBTiles Modal - نافذة MBTiles
|
|
9170
|
+
'mbtiles.title': 'استيراد / تصدير MBTiles',
|
|
9171
|
+
'mbtiles.regionInfo': 'معلومات المنطقة',
|
|
9172
|
+
'mbtiles.id': 'المعرف',
|
|
9173
|
+
'mbtiles.name': 'الاسم',
|
|
9174
|
+
'mbtiles.unnamed': 'بدون اسم',
|
|
9175
|
+
'mbtiles.zoom': 'التكبير',
|
|
9176
|
+
'mbtiles.created': 'تاريخ الإنشاء',
|
|
9177
|
+
'mbtiles.exportTitle': 'التصدير كـ MBTiles',
|
|
9178
|
+
'mbtiles.exportHint': 'احزم بلاطات هذه المنطقة داخل أرشيف MBTiles (SQLite) يمكن فتحه في QGIS و tippecanoe وأدوات أخرى.',
|
|
9179
|
+
'mbtiles.exportButton': 'تنزيل ملف mbtiles.',
|
|
9180
|
+
'mbtiles.preparingExport': 'جاري تجهيز التصدير...',
|
|
9181
|
+
'mbtiles.exportComplete': 'اكتمل التصدير!',
|
|
9182
|
+
'mbtiles.exportFailed': 'فشل التصدير. يرجى المحاولة مرة أخرى.',
|
|
9183
|
+
'mbtiles.importTitle': 'الاستيراد من MBTiles',
|
|
9184
|
+
'mbtiles.selectFile': 'اختر ملف mbtiles.',
|
|
9185
|
+
'mbtiles.fileHint': 'تُدعم ملفات mbtiles. بتنسيق SQLite فقط.',
|
|
9186
|
+
'mbtiles.newRegionName': 'اسم المنطقة الجديد (اختياري)',
|
|
9187
|
+
'mbtiles.newRegionNamePlaceholder': 'اتركه فارغًا لاستخدام الاسم من الملف',
|
|
9188
|
+
'mbtiles.overwriteIfExists': 'الكتابة فوق المنطقة إذا كان المعرف موجودًا',
|
|
9189
|
+
'mbtiles.importButton': 'استيراد ملف mbtiles.',
|
|
9190
|
+
'mbtiles.preparingImport': 'جاري تجهيز الاستيراد...',
|
|
9191
|
+
'mbtiles.importComplete': 'اكتمل الاستيراد!',
|
|
9192
|
+
'mbtiles.importFailed': 'فشل الاستيراد. يرجى المحاولة مرة أخرى.',
|
|
9025
9193
|
// Active Downloads - التحميلات النشطة
|
|
9026
9194
|
'download.activeCount': 'التحميلات النشطة ({{count}})',
|
|
9027
9195
|
// Panel Manager additional strings - سلاسل إضافية للوحة
|
|
@@ -9960,49 +10128,42 @@ class ConfirmationModal {
|
|
|
9960
10128
|
}
|
|
9961
10129
|
|
|
9962
10130
|
/**
|
|
9963
|
-
* Import/Export Modal
|
|
9964
|
-
*
|
|
9965
|
-
*
|
|
10131
|
+
* MBTiles Import/Export Modal
|
|
10132
|
+
*
|
|
10133
|
+
* Focused modal for exchanging regions as binary SQLite MBTiles archives.
|
|
10134
|
+
* Replaces the previous multi-format import/export modal.
|
|
9966
10135
|
*/
|
|
9967
|
-
const modalLogger = logger.scope('
|
|
9968
|
-
class
|
|
10136
|
+
const modalLogger = logger.scope('MBTilesModal');
|
|
10137
|
+
class MBTilesModal {
|
|
9969
10138
|
modal;
|
|
9970
10139
|
options;
|
|
9971
10140
|
isExporting = false;
|
|
9972
10141
|
isImporting = false;
|
|
9973
|
-
// Form elements
|
|
9974
|
-
exportFormatSelect;
|
|
9975
|
-
includeStyleCheckbox;
|
|
9976
|
-
includeTilesCheckbox;
|
|
9977
|
-
includeSpritesCheckbox;
|
|
9978
|
-
includeFontsCheckbox;
|
|
9979
10142
|
exportProgressBar;
|
|
9980
10143
|
exportProgressText;
|
|
10144
|
+
exportProgressContainer;
|
|
9981
10145
|
exportButton;
|
|
9982
10146
|
importFileInput;
|
|
9983
10147
|
importNameInput;
|
|
9984
10148
|
importOverwriteCheckbox;
|
|
9985
10149
|
importProgressBar;
|
|
9986
10150
|
importProgressText;
|
|
10151
|
+
importProgressContainer;
|
|
9987
10152
|
importButton;
|
|
9988
10153
|
constructor(options) {
|
|
9989
10154
|
this.options = options;
|
|
9990
10155
|
}
|
|
9991
10156
|
show() {
|
|
9992
10157
|
const modalConfig = {
|
|
9993
|
-
title: t('
|
|
10158
|
+
title: t('mbtiles.title'),
|
|
9994
10159
|
subtitle: this.options.region.name || this.options.region.id,
|
|
9995
10160
|
size: 'md',
|
|
9996
10161
|
closable: true,
|
|
9997
10162
|
onClose: () => this.hide(),
|
|
9998
10163
|
};
|
|
9999
10164
|
this.modal = new Modal(modalConfig);
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
this.modal.setContent(content);
|
|
10003
|
-
// Create footer with close button
|
|
10004
|
-
const footer = this.createFooter();
|
|
10005
|
-
this.modal.setFooter(footer);
|
|
10165
|
+
this.modal.setContent(this.createContent());
|
|
10166
|
+
this.modal.setFooter(this.createFooter());
|
|
10006
10167
|
this.modal.show();
|
|
10007
10168
|
this.attachEventListeners();
|
|
10008
10169
|
return this.modal.getElement();
|
|
@@ -10011,217 +10172,96 @@ class ImportExportModal {
|
|
|
10011
10172
|
this.modal?.hide();
|
|
10012
10173
|
this.options.onClose();
|
|
10013
10174
|
}
|
|
10175
|
+
destroy() {
|
|
10176
|
+
this.modal?.destroy();
|
|
10177
|
+
}
|
|
10014
10178
|
createContent() {
|
|
10015
10179
|
const content = document.createElement('div');
|
|
10016
|
-
content.className = 'flex flex-col gap-6';
|
|
10180
|
+
content.className = 'flex flex-col gap-6 py-2';
|
|
10017
10181
|
if (i18n.isRTL()) {
|
|
10018
10182
|
content.setAttribute('dir', 'rtl');
|
|
10019
10183
|
}
|
|
10020
|
-
|
|
10021
|
-
|
|
10022
|
-
content.appendChild(
|
|
10023
|
-
// Export/Import Grid
|
|
10024
|
-
const gridContainer = document.createElement('div');
|
|
10025
|
-
gridContainer.className = 'grid grid-cols-1 gap-6';
|
|
10026
|
-
// Export Section
|
|
10027
|
-
const exportSection = this.createExportSection();
|
|
10028
|
-
gridContainer.appendChild(exportSection);
|
|
10029
|
-
// Import Section
|
|
10030
|
-
const importSection = this.createImportSection();
|
|
10031
|
-
gridContainer.appendChild(importSection);
|
|
10032
|
-
content.appendChild(gridContainer);
|
|
10033
|
-
// Format Guide
|
|
10034
|
-
const formatGuide = this.createFormatGuide();
|
|
10035
|
-
content.appendChild(formatGuide);
|
|
10184
|
+
content.appendChild(this.createRegionInfoLine());
|
|
10185
|
+
content.appendChild(this.createExportSection());
|
|
10186
|
+
content.appendChild(this.createImportSection());
|
|
10036
10187
|
return content;
|
|
10037
10188
|
}
|
|
10038
|
-
|
|
10039
|
-
const
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
</div>
|
|
10051
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
10052
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.name')}</span>
|
|
10053
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${escapeHtml$1(this.options.region.name || t('importExport.unnamed'))}</div>
|
|
10054
|
-
</div>
|
|
10055
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
10056
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.zoom')}</span>
|
|
10057
|
-
<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>
|
|
10058
|
-
</div>
|
|
10059
|
-
<div class="p-3 rounded-lg bg-white/40 dark:bg-black/20">
|
|
10060
|
-
<span class="font-medium text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">${t('importExport.created')}</span>
|
|
10061
|
-
<div class="text-gray-900 dark:text-white font-medium mt-1">${new Date(this.options.region.created).toLocaleDateString()}</div>
|
|
10062
|
-
</div>
|
|
10063
|
-
</div>
|
|
10189
|
+
createRegionInfoLine() {
|
|
10190
|
+
const { region } = this.options;
|
|
10191
|
+
const line = document.createElement('div');
|
|
10192
|
+
line.className =
|
|
10193
|
+
'flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-gray-500 dark:text-gray-400';
|
|
10194
|
+
line.innerHTML = `
|
|
10195
|
+
<span class="flex items-center gap-1">
|
|
10196
|
+
${icons.mapPin({ size: 12, color: 'currentColor' })}
|
|
10197
|
+
<span class="font-mono">${escapeHtml$1(region.id)}</span>
|
|
10198
|
+
</span>
|
|
10199
|
+
<span>Z${escapeHtml$1(region.minZoom)}-${escapeHtml$1(region.maxZoom)}</span>
|
|
10200
|
+
<span>${new Date(region.created).toLocaleDateString()}</span>
|
|
10064
10201
|
`;
|
|
10065
|
-
return
|
|
10202
|
+
return line;
|
|
10066
10203
|
}
|
|
10067
10204
|
createExportSection() {
|
|
10068
|
-
const section =
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10076
|
-
|
|
10077
|
-
|
|
10078
|
-
|
|
10079
|
-
|
|
10080
|
-
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
`;
|
|
10084
|
-
section.appendChild(header);
|
|
10085
|
-
const formContainer = document.createElement('div');
|
|
10086
|
-
formContainer.className = 'space-y-5';
|
|
10087
|
-
// Format Selection
|
|
10088
|
-
const formatGroup = document.createElement('div');
|
|
10089
|
-
const formatLabel = document.createElement('label');
|
|
10090
|
-
formatLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
10091
|
-
formatLabel.textContent = t('importExport.exportFormat');
|
|
10092
|
-
this.exportFormatSelect = document.createElement('select');
|
|
10093
|
-
this.exportFormatSelect.className =
|
|
10094
|
-
'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';
|
|
10095
|
-
this.exportFormatSelect.innerHTML = `
|
|
10096
|
-
<option value="json">${t('importExport.formatJson')}</option>
|
|
10097
|
-
<option value="pmtiles">${t('importExport.formatPmtiles')}</option>
|
|
10098
|
-
<option value="mbtiles">${t('importExport.formatMbtiles')}</option>
|
|
10099
|
-
`;
|
|
10100
|
-
const formatHint = document.createElement('p');
|
|
10101
|
-
formatHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
10102
|
-
formatHint.textContent = t('importExport.formatHint');
|
|
10103
|
-
formatGroup.appendChild(formatLabel);
|
|
10104
|
-
formatGroup.appendChild(this.exportFormatSelect);
|
|
10105
|
-
formatGroup.appendChild(formatHint);
|
|
10106
|
-
formContainer.appendChild(formatGroup);
|
|
10107
|
-
// Export Options
|
|
10108
|
-
const optionsGroup = document.createElement('div');
|
|
10109
|
-
const optionsLabel = document.createElement('label');
|
|
10110
|
-
optionsLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-3';
|
|
10111
|
-
optionsLabel.textContent = t('importExport.includeComponents');
|
|
10112
|
-
const checkboxContainer = document.createElement('div');
|
|
10113
|
-
checkboxContainer.className = 'grid grid-cols-1 sm:grid-cols-2 gap-3';
|
|
10114
|
-
const createCheckbox = (text, checked = true) => {
|
|
10115
|
-
const label = document.createElement('label');
|
|
10116
|
-
label.className =
|
|
10117
|
-
'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';
|
|
10118
|
-
const input = document.createElement('input');
|
|
10119
|
-
input.type = 'checkbox';
|
|
10120
|
-
input.checked = checked;
|
|
10121
|
-
input.className =
|
|
10122
|
-
'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';
|
|
10123
|
-
const span = document.createElement('span');
|
|
10124
|
-
span.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
10125
|
-
span.textContent = text;
|
|
10126
|
-
label.appendChild(input);
|
|
10127
|
-
label.appendChild(span);
|
|
10128
|
-
return { label, input };
|
|
10129
|
-
};
|
|
10130
|
-
const styleCheck = createCheckbox(t('importExport.styleConfig'));
|
|
10131
|
-
this.includeStyleCheckbox = styleCheck.input;
|
|
10132
|
-
checkboxContainer.appendChild(styleCheck.label);
|
|
10133
|
-
const tilesCheck = createCheckbox(t('importExport.mapTiles'));
|
|
10134
|
-
this.includeTilesCheckbox = tilesCheck.input;
|
|
10135
|
-
checkboxContainer.appendChild(tilesCheck.label);
|
|
10136
|
-
const spritesCheck = createCheckbox(t('importExport.spritesIcons'));
|
|
10137
|
-
this.includeSpritesCheckbox = spritesCheck.input;
|
|
10138
|
-
checkboxContainer.appendChild(spritesCheck.label);
|
|
10139
|
-
const fontsCheck = createCheckbox(t('importExport.fontsGlyphs'));
|
|
10140
|
-
this.includeFontsCheckbox = fontsCheck.input;
|
|
10141
|
-
checkboxContainer.appendChild(fontsCheck.label);
|
|
10142
|
-
optionsGroup.appendChild(optionsLabel);
|
|
10143
|
-
optionsGroup.appendChild(checkboxContainer);
|
|
10144
|
-
formContainer.appendChild(optionsGroup);
|
|
10145
|
-
// Export Progress (hidden by default)
|
|
10146
|
-
const progressContainer = document.createElement('div');
|
|
10147
|
-
progressContainer.className = 'hidden';
|
|
10148
|
-
const progressBarContainer = document.createElement('div');
|
|
10149
|
-
progressBarContainer.className =
|
|
10150
|
-
'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
10151
|
-
this.exportProgressBar = document.createElement('div');
|
|
10152
|
-
this.exportProgressBar.className = 'bg-blue-600 h-2 rounded-full transition-all duration-300';
|
|
10153
|
-
this.exportProgressBar.style.width = '0%';
|
|
10154
|
-
progressBarContainer.appendChild(this.exportProgressBar);
|
|
10155
|
-
this.exportProgressText = document.createElement('p');
|
|
10156
|
-
this.exportProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10157
|
-
this.exportProgressText.textContent = t('importExport.preparingExport');
|
|
10158
|
-
progressContainer.appendChild(progressBarContainer);
|
|
10159
|
-
progressContainer.appendChild(this.exportProgressText);
|
|
10160
|
-
formContainer.appendChild(progressContainer);
|
|
10161
|
-
// Export Button
|
|
10162
|
-
const exportButton = new Button({
|
|
10163
|
-
text: t('importExport.exportRegion'),
|
|
10205
|
+
const section = this.createSection(t('mbtiles.exportTitle'), 'blue', icons.download({ size: 20, color: 'currentColor' }));
|
|
10206
|
+
const form = document.createElement('div');
|
|
10207
|
+
form.className = 'space-y-5';
|
|
10208
|
+
const hint = document.createElement('p');
|
|
10209
|
+
hint.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10210
|
+
hint.textContent = t('mbtiles.exportHint');
|
|
10211
|
+
form.appendChild(hint);
|
|
10212
|
+
// Progress (hidden by default)
|
|
10213
|
+
const progress = this.createProgressBlock('blue', t('mbtiles.preparingExport'));
|
|
10214
|
+
this.exportProgressContainer = progress.container;
|
|
10215
|
+
this.exportProgressBar = progress.bar;
|
|
10216
|
+
this.exportProgressText = progress.text;
|
|
10217
|
+
form.appendChild(progress.container);
|
|
10218
|
+
const exportBtn = new Button({
|
|
10219
|
+
text: t('mbtiles.exportButton'),
|
|
10164
10220
|
variant: 'primary',
|
|
10165
10221
|
icon: icons.download({ size: 16, color: 'white' }),
|
|
10166
|
-
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
10222
|
+
className: 'w-full py-2.5 text-base shadow-lg shadow-blue-500/20',
|
|
10167
10223
|
onClick: () => this.handleExport(),
|
|
10168
10224
|
});
|
|
10169
|
-
this.exportButton =
|
|
10170
|
-
|
|
10171
|
-
section.appendChild(
|
|
10225
|
+
this.exportButton = exportBtn.getElement();
|
|
10226
|
+
form.appendChild(this.exportButton);
|
|
10227
|
+
section.appendChild(form);
|
|
10172
10228
|
return section;
|
|
10173
10229
|
}
|
|
10174
10230
|
createImportSection() {
|
|
10175
|
-
const section =
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
//
|
|
10179
|
-
const accent = document.createElement('div');
|
|
10180
|
-
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-green-500 opacity-50`;
|
|
10181
|
-
section.appendChild(accent);
|
|
10182
|
-
const header = document.createElement('h3');
|
|
10183
|
-
header.className =
|
|
10184
|
-
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
10185
|
-
header.innerHTML = `
|
|
10186
|
-
<div class="p-2 bg-green-500/10 rounded-lg text-green-600 dark:text-green-400">
|
|
10187
|
-
${icons.upload({ size: 20, color: 'currentColor' })}
|
|
10188
|
-
</div>
|
|
10189
|
-
${t('importExport.importRegion')}
|
|
10190
|
-
`;
|
|
10191
|
-
section.appendChild(header);
|
|
10192
|
-
const formContainer = document.createElement('div');
|
|
10193
|
-
formContainer.className = 'space-y-5';
|
|
10194
|
-
// File Selection
|
|
10231
|
+
const section = this.createSection(t('mbtiles.importTitle'), 'green', icons.upload({ size: 20, color: 'currentColor' }));
|
|
10232
|
+
const form = document.createElement('div');
|
|
10233
|
+
form.className = 'space-y-5';
|
|
10234
|
+
// File input
|
|
10195
10235
|
const fileGroup = document.createElement('div');
|
|
10196
10236
|
const fileLabel = document.createElement('label');
|
|
10197
10237
|
fileLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
10198
|
-
fileLabel.textContent = t('
|
|
10238
|
+
fileLabel.textContent = t('mbtiles.selectFile');
|
|
10199
10239
|
this.importFileInput = document.createElement('input');
|
|
10200
10240
|
this.importFileInput.type = 'file';
|
|
10201
|
-
this.importFileInput.accept = '.
|
|
10241
|
+
this.importFileInput.accept = '.mbtiles,application/vnd.sqlite3,application/x-sqlite3';
|
|
10202
10242
|
this.importFileInput.className =
|
|
10203
10243
|
'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';
|
|
10204
10244
|
const fileHint = document.createElement('p');
|
|
10205
10245
|
fileHint.className = 'mt-2 text-xs text-gray-500 dark:text-gray-400 ml-1';
|
|
10206
|
-
fileHint.textContent = t('
|
|
10246
|
+
fileHint.textContent = t('mbtiles.fileHint');
|
|
10207
10247
|
fileGroup.appendChild(fileLabel);
|
|
10208
10248
|
fileGroup.appendChild(this.importFileInput);
|
|
10209
10249
|
fileGroup.appendChild(fileHint);
|
|
10210
|
-
|
|
10211
|
-
// New
|
|
10250
|
+
form.appendChild(fileGroup);
|
|
10251
|
+
// New region name
|
|
10212
10252
|
const nameGroup = document.createElement('div');
|
|
10213
10253
|
const nameLabel = document.createElement('label');
|
|
10214
10254
|
nameLabel.className = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2';
|
|
10215
|
-
nameLabel.textContent = t('
|
|
10255
|
+
nameLabel.textContent = t('mbtiles.newRegionName');
|
|
10216
10256
|
this.importNameInput = document.createElement('input');
|
|
10217
10257
|
this.importNameInput.type = 'text';
|
|
10218
|
-
this.importNameInput.placeholder = t('
|
|
10258
|
+
this.importNameInput.placeholder = t('mbtiles.newRegionNamePlaceholder');
|
|
10219
10259
|
this.importNameInput.className =
|
|
10220
10260
|
'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';
|
|
10221
10261
|
nameGroup.appendChild(nameLabel);
|
|
10222
10262
|
nameGroup.appendChild(this.importNameInput);
|
|
10223
|
-
|
|
10224
|
-
//
|
|
10263
|
+
form.appendChild(nameGroup);
|
|
10264
|
+
// Overwrite toggle
|
|
10225
10265
|
const overwriteLabel = document.createElement('label');
|
|
10226
10266
|
overwriteLabel.className =
|
|
10227
10267
|
'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';
|
|
@@ -10231,79 +10271,85 @@ class ImportExportModal {
|
|
|
10231
10271
|
'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';
|
|
10232
10272
|
const overwriteSpan = document.createElement('span');
|
|
10233
10273
|
overwriteSpan.className = 'text-sm text-gray-700 dark:text-gray-300 font-medium';
|
|
10234
|
-
overwriteSpan.textContent = t('
|
|
10274
|
+
overwriteSpan.textContent = t('mbtiles.overwriteIfExists');
|
|
10235
10275
|
overwriteLabel.appendChild(this.importOverwriteCheckbox);
|
|
10236
10276
|
overwriteLabel.appendChild(overwriteSpan);
|
|
10237
|
-
|
|
10238
|
-
//
|
|
10239
|
-
const
|
|
10240
|
-
|
|
10241
|
-
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10245
|
-
|
|
10246
|
-
|
|
10247
|
-
|
|
10248
|
-
this.importProgressText = document.createElement('p');
|
|
10249
|
-
this.importProgressText.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10250
|
-
this.importProgressText.textContent = t('importExport.preparingImport');
|
|
10251
|
-
progressContainer.appendChild(progressBarContainer);
|
|
10252
|
-
progressContainer.appendChild(this.importProgressText);
|
|
10253
|
-
formContainer.appendChild(progressContainer);
|
|
10254
|
-
// Import Button
|
|
10255
|
-
const importButton = new Button({
|
|
10256
|
-
text: t('importExport.importRegion'),
|
|
10257
|
-
variant: 'success', // Assuming 'success' variant exists in Button component, if not might need style adjustment. Assuming it works based on previous code.
|
|
10277
|
+
form.appendChild(overwriteLabel);
|
|
10278
|
+
// Progress
|
|
10279
|
+
const progress = this.createProgressBlock('green', t('mbtiles.preparingImport'));
|
|
10280
|
+
this.importProgressContainer = progress.container;
|
|
10281
|
+
this.importProgressBar = progress.bar;
|
|
10282
|
+
this.importProgressText = progress.text;
|
|
10283
|
+
form.appendChild(progress.container);
|
|
10284
|
+
// Import button (disabled until a file is selected)
|
|
10285
|
+
const importBtn = new Button({
|
|
10286
|
+
text: t('mbtiles.importButton'),
|
|
10287
|
+
variant: 'success',
|
|
10258
10288
|
icon: icons.upload({ size: 16, color: 'white' }),
|
|
10259
10289
|
className: 'w-full py-2.5 text-base shadow-lg shadow-green-500/20',
|
|
10260
10290
|
disabled: true,
|
|
10261
10291
|
onClick: () => this.handleImport(),
|
|
10262
10292
|
});
|
|
10263
|
-
this.importButton =
|
|
10264
|
-
|
|
10265
|
-
section.appendChild(
|
|
10293
|
+
this.importButton = importBtn.getElement();
|
|
10294
|
+
form.appendChild(this.importButton);
|
|
10295
|
+
section.appendChild(form);
|
|
10266
10296
|
return section;
|
|
10267
10297
|
}
|
|
10268
|
-
|
|
10269
|
-
const
|
|
10270
|
-
|
|
10271
|
-
|
|
10272
|
-
|
|
10273
|
-
${
|
|
10274
|
-
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
|
|
10280
|
-
|
|
10281
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
10282
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">PMTiles</div>
|
|
10283
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.pmtilesDesc')}</div>
|
|
10284
|
-
</div>
|
|
10285
|
-
<div class="p-3 rounded-lg bg-white/50 dark:bg-black/20">
|
|
10286
|
-
<div class="font-bold text-base text-blue-800 dark:text-blue-300 mb-1">MBTiles</div>
|
|
10287
|
-
<div class="text-blue-700 dark:text-blue-400 leading-relaxed">${t('importExport.mbtilesDesc')}</div>
|
|
10288
|
-
</div>
|
|
10298
|
+
createSection(title, accentColor, iconHtml) {
|
|
10299
|
+
const section = document.createElement('div');
|
|
10300
|
+
section.className =
|
|
10301
|
+
'glass-input p-6 rounded-xl border-0 bg-white/40 dark:bg-gray-800/40 relative overflow-hidden';
|
|
10302
|
+
const accent = document.createElement('div');
|
|
10303
|
+
accent.className = `absolute top-0 ${i18n.isRTL() ? 'right-0' : 'left-0'} w-1 h-full bg-${accentColor}-500 opacity-50`;
|
|
10304
|
+
section.appendChild(accent);
|
|
10305
|
+
const header = document.createElement('h3');
|
|
10306
|
+
header.className =
|
|
10307
|
+
'text-lg font-bold text-gray-900 dark:text-white mb-5 flex items-center gap-2.5';
|
|
10308
|
+
header.innerHTML = `
|
|
10309
|
+
<div class="p-2 bg-${accentColor}-500/10 rounded-lg text-${accentColor}-600 dark:text-${accentColor}-400">
|
|
10310
|
+
${iconHtml}
|
|
10289
10311
|
</div>
|
|
10312
|
+
${title}
|
|
10290
10313
|
`;
|
|
10291
|
-
|
|
10314
|
+
section.appendChild(header);
|
|
10315
|
+
return section;
|
|
10316
|
+
}
|
|
10317
|
+
createProgressBlock(accentColor, initialText) {
|
|
10318
|
+
const container = document.createElement('div');
|
|
10319
|
+
container.className = 'hidden';
|
|
10320
|
+
const barWrap = document.createElement('div');
|
|
10321
|
+
barWrap.className = 'bg-gray-200 dark:bg-gray-700 rounded-full h-2 mb-2 overflow-hidden';
|
|
10322
|
+
const bar = document.createElement('div');
|
|
10323
|
+
bar.className = `bg-${accentColor}-600 h-2 rounded-full transition-all duration-300`;
|
|
10324
|
+
bar.style.width = '0%';
|
|
10325
|
+
barWrap.appendChild(bar);
|
|
10326
|
+
const text = document.createElement('p');
|
|
10327
|
+
text.className = 'text-sm text-gray-600 dark:text-gray-400';
|
|
10328
|
+
text.textContent = initialText;
|
|
10329
|
+
container.appendChild(barWrap);
|
|
10330
|
+
container.appendChild(text);
|
|
10331
|
+
return { container, bar, text };
|
|
10332
|
+
}
|
|
10333
|
+
createFooter() {
|
|
10334
|
+
const footer = document.createElement('div');
|
|
10335
|
+
footer.className = 'flex gap-3 justify-end';
|
|
10336
|
+
if (i18n.isRTL()) {
|
|
10337
|
+
footer.setAttribute('dir', 'rtl');
|
|
10338
|
+
}
|
|
10339
|
+
const close = new Button({
|
|
10340
|
+
text: t('app.close'),
|
|
10341
|
+
variant: 'secondary',
|
|
10342
|
+
onClick: () => this.hide(),
|
|
10343
|
+
});
|
|
10344
|
+
footer.appendChild(close.getElement());
|
|
10345
|
+
return footer;
|
|
10292
10346
|
}
|
|
10293
10347
|
attachEventListeners() {
|
|
10294
|
-
// Enable import button when file is selected
|
|
10295
10348
|
if (this.importFileInput && this.importButton) {
|
|
10296
10349
|
this.importFileInput.addEventListener('change', () => {
|
|
10297
|
-
|
|
10298
|
-
|
|
10299
|
-
|
|
10300
|
-
}
|
|
10301
|
-
}
|
|
10302
|
-
else {
|
|
10303
|
-
if (this.importButton) {
|
|
10304
|
-
this.importButton.disabled = true;
|
|
10305
|
-
}
|
|
10306
|
-
}
|
|
10350
|
+
const hasFile = !!(this.importFileInput?.files && this.importFileInput.files.length > 0);
|
|
10351
|
+
if (this.importButton)
|
|
10352
|
+
this.importButton.disabled = !hasFile;
|
|
10307
10353
|
});
|
|
10308
10354
|
}
|
|
10309
10355
|
}
|
|
@@ -10313,34 +10359,27 @@ class ImportExportModal {
|
|
|
10313
10359
|
this.isExporting = true;
|
|
10314
10360
|
if (this.exportButton)
|
|
10315
10361
|
this.exportButton.disabled = true;
|
|
10362
|
+
this.exportProgressContainer?.classList.remove('hidden');
|
|
10316
10363
|
try {
|
|
10317
|
-
const
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10324
|
-
|
|
10325
|
-
|
|
10326
|
-
if (progressContainer) {
|
|
10327
|
-
progressContainer.classList.remove('hidden');
|
|
10328
|
-
}
|
|
10329
|
-
const result = await this.options.exportRegion(this.options.region.id, format, options);
|
|
10330
|
-
if (this.exportProgressBar) {
|
|
10364
|
+
const result = await this.options.exportRegion(this.options.region.id, {
|
|
10365
|
+
onProgress: p => {
|
|
10366
|
+
if (this.exportProgressBar)
|
|
10367
|
+
this.exportProgressBar.style.width = `${p.percentage}%`;
|
|
10368
|
+
if (this.exportProgressText)
|
|
10369
|
+
this.exportProgressText.textContent = p.message;
|
|
10370
|
+
},
|
|
10371
|
+
});
|
|
10372
|
+
if (this.exportProgressBar)
|
|
10331
10373
|
this.exportProgressBar.style.width = '100%';
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
this.exportProgressText.textContent = t('importExport.exportComplete');
|
|
10335
|
-
}
|
|
10374
|
+
if (this.exportProgressText)
|
|
10375
|
+
this.exportProgressText.textContent = t('mbtiles.exportComplete');
|
|
10336
10376
|
this.options.onExport?.(result);
|
|
10337
|
-
|
|
10338
|
-
setTimeout(() => this.hide(), 1500);
|
|
10377
|
+
setTimeout(() => this.hide(), 1200);
|
|
10339
10378
|
}
|
|
10340
10379
|
catch (error) {
|
|
10341
10380
|
modalLogger.error('Export error:', error instanceof Error ? error.message : String(error));
|
|
10342
10381
|
if (this.exportProgressText) {
|
|
10343
|
-
this.exportProgressText.textContent = t('
|
|
10382
|
+
this.exportProgressText.textContent = t('mbtiles.exportFailed');
|
|
10344
10383
|
this.exportProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10345
10384
|
}
|
|
10346
10385
|
}
|
|
@@ -10351,45 +10390,47 @@ class ImportExportModal {
|
|
|
10351
10390
|
}
|
|
10352
10391
|
}
|
|
10353
10392
|
async handleImport() {
|
|
10354
|
-
if (this.isImporting || !this.options.importRegion
|
|
10393
|
+
if (this.isImporting || !this.options.importRegion)
|
|
10394
|
+
return;
|
|
10395
|
+
const file = this.importFileInput?.files?.[0];
|
|
10396
|
+
if (!file)
|
|
10355
10397
|
return;
|
|
10356
10398
|
this.isImporting = true;
|
|
10357
10399
|
if (this.importButton)
|
|
10358
10400
|
this.importButton.disabled = true;
|
|
10401
|
+
this.importProgressContainer?.classList.remove('hidden');
|
|
10359
10402
|
try {
|
|
10360
|
-
const file = this.importFileInput.files[0];
|
|
10361
|
-
const overwrite = this.importOverwriteCheckbox?.checked ?? false;
|
|
10362
|
-
// Show progress
|
|
10363
|
-
const progressContainer = this.importProgressBar?.parentElement?.parentElement;
|
|
10364
|
-
if (progressContainer) {
|
|
10365
|
-
progressContainer.classList.remove('hidden');
|
|
10366
|
-
}
|
|
10367
|
-
// Determine format from file extension
|
|
10368
|
-
const format = file.name.endsWith('.pmtiles')
|
|
10369
|
-
? 'pmtiles'
|
|
10370
|
-
: file.name.endsWith('.mbtiles')
|
|
10371
|
-
? 'mbtiles'
|
|
10372
|
-
: 'json';
|
|
10373
10403
|
const data = {
|
|
10374
10404
|
file,
|
|
10375
|
-
format,
|
|
10376
|
-
overwrite,
|
|
10405
|
+
format: 'mbtiles',
|
|
10406
|
+
overwrite: this.importOverwriteCheckbox?.checked ?? false,
|
|
10407
|
+
newRegionName: this.importNameInput?.value.trim() || undefined,
|
|
10408
|
+
onProgress: p => {
|
|
10409
|
+
if (this.importProgressBar)
|
|
10410
|
+
this.importProgressBar.style.width = `${p.percentage}%`;
|
|
10411
|
+
if (this.importProgressText)
|
|
10412
|
+
this.importProgressText.textContent = p.message;
|
|
10413
|
+
},
|
|
10377
10414
|
};
|
|
10378
10415
|
const result = await this.options.importRegion(data);
|
|
10379
|
-
if (this.importProgressBar)
|
|
10416
|
+
if (this.importProgressBar)
|
|
10380
10417
|
this.importProgressBar.style.width = '100%';
|
|
10381
|
-
}
|
|
10382
10418
|
if (this.importProgressText) {
|
|
10383
|
-
this.importProgressText.textContent =
|
|
10419
|
+
this.importProgressText.textContent = result.success
|
|
10420
|
+
? t('mbtiles.importComplete')
|
|
10421
|
+
: result.message;
|
|
10422
|
+
if (!result.success) {
|
|
10423
|
+
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10424
|
+
}
|
|
10384
10425
|
}
|
|
10385
10426
|
this.options.onImport?.(result);
|
|
10386
|
-
|
|
10387
|
-
|
|
10427
|
+
if (result.success)
|
|
10428
|
+
setTimeout(() => this.hide(), 1200);
|
|
10388
10429
|
}
|
|
10389
10430
|
catch (error) {
|
|
10390
10431
|
modalLogger.error('Import error:', error instanceof Error ? error.message : String(error));
|
|
10391
10432
|
if (this.importProgressText) {
|
|
10392
|
-
this.importProgressText.textContent = t('
|
|
10433
|
+
this.importProgressText.textContent = t('mbtiles.importFailed');
|
|
10393
10434
|
this.importProgressText.classList.add('text-red-600', 'dark:text-red-400');
|
|
10394
10435
|
}
|
|
10395
10436
|
}
|
|
@@ -10399,23 +10440,6 @@ class ImportExportModal {
|
|
|
10399
10440
|
this.importButton.disabled = false;
|
|
10400
10441
|
}
|
|
10401
10442
|
}
|
|
10402
|
-
createFooter() {
|
|
10403
|
-
const footer = document.createElement('div');
|
|
10404
|
-
footer.className = 'flex gap-3 justify-end';
|
|
10405
|
-
if (i18n.isRTL()) {
|
|
10406
|
-
footer.setAttribute('dir', 'rtl');
|
|
10407
|
-
}
|
|
10408
|
-
const closeButton = new Button({
|
|
10409
|
-
text: t('app.close'),
|
|
10410
|
-
variant: 'secondary',
|
|
10411
|
-
onClick: () => this.hide(),
|
|
10412
|
-
});
|
|
10413
|
-
footer.appendChild(closeButton.getElement());
|
|
10414
|
-
return footer;
|
|
10415
|
-
}
|
|
10416
|
-
destroy() {
|
|
10417
|
-
this.modal?.destroy();
|
|
10418
|
-
}
|
|
10419
10443
|
}
|
|
10420
10444
|
|
|
10421
10445
|
/**
|
|
@@ -11129,9 +11153,9 @@ class PanelRenderer extends BaseComponent {
|
|
|
11129
11153
|
<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')}">
|
|
11130
11154
|
${icons.download({ size: 14, color: 'currentColor' })}
|
|
11131
11155
|
</button>
|
|
11132
|
-
|
|
11156
|
+
<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')}">
|
|
11133
11157
|
${icons.deviceFloppy({ size: 14, color: 'currentColor' })}
|
|
11134
|
-
</button>
|
|
11158
|
+
</button>
|
|
11135
11159
|
<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')}">
|
|
11136
11160
|
${icons.trash({ size: 14, color: 'currentColor' })}
|
|
11137
11161
|
</button>
|
|
@@ -11393,7 +11417,7 @@ class PanelRenderer extends BaseComponent {
|
|
|
11393
11417
|
}
|
|
11394
11418
|
}
|
|
11395
11419
|
/**
|
|
11396
|
-
*
|
|
11420
|
+
* Show the MBTiles import/export modal for a region.
|
|
11397
11421
|
*/
|
|
11398
11422
|
async handleImportExport(regionId, _regionData) {
|
|
11399
11423
|
try {
|
|
@@ -11401,44 +11425,26 @@ class PanelRenderer extends BaseComponent {
|
|
|
11401
11425
|
const region = regions.find((r) => r.id === regionId);
|
|
11402
11426
|
if (!region)
|
|
11403
11427
|
return;
|
|
11404
|
-
const
|
|
11428
|
+
const mbtilesModal = new MBTilesModal({
|
|
11405
11429
|
region,
|
|
11406
|
-
onClose: () =>
|
|
11407
|
-
this.modalManager.close();
|
|
11408
|
-
},
|
|
11430
|
+
onClose: () => this.modalManager.close(),
|
|
11409
11431
|
onExport: result => {
|
|
11410
|
-
panelLogger.debug('
|
|
11411
|
-
// Handle export result - could show success message
|
|
11432
|
+
panelLogger.debug('MBTiles export completed:', result);
|
|
11412
11433
|
this.offlineManager.downloadExportedRegion(result);
|
|
11413
11434
|
},
|
|
11414
11435
|
onImport: result => {
|
|
11415
|
-
panelLogger.debug('
|
|
11416
|
-
|
|
11417
|
-
|
|
11418
|
-
},
|
|
11419
|
-
exportRegion: async (regionId, format, options) => {
|
|
11420
|
-
// Delegate to offline manager's export functionality
|
|
11421
|
-
switch (format) {
|
|
11422
|
-
case 'json':
|
|
11423
|
-
return await this.offlineManager.exportRegionAsJSON(regionId, options);
|
|
11424
|
-
case 'pmtiles':
|
|
11425
|
-
return await this.offlineManager.exportRegionAsPMTiles(regionId, options);
|
|
11426
|
-
case 'mbtiles':
|
|
11427
|
-
return await this.offlineManager.exportRegionAsMBTiles(regionId, options);
|
|
11428
|
-
default:
|
|
11429
|
-
throw new Error(`Unsupported export format: ${format}`);
|
|
11430
|
-
}
|
|
11431
|
-
},
|
|
11432
|
-
importRegion: async (data) => {
|
|
11433
|
-
// Delegate to offline manager's import functionality
|
|
11434
|
-
return await this.offlineManager.importRegion(data);
|
|
11436
|
+
panelLogger.debug('MBTiles import completed:', result);
|
|
11437
|
+
if (result.success)
|
|
11438
|
+
this.refresh();
|
|
11435
11439
|
},
|
|
11440
|
+
exportRegion: (id, options) => this.offlineManager.exportRegionAsMBTiles(id, options),
|
|
11441
|
+
importRegion: data => this.offlineManager.importRegion(data),
|
|
11436
11442
|
});
|
|
11437
|
-
const modal =
|
|
11443
|
+
const modal = mbtilesModal.show();
|
|
11438
11444
|
this.modalManager.show(modal);
|
|
11439
11445
|
}
|
|
11440
11446
|
catch (error) {
|
|
11441
|
-
panelLogger.error('Error showing
|
|
11447
|
+
panelLogger.error('Error showing MBTiles modal:', error);
|
|
11442
11448
|
}
|
|
11443
11449
|
}
|
|
11444
11450
|
/**
|
|
@@ -11837,6 +11843,9 @@ class PanelRenderer extends BaseComponent {
|
|
|
11837
11843
|
delete patchedStyle.imports;
|
|
11838
11844
|
panelLogger.debug('Stripped imports from offline style (already flattened)');
|
|
11839
11845
|
}
|
|
11846
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
11847
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
11848
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
11840
11849
|
// Enforce maxzoom for all tile sources to prevent requesting non-existent tiles
|
|
11841
11850
|
// Find the maximum zoom level from all regions using this style
|
|
11842
11851
|
let maxZoom = 14; // Default fallback
|
|
@@ -12578,9 +12587,7 @@ class RegionFormModal {
|
|
|
12578
12587
|
detectProviderFromUrl() {
|
|
12579
12588
|
const styleUrl = this.styleUrlInput?.value || '';
|
|
12580
12589
|
// Simple detection logic
|
|
12581
|
-
if (styleUrl.startsWith('mapbox://') ||
|
|
12582
|
-
styleUrl.includes('mapbox.com') ||
|
|
12583
|
-
styleUrl.includes('api.mapbox.com')) {
|
|
12590
|
+
if (styleUrl.startsWith('mapbox://') || isMapboxHost(styleUrl)) {
|
|
12584
12591
|
if (this.providerSelect)
|
|
12585
12592
|
this.providerSelect.value = 'mapbox';
|
|
12586
12593
|
this.toggleAccessTokenVisibility(true);
|
|
@@ -13344,39 +13351,39 @@ class OfflineManagerControl {
|
|
|
13344
13351
|
}
|
|
13345
13352
|
// Development proxy for CORS issues (when running on localhost)
|
|
13346
13353
|
if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') {
|
|
13347
|
-
|
|
13348
|
-
|
|
13349
|
-
|
|
13350
|
-
if (isTileRequest && url.includes('tiles-a.basemaps.cartocdn.com')) {
|
|
13351
|
-
const proxyUrl = url.replace('https://tiles-a.basemaps.cartocdn.com', '/tiles/carto-a');
|
|
13352
|
-
return originalFetch(proxyUrl, init);
|
|
13353
|
-
}
|
|
13354
|
-
if (isTileRequest && url.includes('tiles-b.basemaps.cartocdn.com')) {
|
|
13355
|
-
const proxyUrl = url.replace('https://tiles-b.basemaps.cartocdn.com', '/tiles/carto-b');
|
|
13356
|
-
return originalFetch(proxyUrl, init);
|
|
13354
|
+
let parsed = null;
|
|
13355
|
+
try {
|
|
13356
|
+
parsed = new URL(url, location.origin);
|
|
13357
13357
|
}
|
|
13358
|
-
|
|
13359
|
-
|
|
13360
|
-
return originalFetch(proxyUrl, init);
|
|
13358
|
+
catch {
|
|
13359
|
+
parsed = null;
|
|
13361
13360
|
}
|
|
13362
|
-
|
|
13363
|
-
|
|
13364
|
-
|
|
13361
|
+
const hostname = parsed?.hostname ?? '';
|
|
13362
|
+
const pathAndQuery = parsed ? parsed.pathname + parsed.search : '';
|
|
13363
|
+
// Proxy Carto tile requests (tiles and TileJSON)
|
|
13364
|
+
const isTileRequest = /\/\d+\/\d+\/\d+\.(pbf|mvt|png|jpg|jpeg|webp)/.test(parsed?.pathname ?? '');
|
|
13365
|
+
const isTileJsonRequest = (parsed?.pathname.endsWith('.json') ?? false) &&
|
|
13366
|
+
(hostname === 'basemaps.cartocdn.com' || hostname.endsWith('.basemaps.cartocdn.com'));
|
|
13367
|
+
const cartoSubdomainProxy = {
|
|
13368
|
+
'tiles-a.basemaps.cartocdn.com': '/tiles/carto-a',
|
|
13369
|
+
'tiles-b.basemaps.cartocdn.com': '/tiles/carto-b',
|
|
13370
|
+
'tiles-c.basemaps.cartocdn.com': '/tiles/carto-c',
|
|
13371
|
+
'tiles-d.basemaps.cartocdn.com': '/tiles/carto-d',
|
|
13372
|
+
};
|
|
13373
|
+
if (isTileRequest && cartoSubdomainProxy[hostname]) {
|
|
13374
|
+
return originalFetch(cartoSubdomainProxy[hostname] + pathAndQuery, init);
|
|
13365
13375
|
}
|
|
13366
13376
|
// Proxy TileJSON requests from tiles.basemaps.cartocdn.com
|
|
13367
|
-
if (isTileJsonRequest &&
|
|
13368
|
-
|
|
13369
|
-
return originalFetch(proxyUrl, init);
|
|
13377
|
+
if (isTileJsonRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13378
|
+
return originalFetch('/carto-api' + pathAndQuery, init);
|
|
13370
13379
|
}
|
|
13371
13380
|
// Fallback for old format (tiles without subdomain)
|
|
13372
|
-
if (isTileRequest &&
|
|
13373
|
-
|
|
13374
|
-
return originalFetch(proxyUrl, init);
|
|
13381
|
+
if (isTileRequest && hostname === 'tiles.basemaps.cartocdn.com') {
|
|
13382
|
+
return originalFetch('/tiles/carto-a' + pathAndQuery, init);
|
|
13375
13383
|
}
|
|
13376
13384
|
// Proxy OpenStreetMap tile requests
|
|
13377
|
-
if (
|
|
13378
|
-
|
|
13379
|
-
return originalFetch(proxyUrl, init);
|
|
13385
|
+
if (hostname === 'tile.openstreetmap.org') {
|
|
13386
|
+
return originalFetch('/tiles/osm' + pathAndQuery, init);
|
|
13380
13387
|
}
|
|
13381
13388
|
}
|
|
13382
13389
|
return originalFetch(input, init);
|
|
@@ -13809,6 +13816,9 @@ class OfflineManagerControl {
|
|
|
13809
13816
|
if (patchedStyle.imports) {
|
|
13810
13817
|
delete patchedStyle.imports;
|
|
13811
13818
|
}
|
|
13819
|
+
// Scrub indoor-only expressions for pre-0.8.1 stored styles that were
|
|
13820
|
+
// downloaded before resolveImports learned to rewrite them.
|
|
13821
|
+
sanitizeIndoorExpressions(patchedStyle);
|
|
13812
13822
|
// If using Service Worker (Mapbox GL JS), convert idb:// to /__offline__/ URLs
|
|
13813
13823
|
if (this.useServiceWorker) {
|
|
13814
13824
|
if (this.swReadyPromise) {
|
|
@@ -13953,5 +13963,5 @@ class OfflineManagerControl {
|
|
|
13953
13963
|
}
|
|
13954
13964
|
}
|
|
13955
13965
|
|
|
13956
|
-
export { AnalyticsService, CONTENT_TYPES, CategorizedError, CleanupService, DB_NAME, DB_VERSION, DOWNLOAD_DEFAULTS, ERROR_MESSAGES, ErrorType, FontService, GLYPH_CONFIG, GZIP_MAGIC_BYTES, GlyphService, ImportExportService, LogLevel, MAPBOX_API, MAPBOX_CACHE_TTL, MAPBOX_CLASSIC_STYLES, MAP_PROVIDERS, MaintenanceService, ModelService, OfflineManagerControl, OfflineMapDBVersionError, OfflineMapManager, RESOURCE_TYPES, RegionService, ResourceService, STORAGE_CONFIG, STORE_NAMES, STYLE_CONFIG, SUCCESS_MESSAGES, ScopedLogger, SpriteService, TILE_CONFIG, TileService, URL_SCHEMES, VALIDATION_PATTERNS, applyProxy, categorizeError, cleanupCompressedTiles, cleanupExpiredTiles, cleanupOldFonts, cleanupOldGlyphs, cleanupOldModels, cleanupOldSprites, cleanupOldStyles, cleanupOldTiles, cleanupService, clearAllCaches, configureLogger, configureProxy, convertStyleForServiceWorker, countCompressedTiles, createProgressTracker, createTileKey, dbPromise, OfflineMapManager as default, deleteStyleById, deleteStyles, deriveTileExtension, detectCssPrefix, detectStyleProvider, downloadFonts, downloadGlyphs, downloadModels, downloadSprites, downloadStyleWithProvider, downloadStyles, downloadTiles, escapeHtml$1 as escapeHtml, extractAccessToken, extractAllFontNames, extractFontNamesFromTextField, fetchResourceWithRetry, fetchWithRetry, fontService, formatBytes, formatDate, generateGlyphUrlsFromStyle, getExpiredResourceCount, getFontAnalytics, getFontStats, getGlyphAnalytics, getGlyphStats, getIcon, getModel, getModelStats, getRegionAnalytics, getSpriteAnalytics, getSpriteStats, getStyleStats, getTileAnalytics, getTileStats, getUserErrorMessage, glyphService, hasImports, i18n, icons, idbFetchHandler, isMapboxProtocol, isStyleDownloaded, loadAllStoredRegions, loadGlyphs, loadStyleById, loadStyles, logger, modelKeyBelongsToStyle, modelService, normalizeSpriteProperty, normalizeStyleUrl, optimizeStorage, parseCacheExpiry, parseTileKey, patchStyleForOffline, performCleanup, processBatch, processStyleSources, registerOfflineServiceWorker, resetOfflineMapDB, resolveImports, resolveMapboxUrl, resourceKeyBelongsToStyle, rewriteMapboxCdnTileUrl, safeExecute, setupAutoCleanup, spriteService, stopAutoCleanup, t, tileService, unregisterOfflineServiceWorker, validateBounds, validateRegionOptions, validateResource, validateStyleForProvider, validateZoomLevels, verifyAndRepairFonts, verifyAndRepairGlyphs, verifyAndRepairModels, verifyAndRepairSprites };
|
|
13966
|
+
export { AnalyticsService, CONTENT_TYPES, CategorizedError, CleanupService, DB_NAME, DB_VERSION, DOWNLOAD_DEFAULTS, ERROR_MESSAGES, ErrorType, FontService, GLYPH_BLOCK_SIZE, GLYPH_CONFIG, GZIP_MAGIC_BYTES, GlyphService, ImportExportService, LogLevel, MAPBOX_API, MAPBOX_CACHE_TTL, MAPBOX_CLASSIC_STYLES, MAP_PROVIDERS, MAX_GLYPH_CODEPOINT, MaintenanceService, ModelService, OfflineManagerControl, OfflineMapDBVersionError, OfflineMapManager, RESOURCE_TYPES, RegionService, ResourceService, STORAGE_CONFIG, STORE_NAMES, STYLE_CONFIG, SUCCESS_MESSAGES, ScopedLogger, SpriteService, TILE_CONFIG, TileService, URL_SCHEMES, VALIDATION_PATTERNS, applyProxy, categorizeError, cleanupCompressedTiles, cleanupExpiredTiles, cleanupOldFonts, cleanupOldGlyphs, cleanupOldModels, cleanupOldSprites, cleanupOldStyles, cleanupOldTiles, cleanupService, clearAllCaches, configureLogger, configureProxy, configureSqlJs, convertStyleForServiceWorker, countCompressedTiles, createProgressTracker, createTileKey, dbPromise, OfflineMapManager as default, deleteStyleById, deleteStyles, deriveTileExtension, detectCssPrefix, detectStyleProvider, downloadFonts, downloadGlyphs, downloadModels, downloadSprites, downloadStyleWithProvider, downloadStyles, downloadTiles, escapeHtml$1 as escapeHtml, extractAccessToken, extractAllFontNames, extractFontNamesFromTextField, extractTileExtensionFromUrl, fetchResourceWithRetry, fetchWithRetry, fontService, formatBytes, formatDate, generateGlyphUrlsFromStyle, getExpiredResourceCount, getFontAnalytics, getFontStats, getGlyphAnalytics, getGlyphStats, getIcon, getModel, getModelStats, getRegionAnalytics, getSpriteAnalytics, getSpriteStats, getSqlJs, getStyleStats, getTileAnalytics, getTileStats, getUrlHostname, getUserErrorMessage, glyphService, hasImports, hostMatches, i18n, icons, idbFetchHandler, isMapboxHost, isMapboxProtocol, isStyleDownloaded, loadAllStoredRegions, loadGlyphs, loadStyleById, loadStyles, logger, modelKeyBelongsToStyle, modelService, normalizeSpriteProperty, normalizeStyleUrl, optimizeStorage, parseCacheExpiry, parseTileKey, patchStyleForOffline, performCleanup, processBatch, processStyleSources, registerOfflineServiceWorker, resetOfflineMapDB, resolveImports, resolveMapboxUrl, resourceKeyBelongsToStyle, rewriteMapboxCdnTileUrl, safeExecute, sanitizeIndoorExpressions, setupAutoCleanup, spriteService, stopAutoCleanup, t, tileService, unregisterOfflineServiceWorker, validateBounds, validateRegionOptions, validateResource, validateStyleForProvider, validateZoomLevels, verifyAndRepairFonts, verifyAndRepairGlyphs, verifyAndRepairModels, verifyAndRepairSprites };
|
|
13957
13967
|
//# sourceMappingURL=index.esm.js.map
|