From 77656ba02372002854f81931ecdad751267f3f4e Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sat, 11 Apr 2026 16:20:47 -0500 Subject: [PATCH] Move contacts map controls to sidebar matching main map layout Replace the JS-built Leaflet control panel with a server-side sidebar (desktop) and mobile floating controls. Band checkboxes, callsign filter, and nav links now match the main propagation map layout. Filter state managed by LiveView, pushed to JS via events. --- assets/js/contacts_map_hook.js | 121 +------- .../live/contact_map_live.ex | 287 +++++++++++++++++- 2 files changed, 287 insertions(+), 121 deletions(-) diff --git a/assets/js/contacts_map_hook.js b/assets/js/contacts_map_hook.js index c80288b3..71de8ce2 100644 --- a/assets/js/contacts_map_hook.js +++ b/assets/js/contacts_map_hook.js @@ -50,98 +50,19 @@ export const ContactsMap = { this.lineLayer.addTo(this.map) this.dotLayer.addTo(this.map) - // Discover all bands - const bandSet = new Set() - for (const c of this.allContacts) bandSet.add(c[4]) - this.allBands = [...bandSet].sort((a, b) => a - b) - for (const b of this.allBands) this.enabledBands.add(b) + // Enable all bands initially + for (const c of this.allContacts) this.enabledBands.add(c[4]) + + // Listen for filter changes from LiveView + this.handleEvent("apply_filter", ({enabled_bands, callsign}) => { + this.enabledBands = new Set(enabled_bands) + this.callsignFilter = (callsign || "").toUpperCase() + this.rebuildMap() + }) - this.buildControlPanel() this.rebuildMap() }, - buildControlPanel() { - const control = L.control({position: "bottomright"}) - const self = this - - control.onAdd = function() { - const div = L.DomUtil.create("div") - div.style.cssText = "background:rgba(30,30,40,0.95);color:#fff;padding:8px 12px;border-radius:8px;font-size:11px;line-height:1.8;max-height:60vh;overflow-y:auto;" - L.DomEvent.disableClickPropagation(div) - L.DomEvent.disableScrollPropagation(div) - - // Callsign filter - let html = `
- -
` - - // Band header with All/None - html += `
- Bands - - - - -
` - - // Band checkboxes - for (const band of self.allBands) { - const color = bandColor(band) - const label = bandLabel(band) - html += `` - } - - div.innerHTML = html - - // Wire callsign filter - const input = div.querySelector("#callsign-filter") - let debounceTimer - input.addEventListener("input", () => { - clearTimeout(debounceTimer) - debounceTimer = setTimeout(() => { - self.callsignFilter = input.value.trim().toUpperCase() - self.rebuildMap() - }, 300) - }) - - // Wire band checkboxes - div.querySelectorAll("input[data-band]").forEach(cb => { - cb.addEventListener("change", () => { - const band = parseInt(cb.dataset.band) - if (cb.checked) { - self.enabledBands.add(band) - } else { - self.enabledBands.delete(band) - } - self.rebuildMap() - }) - }) - - div.querySelector("#bands-all").addEventListener("click", () => { - div.querySelectorAll("input[data-band]").forEach(cb => { cb.checked = true }) - for (const b of self.allBands) self.enabledBands.add(b) - self.rebuildMap() - }) - - div.querySelector("#bands-none").addEventListener("click", () => { - div.querySelectorAll("input[data-band]").forEach(cb => { cb.checked = false }) - self.enabledBands.clear() - self.rebuildMap() - }) - - self.controlDiv = div - return div - } - - control.addTo(this.map) - }, - rebuildMap() { this.lineLayer.clearLayers() this.dotLayer.clearLayers() @@ -158,27 +79,11 @@ export const ContactsMap = { return true }) - // Count per band for display - const bandCounts = {} - for (const c of filtered) { - const band = c[4] - bandCounts[band] = (bandCounts[band] || 0) + 1 - } - - // Update band counts in control panel - if (this.controlDiv) { - this.controlDiv.querySelectorAll(".band-count").forEach(el => { - const band = parseInt(el.dataset.band) - const count = bandCounts[band] || 0 - el.textContent = `(${count.toLocaleString()})` - }) - } - - // Update header count + // Update count displays const countEl = document.getElementById("contact-count") - if (countEl) { - countEl.textContent = `${filtered.length.toLocaleString()} contacts` - } + if (countEl) countEl.textContent = `${filtered.length.toLocaleString()} contacts` + const countMobile = document.getElementById("contact-count-mobile") + if (countMobile) countMobile.textContent = `${filtered.length.toLocaleString()} contacts` // Draw lines const endpointCounts = {} diff --git a/lib/microwaveprop_web/live/contact_map_live.ex b/lib/microwaveprop_web/live/contact_map_live.ex index 2faf2feb..ebc28f02 100644 --- a/lib/microwaveprop_web/live/contact_map_live.ex +++ b/lib/microwaveprop_web/live/contact_map_live.ex @@ -7,18 +7,91 @@ defmodule MicrowavepropWeb.ContactMapLive do 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) + 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), @@ -64,20 +137,208 @@ defmodule MicrowavepropWeb.ContactMapLive do @impl true def render(assigns) do ~H""" -
-
-
-
Contact Map
-
- {Integer.to_string(@contact_count)} contacts +
+
+
+
+ + <%!-- Mobile floating controls --%> +
+
+
+
+ Contact Map +
+ {Integer.to_string(@contact_count)} contacts +
+
+ +
+ + +
+
+
+ + <%!-- Desktop right sidebar --%> +
+ + """ end end