From 89c5c73127adcd14999863da2f710036255b0501 Mon Sep 17 00:00:00 2001 From: Loo Dessart Date: Sat, 4 Jul 2026 16:26:42 +0200 Subject: [PATCH] Show how long trains have been stopped Track and display how long trains have been stopped. Add getTrainStopKey helper and stoppedTrainsSince state in Map to record a timestamp when a train's velocity reaches zero, update it on fetch, and pass the data down to TrainsList, TrainMarker and SelectedTrainPopup. TrainText now accepts stoppedSince, computes a live-updating stopped duration (formatStoppedDuration) and shows it when the train is stationary. Minor prop and typing updates applied to components. --- packages/map/components/Map.tsx | 38 +++++++++++++++++-- .../map/components/Markers/TrainMarker.tsx | 8 +++- .../map/components/SelectedTrainPopup.tsx | 7 +++- packages/map/components/TrainText.tsx | 36 +++++++++++++++++- packages/map/components/TrainsList.tsx | 11 +++++- 5 files changed, 90 insertions(+), 10 deletions(-) diff --git a/packages/map/components/Map.tsx b/packages/map/components/Map.tsx index a154f8c..20e6384 100644 --- a/packages/map/components/Map.tsx +++ b/packages/map/components/Map.tsx @@ -43,6 +43,8 @@ type MapProps = { serverId: string | string[]; }; +const getTrainStopKey = (train: Train) => train.id ?? train.TrainNoLocal; + const LeaftletMap = ({ serverId }: MapProps) => { const [map, setMap] = useState(null); @@ -78,12 +80,33 @@ const LeaftletMap = ({ serverId }: MapProps) => { const { selectedTrain, setSelectedTrain } = useSelectedTrain(); const [stations, setStations] = useState(null); + const [stoppedTrainsSince, setStoppedTrainsSince] = useState< + Record + >({}); const getTrains = useCallback(() => { fetch(`https://panel.simrail.eu:8084/trains-open?serverCode=${serverId}`) .then((res) => res.json()) .then((fetchedTrains) => { - setTrains(fetchedTrains.data); + const trainsData: Train[] = fetchedTrains.data; + + setStoppedTrainsSince((previousStoppedTrainsSince) => { + const nextStoppedTrainsSince: Record = {}; + + for (const train of trainsData) { + const trainKey = getTrainStopKey(train); + const isStopped = Math.round(train.TrainData.Velocity) === 0; + + if (isStopped) { + nextStoppedTrainsSince[trainKey] = + previousStoppedTrainsSince[trainKey] ?? Date.now(); + } + } + + return nextStoppedTrainsSince; + }); + + setTrains(trainsData); }); }, [serverId]); @@ -176,7 +199,13 @@ const LeaftletMap = ({ serverId }: MapProps) => { return ( <> - + { name="Trains" > - + diff --git a/packages/map/components/Markers/TrainMarker.tsx b/packages/map/components/Markers/TrainMarker.tsx index ed790a4..6033149 100644 --- a/packages/map/components/Markers/TrainMarker.tsx +++ b/packages/map/components/Markers/TrainMarker.tsx @@ -10,9 +10,10 @@ import TrainText from "../TrainText"; type TrainMarkerProps = { train: Train; + stoppedSince?: number; }; -const TrainMarker = ({ train }: TrainMarkerProps) => { +const TrainMarker = ({ train, stoppedSince }: TrainMarkerProps) => { const { setSelectedTrain } = useSelectedTrain(); const [avatar, setAvatar] = useState(null); @@ -41,7 +42,9 @@ const TrainMarker = ({ train }: TrainMarkerProps) => { ) botIcon = "/markers/icon-bot-simrail-dark.jpg"; - const borderAreaClass = train.TrainData.InBorderStationArea ? ["in-border-area"] : []; + const borderAreaClass = train.TrainData.InBorderStationArea + ? ["in-border-area"] + : []; const icon = L.icon({ iconUrl: train.TrainData.ControlledBySteamID && avatar ? avatar : botIcon, iconSize: [24, 24], @@ -77,6 +80,7 @@ const TrainMarker = ({ train }: TrainMarkerProps) => { username={username} avatar={avatar} minified={true} + stoppedSince={stoppedSince} /> diff --git a/packages/map/components/SelectedTrainPopup.tsx b/packages/map/components/SelectedTrainPopup.tsx index 3376a26..b5e3a5c 100644 --- a/packages/map/components/SelectedTrainPopup.tsx +++ b/packages/map/components/SelectedTrainPopup.tsx @@ -6,7 +6,11 @@ import { useSelectedTrain } from "../contexts/SelectedTrainContext"; import styles from "../styles/SelectedTrainPopup.module.css"; import TrainText from "./TrainText"; -const SelectedTrainPopup = () => { +type SelectedTrainPopupProps = { + stoppedSince?: number; +}; + +const SelectedTrainPopup = ({ stoppedSince }: SelectedTrainPopupProps) => { const { selectedTrain } = useSelectedTrain(); const renderPopup = readLocalStorageValue({ key: "renderPopup", @@ -41,6 +45,7 @@ const SelectedTrainPopup = () => { train={selectedTrain} username={username ?? ""} avatar={avatar} + stoppedSince={stoppedSince} /> ) : null; diff --git a/packages/map/components/TrainText.tsx b/packages/map/components/TrainText.tsx index 7d08ec8..08b3a05 100644 --- a/packages/map/components/TrainText.tsx +++ b/packages/map/components/TrainText.tsx @@ -6,7 +6,7 @@ import { readLocalStorageValue } from "@mantine/hooks"; import type { Train } from "@simrail/types"; import { useSelectedTrain } from "contexts/SelectedTrainContext"; import { useRouter } from "next/router"; -import { useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { MdClose } from "react-icons/md"; import type { Railcar } from "../types/Railcar"; @@ -15,6 +15,7 @@ type TrainTextProps = { username: string; avatar: string | null; minified?: boolean | null; + stoppedSince?: number; }; interface TrainRailcarInfo { @@ -72,11 +73,26 @@ function extractVehicleInformation( return [rawVehicleName, null]; } +function formatStoppedDuration(totalSeconds: number): string { + if (totalSeconds < 60) { + return `${totalSeconds}s`; + } + + const totalMinutes = Math.floor(totalSeconds / 60); + if (totalMinutes < 60) { + return `${totalMinutes}min`; + } + + const totalHours = Math.floor(totalMinutes / 60); + return `${totalHours}h`; +} + const TrainText = ({ train, username, avatar, minified = false, + stoppedSince, }: TrainTextProps) => { const router = useRouter(); const { id, trainId } = router.query; @@ -139,6 +155,22 @@ const TrainText = ({ // don't think it was true before update either as EMUs prob had multiple traction const roundedSpeed = Math.round(train.TrainData.Velocity); + const [currentTime, setCurrentTime] = useState(Date.now()); + + useEffect(() => { + if (roundedSpeed !== 0 || !stoppedSince) { + return; + } + + setCurrentTime(Date.now()); + const interval = window.setInterval(() => setCurrentTime(Date.now()), 1000); + + return () => window.clearInterval(interval); + }, [roundedSpeed, stoppedSince]); + + const stoppedSeconds = stoppedSince + ? Math.max(0, Math.floor((currentTime - stoppedSince) / 1000)) + : 0; // however, this might be wrong in case the first unit is not yet registered in railcars.json, so we just // fall back to displaying the raw api name in that case @@ -246,7 +278,7 @@ const TrainText = ({
{roundedSpeed === 0 ? ( <> - Train currently stopped + Train currently stopped for: {formatStoppedDuration(stoppedSeconds)}
) : ( diff --git a/packages/map/components/TrainsList.tsx b/packages/map/components/TrainsList.tsx index e216787..c6d8ecc 100644 --- a/packages/map/components/TrainsList.tsx +++ b/packages/map/components/TrainsList.tsx @@ -4,12 +4,19 @@ import type { FC } from "react"; type Props = { trains: Train[]; + stoppedTrainsSince: Record; }; -export const TrainsList: FC = ({ trains }) => ( +const getTrainStopKey = (train: Train) => train.id ?? train.TrainNoLocal; + +export const TrainsList: FC = ({ trains, stoppedTrainsSince }) => ( <> {trains.map((train) => ( - + ))} );