Skip to content

Commit

Permalink
Merge pull request #108 from hotosm/feat/task-request-for-mapping
Browse files Browse the repository at this point in the history
Feat: add task request for mapping functionality
  • Loading branch information
nrjadkry authored Aug 1, 2024
2 parents f0b0522 + 08fef72 commit 3c59665
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 63 deletions.
Binary file added src/frontend/src/assets/images/lock.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
177 changes: 121 additions & 56 deletions src/frontend/src/components/IndividualProject/MapSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unused-vars */
import { useParams } from 'react-router-dom';
import { useCallback, useEffect, useState } from 'react';
Expand All @@ -11,15 +12,17 @@ import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup';
import getBbox from '@turf/bbox';
import { FeatureCollection } from 'geojson';
import { LngLatBoundsLike, Map } from 'maplibre-gl';
import PopupUI from '@Components/common/MapLibreComponents/PopupUI';
import { setProjectState } from '@Store/actions/project';
import { useGetTaskStatesQuery } from '@Api/projects';
import DTMLogo from '@Assets/images/lock.png';
import { postTaskStatus } from '@Services/project';
import { useMutation } from '@tanstack/react-query';
import { toast } from 'react-toastify';

export default function MapSection() {
const { id } = useParams();
const dispatch = useTypedDispatch();

const [tasksBoundaryLayer, setTasksBoundaryLayer] = useState<Record<
const [taskStatusObj, setTaskStatusObj] = useState<Record<
string,
any
> | null>(null);
Expand All @@ -42,10 +45,22 @@ export default function MapSection() {
enabled: !!tasksData,
});

// create combined geojson from individual tasks from the API
useEffect(() => {
if (!map || !tasksData) return;
const { mutate: lockTask } = useMutation<any, any, any, unknown>({
mutationFn: postTaskStatus,
onSuccess: (res: any) => {
toast.success('Task Requested for Mapping');
setTaskStatusObj({
...taskStatusObj,
[res.data.task_id]: 'REQUEST_FOR_MAPPING',
});
},
onError: (err: any) => {
toast.error(err.message);
},
});

useEffect(() => {
if (!map || !taskStates) return;
// @ts-ignore
const taskStatus: Record<string, any> = taskStates?.reduce(
(acc: Record<string, any>, task: Record<string, any>) => {
Expand All @@ -54,45 +69,55 @@ export default function MapSection() {
},
{},
);
const features = tasksData?.map(taskObj => {
return {
type: 'Feature',
geometry: { ...taskObj.outline_geojson.geometry },
properties: {
...taskObj.outline_geojson.properties,
state: taskStatus?.[`${taskObj.id}`] || null,
},
};
});
const taskBoundariesFeatcol = {
type: 'FeatureCollection',
SRID: {
type: 'name',
properties: {
name: 'EPSG:3857',
},
},
features,
};
setTasksBoundaryLayer(taskBoundariesFeatcol);
}, [map, taskStates, tasksData]);
setTaskStatusObj(taskStatus);
}, [map, taskStates]);

// zoom to layer in the project area
useEffect(() => {
if (!tasksBoundaryLayer) return;
const bbox = getBbox(tasksBoundaryLayer as FeatureCollection);
if (!tasksData) return;
const tasksCollectiveGeojson = tasksData?.reduce(
(acc, curr) => {
return {
...acc,
features: [...acc.features, curr.outline_geojson],
};
},
{
type: 'FeatureCollection',
features: [],
},
);
const bbox = getBbox(tasksCollectiveGeojson as FeatureCollection);
map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 });
}, [map, tasksBoundaryLayer]);
}, [map, tasksData]);

const getPopUpButtonText = (taskState: string) => {
if (taskState === 'UNLOCKED_FOR_MAP') return 'Request for Mapping';
if (taskState === '') return '';
return 'nothing';
};
const getPopupUI = useCallback(
(properties: Record<string, any>) => {
const status = taskStatusObj?.[properties?.id];
const popupText = (taskStatus: string) => {
switch (taskStatus) {
case 'UNLOCKED_TO_MAP':
return 'This task is available for mapping';
case 'REQUEST_FOR_MAPPING':
return 'This task is Requested for mapping';
case 'LOCKED_FOR_MAPPING':
return 'This task is locked for mapping';
default:
return 'This Task is completed';
}
};
return <h6>{popupText(status)}</h6>;
},
[taskStatusObj],
);

const getPopupUI = useCallback((properties: Record<string, any>) => {
return <h6>This task is available for mapping</h6>;
}, []);
const handleTaskLockClick = () => {
lockTask({
projectId: id,
taskId: selectedTaskId,
data: { event: 'request' },
});
};

return (
<MapContainer
Expand All @@ -103,31 +128,71 @@ export default function MapSection() {
height: '100%',
}}
>
{tasksBoundaryLayer && (
<VectorLayer
map={map as Map}
id="tasks-layer"
visibleOnMap={!!tasksBoundaryLayer}
geojson={tasksBoundaryLayer as GeojsonType}
interactions={['feature']}
layerOptions={{
type: 'fill',
paint: {
'fill-color': '#328ffd',
'fill-outline-color': '#484848',
'fill-opacity': 0.5,
},
}}
/>
)}
{tasksData &&
tasksData?.map((task: Record<string, any>) => {
return (
<VectorLayer
key={task?.id}
map={map as Map}
id={`tasks-layer-${task?.id}-${taskStatusObj?.[task?.id]}`}
visibleOnMap={task?.id && taskStatusObj}
geojson={task.outline_geojson as GeojsonType}
interactions={['feature']}
layerOptions={
taskStatusObj?.[`${task?.id}`] === 'LOCKED_FOR_MAPPING'
? {
type: 'fill',
paint: {
'fill-color': '#98BBC8',
'fill-outline-color': '#484848',
'fill-opacity': 0.6,
},
}
: taskStatusObj?.[`${task?.id}`] === 'REQUEST_FOR_MAPPING'
? {
type: 'fill',
paint: {
'fill-color': '#F3C5C5',
'fill-outline-color': '#484848',
'fill-opacity': 0.7,
},
}
: taskStatusObj?.[`${task?.id}`] === 'UNLOCKED_TO_VALIDATE'
? {
type: 'fill',
paint: {
'fill-color': '#176149',
'fill-outline-color': '#484848',
'fill-opacity': 0.5,
},
}
: {
type: 'fill',
paint: {
'fill-color': '#ffffff',
'fill-outline-color': '#484848',
'fill-opacity': 0.4,
},
}
}
hasImage={
taskStatusObj?.[`${task?.id}`] === 'LOCKED_FOR_MAPPING' || false
}
image={DTMLogo}
/>
);
})}

<AsyncPopup
map={map as Map}
popupUI={getPopupUI}
title={`Task #${selectedTaskId}`}
fetchPopupData={(properties: Record<string, any>) => {
dispatch(setProjectState({ selectedTaskId: properties.id }));
}}
hideButton={taskStatusObj?.[selectedTaskId] !== 'UNLOCKED_TO_MAP'}
buttonText="Lock Task"
handleBtnClick={() => handleTaskLockClick()}
/>
<BaseLayerSwitcher />
</MapContainer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function AsyncPopup({
isLoading = false,
onClose,
buttonText = 'View More',
hideButton = false,
}: IAsyncPopup) {
const [properties, setProperties] = useState<Record<string, any> | null>(
null,
Expand Down Expand Up @@ -68,7 +69,10 @@ export default function AsyncPopup({
if (!properties) return <div />;

return (
<div ref={popupRef} className="naxatw-w-[17.5rem] naxatw-px-3">
<div
ref={popupRef}
className={`naxatw-w-[17.5rem] naxatw-px-3 ${hideButton ? 'naxatw-pb-3' : ''}`}
>
<div className="naxatw-flex naxatw-items-center naxatw-justify-between naxatw-py-2">
{isLoading ? (
<Skeleton className="naxatw-my-3 naxatw-h-4 naxatw-w-1/2 naxatw-rounded-md naxatw-bg-grey-100 naxatw-shadow-sm" />
Expand All @@ -86,7 +90,7 @@ export default function AsyncPopup({
</span>
</div>
<div dangerouslySetInnerHTML={{ __html: popupHTML }} />
{!isLoading && (
{!isLoading && !hideButton && (
<div className="naxatw-flex naxatw-items-center naxatw-p-3">
<Button
className="naxatw-mx-auto naxatw-bg-red naxatw-font-primary naxatw-text-white"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-param-reassign */
import { useEffect, useMemo, useRef } from 'react';
import { MapMouseEvent } from 'maplibre-gl';
import { v4 as uuidv4 } from 'uuid';
import { IVectorLayer } from '../types';

export default function VectorLayer({
Expand All @@ -12,6 +13,8 @@ export default function VectorLayer({
layerOptions,
onFeatureSelect,
visibleOnMap = true,
hasImage = false,
image,
}: IVectorLayer) {
const sourceId = useMemo(() => id.toString(), [id]);
const hasInteractions = useRef(false);
Expand Down Expand Up @@ -42,6 +45,25 @@ export default function VectorLayer({
layout: {},
...layerOptions,
});

if (hasImage) {
const imageId = uuidv4();
map.loadImage(image, (error, img: any) => {
if (error) throw error;
// Add the loaded image to the style's sprite with the ID 'kitten'.
map.addImage(imageId, img);
});
map.addLayer({
id: imageId,
type: 'symbol',
source: sourceId,
layout: {
'icon-image': imageId,
'icon-size': 1,
'icon-overlap': 'always',
},
});
}
} else if (map.getLayer(sourceId)) {
map.removeLayer(sourceId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export interface IVectorLayer extends ILayer {
geojson: GeojsonType | null;
interactions?: string[];
onFeatureSelect?: (properties: Record<string, any>) => void;
hasImage?: boolean;
image: any;
}

type InteractionsType = 'hover' | 'select';
Expand All @@ -65,6 +67,7 @@ export interface IAsyncPopup {
isLoading?: boolean;
onClose?: () => void;
buttonText?: string;
hideButton?: boolean;
}

export type DrawModeTypes = DrawMode | null | undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ export default function useMapLibreGLMap({
...mapOptions,
});
setMap(mapInstance);

mapInstance.on('load', () => {
setIsMapLoaded(true);
});
// return () => mapInstance.setTarget(undefined);
}, []); // eslint-disable-line

Expand Down
11 changes: 6 additions & 5 deletions src/frontend/src/services/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { authenticated, api } from '.';
export const getTaskStates = (projectId: string) =>
api.get(`/tasks/states/${projectId}`);

export const postTaskStatus = (
projectId: string,
taskId: string,
data: Record<string, any>,
) => authenticated(api).post(`/tasks/event/${projectId}/${taskId}`, data);
export const postTaskStatus = (payload: Record<string, any>) => {
const { projectId, taskId, data } = payload;
return authenticated(api).post(`/tasks/event/${projectId}/${taskId}`, data, {
headers: { 'Content-Type': 'application/json' },
});
};

0 comments on commit 3c59665

Please sign in to comment.