diff --git a/src/backend/app/projects/project_routes.py b/src/backend/app/projects/project_routes.py
index 6a5b9bd0..63b29b80 100644
--- a/src/backend/app/projects/project_routes.py
+++ b/src/backend/app/projects/project_routes.py
@@ -1,3 +1,4 @@
+import json
import os
import uuid
from typing import Annotated, Optional
@@ -29,6 +30,7 @@
from app.users.user_deps import login_required
from app.users.user_schemas import AuthUser
from app.tasks import task_schemas
+from app.utils import geojson_to_kml
router = APIRouter(
@@ -55,6 +57,10 @@ async def download_boundaries(
default=False,
description="Whether to split the area or not. Set to True to download task boundaries, otherwise AOI will be downloaded.",
),
+ export_type: str = Query(
+ default="geojson",
+ description="The format of the file to download. Options are 'geojson' or 'kml'.",
+ ),
):
"""Downloads the AOI or task boundaries for a project as a GeoJSON file.
@@ -64,6 +70,7 @@ async def download_boundaries(
user_data (AuthUser): The authenticated user data, checks if the user has permission.
task_id (Optional[UUID]): The task ID in UUID format. If not provided and split_area is True, all tasks will be downloaded.
split_area (bool): Whether to split the area or not. Set to True to download task boundaries, otherwise AOI will be downloaded.
+ export_type (str): The format of the file to download. Can be either 'geojson' or 'kml'.
Returns:
Response: The HTTP response object containing the downloaded file.
@@ -76,17 +83,43 @@ async def download_boundaries(
if out is None:
raise HTTPException(status_code=404, detail="Geometry not found.")
- filename = (
- (f"task_{task_id}.geojson" if task_id else "project_outline.geojson")
- if split_area
- else "project_aoi.geojson"
- )
+ if isinstance(out, str):
+ out = json.loads(out)
+
+ # Convert the geometry to a FeatureCollection if it is a valid GeoJSON geometry
+ if isinstance(out, dict) and "type" in out and "coordinates" in out:
+ out = {
+ "type": "FeatureCollection",
+ "features": [{"type": "Feature", "geometry": out, "properties": {}}],
+ }
+
+ # Determine filename and content-type based on export type
+ if export_type == "geojson":
+ filename = (
+ f"task_{task_id}.geojson" if task_id else "project_outline.geojson"
+ )
+ if not split_area:
+ filename = "project_aoi.geojson"
+ content_type = "application/geo+json"
+ content = json.dumps(out)
+
+ elif export_type == "kml":
+ filename = f"task_{task_id}.kml" if task_id else "project_outline.kml"
+ if not split_area:
+ filename = "project_aoi.kml"
+ content_type = "application/vnd.google-earth.kml+xml"
+ content = geojson_to_kml(out)
+
+ else:
+ raise HTTPException(
+ status_code=400, detail="Invalid export type specified."
+ )
headers = {
"Content-Disposition": f"attachment; filename={filename}",
- "Content-Type": "application/geo+json",
+ "Content-Type": content_type,
}
- return Response(content=out, headers=headers)
+ return Response(content=content.encode("utf-8"), headers=headers)
except HTTPException as e:
log.error(f"Error during boundaries download: {e.detail}")
diff --git a/src/backend/app/tasks/task_schemas.py b/src/backend/app/tasks/task_schemas.py
index 12cba359..596e75b0 100644
--- a/src/backend/app/tasks/task_schemas.py
+++ b/src/backend/app/tasks/task_schemas.py
@@ -149,6 +149,7 @@ class UserTasksStatsOut(BaseModel):
state: str
project_id: uuid.UUID
project_task_index: int
+ project_name: str
@staticmethod
async def get_tasks_by_user(
@@ -160,6 +161,7 @@ async def get_tasks_by_user(
tasks.id AS task_id,
tasks.project_task_index AS project_task_index,
task_events.project_id AS project_id,
+ projects.name AS project_name,
ST_Area(ST_Transform(tasks.outline, 3857)) / 1000000 AS task_area,
task_events.created_at,
CASE
@@ -173,6 +175,8 @@ async def get_tasks_by_user(
task_events
LEFT JOIN
tasks ON task_events.task_id = tasks.id
+ LEFT JOIN
+ projects ON task_events.project_id = projects.id
WHERE
(
%(role)s = 'DRONE_PILOT' AND task_events.user_id = %(user_id)s
diff --git a/src/backend/app/utils.py b/src/backend/app/utils.py
index ae30711f..cc347631 100644
--- a/src/backend/app/utils.py
+++ b/src/backend/app/utils.py
@@ -470,3 +470,60 @@ async def send_reset_password_email(email: str, token: str):
)
except Exception as e:
log.error(f"Error sending email: {e}")
+
+
+def geojson_to_kml(geojson_data: dict) -> str:
+ """
+ Converts GeoJSON data to KML format.
+
+ Args:
+ geojson_data (dict): GeoJSON data as a dictionary.
+
+ Returns:
+ str: KML formatted string.
+ """
+ kml_output = [
+ '',
+ '',
+ "",
+ ]
+
+ # Iterate through each feature in the GeoJSON
+ for feature in geojson_data.get("features", []):
+ geometry_type = feature["geometry"]["type"]
+ coordinates = feature["geometry"]["coordinates"]
+
+ # Create a KML Placemark for each feature
+ kml_output.append("")
+
+ # Add properties as name or description if available
+ if "properties" in feature and feature["properties"]:
+ if "name" in feature["properties"]:
+ kml_output.append(f"{feature['properties']['name']} ")
+ if "description" in feature["properties"]:
+ kml_output.append(
+ f"{feature['properties']['description']} "
+ )
+
+ # Handle different geometry types (Point, LineString, Polygon)
+ if geometry_type == "Point":
+ lon, lat = coordinates
+ kml_output.append(f"{lon},{lat} ")
+ elif geometry_type == "LineString":
+ coord_string = " ".join([f"{lon},{lat}" for lon, lat in coordinates])
+ kml_output.append(
+ f"{coord_string} "
+ )
+ elif geometry_type == "Polygon":
+ for polygon in coordinates:
+ coord_string = " ".join([f"{lon},{lat}" for lon, lat in polygon])
+ kml_output.append(
+ f"{coord_string} "
+ )
+
+ kml_output.append(" ")
+
+ kml_output.append(" ")
+ kml_output.append(" ")
+
+ return "\n".join(kml_output)
diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx
index ebb53b3a..cfef8088 100644
--- a/src/frontend/src/App.tsx
+++ b/src/frontend/src/App.tsx
@@ -19,6 +19,7 @@ import {
getModalContent,
getPromptDialogContent,
} from '@Constants/modalContents';
+import ScrollToTop from '@Components/common/ScrollToTop';
export default function App() {
const dispatch = useTypedDispatch();
@@ -81,13 +82,21 @@ export default function App() {
>
{getPromptDialogContent(promptDialogContent)?.content}
-
- {generateRoutes({
- routes:
- process.env.NODE_ENV !== 'production'
- ? [...testRoutes, ...appRoutes]
- : appRoutes,
- })}
+
+ {generateRoutes({
+ routes:
+ process.env.NODE_ENV !== 'production'
+ ? [...testRoutes, ...appRoutes]
+ : appRoutes,
+ })}
+
+
>
);
diff --git a/src/frontend/src/api/projects.ts b/src/frontend/src/api/projects.ts
index f3c613f1..7720efb1 100644
--- a/src/frontend/src/api/projects.ts
+++ b/src/frontend/src/api/projects.ts
@@ -5,11 +5,12 @@ import { getTaskStates } from '@Services/project';
import { getUserProfileInfo } from '@Services/common';
export const useGetProjectsListQuery = (
+ projectsFilterByOwner: 'yes' | 'no',
queryOptions?: Partial,
) => {
return useQuery({
- queryKey: ['projects-list'],
- queryFn: getProjectsList,
+ queryKey: ['projects-list', projectsFilterByOwner],
+ queryFn: () => getProjectsList(projectsFilterByOwner === 'yes'),
select: (res: any) => res.data,
...queryOptions,
});
diff --git a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx
index 475a9a90..67129049 100644
--- a/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx
+++ b/src/frontend/src/components/CreateProject/CreateprojectLayout/index.tsx
@@ -164,7 +164,7 @@ const CreateprojectLayout = () => {
dispatch(resetUploadedAndDrawnAreas());
},
onError: err => {
- toast.error(err.message);
+ toast.error(err?.response?.data?.detail || err?.message || '');
},
});
diff --git a/src/frontend/src/components/Dashboard/RequestLogs/index.tsx b/src/frontend/src/components/Dashboard/RequestLogs/index.tsx
index 8f53573a..ccdde506 100644
--- a/src/frontend/src/components/Dashboard/RequestLogs/index.tsx
+++ b/src/frontend/src/components/Dashboard/RequestLogs/index.tsx
@@ -55,8 +55,9 @@ const RequestLogs = () => {
className="naxatw-flex naxatw-h-fit naxatw-w-full naxatw-items-center naxatw-justify-between naxatw-rounded-xl naxatw-border naxatw-border-gray-300 naxatw-p-3"
>
- The Task# {task.project_task_index} is requested
- for Mapping
+ The Task# {task.project_task_index} from{' '}
+ {task?.project_name} project is requested for
+ Mapping.
{
-
+
ID
+
+ Project Name
+
Total task area
@@ -33,9 +36,10 @@ const TaskLogsTable = ({ data: taskList }: ITaskLogsTableProps) => {
{taskList?.map(task => (
-
+
Task# {task?.project_task_index}
+ {task?.project_name}
{Number(task?.task_area)?.toFixed(3)}
diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx
index e62a08ea..8b52ea29 100644
--- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx
+++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/DescriptionBox/index.tsx
@@ -1,20 +1,19 @@
+import { useState } from 'react';
import { useParams } from 'react-router-dom';
+import { format } from 'date-fns';
+import { toast } from 'react-toastify';
import {
useGetIndividualTaskQuery,
useGetTaskAssetsInfo,
useGetTaskWaypointQuery,
} from '@Api/tasks';
-import { useState } from 'react';
-// import { useTypedSelector } from '@Store/hooks';
-import { format } from 'date-fns';
+import { Button } from '@Components/RadixComponents/Button';
import DescriptionBoxComponent from './DescriptionComponent';
import QuestionBox from '../QuestionBox';
import UploadsInformation from '../UploadsInformation';
const DescriptionBox = () => {
- // const secondPageStates = useTypedSelector(state => state.droneOperatorTask);
const [flyable, setFlyable] = useState('yes');
- // const { secondPage } = secondPageStates;
const { taskId, projectId } = useParams();
const { data: taskWayPoints }: any = useGetTaskWaypointQuery(
@@ -98,6 +97,32 @@ const DescriptionBox = () => {
},
});
+ const handleDownloadResult = () => {
+ if (!taskAssetsInformation?.assets_url) return;
+
+ fetch(`${taskAssetsInformation?.assets_url}`, { method: 'GET' })
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`Network response was ${response.statusText}`);
+ }
+ return response.blob();
+ })
+ .then(blob => {
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'assets.zip';
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+ })
+ .catch(error =>
+ toast.error(`There wan an error while downloading file
+ ${error}`),
+ );
+ };
+
return (
<>
@@ -109,12 +134,13 @@ const DescriptionBox = () => {
/>
))}
- {/* {!secondPage && } */}
-
+ {taskAssetsInformation?.image_count === 0 && (
+
+ )}
{taskAssetsInformation?.image_count > 0 && (
@@ -130,6 +156,19 @@ const DescriptionBox = () => {
},
]}
/>
+ {taskAssetsInformation?.assets_url && (
+
+ handleDownloadResult()}
+ >
+ Download Result
+
+
+ )}
)}
>
diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/LoadingBox/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/LoadingBox/index.tsx
index 017905be..3911014a 100644
--- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/LoadingBox/index.tsx
+++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/PopoverBox/LoadingBox/index.tsx
@@ -1,5 +1,6 @@
import Icon from '@Components/common/Icon';
import { toggleModal } from '@Store/actions/common';
+import { useQueryClient } from '@tanstack/react-query';
import { useDispatch } from 'react-redux';
interface IFilesUploadingPopOverProps {
@@ -16,12 +17,12 @@ const FilesUploadingPopOver = ({
uploadedFiles,
}: IFilesUploadingPopOverProps) => {
const dispatch = useDispatch();
- // const navigate = useNavigate();
+ const queryClient = useQueryClient();
- // function to close modal
+ // function to close modal and refetch task assets to update the UI
function closeModal() {
+ queryClient.invalidateQueries(['task-assets-info']);
setTimeout(() => {
- // navigate('/dashboard');
dispatch(toggleModal());
}, 2000);
return null;
diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/UploadsInformation/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/UploadsInformation/index.tsx
index 0a86a0e5..8773b675 100644
--- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/UploadsInformation/index.tsx
+++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/UploadsInformation/index.tsx
@@ -1,8 +1,8 @@
const UploadsInformation = ({ data }: { data: Record[] }) => {
return (
<>
-
-
+
+
Upload Information
diff --git a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx
index f49d37d3..8eca20cd 100644
--- a/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx
+++ b/src/frontend/src/components/DroneOperatorTask/DescriptionSection/index.tsx
@@ -22,6 +22,7 @@ const DroneOperatorDescriptionBox = () => {
const [showDownloadOptions, setShowDownloadOptions] =
useState
(false);
const { width } = useWindowDimensions();
+ const Token = localStorage.getItem('token');
const { data: taskDescription }: Record =
useGetIndividualTaskQuery(taskId as string);
@@ -82,7 +83,7 @@ const DroneOperatorDescriptionBox = () => {
}
};
- const handleDownloadFlightPlan = () => {
+ const downloadFlightPlanKmz = () => {
fetch(
`${BASE_URL}/waypoint/task/${taskId}/?project_id=${projectId}&download=true`,
{ method: 'POST' },
@@ -96,7 +97,6 @@ const DroneOperatorDescriptionBox = () => {
.then(blob => {
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
- // link.setAttribute('download', '');
link.href = url;
link.download = 'flight_plan.kmz';
document.body.appendChild(link);
@@ -110,7 +110,7 @@ const DroneOperatorDescriptionBox = () => {
);
};
- const downloadGeojson = () => {
+ const downloadFlightPlanGeojson = () => {
if (!taskWayPoints) return;
const waypointGeojson = {
type: 'FeatureCollection',
@@ -129,6 +129,60 @@ const DroneOperatorDescriptionBox = () => {
window.URL.revokeObjectURL(url);
};
+ const downloadTaskAreaKml = () => {
+ fetch(
+ `${BASE_URL}/projects/${projectId}/download-boundaries?&task_id=${taskId}&split_area=true&export_type=kml`,
+ { method: 'GET', headers: { 'Access-token': Token || '' } },
+ )
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`Network response was ${response.statusText}`);
+ }
+ return response.blob();
+ })
+ .then(blob => {
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'task_area.kml';
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+ })
+ .catch(error =>
+ toast.error(`There wan an error while downloading file
+ ${error}`),
+ );
+ };
+
+ const downloadTaskAreaGeojson = () => {
+ fetch(
+ `${BASE_URL}/projects/${projectId}/download-boundaries?&task_id=${taskId}&split_area=true&export_type=geojson`,
+ { method: 'GET', headers: { 'Access-token': Token || '' } },
+ )
+ .then(response => {
+ if (!response.ok) {
+ throw new Error(`Network response was ${response.statusText}`);
+ }
+ return response.blob();
+ })
+ .then(blob => {
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = 'task_area.geojson';
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+ })
+ .catch(error =>
+ toast.error(`There wan an error while downloading file
+ ${error}`),
+ );
+ };
+
return (
<>
@@ -148,30 +202,54 @@ const DroneOperatorDescriptionBox = () => {
Download
{showDownloadOptions && (
-
+
+
downloadFlightPlanKmz()}
+ onClick={() => {
+ downloadFlightPlanKmz();
+ setShowDownloadOptions(false);
+ }}
+ >
+ Download flight plan as kmz
+
+
downloadFlightPlanGeojson()}
+ onClick={() => {
+ downloadFlightPlanGeojson();
+ setShowDownloadOptions(false);
+ }}
+ >
+ Download flight plan as geojson
+
handleDownloadFlightPlan()}
+ onKeyDown={() => downloadTaskAreaKml()}
onClick={() => {
- handleDownloadFlightPlan();
+ downloadTaskAreaKml();
setShowDownloadOptions(false);
}}
>
- Download flight plan
+ Download task area as kml
downloadGeojson()}
+ onKeyDown={() => downloadTaskAreaGeojson()}
onClick={() => {
- downloadGeojson();
+ downloadTaskAreaGeojson();
setShowDownloadOptions(false);
}}
>
- Download geojson
+ Download task area as geojson
)}
diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/GetCoordinatesOnClick.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/GetCoordinatesOnClick.tsx
index 66659ad5..f3a8252e 100644
--- a/src/frontend/src/components/DroneOperatorTask/MapSection/GetCoordinatesOnClick.tsx
+++ b/src/frontend/src/components/DroneOperatorTask/MapSection/GetCoordinatesOnClick.tsx
@@ -16,13 +16,16 @@ const GetCoordinatesOnClick = ({
useEffect(() => {
if (!map || !isMapLoaded) return () => {};
map.getCanvas().style.cursor = 'crosshair';
- map.on('click', e => {
+
+ const handleClick = (e: any) => {
const latLng = e.lngLat;
getCoordinates(latLng);
- });
+ };
+ map.on('click', handleClick);
return () => {
map.getCanvas().style.cursor = '';
+ map.off('click', handleClick);
};
}, [map, isMapLoaded, getCoordinates]);
return null;
diff --git a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx
index 5666af81..9d12e1ab 100644
--- a/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx
+++ b/src/frontend/src/components/DroneOperatorTask/MapSection/index.tsx
@@ -12,7 +12,10 @@ import { GeojsonType } from '@Components/common/MapLibreComponents/types';
import { Button } from '@Components/RadixComponents/Button';
import { postTaskWaypoint } from '@Services/tasks';
import { toggleModal } from '@Store/actions/common';
-import { setSelectedTakeOffPoint } from '@Store/actions/droneOperatorTask';
+import {
+ setSelectedTakeOffPoint,
+ setSelectedTakeOffPointOption,
+} from '@Store/actions/droneOperatorTask';
import { useTypedSelector } from '@Store/hooks';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import getBbox from '@turf/bbox';
@@ -81,6 +84,7 @@ const MapSection = ({ className }: { className?: string }) => {
return data;
});
dispatch(setSelectedTakeOffPoint(null));
+ dispatch(setSelectedTakeOffPointOption('current_location'));
},
onError: (err: any) => {
toast.error(err?.response?.data?.detail || err.message);
@@ -158,6 +162,7 @@ const MapSection = ({ className }: { className?: string }) => {
useEffect(
() => () => {
dispatch(setSelectedTakeOffPoint(null));
+ dispatch(setSelectedTakeOffPointOption('current_location'));
},
[dispatch],
);
diff --git a/src/frontend/src/components/IndividualProject/MapSection/index.tsx b/src/frontend/src/components/IndividualProject/MapSection/index.tsx
index f6e4bcdb..7e907844 100644
--- a/src/frontend/src/components/IndividualProject/MapSection/index.tsx
+++ b/src/frontend/src/components/IndividualProject/MapSection/index.tsx
@@ -1,30 +1,29 @@
/* eslint-disable no-nested-ternary */
/* eslint-disable no-unused-vars */
-import { useNavigate, 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 { GeojsonType } from '@Components/common/MapLibreComponents/types';
-import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup';
-import getBbox from '@turf/bbox';
-import { FeatureCollection } from 'geojson';
-import { GeolocateControl, LngLatBoundsLike, Map } from 'maplibre-gl';
-import { setProjectState } from '@Store/actions/project';
import {
useGetProjectsDetailQuery,
useGetTaskStatesQuery,
useGetUserDetailsQuery,
} from '@Api/projects';
import lock from '@Assets/images/lock.png';
+import BaseLayerSwitcherUI from '@Components/common/BaseLayerSwitcher';
+import { useMapLibreGLMap } from '@Components/common/MapLibreComponents';
+import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup';
+import VectorLayer from '@Components/common/MapLibreComponents/Layers/VectorLayer';
+import LocateUser from '@Components/common/MapLibreComponents/LocateUser';
+import MapContainer from '@Components/common/MapLibreComponents/MapContainer';
+import { GeojsonType } from '@Components/common/MapLibreComponents/types';
import { postTaskStatus } from '@Services/project';
+import { setProjectState } from '@Store/actions/project';
+import { useTypedDispatch, useTypedSelector } from '@Store/hooks';
import { useMutation } from '@tanstack/react-query';
-import { toast } from 'react-toastify';
+import getBbox from '@turf/bbox';
import hasErrorBoundary from '@Utils/hasErrorBoundary';
-import baseLayersData from '@Components/common/MapLibreComponents/BaseLayerSwitcher/baseLayers';
-import BaseLayerSwitcherUI from '@Components/common/BaseLayerSwitcher';
-import LocateUser from '@Components/common/MapLibreComponents/LocateUser';
+import { FeatureCollection } from 'geojson';
+import { LngLatBoundsLike, Map } from 'maplibre-gl';
+import { useCallback, useEffect, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+import { toast } from 'react-toastify';
import Legend from './Legend';
const MapSection = () => {
@@ -85,6 +84,20 @@ const MapSection = () => {
},
});
+ const { mutate: unLockTask } = useMutation
({
+ mutationFn: postTaskStatus,
+ onSuccess: (res: any) => {
+ setTaskStatusObj({
+ ...taskStatusObj,
+ [res.data.task_id]: 'UNLOCKED_TO_MAP',
+ });
+ toast.success('Task Unlocked Successfully');
+ },
+ onError: (err: any) => {
+ toast.error(err.message);
+ },
+ });
+
useEffect(() => {
if (!map || !taskStates) return;
// @ts-ignore
@@ -149,6 +162,14 @@ const MapSection = () => {
});
};
+ const handleTaskUnLockClick = () => {
+ unLockTask({
+ projectId: id,
+ taskId: selectedTaskId,
+ data: { event: 'unlock' },
+ });
+ };
+
return (
{
? handleTaskLockClick()
: navigate(`/projects/${id}/tasks/${selectedTaskId}`)
}
+ hasSecondaryButton={
+ taskStatusObj?.[selectedTaskId] === 'LOCKED_FOR_MAPPING' &&
+ lockedUser?.id === userDetails?.id
+ }
+ secondaryButtonText="Unlock Task"
+ handleSecondaryBtnClick={() => handleTaskUnLockClick()}
/>
diff --git a/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx b/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx
index 0c4be1a6..19774feb 100644
--- a/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx
+++ b/src/frontend/src/components/Projects/MapSection/VectorLayerWithCluster.tsx
@@ -103,6 +103,15 @@ export default function VectorLayerWithCluster({
if (map.getLayer(sourceId)) {
map.removeLayer(sourceId);
}
+ if (map.getLayer('clusters')) {
+ map.removeLayer('clusters');
+ }
+ if (map.getLayer('unclustered-point')) {
+ map.removeLayer('unclustered-point');
+ }
+ if (map.getLayer('cluster-count')) {
+ map.removeLayer('cluster-count');
+ }
}
};
}, [geojson, map, mapLoaded, sourceId, visibleOnMap]);
diff --git a/src/frontend/src/components/Projects/MapSection/index.tsx b/src/frontend/src/components/Projects/MapSection/index.tsx
index 81c2b14d..1c39273f 100644
--- a/src/frontend/src/components/Projects/MapSection/index.tsx
+++ b/src/frontend/src/components/Projects/MapSection/index.tsx
@@ -1,18 +1,22 @@
+import { useCallback, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
-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 hasErrorBoundary from '@Utils/hasErrorBoundary';
-import centroid from '@turf/centroid';
+import { LngLatBoundsLike, Map } from 'maplibre-gl';
import getBbox from '@turf/bbox';
-import { useCallback, useEffect, useState } from 'react';
+import centroid from '@turf/centroid';
import { FeatureCollection } from 'geojson';
+import { useGetProjectsListQuery } from '@Api/projects';
+import { useTypedSelector } from '@Store/hooks';
+import { useMapLibreGLMap } from '@Components/common/MapLibreComponents';
import AsyncPopup from '@Components/common/MapLibreComponents/AsyncPopup';
-import { LngLatBoundsLike, Map } from 'maplibre-gl';
+import BaseLayerSwitcher from '@Components/common/MapLibreComponents/BaseLayerSwitcher';
+import MapContainer from '@Components/common/MapLibreComponents/MapContainer';
+import hasErrorBoundary from '@Utils/hasErrorBoundary';
import VectorLayerWithCluster from './VectorLayerWithCluster';
const ProjectsMapSection = () => {
+ const projectsFilterByOwner = useTypedSelector(
+ state => state.createproject.ProjectsFilterByOwner,
+ );
const [projectProperties, setProjectProperties] = useState<
Record
>({});
@@ -27,7 +31,7 @@ const ProjectsMapSection = () => {
disableRotation: true,
});
const { data: projectsList, isLoading }: Record =
- useGetProjectsListQuery({
+ useGetProjectsListQuery(projectsFilterByOwner, {
select: (data: any) => {
// find all polygons centroid and set to geojson save to single geojson
const combinedGeojson = data?.data?.reduce(
@@ -78,17 +82,20 @@ const ProjectsMapSection = () => {
style={{
width: '100%',
height: '100%',
+ borderRadius: '8px',
}}
>
-
+ {projectsList && (
+
+ )}
state.common.showMap);
+ const projectsFilterByOwner = useTypedSelector(
+ state => state.createproject.ProjectsFilterByOwner,
+ );
return (
Projects
+
+
+ dispatch(setCreateProjectState({ ProjectsFilterByOwner: value }))
+ }
+ />
+
+
Show map
+
{signedInAs === 'Project Creator' && (
diff --git a/src/frontend/src/components/common/BaseLayerSwitcher/index.tsx b/src/frontend/src/components/common/BaseLayerSwitcher/index.tsx
index 7ca166a2..e54b5685 100644
--- a/src/frontend/src/components/common/BaseLayerSwitcher/index.tsx
+++ b/src/frontend/src/components/common/BaseLayerSwitcher/index.tsx
@@ -1,5 +1,5 @@
-import { useState } from 'react';
import useOutsideClick from '@Hooks/useOutsideClick';
+import { useState } from 'react';
import BaseLayerSwitcher from '../MapLibreComponents/BaseLayerSwitcher';
import baseLayersData from '../MapLibreComponents/BaseLayerSwitcher/baseLayers';
import { MapInstanceType } from '../MapLibreComponents/types';
@@ -27,6 +27,7 @@ const BaseLayerSwitcherUI = ({
handleToggle();
}}
role="presentation"
+ title="Layer Switcher"
>
layers
diff --git a/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx b/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx
index be22f596..f65bf3a4 100644
--- a/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx
+++ b/src/frontend/src/components/common/MapLibreComponents/AsyncPopup/index.tsx
@@ -1,13 +1,13 @@
/* eslint-disable no-unused-vars */
/* eslint-disable react/no-danger */
-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 type { MapMouseEvent } from 'maplibre-gl';
+import { Popup } from 'maplibre-gl';
+import 'maplibre-gl/dist/maplibre-gl.css';
+import { useEffect, useRef, useState } from 'react';
+import { renderToString } from 'react-dom/server';
import { IAsyncPopup } from '../types';
const popup = new Popup({
@@ -26,6 +26,9 @@ export default function AsyncPopup({
buttonText = 'View More',
hideButton = false,
getCoordOnProperties = false,
+ hasSecondaryButton = false,
+ secondaryButtonText = '',
+ handleSecondaryBtnClick,
showPopup = (_clickedFeature: Record) => true,
}: IAsyncPopup) {
const [properties, setProperties] = useState | null>(
@@ -106,14 +109,27 @@ export default function AsyncPopup({
{!isLoading && !hideButton && (
-
-
handleBtnClick?.(properties)}
- >
- {buttonText}
-
+
+
+ {hasSecondaryButton && (
+ handleSecondaryBtnClick?.(properties)}
+ >
+ {secondaryButtonText}
+
+ )}
+
+ handleBtnClick?.(properties)}
+ >
+ {buttonText}
+
+
)}
diff --git a/src/frontend/src/components/common/MapLibreComponents/types/index.ts b/src/frontend/src/components/common/MapLibreComponents/types/index.ts
index 4155a0af..538e1b48 100644
--- a/src/frontend/src/components/common/MapLibreComponents/types/index.ts
+++ b/src/frontend/src/components/common/MapLibreComponents/types/index.ts
@@ -1,8 +1,8 @@
/* eslint-disable no-unused-vars */
-import type { ReactElement } from 'react';
-import type { Map, MapOptions } from 'maplibre-gl';
-import type { Feature, FeatureCollection, GeoJsonTypes } from 'geojson';
import type { DrawMode } from '@mapbox/mapbox-gl-draw';
+import type { Feature, FeatureCollection, GeoJsonTypes } from 'geojson';
+import type { Map, MapOptions } from 'maplibre-gl';
+import type { ReactElement } from 'react';
export type MapInstanceType = Map;
@@ -83,6 +83,9 @@ export interface IAsyncPopup {
hideButton?: boolean;
getCoordOnProperties?: boolean;
showPopup?: (clickedFeature: Record
) => Boolean;
+ hasSecondaryButton?: boolean;
+ secondaryButtonText?: string;
+ handleSecondaryBtnClick?: (properties: Record) => void;
}
export type DrawModeTypes = DrawMode | null | undefined;
diff --git a/src/frontend/src/components/common/Navbar/index.tsx b/src/frontend/src/components/common/Navbar/index.tsx
index ff1a955a..f0db1d0c 100644
--- a/src/frontend/src/components/common/Navbar/index.tsx
+++ b/src/frontend/src/components/common/Navbar/index.tsx
@@ -10,7 +10,7 @@ export default function Navbar() {
const navigate = useNavigate();
return (
-
+
{
+ const [isVisible, setIsVisible] = useState(false);
+
+ useEffect(() => {
+ const toggleVisibility = () => {
+ const playgroundElement = document.getElementById('app_playground');
+ if (playgroundElement && playgroundElement?.scrollTop > 400) {
+ setIsVisible(true);
+ } else {
+ setIsVisible(false);
+ }
+ };
+
+ const playgroundElement = document.getElementById('app_playground');
+ playgroundElement?.addEventListener('scroll', toggleVisibility);
+
+ return () => {
+ playgroundElement?.removeEventListener('scroll', toggleVisibility);
+ };
+ }, []);
+
+ const scrollToTop = () => {
+ const playgroundElement = document.getElementById('app_playground');
+ playgroundElement?.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ };
+
+ return (
+
+
+ arrow_upward
+
+
+ );
+};
+
+export default ScrollToTop;
diff --git a/src/frontend/src/services/createproject.ts b/src/frontend/src/services/createproject.ts
index 805bf434..18dbdf60 100644
--- a/src/frontend/src/services/createproject.ts
+++ b/src/frontend/src/services/createproject.ts
@@ -1,7 +1,8 @@
/* eslint-disable import/prefer-default-export */
import { authenticated, api } from '.';
-export const getProjectsList = () => authenticated(api).get('/projects/');
+export const getProjectsList = (filterByOwner: boolean) =>
+ authenticated(api).get(`/projects/?filter_by_owner=${filterByOwner}`);
export const getProjectDetail = (id: string) =>
authenticated(api).get(`/projects/${id}`);
diff --git a/src/frontend/src/store/slices/createproject.ts b/src/frontend/src/store/slices/createproject.ts
index d8d042fb..260f6ae1 100644
--- a/src/frontend/src/store/slices/createproject.ts
+++ b/src/frontend/src/store/slices/createproject.ts
@@ -23,6 +23,7 @@ export interface CreateProjectState {
capturedProjectMap: boolean;
projectMapImage: any;
imageMergeType: string;
+ ProjectsFilterByOwner: 'yes' | 'no';
}
const initialState: CreateProjectState = {
@@ -45,6 +46,7 @@ const initialState: CreateProjectState = {
capturedProjectMap: true,
projectMapImage: null,
imageMergeType: 'overlap',
+ ProjectsFilterByOwner: 'no',
};
const setCreateProjectState: CaseReducer<
diff --git a/src/frontend/src/views/Dashboard/index.tsx b/src/frontend/src/views/Dashboard/index.tsx
index 3058a8ae..c151356b 100644
--- a/src/frontend/src/views/Dashboard/index.tsx
+++ b/src/frontend/src/views/Dashboard/index.tsx
@@ -46,7 +46,7 @@ const Dashboard = () => {
});
return (
-
+
Profile
diff --git a/src/frontend/src/views/IndividualProject/index.tsx b/src/frontend/src/views/IndividualProject/index.tsx
index e27ceed6..58580ccb 100644
--- a/src/frontend/src/views/IndividualProject/index.tsx
+++ b/src/frontend/src/views/IndividualProject/index.tsx
@@ -1,19 +1,18 @@
/* eslint-disable jsx-a11y/interactive-supports-focus */
/* eslint-disable jsx-a11y/click-events-have-key-events */
-import { useParams, useNavigate } from 'react-router-dom';
-import { useTypedDispatch, useTypedSelector } from '@Store/hooks';
-import { Flex } from '@Components/common/Layouts';
+import { useGetProjectsDetailQuery } from '@Api/projects';
import Tab from '@Components/common/Tabs';
import {
+ Contributions,
+ Instructions,
MapSection,
Tasks,
- Instructions,
- Contributions,
} from '@Components/IndividualProject';
-import { useGetProjectsDetailQuery } from '@Api/projects';
-import { setProjectState } from '@Store/actions/project';
import { projectOptions } from '@Constants/index';
+import { setProjectState } from '@Store/actions/project';
+import { useTypedDispatch, useTypedSelector } from '@Store/hooks';
import hasErrorBoundary from '@Utils/hasErrorBoundary';
+import { useNavigate, useParams } from 'react-router-dom';
// function to render the content based on active tab
const getActiveTabContent = (
@@ -66,19 +65,19 @@ const IndividualProject = () => {
});
return (
-
+
{/* <----------- temporary breadcrumb -----------> */}
-
+
{
navigate('/projects');
}}
>
Project /
-
+
{
// @ts-ignore
projectData?.name || '--'
@@ -86,8 +85,8 @@ const IndividualProject = () => {
{/* <----------- temporary breadcrumb -----------> */}
-
-
+
+
{
activeTab={individualProjectActiveTab}
clickable
/>
-
+
{getActiveTabContent(
individualProjectActiveTab,
projectData as Record,
@@ -107,10 +106,10 @@ const IndividualProject = () => {
)}
-
);
};
diff --git a/src/frontend/src/views/Projects/index.tsx b/src/frontend/src/views/Projects/index.tsx
index b772f938..db6c1e5f 100644
--- a/src/frontend/src/views/Projects/index.tsx
+++ b/src/frontend/src/views/Projects/index.tsx
@@ -9,11 +9,21 @@ import ProjectCardSkeleton from '@Components/Projects/ProjectCardSkeleton';
import { useEffect } from 'react';
import { getLocalStorageValue } from '@Utils/getLocalStorageValue';
import hasErrorBoundary from '@Utils/hasErrorBoundary';
+import { setCreateProjectState } from '@Store/actions/createproject';
+import { useDispatch } from 'react-redux';
const Projects = () => {
+ const dispatch = useDispatch();
const showMap = useTypedSelector(state => state.common.showMap);
+ const projectsFilterByOwner = useTypedSelector(
+ state => state.createproject.ProjectsFilterByOwner,
+ );
+
// fetch api for projectsList
- const { data: projectsList, isLoading } = useGetProjectsListQuery();
+ const { data: projectsList, isLoading } = useGetProjectsListQuery(
+ projectsFilterByOwner,
+ );
+
const { data: userDetails } = useGetUserDetailsQuery();
const localStorageUserDetails = getLocalStorageValue('userprofile');
@@ -23,8 +33,14 @@ const Projects = () => {
localStorage.setItem('userprofile', userDetailsString as string);
}, [userDetails, localStorageUserDetails]);
+ useEffect(() => {
+ return () => {
+ dispatch(setCreateProjectState({ ProjectsFilterByOwner: 'no' }));
+ };
+ }, [dispatch]);
+
return (
-
+
{
)}
{showMap && (
-
+
)}
diff --git a/src/frontend/src/views/TaskDescription/index.tsx b/src/frontend/src/views/TaskDescription/index.tsx
index fa7b0088..9dcd94e5 100644
--- a/src/frontend/src/views/TaskDescription/index.tsx
+++ b/src/frontend/src/views/TaskDescription/index.tsx
@@ -6,10 +6,9 @@ import hasErrorBoundary from '@Utils/hasErrorBoundary';
const TaskDescription = () => {
const { width } = useWindowDimensions();
-
return (
<>
-
+
diff --git a/src/frontend/src/views/UserProfile/index.tsx b/src/frontend/src/views/UserProfile/index.tsx
index 207efd5f..b634306e 100644
--- a/src/frontend/src/views/UserProfile/index.tsx
+++ b/src/frontend/src/views/UserProfile/index.tsx
@@ -22,6 +22,7 @@ import { removeKeysFromObject } from '@Utils/index';
import { getLocalStorageValue } from '@Utils/getLocalStorageValue';
import Tab from '@Components/common/Tabs';
import hasErrorBoundary from '@Utils/hasErrorBoundary';
+import useWindowDimensions from '@Hooks/useWindowDimensions';
const getActiveFormContent = (
activeTab: number,
@@ -47,7 +48,7 @@ const getActiveFormContent = (
const UserProfile = () => {
const dispatch = useTypedDispatch();
const navigate = useNavigate();
-
+ const { width } = useWindowDimensions();
const signedInAs = localStorage.getItem('signedInAs') || 'Project Creator';
const isDroneOperator =
localStorage.getItem('signedInAs') === 'Drone Operator';
@@ -123,35 +124,33 @@ const UserProfile = () => {
};
return (
-
+
-
-
-
- {}}
- tabOptions={tabOptions}
- activeTab={userProfileActiveTab}
- />
-
-
+
+
+ {}}
+ tabOptions={tabOptions}
+ activeTab={userProfileActiveTab}
+ />
+
+
+
{getActiveFormContent(userProfileActiveTab, signedInAs, formProps)}
- {userProfileActiveTab !== 1 && (
-
- Previous
-
- )}
+
+
+
+ Back
+
{
e.preventDefault();
handleSubmit(onSubmit)();
@@ -162,7 +161,7 @@ const UserProfile = () => {
-
+
);
};