Skip to content

Commit 2971aa0

Browse files
committed
feat(flight): KML trajectory export endpoint
GET /flights/{id}/kml returns a KML file of the flight trajectory.
1 parent f0a5903 commit 2971aa0

4 files changed

Lines changed: 113 additions & 0 deletions

File tree

src/controllers/flight.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,28 @@ async def get_rocketpy_flight_rpy(
145145
flight_service = FlightService.from_flight_model(flight.flight)
146146
return flight_service.get_flight_rpy()
147147

148+
@controller_exception_handler
149+
async def get_flight_kml(
150+
self,
151+
flight_id: str,
152+
) -> bytes:
153+
"""
154+
Get the flight trajectory as a KML file.
155+
156+
Args:
157+
flight_id: str
158+
159+
Returns:
160+
bytes (KML XML)
161+
162+
Raises:
163+
HTTP 404 Not Found: If the flight is not found
164+
in the database.
165+
"""
166+
flight = await self.get_flight_by_id(flight_id)
167+
flight_service = FlightService.from_flight_model(flight.flight)
168+
return flight_service.get_flight_kml()
169+
148170
@controller_exception_handler
149171
async def get_flight_simulation(
150172
self,

src/routes/flight.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,42 @@ async def update_flight_rocket(
301301
)
302302

303303

304+
@router.get(
305+
"/{flight_id}/kml",
306+
responses={
307+
200: {
308+
"description": "KML trajectory file download",
309+
"content": {"application/vnd.google-earth.kml+xml": {}},
310+
}
311+
},
312+
status_code=200,
313+
response_class=Response,
314+
)
315+
async def get_flight_kml(
316+
flight_id: str,
317+
controller: FlightControllerDep,
318+
):
319+
"""
320+
Export a flight trajectory as a KML file for Google Earth.
321+
322+
## Args
323+
``` flight_id: str ```
324+
"""
325+
with tracer.start_as_current_span("get_flight_kml"):
326+
kml = await controller.get_flight_kml(flight_id)
327+
headers = {
328+
"Content-Disposition": (
329+
f'attachment; filename="flight_{flight_id}.kml"'
330+
),
331+
}
332+
return Response(
333+
content=kml,
334+
headers=headers,
335+
media_type="application/vnd.google-earth.kml+xml",
336+
status_code=200,
337+
)
338+
339+
304340
@router.get("/{flight_id}/simulate")
305341
async def get_flight_simulation(
306342
flight_id: str,

src/services/flight.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import json
2+
import os
3+
import tempfile
24
from typing import Self, Tuple
35

46
import numpy as np
57

68
from rocketpy.simulation.flight import Flight as RocketPyFlight
9+
from rocketpy.simulation.flight_data_exporter import FlightDataExporter
710
from rocketpy._encoders import RocketPyEncoder, RocketPyDecoder
811
from rocketpy.mathutils.function import Function
912
from rocketpy.motors.solid_motor import SolidMotor
@@ -499,6 +502,25 @@ def get_flight_simulation(self) -> FlightSimulation:
499502
flight_simulation = FlightSimulation(**encoded_attributes)
500503
return flight_simulation
501504

505+
def get_flight_kml(self) -> bytes:
506+
"""
507+
Get the flight trajectory as a KML file for Google Earth.
508+
509+
Returns:
510+
bytes (UTF-8 encoded KML)
511+
"""
512+
with tempfile.NamedTemporaryFile(
513+
suffix=".kml", delete=False
514+
) as tmp:
515+
tmp_path = tmp.name
516+
try:
517+
FlightDataExporter(self._flight).export_kml(file_name=tmp_path)
518+
with open(tmp_path, "rb") as fh:
519+
return fh.read()
520+
finally:
521+
if os.path.exists(tmp_path):
522+
os.unlink(tmp_path)
523+
502524
def get_flight_rpy(self) -> bytes:
503525
"""
504526
Get the portable JSON ``.rpy`` representation of the flight.

tests/unit/test_routes/test_flights_route.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def mock_controller_instance():
5757
mock_controller.get_rocketpy_flight_rpy = AsyncMock()
5858
mock_controller.import_flight_from_rpy = AsyncMock()
5959
mock_controller.get_flight_notebook = AsyncMock()
60+
mock_controller.get_flight_kml = AsyncMock()
6061
mock_controller.update_environment_by_flight_id = AsyncMock()
6162
mock_controller.update_rocket_by_flight_id = AsyncMock()
6263
mock_controller.create_flight_from_references = AsyncMock()
@@ -539,6 +540,38 @@ def test_read_rocketpy_flight_rpy_server_error(mock_controller_instance):
539540
assert response.json() == {'detail': 'Internal Server Error'}
540541

541542

543+
def test_read_flight_kml(mock_controller_instance):
544+
kml_bytes = b'<?xml version="1.0" encoding="UTF-8"?><kml></kml>'
545+
mock_controller_instance.get_flight_kml = AsyncMock(return_value=kml_bytes)
546+
response = client.get('/flights/123/kml')
547+
assert response.status_code == 200
548+
assert response.content == kml_bytes
549+
assert (
550+
response.headers['content-type']
551+
== 'application/vnd.google-earth.kml+xml'
552+
)
553+
assert 'flight_123.kml' in response.headers['content-disposition']
554+
mock_controller_instance.get_flight_kml.assert_called_once_with('123')
555+
556+
557+
def test_read_flight_kml_not_found(mock_controller_instance):
558+
mock_controller_instance.get_flight_kml.side_effect = HTTPException(
559+
status_code=status.HTTP_404_NOT_FOUND
560+
)
561+
response = client.get('/flights/123/kml')
562+
assert response.status_code == 404
563+
assert response.json() == {'detail': 'Not Found'}
564+
565+
566+
def test_read_flight_kml_server_error(mock_controller_instance):
567+
mock_controller_instance.get_flight_kml.side_effect = HTTPException(
568+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
569+
)
570+
response = client.get('/flights/123/kml')
571+
assert response.status_code == 500
572+
assert response.json() == {'detail': 'Internal Server Error'}
573+
574+
542575
# --- Issue #56: Import flight from .rpy ---
543576

544577

0 commit comments

Comments
 (0)