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 <noreply@anthropic.com>
This commit is contained in:
Marc Shade
2026-02-24 17:26:04 -05:00
co-authored by Claude Opus 4.6
parent b9eb85bb64
commit fc915be90a
+96 -3
View File
@@ -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; }
<span class="layer-count" id="cntClimate">0</span>
<div class="toggle on" style="--lc:#ff6b6b"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="news">
<span class="layer-dot" style="background:var(--accent)"></span>
<span class="layer-label">News</span>
<span class="layer-count" id="cntNews">0</span>
<div class="toggle on" style="--lc:var(--accent)"><div class="knob"></div></div>
</div>
</aside>
<!-- DATA DRAWER -->
@@ -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)) + '<br><span style="opacity:0.6">' + esc(a.feed_name || '') + '</span>', { 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)';