Commit Graph
4 Commits
Author SHA1 Message Date
Gustavo Beneduzi a2d38391e6 merge: wind overlay visibility fixes from feat/wind-gfs 2026-09-14 14:03:11 -03:00
Gustavo Beneduzi b7033d86c2 fix(wind): make the particle overlay actually visible
Live browser QA of the Wind layer surfaced several rendering defects that
unit tests with fakes did not catch:

- The trail fade used a translucent black fill (source-over), which raises
  alpha instead of erasing; combined with additive strokes the overlay
  saturated into a solid white disk. Fade with destination-out instead, and
  draw strokes with normal blending.
- Particles were seeded uniformly over the whole globe, so at regional zoom
  almost none fell on screen. Seed inside the camera's visible rectangle.
- A non-finite particle position (polar divide-by-zero) reached Cesium,
  which threw and aborted the entire frame before any stroke. Guard finite
  coordinates per particle and per projection.
- Exaggerate advection in proportion to camera height, so the same visual
  speed is legible at street level and at hemispheric zoom.
- Brighter colour ramp (the old ramp started near-invisible dark grey) and a
  defensive frame try/catch so one bad frame cannot kill the loop.

Verified: the wind field/colour/particle model renders the real GFS 06z field
correctly in an offline render, and the wind unit tests pass (8/8).
2026-09-14 14:02:48 -03:00
Gustavo Beneduzi bb18e39b7d feat(wind): add ECMWF IFS as a second wind model
Add ECMWF IFS 10 m wind alongside NOAA GFS, selectable at runtime.

The /api/wind proxy becomes a model dispatcher: `?model=gfs|ifs` (default gfs,
unknown → 400), cached per model+cycle+grid. IFS reads the keyless ECMWF Open
Data JSON Lines `.index` (note: `<stem>.index`, not `<grib2>.index`), byte-range
fetches only the `10u`/`10v` messages, decodes them with the same ecCodes WASM
path, and resamples with the existing grid code (ECMWF grids start at lo1=180;
the resampler already wraps columns).

The client layer gains a `model` parameter (setParams/getParams) with a
generation guard so a model switch aborts and refetches exactly once. Registered
as an enabled+options layer with a `model` enum.

Validated end-to-end against the real 2026-09-14 06z IFS cycle on Node 25
(180x91 grid at 2°, 131,040-byte payload, plausible mean |u|/|v|), and GFS
still passes the same check.

Attribution: contains modified Copernicus/ECMWF IFS forecast data, licensed
under CC BY 4.0. Verified: npm test (3412 pass, 0 fail), check:boundaries.
2026-09-14 13:04:35 -03:00
Gustavo Beneduzi deb34ebe35 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.
2026-09-14 12:59:18 -03:00