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.
132 lines
4.1 KiB
JavaScript
132 lines
4.1 KiB
JavaScript
// Band MHz -> color mapping
|
|
const BAND_COLORS = {
|
|
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
|
|
}
|
|
|
|
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) {
|
|
return BAND_COLORS[band] || "#64748b"
|
|
}
|
|
|
|
export const ContactsMap = {
|
|
mounted() {
|
|
this.allContacts = JSON.parse(this.el.dataset.contacts)
|
|
this.callsignFilter = ""
|
|
this.enabledBands = new Set()
|
|
this.lineLayer = L.layerGroup()
|
|
this.dotLayer = L.layerGroup()
|
|
|
|
this.map = L.map(this.el, {
|
|
center: [37.5, -96],
|
|
zoom: 5,
|
|
minZoom: 3,
|
|
maxZoom: 14,
|
|
preferCanvas: true
|
|
})
|
|
|
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
|
attribution: "© OpenStreetMap contributors",
|
|
maxZoom: 19
|
|
}).addTo(this.map)
|
|
|
|
this.lineLayer.addTo(this.map)
|
|
this.dotLayer.addTo(this.map)
|
|
|
|
// 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.rebuildMap()
|
|
},
|
|
|
|
rebuildMap() {
|
|
this.lineLayer.clearLayers()
|
|
this.dotLayer.clearLayers()
|
|
|
|
const filtered = this.allContacts.filter(c => {
|
|
const [, , , , band, s1, s2] = c
|
|
if (!this.enabledBands.has(band)) return false
|
|
if (this.callsignFilter) {
|
|
const match = this.callsignFilter
|
|
const st1 = (s1 || "").toUpperCase()
|
|
const st2 = (s2 || "").toUpperCase()
|
|
if (!st1.includes(match) && !st2.includes(match)) return false
|
|
}
|
|
return true
|
|
})
|
|
|
|
// Update count displays
|
|
const countEl = document.getElementById("contact-count")
|
|
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 = {}
|
|
for (const c of filtered) {
|
|
const [lat1, lon1, lat2, lon2, band, s1, s2, mode, dist, ts, id] = c
|
|
const color = bandColor(band)
|
|
|
|
const line = L.polyline([[lat1, lon1], [lat2, lon2]], {
|
|
color, 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(band)} · ${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}) })
|
|
|
|
this.lineLayer.addLayer(line)
|
|
|
|
const k1 = `${lat1.toFixed(2)},${lon1.toFixed(2)}`
|
|
const k2 = `${lat2.toFixed(2)},${lon2.toFixed(2)}`
|
|
endpointCounts[k1] = (endpointCounts[k1] || 0) + 1
|
|
endpointCounts[k2] = (endpointCounts[k2] || 0) + 1
|
|
}
|
|
|
|
// Draw endpoint dots
|
|
for (const [key, count] of Object.entries(endpointCounts)) {
|
|
const [lat, lon] = key.split(",").map(Number)
|
|
const radius = Math.min(2 + Math.log2(count) * 1.5, 10)
|
|
|
|
L.circleMarker([lat, lon], {
|
|
radius, color: "#fff", weight: 1,
|
|
fillColor: "#3b82f6", fillOpacity: 0.7,
|
|
interactive: false
|
|
}).addTo(this.dotLayer)
|
|
}
|
|
}
|
|
}
|