feat(rover): show current pipeline step next to Calculate spinner
Compute.run now accepts a `progress` callback and reports human-readable labels before each pipeline step (loading grid, looking up elevation, checking road access, scoring cells, etc.). RoverLive feeds it via send/2 to itself and renders the latest label as small text to the left of the Calculate button while it's running.
This commit is contained in:
parent
88d785be50
commit
9f8c4d3b36
3 changed files with 80 additions and 16 deletions
|
|
@ -90,6 +90,7 @@ defmodule Microwaveprop.Rover.Compute do
|
||||||
Keyword.get(deps, :buildings_clutter_lookup, &default_building_clutter/1)
|
Keyword.get(deps, :buildings_clutter_lookup, &default_building_clutter/1)
|
||||||
|
|
||||||
hilltop_snap = Keyword.get(deps, :hilltop_snap, &Hilltop.snap/1)
|
hilltop_snap = Keyword.get(deps, :hilltop_snap, &Hilltop.snap/1)
|
||||||
|
progress = Keyword.get(deps, :progress, fn _ -> :ok end)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
home: home,
|
home: home,
|
||||||
|
|
@ -105,6 +106,7 @@ defmodule Microwaveprop.Rover.Compute do
|
||||||
radius_km = max_distance_km * 1.0
|
radius_km = max_distance_km * 1.0
|
||||||
bbox = bbox_around(home, radius_km)
|
bbox = bbox_around(home, radius_km)
|
||||||
|
|
||||||
|
progress.("Loading propagation grid")
|
||||||
raw_cells = time_step("scores_at", fn -> scores_at.(band_mhz, valid_time, bbox) end)
|
raw_cells = time_step("scores_at", fn -> scores_at.(band_mhz, valid_time, bbox) end)
|
||||||
|
|
||||||
in_radius =
|
in_radius =
|
||||||
|
|
@ -117,17 +119,26 @@ defmodule Microwaveprop.Rover.Compute do
|
||||||
)
|
)
|
||||||
|
|
||||||
points = Enum.map(in_radius, &{&1.lat, &1.lon})
|
points = Enum.map(in_radius, &{&1.lat, &1.lon})
|
||||||
|
progress.("Looking up elevation")
|
||||||
elev_map = time_step("elev_lookup", fn -> elev_lookup.(points) end)
|
elev_map = time_step("elev_lookup", fn -> elev_lookup.(points) end)
|
||||||
|
progress.("Loading building footprints")
|
||||||
_ = time_step("buildings_load", fn -> BuildingsLoader.ensure_loaded_for_bbox(bbox) end)
|
_ = time_step("buildings_load", fn -> BuildingsLoader.ensure_loaded_for_bbox(bbox) end)
|
||||||
|
progress.("Computing path clearance")
|
||||||
clearance_map = time_step("clearance", fn -> clearance_lookup.(in_radius, selected_stations) end)
|
clearance_map = time_step("clearance", fn -> clearance_lookup.(in_radius, selected_stations) end)
|
||||||
|
progress.("Measuring terrain prominence")
|
||||||
prominence_map = time_step("prominence", fn -> prominence_lookup.(in_radius) end)
|
prominence_map = time_step("prominence", fn -> prominence_lookup.(in_radius) end)
|
||||||
|
|
||||||
road_map =
|
road_map =
|
||||||
if Application.get_env(:microwaveprop, :rover_road_proximity_enabled, true),
|
if Application.get_env(:microwaveprop, :rover_road_proximity_enabled, true) do
|
||||||
do: time_step("road_proximity", fn -> fetch_road_map(road_lookup, in_radius, bbox) end),
|
progress.("Checking road access")
|
||||||
else: %{}
|
time_step("road_proximity", fn -> fetch_road_map(road_lookup, in_radius, bbox) end)
|
||||||
|
else
|
||||||
|
%{}
|
||||||
|
end
|
||||||
|
|
||||||
|
progress.("Scanning building clutter")
|
||||||
clutter_map = time_step("building_clutter", fn -> buildings_clutter_lookup.(in_radius) end)
|
clutter_map = time_step("building_clutter", fn -> buildings_clutter_lookup.(in_radius) end)
|
||||||
|
progress.("Scoring cells")
|
||||||
|
|
||||||
home_elev = home.elev_m || 0
|
home_elev = home.elev_m || 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,7 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
station_form_error: nil,
|
station_form_error: nil,
|
||||||
home_form_error: nil,
|
home_form_error: nil,
|
||||||
scoring_loading: false,
|
scoring_loading: false,
|
||||||
|
scoring_step: nil,
|
||||||
url_loaded?: false
|
url_loaded?: false
|
||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
@ -324,7 +325,12 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(top_candidates: cands, selected_candidate: nil, scoring_loading: false)
|
|> assign(
|
||||||
|
top_candidates: cands,
|
||||||
|
selected_candidate: nil,
|
||||||
|
scoring_loading: false,
|
||||||
|
scoring_step: nil
|
||||||
|
)
|
||||||
|> apply_scoring_warnings(warnings)
|
|> apply_scoring_warnings(warnings)
|
||||||
|> push_event("candidate_paths", %{candidate: nil, paths: []})
|
|> push_event("candidate_paths", %{candidate: nil, paths: []})
|
||||||
|> push_event("rover_results", %{
|
|> push_event("rover_results", %{
|
||||||
|
|
@ -338,10 +344,15 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(top_candidates: [], scoring_loading: false)
|
|> assign(top_candidates: [], scoring_loading: false, scoring_step: nil)
|
||||||
|> put_flash(:error, "Calculate failed: #{inspect(reason)}")}
|
|> put_flash(:error, "Calculate failed: #{inspect(reason)}")}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:rover_progress, label}, socket) do
|
||||||
|
{:noreply, assign(socket, scoring_step: label)}
|
||||||
|
end
|
||||||
|
|
||||||
defp apply_scoring_warnings(socket, []), do: clear_flash(socket)
|
defp apply_scoring_warnings(socket, []), do: clear_flash(socket)
|
||||||
|
|
||||||
defp apply_scoring_warnings(socket, warnings) do
|
defp apply_scoring_warnings(socket, warnings) do
|
||||||
|
|
@ -693,10 +704,12 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
|
|
||||||
defp start_scoring(socket) do
|
defp start_scoring(socket) do
|
||||||
args = scoring_args(socket)
|
args = scoring_args(socket)
|
||||||
|
pid = self()
|
||||||
|
progress = fn label -> send(pid, {:rover_progress, label}) end
|
||||||
|
|
||||||
socket
|
socket
|
||||||
|> assign(scoring_loading: true)
|
|> assign(scoring_loading: true, scoring_step: "Starting…")
|
||||||
|> start_async(:scoring, fn -> Compute.run(args) end)
|
|> start_async(:scoring, fn -> Compute.run(args, progress: progress) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp scoring_args(socket) do
|
defp scoring_args(socket) do
|
||||||
|
|
@ -908,15 +921,23 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
{band_label(@band)} · Forecast +{@forecast_hour}h
|
{band_label(@band)} · Forecast +{@forecast_hour}h
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex items-center gap-3">
|
||||||
type="button"
|
<span
|
||||||
phx-click="calculate"
|
:if={@scoring_loading and @scoring_step}
|
||||||
class="btn btn-primary btn-sm"
|
class="text-xs text-base-content/60 font-mono"
|
||||||
disabled={@scoring_loading}
|
>
|
||||||
>
|
{@scoring_step}
|
||||||
<span :if={@scoring_loading} class="loading loading-spinner loading-xs"></span>
|
</span>
|
||||||
{if @scoring_loading, do: "Calculating…", else: "Calculate"}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
|
phx-click="calculate"
|
||||||
|
class="btn btn-primary btn-sm"
|
||||||
|
disabled={@scoring_loading}
|
||||||
|
>
|
||||||
|
<span :if={@scoring_loading} class="loading loading-spinner loading-xs"></span>
|
||||||
|
{if @scoring_loading, do: "Calculating…", else: "Calculate"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="flex flex-1 min-h-0">
|
<div class="flex flex-1 min-h-0">
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,38 @@ defmodule Microwaveprop.Rover.ComputeTest do
|
||||||
assert ref_score - tall_score >= 5.5
|
assert ref_score - tall_score >= 5.5
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "calls progress callback with each pipeline step label" do
|
||||||
|
home = %{lat: 33.0, lon: -96.0, elev_m: 200}
|
||||||
|
stations = [%{callsign: "W5LUA", lat: 33.10, lon: -96.625, selected: true}]
|
||||||
|
grid = build_grid(home.lat, home.lon)
|
||||||
|
|
||||||
|
test_pid = self()
|
||||||
|
progress = fn label -> send(test_pid, {:progress, label}) end
|
||||||
|
|
||||||
|
Compute.run(
|
||||||
|
%{
|
||||||
|
home: home,
|
||||||
|
stations: stations,
|
||||||
|
band_mhz: 10_000,
|
||||||
|
valid_time: ~U[2026-04-25 12:00:00Z],
|
||||||
|
mode: :ssb,
|
||||||
|
max_distance_km: 65.0,
|
||||||
|
min_elev_gain: 0
|
||||||
|
},
|
||||||
|
scores_at: fn _, _, _ -> grid end,
|
||||||
|
elev_lookup: fn points -> Map.new(points, fn p -> {p, 250} end) end,
|
||||||
|
clearance_lookup: fn _, _ -> %{} end,
|
||||||
|
prominence_lookup: fn cells -> Map.new(cells, fn c -> {{c.lat, c.lon}, 0} end) end,
|
||||||
|
road_lookup: fn _, _ -> {:error, :stubbed} end,
|
||||||
|
progress: progress
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_received {:progress, "Loading propagation grid"}
|
||||||
|
assert_received {:progress, "Looking up elevation"}
|
||||||
|
assert_received {:progress, "Computing path clearance"}
|
||||||
|
assert_received {:progress, "Scoring cells"}
|
||||||
|
end
|
||||||
|
|
||||||
test "filters cells failing min_elev_gain" do
|
test "filters cells failing min_elev_gain" do
|
||||||
home = %{lat: 33.0, lon: -96.0, elev_m: 500}
|
home = %{lat: 33.0, lon: -96.0, elev_m: 500}
|
||||||
stations = [%{callsign: "W5LUA", lat: 33.1, lon: -96.6, selected: true}]
|
stations = [%{callsign: "W5LUA", lat: 33.1, lon: -96.6, selected: true}]
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue