From b6df80a297b9d9c0bb8c73431722250342c2a7f5 Mon Sep 17 00:00:00 2001 From: Clara Ni Date: Tue, 18 Jun 2024 16:07:50 +0200 Subject: [PATCH] front: fix train on map (ts v2) --- .../views/v2/SimulationResultsV2.tsx | 11 +- .../Map/components/TrainOnMap/TrainOnMap.tsx | 120 ++++++++++++++ .../Map/components/TrainOnMap/getTrainBody.ts | 134 +++++++++++++++ .../ChartHelpers/enableInteractivity.tsx | 2 +- .../TrainHoverPosition.tsx | 3 +- .../getSelectedTrainHoverPositions.ts | 39 +++++ .../components/SimulationResultsMapV2.tsx | 156 +++++++----------- .../SpaceCurvesSlopes/SpaceCurvesSlopesV2.tsx | 19 ++- .../SpeedSpaceChart/SpeedSpaceChartV2.tsx | 18 +- .../SpeedSpaceChart/useSpeedSpaceChart.ts | 2 +- .../components/TimeButtons.tsx | 11 +- front/src/utils/date.ts | 2 +- front/src/utils/numbers.ts | 11 +- front/src/utils/physics.ts | 16 +- 14 files changed, 414 insertions(+), 130 deletions(-) create mode 100644 front/src/common/Map/components/TrainOnMap/TrainOnMap.tsx create mode 100644 front/src/common/Map/components/TrainOnMap/getTrainBody.ts create mode 100644 front/src/modules/simulationResult/components/SimulationResultsMap/getSelectedTrainHoverPositions.ts 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; }