@dclimate/zarr-map 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +374 -0
  3. package/dist/core/index.cjs +1603 -0
  4. package/dist/core/index.cjs.map +1 -0
  5. package/dist/core/index.d.cts +50 -0
  6. package/dist/core/index.d.ts +50 -0
  7. package/dist/core/index.js +1575 -0
  8. package/dist/core/index.js.map +1 -0
  9. package/dist/dclimate/index.cjs +1859 -0
  10. package/dist/dclimate/index.cjs.map +1 -0
  11. package/dist/dclimate/index.d.cts +80 -0
  12. package/dist/dclimate/index.d.ts +80 -0
  13. package/dist/dclimate/index.js +1856 -0
  14. package/dist/dclimate/index.js.map +1 -0
  15. package/dist/index.cjs +3071 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +5 -0
  18. package/dist/index.d.ts +5 -0
  19. package/dist/index.js +3038 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/jaxray-CZWT_ZgD.d.ts +57 -0
  22. package/dist/jaxray-D_mmLPHk.d.cts +57 -0
  23. package/dist/react/index.cjs +3953 -0
  24. package/dist/react/index.cjs.map +1 -0
  25. package/dist/react/index.d.cts +71 -0
  26. package/dist/react/index.d.ts +71 -0
  27. package/dist/react/index.js +3945 -0
  28. package/dist/react/index.js.map +1 -0
  29. package/dist/renderers/index.cjs +903 -0
  30. package/dist/renderers/index.cjs.map +1 -0
  31. package/dist/renderers/index.d.cts +115 -0
  32. package/dist/renderers/index.d.ts +115 -0
  33. package/dist/renderers/index.js +899 -0
  34. package/dist/renderers/index.js.map +1 -0
  35. package/dist/types-DEZwfJNY.d.cts +210 -0
  36. package/dist/types-DEZwfJNY.d.ts +210 -0
  37. package/docs/README.md +12 -0
  38. package/docs/api-design.md +185 -0
  39. package/docs/architecture.md +246 -0
  40. package/docs/decision-record-renderer.md +144 -0
  41. package/docs/package-boundaries.md +50 -0
  42. package/docs/release-checklist.md +31 -0
  43. package/package.json +121 -0
@@ -0,0 +1,1603 @@
1
+ 'use strict';
2
+
3
+ // src/core/errors.ts
4
+ var GridError = class extends Error {
5
+ code;
6
+ cause;
7
+ dimension;
8
+ variable;
9
+ sourceId;
10
+ context;
11
+ constructor(details) {
12
+ super(details.message);
13
+ this.name = "GridError";
14
+ this.code = details.code;
15
+ this.cause = details.cause;
16
+ this.dimension = details.dimension;
17
+ this.variable = details.variable;
18
+ this.sourceId = details.sourceId;
19
+ this.context = details.context;
20
+ }
21
+ };
22
+ function isGridError(error) {
23
+ return error instanceof GridError;
24
+ }
25
+ function toGridError(details) {
26
+ return details instanceof GridError ? details : new GridError(details);
27
+ }
28
+
29
+ // src/core/color.ts
30
+ var builtinPalettes = {
31
+ viridis: ["#440154", "#31688e", "#35b779", "#fde725"],
32
+ inferno: ["#000004", "#57106e", "#bc3754", "#f98e09", "#fcffa4"],
33
+ magma: ["#000004", "#3b0f70", "#8c2981", "#de4968", "#fe9f6d", "#fcfdbf"],
34
+ plasma: ["#0d0887", "#7e03a8", "#cc4778", "#f89540", "#f0f921"],
35
+ cividis: ["#00204c", "#31446b", "#666870", "#958f78", "#c6b866", "#ffe945"],
36
+ turbo: ["#30123b", "#466be3", "#35ab6b", "#faba39", "#7a0403"],
37
+ greys: ["#000000", "#777777", "#ffffff"],
38
+ grays: ["#000000", "#777777", "#ffffff"],
39
+ temperature: ["#2c5ab4", "#67b0f0", "#e0e4dc", "#ffb266", "#cd4434"],
40
+ precipitation: ["#bae6fd", "#60a5fa", "#2563eb", "#4f46e5", "#6d28d9"],
41
+ humidity: ["#374151", "#2d5a6e", "#1890b0", "#2dd4bf", "#99f6e4"],
42
+ vegetation: ["#f7fee7", "#bef264", "#4ade80", "#15803d", "#064e3b"],
43
+ wind: ["#e2e8f0", "#7dd3fc", "#2dd4bf", "#14b8a6"]
44
+ };
45
+ function validateColorScale(colorScale) {
46
+ const [min, max] = colorScale.domain;
47
+ if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max) {
48
+ throw new GridError({
49
+ code: "INVALID_COLOR_SCALE",
50
+ message: "Color scale domain must be finite and ordered as [min, max].",
51
+ context: { domain: colorScale.domain }
52
+ });
53
+ }
54
+ if (!colorScale.palette) {
55
+ throw new GridError({
56
+ code: "INVALID_COLOR_SCALE",
57
+ message: "Color scale must include a palette name or color stop array."
58
+ });
59
+ }
60
+ paletteToRendererColorMap(colorScale.palette);
61
+ return colorScale;
62
+ }
63
+ function colorScaleToRendererOptions(colorScale) {
64
+ if (!colorScale) {
65
+ return void 0;
66
+ }
67
+ validateColorScale(colorScale);
68
+ return {
69
+ colormap: paletteToRendererColorMap(colorScale.palette),
70
+ clim: colorScale.domain
71
+ };
72
+ }
73
+ function paletteToRendererColorMap(palette) {
74
+ if (typeof palette !== "string") {
75
+ if (palette.length < 2) {
76
+ throw new GridError({
77
+ code: "INVALID_COLOR_SCALE",
78
+ message: "Custom color palettes must include at least two color stops.",
79
+ context: { palette }
80
+ });
81
+ }
82
+ const colors2 = palette.map((color) => color.trim());
83
+ const invalid = colors2.find((color) => color.length === 0);
84
+ if (invalid !== void 0) {
85
+ throw new GridError({
86
+ code: "INVALID_COLOR_SCALE",
87
+ message: "Custom color palettes cannot include empty color stops.",
88
+ context: { palette }
89
+ });
90
+ }
91
+ return colors2;
92
+ }
93
+ const normalized = palette.toLowerCase();
94
+ const colors = builtinPalettes[normalized];
95
+ if (!colors) {
96
+ throw new GridError({
97
+ code: "INVALID_COLOR_SCALE",
98
+ message: `Unsupported renderer palette "${palette}". Use one of: ${Object.keys(builtinPalettes).join(", ")}.`,
99
+ context: { palette }
100
+ });
101
+ }
102
+ return colors;
103
+ }
104
+ function colorScaleDisplayDomain(colorScale) {
105
+ const { displayUnit, quantity, unit } = colorScale;
106
+ if (!displayUnit || !unit || displayUnit === unit || !quantity) {
107
+ return colorScale.domain;
108
+ }
109
+ return [
110
+ convertDisplayValue(colorScale.domain[0], {
111
+ fromUnit: unit,
112
+ quantity,
113
+ toUnit: displayUnit
114
+ }),
115
+ convertDisplayValue(colorScale.domain[1], {
116
+ fromUnit: unit,
117
+ quantity,
118
+ toUnit: displayUnit
119
+ })
120
+ ];
121
+ }
122
+ function convertDisplayValue(value, options) {
123
+ if (!Number.isFinite(value)) {
124
+ return value;
125
+ }
126
+ if (options.quantity === "temperature") {
127
+ return temperatureFromKelvin(temperatureToKelvin(value, options.fromUnit), options.toUnit);
128
+ }
129
+ return precipitationFromMeters(precipitationToMeters(value, options.fromUnit), options.toUnit);
130
+ }
131
+ function temperatureToKelvin(value, unit) {
132
+ const normalized = normalizeUnit(unit);
133
+ if (normalized === "k" || normalized === "kelvin") {
134
+ return value;
135
+ }
136
+ if (["c", "degc", "celsius"].includes(normalized)) {
137
+ return value + 273.15;
138
+ }
139
+ if (["f", "degf", "fahrenheit"].includes(normalized)) {
140
+ return (value - 32) * 5 / 9 + 273.15;
141
+ }
142
+ throw unsupportedUnit(unit, "temperature");
143
+ }
144
+ function temperatureFromKelvin(value, unit) {
145
+ const normalized = normalizeUnit(unit);
146
+ if (normalized === "k" || normalized === "kelvin") {
147
+ return value;
148
+ }
149
+ if (["c", "degc", "celsius"].includes(normalized)) {
150
+ return value - 273.15;
151
+ }
152
+ if (["f", "degf", "fahrenheit"].includes(normalized)) {
153
+ return (value - 273.15) * 9 / 5 + 32;
154
+ }
155
+ throw unsupportedUnit(unit, "temperature");
156
+ }
157
+ function precipitationToMeters(value, unit) {
158
+ const normalized = normalizeUnit(unit);
159
+ if (["m", "meter", "meters"].includes(normalized)) {
160
+ return value;
161
+ }
162
+ if (["mm", "millimeter", "millimeters"].includes(normalized)) {
163
+ return value / 1e3;
164
+ }
165
+ if (isKilogramPerSquareMeter(normalized)) {
166
+ return value / 1e3;
167
+ }
168
+ if (["in", "inch", "inches"].includes(normalized)) {
169
+ return value * 0.0254;
170
+ }
171
+ throw unsupportedUnit(unit, "precipitation");
172
+ }
173
+ function precipitationFromMeters(value, unit) {
174
+ const normalized = normalizeUnit(unit);
175
+ if (["m", "meter", "meters"].includes(normalized)) {
176
+ return value;
177
+ }
178
+ if (["mm", "millimeter", "millimeters"].includes(normalized)) {
179
+ return value * 1e3;
180
+ }
181
+ if (isKilogramPerSquareMeter(normalized)) {
182
+ return value * 1e3;
183
+ }
184
+ if (["in", "inch", "inches"].includes(normalized)) {
185
+ return value / 0.0254;
186
+ }
187
+ throw unsupportedUnit(unit, "precipitation");
188
+ }
189
+ function isKilogramPerSquareMeter(normalizedUnit) {
190
+ const compact = normalizedUnit.replace(/\s+/g, "");
191
+ return ["kgm**-2", "kgm^-2", "kgm-2", "kg/m2", "kg/m^2"].includes(compact);
192
+ }
193
+ function normalizeUnit(unit) {
194
+ return unit.trim().toLowerCase().replace(/^degrees?_?/, "deg").replace("\xB0", "deg");
195
+ }
196
+ function unsupportedUnit(unit, quantity) {
197
+ return new GridError({
198
+ code: "UNSUPPORTED_UNIT",
199
+ message: `Unsupported ${quantity} display unit "${unit}".`,
200
+ context: { quantity, unit }
201
+ });
202
+ }
203
+
204
+ // src/core/validation.ts
205
+ var xAliases = /* @__PURE__ */ new Set(["x", "lon", "lng", "longitude"]);
206
+ var yAliases = /* @__PURE__ */ new Set(["y", "lat", "latitude"]);
207
+ var numericDtypes = /* @__PURE__ */ new Set([
208
+ "int8",
209
+ "uint8",
210
+ "int16",
211
+ "uint16",
212
+ "int32",
213
+ "uint32",
214
+ "float32",
215
+ "float64",
216
+ "i1",
217
+ "u1",
218
+ "i2",
219
+ "u2",
220
+ "i4",
221
+ "u4",
222
+ "f4",
223
+ "f8"
224
+ ]);
225
+ function findVariable(source, variableName) {
226
+ return source.variables.find(
227
+ (variable) => variable.name === variableName || variable.path === variableName
228
+ );
229
+ }
230
+ function findDimension(source, dimensionName) {
231
+ return source.dimensions.find((dimension) => dimension.name === dimensionName);
232
+ }
233
+ function inferSpatialDimensions(source, variableName = source.variables[0]?.name) {
234
+ const variable = variableName ? findVariable(source, variableName) : source.variables[0];
235
+ if (source.spatialDimensions) {
236
+ return variableIncludesSpatialDimensions(variable, source.spatialDimensions) ? source.spatialDimensions : void 0;
237
+ }
238
+ const candidateNames = variable?.dimensions ?? source.dimensions.map((dimension) => dimension.name);
239
+ let x;
240
+ let y;
241
+ for (const name of candidateNames) {
242
+ const dimension = findDimension(source, name);
243
+ const normalizedName = name.toLowerCase();
244
+ const standardName = String(dimension?.standardName ?? "").toLowerCase();
245
+ const kind = dimension?.kind;
246
+ if (!x && (kind === "x" || xAliases.has(normalizedName) || standardName === "longitude")) {
247
+ x = name;
248
+ }
249
+ if (!y && (kind === "y" || yAliases.has(normalizedName) || standardName === "latitude")) {
250
+ y = name;
251
+ }
252
+ }
253
+ return x && y ? { x, y } : void 0;
254
+ }
255
+ function variableIncludesSpatialDimensions(variable, spatialDimensions) {
256
+ if (!variable) {
257
+ return false;
258
+ }
259
+ return variable.dimensions.includes(spatialDimensions.x) && variable.dimensions.includes(spatialDimensions.y);
260
+ }
261
+ function validateGridDataSource(source, options = {}) {
262
+ const errors = [];
263
+ const warnings = [];
264
+ if (source.variables.length === 0) {
265
+ errors.push(
266
+ new GridError({
267
+ code: "MISSING_VARIABLE",
268
+ message: "GridDataSource must expose at least one variable.",
269
+ sourceId: source.id
270
+ })
271
+ );
272
+ }
273
+ const variable = options.variable ? findVariable(source, options.variable) : source.variables[0];
274
+ if (options.variable && !variable) {
275
+ errors.push(
276
+ new GridError({
277
+ code: "MISSING_VARIABLE",
278
+ message: `Variable "${options.variable}" is not present in the grid source.`,
279
+ sourceId: source.id,
280
+ variable: options.variable
281
+ })
282
+ );
283
+ }
284
+ for (const dimension of source.dimensions) {
285
+ if (!Number.isInteger(dimension.size) || dimension.size <= 0) {
286
+ errors.push(
287
+ new GridError({
288
+ code: "UNSUPPORTED_DIMENSION",
289
+ message: `Dimension "${dimension.name}" must have a positive integer size.`,
290
+ dimension: dimension.name,
291
+ sourceId: source.id
292
+ })
293
+ );
294
+ }
295
+ if (dimension.coordinates && dimension.coordinates.length !== dimension.size) {
296
+ errors.push(
297
+ new GridError({
298
+ code: "MISSING_COORDINATES",
299
+ message: `Dimension "${dimension.name}" has ${dimension.coordinates.length} coordinates for size ${dimension.size}.`,
300
+ dimension: dimension.name,
301
+ sourceId: source.id
302
+ })
303
+ );
304
+ }
305
+ }
306
+ if (variable) {
307
+ if (!isNumericDType(variable.dtype)) {
308
+ errors.push(
309
+ new GridError({
310
+ code: "UNSUPPORTED_DTYPE",
311
+ message: `Variable "${variable.name}" has unsupported dtype "${variable.dtype}".`,
312
+ variable: variable.name,
313
+ sourceId: source.id
314
+ })
315
+ );
316
+ }
317
+ if (variable.dimensions.length !== variable.shape.length) {
318
+ errors.push(
319
+ new GridError({
320
+ code: "MISSING_DIMENSION",
321
+ message: `Variable "${variable.name}" has ${variable.dimensions.length} dimensions but ${variable.shape.length} shape entries.`,
322
+ variable: variable.name,
323
+ sourceId: source.id
324
+ })
325
+ );
326
+ }
327
+ variable.dimensions.forEach((dimensionName, index) => {
328
+ const dimension = findDimension(source, dimensionName);
329
+ if (!dimension) {
330
+ errors.push(
331
+ new GridError({
332
+ code: "MISSING_DIMENSION",
333
+ message: `Variable "${variable.name}" references missing dimension "${dimensionName}".`,
334
+ dimension: dimensionName,
335
+ variable: variable.name,
336
+ sourceId: source.id
337
+ })
338
+ );
339
+ return;
340
+ }
341
+ const expectedSize = variable.shape[index];
342
+ if (expectedSize !== dimension.size) {
343
+ warnings.push(
344
+ `Variable "${variable.name}" shape for dimension "${dimensionName}" is ${expectedSize}, but the dimension size is ${dimension.size}.`
345
+ );
346
+ }
347
+ });
348
+ const spatialDimensions = inferSpatialDimensions(source, variable.name);
349
+ if (!spatialDimensions) {
350
+ errors.push(
351
+ new GridError({
352
+ code: "MISSING_SPATIAL_DIMENSIONS",
353
+ message: `Variable "${variable.name}" needs latitude/longitude or x/y spatial dimensions before it can render.`,
354
+ variable: variable.name,
355
+ sourceId: source.id
356
+ })
357
+ );
358
+ }
359
+ if (options.requireRenderable && !source.bounds) {
360
+ errors.push(
361
+ new GridError({
362
+ code: "MISSING_BOUNDS",
363
+ message: "Renderable grid sources must include explicit [west, south, east, north] bounds.",
364
+ variable: variable.name,
365
+ sourceId: source.id
366
+ })
367
+ );
368
+ }
369
+ }
370
+ return {
371
+ ok: errors.length === 0,
372
+ errors,
373
+ warnings
374
+ };
375
+ }
376
+ function assertValidGridDataSource(source, options = {}) {
377
+ const result = validateGridDataSource(source, options);
378
+ if (!result.ok) {
379
+ throw result.errors[0];
380
+ }
381
+ }
382
+ function isNumericDType(dtype) {
383
+ return numericDtypes.has(dtype.toLowerCase()) || /^[<>|]?[ifu]\d+$/.test(dtype.toLowerCase());
384
+ }
385
+
386
+ // src/core/selectors.ts
387
+ function indexSelector(index) {
388
+ return { kind: "index", index };
389
+ }
390
+ function coordinateSelector(value) {
391
+ return { kind: "coordinate", value };
392
+ }
393
+ function isoTimeSelector(iso) {
394
+ return { kind: "isoTime", iso };
395
+ }
396
+ function normalizeSelectors(source, variableName, selectors = {}) {
397
+ const variable = findVariable(source, variableName);
398
+ if (!variable) {
399
+ throw new GridError({
400
+ code: "MISSING_VARIABLE",
401
+ message: `Variable "${variableName}" is not present in the grid source.`,
402
+ variable: variableName,
403
+ sourceId: source.id
404
+ });
405
+ }
406
+ const normalized = {};
407
+ for (const dimensionName of variable.dimensions) {
408
+ const dimension = findDimension(source, dimensionName);
409
+ if (!dimension) {
410
+ throw new GridError({
411
+ code: "MISSING_DIMENSION",
412
+ message: `Selector normalization cannot find dimension "${dimensionName}".`,
413
+ dimension: dimensionName,
414
+ variable: variableName,
415
+ sourceId: source.id
416
+ });
417
+ }
418
+ const input = selectors[dimensionName];
419
+ if (input === void 0) {
420
+ normalized[dimensionName] = defaultSelectorForDimension(dimension);
421
+ continue;
422
+ }
423
+ normalized[dimensionName] = normalizeSelectorValue(dimension, input, source.id);
424
+ }
425
+ return normalized;
426
+ }
427
+ function normalizeSelectorValue(dimension, input, sourceId) {
428
+ if (input instanceof Date) {
429
+ return { kind: "coordinate", value: input.toISOString() };
430
+ }
431
+ if (typeof input !== "object") {
432
+ return normalizeCoordinateSelector(dimension, input, sourceId);
433
+ }
434
+ if (input.kind === "coordinate") {
435
+ const value = input.value instanceof Date ? input.value.toISOString() : input.value;
436
+ return normalizeCoordinateSelector(dimension, value, sourceId);
437
+ }
438
+ if (input.kind === "index") {
439
+ assertIndexInRange(dimension, input.index, sourceId);
440
+ const value = dimension.coordinates?.[input.index];
441
+ return {
442
+ kind: "index",
443
+ index: input.index,
444
+ value: value instanceof Date ? value.toISOString() : value
445
+ };
446
+ }
447
+ if (input.kind === "isoTime") {
448
+ const value = findCoordinateForIso(dimension, input.iso);
449
+ return value === void 0 ? { kind: "isoTime", iso: input.iso } : { kind: "isoTime", iso: input.iso, value };
450
+ }
451
+ throw new GridError({
452
+ code: "UNSUPPORTED_SELECTOR",
453
+ message: `Unsupported selector for dimension "${dimension.name}".`,
454
+ dimension: dimension.name,
455
+ sourceId
456
+ });
457
+ }
458
+ function toRendererSelectors(selectors) {
459
+ const output = {};
460
+ for (const [dimensionName, selector] of Object.entries(selectors)) {
461
+ if (selector.kind === "index") {
462
+ output[dimensionName] = { selected: selector.index, type: "index" };
463
+ continue;
464
+ }
465
+ if (selector.kind === "isoTime") {
466
+ output[dimensionName] = {
467
+ selected: selector.value ?? selector.iso,
468
+ type: "value"
469
+ };
470
+ continue;
471
+ }
472
+ output[dimensionName] = { selected: selector.value, type: "value" };
473
+ }
474
+ return output;
475
+ }
476
+ function selectorToIndex(dimension, selector) {
477
+ if (selector.kind === "index") {
478
+ return selector.index;
479
+ }
480
+ const selected = selector.kind === "isoTime" ? selector.value ?? selector.iso : selector.value;
481
+ const index = dimension.coordinates?.findIndex((coordinate) => sameCoordinate(coordinate, selected)) ?? -1;
482
+ if (index >= 0) {
483
+ return index;
484
+ }
485
+ throw new GridError({
486
+ code: "UNSUPPORTED_SELECTOR",
487
+ message: `Selector value "${String(selected)}" does not match coordinates for dimension "${dimension.name}".`,
488
+ dimension: dimension.name
489
+ });
490
+ }
491
+ function listTimeCoordinates(source, dimensionName = "time") {
492
+ const dimension = findDimension(source, dimensionName);
493
+ if (!dimension) {
494
+ throw new GridError({
495
+ code: "MISSING_DIMENSION",
496
+ message: `Time dimension "${dimensionName}" is not present in the grid source.`,
497
+ dimension: dimensionName,
498
+ sourceId: source.id
499
+ });
500
+ }
501
+ if (!dimension.coordinates) {
502
+ throw new GridError({
503
+ code: "MISSING_COORDINATES",
504
+ message: `Time dimension "${dimensionName}" needs coordinate values for a slider.`,
505
+ dimension: dimensionName,
506
+ sourceId: source.id
507
+ });
508
+ }
509
+ const coordinates = dimension.coordinates.map((coordinate, index) => {
510
+ const decoded = decodeTimeCoordinate(coordinate, dimension.units);
511
+ const forecastLabel = formatForecastStepLabel(source, dimension, coordinate);
512
+ return {
513
+ index,
514
+ value: decoded.value,
515
+ label: forecastLabel ?? (decoded.iso ? formatHumanUtcDateTime(decoded.iso) : String(decoded.value)),
516
+ ...decoded.iso ? { iso: decoded.iso } : {}
517
+ };
518
+ });
519
+ assertMonotonicTimeCoordinates(coordinates, dimension.name, source.id);
520
+ return coordinates;
521
+ }
522
+ function formatForecastStepLabel(source, dimension, coordinate) {
523
+ if (!isForecastStepDimension(dimension.name)) {
524
+ return void 0;
525
+ }
526
+ const reference = forecastReferenceDate(source);
527
+ const offsetMs = forecastStepOffsetMs(coordinate, dimension.units);
528
+ if (!reference || offsetMs === void 0) {
529
+ return void 0;
530
+ }
531
+ return formatHumanUtcDateTime(new Date(reference.getTime() + offsetMs));
532
+ }
533
+ function isForecastStepDimension(name) {
534
+ return normalizeDimensionName(name) === "step";
535
+ }
536
+ function forecastReferenceDate(source) {
537
+ const referenceDimension = source.dimensions.find(
538
+ (dimension) => ["forecastreferencetime", "forecastreferencedate"].includes(
539
+ normalizeDimensionName(dimension.name)
540
+ )
541
+ );
542
+ if (!referenceDimension) {
543
+ return void 0;
544
+ }
545
+ const referenceCoordinate = referenceDimension.coordinates?.[0];
546
+ if (referenceCoordinate === void 0) {
547
+ return void 0;
548
+ }
549
+ const decoded = decodeTimeCoordinate(referenceCoordinate, referenceDimension.units);
550
+ const referenceValue = decoded.iso ?? (typeof decoded.value === "string" ? decoded.value : void 0);
551
+ if (!referenceValue) {
552
+ return void 0;
553
+ }
554
+ const date = new Date(referenceValue);
555
+ return Number.isNaN(date.getTime()) ? void 0 : date;
556
+ }
557
+ function forecastStepOffsetMs(coordinate, units) {
558
+ const value = numericCoordinateValue(coordinate);
559
+ if (value === void 0) {
560
+ return void 0;
561
+ }
562
+ return value * forecastStepUnitMultiplier(units);
563
+ }
564
+ function numericCoordinateValue(coordinate) {
565
+ if (typeof coordinate === "number") {
566
+ return Number.isFinite(coordinate) ? coordinate : void 0;
567
+ }
568
+ if (typeof coordinate !== "string") {
569
+ return void 0;
570
+ }
571
+ const value = Number(coordinate);
572
+ return Number.isFinite(value) ? value : void 0;
573
+ }
574
+ function forecastStepUnitMultiplier(units) {
575
+ const normalizedUnits = units?.toLowerCase() ?? "";
576
+ if (normalizedUnits.includes("second")) {
577
+ return 1e3;
578
+ }
579
+ if (normalizedUnits.includes("minute")) {
580
+ return 6e4;
581
+ }
582
+ if (normalizedUnits.includes("day")) {
583
+ return 864e5;
584
+ }
585
+ return 36e5;
586
+ }
587
+ function normalizeDimensionName(name) {
588
+ return name.toLowerCase().replace(/[_\-\s]+/g, "");
589
+ }
590
+ var UTC_MONTH_LABELS = [
591
+ "Jan",
592
+ "Feb",
593
+ "Mar",
594
+ "Apr",
595
+ "May",
596
+ "Jun",
597
+ "Jul",
598
+ "Aug",
599
+ "Sep",
600
+ "Oct",
601
+ "Nov",
602
+ "Dec"
603
+ ];
604
+ function formatHumanUtcDateTime(input) {
605
+ const date = input instanceof Date ? input : new Date(input);
606
+ return [
607
+ UTC_MONTH_LABELS[date.getUTCMonth()],
608
+ " ",
609
+ date.getUTCDate(),
610
+ ", ",
611
+ date.getUTCFullYear(),
612
+ ", ",
613
+ padDatePart(date.getUTCHours()),
614
+ ":",
615
+ padDatePart(date.getUTCMinutes()),
616
+ " UTC"
617
+ ].join("");
618
+ }
619
+ function padDatePart(value) {
620
+ return String(value).padStart(2, "0");
621
+ }
622
+ function createDebouncer(callback, waitMs) {
623
+ let timeout;
624
+ let lastArgs;
625
+ const flush = () => {
626
+ if (!lastArgs) {
627
+ return;
628
+ }
629
+ const args = lastArgs;
630
+ lastArgs = void 0;
631
+ if (timeout) {
632
+ clearTimeout(timeout);
633
+ timeout = void 0;
634
+ }
635
+ callback(...args);
636
+ };
637
+ return {
638
+ call(...args) {
639
+ lastArgs = args;
640
+ if (timeout) {
641
+ clearTimeout(timeout);
642
+ }
643
+ timeout = setTimeout(flush, waitMs);
644
+ },
645
+ flush,
646
+ cancel() {
647
+ if (timeout) {
648
+ clearTimeout(timeout);
649
+ }
650
+ timeout = void 0;
651
+ lastArgs = void 0;
652
+ }
653
+ };
654
+ }
655
+ function defaultSelectorForDimension(dimension) {
656
+ const firstValue = dimension.coordinates?.[0];
657
+ if (firstValue !== void 0) {
658
+ return {
659
+ kind: "coordinate",
660
+ value: firstValue instanceof Date ? firstValue.toISOString() : firstValue
661
+ };
662
+ }
663
+ return { kind: "index", index: 0 };
664
+ }
665
+ function normalizeCoordinateSelector(dimension, value, sourceId) {
666
+ if (dimension.coordinates && !dimension.coordinates.some((coordinate) => sameCoordinate(coordinate, value))) {
667
+ throw new GridError({
668
+ code: "UNSUPPORTED_SELECTOR",
669
+ message: `Selector value "${String(value)}" does not match coordinates for dimension "${dimension.name}".`,
670
+ dimension: dimension.name,
671
+ sourceId
672
+ });
673
+ }
674
+ return { kind: "coordinate", value };
675
+ }
676
+ function assertIndexInRange(dimension, index, sourceId) {
677
+ if (!Number.isInteger(index) || index < 0 || index >= dimension.size) {
678
+ throw new GridError({
679
+ code: "UNSUPPORTED_SELECTOR",
680
+ message: `Index selector ${index} is outside dimension "${dimension.name}" size ${dimension.size}.`,
681
+ dimension: dimension.name,
682
+ sourceId
683
+ });
684
+ }
685
+ }
686
+ function findCoordinateForIso(dimension, iso) {
687
+ return dimension.coordinates?.map((coordinate) => decodeTimeCoordinate(coordinate, dimension.units)).find((coordinate) => coordinate.iso === iso || coordinate.value === iso)?.value;
688
+ }
689
+ function decodeTimeCoordinate(coordinate, units) {
690
+ if (coordinate instanceof Date) {
691
+ return { value: coordinate.toISOString(), iso: coordinate.toISOString() };
692
+ }
693
+ if (typeof coordinate === "string") {
694
+ const date = new Date(coordinate);
695
+ return Number.isNaN(date.getTime()) ? { value: coordinate } : { value: coordinate, iso: date.toISOString() };
696
+ }
697
+ const cfTime = decodeCfTime(coordinate, units);
698
+ return cfTime ? { value: coordinate, iso: cfTime } : { value: coordinate };
699
+ }
700
+ function decodeCfTime(value, units) {
701
+ if (!units) {
702
+ return void 0;
703
+ }
704
+ const match = /^(seconds|minutes|hours|days) since ([0-9]{4}-[0-9]{2}-[0-9]{2})(?:[ T]([0-9:.Z+-]+))?/i.exec(
705
+ units
706
+ );
707
+ if (!match) {
708
+ return void 0;
709
+ }
710
+ const unit = match[1]?.toLowerCase();
711
+ const date = match[2];
712
+ const time = match[3] ?? "00:00:00Z";
713
+ const origin = /* @__PURE__ */ new Date(`${date}T${time.replace(/Z?$/, "Z")}`);
714
+ if (Number.isNaN(origin.getTime())) {
715
+ return void 0;
716
+ }
717
+ const multiplier = unit === "seconds" ? 1e3 : unit === "minutes" ? 6e4 : unit === "hours" ? 36e5 : 864e5;
718
+ return new Date(origin.getTime() + value * multiplier).toISOString();
719
+ }
720
+ function assertMonotonicTimeCoordinates(coordinates, dimensionName, sourceId) {
721
+ let direction;
722
+ for (let index = 1; index < coordinates.length; index += 1) {
723
+ const previous = comparableTimeValue(coordinates[index - 1]);
724
+ const current = comparableTimeValue(coordinates[index]);
725
+ if (previous === current) {
726
+ throw new GridError({
727
+ code: "UNSUPPORTED_DIMENSION",
728
+ message: `Time dimension "${dimensionName}" has duplicate coordinate values.`,
729
+ dimension: dimensionName,
730
+ sourceId
731
+ });
732
+ }
733
+ const pairDirection = current > previous ? "ascending" : "descending";
734
+ direction ??= pairDirection;
735
+ if (pairDirection !== direction) {
736
+ throw new GridError({
737
+ code: "UNSUPPORTED_DIMENSION",
738
+ message: `Time dimension "${dimensionName}" must be monotonic for slider controls.`,
739
+ dimension: dimensionName,
740
+ sourceId
741
+ });
742
+ }
743
+ }
744
+ }
745
+ function comparableTimeValue(coordinate) {
746
+ if (!coordinate) {
747
+ return 0;
748
+ }
749
+ if (coordinate.iso) {
750
+ return new Date(coordinate.iso).getTime();
751
+ }
752
+ if (typeof coordinate.value === "number") {
753
+ return coordinate.value;
754
+ }
755
+ const parsed = new Date(coordinate.value).getTime();
756
+ return Number.isNaN(parsed) ? coordinate.index : parsed;
757
+ }
758
+ function sameCoordinate(left, right) {
759
+ const normalizedLeft = left instanceof Date ? left.toISOString() : left;
760
+ return normalizedLeft === right || String(normalizedLeft) === String(right);
761
+ }
762
+
763
+ // src/core/jaxray.ts
764
+ function createJaxraySource(input, options = {}) {
765
+ const dataset = isDatasetLike(input) ? input : arrayToDataset(input);
766
+ const variables = inferVariables(dataset, options);
767
+ const dimensions = inferDimensions(dataset, variables, options);
768
+ const source = {
769
+ id: options.id ?? dataset.id,
770
+ label: options.label,
771
+ source: options.source,
772
+ store: options.store ?? dataset.store,
773
+ zarrVersion: options.zarrVersion,
774
+ crs: options.crs ?? stringAttr(dataset.attrs, "crs") ?? "EPSG:4326",
775
+ proj4: options.proj4 ?? stringAttr(dataset.attrs, "proj4"),
776
+ bounds: options.bounds ?? inferBounds(dimensions, dataset.attrs),
777
+ spatialDimensions: options.spatialDimensions,
778
+ dimensions,
779
+ variables,
780
+ metadata: dataset.attrs,
781
+ readSlice: async (request) => readJaxraySlice(dataset, source, request.variable, request.selectors)
782
+ };
783
+ const firstSpatialVariable = variables.find(
784
+ (variable) => inferSpatialDimensions(source, variable.name)
785
+ );
786
+ source.spatialDimensions = options.spatialDimensions ?? inferSpatialDimensions(source) ?? (firstSpatialVariable ? inferSpatialDimensions(source, firstSpatialVariable.name) : void 0);
787
+ assertValidGridDataSource(source, {
788
+ variable: firstSpatialVariable?.name ?? variables[0]?.name
789
+ });
790
+ return source;
791
+ }
792
+ function isDatasetLike(input) {
793
+ return "data_vars" in input || "dataVars" in input || "variables" in input && !("shape" in input) || "sizes" in input;
794
+ }
795
+ function arrayToDataset(array) {
796
+ const name = array.name ?? "variable";
797
+ return {
798
+ data_vars: { [name]: array },
799
+ coords: array.coords,
800
+ attrs: array.attrs
801
+ };
802
+ }
803
+ function inferVariables(dataset, options) {
804
+ return Array.from(variableEntries(dataset)).map(([name, array]) => {
805
+ const dimensions = array.dims ?? array.dimensions;
806
+ if (!dimensions) {
807
+ throw new GridError({
808
+ code: "MISSING_DIMENSION",
809
+ message: `Jaxray variable "${name}" does not expose dims/dimensions metadata.`,
810
+ variable: name
811
+ });
812
+ }
813
+ const shape = array.shape ?? dimensions.map((dimensionName) => dimensionSize(dataset, dimensionName));
814
+ if (!shape) {
815
+ throw new GridError({
816
+ code: "MISSING_DIMENSION",
817
+ message: `Jaxray variable "${name}" does not expose shape metadata.`,
818
+ variable: name
819
+ });
820
+ }
821
+ const override = options.variables?.[name] ?? {};
822
+ return {
823
+ name,
824
+ path: stringAttr(array.attrs, "path"),
825
+ dtype: override.dtype ?? array.dtype ?? stringAttr(array.attrs, "dtype") ?? "float32",
826
+ dimensions,
827
+ shape,
828
+ chunks: override.chunks ?? array.chunks,
829
+ units: override.units ?? stringAttr(array.attrs, "units"),
830
+ longName: override.longName ?? stringAttr(array.attrs, "long_name"),
831
+ standardName: override.standardName ?? stringAttr(array.attrs, "standard_name"),
832
+ fillValue: override.fillValue ?? numberAttr(array.attrs, "_FillValue") ?? numberAttr(array.attrs, "fill_value") ?? null,
833
+ scaleFactor: override.scaleFactor ?? numberAttr(array.attrs, "scale_factor"),
834
+ addOffset: override.addOffset ?? numberAttr(array.attrs, "add_offset"),
835
+ attrs: array.attrs
836
+ };
837
+ });
838
+ }
839
+ function inferDimensions(dataset, variables, options) {
840
+ const dimensions = /* @__PURE__ */ new Map();
841
+ const coords = dataset.coords;
842
+ for (const variable of variables) {
843
+ variable.dimensions.forEach((name, index) => {
844
+ const existing = dimensions.get(name);
845
+ const coordinates = normalizeCoordinateArray(coordinateValue(coords, name));
846
+ const override = options.dimensions?.[name] ?? {};
847
+ const inferredKind = inferDimensionKind(name);
848
+ const size = variable.shape[index] ?? coordinates?.length ?? 0;
849
+ dimensions.set(name, {
850
+ ...existing,
851
+ name,
852
+ size: override.size ?? existing?.size ?? size,
853
+ kind: override.kind ?? existing?.kind ?? inferredKind,
854
+ coordinates: override.coordinates ?? existing?.coordinates ?? coordinates,
855
+ units: override.units ?? existing?.units,
856
+ calendar: override.calendar ?? existing?.calendar,
857
+ longName: override.longName ?? existing?.longName,
858
+ standardName: override.standardName ?? existing?.standardName,
859
+ ascending: override.ascending ?? existing?.ascending ?? inferAscending(coordinates),
860
+ attrs: override.attrs ?? existing?.attrs
861
+ });
862
+ });
863
+ }
864
+ return Array.from(dimensions.values());
865
+ }
866
+ function inferDimensionKind(name) {
867
+ const normalized = name.toLowerCase();
868
+ if (["lon", "lng", "longitude", "x"].includes(normalized)) {
869
+ return "x";
870
+ }
871
+ if (["lat", "latitude", "y"].includes(normalized)) {
872
+ return "y";
873
+ }
874
+ if (["time", "valid_time", "date"].includes(normalized)) {
875
+ return "time";
876
+ }
877
+ if (["band", "month"].includes(normalized)) {
878
+ return "band";
879
+ }
880
+ if (["level", "height", "pressure"].includes(normalized)) {
881
+ return "vertical";
882
+ }
883
+ return "other";
884
+ }
885
+ function inferBounds(dimensions, attrs) {
886
+ const attrBounds = attrs?.bounds;
887
+ if (Array.isArray(attrBounds) && attrBounds.length === 4 && attrBounds.every((value) => typeof value === "number")) {
888
+ return attrBounds;
889
+ }
890
+ const x = dimensions.find((dimension) => dimension.kind === "x");
891
+ const y = dimensions.find((dimension) => dimension.kind === "y");
892
+ const xCoordinates = x?.coordinates?.filter(
893
+ (value) => typeof value === "number"
894
+ );
895
+ const yCoordinates = y?.coordinates?.filter(
896
+ (value) => typeof value === "number"
897
+ );
898
+ if (!xCoordinates?.length || !yCoordinates?.length) {
899
+ return void 0;
900
+ }
901
+ return [
902
+ Math.min(...xCoordinates),
903
+ Math.min(...yCoordinates),
904
+ Math.max(...xCoordinates),
905
+ Math.max(...yCoordinates)
906
+ ];
907
+ }
908
+ async function readJaxraySlice(dataset, source, variableName, selectors = {}) {
909
+ const entry = variableEntries(dataset).find(([name2]) => name2 === variableName);
910
+ if (!entry) {
911
+ throw new GridError({
912
+ code: "MISSING_VARIABLE",
913
+ message: `Jaxray source cannot read missing variable "${variableName}".`,
914
+ variable: variableName,
915
+ sourceId: source.id
916
+ });
917
+ }
918
+ const [name, array] = entry;
919
+ const variable = source.variables.find((candidate) => candidate.name === name);
920
+ if (!variable) {
921
+ throw new GridError({
922
+ code: "MISSING_VARIABLE",
923
+ message: `Jaxray source metadata is missing variable "${name}".`,
924
+ variable: name,
925
+ sourceId: source.id
926
+ });
927
+ }
928
+ const normalizedSelectors = isNormalizedSelectors(selectors) ? selectors : normalizeSelectors(source, variable.name, selectors);
929
+ const selection = variable.dimensions.reduce(
930
+ (accumulator, dimensionName) => {
931
+ const dimension = findDimension(source, dimensionName);
932
+ if (!dimension) {
933
+ return accumulator;
934
+ }
935
+ accumulator[dimensionName] = selectorToIndex(
936
+ dimension,
937
+ normalizedSelectors[dimensionName] ?? { kind: "index", index: 0 }
938
+ );
939
+ return accumulator;
940
+ },
941
+ {}
942
+ );
943
+ const value = await readArrayValue(array, variable, selection);
944
+ return {
945
+ variable: name,
946
+ selectors: normalizedSelectors,
947
+ dimensions: [],
948
+ shape: [],
949
+ data: [value],
950
+ unit: variable.units,
951
+ fillValue: variable.fillValue
952
+ };
953
+ }
954
+ async function readArrayValue(array, variable, selection) {
955
+ if (array.get) {
956
+ return array.get(selection);
957
+ }
958
+ if (array.isel) {
959
+ const selected = await array.isel(selection);
960
+ const computed = selected.isLazy && selected.compute ? await selected.compute() : selected;
961
+ return scalarFromArrayLike(computed.data ?? computed.values);
962
+ }
963
+ const data = array.data ?? array.values;
964
+ if (!data) {
965
+ throw new GridError({
966
+ code: "SOURCE_LOAD_FAILED",
967
+ message: `Jaxray variable "${variable.name}" does not expose readable data for query helpers.`,
968
+ variable: variable.name
969
+ });
970
+ }
971
+ let offset = 0;
972
+ let stride = 1;
973
+ for (let index = variable.dimensions.length - 1; index >= 0; index -= 1) {
974
+ const dimensionName = variable.dimensions[index];
975
+ if (!dimensionName) {
976
+ continue;
977
+ }
978
+ offset += (selection[dimensionName] ?? 0) * stride;
979
+ stride *= variable.shape[index] ?? 1;
980
+ }
981
+ return data[offset] ?? Number.NaN;
982
+ }
983
+ function scalarFromArrayLike(value) {
984
+ if (typeof value === "number") {
985
+ return value;
986
+ }
987
+ if (Array.isArray(value) || ArrayBuffer.isView(value)) {
988
+ const first = Array.from(value)[0];
989
+ return scalarFromArrayLike(first);
990
+ }
991
+ return Number.NaN;
992
+ }
993
+ function variableEntries(dataset) {
994
+ if (Array.isArray(dataset.dataVars) && typeof dataset.getVariable === "function") {
995
+ return dataset.dataVars.map((name) => [name, dataset.getVariable?.(name) ?? {}]);
996
+ }
997
+ if (Array.isArray(dataset.dataVars)) {
998
+ throw new GridError({
999
+ code: "MISSING_VARIABLE",
1000
+ message: "Jaxray Dataset exposes dataVars names but no getVariable(name) method."
1001
+ });
1002
+ }
1003
+ const variables = dataset.data_vars ?? dataset.dataVars ?? dataset.variables ?? {};
1004
+ if (variables instanceof Map) {
1005
+ return Array.from(variables.entries());
1006
+ }
1007
+ return Object.entries(variables);
1008
+ }
1009
+ function normalizeCoordinateArray(input) {
1010
+ const raw = Array.isArray(input) ? input : ArrayBuffer.isView(input) ? Array.from(input) : typeof input === "object" && input !== null && "values" in input && isArrayLike(input.values) ? Array.from(input.values) : typeof input === "object" && input !== null && "data" in input && isArrayLike(input.data) ? Array.from(input.data) : void 0;
1011
+ return raw?.filter(
1012
+ (value) => typeof value === "number" || typeof value === "string"
1013
+ );
1014
+ }
1015
+ function coordinateValue(coords, name) {
1016
+ if (!coords) {
1017
+ return void 0;
1018
+ }
1019
+ return coords instanceof Map ? coords.get(name) : coords[name];
1020
+ }
1021
+ function dimensionSize(dataset, dimensionName) {
1022
+ const sizes = dataset.sizes;
1023
+ if (!sizes) {
1024
+ return 0;
1025
+ }
1026
+ return sizes instanceof Map ? sizes.get(dimensionName) ?? 0 : sizes[dimensionName] ?? 0;
1027
+ }
1028
+ function isArrayLike(value) {
1029
+ return Array.isArray(value) || ArrayBuffer.isView(value) || typeof value === "object" && value !== null && "length" in value && typeof value.length === "number";
1030
+ }
1031
+ function inferAscending(coordinates) {
1032
+ if (!coordinates || coordinates.length < 2) {
1033
+ return void 0;
1034
+ }
1035
+ const first = coordinates[0];
1036
+ const second = coordinates[1];
1037
+ return typeof first === "number" && typeof second === "number" ? second > first : void 0;
1038
+ }
1039
+ function stringAttr(attrs, name) {
1040
+ const value = attrs?.[name];
1041
+ return typeof value === "string" ? value : void 0;
1042
+ }
1043
+ function numberAttr(attrs, name) {
1044
+ const value = attrs?.[name];
1045
+ return typeof value === "number" ? value : void 0;
1046
+ }
1047
+ function isNormalizedSelectors(selectors) {
1048
+ return Object.values(selectors).every(
1049
+ (value) => typeof value === "object" && value !== null && "kind" in value
1050
+ );
1051
+ }
1052
+
1053
+ // src/core/preflight.ts
1054
+ var dtypeByteSizes = {
1055
+ int8: 1,
1056
+ uint8: 1,
1057
+ i1: 1,
1058
+ u1: 1,
1059
+ int16: 2,
1060
+ uint16: 2,
1061
+ i2: 2,
1062
+ u2: 2,
1063
+ int32: 4,
1064
+ uint32: 4,
1065
+ float32: 4,
1066
+ i4: 4,
1067
+ u4: 4,
1068
+ f4: 4,
1069
+ float64: 8,
1070
+ f8: 8
1071
+ };
1072
+ function preflightGridRequest(options) {
1073
+ const errors = [];
1074
+ const warnings = [];
1075
+ const dimensions = {};
1076
+ const variable = findVariable(options.source, options.variable);
1077
+ if (!variable) {
1078
+ errors.push(
1079
+ new GridError({
1080
+ code: "MISSING_VARIABLE",
1081
+ message: `Variable "${options.variable}" is not present in the grid source.`,
1082
+ sourceId: options.source.id,
1083
+ variable: options.variable
1084
+ })
1085
+ );
1086
+ return { ok: false, dimensions, warnings, errors };
1087
+ }
1088
+ const spatialDimensions = inferSpatialDimensions(options.source, variable.name);
1089
+ for (const dimensionName of variable.dimensions) {
1090
+ const dimension = findDimension(options.source, dimensionName);
1091
+ if (!dimension) {
1092
+ errors.push(
1093
+ new GridError({
1094
+ code: "MISSING_DIMENSION",
1095
+ message: `Variable "${variable.name}" references missing dimension "${dimensionName}".`,
1096
+ dimension: dimensionName,
1097
+ sourceId: options.source.id,
1098
+ variable: variable.name
1099
+ })
1100
+ );
1101
+ continue;
1102
+ }
1103
+ dimensions[dimension.name] = selectedDimensionSize({
1104
+ bounds: options.bounds,
1105
+ dimension,
1106
+ isSpatialX: spatialDimensions?.x === dimension.name,
1107
+ isSpatialY: spatialDimensions?.y === dimension.name,
1108
+ selectorIsPresent: options.selectors?.[dimension.name] !== void 0,
1109
+ sourceBounds: options.source.bounds,
1110
+ timeRange: options.timeRange,
1111
+ warnings
1112
+ });
1113
+ }
1114
+ const cells = Object.values(dimensions).reduce((total, size) => total * size, 1);
1115
+ const byteSize = dtypeByteSize(variable.dtype);
1116
+ const bytes = byteSize === void 0 ? void 0 : cells * byteSize;
1117
+ if (byteSize === void 0) {
1118
+ warnings.push(`Variable "${variable.name}" has unknown dtype byte size "${variable.dtype}".`);
1119
+ }
1120
+ if (options.limits?.maxCells !== void 0 && cells > options.limits.maxCells) {
1121
+ errors.push(
1122
+ new GridError({
1123
+ code: "QUERY_TOO_EXPENSIVE",
1124
+ message: `Grid request would select approximately ${cells} cells, above the limit of ${options.limits.maxCells}.`,
1125
+ context: { cells, maxCells: options.limits.maxCells },
1126
+ sourceId: options.source.id,
1127
+ variable: variable.name
1128
+ })
1129
+ );
1130
+ }
1131
+ if (bytes !== void 0 && options.limits?.maxBytes !== void 0 && bytes > options.limits.maxBytes) {
1132
+ errors.push(
1133
+ new GridError({
1134
+ code: "QUERY_TOO_EXPENSIVE",
1135
+ message: `Grid request would read approximately ${bytes} uncompressed bytes, above the limit of ${options.limits.maxBytes}.`,
1136
+ context: { bytes, maxBytes: options.limits.maxBytes },
1137
+ sourceId: options.source.id,
1138
+ variable: variable.name
1139
+ })
1140
+ );
1141
+ }
1142
+ return {
1143
+ ok: errors.length === 0,
1144
+ cells,
1145
+ bytes,
1146
+ dimensions,
1147
+ warnings,
1148
+ errors
1149
+ };
1150
+ }
1151
+ function selectedDimensionSize(options) {
1152
+ if (options.selectorIsPresent) {
1153
+ return 1;
1154
+ }
1155
+ if (options.bounds && options.isSpatialX) {
1156
+ return spatialCount(options.dimension, options.bounds[0], options.bounds[2], {
1157
+ axis: "x",
1158
+ sourceBounds: options.sourceBounds,
1159
+ warnings: options.warnings
1160
+ });
1161
+ }
1162
+ if (options.bounds && options.isSpatialY) {
1163
+ return spatialCount(options.dimension, options.bounds[1], options.bounds[3], {
1164
+ axis: "y",
1165
+ sourceBounds: options.sourceBounds,
1166
+ warnings: options.warnings
1167
+ });
1168
+ }
1169
+ if (options.timeRange && options.dimension.kind === "time") {
1170
+ return timeRangeCount(options.dimension, options.timeRange, options.warnings);
1171
+ }
1172
+ return options.dimension.size;
1173
+ }
1174
+ function spatialCount(dimension, lowerBound, upperBound, options) {
1175
+ const low = Math.min(lowerBound, upperBound);
1176
+ const high = Math.max(lowerBound, upperBound);
1177
+ const numericCoordinates = dimension.coordinates?.filter(
1178
+ (coordinate) => typeof coordinate === "number"
1179
+ );
1180
+ if (numericCoordinates?.length) {
1181
+ return numericCoordinates.filter((coordinate) => coordinate >= low && coordinate <= high).length;
1182
+ }
1183
+ const sourceRange = spatialRange(options.sourceBounds, options.axis);
1184
+ if (!sourceRange) {
1185
+ options.warnings.push(
1186
+ `Dimension "${dimension.name}" has no coordinates or source bounds, so preflight used the full dimension size.`
1187
+ );
1188
+ return dimension.size;
1189
+ }
1190
+ const [sourceLow, sourceHigh] = sourceRange;
1191
+ const span = sourceHigh - sourceLow;
1192
+ if (span <= 0 || dimension.size <= 0) {
1193
+ return 0;
1194
+ }
1195
+ const overlap = Math.max(0, Math.min(high, sourceHigh) - Math.max(low, sourceLow));
1196
+ if (overlap === 0) {
1197
+ return 0;
1198
+ }
1199
+ return Math.max(1, Math.min(dimension.size, Math.ceil(overlap / span * dimension.size)));
1200
+ }
1201
+ function timeRangeCount(dimension, timeRange, warnings) {
1202
+ if (!dimension.coordinates?.length) {
1203
+ warnings.push(
1204
+ `Time dimension "${dimension.name}" has no coordinates, so preflight used the full time size.`
1205
+ );
1206
+ return dimension.size;
1207
+ }
1208
+ const start = timeToMillis(timeRange.start, dimension.units);
1209
+ const end = timeToMillis(timeRange.end, dimension.units);
1210
+ if (start === void 0 || end === void 0) {
1211
+ warnings.push(
1212
+ `Time range for "${dimension.name}" could not be parsed, so preflight used the full time size.`
1213
+ );
1214
+ return dimension.size;
1215
+ }
1216
+ const low = Math.min(start, end);
1217
+ const high = Math.max(start, end);
1218
+ let count = 0;
1219
+ for (const coordinate of dimension.coordinates) {
1220
+ const value = timeToMillis(coordinate, dimension.units);
1221
+ if (value !== void 0 && value >= low && value <= high) {
1222
+ count += 1;
1223
+ }
1224
+ }
1225
+ return count;
1226
+ }
1227
+ function timeToMillis(value, units) {
1228
+ if (value instanceof Date) {
1229
+ return value.getTime();
1230
+ }
1231
+ if (typeof value === "string") {
1232
+ const parsed = Date.parse(value);
1233
+ return Number.isNaN(parsed) ? void 0 : parsed;
1234
+ }
1235
+ if (typeof value === "number") {
1236
+ return numericTimeToMillis(value, units);
1237
+ }
1238
+ return void 0;
1239
+ }
1240
+ function numericTimeToMillis(value, units) {
1241
+ if (!units) {
1242
+ return void 0;
1243
+ }
1244
+ const match = /^(seconds?|minutes?|hours?|days?) since (.+)$/i.exec(units.trim());
1245
+ if (!match) {
1246
+ return void 0;
1247
+ }
1248
+ const [, unit, epoch] = match;
1249
+ if (!unit || !epoch) {
1250
+ return void 0;
1251
+ }
1252
+ const epochMillis = Date.parse(epoch);
1253
+ if (Number.isNaN(epochMillis)) {
1254
+ return void 0;
1255
+ }
1256
+ const multiplier = unit.toLowerCase().startsWith("second") ? 1e3 : unit.toLowerCase().startsWith("minute") ? 60 * 1e3 : unit.toLowerCase().startsWith("hour") ? 60 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
1257
+ return epochMillis + value * multiplier;
1258
+ }
1259
+ function spatialRange(bounds, axis) {
1260
+ if (!bounds) {
1261
+ return void 0;
1262
+ }
1263
+ return axis === "x" ? [bounds[0], bounds[2]] : [bounds[1], bounds[3]];
1264
+ }
1265
+ function dtypeByteSize(dtype) {
1266
+ const normalized = dtype.toLowerCase().replace(/^[<>|]/, "");
1267
+ return dtypeByteSizes[normalized];
1268
+ }
1269
+
1270
+ // src/core/query.ts
1271
+ async function queryGrid(request) {
1272
+ if (request.signal?.aborted) {
1273
+ throw abortedError();
1274
+ }
1275
+ if (request.source.queryGeometry) {
1276
+ return request.source.queryGeometry({
1277
+ variable: request.variable,
1278
+ geometry: request.geometry,
1279
+ selectors: request.selectors,
1280
+ maxCells: request.maxCells,
1281
+ signal: request.signal
1282
+ });
1283
+ }
1284
+ switch (request.geometry.type) {
1285
+ case "Point":
1286
+ return queryPoint(
1287
+ request.source,
1288
+ request.variable,
1289
+ request.geometry.coordinates,
1290
+ request.selectors,
1291
+ request.signal
1292
+ );
1293
+ case "BoundingBox":
1294
+ return queryBoundingBox({
1295
+ ...request,
1296
+ geometry: request.geometry
1297
+ });
1298
+ case "Polygon":
1299
+ return queryPolygon({
1300
+ ...request,
1301
+ geometry: request.geometry
1302
+ });
1303
+ default:
1304
+ throw new GridError({
1305
+ code: "UNSUPPORTED_GEOMETRY",
1306
+ message: "Unsupported grid query geometry."
1307
+ });
1308
+ }
1309
+ }
1310
+ async function queryPoint(source, variableName, coordinates, selectors = {}, signal) {
1311
+ const context = queryContext(source, variableName, selectors);
1312
+ const xIndex = nearestCoordinateIndex(context.xDimension, coordinates[0], source.bounds, "x");
1313
+ const yIndex = nearestCoordinateIndex(context.yDimension, coordinates[1], source.bounds, "y");
1314
+ const sample = await readSample(source, context, xIndex, yIndex, signal);
1315
+ return {
1316
+ variable: variableName,
1317
+ selectors: context.normalizedSelectors,
1318
+ geometry: { type: "Point", coordinates },
1319
+ unit: context.variable.units,
1320
+ samples: [sample],
1321
+ warnings: []
1322
+ };
1323
+ }
1324
+ async function queryBoundingBox(request) {
1325
+ const context = queryContext(request.source, request.variable, request.selectors);
1326
+ const [west, south, east, north] = request.geometry.bounds;
1327
+ const xIndexes = coordinateIndexesInBounds(context.xDimension, west, east);
1328
+ const yIndexes = coordinateIndexesInBounds(context.yDimension, south, north);
1329
+ const estimatedCells = xIndexes.length * yIndexes.length;
1330
+ const maxCells = request.maxCells ?? 1e4;
1331
+ if (estimatedCells > maxCells) {
1332
+ throw new GridError({
1333
+ code: "QUERY_TOO_EXPENSIVE",
1334
+ message: `Bounding-box query would inspect ${estimatedCells} cells, above the limit of ${maxCells}.`,
1335
+ context: { estimatedCells, maxCells }
1336
+ });
1337
+ }
1338
+ const samples = [];
1339
+ for (const yIndex of yIndexes) {
1340
+ for (const xIndex of xIndexes) {
1341
+ if (request.signal?.aborted) {
1342
+ throw abortedError();
1343
+ }
1344
+ samples.push(await readSample(request.source, context, xIndex, yIndex, request.signal));
1345
+ }
1346
+ }
1347
+ return {
1348
+ variable: request.variable,
1349
+ selectors: context.normalizedSelectors,
1350
+ geometry: request.geometry,
1351
+ unit: context.variable.units,
1352
+ samples,
1353
+ warnings: estimatedCells > 1e3 ? ["Large bounded query; consider lowering maxCells or querying a coarser grid."] : []
1354
+ };
1355
+ }
1356
+ async function queryPolygon(request) {
1357
+ const bounds = polygonBounds(request.geometry.coordinates);
1358
+ const bounded = await queryBoundingBox({
1359
+ ...request,
1360
+ geometry: { type: "BoundingBox", bounds }
1361
+ });
1362
+ return {
1363
+ ...bounded,
1364
+ geometry: request.geometry,
1365
+ samples: bounded.samples.filter(
1366
+ (sample) => pointInPolygon(sample.coordinates, request.geometry.coordinates)
1367
+ ),
1368
+ warnings: [
1369
+ ...bounded.warnings,
1370
+ "Polygon queries are evaluated by scanning the polygon bounding box."
1371
+ ]
1372
+ };
1373
+ }
1374
+ function queryContext(source, variableName, selectors = {}) {
1375
+ const variable = findVariable(source, variableName);
1376
+ if (!variable) {
1377
+ throw new GridError({
1378
+ code: "MISSING_VARIABLE",
1379
+ message: `Variable "${variableName}" is not present in the grid source.`,
1380
+ variable: variableName,
1381
+ sourceId: source.id
1382
+ });
1383
+ }
1384
+ const spatialDimensions = inferSpatialDimensions(source, variable.name);
1385
+ if (!spatialDimensions) {
1386
+ throw new GridError({
1387
+ code: "MISSING_SPATIAL_DIMENSIONS",
1388
+ message: `Variable "${variable.name}" needs spatial dimensions for map queries.`,
1389
+ variable: variable.name,
1390
+ sourceId: source.id
1391
+ });
1392
+ }
1393
+ const xDimension = findDimension(source, spatialDimensions.x);
1394
+ const yDimension = findDimension(source, spatialDimensions.y);
1395
+ if (!xDimension || !yDimension) {
1396
+ throw new GridError({
1397
+ code: "MISSING_SPATIAL_DIMENSIONS",
1398
+ message: "Spatial dimensions are missing from the grid source.",
1399
+ variable: variable.name,
1400
+ sourceId: source.id
1401
+ });
1402
+ }
1403
+ if (!source.readSlice) {
1404
+ throw new GridError({
1405
+ code: "SOURCE_LOAD_FAILED",
1406
+ message: "GridDataSource must implement readSlice or queryGeometry before query helpers can read values.",
1407
+ sourceId: source.id,
1408
+ variable: variable.name
1409
+ });
1410
+ }
1411
+ return {
1412
+ variable,
1413
+ normalizedSelectors: normalizeSelectors(source, variable.name, selectors),
1414
+ xDimension,
1415
+ yDimension,
1416
+ spatialDimensions
1417
+ };
1418
+ }
1419
+ async function readSample(source, context, xIndex, yIndex, signal) {
1420
+ if (signal?.aborted) {
1421
+ throw abortedError();
1422
+ }
1423
+ const selectors = selectorsWithSpatialIndexes(
1424
+ context.normalizedSelectors,
1425
+ context.spatialDimensions.x,
1426
+ xIndex,
1427
+ context.spatialDimensions.y,
1428
+ yIndex
1429
+ );
1430
+ const slice = await source.readSlice?.({
1431
+ variable: context.variable.name,
1432
+ selectors,
1433
+ signal
1434
+ });
1435
+ const rawValue = slice?.data[0];
1436
+ const value = rawValue === void 0 || rawValue === context.variable.fillValue ? null : rawValue * (context.variable.scaleFactor ?? 1) + (context.variable.addOffset ?? 0);
1437
+ return {
1438
+ coordinates: [
1439
+ coordinateAt(context.xDimension, xIndex, source.bounds, "x"),
1440
+ coordinateAt(context.yDimension, yIndex, source.bounds, "y")
1441
+ ],
1442
+ indexes: {
1443
+ [context.spatialDimensions.x]: xIndex,
1444
+ [context.spatialDimensions.y]: yIndex
1445
+ },
1446
+ rawValue: rawValue ?? null,
1447
+ value
1448
+ };
1449
+ }
1450
+ function selectorsWithSpatialIndexes(selectors, xDimension, xIndex, yDimension, yIndex) {
1451
+ return {
1452
+ ...selectors,
1453
+ [xDimension]: { kind: "index", index: xIndex },
1454
+ [yDimension]: { kind: "index", index: yIndex }
1455
+ };
1456
+ }
1457
+ function nearestCoordinateIndex(dimension, value, bounds, axis) {
1458
+ if (!dimension.coordinates) {
1459
+ return boundedCoordinateIndex(dimension, value, bounds, axis);
1460
+ }
1461
+ let bestIndex = 0;
1462
+ let bestDistance = Number.POSITIVE_INFINITY;
1463
+ dimension.coordinates.forEach((coordinate, index) => {
1464
+ if (typeof coordinate !== "number") {
1465
+ return;
1466
+ }
1467
+ const distance = Math.abs(coordinate - value);
1468
+ if (distance < bestDistance) {
1469
+ bestDistance = distance;
1470
+ bestIndex = index;
1471
+ }
1472
+ });
1473
+ return bestIndex;
1474
+ }
1475
+ function coordinateIndexesInBounds(dimension, min, max) {
1476
+ if (!dimension.coordinates) {
1477
+ return range(
1478
+ clamp(Math.floor(min), 0, dimension.size - 1),
1479
+ clamp(Math.ceil(max), 0, dimension.size - 1)
1480
+ );
1481
+ }
1482
+ const low = Math.min(min, max);
1483
+ const high = Math.max(min, max);
1484
+ return dimension.coordinates.reduce((indexes, coordinate, index) => {
1485
+ if (typeof coordinate === "number" && coordinate >= low && coordinate <= high) {
1486
+ indexes.push(index);
1487
+ }
1488
+ return indexes;
1489
+ }, []);
1490
+ }
1491
+ function coordinateAt(dimension, index, bounds, axis) {
1492
+ const coordinate = dimension.coordinates?.[index];
1493
+ if (typeof coordinate === "number") {
1494
+ return coordinate;
1495
+ }
1496
+ return boundedCoordinateAt(dimension, index, bounds, axis);
1497
+ }
1498
+ function boundedCoordinateIndex(dimension, value, bounds, axis) {
1499
+ const range2 = spatialRange2(bounds, axis);
1500
+ if (!range2) {
1501
+ return clamp(Math.round(value), 0, dimension.size - 1);
1502
+ }
1503
+ const [min, max] = range2;
1504
+ const span = max - min;
1505
+ if (span <= 0 || dimension.size <= 1) {
1506
+ return 0;
1507
+ }
1508
+ const ascending = dimension.ascending !== false;
1509
+ const ratio = ascending ? (value - min) / span : (max - value) / span;
1510
+ return clamp(Math.round(ratio * (dimension.size - 1)), 0, dimension.size - 1);
1511
+ }
1512
+ function boundedCoordinateAt(dimension, index, bounds, axis) {
1513
+ const range2 = spatialRange2(bounds, axis);
1514
+ if (!range2 || dimension.size <= 1) {
1515
+ return index;
1516
+ }
1517
+ const [min, max] = range2;
1518
+ const ratio = clamp(index, 0, dimension.size - 1) / (dimension.size - 1);
1519
+ return dimension.ascending === false ? max - ratio * (max - min) : min + ratio * (max - min);
1520
+ }
1521
+ function spatialRange2(bounds, axis) {
1522
+ if (!bounds) {
1523
+ return void 0;
1524
+ }
1525
+ return axis === "x" ? [bounds[0], bounds[2]] : [bounds[1], bounds[3]];
1526
+ }
1527
+ function range(start, end) {
1528
+ const values = [];
1529
+ for (let index = start; index <= end; index += 1) {
1530
+ values.push(index);
1531
+ }
1532
+ return values;
1533
+ }
1534
+ function clamp(value, min, max) {
1535
+ return Math.min(max, Math.max(min, value));
1536
+ }
1537
+ function polygonBounds(coordinates) {
1538
+ return coordinates.reduce(
1539
+ (bounds, [x, y]) => [
1540
+ Math.min(bounds[0], x),
1541
+ Math.min(bounds[1], y),
1542
+ Math.max(bounds[2], x),
1543
+ Math.max(bounds[3], y)
1544
+ ],
1545
+ [
1546
+ Number.POSITIVE_INFINITY,
1547
+ Number.POSITIVE_INFINITY,
1548
+ Number.NEGATIVE_INFINITY,
1549
+ Number.NEGATIVE_INFINITY
1550
+ ]
1551
+ );
1552
+ }
1553
+ function pointInPolygon(point, polygon) {
1554
+ let inside = false;
1555
+ for (let index = 0, previous = polygon.length - 1; index < polygon.length; previous = index, index += 1) {
1556
+ const currentPoint = polygon[index];
1557
+ const previousPoint = polygon[previous];
1558
+ if (!currentPoint || !previousPoint) {
1559
+ continue;
1560
+ }
1561
+ const intersects = currentPoint[1] > point[1] !== previousPoint[1] > point[1] && point[0] < (previousPoint[0] - currentPoint[0]) * (point[1] - currentPoint[1]) / (previousPoint[1] - currentPoint[1]) + currentPoint[0];
1562
+ if (intersects) {
1563
+ inside = !inside;
1564
+ }
1565
+ }
1566
+ return inside;
1567
+ }
1568
+ function abortedError() {
1569
+ return new GridError({
1570
+ code: "ABORTED",
1571
+ message: "Grid query was aborted."
1572
+ });
1573
+ }
1574
+
1575
+ exports.GridError = GridError;
1576
+ exports.assertValidGridDataSource = assertValidGridDataSource;
1577
+ exports.colorScaleDisplayDomain = colorScaleDisplayDomain;
1578
+ exports.colorScaleToRendererOptions = colorScaleToRendererOptions;
1579
+ exports.convertDisplayValue = convertDisplayValue;
1580
+ exports.coordinateSelector = coordinateSelector;
1581
+ exports.createDebouncer = createDebouncer;
1582
+ exports.createJaxraySource = createJaxraySource;
1583
+ exports.findDimension = findDimension;
1584
+ exports.findVariable = findVariable;
1585
+ exports.indexSelector = indexSelector;
1586
+ exports.inferSpatialDimensions = inferSpatialDimensions;
1587
+ exports.isGridError = isGridError;
1588
+ exports.isNumericDType = isNumericDType;
1589
+ exports.isoTimeSelector = isoTimeSelector;
1590
+ exports.listTimeCoordinates = listTimeCoordinates;
1591
+ exports.normalizeSelectorValue = normalizeSelectorValue;
1592
+ exports.normalizeSelectors = normalizeSelectors;
1593
+ exports.paletteToRendererColorMap = paletteToRendererColorMap;
1594
+ exports.preflightGridRequest = preflightGridRequest;
1595
+ exports.queryGrid = queryGrid;
1596
+ exports.queryPoint = queryPoint;
1597
+ exports.selectorToIndex = selectorToIndex;
1598
+ exports.toGridError = toGridError;
1599
+ exports.toRendererSelectors = toRendererSelectors;
1600
+ exports.validateColorScale = validateColorScale;
1601
+ exports.validateGridDataSource = validateGridDataSource;
1602
+ //# sourceMappingURL=index.cjs.map
1603
+ //# sourceMappingURL=index.cjs.map