Added track hover info
+Added a popup showing hovered track's info
This commit is contained in:
parent
715a8ffc7c
commit
66eae33e80
@ -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) {
|
||||
<SceneryRoot />
|
||||
<DistanceMeter />
|
||||
</ZoomPanWrapper>
|
||||
<TrackHoverInfo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -19,7 +19,8 @@ const ObjectRendererQueue = [
|
||||
'additionalComponents': [
|
||||
ElectrificationStatusPopup
|
||||
],
|
||||
'cond': (layers) => layers['tracks']
|
||||
'cond': (layers) => layers['tracks'],
|
||||
'pointerEvents': true
|
||||
},
|
||||
{
|
||||
'name': 'isolations-ids',
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
|
||||
19
src/components/map/track-hover-info/TrackHoverInfo.css
Normal file
19
src/components/map/track-hover-info/TrackHoverInfo.css
Normal file
@ -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;
|
||||
}
|
||||
46
src/components/map/track-hover-info/TrackHoverInfo.js
Normal file
46
src/components/map/track-hover-info/TrackHoverInfo.js
Normal file
@ -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 (
|
||||
<div className="track-hover-info" ref={divRef} style={divStyle}>
|
||||
<TrackHoverInfoPopup track={hoveredTrack} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
17
src/components/map/track-hover-info/TrackHoverInfoConsts.js
Normal file
17
src/components/map/track-hover-info/TrackHoverInfoConsts.js
Normal file
@ -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;
|
||||
53
src/components/map/track-hover-info/TrackHoverInfoPopup.js
Normal file
53
src/components/map/track-hover-info/TrackHoverInfoPopup.js
Normal file
@ -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 (
|
||||
<div className="track-hover-info-popup">
|
||||
<table>
|
||||
<tbody>
|
||||
<InfoPopupItem value={track['id_station']} label="Track ID" />
|
||||
<InfoPopupItem value={track['id_isolation']} label="Isolation ID" />
|
||||
<InfoPopupItem value={track['derailspeed']} label="Derail speed" options={{suffix: ' km/h'}} />
|
||||
<InfoPopupItem value={track['maxspeed']} label="Max. speed" options={{suffix: ' km/h'}} />
|
||||
<InfoPopupItem value={slopeArr} label="Slope" options={slopeOptions} />
|
||||
<InfoPopupItem value={track['electrificationStatus']} label="Electrification" options={{translationConstKey: 'electrification'}} />
|
||||
<InfoPopupItem value={track['type']} label="Type" options={{translationConstKey: 'type'}} />
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<tr>
|
||||
<th>{label}</th>
|
||||
<td>{textValue}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
|
||||
19
src/services/trackHoverInfoService.js
Normal file
19
src/services/trackHoverInfoService.js
Normal file
@ -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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user