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.
Files changed (77) hide show
  1. package/README.md +2 -0
  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 +1 -1
  5. package/dist/983.autumnplot-gl.js.map +1 -1
  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 +6 -4
  10. package/lib/AutumnTypes.js +4 -1
  11. package/lib/Barbs.d.ts +11 -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/ColorBar.d.ts +6 -1
  16. package/lib/ColorBar.js +10 -4
  17. package/lib/Colormap.d.ts +8 -1
  18. package/lib/Colormap.js +24 -1
  19. package/lib/Contour.d.ts +16 -1
  20. package/lib/Contour.js +14 -2
  21. package/lib/{ContourCreator.d.ts → ContourCreator.worker.d.ts} +10 -11
  22. package/lib/{ContourCreator.js → ContourCreator.worker.js} +15 -14
  23. package/lib/Fill.d.ts +29 -11
  24. package/lib/Fill.js +38 -18
  25. package/lib/Hodographs.d.ts +13 -3
  26. package/lib/Hodographs.js +11 -1
  27. package/lib/Map.d.ts +3 -4
  28. package/lib/Map.js +49 -51
  29. package/lib/Paintball.d.ts +13 -5
  30. package/lib/Paintball.js +96 -46
  31. package/lib/PlotComponent.d.ts +8 -3
  32. package/lib/PlotComponent.js +35 -1
  33. package/lib/PlotLayer.worker.js +1 -1
  34. package/lib/RawField.d.ts +221 -27
  35. package/lib/RawField.js +405 -58
  36. package/lib/StationPlot.d.ts +22 -6
  37. package/lib/StationPlot.js +88 -22
  38. package/lib/TextCollection.d.ts +5 -0
  39. package/lib/TextCollection.js +79 -9
  40. package/lib/WasmInterface.d.ts +7 -0
  41. package/lib/WasmInterface.js +11 -0
  42. package/lib/WorkerPool.d.ts +8 -0
  43. package/lib/WorkerPool.js +77 -0
  44. package/lib/cpp/marchingsquares.js +127 -13
  45. package/lib/cpp/marchingsquares.wasm +0 -0
  46. package/lib/cpp/marchingsquares_embind.d.ts +16 -3
  47. package/lib/grids/AutoZoom.d.ts +21 -0
  48. package/lib/grids/AutoZoom.js +63 -0
  49. package/lib/grids/DomainBuffer.d.ts +14 -0
  50. package/lib/grids/DomainBuffer.js +16 -0
  51. package/lib/grids/Geostationary.d.ts +35 -0
  52. package/lib/grids/Geostationary.js +47 -0
  53. package/lib/grids/Grid.d.ts +36 -0
  54. package/lib/grids/Grid.js +12 -0
  55. package/lib/grids/GridCoordinates.d.ts +10 -0
  56. package/lib/grids/GridCoordinates.js +64 -0
  57. package/lib/grids/LambertGrid.d.ts +73 -0
  58. package/lib/grids/LambertGrid.js +92 -0
  59. package/lib/grids/PlateCarreeGrid.d.ts +46 -0
  60. package/lib/grids/PlateCarreeGrid.js +55 -0
  61. package/lib/grids/PlateCarreeRotatedGrid.d.ts +53 -0
  62. package/lib/grids/PlateCarreeRotatedGrid.js +65 -0
  63. package/lib/grids/RadarSweepGrid.d.ts +46 -0
  64. package/lib/grids/RadarSweepGrid.js +74 -0
  65. package/lib/grids/StructuredGrid.d.ts +49 -0
  66. package/lib/grids/StructuredGrid.js +103 -0
  67. package/lib/grids/UnstructuredGrid.d.ts +56 -0
  68. package/lib/grids/UnstructuredGrid.js +102 -0
  69. package/lib/index.d.ts +15 -4
  70. package/lib/index.js +15 -7
  71. package/lib/utils.d.ts +11 -2
  72. package/lib/utils.js +63 -1
  73. package/package.json +3 -2
  74. package/lib/Grid.d.ts +0 -270
  75. package/lib/Grid.js +0 -600
  76. package/lib/ParticleTracer.d.ts +0 -19
  77. 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 { getGLFormatTypeAlignment } from "./PlotComponent";
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
- 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;
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 = (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);
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
- 'width': this.grid.ni, 'height': this.grid.nj, 'image': tex_data,
45
- 'mag_filter': image_mag_filter, 'row_alignment': row_alignment,
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
- function* mapGenerator(gen, func) {
67
- for (const elem of gen) {
68
- yield func(elem);
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.grid.sampleNearestGridPoint(lon, lat, this.data).sample;
220
+ return this.sampleFieldWithCoord(lon, lat).sample;
78
221
  }
79
222
  }
80
- /** A class representing a 2D gridded field of vectors */
81
- 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
+ }
82
283
  /**
83
- * Create a vector field.
84
- * @param grid - The grid on which the vector components are defined
85
- * @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
86
- * @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
87
- * @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`
88
305
  */
89
- 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;
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
- /** @internal */
96
- getTextureData() {
97
- // Need to give float16 data as uint16s to make WebGL happy: https://github.com/petamoriken/float16/issues/105
98
- const raw_u = this.u.data;
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
- const v_image = { 'format': format, 'type': type,
114
- 'width': this.grid.ni, 'height': this.grid.nj, 'image': v_thin,
115
- '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,
116
354
  };
117
- return { u: u_image, v: v_image };
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 new_grid = this.grid.getThinnedGrid(thin_fac, map_max_zoom);
122
- const thin_u = new_grid.thinDataArray(this.grid, this.u.data);
123
- const thin_v = new_grid.thinDataArray(this.grid, this.v.data);
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.grid.sampleNearestGridPoint(lon, lat, this.u.data);
132
- 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);
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 };
@@ -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 { Grid } from "./Grid";
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
- * Whether to draw a halo (outline) around the string
107
- * @default true;
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 Grid, MapType extends MapLikeType, ObsFieldName extends string> extends PlotComponent<MapType> {
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;