import { writeFavorites } from "~/utils/idb"; export interface Stop { stopId: string; stopCode?: string; name: string; latitude?: number; longitude?: number; lines: { line: string; colour: string; textColour: string; }[]; favourite?: boolean; type?: "bus" | "coach" | "train" | "unknown"; } interface CacheEntry { stop: Stop; timestamp: number; } const CACHE_KEY = `stops_cache`; const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24 hours // In-memory cache for the current session const memoryCache: Record = {}; // Custom names loaded from localStorage per region let customNames: Record = {}; // Helper to normalize ID function normalizeId(id: number | string): string { const s = String(id); if (s.includes(":")) return s; return `vitrasa:${s}`; } function getPersistentCache(): Record { const raw = localStorage.getItem(CACHE_KEY); if (!raw) return {}; try { return JSON.parse(raw); } catch { return {}; } } function savePersistentCache(cache: Record) { localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); } async function fetchStopsByIds(ids: string[]): Promise> { if (ids.length === 0) return {}; const normalizedIds = ids.map(normalizeId); const now = Date.now(); const persistentCache = getPersistentCache(); const result: Record = {}; const toFetch: string[] = []; for (const id of normalizedIds) { if (memoryCache[id]) { result[id] = memoryCache[id]; continue; } const cached = persistentCache[id]; if (cached && now - cached.timestamp < CACHE_DURATION) { memoryCache[id] = cached.stop; result[id] = cached.stop; continue; } toFetch.push(id); } if (toFetch.length > 0) { try { const response = await fetch(`/api/stops?ids=${toFetch.join(",")}`); if (!response.ok) throw new Error("Failed to fetch stops"); const data = await response.json(); for (const [id, stopData] of Object.entries(data)) { const stop: Stop = { stopId: (stopData as any).id, stopCode: (stopData as any).code, name: (stopData as any).name, lines: (stopData as any).routes.map((r: any) => ({ line: r.shortName, colour: r.colour, textColour: r.textColour, })), type: (stopData as any).id.startsWith("renfe:") ? "train" : (stopData as any).id.startsWith("xunta:") ? "coach" : "bus", }; memoryCache[id] = stop; result[id] = stop; persistentCache[id] = { stop, timestamp: now }; } savePersistentCache(persistentCache); } catch (error) { console.error("Error fetching stops:", error); } } return result; } async function getStopById(stopId: string | number): Promise { const id = normalizeId(stopId); const stops = await fetchStopsByIds([id]); const stop = stops[id]; if (stop) { stop.favourite = isFavourite(id); } return stop; } function getDisplayName(stop: Stop): string { const custom = getCustomName(stop.stopId); return custom || stop.name; } function setCustomName(stopId: string | number, label: string) { const id = normalizeId(stopId); if (!customNames) { const rawCustom = localStorage.getItem(`customStopNames`); customNames = rawCustom ? JSON.parse(rawCustom) : {}; } customNames[id] = label; localStorage.setItem(`customStopNames`, JSON.stringify(customNames)); } function removeCustomName(stopId: string | number) { const id = normalizeId(stopId); if (!customNames) { const rawCustom = localStorage.getItem(`customStopNames`); customNames = rawCustom ? JSON.parse(rawCustom) : {}; } if (customNames[id]) { delete customNames[id]; localStorage.setItem(`customStopNames`, JSON.stringify(customNames)); } } function getCustomName(stopId: string | number): string | undefined { const id = normalizeId(stopId); if (!customNames) { const rawCustom = localStorage.getItem(`customStopNames`); customNames = rawCustom ? JSON.parse(rawCustom) : {}; } return customNames[id]; } function addFavourite(stopId: string | number) { const id = normalizeId(stopId); const rawFavouriteStops = localStorage.getItem(`favouriteStops`); let favouriteStops: string[] = []; if (rawFavouriteStops) { favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map( normalizeId ); } if (!favouriteStops.includes(id)) { favouriteStops.push(id); localStorage.setItem(`favouriteStops`, JSON.stringify(favouriteStops)); writeFavorites("favouriteStops", favouriteStops).catch(() => { /* best-effort */ }); } } function removeFavourite(stopId: string | number) { const id = normalizeId(stopId); const rawFavouriteStops = localStorage.getItem(`favouriteStops`); let favouriteStops: string[] = []; if (rawFavouriteStops) { favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map( normalizeId ); } const newFavouriteStops = favouriteStops.filter((sid) => sid !== id); localStorage.setItem(`favouriteStops`, JSON.stringify(newFavouriteStops)); writeFavorites("favouriteStops", newFavouriteStops).catch(() => { /* best-effort */ }); } function isFavourite(stopId: string | number): boolean { const id = normalizeId(stopId); const rawFavouriteStops = localStorage.getItem(`favouriteStops`); if (rawFavouriteStops) { const favouriteStops = ( JSON.parse(rawFavouriteStops) as (number | string)[] ).map(normalizeId); return favouriteStops.includes(id); } return false; } const RECENT_STOPS_LIMIT = 10; function pushRecent(stopId: string | number) { const id = normalizeId(stopId); const rawRecentStops = localStorage.getItem(`recentStops`); let recentStops: string[] = []; if (rawRecentStops) { recentStops = (JSON.parse(rawRecentStops) as (number | string)[]).map( normalizeId ); } // Remove if already exists to move to front recentStops = recentStops.filter((sid) => sid !== id); recentStops.unshift(id); if (recentStops.length > RECENT_STOPS_LIMIT) { recentStops = recentStops.slice(0, RECENT_STOPS_LIMIT); } localStorage.setItem(`recentStops`, JSON.stringify(recentStops)); } function getRecent(): string[] { const rawRecentStops = localStorage.getItem(`recentStops`); if (rawRecentStops) { return (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId); } return []; } function getFavouriteIds(): string[] { const rawFavouriteStops = localStorage.getItem(`favouriteStops`); if (rawFavouriteStops) { return (JSON.parse(rawFavouriteStops) as (number | string)[]).map( normalizeId ); } return []; } function getTileUrlTemplate(): string { return window.location.origin + "/api/tiles/stops/{z}/{x}/{y}"; } export default { getStopById, fetchStopsByIds, getCustomName, getDisplayName, setCustomName, removeCustomName, addFavourite, removeFavourite, isFavourite, pushRecent, getRecent, getFavouriteIds, getTileUrlTemplate, };