Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Production Release #243

Merged
merged 41 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1540c17
feat: add secondary button on `AsyncPopup` component
Sep 24, 2024
87e8670
feat(project-description): implement task unlock feature
Sep 24, 2024
b808406
feat: tooltip for base layer switcher
Sep 24, 2024
3756b5f
feat(project-description): make responsive
Sep 24, 2024
170afd8
feat(project-dashboard): border radius on map
Sep 24, 2024
a28fbba
feat(project-description): show unlock button only if the task is loc…
Sep 24, 2024
2b45720
fix(task-description): click event persists after the starting point …
Sep 25, 2024
b330474
feat(task-description): all the variables related to take-off point u…
Sep 25, 2024
c1416bd
feat: add `ScrollToTop` component
Sep 25, 2024
ce1a88f
feat: make navbar fixed height
Sep 25, 2024
5af6eb9
style(task-description): update height
Sep 25, 2024
3b3f5a6
feat: implement scroll to top on all pages
Sep 25, 2024
07cb1eb
feat(update-profile): update placeholder text
Sep 25, 2024
2054888
feat(user-profile): make responsive
Sep 25, 2024
9b4964b
Merge pull request #236 from hotosm/feat/change-take-off-point
nrjadkry Sep 26, 2024
92c2cf3
feat(user-profile): make responsive
Sep 25, 2024
d73e32c
Merge pull request #237 from hotosm/feat/user-profile
nrjadkry Sep 26, 2024
1c33145
feat: added project name list tasks based on user endpoint
Pradip-p Sep 26, 2024
179770c
Merge pull request #238 from hotosm/feat/add-project-name
nrjadkry Sep 26, 2024
4feef1e
style(task-description): update uploaded information style
Sep 26, 2024
346e041
fix(task-description): task description comments
Sep 26, 2024
be9fc15
feat(task-description): re-fetch uploaded information and close file …
Sep 26, 2024
1069606
feat(task-description): implement each-task orthophoto download
Sep 26, 2024
ec6b1ed
Merge branch 'develop' of github.com:hotosm/drone-tm into feat/user-p…
Sep 26, 2024
644be84
Merge pull request #239 from hotosm/feat/user-profile
nrjadkry Sep 26, 2024
f9da9d1
fix(project-creation): show error message detail insted of error mess…
Sep 26, 2024
9b4f5de
feat(project-dashboard): apply projects filter by owner
Sep 26, 2024
fe310d6
refactor: remove cluster layers on map component unmount
Sep 26, 2024
51b4943
feat(project-dashboard): add dropdown for owner projects filter
Sep 26, 2024
d21edd6
style(individual-project): increase map section size and reduce table…
Sep 26, 2024
e805297
feat(dashboard): show project name on task logs and request logs
Sep 26, 2024
93813fd
feat(dashboard): make responsive
Sep 26, 2024
83919d4
Merge pull request #240 from hotosm/feat/user-profile
nrjadkry Sep 26, 2024
9c7b071
feat: added KML download format in download boundaries API
Pradip-p Sep 27, 2024
28a511d
fix: convert the str geomerty into feature collection
Pradip-p Sep 27, 2024
7639115
Merge pull request #241 from hotosm/feat/feat/kml-integration
nrjadkry Sep 27, 2024
2fef0f0
style(dashboard): fix dropdown filter UI
Sep 27, 2024
51680ed
feat(task-description): show only the download result button if ortho…
Sep 27, 2024
f5e43a2
Merge branch 'develop' of github.com:hotosm/drone-tm into feat/user-p…
Sep 27, 2024
b6a20ec
feat(task-description): download task area on kml/geojson format
Sep 27, 2024
4c2cba9
Merge pull request #242 from hotosm/feat/user-profile
nrjadkry Sep 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 40 additions & 7 deletions src/backend/app/projects/project_routes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import os
import uuid
from typing import Annotated, Optional
Expand Down Expand Up @@ -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(
Expand All @@ -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.

Expand All @@ -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.
Expand All @@ -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}")
Expand Down
4 changes: 4 additions & 0 deletions src/backend/app/tasks/task_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand All @@ -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
Expand Down
57 changes: 57 additions & 0 deletions src/backend/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<kml xmlns="http://www.opengis.net/kml/2.2">',
"<Document>",
]

# 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("<Placemark>")

# Add properties as name or description if available
if "properties" in feature and feature["properties"]:
if "name" in feature["properties"]:
kml_output.append(f"<name>{feature['properties']['name']}</name>")
if "description" in feature["properties"]:
kml_output.append(
f"<description>{feature['properties']['description']}</description>"
)

# Handle different geometry types (Point, LineString, Polygon)
if geometry_type == "Point":
lon, lat = coordinates
kml_output.append(f"<Point><coordinates>{lon},{lat}</coordinates></Point>")
elif geometry_type == "LineString":
coord_string = " ".join([f"{lon},{lat}" for lon, lat in coordinates])
kml_output.append(
f"<LineString><coordinates>{coord_string}</coordinates></LineString>"
)
elif geometry_type == "Polygon":
for polygon in coordinates:
coord_string = " ".join([f"{lon},{lat}" for lon, lat in polygon])
kml_output.append(
f"<Polygon><outerBoundaryIs><LinearRing><coordinates>{coord_string}</coordinates></LinearRing></outerBoundaryIs></Polygon>"
)

kml_output.append("</Placemark>")

kml_output.append("</Document>")
kml_output.append("</kml>")

return "\n".join(kml_output)
23 changes: 16 additions & 7 deletions src/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
getModalContent,
getPromptDialogContent,
} from '@Constants/modalContents';
import ScrollToTop from '@Components/common/ScrollToTop';

export default function App() {
const dispatch = useTypedDispatch();
Expand Down Expand Up @@ -81,13 +82,21 @@ export default function App() {
>
{getPromptDialogContent(promptDialogContent)?.content}
</PromptDialog>

{generateRoutes({
routes:
process.env.NODE_ENV !== 'production'
? [...testRoutes, ...appRoutes]
: appRoutes,
})}
<div
id="app_playground"
className="app_playground scrollbar naxatw-overflow-y-auto naxatw-px-3 md:naxatw-px-0"
style={{
height: 'calc(100vh-3.5rem)',
}}
>
{generateRoutes({
routes:
process.env.NODE_ENV !== 'production'
? [...testRoutes, ...appRoutes]
: appRoutes,
})}
</div>
<ScrollToTop />
</div>
</>
);
Expand Down
5 changes: 3 additions & 2 deletions src/frontend/src/api/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { getTaskStates } from '@Services/project';
import { getUserProfileInfo } from '@Services/common';

export const useGetProjectsListQuery = (
projectsFilterByOwner: 'yes' | 'no',
queryOptions?: Partial<UseQueryOptions>,
) => {
return useQuery({
queryKey: ['projects-list'],
queryFn: getProjectsList,
queryKey: ['projects-list', projectsFilterByOwner],
queryFn: () => getProjectsList(projectsFilterByOwner === 'yes'),
select: (res: any) => res.data,
...queryOptions,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ const CreateprojectLayout = () => {
dispatch(resetUploadedAndDrawnAreas());
},
onError: err => {
toast.error(err.message);
toast.error(err?.response?.data?.detail || err?.message || '');
},
});

Expand Down
5 changes: 3 additions & 2 deletions src/frontend/src/components/Dashboard/RequestLogs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
>
<div>
The <strong>Task# {task.project_task_index}</strong> is requested
for Mapping
The <strong>Task# {task.project_task_index}</strong> from{' '}
<strong>{task?.project_name}</strong> project is requested for
Mapping.
</div>
<div className="naxatw-flex naxatw-w-[172px] naxatw-gap-3">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ const TaskLogsTable = ({ data: taskList }: ITaskLogsTableProps) => {
<table className="naxatw-w-full naxatw-overflow-hidden naxatw-rounded-lg">
<thead>
<tr className="naxatw-bg-red naxatw-text-left naxatw-font-normal naxatw-text-white">
<td className="naxatw-w-80 naxatw-border-r-2 naxatw-px-2 naxatw-py-1">
<td className="naxatw-w-20 naxatw-border-r-2 naxatw-px-2 naxatw-py-1">
ID
</td>
<td className="naxatw-min-w-30 naxatw-border-r-2 naxatw-px-2 naxatw-py-1">
Project Name
</td>
<td className="naxatw-border-r-2 naxatw-px-2 naxatw-py-1">
Total task area
</td>
Expand All @@ -33,9 +36,10 @@ const TaskLogsTable = ({ data: taskList }: ITaskLogsTableProps) => {
<tbody>
{taskList?.map(task => (
<tr key={task.task_id}>
<td className="naxatw-px-2 naxatw-py-1">
<td className="naxatw-line-clamp-1 naxatw-px-2 naxatw-py-1">
Task# {task?.project_task_index}
</td>
<td className="naxatw-px-2 naxatw-py-1">{task?.project_name}</td>
<td className="naxatw-px-2 naxatw-py-1">
{Number(task?.task_area)?.toFixed(3)}
</td>
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand Down Expand Up @@ -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 (
<>
<div className="naxatw-flex naxatw-flex-col naxatw-gap-5">
Expand All @@ -109,12 +134,13 @@ const DescriptionBox = () => {
/>
))}
</div>
{/* {!secondPage && <QuestionBox />} */}
<QuestionBox
setFlyable={setFlyable}
flyable={flyable}
haveNoImages={taskAssetsInformation?.image_count === 0}
/>
{taskAssetsInformation?.image_count === 0 && (
<QuestionBox
setFlyable={setFlyable}
flyable={flyable}
haveNoImages={taskAssetsInformation?.image_count === 0}
/>
)}

{taskAssetsInformation?.image_count > 0 && (
<div className="naxatw-flex naxatw-flex-col naxatw-gap-5">
Expand All @@ -130,6 +156,19 @@ const DescriptionBox = () => {
},
]}
/>
{taskAssetsInformation?.assets_url && (
<div className="">
<Button
variant="ghost"
className="naxatw-bg-red naxatw-text-white disabled:!naxatw-cursor-not-allowed disabled:naxatw-bg-gray-500 disabled:naxatw-text-white"
leftIcon="download"
iconClassname="naxatw-text-[1.125rem]"
onClick={() => handleDownloadResult()}
>
Download Result
</Button>
</div>
)}
</div>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const UploadsInformation = ({ data }: { data: Record<string, any>[] }) => {
return (
<>
<div className="naxatw-flex naxatw-w-full naxatw-flex-col naxatw-gap-5">
<div className="naxatw-flex naxatw-flex-col naxatw-gap-3">
<div className="naxatw-flex naxatw-w-full naxatw-flex-col naxatw-gap-2">
<div className="naxatw-flex naxatw-flex-col naxatw-gap-2">
<p className="naxatw-text-[0.875rem] naxatw-font-semibold naxatw-leading-normal naxatw-tracking-[0.0175rem] naxatw-text-[#D73F3F]">
Upload Information
</p>
Expand Down
Loading
Loading