map-gl-offline 0.8.8 โ 0.9.0
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 +62 -191
- package/dist/idb-offline-sw.js +78 -11
- package/dist/index.d.ts +3 -3
- package/dist/index.esm.js +2065 -809
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +2072 -807
- package/dist/index.js.map +1 -1
- package/dist/index.umd.js +9502 -711
- package/dist/index.umd.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/managers/offlineMapManager/analyticsManagement.d.ts +1 -1
- package/dist/managers/offlineMapManager/base.d.ts +5 -5
- package/dist/managers/offlineMapManager/cleanupManagement.d.ts +6 -1
- package/dist/managers/offlineMapManager/importExportManagement.d.ts +1 -1
- package/dist/managers/offlineMapManager/maintenanceManagement.d.ts +1 -1
- package/dist/managers/offlineMapManager/regionManagement.d.ts +2 -2
- package/dist/managers/offlineMapManager/resourceManagement.d.ts +1 -1
- package/dist/managers/offlineMapManager/styleManagement.d.ts +1 -1
- package/dist/services/analyticsService.d.ts +1 -1
- package/dist/services/cleanupService.d.ts +15 -1
- package/dist/services/fontService.d.ts +1 -1
- package/dist/services/glyphService.d.ts +1 -1
- package/dist/services/importExportService.d.ts +1 -1
- package/dist/services/maintenanceService.d.ts +2 -2
- package/dist/services/modelService.d.ts +3 -1
- package/dist/services/regionService.d.ts +21 -11
- package/dist/services/resourceService.d.ts +2 -2
- package/dist/services/runtimeAssetService.d.ts +12 -0
- package/dist/services/spriteService.d.ts +1 -1
- package/dist/services/styleService.d.ts +14 -1
- package/dist/services/tileService.d.ts +1 -1
- package/dist/sheet/collar.d.ts +34 -0
- package/dist/storage/indexedDbManager.d.ts +1 -1
- package/dist/style.css +2 -2
- package/dist/sw/shared.d.ts +31 -3
- package/dist/types/font.d.ts +5 -1
- package/dist/types/import-export.d.ts +5 -0
- package/dist/types/maintenance.d.ts +3 -1
- package/dist/types/region.d.ts +29 -2
- package/dist/types/style.d.ts +4 -0
- package/dist/types/tile.d.ts +31 -10
- package/dist/ui/components/PanelActions.d.ts +1 -1
- package/dist/ui/components/PanelHeader.d.ts +1 -1
- package/dist/ui/components/RegionList.d.ts +1 -1
- package/dist/ui/components/shared/Button.d.ts +6 -0
- package/dist/ui/components/shared/LanguageSelector.d.ts +1 -1
- package/dist/ui/components/shared/MapControlButton.d.ts +1 -1
- package/dist/ui/components/shared/PanelContent.d.ts +3 -3
- package/dist/ui/components/shared/RegionDrawingTool.d.ts +2 -2
- package/dist/ui/controls/polygonControl.d.ts +7 -1
- package/dist/ui/controls/regionControl.d.ts +3 -3
- package/dist/ui/managers/ControlButtonManager.d.ts +1 -1
- package/dist/ui/managers/PanelManager.d.ts +16 -3
- package/dist/ui/managers/downloadManager.d.ts +7 -3
- package/dist/ui/modals/mbtilesModal.d.ts +1 -1
- package/dist/ui/modals/regionDetailsModal.d.ts +1 -1
- package/dist/ui/modals/regionFormModal.d.ts +11 -1
- package/dist/ui/offlineManagerControl.d.ts +4 -1
- package/dist/ui/translations/ar.d.ts +15 -1
- package/dist/ui/translations/en.d.ts +15 -1
- package/dist/ui/translations/index.d.ts +2 -0
- package/dist/utils/constants.d.ts +10 -3
- package/dist/utils/convertStyleForSW.d.ts +1 -1
- package/dist/utils/download.d.ts +1 -1
- package/dist/utils/formatting.d.ts +11 -0
- package/dist/utils/importResolver.d.ts +1 -1
- package/dist/utils/sqlJsLoader.d.ts +1 -2
- package/dist/utils/styleConfig.d.ts +39 -0
- package/dist/utils/styleProviderUtils.d.ts +21 -8
- package/dist/utils/styleUtils.d.ts +61 -10
- package/dist/utils/tileRange.d.ts +63 -0
- package/dist/utils/validation.d.ts +1 -1
- package/package.json +45 -40
package/README.md
CHANGED
|
@@ -1,111 +1,84 @@
|
|
|
1
|
-
#
|
|
1
|
+
# map-gl-offline
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/js/map-gl-offline)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://www.typescriptlang.org/)
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
Save a map area to the browser, then keep using it with no connection. Works with **MapLibre GL JS** and **Mapbox GL JS**, and stores everything in IndexedDB.
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
**[Documentation](https://map-gl-offline.netlify.app)** | **[Live demo](https://map-gl-offline-demo.netlify.app)** | **[Issues](https://github.com/muimsd/map-gl-offline/issues)**
|
|
10
9
|
|
|
11
|
-

|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- ๐จ **UI control** โ glassmorphic panel, dark/light themes, English/Arabic (RTL), polygon drawing
|
|
20
|
-
- ๐ ๏ธ **Programmatic API** โ `downloadRegion` with per-phase progress, full TypeScript types
|
|
12
|
+
- **Complete regions:** style, tiles, sprites, fonts and 3D models, including Mapbox Standard's terrain and buildings.
|
|
13
|
+
- **A control or an API:** add a ready-made panel where people draw an area and download it, or call `downloadRegion` yourself.
|
|
14
|
+
- **Accurate progress:** one percentage that only moves forward, with exact counts for each phase.
|
|
15
|
+
- **Storage management:** usage reports, expiry dates and automatic cleanup.
|
|
16
|
+
- **MBTiles:** export to and import from the format QGIS and tippecanoe read.
|
|
17
|
+
- **English and Arabic** in the control, including right-to-left layout.
|
|
21
18
|
|
|
22
19
|
## Install
|
|
23
20
|
|
|
24
21
|
```bash
|
|
25
|
-
npm install map-gl-offline
|
|
22
|
+
npm install map-gl-offline maplibre-gl
|
|
23
|
+
# or
|
|
24
|
+
npm install map-gl-offline mapbox-gl
|
|
26
25
|
```
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
```html
|
|
31
|
-
<script src="https://unpkg.com/map-gl-offline/dist/index.umd.js"></script>
|
|
32
|
-
<link rel="stylesheet" href="https://unpkg.com/map-gl-offline/style.css" />
|
|
33
|
-
```
|
|
27
|
+
Load only one renderer per page: [MapLibre GL 6 breaks Mapbox GL](https://map-gl-offline.netlify.app/docs/guides/troubleshooting#a-mapbox-map-is-blank) when they share one.
|
|
34
28
|
|
|
35
|
-
## Quick
|
|
29
|
+
## Quick start
|
|
36
30
|
|
|
37
31
|
### MapLibre GL JS
|
|
38
32
|
|
|
39
|
-
```
|
|
40
|
-
import maplibregl from 'maplibre-gl';
|
|
33
|
+
```ts
|
|
34
|
+
import * as maplibregl from 'maplibre-gl';
|
|
41
35
|
import { OfflineMapManager, OfflineManagerControl } from 'map-gl-offline';
|
|
42
36
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
43
37
|
import 'map-gl-offline/style.css';
|
|
44
38
|
|
|
45
|
-
const
|
|
46
|
-
container: 'map',
|
|
47
|
-
style: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
|
|
48
|
-
center: [-74.006, 40.7128],
|
|
49
|
-
zoom: 12,
|
|
50
|
-
});
|
|
39
|
+
const styleUrl = 'https://tiles.openfreemap.org/styles/liberty';
|
|
51
40
|
|
|
52
|
-
const
|
|
41
|
+
const map = new maplibregl.Map({ container: 'map', style: styleUrl });
|
|
53
42
|
|
|
54
|
-
map.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
map.addControl(control, 'top-right');
|
|
60
|
-
});
|
|
43
|
+
map.addControl(
|
|
44
|
+
new OfflineManagerControl(new OfflineMapManager(), { styleUrl, mapLib: maplibregl }),
|
|
45
|
+
'top-right'
|
|
46
|
+
);
|
|
61
47
|
```
|
|
62
48
|
|
|
49
|
+
With a bundler, MapLibre 6 also needs [its worker URL set](https://map-gl-offline.netlify.app/docs/getting-started#maplibre-gl-js).
|
|
50
|
+
|
|
63
51
|
### Mapbox GL JS
|
|
64
52
|
|
|
65
|
-
Mapbox GL
|
|
53
|
+
Mapbox GL serves stored tiles through a Service Worker, so copy its script to your `public/` folder first:
|
|
66
54
|
|
|
67
55
|
```bash
|
|
68
|
-
npx map-gl-offline init
|
|
69
|
-
# or add to vite.config.js:
|
|
70
|
-
# import { offlineSwPlugin } from 'map-gl-offline/vite-plugin';
|
|
71
|
-
# plugins: [offlineSwPlugin()]
|
|
72
|
-
# or manually: cp node_modules/map-gl-offline/dist/idb-offline-sw.js public/
|
|
56
|
+
npx map-gl-offline init
|
|
73
57
|
```
|
|
74
58
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```typescript
|
|
59
|
+
```ts
|
|
78
60
|
import mapboxgl from 'mapbox-gl';
|
|
79
61
|
import { OfflineMapManager, OfflineManagerControl } from 'map-gl-offline';
|
|
80
62
|
import 'mapbox-gl/dist/mapbox-gl.css';
|
|
81
63
|
import 'map-gl-offline/style.css';
|
|
82
64
|
|
|
83
65
|
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
|
|
66
|
+
const styleUrl = 'mapbox://styles/mapbox/standard';
|
|
84
67
|
|
|
85
|
-
const map = new mapboxgl.Map({
|
|
86
|
-
container: 'map',
|
|
87
|
-
style: 'mapbox://styles/mapbox/standard',
|
|
88
|
-
center: [-74.006, 40.7128],
|
|
89
|
-
zoom: 12,
|
|
90
|
-
});
|
|
68
|
+
const map = new mapboxgl.Map({ container: 'map', style: styleUrl });
|
|
91
69
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}),
|
|
99
|
-
'top-right',
|
|
100
|
-
),
|
|
70
|
+
map.addControl(
|
|
71
|
+
new OfflineManagerControl(new OfflineMapManager(), {
|
|
72
|
+
styleUrl,
|
|
73
|
+
accessToken: mapboxgl.accessToken,
|
|
74
|
+
}),
|
|
75
|
+
'top-right'
|
|
101
76
|
);
|
|
102
77
|
```
|
|
103
78
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`downloadRegion` runs the full pipeline (**style โ sprites โ glyphs โ models โ tiles โ metadata**) with per-phase progress. `addRegion` alone only stores metadata โ use `downloadRegion` to actually fetch assets.
|
|
79
|
+
### Download from code
|
|
107
80
|
|
|
108
|
-
```
|
|
81
|
+
```ts
|
|
109
82
|
import { OfflineMapManager } from 'map-gl-offline';
|
|
110
83
|
|
|
111
84
|
const offlineManager = new OfflineMapManager();
|
|
@@ -114,151 +87,49 @@ await offlineManager.downloadRegion(
|
|
|
114
87
|
{
|
|
115
88
|
id: 'downtown',
|
|
116
89
|
name: 'Downtown',
|
|
117
|
-
bounds: [
|
|
90
|
+
bounds: [
|
|
91
|
+
[-74.02, 40.7], // south-west: [longitude, latitude]
|
|
92
|
+
[-73.97, 40.75], // north-east
|
|
93
|
+
],
|
|
118
94
|
minZoom: 10,
|
|
119
95
|
maxZoom: 16,
|
|
120
|
-
styleUrl: 'https://
|
|
96
|
+
styleUrl: 'https://tiles.openfreemap.org/styles/liberty',
|
|
121
97
|
},
|
|
122
98
|
{
|
|
123
|
-
onProgress: ({ phase,
|
|
124
|
-
console.log(
|
|
99
|
+
onProgress: ({ overallPercentage, phase, completed, total }) => {
|
|
100
|
+
console.log(`${Math.round(overallPercentage)}%: ${phase} ${completed}/${total}`);
|
|
125
101
|
},
|
|
126
|
-
},
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
// Manage
|
|
130
|
-
await offlineManager.listStoredRegions();
|
|
131
|
-
await offlineManager.getStoredRegion('downtown');
|
|
132
|
-
await offlineManager.deleteRegion('downtown');
|
|
133
|
-
|
|
134
|
-
// Analytics & cleanup
|
|
135
|
-
await offlineManager.getComprehensiveStorageAnalytics();
|
|
136
|
-
await offlineManager.cleanupExpiredRegions();
|
|
137
|
-
await offlineManager.setupAutoCleanup({ intervalHours: 24, maxAge: 30 });
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### Multi-region downloads (global overview + city detail)
|
|
141
|
-
|
|
142
|
-
For app shipping use cases โ a low-zoom basemap of the world, plus high-zoom detail in the cities your users actually visit โ set `multipleRegions: true` on each region so the manager reuses the shared style/sprites/glyphs across downloads. The exported `BoundingBox` type keeps city lists from being widened to `number[][]`, so you can write the bounds inline without `as` casts.
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
import mapboxgl from 'mapbox-gl';
|
|
146
|
-
import {
|
|
147
|
-
OfflineMapManager,
|
|
148
|
-
type BoundingBox,
|
|
149
|
-
type DownloadRegionProgress,
|
|
150
|
-
} from 'map-gl-offline';
|
|
151
|
-
|
|
152
|
-
const offlineManager = new OfflineMapManager();
|
|
153
|
-
const STYLE_URL = 'mapbox://styles/mapbox/standard';
|
|
154
|
-
|
|
155
|
-
const opts = {
|
|
156
|
-
accessToken: mapboxgl.accessToken, // `string | null` is accepted โ no cast needed
|
|
157
|
-
onProgress: ({ phase, percentage, message }: DownloadRegionProgress) =>
|
|
158
|
-
console.log(`[${phase}] ${percentage.toFixed(1)}% ${message ?? ''}`),
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// 1) Whole planet, low zoom only (~5,500 tiles/source) โ countries, major cities
|
|
162
|
-
await offlineManager.downloadRegion(
|
|
163
|
-
{
|
|
164
|
-
id: 'global-overview',
|
|
165
|
-
name: 'Global overview',
|
|
166
|
-
bounds: [[-180, -85.0511], [180, 85.0511]], // ยฑ85.0511ยฐ = Web Mercator cutoff
|
|
167
|
-
minZoom: 0,
|
|
168
|
-
maxZoom: 6,
|
|
169
|
-
styleUrl: STYLE_URL,
|
|
170
|
-
multipleRegions: true,
|
|
171
|
-
},
|
|
172
|
-
opts,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// 2) High-detail per city โ tight bbox per place your users actually go
|
|
176
|
-
const cities: Array<{ id: string; name: string; bounds: BoundingBox }> = [
|
|
177
|
-
{ id: 'nyc', name: 'New York', bounds: [[-74.05, 40.68], [-73.90, 40.82]] },
|
|
178
|
-
{ id: 'london', name: 'London', bounds: [[-0.25, 51.43], [0.02, 51.57]] },
|
|
179
|
-
];
|
|
180
|
-
|
|
181
|
-
for (const city of cities) {
|
|
182
|
-
await offlineManager.downloadRegion(
|
|
183
|
-
{
|
|
184
|
-
id: city.id,
|
|
185
|
-
name: city.name,
|
|
186
|
-
bounds: city.bounds,
|
|
187
|
-
minZoom: 6, // overlaps the overview's maxZoom โ clean handoff, no seam
|
|
188
|
-
maxZoom: 14, // street-level detail
|
|
189
|
-
styleUrl: STYLE_URL,
|
|
190
|
-
multipleRegions: true,
|
|
191
|
-
},
|
|
192
|
-
opts,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
> **Don't download the whole globe at high zoom.** The tile count quadruples per zoom level โ `minZoom: 0, maxZoom: 15` for the whole planet is ~1.4 billion tiles per source, which will blow past IndexedDB quota and may violate provider TOS. The two-tier setup above gives countries-everywhere plus streets-where-it-matters for a few thousand tiles total.
|
|
198
|
-
|
|
199
|
-
### Sparse-source detection
|
|
200
|
-
|
|
201
|
-
For composite styles (e.g. Mapbox Standard) that reference sparse tilesets like `indoor-v3` or `landmark-pois-v1`, the tile downloader probes start/middle/end tiles per source and drops any that return majority-404. Disable with `tileOptions: { probeSourcesBeforeDownload: false }`.
|
|
202
|
-
|
|
203
|
-
Three Mapbox Standard sub-tilesets are sparse-by-design across the planet โ `mapbox.indoor-v3`, `mapbox.landmark-pois-v1`, `mapbox.procedural-buildings-v1`. Since 0.8.8 these are hard-skipped **before** the probe step so no network request is issued (and no 404s land in devtools). Opt out with `tileOptions: { skipKnownSparseSources: false }` to run them through the probe path instead.
|
|
204
|
-
|
|
205
|
-
### Recovering from an incompatible DB
|
|
206
|
-
|
|
207
|
-
If another app on the same origin has created `offline-map-db` at a newer version, `dbPromise` throws a typed error. Offer a reset UX:
|
|
208
|
-
|
|
209
|
-
```typescript
|
|
210
|
-
import { dbPromise, OfflineMapDBVersionError, resetOfflineMapDB } from 'map-gl-offline';
|
|
211
|
-
|
|
212
|
-
try {
|
|
213
|
-
await dbPromise;
|
|
214
|
-
} catch (err) {
|
|
215
|
-
if (err instanceof OfflineMapDBVersionError) {
|
|
216
|
-
if (confirm('Offline storage is incompatible. Clear it?')) {
|
|
217
|
-
await resetOfflineMapDB(); // destructive
|
|
218
|
-
location.reload();
|
|
219
|
-
}
|
|
220
102
|
}
|
|
221
|
-
|
|
103
|
+
);
|
|
222
104
|
```
|
|
223
105
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
## API at a glance
|
|
227
|
-
|
|
228
|
-
- **Regions** โ `downloadRegion`, `loadRegion`, `addRegion`, `getStoredRegion`, `listStoredRegions`, `listRegions`, `deleteRegion`
|
|
229
|
-
- **Analytics** โ `getComprehensiveStorageAnalytics`, `getRegionAnalytics`, `getTileStats`, `getFontStats`, `getSpriteStats`, `getGlyphStats`
|
|
230
|
-
- **Cleanup** โ `cleanupExpiredRegions`, `performSmartCleanup`, `cleanupOld{Fonts,Sprites,Glyphs}`, `verifyAndRepair{Fonts,Sprites,Glyphs}`, `setupAutoCleanup`, `performCompleteMaintenance`
|
|
231
|
-
- **Import / Export** โ `exportRegionAsMBTiles`, `importRegion`, `downloadExportedRegion` (binary SQLite MBTiles, QGIS/tippecanoe-compatible)
|
|
232
|
-
- **Storage utilities** โ `dbPromise`, `OfflineMapDBVersionError`, `resetOfflineMapDB`, `loadAllStoredRegions`, `resourceKeyBelongsToStyle`
|
|
106
|
+
Each zoom level has about four times the tiles of the one before, so keep high zooms to small areas. On Mapbox Standard, buildings start at zoom 15.
|
|
233
107
|
|
|
234
|
-
|
|
108
|
+
## Documentation
|
|
235
109
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
110
|
+
- [Getting started](https://map-gl-offline.netlify.app/docs/getting-started)
|
|
111
|
+
- [Downloading regions](https://map-gl-offline.netlify.app/docs/guides/downloading-regions)
|
|
112
|
+
- [Mapbox GL and Standard](https://map-gl-offline.netlify.app/docs/guides/mapbox)
|
|
113
|
+
- [The offline control](https://map-gl-offline.netlify.app/docs/guides/ui-control)
|
|
114
|
+
- [Storage and cleanup](https://map-gl-offline.netlify.app/docs/guides/storage)
|
|
115
|
+
- [MBTiles](https://map-gl-offline.netlify.app/docs/guides/mbtiles)
|
|
116
|
+
- [Troubleshooting](https://map-gl-offline.netlify.app/docs/guides/troubleshooting)
|
|
117
|
+
- [API reference](https://map-gl-offline.netlify.app/docs/api-reference)
|
|
239
118
|
|
|
240
119
|
## Contributing
|
|
241
120
|
|
|
242
121
|
```bash
|
|
243
122
|
git clone https://github.com/muimsd/map-gl-offline.git
|
|
244
|
-
cd map-gl-offline
|
|
245
|
-
npm
|
|
246
|
-
npm
|
|
247
|
-
npm
|
|
123
|
+
cd map-gl-offline
|
|
124
|
+
npm install
|
|
125
|
+
npm run dev # The demo
|
|
126
|
+
npm test # Unit tests
|
|
248
127
|
```
|
|
249
128
|
|
|
250
|
-
|
|
129
|
+
See [Architecture](https://map-gl-offline.netlify.app/docs/architecture) for how it fits together, and the [changelog](CHANGELOG.md) for release notes. Issues and pull requests are welcome.
|
|
251
130
|
|
|
252
131
|
## License
|
|
253
132
|
|
|
254
133
|
MIT ยฉ [Muhammad Imran Siddique](https://github.com/muimsd)
|
|
255
134
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<div align="center">
|
|
259
|
-
|
|
260
|
-
[๐ Docs](https://map-gl-offline.netlify.app) ยท [๐ฎ Demo](https://map-gl-offline-demo.netlify.app) ยท [โญ GitHub](https://github.com/muimsd/map-gl-offline)
|
|
261
|
-
|
|
262
|
-
<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: 45px;"></a>
|
|
263
|
-
|
|
264
|
-
</div>
|
|
135
|
+
If this project helps you, you can [buy me a coffee](https://www.buymeacoffee.com/muimsd).
|
package/dist/idb-offline-sw.js
CHANGED
|
@@ -9,7 +9,29 @@
|
|
|
9
9
|
(() => {
|
|
10
10
|
// src/sw/shared.ts
|
|
11
11
|
var OFFLINE_PREFIX = "/__offline__/";
|
|
12
|
+
function safeDecodeURIComponent(value) {
|
|
13
|
+
try {
|
|
14
|
+
return decodeURIComponent(value);
|
|
15
|
+
} catch {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
12
19
|
var DB_NAME = "offline-map-db";
|
|
20
|
+
var MAPBOX_RUNTIME_ORIGIN = "https://api.mapbox.com";
|
|
21
|
+
var MAPBOX_RUNTIME_CACHE = "map-gl-offline-runtime-v1";
|
|
22
|
+
var MAPBOX_RUNTIME_ASSETS = [
|
|
23
|
+
"/mapbox-gl-js/draco_decoder_gltf_v1.5.6.wasm",
|
|
24
|
+
"/mapbox-gl-js/meshopt_base_v1.2.wasm",
|
|
25
|
+
"/mapbox-gl-js/meshopt_simd_v1.2.wasm",
|
|
26
|
+
"/mapbox-gl-js/rtl_text_v1.0.0.wasm",
|
|
27
|
+
"/mapbox-gl-js/building-gen/building_gen_v1.2.6.wasm",
|
|
28
|
+
"/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.3.0/mapbox-gl-rtl-text.js"
|
|
29
|
+
];
|
|
30
|
+
function isMapboxRuntimeAsset(url) {
|
|
31
|
+
if (!url.startsWith(`${MAPBOX_RUNTIME_ORIGIN}/`)) return false;
|
|
32
|
+
const path = url.slice(MAPBOX_RUNTIME_ORIGIN.length).split(/[?#]/)[0];
|
|
33
|
+
return MAPBOX_RUNTIME_ASSETS.includes(path);
|
|
34
|
+
}
|
|
13
35
|
function makeTileKey(x, y, z, styleId, sourceId, ext) {
|
|
14
36
|
return `${styleId}:${sourceId}:${z}:${x}:${y}.${ext}`;
|
|
15
37
|
}
|
|
@@ -43,6 +65,33 @@
|
|
|
43
65
|
const fontstacks = fontstackPart.split(",").map((f) => f.trim()).filter(Boolean);
|
|
44
66
|
return { fontstacks, rangePart };
|
|
45
67
|
}
|
|
68
|
+
function readVarint(bytes, start) {
|
|
69
|
+
let value = 0;
|
|
70
|
+
let multiplier = 1;
|
|
71
|
+
for (let pos = start; pos < bytes.length && pos < start + 5; pos++) {
|
|
72
|
+
value += (bytes[pos] & 127) * multiplier;
|
|
73
|
+
if (bytes[pos] < 128) return { value, next: pos + 1 };
|
|
74
|
+
multiplier *= 128;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function isEmptyGlyphRange(data) {
|
|
79
|
+
const bytes = new Uint8Array(data);
|
|
80
|
+
if (bytes[0] !== 10) return false;
|
|
81
|
+
const stack = readVarint(bytes, 1);
|
|
82
|
+
if (!stack) return false;
|
|
83
|
+
const end = stack.next + stack.value;
|
|
84
|
+
if (end !== bytes.length) return false;
|
|
85
|
+
let pos = stack.next;
|
|
86
|
+
while (pos < end) {
|
|
87
|
+
const tag = bytes[pos];
|
|
88
|
+
if ((tag & 7) !== 2 || tag >> 3 === 3) return false;
|
|
89
|
+
const length = readVarint(bytes, pos + 1);
|
|
90
|
+
if (!length) return false;
|
|
91
|
+
pos = length.next + length.value;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
46
95
|
function glyphCandidateKeys(actualStyleId, downloadId, fontstack, rangePart) {
|
|
47
96
|
const glyphPath = `${fontstack}/${rangePart}`;
|
|
48
97
|
const normalizedPath = glyphPath.endsWith(".pbf") ? glyphPath : `${glyphPath}.pbf`;
|
|
@@ -175,6 +224,11 @@
|
|
|
175
224
|
return cached.styleEntry;
|
|
176
225
|
}
|
|
177
226
|
try {
|
|
227
|
+
const direct = await idbGet(db, "styles", regionId);
|
|
228
|
+
if (direct) {
|
|
229
|
+
regionToStyleCache.set(regionId, { styleEntry: direct, ts: Date.now() });
|
|
230
|
+
return direct;
|
|
231
|
+
}
|
|
178
232
|
const all = await idbGetAll(db, "styles");
|
|
179
233
|
const hit = findStyleByRegionIdIn(all, regionId);
|
|
180
234
|
if (hit) {
|
|
@@ -224,7 +278,7 @@
|
|
|
224
278
|
const yExt = rest[rest.length - 1];
|
|
225
279
|
const x = parseInt(rest[rest.length - 2], 10);
|
|
226
280
|
const z = Math.floor(parseFloat(rest[rest.length - 3]));
|
|
227
|
-
const sourceKey = rest.slice(0, rest.length - 3).join("/");
|
|
281
|
+
const sourceKey = safeDecodeURIComponent(rest.slice(0, rest.length - 3).join("/"));
|
|
228
282
|
const parsed = parseTileYExt(yExt);
|
|
229
283
|
if (!parsed || Number.isNaN(x) || Number.isNaN(z)) {
|
|
230
284
|
return new Response("Invalid tile coordinates", { status: 400 });
|
|
@@ -242,24 +296,27 @@
|
|
|
242
296
|
async function handleGlyph(db, downloadId, rest) {
|
|
243
297
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
244
298
|
const styleId = styleEntry?.key ?? downloadId;
|
|
245
|
-
const { fontstacks, rangePart } = parseGlyphPath(
|
|
299
|
+
const { fontstacks, rangePart } = parseGlyphPath(safeDecodeURIComponent(rest.join("/")));
|
|
300
|
+
const glyphResponse = (data) => new Response(data, {
|
|
301
|
+
status: 200,
|
|
302
|
+
headers: { "Content-Type": "application/x-protobuf" }
|
|
303
|
+
});
|
|
304
|
+
let emptyRange;
|
|
246
305
|
for (const fontstack of fontstacks) {
|
|
247
306
|
for (const key of glyphCandidateKeys(styleId, downloadId, fontstack, rangePart)) {
|
|
248
307
|
const resource = await idbGet(db, "glyphs", key);
|
|
249
308
|
if (resource?.data) {
|
|
250
|
-
return
|
|
251
|
-
|
|
252
|
-
headers: { "Content-Type": "application/x-protobuf" }
|
|
253
|
-
});
|
|
309
|
+
if (!isEmptyGlyphRange(resource.data)) return glyphResponse(resource.data);
|
|
310
|
+
emptyRange ?? (emptyRange = resource.data);
|
|
254
311
|
}
|
|
255
312
|
}
|
|
256
313
|
}
|
|
257
|
-
return
|
|
314
|
+
return glyphResponse(emptyRange ?? new ArrayBuffer(0));
|
|
258
315
|
}
|
|
259
316
|
async function handleSprite(db, downloadId, rest) {
|
|
260
317
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
261
318
|
const styleId = styleEntry?.key ?? downloadId;
|
|
262
|
-
const path =
|
|
319
|
+
const path = safeDecodeURIComponent(rest.join("/"));
|
|
263
320
|
for (const key of spriteCandidateKeys(styleId, downloadId, path)) {
|
|
264
321
|
const resource = await idbGet(db, "sprites", key);
|
|
265
322
|
if (resource?.data) {
|
|
@@ -273,7 +330,7 @@
|
|
|
273
330
|
async function handleModel(db, downloadId, rest) {
|
|
274
331
|
const styleEntry = await findStyleByRegionId(db, downloadId);
|
|
275
332
|
const styleId = styleEntry?.key ?? downloadId;
|
|
276
|
-
const path =
|
|
333
|
+
const path = safeDecodeURIComponent(rest.join("/"));
|
|
277
334
|
for (const key of modelCandidateKeys(styleId, downloadId, path)) {
|
|
278
335
|
const resource = await idbGet(db, "models", key);
|
|
279
336
|
if (resource?.data) {
|
|
@@ -286,7 +343,7 @@
|
|
|
286
343
|
return new Response("Model not found", { status: 404 });
|
|
287
344
|
}
|
|
288
345
|
async function handleTileJSON(db, downloadId, rest) {
|
|
289
|
-
const path =
|
|
346
|
+
const path = safeDecodeURIComponent(rest.join("/"));
|
|
290
347
|
let styleEntry = await idbGet(db, "styles", downloadId);
|
|
291
348
|
if (!styleEntry?.style?.sources) {
|
|
292
349
|
styleEntry = await findStyleByRegionId(db, downloadId);
|
|
@@ -314,15 +371,25 @@
|
|
|
314
371
|
}
|
|
315
372
|
self.addEventListener("fetch", (event) => {
|
|
316
373
|
const url = event.request.url;
|
|
374
|
+
if (isMapboxRuntimeAsset(url)) {
|
|
375
|
+
event.respondWith(serveRuntimeAsset(event.request));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
317
378
|
const idx = url.indexOf(OFFLINE_PREFIX);
|
|
318
379
|
if (idx === -1) return;
|
|
319
380
|
event.respondWith(handleOfflineRequest(url, idx));
|
|
320
381
|
});
|
|
382
|
+
async function serveRuntimeAsset(request) {
|
|
383
|
+
const cache = await caches.open(MAPBOX_RUNTIME_CACHE);
|
|
384
|
+
const cached = await cache.match(request.url, { ignoreSearch: true });
|
|
385
|
+
return cached ?? fetch(request);
|
|
386
|
+
}
|
|
321
387
|
async function handleOfflineRequest(url, prefixIndex) {
|
|
322
388
|
try {
|
|
323
389
|
const path = url.substring(prefixIndex + OFFLINE_PREFIX.length);
|
|
324
390
|
const parts = path.split("/");
|
|
325
|
-
const [
|
|
391
|
+
const [encodedDownloadId, type, ...rest] = parts;
|
|
392
|
+
const downloadId = safeDecodeURIComponent(encodedDownloadId ?? "");
|
|
326
393
|
if (!downloadId || !type) {
|
|
327
394
|
return new Response("Invalid offline URL", { status: 400 });
|
|
328
395
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* @example Basic usage
|
|
12
12
|
* ```typescript
|
|
13
13
|
* import { OfflineMapManager, OfflineManagerControl } from 'map-gl-offline';
|
|
14
|
-
* import maplibregl from 'maplibre-gl';
|
|
14
|
+
* import * as maplibregl from 'maplibre-gl';
|
|
15
15
|
* import 'map-gl-offline/style.css';
|
|
16
16
|
*
|
|
17
17
|
* const map = new maplibregl.Map({
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* styleUrl: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
|
|
47
47
|
* },
|
|
48
48
|
* {
|
|
49
|
-
* onProgress: ({ phase, completed, total,
|
|
50
|
-
* console.log(
|
|
49
|
+
* onProgress: ({ phase, completed, total, overallPercentage }) => {
|
|
50
|
+
* console.log(`${overallPercentage.toFixed(0)}%, [${phase}] ${completed}/${total}`);
|
|
51
51
|
* },
|
|
52
52
|
* }
|
|
53
53
|
* );
|