map-gl-offline 0.6.0 → 0.7.0
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 +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +368 -24
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +375 -23
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +375 -23
- package/dist/index.umd.js.map +1 -1
- package/dist/managers/offlineMapManager/resourceManagement.d.ts +4 -0
- package/dist/services/modelService.d.ts +57 -0
- package/dist/services/resourceService.d.ts +11 -1
- package/dist/types/database.d.ts +9 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/model.d.ts +62 -0
- package/dist/types/region.d.ts +11 -1
- package/dist/types/style.d.ts +11 -2
- package/dist/ui/managers/downloadManager.d.ts +1 -1
- package/dist/utils/constants.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -160,6 +160,8 @@ try {
|
|
|
160
160
|
}
|
|
161
161
|
```
|
|
162
162
|
|
|
163
|
+
> **Upgrading from 0.5.x?** Read the [0.6.0 migration guide](https://map-gl-offline.netlify.app/docs/migration-0.6) — covers the rename of `ResourceService.getXxxStatistics` → `getXxxStats`, the `addRegion` vs `downloadRegion` split, and the `expiry` timestamp fix.
|
|
164
|
+
|
|
163
165
|
## API at a glance
|
|
164
166
|
|
|
165
167
|
- **Regions** — `downloadRegion`, `loadRegion`, `addRegion`, `getStoredRegion`, `listStoredRegions`, `listRegions`, `deleteRegion`
|
package/dist/index.d.ts
CHANGED
|
@@ -58,6 +58,7 @@ export * from './services/tileService';
|
|
|
58
58
|
export * from './services/fontService';
|
|
59
59
|
export * from './services/glyphService';
|
|
60
60
|
export * from './services/spriteService';
|
|
61
|
+
export * from './services/modelService';
|
|
61
62
|
export * from './services/cleanupService';
|
|
62
63
|
export * from './services/styleService';
|
|
63
64
|
export * from './services/regionService';
|
package/dist/index.esm.js
CHANGED
|
@@ -12,7 +12,7 @@ import { polygon, convertArea, featureCollection } from '@turf/helpers';
|
|
|
12
12
|
*/
|
|
13
13
|
// IndexedDB Configuration
|
|
14
14
|
const DB_NAME = 'offline-map-db';
|
|
15
|
-
const DB_VERSION =
|
|
15
|
+
const DB_VERSION = 4;
|
|
16
16
|
// Store Names (regions are stored inside styles.regions[], not as a separate store)
|
|
17
17
|
const STORE_NAMES = {
|
|
18
18
|
TILES: 'tiles',
|
|
@@ -20,6 +20,7 @@ const STORE_NAMES = {
|
|
|
20
20
|
SPRITES: 'sprites',
|
|
21
21
|
GLYPHS: 'glyphs',
|
|
22
22
|
FONTS: 'fonts',
|
|
23
|
+
MODELS: 'models',
|
|
23
24
|
};
|
|
24
25
|
// Download Configuration
|
|
25
26
|
const DOWNLOAD_DEFAULTS = {
|
|
@@ -227,7 +228,7 @@ async function resetOfflineMapDB() {
|
|
|
227
228
|
* Called during initial database creation or when stores are missing.
|
|
228
229
|
*/
|
|
229
230
|
function createStores(db) {
|
|
230
|
-
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts'];
|
|
231
|
+
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts', 'models'];
|
|
231
232
|
for (const storeName of stores) {
|
|
232
233
|
if (!db.objectStoreNames.contains(storeName)) {
|
|
233
234
|
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
@@ -309,6 +310,7 @@ function migrateRegionsToStyles(transaction) {
|
|
|
309
310
|
* - sprites: Sprite images and JSON
|
|
310
311
|
* - glyphs: Font glyph data
|
|
311
312
|
* - fonts: Font files
|
|
313
|
+
* - models: 3D model files (.glb) for Mapbox Standard tree/turbine layers
|
|
312
314
|
* - regions: (deprecated) Legacy region storage, migrated to styles.regions[]
|
|
313
315
|
*
|
|
314
316
|
* @example
|
|
@@ -328,6 +330,9 @@ async function openOfflineMapDB() {
|
|
|
328
330
|
if (oldVersion > 0 && oldVersion < 3) {
|
|
329
331
|
migrateRegionsToStyles(transaction);
|
|
330
332
|
}
|
|
333
|
+
// Migration: v3 -> v4
|
|
334
|
+
// Adds the `models` store for Mapbox Standard 3D model assets.
|
|
335
|
+
// No data migration needed — createStores above handles it.
|
|
331
336
|
},
|
|
332
337
|
});
|
|
333
338
|
}
|
|
@@ -1184,6 +1189,7 @@ const idbLogger = logger.scope('IDBFetch');
|
|
|
1184
1189
|
// idb://{downloadId}/tile/{sourceKey}/{url}
|
|
1185
1190
|
// idb://{downloadId}/glyph/{fontstack}/{range}.pbf
|
|
1186
1191
|
// idb://{downloadId}/sprite/{spriteName}
|
|
1192
|
+
// idb://{styleId}/model/{modelName}
|
|
1187
1193
|
// idb://{downloadId}/tilesjson/{url}
|
|
1188
1194
|
// Cache for region ID to style mapping to avoid repeated DB queries
|
|
1189
1195
|
const regionToStyleCache = new Map();
|
|
@@ -1555,6 +1561,33 @@ async function idbFetchHandler(url, init) {
|
|
|
1555
1561
|
}
|
|
1556
1562
|
break;
|
|
1557
1563
|
}
|
|
1564
|
+
case 'model': {
|
|
1565
|
+
// Model URLs are rewritten by patchStyleForOffline to:
|
|
1566
|
+
// idb://{styleId}/model/{modelName}
|
|
1567
|
+
// 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
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1572
|
+
const actualStyleId = styleEntry?.key || downloadId;
|
|
1573
|
+
const candidates = Array.from(new Set([
|
|
1574
|
+
`${actualStyleId}::model::${decodedResourcePath}`,
|
|
1575
|
+
`${downloadId}::model::${decodedResourcePath}`,
|
|
1576
|
+
]));
|
|
1577
|
+
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1578
|
+
for (const candidateKey of candidates) {
|
|
1579
|
+
const resource = await db.get('models', candidateKey);
|
|
1580
|
+
if (resource?.data) {
|
|
1581
|
+
idbLogger.debug(`Found model using key: ${candidateKey}`);
|
|
1582
|
+
return new Response(resource.data, {
|
|
1583
|
+
status: 200,
|
|
1584
|
+
headers: { 'Content-Type': resource.contentType || 'model/gltf-binary' },
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
idbLogger.warn(`Model not found, tried keys: ${candidates.join(', ')}`);
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1558
1591
|
case 'tilesjson': {
|
|
1559
1592
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1560
1593
|
// First try direct lookup (for style-level downloads)
|
|
@@ -2010,14 +2043,29 @@ function patchStyleForOffline(style, downloadId, maxZoom, tileExtension, styleId
|
|
|
2010
2043
|
});
|
|
2011
2044
|
}
|
|
2012
2045
|
}
|
|
2013
|
-
// Patch top-level models (Mapbox Standard 3D
|
|
2046
|
+
// Patch top-level models (Mapbox Standard 3D trees / wind turbines).
|
|
2047
|
+
// Two shapes exist in the wild:
|
|
2048
|
+
// - Mapbox Standard: `{ "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb" }` (string values)
|
|
2049
|
+
// - Older/generic: `{ "name": { "uri": "mapbox://..." } }` (object values)
|
|
2050
|
+
// Models are keyed on the style ID (like sprites) so they can be shared
|
|
2051
|
+
// across regions.
|
|
2014
2052
|
if (style.models) {
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2053
|
+
const modelBaseId = styleId || downloadId;
|
|
2054
|
+
const models = style.models;
|
|
2055
|
+
let patchedCount = 0;
|
|
2056
|
+
for (const [modelId, value] of Object.entries(models)) {
|
|
2057
|
+
if (typeof value === 'string') {
|
|
2058
|
+
models[modelId] = `idb://${modelBaseId}/model/${modelId}`;
|
|
2059
|
+
patchedCount++;
|
|
2060
|
+
}
|
|
2061
|
+
else if (value && typeof value === 'object' && 'uri' in value && value.uri) {
|
|
2062
|
+
value.uri = `idb://${modelBaseId}/model/${modelId}`;
|
|
2063
|
+
patchedCount++;
|
|
2018
2064
|
}
|
|
2019
2065
|
}
|
|
2020
|
-
|
|
2066
|
+
if (patchedCount > 0) {
|
|
2067
|
+
styleLogger.debug(`Patched ${patchedCount} model URIs (styleId: ${modelBaseId})`);
|
|
2068
|
+
}
|
|
2021
2069
|
}
|
|
2022
2070
|
styleLogger.debug(`Final patched style:`, style);
|
|
2023
2071
|
return style;
|
|
@@ -2585,12 +2633,22 @@ function convertStyleForServiceWorker(style) {
|
|
|
2585
2633
|
});
|
|
2586
2634
|
}
|
|
2587
2635
|
}
|
|
2588
|
-
// Convert models
|
|
2636
|
+
// Convert models. Two shapes in the wild:
|
|
2637
|
+
// - Mapbox Standard: `{ name: "idb://..." }` (string value)
|
|
2638
|
+
// - Older/generic: `{ name: { uri: "idb://..." } }` (object value)
|
|
2589
2639
|
if (converted.models) {
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2640
|
+
const models = converted.models;
|
|
2641
|
+
for (const modelKey of Object.keys(models)) {
|
|
2642
|
+
const value = models[modelKey];
|
|
2643
|
+
if (typeof value === 'string') {
|
|
2644
|
+
if (value.startsWith('idb://')) {
|
|
2645
|
+
models[modelKey] = replace(value);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
else if (value && typeof value === 'object') {
|
|
2649
|
+
if (typeof value.uri === 'string' && value.uri.startsWith('idb://')) {
|
|
2650
|
+
value.uri = replace(value.uri);
|
|
2651
|
+
}
|
|
2594
2652
|
}
|
|
2595
2653
|
}
|
|
2596
2654
|
}
|
|
@@ -4911,7 +4969,15 @@ class RegionService {
|
|
|
4911
4969
|
deletedSprites++;
|
|
4912
4970
|
}
|
|
4913
4971
|
}
|
|
4914
|
-
|
|
4972
|
+
let deletedModels = 0;
|
|
4973
|
+
const modelTx = db.transaction('models', 'readwrite');
|
|
4974
|
+
for await (const cursor of modelTx.store) {
|
|
4975
|
+
if (resourceKeyBelongsToStyle(cursor.value.key, styleId)) {
|
|
4976
|
+
await cursor.delete();
|
|
4977
|
+
deletedModels++;
|
|
4978
|
+
}
|
|
4979
|
+
}
|
|
4980
|
+
regionLogger$1.info(`Deleted style resources: ${deletedFonts} fonts, ${deletedGlyphs} glyphs, ${deletedSprites} sprites, ${deletedModels} models`);
|
|
4915
4981
|
}
|
|
4916
4982
|
/**
|
|
4917
4983
|
* Delete all tiles for a style
|
|
@@ -5019,7 +5085,7 @@ class RegionService {
|
|
|
5019
5085
|
if (!region.styleUrl) {
|
|
5020
5086
|
throw new Error('Region must have a styleUrl');
|
|
5021
5087
|
}
|
|
5022
|
-
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, glyphRanges, tileOptions, } = options;
|
|
5088
|
+
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, skipModels = false, glyphRanges, tileOptions, } = options;
|
|
5023
5089
|
const emit = (phase, completed, total, message) => {
|
|
5024
5090
|
if (!onProgress)
|
|
5025
5091
|
return;
|
|
@@ -5074,8 +5140,13 @@ class RegionService {
|
|
|
5074
5140
|
const spriteSources = normalizeSpriteProperty(originalSpriteUrl);
|
|
5075
5141
|
if (spriteSources.length > 0) {
|
|
5076
5142
|
const { downloadSprites } = await Promise.resolve().then(function () { return spriteService$1; });
|
|
5077
|
-
|
|
5078
|
-
|
|
5143
|
+
// Standard four sprite variants. For Mapbox Standard, an `iconset.pbf`
|
|
5144
|
+
// sibling is also served under the same /styles/v1/.../<hash>/ path
|
|
5145
|
+
// — we detect that case per-source below and append it to the list.
|
|
5146
|
+
const baseSuffixes = ['.json', '.png', '@2x.json', '@2x.png'];
|
|
5147
|
+
// Estimate total files assuming iconset is always included (the actual
|
|
5148
|
+
// number may be smaller; the emit helper clamps progress to total).
|
|
5149
|
+
const totalFiles = spriteSources.length * (baseSuffixes.length + 1);
|
|
5079
5150
|
let completed = 0;
|
|
5080
5151
|
emit('sprites', 0, totalFiles, 'Downloading sprites');
|
|
5081
5152
|
for (const source of spriteSources) {
|
|
@@ -5084,9 +5155,27 @@ class RegionService {
|
|
|
5084
5155
|
spriteBase = resolveMapboxUrl(spriteBase, effectiveAccessToken);
|
|
5085
5156
|
}
|
|
5086
5157
|
const qIndex = spriteBase.indexOf('?');
|
|
5087
|
-
const
|
|
5088
|
-
|
|
5089
|
-
|
|
5158
|
+
const suffixes = [...baseSuffixes];
|
|
5159
|
+
// Mapbox Standard serves an iconset.pbf alongside the sprite under
|
|
5160
|
+
// /styles/v1/{owner}/{style}/{hash}/sprite → the sibling file is
|
|
5161
|
+
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5162
|
+
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5163
|
+
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5164
|
+
const isMapboxStandardSprite = /api\.mapbox\.com\/styles\/v1\/.+\/sprite$/.test(pathWithoutQuery);
|
|
5165
|
+
if (isMapboxStandardSprite) {
|
|
5166
|
+
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5167
|
+
suffixes.push('__ICONSET__');
|
|
5168
|
+
}
|
|
5169
|
+
const spriteUrls = suffixes.map(suffix => {
|
|
5170
|
+
if (suffix === '__ICONSET__') {
|
|
5171
|
+
// Replace trailing `sprite` with `iconset.pbf`, preserving query.
|
|
5172
|
+
const base = pathWithoutQuery.replace(/sprite$/, 'iconset.pbf');
|
|
5173
|
+
return qIndex !== -1 ? base + spriteBase.slice(qIndex) : base;
|
|
5174
|
+
}
|
|
5175
|
+
return qIndex !== -1
|
|
5176
|
+
? spriteBase.slice(0, qIndex) + suffix + spriteBase.slice(qIndex)
|
|
5177
|
+
: spriteBase + suffix;
|
|
5178
|
+
});
|
|
5090
5179
|
try {
|
|
5091
5180
|
const result = await downloadSprites(spriteUrls, styleId, {
|
|
5092
5181
|
enableValidation: true,
|
|
@@ -5132,7 +5221,42 @@ class RegionService {
|
|
|
5132
5221
|
}
|
|
5133
5222
|
}
|
|
5134
5223
|
}
|
|
5135
|
-
// 4.
|
|
5224
|
+
// 4. Models — Mapbox Standard's `style.models` references 3D tree /
|
|
5225
|
+
// turbine .glb assets. Two value shapes exist in the wild
|
|
5226
|
+
// (plain string or `{ uri }`) — we accept both.
|
|
5227
|
+
let modelResult;
|
|
5228
|
+
if (!skipModels && storedStyle.models) {
|
|
5229
|
+
const rawModels = storedStyle.models;
|
|
5230
|
+
const resolved = {};
|
|
5231
|
+
for (const [name, value] of Object.entries(rawModels)) {
|
|
5232
|
+
const uri = typeof value === 'string' ? value : value?.uri;
|
|
5233
|
+
if (!uri)
|
|
5234
|
+
continue;
|
|
5235
|
+
if (uri.startsWith('idb://'))
|
|
5236
|
+
continue; // already patched
|
|
5237
|
+
const httpUrl = isMapboxProtocol(uri) && effectiveAccessToken
|
|
5238
|
+
? resolveMapboxUrl(uri, effectiveAccessToken)
|
|
5239
|
+
: uri;
|
|
5240
|
+
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
|
|
5241
|
+
resolved[name] = httpUrl;
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
if (Object.keys(resolved).length > 0) {
|
|
5245
|
+
const { downloadModels } = await Promise.resolve().then(function () { return modelService$1; });
|
|
5246
|
+
emit('models', 0, Object.keys(resolved).length, 'Downloading 3D models');
|
|
5247
|
+
try {
|
|
5248
|
+
modelResult = await downloadModels(resolved, styleId, {
|
|
5249
|
+
onProgress: (progress) => {
|
|
5250
|
+
emit('models', progress.completed, progress.total, 'Downloading 3D models');
|
|
5251
|
+
},
|
|
5252
|
+
});
|
|
5253
|
+
}
|
|
5254
|
+
catch (error) {
|
|
5255
|
+
regionLogger$1.warn('Model download failed (non-fatal):', error);
|
|
5256
|
+
}
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
// 5. Tiles — use the stored (source-embedded) style, which still has HTTP tile URLs
|
|
5136
5260
|
const { downloadTiles } = await Promise.resolve().then(function () { return tileService$1; });
|
|
5137
5261
|
const regionForTiles = { ...region, styleId };
|
|
5138
5262
|
emit('tiles', 0, 100, 'Downloading tiles');
|
|
@@ -5145,7 +5269,7 @@ class RegionService {
|
|
|
5145
5269
|
tileOptions?.onProgress?.(progress);
|
|
5146
5270
|
},
|
|
5147
5271
|
});
|
|
5148
|
-
//
|
|
5272
|
+
// 6. Metadata — must run last, since addRegion patches style URLs to idb://.
|
|
5149
5273
|
// Do NOT auto-fill tileExtension from tileResult: that's only the first
|
|
5150
5274
|
// source's extension, and addRegion feeds it to patchStyleForOffline which
|
|
5151
5275
|
// would override ALL sources — breaking mixed raster+vector styles. The
|
|
@@ -5161,6 +5285,7 @@ class RegionService {
|
|
|
5161
5285
|
styleResult,
|
|
5162
5286
|
spriteResults,
|
|
5163
5287
|
glyphResult,
|
|
5288
|
+
modelResult,
|
|
5164
5289
|
tileResult,
|
|
5165
5290
|
};
|
|
5166
5291
|
}
|
|
@@ -6299,10 +6424,15 @@ class TileService {
|
|
|
6299
6424
|
tilesLength: config.tiles ? config.tiles.length : 0,
|
|
6300
6425
|
url: config.url,
|
|
6301
6426
|
});
|
|
6302
|
-
// Handle tile-based sources (vector, raster, raster-dem, batched-model
|
|
6427
|
+
// Handle tile-based sources (vector, raster, raster-dem, batched-model,
|
|
6428
|
+
// raster-array). `raster-array` is used by Mapbox Standard for layers
|
|
6429
|
+
// like `mapbox-landmarks` (mapbox.mapbox-landmark-icons-v1) — the tiles
|
|
6430
|
+
// are fetched from the same /v4/ endpoint as other tilesets, so the
|
|
6431
|
+
// TileJSON resolution path below handles them uniformly.
|
|
6303
6432
|
if (config.type === 'vector' ||
|
|
6304
6433
|
config.type === 'raster' ||
|
|
6305
6434
|
config.type === 'raster-dem' ||
|
|
6435
|
+
config.type === 'raster-array' ||
|
|
6306
6436
|
config.type === 'batched-model') {
|
|
6307
6437
|
// Handle direct tile URLs in the source config
|
|
6308
6438
|
if (config.tiles && Array.isArray(config.tiles) && config.tiles.length > 0) {
|
|
@@ -7035,6 +7165,201 @@ var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
|
7035
7165
|
verifyAndRepairGlyphs: verifyAndRepairGlyphs
|
|
7036
7166
|
});
|
|
7037
7167
|
|
|
7168
|
+
const modelLogger = logger.scope('ModelService');
|
|
7169
|
+
/**
|
|
7170
|
+
* Build the storage key for a model. Kept consistent with sprite/glyph
|
|
7171
|
+
* conventions: `{styleId}::model::{modelName}`.
|
|
7172
|
+
*/
|
|
7173
|
+
function modelKey(styleId, modelName) {
|
|
7174
|
+
return `${styleId}::model::${modelName}`;
|
|
7175
|
+
}
|
|
7176
|
+
/** True when the given key belongs to the given styleId's model store prefix. */
|
|
7177
|
+
function modelKeyBelongsToStyle(key, styleId) {
|
|
7178
|
+
return key.startsWith(`${styleId}::model::`);
|
|
7179
|
+
}
|
|
7180
|
+
/**
|
|
7181
|
+
* Service for downloading, storing, and serving Mapbox 3D model (.glb) files.
|
|
7182
|
+
*
|
|
7183
|
+
* Mapbox Standard declares 32 models at the top of `style.models`:
|
|
7184
|
+
*
|
|
7185
|
+
* ```json
|
|
7186
|
+
* {
|
|
7187
|
+
* "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb",
|
|
7188
|
+
* ...
|
|
7189
|
+
* }
|
|
7190
|
+
* ```
|
|
7191
|
+
*
|
|
7192
|
+
* `model` layers (e.g. `trees`, `wind-turbine-towers`) pick one by name at
|
|
7193
|
+
* render time. For offline use each referenced URL is fetched and stored
|
|
7194
|
+
* here, and `patchStyleForOffline` rewrites the dictionary entries to
|
|
7195
|
+
* `idb://{styleId}/model/{name}` URLs.
|
|
7196
|
+
*/
|
|
7197
|
+
class ModelService {
|
|
7198
|
+
db = dbPromise;
|
|
7199
|
+
/**
|
|
7200
|
+
* Download the set of models referenced by `style.models` for one style.
|
|
7201
|
+
*
|
|
7202
|
+
* @param models `{ modelName: resolvedHttpUrl }` — URLs must already be
|
|
7203
|
+
* resolved (mapbox:// URLs should be resolved by the caller).
|
|
7204
|
+
* @param styleId The owning style's key.
|
|
7205
|
+
*/
|
|
7206
|
+
async downloadModels(models, styleId, options = {}) {
|
|
7207
|
+
const db = await this.db;
|
|
7208
|
+
const { onProgress, batchSize = 4, maxRetries = 3, skipExisting = true, timeoutMs = 30000, } = options;
|
|
7209
|
+
const entries = Object.entries(models);
|
|
7210
|
+
const progressTracker = createProgressTracker(entries.length);
|
|
7211
|
+
const result = {
|
|
7212
|
+
totalModels: entries.length,
|
|
7213
|
+
downloadedModels: 0,
|
|
7214
|
+
skippedModels: 0,
|
|
7215
|
+
failedModels: 0,
|
|
7216
|
+
totalSize: 0,
|
|
7217
|
+
errors: [],
|
|
7218
|
+
};
|
|
7219
|
+
const emit = () => onProgress?.(progressTracker.getProgress());
|
|
7220
|
+
emit();
|
|
7221
|
+
if (entries.length === 0)
|
|
7222
|
+
return result;
|
|
7223
|
+
// Pre-compute existing keys for skipExisting
|
|
7224
|
+
const existingKeys = new Set();
|
|
7225
|
+
if (skipExisting) {
|
|
7226
|
+
const tx = db.transaction('models', 'readonly');
|
|
7227
|
+
for await (const cursor of tx.store) {
|
|
7228
|
+
existingKeys.add(cursor.value.key);
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
await processBatch(entries, async ([modelName, url]) => {
|
|
7232
|
+
const key = modelKey(styleId, modelName);
|
|
7233
|
+
const label = `${styleId}::${modelName}`;
|
|
7234
|
+
if (skipExisting && existingKeys.has(key)) {
|
|
7235
|
+
result.skippedModels++;
|
|
7236
|
+
progressTracker.update(1, label);
|
|
7237
|
+
emit();
|
|
7238
|
+
return;
|
|
7239
|
+
}
|
|
7240
|
+
try {
|
|
7241
|
+
const response = await fetchResourceWithRetry(url, {
|
|
7242
|
+
retries: maxRetries,
|
|
7243
|
+
timeout: timeoutMs,
|
|
7244
|
+
proxyType: 'tiles',
|
|
7245
|
+
});
|
|
7246
|
+
if (response.type === 'json') {
|
|
7247
|
+
throw new Error('Unexpected JSON response for model');
|
|
7248
|
+
}
|
|
7249
|
+
const data = response.data;
|
|
7250
|
+
const contentType = ('contentType' in response && response.contentType) || 'model/gltf-binary';
|
|
7251
|
+
const entry = {
|
|
7252
|
+
key,
|
|
7253
|
+
data,
|
|
7254
|
+
contentType,
|
|
7255
|
+
size: data.byteLength,
|
|
7256
|
+
url,
|
|
7257
|
+
styleId,
|
|
7258
|
+
modelName,
|
|
7259
|
+
lastModified: Date.now(),
|
|
7260
|
+
downloadedAt: new Date().toISOString(),
|
|
7261
|
+
expires: response.expires,
|
|
7262
|
+
};
|
|
7263
|
+
await db.put('models', entry);
|
|
7264
|
+
result.downloadedModels++;
|
|
7265
|
+
result.totalSize += data.byteLength;
|
|
7266
|
+
progressTracker.update(1, label);
|
|
7267
|
+
}
|
|
7268
|
+
catch (err) {
|
|
7269
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7270
|
+
result.failedModels++;
|
|
7271
|
+
result.errors.push({ url, error: message });
|
|
7272
|
+
modelLogger.warn(`Failed to download model "${modelName}" from ${url}:`, err);
|
|
7273
|
+
progressTracker.update(1, label, message);
|
|
7274
|
+
}
|
|
7275
|
+
emit();
|
|
7276
|
+
}, { batchSize });
|
|
7277
|
+
modelLogger.info(`Models downloaded for style ${styleId}: ${result.downloadedModels} new, ${result.skippedModels} skipped, ${result.failedModels} failed`);
|
|
7278
|
+
return result;
|
|
7279
|
+
}
|
|
7280
|
+
/** Retrieve a single model by `{styleId, modelName}`. */
|
|
7281
|
+
async getModel(styleId, modelName) {
|
|
7282
|
+
const db = await this.db;
|
|
7283
|
+
return db.get('models', modelKey(styleId, modelName));
|
|
7284
|
+
}
|
|
7285
|
+
/** Aggregate stats across all stored models. */
|
|
7286
|
+
async getModelStats() {
|
|
7287
|
+
const db = await this.db;
|
|
7288
|
+
const stats = {
|
|
7289
|
+
count: 0,
|
|
7290
|
+
totalSize: 0,
|
|
7291
|
+
averageSize: 0,
|
|
7292
|
+
models: [],
|
|
7293
|
+
modelsByStyle: {},
|
|
7294
|
+
};
|
|
7295
|
+
const tx = db.transaction('models', 'readonly');
|
|
7296
|
+
for await (const cursor of tx.store) {
|
|
7297
|
+
const m = cursor.value;
|
|
7298
|
+
stats.count++;
|
|
7299
|
+
stats.totalSize += m.size;
|
|
7300
|
+
stats.models.push({ name: m.modelName, size: m.size, lastModified: m.lastModified });
|
|
7301
|
+
stats.modelsByStyle[m.styleId] = (stats.modelsByStyle[m.styleId] ?? 0) + 1;
|
|
7302
|
+
}
|
|
7303
|
+
stats.averageSize = stats.count > 0 ? stats.totalSize / stats.count : 0;
|
|
7304
|
+
return stats;
|
|
7305
|
+
}
|
|
7306
|
+
/**
|
|
7307
|
+
* Delete models older than `maxAge` days. Defaults to 30.
|
|
7308
|
+
*/
|
|
7309
|
+
async cleanupOldModels(maxAge = 30) {
|
|
7310
|
+
const db = await this.db;
|
|
7311
|
+
const cutoff = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7312
|
+
let deleted = 0;
|
|
7313
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7314
|
+
for await (const cursor of tx.store) {
|
|
7315
|
+
if (cursor.value.lastModified < cutoff) {
|
|
7316
|
+
await cursor.delete();
|
|
7317
|
+
deleted++;
|
|
7318
|
+
}
|
|
7319
|
+
}
|
|
7320
|
+
return deleted;
|
|
7321
|
+
}
|
|
7322
|
+
/**
|
|
7323
|
+
* Basic integrity check: remove entries with empty/missing data.
|
|
7324
|
+
*/
|
|
7325
|
+
async verifyAndRepairModels() {
|
|
7326
|
+
const db = await this.db;
|
|
7327
|
+
let verified = 0;
|
|
7328
|
+
let removed = 0;
|
|
7329
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7330
|
+
for await (const cursor of tx.store) {
|
|
7331
|
+
const m = cursor.value;
|
|
7332
|
+
if (!m.data || m.data.byteLength === 0) {
|
|
7333
|
+
await cursor.delete();
|
|
7334
|
+
removed++;
|
|
7335
|
+
}
|
|
7336
|
+
else {
|
|
7337
|
+
verified++;
|
|
7338
|
+
}
|
|
7339
|
+
}
|
|
7340
|
+
return { verified, repaired: 0, removed };
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
// Singleton + convenience exports, matching other service modules.
|
|
7344
|
+
const modelService = new ModelService();
|
|
7345
|
+
const downloadModels = (models, styleId, options) => modelService.downloadModels(models, styleId, options);
|
|
7346
|
+
const getModel = (styleId, modelName) => modelService.getModel(styleId, modelName);
|
|
7347
|
+
const getModelStats = () => modelService.getModelStats();
|
|
7348
|
+
const cleanupOldModels = (maxAge) => modelService.cleanupOldModels(maxAge);
|
|
7349
|
+
const verifyAndRepairModels = () => modelService.verifyAndRepairModels();
|
|
7350
|
+
|
|
7351
|
+
var modelService$1 = /*#__PURE__*/Object.freeze({
|
|
7352
|
+
__proto__: null,
|
|
7353
|
+
ModelService: ModelService,
|
|
7354
|
+
cleanupOldModels: cleanupOldModels,
|
|
7355
|
+
downloadModels: downloadModels,
|
|
7356
|
+
getModel: getModel,
|
|
7357
|
+
getModelStats: getModelStats,
|
|
7358
|
+
modelKeyBelongsToStyle: modelKeyBelongsToStyle,
|
|
7359
|
+
modelService: modelService,
|
|
7360
|
+
verifyAndRepairModels: verifyAndRepairModels
|
|
7361
|
+
});
|
|
7362
|
+
|
|
7038
7363
|
class ResourceService {
|
|
7039
7364
|
// Tile Management Methods
|
|
7040
7365
|
async downloadTilesWithOptions(region, style, styleId, options = {}) {
|
|
@@ -7103,6 +7428,19 @@ class ResourceService {
|
|
|
7103
7428
|
async verifyAndRepairGlyphs() {
|
|
7104
7429
|
return verifyAndRepairGlyphs();
|
|
7105
7430
|
}
|
|
7431
|
+
// 3D Model Management Methods
|
|
7432
|
+
async downloadModelsWithOptions(models, styleId, options = {}) {
|
|
7433
|
+
return downloadModels(models, styleId, options);
|
|
7434
|
+
}
|
|
7435
|
+
async getModelStats() {
|
|
7436
|
+
return getModelStats();
|
|
7437
|
+
}
|
|
7438
|
+
async cleanupOldModels(options) {
|
|
7439
|
+
return cleanupOldModels(options?.maxAge);
|
|
7440
|
+
}
|
|
7441
|
+
async verifyAndRepairModels() {
|
|
7442
|
+
return verifyAndRepairModels();
|
|
7443
|
+
}
|
|
7106
7444
|
}
|
|
7107
7445
|
|
|
7108
7446
|
// The underlying stats functions already iterate every entry in their
|
|
@@ -7897,6 +8235,10 @@ const createResourceManagement = (services) => ({
|
|
|
7897
8235
|
loadGlyphsForStyle: (...args) => services.resourceService.loadGlyphsForStyle(...args),
|
|
7898
8236
|
cleanupOldGlyphs: (...args) => services.resourceService.cleanupOldGlyphs(...args),
|
|
7899
8237
|
verifyAndRepairGlyphs: (...args) => services.resourceService.verifyAndRepairGlyphs(...args),
|
|
8238
|
+
downloadModelsWithOptions: (...args) => services.resourceService.downloadModelsWithOptions(...args),
|
|
8239
|
+
getModelStats: (...args) => services.resourceService.getModelStats(...args),
|
|
8240
|
+
cleanupOldModels: (...args) => services.resourceService.cleanupOldModels(...args),
|
|
8241
|
+
verifyAndRepairModels: (...args) => services.resourceService.verifyAndRepairModels(...args),
|
|
7900
8242
|
});
|
|
7901
8243
|
|
|
7902
8244
|
const createAnalyticsManagement = (services, deps) => ({
|
|
@@ -11543,10 +11885,12 @@ class PanelRenderer extends BaseComponent {
|
|
|
11543
11885
|
panelLogger.debug(`Fixed legacy source ${sourceId}: added tiles, removed url`);
|
|
11544
11886
|
}
|
|
11545
11887
|
}
|
|
11546
|
-
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11888
|
+
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11889
|
+
// buildings and raster-array for Mapbox Standard landmark icons).
|
|
11547
11890
|
if (src.type === 'vector' ||
|
|
11548
11891
|
src.type === 'raster' ||
|
|
11549
11892
|
src.type === 'raster-dem' ||
|
|
11893
|
+
src.type === 'raster-array' ||
|
|
11550
11894
|
src.type === 'batched-model') {
|
|
11551
11895
|
const originalMaxzoom = src.maxzoom;
|
|
11552
11896
|
// Use the lower of region maxZoom and source's original maxzoom so we
|
|
@@ -13609,5 +13953,5 @@ class OfflineManagerControl {
|
|
|
13609
13953
|
}
|
|
13610
13954
|
}
|
|
13611
13955
|
|
|
13612
|
-
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, 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, cleanupOldSprites, cleanupOldStyles, cleanupOldTiles, cleanupService, clearAllCaches, configureLogger, configureProxy, convertStyleForServiceWorker, countCompressedTiles, createProgressTracker, createTileKey, dbPromise, OfflineMapManager as default, deleteStyleById, deleteStyles, deriveTileExtension, detectCssPrefix, detectStyleProvider, downloadFonts, downloadGlyphs, downloadSprites, downloadStyleWithProvider, downloadStyles, downloadTiles, escapeHtml$1 as escapeHtml, extractAccessToken, extractAllFontNames, extractFontNamesFromTextField, fetchResourceWithRetry, fetchWithRetry, fontService, formatBytes, formatDate, generateGlyphUrlsFromStyle, getExpiredResourceCount, getFontAnalytics, getFontStats, getGlyphAnalytics, getGlyphStats, getIcon, getRegionAnalytics, getSpriteAnalytics, getSpriteStats, getStyleStats, getTileAnalytics, getTileStats, getUserErrorMessage, glyphService, hasImports, i18n, icons, idbFetchHandler, isMapboxProtocol, isStyleDownloaded, loadAllStoredRegions, loadGlyphs, loadStyleById, loadStyles, logger, 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, verifyAndRepairSprites };
|
|
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 };
|
|
13613
13957
|
//# sourceMappingURL=index.esm.js.map
|