Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
11 changes: 11 additions & 0 deletions .vscode/settings.json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add this folder to .gitignore. also, in order to have test run by the CI, unittests should be done with django

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add this to .gitignore

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"python.testing.unittestArgs": [
"-v",
"-s",
"./src",
"-p",
"*test*.py"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true
}
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ django-cors-headers = "^4.5.0"
asgiref = "^3.8.1"



[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
18 changes: 17 additions & 1 deletion src/mapping/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0.6 on 2024-10-08 02:09
# Generated by Django 5.1.2 on 2024-10-19 20:18

from django.db import migrations, models

Expand All @@ -25,4 +25,20 @@ class Migration(migrations.Migration):
("area_of_interest", models.JSONField()),
],
),
migrations.CreateModel(
name="MappingRoute",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("points_on_route", models.JSONField()),
("altitude", models.FloatField()),
],
),
]
17 changes: 17 additions & 0 deletions src/mapping/models.py

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does storing these waypoint as a json instead of an array of Waypoints offer any noticeable speedup?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^

Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,20 @@ def save(self, **kwargs):
if AreaOfInterest.objects.count() != 0:
AreaOfInterest.objects.all().delete()
super().save(**kwargs) # Call the "real" save() method.


class MappingRoute(models.Model):
"""Singleton Model for drone route for mapping"""

# fields
# Stored list of points as arbitrary length JSON field to be consistent with area of interest
# altitude is how high the drone needs to be from ground when taking these pictures.
points_on_route = models.JSONField()
altitude = models.FloatField()

# methods
def save(self, **kwargs):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is a Singleton, please add that to the class name. Not too sure if a Singleton design pattern is necessary though, we can easily support two mapping areas that we do sequentially

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^


if MappingRoute.objects.count() != 0:
MappingRoute.objects.all().delete()
super().save(**kwargs)
21 changes: 21 additions & 0 deletions src/mapping/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import math


def distance(p1x, p1y, p2x, p2y):
return math.sqrt(math.pow(p1x - p2x, 2) + math.pow(p1y - p2y, 2))


def meshl(xcount, ycount, overlap):
meshGrid = []
for i in range(xcount * ycount):
meshGrid.append(((i % xcount) + overlap) / (xcount - (1 - 2 * overlap)))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be separated into a nested loop for code clarity?

return meshGrid


def meshr(xcount, ycount, overlap):
meshGrid = []
for i in range(xcount * ycount):
meshGrid.append(
(math.floor(i / xcount) + overlap) / (ycount - (1 - 2 * overlap))
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above here

return meshGrid
42 changes: 42 additions & 0 deletions src/mapping/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .serializers import AreaOfInterestSerializer
from rest_framework.test import APITestCase
import json
import math


class AreaOfInterestValidationTest(TestCase):
Expand Down Expand Up @@ -182,3 +183,44 @@ def test_post_n_get_area_of_interest(self):
self.assertFalse("altitude" in returned_object["area_of_interest"][1])
self.assertFalse("altitude" in returned_object["area_of_interest"][2])
self.assertFalse("altitude" in returned_object["area_of_interest"][3])


class MappingRouteTest(APITestCase):
def setup(self):
pass

def test_get_route_from_area(self):
test_object = {
"area_of_interest": [
{"latitude": 2, "longitude": 11.4, "altitude": 3},
{"latitude": 0, "longitude": 116, "altitude": 6},
{"latitude": 193, "longitude": 110, "altitude": 9},
{"latitude": 200, "longitude": 5, "altitude": 12},
]
}

post_response = self.client.post(
"/api/mapping/area_of_interest",
json.dumps(test_object),
content_type="application/json",
)

self.assertEqual(post_response.status_code, 200)

post_response = self.client.post(
"/api/mapping/points_on_route",
content_type="application/json",
)

self.assertEqual(post_response.status_code, 200)

get_response = self.client.get(
"/api/mapping/points_on_route",
)

returned_object = json.loads(get_response.content)
self.assertEqual(get_response.status_code, 200)

points = returned_object["points_on_route"]

self.assertEqual(math.floor(points[0][0]), 176)
5 changes: 5 additions & 0 deletions src/mapping/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@
views.process_area_of_interest,
name="process_area_of_interest",
),
path(
"points_on_route",
views.process_points_on_route,
name="process_points_on_route",
),
]
99 changes: 99 additions & 0 deletions src/mapping/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
from django.views.decorators.csrf import csrf_exempt
from .serializers import AreaOfInterestSerializer
from .models import AreaOfInterest
from .models import MappingRoute
from .service import distance
from .service import meshl
from .service import meshr
import json
import math

# pylint: disable=no-member

Expand Down Expand Up @@ -39,3 +44,97 @@ def process_area_of_interest(request):
return HttpResponse("Correct Address, Incorrect Method", status=405)
except (KeyError, ValueError, TypeError) as e:
return HttpResponse("Server Error\n" + str(e), status=500)


@csrf_exempt
def process_points_on_route(request):
# Uses 4 most recent boundary points to make route mapping
try:
if request.method == "POST":
# Gets most recent Boundary points for computation
area_modelled = AreaOfInterest.objects.last()
if area_modelled is None:
return HttpResponse("No Area Of Interest Saved", status=204)
else:
# Camera Parameters (Focal length, Sensor Width, Sensor Height, Image Width
# Image Height, Ground Sample Distance, Image overlap %)
d_focal, sw, iw, ih, gsd, o = (
0.012,
0.0131328,
3840,
2160,
0.025,
0.317,
)

width, height = gsd * iw, gsd * ih
area = json.loads(area_modelled.area_of_interest)

# Generating List of Points
p1, p2, p3, p4 = area
p1x = p1["latitude"]
p1y = p1["longitude"]
p2x = p2["latitude"]
p2y = p2["longitude"]
p3x = p3["latitude"]
p3y = p3["longitude"]
p4x = p4["latitude"]
p4y = p4["longitude"]
# required altitude (meters)
alt = (iw * d_focal * gsd) / sw

# Maximum X and Y distances
ylen1 = distance(p1x, p1y, p2x, p2y)
ylen2 = distance(p3x, p3y, p4x, p4y)
ymax = max(ylen1, ylen2)

xlen1 = distance(p1x, p1y, p4x, p4y)
xlen2 = distance(p2x, p2y, p3x, p3y)
xmax = max(xlen1, xlen2)

# Num pictures needed on X-axis and Y-axis
xcount = math.ceil((xmax - (o * width)) / ((1 - o) * width)) + 1
ycount = math.ceil((ymax - (o * height)) / ((1 - o) * height)) + 1

# Creating Mesh Grids
gridl = meshl(xcount, ycount, o)
gridr = meshr(xcount, ycount, o)

# Projecting Mesh Grid onto images
final_grid = []
for i in range(xcount * ycount):
newx = (p3x + gridl[i] * (p2x - p3x)) + gridr[i] * (
p4x - p3x + gridl[i] * (p1x - p2x + p3x - p4x)
)
newy = (p3y + gridl[i] * (p2y - p3y)) + gridr[i] * (
p4y - p3y + gridl[i] * (p1y - p2y + p3y - p4y)
)
final_grid.append([newx, newy])

points = MappingRoute(
points_on_route=json.dumps(final_grid), altitude=json.dumps(alt)
)
points.save()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine for now, but we will probably not need the grid to be saved once we've integrated your work with Kai's


return HttpResponse(
"New Mapping Route Successfully Saved" + "\n" + str(points),
status=200,
)

elif request.method == "GET":
# Getting most recent drone rout from MappingRoute
route = MappingRoute.objects.last()
if route is None:
return HttpResponse("No Drone Route Saved", status=204)

points = {
"points_on_route": json.loads(route.points_on_route),
"altitude": route.altitude,
}

return JsonResponse(points, status=200)

else:
return HttpResponse("Correct Address, Incorrect Method", status=405)
except (KeyError, ValueError, TypeError) as e:
return HttpResponse("Server Error\n" + str(e), status=500)