navigator/src/lib/savedPoints.ts

120 lines
3.4 KiB
TypeScript

/**
* 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<SavedPoint[]>(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, 'id' | 'created'>): 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 };
}