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.
Files changed (84) hide show
  1. package/README.md +13 -207
  2. package/dist/812.autumnplot-gl.js +2 -0
  3. package/dist/812.autumnplot-gl.js.map +1 -0
  4. package/dist/983.autumnplot-gl.js +2 -0
  5. package/dist/983.autumnplot-gl.js.map +1 -0
  6. package/dist/autumnplot-gl.js +1 -1
  7. package/dist/autumnplot-gl.js.map +1 -1
  8. package/dist/marchingsquares.wasm +0 -0
  9. package/lib/AutumnTypes.d.ts +38 -5
  10. package/lib/AutumnTypes.js +7 -1
  11. package/lib/Barbs.d.ts +12 -2
  12. package/lib/Barbs.js +9 -0
  13. package/lib/BillboardCollection.d.ts +2 -2
  14. package/lib/BillboardCollection.js +14 -14
  15. package/lib/Color.d.ts +1 -0
  16. package/lib/Color.js +1 -0
  17. package/lib/ColorBar.d.ts +14 -0
  18. package/lib/ColorBar.js +15 -8
  19. package/lib/Colormap.d.ts +9 -1
  20. package/lib/Colormap.js +24 -1
  21. package/lib/Contour.d.ts +26 -1
  22. package/lib/Contour.js +24 -2
  23. package/lib/ContourCreator.worker.d.ts +25 -0
  24. package/lib/{ContourCreator.js → ContourCreator.worker.js} +15 -14
  25. package/lib/Fill.d.ts +31 -11
  26. package/lib/Fill.js +38 -18
  27. package/lib/Hodographs.d.ts +19 -3
  28. package/lib/Hodographs.js +45 -20
  29. package/lib/Map.d.ts +13 -1
  30. package/lib/Map.js +62 -8
  31. package/lib/Paintball.d.ts +14 -5
  32. package/lib/Paintball.js +96 -46
  33. package/lib/PlotComponent.d.ts +9 -3
  34. package/lib/PlotComponent.js +36 -1
  35. package/lib/PlotLayer.d.ts +2 -2
  36. package/lib/PlotLayer.js +2 -2
  37. package/lib/PlotLayer.worker.js +9 -3
  38. package/lib/RawField.d.ts +223 -27
  39. package/lib/RawField.js +413 -59
  40. package/lib/StationPlot.d.ts +78 -11
  41. package/lib/StationPlot.js +113 -30
  42. package/lib/TextCollection.d.ts +5 -0
  43. package/lib/TextCollection.js +82 -9
  44. package/lib/WasmInterface.d.ts +7 -0
  45. package/lib/WasmInterface.js +11 -0
  46. package/lib/WorkerPool.d.ts +8 -0
  47. package/lib/WorkerPool.js +77 -0
  48. package/lib/cpp/marchingsquares.js +127 -13
  49. package/lib/cpp/marchingsquares.wasm +0 -0
  50. package/lib/cpp/marchingsquares_embind.d.ts +16 -3
  51. package/lib/grids/AutoZoom.d.ts +21 -0
  52. package/lib/grids/AutoZoom.js +63 -0
  53. package/lib/grids/DomainBuffer.d.ts +14 -0
  54. package/lib/grids/DomainBuffer.js +16 -0
  55. package/lib/grids/Geostationary.d.ts +35 -0
  56. package/lib/grids/Geostationary.js +47 -0
  57. package/lib/grids/Grid.d.ts +36 -0
  58. package/lib/grids/Grid.js +12 -0
  59. package/lib/grids/GridCoordinates.d.ts +10 -0
  60. package/lib/grids/GridCoordinates.js +64 -0
  61. package/lib/grids/LambertGrid.d.ts +73 -0
  62. package/lib/grids/LambertGrid.js +92 -0
  63. package/lib/grids/PlateCarreeGrid.d.ts +46 -0
  64. package/lib/grids/PlateCarreeGrid.js +55 -0
  65. package/lib/grids/PlateCarreeRotatedGrid.d.ts +53 -0
  66. package/lib/grids/PlateCarreeRotatedGrid.js +65 -0
  67. package/lib/grids/RadarSweepGrid.d.ts +46 -0
  68. package/lib/grids/RadarSweepGrid.js +74 -0
  69. package/lib/grids/StructuredGrid.d.ts +49 -0
  70. package/lib/grids/StructuredGrid.js +103 -0
  71. package/lib/grids/UnstructuredGrid.d.ts +56 -0
  72. package/lib/grids/UnstructuredGrid.js +102 -0
  73. package/lib/index.d.ts +23 -6
  74. package/lib/index.js +18 -8
  75. package/lib/utils.d.ts +11 -2
  76. package/lib/utils.js +63 -1
  77. package/package.json +4 -3
  78. package/dist/110.autumnplot-gl.js +0 -2
  79. package/dist/110.autumnplot-gl.js.map +0 -1
  80. package/lib/ContourCreator.d.ts +0 -22
  81. package/lib/Grid.d.ts +0 -263
  82. package/lib/Grid.js +0 -547
  83. package/lib/ParticleTracer.d.ts +0 -19
  84. package/lib/ParticleTracer.js +0 -37
package/lib/RawField.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { Float16Array } from "@petamoriken/float16";
2
- import { contourCreator } from "./ContourCreator";
2
+ import { isContourable, isStormRelativeWindProfile } from "./AutumnTypes";
3
3
  import { Cache, getArrayConstructor, zip } from "./utils";
4
- import { getGLFormatTypeAlignment } from "./PlotComponent";
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
- return await contourCreator(this.data, this.grid, opts);
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 = (raw_data_type == 'float32' || raw_data_type == 'uint8') ? raw_data : new Uint16Array(raw_data.buffer);
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
- 'width': this.grid.ni, 'height': this.grid.nj, 'image': tex_data,
44
- 'mag_filter': image_mag_filter, 'row_alignment': row_alignment,
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
- function* mapGenerator(gen, func) {
66
- for (const elem of gen) {
67
- yield func(elem);
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.grid.sampleNearestGridPoint(lon, lat, this.data).sample;
220
+ return this.sampleFieldWithCoord(lon, lat).sample;
77
221
  }
78
222
  }
79
- /** A class representing a 2D gridded field of vectors */
80
- class RawVectorField {
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
- * Create a vector field.
83
- * @param grid - The grid on which the vector components are defined
84
- * @param u - 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
85
- * @param v - 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
86
- * @param opts - Options for creating the vector field.
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
- constructor(grid, u, v, opts) {
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
- /** @internal */
95
- getTextureData() {
96
- // Need to give float16 data as uint16s to make WebGL happy: https://github.com/petamoriken/float16/issues/105
97
- const raw_u = this.u.data;
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
- const v_image = { 'format': format, 'type': type,
113
- 'width': this.grid.ni, 'height': this.grid.nj, 'image': v_thin,
114
- 'mag_filter': mag_filter, 'row_alignment': row_alignment,
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
- return { u: u_image, v: v_image };
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 new_grid = this.grid.getThinnedGrid(thin_fac, map_max_zoom);
121
- const thin_u = new_grid.thinDataArray(this.grid, this.u.data);
122
- const thin_v = new_grid.thinDataArray(this.grid, this.v.data);
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.grid.sampleNearestGridPoint(lon, lat, this.u.data);
131
- const v_sample = this.grid.sampleNearestGridPoint(lon, lat, this.v.data);
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
- u[idx] = prof.smu;
164
- v[idx] = prof.smv;
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 };