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);
+}