feat(rover): auto-resolve grid from callsign on add; inline-edit grid per row; mi/ft units
This commit is contained in:
parent
ea95ae3596
commit
5631444abd
1 changed files with 117 additions and 13 deletions
|
|
@ -199,20 +199,51 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
def handle_event("add_station", params, socket) do
|
||||
callsign = params |> Map.get("callsign", "") |> String.trim()
|
||||
grid = params |> Map.get("grid", "") |> String.trim()
|
||||
attrs = build_station_attrs(callsign, grid)
|
||||
|
||||
case create_station(socket, attrs) do
|
||||
{:ok, stations} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(fixed_stations: stations, station_form_error: nil)
|
||||
|> sync_url()}
|
||||
case resolve_station_grid(callsign, grid) do
|
||||
{:ok, resolved_grid} ->
|
||||
attrs = build_station_attrs(callsign, resolved_grid)
|
||||
|
||||
case create_station(socket, attrs) do
|
||||
{:ok, stations} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(fixed_stations: stations, station_form_error: nil)
|
||||
|> sync_url()}
|
||||
|
||||
{:error, msg} ->
|
||||
{:noreply, assign(socket, station_form_error: msg)}
|
||||
end
|
||||
|
||||
{:error, msg} ->
|
||||
{:noreply, assign(socket, station_form_error: msg)}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("update_station_grid", %{"station_id" => id, "grid" => new_grid}, socket) do
|
||||
new_grid = String.trim(new_grid)
|
||||
|
||||
cond do
|
||||
new_grid == "" ->
|
||||
{:noreply, socket}
|
||||
|
||||
not Maidenhead.valid?(new_grid) ->
|
||||
{:noreply, assign(socket, station_form_error: "invalid grid: #{new_grid}")}
|
||||
|
||||
true ->
|
||||
case mutate_station_grid(socket, id, new_grid) do
|
||||
{:ok, stations} ->
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(fixed_stations: stations, station_form_error: nil)
|
||||
|> sync_url()}
|
||||
|
||||
{:error, msg} ->
|
||||
{:noreply, assign(socket, station_form_error: msg)}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("set_home_qth", params, socket) do
|
||||
input = params |> Map.get("home_grid", "") |> String.trim()
|
||||
|
||||
|
|
@ -283,6 +314,30 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
put_flash(socket, :info, Enum.join(warnings, " "))
|
||||
end
|
||||
|
||||
# When the user supplies just a callsign, look it up via QRZ (with the
|
||||
# Google Maps geocoder fallback already wired into CallsignClient) and
|
||||
# use the returned 10-char grid. Explicit user-entered grids win over
|
||||
# the lookup so an operator can override an old QRZ entry.
|
||||
defp resolve_station_grid(_call, grid) when is_binary(grid) and grid != "", do: {:ok, grid}
|
||||
|
||||
defp resolve_station_grid("", _grid), do: {:error, "callsign required"}
|
||||
|
||||
defp resolve_station_grid(call, _grid) do
|
||||
case CallsignClient.locate(call) do
|
||||
{:ok, %{gridsquare: g}} when is_binary(g) and byte_size(g) >= 4 ->
|
||||
{:ok, g}
|
||||
|
||||
{:ok, %{lat: lat, lon: lon}} when is_number(lat) and is_number(lon) ->
|
||||
{:ok, Maidenhead.from_latlon(lat, lon, 10)}
|
||||
|
||||
{:ok, _} ->
|
||||
{:error, "QRZ lookup for #{call} returned no grid or coords"}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "couldn't locate #{call}: #{inspect(reason)}"}
|
||||
end
|
||||
end
|
||||
|
||||
# ── Mutation helpers ────────────────────────────────────────────────
|
||||
|
||||
defp handle_station_mutation(socket, id, fun) do
|
||||
|
|
@ -350,6 +405,32 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
{:ok, stations}
|
||||
end
|
||||
|
||||
defp mutate_station_grid(%{assigns: %{persisted?: true}} = socket, id, new_grid) do
|
||||
user = current_user(socket)
|
||||
|
||||
case Rover.update_station(user, id, %{"grid" => new_grid, "lat" => nil, "lon" => nil}) do
|
||||
{:ok, _} -> {:ok, Rover.list_stations(user)}
|
||||
{:error, :not_found} -> {:error, "station not found"}
|
||||
{:error, %Ecto.Changeset{} = cs} -> {:error, changeset_first_error(cs)}
|
||||
end
|
||||
end
|
||||
|
||||
defp mutate_station_grid(socket, id, new_grid) do
|
||||
stations =
|
||||
Enum.map(socket.assigns.fixed_stations, fn s ->
|
||||
if station_id(s) == id, do: replace_anonymous_station_grid(s, new_grid), else: s
|
||||
end)
|
||||
|
||||
{:ok, stations}
|
||||
end
|
||||
|
||||
defp replace_anonymous_station_grid(station, new_grid) do
|
||||
case Maidenhead.to_latlon(new_grid) do
|
||||
{:ok, {lat, lon}} -> %{station | grid: new_grid, lat: lat, lon: lon}
|
||||
:error -> station
|
||||
end
|
||||
end
|
||||
|
||||
defp create_station(%{assigns: %{persisted?: true}} = socket, attrs) do
|
||||
user = current_user(socket)
|
||||
|
||||
|
|
@ -620,6 +701,12 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
defp station_id(%{callsign: call}) when is_binary(call), do: "call:" <> call
|
||||
defp station_id(_), do: ""
|
||||
|
||||
defp km_to_mi(km) when is_number(km), do: round(km * 0.621371)
|
||||
defp km_to_mi(_), do: 0
|
||||
|
||||
defp elev_ft(nil), do: "—"
|
||||
defp elev_ft(m) when is_number(m), do: "#{round(m * 3.28084)} ft"
|
||||
|
||||
# ── Render ───────────────────────────────────────────────────────────
|
||||
|
||||
@impl true
|
||||
|
|
@ -683,10 +770,10 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
</span>
|
||||
</div>
|
||||
<div class="text-xs opacity-70">
|
||||
{round(c.distance_km)} km {c.bearing_compass} of home
|
||||
{km_to_mi(c.distance_km)} mi {c.bearing_compass} of home
|
||||
</div>
|
||||
<div class="text-xs opacity-70">
|
||||
{c.elev_m} m · {round(c.drive_min)} min drive
|
||||
{elev_ft(c.elev_m)} · {round(c.drive_min)} min drive
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -725,7 +812,7 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
<.sidebar_section title="FIXED STATIONS">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div :for={s <- @fixed_stations} class="flex items-center gap-2 py-1">
|
||||
<label class="cursor-pointer flex items-center gap-2 flex-1 min-w-0">
|
||||
<label class="cursor-pointer flex items-center gap-2 min-w-0 shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="checkbox checkbox-xs"
|
||||
|
|
@ -733,9 +820,26 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
phx-click="toggle_station"
|
||||
phx-value-id={station_id(s)}
|
||||
/>
|
||||
<span class="font-mono text-xs flex-1 truncate">{s.callsign}</span>
|
||||
<span :if={s.grid} class="opacity-60 text-[11px]">{s.grid}</span>
|
||||
<span class="font-mono text-xs truncate">{s.callsign}</span>
|
||||
</label>
|
||||
<form
|
||||
phx-submit="update_station_grid"
|
||||
class="flex-1 min-w-0"
|
||||
id={"station-grid-form-" <> station_id(s)}
|
||||
>
|
||||
<input type="hidden" name="station_id" value={station_id(s)} />
|
||||
<input
|
||||
type="text"
|
||||
name="grid"
|
||||
value={s.grid || ""}
|
||||
placeholder="grid"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
phx-debounce="blur"
|
||||
title="Click to edit; press Enter or click away to save"
|
||||
class="font-mono text-[11px] w-full bg-transparent border-0 border-b border-transparent focus:border-base-content/30 focus:bg-base-100/30 focus:outline-none px-1 py-0 opacity-60 focus:opacity-100"
|
||||
/>
|
||||
</form>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-ghost btn-xs btn-square opacity-50 hover:opacity-100 shrink-0"
|
||||
|
|
@ -751,7 +855,7 @@ defmodule MicrowavepropWeb.RoverLive do
|
|||
<input
|
||||
type="text"
|
||||
name="callsign"
|
||||
placeholder="Call"
|
||||
placeholder="Call (grid auto-looked-up)"
|
||||
class="input input-bordered input-xs flex-1"
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue