fix(input): correct lon mapping
This commit is contained in:
parent
19557f3a62
commit
84d1664b29
20 changed files with 1197 additions and 137 deletions
|
|
@ -68,9 +68,20 @@ const (
|
|||
// Rasterize samples field over req and returns the U/V grid payload.
|
||||
//
|
||||
// Data is laid out in wind-js scan order: row 0 is the northernmost
|
||||
// latitude (la1), each row runs west→east, longitudes increasing. Per-cell
|
||||
// sampling errors (e.g. altitude outside the model) are written as 0 rather
|
||||
// than failing the whole request; a time outside coverage is a hard error.
|
||||
// latitude (la1), each row runs west→east, longitudes increasing.
|
||||
//
|
||||
// Cells that cannot be sampled individually — a regional dataset queried outside
|
||||
// its region — are written as 0 and the request still succeeds, because a partly
|
||||
// covered grid is worth drawing. A grid in which nothing could be sampled is an
|
||||
// error instead. Time is one value for the whole request, so a time past the
|
||||
// dataset's horizon fails every cell, and returning that as zeros made a velocity
|
||||
// layer draw an atmosphere at perfect rest where the honest answer is "we have no
|
||||
// data for this time".
|
||||
//
|
||||
// Known gap: an individual zero-filled cell is indistinguishable from genuine
|
||||
// calm in the payload. The wind-js format expresses missing data as null, which
|
||||
// would mean typing Data as []*float64; there is no consumer to validate that
|
||||
// against since the browser wind layer was removed in the Cesium migration.
|
||||
func Rasterize(field weather.WindField, req Request) (Field, error) {
|
||||
step := req.Step
|
||||
if step <= 0 {
|
||||
|
|
@ -125,6 +136,8 @@ func Rasterize(field weather.WindField, req Request) (Field, error) {
|
|||
v := make([]float64, nx*ny)
|
||||
|
||||
// Row 0 = north (la1); rows descend in latitude.
|
||||
var failed int
|
||||
var firstErr error
|
||||
for j := range ny {
|
||||
lat := maxLat - float64(j)*step
|
||||
for i := range nx {
|
||||
|
|
@ -132,12 +145,20 @@ func Rasterize(field weather.WindField, req Request) (Field, error) {
|
|||
s, err := field.Wind(req.Time, lat, normLng(lng), req.Altitude)
|
||||
idx := j*nx + i
|
||||
if err != nil {
|
||||
continue // leave as 0
|
||||
failed++
|
||||
if firstErr == nil {
|
||||
firstErr = err
|
||||
}
|
||||
continue // leave as 0; see the doc comment on this distinction
|
||||
}
|
||||
u[idx] = s.U
|
||||
v[idx] = s.V
|
||||
}
|
||||
}
|
||||
if failed == nx*ny {
|
||||
return nil, fmt.Errorf("no wind data anywhere in the requested grid at %s: %w",
|
||||
time.Unix(int64(req.Time), 0).UTC().Format(time.RFC3339), firstErr)
|
||||
}
|
||||
|
||||
refTime := time.Unix(int64(req.Time), 0).UTC().Format("2006-01-02T15:04:05.000Z")
|
||||
mk := func(num int, name string, data []float64) Component {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package windviz
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -94,3 +96,86 @@ func TestCacheRoundTrip(t *testing.T) {
|
|||
t.Errorf("cache should hit after put")
|
||||
}
|
||||
}
|
||||
|
||||
// horizonWind has data up to horizon seconds and fails past it, the way a real
|
||||
// dataset's time axis does.
|
||||
type horizonWind struct{ horizon float64 }
|
||||
|
||||
func (w horizonWind) Wind(t, _, _, _ float64) (weather.Sample, error) {
|
||||
if t > w.horizon {
|
||||
return weather.Sample{}, fmt.Errorf("hour=%v out of range", t/3600)
|
||||
}
|
||||
return weather.Sample{U: 7, V: -3}, nil
|
||||
}
|
||||
func (w horizonWind) Epoch() time.Time { return time.Unix(0, 0).UTC() }
|
||||
func (w horizonWind) Source() string { return "test" }
|
||||
|
||||
// northOnlyWind has data only in the northern hemisphere, the way a regional
|
||||
// subset does. Its failures are genuinely per-cell.
|
||||
type northOnlyWind struct{}
|
||||
|
||||
func (northOnlyWind) Wind(_, lat, _, _ float64) (weather.Sample, error) {
|
||||
if lat < 0 {
|
||||
return weather.Sample{}, fmt.Errorf("lat=%v outside region", lat)
|
||||
}
|
||||
return weather.Sample{U: 5, V: 1}, nil
|
||||
}
|
||||
func (northOnlyWind) Epoch() time.Time { return time.Unix(0, 0).UTC() }
|
||||
func (northOnlyWind) Source() string { return "test" }
|
||||
|
||||
// TestRasterizeRefusesWhenNoCellHasData is the case the package comment already
|
||||
// promised and the code never implemented ("a time outside coverage is a hard
|
||||
// error").
|
||||
//
|
||||
// Time is one value for the whole request, so a time past the dataset's horizon
|
||||
// fails every cell. Each failure was written as a zero, so the response was a
|
||||
// complete grid of zero wind — a velocity layer draws that as an atmosphere at
|
||||
// perfect rest. Missing data must not be rendered as calm air.
|
||||
func TestRasterizeRefusesWhenNoCellHasData(t *testing.T) {
|
||||
f := horizonWind{horizon: 600}
|
||||
|
||||
out, err := Rasterize(f, Request{Time: 700, MinLng: 0, MaxLng: 360, Step: 90})
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("Rasterize returned %d components instead of an error", len(out))
|
||||
}
|
||||
if !strings.Contains(err.Error(), "out of range") {
|
||||
t.Errorf("error %q does not carry the sampler's reason", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRasterizeStillSucceedsWithinCoverage keeps the check above from rejecting
|
||||
// healthy requests.
|
||||
func TestRasterizeStillSucceedsWithinCoverage(t *testing.T) {
|
||||
f := horizonWind{horizon: 600}
|
||||
|
||||
out, err := Rasterize(f, Request{Time: 300, MinLng: 0, MaxLng: 360, Step: 90})
|
||||
if err != nil {
|
||||
t.Fatalf("Rasterize within coverage: %v", err)
|
||||
}
|
||||
if got := out[0].Data[0]; got != 7 {
|
||||
t.Errorf("u = %v, want 7", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRasterizeZeroFillsIndividualGaps preserves the documented per-cell
|
||||
// behaviour: a partly-covered grid is still worth drawing, so gaps stay zero and
|
||||
// the request succeeds. Only a grid with nothing in it is refused.
|
||||
func TestRasterizeZeroFillsIndividualGaps(t *testing.T) {
|
||||
out, err := Rasterize(northOnlyWind{}, Request{
|
||||
MinLat: -30, MaxLat: 30, MinLng: 0, MaxLng: 30, Step: 30,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Rasterize partly-covered grid: %v", err)
|
||||
}
|
||||
|
||||
// Row 0 is the northernmost latitude (+30), the last row is -30.
|
||||
nx := out[0].Header.Nx
|
||||
ny := out[0].Header.Ny
|
||||
if got := out[0].Data[0]; got != 5 {
|
||||
t.Errorf("northern cell u = %v, want 5", got)
|
||||
}
|
||||
if got := out[0].Data[(ny-1)*nx]; got != 0 {
|
||||
t.Errorf("southern gap u = %v, want 0", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue