fix(rover-planning): derive grid from lat/lon for coord-only stations

When a user types raw "lat,lon" coordinates into a station input,
LocationResolver populates only `lat` / `lon` — `callsign` and `grid`
stay blank. The path-profiles "Station" cell was falling through to
"33.189, -96.452" instead of showing a useful grid.

Now `station_label/1` (display) and `station_endpoint/1` (path-URL
destination) derive a 6-/8-char Maidenhead grid from the stored
lat/lon when no grid is on the row, matching the rest of the page's
grid-first presentation.
This commit is contained in:
Graham McIntire 2026-05-03 12:43:32 -05:00
parent 7edfc64996
commit 5c3a791665
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 51 additions and 12 deletions

View file

@ -153,24 +153,36 @@ 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
defp station_label(%{station: %{lat: lat, lon: lon}}) when is_number(lat) and is_number(lon),
do: "#{Float.round(lat, 3)}, #{Float.round(lon, 3)}"
# Grid+callsign-first station label. Stations entered as raw lat/lon
# have no callsign or grid stored, so derive a 6-char grid from the
# coords — that's what the user wants to see, not bare decimals.
defp station_label(%{station: %{} = station}), do: station_label_for(station)
defp station_label(_), do: ""
defp station_label_for(%{callsign: c, grid: g}) when is_binary(c) and c != "" and is_binary(g) and g != "",
do: "#{c} · #{g}"
defp station_label_for(%{callsign: c, lat: lat, lon: lon})
when is_binary(c) and c != "" and is_number(lat) and is_number(lon),
do: "#{c} · #{Maidenhead.from_latlon(lat, lon, 6)}"
defp station_label_for(%{callsign: c}) when is_binary(c) and c != "", do: c
defp station_label_for(%{grid: g}) when is_binary(g) and g != "", do: g
defp station_label_for(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon),
do: Maidenhead.from_latlon(lat, lon, 6)
defp station_label_for(_), 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.
# any stored or derived grid (PathLive's LocationResolver re-resolves
# both back to the same point), falling back to bare coordinates only
# when nothing else is available.
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(%{lat: lat, lon: lon}) when is_number(lat) and is_number(lon),
do: Maidenhead.from_latlon(lat, lon, 8)
defp station_endpoint(_), do: ""

View file

@ -88,6 +88,33 @@ defmodule MicrowavepropWeb.RoverPlanningLiveTest do
assert html =~ grid_10
end
test "station label uses derived grid when only lat/lon are stored", %{conn: conn} do
user = AccountsFixtures.user_fixture()
{:ok, _} = Rover.create_location(user, %{lat: 32.0, lon: -97.0, status: :good})
# User enters raw coords (no callsign or grid lookup) → station ends
# up with lat/lon set but callsign/grid blank.
{:ok, mission} =
RoverPlanning.create_mission(user, %{
"name" => "Coord-only station mission",
"band_mhz" => 10_000,
"only_known_good" => true,
"rover_height_ft" => 8.0,
"station_height_ft" => 30.0,
"stations" => %{"0" => %{"input" => "33.189,-96.452", "position" => 0}}
})
{:ok, _lv, html} = live(conn, ~p"/rover-planning/#{mission.id}")
derived = Maidenhead.from_latlon(33.189, -96.452, 6)
assert html =~ derived
# The path-profiles "Station" cell must render the derived grid,
# not the bare lat/lon. Match the exact <td>…</td> shape so we
# don't trip on the Stationary-stations list at the top of the
# page (which legitimately shows lat/lon as a secondary line).
refute html =~ ~s(<td class="font-mono text-xs">33.189, -96.452</td>)
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")