import type { RouteResult, TranslateFn } from '@/lib/routing'; /** * Tiny localStorage cache of the last few routes. * * Why: routing needs the network. If you lose signal mid-skate (or the routing * server is briefly down), Directions would show nothing at all. Keeping the * last route per destination+mode means we can still show the line, the time and * the steps — clearly labelled as cached — instead of an error. * * The full OSRM geometry can be thousands of points, so the line is * DOWNSAMPLED before storing (a route line at this zoom doesn't need them all). * Steps are kept verbatim — they're what you actually read. */ const KEY = 'skatespots:route-cache:v1'; const MAX_ENTRIES = 8; const MAX_POINTS = 250; interface StoredRoute { at: number; distanceM: number; durationS: number; steps?: RouteResult['steps']; coordinates: Array<[number, number]>; } type Store = Record; function keyFor(from: { lat: number; lng: number }, to: { lat: number; lng: number }, mode: string): string { // Origin is part of the key: a route saved from a different start point is not // a valid fallback for the one the user chose (the "Saved route" label would lie). return `${from.lat.toFixed(4)},${from.lng.toFixed(4)}>${to.lat.toFixed(4)},${to.lng.toFixed(4)}:${mode}`; } function read(): Store { try { const raw = localStorage.getItem(KEY); return raw ? (JSON.parse(raw) as Store) : {}; } catch { return {}; } } function write(store: Store): void { try { localStorage.setItem(KEY, JSON.stringify(store)); } catch { // quota / private mode — a cache is a nicety, never an error } } /** Keep every Nth point so the stored line stays small. */ function downsample(coords: Array<[number, number]>, max = MAX_POINTS): Array<[number, number]> { if (coords.length <= max) return coords; const step = Math.ceil(coords.length / max); const out: Array<[number, number]> = []; for (let i = 0; i < coords.length; i += step) out.push(coords[i]); const last = coords[coords.length - 1]; if (out[out.length - 1] !== last) out.push(last); return out; } /** Remember a freshly-fetched route (best effort). */ export function saveRoute( from: { lat: number; lng: number }, to: { lat: number; lng: number }, mode: string, route: RouteResult, ): void { if (!route.coordinates?.length) return; const store = read(); store[keyFor(from, to, mode)] = { at: Date.now(), distanceM: route.distanceM, durationS: route.durationS, steps: route.steps, coordinates: downsample(route.coordinates), }; // Keep only the newest few destinations so this can never grow unbounded. const newest = Object.entries(store) .sort((a, b) => b[1].at - a[1].at) .slice(0, MAX_ENTRIES); write(Object.fromEntries(newest)); } /** Read a cached route for this destination+mode, with how old it is. */ export function loadRoute( from: { lat: number; lng: number }, to: { lat: number; lng: number }, mode: string, ): { route: RouteResult; ageMs: number } | null { const stored = read()[keyFor(from, to, mode)]; if (!stored || !Array.isArray(stored.coordinates) || stored.coordinates.length < 2) return null; return { route: { coordinates: stored.coordinates, distanceM: stored.distanceM, durationS: stored.durationS, steps: stored.steps, mode: mode as RouteResult['mode'], approximate: false, }, ageMs: Date.now() - stored.at, }; } /** "just now" | "12 min ago" | "2 h ago" — for the cached-route badge. */ export function formatAge(ageMs: number, t: TranslateFn): string { const mins = Math.round(ageMs / 60000); if (mins < 1) return t('routeUi.ageJustNow'); if (mins < 60) return t('routeUi.ageMinAgo', { count: mins }); const hours = Math.round(mins / 60); if (hours < 24) return t('routeUi.ageHourAgo', { count: hours }); return t('routeUi.ageDayAgo', { count: Math.round(hours / 24) }); }