From 2cf559638e3f8b0d77a6a890e9655f6ddb15139b Mon Sep 17 00:00:00 2001 From: romainvalls Date: Tue, 25 Jun 2024 20:32:55 +0200 Subject: [PATCH] front: adapt power restriction v2 inputs --- .../manageTrainSchedule.json | 1 + .../manageTrainSchedule.json | 1 + .../views/v2/ManageTrainScheduleV2.tsx | 17 ++- .../IntervalsEditor/IntervalsEditor.tsx | 11 +- .../IntervalsEditorCommonForm.tsx | 12 +- .../IntervalsEditorTooltip.tsx | 8 +- .../PowerRestrictionsSelectorV2.tsx | 58 +++++++- .../reducers/osrdconf/osrdConfCommon/index.ts | 140 +++++++++++++++++- .../reducers/osrdconf/osrdConfCommon/utils.ts | 15 ++ front/src/utils/strings.ts | 6 + 10 files changed, 248 insertions(+), 21 deletions(-) create mode 100644 front/src/reducers/osrdconf/osrdConfCommon/utils.ts diff --git a/front/public/locales/en/operationalStudies/manageTrainSchedule.json b/front/public/locales/en/operationalStudies/manageTrainSchedule.json index 585dcc696ac..0f5eba5eeac 100644 --- a/front/public/locales/en/operationalStudies/manageTrainSchedule.json +++ b/front/public/locales/en/operationalStudies/manageTrainSchedule.json @@ -82,6 +82,7 @@ "powerRestrictionExplanationText": "By default, the simulation will use the nominal power.", "powerRestrictionEmptyExplanationText": "The selected rolling stock does not have any power restrictions. You can edit it in the rolling stock editor.", "removeVia": "Remove this waypoint", + "resetPowerRestrictions": "Reset power restrictions", "restartPathfinding": "Restart search", "rollingstock": "Rolling stock", "saving": "Saving... please wait", diff --git a/front/public/locales/fr/operationalStudies/manageTrainSchedule.json b/front/public/locales/fr/operationalStudies/manageTrainSchedule.json index b6b2af9519b..5c435dc9608 100644 --- a/front/public/locales/fr/operationalStudies/manageTrainSchedule.json +++ b/front/public/locales/fr/operationalStudies/manageTrainSchedule.json @@ -84,6 +84,7 @@ "powerRestrictionEmptyExplanationText": "Le matériel roulant sélectionné ne possède pas de restrictions de puissance. Vous pouvez en créer dans l'éditeur de matériel roulant.", "removeVia": "Retirer ce via", "restartPathfinding": "Relancer la recherche", + "resetPowerRestrictions": "Réinitialiser les restrictions de puissance", "rollingstock": "Matériel", "saving": "Enregistrement en cours... veuillez patienter", "simulation": "Simulation", diff --git a/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx b/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx index 81dff98c3d9..7bee5eeb629 100644 --- a/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx +++ b/front/src/applications/operationalStudies/views/v2/ManageTrainScheduleV2.tsx @@ -1,5 +1,6 @@ -import React, { useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; +import { compact } from 'lodash'; import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; @@ -16,6 +17,7 @@ import { useOsrdConfSelectors } from 'common/osrdContext'; import { useStoreDataForSpeedLimitByTagSelector } from 'common/SpeedLimitByTagSelector/useStoreDataForSpeedLimitByTagSelector'; import Tabs from 'common/Tabs'; import ItineraryV2 from 'modules/pathfinding/components/Itinerary/ItineraryV2'; +import { upsertViasInOPs } from 'modules/pathfinding/utils'; import PowerRestrictionsSelectorV2 from 'modules/powerRestriction/components/PowerRestrictionsSelectorV2'; import RollingStock2Img from 'modules/rollingStock/components/RollingStock2Img'; import { RollingStockSelector } from 'modules/rollingStock/components/RollingStockSelector'; @@ -56,6 +58,19 @@ const ManageTrainScheduleV2 = () => { value: string; }; + useEffect(() => { + if (pathProperties) { + const allVias = upsertViasInOPs( + pathProperties.suggestedOperationalPoints, + compact(pathSteps) + ); + setPathProperties({ + ...pathProperties, + allVias, + }); + } + }, [pathSteps]); + const pathElectrificationRanges = (): IntervalItem[] => { if (!pathProperties || !pathProperties.electrifications) return []; diff --git a/front/src/common/IntervalsEditor/IntervalsEditor.tsx b/front/src/common/IntervalsEditor/IntervalsEditor.tsx index e42d5eefaba..a73e66de136 100644 --- a/front/src/common/IntervalsEditor/IntervalsEditor.tsx +++ b/front/src/common/IntervalsEditor/IntervalsEditor.tsx @@ -52,6 +52,7 @@ type IntervalsEditorProps = { /** Total length of the path */ totalLength: number; disableDrag?: boolean; + onResizeFromInput?: (intervalIndex: number, newEnd: number, context: 'begin' | 'end') => void; } & ( | { intervalType: INTERVAL_TYPES.NUMBER; @@ -99,6 +100,7 @@ const IntervalsEditor = (props: IntervalsEditorProps) => { mergeTool: true, }, disableDrag = false, + onResizeFromInput, } = props; // Which segment areas are visible @@ -113,13 +115,13 @@ const IntervalsEditor = (props: IntervalsEditorProps) => { // Data to display const [resizingData, setResizingData] = useState(data); - useEffect(() => { - setResizingData(data); - }, [data]); - // Which segment is selected const [selected, setSelected] = useState(null); + useEffect(() => { + setResizingData(data); + }, [data, selected]); + // For mouse click / doubleClick const [clickTimeout, setClickTimeout] = useState(null); const [clickPrevent, setClickPrevent] = useState(false); @@ -333,6 +335,7 @@ const IntervalsEditor = (props: IntervalsEditorProps) => { interval={data[selected]} selectedIntervalIndex={selected} setData={setData} + onInputChange={onResizeFromInput} setSelectedIntervalIndex={setSelected} totalLength={totalLength} defaultValue={defaultValue} diff --git a/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx b/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx index f28d948c804..0c374b4bc60 100644 --- a/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx +++ b/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx @@ -12,7 +12,8 @@ type IntervalsEditorFormProps = { data: IntervalItem[]; interval: IntervalItem; selectedIntervalIndex: number; - setData: (newData: IntervalItem[]) => void; + setData: (newData: IntervalItem[], selectedIntervalIndex?: number) => void; + onInputChange?: (intervalIndex: number, newBegin: number, context: 'begin' | 'end') => void; setSelectedIntervalIndex: (selectedIntervalIndex: number) => void; totalLength: number; defaultValue: string | number; @@ -26,11 +27,12 @@ const IntervalsEditorCommonForm = ({ setSelectedIntervalIndex, totalLength, defaultValue, + onInputChange, }: IntervalsEditorFormProps) => { const { t } = useTranslation('common/common'); const [begin, setBegin] = useState(Math.round(interval.begin)); - const [end, setEnd] = useState(Math.round(interval.end)); + const [end, setEnd] = useState(Math.round(interval.end)); useEffect(() => { setBegin(Math.round(interval.begin)); @@ -53,7 +55,11 @@ const IntervalsEditorCommonForm = ({ fieldName: 'value', defaultValue, }); - setData(fixedResults); + if (onInputChange) { + onInputChange(selectedIntervalIndex, newPosition, context); + } else { + setData(fixedResults, selectedIntervalIndex); + } // update the selected interval if needed // corner case: if we create a new empty first segment diff --git a/front/src/common/IntervalsEditor/IntervalsEditorTooltip.tsx b/front/src/common/IntervalsEditor/IntervalsEditorTooltip.tsx index 705abeb5458..f9ea462b62d 100644 --- a/front/src/common/IntervalsEditor/IntervalsEditorTooltip.tsx +++ b/front/src/common/IntervalsEditor/IntervalsEditorTooltip.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; +import { formatMValue } from 'utils/strings'; + import type { IntervalItem } from './types'; interface IntervalsEditorTooltip { @@ -13,15 +15,15 @@ const IntervalsEditorTooltip = ({ item, point }: IntervalsEditorTooltip) => { const { t } = useTranslation('common/common'); return (
-
{point && {Math.round(point)}}
+
{point && {Math.round(formatMValue(point))}}
{t('begin')} - {Math.round(item.begin)} + {Math.round(formatMValue(item.begin))}
{t('end')} - {Math.round(item.end)} + {Math.round(formatMValue(item.end))}
{t('value')} diff --git a/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx b/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx index 3a1196fbebc..4886d6f9c95 100644 --- a/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx +++ b/front/src/modules/powerRestriction/components/PowerRestrictionsSelectorV2.tsx @@ -46,12 +46,12 @@ const PowerRestrictionsSelectorV2 = ({ resetPowerRestrictionRangesV2, cutPowerRestrictionRangesV2, deletePowerRestrictionRangesV2, + resizePowerRestrictionRangeV2, } = useOsrdConfActions(); const powerRestrictionRanges = useSelector(getPowerRestrictionV2); const pathSteps = compact(useSelector(getPathSteps)); // pathSteps + points de coupure d'électricité - // const [updatedPathSteps, setUpdatedPathSteps] = useState([]); const [cutPositions, setCutPositions] = useState([]); const [intervalsEditorData, setIntervalsEditorData] = useState([]); @@ -198,6 +198,53 @@ const PowerRestrictionsSelectorV2 = ({ dispatch(deletePowerRestrictionRangesV2({ from: fromPathStep, to: toPathStep })); } }; + + const resizeSegmentByInput = ( + selectedSegmentIndex: number, + newEnd: number, + context: 'begin' | 'end' + ) => { + const firstIndex = context === 'end' ? selectedSegmentIndex : selectedSegmentIndex - 1; + + let firstRestriction: PowerRestrictionV2 | undefined; + let secondRestriction: PowerRestrictionV2 | undefined; + if (firstIndex >= 0) { + const firstRangeData = intervalsEditorData[firstIndex]; + if (firstRangeData.value !== NO_POWER_RESTRICTION) { + const fromPathStep = pathSteps.find((step) => step.positionOnPath === firstRangeData.begin); + const toPathStep = pathSteps.find((step) => step.positionOnPath === firstRangeData.end); + if (fromPathStep && toPathStep) { + firstRestriction = powerRestrictionRanges.find( + (restriction) => + restriction.from === fromPathStep.id && restriction.to === toPathStep.id + ); + } + } + } + + const secondRangeData = intervalsEditorData[firstIndex + 1]; + if (secondRangeData.value !== NO_POWER_RESTRICTION) { + const fromPathStep = pathSteps.find((step) => step.positionOnPath === secondRangeData.begin); + const toPathStep = pathSteps.find((step) => step.positionOnPath === secondRangeData.end); + if (fromPathStep && toPathStep) { + secondRestriction = powerRestrictionRanges.find( + (restriction) => restriction.from === fromPathStep.id && restriction.to === toPathStep.id + ); + } + } + + let newEndPathStep = pathSteps.find((pathStep) => pathStep.positionOnPath === newEnd); + if (!newEndPathStep) { + newEndPathStep = createPathStep(newEnd, cumulativeSums, pathProperties, pathSteps); + } + + if (firstRestriction && newEndPathStep) { + dispatch( + resizePowerRestrictionRangeV2({ firstRestriction, secondRestriction, newEndPathStep }) + ); + } + }; + const formatElectricalRanges = ( ranges: PowerRestrictionV2[] ): { begin: number; end: number; value: string }[] => { @@ -324,9 +371,14 @@ const PowerRestrictionsSelectorV2 = ({ deleteTool: true, }} disableDrag + onResizeFromInput={resizeSegmentByInput} /> - ) : ( diff --git a/front/src/reducers/osrdconf/osrdConfCommon/index.ts b/front/src/reducers/osrdconf/osrdConfCommon/index.ts index a1505a6d957..119342b3f63 100644 --- a/front/src/reducers/osrdconf/osrdConfCommon/index.ts +++ b/front/src/reducers/osrdconf/osrdConfCommon/index.ts @@ -1,6 +1,6 @@ import type { CaseReducer, PayloadAction, PrepareAction } from '@reduxjs/toolkit'; import type { Draft } from 'immer'; -import { compact, keyBy, omit, sortBy } from 'lodash'; +import { compact, isEqual, keyBy, omit, sortBy } from 'lodash'; import nextId from 'react-id-generator'; import type { PointOnMap, PowerRestrictionV2 } from 'applications/operationalStudies/consts'; @@ -22,6 +22,8 @@ import { addElementAtIndex, removeElementAtIndex, replaceElementAtIndex } from ' import { formatIsoDate } from 'utils/date'; import type { ArrayElement } from 'utils/types'; +import { checkValidPathStep } from './utils'; + export const defaultCommonConf: OsrdConfState = { constraintDistribution: 'MARECO', name: '', @@ -122,8 +124,15 @@ interface CommonConfReducers extends InfraStateReducers S, PayloadAction<{ from: PathStep; to: PathStep }> >; + ['resizePowerRestrictionRangeV2']: CaseReducer< + S, + PayloadAction<{ + firstRestriction: PowerRestrictionV2; + secondRestriction?: PowerRestrictionV2; + newEndPathStep: PathStep; + }> + >; ['resetPowerRestrictionRangesV2']: CaseReducer; - // ajouter une nouvelle action updatePowerRestrionRangesV2 ['updateTrainScheduleIDsToModify']: CaseReducer>; ['updateFeatureInfoClick']: CaseReducer>; ['updatePathSteps']: CaseReducer>; @@ -450,11 +459,6 @@ export function buildCommonConfReducers(): CommonConfRe (restriction) => restriction.from !== from.id && restriction.to !== to.id ); - const checkValidPathStep = (pathStep: PathStep) => { - if (!pathStep || pathStep.locked || pathStep.arrival || pathStep.stopFor) return false; - return true; - }; - // TO DO: retirer les path steps inutilisés // / ! \ verifier le champ des marges après rebase const fromIsUsed = newPowerRestrictionRangesV2.some( @@ -486,6 +490,128 @@ export function buildCommonConfReducers(): CommonConfRe state.pathSteps = newPathSteps; state.powerRestrictionV2 = newPowerRestrictionRangesV2; }, + resizePowerRestrictionRangeV2( + state: Draft, + action: PayloadAction<{ + firstRestriction: PowerRestrictionV2; + secondRestriction?: PowerRestrictionV2; + newEndPathStep: PathStep; + }> + ) { + const { firstRestriction, secondRestriction, newEndPathStep } = action.payload; + let newPathSteps = [...state.pathSteps]; + let newPowerRestrictionRanges = state.powerRestrictionV2.filter( + (restriction) => + !isEqual(restriction, firstRestriction) || !isEqual(restriction, secondRestriction) + ); + + // remove the previous end pathStep of the first restriction if needed + const oldPathStepEndId = firstRestriction.to; + const oldPathStepEnd = newPathSteps.find( + (pathStep) => pathStep && pathStep.id === oldPathStepEndId + ); + if (oldPathStepEnd) { + const oldPathStepEndIsUsed = newPowerRestrictionRanges.some( + (restriction) => + restriction.from === oldPathStepEndId || restriction.to === oldPathStepEndId + ); + if (oldPathStepEndIsUsed && checkValidPathStep(oldPathStepEnd)) { + newPathSteps = newPathSteps.filter( + (pathStep) => pathStep?.positionOnPath !== oldPathStepEnd.positionOnPath + ); + } + } + + // create the new to pathStep if it does not exist + const newEndPathStepExists = newPathSteps.some( + (pathStep) => pathStep && pathStep.id === newEndPathStep.id + ); + if (!newEndPathStepExists) { + const index = newPathSteps.findIndex( + (step) => step?.positionOnPath && step.positionOnPath > newEndPathStep.positionOnPath! + ); + + newPathSteps = addElementAtIndex(newPathSteps, index, newEndPathStep); + } + + // remove the previous from pathStep of the second restriction if needed + if (secondRestriction) { + const oldBeginPathStepId = secondRestriction.from; + const oldBeginPathStep = newPathSteps.find( + (pathStep) => pathStep && pathStep.id === oldBeginPathStepId + ); + + if (oldBeginPathStep) { + const oldBeginPathStepIsUsed = newPowerRestrictionRanges.some( + (restriction) => + restriction.from === oldBeginPathStepId || restriction.to === oldBeginPathStepId + ); + + if (!oldBeginPathStepIsUsed && checkValidPathStep(oldBeginPathStep)) { + newPathSteps = newPathSteps.filter( + (pathStep) => pathStep?.positionOnPath !== oldBeginPathStep.positionOnPath + ); + } + } + } + + // update the powerRestrictions + newPowerRestrictionRanges = newPowerRestrictionRanges.map((restriction) => { + if (restriction.to === firstRestriction.to) { + return { ...restriction, to: newEndPathStep.id }; + } + if (restriction.from === secondRestriction?.from) { + return { ...restriction, from: newEndPathStep.id }; + } + return restriction; + }); + // retirer les ranges qui sont complètement recouvertes + if (secondRestriction) { + const secondRestrictionToPathStep = newPathSteps.find( + (pathStep) => pathStep && pathStep.id === secondRestriction.to + ); + if (secondRestrictionToPathStep) { + const isRangeCovered = (range: PowerRestrictionV2) => { + const fromPathStep = newPathSteps.find( + (pathStep) => pathStep && pathStep.id === range.from + ); + const toPathStep = newPathSteps.find( + (pathStep) => pathStep && pathStep.id === range.to + ); + + if ( + fromPathStep?.positionOnPath === undefined || + toPathStep?.positionOnPath === undefined + ) { + return false; + } + return ( + fromPathStep?.positionOnPath >= newEndPathStep.positionOnPath! && + toPathStep?.positionOnPath <= secondRestrictionToPathStep.positionOnPath! + ); + }; + newPowerRestrictionRanges = newPowerRestrictionRanges.filter( + (restriction) => !isRangeCovered(restriction) + ); + } + if (secondRestrictionToPathStep) { + const secondRestrictionToPathStepIsUsed = newPowerRestrictionRanges.some( + (restriction) => + restriction.from === secondRestrictionToPathStep.id || + restriction.to === secondRestrictionToPathStep.id + ); + + if (!secondRestrictionToPathStepIsUsed && newPathSteps) { + newPathSteps = newPathSteps.filter( + (pathStep) => pathStep && pathStep.id !== secondRestrictionToPathStep!.id + ); + } + } + } + + state.pathSteps = newPathSteps; + state.powerRestrictionV2 = newPowerRestrictionRanges; + }, // TODO Remove this resetPowerRestrictionRangesV2(state: Draft) { state.powerRestrictionV2 = []; diff --git a/front/src/reducers/osrdconf/osrdConfCommon/utils.ts b/front/src/reducers/osrdconf/osrdConfCommon/utils.ts new file mode 100644 index 00000000000..23c3de1797b --- /dev/null +++ b/front/src/reducers/osrdconf/osrdConfCommon/utils.ts @@ -0,0 +1,15 @@ +/* eslint-disable import/prefer-default-export */ + +import type { PathStep } from '../types'; + +export const checkValidPathStep = (pathStep: PathStep) => { + if ( + !pathStep || + pathStep.locked || + pathStep.arrival || + pathStep.stopFor || + pathStep.theoreticalMargin + ) + return false; + return true; +}; diff --git a/front/src/utils/strings.ts b/front/src/utils/strings.ts index c2a0e960bb5..77dae7e1ff3 100644 --- a/front/src/utils/strings.ts +++ b/front/src/utils/strings.ts @@ -23,6 +23,12 @@ export function formatKmValue(value: number, unit: Unit = 'meters', digits = 3) return `${(value / divider).toFixed(digits)}${NO_BREAK_SPACE}km`; } +export function formatMValue(value: number) { + const divider = 1000; + + return value / divider; +} + export const createPowerRestrictions = ( boundaries: number[], values: { type: string; voltage?: string }[]