mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 03:14:00 +00:00
feat(wind): add keyless NOAA GFS wind layer with animated particles
Add a Wind layer animating the global 10 m wind field from NOAA GFS. The /api/wind proxy selects the latest available 0.25° cycle, reads its .idx inventory, byte-range fetches only the UGRD/VGRD 10 m GRIB2 messages (~1 MB each instead of the whole ~500 MB file), decodes them with ecCodes (WASM), resamples to a compact grid (1° default), and serves a manifest plus a Float32 U/V payload cached per cycle for an hour (single-flight, last-good on failure). The client renders nullschool-style particles in a canvas overlay: bilinear field sampling with longitude wrap, per-frame advection, Cesium camera projection, and globe-occlusion culling via EllipsoidalOccluder. Validated end-to-end against the real 2026-09-14 06z cycle on Node 25 (180x91 grid at 2°, 131,040-byte payload, plausible mean |u|/|v|). The WASM decoder requires Node >=24, which the repo already requires. Registered as `wind` (token k) with attribution and docs. Verified: npm test (3412 pass, 0 fail), npm run check:boundaries.
This commit is contained in:
1 parent
844c25212c
commit
deb34ebe35
28 files changed
+1122
-3
No files matched your search
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Resample a GRIB wind grid into a compact equirectangular grid.
|
||||
*
|
||||
* Source grids are north-to-south (`la1` at the top, `dj` increasing toward the
|
||||
* south) and start at `lo1`. Longitude wraps across the seam; latitude clamps.
|
||||
*
|
||||
* @param {{u: ArrayLike<number>, v: ArrayLike<number>, ni: number, nj: number,
|
||||
* lo1: number, la1: number, di: number, dj: number, dx?: number, dy?: number}} input
|
||||
* @returns {{nx: number, ny: number, lo1: number, la1: number, dx: number,
|
||||
* dy: number, u: Float32Array, v: Float32Array}}
|
||||
*/
|
||||
export function resampleWindGrid({
|
||||
u,
|
||||
v,
|
||||
ni,
|
||||
nj,
|
||||
lo1,
|
||||
la1,
|
||||
di,
|
||||
dj,
|
||||
dx = 1,
|
||||
dy = 1,
|
||||
}) {
|
||||
const nx = Math.round(360 / dx);
|
||||
const ny = Math.round(180 / dy) + 1;
|
||||
const outU = new Float32Array(nx * ny);
|
||||
const outV = new Float32Array(nx * ny);
|
||||
const sample = (source, lon, lat) => {
|
||||
const x = ((lon - lo1) / di + ni) % ni;
|
||||
const y = Math.max(0, Math.min(nj - 1, (la1 - lat) / dj));
|
||||
const x0 = Math.floor(x);
|
||||
const x1 = (x0 + 1) % ni;
|
||||
const y0 = Math.floor(y);
|
||||
const y1 = Math.min(nj - 1, y0 + 1);
|
||||
const fx = x - x0;
|
||||
const fy = y - y0;
|
||||
const top = source[y0 * ni + x0] * (1 - fx) + source[y0 * ni + x1] * fx;
|
||||
const bottom = source[y1 * ni + x0] * (1 - fx) + source[y1 * ni + x1] * fx;
|
||||
return top * (1 - fy) + bottom * fy;
|
||||
};
|
||||
for (let y = 0; y < ny; y += 1) {
|
||||
for (let x = 0; x < nx; x += 1) {
|
||||
const index = y * nx + x;
|
||||
const lon = x * dx;
|
||||
const lat = 90 - y * dy;
|
||||
outU[index] = sample(u, lon, lat);
|
||||
outV[index] = sample(v, lon, lat);
|
||||
}
|
||||
}
|
||||
return { nx, ny, lo1: 0, la1: 90, dx, dy, u: outU, v: outV };
|
||||
}
|
||||
Reference in new issue
Block a user