@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,3953 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+
6
+ // src/core/errors.ts
7
+ var GridError = class extends Error {
8
+ code;
9
+ cause;
10
+ dimension;
11
+ variable;
12
+ sourceId;
13
+ context;
14
+ constructor(details) {
15
+ super(details.message);
16
+ this.name = "GridError";
17
+ this.code = details.code;
18
+ this.cause = details.cause;
19
+ this.dimension = details.dimension;
20
+ this.variable = details.variable;
21
+ this.sourceId = details.sourceId;
22
+ this.context = details.context;
23
+ }
24
+ };
25
+ function isGridError(error) {
26
+ return error instanceof GridError;
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 normalizeSelectors(source, variableName, selectors = {}) {
388
+ const variable = findVariable(source, variableName);
389
+ if (!variable) {
390
+ throw new GridError({
391
+ code: "MISSING_VARIABLE",
392
+ message: `Variable "${variableName}" is not present in the grid source.`,
393
+ variable: variableName,
394
+ sourceId: source.id
395
+ });
396
+ }
397
+ const normalized = {};
398
+ for (const dimensionName of variable.dimensions) {
399
+ const dimension = findDimension(source, dimensionName);
400
+ if (!dimension) {
401
+ throw new GridError({
402
+ code: "MISSING_DIMENSION",
403
+ message: `Selector normalization cannot find dimension "${dimensionName}".`,
404
+ dimension: dimensionName,
405
+ variable: variableName,
406
+ sourceId: source.id
407
+ });
408
+ }
409
+ const input = selectors[dimensionName];
410
+ if (input === void 0) {
411
+ normalized[dimensionName] = defaultSelectorForDimension(dimension);
412
+ continue;
413
+ }
414
+ normalized[dimensionName] = normalizeSelectorValue(dimension, input, source.id);
415
+ }
416
+ return normalized;
417
+ }
418
+ function normalizeSelectorValue(dimension, input, sourceId) {
419
+ if (input instanceof Date) {
420
+ return { kind: "coordinate", value: input.toISOString() };
421
+ }
422
+ if (typeof input !== "object") {
423
+ return normalizeCoordinateSelector(dimension, input, sourceId);
424
+ }
425
+ if (input.kind === "coordinate") {
426
+ const value = input.value instanceof Date ? input.value.toISOString() : input.value;
427
+ return normalizeCoordinateSelector(dimension, value, sourceId);
428
+ }
429
+ if (input.kind === "index") {
430
+ assertIndexInRange(dimension, input.index, sourceId);
431
+ const value = dimension.coordinates?.[input.index];
432
+ return {
433
+ kind: "index",
434
+ index: input.index,
435
+ value: value instanceof Date ? value.toISOString() : value
436
+ };
437
+ }
438
+ if (input.kind === "isoTime") {
439
+ const value = findCoordinateForIso(dimension, input.iso);
440
+ return value === void 0 ? { kind: "isoTime", iso: input.iso } : { kind: "isoTime", iso: input.iso, value };
441
+ }
442
+ throw new GridError({
443
+ code: "UNSUPPORTED_SELECTOR",
444
+ message: `Unsupported selector for dimension "${dimension.name}".`,
445
+ dimension: dimension.name,
446
+ sourceId
447
+ });
448
+ }
449
+ function toRendererSelectors(selectors) {
450
+ const output = {};
451
+ for (const [dimensionName, selector] of Object.entries(selectors)) {
452
+ if (selector.kind === "index") {
453
+ output[dimensionName] = { selected: selector.index, type: "index" };
454
+ continue;
455
+ }
456
+ if (selector.kind === "isoTime") {
457
+ output[dimensionName] = {
458
+ selected: selector.value ?? selector.iso,
459
+ type: "value"
460
+ };
461
+ continue;
462
+ }
463
+ output[dimensionName] = { selected: selector.value, type: "value" };
464
+ }
465
+ return output;
466
+ }
467
+ function selectorToIndex(dimension, selector) {
468
+ if (selector.kind === "index") {
469
+ return selector.index;
470
+ }
471
+ const selected = selector.kind === "isoTime" ? selector.value ?? selector.iso : selector.value;
472
+ const index = dimension.coordinates?.findIndex((coordinate) => sameCoordinate(coordinate, selected)) ?? -1;
473
+ if (index >= 0) {
474
+ return index;
475
+ }
476
+ throw new GridError({
477
+ code: "UNSUPPORTED_SELECTOR",
478
+ message: `Selector value "${String(selected)}" does not match coordinates for dimension "${dimension.name}".`,
479
+ dimension: dimension.name
480
+ });
481
+ }
482
+ function listTimeCoordinates(source, dimensionName = "time") {
483
+ const dimension = findDimension(source, dimensionName);
484
+ if (!dimension) {
485
+ throw new GridError({
486
+ code: "MISSING_DIMENSION",
487
+ message: `Time dimension "${dimensionName}" is not present in the grid source.`,
488
+ dimension: dimensionName,
489
+ sourceId: source.id
490
+ });
491
+ }
492
+ if (!dimension.coordinates) {
493
+ throw new GridError({
494
+ code: "MISSING_COORDINATES",
495
+ message: `Time dimension "${dimensionName}" needs coordinate values for a slider.`,
496
+ dimension: dimensionName,
497
+ sourceId: source.id
498
+ });
499
+ }
500
+ const coordinates = dimension.coordinates.map((coordinate, index) => {
501
+ const decoded = decodeTimeCoordinate(coordinate, dimension.units);
502
+ const forecastLabel = formatForecastStepLabel(source, dimension, coordinate);
503
+ return {
504
+ index,
505
+ value: decoded.value,
506
+ label: forecastLabel ?? (decoded.iso ? formatHumanUtcDateTime(decoded.iso) : String(decoded.value)),
507
+ ...decoded.iso ? { iso: decoded.iso } : {}
508
+ };
509
+ });
510
+ assertMonotonicTimeCoordinates(coordinates, dimension.name, source.id);
511
+ return coordinates;
512
+ }
513
+ function formatForecastStepLabel(source, dimension, coordinate) {
514
+ if (!isForecastStepDimension(dimension.name)) {
515
+ return void 0;
516
+ }
517
+ const reference = forecastReferenceDate(source);
518
+ const offsetMs = forecastStepOffsetMs(coordinate, dimension.units);
519
+ if (!reference || offsetMs === void 0) {
520
+ return void 0;
521
+ }
522
+ return formatHumanUtcDateTime(new Date(reference.getTime() + offsetMs));
523
+ }
524
+ function isForecastStepDimension(name) {
525
+ return normalizeDimensionName(name) === "step";
526
+ }
527
+ function forecastReferenceDate(source) {
528
+ const referenceDimension = source.dimensions.find(
529
+ (dimension) => ["forecastreferencetime", "forecastreferencedate"].includes(
530
+ normalizeDimensionName(dimension.name)
531
+ )
532
+ );
533
+ if (!referenceDimension) {
534
+ return void 0;
535
+ }
536
+ const referenceCoordinate = referenceDimension.coordinates?.[0];
537
+ if (referenceCoordinate === void 0) {
538
+ return void 0;
539
+ }
540
+ const decoded = decodeTimeCoordinate(referenceCoordinate, referenceDimension.units);
541
+ const referenceValue = decoded.iso ?? (typeof decoded.value === "string" ? decoded.value : void 0);
542
+ if (!referenceValue) {
543
+ return void 0;
544
+ }
545
+ const date = new Date(referenceValue);
546
+ return Number.isNaN(date.getTime()) ? void 0 : date;
547
+ }
548
+ function forecastStepOffsetMs(coordinate, units) {
549
+ const value = numericCoordinateValue(coordinate);
550
+ if (value === void 0) {
551
+ return void 0;
552
+ }
553
+ return value * forecastStepUnitMultiplier(units);
554
+ }
555
+ function numericCoordinateValue(coordinate) {
556
+ if (typeof coordinate === "number") {
557
+ return Number.isFinite(coordinate) ? coordinate : void 0;
558
+ }
559
+ if (typeof coordinate !== "string") {
560
+ return void 0;
561
+ }
562
+ const value = Number(coordinate);
563
+ return Number.isFinite(value) ? value : void 0;
564
+ }
565
+ function forecastStepUnitMultiplier(units) {
566
+ const normalizedUnits = units?.toLowerCase() ?? "";
567
+ if (normalizedUnits.includes("second")) {
568
+ return 1e3;
569
+ }
570
+ if (normalizedUnits.includes("minute")) {
571
+ return 6e4;
572
+ }
573
+ if (normalizedUnits.includes("day")) {
574
+ return 864e5;
575
+ }
576
+ return 36e5;
577
+ }
578
+ function normalizeDimensionName(name) {
579
+ return name.toLowerCase().replace(/[_\-\s]+/g, "");
580
+ }
581
+ var UTC_MONTH_LABELS = [
582
+ "Jan",
583
+ "Feb",
584
+ "Mar",
585
+ "Apr",
586
+ "May",
587
+ "Jun",
588
+ "Jul",
589
+ "Aug",
590
+ "Sep",
591
+ "Oct",
592
+ "Nov",
593
+ "Dec"
594
+ ];
595
+ function formatHumanUtcDateTime(input) {
596
+ const date = input instanceof Date ? input : new Date(input);
597
+ return [
598
+ UTC_MONTH_LABELS[date.getUTCMonth()],
599
+ " ",
600
+ date.getUTCDate(),
601
+ ", ",
602
+ date.getUTCFullYear(),
603
+ ", ",
604
+ padDatePart(date.getUTCHours()),
605
+ ":",
606
+ padDatePart(date.getUTCMinutes()),
607
+ " UTC"
608
+ ].join("");
609
+ }
610
+ function padDatePart(value) {
611
+ return String(value).padStart(2, "0");
612
+ }
613
+ function createDebouncer(callback, waitMs) {
614
+ let timeout;
615
+ let lastArgs;
616
+ const flush = () => {
617
+ if (!lastArgs) {
618
+ return;
619
+ }
620
+ const args = lastArgs;
621
+ lastArgs = void 0;
622
+ if (timeout) {
623
+ clearTimeout(timeout);
624
+ timeout = void 0;
625
+ }
626
+ callback(...args);
627
+ };
628
+ return {
629
+ call(...args) {
630
+ lastArgs = args;
631
+ if (timeout) {
632
+ clearTimeout(timeout);
633
+ }
634
+ timeout = setTimeout(flush, waitMs);
635
+ },
636
+ flush,
637
+ cancel() {
638
+ if (timeout) {
639
+ clearTimeout(timeout);
640
+ }
641
+ timeout = void 0;
642
+ lastArgs = void 0;
643
+ }
644
+ };
645
+ }
646
+ function defaultSelectorForDimension(dimension) {
647
+ const firstValue = dimension.coordinates?.[0];
648
+ if (firstValue !== void 0) {
649
+ return {
650
+ kind: "coordinate",
651
+ value: firstValue instanceof Date ? firstValue.toISOString() : firstValue
652
+ };
653
+ }
654
+ return { kind: "index", index: 0 };
655
+ }
656
+ function normalizeCoordinateSelector(dimension, value, sourceId) {
657
+ if (dimension.coordinates && !dimension.coordinates.some((coordinate) => sameCoordinate(coordinate, value))) {
658
+ throw new GridError({
659
+ code: "UNSUPPORTED_SELECTOR",
660
+ message: `Selector value "${String(value)}" does not match coordinates for dimension "${dimension.name}".`,
661
+ dimension: dimension.name,
662
+ sourceId
663
+ });
664
+ }
665
+ return { kind: "coordinate", value };
666
+ }
667
+ function assertIndexInRange(dimension, index, sourceId) {
668
+ if (!Number.isInteger(index) || index < 0 || index >= dimension.size) {
669
+ throw new GridError({
670
+ code: "UNSUPPORTED_SELECTOR",
671
+ message: `Index selector ${index} is outside dimension "${dimension.name}" size ${dimension.size}.`,
672
+ dimension: dimension.name,
673
+ sourceId
674
+ });
675
+ }
676
+ }
677
+ function findCoordinateForIso(dimension, iso) {
678
+ return dimension.coordinates?.map((coordinate) => decodeTimeCoordinate(coordinate, dimension.units)).find((coordinate) => coordinate.iso === iso || coordinate.value === iso)?.value;
679
+ }
680
+ function decodeTimeCoordinate(coordinate, units) {
681
+ if (coordinate instanceof Date) {
682
+ return { value: coordinate.toISOString(), iso: coordinate.toISOString() };
683
+ }
684
+ if (typeof coordinate === "string") {
685
+ const date = new Date(coordinate);
686
+ return Number.isNaN(date.getTime()) ? { value: coordinate } : { value: coordinate, iso: date.toISOString() };
687
+ }
688
+ const cfTime = decodeCfTime(coordinate, units);
689
+ return cfTime ? { value: coordinate, iso: cfTime } : { value: coordinate };
690
+ }
691
+ function decodeCfTime(value, units) {
692
+ if (!units) {
693
+ return void 0;
694
+ }
695
+ const match = /^(seconds|minutes|hours|days) since ([0-9]{4}-[0-9]{2}-[0-9]{2})(?:[ T]([0-9:.Z+-]+))?/i.exec(
696
+ units
697
+ );
698
+ if (!match) {
699
+ return void 0;
700
+ }
701
+ const unit = match[1]?.toLowerCase();
702
+ const date = match[2];
703
+ const time = match[3] ?? "00:00:00Z";
704
+ const origin = /* @__PURE__ */ new Date(`${date}T${time.replace(/Z?$/, "Z")}`);
705
+ if (Number.isNaN(origin.getTime())) {
706
+ return void 0;
707
+ }
708
+ const multiplier = unit === "seconds" ? 1e3 : unit === "minutes" ? 6e4 : unit === "hours" ? 36e5 : 864e5;
709
+ return new Date(origin.getTime() + value * multiplier).toISOString();
710
+ }
711
+ function assertMonotonicTimeCoordinates(coordinates, dimensionName, sourceId) {
712
+ let direction;
713
+ for (let index = 1; index < coordinates.length; index += 1) {
714
+ const previous = comparableTimeValue(coordinates[index - 1]);
715
+ const current = comparableTimeValue(coordinates[index]);
716
+ if (previous === current) {
717
+ throw new GridError({
718
+ code: "UNSUPPORTED_DIMENSION",
719
+ message: `Time dimension "${dimensionName}" has duplicate coordinate values.`,
720
+ dimension: dimensionName,
721
+ sourceId
722
+ });
723
+ }
724
+ const pairDirection = current > previous ? "ascending" : "descending";
725
+ direction ??= pairDirection;
726
+ if (pairDirection !== direction) {
727
+ throw new GridError({
728
+ code: "UNSUPPORTED_DIMENSION",
729
+ message: `Time dimension "${dimensionName}" must be monotonic for slider controls.`,
730
+ dimension: dimensionName,
731
+ sourceId
732
+ });
733
+ }
734
+ }
735
+ }
736
+ function comparableTimeValue(coordinate) {
737
+ if (!coordinate) {
738
+ return 0;
739
+ }
740
+ if (coordinate.iso) {
741
+ return new Date(coordinate.iso).getTime();
742
+ }
743
+ if (typeof coordinate.value === "number") {
744
+ return coordinate.value;
745
+ }
746
+ const parsed = new Date(coordinate.value).getTime();
747
+ return Number.isNaN(parsed) ? coordinate.index : parsed;
748
+ }
749
+ function sameCoordinate(left, right) {
750
+ const normalizedLeft = left instanceof Date ? left.toISOString() : left;
751
+ return normalizedLeft === right || String(normalizedLeft) === String(right);
752
+ }
753
+
754
+ // src/core/jaxray.ts
755
+ function createJaxraySource(input, options = {}) {
756
+ const dataset = isDatasetLike(input) ? input : arrayToDataset(input);
757
+ const variables = inferVariables(dataset, options);
758
+ const dimensions = inferDimensions(dataset, variables, options);
759
+ const source = {
760
+ id: options.id ?? dataset.id,
761
+ label: options.label,
762
+ source: options.source,
763
+ store: options.store ?? dataset.store,
764
+ zarrVersion: options.zarrVersion,
765
+ crs: options.crs ?? stringAttr(dataset.attrs, "crs") ?? "EPSG:4326",
766
+ proj4: options.proj4 ?? stringAttr(dataset.attrs, "proj4"),
767
+ bounds: options.bounds ?? inferBounds(dimensions, dataset.attrs),
768
+ spatialDimensions: options.spatialDimensions,
769
+ dimensions,
770
+ variables,
771
+ metadata: dataset.attrs,
772
+ readSlice: async (request) => readJaxraySlice(dataset, source, request.variable, request.selectors)
773
+ };
774
+ const firstSpatialVariable = variables.find(
775
+ (variable) => inferSpatialDimensions(source, variable.name)
776
+ );
777
+ source.spatialDimensions = options.spatialDimensions ?? inferSpatialDimensions(source) ?? (firstSpatialVariable ? inferSpatialDimensions(source, firstSpatialVariable.name) : void 0);
778
+ assertValidGridDataSource(source, {
779
+ variable: firstSpatialVariable?.name ?? variables[0]?.name
780
+ });
781
+ return source;
782
+ }
783
+ function isDatasetLike(input) {
784
+ return "data_vars" in input || "dataVars" in input || "variables" in input && !("shape" in input) || "sizes" in input;
785
+ }
786
+ function arrayToDataset(array) {
787
+ const name = array.name ?? "variable";
788
+ return {
789
+ data_vars: { [name]: array },
790
+ coords: array.coords,
791
+ attrs: array.attrs
792
+ };
793
+ }
794
+ function inferVariables(dataset, options) {
795
+ return Array.from(variableEntries(dataset)).map(([name, array]) => {
796
+ const dimensions = array.dims ?? array.dimensions;
797
+ if (!dimensions) {
798
+ throw new GridError({
799
+ code: "MISSING_DIMENSION",
800
+ message: `Jaxray variable "${name}" does not expose dims/dimensions metadata.`,
801
+ variable: name
802
+ });
803
+ }
804
+ const shape = array.shape ?? dimensions.map((dimensionName) => dimensionSize(dataset, dimensionName));
805
+ if (!shape) {
806
+ throw new GridError({
807
+ code: "MISSING_DIMENSION",
808
+ message: `Jaxray variable "${name}" does not expose shape metadata.`,
809
+ variable: name
810
+ });
811
+ }
812
+ const override = options.variables?.[name] ?? {};
813
+ return {
814
+ name,
815
+ path: stringAttr(array.attrs, "path"),
816
+ dtype: override.dtype ?? array.dtype ?? stringAttr(array.attrs, "dtype") ?? "float32",
817
+ dimensions,
818
+ shape,
819
+ chunks: override.chunks ?? array.chunks,
820
+ units: override.units ?? stringAttr(array.attrs, "units"),
821
+ longName: override.longName ?? stringAttr(array.attrs, "long_name"),
822
+ standardName: override.standardName ?? stringAttr(array.attrs, "standard_name"),
823
+ fillValue: override.fillValue ?? numberAttr(array.attrs, "_FillValue") ?? numberAttr(array.attrs, "fill_value") ?? null,
824
+ scaleFactor: override.scaleFactor ?? numberAttr(array.attrs, "scale_factor"),
825
+ addOffset: override.addOffset ?? numberAttr(array.attrs, "add_offset"),
826
+ attrs: array.attrs
827
+ };
828
+ });
829
+ }
830
+ function inferDimensions(dataset, variables, options) {
831
+ const dimensions = /* @__PURE__ */ new Map();
832
+ const coords = dataset.coords;
833
+ for (const variable of variables) {
834
+ variable.dimensions.forEach((name, index) => {
835
+ const existing = dimensions.get(name);
836
+ const coordinates = normalizeCoordinateArray(coordinateValue(coords, name));
837
+ const override = options.dimensions?.[name] ?? {};
838
+ const inferredKind = inferDimensionKind(name);
839
+ const size = variable.shape[index] ?? coordinates?.length ?? 0;
840
+ dimensions.set(name, {
841
+ ...existing,
842
+ name,
843
+ size: override.size ?? existing?.size ?? size,
844
+ kind: override.kind ?? existing?.kind ?? inferredKind,
845
+ coordinates: override.coordinates ?? existing?.coordinates ?? coordinates,
846
+ units: override.units ?? existing?.units,
847
+ calendar: override.calendar ?? existing?.calendar,
848
+ longName: override.longName ?? existing?.longName,
849
+ standardName: override.standardName ?? existing?.standardName,
850
+ ascending: override.ascending ?? existing?.ascending ?? inferAscending(coordinates),
851
+ attrs: override.attrs ?? existing?.attrs
852
+ });
853
+ });
854
+ }
855
+ return Array.from(dimensions.values());
856
+ }
857
+ function inferDimensionKind(name) {
858
+ const normalized = name.toLowerCase();
859
+ if (["lon", "lng", "longitude", "x"].includes(normalized)) {
860
+ return "x";
861
+ }
862
+ if (["lat", "latitude", "y"].includes(normalized)) {
863
+ return "y";
864
+ }
865
+ if (["time", "valid_time", "date"].includes(normalized)) {
866
+ return "time";
867
+ }
868
+ if (["band", "month"].includes(normalized)) {
869
+ return "band";
870
+ }
871
+ if (["level", "height", "pressure"].includes(normalized)) {
872
+ return "vertical";
873
+ }
874
+ return "other";
875
+ }
876
+ function inferBounds(dimensions, attrs) {
877
+ const attrBounds = attrs?.bounds;
878
+ if (Array.isArray(attrBounds) && attrBounds.length === 4 && attrBounds.every((value) => typeof value === "number")) {
879
+ return attrBounds;
880
+ }
881
+ const x = dimensions.find((dimension) => dimension.kind === "x");
882
+ const y = dimensions.find((dimension) => dimension.kind === "y");
883
+ const xCoordinates = x?.coordinates?.filter(
884
+ (value) => typeof value === "number"
885
+ );
886
+ const yCoordinates = y?.coordinates?.filter(
887
+ (value) => typeof value === "number"
888
+ );
889
+ if (!xCoordinates?.length || !yCoordinates?.length) {
890
+ return void 0;
891
+ }
892
+ return [
893
+ Math.min(...xCoordinates),
894
+ Math.min(...yCoordinates),
895
+ Math.max(...xCoordinates),
896
+ Math.max(...yCoordinates)
897
+ ];
898
+ }
899
+ async function readJaxraySlice(dataset, source, variableName, selectors = {}) {
900
+ const entry = variableEntries(dataset).find(([name2]) => name2 === variableName);
901
+ if (!entry) {
902
+ throw new GridError({
903
+ code: "MISSING_VARIABLE",
904
+ message: `Jaxray source cannot read missing variable "${variableName}".`,
905
+ variable: variableName,
906
+ sourceId: source.id
907
+ });
908
+ }
909
+ const [name, array] = entry;
910
+ const variable = source.variables.find((candidate) => candidate.name === name);
911
+ if (!variable) {
912
+ throw new GridError({
913
+ code: "MISSING_VARIABLE",
914
+ message: `Jaxray source metadata is missing variable "${name}".`,
915
+ variable: name,
916
+ sourceId: source.id
917
+ });
918
+ }
919
+ const normalizedSelectors = isNormalizedSelectors(selectors) ? selectors : normalizeSelectors(source, variable.name, selectors);
920
+ const selection = variable.dimensions.reduce(
921
+ (accumulator, dimensionName) => {
922
+ const dimension = findDimension(source, dimensionName);
923
+ if (!dimension) {
924
+ return accumulator;
925
+ }
926
+ accumulator[dimensionName] = selectorToIndex(
927
+ dimension,
928
+ normalizedSelectors[dimensionName] ?? { kind: "index", index: 0 }
929
+ );
930
+ return accumulator;
931
+ },
932
+ {}
933
+ );
934
+ const value = await readArrayValue(array, variable, selection);
935
+ return {
936
+ variable: name,
937
+ selectors: normalizedSelectors,
938
+ dimensions: [],
939
+ shape: [],
940
+ data: [value],
941
+ unit: variable.units,
942
+ fillValue: variable.fillValue
943
+ };
944
+ }
945
+ async function readArrayValue(array, variable, selection) {
946
+ if (array.get) {
947
+ return array.get(selection);
948
+ }
949
+ if (array.isel) {
950
+ const selected = await array.isel(selection);
951
+ const computed = selected.isLazy && selected.compute ? await selected.compute() : selected;
952
+ return scalarFromArrayLike(computed.data ?? computed.values);
953
+ }
954
+ const data = array.data ?? array.values;
955
+ if (!data) {
956
+ throw new GridError({
957
+ code: "SOURCE_LOAD_FAILED",
958
+ message: `Jaxray variable "${variable.name}" does not expose readable data for query helpers.`,
959
+ variable: variable.name
960
+ });
961
+ }
962
+ let offset = 0;
963
+ let stride = 1;
964
+ for (let index = variable.dimensions.length - 1; index >= 0; index -= 1) {
965
+ const dimensionName = variable.dimensions[index];
966
+ if (!dimensionName) {
967
+ continue;
968
+ }
969
+ offset += (selection[dimensionName] ?? 0) * stride;
970
+ stride *= variable.shape[index] ?? 1;
971
+ }
972
+ return data[offset] ?? Number.NaN;
973
+ }
974
+ function scalarFromArrayLike(value) {
975
+ if (typeof value === "number") {
976
+ return value;
977
+ }
978
+ if (Array.isArray(value) || ArrayBuffer.isView(value)) {
979
+ const first = Array.from(value)[0];
980
+ return scalarFromArrayLike(first);
981
+ }
982
+ return Number.NaN;
983
+ }
984
+ function variableEntries(dataset) {
985
+ if (Array.isArray(dataset.dataVars) && typeof dataset.getVariable === "function") {
986
+ return dataset.dataVars.map((name) => [name, dataset.getVariable?.(name) ?? {}]);
987
+ }
988
+ if (Array.isArray(dataset.dataVars)) {
989
+ throw new GridError({
990
+ code: "MISSING_VARIABLE",
991
+ message: "Jaxray Dataset exposes dataVars names but no getVariable(name) method."
992
+ });
993
+ }
994
+ const variables = dataset.data_vars ?? dataset.dataVars ?? dataset.variables ?? {};
995
+ if (variables instanceof Map) {
996
+ return Array.from(variables.entries());
997
+ }
998
+ return Object.entries(variables);
999
+ }
1000
+ function normalizeCoordinateArray(input) {
1001
+ 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;
1002
+ return raw?.filter(
1003
+ (value) => typeof value === "number" || typeof value === "string"
1004
+ );
1005
+ }
1006
+ function coordinateValue(coords, name) {
1007
+ if (!coords) {
1008
+ return void 0;
1009
+ }
1010
+ return coords instanceof Map ? coords.get(name) : coords[name];
1011
+ }
1012
+ function dimensionSize(dataset, dimensionName) {
1013
+ const sizes = dataset.sizes;
1014
+ if (!sizes) {
1015
+ return 0;
1016
+ }
1017
+ return sizes instanceof Map ? sizes.get(dimensionName) ?? 0 : sizes[dimensionName] ?? 0;
1018
+ }
1019
+ function isArrayLike(value) {
1020
+ return Array.isArray(value) || ArrayBuffer.isView(value) || typeof value === "object" && value !== null && "length" in value && typeof value.length === "number";
1021
+ }
1022
+ function inferAscending(coordinates) {
1023
+ if (!coordinates || coordinates.length < 2) {
1024
+ return void 0;
1025
+ }
1026
+ const first = coordinates[0];
1027
+ const second = coordinates[1];
1028
+ return typeof first === "number" && typeof second === "number" ? second > first : void 0;
1029
+ }
1030
+ function stringAttr(attrs, name) {
1031
+ const value = attrs?.[name];
1032
+ return typeof value === "string" ? value : void 0;
1033
+ }
1034
+ function numberAttr(attrs, name) {
1035
+ const value = attrs?.[name];
1036
+ return typeof value === "number" ? value : void 0;
1037
+ }
1038
+ function isNormalizedSelectors(selectors) {
1039
+ return Object.values(selectors).every(
1040
+ (value) => typeof value === "object" && value !== null && "kind" in value
1041
+ );
1042
+ }
1043
+
1044
+ // src/core/preflight.ts
1045
+ var dtypeByteSizes = {
1046
+ int8: 1,
1047
+ uint8: 1,
1048
+ i1: 1,
1049
+ u1: 1,
1050
+ int16: 2,
1051
+ uint16: 2,
1052
+ i2: 2,
1053
+ u2: 2,
1054
+ int32: 4,
1055
+ uint32: 4,
1056
+ float32: 4,
1057
+ i4: 4,
1058
+ u4: 4,
1059
+ f4: 4,
1060
+ float64: 8,
1061
+ f8: 8
1062
+ };
1063
+ function preflightGridRequest(options) {
1064
+ const errors = [];
1065
+ const warnings = [];
1066
+ const dimensions = {};
1067
+ const variable = findVariable(options.source, options.variable);
1068
+ if (!variable) {
1069
+ errors.push(
1070
+ new GridError({
1071
+ code: "MISSING_VARIABLE",
1072
+ message: `Variable "${options.variable}" is not present in the grid source.`,
1073
+ sourceId: options.source.id,
1074
+ variable: options.variable
1075
+ })
1076
+ );
1077
+ return { ok: false, dimensions, warnings, errors };
1078
+ }
1079
+ const spatialDimensions = inferSpatialDimensions(options.source, variable.name);
1080
+ for (const dimensionName of variable.dimensions) {
1081
+ const dimension = findDimension(options.source, dimensionName);
1082
+ if (!dimension) {
1083
+ errors.push(
1084
+ new GridError({
1085
+ code: "MISSING_DIMENSION",
1086
+ message: `Variable "${variable.name}" references missing dimension "${dimensionName}".`,
1087
+ dimension: dimensionName,
1088
+ sourceId: options.source.id,
1089
+ variable: variable.name
1090
+ })
1091
+ );
1092
+ continue;
1093
+ }
1094
+ dimensions[dimension.name] = selectedDimensionSize({
1095
+ bounds: options.bounds,
1096
+ dimension,
1097
+ isSpatialX: spatialDimensions?.x === dimension.name,
1098
+ isSpatialY: spatialDimensions?.y === dimension.name,
1099
+ selectorIsPresent: options.selectors?.[dimension.name] !== void 0,
1100
+ sourceBounds: options.source.bounds,
1101
+ timeRange: options.timeRange,
1102
+ warnings
1103
+ });
1104
+ }
1105
+ const cells = Object.values(dimensions).reduce((total, size) => total * size, 1);
1106
+ const byteSize = dtypeByteSize(variable.dtype);
1107
+ const bytes = byteSize === void 0 ? void 0 : cells * byteSize;
1108
+ if (byteSize === void 0) {
1109
+ warnings.push(`Variable "${variable.name}" has unknown dtype byte size "${variable.dtype}".`);
1110
+ }
1111
+ if (options.limits?.maxCells !== void 0 && cells > options.limits.maxCells) {
1112
+ errors.push(
1113
+ new GridError({
1114
+ code: "QUERY_TOO_EXPENSIVE",
1115
+ message: `Grid request would select approximately ${cells} cells, above the limit of ${options.limits.maxCells}.`,
1116
+ context: { cells, maxCells: options.limits.maxCells },
1117
+ sourceId: options.source.id,
1118
+ variable: variable.name
1119
+ })
1120
+ );
1121
+ }
1122
+ if (bytes !== void 0 && options.limits?.maxBytes !== void 0 && bytes > options.limits.maxBytes) {
1123
+ errors.push(
1124
+ new GridError({
1125
+ code: "QUERY_TOO_EXPENSIVE",
1126
+ message: `Grid request would read approximately ${bytes} uncompressed bytes, above the limit of ${options.limits.maxBytes}.`,
1127
+ context: { bytes, maxBytes: options.limits.maxBytes },
1128
+ sourceId: options.source.id,
1129
+ variable: variable.name
1130
+ })
1131
+ );
1132
+ }
1133
+ return {
1134
+ ok: errors.length === 0,
1135
+ cells,
1136
+ bytes,
1137
+ dimensions,
1138
+ warnings,
1139
+ errors
1140
+ };
1141
+ }
1142
+ function selectedDimensionSize(options) {
1143
+ if (options.selectorIsPresent) {
1144
+ return 1;
1145
+ }
1146
+ if (options.bounds && options.isSpatialX) {
1147
+ return spatialCount(options.dimension, options.bounds[0], options.bounds[2], {
1148
+ axis: "x",
1149
+ sourceBounds: options.sourceBounds,
1150
+ warnings: options.warnings
1151
+ });
1152
+ }
1153
+ if (options.bounds && options.isSpatialY) {
1154
+ return spatialCount(options.dimension, options.bounds[1], options.bounds[3], {
1155
+ axis: "y",
1156
+ sourceBounds: options.sourceBounds,
1157
+ warnings: options.warnings
1158
+ });
1159
+ }
1160
+ if (options.timeRange && options.dimension.kind === "time") {
1161
+ return timeRangeCount(options.dimension, options.timeRange, options.warnings);
1162
+ }
1163
+ return options.dimension.size;
1164
+ }
1165
+ function spatialCount(dimension, lowerBound, upperBound, options) {
1166
+ const low = Math.min(lowerBound, upperBound);
1167
+ const high = Math.max(lowerBound, upperBound);
1168
+ const numericCoordinates2 = dimension.coordinates?.filter(
1169
+ (coordinate) => typeof coordinate === "number"
1170
+ );
1171
+ if (numericCoordinates2?.length) {
1172
+ return numericCoordinates2.filter((coordinate) => coordinate >= low && coordinate <= high).length;
1173
+ }
1174
+ const sourceRange = spatialRange(options.sourceBounds, options.axis);
1175
+ if (!sourceRange) {
1176
+ options.warnings.push(
1177
+ `Dimension "${dimension.name}" has no coordinates or source bounds, so preflight used the full dimension size.`
1178
+ );
1179
+ return dimension.size;
1180
+ }
1181
+ const [sourceLow, sourceHigh] = sourceRange;
1182
+ const span = sourceHigh - sourceLow;
1183
+ if (span <= 0 || dimension.size <= 0) {
1184
+ return 0;
1185
+ }
1186
+ const overlap = Math.max(0, Math.min(high, sourceHigh) - Math.max(low, sourceLow));
1187
+ if (overlap === 0) {
1188
+ return 0;
1189
+ }
1190
+ return Math.max(1, Math.min(dimension.size, Math.ceil(overlap / span * dimension.size)));
1191
+ }
1192
+ function timeRangeCount(dimension, timeRange, warnings) {
1193
+ if (!dimension.coordinates?.length) {
1194
+ warnings.push(
1195
+ `Time dimension "${dimension.name}" has no coordinates, so preflight used the full time size.`
1196
+ );
1197
+ return dimension.size;
1198
+ }
1199
+ const start = timeToMillis(timeRange.start, dimension.units);
1200
+ const end = timeToMillis(timeRange.end, dimension.units);
1201
+ if (start === void 0 || end === void 0) {
1202
+ warnings.push(
1203
+ `Time range for "${dimension.name}" could not be parsed, so preflight used the full time size.`
1204
+ );
1205
+ return dimension.size;
1206
+ }
1207
+ const low = Math.min(start, end);
1208
+ const high = Math.max(start, end);
1209
+ let count = 0;
1210
+ for (const coordinate of dimension.coordinates) {
1211
+ const value = timeToMillis(coordinate, dimension.units);
1212
+ if (value !== void 0 && value >= low && value <= high) {
1213
+ count += 1;
1214
+ }
1215
+ }
1216
+ return count;
1217
+ }
1218
+ function timeToMillis(value, units) {
1219
+ if (value instanceof Date) {
1220
+ return value.getTime();
1221
+ }
1222
+ if (typeof value === "string") {
1223
+ const parsed = Date.parse(value);
1224
+ return Number.isNaN(parsed) ? void 0 : parsed;
1225
+ }
1226
+ if (typeof value === "number") {
1227
+ return numericTimeToMillis(value, units);
1228
+ }
1229
+ return void 0;
1230
+ }
1231
+ function numericTimeToMillis(value, units) {
1232
+ if (!units) {
1233
+ return void 0;
1234
+ }
1235
+ const match = /^(seconds?|minutes?|hours?|days?) since (.+)$/i.exec(units.trim());
1236
+ if (!match) {
1237
+ return void 0;
1238
+ }
1239
+ const [, unit, epoch] = match;
1240
+ if (!unit || !epoch) {
1241
+ return void 0;
1242
+ }
1243
+ const epochMillis = Date.parse(epoch);
1244
+ if (Number.isNaN(epochMillis)) {
1245
+ return void 0;
1246
+ }
1247
+ const multiplier = unit.toLowerCase().startsWith("second") ? 1e3 : unit.toLowerCase().startsWith("minute") ? 60 * 1e3 : unit.toLowerCase().startsWith("hour") ? 60 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
1248
+ return epochMillis + value * multiplier;
1249
+ }
1250
+ function spatialRange(bounds, axis) {
1251
+ if (!bounds) {
1252
+ return void 0;
1253
+ }
1254
+ return axis === "x" ? [bounds[0], bounds[2]] : [bounds[1], bounds[3]];
1255
+ }
1256
+ function dtypeByteSize(dtype) {
1257
+ const normalized = dtype.toLowerCase().replace(/^[<>|]/, "");
1258
+ return dtypeByteSizes[normalized];
1259
+ }
1260
+
1261
+ // src/core/query.ts
1262
+ async function queryPoint(source, variableName, coordinates, selectors = {}, signal) {
1263
+ const context = queryContext(source, variableName, selectors);
1264
+ const xIndex = nearestCoordinateIndex(context.xDimension, coordinates[0], source.bounds, "x");
1265
+ const yIndex = nearestCoordinateIndex(context.yDimension, coordinates[1], source.bounds, "y");
1266
+ const sample = await readSample(source, context, xIndex, yIndex, signal);
1267
+ return {
1268
+ variable: variableName,
1269
+ selectors: context.normalizedSelectors,
1270
+ geometry: { type: "Point", coordinates },
1271
+ unit: context.variable.units,
1272
+ samples: [sample],
1273
+ warnings: []
1274
+ };
1275
+ }
1276
+ function queryContext(source, variableName, selectors = {}) {
1277
+ const variable = findVariable(source, variableName);
1278
+ if (!variable) {
1279
+ throw new GridError({
1280
+ code: "MISSING_VARIABLE",
1281
+ message: `Variable "${variableName}" is not present in the grid source.`,
1282
+ variable: variableName,
1283
+ sourceId: source.id
1284
+ });
1285
+ }
1286
+ const spatialDimensions = inferSpatialDimensions(source, variable.name);
1287
+ if (!spatialDimensions) {
1288
+ throw new GridError({
1289
+ code: "MISSING_SPATIAL_DIMENSIONS",
1290
+ message: `Variable "${variable.name}" needs spatial dimensions for map queries.`,
1291
+ variable: variable.name,
1292
+ sourceId: source.id
1293
+ });
1294
+ }
1295
+ const xDimension = findDimension(source, spatialDimensions.x);
1296
+ const yDimension = findDimension(source, spatialDimensions.y);
1297
+ if (!xDimension || !yDimension) {
1298
+ throw new GridError({
1299
+ code: "MISSING_SPATIAL_DIMENSIONS",
1300
+ message: "Spatial dimensions are missing from the grid source.",
1301
+ variable: variable.name,
1302
+ sourceId: source.id
1303
+ });
1304
+ }
1305
+ if (!source.readSlice) {
1306
+ throw new GridError({
1307
+ code: "SOURCE_LOAD_FAILED",
1308
+ message: "GridDataSource must implement readSlice or queryGeometry before query helpers can read values.",
1309
+ sourceId: source.id,
1310
+ variable: variable.name
1311
+ });
1312
+ }
1313
+ return {
1314
+ variable,
1315
+ normalizedSelectors: normalizeSelectors(source, variable.name, selectors),
1316
+ xDimension,
1317
+ yDimension,
1318
+ spatialDimensions
1319
+ };
1320
+ }
1321
+ async function readSample(source, context, xIndex, yIndex, signal) {
1322
+ const selectors = selectorsWithSpatialIndexes(
1323
+ context.normalizedSelectors,
1324
+ context.spatialDimensions.x,
1325
+ xIndex,
1326
+ context.spatialDimensions.y,
1327
+ yIndex
1328
+ );
1329
+ const slice = await source.readSlice?.({
1330
+ variable: context.variable.name,
1331
+ selectors,
1332
+ signal
1333
+ });
1334
+ const rawValue = slice?.data[0];
1335
+ const value = rawValue === void 0 || rawValue === context.variable.fillValue ? null : rawValue * (context.variable.scaleFactor ?? 1) + (context.variable.addOffset ?? 0);
1336
+ return {
1337
+ coordinates: [
1338
+ coordinateAt(context.xDimension, xIndex, source.bounds, "x"),
1339
+ coordinateAt(context.yDimension, yIndex, source.bounds, "y")
1340
+ ],
1341
+ indexes: {
1342
+ [context.spatialDimensions.x]: xIndex,
1343
+ [context.spatialDimensions.y]: yIndex
1344
+ },
1345
+ rawValue: rawValue ?? null,
1346
+ value
1347
+ };
1348
+ }
1349
+ function selectorsWithSpatialIndexes(selectors, xDimension, xIndex, yDimension, yIndex) {
1350
+ return {
1351
+ ...selectors,
1352
+ [xDimension]: { kind: "index", index: xIndex },
1353
+ [yDimension]: { kind: "index", index: yIndex }
1354
+ };
1355
+ }
1356
+ function nearestCoordinateIndex(dimension, value, bounds, axis) {
1357
+ if (!dimension.coordinates) {
1358
+ return boundedCoordinateIndex(dimension, value, bounds, axis);
1359
+ }
1360
+ let bestIndex = 0;
1361
+ let bestDistance = Number.POSITIVE_INFINITY;
1362
+ dimension.coordinates.forEach((coordinate, index) => {
1363
+ if (typeof coordinate !== "number") {
1364
+ return;
1365
+ }
1366
+ const distance = Math.abs(coordinate - value);
1367
+ if (distance < bestDistance) {
1368
+ bestDistance = distance;
1369
+ bestIndex = index;
1370
+ }
1371
+ });
1372
+ return bestIndex;
1373
+ }
1374
+ function coordinateAt(dimension, index, bounds, axis) {
1375
+ const coordinate = dimension.coordinates?.[index];
1376
+ if (typeof coordinate === "number") {
1377
+ return coordinate;
1378
+ }
1379
+ return boundedCoordinateAt(dimension, index, bounds, axis);
1380
+ }
1381
+ function boundedCoordinateIndex(dimension, value, bounds, axis) {
1382
+ const range = spatialRange2(bounds, axis);
1383
+ if (!range) {
1384
+ return clamp(Math.round(value), 0, dimension.size - 1);
1385
+ }
1386
+ const [min, max] = range;
1387
+ const span = max - min;
1388
+ if (span <= 0 || dimension.size <= 1) {
1389
+ return 0;
1390
+ }
1391
+ const ascending = dimension.ascending !== false;
1392
+ const ratio = ascending ? (value - min) / span : (max - value) / span;
1393
+ return clamp(Math.round(ratio * (dimension.size - 1)), 0, dimension.size - 1);
1394
+ }
1395
+ function boundedCoordinateAt(dimension, index, bounds, axis) {
1396
+ const range = spatialRange2(bounds, axis);
1397
+ if (!range || dimension.size <= 1) {
1398
+ return index;
1399
+ }
1400
+ const [min, max] = range;
1401
+ const ratio = clamp(index, 0, dimension.size - 1) / (dimension.size - 1);
1402
+ return dimension.ascending === false ? max - ratio * (max - min) : min + ratio * (max - min);
1403
+ }
1404
+ function spatialRange2(bounds, axis) {
1405
+ if (!bounds) {
1406
+ return void 0;
1407
+ }
1408
+ return axis === "x" ? [bounds[0], bounds[2]] : [bounds[1], bounds[3]];
1409
+ }
1410
+ function clamp(value, min, max) {
1411
+ return Math.min(max, Math.max(min, value));
1412
+ }
1413
+
1414
+ // src/dclimate/index.ts
1415
+ var DEFAULT_TIME_KEYS = [
1416
+ "time",
1417
+ "valid_time",
1418
+ "datetime",
1419
+ "date",
1420
+ "forecast_reference_time",
1421
+ "forecast_time",
1422
+ "analysis_time",
1423
+ "initial_time",
1424
+ "verification_time",
1425
+ "step",
1426
+ "t"
1427
+ ];
1428
+ var DClimateAdapterError = class extends GridError {
1429
+ constructor(code, message, cause, context) {
1430
+ super({
1431
+ code: code === "unsupported" ? "UNSUPPORTED_DIMENSION" : "SOURCE_LOAD_FAILED",
1432
+ message,
1433
+ cause,
1434
+ context: {
1435
+ adapter: "dclimate",
1436
+ adapterCode: code,
1437
+ ...context
1438
+ }
1439
+ });
1440
+ this.name = "DClimateAdapterError";
1441
+ }
1442
+ };
1443
+ function reportProgress(options, progress) {
1444
+ options.onProgress?.({
1445
+ ...progress,
1446
+ percent: Math.max(0, Math.min(100, progress.percent))
1447
+ });
1448
+ }
1449
+ function runDClimatePreflight(source, options) {
1450
+ const config = options.preflight;
1451
+ if (!config) {
1452
+ return;
1453
+ }
1454
+ const selectionBounds2 = options.selection?.bounds ? normalizeSelectionBounds(options.selection.bounds, options.selection.boundsOptions).bounds : void 0;
1455
+ const result = preflightGridRequest({
1456
+ bounds: config.bounds ?? selectionBounds2,
1457
+ limits: config.limits,
1458
+ selectors: config.selectors,
1459
+ source,
1460
+ timeRange: config.timeRange ?? options.selection?.timeRange,
1461
+ variable: config.variable ?? source.variables[0]?.name ?? ""
1462
+ });
1463
+ config.onResult?.(result);
1464
+ if (!result.ok) {
1465
+ throw result.errors[0];
1466
+ }
1467
+ }
1468
+ async function createDClimateSource(request, options = {}) {
1469
+ try {
1470
+ reportProgress(options, {
1471
+ stage: "request",
1472
+ message: "Preparing dClimate request",
1473
+ percent: 2
1474
+ });
1475
+ const loaded = await loadDClimateDataset(request, options);
1476
+ reportProgress(options, {
1477
+ stage: "dataset",
1478
+ message: "Dataset metadata loaded",
1479
+ percent: 45
1480
+ });
1481
+ const metadata = extractMetadata(loaded);
1482
+ const cid = extractString(loaded, ["cid", "rootCid", "hash"]) ?? metadata?.cid ?? request.cid;
1483
+ const dataset = extractDataset(loaded);
1484
+ const selectedForRendering = Boolean(options.selection?.bounds);
1485
+ if (!dataset) {
1486
+ const existingStore = options.store ?? extractStore(loaded);
1487
+ const hasExternalSource = Boolean(
1488
+ existingStore || options.source || request.cid || options.openIpfsStore || options.gatewayUrl
1489
+ );
1490
+ throw new DClimateAdapterError(
1491
+ "metadata",
1492
+ hasExternalSource ? "dClimate CID/store loading requires dataset metadata before it can be normalized. Provide a dClimate client response with variables, dimensions, coordinates, bounds, CRS, fill values, and units, or use createJaxraySource with explicit metadata overrides." : "dClimate response did not include a dataset, readable store, source URL, or CID.",
1493
+ void 0,
1494
+ { request, cid, hasStore: Boolean(existingStore), source: options.source }
1495
+ );
1496
+ }
1497
+ reportProgress(options, {
1498
+ stage: "source",
1499
+ message: "Normalizing raster metadata",
1500
+ percent: 55
1501
+ });
1502
+ const source = createJaxraySource(dataset, {
1503
+ ...options,
1504
+ id: options.id ?? sourceIdForRequest(request),
1505
+ source: options.source ?? (selectedForRendering ? void 0 : gatewaySourceForCid(cid, options.gatewayUrl)),
1506
+ store: options.store ?? (selectedForRendering ? void 0 : extractStore(loaded)),
1507
+ label: options.label ?? labelForRequest(request)
1508
+ });
1509
+ runDClimatePreflight(source, options);
1510
+ reportProgress(options, {
1511
+ stage: "store",
1512
+ message: selectedForRendering ? "Preparing bounded raster chunks" : "Preparing raster source",
1513
+ percent: selectedForRendering ? 62 : 72
1514
+ });
1515
+ const selectedStore = selectedForRendering ? await createInMemoryZarrStore(dataset) : void 0;
1516
+ const selectedStoreByteLength = selectedStore?.byteLength;
1517
+ reportProgress(options, {
1518
+ stage: "store",
1519
+ message: selectedForRendering ? "Prepared bounded raster chunks" : "Prepared raster source",
1520
+ percent: selectedForRendering ? 78 : 80,
1521
+ byteLength: selectedStoreByteLength
1522
+ });
1523
+ reportProgress(options, {
1524
+ stage: "source",
1525
+ message: "Normalizing raster metadata",
1526
+ percent: 84,
1527
+ byteLength: selectedStoreByteLength
1528
+ });
1529
+ const store = options.store ?? selectedStore ?? (selectedForRendering ? void 0 : source.store ?? extractStore(loaded)) ?? (!selectedForRendering && cid && (options.gatewayUrl || options.openIpfsStore) ? await openStore(cid, options) : void 0);
1530
+ reportProgress(options, {
1531
+ stage: "ready",
1532
+ message: "dClimate source ready",
1533
+ percent: 100,
1534
+ byteLength: selectedStoreByteLength
1535
+ });
1536
+ return {
1537
+ ...source,
1538
+ store,
1539
+ metadata: {
1540
+ ...source.metadata,
1541
+ dclimate: {
1542
+ request,
1543
+ cid,
1544
+ clientMetadata: metadata,
1545
+ gatewayUrl: options.gatewayUrl,
1546
+ selectedStoreByteLength
1547
+ }
1548
+ }
1549
+ };
1550
+ } catch (error) {
1551
+ if (error instanceof DClimateAdapterError || error instanceof GridError) {
1552
+ throw error;
1553
+ }
1554
+ throw new DClimateAdapterError(
1555
+ "catalog",
1556
+ "Failed to resolve or load the dClimate dataset.",
1557
+ error,
1558
+ {
1559
+ request
1560
+ }
1561
+ );
1562
+ }
1563
+ }
1564
+ async function loadDClimateDataset(request, options) {
1565
+ if (options.client) {
1566
+ reportProgress(options, {
1567
+ stage: "client",
1568
+ message: "Using injected dClimate client",
1569
+ percent: 8
1570
+ });
1571
+ return callClient(options.client, request, options);
1572
+ }
1573
+ if (request.cid && !request.collection && !request.dataset) {
1574
+ reportProgress(options, {
1575
+ stage: "dataset",
1576
+ message: "Using CID dataset reference",
1577
+ percent: 35
1578
+ });
1579
+ return { cid: request.cid };
1580
+ }
1581
+ reportProgress(options, {
1582
+ stage: "client",
1583
+ message: "Loading dClimate client",
1584
+ percent: 8
1585
+ });
1586
+ const module = await import('@dclimate/dclimate-client-js');
1587
+ const Client = module.DClimateClient ?? module.default;
1588
+ if (!Client) {
1589
+ throw new DClimateAdapterError(
1590
+ "catalog",
1591
+ "@dclimate/dclimate-client-js did not export DClimateClient."
1592
+ );
1593
+ }
1594
+ reportProgress(options, {
1595
+ stage: "dataset",
1596
+ message: "Resolving dClimate dataset",
1597
+ percent: 18
1598
+ });
1599
+ return callClient(new Client(options.clientOptions), request, options);
1600
+ }
1601
+ async function callClient(client, request, options) {
1602
+ if (options.selection?.bounds && client.loadDataset) {
1603
+ return loadBoundedDataset(client, request, options);
1604
+ }
1605
+ if (options.selection) {
1606
+ if (client.selectDataset) {
1607
+ return client.selectDataset({
1608
+ request,
1609
+ selection: options.selection,
1610
+ options: {
1611
+ ...options.loadDatasetOptions,
1612
+ gatewayUrl: options.gatewayUrl,
1613
+ returnJaxrayDataset: false
1614
+ }
1615
+ });
1616
+ }
1617
+ if (options.selection.bounds) {
1618
+ return loadBoundedDataset(client, request, options);
1619
+ }
1620
+ throw new DClimateAdapterError(
1621
+ "unsupported",
1622
+ "Injected dClimate client must expose selectDataset when source selection is requested."
1623
+ );
1624
+ }
1625
+ if (client.loadDataset) {
1626
+ return client.loadDataset({
1627
+ request,
1628
+ options: {
1629
+ ...options.loadDatasetOptions,
1630
+ gatewayUrl: options.gatewayUrl,
1631
+ returnJaxrayDataset: true
1632
+ }
1633
+ });
1634
+ }
1635
+ if (client.getDataset) {
1636
+ return client.getDataset(request);
1637
+ }
1638
+ if (client.resolveDataset) {
1639
+ return client.resolveDataset(request);
1640
+ }
1641
+ throw new DClimateAdapterError(
1642
+ "catalog",
1643
+ "Injected dClimate client must expose loadDataset, selectDataset, getDataset, or resolveDataset."
1644
+ );
1645
+ }
1646
+ async function loadBoundedDataset(client, request, options) {
1647
+ if (!client.loadDataset) {
1648
+ throw new DClimateAdapterError(
1649
+ "unsupported",
1650
+ "Injected dClimate client must expose loadDataset when bounds selection is requested."
1651
+ );
1652
+ }
1653
+ const loaded = await client.loadDataset({
1654
+ request,
1655
+ options: {
1656
+ ...options.loadDatasetOptions,
1657
+ gatewayUrl: options.gatewayUrl,
1658
+ returnJaxrayDataset: false
1659
+ }
1660
+ });
1661
+ reportProgress(options, {
1662
+ stage: "selection",
1663
+ message: "Selecting requested time and bounds",
1664
+ percent: 35
1665
+ });
1666
+ const selected = await applyBoundedSelection(loaded, options.selection);
1667
+ reportProgress(options, {
1668
+ stage: "selection",
1669
+ message: "Selected bounded raster window",
1670
+ percent: 52
1671
+ });
1672
+ return replaceLoadedDataset(loaded, selected);
1673
+ }
1674
+ async function applyBoundedSelection(loaded, selection) {
1675
+ const dataset = extractGeoTemporalDataset(loaded);
1676
+ if (!dataset) {
1677
+ throw new DClimateAdapterError(
1678
+ "unsupported",
1679
+ "dClimate bounds selection requires a GeoTemporalDataset response."
1680
+ );
1681
+ }
1682
+ const timeSelected = selection?.timeRange ? await applyTimeRangeSelection(dataset, selection.timeRange) : dataset;
1683
+ const coordinateSelected = await applyCoordinateSelection(timeSelected, selection?.coordinates);
1684
+ if (!selection?.bounds) {
1685
+ return coordinateSelected;
1686
+ }
1687
+ const { bounds, boundsOptions } = normalizeSelectionBounds(
1688
+ selection.bounds,
1689
+ selection.boundsOptions
1690
+ );
1691
+ const [west, south, east, north] = bounds;
1692
+ const gridSelected = await selectGriddedBounds(coordinateSelected, bounds, boundsOptions);
1693
+ if (gridSelected) {
1694
+ return gridSelected;
1695
+ }
1696
+ if (!hasRectangleSelection(coordinateSelected)) {
1697
+ throw new DClimateAdapterError(
1698
+ "unsupported",
1699
+ "dClimate bounds selection requires rectangle or gridded axis selection support."
1700
+ );
1701
+ }
1702
+ return coordinateSelected.rectangle(south, west, north, east, boundsOptions);
1703
+ }
1704
+ async function applyTimeRangeSelection(value, timeRange) {
1705
+ const gridSelected = await selectGriddedTimeRange(value, timeRange);
1706
+ if (gridSelected) {
1707
+ return gridSelected;
1708
+ }
1709
+ if (!hasTimeRange(value)) {
1710
+ return value;
1711
+ }
1712
+ return value.timeRange(timeRange);
1713
+ }
1714
+ async function selectGriddedTimeRange(value, timeRange) {
1715
+ const grid = extractSelectableGrid(value);
1716
+ if (!grid?.coords || typeof grid.sel !== "function") {
1717
+ return void 0;
1718
+ }
1719
+ const timeKey = inferCoordinateKey(grid.coords, DEFAULT_TIME_KEYS);
1720
+ if (!timeKey) {
1721
+ return void 0;
1722
+ }
1723
+ const coordinates = coordinateValues(grid.coords[timeKey]);
1724
+ if (!coordinates?.length) {
1725
+ return void 0;
1726
+ }
1727
+ const range = timeCoordinateRange(coordinates, timeRange, timeCoordinateUnits(grid, timeKey));
1728
+ if (!range) {
1729
+ return void 0;
1730
+ }
1731
+ return grid.sel({
1732
+ [timeKey]: range
1733
+ });
1734
+ }
1735
+ async function applyCoordinateSelection(value, coordinates) {
1736
+ if (!coordinates || Object.keys(coordinates).length === 0) {
1737
+ return value;
1738
+ }
1739
+ const grid = extractSelectableGrid(value);
1740
+ if (!grid || typeof grid.sel !== "function") {
1741
+ throw new DClimateAdapterError(
1742
+ "unsupported",
1743
+ "dClimate coordinate selection requires gridded axis selection support."
1744
+ );
1745
+ }
1746
+ return grid.sel(normalizeCoordinateSelection(coordinates));
1747
+ }
1748
+ function normalizeCoordinateSelection(coordinates) {
1749
+ return Object.fromEntries(
1750
+ Object.entries(coordinates).map(([dimension, value]) => [
1751
+ dimension,
1752
+ normalizeCoordinateSelectionValue(value)
1753
+ ])
1754
+ );
1755
+ }
1756
+ function normalizeCoordinateSelectionValue(value) {
1757
+ if (value instanceof Date) {
1758
+ return value.toISOString();
1759
+ }
1760
+ if (typeof value !== "object") {
1761
+ return value;
1762
+ }
1763
+ return {
1764
+ start: value.start instanceof Date ? value.start.toISOString() : value.start,
1765
+ stop: value.stop instanceof Date ? value.stop.toISOString() : value.stop
1766
+ };
1767
+ }
1768
+ async function selectGriddedBounds(value, bounds, boundsOptions) {
1769
+ const grid = extractSelectableGrid(value);
1770
+ if (!grid?.coords || typeof grid.sel !== "function") {
1771
+ return void 0;
1772
+ }
1773
+ const latitudeKey = boundsOptions?.latitudeKey ?? inferCoordinateKey(grid.coords, ["latitude", "lat", "y"]);
1774
+ const longitudeKey = boundsOptions?.longitudeKey ?? inferCoordinateKey(grid.coords, ["longitude", "lon", "lng", "x"]);
1775
+ if (!latitudeKey || !longitudeKey) {
1776
+ return void 0;
1777
+ }
1778
+ const latitudeCoords = numericCoordinates(grid.coords[latitudeKey]);
1779
+ const longitudeCoords = numericCoordinates(grid.coords[longitudeKey]);
1780
+ if (!latitudeCoords || !longitudeCoords || !hasGriddedSpatialAxes(grid, latitudeKey, longitudeKey)) {
1781
+ return void 0;
1782
+ }
1783
+ const [west, south, east, north] = bounds;
1784
+ return grid.sel({
1785
+ [latitudeKey]: coordinateRange(latitudeCoords, south, north),
1786
+ [longitudeKey]: coordinateRange(longitudeCoords, west, east)
1787
+ });
1788
+ }
1789
+ function extractSelectableGrid(value) {
1790
+ if (!isRecord(value)) {
1791
+ return void 0;
1792
+ }
1793
+ const data = value.data;
1794
+ if (isSelectableGrid(data)) {
1795
+ return data;
1796
+ }
1797
+ return isSelectableGrid(value) ? value : void 0;
1798
+ }
1799
+ function isSelectableGrid(value) {
1800
+ return isRecord(value) && isRecord(value.coords) && typeof value.sel === "function";
1801
+ }
1802
+ function timeCoordinateRange(coordinates, timeRange, units) {
1803
+ const start = timeRangeBoundaryMillis(timeRange.start);
1804
+ const end = timeRangeBoundaryMillis(timeRange.end);
1805
+ if (start === void 0 || end === void 0) {
1806
+ return void 0;
1807
+ }
1808
+ const lowerBound = Math.min(start, end);
1809
+ const upperBound = Math.max(start, end);
1810
+ const matchingCoordinates = coordinates.map((value) => ({ value, millis: timeCoordinateMillis(value, units) })).filter(
1811
+ (coordinate) => coordinate.millis !== void 0 && coordinate.millis >= lowerBound && coordinate.millis <= upperBound
1812
+ );
1813
+ if (matchingCoordinates.length === 0) {
1814
+ throw new DClimateAdapterError(
1815
+ "unsupported",
1816
+ "dClimate time range selection did not include any available time coordinates.",
1817
+ void 0,
1818
+ { timeRange }
1819
+ );
1820
+ }
1821
+ return {
1822
+ start: matchingCoordinates[0]?.value,
1823
+ stop: matchingCoordinates.at(-1)?.value
1824
+ };
1825
+ }
1826
+ function timeCoordinateUnits(grid, timeKey) {
1827
+ const attrs = grid.coordAttrs?.[timeKey];
1828
+ if (isRecord(attrs) && typeof attrs.units === "string") {
1829
+ return attrs.units;
1830
+ }
1831
+ return void 0;
1832
+ }
1833
+ function timeRangeBoundaryMillis(value) {
1834
+ const date = value instanceof Date ? value : new Date(value);
1835
+ return Number.isNaN(date.getTime()) ? void 0 : date.getTime();
1836
+ }
1837
+ function timeCoordinateMillis(value, units) {
1838
+ if (value instanceof Date) {
1839
+ return value.getTime();
1840
+ }
1841
+ if (typeof value === "string") {
1842
+ const date = new Date(value);
1843
+ return Number.isNaN(date.getTime()) ? void 0 : date.getTime();
1844
+ }
1845
+ if (typeof value === "number" && Number.isFinite(value) && units) {
1846
+ return cfTimeCoordinateMillis(value, units);
1847
+ }
1848
+ return void 0;
1849
+ }
1850
+ function cfTimeCoordinateMillis(value, units) {
1851
+ const match = /^(seconds?|minutes?|hours?|days?) since ([0-9]{4}-[0-9]{2}-[0-9]{2})(?:[ T]([0-9:.Z+-]+))?/i.exec(
1852
+ units
1853
+ );
1854
+ if (!match) {
1855
+ return void 0;
1856
+ }
1857
+ const unit = match[1]?.toLowerCase();
1858
+ const date = match[2];
1859
+ const time = match[3] ?? "00:00:00Z";
1860
+ const origin = /* @__PURE__ */ new Date(`${date}T${time.replace(/Z?$/, "Z")}`);
1861
+ if (Number.isNaN(origin.getTime())) {
1862
+ return void 0;
1863
+ }
1864
+ return origin.getTime() + value * cfTimeUnitMultiplier(unit);
1865
+ }
1866
+ function cfTimeUnitMultiplier(unit) {
1867
+ if (unit?.startsWith("second")) {
1868
+ return 1e3;
1869
+ }
1870
+ if (unit?.startsWith("minute")) {
1871
+ return 6e4;
1872
+ }
1873
+ if (unit?.startsWith("hour")) {
1874
+ return 36e5;
1875
+ }
1876
+ return 864e5;
1877
+ }
1878
+ function inferCoordinateKey(coords, candidates) {
1879
+ const keys = Object.keys(coords);
1880
+ const normalizedKeys = keys.map((key) => key.toLowerCase().replace(/[_\-\s]+/g, ""));
1881
+ for (const candidate of candidates) {
1882
+ const index = normalizedKeys.indexOf(candidate.toLowerCase().replace(/[_\-\s]+/g, ""));
1883
+ if (index !== -1) {
1884
+ return keys[index];
1885
+ }
1886
+ }
1887
+ return void 0;
1888
+ }
1889
+ function numericCoordinates(values) {
1890
+ const raw = coordinateValues(values);
1891
+ if (!raw?.length) {
1892
+ return void 0;
1893
+ }
1894
+ const coordinates = raw.map((value) => typeof value === "number" ? value : Number(value));
1895
+ return coordinates.every((value) => Number.isFinite(value)) ? coordinates : void 0;
1896
+ }
1897
+ function hasGriddedSpatialAxes(grid, latitudeKey, longitudeKey) {
1898
+ if (!Array.isArray(grid.dataVars) || typeof grid.getVariable !== "function") {
1899
+ return false;
1900
+ }
1901
+ return grid.dataVars.some((name) => {
1902
+ if (typeof name !== "string") {
1903
+ return false;
1904
+ }
1905
+ const variable = grid.getVariable?.(name);
1906
+ return isRecord(variable) && Array.isArray(variable.dims) ? variable.dims.includes(latitudeKey) && variable.dims.includes(longitudeKey) : false;
1907
+ });
1908
+ }
1909
+ function coordinateRange(coordinates, lowerBound, upperBound) {
1910
+ const first = coordinates[0];
1911
+ const last = coordinates.at(-1);
1912
+ const ascending = first === void 0 || last === void 0 || first <= last;
1913
+ const start = ascending ? firstCoordinateAtOrAbove(coordinates, lowerBound) : firstCoordinateAtOrBelow(coordinates, upperBound);
1914
+ const stop = ascending ? lastCoordinateAtOrBelow(coordinates, upperBound) : lastCoordinateAtOrAbove(coordinates, lowerBound);
1915
+ return { start, stop };
1916
+ }
1917
+ function firstCoordinateAtOrAbove(coordinates, bound) {
1918
+ return binarySearchCoordinate(coordinates, (coordinate) => coordinate >= bound, "first") ?? nearestCoordinate(coordinates, bound);
1919
+ }
1920
+ function firstCoordinateAtOrBelow(coordinates, bound) {
1921
+ return binarySearchCoordinate(coordinates, (coordinate) => coordinate <= bound, "first") ?? nearestCoordinate(coordinates, bound);
1922
+ }
1923
+ function lastCoordinateAtOrBelow(coordinates, bound) {
1924
+ return binarySearchCoordinate(coordinates, (coordinate) => coordinate <= bound, "last") ?? nearestCoordinate(coordinates, bound);
1925
+ }
1926
+ function lastCoordinateAtOrAbove(coordinates, bound) {
1927
+ return binarySearchCoordinate(coordinates, (coordinate) => coordinate >= bound, "last") ?? nearestCoordinate(coordinates, bound);
1928
+ }
1929
+ function binarySearchCoordinate(coordinates, matches, position) {
1930
+ let low = 0;
1931
+ let high = coordinates.length - 1;
1932
+ let found;
1933
+ while (low <= high) {
1934
+ const middle = Math.floor((low + high) / 2);
1935
+ const coordinate = coordinates[middle];
1936
+ if (coordinate === void 0) {
1937
+ break;
1938
+ }
1939
+ if (matches(coordinate)) {
1940
+ found = coordinate;
1941
+ if (position === "first") {
1942
+ high = middle - 1;
1943
+ } else {
1944
+ low = middle + 1;
1945
+ }
1946
+ continue;
1947
+ }
1948
+ if (position === "first") {
1949
+ low = middle + 1;
1950
+ } else {
1951
+ high = middle - 1;
1952
+ }
1953
+ }
1954
+ return found;
1955
+ }
1956
+ function nearestCoordinate(coordinates, value) {
1957
+ return coordinates.reduce(
1958
+ (nearest, coordinate) => Math.abs(coordinate - value) < Math.abs(nearest - value) ? coordinate : nearest
1959
+ );
1960
+ }
1961
+ function replaceLoadedDataset(loaded, dataset) {
1962
+ if (Array.isArray(loaded)) {
1963
+ return [dataset, loaded[1]];
1964
+ }
1965
+ return dataset;
1966
+ }
1967
+ async function createInMemoryZarrStore(dataset) {
1968
+ if (!dataset?.coords || !Array.isArray(dataset.dataVars) || typeof dataset.getVariable !== "function") {
1969
+ return void 0;
1970
+ }
1971
+ const entries = /* @__PURE__ */ new Map();
1972
+ const encoder = new TextEncoder();
1973
+ const addJson = (path, value) => {
1974
+ entries.set(path, encoder.encode(JSON.stringify(value)));
1975
+ };
1976
+ const addBytes = (path, value) => {
1977
+ entries.set(path, value);
1978
+ };
1979
+ addJson(".zgroup", { zarr_format: 2 });
1980
+ addJson(".zattrs", {});
1981
+ for (const [dimensionName, coordinateValues2] of coordinateEntries(dataset.coords)) {
1982
+ const coordinates = numericCoordinates(coordinateValues2);
1983
+ const strings = coordinates ? void 0 : stringCoordinates(coordinateValues2);
1984
+ if (!coordinates && !strings) {
1985
+ continue;
1986
+ }
1987
+ if (strings) {
1988
+ addJson(`${dimensionName}/.zarray`, {
1989
+ zarr_format: 2,
1990
+ shape: [strings.length],
1991
+ chunks: [strings.length],
1992
+ dtype: "|O",
1993
+ compressor: null,
1994
+ fill_value: null,
1995
+ order: "C",
1996
+ filters: [{ id: "vlen-utf8" }]
1997
+ });
1998
+ addJson(`${dimensionName}/.zattrs`, {
1999
+ _ARRAY_DIMENSIONS: [dimensionName]
2000
+ });
2001
+ addBytes(`${dimensionName}/0`, encodeVLenUtf8(strings));
2002
+ continue;
2003
+ }
2004
+ if (!coordinates) {
2005
+ continue;
2006
+ }
2007
+ addJson(`${dimensionName}/.zarray`, {
2008
+ zarr_format: 2,
2009
+ shape: [coordinates.length],
2010
+ chunks: [coordinates.length],
2011
+ dtype: "<f8",
2012
+ compressor: null,
2013
+ fill_value: null,
2014
+ order: "C",
2015
+ filters: null
2016
+ });
2017
+ addJson(`${dimensionName}/.zattrs`, {
2018
+ _ARRAY_DIMENSIONS: [dimensionName]
2019
+ });
2020
+ addBytes(`${dimensionName}/0`, encodeFloat64(coordinates));
2021
+ }
2022
+ for (const variableName of dataset.dataVars) {
2023
+ if (typeof variableName !== "string") {
2024
+ continue;
2025
+ }
2026
+ const variable = dataset.getVariable(variableName);
2027
+ if (!variable?.dims?.length || !variable.shape?.length) {
2028
+ continue;
2029
+ }
2030
+ const computed = await variable.compute?.();
2031
+ const values = flattenNumericValues(
2032
+ computed?.data ?? computed?.values ?? variable.data ?? variable.values
2033
+ );
2034
+ if (!values) {
2035
+ return void 0;
2036
+ }
2037
+ const chunks = chunkShapeFor(variable.shape);
2038
+ addJson(`${variableName}/.zarray`, {
2039
+ zarr_format: 2,
2040
+ shape: variable.shape,
2041
+ chunks,
2042
+ dtype: "<f4",
2043
+ compressor: null,
2044
+ fill_value: "NaN",
2045
+ order: "C",
2046
+ filters: null
2047
+ });
2048
+ addJson(`${variableName}/.zattrs`, {
2049
+ ...variable.attrs,
2050
+ _ARRAY_DIMENSIONS: variable.dims
2051
+ });
2052
+ addVariableChunks(entries, variableName, values, variable.shape, chunks);
2053
+ }
2054
+ const byteLength = Array.from(entries.values()).reduce(
2055
+ (total, value) => total + value.byteLength,
2056
+ 0
2057
+ );
2058
+ return {
2059
+ byteLength,
2060
+ get: async (key) => entries.get(normalizeStoreKey(key))
2061
+ };
2062
+ }
2063
+ function coordinateEntries(coords) {
2064
+ if (!coords) {
2065
+ return [];
2066
+ }
2067
+ return coords instanceof Map ? Array.from(coords.entries()) : Object.entries(coords);
2068
+ }
2069
+ function coordinateValues(values) {
2070
+ if (Array.isArray(values)) {
2071
+ return values;
2072
+ }
2073
+ if (ArrayBuffer.isView(values)) {
2074
+ return arrayBufferViewValues(values);
2075
+ }
2076
+ if (!isRecord(values)) {
2077
+ return void 0;
2078
+ }
2079
+ if (Array.isArray(values.values)) {
2080
+ return values.values;
2081
+ }
2082
+ if (ArrayBuffer.isView(values.values)) {
2083
+ return arrayBufferViewValues(values.values);
2084
+ }
2085
+ if (Array.isArray(values.data)) {
2086
+ return values.data;
2087
+ }
2088
+ if (ArrayBuffer.isView(values.data)) {
2089
+ return arrayBufferViewValues(values.data);
2090
+ }
2091
+ return void 0;
2092
+ }
2093
+ function arrayBufferViewValues(values) {
2094
+ return "length" in values ? Array.from(values) : void 0;
2095
+ }
2096
+ function stringCoordinates(values) {
2097
+ const raw = coordinateValues(values);
2098
+ if (!raw?.length) {
2099
+ return void 0;
2100
+ }
2101
+ const coordinates = raw.map((value) => {
2102
+ if (value instanceof Date) {
2103
+ return value.toISOString();
2104
+ }
2105
+ return typeof value === "string" ? value : void 0;
2106
+ });
2107
+ return coordinates.every((value) => value !== void 0) ? coordinates : void 0;
2108
+ }
2109
+ function normalizeStoreKey(key) {
2110
+ return key.replace(/^\/+/, "");
2111
+ }
2112
+ function chunkShapeFor(shape) {
2113
+ if (shape.length === 0) {
2114
+ return [];
2115
+ }
2116
+ return shape.map((size, index) => index === 0 && shape.length > 2 ? 1 : size);
2117
+ }
2118
+ function addVariableChunks(entries, variableName, values, shape, chunks) {
2119
+ if (shape.length === 0) {
2120
+ entries.set(`${variableName}/0`, encodeFloat32(values));
2121
+ return;
2122
+ }
2123
+ const chunkCounts = shape.map((size, index) => Math.ceil(size / (chunks[index] ?? size)));
2124
+ const chunkIndices = enumerateChunkIndices(chunkCounts);
2125
+ for (const chunkIndex of chunkIndices) {
2126
+ const contiguousRange = leadingContiguousChunkRange(shape, chunks, chunkIndex);
2127
+ const bytes = contiguousRange ? encodeFloat32(values, contiguousRange.start, contiguousRange.length) : encodeFloat32(extractChunk(values, shape, chunks, chunkIndex));
2128
+ entries.set(`${variableName}/${chunkIndex.join(".")}`, bytes);
2129
+ }
2130
+ }
2131
+ function leadingContiguousChunkRange(shape, chunks, chunkIndex) {
2132
+ if (shape.length === 0) {
2133
+ return { start: 0, length: 1 };
2134
+ }
2135
+ const starts = chunkIndex.map((index, dimension) => index * (chunks[dimension] ?? 1));
2136
+ const stops = starts.map(
2137
+ (start, dimension) => Math.min(start + (chunks[dimension] ?? 1), shape[dimension] ?? start)
2138
+ );
2139
+ for (let dimension = 1; dimension < shape.length; dimension += 1) {
2140
+ if ((starts[dimension] ?? 0) !== 0 || (stops[dimension] ?? 0) !== (shape[dimension] ?? 0)) {
2141
+ return void 0;
2142
+ }
2143
+ }
2144
+ const trailingSize = shape.slice(1).reduce((total, size) => total * size, 1);
2145
+ const leadingStart = starts[0] ?? 0;
2146
+ const leadingStop = stops[0] ?? leadingStart;
2147
+ return {
2148
+ start: leadingStart * trailingSize,
2149
+ length: Math.max(0, leadingStop - leadingStart) * trailingSize
2150
+ };
2151
+ }
2152
+ function enumerateChunkIndices(chunkCounts) {
2153
+ const results = [];
2154
+ const visit = (prefix, dimension) => {
2155
+ if (dimension === chunkCounts.length) {
2156
+ results.push(prefix);
2157
+ return;
2158
+ }
2159
+ const count = chunkCounts[dimension] ?? 0;
2160
+ for (let index = 0; index < count; index += 1) {
2161
+ visit([...prefix, index], dimension + 1);
2162
+ }
2163
+ };
2164
+ visit([], 0);
2165
+ return results;
2166
+ }
2167
+ function extractChunk(values, shape, chunks, chunkIndex) {
2168
+ const starts = chunkIndex.map((index, dimension) => index * (chunks[dimension] ?? 1));
2169
+ const stops = starts.map(
2170
+ (start, dimension) => Math.min(start + (chunks[dimension] ?? 1), shape[dimension] ?? start)
2171
+ );
2172
+ const result = [];
2173
+ const visit = (indices, dimension) => {
2174
+ if (dimension === shape.length) {
2175
+ result.push(values[flatIndex(indices, shape)] ?? Number.NaN);
2176
+ return;
2177
+ }
2178
+ const start = starts[dimension] ?? 0;
2179
+ const stop = stops[dimension] ?? start;
2180
+ for (let index = start; index < stop; index += 1) {
2181
+ visit([...indices, index], dimension + 1);
2182
+ }
2183
+ };
2184
+ visit([], 0);
2185
+ return result;
2186
+ }
2187
+ function flatIndex(indices, shape) {
2188
+ return indices.reduce((offset, index, dimension) => offset * (shape[dimension] ?? 1) + index, 0);
2189
+ }
2190
+ function flattenNumericValues(value) {
2191
+ if (value == null) {
2192
+ return void 0;
2193
+ }
2194
+ if (typeof value === "number") {
2195
+ return [value];
2196
+ }
2197
+ if (isFlatNumericArrayLike(value)) {
2198
+ return value;
2199
+ }
2200
+ if (!Array.isArray(value)) {
2201
+ return void 0;
2202
+ }
2203
+ const result = [];
2204
+ const visit = (current) => {
2205
+ if (typeof current === "number") {
2206
+ result.push(current);
2207
+ return;
2208
+ }
2209
+ if (isFlatNumericArrayLike(current)) {
2210
+ for (let index = 0; index < current.length; index += 1) {
2211
+ result.push(current[index] ?? Number.NaN);
2212
+ }
2213
+ return;
2214
+ }
2215
+ if (Array.isArray(current)) {
2216
+ for (const item of current) {
2217
+ visit(item);
2218
+ }
2219
+ }
2220
+ };
2221
+ visit(value);
2222
+ return result;
2223
+ }
2224
+ function isFlatNumericArrayLike(value) {
2225
+ if (Array.isArray(value)) {
2226
+ return value.every((item) => typeof item === "number");
2227
+ }
2228
+ return ArrayBuffer.isView(value) && "length" in value && typeof value.length === "number" && !isBigIntArray(value);
2229
+ }
2230
+ function isBigIntArray(value) {
2231
+ return typeof BigInt64Array !== "undefined" && value instanceof BigInt64Array ? true : typeof BigUint64Array !== "undefined" && value instanceof BigUint64Array;
2232
+ }
2233
+ function encodeFloat32(values, start = 0, length = values.length - start) {
2234
+ if (length <= 0) {
2235
+ return new Uint8Array();
2236
+ }
2237
+ if (values instanceof Float32Array && isNativeLittleEndian()) {
2238
+ return new Uint8Array(values.buffer, values.byteOffset + start * 4, length * 4);
2239
+ }
2240
+ const bytes = new Uint8Array(length * 4);
2241
+ const view = new DataView(bytes.buffer);
2242
+ for (let index = 0; index < length; index += 1) {
2243
+ view.setFloat32(index * 4, values[start + index] ?? Number.NaN, true);
2244
+ }
2245
+ return bytes;
2246
+ }
2247
+ var nativeLittleEndian;
2248
+ function isNativeLittleEndian() {
2249
+ nativeLittleEndian ??= new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;
2250
+ return nativeLittleEndian;
2251
+ }
2252
+ function encodeFloat64(values) {
2253
+ const bytes = new Uint8Array(values.length * 8);
2254
+ const view = new DataView(bytes.buffer);
2255
+ values.forEach((value, index) => view.setFloat64(index * 8, value, true));
2256
+ return bytes;
2257
+ }
2258
+ function encodeVLenUtf8(values) {
2259
+ const encoder = new TextEncoder();
2260
+ const encodedValues = values.map((value) => encoder.encode(value));
2261
+ const byteLength = 4 + encodedValues.reduce((total, value) => total + 4 + value.byteLength, 0);
2262
+ const bytes = new Uint8Array(byteLength);
2263
+ const view = new DataView(bytes.buffer);
2264
+ let offset = 0;
2265
+ view.setUint32(offset, encodedValues.length, true);
2266
+ offset += 4;
2267
+ for (const value of encodedValues) {
2268
+ view.setUint32(offset, value.byteLength, true);
2269
+ offset += 4;
2270
+ bytes.set(value, offset);
2271
+ offset += value.byteLength;
2272
+ }
2273
+ return bytes;
2274
+ }
2275
+ function normalizeSelectionBounds(bounds, fallbackOptions) {
2276
+ const isTuple = isSelectionBoundsTuple(bounds);
2277
+ const normalizedBounds = isTuple ? bounds : [bounds.west, bounds.south, bounds.east, bounds.north];
2278
+ if (normalizedBounds.length !== 4 || normalizedBounds.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
2279
+ throw new DClimateAdapterError(
2280
+ "metadata",
2281
+ "dClimate selection bounds must be finite [west, south, east, north] numbers."
2282
+ );
2283
+ }
2284
+ const [west, south, east, north] = normalizedBounds;
2285
+ if (west >= east || south >= north) {
2286
+ throw new DClimateAdapterError(
2287
+ "metadata",
2288
+ "dClimate selection bounds must satisfy west < east and south < north."
2289
+ );
2290
+ }
2291
+ return {
2292
+ bounds: [west, south, east, north],
2293
+ boundsOptions: isTuple ? fallbackOptions : bounds.options ?? fallbackOptions
2294
+ };
2295
+ }
2296
+ function isSelectionBoundsTuple(bounds) {
2297
+ return Array.isArray(bounds);
2298
+ }
2299
+ async function openStore(cid, options) {
2300
+ try {
2301
+ if (options.openIpfsStore) {
2302
+ return options.openIpfsStore(cid, { gatewayUrl: options.gatewayUrl });
2303
+ }
2304
+ const module = await import('@dclimate/jaxray');
2305
+ const opened = await module.openIpfsStore?.(cid, { gatewayUrl: options.gatewayUrl });
2306
+ if (isRecord(opened) && "store" in opened && isReadableStore(opened.store)) {
2307
+ return opened.store;
2308
+ }
2309
+ return isReadableStore(opened) ? opened : void 0;
2310
+ } catch (error) {
2311
+ throw new DClimateAdapterError(
2312
+ "gateway",
2313
+ `Failed to open dClimate CID "${cid}" through Jaxray.`,
2314
+ error,
2315
+ {
2316
+ cid,
2317
+ gatewayUrl: options.gatewayUrl
2318
+ }
2319
+ );
2320
+ }
2321
+ }
2322
+ function extractDataset(value) {
2323
+ if (Array.isArray(value)) {
2324
+ return extractDataset(value[0]);
2325
+ }
2326
+ if (!isRecord(value)) {
2327
+ return void 0;
2328
+ }
2329
+ const candidate = value.data ?? value.dataset ?? value.jaxrayDataset ?? value.geoTemporalDataset ?? (hasDatasetShape(value) ? value : void 0);
2330
+ return isRecord(candidate) ? candidate : void 0;
2331
+ }
2332
+ function extractGeoTemporalDataset(value) {
2333
+ if (Array.isArray(value)) {
2334
+ return extractGeoTemporalDataset(value[0]);
2335
+ }
2336
+ if (!isRecord(value)) {
2337
+ return void 0;
2338
+ }
2339
+ return hasRectangleSelection(value) || hasTimeRange(value) ? value : void 0;
2340
+ }
2341
+ function hasTimeRange(value) {
2342
+ return isRecord(value) && typeof value.timeRange === "function";
2343
+ }
2344
+ function hasRectangleSelection(value) {
2345
+ return isRecord(value) && typeof value.rectangle === "function";
2346
+ }
2347
+ function extractMetadata(value) {
2348
+ if (Array.isArray(value)) {
2349
+ return isRecord(value[1]) ? value[1] : void 0;
2350
+ }
2351
+ if (!isRecord(value)) {
2352
+ return void 0;
2353
+ }
2354
+ const metadata = value.metadata ?? value.clientMetadata;
2355
+ return isRecord(metadata) ? metadata : void 0;
2356
+ }
2357
+ function extractStore(value) {
2358
+ if (!isRecord(value)) {
2359
+ return void 0;
2360
+ }
2361
+ const candidate = value.store ?? value.zarrStore ?? value.ipfsStore;
2362
+ return isReadableStore(candidate) ? candidate : void 0;
2363
+ }
2364
+ function extractString(value, keys) {
2365
+ const metadata = extractMetadata(value);
2366
+ if (metadata) {
2367
+ for (const key of keys) {
2368
+ const candidate = metadata[key];
2369
+ if (typeof candidate === "string") {
2370
+ return candidate;
2371
+ }
2372
+ }
2373
+ }
2374
+ if (!isRecord(value)) {
2375
+ return void 0;
2376
+ }
2377
+ for (const key of keys) {
2378
+ const candidate = value[key];
2379
+ if (typeof candidate === "string") {
2380
+ return candidate;
2381
+ }
2382
+ }
2383
+ return void 0;
2384
+ }
2385
+ function gatewaySourceForCid(cid, gatewayUrl) {
2386
+ if (!cid || !gatewayUrl) {
2387
+ return void 0;
2388
+ }
2389
+ return `${gatewayUrl.replace(/\/$/, "")}/ipfs/${cid}`;
2390
+ }
2391
+ function sourceIdForRequest(request) {
2392
+ if (request.cid) {
2393
+ return `dclimate-${request.cid.slice(0, 12)}`;
2394
+ }
2395
+ return [request.collection, request.dataset, request.variant].filter(Boolean).join("-");
2396
+ }
2397
+ function labelForRequest(request) {
2398
+ return [request.collection, request.dataset, request.variant].filter(Boolean).join(" / ") || request.cid;
2399
+ }
2400
+ function isRecord(value) {
2401
+ return typeof value === "object" && value !== null;
2402
+ }
2403
+ function hasDatasetShape(value) {
2404
+ return "data_vars" in value || "dataVars" in value || "variables" in value;
2405
+ }
2406
+ function isReadableStore(value) {
2407
+ return isRecord(value) && typeof value.get === "function";
2408
+ }
2409
+
2410
+ // src/renderers/maplibre.ts
2411
+ function createMapLibreGridLayer(options) {
2412
+ return new MapLibreGridLayerController(options);
2413
+ }
2414
+ function buildZarrLayerOptions(options) {
2415
+ assertValidGridDataSource(options.source, {
2416
+ variable: options.variable,
2417
+ requireRenderable: true
2418
+ });
2419
+ const variable = options.source.variables.find(
2420
+ (candidate) => candidate.name === options.variable || candidate.path === options.variable
2421
+ );
2422
+ if (!variable) {
2423
+ throw new GridError({
2424
+ code: "MISSING_VARIABLE",
2425
+ message: `Variable "${options.variable}" is not present in the grid source.`,
2426
+ variable: options.variable,
2427
+ sourceId: options.source.id
2428
+ });
2429
+ }
2430
+ const spatialDimensions = inferSpatialDimensions(options.source, variable.name);
2431
+ if (!spatialDimensions) {
2432
+ throw new GridError({
2433
+ code: "MISSING_SPATIAL_DIMENSIONS",
2434
+ message: `Variable "${variable.name}" needs spatial dimensions before rendering.`,
2435
+ variable: variable.name,
2436
+ sourceId: options.source.id
2437
+ });
2438
+ }
2439
+ const colorOptions = colorScaleToRendererOptions(options.colorScale);
2440
+ const rendererVariable = variable.path ?? variable.name;
2441
+ const normalizedSelectors = normalizeSelectors(options.source, variable.name, options.selectors);
2442
+ const rendererSelectors = omitSpatialSelectors(normalizedSelectors, spatialDimensions);
2443
+ const yDimension = options.source.dimensions.find(
2444
+ (dimension) => dimension.name === spatialDimensions.y
2445
+ );
2446
+ const shaderOptions = buildColorScaleShaderOptions(options.colorScale, rendererVariable);
2447
+ return {
2448
+ id: options.layerId ?? `zarr-map-${options.source.id ?? variable.name}`,
2449
+ source: options.source.source,
2450
+ store: options.source.store,
2451
+ variable: rendererVariable,
2452
+ selector: toRendererSelectors(rendererSelectors),
2453
+ spatialDimensions: { lon: spatialDimensions.x, lat: spatialDimensions.y },
2454
+ zarrVersion: options.source.zarrVersion,
2455
+ crs: options.source.crs,
2456
+ proj4: options.source.proj4,
2457
+ bounds: options.source.bounds,
2458
+ fillValue: variable.fillValue,
2459
+ colormap: colorOptions?.colormap,
2460
+ clim: colorOptions?.clim,
2461
+ opacity: options.opacity,
2462
+ latIsAscending: yDimension?.ascending,
2463
+ customFrag: shaderOptions?.customFrag,
2464
+ uniforms: shaderOptions?.uniforms,
2465
+ onLoad: () => {
2466
+ options.onLoadingChange?.(false);
2467
+ options.onLoadingStateChange?.(completeLoadingState());
2468
+ },
2469
+ onLoadingStateChange: (state) => {
2470
+ const loadingState = normalizeLoadingState(state);
2471
+ options.onLoadingChange?.(loadingState.loading);
2472
+ options.onLoadingStateChange?.(loadingState);
2473
+ },
2474
+ onError: (error) => options.onError?.(
2475
+ new GridError({
2476
+ code: "CHUNK_LOAD_FAILED",
2477
+ message: "Zarr renderer reported a metadata or chunk loading error.",
2478
+ cause: error,
2479
+ sourceId: options.source.id,
2480
+ variable: options.variable
2481
+ })
2482
+ )
2483
+ };
2484
+ }
2485
+ function buildColorScaleShaderOptions(colorScale, rendererVariable) {
2486
+ const transparentPalette = transparentPaletteKind(colorScale);
2487
+ if (!transparentPalette || !isValidGlslIdentifier(rendererVariable)) {
2488
+ return void 0;
2489
+ }
2490
+ return {
2491
+ customFrag: transparentPalette === "vegetation" ? vegetationOverlayShader(rendererVariable) : transparentPalette === "magma" ? magmaTransparentLowerBoundShader(rendererVariable) : precipitationOverlayShader(rendererVariable),
2492
+ uniforms: {}
2493
+ };
2494
+ }
2495
+ function precipitationOverlayShader(rendererVariable) {
2496
+ return `
2497
+ if (${rendererVariable} <= 0.0) {
2498
+ discard;
2499
+ }
2500
+
2501
+ float rescaled = clamp((${rendererVariable} - clim.x) / (clim.y - clim.x), 0.0, 1.0);
2502
+ vec4 c = texture(colormap, vec2(rescaled, 0.5));
2503
+ float precipAlpha = opacity * mix(0.22, 1.0, rescaled);
2504
+ fragColor = vec4(c.rgb, precipAlpha);
2505
+ fragColor.rgb *= fragColor.a;
2506
+ `;
2507
+ }
2508
+ function vegetationOverlayShader(rendererVariable) {
2509
+ return `
2510
+ float vegetationRange = clim.y - clim.x;
2511
+ float noVegetationCutoff = clim.x + vegetationRange * 0.04;
2512
+ if (${rendererVariable} <= noVegetationCutoff) {
2513
+ discard;
2514
+ }
2515
+
2516
+ float rescaled = clamp((${rendererVariable} - clim.x) / vegetationRange, 0.0, 1.0);
2517
+ vec4 c = texture(colormap, vec2(rescaled, 0.5));
2518
+ float vegetationAlpha = opacity * mix(0.14, 1.0, smoothstep(0.04, 0.85, rescaled));
2519
+ fragColor = vec4(c.rgb, vegetationAlpha);
2520
+ fragColor.rgb *= fragColor.a;
2521
+ `;
2522
+ }
2523
+ function magmaTransparentLowerBoundShader(rendererVariable) {
2524
+ return `
2525
+ float rescaled = clamp((${rendererVariable} - clim.x) / (clim.y - clim.x), 0.0, 1.0);
2526
+ vec4 c = texture(colormap, vec2(rescaled, 0.5));
2527
+ float magmaAlpha = opacity * smoothstep(0.0, 0.2, rescaled);
2528
+ fragColor = vec4(c.rgb, magmaAlpha);
2529
+ fragColor.rgb *= fragColor.a;
2530
+ `;
2531
+ }
2532
+ function transparentPaletteKind(colorScale) {
2533
+ if (typeof colorScale?.palette !== "string") {
2534
+ return void 0;
2535
+ }
2536
+ const palette = colorScale.palette.toLowerCase();
2537
+ if (palette === "magma" || palette === "precipitation" || palette === "vegetation") {
2538
+ return palette;
2539
+ }
2540
+ return void 0;
2541
+ }
2542
+ function isValidGlslIdentifier(value) {
2543
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
2544
+ }
2545
+ function colorScaleShaderSignature(options) {
2546
+ const variable = options.source.variables.find(
2547
+ (candidate) => candidate.name === options.variable || candidate.path === options.variable
2548
+ );
2549
+ const rendererVariable = variable?.path ?? variable?.name ?? options.variable;
2550
+ return buildColorScaleShaderOptions(options.colorScale, rendererVariable)?.customFrag ?? "";
2551
+ }
2552
+ var MapLibreGridLayerController = class {
2553
+ id;
2554
+ ownsMap;
2555
+ options;
2556
+ map;
2557
+ layer;
2558
+ removed = false;
2559
+ constructor(options) {
2560
+ this.id = options.layerId ?? `zarr-map-${options.source.id ?? options.variable}`;
2561
+ this.options = { ...options, layerId: this.id };
2562
+ this.map = options.map;
2563
+ this.ownsMap = !options.map;
2564
+ }
2565
+ async mount() {
2566
+ if (this.layer || this.removed) {
2567
+ return;
2568
+ }
2569
+ this.options.onLoadingChange?.(true);
2570
+ this.options.onLoadingStateChange?.({
2571
+ loading: true,
2572
+ metadata: false,
2573
+ chunks: false,
2574
+ percent: 5,
2575
+ stage: "map"
2576
+ });
2577
+ try {
2578
+ this.map ??= await this.createMap();
2579
+ await this.whenMapReady(this.map);
2580
+ this.options.onLoadingStateChange?.({
2581
+ loading: true,
2582
+ metadata: false,
2583
+ chunks: false,
2584
+ percent: 25,
2585
+ stage: "layer"
2586
+ });
2587
+ this.layer = await (this.options.layerFactory ?? createDefaultZarrLayer)(
2588
+ buildZarrLayerOptions(this.options)
2589
+ );
2590
+ this.map.addLayer(this.layer, this.options.beforeId);
2591
+ this.options.onLoadingStateChange?.({
2592
+ loading: true,
2593
+ metadata: false,
2594
+ chunks: true,
2595
+ percent: 65,
2596
+ stage: "chunks"
2597
+ });
2598
+ } catch (error) {
2599
+ this.options.onLoadingChange?.(false);
2600
+ this.handleError("RENDERER_SETUP_FAILED", "Failed to create the MapLibre Zarr layer.", error);
2601
+ }
2602
+ }
2603
+ async update(update) {
2604
+ const nextOptions = {
2605
+ ...this.options,
2606
+ ...update
2607
+ };
2608
+ if (!this.layer) {
2609
+ this.options = nextOptions;
2610
+ await this.mount();
2611
+ return;
2612
+ }
2613
+ try {
2614
+ if (update.source && update.source !== this.options.source) {
2615
+ await this.replaceLayer(nextOptions);
2616
+ this.options = nextOptions;
2617
+ return;
2618
+ }
2619
+ if (colorScaleShaderSignature(nextOptions) !== colorScaleShaderSignature(this.options)) {
2620
+ await this.replaceLayer(nextOptions);
2621
+ this.options = nextOptions;
2622
+ return;
2623
+ }
2624
+ this.options = nextOptions;
2625
+ if (update.colorScale) {
2626
+ const colorOptions = colorScaleToRendererOptions(update.colorScale);
2627
+ this.layer.setColormap?.(colorOptions?.colormap ?? []);
2628
+ this.layer.setClim?.(colorOptions?.clim ?? update.colorScale.domain);
2629
+ }
2630
+ if (typeof update.opacity === "number") {
2631
+ this.layer.setOpacity?.(update.opacity);
2632
+ }
2633
+ if (update.variable) {
2634
+ this.layer.setVariable?.(update.variable);
2635
+ }
2636
+ if (update.selectors || update.variable || update.source) {
2637
+ const variable = update.variable ?? this.options.variable;
2638
+ const source = update.source ?? this.options.source;
2639
+ const spatialDimensions = inferSpatialDimensions(source, variable);
2640
+ const normalizedSelectors = normalizeSelectors(source, variable, this.options.selectors);
2641
+ const rendererSelectors = spatialDimensions ? omitSpatialSelectors(normalizedSelectors, spatialDimensions) : normalizedSelectors;
2642
+ this.layer.setSelector?.(toRendererSelectors(rendererSelectors));
2643
+ }
2644
+ } catch (error) {
2645
+ this.handleError("RENDERER_SETUP_FAILED", "Failed to update the MapLibre Zarr layer.", error);
2646
+ }
2647
+ }
2648
+ async replaceLayer(nextOptions) {
2649
+ if (!this.map) {
2650
+ this.options = nextOptions;
2651
+ await this.mount();
2652
+ return;
2653
+ }
2654
+ this.options.onLoadingChange?.(true);
2655
+ this.options.onLoadingStateChange?.({
2656
+ loading: true,
2657
+ metadata: false,
2658
+ chunks: false,
2659
+ percent: 5,
2660
+ stage: "layer"
2661
+ });
2662
+ const previousLayer = this.layer;
2663
+ if (previousLayer && this.map.getLayer(previousLayer.id)) {
2664
+ this.map.removeLayer(previousLayer.id);
2665
+ }
2666
+ if (previousLayer?.id && this.map.getSource?.(previousLayer.id)) {
2667
+ this.map.removeSource?.(previousLayer.id);
2668
+ }
2669
+ this.layer = void 0;
2670
+ try {
2671
+ this.options.onLoadingStateChange?.({
2672
+ loading: true,
2673
+ metadata: false,
2674
+ chunks: false,
2675
+ percent: 35,
2676
+ stage: "layer"
2677
+ });
2678
+ const nextLayer = await (nextOptions.layerFactory ?? createDefaultZarrLayer)(
2679
+ buildZarrLayerOptions(nextOptions)
2680
+ );
2681
+ this.layer = nextLayer;
2682
+ this.map.addLayer(nextLayer, nextOptions.beforeId);
2683
+ this.options.onLoadingStateChange?.({
2684
+ loading: true,
2685
+ metadata: false,
2686
+ chunks: true,
2687
+ percent: 65,
2688
+ stage: "chunks"
2689
+ });
2690
+ } catch (error) {
2691
+ this.options.onLoadingChange?.(false);
2692
+ this.handleError("RENDERER_SETUP_FAILED", "Failed to update the MapLibre Zarr layer.", error);
2693
+ }
2694
+ }
2695
+ remove() {
2696
+ if (this.removed) {
2697
+ return;
2698
+ }
2699
+ this.removed = true;
2700
+ if (this.layer && this.map?.getLayer(this.layer.id)) {
2701
+ this.map.removeLayer(this.layer.id);
2702
+ }
2703
+ if (this.layer?.id && this.map?.getSource?.(this.layer.id)) {
2704
+ this.map.removeSource?.(this.layer.id);
2705
+ }
2706
+ if (this.ownsMap) {
2707
+ this.map?.remove?.();
2708
+ }
2709
+ this.layer = void 0;
2710
+ this.map = void 0;
2711
+ }
2712
+ async query(geometry) {
2713
+ if (!this.layer?.queryData) {
2714
+ throw new GridError({
2715
+ code: "UNSUPPORTED_GEOMETRY",
2716
+ message: "The active renderer layer does not expose queryData."
2717
+ });
2718
+ }
2719
+ return this.layer.queryData(geometry);
2720
+ }
2721
+ getMap() {
2722
+ return this.map;
2723
+ }
2724
+ getLayer() {
2725
+ return this.layer;
2726
+ }
2727
+ async createMap() {
2728
+ if (!this.options.mapConfig) {
2729
+ throw new GridError({
2730
+ code: "RENDERER_SETUP_FAILED",
2731
+ message: "Provide either an existing map instance or mapConfig to create one."
2732
+ });
2733
+ }
2734
+ if (this.options.mapFactory) {
2735
+ return this.options.mapFactory(this.options.mapConfig);
2736
+ }
2737
+ const maplibre = await import('maplibre-gl');
2738
+ return new maplibre.Map(
2739
+ this.options.mapConfig
2740
+ );
2741
+ }
2742
+ async whenMapReady(map) {
2743
+ if (this.isStyleReady(map) || !map.once) {
2744
+ return;
2745
+ }
2746
+ if (map.isStyleLoaded) {
2747
+ await new Promise((resolve) => {
2748
+ let resolved = false;
2749
+ const resolveWhenStyleReady = () => {
2750
+ if (resolved) {
2751
+ return;
2752
+ }
2753
+ if (this.isStyleReady(map)) {
2754
+ resolved = true;
2755
+ resolve();
2756
+ return;
2757
+ }
2758
+ map.once?.("styledata", resolveWhenStyleReady);
2759
+ };
2760
+ map.once?.("styledata", resolveWhenStyleReady);
2761
+ map.once?.("load", resolveWhenStyleReady);
2762
+ });
2763
+ return;
2764
+ }
2765
+ if (!map.loaded || map.loaded()) {
2766
+ return;
2767
+ }
2768
+ await new Promise((resolve) => {
2769
+ map.once?.("load", () => resolve());
2770
+ });
2771
+ }
2772
+ isStyleReady(map) {
2773
+ return map.isStyleLoaded?.() ?? map.loaded?.() ?? true;
2774
+ }
2775
+ handleError(code, message, cause) {
2776
+ const gridError = cause instanceof GridError ? cause : new GridError({
2777
+ code,
2778
+ message: errorMessageWithCause(message, cause),
2779
+ cause,
2780
+ sourceId: this.options.source.id,
2781
+ variable: this.options.variable
2782
+ });
2783
+ this.options.onError?.(gridError);
2784
+ throw gridError;
2785
+ }
2786
+ };
2787
+ function errorMessageWithCause(message, cause) {
2788
+ if (cause instanceof Error && cause.message) {
2789
+ return `${message} ${cause.message}`;
2790
+ }
2791
+ return message;
2792
+ }
2793
+ function omitSpatialSelectors(selectors, spatialDimensions) {
2794
+ return Object.fromEntries(
2795
+ Object.entries(selectors).filter(([dimensionName]) => {
2796
+ return dimensionName !== spatialDimensions.x && dimensionName !== spatialDimensions.y;
2797
+ })
2798
+ );
2799
+ }
2800
+ function normalizeLoadingState(state) {
2801
+ if (typeof state === "boolean") {
2802
+ return {
2803
+ loading: state,
2804
+ metadata: state,
2805
+ chunks: false,
2806
+ percent: state ? 35 : 100,
2807
+ stage: state ? "metadata" : "complete"
2808
+ };
2809
+ }
2810
+ const loading = Boolean(state.loading ?? state.metadata ?? state.chunks);
2811
+ const metadata = Boolean(state.metadata);
2812
+ const chunks = Boolean(state.chunks);
2813
+ return {
2814
+ loading,
2815
+ metadata,
2816
+ chunks,
2817
+ stage: state.stage ?? loadingStage(loading, metadata, chunks),
2818
+ percent: typeof state.percent === "number" ? Math.max(0, Math.min(100, state.percent)) : loading ? loadingPercent(metadata, chunks) : 100
2819
+ };
2820
+ }
2821
+ function loadingStage(loading, metadata, chunks) {
2822
+ if (!loading) {
2823
+ return "complete";
2824
+ }
2825
+ if (chunks) {
2826
+ return "chunks";
2827
+ }
2828
+ if (metadata) {
2829
+ return "metadata";
2830
+ }
2831
+ return "layer";
2832
+ }
2833
+ function loadingPercent(metadata, chunks) {
2834
+ if (chunks) {
2835
+ return 85;
2836
+ }
2837
+ if (metadata) {
2838
+ return 45;
2839
+ }
2840
+ return 65;
2841
+ }
2842
+ function completeLoadingState() {
2843
+ return {
2844
+ loading: false,
2845
+ metadata: false,
2846
+ chunks: false,
2847
+ percent: 100,
2848
+ stage: "complete"
2849
+ };
2850
+ }
2851
+ var zarrLayerConstructorPromise;
2852
+ async function createDefaultZarrLayer(options) {
2853
+ const ZarrLayer = await loadDefaultZarrLayerConstructor();
2854
+ return new ZarrLayer(options);
2855
+ }
2856
+ async function loadDefaultZarrLayerConstructor() {
2857
+ zarrLayerConstructorPromise ??= import('@carbonplan/zarr-layer').then((module) => {
2858
+ const zarrLayerModule = module;
2859
+ const ZarrLayer = zarrLayerModule.ZarrLayer ?? zarrLayerModule.default;
2860
+ if (!ZarrLayer) {
2861
+ throw new GridError({
2862
+ code: "RENDERER_SETUP_FAILED",
2863
+ message: "@carbonplan/zarr-layer did not export ZarrLayer."
2864
+ });
2865
+ }
2866
+ return ZarrLayer;
2867
+ }).catch((error) => {
2868
+ zarrLayerConstructorPromise = void 0;
2869
+ throw error;
2870
+ });
2871
+ return zarrLayerConstructorPromise;
2872
+ }
2873
+ function useMountEffect(effect) {
2874
+ react.useEffect(effect, []);
2875
+ }
2876
+ var BASE_PLAYBACK_INTERVAL_MS = 700;
2877
+ var PLAYBACK_SPEED_OPTIONS = [1, 1.5, 2, 3];
2878
+ function Legend({ colorScale, label }) {
2879
+ validateColorScale(colorScale);
2880
+ const [min, max] = colorScaleDisplayDomain(colorScale);
2881
+ const unit = colorScale.displayUnit ?? colorScale.unit;
2882
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: legendStyle, children: [
2883
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: legendHeaderStyle, children: [
2884
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label ?? "Value" }),
2885
+ unit ? /* @__PURE__ */ jsxRuntime.jsx("span", { children: unit }) : null
2886
+ ] }),
2887
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: gradientStyle(colorScale.palette) }),
2888
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: legendScaleStyle, children: [
2889
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatNumber(min) }),
2890
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatNumber(max) })
2891
+ ] })
2892
+ ] });
2893
+ }
2894
+ function TimeSlider({
2895
+ source,
2896
+ dimension = "time",
2897
+ value,
2898
+ defaultValue = 0,
2899
+ debounceMs = 150,
2900
+ autoPlay = false,
2901
+ playback = false,
2902
+ playbackIntervalMs = BASE_PLAYBACK_INTERVAL_MS,
2903
+ onChange
2904
+ }) {
2905
+ const coordinates = listTimeCoordinates(source, dimension);
2906
+ const playbackTimerRef = react.useRef(void 0);
2907
+ const playbackRef = react.useRef(void 0);
2908
+ const playbackSpeedIndexRef = react.useRef(0);
2909
+ const [isPlaying, setIsPlaying] = react.useState(false);
2910
+ const [playbackSpeedIndex, setPlaybackSpeedIndex] = react.useState(0);
2911
+ const [uncontrolledIndex, setUncontrolledIndex] = react.useState(defaultValue);
2912
+ const maxIndex = Math.max(coordinates.length - 1, 0);
2913
+ const activeIndex = clampIndex(value ?? uncontrolledIndex, maxIndex);
2914
+ const activeCoordinate = coordinates[activeIndex] ?? coordinates[0];
2915
+ const playbackSpeed = PLAYBACK_SPEED_OPTIONS[playbackSpeedIndex] ?? 1;
2916
+ const activePlaybackIntervalMs = playbackIntervalMs / playbackSpeed;
2917
+ const debouncer = react.useMemo(
2918
+ () => createDebouncer((coordinate) => {
2919
+ onChange?.(coordinate);
2920
+ }, debounceMs),
2921
+ [debounceMs, onChange]
2922
+ );
2923
+ const debouncerRef = react.useRef(debouncer);
2924
+ debouncerRef.current = debouncer;
2925
+ playbackSpeedIndexRef.current = playbackSpeedIndex;
2926
+ const stopPlayback = react.useCallback(() => {
2927
+ if (playbackTimerRef.current) {
2928
+ clearInterval(playbackTimerRef.current);
2929
+ playbackTimerRef.current = void 0;
2930
+ }
2931
+ setIsPlaying(false);
2932
+ }, []);
2933
+ const selectCoordinate = (nextIndex, options = { debounced: true }) => {
2934
+ const boundedIndex = clampIndex(nextIndex, maxIndex);
2935
+ if (value === void 0) {
2936
+ setUncontrolledIndex(boundedIndex);
2937
+ }
2938
+ const coordinate = coordinates[boundedIndex];
2939
+ if (!coordinate) {
2940
+ stopPlayback();
2941
+ return;
2942
+ }
2943
+ if (options.debounced) {
2944
+ debouncer.call(coordinate);
2945
+ return;
2946
+ }
2947
+ debouncer.cancel();
2948
+ onChange?.(coordinate);
2949
+ };
2950
+ playbackRef.current = {
2951
+ activeIndex,
2952
+ coordinates,
2953
+ selectCoordinate
2954
+ };
2955
+ const advancePlayback = react.useCallback(() => {
2956
+ const frameState = playbackRef.current;
2957
+ if (!frameState || frameState.coordinates.length <= 1) {
2958
+ stopPlayback();
2959
+ return;
2960
+ }
2961
+ frameState.selectCoordinate((frameState.activeIndex + 1) % frameState.coordinates.length, {
2962
+ debounced: false
2963
+ });
2964
+ }, [stopPlayback]);
2965
+ const startPlaybackTimer = react.useCallback(
2966
+ (intervalMs) => {
2967
+ if (playbackTimerRef.current) {
2968
+ clearInterval(playbackTimerRef.current);
2969
+ }
2970
+ playbackTimerRef.current = setInterval(advancePlayback, intervalMs);
2971
+ },
2972
+ [advancePlayback]
2973
+ );
2974
+ const startPlayback = () => {
2975
+ if (coordinates.length <= 1) {
2976
+ return;
2977
+ }
2978
+ setIsPlaying(true);
2979
+ advancePlayback();
2980
+ startPlaybackTimer(activePlaybackIntervalMs);
2981
+ };
2982
+ const togglePlayback = () => {
2983
+ if (playbackTimerRef.current) {
2984
+ stopPlayback();
2985
+ return;
2986
+ }
2987
+ startPlayback();
2988
+ };
2989
+ const cyclePlaybackSpeed = () => {
2990
+ const nextIndex = (playbackSpeedIndexRef.current + 1) % PLAYBACK_SPEED_OPTIONS.length;
2991
+ const nextSpeed = PLAYBACK_SPEED_OPTIONS[nextIndex] ?? 1;
2992
+ playbackSpeedIndexRef.current = nextIndex;
2993
+ setPlaybackSpeedIndex(nextIndex);
2994
+ if (playbackTimerRef.current) {
2995
+ startPlaybackTimer(playbackIntervalMs / nextSpeed);
2996
+ }
2997
+ };
2998
+ useMountEffect(() => {
2999
+ if (autoPlay && playback) {
3000
+ startPlayback();
3001
+ }
3002
+ return () => {
3003
+ debouncerRef.current.cancel();
3004
+ stopPlayback();
3005
+ };
3006
+ });
3007
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: playback ? sliderShellStyle : sliderShellWithoutPlaybackStyle, children: [
3008
+ playback ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: playbackControlsStyle, children: [
3009
+ /* @__PURE__ */ jsxRuntime.jsx(
3010
+ "button",
3011
+ {
3012
+ "aria-label": isPlaying ? "Pause time playback" : "Play time playback",
3013
+ disabled: coordinates.length <= 1,
3014
+ onClick: togglePlayback,
3015
+ style: playButtonStyle,
3016
+ type: "button",
3017
+ children: /* @__PURE__ */ jsxRuntime.jsx(PlaybackIcon, { playing: isPlaying })
3018
+ }
3019
+ ),
3020
+ /* @__PURE__ */ jsxRuntime.jsx(
3021
+ "button",
3022
+ {
3023
+ "aria-label": `Playback speed ${formatPlaybackSpeed(playbackSpeed)}. Click to cycle speed.`,
3024
+ disabled: coordinates.length <= 1,
3025
+ onClick: cyclePlaybackSpeed,
3026
+ style: playbackSpeedButtonStyle,
3027
+ type: "button",
3028
+ children: formatPlaybackSpeed(playbackSpeed)
3029
+ }
3030
+ )
3031
+ ] }) : null,
3032
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { style: sliderBodyStyle, children: [
3033
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: sliderLabelStyle, children: activeCoordinate?.label ?? "No time coordinates" }),
3034
+ /* @__PURE__ */ jsxRuntime.jsx(
3035
+ "input",
3036
+ {
3037
+ "aria-label": "Time",
3038
+ disabled: coordinates.length === 0,
3039
+ max: maxIndex,
3040
+ min: 0,
3041
+ onChange: (event) => {
3042
+ selectCoordinate(Number(event.currentTarget.value));
3043
+ },
3044
+ step: 1,
3045
+ style: sliderStyle,
3046
+ type: "range",
3047
+ value: activeIndex
3048
+ }
3049
+ )
3050
+ ] })
3051
+ ] });
3052
+ }
3053
+ function formatPlaybackSpeed(speed) {
3054
+ return `${Number.isInteger(speed) ? speed.toFixed(0) : String(speed)}x`;
3055
+ }
3056
+ function clampIndex(index, maxIndex) {
3057
+ if (!Number.isFinite(index)) {
3058
+ return 0;
3059
+ }
3060
+ return Math.max(0, Math.min(maxIndex, Math.round(index)));
3061
+ }
3062
+ function PlaybackIcon({ playing }) {
3063
+ if (playing) {
3064
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { "aria-hidden": "true", style: pauseIconStyle, children: [
3065
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: pauseBarStyle }),
3066
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: pauseBarStyle })
3067
+ ] });
3068
+ }
3069
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", style: playIconStyle });
3070
+ }
3071
+ function ZarrMap(props) {
3072
+ const instanceKey = JSON.stringify(props.map);
3073
+ return /* @__PURE__ */ jsxRuntime.jsx(ZarrMapInstance, { ...props }, instanceKey);
3074
+ }
3075
+ function ZarrMapInstance({
3076
+ source,
3077
+ variable,
3078
+ selector,
3079
+ initialSelector,
3080
+ colorScale,
3081
+ opacity,
3082
+ map,
3083
+ timeDimension,
3084
+ controls,
3085
+ className,
3086
+ style,
3087
+ renderer,
3088
+ formatInspectValue,
3089
+ onSelectorChange,
3090
+ onInspect,
3091
+ onLoadingStateChange,
3092
+ onReady,
3093
+ children
3094
+ }) {
3095
+ const controllerRef = react.useRef(void 0);
3096
+ const hoverInspectTargetRef = react.useRef(void 0);
3097
+ const hoverInspectTimerRef = react.useRef(void 0);
3098
+ const inspectCleanupRef = react.useRef(void 0);
3099
+ const inspectRequestIdRef = react.useRef(0);
3100
+ const mapContainerRef = react.useRef(void 0);
3101
+ const propsRef = react.useRef(void 0);
3102
+ const lastUpdateSignatureRef = react.useRef(void 0);
3103
+ const lastSourceRef = react.useRef(void 0);
3104
+ const [status, setStatus] = react.useState("idle");
3105
+ const [errorMessage, setErrorMessage] = react.useState();
3106
+ const [inspectState, setInspectState] = react.useState();
3107
+ const [loadingPercent2, setLoadingPercent] = react.useState(0);
3108
+ const [currentSelector, setCurrentSelector] = react.useState(
3109
+ selector ?? initialSelector ?? {}
3110
+ );
3111
+ const resolvedTimeDimension = timeDimension ?? source.dimensions.find((dimension) => dimension.kind === "time")?.name ?? "time";
3112
+ const activeSelector = selector ?? currentSelector;
3113
+ const updateSignature = JSON.stringify({
3114
+ colorScale,
3115
+ opacity,
3116
+ selector: activeSelector,
3117
+ sourceId: source.id,
3118
+ variable
3119
+ });
3120
+ const inspectControl = normalizeInspectControl(controls?.inspect);
3121
+ const inspectSample = inspectState?.result.samples[0];
3122
+ const visibleInspectState = inspectControl.enabled && inspectState?.mode === inspectControl.mode && inspectSample && (inspectState.mode !== "hover" || inspectSample.value !== null) ? inspectState : void 0;
3123
+ const visibleInspectSample = visibleInspectState ? inspectSample : void 0;
3124
+ propsRef.current = {
3125
+ colorScale,
3126
+ controls,
3127
+ currentSelector,
3128
+ map,
3129
+ formatInspectValue,
3130
+ onInspect,
3131
+ onLoadingStateChange,
3132
+ onReady,
3133
+ opacity,
3134
+ renderer,
3135
+ resolvedTimeDimension,
3136
+ selector,
3137
+ source,
3138
+ updateSignature,
3139
+ variable
3140
+ };
3141
+ const clearHoverInspect = react.useCallback((clearResult = false) => {
3142
+ hoverInspectTargetRef.current = void 0;
3143
+ if (hoverInspectTimerRef.current) {
3144
+ clearTimeout(hoverInspectTimerRef.current);
3145
+ hoverInspectTimerRef.current = void 0;
3146
+ }
3147
+ inspectRequestIdRef.current += 1;
3148
+ if (clearResult) {
3149
+ setInspectState(void 0);
3150
+ }
3151
+ }, []);
3152
+ const queryInspect = react.useCallback(
3153
+ (mounted, coordinates, mode, point) => {
3154
+ const requestId = inspectRequestIdRef.current += 1;
3155
+ void queryInspectPoint(controllerRef.current, mounted, coordinates).then((result) => {
3156
+ if (inspectRequestIdRef.current !== requestId) {
3157
+ return;
3158
+ }
3159
+ setInspectState({ mode, point, result });
3160
+ propsRef.current?.onInspect?.(result);
3161
+ }).catch((error) => {
3162
+ if (inspectRequestIdRef.current !== requestId) {
3163
+ return;
3164
+ }
3165
+ setErrorMessage(error instanceof Error ? error.message : String(error));
3166
+ setStatus("error");
3167
+ });
3168
+ },
3169
+ []
3170
+ );
3171
+ const handleMapClick = react.useCallback(
3172
+ (event) => {
3173
+ const mounted = propsRef.current;
3174
+ const inspect = normalizeInspectControl(mounted?.controls?.inspect);
3175
+ const coordinates = extractLngLat(event);
3176
+ if (!mounted || !inspect.enabled || inspect.mode !== "click" || !coordinates) {
3177
+ return;
3178
+ }
3179
+ clearHoverInspect();
3180
+ queryInspect(mounted, coordinates, "click");
3181
+ },
3182
+ [clearHoverInspect, queryInspect]
3183
+ );
3184
+ const handleMapMouseMove = react.useCallback(
3185
+ (event) => {
3186
+ const mounted = propsRef.current;
3187
+ const inspect = normalizeInspectControl(mounted?.controls?.inspect);
3188
+ const coordinates = extractLngLat(event);
3189
+ const point = extractInspectTooltipPoint(event, mapContainerRef.current);
3190
+ if (!mounted || !inspect.enabled || inspect.mode !== "hover" || !coordinates || !point) {
3191
+ return;
3192
+ }
3193
+ const runQuery = () => {
3194
+ hoverInspectTimerRef.current = void 0;
3195
+ const current = propsRef.current;
3196
+ const currentInspect = normalizeInspectControl(current?.controls?.inspect);
3197
+ if (!current || !currentInspect.enabled || currentInspect.mode !== "hover") {
3198
+ return;
3199
+ }
3200
+ queryInspect(current, coordinates, "hover", point);
3201
+ };
3202
+ clearHoverInspect();
3203
+ hoverInspectTargetRef.current = { coordinates, point };
3204
+ if (inspect.debounceMs > 0) {
3205
+ hoverInspectTimerRef.current = setTimeout(runQuery, inspect.debounceMs);
3206
+ return;
3207
+ }
3208
+ runQuery();
3209
+ },
3210
+ [clearHoverInspect, queryInspect]
3211
+ );
3212
+ const handleMapMouseOut = react.useCallback(() => {
3213
+ const inspect = normalizeInspectControl(propsRef.current?.controls?.inspect);
3214
+ if (!inspect.enabled || inspect.mode !== "hover") {
3215
+ return;
3216
+ }
3217
+ clearHoverInspect(true);
3218
+ }, [clearHoverInspect]);
3219
+ const attachContainer = react.useCallback(
3220
+ (container) => {
3221
+ if (!container) {
3222
+ mapContainerRef.current = void 0;
3223
+ clearHoverInspect();
3224
+ inspectCleanupRef.current?.();
3225
+ inspectCleanupRef.current = void 0;
3226
+ controllerRef.current?.remove();
3227
+ controllerRef.current = void 0;
3228
+ return;
3229
+ }
3230
+ mapContainerRef.current = container;
3231
+ const mounted = propsRef.current;
3232
+ if (!mounted || controllerRef.current) {
3233
+ return;
3234
+ }
3235
+ lastUpdateSignatureRef.current = mounted.updateSignature;
3236
+ lastSourceRef.current = mounted.source;
3237
+ setStatus("loading");
3238
+ const controller = createMapLibreGridLayer({
3239
+ source: mounted.source,
3240
+ variable: mounted.variable,
3241
+ selectors: mounted.selector ?? mounted.currentSelector,
3242
+ colorScale: mounted.colorScale,
3243
+ opacity: mounted.opacity,
3244
+ mapConfig: {
3245
+ ...mounted.map,
3246
+ container
3247
+ },
3248
+ layerFactory: mounted.renderer?.layerFactory,
3249
+ mapFactory: mounted.renderer?.mapFactory,
3250
+ onLoadingChange: (loading) => setStatus(loading ? "loading" : "ready"),
3251
+ onLoadingStateChange: (loadingState) => {
3252
+ setLoadingPercent(loadingState.percent);
3253
+ setStatus(loadingState.loading ? "loading" : "ready");
3254
+ propsRef.current?.onLoadingStateChange?.(loadingState);
3255
+ },
3256
+ onError: (error) => {
3257
+ setErrorMessage(error.message);
3258
+ setStatus("error");
3259
+ }
3260
+ });
3261
+ controllerRef.current = controller;
3262
+ controller.mount().then(() => {
3263
+ const activeMap = controller.getMap();
3264
+ if (activeMap?.on) {
3265
+ activeMap.on("click", handleMapClick);
3266
+ activeMap.on("mousemove", handleMapMouseMove);
3267
+ activeMap.on("mouseout", handleMapMouseOut);
3268
+ activeMap.on("mouseleave", handleMapMouseOut);
3269
+ inspectCleanupRef.current = () => {
3270
+ activeMap.off?.("click", handleMapClick);
3271
+ activeMap.off?.("mousemove", handleMapMouseMove);
3272
+ activeMap.off?.("mouseout", handleMapMouseOut);
3273
+ activeMap.off?.("mouseleave", handleMapMouseOut);
3274
+ };
3275
+ }
3276
+ propsRef.current?.onReady?.(controller);
3277
+ }).catch((error) => {
3278
+ setErrorMessage(error instanceof Error ? error.message : String(error));
3279
+ setStatus("error");
3280
+ });
3281
+ },
3282
+ [clearHoverInspect, handleMapClick, handleMapMouseMove, handleMapMouseOut]
3283
+ );
3284
+ react.useLayoutEffect(() => {
3285
+ const controller = controllerRef.current;
3286
+ if (!controller) {
3287
+ return;
3288
+ }
3289
+ const sourceChanged = lastSourceRef.current !== source;
3290
+ if (!sourceChanged && lastUpdateSignatureRef.current === updateSignature) {
3291
+ return;
3292
+ }
3293
+ lastSourceRef.current = source;
3294
+ lastUpdateSignatureRef.current = updateSignature;
3295
+ void controller.update({
3296
+ source,
3297
+ variable,
3298
+ selectors: activeSelector,
3299
+ colorScale,
3300
+ opacity
3301
+ }).then(() => {
3302
+ const mounted = propsRef.current;
3303
+ const hoverTarget = hoverInspectTargetRef.current;
3304
+ const inspect = normalizeInspectControl(mounted?.controls?.inspect);
3305
+ if (!mounted || !hoverTarget || !inspect.enabled || inspect.mode !== "hover") {
3306
+ return;
3307
+ }
3308
+ queryInspect(mounted, hoverTarget.coordinates, "hover", hoverTarget.point);
3309
+ }).catch((error) => {
3310
+ setErrorMessage(error instanceof Error ? error.message : String(error));
3311
+ setStatus("error");
3312
+ });
3313
+ }, [activeSelector, colorScale, opacity, queryInspect, source, updateSignature, variable]);
3314
+ const handleTimeChange = (coordinate) => {
3315
+ const nextSelector = {
3316
+ ...activeSelector,
3317
+ [resolvedTimeDimension]: coordinate.iso ? { kind: "isoTime", iso: coordinate.iso } : { kind: "coordinate", value: coordinate.value }
3318
+ };
3319
+ setCurrentSelector(nextSelector);
3320
+ onSelectorChange?.(nextSelector);
3321
+ };
3322
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className, style: { ...mapShellStyle, ...style }, children: [
3323
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: attachContainer, style: mapContainerStyle }),
3324
+ controls?.legend && colorScale ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: legendOverlayStyle, children: /* @__PURE__ */ jsxRuntime.jsx(Legend, { colorScale, label: variable }) }) : null,
3325
+ controls?.timeSlider ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: sliderOverlayStyle, children: /* @__PURE__ */ jsxRuntime.jsx(
3326
+ TimeSlider,
3327
+ {
3328
+ dimension: resolvedTimeDimension,
3329
+ autoPlay: controls.timePlaybackAutoPlay,
3330
+ playback: controls.timePlayback,
3331
+ value: timeIndexForSelector(source, resolvedTimeDimension, activeSelector),
3332
+ source,
3333
+ onChange: handleTimeChange
3334
+ },
3335
+ `${source.id}:${resolvedTimeDimension}`
3336
+ ) }) : null,
3337
+ visibleInspectState && visibleInspectSample ? /* @__PURE__ */ jsxRuntime.jsxs(
3338
+ "div",
3339
+ {
3340
+ role: visibleInspectState.mode === "hover" ? "tooltip" : void 0,
3341
+ style: visibleInspectState.mode === "hover" && visibleInspectState.point ? inspectTooltipStyle(visibleInspectState.point) : inspectOverlayStyle,
3342
+ children: [
3343
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: formatCoordinatePair(visibleInspectSample.coordinates) }),
3344
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: formatInspectSample(visibleInspectState.result, colorScale, formatInspectValue) })
3345
+ ]
3346
+ }
3347
+ ) : null,
3348
+ status === "loading" ? /* @__PURE__ */ jsxRuntime.jsx(LoadingStatus, { label: "Loading raster", percent: loadingPercent2 }) : null,
3349
+ status === "error" ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: errorStyle, children: errorMessage ?? "Failed to render raster" }) : null,
3350
+ children
3351
+ ] });
3352
+ }
3353
+ function LoadingStatus({ label, percent }) {
3354
+ const percentLabel = typeof percent === "number" ? formatPercent(percent) : void 0;
3355
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "aria-live": "polite", role: "status", style: statusStyle, children: [
3356
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: statusHeaderStyle, children: [
3357
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: label }),
3358
+ percentLabel ? /* @__PURE__ */ jsxRuntime.jsx("strong", { style: statusPercentStyle, children: percentLabel }) : null
3359
+ ] }),
3360
+ percentLabel ? /* @__PURE__ */ jsxRuntime.jsx("div", { style: statusTrackStyle, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { ...statusProgressStyle, width: percentLabel } }) }) : null
3361
+ ] });
3362
+ }
3363
+ function formatPercent(percent) {
3364
+ return `${Math.max(0, Math.min(100, Math.round(percent)))}%`;
3365
+ }
3366
+ function normalizeInspectControl(control) {
3367
+ if (!control) {
3368
+ return { enabled: false, mode: "click", debounceMs: 0 };
3369
+ }
3370
+ if (control === true) {
3371
+ return { enabled: true, mode: "click", debounceMs: 0 };
3372
+ }
3373
+ if (control === "click") {
3374
+ return { enabled: true, mode: "click", debounceMs: 0 };
3375
+ }
3376
+ if (control === "hover") {
3377
+ return { enabled: true, mode: "hover", debounceMs: 100 };
3378
+ }
3379
+ const mode = control.mode ?? "click";
3380
+ return {
3381
+ enabled: true,
3382
+ mode,
3383
+ debounceMs: Math.max(0, control.debounceMs ?? (mode === "hover" ? 100 : 0))
3384
+ };
3385
+ }
3386
+ function DClimateZarrMap(props) {
3387
+ const sourceOptions = normalizeDClimateSourceOptions(props);
3388
+ const loadKey = `${JSON.stringify(props.dataset)}:${props.gatewayUrl ?? ""}:${JSON.stringify(sourceOptions ?? {})}:${JSON.stringify(props.preflight ?? {})}`;
3389
+ return /* @__PURE__ */ jsxRuntime.jsx(DClimateZarrMapLoader, { ...props, sourceOptions }, loadKey);
3390
+ }
3391
+ function normalizeDClimateSourceOptions({
3392
+ bounds,
3393
+ boundsOptions,
3394
+ sourceOptions,
3395
+ timeRange
3396
+ }) {
3397
+ const selection = {
3398
+ ...sourceOptions?.selection
3399
+ };
3400
+ if (bounds) {
3401
+ selection.bounds = bounds;
3402
+ }
3403
+ if (boundsOptions) {
3404
+ selection.boundsOptions = boundsOptions;
3405
+ }
3406
+ if (timeRange) {
3407
+ selection.timeRange = timeRange;
3408
+ }
3409
+ return Object.keys(selection).length === 0 ? sourceOptions : {
3410
+ ...sourceOptions,
3411
+ selection
3412
+ };
3413
+ }
3414
+ function normalizePreflightConfig(preflight, onPreflight, defaults) {
3415
+ if (!preflight && !onPreflight) {
3416
+ return void 0;
3417
+ }
3418
+ return {
3419
+ ...preflight,
3420
+ bounds: preflight?.bounds ?? defaults.bounds,
3421
+ onResult: (result) => {
3422
+ preflight?.onResult?.(result);
3423
+ onPreflight?.(result);
3424
+ },
3425
+ timeRange: preflight?.timeRange ?? defaults.timeRange,
3426
+ variable: preflight?.variable ?? defaults.variable
3427
+ };
3428
+ }
3429
+ function selectionBounds(selection) {
3430
+ const bounds = selection?.bounds;
3431
+ if (!bounds) {
3432
+ return void 0;
3433
+ }
3434
+ return isSelectionBoundsTuple2(bounds) ? [bounds[0], bounds[1], bounds[2], bounds[3]] : [bounds.west, bounds.south, bounds.east, bounds.north];
3435
+ }
3436
+ function isSelectionBoundsTuple2(bounds) {
3437
+ return Array.isArray(bounds);
3438
+ }
3439
+ function DClimateZarrMapLoader({
3440
+ dataset,
3441
+ sourceOptions,
3442
+ preflight,
3443
+ onPreflight,
3444
+ client,
3445
+ clientOptions,
3446
+ gatewayUrl,
3447
+ openIpfsStore,
3448
+ ...mapProps
3449
+ }) {
3450
+ const [source, setSource] = react.useState();
3451
+ const [error, setError] = react.useState();
3452
+ useMountEffect(() => {
3453
+ let mounted = true;
3454
+ void createDClimateSource(dataset, {
3455
+ ...sourceOptions,
3456
+ client,
3457
+ clientOptions,
3458
+ gatewayUrl,
3459
+ openIpfsStore,
3460
+ preflight: normalizePreflightConfig(preflight, onPreflight, {
3461
+ bounds: selectionBounds(sourceOptions?.selection),
3462
+ timeRange: sourceOptions?.selection?.timeRange,
3463
+ variable: mapProps.variable
3464
+ })
3465
+ }).then((loadedSource) => {
3466
+ if (mounted) {
3467
+ setSource(loadedSource);
3468
+ }
3469
+ }).catch((loadError) => {
3470
+ if (mounted) {
3471
+ setError(loadError instanceof Error ? loadError.message : String(loadError));
3472
+ }
3473
+ });
3474
+ return () => {
3475
+ mounted = false;
3476
+ };
3477
+ });
3478
+ if (error) {
3479
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { style: errorStyle, children: error });
3480
+ }
3481
+ if (!source) {
3482
+ return /* @__PURE__ */ jsxRuntime.jsx(LoadingStatus, { label: "Loading dClimate dataset" });
3483
+ }
3484
+ return /* @__PURE__ */ jsxRuntime.jsx(ZarrMap, { ...mapProps, source });
3485
+ }
3486
+ var mapShellStyle = {
3487
+ background: "#020304",
3488
+ minHeight: 420,
3489
+ overflow: "hidden",
3490
+ position: "relative",
3491
+ width: "100%"
3492
+ };
3493
+ var mapContainerStyle = {
3494
+ height: "100%",
3495
+ minHeight: 420,
3496
+ width: "100%"
3497
+ };
3498
+ var legendOverlayStyle = {
3499
+ bottom: 24,
3500
+ left: 24,
3501
+ position: "absolute",
3502
+ zIndex: 2
3503
+ };
3504
+ var sliderOverlayStyle = {
3505
+ bottom: 24,
3506
+ left: 230,
3507
+ maxWidth: "calc(100% - 254px)",
3508
+ position: "absolute",
3509
+ right: 24,
3510
+ zIndex: 2
3511
+ };
3512
+ var statusStyle = {
3513
+ WebkitBackdropFilter: "blur(18px)",
3514
+ backdropFilter: "blur(18px)",
3515
+ background: "rgba(4, 8, 12, 0.78)",
3516
+ border: "1px solid rgba(125, 211, 252, 0.2)",
3517
+ borderRadius: 8,
3518
+ boxShadow: "0 20px 54px rgba(0, 0, 0, 0.42)",
3519
+ color: "#f8fafc",
3520
+ fontSize: 13,
3521
+ minWidth: 210,
3522
+ padding: "12px 13px",
3523
+ position: "absolute",
3524
+ right: 24,
3525
+ top: 24,
3526
+ zIndex: 3
3527
+ };
3528
+ var statusHeaderStyle = {
3529
+ alignItems: "center",
3530
+ display: "flex",
3531
+ gap: 14,
3532
+ fontWeight: 650,
3533
+ justifyContent: "space-between",
3534
+ lineHeight: 1.2
3535
+ };
3536
+ var statusPercentStyle = {
3537
+ color: "#fde725",
3538
+ fontSize: 12,
3539
+ fontWeight: 720
3540
+ };
3541
+ var statusTrackStyle = {
3542
+ background: "rgba(148, 163, 184, 0.2)",
3543
+ borderRadius: 999,
3544
+ height: 4,
3545
+ marginTop: 10,
3546
+ overflow: "hidden",
3547
+ width: "100%"
3548
+ };
3549
+ var statusProgressStyle = {
3550
+ background: "linear-gradient(90deg, #31688e, #35b779, #fde725)",
3551
+ borderRadius: 999,
3552
+ boxShadow: "0 0 14px rgba(53, 183, 121, 0.5)",
3553
+ height: "100%",
3554
+ transition: "width 150ms ease"
3555
+ };
3556
+ var inspectBaseStyle = {
3557
+ WebkitBackdropFilter: "blur(18px)",
3558
+ backdropFilter: "blur(18px)",
3559
+ background: "rgba(4, 8, 12, 0.78)",
3560
+ border: "1px solid rgba(125, 211, 252, 0.2)",
3561
+ borderRadius: 8,
3562
+ boxShadow: "0 20px 54px rgba(0, 0, 0, 0.42)",
3563
+ color: "#f8fafc",
3564
+ display: "grid",
3565
+ gap: 4,
3566
+ fontSize: 13,
3567
+ padding: "11px 13px",
3568
+ zIndex: 3
3569
+ };
3570
+ var inspectOverlayStyle = {
3571
+ ...inspectBaseStyle,
3572
+ position: "absolute",
3573
+ right: 24,
3574
+ top: 24
3575
+ };
3576
+ var errorStyle = {
3577
+ ...statusStyle,
3578
+ background: "rgba(69, 10, 10, 0.78)",
3579
+ borderColor: "rgba(248, 113, 113, 0.5)",
3580
+ color: "#fecaca"
3581
+ };
3582
+ var legendStyle = {
3583
+ WebkitBackdropFilter: "blur(18px)",
3584
+ backdropFilter: "blur(18px)",
3585
+ background: "rgba(4, 8, 12, 0.78)",
3586
+ border: "1px solid rgba(125, 211, 252, 0.2)",
3587
+ borderRadius: 8,
3588
+ boxShadow: "0 20px 54px rgba(0, 0, 0, 0.42)",
3589
+ color: "#f8fafc",
3590
+ padding: 13,
3591
+ width: 182
3592
+ };
3593
+ var legendHeaderStyle = {
3594
+ color: "#f8fafc",
3595
+ display: "flex",
3596
+ fontSize: 12,
3597
+ fontWeight: 700,
3598
+ justifyContent: "space-between",
3599
+ marginBottom: 8
3600
+ };
3601
+ var legendScaleStyle = {
3602
+ color: "#cbd5e1",
3603
+ display: "flex",
3604
+ fontSize: 12,
3605
+ justifyContent: "space-between",
3606
+ marginTop: 6
3607
+ };
3608
+ var sliderShellStyle = {
3609
+ alignItems: "center",
3610
+ WebkitBackdropFilter: "blur(18px)",
3611
+ backdropFilter: "blur(18px)",
3612
+ background: "rgba(4, 8, 12, 0.74)",
3613
+ border: "1px solid rgba(125, 211, 252, 0.2)",
3614
+ borderRadius: 8,
3615
+ boxShadow: "0 20px 54px rgba(0, 0, 0, 0.42)",
3616
+ color: "#f8fafc",
3617
+ display: "grid",
3618
+ gap: 12,
3619
+ gridTemplateColumns: "88px minmax(0, 1fr)",
3620
+ padding: "11px 13px"
3621
+ };
3622
+ var sliderShellWithoutPlaybackStyle = {
3623
+ ...sliderShellStyle,
3624
+ gridTemplateColumns: "minmax(0, 1fr)"
3625
+ };
3626
+ var sliderBodyStyle = {
3627
+ alignItems: "center",
3628
+ display: "grid",
3629
+ gap: 12,
3630
+ gridTemplateColumns: "minmax(150px, 240px) minmax(160px, 1fr)",
3631
+ minWidth: 0
3632
+ };
3633
+ var playbackControlsStyle = {
3634
+ alignItems: "center",
3635
+ display: "flex",
3636
+ gap: 8
3637
+ };
3638
+ var playButtonStyle = {
3639
+ alignItems: "center",
3640
+ background: "rgba(15, 23, 42, 0.86)",
3641
+ border: "1px solid rgba(125, 211, 252, 0.28)",
3642
+ borderRadius: 6,
3643
+ color: "#f8fafc",
3644
+ cursor: "pointer",
3645
+ display: "inline-flex",
3646
+ height: 40,
3647
+ justifyContent: "center",
3648
+ padding: 0,
3649
+ width: 40
3650
+ };
3651
+ var playbackSpeedButtonStyle = {
3652
+ ...playButtonStyle,
3653
+ fontSize: 12,
3654
+ fontVariantNumeric: "tabular-nums",
3655
+ fontWeight: 760,
3656
+ lineHeight: 1
3657
+ };
3658
+ var playIconStyle = {
3659
+ borderBottom: "7px solid transparent",
3660
+ borderLeft: "11px solid #f8fafc",
3661
+ borderTop: "7px solid transparent",
3662
+ display: "block",
3663
+ height: 0,
3664
+ marginLeft: 3,
3665
+ width: 0
3666
+ };
3667
+ var pauseIconStyle = {
3668
+ display: "inline-flex",
3669
+ gap: 4,
3670
+ height: 16
3671
+ };
3672
+ var pauseBarStyle = {
3673
+ background: "#f8fafc",
3674
+ borderRadius: 2,
3675
+ display: "block",
3676
+ height: 16,
3677
+ width: 5
3678
+ };
3679
+ var sliderLabelStyle = {
3680
+ color: "#cbd5e1",
3681
+ fontSize: 12,
3682
+ fontWeight: 600,
3683
+ overflow: "hidden",
3684
+ textOverflow: "ellipsis",
3685
+ whiteSpace: "nowrap"
3686
+ };
3687
+ var sliderStyle = {
3688
+ accentColor: "#35b779",
3689
+ width: "100%"
3690
+ };
3691
+ function inspectTooltipStyle(point) {
3692
+ return {
3693
+ ...inspectBaseStyle,
3694
+ left: point.x,
3695
+ maxWidth: 220,
3696
+ pointerEvents: "none",
3697
+ position: "absolute",
3698
+ top: point.y,
3699
+ transform: `translate(${point.horizontal === "left" ? "calc(-100% - 12px)" : "12px"}, ${point.vertical === "above" ? "calc(-100% - 12px)" : "12px"})`
3700
+ };
3701
+ }
3702
+ function gradientStyle(palette) {
3703
+ const gradient = `linear-gradient(90deg, ${paletteToCssGradientColors(palette).join(", ")})`;
3704
+ return {
3705
+ background: gradient,
3706
+ borderRadius: 4,
3707
+ boxShadow: "0 0 18px rgba(53, 183, 121, 0.2)",
3708
+ height: 10,
3709
+ width: "100%"
3710
+ };
3711
+ }
3712
+ function paletteToCssGradientColors(palette) {
3713
+ const colors = paletteToRendererColorMap(palette);
3714
+ if (hasTransparentLowerBound(palette)) {
3715
+ return ["rgba(0, 0, 0, 0)", ...colors];
3716
+ }
3717
+ return colors;
3718
+ }
3719
+ function hasTransparentLowerBound(palette) {
3720
+ if (typeof palette !== "string") {
3721
+ return false;
3722
+ }
3723
+ const normalized = palette.toLowerCase();
3724
+ return normalized === "magma" || normalized === "precipitation" || normalized === "vegetation";
3725
+ }
3726
+ function formatInspectSample(result, colorScale, formatter) {
3727
+ const sample = result.samples[0];
3728
+ if (!sample) {
3729
+ return "No data";
3730
+ }
3731
+ if (formatter) {
3732
+ return formatter(sample, result);
3733
+ }
3734
+ if (sample.value === null) {
3735
+ return "No data";
3736
+ }
3737
+ const display = inspectDisplayValue(sample.value, result.unit, colorScale);
3738
+ return `${formatNumber(display.value)}${display.unit ? ` ${display.unit}` : ""}`;
3739
+ }
3740
+ function inspectDisplayValue(value, sourceUnit, colorScale) {
3741
+ const fromUnit = colorScale?.unit ?? sourceUnit;
3742
+ const toUnit = colorScale?.displayUnit;
3743
+ if (fromUnit && toUnit && colorScale?.quantity) {
3744
+ return {
3745
+ value: convertDisplayValue(value, {
3746
+ fromUnit,
3747
+ quantity: colorScale.quantity,
3748
+ toUnit
3749
+ }),
3750
+ unit: toUnit
3751
+ };
3752
+ }
3753
+ return {
3754
+ value,
3755
+ unit: toUnit ?? sourceUnit
3756
+ };
3757
+ }
3758
+ function formatNumber(value) {
3759
+ if (value !== 0 && Math.abs(value) < 0.01) {
3760
+ return new Intl.NumberFormat("en", {
3761
+ maximumSignificantDigits: 2
3762
+ }).format(value);
3763
+ }
3764
+ return new Intl.NumberFormat("en", {
3765
+ maximumFractionDigits: Math.abs(value) < 10 ? 2 : 0
3766
+ }).format(value);
3767
+ }
3768
+ function formatCoordinatePair(coordinates) {
3769
+ return `${coordinates[1].toFixed(3)}, ${coordinates[0].toFixed(3)}`;
3770
+ }
3771
+ function timeIndexForSelector(source, dimensionName, selectors) {
3772
+ const selector = selectors[dimensionName];
3773
+ if (!selector) {
3774
+ return void 0;
3775
+ }
3776
+ const coordinates = listTimeCoordinates(source, dimensionName);
3777
+ if (typeof selector !== "object" || selector instanceof Date) {
3778
+ const value = selector instanceof Date ? selector.toISOString() : selector;
3779
+ const index = coordinates.findIndex(
3780
+ (coordinate) => coordinate.value === selector || coordinate.iso === value
3781
+ );
3782
+ return index === -1 ? void 0 : index;
3783
+ }
3784
+ if (selector.kind === "index") {
3785
+ return clampIndex(selector.index, Math.max(coordinates.length - 1, 0));
3786
+ }
3787
+ if (selector.kind === "isoTime") {
3788
+ const index = coordinates.findIndex((coordinate) => coordinate.iso === selector.iso);
3789
+ return index === -1 ? void 0 : index;
3790
+ }
3791
+ if (selector.kind === "coordinate") {
3792
+ const index = coordinates.findIndex((coordinate) => coordinate.value === selector.value);
3793
+ return index === -1 ? void 0 : index;
3794
+ }
3795
+ return void 0;
3796
+ }
3797
+ function extractLngLat(event) {
3798
+ if (typeof event !== "object" || event === null || !("lngLat" in event)) {
3799
+ return void 0;
3800
+ }
3801
+ const lngLat = event.lngLat;
3802
+ if (typeof lngLat !== "object" || lngLat === null) {
3803
+ return void 0;
3804
+ }
3805
+ if ("toArray" in lngLat && typeof lngLat.toArray === "function") {
3806
+ const value = lngLat.toArray();
3807
+ return Array.isArray(value) && typeof value[0] === "number" && typeof value[1] === "number" ? [value[0], value[1]] : void 0;
3808
+ }
3809
+ if ("lng" in lngLat && "lat" in lngLat && typeof lngLat.lng === "number" && typeof lngLat.lat === "number") {
3810
+ return [lngLat.lng, lngLat.lat];
3811
+ }
3812
+ return void 0;
3813
+ }
3814
+ function extractInspectTooltipPoint(event, container) {
3815
+ const point = extractMapPoint(event) ?? extractClientPoint(event, container);
3816
+ if (!point) {
3817
+ return void 0;
3818
+ }
3819
+ const width = container?.clientWidth ?? 0;
3820
+ const height = container?.clientHeight ?? 0;
3821
+ return {
3822
+ x: point[0],
3823
+ y: point[1],
3824
+ horizontal: width > 0 && point[0] > width - 240 ? "left" : "right",
3825
+ vertical: height > 0 && point[1] > height - 110 ? "above" : "below"
3826
+ };
3827
+ }
3828
+ function extractMapPoint(event) {
3829
+ if (!isRecord2(event) || !isRecord2(event.point)) {
3830
+ return void 0;
3831
+ }
3832
+ const { x, y } = event.point;
3833
+ return typeof x === "number" && typeof y === "number" ? [x, y] : void 0;
3834
+ }
3835
+ function extractClientPoint(event, container) {
3836
+ if (!container || !isRecord2(event)) {
3837
+ return void 0;
3838
+ }
3839
+ const originalEvent = event.originalEvent;
3840
+ if (!isRecord2(originalEvent)) {
3841
+ return void 0;
3842
+ }
3843
+ const { clientX, clientY } = originalEvent;
3844
+ if (typeof clientX !== "number" || typeof clientY !== "number") {
3845
+ return void 0;
3846
+ }
3847
+ const rect = container.getBoundingClientRect();
3848
+ return [clientX - rect.left, clientY - rect.top];
3849
+ }
3850
+ async function queryInspectPoint(controller, context, coordinates) {
3851
+ const selectors = context.selector ?? context.currentSelector;
3852
+ if (controller) {
3853
+ try {
3854
+ const rendered = await controller.query({ type: "Point", coordinates });
3855
+ const result = renderedPointQueryToGridResult(rendered, context, coordinates, selectors);
3856
+ if (result) {
3857
+ return result;
3858
+ }
3859
+ } catch (error) {
3860
+ if (!isGridError(error) || error.code !== "UNSUPPORTED_GEOMETRY") {
3861
+ throw error;
3862
+ }
3863
+ }
3864
+ }
3865
+ return queryPoint(context.source, context.variable, coordinates, selectors);
3866
+ }
3867
+ function renderedPointQueryToGridResult(rendered, context, clickedCoordinates, selectors) {
3868
+ if (!isRecord2(rendered)) {
3869
+ return void 0;
3870
+ }
3871
+ const variable = context.source.variables.find(
3872
+ (candidate) => candidate.name === context.variable || candidate.path === context.variable
3873
+ );
3874
+ const renderedValues = findRenderedVariableValues(rendered, [
3875
+ context.variable,
3876
+ variable?.name,
3877
+ variable?.path
3878
+ ]);
3879
+ if (renderedValues === void 0) {
3880
+ return void 0;
3881
+ }
3882
+ const value = firstFiniteNumber(renderedValues) ?? null;
3883
+ return {
3884
+ variable: context.variable,
3885
+ selectors: normalizeSelectors(context.source, context.variable, selectors),
3886
+ geometry: { type: "Point", coordinates: clickedCoordinates },
3887
+ unit: variable?.units,
3888
+ samples: [
3889
+ {
3890
+ coordinates: renderedPointCoordinates(rendered, clickedCoordinates),
3891
+ indexes: {},
3892
+ rawValue: value,
3893
+ value
3894
+ }
3895
+ ],
3896
+ warnings: []
3897
+ };
3898
+ }
3899
+ function findRenderedVariableValues(rendered, preferredKeys) {
3900
+ for (const key of preferredKeys) {
3901
+ if (key && key in rendered) {
3902
+ return rendered[key];
3903
+ }
3904
+ }
3905
+ return Object.entries(rendered).find(
3906
+ ([key]) => key !== "coordinates" && key !== "dimensions"
3907
+ )?.[1];
3908
+ }
3909
+ function renderedPointCoordinates(rendered, fallback) {
3910
+ const coordinates = rendered.coordinates;
3911
+ if (!isRecord2(coordinates)) {
3912
+ return fallback;
3913
+ }
3914
+ const longitude = firstFiniteNumber(coordinates.lon) ?? firstFiniteNumber(coordinates.lng) ?? fallback[0];
3915
+ const latitude = firstFiniteNumber(coordinates.lat) ?? fallback[1];
3916
+ return [longitude, latitude];
3917
+ }
3918
+ function firstFiniteNumber(value) {
3919
+ if (typeof value === "number") {
3920
+ return Number.isFinite(value) ? value : void 0;
3921
+ }
3922
+ if (Array.isArray(value)) {
3923
+ for (const item of value) {
3924
+ const number = firstFiniteNumber(item);
3925
+ if (number !== void 0) {
3926
+ return number;
3927
+ }
3928
+ }
3929
+ }
3930
+ if (ArrayBuffer.isView(value) && !(value instanceof DataView)) {
3931
+ const values = value;
3932
+ for (let index = 0; index < values.length; index += 1) {
3933
+ const number = firstFiniteNumber(values[index]);
3934
+ if (number !== void 0) {
3935
+ return number;
3936
+ }
3937
+ }
3938
+ }
3939
+ return void 0;
3940
+ }
3941
+ function isRecord2(value) {
3942
+ return typeof value === "object" && value !== null;
3943
+ }
3944
+
3945
+ exports.BASE_PLAYBACK_INTERVAL_MS = BASE_PLAYBACK_INTERVAL_MS;
3946
+ exports.DClimateZarrMap = DClimateZarrMap;
3947
+ exports.Legend = Legend;
3948
+ exports.PLAYBACK_SPEED_OPTIONS = PLAYBACK_SPEED_OPTIONS;
3949
+ exports.TimeSlider = TimeSlider;
3950
+ exports.ZarrMap = ZarrMap;
3951
+ exports.useMountEffect = useMountEffect;
3952
+ //# sourceMappingURL=index.cjs.map
3953
+ //# sourceMappingURL=index.cjs.map