From 9abcaa487b1d6acfb1c8183b82dee693fb943cf3 Mon Sep 17 00:00:00 2001 From: Clara Ni Date: Thu, 4 Jul 2024 15:01:53 +0200 Subject: [PATCH] clean code clara --- .../manageTrainSchedule.json | 5 +- .../manageTrainSchedule.json | 5 +- .../applications/operationalStudies/consts.ts | 13 +- .../applications/operationalStudies/types.ts | 7 +- .../applications/operationalStudies/utils.ts | 21 +- .../views/v2/ManageTrainScheduleV2.tsx | 79 +-- .../stdcmV2/components/StdcmVias.tsx | 4 +- .../IntervalsEditor/IntervalsEditor.tsx | 5 +- .../IntervalsEditorCommonForm.tsx | 15 +- .../components/Itinerary/ItineraryV2.tsx | 4 +- .../Itinerary/ModalSuggestedVias.tsx | 8 +- .../components/Pathfinding/TypeAndPathV2.tsx | 2 +- .../helpers/__tests__/getPathVoltages.spec.ts | 137 +++++ .../pathfinding/helpers/getPathVoltages.ts | 108 ++++ .../pathfinding/hook/usePathfinding.ts | 46 +- front/src/modules/pathfinding/utils.ts | 1 + .../PowerRestrictionsSelectorV2.tsx | 409 +++------------ ...ormatPowerRestrionRangesWithHandle.spec.ts | 4 +- .../helpers/__tests__/sampleData.ts | 46 +- .../helpers/createPathStep.ts | 109 +++- ...formatPowerRestrictionRangesWithHandled.ts | 65 +-- .../helpers/formatPowerRestrictions.ts | 5 +- .../helpers/getRestrictionsToResize.ts | 103 ++++ .../helpers/powerRestrictionWarnings.ts | 58 +++ .../modules/powerRestriction/helpers/utils.ts | 78 ++- .../usePowerRestrictionSelectorBehaviours.ts | 165 ++++++ .../hooks/usePowerRestrictionSelectorData.ts | 109 ++++ .../ChartHelpers/drawPowerRestriction.ts | 2 +- .../components/SpeedSpaceChart/utils.ts | 2 +- front/src/modules/timesStops/TimesStops.tsx | 2 +- .../helpers/adjustConfWithTrainToModify.ts | 2 +- .../helpers/checkCurrentConfig.ts | 5 +- .../helpers/formatTrainSchedulePayload.ts | 5 +- .../components/ManageTrainSchedule/types.ts | 3 +- .../osrdconf/operationalStudiesConf/index.ts | 3 + .../powerRestrictionReducer.ts | 242 +++++++++ .../utils.ts | 90 +--- .../osrdConfCommon/__tests__/utils.ts | 2 +- .../reducers/osrdconf/osrdConfCommon/index.ts | 484 +----------------- front/src/utils/physics.ts | 5 + front/src/utils/strings.ts | 4 +- 41 files changed, 1275 insertions(+), 1187 deletions(-) create mode 100644 front/src/modules/pathfinding/helpers/__tests__/getPathVoltages.spec.ts create mode 100644 front/src/modules/pathfinding/helpers/getPathVoltages.ts create mode 100644 front/src/modules/powerRestriction/helpers/getRestrictionsToResize.ts create mode 100644 front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorBehaviours.ts create mode 100644 front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorData.ts create mode 100644 front/src/reducers/osrdconf/operationalStudiesConf/powerRestrictionReducer.ts rename front/src/reducers/osrdconf/{osrdConfCommon => operationalStudiesConf}/utils.ts (55%) diff --git a/front/public/locales/en/operationalStudies/manageTrainSchedule.json b/front/public/locales/en/operationalStudies/manageTrainSchedule.json index 3422f3f08eb..f85b96dc8ce 100644 --- a/front/public/locales/en/operationalStudies/manageTrainSchedule.json +++ b/front/public/locales/en/operationalStudies/manageTrainSchedule.json @@ -130,10 +130,11 @@ "electrification": "Electrification", "inconsistent_one": "The interval below has inconsistencies. The default speed effort curve will be used for this.", "inconsistent_other": "The intervals below contain inconsistencies. The default speed effort curves will be used for these.", - "powerRestrictionInvalidCombination": "- {{powerRestrictionCode}} is incompatible with electrification {{electrification}} of the path between {{begin}}m and {{end}}m", + "marginsAndPowerRestrictionsReset": "Margins and power restrictions have been reset.", "missingPowerRestriction": "- Missing power restriction between {{begin}}m and {{end}}m.", "modeNotHandled": "- No power restriction should be given between {{begin}}m and {{end}}m since the electrification mode {{electrification}} is not handled.", "pathfindingChange": "Pathfinding changed", - "marginsAndPowerRestrictionsReset": "Margins and power restrictions have been reset." + "powerRestrictionInvalidCombination": "- {{powerRestrictionCode}} is incompatible with electrification {{electrification}} of the path between {{begin}}m and {{end}}m", + "powerRestrictionsReset": "Power restrictions have been reset." } } diff --git a/front/public/locales/fr/operationalStudies/manageTrainSchedule.json b/front/public/locales/fr/operationalStudies/manageTrainSchedule.json index 6db025db1ba..8840d4bc495 100644 --- a/front/public/locales/fr/operationalStudies/manageTrainSchedule.json +++ b/front/public/locales/fr/operationalStudies/manageTrainSchedule.json @@ -131,10 +131,11 @@ "electrification": "Électrification", "inconsistent_one": "Un intervalle comporte des incohérences. La courbe effort vitesse par défaut sera utilisée pour celui-ci.", "inconsistent_other": "Plusieurs intervalles comportent des incohérences. Les courbes effort vitesse par défaut seront utilisées pour ceux-ci.", - "powerRestrictionInvalidCombination": "- Code {{powerRestrictionCode}} incompatible avec l'électrification à {{electrification}} de l'itinéraire entre {{begin}}m et {{end}}m. ", + "marginsAndPowerRestrictionsReset": "Les marges et les restrictions de puissance ont été réinitialisées.", "missingPowerRestriction": "- Restriction de puissance manquante entre {{begin}}m et {{end}}m.", "modeNotHandled": "- Aucune restriction de puissance ne devrait être renseignée entre {{begin}}m and {{end}}m comme le matériel roulant ne supporte pas l'électrification {{electrification}}.", "pathfindingChange": "Changement de chemin", - "marginsAndPowerRestrictionsReset": "Les marges et les restrictions de puissance ont été réinitialisées." + "powerRestrictionInvalidCombination": "- Code {{powerRestrictionCode}} incompatible avec l'électrification à {{electrification}} de l'itinéraire entre {{begin}}m et {{end}}m.", + "powerRestrictionsReset": "Les restrictions de puissance ont été réinitialisées." } } diff --git a/front/src/applications/operationalStudies/consts.ts b/front/src/applications/operationalStudies/consts.ts index 3f0b09e8637..88472ffe766 100644 --- a/front/src/applications/operationalStudies/consts.ts +++ b/front/src/applications/operationalStudies/consts.ts @@ -1,10 +1,15 @@ import type { Position } from 'geojson'; -import type { ElectrificationRange, ElectrificationUsage } from 'common/api/osrdEditoastApi'; +import type { + ElectrificationRange, + ElectrificationUsage, + TrainScheduleBase, +} 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'; +import type { ArrayElement } from 'utils/types'; export const BLOCKTYPES = [ { @@ -125,11 +130,7 @@ export type StudyType = typeof STUDY_TYPES; export type PowerRestrictionRange = LinearMetadataItem<{ value: string }>; -export type PowerRestrictionV2 = { - from: string; - to: string; - code: string; -}; +export type PowerRestrictionV2 = ArrayElement; // electrical profiles interface Profile { diff --git a/front/src/applications/operationalStudies/types.ts b/front/src/applications/operationalStudies/types.ts index 2151f136a18..f9a0f07ef79 100644 --- a/front/src/applications/operationalStudies/types.ts +++ b/front/src/applications/operationalStudies/types.ts @@ -3,6 +3,7 @@ import type { PathResponse, PathfindingResult, ProjectPathTrainResult, + RangedValue, SimulationResponse, } from 'common/api/osrdEditoastApi'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; @@ -108,6 +109,7 @@ export type PositionData = { position: number; }; +/** Start and stop are in meters */ export type ElectrificationRangeV2 = { electrificationUsage: ElectrificationUsageV2; start: number; @@ -145,15 +147,14 @@ export type ElectricalProfileValue = Extract< { status: 'success' } >['electrical_profiles']['values'][number]; -/** - * Electrifications start and stop are in meters - */ +/** Electrifications start and stop are in meters */ export type PathPropertiesFormatted = { electrifications: ElectrificationRangeV2[]; curves: PositionData<'radius'>[]; slopes: PositionData<'gradient'>[]; operationalPoints: NonNullable; geometry: NonNullable; + voltages: RangedValue[]; }; export type SimulationResponseSuccess = Extract; diff --git a/front/src/applications/operationalStudies/utils.ts b/front/src/applications/operationalStudies/utils.ts index 1c9d9c897b3..15a31a865ff 100644 --- a/front/src/applications/operationalStudies/utils.ts +++ b/front/src/applications/operationalStudies/utils.ts @@ -1,10 +1,7 @@ import { omit } from 'lodash'; -import type { - PathProperties, - ProjectPathTrainResult, - RangedValue, -} from 'common/api/osrdEditoastApi'; +import type { PathProperties, ProjectPathTrainResult } from 'common/api/osrdEditoastApi'; +import getPathVoltages from 'modules/pathfinding/helpers/getPathVoltages'; import { convertUTCDateToLocalDate, isoDateToMs } from 'utils/date'; import { mmToM } from 'utils/physics'; import { ms2sec } from 'utils/timeManipulation'; @@ -86,14 +83,6 @@ export const transformBoundariesDataToRangesData = < return formatedData; }; -export const addRange = (ranges: RangedValue[], begin: number, end: number, value: string) => { - ranges.push({ - begin, - end, - value, - }); -}; - export const formatElectrificationRanges = ( electrifications: ElectricalRangesData[], electricalProfiles: ElectricalRangesData[] @@ -191,12 +180,18 @@ export const preparePathPropertiesData = ( electricalProfilesRanges ); + const voltageRanges = getPathVoltages( + electrifications as NonNullable, + pathLength + ); + return { electrifications: electrificationRanges, curves: formattedCurves, slopes: formattedSlopes, operationalPoints: operational_points as NonNullable, geometry: geometry as NonNullable, + voltages: voltageRanges, }; }; diff --git a/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx b/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx index bcdf8c30da4..0a6dbf79cb1 100644 --- a/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx +++ b/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx @@ -4,20 +4,16 @@ import { compact } from 'lodash'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; -import type { - ElectrificationVoltage, - ManageTrainSchedulePathProperties, -} from 'applications/operationalStudies/types'; -import { addRange } from 'applications/operationalStudies/utils'; +import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; import allowancesPic from 'assets/pictures/components/allowances.svg'; import pahtFindingPic from 'assets/pictures/components/pathfinding.svg'; import simulationSettings from 'assets/pictures/components/simulationSettings.svg'; import rollingStockPic from 'assets/pictures/components/train.svg'; -import type { IntervalItem } from 'common/IntervalsEditor/types'; import { useOsrdConfSelectors } from 'common/osrdContext'; import { useStoreDataForSpeedLimitByTagSelector } from 'common/SpeedLimitByTagSelector/useStoreDataForSpeedLimitByTagSelector'; import Tabs from 'common/Tabs'; import ItineraryV2 from 'modules/pathfinding/components/Itinerary/ItineraryV2'; +import getPathVoltages from 'modules/pathfinding/helpers/getPathVoltages'; import { upsertViasInOPs } from 'modules/pathfinding/utils'; import PowerRestrictionsSelectorV2 from 'modules/powerRestriction/components/PowerRestrictionsSelectorV2'; import RollingStock2Img from 'modules/rollingStock/components/RollingStock2Img'; @@ -29,8 +25,6 @@ import { Map } from 'modules/trainschedule/components/ManageTrainSchedule'; import SimulationSettings from 'modules/trainschedule/components/ManageTrainSchedule/SimulationSettings'; import TrainSettings from 'modules/trainschedule/components/ManageTrainSchedule/TrainSettings'; import { formatKmValue } from 'utils/strings'; -import { upsertViasInOPs } from 'modules/pathfinding/utils'; -import { compact } from 'lodash'; const ManageTrainScheduleV2 = () => { const { t } = useTranslation(['operationalStudies/manageTrainSchedule']); @@ -61,67 +55,12 @@ const ManageTrainScheduleV2 = () => { allWaypoints, }); } - }, []) - - const [initialPowerRestrictions, setInitialPowerRestrictions] = useState([]); - - const isElectrification = ( - value: ElectrificationVoltage - ): value is { type: 'electrification'; voltage: string } => value.type === 'electrification'; + }, []); - type RangedValueV2 = { - begin: number; - end: number; - value: string; - }; - - useEffect(() => { - if (pathProperties) { - const allVias = upsertViasInOPs( - pathProperties.suggestedOperationalPoints, - compact(pathSteps) - ); - setPathProperties({ - ...pathProperties, - allVias, - }); - } - }, [pathSteps]); - - const pathElectrificationRanges = (): IntervalItem[] => { - if (!pathProperties || !pathProperties.electrifications) return []; - - const boundaries = [0, ...pathProperties.electrifications.boundaries, pathProperties.length]; - const values = [...pathProperties.electrifications.values]; - - const ranges: RangedValueV2[] = []; - let start = boundaries[0]; - let currentVoltage = isElectrification(values[0]) ? values[0].voltage : ''; - - for (let i = 1; i < values.length; i += 1) { - const currentValue = values[i]; - if (isElectrification(currentValue) && currentValue.voltage !== currentVoltage) { - addRange(ranges, start, boundaries[i], currentVoltage); - start = boundaries[i]; - currentVoltage = currentValue.voltage; - } - } - - // Add the last segment - addRange(ranges, start, boundaries[boundaries.length - 1], currentVoltage); - setInitialPowerRestrictions(ranges); - return ranges; - }; - - useMemo(() => { - if (!pathProperties) return []; - - return pathElectrificationRanges().map((electrificationRange) => ({ - begin: electrificationRange.begin, - end: electrificationRange.end, - value: `${electrificationRange.value}`, - })); - }, [pathProperties]); + const voltageRanges = useMemo( + () => getPathVoltages(pathProperties?.electrifications, pathProperties?.length), + [pathProperties] + ); const tabRollingStock = { id: 'rollingstock', @@ -211,11 +150,11 @@ const ManageTrainScheduleV2 = () => { dispatchUpdateSpeedLimitByTag={dispatchUpdateSpeedLimitByTag} constraintDistribution={constraintDistribution} /> - {rollingStock && isElectric(rollingStock.effort_curves.modes) && ( + {rollingStock && isElectric(rollingStock.effort_curves.modes) && pathProperties && ( )} diff --git a/front/src/applications/stdcmV2/components/StdcmVias.tsx b/front/src/applications/stdcmV2/components/StdcmVias.tsx index 4dad1c84af4..72a91b97531 100644 --- a/front/src/applications/stdcmV2/components/StdcmVias.tsx +++ b/front/src/applications/stdcmV2/components/StdcmVias.tsx @@ -30,7 +30,7 @@ const StdcmVias = ({ disabled = false, setCurrentSimulationInputs }: StdcmConfig const updatePathStepsList = (pathStep: PathStep | null, index: number) => { const newPathSteps = replaceElementAtIndex(pathSteps, index, pathStep); - dispatch(updatePathSteps(newPathSteps)); + dispatch(updatePathSteps({ pathSteps: newPathSteps })); }; const updatePathStepStopTime = (stopTime: string, index: number) => { @@ -95,7 +95,7 @@ const StdcmVias = ({ disabled = false, setCurrentSimulationInputs }: StdcmConfig Icon={} onClick={() => { const newPathSteps = addElementAtIndex(pathSteps, pathSteps.length - 1, null); - dispatch(updatePathSteps(newPathSteps)); + dispatch(updatePathSteps({ pathSteps: newPathSteps })); }} /> diff --git a/front/src/common/IntervalsEditor/IntervalsEditor.tsx b/front/src/common/IntervalsEditor/IntervalsEditor.tsx index a72c757d30c..07e7d057bf5 100644 --- a/front/src/common/IntervalsEditor/IntervalsEditor.tsx +++ b/front/src/common/IntervalsEditor/IntervalsEditor.tsx @@ -30,7 +30,7 @@ import { import { createEmptySegmentAt, removeSegment } from './utils'; import ZoomButtons from './ZoomButtons'; -type IntervalsEditorProps = { +export type IntervalsEditorProps = { /** Additionnal read-only data that will be displayed along the path, below the intervals editor */ additionalData?: AdditionalDataItem[]; /** Default value used when a new range is created */ @@ -54,9 +54,8 @@ type IntervalsEditorProps = { disableDrag?: boolean; onResizeFromInput?: ( intervalIndex: number, - newEnd: number, context: 'begin' | 'end', - newBegin: number + newPosition: number ) => void; } & ( | { diff --git a/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx b/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx index dd01a8f6951..7f5ffbc1a62 100644 --- a/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx +++ b/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx @@ -6,6 +6,7 @@ import DebouncedNumberInputSNCF from 'common/BootstrapSNCF/FormSNCF/DebouncedNum import { fixLinearMetadataItems, resizeSegment } from 'common/IntervalsDataViz/data'; import { notEmpty } from 'common/IntervalsDataViz/utils'; +import type { IntervalsEditorProps } from './IntervalsEditor'; import type { IntervalItem } from './types'; type IntervalsEditorFormProps = { @@ -13,12 +14,7 @@ type IntervalsEditorFormProps = { interval: IntervalItem; selectedIntervalIndex: number; setData: (newData: IntervalItem[], selectedIntervalIndex?: number) => void; - onInputChange?: ( - intervalIndex: number, - newEnd: number, - context: 'begin' | 'end', - newBegin: number - ) => void; + onInputChange?: IntervalsEditorProps['onResizeFromInput']; setSelectedIntervalIndex: (selectedIntervalIndex: number) => void; totalLength: number; defaultValue: string | number; @@ -62,12 +58,7 @@ const IntervalsEditorCommonForm = ({ defaultValue, }); if (onInputChange) { - onInputChange( - selectedIntervalIndex, - context === 'end' ? newPosition : interval.end, - context, - context === 'begin' ? newPosition : interval.begin - ); // Pass newBegin when context is 'begin' + onInputChange(selectedIntervalIndex, context, newPosition); // Pass newBegin when context is 'begin' } else { setData(fixedResults, selectedIntervalIndex); } diff --git a/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx b/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx index 9b976e3f719..a5afe6c3ba8 100644 --- a/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx +++ b/front/src/modules/pathfinding/components/Itinerary/ItineraryV2.tsx @@ -61,12 +61,12 @@ const ItineraryV2 = ({ const inverseOD = () => { const revertedPathSteps = [...pathSteps].reverse(); - dispatch(updatePathSteps(revertedPathSteps)); + dispatch(updatePathSteps({ pathSteps: revertedPathSteps, resetPowerRestrictions: true })); }; const resetPathfinding = () => { setPathProperties(undefined); - dispatch(updatePathSteps([null, null])); + dispatch(updatePathSteps({ pathSteps: [null, null], resetPowerRestrictions: true })); }; useEffect(() => { diff --git a/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx b/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx index 8dea4c4e3c6..b6de0193edf 100644 --- a/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx +++ b/front/src/modules/pathfinding/components/Itinerary/ModalSuggestedVias.tsx @@ -37,12 +37,12 @@ const ModalSuggestedVias = ({ suggestedVias }: ModalSuggestedViasProps) => { const removeViaFromPath = (op: SuggestedOP) => { const updatedPathSteps = [...pathSteps]; dispatch( - updatePathSteps( - compact(updatedPathSteps).filter( + updatePathSteps({ + pathSteps: compact(updatedPathSteps).filter( (step) => ('uic' in step && step.uic !== op.uic) || ('track' in step && step.track !== op.track) - ) - ) + ), + }) ); }; diff --git a/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx b/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx index 6d425725dcb..950624017d3 100644 --- a/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx +++ b/front/src/modules/pathfinding/components/Pathfinding/TypeAndPathV2.tsx @@ -241,7 +241,7 @@ const TypeAndPathV2 = ({ setPathProperties }: PathfindingProps) => { stopFor: i === opList.length - 1 ? '0' : undefined, }; }); - dispatch(updatePathSteps(pathSteps)); + dispatch(updatePathSteps({ pathSteps, resetPowerRestrictions: true })); } } // TODO TS2 : test errors display after core / editoast connexion for pathProperties diff --git a/front/src/modules/pathfinding/helpers/__tests__/getPathVoltages.spec.ts b/front/src/modules/pathfinding/helpers/__tests__/getPathVoltages.spec.ts new file mode 100644 index 00000000000..2200c8574a1 --- /dev/null +++ b/front/src/modules/pathfinding/helpers/__tests__/getPathVoltages.spec.ts @@ -0,0 +1,137 @@ +import type { PathProperties } from 'common/api/generatedEditoastApi'; + +import getPathVoltages from '../getPathVoltages'; + +const pathLength = 10; + +describe('getPathVoltages', () => { + it('should return 1 range if 1500V - neutral - 1500V', () => { + const electrifications: NonNullable = { + boundaries: [4, 6], + values: [ + { type: 'electrification', voltage: '1500V' }, + { type: 'neutral_section', lower_pantograph: true }, + { type: 'electrification', voltage: '1500V' }, + ], + }; + + const expectedResult = [{ begin: 0, end: 10, value: '1500V' }]; + + const result = getPathVoltages(electrifications, pathLength); + expect(result).toEqual(expectedResult); + }); + + it('should return 3 ranges if 1500V - neutral - 25000V', () => { + const electrifications: NonNullable = { + boundaries: [4, 6], + values: [ + { type: 'electrification', voltage: '1500V' }, + { type: 'neutral_section', lower_pantograph: true }, + { type: 'electrification', voltage: '25000V' }, + ], + }; + + const expectedResult = [ + { begin: 0, end: 4, value: '1500V' }, + { begin: 4, end: 6, value: '' }, + { begin: 6, end: 10, value: '25000V' }, + ]; + + const result = getPathVoltages(electrifications, pathLength); + expect(result).toEqual(expectedResult); + }); + + it('should return 3 ranges if 1500V - non electrified - 25000V', () => { + const electrifications: NonNullable = { + boundaries: [4, 6], + values: [ + { type: 'electrification', voltage: '1500V' }, + { type: 'non_electrified' }, + { type: 'electrification', voltage: '25000V' }, + ], + }; + + const expectedResult = [ + { begin: 0, end: 4, value: '1500V' }, + { begin: 4, end: 6, value: '' }, + { begin: 6, end: 10, value: '25000V' }, + ]; + + const result = getPathVoltages(electrifications, pathLength); + expect(result).toEqual(expectedResult); + }); + + it('should return 3 ranges if 1500V - non electrified - 1500V', () => { + const electrifications: NonNullable = { + boundaries: [4, 6], + values: [ + { type: 'electrification', voltage: '1500V' }, + { type: 'non_electrified' }, + { type: 'electrification', voltage: '1500V' }, + ], + }; + + const expectedResult = [ + { begin: 0, end: 4, value: '1500V' }, + { begin: 4, end: 6, value: '' }, + { begin: 6, end: 10, value: '1500V' }, + ]; + + const result = getPathVoltages(electrifications, pathLength); + expect(result).toEqual(expectedResult); + }); + + it('should return 2 range if non electrified - 1500V', () => { + const electrifications: NonNullable = { + boundaries: [5], + values: [{ type: 'non_electrified' }, { type: 'electrification', voltage: '1500V' }], + }; + + const expectedResult = [ + { begin: 0, end: 5, value: '' }, + { begin: 5, end: 10, value: '1500V' }, + ]; + + const result = getPathVoltages(electrifications, pathLength); + expect(result).toEqual(expectedResult); + }); + + it('should return 2 range if neutral - non electrified - 1500V', () => { + const electrifications: NonNullable = { + boundaries: [2, 5], + values: [ + { type: 'neutral_section', lower_pantograph: true }, + { type: 'non_electrified' }, + { type: 'electrification', voltage: '1500V' }, + ], + }; + + const expectedResult = [ + { begin: 0, end: 5, value: '' }, + { begin: 5, end: 10, value: '1500V' }, + ]; + + const result = getPathVoltages(electrifications, pathLength); + expect(result).toEqual(expectedResult); + }); + + it('should return 2 range if non electrified - 1500V - neutral', () => { + const electrifications: NonNullable = { + boundaries: [2, 8], + values: [ + { type: 'non_electrified' }, + { type: 'electrification', voltage: '1500V' }, + { type: 'neutral_section', lower_pantograph: true }, + ], + }; + + const expectedResult = [ + { begin: 0, end: 2, value: '' }, + { begin: 2, end: 8, value: '1500V' }, + { begin: 8, end: 10, value: '' }, + ]; + + const result = getPathVoltages(electrifications, pathLength); + expect(result).toEqual(expectedResult); + }); +}); diff --git a/front/src/modules/pathfinding/helpers/getPathVoltages.ts b/front/src/modules/pathfinding/helpers/getPathVoltages.ts new file mode 100644 index 00000000000..ff91b3ec131 --- /dev/null +++ b/front/src/modules/pathfinding/helpers/getPathVoltages.ts @@ -0,0 +1,108 @@ +import type { ElectrificationVoltage } from 'applications/operationalStudies/types'; +import type { PathProperties, RangedValue } from 'common/api/osrdEditoastApi'; + +const isElectrification = ( + value: ElectrificationVoltage +): value is { type: 'electrification'; voltage: string } => value.type === 'electrification'; + +const isNeutralSection = ( + value: ElectrificationVoltage +): value is { + lower_pantograph: boolean; + type: 'neutral_section'; +} => value.type === 'neutral_section'; + +const isNonElectrified = (value: ElectrificationVoltage): value is { type: 'non_electrified' } => + value.type === 'non_electrified'; + +/** + * Given electrifications on path, return the list of voltages on the path ranges + * + * Filter the neutral sections and group the ranges + */ +const getPathVoltages = ( + electrifications?: NonNullable, + pathLength?: number +): RangedValue[] => { + if (!electrifications || !pathLength) return []; + + const boundaries = [...electrifications.boundaries, pathLength]; + + const ranges: RangedValue[] = []; + let start = 0; + let currentVoltage: string = ''; + + electrifications.values.forEach((electrification, index) => { + if (isNonElectrified(electrification)) { + // add the previous range + if (currentVoltage) { + ranges.push({ + begin: start, + end: boundaries[index - 1], + value: currentVoltage, + }); + currentVoltage = ''; + start = index === 0 ? 0 : boundaries[index - 1]; + } + } + + if (!currentVoltage && isElectrification(electrification)) { + // add a non electrified range + if (index > 0) { + ranges.push({ + begin: start, + end: boundaries[index - 1], + value: '', + }); + } + currentVoltage = electrification.voltage; + start = index === 0 ? 0 : boundaries[index - 1]; + } + + // add the last range + if (index === electrifications.values.length - 1) { + ranges.push({ + begin: start, + end: pathLength, + value: currentVoltage, + }); + return; + } + + if (!currentVoltage) return; + + // if 2 electrifications not separated by a neutral section + if (isElectrification(electrification) && electrification.voltage !== currentVoltage) { + // add the previous range + ranges.push({ + begin: start, + end: boundaries[index], + value: currentVoltage, + }); + start = boundaries[index]; + currentVoltage = electrification.voltage; + return; + } + + // if electrification is a neutral section + if (isNeutralSection(electrification)) { + // add the range if the following range is not an electrification or has not the same voltage than currentVoltage + const nextElectrification = electrifications.values[index + 1]; + if ( + !isElectrification(nextElectrification) || + (isElectrification(nextElectrification) && nextElectrification.voltage !== currentVoltage) + ) { + ranges.push({ + begin: start, + end: boundaries[index - 1], + value: currentVoltage, + }); + currentVoltage = ''; + start = boundaries[index - 1]; + } + } + }); + return ranges; +}; + +export default getPathVoltages; diff --git a/front/src/modules/pathfinding/hook/usePathfinding.ts b/front/src/modules/pathfinding/hook/usePathfinding.ts index 321a3fc0b01..63d14d0d8db 100644 --- a/front/src/modules/pathfinding/hook/usePathfinding.ts +++ b/front/src/modules/pathfinding/hook/usePathfinding.ts @@ -21,7 +21,7 @@ import { } from 'modules/pathfinding/utils'; import { useStoreDataForRollingStockSelector } from 'modules/rollingStock/components/RollingStockSelector/useStoreDataForRollingStockSelector'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; -import { setFailure } from 'reducers/main'; +import { setFailure, setWarning } from 'reducers/main'; import type { PathStep } from 'reducers/osrdconf/types'; import { useAppDispatch } from 'store'; import { castErrorToFailure } from 'utils/error'; @@ -138,15 +138,8 @@ export const usePathfindingV2 = ( ) => { const { t } = useTranslation(['operationalStudies/manageTrainSchedule']); const dispatch = useAppDispatch(); - const { - getInfraID, - getOriginV2, - getDestinationV2, - getViasV2, - getPathSteps, - // getPowerRestrictionRanges, - // getAllowances, - } = useOsrdConfSelectors(); + const { getInfraID, getOriginV2, getDestinationV2, getViasV2, getPathSteps } = + useOsrdConfSelectors(); const infraId = useSelector(getInfraID, isEqual); const origin = useSelector(getOriginV2, isEqual); const destination = useSelector(getDestinationV2, isEqual); @@ -154,9 +147,7 @@ export const usePathfindingV2 = ( const pathSteps = useSelector(getPathSteps); const { infra, reloadCount, setIsInfraError } = useInfraStatus(); const { rollingStock } = useStoreDataForRollingStockSelector(); - // TODO TS2 : update this parts in margins and powerrestriction issues - // const powerRestrictions = useSelector(getPowerRestrictionRanges, isEqual); - // const allowances = useSelector(getAllowances, isEqual); + const initializerArgs = { origin, destination, @@ -173,11 +164,7 @@ export const usePathfindingV2 = ( const [postPathProperties] = osrdEditoastApi.endpoints.postV2InfraByInfraIdPathProperties.useMutation(); - const { - updatePathSteps, - // updatePowerRestrictionRanges, - // updateAllowances, - } = useOsrdConfActions(); + const { updatePathSteps } = useOsrdConfActions(); const generatePathfindingParams = (): PostV2InfraByInfraIdPathfindingBlocksApiArg | null => { setPathProperties(undefined); @@ -266,7 +253,15 @@ export const usePathfindingV2 = ( }), }; }); - dispatch(updatePathSteps(updatedPathSteps)); + dispatch( + updatePathSteps({ pathSteps: updatedPathSteps, resetPowerRestrictions: true }) + ); + dispatch( + setWarning({ + title: t('warningMessages.pathfindingChange'), + text: t('warningMessages.marginsAndPowerRestrictionsReset'), + }) + ); const allWaypoints = upsertViasInOPs( suggestedOperationalPoints, @@ -279,21 +274,10 @@ export const usePathfindingV2 = ( suggestedOperationalPoints, allWaypoints, length: pathfindingResult.length, + trackSectionRanges: pathfindingResult.track_section_ranges, }); pathfindingDispatch({ type: 'PATHFINDING_FINISHED' }); - - // TODO TS2 : adapt this in margins and power restrictions issues - // * if (!isEmptyArray(powerRestrictions) || !isEmptyArray(allowances)) { - // * dispatch(updatePowerRestrictionRanges([])); - // * dispatch(updateAllowances([])); - // * dispatch( - // * setWarning({ - // * title: t('warningMessages.pathfindingChange'), - // * text: t('warningMessages.marginsAndPowerRestrictionsReset'), - // * }) - // * ); - // * } } } else { pathfindingDispatch({ diff --git a/front/src/modules/pathfinding/utils.ts b/front/src/modules/pathfinding/utils.ts index 12627850acc..b66b0e238da 100644 --- a/front/src/modules/pathfinding/utils.ts +++ b/front/src/modules/pathfinding/utils.ts @@ -56,6 +56,7 @@ export const getPathfindingQuery = ({ if ('track' in step) { return { track: step.track, + // TODO: step offset should be in mm in the store /!\ // pathfinding blocks endpoint requires offsets in mm offset: step.offset * 1000, }; diff --git a/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx b/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx index 2ffa597823b..321ecd69ff3 100644 --- a/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx +++ b/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx @@ -1,351 +1,60 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React from 'react'; import { Alert } from '@osrd-project/ui-icons'; -import { compact, isEmpty, keyBy, last } from 'lodash'; +import { isEmpty } from 'lodash'; import { useTranslation } from 'react-i18next'; -import nextId from 'react-id-generator'; -import { useSelector } from 'react-redux'; -import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; import icon from 'assets/pictures/components/power_restrictions.svg'; -import type { RangedValue, RollingStock, TrackRange } from 'common/api/osrdEditoastApi'; +import type { RangedValue, RollingStock } from 'common/api/osrdEditoastApi'; import IntervalsEditor from 'common/IntervalsEditor/IntervalsEditor'; import { INTERVAL_TYPES } from 'common/IntervalsEditor/types'; -import type { IntervalItem } from 'common/IntervalsEditor/types'; -import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; +import { useOsrdConfActions } from 'common/osrdContext'; +import type { OperationalStudiesConfSliceActions } from 'reducers/osrdconf/operationalStudiesConf'; import { useAppDispatch } from 'store'; -import { getPointCoordinates } from 'utils/geometry'; -import { mToMm } from 'utils/physics'; +import { mmToM } from 'utils/physics'; import { NO_POWER_RESTRICTION } from '../consts'; -import createPathStep from '../helpers/createPathStep'; -import formatPowerRestrictions from '../helpers/formatPowerRestrictions'; -import { getPowerRestrictionsWarnings, countWarnings } from '../helpers/powerRestrictionWarnings'; -import { getPathStep, getPowerRestriction, getPowerRestrictionBis } from '../helpers/utils'; - -const DEFAULT_SEGMENT_LENGTH = 1000; +import usePowerRestrictionSelector from '../hooks/usePowerRestrictionSelectorData'; type PowerRestrictionsSelectorProps = { - pathElectrificationRanges: RangedValue[]; - rollingStockPowerRestrictions: RollingStock['power_restrictions']; + voltageRanges: RangedValue[]; rollingStockModes: RollingStock['effort_curves']['modes']; - pathProperties: ManageTrainSchedulePathProperties | undefined; + rollingStockPowerRestrictions: RollingStock['power_restrictions']; + pathProperties: ManageTrainSchedulePathProperties; }; const PowerRestrictionsSelectorV2 = ({ - pathElectrificationRanges, + voltageRanges, rollingStockModes, rollingStockPowerRestrictions, pathProperties, }: PowerRestrictionsSelectorProps) => { const { t } = useTranslation(['operationalStudies/manageTrainSchedule']); - const dispatch = useAppDispatch(); - const { getPowerRestrictionV2, getPathSteps } = useOsrdConfSelectors(); - const { - upsertPowerRestrictionRangesV2, - resetPowerRestrictionRangesV2, - cutPowerRestrictionRangesV2, - deletePowerRestrictionRangesV2, - resizeSegmentEndInput, - resizeSegmentBeginInput, - } = useOsrdConfActions(); - const powerRestrictionRanges = useSelector(getPowerRestrictionV2); - const pathSteps = compact(useSelector(getPathSteps)); - - // pathSteps + electrification change points - - const [cutPositions, setCutPositions] = useState([]); - const [intervalsEditorData, setIntervalsEditorData] = useState([]); - - const pathLength = useMemo(() => { - const lastPathSegment = last(pathElectrificationRanges); - return lastPathSegment ? lastPathSegment.end : DEFAULT_SEGMENT_LENGTH; - }, [pathElectrificationRanges]); - - const electrificationChangePoints = useMemo(() => { - const specialPoints = pathElectrificationRanges.map((range) => ({ - position: range.end, - })); - specialPoints.pop(); - return specialPoints; - }, [pathElectrificationRanges]); - - const cumulativeSums = useMemo( - () => - pathProperties?.trackSectionRanges.reduce((acc, section) => { - const lastSum = acc.length > 0 ? acc[acc.length - 1] : 0; - const adjustedEnd = section.end - (section.begin > 0 ? section.begin : 0); - acc.push(lastSum + adjustedEnd); - return acc; - }, []) || [], - [pathProperties] - ); - - const powerRestrictionOptions = useMemo( - () => [NO_POWER_RESTRICTION, ...Object.keys(rollingStockPowerRestrictions)], - [rollingStockPowerRestrictions] - ); - - const findTrackSectionIndex = (position: number) => { - for (let i = 0; i < cumulativeSums.length; i += 1) { - if (position <= cumulativeSums[i]) { - return i; - } - } - return -1; - }; - - const findTrackSection = (position: number) => { - const index = findTrackSectionIndex(position); - return index !== -1 ? pathProperties?.trackSectionRanges[index] : null; - }; - - const calculateOffset = (trackSectionRange: TrackRange, position: number) => { - const index = findTrackSectionIndex(position); - const inferiorSum = cumulativeSums[index]; - return trackSectionRange.direction === 'START_TO_STOP' - ? inferiorSum - position - : position - inferiorSum; - }; - - const editPowerRestrictionRanges = ( - newPowerRestrictionRanges: IntervalItem[], - selectedIntervalIndex?: number - ) => { - if (selectedIntervalIndex === undefined) { - return; - } - const newDispatchedValue = newPowerRestrictionRanges[selectedIntervalIndex]; - - let fromPathStep = pathSteps.find( - (step) => step.positionOnPath === mToMm(newDispatchedValue.begin) - ); - if (!fromPathStep) { - fromPathStep = createPathStep( - mToMm(newDispatchedValue.begin), - cumulativeSums, - pathProperties, - pathSteps - ); - } - let toPathStep = pathSteps.find( - (step) => step.positionOnPath === mToMm(newDispatchedValue.end) - ); - if (!toPathStep) { - toPathStep = createPathStep( - mToMm(newDispatchedValue.end), - cumulativeSums, - pathProperties, - pathSteps - ); - } - - if (fromPathStep && toPathStep) { - if (newDispatchedValue.value !== NO_POWER_RESTRICTION) { - dispatch( - upsertPowerRestrictionRangesV2({ - from: fromPathStep, - to: toPathStep, - code: newDispatchedValue.value.toString(), - }) - ); - } else { - dispatch(deletePowerRestrictionRangesV2({ from: fromPathStep, to: toPathStep })); - } - } - }; - - const cutPowerRestrictionRange = (rawCutAtPosition: number) => { - const cutAtPosition = mToMm(rawCutAtPosition); - const intervalCut = intervalsEditorData.find( - (interval) => interval.begin <= cutAtPosition && interval.end >= cutAtPosition - ); - if (!intervalCut || !pathProperties || intervalCut.value === NO_POWER_RESTRICTION) { - const newCutPositions = !cutPositions.length - ? [cutAtPosition] - : cutPositions.flatMap((position, index) => { - if (position > cutAtPosition) { - return [cutAtPosition, position]; - } - if (index === cutPositions.length - 1) { - return [position, cutAtPosition]; - } - return [position]; - }); - setCutPositions(newCutPositions); - return; - } - - const trackSectionRangeAtCut = findTrackSection(cutAtPosition); - - if (trackSectionRangeAtCut) { - const offsetAtCut = calculateOffset(trackSectionRangeAtCut, cutAtPosition); - const coordinatesAtCut = getPointCoordinates( - pathProperties.geometry, - pathLength, - cutAtPosition - ); - const cutAt = { - id: nextId(), - positionOnPath: cutAtPosition, - coordinates: coordinatesAtCut, - track: trackSectionRangeAtCut.track_section, - offset: offsetAtCut, - }; - - dispatch(cutPowerRestrictionRangesV2({ cutAt })); - } - }; - - const deletePowerRestrictionRange = (from: number, to: number) => { - const fromPathStep = pathSteps.find((step) => step.positionOnPath === mToMm(from)); - const toPathStep = pathSteps.find((step) => step.positionOnPath === mToMm(to)); - - if (fromPathStep && toPathStep) { - dispatch(deletePowerRestrictionRangesV2({ from: fromPathStep, to: toPathStep })); - } - }; - const resizeSegments = ( - selectedSegmentIndex: number, - newEnd: number, - context: 'begin' | 'end', - newBegin: number - ) => { - const selectedRangeData = intervalsEditorData[selectedSegmentIndex]; - const selectedRestriction = getPowerRestrictionBis( - pathSteps, - powerRestrictionRanges, - selectedRangeData - ); - - // récupérer l'autre range qui est modifiée - const { rangeData: otherRangeData, powerRestrictionRange: otherRestriction } = - getPowerRestriction( - pathSteps, - powerRestrictionRanges, - intervalsEditorData, - context === 'end' ? newEnd : newBegin, - selectedSegmentIndex, - context - ); - - if (!selectedRestriction) return; - - if (context === 'end') { - // trouver la restriction qui correspond seulement si la value de la rangeData n'est pas NO_POWER_RESTRICTION - let firstRestriction: PowerRestrictionV2 = selectedRestriction; - let secondRestriction: PowerRestrictionV2 | undefined = otherRestriction; - - if (otherRangeData && otherRestriction && otherRangeData.begin < selectedRangeData.begin) { - firstRestriction = otherRestriction; - secondRestriction = selectedRestriction; - } - - // secondRestriction will be null if no restriction is found - let newEndPathStep = getPathStep(pathSteps, mToMm(newEnd)); - - if (!newEndPathStep) { - newEndPathStep = createPathStep(mToMm(newEnd), cumulativeSums, pathProperties, pathSteps); - } - - if (newEndPathStep) { - dispatch( - resizeSegmentEndInput({ - firstRestriction, - secondRestriction, - newEndPathStep, - }) - ); - } - } - if (context === 'begin') { - // trouver la restriction qui correspond seulement si la value de la rangeData n'est pas NO_POWER_RESTRICTION - let firstRestriction: PowerRestrictionV2 | undefined = otherRestriction; - let secondRestriction: PowerRestrictionV2 = selectedRestriction; - - if (otherRangeData && otherRestriction && otherRangeData.begin > selectedRangeData.begin) { - firstRestriction = selectedRestriction; - secondRestriction = otherRestriction; - } - - let newFromPathStep = getPathStep(pathSteps, mToMm(newBegin)); - if (!newFromPathStep) { - newFromPathStep = createPathStep( - mToMm(newBegin), - cumulativeSums, - pathProperties, - pathSteps - ); - } - - if (newFromPathStep) { - dispatch( - resizeSegmentBeginInput({ - firstRestriction: secondRestriction, - previousRestriction: firstRestriction, - newFromPathStep, - }) - ); - } - } - }; - - const formatElectricalRanges = ( - ranges: PowerRestrictionV2[] - ): { begin: number; end: number; value: string }[] => { - const pathStepsById = keyBy(pathSteps, 'id'); - const formattedRanges = compact( - ranges.map((range) => { - const begin = pathStepsById[range.from]?.positionOnPath; - const end = pathStepsById[range.to]?.positionOnPath; - - if (begin !== undefined && end !== undefined) { - return { - begin, - end, - value: range.code, - }; - } - return null; - }) - ); - return formattedRanges; - }; - - /** Check the compatibility between the powerRestrictionRanges and the electrifications */ - const powerRestrictionsWarnings = useMemo( - () => - !isEmpty(rollingStockPowerRestrictions) && - !isEmpty(pathElectrificationRanges) && - !isEmpty(powerRestrictionRanges) - ? getPowerRestrictionsWarnings( - formatElectricalRanges(powerRestrictionRanges), - pathElectrificationRanges, - rollingStockModes - ) - : undefined, - [powerRestrictionRanges] - ); + const dispatch = useAppDispatch(); + const { resetPowerRestrictionRangesV2 } = + useOsrdConfActions() as OperationalStudiesConfSliceActions; - const totalPowerRestrictionWarnings = useMemo( - () => countWarnings(powerRestrictionsWarnings), - [powerRestrictionsWarnings] + const { + ranges, + compatibleVoltageRanges, + electrificationChangePoints, + pathLength, // in meters + powerRestrictionOptions, + warnings, + warningsNb, + resizeSegments, + deletePowerRestrictionRange, + cutPowerRestrictionRange, + editPowerRestrictionRanges, + } = usePowerRestrictionSelector( + voltageRanges, + rollingStockPowerRestrictions, + rollingStockModes, + pathProperties ); - useEffect(() => { - if (pathProperties) { - const newIntervalEditorData = formatPowerRestrictions( - powerRestrictionRanges, - [...electrificationChangePoints.map(({ position }) => position), ...cutPositions], - compact(pathSteps), - pathProperties?.length - ); - setIntervalsEditorData(newIntervalEditorData); - } - }, [electrificationChangePoints, cutPositions, powerRestrictionRanges]); - return (
@@ -354,57 +63,57 @@ const PowerRestrictionsSelectorV2 = ({ {!isEmpty(rollingStockPowerRestrictions) ? ( <>

{t('powerRestrictionExplanationText')}

- {totalPowerRestrictionWarnings > 0 && ( + {warningsNb > 0 && (
- {t('warningMessages.inconsistent', { count: totalPowerRestrictionWarnings })} + {t('warningMessages.inconsistent', { count: warningsNb })}
- {powerRestrictionsWarnings && - powerRestrictionsWarnings.modeNotSupportedWarnings.map( - ({ begin, end, electrification }) => ( - - {t('warningMessages.modeNotHandled', { begin, end, electrification })} - - ) - )} - - {powerRestrictionsWarnings && - Object.values(powerRestrictionsWarnings.invalidCombinationWarnings).map( + {warnings && + warnings.modeNotSupportedWarnings.map(({ begin, end, electrification }) => ( + + {t('warningMessages.modeNotHandled', { + begin: mmToM(begin), + end: mmToM(end), + electrification, + })} + + ))} + + {warnings && + Object.values(warnings.invalidCombinationWarnings).map( ({ powerRestrictionCode, electrification, begin, end }) => ( {t('warningMessages.powerRestrictionInvalidCombination', { powerRestrictionCode, electrification, - begin, - end, - })} - - ) - )} - {powerRestrictionsWarnings && - powerRestrictionsWarnings.missingPowerRestrictionWarnings.map( - ({ begin, end }) => ( - - {t('warningMessages.missingPowerRestriction', { - begin, - end, + begin: mmToM(begin), + end: mmToM(end), })} ) )} + {warnings && + warnings.missingPowerRestrictionWarnings.map(({ begin, end }) => ( + + {t('warningMessages.missingPowerRestriction', { + begin: mmToM(begin), + end: mmToM(end), + })} + + ))}
)} { it('should properly format power restrictions ranges with handled property', () => { const result = addHandledToPowerRestrictions( powerRestrictionRanges, - electrificationRangesForPowerRestrictions, + voltageRangesForPowerRestrictions, effortCurves ); diff --git a/front/src/modules/powerRestriction/helpers/__tests__/sampleData.ts b/front/src/modules/powerRestriction/helpers/__tests__/sampleData.ts index 6b8b7ffa7bd..ab0017c176e 100644 --- a/front/src/modules/powerRestriction/helpers/__tests__/sampleData.ts +++ b/front/src/modules/powerRestriction/helpers/__tests__/sampleData.ts @@ -1,4 +1,3 @@ -import type { ElectrificationRangeV2 } from 'applications/operationalStudies/types'; import type { EffortCurves, RangedValue, @@ -456,52 +455,31 @@ export const formattedPowerRestrictionRanges: Omit[] = [ { start: 0, - stop: 1, + stop: 1000, code: 'code1', }, { - start: 2, - stop: 3, + start: 2000, + stop: 3000, code: 'code2', }, { - start: 3, - stop: 4, + start: 3000, + stop: 4000, code: 'code1', }, ]; -export const electrificationRangesForPowerRestrictions: ElectrificationRangeV2[] = [ - { - start: 0, - stop: 2, - electrificationUsage: { - type: 'electrification', - voltage: '1500V', - electrical_profile_type: 'profile', - profile: 'O', - handled: true, - }, - }, +export const voltageRangesForPowerRestrictions: RangedValue[] = [ { - start: 2, - stop: 3, - electrificationUsage: { - lower_pantograph: true, - type: 'neutral_section', - electrical_profile_type: 'no_profile', - }, + begin: 0, + end: 2000, + value: '1500V', }, { - start: 3, - stop: 4, - electrificationUsage: { - type: 'electrification', - voltage: '25000V', - electrical_profile_type: 'profile', - profile: '25000V', - handled: true, - }, + begin: 3000, + end: 4000, + value: '25000V', }, ]; diff --git a/front/src/modules/powerRestriction/helpers/createPathStep.ts b/front/src/modules/powerRestriction/helpers/createPathStep.ts index 8d7b4f6d536..c23c0cb45ba 100644 --- a/front/src/modules/powerRestriction/helpers/createPathStep.ts +++ b/front/src/modules/powerRestriction/helpers/createPathStep.ts @@ -2,56 +2,65 @@ import nextId from 'react-id-generator'; import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; import type { TrackRange } from 'common/api/osrdEditoastApi'; +import type { IntervalItem } from 'common/IntervalsEditor/types'; import type { PathStep } from 'reducers/osrdconf/types'; import { getPointCoordinates } from 'utils/geometry'; +import { mTokm, mToMm } from 'utils/physics'; -const findTrackSectionIndex = (position: number, cumulativeSums: number[]) => { - for (let i = 0; i < cumulativeSums.length; i += 1) { - if (position <= cumulativeSums[i]) { +import { NO_POWER_RESTRICTION } from '../consts'; + +export const findTrackSectionIndex = (position: number, tracksLengthCumulativeSums: number[]) => { + for (let i = 0; i < tracksLengthCumulativeSums.length; i += 1) { + if (position <= tracksLengthCumulativeSums[i]) { return i; } } return -1; }; -const findTrackSection = ( - position: number, - cumulativeSums: number[], - pathProperties: ManageTrainSchedulePathProperties | undefined +export const findTrackSection = ( + position: number, // in mm + tracksLengthCumulativeSums: number[], + pathProperties: ManageTrainSchedulePathProperties ) => { - const index = findTrackSectionIndex(position, cumulativeSums); - return index !== -1 ? pathProperties?.trackSectionRanges[index] : null; + const index = findTrackSectionIndex(position, tracksLengthCumulativeSums); + return index !== -1 ? pathProperties.trackSectionRanges[index] : null; }; -const calculateOffset = ( +export const calculateOffset = ( trackSectionRange: TrackRange, - position: number, - cumulativeSums: number[] + position: number, // in mm + tracksLengthCumulativeSums: number[] // in meters ) => { - const index = findTrackSectionIndex(position, cumulativeSums); - const inferiorSum = cumulativeSums[index]; + const index = findTrackSectionIndex(position, tracksLengthCumulativeSums); + const inferiorSum = tracksLengthCumulativeSums[index]; return trackSectionRange.direction === 'START_TO_STOP' ? inferiorSum - position : position - inferiorSum; }; const createPathStep = ( - positionOnPath: number, - cumulativeSums: number[], - pathProperties: ManageTrainSchedulePathProperties | undefined, + positionOnPathInM: number, // in meters + tracksLengthCumulativeSums: number[], + pathProperties: ManageTrainSchedulePathProperties, pathSteps: PathStep[] -) => { +): PathStep | undefined => { + const positionOnPath = mToMm(positionOnPathInM); if ( - !pathProperties || positionOnPath === 0 || new Set(pathSteps.map((step) => step?.positionOnPath)).has(positionOnPath) ) return undefined; - const trackSectionRange = findTrackSection(positionOnPath, cumulativeSums, pathProperties); + const trackSectionRange = findTrackSection( + positionOnPath, + tracksLengthCumulativeSums, + pathProperties + ); if (!trackSectionRange) return undefined; - const offset = calculateOffset(trackSectionRange, positionOnPath, cumulativeSums); + const offset = calculateOffset(trackSectionRange, positionOnPath, tracksLengthCumulativeSums); + const coordinates = getPointCoordinates( pathProperties.geometry, pathProperties.length, @@ -63,7 +72,63 @@ const createPathStep = ( positionOnPath, coordinates, track: trackSectionRange.track_section, - offset, + offset: mTokm(offset), + }; +}; + +export const createCutAtPathStep = ( + cutAtPositionInM: number, + pathProperties: ManageTrainSchedulePathProperties, + rangesData: IntervalItem[], + cutPositions: number[], + tracksLengthCumulativeSums: number[], + setCutPositions: (newCutPosition: number[]) => void +): PathStep | null => { + const intervalCut = rangesData.find( + (interval) => interval.begin <= cutAtPositionInM && interval.end >= cutAtPositionInM + ); + + if (!intervalCut || intervalCut.value === NO_POWER_RESTRICTION) { + const newCutPositions = !cutPositions.length + ? [cutAtPositionInM] + : cutPositions.flatMap((position, index) => { + if (position > cutAtPositionInM) { + return [cutAtPositionInM, position]; + } + if (index === cutPositions.length - 1) { + return [position, cutAtPositionInM]; + } + return [position]; + }); + setCutPositions(newCutPositions); + return null; + } + + const cutAtPosition = mToMm(cutAtPositionInM); + const trackSectionRangeAtCut = findTrackSection( + cutAtPosition, + tracksLengthCumulativeSums, + pathProperties + ); + + if (!trackSectionRangeAtCut) return null; + + const offsetAtCut = calculateOffset( + trackSectionRangeAtCut, + cutAtPosition, + tracksLengthCumulativeSums + ); + const coordinatesAtCut = getPointCoordinates( + pathProperties.geometry, + pathProperties.length, + cutAtPosition + ); + return { + id: nextId(), + positionOnPath: cutAtPosition, + coordinates: coordinatesAtCut, + track: trackSectionRangeAtCut.track_section, + offset: offsetAtCut, }; }; diff --git a/front/src/modules/powerRestriction/helpers/formatPowerRestrictionRangesWithHandled.ts b/front/src/modules/powerRestriction/helpers/formatPowerRestrictionRangesWithHandled.ts index fd1d0e7967b..639a0192bd7 100644 --- a/front/src/modules/powerRestriction/helpers/formatPowerRestrictionRangesWithHandled.ts +++ b/front/src/modules/powerRestriction/helpers/formatPowerRestrictionRangesWithHandled.ts @@ -1,18 +1,16 @@ import { compact } from 'lodash'; -import type { - ElectrificationRangeV2, - PathPropertiesFormatted, -} from 'applications/operationalStudies/types'; +import type { PathPropertiesFormatted } from 'applications/operationalStudies/types'; import type { PathfindingResultSuccess, + RangedValue, RollingStock, SimulationPowerRestrictionRange, TrainScheduleBase, TrainScheduleResult, } from 'common/api/osrdEditoastApi'; import { getRollingStockPowerRestrictionsByMode } from 'modules/rollingStock/helpers/powerRestrictions'; -import { mmToM } from 'utils/physics'; +import { mToMm, mmToM } from 'utils/physics'; /** * Format power restrictions data to ranges data base on path steps position @@ -38,39 +36,46 @@ export const formatPowerRestrictionRanges = ( }) ); -/** - * Format power restrictions data to be used in simulation results charts - */ +/** Format power restrictions data to be used in simulation results charts */ export const addHandledToPowerRestrictions = ( powerRestrictionRanges: Omit[], - electrificationRanges: ElectrificationRangeV2[], + voltageRanges: RangedValue[], 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 - ); + const restrictionsWithHandled: SimulationPowerRestrictionRange[] = []; - let isHandled = false; - if ( - foundElectrificationRange && - foundElectrificationRange.electrificationUsage.type === 'electrification' && - powerRestrictionsByMode[foundElectrificationRange.electrificationUsage.voltage] - ) { - isHandled = powerRestrictionsByMode[ - foundElectrificationRange.electrificationUsage.voltage - ].includes(powerRestrictionRange.code); - } + powerRestrictionRanges.forEach((powerRestrictionRange) => { + const powerRestrictionBeginInMm = mToMm(powerRestrictionRange.start); + const powerRestrictionEndInMm = mToMm(powerRestrictionRange.stop); - return { - ...powerRestrictionRange, - handled: isHandled, - }; + // find all the voltage ranges which overlap the powerRestrictionRange + voltageRanges.forEach((voltageRange) => { + const voltageBeginIsInRange = + powerRestrictionBeginInMm <= voltageRange.begin && + voltageRange.begin <= powerRestrictionEndInMm; + const voltageEndIsInRange = + powerRestrictionBeginInMm <= voltageRange.end && + voltageRange.end <= powerRestrictionEndInMm; + + if (voltageBeginIsInRange || voltageEndIsInRange) { + const powerRestrictionForVoltage = powerRestrictionsByMode[voltageRange.value]; + const isHandled = + powerRestrictionForVoltage && + powerRestrictionForVoltage.includes(powerRestrictionRange.code); + + // add the restriction corresponding to the voltage range + restrictionsWithHandled.push({ + start: voltageBeginIsInRange ? mmToM(voltageRange.begin) : powerRestrictionRange.start, + stop: voltageEndIsInRange ? mmToM(voltageRange.end) : powerRestrictionRange.stop, + code: powerRestrictionRange.code, + handled: isHandled, + }); + } + }); }); + return restrictionsWithHandled; }; const formatPowerRestrictionRangesWithHandled = ({ @@ -96,7 +101,7 @@ const formatPowerRestrictionRangesWithHandled = ({ ); const powerRestrictionsWithHandled = addHandledToPowerRestrictions( powerRestrictionsRanges, - pathProperties.electrifications, + pathProperties.voltages, selectedTrainRollingStock.effort_curves.modes ); diff --git a/front/src/modules/powerRestriction/helpers/formatPowerRestrictions.ts b/front/src/modules/powerRestriction/helpers/formatPowerRestrictions.ts index 4de5f86bb26..39c9108099e 100644 --- a/front/src/modules/powerRestriction/helpers/formatPowerRestrictions.ts +++ b/front/src/modules/powerRestriction/helpers/formatPowerRestrictions.ts @@ -49,7 +49,8 @@ const reducePowerRestrictions = (acc: IntervalItem[], restriction: PowerRestrictionV2, index: number): IntervalItem[] => { const fromPathStep = pathStepById[restriction.from]; const toPathStep = pathStepById[restriction.to]; - const from = fromPathStep?.positionOnPath ? mmToM(fromPathStep?.positionOnPath) : undefined; + const from = + fromPathStep?.positionOnPath !== undefined ? mmToM(fromPathStep?.positionOnPath) : undefined; const to = toPathStep?.positionOnPath ? mmToM(toPathStep?.positionOnPath) : undefined; const prevEnd = isEmpty(acc) ? 0 : acc[acc.length - 1].end; @@ -64,7 +65,7 @@ const reducePowerRestrictions = acc.push({ begin: prevEnd, end: from, value: 'NO_POWER_RESTRICTION' }); } } - acc.push({ begin: from, end: to, value: restriction.code }); + acc.push({ begin: from, end: to, value: restriction.value }); } return acc; }; diff --git a/front/src/modules/powerRestriction/helpers/getRestrictionsToResize.ts b/front/src/modules/powerRestriction/helpers/getRestrictionsToResize.ts new file mode 100644 index 00000000000..f7f5907951f --- /dev/null +++ b/front/src/modules/powerRestriction/helpers/getRestrictionsToResize.ts @@ -0,0 +1,103 @@ +import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; +import type { IntervalItem } from 'common/IntervalsEditor/types'; +import type { PathStep } from 'reducers/osrdconf/types'; + +import { getPathStep } from './utils'; +import { NO_POWER_RESTRICTION } from '../consts'; + +const getPowerRestrictionFromRange = ( + pathSteps: PathStep[], + powerRestrictionRanges: PowerRestrictionV2[], + rangeData: IntervalItem +): PowerRestrictionV2 | null => { + const fromPathStep = getPathStep(pathSteps, rangeData.begin); + const toPathStep = getPathStep(pathSteps, rangeData.end); + + if (!fromPathStep || !toPathStep) return null; + + const powerRestrictionRange = powerRestrictionRanges.find( + (restriction) => restriction.from === fromPathStep.id && restriction.to === toPathStep.id + ); + return powerRestrictionRange || null; +}; + +/** + * Given the new position of the modified extremity, return the powerRestrictions to update + */ +const getPowerRestrictionsToUpdate = ( + pathSteps: PathStep[], + powerRestrictionRanges: PowerRestrictionV2[], + ranges: IntervalItem[], + position: number, + selectedRangeIndex: number +) => { + const selectedRange = ranges[selectedRangeIndex]; + const selectedRestriction = getPowerRestrictionFromRange( + pathSteps, + powerRestrictionRanges, + selectedRange + ); + if (!selectedRestriction) return null; + + const otherRangeIndex = ranges.findIndex( + (range) => range.begin <= position && position <= range.end + ); + + const otherRange = otherRangeIndex !== selectedRangeIndex ? ranges[otherRangeIndex] : undefined; + + if (!otherRange || otherRange.value === NO_POWER_RESTRICTION) + return { selectedRange, selectedRestriction }; + + const otherRestriction = getPowerRestrictionFromRange( + pathSteps, + powerRestrictionRanges, + otherRange + ); + return { selectedRange, selectedRestriction, otherRange, otherRestriction }; +}; + +const getRestrictionsToResize = ( + ranges: IntervalItem[], + selectedRangeIndex: number, + context: 'begin' | 'end', + newPosition: number, + pathSteps: PathStep[], + powerRestrictionRanges: PowerRestrictionV2[] +) => { + const result = getPowerRestrictionsToUpdate( + pathSteps, + powerRestrictionRanges, + ranges, + newPosition, + selectedRangeIndex + ); + if (!result) return null; + + const { selectedRange, selectedRestriction, otherRange, otherRestriction } = result; + + let firstRestriction: PowerRestrictionV2 | undefined; + let secondRestriction: PowerRestrictionV2 | undefined; + if (context === 'begin') { + // default hypothesis: the begin was decremented + firstRestriction = otherRestriction || undefined; + secondRestriction = selectedRestriction; + + if (otherRange && otherRestriction && otherRange.begin > selectedRange.begin) { + firstRestriction = selectedRestriction; + secondRestriction = otherRestriction; + } + } else { + // default hypothesis: the end was incremented + firstRestriction = selectedRestriction; + secondRestriction = otherRestriction || undefined; + + if (otherRange && otherRestriction && otherRange.begin < selectedRange.begin) { + firstRestriction = otherRestriction; + secondRestriction = selectedRestriction; + } + } + + return { firstRestriction, secondRestriction }; +}; + +export default getRestrictionsToResize; diff --git a/front/src/modules/powerRestriction/helpers/powerRestrictionWarnings.ts b/front/src/modules/powerRestriction/helpers/powerRestrictionWarnings.ts index 9f285d2a425..8d2b3aef2e5 100644 --- a/front/src/modules/powerRestriction/helpers/powerRestrictionWarnings.ts +++ b/front/src/modules/powerRestriction/helpers/powerRestrictionWarnings.ts @@ -1,8 +1,13 @@ +import { compact, isEmpty, keyBy } from 'lodash'; + +import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; import type { RangedValue, RollingStock } from 'common/api/osrdEditoastApi'; import { NO_POWER_RESTRICTION } from 'modules/powerRestriction/consts'; import type { PowerRestrictionWarnings } from 'modules/powerRestriction/types'; import { getRollingStockPowerRestrictionsByMode } from 'modules/rollingStock/helpers/powerRestrictions'; +import type { PathStep } from 'reducers/osrdconf/types'; +// TODO drop v1: convert begin and end in meters here instead of in PowerRestrictionsSelectorV2 const getInvalidZoneBoundaries = ( powerRestrictionRange: RangedValue, electrificationRange: RangedValue @@ -97,3 +102,56 @@ export const countWarnings = ( missingPowerRestrictionWarnings.length ); }; + +const formatElectricalRanges = ( + ranges: PowerRestrictionV2[], + pathStepsById: Record +): { begin: number; end: number; value: string }[] => { + const formattedRanges = compact( + ranges.map((range) => { + const begin = pathStepsById[range.from]?.positionOnPath; + const end = pathStepsById[range.to]?.positionOnPath; + + if (begin !== undefined && end !== undefined) { + return { + begin, + end, + value: range.value, + }; + } + return null; + }) + ); + return formattedRanges; +}; + +const getPowerRestrictionsWarningsData = ({ + pathSteps, + pathElectrificationRanges, + rollingStockModes, + powerRestrictionRanges, +}: { + pathSteps: PathStep[]; + rollingStockPowerRestrictions: RollingStock['power_restrictions']; + pathElectrificationRanges: RangedValue[]; + powerRestrictionRanges: PowerRestrictionV2[]; + rollingStockModes: RollingStock['effort_curves']['modes']; +}) => { + const pathStepsById = keyBy(pathSteps, 'id'); + const warnings = + !isEmpty(pathElectrificationRanges) && !isEmpty(powerRestrictionRanges) + ? getPowerRestrictionsWarnings( + formatElectricalRanges(powerRestrictionRanges, pathStepsById), + pathElectrificationRanges, + rollingStockModes + ) + : undefined; + const warningsNb = warnings ? countWarnings(warnings) : 0; + + return { + warnings, + warningsNb, + }; +}; + +export default getPowerRestrictionsWarningsData; diff --git a/front/src/modules/powerRestriction/helpers/utils.ts b/front/src/modules/powerRestriction/helpers/utils.ts index 9a53893cefc..16e1d055c6f 100644 --- a/front/src/modules/powerRestriction/helpers/utils.ts +++ b/front/src/modules/powerRestriction/helpers/utils.ts @@ -1,60 +1,44 @@ -import { isEqual } from 'lodash'; - -import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; +import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; import type { IntervalItem } from 'common/IntervalsEditor/types'; import type { PathStep } from 'reducers/osrdconf/types'; import { mToMm } from 'utils/physics'; -export const getPathStep = (pathSteps: PathStep[], position: number) => - pathSteps.find((step) => step.positionOnPath === position); - -export const getPowerRestrictionBis = ( - pathSteps: PathStep[], - powerRestrictionRanges: PowerRestrictionV2[], - rangeData: IntervalItem -): PowerRestrictionV2 | undefined => { - const fromPathStep = getPathStep(pathSteps, mToMm(rangeData.begin)); - const toPathStep = getPathStep(pathSteps, mToMm(rangeData.end)); +import createPathStep from './createPathStep'; - if (!fromPathStep || !toPathStep) return; +/** Get PathStep located at the given position on path (in meters), if it exists. */ +export const getPathStep = (pathSteps: PathStep[], positionInM: number) => + pathSteps.find((step) => step.positionOnPath === mToMm(positionInM)); - const powerRestrictionRange = powerRestrictionRanges.find( - (restriction) => restriction.from === fromPathStep.id && restriction.to === toPathStep.id - ); - return powerRestrictionRange; +export const getOrCreatePathStepAtPosition = ( + positionOnPathInM: number, + pathSteps: PathStep[], + tracksLengthCumulativeSums: number[], + pathProperties: ManageTrainSchedulePathProperties +) => { + const pathStep = getPathStep(pathSteps, positionOnPathInM); + if (pathStep) { + return pathStep; + } + return createPathStep(positionOnPathInM, tracksLengthCumulativeSums, pathProperties, pathSteps); }; -export const getPowerRestriction = ( +export const extractPathStepsFromRange = ( + range: IntervalItem, pathSteps: PathStep[], - powerRestrictionRanges: PowerRestrictionV2[], - rangesData: IntervalItem[], - position: number, - selectedSegmentIndex: number, - context: 'end' | 'begin' -): { rangeData?: IntervalItem; powerRestrictionRange?: PowerRestrictionV2 } => { - const rangeDataIndex = rangesData.findIndex( - (range) => range.begin <= position && position <= range.end + tracksLengthCumulativeSums: number[], + pathProperties: ManageTrainSchedulePathProperties +) => { + const from = getOrCreatePathStepAtPosition( + range.begin, + pathSteps, + tracksLengthCumulativeSums, + pathProperties ); - - let rangeData: IntervalItem | undefined; - if (rangeDataIndex === selectedSegmentIndex) { - if (context === 'end') { - if (rangeDataIndex + 1 < rangesData.length) { - rangeData = rangesData[rangeDataIndex + 1]; - } - } else if (rangeDataIndex > 0) { - rangeData = rangesData[rangeDataIndex - 1]; - } - } else { - rangeData = rangesData[rangeDataIndex]; - } - - if (!rangeData) return { rangeData, powerRestrictionRange: undefined }; - - const powerRestrictionRange = getPowerRestrictionBis( + const to = getOrCreatePathStepAtPosition( + range.end, pathSteps, - powerRestrictionRanges, - rangeData + tracksLengthCumulativeSums, + pathProperties ); - return { rangeData, powerRestrictionRange }; + return { from, to }; }; diff --git a/front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorBehaviours.ts b/front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorBehaviours.ts new file mode 100644 index 00000000000..38207a669e9 --- /dev/null +++ b/front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorBehaviours.ts @@ -0,0 +1,165 @@ +import { useMemo } from 'react'; + +import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; +import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; +import type { IntervalItem } from 'common/IntervalsEditor/types'; +import { useOsrdConfActions } from 'common/osrdContext'; +import { createCutAtPathStep } from 'modules/powerRestriction/helpers/createPathStep'; +import type { OperationalStudiesConfSliceActions } from 'reducers/osrdconf/operationalStudiesConf'; +import type { PathStep } from 'reducers/osrdconf/types'; +import { useAppDispatch } from 'store'; + +import { NO_POWER_RESTRICTION } from '../consts'; +import getRestrictionsToResize from '../helpers/getRestrictionsToResize'; +import { + extractPathStepsFromRange, + getOrCreatePathStepAtPosition, + getPathStep, +} from '../helpers/utils'; + +type UsePowerRestrictionSelectorBehavioursArgs = { + ranges: IntervalItem[]; + cutPositions: number[]; + pathProperties: ManageTrainSchedulePathProperties; + pathSteps: PathStep[]; + powerRestrictionRanges: PowerRestrictionV2[]; + setCutPositions: (newCutPosition: number[]) => void; +}; + +const usePowerRestrictionSelectorBehaviours = ({ + cutPositions, + pathProperties, + pathSteps, + powerRestrictionRanges, + ranges, + setCutPositions, +}: UsePowerRestrictionSelectorBehavioursArgs) => { + const dispatch = useAppDispatch(); + + const { + upsertPowerRestrictionRangesV2, + cutPowerRestrictionRangesV2, + deletePowerRestrictionRangesV2, + resizeSegmentEndInput, + resizeSegmentBeginInput, + } = useOsrdConfActions() as OperationalStudiesConfSliceActions; + + /** Cumulative sums of the trackSections' length on path (in meters) */ + const tracksLengthCumulativeSums = useMemo( + () => + pathProperties.trackSectionRanges.reduce((acc, range, index) => { + const rangeLength = range.end - range.begin; + if (index === 0) { + acc.push(rangeLength); + } else { + acc.push(acc[acc.length - 1] + rangeLength); + } + return acc; + }, [] as number[]), + [pathProperties.trackSectionRanges] + ); + + const editPowerRestrictionRanges = ( + newPowerRestrictionRanges: IntervalItem[], + selectedIntervalIndex?: number + ) => { + if (selectedIntervalIndex === undefined) return; + + const newRange = newPowerRestrictionRanges[selectedIntervalIndex]; + const { from, to } = extractPathStepsFromRange( + newRange, + pathSteps, + tracksLengthCumulativeSums, + pathProperties + ); + + if (from && to) { + if (newRange.value !== NO_POWER_RESTRICTION) { + dispatch( + upsertPowerRestrictionRangesV2({ + from, + to, + code: newRange.value.toString(), + }) + ); + } else { + dispatch(deletePowerRestrictionRangesV2({ from, to })); + } + } + }; + + const cutPowerRestrictionRange = (cutAtPositionInM: number) => { + const cutAt = createCutAtPathStep( + cutAtPositionInM, + pathProperties, + ranges, + cutPositions, + tracksLengthCumulativeSums, + setCutPositions + ); + if (cutAt) { + dispatch(cutPowerRestrictionRangesV2({ cutAt })); + } + }; + + const deletePowerRestrictionRange = (from: number, to: number) => { + const fromPathStep = getPathStep(pathSteps, from); + const toPathStep = getPathStep(pathSteps, to); + + if (fromPathStep && toPathStep) { + dispatch(deletePowerRestrictionRangesV2({ from: fromPathStep, to: toPathStep })); + } + }; + + const resizeSegments = ( + selectedRangeIndex: number, + context: 'begin' | 'end', + newPosition: number + ) => { + const result = getRestrictionsToResize( + ranges, + selectedRangeIndex, + context, + newPosition, + pathSteps, + powerRestrictionRanges + ); + if (!result) return; + const { firstRestriction, secondRestriction } = result; + + const newPathStep = getOrCreatePathStepAtPosition( + newPosition, + pathSteps, + tracksLengthCumulativeSums, + pathProperties + ); + if (!newPathStep) return; + + if (context === 'begin') { + if (secondRestriction) + dispatch( + resizeSegmentBeginInput({ + firstRestriction, + secondRestriction, + newFromPathStep: newPathStep, + }) + ); + } else if (firstRestriction) + dispatch( + resizeSegmentEndInput({ + firstRestriction, + secondRestriction, + newEndPathStep: newPathStep, + }) + ); + }; + + return { + resizeSegments, + deletePowerRestrictionRange, + cutPowerRestrictionRange, + editPowerRestrictionRanges, + }; +}; + +export default usePowerRestrictionSelectorBehaviours; diff --git a/front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorData.ts b/front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorData.ts new file mode 100644 index 00000000000..09e33cc7fbb --- /dev/null +++ b/front/src/modules/powerRestriction/hooks/usePowerRestrictionSelectorData.ts @@ -0,0 +1,109 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { compact } from 'lodash'; +import { useSelector } from 'react-redux'; + +import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; +import type { RangedValue, RollingStock } from 'common/api/osrdEditoastApi'; +import type { IntervalItem } from 'common/IntervalsEditor/types'; +import { useOsrdConfSelectors } from 'common/osrdContext'; +import { mmToM } from 'utils/physics'; + +import usePowerRestrictionSelectorBehaviours from './usePowerRestrictionSelectorBehaviours'; +import { NO_POWER_RESTRICTION } from '../consts'; +import formatPowerRestrictions from '../helpers/formatPowerRestrictions'; +import getPowerRestrictionsWarningsData from '../helpers/powerRestrictionWarnings'; + +const usePowerRestrictionSelector = ( + voltageRanges: RangedValue[], + rollingStockPowerRestrictions: RollingStock['power_restrictions'], + rollingStockModes: RollingStock['effort_curves']['modes'], + pathProperties: ManageTrainSchedulePathProperties +) => { + const { getPowerRestrictionV2, getPathSteps } = useOsrdConfSelectors(); + const powerRestrictionRanges = useSelector(getPowerRestrictionV2); + const pathSteps = compact(useSelector(getPathSteps)); + + const [cutPositions, setCutPositions] = useState([]); // in meters + const [ranges, setRanges] = useState([]); + + const electrificationChangePoints = useMemo(() => { + const specialPoints = voltageRanges.map((range) => ({ + position: mmToM(range.end), + })); + specialPoints.pop(); + return specialPoints; + }, [voltageRanges]); + + const powerRestrictionOptions = useMemo( + () => [NO_POWER_RESTRICTION, ...Object.keys(rollingStockPowerRestrictions)], + [rollingStockPowerRestrictions] + ); + + const compatibleVoltageRanges = useMemo(() => { + const handledModes = Object.keys(rollingStockModes); + return voltageRanges.map(({ begin, end, value: mode }) => ({ + begin, + end, + value: handledModes.includes(mode) ? mode : '', + })); + }, [voltageRanges]); + + const { + resizeSegments, + deletePowerRestrictionRange, + cutPowerRestrictionRange, + editPowerRestrictionRanges, + } = usePowerRestrictionSelectorBehaviours({ + cutPositions, + pathProperties, + pathSteps, + powerRestrictionRanges, + ranges, + setCutPositions, + }); + + const { warnings, warningsNb } = useMemo( + () => + getPowerRestrictionsWarningsData({ + pathSteps, + rollingStockPowerRestrictions, + pathElectrificationRanges: voltageRanges, + rollingStockModes, + powerRestrictionRanges, + }), + [ + pathSteps, + rollingStockPowerRestrictions, + voltageRanges, + rollingStockModes, + powerRestrictionRanges, + ] + ); + + useEffect(() => { + const newRanges = formatPowerRestrictions( + powerRestrictionRanges, + [...electrificationChangePoints.map(({ position }) => position), ...cutPositions], + compact(pathSteps), + mmToM(pathProperties.length) + ); + setRanges(newRanges); + }, [electrificationChangePoints, cutPositions, powerRestrictionRanges]); + + return { + ranges, + compatibleVoltageRanges, + electrificationChangePoints, + pathLength: mmToM(pathProperties.length), + powerRestrictionOptions, + warnings, + warningsNb, + resizeSegments, + deletePowerRestrictionRange, + cutPowerRestrictionRange, + editPowerRestrictionRanges, + }; +}; + +export default usePowerRestrictionSelector; diff --git a/front/src/modules/simulationResult/components/ChartHelpers/drawPowerRestriction.ts b/front/src/modules/simulationResult/components/ChartHelpers/drawPowerRestriction.ts index 1510a3c1a5f..666cea43858 100644 --- a/front/src/modules/simulationResult/components/ChartHelpers/drawPowerRestriction.ts +++ b/front/src/modules/simulationResult/components/ChartHelpers/drawPowerRestriction.ts @@ -84,7 +84,7 @@ const drawPowerRestriction = ( } }; - if (!isIncompatible && isRestriction) drawZone.call(addTextZone); + drawZone.call(addTextZone); // create pop-up when hovering rect-profile drawZone diff --git a/front/src/modules/simulationResult/components/SpeedSpaceChart/utils.ts b/front/src/modules/simulationResult/components/SpeedSpaceChart/utils.ts index bd7750819ae..1283ff366ad 100644 --- a/front/src/modules/simulationResult/components/SpeedSpaceChart/utils.ts +++ b/front/src/modules/simulationResult/components/SpeedSpaceChart/utils.ts @@ -157,7 +157,7 @@ export const createPowerRestrictionSegment = ( // figure out if the power restriction is incompatible or missing` const isRestriction = powerRestrictionRange.handled; const isIncompatiblePowerRestriction = !!powerRestrictionRange.code; - const isStriped = !!powerRestrictionRange.code; + const isStriped = !powerRestrictionRange.code || !powerRestrictionRange.handled; const segment: PowerRestrictionSegment = { position_start: powerRestrictionRange.start, diff --git a/front/src/modules/timesStops/TimesStops.tsx b/front/src/modules/timesStops/TimesStops.tsx index dd7e04ff806..a736ed51b3c 100644 --- a/front/src/modules/timesStops/TimesStops.tsx +++ b/front/src/modules/timesStops/TimesStops.tsx @@ -94,7 +94,7 @@ const TimesStops = ({ pathProperties, pathSteps = [], startTime }: TimesStopsPro }); const updatedPathSteps = removeElementAtIndex(pathSteps, index); - dispatch(updatePathSteps(updatedPathSteps)); + dispatch(updatePathSteps({ pathSteps: updatedPathSteps })); }; return ( diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify.ts index be9d789393e..636f9ab2823 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify.ts @@ -145,7 +145,7 @@ export function adjustConfWithTrainToModifyV2( dispatch(updateRollingStockID(rollingStockId)); dispatch(updateName(train_name)); dispatch(updateDepartureTime(start_time)); - dispatch(updatePathSteps(pathSteps)); + dispatch(updatePathSteps({ pathSteps, resetPowerRestrictions: true })); dispatch(updateInitialSpeed(initial_speed ? msToKmh(initial_speed) : 0)); if (options?.use_electrical_profiles === usingElectricalProfiles) diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts index a46663dc115..edc60b710dc 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/checkCurrentConfig.ts @@ -31,6 +31,7 @@ const checkCurrentConfig = ( initialSpeed, usingElectricalProfiles, rollingStockComfortV2, + powerRestrictionV2, startTime, } = osrdconf; let error = false; @@ -153,9 +154,7 @@ const checkCurrentConfig = ( margins: formatMargin(compact(pathSteps)), schedule: formatSchedule(compact(pathSteps), startTime), - - // TODO TS2 : adapt this for power restrictions issue - // powerRestrictions: formatPowerRestrictions(pathSteps) + powerRestrictions: powerRestrictionV2, firstStartTime: startTime, speedLimitByTag, }; diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts index c39c24c12fd..4dbaf2ff7c3 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/helpers/formatTrainSchedulePayload.ts @@ -18,6 +18,7 @@ export default function formatTrainSchedulePayload( usingElectricalProfiles, rollingStockComfort, margins, + powerRestrictions, } = validConfig; return { @@ -30,10 +31,8 @@ export default function formatTrainSchedulePayload( use_electrical_profiles: usingElectricalProfiles, }, path, - // TODO TS2 : handle power restrictions - // power_restrictions: validConfig.powerRestrictions, + power_restrictions: powerRestrictions, rolling_stock_name: rollingStockName, - // TODO TS2 : handle handle margins schedule: validConfig.schedule, speed_limit_tag: speedLimitByTag, start_time: startTime, diff --git a/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts b/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts index df32573a70e..1f397e606be 100644 --- a/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts +++ b/front/src/modules/trainschedule/components/ManageTrainSchedule/types.ts @@ -53,10 +53,9 @@ export type ValidConfig = { initialSpeed: number; usingElectricalProfiles: boolean; path: TrainScheduleBase['path']; - // TODO TS2 : adapt this for times and stops / power restrictions issues margins: TrainScheduleBase['margins']; schedule: TrainScheduleBase['schedule']; - // powerRestrictions: TrainScheduleBase['power_restrictions'] + powerRestrictions?: TrainScheduleBase['power_restrictions']; firstStartTime: string; speedLimitByTag?: string; }; diff --git a/front/src/reducers/osrdconf/operationalStudiesConf/index.ts b/front/src/reducers/osrdconf/operationalStudiesConf/index.ts index 512768bd25b..3c1f7132b1d 100644 --- a/front/src/reducers/osrdconf/operationalStudiesConf/index.ts +++ b/front/src/reducers/osrdconf/operationalStudiesConf/index.ts @@ -3,6 +3,8 @@ import { createSlice } from '@reduxjs/toolkit'; import { defaultCommonConf, buildCommonConfReducers } from 'reducers/osrdconf/osrdConfCommon'; import type { OsrdConfState } from 'reducers/osrdconf/types'; +import { builPowerRestrictionReducer } from './powerRestrictionReducer'; + export type OperationalStudiesConfState = OsrdConfState; export const operationalStudiesConfSlice = createSlice({ @@ -10,6 +12,7 @@ export const operationalStudiesConfSlice = createSlice({ initialState: defaultCommonConf, reducers: { ...buildCommonConfReducers(), + ...builPowerRestrictionReducer(), }, }); diff --git a/front/src/reducers/osrdconf/operationalStudiesConf/powerRestrictionReducer.ts b/front/src/reducers/osrdconf/operationalStudiesConf/powerRestrictionReducer.ts new file mode 100644 index 00000000000..a67278788e9 --- /dev/null +++ b/front/src/reducers/osrdconf/operationalStudiesConf/powerRestrictionReducer.ts @@ -0,0 +1,242 @@ +import type { CaseReducer, PayloadAction } from '@reduxjs/toolkit'; +import type { Draft } from 'immer'; +import { compact, isEqual, keyBy, sortBy } from 'lodash'; + +import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; +import { NO_POWER_RESTRICTION } from 'modules/powerRestriction/consts'; +import type { OsrdConfState, PathStep } from 'reducers/osrdconf/types'; +import { addElementAtIndex } from 'utils/array'; + +import { addPathStep, cleanPathSteps, isRangeCovered, updateRestrictions } from './utils'; + +export type PowerRestrictionReducer = { + ['upsertPowerRestrictionRangesV2']: CaseReducer< + S, + PayloadAction<{ from: PathStep; to: PathStep; code: string }> + >; + ['cutPowerRestrictionRangesV2']: CaseReducer>; + ['deletePowerRestrictionRangesV2']: CaseReducer< + S, + PayloadAction<{ from: PathStep; to: PathStep }> + >; + ['resizeSegmentEndInput']: CaseReducer< + S, + PayloadAction<{ + firstRestriction: PowerRestrictionV2; + secondRestriction?: PowerRestrictionV2; + newEndPathStep: PathStep; + }> + >; + ['resizeSegmentBeginInput']: CaseReducer< + S, + PayloadAction<{ + firstRestriction?: PowerRestrictionV2; + secondRestriction: PowerRestrictionV2; + newFromPathStep: PathStep; + }> + >; + ['resetPowerRestrictionRangesV2']: CaseReducer; +}; + +export function builPowerRestrictionReducer(): PowerRestrictionReducer { + return { + upsertPowerRestrictionRangesV2( + state: Draft, + action: PayloadAction<{ from: PathStep; to: PathStep; code: string }> + ) { + const { from, to, code } = action.payload; + let newPathSteps = compact(state.pathSteps); + let newPowerRestrictionRangesV2 = state.powerRestrictionV2.filter( + (restriction) => restriction.from !== from.id && restriction.to !== to.id + ); + + // add new pathSteps + newPathSteps = addPathStep(newPathSteps, from); + newPathSteps = addPathStep(newPathSteps, to); + + const newPathStepsById = keyBy(newPathSteps, 'id'); + + // update power restriction ranges + if (code !== NO_POWER_RESTRICTION) { + newPowerRestrictionRangesV2.push({ from: from.id, to: to.id, value: code }); + newPowerRestrictionRangesV2 = sortBy( + newPowerRestrictionRangesV2, + (range) => newPathStepsById[range.from]?.positionOnPath + ); + } + + state.pathSteps = newPathSteps; + state.powerRestrictionV2 = newPowerRestrictionRangesV2; + }, + + cutPowerRestrictionRangesV2(state: Draft, action: PayloadAction<{ cutAt: PathStep }>) { + const { cutAt } = action.payload; + let newPathSteps = [...state.pathSteps]; + + const pathIds = compact(state.pathSteps).map((step) => step.id); + + if (!pathIds.includes(cutAt.id)) { + const cutAtIndex = newPathSteps.findIndex( + (step) => step?.positionOnPath && step.positionOnPath > cutAt.positionOnPath! + ); + + if (cutAtIndex === -1) return; + + // add the new pathStep at the right index + newPathSteps = addElementAtIndex(newPathSteps, cutAtIndex, cutAt); + + const prevStep = newPathSteps[cutAtIndex - 1]; + const nextStep = newPathSteps[cutAtIndex + 1]; + + if (!prevStep || !nextStep) { + console.error('cutPowerRestrictionRangesV2: prevStep or nextStep is undefined'); + } else { + // update the power restriction ranges by splitting 1 range into 2 + const newPowerRestrictionRangesV2 = state.powerRestrictionV2.reduce( + (acc, powerRestriction) => { + if (powerRestriction.from === prevStep.id) { + acc.push({ + ...powerRestriction, + to: cutAt.id, + }); + acc.push({ + ...powerRestriction, + from: cutAt.id, + to: nextStep.id, + }); + } else { + acc.push(powerRestriction); + } + return acc; + }, + [] as PowerRestrictionV2[] + ); + + state.pathSteps = newPathSteps; + state.powerRestrictionV2 = newPowerRestrictionRangesV2; + } + } + }, + + deletePowerRestrictionRangesV2( + state: Draft, + action: PayloadAction<{ from: PathStep; to: PathStep }> + ) { + const { from, to } = action.payload; + + const newPowerRestrictionRanges = state.powerRestrictionV2.filter( + (restriction) => restriction.from !== from.id && restriction.to !== to.id + ); + + const newPathSteps = [...state.pathSteps] as PathStep[]; + state.pathSteps = cleanPathSteps(newPathSteps, newPowerRestrictionRanges); + state.powerRestrictionV2 = newPowerRestrictionRanges; + }, + + resizeSegmentBeginInput( + state: Draft, + action: PayloadAction<{ + firstRestriction?: PowerRestrictionV2; + secondRestriction: PowerRestrictionV2; + newFromPathStep: PathStep; + }> + ) { + const { firstRestriction, secondRestriction, newFromPathStep } = action.payload; + + // pathSteps should not be undefined or have null values + if (state.pathSteps && !state.pathSteps.some((pathStep) => !pathStep)) { + let newPathSteps = [...state.pathSteps] as PathStep[]; + let newPowerRestrictionRanges = state.powerRestrictionV2.filter( + (restriction) => + !isEqual(restriction, firstRestriction) || !isEqual(restriction, secondRestriction) + ); + + // find the covered ranges + const pathStepEnd = newPathSteps.find((pathStep) => pathStep.id === secondRestriction.to); + const coveredRanges = pathStepEnd + ? newPowerRestrictionRanges.filter((restriction) => + isRangeCovered( + newPathSteps, + restriction, + newFromPathStep.positionOnPath, + pathStepEnd.positionOnPath + ) + ) + : []; + + // add the new pathStep + newPathSteps = addPathStep(newPathSteps, newFromPathStep); + + // update the power restriction ranges + newPowerRestrictionRanges = updateRestrictions( + newPowerRestrictionRanges, + firstRestriction, + secondRestriction, + newFromPathStep.id, + coveredRanges + ); + + // clean pathSteps + newPathSteps = cleanPathSteps(newPathSteps, newPowerRestrictionRanges); + + state.pathSteps = newPathSteps; + state.powerRestrictionV2 = newPowerRestrictionRanges; + } + }, + resizeSegmentEndInput( + state: Draft, + action: PayloadAction<{ + firstRestriction: PowerRestrictionV2; + secondRestriction?: PowerRestrictionV2; + newEndPathStep: PathStep; + }> + ) { + const { firstRestriction, secondRestriction, newEndPathStep } = action.payload; + + // pathSteps should not be undefined or have null values + if (state.pathSteps && !state.pathSteps.some((pathStep) => !pathStep)) { + let newPathSteps = [...state.pathSteps] as PathStep[]; + let newPowerRestrictionRanges = state.powerRestrictionV2.filter( + (restriction) => + !isEqual(restriction, firstRestriction) || !isEqual(restriction, secondRestriction) + ); + const pathStepBegin = newPathSteps.find( + (pathStep) => pathStep.id === firstRestriction.from + ); + + // find the covered ranges + const coveredRanges = pathStepBegin + ? newPowerRestrictionRanges.filter((restriction) => + isRangeCovered( + newPathSteps, + restriction, + pathStepBegin.positionOnPath, + newEndPathStep.positionOnPath + ) + ) + : []; + + // add the new pathStep + newPathSteps = addPathStep(newPathSteps, newEndPathStep); + + // update the power restriction ranges + newPowerRestrictionRanges = updateRestrictions( + newPowerRestrictionRanges, + firstRestriction, + secondRestriction, + newEndPathStep.id, + coveredRanges + ); + + // clean pathSteps + newPathSteps = cleanPathSteps(newPathSteps, newPowerRestrictionRanges); + + state.pathSteps = newPathSteps; + state.powerRestrictionV2 = newPowerRestrictionRanges; + } + }, + resetPowerRestrictionRangesV2(state: Draft) { + state.powerRestrictionV2 = []; + }, + }; +} diff --git a/front/src/reducers/osrdconf/osrdConfCommon/utils.ts b/front/src/reducers/osrdconf/operationalStudiesConf/utils.ts similarity index 55% rename from front/src/reducers/osrdconf/osrdConfCommon/utils.ts rename to front/src/reducers/osrdconf/operationalStudiesConf/utils.ts index ad762955d51..46af9097374 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/utils.ts +++ b/front/src/reducers/osrdconf/operationalStudiesConf/utils.ts @@ -1,42 +1,33 @@ -import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; -import type { PathStep } from '../types'; -import type { IntervalItem } from 'common/IntervalsEditor/types'; import { compact } from 'lodash'; -// Fonction d'assistance pour ajouter des éléments à un index spécifique -export const addElementAtIndex = (array: T[], index: number, element: T): T[] => { - if (index === -1) { - return [...array, element]; - } - return [...array.slice(0, index), element, ...array.slice(index)]; -}; - -// Fonction pour valider les positions des path steps -export const validatePathSteps = (pathSteps: PathStep[]): boolean => { - for (let i = 0; i < pathSteps.length - 1; i++) { - const position = pathSteps[i]?.positionOnPath; - const nextPosition = pathSteps[i + 1]?.positionOnPath; +import type { PowerRestrictionV2 } from 'applications/operationalStudies/consts'; +import { addElementAtIndex } from 'utils/array'; - if (position && nextPosition && position >= nextPosition) { - return false; - } - } - return true; -}; +import type { PathStep } from '../types'; -export const checkValidPathStep = (pathStep: PathStep) => { - if ( - !pathStep || - pathStep.locked || - pathStep.arrival || - pathStep.stopFor || - pathStep.theoreticalMargin - ) - return false; - return true; +/** + * Check if a pathStep can be removed. + * + * It cannot be removed if it is used in at least one power restriction range, if it is locked or has an arrival time, + * a stop duration or a margin. + */ +export const canRemovePathStep = ( + pathStep: PathStep, + powerRestrictionRanges: PowerRestrictionV2[] +) => { + const pathStepIsUsed = powerRestrictionRanges.some( + (restriction) => restriction.from === pathStep.id || restriction.to === pathStep.id + ); + return ( + !pathStepIsUsed && + !pathStep.locked && + !pathStep.arrival && + !pathStep.stopFor && + !pathStep.theoreticalMargin + ); }; -// Fonction pour mettre à jour une restriction +/** Remove some restrictions and update the first and second restrictions with the new path step */ export const updateRestrictions = ( restrictions: PowerRestrictionV2[], firstRestriction: PowerRestrictionV2 | undefined, @@ -72,7 +63,6 @@ export const isRangeCovered = ( positionMin: number | undefined, positionMax: number | undefined ): boolean => { - // récupérer le pathStepFrom et le pathStepTo const pathStepFrom = pathSteps.find((pathStep) => pathStep.id === powerRestrictionRange.from); const pathStepTo = pathSteps.find((pathStep) => pathStep.id === powerRestrictionRange.to); @@ -85,11 +75,9 @@ export const isRangeCovered = ( return false; } - // comparer les positions return positionMin < pathStepFrom.positionOnPath && pathStepTo.positionOnPath < positionMax; }; -// Fonction pour gérer la suppression et l'ajout de PathStep export const addPathStep = (pathSteps: PathStep[], newPathStep: PathStep): PathStep[] => { const newPathStepExists = pathSteps.some((pathStep) => pathStep.id === newPathStep.id); if (!newPathStepExists) { @@ -102,28 +90,7 @@ export const addPathStep = (pathSteps: PathStep[], newPathStep: PathStep): PathS return pathSteps; }; -export const removePathStep = ( - pathSteps: PathStep[], - pathStepId: string, - powerRestrictions: PowerRestrictionV2[] -): PathStep[] => { - const oldPathStepId = pathStepId; - const oldPathStep = pathSteps.find((pathStep) => pathStep.id === oldPathStepId); - - if (oldPathStep) { - const oldPathStepIsUsed = powerRestrictions.some( - (powerRestriction) => - oldPathStep.id === powerRestriction.from && oldPathStep.id === powerRestriction.to - ); - - if (!oldPathStepIsUsed && checkValidPathStep(oldPathStep)) { - return pathSteps.filter((pathStep) => pathStep !== oldPathStep); - } - } - - return pathSteps; -}; - +/** Remove the unused path steps */ export const cleanPathSteps = ( pathSteps: PathStep[], powerRestrictions: PowerRestrictionV2[] @@ -134,12 +101,7 @@ export const cleanPathSteps = ( return acc; } - const pathStepIsUsed = powerRestrictions.some( - (powerRestriction) => - pathStep.id === powerRestriction.from || pathStep.id === powerRestriction.to - ); - - if (!pathStepIsUsed && checkValidPathStep(pathStep)) { + if (canRemovePathStep(pathStep, powerRestrictions)) { return acc; } diff --git a/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts b/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts index 1a1ed321177..4cad6d9e1ca 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts +++ b/front/src/reducers/osrdconf/osrdConfCommon/__tests__/utils.ts @@ -625,7 +625,7 @@ const testCommonConfReducers = (slice: OperationalStudiesConfSlice | StdcmConfSl it('should handle updatePathSteps', () => { const pathSteps = testDataBuilder.buildPathSteps(); - defaultStore.dispatch(slice.actions.updatePathSteps(pathSteps)); + defaultStore.dispatch(slice.actions.updatePathSteps({ pathSteps })); const state = defaultStore.getState()[slice.name]; expect(state.pathSteps).toEqual(pathSteps); }); diff --git a/front/src/reducers/osrdconf/osrdConfCommon/index.ts b/front/src/reducers/osrdconf/osrdConfCommon/index.ts index de362745f75..6443689ebb1 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/index.ts +++ b/front/src/reducers/osrdconf/osrdConfCommon/index.ts @@ -1,13 +1,11 @@ import type { CaseReducer, PayloadAction, PrepareAction } from '@reduxjs/toolkit'; import type { Draft } from 'immer'; -import { compact, first, isEqual, keyBy, omit, sortBy } from 'lodash'; +import { compact, omit } from 'lodash'; import nextId from 'react-id-generator'; -import type { PointOnMap, PowerRestrictionV2 } from 'applications/operationalStudies/consts'; +import type { PointOnMap } from 'applications/operationalStudies/consts'; import type { ManageTrainSchedulePathProperties } from 'applications/operationalStudies/types'; -import type { IntervalItem } from 'common/IntervalsEditor/types'; import { isVia } from 'modules/pathfinding/utils'; -import { NO_POWER_RESTRICTION } from 'modules/powerRestriction/consts'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; import { type InfraStateReducers, buildInfraStateReducers, infraState } from 'reducers/infra'; import { computeLinkedOriginTimes, insertVia, insertViaFromMap } from 'reducers/osrdconf/helpers'; @@ -23,16 +21,6 @@ import { addElementAtIndex, removeElementAtIndex, replaceElementAtIndex } from ' import { formatIsoDate } from 'utils/date'; import type { ArrayElement } from 'utils/types'; -import { - addPathStep, - checkValidPathStep, - cleanPathSteps, - isRangeCovered, - removePathStep, - updateRestrictions, -} from './utils'; -import { log } from 'console'; - export const defaultCommonConf: OsrdConfState = { constraintDistribution: 'MARECO', name: '', @@ -124,35 +112,12 @@ interface CommonConfReducers extends InfraStateReducers ['updateGridMarginBefore']: CaseReducer>; ['updateGridMarginAfter']: CaseReducer>; ['updatePowerRestrictionRanges']: CaseReducer>; - ['upsertPowerRestrictionRangesV2']: CaseReducer< - S, - PayloadAction<{ from: PathStep; to: PathStep; code: string }> - >; - ['cutPowerRestrictionRangesV2']: CaseReducer>; - ['deletePowerRestrictionRangesV2']: CaseReducer< - S, - PayloadAction<{ from: PathStep; to: PathStep }> - >; - ['resizeSegmentEndInput']: CaseReducer< - S, - PayloadAction<{ - firstRestriction: PowerRestrictionV2; - secondRestriction?: PowerRestrictionV2; - newEndPathStep: PathStep; - }> - >; - ['resizeSegmentBeginInput']: CaseReducer< - S, - PayloadAction<{ - firstRestriction: PowerRestrictionV2; - previousRestriction?: PowerRestrictionV2; - newFromPathStep: PathStep; - }> - >; - ['resetPowerRestrictionRangesV2']: CaseReducer; ['updateTrainScheduleIDsToModify']: CaseReducer>; ['updateFeatureInfoClick']: CaseReducer>; - ['updatePathSteps']: CaseReducer>; + ['updatePathSteps']: CaseReducer< + S, + PayloadAction<{ pathSteps: S['pathSteps']; resetPowerRestrictions?: boolean }> + >; ['updateOriginV2']: CaseReducer>>; ['updateDestinationV2']: CaseReducer>>; ['deleteItineraryV2']: CaseReducer; @@ -377,433 +342,6 @@ export function buildCommonConfReducers(): CommonConfRe ) { state.powerRestrictionRanges = action.payload; }, - - upsertPowerRestrictionRangesV2( - state: Draft, - action: PayloadAction<{ from: PathStep; to: PathStep; code: string }> - ) { - const { from, to, code } = action.payload; - let newPathSteps = [...state.pathSteps]; - const pathIds = compact(state.pathSteps).map((step) => step.id); - - if (!pathIds.includes(from.id)) { - const fromIndex = newPathSteps.findIndex( - (step) => step?.positionOnPath && step.positionOnPath > from.positionOnPath! - ); - newPathSteps = addElementAtIndex(newPathSteps, fromIndex, from); - } - if (!pathIds.includes(to.id)) { - const toIndex = newPathSteps.findIndex( - (step) => step?.positionOnPath && step.positionOnPath > to.positionOnPath! - ); - newPathSteps = addElementAtIndex(newPathSteps, toIndex, to); - } - - const newPathStepsById = keyBy(newPathSteps, 'id'); - let newPowerRestrictionRangesV2 = state.powerRestrictionV2.filter( - (restriction) => restriction.from !== from.id && restriction.to !== to.id - ); - if (code !== NO_POWER_RESTRICTION) { - newPowerRestrictionRangesV2.push({ from: from.id, to: to.id, code }); - newPowerRestrictionRangesV2 = sortBy( - newPowerRestrictionRangesV2, - (range) => newPathStepsById[range.from]?.positionOnPath - ); - } - - state.pathSteps = newPathSteps; - state.powerRestrictionV2 = newPowerRestrictionRangesV2; - }, - - cutPowerRestrictionRangesV2(state: Draft, action: PayloadAction<{ cutAt: PathStep }>) { - const { cutAt } = action.payload; - let newPathSteps = [...state.pathSteps]; - - const pathIds = compact(state.pathSteps).map((step) => step.id); - if (!pathIds.includes(cutAt.id)) { - const cutAtIndex = newPathSteps.findIndex( - (step) => step?.positionOnPath && step.positionOnPath > cutAt.positionOnPath! - ); - - newPathSteps = addElementAtIndex(newPathSteps, cutAtIndex, cutAt); - state.pathSteps = newPathSteps; - - const prevStep = newPathSteps[cutAtIndex - 1]; - const nextStep = newPathSteps[cutAtIndex + 1]; - - if (!prevStep || !nextStep) { - console.error('cutPowerRestrictionRangesV2: prevStep or nextStep is undefined'); - } else { - const newPowerRestrictionRangesV2 = state.powerRestrictionV2.reduce( - (acc, powerRestriction) => { - if (powerRestriction.from === prevStep.id) { - acc.push({ - ...powerRestriction, - to: cutAt.id, - }); - acc.push({ - ...powerRestriction, - from: cutAt.id, - to: nextStep.id, - }); - } else { - acc.push(powerRestriction); - } - return acc; - }, - [] as PowerRestrictionV2[] - ); - state.powerRestrictionV2 = newPowerRestrictionRangesV2; - } - } - }, - - deletePowerRestrictionRangesV2( - state: Draft, - action: PayloadAction<{ from: PathStep; to: PathStep }> - ) { - const { from, to } = action.payload; - - const newPowerRestrictionRangesV2 = state.powerRestrictionV2.filter( - (restriction) => restriction.from !== from.id && restriction.to !== to.id - ); - - const fromIsUsed = newPowerRestrictionRangesV2.some( - (restriction) => restriction.from === from.id || restriction.to === from.id - ); - const toIsUsed = newPowerRestrictionRangesV2.some( - (restriction) => restriction.from === to.id || restriction.to === to.id - ); - - let newPathSteps = [...state.pathSteps]; - if (!fromIsUsed && checkValidPathStep(from)) { - newPathSteps = newPathSteps.filter( - (pathStep) => pathStep?.positionOnPath !== from.positionOnPath - ); - } - if (!toIsUsed && checkValidPathStep(to)) { - newPathSteps = newPathSteps.filter( - (pathStep) => pathStep?.positionOnPath !== to.positionOnPath - ); - } - - state.pathSteps = newPathSteps; - state.powerRestrictionV2 = newPowerRestrictionRangesV2; - }, - - // resizeSegmentEndInput( - // state: Draft, - // action: PayloadAction<{ - // firstRestriction: PowerRestrictionV2; - // secondRestriction?: PowerRestrictionV2; - // newFromPathStep: PathStep; - // newEndPathStep: PathStep; - // intervalsEditorData: IntervalItem[]; - // }> - // ) { - // const { - // firstRestriction, - // secondRestriction, - // newEndPathStep, - // newFromPathStep, - // intervalsEditorData, - // } = action.payload; - // console.log( - // { - // firstRestriction, - // secondRestriction, - // newEndPathStep, - // newFromPathStep, - // intervalsEditorData, - // }, - // 'resizeSegmentEndInput' - // ); - // let newPathSteps = managePathSteps(state.pathSteps, firstRestriction, newEndPathStep, true); - // if (secondRestriction) { - // newPathSteps = managePathSteps(newPathSteps, secondRestriction, newEndPathStep); - // } - // console.log(newPathSteps, 'new path steps'); - // let newPowerRestrictionRanges = state.powerRestrictionV2.filter( - // (restriction) => - // !isEqual(restriction, firstRestriction) || !isEqual(restriction, secondRestriction) - // ); - // newPowerRestrictionRanges = updateRestrictions( - // newPowerRestrictionRanges, - // firstRestriction, - // secondRestriction, - // newEndPathStep.id - // ); - - // console.log(newPowerRestrictionRanges, 'new power restriction ranges after all'); - // if (secondRestriction) { - // console.log(secondRestriction, "'second restriction'"); - // const secondRestrictionToPathStep = newPathSteps.find( - // (pathStep) => pathStep && pathStep.id === secondRestriction.to - // ); - // console.log(secondRestrictionToPathStep, 'second restriction to path step'); - // const secondRestrictionFromPathStep = newPathSteps.find( - // (pathStep) => pathStep && pathStep.id === secondRestriction.from - // ); - // console.log(secondRestrictionFromPathStep, 'second restriction from path step'); - // if (secondRestrictionToPathStep) { - // newPowerRestrictionRanges = newPowerRestrictionRanges.filter( - // (restriction) => - // !isRangeCovered(restriction, newFromPathStep, newEndPathStep, newPathSteps) - // ); - // } - // } - - // state.pathSteps = newPathSteps; - // state.powerRestrictionV2 = newPowerRestrictionRanges; - // }, - - resizeSegmentBeginInput( - state: Draft, - action: PayloadAction<{ - firstRestriction: PowerRestrictionV2; - previousRestriction?: PowerRestrictionV2; - newFromPathStep: PathStep; - }> - ) { - const { firstRestriction, previousRestriction, newFromPathStep } = action.payload; - - // pathSteps should not be undefined or have null values - if (state.pathSteps && !state.pathSteps.some((pathStep) => !pathStep)) { - let newPathSteps = [...state.pathSteps] as PathStep[]; - let newPowerRestrictionRanges = state.powerRestrictionV2.filter( - (restriction) => - !isEqual(restriction, firstRestriction) || !isEqual(restriction, previousRestriction) - ); - - // compute les covered ranges - const pathStepEnd = newPathSteps.find((pathStep) => pathStep.id === firstRestriction.to); - const coveredRanges = pathStepEnd - ? newPowerRestrictionRanges.filter((restriction) => - isRangeCovered( - newPathSteps, - restriction, - newFromPathStep.positionOnPath, - pathStepEnd.positionOnPath - ) - ) - : []; - - // ajoute le nouveau pathStep si besoin - newPathSteps = addPathStep(newPathSteps, newFromPathStep); - - // on update powerRestrictions - newPowerRestrictionRanges = updateRestrictions( - newPowerRestrictionRanges, - previousRestriction, - firstRestriction, - newFromPathStep.id, - coveredRanges - ); - - // on clean les pathSteps - newPathSteps = cleanPathSteps(newPathSteps, newPowerRestrictionRanges); - - state.pathSteps = newPathSteps; - state.powerRestrictionV2 = newPowerRestrictionRanges; - } - }, - - resizeSegmentEndInput( - state: Draft, - action: PayloadAction<{ - firstRestriction: PowerRestrictionV2; - secondRestriction?: PowerRestrictionV2; - newEndPathStep: PathStep; - }> - ) { - const { firstRestriction, secondRestriction, newEndPathStep } = action.payload; - - // pathSteps should not be undefined or have null values - if (state.pathSteps && !state.pathSteps.some((pathStep) => !pathStep)) { - let newPathSteps = [...state.pathSteps] as PathStep[]; - let newPowerRestrictionRanges = state.powerRestrictionV2.filter( - (restriction) => - !isEqual(restriction, firstRestriction) || !isEqual(restriction, secondRestriction) - ); - const pathStepBegin = newPathSteps.find( - (pathStep) => pathStep.id === firstRestriction.from - ); - - // calculer les covered ranges - const coveredRanges = pathStepBegin - ? newPowerRestrictionRanges.filter((restriction) => - isRangeCovered( - newPathSteps, - restriction, - pathStepBegin.positionOnPath, - newEndPathStep.positionOnPath - ) - ) - : []; - - // Add path steps for first restriction - newPathSteps = addPathStep(newPathSteps, newEndPathStep); - - // Update power restrictions - newPowerRestrictionRanges = updateRestrictions( - newPowerRestrictionRanges, - firstRestriction, - secondRestriction, - newEndPathStep.id, - coveredRanges - ); - - newPathSteps = cleanPathSteps(newPathSteps, newPowerRestrictionRanges); - - // Final assignment to state - state.pathSteps = newPathSteps; - state.powerRestrictionV2 = newPowerRestrictionRanges; - } - }, - resetPowerRestrictionRangesV2(state: Draft) { - state.powerRestrictionV2 = []; - state.pathSteps = []; - }, - // resizeSegmentBeginInput( - // state: Draft, - // action: PayloadAction<{ - // firstRestriction: PowerRestrictionV2; - // previousRestriction?: PowerRestrictionV2; - // newFromPathStep: PathStep; - // }> - // ) { - // const { firstRestriction, previousRestriction, newFromPathStep } = action.payload; - // let newPathSteps = [...state.pathSteps]; - // let newPowerRestrictionRanges = state.powerRestrictionV2.filter( - // (restriction) => - // !isEqual(restriction, firstRestriction) || !isEqual(restriction, previousRestriction) - // ); - - // // Remove the previous from pathStep of the first restriction if needed - // const oldPathStepBeginId = firstRestriction.from; - // const oldPathStepBegin = newPathSteps.find( - // (pathStep) => pathStep && pathStep.id === oldPathStepBeginId - // ); - - // if (oldPathStepBegin) { - // console.log(oldPathStepBeginId, 'oldPathStepBeginId'); - // const oldPathStepBeginIsUsed = newPowerRestrictionRanges.some( - // (restriction) => - // restriction.from === oldPathStepBeginId || restriction.to === oldPathStepBeginId - // ); - - // if (oldPathStepBeginIsUsed && checkValidPathStep(oldPathStepBegin)) { - // newPathSteps = newPathSteps.filter((pathStep) => pathStep?.id !== oldPathStepBegin.id); - // } - // } - - // // Create the new from pathStep if it does not exist - // const newFromPathStepExists = newPathSteps.some( - // (pathStep) => pathStep && pathStep.id === newFromPathStep.id - // ); - // if (!newFromPathStepExists) { - // const index = newPathSteps.findIndex( - // (step) => step?.positionOnPath && step.positionOnPath > newFromPathStep.positionOnPath! - // ); - - // newPathSteps = addElementAtIndex(newPathSteps, index, newFromPathStep); - // } - - // // Update the powerRestrictions - // newPowerRestrictionRanges = newPowerRestrictionRanges.map((restriction) => { - // if (restriction.from === firstRestriction.from) { - // return { ...restriction, from: newFromPathStep.id }; - // } - // if (restriction.to === previousRestriction?.to) { - // return { ...restriction, to: newFromPathStep.id }; - // } - // return restriction; - // }); - // console.log(previousRestriction, 'previousRestriction'); - // // Handle previous restriction - // if (previousRestriction) { - // const previousRestrictionFromPathStep = newPathSteps.find( - // (pathStep) => pathStep && pathStep.id === previousRestriction.from - // ); - // const previousRestrictionToPathStep = newPathSteps.find( - // (pathStep) => pathStep && pathStep.id === previousRestriction.to - // ); - - // if (previousRestrictionFromPathStep && previousRestrictionToPathStep) { - // const isRangeCovered = (range: PowerRestrictionV2) => { - // const fromPathStep = newPathSteps.find( - // (pathStep) => pathStep && pathStep.id === range.from - // ); - // const toPathStep = newPathSteps.find( - // (pathStep) => pathStep && pathStep.id === range.to - // ); - - // if ( - // fromPathStep?.positionOnPath === undefined || - // toPathStep?.positionOnPath === undefined - // ) { - // return false; - // } - // return ( - // fromPathStep?.positionOnPath >= newFromPathStep.positionOnPath! && - // toPathStep?.positionOnPath <= previousRestrictionToPathStep.positionOnPath! - // ); - // }; - // newPowerRestrictionRanges = newPowerRestrictionRanges.filter( - // (restriction) => !isRangeCovered(restriction) - // ); - - // const previousRestrictionFromPathStepIsUsed = newPowerRestrictionRanges.some( - // (restriction) => - // restriction.from === previousRestrictionFromPathStep.id || - // restriction.to === previousRestrictionFromPathStep.id - // ); - - // const previousRestrictionToPathStepIsUsed = newPowerRestrictionRanges.some( - // (restriction) => - // restriction.from === previousRestrictionToPathStep.id || - // restriction.to === previousRestrictionToPathStep.id - // ); - - // if ( - // !previousRestrictionFromPathStepIsUsed && - // checkValidPathStep(previousRestrictionFromPathStep) - // ) { - // newPathSteps = newPathSteps.filter( - // (pathStep) => pathStep && pathStep.id !== previousRestrictionFromPathStep.id - // ); - // console.log(newPathSteps, 'newPathSteps du from'); - // } - - // if ( - // !previousRestrictionToPathStepIsUsed && - // checkValidPathStep(previousRestrictionToPathStep) - // ) { - // console.log(newPathSteps, 'newPathSteps du to'); - // newPathSteps = newPathSteps.filter( - // (pathStep) => pathStep && pathStep.id !== previousRestrictionToPathStep.id - // ); - // } - // if ( - // !previousRestrictionFromPathStepIsUsed && - // checkValidPathStep(previousRestrictionFromPathStep) - // ) { - // newPathSteps = newPathSteps.filter( - // (pathStep) => pathStep && pathStep.id === previousRestrictionFromPathStep.id - // ); - // } - // } - // } - - // state.pathSteps = newPathSteps; - // state.powerRestrictionV2 = newPowerRestrictionRanges; - // }, - - // // TODO Remove this - // resetPowerRestrictionRangesV2(state: Draft) { - // state.powerRestrictionV2 = []; - // state.pathSteps = []; - // }, updateTrainScheduleIDsToModify( state: Draft, action: PayloadAction @@ -814,8 +352,14 @@ export function buildCommonConfReducers(): CommonConfRe const feature = omit(action.payload.feature, ['_vectorTileFeature']); state.featureInfoClick = { ...action.payload, feature }; }, - updatePathSteps(state: Draft, action: PayloadAction) { - state.pathSteps = action.payload; + updatePathSteps( + state: Draft, + action: PayloadAction<{ pathSteps: S['pathSteps']; resetPowerRestrictions?: boolean }> + ) { + state.pathSteps = action.payload.pathSteps; + if (action.payload.resetPowerRestrictions) { + state.powerRestrictionV2 = []; + } }, updateOriginV2(state: Draft, action: PayloadAction>) { state.pathSteps = replaceElementAtIndex(state.pathSteps, 0, action.payload); diff --git a/front/src/utils/physics.ts b/front/src/utils/physics.ts index 717772191ce..9e43eca6324 100644 --- a/front/src/utils/physics.ts +++ b/front/src/utils/physics.ts @@ -16,6 +16,11 @@ export function mToMm(length: number) { return length * 1000; } +// Convert meters to km +export function mTokm(length: number) { + return length / 1000; +} + // Convert km/h to m/s export function kmhToMs(v: number) { return Math.abs(v / 3.6); diff --git a/front/src/utils/strings.ts b/front/src/utils/strings.ts index c2a0e960bb5..27dc001ff55 100644 --- a/front/src/utils/strings.ts +++ b/front/src/utils/strings.ts @@ -34,7 +34,7 @@ export const createPowerRestrictions = ( powerRestrictions.push({ from: '0', to: boundaries[0].toString(), - code: currentVoltage, + value: currentVoltage, }); for (let i = 0; i < boundaries.length - 1; i += 1) { @@ -47,7 +47,7 @@ export const createPowerRestrictions = ( powerRestrictions.push({ from: boundaries[i].toString(), to: boundaries[i + 1].toString(), - code: currentVoltage, + value: currentVoltage, }); } }