Add progressive web app companion for cross-platform access
Vite + TypeScript PWA that mirrors the Android app's core features: - Pre-processed shelter data (build-time UTM33N→WGS84 conversion) - Leaflet map with shelter markers, user location, and offline tiles - Canvas compass arrow (ported from DirectionArrowView.kt) - IndexedDB shelter cache with 7-day staleness check - Service worker with CacheFirst tiles and precached app shell - i18n for en, nb, nn (ported from Android strings.xml) - iOS/Android compass handling with low-pass filter - Respects user map interaction (no auto-snap on pan/zoom) - Build revision cache-breaker for reliable SW updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
46365b713b
commit
e8428de775
12051 changed files with 1799735 additions and 0 deletions
112
pwa/src/cache/map-cache-manager.ts
vendored
Normal file
112
pwa/src/cache/map-cache-manager.ts
vendored
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Map tile cache manager.
|
||||
* Seeds tiles by programmatically panning a Leaflet map across a grid.
|
||||
* Tiles are cached by the service worker (CacheFirst strategy on OSM URLs).
|
||||
*
|
||||
* Ported from MapCacheManager.kt — same 3×3 grid × 4 zoom levels approach.
|
||||
*/
|
||||
|
||||
import type L from 'leaflet';
|
||||
|
||||
const CACHE_RADIUS_DEGREES = 0.15; // ~15km
|
||||
const CACHE_ZOOM_LEVELS = [10, 12, 14, 16];
|
||||
const GRID_SIZE = 3;
|
||||
const PAN_DELAY_MS = 400; // Slightly longer than Android (300ms) for web
|
||||
|
||||
const STORAGE_KEY = 'mapCache';
|
||||
|
||||
interface CacheMeta {
|
||||
lat: number;
|
||||
lon: number;
|
||||
radius: number;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
function getCacheMeta(): CacheMeta | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
return raw ? JSON.parse(raw) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveCacheMeta(meta: CacheMeta): void {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(meta));
|
||||
}
|
||||
|
||||
/** Check if we have a tile cache covering the given location. */
|
||||
export function hasCacheForLocation(lat: number, lon: number): boolean {
|
||||
const meta = getCacheMeta();
|
||||
if (!meta?.complete) return false;
|
||||
|
||||
const margin = meta.radius * 0.3;
|
||||
return (
|
||||
Math.abs(lat - meta.lat) < meta.radius - margin &&
|
||||
Math.abs(lon - meta.lon) < meta.radius - margin
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed the tile cache by panning the map across the surrounding area.
|
||||
* The service worker's CacheFirst strategy on OSM tile URLs handles storage.
|
||||
*
|
||||
* Reports progress via callback (0 to 1).
|
||||
*/
|
||||
export async function cacheMapArea(
|
||||
map: L.Map,
|
||||
lat: number,
|
||||
lon: number,
|
||||
onProgress?: (progress: number) => void,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const totalSteps = CACHE_ZOOM_LEVELS.length * GRID_SIZE * GRID_SIZE;
|
||||
let step = 0;
|
||||
|
||||
const originalZoom = map.getZoom();
|
||||
const originalCenter = map.getCenter();
|
||||
|
||||
for (const zoom of CACHE_ZOOM_LEVELS) {
|
||||
map.setZoom(zoom);
|
||||
|
||||
for (let row = 0; row < GRID_SIZE; row++) {
|
||||
for (let col = 0; col < GRID_SIZE; col++) {
|
||||
const panLat =
|
||||
lat -
|
||||
CACHE_RADIUS_DEGREES +
|
||||
(2 * CACHE_RADIUS_DEGREES * row) / (GRID_SIZE - 1);
|
||||
const panLon =
|
||||
lon -
|
||||
CACHE_RADIUS_DEGREES +
|
||||
(2 * CACHE_RADIUS_DEGREES * col) / (GRID_SIZE - 1);
|
||||
|
||||
map.setView([panLat, panLon], zoom, { animate: false });
|
||||
map.invalidateSize();
|
||||
|
||||
step++;
|
||||
onProgress?.(step / totalSteps);
|
||||
|
||||
await sleep(PAN_DELAY_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original view
|
||||
map.setView(originalCenter, originalZoom, { animate: false });
|
||||
|
||||
saveCacheMeta({
|
||||
lat,
|
||||
lon,
|
||||
radius: CACHE_RADIUS_DEGREES,
|
||||
complete: true,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue