/* window.NodeReachMap.render(containerId, node, tiers) — focused Leaflet map of a node and its links, coloured by bottleneck tier. Returns a controller: { map, setLinks(links), bounds, destroy() } The map + tiles + node pin + legend are built once; setLinks() redraws ONLY the link layer in place (no teardown/flicker) when the table filter changes. `tiers` is [{min, label, varName}] ordered strong→weak (from node-reach.js, the single source of the thresholds). */ (function () { 'use strict'; function cssVar(name) { var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || '#888'; } function tierFor(tiers, bottleneck) { for (var i = 0; i < tiers.length; i++) { if (bottleneck >= tiers[i].min) return tiers[i]; } return tiers[tiers.length - 1]; } function legendControl(tiers, colors) { var ctl = L.control({ position: 'bottomright' }); ctl.onAdd = function () { var div = L.DomUtil.create('div', 'nq-legend'); var rows = tiers.map(function (t) { return '
' + escapeHtml(t.legend) + '
'; }).join(''); div.innerHTML = '
Bottleneck (weaker direction)
' + rows; return div; }; return ctl; } function render(containerId, node, tiers) { var c = document.getElementById(containerId); if (!c || typeof L === 'undefined') return null; // Resolve the tier colours + marker outline ONCE (not per polyline/marker). var colors = {}; tiers.forEach(function (t) { colors[t.varName] = cssVar(t.varName); }); var outline = cssVar('--surface-0'); // themed marker stroke (was hardcoded #fff) var accent = cssVar('--accent'); var map = L.map(containerId, { zoomControl: true, attributionControl: false }) .setView([node.lat, node.lon], 11); if (typeof window._applyTilesToNodeMap === 'function') { window._applyTilesToNodeMap(map); } else { // Loud, not silent: the tile-preference helper is missing. console.warn('NodeReachMap: _applyTilesToNodeMap unavailable — using OSM fallback'); L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { maxZoom: 19 }).addTo(map); } // Center node: a circleMarker like the neighbours (one glyph family) — just // larger + accent-filled — rather than the heavy default droplet icon. L.circleMarker([node.lat, node.lon], { radius: 8, color: outline, weight: 2, fillColor: accent, fillOpacity: 1 }) .addTo(map).bindPopup(escapeHtml(node.name)); legendControl(tiers, colors).addTo(map); var linkLayer = L.layerGroup().addTo(map); var bounds = [[node.lat, node.lon]]; function setLinks(links) { linkLayer.clearLayers(); bounds = [[node.lat, node.lon]]; links.forEach(function (l) { if (l.lat == null || l.lon == null) return; bounds.push([l.lat, l.lon]); var col = colors[tierFor(tiers, l.bottleneck).varName]; // Constant weight — colour alone encodes bottleneck (no double-encoding). L.polyline([[node.lat, node.lon], [l.lat, l.lon]], { color: col, weight: 2.5, opacity: 0.85 }) .addTo(linkLayer) .bindPopup(escapeHtml(l.name) + '
we ' + l.we_hear + ' / they ' + l.they_hear); L.circleMarker([l.lat, l.lon], { radius: 5, color: outline, weight: 1, fillColor: col, fillOpacity: 1 }) .addTo(linkLayer).bindTooltip(escapeHtml(l.name)); }); try { map.fitBounds(bounds, { padding: [30, 30] }); } catch (e) {} map._nqBounds = bounds; } setTimeout(function () { map.invalidateSize(); }, 120); return { map: map, setLinks: setLinks, get bounds() { return bounds; }, destroy: function () { try { map.remove(); } catch (e) {} } }; } window.NodeReachMap = { render: render }; })();