86397e0980
createPack() resolves when the pack is registered, not when tiles are
done downloading. Wrapping in a Promise that resolves only on
status.state === 'complete' keeps the progress modal visible and the
'Download complete' alert fires only when the pack is actually ready.
Transient tile errors ('stream was reset: CANCEL') are silently ignored
in the error listener — MapLibre retries them internally and they do not
indicate a failed download. The onError callback is removed from the
public API since it was causing spurious alerts mid-download.
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
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<string> {
|
|
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<OfflineRegion[]> {
|
|
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<void> {
|
|
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,
|
|
];
|
|
}
|