Skip to content

Commit bd8ce27

Browse files
committed
front: handle roundtrips in OSRD → NGE converter
Fetch round-trips from the backend, group timetable items, set Trainrun.trainrunDirection accordingly, and populate sourceArrival/targetDeparture. This commit is more easily reviewed by turning off whitespace changes in the diff view. Signed-off-by: Simon Ser <[email protected]>
1 parent de003d2 commit bd8ce27

File tree

1 file changed

+177
-102
lines changed
  • front/src/applications/operationalStudies/components/MacroEditor

1 file changed

+177
-102
lines changed

front/src/applications/operationalStudies/components/MacroEditor/osrdToNge.ts

Lines changed: 177 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { TFunction } from 'i18next';
22
import { uniqBy } from 'lodash';
33

4+
import type { TimetableItemRoundTripGroups } from 'applications/operationalStudies/types';
45
import {
56
getUniqueOpRefsFromTimetableItems,
67
addPathOpsToTimetableItems,
8+
groupRoundTrips,
9+
checkRoundTripCompatible,
710
} from 'applications/operationalStudies/utils';
811
import { osrdEditoastApi, type TrainSchedule } from 'common/api/osrdEditoastApi';
912
import type { TimetableItem, TimetableItemWithPathOps } from 'reducers/osrdconf/types';
@@ -330,13 +333,14 @@ export const loadAndIndexNge = async (
330333
*/
331334
const getNgeTrainruns = (
332335
state: MacroEditorState,
333-
timetableItems: TimetableItem[],
336+
groupedTimetableItems: (readonly [TimetableItem, TimetableItem | null])[],
334337
labels: LabelDto[]
335338
): TrainrunDto[] =>
336-
timetableItems
339+
groupedTimetableItems
340+
.map(([a, b]) => ({ ...a, returnId: b?.id ?? null }))
337341
.filter((timetableItem) => timetableItem.path.length >= 2)
338342
.map((timetableItem, index) => {
339-
state.timetableItemIdByNgeId.set(index + 1, [timetableItem.id, null]);
343+
state.timetableItemIdByNgeId.set(index + 1, [timetableItem.id, timetableItem.returnId]);
340344
const trainrunFrequency = getTrainrunFrequencyFromTimetableItem(timetableItem, state);
341345
return {
342346
id: index + 1,
@@ -347,7 +351,7 @@ const getNgeTrainruns = (
347351
labelIds: (timetableItem.labels || []).map((l) =>
348352
labels.findIndex((e) => e.label === l && e.labelGroupId === TRAINRUN_LABEL_GROUP.id)
349353
),
350-
trainrunDirection: 'one_way',
354+
trainrunDirection: timetableItem.returnId ? 'round_trip' : 'one_way',
351355
};
352356
});
353357

@@ -386,7 +390,7 @@ const createDepartureTimeLock = (scheduleItem: ScheduleItem | undefined, startTi
386390
*/
387391
const getNgeTrainrunSectionsWithNodes = (
388392
state: MacroEditorState,
389-
timetableItems: TimetableItem[],
393+
groupedTimetableItems: (readonly [TimetableItem, TimetableItem | null])[],
390394
labels: LabelDto[]
391395
) => {
392396
let portId = 1;
@@ -416,104 +420,133 @@ const getNgeTrainrunSectionsWithNodes = (
416420
// Track nge nodes
417421
const ngeNodesByPathKey: Record<string, NetzgrafikDto['nodes'][0]> = {};
418422
let trainrunSectionId = 0;
419-
const trainrunSections: TrainrunSectionDto[] = timetableItems.flatMap((timetableItem, index) => {
420-
// Figure out the primary node key for each path item
421-
const pathNodeKeys = timetableItem.path.map((pathItem) => {
422-
const node = state.getNodeByKey(MacroEditorState.getPathKey(pathItem));
423-
return node!.path_item_key;
424-
});
423+
const trainrunSections: TrainrunSectionDto[] = groupedTimetableItems.flatMap(
424+
([timetableItem, returnTimetableItem], index) => {
425+
// Figure out the primary node key for each path item
426+
const pathNodeKeys = timetableItem.path.map((pathItem) => {
427+
const node = state.getNodeByKey(MacroEditorState.getPathKey(pathItem));
428+
return node!.path_item_key;
429+
});
425430

426-
const startTime = new Date(timetableItem.start_time);
427-
428-
// OSRD describes the path in terms of nodes, NGE describes it in terms
429-
// of sections between nodes. Iterate over path items two-by-two to
430-
// convert them.
431-
let prevPort: PortDto | null = null;
432-
return pathNodeKeys.slice(0, -1).map((sourceNodeKey, i) => {
433-
// Get the source node or created it
434-
if (!ngeNodesByPathKey[sourceNodeKey]) {
435-
ngeNodesByPathKey[sourceNodeKey] = castNodeToNge(
436-
state,
437-
state.getNodeByKey(sourceNodeKey)!,
438-
labels
431+
const startTime = new Date(timetableItem.start_time);
432+
const returnStartTime = returnTimetableItem ? new Date(returnTimetableItem.start_time) : null;
433+
434+
// OSRD describes the path in terms of nodes, NGE describes it in terms
435+
// of sections between nodes. Iterate over path items two-by-two to
436+
// convert them.
437+
let prevPort: PortDto | null = null;
438+
return pathNodeKeys.slice(0, -1).map((sourceNodeKey, i) => {
439+
// returnTimetableItem contains the same path as timetableItem but in
440+
// reverse order. `timetableItem.path.length - 1` is the index of the
441+
// last path item, subtracting `i` will iterate from the end of the
442+
// list to the start.
443+
const returnIndex = timetableItem.path.length - 1 - i;
444+
445+
// Get the source node or created it
446+
if (!ngeNodesByPathKey[sourceNodeKey]) {
447+
ngeNodesByPathKey[sourceNodeKey] = castNodeToNge(
448+
state,
449+
state.getNodeByKey(sourceNodeKey)!,
450+
labels
451+
);
452+
}
453+
const sourceNode = ngeNodesByPathKey[sourceNodeKey];
454+
455+
// Get the target node or created it
456+
const targetNodeKey = pathNodeKeys[i + 1];
457+
if (!ngeNodesByPathKey[targetNodeKey]) {
458+
ngeNodesByPathKey[targetNodeKey] = castNodeToNge(
459+
state,
460+
state.getNodeByKey(targetNodeKey)!,
461+
labels
462+
);
463+
}
464+
const targetNode = ngeNodesByPathKey[targetNodeKey];
465+
466+
// Adding port
467+
const sourcePort = createPort(trainrunSectionId);
468+
sourceNode.ports.push(sourcePort);
469+
const targetPort = createPort(trainrunSectionId);
470+
targetNode.ports.push(targetPort);
471+
472+
// Adding schedule
473+
const sourceScheduleEntry = timetableItem.schedule!.find(
474+
(entry) => entry.at === timetableItem.path[i].id
439475
);
440-
}
441-
const sourceNode = ngeNodesByPathKey[sourceNodeKey];
442-
443-
// Get the target node or created it
444-
const targetNodeKey = pathNodeKeys[i + 1];
445-
if (!ngeNodesByPathKey[targetNodeKey]) {
446-
ngeNodesByPathKey[targetNodeKey] = castNodeToNge(
447-
state,
448-
state.getNodeByKey(targetNodeKey)!,
449-
labels
476+
const targetScheduleEntry = timetableItem.schedule!.find(
477+
(entry) => entry.at === timetableItem.path[i + 1].id
478+
);
479+
const returnSourceScheduleEntry = returnTimetableItem?.schedule?.find(
480+
(entry) => entry.at === returnTimetableItem.path[returnIndex].id
481+
);
482+
const returnTargetScheduleEntry = returnTimetableItem?.schedule?.find(
483+
(entry) => entry.at === returnTimetableItem.path[returnIndex - 1].id
450484
);
451-
}
452-
const targetNode = ngeNodesByPathKey[targetNodeKey];
453-
454-
// Adding port
455-
const sourcePort = createPort(trainrunSectionId);
456-
sourceNode.ports.push(sourcePort);
457-
const targetPort = createPort(trainrunSectionId);
458-
targetNode.ports.push(targetPort);
459-
460-
// Adding schedule
461-
const sourceScheduleEntry = timetableItem.schedule!.find(
462-
(entry) => entry.at === timetableItem.path[i].id
463-
);
464-
const targetScheduleEntry = timetableItem.schedule!.find(
465-
(entry) => entry.at === timetableItem.path[i + 1].id
466-
);
467-
468-
// Create a transition between the previous section and the one we're creating
469-
if (prevPort) {
470-
const transition = createTransition(prevPort.id, sourcePort.id);
471-
transition.isNonStopTransit = !sourceScheduleEntry?.stop_for;
472-
sourceNode.transitions.push(transition);
473-
}
474-
prevPort = targetPort;
475-
476-
let sourceDeparture;
477-
if (i === 0) {
478-
sourceDeparture = createTimeLock(startTime, startTime);
479-
} else {
480-
sourceDeparture = createDepartureTimeLock(sourceScheduleEntry, startTime);
481-
}
482-
483-
const targetArrival = createArrivalTimeLock(targetScheduleEntry, startTime);
484-
485-
const travelTime = { ...DEFAULT_TIME_LOCK };
486-
if (targetArrival.consecutiveTime !== null && sourceDeparture.consecutiveTime !== null) {
487-
travelTime.time = targetArrival.consecutiveTime - sourceDeparture.consecutiveTime;
488-
travelTime.consecutiveTime = travelTime.time;
489-
}
490485

491-
const trainrunSection = {
492-
id: trainrunSectionId,
493-
sourceNodeId: sourceNode.id,
494-
sourcePortId: sourcePort.id,
495-
targetNodeId: targetNode.id,
496-
targetPortId: targetPort.id,
497-
travelTime,
498-
sourceDeparture,
499-
sourceArrival: { ...DEFAULT_TIME_LOCK },
500-
targetDeparture: { ...DEFAULT_TIME_LOCK },
501-
targetArrival,
502-
numberOfStops: 0,
503-
trainrunId: index + 1,
504-
resourceId: state.ngeResource.id,
505-
path: {
506-
path: [],
507-
textPositions: [],
508-
},
509-
specificTrainrunSectionFrequencyId: 0,
510-
warnings: [],
511-
};
486+
// Create a transition between the previous section and the one we're creating
487+
if (prevPort) {
488+
const transition = createTransition(prevPort.id, sourcePort.id);
489+
transition.isNonStopTransit = !sourceScheduleEntry?.stop_for;
490+
sourceNode.transitions.push(transition);
491+
}
492+
prevPort = targetPort;
493+
494+
let sourceDeparture;
495+
if (i === 0) {
496+
sourceDeparture = createTimeLock(startTime, startTime);
497+
} else {
498+
sourceDeparture = createDepartureTimeLock(sourceScheduleEntry, startTime);
499+
}
500+
501+
const targetArrival = createArrivalTimeLock(targetScheduleEntry, startTime);
502+
503+
let targetDeparture = { ...DEFAULT_TIME_LOCK };
504+
if (returnStartTime) {
505+
if (returnIndex === 1) {
506+
targetDeparture = createTimeLock(returnStartTime, returnStartTime);
507+
} else {
508+
targetDeparture = createDepartureTimeLock(returnTargetScheduleEntry, returnStartTime);
509+
}
510+
}
511+
512+
let sourceArrival = { ...DEFAULT_TIME_LOCK };
513+
if (returnStartTime) {
514+
sourceArrival = createArrivalTimeLock(returnSourceScheduleEntry, returnStartTime);
515+
}
516+
517+
const travelTime = { ...DEFAULT_TIME_LOCK };
518+
if (targetArrival.consecutiveTime !== null && sourceDeparture.consecutiveTime !== null) {
519+
travelTime.time = targetArrival.consecutiveTime - sourceDeparture.consecutiveTime;
520+
travelTime.consecutiveTime = travelTime.time;
521+
}
522+
523+
const trainrunSection = {
524+
id: trainrunSectionId,
525+
sourceNodeId: sourceNode.id,
526+
sourcePortId: sourcePort.id,
527+
targetNodeId: targetNode.id,
528+
targetPortId: targetPort.id,
529+
travelTime,
530+
sourceDeparture,
531+
sourceArrival,
532+
targetDeparture,
533+
targetArrival,
534+
numberOfStops: 0,
535+
trainrunId: index + 1,
536+
resourceId: state.ngeResource.id,
537+
path: {
538+
path: [],
539+
textPositions: [],
540+
},
541+
specificTrainrunSectionFrequencyId: 0,
542+
warnings: [],
543+
};
512544

513-
trainrunSectionId += 1;
514-
return trainrunSection;
515-
});
516-
});
545+
trainrunSectionId += 1;
546+
return trainrunSection;
547+
});
548+
}
549+
);
517550

518551
return {
519552
trainrunSections,
@@ -540,12 +573,12 @@ const getNgeLabels = (state: MacroEditorState): LabelDto[] =>
540573
*/
541574
export const getNgeDto = (
542575
state: MacroEditorState,
543-
timetableItems: TimetableItem[]
576+
groupedTimetableItems: (readonly [TimetableItem, TimetableItem | null])[]
544577
): NetzgrafikDto => {
545578
const labels = getNgeLabels(state);
546579
return {
547-
...getNgeTrainrunSectionsWithNodes(state, timetableItems, labels),
548-
trainruns: getNgeTrainruns(state, timetableItems, labels),
580+
...getNgeTrainrunSectionsWithNodes(state, groupedTimetableItems, labels),
581+
trainruns: getNgeTrainruns(state, groupedTimetableItems, labels),
549582
resources: [state.ngeResource],
550583
metadata: {
551584
netzgrafikColors: getNetzgrafikColors(),
@@ -580,6 +613,24 @@ const fetchTimetableItemPathOps = async (
580613
return addPathOpsToTimetableItems(timetableItems, opRefs, ops);
581614
};
582615

616+
const groupCompatibleRoundTrips = (
617+
roundTripGroups: TimetableItemRoundTripGroups
618+
): (readonly [TimetableItemWithPathOps, TimetableItemWithPathOps | null])[] => {
619+
const incompatible = [];
620+
const compatible = [];
621+
for (const [a, b] of roundTripGroups.roundTrips) {
622+
if (checkRoundTripCompatible(a, b)) {
623+
compatible.push([a, b] as const);
624+
} else {
625+
incompatible.push(a, b);
626+
}
627+
}
628+
const oneWays = [...roundTripGroups.oneWays, ...roundTripGroups.others, ...incompatible].map(
629+
(timetableItem) => [timetableItem, null] as const
630+
);
631+
return [...oneWays, ...compatible];
632+
};
633+
583634
export const loadNgeDto = async (
584635
state: MacroEditorState,
585636
timetableId: number,
@@ -618,6 +669,30 @@ export const loadNgeDto = async (
618669
dispatch
619670
);
620671

672+
const timetableItemsById = new Map(
673+
timetableItems.map((timetableItem) => [timetableItem.id, timetableItem])
674+
);
675+
676+
const trainScheduleRoundTripsPromise = dispatch(
677+
osrdEditoastApi.endpoints.getTimetableByIdRoundTripsTrainSchedules.initiate(
678+
{ id: timetableId },
679+
{ subscribe: false }
680+
)
681+
);
682+
const pacedTrainRoundTripsPromise = dispatch(
683+
osrdEditoastApi.endpoints.getTimetableByIdRoundTripsPacedTrains.initiate(
684+
{ id: timetableId },
685+
{ subscribe: false }
686+
)
687+
);
688+
const { results: trainScheduleRoundTrips } = await trainScheduleRoundTripsPromise.unwrap();
689+
const { results: pacedTrainRoundTrips } = await pacedTrainRoundTripsPromise.unwrap();
690+
const roundTripGroups = groupRoundTrips(timetableItemsById, {
691+
trainSchedules: trainScheduleRoundTrips,
692+
pacedTrains: pacedTrainRoundTrips,
693+
});
694+
const groupedTimetableItems = groupCompatibleRoundTrips(roundTripGroups);
695+
621696
await loadAndIndexNge(state, timetableItems, dispatch, t);
622-
return getNgeDto(state, timetableItems);
697+
return getNgeDto(state, groupedTimetableItems);
623698
};

0 commit comments

Comments
 (0)