From b2ddc0ef449ccbe7f0d33e539ccdfc1baef04e2c Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Sun, 28 Dec 2025 18:21:17 +0100 Subject: Get favourite stops from OTP instead of pre-generated file --- src/frontend/app/data/StopDataProvider.ts | 262 +++++++++++++++++------------- 1 file changed, 153 insertions(+), 109 deletions(-) (limited to 'src/frontend/app/data/StopDataProvider.ts') diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts index 697e171..76182c7 100644 --- a/src/frontend/app/data/StopDataProvider.ts +++ b/src/frontend/app/data/StopDataProvider.ts @@ -1,28 +1,31 @@ import { APP_CONSTANTS } from "~/config/constants"; -export interface CachedStopList { - timestamp: number; - data: Stop[]; -} - export interface Stop { stopId: string; + stopCode?: string; name: string; latitude?: number; longitude?: number; - lines: string[]; + lines: { + line: string; + colour: string; + textColour: string; + }[]; favourite?: boolean; - amenities?: string[]; + type?: "bus" | "coach" | "train" | "unknown"; +} - title?: string; - message?: string; - alert?: "info" | "warning" | "error"; - cancelled?: boolean; +interface CacheEntry { + stop: Stop; + timestamp: number; } -// In-memory cache and lookup map per region -const cachedStopsByRegion: Record = {}; -const stopsMapByRegion: Record> = {}; +const CACHE_KEY = `stops_cache_${APP_CONSTANTS.id}`; +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 const customNamesByRegion: Record> = {}; @@ -33,82 +36,105 @@ function normalizeId(id: number | string): string { return `vitrasa:${s}`; } -// Initialize cachedStops and customNames once per region -async function initStops() { - if (!cachedStopsByRegion[APP_CONSTANTS.id]) { - const response = await fetch(APP_CONSTANTS.stopsEndpoint); - const rawStops = (await response.json()) as any[]; - - // build array and map - stopsMapByRegion[APP_CONSTANTS.id] = {}; - cachedStopsByRegion[APP_CONSTANTS.id] = rawStops.map((raw) => { - const id = normalizeId(raw.stopId); - const entry = { - ...raw, - stopId: id, - type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"), - favourite: false, - } as Stop; - stopsMapByRegion[APP_CONSTANTS.id][id] = entry; - return entry; - }); - - // load custom names - const rawCustom = localStorage.getItem( - `customStopNames_${APP_CONSTANTS.id}` - ); - if (rawCustom) { - const parsed = JSON.parse(rawCustom); - const normalized: Record = {}; - for (const [key, value] of Object.entries(parsed)) { - normalized[normalizeId(key)] = value as string; - } - customNamesByRegion[APP_CONSTANTS.id] = normalized; - } else { - customNamesByRegion[APP_CONSTANTS.id] = {}; - } +function getPersistentCache(): Record { + const raw = localStorage.getItem(CACHE_KEY); + if (!raw) return {}; + try { + return JSON.parse(raw); + } catch { + return {}; } } -async function getStops(): Promise { - await initStops(); - // update favourites - const rawFav = localStorage.getItem("favouriteStops"); - const favouriteStops = rawFav - ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId) - : []; +function savePersistentCache(cache: Record) { + localStorage.setItem(CACHE_KEY, JSON.stringify(cache)); +} - cachedStopsByRegion["vigo"]!.forEach( - (stop) => (stop.favourite = favouriteStops.includes(stop.stopId)) - ); - return cachedStopsByRegion["vigo"]!; +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; } -// New: get single stop by id async function getStopById(stopId: string | number): Promise { - await initStops(); const id = normalizeId(stopId); - const stop = stopsMapByRegion[APP_CONSTANTS.id]?.[id]; + const stops = await fetchStopsByIds([id]); + const stop = stops[id]; if (stop) { - const rawFav = localStorage.getItem(`favouriteStops_${APP_CONSTANTS.id}`); - const favouriteStops = rawFav - ? (JSON.parse(rawFav) as (number | string)[]).map(normalizeId) - : []; - stop.favourite = favouriteStops.includes(id); + stop.favourite = isFavourite(id); } return stop; } -// Updated display name to include custom names function getDisplayName(stop: Stop): string { - return stop.name; + const custom = getCustomName(stop.stopId); + return custom || stop.name; } -// New: set or remove custom names function setCustomName(stopId: string | number, label: string) { const id = normalizeId(stopId); if (!customNamesByRegion[APP_CONSTANTS.id]) { - customNamesByRegion[APP_CONSTANTS.id] = {}; + const rawCustom = localStorage.getItem( + `customStopNames_${APP_CONSTANTS.id}` + ); + customNamesByRegion[APP_CONSTANTS.id] = rawCustom + ? JSON.parse(rawCustom) + : {}; } customNamesByRegion[APP_CONSTANTS.id][id] = label; localStorage.setItem( @@ -119,7 +145,15 @@ function setCustomName(stopId: string | number, label: string) { function removeCustomName(stopId: string | number) { const id = normalizeId(stopId); - if (customNamesByRegion[APP_CONSTANTS.id]?.[id]) { + if (!customNamesByRegion[APP_CONSTANTS.id]) { + const rawCustom = localStorage.getItem( + `customStopNames_${APP_CONSTANTS.id}` + ); + customNamesByRegion[APP_CONSTANTS.id] = rawCustom + ? JSON.parse(rawCustom) + : {}; + } + if (customNamesByRegion[APP_CONSTANTS.id][id]) { delete customNamesByRegion[APP_CONSTANTS.id][id]; localStorage.setItem( `customStopNames_${APP_CONSTANTS.id}`, @@ -128,15 +162,24 @@ function removeCustomName(stopId: string | number) { } } -// New: get custom label for a stop function getCustomName(stopId: string | number): string | undefined { const id = normalizeId(stopId); - return customNamesByRegion[APP_CONSTANTS.id]?.[id]; + if (!customNamesByRegion[APP_CONSTANTS.id]) { + const rawCustom = localStorage.getItem( + `customStopNames_${APP_CONSTANTS.id}` + ); + customNamesByRegion[APP_CONSTANTS.id] = rawCustom + ? JSON.parse(rawCustom) + : {}; + } + return customNamesByRegion[APP_CONSTANTS.id][id]; } function addFavourite(stopId: string | number) { const id = normalizeId(stopId); - const rawFavouriteStops = localStorage.getItem(`favouriteStops`); + const rawFavouriteStops = localStorage.getItem( + `favouriteStops_${APP_CONSTANTS.id}` + ); let favouriteStops: string[] = []; if (rawFavouriteStops) { favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map( @@ -146,13 +189,18 @@ function addFavourite(stopId: string | number) { if (!favouriteStops.includes(id)) { favouriteStops.push(id); - localStorage.setItem(`favouriteStops`, JSON.stringify(favouriteStops)); + localStorage.setItem( + `favouriteStops_${APP_CONSTANTS.id}`, + JSON.stringify(favouriteStops) + ); } } function removeFavourite(stopId: string | number) { const id = normalizeId(stopId); - const rawFavouriteStops = localStorage.getItem(`favouriteStops`); + const rawFavouriteStops = localStorage.getItem( + `favouriteStops_${APP_CONSTANTS.id}` + ); let favouriteStops: string[] = []; if (rawFavouriteStops) { favouriteStops = (JSON.parse(rawFavouriteStops) as (number | string)[]).map( @@ -161,12 +209,17 @@ function removeFavourite(stopId: string | number) { } const newFavouriteStops = favouriteStops.filter((sid) => sid !== id); - localStorage.setItem(`favouriteStops`, JSON.stringify(newFavouriteStops)); + localStorage.setItem( + `favouriteStops_${APP_CONSTANTS.id}`, + JSON.stringify(newFavouriteStops) + ); } function isFavourite(stopId: string | number): boolean { const id = normalizeId(stopId); - const rawFavouriteStops = localStorage.getItem(`favouriteStops`); + const rawFavouriteStops = localStorage.getItem( + `favouriteStops_${APP_CONSTANTS.id}` + ); if (rawFavouriteStops) { const favouriteStops = ( JSON.parse(rawFavouriteStops) as (number | string)[] @@ -180,29 +233,34 @@ const RECENT_STOPS_LIMIT = 10; function pushRecent(stopId: string | number) { const id = normalizeId(stopId); - const rawRecentStops = localStorage.getItem(`recentStops_vigo`); - let recentStops: Set = new Set(); + const rawRecentStops = localStorage.getItem( + `recentStops_${APP_CONSTANTS.id}` + ); + let recentStops: string[] = []; if (rawRecentStops) { - recentStops = new Set( - (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId) + recentStops = (JSON.parse(rawRecentStops) as (number | string)[]).map( + normalizeId ); } - recentStops.add(id); - if (recentStops.size > RECENT_STOPS_LIMIT) { - const iterator = recentStops.values(); - const val = iterator.next().value as string; - recentStops.delete(val); + // 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_vigo`, - JSON.stringify(Array.from(recentStops)) + `recentStops_${APP_CONSTANTS.id}`, + JSON.stringify(recentStops) ); } function getRecent(): string[] { - const rawRecentStops = localStorage.getItem(`recentStops_vigo`); + const rawRecentStops = localStorage.getItem( + `recentStops_${APP_CONSTANTS.id}` + ); if (rawRecentStops) { return (JSON.parse(rawRecentStops) as (number | string)[]).map(normalizeId); } @@ -210,7 +268,9 @@ function getRecent(): string[] { } function getFavouriteIds(): string[] { - const rawFavouriteStops = localStorage.getItem(`favouriteStops`); + const rawFavouriteStops = localStorage.getItem( + `favouriteStops_${APP_CONSTANTS.id}` + ); if (rawFavouriteStops) { return (JSON.parse(rawFavouriteStops) as (number | string)[]).map( normalizeId @@ -219,28 +279,13 @@ function getFavouriteIds(): string[] { return []; } -// New function to load stops from network -async function loadStopsFromNetwork(): Promise { - const response = await fetch(APP_CONSTANTS.stopsEndpoint); - const rawStops = (await response.json()) as any[]; - return rawStops.map((raw) => { - const id = normalizeId(raw.stopId); - return { - ...raw, - stopId: id, - type: raw.type || (id.startsWith("renfe:") ? "train" : "bus"), - favourite: false, - } as Stop; - }); -} - function getTileUrlTemplate(): string { return window.location.origin + "/api/tiles/stops/{z}/{x}/{y}"; } export default { - getStops, getStopById, + fetchStopsByIds, getCustomName, getDisplayName, setCustomName, @@ -251,6 +296,5 @@ export default { pushRecent, getRecent, getFavouriteIds, - loadStopsFromNetwork, getTileUrlTemplate, }; -- cgit v1.3