diff --git a/frontend/src/features/InterestPoint/layers/InterestPointLayer.jsx b/frontend/src/features/InterestPoint/layers/InterestPointLayer.jsx deleted file mode 100644 index b4c1a7ce32..0000000000 --- a/frontend/src/features/InterestPoint/layers/InterestPointLayer.jsx +++ /dev/null @@ -1,373 +0,0 @@ -import { usePrevious } from '@mtes-mct/monitor-ui' -import { useEffect, useRef, useState } from 'react' - -import { useDispatch, useSelector } from 'react-redux' -import VectorSource from 'ol/source/Vector' -import { MapBox, OPENLAYERS_PROJECTION } from '../../../domain/entities/map/constants' -import Draw from 'ol/interaction/Draw' -import VectorLayer from 'ol/layer/Vector' -import { getInterestPointStyle, POIStyle } from './interestPoint.style' -import { v4 as uuidv4 } from 'uuid' -import { InterestPointOverlay } from '../components/InterestPointOverlay' -import { - deleteInterestPointBeingDrawed, - editInterestPoint, - endInterestPointDraw, - removeInterestPoint, - resetInterestPointFeatureDeletion, - updateInterestPointBeingDrawed, - updateInterestPointKeyBeingDrawed -} from '../slice' -import { - coordinatesAreModified, - coordinatesOrTypeAreModified, - InterestPointType -} from '../utils' -import saveInterestPointFeature from '../../../domain/use_cases/interestPoint/saveInterestPointFeature' -import GeoJSON from 'ol/format/GeoJSON' -import LineString from 'ol/geom/LineString' -import { InterestPointLine } from './interestPointLine' -import { getLength } from 'ol/sphere' -import { LayerProperties } from '../../../domain/entities/layers/constants' -import { setRightMapBoxOpened } from '../../../domain/shared_slices/Global' -import { monitorfishMap } from '../../map/monitorfishMap' - -const DRAW_START_EVENT = 'drawstart' -const DRAW_ABORT_EVENT = 'drawabort' -const DRAW_END_EVENT = 'drawend' - -export const MIN_ZOOM = 7 - -const InterestPointLayer = ({ mapMovingAndZoomEvent }) => { - const dispatch = useDispatch() - - const { - isDrawing, - isEditing, - /** @type {InterestPoint | null} interestPointBeingDrawed */ - interestPointBeingDrawed, - /** @type {InterestPoint[]} interestPoints */ - interestPoints, - triggerInterestPointFeatureDeletion - } = useSelector(state => state.interestPoint) - - const [drawObject, setDrawObject] = useState(null) - - const vectorSourceRef = useRef(null) - function getVectorSource () { - if (vectorSourceRef.current === null) { - vectorSourceRef.current = new VectorSource({ - wrapX: false, - projection: OPENLAYERS_PROJECTION - }) - } - return vectorSourceRef.current - } - - const layerRef = useRef(null) - function getLayer () { - if (layerRef.current === null) { - layerRef.current = new VectorLayer({ - source: getVectorSource(), - renderBuffer: 7, - updateWhileAnimating: true, - updateWhileInteracting: true, - style: (feature, resolution) => getInterestPointStyle(feature, resolution), - zIndex: LayerProperties.INTEREST_POINT.zIndex - }) - } - return layerRef.current - } - - const previousMapZoom = useRef('') - const [interestPointToCoordinates, setInterestPointToCoordinates] = useState(new Map()) - const previousInterestPointBeingDrawed = usePrevious(interestPointBeingDrawed) - - useEffect(() => { - function addLayerToMap () { - monitorfishMap.getLayers().push(getLayer()) - - return () => { - monitorfishMap.removeLayer(getLayer()) - } - } - - addLayerToMap() - }, []) - - useEffect(() => { - function drawExistingFeaturesOnMap () { - if (interestPoints) { - const features = interestPoints.map(interestPoint => { - if (interestPoint.feature) { - const nextFeature = new GeoJSON({ - featureProjection: OPENLAYERS_PROJECTION - }).readFeature(interestPoint.feature) - - const { feature, ...interestPointWithoutFeature } = interestPoint - nextFeature.setProperties(interestPointWithoutFeature) - - return nextFeature - } - - return null - }).filter(feature => feature) - - getVectorSource().addFeatures(features) - } - } - - drawExistingFeaturesOnMap() - }, [interestPoints]) - - useEffect(() => { - if (isDrawing) { - function addEmptyNextInterestPoint () { - dispatch(updateInterestPointBeingDrawed({ - uuid: uuidv4(), - name: null, - type: InterestPointType.FISHING_VESSEL, - coordinates: null, - observations: null - })) - } - - function drawNewFeatureOnMap () { - const draw = new Draw({ - source: getVectorSource(), - type: 'Point', - style: POIStyle - }) - - monitorfishMap.addInteraction(draw) - setDrawObject(draw) - } - - addEmptyNextInterestPoint() - drawNewFeatureOnMap() - } - }, [isDrawing]) - - useEffect(() => { - function removeInteraction () { - if (!isDrawing && drawObject) { - setDrawObject(null) - - function waitForUnwantedZoomAndQuitInteraction () { - setTimeout(() => { - monitorfishMap.removeInteraction(drawObject) - }, 300) - } - - waitForUnwantedZoomAndQuitInteraction() - } - } - - removeInteraction() - }, [isDrawing]) - - useEffect(() => { - function handleDrawEvents () { - if (drawObject) { - drawObject.once(DRAW_START_EVENT, event => { - function startDrawing (event, type) { - dispatch(updateInterestPointBeingDrawed({ - uuid: interestPointBeingDrawed.uuid, - name: null, - type: type, - coordinates: event.feature.getGeometry().getLastCoordinate(), - observations: null - })) - } - - if (interestPointBeingDrawed) { - startDrawing(event, interestPointBeingDrawed.type || InterestPointType.OTHER) - } - }) - - drawObject.once(DRAW_ABORT_EVENT, () => { - dispatch(endInterestPointDraw()) - dispatch(deleteInterestPointBeingDrawed()) - dispatch(setRightMapBoxOpened(undefined)) - }) - - drawObject.once(DRAW_END_EVENT, event => { - dispatch(saveInterestPointFeature(event.feature)) - dispatch(endInterestPointDraw()) - }) - } - } - - handleDrawEvents() - }, [drawObject, interestPointBeingDrawed]) - - useEffect(() => { - function showOrHideInterestPointsOverlays () { - const currentZoom = monitorfishMap.getView().getZoom().toFixed(2) - if (currentZoom !== previousMapZoom.current) { - previousMapZoom.current = currentZoom - if (currentZoom < MIN_ZOOM) { - getVectorSource().forEachFeature(feature => { - feature.set(InterestPointLine.isHiddenByZoomProperty, true) - }) - } else { - getVectorSource().forEachFeature(feature => { - feature.set(InterestPointLine.isHiddenByZoomProperty, false) - }) - } - } - } - - showOrHideInterestPointsOverlays() - }, [mapMovingAndZoomEvent]) - - useEffect(() => { - if (triggerInterestPointFeatureDeletion) { - deleteInterestPoint(triggerInterestPointFeatureDeletion) - resetInterestPointFeatureDeletion() - } - }, [triggerInterestPointFeatureDeletion]) - - useEffect(() => { - function modifyFeatureWhenCoordinatesOrTypeModified () { - if (interestPointBeingDrawed?.coordinates?.length && interestPointBeingDrawed?.uuid) { - const drawingFeatureToUpdate = getVectorSource().getFeatureById(interestPointBeingDrawed.uuid) - - if (drawingFeatureToUpdate && coordinatesOrTypeAreModified(drawingFeatureToUpdate, interestPointBeingDrawed)) { - const { feature, ...interestPointWithoutFeature } = interestPointBeingDrawed - drawingFeatureToUpdate.getGeometry().setCoordinates(interestPointWithoutFeature.coordinates) - drawingFeatureToUpdate.setProperties(interestPointWithoutFeature) - - const nextFeature = new GeoJSON() - .writeFeatureObject(drawingFeatureToUpdate, { featureProjection: OPENLAYERS_PROJECTION }) - - dispatch(updateInterestPointKeyBeingDrawed({ - key: 'feature', - value: nextFeature - })) - } - } - } - - modifyFeatureWhenCoordinatesOrTypeModified() - }, [interestPointBeingDrawed?.coordinates, interestPointBeingDrawed?.type]) - - useEffect(() => { - function initLineWhenInterestPointCoordinatesModified () { - if (interestPointBeingDrawed && previousInterestPointBeingDrawed && coordinatesAreModified(interestPointBeingDrawed, previousInterestPointBeingDrawed)) { - const line = new LineString([interestPointBeingDrawed.coordinates, previousInterestPointBeingDrawed.coordinates]) - const distance = getLength(line, { projection: OPENLAYERS_PROJECTION }) - - if (distance > 10) { - const featureId = InterestPointLine.getFeatureId(interestPointBeingDrawed.uuid) - if (interestPointToCoordinates.has(featureId)) { - interestPointToCoordinates.delete(featureId) - const feature = getVectorSource().getFeatureById(featureId) - if (feature) { - feature.setGeometry(new LineString([interestPointBeingDrawed.coordinates, interestPointBeingDrawed.coordinates])) - } - } - } - } - } - - initLineWhenInterestPointCoordinatesModified() - }, [interestPointBeingDrawed]) - - function deleteInterestPoint (uuid) { - const feature = getVectorSource().getFeatureById(uuid) - if (feature) { - getVectorSource().removeFeature(feature) - getVectorSource().changed() - } - - const featureLine = getVectorSource().getFeatureById(InterestPointLine.getFeatureId(uuid)) - if (featureLine) { - getVectorSource().removeFeature(featureLine) - getVectorSource().changed() - } - - dispatch(removeInterestPoint(uuid)) - } - - function moveInterestPointLine (uuid, coordinates, nextCoordinates, offset) { - const featureId = InterestPointLine.getFeatureId(uuid) - - if (interestPointToCoordinates.has(featureId)) { - const existingLabelLineFeature = getVectorSource().getFeatureById(featureId) - const interestPointFeature = getVectorSource().getFeatureById(uuid) - - if (existingLabelLineFeature) { - if (interestPointFeature) { - existingLabelLineFeature.setGeometry(new LineString([interestPointFeature.getGeometry().getCoordinates(), nextCoordinates])) - } - } - } else { - const interestPointLineFeature = InterestPointLine.getFeature( - coordinates, - nextCoordinates, - featureId) - - getVectorSource().addFeature(interestPointLineFeature) - } - - const nextVesselToCoordinates = interestPointToCoordinates - interestPointToCoordinates.set(featureId, { coordinates: nextCoordinates, offset }) - setInterestPointToCoordinates(nextVesselToCoordinates) - } - - function modifyInterestPoint (uuid) { - dispatch(editInterestPoint(uuid)) - dispatch(setRightMapBoxOpened(MapBox.INTEREST_POINT)) - } - - function deleteInterestPointBeingDrawedAndCloseTool () { - dispatch(endInterestPointDraw()) - dispatch(setRightMapBoxOpened(undefined)) - dispatch(deleteInterestPointBeingDrawed()) - } - - return ( - <> -
- { - interestPoints && Array.isArray(interestPoints) - ? interestPoints.map(interestPoint => { - return - }) - : null - } - { - interestPointBeingDrawed && !isEditing - ? {}} - zoomHasChanged={previousMapZoom.current} - moveLine={moveInterestPointLine} - /> - : null - } -
- - ) -} - -export default InterestPointLayer diff --git a/frontend/src/features/InterestPoint/layers/InterestPointLayer.tsx b/frontend/src/features/InterestPoint/layers/InterestPointLayer.tsx new file mode 100644 index 0000000000..112b110dd2 --- /dev/null +++ b/frontend/src/features/InterestPoint/layers/InterestPointLayer.tsx @@ -0,0 +1,395 @@ +import { useMainAppDispatch } from '@hooks/useMainAppDispatch' +import { useMainAppSelector } from '@hooks/useMainAppSelector' +import { usePrevious } from '@mtes-mct/monitor-ui' +import { omit } from 'lodash' +import GeoJSON from 'ol/format/GeoJSON' +import LineString from 'ol/geom/LineString' +import Draw from 'ol/interaction/Draw' +import VectorLayer from 'ol/layer/Vector' +import VectorSource from 'ol/source/Vector' +import { getLength } from 'ol/sphere' +import { useCallback, useEffect, useRef, useState } from 'react' +import { v4 as uuidv4 } from 'uuid' + +import { getInterestPointStyle, POIStyle } from './interestPoint.style' +import { InterestPointLine } from './InterestPointLine' +import { LayerProperties } from '../../../domain/entities/layers/constants' +import { MapBox, OPENLAYERS_PROJECTION } from '../../../domain/entities/map/constants' +import { setRightMapBoxOpened } from '../../../domain/shared_slices/Global' +import saveInterestPointFeature from '../../../domain/use_cases/interestPoint/saveInterestPointFeature' +import { monitorfishMap } from '../../map/monitorfishMap' +import { InterestPointOverlay } from '../components/InterestPointOverlay' +import { + deleteInterestPointBeingDrawed, + editInterestPoint, + endInterestPointDraw, + removeInterestPoint, + resetInterestPointFeatureDeletion, + updateInterestPointBeingDrawed, + updateInterestPointKeyBeingDrawed +} from '../slice' +import { coordinatesAreModified, coordinatesOrTypeAreModified, InterestPointType } from '../utils' + +import type { DummyObjectToForceEffectHookUpdate } from '@features/map/types' +import type { Feature } from 'ol' +import type { Geometry } from 'ol/geom' + +const DRAW_START_EVENT = 'drawstart' +const DRAW_ABORT_EVENT = 'drawabort' +const DRAW_END_EVENT = 'drawend' + +export const MIN_ZOOM = 7 + +type InterstPointLayerProps = Readonly<{ + mapMovingAndZoomEvent: DummyObjectToForceEffectHookUpdate +}> +export function InterestPointLayer({ mapMovingAndZoomEvent }: InterstPointLayerProps) { + const previousMapZoom = useRef('') + const vectorSourceRef = useRef>>( + new VectorSource({ + // TODO Fix that: `projection` propr does not exist in type `Options>`. + // @ts-ignore + projection: OPENLAYERS_PROJECTION, + wrapX: false + }) + ) + const layerRef = useRef>>( + new VectorLayer({ + renderBuffer: 7, + source: vectorSourceRef.current, + style: (feature, resolution) => getInterestPointStyle(feature, resolution), + updateWhileAnimating: true, + updateWhileInteracting: true, + zIndex: LayerProperties.INTEREST_POINT.zIndex + }) + ) + + const dispatch = useMainAppDispatch() + const { + interestPointBeingDrawed, + interestPoints, + /** @type {InterestPoint | null} interestPointBeingDrawed */ + isDrawing, + /** @type {InterestPoint[]} interestPoints */ + isEditing, + triggerInterestPointFeatureDeletion + } = useMainAppSelector(state => state.interestPoint) + + const [drawObject, setDrawObject] = useState(null) + const [interestPointToCoordinates, setInterestPointToCoordinates] = useState(new Map()) + const previousInterestPointBeingDrawed = usePrevious(interestPointBeingDrawed) + + const addEmptyNextInterestPoint = useCallback(() => { + dispatch( + updateInterestPointBeingDrawed({ + coordinates: null, + name: null, + observations: null, + type: InterestPointType.FISHING_VESSEL, + uuid: uuidv4() + }) + ) + }, [dispatch]) + + const drawNewFeatureOnMap = useCallback(() => { + const draw = new Draw({ + source: vectorSourceRef.current, + style: POIStyle, + type: 'Point' + }) + + monitorfishMap.addInteraction(draw) + setDrawObject(draw) + }, []) + + const waitForUnwantedZoomAndQuitInteraction = useCallback(() => { + setTimeout(() => { + if (drawObject) { + monitorfishMap.removeInteraction(drawObject) + } + }, 300) + }, [drawObject]) + + const startDrawing = useCallback( + (event, type) => { + dispatch( + updateInterestPointBeingDrawed({ + coordinates: event.feature.getGeometry().getLastCoordinate(), + name: null, + observations: null, + type, + uuid: interestPointBeingDrawed?.uuid + }) + ) + }, + [dispatch, interestPointBeingDrawed] + ) + + const handleDrawEvents = useCallback(() => { + if (drawObject) { + drawObject.once(DRAW_START_EVENT, event => { + if (interestPointBeingDrawed) { + startDrawing(event, interestPointBeingDrawed.type || InterestPointType.OTHER) + } + }) + + drawObject.once(DRAW_ABORT_EVENT, () => { + dispatch(endInterestPointDraw()) + dispatch(deleteInterestPointBeingDrawed()) + dispatch(setRightMapBoxOpened(undefined)) + }) + + drawObject.once(DRAW_END_EVENT, event => { + dispatch(saveInterestPointFeature(event.feature)) + dispatch(endInterestPointDraw()) + }) + } + }, [dispatch, drawObject, interestPointBeingDrawed, startDrawing]) + + const showOrHideInterestPointsOverlays = useCallback(() => { + const currentZoom = monitorfishMap.getView().getZoom()?.toFixed(2) + if (!!currentZoom && currentZoom !== previousMapZoom.current) { + previousMapZoom.current = currentZoom + if (Number(currentZoom) < MIN_ZOOM) { + vectorSourceRef.current.forEachFeature(feature => { + feature.set(InterestPointLine.isHiddenByZoomProperty, true) + }) + } else { + vectorSourceRef.current.forEachFeature(feature => { + feature.set(InterestPointLine.isHiddenByZoomProperty, false) + }) + } + } + }, []) + + const deleteInterestPoint = useCallback( + (uuid: string) => { + const feature = vectorSourceRef.current.getFeatureById(uuid) + if (feature) { + vectorSourceRef.current.removeFeature(feature) + vectorSourceRef.current.changed() + } + + const featureLine = vectorSourceRef.current.getFeatureById(InterestPointLine.getFeatureId(uuid)) + if (featureLine) { + vectorSourceRef.current.removeFeature(featureLine) + vectorSourceRef.current.changed() + } + + dispatch(removeInterestPoint(uuid)) + }, + [dispatch] + ) + + const moveInterestPointLine = useCallback( + (uuid: string, coordinates: string[], nextCoordinates: string[], offset: number) => { + const featureId = InterestPointLine.getFeatureId(uuid) + + if (interestPointToCoordinates.has(featureId)) { + const existingLabelLineFeature = vectorSourceRef.current.getFeatureById(featureId) + const interestPointFeature = vectorSourceRef.current.getFeatureById(uuid) + + if (existingLabelLineFeature) { + if (interestPointFeature) { + const geometry = existingLabelLineFeature.getGeometry() + // TODO Fix that. There may be a real bug here: `Property 'getCoordinates' does not exist on type 'Geometry'.`. Legacy code. + // @ts-ignore + const previousCoordinates = geometry?.getCoordinates() + + existingLabelLineFeature.setGeometry(new LineString([previousCoordinates, nextCoordinates])) + } + } + } else { + const interestPointLineFeature = InterestPointLine.getFeature(coordinates, nextCoordinates, featureId) + + vectorSourceRef.current.addFeature(interestPointLineFeature) + } + + const nextVesselToCoordinates = interestPointToCoordinates + interestPointToCoordinates.set(featureId, { coordinates: nextCoordinates, offset }) + setInterestPointToCoordinates(nextVesselToCoordinates) + }, + [interestPointToCoordinates] + ) + + const modifyInterestPoint = useCallback( + (uuid: string) => { + dispatch(editInterestPoint(uuid)) + dispatch(setRightMapBoxOpened(MapBox.INTEREST_POINT)) + }, + [dispatch] + ) + + const deleteInterestPointBeingDrawedAndCloseTool = useCallback(() => { + dispatch(endInterestPointDraw()) + dispatch(setRightMapBoxOpened(undefined)) + dispatch(deleteInterestPointBeingDrawed()) + }, [dispatch]) + + const modifyFeatureWhenCoordinatesOrTypeModified = useCallback(() => { + if (interestPointBeingDrawed?.coordinates?.length && interestPointBeingDrawed?.uuid) { + const drawingFeatureToUpdate = vectorSourceRef.current.getFeatureById(interestPointBeingDrawed.uuid) + + if (drawingFeatureToUpdate && coordinatesOrTypeAreModified(drawingFeatureToUpdate, interestPointBeingDrawed)) { + const interestPointWithoutFeature = omit(interestPointBeingDrawed, 'feature') + const drawingFeatureToUpdateGeometry = drawingFeatureToUpdate.getGeometry() + if (drawingFeatureToUpdateGeometry) { + // TODO Fix that. There may be a real bug here: `Property 'setCoordinates' does not exist on type 'Geometry'.`. Legacy code. + // @ts-ignore + drawingFeatureToUpdateGeometry.setCoordinates(interestPointBeingDrawed.coordinates) + } + drawingFeatureToUpdate.setProperties(interestPointWithoutFeature) + + const nextFeature = new GeoJSON().writeFeatureObject(drawingFeatureToUpdate, { + featureProjection: OPENLAYERS_PROJECTION + }) + + dispatch( + updateInterestPointKeyBeingDrawed({ + key: 'feature', + value: nextFeature + }) + ) + } + } + }, [dispatch, interestPointBeingDrawed]) + + const initLineWhenInterestPointCoordinatesModified = useCallback(() => { + if ( + interestPointBeingDrawed && + previousInterestPointBeingDrawed && + coordinatesAreModified(interestPointBeingDrawed, previousInterestPointBeingDrawed) + ) { + const line = new LineString([interestPointBeingDrawed.coordinates, previousInterestPointBeingDrawed.coordinates]) + const distance = getLength(line, { projection: OPENLAYERS_PROJECTION }) + + if (distance > 10) { + const featureId = InterestPointLine.getFeatureId(interestPointBeingDrawed.uuid) + if (interestPointToCoordinates.has(featureId)) { + interestPointToCoordinates.delete(featureId) + const feature = vectorSourceRef.current.getFeatureById(featureId) + if (feature) { + feature.setGeometry( + new LineString([interestPointBeingDrawed.coordinates, interestPointBeingDrawed.coordinates]) + ) + } + } + } + } + }, [interestPointBeingDrawed, interestPointToCoordinates, previousInterestPointBeingDrawed]) + + useEffect(() => { + const layer = layerRef.current + + monitorfishMap.getLayers().push(layer) + + return () => { + monitorfishMap.removeLayer(layer) + } + }, []) + + useEffect(() => { + function drawExistingFeaturesOnMap() { + if (interestPoints) { + const features = interestPoints + .map(interestPoint => { + if (interestPoint.feature) { + const nextFeature = new GeoJSON({ + featureProjection: OPENLAYERS_PROJECTION + }).readFeature(interestPoint.feature) + + const interestPointWithoutFeature = omit(interestPoint, 'feature') + nextFeature.setProperties(interestPointWithoutFeature) + + return nextFeature + } + + return null + }) + .filter(feature => !!feature) + + vectorSourceRef.current.addFeatures(features) + } + } + + drawExistingFeaturesOnMap() + }, [interestPoints]) + + useEffect(() => { + if (isDrawing) { + addEmptyNextInterestPoint() + drawNewFeatureOnMap() + } + }, [addEmptyNextInterestPoint, drawNewFeatureOnMap, isDrawing]) + + useEffect(() => { + function removeInteraction() { + if (!isDrawing && drawObject) { + setDrawObject(null) + + waitForUnwantedZoomAndQuitInteraction() + } + } + + removeInteraction() + }, [drawObject, isDrawing, waitForUnwantedZoomAndQuitInteraction]) + + useEffect(() => { + handleDrawEvents() + }, [handleDrawEvents]) + + useEffect(() => { + showOrHideInterestPointsOverlays() + }, [mapMovingAndZoomEvent, showOrHideInterestPointsOverlays]) + + useEffect(() => { + if (triggerInterestPointFeatureDeletion) { + deleteInterestPoint(triggerInterestPointFeatureDeletion) + resetInterestPointFeatureDeletion() + } + }, [deleteInterestPoint, triggerInterestPointFeatureDeletion]) + + useEffect(() => { + modifyFeatureWhenCoordinatesOrTypeModified() + }, [modifyFeatureWhenCoordinatesOrTypeModified]) + + useEffect(() => { + initLineWhenInterestPointCoordinatesModified() + }, [initLineWhenInterestPointCoordinatesModified]) + + return ( + <> +
+ {!!interestPoints && + Array.isArray(interestPoints) && + interestPoints.map(interestPoint => ( + + ))} + {!!interestPointBeingDrawed && !isEditing && ( + {}} + moveLine={moveInterestPointLine} + name={interestPointBeingDrawed.name} + observations={interestPointBeingDrawed.observations} + uuid={interestPointBeingDrawed.uuid} + zoomHasChanged={previousMapZoom.current} + /> + )} +
+ + ) +} diff --git a/frontend/src/features/InterestPoint/layers/interestPointLine.js b/frontend/src/features/InterestPoint/layers/InterestPointLine.ts similarity index 55% rename from frontend/src/features/InterestPoint/layers/interestPointLine.js rename to frontend/src/features/InterestPoint/layers/InterestPointLine.ts index aa6374fa32..bac6ad30ed 100644 --- a/frontend/src/features/InterestPoint/layers/interestPointLine.js +++ b/frontend/src/features/InterestPoint/layers/InterestPointLine.ts @@ -4,14 +4,18 @@ import LineString from 'ol/geom/LineString' export class InterestPointLine { static typeProperty = 'type' static isHiddenByZoomProperty = 'isHiddenByZoom' + /** * InterestPointLine object for building OpenLayers interest point line to draggable overlay - * @param {string[]} fromCoordinates - The [longitude, latitude] of the start of the line (the interest point position) - * @param {string[]} toCoordinates - The [longitude, latitude] of the overlay position - * @param {string} featureId - The feature identifier + * + * @param fromCoordinates - The [longitude, latitude] of the start of the line (the interest point position) + * @param toCoordinates - The [longitude, latitude] of the overlay position + * @param featureId - The feature identifier */ - static getFeature(fromCoordinates, toCoordinates, featureId) { + static getFeature(fromCoordinates: string[], toCoordinates: string[], featureId: string) { const interestPointLineFeature = new Feature({ + // TODO Fix that: `ype 'string[]' is not assignable to type 'number | Coordinate'.`. Legacy code. + // @ts-ignore geometry: new LineString([fromCoordinates, toCoordinates]) }) diff --git a/frontend/src/features/InterestPoint/layers/interestPoint.style.tsx b/frontend/src/features/InterestPoint/layers/interestPoint.style.tsx index f9b452852c..9f3a3ffc29 100644 --- a/frontend/src/features/InterestPoint/layers/interestPoint.style.tsx +++ b/frontend/src/features/InterestPoint/layers/interestPoint.style.tsx @@ -4,7 +4,7 @@ import CircleStyle from 'ol/style/Circle' import Fill from 'ol/style/Fill' import Stroke from 'ol/style/Stroke' -import { InterestPointLine } from './interestPointLine' +import { InterestPointLine } from './InterestPointLine' import { INTEREST_POINT_STYLE, InterestPointType } from '../utils' const interestPointStylesCache = new Map() diff --git a/frontend/src/features/Measurement/layers/MeasurementLayer.jsx b/frontend/src/features/Measurement/layers/MeasurementLayer.jsx deleted file mode 100644 index b7f7aaf79a..0000000000 --- a/frontend/src/features/Measurement/layers/MeasurementLayer.jsx +++ /dev/null @@ -1,309 +0,0 @@ -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 LineString from 'ol/geom/LineString' -import { getLength } from 'ol/sphere' -import { - removeMeasurementDrawed, - resetMeasurementTypeToAdd, - setCircleMeasurementInDrawing -} from '../slice' -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 { METERS_PER_UNIT } from 'ol/proj/Units' -import GeoJSON from 'ol/format/GeoJSON' -import MeasurementOverlay from '../components/MeasurementOverlay' -import { getNauticalMilesFromMeters } from '../../../utils' -import saveMeasurement from '../../../domain/use_cases/measurement/saveMeasurement' -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' - -const DRAW_START_EVENT = 'drawstart' -const DRAW_ABORT_EVENT = 'drawabort' -const DRAW_END_EVENT = 'drawend' - -const getNauticalMilesRadiusOfCircle = circle => { - const polygon = fromCircle(circle) - - return getNauticalMilesRadiusOfCircularPolygon(polygon) -} - -const getNauticalMilesOfLine = line => { - const length = getLength(line) - - return `${getNauticalMilesFromMeters(length)} nm` -} - -function getNauticalMilesRadiusOfCircularPolygon (polygon) { - const length = getLength(polygon) - const radius = length / (2 * Math.PI) - - return `r = ${getNauticalMilesFromMeters(radius)} nm` -} - -const MeasurementLayer = () => { - const dispatch = useDispatch() - - const { - measurementTypeToAdd, - measurementsDrawed, - circleMeasurementToAdd - } = useSelector(state => state.measurement) - - const [measurementInProgress, _setMeasurementInProgress] = useState(null) - const measurementInProgressRef = useRef(measurementInProgress) - const setMeasurementInProgress = value => { - measurementInProgressRef.current = value - _setMeasurementInProgress(value) - } - 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 - })) - - useEffect(() => { - function addLayerToMap () { - if (vectorLayer) { - monitorfishMap.getLayers().push(vectorLayer) - } - - return () => { - monitorfishMap.removeLayer(vectorLayer) - } - } - - addLayerToMap() - }, [vectorLayer]) - - useEffect(() => { - function drawExistingFeaturesOnMap () { - if (measurementsDrawed) { - measurementsDrawed.forEach(measurement => { - const feature = new GeoJSON({ - featureProjection: OPENLAYERS_PROJECTION - }).readFeature(measurement.feature) - - vectorSource.addFeature(feature) - }) - } - } - - drawExistingFeaturesOnMap() - }, [measurementsDrawed]) - - useEffect(() => { - if (measurementTypeToAdd) { - function addEmptyNextMeasurement () { - setMeasurementInProgress({ - feature: null, - measurement: null, - coordinates: null - }) - } - - function drawNewFeatureOnMap () { - const draw = new Draw({ - source: vectorSource, - type: measurementTypeToAdd, - style: [measurementStyle, measurementStyleWithCenter] - }) - - monitorfishMap.addInteraction(draw) - setDrawObject(draw) - } - - addEmptyNextMeasurement() - drawNewFeatureOnMap() - } - }, [measurementTypeToAdd]) - - useEffect(() => { - function removeInteraction () { - if (!measurementTypeToAdd && drawObject) { - setDrawObject(null) - setMeasurementInProgress(null) - - waitForUnwantedZoomAndQuitInteraction() - } - } - - function waitForUnwantedZoomAndQuitInteraction () { - setTimeout(() => { - monitorfishMap.removeInteraction(drawObject) - }, 300) - } - - removeInteraction() - }, [measurementTypeToAdd]) - - useEffect(() => { - function addCustomCircleMeasurement () { - const metersForOneNauticalMile = 1852 - const longitude = 1 - const latitude = 0 - const numberOfVertices = 64 - - if (!circleMeasurementHasCoordinatesAndRadiusFromForm() && !circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw()) { - return - } - - function circleMeasurementHasCoordinatesAndRadiusFromForm () { - return circleMeasurementToAdd?.circleCoordinatesToAdd?.length === 2 && circleMeasurementToAdd?.circleRadiusToAdd - } - - function circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw () { - return circleMeasurementToAdd?.circleRadiusToAdd && measurementInProgress?.center?.length === 2 - } - - 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) - } - - const circleFeature = new Feature({ - geometry: circular(coordinates, radiusInMeters, numberOfVertices).transform(WSG84_PROJECTION, OPENLAYERS_PROJECTION), - style: [measurementStyle, measurementStyleWithCenter] - }) - dispatch(saveMeasurement(circleFeature, `r = ${circleMeasurementToAdd.circleRadiusToAdd} nm`)) - } - - addCustomCircleMeasurement() - }, [circleMeasurementToAdd]) - - useEffect(() => { - function handleDrawEvents () { - if (drawObject) { - let listener - - drawObject.on(DRAW_START_EVENT, event => { - listener = startDrawing(event) - }) - - drawObject.on(DRAW_ABORT_EVENT, () => { - unByKey(listener) - dispatch(resetMeasurementTypeToAdd()) - setMeasurementInProgress(null) - }) - - drawObject.on(DRAW_END_EVENT, event => { - dispatch(saveMeasurement(event.feature, measurementInProgressRef.current.measurement)) - - unByKey(listener) - dispatch(resetMeasurementTypeToAdd()) - setMeasurementInProgress(null) - }) - } - } - - handleDrawEvents() - }, [drawObject]) - - useEffect(() => { - if (measurementInProgress?.center || measurementInProgress?.measurement) { - dispatch(setCircleMeasurementInDrawing({ - measurement: measurementInProgress.measurement, - coordinates: measurementInProgress.center - })) - } - }, [measurementInProgress]) - - function deleteFeature (featureId) { - const feature = vectorSource.getFeatureById(featureId) - if (feature) { - vectorSource.removeFeature(feature) - vectorSource.changed() - } - - dispatch(removeMeasurementDrawed(featureId)) - } - - function startDrawing (event) { - const firstTooltipCoordinates = event.coordinate - - setMeasurementInProgress({ - measurement: 0, - coordinates: event.feature.getGeometry().getLastCoordinate(), - center: getCenter(event.feature.getGeometry().getExtent()) - }) - - 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()) - }) - } - } - - updateMeasurementOnNewPoint(changeEvent, firstTooltipCoordinates) - }) - } - - return ( - <> - { - measurementsDrawed.map(measurement => { - return - }) - } - -
- { - measurementInProgress - ? - : null - } -
- - ) -} - -export default React.memo(MeasurementLayer) diff --git a/frontend/src/features/Measurement/layers/MeasurementLayer.tsx b/frontend/src/features/Measurement/layers/MeasurementLayer.tsx new file mode 100644 index 0000000000..c10e4078b8 --- /dev/null +++ b/frontend/src/features/Measurement/layers/MeasurementLayer.tsx @@ -0,0 +1,326 @@ +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 Polygon, { circular, fromCircle } from 'ol/geom/Polygon' +import Draw, { DrawEvent } from 'ol/interaction/Draw' +import VectorLayer from 'ol/layer/Vector' +import { unByKey } from 'ol/Observable' +import { transform } from 'ol/proj' +import { METERS_PER_UNIT } from 'ol/proj/Units' +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 { 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: Circle): string => { + const polygon = fromCircle(circle) + + return getNauticalMilesRadiusOfCircularPolygon(polygon) +} + +const getNauticalMilesOfLine = (line: LineString): string => { + const length = getLength(line) + + return `${getNauticalMilesFromMeters(length)} nm` +} + +function getNauticalMilesRadiusOfCircularPolygon(polygon: Polygon): string { + const length = getLength(polygon) + const radius = length / (2 * Math.PI) + + return `r = ${getNauticalMilesFromMeters(radius)} nm` +} + +type MeasurementInProgress = { + center?: Coordinate + coordinates: null + feature?: null + measurement: number | string | null +} + +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 measurementInProgressRef = useRef(measurementInProgress) + const setMeasurementInProgressWithRef = (nextMeasurementInProgress: MeasurementInProgress | null) => { + measurementInProgressRef.current = nextMeasurementInProgress + setMeasurementInProgress(nextMeasurementInProgress) + } + const [drawObject, setDrawObject] = useState(null) + + const addCustomCircleMeasurement = useCallback( + (nextCircleMeasurementToAdd: { circleCoordinatesToAdd: Coordinates; circleRadiusToAdd: number }) => { + const metersForOneNauticalMile = 1852 + const longitude = 1 + const latitude = 0 + const numberOfVertices = 64 + + if ( + !circleMeasurementHasCoordinatesAndRadiusFromForm() && + !circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw() + ) { + return + } + + function circleMeasurementHasCoordinatesAndRadiusFromForm() { + return ( + nextCircleMeasurementToAdd.circleCoordinatesToAdd?.length === 2 && + nextCircleMeasurementToAdd.circleRadiusToAdd + ) + } + + function circleMeasurementHasRadiusFromFormAndCoordinatesFromDraw() { + return nextCircleMeasurementToAdd.circleRadiusToAdd && measurementInProgress?.center?.length === 2 + } + + assertNotNullish(nextCircleMeasurementToAdd.circleRadiusToAdd) + + 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) + } + + 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] + ) + + const deleteFeature = useCallback( + (featureId: string) => { + const feature = vectorSourceRef.current.getFeatureById(featureId) + if (feature) { + vectorSourceRef.current.removeFeature(feature) + vectorSourceRef.current.changed() + } + + dispatch(removeMeasurementDrawed(featureId)) + }, + [dispatch] + ) + + 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 + }) + + return geometry.on('change', changeEvent => { + const geom = changeEvent.target + + if (geom instanceof LineString) { + const nextMeasurementOutput = getNauticalMilesOfLine(geom) + firstTooltipCoordinates = geom.getLastCoordinate() + + 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(() => { + const vectorLayer = vectorLayerRef.current + + monitorfishMap.getLayers().push(vectorLayer) + + return () => { + monitorfishMap.removeLayer(vectorLayer) + } + }, []) + + useEffect(() => { + if (measurementsDrawed) { + measurementsDrawed.forEach(measurement => { + const feature = new GeoJSON({ + featureProjection: OPENLAYERS_PROJECTION + }).readFeature(measurement.feature) + + vectorSourceRef.current.addFeature(feature) + }) + } + }, [measurementsDrawed]) + + useEffect(() => { + if (measurementTypeToAdd) { + setMeasurementInProgressWithRef({ + coordinates: null, + feature: null, + measurement: null + }) + + const draw = new Draw({ + source: vectorSourceRef.current, + style: [measurementStyle, measurementStyleWithCenter], + type: measurementTypeToAdd + }) + + monitorfishMap.addInteraction(draw) + setDrawObject(draw) + } + }, [measurementTypeToAdd]) + + useEffect(() => { + if (!measurementTypeToAdd && drawObject) { + setDrawObject(null) + setMeasurementInProgressWithRef(null) + + setTimeout(() => { + monitorfishMap.removeInteraction(drawObject) + }, 300) + } + }, [drawObject, measurementTypeToAdd]) + + useEffect(() => { + if (circleMeasurementToAdd) { + addCustomCircleMeasurement(circleMeasurementToAdd) + } + }, [addCustomCircleMeasurement, circleMeasurementToAdd]) + + useEffect(() => { + if (!drawObject) { + return + } + + let listener + + drawObject.on(DRAW_START_EVENT, event => { + listener = startDrawing(event) + }) + + drawObject.on(DRAW_ABORT_EVENT, () => { + unByKey(listener) + + dispatch(resetMeasurementTypeToAdd()) + + setMeasurementInProgressWithRef(null) + }) + + drawObject.on(DRAW_END_EVENT, event => { + assertNotNullish(measurementInProgressRef.current) + + unByKey(listener) + + 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 => ( + + ))} + +
+ {measurementInProgress ? ( + + ) : null} +
+ + ) +} + +export const MeasurementLayer = memo(UnmemoizedMeasurementLayer) diff --git a/frontend/src/features/Measurement/layers/measurement.style.jsx b/frontend/src/features/Measurement/layers/measurement.style.tsx similarity index 61% rename from frontend/src/features/Measurement/layers/measurement.style.jsx rename to frontend/src/features/Measurement/layers/measurement.style.tsx index 127026bb35..5c6642c619 100644 --- a/frontend/src/features/Measurement/layers/measurement.style.jsx +++ b/frontend/src/features/Measurement/layers/measurement.style.tsx @@ -1,32 +1,35 @@ -import Style from 'ol/style/Style' +import { THEME } from '@mtes-mct/monitor-ui' +import { assertNotNullish } from '@utils/assertNotNullish' +import { getCenter } from 'ol/extent' +import Point from 'ol/geom/Point' +import CircleStyle from 'ol/style/Circle' import Fill from 'ol/style/Fill' import Stroke from 'ol/style/Stroke' -import { COLORS } from '@constants/constants' -import CircleStyle from 'ol/style/Circle' -import Point from 'ol/geom/Point' -import { getCenter } from 'ol/extent' +import Style from 'ol/style/Style' export const measurementStyleWithCenter = new Style({ - image: new CircleStyle({ - radius: 2, - fill: new Fill({ - color: COLORS.slateGray - }) - }), geometry: feature => { - if (feature.getGeometry().getType() === 'LineString') { + if (feature.getGeometry()?.getType() === 'LineString') { return undefined } - const extent = feature.getGeometry().getExtent() + const extent = feature.getGeometry()?.getExtent() + assertNotNullish(extent) const center = getCenter(extent) + return new Point(center) - } + }, + image: new CircleStyle({ + fill: new Fill({ + color: THEME.color.slateGray + }), + radius: 2 + }) }) export const measurementStyle = new Style({ stroke: new Stroke({ - color: COLORS.slateGray, + color: THEME.color.slateGray, lineDash: [4, 4], width: 2 }) 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/PriorNotification/components/PriorNotificationCard/index.tsx b/frontend/src/features/PriorNotification/components/PriorNotificationCard/index.tsx index 31fefa3504..0a6a7755c7 100644 --- a/frontend/src/features/PriorNotification/components/PriorNotificationCard/index.tsx +++ b/frontend/src/features/PriorNotification/components/PriorNotificationCard/index.tsx @@ -101,7 +101,7 @@ export function PriorNotificationCard({ /> {isPendingVerification && Le préavis doit être vérifié par le CNSP avant sa diffusion.} - + Le navire doit respecter un délai d’envoi{hasDesignatedPorts && ' et débarquer dans un port désigné'}. @@ -155,9 +155,9 @@ const Body = styled.div` ` const Intro = styled.p<{ - hasNoTopMargin?: boolean + $hasNoTopMargin?: boolean }>` - ${p => p.hasNoTopMargin && 'margin-top: 2px;'} + ${p => p.$hasNoTopMargin && 'margin-top: 2px;'} color: ${p => p.theme.color.slateGray}; font-style: italic; ` diff --git a/frontend/src/features/Reporting/components/ReportingList/index.tsx b/frontend/src/features/Reporting/components/ReportingList/index.tsx index 6da6123705..e79f4c3bff 100644 --- a/frontend/src/features/Reporting/components/ReportingList/index.tsx +++ b/frontend/src/features/Reporting/components/ReportingList/index.tsx @@ -154,7 +154,7 @@ MMSI: ${reporting.mmsi || ''}` + ) @@ -34,16 +33,16 @@ export function EditReporting() { return (
- + ÉDITER LE SIGNALEMENT dispatch(setEditedReportingInSideWindow())} /> - + {editedReportingInSideWindow && editedReportingInSideWindow.flagState && ( )} @@ -73,11 +72,11 @@ export function EditReporting() { } const EditReportingWrapper = styled.div<{ - isEditedInSideWindow: boolean + $isEditedInSideWindow: boolean }>` - background: ${THEME.color.white}; + background: ${p => p.theme.color.white}; height: 100vh; - margin-right: ${p => (p.isEditedInSideWindow ? 0 : -490)}px; + margin-right: ${p => (p.$isEditedInSideWindow ? 0 : -490)}px; position: fixed; right: 0px; top: 0px; @@ -91,13 +90,11 @@ const StyledReportingForm = styled(ReportingForm)` ` const Line = styled.div` - border-bottom: 1px solid ${THEME.color.lightGray}; + border-bottom: 1px solid ${p => p.theme.color.lightGray}; width: 100%; ` -const Flag = styled.img<{ - rel?: 'preload' -}>` +const Flag = styled.img` cursor: pointer; display: inline-block; height: 14; @@ -106,7 +103,7 @@ const Flag = styled.img<{ ` const VesselName = styled.div` - color: ${THEME.color.gunMetal}; + color: ${p => p.theme.color.gunMetal}; font: normal normal bold 16px/22px Marianne; margin-left: 8px; overflow: hidden; @@ -115,7 +112,7 @@ const VesselName = styled.div` ` const InternalReferenceNumber = styled.div` - color: ${THEME.color.gunMetal}; + color: ${p => p.theme.color.gunMetal}; font: normal normal normal 16px/22px Marianne; margin-left: 5; ` @@ -130,10 +127,10 @@ const CloseIcon = styled(CloseIconSVG)` ` const Row = styled.div<{ - topMargin: number + $topMargin: number }>` display: flex; - margin-top: ${p => p.topMargin}; + margin-top: ${p => p.$topMargin}; ` const Header = styled.div` @@ -141,7 +138,7 @@ const Header = styled.div` ` const Title = styled.span` - color: ${THEME.color.slateGray}; + color: ${p => p.theme.color.slateGray}; font: normal normal bold 16px Marianne; letter-spacing: 0px; margin-left: 10px; diff --git a/frontend/src/features/SideWindow/Alert/AlertListAndReportingList/index.tsx b/frontend/src/features/SideWindow/Alert/AlertListAndReportingList/index.tsx index b9d3f51286..155f1f4d81 100644 --- a/frontend/src/features/SideWindow/Alert/AlertListAndReportingList/index.tsx +++ b/frontend/src/features/SideWindow/Alert/AlertListAndReportingList/index.tsx @@ -39,14 +39,14 @@ export function AlertListAndReportingList({ return ( setSelectedTab(AlertAndReportingTab.ALERT)} > Alertes setSelectedTab(AlertAndReportingTab.REPORTING)} > Signalements @@ -75,9 +75,9 @@ const Wrapper = styled.div` // TODO This should be a `<a />` or a `<button />`. const Title = styled.h2<{ - isSelected: boolean + $isSelected: boolean }>` - border-bottom: 5px solid ${p => (p.isSelected ? p.theme.color.charcoal : p.theme.color.white)}; + border-bottom: 5px solid ${p => (p.$isSelected ? p.theme.color.charcoal : p.theme.color.white)}; color: ${p => p.theme.color.gunMetal}; cursor: pointer; display: inline-block; diff --git a/frontend/src/features/map/Map.tsx b/frontend/src/features/map/Map.tsx index 29450aeab8..01c21aa089 100644 --- a/frontend/src/features/map/Map.tsx +++ b/frontend/src/features/map/Map.tsx @@ -29,8 +29,8 @@ import { FeatureWithCodeAndEntityId } from '../../libs/FeatureWithCodeAndEntityI import { AdministrativeLayers } from '../AdministrativeZone/layers/AdministrativeLayers' import { BaseLayer } from '../BaseMap/layers/BaseLayer' import { DrawLayer } from '../Draw/layer' -import InterestPointLayer from '../InterestPoint/layers/InterestPointLayer' -import MeasurementLayer from '../Measurement/layers/MeasurementLayer' +import { InterestPointLayer } from '../InterestPoint/layers/InterestPointLayer' +import { MeasurementLayer } from '../Measurement/layers/MeasurementLayer' import { MissionOverlay } from '../Mission/components/MissionOverlay' import { SelectedMissionOverlay } from '../Mission/components/SelectedMissionOverlay' import { MissionHoveredLayer } from '../Mission/layers/HoveredMissionLayer' @@ -45,6 +45,8 @@ import { HoveredStationOverlay } from '../Station/components/HoveredStationOverl import { SelectedStationOverlay } from '../Station/components/SelectedStationOverlay' import { StationLayer } from '../Station/components/StationLayer' +import type { DummyObjectToForceEffectHookUpdate } from './types' + export function Map() { const isSuperUser = useIsSuperUser() const { areVesselsDisplayed, isMissionsLayerDisplayed, isStationLayerDisplayed } = useMainAppSelector( @@ -53,7 +55,7 @@ export function Map() { const [shouldUpdateView, setShouldUpdateView] = useState(true) const [historyMoveTrigger, setHistoryMoveTrigger] = useState({}) const [hoveredFeature, setHoveredFeature] = useState<Feature | FeatureWithCodeAndEntityId | undefined>(undefined) - const [mapMovingAndZoomEvent, setMapMovingAndZoomEvent] = useState<Object>({}) + const [mapMovingAndZoomEvent, setMapMovingAndZoomEvent] = useState<DummyObjectToForceEffectHookUpdate>({}) const [handlePointerMoveEventPixel, setHandlePointerMoveEventPixel] = useState(null) const hoveredFeatureWithCodeAndEntityId = diff --git a/frontend/src/features/map/types.ts b/frontend/src/features/map/types.ts index 0e6a2e25fe..57ad1b03bb 100644 --- a/frontend/src/features/map/types.ts +++ b/frontend/src/features/map/types.ts @@ -9,3 +9,8 @@ export interface VectorSourceForFeatureWithCodeAndEntityId<G extends Geometry = addFeatures(features: Array<FeatureWithCodeAndEntityId<G>>): void forEachFeature<T>(callback: (feature: FeatureWithCodeAndEntityId<G>) => void): T | undefined } + +// TODO Replace this hack by a cleaner solution. Created for legacy compatibility. +export type DummyObjectToForceEffectHookUpdate = { + dummyUpdate?: true +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 5c4d797772..5426cca363 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,4 +1,5 @@ import { browserTracingIntegration, init, replayIntegration } from '@sentry/react' +import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import { AuthProvider } from 'react-oidc-context' @@ -33,11 +34,17 @@ const { IS_OIDC_ENABLED, oidcConfig } = getOIDCConfig() if (IS_OIDC_ENABLED) { root.render( - // eslint-disable-next-line react/jsx-props-no-spreading - <AuthProvider {...oidcConfig}> - <App /> - </AuthProvider> + <StrictMode> + {/* eslint-disable-next-line react/jsx-props-no-spreading */} + <AuthProvider {...oidcConfig}> + <App /> + </AuthProvider> + </StrictMode> ) } else { - root.render(<App />) + root.render( + <StrictMode> + <App /> + </StrictMode> + ) } diff --git a/frontend/src/ui/card-table/FilterTableInput.tsx b/frontend/src/ui/card-table/FilterTableInput.tsx index 808d48881b..876e4bc3ac 100644 --- a/frontend/src/ui/card-table/FilterTableInput.tsx +++ b/frontend/src/ui/card-table/FilterTableInput.tsx @@ -4,7 +4,7 @@ import { COLORS } from '../../constants/constants' import SearchIconSVG from '../../features/icons/Loupe_dark.svg?react' export const FilterTableInput = styled.input<{ - baseUrl: string + $baseUrl: string }>` background-color: white; border: 1px ${COLORS.lightGray} solid; @@ -14,7 +14,7 @@ export const FilterTableInput = styled.input<{ height: 40px; width: 280px; padding: 0 5px 0 10px; - background-image: url(${p => `${p.baseUrl}${SearchIconSVG}`}); + background-image: url(${p => `${p.$baseUrl}${SearchIconSVG}`}); background-size: 25px; background-position: bottom 3px right 5px; background-repeat: no-repeat;