Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions packages/map/components/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type MapProps = {
serverId: string | string[];
};

const getTrainStopKey = (train: Train) => train.id ?? train.TrainNoLocal;

Comment on lines +46 to +47

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate getTrainStopKey implementation across files.

The exact same helper is redefined independently in packages/map/components/TrainsList.tsx (const getTrainStopKey = (train: Train) => train.id ?? train.TrainNoLocal;). Since TrainsList looks up entries in the stoppedTrainsSince map produced here using its own copy of this key function, any future edit to one copy without the other will silently desynchronize the lookup key and break the stopped-duration feature.

Extract this into a shared module (e.g. packages/map/utils/trains.ts) and import it in both files.

♻️ Proposed fix
+// packages/map/utils/trains.ts
+import type { Train } from "`@simrail/types`";
+
+export const getTrainStopKey = (train: Train) => train.id ?? train.TrainNoLocal;
-const getTrainStopKey = (train: Train) => train.id ?? train.TrainNoLocal;
+import { getTrainStopKey } from "../utils/trains";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/map/components/Map.tsx` around lines 46 - 47, The train stop key
logic is duplicated in Map.tsx and TrainsList.tsx, which can cause the
stoppedTrainsSince lookup to drift if one copy changes. Extract the shared
getTrainStopKey helper into a common utility module and import it from both Map
and TrainsList so the key used to build and read the stoppedTrainsSince map
stays consistent.

const LeaftletMap = ({ serverId }: MapProps) => {
const [map, setMap] = useState<LeafletMap | null>(null);

Expand Down Expand Up @@ -78,12 +80,33 @@ const LeaftletMap = ({ serverId }: MapProps) => {

const { selectedTrain, setSelectedTrain } = useSelectedTrain();
const [stations, setStations] = useState<Station[] | null>(null);
const [stoppedTrainsSince, setStoppedTrainsSince] = useState<
Record<string, number>
>({});

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<string, number> = {};

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);
Comment on lines +83 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Map.tsx slice =="
sed -n '1,220p' packages/map/components/Map.tsx

echo
echo "== Search getTrainStopKey =="
rg -n "getTrainStopKey|stoppedTrainsSince|setStoppedTrainsSince|TrainNoLocal|TrainData\.Velocity" packages/map -S

Repository: simrail/map-v2

Length of output: 8148


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Train type definition =="
rg -n "export type Train|type Train|interface Train" -S .

echo
echo "== stoppedSince consumers =="
sed -n '1,140p' packages/map/components/TrainsList.tsx
echo
sed -n '1,220p' packages/map/components/SelectedTrainPopup.tsx

Repository: simrail/map-v2

Length of output: 2626


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search for Train id field usage in repo =="
rg -n "\.id\b|TrainNoLocal|ControlledBySteamID|TrainData" packages -S

echo
echo "== Look for local type re-exports or docs around `@simrail/types` =="
rg -n "`@simrail/types`|Train =" packages -S

Repository: simrail/map-v2

Length of output: 6209


Preserve stopped timestamps across a missed pollnextStoppedTrainsSince rebuilds from only trainsData, so a still-stopped train loses its elapsed time if one response omits it or if its key switches between id and TrainNoLocal. Keep the previous timestamp until the train is seen moving.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/map/components/Map.tsx` around lines 83 - 109, The stopped-train
tracking in getTrains currently rebuilds stoppedTrainsSince only from the latest
trainsData, so a train can lose its original stop timestamp if it is briefly
missing or its key changes. Update the stopped-train merge logic inside
setStoppedTrainsSince to preserve the existing timestamp for any train that was
already stopped and is still considered stopped, and only remove/reset entries
when the train is seen moving again. Use getTrainStopKey, trainsData, and
previousStoppedTrainsSince to keep the identity stable across polls.

});
}, [serverId]);

Expand Down Expand Up @@ -176,7 +199,13 @@ const LeaftletMap = ({ serverId }: MapProps) => {

return (
<>
<SelectedTrainPopup />
<SelectedTrainPopup
stoppedSince={
selectedTrain
? stoppedTrainsSince[getTrainStopKey(selectedTrain)]
: undefined
}
/>
<MapContainer
center={[50.270908, 19.039993]}
zoom={10}
Expand Down Expand Up @@ -300,7 +329,10 @@ const LeaftletMap = ({ serverId }: MapProps) => {
name="Trains"
>
<LayerGroup>
<TrainsList trains={trains} />
<TrainsList
trains={trains}
stoppedTrainsSince={stoppedTrainsSince}
/>
</LayerGroup>
</LayersControl.Overlay>

Expand Down
8 changes: 6 additions & 2 deletions packages/map/components/Markers/TrainMarker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -77,6 +80,7 @@ const TrainMarker = ({ train }: TrainMarkerProps) => {
username={username}
avatar={avatar}
minified={true}
stoppedSince={stoppedSince}
/>
</Popup>

Expand Down
7 changes: 6 additions & 1 deletion packages/map/components/SelectedTrainPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -41,6 +45,7 @@ const SelectedTrainPopup = () => {
train={selectedTrain}
username={username ?? ""}
avatar={avatar}
stoppedSince={stoppedSince}
/>
</div>
) : null;
Expand Down
36 changes: 34 additions & 2 deletions packages/map/components/TrainText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -15,6 +15,7 @@ type TrainTextProps = {
username: string;
avatar: string | null;
minified?: boolean | null;
stoppedSince?: number;
};

interface TrainRailcarInfo {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -246,7 +278,7 @@ const TrainText = ({
<br />
{roundedSpeed === 0 ? (
<>
Train currently stopped
Train currently stopped for: {formatStoppedDuration(stoppedSeconds)}
<br />
</>
) : (
Expand Down
11 changes: 9 additions & 2 deletions packages/map/components/TrainsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,19 @@ import type { FC } from "react";

type Props = {
trains: Train[];
stoppedTrainsSince: Record<string, number>;
};

export const TrainsList: FC<Props> = ({ trains }) => (
const getTrainStopKey = (train: Train) => train.id ?? train.TrainNoLocal;

export const TrainsList: FC<Props> = ({ trains, stoppedTrainsSince }) => (
<>
{trains.map((train) => (
<TrainMarker key={train.TrainNoLocal} train={train} />
<TrainMarker
key={train.TrainNoLocal}
train={train}
stoppedSince={stoppedTrainsSince[getTrainStopKey(train)]}
/>
))}
</>
);
Loading