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

Editor: fixing some issues on creation/update operation #2547

Merged
merged 4 commits into from
Dec 9, 2022
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
69 changes: 42 additions & 27 deletions front/src/applications/editor/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import maplibregl from 'maplibre-gl';
import ReactMapGL, { AttributionControl, ScaleControl } from 'react-map-gl';
import { withTranslation } from 'react-i18next';
import { TFunction } from 'i18next';

import VirtualLayers from 'applications/osrd/views/OSRDSimulation/VirtualLayers';
import colors from 'common/Map/Consts/colors';
import 'common/Map/Map.scss';
Expand All @@ -17,6 +16,7 @@ import Platforms from '../../common/Map/Layers/Platforms';
import osmBlankStyle from '../../common/Map/Layers/osmBlankStyle';
import OrthoPhoto from '../../common/Map/Layers/OrthoPhoto';
import { Viewport } from '../../reducers/map';
import { getMapMouseEventNearestFeature } from '../../utils/mapboxHelper';
import EditorContext from './context';
import {
CommonToolState,
Expand Down Expand Up @@ -99,26 +99,39 @@ const MapUnplugged: FC<PropsWithChildren<MapProps>> = ({
onMove={(e) => setViewport(e.viewState)}
onMoveStart={() => setMapState((prev) => ({ ...prev, isDragging: true }))}
onMoveEnd={() => setMapState((prev) => ({ ...prev, isDragging: false }))}
onMouseEnter={(e) => {
setMapState((prev) => ({ ...prev, isHovering: true }));
const feature = (e.features || [])[0];
if (activeTool.onHover) {
activeTool.onHover(e, extendedContext);
} else if (feature && feature.properties) {
const entity = feature?.properties?.id
? editorState.entitiesIndex[feature.properties.id]
: undefined;
setToolState({
...toolState,
hovered: entity || null,
});
onMouseMove={(e) => {
const nearestResult = getMapMouseEventNearestFeature(e);
const partialToolState: Partial<CommonToolState> = {
hovered: null,
mousePosition: [e.lngLat.lng, e.lngLat.lat],
};
const partialMapState: Partial<MapState> = { isHovering: false };

// if we hover something
if (nearestResult) {
const { feature } = nearestResult;
const eventWithFeature = {
...e,
preventDefault: e.preventDefault,
features: [feature],
};
partialMapState.isHovering = true;
if (activeTool.onHover) {
activeTool.onHover(eventWithFeature, extendedContext);
} else if (feature && feature.properties) {
const entity = feature?.properties?.id
? editorState.entitiesIndex[feature.properties.id]
: undefined;
partialToolState.hovered = entity || null;
}
} else {
setToolState({ ...toolState, hovered: null });
if (activeTool.onMove) {
activeTool.onMove(e, extendedContext);
}
}
}}
onMouseLeave={() => {
setMapState((prev) => ({ ...prev, isHovering: false }));
setToolState({ ...toolState, mousePosition: null, hovered: null });

setToolState({ ...toolState, ...partialToolState });
setMapState((prev) => ({ ...prev, ...partialMapState }));
}}
onLoad={(e) => {
// need to call resize, otherwise sometime the canvas doesn't take 100%
Expand All @@ -140,17 +153,19 @@ const MapUnplugged: FC<PropsWithChildren<MapProps>> = ({
}
cursor={cursor}
onClick={(e) => {
const nearestResult = getMapMouseEventNearestFeature(e);
const eventWithFeature = nearestResult
? {
...e,
preventDefault: e.preventDefault,
features: [nearestResult.feature],
}
: e;
if (toolState.hovered && activeTool.onClickFeature) {
activeTool.onClickFeature(toolState.hovered, e, extendedContext);
activeTool.onClickFeature(toolState.hovered, eventWithFeature, extendedContext);
}
if (activeTool.onClickMap) {
activeTool.onClickMap(e, extendedContext);
}
}}
onMouseMove={(e) => {
setToolState({ ...toolState, mousePosition: [e.lngLat.lng, e.lngLat.lat] });
if (activeTool.onMove) {
activeTool.onMove(e, extendedContext);
activeTool.onClickMap(eventWithFeature, extendedContext);
}
}}
>
Expand Down
16 changes: 10 additions & 6 deletions front/src/applications/editor/components/EditorForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import Form, { Field, UiSchema } from '@rjsf/core';
import { useSelector } from 'react-redux';
import { GeoJsonProperties } from 'geojson';
import { JSONSchema7 } from 'json-schema';
import { isNil, omitBy } from 'lodash';

import './EditorForm.scss';
import { EditorEntity } from '../../../types';
import { FormComponent, FormLineStringLength } from './LinearMetadata';
import { getJsonSchemaForLayer, getLayerForObjectType } from '../data/utils';
import { getJsonSchemaForLayer, getLayerForObjectType, NEW_ENTITY_ID } from '../data/utils';
import { EditorState } from '../tools/types';

const fields = {
Expand Down Expand Up @@ -57,7 +58,7 @@ const EditorForm: React.FC<PropsWithChildren<EditorFormProps>> = ({
* => recompute formData by fixing LM
*/
useEffect(() => {
setFormData(data.properties);
setFormData(omitBy(data.properties, isNil));
}, [data, schema]);

/**
Expand Down Expand Up @@ -91,13 +92,16 @@ const EditorForm: React.FC<PropsWithChildren<EditorFormProps>> = ({
...(overrideUiSchema || {}),
}}
formData={formData}
formContext={{ geometry: data.geometry, length: data.properties?.length }}
formContext={{
geometry: data.geometry,
length: data.properties?.length,
isCreation: isNil(formData?.id) || formData?.id === NEW_ENTITY_ID,
}}
onError={() => setSubmited(true)}
onSubmit={async (event) => {
onSubmit={async () => {
try {
setError(null);
setFormData(event.formData);
await onSubmit({ ...data, properties: { ...data.properties, ...event.formData } });
await onSubmit({ ...data, properties: { ...data.properties, ...formData } });
} catch (e) {
if (e instanceof Error) setError(e.message);
else setError(JSON.stringify(e));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { WidgetProps } from '@rjsf/core';
import { useTranslation } from 'react-i18next';

Expand All @@ -8,30 +8,29 @@ export const FormLineStringLength: React.FC<WidgetProps> = (props) => {
const { t } = useTranslation();
const { id, value, required, readonly, onChange, formContext } = props;

const [length, setLength] = useState<number>(value);
const [length, setLength] = useState<number>(value || 0);
const [min, setMin] = useState<number>(-Infinity);
const [max, setMax] = useState<number>(Infinity);
const [geoLength, setGeoLength] = useState<number>(0);

useEffect(() => {
setLength(value || 0);
}, [value]);

/**
* When the geometry changes
* => recompute min & max plus its length
*/
useEffect(() => {
const distance = getLineStringDistance(formContext.geometry);
setMin(Math.round(distance - distance * DISTANCE_ERROR_RANGE));
setMax(Math.round(distance + distance * DISTANCE_ERROR_RANGE));
setGeoLength(distance);
}, [formContext.geometry]);

/**
* When the input value change
* => if it is valid, we call the onChange
*/
useEffect(() => {
if (value !== undefined) setLength(value);
else setLength(geoLength);
}, [value, geoLength]);
if (formContext.isCreation) {
setTimeout(() => onChange(distance), 0);
} else {
setMin(Math.round(distance - distance * DISTANCE_ERROR_RANGE));
setMax(Math.round(distance + distance * DISTANCE_ERROR_RANGE));
setGeoLength(distance);
}
}, [formContext.geometry, formContext.isCreation, onChange]);

return (
<div>
Expand All @@ -43,19 +42,15 @@ export const FormLineStringLength: React.FC<WidgetProps> = (props) => {
id={id}
required={required}
type="number"
min={min}
max={max}
step="any"
value={length}
onChange={(e) => {
const nValue = parseFloat(e.target.value);
if (nValue >= min && nValue <= max) onChange(nValue);
else setLength(nValue);
onChange(nValue);
}}
/>
)}
{(length === undefined || length < min || length > max) && (
<p className="text-danger">
{geoLength && (length === undefined || length < min || length > max) && (
<p className="text-warning">
{t('Editor.errors.length-out-of-sync-with-geometry', { min, max })}.{' '}
<button
type="button"
Expand Down
2 changes: 1 addition & 1 deletion front/src/applications/editor/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export async function editorSave(
obj_type: feature.objType,
railjson: {
...feature.properties,
id: feature.properties.id || uuid(),
id: uuid(),
},
})
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { useTranslation } from 'react-i18next';
import { featureCollection } from '@turf/helpers';
import along from '@turf/along';
import { Feature, LineString } from 'geojson';
import { merge } from 'lodash';
import { merge, isEqual } from 'lodash';

import GeoJSONs, { EditorSource, SourcesDefinitionsIndex } from 'common/Map/Layers/GeoJSONs';
import colors from 'common/Map/Consts/colors';
import EditorZone from 'common/Map/Layers/EditorZone';
import { save } from 'reducers/editor';
import { CreateEntityOperation, EditorEntity } from 'types';
import { NULL_GEOMETRY, CreateEntityOperation, EditorEntity } from 'types';
import { SIGNALS_TO_SYMBOLS } from 'common/Map/Consts/SignalsNames';
import { PointEditionState } from './types';
import EditorForm from '../../components/EditorForm';
Expand Down Expand Up @@ -112,11 +112,12 @@ export const BasePointEditionLayers: FC<{
const { mapStyle } = useSelector((s: { map: { mapStyle: string } }) => s.map) as {
mapStyle: string;
};

const [showPopup, setShowPopup] = useState(true);

const renderedEntity = useMemo(() => {
let res: EditorEntity | null = null;
if (entity.geometry) {
if (entity.geometry && !isEqual(entity.geometry, NULL_GEOMETRY)) {
res = entity as EditorEntity;
} else if (nearestPoint) {
if (mergeEntityWithNearestPoint) {
Expand Down Expand Up @@ -216,7 +217,7 @@ export const PointEditionMessages: FC = () => {
PointEditionState<EditorEntity>
>;

if (!state.entity.geometry) {
if (!state.entity.geometry || isEqual(state.entity.geometry, NULL_GEOMETRY)) {
return state.nearestPoint
? t(`Editor.tools.point-edition.help.stop-dragging-on-line`)
: t(`Editor.tools.point-edition.help.stop-dragging-no-line`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { DEFAULT_COMMON_TOOL_STATE, LayerType, Tool } from '../types';
import { getNearestPoint } from '../../../../utils/mapboxHelper';
import { POINT_LAYER_ID, PointEditionLeftPanel, PointEditionMessages } from './components';
import { PointEditionState } from './types';
import { BufferStopEntity, DetectorEntity, SignalEntity } from '../../../../types';
import { NULL_GEOMETRY, BufferStopEntity, DetectorEntity, SignalEntity } from '../../../../types';

type EditorPoint = BufferStopEntity | DetectorEntity | SignalEntity;
interface PointEditionToolParams<T extends EditorPoint> {
Expand Down Expand Up @@ -83,16 +83,15 @@ function getPointEditionTool<T extends EditorPoint>({
// Interactions:
onClickMap(_e, { setState, state }) {
const { isHoveringTarget, entity, nearestPoint } = state;

if (entity.geometry && isHoveringTarget) {
if (entity.geometry && !isEqual(entity.geometry, NULL_GEOMETRY) && isHoveringTarget) {
setState({
...state,
isHoveringTarget: false,
entity: omit(entity, 'geometry') as T,
});
}

if (!entity.geometry && nearestPoint) {
if ((!entity.geometry || isEqual(entity.geometry, NULL_GEOMETRY)) && nearestPoint) {
const newEntity = cloneDeep(entity);
newEntity.geometry = {
type: 'Point',
Expand All @@ -115,15 +114,14 @@ function getPointEditionTool<T extends EditorPoint>({
},
onHover(e, { setState, state, editorState: { entitiesIndex } }) {
const { entity } = state;

const hoveredTarget = (e.features || []).find((f) => f.layer.id === POINT_LAYER_ID);
const hoveredTracks = (e.features || []).flatMap((f) => {
if (f.layer.id !== 'editor/geo/track-main') return [];
const trackEntity = entitiesIndex[(f.properties ?? {}).id];
return trackEntity && trackEntity.objType === 'TrackSection' ? [trackEntity] : [];
}) as Feature<LineString>[];

if (!entity.geometry) {
if (!entity.geometry || isEqual(entity.geometry, NULL_GEOMETRY)) {
if (hoveredTracks.length) {
const nearestPoint = getNearestPoint(hoveredTracks, e.lngLat.toArray());
const angle = nearestPoint.properties.angleAtPoint;
Expand All @@ -134,7 +132,7 @@ function getPointEditionTool<T extends EditorPoint>({
angle,
feature: nearestPoint,
position: nearestPoint.properties.location,
trackSectionID: hoveredTracks[nearestPoint.properties.featureIndex].id as string,
trackSectionID: hoveredTracks[nearestPoint.properties.featureIndex].properties?.id,
},
});
} else {
Expand Down Expand Up @@ -191,7 +189,8 @@ The entity ${entity.properties.id} position computed by Turf.js does not match t
return ['editor/geo/track-main', POINT_LAYER_ID];
},
getCursor({ state }, { isDragging }) {
if (isDragging || !state.entity.geometry) return 'move';
if (isDragging || !state.entity.geometry || isEqual(state.entity.geometry, NULL_GEOMETRY))
return 'move';
if (state.isHoveringTarget) return 'pointer';
return 'default';
},
Expand Down
1 change: 1 addition & 0 deletions front/src/types/mapbox-gl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from 'mapbox-gl';

export type {
MapboxGeoJSONFeature,
MapLayerMouseEvent,
AnyPaint,
CirclePaint,
Expand Down
31 changes: 18 additions & 13 deletions front/src/utils/mapboxHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import nearestPointOnLine from '@turf/nearest-point-on-line';
import nearestPoint, { NearestPoint } from '@turf/nearest-point';
import fnDistance from '@turf/distance';
import fnExplode from '@turf/explode';
import { Zone, MapLayerMouseEvent } from '../types';
import { Zone, MapLayerMouseEvent, MapboxGeoJSONFeature } from '../types';
import { getAngle } from '../applications/editor/data/utils';

/**
Expand Down Expand Up @@ -290,30 +290,35 @@ export function getNearestPoint(lines: Feature<LineString>[], coord: Coord): Nea
*/
export function getMapMouseEventNearestFeature(
e: MapLayerMouseEvent,
opts?: { layersId?: string[]; tolerance: number }
): { feature: Feature; nearest: number[]; distance: number } | null {
opts?: { layersId?: string[]; tolerance: number; excludeOsm: boolean }
): { feature: MapboxGeoJSONFeature; nearest: number[]; distance: number } | null {
const layers = opts?.layersId;
const tolerance = opts?.tolerance || 30;
const tolerance = opts?.tolerance || 15;
const excludeOsm = opts?.excludeOsm || true;
const { target: map, point } = e;
const coord = e.lngLat.toArray();

const features = map.queryRenderedFeatures(
[
[point.x - tolerance / 2, point.y - tolerance / 2],
[point.x + tolerance / 2, point.y + tolerance / 2],
],
{ layers }
);
const features = map
.queryRenderedFeatures(
[
[point.x - tolerance / 2, point.y - tolerance / 2],
[point.x + tolerance / 2, point.y + tolerance / 2],
],
{ layers }
)
.filter((f) => (excludeOsm ? !f.layer.id.startsWith('osm') : true));

const result = head(
sortBy(
features.map((feature: Feature) => {
features.map((feature) => {
let distance = Infinity;
let nearestFeaturePoint: Feature<Point> | null = null;
switch (feature.geometry.type) {
case 'Point': {
nearestFeaturePoint = feature as Feature<Point>;
distance = fnDistance(coord, nearestFeaturePoint.geometry.coordinates);
// we boost point, otherwise when a point is on line,
// it's too easy to find a point of line closest
distance = 0.7 * fnDistance(coord, nearestFeaturePoint.geometry.coordinates);
break;
}
case 'LineString': {
Expand Down