From 66cd80f0680bcb0ca2de620f97f5e56b99fbcb38 Mon Sep 17 00:00:00 2001 From: jaanonim Date: Thu, 1 Jan 2026 18:05:00 +0100 Subject: [PATCH 1/4] category manager --- src/core/trainCategory.ts | 48 +++++++++++++++++------------------ src/utils/categories.ts | 53 +++++++++++++++++++++++++++++++++++++++ src/utils/importer.ts | 25 ++++++------------ 3 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 src/utils/categories.ts diff --git a/src/core/trainCategory.ts b/src/core/trainCategory.ts index a66c53a..99e42d2 100644 --- a/src/core/trainCategory.ts +++ b/src/core/trainCategory.ts @@ -1,43 +1,43 @@ /** * For representation of each Train Type depending on the train model/ company */ + export class TrainCategory { /** Name of the train category */ #name: string; + /** Full name of the train category */ + #fullName: string; /** Priority scale value amongst trains */ - #priority: number = 0; + priority: number = 0; /** The time exceeding which will result in some delay (in seconds) */ - #maxWaitingTime: number = 0; + maxWaitingTime: number = 0; /** Speed value which cannot be surpassed in m/s */ - #maxVelocity: number = 0; + maxVelocity: number = 0; /** Parameter describing capability of gaining the above speed in m/s^2 (maximal acceleration value) */ - #acceleration: number = 0; + acceleration: number = 0; - constructor(name: string, priority: number, maxWaitingTime: number, maxVelocity: number, acceleration: number) { + constructor( + name: string, + fullName: string, + priority: number, + maxWaitingTime: number, + maxVelocity: number, + acceleration: number + ) { this.#name = name; - this.#priority = priority; - this.#maxWaitingTime = maxWaitingTime; - this.#maxVelocity = maxVelocity; - this.#acceleration = acceleration; - } - - /** - * The time exceeding which will result in giving up waiting (in seconds) - */ - get maxWaitingTime() { - return this.#maxWaitingTime; + this.#fullName = fullName; + this.priority = priority; + this.maxWaitingTime = maxWaitingTime; + this.maxVelocity = maxVelocity; + this.acceleration = acceleration; } get name() { return this.#name; } - get priority() { - return this.#priority; - } - get maxVelocity() { - return this.#maxVelocity; - } - get acceleration() { - return this.#acceleration; + get fullName() { + return this.#fullName; } } + +export type EditableTrainCategoryFields = "priority" | "maxVelocity" | "acceleration" | "maxWaitingTime"; diff --git a/src/utils/categories.ts b/src/utils/categories.ts new file mode 100644 index 0000000..30fd53f --- /dev/null +++ b/src/utils/categories.ts @@ -0,0 +1,53 @@ +import { TrainCategory } from "../core/trainCategory"; + +const DEFAULT_CATEGORIES: Record = { + eip: new TrainCategory("EIP", "Express InterCity Premium", 1, 20 * 60, 55.6, 0.6), + eic: new TrainCategory("EIC", "Express InterCity", 2, 25 * 60, 55.6, 0.5), + ec: new TrainCategory("EC", "EuroCity", 2, 30 * 60, 55.6, 0.5), + en: new TrainCategory("EN", "EuroNight", 3, 35 * 60, 44.4, 0.3), + ic: new TrainCategory("IC", "InterCity", 3, 30 * 60, 44.4, 0.4), + d: new TrainCategory("D", "Pociąg dalekobieżny", 4, 35 * 60, 44.4, 0.35), + tlk: new TrainCategory("TLK", "Tanie Linie Kolejowe", 4, 35 * 60, 38.9, 0.35), + ir: new TrainCategory("IR", "InterRegio", 5, 30 * 60, 38.9, 0.35), + ar: new TrainCategory("AR", "Accelerated Regional", 5, 25 * 60, 38.9, 0.45), + r: new TrainCategory("R", "Regio", 6, 20 * 60, 33.3, 0.4), + km: new TrainCategory("KM", "Koleje Mazowieckie", 6, 20 * 60, 33.3, 0.4), + kd: new TrainCategory("KD", "Koleje Dolnośląskie", 6, 20 * 60, 44.4, 0.45), + le: new TrainCategory("LE", "Lokalny Ekspres", 6, 20 * 60, 33.3, 0.45), + skm: new TrainCategory("SKM", "Szybka Kolej Miejska", 6, 15 * 60, 30.6, 0.5), + skw: new TrainCategory("SKW", "Szybka Kolej Miejska Warszawa", 6, 15 * 60, 30.6, 0.55), + wkd: new TrainCategory("WKD", "Warszawska Kolej Dojazdowa", 6, 10 * 60, 25.0, 0.6), + zka: new TrainCategory("ZKA", "Zastępcza Komunikacja Autobusowa", 6, 20 * 60, 33.3, 0.4), + l: new TrainCategory("L", "Pociąg lokalny", 7, 15 * 60, 30.6, 0.35), + ls: new TrainCategory("LS", "Lokalny sezonowy", 7, 15 * 60, 30.6, 0.35), + ks: new TrainCategory("KS", "Koleje Śląskie", 7, 15 * 60, 30.6, 0.35), + kw: new TrainCategory("KW", "Koleje Wielkopolskie", 7, 15 * 60, 30.6, 0.35), + lp: new TrainCategory("LP", "Linia podmiejska", 7, 15 * 60, 30.6, 0.35), + kml: new TrainCategory("KML", "Kolej Metropolitalna", 7, 15 * 60, 30.6, 0.35), + os: new TrainCategory("OS", "Pociąg osobowy", 8, 15 * 60, 30.6, 0.3), + bus: new TrainCategory("BUS", "Autobus", 9, 10 * 60, 22.2, 0.25), + default: new TrainCategory("DEFAULT", "Nieznana kategoria", 10, 15 * 60, 27.8, 0.3), +}; + +class CategoryManager { + private categories: Record; + + constructor() { + this.categories = DEFAULT_CATEGORIES; + } + + getCategories(): TrainCategory[] { + return Object.values(this.categories).sort((a, b) => a.name.localeCompare(b.name)); + } + + getCategory(name: string): TrainCategory { + if (this.categories[name] === undefined) console.warn("Category not found:", name); + return this.categories[name] === undefined ? this.categories["default"] : this.categories[name]; + } + + mapCategory(name: string): TrainCategory { + return this.getCategory(name.toLowerCase()); + } +} + +export const categoryManager = new CategoryManager(); diff --git a/src/utils/importer.ts b/src/utils/importer.ts index 86a21c1..a8a609d 100644 --- a/src/utils/importer.ts +++ b/src/utils/importer.ts @@ -4,23 +4,7 @@ import { TrainCategory } from "../core/trainCategory"; import { Position } from "./position"; import { TrainTemplate } from "../core/trainTemplate"; import { Time } from "./time"; - -function mapCategory(category: string) { - switch (category) { - case "Bus": - return new TrainCategory("Bus", 0, 40 * 60, 16, 1); // maxWaitingTime conversion from minutes to seconds - case "BUS": - return new TrainCategory("BUS", 0, 40 * 60, 16, 1); - case "R": - return new TrainCategory("R", 1, 60 * 60, 30, 2); - case "KS": - return new TrainCategory("KS", 1, 60 * 60, 33, 2); - case "KML": - return new TrainCategory("KML", 1, 60 * 60, 33, 2); - default: - return new TrainCategory(category, 2, 10 * 60, 44, 3); - } -} +import { categoryManager } from "./categories"; export class ImportedData { #stations: Map = new Map(); @@ -55,7 +39,12 @@ export class ImportedData { let previousDepartureTime: Time | null = null; let dayShift = false; - const trainTemplate = new TrainTemplate(t.number, mapCategory(t.category), t.name, t.params); + const trainTemplate = new TrainTemplate( + t.number, + categoryManager.mapCategory(t.category), + t.name, + t.params + ); for (let i = 0; i < t.stops.length; i++) { [previousDepartureTime, dayShift] = this.#importStop( From 9f0da763c674266376b944c132ca16af7ea2510e Mon Sep 17 00:00:00 2001 From: jaanonim Date: Thu, 1 Jan 2026 18:05:36 +0100 Subject: [PATCH 2/4] settings and better scroll bare --- src/css/index.css | 1 + src/css/scroll.css | 27 +++++ src/ui/components/App.tsx | 6 ++ src/ui/components/CategorySetting.tsx | 27 +++++ src/ui/components/CategorySettingsField.tsx | 27 +++++ src/ui/components/Controls.tsx | 7 +- src/ui/components/InfoPanel.tsx | 2 +- src/ui/components/Search.tsx | 4 +- src/ui/components/Settings.tsx | 39 +++++++ src/ui/components/StationInfo.tsx | 107 ++++++++++---------- src/ui/components/TrainInfo.tsx | 79 ++++++++------- 11 files changed, 236 insertions(+), 90 deletions(-) create mode 100644 src/css/scroll.css create mode 100644 src/ui/components/CategorySetting.tsx create mode 100644 src/ui/components/CategorySettingsField.tsx create mode 100644 src/ui/components/Settings.tsx diff --git a/src/css/index.css b/src/css/index.css index b01dc0e..7cff192 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -2,6 +2,7 @@ @import "leaflet"; @import "./icons.css"; @import "./components.css"; +@import "./scroll.css"; #map { position: fixed; diff --git a/src/css/scroll.css b/src/css/scroll.css new file mode 100644 index 0000000..ee4d4b2 --- /dev/null +++ b/src/css/scroll.css @@ -0,0 +1,27 @@ +::-webkit-scrollbar { + width: 12px; + height: 12px; +} + +::-webkit-scrollbar-track { + background: transparent; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: var(--color-stone-700); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--color-stone-800); +} + +::-webkit-scrollbar-button { + display: none; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--color-stone-700) transparent; +} diff --git a/src/ui/components/App.tsx b/src/ui/components/App.tsx index 76df729..eedd4ca 100644 --- a/src/ui/components/App.tsx +++ b/src/ui/components/App.tsx @@ -4,9 +4,11 @@ import Loading from "./Loading"; import Stats from "./Stats"; import InfoPanel from "./InfoPanel"; import Search from "./Search"; +import Settings from "./Settings"; export default function App() { const [openStats, setOpenStats] = useState(false); + const [openSettings, setOpenSettings] = useState(false); return ( <> @@ -15,8 +17,12 @@ export default function App() { onToggleStats={() => { setOpenStats((s) => !s); }} + onToggleSettings={() => { + setOpenSettings((s) => !s); + }} /> {openStats && setOpenStats(false)} />} + {openSettings && setOpenSettings(false)} />} diff --git a/src/ui/components/CategorySetting.tsx b/src/ui/components/CategorySetting.tsx new file mode 100644 index 0000000..7115fab --- /dev/null +++ b/src/ui/components/CategorySetting.tsx @@ -0,0 +1,27 @@ +import { MdOutlineKeyboardArrowDown, MdOutlineKeyboardArrowRight } from "react-icons/md"; +import type { TrainCategory } from "../../core/trainCategory"; +import CategorySettingsField from "./CategorySettingsField"; + +interface CategorySettingsProps { + category: TrainCategory; + isCollapsed?: boolean; + onToggle?: () => void; +} + +export default function CategorySettings({ category, isCollapsed, onToggle }: CategorySettingsProps) { + return ( +
+
+ {isCollapsed ? : } {category.fullName} +
+ {!isCollapsed && ( +
+ + + + +
+ )} +
+ ); +} diff --git a/src/ui/components/CategorySettingsField.tsx b/src/ui/components/CategorySettingsField.tsx new file mode 100644 index 0000000..3d9f16e --- /dev/null +++ b/src/ui/components/CategorySettingsField.tsx @@ -0,0 +1,27 @@ +import { useEffect, useState } from "react"; +import type { EditableTrainCategoryFields, TrainCategory } from "../../core/trainCategory"; + +interface CategorySettingsFieldProps { + field: EditableTrainCategoryFields; + label: string; + category: TrainCategory; +} + +export default function CategorySettingsField({ field, label, category }: CategorySettingsFieldProps) { + const [value, setValue] = useState(category[field]); + useEffect(() => { + category[field] = value; + }, [value, field, category]); + + return ( +
+ {label}: + setValue(parseFloat(e.currentTarget.value))} + /> +
+ ); +} diff --git a/src/ui/components/Controls.tsx b/src/ui/components/Controls.tsx index f6727ed..80b4d47 100644 --- a/src/ui/components/Controls.tsx +++ b/src/ui/components/Controls.tsx @@ -2,12 +2,14 @@ import { FaChartLine, FaForwardStep, FaPause, FaPlay } from "react-icons/fa6"; import useSimulation from "../hooks/useSimulation"; import { VscDebugRestart } from "react-icons/vsc"; import Stats from "./Stats"; +import { MdSettings } from "react-icons/md"; interface ControlsProps { onToggleStats?: () => void; + onToggleSettings?: () => void; } -export default function Controls({ onToggleStats }: ControlsProps) { +export default function Controls({ onToggleStats, onToggleSettings }: ControlsProps) { const [simulation, simulationState, updateSimulationState] = useSimulation(); return ( @@ -46,6 +48,9 @@ export default function Controls({ onToggleStats }: ControlsProps) { +
diff --git a/src/ui/components/InfoPanel.tsx b/src/ui/components/InfoPanel.tsx index b4746d7..1ed9323 100644 --- a/src/ui/components/InfoPanel.tsx +++ b/src/ui/components/InfoPanel.tsx @@ -51,7 +51,7 @@ export default function InfoPanel() { if (selected === null) return null; return ( -
+
diff --git a/src/ui/components/Search.tsx b/src/ui/components/Search.tsx index d7182cc..581d709 100644 --- a/src/ui/components/Search.tsx +++ b/src/ui/components/Search.tsx @@ -50,7 +50,7 @@ export default function Search() { }, [searchText]); return ( -
+
{searchText.length > 0 && ( -
+
{searchResults.map((result, index) => (
void; +} + +export default function Settings({ onClose }: SettingsProps) { + const [openCategory, setOpenCategory] = useState(null as string | null); + + return ( +
+
e.stopPropagation()} + > + +

Settings

+
+ {categoryManager.getCategories().map((category) => ( + setOpenCategory((c) => (c === category.name ? null : category.name))} + /> + ))} +
+
+
+ ); +} diff --git a/src/ui/components/StationInfo.tsx b/src/ui/components/StationInfo.tsx index ed0e58e..ea2cfe5 100644 --- a/src/ui/components/StationInfo.tsx +++ b/src/ui/components/StationInfo.tsx @@ -13,58 +13,61 @@ export default function StationInfo({ station, onSelectTrain }: StationInfoProps return (

{station.name}

- - - - - - - - - - - {station.tracks - .sort((a, b) => { - if (a.platformNumber === b.platformNumber) { - return a.trackNumber.localeCompare(b.trackNumber); - } - return a.platformNumber - b.platformNumber; - }) - .filter( - // skip printing imaginary tracks except those with scheduled arrival/ departure times (e.g. abroad) - (track) => - station.nextArrivalForTrack(track, simulation.currentTime) !== null || - station.nextDepartureForTrack(track, simulation.currentTime) !== null - ) - .map((track) => ( - - - - - - - ))} - -
- Platform -
- (Track) -
OccupancyArrivalDeparture
- {track.platformNumber} ({track.trackNumber}) - { - if (track.train !== null) onSelectTrain(track.train); - }} - > - {track.train === null ? " - " : track.train.displayName()} - - {station.nextArrivalForTrack(track, simulation.currentTime)?.displayArrival() ?? - " - "} - - {station.nextDepartureForTrack(track, simulation.currentTime)?.displayDeparture() ?? - " - "} -
+
+ + + + + + + + + + + {station.tracks + .sort((a, b) => { + if (a.platformNumber === b.platformNumber) { + return a.trackNumber.localeCompare(b.trackNumber); + } + return a.platformNumber - b.platformNumber; + }) + .filter( + // skip printing imaginary tracks except those with scheduled arrival/ departure times (e.g. abroad) + (track) => + station.nextArrivalForTrack(track, simulation.currentTime) !== null || + station.nextDepartureForTrack(track, simulation.currentTime) !== null + ) + .map((track) => ( + + + + + + + ))} + +
+ Platform +
+ (Track) +
OccupancyArrivalDeparture
+ {track.platformNumber} ({track.trackNumber}) + { + if (track.train !== null) onSelectTrain(track.train); + }} + > + {track.train === null ? " - " : track.train.displayName()} + + {station.nextArrivalForTrack(track, simulation.currentTime)?.displayArrival() ?? + " - "} + + {station + .nextDepartureForTrack(track, simulation.currentTime) + ?.displayDeparture() ?? " - "} +
+
); } diff --git a/src/ui/components/TrainInfo.tsx b/src/ui/components/TrainInfo.tsx index ef01602..6e5d342 100644 --- a/src/ui/components/TrainInfo.tsx +++ b/src/ui/components/TrainInfo.tsx @@ -1,6 +1,7 @@ import { use, useRef, useState } from "react"; import type { Train } from "../../core/train"; import useRenderer from "../hooks/useRenderer"; +import { Track } from "../../core/track"; interface TrainInfoProps { train: Train; @@ -14,47 +15,57 @@ export default function TrainInfo({ train, onUpdate }: TrainInfoProps) { return (

{train.displayName()}

-

Speed: {train.velocity.toFixed(2)} m/s

+
+

Category: {train.trainTemplate.type.fullName}

+

Speed: {train.velocity.toFixed(2)} m/s

-
-

Delay: {(train.delay.UIDelayValue / 60).toFixed(2)} min

- - setDelay(parseFloat(v.target.value))} - /> +
+

Delay: {(train.delay.UIDelayValue / 60).toFixed(2)} min

+ + setDelay(parseFloat(v.target.value))} + /> + + +
+

+ {train.trainTemplate.description.map((e, i) => ( + + {e} +
+
+ ))} +

+
- + {train.position instanceof Track && ( + + )} +
-

- {train.trainTemplate.description.map((e) => ( - <> - {e} -
- - ))} -

-
); } From 495546939900e538881348df8cdb489da8312a5e Mon Sep 17 00:00:00 2001 From: jaanonim Date: Thu, 1 Jan 2026 18:10:00 +0100 Subject: [PATCH 3/4] add unit --- src/ui/components/CategorySetting.tsx | 13 +++++++++---- src/ui/components/CategorySettingsField.tsx | 18 +++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/ui/components/CategorySetting.tsx b/src/ui/components/CategorySetting.tsx index 7115fab..2dbd3a2 100644 --- a/src/ui/components/CategorySetting.tsx +++ b/src/ui/components/CategorySetting.tsx @@ -16,10 +16,15 @@ export default function CategorySettings({ category, isCollapsed, onToggle }: Ca
{!isCollapsed && (
- - - - + + + +
)}
diff --git a/src/ui/components/CategorySettingsField.tsx b/src/ui/components/CategorySettingsField.tsx index 3d9f16e..eb79136 100644 --- a/src/ui/components/CategorySettingsField.tsx +++ b/src/ui/components/CategorySettingsField.tsx @@ -4,10 +4,11 @@ import type { EditableTrainCategoryFields, TrainCategory } from "../../core/trai interface CategorySettingsFieldProps { field: EditableTrainCategoryFields; label: string; + unit: string; category: TrainCategory; } -export default function CategorySettingsField({ field, label, category }: CategorySettingsFieldProps) { +export default function CategorySettingsField({ field, label, unit, category }: CategorySettingsFieldProps) { const [value, setValue] = useState(category[field]); useEffect(() => { category[field] = value; @@ -16,12 +17,15 @@ export default function CategorySettingsField({ field, label, category }: Catego return (
{label}: - setValue(parseFloat(e.currentTarget.value))} - /> + + setValue(parseFloat(e.currentTarget.value))} + /> + {unit} +
); } From e2199c440dcb186ff16ea71ec53c5e0cb4cfac32 Mon Sep 17 00:00:00 2001 From: jakseluz Date: Fri, 2 Jan 2026 13:28:30 +0100 Subject: [PATCH 4/4] Add security not to use modifiable TrainCategory values in the simulation logic modules --- src/core/trainCategory.ts | 58 ++++++++++++++++++++++----- src/ui/components/CategorySetting.tsx | 13 ++++-- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/core/trainCategory.ts b/src/core/trainCategory.ts index 99e42d2..b6c2ecc 100644 --- a/src/core/trainCategory.ts +++ b/src/core/trainCategory.ts @@ -8,13 +8,13 @@ export class TrainCategory { /** Full name of the train category */ #fullName: string; /** Priority scale value amongst trains */ - priority: number = 0; + #priority: number = 0; /** The time exceeding which will result in some delay (in seconds) */ - maxWaitingTime: number = 0; + #maxWaitingTime: number = 0; /** Speed value which cannot be surpassed in m/s */ - maxVelocity: number = 0; + #maxVelocity: number = 0; /** Parameter describing capability of gaining the above speed in m/s^2 (maximal acceleration value) */ - acceleration: number = 0; + #acceleration: number = 0; constructor( name: string, @@ -26,18 +26,58 @@ export class TrainCategory { ) { this.#name = name; this.#fullName = fullName; - this.priority = priority; - this.maxWaitingTime = maxWaitingTime; - this.maxVelocity = maxVelocity; - this.acceleration = acceleration; + this.#priority = priority; + this.#maxWaitingTime = maxWaitingTime; + this.#maxVelocity = maxVelocity; + this.#acceleration = acceleration; } + // getters to use in the simulation logic and not to allow direct modification get name() { return this.#name; } get fullName() { return this.#fullName; } + get priority() { + return this.#priority; + } + get maxWaitingTime() { + return this.#maxWaitingTime; + } + get maxVelocity() { + return this.#maxVelocity; + } + get acceleration() { + return this.#acceleration; + } + + // getters for the editable fields in the UI + get UIPriority() { + return this.#priority; + } + get UIMaxWaitingTime() { + return this.#maxWaitingTime; + } + get UIMaxVelocity() { + return this.#maxVelocity; + } + get UIAcceleration() { + return this.#acceleration; + } + // setters for the editable fields in the UI + set UIPriority(value: number) { + this.#priority = value; + } + set UIMaxWaitingTime(value: number) { + this.#maxWaitingTime = value; + } + set UIMaxVelocity(value: number) { + this.#maxVelocity = value; + } + set UIAcceleration(value: number) { + this.#acceleration = value; + } } -export type EditableTrainCategoryFields = "priority" | "maxVelocity" | "acceleration" | "maxWaitingTime"; +export type EditableTrainCategoryFields = "UIPriority" | "UIMaxVelocity" | "UIAcceleration" | "UIMaxWaitingTime"; diff --git a/src/ui/components/CategorySetting.tsx b/src/ui/components/CategorySetting.tsx index 2dbd3a2..eec9df6 100644 --- a/src/ui/components/CategorySetting.tsx +++ b/src/ui/components/CategorySetting.tsx @@ -16,15 +16,20 @@ export default function CategorySettings({ category, isCollapsed, onToggle }: Ca
{!isCollapsed && (
- + - - + +
)}