Examples
Practical examples for common use cases with map-gl-offline.
Table of Contents
- Basic Usage
- Region Management
- Import/Export
- Offline Detection
- Storage Management
- Error Handling
- Advanced Patterns
Basic Usage
Minimal Setup
import maplibregl from 'maplibre-gl';
import { OfflineMapManager, OfflineManagerControl } from 'map-gl-offline';
import 'maplibre-gl/dist/maplibre-gl.css';
import 'map-gl-offline/dist/style.css';
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [0, 0],
zoom: 2,
});
const manager = new OfflineMapManager();
const control = new OfflineManagerControl(manager, {
styleUrl: 'https://demotiles.maplibre.org/style.json',
});
map.addControl(control, 'top-right');
Programmatic Download
const manager = new OfflineMapManager();
// Download New York City area
await manager.addRegion({
id: 'nyc',
name: 'New York City',
bounds: [[-74.259, 40.477], [-73.700, 40.917]],
minZoom: 10,
maxZoom: 15,
styleUrl: 'https://api.maptiler.com/maps/streets/style.json?key=YOUR_KEY',
onProgress: (progress) => {
updateProgressBar(progress.percentage);
updateStatusText(progress.message);
},
});
Region Management
Download Multiple Regions
const regions = [
{ id: 'downtown', name: 'Downtown', bounds: [[-74.02, 40.70], [-73.97, 40.75]] },
{ id: 'brooklyn', name: 'Brooklyn', bounds: [[-74.04, 40.57], [-73.85, 40.74]] },
{ id: 'queens', name: 'Queens', bounds: [[-73.96, 40.68], [-73.70, 40.81]] },
];
for (const region of regions) {
await manager.addRegion({
...region,
minZoom: 10,
maxZoom: 14,
styleUrl: STYLE_URL,
onProgress: (p) => console.log(`${region.name}: ${p.percentage}%`),
});
}
List and Display Regions
const regions = await manager.listStoredRegions();
regions.forEach(region => {
console.log(`
ID: ${region.id}
Name: ${region.name}
Created: ${new Date(region.created).toLocaleDateString()}
Expires: ${region.expiry ? new Date(region.expiry).toLocaleDateString() : 'Never'}
Bounds: ${JSON.stringify(region.bounds)}
`);
});
Update Region Metadata
// Extend expiration
await manager.updateRegion('nyc', {
expiry: Date.now() + 60 * 24 * 60 * 60 * 1000, // 60 days
name: 'New York City (Extended)',
});
Delete Region with Confirmation
async function deleteRegionWithConfirm(regionId: string) {
const region = await manager.getRegion(regionId);
if (!region) return;
const confirmed = confirm(`Delete "${region.name}"? This cannot be undone.`);
if (confirmed) {
await manager.deleteRegion(regionId);
console.log(`Deleted region: ${regionId}`);
}
}
Import/Export
Export to JSON for Backup
async function backupRegion(regionId: string) {
const result = await manager.exportRegionAsJSON(regionId, {
includeStyle: true,
includeTiles: true,
includeSprites: true,
includeFonts: true,
onProgress: (p) => {
console.log(`Exporting: ${p.percentage}% - ${p.stage}`);
},
});
// Trigger download
const url = URL.createObjectURL(result.blob);
const link = document.createElement('a');
link.href = url;
link.download = result.filename;
link.click();
URL.revokeObjectURL(url);
console.log(`Exported ${result.statistics.tilesExported} tiles (${result.size} bytes)`);
}
Export to PMTiles for Web Deployment
async function exportForCDN(regionId: string) {
const result = await manager.exportRegionAsPMTiles(regionId, {
compression: 'gzip',
metadata: {
attribution: '© OpenStreetMap contributors',
version: '1.0.0',
description: 'Offline map tiles',
},
});
// Upload to CDN or save
await uploadToS3(result.blob, `maps/${result.filename}`);
}
Import from File
// HTML: <input type="file" id="import-file" accept=".json,.pmtiles,.mbtiles">
document.getElementById('import-file').addEventListener('change', async (e) => {
const file = e.target.files?.[0];
if (!file) return;
// Determine format from extension
const ext = file.name.split('.').pop().toLowerCase();
const format = ext === 'json' ? 'json' :
ext === 'pmtiles' ? 'pmtiles' : 'mbtiles';
try {
const result = await manager.importRegion({
file,
format,
overwrite: false,
});
if (result.success) {
alert(`Imported "${result.regionId}" with ${result.statistics.tilesImported} tiles`);
} else {
alert(`Import failed: ${result.message}`);
}
} catch (error) {
alert(`Import error: ${error.message}`);
}
});
Transfer Between Devices
// Device A: Export
async function exportForTransfer() {
const regions = await manager.listStoredRegions();
const exports = [];
for (const region of regions) {
const result = await manager.exportRegionAsJSON(region.id);
exports.push({
regionId: region.id,
blob: result.blob,
filename: result.filename,
});
}
// Create zip or transfer individually
return exports;
}
// Device B: Import
async function importFromTransfer(files: File[]) {
for (const file of files) {
await manager.importRegion({
file,
format: 'json',
overwrite: true,
});
}
}
Offline Detection
Basic Offline Handling
const manager = new OfflineMapManager();
const control = new OfflineManagerControl(manager, { styleUrl: STYLE_URL });
map.addControl(control);
// Detect offline and load cached data
window.addEventListener('offline', async () => {
console.log('Network offline');
await control.loadOfflineStyles();
});
window.addEventListener('online', () => {
console.log('Network restored');
// Optionally refresh or sync
});
Service Worker Integration
// In your service worker
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
// Let map-gl-offline handle tile requests
if (url.pathname.includes('/tiles/')) {
// The library's fetch interceptor handles this
return;
}
// Handle other requests with cache-first strategy
event.respondWith(
caches.match(event.request).then(response => {
return response || fetch(event.request);
})
);
});
Progressive Enhancement
class OfflineMapApp {
private manager: OfflineMapManager;
private isOnline = navigator.onLine;
constructor() {
this.manager = new OfflineMapManager();
this.setupListeners();
}
private setupListeners() {
window.addEventListener('online', () => this.handleOnline());
window.addEventListener('offline', () => this.handleOffline());
}
private async handleOffline() {
this.isOnline = false;
this.showOfflineBanner();
// Check if we have cached data
const regions = await this.manager.listStoredRegions();
if (regions.length === 0) {
this.showNoDataWarning();
}
}
private handleOnline() {
this.isOnline = true;
this.hideOfflineBanner();
}
private showOfflineBanner() {
document.getElementById('offline-banner')?.classList.remove('hidden');
}
private hideOfflineBanner() {
document.getElementById('offline-banner')?.classList.add('hidden');
}
}
Storage Management
Monitor Storage Usage
async function getStorageInfo() {
const analytics = await manager.getComprehensiveStorageAnalytics();
console.log(`
Total Size: ${formatBytes(analytics.totalStorageSize)}
Tiles: ${analytics.tiles.count} (${formatBytes(analytics.tiles.totalSize)})
Fonts: ${analytics.fonts.count} (${formatBytes(analytics.fonts.totalSize)})
Sprites: ${analytics.sprites.count} (${formatBytes(analytics.sprites.totalSize)})
`);
// Show recommendations
analytics.recommendations.forEach(rec => console.log(`Recommendation: ${rec}`));
return analytics;
}
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
Automatic Cleanup Strategy
class StorageManager {
private manager: OfflineMapManager;
private maxStorageMB = 500;
constructor(manager: OfflineMapManager) {
this.manager = manager;
this.startAutoCleanup();
}
private startAutoCleanup() {
// Cleanup daily
this.manager.startAutoCleanup({
interval: 24 * 60 * 60 * 1000,
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
});
// Also check storage quota periodically
setInterval(() => this.checkQuota(), 60 * 60 * 1000); // Hourly
}
private async checkQuota() {
const analytics = await this.manager.getComprehensiveStorageAnalytics();
const usedMB = analytics.totalStorageSize / (1024 * 1024);
if (usedMB > this.maxStorageMB * 0.9) {
console.warn('Storage quota near limit, cleaning up...');
await this.manager.cleanupOldTiles(7 * 24 * 60 * 60 * 1000);
}
}
}
Clear All Data
async function clearAllOfflineData() {
const confirmed = confirm('Delete all offline map data? This cannot be undone.');
if (!confirmed) return;
const regions = await manager.listStoredRegions();
for (const region of regions) {
await manager.deleteRegion(region.id);
}
console.log(`Cleared ${regions.length} regions`);
}
Error Handling
Comprehensive Error Handling
import { categorizeError, getUserErrorMessage, ErrorType } from 'map-gl-offline';
async function downloadWithRetry(regionOptions: OfflineRegionOptions, maxRetries = 3) {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await manager.addRegion(regionOptions);
return; // Success
} catch (error) {
lastError = error as Error;
const errorType = categorizeError(error);
const message = getUserErrorMessage(error);
console.error(`Attempt ${attempt}/${maxRetries} failed: ${message}`);
switch (errorType) {
case ErrorType.NETWORK:
// Wait and retry for network errors
await sleep(1000 * attempt);
continue;
case ErrorType.QUOTA:
// Try to free up space
await manager.cleanupOldTiles(7 * 24 * 60 * 60 * 1000);
continue;
case ErrorType.VALIDATION:
// Invalid input, don't retry
throw error;
default:
// Unknown error, retry with backoff
await sleep(1000 * attempt);
continue;
}
}
}
throw lastError;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
User-Friendly Error Messages
async function downloadRegion(options: OfflineRegionOptions) {
try {
await manager.addRegion(options);
showToast('Region downloaded successfully!', 'success');
} catch (error) {
const message = getUserErrorMessage(error);
if (message.includes('quota')) {
showToast('Storage full. Please delete some regions first.', 'error');
} else if (message.includes('network')) {
showToast('Network error. Check your connection and try again.', 'error');
} else {
showToast(`Download failed: ${message}`, 'error');
}
}
}
Advanced Patterns
Custom Progress UI
class DownloadProgressUI {
private element: HTMLElement;
constructor(containerId: string) {
this.element = document.getElementById(containerId)!;
}
async downloadWithProgress(options: OfflineRegionOptions) {
this.show();
this.updatePhase('Preparing...');
try {
await manager.addRegion({
...options,
onProgress: (progress) => {
this.updateProgress(progress.percentage);
this.updatePhase(progress.message || 'Downloading...');
this.updateDetails(`${progress.completed}/${progress.total}`);
},
});
this.updatePhase('Complete!');
await this.delay(1000);
} finally {
this.hide();
}
}
private show() {
this.element.classList.remove('hidden');
}
private hide() {
this.element.classList.add('hidden');
}
private updateProgress(percentage: number) {
const bar = this.element.querySelector('.progress-bar') as HTMLElement;
bar.style.width = `${percentage}%`;
}
private updatePhase(text: string) {
const phase = this.element.querySelector('.phase-text') as HTMLElement;
phase.textContent = text;
}
private updateDetails(text: string) {
const details = this.element.querySelector('.details-text') as HTMLElement;
details.textContent = text;
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
Region Presets
const REGION_PRESETS = {
'new-york': {
name: 'New York City',
bounds: [[-74.259, 40.477], [-73.700, 40.917]] as [[number, number], [number, number]],
minZoom: 10,
maxZoom: 15,
},
'los-angeles': {
name: 'Los Angeles',
bounds: [[-118.668, 33.704], [-117.785, 34.337]] as [[number, number], [number, number]],
minZoom: 10,
maxZoom: 15,
},
'london': {
name: 'London',
bounds: [[-0.510, 51.286], [0.334, 51.692]] as [[number, number], [number, number]],
minZoom: 10,
maxZoom: 15,
},
};
async function downloadPreset(presetId: keyof typeof REGION_PRESETS) {
const preset = REGION_PRESETS[presetId];
if (!preset) throw new Error(`Unknown preset: ${presetId}`);
await manager.addRegion({
id: presetId,
...preset,
styleUrl: STYLE_URL,
});
}
Sync with Remote Server
class RegionSync {
private manager: OfflineMapManager;
private apiUrl: string;
constructor(manager: OfflineMapManager, apiUrl: string) {
this.manager = manager;
this.apiUrl = apiUrl;
}
async syncFromServer() {
// Get server regions
const response = await fetch(`${this.apiUrl}/regions`);
const serverRegions = await response.json();
// Get local regions
const localRegions = await this.manager.listStoredRegions();
const localIds = new Set(localRegions.map(r => r.id));
// Download missing regions
for (const serverRegion of serverRegions) {
if (!localIds.has(serverRegion.id)) {
console.log(`Downloading missing region: ${serverRegion.name}`);
await this.manager.addRegion(serverRegion);
}
}
}
async syncToServer() {
const localRegions = await this.manager.listStoredRegions();
for (const region of localRegions) {
const result = await this.manager.exportRegionAsJSON(region.id);
await fetch(`${this.apiUrl}/regions/${region.id}`, {
method: 'PUT',
body: result.blob,
headers: { 'Content-Type': 'application/json' },
});
}
}
}