Skip to content

Commit

Permalink
front: fix train edition setup
Browse files Browse the repository at this point in the history
Since recently, when opening the view to edit a train, a pathfinding request is automatically launched. But it is launched
before the store update, which creates the bug: the pathfinding is launched with the pathsteps stored in the store (and not the ones
of the train we want to edit) and once the pathfinding request is finished, we update the store with the result of the request (and erase
the path steps of the currently selected train).

To fix this, we can directly update the store before opening the ManageTrainSchedule view.

Signed-off-by: Clara Ni <clara.ni@outlook.fr>
  • Loading branch information
clarani committed Nov 22, 2024
1 parent e32c89b commit 66abf82
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const formatTrainScheduleSummaries = (
};

return {
id: trainSchedule.id,
...trainSchedule,
trainName: trainSchedule.train_name,
startTime,
stopsCount:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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[],
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -237,15 +193,18 @@ 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 {
rollingStock = await getRollingStockByName({
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);
Expand All @@ -254,15 +213,6 @@ const useSetupItineraryForTrainUpdate = (
dispatch(setFailure(castErrorToFailure(e)));
}
}

adjustConfWithTrainToModify(
trainSchedule,
pathSteps || computeBasePathSteps(trainSchedule),
rollingStock?.id,
dispatch,
usingElectricalProfiles,
osrdActions
);
};

setupItineraryForTrainUpdate();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand All @@ -61,6 +65,7 @@ const TimetableTrainCard = ({
};

const editTrainSchedule = () => {
dispatch(selectTrainToEdit(train));
setTrainIdToEdit(train.id);
setDisplayTrainScheduleManagement(MANAGE_TRAIN_SCHEDULE_TYPES.edit);
};
Expand Down
7 changes: 5 additions & 2 deletions front/src/modules/trainschedule/components/Timetable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
68 changes: 68 additions & 0 deletions front/src/modules/trainschedule/helpers/computeBasePathSteps.ts
Original file line number Diff line number Diff line change
@@ -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<TrainScheduleResult, 'path' | 'schedule' | 'margins'>
) =>
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;
33 changes: 32 additions & 1 deletion front/src/reducers/osrdconf/operationalStudiesConf/index.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -13,6 +17,33 @@ export const operationalStudiesConfSlice = createSlice({
reducers: {
...buildCommonConfReducers<OperationalStudiesConfState>(),
...builPowerRestrictionReducer<OperationalStudiesConfState>(),
selectTrainToEdit(
state: Draft<OperationalStudiesConfState>,
action: PayloadAction<TrainScheduleWithDetails>
) {
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 || [];
},
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);
});

0 comments on commit 66abf82

Please sign in to comment.