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.
Files changed (73) hide show
  1. package/README.md +62 -191
  2. package/dist/idb-offline-sw.js +78 -11
  3. package/dist/index.d.ts +3 -3
  4. package/dist/index.esm.js +2065 -809
  5. package/dist/index.esm.js.map +1 -1
  6. package/dist/index.js +2072 -807
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.umd.js +9502 -711
  9. package/dist/index.umd.js.map +1 -1
  10. package/dist/main.d.ts +3 -0
  11. package/dist/managers/offlineMapManager/analyticsManagement.d.ts +1 -1
  12. package/dist/managers/offlineMapManager/base.d.ts +5 -5
  13. package/dist/managers/offlineMapManager/cleanupManagement.d.ts +6 -1
  14. package/dist/managers/offlineMapManager/importExportManagement.d.ts +1 -1
  15. package/dist/managers/offlineMapManager/maintenanceManagement.d.ts +1 -1
  16. package/dist/managers/offlineMapManager/regionManagement.d.ts +2 -2
  17. package/dist/managers/offlineMapManager/resourceManagement.d.ts +1 -1
  18. package/dist/managers/offlineMapManager/styleManagement.d.ts +1 -1
  19. package/dist/services/analyticsService.d.ts +1 -1
  20. package/dist/services/cleanupService.d.ts +15 -1
  21. package/dist/services/fontService.d.ts +1 -1
  22. package/dist/services/glyphService.d.ts +1 -1
  23. package/dist/services/importExportService.d.ts +1 -1
  24. package/dist/services/maintenanceService.d.ts +2 -2
  25. package/dist/services/modelService.d.ts +3 -1
  26. package/dist/services/regionService.d.ts +21 -11
  27. package/dist/services/resourceService.d.ts +2 -2
  28. package/dist/services/runtimeAssetService.d.ts +12 -0
  29. package/dist/services/spriteService.d.ts +1 -1
  30. package/dist/services/styleService.d.ts +14 -1
  31. package/dist/services/tileService.d.ts +1 -1
  32. package/dist/sheet/collar.d.ts +34 -0
  33. package/dist/storage/indexedDbManager.d.ts +1 -1
  34. package/dist/style.css +2 -2
  35. package/dist/sw/shared.d.ts +31 -3
  36. package/dist/types/font.d.ts +5 -1
  37. package/dist/types/import-export.d.ts +5 -0
  38. package/dist/types/maintenance.d.ts +3 -1
  39. package/dist/types/region.d.ts +29 -2
  40. package/dist/types/style.d.ts +4 -0
  41. package/dist/types/tile.d.ts +31 -10
  42. package/dist/ui/components/PanelActions.d.ts +1 -1
  43. package/dist/ui/components/PanelHeader.d.ts +1 -1
  44. package/dist/ui/components/RegionList.d.ts +1 -1
  45. package/dist/ui/components/shared/Button.d.ts +6 -0
  46. package/dist/ui/components/shared/LanguageSelector.d.ts +1 -1
  47. package/dist/ui/components/shared/MapControlButton.d.ts +1 -1
  48. package/dist/ui/components/shared/PanelContent.d.ts +3 -3
  49. package/dist/ui/components/shared/RegionDrawingTool.d.ts +2 -2
  50. package/dist/ui/controls/polygonControl.d.ts +7 -1
  51. package/dist/ui/controls/regionControl.d.ts +3 -3
  52. package/dist/ui/managers/ControlButtonManager.d.ts +1 -1
  53. package/dist/ui/managers/PanelManager.d.ts +16 -3
  54. package/dist/ui/managers/downloadManager.d.ts +7 -3
  55. package/dist/ui/modals/mbtilesModal.d.ts +1 -1
  56. package/dist/ui/modals/regionDetailsModal.d.ts +1 -1
  57. package/dist/ui/modals/regionFormModal.d.ts +11 -1
  58. package/dist/ui/offlineManagerControl.d.ts +4 -1
  59. package/dist/ui/translations/ar.d.ts +15 -1
  60. package/dist/ui/translations/en.d.ts +15 -1
  61. package/dist/ui/translations/index.d.ts +2 -0
  62. package/dist/utils/constants.d.ts +10 -3
  63. package/dist/utils/convertStyleForSW.d.ts +1 -1
  64. package/dist/utils/download.d.ts +1 -1
  65. package/dist/utils/formatting.d.ts +11 -0
  66. package/dist/utils/importResolver.d.ts +1 -1
  67. package/dist/utils/sqlJsLoader.d.ts +1 -2
  68. package/dist/utils/styleConfig.d.ts +39 -0
  69. package/dist/utils/styleProviderUtils.d.ts +21 -8
  70. package/dist/utils/styleUtils.d.ts +61 -10
  71. package/dist/utils/tileRange.d.ts +63 -0
  72. package/dist/utils/validation.d.ts +1 -1
  73. package/package.json +45 -40
package/README.md CHANGED
@@ -1,111 +1,84 @@
1
- # Map GL Offline ๐Ÿ—บ๏ธ
1
+ # map-gl-offline
2
2
 
3
3
  [![npm version](https://badge.fury.io/js/map-gl-offline.svg)](https://badge.fury.io/js/map-gl-offline)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue.svg)](https://www.typescriptlang.org/)
6
5
 
7
- **[๐Ÿ“– 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)**
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
- TypeScript offline-map library for **MapLibre GL JS** and **Mapbox GL JS**. Download styles, tiles, sprites, glyphs, and fonts to IndexedDB; load them back with zero network. Ships with a glassmorphic UI control and a complete programmatic API.
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
- ![Map GL Offline Demo](assets/map-gl-offline-demo.gif)
10
+ ![Downloading a region in the map-gl-offline demo](assets/map-gl-offline-demo.gif)
12
11
 
13
- ## Features
14
-
15
- - ๐Ÿ—บ๏ธ **Offline regions** โ€” polygon selection, smart tile management, extra vector/raster overlays
16
- - ๐ŸŽจ **Full resource capture** โ€” styles, sprites, fonts, glyphs with Unicode ranges
17
- - ๐Ÿ”— **Mapbox GL + MapLibre GL** โ€” auto-detection, `mapbox://` URL resolution, Standard style with 3D/terrain
18
- - ๐Ÿ“Š **Analytics & cleanup** โ€” storage reports, auto-cleanup, quota-aware downloads
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
- Or via CDN as the `mapgloffline` global:
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 Start
29
+ ## Quick start
36
30
 
37
31
  ### MapLibre GL JS
38
32
 
39
- ```typescript
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 map = new maplibregl.Map({
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 offlineManager = new OfflineMapManager();
41
+ const map = new maplibregl.Map({ container: 'map', style: styleUrl });
53
42
 
54
- map.on('load', () => {
55
- const control = new OfflineManagerControl(offlineManager, {
56
- styleUrl: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
57
- mapLib: maplibregl, // enables idb:// protocol in web workers
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 JS v3 lacks `addProtocol`, so the library uses a Service Worker. Run **one** of:
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 # CLI (recommended)
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
- Then:
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
- const offlineManager = new OfflineMapManager();
93
- map.on('load', () =>
94
- map.addControl(
95
- new OfflineManagerControl(offlineManager, {
96
- styleUrl: 'mapbox://styles/mapbox/standard',
97
- accessToken: mapboxgl.accessToken,
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
- ## Programmatic Usage
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
- ```typescript
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: [[-74.0559, 40.7128], [-74.0059, 40.7628]],
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://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
96
+ styleUrl: 'https://tiles.openfreemap.org/styles/liberty',
121
97
  },
122
98
  {
123
- onProgress: ({ phase, percentage, message }) => {
124
- console.log(`[${phase}] ${percentage.toFixed(1)}% ${message ?? ''}`);
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
- > **Upgrading from 0.5.x?** Read the [0.6.0 migration guide](https://map-gl-offline.netlify.app/docs/migration-0.6) โ€” covers the rename of `ResourceService.getXxxStatistics` โ†’ `getXxxStats`, the `addRegion` vs `downloadRegion` split, and the `expiry` timestamp fix.
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
- See the **[full API reference](https://map-gl-offline.netlify.app/docs/api-reference)** and **[examples](https://map-gl-offline.netlify.app/docs/examples)** for every option and pattern.
108
+ ## Documentation
235
109
 
236
- ## Browser compatibility
237
-
238
- Chrome 51+ ยท Firefox 45+ ยท Safari 10+ ยท Edge 79+ ยท modern mobile browsers. Requires IndexedDB and ES2015+.
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 && npm install
245
- npm run dev # dev harness
246
- npm test # unit tests
247
- npm run build # library
123
+ cd map-gl-offline
124
+ npm install
125
+ npm run dev # The demo
126
+ npm test # Unit tests
248
127
  ```
249
128
 
250
- Issues and PRs welcome. See [CHANGELOG.md](CHANGELOG.md) for release notes.
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).
@@ -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(decodeURIComponent(rest.join("/")));
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 new Response(resource.data, {
251
- status: 200,
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 new Response("Glyph not found", { status: 404 });
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 = decodeURIComponent(rest.join("/"));
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 = decodeURIComponent(rest.join("/"));
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 = decodeURIComponent(rest.join("/"));
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 [downloadId, type, ...rest] = parts;
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, percentage }) => {
50
- * console.log(`[${phase}] ${completed}/${total} (${percentage.toFixed(1)}%)`);
49
+ * onProgress: ({ phase, completed, total, overallPercentage }) => {
50
+ * console.log(`${overallPercentage.toFixed(0)}%, [${phase}] ${completed}/${total}`);
51
51
  * },
52
52
  * }
53
53
  * );