diff --git a/front/scripts/i18n-checker.ts b/front/scripts/i18n-checker.ts index af620470175..2c3ec6a46fe 100644 --- a/front/scripts/i18n-checker.ts +++ b/front/scripts/i18n-checker.ts @@ -13,6 +13,8 @@ const IGNORE_MISSING: RegExp[] = [ /translation:default/, /translation:error/, /translation:unspecified/, + // key used by upsertMapWaypointsInOperationalPoints + /translation:requestedPoint/, ]; const IGNORE_UNUSED: RegExp[] = [ /.*-generated$/, diff --git a/front/src/applications/operationalStudies/__tests__/sampleData.ts b/front/src/applications/operationalStudies/__tests__/sampleData.ts index c7b890678cc..584006fd0b9 100644 --- a/front/src/applications/operationalStudies/__tests__/sampleData.ts +++ b/front/src/applications/operationalStudies/__tests__/sampleData.ts @@ -7,7 +7,11 @@ import type { ElectrificationValue, PositionData, } from 'applications/operationalStudies/types'; -import type { SimulationSummaryResult, TrainScheduleResult } from 'common/api/osrdEditoastApi'; +import type { + PathProperties, + SimulationSummaryResult, + TrainScheduleResult, +} from 'common/api/osrdEditoastApi'; export const pathLength = 4000; @@ -551,3 +555,136 @@ export const trainSummaryHonored: Extract = [ + { + id: 'West_station', + part: { + track: 'TA1', + position: 500, + }, + extensions: { + identifier: { + name: 'West_station', + uic: 2, + }, + }, + position: 0, + }, + { + id: 'Mid_West_station', + part: { + track: 'TC1', + position: 550, + }, + extensions: { + identifier: { + name: 'Mid_West_station', + uic: 3, + }, + }, + position: 12050000, + }, + { + id: 'Mid_East_station', + part: { + track: 'TD0', + position: 14000, + }, + extensions: { + identifier: { + name: 'Mid_East_station', + uic: 4, + }, + }, + position: 26500000, + }, +]; + +export const pathInputWithWaypointsByMapOnly: TrainScheduleResult['path'] = [ + { + id: '1', + offset: 6481000, + track: 'TA6', + }, + { + id: '2', + offset: 4733000, + track: 'TA6', + }, +]; + +export const pathInputsEndingWithTwoWaypointsByMap: TrainScheduleResult['path'] = [ + { + id: '1', + offset: 6481000, + track: 'TA6', + }, + { + id: '2', + offset: 679000, + track: 'TC0', + }, + { + id: '3', + offset: 883000, + track: 'TC0', + }, +]; + +export const sampleWithOneOperationalPoint: NonNullable = [ + { + id: 'Mid_West_station', + part: { + track: 'TC0', + position: 550, + }, + extensions: { + identifier: { + name: 'Mid_West_station', + uic: 3, + }, + }, + position: 4069000, + }, +]; diff --git a/front/src/applications/operationalStudies/__tests__/upsertMapWaypointsInOperationalPoints.spec.ts b/front/src/applications/operationalStudies/__tests__/upsertMapWaypointsInOperationalPoints.spec.ts new file mode 100644 index 00000000000..2ac8399f18b --- /dev/null +++ b/front/src/applications/operationalStudies/__tests__/upsertMapWaypointsInOperationalPoints.spec.ts @@ -0,0 +1,253 @@ +import type { TFunction } from 'i18next'; + +import { + pathInputsEndingWithTwoWaypointsByMap, + pathInputsWithNoMapWaypoint, + pathInputsWithOneMapWaypoint, + pathInputWithWaypointsByMapOnly, + sampleWithMultipleOperationalPoints, + sampleWithOneOperationalPoint, +} from './sampleData'; +import { upsertMapWaypointsInOperationalPoints } from '../helpers/upsertMapWaypointsInOperationalPoints'; + +const tMock = ((key: string) => key) as TFunction; + +describe('upsertMapWaypointsInOperationalPoints', () => { + it('should add waypoints at the good position in a path with operational points', () => { + const pathItemPositions = [0, 9246000, 26500000]; + + const operationalPointsWithAllWaypoints = upsertMapWaypointsInOperationalPoints( + pathInputsWithOneMapWaypoint, + pathItemPositions, + sampleWithMultipleOperationalPoints, + tMock + ); + + expect(operationalPointsWithAllWaypoints).toEqual([ + { + id: 'West_station', + part: { + track: 'TA1', + position: 500, + }, + extensions: { + identifier: { + name: 'West_station', + uic: 2, + }, + }, + position: 0, + }, + { + id: '2', + extensions: { + identifier: { + name: 'requestedPoint', + uic: 0, + }, + }, + part: { + track: 'TA6', + position: 7746000, + }, + position: 9246000, + }, + { + id: 'Mid_West_station', + part: { + track: 'TC1', + position: 550, + }, + extensions: { + identifier: { + name: 'Mid_West_station', + uic: 3, + }, + }, + position: 12050000, + }, + { + id: 'Mid_East_station', + part: { + track: 'TD0', + position: 14000, + }, + extensions: { + identifier: { + name: 'Mid_East_station', + uic: 4, + }, + }, + position: 26500000, + }, + ]); + }); + + it('should add waypoints properly even when the last two come from map clicks', () => { + const pathItemPositions = [0, 4198000, 4402000]; + + const operationalPointsWithAllWaypoints = upsertMapWaypointsInOperationalPoints( + pathInputsEndingWithTwoWaypointsByMap, + pathItemPositions, + sampleWithOneOperationalPoint, + tMock + ); + + expect(operationalPointsWithAllWaypoints).toEqual([ + { + id: '1', + extensions: { + identifier: { + name: 'requestedPoint', + uic: 0, + }, + }, + part: { + track: 'TA6', + position: 6481000, + }, + position: 0, + }, + { + id: 'Mid_West_station', + part: { + track: 'TC0', + position: 550, + }, + extensions: { + identifier: { + name: 'Mid_West_station', + uic: 3, + }, + }, + position: 4069000, + }, + { + id: '2', + extensions: { + identifier: { + name: 'requestedPoint', + uic: 0, + }, + }, + part: { + track: 'TC0', + position: 679000, + }, + position: 4198000, + }, + { + id: '3', + extensions: { + identifier: { + name: 'requestedPoint', + uic: 0, + }, + }, + part: { + track: 'TC0', + position: 883000, + }, + position: 4402000, + }, + ]); + }); + + it('should add waypoints properly when there is no op on path', () => { + const pathItemPositions = [0, 1748000]; + + const operationalPointsWithAllWaypoints = upsertMapWaypointsInOperationalPoints( + pathInputWithWaypointsByMapOnly, + pathItemPositions, + [], + tMock + ); + + expect(operationalPointsWithAllWaypoints).toEqual([ + { + id: '1', + extensions: { + identifier: { + name: 'requestedPoint', + uic: 0, + }, + }, + part: { + track: 'TA6', + position: 6481000, + }, + position: 0, + }, + { + id: '2', + extensions: { + identifier: { + name: 'requestedPoint', + uic: 0, + }, + }, + part: { + track: 'TA6', + position: 4733000, + }, + position: 1748000, + }, + ]); + }); + + it('should return the same array if there is no waypoints added by map click', () => { + const pathItemPositions = [0, 12050000, 26500000]; + + const operationalPointsWithAllWaypoints = upsertMapWaypointsInOperationalPoints( + pathInputsWithNoMapWaypoint, + pathItemPositions, + sampleWithMultipleOperationalPoints, + tMock + ); + + expect(operationalPointsWithAllWaypoints).toEqual([ + { + id: 'West_station', + part: { + track: 'TA1', + position: 500, + }, + extensions: { + identifier: { + name: 'West_station', + uic: 2, + }, + }, + position: 0, + }, + { + id: 'Mid_West_station', + part: { + track: 'TC1', + position: 550, + }, + extensions: { + identifier: { + name: 'Mid_West_station', + uic: 3, + }, + }, + position: 12050000, + }, + { + id: 'Mid_East_station', + part: { + track: 'TD0', + position: 14000, + }, + extensions: { + identifier: { + name: 'Mid_East_station', + uic: 4, + }, + }, + position: 26500000, + }, + ]); + }); +}); diff --git a/front/src/applications/operationalStudies/helpers/upsertMapWaypointsInOperationalPoints.ts b/front/src/applications/operationalStudies/helpers/upsertMapWaypointsInOperationalPoints.ts new file mode 100644 index 00000000000..57a0088cee1 --- /dev/null +++ b/front/src/applications/operationalStudies/helpers/upsertMapWaypointsInOperationalPoints.ts @@ -0,0 +1,55 @@ +/* eslint-disable import/prefer-default-export */ +import type { TFunction } from 'i18next'; + +import type { + PathfindingResultSuccess, + PathProperties, + TrainScheduleResult, +} from 'common/api/osrdEditoastApi'; + +/** + * Check if the train path used waypoints added by map click and add them to the operational points + */ +export const upsertMapWaypointsInOperationalPoints = ( + path: TrainScheduleResult['path'], + pathItemsPositions: PathfindingResultSuccess['path_item_positions'], + operationalPoints: NonNullable, + t: TFunction +): NonNullable => { + let waypointCounter = 1; + + return path.reduce( + (operationalPointsWithAllWaypoints, step, i) => { + if (!('track' in step)) return operationalPointsWithAllWaypoints; + + const positionOnPath = pathItemsPositions[i]; + const indexToInsert = operationalPointsWithAllWaypoints.findIndex( + (op) => op.position >= positionOnPath + ); + + const formattedStep: NonNullable[number] = { + id: step.id, + extensions: { + identifier: { + name: t('requestedPoint', { count: waypointCounter }), + uic: 0, + }, + }, + part: { track: step.track, position: step.offset }, + position: positionOnPath, + }; + + waypointCounter += 1; + + // If we can't find any op position greater than the current step position, we add it at the end + if (indexToInsert === -1) { + operationalPointsWithAllWaypoints.push(formattedStep); + } else { + operationalPointsWithAllWaypoints.splice(indexToInsert, 0, formattedStep); + } + + return operationalPointsWithAllWaypoints; + }, + [...operationalPoints] + ); +}; diff --git a/front/src/applications/operationalStudies/utils.ts b/front/src/applications/operationalStudies/utils.ts index 518c74ead93..6fe54af18eb 100644 --- a/front/src/applications/operationalStudies/utils.ts +++ b/front/src/applications/operationalStudies/utils.ts @@ -1,6 +1,8 @@ +import type { TFunction } from 'i18next'; import type { Dictionary } from 'lodash'; import type { + PathfindingResultSuccess, PathProperties, SimulationSummaryResult, TrainScheduleResult, @@ -12,6 +14,7 @@ import { mmToM, sToMs } from 'utils/physics'; import { SMALL_INPUT_MAX_LENGTH } from 'utils/strings'; import { ISO8601Duration2sec, ms2sec } from 'utils/timeManipulation'; +import { upsertMapWaypointsInOperationalPoints } from './helpers/upsertMapWaypointsInOperationalPoints'; import type { BoundariesData, ElectricalBoundariesData, @@ -161,29 +164,28 @@ export const formatElectrificationRanges = ( export const preparePathPropertiesData = ( electricalProfiles: SimulationResponseSuccess['electrical_profiles'], { slopes, curves, electrifications, operational_points, geometry }: PathProperties, - pathLength: number + { path_item_positions, length }: PathfindingResultSuccess, + trainSchedulePath: TrainScheduleResult['path'], + t: TFunction ): PathPropertiesFormatted => { const formattedSlopes = transformBoundariesDataToPositionDataArray( slopes as NonNullable, - pathLength, + length, 'gradient' ); const formattedCurves = transformBoundariesDataToPositionDataArray( curves as NonNullable, - pathLength, + length, 'radius' ); const electrificationsRanges = transformBoundariesDataToRangesData( electrifications as NonNullable, - pathLength + length ); - const electricalProfilesRanges = transformBoundariesDataToRangesData( - electricalProfiles, - pathLength - ); + const electricalProfilesRanges = transformBoundariesDataToRangesData(electricalProfiles, length); const electrificationRanges = formatElectrificationRanges( electrificationsRanges, @@ -192,14 +194,21 @@ export const preparePathPropertiesData = ( const voltageRanges = getPathVoltages( electrifications as NonNullable, - pathLength + length + ); + + const operationalPointsWithAllWaypoints = upsertMapWaypointsInOperationalPoints( + trainSchedulePath, + path_item_positions, + operational_points!, + t ); return { electrifications: electrificationRanges, curves: formattedCurves, slopes: formattedSlopes, - operationalPoints: operational_points as NonNullable, + operationalPoints: operationalPointsWithAllWaypoints, geometry: geometry as NonNullable, voltages: voltageRanges, }; diff --git a/front/src/modules/simulationResult/components/SpaceTimeChart/useGetProjectedTrainOperationalPoints.ts b/front/src/modules/simulationResult/components/SpaceTimeChart/useGetProjectedTrainOperationalPoints.ts index 00ed42b86d6..e1997f37b1f 100644 --- a/front/src/modules/simulationResult/components/SpaceTimeChart/useGetProjectedTrainOperationalPoints.ts +++ b/front/src/modules/simulationResult/components/SpaceTimeChart/useGetProjectedTrainOperationalPoints.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { upsertMapWaypointsInOperationalPoints } from 'applications/operationalStudies/helpers/upsertMapWaypointsInOperationalPoints'; import { STDCM_TRAIN_ID } from 'applications/stdcm/consts'; import { osrdEditoastApi, @@ -43,42 +44,12 @@ const useGetProjectedTrainOperationalPoints = ( }, }).unwrap(); - const operationalPointsWithAllWaypoints = [...operational_points!]; - - // Check if there are vias added by map click and insert them in the operational points - let waypointCounter = 1; - - trainScheduleUsedForProjection.path.forEach((step, i) => { - if (!('track' in step)) return; - - const positionOnPath = pathfindingResult.path_item_positions[i]; - const indexToInsert = operationalPointsWithAllWaypoints.findIndex( - (op) => op.position >= positionOnPath - ); - - const formattedStep: NonNullable[number] = { - id: step.id, - extensions: { - identifier: { - name: t('requestedPoint', { count: waypointCounter }), - uic: 0, - }, - }, - part: { track: step.track, position: step.offset }, - position: positionOnPath, - }; - - waypointCounter += 1; - - // If we can't find any op position greater than the current step position, we add it at the end - // (happens if the last two steps are added by map click or if there isn't any op on the path) - if (indexToInsert === -1) { - operationalPointsWithAllWaypoints.push(formattedStep); - return; - } - - operationalPointsWithAllWaypoints.splice(indexToInsert, 0, formattedStep); - }); + const operationalPointsWithAllWaypoints = upsertMapWaypointsInOperationalPoints( + trainScheduleUsedForProjection.path, + pathfindingResult.path_item_positions, + operational_points!, + t + ); setOperationalPoints(operationalPointsWithAllWaypoints); } diff --git a/front/src/modules/simulationResult/components/SpeedSpaceChart/helpers.ts b/front/src/modules/simulationResult/components/SpeedSpaceChart/helpers.ts index 1a57ed2fd2f..828fca546b8 100644 --- a/front/src/modules/simulationResult/components/SpeedSpaceChart/helpers.ts +++ b/front/src/modules/simulationResult/components/SpeedSpaceChart/helpers.ts @@ -100,8 +100,9 @@ export const formatStops = (operationalPoints: PathPropertiesFormatted['operatio position: { start: mmToKm(position), }, - value: - identifier && sncf ? `${identifier.name} ${sncf.ch !== ('' || '00') ? sncf.ch : ''}` : '', + value: identifier + ? `${identifier.name} ${sncf?.ch && sncf.ch !== ('' || '00') ? sncf.ch : ''}` + : '', })); export const formatElectrifications = ( diff --git a/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts b/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts index d5ea9eccf37..b0f7df2acfd 100644 --- a/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts +++ b/front/src/modules/simulationResult/components/SpeedSpaceChart/useSpeedSpaceChart.ts @@ -4,6 +4,7 @@ import type { LayerData, PowerRestrictionValues, } from '@osrd-project/ui-speedspacechart/dist/types/chartTypes'; +import { useTranslation } from 'react-i18next'; import type { PathPropertiesFormatted } from 'applications/operationalStudies/types'; import { preparePathPropertiesData } from 'applications/operationalStudies/utils'; @@ -26,6 +27,7 @@ const useSpeedSpaceChart = ( simulation?: SimulationResponse, departureTime?: string ) => { + const { t } = useTranslation('simulation'); const infraId = useInfraID(); const [formattedPathProperties, setFormattedPathProperties] = useState(); @@ -63,8 +65,11 @@ const useSpeedSpaceChart = ( const formattedPathProps = preparePathPropertiesData( simulation.electrical_profiles, pathProperties, - pathfindingResult.length + pathfindingResult, + trainScheduleResult.path, + t ); + setFormattedPathProperties(formattedPathProps); // Format power restrictions @@ -79,7 +84,7 @@ const useSpeedSpaceChart = ( }; getPathProperties(); - }, [pathProperties, simulation, infraId, trainScheduleResult, rollingStock]); + }, [pathProperties, infraId, rollingStock]); // setup chart synchronizer useEffect(() => {