1/** 2 * Geographic Infrastructure Concentration component. 3 * 4 * Injects HHI badges into the Featured signal-feed rows and provides 5 * an expandable Leaflet mini-map showing infrastructure distribution. 6 * Leaflet is lazy-loaded from CDN only when the map is first opened. 7 */ 8(function () { 9 "use strict"; 10 11 var API_URL = "https://api.haiphen.io/v1/prospect/geo/concentration"; 12 var LEAFLET_JS = "https://unpkg.com/[email protected]/dist/leaflet.js"; 13 var LEAFLET_CSS = "https://unpkg.com/[email protected]/dist/leaflet.css"; 14 var TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 15 var TILE_ATTR = '© <a href="https://www.openstreetmap.org/copyright">OSM</a>'; 16 17 var COLORS = { high: "#dc2626", medium: "#f59e0b", low: "#10b981" }; 18 19 var _data = null; // API response companies array 20 var _leafletReady = false; 21 var _mapInstance = null; 22 var _panelEl = null; 23 var _rootEl = null; 24 25 // ââ Helpers ââââââââââââââââââââââââââââââââââââââââââââââ 26 27 function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; } 28 29 function fetchGeo(cb) { 30 var ctrl = new AbortController(); 31 var timer = setTimeout(function () { ctrl.abort(); }, 10000); 32 fetch(API_URL, { credentials: "include", signal: ctrl.signal }) 33 .then(function (r) { clearTimeout(timer); return r.ok ? r.json() : Promise.reject(r.status); }) 34 .then(function (d) { cb(null, d); }) 35 .catch(function (e) { clearTimeout(timer); cb(e); }); 36 } 37 38 function loadLeaflet(cb) { 39 if (_leafletReady) return cb(); 40 // CSS 41 var link = document.createElement("link"); 42 link.rel = "stylesheet"; link.href = LEAFLET_CSS; 43 document.head.appendChild(link); 44 // JS 45 var script = document.createElement("script"); 46 script.src = LEAFLET_JS; 47 script.onload = function () { _leafletReady = true; cb(); }; 48 script.onerror = function () { cb(new Error("Leaflet load failed")); }; 49 document.head.appendChild(script); 50 } 51 52 // ââ Badge injection âââââââââââââââââââââââââââââââââââââ 53 54 function injectBadges(rootEl) { 55 if (!_data || !_data.length) return; 56 57 // Build lookup: lowercase company name â company data 58 var lookup = {}; 59 for (var i = 0; i < _data.length; i++) { 60 lookup[_data[i].group_key.toLowerCase()] = _data[i]; 61 } 62 63 var cells = rootEl.querySelectorAll(".hp-feed__cell--company"); 64 for (var c = 0; c < cells.length; c++) { 65 var cell = cells[c]; 66 // Skip if badge already injected 67 if (cell.querySelector(".hp-geo-badge")) continue; 68 69 // Extract company name from cell text (first line before <br>) 70 var name = (cell.childNodes[0] && cell.childNodes[0].textContent || "").trim().toLowerCase(); 71 var match = lookup[name]; 72 if (!match) continue; 73 74 var badge = document.createElement("span"); 75 badge.className = "hp-geo-badge hp-geo-badge--" + match.concentration; 76 badge.textContent = "HHI " + match.hhi.toFixed(2); 77 badge.title = "Infrastructure concentration: " + match.concentration 78 + " (" + match.total_infrastructure + " endpoints in " 79 + match.regions.length + " regions)"; 80 badge.setAttribute("data-geo-company", match.group_key); 81 badge.addEventListener("click", onBadgeClick); 82 cell.appendChild(badge); 83 } 84 } 85 86 // ââ Map panel âââââââââââââââââââââââââââââââââââââââââââ 87 88 function ensurePanel(rootEl) { 89 if (_panelEl) return _panelEl; 90 // Fetch template 91 fetch("components/geo/geo.html", { cache: "no-store" }) 92 .then(function (r) { return r.text(); }) 93 .then(function (html) { 94 var wrapper = document.createElement("div"); 95 wrapper.innerHTML = html; 96 _panelEl = wrapper.firstElementChild; 97 98 // Insert after the signal feed table 99 var feed = rootEl.querySelector("#hp-signal-feed"); 100 if (feed && feed.parentNode) { 101 feed.parentNode.insertBefore(_panelEl, feed.nextSibling); 102 } else { 103 rootEl.appendChild(_panelEl); 104 } 105 106 // Close button 107 var closeBtn = _panelEl.querySelector("[data-geo-close]"); 108 if (closeBtn) { 109 closeBtn.addEventListener("click", function () { 110 _panelEl.classList.remove("is-open"); 111 }); 112 } 113 }); 114 return null; // async â caller should retry 115 } 116 117 function onBadgeClick(e) { 118 e.stopPropagation(); 119 var companyName = e.currentTarget.getAttribute("data-geo-company"); 120 if (!companyName) return; 121 122 var company = null; 123 for (var i = 0; i < _data.length; i++) { 124 if (_data[i].group_key === companyName) { company = _data[i]; break; } 125 } 126 if (!company || !company.regions.length) return; 127 128 showMap(company); 129 } 130 131 function showMap(company) { 132 if (!_panelEl) { 133 // Panel not loaded yet â wait and retry 134 setTimeout(function () { showMap(company); }, 200); 135 return; 136 } 137 138 _panelEl.classList.add("is-open"); 139 140 // Update title 141 var title = _panelEl.querySelector(".hp-geo__title"); 142 if (title) title.textContent = company.group_key + " â Infrastru
142cture Concentration"; 143 144 // Update legend 145 var legend = _panelEl.querySelector("[data-geo-legend]"); 146 if (legend) { 147 legend.textContent = "HHI: " + company.hhi.toFixed(3) + " (" + company.concentration 148 + ") â " + company.total_infrastructure + " endpoints across " 149 + company.regions.length + " regions"; 150 } 151 152 // Load Leaflet and render 153 loadLeaflet(function (err) { 154 if (err) { console.warn("[Geo] Leaflet load error:", err); return; } 155 renderMap(company); 156 }); 157 } 158 159 function renderMap(company) { 160 var mapEl = _panelEl.querySelector("[data-geo-map]"); 161 if (!mapEl) return; 162 163 // Destroy previous instance 164 if (_mapInstance) { _mapInstance.remove(); _mapInstance = null; } 165 166 _mapInstance = L.map(mapEl, { scrollWheelZoom: false }).setView([20, 0], 2); 167 L.tileLayer(TILE_URL, { attribution: TILE_ATTR, maxZoom: 18 }).addTo(_mapInstance); 168 169 var bounds = []; 170 for (var i = 0; i < company.regions.length; i++) { 171 var r = company.regions[i]; 172 if (r.lat == null || r.lng == null) continue; 173 174 var radius = Math.max(6, Math.min(30, Math.sqrt(r.count) * 8)); 175 var color = COLORS[company.concentration] || COLORS.low; 176 177 L.circleMarker([r.lat, r.lng], { 178 radius: radius, 179 fillColor: color, 180 color: "#fff", 181 weight: 1, 182 fillOpacity: 0.7, 183 }).addTo(_mapInstance) 184 .bindPopup( 185 "<strong>" + esc(r.city || "Unknown") + ", " + esc(r.country || "??") + "</strong><br>" 186 + r.count + " endpoints (" + (r.share * 100).toFixed(1) + "%)" 187 ); 188 189 bounds.push([r.lat, r.lng]); 190 } 191 192 if (bounds.length > 1) { 193 _mapInstance.fitBounds(bounds, { padding: [30, 30] }); 194 } else if (bounds.length === 1) { 195 _mapInstance.setView(bounds[0], 6); 196 } 197 198 // Force Leaflet to recalculate after panel opens 199 setTimeout(function () { _mapInstance.invalidateSize(); }, 100); 200 } 201 202 // ââ Summary bar ââââââââââââââââââââââââââââââââââââââââââ 203 204 function injectSummaryBar(rootEl) { 205 if (!_data || !_data.length) return; 206 // Don't inject twice 207 if (rootEl.querySelector(".hp-geo-summary")) return; 208 209 var bar = document.createElement("div"); 210 bar.className = "hp-geo-summary"; 211 bar.innerHTML = '<span class="hp-geo-summary__icon" title="Geographic infrastructure concentration">🌍</span> ' 212 + '<span class="hp-geo-summary__text">
212Infrastructure concentration assessed for <strong>' 213 + _data.length + '</strong> compan' + (_data.length !== 1 ? 'ies' : 'y') 214 + ' — click <span class="hp-geo-badge hp-geo-badge--medium" style="font-size:10px;cursor:default">HHI</span> badges to view maps</span>'; 215 bar.style.cssText = "background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;padding:8px 14px;font-size:12px;color:#166534;margin-bottom:12px;display:flex;align-items:center;gap:8px"; 216 217 // Insert before the feed table 218 var feed = rootEl.querySelector("#hp-signal-feed"); 219 if (feed) { 220 feed.parentNode.insertBefore(bar, feed); 221 } 222 } 223 224 function addColumnHint(rootEl) { 225 var th = rootEl.querySelector(".hp-feed__table th.hp-feed__cell--company, .hp-feed__table th:nth-child(2)"); 226 if (!th || th.querySelector(".hp-geo-hint")) return; 227 var hint = document.createElement("span"); 228 hint.className = "hp-geo-hint"; 229 hint.textContent = " \u24D8"; 230 hint.title = "Some companies have HHI infrastructure concentration badges â click them to view geographic distribution maps"; 231 hint.style.cssText = "color:#5A9BD4;cursor:help;font-size:11px"; 232 th.appendChild(hint); 233 } 234 235 // ââ Init ââââââââââââââââââââââââââââââââââââââââââââââââ 236 237 function init(rootEl) { 238 _rootEl = rootEl; 239 240 fetchGeo(function (err, data) { 241 if (err || !data || !data.companies || !data.companies.length) { 242 if (err) console.warn("[Geo] API error:", err); 243 return; 244 } 245 _data = data.companies; 246 247 // Pre-load the panel template 248 ensurePanel(rootEl); 249 250 // Show summary bar and column hint 251 injectSummaryBar(rootEl); 252 addColumnHint(rootEl); 253 254 // Inject badges into existing rows 255 injectBadges(rootEl); 256 257 // Re-inject after feed filter/sort changes (MutationObserver on tbody) 258 var tbody = rootEl.querySelector(".hp-feed__table tbody"); 259 if (tbody) { 260 new MutationObserver(function () { injectBadges(rootEl); }) 261 .observe(tbody, { childList: true }); 262 } 263 }); 264 } 265 266 function getCompanyNames() { 267 if (!_data) return []; 268 return _data.map(function (c) { return c.group_key; }); 269 } 270 271 // ââ Export ââââââââââââââââââââââââââââââââââââââââââââââ 272 273 window.HAIPHEN = window.HAIPHEN || {}; 274 window.HAIPHEN.Geo = { init: init, getCompanyNames: getCompanyNames }; 275})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.