@ortho-earth/voxel 0.4.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 +14 -4
- package/dist/grid.d.ts +36 -7
- package/dist/grid.js +111 -29
- package/dist/index.d.ts +24 -6
- package/dist/index.js +40 -20
- package/dist/sources.d.ts +91 -0
- package/dist/sources.js +514 -0
- package/dist/voxel-gl.d.ts +21 -5
- package/dist/voxel-gl.js +148 -40
- package/dist/voxel-gpu.js +128 -27
- package/dist/voxel-reader.d.ts +13 -3
- package/dist/voxel-reader.js +10 -7
- package/package.json +2 -2
package/dist/sources.js
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
const lc = (v) => typeof v === "string" ? v.trim().toLowerCase() : "";
|
|
2
|
+
const num1 = (v) => { const x = Array.isArray(v) || ArrayBuffer.isView(v) ? v[0] : v; return typeof x === "number" && Number.isFinite(x) ? x : typeof x === "bigint" ? Number(x) : null; };
|
|
3
|
+
const strAttr = (a, k) => typeof a[k] === "string" ? a[k] : null;
|
|
4
|
+
/** 座標の変数が何の軸か(CF:axis・units・standard_name・名前) */
|
|
5
|
+
export function axisOf(name, attrs) {
|
|
6
|
+
const ax = lc(attrs.axis), u = lc(attrs.units), sn = lc(attrs.standard_name), n = name.toLowerCase();
|
|
7
|
+
if (ax === "x" || sn === "longitude" || /^degrees?_?e(ast)?$/.test(u) || /^(lon|long|longitude|x_lon|nav_lon)$/.test(n))
|
|
8
|
+
return "x";
|
|
9
|
+
if (ax === "y" || sn === "latitude" || /^degrees?_?n(orth)?$/.test(u) || /^(lat|latitude|y_lat|nav_lat)$/.test(n))
|
|
10
|
+
return "y";
|
|
11
|
+
if (ax === "t" || sn === "time" || /\bsince\b/.test(u) || /^(time|t)$/.test(n))
|
|
12
|
+
return "t";
|
|
13
|
+
if (ax === "z" || /^(height|altitude|depth|air_pressure|model_level_number)$/.test(sn) || /^(level|lev|plev|isobaric|pressure|height|alt|altitude|depth|z|bottom_top|pressure_level)$/.test(n) || lc(attrs.positive) === "up" || lc(attrs.positive) === "down")
|
|
14
|
+
return "z";
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
// 表は Map(ファイルの字を鍵にする=__proto__ などで Object の性質を引かない)
|
|
18
|
+
const UNIT_MS = new Map(Object.entries({ ms: 1, millisecond: 1, milliseconds: 1, s: 1e3, sec: 1e3, secs: 1e3, second: 1e3, seconds: 1e3, min: 6e4, mins: 6e4, minute: 6e4, minutes: 6e4, h: 3.6e6, hr: 3.6e6, hrs: 3.6e6, hour: 3.6e6, hours: 3.6e6, d: 8.64e7, day: 8.64e7, days: 8.64e7 }));
|
|
19
|
+
/** CF の時刻("hours since 1900-01-01 00:00:0.0" など)→ ms。読めなければ null */
|
|
20
|
+
export function cfTimes(values, units) {
|
|
21
|
+
// 正規表現の繰り返しを避けて切る(ファイルの字は長さも中身も不定=CodeQL の多項式 regex を出さない)
|
|
22
|
+
const u = (units || "").trim(), at = u.toLowerCase().search(/\ssince\s/);
|
|
23
|
+
if (at < 0)
|
|
24
|
+
return null;
|
|
25
|
+
const k = UNIT_MS.get(u.slice(0, at).trim().toLowerCase());
|
|
26
|
+
if (!k)
|
|
27
|
+
return null;
|
|
28
|
+
let s = u.slice(at + 7).trim();
|
|
29
|
+
if (s.toUpperCase().endsWith("UTC"))
|
|
30
|
+
s = s.slice(0, -3).trimEnd();
|
|
31
|
+
if (s.toUpperCase().endsWith("Z"))
|
|
32
|
+
s = s.slice(0, -1).trimEnd();
|
|
33
|
+
const d = /^(-?\d{1,4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{1,2})(?::(\d{1,2}(?:\.\d*)?))?)?\s*([+-]\d{1,2}(?::?\d{2})?)?$/.exec(s);
|
|
34
|
+
if (!d)
|
|
35
|
+
return null;
|
|
36
|
+
let base = Date.UTC(+d[1], +d[2] - 1, +d[3], +(d[4] || 0), +(d[5] || 0), 0) + Math.round(parseFloat(d[6] || "0") * 1000);
|
|
37
|
+
if (+d[1] < 100) {
|
|
38
|
+
const t = new Date(base);
|
|
39
|
+
t.setUTCFullYear(+d[1]);
|
|
40
|
+
base = t.getTime();
|
|
41
|
+
}
|
|
42
|
+
if (d[7]) {
|
|
43
|
+
const z = /^([+-])(\d{1,2}):?(\d{2})?$/.exec(d[7]);
|
|
44
|
+
base -= (z[1] === "-" ? -1 : 1) * (+z[2] * 60 + +(z[3] || 0)) * 6e4;
|
|
45
|
+
}
|
|
46
|
+
return Array.from(values, v => base + v * k);
|
|
47
|
+
}
|
|
48
|
+
/** 気圧(Pa)→ 高さ(m・標準大気の対流圏の式) */
|
|
49
|
+
export const pressureToHeight = (pa) => 44330.8 * (1 - Math.pow(Math.max(1, pa) / 101325, 0.190263));
|
|
50
|
+
/** 高さの座標 → m(units と positive で) */
|
|
51
|
+
export function heightsOf(values, attrs, name = "") {
|
|
52
|
+
const u = lc(attrs.units), down = lc(attrs.positive) === "down" || /depth/i.test(name) || lc(attrs.standard_name) === "depth";
|
|
53
|
+
let f = (v) => v;
|
|
54
|
+
if (/^(pa|pascal|pascals)$/.test(u))
|
|
55
|
+
f = pressureToHeight;
|
|
56
|
+
else if (/^(hpa|mbar|millibar|millibars|mb)$/.test(u))
|
|
57
|
+
f = v => pressureToHeight(v * 100);
|
|
58
|
+
else if (/^(km|kilometer|kilometers|kilometre|kilometres)$/.test(u))
|
|
59
|
+
f = v => v * 1000;
|
|
60
|
+
return Array.from(values, v => (down ? -1 : 1) * f(v));
|
|
61
|
+
}
|
|
62
|
+
/** セルの中心の並び → 範囲(端のセルの半分だけ外へ)と昇順か。一つだけなら step で幅 */
|
|
63
|
+
export function edges(c, step1 = 1) {
|
|
64
|
+
const n = c.length, a = c[0], b = c[n - 1], asc = !(n > 1 && b < a), d = n > 1 ? Math.abs(b - a) / (n - 1) : step1;
|
|
65
|
+
const lo = Math.min(a, b) - d / 2, hi = Math.max(a, b) + d / 2;
|
|
66
|
+
return { lo, hi, asc };
|
|
67
|
+
}
|
|
68
|
+
/** 選んだ変数(座標でない・数値・次元 2〜4・x と y がある)を探す */
|
|
69
|
+
export function pickVariable(r, want) {
|
|
70
|
+
const coordNames = new Set(r.vars.filter(v => v.dims.length === 1 && v.dims[0] === v.name).map(v => v.name));
|
|
71
|
+
if (want != null) {
|
|
72
|
+
const v = r.vars.find(x => x.name === want);
|
|
73
|
+
if (!v)
|
|
74
|
+
throw new Error(`voxel: variable "${want}" not found (${r.vars.map(x => x.name).join(", ")})`);
|
|
75
|
+
return v;
|
|
76
|
+
}
|
|
77
|
+
const cands = r.vars.filter(v => v.numeric && !coordNames.has(v.name) && v.dims.length >= 2 && v.dims.length <= 4).sort((a, b) => b.dims.length - a.dims.length || b.shape.reduce((x, y) => x * y, 1) - a.shape.reduce((x, y) => x * y, 1));
|
|
78
|
+
if (!cands.length)
|
|
79
|
+
throw new Error(`voxel: no 2D–4D numeric variable to draw (${r.vars.map(x => x.name).join(", ")})`);
|
|
80
|
+
return cands[0];
|
|
81
|
+
}
|
|
82
|
+
/** 読み手 → VoxelData(region・times) */
|
|
83
|
+
export async function toVoxelData(r, o = {}) {
|
|
84
|
+
const v = pickVariable(r, o.variable);
|
|
85
|
+
if (v.dims.length < 2 || v.dims.length > 4)
|
|
86
|
+
throw new Error(`voxel: variable "${v.name}" has ${v.dims.length} dimensions (2–4: [time,] [z,] y, x)`);
|
|
87
|
+
const info = (d) => r.vars.find(x => x.name === d && x.dims.length === 1);
|
|
88
|
+
const kinds = v.dims.map(d => { const c = info(d); return c ? axisOf(d, c.attrs) : axisOf(d, {}); });
|
|
89
|
+
const nd = v.dims.length, ix = nd - 1, iy = nd - 2;
|
|
90
|
+
let it = -1, iz = -1;
|
|
91
|
+
if (nd === 4) {
|
|
92
|
+
it = 0;
|
|
93
|
+
iz = 1;
|
|
94
|
+
}
|
|
95
|
+
else if (nd === 3) {
|
|
96
|
+
if (kinds[0] === "t")
|
|
97
|
+
it = 0;
|
|
98
|
+
else
|
|
99
|
+
iz = 0;
|
|
100
|
+
}
|
|
101
|
+
if (kinds[ix] === "y" && kinds[iy] === "x")
|
|
102
|
+
throw new Error(`voxel: "${v.name}" is ordered (…, x, y); only (…, y, x) is read`);
|
|
103
|
+
const nx = v.shape[ix], ny = v.shape[iy], nz = iz >= 0 ? v.shape[iz] : 1, ntAll = it >= 0 ? v.shape[it] : 1;
|
|
104
|
+
const [f0, f1] = it >= 0 && o.frames ? [Math.max(0, Math.floor(o.frames[0])), Math.min(ntAll, Math.floor(o.frames[1]))] : [0, ntAll];
|
|
105
|
+
if (!(f1 > f0))
|
|
106
|
+
throw new RangeError(`voxel: frames [${o.frames}] select no frame of ${ntAll}`);
|
|
107
|
+
const crd = async (i) => { const c = info(v.dims[i]); return c ? { vals: await r.coord(c.name), attrs: c.attrs } : null; };
|
|
108
|
+
const [cx, cy, cz, ct] = await Promise.all([crd(ix), crd(iy), iz >= 0 ? crd(iz) : null, it >= 0 ? crd(it) : null]);
|
|
109
|
+
if (!cx || !cy)
|
|
110
|
+
throw new Error(`voxel: "${v.name}" needs longitude / latitude coordinates (${v.dims[ix]}, ${v.dims[iy]})`);
|
|
111
|
+
const ex = edges(cx.vals), ey = edges(cy.vals);
|
|
112
|
+
let h0 = 0, h1 = 1000, zAsc = true;
|
|
113
|
+
if (o.heights)
|
|
114
|
+
[h0, h1] = o.heights;
|
|
115
|
+
else if (cz) {
|
|
116
|
+
const hz = heightsOf(cz.vals, cz.attrs, v.dims[iz]), e = edges(hz, 1000);
|
|
117
|
+
h0 = e.lo;
|
|
118
|
+
h1 = e.hi;
|
|
119
|
+
zAsc = e.asc;
|
|
120
|
+
}
|
|
121
|
+
if (o.heights && cz)
|
|
122
|
+
zAsc = edges(heightsOf(cz.vals, cz.attrs, v.dims[iz])).asc;
|
|
123
|
+
if (!(h1 > h0))
|
|
124
|
+
throw new RangeError(`voxel: heights must be [h0, h1] with h0 < h1 (got ${h0}, ${h1})`);
|
|
125
|
+
let times;
|
|
126
|
+
if (it >= 0 && f1 - f0 > 1) {
|
|
127
|
+
const t = ct ? cfTimes(ct.vals.subarray(f0, f1), strAttr(ct.attrs, "units")) : null;
|
|
128
|
+
times = t && t.every((x, i) => i === 0 || x > t[i - 1]) ? t : Array.from({ length: f1 - f0 }, (_, i) => i * 3.6e6); // 読めない時刻=1 時間おき(時計で進む)
|
|
129
|
+
}
|
|
130
|
+
const raw = await r.read(v.name, it >= 0 ? [f0, f1] : null);
|
|
131
|
+
const T = f1 - f0, n = nx * ny * nz;
|
|
132
|
+
if (raw.length !== n * T)
|
|
133
|
+
throw new Error(`voxel: "${v.name}" read ${raw.length} values, expected ${n * T}`);
|
|
134
|
+
// 裏返す(緯度・高さ・経度が降順)=x が東・y が北・z が上へ増える
|
|
135
|
+
const flipX = !ex.asc, flipY = !ey.asc, flipZ = !zAsc;
|
|
136
|
+
let values = raw;
|
|
137
|
+
if (flipX || flipY || flipZ) {
|
|
138
|
+
values = new Float32Array(raw.length);
|
|
139
|
+
for (let t = 0; t < T; t++)
|
|
140
|
+
for (let k = 0; k < nz; k++)
|
|
141
|
+
for (let j = 0; j < ny; j++) {
|
|
142
|
+
const src = t * n + nx * ((flipY ? ny - 1 - j : j) + ny * (flipZ ? nz - 1 - k : k)), dst = t * n + nx * (j + ny * k);
|
|
143
|
+
if (!flipX)
|
|
144
|
+
values.set(raw.subarray(src, src + nx), dst);
|
|
145
|
+
else
|
|
146
|
+
for (let i = 0; i < nx; i++)
|
|
147
|
+
values[dst + i] = raw[src + nx - 1 - i];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
let w = ex.lo, e = ex.hi;
|
|
151
|
+
if (e - w >= 360) {
|
|
152
|
+
w = -180 + ((w + 180) % 360 + 360) % 360;
|
|
153
|
+
e = w + 360 - 1e-9;
|
|
154
|
+
} // 全球=360° ちょうどは region が幅 0 と読むので僅かに縮める
|
|
155
|
+
const s = Math.max(-90, ey.lo), nn = Math.min(90, ey.hi);
|
|
156
|
+
const units = strAttr(v.attrs, "units"), longName = strAttr(v.attrs, "long_name") ?? strAttr(v.attrs, "standard_name");
|
|
157
|
+
const out = { shape: "region", dims: [nx, ny, nz], region: [w, s, e, nn, h0, h1], values, meta: { format: r.format, variable: v.name, units, longName, dims: v.dims.slice(), attrs: v.attrs } };
|
|
158
|
+
if (times)
|
|
159
|
+
out.times = times;
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
/** 値の整え(scale_factor/add_offset・fill・valid range → NaN) */
|
|
163
|
+
export function unpack(src, attrs, fill = null) {
|
|
164
|
+
const sc = num1(attrs.scale_factor) ?? 1, of = num1(attrs.add_offset) ?? 0;
|
|
165
|
+
const fv = num1(attrs._FillValue) ?? fill, mv = num1(attrs.missing_value);
|
|
166
|
+
const vr = Array.isArray(attrs.valid_range) || ArrayBuffer.isView(attrs.valid_range) ? attrs.valid_range : null;
|
|
167
|
+
const vmin = vr ? Number(vr[0]) : num1(attrs.valid_min), vmax = vr ? Number(vr[1]) : num1(attrs.valid_max);
|
|
168
|
+
const out = new Float32Array(src.length);
|
|
169
|
+
for (let i = 0; i < src.length; i++) {
|
|
170
|
+
const x = Number(src[i]);
|
|
171
|
+
if (x !== x || x === fv || x === mv || (vmin != null && x < vmin) || (vmax != null && x > vmax) || Math.abs(x) >= 9.96e36) {
|
|
172
|
+
out[i] = NaN;
|
|
173
|
+
continue;
|
|
174
|
+
} // 9.97e36=NetCDF の既定の fill(float)
|
|
175
|
+
out[i] = x * sc + of;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
// ---- NetCDF classic(CDF-1/2/5・big endian)----
|
|
180
|
+
const NC_DIM = 10, NC_VAR = 11, NC_ATT = 12;
|
|
181
|
+
const NC_SIZE = new Map([[1, 1], [2, 1], [3, 2], [4, 4], [5, 4], [6, 8], [7, 1], [8, 2], [9, 4], [10, 8], [11, 8]]);
|
|
182
|
+
const NC_FILL = new Map([[1, -127], [3, -32767], [4, -2147483647], [5, 9.969209968386869e36], [6, 9.969209968386869e36], [7, 255], [8, 65535], [9, 4294967295]]);
|
|
183
|
+
/** NetCDF の形式("classic"=読める・"hdf5"=NetCDF-4・null=NetCDF でない) */
|
|
184
|
+
export function sniffNetCDF(b) {
|
|
185
|
+
if (b.length >= 4 && b[0] === 0x43 && b[1] === 0x44 && b[2] === 0x46 && (b[3] === 1 || b[3] === 2 || b[3] === 5))
|
|
186
|
+
return "classic";
|
|
187
|
+
if (b.length >= 8 && b[0] === 0x89 && b[1] === 0x48 && b[2] === 0x44 && b[3] === 0x46)
|
|
188
|
+
return "hdf5";
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
export function parseNetCDF(buf) {
|
|
192
|
+
const u8 = buf instanceof Uint8Array ? buf : new Uint8Array(buf), dv = new DataView(u8.buffer, u8.byteOffset, u8.byteLength);
|
|
193
|
+
const kind = sniffNetCDF(u8);
|
|
194
|
+
if (kind === "hdf5")
|
|
195
|
+
throw new Error("voxel: NetCDF-4 (HDF5) is not read yet; save as NetCDF classic (e.g. `nccopy -k classic in.nc out.nc`) or as Zarr");
|
|
196
|
+
if (kind !== "classic")
|
|
197
|
+
throw new Error("voxel: not a NetCDF file (no CDF magic)");
|
|
198
|
+
const ver = u8[3], big = ver === 5;
|
|
199
|
+
let p = 4;
|
|
200
|
+
const u32 = () => { const v = dv.getUint32(p); p += 4; return v; };
|
|
201
|
+
const size = () => { if (big) {
|
|
202
|
+
const v = Number(dv.getBigUint64(p));
|
|
203
|
+
p += 8;
|
|
204
|
+
return v;
|
|
205
|
+
} return u32(); };
|
|
206
|
+
const off = () => { if (ver === 1)
|
|
207
|
+
return u32(); const v = Number(dv.getBigUint64(p)); p += 8; return v; };
|
|
208
|
+
const name = () => { const n = size(), s = new TextDecoder().decode(u8.subarray(p, p + n)); p += (n + 3) & ~3; return s; };
|
|
209
|
+
const vals = (type, n) => {
|
|
210
|
+
const sz = NC_SIZE.get(type);
|
|
211
|
+
if (!sz)
|
|
212
|
+
throw new Error(`voxel: NetCDF type ${type} unknown`);
|
|
213
|
+
const start = p;
|
|
214
|
+
p += (n * sz + 3) & ~3;
|
|
215
|
+
if (type === 2) {
|
|
216
|
+
let e = start + n;
|
|
217
|
+
while (e > start && u8[e - 1] === 0)
|
|
218
|
+
e--;
|
|
219
|
+
return new TextDecoder().decode(u8.subarray(start, e));
|
|
220
|
+
}
|
|
221
|
+
return Array.from({ length: n }, (_, i) => readNc(dv, type, start + i * sz));
|
|
222
|
+
};
|
|
223
|
+
const atts = () => {
|
|
224
|
+
const tag = u32(), n = size(), out = {};
|
|
225
|
+
if (tag !== NC_ATT && !(tag === 0 && n === 0))
|
|
226
|
+
throw new Error("voxel: NetCDF header broken (attributes)");
|
|
227
|
+
for (let i = 0; i < n; i++) {
|
|
228
|
+
const k = name(), type = u32(), m = size();
|
|
229
|
+
const v = vals(type, m);
|
|
230
|
+
out[k] = typeof v === "string" ? v : v.length === 1 ? v[0] : v;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
};
|
|
234
|
+
const numrecs = size();
|
|
235
|
+
let tag = u32(), nd = size();
|
|
236
|
+
if (tag !== NC_DIM && !(tag === 0 && nd === 0))
|
|
237
|
+
throw new Error("voxel: NetCDF header broken (dimensions)");
|
|
238
|
+
const dims = [];
|
|
239
|
+
for (let i = 0; i < nd; i++) {
|
|
240
|
+
const n = name();
|
|
241
|
+
dims.push({ name: n, len: size() });
|
|
242
|
+
}
|
|
243
|
+
const recDim = dims.findIndex(d => d.len === 0);
|
|
244
|
+
const gatts = atts();
|
|
245
|
+
tag = u32();
|
|
246
|
+
const nv = size();
|
|
247
|
+
if (tag !== NC_VAR && !(tag === 0 && nv === 0))
|
|
248
|
+
throw new Error("voxel: NetCDF header broken (variables)");
|
|
249
|
+
const vars = [];
|
|
250
|
+
for (let i = 0; i < nv; i++) {
|
|
251
|
+
const nm = name(), ndi = size(), dimIds = Array.from({ length: ndi }, () => size()), attrs = atts(), type = u32(), vsize = size(), begin = off();
|
|
252
|
+
const rec = ndi > 0 && dimIds[0] === recDim;
|
|
253
|
+
vars.push({ name: nm, dims: dimIds.map(d => dims[d].name), shape: dimIds.map(d => d === recDim ? numrecs : dims[d].len), attrs, numeric: type !== 2, type, begin, vsize, rec, dimIds });
|
|
254
|
+
}
|
|
255
|
+
const recSize = vars.filter(v => v.rec).reduce((a, v) => a + v.vsize, 0);
|
|
256
|
+
const recVars = vars.filter(v => v.rec).length;
|
|
257
|
+
const raw = (v, first) => {
|
|
258
|
+
const sz = NC_SIZE.get(v.type) ?? 1, inner = v.shape.slice(v.rec ? 1 : 0).reduce((a, b) => a * b, 1);
|
|
259
|
+
if (!v.rec) {
|
|
260
|
+
const out = new Float64Array(inner);
|
|
261
|
+
for (let i = 0; i < inner; i++)
|
|
262
|
+
out[i] = readNc(dv, v.type, v.begin + i * sz);
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
const [r0, r1] = first ?? [0, numrecs], out = new Float64Array(inner * (r1 - r0));
|
|
266
|
+
// 一つだけの記録変数は詰めて並ぶ(vsize は 4 の倍数に揃えない=NetCDF の決まり)
|
|
267
|
+
const stride = recVars === 1 ? inner * sz : recSize;
|
|
268
|
+
for (let r = r0; r < r1; r++)
|
|
269
|
+
for (let i = 0; i < inner; i++)
|
|
270
|
+
out[(r - r0) * inner + i] = readNc(dv, v.type, v.begin + r * stride + i * sz);
|
|
271
|
+
return out;
|
|
272
|
+
};
|
|
273
|
+
void gatts;
|
|
274
|
+
return {
|
|
275
|
+
format: "netcdf", vars,
|
|
276
|
+
async read(n, first) {
|
|
277
|
+
const v = vars.find(x => x.name === n);
|
|
278
|
+
if (first && !v.rec) { // 時間が記録の次元でない=全体を読んで先頭の軸を切る
|
|
279
|
+
const all = raw(v, null), inner = v.shape.slice(1).reduce((a, b) => a * b, 1);
|
|
280
|
+
return unpack(all.subarray(first[0] * inner, first[1] * inner), v.attrs, NC_FILL.get(v.type) ?? null);
|
|
281
|
+
}
|
|
282
|
+
return unpack(raw(v, first), v.attrs, NC_FILL.get(v.type) ?? null);
|
|
283
|
+
},
|
|
284
|
+
async coord(n) { const v = vars.find(x => x.name === n); return Float64Array.from(raw(v, null)); },
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function readNc(dv, type, o) {
|
|
288
|
+
switch (type) {
|
|
289
|
+
case 1: return dv.getInt8(o);
|
|
290
|
+
case 2: return dv.getUint8(o);
|
|
291
|
+
case 3: return dv.getInt16(o);
|
|
292
|
+
case 4: return dv.getInt32(o);
|
|
293
|
+
case 5: return dv.getFloat32(o);
|
|
294
|
+
case 6: return dv.getFloat64(o);
|
|
295
|
+
case 7: return dv.getUint8(o);
|
|
296
|
+
case 8: return dv.getUint16(o);
|
|
297
|
+
case 9: return dv.getUint32(o);
|
|
298
|
+
case 10: return Number(dv.getBigInt64(o));
|
|
299
|
+
case 11: return Number(dv.getBigUint64(o));
|
|
300
|
+
default: throw new Error(`voxel: NetCDF type ${type} unknown`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const ZDT = new Map(Object.entries({ b1: [1, "u1"], i1: [1, "i1"], u1: [1, "u1"], i2: [2, "i2"], u2: [2, "u2"], i4: [4, "i4"], u4: [4, "u4"], i8: [8, "i8"], u8: [8, "u8"], f4: [4, "f4"], f8: [8, "f8"] }));
|
|
304
|
+
const V3DT = new Map(Object.entries({ bool: "|b1", int8: "|i1", uint8: "|u1", int16: "<i2", uint16: "<u2", int32: "<i4", uint32: "<u4", int64: "<i8", uint64: "<u8", float32: "<f4", float64: "<f8" }));
|
|
305
|
+
const fillNum = (f) => typeof f === "number" ? f : f === "NaN" ? NaN : typeof f === "string" && /^-?Infinity$/.test(f) ? (f.startsWith("-") ? -Infinity : Infinity) : null;
|
|
306
|
+
/** Zarr のメタ(v2 の .zarray+.zattrs、v3 の zarr.json)→ 読み方 */
|
|
307
|
+
export function zarrArray(meta, attrs = {}, v3 = false) {
|
|
308
|
+
if (!v3) {
|
|
309
|
+
if (meta.order && meta.order !== "C")
|
|
310
|
+
throw new Error("voxel: Zarr arrays in Fortran order are not read");
|
|
311
|
+
if (Array.isArray(meta.filters) && meta.filters.length)
|
|
312
|
+
throw new Error(`voxel: Zarr filters (${meta.filters.map(f => f.id).join(", ")}) are not read`);
|
|
313
|
+
const c = meta.compressor;
|
|
314
|
+
const codec = !c ? "none" : c.id === "gzip" ? "gzip" : c.id === "zlib" ? "zlib" : null;
|
|
315
|
+
if (!codec)
|
|
316
|
+
throw new Error(`voxel: Zarr compressor "${String(c?.id)}" is not read yet (use gzip / zlib or none; e.g. xarray encoding={"var": {"compressor": numcodecs.GZip()}})`);
|
|
317
|
+
const sep = meta.dimension_separator || ".";
|
|
318
|
+
return { shape: meta.shape, chunks: meta.chunks, dtype: String(meta.dtype), fill: fillNum(meta.fill_value), codec, sep, v3, key: cc => cc.join(sep), attrs, dims: attrs._ARRAY_DIMENSIONS ?? [] };
|
|
319
|
+
}
|
|
320
|
+
const grid = meta.chunk_grid;
|
|
321
|
+
if (grid?.name !== "regular")
|
|
322
|
+
throw new Error("voxel: Zarr v3 needs a regular chunk grid");
|
|
323
|
+
const dt = V3DT.get(String(meta.data_type));
|
|
324
|
+
if (!dt)
|
|
325
|
+
throw new Error(`voxel: Zarr data type "${String(meta.data_type)}" is not read`);
|
|
326
|
+
let endian = "<", codec = "none";
|
|
327
|
+
for (const c of meta.codecs ?? []) {
|
|
328
|
+
const nm = c.name, cf = c.configuration ?? {};
|
|
329
|
+
if (nm === "bytes")
|
|
330
|
+
endian = cf.endian === "big" ? ">" : "<";
|
|
331
|
+
else if (nm === "gzip")
|
|
332
|
+
codec = "gzip";
|
|
333
|
+
else if (nm === "crc32c")
|
|
334
|
+
continue;
|
|
335
|
+
else
|
|
336
|
+
throw new Error(`voxel: Zarr codec "${String(nm)}" is not read yet (use bytes + gzip; sharding, zstd and blosc follow)`);
|
|
337
|
+
}
|
|
338
|
+
const ke = meta.chunk_key_encoding;
|
|
339
|
+
const sep = ke?.configuration?.separator ?? (ke?.name === "v2" ? "." : "/");
|
|
340
|
+
const key = ke?.name === "v2" ? (cc) => cc.join(sep) : (cc) => ["c", ...cc].join(sep);
|
|
341
|
+
return { shape: meta.shape, chunks: grid.configuration.chunk_shape, dtype: endian + dt.slice(1), fill: fillNum(meta.fill_value), codec, sep, v3, key, attrs: meta.attributes ?? {}, dims: meta.dimension_names ?? meta.attributes?._ARRAY_DIMENSIONS ?? [] };
|
|
342
|
+
}
|
|
343
|
+
async function inflate(b, codec) {
|
|
344
|
+
if (codec === "none")
|
|
345
|
+
return b;
|
|
346
|
+
const CS = globalThis.DecompressionStream;
|
|
347
|
+
if (!CS)
|
|
348
|
+
throw new Error("voxel: DecompressionStream is not available (gzip / zlib Zarr chunks)");
|
|
349
|
+
const s = new Blob([b]).stream().pipeThrough(new CS(codec === "gzip" ? "gzip" : "deflate"));
|
|
350
|
+
return new Uint8Array(await new Response(s).arrayBuffer());
|
|
351
|
+
}
|
|
352
|
+
function decodeChunk(b, dtype) {
|
|
353
|
+
const le = dtype[0] !== ">", t = ZDT.get(dtype.slice(1));
|
|
354
|
+
if (!t)
|
|
355
|
+
throw new Error(`voxel: Zarr dtype "${dtype}" is not read`);
|
|
356
|
+
const [sz, k] = t, n = Math.floor(b.length / sz), dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
357
|
+
if (k === "f4" && le && b.byteOffset % 4 === 0)
|
|
358
|
+
return new Float32Array(b.buffer, b.byteOffset, n);
|
|
359
|
+
const out = new Float64Array(n);
|
|
360
|
+
for (let i = 0; i < n; i++) {
|
|
361
|
+
const o = i * sz;
|
|
362
|
+
out[i] = k === "f4" ? dv.getFloat32(o, le) : k === "f8" ? dv.getFloat64(o, le) : k === "i1" ? dv.getInt8(o) : k === "u1" ? dv.getUint8(o) : k === "i2" ? dv.getInt16(o, le) : k === "u2" ? dv.getUint16(o, le)
|
|
363
|
+
: k === "i4" ? dv.getInt32(o, le) : k === "u4" ? dv.getUint32(o, le) : k === "i8" ? Number(dv.getBigInt64(o, le)) : Number(dv.getBigUint64(o, le));
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
/** Zarr の配列を読む(先頭の軸を [a, b) に絞れる・無いチャンクは fill) */
|
|
368
|
+
export async function readZarr(get, path, z, first) {
|
|
369
|
+
const nd = z.shape.length, lo = z.shape.map(() => 0), hi = z.shape.slice();
|
|
370
|
+
if (first && nd) {
|
|
371
|
+
lo[0] = first[0];
|
|
372
|
+
hi[0] = first[1];
|
|
373
|
+
}
|
|
374
|
+
const outShape = hi.map((h, i) => h - lo[i]), total = outShape.reduce((a, b) => a * b, 1), out = new Float64Array(total).fill(z.fill ?? NaN);
|
|
375
|
+
const c0 = lo.map((l, i) => Math.floor(l / z.chunks[i])), c1 = hi.map((h, i) => Math.ceil(h / z.chunks[i]));
|
|
376
|
+
const ids = [];
|
|
377
|
+
const walk = (d, acc) => { if (d === nd) {
|
|
378
|
+
ids.push(acc.slice());
|
|
379
|
+
return;
|
|
380
|
+
} for (let c = c0[d]; c < c1[d]; c++) {
|
|
381
|
+
acc[d] = c;
|
|
382
|
+
walk(d + 1, acc);
|
|
383
|
+
} };
|
|
384
|
+
walk(0, new Array(nd));
|
|
385
|
+
const oStride = outShape.map((_, i) => outShape.slice(i + 1).reduce((a, b) => a * b, 1)), cStride = z.chunks.map((_, i) => z.chunks.slice(i + 1).reduce((a, b) => a * b, 1));
|
|
386
|
+
const pre = path ? `${path.replace(/\/$/, "")}/` : "";
|
|
387
|
+
await Promise.all(ids.map(async (cc) => {
|
|
388
|
+
const raw = await get(pre + z.key(cc));
|
|
389
|
+
if (!raw)
|
|
390
|
+
return;
|
|
391
|
+
const vals = decodeChunk(await inflate(raw, z.codec), z.dtype);
|
|
392
|
+
// チャンクの中の範囲(格子の中・絞った範囲の中)を最後の軸で行ごとに写す
|
|
393
|
+
const s = cc.map((c, i) => Math.max(lo[i], c * z.chunks[i])), e = cc.map((c, i) => Math.min(hi[i], (c + 1) * z.chunks[i]));
|
|
394
|
+
const idx = s.slice();
|
|
395
|
+
const last = nd - 1, run = e[last] - s[last];
|
|
396
|
+
for (;;) {
|
|
397
|
+
let co = 0, oo = 0;
|
|
398
|
+
for (let i = 0; i < nd; i++) {
|
|
399
|
+
co += (idx[i] - cc[i] * z.chunks[i]) * cStride[i];
|
|
400
|
+
oo += (idx[i] - lo[i]) * oStride[i];
|
|
401
|
+
}
|
|
402
|
+
for (let r = 0; r < run; r++)
|
|
403
|
+
out[oo + r] = vals[co + r];
|
|
404
|
+
let d = last - 1;
|
|
405
|
+
while (d >= 0) {
|
|
406
|
+
idx[d]++;
|
|
407
|
+
if (idx[d] < e[d])
|
|
408
|
+
break;
|
|
409
|
+
idx[d] = s[d];
|
|
410
|
+
d--;
|
|
411
|
+
}
|
|
412
|
+
if (d < 0)
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}));
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
/** Zarr の店(URL の根)→ 読み手。consolidated(.zmetadata/zarr.json の consolidated_metadata)があれば一覧、無ければ variable と座標を名前で取る */
|
|
419
|
+
export async function openZarr(root, o = {}) {
|
|
420
|
+
const f = o.fetch ?? ((u) => fetch(u));
|
|
421
|
+
let base = root;
|
|
422
|
+
while (base.endsWith("/"))
|
|
423
|
+
base = base.slice(0, -1);
|
|
424
|
+
const get = async (p) => { const r = await f(`${base}/${p}`); if (r.status === 404 || r.status === 403)
|
|
425
|
+
return null; if (!r.ok)
|
|
426
|
+
throw new Error(`voxel: Zarr ${p}: HTTP ${r.status}`); return new Uint8Array(await r.arrayBuffer()); };
|
|
427
|
+
const json = async (p) => { const b = await get(p); return b ? JSON.parse(new TextDecoder().decode(b)) : null; };
|
|
428
|
+
const arrays = new Map();
|
|
429
|
+
const v3root = await json("zarr.json");
|
|
430
|
+
let v3 = false;
|
|
431
|
+
if (v3root && v3root.zarr_format === 3) {
|
|
432
|
+
v3 = true;
|
|
433
|
+
const cm = v3root.consolidated_metadata?.metadata;
|
|
434
|
+
if (cm)
|
|
435
|
+
for (const [k, m] of Object.entries(cm))
|
|
436
|
+
if (m.node_type === "array")
|
|
437
|
+
arrays.set(k, zarrArray(m, {}, true));
|
|
438
|
+
if (v3root.node_type === "array")
|
|
439
|
+
arrays.set("", zarrArray(v3root, {}, true));
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
const zm = await json(".zmetadata");
|
|
443
|
+
const md = zm?.metadata;
|
|
444
|
+
if (md)
|
|
445
|
+
for (const [k, m] of Object.entries(md))
|
|
446
|
+
if (k.endsWith("/.zarray")) {
|
|
447
|
+
const n = k.slice(0, -8);
|
|
448
|
+
arrays.set(n, zarrArray(m, md[`${n}/.zattrs`] ?? {}));
|
|
449
|
+
}
|
|
450
|
+
else if (k === ".zarray")
|
|
451
|
+
arrays.set("", zarrArray(m, md[".zattrs"] ?? {}));
|
|
452
|
+
}
|
|
453
|
+
const load = async (n) => {
|
|
454
|
+
if (arrays.has(n))
|
|
455
|
+
return arrays.get(n);
|
|
456
|
+
const p = n ? `${n}/` : "";
|
|
457
|
+
let z = null;
|
|
458
|
+
if (v3) {
|
|
459
|
+
const m = await json(`${p}zarr.json`);
|
|
460
|
+
if (m?.node_type === "array")
|
|
461
|
+
z = zarrArray(m, {}, true);
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
const m = await json(`${p}.zarray`);
|
|
465
|
+
if (m)
|
|
466
|
+
z = zarrArray(m, (await json(`${p}.zattrs`)) ?? {});
|
|
467
|
+
}
|
|
468
|
+
if (z)
|
|
469
|
+
arrays.set(n, z);
|
|
470
|
+
return z;
|
|
471
|
+
};
|
|
472
|
+
if (!arrays.size) { // 一覧が無い=variable を名前で(無ければ根が配列か)
|
|
473
|
+
const z = await load(o.variable ?? "");
|
|
474
|
+
if (!z)
|
|
475
|
+
throw new Error(`voxel: Zarr store ${root} has no consolidated metadata; pass { variable } (or consolidate it: zarr.consolidate_metadata)`);
|
|
476
|
+
}
|
|
477
|
+
if (o.variable != null)
|
|
478
|
+
await load(o.variable);
|
|
479
|
+
const named = () => [...arrays].map(([k, z]) => ({ name: k || o.variable || "data", dims: z.dims.length ? z.dims : z.shape.map((_, i) => `dim${i}`), shape: z.shape, attrs: z.attrs, numeric: true }));
|
|
480
|
+
// 座標の配列(次元の名前と同じ名前)=一覧に無ければ取りに行く
|
|
481
|
+
for (const v of named())
|
|
482
|
+
for (const d of v.dims)
|
|
483
|
+
if (!arrays.has(d))
|
|
484
|
+
await load(d).catch(() => null);
|
|
485
|
+
const vars = named();
|
|
486
|
+
const pathOf = (n) => arrays.has(n) ? n : "";
|
|
487
|
+
return {
|
|
488
|
+
format: "zarr", vars,
|
|
489
|
+
async read(n, first) { const z = arrays.get(pathOf(n)); return unpack(await readZarr(get, pathOf(n), z, first), z.attrs, z.fill); },
|
|
490
|
+
async coord(n) { const z = arrays.get(n); return Float64Array.from(await readZarr(get, n, z)); },
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
// ---- 入口 ----
|
|
494
|
+
/** URL の種類(.zarr=Zarr の店・それ以外の .nc 等=一つのファイル) */
|
|
495
|
+
export const isZarrUrl = (u) => /\.zarr(\/|\?|#|$)/i.test(u) || /\/zarr\.json$|\/\.zmetadata$/i.test(u);
|
|
496
|
+
/** VoxelSource(URL/bytes)→ VoxelData(region・times・meta) */
|
|
497
|
+
export async function readSource(src, o = {}) {
|
|
498
|
+
if (src instanceof ArrayBuffer || src instanceof Uint8Array) {
|
|
499
|
+
const b = src instanceof Uint8Array ? src : new Uint8Array(src);
|
|
500
|
+
if (!sniffNetCDF(b))
|
|
501
|
+
throw new Error("voxel: bytes are not NetCDF (pass a .zarr URL for Zarr)");
|
|
502
|
+
return toVoxelData(parseNetCDF(b), o);
|
|
503
|
+
}
|
|
504
|
+
const url = String(src);
|
|
505
|
+
if (isZarrUrl(url))
|
|
506
|
+
return toVoxelData(await openZarr(url.replace(/\/(zarr\.json|\.zmetadata)$/i, "").split(/[?#]/)[0], o), o);
|
|
507
|
+
if (/\.(vox|npy)(\?|#|$)/i.test(url))
|
|
508
|
+
throw new Error("voxel: .vox / .npy sources are not read yet; pass VoxelData, NetCDF or Zarr");
|
|
509
|
+
const f = o.fetch ?? ((u) => fetch(u));
|
|
510
|
+
const r = await f(url);
|
|
511
|
+
if (!r.ok)
|
|
512
|
+
throw new Error(`voxel: ${url}: HTTP ${r.status}`);
|
|
513
|
+
return toVoxelData(parseNetCDF(new Uint8Array(await r.arrayBuffer())), o);
|
|
514
|
+
}
|
package/dist/voxel-gl.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OverlayFrameApi } from "@ortho-earth/globe";
|
|
2
|
-
import type { GeoGrid, Clip } from "./grid.js";
|
|
2
|
+
import type { GeoGrid, Clip, Level } from "./grid.js";
|
|
3
3
|
export interface OverlayHost {
|
|
4
4
|
depthGLSL?: string | null;
|
|
5
5
|
requestDraw?(): void;
|
|
@@ -38,8 +38,9 @@ export interface VoxelLayerBody extends VoxelStyle {
|
|
|
38
38
|
off?: [number, number, number];
|
|
39
39
|
own?: [number, number, number, number, number, number];
|
|
40
40
|
full?: [number, number, number]; /** 最大値(R8・RGBA は α)=閾より下なら描かない */
|
|
41
|
-
vmax?: number; /**
|
|
42
|
-
frames?: Uint8Array[] | null;
|
|
41
|
+
vmax?: number; /** 時間の次元=こまごとの値(frames[0]=tex)。style の frame で選ぶ(GPU には見せるこまだけ) */
|
|
42
|
+
frames?: Uint8Array[] | null; /** 粗い段(s=2, 4…)=GPU の予算を越える時に遠い層で使う */
|
|
43
|
+
lods?: LevelBody[] | null;
|
|
43
44
|
shape: "box" | "region";
|
|
44
45
|
dims: [number, number, number];
|
|
45
46
|
comps: 1 | 4;
|
|
@@ -57,6 +58,7 @@ export interface VoxelLayerBody extends VoxelStyle {
|
|
|
57
58
|
range: [number, number];
|
|
58
59
|
on?: boolean;
|
|
59
60
|
}
|
|
61
|
+
export type LevelBody = Pick<Level, "s" | "off" | "own" | "dims" | "tex" | "frames" | "vmax" | "anchor" | "A" | "radius" | "geo">;
|
|
60
62
|
export type VoxelMessage = ({
|
|
61
63
|
type: "layer";
|
|
62
64
|
q: string;
|
|
@@ -72,6 +74,9 @@ export type VoxelMessage = ({
|
|
|
72
74
|
on: boolean;
|
|
73
75
|
} | {
|
|
74
76
|
type: "clear";
|
|
77
|
+
} | {
|
|
78
|
+
type: "budget";
|
|
79
|
+
bytes: number;
|
|
75
80
|
} | {
|
|
76
81
|
type: "probe";
|
|
77
82
|
id: unknown;
|
|
@@ -110,6 +115,15 @@ export declare function normalMats(A: Float64Array, Ainv: Float64Array): {
|
|
|
110
115
|
nrm: Float32Array;
|
|
111
116
|
gradM: Float32Array;
|
|
112
117
|
};
|
|
118
|
+
/** 元のセルの大きさ(m)=box は A の列の短い方・region は cellM の短い方 */
|
|
119
|
+
export declare const cellOf: (A: ArrayLike<number>, geo: GeoGrid | null) => number;
|
|
120
|
+
/** LoD の割り振り(第 5 段 ③)=予算(bytes)に収まる中で一番細かく。遠い(元のセルが小さく見える)層から粗く:
|
|
121
|
+
* lv_i = clamp(floor(x + log2(dist_i / cell_i)), 0, 段の数 − 1) の x を二分で(x が大きいほど粗い・合計は x に単調)。一番粗くしても越えるなら全部一番粗く */
|
|
122
|
+
export declare function planLevels(items: ReadonlyArray<{
|
|
123
|
+
dist: number;
|
|
124
|
+
cell: number;
|
|
125
|
+
bytes: readonly number[];
|
|
126
|
+
}>, budget: number): number[];
|
|
113
127
|
export declare function message(d: VoxelMessage): void;
|
|
114
128
|
/** 刻み(box=セル・region=m)と最大の歩数(光線の空間で) */
|
|
115
129
|
export declare function stepsOf(L: {
|
|
@@ -133,8 +147,9 @@ type BrickLike = {
|
|
|
133
147
|
off: [number, number, number] | null;
|
|
134
148
|
own: [number, number, number, number, number, number] | null;
|
|
135
149
|
full: [number, number, number] | null;
|
|
150
|
+
s?: number;
|
|
136
151
|
};
|
|
137
|
-
/** 描く範囲(テクスチャのセル座標)=断面(親の割合)と持ち分の交わり。空なら null
|
|
152
|
+
/** 描く範囲(テクスチャのセル座標)=断面(親の割合)と持ち分の交わり。空なら null(そのブリックは描かない)。粗い段(s)は親もその段のセルで測る */
|
|
138
153
|
export declare function drawBounds(L: BrickLike): {
|
|
139
154
|
lo: number[];
|
|
140
155
|
hi: number[];
|
|
@@ -152,12 +167,13 @@ export declare function centerDist(L: {
|
|
|
152
167
|
off: [number, number, number] | null;
|
|
153
168
|
geo: GeoGrid | null;
|
|
154
169
|
}, enu: number[]): number;
|
|
155
|
-
/** 当たり(層の中の番号)→ 親の層の id・番号・セル(ブリックは off と full
|
|
170
|
+
/** 当たり(層の中の番号)→ 親の層の id・番号・セル(ブリックは off と full で戻す・粗い段は元のセルの真ん中へ) */
|
|
156
171
|
export declare function pickedCell(q: string, L: {
|
|
157
172
|
layer: string | null;
|
|
158
173
|
dims: [number, number, number];
|
|
159
174
|
off: [number, number, number] | null;
|
|
160
175
|
full: [number, number, number] | null;
|
|
176
|
+
s?: number;
|
|
161
177
|
}, index: number): {
|
|
162
178
|
layer: string;
|
|
163
179
|
index: number;
|