Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
22 changes: 22 additions & 0 deletions src/controllers/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,28 @@ async def get_rocketpy_flight_rpy(
flight_service = FlightService.from_flight_model(flight.flight)
return flight_service.get_flight_rpy()

@controller_exception_handler
async def get_flight_kml(
self,
flight_id: str,
) -> bytes:
"""
Get the flight trajectory as a KML file.

Args:
flight_id: str

Returns:
bytes (KML XML)

Raises:
HTTP 404 Not Found: If the flight is not found
in the database.
"""
flight = await self.get_flight_by_id(flight_id)
flight_service = FlightService.from_flight_model(flight.flight)
return flight_service.get_flight_kml()

@controller_exception_handler
async def get_flight_simulation(
self,
Expand Down
36 changes: 36 additions & 0 deletions src/routes/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,42 @@ async def update_flight_rocket(
)


@router.get(
"/{flight_id}/kml",
responses={
200: {
"description": "KML trajectory file download",
"content": {"application/vnd.google-earth.kml+xml": {}},
}
},
status_code=200,
response_class=Response,
)
async def get_flight_kml(
flight_id: str,
controller: FlightControllerDep,
):
"""
Export a flight trajectory as a KML file for Google Earth.

## Args
``` flight_id: str ```
"""
with tracer.start_as_current_span("get_flight_kml"):
kml = await controller.get_flight_kml(flight_id)
headers = {
"Content-Disposition": (
f'attachment; filename="flight_{flight_id}.kml"'
),
}
return Response(
content=kml,
headers=headers,
media_type="application/vnd.google-earth.kml+xml",
status_code=200,
)


@router.get("/{flight_id}/simulate")
async def get_flight_simulation(
flight_id: str,
Expand Down
22 changes: 22 additions & 0 deletions src/services/flight.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json
import os
import tempfile
from typing import Self, Tuple

import numpy as np

from rocketpy.simulation.flight import Flight as RocketPyFlight
from rocketpy.simulation.flight_data_exporter import FlightDataExporter
from rocketpy._encoders import RocketPyEncoder, RocketPyDecoder
from rocketpy.mathutils.function import Function
from rocketpy.motors.solid_motor import SolidMotor
Expand Down Expand Up @@ -499,6 +502,25 @@ def get_flight_simulation(self) -> FlightSimulation:
flight_simulation = FlightSimulation(**encoded_attributes)
return flight_simulation

def get_flight_kml(self) -> bytes:
"""
Get the flight trajectory as a KML file for Google Earth.

Returns:
bytes (UTF-8 encoded KML)
"""
with tempfile.NamedTemporaryFile(
suffix=".kml", delete=False
) as tmp:
tmp_path = tmp.name
try:
FlightDataExporter(self._flight).export_kml(file_name=tmp_path)
with open(tmp_path, "rb") as fh:
return fh.read()
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)

def get_flight_rpy(self) -> bytes:
"""
Get the portable JSON ``.rpy`` representation of the flight.
Expand Down
33 changes: 33 additions & 0 deletions tests/unit/test_routes/test_flights_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def mock_controller_instance():
mock_controller.get_rocketpy_flight_rpy = AsyncMock()
mock_controller.import_flight_from_rpy = AsyncMock()
mock_controller.get_flight_notebook = AsyncMock()
mock_controller.get_flight_kml = AsyncMock()
mock_controller.update_environment_by_flight_id = AsyncMock()
mock_controller.update_rocket_by_flight_id = AsyncMock()
mock_controller.create_flight_from_references = AsyncMock()
Expand Down Expand Up @@ -539,6 +540,38 @@ def test_read_rocketpy_flight_rpy_server_error(mock_controller_instance):
assert response.json() == {'detail': 'Internal Server Error'}


def test_read_flight_kml(mock_controller_instance):
kml_bytes = b'<?xml version="1.0" encoding="UTF-8"?><kml></kml>'
mock_controller_instance.get_flight_kml = AsyncMock(return_value=kml_bytes)
response = client.get('/flights/123/kml')
assert response.status_code == 200
assert response.content == kml_bytes
assert (
response.headers['content-type']
== 'application/vnd.google-earth.kml+xml'
)
assert 'flight_123.kml' in response.headers['content-disposition']
mock_controller_instance.get_flight_kml.assert_called_once_with('123')


def test_read_flight_kml_not_found(mock_controller_instance):
mock_controller_instance.get_flight_kml.side_effect = HTTPException(
status_code=status.HTTP_404_NOT_FOUND
)
response = client.get('/flights/123/kml')
assert response.status_code == 404
assert response.json() == {'detail': 'Not Found'}


def test_read_flight_kml_server_error(mock_controller_instance):
mock_controller_instance.get_flight_kml.side_effect = HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
)
response = client.get('/flights/123/kml')
assert response.status_code == 500
assert response.json() == {'detail': 'Internal Server Error'}


# --- Issue #56: Import flight from .rpy ---


Expand Down
Loading