diff --git a/monitoring/monitorlib/kml/generation.py b/monitoring/monitorlib/kml/generation.py index fd45630d84..6aa00175b3 100644 --- a/monitoring/monitorlib/kml/generation.py +++ b/monitoring/monitorlib/kml/generation.py @@ -1,4 +1,5 @@ import math +from datetime import datetime import s2sphere from pykml.factory import KML_ElementMaker as kml @@ -48,6 +49,37 @@ def _distance_value_of(distance: Altitude | Radius) -> float: raise NotImplementedError(f"Distance units {distance.units} not yet supported") +def make_basic_placemark( + name: str | None = None, + style_url: str | None = None, + description: str | None = None, + time_start: datetime | None = None, + time_end: datetime | None = None, +): + # Create placemark + args = [] + if name is not None: + args.append(kml.name(name)) + if style_url is not None: + args.append(kml.styleUrl(style_url)) + placemark = kml.Placemark(*args) + if description: + placemark.append(kml.description(description)) + + # Set time range + timespan = None + if time_start: + timespan = kml.TimeSpan(kml.begin(time_start.isoformat())) + if time_end: + if timespan is None: + timespan = kml.TimeSpan() + timespan.append(kml.end(time_end.isoformat())) + if timespan is not None: + placemark.append(timespan) + + return placemark + + def make_placemark_from_volume( v4: Volume4D, name: str | None = None, @@ -74,25 +106,15 @@ def make_placemark_from_volume( raise NotImplementedError("No vertices found") # Create placemark - args = [] - if name is not None: - args.append(kml.name(name)) - if style_url is not None: - args.append(kml.styleUrl(style_url)) - placemark = kml.Placemark(*args) - if description: - placemark.append(kml.description(description)) - - # Set time range - timespan = None - if "time_start" in v4 and v4.time_start: - timespan = kml.TimeSpan(kml.begin(v4.time_start.datetime.isoformat())) - if "time_end" in v4 and v4.time_end: - if timespan is None: - timespan = kml.TimeSpan() - timespan.append(kml.end(v4.time_end.datetime.isoformat())) - if timespan is not None: - placemark.append(timespan) + placemark = make_basic_placemark( + name=name, + style_url=style_url, + description=description, + time_start=v4.time_start.datetime + if "time_start" in v4 and v4.time_start + else None, + time_end=v4.time_end.datetime if "time_end" in v4 and v4.time_end else None, + ) # Create top and bottom of the volume avg = s2sphere.LatLng.from_degrees( @@ -192,6 +214,23 @@ def make_placemark_from_volume( return placemark +def add_point( + placemark, + lat_degrees: float, + lng_degrees: float, +) -> None: + placemark.append(kml.Point(kml.coordinates(f"{lng_degrees},{lat_degrees},0"))) + + +def add_linestring( + placemark, + lng_lat_alt: list[tuple[float, float, float]], +): + # Create the point + coord_string = " ".join(f"{lng},{lat},{alt}" for (lng, lat, alt) in lng_lat_alt) + placemark.append(kml.LineString(kml.tessellate(1), kml.coordinates(coord_string))) + + def query_styles() -> list: """Provides KML styles for query areas.""" return [ diff --git a/monitoring/monitorlib/kml/rid.py b/monitoring/monitorlib/kml/rid.py new file mode 100644 index 0000000000..062e46e51b --- /dev/null +++ b/monitoring/monitorlib/kml/rid.py @@ -0,0 +1,149 @@ +from datetime import timedelta + +from pykml.factory import KML_ElementMaker as kml +from uas_standards.astm.f3411.v22a.api import GetFlightsResponse +from uas_standards.interuss.automated_testing.rid.v1.injection import ( + ChangeTestResponse, + CreateTestParameters, +) + +from monitoring.monitorlib.kml.generation import ( + add_linestring, + add_point, + make_basic_placemark, +) + + +def create_test(req: CreateTestParameters, resp: ChangeTestResponse) -> list: + result = [] + for test_flight in resp.injected_flights: + folder = kml.Folder(kml.name(f"Test flight {test_flight.injection_id}")) + requested_flight = None + for flight in req.requested_flights: + if flight.injection_id == test_flight.injection_id: + requested_flight = flight + for i, telemetry in enumerate(test_flight.telemetry): + if ( + "position" in telemetry + and telemetry.position + and "lat" in telemetry.position + and telemetry.position.lat + and "lng" in telemetry.position + and telemetry.position.lng + ): + style_url = "#modifiedtelemetry" + if requested_flight and len(requested_flight.telemetry) > i: + rt = requested_flight.telemetry[i] + if ( + "position" in rt + and rt.position + and "lat" in rt.position + and rt.position.lat == telemetry.position.lat + and "lng" in rt.position + and rt.position.lng == telemetry.position.lng + ): + style_url = "#unmodifiedtelemetry" + if "timestamp" in telemetry and telemetry.timestamp: + time_start = telemetry.timestamp.datetime + if i < len(test_flight.telemetry) - 1: + tt = test_flight.telemetry[i + 1] + if "timestamp" in tt and tt.timestamp: + time_end = tt.timestamp.datetime + else: + time_end = time_start + timedelta(seconds=1) + else: + time_end = time_start + timedelta(seconds=1) + else: + time_start = None + time_end = None + point = make_basic_placemark( + name=f"Telemetry {i}", + style_url=style_url, + time_start=time_start, + time_end=time_end, + ) + add_point(point, telemetry.position.lat, telemetry.position.lng) + folder.append(point) + result.append(folder) + return result + + +def get_flights_v22a(url: str, resp: GetFlightsResponse) -> list: + result = [] + # TODO: render request bounding box from query parameter in url + for flight in resp.flights or [] if "flights" in resp else []: + folder = kml.Folder(kml.name(flight.id)) + if ( + "current_state" in flight + and flight.current_state + and "lat" in flight.current_state.position + and flight.current_state.position.lat + and "lng" in flight.current_state.position + and flight.current_state.position.lng + ): + point = make_basic_placemark( + name=f"{flight.aircraft_type.name} {flight.id}{' SIMULATED' if flight.simulated else ''}", + style_url="#aircraft", + ) + add_point( + point, + flight.current_state.position.lat, + flight.current_state.position.lng, + ) + folder.append(point) + if "recent_positions" in flight and flight.recent_positions: + tail = make_basic_placemark( + name=f"{flight.id} recent positions", style_url="#ridtail" + ) + add_linestring( + tail, + [ + (rp.position.lng, rp.position.lat, 0) + for rp in flight.recent_positions + if "lng" in rp.position + and rp.position.lng + and "lat" in rp.position + and rp.position.lat + ], + ) + folder.append(tail) + + result.append(folder) + return result + + +def rid_styles() -> list: + """Provides KML styles used by RID visualizations above.""" + return [ + kml.Style( + kml.IconStyle( + kml.scale(1.4), + kml.Icon( + kml.href( + "http://maps.google.com/mapfiles/kml/shapes/placemark_circle.png" + ) + ), + ), + id="unmodifiedtelemetry", + ), + kml.Style( + kml.IconStyle( + kml.scale(1.4), + kml.Icon( + kml.href( + "http://maps.google.com/mapfiles/kml/shapes/placemark_square.png" + ) + ), + ), + id="modifiedtelemetry", + ), + kml.Style( + kml.IconStyle( + kml.scale(1.4), + kml.Icon( + kml.href("http://maps.google.com/mapfiles/kml/shapes/airports.png") + ), + ), + id="aircraft", + ), + ] diff --git a/monitoring/uss_qualifier/reports/sequence_view/kml.py b/monitoring/uss_qualifier/reports/sequence_view/kml.py index f21f2ac714..511c6aa79f 100644 --- a/monitoring/uss_qualifier/reports/sequence_view/kml.py +++ b/monitoring/uss_qualifier/reports/sequence_view/kml.py @@ -2,10 +2,10 @@ from typing import Protocol, get_type_hints from implicitdict import ImplicitDict -from loguru import logger from lxml import etree from pykml.factory import KML_ElementMaker as kml from pykml.util import format_xml_with_cdata +from uas_standards.astm.f3411.v22a import api as f3411v22a from uas_standards.astm.f3548.v21.api import ( GetOperationalIntentDetailsResponse, QueryOperationalIntentReferenceParameters, @@ -15,6 +15,7 @@ UpsertFlightPlanRequest, UpsertFlightPlanResponse, ) +from uas_standards.interuss.automated_testing.rid.v1 import injection as rid_injection from monitoring.monitorlib.errors import stacktrace_string from monitoring.monitorlib.fetch import Query, QueryType @@ -28,6 +29,7 @@ upsert_flight_plan, ) from monitoring.monitorlib.kml.generation import query_styles +from monitoring.monitorlib.kml.rid import create_test, get_flights_v22a, rid_styles from monitoring.uss_qualifier.reports.sequence_view.summary_types import TestedScenario @@ -125,7 +127,6 @@ def make_scenario_kml(scenario: TestedScenario) -> str: ) except ValueError as e: msg = f"Error parsing request into {render_info.request_type.__name__}" - logger.warning(msg) query_folder.append( kml.Folder( kml.name(msg), @@ -144,7 +145,6 @@ def make_scenario_kml(scenario: TestedScenario) -> str: ) except ValueError as e: msg = f"Error parsing response into {render_info.response_type.__name__}" - logger.warning(msg) query_folder.append( kml.Folder( kml.name(msg), @@ -164,7 +164,11 @@ def make_scenario_kml(scenario: TestedScenario) -> str: ) doc = kml.kml( kml.Document( - *query_styles(), *f3548v21_styles(), *flight_planning_styles(), top_folder + *query_styles(), + *f3548v21_styles(), + *flight_planning_styles(), + *rid_styles(), + top_folder, ) ) return etree.tostring(format_xml_with_cdata(doc), pretty_print=True).decode("utf-8") @@ -188,3 +192,15 @@ def render_flight_planning_upsert_flight_plan( req: UpsertFlightPlanRequest, resp: UpsertFlightPlanResponse ): return [upsert_flight_plan(req, resp)] + + +@query_kml_renderer(QueryType.InterussRIDAutomatedTestingV1CreateTest) # pyright: ignore[reportArgumentType] +def render_rid_injection_create_test( + req: rid_injection.CreateTestParameters, resp: rid_injection.ChangeTestResponse +): + return create_test(req, resp) + + +@query_kml_renderer(QueryType.F3411v22aUSSSearchFlights) # pyright: ignore[reportArgumentType] +def render_f3411_22a_search_flights(query: Query, resp: f3411v22a.GetFlightsResponse): + return get_flights_v22a(query.request.url, resp) diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/display_data_evaluator_flight_presence.md b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/display_data_evaluator_flight_presence.md index 8eb16f0e10..f50111fa46 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/display_data_evaluator_flight_presence.md +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/display_data_evaluator_flight_presence.md @@ -10,9 +10,9 @@ The timestamps of the injected telemetry usually start in the future. If a flig **[astm.f3411.v22a.NET0610](../../../../requirements/astm/f3411/v22a.md)** requires that SPs make all UAS operations discoverable over the duration of the flight plus *NetMaxNearRealTimeDataPeriod*, so each injected flight should be observable during this time. If a flight is not observed during its appropriate time period, this check will fail. -**[astm.f3411.v22a.NET0710,1](../../../../requirements/astm/f3411/v22a.md)** and **[astm.f3411.v22a.NET0340](../../../../requirements/astm/f3411/v22a.md) require a Service Provider to implement the GET flights endpoint. This check will also fail if uss_qualifier cannot query that endpoint (specified in the ISA present in the DSS) successfully. +**[astm.f3411.v22a.NET0710,1](../../../../requirements/astm/f3411/v22a.md)** and **[astm.f3411.v22a.NET0340](../../../../requirements/astm/f3411/v22a.md)** require a Service Provider to implement the GET flights endpoint. This check will also fail if uss_qualifier cannot query that endpoint (specified in the ISA present in the DSS) successfully. -The identity of flights is determined by precisely matching the known injected positions. If the flight can be found, the USS may not have met **[astm.f3411.v22a.NET0260,Table1,10](../../../../requirements/astm/f3411/v22a.md)** or **[astm.f3411.v22a.NET0260,Table1,11](../../../../requirements/astm/f3411/v22a.md)** prescribing provision of position data consistent with the common data dictionary. +The identity of flights is determined by precisely matching the known injected positions. If the flight cannot be found, the USS may not have met **[astm.f3411.v22a.NET0260,Table1,10](../../../../requirements/astm/f3411/v22a.md)** or **[astm.f3411.v22a.NET0260,Table1,11](../../../../requirements/astm/f3411/v22a.md)** prescribing provision of position data consistent with the common data dictionary. ## ⚠️ Lingering flight check diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md index 0f71c7b304..c83c8bfb02 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md @@ -428,7 +428,7 @@