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 +45 -89
- package/dist/index.esm.js +234 -1
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +234 -1
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +234 -1
- package/dist/index.umd.js.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/region.d.ts +28 -0
- package/dist/ui/controls/regionControl.d.ts +11 -0
- package/dist/ui/modals/regionFormModal.d.ts +30 -0
- package/dist/ui/translations/ar.d.ts +8 -0
- package/dist/ui/translations/en.d.ts +8 -0
- package/package.json +3 -3
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!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
530
|
+
**v0.5.2:** CLI command (`npx map-gl-offline init`), Vite plugin for Service Worker, Mapbox GL example app.
|
|
570
531
|
|
|
571
|
-
|
|
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 {
|