1/** 2 * Context Panel â Map Tab 3 * 4 * Renders a Leaflet map showing infrastructure concentration 5 * for the selected entity. Lazy-loads Leaflet from CDN. 6 */ 7(function () { 8 "use strict"; 9 10 var API_URL = "https://api.haiphen.io/v1/prospect/geo/concentration"; 11 var LEAFLET_JS = "https://unpkg.com/[email protected]/dist/leaflet.js"; 12 var LEAFLET_CSS = "https://unpkg.com/[email protected]/dist/leaflet.css"; 13 var TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 14 var TILE_ATTR = '© <a href="https://www.openstreetmap.org/copyright">OSM</a>'; 15 16 var COLORS = { high: "#dc2626", medium: "#f59e0b", low: "#10b981" }; 17 18 var _geoData = null; 19 var _leafletReady = false; 20 var _mapInstance = null; 21 var _currentCompany = null; 22 23 function esc(s) { var d = document.createElement("div"); d.textContent = s; return d.innerHTML; } 24 25 function loadLeaflet(cb) { 26 if (_leafletReady) return cb(); 27 var link = document.createElement("link"); 28 link.rel = "stylesheet"; link.href = LEAFLET_CSS; 29 document.head.appendChild(link); 30 var script = document.createElement("script"); 31 script.src = LEAFLET_JS; 32 script.onload = function () { _leafletReady = true; cb(); }; 33 script.onerror = function () { cb(new Error("Leaflet load failed")); }; 34 document.head.appendChild(script); 35 } 36 37 function fetchGeo(cb) { 38 if (_geoData) return cb(null, _geoData); 39 var ctrl = new AbortController(); 40 var timer = setTimeout(function () { ctrl.abort(); }, 10000); 41 fetch(API_URL, { credentials: "include", signal: ctrl.signal }) 42 .then(function (r) { clearTimeout(timer); return r.ok ? r.json() : Promise.reject(r.status); }) 43 .then(function (d) { _geoData = d.companies || []; cb(null, _geoData); }) 44 .catch(function (e) { clearTimeout(timer); cb(e); }); 45 } 46
47 function renderMap(pane, company) { 48 var mapEl = pane.querySelector(".hp-obs__map-container"); 49 if (!mapEl) return; 50 51 if (_mapInstance) { _mapInstance.remove(); _mapInstance = null; } 52 53 _mapInstance = L.map(mapEl, { scrollWheelZoom: false }).setView([20, 0], 2); 54 L.tileLayer(TILE_URL, { attribution: TILE_ATTR, maxZoom: 18 }).addTo(_mapInstance); 55 56 var bounds = []; 57 for (var i = 0; i < company.regions.length; i++) { 58 var r = company.regions[i]; 59 if (r.lat == null || r.lng == null) continue; 60 61 var radius = Math.max(6, Math.min(30, Math.sqrt(r.count) * 8)); 62 var color = COLORS[company.concentration] || COLORS.low; 63 64 L.circleMarker([r.lat, r.lng], { 65 radius: radius, fillColor: color, color: "#fff", weight: 1, fillOpacity: 0.7, 66 }).addTo(_mapInstance).bindPopup( 67 "<strong>" + esc(r.city || "Unknown") + ", " + esc(r.country || "??") + "</strong><br>" 68 + r.count + " endpoints (" + (r.share * 100).toFixed(1) + "%)" 69 ); 70 bounds.push([r.lat, r.lng]); 71 } 72 73 if (bounds.length > 1) { 74 _mapInstance.fitBounds(bounds, { padding: [30, 30] }); 75 } else if (bounds.length === 1) { 76 _mapInstance.setView(bounds[0], 6); 77 } 78 79 setTimeout(function () { if (_mapInstance) _mapInstance.invalidateSize(); }, 100); 80 } 81 82 function render(pane, entity) { 83 if (!pane || !entity) return; 84 85 var companyName = (entity.company_name || "").toLowerCase(); 86 87 // If same company, just invalidate map size 88 if (_currentCompany === companyName && _mapInstance) { 89 setTimeout(function () { _mapInstance.invalidateSize(); }, 50); 90 return; 91 } 92 _currentCompany = companyName; 93 94 pane.innerHTML = '<div class="hp-obs__map-container"></div>' 95 + '<div class="hp-obs__map-legend">Loading geographic data…</div>'; 96 97 fetchGeo(function (err, companies) { 98 if (err || !companies) { 99 pane.querySelector(".hp-obs__map-legend").textContent = "Geographic data unavailable."; 100 return; 101 } 102 103 var match = null; 104 for (var i = 0; i < companies.length; i++) { 105 if (companies[i].group_key.toLowerCase() === companyName) { 106 match = companies[i]; break; 107 } 108 } 109 110 if (!match || !match.regions || !match.regions.length) { 111 pane.querySelector(".hp-obs__map-legend").textContent = "No infrastructure data for " + (entity.company_name || "this entity") + "."; 112 return; 113 } 114 115 var legend = pane.querySelector(".hp-obs__map-legend"); 116 if (legend) { 117 legend.textContent = "HHI: " + match.hhi.toFixed(3) + " (" + match.concentration 118 + ") â " + match.total_infrastructure + " endpoints across " 119 + match.regions.length + " regions"; 120 } 121 122 loadLeaflet(function (leafErr) { 123 if (leafErr) { 124 if (legend) legend.textContent = "Map library failed to load."; 125 return; 126 } 127 renderMap(pane, match); 128 }); 129 }); 130 } 131 132 window.HAIPHEN = window.HAIPHEN || {}; 133 window.HAIPHEN.Featured = window.HAIPHEN.Featured || {}; 134 window.HAIPHEN.Featured.ContextMap = { render: render }; 135})();
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.