mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Wind (NOAA GFS and ECMWF IFS: animated particles, colour field, a reading at map centre), observed weather (NOAA MRMS radar, GOES and NESDIS infrared clouds, lightning density) on one shared history timeline, NHC and CPHC cyclone advisories with tracks and cones, and a WEATHER panel in the right rail. On 3D Tiles the observed products render as raised shells with a sharper window around the view. Co-authored-by: Gustavo Beneduzi <[email protected]> Co-authored-by: Daniel Slay <[email protected]> Co-authored-by: Rehaan Delmotra <[email protected]> Co-authored-by: Cursor Agent <[email protected]>
131 lines
4.2 KiB
JavaScript
131 lines
4.2 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
ifsObjectUrls,
|
|
parseIfsIndex,
|
|
ifsWindRanges,
|
|
nearestIfsStep,
|
|
selectLatestIfsCycle,
|
|
fetchIfsWind,
|
|
} from '../../server/providers/wind/ifs.js';
|
|
import { nearestGfsStep } from '../../server/providers/wind/catalog.js';
|
|
test('IFS inventory selects only 10 m components and distinct index suffix', () => {
|
|
const urls = ifsObjectUrls({ date: '20260915', hour: 6, step: 9 });
|
|
assert.match(urls.index, /9h-oper-fc\.index$/);
|
|
assert.equal(urls.index.includes('.grib2.index'), false);
|
|
assert.deepEqual(
|
|
ifsWindRanges(
|
|
parseIfsIndex(
|
|
'{"param":"10u","_offset":20,"_length":10}\n{"param":"10v","_offset":40,"_length":15}',
|
|
),
|
|
),
|
|
{ u: { start: 20, end: 29 }, v: { start: 40, end: 54 } },
|
|
);
|
|
assert.deepEqual(selectLatestIfsCycle(Date.UTC(2026, 8, 15, 4)), {
|
|
date: '20260914',
|
|
hour: 18,
|
|
});
|
|
assert.equal(nearestIfsStep(8, 6), 9);
|
|
assert.equal(nearestIfsStep(200, 6), 144);
|
|
assert.equal(nearestIfsStep(151, 0), 150);
|
|
assert.equal(nearestGfsStep(8.2), 8);
|
|
});
|
|
|
|
test('IFS 2t and msl companions use the same issue/valid object as 10u and 10v', async () => {
|
|
const entries = ['10u', '10v', '2t', 'msl'].map((param, i) => ({
|
|
param,
|
|
_offset: i * 2,
|
|
_length: 2,
|
|
}));
|
|
for (const overlay of ['none', 'temperature', 'pressure']) {
|
|
const calls = [];
|
|
const result = await fetchIfsWind({
|
|
overlay,
|
|
targetDx: 90,
|
|
now: () => Date.UTC(2026, 8, 15, 14),
|
|
fetchImpl: async (url, options) => {
|
|
calls.push({ url, options });
|
|
if (url.endsWith('.index'))
|
|
return new Response(
|
|
entries.map((entry) => JSON.stringify(entry)).join('\n'),
|
|
);
|
|
const start = Number(/bytes=(\d+)-/.exec(options.headers.Range)[1]);
|
|
return new Response(Uint8Array.of(start, 0), { status: 206 });
|
|
},
|
|
decodeImpl: async (bytes) => ({
|
|
ni: 4,
|
|
nj: 3,
|
|
lo1: 0,
|
|
la1: 90,
|
|
di: 90,
|
|
dj: 90,
|
|
values: new Float64Array(12).fill(
|
|
bytes[0] === 4 ? 283.15 : bytes[0] === 6 ? 100000 : bytes[0],
|
|
),
|
|
units: bytes[0] === 4 ? 'K' : bytes[0] === 6 ? 'Pa' : 'm/s',
|
|
}),
|
|
});
|
|
assert.equal(calls.length, overlay === 'none' ? 3 : 4);
|
|
assert.equal(new Set(calls.slice(1).map((c) => c.url)).size, 1);
|
|
assert.ok(calls.every((c) => c.options.redirect === 'error'));
|
|
assert.ok(calls[0].url.endsWith('-9h-oper-fc.index'));
|
|
assert.equal(result.cycle.runIso, '2026-09-15T06:00:00.000Z');
|
|
assert.equal(result.cycle.validIso, '2026-09-15T15:00:00.000Z');
|
|
if (overlay === 'none') assert.equal(result.scalar, undefined);
|
|
else
|
|
assert.ok(
|
|
Math.abs(
|
|
result.grid.scalar[0] - (overlay === 'temperature' ? 10 : 1000),
|
|
) < 0.0001,
|
|
);
|
|
}
|
|
assert.throws(
|
|
() =>
|
|
ifsWindRanges(
|
|
parseIfsIndex(
|
|
entries
|
|
.slice(0, 2)
|
|
.map((entry) => JSON.stringify(entry))
|
|
.join('\n'),
|
|
),
|
|
{ overlay: 'pressure' },
|
|
),
|
|
/missing msl/,
|
|
);
|
|
});
|
|
|
|
test('IFS optional pressure refusal preserves same-cycle wind', async () => {
|
|
const entries = ['10u', '10v', 'msl'].map((param, i) => ({
|
|
param,
|
|
_offset: i * 2,
|
|
_length: 2,
|
|
}));
|
|
const result = await fetchIfsWind({
|
|
overlay: 'pressure',
|
|
targetDx: 90,
|
|
now: () => Date.UTC(2026, 8, 15, 14),
|
|
fetchImpl: async (url, options) => {
|
|
if (url.endsWith('.index'))
|
|
return new Response(
|
|
entries.map((entry) => JSON.stringify(entry)).join('\n'),
|
|
);
|
|
const start = Number(/bytes=(\d+)-/.exec(options.headers.Range)[1]);
|
|
if (start === 4) return new Response('Unavailable', { status: 503 });
|
|
return new Response(Uint8Array.of(start, 0), { status: 206 });
|
|
},
|
|
decodeImpl: async (bytes) => ({
|
|
ni: 4,
|
|
nj: 3,
|
|
lo1: 0,
|
|
la1: 90,
|
|
di: 90,
|
|
dj: 90,
|
|
values: new Float64Array(12).fill(bytes[0]),
|
|
}),
|
|
});
|
|
assert.equal(result.scalarError, 'Mean sea level pressure field unavailable');
|
|
assert.equal(result.grid.scalar, undefined);
|
|
assert.deepEqual([...result.grid.v], Array(12).fill(2));
|
|
assert.equal(result.cycle.validIso, '2026-09-15T15:00:00.000Z');
|
|
});
|