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 Jun 18, 2024
1 parent b9bacda commit 077a8d7
Show file tree
Hide file tree
Showing 12 changed files with 410 additions and 124 deletions.
2 changes: 1 addition & 1 deletion front/src/applications/operationalStudies/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ export const useSimulationResults = () => {
selectedTrain: selectedTrainSchedule,
selectedTrainRollingStock,
selectedTrainPowerRestrictions: formattedPowerRestrictions,
trainSimulation,
trainSimulation: trainSimulation?.status === 'success' ? trainSimulation : undefined,
pathProperties,
};
};
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 && selectedTrainRollingStock && 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;
137 changes: 137 additions & 0 deletions front/src/common/Map/components/TrainOnMap/getTrainBody.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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 = {
left: 0.05,
right: 0.05,
up: 0.05,
upWidth: 0.019,
down: 0.02,
};
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
@@ -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 | null => {
const { headPosition, tailPosition } = positionValues;

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

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 077a8d7

Please sign in to comment.