Skip to content

Commit

Permalink
fix: merge conflict
Browse files Browse the repository at this point in the history
  • Loading branch information
nrjadkry committed Dec 11, 2024
2 parents 8eb4d97 + 8b2afe4 commit 4ded064
Show file tree
Hide file tree
Showing 13 changed files with 723 additions and 200 deletions.
8 changes: 4 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,23 +79,23 @@ repos:

# Deps: ensure Python uv lockfile is up to date
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.5.5
rev: 0.5.7
hooks:
- id: uv-lock
files: src/backend/pyproject.toml
args: [--project, src/backend]

# Versioning: Commit messages & changelog
- repo: https://github.com/commitizen-tools/commitizen
rev: v4.0.0
rev: v4.1.0
hooks:
- id: commitizen
stages: [commit-msg]

# Lint / autoformat: Python code
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: "v0.8.1"
rev: "v0.8.2"
hooks:
# Run the linter
- id: ruff
Expand All @@ -107,7 +107,7 @@ repos:

# Autoformat: YAML, JSON, Markdown, etc.
- repo: https://github.com/pycontribs/mirrors-prettier
rev: v3.3.3
rev: v3.4.2
hooks:
- id: prettier
args:
Expand Down
32 changes: 30 additions & 2 deletions src/backend/app/projects/project_routes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import json
import os
import shutil
import uuid
from typing import Annotated, Optional
from uuid import UUID
Expand Down Expand Up @@ -39,7 +40,7 @@
)
from app.users import user_schemas
from minio.deleteobjects import DeleteObject
from drone_flightplan import waypoints
from drone_flightplan import waypoints, add_elevation_from_dem

router = APIRouter(
prefix=f"{settings.API_PREFIX}/projects",
Expand Down Expand Up @@ -668,6 +669,7 @@ async def get_project_waypoints_counts(
meters: float = 100,
project_geojson: UploadFile = File(...),
is_terrain_follow: bool = False,
dem: UploadFile = File(None),
user_data: AuthUser = Depends(login_required),
):
"""
Expand Down Expand Up @@ -708,6 +710,32 @@ async def get_project_waypoints_counts(
generate_3d=generate_3d,
take_off_point=None,
)

# Handle terrain-following logic if a DEM is provided
points_with_elevation = points
if is_terrain_follow and dem:
temp_dir = f"/tmp/{uuid.uuid4()}"
try:
os.makedirs(temp_dir, exist_ok=True)
dem_path = os.path.join(temp_dir, "dem.tif")
outfile_with_elevation = os.path.join(
temp_dir, "output_file_with_elevation.geojson"
)

with open(dem_path, "wb") as dem_file:
dem_file.write(await dem.read())

add_elevation_from_dem(dem_path, points, outfile_with_elevation)

with open(outfile_with_elevation, "r") as inpointsfile:
points_with_elevation = inpointsfile.read()
except Exception as e:
log.error(f"Error processing DEM: {e}")

finally:
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)

return {
"avg_no_of_waypoints": len(json.loads(points)["features"]),
"avg_no_of_waypoints": len(json.loads(points_with_elevation)["features"]),
}
4 changes: 3 additions & 1 deletion src/backend/app/tasks/task_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ class TaskDetailsOut(BaseModel):
side_overlap: Optional[float] = None
gsd_cm_px: Optional[float] = None
gimble_angles_degrees: Optional[int] = None
centroid: dict

@field_validator("state", mode="after")
@classmethod
Expand Down Expand Up @@ -312,7 +313,8 @@ async def get_task_details(db: Connection, task_id: uuid.UUID):
),
'id', tasks.id
) AS outline,
-- Calculate the centroid of the outline
ST_AsGeoJSON(ST_Centroid(tasks.outline))::jsonb AS centroid,
te.created_at,
te.updated_at,
te.state,
Expand Down
51 changes: 50 additions & 1 deletion src/backend/app/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import json
import base64
from datetime import datetime, timezone
from typing import Optional, Union, Any
from typing import Dict, Optional, Union, Any
from geojson_pydantic import Feature, MultiPolygon, Polygon
from geojson_pydantic import FeatureCollection as FeatCol
from geoalchemy2 import WKBElement
Expand All @@ -21,6 +21,9 @@
from email.mime.text import MIMEText
from email.utils import formataddr
from aiosmtplib import send as send_email
from shapely.geometry import Point
from shapely.ops import transform
from pyproj import Transformer


log = logging.getLogger(__name__)
Expand Down Expand Up @@ -551,3 +554,49 @@ async def send_project_approval_email_to_regulator(
subject="Project Review Request for Drone Operations Approval",
html_content=html_content,
)


def calculate_flight_time_from_placemarks(placemarks: Dict) -> Dict:
"""
Calculate the total and average flight time based on placemarks and dynamically format the output.
Args:
placemarks (Dict): GeoJSON-like data structure with flight plan.
Returns:
Dict: Contains formatted total flight time and segment times.
"""
total_time = 0
features = placemarks["features"]
transformer = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
for i in range(1, len(features)):
# Extract current and previous coordinates
prev_coords = features[i - 1]["geometry"]["coordinates"][:2]
curr_coords = features[i]["geometry"]["coordinates"][:2]
speed = features[i]["properties"]["speed"] # Speed in m/s

# Create Shapely Points and transform to planar coordinates for distance calculation
prev_point = Point(transform(transformer.transform, Point(prev_coords)))
curr_point = Point(transform(transformer.transform, Point(curr_coords)))

# Calculate distance (meters) and time (seconds)
distance = prev_point.distance(curr_point)
segment_time = distance / speed
total_time += segment_time

# Dynamically format the total flight time
hours = int(total_time // 3600)
minutes = int((total_time % 3600) // 60)
seconds = round(total_time % 60, 2)

if total_time < 60:
formatted_time = f"{seconds} seconds"
elif total_time < 3600:
formatted_time = f"{minutes} minutes {seconds:.2f} seconds"
else:
formatted_time = f"{hours} hours {minutes} minutes {seconds:.2f} seconds"

return {
"total_flight_time": formatted_time,
"total_flight_time_seconds": round(total_time, 2),
}
36 changes: 31 additions & 5 deletions src/backend/app/waypoints/waypoint_routes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import uuid
import geojson
import shutil
Expand All @@ -18,16 +19,19 @@
get_take_off_point_from_db,
update_take_off_point_in_db,
)
from app.waypoints.waypoint_logic import check_point_within_buffer
from app.waypoints.waypoint_logic import (
check_point_within_buffer,
)
from app.db import database
from app.utils import merge_multipolygon
from app.utils import calculate_flight_time_from_placemarks, merge_multipolygon
from app.s3 import get_file_from_bucket
from typing import Annotated
from psycopg import Connection
from app.projects import project_deps
from shapely.geometry import shape
from app.waypoints import waypoint_schemas


# Constant to convert gsd to Altitude above ground level
GSD_to_AGL_CONST = 29.7 # For DJI Mini 4 Pro

Expand Down Expand Up @@ -144,10 +148,12 @@ async def get_task_waypoint(
outfile = outfile = f"/tmp/{uuid.uuid4()}"
kmz_file = wpml.create_wpml(placemarks, outfile)
return FileResponse(
kmz_file, media_type="application/zip", filename="flight_plan.kmz"
kmz_file,
media_type="application/zip",
filename=f"{task_id}_flight_plan.kmz",
)

return placemarks
flight_data = calculate_flight_time_from_placemarks(placemarks)
return {"results": placemarks, "flight_data": flight_data}


@router.post("/")
Expand Down Expand Up @@ -254,3 +260,23 @@ async def generate_kmz(
return FileResponse(
output_file, media_type="application/zip", filename="output.kmz"
)


@router.post("/{task_id}/generate-kmz/")
async def generate_kmz_with_placemarks(
task_id: uuid.UUID, data: waypoint_schemas.PlacemarksFeature
):
try:
outfile = f"/tmp/{task_id}_flight_plan.kmz"

kmz_file = wpml.create_wpml(data.model_dump(), outfile)
if not os.path.exists(kmz_file):
raise HTTPException(status_code=500, detail="Failed to generate KMZ file.")
return FileResponse(
kmz_file,
media_type="application/zip",
filename=f"{task_id}_flight_plan.kmz",
)

except Exception as e:
raise HTTPException(status_code=500, detail=f"Error generating KMZ: {str(e)}")
32 changes: 32 additions & 0 deletions src/backend/app/waypoints/waypoint_schemas.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import json
from typing import List, Optional
from pydantic import BaseModel, model_validator
from geojson_pydantic import FeatureCollection, Feature, Point


class PointField(BaseModel):
Expand All @@ -12,3 +14,33 @@ def validate_to_json(cls, value):
if isinstance(value, str):
return cls(**json.loads(value))
return value


class Properties(BaseModel):
altitude: float
gimbal_angle: str
heading: float
index: int
speed: float
take_photo: bool
elevation: Optional[float] = None


class Geometry(Point):
pass


class Feature(Feature):
geometry: Geometry
properties: Properties


class CRS(BaseModel):
properties: dict
type: str


class PlacemarksFeature(FeatureCollection):
type: str = "FeatureCollection"
crs: Optional[CRS] = None
features: List[Feature]
6 changes: 6 additions & 0 deletions src/frontend/src/assets/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ body {
.scrollbar-images-grid::-webkit-scrollbar-thumb {
background: transparent;
}

.has-dropshadow {
box-shadow:
2px 2px 5px rgb(255, 0, 0),
2px 2px 3px rgba(255, 0, 0, 0.692);
}
@keyframes pulse-animation {
0% {
box-shadow: 0 0 0 0 rgba(0, 70, 119, 0.5);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ const DescriptionBox = () => {
},
});

const { data: flightTimeData }: any = useGetTaskWaypointQuery(
projectId as string,
taskId as string,
{
select: ({ data }: any) => data.flight_data,
},
);

const { data: taskDescription }: Record<string, any> =
useGetIndividualTaskQuery(taskId as string, {
enabled: !!taskWayPoints,
Expand Down Expand Up @@ -126,7 +134,11 @@ const DescriptionBox = () => {
{ name: 'Total points', value: taskWayPoints?.length },
{
name: 'Est. flight time',
value: taskData?.flight_time || null,
value: flightTimeData?.total_flight_time || null,
},
{
name: 'Est. flight time in seconds',
value: flightTimeData?.total_flight_time_seconds || null,
},
],
},
Expand Down
Loading

0 comments on commit 4ded064

Please sign in to comment.