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 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 = 3;
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 landmarks)
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
- for (const [modelId, modelConfig] of Object.entries(style.models)) {
2016
- if (modelConfig.uri) {
2017
- modelConfig.uri = `idb://${downloadId}/model/${modelId}`;
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
- styleLogger.debug(`Patched ${Object.keys(style.models).length} model URIs`);
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
- for (const modelKey of Object.keys(converted.models)) {
2591
- const model = converted.models[modelKey];
2592
- if (model.uri && typeof model.uri === 'string' && model.uri.startsWith('idb://')) {
2593
- model.uri = replace(model.uri);
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
- regionLogger$1.info(`Deleted style resources: ${deletedFonts} fonts, ${deletedGlyphs} glyphs, ${deletedSprites} sprites`);
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
- const suffixes = ['.json', '.png', '@2x.json', '@2x.png'];
5078
- const totalFiles = spriteSources.length * suffixes.length;
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 spriteUrls = suffixes.map(suffix => qIndex !== -1
5088
- ? spriteBase.slice(0, qIndex) + suffix + spriteBase.slice(qIndex)
5089
- : spriteBase + suffix);
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. Tilesuse the stored (source-embedded) style, which still has HTTP tile URLs
5224
+ // 4. ModelsMapbox 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
- // 5. Metadata — must run last, since addRegion patches style URLs to idb://.
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 buildings)
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