docs: add registry dockge and dashboard operations
All checks were successful
Build / test-and-image (push) Successful in 2m39s

This commit is contained in:
2026-05-16 21:18:51 +02:00
parent 85f97bb2a6
commit 42b7fc2024
10 changed files with 372 additions and 87 deletions

View File

@@ -75,6 +75,14 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
/* 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{cursor:pointer;transition:border-color .15s ease,background .15s ease,opacity .15s ease}
.layer-item:hover{border-color:rgba(100,240,200,0.24);background:rgba(100,240,200,0.04)}
.layer-item.focused{border-color:rgba(100,240,200,0.6);background:rgba(100,240,200,0.08)}
.layer-item.hidden-layer{opacity:.45;border-color:rgba(255,95,99,0.18)}
.layer-mode{font-family:var(--mono);font-size:8px;color:var(--dim);margin-top:2px;text-transform:uppercase}
.sensor-actions{display:flex;gap:6px;align-items:center}
.mini-btn{border:1px solid rgba(100,240,200,0.18);background:rgba(100,240,200,0.04);color:var(--dim);font-family:var(--mono);font-size:9px;padding:3px 6px;cursor:pointer}
.mini-btn:hover{color:var(--accent);border-color:rgba(100,240,200,0.4)}
.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)}
@@ -394,8 +402,23 @@ let globeInitialized = false;
let flightsVisible = true;
let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true';
let isFlat = shouldStartFlat();
let layerModes = JSON.parse(localStorage.getItem('crucix_layer_modes') || '{}');
let spaceDisplayMode = localStorage.getItem('crucix_space_display') || 'icons';
let currentRegion = 'world';
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
const layerTypeMap = {
air: ['air'],
thermal: ['thermal'],
sdr: ['sdr'],
maritime: ['maritime'],
nuke: ['nuke', 'radiation'],
conflict: ['conflict'],
health: ['health'],
news: ['news', 'gdelt'],
osint: ['osint'],
space: ['space'],
};
const signalGuideItems = [
{
term:'No Callsign',
@@ -602,6 +625,44 @@ function renderTopbar(){
}
// === LEFT RAIL ===
function layerMode(key){ return layerModes[key] || 'normal'; }
function layerModeLabel(key){ return layerMode(key) === 'focus' ? 'focused' : layerMode(key) === 'hidden' ? 'hidden' : 'normal'; }
function setLayerMode(key, mode){
if(mode === 'normal') delete layerModes[key]; else layerModes[key] = mode;
localStorage.setItem('crucix_layer_modes', JSON.stringify(layerModes));
renderLeftRail();
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
else plotMarkers();
}
function cycleLayerMode(key, event){
const current = layerMode(key);
const next = event && (event.shiftKey || event.ctrlKey || event.metaKey)
? (current === 'hidden' ? 'normal' : 'hidden')
: (current === 'focus' ? 'normal' : 'focus');
setLayerMode(key, next);
}
function resetLayerModes(){
layerModes = {};
localStorage.removeItem('crucix_layer_modes');
renderLeftRail();
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
else plotMarkers();
}
function toggleSpaceDisplay(){
spaceDisplayMode = spaceDisplayMode === 'icons' ? 'orbits' : 'icons';
localStorage.setItem('crucix_space_display', spaceDisplayMode);
renderLeftRail();
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
else plotMarkers();
}
function shouldShowType(type){
const focused = Object.entries(layerModes).filter(([,mode]) => mode === 'focus').flatMap(([key]) => layerTypeMap[key] || []);
const hidden = Object.entries(layerModes).filter(([,mode]) => mode === 'hidden').flatMap(([key]) => layerTypeMap[key] || []);
if(hidden.includes(type)) return false;
if(focused.length > 0) return focused.includes(type);
return true;
}
function renderLeftRail(){
const totalAir=D.air.reduce((s,a)=>s+a.total,0);
const totalThermal=D.thermal.reduce((s,t)=>s+t.det,0);
@@ -610,16 +671,16 @@ function renderLeftRail(){
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)')}`}
{key:'air',name:t('layers.airActivity','Air Activity'),count:totalAir,dot:'air',sub:`${D.air.length} ${t('layers.theaters','theaters')}`},
{key:'thermal',name:t('layers.thermalSpikes','Thermal Spikes'),count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`},
{key:'sdr',name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`},
{key:'maritime',name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')},
{key:'nuke',name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')},
{key:'conflict',name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`},
{key:'health',name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')},
{key:'news',name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')},
{key:'osint',name:t('layers.osintFeed','OSINT Feed'),count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`},
{key:'space',name:t('layers.spaceActivity','Satellites'),count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`}
];
const allNormal=D.nuke.every(s=>!s.anom);
const nukeHtml=D.nuke.map(s=>`<div class="site-row"><span>${s.site}</span><span class="site-val">${s.n>0?(s.cpm?.toFixed(1)||'--')+' CPM':'No data'}</span></div>`).join('');
@@ -632,8 +693,8 @@ function renderLeftRail(){
document.getElementById('leftRail').innerHTML=`
<div class="g-panel">
<div class="sec-head"><h3>${t('panels.sensorGrid','Sensor Grid')}</h3><span class="badge">${t('badges.live','LIVE')}</span></div>
${layers.map(l=>`<div class="layer-item"><div class="layer-left"><div class="ldot ${l.dot}"></div><div><div class="layer-name">${l.name}</div><div class="layer-sub">${l.sub}</div></div></div><div class="layer-count">${l.count}</div></div>`).join('')}
<div class="sec-head"><h3>${t('panels.sensorGrid','Sensor Grid')}</h3><div class="sensor-actions"><button class="mini-btn" onclick="resetLayerModes();event.stopPropagation()">RESET</button><span class="badge">${t('badges.live','LIVE')}</span></div></div>
${layers.map(l=>`<div class="layer-item ${layerMode(l.key)==='focus'?'focused':''} ${layerMode(l.key)==='hidden'?'hidden-layer':''}" onclick="cycleLayerMode('${l.key}',event)" title="Click to focus. Shift/Ctrl-click to hide."><div class="layer-left"><div class="ldot ${l.dot}"></div><div><div class="layer-name">${l.name}</div><div class="layer-sub">${l.sub}</div><div class="layer-mode">${layerModeLabel(l.key)}</div></div></div><div class="layer-count">${l.count}</div></div>`).join('')}
</div>
<div class="g-panel">
<div class="sec-head"><h3>${t('panels.nuclearWatch','Nuclear Watch')}</h3><span class="badge">${t('badges.radiation','RADIATION')}</span></div>
@@ -651,7 +712,7 @@ function renderLeftRail(){
<div class="econ-row"><span class="elabel">${t('metrics.natDebt','Nat. Debt')}</span><span class="eval">$${(parseFloat(D.treasury.totalDebt)/1e12).toFixed(2)}T</span></div>
</div>
<div class="g-panel">
<div class="sec-head"><h3>${t('panels.spaceWatch','Space Watch')}</h3><span class="badge">${t('badges.orbital','CELESTRAK')}</span></div>
<div class="sec-head"><h3>${t('panels.spaceWatch','Space Watch')}</h3><button class="mini-btn" onclick="toggleSpaceDisplay()">${spaceDisplayMode.toUpperCase()}</button></div>
${D.space ? `
<div class="econ-row"><span class="elabel">New Objects (30d)</span><span class="eval" style="color:var(--accent2)">${D.space.totalNewObjects||0}</span></div>
<div class="econ-row"><span class="elabel">Military Sats</span><span class="eval">${D.space.militarySats||0}</span></div>
@@ -953,7 +1014,7 @@ function plotMarkers(){
});
// === ISS + Space Stations (bright white, pulsing) ===
(D.space?.stationPositions||[]).forEach(s=>{
if(spaceDisplayMode === 'icons') (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,
@@ -973,12 +1034,15 @@ function plotMarkers(){
});
});
// Set points on globe
globe.pointsData(points);
globe.labelsData(labels);
// === ACLED CONFLICT EVENTS (pulsing rings) ===
const conflictRings = (D.acled?.deadliestEvents || []).filter(e => e.lat && e.lon).map(e => {
const visiblePoints = points.filter(p => shouldShowType(p.type));
const visibleLabels = labels.filter(l => !l.type || shouldShowType(l.type));
// Set points on globe
globe.pointsData(visiblePoints);
globe.labelsData(visibleLabels);
const conflictRings = shouldShowType('conflict') ? (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,
@@ -988,12 +1052,12 @@ function plotMarkers(){
popHead: e.type || 'CONFLICT', popMeta: 'ACLED Conflict Data',
popText: `${e.fatalities} fatalities<br>${e.location}, ${e.country}<br>Date: ${e.date}`
};
});
}) : [];
globe.ringsData(conflictRings);
// === FLIGHT CORRIDORS (3D arcs) ===
const arcs = [];
if(flightsVisible){
if(flightsVisible && shouldShowType('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},
@@ -1040,6 +1104,17 @@ function plotMarkers(){
});
});
}
if(spaceDisplayMode === 'orbits' && shouldShowType('space')){
(D.space?.stationPositions||[]).forEach(s=>{
arcs.push({
startLat: Math.max(-65, s.lat - 18), startLng: s.lon - 80,
endLat: Math.min(65, s.lat + 18), endLng: s.lon + 80,
color: ['rgba(224,176,255,0.55)','rgba(224,176,255,0.12)'],
stroke: 0.7,
label: `${s.name} approximate orbital track`
});
});
}
globe.arcsData(arcs);
// Zoom-aware marker sizing: scale markers and labels with camera altitude
@@ -1055,11 +1130,11 @@ function plotMarkers(){
globe.arcDashAnimateTime(lowPerfMode ? 0 : 2000);
// Priority-based point visibility: hide low-priority markers when zoomed out
if(alt > 2.0){
globe.pointsData(points.filter(p => (p.priority||3) <= 1));
globe.pointsData(visiblePoints.filter(p => (p.priority||3) <= 1));
} else if(alt > 1.2){
globe.pointsData(points.filter(p => (p.priority||3) <= 2));
globe.pointsData(visiblePoints.filter(p => (p.priority||3) <= 2));
} else {
globe.pointsData(points);
globe.pointsData(visiblePoints);
}
};
if(typeof globe.onZoom==='function') globe.onZoom(onGlobeZoom);
@@ -1178,7 +1253,7 @@ function initFlatMap(){
flatG.selectAll('.marker-circle').attr('r',function(){return +this.dataset.baseR/Math.sqrt(k)});
flatG.selectAll('.marker-label').style('font-size',Math.max(7,9/Math.sqrt(k))+'px')
.style('display',k>=2.5?'block':'none');
});
}).on('end',()=>{ flatG.selectAll('.marker-label').style('display','block'); });
flatSvg.call(flatZoom);
drawFlatMap();
}
@@ -1197,7 +1272,8 @@ function drawFlatMap(){
function plotFlatMarkers(){
const mg=flatG.append('g').attr('class','markers');
const proj=flatProjection;
const addPt=(lat,lon,r,fill,stroke,onClick,priority)=>{
const addPt=(lat,lon,r,fill,stroke,onClick,priority,type)=>{
if(type && !shouldShowType(type)) return null;
const[x,y]=proj([lon,lat]);if(!x||!y)return null;
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',priority||3);
if(onClick) g.on('click',ev=>{ev.stopPropagation();onClick(ev)});
@@ -1215,12 +1291,12 @@ function plotFlatMarkers(){
});
}
// Thermal
D.thermal.forEach(t=>t.fires.forEach(f=>{
if(shouldShowType('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}<br>FRP: ${f.frp.toFixed(1)} MW`,'FIRMS'),3);
}));
// Chokepoints
D.chokepoints.forEach(cp=>{
if(shouldShowType('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')});
@@ -1229,30 +1305,30 @@ function plotFlatMarkers(){
});
// 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(shouldShowType('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}<br>${z.region}`,'KiwiSDR'),3)}));
if(shouldShowType('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}<br>${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(shouldShowType('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(shouldShowType('health')) 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(shouldShowType('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(shouldShowType('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)});
// 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(shouldShowType('environment')) (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(spaceDisplayMode === 'icons' && shouldShowType('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(shouldShowType('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(shouldShowType('conflict')) (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)
@@ -1261,7 +1337,7 @@ function plotFlatMarkers(){
g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)');
});
// Flight corridors
if(flightsVisible){
if(flightsVisible && shouldShowType('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');