prop/assets/js/contacts_map_hook.js
Graham McIntire cd8d7a0f52
Add contact detail map, elevation profile chart, and UI improvements
- Leaflet map on contact detail pages showing both station locations
- SRTM elevation profile with Chart.js: terrain, LOS, Fresnel zone
- Path info: distance, azimuth, elevation angle in both directions
- Compact 4-column grid layout for contact details
- Contact map page with all contacts plotted
- Rename QSOs to Contacts in page header
- Wrap algo page in standard layout
2026-04-01 12:34:03 -05:00

117 lines
3.6 KiB
JavaScript

// 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
}
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 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 using canvas polylines
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]], {
color: color,
weight: 1,
opacity: 0.4,
interactive: false
}))
// 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 = BAND_LABELS[parseInt(band)] || band + " MHz"
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)
}
}