|
97 | 97 | .econ-row .eval{font-family:var(--mono);font-weight:600} |
98 | 98 |
|
99 | 99 | /* CENTER: MAP */ |
| 100 | +.map-region-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--border);background:var(--panel);backdrop-filter:blur(20px)} |
100 | 101 | .map-container{flex:1;min-height:560px;border:1px solid var(--border);background:radial-gradient(ellipse at center,rgba(4,12,20,1),rgba(2,4,8,1));position:relative;overflow:hidden} |
101 | 102 | #globeViz{width:100%;height:100%;cursor:grab} |
102 | 103 | #globeViz:active{cursor:grabbing} |
|
272 | 273 | .topbar{padding:10px 12px} |
273 | 274 | .top-left,.top-center,.top-right{width:100%} |
274 | 275 | .top-center{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px} |
| 276 | + .map-region-bar{display:none} |
275 | 277 | .top-right{gap:6px} |
276 | 278 | .region-btn,.meta-pill,.alert-badge,.guide-btn{font-size:10px} |
277 | 279 | .grid{display:flex;flex-direction:column} |
|
331 | 333 | <div class="grid"> |
332 | 334 | <div class="col" id="leftRail"></div> |
333 | 335 | <div class="col" id="centerCol"> |
| 336 | + <div class="map-region-bar" id="mapRegionBar"></div> |
334 | 337 | <div class="map-container" id="mapContainer"> |
335 | 338 | <div id="globeViz"></div> |
336 | 339 | <svg id="flatMapSvg" style="display:none;width:100%;height:100%;position:absolute;top:0;left:0;cursor:grab"></svg> |
|
389 | 392 | let flightsVisible = true; |
390 | 393 | let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true'; |
391 | 394 | let isFlat = shouldStartFlat(); |
| 395 | +let currentRegion = 'world'; |
392 | 396 | let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH; |
393 | 397 | const signalGuideItems = [ |
394 | 398 | { |
|
554 | 558 | } |
555 | 559 |
|
556 | 560 | // === TOPBAR === |
| 561 | +function getRegionControlsMarkup(){ |
| 562 | + return ['world','americas','europe','middleEast','asiaPacific','africa'].map(r=> |
| 563 | + `<button class="region-btn ${r===currentRegion?'active':''}" data-region="${r}" onclick="setRegion('${r}')">${r==='middleEast'?'MIDDLE EAST':r==='asiaPacific'?'ASIA PACIFIC':r.toUpperCase()}</button>` |
| 564 | + ).join(''); |
| 565 | +} |
| 566 | + |
| 567 | +function renderRegionControls(){ |
| 568 | + const mapRegionBar = document.getElementById('mapRegionBar'); |
| 569 | + if(!mapRegionBar) return; |
| 570 | + if(isMobileLayout()){ |
| 571 | + mapRegionBar.innerHTML = ''; |
| 572 | + mapRegionBar.style.display = 'none'; |
| 573 | + return; |
| 574 | + } |
| 575 | + mapRegionBar.innerHTML = getRegionControlsMarkup(); |
| 576 | + mapRegionBar.style.display = 'flex'; |
| 577 | +} |
| 578 | + |
557 | 579 | function renderTopbar(){ |
| 580 | + const mobile = isMobileLayout(); |
558 | 581 | const ts = new Date(D.meta.timestamp); |
559 | 582 | const d = ts.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase(); |
560 | 583 | const timeStr = ts.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true}); |
|
563 | 586 | <span class="brand">CRUCIX MONITOR</span> |
564 | 587 | <span class="regime-chip"><span class="blink"></span>WARTIME STAGFLATION RISK</span> |
565 | 588 | </div> |
566 | | - <div class="top-center"> |
567 | | - ${['world','americas','europe','middleEast','asiaPacific','africa'].map(r=> |
568 | | - `<button class="region-btn ${r==='world'?'active':''}" data-region="${r}" onclick="setRegion('${r}')">${r==='middleEast'?'MIDDLE EAST':r==='asiaPacific'?'ASIA PACIFIC':r.toUpperCase()}</button>` |
569 | | - ).join('')} |
570 | | - </div> |
| 589 | + ${mobile ? `<div class="top-center">${getRegionControlsMarkup()}</div>` : ''} |
571 | 590 | <div class="top-right"> |
572 | 591 | <button class="meta-pill perf-pill" onclick="togglePerfMode()" title="Reduce visual effects and start mobile in flat mode">${t('dashboard.perf','PERF')} <span class="v" id="perfStatus">${lowPerfMode?t('dashboard.perfLow','LOW'):t('dashboard.perfHigh','HIGH')}</span></button> |
573 | 592 | <span class="meta-pill">${t('dashboard.sweep','SWEEP')} <span class="v">${(D.meta.totalDurationMs/1000).toFixed(1)}s</span></span> |
|
577 | 596 | <button class="guide-btn" onclick="openGlossary()">${t('dashboard.guideBtn','What Signals Mean')}</button> |
578 | 597 | <span class="alert-badge">${t('dashboard.highAlert','HIGH ALERT')}</span> |
579 | 598 | </div>`; |
| 599 | + renderRegionControls(); |
580 | 600 | } |
581 | 601 |
|
582 | 602 | // === LEFT RAIL === |
|
1077 | 1097 | flightsVisible = !flightsVisible; |
1078 | 1098 | const btn = document.getElementById('flightToggle'); |
1079 | 1099 | btn.classList.toggle('off', !flightsVisible); |
| 1100 | + if(isFlat){ |
| 1101 | + if(flatG){ |
| 1102 | + flatG.selectAll('*').remove(); |
| 1103 | + drawFlatMap(); |
| 1104 | + } |
| 1105 | + return; |
| 1106 | + } |
1080 | 1107 | if(!globe){ |
1081 | 1108 | return; |
1082 | 1109 | } |
|
1149 | 1176 | flatG.selectAll('.marker-circle').attr('r',function(){return +this.dataset.baseR/Math.sqrt(k)}); |
1150 | 1177 | flatG.selectAll('.marker-label').style('font-size',Math.max(7,9/Math.sqrt(k))+'px') |
1151 | 1178 | .style('display',k>=2.5?'block':'none'); |
1152 | | - // Priority-based visibility: hide low-priority markers at low zoom |
1153 | | - flatG.selectAll('[data-priority]').style('display',function(){ |
1154 | | - const p=+this.dataset.priority; |
1155 | | - if(p<=1) return 'block'; |
1156 | | - if(p<=2) return k>=2?'block':'none'; |
1157 | | - return k>=3.5?'block':'none'; |
1158 | | - }); |
1159 | 1179 | }); |
1160 | 1180 | flatSvg.call(flatZoom); |
1161 | 1181 | drawFlatMap(); |
|
1184 | 1204 | }; |
1185 | 1205 | // Air |
1186 | 1206 | 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}]; |
1187 | | - D.air.forEach((a,i)=>{ |
1188 | | - const c=airCoords[i];if(!c)return; |
1189 | | - const g=addPt(c.lat,c.lon,4+a.total/40,'rgba(100,240,200,0.7)','rgba(100,240,200,0.3)', |
1190 | | - ev=>showPopup(ev,a.region,`${a.total} aircraft<br>No callsign: ${a.noCallsign}<br>High alt: ${a.highAlt}`,'Air Activity'),1); |
1191 | | - if(g) g.append('text').attr('class','marker-label').attr('x',10).attr('y',3).attr('fill','var(--dim)').attr('font-size','9px').attr('font-family','var(--mono)').text(a.region.replace(' Region','')+' '+a.total); |
1192 | | - }); |
| 1207 | + if(flightsVisible){ |
| 1208 | + D.air.forEach((a,i)=>{ |
| 1209 | + const c=airCoords[i];if(!c)return; |
| 1210 | + const g=addPt(c.lat,c.lon,4+a.total/40,'rgba(100,240,200,0.7)','rgba(100,240,200,0.3)', |
| 1211 | + ev=>showPopup(ev,a.region,`${a.total} aircraft<br>No callsign: ${a.noCallsign}<br>High alt: ${a.highAlt}`,'Air Activity'),1); |
| 1212 | + if(g) g.append('text').attr('class','marker-label').attr('x',10).attr('y',3).attr('fill','var(--dim)').attr('font-size','9px').attr('font-family','var(--mono)').text(a.region.replace(' Region','')+' '+a.total); |
| 1213 | + }); |
| 1214 | + } |
1193 | 1215 | // Thermal |
1194 | 1216 | D.thermal.forEach(t=>t.fires.forEach(f=>{ |
1195 | 1217 | addPt(f.lat,f.lon,2+Math.min(f.frp/50,5),'rgba(255,95,99,0.6)','rgba(255,95,99,0.2)', |
|
1237 | 1259 | g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)'); |
1238 | 1260 | }); |
1239 | 1261 | // Flight corridors |
1240 | | - 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}]; |
1241 | | - 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}]; |
1242 | | - const cG=flatG.append('g').attr('class','corridors-layer'); |
1243 | | - for(let i=0;i<D.air.length;i++){for(let j=i+1;j<D.air.length;j++){ |
1244 | | - const a=D.air[i],b=D.air[j],from=airCoordsFlight[i],to=airCoordsFlight[j]; |
1245 | | - if(!from||!to)continue;const traffic=a.total+b.total;if(traffic<30)continue; |
1246 | | - const ncR=(a.noCallsign+b.noCallsign)/Math.max(traffic,1); |
1247 | | - const clr=ncR>0.15?'rgba(255,95,99,0.4)':ncR>0.05?'rgba(255,184,76,0.35)':'rgba(100,240,200,0.25)'; |
1248 | | - const interp=d3.geoInterpolate([from.lon,from.lat],[to.lon,to.lat]); |
1249 | | - const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); |
1250 | | - const feat={type:'Feature',geometry:{type:'LineString',coordinates:coords}}; |
1251 | | - cG.append('path').datum(feat).attr('d',flatPath).attr('fill','none').attr('stroke',clr).attr('stroke-width',Math.max(0.8,Math.min(3,traffic/80))); |
1252 | | - }} |
1253 | | - D.air.forEach((a,i)=>{if(!airCoordsFlight[i]||a.total<25)return;hubs.forEach(hub=>{ |
1254 | | - if(Math.abs(airCoordsFlight[i].lat-hub.lat)+Math.abs(airCoordsFlight[i].lon-hub.lon)<20)return; |
1255 | | - const interp=d3.geoInterpolate([airCoordsFlight[i].lon,airCoordsFlight[i].lat],[hub.lon,hub.lat]); |
1256 | | - const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); |
1257 | | - cG.append('path').datum({type:'Feature',geometry:{type:'LineString',coordinates:coords}}).attr('d',flatPath).attr('fill','none').attr('stroke','rgba(100,240,200,0.15)').attr('stroke-width',0.6); |
1258 | | - })}); |
| 1262 | + if(flightsVisible){ |
| 1263 | + 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}]; |
| 1264 | + 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}]; |
| 1265 | + const cG=flatG.append('g').attr('class','corridors-layer'); |
| 1266 | + for(let i=0;i<D.air.length;i++){for(let j=i+1;j<D.air.length;j++){ |
| 1267 | + const a=D.air[i],b=D.air[j],from=airCoordsFlight[i],to=airCoordsFlight[j]; |
| 1268 | + if(!from||!to)continue;const traffic=a.total+b.total;if(traffic<30)continue; |
| 1269 | + const ncR=(a.noCallsign+b.noCallsign)/Math.max(traffic,1); |
| 1270 | + const clr=ncR>0.15?'rgba(255,95,99,0.4)':ncR>0.05?'rgba(255,184,76,0.35)':'rgba(100,240,200,0.25)'; |
| 1271 | + const interp=d3.geoInterpolate([from.lon,from.lat],[to.lon,to.lat]); |
| 1272 | + const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); |
| 1273 | + const feat={type:'Feature',geometry:{type:'LineString',coordinates:coords}}; |
| 1274 | + cG.append('path').datum(feat).attr('d',flatPath).attr('fill','none').attr('stroke',clr).attr('stroke-width',Math.max(0.8,Math.min(3,traffic/80))); |
| 1275 | + }} |
| 1276 | + D.air.forEach((a,i)=>{if(!airCoordsFlight[i]||a.total<25)return;hubs.forEach(hub=>{ |
| 1277 | + if(Math.abs(airCoordsFlight[i].lat-hub.lat)+Math.abs(airCoordsFlight[i].lon-hub.lon)<20)return; |
| 1278 | + const interp=d3.geoInterpolate([airCoordsFlight[i].lon,airCoordsFlight[i].lat],[hub.lon,hub.lat]); |
| 1279 | + const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); |
| 1280 | + cG.append('path').datum({type:'Feature',geometry:{type:'LineString',coordinates:coords}}).attr('d',flatPath).attr('fill','none').attr('stroke','rgba(100,240,200,0.15)').attr('stroke-width',0.6); |
| 1281 | + })}); |
| 1282 | + } |
1259 | 1283 | } |
1260 | 1284 |
|
1261 | 1285 | // Update setRegion for flat mode |
|
1265 | 1289 | const _origMapZoom = mapZoom; |
1266 | 1290 |
|
1267 | 1291 | function setRegion(r){ |
| 1292 | + currentRegion = r; |
1268 | 1293 | document.querySelectorAll('.region-btn').forEach(b=>b.classList.toggle('active',b.dataset.region===r)); |
1269 | 1294 | closePopup(); |
1270 | 1295 | if(isFlat && flatSvg && flatZoom){ |
|
1553 | 1578 | document.querySelectorAll('.mbar span,.smb span').forEach(bar=>{const w=bar.style.width;bar.style.width='0%';gsap.to(bar,{width:w,duration:1,ease:'power2.out'})}); |
1554 | 1579 | document.querySelectorAll('.spark-bar').forEach(bar=>{const h=bar.style.height;bar.style.height='0%';gsap.to(bar,{height:h,duration:0.8,ease:'power2.out'})}); |
1555 | 1580 | },1000); |
1556 | | - },4.0); |
| 1581 | + },[],4.0); |
1557 | 1582 | } |
1558 | 1583 |
|
1559 | 1584 | function isMobileLayout(){ return window.innerWidth <= 1100; } |
|
1644 | 1669 | const mobileNow = isMobileLayout(); |
1645 | 1670 | if(force || lastResponsiveMobile === null || mobileNow !== lastResponsiveMobile){ |
1646 | 1671 | lastResponsiveMobile = mobileNow; |
| 1672 | + renderTopbar(); |
1647 | 1673 | renderLeftRail(); |
1648 | 1674 | renderLower(); |
1649 | 1675 | renderRight(); |
|
0 commit comments