defmodule MicrowavepropWeb.ContactMapLive do @moduledoc false use MicrowavepropWeb, :live_view import Ecto.Query alias Microwaveprop.Radio.Contact alias Microwaveprop.Repo @band_colors %{ 1296 => "#475569", 2304 => "#7c3aed", 3456 => "#4f46e5", 5760 => "#2563eb", 10_000 => "#059669", 24_000 => "#d97706", 47_000 => "#ea580c", 68_000 => "#dc2626", 75_000 => "#c026d3", 122_000 => "#db2777", 134_000 => "#e11d48", 241_000 => "#b91c1c" } @impl true def mount(_params, _session, socket) do contacts = load_contacts() bands = contacts |> Enum.map(fn [_, _, _, _, band | _] -> band end) |> Enum.uniq() |> Enum.sort() enabled_bands = MapSet.new(bands) {:ok, assign(socket, page_title: "Contact Map", contacts_json: Jason.encode!(contacts), contact_count: length(contacts), all_bands: bands, enabled_bands: enabled_bands, callsign_filter: "" )} end @impl true def handle_event("toggle_band", %{"band" => band_str}, socket) do band = String.to_integer(band_str) enabled = if MapSet.member?(socket.assigns.enabled_bands, band) do MapSet.delete(socket.assigns.enabled_bands, band) else MapSet.put(socket.assigns.enabled_bands, band) end socket = assign(socket, enabled_bands: enabled) {:noreply, push_filter(socket)} end def handle_event("bands_all", _params, socket) do socket = assign(socket, enabled_bands: MapSet.new(socket.assigns.all_bands)) {:noreply, push_filter(socket)} end def handle_event("bands_none", _params, socket) do socket = assign(socket, enabled_bands: MapSet.new()) {:noreply, push_filter(socket)} end def handle_event("filter_callsign", %{"callsign" => callsign}, socket) do socket = assign(socket, callsign_filter: String.trim(callsign)) {:noreply, push_filter(socket)} end defp push_filter(socket) do push_event(socket, "apply_filter", %{ enabled_bands: MapSet.to_list(socket.assigns.enabled_bands), callsign: socket.assigns.callsign_filter }) end defp band_label(mhz) when mhz >= 1000 do ghz = mhz / 1000 label = if ghz == trunc(ghz), do: "#{trunc(ghz)}", else: "#{Float.round(ghz / 1, 1)}" "#{label} GHz" end defp band_label(mhz), do: "#{mhz} MHz" defp band_color(mhz), do: Map.get(@band_colors, mhz, "#64748b") 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"""