Download globe-math.mjs from mmrech/ASCEND: direct link, hf CLI and curl.
- Browser
- Download file 3.42 kB
-
https://huggingface.co/spaces/mmrech/ASCEND/resolve/main/globe-math.mjs
- Command line
-
hf download hf://spaces/mmrech/ASCEND/globe-math.mjs
-
curl -L -o globe-math.mjs https://huggingface.co/spaces/mmrech/ASCEND/resolve/main/globe-math.mjs
3.42 kB
| // Three.js sphere UVs place Greenwich at u=.25; geographic textures at u=.5. | |
| export const EQUIRECTANGULAR_OFFSET=.25; | |
| const RAD=Math.PI/180; | |
| export function normaliseLongitude(lon){return lon>=-180&&lon<180?lon:((lon+180)%360+360)%360-180;} | |
| export function lonLatToVector(lon,lat,radius=1){ | |
| const a=lon*RAD,b=lat*RAD,c=Math.cos(b); | |
| return {x:radius*c*Math.sin(a),y:radius*Math.sin(b),z:radius*c*Math.cos(a)}; | |
| } | |
| export function cameraFocus(country){return {longitude:normaliseLongitude(country.lon),latitude:Math.max(-75,Math.min(75,country.lat))};} | |
| export function isMarkerVisible(lon,lat,camera,distance=3){ | |
| const p=lonLatToVector(lon,lat),c=lonLatToVector(camera.longitude,camera.latitude); | |
| return p.x*c.x+p.y*c.y+p.z*c.z>1/distance; | |
| } | |
| export function metricFraction(value,min,max){ | |
| if(value===null||value===undefined||!Number.isFinite(value))return null; | |
| if(max===min)return .5; | |
| return Math.max(0,Math.min(1,(value-min)/(max-min))); | |
| } | |
| export function makeGraticule(spacing=30,step=3,radius=1.002){ | |
| const lines=[]; | |
| for(let lon=-180;lon<180;lon+=spacing)for(let lat=-90;lat<90;lat+=step) | |
| lines.push([lonLatToVector(lon,lat,radius),lonLatToVector(lon,Math.min(90,lat+step),radius)]); | |
| for(let lat=-60;lat<=60;lat+=spacing)for(let lon=-180;lon<180;lon+=step) | |
| lines.push([lonLatToVector(lon,lat,radius),lonLatToVector(Math.min(180,lon+step),lat,radius)]); | |
| return lines; | |
| } | |
| /** Inverse of lonLatToVector for a unit-sphere hit point. */ | |
| export function vectorToLonLat(x,y,z){ | |
| const r=Math.hypot(x,y,z)||1; | |
| return {lon:normaliseLongitude(Math.atan2(x/r,z/r)/RAD),lat:Math.asin(Math.max(-1,Math.min(1,y/r)))/RAD}; | |
| } | |
| /** Even-odd point-in-ring test on [lon,lat] pairs (planar; adequate for 1:110m reference geometry). */ | |
| export function pointInRing(lon,lat,ring){ | |
| let inside=false; | |
| for(let i=0,j=ring.length-1;i<ring.length;j=i++){ | |
| const [xi,yi]=ring[i],[xj,yj]=ring[j]; | |
| if((yi>lat)!==(yj>lat)&&lon<(xj-xi)*(lat-yi)/((yj-yi)||1e-12)+xi)inside=!inside; | |
| } | |
| return inside; | |
| } | |
| /** polygons: array of polygons, each an array of rings (outer first, then holes). */ | |
| export function pointInPolygons(lon,lat,polygons){ | |
| for(const rings of polygons){ | |
| if(!rings.length||!pointInRing(lon,lat,rings[0]))continue; | |
| let inHole=false;for(let k=1;k<rings.length;k++)if(pointInRing(lon,lat,rings[k])){inHole=true;break;} | |
| if(!inHole)return true; | |
| } | |
| return false; | |
| } | |
| /** Fraction in [0,1] on a linear or log10 scale; null for missing or non-positive log values. */ | |
| export function scaleFraction(value,min,max,scale='linear'){ | |
| if(value===null||value===undefined||!Number.isFinite(value))return null; | |
| if(scale==='log'){if(!(value>0)||!(min>0)||!(max>0))return null;const a=Math.log10(min),b=Math.log10(max);return b===a?.5:Math.max(0,Math.min(1,(Math.log10(value)-a)/(b-a)));} | |
| return metricFraction(value,min,max); | |
| } | |
| /** Sequential ramp used by the choropleth and legend: deep teal → mint → pale lime. Returns CSS hex. */ | |
| export const RAMP=['#1d4a50','#226e6c','#2f9283','#63b79a','#a4d9b4','#e3f6d2']; | |
| export function rampColor(t){ | |
| if(t===null||t===undefined)return '#2b3f44'; | |
| const n=RAMP.length-1,x=Math.max(0,Math.min(1,t))*n,i=Math.min(n-1,Math.floor(x)),f=x-i; | |
| const a=hex(RAMP[i]),b=hex(RAMP[i+1]); | |
| return '#'+[0,1,2].map(k=>Math.round(a[k]+(b[k]-a[k])*f).toString(16).padStart(2,'0')).join(''); | |
| } | |
| function hex(h){return [1,3,5].map(i=>parseInt(h.slice(i,i+2),16));} | |