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/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ var tilebelt__namespace = /*#__PURE__*/_interopNamespaceDefault(tilebelt);
|
|
|
35
35
|
*/
|
|
36
36
|
// IndexedDB Configuration
|
|
37
37
|
const DB_NAME = 'offline-map-db';
|
|
38
|
-
const DB_VERSION =
|
|
38
|
+
const DB_VERSION = 4;
|
|
39
39
|
// Store Names (regions are stored inside styles.regions[], not as a separate store)
|
|
40
40
|
const STORE_NAMES = {
|
|
41
41
|
TILES: 'tiles',
|
|
@@ -43,6 +43,7 @@ const STORE_NAMES = {
|
|
|
43
43
|
SPRITES: 'sprites',
|
|
44
44
|
GLYPHS: 'glyphs',
|
|
45
45
|
FONTS: 'fonts',
|
|
46
|
+
MODELS: 'models',
|
|
46
47
|
};
|
|
47
48
|
// Download Configuration
|
|
48
49
|
const DOWNLOAD_DEFAULTS = {
|
|
@@ -250,7 +251,7 @@ async function resetOfflineMapDB() {
|
|
|
250
251
|
* Called during initial database creation or when stores are missing.
|
|
251
252
|
*/
|
|
252
253
|
function createStores(db) {
|
|
253
|
-
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts'];
|
|
254
|
+
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts', 'models'];
|
|
254
255
|
for (const storeName of stores) {
|
|
255
256
|
if (!db.objectStoreNames.contains(storeName)) {
|
|
256
257
|
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
@@ -332,6 +333,7 @@ function migrateRegionsToStyles(transaction) {
|
|
|
332
333
|
* - sprites: Sprite images and JSON
|
|
333
334
|
* - glyphs: Font glyph data
|
|
334
335
|
* - fonts: Font files
|
|
336
|
+
* - models: 3D model files (.glb) for Mapbox Standard tree/turbine layers
|
|
335
337
|
* - regions: (deprecated) Legacy region storage, migrated to styles.regions[]
|
|
336
338
|
*
|
|
337
339
|
* @example
|
|
@@ -351,6 +353,9 @@ async function openOfflineMapDB() {
|
|
|
351
353
|
if (oldVersion > 0 && oldVersion < 3) {
|
|
352
354
|
migrateRegionsToStyles(transaction);
|
|
353
355
|
}
|
|
356
|
+
// Migration: v3 -> v4
|
|
357
|
+
// Adds the `models` store for Mapbox Standard 3D model assets.
|
|
358
|
+
// No data migration needed — createStores above handles it.
|
|
354
359
|
},
|
|
355
360
|
});
|
|
356
361
|
}
|
|
@@ -1207,6 +1212,7 @@ const idbLogger = logger.scope('IDBFetch');
|
|
|
1207
1212
|
// idb://{downloadId}/tile/{sourceKey}/{url}
|
|
1208
1213
|
// idb://{downloadId}/glyph/{fontstack}/{range}.pbf
|
|
1209
1214
|
// idb://{downloadId}/sprite/{spriteName}
|
|
1215
|
+
// idb://{styleId}/model/{modelName}
|
|
1210
1216
|
// idb://{downloadId}/tilesjson/{url}
|
|
1211
1217
|
// Cache for region ID to style mapping to avoid repeated DB queries
|
|
1212
1218
|
const regionToStyleCache = new Map();
|
|
@@ -1578,6 +1584,33 @@ async function idbFetchHandler(url, init) {
|
|
|
1578
1584
|
}
|
|
1579
1585
|
break;
|
|
1580
1586
|
}
|
|
1587
|
+
case 'model': {
|
|
1588
|
+
// Model URLs are rewritten by patchStyleForOffline to:
|
|
1589
|
+
// idb://{styleId}/model/{modelName}
|
|
1590
|
+
// Models are keyed by {styleId}::model::{modelName} in the store.
|
|
1591
|
+
// Mirror the sprite resolution fallback: try the style ID first,
|
|
1592
|
+
// then the download/region ID (in case the request came through a
|
|
1593
|
+
// region-scoped URL).
|
|
1594
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1595
|
+
const actualStyleId = styleEntry?.key || downloadId;
|
|
1596
|
+
const candidates = Array.from(new Set([
|
|
1597
|
+
`${actualStyleId}::model::${decodedResourcePath}`,
|
|
1598
|
+
`${downloadId}::model::${decodedResourcePath}`,
|
|
1599
|
+
]));
|
|
1600
|
+
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1601
|
+
for (const candidateKey of candidates) {
|
|
1602
|
+
const resource = await db.get('models', candidateKey);
|
|
1603
|
+
if (resource?.data) {
|
|
1604
|
+
idbLogger.debug(`Found model using key: ${candidateKey}`);
|
|
1605
|
+
return new Response(resource.data, {
|
|
1606
|
+
status: 200,
|
|
1607
|
+
headers: { 'Content-Type': resource.contentType || 'model/gltf-binary' },
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
idbLogger.warn(`Model not found, tried keys: ${candidates.join(', ')}`);
|
|
1612
|
+
break;
|
|
1613
|
+
}
|
|
1581
1614
|
case 'tilesjson': {
|
|
1582
1615
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1583
1616
|
// First try direct lookup (for style-level downloads)
|
|
@@ -2033,14 +2066,29 @@ function patchStyleForOffline(style, downloadId, maxZoom, tileExtension, styleId
|
|
|
2033
2066
|
});
|
|
2034
2067
|
}
|
|
2035
2068
|
}
|
|
2036
|
-
// Patch top-level models (Mapbox Standard 3D
|
|
2069
|
+
// Patch top-level models (Mapbox Standard 3D trees / wind turbines).
|
|
2070
|
+
// Two shapes exist in the wild:
|
|
2071
|
+
// - Mapbox Standard: `{ "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb" }` (string values)
|
|
2072
|
+
// - Older/generic: `{ "name": { "uri": "mapbox://..." } }` (object values)
|
|
2073
|
+
// Models are keyed on the style ID (like sprites) so they can be shared
|
|
2074
|
+
// across regions.
|
|
2037
2075
|
if (style.models) {
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2076
|
+
const modelBaseId = styleId || downloadId;
|
|
2077
|
+
const models = style.models;
|
|
2078
|
+
let patchedCount = 0;
|
|
2079
|
+
for (const [modelId, value] of Object.entries(models)) {
|
|
2080
|
+
if (typeof value === 'string') {
|
|
2081
|
+
models[modelId] = `idb://${modelBaseId}/model/${modelId}`;
|
|
2082
|
+
patchedCount++;
|
|
2083
|
+
}
|
|
2084
|
+
else if (value && typeof value === 'object' && 'uri' in value && value.uri) {
|
|
2085
|
+
value.uri = `idb://${modelBaseId}/model/${modelId}`;
|
|
2086
|
+
patchedCount++;
|
|
2041
2087
|
}
|
|
2042
2088
|
}
|
|
2043
|
-
|
|
2089
|
+
if (patchedCount > 0) {
|
|
2090
|
+
styleLogger.debug(`Patched ${patchedCount} model URIs (styleId: ${modelBaseId})`);
|
|
2091
|
+
}
|
|
2044
2092
|
}
|
|
2045
2093
|
styleLogger.debug(`Final patched style:`, style);
|
|
2046
2094
|
return style;
|
|
@@ -2608,12 +2656,22 @@ function convertStyleForServiceWorker(style) {
|
|
|
2608
2656
|
});
|
|
2609
2657
|
}
|
|
2610
2658
|
}
|
|
2611
|
-
// Convert models
|
|
2659
|
+
// Convert models. Two shapes in the wild:
|
|
2660
|
+
// - Mapbox Standard: `{ name: "idb://..." }` (string value)
|
|
2661
|
+
// - Older/generic: `{ name: { uri: "idb://..." } }` (object value)
|
|
2612
2662
|
if (converted.models) {
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2663
|
+
const models = converted.models;
|
|
2664
|
+
for (const modelKey of Object.keys(models)) {
|
|
2665
|
+
const value = models[modelKey];
|
|
2666
|
+
if (typeof value === 'string') {
|
|
2667
|
+
if (value.startsWith('idb://')) {
|
|
2668
|
+
models[modelKey] = replace(value);
|
|
2669
|
+
}
|
|
2670
|
+
}
|
|
2671
|
+
else if (value && typeof value === 'object') {
|
|
2672
|
+
if (typeof value.uri === 'string' && value.uri.startsWith('idb://')) {
|
|
2673
|
+
value.uri = replace(value.uri);
|
|
2674
|
+
}
|
|
2617
2675
|
}
|
|
2618
2676
|
}
|
|
2619
2677
|
}
|
|
@@ -4934,7 +4992,15 @@ class RegionService {
|
|
|
4934
4992
|
deletedSprites++;
|
|
4935
4993
|
}
|
|
4936
4994
|
}
|
|
4937
|
-
|
|
4995
|
+
let deletedModels = 0;
|
|
4996
|
+
const modelTx = db.transaction('models', 'readwrite');
|
|
4997
|
+
for await (const cursor of modelTx.store) {
|
|
4998
|
+
if (resourceKeyBelongsToStyle(cursor.value.key, styleId)) {
|
|
4999
|
+
await cursor.delete();
|
|
5000
|
+
deletedModels++;
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
regionLogger$1.info(`Deleted style resources: ${deletedFonts} fonts, ${deletedGlyphs} glyphs, ${deletedSprites} sprites, ${deletedModels} models`);
|
|
4938
5004
|
}
|
|
4939
5005
|
/**
|
|
4940
5006
|
* Delete all tiles for a style
|
|
@@ -5042,7 +5108,7 @@ class RegionService {
|
|
|
5042
5108
|
if (!region.styleUrl) {
|
|
5043
5109
|
throw new Error('Region must have a styleUrl');
|
|
5044
5110
|
}
|
|
5045
|
-
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, glyphRanges, tileOptions, } = options;
|
|
5111
|
+
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, skipModels = false, glyphRanges, tileOptions, } = options;
|
|
5046
5112
|
const emit = (phase, completed, total, message) => {
|
|
5047
5113
|
if (!onProgress)
|
|
5048
5114
|
return;
|
|
@@ -5097,8 +5163,13 @@ class RegionService {
|
|
|
5097
5163
|
const spriteSources = normalizeSpriteProperty(originalSpriteUrl);
|
|
5098
5164
|
if (spriteSources.length > 0) {
|
|
5099
5165
|
const { downloadSprites } = await Promise.resolve().then(function () { return spriteService$1; });
|
|
5100
|
-
|
|
5101
|
-
|
|
5166
|
+
// Standard four sprite variants. For Mapbox Standard, an `iconset.pbf`
|
|
5167
|
+
// sibling is also served under the same /styles/v1/.../<hash>/ path
|
|
5168
|
+
// — we detect that case per-source below and append it to the list.
|
|
5169
|
+
const baseSuffixes = ['.json', '.png', '@2x.json', '@2x.png'];
|
|
5170
|
+
// Estimate total files assuming iconset is always included (the actual
|
|
5171
|
+
// number may be smaller; the emit helper clamps progress to total).
|
|
5172
|
+
const totalFiles = spriteSources.length * (baseSuffixes.length + 1);
|
|
5102
5173
|
let completed = 0;
|
|
5103
5174
|
emit('sprites', 0, totalFiles, 'Downloading sprites');
|
|
5104
5175
|
for (const source of spriteSources) {
|
|
@@ -5107,9 +5178,27 @@ class RegionService {
|
|
|
5107
5178
|
spriteBase = resolveMapboxUrl(spriteBase, effectiveAccessToken);
|
|
5108
5179
|
}
|
|
5109
5180
|
const qIndex = spriteBase.indexOf('?');
|
|
5110
|
-
const
|
|
5111
|
-
|
|
5112
|
-
|
|
5181
|
+
const suffixes = [...baseSuffixes];
|
|
5182
|
+
// Mapbox Standard serves an iconset.pbf alongside the sprite under
|
|
5183
|
+
// /styles/v1/{owner}/{style}/{hash}/sprite → the sibling file is
|
|
5184
|
+
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5185
|
+
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5186
|
+
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5187
|
+
const isMapboxStandardSprite = /api\.mapbox\.com\/styles\/v1\/.+\/sprite$/.test(pathWithoutQuery);
|
|
5188
|
+
if (isMapboxStandardSprite) {
|
|
5189
|
+
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5190
|
+
suffixes.push('__ICONSET__');
|
|
5191
|
+
}
|
|
5192
|
+
const spriteUrls = suffixes.map(suffix => {
|
|
5193
|
+
if (suffix === '__ICONSET__') {
|
|
5194
|
+
// Replace trailing `sprite` with `iconset.pbf`, preserving query.
|
|
5195
|
+
const base = pathWithoutQuery.replace(/sprite$/, 'iconset.pbf');
|
|
5196
|
+
return qIndex !== -1 ? base + spriteBase.slice(qIndex) : base;
|
|
5197
|
+
}
|
|
5198
|
+
return qIndex !== -1
|
|
5199
|
+
? spriteBase.slice(0, qIndex) + suffix + spriteBase.slice(qIndex)
|
|
5200
|
+
: spriteBase + suffix;
|
|
5201
|
+
});
|
|
5113
5202
|
try {
|
|
5114
5203
|
const result = await downloadSprites(spriteUrls, styleId, {
|
|
5115
5204
|
enableValidation: true,
|
|
@@ -5155,7 +5244,42 @@ class RegionService {
|
|
|
5155
5244
|
}
|
|
5156
5245
|
}
|
|
5157
5246
|
}
|
|
5158
|
-
// 4.
|
|
5247
|
+
// 4. Models — Mapbox Standard's `style.models` references 3D tree /
|
|
5248
|
+
// turbine .glb assets. Two value shapes exist in the wild
|
|
5249
|
+
// (plain string or `{ uri }`) — we accept both.
|
|
5250
|
+
let modelResult;
|
|
5251
|
+
if (!skipModels && storedStyle.models) {
|
|
5252
|
+
const rawModels = storedStyle.models;
|
|
5253
|
+
const resolved = {};
|
|
5254
|
+
for (const [name, value] of Object.entries(rawModels)) {
|
|
5255
|
+
const uri = typeof value === 'string' ? value : value?.uri;
|
|
5256
|
+
if (!uri)
|
|
5257
|
+
continue;
|
|
5258
|
+
if (uri.startsWith('idb://'))
|
|
5259
|
+
continue; // already patched
|
|
5260
|
+
const httpUrl = isMapboxProtocol(uri) && effectiveAccessToken
|
|
5261
|
+
? resolveMapboxUrl(uri, effectiveAccessToken)
|
|
5262
|
+
: uri;
|
|
5263
|
+
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
|
|
5264
|
+
resolved[name] = httpUrl;
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
if (Object.keys(resolved).length > 0) {
|
|
5268
|
+
const { downloadModels } = await Promise.resolve().then(function () { return modelService$1; });
|
|
5269
|
+
emit('models', 0, Object.keys(resolved).length, 'Downloading 3D models');
|
|
5270
|
+
try {
|
|
5271
|
+
modelResult = await downloadModels(resolved, styleId, {
|
|
5272
|
+
onProgress: (progress) => {
|
|
5273
|
+
emit('models', progress.completed, progress.total, 'Downloading 3D models');
|
|
5274
|
+
},
|
|
5275
|
+
});
|
|
5276
|
+
}
|
|
5277
|
+
catch (error) {
|
|
5278
|
+
regionLogger$1.warn('Model download failed (non-fatal):', error);
|
|
5279
|
+
}
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5282
|
+
// 5. Tiles — use the stored (source-embedded) style, which still has HTTP tile URLs
|
|
5159
5283
|
const { downloadTiles } = await Promise.resolve().then(function () { return tileService$1; });
|
|
5160
5284
|
const regionForTiles = { ...region, styleId };
|
|
5161
5285
|
emit('tiles', 0, 100, 'Downloading tiles');
|
|
@@ -5168,7 +5292,7 @@ class RegionService {
|
|
|
5168
5292
|
tileOptions?.onProgress?.(progress);
|
|
5169
5293
|
},
|
|
5170
5294
|
});
|
|
5171
|
-
//
|
|
5295
|
+
// 6. Metadata — must run last, since addRegion patches style URLs to idb://.
|
|
5172
5296
|
// Do NOT auto-fill tileExtension from tileResult: that's only the first
|
|
5173
5297
|
// source's extension, and addRegion feeds it to patchStyleForOffline which
|
|
5174
5298
|
// would override ALL sources — breaking mixed raster+vector styles. The
|
|
@@ -5184,6 +5308,7 @@ class RegionService {
|
|
|
5184
5308
|
styleResult,
|
|
5185
5309
|
spriteResults,
|
|
5186
5310
|
glyphResult,
|
|
5311
|
+
modelResult,
|
|
5187
5312
|
tileResult,
|
|
5188
5313
|
};
|
|
5189
5314
|
}
|
|
@@ -6322,10 +6447,15 @@ class TileService {
|
|
|
6322
6447
|
tilesLength: config.tiles ? config.tiles.length : 0,
|
|
6323
6448
|
url: config.url,
|
|
6324
6449
|
});
|
|
6325
|
-
// Handle tile-based sources (vector, raster, raster-dem, batched-model
|
|
6450
|
+
// Handle tile-based sources (vector, raster, raster-dem, batched-model,
|
|
6451
|
+
// raster-array). `raster-array` is used by Mapbox Standard for layers
|
|
6452
|
+
// like `mapbox-landmarks` (mapbox.mapbox-landmark-icons-v1) — the tiles
|
|
6453
|
+
// are fetched from the same /v4/ endpoint as other tilesets, so the
|
|
6454
|
+
// TileJSON resolution path below handles them uniformly.
|
|
6326
6455
|
if (config.type === 'vector' ||
|
|
6327
6456
|
config.type === 'raster' ||
|
|
6328
6457
|
config.type === 'raster-dem' ||
|
|
6458
|
+
config.type === 'raster-array' ||
|
|
6329
6459
|
config.type === 'batched-model') {
|
|
6330
6460
|
// Handle direct tile URLs in the source config
|
|
6331
6461
|
if (config.tiles && Array.isArray(config.tiles) && config.tiles.length > 0) {
|
|
@@ -7058,6 +7188,201 @@ var glyphService$1 = /*#__PURE__*/Object.freeze({
|
|
|
7058
7188
|
verifyAndRepairGlyphs: verifyAndRepairGlyphs
|
|
7059
7189
|
});
|
|
7060
7190
|
|
|
7191
|
+
const modelLogger = logger.scope('ModelService');
|
|
7192
|
+
/**
|
|
7193
|
+
* Build the storage key for a model. Kept consistent with sprite/glyph
|
|
7194
|
+
* conventions: `{styleId}::model::{modelName}`.
|
|
7195
|
+
*/
|
|
7196
|
+
function modelKey(styleId, modelName) {
|
|
7197
|
+
return `${styleId}::model::${modelName}`;
|
|
7198
|
+
}
|
|
7199
|
+
/** True when the given key belongs to the given styleId's model store prefix. */
|
|
7200
|
+
function modelKeyBelongsToStyle(key, styleId) {
|
|
7201
|
+
return key.startsWith(`${styleId}::model::`);
|
|
7202
|
+
}
|
|
7203
|
+
/**
|
|
7204
|
+
* Service for downloading, storing, and serving Mapbox 3D model (.glb) files.
|
|
7205
|
+
*
|
|
7206
|
+
* Mapbox Standard declares 32 models at the top of `style.models`:
|
|
7207
|
+
*
|
|
7208
|
+
* ```json
|
|
7209
|
+
* {
|
|
7210
|
+
* "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb",
|
|
7211
|
+
* ...
|
|
7212
|
+
* }
|
|
7213
|
+
* ```
|
|
7214
|
+
*
|
|
7215
|
+
* `model` layers (e.g. `trees`, `wind-turbine-towers`) pick one by name at
|
|
7216
|
+
* render time. For offline use each referenced URL is fetched and stored
|
|
7217
|
+
* here, and `patchStyleForOffline` rewrites the dictionary entries to
|
|
7218
|
+
* `idb://{styleId}/model/{name}` URLs.
|
|
7219
|
+
*/
|
|
7220
|
+
class ModelService {
|
|
7221
|
+
db = dbPromise;
|
|
7222
|
+
/**
|
|
7223
|
+
* Download the set of models referenced by `style.models` for one style.
|
|
7224
|
+
*
|
|
7225
|
+
* @param models `{ modelName: resolvedHttpUrl }` — URLs must already be
|
|
7226
|
+
* resolved (mapbox:// URLs should be resolved by the caller).
|
|
7227
|
+
* @param styleId The owning style's key.
|
|
7228
|
+
*/
|
|
7229
|
+
async downloadModels(models, styleId, options = {}) {
|
|
7230
|
+
const db = await this.db;
|
|
7231
|
+
const { onProgress, batchSize = 4, maxRetries = 3, skipExisting = true, timeoutMs = 30000, } = options;
|
|
7232
|
+
const entries = Object.entries(models);
|
|
7233
|
+
const progressTracker = createProgressTracker(entries.length);
|
|
7234
|
+
const result = {
|
|
7235
|
+
totalModels: entries.length,
|
|
7236
|
+
downloadedModels: 0,
|
|
7237
|
+
skippedModels: 0,
|
|
7238
|
+
failedModels: 0,
|
|
7239
|
+
totalSize: 0,
|
|
7240
|
+
errors: [],
|
|
7241
|
+
};
|
|
7242
|
+
const emit = () => onProgress?.(progressTracker.getProgress());
|
|
7243
|
+
emit();
|
|
7244
|
+
if (entries.length === 0)
|
|
7245
|
+
return result;
|
|
7246
|
+
// Pre-compute existing keys for skipExisting
|
|
7247
|
+
const existingKeys = new Set();
|
|
7248
|
+
if (skipExisting) {
|
|
7249
|
+
const tx = db.transaction('models', 'readonly');
|
|
7250
|
+
for await (const cursor of tx.store) {
|
|
7251
|
+
existingKeys.add(cursor.value.key);
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
await processBatch(entries, async ([modelName, url]) => {
|
|
7255
|
+
const key = modelKey(styleId, modelName);
|
|
7256
|
+
const label = `${styleId}::${modelName}`;
|
|
7257
|
+
if (skipExisting && existingKeys.has(key)) {
|
|
7258
|
+
result.skippedModels++;
|
|
7259
|
+
progressTracker.update(1, label);
|
|
7260
|
+
emit();
|
|
7261
|
+
return;
|
|
7262
|
+
}
|
|
7263
|
+
try {
|
|
7264
|
+
const response = await fetchResourceWithRetry(url, {
|
|
7265
|
+
retries: maxRetries,
|
|
7266
|
+
timeout: timeoutMs,
|
|
7267
|
+
proxyType: 'tiles',
|
|
7268
|
+
});
|
|
7269
|
+
if (response.type === 'json') {
|
|
7270
|
+
throw new Error('Unexpected JSON response for model');
|
|
7271
|
+
}
|
|
7272
|
+
const data = response.data;
|
|
7273
|
+
const contentType = ('contentType' in response && response.contentType) || 'model/gltf-binary';
|
|
7274
|
+
const entry = {
|
|
7275
|
+
key,
|
|
7276
|
+
data,
|
|
7277
|
+
contentType,
|
|
7278
|
+
size: data.byteLength,
|
|
7279
|
+
url,
|
|
7280
|
+
styleId,
|
|
7281
|
+
modelName,
|
|
7282
|
+
lastModified: Date.now(),
|
|
7283
|
+
downloadedAt: new Date().toISOString(),
|
|
7284
|
+
expires: response.expires,
|
|
7285
|
+
};
|
|
7286
|
+
await db.put('models', entry);
|
|
7287
|
+
result.downloadedModels++;
|
|
7288
|
+
result.totalSize += data.byteLength;
|
|
7289
|
+
progressTracker.update(1, label);
|
|
7290
|
+
}
|
|
7291
|
+
catch (err) {
|
|
7292
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7293
|
+
result.failedModels++;
|
|
7294
|
+
result.errors.push({ url, error: message });
|
|
7295
|
+
modelLogger.warn(`Failed to download model "${modelName}" from ${url}:`, err);
|
|
7296
|
+
progressTracker.update(1, label, message);
|
|
7297
|
+
}
|
|
7298
|
+
emit();
|
|
7299
|
+
}, { batchSize });
|
|
7300
|
+
modelLogger.info(`Models downloaded for style ${styleId}: ${result.downloadedModels} new, ${result.skippedModels} skipped, ${result.failedModels} failed`);
|
|
7301
|
+
return result;
|
|
7302
|
+
}
|
|
7303
|
+
/** Retrieve a single model by `{styleId, modelName}`. */
|
|
7304
|
+
async getModel(styleId, modelName) {
|
|
7305
|
+
const db = await this.db;
|
|
7306
|
+
return db.get('models', modelKey(styleId, modelName));
|
|
7307
|
+
}
|
|
7308
|
+
/** Aggregate stats across all stored models. */
|
|
7309
|
+
async getModelStats() {
|
|
7310
|
+
const db = await this.db;
|
|
7311
|
+
const stats = {
|
|
7312
|
+
count: 0,
|
|
7313
|
+
totalSize: 0,
|
|
7314
|
+
averageSize: 0,
|
|
7315
|
+
models: [],
|
|
7316
|
+
modelsByStyle: {},
|
|
7317
|
+
};
|
|
7318
|
+
const tx = db.transaction('models', 'readonly');
|
|
7319
|
+
for await (const cursor of tx.store) {
|
|
7320
|
+
const m = cursor.value;
|
|
7321
|
+
stats.count++;
|
|
7322
|
+
stats.totalSize += m.size;
|
|
7323
|
+
stats.models.push({ name: m.modelName, size: m.size, lastModified: m.lastModified });
|
|
7324
|
+
stats.modelsByStyle[m.styleId] = (stats.modelsByStyle[m.styleId] ?? 0) + 1;
|
|
7325
|
+
}
|
|
7326
|
+
stats.averageSize = stats.count > 0 ? stats.totalSize / stats.count : 0;
|
|
7327
|
+
return stats;
|
|
7328
|
+
}
|
|
7329
|
+
/**
|
|
7330
|
+
* Delete models older than `maxAge` days. Defaults to 30.
|
|
7331
|
+
*/
|
|
7332
|
+
async cleanupOldModels(maxAge = 30) {
|
|
7333
|
+
const db = await this.db;
|
|
7334
|
+
const cutoff = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7335
|
+
let deleted = 0;
|
|
7336
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7337
|
+
for await (const cursor of tx.store) {
|
|
7338
|
+
if (cursor.value.lastModified < cutoff) {
|
|
7339
|
+
await cursor.delete();
|
|
7340
|
+
deleted++;
|
|
7341
|
+
}
|
|
7342
|
+
}
|
|
7343
|
+
return deleted;
|
|
7344
|
+
}
|
|
7345
|
+
/**
|
|
7346
|
+
* Basic integrity check: remove entries with empty/missing data.
|
|
7347
|
+
*/
|
|
7348
|
+
async verifyAndRepairModels() {
|
|
7349
|
+
const db = await this.db;
|
|
7350
|
+
let verified = 0;
|
|
7351
|
+
let removed = 0;
|
|
7352
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7353
|
+
for await (const cursor of tx.store) {
|
|
7354
|
+
const m = cursor.value;
|
|
7355
|
+
if (!m.data || m.data.byteLength === 0) {
|
|
7356
|
+
await cursor.delete();
|
|
7357
|
+
removed++;
|
|
7358
|
+
}
|
|
7359
|
+
else {
|
|
7360
|
+
verified++;
|
|
7361
|
+
}
|
|
7362
|
+
}
|
|
7363
|
+
return { verified, repaired: 0, removed };
|
|
7364
|
+
}
|
|
7365
|
+
}
|
|
7366
|
+
// Singleton + convenience exports, matching other service modules.
|
|
7367
|
+
const modelService = new ModelService();
|
|
7368
|
+
const downloadModels = (models, styleId, options) => modelService.downloadModels(models, styleId, options);
|
|
7369
|
+
const getModel = (styleId, modelName) => modelService.getModel(styleId, modelName);
|
|
7370
|
+
const getModelStats = () => modelService.getModelStats();
|
|
7371
|
+
const cleanupOldModels = (maxAge) => modelService.cleanupOldModels(maxAge);
|
|
7372
|
+
const verifyAndRepairModels = () => modelService.verifyAndRepairModels();
|
|
7373
|
+
|
|
7374
|
+
var modelService$1 = /*#__PURE__*/Object.freeze({
|
|
7375
|
+
__proto__: null,
|
|
7376
|
+
ModelService: ModelService,
|
|
7377
|
+
cleanupOldModels: cleanupOldModels,
|
|
7378
|
+
downloadModels: downloadModels,
|
|
7379
|
+
getModel: getModel,
|
|
7380
|
+
getModelStats: getModelStats,
|
|
7381
|
+
modelKeyBelongsToStyle: modelKeyBelongsToStyle,
|
|
7382
|
+
modelService: modelService,
|
|
7383
|
+
verifyAndRepairModels: verifyAndRepairModels
|
|
7384
|
+
});
|
|
7385
|
+
|
|
7061
7386
|
class ResourceService {
|
|
7062
7387
|
// Tile Management Methods
|
|
7063
7388
|
async downloadTilesWithOptions(region, style, styleId, options = {}) {
|
|
@@ -7126,6 +7451,19 @@ class ResourceService {
|
|
|
7126
7451
|
async verifyAndRepairGlyphs() {
|
|
7127
7452
|
return verifyAndRepairGlyphs();
|
|
7128
7453
|
}
|
|
7454
|
+
// 3D Model Management Methods
|
|
7455
|
+
async downloadModelsWithOptions(models, styleId, options = {}) {
|
|
7456
|
+
return downloadModels(models, styleId, options);
|
|
7457
|
+
}
|
|
7458
|
+
async getModelStats() {
|
|
7459
|
+
return getModelStats();
|
|
7460
|
+
}
|
|
7461
|
+
async cleanupOldModels(options) {
|
|
7462
|
+
return cleanupOldModels(options?.maxAge);
|
|
7463
|
+
}
|
|
7464
|
+
async verifyAndRepairModels() {
|
|
7465
|
+
return verifyAndRepairModels();
|
|
7466
|
+
}
|
|
7129
7467
|
}
|
|
7130
7468
|
|
|
7131
7469
|
// The underlying stats functions already iterate every entry in their
|
|
@@ -7920,6 +8258,10 @@ const createResourceManagement = (services) => ({
|
|
|
7920
8258
|
loadGlyphsForStyle: (...args) => services.resourceService.loadGlyphsForStyle(...args),
|
|
7921
8259
|
cleanupOldGlyphs: (...args) => services.resourceService.cleanupOldGlyphs(...args),
|
|
7922
8260
|
verifyAndRepairGlyphs: (...args) => services.resourceService.verifyAndRepairGlyphs(...args),
|
|
8261
|
+
downloadModelsWithOptions: (...args) => services.resourceService.downloadModelsWithOptions(...args),
|
|
8262
|
+
getModelStats: (...args) => services.resourceService.getModelStats(...args),
|
|
8263
|
+
cleanupOldModels: (...args) => services.resourceService.cleanupOldModels(...args),
|
|
8264
|
+
verifyAndRepairModels: (...args) => services.resourceService.verifyAndRepairModels(...args),
|
|
7923
8265
|
});
|
|
7924
8266
|
|
|
7925
8267
|
const createAnalyticsManagement = (services, deps) => ({
|
|
@@ -11566,10 +11908,12 @@ class PanelRenderer extends BaseComponent {
|
|
|
11566
11908
|
panelLogger.debug(`Fixed legacy source ${sourceId}: added tiles, removed url`);
|
|
11567
11909
|
}
|
|
11568
11910
|
}
|
|
11569
|
-
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11911
|
+
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11912
|
+
// buildings and raster-array for Mapbox Standard landmark icons).
|
|
11570
11913
|
if (src.type === 'vector' ||
|
|
11571
11914
|
src.type === 'raster' ||
|
|
11572
11915
|
src.type === 'raster-dem' ||
|
|
11916
|
+
src.type === 'raster-array' ||
|
|
11573
11917
|
src.type === 'batched-model') {
|
|
11574
11918
|
const originalMaxzoom = src.maxzoom;
|
|
11575
11919
|
// Use the lower of region maxZoom and source's original maxzoom so we
|
|
@@ -13650,6 +13994,7 @@ exports.MAPBOX_CACHE_TTL = MAPBOX_CACHE_TTL;
|
|
|
13650
13994
|
exports.MAPBOX_CLASSIC_STYLES = MAPBOX_CLASSIC_STYLES;
|
|
13651
13995
|
exports.MAP_PROVIDERS = MAP_PROVIDERS;
|
|
13652
13996
|
exports.MaintenanceService = MaintenanceService;
|
|
13997
|
+
exports.ModelService = ModelService;
|
|
13653
13998
|
exports.OfflineManagerControl = OfflineManagerControl;
|
|
13654
13999
|
exports.OfflineMapDBVersionError = OfflineMapDBVersionError;
|
|
13655
14000
|
exports.OfflineMapManager = OfflineMapManager;
|
|
@@ -13672,6 +14017,7 @@ exports.cleanupCompressedTiles = cleanupCompressedTiles;
|
|
|
13672
14017
|
exports.cleanupExpiredTiles = cleanupExpiredTiles;
|
|
13673
14018
|
exports.cleanupOldFonts = cleanupOldFonts;
|
|
13674
14019
|
exports.cleanupOldGlyphs = cleanupOldGlyphs;
|
|
14020
|
+
exports.cleanupOldModels = cleanupOldModels;
|
|
13675
14021
|
exports.cleanupOldSprites = cleanupOldSprites;
|
|
13676
14022
|
exports.cleanupOldStyles = cleanupOldStyles;
|
|
13677
14023
|
exports.cleanupOldTiles = cleanupOldTiles;
|
|
@@ -13692,6 +14038,7 @@ exports.detectCssPrefix = detectCssPrefix;
|
|
|
13692
14038
|
exports.detectStyleProvider = detectStyleProvider;
|
|
13693
14039
|
exports.downloadFonts = downloadFonts;
|
|
13694
14040
|
exports.downloadGlyphs = downloadGlyphs;
|
|
14041
|
+
exports.downloadModels = downloadModels;
|
|
13695
14042
|
exports.downloadSprites = downloadSprites;
|
|
13696
14043
|
exports.downloadStyleWithProvider = downloadStyleWithProvider;
|
|
13697
14044
|
exports.downloadStyles = downloadStyles;
|
|
@@ -13712,6 +14059,8 @@ exports.getFontStats = getFontStats;
|
|
|
13712
14059
|
exports.getGlyphAnalytics = getGlyphAnalytics;
|
|
13713
14060
|
exports.getGlyphStats = getGlyphStats;
|
|
13714
14061
|
exports.getIcon = getIcon;
|
|
14062
|
+
exports.getModel = getModel;
|
|
14063
|
+
exports.getModelStats = getModelStats;
|
|
13715
14064
|
exports.getRegionAnalytics = getRegionAnalytics;
|
|
13716
14065
|
exports.getSpriteAnalytics = getSpriteAnalytics;
|
|
13717
14066
|
exports.getSpriteStats = getSpriteStats;
|
|
@@ -13731,6 +14080,8 @@ exports.loadGlyphs = loadGlyphs;
|
|
|
13731
14080
|
exports.loadStyleById = loadStyleById;
|
|
13732
14081
|
exports.loadStyles = loadStyles;
|
|
13733
14082
|
exports.logger = logger;
|
|
14083
|
+
exports.modelKeyBelongsToStyle = modelKeyBelongsToStyle;
|
|
14084
|
+
exports.modelService = modelService;
|
|
13734
14085
|
exports.normalizeSpriteProperty = normalizeSpriteProperty;
|
|
13735
14086
|
exports.normalizeStyleUrl = normalizeStyleUrl;
|
|
13736
14087
|
exports.optimizeStorage = optimizeStorage;
|
|
@@ -13760,5 +14111,6 @@ exports.validateStyleForProvider = validateStyleForProvider;
|
|
|
13760
14111
|
exports.validateZoomLevels = validateZoomLevels;
|
|
13761
14112
|
exports.verifyAndRepairFonts = verifyAndRepairFonts;
|
|
13762
14113
|
exports.verifyAndRepairGlyphs = verifyAndRepairGlyphs;
|
|
14114
|
+
exports.verifyAndRepairModels = verifyAndRepairModels;
|
|
13763
14115
|
exports.verifyAndRepairSprites = verifyAndRepairSprites;
|
|
13764
14116
|
//# sourceMappingURL=index.js.map
|