From ee7069d2e235a307adab4d7c459f5633f13a0294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20Ellil=C3=A4?= Date: Fri, 15 Nov 2024 20:11:43 +0200 Subject: [PATCH] fix: more graciously handle missing journey legs --- src/components/LegendModal.tsx | 19 +- src/lib/vr.ts | 54 +++-- src/pages/train/[date]/[train].tsx | 348 +++++++++++++++++------------ 3 files changed, 254 insertions(+), 167 deletions(-) diff --git a/src/components/LegendModal.tsx b/src/components/LegendModal.tsx index 40f98bd..c8135ef 100644 --- a/src/components/LegendModal.tsx +++ b/src/components/LegendModal.tsx @@ -63,9 +63,12 @@ export const LegendModal = ({ Voit klikata paikkaa nähdäksesi sen varauksen asemalta toiselle. Voit myös rajata asemaväliä liikuttamalla liukusäätimen päätepisteitä.

- - - + + + {heatmapEnabled ? ( )} - - + + + + +

Vaihda keltaisesta väliväristä edistyneempään lämpökarttaan:  v ?? null), + placeType: z + .string() + .nullable() + .optional() + .transform((v) => v ?? null), type: z.string(), floorCount: z.number(), order: z.number(), @@ -255,23 +260,44 @@ export async function getTrainOnDate(date: string, trainNumber: string) { const dep = train.timeTableRows[i * 2]!; const arr = train.timeTableRows[i * 2 + 1]!; const trainNumber = train.trainNumber.toString(); - const wagons = await getWagonMapData( - dep.stationShortCode, - arr.stationShortCode, - new Date(dep.scheduledTime), - trainNumber, - auth.sessionId, - auth.token - ); - return { - dep, - arr, - wagons, - }; + try { + const wagons = await getWagonMapData( + dep.stationShortCode, + arr.stationShortCode, + new Date(dep.scheduledTime), + trainNumber, + auth.sessionId, + auth.token + ); + return { + dep, + arr, + wagons, + }; + } catch (e) { + void error( + { + date, + train: trainNumber, + message: "Wagon map data fetching failed", + }, + e + ).catch(console.error); + return { + dep, + arr, + wagons: null, + }; + } }) ), }; + const nullWagons = newTrain.timeTableRows.reduce((a, tt) => a + (tt.wagons == null ? 1 : 0), 0); + if (nullWagons == newTrain.timeTableRows.length || nullWagons > 3) { + throw new Error("Too many null wagons!"); + } + return newTrain; } diff --git a/src/pages/train/[date]/[train].tsx b/src/pages/train/[date]/[train].tsx index 29da500..f966e03 100644 --- a/src/pages/train/[date]/[train].tsx +++ b/src/pages/train/[date]/[train].tsx @@ -1,10 +1,10 @@ import { LeftCircleOutlined, QuestionCircleOutlined, ReloadOutlined } from "@ant-design/icons"; -import { Button, Slider, message } from "antd"; +import { Button, Modal, Slider, message } from "antd"; import dayjs from "dayjs"; import { type GetServerSideProps, type InferGetServerSidePropsType } from "next"; import Head from "next/head"; import { useRouter } from "next/router"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; // @ts-expect-error no types exists import { SvgLoader, SvgProxy } from "react-svgmt"; import { ZodError } from "zod"; @@ -70,6 +70,9 @@ export default function TrainPage({ const [timeRange, setTimeRange] = useState(initialRange ?? [0, 0]); const [LModalOpen, setLModalOpen] = useState(false); + const [missingModalOpen, setMissingModalOpen] = useState(false); + // some weird antd modal hydration issues + useEffect(() => setMissingModalOpen(true), []); const [selectedSeat, setSelectedSeat] = useState(initialSelectedSeat ?? null); @@ -78,9 +81,13 @@ export default function TrainPage({ useEffect(() => { // @ts-expect-error no types for globally available plausible function // eslint-disable-next-line - if (window.plausible) window.plausible("pageview", { - u: (window.location.origin + window.location.pathname).split("/").filter((_,i) => i != 4).join("/") - }); + if (window.plausible) + window.plausible("pageview", { + u: (window.location.origin + window.location.pathname) + .split("/") + .filter((_, i) => i != 4) + .join("/"), + }); }, []); useEffect(() => { @@ -119,6 +126,25 @@ export default function TrainPage({ } }, [router, train, stations, initialRange, initialSelectedSeat, timeRange, selectedSeat]); + const isInComplete = useMemo( + () => + wagons + ? wagons.some((w) => + w.floors.some((f) => f.seats.some((s) => s.status.some((ss) => ss === "missing"))) + ) + : false, + [wagons] + ); + const missingRanges = useMemo( + () => + wagons && stations + ? stations.map((_, i) => + wagons.some((w) => w.floors.some((f) => f.seats.some((s) => s.status[i] === "missing"))) + ) + : [], + [wagons, stations] + ); + useEffect(() => { if (!initialSelectedSeat || !mainMapRef) return; let cancelAnimation: (() => void) | null = null; @@ -214,10 +240,13 @@ export default function TrainPage({ const color = seat ? { + missing: "#45475a", unavailable: "#9399b2", reserved: "#f38ba8", open: "#a6e3a1", }[seat.status[i + timeRange[0]!]!] + : isInComplete && missingRanges.slice(timeRange[0], timeRange[1])[i] + ? "#45475a" : "#7f849c"; if (i === 0) leftHandle.style.setProperty("--handle-color", color); @@ -235,7 +264,7 @@ export default function TrainPage({ const sliderEl = document.getElementsByClassName("ant-slider-track-1")[0] as HTMLElement; if (sliderEl) sliderEl.style.background = styleStr; - }, [timeRange, selectedSeat, wagons]); + }, [timeRange, selectedSeat, wagons, isInComplete, missingRanges]); useEffect(() => { router.prefetch(date ? `/?date=${date}` : "/").catch(console.error); @@ -384,7 +413,7 @@ export default function TrainPage({ const bookedEcoSeats = ecoSeatObjs.filter((s) => s.status.includes("reserved")).length; const percentageBookedEcoSeats = ecoSeatObjs.reduce((a, c) => a + c.status.filter((s) => s === "reserved").length, 0) / - ecoSeatObjs.reduce((a, c) => a + c.status.length, 0); + ecoSeatObjs.reduce((a, c) => a + c.status.filter((s) => s !== "missing").length, 0); const siteTitle = `VenaaRauhassa - ${train.trainType}${train.trainNumber} - ${fiDate}`; const description = @@ -433,6 +462,21 @@ export default function TrainPage({ {messageContextHolder} + {isInComplete ? ( + setMissingModalOpen(false)} + onCancel={() => setMissingModalOpen(false)} + footer={null} + > +

+ Osa ominaisuuksista on poistettu käytöstä ja tiedot eivät välttämättä pidä paikkaansa. + Näet aikajanalla tumman harmaalla kohdat, joista tietoja ei ole saatavilla. +

+ + ) : null} +
r === "unavailable"); - const allReserved = statusRange.every((r) => r === "reserved"); - const allOpen = statusRange.every((r) => r === "open"); + const allUnavailable = statusRange + .filter((r) => r !== "missing") + .every((r) => r === "unavailable"); + const allReserved = statusRange + .filter((r) => r !== "missing") + .every((r) => r === "reserved"); + const allOpen = statusRange + .filter((r) => r !== "missing") + .every((r) => r === "open"); const extra = seat.productType === "EXTRA_CLASS_SEAT"; const restaurant = seat.productType === "SEAT_UPSTAIRS_RESTAURANT_WAGON"; @@ -675,7 +725,7 @@ export default function TrainPage({ 20 * (8 / 2 - (statusRange.filter((r) => r === "reserved").length / - statusRange.length) * + statusRange.filter((r) => r !== "missing").length) * 8) ); return "#f9e2af"; @@ -743,165 +793,168 @@ export default function TrainPage({ mainMapRef={mainMapRef} /> -
- -
+ ✨ Löydä paikkasi ✨{" "} + + Beta + + +
+ )}
{ station: allStations[station.stationShortCode] ?? station.stationShortCode, })); - const rawWagons: (typeof train)["timeTableRows"][number]["wagons"][string][] = []; + const rawWagons: NonNullable<(typeof train)["timeTableRows"][number]["wagons"]>[string][] = []; for (const timeTableRow of train.timeTableRows) { + if (!timeTableRow.wagons) continue; for (const wagon of Object.values(timeTableRow.wagons)) { if (!rawWagons.some((w) => w.number === wagon.number)) { rawWagons.push(wagon); @@ -1066,6 +1120,8 @@ export const getServerSideProps = (async (context) => { number: place.number, section: place.logicalSection, status: train.timeTableRows.map((row) => { + // VR API IS VERY STUPID AND SOMETIMES IS JUST MISSING ENTIRE JOURNEY LEGS + if (!row.wagons) return "missing"; const rowWagon = Object.values(row.wagons).find( (wagon2) => wagon2.number === wagon.number ); @@ -1137,9 +1193,11 @@ export const getServerSideProps = (async (context) => { { date: context.params.date, train: context.params.train, - ...(e instanceof Error ? { - message: e.message - } : {}), + ...(e instanceof Error + ? { + message: e.message, + } + : {}), url: "<" + getBaseURL() + context.resolvedUrl + ">", }, e @@ -1195,7 +1253,7 @@ export type Floor = { export type Seat = { number: number; section: number; - status: ("open" | "reserved" | "unavailable")[]; + status: ("open" | "reserved" | "unavailable" | "missing")[]; productType: string; type: string; services: string[];