autumnplot-gl 4.0.0 → 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 +2 -0
- package/dist/812.autumnplot-gl.js +2 -0
- package/dist/812.autumnplot-gl.js.map +1 -0
- package/dist/983.autumnplot-gl.js +1 -1
- package/dist/983.autumnplot-gl.js.map +1 -1
- 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 +6 -4
- package/lib/AutumnTypes.js +4 -1
- package/lib/Barbs.d.ts +11 -2
- package/lib/Barbs.js +9 -0
- package/lib/BillboardCollection.d.ts +2 -2
- package/lib/BillboardCollection.js +14 -14
- package/lib/ColorBar.d.ts +6 -1
- package/lib/ColorBar.js +10 -4
- package/lib/Colormap.d.ts +8 -1
- package/lib/Colormap.js +24 -1
- package/lib/Contour.d.ts +16 -1
- package/lib/Contour.js +14 -2
- package/lib/{ContourCreator.d.ts → ContourCreator.worker.d.ts} +10 -11
- package/lib/{ContourCreator.js → ContourCreator.worker.js} +15 -14
- package/lib/Fill.d.ts +29 -11
- package/lib/Fill.js +38 -18
- package/lib/Hodographs.d.ts +13 -3
- package/lib/Hodographs.js +11 -1
- package/lib/Map.d.ts +3 -4
- package/lib/Map.js +49 -51
- package/lib/Paintball.d.ts +13 -5
- package/lib/Paintball.js +96 -46
- package/lib/PlotComponent.d.ts +8 -3
- package/lib/PlotComponent.js +35 -1
- package/lib/PlotLayer.worker.js +1 -1
- package/lib/RawField.d.ts +221 -27
- package/lib/RawField.js +405 -58
- package/lib/StationPlot.d.ts +22 -6
- package/lib/StationPlot.js +88 -22
- package/lib/TextCollection.d.ts +5 -0
- package/lib/TextCollection.js +79 -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 +15 -4
- package/lib/index.js +15 -7
- package/lib/utils.d.ts +11 -2
- package/lib/utils.js +63 -1
- package/package.json +3 -2
- package/lib/Grid.d.ts +0 -270
- package/lib/Grid.js +0 -600
- package/lib/ParticleTracer.d.ts +0 -19
- package/lib/ParticleTracer.js +0 -37
package/lib/RawField.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Float16Array } from "@petamoriken/float16";
|
|
2
|
-
import { isStormRelativeWindProfile } from "./AutumnTypes";
|
|
3
|
-
import { contourCreator } from "./ContourCreator";
|
|
2
|
+
import { isContourable, isStormRelativeWindProfile } from "./AutumnTypes";
|
|
4
3
|
import { Cache, getArrayConstructor, zip } from "./utils";
|
|
5
|
-
import {
|
|
4
|
+
import { WGLTexture } from "autumn-wgl";
|
|
5
|
+
import { getContourWorkerPool, getGLFormatTypeAlignment } from "./PlotComponent";
|
|
6
6
|
function getArrayDType(ary) {
|
|
7
7
|
if (ary instanceof Float32Array) {
|
|
8
8
|
return 'float32';
|
|
@@ -10,40 +10,157 @@ function getArrayDType(ary) {
|
|
|
10
10
|
else if (ary instanceof Uint8Array) {
|
|
11
11
|
return 'uint8';
|
|
12
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
|
+
}
|
|
13
25
|
return 'float16';
|
|
14
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
|
+
}
|
|
15
77
|
/** A class representing a raw 2D field of gridded data, such as height or u wind. */
|
|
16
|
-
class RawScalarField {
|
|
78
|
+
class RawScalarField extends ExpressionScalarField {
|
|
17
79
|
/**
|
|
18
80
|
* Create a data field.
|
|
19
81
|
* @param grid - The grid on which the data are defined
|
|
20
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.
|
|
21
83
|
*/
|
|
22
84
|
constructor(grid, data) {
|
|
85
|
+
super();
|
|
23
86
|
this.grid = grid;
|
|
24
87
|
this.data = data;
|
|
25
88
|
if (grid.ni * grid.nj != data.length) {
|
|
26
89
|
throw `Data size (${data.length}) doesn't match the grid dimensions (${grid.ni} x ${grid.nj}; expected ${grid.ni * grid.nj} points)`;
|
|
27
90
|
}
|
|
28
91
|
this.contour_cache = new Cache(async (opts) => {
|
|
29
|
-
|
|
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;
|
|
30
108
|
});
|
|
31
109
|
}
|
|
32
110
|
/** @internal */
|
|
111
|
+
get aryConstructor() {
|
|
112
|
+
return getArrayConstructor(this.data);
|
|
113
|
+
}
|
|
114
|
+
/** @internal */
|
|
115
|
+
get dtypes() {
|
|
116
|
+
return [getArrayDType(this.data)];
|
|
117
|
+
}
|
|
118
|
+
/** @internal */
|
|
33
119
|
getTextureData() {
|
|
34
120
|
// Need to give float16 data as uint16s to make WebGL happy: https://github.com/petamoriken/float16/issues/105
|
|
35
121
|
const raw_data = this.data;
|
|
36
122
|
const raw_data_type = getArrayDType(raw_data);
|
|
37
|
-
const data =
|
|
123
|
+
const data = ['float32', 'uint8', 'uint32', 'uint16', 'int32', 'int16'].includes(raw_data_type) ? raw_data : new Uint16Array(raw_data.buffer);
|
|
38
124
|
return data;
|
|
39
125
|
}
|
|
40
126
|
getWGLTextureSpec(gl, image_mag_filter) {
|
|
41
127
|
const tex_data = this.getTextureData();
|
|
42
128
|
const { format, type, row_alignment } = getGLFormatTypeAlignment(gl, getArrayDType(this.data));
|
|
43
|
-
return { 'format': format, 'type': type,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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';
|
|
47
164
|
}
|
|
48
165
|
/**
|
|
49
166
|
* Get contour data as an object with each contour level being a separate property.
|
|
@@ -54,7 +171,7 @@ class RawScalarField {
|
|
|
54
171
|
return await this.contour_cache.getValue(opts);
|
|
55
172
|
}
|
|
56
173
|
/**
|
|
57
|
-
* 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.
|
|
58
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.
|
|
59
176
|
* @param args - The RawScalarFields to aggregate
|
|
60
177
|
* @returns a new gridded field
|
|
@@ -63,73 +180,255 @@ class RawScalarField {
|
|
|
63
180
|
* wind_speed_field = RawScalarField.aggreateFields(Math.hypot, u_field, v_field);
|
|
64
181
|
*/
|
|
65
182
|
static aggregateFields(func, ...args) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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];
|
|
70
196
|
}
|
|
71
|
-
const arrayType = getArrayConstructor(args[0].data);
|
|
72
|
-
const zipped_args = zip(...args.map(a => a.data));
|
|
73
|
-
const agg_data = new arrayType(mapGenerator(zipped_args, (a) => func(...a)));
|
|
74
|
-
return new RawScalarField(args[0].grid, agg_data);
|
|
75
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
|
+
*/
|
|
76
219
|
sampleField(lon, lat) {
|
|
77
|
-
return this.
|
|
220
|
+
return this.sampleFieldWithCoord(lon, lat).sample;
|
|
78
221
|
}
|
|
79
222
|
}
|
|
80
|
-
|
|
81
|
-
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
|
+
}
|
|
82
283
|
/**
|
|
83
|
-
*
|
|
84
|
-
* @param
|
|
85
|
-
* @param
|
|
86
|
-
* @
|
|
87
|
-
|
|
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`
|
|
88
305
|
*/
|
|
89
|
-
|
|
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;
|
|
90
333
|
opts = opts === undefined ? {} : opts;
|
|
91
|
-
this.u = new RawScalarField(grid, u);
|
|
92
|
-
this.v = new RawScalarField(grid, v);
|
|
93
334
|
this.relative_to = opts.relative_to === undefined ? 'grid' : opts.relative_to;
|
|
94
335
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const raw_v = this.v.data;
|
|
100
|
-
const u_raw_data_type = getArrayDType(raw_u);
|
|
101
|
-
const v_raw_data_type = getArrayDType(raw_u);
|
|
102
|
-
const u = (u_raw_data_type == 'float32' || u_raw_data_type == 'uint8') ? raw_u : new Uint16Array(raw_u.buffer);
|
|
103
|
-
const v = (v_raw_data_type == 'float32' || v_raw_data_type == 'uint8') ? raw_v : new Uint16Array(raw_v.buffer);
|
|
104
|
-
return { u: u, v: v };
|
|
105
|
-
}
|
|
106
|
-
getWGLTextureSpecs(gl, mag_filter) {
|
|
107
|
-
const { u: u_thin, v: v_thin } = this.getTextureData();
|
|
108
|
-
const { format, type, row_alignment } = getGLFormatTypeAlignment(gl, getArrayDType(this.u.data));
|
|
109
|
-
const u_image = { 'format': format, 'type': type,
|
|
110
|
-
'width': this.grid.ni, 'height': this.grid.nj, 'image': u_thin,
|
|
111
|
-
'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,
|
|
112
340
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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,
|
|
116
354
|
};
|
|
117
|
-
|
|
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;
|
|
400
|
+
};
|
|
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);
|
|
118
411
|
}
|
|
119
412
|
/** @internal */
|
|
120
413
|
getThinnedField(thin_fac, map_max_zoom) {
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
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 });
|
|
125
417
|
}
|
|
126
418
|
/** @internal */
|
|
127
419
|
get grid() {
|
|
128
420
|
return this.u.grid;
|
|
129
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
|
+
*/
|
|
130
429
|
sampleField(lon, lat) {
|
|
131
|
-
const u_sample = this.
|
|
132
|
-
const v_sample = this.
|
|
430
|
+
const u_sample = this.u.sampleFieldWithCoord(lon, lat);
|
|
431
|
+
const v_sample = this.v.sampleFieldWithCoord(lon, lat);
|
|
133
432
|
const rot = this.relative_to == 'earth' ? 0 : this.grid.getVectorRotationAtPoint(u_sample.sample_lon, u_sample.sample_lat);
|
|
134
433
|
const mag = Math.hypot(u_sample.sample, v_sample.sample);
|
|
135
434
|
let brg = (Math.PI / 2 - Math.atan2(-v_sample.sample, -u_sample.sample) + rot) * 180 / Math.PI;
|
|
@@ -139,6 +438,54 @@ class RawVectorField {
|
|
|
139
438
|
brg += 360;
|
|
140
439
|
return [brg, mag];
|
|
141
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 {
|
|
142
489
|
}
|
|
143
490
|
/** A class grid of wind profiles */
|
|
144
491
|
class RawProfileField {
|
|
@@ -243,4 +590,4 @@ class RawObsField {
|
|
|
243
590
|
return new RawVectorField(this.grid, u_data, v_data, { relative_to: 'earth' });
|
|
244
591
|
}
|
|
245
592
|
}
|
|
246
|
-
export { RawScalarField, RawVectorField, RawProfileField, RawObsField };
|
|
593
|
+
export { RawScalarField, ComputedScalarField, RawVectorField, ComputedVectorField, RawProfileField, RawObsField };
|
package/lib/StationPlot.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { RenderMethodArg, WebGLAnyRenderingContext } from "./AutumnTypes";
|
|
2
2
|
import { MapLikeType } from "./Map";
|
|
3
3
|
import { PlotComponent } from "./PlotComponent";
|
|
4
|
-
import {
|
|
4
|
+
import { AutoZoomGrid } from "./grids/AutoZoom";
|
|
5
5
|
import { RawObsField } from "./RawField";
|
|
6
|
+
import { ColorMap } from "./Colormap";
|
|
6
7
|
/**
|
|
7
8
|
* Positions around the station plot at which to draw the various elements
|
|
8
9
|
*
|
|
@@ -31,6 +32,11 @@ interface SPNumberConfig {
|
|
|
31
32
|
* @default '#000000'
|
|
32
33
|
*/
|
|
33
34
|
color?: string;
|
|
35
|
+
/**
|
|
36
|
+
* A colormap to use for coloring numeric values
|
|
37
|
+
* @default null
|
|
38
|
+
*/
|
|
39
|
+
cmap?: ColorMap | null;
|
|
34
40
|
/**
|
|
35
41
|
* Whether to draw a halo (outline) around the number
|
|
36
42
|
* @default true;
|
|
@@ -101,11 +107,11 @@ interface SPSymbolConfig {
|
|
|
101
107
|
* The color to use to draw the symbol
|
|
102
108
|
* @default '#000000'
|
|
103
109
|
*/
|
|
104
|
-
color?: string;
|
|
110
|
+
color?: string | ((symbol: SPSymbol | null, category: SPSymbolCategory) => string);
|
|
105
111
|
/**
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
* Whether to draw a halo (outline) around the string
|
|
113
|
+
* @default true;
|
|
114
|
+
*/
|
|
109
115
|
halo?: boolean;
|
|
110
116
|
/**
|
|
111
117
|
* The color to use for the halo (outline)
|
|
@@ -156,8 +162,18 @@ interface StationPlotOptions<ObsFieldName extends string> {
|
|
|
156
162
|
*/
|
|
157
163
|
font_url_template?: string;
|
|
158
164
|
}
|
|
165
|
+
type SPSymbolCategory = 'freezing_rain' | 'sleet' | 'snow' | 'rain' | 'blowing_dust' | 'thunder' | 'fog' | 'none';
|
|
159
166
|
/**
|
|
160
167
|
* Station model plots for observed data
|
|
168
|
+
*
|
|
169
|
+
* ## Grid Compatibility
|
|
170
|
+
* - :white_check_mark: `PlateCarreeGrid`
|
|
171
|
+
* - :white_check_mark: `PlateCarreeRotatedGrid`
|
|
172
|
+
* - :white_check_mark: `LambertGrid`
|
|
173
|
+
* - :white_check_mark: `UnstructuredGrid`
|
|
174
|
+
* - :x: `RadarSweepGrid`
|
|
175
|
+
* - :x: `Geostationary`
|
|
176
|
+
*
|
|
161
177
|
* @example
|
|
162
178
|
* // Specify how to set up the station plot
|
|
163
179
|
* const station_plot_locs = {
|
|
@@ -171,7 +187,7 @@ interface StationPlotOptions<ObsFieldName extends string> {
|
|
|
171
187
|
* // Create the station plot
|
|
172
188
|
* const station_plot = new StationPlot(obs_field, {config: station_plot_locs, thin_fac: 8, font_size: 14});
|
|
173
189
|
*/
|
|
174
|
-
declare class StationPlot<GridType extends
|
|
190
|
+
declare class StationPlot<GridType extends AutoZoomGrid, MapType extends MapLikeType, ObsFieldName extends string> extends PlotComponent<MapType> {
|
|
175
191
|
private field;
|
|
176
192
|
readonly opts: Required<StationPlotOptions<ObsFieldName>>;
|
|
177
193
|
private gl_elems;
|