From 102c0fd3ac24380ea9c9c8c1d8761571b190bddb Mon Sep 17 00:00:00 2001 From: PetroczyP Date: Fri, 20 Mar 2026 21:10:29 +0100 Subject: [PATCH 1/3] feat: add layer toggles to Sensor Grid for all 13 map layers Users can now click any Sensor Grid row to show/hide its corresponding map layer on both 3D Globe and D3 flat map. Toggle state persists in localStorage. Absorbs the standalone flight toggle into Air Activity. Adds 3 new Sensor Grid rows: NOAA Weather, EPA RadNet, GDELT Events. Co-Authored-By: Claude Opus 4.6 --- dashboard/public/jarvis.html | 180 ++++++++++++++++++++--------------- 1 file changed, 105 insertions(+), 75 deletions(-) diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index 0be78650..ef6dbaa9 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -67,7 +67,8 @@ /* GRID */ .grid{display:grid;grid-template-columns:240px 1fr 340px;gap:10px;margin-top:10px;min-height:calc(100vh - 100px)} .col{display:flex;flex-direction:column;gap:10px} -.g-panel{border:1px solid var(--border);background:var(--glass);backdrop-filter:blur(16px);padding:12px;position:relative;overflow:hidden} +#leftRail{overflow-y:auto} +.g-panel{border:1px solid var(--border);background:var(--glass);backdrop-filter:blur(16px);padding:12px;position:relative;flex-shrink:0} .g-panel::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(100,240,200,0.15),transparent);pointer-events:none} .sec-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px} .sec-head h3{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.14em;text-transform:uppercase;color:var(--dim)} @@ -86,6 +87,18 @@ .ldot.health{background:#69f0ae;box-shadow:0 0 6px rgba(105,240,174,0.4)} .ldot.news{background:#81d4fa;box-shadow:0 0 6px rgba(129,212,250,0.4)} .ldot.space{background:#e0b0ff;box-shadow:0 0 6px rgba(224,176,255,0.4)} +.ldot.osint{background:var(--warn);box-shadow:0 0 6px rgba(255,184,76,0.4)} +.ldot.noaa{background:#ff9800;box-shadow:0 0 6px rgba(255,152,0,0.4)} +.ldot.epa{background:#cddc39;box-shadow:0 0 6px rgba(205,220,57,0.4)} +.ldot.gdelt{background:#6495ed;box-shadow:0 0 6px rgba(100,149,237,0.4)} +.layer-item{cursor:pointer;transition:opacity 0.2s,border-color 0.2s} +.layer-item-off{opacity:0.35} +.layer-item-off .ldot{box-shadow:none} +.layer-item-off .layer-name{color:#8899aa} +.layer-item-off .layer-sub{color:#445566} +.layer-item-off .layer-count{color:#556677} +.layer-eye{font-size:11px;margin-left:6px;flex-shrink:0;opacity:0.5} +.layer-item-off .layer-eye{opacity:0.2} .layer-name{font-size:12px;font-weight:500} .layer-sub{font-size:10px;color:var(--dim)} .layer-count{font-family:var(--mono);font-size:13px;font-weight:600;color:var(--accent)} @@ -346,7 +359,6 @@
-
@@ -392,9 +404,44 @@ // === GLOBALS === let globe = null; let globeInitialized = false; -let flightsVisible = true; let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true'; let isFlat = shouldStartFlat(); + +// === LAYER TOGGLE STATE === +const LAYERS = [ + {id:'air', label:'Air Activity', color:'#64f0c8', dot:'air'}, + {id:'thermal', label:'Thermal Spikes', color:'#ff5f63', dot:'thermal'}, + {id:'sdr', label:'SDR Coverage', color:'#44ccff', dot:'sdr'}, + {id:'maritime',label:'Maritime Watch', color:'#b388ff', dot:'maritime'}, + {id:'nuke', label:'Nuclear Sites', color:'#ffe082', dot:'nuke'}, + {id:'acled', label:'Conflict Events',color:'rgba(255,120,80,0.8)',dot:'incident'}, + {id:'who', label:'Health Watch', color:'#69f0ae', dot:'health'}, + {id:'news', label:'World News', color:'#81d4fa', dot:'news'}, + {id:'osint', label:'OSINT Feed', color:'#ffb84c', dot:'osint'}, + {id:'space', label:'Satellites', color:'#e0b0ff', dot:'space'}, + {id:'noaa', label:'Weather Alerts', color:'#ff9800', dot:'noaa'}, + {id:'epa', label:'EPA RadNet', color:'#cddc39', dot:'epa'}, + {id:'gdelt', label:'GDELT Events', color:'#6495ed', dot:'gdelt'}, +]; +let layerVisibility = (()=>{ + try { + const saved = JSON.parse(localStorage.getItem('crucix-layers')); + if (saved && typeof saved === 'object') { + const vis = {}; + for (const l of LAYERS) vis[l.id] = saved[l.id] !== undefined ? saved[l.id] : true; + return vis; + } + } catch {} + return Object.fromEntries(LAYERS.map(l => [l.id, true])); +})(); +function toggleLayer(id) { + layerVisibility[id] = !layerVisibility[id]; + localStorage.setItem('crucix-layers', JSON.stringify(layerVisibility)); + renderLeftRail(); + renderMapLegend(); + plotMarkers(); + if (isFlat && flatG) { flatG.selectAll('*').remove(); drawFlatMap(); } +} let currentRegion = 'world'; let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH; const signalGuideItems = [ @@ -610,18 +657,21 @@ const newsCount=(D.news||[]).length; const conflictEvents = D.acled?.totalEvents || 0; const conflictFatal = D.acled?.totalFatalities || 0; - const layers=[ - {name:t('layers.airActivity','Air Activity'),count:totalAir,dot:'air',sub:`${D.air.length} ${t('layers.theaters','theaters')}`}, - {name:t('layers.thermalSpikes','Thermal Spikes'),count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`}, - {name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`}, - {name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')}, - {name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')}, - {name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`}, - {name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')}, - {name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')}, - {name:t('layers.osintFeed','OSINT Feed'),count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`}, - {name:t('layers.spaceActivity','Satellites'),count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`} - ]; + const layerData = { + air: {count:totalAir, sub:`${D.air.length} ${t('layers.theaters','theaters')}`}, + thermal: {count:totalThermal.toLocaleString(), sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`}, + sdr: {count:D.sdr.total, sub:`${D.sdr.online} ${t('layers.online','online')}`}, + maritime: {count:D.chokepoints.length, sub:t('layers.chokepoints','chokepoints')}, + nuke: {count:D.nuke.length, sub:t('layers.monitors','monitors')}, + acled: {count:conflictEvents, sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`}, + who: {count:D.who.length, sub:t('layers.whoAlerts','WHO alerts')}, + news: {count:newsCount, sub:t('layers.rssGeolocated','RSS geolocated')}, + osint: {count:D.tg.posts, sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`}, + space: {count:D.space?.militarySats||0, sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`}, + noaa: {count:D.noaa?.totalAlerts||0, sub:t('layers.severeWeather','severe weather')}, + epa: {count:D.epa?.totalReadings||0, sub:t('layers.radiationNet','radiation net')}, + gdelt: {count:(D.gdelt?.geoPoints||[]).length, sub:t('layers.globalNewsGeo','global news geo')}, + }; const allNormal=D.nuke.every(s=>!s.anom); const nukeHtml=D.nuke.map(s=>`
${s.site}${s.n>0?(s.cpm?.toFixed(1)||'--')+' CPM':'No data'}
`).join(''); const vix=D.fred.find(f=>f.id==='VIXCLS'); @@ -634,7 +684,11 @@ document.getElementById('leftRail').innerHTML=`

${t('panels.sensorGrid','Sensor Grid')}

${t('badges.live','LIVE')}
- ${layers.map(l=>`
${l.name}
${l.sub}
${l.count}
`).join('')} + ${LAYERS.map(l=>{ + const vis = layerVisibility[l.id]; + const d = layerData[l.id] || {count:0, sub:''}; + return `
${l.label}
${vis ? d.sub : 'hidden'}
${vis ? d.count : '\u2014'}
👁️
`; + }).join('')}

${t('panels.nuclearWatch','Nuclear Watch')}

${t('badges.radiation','RADIATION')}
@@ -680,10 +734,13 @@ } function renderMapLegend(){ + const legendMap = {air:'Air Traffic',thermal:'Thermal/Fire',acled:'Conflict',sdr:'SDR Receiver',nuke:'Nuclear Site',maritime:'Chokepoint',osint:'OSINT Event',who:'Health Alert',news:'World News',noaa:'Weather Alert',epa:'EPA RadNet',space:'Space Station',gdelt:'GDELT Event'}; document.getElementById('mapLegend').innerHTML= - [{c:'#64f0c8',l:t('map.airTraffic','Air Traffic')},{c:'#ff5f63',l:t('map.thermalFire','Thermal/Fire')},{c:'rgba(255,120,80,0.8)',l:t('map.conflict','Conflict')},{c:'#44ccff',l:t('map.sdrReceiver','SDR Receiver')}, - {c:'#ffe082',l:t('map.nuclearSite','Nuclear Site')},{c:'#b388ff',l:t('map.chokepoint','Chokepoint')},{c:'#ffb84c',l:t('map.osintEvent','OSINT Event')},{c:'#69f0ae',l:t('map.healthAlert','Health Alert')},{c:'#81d4fa',l:t('map.worldNews','World News')},{c:'#ff9800',l:t('map.weatherAlert','Weather Alert')},{c:'#cddc39',l:t('map.epaRadNet','EPA RadNet')},{c:'#ffffff',l:t('map.spaceStation','Space Station')},{c:'#6495ed',l:t('map.gdeltEvent','GDELT Event')}] - .map(x=>`
${x.l}
`).join(''); + LAYERS.map(l=>{ + const vis = layerVisibility[l.id]; + const label = legendMap[l.id] || l.label; + return `
${label}
`; + }).join(''); } function initMap(){ @@ -842,7 +899,7 @@ // === Air hotspots (green) === const airCoords=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}]; - if(flightsVisible) D.air.forEach((a,i)=>{ + if(layerVisibility.air) D.air.forEach((a,i)=>{ const c=airCoords[i]; if(!c) return; points.push({ lat:c.lat, lng:c.lon, size:0.25+a.total/200, alt:0.015, @@ -855,7 +912,7 @@ }); // === Thermal/fire (red) === - D.thermal.forEach(t=>{ + if(layerVisibility.thermal) D.thermal.forEach(t=>{ t.fires.forEach(f=>{ points.push({ lat:f.lat, lng:f.lon, size:0.12+Math.min(f.frp/200,0.3), alt:0.008, @@ -867,7 +924,7 @@ }); // === Maritime chokepoints (purple) === - D.chokepoints.forEach(cp=>{ + if(layerVisibility.maritime) D.chokepoints.forEach(cp=>{ points.push({ lat:cp.lat, lng:cp.lon, size:0.35, alt:0.02, color:'rgba(179,136,255,0.8)', type:'maritime', priority:1, @@ -878,7 +935,7 @@ // === Nuclear sites (yellow) === const nukeCoords=[{lat:47.5,lon:34.6},{lat:51.4,lon:30.1},{lat:28.8,lon:50.9},{lat:39.8,lon:125.8},{lat:37.4,lon:141},{lat:31.0,lon:35.1}]; - D.nuke.forEach((n,i)=>{ + if(layerVisibility.nuke) D.nuke.forEach((n,i)=>{ const c=nukeCoords[i]; if(!c) return; points.push({ lat:c.lat, lng:c.lon, size:0.3, alt:0.012, @@ -889,7 +946,7 @@ }); // === SDR receivers (cyan) === - D.sdr.zones.forEach(z=>{ + if(layerVisibility.sdr) D.sdr.zones.forEach(z=>{ z.receivers.forEach(r=>{ points.push({ lat:r.lat, lng:r.lon, size:0.15, alt:0.005, @@ -902,7 +959,7 @@ // === OSINT events from Telegram (orange) === const osintGeo=[{lat:45,lon:41,idx:0},{lat:48,lon:37,idx:1},{lat:48.5,lon:37.5,idx:2},{lat:45,lon:40.2,idx:3},{lat:50.6,lon:36.6,idx:5},{lat:48.5,lon:35,idx:6}]; - osintGeo.forEach(o=>{ + if(layerVisibility.osint) osintGeo.forEach(o=>{ const post=D.tg.urgent[o.idx]; if(!post) return; points.push({ lat:o.lat, lng:o.lon, size:0.3, alt:0.018, @@ -914,7 +971,7 @@ // === WHO health alerts (green) === const whoGeo=[{lat:0.3,lon:32.6},{lat:-6.2,lon:106.8},{lat:-4.3,lon:15.3},{lat:35,lon:105},{lat:12.5,lon:105},{lat:35,lon:105},{lat:28,lon:84},{lat:24,lon:45},{lat:30,lon:70},{lat:-0.8,lon:11.6}]; - D.who.slice(0,10).forEach((w,i)=>{ + if(layerVisibility.who) D.who.slice(0,10).forEach((w,i)=>{ const c=whoGeo[i]; if(!c) return; points.push({ lat:c.lat, lng:c.lon, size:0.25, alt:0.01, @@ -924,7 +981,7 @@ }); // === News markers (light blue) === - (D.news||[]).forEach(n=>{ + if(layerVisibility.news) (D.news||[]).forEach(n=>{ points.push({ lat:n.lat, lng:n.lon, size:0.2, alt:0.008, color:'rgba(129,212,250,0.7)', type:'news', priority:3, @@ -934,7 +991,7 @@ }); // === NOAA severe weather alerts (orange) === - (D.noaa?.alerts||[]).forEach(a=>{ + if(layerVisibility.noaa) (D.noaa?.alerts||[]).forEach(a=>{ points.push({ lat:a.lat, lng:a.lon, size:0.22, alt:0.01, color:'rgba(255,152,0,0.8)', type:'weather', priority:2, @@ -944,7 +1001,7 @@ }); // === EPA RadNet stations (lime green) === - (D.epa?.stations||[]).forEach(s=>{ + if(layerVisibility.epa) (D.epa?.stations||[]).forEach(s=>{ points.push({ lat:s.lat, lng:s.lon, size:0.18, alt:0.006, color:'rgba(205,220,57,0.7)', type:'radiation', priority:3, @@ -954,7 +1011,7 @@ }); // === ISS + Space Stations (bright white, pulsing) === - (D.space?.stationPositions||[]).forEach(s=>{ + if(layerVisibility.space) (D.space?.stationPositions||[]).forEach(s=>{ points.push({ lat:s.lat, lng:s.lon, size:0.4, alt:0.04, color:'rgba(255,255,255,0.95)', type:'space', priority:1, @@ -965,7 +1022,7 @@ }); // === GDELT geo events (steel blue) === - (D.gdelt?.geoPoints||[]).forEach(g=>{ + if(layerVisibility.gdelt) (D.gdelt?.geoPoints||[]).forEach(g=>{ points.push({ lat:g.lat, lng:g.lon, size:0.15+Math.min(g.count/50,0.2), alt:0.007, color:'rgba(100,149,237,0.6)', type:'gdelt', priority:3, @@ -979,7 +1036,7 @@ globe.labelsData(labels); // === ACLED CONFLICT EVENTS (pulsing rings) === - const conflictRings = (D.acled?.deadliestEvents || []).filter(e => e.lat && e.lon).map(e => { + const conflictRings = !layerVisibility.acled ? [] : (D.acled?.deadliestEvents || []).filter(e => e.lat && e.lon).map(e => { const logFatal = Math.log2(Math.max(e.fatalities, 1)); return { lat: e.lat, lng: e.lon, @@ -994,7 +1051,7 @@ // === FLIGHT CORRIDORS (3D arcs) === const arcs = []; - if(flightsVisible){ + if(layerVisibility.air){ const airCoordsFlight = [ {region:'Middle East',lat:30,lon:44}, {region:'Taiwan Strait',lat:24,lon:120}, {region:'Ukraine Region',lat:49,lon:32}, {region:'Baltic Region',lat:57,lon:24}, @@ -1095,33 +1152,6 @@ } function closePopup(){document.getElementById('mapPopup').classList.remove('show')} -// === MAP CONTROLS === -function toggleFlights() { - flightsVisible = !flightsVisible; - const btn = document.getElementById('flightToggle'); - btn.classList.toggle('off', !flightsVisible); - if(isFlat){ - if(flatG){ - flatG.selectAll('*').remove(); - drawFlatMap(); - } - return; - } - if(!globe){ - return; - } - if(flightsVisible) { - plotMarkers(); // re-render with arcs - } else { - globe.arcsData([]); // hide arcs - // Remove air-type points - const pts = globe.pointsData().filter(p => p.type !== 'air'); - globe.pointsData(pts); - const lbls = globe.labelsData().filter(l => l.text && !l.text.match(/\d+$/)); - globe.labelsData(lbls); - } -} - // === FLAT/GLOBE TOGGLE === const flatRegionBounds = { world:[[-180,-60],[180,80]], americas:[[-130,10],[-60,55]], europe:[[-12,34],[45,72]], @@ -1207,7 +1237,7 @@ }; // Air const airCoords=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}]; - if(flightsVisible){ + if(layerVisibility.air){ D.air.forEach((a,i)=>{ const c=airCoords[i];if(!c)return; const g=addPt(c.lat,c.lon,4+a.total/40,'rgba(100,240,200,0.7)','rgba(100,240,200,0.3)', @@ -1216,12 +1246,12 @@ }); } // Thermal - D.thermal.forEach(t=>t.fires.forEach(f=>{ + if(layerVisibility.thermal) D.thermal.forEach(t=>t.fires.forEach(f=>{ addPt(f.lat,f.lon,2+Math.min(f.frp/50,5),'rgba(255,95,99,0.6)','rgba(255,95,99,0.2)', ev=>showPopup(ev,'Thermal',`${t.region}
FRP: ${f.frp.toFixed(1)} MW`,'FIRMS'),3); })); // Chokepoints - D.chokepoints.forEach(cp=>{ + if(layerVisibility.maritime) D.chokepoints.forEach(cp=>{ const[x,y]=proj([cp.lon,cp.lat]);if(!x||!y)return; const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1) .on('click',ev=>{ev.stopPropagation();showPopup(ev,cp.label,cp.note,'Maritime')}); @@ -1230,30 +1260,30 @@ }); // Nuclear const nukeCoords=[{lat:47.5,lon:34.6},{lat:51.4,lon:30.1},{lat:28.8,lon:50.9},{lat:39.8,lon:125.8},{lat:37.4,lon:141},{lat:31.0,lon:35.1}]; - D.nuke.forEach((n,i)=>{const c=nukeCoords[i];if(!c)return;addPt(c.lat,c.lon,4,'rgba(255,224,130,0.7)','rgba(255,224,130,0.3)',ev=>showPopup(ev,n.site,`CPM: ${n.cpm?.toFixed(1)||'--'}`,'Radiation'),2)}); + if(layerVisibility.nuke) D.nuke.forEach((n,i)=>{const c=nukeCoords[i];if(!c)return;addPt(c.lat,c.lon,4,'rgba(255,224,130,0.7)','rgba(255,224,130,0.3)',ev=>showPopup(ev,n.site,`CPM: ${n.cpm?.toFixed(1)||'--'}`,'Radiation'),2)}); // SDR - D.sdr.zones.forEach(z=>z.receivers.forEach(r=>{addPt(r.lat,r.lon,2.5,'rgba(68,204,255,0.5)','rgba(68,204,255,0.2)',ev=>showPopup(ev,'SDR',`${r.name}
${z.region}`,'KiwiSDR'),3)})); + if(layerVisibility.sdr) D.sdr.zones.forEach(z=>z.receivers.forEach(r=>{addPt(r.lat,r.lon,2.5,'rgba(68,204,255,0.5)','rgba(68,204,255,0.2)',ev=>showPopup(ev,'SDR',`${r.name}
${z.region}`,'KiwiSDR'),3)})); // OSINT const osintGeo=[{lat:45,lon:41,idx:0},{lat:48,lon:37,idx:1},{lat:48.5,lon:37.5,idx:2},{lat:45,lon:40.2,idx:3},{lat:50.6,lon:36.6,idx:5},{lat:48.5,lon:35,idx:6}]; - osintGeo.forEach(o=>{const p=D.tg.urgent[o.idx];if(!p)return;addPt(o.lat,o.lon,4,'rgba(255,184,76,0.7)','rgba(255,184,76,0.3)',ev=>showPopup(ev,(p.channel||'').toUpperCase(),cleanText(p.text?.substring(0,200)||''),`${p.views||'?'} views`),2)}); + if(layerVisibility.osint) osintGeo.forEach(o=>{const p=D.tg.urgent[o.idx];if(!p)return;addPt(o.lat,o.lon,4,'rgba(255,184,76,0.7)','rgba(255,184,76,0.3)',ev=>showPopup(ev,(p.channel||'').toUpperCase(),cleanText(p.text?.substring(0,200)||''),`${p.views||'?'} views`),2)}); // WHO const whoGeo=[{lat:0.3,lon:32.6},{lat:-6.2,lon:106.8},{lat:-4.3,lon:15.3},{lat:35,lon:105},{lat:12.5,lon:105},{lat:35,lon:105},{lat:28,lon:84},{lat:24,lon:45},{lat:30,lon:70},{lat:-0.8,lon:11.6}]; - D.who.slice(0,10).forEach((w,i)=>{const c=whoGeo[i];if(!c)return;addPt(c.lat,c.lon,3.5,'rgba(105,240,174,0.6)','rgba(105,240,174,0.2)',ev=>showPopup(ev,w.title,w.summary||'','WHO'),2)}); + if(layerVisibility.who) D.who.slice(0,10).forEach((w,i)=>{const c=whoGeo[i];if(!c)return;addPt(c.lat,c.lon,3.5,'rgba(105,240,174,0.6)','rgba(105,240,174,0.2)',ev=>showPopup(ev,w.title,w.summary||'','WHO'),2)}); // News - (D.news||[]).forEach(n=>{addPt(n.lat,n.lon,3,'rgba(129,212,250,0.6)','rgba(129,212,250,0.2)',ev=>showPopup(ev,n.source+' NEWS',cleanText(n.title),n.region),3)}); + if(layerVisibility.news) (D.news||[]).forEach(n=>{addPt(n.lat,n.lon,3,'rgba(129,212,250,0.6)','rgba(129,212,250,0.2)',ev=>showPopup(ev,n.source+' NEWS',cleanText(n.title),n.region),3)}); // NOAA weather - (D.noaa?.alerts||[]).forEach(a=>{addPt(a.lat,a.lon,4,'rgba(255,152,0,0.7)','rgba(255,152,0,0.3)',ev=>showPopup(ev,a.event,a.headline||'','NOAA/NWS'),2)}); + if(layerVisibility.noaa) (D.noaa?.alerts||[]).forEach(a=>{addPt(a.lat,a.lon,4,'rgba(255,152,0,0.7)','rgba(255,152,0,0.3)',ev=>showPopup(ev,a.event,a.headline||'','NOAA/NWS'),2)}); // EPA RadNet - (D.epa?.stations||[]).forEach(s=>{addPt(s.lat,s.lon,3,'rgba(205,220,57,0.6)','rgba(205,220,57,0.2)',ev=>showPopup(ev,'RadNet: '+s.location,`${s.analyte||'--'}: ${s.result||'--'} ${s.unit||''}`,'EPA'),3)}); + if(layerVisibility.epa) (D.epa?.stations||[]).forEach(s=>{addPt(s.lat,s.lon,3,'rgba(205,220,57,0.6)','rgba(205,220,57,0.2)',ev=>showPopup(ev,'RadNet: '+s.location,`${s.analyte||'--'}: ${s.result||'--'} ${s.unit||''}`,'EPA'),3)}); // Space stations - (D.space?.stationPositions||[]).forEach(s=>{ + if(layerVisibility.space) (D.space?.stationPositions||[]).forEach(s=>{ const g=addPt(s.lat,s.lon,5,'rgba(255,255,255,0.9)','rgba(255,255,255,0.4)',ev=>showPopup(ev,s.name,'Orbital position estimate','Space Station'),1); if(g) g.append('text').attr('class','marker-label').attr('x',8).attr('y',3).attr('fill','rgba(255,255,255,0.7)').attr('font-size','8px').attr('font-family','var(--mono)').text(s.name.split('(')[0].trim()); }); // GDELT geo events - (D.gdelt?.geoPoints||[]).forEach(g=>{addPt(g.lat,g.lon,2.5,'rgba(100,149,237,0.5)','rgba(100,149,237,0.2)',ev=>showPopup(ev,'GDELT Event',g.name||'','GDELT · '+g.count+' reports'),3)}); + if(layerVisibility.gdelt) (D.gdelt?.geoPoints||[]).forEach(g=>{addPt(g.lat,g.lon,2.5,'rgba(100,149,237,0.5)','rgba(100,149,237,0.2)',ev=>showPopup(ev,'GDELT Event',g.name||'','GDELT · '+g.count+' reports'),3)}); // ACLED - (D.acled?.deadliestEvents||[]).filter(e=>e.lat&&e.lon).forEach(e=>{ + if(layerVisibility.acled) (D.acled?.deadliestEvents||[]).filter(e=>e.lat&&e.lon).forEach(e=>{ const[x,y]=proj([e.lon,e.lat]);if(!x||!y)return; const r=Math.max(4,Math.min(14,2+Math.log2(Math.max(e.fatalities,1))*1.5)); const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1) @@ -1262,7 +1292,7 @@ g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)'); }); // Flight corridors - if(flightsVisible){ + if(layerVisibility.air){ const airCoordsFlight=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}]; const hubs=[{lat:40.6,lon:-73.8},{lat:51.5,lon:-0.5},{lat:25.3,lon:55.4},{lat:1.4,lon:103.8},{lat:-33.9,lon:151.2},{lat:-23.4,lon:-46.5}]; const cG=flatG.append('g').attr('class','corridors-layer'); @@ -1576,7 +1606,7 @@ tl.to('#main',{opacity:1,duration:0.6},3.9); tl.call(()=>{ gsap.from('.g-panel,.topbar,.map-container',{opacity:0,y:20,scale:0.97,duration:0.5,stagger:0.06,ease:'power2.out'}); - setTimeout(()=>gsap.from('.layer-item,.site-row,.econ-row',{opacity:0,x:-12,duration:0.25,stagger:0.03,ease:'power1.out'}),500); + setTimeout(()=>gsap.from('.site-row,.econ-row',{opacity:0,x:-12,duration:0.25,stagger:0.03,ease:'power1.out'}),500); setTimeout(()=>gsap.from('.ic',{opacity:0,y:12,duration:0.25,stagger:0.03,ease:'power1.out'}),600); setTimeout(()=>gsap.from('.mc',{opacity:0,y:8,duration:0.25,stagger:0.04,ease:'power1.out'}),800); setTimeout(()=>gsap.from('.idea-card',{opacity:0,x:12,duration:0.3,stagger:0.06,ease:'power1.out'}),900); From c0b7b5a2878cf6d8a91ef6b9438a839714ca8b60 Mon Sep 17 00:00:00 2001 From: PetroczyP Date: Sat, 21 Mar 2026 18:54:42 +0100 Subject: [PATCH 2/3] feat: add world capitals layer with 245 cities on globe and flat map Adds 245 world capitals sourced from REST Countries API as a static reference layer. Capitals appear as gold dots on both 3D globe and D3 flat map, togglable via the Sensor Grid panel. Each popup shows flag emoji, city name, country, and ISO code. Co-Authored-By: Claude Opus 4.6 --- dashboard/public/jarvis.html | 265 ++++++++++++++++++++++++++++++++++- 1 file changed, 264 insertions(+), 1 deletion(-) diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index ef6dbaa9..2dacd263 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -91,6 +91,7 @@ .ldot.noaa{background:#ff9800;box-shadow:0 0 6px rgba(255,152,0,0.4)} .ldot.epa{background:#cddc39;box-shadow:0 0 6px rgba(205,220,57,0.4)} .ldot.gdelt{background:#6495ed;box-shadow:0 0 6px rgba(100,149,237,0.4)} +.ldot.capital{background:#ffd54f;box-shadow:0 0 6px rgba(255,213,79,0.4)} .layer-item{cursor:pointer;transition:opacity 0.2s,border-color 0.2s} .layer-item-off{opacity:0.35} .layer-item-off .ldot{box-shadow:none} @@ -422,6 +423,7 @@ {id:'noaa', label:'Weather Alerts', color:'#ff9800', dot:'noaa'}, {id:'epa', label:'EPA RadNet', color:'#cddc39', dot:'epa'}, {id:'gdelt', label:'GDELT Events', color:'#6495ed', dot:'gdelt'}, + {id:'capitals',label:'World Capitals',color:'#ffd54f', dot:'capital'}, ]; let layerVisibility = (()=>{ try { @@ -442,6 +444,253 @@ plotMarkers(); if (isFlat && flatG) { flatG.selectAll('*').remove(); drawFlatMap(); } } +const CAPITALS=[ +{name:"Kabul",country:"Afghanistan",iso2:"AF",flag:"\ud83c\udde6\ud83c\uddeb",lat:34.52,lng:69.18}, +{name:"Tirana",country:"Albania",iso2:"AL",flag:"\ud83c\udde6\ud83c\uddf1",lat:41.32,lng:19.82}, +{name:"Algiers",country:"Algeria",iso2:"DZ",flag:"\ud83c\udde9\ud83c\uddff",lat:36.75,lng:3.05}, +{name:"Pago Pago",country:"American Samoa",iso2:"AS",flag:"\ud83c\udde6\ud83c\uddf8",lat:-14.27,lng:-170.7}, +{name:"Andorra la Vella",country:"Andorra",iso2:"AD",flag:"\ud83c\udde6\ud83c\udde9",lat:42.5,lng:1.52}, +{name:"Luanda",country:"Angola",iso2:"AO",flag:"\ud83c\udde6\ud83c\uddf4",lat:-8.83,lng:13.22}, +{name:"The Valley",country:"Anguilla",iso2:"AI",flag:"\ud83c\udde6\ud83c\uddee",lat:18.22,lng:-63.05}, +{name:"Saint John's",country:"Antigua and Barbuda",iso2:"AG",flag:"\ud83c\udde6\ud83c\uddec",lat:17.12,lng:-61.85}, +{name:"Buenos Aires",country:"Argentina",iso2:"AR",flag:"\ud83c\udde6\ud83c\uddf7",lat:-34.58,lng:-58.67}, +{name:"Yerevan",country:"Armenia",iso2:"AM",flag:"\ud83c\udde6\ud83c\uddf2",lat:40.17,lng:44.5}, +{name:"Oranjestad",country:"Aruba",iso2:"AW",flag:"\ud83c\udde6\ud83c\uddfc",lat:12.52,lng:-70.03}, +{name:"Canberra",country:"Australia",iso2:"AU",flag:"\ud83c\udde6\ud83c\uddfa",lat:-35.27,lng:149.13}, +{name:"Vienna",country:"Austria",iso2:"AT",flag:"\ud83c\udde6\ud83c\uddf9",lat:48.2,lng:16.37}, +{name:"Baku",country:"Azerbaijan",iso2:"AZ",flag:"\ud83c\udde6\ud83c\uddff",lat:40.38,lng:49.87}, +{name:"Nassau",country:"Bahamas",iso2:"BS",flag:"\ud83c\udde7\ud83c\uddf8",lat:25.08,lng:-77.35}, +{name:"Manama",country:"Bahrain",iso2:"BH",flag:"\ud83c\udde7\ud83c\udded",lat:26.23,lng:50.57}, +{name:"Dhaka",country:"Bangladesh",iso2:"BD",flag:"\ud83c\udde7\ud83c\udde9",lat:23.72,lng:90.4}, +{name:"Bridgetown",country:"Barbados",iso2:"BB",flag:"\ud83c\udde7\ud83c\udde7",lat:13.1,lng:-59.62}, +{name:"Minsk",country:"Belarus",iso2:"BY",flag:"\ud83c\udde7\ud83c\uddfe",lat:53.9,lng:27.57}, +{name:"Brussels",country:"Belgium",iso2:"BE",flag:"\ud83c\udde7\ud83c\uddea",lat:50.83,lng:4.33}, +{name:"Belmopan",country:"Belize",iso2:"BZ",flag:"\ud83c\udde7\ud83c\uddff",lat:17.25,lng:-88.77}, +{name:"Porto-Novo",country:"Benin",iso2:"BJ",flag:"\ud83c\udde7\ud83c\uddef",lat:6.48,lng:2.62}, +{name:"Hamilton",country:"Bermuda",iso2:"BM",flag:"\ud83c\udde7\ud83c\uddf2",lat:32.28,lng:-64.78}, +{name:"Thimphu",country:"Bhutan",iso2:"BT",flag:"\ud83c\udde7\ud83c\uddf9",lat:27.47,lng:89.63}, +{name:"Sucre",country:"Bolivia",iso2:"BO",flag:"\ud83c\udde7\ud83c\uddf4",lat:-19.02,lng:-65.26}, +{name:"Sarajevo",country:"Bosnia and Herzegovina",iso2:"BA",flag:"\ud83c\udde7\ud83c\udde6",lat:43.87,lng:18.42}, +{name:"Gaborone",country:"Botswana",iso2:"BW",flag:"\ud83c\udde7\ud83c\uddfc",lat:-24.63,lng:25.9}, +{name:"Bras\u00edlia",country:"Brazil",iso2:"BR",flag:"\ud83c\udde7\ud83c\uddf7",lat:-15.79,lng:-47.88}, +{name:"Diego Garcia",country:"British Indian Ocean Territory",iso2:"IO",flag:"\ud83c\uddee\ud83c\uddf4",lat:-7.3,lng:72.4}, +{name:"Road Town",country:"British Virgin Islands",iso2:"VG",flag:"\ud83c\uddfb\ud83c\uddec",lat:18.42,lng:-64.62}, +{name:"Bandar Seri Begawan",country:"Brunei",iso2:"BN",flag:"\ud83c\udde7\ud83c\uddf3",lat:4.88,lng:114.93}, +{name:"Sofia",country:"Bulgaria",iso2:"BG",flag:"\ud83c\udde7\ud83c\uddec",lat:42.68,lng:23.32}, +{name:"Ouagadougou",country:"Burkina Faso",iso2:"BF",flag:"\ud83c\udde7\ud83c\uddeb",lat:12.37,lng:-1.52}, +{name:"Gitega",country:"Burundi",iso2:"BI",flag:"\ud83c\udde7\ud83c\uddee",lat:-3.43,lng:29.93}, +{name:"Phnom Penh",country:"Cambodia",iso2:"KH",flag:"\ud83c\uddf0\ud83c\udded",lat:11.55,lng:104.92}, +{name:"Yaound\u00e9",country:"Cameroon",iso2:"CM",flag:"\ud83c\udde8\ud83c\uddf2",lat:3.85,lng:11.5}, +{name:"Ottawa",country:"Canada",iso2:"CA",flag:"\ud83c\udde8\ud83c\udde6",lat:45.42,lng:-75.7}, +{name:"Praia",country:"Cape Verde",iso2:"CV",flag:"\ud83c\udde8\ud83c\uddfb",lat:14.92,lng:-23.52}, +{name:"Kralendijk",country:"Caribbean Netherlands",iso2:"BQ",flag:"\ud83c\udde7\ud83c\uddf6",lat:12.14,lng:-68.27}, +{name:"George Town",country:"Cayman Islands",iso2:"KY",flag:"\ud83c\uddf0\ud83c\uddfe",lat:19.3,lng:-81.38}, +{name:"Bangui",country:"Central African Republic",iso2:"CF",flag:"\ud83c\udde8\ud83c\uddeb",lat:4.37,lng:18.58}, +{name:"N'Djamena",country:"Chad",iso2:"TD",flag:"\ud83c\uddf9\ud83c\udde9",lat:12.1,lng:15.03}, +{name:"Santiago",country:"Chile",iso2:"CL",flag:"\ud83c\udde8\ud83c\uddf1",lat:-33.45,lng:-70.67}, +{name:"Beijing",country:"China",iso2:"CN",flag:"\ud83c\udde8\ud83c\uddf3",lat:39.92,lng:116.38}, +{name:"Flying Fish Cove",country:"Christmas Island",iso2:"CX",flag:"\ud83c\udde8\ud83c\uddfd",lat:-10.42,lng:105.68}, +{name:"West Island",country:"Cocos (Keeling) Islands",iso2:"CC",flag:"\ud83c\udde8\ud83c\udde8",lat:-12.17,lng:96.83}, +{name:"Bogot\u00e1",country:"Colombia",iso2:"CO",flag:"\ud83c\udde8\ud83c\uddf4",lat:4.71,lng:-74.07}, +{name:"Moroni",country:"Comoros",iso2:"KM",flag:"\ud83c\uddf0\ud83c\uddf2",lat:-11.7,lng:43.23}, +{name:"Avarua",country:"Cook Islands",iso2:"CK",flag:"\ud83c\udde8\ud83c\uddf0",lat:-21.2,lng:-159.77}, +{name:"San Jos\u00e9",country:"Costa Rica",iso2:"CR",flag:"\ud83c\udde8\ud83c\uddf7",lat:9.93,lng:-84.09}, +{name:"Zagreb",country:"Croatia",iso2:"HR",flag:"\ud83c\udded\ud83c\uddf7",lat:45.8,lng:16}, +{name:"Havana",country:"Cuba",iso2:"CU",flag:"\ud83c\udde8\ud83c\uddfa",lat:23.12,lng:-82.35}, +{name:"Willemstad",country:"Cura\u00e7ao",iso2:"CW",flag:"\ud83c\udde8\ud83c\uddfc",lat:12.1,lng:-68.92}, +{name:"Nicosia",country:"Cyprus",iso2:"CY",flag:"\ud83c\udde8\ud83c\uddfe",lat:35.17,lng:33.37}, +{name:"Prague",country:"Czechia",iso2:"CZ",flag:"\ud83c\udde8\ud83c\uddff",lat:50.08,lng:14.47}, +{name:"Kinshasa",country:"DR Congo",iso2:"CD",flag:"\ud83c\udde8\ud83c\udde9",lat:-4.32,lng:15.3}, +{name:"Copenhagen",country:"Denmark",iso2:"DK",flag:"\ud83c\udde9\ud83c\uddf0",lat:55.67,lng:12.58}, +{name:"Djibouti",country:"Djibouti",iso2:"DJ",flag:"\ud83c\udde9\ud83c\uddef",lat:11.58,lng:43.15}, +{name:"Roseau",country:"Dominica",iso2:"DM",flag:"\ud83c\udde9\ud83c\uddf2",lat:15.3,lng:-61.4}, +{name:"Santo Domingo",country:"Dominican Republic",iso2:"DO",flag:"\ud83c\udde9\ud83c\uddf4",lat:18.47,lng:-69.9}, +{name:"Quito",country:"Ecuador",iso2:"EC",flag:"\ud83c\uddea\ud83c\udde8",lat:-0.22,lng:-78.5}, +{name:"Cairo",country:"Egypt",iso2:"EG",flag:"\ud83c\uddea\ud83c\uddec",lat:30.05,lng:31.25}, +{name:"San Salvador",country:"El Salvador",iso2:"SV",flag:"\ud83c\uddf8\ud83c\uddfb",lat:13.7,lng:-89.2}, +{name:"Ciudad de la Paz",country:"Equatorial Guinea",iso2:"GQ",flag:"\ud83c\uddec\ud83c\uddf6",lat:1.35,lng:10.49}, +{name:"Asmara",country:"Eritrea",iso2:"ER",flag:"\ud83c\uddea\ud83c\uddf7",lat:15.33,lng:38.93}, +{name:"Tallinn",country:"Estonia",iso2:"EE",flag:"\ud83c\uddea\ud83c\uddea",lat:59.43,lng:24.72}, +{name:"Mbabane",country:"Eswatini",iso2:"SZ",flag:"\ud83c\uddf8\ud83c\uddff",lat:-26.32,lng:31.13}, +{name:"Addis Ababa",country:"Ethiopia",iso2:"ET",flag:"\ud83c\uddea\ud83c\uddf9",lat:9.03,lng:38.7}, +{name:"Stanley",country:"Falkland Islands",iso2:"FK",flag:"\ud83c\uddeb\ud83c\uddf0",lat:-51.7,lng:-57.85}, +{name:"T\u00f3rshavn",country:"Faroe Islands",iso2:"FO",flag:"\ud83c\uddeb\ud83c\uddf4",lat:62.01,lng:-6.77}, +{name:"Suva",country:"Fiji",iso2:"FJ",flag:"\ud83c\uddeb\ud83c\uddef",lat:-18.13,lng:178.42}, +{name:"Helsinki",country:"Finland",iso2:"FI",flag:"\ud83c\uddeb\ud83c\uddee",lat:60.17,lng:24.93}, +{name:"Paris",country:"France",iso2:"FR",flag:"\ud83c\uddeb\ud83c\uddf7",lat:48.87,lng:2.33}, +{name:"Cayenne",country:"French Guiana",iso2:"GF",flag:"\ud83c\uddec\ud83c\uddeb",lat:4.94,lng:-52.33}, +{name:"Papeet\u0113",country:"French Polynesia",iso2:"PF",flag:"\ud83c\uddf5\ud83c\uddeb",lat:-17.53,lng:-149.56}, +{name:"Port-aux-Fran\u00e7ais",country:"French Southern and Antarctic Lands",iso2:"TF",flag:"\ud83c\uddf9\ud83c\uddeb",lat:48.81,lng:-1.4}, +{name:"Libreville",country:"Gabon",iso2:"GA",flag:"\ud83c\uddec\ud83c\udde6",lat:0.38,lng:9.45}, +{name:"Banjul",country:"Gambia",iso2:"GM",flag:"\ud83c\uddec\ud83c\uddf2",lat:13.45,lng:-16.57}, +{name:"Tbilisi",country:"Georgia",iso2:"GE",flag:"\ud83c\uddec\ud83c\uddea",lat:41.68,lng:44.83}, +{name:"Berlin",country:"Germany",iso2:"DE",flag:"\ud83c\udde9\ud83c\uddea",lat:52.52,lng:13.4}, +{name:"Accra",country:"Ghana",iso2:"GH",flag:"\ud83c\uddec\ud83c\udded",lat:5.55,lng:-0.22}, +{name:"Gibraltar",country:"Gibraltar",iso2:"GI",flag:"\ud83c\uddec\ud83c\uddee",lat:36.13,lng:-5.35}, +{name:"Athens",country:"Greece",iso2:"GR",flag:"\ud83c\uddec\ud83c\uddf7",lat:37.98,lng:23.73}, +{name:"Nuuk",country:"Greenland",iso2:"GL",flag:"\ud83c\uddec\ud83c\uddf1",lat:64.18,lng:-51.75}, +{name:"St. George's",country:"Grenada",iso2:"GD",flag:"\ud83c\uddec\ud83c\udde9",lat:32.38,lng:-64.68}, +{name:"Basse-Terre",country:"Guadeloupe",iso2:"GP",flag:"\ud83c\uddec\ud83c\uddf5",lat:16.03,lng:-61.73}, +{name:"Hag\u00e5t\u00f1a",country:"Guam",iso2:"GU",flag:"\ud83c\uddec\ud83c\uddfa",lat:13.48,lng:144.75}, +{name:"Guatemala City",country:"Guatemala",iso2:"GT",flag:"\ud83c\uddec\ud83c\uddf9",lat:14.62,lng:-90.52}, +{name:"St. Peter Port",country:"Guernsey",iso2:"GG",flag:"\ud83c\uddec\ud83c\uddec",lat:49.45,lng:-2.54}, +{name:"Conakry",country:"Guinea",iso2:"GN",flag:"\ud83c\uddec\ud83c\uddf3",lat:9.5,lng:-13.7}, +{name:"Bissau",country:"Guinea-Bissau",iso2:"GW",flag:"\ud83c\uddec\ud83c\uddfc",lat:11.85,lng:-15.58}, +{name:"Georgetown",country:"Guyana",iso2:"GY",flag:"\ud83c\uddec\ud83c\uddfe",lat:6.8,lng:-58.15}, +{name:"Port-au-Prince",country:"Haiti",iso2:"HT",flag:"\ud83c\udded\ud83c\uddf9",lat:18.53,lng:-72.33}, +{name:"Tegucigalpa",country:"Honduras",iso2:"HN",flag:"\ud83c\udded\ud83c\uddf3",lat:14.1,lng:-87.22}, +{name:"City of Victoria",country:"Hong Kong",iso2:"HK",flag:"\ud83c\udded\ud83c\uddf0",lat:22.267,lng:114.188}, +{name:"Budapest",country:"Hungary",iso2:"HU",flag:"\ud83c\udded\ud83c\uddfa",lat:47.5,lng:19.08}, +{name:"Reykjavik",country:"Iceland",iso2:"IS",flag:"\ud83c\uddee\ud83c\uddf8",lat:64.15,lng:-21.95}, +{name:"New Delhi",country:"India",iso2:"IN",flag:"\ud83c\uddee\ud83c\uddf3",lat:28.6,lng:77.2}, +{name:"Jakarta",country:"Indonesia",iso2:"ID",flag:"\ud83c\uddee\ud83c\udde9",lat:-6.17,lng:106.82}, +{name:"Tehran",country:"Iran",iso2:"IR",flag:"\ud83c\uddee\ud83c\uddf7",lat:35.7,lng:51.42}, +{name:"Baghdad",country:"Iraq",iso2:"IQ",flag:"\ud83c\uddee\ud83c\uddf6",lat:33.33,lng:44.4}, +{name:"Dublin",country:"Ireland",iso2:"IE",flag:"\ud83c\uddee\ud83c\uddea",lat:53.32,lng:-6.23}, +{name:"Douglas",country:"Isle of Man",iso2:"IM",flag:"\ud83c\uddee\ud83c\uddf2",lat:54.15,lng:-4.48}, +{name:"Jerusalem",country:"Israel",iso2:"IL",flag:"\ud83c\uddee\ud83c\uddf1",lat:31.77,lng:35.23}, +{name:"Rome",country:"Italy",iso2:"IT",flag:"\ud83c\uddee\ud83c\uddf9",lat:41.9,lng:12.48}, +{name:"Yamoussoukro",country:"Ivory Coast",iso2:"CI",flag:"\ud83c\udde8\ud83c\uddee",lat:6.82,lng:-5.27}, +{name:"Kingston",country:"Jamaica",iso2:"JM",flag:"\ud83c\uddef\ud83c\uddf2",lat:17.997,lng:-76.7936}, +{name:"Tokyo",country:"Japan",iso2:"JP",flag:"\ud83c\uddef\ud83c\uddf5",lat:35.68,lng:139.75}, +{name:"Saint Helier",country:"Jersey",iso2:"JE",flag:"\ud83c\uddef\ud83c\uddea",lat:49.18,lng:-2.1}, +{name:"Amman",country:"Jordan",iso2:"JO",flag:"\ud83c\uddef\ud83c\uddf4",lat:31.95,lng:35.93}, +{name:"Astana",country:"Kazakhstan",iso2:"KZ",flag:"\ud83c\uddf0\ud83c\uddff",lat:51.16,lng:71.45}, +{name:"Nairobi",country:"Kenya",iso2:"KE",flag:"\ud83c\uddf0\ud83c\uddea",lat:-1.28,lng:36.82}, +{name:"South Tarawa",country:"Kiribati",iso2:"KI",flag:"\ud83c\uddf0\ud83c\uddee",lat:1.33,lng:172.98}, +{name:"Pristina",country:"Kosovo",iso2:"XK",flag:"\ud83c\uddfd\ud83c\uddf0",lat:42.67,lng:21.17}, +{name:"Kuwait City",country:"Kuwait",iso2:"KW",flag:"\ud83c\uddf0\ud83c\uddfc",lat:29.37,lng:47.97}, +{name:"Bishkek",country:"Kyrgyzstan",iso2:"KG",flag:"\ud83c\uddf0\ud83c\uddec",lat:42.87,lng:74.6}, +{name:"Vientiane",country:"Laos",iso2:"LA",flag:"\ud83c\uddf1\ud83c\udde6",lat:17.97,lng:102.6}, +{name:"Riga",country:"Latvia",iso2:"LV",flag:"\ud83c\uddf1\ud83c\uddfb",lat:56.95,lng:24.1}, +{name:"Beirut",country:"Lebanon",iso2:"LB",flag:"\ud83c\uddf1\ud83c\udde7",lat:33.87,lng:35.5}, +{name:"Maseru",country:"Lesotho",iso2:"LS",flag:"\ud83c\uddf1\ud83c\uddf8",lat:-29.32,lng:27.48}, +{name:"Monrovia",country:"Liberia",iso2:"LR",flag:"\ud83c\uddf1\ud83c\uddf7",lat:6.3,lng:-10.8}, +{name:"Tripoli",country:"Libya",iso2:"LY",flag:"\ud83c\uddf1\ud83c\uddfe",lat:32.88,lng:13.17}, +{name:"Vaduz",country:"Liechtenstein",iso2:"LI",flag:"\ud83c\uddf1\ud83c\uddee",lat:47.13,lng:9.52}, +{name:"Vilnius",country:"Lithuania",iso2:"LT",flag:"\ud83c\uddf1\ud83c\uddf9",lat:54.68,lng:25.32}, +{name:"Luxembourg",country:"Luxembourg",iso2:"LU",flag:"\ud83c\uddf1\ud83c\uddfa",lat:49.6,lng:6.12}, +{name:"Antananarivo",country:"Madagascar",iso2:"MG",flag:"\ud83c\uddf2\ud83c\uddec",lat:-18.92,lng:47.52}, +{name:"Lilongwe",country:"Malawi",iso2:"MW",flag:"\ud83c\uddf2\ud83c\uddfc",lat:-13.97,lng:33.78}, +{name:"Kuala Lumpur",country:"Malaysia",iso2:"MY",flag:"\ud83c\uddf2\ud83c\uddfe",lat:3.17,lng:101.7}, +{name:"Mal\u00e9",country:"Maldives",iso2:"MV",flag:"\ud83c\uddf2\ud83c\uddfb",lat:4.17,lng:73.51}, +{name:"Bamako",country:"Mali",iso2:"ML",flag:"\ud83c\uddf2\ud83c\uddf1",lat:12.65,lng:-8}, +{name:"Valletta",country:"Malta",iso2:"MT",flag:"\ud83c\uddf2\ud83c\uddf9",lat:35.88,lng:14.5}, +{name:"Majuro",country:"Marshall Islands",iso2:"MH",flag:"\ud83c\uddf2\ud83c\udded",lat:7.1,lng:171.38}, +{name:"Fort-de-France",country:"Martinique",iso2:"MQ",flag:"\ud83c\uddf2\ud83c\uddf6",lat:14.6,lng:-61.08}, +{name:"Nouakchott",country:"Mauritania",iso2:"MR",flag:"\ud83c\uddf2\ud83c\uddf7",lat:18.07,lng:-15.97}, +{name:"Port Louis",country:"Mauritius",iso2:"MU",flag:"\ud83c\uddf2\ud83c\uddfa",lat:-20.15,lng:57.48}, +{name:"Mamoudzou",country:"Mayotte",iso2:"YT",flag:"\ud83c\uddfe\ud83c\uddf9",lat:-12.78,lng:45.22}, +{name:"Mexico City",country:"Mexico",iso2:"MX",flag:"\ud83c\uddf2\ud83c\uddfd",lat:19.43,lng:-99.13}, +{name:"Palikir",country:"Micronesia",iso2:"FM",flag:"\ud83c\uddeb\ud83c\uddf2",lat:6.92,lng:158.15}, +{name:"Chi\u0219in\u0103u",country:"Moldova",iso2:"MD",flag:"\ud83c\uddf2\ud83c\udde9",lat:47.01,lng:28.9}, +{name:"Monaco",country:"Monaco",iso2:"MC",flag:"\ud83c\uddf2\ud83c\udde8",lat:43.73,lng:7.42}, +{name:"Ulan Bator",country:"Mongolia",iso2:"MN",flag:"\ud83c\uddf2\ud83c\uddf3",lat:47.92,lng:106.91}, +{name:"Podgorica",country:"Montenegro",iso2:"ME",flag:"\ud83c\uddf2\ud83c\uddea",lat:42.43,lng:19.27}, +{name:"Plymouth",country:"Montserrat",iso2:"MS",flag:"\ud83c\uddf2\ud83c\uddf8",lat:16.7,lng:-62.22}, +{name:"Rabat",country:"Morocco",iso2:"MA",flag:"\ud83c\uddf2\ud83c\udde6",lat:34.02,lng:-6.82}, +{name:"Maputo",country:"Mozambique",iso2:"MZ",flag:"\ud83c\uddf2\ud83c\uddff",lat:-25.95,lng:32.58}, +{name:"Naypyidaw",country:"Myanmar",iso2:"MM",flag:"\ud83c\uddf2\ud83c\uddf2",lat:19.76,lng:96.07}, +{name:"Windhoek",country:"Namibia",iso2:"NA",flag:"\ud83c\uddf3\ud83c\udde6",lat:-22.57,lng:17.08}, +{name:"Yaren",country:"Nauru",iso2:"NR",flag:"\ud83c\uddf3\ud83c\uddf7",lat:-0.55,lng:166.92}, +{name:"Kathmandu",country:"Nepal",iso2:"NP",flag:"\ud83c\uddf3\ud83c\uddf5",lat:27.72,lng:85.32}, +{name:"Amsterdam",country:"Netherlands",iso2:"NL",flag:"\ud83c\uddf3\ud83c\uddf1",lat:52.35,lng:4.92}, +{name:"Noum\u00e9a",country:"New Caledonia",iso2:"NC",flag:"\ud83c\uddf3\ud83c\udde8",lat:-22.27,lng:166.45}, +{name:"Wellington",country:"New Zealand",iso2:"NZ",flag:"\ud83c\uddf3\ud83c\uddff",lat:-41.3,lng:174.78}, +{name:"Managua",country:"Nicaragua",iso2:"NI",flag:"\ud83c\uddf3\ud83c\uddee",lat:12.13,lng:-86.25}, +{name:"Niamey",country:"Niger",iso2:"NE",flag:"\ud83c\uddf3\ud83c\uddea",lat:13.52,lng:2.12}, +{name:"Abuja",country:"Nigeria",iso2:"NG",flag:"\ud83c\uddf3\ud83c\uddec",lat:9.08,lng:7.53}, +{name:"Alofi",country:"Niue",iso2:"NU",flag:"\ud83c\uddf3\ud83c\uddfa",lat:-19.02,lng:-169.92}, +{name:"Kingston",country:"Norfolk Island",iso2:"NF",flag:"\ud83c\uddf3\ud83c\uddeb",lat:-29.05,lng:167.97}, +{name:"Pyongyang",country:"North Korea",iso2:"KP",flag:"\ud83c\uddf0\ud83c\uddf5",lat:39.02,lng:125.75}, +{name:"Skopje",country:"North Macedonia",iso2:"MK",flag:"\ud83c\uddf2\ud83c\uddf0",lat:42,lng:21.43}, +{name:"Saipan",country:"Northern Mariana Islands",iso2:"MP",flag:"\ud83c\uddf2\ud83c\uddf5",lat:15.2,lng:145.75}, +{name:"Oslo",country:"Norway",iso2:"NO",flag:"\ud83c\uddf3\ud83c\uddf4",lat:59.92,lng:10.75}, +{name:"Muscat",country:"Oman",iso2:"OM",flag:"\ud83c\uddf4\ud83c\uddf2",lat:23.62,lng:58.58}, +{name:"Islamabad",country:"Pakistan",iso2:"PK",flag:"\ud83c\uddf5\ud83c\uddf0",lat:33.68,lng:73.05}, +{name:"Ngerulmud",country:"Palau",iso2:"PW",flag:"\ud83c\uddf5\ud83c\uddfc",lat:7.5,lng:134.62}, +{name:"Ramallah",country:"Palestine",iso2:"PS",flag:"\ud83c\uddf5\ud83c\uddf8",lat:31.9,lng:35.2}, +{name:"Panama City",country:"Panama",iso2:"PA",flag:"\ud83c\uddf5\ud83c\udde6",lat:8.97,lng:-79.53}, +{name:"Port Moresby",country:"Papua New Guinea",iso2:"PG",flag:"\ud83c\uddf5\ud83c\uddec",lat:-9.45,lng:147.18}, +{name:"Asunci\u00f3n",country:"Paraguay",iso2:"PY",flag:"\ud83c\uddf5\ud83c\uddfe",lat:-25.28,lng:-57.57}, +{name:"Lima",country:"Peru",iso2:"PE",flag:"\ud83c\uddf5\ud83c\uddea",lat:-12.05,lng:-77.05}, +{name:"Manila",country:"Philippines",iso2:"PH",flag:"\ud83c\uddf5\ud83c\udded",lat:14.6,lng:120.97}, +{name:"Adamstown",country:"Pitcairn Islands",iso2:"PN",flag:"\ud83c\uddf5\ud83c\uddf3",lat:-25.07,lng:-130.08}, +{name:"Warsaw",country:"Poland",iso2:"PL",flag:"\ud83c\uddf5\ud83c\uddf1",lat:52.25,lng:21}, +{name:"Lisbon",country:"Portugal",iso2:"PT",flag:"\ud83c\uddf5\ud83c\uddf9",lat:38.72,lng:-9.13}, +{name:"San Juan",country:"Puerto Rico",iso2:"PR",flag:"\ud83c\uddf5\ud83c\uddf7",lat:18.47,lng:-66.12}, +{name:"Doha",country:"Qatar",iso2:"QA",flag:"\ud83c\uddf6\ud83c\udde6",lat:25.28,lng:51.53}, +{name:"Brazzaville",country:"Republic of the Congo",iso2:"CG",flag:"\ud83c\udde8\ud83c\uddec",lat:-4.25,lng:15.28}, +{name:"Bucharest",country:"Romania",iso2:"RO",flag:"\ud83c\uddf7\ud83c\uddf4",lat:44.43,lng:26.1}, +{name:"Moscow",country:"Russia",iso2:"RU",flag:"\ud83c\uddf7\ud83c\uddfa",lat:55.75,lng:37.6}, +{name:"Kigali",country:"Rwanda",iso2:"RW",flag:"\ud83c\uddf7\ud83c\uddfc",lat:-1.95,lng:30.05}, +{name:"Saint-Denis",country:"R\u00e9union",iso2:"RE",flag:"\ud83c\uddf7\ud83c\uddea",lat:-20.88,lng:55.45}, +{name:"Gustavia",country:"Saint Barth\u00e9lemy",iso2:"BL",flag:"\ud83c\udde7\ud83c\uddf1",lat:17.88,lng:-62.85}, +{name:"Jamestown",country:"Saint Helena, Ascension and Tristan da Cunha",iso2:"SH",flag:"\ud83c\uddf8\ud83c\udded",lat:-15.93,lng:-5.72}, +{name:"Basseterre",country:"Saint Kitts and Nevis",iso2:"KN",flag:"\ud83c\uddf0\ud83c\uddf3",lat:17.3,lng:-62.72}, +{name:"Castries",country:"Saint Lucia",iso2:"LC",flag:"\ud83c\uddf1\ud83c\udde8",lat:14,lng:-61}, +{name:"Marigot",country:"Saint Martin",iso2:"MF",flag:"\ud83c\uddf2\ud83c\uddeb",lat:18.07,lng:-63.08}, +{name:"Saint-Pierre",country:"Saint Pierre and Miquelon",iso2:"PM",flag:"\ud83c\uddf5\ud83c\uddf2",lat:46.77,lng:-56.18}, +{name:"Kingstown",country:"Saint Vincent and the Grenadines",iso2:"VC",flag:"\ud83c\uddfb\ud83c\udde8",lat:13.13,lng:-61.22}, +{name:"Apia",country:"Samoa",iso2:"WS",flag:"\ud83c\uddfc\ud83c\uddf8",lat:-13.82,lng:-171.77}, +{name:"City of San Marino",country:"San Marino",iso2:"SM",flag:"\ud83c\uddf8\ud83c\uddf2",lat:43.94,lng:12.45}, +{name:"Riyadh",country:"Saudi Arabia",iso2:"SA",flag:"\ud83c\uddf8\ud83c\udde6",lat:24.65,lng:46.7}, +{name:"Dakar",country:"Senegal",iso2:"SN",flag:"\ud83c\uddf8\ud83c\uddf3",lat:14.73,lng:-17.63}, +{name:"Belgrade",country:"Serbia",iso2:"RS",flag:"\ud83c\uddf7\ud83c\uddf8",lat:44.83,lng:20.5}, +{name:"Victoria",country:"Seychelles",iso2:"SC",flag:"\ud83c\uddf8\ud83c\udde8",lat:-4.62,lng:55.45}, +{name:"Freetown",country:"Sierra Leone",iso2:"SL",flag:"\ud83c\uddf8\ud83c\uddf1",lat:8.48,lng:-13.23}, +{name:"Singapore",country:"Singapore",iso2:"SG",flag:"\ud83c\uddf8\ud83c\uddec",lat:1.28,lng:103.85}, +{name:"Philipsburg",country:"Sint Maarten",iso2:"SX",flag:"\ud83c\uddf8\ud83c\uddfd",lat:18.02,lng:-63.03}, +{name:"Bratislava",country:"Slovakia",iso2:"SK",flag:"\ud83c\uddf8\ud83c\uddf0",lat:48.15,lng:17.12}, +{name:"Ljubljana",country:"Slovenia",iso2:"SI",flag:"\ud83c\uddf8\ud83c\uddee",lat:46.05,lng:14.52}, +{name:"Honiara",country:"Solomon Islands",iso2:"SB",flag:"\ud83c\uddf8\ud83c\udde7",lat:-9.43,lng:159.95}, +{name:"Mogadishu",country:"Somalia",iso2:"SO",flag:"\ud83c\uddf8\ud83c\uddf4",lat:2.07,lng:45.33}, +{name:"Pretoria",country:"South Africa",iso2:"ZA",flag:"\ud83c\uddff\ud83c\udde6",lat:-25.7,lng:28.22}, +{name:"King Edward Point",country:"South Georgia",iso2:"GS",flag:"\ud83c\uddec\ud83c\uddf8",lat:-54.28,lng:-36.5}, +{name:"Seoul",country:"South Korea",iso2:"KR",flag:"\ud83c\uddf0\ud83c\uddf7",lat:37.55,lng:126.98}, +{name:"Juba",country:"South Sudan",iso2:"SS",flag:"\ud83c\uddf8\ud83c\uddf8",lat:4.85,lng:31.62}, +{name:"Madrid",country:"Spain",iso2:"ES",flag:"\ud83c\uddea\ud83c\uddf8",lat:40.4,lng:-3.68}, +{name:"Sri Jayawardenepura Kotte",country:"Sri Lanka",iso2:"LK",flag:"\ud83c\uddf1\ud83c\uddf0",lat:6.89,lng:79.9}, +{name:"Khartoum",country:"Sudan",iso2:"SD",flag:"\ud83c\uddf8\ud83c\udde9",lat:15.6,lng:32.53}, +{name:"Paramaribo",country:"Suriname",iso2:"SR",flag:"\ud83c\uddf8\ud83c\uddf7",lat:5.83,lng:-55.17}, +{name:"Longyearbyen",country:"Svalbard and Jan Mayen",iso2:"SJ",flag:"\ud83c\uddf8\ud83c\uddef",lat:78.22,lng:15.63}, +{name:"Stockholm",country:"Sweden",iso2:"SE",flag:"\ud83c\uddf8\ud83c\uddea",lat:59.33,lng:18.05}, +{name:"Bern",country:"Switzerland",iso2:"CH",flag:"\ud83c\udde8\ud83c\udded",lat:46.92,lng:7.47}, +{name:"Damascus",country:"Syria",iso2:"SY",flag:"\ud83c\uddf8\ud83c\uddfe",lat:33.5,lng:36.3}, +{name:"S\u00e3o Tom\u00e9",country:"S\u00e3o Tom\u00e9 and Pr\u00edncipe",iso2:"ST",flag:"\ud83c\uddf8\ud83c\uddf9",lat:0.34,lng:6.73}, +{name:"Taipei",country:"Taiwan",iso2:"TW",flag:"\ud83c\uddf9\ud83c\uddfc",lat:25.03,lng:121.52}, +{name:"Dushanbe",country:"Tajikistan",iso2:"TJ",flag:"\ud83c\uddf9\ud83c\uddef",lat:38.55,lng:68.77}, +{name:"Dodoma",country:"Tanzania",iso2:"TZ",flag:"\ud83c\uddf9\ud83c\uddff",lat:-6.16,lng:35.75}, +{name:"Bangkok",country:"Thailand",iso2:"TH",flag:"\ud83c\uddf9\ud83c\udded",lat:13.75,lng:100.52}, +{name:"Dili",country:"Timor-Leste",iso2:"TL",flag:"\ud83c\uddf9\ud83c\uddf1",lat:-8.58,lng:125.6}, +{name:"Lom\u00e9",country:"Togo",iso2:"TG",flag:"\ud83c\uddf9\ud83c\uddec",lat:6.14,lng:1.21}, +{name:"Fakaofo",country:"Tokelau",iso2:"TK",flag:"\ud83c\uddf9\ud83c\uddf0",lat:-9.38,lng:-171.22}, +{name:"Nuku\u02bbalofa",country:"Tonga",iso2:"TO",flag:"\ud83c\uddf9\ud83c\uddf4",lat:-21.13,lng:-175.2}, +{name:"Port of Spain",country:"Trinidad and Tobago",iso2:"TT",flag:"\ud83c\uddf9\ud83c\uddf9",lat:10.65,lng:-61.52}, +{name:"Tunis",country:"Tunisia",iso2:"TN",flag:"\ud83c\uddf9\ud83c\uddf3",lat:36.8,lng:10.18}, +{name:"Ankara",country:"Turkey",iso2:"TR",flag:"\ud83c\uddf9\ud83c\uddf7",lat:39.93,lng:32.87}, +{name:"Ashgabat",country:"Turkmenistan",iso2:"TM",flag:"\ud83c\uddf9\ud83c\uddf2",lat:37.95,lng:58.38}, +{name:"Cockburn Town",country:"Turks and Caicos Islands",iso2:"TC",flag:"\ud83c\uddf9\ud83c\udde8",lat:21.46,lng:-71.14}, +{name:"Funafuti",country:"Tuvalu",iso2:"TV",flag:"\ud83c\uddf9\ud83c\uddfb",lat:-8.52,lng:179.22}, +{name:"Kampala",country:"Uganda",iso2:"UG",flag:"\ud83c\uddfa\ud83c\uddec",lat:0.32,lng:32.55}, +{name:"Kyiv",country:"Ukraine",iso2:"UA",flag:"\ud83c\uddfa\ud83c\udde6",lat:50.43,lng:30.52}, +{name:"Abu Dhabi",country:"United Arab Emirates",iso2:"AE",flag:"\ud83c\udde6\ud83c\uddea",lat:24.47,lng:54.37}, +{name:"London",country:"United Kingdom",iso2:"GB",flag:"\ud83c\uddec\ud83c\udde7",lat:51.5,lng:-0.08}, +{name:"Washington, D.C.",country:"United States",iso2:"US",flag:"\ud83c\uddfa\ud83c\uddf8",lat:38.89,lng:-77.05}, +{name:"Charlotte Amalie",country:"United States Virgin Islands",iso2:"VI",flag:"\ud83c\uddfb\ud83c\uddee",lat:18.35,lng:-64.93}, +{name:"Montevideo",country:"Uruguay",iso2:"UY",flag:"\ud83c\uddfa\ud83c\uddfe",lat:-34.85,lng:-56.17}, +{name:"Tashkent",country:"Uzbekistan",iso2:"UZ",flag:"\ud83c\uddfa\ud83c\uddff",lat:41.32,lng:69.25}, +{name:"Port Vila",country:"Vanuatu",iso2:"VU",flag:"\ud83c\uddfb\ud83c\uddfa",lat:-17.73,lng:168.32}, +{name:"Vatican City",country:"Vatican City",iso2:"VA",flag:"\ud83c\uddfb\ud83c\udde6",lat:41.9,lng:12.45}, +{name:"Caracas",country:"Venezuela",iso2:"VE",flag:"\ud83c\uddfb\ud83c\uddea",lat:10.48,lng:-66.87}, +{name:"Hanoi",country:"Vietnam",iso2:"VN",flag:"\ud83c\uddfb\ud83c\uddf3",lat:21.03,lng:105.85}, +{name:"Mata-Utu",country:"Wallis and Futuna",iso2:"WF",flag:"\ud83c\uddfc\ud83c\uddeb",lat:-13.95,lng:-171.93}, +{name:"El Aai\u00fan",country:"Western Sahara",iso2:"EH",flag:"\ud83c\uddea\ud83c\udded",lat:-13.28,lng:27.14}, +{name:"Sana'a",country:"Yemen",iso2:"YE",flag:"\ud83c\uddfe\ud83c\uddea",lat:15.37,lng:44.19}, +{name:"Lusaka",country:"Zambia",iso2:"ZM",flag:"\ud83c\uddff\ud83c\uddf2",lat:-15.42,lng:28.28}, +{name:"Harare",country:"Zimbabwe",iso2:"ZW",flag:"\ud83c\uddff\ud83c\uddfc",lat:-17.82,lng:31.03}, +{name:"Mariehamn",country:"\u00c5land Islands",iso2:"AX",flag:"\ud83c\udde6\ud83c\uddfd",lat:60.12,lng:19.9} +]; let currentRegion = 'world'; let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH; const signalGuideItems = [ @@ -671,6 +920,7 @@ noaa: {count:D.noaa?.totalAlerts||0, sub:t('layers.severeWeather','severe weather')}, epa: {count:D.epa?.totalReadings||0, sub:t('layers.radiationNet','radiation net')}, gdelt: {count:(D.gdelt?.geoPoints||[]).length, sub:t('layers.globalNewsGeo','global news geo')}, + capitals: {count:CAPITALS.length, sub:t('layers.worldCapitals','world capitals')}, }; const allNormal=D.nuke.every(s=>!s.anom); const nukeHtml=D.nuke.map(s=>`
${s.site}${s.n>0?(s.cpm?.toFixed(1)||'--')+' CPM':'No data'}
`).join(''); @@ -734,7 +984,7 @@ } function renderMapLegend(){ - const legendMap = {air:'Air Traffic',thermal:'Thermal/Fire',acled:'Conflict',sdr:'SDR Receiver',nuke:'Nuclear Site',maritime:'Chokepoint',osint:'OSINT Event',who:'Health Alert',news:'World News',noaa:'Weather Alert',epa:'EPA RadNet',space:'Space Station',gdelt:'GDELT Event'}; + const legendMap = {air:'Air Traffic',thermal:'Thermal/Fire',acled:'Conflict',sdr:'SDR Receiver',nuke:'Nuclear Site',maritime:'Chokepoint',osint:'OSINT Event',who:'Health Alert',news:'World News',noaa:'Weather Alert',epa:'EPA RadNet',space:'Space Station',gdelt:'GDELT Event',capitals:'World Capital'}; document.getElementById('mapLegend').innerHTML= LAYERS.map(l=>{ const vis = layerVisibility[l.id]; @@ -1031,6 +1281,17 @@ }); }); + // === World Capitals (gold) === + if(layerVisibility.capitals) CAPITALS.forEach(c=>{ + points.push({ + lat:c.lat, lng:c.lng, size:0.12, alt:0.005, + color:'rgba(255,213,79,0.7)', type:'capital', priority:3, + popHead:c.flag+' '+c.name, + popMeta:c.country+' ('+c.iso2+')', + popText:'Capital city' + }); + }); + // Set points on globe globe.pointsData(points); globe.labelsData(labels); @@ -1282,6 +1543,8 @@ }); // GDELT geo events if(layerVisibility.gdelt) (D.gdelt?.geoPoints||[]).forEach(g=>{addPt(g.lat,g.lon,2.5,'rgba(100,149,237,0.5)','rgba(100,149,237,0.2)',ev=>showPopup(ev,'GDELT Event',g.name||'','GDELT · '+g.count+' reports'),3)}); + // Capitals + if(layerVisibility.capitals) CAPITALS.forEach(c=>{addPt(c.lat,c.lng,2,'rgba(255,213,79,0.6)','rgba(255,213,79,0.25)',ev=>showPopup(ev,c.flag+' '+c.name,'Capital city',c.country+' ('+c.iso2+')'),3)}); // ACLED if(layerVisibility.acled) (D.acled?.deadliestEvents||[]).filter(e=>e.lat&&e.lon).forEach(e=>{ const[x,y]=proj([e.lon,e.lat]);if(!x||!y)return; From dd14c8933e3dbd85796ddb61a167458487767ad1 Mon Sep 17 00:00:00 2001 From: PetroczyP Date: Mon, 23 Mar 2026 01:23:30 +0100 Subject: [PATCH 3/3] fix: correct 3 capital coordinates, harden localStorage, clean up CSS - Fix wrong coordinates: El Aaiun (lat/lng swapped), Port-aux-Francais (placed in France instead of Kerguelen), St. George's (Bermuda coords instead of Grenada) - Wrap all localStorage access in try-catch with console.warn logging - Add layer ID validation in toggleLayer to prevent prototype pollution - Guard plotMarkers with isFlat check to skip wasted 3D recompute - Merge duplicate .layer-item CSS rules into single declaration - Simplify layerVisibility init (remove IIFE, single default path) Co-Authored-By: Claude Opus 4.6 --- dashboard/public/jarvis.html | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index 2dacd263..d3a368c5 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -75,7 +75,7 @@ .badge{font-family:var(--mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);color:var(--accent)} /* LEFT RAIL */ -.layer-item{display:flex;align-items:center;justify-content:space-between;padding:8px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);margin-bottom:4px} +.layer-item{display:flex;align-items:center;justify-content:space-between;padding:8px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);margin-bottom:4px;cursor:pointer;transition:opacity 0.2s,border-color 0.2s} .layer-left{display:flex;align-items:center;gap:8px} .ldot{width:10px;height:10px;border-radius:50%;flex-shrink:0} .ldot.air{background:var(--accent);box-shadow:0 0 6px rgba(100,240,200,0.4)} @@ -92,7 +92,6 @@ .ldot.epa{background:#cddc39;box-shadow:0 0 6px rgba(205,220,57,0.4)} .ldot.gdelt{background:#6495ed;box-shadow:0 0 6px rgba(100,149,237,0.4)} .ldot.capital{background:#ffd54f;box-shadow:0 0 6px rgba(255,213,79,0.4)} -.layer-item{cursor:pointer;transition:opacity 0.2s,border-color 0.2s} .layer-item-off{opacity:0.35} .layer-item-off .ldot{box-shadow:none} .layer-item-off .layer-name{color:#8899aa} @@ -405,7 +404,8 @@ // === GLOBALS === let globe = null; let globeInitialized = false; -let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true'; +let lowPerfMode = false; +try { lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true'; } catch (e) { console.warn('[Crucix] localStorage unavailable:', e.message); } let isFlat = shouldStartFlat(); // === LAYER TOGGLE STATE === @@ -425,23 +425,21 @@ {id:'gdelt', label:'GDELT Events', color:'#6495ed', dot:'gdelt'}, {id:'capitals',label:'World Capitals',color:'#ffd54f', dot:'capital'}, ]; -let layerVisibility = (()=>{ - try { - const saved = JSON.parse(localStorage.getItem('crucix-layers')); - if (saved && typeof saved === 'object') { - const vis = {}; - for (const l of LAYERS) vis[l.id] = saved[l.id] !== undefined ? saved[l.id] : true; - return vis; - } - } catch {} - return Object.fromEntries(LAYERS.map(l => [l.id, true])); -})(); +let layerVisibility = Object.fromEntries(LAYERS.map(l => [l.id, true])); +try { + const saved = JSON.parse(localStorage.getItem('crucix-layers')); + if (saved && typeof saved === 'object') { + for (const l of LAYERS) if (saved[l.id] !== undefined) layerVisibility[l.id] = saved[l.id]; + } +} catch (e) { console.warn('[Crucix] Could not restore layer preferences:', e.message); } function toggleLayer(id) { + if (!LAYERS.some(l => l.id === id)) return; layerVisibility[id] = !layerVisibility[id]; - localStorage.setItem('crucix-layers', JSON.stringify(layerVisibility)); + try { localStorage.setItem('crucix-layers', JSON.stringify(layerVisibility)); } + catch (e) { console.warn('[Crucix] Could not save layer preferences:', e.message); } renderLeftRail(); renderMapLegend(); - plotMarkers(); + if (!isFlat) plotMarkers(); if (isFlat && flatG) { flatG.selectAll('*').remove(); drawFlatMap(); } } const CAPITALS=[ @@ -520,7 +518,7 @@ {name:"Paris",country:"France",iso2:"FR",flag:"\ud83c\uddeb\ud83c\uddf7",lat:48.87,lng:2.33}, {name:"Cayenne",country:"French Guiana",iso2:"GF",flag:"\ud83c\uddec\ud83c\uddeb",lat:4.94,lng:-52.33}, {name:"Papeet\u0113",country:"French Polynesia",iso2:"PF",flag:"\ud83c\uddf5\ud83c\uddeb",lat:-17.53,lng:-149.56}, -{name:"Port-aux-Fran\u00e7ais",country:"French Southern and Antarctic Lands",iso2:"TF",flag:"\ud83c\uddf9\ud83c\uddeb",lat:48.81,lng:-1.4}, +{name:"Port-aux-Fran\u00e7ais",country:"French Southern and Antarctic Lands",iso2:"TF",flag:"\ud83c\uddf9\ud83c\uddeb",lat:-49.35,lng:70.22}, {name:"Libreville",country:"Gabon",iso2:"GA",flag:"\ud83c\uddec\ud83c\udde6",lat:0.38,lng:9.45}, {name:"Banjul",country:"Gambia",iso2:"GM",flag:"\ud83c\uddec\ud83c\uddf2",lat:13.45,lng:-16.57}, {name:"Tbilisi",country:"Georgia",iso2:"GE",flag:"\ud83c\uddec\ud83c\uddea",lat:41.68,lng:44.83}, @@ -529,7 +527,7 @@ {name:"Gibraltar",country:"Gibraltar",iso2:"GI",flag:"\ud83c\uddec\ud83c\uddee",lat:36.13,lng:-5.35}, {name:"Athens",country:"Greece",iso2:"GR",flag:"\ud83c\uddec\ud83c\uddf7",lat:37.98,lng:23.73}, {name:"Nuuk",country:"Greenland",iso2:"GL",flag:"\ud83c\uddec\ud83c\uddf1",lat:64.18,lng:-51.75}, -{name:"St. George's",country:"Grenada",iso2:"GD",flag:"\ud83c\uddec\ud83c\udde9",lat:32.38,lng:-64.68}, +{name:"St. George's",country:"Grenada",iso2:"GD",flag:"\ud83c\uddec\ud83c\udde9",lat:12.05,lng:-61.75}, {name:"Basse-Terre",country:"Guadeloupe",iso2:"GP",flag:"\ud83c\uddec\ud83c\uddf5",lat:16.03,lng:-61.73}, {name:"Hag\u00e5t\u00f1a",country:"Guam",iso2:"GU",flag:"\ud83c\uddec\ud83c\uddfa",lat:13.48,lng:144.75}, {name:"Guatemala City",country:"Guatemala",iso2:"GT",flag:"\ud83c\uddec\ud83c\uddf9",lat:14.62,lng:-90.52}, @@ -685,7 +683,7 @@ {name:"Caracas",country:"Venezuela",iso2:"VE",flag:"\ud83c\uddfb\ud83c\uddea",lat:10.48,lng:-66.87}, {name:"Hanoi",country:"Vietnam",iso2:"VN",flag:"\ud83c\uddfb\ud83c\uddf3",lat:21.03,lng:105.85}, {name:"Mata-Utu",country:"Wallis and Futuna",iso2:"WF",flag:"\ud83c\uddfc\ud83c\uddeb",lat:-13.95,lng:-171.93}, -{name:"El Aai\u00fan",country:"Western Sahara",iso2:"EH",flag:"\ud83c\uddea\ud83c\udded",lat:-13.28,lng:27.14}, +{name:"El Aai\u00fan",country:"Western Sahara",iso2:"EH",flag:"\ud83c\uddea\ud83c\udded",lat:27.15,lng:-13.20}, {name:"Sana'a",country:"Yemen",iso2:"YE",flag:"\ud83c\uddfe\ud83c\uddea",lat:15.37,lng:44.19}, {name:"Lusaka",country:"Zambia",iso2:"ZM",flag:"\ud83c\uddff\ud83c\uddf2",lat:-15.42,lng:28.28}, {name:"Harare",country:"Zimbabwe",iso2:"ZW",flag:"\ud83c\uddff\ud83c\uddfc",lat:-17.82,lng:31.03},