@fxi/zartigl 0.4.2 → 0.6.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
@@ -49,7 +71,7 @@ const z = new Zartigl({
49
71
  time: new Date("2025-01-01T00:00:00Z"),
50
72
  depth: 0,
51
73
  settings: { palette: "rdylbu", opacity: 0.9 },
52
- geoVideo: { autoplay: false, loop: true, playbackRate: 1 },
74
+ geoVideo: { autoplay: false, loop: true, stepsPerSecond: 4 },
53
75
  });
54
76
 
55
77
  z.on("status", (status) => console.log(status.phase));
@@ -114,7 +136,8 @@ z.on("status", (status) => {
114
136
  // GeoVideo uses native media playback and reports scientific time updates.
115
137
  z.on("timeChange", (time) => console.log(new Date(time)));
116
138
  z.on("playbackChange", (playing) => console.log({ playing }));
117
- await z.update({ geoVideo: { playbackRate: 2, loop: false } });
139
+ // Speed in time steps per second means the same on every artifact.
140
+ await z.update({ geoVideo: { stepsPerSecond: 16, loop: false } });
118
141
 
119
142
  // Embedding applications can stop rendering and abort field requests while
120
143
  // retaining the latest requested time/depth for one reload on resume.
@@ -138,6 +161,50 @@ finishes publishing it; a renderable sparse frame is cached normally.
138
161
 
139
162
  `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
163
 
164
+ ### Projversor and story entries
165
+
166
+ Two entries need no MapLibre (`maplibre-gl` is an optional peer):
167
+
168
+ - `@fxi/zartigl/projversor`: GeoVideo archives on a WebGL2 sphere that morphs
169
+ between world projections.
170
+ - `@fxi/zartigl/story`: the story runtime, with JSON documents, localized
171
+ copy and scroll activation.
172
+
173
+ ```ts
174
+ import { Projversor, ProjversorStoryView } from "@fxi/zartigl/projversor";
175
+ import {
176
+ StoryScroller,
177
+ parseStoryDocuments,
178
+ sceneViewId,
179
+ } from "@fxi/zartigl/story";
180
+
181
+ // Three stacked canvases in a positioned stage. Attach them before
182
+ // constructing: the overlay reads its colors (--ocean, --land, --coast,
183
+ // --graticule…) from the front canvas computed style. Throws without WebGL2.
184
+ const projversor = new Projversor({ stage, back, gl, front });
185
+ projversor.setPlaying(true);
186
+
187
+ // One view per scene: layer, preset, rotate, zoom, palette, tint, time…
188
+ const { story, views } = parseStoryDocuments(storyJson, viewsJson);
189
+ const adapter = new ProjversorStoryView(projversor);
190
+
191
+ // The section crossing 60% of the viewport height activates its view.
192
+ const scroller = new StoryScroller(sections, (index) => {
193
+ const id = sceneViewId(story.scenes[index]);
194
+ const view = views.views.find((v) => v.id === id);
195
+ if (view) {
196
+ void adapter.activate(view);
197
+ }
198
+ }, { line: 0.6 });
199
+
200
+ // Later: scroller.destroy(); adapter.destroy();
201
+ ```
202
+
203
+ `parseProjversorView` validates a view against the projversor layers and
204
+ presets. `goTo`, `jumpTo` and `setInset` move the camera, keeping a screen area
205
+ free for text. The finer coastline loads in a separate chunk after the first
206
+ paint. `src/demo-site/` is a complete scroll story page built on both entries.
207
+
141
208
  ## Alternatives And Project Fit
142
209
 
143
210
  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 +302,53 @@ const z = new Zartigl({
235
302
  geoVideo: {
236
303
  autoplay: true,
237
304
  loop: true,
238
- playbackRate: 1,
305
+ stepsPerSecond: 8,
239
306
  },
240
307
  });
241
308
 
242
309
  await z.init();
243
310
  ```
244
311
 
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.
312
+ The source UUID selects the entry's published GeoVideo. Globe projection
313
+ demonstrates its polar coverage. `loop` and `stepsPerSecond` are optional
314
+ playback settings. Browsers may restrict autoplay, so embedding applications
315
+ should provide a user-gesture fallback when necessary.
316
+
317
+ ### Archives
318
+
319
+ Most catalog entries carry a GeoVideo archive source: an `indexUrl` pointing to
320
+ an `index.json` that lists immutable calendar chunks (monthly, yearly, or per
321
+ decade, depending on cadence). zartigl reads the index at runtime, revalidates
322
+ it every few minutes, and expands it into one source per chunk, so newly
323
+ published chunks appear without a catalog release. Times outside the published
324
+ chunks, deeper levels, and point queries fall back to Zarr.
325
+
326
+ During playback the next chunk's manifest, media, and mask load while the
327
+ current one plays; at its end the layer swaps media in place and keeps playing,
328
+ so a whole archive plays as one continuous timeline.
329
+
330
+ Archives are produced by `scripts/geovideo/archive.py` from the policies in
331
+ [`scripts/geovideo/archive.json`](scripts/geovideo/archive.json). Every product
332
+ is archived over its full upstream time axis; only event scenes, such as the
333
+ regional Cyclone Chido archive, keep a fixed window. The
334
+ [`GeoVideo archive`](.github/workflows/geovideo.yml) workflow renders pending
335
+ chunks daily, and on demand with a longer budget, on the self-hosted `bigproc`
336
+ runner. Published chunks are never dropped from an index, so an archive keeps
337
+ periods that upstream later removes.
338
+
339
+ Follow progress against the published indexes. The command reads the upstream
340
+ time axes and the indexes with the S3 credentials in `.env`, writes nothing,
341
+ and can run while the workflow renders:
342
+
343
+ ```bash
344
+ npm run geovideo:progress
345
+ # 80/500 Mediterranean Significant Wave Height
346
+ # Total 971/5347 chunks (18.2%); 32/61 archives complete
347
+ ```
348
+
349
+ `npm run geovideo:plan` prints the same plan as JSON, chunk by chunk.
350
+
351
+ ### Single Renders
249
352
 
250
353
  ```bash
251
354
  uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json --dry-run
@@ -253,7 +356,9 @@ uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json
253
356
  uv run scripts/geovideo/render.py scripts/geovideo/examples/sst-anomaly.json --upload
254
357
  ```
255
358
 
256
- The Arctic sea-ice configuration covers June 2022 through August 2026 and is
359
+ `render.py` (scalar) and `render_vector.py` (vector) produce one standalone
360
+ artifact from a configuration in `scripts/geovideo/examples/`, such as the
361
+ hourly Cyclone Chido wind scene used by the story. The Arctic sea-ice example is
257
362
  available at
258
363
  [`scripts/geovideo/examples/sea-ice-thickness-arctic.json`](scripts/geovideo/examples/sea-ice-thickness-arctic.json).
259
364
 
@@ -320,6 +425,7 @@ npm test
320
425
  npm run build:prod
321
426
  npm run build:lib
322
427
  npm run catalog:validate
428
+ npm run test:geovideo
323
429
  ```
324
430
 
325
431
  `npm run release:check` runs the main validation path before publishing.
@@ -0,0 +1,38 @@
1
+ import { G as s } from "./geovideo-index-CD5wflJT.js";
2
+ function u(o, e) {
3
+ const a = Math.max(
4
+ 0,
5
+ Math.min(
6
+ 1,
7
+ (o - e.codeMin) / (e.codeMax - e.codeMin)
8
+ )
9
+ );
10
+ return e.valueMin + a * (e.valueMax - e.valueMin);
11
+ }
12
+ function d(o, e, a, c) {
13
+ const t = e.length;
14
+ if (o.length !== t)
15
+ throw new Error(
16
+ `GeoVideo scalar frame has ${o.length} codes, expected ${t}`
17
+ );
18
+ const n = new Float32Array(256);
19
+ for (let r = 0; r < 256; r++)
20
+ n[r] = u(r, c);
21
+ const i = a * 255, l = new Float32Array(t);
22
+ for (let r = 0; r < t; r++)
23
+ l[r] = e[r] >= i ? n[o[r]] : NaN;
24
+ return l;
25
+ }
26
+ class m extends s {
27
+ constructor(e, a = {}) {
28
+ if (e.encoding.kind !== "scalar-luma")
29
+ throw new Error("GeoVideoScalarSource requires a scalar-luma manifest");
30
+ super(e, 1, a), this.encoding = e.encoding;
31
+ }
32
+ decodeCodes(e, a, c) {
33
+ return d(e, a, c, this.encoding);
34
+ }
35
+ }
36
+ export {
37
+ m as G
38
+ };
package/dist/catalog.d.ts CHANGED
@@ -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;
@@ -124,7 +136,19 @@ export declare function getCatalogEntry(id: string, data?: Catalog): CatalogEntr
124
136
 
125
137
  export declare type LocalizedText = Record<string, string>;
126
138
 
127
- export declare function pickPreferredSource(entry: CatalogEntry): CatalogSource;
139
+ /**
140
+ * Preferred render source: GeoVideo, then WMTS (scalar), then Zarr, skipping
141
+ * fixed-period sources that cannot show the requested time. Several GeoVideo
142
+ * periods may coexist; the latest covering one wins. GeoVideo is a
143
+ * lightweight display transport; Zarr stays the fallback and query source.
144
+ */
145
+ export declare function pickPreferredSource(entry: CatalogEntry, request?: SourceTimeRequest): CatalogSource;
146
+
147
+ /**
148
+ * First source type in `priority` that the entry provides, preferring sources
149
+ * covering the request, then the latest period. Undefined when no type matches.
150
+ */
151
+ export declare function pickSourceByPriority(entry: CatalogEntry, priority: readonly CatalogSource["type"][], request?: SourceTimeRequest): CatalogSource | undefined;
128
152
 
129
153
  export declare function requireCatalogEntry(id: string, data?: Catalog): CatalogEntry;
130
154
 
@@ -132,4 +156,18 @@ export declare function resolveLocalizedText(value: LocalizedText | undefined, l
132
156
 
133
157
  export declare function searchCatalog(query: string, options?: CatalogSearchOptions, data?: Catalog): CatalogEntry[];
134
158
 
159
+ /**
160
+ * Whether a source can show the requested time. Only fixed-period sources
161
+ * (rendered artifacts such as GeoVideo) declare hard bounds; live sources are
162
+ * assumed to cover any request and are bounded by their own metadata.
163
+ */
164
+ export declare function sourceCoversTime(source: CatalogSource, request?: SourceTimeRequest): boolean;
165
+
166
+ /** Requested instant and/or window, in epoch milliseconds. */
167
+ export declare interface SourceTimeRequest {
168
+ time?: number;
169
+ start?: number;
170
+ end?: number;
171
+ }
172
+
135
173
  export { }