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/dist/index.js
CHANGED
|
@@ -5379,6 +5379,21 @@ class RegionService {
|
|
|
5379
5379
|
if (!Array.isArray(styleEntry.regions)) {
|
|
5380
5380
|
styleEntry.regions = [];
|
|
5381
5381
|
}
|
|
5382
|
+
// Inject extra sources into the style so they get patched for offline use
|
|
5383
|
+
if (region.extraSources && region.extraSources.length > 0) {
|
|
5384
|
+
for (const extra of region.extraSources) {
|
|
5385
|
+
if (!styleEntry.style.sources[extra.id]) {
|
|
5386
|
+
styleEntry.style.sources[extra.id] = {
|
|
5387
|
+
type: extra.type || 'vector',
|
|
5388
|
+
tiles: extra.tiles,
|
|
5389
|
+
...(extra.minzoom !== undefined ? { minzoom: extra.minzoom } : {}),
|
|
5390
|
+
...(extra.maxzoom !== undefined ? { maxzoom: extra.maxzoom } : {}),
|
|
5391
|
+
...(extra.attribution ? { attribution: extra.attribution } : {}),
|
|
5392
|
+
};
|
|
5393
|
+
regionLogger$1.debug(`Injected extra source into style: ${extra.id}`);
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5382
5397
|
// Patch style for offline use with the region's maxZoom and tileExtension
|
|
5383
5398
|
// Pass styleId for sprites since they're stored with the style ID, not region ID
|
|
5384
5399
|
patchStyleForOffline(styleEntry.style, region.id, region.maxZoom, region.tileExtension, styleId);
|
|
@@ -5541,6 +5556,21 @@ class TileService {
|
|
|
5541
5556
|
if (!style?.sources || Object.keys(style.sources).length === 0) {
|
|
5542
5557
|
throw new Error('Style does not contain any sources to download tiles from');
|
|
5543
5558
|
}
|
|
5559
|
+
// Inject extra sources from the region into the style for downloading
|
|
5560
|
+
if (region.extraSources && region.extraSources.length > 0) {
|
|
5561
|
+
for (const extra of region.extraSources) {
|
|
5562
|
+
if (!style.sources[extra.id]) {
|
|
5563
|
+
style.sources[extra.id] = {
|
|
5564
|
+
type: extra.type || 'vector',
|
|
5565
|
+
tiles: extra.tiles,
|
|
5566
|
+
...(extra.minzoom !== undefined ? { minzoom: extra.minzoom } : {}),
|
|
5567
|
+
...(extra.maxzoom !== undefined ? { maxzoom: extra.maxzoom } : {}),
|
|
5568
|
+
...(extra.attribution ? { attribution: extra.attribution } : {}),
|
|
5569
|
+
};
|
|
5570
|
+
tileLogger.debug(`Injected extra source: ${extra.id}`, extra.tiles);
|
|
5571
|
+
}
|
|
5572
|
+
}
|
|
5573
|
+
}
|
|
5544
5574
|
// Generate tile coordinates once for the region
|
|
5545
5575
|
const tileCoords = this.generateTileCoordinates(region);
|
|
5546
5576
|
tileLogger.debug('🔍 ABOUT TO CALL extractTileSources with style:', {
|
|
@@ -8100,6 +8130,14 @@ const en = {
|
|
|
8100
8130
|
'regionForm.downloadRegion': 'Download Region',
|
|
8101
8131
|
'regionForm.area': 'Area',
|
|
8102
8132
|
'regionForm.bounds': 'Bounds',
|
|
8133
|
+
'regionForm.extraSources': 'Additional Tile Sources',
|
|
8134
|
+
'regionForm.extraSourcesInfo': 'Select extra layers from the map to include in this offline region',
|
|
8135
|
+
'regionForm.noExtraSources': 'No additional tile sources found on the map',
|
|
8136
|
+
'regionForm.sourceType.vector': 'Vector',
|
|
8137
|
+
'regionForm.sourceType.raster': 'Raster',
|
|
8138
|
+
'regionForm.sourceType.raster-dem': 'Terrain',
|
|
8139
|
+
'regionForm.selectAll': 'Select All',
|
|
8140
|
+
'regionForm.deselectAll': 'Deselect All',
|
|
8103
8141
|
// Region List
|
|
8104
8142
|
'regionList.empty': 'No offline styles or regions found. Click "Add Region" to get started.',
|
|
8105
8143
|
'regionList.emptyFallback': 'No offline regions found. Click "Add Region" to get started.',
|
|
@@ -8315,6 +8353,14 @@ const ar = {
|
|
|
8315
8353
|
'regionForm.downloadRegion': 'تحميل المنطقة',
|
|
8316
8354
|
'regionForm.area': 'المساحة',
|
|
8317
8355
|
'regionForm.bounds': 'الحدود',
|
|
8356
|
+
'regionForm.extraSources': 'مصادر بلاطات إضافية',
|
|
8357
|
+
'regionForm.extraSourcesInfo': 'اختر طبقات إضافية من الخريطة لتضمينها في هذه المنطقة',
|
|
8358
|
+
'regionForm.noExtraSources': 'لم يتم العثور على مصادر بلاطات إضافية على الخريطة',
|
|
8359
|
+
'regionForm.sourceType.vector': 'متجه',
|
|
8360
|
+
'regionForm.sourceType.raster': 'نقطي',
|
|
8361
|
+
'regionForm.sourceType.raster-dem': 'تضاريس',
|
|
8362
|
+
'regionForm.selectAll': 'تحديد الكل',
|
|
8363
|
+
'regionForm.deselectAll': 'إلغاء تحديد الكل',
|
|
8318
8364
|
// Region List - قائمة المناطق
|
|
8319
8365
|
'regionList.empty': 'لم يتم العثور على أنماط أو مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
|
|
8320
8366
|
'regionList.emptyFallback': 'لم يتم العثور على مناطق غير متصلة. انقر على "إضافة منطقة" للبدء.',
|
|
@@ -11667,6 +11713,8 @@ class RegionFormModal {
|
|
|
11667
11713
|
providerSelect;
|
|
11668
11714
|
accessTokenInput;
|
|
11669
11715
|
accessTokenGroup;
|
|
11716
|
+
// Extra sources picker
|
|
11717
|
+
sourceCheckboxes = new Map();
|
|
11670
11718
|
constructor(options) {
|
|
11671
11719
|
this.options = options;
|
|
11672
11720
|
}
|
|
@@ -11838,10 +11886,124 @@ class RegionFormModal {
|
|
|
11838
11886
|
`;
|
|
11839
11887
|
this.accessTokenGroup.appendChild(tokenHelp);
|
|
11840
11888
|
form.appendChild(this.accessTokenGroup);
|
|
11889
|
+
// Extra sources picker
|
|
11890
|
+
if (this.options.mapSources && this.options.mapSources.length > 0) {
|
|
11891
|
+
const sourcesSection = this.createSourcesPicker(this.options.mapSources);
|
|
11892
|
+
form.appendChild(sourcesSection);
|
|
11893
|
+
}
|
|
11841
11894
|
// Initialize provider detection
|
|
11842
11895
|
this.detectProviderFromUrl();
|
|
11843
11896
|
return form;
|
|
11844
11897
|
}
|
|
11898
|
+
/**
|
|
11899
|
+
* Create the extra sources picker section
|
|
11900
|
+
*/
|
|
11901
|
+
createSourcesPicker(sources) {
|
|
11902
|
+
const group = document.createElement('div');
|
|
11903
|
+
group.className = 'flex flex-col gap-3';
|
|
11904
|
+
// Header with label and select all/deselect all
|
|
11905
|
+
const header = document.createElement('div');
|
|
11906
|
+
header.className = 'flex items-center justify-between';
|
|
11907
|
+
header.innerHTML = `
|
|
11908
|
+
<label class="block text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
11909
|
+
${t('regionForm.extraSources')}
|
|
11910
|
+
</label>
|
|
11911
|
+
`;
|
|
11912
|
+
const toggleBtn = document.createElement('button');
|
|
11913
|
+
toggleBtn.type = 'button';
|
|
11914
|
+
toggleBtn.className =
|
|
11915
|
+
'text-xs text-primary-600 dark:text-primary-400 hover:underline cursor-pointer font-medium';
|
|
11916
|
+
toggleBtn.textContent = t('regionForm.selectAll');
|
|
11917
|
+
let allSelected = false;
|
|
11918
|
+
toggleBtn.addEventListener('click', () => {
|
|
11919
|
+
allSelected = !allSelected;
|
|
11920
|
+
for (const { checkbox } of this.sourceCheckboxes.values()) {
|
|
11921
|
+
checkbox.checked = allSelected;
|
|
11922
|
+
}
|
|
11923
|
+
toggleBtn.textContent = allSelected ? t('regionForm.deselectAll') : t('regionForm.selectAll');
|
|
11924
|
+
});
|
|
11925
|
+
header.appendChild(toggleBtn);
|
|
11926
|
+
group.appendChild(header);
|
|
11927
|
+
// Info text
|
|
11928
|
+
const info = document.createElement('div');
|
|
11929
|
+
info.className = 'text-xs text-gray-500 dark:text-gray-400 flex items-center gap-1';
|
|
11930
|
+
info.innerHTML = `${icons.infoCircle({ size: 12 })} ${t('regionForm.extraSourcesInfo')}`;
|
|
11931
|
+
group.appendChild(info);
|
|
11932
|
+
// Source list
|
|
11933
|
+
const list = document.createElement('div');
|
|
11934
|
+
list.className =
|
|
11935
|
+
'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';
|
|
11936
|
+
for (const source of sources) {
|
|
11937
|
+
const row = document.createElement('label');
|
|
11938
|
+
row.className =
|
|
11939
|
+
'flex items-center gap-3 p-2 rounded-lg hover:bg-white/60 dark:hover:bg-gray-700/40 cursor-pointer transition-colors';
|
|
11940
|
+
const checkbox = document.createElement('input');
|
|
11941
|
+
checkbox.type = 'checkbox';
|
|
11942
|
+
checkbox.className =
|
|
11943
|
+
'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';
|
|
11944
|
+
checkbox.value = source.id;
|
|
11945
|
+
row.appendChild(checkbox);
|
|
11946
|
+
this.sourceCheckboxes.set(source.id, { checkbox, source });
|
|
11947
|
+
const details = document.createElement('div');
|
|
11948
|
+
details.className = 'flex flex-col min-w-0';
|
|
11949
|
+
const nameRow = document.createElement('div');
|
|
11950
|
+
nameRow.className = 'flex items-center gap-2';
|
|
11951
|
+
const name = document.createElement('span');
|
|
11952
|
+
name.className = 'text-sm font-medium text-gray-800 dark:text-gray-200 truncate';
|
|
11953
|
+
name.textContent = source.id;
|
|
11954
|
+
nameRow.appendChild(name);
|
|
11955
|
+
const typeKey = `regionForm.sourceType.${source.type}`;
|
|
11956
|
+
const badge = document.createElement('span');
|
|
11957
|
+
badge.className =
|
|
11958
|
+
source.type === 'vector'
|
|
11959
|
+
? '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'
|
|
11960
|
+
: source.type === 'raster'
|
|
11961
|
+
? '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'
|
|
11962
|
+
: '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';
|
|
11963
|
+
badge.textContent = t(typeKey);
|
|
11964
|
+
nameRow.appendChild(badge);
|
|
11965
|
+
details.appendChild(nameRow);
|
|
11966
|
+
// Zoom range info
|
|
11967
|
+
const zoomMin = source.minzoom ?? 0;
|
|
11968
|
+
const zoomMax = source.maxzoom ?? 22;
|
|
11969
|
+
const meta = document.createElement('span');
|
|
11970
|
+
meta.className = 'text-[11px] text-gray-500 dark:text-gray-400 truncate';
|
|
11971
|
+
meta.textContent = `z${zoomMin}-${zoomMax}`;
|
|
11972
|
+
if (source.tiles.length > 0) {
|
|
11973
|
+
try {
|
|
11974
|
+
const hostname = new URL(source.tiles[0]).hostname;
|
|
11975
|
+
meta.textContent += ` · ${escapeHtml$1(hostname)}`;
|
|
11976
|
+
}
|
|
11977
|
+
catch {
|
|
11978
|
+
// ignore invalid URLs
|
|
11979
|
+
}
|
|
11980
|
+
}
|
|
11981
|
+
details.appendChild(meta);
|
|
11982
|
+
row.appendChild(details);
|
|
11983
|
+
list.appendChild(row);
|
|
11984
|
+
}
|
|
11985
|
+
group.appendChild(list);
|
|
11986
|
+
return group;
|
|
11987
|
+
}
|
|
11988
|
+
/**
|
|
11989
|
+
* Get the selected extra sources from the picker
|
|
11990
|
+
*/
|
|
11991
|
+
getSelectedExtraSources() {
|
|
11992
|
+
const selected = [];
|
|
11993
|
+
for (const { checkbox, source } of this.sourceCheckboxes.values()) {
|
|
11994
|
+
if (checkbox.checked) {
|
|
11995
|
+
selected.push({
|
|
11996
|
+
id: source.id,
|
|
11997
|
+
type: source.type,
|
|
11998
|
+
tiles: source.tiles,
|
|
11999
|
+
minzoom: source.minzoom,
|
|
12000
|
+
maxzoom: source.maxzoom,
|
|
12001
|
+
attribution: source.attribution,
|
|
12002
|
+
});
|
|
12003
|
+
}
|
|
12004
|
+
}
|
|
12005
|
+
return selected;
|
|
12006
|
+
}
|
|
11845
12007
|
/**
|
|
11846
12008
|
* Handle style URL changes to auto-detect provider
|
|
11847
12009
|
*/
|
|
@@ -11928,6 +12090,7 @@ class RegionFormModal {
|
|
|
11928
12090
|
*/
|
|
11929
12091
|
async handleSave() {
|
|
11930
12092
|
try {
|
|
12093
|
+
const selectedSources = this.getSelectedExtraSources();
|
|
11931
12094
|
const formData = {
|
|
11932
12095
|
name: this.nameInput?.value || `Region ${Date.now()}`,
|
|
11933
12096
|
minZoom: parseInt(this.minZoomInput?.value || '1'),
|
|
@@ -11937,6 +12100,7 @@ class RegionFormModal {
|
|
|
11937
12100
|
// Enhanced Mapbox GL support
|
|
11938
12101
|
provider: this.providerSelect?.value,
|
|
11939
12102
|
accessToken: this.accessTokenInput?.value || undefined,
|
|
12103
|
+
extraSources: selectedSources.length > 0 ? selectedSources : undefined,
|
|
11940
12104
|
};
|
|
11941
12105
|
this.modal?.hide();
|
|
11942
12106
|
await this.options.onSave(formData);
|
|
@@ -12077,10 +12241,77 @@ class RegionControl {
|
|
|
12077
12241
|
this.polygonControl.triggerSave();
|
|
12078
12242
|
}
|
|
12079
12243
|
}
|
|
12244
|
+
/**
|
|
12245
|
+
* Get the source IDs that belong to the current style URL.
|
|
12246
|
+
* These are the sources the style already includes and will be downloaded automatically.
|
|
12247
|
+
*/
|
|
12248
|
+
async getStyleSourceIds() {
|
|
12249
|
+
try {
|
|
12250
|
+
const styleUrl = this.options.styleUrl;
|
|
12251
|
+
if (!styleUrl)
|
|
12252
|
+
return new Set();
|
|
12253
|
+
const response = await fetch(styleUrl);
|
|
12254
|
+
if (!response.ok)
|
|
12255
|
+
return new Set();
|
|
12256
|
+
const styleJson = (await response.json());
|
|
12257
|
+
if (styleJson.sources && typeof styleJson.sources === 'object') {
|
|
12258
|
+
return new Set(Object.keys(styleJson.sources));
|
|
12259
|
+
}
|
|
12260
|
+
}
|
|
12261
|
+
catch (error) {
|
|
12262
|
+
regionLogger.warn('Failed to fetch style for source filtering:', error);
|
|
12263
|
+
}
|
|
12264
|
+
return new Set();
|
|
12265
|
+
}
|
|
12266
|
+
/**
|
|
12267
|
+
* Extract tile sources from the live map instance that are NOT part of the current style.
|
|
12268
|
+
* Returns vector, raster, and raster-dem sources that have tile URLs,
|
|
12269
|
+
* excluding sources that already belong to the style (those are downloaded automatically).
|
|
12270
|
+
*/
|
|
12271
|
+
async extractExtraMapSources() {
|
|
12272
|
+
try {
|
|
12273
|
+
const style = this.map?.getStyle?.();
|
|
12274
|
+
if (!style || !style.sources || typeof style.sources !== 'object')
|
|
12275
|
+
return [];
|
|
12276
|
+
// Fetch the original style's source IDs so we can exclude them
|
|
12277
|
+
const styleSourceIds = await this.getStyleSourceIds();
|
|
12278
|
+
const sources = [];
|
|
12279
|
+
for (const [id, raw] of Object.entries(style.sources)) {
|
|
12280
|
+
// Skip sources that belong to the style itself
|
|
12281
|
+
if (styleSourceIds.has(id))
|
|
12282
|
+
continue;
|
|
12283
|
+
const type = raw.type;
|
|
12284
|
+
if (type !== 'vector' && type !== 'raster' && type !== 'raster-dem')
|
|
12285
|
+
continue;
|
|
12286
|
+
const tiles = raw.tiles;
|
|
12287
|
+
// Only include sources that have resolvable tile URLs (not idb://)
|
|
12288
|
+
if (!tiles || !Array.isArray(tiles) || tiles.length === 0)
|
|
12289
|
+
continue;
|
|
12290
|
+
const hasHttpTiles = tiles.some((t) => t.startsWith('http://') || t.startsWith('https://'));
|
|
12291
|
+
if (!hasHttpTiles)
|
|
12292
|
+
continue;
|
|
12293
|
+
sources.push({
|
|
12294
|
+
id,
|
|
12295
|
+
type: type,
|
|
12296
|
+
tiles: tiles.filter((t) => t.startsWith('http://') || t.startsWith('https://')),
|
|
12297
|
+
minzoom: typeof raw.minzoom === 'number' ? raw.minzoom : undefined,
|
|
12298
|
+
maxzoom: typeof raw.maxzoom === 'number' ? raw.maxzoom : undefined,
|
|
12299
|
+
attribution: typeof raw.attribution === 'string' ? raw.attribution : undefined,
|
|
12300
|
+
});
|
|
12301
|
+
}
|
|
12302
|
+
return sources;
|
|
12303
|
+
}
|
|
12304
|
+
catch (error) {
|
|
12305
|
+
regionLogger.warn('Failed to extract map sources:', error);
|
|
12306
|
+
return [];
|
|
12307
|
+
}
|
|
12308
|
+
}
|
|
12080
12309
|
/**
|
|
12081
12310
|
* Show region form modal
|
|
12082
12311
|
*/
|
|
12083
|
-
showRegionForm(bounds, area) {
|
|
12312
|
+
async showRegionForm(bounds, area) {
|
|
12313
|
+
const mapSources = await this.extractExtraMapSources();
|
|
12314
|
+
regionLogger.debug(`Discovered ${mapSources.length} extra tile sources from map`);
|
|
12084
12315
|
const formOptions = {
|
|
12085
12316
|
bounds,
|
|
12086
12317
|
area,
|
|
@@ -12089,6 +12320,7 @@ class RegionControl {
|
|
|
12089
12320
|
styleUrl: this.options.styleUrl,
|
|
12090
12321
|
accessToken: this.options.accessToken,
|
|
12091
12322
|
onThemeToggle: () => this.options.onRegionSaved?.(),
|
|
12323
|
+
mapSources,
|
|
12092
12324
|
};
|
|
12093
12325
|
this.regionFormModal = new RegionFormModal(formOptions);
|
|
12094
12326
|
const modal = this.regionFormModal.show();
|
|
@@ -12252,6 +12484,7 @@ class DownloadManager {
|
|
|
12252
12484
|
minZoom: formData.minZoom,
|
|
12253
12485
|
maxZoom: formData.maxZoom,
|
|
12254
12486
|
styleUrl: formData.styleUrl,
|
|
12487
|
+
extraSources: formData.extraSources,
|
|
12255
12488
|
};
|
|
12256
12489
|
const regionId = regionConfig.id;
|
|
12257
12490
|
try {
|