Newer
Older
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,
countdown: parseInt(departure.countdown),
cancelled: departure.is_cancelled === 1
],
countdown: parseInt(departure.countdown)
// 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>
)}
</PanelContent>
</PanelWrapper>
);
};
export default FahrplanPanel;
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
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;
}
console.warn("While parsing delay, the string was not interpretable as number", input);
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}`);
}