diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index d83955a..ce525bc 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -9,8 +9,6 @@ - - @@ -365,7 +380,8 @@ let D = null; // === GLOBALS === let globe = null; -let globeInitialized = false; +let globeLoaded = false; +let globeInitPromise = null; let flightsVisible = true; let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true'; let isFlat = shouldStartFlat(); @@ -495,6 +511,18 @@ if(lowPerfMode) document.body.classList.add('low-perf'); +function globeArcDashAnimateTimeFor(altitude) { + if (lowPerfMode) return 0; + return altitude < 1.0 ? 2000 : 0; +} + +function syncGlobePerfSettings() { + if (!globe) return; + const altitude = globe.pointOfView().altitude; + globe.controls().autoRotate = !lowPerfMode; + globe.arcDashAnimateTime(globeArcDashAnimateTimeFor(altitude)); +} + function isWeakMobileDevice(){ const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches; const memory = navigator.deviceMemory || 0; @@ -521,9 +549,8 @@ document.body.classList.toggle('low-perf', lowPerfMode); const perfStatus = document.getElementById('perfStatus'); if(perfStatus) perfStatus.textContent = lowPerfMode ? 'LOW' : 'HIGH'; - if(globe){ - globe.controls().autoRotate = !lowPerfMode; - globe.arcDashAnimateTime(lowPerfMode ? 0 : 2000); + if (globe) { + syncGlobePerfSettings(); } if(lowPerfMode && isMobileLayout() && !isFlat){ toggleMapMode(); @@ -533,6 +560,36 @@ } } +const scriptLoadPromises = new Map(); + +function loadScript(url) { + if (scriptLoadPromises.has(url)) return scriptLoadPromises.get(url); + const promise = new Promise((resolve, reject) => { + const existing = document.querySelector(`script[src="${url}"]`); + const script = existing || document.createElement('script'); + const onLoad = () => { + script.dataset.loaded = 'true'; + resolve(); + }; + const onError = (err) => { + scriptLoadPromises.delete(url); + reject(err); + }; + if (script.dataset.loaded === 'true') { + resolve(); + return; + } + script.addEventListener('load', onLoad, { once: true }); + script.addEventListener('error', onError, { once: true }); + if (!existing) { + script.src = url; + document.head.appendChild(script); + } + }); + scriptLoadPromises.set(url, promise); + return promise; +} + // === TOPBAR === function renderTopbar(){ const ts = new Date(D.meta.timestamp); @@ -659,157 +716,171 @@ } setMapLoading(true, 'Initializing 3D Globe'); requestAnimationFrame(() => { - try { - initGlobe(); + initGlobe().then((globeReady) => { + setMapLoading(false); + if(!globeReady || !globe){ + isFlat = true; + document.getElementById('globeViz').style.display = 'none'; + document.getElementById('flatMapSvg').style.display = 'block'; + document.getElementById('projToggle').textContent = 'GLOBE MODE'; + document.getElementById('mapHint').textContent = '3D ERROR: LOAD FAILED'; + if(!flatSvg) initFlatMap(); + else { flatG.selectAll('*').remove(); drawFlatMap(); } + return; + } + if(typeof globe.resumeAnimation === 'function') globe.resumeAnimation(); + document.getElementById('globeViz').style.display = 'block'; + document.getElementById('flatMapSvg').style.display = 'none'; + document.getElementById('projToggle').textContent = 'FLAT MODE'; + document.getElementById('mapHint').textContent = 'DRAG TO ROTATE · SCROLL TO ZOOM'; + }).catch(() => { setMapLoading(false); - } catch { isFlat = true; document.getElementById('globeViz').style.display = 'none'; document.getElementById('flatMapSvg').style.display = 'block'; document.getElementById('projToggle').textContent = 'GLOBE MODE'; - document.getElementById('mapHint').textContent = '3D LOAD FAILED · FLAT MODE'; + document.getElementById('mapHint').textContent = '3D ERROR: LOAD FAILED'; if(!flatSvg) initFlatMap(); else { flatG.selectAll('*').remove(); drawFlatMap(); } - setMapLoading(false); - } + }); }); } -function initGlobe(){ - if(globeInitialized && globe){ - if(typeof globe.resumeAnimation === 'function') globe.resumeAnimation(); - document.getElementById('globeViz').style.display = 'block'; - document.getElementById('flatMapSvg').style.display = 'none'; - document.getElementById('projToggle').textContent = 'FLAT MODE'; - document.getElementById('mapHint').textContent = 'DRAG TO ROTATE · SCROLL TO ZOOM'; - return; - } - const container = document.getElementById('mapContainer'); - const w = container.clientWidth; - const h = container.clientHeight || 560; - - globe = Globe() - .width(w) - .height(h) - .globeImageUrl('//unpkg.com/three-globe@2.33.0/example/img/earth-night.jpg') - .bumpImageUrl('//unpkg.com/three-globe@2.33.0/example/img/earth-topology.png') - .backgroundImageUrl('') - .backgroundColor('rgba(0,0,0,0)') - .atmosphereColor('#64f0c8') - .atmosphereAltitude(0.18) - .showGraticules(true) - // Points layer (main markers) - .pointAltitude(d => d.alt || 0.01) - .pointRadius(d => d.size || 0.3) - .pointColor(d => d.color) - .pointLabel(d => `${d.popHead||''}
${d.popMeta||''}`) - .onPointClick((pt, ev) => { showPopup(ev, pt.popHead, pt.popText, pt.popMeta, pt.lat, pt.lng, pt.alt); }) - .onPointHover(pt => { document.getElementById('globeViz').style.cursor = pt ? 'pointer' : 'grab'; }) - // Arcs layer (flight corridors) - .arcColor(d => d.color) - .arcStroke(d => d.stroke || 0.4) - .arcDashLength(0.4) - .arcDashGap(0.2) - .arcDashAnimateTime(2000) - .arcAltitudeAutoScale(0.3) - .arcLabel(d => d.label || '') - // Rings layer (pulsing conflict events) - .ringColor(d => t => `rgba(255,120,80,${1-t})`) - .ringMaxRadius(d => d.maxR || 3) - .ringPropagationSpeed(d => d.speed || 2) - .ringRepeatPeriod(d => d.period || 800) - // Labels layer - .labelText(d => d.text) - .labelSize(d => d.size || 0.4) - .labelColor(d => d.color || 'rgba(106,138,130,0.9)') - .labelDotRadius(0) - .labelAltitude(0.012) - .labelResolution(2) - (document.getElementById('globeViz')); - - // Style the WebGL scene - const scene = globe.scene(); - const renderer = globe.renderer(); - renderer.setClearColor(0x000000, 0); - - // Add subtle stars background - const starGeom = new THREE.BufferGeometry(); - const starVerts = []; - for(let i=0; i<2000; i++){ - const r = 800 + Math.random()*200; - const theta = Math.random()*Math.PI*2; - const phi = Math.acos(2*Math.random()-1); - starVerts.push(r*Math.sin(phi)*Math.cos(theta), r*Math.sin(phi)*Math.sin(theta), r*Math.cos(phi)); - } - starGeom.setAttribute('position', new THREE.Float32BufferAttribute(starVerts, 3)); - const starMat = new THREE.PointsMaterial({color:0x88bbaa, size:0.8, transparent:true, opacity:0.6}); - scene.add(new THREE.Points(starGeom, starMat)); - - // Customize graticule color - scene.traverse(obj => { - if(obj.material && obj.type === 'Line'){ - obj.material.color.set(0x1a3a2a); - obj.material.opacity = 0.3; - obj.material.transparent = true; - } - }); - - // Set initial POV - globe.pointOfView(regionPOV.world, 0); +async function initGlobe(){ + if (globeLoaded) return !!globe; + if (globeInitPromise) return globeInitPromise; + globeInitPromise = (async () => { + const hint = document.getElementById('mapHint'); + const oldHint = hint.textContent; + hint.textContent = 'LOADING 3D ENGINE...'; - // Auto-rotate slowly - globe.controls().autoRotate = !lowPerfMode; - globe.controls().autoRotateSpeed = 0.3; - globe.controls().enableDamping = true; - globe.controls().dampingFactor = 0.1; - - // Stop auto-rotate on interaction, resume after 10s - let rotateTimeout; - const el = document.getElementById('globeViz'); - el.addEventListener('mousedown', () => { - globe.controls().autoRotate = false; - clearTimeout(rotateTimeout); - }); - el.addEventListener('mouseup', () => { - rotateTimeout = setTimeout(() => { if(globe && !lowPerfMode) globe.controls().autoRotate = true; }, 10000); - }); + try { + if (!window.Globe) await loadScript('https://unpkg.com/globe.gl@2.33.0'); + if (!window.Globe) throw new Error('Globe.gl did not load'); + + const container = document.getElementById('mapContainer'); + const w = container.clientWidth; + const h = container.clientHeight || 560; + + globe = window.Globe() + .width(w) + .height(h) + .globeImageUrl('//unpkg.com/three-globe@2.33.0/example/img/earth-night.jpg') + // Simplified: no bump map + .backgroundImageUrl('') + .backgroundColor('rgba(0,0,0,0)') + .atmosphereColor('#64f0c8') + .atmosphereAltitude(0.12) // Simplified: thinner atmosphere + .showGraticules(true) + // Points layer (main markers) + .pointAltitude(d => d.alt || 0.01) + .pointRadius(d => d.size || 0.3) + .pointColor(d => d.color) + .pointLabel(d => `${d.popHead||''}
${d.popMeta||''}`) + .onPointClick((pt, ev) => { showPopup(ev, pt.popHead, pt.popText, pt.popMeta, pt.lat, pt.lng, pt.alt); }) + .onPointHover(pt => { document.getElementById('globeViz').style.cursor = pt ? 'pointer' : 'grab'; }) + // Arcs layer (flight corridors) + .arcColor(d => d.color) + .arcStroke(d => d.stroke || 0.4) + .arcDashLength(0.4) + .arcDashGap(0.2) + .arcDashAnimateTime(globeArcDashAnimateTimeFor(regionPOV.world.altitude)) + .arcAltitudeAutoScale(0.3) + .arcLabel(d => d.label || '') + // Rings layer (pulsing conflict events) + .ringColor(d => t => `rgba(255,120,80,${1-t})`) + .ringMaxRadius(d => d.maxR || 3) + .ringPropagationSpeed(d => d.speed || 2) + .ringRepeatPeriod(d => d.period || 800) + // Labels layer + .labelText(d => d.text) + .labelSize(d => d.size || 0.4) + .labelColor(d => d.color || 'rgba(106,138,130,0.9)') + .labelDotRadius(0) + .labelAltitude(0.012) + .labelResolution(2) + (document.getElementById('globeViz')); + + // Style the WebGL scene + const scene = globe.scene(); + const renderer = globe.renderer(); + renderer.setClearColor(0x000000, 0); + + // Customize graticule color + scene.traverse(obj => { + if(obj.material && obj.type === 'Line'){ + obj.material.color.set(0x1a3a2a); + obj.material.opacity = 0.3; + obj.material.transparent = true; + } + }); - // Plot globe markers (preloaded but hidden) - plotMarkers(); + // Set initial POV + globe.pointOfView(regionPOV.world, 0); + + // Auto-rotate slowly + globe.controls().autoRotate = !lowPerfMode; + globe.controls().autoRotateSpeed = 0.3; + globe.controls().enableDamping = true; + globe.controls().dampingFactor = 0.1; + + // Stop auto-rotate on interaction, resume after 10s + let rotateTimeout; + const el = document.getElementById('globeViz'); + el.addEventListener('mousedown', () => { + globe.controls().autoRotate = false; + clearTimeout(rotateTimeout); + }); + el.addEventListener('mouseup', () => { + rotateTimeout = setTimeout(() => { + if (globe) syncGlobePerfSettings(); + }, 10000); + }); - // Start in flat mode — hide globe, show flat map - if(isFlat){ - document.getElementById('globeViz').style.display = 'none'; - document.getElementById('flatMapSvg').style.display = 'block'; - initFlatMap(); - } else { - document.getElementById('globeViz').style.display = 'block'; - document.getElementById('flatMapSvg').style.display = 'none'; - document.getElementById('projToggle').textContent = 'FLAT MODE'; - document.getElementById('mapHint').textContent = 'DRAG TO ROTATE · SCROLL TO ZOOM'; - } + // Resize handler + window.addEventListener('resize', () => { + const c = document.getElementById('mapContainer'); + if (globe) globe.width(c.clientWidth).height(c.clientHeight || 560); + }); - globeInitialized = true; + // Plot globe markers + plotMarkers(); + globeLoaded = true; + hint.textContent = oldHint; + return true; + } catch (err) { + console.error('Globe initialization failed:', err); + globe = null; + hint.textContent = '3D ERROR: LOAD FAILED'; + isFlat = true; + return false; + } finally { + globeInitPromise = null; + } + })(); + return globeInitPromise; } function plotMarkers(){ - if(!globe) return; + if (!globe) return; const points = []; const labels = []; // === 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)=>{ - const c=airCoords[i]; if(!c) return; - points.push({ - lat:c.lat, lng:c.lon, size:0.25+a.total/200, alt:0.015, - color:'rgba(100,240,200,0.8)', type:'air', priority:1, - label: a.region.replace(' Region','')+' '+a.total, - popHead: a.region, popMeta: 'Air Activity', - popText: `${a.total} aircraft tracked
No callsign: ${a.noCallsign}
High altitude: ${a.highAlt}
Top: ${a.top.slice(0,3).map(t=>t[0]+' ('+t[1]+')').join(', ')}` + if (flightsVisible) { + 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, + color:'rgba(100,240,200,0.8)', type:'air', priority:1, + label: a.region.replace(' Region','')+' '+a.total, + popHead: a.region, popMeta: 'Air Activity', + popText: `${a.total} aircraft tracked
No callsign: ${a.noCallsign}
High altitude: ${a.highAlt}
Top: ${a.top.slice(0,3).map(t=>t[0]+' ('+t[1]+')').join(', ')}` + }); + labels.push({lat:c.lat, lng:c.lon+2, text:a.region.replace(' Region','')+' '+a.total, size:0.35, color:'rgba(106,138,130,0.8)'}); }); - labels.push({lat:c.lat, lng:c.lon+2, text:a.region.replace(' Region','')+' '+a.total, size:0.35, color:'rgba(106,138,130,0.8)'}); - }); + } // === Thermal/fire (red) === D.thermal.forEach(t=>{ @@ -998,7 +1069,7 @@ }); }); } - globe.arcsData(arcs); + globe.arcsData(flightsVisible ? arcs : []); // Zoom-aware marker sizing: scale markers and labels with camera altitude const onGlobeZoom = () => { @@ -1010,17 +1081,13 @@ globe.labelSize(d => showLabels ? (d.size || 0.4) : 0); // Scale arc strokes with zoom globe.arcStroke(d => (d.stroke || 0.4) * Math.max(0.5, Math.min(1.5, 1.2 / alt))); - 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)); - } else if(alt > 1.2){ - globe.pointsData(points.filter(p => (p.priority||3) <= 2)); - } else { - globe.pointsData(points); - } + // Low-perf mode must stay sticky even after zoom changes. + globe.arcDashAnimateTime(globeArcDashAnimateTimeFor(alt)); + // Show all points at all zoom levels + globe.pointsData(points); }; if(typeof globe.onZoom==='function') globe.onZoom(onGlobeZoom); + onGlobeZoom(); } function showPopup(event,head,text,meta,lat,lng,alt){ @@ -1057,18 +1124,17 @@ flightsVisible = !flightsVisible; const btn = document.getElementById('flightToggle'); btn.classList.toggle('off', !flightsVisible); - if(!globe){ + + if (isFlat) { + if (flatG) { + flatG.selectAll('*').remove(); + drawFlatMap(); + } 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); + + if (globe) { + plotMarkers(); } } @@ -1097,21 +1163,30 @@ flatEl.style.display = 'none'; setMapLoading(true, 'Initializing 3D Globe'); requestAnimationFrame(() => { - try { - initGlobe(); + initGlobe().then((globeReady) => { + setMapLoading(false); + if(!globeReady || !globe){ + isFlat = true; + globeEl.style.display = 'none'; + flatEl.style.display = 'block'; + btn.textContent = 'GLOBE MODE'; + hint.textContent = '3D ERROR: LOAD FAILED'; + if(!flatSvg) initFlatMap(); + else { flatG.selectAll('*').remove(); drawFlatMap(); } + return; + } if(globe && typeof globe.resumeAnimation === 'function') globe.resumeAnimation(); globeEl.style.display = 'block'; + }).catch(() => { setMapLoading(false); - } catch { isFlat = true; globeEl.style.display = 'none'; flatEl.style.display = 'block'; btn.textContent = 'GLOBE MODE'; - hint.textContent = '3D LOAD FAILED · FLAT MODE'; + hint.textContent = '3D ERROR: LOAD FAILED'; if(!flatSvg) initFlatMap(); else { flatG.selectAll('*').remove(); drawFlatMap(); } - setMapLoading(false); - } + }); }); } } @@ -1129,13 +1204,8 @@ 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'); - // Priority-based visibility: hide low-priority markers at low zoom - flatG.selectAll('[data-priority]').style('display',function(){ - const p=+this.dataset.priority; - if(p<=1) return 'block'; - if(p<=2) return k>=2?'block':'none'; - return k>=3.5?'block':'none'; - }); + // Show all markers + flatG.selectAll('[data-priority]').style('display', 'block'); }); flatSvg.call(flatZoom); drawFlatMap(); @@ -1164,12 +1234,14 @@ }; // 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}]; - 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)', - ev=>showPopup(ev,a.region,`${a.total} aircraft
No callsign: ${a.noCallsign}
High alt: ${a.highAlt}`,'Air Activity'),1); - 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); - }); + if (flightsVisible) { + 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)', + ev=>showPopup(ev,a.region,`${a.total} aircraft
No callsign: ${a.noCallsign}
High alt: ${a.highAlt}`,'Air Activity'),1); + 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); + }); + } // 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)', @@ -1217,25 +1289,27 @@ g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)'); }); // Flight corridors - 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'); - for(let i=0;i0.15?'rgba(255,95,99,0.4)':ncR>0.05?'rgba(255,184,76,0.35)':'rgba(100,240,200,0.25)'; - const interp=d3.geoInterpolate([from.lon,from.lat],[to.lon,to.lat]); - const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); - const feat={type:'Feature',geometry:{type:'LineString',coordinates:coords}}; - 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))); - }} - D.air.forEach((a,i)=>{if(!airCoordsFlight[i]||a.total<25)return;hubs.forEach(hub=>{ - if(Math.abs(airCoordsFlight[i].lat-hub.lat)+Math.abs(airCoordsFlight[i].lon-hub.lon)<20)return; - const interp=d3.geoInterpolate([airCoordsFlight[i].lon,airCoordsFlight[i].lat],[hub.lon,hub.lat]); - const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); - 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); - })}); + if (flightsVisible) { + 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'); + for(let i=0;i0.15?'rgba(255,95,99,0.4)':ncR>0.05?'rgba(255,184,76,0.35)':'rgba(100,240,200,0.25)'; + const interp=d3.geoInterpolate([from.lon,from.lat],[to.lon,to.lat]); + const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); + const feat={type:'Feature',geometry:{type:'LineString',coordinates:coords}}; + 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))); + }} + D.air.forEach((a,i)=>{if(!airCoordsFlight[i]||a.total<25)return;hubs.forEach(hub=>{ + if(Math.abs(airCoordsFlight[i].lat-hub.lat)+Math.abs(airCoordsFlight[i].lon-hub.lon)<20)return; + const interp=d3.geoInterpolate([airCoordsFlight[i].lon,airCoordsFlight[i].lat],[hub.lon,hub.lat]); + const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40)); + 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); + })}); + } } // Update setRegion for flat mode