import { OfflineManager, type OfflinePackStatus } from '@maplibre/maplibre-react-native'; type LngLatBounds = [number, number, number, number]; // [west, south, east, north] // Vector style URL — only vector styles support offline packs export const OFFLINE_STYLE_URL = 'https://tiles.openfreemap.org/styles/liberty'; // Zoom levels: 6 (region overview) → 16 (street detail, good for cycling) const MIN_ZOOM = 6; const MAX_ZOOM = 16; export interface OfflineRegion { id: string; name: string; bounds: LngLatBounds; createdAt: string; completedTiles: number; totalSizeBytes: number; state: 'active' | 'inactive' | 'complete'; percentage: number; } // ── Download ────────────────────────────────────────────────────────────────── /** * Download a region and return a Promise that resolves only when the pack * reaches state 'complete'. Transient tile-fetch errors (e.g. "stream was * reset: CANCEL") are silently ignored — MapLibre retries them internally * and they do not indicate a failed download. */ export function downloadRegion( name: string, bounds: LngLatBounds, onProgress: (pct: number, tilesDown: number, sizeBytes: number) => void, ): Promise { OfflineManager.setProgressEventThrottle(500); return new Promise((resolve, reject) => { let packId: string; OfflineManager.createPack( { mapStyle: OFFLINE_STYLE_URL, bounds, minZoom: MIN_ZOOM, maxZoom: MAX_ZOOM, metadata: { name, createdAt: new Date().toISOString() }, }, (pack, status: OfflinePackStatus) => { packId = pack.id; onProgress(status.percentage, status.completedTileCount, status.completedTileSize); if (status.state === 'complete') { resolve(packId); } }, (_pack, _error) => { // Transient tile-level errors — MapLibre retries these automatically. // Do not reject: the overall download is still in progress. }, ).catch(reject); // only rejects on createPack failure (e.g. bad style URL) }); } // ── List ────────────────────────────────────────────────────────────────────── export async function listRegions(): Promise { const packs = await OfflineManager.getPacks(); const regions: OfflineRegion[] = []; for (const pack of packs) { const status = await pack.status(); const meta = pack.metadata as { name?: string; createdAt?: string }; regions.push({ id: pack.id, name: meta.name ?? 'Unnamed region', bounds: pack.bounds, createdAt: meta.createdAt ?? '', completedTiles: status.completedTileCount, totalSizeBytes: status.completedTileSize, state: status.state, percentage: status.percentage, }); } return regions; } // ── Delete ──────────────────────────────────────────────────────────────────── export async function deleteRegion(id: string): Promise { await OfflineManager.deletePack(id); } // ── Helpers ─────────────────────────────────────────────────────────────────── export function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } /** Expand a [w,s,e,n] bounds by `km` kilometres in every direction. */ export function expandBounds(bounds: LngLatBounds, km: number): LngLatBounds { const latDelta = km / 111; const lonDelta = km / (111 * Math.cos((bounds[1] + bounds[3]) / 2 * Math.PI / 180)); return [ bounds[0] - lonDelta, bounds[1] - latDelta, bounds[2] + lonDelta, bounds[3] + latDelta, ]; }