Skip to content
Snippets Groups Projects
FahrplanPanel.tsx 4.83 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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";
    
    
    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
      }[],
      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
            if((props.definition.filter.types ?? []).includes(departure.type)) {
              continue;
            }
    
            // Find existing route with same uid
    
            const existing_ind = newRoutes.findIndex(r => r.uid === departure.key)
    
    
            // Pre-compute values that will be needed regardless
            const delay = stringToDelay(departure.delay);
            const arrival = processArrival(departure.sched_date, departure.time);
    
            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,
                    countdown: parseInt(departure.countdown)
    
                ],
                countdown: parseInt(departure.countdown)
    
              })
            } 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),
                countdown: parseInt(departure.countdown)
    
    
              newRoutes[existing_ind].stops = newRoutes[existing_ind].stops.sort((a, b) => a.countdown - b.countdown)
    
          newRoutes = newRoutes.sort((a, b) => a.countdown - b.countdown)
    
    
          // 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"}>
            <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>
    
    
      );
    };
    
    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);
    
    }
    
    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}`);
    }