From fca60045ea629739e3fbfb766b2cf947966f2202 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 22 Oct 2025 10:02:19 +0200 Subject: [PATCH 1/2] front: fix track offset nodes in NGE When building an OSRD train path, we were unconditionally using the trigram from the NGE node's betriebspunktName field. This worked fine for OPs with a trigram, but fell apart for track offsets (and infrastructures without trigrams). Instead, reconstruct the PathItemLocation from the path_item_key stored in MacroEditorState.nodes. As a bonus, this makes it so path item location types are no longer changed to trigrams when editing a train from NGE: if a train was created with UICs from OSRD, editing it in NGE will retain UICs now. We still need to keep the old betriebspunktName-based logic for NGE JSON file imports. Signed-off-by: Simon Ser Closes: https://github.com/OpenRailAssociation/osrd/issues/12618 --- .../MacroEditor/MacroEditorState.ts | 34 +++++++++++-- .../components/MacroEditor/ngeToOsrd.ts | 51 ++++++++++++++----- 2 files changed, 68 insertions(+), 17 deletions(-) diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts index 22083c25091..70ac523de2b 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts @@ -1,7 +1,11 @@ import { sortBy } from 'lodash'; -import type { MacroNodeResponse, OperationalPoint } from 'common/api/osrdEditoastApi'; -import type { TimetableItemId, TimetableItem } from 'reducers/osrdconf/types'; +import type { + MacroNodeResponse, + OperationalPoint, + PathItemLocation, +} from 'common/api/osrdEditoastApi'; +import type { TimetableItemId } from 'reducers/osrdconf/types'; import type { TrainrunCategory, TrainrunFrequency } from '../NGE/types'; @@ -254,7 +258,7 @@ export default class MacroEditorState { /** * Given an path step, returns its pathKey */ - static getPathKey(item: TimetableItem['path'][0]): string { + static getPathKey(item: PathItemLocation): string { if ('trigram' in item) return `trigram:${item.trigram}${item.secondary_code ? `/${item.secondary_code}` : ''}`; if ('operational_point' in item) return `op_id:${item.operational_point}`; @@ -280,4 +284,28 @@ export default class MacroEditorState { } return result; } + + static parsePathKey(key: string): PathItemLocation { + const [type, value] = key.split(':'); + if (!value) throw new Error('Invalid path key'); + switch (type) { + case 'op_id': { + return { operational_point: value }; + } + case 'trigram': { + const [trigram, secondary_code] = value.split('/'); + return { trigram, secondary_code }; + } + case 'uic': { + const [uic, secondary_code] = value.split('/'); + return { uic: Number(uic), secondary_code }; + } + case 'track_offset': { + const [track, offset] = value.split('+'); + return { track, offset: Number(offset) }; + } + default: + throw new Error(`Invalid path key type "${type}"`); + } + } } diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd.ts index 4055649396c..98c2017a640 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/ngeToOsrd.ts @@ -10,6 +10,7 @@ import { type PacedTrain, type SearchResultItemOperationalPoint, type TrainSchedule, + type PathItemLocation, } from 'common/api/osrdEditoastApi'; import { createPacedTrain, @@ -44,7 +45,7 @@ import { DEFAULT_TIME_WINDOW, TRAINRUN_DIRECTIONS, } from './consts'; -import type MacroEditorState from './MacroEditorState'; +import MacroEditorState from './MacroEditorState'; import type { NodeIndexed } from './MacroEditorState'; import { createMacroNode, @@ -165,11 +166,21 @@ const getTrainrunSectionsByTrainrunId = ( return orderedSectionPaths; }; -const createPathItemFromNode = (node: NodeDto, index: number) => { - const [trigram, secondaryCode] = node.betriebspunktName.split('/'); +const createPathItemFromNode = ( + node: NodeDto, + index: number, + state?: MacroEditorState +): TrainSchedule['path'][number] => { + let pathItemLocation: PathItemLocation; + if (state) { + const indexedNode = state.getNodeByNgeId(node.id)!; + pathItemLocation = MacroEditorState.parsePathKey(indexedNode.path_item_key); + } else { + const [trigram, secondary_code] = node.betriebspunktName.split('/'); + pathItemLocation = { trigram, secondary_code }; + } return { - trigram, - secondary_code: secondaryCode, + ...pathItemLocation, id: `${node.id}-${index}`, deleted: false, // TODO : handle this case in xml import refacto @@ -196,16 +207,17 @@ const formatDateDifferenceFrom = (start: Date, stop: Date) => export const generatePath = ( trainrunSections: TrainrunSectionDto[], nodes: NodeDto[], - trainrunDirection: TRAINRUN_DIRECTIONS + trainrunDirection: TRAINRUN_DIRECTIONS, + state?: MacroEditorState ): TrainSchedule['path'] => { const isForward = trainrunDirection === TRAINRUN_DIRECTIONS.FORWARD; const path = trainrunSections.map((section, index) => { const fromNode = getNodeById(nodes, isForward ? section.sourceNodeId : section.targetNodeId); const toNode = getNodeById(nodes, isForward ? section.targetNodeId : section.sourceNodeId); if (!fromNode || !toNode) return []; - const originPathItem = createPathItemFromNode(fromNode, index); + const originPathItem = createPathItemFromNode(fromNode, index, state); if (index === trainrunSections.length - 1) { - const destinationPathItem = createPathItemFromNode(toNode, index + 1); + const destinationPathItem = createPathItemFromNode(toNode, index + 1, state); return [originPathItem, destinationPathItem]; } return [originPathItem]; @@ -341,7 +353,8 @@ const generatePathAndSchedule = ( trainrunSections: TrainrunSectionDto[], nodes: NodeDto[], baseDate?: Date, - trainrunDirection: TRAINRUN_DIRECTIONS = TRAINRUN_DIRECTIONS.FORWARD + trainrunDirection: TRAINRUN_DIRECTIONS = TRAINRUN_DIRECTIONS.FORWARD, + state?: MacroEditorState ) => { let sections = trainrunSections; if (trainrunDirection === TRAINRUN_DIRECTIONS.BACKWARD) { @@ -349,7 +362,7 @@ const generatePathAndSchedule = ( } const startDate = calculateStartDate(sections, baseDate ?? new Date(), trainrunDirection); - const path = generatePath(sections, nodes, trainrunDirection); + const path = generatePath(sections, nodes, trainrunDirection, state); const schedule = generateSchedule(sections, nodes, startDate, trainrunDirection); return { start_time: startDate.toISOString(), path, schedule }; }; @@ -418,13 +431,20 @@ const handleCreateTimetableItem = async ( 'ngeToOsrd handleCreateTimetableItem received a one_way train dto instead of a round trip' ); } - const pathAndSchedule = generatePathAndSchedule(trainrunSections, netzgrafikDto.nodes); + const pathAndSchedule = generatePathAndSchedule( + trainrunSections, + netzgrafikDto.nodes, + undefined, + TRAINRUN_DIRECTIONS.FORWARD, + state + ); const returnPathAndSchedule = generatePathAndSchedule( trainrunSections, netzgrafikDto.nodes, undefined, - TRAINRUN_DIRECTIONS.BACKWARD + TRAINRUN_DIRECTIONS.BACKWARD, + state ); await populateSecondaryCodesInPath( @@ -590,7 +610,9 @@ const handleUpdateTimetableItem = async ({ const forwardPathAndSchedule = generatePathAndSchedule( trainrunSections, netzgrafikDto.nodes, - new Date(oldForwardTimetableItem.start_time) + new Date(oldForwardTimetableItem.start_time), + TRAINRUN_DIRECTIONS.FORWARD, + state ); await populateSecondaryCodesInPath(forwardPathAndSchedule.path, infraId, dispatch); @@ -672,7 +694,8 @@ const handleUpdateTimetableItem = async ({ trainrunSections, netzgrafikDto.nodes, new Date(oldForwardTimetableItem.start_time), - TRAINRUN_DIRECTIONS.BACKWARD + TRAINRUN_DIRECTIONS.BACKWARD, + state ); await populateSecondaryCodesInPath(returnPathAndSchedule.path, infraId, dispatch); From 4d14e34019cf485c3b6978293fb697f9369dad64 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Tue, 28 Oct 2025 14:58:13 +0100 Subject: [PATCH 2/2] front: handle missing UIC/trigram in MacroEditorState.getPathKeys() These are optional fields so they can be undefined. Avoid generating bogus path item keys such as "uic:undefined" in that case. Signed-off-by: Simon Ser --- .../views/Scenario/components/MacroEditor/MacroEditorState.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts index 70ac523de2b..1792a2c16a3 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts +++ b/front/src/applications/operationalStudies/views/Scenario/components/MacroEditor/MacroEditorState.ts @@ -277,8 +277,8 @@ export default class MacroEditorState { const result = []; result.push(`op_id:${op.id}`); - result.push(`trigram:${trigram}${ch ? `/${ch}` : ''}`); - result.push(`uic:${uic}${ch ? `/${ch}` : ''}`); + if (trigram) result.push(`trigram:${trigram}${ch ? `/${ch}` : ''}`); + if (uic) result.push(`uic:${uic}${ch ? `/${ch}` : ''}`); for (const opPart of op.parts) { result.push(`track_offset:${opPart.track}+${opPart.position}`); }