diff --git a/.DS_Store b/.DS_Store index f5afdda1..aa4e7171 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/client/package.json b/client/package.json index 9d575e32..9970e75e 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "@dnd-kit/utilities": "3.2.0", "@heroicons/react": "2.1.3", "@hookform/resolvers": "^3.1.1", + "@mapbox/mapbox-gl-draw": "^1.4.3", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.4", diff --git a/client/src/components/map/draw/index.tsx b/client/src/components/map/draw/index.tsx new file mode 100644 index 00000000..4a07e980 --- /dev/null +++ b/client/src/components/map/draw/index.tsx @@ -0,0 +1,167 @@ +import MapboxDraw, { DrawModeChangeEvent } from '@mapbox/mapbox-gl-draw'; +import { useControl, ControlPosition } from 'react-map-gl'; +import { useEffect } from 'react'; + +import { Layer } from 'mapbox-gl'; + +const DRAWING_STYLES: Layer[] = [ + // ACTIVE (being drawn) + // line stroke + { + id: 'gl-draw-line', + type: 'line', + filter: ['all', ['==', '$type', 'LineString'], ['!=', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#00857F', + 'line-width': 3, + }, + }, + // polygon fill + { + id: 'gl-draw-polygon-fill', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], + paint: { + 'fill-color': 'transparent', + 'fill-outline-color': '#00857F', + 'fill-opacity': 0.1, + }, + }, + // polygon outline stroke + // This doesn't style the first edge of the polygon, which uses the line stroke styling instead + { + id: 'gl-draw-polygon-stroke-active', + type: 'line', + filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#00857F', + 'line-width': 3, + }, + }, + // vertex point halos + { + id: 'gl-draw-polygon-and-line-vertex-halo-active', + type: 'circle', + filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], + paint: { + 'circle-radius': 5, + 'circle-color': 'white', + 'circle-stroke-width': 0.5, + 'circle-stroke-color': 'hsla(0, 0%, 0%, 0.15)', + }, + }, + // vertex points + { + id: 'gl-draw-polygon-and-line-vertex-active', + type: 'circle', + filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], + paint: { + 'circle-radius': 5, + 'circle-color': 'white', + 'circle-stroke-width': 0.5, + 'circle-stroke-color': 'hsla(0, 0%, 0%, 0.15)', + }, + }, + + // INACTIVE (static, already drawn) + // line stroke + { + id: 'gl-draw-line-static', + type: 'line', + filter: ['all', ['==', '$type', 'LineString'], ['==', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#000', + 'line-width': 3, + }, + }, + // polygon fill + { + id: 'gl-draw-polygon-fill-static', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']], + paint: { + 'fill-color': '#000', + 'fill-outline-color': '#000', + 'fill-opacity': 0.1, + }, + }, + // polygon outline + { + id: 'gl-draw-polygon-stroke-static', + type: 'line', + filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#000', + 'line-width': 3, + }, + }, +] satisfies Layer[]; + +type DrawControlProps = ConstructorParameters[0] & { + position?: ControlPosition; + onCreate?: (event: { features: GeoJSON.Feature[] }) => void; + onUpdate?: (event: { features: GeoJSON.Feature[]; action: string }) => void; + onDelete?: (event: { features: GeoJSON.Feature[] }) => void; + onModeChange?: (event: DrawModeChangeEvent) => void; + displayControlsDefault?: boolean; + customPolygon?: GeoJSON.FeatureCollection; + onSetCustomPolygon?: (customPolygon) => void; + styles?: typeof DRAWING_STYLES; +}; + +export const MapDraw = (props: DrawControlProps) => { + console.log(props, 'map props'); + const drawRef = useControl( + () => new MapboxDraw(props), + ({ map }) => { + props.onCreate && map.on('draw.create', props.onCreate); + props.onUpdate && map.on('draw.update', props.onUpdate); + props.onDelete && map.on('draw.delete', props.onDelete); + props.onModeChange && map.on('draw.modechange', props.onModeChange); + }, + + { + position: props.position, + } + ); + + const { onSetCustomPolygon, customPolygon } = props; + +// useEffect(() => { +// if (!customPolygon) { +// console.log({ customPolygon }, 'customPolygon iside useEffect'); +// drawRef.changeMode('draw_polygon'); +// } +// }, [drawRef, customPolygon]); + + useEffect(() => { + if (!drawRef) return null; + if (customPolygon) { + drawRef.add(customPolygon); + + if (onSetCustomPolygon) { + onSetCustomPolygon(customPolygon); + } + } + }, [onSetCustomPolygon, customPolygon, drawRef]); + + return null; +}; + +export default MapDraw; diff --git a/client/src/containers/map/index.tsx b/client/src/containers/map/index.tsx index 46aac44b..421b6147 100644 --- a/client/src/containers/map/index.tsx +++ b/client/src/containers/map/index.tsx @@ -1,8 +1,13 @@ 'use client'; +import MapboxDraw, { DrawModeChangeEvent } from '@mapbox/mapbox-gl-draw'; +import { ControlPosition } from 'react-map-gl'; + +import { Layer } from 'mapbox-gl'; + import { useCallback, useMemo, useState } from 'react'; -import { LngLatBoundsLike, MapLayerMouseEvent, useMap, Marker } from 'react-map-gl'; +import { LngLatBoundsLike, MapLayerMouseEvent, useMap, Marker, useControl } from 'react-map-gl'; import dynamic from 'next/dynamic'; import Image from 'next/image'; @@ -27,9 +32,10 @@ import { DEFAULT_BBOX } from '@/components/map/constants'; import Controls from '@/components/map/controls'; import SettingsControl from '@/components/map/controls/settings'; import ZoomControl from '@/components/map/controls/zoom'; +import MapDraw from '@/components/map/draw'; import { CustomMapProps } from '@/components/map/types'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; - +import "@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css"; const LayerManager = dynamic(() => import('@/containers/map/layer-manager'), { ssr: false, }); @@ -38,6 +44,127 @@ const Legend = dynamic(() => import('@/containers/map/legend'), { ssr: false, }); +const DRAWING_STYLES: Layer[] = [ + // ACTIVE (being drawn) + // line stroke + { + id: 'gl-draw-line', + type: 'line', + filter: ['all', ['==', '$type', 'LineString'], ['!=', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#00857F', + 'line-width': 3, + }, + }, + // polygon fill + { + id: 'gl-draw-polygon-fill', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], + paint: { + 'fill-color': 'transparent', + 'fill-outline-color': '#00857F', + 'fill-opacity': 0.1, + }, + }, + // polygon outline stroke + // This doesn't style the first edge of the polygon, which uses the line stroke styling instead + { + id: 'gl-draw-polygon-stroke-active', + type: 'line', + filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#00857F', + 'line-width': 3, + }, + }, + // vertex point halos + { + id: 'gl-draw-polygon-and-line-vertex-halo-active', + type: 'circle', + filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], + paint: { + 'circle-radius': 5, + 'circle-color': 'white', + 'circle-stroke-width': 0.5, + 'circle-stroke-color': 'hsla(0, 0%, 0%, 0.15)', + }, + }, + // vertex points + { + id: 'gl-draw-polygon-and-line-vertex-active', + type: 'circle', + filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']], + paint: { + 'circle-radius': 5, + 'circle-color': 'white', + 'circle-stroke-width': 0.5, + 'circle-stroke-color': 'hsla(0, 0%, 0%, 0.15)', + }, + }, + + // INACTIVE (static, already drawn) + // line stroke + { + id: 'gl-draw-line-static', + type: 'line', + filter: ['all', ['==', '$type', 'LineString'], ['==', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#000', + 'line-width': 3, + }, + }, + // polygon fill + { + id: 'gl-draw-polygon-fill-static', + type: 'fill', + filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']], + paint: { + 'fill-color': '#000', + 'fill-outline-color': '#000', + 'fill-opacity': 0.1, + }, + }, + // polygon outline + { + id: 'gl-draw-polygon-stroke-static', + type: 'line', + filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']], + layout: { + 'line-cap': 'round', + 'line-join': 'round', + }, + paint: { + 'line-color': '#000', + 'line-width': 3, + }, + }, +] satisfies Layer[]; + +type DrawControlProps = ConstructorParameters[0] & { + position?: ControlPosition; + onCreate?: (event: { features: GeoJSON.Feature[] }) => void; + onUpdate?: (event: { features: GeoJSON.Feature[]; action: string }) => void; + onDelete?: (event: { features: GeoJSON.Feature[] }) => void; + onModeChange?: (event: DrawModeChangeEvent) => void; + displayControlsDefault?: boolean; + customPolygon?: GeoJSON.FeatureCollection; + onSetCustomPolygon?: (customPolygon) => void; + styles?: typeof DRAWING_STYLES; +}; + const DEFAULT_PROPS: CustomMapProps = { id: 'default', initialViewState: { @@ -58,6 +185,14 @@ const INITIAL_PROJECTS_POPUP = { info: null, }; +function DrawControl(props: DrawControlProps) { + useControl(() => new MapboxDraw(props), { + position: props.position, + }); + + return null; +} + export default function MapContainer() { const { id, initialViewState, minZoom, maxZoom } = DEFAULT_PROPS; const queryParams = useSyncQueryParams(); @@ -260,6 +395,11 @@ export default function MapContainer() { [setCursor, map, hoveredStateIdProjectsCircle, hoveredStateIdProjectsFill] ); + const handleUserDrawing = (event) => { + console.log(event, 'event'); + return event + } + return (
+ + {/* */} {locationPopUp.popup && (