@fxi/zartigl 0.5.0 → 0.7.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.
package/README.md CHANGED
@@ -1,11 +1,31 @@
1
1
  # zartigl
2
2
 
3
- A MapLibre GL JS plugin for exploring cloud-hosted Zarr geoscience data directly in the browser.
3
+ Cloud-native ocean and climate data on a MapLibre map: a browser runtime, a
4
+ curated catalog, and a pipeline that pre-renders long timelines as streamable
5
+ video.
4
6
 
5
7
  [![Demo](https://img.shields.io/badge/demo-live-blue)](https://fxi.io/zartigl/)
6
8
  [![Story](https://img.shields.io/badge/story-explore-8b5cf6)](https://fxi.io/zartigl/story/)
7
9
 
8
- Zartigl renders scalar rasters and vector particle fields from multidimensional Zarr stores without a dedicated tile server. It is aimed at expert analysis workflows where time, depth or pressure level, point inspection, and reproducible map state matter as much as the visual layer.
10
+ Zartigl started as a MapLibre plugin that renders scalar rasters and vector
11
+ particle fields straight from multidimensional Zarr stores, without a tile
12
+ server. It now also maintains the data side that makes those layers fast and
13
+ reproducible: a provider-neutral catalog of Copernicus Marine products, and
14
+ GeoVideo archives that turn years of daily or hourly fields into chunked H.264
15
+ videos played back continuously in the browser. It is aimed at expert
16
+ workflows where time, depth or pressure level, point inspection, and
17
+ reproducible map state matter as much as the visual layer, and it powers the
18
+ ARCO layers of [MapX](https://app.mapx.org).
19
+
20
+ ## What Is In This Repository
21
+
22
+ | Part | Location | Role |
23
+ | --- | --- | --- |
24
+ | Runtime library | `src/lib/` | `@fxi/zartigl`: the `Zartigl` facade, Zarr/WMTS/GeoVideo layers, shaders, particle simulation, point queries |
25
+ | Catalog | `src/catalog/`, `scripts/catalog_builder/` | `@fxi/zartigl/catalog` presets and the tools that discover, describe, and validate entries |
26
+ | GeoVideo pipeline | `scripts/geovideo/`, `.github/workflows/geovideo.yml` | Single renders and incremental archives published to S3, rendered daily on a self-hosted runner |
27
+ | Demos | `src/demo-prod/`, `src/demo-story/` | Technical demo and narrative story, deployed to GitHub Pages |
28
+ | MapX integration | `src/mapx/` | Widget and standalone snippet generation for reproducible MapX views |
9
29
 
10
30
  ## Features
11
31
 
@@ -16,7 +36,9 @@ Zartigl renders scalar rasters and vector particle fields from multidimensional
16
36
  - Point queries for time series and vertical profiles when a point-series store is available
17
37
  - UUID-addressed catalog entries with independently selectable Zarr, WMTS, and GeoVideo sources
18
38
  - MapLibre custom layer integration, including Mercator and globe rendering paths
19
- - Pre-rendered GeoVideo sources with true polar globe draping and portable masks
39
+ - Pre-rendered scalar and vector GeoVideo with true polar globe draping and portable masks
40
+ - GeoVideo archives split into calendar chunks, with the next chunk preloaded so
41
+ playback runs across the whole record without gaps
20
42
  - No server component required for public CORS-enabled stores
21
43
 
22
44
  ## Quick Start
@@ -48,8 +70,8 @@ const z = new Zartigl({
48
70
  timeRange: { trailing: "P1M" },
49
71
  time: new Date("2025-01-01T00:00:00Z"),
50
72
  depth: 0,
51
- settings: { palette: "rdylbu", opacity: 0.9 },
52
- geoVideo: { autoplay: false, loop: true, playbackRate: 1 },
73
+ style: { palette: "rdylbu", raster: { opacity: 0.9 } },
74
+ geoVideo: { autoplay: false, loop: true, stepsPerSecond: 4 },
53
75
  });
54
76
 
55
77
  z.on("status", (status) => console.log(status.phase));
@@ -59,10 +81,76 @@ await z.init();
59
81
  Use `await z.update({ ... })` to change any subset of the active configuration.
60
82
  Pass `layer` and `source` together when both should switch atomically.
61
83
 
84
+ `source` takes a source id or a preference:
85
+
86
+ - `"geovideo"`: fast; surface level and GeoVideo dates only.
87
+ - `"zarr"`: all dates and depths; heavier on the client.
88
+ - `"auto"`: GeoVideo, falling back to Zarr for other dates and depths; the
89
+ layer style always applies. WMTS only when the entry has neither.
90
+ - `"wmts"`: rendered by the server, palette and legend included; no
91
+ particles, and the layer style has no effect.
92
+
62
93
  `init()` and layer/source updates resolve after consolidated metadata and coordinate axes have loaded. Metadata getters are therefore authoritative afterward, including exact values for irregular time axes.
63
94
 
64
95
  The root import does not bundle the catalog presets. Import catalog data from `@fxi/zartigl/catalog` only when you want the built-in catalog.
65
96
 
97
+ ## Layer style
98
+
99
+ A layer style has the same shape everywhere: catalog `defaults`, the
100
+ `Zartigl` `style` option and `update({ style })`, story views, projversor,
101
+ the MapX `ArcoMapLegend` and the snippets the demo exports.
102
+
103
+ ```jsonc
104
+ {
105
+ "palette": "thermal",
106
+ "renderMode": "particles", // or "raster", "raster+particles"
107
+ "particles": {
108
+ "density": 0.03,
109
+ "speed": 1,
110
+ "fade": 0.7,
111
+ "grade": "vibrant",
112
+ "backdrop": { "enabled": true, "color": "#000000", "opacity": 0.3 }
113
+ },
114
+ "raster": { "opacity": 0.9, "logScale": false, "vibrance": 0.2, "colorDomain": null }
115
+ }
116
+ ```
117
+
118
+ `particles.grade` is `"off"`, `"vibrant"`, or a tuned vibrant grade
119
+ `{ exposure, contrast, saturation, highlights }`; it is replaced as a whole.
120
+
121
+ `particles.backdrop` paints a flat color under the particle trails, on the
122
+ valid cells of the field only (the ocean, not the land), so particles stay
123
+ readable over a light basemap. It is off by default; an object is on unless
124
+ `enabled` is false, and its keys left out take the defaults (`color:
125
+ "#000000"`, `#rgb` or `#rrggbb`; `opacity: 0.5`). Like the grade, it is
126
+ replaced as a whole.
127
+
128
+ Keys merge one by one: engine defaults (`LAYER_STYLE_DEFAULTS`), then the
129
+ catalog entry `defaults`, then the engine's own layer style, then the view.
130
+ From `@fxi/zartigl/catalog`: `resolveEntryStyle(entry, ...styles)` gives the
131
+ style a layer is drawn with, `mergeStyles` patches an override,
132
+ `diffStyle` keeps what differs from a resolved style, and `parseLayerStyle`
133
+ validates, with the path of the offending key. `z.getStyle()` returns the
134
+ style drawn, `z.getStyleOverride()` the override.
135
+
136
+ The view style is an override that replaces the previous one:
137
+ `update({ style })` and projversor `apply({ style })` return every style key
138
+ to the layer's own, then apply it (`{}` clears it). Without `style`, the
139
+ override stays, across layers too. Scalar layers take no `particles` and only
140
+ the `"raster"` render mode.
141
+
142
+ `particles.speed` is always the particle speed; playback speed lives
143
+ elsewhere (`geoVideo.stepsPerSecond`, or a projversor view `playback.speed`).
144
+ In projversor, playback also scales the particles by `particleSpeedFactor`:
145
+ 0.325 at 1×, 1 at 5×.
146
+
147
+ `settings` carries engine tuning only (`particleState`,
148
+ `rgba8MaxParticleZoom`). Flat style keys in it (`particleDensity`,
149
+ `opacity`, `colorGrade`…) are deprecated until 1.0: they patch the style
150
+ override, `style` winning, with one warning per key
151
+ (`splitLegacySettings`, `LEGACY_STYLE_SETTINGS`). A custom palette ramp
152
+ stays a setting.
153
+
66
154
  ## Demo
67
155
 
68
156
  The production demo is the default Vite app and is published at
@@ -114,7 +202,8 @@ z.on("status", (status) => {
114
202
  // GeoVideo uses native media playback and reports scientific time updates.
115
203
  z.on("timeChange", (time) => console.log(new Date(time)));
116
204
  z.on("playbackChange", (playing) => console.log({ playing }));
117
- await z.update({ geoVideo: { playbackRate: 2, loop: false } });
205
+ // Speed in time steps per second means the same on every artifact.
206
+ await z.update({ geoVideo: { stepsPerSecond: 16, loop: false } });
118
207
 
119
208
  // Embedding applications can stop rendering and abort field requests while
120
209
  // retaining the latest requested time/depth for one reload on resume.
@@ -138,6 +227,24 @@ finishes publishing it; a renderable sparse frame is cached normally.
138
227
 
139
228
  `VectorLayer`, `ScalarLayer`, `GeoVideoLayer`, and `ZarrSource` remain exported for advanced use cases that need direct control over renderer internals. Ordinary MapLibre integrations should prefer `Zartigl`.
140
229
 
230
+ ### Entries without MapLibre
231
+
232
+ Three entries need no MapLibre (`maplibre-gl` is an optional peer):
233
+
234
+ - `@fxi/zartigl/engine`: GeoVideo manifests, indexes, decoders and archive
235
+ timelines, palettes and color ramps, color grade, backdrop and trail fade,
236
+ WebGL helpers and the shared GLSL chunks as strings (`VIBRANCE_GLSL`,
237
+ `COLOR_GRADE_GLSL`, `RANDOM_GLSL`).
238
+ - `@fxi/zartigl/catalog`: the built-in catalog, named palettes and styles.
239
+ - `@fxi/zartigl/story`: the story runtime, with JSON documents, localized
240
+ copy and scroll activation.
241
+
242
+ [projversor](https://github.com/fxi/projversor) (`@fxi/projversor`), the
243
+ renderer of GeoVideo archives on a sphere that morphs between world
244
+ projections, is built on these entries in its own repository, with its demos.
245
+ zartigl never imports it. It replaces the former `@fxi/zartigl/projversor`
246
+ entry: the same names are exported by `@fxi/projversor/zartigl`.
247
+
141
248
  ## Alternatives And Project Fit
142
249
 
143
250
  Zartigl overlaps with several useful projects. The goal is not to replace them, but to cover a specific cloud-native geoscience workflow: public Zarr stores and compatible WMTS/GeoVideo sources, MapLibre integration, animated vector particles, scalar rasters, time/depth controls, point inspection, and reproducible widget configuration without a dedicated tile server.
@@ -235,17 +342,53 @@ const z = new Zartigl({
235
342
  geoVideo: {
236
343
  autoplay: true,
237
344
  loop: true,
238
- playbackRate: 1,
345
+ stepsPerSecond: 8,
239
346
  },
240
347
  });
241
348
 
242
349
  await z.init();
243
350
  ```
244
351
 
245
- The source UUID selects the entry's published GeoVideo manifest. Globe projection demonstrates its polar
246
- coverage. `loop` and `playbackRate` are optional playback settings. Browsers
247
- may restrict autoplay, so embedding applications should provide a user-gesture
248
- fallback when necessary.
352
+ The source UUID selects the entry's published GeoVideo. Globe projection
353
+ demonstrates its polar coverage. `loop` and `stepsPerSecond` are optional
354
+ playback settings. Browsers may restrict autoplay, so embedding applications
355
+ should provide a user-gesture fallback when necessary.
356
+
357
+ ### Archives
358
+
359
+ Most catalog entries carry a GeoVideo archive source: an `indexUrl` pointing to
360
+ an `index.json` that lists immutable calendar chunks (monthly, yearly, or per
361
+ decade, depending on cadence). zartigl reads the index at runtime, revalidates
362
+ it every few minutes, and expands it into one source per chunk, so newly
363
+ published chunks appear without a catalog release. Times outside the published
364
+ chunks, deeper levels, and point queries fall back to Zarr.
365
+
366
+ During playback the next chunk's manifest, media, and mask load while the
367
+ current one plays; at its end the layer swaps media in place and keeps playing,
368
+ so a whole archive plays as one continuous timeline.
369
+
370
+ Archives are produced by `scripts/geovideo/archive.py` from the policies in
371
+ [`scripts/geovideo/archive.json`](scripts/geovideo/archive.json). Every product
372
+ is archived over its full upstream time axis; only event scenes, such as the
373
+ regional Cyclone Chido archive, keep a fixed window. The
374
+ [`GeoVideo archive`](.github/workflows/geovideo.yml) workflow renders pending
375
+ chunks daily, and on demand with a longer budget, on the self-hosted `bigproc`
376
+ runner. Published chunks are never dropped from an index, so an archive keeps
377
+ periods that upstream later removes.
378
+
379
+ Follow progress against the published indexes. The command reads the upstream
380
+ time axes and the indexes with the S3 credentials in `.env`, writes nothing,
381
+ and can run while the workflow renders:
382
+
383
+ ```bash
384
+ npm run geovideo:progress
385
+ # 80/500 Mediterranean Significant Wave Height
386
+ # Total 971/5347 chunks (18.2%); 32/61 archives complete
387
+ ```
388
+
389
+ `npm run geovideo:plan` prints the same plan as JSON, chunk by chunk.
390
+
391
+ ### Single Renders
249
392
 
250
393
  ```bash
251
394
  uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json --dry-run
@@ -253,7 +396,9 @@ uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json
253
396
  uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json --upload
254
397
  ```
255
398
 
256
- The Arctic sea-ice configuration covers June 2022 through August 2026 and is
399
+ `render.py` (scalar) and `render_vector.py` (vector) produce one standalone
400
+ artifact from a configuration in `scripts/geovideo/examples/`, such as the
401
+ hourly Cyclone Chido wind scene used by the story. The Arctic sea-ice example is
257
402
  available at
258
403
  [`scripts/geovideo/examples/sea-ice-thickness-arctic.json`](scripts/geovideo/examples/sea-ice-thickness-arctic.json).
259
404
 
@@ -320,6 +465,7 @@ npm test
320
465
  npm run build:prod
321
466
  npm run build:lib
322
467
  npm run catalog:validate
468
+ npm run test:geovideo
323
469
  ```
324
470
 
325
471
  `npm run release:check` runs the main validation path before publishing.
@@ -0,0 +1,79 @@
1
+ const i = {
2
+ exposure: 0.8,
3
+ contrast: 0.6,
4
+ saturation: 0.4,
5
+ highlights: 0.6
6
+ }, l = Object.freeze({
7
+ enabled: !1,
8
+ ...i
9
+ }), s = Object.freeze({
10
+ enabled: !0,
11
+ ...i
12
+ }), u = {
13
+ exposure: [0, 4],
14
+ contrast: [0, 1],
15
+ saturation: [-1, 1],
16
+ highlights: [0, 1]
17
+ }, a = u;
18
+ function f(e) {
19
+ if (!e)
20
+ return { ...l };
21
+ const o = {
22
+ ...s,
23
+ enabled: e.enabled ?? !0
24
+ };
25
+ for (const n of Object.keys(a)) {
26
+ const t = e[n];
27
+ if (typeof t == "number" && Number.isFinite(t)) {
28
+ const [r, b] = a[n];
29
+ o[n] = Math.min(b, Math.max(r, t));
30
+ }
31
+ }
32
+ return o;
33
+ }
34
+ function h(e) {
35
+ return [e.exposure, e.contrast, e.saturation, e.highlights];
36
+ }
37
+ function m(e) {
38
+ return e === void 0 || e === "off" ? { ...l } : e === "vibrant" ? { ...s } : f({ ...e, enabled: !0 });
39
+ }
40
+ function y(e) {
41
+ const o = f(e);
42
+ if (!o.enabled)
43
+ return "off";
44
+ const n = Object.keys(a);
45
+ return n.every((t) => o[t] === s[t]) ? "vibrant" : Object.fromEntries(n.map((t) => [t, o[t]]));
46
+ }
47
+ const c = Object.freeze({
48
+ enabled: !1,
49
+ color: "#000000",
50
+ opacity: 0.5
51
+ }), d = /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i, O = (e) => typeof e == "string" && d.test(e);
52
+ function R(e) {
53
+ if (!e)
54
+ return { ...c };
55
+ const o = { ...c, enabled: !0 };
56
+ return typeof e.enabled == "boolean" && (o.enabled = e.enabled), O(e.color) && (o.color = e.color), typeof e.opacity == "number" && Number.isFinite(e.opacity) && (o.opacity = Math.min(1, Math.max(0, e.opacity))), o;
57
+ }
58
+ function p(e) {
59
+ const o = e.color.slice(1), n = o.length === 3 ? o.split("").map((r) => r + r).join("") : o, t = (r) => parseInt(n.slice(r, r + 2), 16) / 255;
60
+ return [
61
+ t(0),
62
+ t(2),
63
+ t(4),
64
+ e.enabled ? e.opacity : 0
65
+ ];
66
+ }
67
+ export {
68
+ c as B,
69
+ l as C,
70
+ R as a,
71
+ p as b,
72
+ h as c,
73
+ y as d,
74
+ s as e,
75
+ u as f,
76
+ m as g,
77
+ O as i,
78
+ f as r
79
+ };
package/dist/catalog.d.ts CHANGED
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Solid color under the particle trails, drawn on valid data cells only: it
3
+ * keeps particles readable over a light basemap. Off by default; an object
4
+ * without `enabled` is on.
5
+ */
6
+ export declare interface BackdropStyle {
7
+ enabled: boolean;
8
+ /** Hex color, `#rgb` or `#rrggbb`. */
9
+ color: string;
10
+ opacity: number;
11
+ }
12
+
1
13
  export declare interface Catalog {
2
14
  schemaVersion: 2;
3
15
  defaultLocale: string;
@@ -15,22 +27,10 @@ export declare interface CatalogEntry {
15
27
  tags?: string[];
16
28
  kind: "scalar" | "vector";
17
29
  sources: CatalogSource[];
18
- defaults: {
30
+ /** Default source, plus the layer's base style. */
31
+ defaults: LayerStyleInput & {
19
32
  sourceId: string;
20
33
  querySourceId?: string;
21
- palette?: string;
22
- renderMode?: "particles" | "raster" | "raster+particles";
23
- particles?: {
24
- density?: number;
25
- speed?: number;
26
- fade?: number;
27
- };
28
- raster?: {
29
- opacity?: number;
30
- logScale?: boolean;
31
- vibrance?: number;
32
- colorDomain?: [number, number];
33
- };
34
34
  };
35
35
  }
36
36
 
@@ -39,9 +39,21 @@ export declare interface CatalogEntrySelector extends CatalogSearchOptions {
39
39
  variableId?: string;
40
40
  }
41
41
 
42
+ /**
43
+ * An incremental archive of calendar-aligned artifacts listed by a mutable
44
+ * index; zartigl resolves it to the chunk covering the requested time.
45
+ */
46
+ declare interface CatalogGeoVideoArchiveSource extends CatalogSourceBase {
47
+ type: "geovideo";
48
+ indexUrl: string;
49
+ manifestUrl?: never;
50
+ }
51
+
52
+ /** One published artifact. */
42
53
  export declare interface CatalogGeoVideoSource extends CatalogSourceBase {
43
54
  type: "geovideo";
44
55
  manifestUrl: string;
56
+ indexUrl?: never;
45
57
  }
46
58
 
47
59
  export declare interface CatalogSearchOptions {
@@ -51,7 +63,7 @@ export declare interface CatalogSearchOptions {
51
63
  provider?: string;
52
64
  }
53
65
 
54
- export declare type CatalogSource = CatalogZarrSource | CatalogWmtsSource | CatalogGeoVideoSource;
66
+ export declare type CatalogSource = CatalogZarrSource | CatalogWmtsSource | CatalogGeoVideoSource | CatalogGeoVideoArchiveSource;
55
67
 
56
68
  declare interface CatalogSourceBase {
57
69
  id: string;
@@ -114,6 +126,9 @@ export declare interface CatalogZarrSource extends CatalogSourceBase {
114
126
  variables: CatalogVariables;
115
127
  }
116
128
 
129
+ /** The keys of `style` whose value differs from `base`'s. */
130
+ export declare function diffStyle(style: LayerStyleInput, base: LayerStyle): LayerStyleInput;
131
+
117
132
  export declare function findCatalogEntries(selector: CatalogEntrySelector, data?: Catalog): CatalogEntry[];
118
133
 
119
134
  export declare function formatTime(ms: number): string;
@@ -122,14 +137,158 @@ export declare function formatVertical(v: number, label: string, units?: string)
122
137
 
123
138
  export declare function getCatalogEntry(id: string, data?: Catalog): CatalogEntry | undefined;
124
139
 
140
+ /** Vibrant grade values; each one left out takes the vibrant preset's. */
141
+ export declare interface GradeTuning {
142
+ /** Mid-tone lift [0, 4]. */
143
+ exposure?: number;
144
+ /** S-curve strength [0, 1]. */
145
+ contrast?: number;
146
+ /** Vibrance-weighted saturation [-1, 1]. */
147
+ saturation?: number;
148
+ /** Push of the bright half toward white [0, 1]. */
149
+ highlights?: number;
150
+ }
151
+
152
+ /** Engine fallbacks, under the catalog `defaults`. */
153
+ export declare const LAYER_STYLE_DEFAULTS: Readonly<LayerStyle>;
154
+
155
+ export declare interface LayerStyle {
156
+ /** Palette id; undefined lets the engine pick its own. */
157
+ palette?: string;
158
+ renderMode: StyleRenderMode;
159
+ particles: ParticleStyle;
160
+ raster: RasterStyle;
161
+ }
162
+
163
+ /** A partial style: any key left out takes the value below it. */
164
+ export declare interface LayerStyleInput {
165
+ palette?: string;
166
+ renderMode?: StyleRenderMode;
167
+ particles?: ParticleStyleInput;
168
+ raster?: Partial<RasterStyle>;
169
+ }
170
+
125
171
  export declare type LocalizedText = Record<string, string>;
126
172
 
127
- export declare function pickPreferredSource(entry: CatalogEntry): CatalogSource;
173
+ /**
174
+ * One partial style from several, later inputs winning key by key: patch an
175
+ * override without resolving defaults.
176
+ */
177
+ export declare function mergeStyles(...inputs: (LayerStyleInput | undefined)[]): LayerStyleInput;
178
+
179
+ /**
180
+ * Validate a style, throwing an error that names the offending path, e.g.
181
+ * `views.swell.config.style.particles.density must be a number in (0, 0.2]`.
182
+ */
183
+ export declare function parseLayerStyle(value: unknown, path: string, options?: ParseLayerStyleOptions): LayerStyleInput;
184
+
185
+ /** Which keys a caller supports, and for which layer kind. */
186
+ export declare interface ParseLayerStyleOptions {
187
+ kind?: "scalar" | "vector";
188
+ /** Keys left out are rejected as unsupported; all when omitted. */
189
+ supports?: {
190
+ palette?: boolean;
191
+ renderMode?: boolean;
192
+ particles?: readonly (keyof ParticleStyle)[];
193
+ raster?: readonly (keyof RasterStyle)[];
194
+ };
195
+ }
196
+
197
+ /** Style keys, for checks against other definitions of the same shape. */
198
+ export declare const PARTICLE_STYLE_KEYS: (keyof ParticleStyle)[];
199
+
200
+ /** Particle trail color grade: a preset, or the vibrant one tuned. */
201
+ export declare type ParticleGrade = "off" | "vibrant" | GradeTuning;
202
+
203
+ export declare interface ParticleStyle {
204
+ /** Particles per device pixel, (0, 0.2]. */
205
+ density: number;
206
+ /** Css px per frame for the fastest value of the field, (0, 10]. */
207
+ speed: number;
208
+ /** Trail length, [0, 1] (longer when higher). */
209
+ fade: number;
210
+ /** Trail color grade; replaced as a whole, not merged. */
211
+ grade: ParticleGrade;
212
+ /** Solid color under the trails; replaced as a whole, not merged. */
213
+ backdrop: BackdropStyle;
214
+ }
215
+
216
+ /** Partial particle style; a partial backdrop takes the defaults' keys. */
217
+ export declare type ParticleStyleInput = Partial<Omit<ParticleStyle, "backdrop">> & {
218
+ backdrop?: Partial<BackdropStyle>;
219
+ };
220
+
221
+ /**
222
+ * Preferred render source: GeoVideo, then Zarr, then WMTS (scalar), skipping
223
+ * fixed-period sources that cannot show the requested time. Several GeoVideo
224
+ * periods may coexist; the latest covering one wins. GeoVideo is a
225
+ * lightweight display transport; Zarr stays the fallback and query source.
226
+ */
227
+ export declare function pickPreferredSource(entry: CatalogEntry, request?: SourceTimeRequest): CatalogSource;
228
+
229
+ /**
230
+ * First source type in `priority` that the entry provides, preferring sources
231
+ * covering the request, then the latest period. Undefined when no type matches.
232
+ */
233
+ export declare function pickSourceByPriority(entry: CatalogEntry, priority: readonly CatalogSource["type"][], request?: SourceTimeRequest): CatalogSource | undefined;
234
+
235
+ export declare const RASTER_STYLE_KEYS: (keyof RasterStyle)[];
236
+
237
+ export declare interface RasterStyle {
238
+ opacity: number;
239
+ logScale: boolean;
240
+ /** Vibrance adjustment, [-1, 1]. */
241
+ vibrance: number;
242
+ /** Fixed physical-value domain; null follows the data. */
243
+ colorDomain: [number, number] | null;
244
+ }
128
245
 
129
246
  export declare function requireCatalogEntry(id: string, data?: Catalog): CatalogEntry;
130
247
 
248
+ /**
249
+ * The style a catalog layer is drawn with: its own style, then `inputs`.
250
+ * Scalar layers draw a raster.
251
+ */
252
+ export declare function resolveEntryStyle(entry: StyledEntry | undefined, ...inputs: (LayerStyleInput | undefined)[]): LayerStyle;
253
+
254
+ /**
255
+ * Merge styles over {@link LAYER_STYLE_DEFAULTS}, later inputs winning key by
256
+ * key. Usual order: catalog `defaults`, engine style, view.
257
+ */
258
+ export declare function resolveLayerStyle(...inputs: (LayerStyleInput | undefined)[]): LayerStyle;
259
+
131
260
  export declare function resolveLocalizedText(value: LocalizedText | undefined, locale: string, defaultLocale?: string): string;
132
261
 
133
262
  export declare function searchCatalog(query: string, options?: CatalogSearchOptions, data?: Catalog): CatalogEntry[];
134
263
 
264
+ /**
265
+ * Whether a source can show the requested time. Only fixed-period sources
266
+ * (rendered artifacts such as GeoVideo) declare hard bounds; live sources are
267
+ * assumed to cover any request and are bounded by their own metadata.
268
+ */
269
+ export declare function sourceCoversTime(source: CatalogSource, request?: SourceTimeRequest): boolean;
270
+
271
+ /** Requested instant and/or window, in epoch milliseconds. */
272
+ export declare interface SourceTimeRequest {
273
+ time?: number;
274
+ start?: number;
275
+ end?: number;
276
+ }
277
+
278
+ /** A catalog entry, or anything with its kind and default style. */
279
+ export declare interface StyledEntry {
280
+ kind: "scalar" | "vector";
281
+ defaults?: LayerStyleInput;
282
+ }
283
+
284
+ /** `style` without the keys a layer of `kind` cannot draw. */
285
+ export declare function styleForKind(style: LayerStyleInput, kind: StyledEntry["kind"] | undefined): LayerStyleInput;
286
+
287
+ /**
288
+ * Layer style: how a layer looks, with the same keys in the catalog
289
+ * `defaults`, the engines and story views. Playback (how time moves) is not
290
+ * part of it, so `particles.speed` is always the particle speed.
291
+ */
292
+ export declare type StyleRenderMode = "particles" | "raster" | "raster+particles";
293
+
135
294
  export { }