177 lines
23 KiB
JavaScript
177 lines
23 KiB
JavaScript
(function () {
|
||
'use strict';
|
||
|
||
const format = value => Number(Number(value).toPrecision(6)).toString();
|
||
const palette = [[68,1,84],[59,82,139],[33,145,140],[94,201,98],[253,231,37]];
|
||
|
||
window.ExoVisualization = function (container) {
|
||
const root = container;
|
||
let index = null, frameUrl = null, seriesUrl = null, frameAbort = null, seriesAbort = null;
|
||
let frameToken = 0, seriesToken = 0, frame = null, chart = null, gl = null, texture = null, program = null;
|
||
let raf = 0, observer = null, destroyed = false, mode = 'map', time = 0, level = 0, field = '', min = 0, max = 1;
|
||
let rotation = 0, pitch = 0, drag = null, bounds = null, textureDirty = true, software = false;
|
||
let composite = null, rgbFrames = null, rendering = 'field';
|
||
const raster = document.createElement('canvas');
|
||
const rasterContext = raster.getContext('2d');
|
||
root.innerHTML = `<div class="viz-root"><div class="viz-controls"><label>View <select data-view><option value="field">Single field</option><option value="rgb">Temperature / rainfall / elevation</option></select></label><label>Field <select data-field></select></label><label>Time <input data-time type="range" min="0" max="0" value="0"></label><output data-time-label></output><label>Level <select data-level></select></label><button type="button" data-mode>Globe</button><button type="button" data-play aria-pressed="false">Play</button></div><div class="viz-view"><canvas class="viz-map" tabindex="0" aria-label="Spatial field map. Use arrow keys to inspect cells."></canvas><canvas class="viz-globe" tabindex="0" hidden aria-label="Spatial field globe. Drag or use arrow keys to rotate."></canvas><div class="viz-empty" role="status">No spatial data</div><div class="viz-tooltip" hidden></div></div><div class="viz-footer"><div class="viz-legend"><span data-min></span><div></div><span data-max></span></div><div class="viz-rgb-legend" hidden></div><output class="viz-inspector" data-inspector>Move over the field to inspect a value.</output><canvas class="viz-chart" aria-label="Global mean by time"></canvas></div></div>`;
|
||
const $ = s => root.querySelector(s), map = $('.viz-map'), globe = $('.viz-globe'), chartCanvas = $('.viz-chart'), empty = $('.viz-empty'), tip = $('.viz-tooltip');
|
||
const viewSelect = $('[data-view]'), fieldSelect = $('[data-field]'), levelSelect = $('[data-level]'), timeInput = $('[data-time]');
|
||
const context = map.getContext('2d');
|
||
$('.viz-legend div').style.background='linear-gradient(90deg,'+palette.map(c=>'rgb('+c.join(',')+')').join(',')+')';
|
||
function currentField() { return (index && index.fields || []).find(x => x.name === field); }
|
||
function isComposite() { return rendering === 'rgb' && Boolean(composite); }
|
||
function showLegend() {
|
||
$('.viz-legend').hidden=isComposite();$('.viz-rgb-legend').hidden=!isComposite();
|
||
const legend=$('.viz-rgb-legend');legend.replaceChildren();
|
||
if(!isComposite())return;
|
||
composite.channels.forEach((channel,i)=>{const row=document.createElement('div'),name=document.createElement('span'),bar=document.createElement('i'),range=document.createElement('span');name.textContent=channel.label;range.textContent=`${format(channel.min)}–${format(channel.max)} ${channel.unit}`;bar.style.background=`linear-gradient(90deg,#000,${['#ff0000','#00ff00','#0000ff'][i]})`;row.append(name,bar,range);legend.append(row);});
|
||
}
|
||
function setStatus(text) { empty.textContent = text; empty.hidden = false; }
|
||
function color(v) {
|
||
if (!Number.isFinite(v)) return [0,0,0,0];
|
||
const t = max === min ? .5 : Math.max(0, Math.min(1, (v-min)/(max-min)));
|
||
const position=t*(palette.length-1),i=Math.min(palette.length-2,Math.floor(position)),fraction=position-i;
|
||
return palette[i].map((value,c)=>value+(palette[i+1][c]-value)*fraction);
|
||
}
|
||
function sizeCanvas(canvas,limit=Infinity) { const r=canvas.getBoundingClientRect(),d=Math.min(window.devicePixelRatio||1,2),scale=Math.min(1,limit/Math.max(r.width*d,r.height*d)),w=Math.max(1,Math.round(r.width*d*scale)),h=Math.max(1,Math.round(r.height*d*scale));if(canvas.width!==w)canvas.width=w;if(canvas.height!==h)canvas.height=h;return d*scale; }
|
||
function coords() { const lat=index&&index.lat||[], lon=index&&index.lon||[]; return {lat,lon}; }
|
||
function nearest(a,v,period=0) { let best=0,dist=Infinity;for(let i=0;i<a.length;i++){let d=Math.abs(Number(a[i])-v);if(period){d%=period;d=Math.min(d,period-d);}if(d<dist){dist=d;best=i;}}return best; }
|
||
function updateRaster() {
|
||
if (!frame || !textureDirty) return;
|
||
const rows=frame.length, cols=frame[0].length, height=Math.max(256,rows*2);
|
||
const {lat,lon}=coords(), step=lon.length>1?Number(lon[1])-Number(lon[0]):360;
|
||
bounds={latMin:-90,latMax:90,lonMin:Number(lon[0])-step/2,lonMax:Number(lon[0])+360-step/2,rows,cols};
|
||
raster.width=cols; raster.height=height;
|
||
const image=rasterContext.createImageData(cols,height),rgb=isComposite(),nativeColors=rgb?new Uint8ClampedArray(rows*cols*4):null;
|
||
if(rgb)for(let row=0;row<rows;row++)for(let x=0;x<cols;x++){
|
||
const p=(row*cols+x)*4,red=window.ExoComposite.normalise(rgbFrames[0][row][x],composite.channels[0]),green=window.ExoComposite.normalise(rgbFrames[1][row][x],composite.channels[1]),blue=window.ExoComposite.normalise(rgbFrames[2][row][x],composite.channels[2]);
|
||
nativeColors[p]=red*255;nativeColors[p+1]=green*255;nativeColors[p+2]=blue*255;nativeColors[p+3]=Number.isFinite(red)&&Number.isFinite(green)&&Number.isFinite(blue)?255:0;
|
||
}
|
||
for(let y=0;y<height;y++) {
|
||
const row=nearest(lat,90-(y+.5)/height*180);
|
||
if(rgb){image.data.set(nativeColors.subarray(row*cols*4,(row+1)*cols*4),y*cols*4);continue;}
|
||
for(let x=0;x<cols;x++) {
|
||
const c=color(Number(frame[row][x])),p=(y*cols+x)*4;
|
||
image.data[p]=c[0];image.data[p+1]=c[1];image.data[p+2]=c[2];image.data[p+3]=c.length===4?c[3]:255;
|
||
}
|
||
}
|
||
rasterContext.putImageData(image,0,0);
|
||
}
|
||
function drawMap() {
|
||
if (destroyed || !frame || mode !== 'map') return;
|
||
updateRaster(); sizeCanvas(map); context.clearRect(0,0,map.width,map.height);
|
||
context.imageSmoothingEnabled=false; context.drawImage(raster,0,0,map.width,map.height);
|
||
}
|
||
function initGL() {
|
||
if(gl)return true;
|
||
gl=globe.getContext('webgl',{alpha:false,antialias:true})||globe.getContext('experimental-webgl');
|
||
if(!gl)return false;
|
||
const vs='attribute vec2 p; varying vec2 uv; void main(){uv=(p+1.0)*0.5;gl_Position=vec4(p,0.,1.);}';
|
||
const fs='precision mediump float; varying vec2 uv; uniform sampler2D tex; uniform float aspect; uniform float rot; uniform float pitch; uniform float offset; uniform float shade; void main(){vec2 q=(uv*2.0-1.0);q.x*=max(aspect,1.);q.y/=min(aspect,1.);float r=length(q);if(r>1.){gl_FragColor=vec4(0.035,0.055,0.08,1.);return;}float z=sqrt(max(0.,1.-r*r));vec3 n=vec3(q.x,q.y*cos(pitch)+z*sin(pitch),z*cos(pitch)-q.y*sin(pitch));float lon=atan(n.x,n.z)+rot;float lat=asin(clamp(n.y,-1.,1.));vec2 t=vec2(fract(lon/6.2831853+offset),0.5-lat/3.14159265);vec4 c=texture2D(tex,t);float light=0.35+0.65*max(0.,dot(normalize(vec3(q,z)),normalize(vec3(-.4,.5,1.))));gl_FragColor=vec4(mix(vec3(.035,.055,.08),c.rgb*mix(1.,light,shade),c.a),1.);}';
|
||
function shader(type,src){const s=gl.createShader(type);gl.shaderSource(s,src);gl.compileShader(s);if(!gl.getShaderParameter(s,gl.COMPILE_STATUS))return null;return s;}
|
||
const a=shader(gl.VERTEX_SHADER,vs),b=shader(gl.FRAGMENT_SHADER,fs);if(!a||!b)return false;
|
||
program=gl.createProgram();gl.attachShader(program,a);gl.attachShader(program,b);gl.linkProgram(program);if(!gl.getProgramParameter(program,gl.LINK_STATUS))return false;
|
||
const buf=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,buf);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);gl.useProgram(program);const loc=gl.getAttribLocation(program,'p');gl.enableVertexAttribArray(loc);gl.vertexAttribPointer(loc,2,gl.FLOAT,false,0,0);texture=gl.createTexture();gl.bindTexture(gl.TEXTURE_2D,texture);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MIN_FILTER,gl.NEAREST);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_MAG_FILTER,gl.NEAREST);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_S,gl.CLAMP_TO_EDGE);gl.texParameteri(gl.TEXTURE_2D,gl.TEXTURE_WRAP_T,gl.CLAMP_TO_EDGE);return true;
|
||
}
|
||
function drawGlobe() {
|
||
if(destroyed||mode!=='globe'||!frame)return;
|
||
if(software||!initGL()){software=true;globe.hidden=true;map.hidden=false;drawSoftwareGlobe();return;}
|
||
updateRaster(); sizeCanvas(globe,640);
|
||
gl.viewport(0,0,globe.width,globe.height);gl.bindTexture(gl.TEXTURE_2D,texture);
|
||
if(textureDirty){gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,false);gl.texImage2D(gl.TEXTURE_2D,0,gl.RGBA,raster.width,raster.height,0,gl.RGBA,gl.UNSIGNED_BYTE,rasterContext.getImageData(0,0,raster.width,raster.height).data);textureDirty=false;}
|
||
gl.useProgram(program);gl.uniform1f(gl.getUniformLocation(program,'shade'),isComposite()?0:1);gl.uniform1f(gl.getUniformLocation(program,'aspect'),globe.width/globe.height);gl.uniform1f(gl.getUniformLocation(program,'rot'),rotation);gl.uniform1f(gl.getUniformLocation(program,'pitch'),pitch);gl.uniform1f(gl.getUniformLocation(program,'offset'),.5/raster.width-Number(index.lon[0])/360);gl.drawArrays(gl.TRIANGLE_STRIP,0,4);
|
||
}
|
||
function drawSoftwareGlobe(){
|
||
updateRaster();sizeCanvas(map,320);
|
||
const w=map.width,h=map.height,aspect=w/h,image=context.createImageData(w,h),source=rasterContext.getImageData(0,0,raster.width,raster.height).data,cp=Math.cos(pitch),sp=Math.sin(pitch),offset=.5/raster.width-Number(index.lon[0])/360,lightLength=Math.sqrt(1.41);
|
||
for(let y=0;y<h;y++)for(let x=0;x<w;x++){
|
||
const p=(y*w+x)*4,qx=((x+.5)/w*2-1)*Math.max(aspect,1),qy=(1-(y+.5)/h*2)/Math.min(aspect,1),squared=qx*qx+qy*qy;
|
||
image.data[p]=9;image.data[p+1]=14;image.data[p+2]=20;image.data[p+3]=255;
|
||
if(squared>1)continue;
|
||
const z=Math.sqrt(1-squared),ny=qy*cp+z*sp,nz=z*cp-qy*sp,latitude=Math.asin(Math.max(-1,Math.min(1,ny))),longitude=Math.atan2(qx,nz)+rotation,u=((longitude/(2*Math.PI)+offset)%1+1)%1,v=.5-latitude/Math.PI,si=(Math.min(raster.height-1,Math.floor(v*raster.height))*raster.width+Math.min(raster.width-1,Math.floor(u*raster.width)))*4,light=isComposite()?1:.35+.65*Math.max(0,(-.4*qx+.5*qy+z)/lightLength),alpha=source[si+3]/255;
|
||
for(let channel=0;channel<3;channel++)image.data[p+channel]=image.data[p+channel]*(1-alpha)+source[si+channel]*light*alpha;
|
||
}
|
||
context.putImageData(image,0,0);textureDirty=false;
|
||
}
|
||
function redraw(){if(mode==='map')drawMap();else drawGlobe();}
|
||
function drawChart(values) {
|
||
const c=chartCanvas.getContext('2d'),d=sizeCanvas(chartCanvas),w=chartCanvas.width,h=chartCanvas.height;
|
||
c.clearRect(0,0,w,h);if(!values?.mean)return;
|
||
const valid=values.mean.filter(v=>v!==null&&Number.isFinite(Number(v))).map(Number);
|
||
if(!valid.length)return;
|
||
const lo=Math.min(...valid),hi=Math.max(...valid),left=60*d,right=w-12*d,top=18*d,bottom=h-20*d;
|
||
const times=values.time||index.time,start=Number(times[0]),end=Number(times.at(-1));
|
||
c.fillStyle='#a8b7b9';c.font=`${10*d}px sans-serif`;
|
||
c.fillText(`Global mean ${isComposite()?'temperature ':''}${values.unit||''}`,left,12*d);
|
||
c.fillText(hi.toPrecision(4),0,top+8*d);c.fillText(lo.toPrecision(4),0,bottom);
|
||
c.fillText(`${format(start)} ${values.time_unit||''}`,left,h-3*d);
|
||
c.textAlign='right';c.fillText(format(end),right,h-3*d);c.textAlign='left';
|
||
c.strokeStyle='#65b7d8';c.lineWidth=2*d;c.beginPath();let gap=true;
|
||
values.mean.forEach((v,i)=>{if(v===null||!Number.isFinite(Number(v))){gap=true;return;}
|
||
const x=left+(Number(times[i])-start)/(end-start||1)*(right-left),y=bottom-(Number(v)-lo)/(hi-lo||1)*(bottom-top);
|
||
if(gap)c.moveTo(x,y);else c.lineTo(x,y);gap=false;
|
||
if(values.mean.length===1)c.rect(x-2*d,y-2*d,4*d,4*d);
|
||
});c.stroke();
|
||
}
|
||
async function readPlane(meta,signal) {
|
||
const response=await fetch(frameUrl(meta.name,Math.min(time,meta.shape[0]-1),isComposite()?0:level),{signal,credentials:'same-origin'});
|
||
if(!response.ok)throw Error(`Frame request failed (${response.status})`);
|
||
const raw=new Float32Array(await response.arrayBuffer()),rows=meta.shape[2],cols=meta.shape[3];
|
||
if(!rows||!cols||raw.length!==rows*cols)throw Error('Frame dimensions do not match the dataset index');
|
||
return Array.from({length:rows},(_,r)=>raw.subarray(r*cols,(r+1)*cols));
|
||
}
|
||
async function loadFrame() {
|
||
if(!index||!field||!frameUrl)return;
|
||
const token=++frameToken;if(frameAbort)frameAbort.abort();frameAbort=new AbortController();frame=null;rgbFrames=null;tip.hidden=true;
|
||
$('[data-inspector]').textContent='Loading field…';setStatus('Loading field…');
|
||
const rgb=isComposite(),metas=rgb?composite.channels.map(channel=>index.fields.find(meta=>meta.name===channel.name)):[currentField()];
|
||
try{
|
||
const planes=await Promise.all(metas.map(meta=>readPlane(meta,frameAbort.signal)));
|
||
if(token!==frameToken||destroyed)return;
|
||
frame=planes[0];if(rgb)rgbFrames=planes;textureDirty=true;
|
||
const fieldMeta=metas[0];min=fieldMeta.min==null?NaN:Number(fieldMeta.min);max=fieldMeta.max==null?NaN:Number(fieldMeta.max);
|
||
$('[data-min]').textContent=Number.isFinite(min)?min.toPrecision(4):'';$('[data-max]').textContent=Number.isFinite(max)?max.toPrecision(4):'';
|
||
empty.hidden=true;redraw();if(inspected)inspectCell(...inspected);else $('[data-inspector]').textContent='Tap or point at the field to inspect a value.';
|
||
}catch(e){if(e.name!=='AbortError'&&token===frameToken&&!destroyed){frameAbort.abort();const message=e.message||'Could not load field';setStatus(message);$('[data-inspector]').textContent=message;}}
|
||
}
|
||
async function loadSeries(){if(!index||!field||!seriesUrl)return;const token=++seriesToken;if(seriesAbort)seriesAbort.abort();seriesAbort=new AbortController();chart=null;drawChart(null);try{const response=await fetch(seriesUrl(field,level),{signal:seriesAbort.signal,credentials:'same-origin'});if(!response.ok)throw Error(`Series request failed (${response.status})`);const data=await response.json();if(token===seriesToken&&!destroyed){if(!data.ok)throw Error(data.error||'Could not load global means');chart=data.data;drawChart(chart);}}catch(e){if(e.name!=='AbortError'&&token===seriesToken)drawChart(null);} }
|
||
function changed(){
|
||
const previous=rendering;rendering=viewSelect.value;const viewChanged=previous!==rendering;
|
||
const next=isComposite()?composite.channels[0].name:fieldSelect.value,old=field,oldLevel=level;field=next;
|
||
fieldSelect.closest('label').hidden=isComposite();showLegend();
|
||
if(next!==old||!levelSelect.options.length){
|
||
levelSelect.innerHTML='';const count=currentField()?.shape[1]||1;
|
||
for(let i=0;i<count;i++){const option=document.createElement('option');option.value=i;option.textContent=String(index.levels?.[i]??i);levelSelect.append(option);}
|
||
levelSelect.closest('label').hidden=count===1||isComposite();
|
||
}
|
||
level=Number(levelSelect.value)||0;time=Number(timeInput.value)||0;
|
||
const times=index?.time||[];$('[data-time-label]').textContent=times[time]==null?String(time):`${format(times[time])} ${index.time_unit||''}`;
|
||
loadFrame();if(next!==old||level!==oldLevel||viewChanged||!chart)loadSeries();
|
||
}
|
||
viewSelect.addEventListener('change',changed);fieldSelect.addEventListener('change',changed);levelSelect.addEventListener('change',changed);timeInput.addEventListener('input',changed);
|
||
$('[data-mode]').addEventListener('click',()=>{mode=mode==='map'?'globe':'map';map.hidden=mode!=='map'&&!software;globe.hidden=mode!=='globe'||software;$('[data-mode]').textContent=mode==='map'?'Globe':'Map';map.setAttribute('aria-label',mode==='map'?'Spatial field map. Use arrow keys to inspect cells.':'Spatial field globe. Drag or use arrow keys to rotate.');redraw();});
|
||
let playTimer=0;const play=$('[data-play]');function stopPlayback(){if(playTimer)clearInterval(playTimer);playTimer=0;play.textContent='Play';play.setAttribute('aria-pressed','false');}play.addEventListener('click',()=>{if(playTimer){stopPlayback();return;}play.textContent='Pause';play.setAttribute('aria-pressed','true');playTimer=setInterval(()=>{if(document.hidden)return;const n=Number(timeInput.max);timeInput.value=String((Number(timeInput.value)+1)%(n+1));changed();},600);});
|
||
let inspected=null;
|
||
function inspectCell(ri,ci){if(!frame)return;inspected=[ri,ci];const value=Number(frame[ri][ci]),{lat,lon}=coords(),position=`${Number(lat[ri]).toFixed(2)}°, ${Number(lon[ci]).toFixed(2)}°`;
|
||
const text=isComposite()?position+'\n'+composite.channels.map((channel,i)=>{const v=Number(rgbFrames[i][ri][ci])*channel.factor;return`${channel.label}: ${Number.isFinite(v)?v.toPrecision(5)+' '+channel.unit:'missing'}`;}).join('\n'):`${position} · ${Number.isFinite(value)?value.toPrecision(5)+(currentField().unit?' '+currentField().unit:''):'missing'}`;tip.textContent=text;$('[data-inspector]').textContent=text;}
|
||
function inspectPointer(event,canvas,latitude,longitude){const {lat,lon}=coords();inspectCell(nearest(lat,latitude),nearest(lon,longitude,360));const r=canvas.getBoundingClientRect();tip.hidden=false;tip.style.left=Math.max(0,Math.min(r.width-tip.offsetWidth-8,event.clientX-r.left+12))+'px';tip.style.top=Math.max(0,Math.min(r.height-tip.offsetHeight-8,event.clientY-r.top+12))+'px';}
|
||
function mapPointer(event){if(mode==='globe'){globePointer(event,map);return;}if(!frame||!bounds)return;const r=map.getBoundingClientRect(),x=(event.clientX-r.left)/r.width,y=(event.clientY-r.top)/r.height;inspectPointer(event,map,90-y*180,bounds.lonMin+x*360);}
|
||
map.addEventListener('pointermove',mapPointer);
|
||
map.addEventListener('pointerdown',event=>{if(mode==='globe')startDrag(event,map);});
|
||
map.addEventListener('pointerup',event=>{drag=null;mapPointer(event);});map.addEventListener('pointercancel',()=>{drag=null;});
|
||
map.addEventListener('pointerleave',()=>{tip.hidden=true;});
|
||
map.addEventListener('keydown',event=>{if(mode==='globe'){rotateKeys(event);return;}if(!frame)return;let [ri,ci]=inspected||[Math.floor(frame.length/2),Math.floor(frame[0].length/2)];if(event.key==='ArrowUp')ri--;else if(event.key==='ArrowDown')ri++;else if(event.key==='ArrowLeft')ci--;else if(event.key==='ArrowRight')ci++;else return;event.preventDefault();inspectCell(Math.max(0,Math.min(frame.length-1,ri)),(ci+frame[0].length)%frame[0].length);});
|
||
function startDrag(event,canvas){drag={x:event.clientX,y:event.clientY,rot:rotation,pitch};canvas.setPointerCapture(event.pointerId);tip.hidden=true;}
|
||
function globePointer(event,canvas){if(drag){rotation=drag.rot+(event.clientX-drag.x)/Math.max(1,canvas.clientWidth)*Math.PI*2;pitch=Math.max(-Math.PI/2,Math.min(Math.PI/2,drag.pitch+(event.clientY-drag.y)/Math.max(1,canvas.clientHeight)*Math.PI));if(!raf)raf=requestAnimationFrame(()=>{raf=0;redraw();});return;}if(!frame||mode!=='globe')return;const r=canvas.getBoundingClientRect(),x=((event.clientX-r.left)/r.width*2-1)*Math.max(1,r.width/r.height),y=(1-(event.clientY-r.top)/r.height*2)/Math.min(1,r.width/r.height),squared=x*x+y*y;if(squared>1){tip.hidden=true;return;}const z=Math.sqrt(1-squared),ny=y*Math.cos(pitch)+z*Math.sin(pitch),nz=z*Math.cos(pitch)-y*Math.sin(pitch);inspectPointer(event,canvas,Math.asin(Math.max(-1,Math.min(1,ny)))*180/Math.PI,(Math.atan2(x,nz)+rotation)*180/Math.PI);}
|
||
globe.addEventListener('pointerdown',event=>startDrag(event,globe));globe.addEventListener('pointermove',event=>globePointer(event,globe));
|
||
globe.addEventListener('pointerup',event=>{drag=null;globePointer(event,globe);});globe.addEventListener('pointercancel',()=>{drag=null;});globe.addEventListener('pointerleave',()=>{tip.hidden=true;});
|
||
function rotateKeys(event){if(event.key==='ArrowLeft')rotation-=.1;else if(event.key==='ArrowRight')rotation+=.1;else if(event.key==='ArrowUp')pitch+=.1;else if(event.key==='ArrowDown')pitch-=.1;else if(event.key==='Home'){rotation=0;pitch=0;}else return;event.preventDefault();pitch=Math.max(-Math.PI/2,Math.min(Math.PI/2,pitch));redraw();}
|
||
globe.addEventListener('keydown',rotateKeys);
|
||
globe.addEventListener('webglcontextlost',event=>{event.preventDefault();gl=null;program=null;texture=null;textureDirty=true;software=true;redraw();});
|
||
globe.addEventListener('webglcontextrestored',()=>{software=false;textureDirty=true;redraw();});
|
||
const visibility=()=>{if(!document.hidden)redraw();};
|
||
observer=new ResizeObserver(()=>{if(!document.hidden){redraw();drawChart(chart);}});observer.observe(root);document.addEventListener('visibilitychange',visibility);
|
||
return { setDataset(next,frameFn,seriesFn,options={}){stopPlayback();inspected=null;tip.hidden=true;$('[data-inspector]').textContent='Point at the map to inspect a value.';index=next||null;composite=window.ExoComposite?.create(index,options.gravity)||null;viewSelect.querySelector('[value="rgb"]').disabled=!composite;viewSelect.closest('label').hidden=!composite;if(!composite)viewSelect.value='field';rendering=viewSelect.value;rgbFrames=null;frameUrl=frameFn;seriesUrl=seriesFn;frame=null;chart=null;textureDirty=true;if(frameAbort)frameAbort.abort();if(seriesAbort)seriesAbort.abort();frameToken++;seriesToken++;const previousField=fieldSelect.value,previousLevel=level;fieldSelect.innerHTML='';levelSelect.innerHTML='';const fields=index&&index.fields||[];for(const f of fields){const o=document.createElement('option');o.value=f.name;o.textContent=(f.label||f.name).replaceAll('_',' ')+(f.unit?' ('+f.unit+')':'');fieldSelect.appendChild(o);}field=(fields.find(f=>f.name===previousField)||fields.find(f=>f.name==='ts')||fields.find(f=>f.name==='elevation')||fields[0])?.name||'';fieldSelect.value=field;const count=currentField()?.shape[1]||0;levelSelect.closest('label').hidden=count<=1;for(let i=0;i<count;i++){const o=document.createElement('option');o.value=String(i);o.textContent=(index.levels&&index.levels[i]!=null)?String(index.levels[i]):String(i);levelSelect.appendChild(o);}const times=index&&index.time||[];timeInput.disabled=times.length<=1;$('[data-play]').disabled=times.length<=1;timeInput.max=String(Math.max(0,times.length-1));timeInput.value='0';time=0;level=Math.min(previousLevel,Math.max(0,count-1));levelSelect.value=String(level);empty.hidden=fields.length>0;setStatus(fields.length?'Loading field…':'No spatial data');if(fields.length)changed();else{drawChart(null);}}, destroy(){destroyed=true;if(frameAbort)frameAbort.abort();if(seriesAbort)seriesAbort.abort();if(observer)observer.disconnect();if(raf)cancelAnimationFrame(raf);stopPlayback();document.removeEventListener('visibilitychange',visibility);if(gl){gl.deleteTexture(texture);gl.deleteProgram(program);}root.innerHTML='';} };
|
||
};
|
||
})();
|