Add interactive popups to contact map, dedup reciprocals, darken line colors
This commit is contained in:
parent
cd8d7a0f52
commit
f7a753626d
3 changed files with 69 additions and 26 deletions
|
|
@ -1,23 +1,25 @@
|
|||
// Band MHz -> color mapping
|
||||
const BAND_COLORS = {
|
||||
1296: "#94a3b8", // slate
|
||||
2304: "#a78bfa", // violet
|
||||
3456: "#818cf8", // indigo
|
||||
5760: "#60a5fa", // blue
|
||||
10000: "#34d399", // emerald
|
||||
24000: "#fbbf24", // amber
|
||||
47000: "#fb923c", // orange
|
||||
68000: "#f87171", // red
|
||||
75000: "#e879f9", // fuchsia
|
||||
122000: "#f472b6", // pink
|
||||
134000: "#fb7185", // rose
|
||||
241000: "#ff5555", // bright red
|
||||
1296: "#475569", // slate-600
|
||||
2304: "#7c3aed", // violet-600
|
||||
3456: "#4f46e5", // indigo-600
|
||||
5760: "#2563eb", // blue-600
|
||||
10000: "#059669", // emerald-600
|
||||
24000: "#d97706", // amber-600
|
||||
47000: "#ea580c", // orange-600
|
||||
68000: "#dc2626", // red-600
|
||||
75000: "#c026d3", // fuchsia-600
|
||||
122000: "#db2777", // pink-600
|
||||
134000: "#e11d48", // rose-600
|
||||
241000: "#b91c1c", // red-700
|
||||
}
|
||||
|
||||
const BAND_LABELS = {
|
||||
1296: "1296 MHz", 2304: "2304 MHz", 3456: "3456 MHz", 5760: "5760 MHz",
|
||||
10000: "10 GHz", 24000: "24 GHz", 47000: "47 GHz", 68000: "68 GHz",
|
||||
75000: "76 GHz", 122000: "122 GHz", 134000: "134 GHz", 241000: "241 GHz"
|
||||
function bandLabel(mhz) {
|
||||
if (mhz >= 1000) {
|
||||
const ghz = mhz / 1000
|
||||
return (Number.isInteger(ghz) ? ghz : ghz.toFixed(1)) + " GHz"
|
||||
}
|
||||
return mhz + " MHz"
|
||||
}
|
||||
|
||||
function bandColor(band) {
|
||||
|
|
@ -52,20 +54,35 @@ export const ContactsMap = {
|
|||
// Collect all endpoint coords for endpoint density layer
|
||||
const endpointCounts = {}
|
||||
|
||||
// Draw lines per band using canvas polylines
|
||||
// Draw lines per band — interactive with popups
|
||||
const bandLayers = {}
|
||||
for (const [band, entries] of Object.entries(byBand)) {
|
||||
const color = bandColor(parseInt(band))
|
||||
const lines = []
|
||||
|
||||
for (const c of entries) {
|
||||
const [lat1, lon1, lat2, lon2] = c
|
||||
lines.push(L.polyline([[lat1, lon1], [lat2, lon2]], {
|
||||
const [lat1, lon1, lat2, lon2, bnd, s1, s2, mode, dist, ts, id] = c
|
||||
const line = L.polyline([[lat1, lon1], [lat2, lon2]], {
|
||||
color: color,
|
||||
weight: 1,
|
||||
opacity: 0.4,
|
||||
interactive: false
|
||||
}))
|
||||
weight: 2,
|
||||
opacity: 0.5
|
||||
})
|
||||
|
||||
const distStr = dist != null ? `${Math.round(dist)} km` : "—"
|
||||
line.bindPopup(
|
||||
`<div style="font-size:12px;line-height:1.5;">` +
|
||||
`<strong>${s1 || "?"} ↔ ${s2 || "?"}</strong><br/>` +
|
||||
`${bandLabel(bnd)} · ${mode || "?"}<br/>` +
|
||||
`${distStr} · ${ts || "?"} UTC<br/>` +
|
||||
`<a href="/contacts/${id}" style="color:#3b82f6;">View details →</a>` +
|
||||
`</div>`,
|
||||
{closeButton: false, offset: [0, -4]}
|
||||
)
|
||||
|
||||
line.on("mouseover", function() { this.setStyle({weight: 4, opacity: 1}) })
|
||||
line.on("mouseout", function() { this.setStyle({weight: 2, opacity: 0.5}) })
|
||||
|
||||
lines.push(line)
|
||||
|
||||
// Track endpoints
|
||||
const k1 = `${lat1.toFixed(2)},${lon1.toFixed(2)}`
|
||||
|
|
@ -106,7 +123,7 @@ export const ContactsMap = {
|
|||
let html = "<div style='font-weight:700;margin-bottom:4px;'>Bands</div>"
|
||||
for (const [band, entries] of sorted) {
|
||||
const color = bandColor(parseInt(band))
|
||||
const label = BAND_LABELS[parseInt(band)] || band + " MHz"
|
||||
const label = bandLabel(parseInt(band))
|
||||
html += `<div><span style="display:inline-block;width:14px;height:3px;background:${color};margin-right:6px;vertical-align:middle;border-radius:1px;"></span>${label} (${entries.length.toLocaleString()})</div>`
|
||||
}
|
||||
div.innerHTML = html
|
||||
|
|
|
|||
|
|
@ -23,9 +23,15 @@ defmodule MicrowavepropWeb.ContactMapLive 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
|
||||
band: c.band,
|
||||
station1: c.station1,
|
||||
station2: c.station2,
|
||||
mode: c.mode,
|
||||
distance_km: c.distance_km,
|
||||
qso_timestamp: c.qso_timestamp
|
||||
}
|
||||
)
|
||||
|> Repo.all()
|
||||
|
|
@ -35,12 +41,24 @@ defmodule MicrowavepropWeb.ContactMapLive do
|
|||
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]
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ defmodule MicrowavepropWeb.ContactLiveTest do
|
|||
alias Microwaveprop.Radio.Contact
|
||||
alias Microwaveprop.Repo
|
||||
|
||||
setup do
|
||||
Req.Test.stub(Microwaveprop.Terrain.ElevationClient, fn conn ->
|
||||
Req.Test.json(conn, [])
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp create_contact(attrs \\ %{}) do
|
||||
default = %{
|
||||
station1: "W5XD",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue