autumnplot-gl 4.0.0-beta → 4.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.
- package/README.md +13 -207
- package/dist/812.autumnplot-gl.js +2 -0
- package/dist/812.autumnplot-gl.js.map +1 -0
- package/dist/983.autumnplot-gl.js +2 -0
- package/dist/983.autumnplot-gl.js.map +1 -0
- package/dist/autumnplot-gl.js +1 -1
- package/dist/autumnplot-gl.js.map +1 -1
- package/dist/marchingsquares.wasm +0 -0
- package/lib/AutumnTypes.d.ts +38 -5
- package/lib/AutumnTypes.js +7 -1
- package/lib/Barbs.d.ts +12 -2
- package/lib/Barbs.js +9 -0
- package/lib/BillboardCollection.d.ts +2 -2
- package/lib/BillboardCollection.js +14 -14
- package/lib/Color.d.ts +1 -0
- package/lib/Color.js +1 -0
- package/lib/ColorBar.d.ts +14 -0
- package/lib/ColorBar.js +15 -8
- package/lib/Colormap.d.ts +9 -1
- package/lib/Colormap.js +24 -1
- package/lib/Contour.d.ts +26 -1
- package/lib/Contour.js +24 -2
- package/lib/ContourCreator.worker.d.ts +25 -0
- package/lib/{ContourCreator.js → ContourCreator.worker.js} +15 -14
- package/lib/Fill.d.ts +31 -11
- package/lib/Fill.js +38 -18
- package/lib/Hodographs.d.ts +19 -3
- package/lib/Hodographs.js +45 -20
- package/lib/Map.d.ts +13 -1
- package/lib/Map.js +62 -8
- package/lib/Paintball.d.ts +14 -5
- package/lib/Paintball.js +96 -46
- package/lib/PlotComponent.d.ts +9 -3
- package/lib/PlotComponent.js +36 -1
- package/lib/PlotLayer.d.ts +2 -2
- package/lib/PlotLayer.js +2 -2
- package/lib/PlotLayer.worker.js +9 -3
- package/lib/RawField.d.ts +223 -27
- package/lib/RawField.js +413 -59
- package/lib/StationPlot.d.ts +78 -11
- package/lib/StationPlot.js +113 -30
- package/lib/TextCollection.d.ts +5 -0
- package/lib/TextCollection.js +82 -9
- package/lib/WasmInterface.d.ts +7 -0
- package/lib/WasmInterface.js +11 -0
- package/lib/WorkerPool.d.ts +8 -0
- package/lib/WorkerPool.js +77 -0
- package/lib/cpp/marchingsquares.js +127 -13
- package/lib/cpp/marchingsquares.wasm +0 -0
- package/lib/cpp/marchingsquares_embind.d.ts +16 -3
- package/lib/grids/AutoZoom.d.ts +21 -0
- package/lib/grids/AutoZoom.js +63 -0
- package/lib/grids/DomainBuffer.d.ts +14 -0
- package/lib/grids/DomainBuffer.js +16 -0
- package/lib/grids/Geostationary.d.ts +35 -0
- package/lib/grids/Geostationary.js +47 -0
- package/lib/grids/Grid.d.ts +36 -0
- package/lib/grids/Grid.js +12 -0
- package/lib/grids/GridCoordinates.d.ts +10 -0
- package/lib/grids/GridCoordinates.js +64 -0
- package/lib/grids/LambertGrid.d.ts +73 -0
- package/lib/grids/LambertGrid.js +92 -0
- package/lib/grids/PlateCarreeGrid.d.ts +46 -0
- package/lib/grids/PlateCarreeGrid.js +55 -0
- package/lib/grids/PlateCarreeRotatedGrid.d.ts +53 -0
- package/lib/grids/PlateCarreeRotatedGrid.js +65 -0
- package/lib/grids/RadarSweepGrid.d.ts +46 -0
- package/lib/grids/RadarSweepGrid.js +74 -0
- package/lib/grids/StructuredGrid.d.ts +49 -0
- package/lib/grids/StructuredGrid.js +103 -0
- package/lib/grids/UnstructuredGrid.d.ts +56 -0
- package/lib/grids/UnstructuredGrid.js +102 -0
- package/lib/index.d.ts +23 -6
- package/lib/index.js +18 -8
- package/lib/utils.d.ts +11 -2
- package/lib/utils.js +63 -1
- package/package.json +4 -3
- package/dist/110.autumnplot-gl.js +0 -2
- package/dist/110.autumnplot-gl.js.map +0 -1
- package/lib/ContourCreator.d.ts +0 -22
- package/lib/Grid.d.ts +0 -263
- package/lib/Grid.js +0 -547
- package/lib/ParticleTracer.d.ts +0 -19
- package/lib/ParticleTracer.js +0 -37
package/lib/RawField.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Float16Array } from "@petamoriken/float16";
|
|
2
|
-
import {
|
|
2
|
+
import { isContourable, isStormRelativeWindProfile } from "./AutumnTypes";
|
|
3
3
|
import { Cache, getArrayConstructor, zip } from "./utils";
|
|
4
|
-
import {
|
|
4
|
+
import { WGLTexture } from "autumn-wgl";
|
|
5
|
+
import { getContourWorkerPool, getGLFormatTypeAlignment } from "./PlotComponent";
|
|
5
6
|
function getArrayDType(ary) {
|
|
6
7
|
if (ary instanceof Float32Array) {
|
|
7
8
|
return 'float32';
|
|
@@ -9,40 +10,157 @@ function getArrayDType(ary) {
|
|
|
9
10
|
else if (ary instanceof Uint8Array) {
|
|
10
11
|
return 'uint8';
|
|
11
12
|
}
|
|
13
|
+
else if (ary instanceof Uint16Array) {
|
|
14
|
+
return 'uint16';
|
|
15
|
+
}
|
|
16
|
+
else if (ary instanceof Uint32Array) {
|
|
17
|
+
return 'uint32';
|
|
18
|
+
}
|
|
19
|
+
else if (ary instanceof Int16Array) {
|
|
20
|
+
return 'int16';
|
|
21
|
+
}
|
|
22
|
+
else if (ary instanceof Int32Array) {
|
|
23
|
+
return 'int32';
|
|
24
|
+
}
|
|
12
25
|
return 'float16';
|
|
13
26
|
}
|
|
27
|
+
class ExpressionScalarField {
|
|
28
|
+
operand(other, operand) {
|
|
29
|
+
const FUNCS = {
|
|
30
|
+
'+': (a, b) => a + b,
|
|
31
|
+
'-': (a, b) => a - b,
|
|
32
|
+
'*': (a, b) => a * b,
|
|
33
|
+
'/': (a, b) => a / b,
|
|
34
|
+
};
|
|
35
|
+
if (typeof other === 'number') {
|
|
36
|
+
return new ComputedScalarField([this], `{0} ${operand} ${other.toFixed(100)}`, v => FUNCS[operand](v, other));
|
|
37
|
+
}
|
|
38
|
+
return new ComputedScalarField([this, other], `{0} ${operand} {1}`, FUNCS[operand]);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Multiply this field by another scalar. The computation occurs on the GPU if the resulting field is used in a plot component or on the CPU if
|
|
42
|
+
* {@link ComputedScalarField.renderCPU | renderCPU()} is called on the resulting field.
|
|
43
|
+
* @param other - Scalar to multiply this field by
|
|
44
|
+
* @returns A `ComputedScalarField` representing the multiplied field
|
|
45
|
+
*/
|
|
46
|
+
multiply(other) {
|
|
47
|
+
return this.operand(other, '*');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Divide this field by another scalar. The computation occurs on the GPU if the resulting field is used in a plot component or on the CPU if
|
|
51
|
+
* {@link ComputedScalarField.renderCPU | renderCPU()} is called on the resulting field.
|
|
52
|
+
* @param other - Scalar to divide this field by
|
|
53
|
+
* @returns A `ComputedScalarField` representing the divided field
|
|
54
|
+
*/
|
|
55
|
+
divide(other) {
|
|
56
|
+
return this.operand(other, '/');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Add this field to another scalar. The computation occurs on the GPU if the resulting field is used in a plot component or on the CPU if
|
|
60
|
+
* {@link ComputedScalarField.renderCPU | renderCPU()} is called on the resulting field.
|
|
61
|
+
* @param other - Scalar to add to this field
|
|
62
|
+
* @returns A `ComputedScalarField` representing the added field
|
|
63
|
+
*/
|
|
64
|
+
add(other) {
|
|
65
|
+
return this.operand(other, '+');
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Subtract another scalar from this field. The computation occurs on the GPU if the resulting field is used in a plot component or on the CPU if
|
|
69
|
+
* {@link ComputedScalarField.renderCPU | renderCPU()} is called on the resulting field.
|
|
70
|
+
* @param other - Scalar to subtract from this field
|
|
71
|
+
* @returns A `ComputedScalarField` representing the subtracted field
|
|
72
|
+
*/
|
|
73
|
+
subtract(other) {
|
|
74
|
+
return this.operand(other, '-');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
14
77
|
/** A class representing a raw 2D field of gridded data, such as height or u wind. */
|
|
15
|
-
class RawScalarField {
|
|
78
|
+
class RawScalarField extends ExpressionScalarField {
|
|
16
79
|
/**
|
|
17
80
|
* Create a data field.
|
|
18
81
|
* @param grid - The grid on which the data are defined
|
|
19
82
|
* @param data - The data, which should be given as a 1D array in row-major order, with the first element being at the lower-left corner of the grid.
|
|
20
83
|
*/
|
|
21
84
|
constructor(grid, data) {
|
|
85
|
+
super();
|
|
22
86
|
this.grid = grid;
|
|
23
87
|
this.data = data;
|
|
24
88
|
if (grid.ni * grid.nj != data.length) {
|
|
25
89
|
throw `Data size (${data.length}) doesn't match the grid dimensions (${grid.ni} x ${grid.nj}; expected ${grid.ni * grid.nj} points)`;
|
|
26
90
|
}
|
|
27
91
|
this.contour_cache = new Cache(async (opts) => {
|
|
28
|
-
|
|
92
|
+
if (getArrayDType(this.data) != 'float16' && getArrayDType(this.data) != 'float32')
|
|
93
|
+
throw `Grid is of type ${getArrayDType(this.data)}, which is not contourable (should be either float16 or float32)`;
|
|
94
|
+
const tex_data = this.getTextureData();
|
|
95
|
+
if (!isContourable(tex_data))
|
|
96
|
+
throw `Type check for contourable array failed`;
|
|
97
|
+
const pool = getContourWorkerPool(undefined, 1); // 1 worker is the default; if the user requests more, the pool will be pre-created with the correct number of workers
|
|
98
|
+
const contour_data = await pool.contourCreator(tex_data, grid.getGridCoords(), opts);
|
|
99
|
+
for (const v in contour_data) {
|
|
100
|
+
for (let ic = 0; ic < contour_data[v].length; ic++) {
|
|
101
|
+
for (let ip = 0; ip < contour_data[v][ic].length; ip++) {
|
|
102
|
+
const [x, y] = contour_data[v][ic][ip];
|
|
103
|
+
contour_data[v][ic][ip] = grid.transform(x, y, { inverse: true });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return contour_data;
|
|
29
108
|
});
|
|
30
109
|
}
|
|
31
110
|
/** @internal */
|
|
111
|
+
get aryConstructor() {
|
|
112
|
+
return getArrayConstructor(this.data);
|
|
113
|
+
}
|
|
114
|
+
/** @internal */
|
|
115
|
+
get dtypes() {
|
|
116
|
+
return [getArrayDType(this.data)];
|
|
117
|
+
}
|
|
118
|
+
/** @internal */
|
|
32
119
|
getTextureData() {
|
|
33
120
|
// Need to give float16 data as uint16s to make WebGL happy: https://github.com/petamoriken/float16/issues/105
|
|
34
121
|
const raw_data = this.data;
|
|
35
122
|
const raw_data_type = getArrayDType(raw_data);
|
|
36
|
-
const data =
|
|
123
|
+
const data = ['float32', 'uint8', 'uint32', 'uint16', 'int32', 'int16'].includes(raw_data_type) ? raw_data : new Uint16Array(raw_data.buffer);
|
|
37
124
|
return data;
|
|
38
125
|
}
|
|
39
126
|
getWGLTextureSpec(gl, image_mag_filter) {
|
|
40
127
|
const tex_data = this.getTextureData();
|
|
41
128
|
const { format, type, row_alignment } = getGLFormatTypeAlignment(gl, getArrayDType(this.data));
|
|
42
|
-
return { 'format': format, 'type': type,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
129
|
+
return new Map([['_0', { 'format': format, 'type': type,
|
|
130
|
+
'width': this.grid.ni, 'height': this.grid.nj, 'image': tex_data,
|
|
131
|
+
'mag_filter': image_mag_filter, 'min_filter': image_mag_filter, 'row_alignment': row_alignment,
|
|
132
|
+
}]]);
|
|
133
|
+
}
|
|
134
|
+
updateTexImageData(gl, image_mag_filter, fill_textures) {
|
|
135
|
+
const fill_texture_specs = this.getWGLTextureSpec(gl, image_mag_filter);
|
|
136
|
+
if (fill_textures === null) {
|
|
137
|
+
fill_textures = new Map(this.getSamplerIds().map(key => {
|
|
138
|
+
const key_fill_image = fill_texture_specs.get(key);
|
|
139
|
+
if (key_fill_image === undefined)
|
|
140
|
+
throw `Missing key '${key}' in fill_texture_specs`;
|
|
141
|
+
return [key, new WGLTexture(gl, key_fill_image)];
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.getSamplerIds().forEach(key => {
|
|
146
|
+
const key_fill_image = fill_texture_specs.get(key);
|
|
147
|
+
if (key_fill_image === undefined)
|
|
148
|
+
throw `Missing key '${key}' in fill_texture_specs`;
|
|
149
|
+
const tex = fill_textures?.get(key);
|
|
150
|
+
if (tex === undefined)
|
|
151
|
+
throw `Missing key '${key}' in fill_textures`;
|
|
152
|
+
tex.setImageData(key_fill_image);
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return fill_textures;
|
|
156
|
+
}
|
|
157
|
+
/** @internal */
|
|
158
|
+
getSamplerIds() {
|
|
159
|
+
return ['_0'];
|
|
160
|
+
}
|
|
161
|
+
/** @internal */
|
|
162
|
+
getExpression() {
|
|
163
|
+
return '_0';
|
|
46
164
|
}
|
|
47
165
|
/**
|
|
48
166
|
* Get contour data as an object with each contour level being a separate property.
|
|
@@ -53,7 +171,7 @@ class RawScalarField {
|
|
|
53
171
|
return await this.contour_cache.getValue(opts);
|
|
54
172
|
}
|
|
55
173
|
/**
|
|
56
|
-
* Create a new field by aggregating a number of fields using a specific function
|
|
174
|
+
* Create a new field by aggregating a number of fields using a specific function. This computation occurs on the CPU.
|
|
57
175
|
* @param func - A function that will be applied each element of the field. It should take the same number of arguments as fields you have and return a single number.
|
|
58
176
|
* @param args - The RawScalarFields to aggregate
|
|
59
177
|
* @returns a new gridded field
|
|
@@ -62,73 +180,255 @@ class RawScalarField {
|
|
|
62
180
|
* wind_speed_field = RawScalarField.aggreateFields(Math.hypot, u_field, v_field);
|
|
63
181
|
*/
|
|
64
182
|
static aggregateFields(func, ...args) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
183
|
+
return (new ComputedScalarField(args, '', func)).renderCPU();
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Run computations on a scalar field on the CPU (for a `RawScalarField`, this is a no-op). The function blocks the main thread, so avoid calling it if possible.
|
|
187
|
+
* @returns The computed grid in a `RawScalarField`
|
|
188
|
+
*/
|
|
189
|
+
renderCPU() {
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
/** @internal */
|
|
193
|
+
*iterateCPU() {
|
|
194
|
+
for (let i = 0; i < this.data.length; i++) {
|
|
195
|
+
yield this.data[i];
|
|
69
196
|
}
|
|
70
|
-
const arrayType = getArrayConstructor(args[0].data);
|
|
71
|
-
const zipped_args = zip(...args.map(a => a.data));
|
|
72
|
-
const agg_data = new arrayType(mapGenerator(zipped_args, (a) => func(...a)));
|
|
73
|
-
return new RawScalarField(args[0].grid, agg_data);
|
|
74
197
|
}
|
|
198
|
+
/** @internal */
|
|
199
|
+
getThinnedField(thin_fac, map_max_zoom) {
|
|
200
|
+
const new_grid = this.grid.getThinnedGrid(thin_fac, map_max_zoom);
|
|
201
|
+
const thin_data = new_grid.thinDataArray(this.grid, this.data);
|
|
202
|
+
return new RawScalarField(new_grid, thin_data);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Sample this field at a given latitude and longitude.
|
|
206
|
+
* @param lon - Longitude of the sample in degrees east
|
|
207
|
+
* @param lat - Latitude of the sample in degrees north
|
|
208
|
+
* @returns The value of the nearest grid point along with the grid point latitude and longitude, or NaNs if the point is outside the grid.
|
|
209
|
+
*/
|
|
210
|
+
sampleFieldWithCoord(lon, lat) {
|
|
211
|
+
return this.grid.sampleNearestGridPoint(lon, lat, this.data);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Sample this field at a given latitude and longitude.
|
|
215
|
+
* @param lon - Longitude of the sample in degrees east
|
|
216
|
+
* @param lat - Latitude of the sample in degrees north
|
|
217
|
+
* @returns The value of the nearest grid point, or NaN if the point is outside the grid.
|
|
218
|
+
*/
|
|
75
219
|
sampleField(lon, lat) {
|
|
76
|
-
return this.
|
|
220
|
+
return this.sampleFieldWithCoord(lon, lat).sample;
|
|
77
221
|
}
|
|
78
222
|
}
|
|
79
|
-
|
|
80
|
-
class
|
|
223
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz';
|
|
224
|
+
class ComputedScalarField extends ExpressionScalarField {
|
|
225
|
+
constructor(raw_fields, expression, cpu_func) {
|
|
226
|
+
super();
|
|
227
|
+
this.raw_fields = raw_fields;
|
|
228
|
+
this.expression = expression;
|
|
229
|
+
this.cpu_func = cpu_func;
|
|
230
|
+
}
|
|
231
|
+
/** @internal */
|
|
232
|
+
get grid() {
|
|
233
|
+
return this.raw_fields[0].grid;
|
|
234
|
+
}
|
|
235
|
+
/** @internal */
|
|
236
|
+
get aryConstructor() {
|
|
237
|
+
return this.raw_fields[0].aryConstructor;
|
|
238
|
+
}
|
|
239
|
+
/** @internal */
|
|
240
|
+
get dtypes() {
|
|
241
|
+
return this.raw_fields.map(f => f.dtypes).flat();
|
|
242
|
+
}
|
|
243
|
+
/** @internal */
|
|
244
|
+
updateTexImageData(gl, image_mag_filter, fill_textures) {
|
|
245
|
+
const fill_textures_ret = new Map();
|
|
246
|
+
this.raw_fields.forEach((field, idx) => {
|
|
247
|
+
let fill_textures_pre_field = null;
|
|
248
|
+
if (fill_textures !== null) {
|
|
249
|
+
fill_textures_pre_field = new Map();
|
|
250
|
+
for (let [key, val] of fill_textures) {
|
|
251
|
+
if (key[key.length - 1] == chars[idx])
|
|
252
|
+
fill_textures_pre_field.set(key.slice(0, -1), val);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const fill_textures_field = field.updateTexImageData(gl, image_mag_filter, fill_textures_pre_field);
|
|
256
|
+
for (let [key, val] of fill_textures_field) {
|
|
257
|
+
fill_textures_ret.set(`${key}${chars[idx]}`, val);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return fill_textures_ret;
|
|
261
|
+
}
|
|
262
|
+
/** @internal */
|
|
263
|
+
getSamplerIds() {
|
|
264
|
+
return this.raw_fields.map((f, i) => f.getSamplerIds().map(id => `${id}${chars[i]}`)).flat();
|
|
265
|
+
}
|
|
266
|
+
/** @internal */
|
|
267
|
+
getExpression() {
|
|
268
|
+
let expression = this.expression;
|
|
269
|
+
this.raw_fields.forEach((field, idx) => {
|
|
270
|
+
let field_expr = field.getExpression();
|
|
271
|
+
const matches = field_expr.match(/_0[a-z]*/g);
|
|
272
|
+
if (matches === null)
|
|
273
|
+
throw `Field expression not found`;
|
|
274
|
+
matches.forEach(m => field_expr = field_expr.replace(m, `${m}${chars[idx]}`));
|
|
275
|
+
expression = expression.replace(`{${idx}}`, field_expr);
|
|
276
|
+
});
|
|
277
|
+
return `(${expression})`;
|
|
278
|
+
}
|
|
279
|
+
/** @internal */
|
|
280
|
+
getThinnedField(thin_fac, map_max_zoom) {
|
|
281
|
+
return new ComputedScalarField(this.raw_fields.map(f => f.getThinnedField(thin_fac, map_max_zoom)), this.expression, this.cpu_func);
|
|
282
|
+
}
|
|
81
283
|
/**
|
|
82
|
-
*
|
|
83
|
-
* @param
|
|
84
|
-
* @param
|
|
85
|
-
* @
|
|
86
|
-
|
|
284
|
+
* Sample this field at a given latitude and longitude.
|
|
285
|
+
* @param lon - Longitude of the sample in degrees east
|
|
286
|
+
* @param lat - Latitude of the sample in degrees north
|
|
287
|
+
* @returns The value of the nearest grid point along with the grid point latitude and longitude, or NaNs if the point is outside the grid.
|
|
288
|
+
*/
|
|
289
|
+
sampleFieldWithCoord(lon, lat) {
|
|
290
|
+
const field_samples = this.raw_fields.map(f => f.sampleFieldWithCoord(lon, lat));
|
|
291
|
+
return { sample: this.cpu_func(...field_samples.map(s => s.sample)), sample_lon: field_samples[0].sample_lon, sample_lat: field_samples[0].sample_lat };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Sample this field at a given latitude and longitude.
|
|
295
|
+
* @param lon - Longitude of the sample in degrees east
|
|
296
|
+
* @param lat - Latitude of the sample in degrees north
|
|
297
|
+
* @returns The value of the nearest grid point, or NaN if the point is outside the grid.
|
|
298
|
+
*/
|
|
299
|
+
sampleField(lon, lat) {
|
|
300
|
+
return this.sampleFieldWithCoord(lon, lat).sample;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Run computations on a scalar field on the CPU. The function blocks the main thread, so avoid calling it if possible.
|
|
304
|
+
* @returns The computed grid in a `RawScalarField`
|
|
87
305
|
*/
|
|
88
|
-
|
|
306
|
+
renderCPU() {
|
|
307
|
+
const ary = new this.aryConstructor([...this.iterateCPU()]);
|
|
308
|
+
return new RawScalarField(this.grid, ary);
|
|
309
|
+
}
|
|
310
|
+
/** @internal */
|
|
311
|
+
*iterateCPU() {
|
|
312
|
+
function* mapGenerator(gen, func) {
|
|
313
|
+
for (const elem of gen) {
|
|
314
|
+
yield func(...elem);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const zipped_args = zip(...this.raw_fields.map(a => a.iterateCPU()));
|
|
318
|
+
for (const elem of mapGenerator(zipped_args, this.cpu_func)) {
|
|
319
|
+
yield elem;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function scalarIdToVectorComponentId(id, component) {
|
|
324
|
+
return `_${component}${id.slice(1)}`;
|
|
325
|
+
}
|
|
326
|
+
function vectorComponentIdToScalarId(id) {
|
|
327
|
+
return `_${id.slice(2)}`;
|
|
328
|
+
}
|
|
329
|
+
class ExpressionVectorField {
|
|
330
|
+
constructor(u, v, opts) {
|
|
331
|
+
this.u = u;
|
|
332
|
+
this.v = v;
|
|
89
333
|
opts = opts === undefined ? {} : opts;
|
|
90
|
-
this.u = new RawScalarField(grid, u);
|
|
91
|
-
this.v = new RawScalarField(grid, v);
|
|
92
334
|
this.relative_to = opts.relative_to === undefined ? 'grid' : opts.relative_to;
|
|
93
335
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const raw_v = this.v.data;
|
|
99
|
-
const u_raw_data_type = getArrayDType(raw_u);
|
|
100
|
-
const v_raw_data_type = getArrayDType(raw_u);
|
|
101
|
-
const u = (u_raw_data_type == 'float32' || u_raw_data_type == 'uint8') ? raw_u : new Uint16Array(raw_u.buffer);
|
|
102
|
-
const v = (v_raw_data_type == 'float32' || v_raw_data_type == 'uint8') ? raw_v : new Uint16Array(raw_v.buffer);
|
|
103
|
-
return { u: u, v: v };
|
|
104
|
-
}
|
|
105
|
-
getWGLTextureSpecs(gl, mag_filter) {
|
|
106
|
-
const { u: u_thin, v: v_thin } = this.getTextureData();
|
|
107
|
-
const { format, type, row_alignment } = getGLFormatTypeAlignment(gl, getArrayDType(this.u.data));
|
|
108
|
-
const u_image = { 'format': format, 'type': type,
|
|
109
|
-
'width': this.grid.ni, 'height': this.grid.nj, 'image': u_thin,
|
|
110
|
-
'mag_filter': mag_filter, 'row_alignment': row_alignment,
|
|
336
|
+
operandScalar(other, operand) {
|
|
337
|
+
const FUNCS = {
|
|
338
|
+
'*': (a, b) => a * b,
|
|
339
|
+
'/': (a, b) => a / b,
|
|
111
340
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
341
|
+
if (typeof other === 'number') {
|
|
342
|
+
const u = new ComputedScalarField([this.u], `{0} ${operand} ${other.toFixed(100)}`, v => FUNCS[operand](v, other));
|
|
343
|
+
const v = new ComputedScalarField([this.v], `{0} ${operand} ${other.toFixed(100)}`, v => FUNCS[operand](v, other));
|
|
344
|
+
return new ComputedVectorField(u, v, { relative_to: this.relative_to });
|
|
345
|
+
}
|
|
346
|
+
const u = new ComputedScalarField([this.u, other], `{0} ${operand} {1}`, FUNCS[operand]);
|
|
347
|
+
const v = new ComputedScalarField([this.v, other], `{0} ${operand} {1}`, FUNCS[operand]);
|
|
348
|
+
return new ComputedVectorField(u, v, { relative_to: this.relative_to });
|
|
349
|
+
}
|
|
350
|
+
operandVector(other, operand) {
|
|
351
|
+
const FUNCS = {
|
|
352
|
+
'+': (a, b) => a + b,
|
|
353
|
+
'-': (a, b) => a - b,
|
|
354
|
+
};
|
|
355
|
+
const u = new ComputedScalarField([this.u, other.u], `{0} ${operand} {1}`, FUNCS[operand]);
|
|
356
|
+
const v = new ComputedScalarField([this.v, other.v], `{0} ${operand} {1}`, FUNCS[operand]);
|
|
357
|
+
return new ComputedVectorField(u, v, { relative_to: this.relative_to });
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Multiply this vector field by a scalar. The multiplication occurs on the GPU if the resulting field is used in a plot component.
|
|
361
|
+
* @param other - Scalar to multiply by. Can be either a number or a scalar field.
|
|
362
|
+
* @returns A `ComputedVectorField` representing the multiplied vector field
|
|
363
|
+
*/
|
|
364
|
+
multiply(other) {
|
|
365
|
+
return this.operandScalar(other, '*');
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Divide this vector field by a scalar. The division occurs on the GPU if the resulting field is used in a plot component.
|
|
369
|
+
* @param other - Scalar to divide by. Can be either a number or a scalar field.
|
|
370
|
+
* @returns A `ComputedVectorField` representing the divided vector field
|
|
371
|
+
*/
|
|
372
|
+
divide(other) {
|
|
373
|
+
return this.operandScalar(other, '/');
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Add this vector field to another vector field. The addition occurs on the GPU if the resulting field is used in a plot component.
|
|
377
|
+
* @param other Vector field to add.
|
|
378
|
+
* @returns A `ComputedVectorField` representing the added vector field
|
|
379
|
+
*/
|
|
380
|
+
add(other) {
|
|
381
|
+
return this.operandVector(other, '+');
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Subtract another vector field from this vector field. The subtraction occurs on the GPU if the resulting field is used in a plot component.
|
|
385
|
+
* @param other Vector field to subtract.
|
|
386
|
+
* @returns A `ComputedVectorField` representing the subtracted vector field
|
|
387
|
+
*/
|
|
388
|
+
subtract(other) {
|
|
389
|
+
return this.operandVector(other, '-');
|
|
390
|
+
}
|
|
391
|
+
/** @internal */
|
|
392
|
+
updateTexImageData(gl, image_mag_filter, fill_textures) {
|
|
393
|
+
const translateKeys = (map, component, reverse) => {
|
|
394
|
+
const map_trans = new Map();
|
|
395
|
+
const translator = reverse ? vectorComponentIdToScalarId : scalarIdToVectorComponentId;
|
|
396
|
+
map.forEach((value, key) => {
|
|
397
|
+
map_trans.set(translator(key, component), value);
|
|
398
|
+
});
|
|
399
|
+
return map_trans;
|
|
115
400
|
};
|
|
116
|
-
|
|
401
|
+
const tex_u = this.u.updateTexImageData(gl, image_mag_filter, fill_textures === null ? null : translateKeys(fill_textures.u, 'u', true));
|
|
402
|
+
const tex_v = this.v.updateTexImageData(gl, image_mag_filter, fill_textures === null ? null : translateKeys(fill_textures.v, 'v', true));
|
|
403
|
+
return { u: translateKeys(tex_u, 'u', false), v: translateKeys(tex_v, 'v', false) };
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Get the magnitude of the vector field as a scalar field. The magnitude calculation occurs on the GPU if this field is used in a plot component.
|
|
407
|
+
* @returns A `ComputedScalarField` representing the subtracted vector field
|
|
408
|
+
*/
|
|
409
|
+
magnitude() {
|
|
410
|
+
return new ComputedScalarField([this.u, this.v], 'length(vec2({0}, {1}))', Math.hypot);
|
|
117
411
|
}
|
|
118
412
|
/** @internal */
|
|
119
413
|
getThinnedField(thin_fac, map_max_zoom) {
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
return new RawVectorField(new_grid, thin_u, thin_v, { relative_to: this.relative_to });
|
|
414
|
+
const thin_u = this.u.getThinnedField(thin_fac, map_max_zoom);
|
|
415
|
+
const thin_v = this.v.getThinnedField(thin_fac, map_max_zoom);
|
|
416
|
+
return new ComputedVectorField(thin_u, thin_v, { relative_to: this.relative_to });
|
|
124
417
|
}
|
|
125
418
|
/** @internal */
|
|
126
419
|
get grid() {
|
|
127
420
|
return this.u.grid;
|
|
128
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* Sample this field at a given latitude and longitude.
|
|
424
|
+
* @param lon - Longitude of the sample in degrees east
|
|
425
|
+
* @param lat - Latitude of the sample in degrees north
|
|
426
|
+
* @returns A tuple containing the [`bearing`, `magnitude`] of the vector field at the nearest grid point. The bearing is given as degrees from north, increasing clockwise.
|
|
427
|
+
* If the point is outside the grid, it returns [NaN, NaN] instead.
|
|
428
|
+
*/
|
|
129
429
|
sampleField(lon, lat) {
|
|
130
|
-
const u_sample = this.
|
|
131
|
-
const v_sample = this.
|
|
430
|
+
const u_sample = this.u.sampleFieldWithCoord(lon, lat);
|
|
431
|
+
const v_sample = this.v.sampleFieldWithCoord(lon, lat);
|
|
132
432
|
const rot = this.relative_to == 'earth' ? 0 : this.grid.getVectorRotationAtPoint(u_sample.sample_lon, u_sample.sample_lat);
|
|
133
433
|
const mag = Math.hypot(u_sample.sample, v_sample.sample);
|
|
134
434
|
let brg = (Math.PI / 2 - Math.atan2(-v_sample.sample, -u_sample.sample) + rot) * 180 / Math.PI;
|
|
@@ -138,6 +438,54 @@ class RawVectorField {
|
|
|
138
438
|
brg += 360;
|
|
139
439
|
return [brg, mag];
|
|
140
440
|
}
|
|
441
|
+
/** @internal */
|
|
442
|
+
getSamplerIds() {
|
|
443
|
+
return {
|
|
444
|
+
u: this.u.getSamplerIds().map(id => scalarIdToVectorComponentId(id, 'u')),
|
|
445
|
+
v: this.v.getSamplerIds().map(id => scalarIdToVectorComponentId(id, 'v')),
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
/** @internal */
|
|
449
|
+
getExpressions() {
|
|
450
|
+
const translateVariables = (expr, component) => {
|
|
451
|
+
const matches = expr.match(/_0[a-z]*/g);
|
|
452
|
+
if (matches === null)
|
|
453
|
+
throw `Field expression not found`;
|
|
454
|
+
matches.forEach(m => expr = expr.replace(m, scalarIdToVectorComponentId(m, component)));
|
|
455
|
+
return expr;
|
|
456
|
+
};
|
|
457
|
+
return {
|
|
458
|
+
u: translateVariables(this.u.getExpression(), 'u'),
|
|
459
|
+
v: translateVariables(this.v.getExpression(), 'v'),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/** A class representing a 2D gridded field of vectors */
|
|
464
|
+
class RawVectorField extends ExpressionVectorField {
|
|
465
|
+
/**
|
|
466
|
+
* Create a vector field.
|
|
467
|
+
* @param grid - The grid on which the vector components are defined
|
|
468
|
+
* @param u_ary - The u (east/west) component of the vectors, which should be given as a 1D array in row-major order, with the first element being at the lower-left corner of the grid
|
|
469
|
+
* @param v_ary - The v (north/south) component of the vectors, which should be given as a 1D array in row-major order, with the first element being at the lower-left corner of the grid
|
|
470
|
+
* @param opts - Options for creating the vector field.
|
|
471
|
+
*/
|
|
472
|
+
constructor(grid, u_ary, v_ary, opts) {
|
|
473
|
+
const u = new RawScalarField(grid, u_ary);
|
|
474
|
+
const v = new RawScalarField(grid, v_ary);
|
|
475
|
+
super(u, v, opts);
|
|
476
|
+
this.u_ary = u_ary;
|
|
477
|
+
this.v_ary = v_ary;
|
|
478
|
+
}
|
|
479
|
+
/** @internal */
|
|
480
|
+
getSamplerIds() {
|
|
481
|
+
return { u: ['_u0'], v: ['_v0'] };
|
|
482
|
+
}
|
|
483
|
+
/** @internal */
|
|
484
|
+
getExpressions() {
|
|
485
|
+
return { u: '_u0', v: '_v0' };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
class ComputedVectorField extends ExpressionVectorField {
|
|
141
489
|
}
|
|
142
490
|
/** A class grid of wind profiles */
|
|
143
491
|
class RawProfileField {
|
|
@@ -160,8 +508,14 @@ class RawProfileField {
|
|
|
160
508
|
const v = new Float16Array(this.grid.ni * this.grid.nj).fill(parseFloat('nan'));
|
|
161
509
|
profiles.forEach(prof => {
|
|
162
510
|
const idx = prof.ilon + this.grid.ni * prof.jlat;
|
|
163
|
-
|
|
164
|
-
|
|
511
|
+
if (isStormRelativeWindProfile(prof)) {
|
|
512
|
+
u[idx] = prof.smu;
|
|
513
|
+
v[idx] = prof.smv;
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
u[idx] = 0;
|
|
517
|
+
v[idx] = 0;
|
|
518
|
+
}
|
|
165
519
|
});
|
|
166
520
|
return new RawVectorField(this.grid, u, v, { relative_to: 'grid' });
|
|
167
521
|
}
|
|
@@ -236,4 +590,4 @@ class RawObsField {
|
|
|
236
590
|
return new RawVectorField(this.grid, u_data, v_data, { relative_to: 'earth' });
|
|
237
591
|
}
|
|
238
592
|
}
|
|
239
|
-
export { RawScalarField, RawVectorField, RawProfileField, RawObsField };
|
|
593
|
+
export { RawScalarField, ComputedScalarField, RawVectorField, ComputedVectorField, RawProfileField, RawObsField };
|