-
Notifications
You must be signed in to change notification settings - Fork 6
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
feat: add endpoint to upload task orthophoto to OpenAerialMap (OAM) #346
base: develop
Are you sure you want to change the base?
Changes from all commits
5c61892
077403d
b9bb196
dd2fa34
b56e89e
86741fe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
from fastapi import HTTPException | ||
import requests | ||
from urllib.parse import urlencode | ||
import json | ||
from app.models.enums import HTTPStatus | ||
from loguru import logger as log | ||
|
||
|
||
async def upload_orthophoto_to_oam(oam_params, download_url): | ||
""" | ||
Uploads an orthophoto to OpenAerialMap (OAM) using provided parameters. | ||
|
||
Args: | ||
oam_params: A dictionary of OAM parameters. | ||
download_url: The URL of the orthophoto file. | ||
""" | ||
try: | ||
# Prepare the API URL with encoded parameters | ||
api_url = f"https://api.openaerialmap.org/dronedeploy?{urlencode(oam_params)}" | ||
|
||
response = requests.post( | ||
api_url, | ||
json={"download_path": download_url}, | ||
) | ||
|
||
res = response.json() | ||
if ( | ||
response.status_code == 200 | ||
and "results" in res | ||
and "upload" in res["results"] | ||
): | ||
oam_upload_id = res["results"]["upload"] | ||
log.info( | ||
f"Orthophoto successfully uploaded to OAM with ID: {oam_upload_id}" | ||
) | ||
return oam_upload_id | ||
else: | ||
err_msg = f"Failed to upload orthophoto. Response: {json.dumps(res)}" | ||
raise HTTPException( | ||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=err_msg | ||
) | ||
|
||
except Exception as e: | ||
raise HTTPException( | ||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, | ||
detail=f"Error: {e}", | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
from datetime import datetime | ||
import uuid | ||
from typing import Annotated | ||
from app.projects import project_deps, project_schemas | ||
|
@@ -9,6 +10,8 @@ | |
from psycopg import Connection | ||
from app.db import database | ||
from loguru import logger as log | ||
from app.s3 import get_orthophoto_url | ||
from app.tasks import oam | ||
|
||
router = APIRouter( | ||
prefix=f"{settings.API_PREFIX}/tasks", | ||
|
@@ -86,3 +89,43 @@ async def new_event( | |
user_data, | ||
background_tasks, | ||
) | ||
|
||
|
||
@router.post("/upload/{project_id}/{task_id}") | ||
async def upload_orthophoto_to_oam( | ||
project_id: uuid.UUID, | ||
task_id: uuid.UUID, | ||
user_data: Annotated[AuthUser, Depends(login_required)], | ||
project: Annotated[ | ||
project_schemas.DbProject, Depends(project_deps.get_project_by_id) | ||
], | ||
): | ||
""" | ||
Uploads an orthophoto (TIFF) file to OpenAerialMap (OAM). | ||
|
||
Args: | ||
project_id: The UUID of the project. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You only really need to add docstrings with Plus docstrings are useful for devs to consume functions, but the route is the top level of the logic and nobody is importing and using the routes elsewhere |
||
task_id: The UUID of the task. | ||
user_data: Authenticated user data. | ||
project: Project details fetched by project ID. | ||
""" | ||
|
||
s3_url = get_orthophoto_url(settings.S3_BUCKET_NAME, project_id, task_id) | ||
oam_params = { | ||
"acquisition_end": datetime.now().isoformat(), | ||
"acquisition_start": datetime.now().isoformat(), | ||
"provider": f"{user_data.name}", | ||
"sensor": "DJI MINI4", | ||
"tags": "", | ||
"title": project.name, | ||
"token": settings.OAM_API_TOKEN, | ||
} | ||
|
||
oam_upload_id = await oam.upload_orthophoto_to_oam(oam_params, s3_url) | ||
# NOTE: Status of the uploaded orthophoto can be checked on OpenAerialMap using the OAM upload ID.https://map.openaerialmap.org/#/upload/status/673dbb268ac1b1000173a51d? | ||
return { | ||
"message": "Upload initiated", | ||
"project_id": project_id, | ||
"task_id": task_id, | ||
"oam_id": oam_upload_id, | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { FormEvent, KeyboardEvent, useState } from 'react'; | ||
import { FormControl, Input } from '../FormUI'; | ||
import ErrorMessage from '../FormUI/ErrorMessage'; | ||
import { FlexRow } from '../Layouts'; | ||
|
||
interface IMultipleEmailInput { | ||
emails: string[] | []; | ||
// eslint-disable-next-line no-unused-vars | ||
onEmailAdd: (emails: string[]) => void; | ||
} | ||
|
||
const MultipleEmailInput = ({ emails, onEmailAdd }: IMultipleEmailInput) => { | ||
const [inputEmail, setInputEmail] = useState(''); | ||
const [emailList, setEmailList] = useState(emails || []); | ||
const [error, setError] = useState(''); | ||
|
||
const handleChange = (e: FormEvent<HTMLInputElement>) => { | ||
setInputEmail(e.currentTarget.value?.trim()); | ||
setError(''); | ||
}; | ||
|
||
const addInputEmailOnList = () => { | ||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/?.test(inputEmail)) | ||
return setError('Email is invalid'); | ||
if (emailList?.find(email => email === inputEmail)) | ||
return setError('Email already exists on list'); | ||
setInputEmail(''); | ||
const newEmailList = [...emailList, inputEmail]; | ||
|
||
setEmailList(prev => { | ||
const newList = [...prev, inputEmail]; | ||
onEmailAdd(newList); | ||
return newList; | ||
}); | ||
onEmailAdd(newEmailList); | ||
return () => {}; | ||
}; | ||
|
||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { | ||
if (e.key === 'Enter') { | ||
addInputEmailOnList(); | ||
} | ||
return () => {}; | ||
}; | ||
|
||
const handleDeleteEmail = (email: string) => { | ||
setEmailList(prev => { | ||
const newList = prev?.filter(prevEmail => prevEmail !== email); | ||
onEmailAdd(newList); | ||
return newList; | ||
}); | ||
}; | ||
|
||
return ( | ||
<FormControl className="naxatw-relative"> | ||
<Input | ||
placeholder="Enter email and press enter or click '+' icon to add" | ||
onChange={handleChange} | ||
value={inputEmail} | ||
onKeyDown={handleKeyDown} | ||
/> | ||
|
||
<i | ||
className="material-icons naxatw-absolute naxatw-right-2 naxatw-top-[6px] naxatw-z-30 naxatw-cursor-pointer naxatw-rounded-full naxatw-text-red hover:naxatw-bg-redlight" | ||
onClick={() => addInputEmailOnList()} | ||
role="button" | ||
tabIndex={0} | ||
onKeyDown={() => {}} | ||
> | ||
add | ||
</i> | ||
|
||
<ErrorMessage message={error} /> | ||
<FlexRow gap={2} className="naxatw-flex-wrap"> | ||
{emailList?.map((email: string) => ( | ||
<div | ||
key={email} | ||
className="naxatw-flex naxatw-w-fit naxatw-items-center naxatw-gap-1 naxatw-rounded-xl naxatw-border naxatw-border-black naxatw-bg-gray-50 naxatw-px-2 naxatw-py-0.5" | ||
> | ||
<div className="naxatw-flex naxatw-items-center naxatw-text-sm naxatw-leading-4"> | ||
{email} | ||
</div> | ||
<i | ||
className="material-icons naxatw-cursor-pointer naxatw-rounded-full naxatw-text-center naxatw-text-base hover:naxatw-bg-redlight" | ||
tabIndex={0} | ||
role="button" | ||
onKeyDown={() => {}} | ||
onClick={() => handleDeleteEmail(email)} | ||
> | ||
close | ||
</i> | ||
</div> | ||
))} | ||
</FlexRow> | ||
</FormControl> | ||
); | ||
}; | ||
|
||
export default MultipleEmailInput; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This probably isn't a comment specific to this PR, but the project as a whole.
The endpoint structure for FMTM doesn't necessarily conform to the REST spec / best practices, but we are working on it & hopefully that's changing over time 🙏
So typically it would be set up like this:
If the task id is a unique identifier on it's own.
/projects/{project_id}
/projects/{project_id}/do-something
/tasks/{task_id}
/tasks/{task_id}/do-something
If task ids are only unique in relation to the project they are within:
/projects/{project_id}
/projects/{project_id}/tasks/{task_id}
/projects/{project_id}/tasks/{task_id}/do-something
So for this endpoint, as task_id is a globally unique UUID, it would make sense to create:
/tasks/{task_id}/upload-ortho
or something similar.
Just for info going forward! I'm not saying things have to change immediately, or even at all, but it could be a nice to have into the future 😄