diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..14d46b0 Binary files /dev/null and b/.DS_Store differ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e4c9f29 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "python.testing.unittestArgs": [ + "-v", + "-s", + "./src", + "-p", + "*test*.py" + ], + "python.testing.pytestEnabled": false, + "python.testing.unittestEnabled": true +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b193896..8d4ba61 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ -FROM python:3.12 +FROM python:3.10 RUN apt-get update && apt-get install -y \ redis-server \ supervisor \ + curl \ && apt-get clean && rm -rf /var/lib/apt/lists/* RUN mkdir -p /src /var/log/supervisor @@ -12,9 +13,12 @@ WORKDIR /src COPY ./src /src COPY pyproject.toml /src -RUN pip3 install poetry \ - && poetry config virtualenvs.create false \ - && poetry install --only main +# Install poetry using pip +RUN pip install poetry==1.7.1 + +# Install dependencies +RUN poetry config virtualenvs.create false \ + && poetry install --no-interaction --no-ansi --only main RUN cat < /etc/supervisor/supervisord.conf [supervisord] diff --git a/pyproject.toml b/pyproject.toml index 263733b..4190d31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ readme = "README.md" [tool.poetry.dependencies] python = "^3.10" django = "^5.0.6" -django-rest-framework = "^0.1.0" +djangorestframework = "^3.14.0" django-polymorphic = "^3.1.0" drf-spectacular = "^0.27.2" pillow = "^10.3.0" @@ -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" diff --git a/src/mapping/migrations/0001_initial.py b/src/mapping/migrations/0001_initial.py index e6572a1..6c9b8c9 100644 --- a/src/mapping/migrations/0001_initial.py +++ b/src/mapping/migrations/0001_initial.py @@ -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 @@ -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()), + ], + ), ] diff --git a/src/mapping/service.py b/src/mapping/service.py new file mode 100644 index 0000000..6ce8520 --- /dev/null +++ b/src/mapping/service.py @@ -0,0 +1,51 @@ +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 y in range(ycount): + for x in range(xcount): + meshGrid.append((x + overlap) / (xcount - (1 - 2 * overlap))) + return meshGrid + + +def meshr(xcount, ycount, overlap): + meshGrid = [] + + for y in range(ycount): + for x in range(xcount): + meshGrid.append((math.floor(y) + overlap) / (ycount - (1 - 2 * overlap))) + return meshGrid + + +# Function to convert lat/lon differences to meters relative to a reference point +def lat_lon_to_meters(coords): + """ + Converts latitude and longitude to Cartesian coordinates in meters. + Args: + coords: List of tuples [(lat, lon), ...]. + Returns: + List of tuples [(x, y), ...] where x and y are distances in meters. + """ + # Earth's approximate conversion factors + meters_per_degree_lat = 111320 # meters per degree latitude + ref_lat, ref_lon = coords[0] # Reference point (origin) + ref_lat_rad = math.radians(ref_lat) # Convert latitude to radians + + # Convert to meters relative to the reference point + meter_coords = [ + ( + (lon - ref_lon) + * meters_per_degree_lat + * math.cos(ref_lat_rad), # x: longitude adjusted by cosine + (lat - ref_lat) * meters_per_degree_lat, # y: latitude + ) + for lat, lon in coords + ] + return meter_coords diff --git a/src/mapping/tests.py b/src/mapping/tests.py index 2f99175..533df44 100644 --- a/src/mapping/tests.py +++ b/src/mapping/tests.py @@ -2,6 +2,7 @@ from .serializers import AreaOfInterestSerializer from rest_framework.test import APITestCase import json +import math class AreaOfInterestValidationTest(TestCase): @@ -182,3 +183,56 @@ 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": 0, "longitude": 0, "altitude": 0}, + {"latitude": 28.21, "longitude": 131.25, "altitude": 0}, + {"latitude": 354.00, "longitude": 61.89, "altitude": 0}, + {"latitude": 323.69, "longitude": -71.69, "altitude": 0}, + ] + } + + 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"] + + # Point1 matches intended + self.assertEqual(math.floor(points[0][0]), 311) + self.assertEqual(math.floor(points[0][1]), 0) + + # Point2 + self.assertEqual(math.floor(points[1][0]), 273) + self.assertEqual(math.floor(points[1][1]), 9) + + # Point3 + self.assertEqual(math.floor(points[2][0]), 234) + self.assertEqual(math.floor(points[2][1]), 17) + + self.assertEqual(len(points), 8) diff --git a/src/mapping/urls.py b/src/mapping/urls.py index d52ee20..253a4f4 100644 --- a/src/mapping/urls.py +++ b/src/mapping/urls.py @@ -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", + ), ] diff --git a/src/mapping/views.py b/src/mapping/views.py index e9b890d..4cc3139 100644 --- a/src/mapping/views.py +++ b/src/mapping/views.py @@ -2,7 +2,12 @@ from django.views.decorators.csrf import csrf_exempt from .serializers import AreaOfInterestSerializer from .models import AreaOfInterest +from .service import distance +from .service import meshl +from .service import meshr +from nav.models import Route, OrderedWaypoint import json +import math # pylint: disable=no-member @@ -39,3 +44,123 @@ 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, + 5472, + 3648, + 0.036, + 0.7, + ) + + 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 + + # Num pictures needed on X-axis and Y-axis + xcount = ( + math.ceil( + ( + max( + distance(p1x, p1y, p2x, p2y), + distance(p1x, p1y, p4x, p4y), + ) + - o * height + ) + / ((1 - o) * height) + ) + + 1 + ) + ycount = ( + math.ceil( + ( + min( + distance(p1x, p1y, p2x, p2y), + distance(p1x, p1y, p4x, p4y), + ) + - o * width + ) + / ((1 - o) * width) + ) + + 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]) + + route = Route.objects.create(name="mapping_route") + + # Create ordered waypoints for each point + for i, (lat, lon) in enumerate(final_grid): + OrderedWaypoint.objects.create( + name=f"Waypoint {i+1}", + latitude=lat, + longitude=lon, + altitude=alt, # Set appropriate altitude as needed + order=i, + route=route, + ) + + route.save() + + return HttpResponse( + "New Mapping Route Successfully Saved" + "\n" + str(final_grid), + status=200, + ) + + elif request.method == "GET": + # Getting most recent drone route from Waypoints + route = Route.objects.last() + if route is None: + return HttpResponse("No Drone Route Saved", status=204) + + # Get all waypoints for this route ordered by their order field + waypoints = route.waypoints.order_by("order") + points_on_route = [[wp.latitude, wp.longitude] for wp in waypoints] + + return JsonResponse({"points_on_route": points_on_route}, 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)