From 625d185a39985f09d26676c54bb7a2cf9b707071 Mon Sep 17 00:00:00 2001 From: SharglutDev Date: Wed, 22 May 2024 22:11:25 +0200 Subject: [PATCH] front: adapt simulation results for trainschedule v2 --- .../__tests__/sampleData.ts | 556 ++++++++++++++++++ .../__tests__/utils.spec.ts | 111 ++++ .../applications/operationalStudies/consts.ts | 80 +-- .../applications/operationalStudies/hooks.ts | 207 ++++++- .../applications/operationalStudies/types.ts | 72 ++- .../applications/operationalStudies/utils.ts | 303 ++++++++++ .../views/v2/ScenarioV2.tsx | 37 +- .../views/v2/SimulationResultsV2.tsx | 489 ++++++++------- .../views/v2/getSimulationResultsV2.ts | 53 +- .../helpers/powerRestrictionSelector.ts | 29 +- .../__tests__/powerRestrictions.spec.ts | 16 + .../rollingStock/helpers/powerRestrictions.ts | 25 + .../components/ChartHelpers/ChartHelpers.ts | 47 +- .../ChartHelpers/ChartSynchronizerV2.ts | 106 ++++ .../components/ChartHelpers/drawArea.ts | 3 +- .../components/ChartHelpers/drawCurve.ts | 7 +- .../ChartHelpers/drawElectricalProfile.ts | 234 ++++++++ .../components/ChartHelpers/drawHelpers.ts | 8 +- .../ChartHelpers/drawPowerRestriction.ts | 3 +- .../components/ChartHelpers/drawRect.ts | 1 + .../ChartHelpers/enableInteractivity.tsx | 131 ++++- .../ChartHelpers/getScaleDomainFromValues.ts | 8 + .../components/SimulationResultsMapV2.tsx | 388 ++++++++++++ .../components/SpaceCurvesSlopes.tsx | 56 +- .../SpaceCurvesSlopes/SpaceCurvesSlopesV2.tsx | 275 +++++++++ .../components/SpaceCurvesSlopes/utils.ts | 59 ++ .../SpaceTimeChart/SpaceTimeChartV2.tsx | 352 +++++++++++ .../components/SpaceTimeChart/createTrain.ts | 47 ++ .../components/SpaceTimeChart/d3Helpers.ts | 88 ++- .../components/SpaceTimeChart/drawTrain.ts | 39 +- .../components/SpaceTimeChart/hooks.ts | 54 ++ .../useStoreDataForSpaceTimeChart.ts | 23 + .../SpeedSpaceChart/SpeedSpaceChart.tsx | 4 +- .../SpeedSpaceChart/SpeedSpaceChartV2.tsx | 333 +++++++++++ .../SpeedSpaceChart/SpeedSpaceSettings.tsx | 19 +- .../components/SpeedSpaceChart/consts.ts | 26 + .../components/SpeedSpaceChart/d3Helpers.ts | 263 +++++++-- .../components/SpeedSpaceChart/prepareData.ts | 116 +++- .../components/SpeedSpaceChart/types.ts | 132 +++++ .../components/SpeedSpaceChart/utils.ts | 141 ++++- .../components/TimeButtons.tsx | 23 +- .../components/TrainDetailsV2.tsx | 115 ++++ .../simulationResult/components/sampleData.ts | 1 + front/src/modules/simulationResult/types.ts | 15 + .../DriverTrainSchedule.tsx | 2 +- .../DriverTrainScheduleHeader.tsx | 4 +- .../DriverTrainScheduleHelpers.ts | 136 ----- .../DriverTrainScheduleStop.tsx | 2 +- .../DriverTrainScheduleStopList.tsx | 2 +- .../DriverTrainScheduleTypes.tsx | 6 - .../components/DriverTrainSchedule/consts.ts | 17 + .../driverTrainScheduleExportCSV.ts | 2 +- .../DriverTrainScheduleHeaderV2.tsx | 126 ++++ .../DriverTrainScheduleStopListV2.tsx | 97 +++ .../DriverTrainScheduleStopV2.tsx | 114 ++++ .../DriverTrainScheduleV2.tsx | 88 +++ .../exportDriverScheduleCSV.ts | 245 ++++++++ .../components/DriverTrainScheduleV2/types.ts | 13 + .../components/DriverTrainScheduleV2/utils.ts | 271 +++++++++ .../AddTrainScheduleV2Button.tsx | 10 +- .../TimetableManageTrainScheduleV2.tsx | 2 +- .../TimetableV2/TimetableTrainCardV2.tsx | 2 +- front/src/utils/date.ts | 9 +- front/src/utils/physics.ts | 5 + front/src/utils/timeManipulation.ts | 8 +- 65 files changed, 5534 insertions(+), 722 deletions(-) create mode 100644 front/src/applications/operationalStudies/__tests__/sampleData.ts create mode 100644 front/src/applications/operationalStudies/__tests__/utils.spec.ts create mode 100644 front/src/applications/operationalStudies/utils.ts create mode 100644 front/src/modules/rollingStock/helpers/__tests__/powerRestrictions.spec.ts create mode 100644 front/src/modules/rollingStock/helpers/powerRestrictions.ts create mode 100644 front/src/modules/simulationResult/components/ChartHelpers/ChartSynchronizerV2.ts create mode 100644 front/src/modules/simulationResult/components/SimulationResultsMapV2.tsx create mode 100644 front/src/modules/simulationResult/components/SpaceCurvesSlopes/SpaceCurvesSlopesV2.tsx create mode 100644 front/src/modules/simulationResult/components/SpaceCurvesSlopes/utils.ts create mode 100644 front/src/modules/simulationResult/components/SpaceTimeChart/SpaceTimeChartV2.tsx create mode 100644 front/src/modules/simulationResult/components/SpaceTimeChart/hooks.ts create mode 100644 front/src/modules/simulationResult/components/SpeedSpaceChart/SpeedSpaceChartV2.tsx create mode 100644 front/src/modules/simulationResult/components/SpeedSpaceChart/consts.ts create mode 100644 front/src/modules/simulationResult/components/SpeedSpaceChart/types.ts create mode 100644 front/src/modules/simulationResult/components/TrainDetailsV2.tsx delete mode 100644 front/src/modules/trainschedule/components/DriverTrainSchedule/DriverTrainScheduleHelpers.ts delete mode 100644 front/src/modules/trainschedule/components/DriverTrainSchedule/DriverTrainScheduleTypes.tsx create mode 100644 front/src/modules/trainschedule/components/DriverTrainSchedule/consts.ts create mode 100644 front/src/modules/trainschedule/components/DriverTrainScheduleV2/DriverTrainScheduleHeaderV2.tsx create mode 100644 front/src/modules/trainschedule/components/DriverTrainScheduleV2/DriverTrainScheduleStopListV2.tsx create mode 100644 front/src/modules/trainschedule/components/DriverTrainScheduleV2/DriverTrainScheduleStopV2.tsx create mode 100644 front/src/modules/trainschedule/components/DriverTrainScheduleV2/DriverTrainScheduleV2.tsx create mode 100644 front/src/modules/trainschedule/components/DriverTrainScheduleV2/exportDriverScheduleCSV.ts create mode 100644 front/src/modules/trainschedule/components/DriverTrainScheduleV2/types.ts create mode 100644 front/src/modules/trainschedule/components/DriverTrainScheduleV2/utils.ts diff --git a/front/src/applications/operationalStudies/__tests__/sampleData.ts b/front/src/applications/operationalStudies/__tests__/sampleData.ts new file mode 100644 index 00000000000..2600b4a820f --- /dev/null +++ b/front/src/applications/operationalStudies/__tests__/sampleData.ts @@ -0,0 +1,556 @@ +import type { + BoundariesData, + ElectricalBoundariesData, + ElectricalProfileValue, + ElectricalRangesData, + ElectrificationRangeV2, + ElectrificationValue, + PositionData, +} from 'applications/operationalStudies/types'; +import type { + EffortCurves, + SimulationPowerRestrictionRange, + TrainScheduleBase, +} from 'common/api/osrdEditoastApi'; + +export const pathLength = 4000; + +/** + * Data for transformBoundariesDataToPositionDataArray + */ + +export const boundariesDataWithNumber: BoundariesData = { + boundaries: [1000, 2000, 3000], + values: [1, 2, 3, 4], +}; + +export const getExpectedResultDataNumber = ( + value: T +): PositionData[] => + [ + { position: 0, [value]: 0 }, + { position: 1, [value]: 1 }, + { position: 2, [value]: 2 }, + { position: 3, [value]: 3 }, + { position: 4, [value]: 4 }, + ] as PositionData[]; + +/** + * Data for transformBoundariesDataToRangesData + */ + +export const boundariesDataWithElectrification: ElectricalBoundariesData = { + boundaries: [1000, 2000, 3000], + values: [ + { + type: 'electrification', + voltage: '1500V', + }, + { + lower_pantograph: true, + type: 'neutral_section', + }, + { + type: 'non_electrified', + }, + { + type: 'electrification', + voltage: '25000V', + }, + ], +}; + +export const boundariesDataWithElectrificalProfile: ElectricalBoundariesData = + { + boundaries: [1000, 2000, 3000], + values: [ + { + electrical_profile_type: 'profile', + profile: 'O', + handled: true, + }, + { + electrical_profile_type: 'no_profile', + }, + { + electrical_profile_type: 'no_profile', + }, + { + electrical_profile_type: 'profile', + profile: '25000V', + handled: false, + }, + ], + }; + +/** + * Data for formatElectrificationRanges + */ + +export const electrificationRangesData: ElectricalRangesData[] = [ + { + start: 0, + stop: 1, + values: { + type: 'electrification', + voltage: '1500V', + }, + }, + { + start: 1, + stop: 2, + values: { + lower_pantograph: true, + type: 'neutral_section', + }, + }, + { + start: 2, + stop: 3, + values: { + type: 'non_electrified', + }, + }, + { + start: 3, + stop: 4, + values: { + type: 'electrification', + voltage: '25000V', + }, + }, +]; + +export const electrificationRangesDataLarge: ElectricalRangesData[] = [ + { + start: 0, + stop: 1, + values: { + type: 'electrification', + voltage: '1500V', + }, + }, + { + start: 1, + stop: 2, + values: { + lower_pantograph: true, + type: 'neutral_section', + }, + }, + { + start: 2, + stop: 3, + values: { + lower_pantograph: false, + type: 'neutral_section', + }, + }, + { + start: 3, + stop: 4, + values: { + type: 'non_electrified', + }, + }, + { + start: 4, + stop: 5, + values: { + type: 'electrification', + voltage: '25000V', + }, + }, + { + start: 5, + stop: 6, + values: { + type: 'electrification', + voltage: '1500V', + }, + }, +]; + +export const electricalProfileRangesData: ElectricalRangesData[] = [ + { + start: 0, + stop: 1, + values: { + electrical_profile_type: 'profile', + profile: 'O', + handled: true, + }, + }, + { + start: 1, + stop: 2, + values: { + electrical_profile_type: 'no_profile', + }, + }, + { + start: 2, + stop: 3, + values: { + electrical_profile_type: 'no_profile', + }, + }, + { + start: 3, + stop: 4, + values: { + electrical_profile_type: 'profile', + profile: '25000V', + handled: false, + }, + }, +]; + +export const electricalProfileRangesDataShort: ElectricalRangesData[] = [ + { + start: 0, + stop: 1, + values: { + electrical_profile_type: 'profile', + profile: 'O', + handled: true, + }, + }, + { + start: 1, + stop: 4, + values: { + electrical_profile_type: 'no_profile', + }, + }, + { + start: 4, + stop: 5, + values: { + electrical_profile_type: 'profile', + profile: '25000V', + handled: false, + }, + }, + { + start: 5, + stop: 6, + values: { + electrical_profile_type: 'profile', + profile: 'A1', + handled: true, + }, + }, +]; + +export const electrificationRanges: ElectrificationRangeV2[] = [ + { + start: 0, + stop: 1, + electrificationUsage: { + type: 'electrification', + voltage: '1500V', + electrical_profile_type: 'profile', + profile: 'O', + handled: true, + }, + }, + { + start: 1, + stop: 2, + electrificationUsage: { + lower_pantograph: true, + type: 'neutral_section', + electrical_profile_type: 'no_profile', + }, + }, + { + start: 2, + stop: 3, + electrificationUsage: { + type: 'non_electrified', + electrical_profile_type: 'no_profile', + }, + }, + { + start: 3, + stop: 4, + electrificationUsage: { + type: 'electrification', + voltage: '25000V', + electrical_profile_type: 'profile', + profile: '25000V', + handled: false, + }, + }, +]; + +export const electrificationRangesLarge: ElectrificationRangeV2[] = [ + { + start: 0, + stop: 1, + electrificationUsage: { + type: 'electrification', + voltage: '1500V', + electrical_profile_type: 'profile', + profile: 'O', + handled: true, + }, + }, + { + start: 1, + stop: 2, + electrificationUsage: { + lower_pantograph: true, + type: 'neutral_section', + electrical_profile_type: 'no_profile', + }, + }, + { + start: 2, + stop: 3, + electrificationUsage: { + lower_pantograph: false, + type: 'neutral_section', + electrical_profile_type: 'no_profile', + }, + }, + { + start: 3, + stop: 4, + electrificationUsage: { + type: 'non_electrified', + electrical_profile_type: 'no_profile', + }, + }, + { + start: 4, + stop: 5, + electrificationUsage: { + type: 'electrification', + voltage: '25000V', + electrical_profile_type: 'profile', + profile: '25000V', + handled: false, + }, + }, + { + start: 5, + stop: 6, + electrificationUsage: { + type: 'electrification', + voltage: '1500V', + electrical_profile_type: 'profile', + profile: 'A1', + handled: true, + }, + }, +]; + +/** + * Data for getRollingStockPowerRestrictionsByMode + */ +export const effortCurves: EffortCurves['modes'] = { + '1500V': { + curves: [ + { + cond: { + comfort: 'STANDARD', + electrical_profile_level: 'level1', + power_restriction_code: 'code1', + }, + curve: { + max_efforts: [100, 200, 300], + speeds: [50, 100, 150], + }, + }, + { + cond: { + comfort: 'STANDARD', + electrical_profile_level: 'level1', + power_restriction_code: 'code2', + }, + curve: { + max_efforts: [100, 200, 300], + speeds: [50, 100, 150], + }, + }, + { + cond: { + comfort: 'AC', + electrical_profile_level: 'level1', + power_restriction_code: 'code2', + }, + curve: { + max_efforts: [100, 200, 300], + speeds: [50, 100, 150], + }, + }, + ], + default_curve: { + max_efforts: [100, 200, 300], + speeds: [50, 100, 150], + }, + is_electric: true, + }, + '25000V': { + curves: [ + { + cond: { + comfort: 'AC', + electrical_profile_level: 'level2', + power_restriction_code: 'code3', + }, + curve: { + max_efforts: [400, 500, 600], + speeds: [200, 250, 300], + }, + }, + { + cond: { + comfort: 'AC', + electrical_profile_level: 'level2', + power_restriction_code: 'code4', + }, + curve: { + max_efforts: [400, 500, 600], + speeds: [200, 250, 300], + }, + }, + ], + + default_curve: { + max_efforts: [400, 500, 600], + speeds: [200, 250, 300], + }, + is_electric: false, + }, +}; + +/** + * Data for formatPowerRestrictionRanges + */ + +export const powerRestriction: NonNullable = [ + { + from: 'step1', + to: 'step2', + value: 'code1', + }, + { + from: 'step3', + to: 'step4', + value: 'code2', + }, +]; + +export const stepPath: TrainScheduleBase['path'] = [ + { + uic: 12345, + id: 'step1', + }, + { + uic: 45686, + id: 'step2', + }, + { + uic: 93405, + id: 'step3', + }, + { + uic: 93405, + id: 'step4', + }, +]; + +export const stepPathPositions = [0, 1000, 2000, 3000]; + +export const formattedPowerRestrictionRanges: Omit[] = [ + { + start: 0, + stop: 1, + code: 'code1', + }, + { + start: 2, + stop: 3, + code: 'code2', + }, +]; + +/** + * Data for formatPowerRestrictionRangesWithHandled + */ + +export const powerRestrictionRanges: Omit[] = [ + { + start: 0, + stop: 1, + code: 'code1', + }, + { + start: 2, + stop: 3, + code: 'code2', + }, + { + start: 3, + stop: 4, + code: 'code1', + }, +]; + +export const electrificationRangesForPowerRestrictions: ElectrificationRangeV2[] = [ + { + start: 0, + stop: 2, + electrificationUsage: { + type: 'electrification', + voltage: '1500V', + electrical_profile_type: 'profile', + profile: 'O', + handled: true, + }, + }, + { + start: 2, + stop: 3, + electrificationUsage: { + lower_pantograph: true, + type: 'neutral_section', + electrical_profile_type: 'no_profile', + }, + }, + { + start: 3, + stop: 4, + electrificationUsage: { + type: 'electrification', + voltage: '25000V', + electrical_profile_type: 'profile', + profile: '25000V', + handled: true, + }, + }, +]; + +export const powerRestrictionRangesWithHandled: SimulationPowerRestrictionRange[] = [ + { + start: 0, + stop: 1, + code: 'code1', + handled: true, + }, + { + start: 2, + stop: 3, + code: 'code2', + handled: false, + }, + { + start: 3, + stop: 4, + code: 'code1', + handled: false, + }, +]; diff --git a/front/src/applications/operationalStudies/__tests__/utils.spec.ts b/front/src/applications/operationalStudies/__tests__/utils.spec.ts new file mode 100644 index 00000000000..947bf727de2 --- /dev/null +++ b/front/src/applications/operationalStudies/__tests__/utils.spec.ts @@ -0,0 +1,111 @@ +import { + formatElectrificationRanges, + formatPowerRestrictionRanges, + formatPowerRestrictionRangesWithHandled, + transformBoundariesDataToPositionDataArray, + transformBoundariesDataToRangesData, +} from 'applications/operationalStudies/utils'; + +import { + boundariesDataWithElectrificalProfile, + boundariesDataWithElectrification, + boundariesDataWithNumber, + effortCurves, + electricalProfileRangesData, + electricalProfileRangesDataShort, + electrificationRanges, + electrificationRangesData, + electrificationRangesDataLarge, + electrificationRangesForPowerRestrictions, + electrificationRangesLarge, + formattedPowerRestrictionRanges, + getExpectedResultDataNumber, + pathLength, + powerRestriction, + powerRestrictionRanges, + powerRestrictionRangesWithHandled, + stepPath, + stepPathPositions, +} from './sampleData'; + +describe('transformBoundariesDataToPositionDataArray', () => { + it('should transform boundaries data to position data array for gradient', () => { + const result = transformBoundariesDataToPositionDataArray( + boundariesDataWithNumber, + pathLength, + 'gradient' + ); + + expect(result).toEqual(getExpectedResultDataNumber('gradient')); + }); + + it('should transform boundaries data to position data array for radius', () => { + const result = transformBoundariesDataToPositionDataArray( + boundariesDataWithNumber, + pathLength, + 'radius' + ); + + expect(result).toEqual(getExpectedResultDataNumber('radius')); + }); +}); + +describe('transformBoundariesDataToRangesData', () => { + it('should transform boundaries data to ranges data for electrification', () => { + const result = transformBoundariesDataToRangesData( + boundariesDataWithElectrification, + pathLength + ); + + expect(result).toEqual(electrificationRangesData); + }); + + it('should transform boundaries data to ranges data for electrical profile', () => { + const result = transformBoundariesDataToRangesData( + boundariesDataWithElectrificalProfile, + pathLength + ); + + expect(result).toEqual(electricalProfileRangesData); + }); +}); + +describe('formatElectrificationRanges', () => { + it('should properly format electrification ranges if both parameters have same length', () => { + const result = formatElectrificationRanges( + electrificationRangesData, + electricalProfileRangesData + ); + + expect(result).toEqual(electrificationRanges); + }); + + it('should properly format electrification ranges if electrification is longer than electrical profiles', () => { + const result = formatElectrificationRanges( + electrificationRangesDataLarge, + electricalProfileRangesDataShort + ); + + expect(result).toEqual(electrificationRangesLarge); + }); +}); + +describe('formatPowerRestrictionRanges', () => { + it('should properly format power restrictions ranges', () => { + const result = formatPowerRestrictionRanges(powerRestriction, stepPath, stepPathPositions); + + expect(result).toEqual(formattedPowerRestrictionRanges); + }); +}); + +describe('formatPowerRestrictionRangesWithHandled', () => { + it('should properly format power restrictions ranges with handled property', () => { + const result = formatPowerRestrictionRangesWithHandled( + powerRestrictionRanges, + electrificationRangesForPowerRestrictions, + effortCurves + ); + + expect(result).toEqual(powerRestrictionRangesWithHandled); + }); +}); diff --git a/front/src/applications/operationalStudies/consts.ts b/front/src/applications/operationalStudies/consts.ts index 4eebfc4f3df..299d9127e37 100644 --- a/front/src/applications/operationalStudies/consts.ts +++ b/front/src/applications/operationalStudies/consts.ts @@ -1,12 +1,9 @@ import type { Position } from 'geojson'; -import type { - ElectrificationRange, - ElectrificationUsage, - SimulationPowerRestrictionRange, -} from 'common/api/osrdEditoastApi'; +import type { ElectrificationRange, ElectrificationUsage } from 'common/api/osrdEditoastApi'; import type { LinearMetadataItem } from 'common/IntervalsDataViz/types'; import i18n from 'i18n'; +import type { Mode } from 'modules/simulationResult/components/SpeedSpaceChart/types'; import type { HeightPosition } from 'reducers/osrdsimulation/types'; export const BLOCKTYPES = [ @@ -140,6 +137,7 @@ interface Profile { export const DRAWING_KEYS: (keyof HeightPosition)[] = ['position', 'height']; export type DrawingKeys = typeof DRAWING_KEYS; +// TODO DROP V1: remove this export interface ElectricalConditionSegment { position_start: number; position_end: number; @@ -160,32 +158,7 @@ export interface ElectricalConditionSegment { isIncompatiblePowerRestriction: boolean; } -interface AC { - '25000V': string; - '22500V': string; - '20000V': string; -} -interface DC { - O: string; - A: string; - A1: string; - B: string; - B1: string; - C: string; - D: string; - E: string; - F: string; - G: string; -} - -interface Mode { - '25000V': AC | string; - '1500V': DC | string; - thermal: string; - '15000V': string; - '3000V': string; -} - +// TODO DROP V1: remove this const electricalProfileColorsWithProfile: Mode = { '25000V': { '25000V': '#6E1E78', '22500V': '#A453AD', '20000V': '#DD87E5' }, '1500V': { @@ -205,6 +178,7 @@ const electricalProfileColorsWithProfile: Mode = { '3000V': '#1FBE00', }; +// TODO DROP V1: remove this const electricalProfileColorsWithoutProfile: Mode = { '25000V': '#6E1E78', '1500V': '#FF0037', @@ -230,6 +204,7 @@ export const legend: Profile[] = [ }, ]; +// TODO DROP V1: remove this export const createProfileSegment = ( fullElectrificationRange: ElectrificationRange[], electrificationRange: ElectrificationRange @@ -298,46 +273,3 @@ export const createProfileSegment = ( return segment; }; - -export interface PowerRestrictionSegment { - position_start: number; - position_end: number; - position_middle: number; - lastPosition: number; - height_start: number; - height_end: number; - height_middle: number; - seenRestriction: string; - usedRestriction: boolean; - isStriped: boolean; - isRestriction: boolean; - isIncompatiblePowerRestriction: boolean; -} - -export const createPowerRestrictionSegment = ( - fullPowerRestrictionRange: SimulationPowerRestrictionRange[], - powerRestrictionRange: SimulationPowerRestrictionRange -) => { - // figure out if the power restriction is incompatible or missing - const isRestriction = powerRestrictionRange.handled; - const isIncompatiblePowerRestriction = - !!powerRestrictionRange.code && !powerRestrictionRange.handled; - const isStriped = !!powerRestrictionRange.code && !powerRestrictionRange.handled; - - const segment: PowerRestrictionSegment = { - position_start: powerRestrictionRange.start, - position_end: powerRestrictionRange.stop, - position_middle: (powerRestrictionRange.start + powerRestrictionRange.stop) / 2, - lastPosition: fullPowerRestrictionRange.slice(-1)[0].stop, - height_start: 4, - height_end: 24, - height_middle: 14, - seenRestriction: powerRestrictionRange.code || '', - usedRestriction: powerRestrictionRange.handled, - isStriped, - isRestriction, - isIncompatiblePowerRestriction, - }; - - return segment; -}; diff --git a/front/src/applications/operationalStudies/hooks.ts b/front/src/applications/operationalStudies/hooks.ts index 7c474524bf2..7e931830523 100644 --- a/front/src/applications/operationalStudies/hooks.ts +++ b/front/src/applications/operationalStudies/hooks.ts @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; @@ -7,19 +7,33 @@ import { osrdEditoastApi, type PostV2InfraByInfraIdPathPropertiesApiArg, type PostV2InfraByInfraIdPathfindingBlocksApiArg, + type SimulationPowerRestrictionRange, } from 'common/api/osrdEditoastApi'; -import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; +import { useInfraID, useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; import { formatSuggestedOperationalPoints, upsertViasInOPs } from 'modules/pathfinding/utils'; import { getSupportedElectrification, isThermal } from 'modules/rollingStock/helpers/electric'; +import { sec2d3datetime } from 'modules/simulationResult/components/ChartHelpers/ChartHelpers'; +import { + ChartSynchronizerV2, + type ChartSynchronizerTrainData, +} from 'modules/simulationResult/components/ChartHelpers/ChartSynchronizerV2'; import { adjustConfWithTrainToModifyV2 } from 'modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; import { setFailure } from 'reducers/main'; import type { PathStep } from 'reducers/osrdconf/types'; +import { getSelectedTrainId } from 'reducers/osrdsimulation/selectors'; import { useAppDispatch } from 'store'; +import { isoDateToMs } from 'utils/date'; import { castErrorToFailure } from 'utils/error'; import { getPointCoordinates } from 'utils/geometry'; +import { mmToM } from 'utils/physics'; -import type { ManageTrainSchedulePathProperties } from './types'; +import type { ManageTrainSchedulePathProperties, PathPropertiesFormatted } from './types'; +import { + formatPowerRestrictionRanges, + formatPowerRestrictionRangesWithHandled, + preparePathPropertiesData, +} from './utils'; /** * Hook to relaunch the pathfinding when editing a train @@ -131,4 +145,189 @@ export const useSetupItineraryForTrainUpdate = ( }, [trainScheduleIDsToModify]); }; -export default useSetupItineraryForTrainUpdate; +/** + * Prepare datas to be used in simulation results + */ +export const useSimulationResults = () => { + const infraId = useInfraID(); + const selectedTrainId = useSelector(getSelectedTrainId); + + const [pathProperties, setPathProperties] = useState(); + const [formattedPowerRestrictions, setFormattedPowerRestrictions] = useState< + SimulationPowerRestrictionRange[] + >([]); + + const { data: selectedTrainSchedule } = osrdEditoastApi.endpoints.getV2TrainScheduleById.useQuery( + { + id: selectedTrainId as number, + }, + { skip: !selectedTrainId } + ); + + const { data: selectedTrainRollingStock } = + osrdEditoastApi.endpoints.getRollingStockNameByRollingStockName.useQuery( + { + rollingStockName: selectedTrainSchedule?.rolling_stock_name as string, + }, + { skip: !selectedTrainSchedule } + ); + + const { data: pathfindingResult } = osrdEditoastApi.endpoints.getV2TrainScheduleByIdPath.useQuery( + { + id: selectedTrainId as number, + infraId: infraId as number, + }, + { skip: !selectedTrainId || !infraId } + ); + + const { data: trainSimulation } = + osrdEditoastApi.endpoints.getV2TrainScheduleByIdSimulation.useQuery( + { id: selectedTrainId as number, infraId: infraId as number }, + { skip: !selectedTrainId || !infraId } + ); + + const [postPathProperties] = + enhancedEditoastApi.endpoints.postV2InfraByInfraIdPathProperties.useMutation(); + + useEffect(() => { + const getPathProperties = async () => { + if ( + infraId && + selectedTrainSchedule && + selectedTrainRollingStock && + pathfindingResult && + pathfindingResult.status === 'success' && + trainSimulation?.status === 'success' + ) { + const pathPropertiesParams: PostV2InfraByInfraIdPathPropertiesApiArg = { + infraId, + props: ['electrifications', 'geometry', 'operational_points', 'curves', 'slopes'], + pathPropertiesInput: { + track_section_ranges: pathfindingResult.track_section_ranges, + }, + }; + const pathPropertiesResult = await postPathProperties(pathPropertiesParams).unwrap(); + + const formattedPathProperties = preparePathPropertiesData( + trainSimulation.electrical_profiles, + pathPropertiesResult, + pathfindingResult.length + ); + setPathProperties(formattedPathProperties); + + // Format power restrictions + if ( + selectedTrainSchedule && + selectedTrainSchedule.power_restrictions && + selectedTrainRollingStock + ) { + const powerRestrictionsRanges = formatPowerRestrictionRanges( + selectedTrainSchedule.power_restrictions, + selectedTrainSchedule.path, + pathfindingResult.path_items_positions + ); + const powerRestrictionsWithHandled = formatPowerRestrictionRangesWithHandled( + powerRestrictionsRanges, + formattedPathProperties.electrifications, + selectedTrainRollingStock.effort_curves.modes + ); + + setFormattedPowerRestrictions(powerRestrictionsWithHandled); + } + + // Format chart synchronizer data + const { + baseHeadPositions, + baseTailPositions, + baseSpeeds, + marginSpeeds, + ecoHeadPosition, + ecoTailPosition, + ecoSpeeds, + } = trainSimulation.base.positions.reduce( + (results, position, index) => { + const positionInMeters = mmToM(position); + + // TODO GET v2 : probably remove this conversion as trains will travel on several days + // The chart time axis is set by d3 function *sec2d3datetime* which start the chart at 01/01/1900 00:00:00 + // As javascript new Date() util takes count of the minutes lost since 1/1/1900 (9min and 21s), we have + // to use sec2d3datetime here as well to set the times on the chart + const timeDifferenceMinutes = new Date().getTimezoneOffset(); + const time = sec2d3datetime( + isoDateToMs(selectedTrainSchedule.start_time) / 1000 + + Math.abs(timeDifferenceMinutes) * 60 + + trainSimulation.base.times[index] / 1000 + ); + + if (!time) { + return results; + } + + results.baseHeadPositions.push({ position: positionInMeters, time }); + results.baseTailPositions.push({ + position: positionInMeters - selectedTrainRollingStock.length, + time, + }); + results.baseSpeeds.push({ + position: positionInMeters, + time, + speed: trainSimulation.base.speeds[index], + }); + results.marginSpeeds.push({ + position: positionInMeters, + time, + speed: trainSimulation.final_output.speeds[index], + }); + results.ecoHeadPosition.push({ position, time }); + results.ecoTailPosition.push({ + position: positionInMeters - selectedTrainRollingStock.length, + time, + }); + results.ecoSpeeds.push({ + position: positionInMeters, + time, + speed: trainSimulation.final_output.speeds[index], + }); + return results; + }, + { + baseHeadPositions: [] as { time: Date; position: number }[], + baseTailPositions: [] as { time: Date; position: number }[], + baseSpeeds: [] as { time: Date; position: number; speed: number }[], + marginSpeeds: [] as { time: Date; position: number; speed: number }[], + ecoHeadPosition: [] as { time: Date; position: number }[], + ecoTailPosition: [] as { time: Date; position: number }[], + ecoSpeeds: [] as { time: Date; position: number; speed: number }[], + } + ); + + const formattedChartSynchronizerData: ChartSynchronizerTrainData = { + headPosition: baseHeadPositions, + tailPosition: baseTailPositions, + speed: baseSpeeds, + margins_speed: marginSpeeds, + eco_headPosition: ecoHeadPosition, + eco_tailPosition: ecoTailPosition, + eco_speed: ecoSpeeds, + }; + + ChartSynchronizerV2.getInstance().setTrainData(formattedChartSynchronizerData); + } + }; + getPathProperties(); + }, [ + pathfindingResult, + trainSimulation, + infraId, + selectedTrainSchedule, + selectedTrainRollingStock, + ]); + + return { + selectedTrain: selectedTrainSchedule, + selectedTrainRollingStock, + selectedTrainPowerRestrictions: formattedPowerRestrictions, + trainSimulation, + pathProperties, + }; +}; diff --git a/front/src/applications/operationalStudies/types.ts b/front/src/applications/operationalStudies/types.ts index 7ab8183b30a..065301852c2 100644 --- a/front/src/applications/operationalStudies/types.ts +++ b/front/src/applications/operationalStudies/types.ts @@ -1,4 +1,9 @@ -import type { PathProperties, PathResponse } from 'common/api/osrdEditoastApi'; +import type { + PathProperties, + PathResponse, + ProjectPathTrainResult, + SimulationResponse, +} from 'common/api/osrdEditoastApi'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; export interface Destination { @@ -85,3 +90,68 @@ export type ManageTrainSchedulePathProperties = { allVias: SuggestedOP[]; length: number; }; + +/** + * Properties signal_updates time_end and time_start are in seconds taking count of the departure time + */ +export type TrainSpaceTimeData = { + id: number; + trainName: string; + spaceTimeCurves: { time: number; headPosition: number; tailPosition: number }[][]; +} & Omit; + +export type PositionData = { + [key in T]: number; +} & { + position: number; +}; + +export type ElectrificationRangeV2 = { + electrificationUsage: ElectrificationUsageV2; + start: number; + stop: number; +}; + +export type ElectrificationUsageV2 = ElectrificationValue & + SimulationResponseSuccess['electrical_profiles']['values'][number]; + +export type BoundariesData = { + /** List of `n` boundaries of the ranges. + A boundary is a distance from the beginning of the path in mm. */ + boundaries: number[]; + /** List of `n+1` values associated to the ranges */ + values: number[]; +}; + +export type ElectricalBoundariesData = { + boundaries: number[]; + values: T[]; +}; + +export type ElectricalRangesData = { + start: number; + stop: number; + values: T; +}; + +export type ElectrificationValue = NonNullable< + PathProperties['electrifications'] +>['values'][number]; + +export type ElectricalProfileValue = Extract< + SimulationResponse, + { status: 'success' } +>['electrical_profiles']['values'][number]; + +/** + * Electrifications start and stop are in meters + */ +export type PathPropertiesFormatted = { + electrifications: ElectrificationRangeV2[]; + curves: PositionData<'radius'>[]; + slopes: PositionData<'gradient'>[]; + operationalPoints: NonNullable; + geometry: NonNullable; +}; + +export type SimulationResponseSuccess = Extract; diff --git a/front/src/applications/operationalStudies/utils.ts b/front/src/applications/operationalStudies/utils.ts new file mode 100644 index 00000000000..76710fe5a77 --- /dev/null +++ b/front/src/applications/operationalStudies/utils.ts @@ -0,0 +1,303 @@ +import { compact, omit } from 'lodash'; + +import type { + PathProperties, + PathfindingResultSuccess, + ProjectPathTrainResult, + RollingStock, + SimulationPowerRestrictionRange, + TrainScheduleBase, +} from 'common/api/osrdEditoastApi'; +import { getRollingStockPowerRestrictionsByMode } from 'modules/rollingStock/helpers/powerRestrictions'; +import { convertUTCDateToLocalDate, isoDateToMs } from 'utils/date'; +import { mmToM } from 'utils/physics'; +import { ms2sec } from 'utils/timeManipulation'; + +import type { + BoundariesData, + ElectricalBoundariesData, + ElectricalProfileValue, + ElectricalRangesData, + ElectrificationRangeV2, + ElectrificationValue, + PathPropertiesFormatted, + PositionData, + SimulationResponseSuccess, + TrainSpaceTimeData, +} from './types'; + +/** + * Transform datas received with boundaries / values format : + * - boundaries : List of `n` boundaries of the ranges. A boundary is a distance + * from the beginning of the path in mm. + - values : List of `n+1` values associated to the ranges. + @returns an array of PositionData with the position in meters and the associated value + depending on the kind of data provided. As the boundaries don't include the path's origin and destination + positions, we add them manually. + */ +export const transformBoundariesDataToPositionDataArray = ( + boundariesData: BoundariesData, + pathLength: number, + value: T +): PositionData[] => { + const formatedData = boundariesData.boundaries.reduce( + (acc, boundary, index) => { + const newData = { + position: mmToM(boundary), + [value]: boundariesData.values[index], + } as PositionData; + acc.push(newData); + return acc; + }, + [{ position: 0, [value]: 0 }] as PositionData[] + ); + + formatedData.push({ + position: mmToM(pathLength), + [value]: boundariesData.values[boundariesData.values.length - 1], + } as PositionData); + + return formatedData; +}; + +/** + * Transform electrifications received with boundaries / values format : + * - boundaries : List of `n` boundaries of the ranges. A boundary is a distance + * from the beginning of the path in mm. + - values : List of `n+1` values associated to the ranges. + @returns an array of electrifications ranges with the start and stop of the range in meters and + the associated value. As the boundaries don't include the path's origin and destination + positions, we add them manually. + */ +export const transformBoundariesDataToRangesData = < + T extends ElectrificationValue | ElectricalProfileValue, +>( + boundariesData: ElectricalBoundariesData, + pathLength: number +): ElectricalRangesData[] => { + const formatedData = boundariesData.boundaries.map((boundary, index) => ({ + start: index === 0 ? 0 : mmToM(boundariesData.boundaries[index - 1]), + stop: mmToM(boundary), + values: boundariesData.values[index], + })); + + formatedData.push({ + start: mmToM(boundariesData.boundaries[boundariesData.boundaries.length - 1]), + stop: mmToM(pathLength), + values: boundariesData.values[boundariesData.values.length - 1], + }); + + return formatedData; +}; + +export const formatElectrificationRanges = ( + electrifications: ElectricalRangesData[], + electricalProfiles: ElectricalRangesData[] +): ElectrificationRangeV2[] => + // Electrifications can be of three types, electricalProfiles only two, so we know electrifications + // will always be at least as long as electricalProfiles so we can use it as the main array + electrifications.reduce((acc: ElectrificationRangeV2[], curr, index) => { + const currentElectrification = curr; + const currentProfile = electricalProfiles[index]; + + // currentProfile is defined as long as we didn't reach the end of electricalProfiles array + if (currentProfile) { + // If start and stop are identical, we can merge the two items + if ( + currentElectrification.start === currentProfile.start && + currentElectrification.stop === currentProfile.stop + ) { + acc.push({ + electrificationUsage: { + ...currentElectrification.values, + ...currentProfile.values, + }, + start: currentElectrification.start, + stop: currentElectrification.stop, + }); + } else { + // Find the profile matching the current electrification range + // We know we will find one because currentProfile is still defined + const associatedProfile = electricalProfiles.find( + (profile) => profile.stop >= currentElectrification.stop + ) as ElectricalRangesData; + + acc.push({ + electrificationUsage: { + ...currentElectrification.values, + ...associatedProfile.values, + }, + start: currentElectrification.start, + stop: currentElectrification.stop, + }); + } + // If we reached the end of the electricalProfiles array, we use its last value for the profile + } else { + // Find the profile matching the current electrification range + // We know we will find one because theirs last stops are the same + const associatedProfile = electricalProfiles.find( + (profile) => profile.stop >= currentElectrification.stop + ) as ElectricalRangesData; + + acc.push({ + electrificationUsage: { + ...currentElectrification.values, + ...associatedProfile.values, + }, + start: currentElectrification.start, + stop: currentElectrification.stop, + }); + } + + return acc; + }, []); + +/** + * Format path propreties data to be used in simulation results charts + */ +export const preparePathPropertiesData = ( + electricalProfiles: SimulationResponseSuccess['electrical_profiles'], + { slopes, curves, electrifications, operational_points, geometry }: PathProperties, + pathLength: number +): PathPropertiesFormatted => { + const formattedSlopes = transformBoundariesDataToPositionDataArray( + slopes as NonNullable, + pathLength, + 'gradient' + ); + + const formattedCurves = transformBoundariesDataToPositionDataArray( + curves as NonNullable, + pathLength, + 'radius' + ); + + const electrificationsRanges = transformBoundariesDataToRangesData( + electrifications as NonNullable, + pathLength + ) as ElectricalRangesData[]; + + const electricalProfilesRanges = transformBoundariesDataToRangesData( + electricalProfiles, + pathLength + ) as ElectricalRangesData[]; + + const electrificationRanges = formatElectrificationRanges( + electrificationsRanges, + electricalProfilesRanges + ); + + return { + electrifications: electrificationRanges, + curves: formattedCurves, + slopes: formattedSlopes, + operationalPoints: operational_points as NonNullable, + geometry: geometry as NonNullable, + }; +}; + +/** + * Format power restrictions data to ranges data base on path steps position + */ +export const formatPowerRestrictionRanges = ( + powerRestrictions: NonNullable, + path: TrainScheduleBase['path'], + stepsPathPositions: PathfindingResultSuccess['path_items_positions'] +): Omit[] => + compact( + powerRestrictions.map((powerRestriction) => { + const startStep = path.findIndex((step) => step.id === powerRestriction.from); + const stopStep = path.findIndex((step) => step.id === powerRestriction.to); + if (startStep === -1 || stopStep === -1) { + console.error('Power restriction range not found in path steps.'); + return null; + } + return { + start: mmToM(stepsPathPositions[startStep]), + stop: mmToM(stepsPathPositions[stopStep]), + code: powerRestriction.value, + }; + }) + ); + +/** + * Format power restrictions data to be used in simulation results charts + */ +export const formatPowerRestrictionRangesWithHandled = ( + powerRestrictionRanges: Omit[], + electrificationRanges: ElectrificationRangeV2[], + rollingStockEffortCurves: RollingStock['effort_curves']['modes'] +): SimulationPowerRestrictionRange[] => { + const powerRestrictionsByMode = getRollingStockPowerRestrictionsByMode(rollingStockEffortCurves); + + return powerRestrictionRanges.map((powerRestrictionRange) => { + const foundElectrificationRange = electrificationRanges.find( + (electrificationRange) => + electrificationRange.start <= powerRestrictionRange.start && + electrificationRange.stop >= powerRestrictionRange.stop + ); + + let isHandled = false; + if ( + foundElectrificationRange && + foundElectrificationRange.electrificationUsage.type === 'electrification' + ) { + isHandled = powerRestrictionsByMode[ + foundElectrificationRange.electrificationUsage.voltage + ].includes(powerRestrictionRange.code); + } + + return { + ...powerRestrictionRange, + handled: isHandled, + }; + }); +}; + +/** + * Convert an UTC departure time in ISO8601 to seconds and + * convert it to local time + */ +export const convertDepartureTimeIntoSec = (departureTime: string) => { + const isoDateInSec = ms2sec(isoDateToMs(departureTime)); + return convertUTCDateToLocalDate(isoDateInSec); +}; + +export const formatSpaceTimeData = ( + trainId: string, + projectPathTrainResult: ProjectPathTrainResult, + trainName?: string +): TrainSpaceTimeData => { + const spaceTimeCurves = projectPathTrainResult.space_time_curves.map((spaceTimeCurve) => + spaceTimeCurve.times.map((time, index) => ({ + // time refers to the time elapsed since departure so we need to add it to the start time + time: convertDepartureTimeIntoSec(projectPathTrainResult.departure_time) + ms2sec(time), + headPosition: mmToM(spaceTimeCurve.positions[index]), + tailPosition: mmToM( + spaceTimeCurve.positions[index] - projectPathTrainResult.rolling_stock_length + ), + })) + ); + + // We keep snake case here because we don't want to change everything in the d3 helpers + // since we will remove them soon + const signal_updates = projectPathTrainResult.signal_updates.map((signalUpdate) => ({ + ...signalUpdate, + position_end: mmToM(signalUpdate.position_end), + position_start: mmToM(signalUpdate.position_start), + time_end: + convertDepartureTimeIntoSec(projectPathTrainResult.departure_time) + + ms2sec(signalUpdate.time_end), + time_start: + convertDepartureTimeIntoSec(projectPathTrainResult.departure_time) + + ms2sec(signalUpdate.time_start), + })); + + return { + ...omit(projectPathTrainResult, ['space_time_curves', 'signal_updates']), + spaceTimeCurves, + signal_updates, + id: +trainId, + trainName: trainName || 'Train name not found', + }; +}; diff --git a/front/src/applications/operationalStudies/views/v2/ScenarioV2.tsx b/front/src/applications/operationalStudies/views/v2/ScenarioV2.tsx index e60c061bf9f..5256ea995e1 100644 --- a/front/src/applications/operationalStudies/views/v2/ScenarioV2.tsx +++ b/front/src/applications/operationalStudies/views/v2/ScenarioV2.tsx @@ -9,11 +9,9 @@ import { useParams } from 'react-router-dom'; import BreadCrumbs from 'applications/operationalStudies/components/BreadCrumbs'; import InfraLoadingState from 'applications/operationalStudies/components/Scenario/InfraLoadingState'; import { MANAGE_TRAIN_SCHEDULE_TYPES } from 'applications/operationalStudies/consts'; +import type { TrainSpaceTimeData } from 'applications/operationalStudies/types'; import infraLogo from 'assets/pictures/components/tracks.svg'; -import { - osrdEditoastApi, - type PostV2TrainScheduleProjectPathApiResponse, -} from 'common/api/osrdEditoastApi'; +import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; import { useModal } from 'common/BootstrapSNCF/ModalSNCF'; import NavBarSNCF from 'common/BootstrapSNCF/NavBarSNCF'; import { useInfraID, useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; @@ -26,9 +24,10 @@ import { updateTrainIdUsedForProjection } from 'reducers/osrdsimulation/actions' import { getTrainIdUsedForProjection, getSelectedTrainId } from 'reducers/osrdsimulation/selectors'; import { useAppDispatch } from 'store'; -import { getSimulationResultsV2, selectProjectionV2 } from './getSimulationResultsV2'; +import { getSpaceTimeChartData, selectProjectionV2 } from './getSimulationResultsV2'; import ImportTrainScheduleV2 from './ImportTrainScheduleV2'; import ManageTrainScheduleV2 from './ManageTrainScheduleV2'; +import SimulationResultsV2 from './SimulationResultsV2'; type SimulationParams = { projectId: string; @@ -46,8 +45,7 @@ const ScenarioV2 = () => { const [collapsedTimetable, setCollapsedTimetable] = useState(false); const [isInfraLoaded, setIsInfraLoaded] = useState(false); const [reloadCount, setReloadCount] = useState(1); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [spaceTimeData, setSpaceTimeData] = useState(); + const [trainSpaceTimeData, setTrainSpaceTimeData] = useState([]); const [trainResultsToFetch, setTrainResultsToFetch] = useState(); const isUpdating = useSelector((state: RootState) => state.osrdsimulation.isUpdating); @@ -161,14 +159,25 @@ const ScenarioV2 = () => { if (timetable && infra?.state === 'CACHED' && trainIdUsedForProjection && infraId) { // If trainResultsToFetch is undefined that means it's the first load of the scenario // and we want to get all timetable trains results - getSimulationResultsV2( + getSpaceTimeChartData( trainResultsToFetch ?? timetable.train_ids, trainIdUsedForProjection, infraId, - setSpaceTimeData + setTrainSpaceTimeData ); } - }, [timetable, infra, trainIdUsedForProjection]); + }, [timetable, infra]); + + useEffect(() => { + if (timetable && infra?.state === 'CACHED' && trainIdUsedForProjection && infraId) { + getSpaceTimeChartData( + timetable.train_ids, + trainIdUsedForProjection, + infraId, + setTrainSpaceTimeData + ); + } + }, [trainIdUsedForProjection]); useEffect(() => { if (!projectId || !studyId || !scenarioId) { @@ -344,13 +353,13 @@ const ScenarioV2 = () => { )} - {/* {isInfraLoaded && infra && ( + {isInfraLoaded && infra && ( - )} */} + )} diff --git a/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx b/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx index 3b732417a01..91f0ce8878d 100644 --- a/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx +++ b/front/src/applications/operationalStudies/views/v2/SimulationResultsV2.tsx @@ -1,276 +1,263 @@ -// import React, { useEffect, useState, useRef } from 'react'; +import React, { useEffect, useState, useRef, useMemo } from 'react'; -// import { ChevronLeft, ChevronRight } from '@osrd-project/ui-icons'; -// import cx from 'classnames'; -// import { useTranslation } from 'react-i18next'; -// import { useSelector } from 'react-redux'; -// import { Rnd } from 'react-rnd'; +import { ChevronLeft, ChevronRight } from '@osrd-project/ui-icons'; +import cx from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import { Rnd } from 'react-rnd'; -// import { osrdEditoastApi, type SimulationReport } from 'common/api/osrdEditoastApi'; -// import SimulationWarpedMap from 'common/Map/WarpedMap/SimulationWarpedMap'; -// import getScaleDomainFromValues from 'modules/simulationResult/components/ChartHelpers/getScaleDomainFromValues'; -// import SimulationResultsMap from 'modules/simulationResult/components/SimulationResultsMap'; -// import SpaceCurvesSlopes from 'modules/simulationResult/components/SpaceCurvesSlopes'; -// import SpaceTimeChart from 'modules/simulationResult/components/SpaceTimeChart/SpaceTimeChart'; -// import { useStoreDataForSpaceTimeChart } from 'modules/simulationResult/components/SpaceTimeChart/useStoreDataForSpaceTimeChart'; -// import SpeedSpaceChart from 'modules/simulationResult/components/SpeedSpaceChart/SpeedSpaceChart'; -// import TimeButtons from 'modules/simulationResult/components/TimeButtons'; -// import TrainDetails from 'modules/simulationResult/components/TrainDetails'; -// import type { PositionScaleDomain, TimeScaleDomain } from 'modules/simulationResult/types'; -// import DriverTrainSchedule from 'modules/trainschedule/components/DriverTrainSchedule/DriverTrainSchedule'; -// import { updateViewport, type Viewport } from 'reducers/map'; -// import { updateSelectedProjection, updateSimulation } from 'reducers/osrdsimulation/actions'; -// import { getIsUpdating } from 'reducers/osrdsimulation/selectors'; -// import { -// persistentRedoSimulation, -// persistentUndoSimulation, -// } from 'reducers/osrdsimulation/simulation'; -// // TIMELINE DISABLED // import TimeLine from 'modules/simulationResult/components/TimeLine/TimeLine'; -// import type { Train } from 'reducers/osrdsimulation/types'; -// import { useAppDispatch } from 'store'; +import { useSimulationResults } from 'applications/operationalStudies/hooks'; +import type { TrainSpaceTimeData } from 'applications/operationalStudies/types'; +import SimulationWarpedMap from 'common/Map/WarpedMap/SimulationWarpedMap'; +import { getScaleDomainFromValuesV2 } from 'modules/simulationResult/components/ChartHelpers/getScaleDomainFromValues'; +import SimulationResultsMapV2 from 'modules/simulationResult/components/SimulationResultsMapV2'; +import SpaceCurvesSlopesV2 from 'modules/simulationResult/components/SpaceCurvesSlopes/SpaceCurvesSlopesV2'; +import SpaceTimeChartV2 from 'modules/simulationResult/components/SpaceTimeChart/SpaceTimeChartV2'; +import { useStoreDataForSpaceTimeChartV2 } from 'modules/simulationResult/components/SpaceTimeChart/useStoreDataForSpaceTimeChart'; +import SpeedSpaceChartV2 from 'modules/simulationResult/components/SpeedSpaceChart/SpeedSpaceChartV2'; +import TimeButtons from 'modules/simulationResult/components/TimeButtons'; +import TrainDetailsV2 from 'modules/simulationResult/components/TrainDetailsV2'; +import type { PositionScaleDomain, TimeScaleDomain } from 'modules/simulationResult/types'; +import DriverTrainScheduleV2 from 'modules/trainschedule/components/DriverTrainScheduleV2/DriverTrainScheduleV2'; +import { updateViewport, type Viewport } from 'reducers/map'; +import { getIsUpdating } from 'reducers/osrdsimulation/selectors'; +// TIMELINE DISABLED // import TimeLine from 'modules/simulationResult/components/TimeLine/TimeLine'; +import { useAppDispatch } from 'store'; -// const MAP_MIN_HEIGHT = 450; +const MAP_MIN_HEIGHT = 450; -// type SimulationResultsProps = { -// collapsedTimetable: boolean; -// setTrainResultsToFetch: (trainSchedulesIDs?: number[]) => void; -// }; +type SimulationResultsV2Props = { + collapsedTimetable: boolean; + setTrainResultsToFetch: (trainSchedulesIDs?: number[]) => void; + spaceTimeData: TrainSpaceTimeData[]; +}; -// const SimulationResultsV2 = ({ -// collapsedTimetable, -// setTrainResultsToFetch, -// }: SimulationResultsProps) => { -// const { t } = useTranslation('simulation'); -// const dispatch = useAppDispatch(); +const SimulationResultsV2 = ({ + collapsedTimetable, + setTrainResultsToFetch, + spaceTimeData, +}: SimulationResultsV2Props) => { + const { t } = useTranslation('simulation'); + const dispatch = useAppDispatch(); + // TIMELINE DISABLED // const { chart } = useSelector(getOsrdSimulation); + const isUpdating = useSelector(getIsUpdating); + const { infraId, trainIdUsedForProjection, simulationIsPlaying, dispatchUpdateSelectedTrainId } = + useStoreDataForSpaceTimeChartV2(); -// // TIMELINE DISABLED // const { chart } = useSelector(getOsrdSimulation); -// const isUpdating = useSelector(getIsUpdating); + const timeTableRef = useRef(null); + const [extViewport, setExtViewport] = useState(undefined); + const [showWarpedMap, setShowWarpedMap] = useState(false); -// const timeTableRef = useRef(null); -// const [extViewport, setExtViewport] = useState(undefined); -// const [showWarpedMap, setShowWarpedMap] = useState(false); + const [heightOfSpaceTimeChart, setHeightOfSpaceTimeChart] = useState(600); + const [heightOfSpeedSpaceChart, setHeightOfSpeedSpaceChart] = useState(250); + const [heightOfSimulationMap] = useState(MAP_MIN_HEIGHT); + const [heightOfSpaceCurvesSlopesChart, setHeightOfSpaceCurvesSlopesChart] = useState(150); + const [initialHeightOfSpaceCurvesSlopesChart, setInitialHeightOfSpaceCurvesSlopesChart] = + useState(heightOfSpaceCurvesSlopesChart); -// const [heightOfSpaceTimeChart, setHeightOfSpaceTimeChart] = useState(600); -// const [heightOfSpeedSpaceChart, setHeightOfSpeedSpaceChart] = useState(250); -// const [heightOfSimulationMap] = useState(MAP_MIN_HEIGHT); -// const [heightOfSpaceCurvesSlopesChart, setHeightOfSpaceCurvesSlopesChart] = useState(150); -// const [initialHeightOfSpaceCurvesSlopesChart, setInitialHeightOfSpaceCurvesSlopesChart] = -// useState(heightOfSpaceCurvesSlopesChart); + const [timeScaleDomain, setTimeScaleDomain] = useState({ + range: undefined, + source: undefined, + }); -// const [timeScaleDomain, setTimeScaleDomain] = useState({ -// range: undefined, -// source: undefined, -// }); + // X scale domain shared between SpeedSpace and SpaceCurvesSlopes charts. + const [positionScaleDomain, setPositionScaleDomain] = useState({ + initial: [], + current: [], + source: undefined, + }); -// // X scale domain shared between SpeedSpace and SpaceCurvesSlopes charts. -// const [positionScaleDomain, setPositionScaleDomain] = useState({ -// initial: [], -// current: [], -// source: undefined, -// }); + const { + selectedTrain, + selectedTrainRollingStock, + selectedTrainPowerRestrictions, + trainSimulation, + pathProperties, + } = useSimulationResults(); -// const { -// allowancesSettings, -// selectedTrain, -// selectedProjection, -// simulation, -// simulationIsPlaying, -// dispatchUpdateSelectedTrainId, -// dispatchPersistentUpdateSimulation, -// } = useStoreDataForSpaceTimeChart(); + const trainUsedForProjectionSpaceTimeData = useMemo( + () => + selectedTrain ? spaceTimeData.find((_train) => _train.id === selectedTrain.id) : undefined, + [selectedTrain, spaceTimeData] + ); -// const { data: selectedTrainSchedule } = osrdEditoastApi.endpoints.getTrainScheduleById.useQuery( -// { -// id: selectedTrain?.id as number, -// }, -// { skip: !selectedTrain } -// ); + useEffect(() => { + if (extViewport !== undefined) { + dispatch( + updateViewport({ + ...extViewport, + }) + ); + } + }, [extViewport]); -// const { data: selectedTrainRollingStock } = -// osrdEditoastApi.endpoints.getLightRollingStockByRollingStockId.useQuery( -// { -// rollingStockId: selectedTrainSchedule?.rolling_stock_id as number, -// }, -// { skip: !selectedTrainSchedule } -// ); + useEffect(() => { + if (trainSimulation && trainSimulation.status === 'success') { + const { positions } = trainSimulation.final_output; + const newPositionsScaleDomain = getScaleDomainFromValuesV2(positions); + setPositionScaleDomain({ + initial: newPositionsScaleDomain, + current: newPositionsScaleDomain, + }); + } + }, [trainSimulation]); -// const handleKey = (e: KeyboardEvent) => { -// if (e.key === 'z' && e.metaKey) { -// dispatch(persistentUndoSimulation()); -// } -// if (e.key === 'e' && e.metaKey) { -// dispatch(persistentRedoSimulation()); -// } -// }; + if (!trainSimulation) return null; -// useEffect(() => { -// // Setup the listener to undi /redo -// window.addEventListener('keydown', handleKey); -// return function cleanup() { -// window.removeEventListener('keydown', handleKey); -// dispatch(updateSelectedProjection(undefined)); -// dispatch(updateSimulation({ trains: [] })); -// }; -// }, []); + if (trainSimulation.status !== 'success' && !isUpdating) return null; -// useEffect(() => { -// if (extViewport !== undefined) { -// dispatch( -// updateViewport({ -// ...extViewport, -// }) -// ); -// } -// }, [extViewport]); + return ( +
+ {/* SIMULATION : STICKY BAR */} + {selectedTrain && ( +
+
+
+ +
+ {trainUsedForProjectionSpaceTimeData && ( + + )} +
+
+ )} -// useEffect(() => { -// if (selectedTrain) { -// const positions = selectedTrain.base.speeds.map((speed) => speed.position); -// const newPositionsScaleDomain = getScaleDomainFromValues(positions); -// setPositionScaleDomain({ -// initial: newPositionsScaleDomain, -// current: newPositionsScaleDomain, -// }); -// } -// }, [selectedTrain]); + {/* SIMULATION: TIMELINE — TEMPORARILY DISABLED + {simulation.trains.length && ( + + )} + */} -// return simulation.trains.length === 0 && !isUpdating ? null : ( -//
-// {/* SIMULATION : STICKY BAR */} -//
-//
-//
-// {selectedTrain && } -//
-//
-// -//
-//
-//
+ {/* SIMULATION : SPACE TIME CHART */} + {spaceTimeData && selectedTrain && pathProperties && ( +
+ + -// {/* SIMULATION: TIMELINE — TEMPORARILY DISABLED -// {simulation.trains.length && ( -// -// )} -// */} +
+
+ +
+
+
+ )} -// {/* SIMULATION : SPACE TIME CHART */} -//
-// -// + {/* TRAIN : SPACE SPEED CHART */} + {selectedTrainRollingStock && trainSimulation && pathProperties && ( +
+
+ +
+
+ )} -//
-//
-// {simulation.trains.length > 0 && ( -// -// )} -//
-//
-//
+ {/* TRAIN : CURVES & SLOPES */} + {trainSimulation.status === 'success' && pathProperties && ( +
+
+ + setInitialHeightOfSpaceCurvesSlopesChart(heightOfSpaceCurvesSlopesChart) + } + onResize={(_e, _dir, _refToElement, delta) => { + setHeightOfSpaceCurvesSlopesChart( + initialHeightOfSpaceCurvesSlopesChart + delta.height + ); + }} + > + + +
+
+ )} -// {/* TRAIN : SPACE SPEED CHART */} -// {selectedTrain && ( -//
-//
-// -//
-//
-// )} + {/* SIMULATION : MAP */} +
+
+
+ +
+
+
-// {/* TRAIN : CURVES & SLOPES */} -//
-//
-// {selectedTrain && ( -// -// setInitialHeightOfSpaceCurvesSlopesChart(heightOfSpaceCurvesSlopesChart) -// } -// onResize={(_e, _dir, _refToElement, delta) => { -// setHeightOfSpaceCurvesSlopesChart( -// initialHeightOfSpaceCurvesSlopesChart + delta.height -// ); -// }} -// > -// -// -// )} -//
-//
- -// {/* TRAIN : DRIVER TRAIN SCHEDULE */} -// {selectedTrain && selectedTrainRollingStock && ( -//
-// -//
-// )} - -// {/* SIMULATION : MAP */} -//
-//
-//
-// -//
-//
-//
-//
-// ); -// }; - -const SimulationResultsV2 = () => null; + {/* TRAIN : DRIVER TRAIN SCHEDULE */} + {selectedTrain && + trainSimulation.status === 'success' && + pathProperties && + selectedTrainRollingStock && + infraId && ( +
+ +
+ )} +
+ ); +}; export default SimulationResultsV2; diff --git a/front/src/applications/operationalStudies/views/v2/getSimulationResultsV2.ts b/front/src/applications/operationalStudies/views/v2/getSimulationResultsV2.ts index b98a7909f81..c9c647a4563 100644 --- a/front/src/applications/operationalStudies/views/v2/getSimulationResultsV2.ts +++ b/front/src/applications/operationalStudies/views/v2/getSimulationResultsV2.ts @@ -1,8 +1,7 @@ +import type { TrainSpaceTimeData } from 'applications/operationalStudies/types'; +import { formatSpaceTimeData } from 'applications/operationalStudies/utils'; import { enhancedEditoastApi } from 'common/api/enhancedEditoastApi'; -import { - osrdEditoastApi, - type PostV2TrainScheduleProjectPathApiResponse, -} from 'common/api/osrdEditoastApi'; +import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; import i18n from 'i18n'; import { setFailure } from 'reducers/main'; import { @@ -11,6 +10,7 @@ import { updateSelectedTrainId, } from 'reducers/osrdsimulation/actions'; import { store } from 'store'; +import { replaceElementAtIndex } from 'utils/array'; import { castErrorToFailure } from 'utils/error'; export const selectProjectionV2 = ( @@ -42,13 +42,11 @@ export const selectProjectionV2 = ( store.dispatch(updateSelectedTrainId(trainIds[0])); }; -export const getSimulationResultsV2 = async ( +export const getSpaceTimeChartData = async ( trainSchedulesIDs: number[], trainIdUsedForProjection: number, infraId: number, - setSpaceTimeData: React.Dispatch< - React.SetStateAction - > + setSpaceTimeData: React.Dispatch> ) => { if (trainSchedulesIDs.length > 0) { store.dispatch(updateIsUpdating(true)); @@ -61,7 +59,11 @@ export const getSimulationResultsV2 = async ( }) ); - if (pathfindingResult && pathfindingResult.status === 'success') { + const { data: trainSchedules } = await store.dispatch( + osrdEditoastApi.endpoints.getV2TrainSchedule.initiate({ ids: trainSchedulesIDs }) + ); + + if (pathfindingResult && pathfindingResult.status === 'success' && trainSchedules) { const { blocks, routes, track_section_ranges } = pathfindingResult; const projectPathTrainResult = await store .dispatch( @@ -73,13 +75,36 @@ export const getSimulationResultsV2 = async ( ) .unwrap(); - setSpaceTimeData((prev) => { - const updatedData = { ...prev }; + setSpaceTimeData((prevTrains) => { + let newSpaceTimeData = [...prevTrains]; + // For each key (train id) in projectPathTrainResult, we either add it or update it in the state - Object.keys(projectPathTrainResult).forEach((key) => { - updatedData[key] = projectPathTrainResult[key]; + Object.keys(projectPathTrainResult).forEach((trainId) => { + const currentProjectedTrain = projectPathTrainResult[trainId]; + + const matchingTrain = trainSchedules.find((train) => train.id === +trainId); + + const formattedProjectedPathTrainResult = formatSpaceTimeData( + trainId, + currentProjectedTrain, + matchingTrain?.train_name + ); + + const foundTrainIndex = newSpaceTimeData.findIndex( + (train) => train.id.toString() === trainId + ); + if (foundTrainIndex !== -1) { + newSpaceTimeData = replaceElementAtIndex( + newSpaceTimeData, + foundTrainIndex, + formattedProjectedPathTrainResult + ); + } else { + newSpaceTimeData.push(formattedProjectedPathTrainResult); + } }); - return updatedData; + + return newSpaceTimeData; }); } } catch (e) { diff --git a/front/src/modules/powerRestriction/helpers/powerRestrictionSelector.ts b/front/src/modules/powerRestriction/helpers/powerRestrictionSelector.ts index 0e36441429c..619646df430 100644 --- a/front/src/modules/powerRestriction/helpers/powerRestrictionSelector.ts +++ b/front/src/modules/powerRestriction/helpers/powerRestrictionSelector.ts @@ -1,34 +1,9 @@ -import { compact, groupBy, reduce, uniq, type Dictionary } from 'lodash'; +import { groupBy, type Dictionary } from 'lodash'; import type { RangedValue, RollingStock } from 'common/api/osrdEditoastApi'; import NO_POWER_RESTRICTION from 'modules/powerRestriction/consts'; import type { PowerRestrictionWarning } from 'modules/powerRestriction/types'; - -/** - * Return the power restriction codes of the rolling stock by mode - * - * ex: { "1500V": ["C1US", "C2US"], "25000V": ["M1US"], "thermal": []} - */ -const getRollingStockPowerRestrictionsByMode = ( - rollingStockModes: RollingStock['effort_curves']['modes'] -): { [mode: string]: string[] } => { - const curvesModesKey = Object.keys(rollingStockModes); - - return reduce( - curvesModesKey, - (result, mode) => { - const powerCodes = rollingStockModes[mode].curves.map( - (curve) => curve.cond.power_restriction_code - ); - compact(uniq(powerCodes)); - return { - ...result, - [mode]: powerCodes, - }; - }, - {} - ); -}; +import { getRollingStockPowerRestrictionsByMode } from 'modules/rollingStock/helpers/powerRestrictions'; /** * Depending on the electrification on a train's path and the power restriction codes selected by the user, diff --git a/front/src/modules/rollingStock/helpers/__tests__/powerRestrictions.spec.ts b/front/src/modules/rollingStock/helpers/__tests__/powerRestrictions.spec.ts new file mode 100644 index 00000000000..21f390968c8 --- /dev/null +++ b/front/src/modules/rollingStock/helpers/__tests__/powerRestrictions.spec.ts @@ -0,0 +1,16 @@ +import { effortCurves } from 'applications/operationalStudies/__tests__/sampleData'; + +import { getRollingStockPowerRestrictionsByMode } from '../powerRestrictions'; + +const powerRestrictionsByMode = { + '1500V': ['code1', 'code2'], + '25000V': ['code3', 'code4'], +}; + +describe('getRollingStockPowerRestrictionsByMode', () => { + it('should properly format power restrictions by electrification mode without duplicate', () => { + const result = getRollingStockPowerRestrictionsByMode(effortCurves); + + expect(result).toEqual(powerRestrictionsByMode); + }); +}); diff --git a/front/src/modules/rollingStock/helpers/powerRestrictions.ts b/front/src/modules/rollingStock/helpers/powerRestrictions.ts new file mode 100644 index 00000000000..a8bf7438aff --- /dev/null +++ b/front/src/modules/rollingStock/helpers/powerRestrictions.ts @@ -0,0 +1,25 @@ +/* eslint-disable import/prefer-default-export */ +import { compact, uniq } from 'lodash'; + +import type { RollingStock } from 'common/api/osrdEditoastApi'; + +/** + * Return the power restriction codes of the rolling stock by mode + * + * ex: { "1500V": ["C1US", "C2US"], "25000V": ["M1US"], "thermal": []} + */ +export const getRollingStockPowerRestrictionsByMode = ( + rollingStockModes: RollingStock['effort_curves']['modes'] +): { [mode: string]: string[] } => { + const curvesModesKey = Object.keys(rollingStockModes); + + return curvesModesKey.reduce((result, mode) => { + const powerCodes = rollingStockModes[mode].curves.map( + (curve) => curve.cond.power_restriction_code + ); + return { + ...result, + [mode]: compact(uniq(powerCodes)), + }; + }, {}); +}; diff --git a/front/src/modules/simulationResult/components/ChartHelpers/ChartHelpers.ts b/front/src/modules/simulationResult/components/ChartHelpers/ChartHelpers.ts index e174b694e22..689b0eaf8bb 100644 --- a/front/src/modules/simulationResult/components/ChartHelpers/ChartHelpers.ts +++ b/front/src/modules/simulationResult/components/ChartHelpers/ChartHelpers.ts @@ -3,6 +3,8 @@ import * as d3 from 'd3'; import type { BaseType } from 'd3'; import { has, last, memoize } from 'lodash'; +import type { PositionData } from 'applications/operationalStudies/types'; +import type { ReportTrainData } from 'modules/simulationResult/components/SpeedSpaceChart/types'; import type { ChartAxes, ListValues, XAxis, Y2Axis, YAxis } from 'modules/simulationResult/consts'; import type { Position, @@ -104,7 +106,7 @@ export function makeStairCase(data: Array<{ time: number; position: number }>) { return newData; } -// Time shift a train +// TODO DROP V1 : remove this export const timeShiftTrain = (train: Train, offset: number): Train => ({ ...train, base: { @@ -225,6 +227,7 @@ export type MergedBlock = { value1: number; }; +// TODO DROP V1 : remove this // called with keyValues // ['position', 'gradient'] // ['position', 'speed'] @@ -239,6 +242,20 @@ export const mergeDatasAreaConstant = ( value1: data2, })) as MergedBlock[]; +// called with keyValues +// ['position', 'gradient'] +// ['position', 'speed'] +export const mergeDatasAreaConstantV2 = ( + data1: PositionData<'gradient'>[], + data2: number, + keyValues: Keys[] +): MergedBlock[] => + data1.map((step) => ({ + [keyValues[0]]: step[keyValues[0]], + value0: step[keyValues[1]], + value1: data2, + })) as MergedBlock[]; + export const gridX = (axisScale: SimulationD3Scale, height: number) => d3 .axisBottom(axisScale) @@ -263,7 +280,7 @@ export const gridY2 = (axisScale: SimulationD3Scale, width: number) => // Interpolation of cursor based on space position // ['position', 'speed'] export const interpolateOnPosition = ( - dataSimulation: { speed: PositionSpeedTime[] }, + dataSimulation: { speed: PositionSpeedTime[] | ReportTrainData[] }, // TODO DROP V1 : remove PositionSpeedTime type positionLocal: number ) => { const bisect = d3.bisector((d) => d.position).left; @@ -287,12 +304,13 @@ export const interpolateOnTime = < >( dataSimulation: SimulationData | undefined, keyValues: ChartAxes, - listValues: ListValues + listValues: ListValues, + isTrainScheduleV2: boolean = false ) => { if (dataSimulation) { if (!interpolateCache.has(dataSimulation)) { const newInterpolate = memoize( - specificInterpolateOnTime(dataSimulation, keyValues, listValues), + specificInterpolateOnTime(dataSimulation, keyValues, listValues, isTrainScheduleV2), (timePosition: Date | number) => { if (typeof timePosition === 'number') { return timePosition; @@ -304,7 +322,7 @@ export const interpolateOnTime = < } return interpolateCache.get(dataSimulation) as (time: Time) => PositionsSpeedTimes