---
.../ManchetteWithSpaceTimeChart.tsx | 120 ++++++++++++++++--
.../helpers/__tests__/utils.spec.ts | 86 +++++++++++++
.../SpaceTimeChart/helpers/utils.ts | 34 +++++
front/src/modules/simulationResult/types.ts | 7 +
4 files changed, 233 insertions(+), 14 deletions(-)
create mode 100644 front/src/modules/simulationResult/components/SpaceTimeChart/helpers/__tests__/utils.spec.ts
create mode 100644 front/src/modules/simulationResult/components/SpaceTimeChart/helpers/utils.ts
diff --git a/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx b/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx
index 71a9489bc6c..770394e2801 100644
--- a/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx
+++ b/front/src/modules/simulationResult/components/ManchetteWithSpaceTimeChart/ManchetteWithSpaceTimeChart.tsx
@@ -1,4 +1,4 @@
-import { useRef, useState } from 'react';
+import { useMemo, useRef, useState } from 'react';
import { KebabHorizontal } from '@osrd-project/ui-icons';
import { Manchette } from '@osrd-project/ui-manchette';
@@ -11,12 +11,18 @@ import {
OccupancyBlockLayer,
} from '@osrd-project/ui-spacetimechart';
import type { Conflict } from '@osrd-project/ui-spacetimechart';
+import { compact } from 'lodash';
import type { OperationalPoint, TrainSpaceTimeData } from 'applications/operationalStudies/types';
import upward from 'assets/pictures/workSchedules/ScheduledMaintenanceUp.svg';
import type { PostWorkSchedulesProjectPathApiResponse } from 'common/api/osrdEditoastApi';
+import cutSpaceTimeRect from 'modules/simulationResult/components/SpaceTimeChart/helpers/utils';
import { ASPECT_LABELS_COLORS } from 'modules/simulationResult/consts';
-import type { AspectLabel, WaypointsPanelData } from 'modules/simulationResult/types';
+import type {
+ AspectLabel,
+ LayerRangeData,
+ WaypointsPanelData,
+} from 'modules/simulationResult/types';
import SettingsPanel from './SettingsPanel';
import ManchetteMenuButton from '../SpaceTimeChart/ManchetteMenuButton';
@@ -45,9 +51,91 @@ const ManchetteWithSpaceTimeChartWrapper = ({
const [waypointsPanelIsOpen, setWaypointsPanelIsOpen] = useState(false);
+ // Cut the space time chart curves if the first or last waypoints are hidden
+ const spaceTimeChartLayersData = useMemo(() => {
+ let filteredProjectPathTrainResult = projectPathTrainResult;
+ let filteredConflicts = conflicts;
+
+ if (!waypointsPanelData || waypointsPanelData.filteredWaypoints.length < 2)
+ return { filteredProjectPathTrainResult, filteredConflicts };
+
+ const { filteredWaypoints } = waypointsPanelData;
+ const firstPosition = filteredWaypoints.at(0)!.position;
+ const lastPosition = filteredWaypoints.at(-1)!.position;
+
+ if (firstPosition !== 0 || lastPosition !== operationalPoints.at(-1)!.position) {
+ filteredProjectPathTrainResult = projectPathTrainResult.map((train) => ({
+ ...train,
+ space_time_curves: train.space_time_curves.map(({ positions, times }) => {
+ const cutPositions: number[] = [];
+ const cutTimes: number[] = [];
+
+ for (let i = 1; i < positions.length; i += 1) {
+ const currentRange: LayerRangeData = {
+ spaceStart: positions[i - 1],
+ spaceEnd: positions[i],
+ timeStart: times[i - 1],
+ timeEnd: times[i],
+ };
+
+ const interpolatedRange = cutSpaceTimeRect(currentRange, firstPosition, lastPosition);
+
+ // TODO : remove reformatting the datas when https://github.com/OpenRailAssociation/osrd-ui/issues/694 is merged
+ if (!interpolatedRange) continue;
+
+ if (i === 1 || cutPositions.length === 0) {
+ cutPositions.push(interpolatedRange.spaceStart);
+ cutTimes.push(interpolatedRange.timeStart);
+ }
+ cutPositions.push(interpolatedRange.spaceEnd);
+ cutTimes.push(interpolatedRange.timeEnd);
+ }
+
+ return {
+ positions: cutPositions,
+ times: cutTimes,
+ };
+ }),
+ signal_updates: compact(
+ train.signal_updates.map((signal) => {
+ const updatedSignalRange = cutSpaceTimeRect(
+ {
+ spaceStart: signal.position_start,
+ spaceEnd: signal.position_end,
+ timeStart: signal.time_start,
+ timeEnd: signal.time_end,
+ },
+ firstPosition,
+ lastPosition
+ );
+
+ if (!updatedSignalRange) return null;
+
+ // TODO : remove reformatting the datas when https://github.com/OpenRailAssociation/osrd-ui/issues/694 is merged
+ return {
+ ...signal,
+ position_start: updatedSignalRange.spaceStart,
+ position_end: updatedSignalRange.spaceEnd,
+ time_start: updatedSignalRange.timeStart,
+ time_end: updatedSignalRange.timeEnd,
+ };
+ })
+ ),
+ }));
+
+ filteredConflicts = compact(
+ conflicts.map((conflict) => cutSpaceTimeRect(conflict, firstPosition, lastPosition))
+ );
+
+ return { filteredProjectPathTrainResult, filteredConflicts };
+ }
+
+ return { filteredProjectPathTrainResult, filteredConflicts };
+ }, [waypointsPanelData?.filteredWaypoints, projectPathTrainResult, conflicts]);
+
const { manchetteProps, spaceTimeChartProps, handleScroll } = useManchettesWithSpaceTimeChart(
waypointsPanelData?.filteredWaypoints ?? operationalPoints,
- projectPathTrainResult,
+ spaceTimeChartLayersData.filteredProjectPathTrainResult,
manchetteWithSpaceTimeChartRef,
selectedTrainScheduleId
);
@@ -58,17 +146,19 @@ const ManchetteWithSpaceTimeChartWrapper = ({
showSignalsStates: false,
});
- const occupancyBlocks = projectPathTrainResult.flatMap((train) => {
- const departureTime = new Date(train.departure_time).getTime();
+ const occupancyBlocks = spaceTimeChartLayersData.filteredProjectPathTrainResult.flatMap(
+ (train) => {
+ const departureTime = new Date(train.departure_time).getTime();
- return train.signal_updates.map((block) => ({
- timeStart: departureTime + block.time_start,
- timeEnd: departureTime + block.time_end,
- spaceStart: block.position_start,
- spaceEnd: block.position_end,
- color: ASPECT_LABELS_COLORS[block.aspect_label as AspectLabel],
- }));
- });
+ return train.signal_updates.map((block) => ({
+ timeStart: departureTime + block.time_start,
+ timeEnd: departureTime + block.time_end,
+ spaceStart: block.position_start,
+ spaceEnd: block.position_end,
+ color: ASPECT_LABELS_COLORS[block.aspect_label as AspectLabel],
+ }));
+ }
+ );
return (
@@ -134,7 +224,9 @@ const ManchetteWithSpaceTimeChartWrapper = ({
imageUrl={upward}
/>
)}
- {settings.showConflicts && }
+ {settings.showConflicts && (
+
+ )}
{settings.showSignalsStates && (
)}
diff --git a/front/src/modules/simulationResult/components/SpaceTimeChart/helpers/__tests__/utils.spec.ts b/front/src/modules/simulationResult/components/SpaceTimeChart/helpers/__tests__/utils.spec.ts
new file mode 100644
index 00000000000..7d579e523b4
--- /dev/null
+++ b/front/src/modules/simulationResult/components/SpaceTimeChart/helpers/__tests__/utils.spec.ts
@@ -0,0 +1,86 @@
+import { describe, it, expect } from 'vitest';
+
+import cutSpaceTimeRect from '../utils';
+
+describe('interpolateRange', () => {
+ it('should return null if the interpolated range ends before the cut space', () => {
+ const range = {
+ spaceStart: 3,
+ spaceEnd: 5,
+ timeStart: 100,
+ timeEnd: 200,
+ };
+ const interpolatedRange = cutSpaceTimeRect(range, 1, 3);
+ expect(interpolatedRange).toBeNull();
+ });
+
+ it('should return null if the interpolated range starts after the cut space', () => {
+ const range = {
+ spaceStart: 3,
+ spaceEnd: 5,
+ timeStart: 100,
+ timeEnd: 200,
+ };
+ const interpolatedRange = cutSpaceTimeRect(range, 5, 7);
+ expect(interpolatedRange).toBeNull();
+ });
+
+ it('should return the same range if its ranges are inside the cut space', () => {
+ const range = {
+ spaceStart: 3,
+ spaceEnd: 5,
+ timeStart: 100,
+ timeEnd: 200,
+ };
+ const interpolatedRange = cutSpaceTimeRect(range, 2, 7);
+ expect(interpolatedRange).toEqual(range);
+ });
+
+ it('should return the interpolated range when the start position is outside the cut space', () => {
+ const range = {
+ spaceStart: 3,
+ spaceEnd: 5,
+ timeStart: 100,
+ timeEnd: 200,
+ };
+ const interpolatedRange = cutSpaceTimeRect(range, 4, 5);
+ expect(interpolatedRange).toEqual({
+ spaceStart: 4,
+ spaceEnd: 5,
+ timeStart: 150,
+ timeEnd: 200,
+ });
+ });
+
+ it('should return the interpolated range when the end position is is outside the cut space', () => {
+ const range = {
+ spaceStart: 3,
+ spaceEnd: 6,
+ timeStart: 100,
+ timeEnd: 160,
+ };
+ const interpolatedRange = cutSpaceTimeRect(range, 3, 5);
+ expect(interpolatedRange).toEqual({
+ spaceStart: 3,
+ spaceEnd: 5,
+ timeStart: 100,
+ timeEnd: 140,
+ });
+ });
+
+ it('should return the interpolated range when both positions are outside the cut space', () => {
+ const range = {
+ spaceStart: 3,
+ spaceEnd: 6,
+ timeStart: 100,
+ timeEnd: 160,
+ };
+ const interpolatedRange = cutSpaceTimeRect(range, 4, 5);
+ expect(interpolatedRange).toEqual({
+ spaceStart: 4,
+ spaceEnd: 5,
+ timeStart: 120,
+ timeEnd: 140,
+ });
+ });
+});
diff --git a/front/src/modules/simulationResult/components/SpaceTimeChart/helpers/utils.ts b/front/src/modules/simulationResult/components/SpaceTimeChart/helpers/utils.ts
new file mode 100644
index 00000000000..dc010f8e9cf
--- /dev/null
+++ b/front/src/modules/simulationResult/components/SpaceTimeChart/helpers/utils.ts
@@ -0,0 +1,34 @@
+import type { LayerRangeData } from '../../../types';
+
+const cutSpaceTimeRect = (
+ range: LayerRangeData,
+ minSpace: number,
+ maxSpace: number
+): LayerRangeData | null => {
+ let { timeStart, timeEnd, spaceStart, spaceEnd } = range;
+
+ if (spaceEnd <= minSpace || spaceStart >= maxSpace) {
+ return null;
+ }
+
+ if (spaceStart < minSpace) {
+ const interpolationFactor = (minSpace - spaceStart) / (spaceEnd - spaceStart);
+ spaceStart = minSpace;
+ timeStart += (timeEnd - timeStart) * interpolationFactor;
+ }
+
+ if (spaceEnd > maxSpace) {
+ const interpolationFactor = (spaceEnd - maxSpace) / (spaceEnd - spaceStart);
+ spaceEnd = maxSpace;
+ timeEnd -= (timeEnd - timeStart) * interpolationFactor;
+ }
+
+ return {
+ spaceStart,
+ spaceEnd,
+ timeStart,
+ timeEnd,
+ };
+};
+
+export default cutSpaceTimeRect;
diff --git a/front/src/modules/simulationResult/types.ts b/front/src/modules/simulationResult/types.ts
index 32654dd8ede..658fd105329 100644
--- a/front/src/modules/simulationResult/types.ts
+++ b/front/src/modules/simulationResult/types.ts
@@ -42,6 +42,13 @@ export type WaypointsPanelData = {
projectionPath: TrainScheduleBase['path'];
};
+export type LayerRangeData = {
+ spaceStart: number;
+ spaceEnd: number;
+ timeStart: number;
+ timeEnd: number;
+};
+
export type AspectLabel =
| 'VL'
| '300VL'