-
+
void;
+}) {
+ const dispatch = useTypedDispatch();
-export default function MapSection() {
const { map, isMapLoaded } = useMapLibreGLMap({
mapOptions: {
zoom: 5,
@@ -19,18 +29,42 @@ export default function MapSection() {
disableRotation: true,
});
- const uploadedProjectArea = useTypedSelector(
- state => state.createproject.uploadedProjectArea,
+ const drawProjectAreaEnable = useTypedSelector(
+ state => state.createproject.drawProjectAreaEnable,
+ );
+ const drawNoFlyZoneEnable = useTypedSelector(
+ state => state.createproject.drawNoFlyZoneEnable,
);
- const uploadedNoFlyZone = useTypedSelector(
- state => state.createproject.uploadedNoFlyZone,
+
+ const handleDrawEnd = (geojson: GeojsonType | null) => {
+ if (drawProjectAreaEnable) {
+ dispatch(setCreateProjectState({ drawnProjectArea: geojson }));
+ }
+ dispatch(setCreateProjectState({ drawnNoFlyZone: geojson }));
+ };
+
+ const { resetDraw } = useDrawTool({
+ map,
+ enable: drawProjectAreaEnable || drawNoFlyZoneEnable,
+ drawMode: 'draw_polygon',
+ styles: drawStyles,
+ onDrawEnd: handleDrawEnd,
+ });
+
+ useEffect(() => {
+ onResetButtonClick(resetDraw);
+ }, [onResetButtonClick, resetDraw]);
+
+ const projectArea = useTypedSelector(
+ state => state.createproject.projectArea,
);
+ const noFlyZone = useTypedSelector(state => state.createproject.noFlyZone);
useEffect(() => {
- if (!uploadedProjectArea) return;
- const bbox = getBbox(uploadedProjectArea as FeatureCollection);
+ if (!projectArea) return;
+ const bbox = getBbox(projectArea as FeatureCollection);
map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 });
- }, [map, uploadedProjectArea]);
+ }, [map, projectArea]);
return (
state.createproject.uploadedProjectArea,
- );
- const uploadedNoFlyZone = useTypedSelector(
- state => state.createproject.uploadedNoFlyZone,
+ const [resetDrawTool, setResetDrawTool] = useState void)>(null);
+
+ const projectArea = useTypedSelector(
+ state => state.createproject.projectArea,
);
+ const noFlyZone = useTypedSelector(state => state.createproject.noFlyZone);
const isNoflyzonePresent = useTypedSelector(
state => state.createproject.isNoflyzonePresent,
);
+ const drawProjectAreaEnable = useTypedSelector(
+ state => state.createproject.drawProjectAreaEnable,
+ );
+ const drawNoFlyZoneEnable = useTypedSelector(
+ state => state.createproject.drawNoFlyZoneEnable,
+ );
+ const drawnProjectArea = useTypedSelector(
+ state => state.createproject.drawnProjectArea,
+ );
+ const drawnNoFlyZone = useTypedSelector(
+ state => state.createproject.drawnNoFlyZone,
+ );
+
+ const handleResetButtonClick = useCallback((resetFunction: any) => {
+ setResetDrawTool(() => resetFunction);
+ }, []);
+
+ const handleDrawProjectAreaClick = () => {
+ if (!drawProjectAreaEnable) {
+ dispatch(setCreateProjectState({ drawProjectAreaEnable: true }));
+ return;
+ }
+ const drawnArea =
+ drawnProjectArea && area(drawnProjectArea as FeatureCollection);
+ if (drawnArea && drawnArea > 1000000) {
+ toast.error('Drawn Area should not exceed 100km²');
+ dispatch(
+ setCreateProjectState({
+ drawProjectAreaEnable: false,
+ drawnProjectArea: null,
+ }),
+ );
+ // @ts-ignore
+ resetDrawTool();
+ return;
+ }
+ dispatch(
+ setCreateProjectState({
+ projectArea: drawnProjectArea,
+ drawProjectAreaEnable: false,
+ }),
+ );
+ setValue('outline_geojson', drawnProjectArea);
+ if (resetDrawTool) {
+ resetDrawTool();
+ }
+ };
+
+ const handleDrawNoFlyZoneClick = () => {
+ if (!drawNoFlyZoneEnable) {
+ dispatch(setCreateProjectState({ drawNoFlyZoneEnable: true }));
+ return;
+ }
+ const drawnNoFlyZoneArea =
+ drawnProjectArea && area(drawnNoFlyZone as FeatureCollection);
+ if (drawnNoFlyZoneArea && drawnNoFlyZoneArea > 1000000) {
+ toast.error('Drawn Area should not exceed 100km²');
+ dispatch(
+ setCreateProjectState({
+ drawNoFlyZoneEnable: false,
+ drawnNoFlyZone: null,
+ }),
+ );
+ // @ts-ignore
+ resetDrawTool();
+ return;
+ }
+ dispatch(
+ setCreateProjectState({
+ noFlyZone: drawnNoFlyZone,
+ drawNoFlyZoneEnable: false,
+ }),
+ );
+ setValue('outline_no_fly_zones', drawnNoFlyZone);
+ if (resetDrawTool) {
+ resetDrawTool();
+ }
+ };
- const projectArea =
- uploadedProjectArea && area(uploadedProjectArea as FeatureCollection);
- const noFlyZoneArea =
- uploadedNoFlyZone && area(uploadedNoFlyZone as FeatureCollection);
+ const totalProjectArea =
+ projectArea && area(projectArea as FeatureCollection);
+ const noFlyZoneArea = noFlyZone && area(noFlyZone as FeatureCollection);
const handleProjectAreaFileChange = (file: Record[]) => {
if (!file) return;
@@ -46,9 +129,7 @@ export default function DefineAOI({
geojson.then(z => {
if (typeof z === 'object' && !Array.isArray(z) && z !== null) {
const convertedGeojson = flatten(z);
- dispatch(
- setCreateProjectState({ uploadedProjectArea: convertedGeojson }),
- );
+ dispatch(setCreateProjectState({ projectArea: convertedGeojson }));
setValue('outline_geojson', convertedGeojson);
}
});
@@ -65,9 +146,7 @@ export default function DefineAOI({
geojson.then(z => {
if (typeof z === 'object' && !Array.isArray(z) && z !== null) {
const convertedGeojson = flatten(z);
- dispatch(
- setCreateProjectState({ uploadedNoFlyZone: convertedGeojson }),
- );
+ dispatch(setCreateProjectState({ noFlyZone: convertedGeojson }));
setValue('outline_no_fly_zones', convertedGeojson);
}
});
@@ -82,45 +161,68 @@ export default function DefineAOI({
-
Project Area
- {!uploadedProjectArea ? (
+
+ Project Area *
+
+ {!projectArea ? (
<>
-
-
-
- or
-
+
+
+ {drawnProjectArea && (
+ {
+ dispatch(
+ setCreateProjectState({ drawnProjectArea: null }),
+ );
+ if (resetDrawTool) {
+ resetDrawTool();
+ }
+ }}
+ />
+ )}
-
- (
-
+
+
+ or
+
+
+
+ (
+
+ )}
/>
- )}
- />
-
-
+
+
+ >
+ )}
>
) : (
<>
@@ -129,15 +231,13 @@ export default function DefineAOI({
className="naxatw-mt-2 naxatw-border naxatw-border-red naxatw-text-red"
rightIcon="restart_alt"
onClick={() => {
- dispatch(
- setCreateProjectState({ uploadedProjectArea: null }),
- );
+ dispatch(resetUploadedAndDrawnAreas());
}}
>
Reset Project Area
- Total Area: {Math.trunc(projectArea as number)} m2
+ Total Area: {Math.trunc(totalProjectArea as number)} m²
{isNoflyzonePresent === 'yes' && (
- {uploadedNoFlyZone ? (
+ {noFlyZone ? (
<>
@@ -214,7 +339,7 @@ export default function DefineAOI({
)}
-
+
diff --git a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx
index d80266a2..44d192e6 100644
--- a/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx
+++ b/src/frontend/src/components/CreateProject/FormContents/GenerateTask/MapSection/index.tsx
@@ -19,18 +19,18 @@ export default function MapSection() {
disableRotation: true,
});
- const uploadedProjectArea = useTypedSelector(
- state => state.createproject.uploadedProjectArea,
+ const projectArea = useTypedSelector(
+ state => state.createproject.projectArea,
);
const splitGeojson = useTypedSelector(
state => state.createproject.splitGeojson,
);
useEffect(() => {
- if (!uploadedProjectArea) return;
- const bbox = getBbox(uploadedProjectArea as FeatureCollection);
+ if (!projectArea) return;
+ const bbox = getBbox(projectArea as FeatureCollection);
map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 });
- }, [map, uploadedProjectArea]);
+ }, [map, projectArea]);
return (
state.createproject.uploadedProjectArea,
+ const projectArea = useTypedSelector(
+ state => state.createproject.projectArea,
);
const geojsonFile =
- !!uploadedProjectArea && convertGeojsonToFile(uploadedProjectArea);
+ !!projectArea && convertGeojsonToFile(projectArea as Record);
const payload = prepareFormData({ project_geojson: geojsonFile, dimension });
@@ -40,7 +40,7 @@ export default function GenerateTask({ formProps }: { formProps: any }) {
-
+
{
- if (!uploadedProjectArea) return;
+ if (!projectArea) return;
mutate(payload);
}}
>
diff --git a/src/frontend/src/components/GoogleAuth/index.tsx b/src/frontend/src/components/GoogleAuth/index.tsx
index 0d8c38b4..7d9d60a7 100644
--- a/src/frontend/src/components/GoogleAuth/index.tsx
+++ b/src/frontend/src/components/GoogleAuth/index.tsx
@@ -29,6 +29,7 @@ function GoogleAuth() {
const response = await fetch(callbackUrl, { credentials: 'include' });
const token = await response.json();
localStorage.setItem('token', token.access_token);
+ localStorage.setItem('refresh', token.refresh_token);
// fetch user details
const response2 = await fetch(userDetailsUrl, {
diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx
index 247586f8..615c2c67 100644
--- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx
+++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx
@@ -43,6 +43,7 @@ export default function MapSection() {
id={singleTask.id}
visibleOnMap={!!singleTask?.outline_geojson}
geojson={singleTask?.outline_geojson}
+ interactions={['feature']}
layerOptions={{
type: 'fill',
paint: {
diff --git a/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx b/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx
index 8e05d2a1..291ddce0 100644
--- a/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx
+++ b/src/frontend/src/components/LandingPage/ClientsAndPartners/index.tsx
@@ -2,6 +2,8 @@ import Image from '@Components/RadixComponents/Image';
import { motion } from 'framer-motion';
import worldBankLogo from '@Assets/images/LandingPage/WorldbankLogo.png';
import { fadeUpVariant } from '@Constants/animations';
+import gfdrrLogo from '@Assets/images/GFDRR-logo.png';
+import { FlexRow } from '@Components/common/Layouts';
export default function ClientAndPartners() {
return (
@@ -25,7 +27,10 @@ export default function ClientAndPartners() {
transition={{ duration: 0.7 }}
viewport={{ once: true }}
>
-
+
+
+
+
diff --git a/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts b/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts
index b5098b37..d752c670 100644
--- a/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts
+++ b/src/frontend/src/components/common/MapLibreComponents/Layers/VectorLayer.ts
@@ -1,4 +1,6 @@
-import { useEffect, useMemo } from 'react';
+/* eslint-disable no-param-reassign */
+import { useEffect, useMemo, useRef } from 'react';
+import { MapMouseEvent } from 'maplibre-gl';
import { IVectorLayer } from '../types';
export default function VectorLayer({
@@ -6,10 +8,17 @@ export default function VectorLayer({
id,
geojson,
isMapLoaded,
+ interactions = [],
layerOptions,
+ onFeatureSelect,
visibleOnMap = true,
}: IVectorLayer) {
const sourceId = useMemo(() => id.toString(), [id]);
+ const hasInteractions = useRef(false);
+
+ useEffect(() => {
+ hasInteractions.current = !!interactions.length;
+ }, [interactions]);
useEffect(() => {
if (!map || !isMapLoaded || !geojson) return;
@@ -38,6 +47,42 @@ export default function VectorLayer({
}
}, [map, isMapLoaded, visibleOnMap, sourceId, geojson]); // eslint-disable-line
+ // change cursor to pointer on feature hover
+ useEffect(() => {
+ if (!map) return () => {};
+ function onMouseOver() {
+ if (!map || !hasInteractions.current) return;
+ map.getCanvas().style.cursor = 'pointer';
+ }
+ function onMouseLeave() {
+ if (!map || !hasInteractions.current) return;
+ map.getCanvas().style.cursor = '';
+ }
+ map.on('mouseover', sourceId, onMouseOver);
+ map.on('mouseleave', sourceId, onMouseLeave);
+ // remove event handlers on unmount
+ return () => {
+ map.off('mouseover', onMouseOver);
+ map.off('mouseleave', onMouseLeave);
+ };
+ }, [map, sourceId]);
+
+ // add select interaction & return properties on feature select
+ useEffect(() => {
+ if (!map || !interactions.includes('feature')) return () => {};
+ function handleSelectInteraction(event: MapMouseEvent) {
+ if (!map) return;
+ map.getCanvas().style.cursor = 'pointer';
+ // @ts-ignore
+ const { features } = event;
+ if (!features?.length) return;
+ const { properties, layer } = features[0];
+ onFeatureSelect?.({ ...properties, layer: layer?.id });
+ }
+ map.on('click', sourceId, handleSelectInteraction);
+ return () => map.off('click', sourceId, handleSelectInteraction);
+ }, [map, interactions, sourceId, onFeatureSelect]);
+
useEffect(
() => () => {
if (map?.getSource(sourceId)) {
diff --git a/src/frontend/src/components/common/MapLibreComponents/helpers/reverseLineString.ts b/src/frontend/src/components/common/MapLibreComponents/helpers/reverseLineString.ts
new file mode 100644
index 00000000..5e799a72
--- /dev/null
+++ b/src/frontend/src/components/common/MapLibreComponents/helpers/reverseLineString.ts
@@ -0,0 +1,17 @@
+export default function reverseLineString(geojson: any) {
+ const geometry = geojson?.features ? geojson.features[0].geometry : geojson;
+ if (!geometry) return geojson;
+ return {
+ type: 'FeatureCollection',
+ features: [
+ {
+ type: 'Feature',
+ properties: {},
+ geometry: {
+ ...geometry,
+ coordinates: [...geometry.coordinates].reverse(),
+ },
+ },
+ ],
+ };
+}
diff --git a/src/frontend/src/components/common/MapLibreComponents/types/index.ts b/src/frontend/src/components/common/MapLibreComponents/types/index.ts
index 32d52417..3250e895 100644
--- a/src/frontend/src/components/common/MapLibreComponents/types/index.ts
+++ b/src/frontend/src/components/common/MapLibreComponents/types/index.ts
@@ -42,7 +42,9 @@ export interface ILayer {
export type GeojsonType = GeoJsonTypes | FeatureCollection | Feature;
export interface IVectorLayer extends ILayer {
- geojson: GeojsonType;
+ geojson: GeojsonType | null;
+ interactions?: string[];
+ onFeatureSelect?: (properties: Record) => void;
}
type InteractionsType = 'hover' | 'select';
diff --git a/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts b/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts
index 6bc2bb34..9c262a77 100644
--- a/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts
+++ b/src/frontend/src/components/common/MapLibreComponents/useDrawTool/index.ts
@@ -1,12 +1,28 @@
-import { useCallback, useEffect, useMemo } from 'react';
+/* eslint-disable no-param-reassign */
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { Popup } from 'maplibre-gl';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import StaticMode from '@mapbox/mapbox-gl-draw-static-mode';
+import CutLineMode from 'mapbox-gl-draw-cut-line-mode';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
+import DirectionArrow from '@Assets/images/navigation-image.png';
import { DrawModeTypes, IUseDrawToolProps } from '../types';
+import reverseLineString from '../helpers/reverseLineString';
const { modes } = MapboxDraw;
// @ts-ignore
modes.static = StaticMode;
+// @ts-ignore
+modes.cut_line = CutLineMode;
+
+const popup = new Popup({
+ closeButton: false,
+ closeOnClick: false,
+ className: 'map-tooltip',
+ offset: 12,
+});
+
+const lineStringTypes = ['LineString', 'MultiLineString'];
export default function useDrawTool({
map,
@@ -16,32 +32,41 @@ export default function useDrawTool({
geojson,
onDrawEnd,
}: IUseDrawToolProps) {
+ const [isFeatureSelected, setIsFeatureSelected] = useState(false);
+ const [isDrawLayerAdded, setIsDrawLayerAdded] = useState(false);
+ const [drawStates, setDrawStates] = useState([]);
+ const [redoStates, setRedoStates] = useState([]);
+
+ // create draw instance
const draw = useMemo(
() =>
new MapboxDraw({
displayControlsDefault: false,
- // controls: {
- // polygon: true,
- // trash: true,
- // },
styles,
defaultMode: 'draw_polygon',
// @ts-ignore
modes,
+ drawControl: true,
}),
[], // eslint-disable-line
);
+ // check if draw layer is added to map
useEffect(() => {
- if (!map) return;
- if (!enable || !drawMode) {
- // @ts-ignore
- if (map.hasControl(draw)) {
- // @ts-ignore
- map.removeControl(draw);
- }
- return;
+ if (!map) return () => {};
+ function handleSourceDataAdd(e: any) {
+ if (e.sourceId !== 'mapbox-gl-draw-cold') return;
+ setIsDrawLayerAdded(true);
}
+ map.on('sourcedata', handleSourceDataAdd);
+ return () => {
+ map.off('sourcedata', handleSourceDataAdd);
+ };
+ }, [map]);
+
+ // add control to map & geojson to draw
+ useEffect(() => {
+ if (!map || !enable || !drawMode) return;
// @ts-ignore
if (!map.hasControl(draw)) {
// @ts-ignore
@@ -52,26 +77,17 @@ export default function useDrawTool({
if (geojson) {
// @ts-ignore
draw.add(geojson);
- draw.changeMode('static');
}
}
}, [map, draw, enable, drawMode, geojson]);
- // useEffect(() => {
- // if (!enable || !drawMode) return;
- // // @ts-ignore
- // if (map?.hasControl(draw)) {
- // // @ts-ignore
- // draw.changeMode(drawMode);
- // }
- // }, [map, enable, draw, drawMode]);
-
+ // draw event listener
useEffect(() => {
- if (!map) return () => {};
+ if (!map || !enable) return () => {};
function handleDrawEnd() {
const data = draw.getAll();
onDrawEnd(data);
- // draw.changeMode('static');
+ setDrawStates(prev => [...prev, data]);
}
map.on('draw.create', handleDrawEnd);
map.on('draw.delete', handleDrawEnd);
@@ -83,12 +99,155 @@ export default function useDrawTool({
map.off('draw.update', handleDrawEnd);
map.off('draw.resetDraw', handleDrawEnd);
};
- }, [map, draw, onDrawEnd]);
+ }, [map, draw, enable, onDrawEnd]);
+
+ useEffect(() => {
+ if (!map || !enable) return () => {};
+ function handleDrawEnd() {
+ const selectedIds = draw.getSelectedIds();
+ setIsFeatureSelected(!!selectedIds.length);
+ }
+ map.on('draw.selectionchange', handleDrawEnd);
+ return () => {
+ map.off('draw.selectionchange', handleDrawEnd);
+ };
+ }, [map, enable, draw]);
+
+ // add start/end circle marker to lineStringTypes
+ useEffect(() => {
+ if (!map || !geojson || !enable || !isDrawLayerAdded || isFeatureSelected)
+ return () => {};
+ const featureCollection = draw.getAll();
+ const { geometry } = featureCollection.features[0];
+ if (!lineStringTypes.includes(geometry.type)) return () => {};
+ // @ts-ignore
+ const coordinates = featureCollection.features[0].geometry?.coordinates;
+ const firstCoords = coordinates[0];
+ const lastCoords = coordinates[coordinates.length - 1];
+ map.addSource('line-start-point', {
+ type: 'geojson',
+ data: {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: firstCoords,
+ },
+ },
+ });
+ map.addSource('line-end-point', {
+ type: 'geojson',
+ data: {
+ type: 'Feature',
+ geometry: {
+ type: 'Point',
+ coordinates: lastCoords,
+ },
+ },
+ });
+ map.addLayer({
+ id: 'line-start-point',
+ type: 'circle',
+ source: 'line-start-point',
+ paint: {
+ 'circle-radius': 6,
+ 'circle-color': '#0088F8',
+ },
+ });
+ map.addLayer({
+ id: 'line-end-point',
+ type: 'circle',
+ source: 'line-end-point',
+ paint: {
+ 'circle-radius': 6,
+ 'circle-color': '#e55e5e',
+ },
+ });
+ return () => {
+ map.removeLayer('line-start-point');
+ map.removeLayer('line-end-point');
+ map.removeSource('line-start-point');
+ map.removeSource('line-end-point');
+ };
+ }, [map, draw, geojson, enable, isDrawLayerAdded, isFeatureSelected]);
+
+ // add direction arrow to lineStringTypes
+ useEffect(() => {
+ if (!map || !enable || !geojson || !isDrawLayerAdded || isFeatureSelected)
+ return () => {};
+ const featureCollection = draw.getAll();
+ const { geometry } = featureCollection.features[0];
+ if (!lineStringTypes.includes(geometry.type)) return () => {};
+ map.loadImage(DirectionArrow, (err, image) => {
+ if (err) return;
+ if (map.getLayer('arrowId')) return;
+ // @ts-ignore
+ map.addImage('arrow', image);
+ map.addLayer({
+ id: 'arrowId',
+ type: 'symbol',
+ source: 'mapbox-gl-draw-cold',
+ layout: {
+ 'symbol-placement': 'line',
+ 'symbol-spacing': 100,
+ 'icon-allow-overlap': false,
+ 'icon-image': 'arrow',
+ 'icon-size': 0.5,
+ visibility: 'visible',
+ 'icon-rotate': 90,
+ },
+ });
+ });
+ return () => {
+ if (map.getLayer('arrowId')) {
+ map.removeImage('arrow');
+ map.removeLayer('arrowId');
+ }
+ };
+ }, [map, draw, geojson, enable, isDrawLayerAdded, isFeatureSelected]);
+
+ // add tooltip before draw start
+ useEffect(() => {
+ if (!map || !drawMode?.includes('draw') || isDrawLayerAdded)
+ return () => {};
+ const handleMouseMove = (e: any) => {
+ map.getCanvas().style.cursor = 'crosshair';
+ const description = 'Click to start drawing shape';
+ popup.setLngLat(e.lngLat).setHTML(description).addTo(map);
+ };
+ map.on('mousemove', handleMouseMove);
+ return () => {
+ map.off('mousemove', handleMouseMove);
+ map.getCanvas().style.cursor = '';
+ popup.remove();
+ };
+ }, [map, drawMode, isDrawLayerAdded]);
+
+ // remove draw control on unmount
+ useEffect(() => {
+ if (!map) return () => {};
+ return () => {
+ // @ts-ignore
+ if (map.hasControl(draw)) {
+ // @ts-ignore
+ map.removeControl(draw);
+ setIsDrawLayerAdded(false);
+ setIsFeatureSelected(false);
+ setDrawStates([]);
+ setRedoStates([]);
+ }
+ };
+ }, [map, draw, enable, drawMode, geojson]);
+ // reset draw function
const resetDraw = useCallback(() => {
if (!map) return;
// @ts-ignore
if (map.hasControl(draw)) {
+ // remove arrow layer before removing control
+ if (map.getLayer('arrowId')) {
+ map.removeImage('arrow');
+ map.removeLayer('arrowId');
+ }
// @ts-ignore
map.removeControl(draw);
}
@@ -99,15 +258,18 @@ export default function useDrawTool({
// @ts-ignore
if (geojson) {
draw.changeMode('static');
+ // setIsDrawLayerAdded(true);
} else {
// @ts-ignore
draw.changeMode(drawMode);
}
}
onDrawEnd(null);
- // draw.changeMode('static');
+ setDrawStates([]);
+ setIsDrawLayerAdded(false);
}, [map, draw, drawMode, geojson]); // eslint-disable-line
+ // set draw mode
const setDrawMode = useCallback(
(mode: DrawModeTypes) => {
if (!map || !enable || !mode) {
@@ -133,5 +295,88 @@ export default function useDrawTool({
[map, draw, enable, geojson, drawMode],
);
- return { draw, resetDraw, setDrawMode };
+ // console.log(geojson, 'geojson');
+
+ // Function to undo the last drawn coordinate
+ const undo = useCallback(() => {
+ if (drawStates.length <= 1) {
+ const lastLine = drawStates[drawStates.length - 1];
+ if (lastLine) {
+ const { coordinates } = lastLine.features[0].geometry;
+ if (coordinates.length > 1) {
+ const updatedCoordinates = coordinates.slice(0, -1); // Remove the last coordinate
+ const updatedLine = {
+ ...lastLine,
+ features: [
+ {
+ ...lastLine.features[0],
+ geometry: {
+ ...lastLine.features[0].geometry,
+ coordinates: updatedCoordinates,
+ },
+ },
+ ],
+ };
+ setRedoStates([...redoStates, lastLine]); // Track the undone state for redo
+ setDrawStates(prev => [...prev.slice(0, -1), updatedLine]); // Update the line history with the modified line
+ draw.delete(lastLine.features[0].id); // Delete the last drawn line from the map
+ draw.add(updatedLine); // Add the updated line back to the map
+ onDrawEnd(updatedLine);
+ } else {
+ setRedoStates([...redoStates, lastLine]); // Track the undone state for redo
+ setDrawStates(prev => prev.slice(0, -1)); // Remove the line from the line history
+ draw.delete(lastLine.features[0].id); // Delete the last drawn line from the map
+ }
+ }
+ } else {
+ const nextStates = drawStates.slice(0, -1);
+ setRedoStates([...redoStates, drawStates[drawStates.length - 1]]); // Track the undone state for redo
+ if (drawStates.length >= 1) {
+ draw.deleteAll();
+ }
+ const currentState = nextStates[nextStates.length - 1];
+ draw.add(currentState);
+ onDrawEnd(currentState);
+ setDrawStates(nextStates);
+ }
+ }, [drawStates, draw, onDrawEnd, redoStates]);
+
+ const redo = useCallback(() => {
+ const nextState = redoStates[redoStates.length - 1];
+ if (nextState) {
+ setDrawStates(prev => [...prev, nextState]); // Add the next state to drawStates
+ setRedoStates(prev => prev.slice(0, -1)); // Remove the next state from redoStates
+ draw.deleteAll();
+ draw.add(nextState);
+ onDrawEnd(nextState);
+ }
+ }, [redoStates, draw, onDrawEnd]);
+
+ // useEffect to handle undo and redo events
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ undo();
+ } else if (e.key === 'y' && (e.ctrlKey || e.metaKey)) {
+ e.preventDefault();
+ redo();
+ }
+ };
+ window.addEventListener('keydown', handleKeyDown);
+ return () => {
+ window.removeEventListener('keydown', handleKeyDown);
+ };
+ }, [undo, redo]);
+
+ // reverse line geometry
+ const reverseLineGeometry = useCallback(() => {
+ const reversedLineString = reverseLineString(draw.getAll());
+ draw.set(reversedLineString);
+ onDrawEnd(reversedLineString);
+ setIsFeatureSelected(false);
+ setIsDrawLayerAdded(false);
+ }, [draw, onDrawEnd]);
+
+ return { draw, resetDraw, setDrawMode, reverseLineGeometry, undo, redo };
}
diff --git a/src/frontend/src/constants/modalContents.tsx b/src/frontend/src/constants/modalContents.tsx
index 9db7ffd0..76cd412a 100644
--- a/src/frontend/src/constants/modalContents.tsx
+++ b/src/frontend/src/constants/modalContents.tsx
@@ -1,6 +1,10 @@
import { ReactElement } from 'react';
+import ExitCreateProjectModal from '@Components/CreateProject/ExitCreateProjectModal';
-export type ModalContentsType = 'sign-up-success' | null;
+export type ModalContentsType =
+ | 'sign-up-success'
+ | 'quit-create-project'
+ | null;
export type PromptDialogContentsType = 'delete-layer' | null;
type ModalReturnType = {
@@ -17,6 +21,11 @@ export function getModalContent(content: ModalContentsType): ModalReturnType {
title: '',
content: <>>,
};
+ case 'quit-create-project':
+ return {
+ title: 'Unsaved Changes!',
+ content: ,
+ };
default:
return {
title: '',
diff --git a/src/frontend/src/react-app-env.d.ts b/src/frontend/src/react-app-env.d.ts
index 1b4a201b..b2d3851d 100644
--- a/src/frontend/src/react-app-env.d.ts
+++ b/src/frontend/src/react-app-env.d.ts
@@ -4,3 +4,4 @@ declare module '*.jpeg';
declare module '*.jpg';
declare module 'uuid';
declare module '@mapbox/mapbox-gl-draw-static-mode';
+declare module 'mapbox-gl-draw-cut-line-mode';
diff --git a/src/frontend/src/store/actions/createproject.ts b/src/frontend/src/store/actions/createproject.ts
index afac1353..78a71935 100644
--- a/src/frontend/src/store/actions/createproject.ts
+++ b/src/frontend/src/store/actions/createproject.ts
@@ -1,4 +1,5 @@
/* eslint-disable import/prefer-default-export */
import { createProjectSlice } from '@Store/slices/createproject';
-export const { setCreateProjectState } = createProjectSlice.actions;
+export const { setCreateProjectState, resetUploadedAndDrawnAreas } =
+ createProjectSlice.actions;
diff --git a/src/frontend/src/store/slices/createproject.ts b/src/frontend/src/store/slices/createproject.ts
index d8c61ab4..794feb39 100644
--- a/src/frontend/src/store/slices/createproject.ts
+++ b/src/frontend/src/store/slices/createproject.ts
@@ -1,3 +1,4 @@
+import { GeojsonType } from '@Components/common/MapLibreComponents/types';
import { createSlice } from '@reduxjs/toolkit';
import type { CaseReducer, PayloadAction } from '@reduxjs/toolkit';
import persist from '@Store/persist';
@@ -9,8 +10,12 @@ export interface CreateProjectState {
contributionsOption: 'public' | 'invite_with_email';
generateTaskOption: 'divide_hexagon' | 'divide_rectangle';
isNoflyzonePresent: 'yes' | 'no';
- uploadedProjectArea: Record | null;
- uploadedNoFlyZone: Record | null;
+ projectArea: GeojsonType | null;
+ noFlyZone: GeojsonType | null;
+ drawProjectAreaEnable: boolean;
+ drawNoFlyZoneEnable: boolean;
+ drawnProjectArea: GeojsonType | null;
+ drawnNoFlyZone: GeojsonType | null;
splitGeojson: Record | null;
isTerrainFollow: string;
}
@@ -22,25 +27,41 @@ const initialState: CreateProjectState = {
contributionsOption: 'public',
generateTaskOption: 'divide_rectangle',
isNoflyzonePresent: 'no',
- uploadedProjectArea: null,
- uploadedNoFlyZone: null,
+ projectArea: null,
+ noFlyZone: null,
+ drawProjectAreaEnable: false,
+ drawNoFlyZoneEnable: false,
+ drawnProjectArea: null,
+ drawnNoFlyZone: null,
splitGeojson: null,
isTerrainFollow: 'flat',
};
const setCreateProjectState: CaseReducer<
CreateProjectState,
- PayloadAction>>
+ PayloadAction>
> = (state, action) => ({
...state,
...action.payload,
});
+const resetUploadedAndDrawnAreas: CaseReducer = state => ({
+ ...state,
+ isNoflyzonePresent: initialState.isNoflyzonePresent,
+ projectArea: initialState.projectArea,
+ noFlyZone: initialState.noFlyZone,
+ drawProjectAreaEnable: initialState.drawProjectAreaEnable,
+ drawNoFlyZoneEnable: initialState.drawNoFlyZoneEnable,
+ drawnProjectArea: initialState.drawnProjectArea,
+ drawnNoFlyZone: initialState.drawnNoFlyZone,
+});
+
const createProjectSlice = createSlice({
name: 'create project',
initialState,
reducers: {
setCreateProjectState,
+ resetUploadedAndDrawnAreas,
},
});