Skip to content

Commit

Permalink
front: fix train on map (ts v2)
Browse files Browse the repository at this point in the history
  • Loading branch information
clarani committed Jul 5, 2024
1 parent 01fc006 commit b6df80a
Show file tree
Hide file tree
Showing 14 changed files with 414 additions and 130 deletions.
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

0 comments on commit b6df80a

Please sign in to comment.