/** * savedPoints.ts — browser-local "Saved Points" store. * ---------------------------------------------------------------------- * A Saved Point is a user-clicked location that isn't a known POI. Points are * persisted to localStorage so they survive reloads, and are rendered as a * toggleable "Saved Points" layer. They can also serve as route origins / * destinations. * * The store is a Svelte writable store; subscribe to it to re-render the layer * when points change. */ import { writable } from 'svelte/store'; export interface SavedPoint { id: string; lat: number; lon: number; label?: string; created: number; } const KEY = 'navigator.savedPoints.v1'; function load(): SavedPoint[] { try { const raw = localStorage.getItem(KEY); if (!raw) return []; const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) return []; return parsed .filter((p) => p && typeof p.lat === 'number' && typeof p.lon === 'number') .map((p) => ({ id: String(p.id ?? `${p.lat},${p.lon}`), lat: p.lat, lon: p.lon, label: typeof p.label === 'string' ? p.label : undefined, created: typeof p.created === 'number' ? p.created : Date.now() })); } catch { return []; } } function createStore() { const store = writable(load()); store.subscribe((points) => { try { localStorage.setItem(KEY, JSON.stringify(points)); } catch { /* storage unavailable — ignore */ } }); return store; } export const savedPoints = createStore(); export function addSavedPoint(point: Omit): SavedPoint { const sp: SavedPoint = { ...point, id: crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random()}`, created: Date.now() }; savedPoints.update((pts) => [...pts, sp]); return sp; } export function removeSavedPoint(id: string): void { savedPoints.update((pts) => pts.filter((p) => p.id !== id)); } export function clearSavedPoints(): void { savedPoints.set([]); } /** * Serialize the current points to a JSON string (for export / download). */ export function exportSavedPoints(): string { let pts: SavedPoint[] = []; savedPoints.subscribe((p) => (pts = p))(); return JSON.stringify({ type: 'navigator.savedPoints', version: 1, points: pts }, null, 2); } /** * Merge imported points into the store, de-duplicating by id (existing ids are * kept; new ids are added). Returns the number of points added. */ export function importSavedPoints(json: string): { added: number; skipped: number } { let parsed: unknown; try { parsed = JSON.parse(json); } catch { throw new Error('Invalid JSON file.'); } const list = (parsed as { points?: unknown[] })?.points; if (!Array.isArray(list)) throw new Error('Unrecognized file format (expected { points: [...] }).'); const valid: SavedPoint[] = list .filter((p): p is SavedPoint => !!p && typeof (p as SavedPoint).lat === 'number' && typeof (p as SavedPoint).lon === 'number') .map((p) => ({ id: String(p.id ?? `${p.lat},${p.lon}`), lat: p.lat, lon: p.lon, label: typeof p.label === 'string' ? p.label : undefined, created: typeof p.created === 'number' ? p.created : Date.now() })); let added = 0; let skipped = 0; savedPoints.update((pts) => { const existing = new Set(pts.map((p) => p.id)); const out = [...pts]; for (const p of valid) { if (existing.has(p.id)) { skipped++; continue; } out.push(p); existing.add(p.id); added++; } return out; }); return { added, skipped }; }