Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

train extremities #2139

Merged
merged 13 commits into from
Oct 25, 2022
3 changes: 3 additions & 0 deletions front/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
"@turf/along": "^6.5.0",
"@turf/bbox": "^6.0.1",
"@turf/bbox-clip": "^6.3.0",
"@turf/bearing": "^6.5.0",
"@turf/bezier-spline": "^6.5.0",
"@turf/boolean-intersects": "^6.3.0",
"@turf/boolean-point-in-polygon": "^6.3.0",
"@turf/distance": "^6.3.0",
Expand All @@ -25,6 +27,7 @@
"@turf/line-split": "^6.5.0",
"@turf/nearest-point": "^6.3.0",
"@turf/nearest-point-on-line": "^6.3.0",
"@turf/transform-translate": "^6.5.0",
"axios": "^0.27.2",
"classnames": "^2.3.2",
"d3": "^5.16.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Layer, Source, Marker } from 'react-map-gl';
import along from '@turf/along';
import lineSliceAlong from '@turf/line-slice-along';
import length from '@turf/length';
import bezierSpline from '@turf/bezier-spline';
import transformTranslate from '@turf/transform-translate';
import { Point, polygon, lineString } from '@turf/helpers';
import { Feature, LineString } from 'geojson';
import cx from 'classnames';
import { get } from 'lodash';
import { mapValues, get } from 'lodash';

import { RootState } from 'reducers';
import { Viewport } from 'reducers/map';
import { AllowancesSetting } from 'reducers/osrdsimulation';
import { datetime2time } from 'utils/timeManipulation';
import { boundedValue } from 'utils/numbers';
import { getCurrentBearing } from 'utils/geometry';
import { TrainPosition } from './types';

function getFill(isSelectedTrain: boolean, ecoBlocks: boolean) {
Expand Down Expand Up @@ -46,31 +51,135 @@ function getSpeedAndTimeLabel(isSelectedTrain: boolean, ecoBlocks: boolean, poin
);
}

interface TriangleSideDimensions {
left: number;
right: number;
up: number;
upWidth: number;
down: number;
}

// When the train is backward, lineSliceAlong will crash. we need to have head and tail in the right order
export function makeDisplayedHeadAndTail(point: TrainPosition, geojsonPath: Feature<LineString>) {
export function makeDisplayedHeadAndTail(
point: TrainPosition,
geojsonPath: Feature<LineString>,
sideDimensions: {
head: TriangleSideDimensions;
tail: TriangleSideDimensions;
}
) {
const pathLength = length(geojsonPath);
const trueHead = Math.max(point.tailDistanceAlong, point.headDistanceAlong);
const trueTail = Math.min(point.tailDistanceAlong, point.headDistanceAlong);
const [trueTail, trueHead] = [point.tailDistanceAlong, point.headDistanceAlong].sort(
(a, b) => a - b
);

const headMinusTriangle = trueHead - sideDimensions.head.up;
const tailPlusTriangle = Math.min(trueTail + sideDimensions.tail.down, headMinusTriangle);
const boundedHead = boundedValue(headMinusTriangle, [0, pathLength]);
const boundedTail = boundedValue(tailPlusTriangle, [0, pathLength]);

const headPosition = along(geojsonPath, boundedHead);
const tailPosition = along(geojsonPath, boundedTail);
return {
head: boundedValue(trueHead, 0, pathLength),
tail: boundedValue(trueTail, 0, pathLength),
headDistance: boundedHead,
tailDistance: boundedTail,
headPosition,
tailPosition,
};
}

function getLengthFactorToKeepLabelPlacedCorrectlyWhenZooming(viewport: Viewport, threshold = 12) {
function getzoomPowerOf2LengthFactor(viewport: Viewport, threshold = 12) {
return 2 ** (threshold - viewport?.zoom);
}

function getTriangleSideDimensions(zoomLengthFactor: number, size = 1) {
const scaleNumber = (x: number) => x * zoomLengthFactor * size;
const head = {
left: 0.05,
right: 0.05,
up: 0.1,
upWidth: 0.019,
down: 0.02,
};
const tail = {
left: 0.05,
right: 0.05,
up: 0.05,
upWidth: 0.019,
down: 0.02,
};
return {
head: mapValues(head, scaleNumber),
tail: mapValues(tail, scaleNumber),
};
}

function getHeadTriangle(
trainGeoJsonPath: Feature<LineString>,
position: Feature<Point>,
sideDimensions: Record<string, number>
) {
const bearing = getCurrentBearing(trainGeoJsonPath);
const left = transformTranslate(position, sideDimensions.left, bearing - 90);
const right = transformTranslate(position, sideDimensions.right, bearing + 90);
const up = transformTranslate(position, sideDimensions.up, bearing);
const down = transformTranslate(position, sideDimensions.down, bearing + 180);
const upLeft = transformTranslate(up, sideDimensions.upWidth, bearing - 90);
const upRight = transformTranslate(up, sideDimensions.upWidth, bearing + 90);
const coordinates = [
down.geometry.coordinates,
left.geometry.coordinates,
upLeft.geometry.coordinates,
upRight.geometry.coordinates,
right.geometry.coordinates,
down.geometry.coordinates,
];
const contour = lineString(coordinates);
const bezier = bezierSpline(contour);
const triangle = polygon([bezier.geometry.coordinates]);
return triangle;
}

function getTrainGeoJsonPath(
geojsonPath: Feature<LineString>,
tailDistance: number,
headDistance: number
) {
const threshold = 0.0005;
if (headDistance - tailDistance > threshold) {
return lineSliceAlong(geojsonPath, tailDistance, headDistance);
}
return lineSliceAlong(geojsonPath, headDistance - threshold, headDistance);
}

function getTrainPieces(
point: TrainPosition,
geojsonPath: Feature<LineString>,
zoomLengthFactor: number
) {
const sideDimensions = getTriangleSideDimensions(zoomLengthFactor, 2);
const { tailDistance, headDistance, headPosition, tailPosition } = makeDisplayedHeadAndTail(
point,
geojsonPath,
sideDimensions
);
const trainGeoJsonPath = getTrainGeoJsonPath(geojsonPath, tailDistance, headDistance);
const headTriangle = getHeadTriangle(trainGeoJsonPath, headPosition, sideDimensions.head);
const rearTriangle = getHeadTriangle(trainGeoJsonPath, tailPosition, sideDimensions.tail);
return [trainGeoJsonPath, headTriangle, rearTriangle];
}

interface TrainHoverPositionProps {
point: TrainPosition;
isSelectedTrain?: boolean;
geojsonPath: Feature<LineString>;
}

const shiftFactor = {
long: 1 / 450,
lat: 1 / 1000,
const labelShiftFactor = {
long: 0.005,
lat: 0.0011,
};

function TrainHoverPosition(props: TrainHoverPositionProps) {
const { point, isSelectedTrain = false, geojsonPath } = props;
const { selectedTrain, allowancesSettings } = useSelector(
Expand All @@ -84,32 +193,51 @@ function TrainHoverPosition(props: TrainHoverPositionProps) {
const label = getSpeedAndTimeLabel(isSelectedTrain, ecoBlocks, point);

if (geojsonPath && point.headDistanceAlong && point.tailDistanceAlong) {
const zoomLengthFactor = getLengthFactorToKeepLabelPlacedCorrectlyWhenZooming(viewport);
const { tail, head } = makeDisplayedHeadAndTail(point, geojsonPath);
const trainGeoJsonPath = lineSliceAlong(geojsonPath, tail, head);

const zoomLengthFactor = getzoomPowerOf2LengthFactor(viewport);
const [trainGeoJsonPath, headTriangle, rearTriangle] = getTrainPieces(
point,
geojsonPath,
zoomLengthFactor
);
return (
<>
<Marker
className="map-search-marker"
longitude={
point.headPosition.geometry.coordinates[0] + zoomLengthFactor * shiftFactor.long
point.headPosition.geometry.coordinates[0] + zoomLengthFactor * labelShiftFactor.long
}
latitude={
point.headPosition.geometry.coordinates[1] + zoomLengthFactor * labelShiftFactor.lat
}
latitude={point.headPosition.geometry.coordinates[1] + zoomLengthFactor * shiftFactor.lat}
>
{label}
</Marker>
<Source type="geojson" data={headTriangle}>
<Layer
id={`${point.id}-head`}
type="fill"
paint={{
'fill-color': fill,
}}
/>
</Source>
<Source type="geojson" data={rearTriangle}>
<Layer
id={`${point.id}-rear`}
type="fill"
paint={{
'fill-color': fill,
}}
/>
</Source>
<Source type="geojson" data={trainGeoJsonPath}>
<Layer
id={`${point.id}-path`}
type="line"
paint={{
'line-width': 8,
'line-width': 16,
'line-color': fill,
}}
layout={{
'line-cap': 'round',
}}
/>
</Source>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,25 @@ const unitKmSquare = [

const unitKmLine = [convertKmCoordsToDegree([0, 0]), convertKmCoordsToDegree([1, 0])];

const makeSideDimensions = (headUp = 0, tailDown = 0) => ({
head: {
left: 0,
right: 0,
up: headUp,
upWidth: 0,
down: 0,
},
tail: {
left: 0,
right: 0,
up: 0,
upWidth: 0,
down: tailDown,
},
});
describe('makeDisplayedHeadAndTail', () => {
describe('normal train', () => {
it('should return train head and tail to display', () => {
it('should return train head and tail distance along the path', () => {
const trainPosition: TrainPosition = {
id: 'train',
headPosition: pointFromKmCoords([1, 0]),
Expand All @@ -35,10 +51,78 @@ describe('makeDisplayedHeadAndTail', () => {
};
const pathPoints = unitKmSquare;
const pathLineString = lineString(pathPoints);
const { head, tail } = makeDisplayedHeadAndTail(trainPosition, pathLineString);
const sideDimensions = makeSideDimensions();
const { headDistance: head, tailDistance: tail } = makeDisplayedHeadAndTail(
trainPosition,
pathLineString,
sideDimensions
);
expect(head).toEqual(1);
expect(tail).toEqual(0);
});
it('should take the size of the tail and head triangle into account', () => {
const trainPosition: TrainPosition = {
id: 'train',
headPosition: pointFromKmCoords([1, 0]),
tailPosition: pointFromKmCoords([0, 0]),
headDistanceAlong: 1,
tailDistanceAlong: 0,
speedTime: { speed: 0, time: 0 },
trainLength: 1,
};
const pathPoints = unitKmSquare;
const pathLineString = lineString(pathPoints);
const sideDimensions = makeSideDimensions(0.1, 0.1);
const { headDistance: head, tailDistance: tail } = makeDisplayedHeadAndTail(
trainPosition,
pathLineString,
sideDimensions
);
expect(head).toEqual(0.9);
expect(tail).toEqual(0.1);
});
it('should avoid overlapping head and tail. Head should be exact/tail should not exceed over the head', () => {
const trainPosition: TrainPosition = {
id: 'train',
headPosition: pointFromKmCoords([1, 0]),
tailPosition: pointFromKmCoords([0, 0]),
headDistanceAlong: 1,
tailDistanceAlong: 0,
speedTime: { speed: 0, time: 0 },
trainLength: 1,
};
const pathPoints = unitKmSquare;
const pathLineString = lineString(pathPoints);
const sideDimensions = makeSideDimensions(0.6, 0.7);
const { headDistance: head, tailDistance: tail } = makeDisplayedHeadAndTail(
trainPosition,
pathLineString,
sideDimensions
);
expect(head).toEqual(0.4);
expect(tail).toEqual(0.4);
});
test('triangle correction should not make head/tail go beyound track limits', () => {
const trainPosition: TrainPosition = {
id: 'train',
headPosition: pointFromKmCoords([1, 0]),
tailPosition: pointFromKmCoords([0, 0]),
headDistanceAlong: 1,
tailDistanceAlong: 0,
speedTime: { speed: 0, time: 0 },
trainLength: 1,
};
const pathPoints = unitKmSquare;
const pathLineString = lineString(pathPoints);
const sideDimensions = makeSideDimensions(10, 10);
const { headDistance: head, tailDistance: tail } = makeDisplayedHeadAndTail(
trainPosition,
pathLineString,
sideDimensions
);
expect(head).toEqual(0);
expect(tail).toEqual(0);
});
});
describe('backward train', () => {
it('should return train head and tail', () => {
Expand All @@ -53,7 +137,12 @@ describe('makeDisplayedHeadAndTail', () => {
};
const pathPoints = unitKmSquare;
const pathLineString = lineString(pathPoints);
const { head, tail } = makeDisplayedHeadAndTail(trainPosition, pathLineString);
const sideDimensions = makeSideDimensions();
const { headDistance: head, tailDistance: tail } = makeDisplayedHeadAndTail(
trainPosition,
pathLineString,
sideDimensions
);
expect(head).toEqual(1);
expect(tail).toEqual(0);
});
Expand All @@ -71,7 +160,12 @@ describe('makeDisplayedHeadAndTail', () => {
};
const pathPoints = unitKmLine;
const pathLineString = lineString(pathPoints);
const { head, tail } = makeDisplayedHeadAndTail(trainPosition, pathLineString);
const sideDimensions = makeSideDimensions();
const { headDistance: head, tailDistance: tail } = makeDisplayedHeadAndTail(
trainPosition,
pathLineString,
sideDimensions
);
expect(head).toBeCloseTo(1, 6);
expect(tail).toBeCloseTo(1, 6);
});
Expand Down
Loading