Skip to content
Merged
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
77 changes: 58 additions & 19 deletions monitoring/monitorlib/kml/generation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import math
from datetime import datetime

import s2sphere
from pykml.factory import KML_ElementMaker as kml
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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 [
Expand Down
149 changes: 149 additions & 0 deletions monitoring/monitorlib/kml/rid.py
Original file line number Diff line number Diff line change
@@ -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",
),
]
24 changes: 20 additions & 4 deletions monitoring/uss_qualifier/reports/sequence_view/kml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand All @@ -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


Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand All @@ -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")
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@
<tr>
<td><a href="../../../requirements/astm/f3411/v22a.md">NET0340</a></td>
<td>Implemented</td>
<td><a href="../../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
<td><a href="../../../scenarios/astm/netrid/v22a/networked_uas_disconnect.md">ASTM NetRID networked UAS disconnection</a><br><a href="../../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
</tr>
<tr>
<td><a href="../../../requirements/astm/f3411/v22a.md">NET0420</a></td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0340</a></td>
<td>Implemented</td>
<td><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
<td><a href="../../scenarios/astm/netrid/v22a/networked_uas_disconnect.md">ASTM NetRID networked UAS disconnection</a><br><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
</tr>
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0420</a></td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0340</a></td>
<td>Implemented</td>
<td><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
<td><a href="../../scenarios/astm/netrid/v22a/networked_uas_disconnect.md">ASTM NetRID networked UAS disconnection</a><br><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
</tr>
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0420</a></td>
Expand Down
Loading