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
|
def handle_event("add_station", params, socket) do
|
||||||
callsign = params |> Map.get("callsign", "") |> String.trim()
|
callsign = params |> Map.get("callsign", "") |> String.trim()
|
||||||
grid = params |> Map.get("grid", "") |> String.trim()
|
grid = params |> Map.get("grid", "") |> String.trim()
|
||||||
attrs = build_station_attrs(callsign, grid)
|
|
||||||
|
|
||||||
case create_station(socket, attrs) do
|
case resolve_station_grid(callsign, grid) do
|
||||||
{:ok, stations} ->
|
{:ok, resolved_grid} ->
|
||||||
{:noreply,
|
attrs = build_station_attrs(callsign, resolved_grid)
|
||||||
socket
|
|
||||||
|> assign(fixed_stations: stations, station_form_error: nil)
|
case create_station(socket, attrs) do
|
||||||
|> sync_url()}
|
{: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} ->
|
{:error, msg} ->
|
||||||
{:noreply, assign(socket, station_form_error: msg)}
|
{:noreply, assign(socket, station_form_error: msg)}
|
||||||
end
|
end
|
||||||
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
|
def handle_event("set_home_qth", params, socket) do
|
||||||
input = params |> Map.get("home_grid", "") |> String.trim()
|
input = params |> Map.get("home_grid", "") |> String.trim()
|
||||||
|
|
||||||
|
|
@ -283,6 +314,30 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
put_flash(socket, :info, Enum.join(warnings, " "))
|
put_flash(socket, :info, Enum.join(warnings, " "))
|
||||||
end
|
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 ────────────────────────────────────────────────
|
# ── Mutation helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
defp handle_station_mutation(socket, id, fun) do
|
defp handle_station_mutation(socket, id, fun) do
|
||||||
|
|
@ -350,6 +405,32 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
{:ok, stations}
|
{:ok, stations}
|
||||||
end
|
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
|
defp create_station(%{assigns: %{persisted?: true}} = socket, attrs) do
|
||||||
user = current_user(socket)
|
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(%{callsign: call}) when is_binary(call), do: "call:" <> call
|
||||||
defp station_id(_), do: ""
|
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 ───────────────────────────────────────────────────────────
|
# ── Render ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -683,10 +770,10 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs opacity-70">
|
<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>
|
||||||
<div class="text-xs opacity-70">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -725,7 +812,7 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
<.sidebar_section title="FIXED STATIONS">
|
<.sidebar_section title="FIXED STATIONS">
|
||||||
<div class="flex flex-col gap-0.5">
|
<div class="flex flex-col gap-0.5">
|
||||||
<div :for={s <- @fixed_stations} class="flex items-center gap-2 py-1">
|
<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
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="checkbox checkbox-xs"
|
class="checkbox checkbox-xs"
|
||||||
|
|
@ -733,9 +820,26 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
phx-click="toggle_station"
|
phx-click="toggle_station"
|
||||||
phx-value-id={station_id(s)}
|
phx-value-id={station_id(s)}
|
||||||
/>
|
/>
|
||||||
<span class="font-mono text-xs flex-1 truncate">{s.callsign}</span>
|
<span class="font-mono text-xs truncate">{s.callsign}</span>
|
||||||
<span :if={s.grid} class="opacity-60 text-[11px]">{s.grid}</span>
|
|
||||||
</label>
|
</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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-ghost btn-xs btn-square opacity-50 hover:opacity-100 shrink-0"
|
class="btn btn-ghost btn-xs btn-square opacity-50 hover:opacity-100 shrink-0"
|
||||||
|
|
@ -751,7 +855,7 @@ defmodule MicrowavepropWeb.RoverLive do
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
name="callsign"
|
name="callsign"
|
||||||
placeholder="Call"
|
placeholder="Call (grid auto-looked-up)"
|
||||||
class="input input-bordered input-xs flex-1"
|
class="input input-bordered input-xs flex-1"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue