Skip to content

Commit

Permalink
Merge pull request #98 from hotosm/feat/drone-operator-lock-task
Browse files Browse the repository at this point in the history
Feat/drone operator lock task
  • Loading branch information
nrjadkry authored Jul 25, 2024
2 parents f68ca14 + 93ebd43 commit b6781b3
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 23 deletions.
14 changes: 14 additions & 0 deletions src/frontend/src/api/projects.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable import/prefer-default-export */
import { UseQueryOptions, useQuery } from '@tanstack/react-query';
import { getProjectsList, getProjectDetail } from '@Services/createproject';
import { getTaskStates } from '@Services/project';
import { getUserProfileInfo } from '@Services/common';

export const useGetProjectsListQuery = (
Expand All @@ -27,6 +28,19 @@ export const useGetProjectsDetailQuery = (
});
};

export const useGetTaskStatesQuery = (
projectId: string,
queryOptions?: Partial<UseQueryOptions>,
) => {
return useQuery({
queryKey: ['project-task-states'],
queryFn: () => getTaskStates(projectId),
select: (res: any) => res.data,
enabled: !!projectId,
...queryOptions,
});
};

export const useGetUserDetailsQuery = (
queryOptions?: Partial<UseQueryOptions>,
) => {
Expand Down
105 changes: 90 additions & 15 deletions src/frontend/src/components/IndividualProject/MapSection/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { useEffect } from 'react';
import { useTypedSelector } from '@Store/hooks';
/* eslint-disable no-unused-vars */
import { useParams } from 'react-router-dom';
import { useCallback, useEffect, useState } from 'react';
import { useTypedSelector, useTypedDispatch } from '@Store/hooks';
import { useMapLibreGLMap } from '@Components/common/MapLibreComponents';
import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer';
import MapContainer from '@Components/common/MapLibreComponents/MapContainer';
import BaseLayerSwitcher from '@Components/common/MapLibreComponents/BaseLayerSwitcher';
import { LngLatBoundsLike, Map } from 'maplibre-gl';
import { GeojsonType } from '@Components/common/MapLibreComponents/types';
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';

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

const [tasksBoundaryLayer, setTasksBoundaryLayer] = useState<Record<
string,
any
> | null>(null);

const { map, isMapLoaded } = useMapLibreGLMap({
mapOptions: {
zoom: 5,
Expand All @@ -18,31 +33,82 @@ export default function MapSection() {
disableRotation: true,
});

const tasksGeojson = useTypedSelector(state => state.project.tasksGeojson);
const projectArea = useTypedSelector(state => state.project.projectArea);
const selectedTaskId = useTypedSelector(
state => state.project.selectedTaskId,
);
const tasksData = useTypedSelector(state => state.project.tasksData);

const { data: taskStates } = useGetTaskStatesQuery(id as string, {
enabled: !!tasksData,
});

// create combined geojson from individual tasks from the API
useEffect(() => {
if (!map || !tasksData) return;

// @ts-ignore
const taskStatus: Record<string, any> = taskStates?.reduce(
(acc: Record<string, any>, task: Record<string, any>) => {
acc[task.task_id] = task.state;
return acc;
},
{},
);
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]);

// zoom to layer in the project area
useEffect(() => {
if (!projectArea) return;
const bbox = getBbox(projectArea as FeatureCollection);
if (!tasksBoundaryLayer) return;
const bbox = getBbox(tasksBoundaryLayer as FeatureCollection);
map?.fitBounds(bbox as LngLatBoundsLike, { padding: 25 });
}, [map, projectArea]);
}, [map, tasksBoundaryLayer]);

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>) => {
return <h6>This task is available for mapping</h6>;
}, []);

return (
<MapContainer
map={map}
isMapLoaded={isMapLoaded}
style={{
width: '100%',
height: '582px',
height: '100%',
}}
>
{tasksGeojson?.map(singleTask => (
{tasksBoundaryLayer && (
<VectorLayer
map={map as Map}
key={singleTask.id}
id={singleTask.id}
visibleOnMap={!!singleTask?.outline_geojson}
geojson={singleTask?.outline_geojson}
id="tasks-layer"
visibleOnMap={!!tasksBoundaryLayer}
geojson={tasksBoundaryLayer as GeojsonType}
interactions={['feature']}
layerOptions={{
type: 'fill',
Expand All @@ -53,7 +119,16 @@ export default function MapSection() {
},
}}
/>
))}
)}
<AsyncPopup
map={map as Map}
popupUI={getPopupUI}
title={`Task #${selectedTaskId}`}
fetchPopupData={(properties: Record<string, any>) => {
dispatch(setProjectState({ selectedTaskId: properties.id }));
}}
buttonText="Lock Task"
/>
<BaseLayerSwitcher />
</MapContainer>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'react';
import { renderToString } from 'react-dom/server';
import { Popup } from 'maplibre-gl';
import type { MapMouseEvent } from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import '@Components/common/MapLibreComponents/map.css';
import { Button } from '@Components/RadixComponents/Button';
import Skeleton from '@Components/RadixComponents/Skeleton';
import { IAsyncPopup } from '../types';
Expand All @@ -21,6 +23,7 @@ export default function AsyncPopup({
handleBtnClick,
isLoading = false,
onClose,
buttonText = 'View More',
}: IAsyncPopup) {
const [properties, setProperties] = useState<Record<string, any> | null>(
null,
Expand Down Expand Up @@ -70,7 +73,7 @@ export default function AsyncPopup({
{isLoading ? (
<Skeleton className="naxatw-my-3 naxatw-h-4 naxatw-w-1/2 naxatw-rounded-md naxatw-bg-grey-100 naxatw-shadow-sm" />
) : (
<p className="naxatw-btn-text naxatw-text-primary-400">{title}</p>
<p className="naxatw-text-body-btn naxatw-text-red">{title}</p>
)}
<span
role="button"
Expand All @@ -84,13 +87,13 @@ export default function AsyncPopup({
</div>
<div dangerouslySetInnerHTML={{ __html: popupHTML }} />
{!isLoading && (
<div className="naxatw-p-3">
<div className="naxatw-flex naxatw-items-center naxatw-p-3">
<Button
variant="ghost"
className="naxatw-mx-auto naxatw-bg-red naxatw-font-primary naxatw-text-white"
size="sm"
onClick={() => handleBtnClick?.(properties)}
>
View More
{buttonText}
</Button>
</div>
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function PopupUI({ data = {} }: IPopupUIProps) {
);

return (
<ul className="scrollbar naxatw-flex naxatw-h-[12.5rem] naxatw-flex-col naxatw-overflow-y-auto naxatw-border-y-[1px] naxatw-border-y-grey-500 naxatw-text-grey-800">
<ul className="scrollbar naxatw-flex naxatw-h-[2rem] naxatw-flex-col naxatw-overflow-y-auto naxatw-border-y-[1px] naxatw-border-y-grey-500 naxatw-text-grey-800">
{popupData &&
Object.keys(popupData).map(key => (
<li
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface IAsyncPopup {
handleBtnClick?: (properties: Record<string, any>) => void;
isLoading?: boolean;
onClose?: () => void;
buttonText?: string;
}

export type DrawModeTypes = DrawMode | null | undefined;
Expand Down
11 changes: 11 additions & 0 deletions src/frontend/src/services/project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable import/prefer-default-export */
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);
6 changes: 4 additions & 2 deletions src/frontend/src/store/slices/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ import persist from '@Store/persist';

export interface ProjectState {
individualProjectActiveTab: string;
tasksGeojson: Record<string, any>[] | null;
tasksData: Record<string, any>[] | null;
projectArea: Record<string, any> | null;
selectedTaskId: string;
}

const initialState: ProjectState = {
individualProjectActiveTab: 'tasks',
tasksGeojson: null,
tasksData: null,
projectArea: null,
selectedTaskId: '',
};

const setProjectState: CaseReducer<
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/src/views/IndividualProject/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function IndividualProject() {
onSuccess: (res: any) =>
dispatch(
setProjectState({
tasksGeojson: res.tasks,
tasksData: res.tasks,
projectArea: res.outline_geojson,
}),
),
Expand Down

0 comments on commit b6781b3

Please sign in to comment.