prop/lib/microwaveprop_web/live/contact_map_live.ex
Graham McIntire 2aa76727f9 Add callsign filter and band counts to contacts map
- Callsign input filters contacts where either station matches
  (case-insensitive substring match, debounced 300ms)
- Band checkboxes now show per-band count of visible contacts
- All/None buttons for quick band selection
- Header count updates dynamically with filters
- Complete rewrite: rebuilds map on any filter change instead of
  toggling individual layers (simpler, handles cross-filter correctly)
2026-04-11 15:26:57 -05:00

81 lines
2.3 KiB
Elixir

defmodule MicrowavepropWeb.ContactMapLive do
@moduledoc false
use MicrowavepropWeb, :live_view
import Ecto.Query
alias Microwaveprop.Radio.Contact
alias Microwaveprop.Repo
@impl true
def mount(_params, _session, socket) do
contacts = load_contacts()
{:ok,
assign(socket,
page_title: "Contact Map",
contacts_json: Jason.encode!(contacts),
contact_count: length(contacts)
)}
end
defp load_contacts do
from(c in Contact,
where: not is_nil(c.pos1) and not is_nil(c.pos2),
select: %{
id: c.id,
pos1: c.pos1,
pos2: c.pos2,
band: c.band,
station1: c.station1,
station2: c.station2,
mode: c.mode,
distance_km: c.distance_km,
qso_timestamp: c.qso_timestamp
}
)
|> Repo.all()
|> Enum.map(fn c ->
lat1 = c.pos1["lat"]
lon1 = c.pos1["lon"] || c.pos1["lng"]
lat2 = c.pos2["lat"]
lon2 = c.pos2["lon"] || c.pos2["lng"]
band = if c.band, do: Decimal.to_integer(c.band), else: 0
dist = if c.distance_km, do: Decimal.to_float(c.distance_km)
ts = if c.qso_timestamp, do: Calendar.strftime(c.qso_timestamp, "%Y-%m-%d %H:%M")
if lat1 && lon1 && lat2 && lon2 do
[lat1, lon1, lat2, lon2, band, c.station1, c.station2, c.mode, dist, ts, c.id]
end
end)
|> Enum.reject(&is_nil/1)
|> dedup_reciprocals()
end
# Deduplicate reciprocal contacts (A↔B same band/hour = same contact logged by both sides)
defp dedup_reciprocals(contacts) do
Enum.uniq_by(contacts, fn [_lat1, _lon1, _lat2, _lon2, band, s1, s2, _mode, _dist, ts, _id] ->
stations = Enum.sort([s1 || "", s2 || ""])
hour = if ts, do: String.slice(ts, 0..12), else: ""
{stations, band, hour}
end)
end
@impl true
def render(assigns) do
~H"""
<div
id="contacts-map"
phx-hook="ContactsMap"
phx-update="ignore"
class="fixed inset-0 z-0"
data-contacts={@contacts_json}
>
</div>
<div class="fixed top-3 left-14 z-[1000] bg-base-100 shadow rounded-box border border-base-300 px-3 py-2 flex flex-col gap-1">
<div class="font-bold text-sm">Contact Map</div>
<div id="contact-count" class="text-xs opacity-70">{Integer.to_string(@contact_count)} contacts</div>
</div>
"""
end
end