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

front: fix train on map (ts v2) #7773

Merged
merged 1 commit into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -240,14 +240,23 @@ const SimulationResultsV2 = ({
<SimulationResultsMapV2
setExtViewport={setExtViewport}
geometry={pathProperties?.geometry}
trainSimulation={
selectedTrain && trainSimulation
? {
...trainSimulation,
trainId: selectedTrain.id,
startTime: selectedTrain.start_time,
}
: undefined
}
/>
</div>
</div>
</div>

{/* TRAIN : DRIVER TRAIN SCHEDULE */}
{selectedTrain &&
trainSimulation.status === 'success' &&
trainSimulation &&
pathProperties &&
selectedTrainRollingStock &&
infraId && (
Expand Down
120 changes: 120 additions & 0 deletions front/src/common/Map/components/TrainOnMap/TrainOnMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React, { useMemo } from 'react';

import type { Position } from '@turf/helpers';
import cx from 'classnames';
import type { Feature, LineString } from 'geojson';
import { Source, Marker } from 'react-map-gl/maplibre';

import type { SimulationResponseSuccess } from 'applications/operationalStudies/types';
import OrderedLayer from 'common/Map/Layers/OrderedLayer';
import { LAYERS, LAYER_GROUPS_ORDER } from 'config/layerOrder';
import type { Viewport } from 'reducers/map';
import { datetime2time } from 'utils/timeManipulation';

import { getTrainPieces } from './getTrainBody';

/** Information of the train at a precise moment */
export type TrainCurrentInfo = {
trainId: number;
headPositionCoord: Position;
headDistanceAlong: number; // in km
tailDistanceAlong: number; // in km
speed: number;
time: Date;
};

const LABEL_SHIFT_FACTORS = {
LONG: 0.005,
LAT: 0.0011,
};

const TrainLabel = ({
isEcoTrain,
trainInfo,
}: {
isEcoTrain: boolean;
trainInfo: TrainCurrentInfo;
}) => (
<>
<span
className={cx(
'small',
'train-speed-label',
'font-weight-bold',
isEcoTrain ? 'text-secondary' : 'text-primary'
)}
>
{Math.round(trainInfo.speed)}
km/h
</span>
<span className="ml-2 small train-speed-label">{`${datetime2time(trainInfo.time)}`}</span>
</>
);

const getZoomPowerOf2LengthFactor = (viewport: Viewport, threshold = 12) =>
2 ** (threshold - viewport.zoom);

type TrainOnMapProps = {
trainInfo: TrainCurrentInfo;
trainSimulation: SimulationResponseSuccess;
geojsonPath: Feature<LineString>;
viewport: Viewport;
};

// TO DO DROP V1: remove this comment
// TrainOnMap corresponds to TrainHoverPositionV2
const TrainOnMap = ({ trainInfo, geojsonPath, viewport, trainSimulation }: TrainOnMapProps) => {
const zoomLengthFactor = getZoomPowerOf2LengthFactor(viewport);

const { trainBody, trainExtremities } = getTrainPieces(trainInfo, geojsonPath, zoomLengthFactor);

const coordinates = useMemo(
() => ({
lat: trainInfo.headPositionCoord[1] + zoomLengthFactor * LABEL_SHIFT_FACTORS.LAT,
long: trainInfo.headPositionCoord[0] + zoomLengthFactor * LABEL_SHIFT_FACTORS.LONG,
}),
[trainInfo]
);

const isEcoTrain = useMemo(
() => trainSimulation.base.energy_consumption < trainSimulation.final_output.energy_consumption,
[trainSimulation]
);

return (
<>
<Marker longitude={coordinates.long} latitude={coordinates.lat}>
<TrainLabel isEcoTrain={isEcoTrain} trainInfo={trainInfo} />
</Marker>
{trainExtremities.map((trainExtremity) => (
<Source
type="geojson"
data={trainExtremity.data}
key={`${trainInfo.trainId}-${trainExtremity.name}`}
>
<OrderedLayer
id={`${trainInfo.trainId}-${trainExtremity.name}`}
type="fill"
paint={{
'fill-color': '#303383',
}}
layerOrder={LAYER_GROUPS_ORDER[LAYERS.TRAIN.GROUP]}
/>
</Source>
))}
<Source type="geojson" data={trainBody.data}>
<OrderedLayer
id={`${trainInfo.trainId}-path`}
type="line"
paint={{
'line-width': 16,
'line-color': '#303383',
}}
layerOrder={LAYER_GROUPS_ORDER[LAYERS.TRAIN.GROUP]}
/>
</Source>
</>
);
};

export default TrainOnMap;
134 changes: 134 additions & 0 deletions front/src/common/Map/components/TrainOnMap/getTrainBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import along from '@turf/along';
import bezierSpline from '@turf/bezier-spline';
import { type Point, polygon, lineString } from '@turf/helpers';
import length from '@turf/length';
import lineSliceAlong from '@turf/line-slice-along';
import transformTranslate from '@turf/transform-translate';
import type { Feature, LineString } from 'geojson';
import { mapValues } from 'lodash';

import { getCurrentBearing } from 'utils/geometry';
import { clamp } from 'utils/numbers';

import type { TrainCurrentInfo } from './TrainOnMap';

type 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 const computeExtremityPositionPoints = (
{ headDistanceAlong, tailDistanceAlong }: TrainCurrentInfo,
geojsonPath: Feature<LineString>,
sideDimensions: {
head: TriangleSideDimensions;
tail: TriangleSideDimensions;
}
) => {
const headMinusTriangle = headDistanceAlong - sideDimensions.head.up;
const tailPlusTriangle = Math.min(
tailDistanceAlong + sideDimensions.tail.down,
headMinusTriangle
);

const pathLength = length(geojsonPath);
const headDistance = clamp(headMinusTriangle, [0, pathLength]);
const tailDistance = clamp(tailPlusTriangle, [0, pathLength]);

const headPositionPoint = along(geojsonPath, headDistance);
const tailPositionPoint = along(geojsonPath, tailDistance);

return {
headDistance,
tailDistance,
headPositionPoint,
tailPositionPoint,
};
};

const getTriangleSideDimensions = (zoomLengthFactor: number, size = 2) => {
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 = {
...head,
up: 0.05,
};
return {
head: mapValues(head, scaleNumber),
tail: mapValues(tail, scaleNumber),
};
};

const getTriangle = (
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;
};

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

export const getTrainPieces = (
trainInfo: TrainCurrentInfo,
geojsonPath: Feature<LineString>,
zoomLengthFactor: number
) => {
const sideDimensions = getTriangleSideDimensions(zoomLengthFactor);

const { tailDistance, headDistance, headPositionPoint, tailPositionPoint } =
computeExtremityPositionPoints(trainInfo, geojsonPath, sideDimensions);

const trainGeoJsonPath = getTrainGeoJsonPath(geojsonPath, tailDistance, headDistance);
const headTriangle = getTriangle(trainGeoJsonPath, headPositionPoint, sideDimensions.head);
const rearTriangle = getTriangle(trainGeoJsonPath, tailPositionPoint, sideDimensions.tail);

return {
trainBody: { name: 'path', data: trainGeoJsonPath },
trainExtremities: [
{ name: 'head', data: headTriangle },
{ name: 'tail', data: rearTriangle },
],
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ export const enableInteractivityV2 = <
const positionLocal = chart.x.invert(pointer(event, event.currentTarget)[0]) as number;
timePositionLocal = interpolateOnPositionV2(
selectedTrainData as { speed: PositionSpeedTime[] },
positionLocal,
Math.max(positionLocal, 0),
isoDateWithTimezoneToSec(selectedTrainDepartureDate)
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// TO DO DROP V1: remove this file
import React from 'react';

import along from '@turf/along';
Expand All @@ -15,7 +16,7 @@ import OrderedLayer from 'common/Map/Layers/OrderedLayer';
import type { Viewport } from 'reducers/map';
import type { AllowancesSetting, AllowancesSettings, Train } from 'reducers/osrdsimulation/types';
import { getCurrentBearing } from 'utils/geometry';
import { boundedValue } from 'utils/numbers';
import { clamp as boundedValue } from 'utils/numbers';
import { datetime2time } from 'utils/timeManipulation';

import type { TrainPosition } from './types';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import along from '@turf/along';
import { lineString } from '@turf/helpers';
import type { Feature, LineString } from 'geojson';
import { max, min } from 'lodash';

import type { TrainCurrentInfo } from 'common/Map/components/TrainOnMap/TrainOnMap';
import type { PositionsSpeedTimes } from 'reducers/osrdsimulation/types';
import { mToKm } from 'utils/physics';

const getSelectedTrainHoverPositions = (
geojsonPath: Feature<LineString>,
positionValues: PositionsSpeedTimes<Date>,
trainId: number
): TrainCurrentInfo | undefined => {
const { headPosition, tailPosition } = positionValues;

if (headPosition === undefined || tailPosition === undefined) {
return undefined;
}

const headDistanceAlong = mToKm(headPosition.position);
const tailDistanceAlong = mToKm(tailPosition.position);

const line = lineString(geojsonPath.geometry.coordinates);
const headPositionPoint = along(line, headDistanceAlong, {
units: 'kilometers',
});

return {
trainId,
headPositionCoord: headPositionPoint.geometry.coordinates,
headDistanceAlong: max([headDistanceAlong, tailDistanceAlong])!,
tailDistanceAlong: min([headDistanceAlong, tailDistanceAlong])!,
speed: positionValues.speed.speed,
time: positionValues.speed.time,
};
};

export default getSelectedTrainHoverPositions;
Loading
Loading