diff options
Diffstat (limited to 'src/frontend/app')
| -rw-r--r-- | src/frontend/app/components/StopItemSkeleton.tsx | 2 | ||||
| -rw-r--r-- | src/frontend/app/components/StopSummarySheet.tsx | 2 | ||||
| -rw-r--r-- | src/frontend/app/data/StopDataProvider.ts | 127 | ||||
| -rw-r--r-- | src/frontend/app/routes/map.tsx | 28 | ||||
| -rw-r--r-- | src/frontend/app/routes/stops-$id.tsx | 49 |
5 files changed, 136 insertions, 72 deletions
diff --git a/src/frontend/app/components/StopItemSkeleton.tsx b/src/frontend/app/components/StopItemSkeleton.tsx index 68172fd..778b5e1 100644 --- a/src/frontend/app/components/StopItemSkeleton.tsx +++ b/src/frontend/app/components/StopItemSkeleton.tsx @@ -4,7 +4,7 @@ import "react-loading-skeleton/dist/skeleton.css"; interface StopItemSkeletonProps { showId?: boolean; - stopId?: number; + stopId?: string; } const StopItemSkeleton: React.FC<StopItemSkeletonProps> = ({ diff --git a/src/frontend/app/components/StopSummarySheet.tsx b/src/frontend/app/components/StopSummarySheet.tsx index 17c0afd..e85dda3 100644 --- a/src/frontend/app/components/StopSummarySheet.tsx +++ b/src/frontend/app/components/StopSummarySheet.tsx @@ -26,7 +26,7 @@ interface ErrorInfo { } const loadConsolidatedData = async ( - stopId: number + stopId: string ): Promise<ConsolidatedCirculation[]> => { const resp = await fetch( `${REGION_DATA.consolidatedCirculationsEndpoint}?stopId=${stopId}`, diff --git a/src/frontend/app/data/StopDataProvider.ts b/src/frontend/app/data/StopDataProvider.ts index abe7123..920c7e1 100644 --- a/src/frontend/app/data/StopDataProvider.ts +++ b/src/frontend/app/data/StopDataProvider.ts @@ -11,7 +11,8 @@ export type StopName = { }; export interface Stop { - stopId: number; + stopId: string; + type?: 'bus' | 'train'; name: StopName; latitude?: number; longitude?: number; @@ -27,29 +28,46 @@ export interface Stop { // In-memory cache and lookup map per region const cachedStopsByRegion: Record<string, Stop[] | null> = {}; -const stopsMapByRegion: Record<string, Record<number, Stop>> = {}; +const stopsMapByRegion: Record<string, Record<string, Stop>> = {}; // Custom names loaded from localStorage per region -const customNamesByRegion: Record<string, Record<number, string>> = {}; +const customNamesByRegion: Record<string, Record<string, string>> = {}; + +// Helper to normalize ID +function normalizeId(id: number | string): string { + const s = String(id); + if (s.includes(':')) return s; + return `vitrasa:${s}`; +} // Initialize cachedStops and customNames once per region async function initStops() { if (!cachedStopsByRegion[REGION_DATA.id]) { const response = await fetch(REGION_DATA.stopsEndpoint); - const stops = (await response.json()) as Stop[]; + const rawStops = (await response.json()) as any[]; + // build array and map stopsMapByRegion[REGION_DATA.id] = {}; - cachedStopsByRegion[REGION_DATA.id] = stops.map((stop) => { - const entry = { ...stop, favourite: false } as Stop; - stopsMapByRegion[REGION_DATA.id][stop.stopId] = entry; + cachedStopsByRegion[REGION_DATA.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[REGION_DATA.id][id] = entry; return entry; }); + // load custom names const rawCustom = localStorage.getItem(`customStopNames_${REGION_DATA.id}`); if (rawCustom) { - customNamesByRegion[REGION_DATA.id] = JSON.parse(rawCustom) as Record< - number, - string - >; + const parsed = JSON.parse(rawCustom); + const normalized: Record<string, string> = {}; + for (const [key, value] of Object.entries(parsed)) { + normalized[normalizeId(key)] = value as string; + } + customNamesByRegion[REGION_DATA.id] = normalized; } else { customNamesByRegion[REGION_DATA.id] = {}; } @@ -60,7 +78,8 @@ async function getStops(): Promise<Stop[]> { await initStops(); // update favourites const rawFav = localStorage.getItem("favouriteStops_vigo"); - const favouriteStops = rawFav ? (JSON.parse(rawFav) as number[]) : []; + const favouriteStops = rawFav ? (JSON.parse(rawFav) as (number|string)[]).map(normalizeId) : []; + cachedStopsByRegion["vigo"]!.forEach( (stop) => (stop.favourite = favouriteStops.includes(stop.stopId)) ); @@ -69,14 +88,15 @@ async function getStops(): Promise<Stop[]> { // New: get single stop by id async function getStopById( - stopId: number + stopId: string | number ): Promise<Stop | undefined> { await initStops(); - const stop = stopsMapByRegion[REGION_DATA.id]?.[stopId]; + const id = normalizeId(stopId); + const stop = stopsMapByRegion[REGION_DATA.id]?.[id]; if (stop) { const rawFav = localStorage.getItem(`favouriteStops_${REGION_DATA.id}`); - const favouriteStops = rawFav ? (JSON.parse(rawFav) as number[]) : []; - stop.favourite = favouriteStops.includes(stopId); + const favouriteStops = rawFav ? (JSON.parse(rawFav) as (number|string)[]).map(normalizeId) : []; + stop.favourite = favouriteStops.includes(id); } return stop; } @@ -90,20 +110,22 @@ function getDisplayName(stop: Stop): string { } // New: set or remove custom names -function setCustomName(stopId: number, label: string) { +function setCustomName(stopId: string | number, label: string) { + const id = normalizeId(stopId); if (!customNamesByRegion[REGION_DATA.id]) { customNamesByRegion[REGION_DATA.id] = {}; } - customNamesByRegion[REGION_DATA.id][stopId] = label; + customNamesByRegion[REGION_DATA.id][id] = label; localStorage.setItem( `customStopNames_${REGION_DATA.id}`, JSON.stringify(customNamesByRegion[REGION_DATA.id]) ); } -function removeCustomName(stopId: number) { - if (customNamesByRegion[REGION_DATA.id]?.[stopId]) { - delete customNamesByRegion[REGION_DATA.id][stopId]; +function removeCustomName(stopId: string | number) { + const id = normalizeId(stopId); + if (customNamesByRegion[REGION_DATA.id]?.[id]) { + delete customNamesByRegion[REGION_DATA.id][id]; localStorage.setItem( `customStopNames_${REGION_DATA.id}`, JSON.stringify(customNamesByRegion[REGION_DATA.id]) @@ -112,19 +134,21 @@ function removeCustomName(stopId: number) { } // New: get custom label for a stop -function getCustomName(stopId: number): string | undefined { - return customNamesByRegion[REGION_DATA.id]?.[stopId]; +function getCustomName(stopId: string | number): string | undefined { + const id = normalizeId(stopId); + return customNamesByRegion[REGION_DATA.id]?.[id]; } -function addFavourite(stopId: number) { +function addFavourite(stopId: string | number) { + const id = normalizeId(stopId); const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); - let favouriteStops: number[] = []; + let favouriteStops: string[] = []; if (rawFavouriteStops) { - favouriteStops = JSON.parse(rawFavouriteStops) as number[]; + favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); } - if (!favouriteStops.includes(stopId)) { - favouriteStops.push(stopId); + if (!favouriteStops.includes(id)) { + favouriteStops.push(id); localStorage.setItem( `favouriteStops_vigo`, JSON.stringify(favouriteStops) @@ -132,42 +156,45 @@ function addFavourite(stopId: number) { } } -function removeFavourite(stopId: number) { +function removeFavourite(stopId: string | number) { + const id = normalizeId(stopId); const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); - let favouriteStops: number[] = []; + let favouriteStops: string[] = []; if (rawFavouriteStops) { - favouriteStops = JSON.parse(rawFavouriteStops) as number[]; + favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); } - const newFavouriteStops = favouriteStops.filter((id) => id !== stopId); + const newFavouriteStops = favouriteStops.filter((sid) => sid !== id); localStorage.setItem( `favouriteStops_vigo`, JSON.stringify(newFavouriteStops) ); } -function isFavourite(stopId: number): boolean { +function isFavourite(stopId: string | number): boolean { + const id = normalizeId(stopId); const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); if (rawFavouriteStops) { - const favouriteStops = JSON.parse(rawFavouriteStops) as number[]; - return favouriteStops.includes(stopId); + const favouriteStops = (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); + return favouriteStops.includes(id); } return false; } const RECENT_STOPS_LIMIT = 10; -function pushRecent(stopId: number) { +function pushRecent(stopId: string | number) { + const id = normalizeId(stopId); const rawRecentStops = localStorage.getItem(`recentStops_vigo`); - let recentStops: Set<number> = new Set(); + let recentStops: Set<string> = new Set(); if (rawRecentStops) { - recentStops = new Set(JSON.parse(rawRecentStops) as number[]); + recentStops = new Set((JSON.parse(rawRecentStops) as (number|string)[]).map(normalizeId)); } - recentStops.add(stopId); + recentStops.add(id); if (recentStops.size > RECENT_STOPS_LIMIT) { const iterator = recentStops.values(); - const val = iterator.next().value as number; + const val = iterator.next().value as string; recentStops.delete(val); } @@ -177,18 +204,18 @@ function pushRecent(stopId: number) { ); } -function getRecent(): number[] { +function getRecent(): string[] { const rawRecentStops = localStorage.getItem(`recentStops_vigo`); if (rawRecentStops) { - return JSON.parse(rawRecentStops) as number[]; + return (JSON.parse(rawRecentStops) as (number|string)[]).map(normalizeId); } return []; } -function getFavouriteIds(): number[] { +function getFavouriteIds(): string[] { const rawFavouriteStops = localStorage.getItem(`favouriteStops_vigo`); if (rawFavouriteStops) { - return JSON.parse(rawFavouriteStops) as number[]; + return (JSON.parse(rawFavouriteStops) as (number|string)[]).map(normalizeId); } return []; } @@ -196,8 +223,16 @@ function getFavouriteIds(): number[] { // New function to load stops from network async function loadStopsFromNetwork(): Promise<Stop[]> { const response = await fetch(REGION_DATA.stopsEndpoint); - const stops = (await response.json()) as Stop[]; - return stops.map((stop) => ({ ...stop, favourite: false }) as Stop); + 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; + }); } export default { diff --git a/src/frontend/app/routes/map.tsx b/src/frontend/app/routes/map.tsx index 461e891..402bf60 100644 --- a/src/frontend/app/routes/map.tsx +++ b/src/frontend/app/routes/map.tsx @@ -35,7 +35,7 @@ export default function StopMap() { const [stops, setStops] = useState< GeoJsonFeature< Point, - { stopId: number; name: string; lines: string[]; cancelled?: boolean } + { stopId: string; name: string; lines: string[]; cancelled?: boolean, prefix: string } >[] >([]); const [selectedStop, setSelectedStop] = useState<Stop | null>(null); @@ -65,7 +65,7 @@ export default function StopMap() { StopDataProvider.getStops().then((data) => { const features: GeoJsonFeature< Point, - { stopId: number; name: string; lines: string[]; cancelled?: boolean } + { stopId: string; name: string; lines: string[]; cancelled?: boolean, prefix: string } >[] = data.map((s) => ({ type: "Feature", geometry: { @@ -77,6 +77,7 @@ export default function StopMap() { name: s.name.original, lines: s.lines, cancelled: s.cancelled ?? false, + prefix: s.stopId.startsWith("renfe:") ? "stop-renfe" : (s.cancelled ? "stop-vitrasa-cancelled" : "stop-vitrasa"), }, })); setStops(features); @@ -152,7 +153,7 @@ export default function StopMap() { return; } - const stopId = parseInt(props.stopId, 10); + const stopId = props.stopId; // fetch full stop to get lines array StopDataProvider.getStopById(stopId) @@ -200,25 +201,23 @@ export default function StopMap() { <Layer id="stops" type="symbol" - minzoom={13} + minzoom={11} source="stops-source" layout={{ "icon-image": [ - "case", - ["coalesce", ["get", "cancelled"], false], - `stop-vigo-cancelled`, - `stop-vigo`, + "get", + "prefix" ], "icon-size": [ "interpolate", ["linear"], ["zoom"], 13, - 0.4, - 14, 0.7, + 16, + 0.8, 18, - 1.0, + 1.2, ], "icon-allow-overlap": true, "icon-ignore-placement": true, @@ -239,7 +238,12 @@ export default function StopMap() { "text-size": ["interpolate", ["linear"], ["zoom"], 11, 8, 22, 16], }} paint={{ - "text-color": `${REGION_DATA.textColour}`, + "text-color": [ + "case", + ["==", ["get", "prefix"], "stop-renfe"], + "#870164", + "#e72b37" + ], "text-halo-color": "#FFF", "text-halo-width": 1, }} diff --git a/src/frontend/app/routes/stops-$id.tsx b/src/frontend/app/routes/stops-$id.tsx index 31cc75f..d836c12 100644 --- a/src/frontend/app/routes/stops-$id.tsx +++ b/src/frontend/app/routes/stops-$id.tsx @@ -72,10 +72,35 @@ const loadConsolidatedData = async ( return await resp.json(); }; +export interface ConsolidatedCirculation { + line: string; + route: string; + schedule?: { + running: boolean; + minutes: number; + serviceId: string; + tripId: string; + shapeId?: string; + }; + realTime?: { + minutes: number; + distance: number; + }; + currentPosition?: { + latitude: number; + longitude: number; + orientationDegrees: number; + shapeIndex?: number; + }; + isPreviousTrip?: boolean; + previousTripShapeId?: string; + nextStreets?: string[]; +} + export default function Estimates() { const { t } = useTranslation(); const params = useParams(); - const stopIdNum = parseInt(params.id ?? ""); + const stopId = params.id ?? ""; const [customName, setCustomName] = useState<string | undefined>(undefined); const [stopData, setStopData] = useState<Stop | undefined>(undefined); @@ -98,8 +123,8 @@ export default function Estimates() { if (customName) return customName; if (stopData?.name.intersect) return stopData.name.intersect; if (stopData?.name.original) return stopData.name.original; - return `Parada ${stopIdNum}`; - }, [customName, stopData, stopIdNum]); + return `Parada ${stopId}`; + }, [customName, stopData, stopId]); usePageTitle(getStopDisplayName()); @@ -128,21 +153,21 @@ export default function Estimates() { try { setDataError(null); - const body = await loadConsolidatedData(params.id!); + const body = await loadConsolidatedData(stopId); setData(body); setDataDate(new Date()); // Load stop data from StopDataProvider - const stop = await StopDataProvider.getStopById(stopIdNum); + const stop = await StopDataProvider.getStopById(stopId); setStopData(stop); - setCustomName(StopDataProvider.getCustomName(stopIdNum)); + setCustomName(StopDataProvider.getCustomName(stopId)); } catch (error) { console.error("Error loading consolidated data:", error); setDataError(parseError(error)); setData(null); setDataDate(null); } - }, [params.id, stopIdNum]); + }, [stopId]); const refreshData = useCallback(async () => { await Promise.all([loadData()]); @@ -170,19 +195,19 @@ export default function Estimates() { setDataLoading(true); loadData(); - StopDataProvider.pushRecent(parseInt(params.id ?? "")); + StopDataProvider.pushRecent(stopId); setFavourited( - StopDataProvider.isFavourite(parseInt(params.id ?? "")) + StopDataProvider.isFavourite(stopId) ); setDataLoading(false); - }, [params.id, loadData]); + }, [stopId, loadData]); const toggleFavourite = () => { if (favourited) { - StopDataProvider.removeFavourite(stopIdNum); + StopDataProvider.removeFavourite(stopId); setFavourited(false); } else { - StopDataProvider.addFavourite(stopIdNum); + StopDataProvider.addFavourite(stopId); setFavourited(true); } }; |
