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.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 {
|