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/dist/index.umd.js CHANGED
@@ -5373,6 +5373,21 @@
5373
5373
  if (!Array.isArray(styleEntry.regions)) {
5374
5374
  styleEntry.regions = [];
5375
5375
  }
5376
+ // Inject extra sources into the style so they get patched for offline use
5377
+ if (region.extraSources && region.extraSources.length > 0) {
5378
+ for (const extra of region.extraSources) {
5379
+ if (!styleEntry.style.sources[extra.id]) {
5380
+ styleEntry.style.sources[extra.id] = {
5381
+ type: extra.type || 'vector',
5382
+ tiles: extra.tiles,
5383
+ ...(extra.minzoom !== undefined ? { minzoom: extra.minzoom } : {}),
5384
+ ...(extra.maxzoom !== undefined ? { maxzoom: extra.maxzoom } : {}),
5385
+ ...(extra.attribution ? { attribution: extra.attribution } : {}),
5386
+ };
5387
+ regionLogger$1.debug(`Injected extra source into style: ${extra.id}`);
5388
+ }
5389
+ }
5390
+ }
5376
5391
  // Patch style for offline use with the region's maxZoom and tileExtension
5377
5392
  // Pass styleId for sprites since they're stored with the style ID, not region ID
5378
5393
  patchStyleForOffline(styleEntry.style, region.id, region.maxZoom, region.tileExtension, styleId);
@@ -5535,6 +5550,21 @@
5535
5550
  if (!style?.sources || Object.keys(style.sources).length === 0) {
5536
5551
  throw new Error('Style does not contain any sources to download tiles from');
5537
5552
  }
5553
+ // Inject extra sources from the region into the style for downloading
5554
+ if (region.extraSources && region.extraSources.length > 0) {
5555
+ for (const extra of region.extraSources) {
5556
+ if (!style.sources[extra.id]) {
5557
+ style.sources[extra.id] = {
5558
+ type: extra.type || 'vector',
5559
+ tiles: extra.tiles,
5560
+ ...(extra.minzoom !== undefined ? { minzoom: extra.minzoom } : {}),
5561
+ ...(extra.maxzoom !== undefined ? { maxzoom: extra.maxzoom } : {}),
5562
+ ...(extra.attribution ? { attribution: extra.attribution } : {}),
5563
+ };
5564
+ tileLogger.debug(`Injected extra source: ${extra.id}`, extra.tiles);
5565
+ }
5566
+ }
5567
+ }
5538
5568
  // Generate tile coordinates once for the region
5539
5569
  const tileCoords = this.generateTileCoordinates(region);
5540
5570
  tileLogger.debug('🔍 ABOUT TO CALL extractTileSources with style:', {
@@ -8094,6 +8124,14 @@
8094
8124
  'regionForm.downloadRegion': 'Download Region',
8095
8125
  'regionForm.area': 'Area',
8096
8126
  'regionForm.bounds': 'Bounds',
8127
+ 'regionForm.extraSources': 'Additional Tile Sources',
8128
+ 'regionForm.extraSourcesInfo': 'Select extra layers from the map to include in this offline region',
8129
+ 'regionForm.noExtraSources': 'No additional tile sources found on the map',
8130
+ 'regionForm.sourceType.vector': 'Vector',
8131
+ 'regionForm.sourceType.raster': 'Raster',
8132
+ 'regionForm.sourceType.raster-dem': 'Terrain',
8133
+ 'regionForm.selectAll': 'Select All',
8134
+ 'regionForm.deselectAll': 'Deselect All',
8097
8135
  // Region List
8098
8136
  'regionList.empty': 'No offline styles or regions found. Click "Add Region" to get started.',
8099
8137
  'regionList.emptyFallback': 'No offline regions found. Click "Add Region" to get started.',
@@ -8309,6 +8347,14 @@
8309
8347
  'regionForm.downloadRegion': 'تحميل المنطقة',
8310
8348
  'regionForm.area': 'المساحة',
8311
8349
  'regionForm.bounds': 'الحدود',
8350
+ 'regionForm.extraSources': 'مصادر بلاطات إضافية',
8351
+ 'regionForm.extraSourcesInfo': 'اختر طبقات إضافية من الخريطة لتضمينها في هذه المنطقة',
8352
+ 'regionForm.noExtraSources': 'لم يتم العثور على مصادر بلاطات إضافية على الخريطة',
8353
+ 'regionForm.sourceType.vector': 'متجه',
8354
+ 'regionForm.sourceType.raster': 'نقطي',
8355
+ 'regionForm.sourceType.raster-dem': 'تضاريس',
8356
+ 'regionForm.selectAll': 'تحديد الكل',
8357
+ 'regionForm.deselectAll': 'إلغاء تحديد الكل',
8312
8358
  // Region List - قائمة المناطق
8313
8359
  'regionList.empty': 'لم يتم العثور على أنماط أو مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
8314
8360
  'regionList.emptyFallback': 'لم يتم العثور على مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
@@ -11661,6 +11707,8 @@
11661
11707
  providerSelect;
11662
11708
  accessTokenInput;
11663
11709
  accessTokenGroup;
11710
+ // Extra sources picker
11711
+ sourceCheckboxes = new Map();
11664
11712
  constructor(options) {
11665
11713
  this.options = options;
11666
11714
  }
@@ -11832,10 +11880,124 @@
11832
11880
  `;
11833
11881
  this.accessTokenGroup.appendChild(tokenHelp);
11834
11882
  form.appendChild(this.accessTokenGroup);
11883
+ // Extra sources picker
11884
+ if (this.options.mapSources && this.options.mapSources.length > 0) {
11885
+ const sourcesSection = this.createSourcesPicker(this.options.mapSources);
11886
+ form.appendChild(sourcesSection);
11887
+ }
11835
11888
  // Initialize provider detection
11836
11889
  this.detectProviderFromUrl();
11837
11890
  return form;
11838
11891
  }
11892
+ /**
11893
+ * Create the extra sources picker section
11894
+ */
11895
+ createSourcesPicker(sources) {
11896
+ const group = document.createElement('div');
11897
+ group.className = 'flex flex-col gap-3';
11898
+ // Header with label and select all/deselect all
11899
+ const header = document.createElement('div');
11900
+ header.className = 'flex items-center justify-between';
11901
+ header.innerHTML = `
11902
+ <label class="block text-sm font-semibold text-gray-700 dark:text-gray-300">
11903
+ ${t('regionForm.extraSources')}
11904
+ </label>
11905
+ `;
11906
+ const toggleBtn = document.createElement('button');
11907
+ toggleBtn.type = 'button';
11908
+ toggleBtn.className =
11909
+ 'text-xs text-primary-600 dark:text-primary-400 hover:underline cursor-pointer font-medium';
11910
+ toggleBtn.textContent = t('regionForm.selectAll');
11911
+ let allSelected = false;
11912
+ toggleBtn.addEventListener('click', () => {
11913
+ allSelected = !allSelected;
11914
+ for (const { checkbox } of this.sourceCheckboxes.values()) {
11915
+ checkbox.checked = allSelected;
11916
+ }
11917
+ toggleBtn.textContent = allSelected ? t('regionForm.deselectAll') : t('regionForm.selectAll');
11918
+ });
11919
+ header.appendChild(toggleBtn);
11920
+ group.appendChild(header);
11921
+ // Info text
11922
+ const info = document.createElement('div');
11923
+ info.className = 'text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1';
11924
+ info.innerHTML = `${icons.infoCircle({ size: 12 })} ${t('regionForm.extraSourcesInfo')}`;
11925
+ group.appendChild(info);
11926
+ // Source list
11927
+ const list = document.createElement('div');
11928
+ list.className =
11929
+ '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';
11930
+ for (const source of sources) {
11931
+ const row = document.createElement('label');
11932
+ row.className =
11933
+ 'flex items-center gap-3 p-2 rounded-lg hover:bg-white/60 dark:hover:bg-gray-700/40 cursor-pointer transition-colors';
11934
+ const checkbox = document.createElement('input');
11935
+ checkbox.type = 'checkbox';
11936
+ checkbox.className =
11937
+ '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';
11938
+ checkbox.value = source.id;
11939
+ row.appendChild(checkbox);
11940
+ this.sourceCheckboxes.set(source.id, { checkbox, source });
11941
+ const details = document.createElement('div');
11942
+ details.className = 'flex flex-col min-w-0';
11943
+ const nameRow = document.createElement('div');
11944
+ nameRow.className = 'flex items-center gap-2';
11945
+ const name = document.createElement('span');
11946
+ name.className = 'text-sm font-medium text-gray-800 dark:text-gray-200 truncate';
11947
+ name.textContent = source.id;
11948
+ nameRow.appendChild(name);
11949
+ const typeKey = `regionForm.sourceType.${source.type}`;
11950
+ const badge = document.createElement('span');
11951
+ badge.className =
11952
+ source.type === 'vector'
11953
+ ? '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'
11954
+ : source.type === 'raster'
11955
+ ? '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'
11956
+ : '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';
11957
+ badge.textContent = t(typeKey);
11958
+ nameRow.appendChild(badge);
11959
+ details.appendChild(nameRow);
11960
+ // Zoom range info
11961
+ const zoomMin = source.minzoom ?? 0;
11962
+ const zoomMax = source.maxzoom ?? 22;
11963
+ const meta = document.createElement('span');
11964
+ meta.className = 'text-[11px] text-gray-500 dark:text-gray-400 truncate';
11965
+ meta.textContent = `z${zoomMin}-${zoomMax}`;
11966
+ if (source.tiles.length > 0) {
11967
+ try {
11968
+ const hostname = new URL(source.tiles[0]).hostname;
11969
+ meta.textContent += ` · ${escapeHtml$1(hostname)}`;
11970
+ }
11971
+ catch {
11972
+ // ignore invalid URLs
11973
+ }
11974
+ }
11975
+ details.appendChild(meta);
11976
+ row.appendChild(details);
11977
+ list.appendChild(row);
11978
+ }
11979
+ group.appendChild(list);
11980
+ return group;
11981
+ }
11982
+ /**
11983
+ * Get the selected extra sources from the picker
11984
+ */
11985
+ getSelectedExtraSources() {
11986
+ const selected = [];
11987
+ for (const { checkbox, source } of this.sourceCheckboxes.values()) {
11988
+ if (checkbox.checked) {
11989
+ selected.push({
11990
+ id: source.id,
11991
+ type: source.type,
11992
+ tiles: source.tiles,
11993
+ minzoom: source.minzoom,
11994
+ maxzoom: source.maxzoom,
11995
+ attribution: source.attribution,
11996
+ });
11997
+ }
11998
+ }
11999
+ return selected;
12000
+ }
11839
12001
  /**
11840
12002
  * Handle style URL changes to auto-detect provider
11841
12003
  */
@@ -11922,6 +12084,7 @@
11922
12084
  */
11923
12085
  async handleSave() {
11924
12086
  try {
12087
+ const selectedSources = this.getSelectedExtraSources();
11925
12088
  const formData = {
11926
12089
  name: this.nameInput?.value || `Region ${Date.now()}`,
11927
12090
  minZoom: parseInt(this.minZoomInput?.value || '1'),
@@ -11931,6 +12094,7 @@
11931
12094
  // Enhanced Mapbox GL support
11932
12095
  provider: this.providerSelect?.value,
11933
12096
  accessToken: this.accessTokenInput?.value || undefined,
12097
+ extraSources: selectedSources.length > 0 ? selectedSources : undefined,
11934
12098
  };
11935
12099
  this.modal?.hide();
11936
12100
  await this.options.onSave(formData);
@@ -12071,10 +12235,77 @@
12071
12235
  this.polygonControl.triggerSave();
12072
12236
  }
12073
12237
  }
12238
+ /**
12239
+ * Get the source IDs that belong to the current style URL.
12240
+ * These are the sources the style already includes and will be downloaded automatically.
12241
+ */
12242
+ async getStyleSourceIds() {
12243
+ try {
12244
+ const styleUrl = this.options.styleUrl;
12245
+ if (!styleUrl)
12246
+ return new Set();
12247
+ const response = await fetch(styleUrl);
12248
+ if (!response.ok)
12249
+ return new Set();
12250
+ const styleJson = (await response.json());
12251
+ if (styleJson.sources && typeof styleJson.sources === 'object') {
12252
+ return new Set(Object.keys(styleJson.sources));
12253
+ }
12254
+ }
12255
+ catch (error) {
12256
+ regionLogger.warn('Failed to fetch style for source filtering:', error);
12257
+ }
12258
+ return new Set();
12259
+ }
12260
+ /**
12261
+ * Extract tile sources from the live map instance that are NOT part of the current style.
12262
+ * Returns vector, raster, and raster-dem sources that have tile URLs,
12263
+ * excluding sources that already belong to the style (those are downloaded automatically).
12264
+ */
12265
+ async extractExtraMapSources() {
12266
+ try {
12267
+ const style = this.map?.getStyle?.();
12268
+ if (!style || !style.sources || typeof style.sources !== 'object')
12269
+ return [];
12270
+ // Fetch the original style's source IDs so we can exclude them
12271
+ const styleSourceIds = await this.getStyleSourceIds();
12272
+ const sources = [];
12273
+ for (const [id, raw] of Object.entries(style.sources)) {
12274
+ // Skip sources that belong to the style itself
12275
+ if (styleSourceIds.has(id))
12276
+ continue;
12277
+ const type = raw.type;
12278
+ if (type !== 'vector' && type !== 'raster' && type !== 'raster-dem')
12279
+ continue;
12280
+ const tiles = raw.tiles;
12281
+ // Only include sources that have resolvable tile URLs (not idb://)
12282
+ if (!tiles || !Array.isArray(tiles) || tiles.length === 0)
12283
+ continue;
12284
+ const hasHttpTiles = tiles.some((t) => t.startsWith('http://') || t.startsWith('https://'));
12285
+ if (!hasHttpTiles)
12286
+ continue;
12287
+ sources.push({
12288
+ id,
12289
+ type: type,
12290
+ tiles: tiles.filter((t) => t.startsWith('http://') || t.startsWith('https://')),
12291
+ minzoom: typeof raw.minzoom === 'number' ? raw.minzoom : undefined,
12292
+ maxzoom: typeof raw.maxzoom === 'number' ? raw.maxzoom : undefined,
12293
+ attribution: typeof raw.attribution === 'string' ? raw.attribution : undefined,
12294
+ });
12295
+ }
12296
+ return sources;
12297
+ }
12298
+ catch (error) {
12299
+ regionLogger.warn('Failed to extract map sources:', error);
12300
+ return [];
12301
+ }
12302
+ }
12074
12303
  /**
12075
12304
  * Show region form modal
12076
12305
  */
12077
- showRegionForm(bounds, area) {
12306
+ async showRegionForm(bounds, area) {
12307
+ const mapSources = await this.extractExtraMapSources();
12308
+ regionLogger.debug(`Discovered ${mapSources.length} extra tile sources from map`);
12078
12309
  const formOptions = {
12079
12310
  bounds,
12080
12311
  area,
@@ -12083,6 +12314,7 @@
12083
12314
  styleUrl: this.options.styleUrl,
12084
12315
  accessToken: this.options.accessToken,
12085
12316
  onThemeToggle: () => this.options.onRegionSaved?.(),
12317
+ mapSources,
12086
12318
  };
12087
12319
  this.regionFormModal = new RegionFormModal(formOptions);
12088
12320
  const modal = this.regionFormModal.show();
@@ -12246,6 +12478,7 @@
12246
12478
  minZoom: formData.minZoom,
12247
12479
  maxZoom: formData.maxZoom,
12248
12480
  styleUrl: formData.styleUrl,
12481
+ extraSources: formData.extraSources,
12249
12482
  };
12250
12483
  const regionId = regionConfig.id;
12251
12484
  try {