// 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( `
` + `${s1 || "?"} ↔ ${s2 || "?"}
` + `${bandLabel(bnd)} · ${mode || "?"}
` + `${distStr} · ${ts || "?"} UTC
` + `View details →` + `
`, {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 = "
Bands
" for (const [band, entries] of sorted) { const color = bandColor(parseInt(band)) const label = bandLabel(parseInt(band)) html += `
${label} (${entries.length.toLocaleString()})
` } div.innerHTML = html return div } legend.addTo(this.map) } }