From ad692318b3b18b7df7a6aca006507893fdf5ee5b Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Wed, 11 Sep 2024 09:31:28 +0200 Subject: [PATCH] Refactor & prevent duplicate map layer collection push in MeasurementLayer --- .../Measurement/layers/MeasurementLayer.tsx | 463 +++++++++--------- frontend/src/features/Measurement/slice.ts | 7 +- frontend/src/features/map/Map.tsx | 2 +- 3 files changed, 246 insertions(+), 226 deletions(-) diff --git a/frontend/src/features/Measurement/layers/MeasurementLayer.tsx b/frontend/src/features/Measurement/layers/MeasurementLayer.tsx index b7f7aaf79a..c10e4078b8 100644 --- a/frontend/src/features/Measurement/layers/MeasurementLayer.tsx +++ b/frontend/src/features/Measurement/layers/MeasurementLayer.tsx @@ -1,309 +1,326 @@ -import React, { useEffect, useRef, useState } from 'react' - -import { useDispatch, useSelector } from 'react-redux' -import VectorSource from 'ol/source/Vector' -import { MeasurementType, OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../../domain/entities/map/constants' -import Draw from 'ol/interaction/Draw' -import { unByKey } from 'ol/Observable' +import { useMainAppDispatch } from '@hooks/useMainAppDispatch' +import { useMainAppSelector } from '@hooks/useMainAppSelector' +import { assertNotNullish } from '@utils/assertNotNullish' +import { noop } from 'lodash' +import { getCenter } from 'ol/extent' +import Feature from 'ol/Feature' +import GeoJSON from 'ol/format/GeoJSON' +import Circle from 'ol/geom/Circle' import LineString from 'ol/geom/LineString' -import { getLength } from 'ol/sphere' -import { - removeMeasurementDrawed, - resetMeasurementTypeToAdd, - setCircleMeasurementInDrawing -} from '../slice' +import Polygon, { circular, fromCircle } from 'ol/geom/Polygon' +import Draw, { DrawEvent } from 'ol/interaction/Draw' import VectorLayer from 'ol/layer/Vector' -import Circle from 'ol/geom/Circle' -import { circular, fromCircle } from 'ol/geom/Polygon' -import Feature from 'ol/Feature' +import { unByKey } from 'ol/Observable' +import { transform } from 'ol/proj' import { METERS_PER_UNIT } from 'ol/proj/Units' -import GeoJSON from 'ol/format/GeoJSON' -import MeasurementOverlay from '../components/MeasurementOverlay' -import { getNauticalMilesFromMeters } from '../../../utils' +import VectorSource from 'ol/source/Vector' +import { getLength } from 'ol/sphere' +import { memo, useCallback, useEffect, useRef, useState } from 'react' + +import { OPENLAYERS_PROJECTION, WSG84_PROJECTION } from '../../../domain/entities/map/constants' import saveMeasurement from '../../../domain/use_cases/measurement/saveMeasurement' +import { getNauticalMilesFromMeters } from '../../../utils' +import MeasurementOverlay from '../components/MeasurementOverlay' +import { removeMeasurementDrawed, resetMeasurementTypeToAdd, setCircleMeasurementInDrawing } from '../slice' import { measurementStyle, measurementStyleWithCenter } from './measurement.style' -import { transform } from 'ol/proj' -import { getCenter } from 'ol/extent' import { LayerProperties } from '../../../domain/entities/layers/constants' import { monitorfishMap } from '../../map/monitorfishMap' +import type { Coordinates } from '@mtes-mct/monitor-ui' +import type { Coordinate } from 'ol/coordinate' + const DRAW_START_EVENT = 'drawstart' const DRAW_ABORT_EVENT = 'drawabort' const DRAW_END_EVENT = 'drawend' -const getNauticalMilesRadiusOfCircle = circle => { +const getNauticalMilesRadiusOfCircle = (circle: Circle): string => { const polygon = fromCircle(circle) return getNauticalMilesRadiusOfCircularPolygon(polygon) } -const getNauticalMilesOfLine = line => { +const getNauticalMilesOfLine = (line: LineString): string => { const length = getLength(line) return `${getNauticalMilesFromMeters(length)} nm` } -function getNauticalMilesRadiusOfCircularPolygon (polygon) { +function getNauticalMilesRadiusOfCircularPolygon(polygon: Polygon): string { const length = getLength(polygon) const radius = length / (2 * Math.PI) return `r = ${getNauticalMilesFromMeters(radius)} nm` } -const MeasurementLayer = () => { - const dispatch = useDispatch() +type MeasurementInProgress = { + center?: Coordinate + coordinates: null + feature?: null + measurement: number | string | null +} - const { - measurementTypeToAdd, - measurementsDrawed, - circleMeasurementToAdd - } = useSelector(state => state.measurement) +function UnmemoizedMeasurementLayer() { + const vectorSourceRef = useRef( + new VectorSource({ + // TODO Fix TS error `'projection' does not exist in type 'Options>'`. + // @ts-ignore + projection: OPENLAYERS_PROJECTION, + wrapX: false + }) + ) + const vectorLayerRef = useRef( + new VectorLayer({ + className: LayerProperties.MEASUREMENT.code, + renderBuffer: 7, + source: vectorSourceRef.current, + style: [measurementStyle, measurementStyleWithCenter], + updateWhileAnimating: true, + updateWhileInteracting: true, + zIndex: LayerProperties.MEASUREMENT.zIndex + }) + ) + + const dispatch = useMainAppDispatch() + const { circleMeasurementToAdd, measurementsDrawed, measurementTypeToAdd } = useMainAppSelector( + state => state.measurement + ) - const [measurementInProgress, _setMeasurementInProgress] = useState(null) + const [measurementInProgress, setMeasurementInProgress] = useState(null) const measurementInProgressRef = useRef(measurementInProgress) - const setMeasurementInProgress = value => { - measurementInProgressRef.current = value - _setMeasurementInProgress(value) + const setMeasurementInProgressWithRef = (nextMeasurementInProgress: MeasurementInProgress | null) => { + measurementInProgressRef.current = nextMeasurementInProgress + setMeasurementInProgress(nextMeasurementInProgress) } - const [drawObject, setDrawObject] = useState(null) - const [vectorSource] = useState(new VectorSource({ - wrapX: false, - projection: OPENLAYERS_PROJECTION - })) - const [vectorLayer] = useState(new VectorLayer({ - source: vectorSource, - renderBuffer: 7, - updateWhileAnimating: true, - updateWhileInteracting: true, - style: [measurementStyle, measurementStyleWithCenter], - className: LayerProperties.MEASUREMENT.code, - zIndex: LayerProperties.MEASUREMENT.zIndex - })) + const [drawObject, setDrawObject] = useState(null) - useEffect(() => { - function addLayerToMap () { - if (vectorLayer) { - monitorfishMap.getLayers().push(vectorLayer) - } + const addCustomCircleMeasurement = useCallback( + (nextCircleMeasurementToAdd: { circleCoordinatesToAdd: Coordinates; circleRadiusToAdd: number }) => { + const metersForOneNauticalMile = 1852 + const longitude = 1 + const latitude = 0 + const numberOfVertices = 64 - return () => { - monitorfishMap.removeLayer(vectorLayer) + if ( + !circleMeasurementHasCoordinatesAndRadiusFromForm() && + !circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw() + ) { + return } - } - addLayerToMap() - }, [vectorLayer]) + function circleMeasurementHasCoordinatesAndRadiusFromForm() { + return ( + nextCircleMeasurementToAdd.circleCoordinatesToAdd?.length === 2 && + nextCircleMeasurementToAdd.circleRadiusToAdd + ) + } - useEffect(() => { - function drawExistingFeaturesOnMap () { - if (measurementsDrawed) { - measurementsDrawed.forEach(measurement => { - const feature = new GeoJSON({ - featureProjection: OPENLAYERS_PROJECTION - }).readFeature(measurement.feature) - - vectorSource.addFeature(feature) - }) + function circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw() { + return nextCircleMeasurementToAdd.circleRadiusToAdd && measurementInProgress?.center?.length === 2 } - } - drawExistingFeaturesOnMap() - }, [measurementsDrawed]) + assertNotNullish(nextCircleMeasurementToAdd.circleRadiusToAdd) - useEffect(() => { - if (measurementTypeToAdd) { - function addEmptyNextMeasurement () { - setMeasurementInProgress({ - feature: null, - measurement: null, - coordinates: null - }) + const radiusInMeters = METERS_PER_UNIT.m * nextCircleMeasurementToAdd.circleRadiusToAdd * metersForOneNauticalMile + let coordinates: Coordinate = [] + if (circleMeasurementHasCoordinatesAndRadiusFromForm()) { + coordinates = [ + nextCircleMeasurementToAdd.circleCoordinatesToAdd[longitude], + nextCircleMeasurementToAdd.circleCoordinatesToAdd[latitude] + ] + } else if (circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw()) { + assertNotNullish(measurementInProgress?.center) + + coordinates = transform(measurementInProgress.center, OPENLAYERS_PROJECTION, WSG84_PROJECTION) } - function drawNewFeatureOnMap () { - const draw = new Draw({ - source: vectorSource, - type: measurementTypeToAdd, - style: [measurementStyle, measurementStyleWithCenter] - }) + const circleFeature = new Feature({ + geometry: circular(coordinates, radiusInMeters, numberOfVertices).transform( + WSG84_PROJECTION, + OPENLAYERS_PROJECTION + ), + style: [measurementStyle, measurementStyleWithCenter] + }) + dispatch(saveMeasurement(circleFeature, `r = ${nextCircleMeasurementToAdd.circleRadiusToAdd} nm`)) + }, + [dispatch, measurementInProgress?.center] + ) - monitorfishMap.addInteraction(draw) - setDrawObject(draw) + const deleteFeature = useCallback( + (featureId: string) => { + const feature = vectorSourceRef.current.getFeatureById(featureId) + if (feature) { + vectorSourceRef.current.removeFeature(feature) + vectorSourceRef.current.changed() } - addEmptyNextMeasurement() - drawNewFeatureOnMap() - } - }, [measurementTypeToAdd]) + dispatch(removeMeasurementDrawed(featureId)) + }, + [dispatch] + ) - useEffect(() => { - function removeInteraction () { - if (!measurementTypeToAdd && drawObject) { - setDrawObject(null) - setMeasurementInProgress(null) + const startDrawing = useCallback((event: DrawEvent) => { + // TODO Fix TS error `Property 'coordinate' does not exist on type 'DrawEvent'`. + // @ts-ignore + let firstTooltipCoordinates = event.coordinate + const geometry = event.feature.getGeometry() + assertNotNullish(geometry) + + setMeasurementInProgressWithRef({ + center: getCenter(geometry.getExtent()), + // TODO Fix TS error `Property 'getLastCoordinate' does not exist on type 'Geometry'`. + // @ts-ignore + coordinates: geometry.getLastCoordinate(), + measurement: 0 + }) - waitForUnwantedZoomAndQuitInteraction() - } - } + return geometry.on('change', changeEvent => { + const geom = changeEvent.target - function waitForUnwantedZoomAndQuitInteraction () { - setTimeout(() => { - monitorfishMap.removeInteraction(drawObject) - }, 300) - } + if (geom instanceof LineString) { + const nextMeasurementOutput = getNauticalMilesOfLine(geom) + firstTooltipCoordinates = geom.getLastCoordinate() - removeInteraction() - }, [measurementTypeToAdd]) + setMeasurementInProgressWithRef({ + coordinates: firstTooltipCoordinates, + measurement: nextMeasurementOutput + }) + } else if (geom instanceof Circle) { + const nextMeasurementOutput = getNauticalMilesRadiusOfCircle(geom) + firstTooltipCoordinates = geom.getLastCoordinate() + + setMeasurementInProgressWithRef({ + center: getCenter(geom.getExtent()), + coordinates: firstTooltipCoordinates, + measurement: nextMeasurementOutput + }) + } + }) + }, []) useEffect(() => { - function addCustomCircleMeasurement () { - const metersForOneNauticalMile = 1852 - const longitude = 1 - const latitude = 0 - const numberOfVertices = 64 - - if (!circleMeasurementHasCoordinatesAndRadiusFromForm() && !circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw()) { - return - } + const vectorLayer = vectorLayerRef.current - function circleMeasurementHasCoordinatesAndRadiusFromForm () { - return circleMeasurementToAdd?.circleCoordinatesToAdd?.length === 2 && circleMeasurementToAdd?.circleRadiusToAdd - } + monitorfishMap.getLayers().push(vectorLayer) - function circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw () { - return circleMeasurementToAdd?.circleRadiusToAdd && measurementInProgress?.center?.length === 2 - } + return () => { + monitorfishMap.removeLayer(vectorLayer) + } + }, []) - const radiusInMeters = METERS_PER_UNIT.m * circleMeasurementToAdd.circleRadiusToAdd * metersForOneNauticalMile - let coordinates = [] - if (circleMeasurementHasCoordinatesAndRadiusFromForm()) { - coordinates = [circleMeasurementToAdd.circleCoordinatesToAdd[longitude], circleMeasurementToAdd.circleCoordinatesToAdd[latitude]] - } else if (circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw()) { - coordinates = transform(measurementInProgress?.center, OPENLAYERS_PROJECTION, WSG84_PROJECTION) - } + useEffect(() => { + if (measurementsDrawed) { + measurementsDrawed.forEach(measurement => { + const feature = new GeoJSON({ + featureProjection: OPENLAYERS_PROJECTION + }).readFeature(measurement.feature) - const circleFeature = new Feature({ - geometry: circular(coordinates, radiusInMeters, numberOfVertices).transform(WSG84_PROJECTION, OPENLAYERS_PROJECTION), - style: [measurementStyle, measurementStyleWithCenter] + vectorSourceRef.current.addFeature(feature) }) - dispatch(saveMeasurement(circleFeature, `r = ${circleMeasurementToAdd.circleRadiusToAdd} nm`)) } - - addCustomCircleMeasurement() - }, [circleMeasurementToAdd]) + }, [measurementsDrawed]) useEffect(() => { - function handleDrawEvents () { - if (drawObject) { - let listener + if (measurementTypeToAdd) { + setMeasurementInProgressWithRef({ + coordinates: null, + feature: null, + measurement: null + }) - drawObject.on(DRAW_START_EVENT, event => { - listener = startDrawing(event) - }) + const draw = new Draw({ + source: vectorSourceRef.current, + style: [measurementStyle, measurementStyleWithCenter], + type: measurementTypeToAdd + }) - drawObject.on(DRAW_ABORT_EVENT, () => { - unByKey(listener) - dispatch(resetMeasurementTypeToAdd()) - setMeasurementInProgress(null) - }) + monitorfishMap.addInteraction(draw) + setDrawObject(draw) + } + }, [measurementTypeToAdd]) - drawObject.on(DRAW_END_EVENT, event => { - dispatch(saveMeasurement(event.feature, measurementInProgressRef.current.measurement)) + useEffect(() => { + if (!measurementTypeToAdd && drawObject) { + setDrawObject(null) + setMeasurementInProgressWithRef(null) - unByKey(listener) - dispatch(resetMeasurementTypeToAdd()) - setMeasurementInProgress(null) - }) - } + setTimeout(() => { + monitorfishMap.removeInteraction(drawObject) + }, 300) } - - handleDrawEvents() - }, [drawObject]) + }, [drawObject, measurementTypeToAdd]) useEffect(() => { - if (measurementInProgress?.center || measurementInProgress?.measurement) { - dispatch(setCircleMeasurementInDrawing({ - measurement: measurementInProgress.measurement, - coordinates: measurementInProgress.center - })) + if (circleMeasurementToAdd) { + addCustomCircleMeasurement(circleMeasurementToAdd) } - }, [measurementInProgress]) + }, [addCustomCircleMeasurement, circleMeasurementToAdd]) - function deleteFeature (featureId) { - const feature = vectorSource.getFeatureById(featureId) - if (feature) { - vectorSource.removeFeature(feature) - vectorSource.changed() + useEffect(() => { + if (!drawObject) { + return } - dispatch(removeMeasurementDrawed(featureId)) - } + let listener - function startDrawing (event) { - const firstTooltipCoordinates = event.coordinate + drawObject.on(DRAW_START_EVENT, event => { + listener = startDrawing(event) + }) - setMeasurementInProgress({ - measurement: 0, - coordinates: event.feature.getGeometry().getLastCoordinate(), - center: getCenter(event.feature.getGeometry().getExtent()) + drawObject.on(DRAW_ABORT_EVENT, () => { + unByKey(listener) + + dispatch(resetMeasurementTypeToAdd()) + + setMeasurementInProgressWithRef(null) }) - return event.feature.getGeometry().on('change', changeEvent => { - function updateMeasurementOnNewPoint (event, tooltipCoordinates) { - const geom = event.target - - if (geom instanceof LineString) { - const nextMeasurementOutput = getNauticalMilesOfLine(geom) - tooltipCoordinates = geom.getLastCoordinate() - - setMeasurementInProgress({ - measurement: nextMeasurementOutput, - coordinates: tooltipCoordinates - }) - } else if (geom instanceof Circle) { - const nextMeasurementOutput = getNauticalMilesRadiusOfCircle(geom) - tooltipCoordinates = geom.getLastCoordinate() - - setMeasurementInProgress({ - measurement: nextMeasurementOutput, - coordinates: tooltipCoordinates, - center: getCenter(geom.getExtent()) - }) - } - } + drawObject.on(DRAW_END_EVENT, event => { + assertNotNullish(measurementInProgressRef.current) + + unByKey(listener) - updateMeasurementOnNewPoint(changeEvent, firstTooltipCoordinates) + dispatch(saveMeasurement(event.feature, measurementInProgressRef.current.measurement)) + dispatch(resetMeasurementTypeToAdd()) + + setMeasurementInProgressWithRef(null) }) - } + }, [dispatch, drawObject, startDrawing]) + + useEffect(() => { + if (!!measurementInProgress?.center || !!measurementInProgress?.measurement) { + dispatch( + setCircleMeasurementInDrawing({ + coordinates: measurementInProgress.center, + measurement: measurementInProgress.measurement + }) + ) + } + }, [dispatch, measurementInProgress]) return ( <> - { - measurementsDrawed.map(measurement => { - return - }) - } + {measurementsDrawed.map(measurement => ( + + ))}
- { - measurementInProgress - ? - : null - } + {measurementInProgress ? ( + + ) : null}
) } -export default React.memo(MeasurementLayer) +export const MeasurementLayer = memo(UnmemoizedMeasurementLayer) diff --git a/frontend/src/features/Measurement/slice.ts b/frontend/src/features/Measurement/slice.ts index d1b8fbcbe6..031194d586 100644 --- a/frontend/src/features/Measurement/slice.ts +++ b/frontend/src/features/Measurement/slice.ts @@ -3,6 +3,7 @@ import { createSlice } from '@reduxjs/toolkit' import { getLocalStorageState } from '../../utils' import type { MeasurementType } from '../../domain/entities/map/constants' +import type { Coordinates } from '@mtes-mct/monitor-ui' const measurementsLocalStorageKey = 'measurements' @@ -12,8 +13,10 @@ export type MeasurementState = { coordinates: number[] measurement: any } | null - // TODO Type this prop. - circleMeasurementToAdd: null + circleMeasurementToAdd: { + circleCoordinatesToAdd: Coordinates + circleRadiusToAdd: number + } | null measurementTypeToAdd: MeasurementType | null // TODO Type this prop. measurementsDrawed: Record[] diff --git a/frontend/src/features/map/Map.tsx b/frontend/src/features/map/Map.tsx index 66c75d9cf8..01c21aa089 100644 --- a/frontend/src/features/map/Map.tsx +++ b/frontend/src/features/map/Map.tsx @@ -30,7 +30,7 @@ import { AdministrativeLayers } from '../AdministrativeZone/layers/Administrativ import { BaseLayer } from '../BaseMap/layers/BaseLayer' import { DrawLayer } from '../Draw/layer' import { InterestPointLayer } from '../InterestPoint/layers/InterestPointLayer' -import MeasurementLayer from '../Measurement/layers/MeasurementLayer' +import { MeasurementLayer } from '../Measurement/layers/MeasurementLayer' import { MissionOverlay } from '../Mission/components/MissionOverlay' import { SelectedMissionOverlay } from '../Mission/components/SelectedMissionOverlay' import { MissionHoveredLayer } from '../Mission/layers/HoveredMissionLayer'