import { describe, it, expect } from 'vitest'; import { decodeWindField, createWindInterpolator } from '$domain'; import type { WindField, WindHeader } from '$domain'; /** * A 2×2 grid that reproduces the predictor's real conventions: longitudes in the * 0..360 frame (358 → 0, i.e. -2° → 0°) and a north→south scan (la1 > la2) with * a *positive* dy. Getting either wrong misplaces every vector. */ const header: WindHeader = { parameterUnit: 'm.s-1', parameterNumberName: 'wind', nx: 2, ny: 2, lo1: 358, la1: 10, lo2: 0, la2: 8, dx: 2, dy: 2, refTime: '2026-01-01T00:00:00Z' }; // Row-major: [ (10,-2) (10,0) ] then [ (8,-2) (8,0) ] const field: WindField = [ { header, data: [10, 0, 0, 0] }, // U (eastward) { header, data: [0, 10, 0, 0] } // V (northward) ]; describe('decodeWindField', () => { it('places vectors using the grid extent, not raw dx/dy', () => { const v = decodeWindField(field); expect(v).toHaveLength(4); // First point: la1/lo1 wrapped from 358 into (-180, 180]. expect(v[0].lat).toBe(10); expect(v[0].lng).toBe(-2); // Second column steps east across the 0/360 seam. expect(v[1].lng).toBe(0); // Second row scans southward despite dy being positive. expect(v[2].lat).toBe(8); }); it('derives speed and the bearing the wind blows TO', () => { const v = decodeWindField(field); expect(v[0].speed).toBeCloseTo(10, 6); expect(v[0].bearing).toBeCloseTo(90, 6); // pure U → eastward expect(v[1].bearing).toBeCloseTo(0, 6); // pure V → northward }); }); describe('createWindInterpolator', () => { it('returns the exact grid value at a node', () => { const at = createWindInterpolator(field); expect(at(-2, 10)).toEqual([10, 0]); expect(at(0, 10)).toEqual([0, 10]); }); it('blends between nodes', () => { const at = createWindInterpolator(field); const mid = at(-1, 10)!; expect(mid[0]).toBeCloseTo(5, 6); expect(mid[1]).toBeCloseTo(5, 6); }); it('returns null outside the grid', () => { const at = createWindInterpolator(field); expect(at(-2, 20)).toBeNull(); // north of la1 expect(at(-2, 0)).toBeNull(); // south of la2 }); });