Files
bincio-rec/src/services/offline.ts
T
Davide Scaini 86397e0980 fix: offline download — wait for completion, suppress transient errors
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.
2026-06-04 01:06:39 +02:00

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,
];
}