Skip to content

Commit

Permalink
Merge pull request #105 from hotosm/feat/cluster-projects
Browse files Browse the repository at this point in the history
Feat/cluster projects
  • Loading branch information
nrjadkry authored Jul 30, 2024
2 parents aea4cf0 + 273ad54 commit 91fcce3
Show file tree
Hide file tree
Showing 7 changed files with 300 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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({
Expand All @@ -58,23 +78,54 @@ export default function MapSection({
const projectArea = useTypedSelector(
state => state.createproject.projectArea,
);
const noFlyZone = useTypedSelector(state => state.createproject.noFlyZone);

useEffect(() => {
if (!projectArea) return;
const bbox = getBbox(projectArea as FeatureCollection);
map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 });
}, [map, projectArea]);

const drawSaveFromMap = () => {
if (drawProjectAreaEnable) {
handleDrawProjectAreaClick();
} else {
resetDraw();
}
};

return (
<MapContainer
map={map}
isMapLoaded={isMapLoaded}
style={{
width: '100%',
height: '448px',
position: 'relative',
}}
>
{(drawNoFlyZoneEnable || drawProjectAreaEnable) && (
<div className="naxatw-absolute naxatw-right-[calc(50%_-_75px)] naxatw-top-2 naxatw-z-50 naxatw-flex naxatw-h-9 naxatw-w-[150px] naxatw-rounded-lg naxatw-bg-white">
<div className="naxatw-flex naxatw-w-full naxatw-items-center naxatw-justify-evenly">
<i
className="material-icons-outlined naxatw-w-full naxatw-cursor-pointer naxatw-rounded-l-md naxatw-border-r naxatw-text-center hover:naxatw-bg-gray-100"
role="presentation"
onClick={() => drawSaveFromMap()}
>
save
</i>
<i
className="material-icons-outlined naxatw-w-full naxatw-cursor-pointer naxatw-text-center hover:naxatw-bg-gray-100"
role="presentation"
onClick={() => {
dispatch(setCreateProjectState({ drawnNoFlyZone: noFlyZone }));
resetDraw();
}}
>
restart_alt
</i>
</div>
</div>
)}
<VectorLayer
map={map as Map}
isMapLoaded={isMapLoaded}
Expand Down Expand Up @@ -105,6 +156,23 @@ export default function MapSection({
},
}}
/>

<VectorLayer
map={map as Map}
isMapLoaded={isMapLoaded}
id="uploaded-no-fly-zone-ongoing"
geojson={drawnNoFlyZone as GeojsonType}
visibleOnMap={!!drawnNoFlyZone}
layerOptions={{
type: 'fill',
paint: {
'fill-color': '#404040',
'fill-outline-color': '#D33A38',
'fill-opacity': 0.3,
},
}}
/>

<BaseLayerSwitcher />
</MapContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -124,9 +126,10 @@ export default function DefineAOI({

const handleProjectAreaFileChange = (file: Record<string, any>[]) => {
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 }));
Expand All @@ -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<string, any>[]) => {
if (!file) return;
const geojson = validateGeoJSON(file[0]?.file);
Expand Down Expand Up @@ -206,16 +236,20 @@ export default function DefineAOI({
rules={{
required: 'Project Area is Required',
}}
render={({ field: { value } }) => (
<FileUpload
name="outline_geojson"
data={value}
onChange={handleProjectAreaFileChange}
fileAccept=".geojson, .kml"
placeholder="Upload project area (zipped shapefile, geojson or kml files)"
{...formProps}
/>
)}
render={({ field: { value } }) => {
// console.log(value, 'value12');
return (
<FileUpload
name="outline_geojson"
data={value}
onChange={handleProjectAreaFileChange}
fileAccept=".geojson, .kml"
placeholder="Upload project area (zipped shapefile, geojson or kml files)"
isValid={validateAreaOfFileUpload}
{...formProps}
/>
);
}}
/>
<ErrorMessage
message={errors?.outline_geojson?.message as string}
Expand Down Expand Up @@ -264,6 +298,8 @@ export default function DefineAOI({
dispatch(
setCreateProjectState({
noFlyZone: null,
drawnNoFlyZone: null,
drawNoFlyZoneEnable: false,
}),
)
}
Expand Down Expand Up @@ -322,6 +358,7 @@ export default function DefineAOI({
name="outline_no_fly_zones"
data={value}
onChange={handleNoFlyZoneFileChange}
isValid={validateAreaOfFileUpload}
fileAccept=".geojson, .kml"
placeholder="Upload project area (zipped shapefile, geojson or kml files)"
{...formProps}
Expand All @@ -339,7 +376,10 @@ export default function DefineAOI({
)}
</div>
<div className="naxatw-col-span-2 naxatw-overflow-hidden naxatw-rounded-md naxatw-border naxatw-border-[#F3C6C6]">
<MapSection onResetButtonClick={handleResetButtonClick} />
<MapSection
onResetButtonClick={handleResetButtonClick}
handleDrawProjectAreaClick={handleDrawProjectAreaClick}
/>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Loading

0 comments on commit 91fcce3

Please sign in to comment.