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'