diff --git a/lib/microwaveprop_web/live/rover_planning_live/show.ex b/lib/microwaveprop_web/live/rover_planning_live/show.ex index 802cf90e..bc643e84 100644 --- a/lib/microwaveprop_web/live/rover_planning_live/show.ex +++ b/lib/microwaveprop_web/live/rover_planning_live/show.ex @@ -10,6 +10,7 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do alias Microwaveprop.Radio.Maidenhead alias Microwaveprop.RoverPlanning alias Microwaveprop.RoverPlanning.Mission + alias Phoenix.LiveView.JS @impl true def mount(%{"id" => id}, _session, socket) do @@ -152,6 +153,9 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do defp format_meters(nil), do: "—" defp format_meters(value) when is_number(value), do: "#{Float.round(value * 1.0, 1)} m" + defp station_label(%{station: %{callsign: c, grid: g}}) when is_binary(c) and c != "" and is_binary(g) and g != "", + do: "#{c} · #{g}" + defp station_label(%{station: %{callsign: c}}) when is_binary(c) and c != "", do: c defp station_label(%{station: %{grid: g}}) when is_binary(g) and g != "", do: g @@ -160,6 +164,41 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do defp station_label(_), do: "—" + # Endpoint string used for /path?destination=…. Prefers callsign, then + # grid (so PathLive's LocationResolver re-resolves to the same point), + # falling back to bare coordinates. + defp station_endpoint(%{callsign: c}) when is_binary(c) and c != "", do: c + defp station_endpoint(%{grid: g}) when is_binary(g) and g != "", do: g + + defp station_endpoint(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon), do: "#{lat},#{lon}" + + defp station_endpoint(_), do: "" + + # Builds the /path URL pre-filling source (rover-location, max-precision + # grid), destination (station endpoint), band, and rover/station heights + # so the user lands on a ready-to-compute Path Calculator screen. + defp path_url(_mission, %{rover_location: nil}), do: nil + defp path_url(_mission, %{station: nil}), do: nil + + defp path_url(%Mission{} = mission, %{rover_location: %{lat: lat, lon: lon}, station: station}) + when is_number(lat) and is_number(lon) do + src = Maidenhead.from_latlon(lat, lon, 10) + dst = station_endpoint(station) + + query = + URI.encode_query(%{ + "source" => src, + "destination" => dst, + "band" => Integer.to_string(mission.band_mhz), + "src_height_ft" => Float.to_string(mission.rover_height_ft * 1.0), + "dst_height_ft" => Float.to_string(mission.station_height_ft * 1.0) + }) + + "/path?" <> query + end + + defp path_url(_, _), do: nil + @impl true def render(assigns) do summary = progress_summary(assigns.paths) @@ -270,7 +309,14 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do - + {station_label(path)} {status_badge(path.status)} @@ -297,7 +343,10 @@ defmodule MicrowavepropWeb.RoverPlanningLive.Show do end defp rover_location_heading(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon) do - grid = Maidenhead.from_latlon(lat, lon, 6) + # 10-char grid is the maximum useful Maidenhead precision; anything + # finer is below the float lat/lon's resolution. The user wants the + # full precision, not a 6-char truncation. + grid = Maidenhead.from_latlon(lat, lon, 10) "#{grid} (#{Float.round(lat, 4)}, #{Float.round(lon, 4)})" end diff --git a/test/microwaveprop_web/live/rover_planning_live_test.exs b/test/microwaveprop_web/live/rover_planning_live_test.exs index fca4121a..c7623346 100644 --- a/test/microwaveprop_web/live/rover_planning_live_test.exs +++ b/test/microwaveprop_web/live/rover_planning_live_test.exs @@ -4,6 +4,7 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do import Phoenix.LiveViewTest alias Microwaveprop.AccountsFixtures + alias Microwaveprop.Radio.Maidenhead alias Microwaveprop.Repo alias Microwaveprop.Rover alias Microwaveprop.RoverPlanning @@ -74,6 +75,50 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do assert html =~ "Path profiles" end + test "group heading uses max-precision Maidenhead grid (10 chars)", %{conn: conn} do + user = AccountsFixtures.user_fixture() + mission = create_mission(user, "Precision mission") + + {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}") + + # Maidenhead.from_latlon(32.0, -97.0, 10) → "EM12VX00AA" (or similar + # 10-char grid). The group heading must NOT truncate at 6 chars. + grid_10 = Maidenhead.from_latlon(32.0, -97.0, 10) + assert String.length(grid_10) == 10 + assert html =~ grid_10 + end + + test "station label shows callsign + grid together when both are known", %{conn: conn} do + user = AccountsFixtures.user_fixture() + mission = create_mission(user, "Callsign-grid mission") + + [path | _] = Path |> Repo.all() |> Repo.preload(:station) + {:ok, _} = path.station |> Ecto.Changeset.change(%{callsign: "AA5C", grid: "EM13se"}) |> Repo.update() + + {:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}") + + # The path table should display "AA5C · EM13se" so the operator can + # see both at a glance — not just one or the other and not lat/lon. + assert html =~ "AA5C · EM13se" + end + + test "path rows link to /path with mission params prefilled", %{conn: conn} do + user = AccountsFixtures.user_fixture() + mission = create_mission(user, "Click-through mission") + + {:ok, lv, _html} = live(conn, ~p"/rover-planning/#{mission.id}") + + # Each path row carries a phx-click that navigates to /path with + # the mission's band + heights and the rover/station endpoints + # filled in. Assert the click handler is present on the . + rendered = render(lv) + assert rendered =~ "phx-click" + assert rendered =~ "/path?" + assert rendered =~ "band=10000" + assert rendered =~ "src_height_ft=8.0" + assert rendered =~ "dst_height_ft=30.0" + end + test "groups paths by rover location", %{conn: conn} do user = AccountsFixtures.user_fixture()