Map tiles: new lib/tileCache.ts (Dexie SkateSpotsTilesDB, bounded to ~3000 tiles) and lib/cachedTileLayer.ts, a Leaflet TileLayer that reads through it (cached -> fetch+cache -> plain URL fallback). Wired into both maps (SpotMap, RouteDialog). Chosen over a service worker so it works in the Capacitor WebView too, where a SW never sees tile requests. Photos: useUploadFile drops from three Blossom hosts to two and records ['fallback', '<mirror>/<sha256>']. BlossomUploader signs once and Promise.any PUTs to every server in parallel, so the old three-host list was triple- uploading every photo (the 'tries them in order' comment was wrong). Read side: lib/imageFallback.ts derives same-hash mirrors and ImageWithFallback retries them before showing the placeholder - one choke point for ~20 photo call sites, so a dead host no longer blanks a spot.
62 lines
1.9 KiB
TypeScript
62 lines
1.9 KiB
TypeScript
import L from 'leaflet';
|
|
|
|
import { getTile, putTile } from '@/lib/tileCache';
|
|
|
|
/**
|
|
* A Leaflet tile layer that reads through an on-device IndexedDB cache.
|
|
*
|
|
* Behaviour per tile:
|
|
* 1. cached blob → draw it immediately (works fully offline);
|
|
* 2. else fetch → cache the bytes → draw them;
|
|
* 3. offline and uncached → fall back to the plain tile URL, so Leaflet shows
|
|
* its normal missing-tile state instead of hanging forever.
|
|
*
|
|
* Works on web and in the Capacitor WebView alike, unlike a service worker.
|
|
*/
|
|
export class CachedTileLayer extends L.TileLayer {
|
|
protected createTile(coords: L.Coords, done: L.DoneCallback): HTMLElement {
|
|
const tile = document.createElement('img');
|
|
tile.alt = '';
|
|
tile.setAttribute('role', 'presentation');
|
|
|
|
const key = `${coords.z}/${coords.x}/${coords.y}`;
|
|
const url = this.getTileUrl(coords);
|
|
|
|
const useSrc = (src: string, revoke: boolean) => {
|
|
tile.onload = () => {
|
|
if (revoke) URL.revokeObjectURL(src);
|
|
done(undefined, tile);
|
|
};
|
|
tile.onerror = (e) => done(e as unknown as Error, tile);
|
|
tile.src = src;
|
|
};
|
|
|
|
void (async () => {
|
|
const cached = await getTile(key);
|
|
if (cached) {
|
|
useSrc(URL.createObjectURL(cached), true);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const res = await fetch(url, { mode: 'cors' });
|
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
const blob = await res.blob();
|
|
if (blob.size > 0) void putTile(key, blob);
|
|
useSrc(URL.createObjectURL(blob), true);
|
|
} catch {
|
|
// No cache and no network: let the browser try the URL directly.
|
|
useSrc(url, false);
|
|
}
|
|
})();
|
|
|
|
return tile;
|
|
}
|
|
}
|
|
|
|
export function createCachedTileLayer(
|
|
urlTemplate: string,
|
|
options?: L.TileLayerOptions,
|
|
): CachedTileLayer {
|
|
return new CachedTileLayer(urlTemplate, options);
|
|
}
|