diff --git a/front/src/applications/operationalStudies/views/Scenario/components/SimulationResults/SimulationResults.tsx b/front/src/applications/operationalStudies/views/Scenario/components/SimulationResults/SimulationResults.tsx index b214dd9233a..1d5176aea5f 100644 --- a/front/src/applications/operationalStudies/views/Scenario/components/SimulationResults/SimulationResults.tsx +++ b/front/src/applications/operationalStudies/views/Scenario/components/SimulationResults/SimulationResults.tsx @@ -117,6 +117,7 @@ const SimulationResults = ({ timetableId, pathfinding: projectionData?.pathfinding, projectedOperationalPoints: projectionData?.operationalPoints, + operationalPointReferences: projectionData?.operationalPointReferences, }); const { diff --git a/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useGetProjectedTrainOperationalPoints.ts b/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useGetProjectedTrainOperationalPoints.ts index 6c7131686fe..7997244928c 100644 --- a/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useGetProjectedTrainOperationalPoints.ts +++ b/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useGetProjectedTrainOperationalPoints.ts @@ -7,6 +7,7 @@ import { useSelector } from 'react-redux'; import { upsertMapWaypointsInOperationalPoints } from 'applications/operationalStudies/helpers/upsertMapWaypointsInOperationalPoints'; import type { CorePathfindingResultSuccess, + OperationalPointReference, TrainScheduleResponse, } from 'common/api/osrdEditoastApi'; import { isStation } from 'modules/pathfinding/utils'; @@ -14,18 +15,32 @@ import type { PathOperationalPoint, ProjectionData } from 'modules/simulationRes import { getProjectionType } from 'reducers/simulationResults/selectors'; import { getWaypointsLocalStorageKey } from './helpers/utils'; + +function buildOpRefByPosition( + path: TrainScheduleResponse['path'], + pathfinding: CorePathfindingResultSuccess +): Map { + const mapOpRef = new Map(); + path.forEach((step, i) => { + if (step.location.type === 'operational_point_part_reference') { + mapOpRef.set(pathfinding.path_item_positions[i], step.location.operational_point); + } + }); + return mapOpRef; +} + const useGetProjectedTrainOperationalPoints = ({ - infraId, timetableId, path, pathfinding, projectedOperationalPoints, + operationalPointReferences, }: { - infraId: number; timetableId: number | undefined; path?: TrainScheduleResponse['path']; pathfinding?: CorePathfindingResultSuccess; projectedOperationalPoints?: ProjectionData['operationalPoints']; + operationalPointReferences?: OperationalPointReference[]; }) => { const { t } = useTranslation('operational-studies'); const projectionType = useSelector(getProjectionType); @@ -35,54 +50,70 @@ const useGetProjectedTrainOperationalPoints = ({ useState(operationalPoints); useEffect(() => { - const getOperationalPoints = async () => { - let operationalPointsWithUniqueIds: PathOperationalPoint[] = - projectedOperationalPoints?.map((op, i) => ({ - ...omit(op, 'id'), - waypointId: `${op.id}-${op.position}-${i}`, - opId: op.id, - })) || []; - - operationalPointsWithUniqueIds = - projectionType === 'trackProjection' && path && pathfinding - ? upsertMapWaypointsInOperationalPoints( - 'PathOperationalPoint', - path, - pathfinding.path_item_positions, - operationalPointsWithUniqueIds, - t - ) - : operationalPointsWithUniqueIds; - - setOperationalPoints(operationalPointsWithUniqueIds); - - const stringifiedSavedWaypoints = localStorage.getItem( - getWaypointsLocalStorageKey(timetableId, path) - ); - - if (stringifiedSavedWaypoints) { - operationalPointsWithUniqueIds = JSON.parse( - stringifiedSavedWaypoints - ) as PathOperationalPoint[]; - } else { - // If the manchette hasn't been saved, we want to display by default only - // the waypoints with CH BV/00/'' and the path steps (origin, destination, vias) - - const lastIndex = operationalPointsWithUniqueIds.length - 1; - operationalPointsWithUniqueIds = operationalPointsWithUniqueIds.filter((op, i) => { - if (i === 0 || i === lastIndex) return true; - // handle waypoints added from the map - if (!op.extensions?.sncf) return true; - // handle waypoints added from the pathfinding or operational points on path - return isStation(op.extensions.sncf.ch) || op.weight === 100; - }); - } - - setFilteredOperationalPoints(operationalPointsWithUniqueIds); - }; - - getOperationalPoints(); - }, [path, pathfinding, infraId, projectedOperationalPoints, timetableId, t, projectionType]); + // Successful pathfinding: position → original OP reference for path steps. + // Failed pathfinding: pathfinding is undefined, map stays null (index-based fallback below). + const opRefByPosition = path && pathfinding ? buildOpRefByPosition(path, pathfinding) : null; + + const baseOps: PathOperationalPoint[] = + projectedOperationalPoints?.map((op, i) => ({ + ...omit(op, 'id'), + waypointId: `${op.id}-${op.position}-${i}`, + opId: op.id, + // Successful pathfinding: match by position against path steps. + // Failed pathfinding: operationalPointReferences[i] is the original opRef for projectedOperationalPoints[i]. + opRef: opRefByPosition ? opRefByPosition.get(op.position) : operationalPointReferences?.[i], + })) || []; + + const operationalPointsWithUniqueIds = + projectionType === 'trackProjection' && path && pathfinding + ? upsertMapWaypointsInOperationalPoints( + 'PathOperationalPoint', + path, + pathfinding.path_item_positions, + baseOps, + t + ) + : baseOps; + + setOperationalPoints(operationalPointsWithUniqueIds); + + const opRefByWaypointId = new Map( + operationalPointsWithUniqueIds.map((op) => [op.waypointId, op.opRef]) + ); + + const stringifiedSavedWaypoints = localStorage.getItem( + getWaypointsLocalStorageKey(timetableId, path) + ); + + let filteredOps: PathOperationalPoint[]; + if (stringifiedSavedWaypoints) { + filteredOps = (JSON.parse(stringifiedSavedWaypoints) as PathOperationalPoint[]).map((op) => ({ + ...op, + opRef: opRefByWaypointId.get(op.waypointId), + })); + } else { + // If the manchette hasn't been saved, we want to display by default only + // the waypoints with CH BV/00/'' and the path steps (origin, destination, vias) + const lastIndex = operationalPointsWithUniqueIds.length - 1; + filteredOps = operationalPointsWithUniqueIds.filter((op, i) => { + if (i === 0 || i === lastIndex) return true; + // handle waypoints added from the map + if (!op.extensions?.sncf) return true; + // handle waypoints added from the pathfinding or operational points on path + return isStation(op.extensions.sncf.ch) || op.weight === 100; + }); + } + + setFilteredOperationalPoints(filteredOps); + }, [ + path, + pathfinding, + projectedOperationalPoints, + operationalPointReferences, + timetableId, + t, + projectionType, + ]); return { operationalPoints, filteredOperationalPoints, setFilteredOperationalPoints }; }; diff --git a/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useTrackOccupancy.ts b/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useTrackOccupancy.ts index aff379b47ad..89ce37335c2 100644 --- a/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useTrackOccupancy.ts +++ b/front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useTrackOccupancy.ts @@ -58,6 +58,7 @@ function getOperationalPointReference( op: PathOperationalPoint | undefined ): OperationalPointReference | undefined { if (!op) return undefined; + if (op.opRef) return op.opRef; // Only use the opId when it refers to a real infra OP. Virtual OPs (unrecognised, created // by usePathProjection when pathfinding fails) have a synthetic id like "virtual_op_Zürich" // and an empty part.track — they must be matched by trigram/uic instead. diff --git a/front/src/modules/simulationResult/types.ts b/front/src/modules/simulationResult/types.ts index 35d9207880f..a6e94a26fe6 100644 --- a/front/src/modules/simulationResult/types.ts +++ b/front/src/modules/simulationResult/types.ts @@ -9,6 +9,7 @@ import type { import type { PacedTrainException, CoreSignalUpdate, + OperationalPointReference, PathProperties, RollingStockWithLiveries, SimulationResponseSuccess, @@ -30,6 +31,7 @@ export type EditoastPathOperationalPoint = NonNullable< export type PathOperationalPoint = Omit & { waypointId: string; opId: string | null; + opRef?: OperationalPointReference; }; // Space Time Chart