134 lines
4.3 KiB
JavaScript
134 lines
4.3 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 || "?"} ↔ ${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)}`
|
|
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 legend
|
|
const legend = L.control({position: "bottomright"})
|
|
legend.onAdd = function() {
|
|
const div = L.DomUtil.create("div")
|
|
div.style.cssText = "background:rgba(30,30,40,0.9);color:#fff;padding:8px 12px;border-radius:8px;font-size:11px;line-height:1.6;"
|
|
|
|
const sorted = Object.entries(byBand).sort((a, b) => parseInt(a[0]) - parseInt(b[0]))
|
|
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 = 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
|
|
return div
|
|
}
|
|
legend.addTo(this.map)
|
|
}
|
|
}
|