diff --git a/assets/js/app.js b/assets/js/app.js index db25699d..912daa58 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -31,6 +31,8 @@ import topbar from "../vendor/topbar" import {PropagationMap} from "./propagation_map_hook" import {ContactMap} from "./contact_map_hook" +import {ContactsMap} from "./contacts_map_hook" +import {ElevationProfile} from "./elevation_profile_hook" const UtcClock = { mounted() { @@ -52,7 +54,7 @@ const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: {_csrf_token: csrfToken}, - hooks: {...colocatedHooks, PropagationMap, UtcClock, ContactMap}, + hooks: {...colocatedHooks, PropagationMap, UtcClock, ContactMap, ContactsMap, ElevationProfile}, }) // Show progress bar on live navigation and form submits diff --git a/assets/js/contacts_map_hook.js b/assets/js/contacts_map_hook.js new file mode 100644 index 00000000..9ecec39a --- /dev/null +++ b/assets/js/contacts_map_hook.js @@ -0,0 +1,117 @@ +// 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 = "
Locations approximate, in the center of the grid squares
++ Locations approximate, in the center of the grid squares +
<% end %> + <%= if @elevation_profile do %> +