From 940b42937fa1cdc334523e8f7b270ac54627e69e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Niklas=20Schr=C3=B6tler?= <niklas@allround.digital>
Date: Mon, 27 Nov 2023 01:21:25 +0100
Subject: [PATCH] Tailwind: Scaled type for target screen

---
 src/panels/Fahrplan/FahrplanPanel.tsx         | 202 +++++++++++++++---
 .../Fahrplan/components/PlanElement.tsx       |  45 +++-
 .../Fahrplan/components/ProgressIndicator.tsx |  65 +++---
 src/panels/Fahrplan/types/vrrfAPI.ts          |  42 ++++
 4 files changed, 290 insertions(+), 64 deletions(-)
 create mode 100644 src/panels/Fahrplan/types/vrrfAPI.ts

diff --git a/src/panels/Fahrplan/FahrplanPanel.tsx b/src/panels/Fahrplan/FahrplanPanel.tsx
index 64c18fd..516472d 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 99a7885..7f366b5 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 9de47d1..a40226f 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)&nbsp;&nbsp;
+              </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 0000000..54c97d0
--- /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
+  }
+}
-- 
GitLab