diff --git a/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx b/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx
index 060600a7608..6dd6a34c57e 100644
--- a/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx
+++ b/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx
@@ -240,6 +240,15 @@ const SimulationResultsV2 = ({
@@ -247,7 +256,7 @@ const SimulationResultsV2 = ({
{/* TRAIN : DRIVER TRAIN SCHEDULE */}
{selectedTrain &&
- trainSimulation.status === 'success' &&
+ trainSimulation &&
pathProperties &&
selectedTrainRollingStock &&
infraId && (
diff --git a/front/src/common/Map/components/TrainOnMap/TrainOnMap.tsx b/front/src/common/Map/components/TrainOnMap/TrainOnMap.tsx
new file mode 100644
index 00000000000..bd4484450ec
--- /dev/null
+++ b/front/src/common/Map/components/TrainOnMap/TrainOnMap.tsx
@@ -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;
+}) => (
+ <>
+
+ {Math.round(trainInfo.speed)}
+ km/h
+
+ {`${datetime2time(trainInfo.time)}`}
+ >
+);
+
+const getZoomPowerOf2LengthFactor = (viewport: Viewport, threshold = 12) =>
+ 2 ** (threshold - viewport.zoom);
+
+type TrainOnMapProps = {
+ trainInfo: TrainCurrentInfo;
+ trainSimulation: SimulationResponseSuccess;
+ geojsonPath: Feature;
+ 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 (
+ <>
+
+
+
+ {trainExtremities.map((trainExtremity) => (
+
+
+
+ ))}
+
+
+
+ >
+ );
+};
+
+export default TrainOnMap;
diff --git a/front/src/common/Map/components/TrainOnMap/getTrainBody.ts b/front/src/common/Map/components/TrainOnMap/getTrainBody.ts
new file mode 100644
index 00000000000..46917b63c19
--- /dev/null
+++ b/front/src/common/Map/components/TrainOnMap/getTrainBody.ts
@@ -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,
+ 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,
+ position: Feature,
+ sideDimensions: Record
+) => {
+ 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,
+ 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,
+ 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 },
+ ],
+ };
+};
diff --git a/front/src/modules/simulationResult/components/ChartHelpers/enableInteractivity.tsx b/front/src/modules/simulationResult/components/ChartHelpers/enableInteractivity.tsx
index 5219c3644d1..cb88d837b9a 100644
--- a/front/src/modules/simulationResult/components/ChartHelpers/enableInteractivity.tsx
+++ b/front/src/modules/simulationResult/components/ChartHelpers/enableInteractivity.tsx
@@ -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)
);
diff --git a/front/src/modules/simulationResult/components/SimulationResultsMap/TrainHoverPosition.tsx b/front/src/modules/simulationResult/components/SimulationResultsMap/TrainHoverPosition.tsx
index 8c55ca2b5c5..91c66b4ec15 100644
--- a/front/src/modules/simulationResult/components/SimulationResultsMap/TrainHoverPosition.tsx
+++ b/front/src/modules/simulationResult/components/SimulationResultsMap/TrainHoverPosition.tsx
@@ -1,3 +1,4 @@
+// TO DO DROP V1: remove this file
import React from 'react';
import along from '@turf/along';
@@ -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';
diff --git a/front/src/modules/simulationResult/components/SimulationResultsMap/getSelectedTrainHoverPositions.ts b/front/src/modules/simulationResult/components/SimulationResultsMap/getSelectedTrainHoverPositions.ts
new file mode 100644
index 00000000000..3c1b68d00d7
--- /dev/null
+++ b/front/src/modules/simulationResult/components/SimulationResultsMap/getSelectedTrainHoverPositions.ts
@@ -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,
+ positionValues: PositionsSpeedTimes,
+ 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;
diff --git a/front/src/modules/simulationResult/components/SimulationResultsMapV2.tsx b/front/src/modules/simulationResult/components/SimulationResultsMapV2.tsx
index 584746f1f4e..2877e327e88 100644
--- a/front/src/modules/simulationResult/components/SimulationResultsMapV2.tsx
+++ b/front/src/modules/simulationResult/components/SimulationResultsMapV2.tsx
@@ -4,23 +4,18 @@ import bbox from '@turf/bbox';
import { lineString, point } from '@turf/helpers';
import lineLength from '@turf/length';
import lineSlice from '@turf/line-slice';
-import { keyBy } from 'lodash';
import type { MapLayerMouseEvent } from 'maplibre-gl';
import ReactMapGL, { AttributionControl, ScaleControl } from 'react-map-gl/maplibre';
import type { MapRef } from 'react-map-gl/maplibre';
import { useSelector } from 'react-redux';
import { useParams } from 'react-router-dom';
-/* Main data & layers */
-
-/* Settings & Buttons */
-
-/* Objects & various */
-
-/* Interactions */
-
-import type { PathPropertiesFormatted } from 'applications/operationalStudies/types';
+import type {
+ PathPropertiesFormatted,
+ SimulationResponseSuccess,
+} from 'applications/operationalStudies/types';
import MapButtons from 'common/Map/Buttons/MapButtons';
+import TrainOnMap, { type TrainCurrentInfo } from 'common/Map/components/TrainOnMap/TrainOnMap';
import { CUSTOM_ATTRIBUTION } from 'common/Map/const';
import colors from 'common/Map/Consts/colors';
import Background from 'common/Map/Layers/Background';
@@ -50,67 +45,58 @@ import { removeSearchItemMarkersOnMap } from 'common/Map/utils';
import { computeBBoxViewport } from 'common/Map/WarpedMap/core/helpers';
import { useInfraID } from 'common/osrdContext';
import { LAYER_GROUPS_ORDER, LAYERS } from 'config/layerOrder';
-import {
- getDirection,
- interpolateOnPosition,
-} from 'modules/simulationResult/components/ChartHelpers/ChartHelpers';
import RenderItinerary from 'modules/simulationResult/components/SimulationResultsMap/RenderItinerary';
-import TrainHoverPosition from 'modules/simulationResult/components/SimulationResultsMap/TrainHoverPosition';
-import type { TrainPosition } from 'modules/simulationResult/components/SimulationResultsMap/types';
import VirtualLayers from 'modules/simulationResult/components/SimulationResultsMap/VirtualLayers';
import type { RootState } from 'reducers';
import { updateViewport } from 'reducers/map';
import type { Viewport } from 'reducers/map';
import { getLayersSettings, getTerrain3DExaggeration } from 'reducers/map/selectors';
-import { getPresentSimulation, getSelectedTrain } from 'reducers/osrdsimulation/selectors';
-import type { Train } from 'reducers/osrdsimulation/types';
import { useAppDispatch } from 'store';
+import { isoDateWithTimezoneToSec } from 'utils/date';
+import { kmToM, mmToM, msToKmh } from 'utils/physics';
+import { interpolateOnPositionV2 } from './ChartHelpers/ChartHelpers';
import { useChartSynchronizerV2 } from './ChartSynchronizer';
-import { getRegimeKey, getSimulationHoverPositions } from './SimulationResultsMap/helpers';
+import getSelectedTrainHoverPositions from './SimulationResultsMap/getSelectedTrainHoverPositions';
type MapProps = {
setExtViewport: (viewport: Viewport) => void;
geometry?: PathPropertiesFormatted['geometry'];
+ trainSimulation?: SimulationResponseSuccess & { trainId: number; startTime: string };
};
-const Map: FC = ({ geometry }) => {
+const SimulationResultMapV2: FC = ({ geometry, trainSimulation }) => {
+ const { urlLat = '', urlLon = '', urlZoom = '', urlBearing = '', urlPitch = '' } = useParams();
+
const mapBlankStyle = useMapBlankStyle();
- const [mapLoaded, setMapLoaded] = useState(false);
const { viewport, mapSearchMarker, mapStyle, showOSM } = useSelector(
(state: RootState) => state.map
);
- const { isPlaying, allowancesSettings } = useSelector((state: RootState) => state.osrdsimulation);
- const simulation = useSelector(getPresentSimulation);
- const trains = useMemo(() => keyBy(simulation.trains, 'id'), [simulation.trains]);
- const selectedTrain = useSelector(getSelectedTrain);
+ const { isPlaying } = useSelector((state: RootState) => state.osrdsimulation);
const terrain3DExaggeration = useSelector(getTerrain3DExaggeration);
const layersSettings = useSelector(getLayersSettings);
+ const [mapLoaded, setMapLoaded] = useState(false);
+ const [interactiveLayerIds, setInteractiveLayerIds] = useState([]);
+ const [selectedTrainHoverPosition, setSelectedTrainHoverPosition] = useState();
+
const geojsonPath = useMemo(() => geometry && lineString(geometry.coordinates), [geometry]);
- const [selectedTrainHoverPosition, setTrainHoverPosition] = useState();
- const [otherTrainsHoverPosition, setOtherTrainsHoverPosition] = useState([]);
- const { urlLat = '', urlLon = '', urlZoom = '', urlBearing = '', urlPitch = '' } = useParams();
const dispatch = useAppDispatch();
const { updateTimePosition } = useChartSynchronizerV2(
- (timePosition, positionValues) => {
- if (timePosition && geojsonPath) {
- const positions = getSimulationHoverPositions(
+ (_, positionValues) => {
+ if (trainSimulation && geojsonPath) {
+ const selectedTrainPosition = getSelectedTrainHoverPositions(
geojsonPath,
- simulation,
- timePosition,
positionValues,
- selectedTrain?.id,
- allowancesSettings
+ trainSimulation.trainId
);
- setTrainHoverPosition(positions.find((train) => train.isSelected));
- setOtherTrainsHoverPosition(positions.filter((train) => !train.isSelected));
+ setSelectedTrainHoverPosition(selectedTrainPosition);
}
},
'simulation-result-map',
- [geojsonPath, simulation, selectedTrain, allowancesSettings]
+ [geojsonPath, trainSimulation]
);
const updateViewportChange = useCallback(
@@ -134,49 +120,42 @@ const Map: FC = ({ geometry }) => {
});
};
- const onFeatureHover = (e: MapLayerMouseEvent) => {
- if (mapLoaded && !isPlaying && e && geojsonPath?.geometry.coordinates && selectedTrain) {
+ const onPathHover = (e: MapLayerMouseEvent) => {
+ if (mapLoaded && !isPlaying && e && geojsonPath && trainSimulation) {
const line = lineString(geojsonPath.geometry.coordinates);
const cursorPoint = point(e.lngLat.toArray());
- const key = getRegimeKey(selectedTrain.id);
- const train = selectedTrain[key];
- if (train) {
- const lastCoordinates =
- geojsonPath.geometry.coordinates[geojsonPath.geometry.coordinates.length - 1];
- const startCoordinates = getDirection(train.head_positions)
- ? [geojsonPath.geometry.coordinates[0][0], geojsonPath.geometry.coordinates[0][1]]
- : [lastCoordinates[0], lastCoordinates[1]];
- const start = point(startCoordinates);
- const sliced = lineSlice(start, cursorPoint, line);
- const positionLocal = lineLength(sliced, { units: 'kilometers' }) * 1000;
- const timePositionLocal = interpolateOnPosition({ speed: train.speeds }, positionLocal);
- if (timePositionLocal instanceof Date) {
- updateTimePosition(timePositionLocal);
- } else {
- throw new Error(
- 'Map onFeatureHover, try to update TimePositionValue with incorrect imput'
- );
- }
+
+ const startCoordinates = geojsonPath.geometry.coordinates[0];
+
+ const start = point(startCoordinates);
+ const sliced = lineSlice(start, cursorPoint, line);
+ const positionLocal = kmToM(lineLength(sliced, { units: 'kilometers' }));
+
+ const baseSpeedData = trainSimulation.base.speeds.map((speed, i) => ({
+ speed: msToKmh(speed),
+ position: mmToM(trainSimulation.base.positions[i]),
+ time: trainSimulation.base.times[i],
+ }));
+ const timePositionLocal = interpolateOnPositionV2(
+ { speed: baseSpeedData },
+ positionLocal,
+ isoDateWithTimezoneToSec(trainSimulation.startTime)
+ );
+
+ if (timePositionLocal instanceof Date) {
+ updateTimePosition(timePositionLocal);
+ } else {
+ throw new Error('Map onFeatureHover, try to update TimePositionValue with incorrect imput');
}
}
};
- function defineInteractiveLayers() {
- const interactiveLayersLocal: string[] = [];
- if (mapLoaded && geojsonPath) {
- interactiveLayersLocal.push('geojsonPath');
- interactiveLayersLocal.push('main-train-path');
- otherTrainsHoverPosition.forEach((train) => {
- interactiveLayersLocal.push(`${train.id}-path`);
- });
- }
- return interactiveLayersLocal;
- }
- const [interactiveLayerIds, setInteractiveLayerIds] = useState([]);
useEffect(() => {
- setInteractiveLayerIds(defineInteractiveLayers());
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [geojsonPath, otherTrainsHoverPosition.length]);
+ const interactiveLayers: string[] =
+ mapLoaded && geojsonPath ? ['geojsonPath', 'main-train-path'] : [];
+ setInteractiveLayerIds(interactiveLayers);
+ }, [geojsonPath]);
+
useEffect(() => {
if (mapRef.current) {
if (urlLat) {
@@ -222,7 +201,7 @@ const Map: FC = ({ geometry }) => {
mapStyle={mapBlankStyle}
onMove={(e) => updateViewportChange(e.viewState)}
attributionControl={false} // Defined below
- onMouseEnter={onFeatureHover}
+ onMouseEnter={onPathHover}
onResize={(e) => {
updateViewportChange({
width: e.target.getContainer().offsetWidth,
@@ -358,34 +337,17 @@ const Map: FC = ({ geometry }) => {
/>
)}
- {geojsonPath && selectedTrainHoverPosition && selectedTrain && (
-
)}
- {geojsonPath &&
- otherTrainsHoverPosition.map((pt) =>
- trains[pt.trainId] ? (
-
- ) : null
- )}
>
);
};
-export default Map;
+export default SimulationResultMapV2;
diff --git a/front/src/modules/simulationResult/components/SpaceCurvesSlopes/SpaceCurvesSlopesV2.tsx b/front/src/modules/simulationResult/components/SpaceCurvesSlopes/SpaceCurvesSlopesV2.tsx
index cf43b916e2f..eb1a5f34975 100644
--- a/front/src/modules/simulationResult/components/SpaceCurvesSlopes/SpaceCurvesSlopesV2.tsx
+++ b/front/src/modules/simulationResult/components/SpaceCurvesSlopes/SpaceCurvesSlopesV2.tsx
@@ -4,11 +4,13 @@ import * as d3 from 'd3';
import { CgLoadbar } from 'react-icons/cg';
import { useSelector } from 'react-redux';
-import type { PathPropertiesFormatted } from 'applications/operationalStudies/types';
-import type { SimulationResponse } from 'common/api/osrdEditoastApi';
+import type {
+ PathPropertiesFormatted,
+ SimulationResponseSuccess,
+} from 'applications/operationalStudies/types';
import {
defineLinear,
- interpolateOnPosition,
+ interpolateOnPositionV2,
mergeDatasAreaConstantV2,
} from 'modules/simulationResult/components/ChartHelpers/ChartHelpers';
import defineChart from 'modules/simulationResult/components/ChartHelpers/defineChart';
@@ -26,7 +28,7 @@ import { CHART_AXES } from 'modules/simulationResult/consts';
import type { PositionScaleDomain, SpaceCurvesSlopesDataV2 } from 'modules/simulationResult/types';
import { getIsPlaying } from 'reducers/osrdsimulation/selectors';
import type { Chart, SpeedSpaceChart } from 'reducers/osrdsimulation/types';
-import { dateIsInRange } from 'utils/date';
+import { dateIsInRange, isoDateWithTimezoneToSec } from 'utils/date';
import { mmToM } from 'utils/physics';
import { drawAxisTitle, drawSpaceCurvesSlopesChartCurve } from './utils';
@@ -36,7 +38,7 @@ const CHART_ID = 'SpaceCurvesSlopes';
type SpaceCurvesSlopesV2Props = {
initialHeight: number;
- trainSimulation: Extract;
+ trainSimulation: SimulationResponseSuccess;
pathProperties: PathPropertiesFormatted;
sharedXScaleDomain?: PositionScaleDomain;
setSharedXScaleDomain?: React.Dispatch>;
@@ -91,10 +93,9 @@ const SpaceCurvesSlopesV2 = ({
const timeScaleRange: [Date, Date] = useMemo(() => {
if (chart) {
const spaceScaleRange = chart.x.domain();
- return spaceScaleRange.map((position) => interpolateOnPosition(trainData, position)) as [
- Date,
- Date,
- ];
+ return spaceScaleRange.map((position) =>
+ interpolateOnPositionV2(trainData, position, isoDateWithTimezoneToSec(departureTime))
+ ) as [Date, Date];
}
return [new Date(), new Date()];
}, [chart]);
diff --git a/front/src/modules/simulationResult/components/SpeedSpaceChart/SpeedSpaceChartV2.tsx b/front/src/modules/simulationResult/components/SpeedSpaceChart/SpeedSpaceChartV2.tsx
index 46883abf5f0..b8c0a4ccadc 100644
--- a/front/src/modules/simulationResult/components/SpeedSpaceChart/SpeedSpaceChartV2.tsx
+++ b/front/src/modules/simulationResult/components/SpeedSpaceChart/SpeedSpaceChartV2.tsx
@@ -7,13 +7,15 @@ import { GiResize } from 'react-icons/gi';
import { useSelector } from 'react-redux';
import { Rnd } from 'react-rnd';
-import type { PathPropertiesFormatted } from 'applications/operationalStudies/types';
+import type {
+ PathPropertiesFormatted,
+ SimulationResponseSuccess,
+} from 'applications/operationalStudies/types';
import type {
LightRollingStock,
SimulationPowerRestrictionRange,
- SimulationResponse,
} from 'common/api/osrdEditoastApi';
-import { interpolateOnPosition } from 'modules/simulationResult/components/ChartHelpers/ChartHelpers';
+import { interpolateOnPositionV2 } from 'modules/simulationResult/components/ChartHelpers/ChartHelpers';
import {
enableInteractivityV2,
traceVerticalLine,
@@ -29,7 +31,7 @@ import { updateSpeedSpaceSettings } from 'reducers/osrdsimulation/actions';
import { getIsPlaying, getSpeedSpaceSettings } from 'reducers/osrdsimulation/selectors';
import type { SpeedSpaceChart, SpeedSpaceSettingsType } from 'reducers/osrdsimulation/types';
import { useAppDispatch } from 'store';
-import { dateIsInRange } from 'utils/date';
+import { dateIsInRange, isoDateWithTimezoneToSec } from 'utils/date';
import ElectricalProfilesLegend from './ElectricalProfilesLegend';
import { prepareSpeedSpaceDataV2 } from './prepareData';
@@ -46,7 +48,7 @@ const SETTINGS_TO_AXIS = {
export type SpeedSpaceChartV2Props = {
initialHeight: number;
onSetChartBaseHeight: (chartBaseHeight: number) => void;
- trainSimulation: SimulationResponse;
+ trainSimulation: SimulationResponseSuccess;
selectedTrainPowerRestrictions: SimulationPowerRestrictionRange[];
pathProperties: PathPropertiesFormatted;
trainRollingStock?: LightRollingStock;
@@ -109,7 +111,11 @@ const SpeedSpaceChartV2 = ({
if (chart && formattedTrainSimulation) {
const spaceScaleRange = chart.x.domain();
return spaceScaleRange.map((position) =>
- interpolateOnPosition(formattedTrainSimulation, position)
+ interpolateOnPositionV2(
+ formattedTrainSimulation,
+ position,
+ isoDateWithTimezoneToSec(departureTime)
+ )
) as [Date, Date];
}
return [new Date(), new Date()];
diff --git a/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts b/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts
index 3927edc6fda..1446462fd6d 100644
--- a/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts
+++ b/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts
@@ -89,7 +89,7 @@ const useSpeedSpaceChart = (
return trainScheduleResult &&
rollingStock &&
formattedPowerRestrictions &&
- simulation &&
+ simulation?.status === 'success' &&
formattedPathProperties &&
departureTime
? {
diff --git a/front/src/modules/simulationResult/components/TimeButtons.tsx b/front/src/modules/simulationResult/components/TimeButtons.tsx
index 8804b0e3bc2..87c0d9b99be 100644
--- a/front/src/modules/simulationResult/components/TimeButtons.tsx
+++ b/front/src/modules/simulationResult/components/TimeButtons.tsx
@@ -1,10 +1,10 @@
import React, { useState } from 'react';
-import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import { FaBackward, FaPause, FaPlay, FaStop } from 'react-icons/fa';
import { useSelector } from 'react-redux';
+import { convertDepartureTimeIntoSec } from 'applications/operationalStudies/utils';
import type { SimulationReport } from 'common/api/osrdEditoastApi';
import InputSNCF from 'common/BootstrapSNCF/InputSNCF';
import { updateIsPlaying } from 'reducers/osrdsimulation/actions';
@@ -58,7 +58,8 @@ const TimeButtons = ({ selectedTrain, departureTime }: TimeButtonsProps) => {
clearInterval(playInterval);
setPlayInterval(undefined);
if (trainScheduleV2Activated) {
- if (departureTime) updateTimePositionV2(dayjs(departureTime, 'D/MM/YYYY HH:mm:ss').toDate());
+ if (departureTime)
+ updateTimePositionV2(sec2datetime(convertDepartureTimeIntoSec(departureTime)));
} else if (selectedTrain) {
updateTimePosition(sec2datetime(selectedTrain.base.stops[0].time));
}
@@ -83,7 +84,11 @@ const TimeButtons = ({ selectedTrain, departureTime }: TimeButtonsProps) => {
} else {
i += factor.steps;
}
- updateTimePosition(new Date(i * 1000));
+ if (trainScheduleV2Activated) {
+ updateTimePositionV2(new Date(i * 1000));
+ } else {
+ updateTimePosition(new Date(i * 1000));
+ }
}, factor.ms);
setPlayInterval(playIntervalLocal);
dispatch(updateIsPlaying(true));
diff --git a/front/src/utils/date.ts b/front/src/utils/date.ts
index 136b750aee3..6a58f029384 100644
--- a/front/src/utils/date.ts
+++ b/front/src/utils/date.ts
@@ -117,7 +117,7 @@ export const formatDateToString = (date: Date) => {
/** check whether a date is included in the range or not */
export function dateIsInRange(date: Date, range: [Date, Date]) {
- return date > range[0] && date < range[1];
+ return range[0] <= date && date <= range[1];
}
export const formatDateForInput = (date?: string | null) => (date ? date.substring(0, 10) : '');
diff --git a/front/src/utils/numbers.ts b/front/src/utils/numbers.ts
index 6c1581add4f..f220158a15a 100644
--- a/front/src/utils/numbers.ts
+++ b/front/src/utils/numbers.ts
@@ -1,10 +1,7 @@
-export function boundedValue(value: number, [min, max]: [number, number]) {
- if (value >= max) {
- return max;
- }
- if (value <= min) {
- return min;
- }
+/** Returns a value clamped within the inclusive range [min, max] */
+export function clamp(value: number, [min, max]: [number, number]) {
+ if (value >= max) return max;
+ if (value <= min) return min;
return value;
}
diff --git a/front/src/utils/physics.ts b/front/src/utils/physics.ts
index b195a7e6912..9c904e60f86 100644
--- a/front/src/utils/physics.ts
+++ b/front/src/utils/physics.ts
@@ -6,17 +6,27 @@ export function jouleToKwh(jouleEnergy: number, roundedUp = false) {
return value;
}
-// Convert millimeters to meters
+/** Convert meters to km */
+export function mToKm(length: number) {
+ return length / 1000;
+}
+
+/** Convert km to meters */
+export function kmToM(length: number) {
+ return length * 1000;
+}
+
+/** Convert millimeters to meters */
export function mmToM(length: number) {
return length / 1000;
}
-// Convert km/h to m/s
+/** Convert km/h to m/s */
export function kmhToMs(v: number) {
return Math.abs(v / 3.6);
}
-// Convert m/s to km/h
+/** Convert m/s to km/h */
export function msToKmh(v: number) {
return v * 3.6;
}