Skip to content

Commit 93db60a

Browse files
feat: add value() method to WMT reader for multi-variable point snapshots
1 parent a226cce commit 93db60a

4 files changed

Lines changed: 146 additions & 45 deletions

File tree

wmtiles-js/README.md

Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -28,54 +28,38 @@ wmt.referenceTime; // Date
2828
wmt.variables; // ReadonlyArray<Variable>
2929
wmt.timeStepCount; // 81
3030
wmt.timeAxis; // { kind: "regular", start, intervalMs, count } | { kind: "irregular", times }
31+
```
3132

32-
// Resolve a variable handle once, reuse for many requests.
33-
const t2m = wmt.variable("temperature_2m");
34-
35-
t2m.unit; // "K"
36-
t2m.range; // { min, max }
37-
t2m.colormap; // "magma"
38-
39-
// Fetch one tile (Float32Array of tileSize² values; NaN where NoData).
40-
const pixels = await t2m.tile({ time: 12, z: 5, x: 16, y: 11 });
33+
There are two ways to pull data out of a file:
4134

42-
// Or by absolute time — must match a step exactly.
43-
const pixels2 = await t2m.tile({
44-
time: new Date("2026-05-06T12:00:00Z"),
45-
z: 5, x: 16, y: 11,
46-
});
35+
| You want… | Use | Returns |
36+
|---|---|---|
37+
| The value of a few variables at one (lat, lon, time) | [`wmt.value()`](#point-snapshot-wmtvalue) | scalars per variable |
38+
| The time series of a few variables at one (lat, lon) | [`wmt.forecast()`](#point-time-series-wmtforecast) | `Float32Array` per variable |
39+
| Raster pixels of a tile to render on a map | [`variable.tile()` / `tiles()`](#tile-rendering-for-maps) | `Float32Array` of pixels |
4740

48-
// Sample a single point (nearest pixel). Defaults z to maxZoom.
49-
const valueK = await t2m.sample({ time: 12, lat: 52.52, lon: 13.405 });
50-
```
41+
Prefer the point APIs (`value`, `forecast`) when you only need scalars. They handle variable lookup, time resolution, and missing-data NaN-filling in one call. The tile API is for map renderers that need full raster pixels per tile.
5142

52-
### Batched tile fetch
43+
### Point snapshot: `wmt.value()`
5344

54-
For UIs that paint several tiles in one frame, `tiles()` issues 1–2 coalesced
55-
range requests instead of one per tile (when all tiles share the same
56-
variable + time block):
45+
Many variables at one point at one time. Useful for map-click tooltips showing "temperature + wind + precip right here, right now":
5746

5847
```ts
59-
const frame = await t2m.tiles({
60-
time: 12,
61-
coords: [
62-
{ z: 5, x: 16, y: 11 },
63-
{ z: 5, x: 17, y: 11 },
64-
{ z: 5, x: 18, y: 11 },
65-
],
48+
const snap = await wmt.value({
49+
lat: 52.52,
50+
lon: 13.405,
51+
time: 0, // step index or Date
52+
variables: ["dbzh", "temperature_2m"],
6653
});
67-
// frame[i] is always a Float32Array (NaN-filled if missing/out-of-range).
68-
```
6954

70-
You can tune coalescing with the `coalesce` option:
71-
72-
```ts
73-
await t2m.tiles({ time: 12, coords, coalesce: { maxGapBytes: 32_000 } });
55+
snap.time; // Date, the resolved absolute time
56+
snap.values.dbzh; // number, NaN if missing/NoData
57+
snap.values.temperature_2m;
7458
```
7559

76-
### Forecast / time-series at a point
60+
### Point time-series: `wmt.forecast()`
7761

78-
Sample one or more variables at a (lat, lon) point across the whole time axis:
62+
Many variables at one point across the time axis:
7963

8064
```ts
8165
const fc = await wmt.forecast({
@@ -84,9 +68,9 @@ const fc = await wmt.forecast({
8468
variables: ["dbzh", "temperature_2m"],
8569
});
8670

87-
fc.times; // Date[] one per step, aligned with all series
88-
fc.values.dbzh; // Float32Array fc.values.dbzh[i] is at fc.times[i]
89-
fc.values.dbzh[0]; // NaN if missing/NoData
71+
fc.times; // Date[], one per step, aligned with all series
72+
fc.values.dbzh; // Float32Array; fc.values.dbzh[i] is at fc.times[i]
73+
fc.values.dbzh[0]; // NaN if missing/NoData
9074
```
9175

9276
Optional `z` (defaults to `maxZoom`) and `timeRange` to restrict the window:
@@ -104,9 +88,55 @@ await wmt.forecast({
10488
});
10589
```
10690

107-
`forecast()` fans out one parallel request per `(variable, time step)` — there is
108-
no cross-step coalescing, because different time steps live in different blocks.
109-
Units for each series stay on the variable handle (`wmt.variable("dbzh").unit`).
91+
`forecast()` fans out one parallel request per `(variable, time step)`. There is no cross-step coalescing, because different time steps live in different blocks. For per-variable metadata (`unit`, `colormap`, `range`) reach for the variable handle: `wmt.variable("dbzh").unit`.
92+
93+
### Tile rendering for maps
94+
95+
Map renderers need the actual raster pixels of a tile, not point samples. Resolve a `Variable` handle once and reuse it for every tile in every frame:
96+
97+
```ts
98+
const t2m = wmt.variable("temperature_2m");
99+
100+
t2m.unit; // "K"
101+
t2m.range; // { min, max }, feed into your colormap
102+
t2m.colormap; // "magma"
103+
104+
// Fetch one tile (Float32Array of tileSize² values; NaN where NoData).
105+
const pixels = await t2m.tile({ time: 12, z: 5, x: 16, y: 11 });
106+
107+
// Or by absolute time, must match a step exactly.
108+
const pixels2 = await t2m.tile({
109+
time: new Date("2026-05-06T12:00:00Z"),
110+
z: 5, x: 16, y: 11,
111+
});
112+
```
113+
114+
For UIs that paint several tiles in one frame, `tiles()` coalesces 1 or 2 range requests instead of one per tile (when all tiles share the same variable + time block):
115+
116+
```ts
117+
const frame = await t2m.tiles({
118+
time: 12,
119+
coords: [
120+
{ z: 5, x: 16, y: 11 },
121+
{ z: 5, x: 17, y: 11 },
122+
{ z: 5, x: 18, y: 11 },
123+
],
124+
});
125+
// frame[i] is always a Float32Array (NaN-filled if missing/out-of-range).
126+
```
127+
128+
Tune coalescing with the `coalesce` option:
129+
130+
```ts
131+
await t2m.tiles({ time: 12, coords, coalesce: { maxGapBytes: 32_000 } });
132+
```
133+
134+
If you only need one pixel and want a plain `number | null` (rather than the wrapped `wmt.value()` result), there is also:
135+
136+
```ts
137+
const valueK = await t2m.sample({ time: 12, lat: 52.52, lon: 13.405 });
138+
// number | null (null = out-of-range zoom / invalid coords; NaN = NoData)
139+
```
110140

111141
### Loading from a buffer
112142

@@ -121,7 +151,7 @@ const wmt = await open(
121151

122152
### Custom byte source
123153

124-
Implement the `ByteSource` interface one method, async byte-range reads:
154+
Implement the `ByteSource` interface with one method, async byte-range reads:
125155

126156
```ts
127157
import { open, type ByteSource } from "wmtiles";
@@ -148,8 +178,8 @@ All thrown errors derive from `WMTError`:
148178
|---|---|
149179
| `SourceError` | Source/read failure, for example an HTTP server that ignores range requests. |
150180
| `FormatError` | Malformed file: bad magic, bad CRC, truncated buffers, unsupported version. |
151-
| `UnknownVariableError` | `wmt.variable("foo")` for an absent name. |
152-
| `TimeOutOfRangeError` | `tile()` / `sample()` with a `Date` that doesn't align to a step, or an out-of-range index. |
181+
| `UnknownVariableError` | `wmt.variable("foo")`, `wmt.value({ variables: ["foo"] })`, etc. for an absent name. |
182+
| `TimeOutOfRangeError` | A `Date` that doesn't align to a step, an out-of-range index, or `timeRange` where `start > end`. |
153183

154184
```ts
155185
import { UnknownVariableError } from "wmtiles";

wmtiles-js/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export {
2020
type SampleRequest,
2121
type ForecastRequest,
2222
type ForecastResult,
23+
type ValueRequest,
24+
type ValueResult,
2325
} from "./reader.js";
2426

2527
// Typed errors.

wmtiles-js/src/reader.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,38 @@ test("WMT.forecast validates inputs and signals missing data with NaN", async ()
213213
for (const v of fc.values.temp) expect(Number.isNaN(v)).toBe(true);
214214
});
215215

216+
test("WMT.value returns a snapshot of many variables at one time", async () => {
217+
const r = await WMT.open(bytesSource(load("multistep.wmt")));
218+
219+
// step 2 → temp = 102, wind = 202 (offset + step from fixture)
220+
const snap = await r.value({
221+
lat: 0, lon: 0, time: 2, variables: ["temp", "wind"],
222+
});
223+
expect(snap.time.toISOString()).toBe("2026-05-03T02:00:00.000Z");
224+
expect(snap.values.temp).toBe(102);
225+
expect(snap.values.wind).toBe(202);
226+
227+
// Date-based time ref must produce the same result.
228+
const byDate = await r.value({
229+
lat: 0, lon: 0,
230+
time: new Date("2026-05-03T02:00:00Z"),
231+
variables: ["temp"],
232+
});
233+
expect(byDate.values.temp).toBe(102);
234+
235+
// Unknown variable name throws before any I/O.
236+
await expect(
237+
r.value({ lat: 0, lon: 0, time: 0, variables: ["nope"] }),
238+
).rejects.toBeInstanceOf(UnknownVariableError);
239+
240+
// Invalid coords → NaN per variable (never null).
241+
const oob = await r.value({
242+
lat: 91, lon: 0, time: 0, variables: ["temp", "wind"],
243+
});
244+
expect(Number.isNaN(oob.values.temp)).toBe(true);
245+
expect(Number.isNaN(oob.values.wind)).toBe(true);
246+
});
247+
216248
test("crc_corrupted.wmt falls back to previous snapshot", async () => {
217249
const r = await WMT.open(bytesSource(load("crc_corrupted.wmt")));
218250
const names = r.variables.map((v) => v.name).sort();

wmtiles-js/src/reader.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,22 @@ export interface ForecastResult {
121121
readonly values: Readonly<Record<string, Float32Array>>;
122122
}
123123

124+
export interface ValueRequest {
125+
lat: number;
126+
lon: number;
127+
time: TimeRef;
128+
variables: readonly string[];
129+
/** Defaults to maxZoom of the file. */
130+
z?: number;
131+
}
132+
133+
export interface ValueResult {
134+
/** Resolved absolute time (matters when caller passed a step index). */
135+
readonly time: Date;
136+
/** NaN marks missing/NoData. Key set equals req.variables. */
137+
readonly values: Readonly<Record<string, number>>;
138+
}
139+
124140
// ---------- Sources ----------
125141

126142
export function httpSource(url: string | URL, init?: RequestInit): ByteSource {
@@ -524,6 +540,27 @@ export class WMT {
524540
return { times, values };
525541
}
526542

543+
/**
544+
* Snapshot of several variables at one point at one time. The dual of
545+
* forecast(): same point, single time step, many variables. NaN = missing.
546+
*/
547+
async value(req: ValueRequest): Promise<ValueResult> {
548+
const vars = req.variables.map((name) => this.variable(name));
549+
const t = this.timeIndexOf(req.time);
550+
const z = req.z ?? this._header.maxZoom;
551+
552+
const values: Record<string, number> = {};
553+
await Promise.all(
554+
vars.map(async (v) => {
555+
const val = await this._sample(v.id, t, req.lat, req.lon, z);
556+
// null = out-of-bbox / out-of-range zoom / missing tile → NaN.
557+
values[v.name] = val ?? NaN;
558+
}),
559+
);
560+
561+
return { time: this.timeAt(t), values };
562+
}
563+
527564
// ---- Internal: fetch helpers (called by Variable) ----
528565

529566
private async _fetchTile(

0 commit comments

Comments
 (0)