From 8a73067fc87be091cea1e8bd5e04b605c12cf162 Mon Sep 17 00:00:00 2001 From: Theo Macron Date: Wed, 6 Dec 2023 00:41:18 +0100 Subject: [PATCH] front: add new tracksection length, curves, slopes and loading gauges behavior --- front/public/locales/en/translation.json | 5 +- front/public/locales/fr/translation.json | 5 +- .../LinearMetadata/FormComponent.tsx | 62 +++++++++++++++++-- .../editor/tools/trackEdition/components.tsx | 41 ++++++++++-- .../editor/tools/trackEdition/tool.tsx | 16 ++++- .../editor/tools/trackEdition/utils.ts | 15 ++++- .../FormSNCF/DebouncedNumberInputSNCF.tsx | 24 +++++-- .../src/common/IntervalsDataViz/data.test.ts | 4 +- front/src/common/IntervalsDataViz/data.ts | 22 ++++--- .../IntervalsEditorCommonForm.tsx | 4 ++ front/src/types/editor.ts | 8 ++- 11 files changed, 172 insertions(+), 34 deletions(-) diff --git a/front/public/locales/en/translation.json b/front/public/locales/en/translation.json index f9d05e601de..0208d6bcf6b 100644 --- a/front/public/locales/en/translation.json +++ b/front/public/locales/en/translation.json @@ -450,8 +450,9 @@ "mode-add-point": "Add a point", "mode-delete-point": "Delete points", "mode-move-point": "Move points", - "toggle-anchoring": "Toggle anchoring on/off", - "save-line": "Save the line" + "reset-line": "Reset data", + "save-line": "Save the line", + "toggle-anchoring": "Toggle anchoring on/off" }, "help": { "add-anchor-point": "Click to add a point at the end of the track section. Click on the track to add an intermediate point.", diff --git a/front/public/locales/fr/translation.json b/front/public/locales/fr/translation.json index 0821f91765c..ab6883b34e9 100644 --- a/front/public/locales/fr/translation.json +++ b/front/public/locales/fr/translation.json @@ -450,8 +450,9 @@ "mode-add-point": "Ajouter un point", "mode-delete-point": "Supprimer des points", "mode-move-point": "Déplacer les points", - "toggle-anchoring": "Activer / désactiver l'ancrage automatique", - "save-line": "Sauvegarder la ligne" + "reset-line": "Réinitialiser les données", + "save-line": "Sauvegarder la ligne", + "toggle-anchoring": "Activer / désactiver l'ancrage automatique" }, "help": { "add-anchor-point": "Cliquez pour ajouter un point au bout de la section de ligne. Cliquez sur la ligne pour ajouter un point intermédiaire.", diff --git a/front/src/applications/editor/components/LinearMetadata/FormComponent.tsx b/front/src/applications/editor/components/LinearMetadata/FormComponent.tsx index 1c07dc17b77..32b4f855043 100644 --- a/front/src/applications/editor/components/LinearMetadata/FormComponent.tsx +++ b/front/src/applications/editor/components/LinearMetadata/FormComponent.tsx @@ -30,11 +30,10 @@ import { LinearMetadataTooltip } from './tooltip'; import { FormBeginEndWidget } from './FormBeginEndWidget'; import 'common/IntervalsDataViz/style.scss'; -export const FormComponent: React.FC = (props) => { +const IntervalEditorComponent: React.FC = (props) => { const { name, formContext, formData, schema, onChange, registry } = props; const { openModal, closeModal } = useModal(); const { t } = useTranslation(); - const Fields = utils.getDefaultRegistry().fields; // Wich segment area is visible const [viewBox, setViewBox] = useState<[number, number] | null>(null); @@ -145,9 +144,6 @@ export const FormComponent: React.FC = (props) => { setSelectedData(selected !== null && data[selected] ? data[selected] : null); }, [selected, data]); - if (!LINEAR_METADATA_FIELDS.includes(name)) - return ; - return (
@@ -427,4 +423,60 @@ export const FormComponent: React.FC = (props) => { ); }; +export const FormComponent: React.FC = (props) => { + const { name, formContext, schema, registry } = props; + const Fields = utils.getDefaultRegistry().fields; + + // Get the distance of the geometry + const distance = useMemo(() => { + if (!isNil(formContext.length)) { + return formContext.length as number; + } + if (formContext.geometry?.type === 'LineString') { + return getLineStringDistance(formContext.geometry); + } + return 0; + }, [formContext]); + + // Remove the 'valueField' required field because it is required by the backend. However, + // the segment with missing values is filtered in 'customOnChange' before being sent to the backend, + // and then re-added by 'fixLinearMetadataItems'. + const requiredFilter = (requireds: string[]) => + requireds.filter((r) => ['end', 'begin'].includes(r)); + + // Compute the JSON schema of the linear metadata item + const jsonSchema = useMemo( + () => + getFieldJsonSchema( + schema, + registry.rootSchema, + requiredFilter, + distance + ? { + begin: { + minimum: 0, + maximum: distance, + }, + end: { + minimum: 0, + maximum: distance, + }, + } + : {} + ), + [schema, registry.rootSchema, distance] + ); + + if (LINEAR_METADATA_FIELDS.includes(name)) + return ( + + ); + return ; +}; + export default FormComponent; diff --git a/front/src/applications/editor/tools/trackEdition/components.tsx b/front/src/applications/editor/tools/trackEdition/components.tsx index a42e7dfdcae..98ce58a48aa 100644 --- a/front/src/applications/editor/tools/trackEdition/components.tsx +++ b/front/src/applications/editor/tools/trackEdition/components.tsx @@ -26,9 +26,10 @@ import { save } from 'reducers/editor'; import { getMap } from 'reducers/map/selectors'; import { getInfraID } from 'reducers/osrdconf/selectors'; import { CatenaryEntity, SpeedSectionEntity, TrackSectionEntity } from 'types'; - +import DebouncedNumberInputSNCF from 'common/BootstrapSNCF/FormSNCF/DebouncedNumberInputSNCF'; +import { WidgetProps } from '@rjsf/core'; import { TrackEditionState } from './types'; -import { injectGeometry } from './utils'; +import { injectGeometry, removeInvalidRanges } from './utils'; export const TRACK_LAYER_ID = 'trackEditionTool/new-track-path'; export const POINTS_LAYER_ID = 'trackEditionTool/new-track-points'; @@ -346,6 +347,14 @@ export const TrackEditionLayers: FC = () => { ); }; +export const CustomLengthInput: React.FC = (props) => { + const { onChange, value } = props; + + return ( + + ); +}; + export const TrackEditionLeftPanel: FC = () => { const dispatch = useDispatch(); const { t } = useTranslation(); @@ -354,7 +363,7 @@ export const TrackEditionLeftPanel: FC = () => { EditorContext ) as ExtendedEditorContextType; const submitBtnRef = useRef(null); - const { track } = state; + const { track, initialTrack } = state; const isNew = track.properties.id === NEW_ENTITY_ID; // Hack to be able to launch the submit event from the rjsf form by using @@ -371,6 +380,11 @@ export const TrackEditionLeftPanel: FC = () => { <> { // eslint-disable-next-line @typescript-eslint/no-explicit-any const res: any = await dispatch( @@ -403,7 +417,26 @@ export const TrackEditionLeftPanel: FC = () => { }); }} onChange={(newTrack) => { - setState({ ...state, track: newTrack as TrackSectionEntity }); + let checkedTrack = { ...newTrack }; + if (initialTrack.properties.length !== newTrack.properties.length) { + const { loading_gauge_limits, slopes, curves, length: newLength } = newTrack.properties; + const validLoadingGaugeLimits = removeInvalidRanges(loading_gauge_limits, newLength); + const validCurves = removeInvalidRanges(curves, newLength); + const validSlopes = removeInvalidRanges(slopes, newLength); + checkedTrack = { + ...checkedTrack, + properties: { + ...checkedTrack.properties, + loading_gauge_limits: validLoadingGaugeLimits, + slopes: validSlopes, + curves: validCurves, + }, + }; + } + setState({ + ...state, + track: checkedTrack as TrackSectionEntity, + }); }} >
diff --git a/front/src/applications/editor/tools/trackEdition/tool.tsx b/front/src/applications/editor/tools/trackEdition/tool.tsx index d9e59a9e817..d20e9ac1d20 100644 --- a/front/src/applications/editor/tools/trackEdition/tool.tsx +++ b/front/src/applications/editor/tools/trackEdition/tool.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { cloneDeep, isEmpty, isEqual } from 'lodash'; import { MdShowChart } from 'react-icons/md'; import { RiDragMoveLine } from 'react-icons/ri'; -import { BiAnchor, BiArrowFromLeft, BiArrowToRight } from 'react-icons/bi'; +import { BiAnchor, BiArrowFromLeft, BiArrowToRight, BiReset } from 'react-icons/bi'; import { Feature, LineString } from 'geojson'; import getNearestPoint from '@turf/nearest-point'; import { featureCollection } from '@turf/helpers'; @@ -65,6 +65,19 @@ const TrackEditionTool: Tool = { } }, }, + { + id: 'reset-entity', + icon: BiReset, + labelTranslationKey: `Editor.tools.track-edition.actions.reset-line`, + isDisabled({ state: { track, initialTrack } }) { + return isEqual(track, initialTrack); + }, + onClick({ setState, state: { initialTrack } }) { + setState({ + track: cloneDeep(initialTrack), + }); + }, + }, ], [ { @@ -219,7 +232,6 @@ const TrackEditionTool: Tool = { const position = nearestPoint.geometry.coordinates as [number, number]; const index = nearestPoint.properties.sectionIndex; const newState = cloneDeep(state); - newState.track.geometry.coordinates.splice(index + 1, 0, position); newState.track = entityDoUpdate(newState.track, state.track.geometry); newState.nearestPoint = null; diff --git a/front/src/applications/editor/tools/trackEdition/utils.ts b/front/src/applications/editor/tools/trackEdition/utils.ts index 5f9135050c0..5fd93d20dc5 100644 --- a/front/src/applications/editor/tools/trackEdition/utils.ts +++ b/front/src/applications/editor/tools/trackEdition/utils.ts @@ -1,7 +1,7 @@ import { EditorEntity, TrackSectionEntity } from 'types'; +import { LinearMetadataItem } from 'common/IntervalsDataViz/types'; import { NEW_ENTITY_ID } from '../../data/utils'; -// eslint-disable-next-line import/prefer-default-export export function getNewLine(points: [number, number][]): TrackSectionEntity { return { type: 'Feature', @@ -15,6 +15,7 @@ export function getNewLine(points: [number, number][]): TrackSectionEntity { length: 0, slopes: [], curves: [], + loading_gauge_limits: [], }, }; } @@ -29,3 +30,15 @@ export function injectGeometry(track: EditorEntity): EditorEntity { }, }; } + +/** + * Remove the invalid ranges when the length of the track section has been modified + * - keep ranges if begin is undefined in case we just added a new one or if we deleted the begin input value + * - remove ranges which start after the new end + * - cut the ranges which start before the new end but end after it + */ +export function removeInvalidRanges(values: LinearMetadataItem[], newLength: number) { + return values + .filter((item) => item.begin < newLength || item.begin === undefined) + .map((item) => (item.end >= newLength ? { ...item, end: newLength } : item)); +} diff --git a/front/src/common/BootstrapSNCF/FormSNCF/DebouncedNumberInputSNCF.tsx b/front/src/common/BootstrapSNCF/FormSNCF/DebouncedNumberInputSNCF.tsx index f9644977a92..386edd950cb 100644 --- a/front/src/common/BootstrapSNCF/FormSNCF/DebouncedNumberInputSNCF.tsx +++ b/front/src/common/BootstrapSNCF/FormSNCF/DebouncedNumberInputSNCF.tsx @@ -10,6 +10,8 @@ type DebouncedNumberInputSNCFProps = { id?: string; max?: number; min?: number; + sm?: boolean; + showFlex?: boolean; }; const DebouncedNumberInputSNCF = ({ @@ -18,8 +20,10 @@ const DebouncedNumberInputSNCF = ({ setInput, debouncedDelay = 800, id = '', - max = 100, + max, min = 0, + sm = false, + showFlex = false, }: DebouncedNumberInputSNCFProps) => { const [value, setValue] = useState(input); @@ -28,19 +32,27 @@ const DebouncedNumberInputSNCF = ({ }, [input]); const checkChangedInput = (newValue: number | null) => { - if (newValue !== null && newValue !== input && min <= newValue && newValue <= max) + if ( + newValue !== null && + newValue !== input && + min <= newValue && + (max === undefined || newValue <= max) + ) { setInput(newValue); - else if (value === null && input !== 0) setInput(0); + } else if (value === null && input !== 0) { + const previousValue = input; + setInput(previousValue); + } }; useDebouncedFunc(value, debouncedDelay, checkChangedInput); return ( -
+
); diff --git a/front/src/common/IntervalsDataViz/data.test.ts b/front/src/common/IntervalsDataViz/data.test.ts index 91a6c6b1ce8..4a94fe60bf0 100644 --- a/front/src/common/IntervalsDataViz/data.test.ts +++ b/front/src/common/IntervalsDataViz/data.test.ts @@ -79,7 +79,9 @@ function checkWrapperValidity( expect(result[0].begin).toEqual(0); // we round due to some approximation that result to a diff (below millimeter) if (newLine) - expect(Math.round(last(result)?.end || 0)).toEqual(Math.round(getLineStringDistance(newLine))); + expect(Math.round(last(result)?.end || 0)).not.toEqual( + Math.round(getLineStringDistance(newLine)) + ); // Checking the continuity tail(result).forEach((value, index) => { expect(value.begin <= value.end).toEqual(true); diff --git a/front/src/common/IntervalsDataViz/data.ts b/front/src/common/IntervalsDataViz/data.ts index af6a43719f1..ee67b0823f6 100644 --- a/front/src/common/IntervalsDataViz/data.ts +++ b/front/src/common/IntervalsDataViz/data.ts @@ -5,6 +5,7 @@ import { utils } from '@rjsf/core'; import lineSplit from '@turf/line-split'; import fnLength from '@turf/length'; import { EditorEntity } from 'types/editor'; +import { removeInvalidRanges } from 'applications/editor/tools/trackEdition/utils'; import { LinearMetadataItem, OperationalPoint } from './types'; export const LINEAR_METADATA_FIELDS = ['slopes', 'curves']; @@ -219,17 +220,17 @@ export function mergeIn( * - if empty it generate one * - if there is a gap at begin/end or inside, it is created * - if there is an overlaps, remove it - * @param value The linear metadata + * @param items The linear metadata * @param lineLength The full length of the linearmetadata (should be computed from the LineString or given by the user) * @param opts If defined, it allows the function to fill gaps with default field value */ export function fixLinearMetadataItems( - value: Array> | undefined, + items: Array> | undefined, lineLength: number, opts?: { fieldName: string; defaultValue: unknown } ): Array> { // simple scenario - if (!value || value.length === 0) { + if (!items || items.length === 0) { return [ { begin: 0, @@ -238,6 +239,7 @@ export function fixLinearMetadataItems( } as LinearMetadataItem, ]; } + const filteredItems = removeInvalidRanges(items, lineLength); function haveAdditionalKeys(item: LinearMetadataItem, itemToCompare: LinearMetadataItem) { const keys = Object.keys(item); @@ -249,7 +251,7 @@ export function fixLinearMetadataItems( } // merge empty adjacent items - let fixedLinearMetadata: Array> = sortBy(value, ['begin']); + let fixedLinearMetadata: Array> = sortBy(filteredItems, ['begin']); // Order the array and fix it by filling gaps if there are some fixedLinearMetadata = fixedLinearMetadata.flatMap((item, index, array) => { @@ -362,7 +364,6 @@ export function update( linearMetadata: Array> ): Array> { if (linearMetadata.length === 0) return []; - // Compute the source coordinates of the changed point // by doing // - a diff between source & target for change @@ -660,9 +661,12 @@ export function getClosestOperationalPoint( * @param sourceLine The original LineString (before the change) * @returns The entity modified in adquation */ + export function entityDoUpdate(entity: T, sourceLine: LineString): T { - if (entity.geometry.type === 'LineString' && !isNil(entity.properties)) { - const newProps: EditorEntity['properties'] = { id: entity.properties.id }; + const newProps: EditorEntity['properties'] = { id: entity.properties.id }; + // The modification of the linestring modifies the entity real properties only during initialization. + const isInitialization = sourceLine.coordinates.length === 0; + if (entity.geometry.type === 'LineString' && !isNil(entity.properties) && isInitialization) { Object.keys(entity.properties).forEach((name) => { const value = (entity.properties as { [key: string]: unknown })[name]; // is a LM ? @@ -672,9 +676,7 @@ export function entityDoUpdate(entity: T, sourceLine: Li newProps[name] = value; } }); - // eslint-disable-next-line dot-notation - newProps['length'] = getLineStringDistance(entity.geometry as LineString); - + newProps.length = getLineStringDistance(entity.geometry as LineString); return { ...entity, properties: newProps }; } return entity; diff --git a/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx b/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx index 097f9a420f8..751d76d078f 100644 --- a/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx +++ b/front/src/common/IntervalsEditor/IntervalsEditorCommonForm.tsx @@ -75,6 +75,8 @@ const IntervalsEditorCommonForm = ({ label={t('begin')} setInput={setBegin} max={interval.end} + sm + showFlex />
); diff --git a/front/src/types/editor.ts b/front/src/types/editor.ts index 2c8a8f6b479..49b3721ce9a 100644 --- a/front/src/types/editor.ts +++ b/front/src/types/editor.ts @@ -1,6 +1,11 @@ import { JSONSchema7 } from 'json-schema'; import { Feature, GeoJsonProperties, Geometry, Point, LineString, MultiLineString } from 'geojson'; -import { Direction, DirectionalTrackRange, ObjectType } from 'common/api/osrdEditoastApi'; +import { + Direction, + DirectionalTrackRange, + LoadingGaugeType, + ObjectType, +} from 'common/api/osrdEditoastApi'; import { LinearMetadataItem } from 'common/IntervalsDataViz/types'; import { NullGeometry } from './geospatial'; @@ -24,6 +29,7 @@ export interface TrackSectionEntity { length: number; slopes: LinearMetadataItem<{ gradient: number }>[]; + loading_gauge_limits: LinearMetadataItem<{ category: LoadingGaugeType }>[]; curves: LinearMetadataItem<{ radius: number }>[]; extensions?: { sncf?: {