Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix track section length when editing geometry #6017

Merged
merged 1 commit into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions front/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
5 changes: 3 additions & 2 deletions front/public/locales/fr/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ import { LinearMetadataTooltip } from './tooltip';
import { FormBeginEndWidget } from './FormBeginEndWidget';
import 'common/IntervalsDataViz/style.scss';

export const FormComponent: React.FC<FieldProps> = (props) => {
const IntervalEditorComponent: React.FC<FieldProps> = (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);
Expand Down Expand Up @@ -145,9 +144,6 @@ export const FormComponent: React.FC<FieldProps> = (props) => {
setSelectedData(selected !== null && data[selected] ? data[selected] : null);
}, [selected, data]);

if (!LINEAR_METADATA_FIELDS.includes(name))
return <Fields.ArrayField {...props} schema={jsonSchema} />;

return (
<div className="linear-metadata">
<div className="header">
Expand Down Expand Up @@ -427,4 +423,60 @@ export const FormComponent: React.FC<FieldProps> = (props) => {
);
};

export const FormComponent: React.FC<FieldProps> = (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 (
<IntervalEditorComponent
jsonSchema={jsonSchema}
distance={distance}
requiredFilter={requiredFilter}
{...props}
/>
);
return <Fields.ArrayField {...props} schema={jsonSchema} />;
};

export default FormComponent;
41 changes: 37 additions & 4 deletions front/src/applications/editor/tools/trackEdition/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -346,6 +347,14 @@ export const TrackEditionLayers: FC = () => {
);
};

export const CustomLengthInput: React.FC<WidgetProps> = (props) => {
const { onChange, value } = props;

return (
<DebouncedNumberInputSNCF debouncedDelay={1500} input={value} setInput={onChange} label="" />
);
};

export const TrackEditionLeftPanel: FC = () => {
const dispatch = useDispatch();
const { t } = useTranslation();
Expand All @@ -354,7 +363,7 @@ export const TrackEditionLeftPanel: FC = () => {
EditorContext
) as ExtendedEditorContextType<TrackEditionState>;
const submitBtnRef = useRef<HTMLButtonElement>(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
Expand All @@ -371,6 +380,11 @@ export const TrackEditionLeftPanel: FC = () => {
<>
<EditorForm
data={track}
overrideUiSchema={{
length: {
'ui:widget': CustomLengthInput,
},
}}
onSubmit={async (savedEntity) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const res: any = await dispatch(
Expand Down Expand Up @@ -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,
});
}}
>
<div>
Expand Down
16 changes: 14 additions & 2 deletions front/src/applications/editor/tools/trackEdition/tool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -65,6 +65,19 @@ const TrackEditionTool: Tool<TrackEditionState> = {
}
},
},
{
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),
});
},
},
],
[
{
Expand Down Expand Up @@ -219,7 +232,6 @@ const TrackEditionTool: Tool<TrackEditionState> = {
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;
Expand Down
15 changes: 14 additions & 1 deletion front/src/applications/editor/tools/trackEdition/utils.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -15,6 +15,7 @@ export function getNewLine(points: [number, number][]): TrackSectionEntity {
length: 0,
slopes: [],
curves: [],
loading_gauge_limits: [],
},
};
}
Expand All @@ -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<T>(values: LinearMetadataItem<T>[], newLength: number) {
return values
.filter((item) => item.begin < newLength || item.begin === undefined)
.map((item) => (item.end >= newLength ? { ...item, end: newLength } : item));
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ type DebouncedNumberInputSNCFProps = {
id?: string;
max?: number;
min?: number;
sm?: boolean;
showFlex?: boolean;
};

const DebouncedNumberInputSNCF = ({
Expand All @@ -18,8 +20,10 @@ const DebouncedNumberInputSNCF = ({
setInput,
debouncedDelay = 800,
id = '',
max = 100,
max,
min = 0,
sm = false,
showFlex = false,
}: DebouncedNumberInputSNCFProps) => {
const [value, setValue] = useState<number | null>(input);

Expand All @@ -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 (
<div className="debounced-number-input">
<div className={`${showFlex && 'debounced-number-input'}`}>
clarani marked this conversation as resolved.
Show resolved Hide resolved
<InputSNCF
type="number"
id={id}
isInvalid={value !== null && (value < min || max < value)}
isInvalid={value !== null && (value < min || (max !== undefined && max < value))}
label={label}
max={max}
min={min}
Expand All @@ -49,7 +61,7 @@ const DebouncedNumberInputSNCF = ({
setValue(e.target.value !== '' ? parseFloat(e.target.value) : null);
}}
value={value !== null ? value : ''}
sm
sm={sm}
/>
</div>
);
Expand Down
22 changes: 12 additions & 10 deletions front/src/common/IntervalsDataViz/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down Expand Up @@ -219,17 +220,17 @@ export function mergeIn<T>(
* - 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<T>(
value: Array<LinearMetadataItem<T>> | undefined,
items: Array<LinearMetadataItem<T>> | undefined,
lineLength: number,
opts?: { fieldName: string; defaultValue: unknown }
): Array<LinearMetadataItem<T>> {
// simple scenario
if (!value || value.length === 0) {
if (!items || items.length === 0) {
return [
{
begin: 0,
Expand All @@ -238,6 +239,7 @@ export function fixLinearMetadataItems<T>(
} as LinearMetadataItem<T>,
];
}
const filteredItems = removeInvalidRanges(items, lineLength);

function haveAdditionalKeys(item: LinearMetadataItem, itemToCompare: LinearMetadataItem) {
const keys = Object.keys(item);
Expand All @@ -249,7 +251,7 @@ export function fixLinearMetadataItems<T>(
}

// merge empty adjacent items
let fixedLinearMetadata: Array<LinearMetadataItem<T>> = sortBy(value, ['begin']);
let fixedLinearMetadata: Array<LinearMetadataItem<T>> = sortBy(filteredItems, ['begin']);

// Order the array and fix it by filling gaps if there are some
fixedLinearMetadata = fixedLinearMetadata.flatMap((item, index, array) => {
Expand Down Expand Up @@ -362,7 +364,6 @@ export function update<T>(
linearMetadata: Array<LinearMetadataItem<T>>
): Array<LinearMetadataItem<T>> {
if (linearMetadata.length === 0) return [];

// Compute the source coordinates of the changed point
// by doing
// - a diff between source & target for change
Expand Down Expand Up @@ -660,9 +661,12 @@ export function getClosestOperationalPoint(
* @param sourceLine The original LineString (before the change)
* @returns The entity modified in adquation
*/

export function entityDoUpdate<T extends EditorEntity>(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 ?
Expand All @@ -672,9 +676,7 @@ export function entityDoUpdate<T extends EditorEntity>(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;
Expand Down
Loading
Loading