From fc915be90a18120eb9b8c6a44178f1b6eef11c8b Mon Sep 17 00:00:00 2001 From: Marc Shade Date: Tue, 24 Feb 2026 17:26:04 -0500 Subject: [PATCH] feat: news geolocation map layer with 75-country centroid lookup Client-side geolocation scans article titles/summaries for country mentions and plots them at country centroids with jitter. Integrated into layer toggles, search, HUD click-to-zoom, and detail panel. Co-Authored-By: Claude Opus 4.6 --- src/world_intel_mcp/dashboard/index.html | 99 +++++++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/src/world_intel_mcp/dashboard/index.html b/src/world_intel_mcp/dashboard/index.html index cc300a2..2ec5ad3 100644 --- a/src/world_intel_mcp/dashboard/index.html +++ b/src/world_intel_mcp/dashboard/index.html @@ -445,6 +445,13 @@ a { color: var(--accent); text-decoration: none; } .mk-climate.cool { background: radial-gradient(circle, rgba(100,181,246,0.7) 20%, rgba(100,181,246,0) 70%); border: 2px solid rgba(100,181,246,0.5); } .mk-climate.cold { background: radial-gradient(circle, rgba(77,168,255,0.8) 20%, rgba(77,168,255,0) 70%); border: 2px solid rgba(77,168,255,0.6); } .mk-climate.sig { animation: glow-climate 2s ease-in-out infinite; } +.mk-news { + width: 6px; height: 6px; border-radius: 50%; + background: rgba(0,229,255,0.7); + border: 1px solid rgba(0,229,255,0.5); + cursor: pointer; transition: transform 0.15s; +} +.mk-news:hover { transform: scale(2); } @keyframes glow-climate { 0%,100% { box-shadow: 0 0 4px rgba(255,107,107,0.3); } 50% { box-shadow: 0 0 18px rgba(255,107,107,0.7); } @@ -641,6 +648,12 @@ a { color: var(--accent); text-decoration: none; } 0
+
+ + News + 0 +
+
@@ -687,7 +700,7 @@ function ago(ts) { if (!ts) return '\u2014'; var s = Math.floor((Date.now() - ne var latestData = null; var map = null; var mapLayers = {}; -var layerState = { quakes: true, military: true, conflict: true, fires: true, convergence: true, nuclear: true, infra: true, exposure: true, airtraffic: true, traffic: true, navwarnings: true, webcams: true, climate: true }; +var layerState = { quakes: true, military: true, conflict: true, fires: true, convergence: true, nuclear: true, infra: true, exposure: true, airtraffic: true, traffic: true, navwarnings: true, webcams: true, climate: true, news: true }; // Restore saved layer toggles from localStorage try { var _saved = JSON.parse(localStorage.getItem('phoenix-layers')); if (_saved) { for (var _k in _saved) { if (layerState.hasOwnProperty(_k)) layerState[_k] = _saved[_k]; } } } catch(e) {} var drawerOpen = false; @@ -722,6 +735,7 @@ function initMap() { mapLayers.navwarnings = L.markerClusterGroup(clusterOpts(null, 40)).addTo(map); mapLayers.webcams = L.layerGroup().addTo(map); mapLayers.climate = L.layerGroup().addTo(map); + mapLayers.news = L.markerClusterGroup(clusterOpts(null, 35)).addTo(map); } // ════════════ LAYER TOGGLES ════════════ @@ -915,6 +929,19 @@ function showDetail(type, d) { ['Coordinates', d.lat != null ? d.lat + ', ' + d.lon : '\u2014'] ]; link = d.player_url || ''; + } else if (type === 'news') { + title = d.title || 'Article'; + subtitle = (d.feed_name || '') + (d._geo_country ? ' \u2014 ' + d._geo_country : ''); + fields = [ + ['Title', d.title || '\u2014'], + ['Source', d.feed_name || '\u2014'], + ['Category', d.category || '\u2014'], + ['Published', d.published ? new Date(d.published).toLocaleString() : '\u2014'], + ['Summary', trunc(d.summary || '', 200)], + ['Tier', d.source_tier || '\u2014'], + ['Region', d._geo_country || '\u2014'] + ]; + link = d.link || ''; } else if (type === 'navwarn') { title = 'NAVAREA ' + (d.navarea || '?') + ' \u2014 ' + (d.id || ''); subtitle = d.status || ''; @@ -1610,6 +1637,68 @@ function updateMapClimate(data) { }); } +// Country name → centroid for news geolocation +var COUNTRY_CENTROIDS = { + 'ukraine':[48.4,31.2],'russia':[61.5,105],'china':[35.9,104.2],'taiwan':[23.7,121], + 'iran':[32.4,53.7],'israel':[31.0,34.8],'palestine':[31.9,35.2],'gaza':[31.4,34.4], + 'syria':[35,38.9],'iraq':[33.2,43.7],'yemen':[15.6,48.5],'lebanon':[33.9,35.9], + 'north korea':[40,127],'south korea':[35.9,128],'japan':[36.2,138.3],'india':[20.6,79], + 'pakistan':[30.4,69.3],'afghanistan':[33.9,67.7],'myanmar':[19.8,96.7], + 'ethiopia':[9.1,40.5],'sudan':[12.9,30.2],'somalia':[5.2,46.2],'nigeria':[9.1,8.7], + 'congo':[-4.0,21.8],'mali':[17.6,-4],'libya':[26.3,17.2],'mozambique':[-18.7,35.5], + 'united states':[37.1,-95.7],'usa':[37.1,-95.7],'u.s.':[37.1,-95.7], + 'united kingdom':[55.4,-3.4],'uk':[55.4,-3.4],'britain':[55.4,-3.4], + 'france':[46.2,2.2],'germany':[51.2,10.4],'italy':[41.9,12.6],'spain':[40.5,-3.7], + 'turkey':[39,35.2],'egypt':[26.8,30.8],'saudi arabia':[23.9,45.1], + 'brazil':[-14.2,-51.9],'mexico':[23.6,-102.6],'canada':[56.1,-106.3], + 'australia':[-25.3,133.8],'south africa':[-30.6,22.9],'colombia':[4.6,-74.3], + 'venezuela':[6.4,-66.6],'argentina':[-38.4,-63.6],'peru':[-9.2,-75], + 'philippines':[12.9,122],'indonesia':[-0.8,113.9],'thailand':[15.9,100.9], + 'vietnam':[14.1,108.3],'malaysia':[4.2,101.9],'singapore':[1.4,103.8], + 'poland':[51.9,19.1],'romania':[45.9,25],'greece':[39.1,21.8], + 'sweden':[60.1,18.6],'norway':[60.5,8.5],'finland':[61.9,25.7], + 'netherlands':[52.1,5.3],'belgium':[50.5,4.5],'switzerland':[46.8,8.2], + 'austria':[47.5,14.6],'hungary':[47.2,19.5],'czech':[49.8,15.5], + 'kenya':[-0.02,37.9],'ghana':[7.9,-1.02],'senegal':[14.5,-14.5], + 'morocco':[31.8,-7.1],'algeria':[28.0,1.7],'tunisia':[33.9,9.5], + 'new zealand':[-40.9,174.9],'chile':[-35.7,-71.5] +}; + +function _geolocateArticle(article) { + var text = ((article.title || '') + ' ' + (article.summary || '')).toLowerCase(); + for (var country in COUNTRY_CENTROIDS) { + if (COUNTRY_CENTROIDS.hasOwnProperty(country) && text.indexOf(country) >= 0) { + return { country: country, lat: COUNTRY_CENTROIDS[country][0], lon: COUNTRY_CENTROIDS[country][1] }; + } + } + return null; +} + +function updateMapNews(data) { + mapLayers.news.clearLayers(); + if (!data || data.error) { $('#cntNews').textContent = '0'; return; } + var articles = data.articles || data.items || []; + var placed = 0; + articles.forEach(function(a) { + var geo = _geolocateArticle(a); + if (!geo) return; + placed++; + // Jitter to avoid exact overlap + var jlat = geo.lat + (Math.random() - 0.5) * 2; + var jlon = geo.lon + (Math.random() - 0.5) * 2; + a._geo_country = geo.country; + a._geo_lat = jlat; + a._geo_lon = jlon; + var mk = L.marker([jlat, jlon], { + icon: L.divIcon({ className: 'mk-news', iconSize: [6, 6], iconAnchor: [3, 3] }) + }); + mk.bindTooltip(esc(trunc(a.title || '?', 40)) + '
' + esc(a.feed_name || '') + '', { className: 'mk-tip', direction: 'top', offset: [0, -4] }); + mk.on('click', function() { showDetail('news', a); }); + mk.addTo(mapLayers.news); + }); + $('#cntNews').textContent = placed; +} + function updateAlertBanner(data) { var el = $('#alertBanner'); if (!data || data.error || !data.alerts || !data.alerts.length) { @@ -2438,7 +2527,8 @@ function updateAll(data) { ['traffic', function() { updateMapTraffic(data.traffic_flow); }], ['navwarnings', function() { updateMapNavWarnings(data.nav_warnings); }], ['webcams', function() { updateMapWebcams(data.webcams); }], - ['climate', function() { updateMapClimate(data.climate_anomalies); }] + ['climate', function() { updateMapClimate(data.climate_anomalies); }], + ['news', function() { updateMapNews(data.news_feed); }] ]; mapUpdates.forEach(function(pair) { try { pair[1](); } catch(e) { console.warn('updateAll: ' + pair[0] + ' failed:', e); } @@ -2526,7 +2616,7 @@ $('#drawerBody').addEventListener('click', function(e) { }); // HUD pill click-to-zoom -var HUD_LAYER_MAP = { 'Quakes': 'quakes', 'Aircraft': 'military', 'Conflict': 'conflict', 'Fires': 'fires', 'Exposed': 'exposure', 'Airborne': 'airtraffic', 'Traffic': 'traffic', 'Cams': 'webcams', 'Nuke Flag': 'nuclear', 'Climate': 'climate' }; +var HUD_LAYER_MAP = { 'Quakes': 'quakes', 'Aircraft': 'military', 'Conflict': 'conflict', 'Fires': 'fires', 'Exposed': 'exposure', 'Airborne': 'airtraffic', 'Traffic': 'traffic', 'Cams': 'webcams', 'Nuke Flag': 'nuclear', 'Climate': 'climate', 'News': 'news' }; $('#hudStats').addEventListener('click', function(e) { var pill = e.target.closest('.stat-pill'); if (!pill) return; @@ -2611,6 +2701,9 @@ function performSearch(q) { var climZones = climSrc.anomalies || climSrc.zones || climSrc.data || []; if (!Array.isArray(climZones) && typeof climZones === 'object') climZones = Object.values(climZones); _scan(climZones, 'lat', 'lon'); + // News articles with geolocation + var newsArticles = (latestData.news_feed || {}).articles || (latestData.news_feed || {}).items || []; + newsArticles.forEach(function(a) { if (a._geo_lat && _matchText(a, q)) coords.push([a._geo_lat, a._geo_lon]); }); if (coords.length > 0) { map.fitBounds(L.latLngBounds(coords), { padding: [60, 60], maxZoom: coords.length === 1 ? 10 : 6 }); $('#searchInput').style.borderColor = 'rgba(0,229,255,0.4)';