diff --git a/src/panels/Fahrplan/FahrplanPanel.tsx b/src/panels/Fahrplan/FahrplanPanel.tsx index 64c18fd9813c8dfb3c938c081c6a0b2ed8651290..516472d3d3be8a137c763508bf079a24134aa519 100644 --- a/src/panels/Fahrplan/FahrplanPanel.tsx +++ b/src/panels/Fahrplan/FahrplanPanel.tsx @@ -1,40 +1,119 @@ -import React from 'react'; +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-react"; + +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 + }[] +} + +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 + if((props.definition.filter.types ?? []).includes(departure.type)) { + continue; + } + + // Find existing route with same uid + const existing_ind = newRoutes.findIndex(r => r.uid === departure.lineref.identifier) + + // Pre-compute values that will be needed regardless + const delay = stringToDelay(departure.delay); + const arrival = processArrival(departure.sched_date, departure.time); + console.log(arrival) + + if(existing_ind === -1) { + // If it does not exist, create a new route + newRoutes.push({ + uid: departure.key, + heading: departure.lineref.direction, + identifier: departure.line, + stops: [ + { + name: departure.internal.stop, + arrival, + delay + } + ] + }) + } else { + // If it doesn't, just add a stop to the existing route + newRoutes[existing_ind].stops.push({ + name: departure.internal.stop, + arrival, + delay: stringToDelay(departure.delay) + }) + } + } + + // Sort the output + + // Write to the display + setRoutes(newRoutes); + } + + update(); + const interval = setInterval(update, 2 * 60 * 1000); + + return () => { + clearInterval(interval); + } + }, []); -const FahrplanPanel = () => { return ( <PanelWrapper> <PanelTitle title={"ÖPNV Monitor"}/> - <PanelContent> - <div className={"flex flex-col gap-3"}> - <PlanElement trainIdentifier={"S1"} trainHeading={"Dortmund Hbf"} stops={[ - { - name: "Essen", - time: "10:00", - delay: 20 - }, - { - name: "Nicht Essen", - time: "10:00", - delay: 20 - } - ]}/> - - <PlanElement trainIdentifier={"S1"} trainHeading={"Dortmund Hbf"} stops={[ - { - name: "Essen", - time: "10:00", - delay: 20 - }, - { - name: "Nicht Essen", - time: "10:00", - delay: 20 - } - ]}/> + <PanelContent className={"flex flex-col"}> + <div 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> + )} </div> </PanelContent> </PanelWrapper> @@ -44,3 +123,68 @@ const FahrplanPanel = () => { }; 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 { + try { + const delay = parseInt(input); + if(delay === 0) { + return undefined; + } + return delay; + } catch (e) { + console.warn("While parsing delay, the string was not interpretable", input); + return undefined; + } +} + +function processArrival(date: string, time: string): Date { + const d_parts = date.split("."); + + console.log(date, time, "to", `${d_parts[2]}-${d_parts[1]}-${d_parts[0]} ${time}`); + return new Date(`${d_parts[2]}-${d_parts[1]}-${d_parts[0]} ${time}`); +} + +// function sortData(data: any) { +// for (var i = 0; i < data.length; ++i) { +// data[i]['stops'] = data[i]['stops'].sort(sortFn); +// data[i]['timeValue'] = data[i]['stops'][0]['timeValue']; +// } +// return data.sort(sortFn); +// } +// +// function sortFn(a: any, b: any) { +// return a['timeValue'] - b['timeValue']; +// } +// +// function calcDateValue(_year: string, _month: string, _day: string, _hour: string, _minute: string): number { +// const year = parseInt(_year) * 12 * 31 * 24 * 60; +// const month = parseInt(_month) * 31 * 24 * 60; +// const day = parseInt(_day) * 24 * 60; +// const hour = parseInt(_hour) * 60; +// const minute = parseInt(_minute); +// return year+month+day+hour+minute; +// } diff --git a/src/panels/Fahrplan/components/PlanElement.tsx b/src/panels/Fahrplan/components/PlanElement.tsx index 99a78856d1aa7a89a27ff72d6c4159ee35610044..7f366b53af21ac44e39d5513ee48616654c4f771 100644 --- a/src/panels/Fahrplan/components/PlanElement.tsx +++ b/src/panels/Fahrplan/components/PlanElement.tsx @@ -1,20 +1,25 @@ import { motion } from 'framer-motion'; import React from 'react'; import ProgressIndicator from "./ProgressIndicator"; +import classNames from "../../../util/classNames"; const PlanElement = (props: { + tripId: string, trainIdentifier: string, trainHeading: string, stops: { - time: string, - delay?: number, - name: string + name: string, + arrival: Date, + delay?: number }[] }) => { return ( <div> <div className={"flex flex-row gap-4 items-center font-semibold"}> - <div className={"flex justify-center items-center bg-green-700 text-lg h-8 w-16 leading-none"}> + <div className={classNames( + "flex justify-center items-center text-lg h-10 w-20 leading-none", + trainIdentifierToColor(props.trainIdentifier) + )}> {props.trainIdentifier} </div> <h3 className={"text-xl"}> @@ -23,12 +28,38 @@ const PlanElement = (props: { </div> - <motion.ol role="list" className="overflow-hidden" aria-hidden="true"> - <ProgressIndicator first={true} id={"a"} name={"Meitnerweg"} arrival={new Date("Mon Jul 17 2023 23:56:56 GMT+0200 (Central European Summer Time)")}/> - <ProgressIndicator id={"b"} name={"Dortmund Universität S"} arrival={new Date("Mon Jul 17 2023 23:57:07 GMT+0200 (Central European Summer Time)")}/> + <motion.ol role="list" className="overflow-hidden" aria-hidden="true"> + { + props.stops.map((stop, index) => ( + <ProgressIndicator + first={index === 0} + id={stop.name} + name={stop.name} + arrival={stop.arrival} + /> + )) + } </motion.ol> </div> ); }; export default PlanElement; + + +const trainIdentifierToColor = (identifier: string): string => { + if(identifier.startsWith("S")) { + return "bg-green-700"; + } + + if(identifier.startsWith("X")) { + return "bg-pink-700"; + } + + // Fixme: This should just be "if first is number" + if(identifier.startsWith("4")) { + return "bg-sky-700"; + } + + return "bg-zinc-700"; +} diff --git a/src/panels/Fahrplan/components/ProgressIndicator.tsx b/src/panels/Fahrplan/components/ProgressIndicator.tsx index 9de47d18ab81d78f73f10c92964825413ec8a7da..a40226f1685cf6e247e19eecba91e5e26fe181eb 100644 --- a/src/panels/Fahrplan/components/ProgressIndicator.tsx +++ b/src/panels/Fahrplan/components/ProgressIndicator.tsx @@ -1,37 +1,37 @@ -import TimeAgo from 'javascript-time-ago' -import de from 'javascript-time-ago/locale/de' -import {useEffect, useState} from "react"; - -export default function ProgressIndicator(props: {first?: boolean, id: string, name: string, arrival: Date}) { - const [timeUntil, setTimeUntil] = useState<string>(""); - - useEffect(() => { - // Setup TimeAgo - TimeAgo.addDefaultLocale(de); - const timeAgo = new TimeAgo('de-DE') - - const update = () => { - setTimeUntil(timeAgo.format(new Date(), {future: true})); - } - - setInterval(update, 1000); - update(); - }, []); +// import TimeAgo from 'javascript-time-ago' +// import de from 'javascript-time-ago/locale/de' +// import {useEffect, useState} from "react"; + +export default function ProgressIndicator(props: {first?: boolean, id: string, name: string, arrival: Date, delay?: number}) { + // const [timeUntil, setTimeUntil] = useState<string>(""); + // + // useEffect(() => { + // // Setup TimeAgo + // TimeAgo.addDefaultLocale(de); + // const timeAgo = new TimeAgo('de-DE') + // + // const update = () => { + // setTimeUntil(timeAgo.format(new Date(), {future: true})); + // } + // + // setInterval(update, 1000); + // update(); + // }, []); return ( - <li className={`relative ${!(props.first ?? false) ? "-mt-3.5" : "-mt-1"} text-sm text-zinc-300`} key={props.id}> + <li className={`relative ${!(props.first ?? false) ? "-mt-3.5" : "-mt-2"} text-sm text-zinc-300`} key={props.id}> <div className={"flex flex-row gap-4"}> - <div className={"w-16 flex flex-row justify-center"}> - <div className="w-[3px] h-6 -mb-5 bg-zinc-400" aria-hidden="true" /> + <div className={"w-20 flex flex-row justify-center"}> + <div className="w-[.25rem] h-6 -mb-5 bg-zinc-400" aria-hidden="true" /> </div> <div className={"flex-1"}></div> </div> <div className="relative flex flex-row items-center gap-4"> - <div className={"w-16 flex flex-row justify-center"}> - <span className="h-9 flex items-center" aria-hidden="true"> - <span className="relative z-10 w-[18px] h-[18px] flex items-center justify-center bg-zinc-400 rounded-full"> - <span className="h-[12px] w-[12px] bg-zinc-900 rounded-full" /> + <div className={"w-20 flex flex-row justify-center"}> + <span className="h-[2.6rem] flex items-center" aria-hidden="true"> + <span className="relative z-10 w-[1.5rem] h-[1.5rem] flex items-center justify-center bg-zinc-400 rounded-full"> + <span className="h-[1rem] w-[1rem] bg-zinc-900 rounded-full" /> </span> </span> </div> @@ -40,10 +40,19 @@ export default function ProgressIndicator(props: {first?: boolean, id: string, n {props.name} </div> - <div> - {timeUntil} + <div className={"tabular-nums"}> + {props.delay && ( + <span className={"text-red-400"}> + (+2) + </span> + )} + {renderTime(props.arrival)} </div> </div> </li> ) } + +const renderTime = (date: Date) => { + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")} Uhr` +} diff --git a/src/panels/Fahrplan/types/vrrfAPI.ts b/src/panels/Fahrplan/types/vrrfAPI.ts new file mode 100644 index 0000000000000000000000000000000000000000..54c97d088a2054ec9ba7d989d18dc7c75a430fc2 --- /dev/null +++ b/src/panels/Fahrplan/types/vrrfAPI.ts @@ -0,0 +1,42 @@ +export type StationResponse = { + error: string | null, + preformatted: string[][], + raw: Departure[], + version: string +} + +export type Departure = { + countdown: string, + date: string, + delay: string, + destination: string, + info: string, + is_cancelled: number, + key: string, + line: string, + lineref: { + direction: string, + identifier: string, + mot: string, + name: string, + operator: string, + route: string, + type: string, + valid: string + }, + mot: string, + next_route: any[], + occupancy: null, + platform: string, + platform_db: number, + platform_name: string, + prev_route: any[], + sched_date: string, + sched_time: string, + time: string, + train_no: null, + type: string, + internal: { + stop: string + } +}