diff --git a/front/src/applications/operationalStudies/helpers/formatTrainScheduleSummaries.ts b/front/src/applications/operationalStudies/helpers/formatTrainScheduleSummaries.ts index 68b1a535b35..9556690f169 100644 --- a/front/src/applications/operationalStudies/helpers/formatTrainScheduleSummaries.ts +++ b/front/src/applications/operationalStudies/helpers/formatTrainScheduleSummaries.ts @@ -59,7 +59,7 @@ const formatTrainScheduleSummaries = ( }; return { - id: trainSchedule.id, + ...trainSchedule, trainName: trainSchedule.train_name, startTime, stopsCount: diff --git a/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts b/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts index 7775fa7629b..913fdbf6572 100644 --- a/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts +++ b/front/src/applications/operationalStudies/hooks/useSetupItineraryForTrainUpdate.ts @@ -16,16 +16,13 @@ import { import { useOsrdConfActions, useOsrdConfSelectors } from 'common/osrdContext'; import { formatSuggestedOperationalPoints, upsertPathStepsInOPs } from 'modules/pathfinding/utils'; import { getSupportedElectrification, isThermal } from 'modules/rollingStock/helpers/electric'; -import { adjustConfWithTrainToModify } from 'modules/trainschedule/components/ManageTrainSchedule/helpers/adjustConfWithTrainToModify'; import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types'; +import computeBasePathSteps from 'modules/trainschedule/helpers/computeBasePathSteps'; import { setFailure } from 'reducers/main'; -import type { OperationalStudiesConfSliceActions } from 'reducers/osrdconf/operationalStudiesConf'; import type { PathStep } from 'reducers/osrdconf/types'; import { useAppDispatch } from 'store'; import { castErrorToFailure } from 'utils/error'; import { getPointCoordinates } from 'utils/geometry'; -import { mmToM } from 'utils/physics'; -import { ISO8601Duration2sec } from 'utils/timeManipulation'; import type { ManageTrainSchedulePathProperties } from '../types'; @@ -34,48 +31,6 @@ type ItineraryForTrainUpdate = { pathProperties: ManageTrainSchedulePathProperties; }; -/** - * create pathSteps in the case pathfinding fails or the train is imported from NGE - */ -const computeBasePathSteps = (trainSchedule: TrainScheduleResult) => - trainSchedule.path.map((step) => { - const correspondingSchedule = trainSchedule.schedule?.find( - (schedule) => schedule.at === step.id - ); - - const { - arrival, - stop_for: stopFor, - locked, - reception_signal: receptionSignal, - } = correspondingSchedule || {}; - - const stepWithoutSecondaryCode = omit(step, ['secondary_code']); - - if ('track' in stepWithoutSecondaryCode) { - stepWithoutSecondaryCode.offset = mmToM(stepWithoutSecondaryCode.offset!); - } - - let name; - if ('trigram' in step) { - name = step.trigram + (step.secondary_code ? `/${step.secondary_code}` : ''); - } else if ('uic' in step) { - name = step.uic.toString(); - } else if ('operational_point' in step) { - name = step.operational_point; - } - - return { - ...stepWithoutSecondaryCode, - ch: 'secondary_code' in step ? step.secondary_code : undefined, - name, - arrival, // ISODurationString - stopFor: stopFor ? ISO8601Duration2sec(stopFor).toString() : stopFor, - locked, - receptionSignal, - } as PathStep; - }); - export function updatePathStepsFromOperationalPoints( pathSteps: PathStep[], suggestedOperationalPoints: SuggestedOP[], @@ -122,11 +77,12 @@ const useSetupItineraryForTrainUpdate = ( setPathProperties: (pathProperties: ManageTrainSchedulePathProperties) => void, trainIdToEdit: number ) => { - const { getInfraID, getUsingElectricalProfiles } = useOsrdConfSelectors(); + const { getInfraID } = useOsrdConfSelectors(); const infraId = useSelector(getInfraID); - const usingElectricalProfiles = useSelector(getUsingElectricalProfiles); const dispatch = useAppDispatch(); - const osrdActions = useOsrdConfActions() as OperationalStudiesConfSliceActions; + + const { updatePathSteps } = useOsrdConfActions(); + const [getTrainScheduleById] = osrdEditoastApi.endpoints.getTrainScheduleById.useLazyQuery({}); const [getRollingStockByName] = osrdEditoastApi.endpoints.getRollingStockNameByRollingStockName.useLazyQuery(); @@ -237,7 +193,6 @@ const useSetupItineraryForTrainUpdate = ( const trainSchedule = await getTrainScheduleById({ id: trainIdToEdit }).unwrap(); let rollingStock: RollingStockWithLiveries | null = null; - let pathSteps: (PathStep | null)[] | undefined; if (trainSchedule.rolling_stock_name) { try { @@ -245,7 +200,11 @@ const useSetupItineraryForTrainUpdate = ( rollingStockName: trainSchedule.rolling_stock_name, }).unwrap(); const itinerary = await computeItineraryForTrainUpdate(trainSchedule, rollingStock); - pathSteps = itinerary?.pathSteps; + const pathSteps = itinerary?.pathSteps; + + if (pathSteps) { + dispatch(updatePathSteps({ pathSteps })); + } if (itinerary?.pathProperties) { setPathProperties(itinerary.pathProperties); @@ -254,15 +213,6 @@ const useSetupItineraryForTrainUpdate = ( dispatch(setFailure(castErrorToFailure(e))); } } - - adjustConfWithTrainToModify( - trainSchedule, - pathSteps || computeBasePathSteps(trainSchedule), - rollingStock?.id, - dispatch, - usingElectricalProfiles, - osrdActions - ); }; setupItineraryForTrainUpdate(); diff --git a/front/src/modules/trainschedule/components/Timetable/TimetableTrainCard.tsx b/front/src/modules/trainschedule/components/Timetable/TimetableTrainCard.tsx index 88fe3cddc31..2bc25621fc5 100644 --- a/front/src/modules/trainschedule/components/Timetable/TimetableTrainCard.tsx +++ b/front/src/modules/trainschedule/components/Timetable/TimetableTrainCard.tsx @@ -11,9 +11,11 @@ import { GiPathDistance } from 'react-icons/gi'; import { MANAGE_TRAIN_SCHEDULE_TYPES } from 'applications/operationalStudies/consts'; import { osrdEditoastApi } from 'common/api/osrdEditoastApi'; import type { TrainScheduleBase, TrainScheduleResult } from 'common/api/osrdEditoastApi'; +import { useOsrdConfActions } from 'common/osrdContext'; import RollingStock2Img from 'modules/rollingStock/components/RollingStock2Img'; import trainNameWithNum from 'modules/trainschedule/components/ManageTrainSchedule/helpers/trainNameHelper'; import { setFailure, setSuccess } from 'reducers/main'; +import type { OperationalStudiesConfSliceActions } from 'reducers/osrdconf/operationalStudiesConf'; import { updateTrainIdUsedForProjection, updateSelectedTrainId } from 'reducers/simulationResults'; import { useAppDispatch } from 'store'; import { formatToIsoDate, isoDateToMs } from 'utils/date'; @@ -51,6 +53,8 @@ const TimetableTrainCard = ({ }: TimetableTrainCardProps) => { const { t } = useTranslation(['operationalStudies/scenario']); const dispatch = useAppDispatch(); + const { selectTrainToEdit } = useOsrdConfActions() as OperationalStudiesConfSliceActions; + const [postTrainSchedule] = osrdEditoastApi.endpoints.postTimetableByIdTrainSchedule.useMutation(); const [getTrainSchedule] = osrdEditoastApi.endpoints.postTrainSchedule.useLazyQuery(); @@ -61,6 +65,7 @@ const TimetableTrainCard = ({ }; const editTrainSchedule = () => { + dispatch(selectTrainToEdit(train)); setTrainIdToEdit(train.id); setDisplayTrainScheduleManagement(MANAGE_TRAIN_SCHEDULE_TYPES.edit); }; diff --git a/front/src/modules/trainschedule/components/Timetable/types.ts b/front/src/modules/trainschedule/components/Timetable/types.ts index 376817b0a90..aad69689cc9 100644 --- a/front/src/modules/trainschedule/components/Timetable/types.ts +++ b/front/src/modules/trainschedule/components/Timetable/types.ts @@ -3,14 +3,17 @@ import type { PathfindingInputError, PathfindingNotFound, SimulationSummaryResult, + TrainScheduleResult, } from 'common/api/osrdEditoastApi'; export type ValidityFilter = 'both' | 'valid' | 'invalid'; export type ScheduledPointsHonoredFilter = 'both' | 'honored' | 'notHonored'; -export type TrainScheduleWithDetails = { - id: number; +export type TrainScheduleWithDetails = Omit< + TrainScheduleResult, + 'train_name' | 'rolling_stock_name' | 'timetable_id' +> & { trainName: string; startTime: Date; arrivalTime: Date | null; diff --git a/front/src/modules/trainschedule/helpers/computeBasePathSteps.ts b/front/src/modules/trainschedule/helpers/computeBasePathSteps.ts new file mode 100644 index 00000000000..3b55d4b01c2 --- /dev/null +++ b/front/src/modules/trainschedule/helpers/computeBasePathSteps.ts @@ -0,0 +1,68 @@ +import type { TrainScheduleResult } from 'common/api/osrdEditoastApi'; +import { omit } from 'lodash'; +import type { PathStep } from 'reducers/osrdconf/types'; +import { mmToM } from 'utils/physics'; +import { ISO8601Duration2sec } from 'utils/timeManipulation'; + +const findCorrespondingMargin = ( + stepId: string, + stepIndex: number, + margins: { boundaries: string[]; values: string[] } +) => { + // The first pathStep will never have its id in boundaries + if (stepIndex === 0) return margins.values[0] === 'none' ? undefined : margins.values[0]; + + const marginIndex = margins.boundaries.findIndex((boundaryId) => boundaryId === stepId); + + return marginIndex !== -1 ? margins.values[marginIndex + 1] : undefined; +}; + +/** Given a trainScheduleResult, extract its pathSteps */ +const computeBasePathSteps = ( + trainSchedule: Pick +) => + trainSchedule.path.map((step, index) => { + const correspondingSchedule = trainSchedule.schedule?.find( + (schedule) => schedule.at === step.id + ); + + const { + arrival, + stop_for: stopFor, + locked, + reception_signal: receptionSignal, + } = correspondingSchedule || {}; + + const stepWithoutSecondaryCode = omit(step, ['secondary_code']); + + if ('track' in stepWithoutSecondaryCode) { + stepWithoutSecondaryCode.offset = mmToM(stepWithoutSecondaryCode.offset!); + } + + let name; + if ('trigram' in step) { + name = step.trigram + (step.secondary_code ? `/${step.secondary_code}` : ''); + } else if ('uic' in step) { + name = step.uic.toString(); + } else if ('operational_point' in step) { + name = step.operational_point; + } + + let theoreticalMargin; + if (trainSchedule.margins && index !== trainSchedule.path.length - 1) { + theoreticalMargin = findCorrespondingMargin(step.id, index, trainSchedule.margins); + } + + return { + ...stepWithoutSecondaryCode, + ch: 'secondary_code' in step ? step.secondary_code : undefined, + name, + arrival, // ISODurationString + stopFor: stopFor ? ISO8601Duration2sec(stopFor).toString() : stopFor, + locked, + receptionSignal, + theoreticalMargin, + } as PathStep; + }); + +export default computeBasePathSteps; diff --git a/front/src/reducers/osrdconf/operationalStudiesConf/index.ts b/front/src/reducers/osrdconf/operationalStudiesConf/index.ts index 3c1f7132b1d..bb9dbba64ea 100644 --- a/front/src/reducers/osrdconf/operationalStudiesConf/index.ts +++ b/front/src/reducers/osrdconf/operationalStudiesConf/index.ts @@ -1,7 +1,11 @@ -import { createSlice } from '@reduxjs/toolkit'; +import { createSlice, type Draft, type PayloadAction } from '@reduxjs/toolkit'; +import type { TrainScheduleWithDetails } from 'modules/trainschedule/components/Timetable/types'; +import computeBasePathSteps from 'modules/trainschedule/helpers/computeBasePathSteps'; import { defaultCommonConf, buildCommonConfReducers } from 'reducers/osrdconf/osrdConfCommon'; import type { OsrdConfState } from 'reducers/osrdconf/types'; +import { convertIsoUtcToLocalTime } from 'utils/date'; +import { msToKmh } from 'utils/physics'; import { builPowerRestrictionReducer } from './powerRestrictionReducer'; @@ -13,6 +17,33 @@ export const operationalStudiesConfSlice = createSlice({ reducers: { ...buildCommonConfReducers(), ...builPowerRestrictionReducer(), + selectTrainToEdit( + state: Draft, + action: PayloadAction + ) { + const { + rollingStock, + trainName, + initial_speed, + start_time, + options, + speedLimitTag, + labels, + power_restrictions, + } = action.payload; + + state.rollingStockID = rollingStock?.id; + state.pathSteps = computeBasePathSteps(action.payload); + state.startTime = convertIsoUtcToLocalTime(start_time); + + state.name = trainName; + state.initialSpeed = initial_speed ? Math.floor(msToKmh(initial_speed) * 10) / 10 : 0; + + state.usingElectricalProfiles = options?.use_electrical_profiles ?? true; + state.labels = labels; + state.speedLimitByTag = speedLimitTag || undefined; + state.powerRestriction = power_restrictions || []; + }, }, }); diff --git a/front/src/reducers/osrdconf/operationalStudiesConf/simulationConfReducers.spec.ts b/front/src/reducers/osrdconf/operationalStudiesConf/simulationConfReducers.spec.ts index 665ae602e7a..6ddec01387d 100644 --- a/front/src/reducers/osrdconf/operationalStudiesConf/simulationConfReducers.spec.ts +++ b/front/src/reducers/osrdconf/operationalStudiesConf/simulationConfReducers.spec.ts @@ -1,5 +1,7 @@ import { describe, it, expect } from 'vitest'; +import type { LightRollingStockWithLiveries } from 'common/api/osrdEditoastApi'; +import type { TrainScheduleWithDetails } from 'modules/trainschedule/components/Timetable/types'; import { operationalStudiesConfSlice } from 'reducers/osrdconf/operationalStudiesConf'; import { defaultCommonConf } from 'reducers/osrdconf/osrdConfCommon'; import testCommonConfReducers from 'reducers/osrdconf/osrdConfCommon/__tests__/utils'; @@ -19,5 +21,68 @@ describe('simulationConfReducer', () => { expect(state).toEqual(defaultCommonConf); }); + it('selectTrainToEdit', () => { + const trainSchedule: TrainScheduleWithDetails = { + id: 1, + trainName: 'train1', + constraint_distribution: 'MARECO', + start_time: '2021-01-01T00:00:00Z', + rollingStock: { id: 1, name: 'rollingStock1' } as LightRollingStockWithLiveries, + path: [ + { id: 'id1', uic: 123 }, + { id: 'id2', uic: 234 }, + ], + margins: { boundaries: ['id2'], values: ['10%', '0%'] }, + startTime: new Date('2021-01-01T00:00:00Z'), + arrivalTime: null, + duration: 1000, + stopsCount: 2, + pathLength: '100', + mechanicalEnergyConsumed: 100, + speedLimitTag: 'MA100', + labels: ['label1'], + isValid: true, + options: { use_electrical_profiles: false }, + }; + + const store = createStore(); + store.dispatch(operationalStudiesConfSlice.actions.selectTrainToEdit(trainSchedule)); + + const state = store.getState()[operationalStudiesConfSlice.name]; + expect(state).toEqual({ + ...defaultCommonConf, + usingElectricalProfiles: false, + labels: ['label1'], + rollingStockID: 1, + speedLimitByTag: 'MA100', + name: 'train1', + pathSteps: [ + { + id: 'id1', + uic: 123, + name: '123', + theoreticalMargin: '10%', + ch: undefined, + arrival: undefined, + stopFor: undefined, + locked: undefined, + receptionSignal: undefined, + }, + { + id: 'id2', + uic: 234, + name: '234', + theoreticalMargin: undefined, + ch: undefined, + arrival: undefined, + stopFor: undefined, + locked: undefined, + receptionSignal: undefined, + }, + ], + startTime: '2021-01-01T00:00:00+00:00', + }); + }); + testCommonConfReducers(operationalStudiesConfSlice); });