Backfill page now sums child partition stats for partitioned tables like hrrr_profiles (relkind 'p') which were excluded by the relkind 'r' filter. Contacts map defers Leaflet init to requestAnimationFrame so the flex container has dimensions before map initialization.
137 lines
4.2 KiB
JavaScript
137 lines
4.2 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()
|
|
|
|
// 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()
|
|
})
|
|
|
|
// Defer map init until container has dimensions
|
|
requestAnimationFrame(() => this.initMap())
|
|
},
|
|
|
|
initMap() {
|
|
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])
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|