From 66eae33e804cc04f137afaad103c5cdde91e8e3d Mon Sep 17 00:00:00 2001 From: izawartka <59137928+izawartka@users.noreply.github.com> Date: Tue, 24 Jun 2025 21:09:43 +0200 Subject: [PATCH] Added track hover info +Added a popup showing hovered track's info --- src/components/map/Map.js | 2 + src/components/map/ObjectsRenderQueue.js | 3 +- .../map/object-renderers/TrackRenderer.js | 11 ++++ .../map/track-hover-info/TrackHoverInfo.css | 19 +++++++ .../map/track-hover-info/TrackHoverInfo.js | 46 ++++++++++++++++ .../track-hover-info/TrackHoverInfoConsts.js | 17 ++++++ .../track-hover-info/TrackHoverInfoPopup.js | 53 +++++++++++++++++++ src/components/toolbar/CustomFileSelect.js | 2 + src/components/toolbar/SceneriesList.js | 2 + src/services/trackHoverInfoService.js | 19 +++++++ 10 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/components/map/track-hover-info/TrackHoverInfo.css create mode 100644 src/components/map/track-hover-info/TrackHoverInfo.js create mode 100644 src/components/map/track-hover-info/TrackHoverInfoConsts.js create mode 100644 src/components/map/track-hover-info/TrackHoverInfoPopup.js create mode 100644 src/services/trackHoverInfoService.js diff --git a/src/components/map/Map.js b/src/components/map/Map.js index 45f064c..c298c9b 100644 --- a/src/components/map/Map.js +++ b/src/components/map/Map.js @@ -2,6 +2,7 @@ import ZoomPanWrapper from './ZoomPanWrapper'; import './Map.css'; import SceneryRoot from './SceneryRoot'; import DistanceMeter from './distance-meter/DistanceMeter'; +import TrackHoverInfo from './track-hover-info/TrackHoverInfo'; export default function Map(props) { return ( @@ -10,6 +11,7 @@ export default function Map(props) { + ) } \ No newline at end of file diff --git a/src/components/map/ObjectsRenderQueue.js b/src/components/map/ObjectsRenderQueue.js index e6f7d8a..a4f84bf 100644 --- a/src/components/map/ObjectsRenderQueue.js +++ b/src/components/map/ObjectsRenderQueue.js @@ -19,7 +19,8 @@ const ObjectRendererQueue = [ 'additionalComponents': [ ElectrificationStatusPopup ], - 'cond': (layers) => layers['tracks'] + 'cond': (layers) => layers['tracks'], + 'pointerEvents': true }, { 'name': 'isolations-ids', diff --git a/src/components/map/object-renderers/TrackRenderer.js b/src/components/map/object-renderers/TrackRenderer.js index 814b51a..de11f1e 100644 --- a/src/components/map/object-renderers/TrackRenderer.js +++ b/src/components/map/object-renderers/TrackRenderer.js @@ -3,6 +3,7 @@ import SettingsContext from "../../../contexts/SettingsContext"; import Constants from "../../../helpers/constants"; import { ElectrificationStatus } from "../../../model/electrification-status"; import MiscHelper from "../../../helpers/miscHelper"; +import { setHoveredTrack, unsetHoveredTrack } from "../../../services/trackHoverInfoService"; export default function TrackRenderer(props) { const { object } = props; @@ -21,6 +22,14 @@ const MemoizedTrackRenderer = React.memo(StatelessTrackRenderer); function StatelessTrackRenderer(props) { const { object, trackColorMode } = props; + const onMouseEnter = () => { + setHoveredTrack(object); + }; + + const onMouseLeave = () => { + unsetHoveredTrack(null); + }; + const path = getTrackPath(object); const color = getTrackColor(object, trackColorMode); const defs = getTrackDefs(object, trackColorMode); @@ -37,6 +46,8 @@ function StatelessTrackRenderer(props) { d={path} stroke={color} className="track" + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} /> ); diff --git a/src/components/map/track-hover-info/TrackHoverInfo.css b/src/components/map/track-hover-info/TrackHoverInfo.css new file mode 100644 index 0000000..451660b --- /dev/null +++ b/src/components/map/track-hover-info/TrackHoverInfo.css @@ -0,0 +1,19 @@ +.track-hover-info { + position: absolute; + z-index: 10000; + margin: 0.5em; + pointer-events: none; +} + +.track-hover-info-popup { + background-color: #222; + border: 2px solid #444; + border-radius: 8px; + padding: 1em; +} + +.track-hover-info-popup th { + text-align: start; + padding-right: 2em; + color: #888; +} diff --git a/src/components/map/track-hover-info/TrackHoverInfo.js b/src/components/map/track-hover-info/TrackHoverInfo.js new file mode 100644 index 0000000..4899b0f --- /dev/null +++ b/src/components/map/track-hover-info/TrackHoverInfo.js @@ -0,0 +1,46 @@ +import { useState, useEffect, useCallback, useRef, useContext } from "react"; +import { hoveredTrack$ } from "../../../services/trackHoverInfoService"; +import TrackHoverInfoPopup from "./TrackHoverInfoPopup"; +import SettingsContext from "../../../contexts/SettingsContext"; +import './TrackHoverInfo.css'; + +export default function TrackHoverInfo() { + const [ hoveredTrack, setHoveredTrack ] = useState(null); + const mousePos = useRef({ x: 0, y: 0 }); + const divRef = useRef(null); + + const handleMouseMove = useCallback((event) => { + mousePos.current.x = event.clientX; + mousePos.current.y = event.clientY; + + if (!divRef.current) return; + divRef.current.style.left = `${mousePos.current.x}px`; + divRef.current.style.top = `${mousePos.current.y}px`; + }, [mousePos, divRef]); + + useEffect(() => { + const subscription = hoveredTrack$.subscribe(date => { + setHoveredTrack(date); + }); + + document.addEventListener('mousemove', handleMouseMove); + + return () => { + subscription.unsubscribe(); + document.removeEventListener('mousemove', handleMouseMove); + } + }, [handleMouseMove]); + + if (!hoveredTrack) return null; + + const divStyle = { + left: `${mousePos.current.x}px`, + top: `${mousePos.current.y}px`, + }; + + return ( +
+ +
+ ); +} diff --git a/src/components/map/track-hover-info/TrackHoverInfoConsts.js b/src/components/map/track-hover-info/TrackHoverInfoConsts.js new file mode 100644 index 0000000..323af63 --- /dev/null +++ b/src/components/map/track-hover-info/TrackHoverInfoConsts.js @@ -0,0 +1,17 @@ +import { ElectrificationStatus } from "../../../model/electrification-status"; + +const TrackHoverInfoConsts = { + electrification: { + [ElectrificationStatus.ELECTRIFIED]: 'Electrified', + [ElectrificationStatus.NON_ELECTRIFIED]: 'Non-electrified', + [ElectrificationStatus.NOT_CHECKED]: 'Unknown', + [ElectrificationStatus.CONFLICT]: 'Error / Conflict', + }, + type: { + 'StandardTrack': 'Standard track', + 'PointTrack': 'Switch track', + 'BezierTrack': 'Bezier track', + } +}; + +export default TrackHoverInfoConsts; diff --git a/src/components/map/track-hover-info/TrackHoverInfoPopup.js b/src/components/map/track-hover-info/TrackHoverInfoPopup.js new file mode 100644 index 0000000..10736d7 --- /dev/null +++ b/src/components/map/track-hover-info/TrackHoverInfoPopup.js @@ -0,0 +1,53 @@ +import TrackHoverInfoConsts from "./TrackHoverInfoConsts"; + +export default function TrackHoverInfoPopup(props) { + const { track } = props; + + const slopeArr = Array.from(new Set([track['start_slope'], track['end_slope']])) + const slopeOptions = {join: ' / ', subOptions: {suffix: ' ‰'}}; + + return ( +
+ + + + + + + + + + +
+
+ ); +} + +function processItemValue(value, options) { + const { translationConstKey, join, subOptions, suffix } = options || {}; + + if (Array.isArray(value)) { + return value.map(item => processItemValue(item, subOptions)).join(join || ", "); + } + + if (translationConstKey) { + return TrackHoverInfoConsts[translationConstKey]?.[value] || value; + } + + if (!value) { + return "N/A"; + } + + return `${value}${suffix || ''}`.trim(); +} + +function InfoPopupItem({ label, value, options = {} }) { + const textValue = processItemValue(value, options); + + return ( + + {label} + {textValue} + + ); +} \ No newline at end of file diff --git a/src/components/toolbar/CustomFileSelect.js b/src/components/toolbar/CustomFileSelect.js index 126b662..d1fbc90 100644 --- a/src/components/toolbar/CustomFileSelect.js +++ b/src/components/toolbar/CustomFileSelect.js @@ -2,6 +2,7 @@ import { useContext } from "react"; import MainContext from "../../contexts/MainContext"; import { useZoomPanEmitter } from "../../hooks/useZoomPubSub"; import SceneryFilesHelper from "../../helpers/sceneryFilesHelper"; +import { resetHoveredTracksStack } from "../../services/trackHoverInfoService"; export default function CustomFileSelect() { const {setScenery, setIsLoading} = useContext(MainContext); @@ -14,6 +15,7 @@ export default function CustomFileSelect() { setIsLoading(true); const scenery = await SceneryFilesHelper.loadCustom(file, center); setScenery(scenery); + resetHoveredTracksStack(); setIsLoading(false); }; diff --git a/src/components/toolbar/SceneriesList.js b/src/components/toolbar/SceneriesList.js index 56bfba9..61fd586 100644 --- a/src/components/toolbar/SceneriesList.js +++ b/src/components/toolbar/SceneriesList.js @@ -4,6 +4,7 @@ import { useZoomPanEmitter } from '../../hooks/useZoomPubSub'; import SceneryFilesHelper from '../../helpers/sceneryFilesHelper'; import { setSceneriesListVersionDate } from '../../services/sceneriesListService'; import Constants from '../../helpers/constants'; +import { resetHoveredTracksStack } from '../../services/trackHoverInfoService'; export default function SceneriesList(props) { const {setScenery, setIsLoading} = useContext(MainContext); @@ -16,6 +17,7 @@ export default function SceneriesList(props) { setIsLoading(true); const scenery = await SceneryFilesHelper.load(name, center); setScenery(scenery); + resetHoveredTracksStack(); setIsLoading(false); }; diff --git a/src/services/trackHoverInfoService.js b/src/services/trackHoverInfoService.js new file mode 100644 index 0000000..aa03d33 --- /dev/null +++ b/src/services/trackHoverInfoService.js @@ -0,0 +1,19 @@ +import { BehaviorSubject } from 'rxjs'; + +const hoveredTracksStack = []; +export const hoveredTrack$ = new BehaviorSubject(null); + +export function setHoveredTrack(track) { + hoveredTracksStack.push(track); + hoveredTrack$.next(track); +} + +export function unsetHoveredTrack(track) { + hoveredTracksStack.splice(hoveredTracksStack.indexOf(track), 1); + hoveredTrack$.next(hoveredTracksStack.at(-1) || null); +} + +export function resetHoveredTracksStack() { + hoveredTracksStack.length = 0; + hoveredTrack$.next(null); +}