feat(rover): home accepts grid/callsign/address; station markers labeled; imperial scale

This commit is contained in:
Graham McIntire 2026-04-25 16:39:57 -05:00
parent 4108a98e5d
commit 9ebb134a03
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
3 changed files with 88 additions and 7 deletions

View file

@ -322,4 +322,20 @@ select.select {
.leaflet-popup { pointer-events: none; }
.leaflet-popup-content-wrapper, .leaflet-popup-tip, .leaflet-popup-close-button { pointer-events: auto; }
/* Rover-page station + home label tooltips */
.rover-station-label,
.rover-home-label {
background: rgba(255, 255, 255, 0.92);
border: none;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
color: #0f172a;
font-size: 10px;
font-weight: 600;
padding: 1px 4px;
border-radius: 3px;
white-space: nowrap;
}
.rover-station-label::before,
.rover-home-label::before { display: none; }
/* This file is for your main application CSS */

View file

@ -66,7 +66,12 @@ function buildStationMarker(station: Station): L.CircleMarker {
fillColor: station.selected ? "#0ea5e9" : "#64748b",
fillOpacity: 0.95,
interactive: true
}).bindTooltip(station.callsign, { direction: "top", offset: [0, -6] })
}).bindTooltip(station.callsign, {
permanent: true,
direction: "right",
offset: [6, 0],
className: "rover-station-label"
})
}
export const RoverMap: Partial<RoverMapHook> = {
@ -144,7 +149,7 @@ export const RoverMap: Partial<RoverMapHook> = {
fill: false
}).addTo(map)
L.control.scale({ metric: true, imperial: false, position: "bottomleft" }).addTo(map)
L.control.scale({ metric: false, imperial: true, position: "bottomleft" }).addTo(map)
// Render initial stations from data attribute
try {
@ -178,6 +183,14 @@ export const RoverMap: Partial<RoverMapHook> = {
this.setDriveRadius(km)
})
this.handleEvent("update_home", ({ lat, lon }: { lat: number; lon: number }) => {
this.homeLat = lat
this.homeLon = lon
if (this.homeMarker) this.homeMarker.setLatLng([lat, lon])
if (this.driveCircle) this.driveCircle.setLatLng([lat, lon])
this.map.flyTo([lat, lon], Math.max(this.map.getZoom(), 8), { duration: 0.6 })
})
this.handleEvent("rover_results", (
{ cells, drive_radius_km }: { cells: Cell[]; drive_radius_km: number }
) => {

View file

@ -9,7 +9,9 @@ defmodule MicrowavepropWeb.RoverLive do
use MicrowavepropWeb, :live_view
alias Microwaveprop.Accounts.User
alias Microwaveprop.Geocoder
alias Microwaveprop.Propagation
alias Microwaveprop.Radio.CallsignClient
alias Microwaveprop.Radio.Maidenhead
alias Microwaveprop.Repo
alias Microwaveprop.Rover
@ -212,12 +214,20 @@ defmodule MicrowavepropWeb.RoverLive do
end
def handle_event("set_home_qth", params, socket) do
grid = params |> Map.get("home_grid", "") |> String.trim()
attrs = %{"home_grid" => grid}
input = params |> Map.get("home_grid", "") |> String.trim()
case update_home(socket, attrs) do
{:ok, home} ->
{:noreply, assign(socket, home: home, home_form_error: nil)}
case resolve_home_input(input) do
{:ok, %{lat: lat, lon: lon, grid: grid}} ->
case update_home(socket, %{"home_grid" => grid, "home_lat" => lat, "home_lon" => lon}) do
{:ok, home} ->
{:noreply,
socket
|> assign(home: home, home_form_error: nil)
|> push_event("update_home", %{lat: home.lat, lon: home.lon})}
{:error, msg} ->
{:noreply, assign(socket, home_form_error: msg)}
end
{:error, msg} ->
{:noreply, assign(socket, home_form_error: msg)}
@ -404,6 +414,11 @@ defmodule MicrowavepropWeb.RoverLive do
end
end
defp update_home(_socket, %{"home_lat" => lat, "home_lon" => lon} = attrs) when is_number(lat) and is_number(lon) do
grid = Map.get(attrs, "home_grid") || home_label(lat * 1.0, lon * 1.0)
{:ok, %{label: grid, grid: grid, lat: lat * 1.0, lon: lon * 1.0, elev_m: nil}}
end
defp update_home(_socket, %{"home_grid" => grid}) when is_binary(grid) and grid != "" do
case Maidenhead.to_latlon(grid) do
{:ok, {lat, lon}} ->
@ -416,6 +431,43 @@ defmodule MicrowavepropWeb.RoverLive do
defp update_home(_socket, _attrs), do: {:error, "grid required"}
# Accepts grid (EM13qc), callsign (W5ABC), or address ("100 Main St, Dallas TX").
# Returns {:ok, %{lat, lon, grid}} or {:error, "..."}.
defp resolve_home_input(""), do: {:error, "enter a grid, callsign, or address"}
defp resolve_home_input(input) do
cond do
Maidenhead.valid?(input) -> resolve_home_grid(input)
callsign?(input) -> resolve_home_callsign(input)
true -> resolve_home_address(input)
end
end
defp resolve_home_grid(grid) do
case Maidenhead.to_latlon(grid) do
{:ok, {lat, lon}} -> {:ok, %{lat: lat, lon: lon, grid: grid}}
:error -> {:error, "invalid grid"}
end
end
defp resolve_home_callsign(call) do
case CallsignClient.locate(call) do
{:ok, %{lat: lat, lon: lon}} -> {:ok, %{lat: lat, lon: lon, grid: home_label(lat, lon)}}
{:error, msg} -> {:error, "callsign: #{msg}"}
end
end
defp resolve_home_address(addr) do
case Geocoder.geocode(addr) do
{:ok, %{lat: lat, lon: lon}} -> {:ok, %{lat: lat, lon: lon, grid: home_label(lat, lon)}}
{:error, msg} -> {:error, "geocode: #{msg}"}
end
end
defp callsign?(input) when is_binary(input) do
String.match?(input, ~r/^[A-Za-z0-9\/]{3,12}$/) and not Maidenhead.valid?(input)
end
defp changeset_first_error(%Ecto.Changeset{errors: [{field, {msg, _}} | _]}) do
"#{field}: #{msg}"
end