neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
Show ADS-B aircraft on the query maps
a94201e · Gabriel Morell · 2026-08-10 22:29
Message
{commit_body(@commit)}
Files changed
modified
apps/room_sanctum/lib/room_sanctum_web/components/query_geospatial_map.ex
+25
−0
@@ -607,6 +607,31 @@ defmodule RoomSanctumWeb.Components.QueryGeospatialMap do
| 607 | 607 | end) |
| 608 | 608 | end |
| 609 | 609 | |
| 610 | + @doc """ | |
| 611 | + Aircraft to plot from a query preview. | |
| 612 | + """ | |
| 613 | + # The preview already holds what the worker returned for this query, so the | |
| 614 | + # map does not need its own read: an area query's result *is* the matching | |
| 615 | + # aircraft, filtered by class and altitude the way the query asks; a flight | |
| 616 | + # query returns one watch carrying the last position it was seen at. | |
| 617 | + # | |
| 618 | + # A watch that has not been sighted yet has no position, and is simply not | |
| 619 | + # plotted rather than being drawn somewhere invented. | |
| 620 | + def aircraft_from_preview(:icarus, preview) when is_list(preview) do | |
| 621 | + Enum.flat_map(preview, fn | |
| 622 | + %{"kind" => "flight", "position" => %{"lat" => _, "lon" => _} = position} -> [position] | |
| 623 | + %{"lat" => _, "lon" => _} = aircraft -> [aircraft] | |
| 624 | + _other -> [] | |
| 625 | + end) | |
| 626 | + end | |
| 627 | + | |
| 628 | + def aircraft_from_preview(_type, _preview), do: [] | |
| 629 | + | |
| 630 | + @doc """ | |
| 631 | + As aircraft_from_preview/2, for a preview already known to be icarus'. | |
| 632 | + """ | |
| 633 | + def aircraft_from_preview_list(preview), do: aircraft_from_preview(:icarus, preview) | |
| 634 | + | |
| 610 | 635 | # ADS-B aircraft. Positions are string-keyed because that is how they leave |
| 611 | 636 | # the adsb.fi payload, and the callsign is the useful label -- registration |
| 612 | 637 | # or the ICAO address only when the flight is not transmitting one. |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_query_live/index.ex
+27
−1
@@ -30,6 +30,7 @@ defmodule RoomSanctumWeb.QueryLive.Index do
| 30 | 30 | |> assign(:available_tints, available_tints) |
| 31 | 31 | |> assign(:queries, queries) |
| 32 | 32 | |> assign(:vehicle_positions, []) |
| 33 | + |> assign(:aircraft, []) | |
| 33 | 34 | |> assign(:show_route_lines, false) |
| 34 | 35 | |> assign(:route_lines, []) |
| 35 | 36 | |> stream(:cfg_queries, queries)} |
@@ -111,7 +112,32 @@ defmodule RoomSanctumWeb.QueryLive.Index do
| 111 | 112 | # Schedule next update |
| 112 | 113 | Process.send_after(self(), :update_vehicle_positions, 30_000) # Every 30 seconds |
| 113 | 114 | |
| 114 | − {:noreply, socket |> assign(:vehicle_positions, vehicle_positions)} | |
| 115 | + {:noreply, | |
| 116 | + socket | |
| 117 | + |> assign(:vehicle_positions, vehicle_positions) | |
| 118 | + |> assign(:aircraft, get_all_aircraft(socket.assigns.queries))} | |
| 119 | + end | |
| 120 | + | |
| 121 | + # One read per icarus query, so each contributes the aircraft it actually | |
| 122 | + # matches -- its own radius, altitude band and class filters -- rather than | |
| 123 | + # everything its source can see. The worker answers from cache between its | |
| 124 | + # own refreshes, and an absent one contributes nothing. | |
| 125 | + defp get_all_aircraft(queries) do | |
| 126 | + if Code.ensure_loaded?(RoomIcarus.Worker) do | |
| 127 | + queries | |
| 128 | + |> Enum.filter(&(&1.source && &1.source.type == :icarus)) | |
| 129 | + |> Enum.flat_map(fn query -> | |
| 130 | + try do | |
| 131 | + RoomIcarus.Worker.read(query.source_id, query.query) | |
| 132 | + catch | |
| 133 | + :exit, _ -> [] | |
| 134 | + end | |
| 135 | + end) | |
| 136 | + |> RoomSanctumWeb.Components.QueryGeospatialMap.aircraft_from_preview_list() | |
| 137 | + |> Enum.uniq_by(& &1["hex"]) | |
| 138 | + else | |
| 139 | + [] | |
| 140 | + end | |
| 115 | 141 | end |
| 116 | 142 | |
| 117 | 143 | # Handle streaming map data batches for high-performance rendering |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_query_live/index.html.heex
+1
−0
@@ -44,6 +44,7 @@
| 44 | 44 | <.query_geospatial_map |
| 45 | 45 | queries={@queries} |
| 46 | 46 | vehicle_positions={@vehicle_positions} |
| 47 | + aircraft={@aircraft} | |
| 47 | 48 | route_lines={if @show_route_lines, do: @route_lines, else: []} |
| 48 | 49 | route_lines_event={if Enum.any?(@queries, &(&1.source && &1.source.type == :gtfs)), do: "toggle-route-lines"} |
| 49 | 50 | show_route_lines={@show_route_lines} |
modified
apps/room_sanctum/lib/room_sanctum_web/live/cfg_query_live/show.html.heex
+1
−0
@@ -123,6 +123,7 @@
| 123 | 123 | <.query_geospatial_map |
| 124 | 124 | queries={[@query]} |
| 125 | 125 | vehicle_positions={@vehicle_positions} |
| 126 | + aircraft={aircraft_from_preview(@query.source && @query.source.type, @preview)} | |
| 126 | 127 | route_lines={if @show_route_lines, do: @route_lines, else: []} |
| 127 | 128 | route_lines_event={if @query.source && @query.source.type == :gtfs, do: "toggle-route-lines"} |
| 128 | 129 | show_route_lines={@show_route_lines} |
modified
apps/room_sanctum/test/room_sanctum_web/components/aircraft_map_test.exs
+52
−0
@@ -97,6 +97,58 @@ defmodule RoomSanctumWeb.AircraftMapTest do
| 97 | 97 | assert html =~ ~s(lat="51.47") |
| 98 | 98 | end |
| 99 | 99 | |
| 100 | + describe "aircraft from a query preview" do | |
| 101 | + test "an area query's preview is already the matching aircraft" do | |
| 102 | + preview = [plane(%{"hex" => "aaa"}), plane(%{"hex" => "bbb"})] | |
| 103 | + | |
| 104 | + assert aircraft_from_preview(:icarus, preview) == preview | |
| 105 | + end | |
| 106 | + | |
| 107 | + test "a flight query contributes the position its watch was last seen at" do | |
| 108 | + watch = %{ | |
| 109 | + "kind" => "flight", | |
| 110 | + "callsign" => "UAL558", | |
| 111 | + "hex" => "a1b2c3", | |
| 112 | + "state" => "enroute", | |
| 113 | + "position" => plane(%{}) | |
| 114 | + } | |
| 115 | + | |
| 116 | + assert aircraft_from_preview(:icarus, [watch]) == [plane(%{})] | |
| 117 | + end | |
| 118 | + | |
| 119 | + test "a watch with no sighting yet is not plotted somewhere invented" do | |
| 120 | + for position <- [nil, %{}, %{"lat" => 37.6}] do | |
| 121 | + watch = %{"kind" => "flight", "callsign" => "UAL558", "position" => position} | |
| 122 | + assert aircraft_from_preview(:icarus, [watch]) == [], | |
| 123 | + "plotted a watch whose position was #{inspect(position)}" | |
| 124 | + end | |
| 125 | + end | |
| 126 | + | |
| 127 | + test "other source types contribute nothing" do | |
| 128 | + assert aircraft_from_preview(:gtfs, [plane(%{})]) == [] | |
| 129 | + assert aircraft_from_preview(:gbfs, [plane(%{})]) == [] | |
| 130 | + end | |
| 131 | + | |
| 132 | + test "a preview that is not a list is tolerated" do | |
| 133 | + assert aircraft_from_preview(:icarus, nil) == [] | |
| 134 | + assert aircraft_from_preview(:icarus, %{"lat" => 1.0, "lon" => 2.0}) == [] | |
| 135 | + end | |
| 136 | + | |
| 137 | + test "entries without a position are skipped rather than failing the lot" do | |
| 138 | + preview = [plane(%{"hex" => "ok"}), %{"hex" => "nofix", "flight" => "GHOST"}] | |
| 139 | + | |
| 140 | + assert aircraft_from_preview(:icarus, preview) == [plane(%{"hex" => "ok"})] | |
| 141 | + end | |
| 142 | + | |
| 143 | + test "what it returns is what the map renders" do | |
| 144 | + watch = %{"kind" => "flight", "position" => plane(%{"hex" => "inflight"})} | |
| 145 | + html = render_map(aircraft_from_preview(:icarus, [watch])) | |
| 146 | + | |
| 147 | + assert html =~ "aircraft_inflight" | |
| 148 | + assert html =~ ~s(bearing="271.4") | |
| 149 | + end | |
| 150 | + end | |
| 151 | + | |
| 100 | 152 | test "no aircraft is not an error" do |
| 101 | 153 | html = render_map([]) |
| 102 | 154 |
Parents: 5f17b6e