diff --git a/package.json b/package.json index 8f73721e..6f52932b 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,8 @@ "react-redux": "^9.1.2", "redux": "^5.0.1", "redux-thunk": "^3.1.0", + "terra-draw": "^1.0.0", + "terra-draw-maplibre-gl-adapter": "^1.0.1", "three": "^0.161.0", "topojson-client": "^3.1.0", "uuid": "^9.0.1", diff --git a/src/components/MlFeatureDraw/MlFeatureDraw.stories.tsx b/src/components/MlFeatureDraw/MlFeatureDraw.stories.tsx new file mode 100644 index 00000000..aeaf2274 --- /dev/null +++ b/src/components/MlFeatureDraw/MlFeatureDraw.stories.tsx @@ -0,0 +1,196 @@ +import React from 'react'; +import mapContextDecorator from '../../decorators/MapContextDecorator'; +import MlFeatureDraw from './MlFeatureDraw'; +import Sidebar from '../../ui_components/Sidebar'; +import useFeatureDraw from '../../hooks/useFeatureDraw'; +import { + TerraDrawPointMode, + TerraDrawLineStringMode, + TerraDrawPolygonMode, + TerraDrawRectangleMode, + TerraDrawFreehandMode, + TerraDrawCircleMode, + TerraDrawSelectMode, +} from 'terra-draw'; +import DeleteIcon from '@mui/icons-material/Delete'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import EditIcon from '@mui/icons-material/Edit'; +import { Button, Tooltip } from '@mui/material'; +import DrawIcon from '@mui/icons-material/Draw'; + +const storyoptions = { + title: 'MapComponents/MlFeatureDraw', + component: MlFeatureDraw, + argTypes: { + modeType: { + control: { + type: 'select', + options: ['point', 'linestring', 'polygon', 'rectangle', 'freehand', 'circle'], + }, + defaultValue: 'polygon', + }, + }, + decorators: mapContextDecorator, +}; + +export default storyoptions; + +const Template = (args: { modeType: string }) => { + const modes = React.useMemo(() => { + let baseMode; + let selectConfig; + + switch (args.modeType) { + case 'point': + baseMode = new TerraDrawPointMode(); + selectConfig = { + flags: { + point: { + feature: { draggable: true }, + }, + }, + }; + break; + + case 'linestring': + baseMode = new TerraDrawLineStringMode(); + selectConfig = { + flags: { + linestring: { + feature: { + draggable: true, + coordinates: { + midpoints: true, + draggable: true, + deletable: true, + }, + }, + }, + }, + }; + break; + + case 'polygon': + baseMode = new TerraDrawPolygonMode(); + selectConfig = { + flags: { + polygon: { + feature: { + draggable: true, + rotateable: true, + scaleable: true, + coordinates: { + midpoints: true, + draggable: true, + deletable: true, + }, + }, + }, + }, + }; + break; + + case 'rectangle': + baseMode = new TerraDrawRectangleMode(); + selectConfig = { + flags: { + rectangle: { + feature: { + draggable: true, + rotateable: true, + scaleable: true, + coordinates: { + midpoints: true, + draggable: true, + deletable: true, + }, + }, + }, + }, + }; + break; + + case 'freehand': + baseMode = new TerraDrawFreehandMode(); + selectConfig = { + flags: { + freehand: { + feature: { + draggable: true, + coordinates: { + midpoints: true, + draggable: true, + deletable: true, + }, + }, + }, + }, + }; + break; + + case 'circle': + baseMode = new TerraDrawCircleMode(); + selectConfig = { + flags: { + circle: { + feature: { + draggable: true, + }, + }, + }, + }; + break; + + default: + throw new Error(`Unknown mode type: ${args.modeType}`); + } + + return [baseMode, new TerraDrawSelectMode(selectConfig)]; + }, [args.modeType]); + + const { startDrawing, stopDrawing, clearDrawing, isDrawing } = useFeatureDraw({ + mapId: 'map_1', + mode: modes, + }); + return ( + <> + + + + + + + + + + + ); +}; + +export const DrawPoint = Template.bind({}); +DrawPoint.args = { modeType: 'point' }; + +export const DrawLine = Template.bind({}); +DrawLine.args = { modeType: 'linestring' }; + +export const DrawPolygon = Template.bind({}); +DrawPolygon.args = { modeType: 'polygon' }; + +export const DrawRectangle = Template.bind({}); +DrawRectangle.args = { modeType: 'rectangle' }; + +export const DrawFreehand = Template.bind({}); +DrawFreehand.args = { modeType: 'freehand' }; + +export const DrawCircle = Template.bind({}); +DrawCircle.args = { modeType: 'circle' }; diff --git a/src/components/MlFeatureDraw/MlFeatureDraw.tsx b/src/components/MlFeatureDraw/MlFeatureDraw.tsx new file mode 100644 index 00000000..c5b627e0 --- /dev/null +++ b/src/components/MlFeatureDraw/MlFeatureDraw.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import useFeatureDraw, { useFeatureDrawProps } from '../../hooks/useFeatureDraw'; + +const MlFeatureDraw: React.FC = (props) => { + useFeatureDraw({ + mapId: props.mapId, + mode: props.mode, + }); + + return <>; +}; + +export default MlFeatureDraw; diff --git a/src/hooks/useFeatureDraw.tsx b/src/hooks/useFeatureDraw.tsx new file mode 100644 index 00000000..19eebd80 --- /dev/null +++ b/src/hooks/useFeatureDraw.tsx @@ -0,0 +1,97 @@ +import useMap from './useMap'; +import { useEffect, useRef, useState } from 'react'; +import { TerraDraw } from 'terra-draw'; +import { TerraDrawMapLibreGLAdapter } from 'terra-draw-maplibre-gl-adapter'; +import { TerraDrawBaseDrawMode } from 'terra-draw/dist/modes/base.mode'; + +export interface useFeatureDrawProps { + /** + * Id of the target MapLibre instance in mapContext + */ + mapId?: string; + /** + * Id of an existing layer in the mapLibre instance to help specify the layer order + * This layer will be visually beneath the layer with the "insertBeforeLayer" id. + */ + insertBeforeLayer?: string; + /** + * drawing mode + */ + mode: TerraDrawBaseDrawMode[]; +} + +const useFeatureDraw = (props: useFeatureDrawProps) => { + const draw = useRef(null); + const [isDrawing, setIsDrawing] = useState(false); + const mapHook = useMap({ + mapId: props.mapId, + waitForLayer: props.insertBeforeLayer, + }); + + const cleanup = () => { + if (draw.current) { + draw.current.stop(); + draw.current = null; + } + setIsDrawing(false); + }; + + const initializeDraw = () => { + if (!mapHook.map) return; + cleanup(); + draw.current = new TerraDraw({ + adapter: new TerraDrawMapLibreGLAdapter(mapHook.map), + modes: props.mode, + }); + }; + + useEffect(() => { + initializeDraw(); + return () => { + cleanup(); + }; + }, [mapHook.map, props.mode]); + + const setMode = (mode: string): void => { + if (!draw.current) return; + + try { + draw.current.setMode(mode); + setIsDrawing(mode !== 'select'); + } catch (error) { + console.error('Error setting mode:', error); + setIsDrawing(false); + } + }; + + const startDrawing = (mode: string) => { + if (!draw.current) initializeDraw(); + if (!draw.current) return; + + try { + if (!draw.current.enabled) { + draw.current.start(); + } + setMode(mode); + } catch (error) { + console.error('Error starting drawing:', error); + cleanup(); + } + }; + + const stopDrawing = (): void => { + if (draw.current) { + console.log('select mode'); + setMode('select'); + } + }; + + const clearDrawing = (): void => { + if (draw.current) { + draw.current.clear(); + } + }; + + return { startDrawing, stopDrawing, clearDrawing, isDrawing }; +}; +export default useFeatureDraw; diff --git a/yarn.lock b/yarn.lock index f3f81b3a..4205dd38 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15239,6 +15239,16 @@ tempy@^1.0.1: type-fest "^0.16.0" unique-string "^2.0.0" +terra-draw-maplibre-gl-adapter@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/terra-draw-maplibre-gl-adapter/-/terra-draw-maplibre-gl-adapter-1.0.1.tgz#1b85fbe3c50836e8e8b0bae796cc1ce018ebcca4" + integrity sha512-B5zM6OhOEwcYegNLP2HybOc+V0ZaQNYuSkOiAcGABV6ZVd5zZCX51GIXcAgOL07okcs1D+7a+4zCgqtD/fLgTg== + +terra-draw@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/terra-draw/-/terra-draw-1.0.0.tgz#ffd6339a8644ed66e589457bc22a670381ea95cd" + integrity sha512-LMD5wLHHSfkXOX0eGjhUV/Pnxedn5MvsKBvGOP6txCY1D1LAIVnIx1g+trTXfBHUjogDJj/vmswsGMD/5LtNKQ== + terser-webpack-plugin@^5.3.1, terser-webpack-plugin@^5.3.10: version "5.3.10" resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz"