Skip to content

Commit 2259ae9

Browse files
committed
front: align margins display and computation to spec in timestops table
Signed-off-by: Alice Khoudli <[email protected]>
1 parent ab6cdbb commit 2259ae9

File tree

8 files changed

+92
-41
lines changed

8 files changed

+92
-41
lines changed

front/src/modules/timesStops/helpers/__tests__/computeMargins.spec.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { keyBy } from 'lodash';
12
import { describe, it, expect } from 'vitest';
23

34
import type { TrainScheduleResult } from 'common/api/osrdEditoastApi';
5+
import type { ScheduleEntry } from 'modules/timesStops/types';
46

5-
import computeMargins from '../computeMargins';
7+
import computeMargins, { getTheoreticalMargins } from '../computeMargins';
68

79
describe('computeMargins', () => {
810
const path = [
@@ -28,19 +30,21 @@ describe('computeMargins', () => {
2830

2931
it('should compute simple margin', () => {
3032
const train = { path, margins } as TrainScheduleResult;
31-
expect(computeMargins(train, 0, pathItemTimes)).toEqual({
33+
const scheduleByAt: Record<string, ScheduleEntry> = keyBy(train.schedule, 'at');
34+
const theoreticalMargins = getTheoreticalMargins(train);
35+
expect(computeMargins(theoreticalMargins, train, scheduleByAt, 0, pathItemTimes)).toEqual({
3236
theoreticalMargin: '10 %',
3337
theoreticalMarginSeconds: '10 s',
3438
calculatedMargin: '15 s',
3539
diffMargins: '5 s',
3640
});
37-
expect(computeMargins(train, 1, pathItemTimes)).toEqual({
41+
expect(computeMargins(theoreticalMargins, train, scheduleByAt, 1, pathItemTimes)).toEqual({
3842
theoreticalMargin: '',
3943
theoreticalMarginSeconds: '20 s',
4044
calculatedMargin: '30 s',
4145
diffMargins: '10 s',
4246
});
43-
expect(computeMargins(train, 2, pathItemTimes)).toEqual({
47+
expect(computeMargins(theoreticalMargins, train, scheduleByAt, 2, pathItemTimes)).toEqual({
4448
theoreticalMargin: undefined,
4549
theoreticalMarginSeconds: undefined,
4650
calculatedMargin: undefined,

front/src/modules/timesStops/helpers/computeMargins.ts

+51-31
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,53 @@
11
import type { TrainScheduleResult } from 'common/api/osrdEditoastApi';
2-
import type { TrainScheduleWithDetails } from 'modules/trainschedule/components/Timetable/types';
2+
import type {
3+
TheoreticalMarginsRecord,
4+
TrainScheduleWithDetails,
5+
} from 'modules/trainschedule/components/Timetable/types';
36
import { ms2sec } from 'utils/timeManipulation';
47

58
import { formatDigitsAndUnit } from './utils';
9+
import type { ScheduleEntry } from '../types';
610

7-
function getTheoreticalMargin(selectedTrainSchedule: TrainScheduleResult, pathStepId: string) {
8-
if (selectedTrainSchedule.path.length === 0) {
11+
export function getTheoreticalMargins(selectedTrainSchedule: TrainScheduleResult) {
12+
const selectedTrainScheduleMargins = selectedTrainSchedule.margins;
13+
if (!selectedTrainScheduleMargins) {
914
return undefined;
1015
}
11-
// pathStep is starting point => we take the first margin
12-
if (selectedTrainSchedule.path[0].id === pathStepId) {
13-
return selectedTrainSchedule.margins?.values[0];
14-
}
15-
const theoreticalMarginBoundaryIndex = selectedTrainSchedule.margins?.boundaries?.findIndex(
16-
(id) => id === pathStepId
17-
);
18-
if (
19-
theoreticalMarginBoundaryIndex === undefined ||
20-
theoreticalMarginBoundaryIndex < 0 ||
21-
theoreticalMarginBoundaryIndex > selectedTrainSchedule.margins!.values.length - 2
22-
) {
23-
return undefined;
24-
}
25-
26-
return selectedTrainSchedule.margins!.values[theoreticalMarginBoundaryIndex + 1];
16+
const theoreticalMargins: TheoreticalMarginsRecord = {};
17+
let marginIndex = 0;
18+
selectedTrainSchedule.path.forEach((step, index) => {
19+
let isBoundary = index === 0;
20+
if (step.id === selectedTrainSchedule.margins?.boundaries[marginIndex]) {
21+
marginIndex += 1;
22+
isBoundary = true;
23+
}
24+
theoreticalMargins[step.id] = {
25+
theoreticalMargin: selectedTrainScheduleMargins.values[marginIndex],
26+
isBoundary,
27+
};
28+
});
29+
return theoreticalMargins;
2730
}
2831

2932
function computeMargins(
33+
theoreticalMargins: TheoreticalMarginsRecord | undefined,
3034
selectedTrainSchedule: TrainScheduleResult,
35+
scheduleByAt: Record<string, ScheduleEntry>,
3136
pathStepIndex: number,
3237
pathItemTimes: NonNullable<TrainScheduleWithDetails['pathItemTimes']> // in ms
3338
) {
3439
const { path, margins } = selectedTrainSchedule;
40+
const pathStepId = path[pathStepIndex].id;
3541
if (
3642
!margins ||
37-
(margins.values.length === 1 && margins.values[0] === '0%') ||
38-
pathStepIndex === selectedTrainSchedule.path.length - 1
43+
pathStepIndex === selectedTrainSchedule.path.length - 1 ||
44+
!theoreticalMargins ||
45+
!theoreticalMargins[pathStepId] ||
46+
!(
47+
scheduleByAt[pathStepId]?.arrival ||
48+
scheduleByAt[pathStepId]?.stop_for ||
49+
theoreticalMargins[pathStepId].isBoundary
50+
)
3951
) {
4052
return {
4153
theoreticalMargin: undefined,
@@ -45,14 +57,21 @@ function computeMargins(
4557
};
4658
}
4759

48-
const pathStepId = path[pathStepIndex].id;
49-
const theoreticalMargin = getTheoreticalMargin(selectedTrainSchedule, pathStepId);
60+
const { theoreticalMargin, isBoundary } = theoreticalMargins[pathStepId];
61+
62+
// find the next pathStep where constraints are defined
63+
let nextIndex = path.length - 1;
5064

51-
// find the previous pathStep where margin was defined
52-
let prevIndex = 0;
53-
for (let index = 1; index < pathStepIndex; index += 1) {
54-
if (margins.boundaries.includes(path[index].id)) {
55-
prevIndex = index;
65+
for (let index = pathStepIndex + 1; index < path.length; index += 1) {
66+
const curStepId = path[index].id;
67+
const curStepSchedule = scheduleByAt[curStepId];
68+
if (
69+
theoreticalMargins[curStepId]?.isBoundary ||
70+
curStepSchedule?.arrival ||
71+
curStepSchedule?.stop_for
72+
) {
73+
nextIndex = index;
74+
break;
5675
}
5776
}
5877

@@ -61,16 +80,17 @@ function computeMargins(
6180
// provisional = margins
6281
// final = margins + requested arrival times
6382
const { base, provisional, final } = pathItemTimes;
64-
const baseDuration = ms2sec(base[pathStepIndex + 1] - base[prevIndex]);
65-
const provisionalDuration = ms2sec(provisional[pathStepIndex + 1] - provisional[prevIndex]);
66-
const finalDuration = ms2sec(final[pathStepIndex + 1] - final[prevIndex]);
83+
const baseDuration = ms2sec(base[nextIndex] - base[pathStepIndex]);
84+
const provisionalDuration = ms2sec(provisional[nextIndex] - provisional[pathStepIndex]);
85+
const finalDuration = ms2sec(final[nextIndex] - final[pathStepIndex]);
6786

6887
// how much longer it took (s) with the margin than without
6988
const provisionalLostTime = provisionalDuration - baseDuration;
7089
const finalLostTime = finalDuration - baseDuration;
7190

7291
return {
7392
theoreticalMargin: formatDigitsAndUnit(theoreticalMargin),
93+
isTheoreticalMarginBoundary: isBoundary,
7494
theoreticalMarginSeconds: `${Math.round(provisionalLostTime)} s`,
7595
calculatedMargin: `${Math.round(finalLostTime)} s`,
7696
diffMargins: `${Math.round(finalLostTime - provisionalLostTime)} s`,

front/src/modules/timesStops/helpers/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const getDigits = (unit: string | undefined) =>
114114
unit === MarginUnit.second || unit === MarginUnit.percent ? 0 : 1;
115115

116116
export function formatDigitsAndUnit(fullValue: string | number | undefined, unit?: string) {
117-
if (fullValue === undefined || fullValue === '0%') {
117+
if (fullValue === undefined) {
118118
return '';
119119
}
120120
if (typeof fullValue === 'number') {

front/src/modules/timesStops/hooks/useOutputTableData.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { calculateTimeDifferenceInSeconds } from 'utils/timeManipulation';
1515

1616
import { ARRIVAL_TIME_ACCEPTABLE_ERROR_MS } from '../consts';
1717
import { computeInputDatetimes } from '../helpers/arrivalTime';
18-
import computeMargins from '../helpers/computeMargins';
18+
import computeMargins, { getTheoreticalMargins } from '../helpers/computeMargins';
1919
import { formatSchedule } from '../helpers/scheduleData';
2020
import { type ScheduleEntry, type TimeStopsRow } from '../types';
2121

@@ -29,6 +29,8 @@ const useOutputTableData = (
2929
const { t } = useTranslation('timesStops');
3030

3131
const scheduleByAt: Record<string, ScheduleEntry> = keyBy(selectedTrainSchedule?.schedule, 'at');
32+
const theoreticalMargins = selectedTrainSchedule && getTheoreticalMargins(selectedTrainSchedule);
33+
3234
const startDatetime = selectedTrainSchedule
3335
? new Date(selectedTrainSchedule.start_time)
3436
: undefined;
@@ -48,8 +50,19 @@ const useOutputTableData = (
4850
computedArrival,
4951
schedule
5052
);
51-
const { theoreticalMargin, theoreticalMarginSeconds, calculatedMargin, diffMargins } =
52-
computeMargins(selectedTrainSchedule, index, pathItemTimes);
53+
const {
54+
theoreticalMargin,
55+
isTheoreticalMarginBoundary,
56+
theoreticalMarginSeconds,
57+
calculatedMargin,
58+
diffMargins,
59+
} = computeMargins(
60+
theoreticalMargins,
61+
selectedTrainSchedule,
62+
scheduleByAt,
63+
index,
64+
pathItemTimes
65+
);
5366

5467
const { theoreticalArrival, arrival, departure, refDate } = computeInputDatetimes(
5568
startDatetime,
@@ -78,6 +91,7 @@ const useOutputTableData = (
7891
onStopSignal,
7992
shortSlipDistance,
8093
theoreticalMargin,
94+
isTheoreticalMarginBoundary,
8195

8296
theoreticalMarginSeconds,
8397
calculatedMargin,

front/src/modules/timesStops/hooks/useTimeStopsColumns.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export const useTimeStopsColumns = <T extends TimeStopsRow>(
158158
continuousUpdates: false,
159159
placeholder: !isOutputTable ? t('theoreticalMarginPlaceholder') : '',
160160
formatBlurredInput: (value) => {
161-
if (!value || value === '0%') return '';
161+
if (!value) return '';
162162
if (!isOutputTable && !marginRegExValidation.test(value)) {
163163
return `${value}${t('theoreticalMarginPlaceholder')}`;
164164
}
@@ -168,7 +168,10 @@ export const useTimeStopsColumns = <T extends TimeStopsRow>(
168168
})
169169
),
170170
cellClassName: ({ rowData }) =>
171-
cx({ invalidCell: !isOutputTable && !rowData.isMarginValid }),
171+
cx({
172+
invalidCell: !isOutputTable && !rowData.isMarginValid,
173+
repeatedValue: rowData.isTheoreticalMarginBoundary === false, // the class should be added on false but not undefined
174+
}),
172175
title: t('theoreticalMargin'),
173176
headerClassName: 'padded-header',
174177
minWidth: 100,

front/src/modules/timesStops/styles/_timesStopsDatasheet.scss

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
color: red !important;
2424
}
2525

26+
.repeatedValue {
27+
color: var(--grey30);
28+
}
29+
2630
.warning-schedule {
2731
background: var(--warning30);
2832
}

front/src/modules/timesStops/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export type TimeStopsRow = {
2121
onStopSignal?: boolean;
2222
shortSlipDistance?: boolean;
2323
theoreticalMargin?: string; // value asked by user
24+
isTheoreticalMarginBoundary?: boolean; // tells whether the theoreticalMargin value was inputted for this line or if it is repeated from a previous line
2425

2526
theoreticalMarginSeconds?: string;
2627
calculatedMargin?: string;

front/src/modules/trainschedule/components/Timetable/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,8 @@ export type InvalidReason =
4343
| Extract<SimulationSummaryResult['status'], 'pathfinding_failure' | 'simulation_failed'>
4444
| PathfindingNotFound['error_type']
4545
| PathfindingInputError['error_type'];
46+
47+
export type TheoreticalMarginsRecord = Record<
48+
string,
49+
{ theoreticalMargin: string; isBoundary: boolean }
50+
>;

0 commit comments

Comments
 (0)