@fxi/zartigl 0.5.0 → 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 +117 -11
- package/dist/GeoVideoScalarSource-DoQAnyTy.js +38 -0
- package/dist/catalog.d.ts +40 -2
- package/dist/catalog.js +111 -62
- package/dist/engine.d.ts +616 -0
- package/dist/engine.js +29 -0
- package/dist/geovideo-index-CD5wflJT.js +833 -0
- package/dist/land-50m-BX3pQS4x.js +15 -0
- package/dist/projversor.d.ts +919 -0
- package/dist/projversor.js +2374 -0
- package/dist/scalar-color-domain-PEK8Hxg1.js +12 -0
- package/dist/story.d.ts +398 -0
- package/dist/story.js +561 -0
- package/dist/zartigl.d.ts +356 -34
- package/dist/zartigl.js +3606 -2637
- package/package.json +49 -7
package/README.md
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
# zartigl
|
|
2
2
|
|
|
3
|
-
|
|
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
|
[](https://fxi.io/zartigl/)
|
|
6
8
|
[](https://fxi.io/zartigl/story/)
|
|
7
9
|
|
|
8
|
-
Zartigl
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
246
|
-
coverage. `loop` and `
|
|
247
|
-
may restrict autoplay, so embedding applications
|
|
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
|
-
|
|
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
|
-
|
|
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 { }
|