import React, {useEffect, useState} from 'react'; import PanelWrapper from "../../meta/PanelWrapper"; import PlanElement from "./components/PlanElement"; import PanelTitle from "../../meta/PanelTitle"; import PanelContent from "../../meta/PanelContent"; import {StationResponse} from "./types/vrrfAPI"; import {Warning} from "@phosphor-icons/react"; import {motion} from 'framer-motion'; export type FahrplanPanelDefinition = { stops: string[], filter: { types?: string[], destinations?: string[] } } type Route = { uid: string, identifier: string, heading: string, stops: { name: string, arrival: Date, delay?: number, countdown: number, cancelled: boolean }[], countdown: number } const FahrplanPanel = (props: {definition: FahrplanPanelDefinition}) => { const [routes, setRoutes] = useState<Route[]>([]); useEffect(() => { const update = async () => { const departures = (await Promise.all(props.definition.stops.map(getStopData))) .map(d => d.raw) .flat(); let newRoutes: Route[] = []; // Determine stop data from the departures for(let departure of departures) { // First throw away all data that belongs to a filtered category const filterTypes = props.definition.filter.types ?? []; if(filterTypes.includes(departure.type)) { continue; } // Find existing route with same uid const existing_ind = newRoutes.findIndex(r => r.uid === departure.key + "-" + departure.lineref.identifier) // Pre-compute values that will be needed regardless const delay = stringToDelay(departure.delay); const arrival = processArrival(departure.sched_date, departure.time); // Throw away stops that are five hours in the future as keys start colliding at some point if(arrival.getTime() >= 5 * 60 * 60 * 1000 + (new Date()).getTime()) { continue; } if(existing_ind === -1) { // If it does not exist, create a new route newRoutes.push({ uid: departure.key + "-" + departure.lineref.identifier, heading: departure.destination, identifier: departure.line, stops: [ { name: departure.internal.stop, arrival, delay, countdown: parseInt(departure.countdown), cancelled: departure.is_cancelled === 1 } ], countdown: parseInt(departure.countdown) }) } else { // If it does, just add a stop to the existing route newRoutes[existing_ind].stops.push({ name: departure.internal.stop, arrival, delay: stringToDelay(departure.delay), countdown: parseInt(departure.countdown), cancelled: departure.is_cancelled === 1 }) newRoutes[existing_ind].stops = newRoutes[existing_ind].stops .sort((a, b) => a.countdown - b.countdown) } } // Sort the output newRoutes = newRoutes.sort((a, b) => { const diff = a.stops[0].arrival.getTime() - b.stops[0].arrival.getTime(); if(diff !== 0) { return diff; } const latestA = Math.max(...a.stops.map(s => s.arrival.getTime())); const latestB = Math.max(...b.stops.map(s => s.arrival.getTime())); return latestA - latestB; }) // Write to the display setRoutes(newRoutes); } update(); const interval = setInterval(update, 2 * 60 * 1000); return () => { clearInterval(interval); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <PanelWrapper> <PanelTitle title={"ÖPNV Monitor"}/> <PanelContent className={"flex flex-col"}> <motion.div layout transition={{duration: .3, ease: "easeOut"}} className={"flex-1 flex flex-col gap-3"}> {routes.map((route) => ( <PlanElement key={route.uid} tripId={route.uid} trainIdentifier={route.identifier} trainHeading={route.heading} stops={route.stops} /> ))} {routes.length === 0 && ( <div className={"flex-1 flex justify-center items-center"}> <div className={"mb-10 flex flex-col items-center"}> <Warning size={48} className={"mb-3"}/> <p className={"max-w-xs text-center text-zinc-400"}> Aktuell sind keine Abfahrtsdaten verfügbar. </p> </div> </div> )} </motion.div> </PanelContent> </PanelWrapper> ); }; export default FahrplanPanel; async function getStopData(stop: string): Promise<StationResponse> { const request = await fetch(`https://vrrf.finalrewind.org/${encodeURIComponent(stop)}.json`); if(!request.ok) { throw new Error("Converting stop did not work"); } const data = await request.json(); if(data.error) { console.warn("Stop data for", stop, "could not be fetched"); } // Add internal reference data data.raw = data.raw.map((r: any) => ({ ...r, internal: { stop } })) console.log(data); return data as StationResponse; } function stringToDelay(input: string): number | undefined { const delay = parseInt(input); if(delay === 0) { return undefined; } if(isNaN(delay)) { console.warn("While parsing delay, the string was not interpretable as number", input); return undefined; } return delay; } function processArrival(date: string, time: string): Date { const d_parts = date.split("."); return new Date(`${d_parts[2]}-${d_parts[1]}-${d_parts[0]} ${time}`); }