map-gl-offline 0.5.3 → 0.5.6

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
@@ -20,6 +20,7 @@ A comprehensive **TypeScript** library for **MapLibre GL JS** and **Mapbox GL JS
20
20
 
21
21
  - 🗺️ **Complete Offline Maps**: Download and store entire map regions with polygon-based selection
22
22
  - 🎯 **Smart Tile Management**: Efficient vector/raster tile downloading, caching, and retrieval with zoom-level optimization
23
+ - 🧩 **Extra Tile Sources**: Save additional vector (MVT/PBF) and raster tile layers alongside the style's own sources
23
24
  - 🔤 **Font & Glyph Support**: Comprehensive font and glyph management with Unicode range support
24
25
  - 🎨 **Sprite Management**: Handle map sprites and icons offline with multi-resolution support (@1x, @2x)
25
26
  - 📊 **Real-time Analytics**: Detailed storage analytics, performance metrics, and optimization recommendations
@@ -347,9 +348,45 @@ interface OfflineRegionOptions {
347
348
  deleteOnExpiry?: boolean; // Auto-delete on expiration
348
349
  multipleRegions?: boolean; // Part of a multi-region download
349
350
  tileExtension?: string; // Tile extension (pbf, mvt, png, etc.)
351
+ extraSources?: ExtraSource[]; // Additional tile sources to download alongside the style
350
352
  }
353
+
354
+ interface ExtraSource {
355
+ id: string; // Unique source identifier
356
+ type?: 'vector' | 'raster' | 'raster-dem'; // Defaults to 'vector'
357
+ tiles: string[]; // Tile URL templates with {z}/{x}/{y}
358
+ minzoom?: number;
359
+ maxzoom?: number;
360
+ attribution?: string;
361
+ }
362
+ ```
363
+
364
+ ### Extra Tile Sources
365
+
366
+ Save additional vector or raster layers (e.g. custom overlays) alongside the style's own sources:
367
+
368
+ ```typescript
369
+ await manager.addRegion({
370
+ id: 'downtown',
371
+ name: 'Downtown',
372
+ bounds: [[-74.05, 40.71], [-74.00, 40.76]],
373
+ minZoom: 10,
374
+ maxZoom: 16,
375
+ styleUrl: 'https://example.com/style.json',
376
+ extraSources: [
377
+ {
378
+ id: 'buildings',
379
+ type: 'vector',
380
+ tiles: ['https://tiles.example.com/buildings/{z}/{x}/{y}.pbf'],
381
+ minzoom: 13,
382
+ maxzoom: 16,
383
+ },
384
+ ],
385
+ });
351
386
  ```
352
387
 
388
+ When using the `OfflineManagerControl`, the region download form auto-discovers tile sources on the live map and shows them as checkboxes — users pick which extra layers to include.
389
+
353
390
  ## 🎯 Use Cases
354
391
 
355
392
  - 🏔️ **Outdoor & Recreation Apps**: Hiking, camping, and adventure apps with offline trail maps
@@ -407,60 +444,6 @@ try {
407
444
  }
408
445
  ```
409
446
 
410
- ### Storage Management
411
-
412
- ```typescript
413
- // Check available storage
414
- if ('storage' in navigator && 'estimate' in navigator.storage) {
415
- const { usage, quota } = await navigator.storage.estimate();
416
- console.log(`Used: ${usage} / ${quota} bytes`);
417
- }
418
-
419
- // Regular cleanup
420
- await manager.cleanupExpiredRegions();
421
-
422
- // Auto-cleanup on startup
423
- await manager.setupAutoCleanup({
424
- intervalHours: 24, // Daily
425
- maxAge: 30, // 30 days
426
- });
427
- ```
428
-
429
- ## 🔍 Troubleshooting
430
-
431
- ### Storage Quota Issues
432
-
433
- ```typescript
434
- // Check quota
435
- const { usage, quota } = await navigator.storage.estimate();
436
- if (usage / quota > 0.9) {
437
- await manager.cleanupExpiredRegions();
438
- }
439
-
440
- // Request persistent storage
441
- if (navigator.storage?.persist) {
442
- const isPersisted = await navigator.storage.persist();
443
- console.log(`Persistent storage: ${isPersisted}`);
444
- }
445
- ```
446
-
447
- ### Performance Issues
448
-
449
- ```typescript
450
- // Reduce concurrency for slower devices
451
- const lightOptions = {
452
- maxConcurrency: 2,
453
- batchSize: 10,
454
- timeout: 30000,
455
- };
456
-
457
- // Use smaller regions
458
- const smallerRegion = {
459
- minZoom: 11, // Start at higher zoom
460
- maxZoom: 15, // End at lower zoom
461
- };
462
- ```
463
-
464
447
  ## 🌐 Browser Compatibility
465
448
 
466
449
  | Browser | Version | Support |
@@ -480,7 +463,7 @@ const smallerRegion = {
480
463
 
481
464
  ## 🤝 Contributing
482
465
 
483
- Contributions are welcome! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
466
+ Contributions are welcome!
484
467
 
485
468
  ### Development Setup
486
469
 
@@ -538,44 +521,15 @@ map-gl-offline/
538
521
 
539
522
  ## 🔄 Recent Updates
540
523
 
541
- ### v0.5.3 (Latest)
542
-
543
- - ✅ **Bundle Size**: Reduced ESM bundle from 783 KB to 565 KB (28% reduction)
544
- - ✅ **Turf Tree-Shaking**: Replaced `@turf/turf` monorepo with individual packages (`@turf/area`, `@turf/bbox-polygon`, `@turf/difference`, `@turf/helpers`)
545
- - ✅ **Externalized Dependencies**: `i18next` and `@turf/*` moved to externals
546
- - ✅ **Removed Unused Dependency**: Removed `@tabler/icons` (unused, 47 MB install)
547
- - ✅ **Bug Fixes**: 20+ bugs resolved from comprehensive codebase audit
548
- - ✅ **Import Atomicity**: Region imports use single IndexedDB transactions
549
- - ✅ **Expired Region Cleanup**: `forceCleanupExpiredRegions` now uses actual expiry timestamps
550
- - ✅ **XSS Prevention**: User data escaped in all UI templates
551
- - ✅ **Code Quality**: Dead code removal, `@/` path alias for all imports
552
-
553
- ### v0.5.2
554
-
555
- - ✅ **CLI Command**: `npx map-gl-offline init` to copy the Service Worker into your project
556
- - ✅ **Vite Plugin**: `offlineSwPlugin()` to auto-copy the Service Worker on each build
557
- - ✅ **Mapbox GL Example**: Full React + Vite example app for Mapbox GL JS
524
+ See [CHANGELOG.md](CHANGELOG.md) for complete version history.
558
525
 
559
- ### v0.5.0
526
+ **v0.5.5 (Latest):** Extra tile source selection for offline regions — pick additional vector/raster layers to download alongside the style's own sources.
560
527
 
561
- - **Mapbox GL JS Support**: Full support for Mapbox styles, including `mapbox://` protocol URL resolution
562
- - ✅ **Mapbox Standard Style**: 3D models, raster-dem terrain, and import-based style resolution
563
- - ✅ **Day/Night Light Presets**: Toggle between day and night lighting for Mapbox Standard
564
- - ✅ **Rain & Snow Weather**: Weather effect controls for Mapbox Standard style
565
- - ✅ **Import Resolver**: Automatic resolution of Mapbox Standard `imports` in styles
566
- - ✅ **Internationalization**: English and Arabic language support with full RTL layout
567
- - ✅ **Auto-detection**: Automatically detects Mapbox vs MapLibre styles
528
+ **v0.5.3:** 28% bundle size reduction (783 KB to 565 KB), 20+ bug fixes, XSS prevention, import atomicity, `@/` path alias for all imports.
568
529
 
569
- ### v0.1.0
530
+ **v0.5.2:** CLI command (`npx map-gl-offline init`), Vite plugin for Service Worker, Mapbox GL example app.
570
531
 
571
- - **Fractional Zoom Fix**: Fixed tile loading at fractional zoom levels
572
- - ✅ **Modern UI**: Glassmorphic design with dark/light theme
573
- - ✅ **Polygon Drawing**: Interactive region selection tool
574
- - ✅ **Enhanced Analytics**: Comprehensive storage insights
575
- - ✅ **Performance**: Optimized downloads and memory usage
576
- - ✅ **TypeScript**: Full type safety throughout
577
-
578
- See [CHANGELOG.md](CHANGELOG.md) for complete version history.
532
+ **v0.5.0:** Mapbox GL JS support, Standard style with 3D/terrain, day/night presets, rain/snow, i18n (English & Arabic with RTL).
579
533
 
580
534
  ## 🙏 Acknowledgments
581
535
 
@@ -597,4 +551,6 @@ MIT © [Muhammad Imran Siddique](https://github.com/muimsd)
597
551
 
598
552
  [📖 Documentation](https://map-gl-offline.netlify.app) • [🎮 Live Demo](https://map-gl-offline-demo.netlify.app) • [⭐ Star on GitHub](https://github.com/muimsd/map-gl-offline)
599
553
 
554
+ <a href="https://www.buymeacoffee.com/muimsd" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
555
+
600
556
  </div>
package/dist/index.esm.js CHANGED
@@ -5356,6 +5356,21 @@ class RegionService {
5356
5356
  if (!Array.isArray(styleEntry.regions)) {
5357
5357
  styleEntry.regions = [];
5358
5358
  }
5359
+ // Inject extra sources into the style so they get patched for offline use
5360
+ if (region.extraSources && region.extraSources.length > 0) {
5361
+ for (const extra of region.extraSources) {
5362
+ if (!styleEntry.style.sources[extra.id]) {
5363
+ styleEntry.style.sources[extra.id] = {
5364
+ type: extra.type || 'vector',
5365
+ tiles: extra.tiles,
5366
+ ...(extra.minzoom !== undefined ? { minzoom: extra.minzoom } : {}),
5367
+ ...(extra.maxzoom !== undefined ? { maxzoom: extra.maxzoom } : {}),
5368
+ ...(extra.attribution ? { attribution: extra.attribution } : {}),
5369
+ };
5370
+ regionLogger$1.debug(`Injected extra source into style: ${extra.id}`);
5371
+ }
5372
+ }
5373
+ }
5359
5374
  // Patch style for offline use with the region's maxZoom and tileExtension
5360
5375
  // Pass styleId for sprites since they're stored with the style ID, not region ID
5361
5376
  patchStyleForOffline(styleEntry.style, region.id, region.maxZoom, region.tileExtension, styleId);
@@ -5518,6 +5533,21 @@ class TileService {
5518
5533
  if (!style?.sources || Object.keys(style.sources).length === 0) {
5519
5534
  throw new Error('Style does not contain any sources to download tiles from');
5520
5535
  }
5536
+ // Inject extra sources from the region into the style for downloading
5537
+ if (region.extraSources && region.extraSources.length > 0) {
5538
+ for (const extra of region.extraSources) {
5539
+ if (!style.sources[extra.id]) {
5540
+ style.sources[extra.id] = {
5541
+ type: extra.type || 'vector',
5542
+ tiles: extra.tiles,
5543
+ ...(extra.minzoom !== undefined ? { minzoom: extra.minzoom } : {}),
5544
+ ...(extra.maxzoom !== undefined ? { maxzoom: extra.maxzoom } : {}),
5545
+ ...(extra.attribution ? { attribution: extra.attribution } : {}),
5546
+ };
5547
+ tileLogger.debug(`Injected extra source: ${extra.id}`, extra.tiles);
5548
+ }
5549
+ }
5550
+ }
5521
5551
  // Generate tile coordinates once for the region
5522
5552
  const tileCoords = this.generateTileCoordinates(region);
5523
5553
  tileLogger.debug('🔍 ABOUT TO CALL extractTileSources with style:', {
@@ -8077,6 +8107,14 @@ const en = {
8077
8107
  'regionForm.downloadRegion': 'Download Region',
8078
8108
  'regionForm.area': 'Area',
8079
8109
  'regionForm.bounds': 'Bounds',
8110
+ 'regionForm.extraSources': 'Additional Tile Sources',
8111
+ 'regionForm.extraSourcesInfo': 'Select extra layers from the map to include in this offline region',
8112
+ 'regionForm.noExtraSources': 'No additional tile sources found on the map',
8113
+ 'regionForm.sourceType.vector': 'Vector',
8114
+ 'regionForm.sourceType.raster': 'Raster',
8115
+ 'regionForm.sourceType.raster-dem': 'Terrain',
8116
+ 'regionForm.selectAll': 'Select All',
8117
+ 'regionForm.deselectAll': 'Deselect All',
8080
8118
  // Region List
8081
8119
  'regionList.empty': 'No offline styles or regions found. Click "Add Region" to get started.',
8082
8120
  'regionList.emptyFallback': 'No offline regions found. Click "Add Region" to get started.',
@@ -8292,6 +8330,14 @@ const ar = {
8292
8330
  'regionForm.downloadRegion': 'تحميل المنطقة',
8293
8331
  'regionForm.area': 'المساحة',
8294
8332
  'regionForm.bounds': 'الحدود',
8333
+ 'regionForm.extraSources': 'مصادر بلاطات إضافية',
8334
+ 'regionForm.extraSourcesInfo': 'اختر طبقات إضافية من الخريطة لتضمينها في هذه المنطقة',
8335
+ 'regionForm.noExtraSources': 'لم يتم العثور على مصادر بلاطات إضافية على الخريطة',
8336
+ 'regionForm.sourceType.vector': 'متجه',
8337
+ 'regionForm.sourceType.raster': 'نقطي',
8338
+ 'regionForm.sourceType.raster-dem': 'تضاريس',
8339
+ 'regionForm.selectAll': 'تحديد الكل',
8340
+ 'regionForm.deselectAll': 'إلغاء تحديد الكل',
8295
8341
  // Region List - قائمة المناطق
8296
8342
  'regionList.empty': 'لم يتم العثور على أنماط أو مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
8297
8343
  'regionList.emptyFallback': 'لم يتم العثور على مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
@@ -11644,6 +11690,8 @@ class RegionFormModal {
11644
11690
  providerSelect;
11645
11691
  accessTokenInput;
11646
11692
  accessTokenGroup;
11693
+ // Extra sources picker
11694
+ sourceCheckboxes = new Map();
11647
11695
  constructor(options) {
11648
11696
  this.options = options;
11649
11697
  }
@@ -11815,10 +11863,124 @@ class RegionFormModal {
11815
11863
  `;
11816
11864
  this.accessTokenGroup.appendChild(tokenHelp);
11817
11865
  form.appendChild(this.accessTokenGroup);
11866
+ // Extra sources picker
11867
+ if (this.options.mapSources && this.options.mapSources.length > 0) {
11868
+ const sourcesSection = this.createSourcesPicker(this.options.mapSources);
11869
+ form.appendChild(sourcesSection);
11870
+ }
11818
11871
  // Initialize provider detection
11819
11872
  this.detectProviderFromUrl();
11820
11873
  return form;
11821
11874
  }
11875
+ /**
11876
+ * Create the extra sources picker section
11877
+ */
11878
+ createSourcesPicker(sources) {
11879
+ const group = document.createElement('div');
11880
+ group.className = 'flex flex-col gap-3';
11881
+ // Header with label and select all/deselect all
11882
+ const header = document.createElement('div');
11883
+ header.className = 'flex items-center justify-between';
11884
+ header.innerHTML = `
11885
+ <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300">
11886
+ ${t('regionForm.extraSources')}
11887
+ </label>
11888
+ `;
11889
+ const toggleBtn = document.createElement('button');
11890
+ toggleBtn.type = 'button';
11891
+ toggleBtn.className =
11892
+ 'text-xs text-primary-600 dark:text-primary-400 hover:underline cursor-pointer font-medium';
11893
+ toggleBtn.textContent = t('regionForm.selectAll');
11894
+ let allSelected = false;
11895
+ toggleBtn.addEventListener('click', () => {
11896
+ allSelected = !allSelected;
11897
+ for (const { checkbox } of this.sourceCheckboxes.values()) {
11898
+ checkbox.checked = allSelected;
11899
+ }
11900
+ toggleBtn.textContent = allSelected ? t('regionForm.deselectAll') : t('regionForm.selectAll');
11901
+ });
11902
+ header.appendChild(toggleBtn);
11903
+ group.appendChild(header);
11904
+ // Info text
11905
+ const info = document.createElement('div');
11906
+ info.className = 'text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1';
11907
+ info.innerHTML = `${icons.infoCircle({ size: 12 })} ${t('regionForm.extraSourcesInfo')}`;
11908
+ group.appendChild(info);
11909
+ // Source list
11910
+ const list = document.createElement('div');
11911
+ list.className =
11912
+ 'flex flex-col gap-2 max-h-48 overflow-y-auto p-3 rounded-xl glass-input border border-gray-100/50 dark:border-gray-700/50 bg-gray-50/50 dark:bg-gray-800/50';
11913
+ for (const source of sources) {
11914
+ const row = document.createElement('label');
11915
+ row.className =
11916
+ 'flex items-center gap-3 p-2 rounded-lg hover:bg-white/60 dark:hover:bg-gray-700/40 cursor-pointer transition-colors';
11917
+ const checkbox = document.createElement('input');
11918
+ checkbox.type = 'checkbox';
11919
+ checkbox.className =
11920
+ 'w-4 h-4 rounded border-gray-300 dark:border-gray-600 text-primary-600 focus:ring-primary-500/50 cursor-pointer flex-shrink-0';
11921
+ checkbox.value = source.id;
11922
+ row.appendChild(checkbox);
11923
+ this.sourceCheckboxes.set(source.id, { checkbox, source });
11924
+ const details = document.createElement('div');
11925
+ details.className = 'flex flex-col min-w-0';
11926
+ const nameRow = document.createElement('div');
11927
+ nameRow.className = 'flex items-center gap-2';
11928
+ const name = document.createElement('span');
11929
+ name.className = 'text-sm font-medium text-gray-800 dark:text-gray-200 truncate';
11930
+ name.textContent = source.id;
11931
+ nameRow.appendChild(name);
11932
+ const typeKey = `regionForm.sourceType.${source.type}`;
11933
+ const badge = document.createElement('span');
11934
+ badge.className =
11935
+ source.type === 'vector'
11936
+ ? 'text-[10px] px-1.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 font-medium flex-shrink-0'
11937
+ : source.type === 'raster'
11938
+ ? 'text-[10px] px-1.5 py-0.5 rounded-full bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-300 font-medium flex-shrink-0'
11939
+ : 'text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-300 font-medium flex-shrink-0';
11940
+ badge.textContent = t(typeKey);
11941
+ nameRow.appendChild(badge);
11942
+ details.appendChild(nameRow);
11943
+ // Zoom range info
11944
+ const zoomMin = source.minzoom ?? 0;
11945
+ const zoomMax = source.maxzoom ?? 22;
11946
+ const meta = document.createElement('span');
11947
+ meta.className = 'text-[11px] text-gray-500 dark:text-gray-400 truncate';
11948
+ meta.textContent = `z${zoomMin}-${zoomMax}`;
11949
+ if (source.tiles.length > 0) {
11950
+ try {
11951
+ const hostname = new URL(source.tiles[0]).hostname;
11952
+ meta.textContent += ` · ${escapeHtml$1(hostname)}`;
11953
+ }
11954
+ catch {
11955
+ // ignore invalid URLs
11956
+ }
11957
+ }
11958
+ details.appendChild(meta);
11959
+ row.appendChild(details);
11960
+ list.appendChild(row);
11961
+ }
11962
+ group.appendChild(list);
11963
+ return group;
11964
+ }
11965
+ /**
11966
+ * Get the selected extra sources from the picker
11967
+ */
11968
+ getSelectedExtraSources() {
11969
+ const selected = [];
11970
+ for (const { checkbox, source } of this.sourceCheckboxes.values()) {
11971
+ if (checkbox.checked) {
11972
+ selected.push({
11973
+ id: source.id,
11974
+ type: source.type,
11975
+ tiles: source.tiles,
11976
+ minzoom: source.minzoom,
11977
+ maxzoom: source.maxzoom,
11978
+ attribution: source.attribution,
11979
+ });
11980
+ }
11981
+ }
11982
+ return selected;
11983
+ }
11822
11984
  /**
11823
11985
  * Handle style URL changes to auto-detect provider
11824
11986
  */
@@ -11905,6 +12067,7 @@ class RegionFormModal {
11905
12067
  */
11906
12068
  async handleSave() {
11907
12069
  try {
12070
+ const selectedSources = this.getSelectedExtraSources();
11908
12071
  const formData = {
11909
12072
  name: this.nameInput?.value || `Region ${Date.now()}`,
11910
12073
  minZoom: parseInt(this.minZoomInput?.value || '1'),
@@ -11914,6 +12077,7 @@ class RegionFormModal {
11914
12077
  // Enhanced Mapbox GL support
11915
12078
  provider: this.providerSelect?.value,
11916
12079
  accessToken: this.accessTokenInput?.value || undefined,
12080
+ extraSources: selectedSources.length > 0 ? selectedSources : undefined,
11917
12081
  };
11918
12082
  this.modal?.hide();
11919
12083
  await this.options.onSave(formData);
@@ -12054,10 +12218,77 @@ class RegionControl {
12054
12218
  this.polygonControl.triggerSave();
12055
12219
  }
12056
12220
  }
12221
+ /**
12222
+ * Get the source IDs that belong to the current style URL.
12223
+ * These are the sources the style already includes and will be downloaded automatically.
12224
+ */
12225
+ async getStyleSourceIds() {
12226
+ try {
12227
+ const styleUrl = this.options.styleUrl;
12228
+ if (!styleUrl)
12229
+ return new Set();
12230
+ const response = await fetch(styleUrl);
12231
+ if (!response.ok)
12232
+ return new Set();
12233
+ const styleJson = (await response.json());
12234
+ if (styleJson.sources && typeof styleJson.sources === 'object') {
12235
+ return new Set(Object.keys(styleJson.sources));
12236
+ }
12237
+ }
12238
+ catch (error) {
12239
+ regionLogger.warn('Failed to fetch style for source filtering:', error);
12240
+ }
12241
+ return new Set();
12242
+ }
12243
+ /**
12244
+ * Extract tile sources from the live map instance that are NOT part of the current style.
12245
+ * Returns vector, raster, and raster-dem sources that have tile URLs,
12246
+ * excluding sources that already belong to the style (those are downloaded automatically).
12247
+ */
12248
+ async extractExtraMapSources() {
12249
+ try {
12250
+ const style = this.map?.getStyle?.();
12251
+ if (!style || !style.sources || typeof style.sources !== 'object')
12252
+ return [];
12253
+ // Fetch the original style's source IDs so we can exclude them
12254
+ const styleSourceIds = await this.getStyleSourceIds();
12255
+ const sources = [];
12256
+ for (const [id, raw] of Object.entries(style.sources)) {
12257
+ // Skip sources that belong to the style itself
12258
+ if (styleSourceIds.has(id))
12259
+ continue;
12260
+ const type = raw.type;
12261
+ if (type !== 'vector' && type !== 'raster' && type !== 'raster-dem')
12262
+ continue;
12263
+ const tiles = raw.tiles;
12264
+ // Only include sources that have resolvable tile URLs (not idb://)
12265
+ if (!tiles || !Array.isArray(tiles) || tiles.length === 0)
12266
+ continue;
12267
+ const hasHttpTiles = tiles.some((t) => t.startsWith('http://') || t.startsWith('https://'));
12268
+ if (!hasHttpTiles)
12269
+ continue;
12270
+ sources.push({
12271
+ id,
12272
+ type: type,
12273
+ tiles: tiles.filter((t) => t.startsWith('http://') || t.startsWith('https://')),
12274
+ minzoom: typeof raw.minzoom === 'number' ? raw.minzoom : undefined,
12275
+ maxzoom: typeof raw.maxzoom === 'number' ? raw.maxzoom : undefined,
12276
+ attribution: typeof raw.attribution === 'string' ? raw.attribution : undefined,
12277
+ });
12278
+ }
12279
+ return sources;
12280
+ }
12281
+ catch (error) {
12282
+ regionLogger.warn('Failed to extract map sources:', error);
12283
+ return [];
12284
+ }
12285
+ }
12057
12286
  /**
12058
12287
  * Show region form modal
12059
12288
  */
12060
- showRegionForm(bounds, area) {
12289
+ async showRegionForm(bounds, area) {
12290
+ const mapSources = await this.extractExtraMapSources();
12291
+ regionLogger.debug(`Discovered ${mapSources.length} extra tile sources from map`);
12061
12292
  const formOptions = {
12062
12293
  bounds,
12063
12294
  area,
@@ -12066,6 +12297,7 @@ class RegionControl {
12066
12297
  styleUrl: this.options.styleUrl,
12067
12298
  accessToken: this.options.accessToken,
12068
12299
  onThemeToggle: () => this.options.onRegionSaved?.(),
12300
+ mapSources,
12069
12301
  };
12070
12302
  this.regionFormModal = new RegionFormModal(formOptions);
12071
12303
  const modal = this.regionFormModal.show();
@@ -12229,6 +12461,7 @@ class DownloadManager {
12229
12461
  minZoom: formData.minZoom,
12230
12462
  maxZoom: formData.maxZoom,
12231
12463
  styleUrl: formData.styleUrl,
12464
+ extraSources: formData.extraSources,
12232
12465
  };
12233
12466
  const regionId = regionConfig.id;
12234
12467
  try {