prop/assets/js/contacts_map_hook.js

177 lines
5.9 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() {
const contacts = JSON.parse(this.el.dataset.contacts)
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)
// Group contacts by band for layered rendering
const byBand = {}
for (const c of contacts) {
const band = c[4]
if (!byBand[band]) byBand[band] = []
byBand[band].push(c)
}
// Collect all endpoint coords for endpoint density layer
const endpointCounts = {}
// 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, bnd, s1, s2, mode, dist, ts, id] = c
const line = L.polyline([[lat1, lon1], [lat2, lon2]], {
color: 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 || "?"} &harr; ${s2 || "?"}</strong><br/>` +
`${bandLabel(bnd)} &middot; ${mode || "?"}<br/>` +
`${distStr} &middot; ${ts || "?"} UTC<br/>` +
`<a href="/contacts/${id}" style="color:#3b82f6;">View details &rarr;</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)}`
const k2 = `${lat2.toFixed(2)},${lon2.toFixed(2)}`
endpointCounts[k1] = (endpointCounts[k1] || 0) + 1
endpointCounts[k2] = (endpointCounts[k2] || 0) + 1
}
const layer = L.layerGroup(lines)
bandLayers[band] = layer
layer.addTo(this.map)
}
// Draw endpoint dots sized by density
const dotLayer = L.layerGroup()
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: radius,
color: "#fff",
weight: 1,
fillColor: "#3b82f6",
fillOpacity: 0.7,
interactive: false
}).addTo(dotLayer)
}
dotLayer.addTo(this.map)
// Build interactive band filter
const bandControl = L.control({position: "bottomright"})
const self = this
bandControl.onAdd = function(map) {
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;"
L.DomEvent.disableClickPropagation(div)
L.DomEvent.disableScrollPropagation(div)
const sorted = Object.entries(byBand).sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
let html = `<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;">
<span style="font-weight:700;">Bands</span>
<span style="display:flex;gap:6px;">
<button id="bands-all" style="background:none;border:none;color:#7dd;cursor:pointer;font-size:10px;text-decoration:underline;">All</button>
<button id="bands-none" style="background:none;border:none;color:#d77;cursor:pointer;font-size:10px;text-decoration:underline;">None</button>
</span>
</div>`
for (const [band, entries] of sorted) {
const color = bandColor(parseInt(band))
const label = bandLabel(parseInt(band))
html += `<label style="display:flex;align-items:center;gap:6px;cursor:pointer;">
<input type="checkbox" data-band="${band}" checked style="accent-color:${color};cursor:pointer;">
<span style="display:inline-block;width:14px;height:3px;background:${color};border-radius:1px;flex-shrink:0;"></span>
<span>${label} (${entries.length.toLocaleString()})</span>
</label>`
}
div.innerHTML = html
// Wire up checkboxes
div.querySelectorAll("input[data-band]").forEach(cb => {
cb.addEventListener("change", () => {
const band = cb.dataset.band
if (cb.checked) {
bandLayers[band].addTo(map)
} else {
bandLayers[band].remove()
}
})
})
div.querySelector("#bands-all").addEventListener("click", () => {
div.querySelectorAll("input[data-band]").forEach(cb => {
cb.checked = true
bandLayers[cb.dataset.band].addTo(map)
})
})
div.querySelector("#bands-none").addEventListener("click", () => {
div.querySelectorAll("input[data-band]").forEach(cb => {
cb.checked = false
bandLayers[cb.dataset.band].remove()
})
})
return div
}
bandControl.addTo(this.map)
}
}