Auto labels rotation
+Added AlwaysUpText component that rotates text by 180 degrees if it is upside down (taking view rotation into account) +Added SimpleLabelText component that always displays text horizontally relative to the view rotation +Added mapRotationService which stores a subscribable view rotation value *Refactored all ObjectRenderers to use the above components for object labels
This commit is contained in:
parent
8db877c2e3
commit
1e6cdf2bfb
@ -2,6 +2,7 @@ import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import './ZoomPanWrapper.css';
|
||||
import Constants from '../../helpers/constants';
|
||||
import { useZoomPanSubscriber, viewBox$, clientRect$, camera$ } from '../../hooks/useZoomPubSub';
|
||||
import { mapRotation$ } from '../../services/mapRotationService';
|
||||
|
||||
export default function ZoomPanWrapper({children}) {
|
||||
const wrapperRef = useRef(null);
|
||||
@ -38,6 +39,7 @@ export default function ZoomPanWrapper({children}) {
|
||||
rafScheduledRef.current = false;
|
||||
cameraWrapperRef.current.setAttribute('transform', getCameraTransformString());
|
||||
camera$.next(cameraRef.current);
|
||||
mapRotation$.next(cameraRef.current.rotation);
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
@ -1,4 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { mapRotation$ } from "../../../services/mapRotationService";
|
||||
import AngleHelper from "../../../helpers/angleHelper";
|
||||
|
||||
export default function DistanceMeterText(props) {
|
||||
const [mapRotation, setMapRotation] = useState(0);
|
||||
const { start, end, totalDistance } = props;
|
||||
|
||||
const cx = (start[0] + end[0]) / 2;
|
||||
@ -7,10 +12,19 @@ export default function DistanceMeterText(props) {
|
||||
const dy = end[1] - start[1];
|
||||
const len = Math.sqrt(dx * dx + dy * dy);
|
||||
const angle = Math.atan2(dy, dx) * (180 / Math.PI);
|
||||
const upsideDown = angle >= 90 || angle <= -90;
|
||||
const rotation = AngleHelper.normalizeDegAngle(mapRotation + angle);
|
||||
const upsideDown = rotation >= 90 && rotation <= 270;
|
||||
const x = cx + len / 2;
|
||||
const y = cy + (upsideDown ? 2 : -2);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = mapRotation$.subscribe(date => {
|
||||
setMapRotation(date);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
import { ReactSVG } from 'react-svg'
|
||||
import AngleHelper from '../../../helpers/angleHelper';
|
||||
import AlwaysUpText from '../text/AlwaysUpText';
|
||||
|
||||
export default function DerailerRenderer(props) {
|
||||
const { object } = props;
|
||||
const [x, y] = object.pos.toSVGCoords();
|
||||
|
||||
const rot = AngleHelper.normalizeDegAngle(object.rot.y);
|
||||
const upsideDown = rot >= 180;
|
||||
const upsideDownRot = upsideDown ? 90 : -90;
|
||||
const anchor = upsideDown ? "end" : "start";
|
||||
|
||||
return (
|
||||
<g className="derailer" transform={`translate(${x}, ${y}) rotate(${object.rot.y})`}>
|
||||
<g className="derailer-icon" transform={`translate(-2.835, -2.835)`}>
|
||||
@ -23,11 +18,12 @@ export default function DerailerRenderer(props) {
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g transform={`translate(0, -3) rotate(${upsideDownRot})`}>
|
||||
<text x="0" y="0" textAnchor={anchor} dominantBaseline="middle">
|
||||
{object.getPrintableDerailerName()}
|
||||
</text>
|
||||
</g>
|
||||
<AlwaysUpText
|
||||
baseRot={object.rot.y}
|
||||
additionalRot={-90}
|
||||
offsetY={-3}
|
||||
text={object.getPrintableDerailerName()}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import SimpleLabelText from "../text/SimpleLabelText";
|
||||
|
||||
export default function IsolationIdRenderer(props) {
|
||||
const { object } = props;
|
||||
const pos = object.points.start.lerp(object.points.end, 0.5);
|
||||
@ -8,16 +10,10 @@ export default function IsolationIdRenderer(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor='middle'
|
||||
id={`isolation-id-${object.id}`}
|
||||
style={{ userSelect: "none" }}
|
||||
className="isolation-id"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
);
|
||||
return <SimpleLabelText
|
||||
text={text}
|
||||
className="isolation-id"
|
||||
x={x}
|
||||
y={y}
|
||||
/>
|
||||
}
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
import { ReactSVG } from "react-svg";
|
||||
import AngleHelper from "../../../helpers/angleHelper";
|
||||
import AlwaysUpText from "../text/AlwaysUpText";
|
||||
|
||||
export default function RouteRenderer(props) {
|
||||
const { object } = props;
|
||||
const [x, y] = object.pos.toSVGCoords();
|
||||
|
||||
const rot = AngleHelper.normalizeDegAngle(object.rot.y);
|
||||
const upsideDown = rot >= 180;
|
||||
const upsideDownRot = upsideDown ? 90 : -90;
|
||||
const offY = object.track_count === 2 ? -22 : -20;
|
||||
const anchor = upsideDown ? "start" : "end";
|
||||
const arrowOffset = - object.track_offset - 37.8;
|
||||
|
||||
return (
|
||||
@ -24,11 +20,15 @@ export default function RouteRenderer(props) {
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g transform={`translate(${object.track_offset}, 0) rotate(${upsideDownRot})`}>
|
||||
<text x='0' y={offY} textAnchor={anchor} dominantBaseline="bottom">
|
||||
{object.route_name}
|
||||
</text>
|
||||
</g>
|
||||
<AlwaysUpText
|
||||
baseRot={object.rot.y}
|
||||
additionalRot={-90}
|
||||
offsetX={object.track_offset}
|
||||
reverseAnchor={true}
|
||||
y={offY}
|
||||
dominantBaseline="bottom"
|
||||
text={object.route_name}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ReactSVG } from 'react-svg';
|
||||
import AngleHelper from '../../../helpers/angleHelper';
|
||||
import AlwaysUpText from '../text/AlwaysUpText';
|
||||
|
||||
export default function SignalBoxRenderer(props) {
|
||||
const { object } = props;
|
||||
@ -7,9 +7,7 @@ export default function SignalBoxRenderer(props) {
|
||||
const text = object.getPrintableSignalBoxName();
|
||||
|
||||
const rotDiff = object.def.undef ? Math.round(object.rot.y / 90) * (-90) : object.def.rot * 90;
|
||||
const rot = AngleHelper.normalizeDegAngle(object.rot.y + rotDiff);
|
||||
const upsideDown = rot >= 90 && rot < 270;
|
||||
const upsideDownRot = upsideDown ? 180 : 0;
|
||||
const rot = object.rot.y + rotDiff;
|
||||
|
||||
return (
|
||||
<g className="signalbox" transform={`translate(${x}, ${y}) rotate(${rot})`}>
|
||||
@ -24,11 +22,13 @@ export default function SignalBoxRenderer(props) {
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g transform={`translate(0, 6) rotate(${upsideDownRot})`}>
|
||||
<text x="0" y="0" textAnchor="middle" dominantBaseline="middle">
|
||||
{text}
|
||||
</text>
|
||||
</g>
|
||||
<AlwaysUpText
|
||||
baseRot={rot - 90}
|
||||
additionalRot={180}
|
||||
offsetY={6}
|
||||
textAnchor='middle'
|
||||
text={text}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ import AngleHelper from '../../../helpers/angleHelper';
|
||||
import DefinedSignalSigns from '../../../model/defs/defined-signal-signs';
|
||||
import SignalElementsEnums from '../../../model/signal-elements/enums';
|
||||
import ExtendedSignalsHelper from '../../../helpers/extendedSignalsHelper';
|
||||
import AlwaysUpText from '../text/AlwaysUpText';
|
||||
import C from '../../../helpers/extendedSignalsConstants';
|
||||
|
||||
export default function SignalExtendedRenderer(props) {
|
||||
@ -24,20 +25,18 @@ export default function SignalExtendedRenderer(props) {
|
||||
|
||||
function SignalNameText(props) {
|
||||
const {object, rot, pointsData} = props;
|
||||
|
||||
const upsideDown = rot >= 180;
|
||||
const upsideDownRot = upsideDown ? 90 : -90;
|
||||
const anchor = upsideDown ? "start" : "end";
|
||||
|
||||
const isOverhead = object.signal_elements.isOverhead();
|
||||
const baseY = isOverhead ? pointsData.headOffsetY + pointsData.polePoints.end : 0;
|
||||
|
||||
return (
|
||||
<g transform={`translate(0, ${baseY + C.NAME_TEXT_OFFSET}) rotate(${upsideDownRot})`}>
|
||||
<text x="0" y="0" textAnchor={anchor} dominantBaseline="middle">
|
||||
{object.getPrintableSignalName()}
|
||||
</text>
|
||||
</g>
|
||||
<AlwaysUpText
|
||||
baseRot={rot}
|
||||
additionalRot={-90}
|
||||
offsetY={baseY + C.NAME_TEXT_OFFSET}
|
||||
text={object.getPrintableSignalName()}
|
||||
reverseAnchor={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
import { ReactSVG } from "react-svg";
|
||||
import AngleHelper from "../../../helpers/angleHelper";
|
||||
import AlwaysUpText from "../text/AlwaysUpText";
|
||||
|
||||
export default function SignalStandardRenderer(props) {
|
||||
const { object } = props;
|
||||
const [x, y] = object.pos.toSVGCoords();
|
||||
|
||||
const rot = AngleHelper.normalizeDegAngle(object.rot.y);
|
||||
const upsideDown = rot >= 180;
|
||||
const upsideDownRot = upsideDown ? 90 : -90;
|
||||
const anchor = upsideDown ? "end" : "start";
|
||||
|
||||
return (
|
||||
<g className="signal" transform={`translate(${x}, ${y}) rotate(${object.rot.y})`}>
|
||||
<g className="signal-icon" transform={`translate(-1.89, -1.89)`}>
|
||||
@ -23,11 +18,12 @@ export default function SignalStandardRenderer(props) {
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g transform={`translate(0, -2) rotate(${upsideDownRot})`}>
|
||||
<text x="0" y="0" textAnchor={anchor} dominantBaseline="middle">
|
||||
{object.getPrintableSignalName()}
|
||||
</text>
|
||||
</g>
|
||||
<AlwaysUpText
|
||||
baseRot={object.rot.y}
|
||||
additionalRot={-90}
|
||||
offsetY={-2}
|
||||
text={object.getPrintableSignalName()}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
import { ReactSVG } from 'react-svg'
|
||||
import AngleHelper from '../../../helpers/angleHelper';
|
||||
import AlwaysUpText from '../text/AlwaysUpText';
|
||||
|
||||
export default function SpawnPointRenderer(props) {
|
||||
const { object } = props;
|
||||
const [x, y] = object.pos.toSVGCoords();
|
||||
|
||||
const rot = AngleHelper.normalizeDegAngle(object.rot.y);
|
||||
const upsideDown = rot >= 180;
|
||||
const upsideDownRot = upsideDown ? 90 : -90;
|
||||
const anchor = upsideDown ? "start" : "end";
|
||||
|
||||
return (
|
||||
<g className="spawn-point" transform={`translate(${x}, ${y}) rotate(${object.rot.y})`}>
|
||||
<g className="signal-icon" transform={`rotate(180) translate(-2.835, -2.835)`}>
|
||||
@ -23,11 +18,13 @@ export default function SpawnPointRenderer(props) {
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
<g transform={`translate(0, 3) rotate(${upsideDownRot})`}>
|
||||
<text x="0" y="0" textAnchor={anchor} dominantBaseline="middle">
|
||||
{object.getPrintableSpawnPointName()}
|
||||
</text>
|
||||
</g>
|
||||
<AlwaysUpText
|
||||
baseRot={object.rot.y}
|
||||
additionalRot={-90}
|
||||
offsetY={3}
|
||||
reverseAnchor={true}
|
||||
text={object.getPrintableSpawnPointName()}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,18 +1,14 @@
|
||||
import SimpleLabelText from "../text/SimpleLabelText";
|
||||
|
||||
export default function SwitchNameRenderer(props) {
|
||||
const { object } = props;
|
||||
const [x, y] = object.pos.toSVGCoords();
|
||||
const text = object.id_switch;
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={y}
|
||||
textAnchor='middle'
|
||||
id={`switch-${object.id}`}
|
||||
style={{ userSelect: "none" }}
|
||||
className="switch-name"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
);
|
||||
return <SimpleLabelText
|
||||
text={text}
|
||||
className="switch-name"
|
||||
x={x}
|
||||
y={y}
|
||||
/>;
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import SimpleLabelText from "../text/SimpleLabelText";
|
||||
|
||||
export default function TrackObjectRenderer(props) {
|
||||
const { object } = props;
|
||||
const [x, y] = object.pos.toSVGCoords();
|
||||
@ -6,29 +8,26 @@ export default function TrackObjectRenderer(props) {
|
||||
return (
|
||||
<g className="track-object">
|
||||
<rect
|
||||
x={x - 2}
|
||||
y={y - 2}
|
||||
width='4px'
|
||||
height='4px'
|
||||
x={x - 1}
|
||||
y={y - 1}
|
||||
width='2px'
|
||||
height='2px'
|
||||
fill='transparent'
|
||||
stroke='none'
|
||||
></rect>
|
||||
<circle
|
||||
cx={x}
|
||||
cy={y}
|
||||
r='1px'
|
||||
r='0.5px'
|
||||
></circle>
|
||||
<text
|
||||
<SimpleLabelText
|
||||
text={text}
|
||||
className="track-object-label"
|
||||
x={x}
|
||||
y={y}
|
||||
id={`track-object-${object.id}`}
|
||||
style={{ userSelect: "none" }}
|
||||
enableBackground="new 0 0 100 100"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
wrapperStyle={{pointerEvents: 'none'}}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
|
||||
29
src/components/map/text/AlwaysUpText.js
Normal file
29
src/components/map/text/AlwaysUpText.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import AngleHelper from "../../../helpers/angleHelper";
|
||||
import { mapRotation$ } from "../../../services/mapRotationService";
|
||||
|
||||
export default function AlwaysUpText(props) {
|
||||
const [mapRotation, setMapRotation] = useState(0);
|
||||
const { baseRot, additionalRot, text, reverseAnchor, offsetX, offsetY } = props;
|
||||
|
||||
const adjustedRot = AngleHelper.normalizeDegAngle(baseRot + mapRotation);
|
||||
const upsideDown = adjustedRot >= 180;
|
||||
const upsideDownRot = upsideDown ? 180 : 0;
|
||||
const anchor = (upsideDown !== !!reverseAnchor) ? "end" : "start";
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = mapRotation$.subscribe(rotation => {
|
||||
setMapRotation(rotation);
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<g transform={`translate(${offsetX || 0}, ${offsetY || 0}) rotate(${upsideDownRot + additionalRot})`}>
|
||||
<text textAnchor={anchor} dominantBaseline='middle' {...props}>
|
||||
{text}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
29
src/components/map/text/SimpleLabelText.js
Normal file
29
src/components/map/text/SimpleLabelText.js
Normal file
@ -0,0 +1,29 @@
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { mapRotation$ } from "../../../services/mapRotationService";
|
||||
|
||||
export default function SimpleLabelText(props) {
|
||||
const { text, x, y, wrapperStyle } = props;
|
||||
const gRef = useRef(null);
|
||||
|
||||
const getTransformString = useCallback((rot) => (
|
||||
`translate(${x || 0}, ${y || 0}) rotate(${rot})`
|
||||
), [x, y]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = mapRotation$.subscribe(rotation => {
|
||||
if(!gRef.current) return;
|
||||
|
||||
gRef.current.setAttribute("transform", getTransformString(-rotation));
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [getTransformString]);
|
||||
|
||||
return (
|
||||
<g transform={getTransformString(0)} ref={gRef} style={wrapperStyle}>
|
||||
<text textAnchor="middle" dominantBaseline="middle" {...props} x={null} y={null}>
|
||||
{text}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
7
src/services/mapRotationService.js
Normal file
7
src/services/mapRotationService.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export const mapRotation$ = new BehaviorSubject(null);
|
||||
|
||||
export function setMapRotation(rotation) {
|
||||
mapRotation$.next(rotation);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user