diff --git a/lib/microwaveprop/rover/compute.ex b/lib/microwaveprop/rover/compute.ex index d0fcffed..74fcb98e 100644 --- a/lib/microwaveprop/rover/compute.ex +++ b/lib/microwaveprop/rover/compute.ex @@ -90,6 +90,7 @@ defmodule Microwaveprop.Rover.Compute do Keyword.get(deps, :buildings_clutter_lookup, &default_building_clutter/1) hilltop_snap = Keyword.get(deps, :hilltop_snap, &Hilltop.snap/1) + progress = Keyword.get(deps, :progress, fn _ -> :ok end) %{ home: home, @@ -105,6 +106,7 @@ defmodule Microwaveprop.Rover.Compute do radius_km = max_distance_km * 1.0 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) in_radius = @@ -117,17 +119,26 @@ defmodule Microwaveprop.Rover.Compute do ) points = Enum.map(in_radius, &{&1.lat, &1.lon}) + progress.("Looking up elevation") 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) + progress.("Computing path clearance") 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) road_map = - if Application.get_env(:microwaveprop, :rover_road_proximity_enabled, true), - do: time_step("road_proximity", fn -> fetch_road_map(road_lookup, in_radius, bbox) end), - else: %{} + if Application.get_env(:microwaveprop, :rover_road_proximity_enabled, true) do + progress.("Checking road access") + 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) + progress.("Scoring cells") home_elev = home.elev_m || 0 diff --git a/lib/microwaveprop_web/live/rover_live.ex b/lib/microwaveprop_web/live/rover_live.ex index c19c1e2d..a9b14005 100644 --- a/lib/microwaveprop_web/live/rover_live.ex +++ b/lib/microwaveprop_web/live/rover_live.ex @@ -66,6 +66,7 @@ defmodule MicrowavepropWeb.RoverLive do station_form_error: nil, home_form_error: nil, scoring_loading: false, + scoring_step: nil, url_loaded?: false )} end @@ -324,7 +325,12 @@ defmodule MicrowavepropWeb.RoverLive do {:noreply, 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) |> push_event("candidate_paths", %{candidate: nil, paths: []}) |> push_event("rover_results", %{ @@ -338,10 +344,15 @@ defmodule MicrowavepropWeb.RoverLive do {:noreply, socket - |> assign(top_candidates: [], scoring_loading: false) + |> assign(top_candidates: [], scoring_loading: false, scoring_step: nil) |> put_flash(:error, "Calculate failed: #{inspect(reason)}")} 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, warnings) do @@ -693,10 +704,12 @@ defmodule MicrowavepropWeb.RoverLive do defp start_scoring(socket) do args = scoring_args(socket) + pid = self() + progress = fn label -> send(pid, {:rover_progress, label}) end socket - |> assign(scoring_loading: true) - |> start_async(:scoring, fn -> Compute.run(args) end) + |> assign(scoring_loading: true, scoring_step: "Starting…") + |> start_async(:scoring, fn -> Compute.run(args, progress: progress) end) end defp scoring_args(socket) do @@ -908,15 +921,23 @@ defmodule MicrowavepropWeb.RoverLive do {band_label(@band)} · Forecast +{@forecast_hour}h - +
+ + {@scoring_step} + + +
diff --git a/test/microwaveprop/rover/compute_test.exs b/test/microwaveprop/rover/compute_test.exs index 50fa6cd0..d060948f 100644 --- a/test/microwaveprop/rover/compute_test.exs +++ b/test/microwaveprop/rover/compute_test.exs @@ -129,6 +129,38 @@ defmodule Microwaveprop.Rover.ComputeTest do assert ref_score - tall_score >= 5.5 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 home = %{lat: 33.0, lon: -96.0, elev_m: 500} stations = [%{callsign: "W5LUA", lat: 33.1, lon: -96.6, selected: true}]