https://github.com/muimsd/map-gl-offline
A TypeScript npm package for MapLibre GL JS and Mapbox GL JS to enable offline tiles.
https://github.com/muimsd/map-gl-offline
gis mapbox-gl maplibre-gl mbtiles offline offline-first pwa service-worker typescript webgl
Last synced: 25 days ago
JSON representation
A TypeScript npm package for MapLibre GL JS and Mapbox GL JS to enable offline tiles.
- Host: GitHub
- URL: https://github.com/muimsd/map-gl-offline
- Owner: muimsd
- License: mit
- Created: 2025-02-23T09:37:22.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-05-22T12:22:19.000Z (26 days ago)
- Last Synced: 2026-05-22T16:15:02.963Z (26 days ago)
- Topics: gis, mapbox-gl, maplibre-gl, mbtiles, offline, offline-first, pwa, service-worker, typescript, webgl
- Language: TypeScript
- Homepage: https://map-gl-offline-demo.netlify.app/
- Size: 7.68 MB
- Stars: 14
- Watchers: 1
- Forks: 1
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
- awesome-maplibre - map-gl-offline - A TypeScript-compatible npm package for MapLibre GL JS that enables comprehensive offline storage and usage of vector/raster tiles. (Utility Libraries / JavaScript)
- awesome-vector-tiles - map-gl-offline - A TypeScript package for MapLibre GL JS and Mapbox GL JS to enable offline tiles. (Mapbox GL JS Plugins)
README
# Map GL Offline ๐บ๏ธ
[](https://badge.fury.io/js/map-gl-offline)
[](https://opensource.org/licenses/MIT)
[](https://www.typescriptlang.org/)
**[๐ 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)**
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.

## Features
- ๐บ๏ธ **Offline regions** โ polygon selection, smart tile management, extra vector/raster overlays
- ๐จ **Full resource capture** โ styles, sprites, fonts, glyphs with Unicode ranges
- ๐ **Mapbox GL + MapLibre GL** โ auto-detection, `mapbox://` URL resolution, Standard style with 3D/terrain
- ๐ **Analytics & cleanup** โ storage reports, auto-cleanup, quota-aware downloads
- ๐จ **UI control** โ glassmorphic panel, dark/light themes, English/Arabic (RTL), polygon drawing
- ๐ ๏ธ **Programmatic API** โ `downloadRegion` with per-phase progress, full TypeScript types
## Install
```bash
npm install map-gl-offline
```
Or via CDN as the `mapgloffline` global:
```html
```
## Quick Start
### MapLibre GL JS
```typescript
import maplibregl from 'maplibre-gl';
import { OfflineMapManager, OfflineManagerControl } from 'map-gl-offline';
import 'maplibre-gl/dist/maplibre-gl.css';
import 'map-gl-offline/style.css';
const map = new maplibregl.Map({
container: 'map',
style: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
center: [-74.006, 40.7128],
zoom: 12,
});
const offlineManager = new OfflineMapManager();
map.on('load', () => {
const control = new OfflineManagerControl(offlineManager, {
styleUrl: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
mapLib: maplibregl, // enables idb:// protocol in web workers
});
map.addControl(control, 'top-right');
});
```
### Mapbox GL JS
Mapbox GL JS v3 lacks `addProtocol`, so the library uses a Service Worker. Run **one** of:
```bash
npx map-gl-offline init # CLI (recommended)
# or add to vite.config.js:
# import { offlineSwPlugin } from 'map-gl-offline/vite-plugin';
# plugins: [offlineSwPlugin()]
# or manually: cp node_modules/map-gl-offline/dist/idb-offline-sw.js public/
```
Then:
```typescript
import mapboxgl from 'mapbox-gl';
import { OfflineMapManager, OfflineManagerControl } from 'map-gl-offline';
import 'mapbox-gl/dist/mapbox-gl.css';
import 'map-gl-offline/style.css';
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/standard',
center: [-74.006, 40.7128],
zoom: 12,
});
const offlineManager = new OfflineMapManager();
map.on('load', () =>
map.addControl(
new OfflineManagerControl(offlineManager, {
styleUrl: 'mapbox://styles/mapbox/standard',
accessToken: mapboxgl.accessToken,
}),
'top-right',
),
);
```
## Programmatic Usage
`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.
```typescript
import { OfflineMapManager } from 'map-gl-offline';
const offlineManager = new OfflineMapManager();
await offlineManager.downloadRegion(
{
id: 'downtown',
name: 'Downtown',
bounds: [[-74.0559, 40.7128], [-74.0059, 40.7628]],
minZoom: 10,
maxZoom: 16,
styleUrl: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
},
{
onProgress: ({ phase, percentage, message }) => {
console.log(`[${phase}] ${percentage.toFixed(1)}% ${message ?? ''}`);
},
},
);
// Manage
await offlineManager.listStoredRegions();
await offlineManager.getStoredRegion('downtown');
await offlineManager.deleteRegion('downtown');
// Analytics & cleanup
await offlineManager.getComprehensiveStorageAnalytics();
await offlineManager.cleanupExpiredRegions();
await offlineManager.setupAutoCleanup({ intervalHours: 24, maxAge: 30 });
```
### Multi-region downloads (global overview + city detail)
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.
```typescript
import mapboxgl from 'mapbox-gl';
import {
OfflineMapManager,
type BoundingBox,
type DownloadRegionProgress,
} from 'map-gl-offline';
const offlineManager = new OfflineMapManager();
const STYLE_URL = 'mapbox://styles/mapbox/standard';
const opts = {
accessToken: mapboxgl.accessToken, // `string | null` is accepted โ no cast needed
onProgress: ({ phase, percentage, message }: DownloadRegionProgress) =>
console.log(`[${phase}] ${percentage.toFixed(1)}% ${message ?? ''}`),
};
// 1) Whole planet, low zoom only (~5,500 tiles/source) โ countries, major cities
await offlineManager.downloadRegion(
{
id: 'global-overview',
name: 'Global overview',
bounds: [[-180, -85.0511], [180, 85.0511]], // ยฑ85.0511ยฐ = Web Mercator cutoff
minZoom: 0,
maxZoom: 6,
styleUrl: STYLE_URL,
multipleRegions: true,
},
opts,
);
// 2) High-detail per city โ tight bbox per place your users actually go
const cities: Array<{ id: string; name: string; bounds: BoundingBox }> = [
{ id: 'nyc', name: 'New York', bounds: [[-74.05, 40.68], [-73.90, 40.82]] },
{ id: 'london', name: 'London', bounds: [[-0.25, 51.43], [0.02, 51.57]] },
];
for (const city of cities) {
await offlineManager.downloadRegion(
{
id: city.id,
name: city.name,
bounds: city.bounds,
minZoom: 6, // overlaps the overview's maxZoom โ clean handoff, no seam
maxZoom: 14, // street-level detail
styleUrl: STYLE_URL,
multipleRegions: true,
},
opts,
);
}
```
> **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.
### Sparse-source detection
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 }`.
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.
### Recovering from an incompatible DB
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:
```typescript
import { dbPromise, OfflineMapDBVersionError, resetOfflineMapDB } from 'map-gl-offline';
try {
await dbPromise;
} catch (err) {
if (err instanceof OfflineMapDBVersionError) {
if (confirm('Offline storage is incompatible. Clear it?')) {
await resetOfflineMapDB(); // destructive
location.reload();
}
}
}
```
> **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.
## API at a glance
- **Regions** โ `downloadRegion`, `loadRegion`, `addRegion`, `getStoredRegion`, `listStoredRegions`, `listRegions`, `deleteRegion`
- **Analytics** โ `getComprehensiveStorageAnalytics`, `getRegionAnalytics`, `getTileStats`, `getFontStats`, `getSpriteStats`, `getGlyphStats`
- **Cleanup** โ `cleanupExpiredRegions`, `performSmartCleanup`, `cleanupOld{Fonts,Sprites,Glyphs}`, `verifyAndRepair{Fonts,Sprites,Glyphs}`, `setupAutoCleanup`, `performCompleteMaintenance`
- **Import / Export** โ `exportRegionAsMBTiles`, `importRegion`, `downloadExportedRegion` (binary SQLite MBTiles, QGIS/tippecanoe-compatible)
- **Storage utilities** โ `dbPromise`, `OfflineMapDBVersionError`, `resetOfflineMapDB`, `loadAllStoredRegions`, `resourceKeyBelongsToStyle`
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.
## Browser compatibility
Chrome 51+ ยท Firefox 45+ ยท Safari 10+ ยท Edge 79+ ยท modern mobile browsers. Requires IndexedDB and ES2015+.
## Contributing
```bash
git clone https://github.com/muimsd/map-gl-offline.git
cd map-gl-offline && npm install
npm run dev # dev harness
npm test # unit tests
npm run build # library
```
Issues and PRs welcome. See [CHANGELOG.md](CHANGELOG.md) for release notes.
## License
MIT ยฉ [Muhammad Imran Siddique](https://github.com/muimsd)
---
