diff --git a/lib/microwaveprop/rover/compute.ex b/lib/microwaveprop/rover/compute.ex index 30a0cc46..2d69895f 100644 --- a/lib/microwaveprop/rover/compute.ex +++ b/lib/microwaveprop/rover/compute.ex @@ -38,7 +38,11 @@ defmodule Microwaveprop.Rover.Compute do min_elev_gain: integer() } - @spec run(run_args(), keyword()) :: %{cells: [map()], top_candidates: [map()]} + @spec run(run_args(), keyword()) :: %{ + cells: [map()], + top_candidates: [map()], + warnings: [String.t()] + } def run(args, deps \\ []) do scores_at = Keyword.get(deps, :scores_at, &Propagation.scores_at/3) elev_lookup = Keyword.get(deps, :elev_lookup, &Elevation.lookup_many/1) @@ -76,15 +80,44 @@ defmodule Microwaveprop.Rover.Compute do |> Enum.filter(&keep_cell?(&1, home_elev, min_elev_gain)) top_candidates = - cells |> Enum.sort_by(& &1.score, :desc) |> Enum.take(@top_n) |> Enum.map(&candidate_payload(&1, home)) + cells + |> Enum.sort_by(& &1.score, :desc) + |> Enum.take(@top_n) + |> Enum.map(&candidate_payload(&1, home)) - %{cells: cells, top_candidates: top_candidates} + warnings = build_warnings(raw_cells, in_radius, elev_map, cells) + + %{cells: cells, top_candidates: top_candidates, warnings: warnings} + end + + defp build_warnings(raw_cells, in_radius, elev_map, cells) do + [] + |> add_warning_if(raw_cells == [], "No HRRR score grid available for this band/time.") + |> add_warning_if(in_radius == [], "No score grid cells within the drive radius.") + |> add_warning_if( + in_radius != [] and elev_map_all_nil?(elev_map), + "Elevation tiles unavailable (SRTM not mounted); ranking ignores elevation." + ) + |> add_warning_if( + in_radius != [] and cells == [], + "All cells filtered out by score/elevation thresholds." + ) + end + + defp add_warning_if(list, true, msg), do: list ++ [msg] + defp add_warning_if(list, false, _msg), do: list + + defp elev_map_all_nil?(elev_map) do + elev_map != %{} and Enum.all?(elev_map, fn {_k, v} -> is_nil(v) end) end defp keep_cell?(nil, _home_elev, _min_gain), do: false - defp keep_cell?(%{elev_m: elev_m, score: score}, home_elev, min_gain) when is_integer(elev_m) do - elev_m - home_elev >= min_gain and score >= 0 + defp keep_cell?(%{score: score, elev_m: elev_m}, home_elev, min_gain) do + # Cells with unknown elevation (no SRTM tile) are kept; elev gain + # filter only applies when we actually know the cell elevation. + elev_ok? = is_nil(elev_m) or elev_m - home_elev >= min_gain + elev_ok? and score >= 0 end defp keep_cell?(_, _, _), do: false @@ -92,33 +125,27 @@ defmodule Microwaveprop.Rover.Compute do defp annotate_cell(cell, elev_map, home, home_elev, mode, stations) do elev_m = Map.get(elev_map, {cell.lat, cell.lon}) - case elev_m do + margins = Enum.map(stations, fn _s -> LinkMargin.link_margin_from_score(cell.score, mode) end) + agg = Aggregator.cell_margin_db(margins) + + case agg do nil -> nil - _ -> - margins = Enum.map(stations, fn _s -> LinkMargin.link_margin_from_score(cell.score, mode) end) - agg = Aggregator.cell_margin_db(margins) + agg_db -> + dist_km = DriveTime.haversine_km({home.lat, home.lon}, {cell.lat, cell.lon}) + elev_bonus = elev_bonus(elev_m, home_elev) + drive_penalty = drive_penalty(dist_km) + score = agg_db + elev_bonus - drive_penalty - case agg do - nil -> - nil - - agg_db -> - dist_km = DriveTime.haversine_km({home.lat, home.lon}, {cell.lat, cell.lon}) - elev_bonus = elev_bonus(elev_m, home_elev) - drive_penalty = drive_penalty(dist_km) - score = agg_db + elev_bonus - drive_penalty - - %{ - lat: cell.lat, - lon: cell.lon, - elev_m: elev_m, - score: score, - distance_km: dist_km, - tier_color: tier_color(score) - } - end + %{ + lat: cell.lat, + lon: cell.lon, + elev_m: elev_m, + score: score, + distance_km: dist_km, + tier_color: tier_color(score) + } end end @@ -141,6 +168,8 @@ defmodule Microwaveprop.Rover.Compute do } end + defp elev_bonus(nil, _elev_home), do: 0.0 + defp elev_bonus(elev_c, elev_home) do diff = (elev_c - elev_home) / 100.0 * @elev_bonus_db_per_100m diff |> max(0.0) |> min(@elev_bonus_cap_db) diff --git a/lib/microwaveprop_web/live/rover_live.ex b/lib/microwaveprop_web/live/rover_live.ex index 29ab7fe9..2c0fdf99 100644 --- a/lib/microwaveprop_web/live/rover_live.ex +++ b/lib/microwaveprop_web/live/rover_live.ex @@ -255,10 +255,13 @@ defmodule MicrowavepropWeb.RoverLive do # ── Async ──────────────────────────────────────────────────────────── @impl true - def handle_async(:scoring, {:ok, %{cells: cells, top_candidates: cands}}, socket) do + def handle_async(:scoring, {:ok, %{cells: cells, top_candidates: cands} = result}, socket) do + warnings = Map.get(result, :warnings, []) + {:noreply, socket |> assign(top_candidates: cands, scoring_loading: false) + |> apply_scoring_warnings(warnings) |> push_event("rover_results", %{ cells: cells, drive_radius_km: socket.assigns.drive_radius_km @@ -267,7 +270,17 @@ defmodule MicrowavepropWeb.RoverLive do def handle_async(:scoring, {:exit, reason}, socket) do Logger.error("rover scoring crashed: #{inspect(reason)}") - {:noreply, assign(socket, top_candidates: [], scoring_loading: false)} + + {:noreply, + socket + |> assign(top_candidates: [], scoring_loading: false) + |> put_flash(:error, "Calculate failed: #{inspect(reason)}")} + end + + defp apply_scoring_warnings(socket, []), do: clear_flash(socket) + + defp apply_scoring_warnings(socket, warnings) do + put_flash(socket, :info, Enum.join(warnings, " ")) end # ── Mutation helpers ────────────────────────────────────────────────