Skip to content

Commit

Permalink
front: handle train schedule with times exceeding midnight
Browse files Browse the repository at this point in the history
  • Loading branch information
anisometropie committed Sep 17, 2024
1 parent 2871b0d commit 5a2c9e0
Show file tree
Hide file tree
Showing 32 changed files with 1,001 additions and 190 deletions.
1 change: 1 addition & 0 deletions front/public/locales/en/timesStops.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"arrivalTime": "Requested arrival Time",
"calculatedArrivalTime": "Calculated arrival time",
"calculatedDepartureTime": "Calculated departure time",
"dayCounter": "D+{{count}}",
"departureTime": "Requested departure Time",
"diffMargins": "Margins diff.",
"name": "Name",
Expand Down
1 change: 1 addition & 0 deletions front/public/locales/fr/timesStops.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"arrivalTime": "Arrivée demandée",
"calculatedArrivalTime": "Arrivée calculée",
"calculatedDepartureTime": "Départ calculé",
"dayCounter": "J+{{count}}",
"departureTime": "Départ demandé",
"diffMargins": "Diff. marges",
"name": "Nom",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { setFailure } from 'reducers/main';
import type { OperationalStudiesConfSliceActions } from 'reducers/osrdconf/operationalStudiesConf';
import type { PathStep } from 'reducers/osrdconf/types';
import { useAppDispatch } from 'store';
import { addDurationToIsoDate } from 'utils/date';
import { castErrorToFailure } from 'utils/error';
import { getPointCoordinates } from 'utils/geometry';
import { mmToM } from 'utils/physics';
Expand Down Expand Up @@ -70,9 +69,7 @@ const computeBasePathSteps = (trainSchedule: TrainScheduleResult) =>
...stepWithoutSecondaryCode,
ch: 'secondary_code' in step ? step.secondary_code : undefined,
name,
arrival: arrival
? addDurationToIsoDate(trainSchedule.start_time, arrival).substring(11, 19)
: arrival,
arrival, // ISODurationString
stopFor: stopFor ? ISO8601Duration2sec(stopFor).toString() : stopFor,
locked,
onStopSignal,
Expand Down
15 changes: 15 additions & 0 deletions front/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ export const DATA_TYPES = {
*/
export type TimeString = string;

/**
* A string with the complete iso format
*
* @example "2024-08-08T10:12:46.209Z"
* @example "2024-08-08T10:12:46Z"
* @example "2024-08-08T10:12:46+02:00"
*/
export type IsoDateTimeString = string;

/**
* A ISO 8601 duration string
* @example "PT3600S"
*/
export type IsoDurationString = string;

export type RangedValue = {
begin: number;
end: number;
Expand Down
13 changes: 10 additions & 3 deletions front/src/modules/pathfinding/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,11 @@ export const upsertPathStepsInOPs = (ops: SuggestedOP[], pathSteps: PathStep[]):
return updatedOPs;
};

export const pathStepMatchesOp = (pathStep: PathStep, op: SuggestedOP, withKP = false) =>
export const pathStepMatchesOp = (
pathStep: PathStep,
op: Pick<SuggestedOP, 'uic' | 'ch' | 'kp' | 'name' | 'opId'>,
withKP = false
) =>
('uic' in pathStep &&
'ch' in pathStep &&
pathStep.uic === op.uic &&
Expand All @@ -173,5 +177,8 @@ export const pathStepMatchesOp = (pathStep: PathStep, op: SuggestedOP, withKP =
* @param withKP - If true, we check the kp compatibility instead of the name.
* It is used in the times and stops table to check if an operational point is a via.
*/
export const isVia = (vias: PathStep[], op: SuggestedOP, withKP = false) =>
vias.some((via) => pathStepMatchesOp(via, op, withKP));
export const isVia = (
vias: PathStep[],
op: Pick<SuggestedOP, 'uic' | 'ch' | 'kp' | 'name' | 'opId'>,
{ withKP = false } = {}
) => vias.some((via) => pathStepMatchesOp(via, op, withKP));
23 changes: 23 additions & 0 deletions front/src/modules/timesStops/ReadOnlyTime.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { CellProps } from 'react-datasheet-grid/dist/types';
import { useTranslation } from 'react-i18next';

import { NO_BREAK_SPACE } from 'utils/strings';

import type { TimeExtraDays } from './types';

type ReadOnlyTimeProps = CellProps<TimeExtraDays | undefined, string>;

const ReadOnlyTime = ({ rowData }: ReadOnlyTimeProps) => {
const { time, daySinceDeparture, dayDisplayed } = rowData || {};
if (!time) {
return null;
}
const { t } = useTranslation('timesStops');
const fullString =
daySinceDeparture !== undefined && dayDisplayed
? `${time}${NO_BREAK_SPACE}${t('dayCounter', { count: daySinceDeparture })}`
: time;
return <div className="read-only-time">{fullString}</div>;
};

export default ReadOnlyTime;
41 changes: 29 additions & 12 deletions front/src/modules/timesStops/TimeInput.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { useRef, useState, useEffect } from 'react';

import cx from 'classnames';
import type { CellProps } from 'react-datasheet-grid/dist/types';
import { useTranslation } from 'react-i18next';

const TimeInput = ({
focus,
rowData,
active,
setRowData,
}: CellProps<string | null | undefined, string>) => {
import type { TimeExtraDays } from './types';

type TimeInputProps = CellProps<TimeExtraDays | undefined, string>;

const TimeInput = ({ focus, rowData, active, setRowData }: TimeInputProps) => {
const { t } = useTranslation('timesStops');
const ref = useRef<HTMLInputElement>(null);
const [tempTimeValue, setTempTimeValue] = useState<string | null | undefined>(rowData);
const [tempTimeValue, setTempTimeValue] = useState<TimeExtraDays | undefined>(rowData);

useEffect(() => {
if (active) {
Expand All @@ -26,8 +28,9 @@ const TimeInput = ({
setTempTimeValue(rowData);
}, [rowData]);

return (
const input = (
<input
// className from react-datasheet-grid library
className="dsg-input"
type="time"
tabIndex={-1}
Expand All @@ -37,9 +40,9 @@ const TimeInput = ({
pointerEvents: focus ? 'auto' : 'none',
opacity: rowData || active ? undefined : 0,
}}
value={tempTimeValue ?? ''}
value={tempTimeValue?.time ?? ''}
onChange={(e) => {
setTempTimeValue(e.target.value);
setTempTimeValue((prev) => ({ ...prev, time: e.target.value }));
}}
onBlur={() => {
// To prevent the operational point to be transformed into a via if we leave the cell empty after focusing it
Expand All @@ -49,8 +52,22 @@ const TimeInput = ({
}}
/>
);
};

TimeInput.displayName = 'TimeInput';
if (tempTimeValue?.daySinceDeparture && tempTimeValue.dayDisplayed) {
return (
<div className="time-input-container">
{input}
<span
className={cx('extra-text', {
'extra-text-firefox': navigator.userAgent.search('Firefox') !== -1,
})}
>
{t('dayCounter', { count: tempTimeValue.daySinceDeparture })}
</span>
</div>
);
}
return input;
};

export default TimeInput;
52 changes: 40 additions & 12 deletions front/src/modules/timesStops/TimesStops.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
import { useState, useEffect } from 'react';

import cx from 'classnames';
import { isEqual } from 'lodash';
import { DynamicDataSheetGrid, type DataSheetGridProps } from 'react-datasheet-grid';
import { useTranslation } from 'react-i18next';

import { useOsrdConfActions } from 'common/osrdContext';
import type { IsoDateTimeString } from 'common/types';
import { isVia } from 'modules/pathfinding/utils';
import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types';
import type { PathStep } from 'reducers/osrdconf/types';
import { useAppDispatch } from 'store';
import { time2sec } from 'utils/timeManipulation';

import { marginRegExValidation } from './consts';
import { formatSuggestedViasToRowVias, transformRowDataOnChange } from './helpers/utils';
import {
formatSuggestedViasToRowVias,
updateRowTimesAndMargin,
updateDaySinceDeparture,
durationSinceStartTime,
} from './helpers/utils';
import { useTimeStopsColumns } from './hooks/useTimeStopsColumns';
import { TableType } from './types';
import type { PathWaypointRow } from './types';
Expand All @@ -22,7 +27,7 @@ export const WITH_KP = true;
type TimesStopsProps = {
allWaypoints?: SuggestedOP[];
pathSteps?: PathStep[];
startTime?: string;
startTime?: IsoDateTimeString;
tableType: TableType;
cellClassName?: DataSheetGridProps['cellClassName'];
stickyRightColumn?: DataSheetGridProps['stickyRightColumn'];
Expand All @@ -42,7 +47,7 @@ const TimesStops = ({
const { t } = useTranslation('timesStops');

const dispatch = useAppDispatch();
const { upsertViaFromSuggestedOP } = useOsrdConfActions();
const { upsertSeveralViasFromSuggestedOP } = useOsrdConfActions();

const [rows, setRows] = useState<PathWaypointRow[]>([]);

Expand All @@ -55,7 +60,7 @@ const TimesStops = ({
startTime,
tableType
);
setRows(suggestedOPs);
setRows(updateDaySinceDeparture(suggestedOPs, startTime, true));
}
}, [allWaypoints, pathSteps, startTime]);

Expand All @@ -74,15 +79,38 @@ const TimesStops = ({
className="time-stops-datasheet"
columns={columns}
value={rows}
onChange={(row: PathWaypointRow[], [op]) => {
onChange={(newRows: PathWaypointRow[], [op]) => {
if (!isInputTable) {
return;
}
const newRowData = transformRowDataOnChange(row[op.fromRowIndex], rows[op.fromRowIndex], op, allWaypoints.length);
if (!newRowData.isMarginValid) {
setRows(row);
let updatedRows = [...newRows];
updatedRows[op.fromRowIndex] = updateRowTimesAndMargin(
newRows[op.fromRowIndex],
rows[op.fromRowIndex],
op,
allWaypoints.length
);
updatedRows = updateDaySinceDeparture(updatedRows, startTime);
if (!updatedRows[op.fromRowIndex].isMarginValid) {
newRows[op.fromRowIndex].isMarginValid = false;
setRows(newRows);
} else if (
!rows[op.fromRowIndex].isMarginValid &&
updatedRows[op.fromRowIndex].isMarginValid
) {
newRows[op.fromRowIndex].isMarginValid = true;
setRows(newRows);
} else {
dispatch(upsertViaFromSuggestedOP(newRowData as SuggestedOP));
const newVias = updatedRows
.filter((row, index) => !isEqual(row, rows[index]))
.map(
(row) =>
({
...row,
...(row.arrival && { arrival: durationSinceStartTime(startTime, row.arrival) }),
}) as SuggestedOP
);
dispatch(upsertSeveralViasFromSuggestedOP(newVias));
}
}}
stickyRightColumn={stickyRightColumn}
Expand All @@ -94,7 +122,7 @@ const TimesStops = ({
activeRow:
rowIndex === 0 ||
rowIndex === allWaypoints.length - 1 ||
isVia(pathSteps || [], rowData, WITH_KP),
isVia(pathSteps || [], rowData, { withKP: true }),
})
}
cellClassName={cellClassName}
Expand Down
11 changes: 9 additions & 2 deletions front/src/modules/timesStops/TimesStopsInput.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable react/jsx-no-useless-fragment */

import { useOsrdConfActions } from 'common/osrdContext';
import { isVia } from 'modules/pathfinding/utils';
import type { SuggestedOP } from 'modules/trainschedule/components/ManageTrainSchedule/types';
import type { PathStep } from 'reducers/osrdconf/types';
import { useAppDispatch } from 'store';
Expand All @@ -16,11 +17,18 @@ type ClearButtonProps = {
pathSteps: PathStep[];
};

const createClearViaButton = ({ removeVia, rowIndex, rowData, allWaypoints }: ClearButtonProps) => {
const createClearViaButton = ({
removeVia,
rowIndex,
rowData,
allWaypoints,
pathSteps,
}: ClearButtonProps) => {
const isClearBtnShown =
allWaypoints &&
rowIndex > 0 &&
rowIndex < allWaypoints.length - 1 &&
isVia(pathSteps || [], rowData, { withKP: true }) &&
(rowData.stopFor !== undefined ||
rowData.theoreticalMargin !== undefined ||
rowData.arrival !== undefined ||
Expand Down Expand Up @@ -79,7 +87,6 @@ const TimesStopsinput = ({ allWaypoints, startTime, pathSteps }: TimesStopsInput
});
dispatch(updatePathSteps({ pathSteps: updatedPathSteps }));
};

return (
<TimesStops
allWaypoints={allWaypoints}
Expand Down
4 changes: 2 additions & 2 deletions front/src/modules/timesStops/TimesStopsOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ const TimesStopsOutput = ({
tableType={TableType.Output}
cellClassName={({ rowData: rowData_ }) => {
const rowData = rowData_ as PathWaypointRow;
const arrivalScheduleNotRespected = rowData.arrival
? rowData.calculatedArrival !== rowData.arrival
const arrivalScheduleNotRespected = rowData.arrival?.time
? rowData.calculatedArrival !== rowData.arrival.time
: false;
const negativeDiffMargins = Number(rowData.diffMargins?.split(NO_BREAK_SPACE)[0]) < 0;
return cx({
Expand Down
27 changes: 14 additions & 13 deletions front/src/modules/timesStops/helpers/__tests__/scheduleData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@ import { describe, it, expect } from 'vitest';
import { computeScheduleData } from '../scheduleData';

describe('computeScheduleData', () => {
it('should compute simple arrival time in the correct timezone', () => {
const schedule = {
at: 'id325',
arrival: 'PT3600S',
stop_for: 'PT100S',
on_stop_signal: false,
locked: false,
};
const startTime = '2024-05-14T00:00:00Z';
describe('same day', () => {
it('should compute simple arrival time in the correct timezone', () => {
const schedule = {
at: 'id325',
arrival: 'PT3600S',
stop_for: 'PT100S',
on_stop_signal: false,
locked: false,
};

expect(computeScheduleData(schedule, startTime)).toEqual({
arrival: 3600,
departure: 3700,
stopFor: 100,
expect(computeScheduleData(schedule)).toEqual({
arrival: 3600,
departure: 3700,
stopFor: 100,
});
});
});
});
Loading

0 comments on commit 5a2c9e0

Please sign in to comment.