diff --git a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx index 3b7db7d9..68ce3b48 100644 --- a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/MapSection/index.tsx @@ -15,8 +15,10 @@ import { setCreateProjectState } from '@Store/actions/createproject'; export default function MapSection({ onResetButtonClick, + handleDrawProjectAreaClick, }: { onResetButtonClick: (reset: any) => void; + handleDrawProjectAreaClick: any; }) { const dispatch = useTypedDispatch(); @@ -35,12 +37,30 @@ export default function MapSection({ const drawNoFlyZoneEnable = useTypedSelector( state => state.createproject.drawNoFlyZoneEnable, ); + const drawnNoFlyZone = useTypedSelector( + state => state.createproject.drawnNoFlyZone, + ); + const noFlyZone = useTypedSelector(state => state.createproject.noFlyZone); const handleDrawEnd = (geojson: GeojsonType | null) => { + if (!geojson) return; if (drawProjectAreaEnable) { dispatch(setCreateProjectState({ drawnProjectArea: geojson })); + } else { + const collectiveGeojson: any = drawnNoFlyZone + ? { + // @ts-ignore + ...drawnNoFlyZone, + features: [ + // @ts-ignore + ...(drawnNoFlyZone?.features || []), + // @ts-ignore + ...(geojson?.features || []), + ], + } + : geojson; + dispatch(setCreateProjectState({ drawnNoFlyZone: collectiveGeojson })); } - dispatch(setCreateProjectState({ drawnNoFlyZone: geojson })); }; const { resetDraw } = useDrawTool({ @@ -58,7 +78,6 @@ export default function MapSection({ const projectArea = useTypedSelector( state => state.createproject.projectArea, ); - const noFlyZone = useTypedSelector(state => state.createproject.noFlyZone); useEffect(() => { if (!projectArea) return; @@ -66,6 +85,14 @@ export default function MapSection({ map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 }); }, [map, projectArea]); + const drawSaveFromMap = () => { + if (drawProjectAreaEnable) { + handleDrawProjectAreaClick(); + } else { + resetDraw(); + } + }; + return ( + {(drawNoFlyZoneEnable || drawProjectAreaEnable) && ( +
+
+ drawSaveFromMap()} + > + save + + { + dispatch(setCreateProjectState({ drawnNoFlyZone: noFlyZone })); + resetDraw(); + }} + > + restart_alt + +
+
+ )} + + +
); diff --git a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx index f16da752..7e8498c7 100644 --- a/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx +++ b/src/frontend/src/components/CreateProject/FormContents/DefineAOI/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable consistent-return */ import { useCallback, useState } from 'react'; import { useTypedDispatch, useTypedSelector } from '@Store/hooks'; import { Controller } from 'react-hook-form'; @@ -63,7 +64,7 @@ export default function DefineAOI({ } const drawnArea = drawnProjectArea && area(drawnProjectArea as FeatureCollection); - if (drawnArea && drawnArea > 1000000) { + if (drawnArea && drawnArea > 100000000) { toast.error('Drawn Area should not exceed 100km²'); dispatch( setCreateProjectState({ @@ -92,6 +93,7 @@ export default function DefineAOI({ dispatch(setCreateProjectState({ drawNoFlyZoneEnable: true })); return; } + if (!drawnNoFlyZone) return; const drawnNoFlyZoneArea = drawnProjectArea && area(drawnNoFlyZone as FeatureCollection); if (drawnNoFlyZoneArea && drawnNoFlyZoneArea > 100000000) { @@ -124,9 +126,10 @@ export default function DefineAOI({ const handleProjectAreaFileChange = (file: Record[]) => { if (!file) return; - const geojson = validateGeoJSON(file[0]?.file); + const geojson: any = validateGeoJSON(file[0]?.file); + try { - geojson.then(z => { + geojson.then((z: any) => { if (typeof z === 'object' && !Array.isArray(z) && z !== null) { const convertedGeojson = flatten(z); dispatch(setCreateProjectState({ projectArea: convertedGeojson })); @@ -139,6 +142,33 @@ export default function DefineAOI({ } }; + // @ts-ignore + const validateAreaOfFileUpload = async (file: any) => { + try { + if (!file) return; + const geojson: any = await validateGeoJSON(file[0]?.file); + if ( + typeof geojson === 'object' && + !Array.isArray(geojson) && + geojson !== null + ) { + const convertedGeojson = flatten(geojson); + const uploadedArea: any = + convertedGeojson && area(convertedGeojson as FeatureCollection); + if (uploadedArea && uploadedArea > 100000000) { + toast.error('Drawn Area should not exceed 100km²'); + return false; + } + return true; + } + return false; + } catch (err: any) { + // eslint-disable-next-line no-console + console.log(err); + return false; + } + }; + const handleNoFlyZoneFileChange = (file: Record[]) => { if (!file) return; const geojson = validateGeoJSON(file[0]?.file); @@ -206,16 +236,20 @@ export default function DefineAOI({ rules={{ required: 'Project Area is Required', }} - render={({ field: { value } }) => ( - - )} + render={({ field: { value } }) => { + // console.log(value, 'value12'); + return ( + + ); + }} />
- +
diff --git a/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx b/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx new file mode 100644 index 00000000..4361491e --- /dev/null +++ b/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx @@ -0,0 +1,108 @@ +import { useEffect } from 'react'; + +export default function VectorLayerWithCluster({ + map, + visibleOnMap, + mapLoaded, + sourceId, + geojson, +}: any) { + useEffect(() => { + if (!map || !mapLoaded || !visibleOnMap || !sourceId) return; + + !map.getSource(sourceId) && + map.addSource(sourceId, { + type: 'geojson', + data: geojson, + cluster: true, + clusterMaxZoom: 14, + clusterRadius: 40, + }); + + !map.getLayer('clusters') && + map.addLayer({ + id: 'clusters', + type: 'circle', + source: sourceId, + filter: ['has', 'point_count'], + paint: { + 'circle-color': '#D73F3F', + 'circle-radius': 15, + }, + }); + + map.setGlyphs( + 'https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf', + ); + + !map.getLayer('cluster-count') && + map.addLayer({ + id: 'cluster-count', + type: 'symbol', + source: sourceId, + filter: ['has', 'point_count'], + layout: { + 'text-field': '{point_count_abbreviated}', + 'text-size': 12, + }, + paint: { + 'text-color': '#fff', + }, + }); + + map.addLayer({ + id: 'unclustered-point', + type: 'circle', + source: sourceId, + filter: ['!', ['has', 'point_count']], + paint: { + 'circle-color': '#11b4da', + 'circle-radius': 6, + 'circle-stroke-width': 1, + 'circle-stroke-color': '#fff', + }, + layout: {}, + }); + + // inspect a cluster on click + map.on('click', 'clusters', (e: any) => { + const features = map.queryRenderedFeatures(e.point, { + layers: ['clusters'], + }); + const clusterId = features[0].properties.cluster_id; + map + .getSource(sourceId) + .getClusterExpansionZoom(clusterId, (err: any, zoom: any) => { + if (err) return; + map.easeTo({ + center: features[0].geometry.coordinates, + zoom, + }); + }); + }); + + map.on('mouseenter', 'clusters', () => { + map.getCanvas().style.cursor = 'pointer'; + }); + map.on('mouseleave', 'clusters', () => { + map.getCanvas().style.cursor = ''; + }); + + map.on('mouseenter', 'unclustered-point', () => { + map.getCanvas().style.cursor = 'pointer'; + }); + map.on('mouseleave', 'unclustered-point', () => { + map.getCanvas().style.cursor = ''; + }); + + return () => { + if (sourceId) { + if (map.getLayer(sourceId)) { + map.removeLayer(sourceId); + } + } + }; + }, [geojson, map, mapLoaded, sourceId, visibleOnMap]); + + return null; +} diff --git a/src/frontend/src/components/Projects/MapSection/index.tsx b/src/frontend/src/components/Projects/MapSection/index.tsx index b2e5a832..b8ddd07d 100644 --- a/src/frontend/src/components/Projects/MapSection/index.tsx +++ b/src/frontend/src/components/Projects/MapSection/index.tsx @@ -1,6 +1,9 @@ import { useMapLibreGLMap } from '@Components/common/MapLibreComponents'; import MapContainer from '@Components/common/MapLibreComponents/MapContainer'; import BaseLayerSwitcher from '@Components/common/MapLibreComponents/BaseLayerSwitcher'; +import { useGetProjectsListQuery } from '@Api/projects'; +import centroid from '@turf/centroid'; +import VectorLayerWithCluster from './VectorLayerWithCluster'; export default function ProjectsMapSection() { const { map, isMapLoaded } = useMapLibreGLMap({ @@ -12,6 +15,31 @@ export default function ProjectsMapSection() { }, disableRotation: true, }); + const { data: projectsList, isLoading } = useGetProjectsListQuery({ + select: (data: any) => { + // find all polygons centroid and set to geojson save to single geojson + const combinedGeojson = data?.data?.reduce( + (acc: Record, current: Record) => { + return { + ...acc, + features: [...acc.features, centroid(current.outline_geojson)], + }; + }, + { + type: 'FeatureCollection', + features: [], + }, + ); + return combinedGeojson; + }, + }); + + // useEffect(() => { + // if (!projectsList) return; + // const bbox = getBbox(projectsList as FeatureCollection); + // map?.fitBounds(bbox as LngLatBoundsLike, { padding: 30 }); + // }, [projectsList, map]); + return ( + + + {/* */} + ); diff --git a/src/frontend/src/components/RadixComponents/Button.tsx b/src/frontend/src/components/RadixComponents/Button.tsx index c7515bed..390c9eb2 100644 --- a/src/frontend/src/components/RadixComponents/Button.tsx +++ b/src/frontend/src/components/RadixComponents/Button.tsx @@ -84,7 +84,7 @@ function Button({ return ( {leftIcon && ( )} - {children} +
{children}
{rightIcon && ( {}; 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.getCanvas().style.cursor = 'crosshair'; + map.getCanvas().style.cursor = ''; + // 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 = ''; + // map.getCanvas().style.cursor = ''; + map.getCanvas().style.cursor = 'crosshair'; popup.remove(); }; }, [map, drawMode, isDrawLayerAdded]); diff --git a/src/frontend/src/components/common/UploadArea/index.tsx b/src/frontend/src/components/common/UploadArea/index.tsx index 5caa0af2..0e1f4523 100644 --- a/src/frontend/src/components/common/UploadArea/index.tsx +++ b/src/frontend/src/components/common/UploadArea/index.tsx @@ -29,6 +29,9 @@ interface IFileUploadProps extends UseFormPropsType { data?: []; placeholder?: string; onChange?: any; + isValid?: + | ((value: any) => boolean | undefined) + | ((value: any) => Promise); } export default function FileUpload({ @@ -40,6 +43,7 @@ export default function FileUpload({ data, placeholder, onChange, + isValid = () => true, }: IFileUploadProps) { const [inputRef, onFileUpload] = useCustomUpload(); const [uploadedFiles, setUploadedFiles] = useState([]); @@ -66,7 +70,7 @@ export default function FileUpload({ setValue(name, []); }, [register, name, setValue]); - const handleFileUpload = (event: FileEvent) => { + const handleFileUpload = async (event: FileEvent) => { const { files } = event.target; const uploaded = Array.from(files).map(file => ({ id: uuidv4(), @@ -76,6 +80,9 @@ export default function FileUpload({ const uploadedFilesState = multiple ? [...uploadedFiles, ...uploaded] : uploaded; + + const valid = await isValid?.(uploadedFilesState); + if (!valid) return; // @ts-ignore setUploadedFiles(uploadedFilesState); setValue(name, uploadedFilesState, { shouldDirty: true });