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.umd.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
// IndexedDB Configuration
|
|
31
31
|
const DB_NAME = 'offline-map-db';
|
|
32
|
-
const DB_VERSION =
|
|
32
|
+
const DB_VERSION = 4;
|
|
33
33
|
// Store Names (regions are stored inside styles.regions[], not as a separate store)
|
|
34
34
|
const STORE_NAMES = {
|
|
35
35
|
TILES: 'tiles',
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
SPRITES: 'sprites',
|
|
38
38
|
GLYPHS: 'glyphs',
|
|
39
39
|
FONTS: 'fonts',
|
|
40
|
+
MODELS: 'models',
|
|
40
41
|
};
|
|
41
42
|
// Download Configuration
|
|
42
43
|
const DOWNLOAD_DEFAULTS = {
|
|
@@ -244,7 +245,7 @@
|
|
|
244
245
|
* Called during initial database creation or when stores are missing.
|
|
245
246
|
*/
|
|
246
247
|
function createStores(db) {
|
|
247
|
-
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts'];
|
|
248
|
+
const stores = ['regions', 'tiles', 'styles', 'sprites', 'glyphs', 'fonts', 'models'];
|
|
248
249
|
for (const storeName of stores) {
|
|
249
250
|
if (!db.objectStoreNames.contains(storeName)) {
|
|
250
251
|
db.createObjectStore(storeName, { keyPath: 'key' });
|
|
@@ -326,6 +327,7 @@
|
|
|
326
327
|
* - sprites: Sprite images and JSON
|
|
327
328
|
* - glyphs: Font glyph data
|
|
328
329
|
* - fonts: Font files
|
|
330
|
+
* - models: 3D model files (.glb) for Mapbox Standard tree/turbine layers
|
|
329
331
|
* - regions: (deprecated) Legacy region storage, migrated to styles.regions[]
|
|
330
332
|
*
|
|
331
333
|
* @example
|
|
@@ -345,6 +347,9 @@
|
|
|
345
347
|
if (oldVersion > 0 && oldVersion < 3) {
|
|
346
348
|
migrateRegionsToStyles(transaction);
|
|
347
349
|
}
|
|
350
|
+
// Migration: v3 -> v4
|
|
351
|
+
// Adds the `models` store for Mapbox Standard 3D model assets.
|
|
352
|
+
// No data migration needed — createStores above handles it.
|
|
348
353
|
},
|
|
349
354
|
});
|
|
350
355
|
}
|
|
@@ -1201,6 +1206,7 @@
|
|
|
1201
1206
|
// idb://{downloadId}/tile/{sourceKey}/{url}
|
|
1202
1207
|
// idb://{downloadId}/glyph/{fontstack}/{range}.pbf
|
|
1203
1208
|
// idb://{downloadId}/sprite/{spriteName}
|
|
1209
|
+
// idb://{styleId}/model/{modelName}
|
|
1204
1210
|
// idb://{downloadId}/tilesjson/{url}
|
|
1205
1211
|
// Cache for region ID to style mapping to avoid repeated DB queries
|
|
1206
1212
|
const regionToStyleCache = new Map();
|
|
@@ -1572,6 +1578,33 @@
|
|
|
1572
1578
|
}
|
|
1573
1579
|
break;
|
|
1574
1580
|
}
|
|
1581
|
+
case 'model': {
|
|
1582
|
+
// Model URLs are rewritten by patchStyleForOffline to:
|
|
1583
|
+
// idb://{styleId}/model/{modelName}
|
|
1584
|
+
// Models are keyed by {styleId}::model::{modelName} in the store.
|
|
1585
|
+
// Mirror the sprite resolution fallback: try the style ID first,
|
|
1586
|
+
// then the download/region ID (in case the request came through a
|
|
1587
|
+
// region-scoped URL).
|
|
1588
|
+
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
1589
|
+
const actualStyleId = styleEntry?.key || downloadId;
|
|
1590
|
+
const candidates = Array.from(new Set([
|
|
1591
|
+
`${actualStyleId}::model::${decodedResourcePath}`,
|
|
1592
|
+
`${downloadId}::model::${decodedResourcePath}`,
|
|
1593
|
+
]));
|
|
1594
|
+
idbLogger.debug(`Model candidates for "${decodedResourcePath}":`, candidates);
|
|
1595
|
+
for (const candidateKey of candidates) {
|
|
1596
|
+
const resource = await db.get('models', candidateKey);
|
|
1597
|
+
if (resource?.data) {
|
|
1598
|
+
idbLogger.debug(`Found model using key: ${candidateKey}`);
|
|
1599
|
+
return new Response(resource.data, {
|
|
1600
|
+
status: 200,
|
|
1601
|
+
headers: { 'Content-Type': resource.contentType || 'model/gltf-binary' },
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
idbLogger.warn(`Model not found, tried keys: ${candidates.join(', ')}`);
|
|
1606
|
+
break;
|
|
1607
|
+
}
|
|
1575
1608
|
case 'tilesjson': {
|
|
1576
1609
|
idbLogger.debug(`Looking for tilejson with downloadId: ${downloadId}, resourcePath: ${decodedResourcePath}`);
|
|
1577
1610
|
// First try direct lookup (for style-level downloads)
|
|
@@ -2027,14 +2060,29 @@
|
|
|
2027
2060
|
});
|
|
2028
2061
|
}
|
|
2029
2062
|
}
|
|
2030
|
-
// Patch top-level models (Mapbox Standard 3D
|
|
2063
|
+
// Patch top-level models (Mapbox Standard 3D trees / wind turbines).
|
|
2064
|
+
// Two shapes exist in the wild:
|
|
2065
|
+
// - Mapbox Standard: `{ "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb" }` (string values)
|
|
2066
|
+
// - Older/generic: `{ "name": { "uri": "mapbox://..." } }` (object values)
|
|
2067
|
+
// Models are keyed on the style ID (like sprites) so they can be shared
|
|
2068
|
+
// across regions.
|
|
2031
2069
|
if (style.models) {
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2070
|
+
const modelBaseId = styleId || downloadId;
|
|
2071
|
+
const models = style.models;
|
|
2072
|
+
let patchedCount = 0;
|
|
2073
|
+
for (const [modelId, value] of Object.entries(models)) {
|
|
2074
|
+
if (typeof value === 'string') {
|
|
2075
|
+
models[modelId] = `idb://${modelBaseId}/model/${modelId}`;
|
|
2076
|
+
patchedCount++;
|
|
2077
|
+
}
|
|
2078
|
+
else if (value && typeof value === 'object' && 'uri' in value && value.uri) {
|
|
2079
|
+
value.uri = `idb://${modelBaseId}/model/${modelId}`;
|
|
2080
|
+
patchedCount++;
|
|
2035
2081
|
}
|
|
2036
2082
|
}
|
|
2037
|
-
|
|
2083
|
+
if (patchedCount > 0) {
|
|
2084
|
+
styleLogger.debug(`Patched ${patchedCount} model URIs (styleId: ${modelBaseId})`);
|
|
2085
|
+
}
|
|
2038
2086
|
}
|
|
2039
2087
|
styleLogger.debug(`Final patched style:`, style);
|
|
2040
2088
|
return style;
|
|
@@ -2602,12 +2650,22 @@
|
|
|
2602
2650
|
});
|
|
2603
2651
|
}
|
|
2604
2652
|
}
|
|
2605
|
-
// Convert models
|
|
2653
|
+
// Convert models. Two shapes in the wild:
|
|
2654
|
+
// - Mapbox Standard: `{ name: "idb://..." }` (string value)
|
|
2655
|
+
// - Older/generic: `{ name: { uri: "idb://..." } }` (object value)
|
|
2606
2656
|
if (converted.models) {
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2657
|
+
const models = converted.models;
|
|
2658
|
+
for (const modelKey of Object.keys(models)) {
|
|
2659
|
+
const value = models[modelKey];
|
|
2660
|
+
if (typeof value === 'string') {
|
|
2661
|
+
if (value.startsWith('idb://')) {
|
|
2662
|
+
models[modelKey] = replace(value);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
else if (value && typeof value === 'object') {
|
|
2666
|
+
if (typeof value.uri === 'string' && value.uri.startsWith('idb://')) {
|
|
2667
|
+
value.uri = replace(value.uri);
|
|
2668
|
+
}
|
|
2611
2669
|
}
|
|
2612
2670
|
}
|
|
2613
2671
|
}
|
|
@@ -4928,7 +4986,15 @@
|
|
|
4928
4986
|
deletedSprites++;
|
|
4929
4987
|
}
|
|
4930
4988
|
}
|
|
4931
|
-
|
|
4989
|
+
let deletedModels = 0;
|
|
4990
|
+
const modelTx = db.transaction('models', 'readwrite');
|
|
4991
|
+
for await (const cursor of modelTx.store) {
|
|
4992
|
+
if (resourceKeyBelongsToStyle(cursor.value.key, styleId)) {
|
|
4993
|
+
await cursor.delete();
|
|
4994
|
+
deletedModels++;
|
|
4995
|
+
}
|
|
4996
|
+
}
|
|
4997
|
+
regionLogger$1.info(`Deleted style resources: ${deletedFonts} fonts, ${deletedGlyphs} glyphs, ${deletedSprites} sprites, ${deletedModels} models`);
|
|
4932
4998
|
}
|
|
4933
4999
|
/**
|
|
4934
5000
|
* Delete all tiles for a style
|
|
@@ -5036,7 +5102,7 @@
|
|
|
5036
5102
|
if (!region.styleUrl) {
|
|
5037
5103
|
throw new Error('Region must have a styleUrl');
|
|
5038
5104
|
}
|
|
5039
|
-
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, glyphRanges, tileOptions, } = options;
|
|
5105
|
+
const { onProgress, provider = 'auto', accessToken, skipGlyphs = false, skipSprites = false, skipModels = false, glyphRanges, tileOptions, } = options;
|
|
5040
5106
|
const emit = (phase, completed, total, message) => {
|
|
5041
5107
|
if (!onProgress)
|
|
5042
5108
|
return;
|
|
@@ -5091,8 +5157,13 @@
|
|
|
5091
5157
|
const spriteSources = normalizeSpriteProperty(originalSpriteUrl);
|
|
5092
5158
|
if (spriteSources.length > 0) {
|
|
5093
5159
|
const { downloadSprites } = await Promise.resolve().then(function () { return spriteService$1; });
|
|
5094
|
-
|
|
5095
|
-
|
|
5160
|
+
// Standard four sprite variants. For Mapbox Standard, an `iconset.pbf`
|
|
5161
|
+
// sibling is also served under the same /styles/v1/.../<hash>/ path
|
|
5162
|
+
// — we detect that case per-source below and append it to the list.
|
|
5163
|
+
const baseSuffixes = ['.json', '.png', '@2x.json', '@2x.png'];
|
|
5164
|
+
// Estimate total files assuming iconset is always included (the actual
|
|
5165
|
+
// number may be smaller; the emit helper clamps progress to total).
|
|
5166
|
+
const totalFiles = spriteSources.length * (baseSuffixes.length + 1);
|
|
5096
5167
|
let completed = 0;
|
|
5097
5168
|
emit('sprites', 0, totalFiles, 'Downloading sprites');
|
|
5098
5169
|
for (const source of spriteSources) {
|
|
@@ -5101,9 +5172,27 @@
|
|
|
5101
5172
|
spriteBase = resolveMapboxUrl(spriteBase, effectiveAccessToken);
|
|
5102
5173
|
}
|
|
5103
5174
|
const qIndex = spriteBase.indexOf('?');
|
|
5104
|
-
const
|
|
5105
|
-
|
|
5106
|
-
|
|
5175
|
+
const suffixes = [...baseSuffixes];
|
|
5176
|
+
// Mapbox Standard serves an iconset.pbf alongside the sprite under
|
|
5177
|
+
// /styles/v1/{owner}/{style}/{hash}/sprite → the sibling file is
|
|
5178
|
+
// /styles/v1/{owner}/{style}/{hash}/iconset.pbf. The last path
|
|
5179
|
+
// segment is `sprite`, so replacing it with `iconset.pbf` works.
|
|
5180
|
+
const pathWithoutQuery = qIndex !== -1 ? spriteBase.slice(0, qIndex) : spriteBase;
|
|
5181
|
+
const isMapboxStandardSprite = /api\.mapbox\.com\/styles\/v1\/.+\/sprite$/.test(pathWithoutQuery);
|
|
5182
|
+
if (isMapboxStandardSprite) {
|
|
5183
|
+
// The path-rewrite suffix replaces the trailing `sprite` segment.
|
|
5184
|
+
suffixes.push('__ICONSET__');
|
|
5185
|
+
}
|
|
5186
|
+
const spriteUrls = suffixes.map(suffix => {
|
|
5187
|
+
if (suffix === '__ICONSET__') {
|
|
5188
|
+
// Replace trailing `sprite` with `iconset.pbf`, preserving query.
|
|
5189
|
+
const base = pathWithoutQuery.replace(/sprite$/, 'iconset.pbf');
|
|
5190
|
+
return qIndex !== -1 ? base + spriteBase.slice(qIndex) : base;
|
|
5191
|
+
}
|
|
5192
|
+
return qIndex !== -1
|
|
5193
|
+
? spriteBase.slice(0, qIndex) + suffix + spriteBase.slice(qIndex)
|
|
5194
|
+
: spriteBase + suffix;
|
|
5195
|
+
});
|
|
5107
5196
|
try {
|
|
5108
5197
|
const result = await downloadSprites(spriteUrls, styleId, {
|
|
5109
5198
|
enableValidation: true,
|
|
@@ -5149,7 +5238,42 @@
|
|
|
5149
5238
|
}
|
|
5150
5239
|
}
|
|
5151
5240
|
}
|
|
5152
|
-
// 4.
|
|
5241
|
+
// 4. Models — Mapbox Standard's `style.models` references 3D tree /
|
|
5242
|
+
// turbine .glb assets. Two value shapes exist in the wild
|
|
5243
|
+
// (plain string or `{ uri }`) — we accept both.
|
|
5244
|
+
let modelResult;
|
|
5245
|
+
if (!skipModels && storedStyle.models) {
|
|
5246
|
+
const rawModels = storedStyle.models;
|
|
5247
|
+
const resolved = {};
|
|
5248
|
+
for (const [name, value] of Object.entries(rawModels)) {
|
|
5249
|
+
const uri = typeof value === 'string' ? value : value?.uri;
|
|
5250
|
+
if (!uri)
|
|
5251
|
+
continue;
|
|
5252
|
+
if (uri.startsWith('idb://'))
|
|
5253
|
+
continue; // already patched
|
|
5254
|
+
const httpUrl = isMapboxProtocol(uri) && effectiveAccessToken
|
|
5255
|
+
? resolveMapboxUrl(uri, effectiveAccessToken)
|
|
5256
|
+
: uri;
|
|
5257
|
+
if (httpUrl.startsWith('http://') || httpUrl.startsWith('https://')) {
|
|
5258
|
+
resolved[name] = httpUrl;
|
|
5259
|
+
}
|
|
5260
|
+
}
|
|
5261
|
+
if (Object.keys(resolved).length > 0) {
|
|
5262
|
+
const { downloadModels } = await Promise.resolve().then(function () { return modelService$1; });
|
|
5263
|
+
emit('models', 0, Object.keys(resolved).length, 'Downloading 3D models');
|
|
5264
|
+
try {
|
|
5265
|
+
modelResult = await downloadModels(resolved, styleId, {
|
|
5266
|
+
onProgress: (progress) => {
|
|
5267
|
+
emit('models', progress.completed, progress.total, 'Downloading 3D models');
|
|
5268
|
+
},
|
|
5269
|
+
});
|
|
5270
|
+
}
|
|
5271
|
+
catch (error) {
|
|
5272
|
+
regionLogger$1.warn('Model download failed (non-fatal):', error);
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
// 5. Tiles — use the stored (source-embedded) style, which still has HTTP tile URLs
|
|
5153
5277
|
const { downloadTiles } = await Promise.resolve().then(function () { return tileService$1; });
|
|
5154
5278
|
const regionForTiles = { ...region, styleId };
|
|
5155
5279
|
emit('tiles', 0, 100, 'Downloading tiles');
|
|
@@ -5162,7 +5286,7 @@
|
|
|
5162
5286
|
tileOptions?.onProgress?.(progress);
|
|
5163
5287
|
},
|
|
5164
5288
|
});
|
|
5165
|
-
//
|
|
5289
|
+
// 6. Metadata — must run last, since addRegion patches style URLs to idb://.
|
|
5166
5290
|
// Do NOT auto-fill tileExtension from tileResult: that's only the first
|
|
5167
5291
|
// source's extension, and addRegion feeds it to patchStyleForOffline which
|
|
5168
5292
|
// would override ALL sources — breaking mixed raster+vector styles. The
|
|
@@ -5178,6 +5302,7 @@
|
|
|
5178
5302
|
styleResult,
|
|
5179
5303
|
spriteResults,
|
|
5180
5304
|
glyphResult,
|
|
5305
|
+
modelResult,
|
|
5181
5306
|
tileResult,
|
|
5182
5307
|
};
|
|
5183
5308
|
}
|
|
@@ -6316,10 +6441,15 @@
|
|
|
6316
6441
|
tilesLength: config.tiles ? config.tiles.length : 0,
|
|
6317
6442
|
url: config.url,
|
|
6318
6443
|
});
|
|
6319
|
-
// Handle tile-based sources (vector, raster, raster-dem, batched-model
|
|
6444
|
+
// Handle tile-based sources (vector, raster, raster-dem, batched-model,
|
|
6445
|
+
// raster-array). `raster-array` is used by Mapbox Standard for layers
|
|
6446
|
+
// like `mapbox-landmarks` (mapbox.mapbox-landmark-icons-v1) — the tiles
|
|
6447
|
+
// are fetched from the same /v4/ endpoint as other tilesets, so the
|
|
6448
|
+
// TileJSON resolution path below handles them uniformly.
|
|
6320
6449
|
if (config.type === 'vector' ||
|
|
6321
6450
|
config.type === 'raster' ||
|
|
6322
6451
|
config.type === 'raster-dem' ||
|
|
6452
|
+
config.type === 'raster-array' ||
|
|
6323
6453
|
config.type === 'batched-model') {
|
|
6324
6454
|
// Handle direct tile URLs in the source config
|
|
6325
6455
|
if (config.tiles && Array.isArray(config.tiles) && config.tiles.length > 0) {
|
|
@@ -7052,6 +7182,201 @@
|
|
|
7052
7182
|
verifyAndRepairGlyphs: verifyAndRepairGlyphs
|
|
7053
7183
|
});
|
|
7054
7184
|
|
|
7185
|
+
const modelLogger = logger.scope('ModelService');
|
|
7186
|
+
/**
|
|
7187
|
+
* Build the storage key for a model. Kept consistent with sprite/glyph
|
|
7188
|
+
* conventions: `{styleId}::model::{modelName}`.
|
|
7189
|
+
*/
|
|
7190
|
+
function modelKey(styleId, modelName) {
|
|
7191
|
+
return `${styleId}::model::${modelName}`;
|
|
7192
|
+
}
|
|
7193
|
+
/** True when the given key belongs to the given styleId's model store prefix. */
|
|
7194
|
+
function modelKeyBelongsToStyle(key, styleId) {
|
|
7195
|
+
return key.startsWith(`${styleId}::model::`);
|
|
7196
|
+
}
|
|
7197
|
+
/**
|
|
7198
|
+
* Service for downloading, storing, and serving Mapbox 3D model (.glb) files.
|
|
7199
|
+
*
|
|
7200
|
+
* Mapbox Standard declares 32 models at the top of `style.models`:
|
|
7201
|
+
*
|
|
7202
|
+
* ```json
|
|
7203
|
+
* {
|
|
7204
|
+
* "maple1-lod1": "mapbox://models/mapbox/maple1-v4-lod1.glb",
|
|
7205
|
+
* ...
|
|
7206
|
+
* }
|
|
7207
|
+
* ```
|
|
7208
|
+
*
|
|
7209
|
+
* `model` layers (e.g. `trees`, `wind-turbine-towers`) pick one by name at
|
|
7210
|
+
* render time. For offline use each referenced URL is fetched and stored
|
|
7211
|
+
* here, and `patchStyleForOffline` rewrites the dictionary entries to
|
|
7212
|
+
* `idb://{styleId}/model/{name}` URLs.
|
|
7213
|
+
*/
|
|
7214
|
+
class ModelService {
|
|
7215
|
+
db = dbPromise;
|
|
7216
|
+
/**
|
|
7217
|
+
* Download the set of models referenced by `style.models` for one style.
|
|
7218
|
+
*
|
|
7219
|
+
* @param models `{ modelName: resolvedHttpUrl }` — URLs must already be
|
|
7220
|
+
* resolved (mapbox:// URLs should be resolved by the caller).
|
|
7221
|
+
* @param styleId The owning style's key.
|
|
7222
|
+
*/
|
|
7223
|
+
async downloadModels(models, styleId, options = {}) {
|
|
7224
|
+
const db = await this.db;
|
|
7225
|
+
const { onProgress, batchSize = 4, maxRetries = 3, skipExisting = true, timeoutMs = 30000, } = options;
|
|
7226
|
+
const entries = Object.entries(models);
|
|
7227
|
+
const progressTracker = createProgressTracker(entries.length);
|
|
7228
|
+
const result = {
|
|
7229
|
+
totalModels: entries.length,
|
|
7230
|
+
downloadedModels: 0,
|
|
7231
|
+
skippedModels: 0,
|
|
7232
|
+
failedModels: 0,
|
|
7233
|
+
totalSize: 0,
|
|
7234
|
+
errors: [],
|
|
7235
|
+
};
|
|
7236
|
+
const emit = () => onProgress?.(progressTracker.getProgress());
|
|
7237
|
+
emit();
|
|
7238
|
+
if (entries.length === 0)
|
|
7239
|
+
return result;
|
|
7240
|
+
// Pre-compute existing keys for skipExisting
|
|
7241
|
+
const existingKeys = new Set();
|
|
7242
|
+
if (skipExisting) {
|
|
7243
|
+
const tx = db.transaction('models', 'readonly');
|
|
7244
|
+
for await (const cursor of tx.store) {
|
|
7245
|
+
existingKeys.add(cursor.value.key);
|
|
7246
|
+
}
|
|
7247
|
+
}
|
|
7248
|
+
await processBatch(entries, async ([modelName, url]) => {
|
|
7249
|
+
const key = modelKey(styleId, modelName);
|
|
7250
|
+
const label = `${styleId}::${modelName}`;
|
|
7251
|
+
if (skipExisting && existingKeys.has(key)) {
|
|
7252
|
+
result.skippedModels++;
|
|
7253
|
+
progressTracker.update(1, label);
|
|
7254
|
+
emit();
|
|
7255
|
+
return;
|
|
7256
|
+
}
|
|
7257
|
+
try {
|
|
7258
|
+
const response = await fetchResourceWithRetry(url, {
|
|
7259
|
+
retries: maxRetries,
|
|
7260
|
+
timeout: timeoutMs,
|
|
7261
|
+
proxyType: 'tiles',
|
|
7262
|
+
});
|
|
7263
|
+
if (response.type === 'json') {
|
|
7264
|
+
throw new Error('Unexpected JSON response for model');
|
|
7265
|
+
}
|
|
7266
|
+
const data = response.data;
|
|
7267
|
+
const contentType = ('contentType' in response && response.contentType) || 'model/gltf-binary';
|
|
7268
|
+
const entry = {
|
|
7269
|
+
key,
|
|
7270
|
+
data,
|
|
7271
|
+
contentType,
|
|
7272
|
+
size: data.byteLength,
|
|
7273
|
+
url,
|
|
7274
|
+
styleId,
|
|
7275
|
+
modelName,
|
|
7276
|
+
lastModified: Date.now(),
|
|
7277
|
+
downloadedAt: new Date().toISOString(),
|
|
7278
|
+
expires: response.expires,
|
|
7279
|
+
};
|
|
7280
|
+
await db.put('models', entry);
|
|
7281
|
+
result.downloadedModels++;
|
|
7282
|
+
result.totalSize += data.byteLength;
|
|
7283
|
+
progressTracker.update(1, label);
|
|
7284
|
+
}
|
|
7285
|
+
catch (err) {
|
|
7286
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7287
|
+
result.failedModels++;
|
|
7288
|
+
result.errors.push({ url, error: message });
|
|
7289
|
+
modelLogger.warn(`Failed to download model "${modelName}" from ${url}:`, err);
|
|
7290
|
+
progressTracker.update(1, label, message);
|
|
7291
|
+
}
|
|
7292
|
+
emit();
|
|
7293
|
+
}, { batchSize });
|
|
7294
|
+
modelLogger.info(`Models downloaded for style ${styleId}: ${result.downloadedModels} new, ${result.skippedModels} skipped, ${result.failedModels} failed`);
|
|
7295
|
+
return result;
|
|
7296
|
+
}
|
|
7297
|
+
/** Retrieve a single model by `{styleId, modelName}`. */
|
|
7298
|
+
async getModel(styleId, modelName) {
|
|
7299
|
+
const db = await this.db;
|
|
7300
|
+
return db.get('models', modelKey(styleId, modelName));
|
|
7301
|
+
}
|
|
7302
|
+
/** Aggregate stats across all stored models. */
|
|
7303
|
+
async getModelStats() {
|
|
7304
|
+
const db = await this.db;
|
|
7305
|
+
const stats = {
|
|
7306
|
+
count: 0,
|
|
7307
|
+
totalSize: 0,
|
|
7308
|
+
averageSize: 0,
|
|
7309
|
+
models: [],
|
|
7310
|
+
modelsByStyle: {},
|
|
7311
|
+
};
|
|
7312
|
+
const tx = db.transaction('models', 'readonly');
|
|
7313
|
+
for await (const cursor of tx.store) {
|
|
7314
|
+
const m = cursor.value;
|
|
7315
|
+
stats.count++;
|
|
7316
|
+
stats.totalSize += m.size;
|
|
7317
|
+
stats.models.push({ name: m.modelName, size: m.size, lastModified: m.lastModified });
|
|
7318
|
+
stats.modelsByStyle[m.styleId] = (stats.modelsByStyle[m.styleId] ?? 0) + 1;
|
|
7319
|
+
}
|
|
7320
|
+
stats.averageSize = stats.count > 0 ? stats.totalSize / stats.count : 0;
|
|
7321
|
+
return stats;
|
|
7322
|
+
}
|
|
7323
|
+
/**
|
|
7324
|
+
* Delete models older than `maxAge` days. Defaults to 30.
|
|
7325
|
+
*/
|
|
7326
|
+
async cleanupOldModels(maxAge = 30) {
|
|
7327
|
+
const db = await this.db;
|
|
7328
|
+
const cutoff = Date.now() - maxAge * 24 * 60 * 60 * 1000;
|
|
7329
|
+
let deleted = 0;
|
|
7330
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7331
|
+
for await (const cursor of tx.store) {
|
|
7332
|
+
if (cursor.value.lastModified < cutoff) {
|
|
7333
|
+
await cursor.delete();
|
|
7334
|
+
deleted++;
|
|
7335
|
+
}
|
|
7336
|
+
}
|
|
7337
|
+
return deleted;
|
|
7338
|
+
}
|
|
7339
|
+
/**
|
|
7340
|
+
* Basic integrity check: remove entries with empty/missing data.
|
|
7341
|
+
*/
|
|
7342
|
+
async verifyAndRepairModels() {
|
|
7343
|
+
const db = await this.db;
|
|
7344
|
+
let verified = 0;
|
|
7345
|
+
let removed = 0;
|
|
7346
|
+
const tx = db.transaction('models', 'readwrite');
|
|
7347
|
+
for await (const cursor of tx.store) {
|
|
7348
|
+
const m = cursor.value;
|
|
7349
|
+
if (!m.data || m.data.byteLength === 0) {
|
|
7350
|
+
await cursor.delete();
|
|
7351
|
+
removed++;
|
|
7352
|
+
}
|
|
7353
|
+
else {
|
|
7354
|
+
verified++;
|
|
7355
|
+
}
|
|
7356
|
+
}
|
|
7357
|
+
return { verified, repaired: 0, removed };
|
|
7358
|
+
}
|
|
7359
|
+
}
|
|
7360
|
+
// Singleton + convenience exports, matching other service modules.
|
|
7361
|
+
const modelService = new ModelService();
|
|
7362
|
+
const downloadModels = (models, styleId, options) => modelService.downloadModels(models, styleId, options);
|
|
7363
|
+
const getModel = (styleId, modelName) => modelService.getModel(styleId, modelName);
|
|
7364
|
+
const getModelStats = () => modelService.getModelStats();
|
|
7365
|
+
const cleanupOldModels = (maxAge) => modelService.cleanupOldModels(maxAge);
|
|
7366
|
+
const verifyAndRepairModels = () => modelService.verifyAndRepairModels();
|
|
7367
|
+
|
|
7368
|
+
var modelService$1 = /*#__PURE__*/Object.freeze({
|
|
7369
|
+
__proto__: null,
|
|
7370
|
+
ModelService: ModelService,
|
|
7371
|
+
cleanupOldModels: cleanupOldModels,
|
|
7372
|
+
downloadModels: downloadModels,
|
|
7373
|
+
getModel: getModel,
|
|
7374
|
+
getModelStats: getModelStats,
|
|
7375
|
+
modelKeyBelongsToStyle: modelKeyBelongsToStyle,
|
|
7376
|
+
modelService: modelService,
|
|
7377
|
+
verifyAndRepairModels: verifyAndRepairModels
|
|
7378
|
+
});
|
|
7379
|
+
|
|
7055
7380
|
class ResourceService {
|
|
7056
7381
|
// Tile Management Methods
|
|
7057
7382
|
async downloadTilesWithOptions(region, style, styleId, options = {}) {
|
|
@@ -7120,6 +7445,19 @@
|
|
|
7120
7445
|
async verifyAndRepairGlyphs() {
|
|
7121
7446
|
return verifyAndRepairGlyphs();
|
|
7122
7447
|
}
|
|
7448
|
+
// 3D Model Management Methods
|
|
7449
|
+
async downloadModelsWithOptions(models, styleId, options = {}) {
|
|
7450
|
+
return downloadModels(models, styleId, options);
|
|
7451
|
+
}
|
|
7452
|
+
async getModelStats() {
|
|
7453
|
+
return getModelStats();
|
|
7454
|
+
}
|
|
7455
|
+
async cleanupOldModels(options) {
|
|
7456
|
+
return cleanupOldModels(options?.maxAge);
|
|
7457
|
+
}
|
|
7458
|
+
async verifyAndRepairModels() {
|
|
7459
|
+
return verifyAndRepairModels();
|
|
7460
|
+
}
|
|
7123
7461
|
}
|
|
7124
7462
|
|
|
7125
7463
|
// The underlying stats functions already iterate every entry in their
|
|
@@ -7914,6 +8252,10 @@
|
|
|
7914
8252
|
loadGlyphsForStyle: (...args) => services.resourceService.loadGlyphsForStyle(...args),
|
|
7915
8253
|
cleanupOldGlyphs: (...args) => services.resourceService.cleanupOldGlyphs(...args),
|
|
7916
8254
|
verifyAndRepairGlyphs: (...args) => services.resourceService.verifyAndRepairGlyphs(...args),
|
|
8255
|
+
downloadModelsWithOptions: (...args) => services.resourceService.downloadModelsWithOptions(...args),
|
|
8256
|
+
getModelStats: (...args) => services.resourceService.getModelStats(...args),
|
|
8257
|
+
cleanupOldModels: (...args) => services.resourceService.cleanupOldModels(...args),
|
|
8258
|
+
verifyAndRepairModels: (...args) => services.resourceService.verifyAndRepairModels(...args),
|
|
7917
8259
|
});
|
|
7918
8260
|
|
|
7919
8261
|
const createAnalyticsManagement = (services, deps) => ({
|
|
@@ -11560,10 +11902,12 @@
|
|
|
11560
11902
|
panelLogger.debug(`Fixed legacy source ${sourceId}: added tiles, removed url`);
|
|
11561
11903
|
}
|
|
11562
11904
|
}
|
|
11563
|
-
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11905
|
+
// Apply maxzoom to all tile sources (including batched-model for 3D
|
|
11906
|
+
// buildings and raster-array for Mapbox Standard landmark icons).
|
|
11564
11907
|
if (src.type === 'vector' ||
|
|
11565
11908
|
src.type === 'raster' ||
|
|
11566
11909
|
src.type === 'raster-dem' ||
|
|
11910
|
+
src.type === 'raster-array' ||
|
|
11567
11911
|
src.type === 'batched-model') {
|
|
11568
11912
|
const originalMaxzoom = src.maxzoom;
|
|
11569
11913
|
// Use the lower of region maxZoom and source's original maxzoom so we
|
|
@@ -13644,6 +13988,7 @@
|
|
|
13644
13988
|
exports.MAPBOX_CLASSIC_STYLES = MAPBOX_CLASSIC_STYLES;
|
|
13645
13989
|
exports.MAP_PROVIDERS = MAP_PROVIDERS;
|
|
13646
13990
|
exports.MaintenanceService = MaintenanceService;
|
|
13991
|
+
exports.ModelService = ModelService;
|
|
13647
13992
|
exports.OfflineManagerControl = OfflineManagerControl;
|
|
13648
13993
|
exports.OfflineMapDBVersionError = OfflineMapDBVersionError;
|
|
13649
13994
|
exports.OfflineMapManager = OfflineMapManager;
|
|
@@ -13666,6 +14011,7 @@
|
|
|
13666
14011
|
exports.cleanupExpiredTiles = cleanupExpiredTiles;
|
|
13667
14012
|
exports.cleanupOldFonts = cleanupOldFonts;
|
|
13668
14013
|
exports.cleanupOldGlyphs = cleanupOldGlyphs;
|
|
14014
|
+
exports.cleanupOldModels = cleanupOldModels;
|
|
13669
14015
|
exports.cleanupOldSprites = cleanupOldSprites;
|
|
13670
14016
|
exports.cleanupOldStyles = cleanupOldStyles;
|
|
13671
14017
|
exports.cleanupOldTiles = cleanupOldTiles;
|
|
@@ -13686,6 +14032,7 @@
|
|
|
13686
14032
|
exports.detectStyleProvider = detectStyleProvider;
|
|
13687
14033
|
exports.downloadFonts = downloadFonts;
|
|
13688
14034
|
exports.downloadGlyphs = downloadGlyphs;
|
|
14035
|
+
exports.downloadModels = downloadModels;
|
|
13689
14036
|
exports.downloadSprites = downloadSprites;
|
|
13690
14037
|
exports.downloadStyleWithProvider = downloadStyleWithProvider;
|
|
13691
14038
|
exports.downloadStyles = downloadStyles;
|
|
@@ -13706,6 +14053,8 @@
|
|
|
13706
14053
|
exports.getGlyphAnalytics = getGlyphAnalytics;
|
|
13707
14054
|
exports.getGlyphStats = getGlyphStats;
|
|
13708
14055
|
exports.getIcon = getIcon;
|
|
14056
|
+
exports.getModel = getModel;
|
|
14057
|
+
exports.getModelStats = getModelStats;
|
|
13709
14058
|
exports.getRegionAnalytics = getRegionAnalytics;
|
|
13710
14059
|
exports.getSpriteAnalytics = getSpriteAnalytics;
|
|
13711
14060
|
exports.getSpriteStats = getSpriteStats;
|
|
@@ -13725,6 +14074,8 @@
|
|
|
13725
14074
|
exports.loadStyleById = loadStyleById;
|
|
13726
14075
|
exports.loadStyles = loadStyles;
|
|
13727
14076
|
exports.logger = logger;
|
|
14077
|
+
exports.modelKeyBelongsToStyle = modelKeyBelongsToStyle;
|
|
14078
|
+
exports.modelService = modelService;
|
|
13728
14079
|
exports.normalizeSpriteProperty = normalizeSpriteProperty;
|
|
13729
14080
|
exports.normalizeStyleUrl = normalizeStyleUrl;
|
|
13730
14081
|
exports.optimizeStorage = optimizeStorage;
|
|
@@ -13754,6 +14105,7 @@
|
|
|
13754
14105
|
exports.validateZoomLevels = validateZoomLevels;
|
|
13755
14106
|
exports.verifyAndRepairFonts = verifyAndRepairFonts;
|
|
13756
14107
|
exports.verifyAndRepairGlyphs = verifyAndRepairGlyphs;
|
|
14108
|
+
exports.verifyAndRepairModels = verifyAndRepairModels;
|
|
13757
14109
|
exports.verifyAndRepairSprites = verifyAndRepairSprites;
|
|
13758
14110
|
|
|
13759
14111
|
Object.defineProperty(exports, '__esModule', { value: true });
|