Skip to content

Commit 7e3db86

Browse files
committed
front: display all tracks for all operational points
Signed-off-by: Theo Macron <theo.macron0315@gmail.com>
1 parent 60e2a77 commit 7e3db86

File tree

4 files changed

+105
-68
lines changed

4 files changed

+105
-68
lines changed

front/src/applications/operationalStudies/views/Scenario/components/SimulationResults/SimulationResults.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ const SimulationResults = ({
114114
updateTrackOccupanciesOnDrag: handleTrainDragInTrackOccupancy,
115115
} = useTrackOccupancy({
116116
infraId,
117-
pathfindingHasFailed: projectionData?.pathfindingStatus === 'failed',
118117
pathOperationalPoints: filteredOperationalPoints,
119118
timetableItemProjections,
120119
});

front/src/modules/simulationResult/components/SpaceTimeChartWrapper/SpaceTimeChartWrapper.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,7 @@ const SpaceTimeChartWrapper = ({
224224

225225
const { waypointMenu, activeWaypointId, handleWaypointClick } = useWaypointMenu(
226226
activeWaypointRef,
227-
waypointsPanelData,
228-
allTrainsProjected,
229-
pathfindingHasFailed
227+
waypointsPanelData
230228
);
231229

232230
const hoveredTrainIdForChart = useMemo(() => {

front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useTrackOccupancy.ts

Lines changed: 103 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ type DeployedWaypoint = {
4343
loading?: boolean;
4444
};
4545

46+
const NO_TRACK_SPECIFIED_ID = '__no_track_specified__';
47+
const NO_TRACK_SPECIFIED_SYMBOL = '[ ]';
48+
4649
type StationLabel = { type?: 'label'; label: string } | { type: 'requestedPoint' };
4750
function extractStationLabel(
4851
stationLabel: StationLabel | undefined,
@@ -58,11 +61,18 @@ function getOperationalPointReference(
5861
op: PathOperationalPoint | undefined
5962
): OperationalPointReference | undefined {
6063
if (!op) return undefined;
61-
if (op.opId) return { type: 'id', operational_point: op.opId };
64+
// Only use the opId when it refers to a real infra OP. Virtual OPs (unrecognised, created
65+
// by usePathProjection when pathfinding fails) have a synthetic id like "virtual_op_Zürich"
66+
// and an empty part.track — they must be matched by trigram/uic instead.
67+
if (op.opId && op.part?.track) return { type: 'id', operational_point: op.opId };
68+
// Normalize empty string ch to null — virtual OPs store ch as '' when the original
69+
// secondary_code was null (see usePathProjection createVirtualOp), and passing ''
70+
// to the backend would filter for OPs with an empty secondary_code rather than any.
71+
const ch = op.extensions?.sncf?.ch || null;
6272
const trigram = op.extensions?.sncf?.trigram;
63-
if (trigram) return { type: 'trigram', trigram, secondary_code: op.extensions?.sncf?.ch };
73+
if (trigram) return { type: 'trigram', trigram, secondary_code: ch };
6474
const uic = op.extensions?.identifier?.uic;
65-
if (uic != null) return { type: 'uic', uic, secondary_code: op.extensions?.sncf?.ch };
75+
if (uic != null) return { type: 'uic', uic, secondary_code: ch };
6676
return undefined;
6777
}
6878

@@ -89,12 +99,10 @@ const useTrackOccupancy = ({
8999
infraId,
90100
timetableItemProjections,
91101
pathOperationalPoints,
92-
pathfindingHasFailed = false,
93102
}: {
94103
infraId: number;
95104
timetableItemProjections: TrainSpaceTimeData[];
96105
pathOperationalPoints: PathOperationalPoint[];
97-
pathfindingHasFailed?: boolean;
98106
}): {
99107
deployedWaypoints: DeployedWaypoint[];
100108
toggleWaypoint: (waypointId: string, selectedState?: boolean) => void;
@@ -171,7 +179,7 @@ const useTrackOccupancy = ({
171179
const fetchTrackOccupancy = useCallback(
172180
async (
173181
opRef: OperationalPointReference | undefined | null,
174-
opId: string | undefined | null,
182+
waypointId: string | undefined | null,
175183
trainsCollection: Record<TimetableItemId, TrainSpaceTimeData>
176184
): Promise<MovableOccupancyZone[]> => {
177185
if (!opRef) return [];
@@ -200,11 +208,16 @@ const useTrackOccupancy = ({
200208
if (pacedResp?.data) {
201209
for (const trackItem of pacedResp.data) {
202210
const { local_track_name: localTrackName, trains } = trackItem;
203-
if (!localTrackName) continue;
204-
const trackId = opId
205-
? localTrackNameToTrackIdRef.current.get(opId)?.get(localTrackName)
206-
: undefined;
207-
if (!trackId) continue;
211+
let trackId: string;
212+
if (!localTrackName) {
213+
trackId = NO_TRACK_SPECIFIED_ID;
214+
} else {
215+
const mappedTrackId = waypointId
216+
? localTrackNameToTrackIdRef.current.get(waypointId)?.get(localTrackName)
217+
: undefined;
218+
// If the track name isn't found in infra, use the name itself as a virtual track ID
219+
trackId = mappedTrackId ?? localTrackName;
220+
}
208221
for (const occupation of trains) {
209222
const pacedId = formatEditoastIdToPacedTrainId(occupation.train_schedule_id);
210223
const train = trainsCollection[pacedId];
@@ -288,32 +301,58 @@ const useTrackOccupancy = ({
288301
const deployedWaypoints = useMemo(() => {
289302
const res: DeployedWaypoint[] = [];
290303

291-
if (tracksState.type === 'ok')
292-
forEach(pathOperationalPointsState, (opState, waypointId) => {
293-
const op = pathOpsByWaypointId.get(waypointId);
294-
if (opState.selected && op?.opId) {
295-
const tracks = tracksState.data[op.opId];
296-
res.push({
297-
waypointId,
298-
operationalPointId: op.opId,
299-
operationalPointPosition: op.position,
300-
operationalPointName: op.extensions?.identifier?.name || undefined,
301-
zones: opState.zones.data?.map((zone) => {
302-
const trainStationLabels = trainsStationLabelsRef.current[zone.trainId];
303-
return {
304-
...zone,
305-
originStation: extractStationLabel(trainStationLabels?.origin, t),
306-
destinationStation: extractStationLabel(trainStationLabels?.destination, t),
307-
};
308-
}),
309-
loading: opState.zones.type === 'loading',
310-
tracks,
311-
});
304+
forEach(pathOperationalPointsState, (opState, waypointId) => {
305+
const op = pathOpsByWaypointId.get(waypointId);
306+
if (opState.selected && op) {
307+
const infraTracks = (tracksState.data ?? {})[waypointId] || [];
308+
const infraTrackIds = new Set(infraTracks.map((track) => track.id));
309+
const trackMapping = localTrackNameToTrackIdRef.current.get(waypointId);
310+
311+
// Remap zones whose trackId is a local_track_name stored before the infra mapping was
312+
// ready (race condition between fetchTrackOccupancy and loadAllTracks). Once the infra
313+
// mapping is available via localTrackNameToTrackIdRef, resolve them to the real track
314+
// section ID so zones land on the correct infra track row.
315+
const resolvedZones = opState.zones.data?.map((zone) => {
316+
if (!infraTrackIds.has(zone.trackId) && trackMapping) {
317+
const remappedId = trackMapping.get(zone.trackId);
318+
if (remappedId) return { ...zone, trackId: remappedId };
319+
}
320+
return zone;
321+
});
322+
323+
// Collect virtual tracks: zones whose trackId still isn't found in the infrastructure
324+
const virtualTrackIds = new Set<string>();
325+
if (resolvedZones) {
326+
for (const zone of resolvedZones) {
327+
if (!infraTrackIds.has(zone.trackId)) virtualTrackIds.add(zone.trackId);
328+
}
312329
}
313-
});
330+
const virtualTracks: Track[] = [...virtualTrackIds].map((id) => ({
331+
id,
332+
name: id === NO_TRACK_SPECIFIED_ID ? NO_TRACK_SPECIFIED_SYMBOL : id,
333+
}));
334+
335+
res.push({
336+
waypointId,
337+
operationalPointId: op?.opId ?? waypointId,
338+
operationalPointPosition: op.position,
339+
operationalPointName: op.extensions?.identifier?.name || undefined,
340+
zones: resolvedZones?.map((zone) => {
341+
const trainStationLabels = trainsStationLabelsRef.current[zone.trainId];
342+
return {
343+
...zone,
344+
originStation: extractStationLabel(trainStationLabels?.origin, t),
345+
destinationStation: extractStationLabel(trainStationLabels?.destination, t),
346+
};
347+
}),
348+
loading: opState.zones.type === 'loading',
349+
tracks: [...infraTracks, ...virtualTracks],
350+
});
351+
}
352+
});
314353

315354
return res;
316-
}, [pathOperationalPointsState, pathOpsByWaypointId, t]);
355+
}, [pathOperationalPointsState, pathOpsByWaypointId, tracksState, t]);
317356

318357
const toggleWaypoint = useCallback(
319358
(waypointId: string, selectedState?: boolean) => {
@@ -333,7 +372,7 @@ const useTrackOccupancy = ({
333372
(ids) =>
334373
fetchTrackOccupancy(
335374
getOperationalPointReference(waypoint),
336-
waypoint.opId,
375+
waypointId,
337376
Object.fromEntries(ids.map((id) => [id, timetableItemProjectionsById.get(id)!]))
338377
),
339378
{
@@ -385,7 +424,7 @@ const useTrackOccupancy = ({
385424

386425
const trains = Object.fromEntries(Array.from(timetableItemProjectionsById.entries()));
387426

388-
fetchTrackOccupancy(opRef, waypoint.opId, trains).then((newZones) => {
427+
fetchTrackOccupancy(opRef, waypointId, trains).then((newZones) => {
389428
if (!newZones.length) return;
390429

391430
updatePathOperationalPointState(waypointId, (state) =>
@@ -450,14 +489,13 @@ const useTrackOccupancy = ({
450489

451490
// Fetch new occupation if dragging has stopped:
452491
if (stopPanning) {
453-
const draggedTrainEditoastId = draggedTrainId;
454492
await Promise.all(
455493
[...impactedPathOperationalPointIDs].map(async (waypointId) => {
456494
const newZones = await fetchTrackOccupancy(
457495
getOperationalPointReference(pathOpsByWaypointId.get(waypointId)),
458-
pathOpsByWaypointId.get(waypointId)?.opId,
496+
waypointId,
459497
{
460-
[draggedTrainEditoastId]: newTrainData,
498+
[draggedTrainId]: newTrainData,
461499
}
462500
);
463501

@@ -489,30 +527,26 @@ const useTrackOccupancy = ({
489527

490528
// Load all tracks from all waypoints on mount / waypoints update:
491529
useEffect(() => {
492-
if (pathfindingHasFailed) {
493-
return;
494-
}
495-
496530
let aborted = false;
497531

498532
const pathOperationalPointsWithoutTracks = pathOperationalPoints.filter(
499533
(op) => !(tracksState.data || {})[op.waypointId]
500534
);
501535
const loadAllTracks = async (
502-
operationalPointReferences: { operational_point: string; type: 'id' }[]
536+
opsWithReferences: { waypointId: string; reference: OperationalPointReference }[]
503537
) => {
504538
setTracksState((state) => ({ type: 'loading', data: state.data || {} }));
505539

506540
try {
507541
const data = await postInfraByInfraIdMatchOperationalPoints({
508542
infraId,
509-
body: { operational_point_references: operationalPointReferences },
543+
body: { operational_point_references: opsWithReferences.map((o) => o.reference) },
510544
}).unwrap();
511545

512546
if (aborted) return;
513547

514548
const allTrackIds = data.related_operational_points.flatMap(([points]) =>
515-
points.parts.map((part) => part.track)
549+
points ? points.parts.map((part) => part.track) : []
516550
);
517551
const fetchedTrackSections = await getTrackSectionsByIds(allTrackIds);
518552

@@ -523,20 +557,21 @@ const useTrackOccupancy = ({
523557

524558
localTrackNameToTrackIdRef.current = new Map();
525559

526-
data.related_operational_points.forEach(([operationalPoint]) => {
560+
opsWithReferences.forEach(({ waypointId: wId }, i) => {
561+
const [operationalPoint] = data.related_operational_points[i];
527562
if (!operationalPoint) return;
528563
const mapping = new Map<string, string>();
529564
for (const part of operationalPoint.parts) {
530565
mapping.set(part.local_track_name, part.track);
531566
}
532-
localTrackNameToTrackIdRef.current.set(operationalPoint.id, mapping);
567+
localTrackNameToTrackIdRef.current.set(wId, mapping);
533568
});
534569

535570
const loadedTracks = fromPairs(
536-
operationalPointReferences.map(({ operational_point }, i) => [
537-
operational_point,
571+
opsWithReferences.map(({ waypointId: wId }, i) => [
572+
wId,
538573
uniqBy(
539-
data.related_operational_points[i][0].parts.map((part) => {
574+
(data.related_operational_points[i][0]?.parts ?? []).map((part) => {
540575
const trackPart = trackSectionByTrackId.get(part.track);
541576
return {
542577
id: part.track,
@@ -548,24 +583,32 @@ const useTrackOccupancy = ({
548583
),
549584
])
550585
);
551-
setTracksState({
586+
setTracksState((state) => ({
552587
type: 'ok',
553-
data: loadedTracks,
554-
});
588+
data: { ...(state.data ?? {}), ...loadedTracks },
589+
}));
555590
} catch (e) {
556591
console.error(e);
557592
}
558593
};
559-
const waypointsPayload = pathOperationalPointsWithoutTracks.flatMap((op) =>
560-
op.opId ? [{ operational_point: op.opId, type: 'id' as const }] : []
561-
);
562-
if (!waypointsPayload.length) return noop;
594+
const waypointsPayload = pathOperationalPointsWithoutTracks.flatMap<{
595+
waypointId: string;
596+
reference: OperationalPointReference;
597+
}>((op) => {
598+
const reference = getOperationalPointReference(op);
599+
if (!reference) return [];
600+
return [{ waypointId: op.waypointId, reference }];
601+
});
602+
if (!waypointsPayload.length) {
603+
setTracksState((state) => ({ type: 'ok', data: state.data || {} }));
604+
return noop;
605+
}
563606

564607
loadAllTracks(waypointsPayload);
565608
return () => {
566609
aborted = true;
567610
};
568-
}, [pathOperationalPoints, pathfindingHasFailed]);
611+
}, [pathOperationalPoints]);
569612

570613
// Update train data for all deployed waypoints on trains update:
571614
useEffect(() => {
@@ -620,7 +663,7 @@ const useTrackOccupancy = ({
620663
forEach(pathOperationalPointsState, async (_, waypointId) => {
621664
const newZones = await fetchTrackOccupancy(
622665
getOperationalPointReference(pathOpsByWaypointId.get(waypointId)),
623-
pathOpsByWaypointId.get(waypointId)?.opId,
666+
waypointId,
624667
Object.fromEntries(
625668
[...addedTrainIDs, ...modifiedTrainIDs].map((id) => [
626669
id,

front/src/modules/simulationResult/components/SpaceTimeChartWrapper/useWaypointMenu.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@ import { getWaypointsLocalStorageKey } from './helpers/utils';
1313

1414
const useWaypointMenu = (
1515
activeWaypointRef: RefObject<HTMLDivElement | null>,
16-
waypointsPanelData?: WaypointsPanelData,
17-
allTrainsProjected?: boolean,
18-
pathfindingHasFailed?: boolean
16+
waypointsPanelData?: WaypointsPanelData
1917
) => {
2018
const {
2119
filteredWaypoints,
@@ -100,7 +98,6 @@ const useWaypointMenu = (
10098

10199
menuItems.push({
102100
dataTestID: 'occupancy-menu-button',
103-
disabled: pathfindingHasFailed || (!isDeployed && !allTrainsProjected),
104101
title: isDeployed
105102
? t('simulationResults.waypointMenu.hideOccupancy')
106103
: t('simulationResults.waypointMenu.showOccupancy'),

0 commit comments

Comments
 (0)