autumnplot-gl 3.1.0 → 4.0.0-beta

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 (56) hide show
  1. package/README.md +6 -11
  2. package/dist/110.autumnplot-gl.js +1 -1
  3. package/dist/110.autumnplot-gl.js.map +1 -1
  4. package/dist/autumnplot-gl.js +1 -1
  5. package/dist/autumnplot-gl.js.map +1 -1
  6. package/dist/marchingsquares.wasm +0 -0
  7. package/lib/AutumnTypes.d.ts +53 -5
  8. package/lib/AutumnTypes.js +25 -1
  9. package/lib/Barbs.d.ts +23 -6
  10. package/lib/Barbs.js +20 -18
  11. package/lib/BillboardCollection.d.ts +16 -8
  12. package/lib/BillboardCollection.js +107 -59
  13. package/lib/Color.d.ts +57 -0
  14. package/lib/Color.js +163 -0
  15. package/lib/ColorBar.d.ts +12 -1
  16. package/lib/ColorBar.js +9 -7
  17. package/lib/Colormap.d.ts +19 -18
  18. package/lib/Colormap.js +84 -23
  19. package/lib/Contour.d.ts +42 -11
  20. package/lib/Contour.js +67 -58
  21. package/lib/ContourCreator.d.ts +4 -0
  22. package/lib/ContourCreator.js +2 -1
  23. package/lib/Fill.d.ts +27 -16
  24. package/lib/Fill.js +105 -83
  25. package/lib/Grid.d.ts +125 -29
  26. package/lib/Grid.js +303 -95
  27. package/lib/Hodographs.d.ts +24 -6
  28. package/lib/Hodographs.js +28 -24
  29. package/lib/Map.js +1 -1
  30. package/lib/Paintball.d.ts +6 -5
  31. package/lib/Paintball.js +38 -32
  32. package/lib/ParticleTracer.d.ts +19 -0
  33. package/lib/ParticleTracer.js +37 -0
  34. package/lib/PlotComponent.d.ts +6 -7
  35. package/lib/PlotComponent.js +17 -7
  36. package/lib/PlotLayer.d.ts +4 -4
  37. package/lib/PlotLayer.worker.d.ts +1 -2
  38. package/lib/PlotLayer.worker.js +22 -57
  39. package/lib/PolylineCollection.d.ts +18 -9
  40. package/lib/PolylineCollection.js +124 -89
  41. package/lib/RawField.d.ts +76 -23
  42. package/lib/RawField.js +138 -29
  43. package/lib/ShaderManager.d.ts +12 -0
  44. package/lib/ShaderManager.js +58 -0
  45. package/lib/StationPlot.d.ts +145 -0
  46. package/lib/StationPlot.js +205 -0
  47. package/lib/TextCollection.d.ts +12 -8
  48. package/lib/TextCollection.js +113 -71
  49. package/lib/cpp/marchingsquares.js +483 -585
  50. package/lib/cpp/marchingsquares.wasm +0 -0
  51. package/lib/cpp/marchingsquares_embind.d.ts +23 -3
  52. package/lib/index.d.ts +7 -4
  53. package/lib/index.js +5 -3
  54. package/lib/utils.d.ts +5 -8
  55. package/lib/utils.js +12 -83
  56. package/package.json +2 -2
package/lib/RawField.js CHANGED
@@ -1,8 +1,15 @@
1
1
  import { Float16Array } from "@petamoriken/float16";
2
2
  import { contourCreator } from "./ContourCreator";
3
- import { Cache, zip } from "./utils";
4
- function getArrayConstructor(ary) {
5
- return ary.constructor;
3
+ import { Cache, getArrayConstructor, zip } from "./utils";
4
+ import { getGLFormatTypeAlignment } from "./PlotComponent";
5
+ function getArrayDType(ary) {
6
+ if (ary instanceof Float32Array) {
7
+ return 'float32';
8
+ }
9
+ else if (ary instanceof Uint8Array) {
10
+ return 'uint8';
11
+ }
12
+ return 'float16';
6
13
  }
7
14
  /** A class representing a raw 2D field of gridded data, such as height or u wind. */
8
15
  class RawScalarField {
@@ -25,15 +32,23 @@ class RawScalarField {
25
32
  getTextureData() {
26
33
  // Need to give float16 data as uint16s to make WebGL happy: https://github.com/petamoriken/float16/issues/105
27
34
  const raw_data = this.data;
28
- let data;
29
- if (raw_data instanceof Float32Array) {
30
- data = raw_data;
31
- }
32
- else {
33
- data = new Uint16Array(raw_data.buffer);
34
- }
35
+ 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);
35
37
  return data;
36
38
  }
39
+ getWGLTextureSpec(gl, image_mag_filter) {
40
+ const tex_data = this.getTextureData();
41
+ 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
+ };
46
+ }
47
+ /**
48
+ * Get contour data as an object with each contour level being a separate property.
49
+ * @param opts - Options for doing the contouring
50
+ * @returns contour data as an object
51
+ */
37
52
  async getContours(opts) {
38
53
  return await this.contour_cache.getValue(opts);
39
54
  }
@@ -57,6 +72,9 @@ class RawScalarField {
57
72
  const agg_data = new arrayType(mapGenerator(zipped_args, (a) => func(...a)));
58
73
  return new RawScalarField(args[0].grid, agg_data);
59
74
  }
75
+ sampleField(lon, lat) {
76
+ return this.grid.sampleNearestGridPoint(lon, lat, this.data).sample;
77
+ }
60
78
  }
61
79
  /** A class representing a 2D gridded field of vectors */
62
80
  class RawVectorField {
@@ -73,35 +91,53 @@ class RawVectorField {
73
91
  this.v = new RawScalarField(grid, v);
74
92
  this.relative_to = opts.relative_to === undefined ? 'grid' : opts.relative_to;
75
93
  }
94
+ /** @internal */
76
95
  getTextureData() {
77
96
  // Need to give float16 data as uint16s to make WebGL happy: https://github.com/petamoriken/float16/issues/105
78
97
  const raw_u = this.u.data;
79
98
  const raw_v = this.v.data;
80
- const u = raw_u instanceof Float32Array ? raw_u : new Uint16Array(raw_u.buffer);
81
- const v = raw_v instanceof Float32Array ? raw_v : new Uint16Array(raw_v.buffer);
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);
82
103
  return { u: u, v: v };
83
104
  }
84
- getThinnedField(thin_x, thin_y) {
85
- const new_grid = this.grid.getThinnedGrid(thin_x, thin_y);
86
- const thinGrid = (data) => {
87
- const arrayType = getArrayConstructor(data);
88
- const new_data = new arrayType(new_grid.ni * new_grid.nj);
89
- for (let i = 0; i < new_grid.ni; i++) {
90
- for (let j = 0; j < new_grid.nj; j++) {
91
- const idx_old = i * thin_x + this.grid.ni * j * thin_y;
92
- const idx = i + new_grid.ni * j;
93
- new_data[idx] = data[idx_old];
94
- }
95
- }
96
- return new_data;
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,
111
+ };
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,
97
115
  };
98
- const thin_u = thinGrid(this.u.data);
99
- const thin_v = thinGrid(this.v.data);
116
+ return { u: u_image, v: v_image };
117
+ }
118
+ /** @internal */
119
+ 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);
100
123
  return new RawVectorField(new_grid, thin_u, thin_v, { relative_to: this.relative_to });
101
124
  }
125
+ /** @internal */
102
126
  get grid() {
103
127
  return this.u.grid;
104
128
  }
129
+ 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);
132
+ const rot = this.relative_to == 'earth' ? 0 : this.grid.getVectorRotationAtPoint(u_sample.sample_lon, u_sample.sample_lat);
133
+ const mag = Math.hypot(u_sample.sample, v_sample.sample);
134
+ let brg = (Math.PI / 2 - Math.atan2(-v_sample.sample, -u_sample.sample) + rot) * 180 / Math.PI;
135
+ if (brg > 360)
136
+ brg -= 360;
137
+ if (brg < 0)
138
+ brg += 360;
139
+ return [brg, mag];
140
+ }
105
141
  }
106
142
  /** A class grid of wind profiles */
107
143
  class RawProfileField {
@@ -114,7 +150,10 @@ class RawProfileField {
114
150
  this.profiles = profiles;
115
151
  this.grid = grid;
116
152
  }
117
- /** Get the gridded storm motion vector field (internal method) */
153
+ /**
154
+ * @internal
155
+ * Get the gridded storm motion vector field
156
+ */
118
157
  getStormMotionGrid() {
119
158
  const profiles = this.profiles;
120
159
  const u = new Float16Array(this.grid.ni * this.grid.nj).fill(parseFloat('nan'));
@@ -126,5 +165,75 @@ class RawProfileField {
126
165
  });
127
166
  return new RawVectorField(this.grid, u, v, { relative_to: 'grid' });
128
167
  }
168
+ /** @internal */
169
+ getProfileCoords() {
170
+ const { lats, lons } = this.grid.getEarthCoords();
171
+ const prof_lats = new Float32Array(this.profiles.length);
172
+ const prof_lons = new Float32Array(this.profiles.length);
173
+ this.profiles.forEach((prof, iprof) => {
174
+ const idx = prof.ilon + prof.jlat * this.grid.ni;
175
+ prof_lats[iprof] = lats[idx];
176
+ prof_lons[iprof] = lons[idx];
177
+ });
178
+ return { lats: prof_lats, lons: prof_lons };
179
+ }
180
+ }
181
+ /** Raw observation data, given as a list of objects */
182
+ class RawObsField {
183
+ /**
184
+ * Create a field of observations
185
+ * @param grid - The grid on which the obs are defined (can be either a structured or unstructured grid)
186
+ * @param data - The observation data. Conceptually, obs are given as a list of individual observations.
187
+ */
188
+ constructor(grid, data) {
189
+ this.grid = grid;
190
+ this.data = data;
191
+ }
192
+ /**
193
+ * @internal
194
+ * Get observation element as a list of scalar numbers
195
+ */
196
+ getScalar(key) {
197
+ const field_data = this.data.map(d => d[key]);
198
+ if (!field_data.map(d => typeof d == 'number' || d === null).reduce((a, b) => a && b, true))
199
+ throw `It doesn't look like ${key} contains scalar numerical data`;
200
+ return field_data;
201
+ }
202
+ /**
203
+ * @internal
204
+ * Get observed element as a list of strings (internal method)
205
+ */
206
+ getStrings(key) {
207
+ const field_data = this.data.map(d => d[key]);
208
+ if (!field_data.map(d => typeof d == 'string' || d === null).reduce((a, b) => a && b, true))
209
+ throw `It doesn't look like ${key} contains string data`;
210
+ return field_data;
211
+ }
212
+ /**
213
+ * @internal
214
+ * Get observed element as a list of vectors (internal method)
215
+ */
216
+ getVector(key) {
217
+ const field_data = this.data.map(d => d[key]);
218
+ if (!field_data.map(d => Array.isArray(d)).reduce((a, b) => a && b, true))
219
+ throw `It doesn't look like ${key} contains vector data`;
220
+ const vector_field_data = field_data;
221
+ const vec2comp = (wspd, wdir) => {
222
+ const u = -wspd * Math.sin(wdir * Math.PI / 180);
223
+ const v = -wspd * Math.cos(wdir * Math.PI / 180);
224
+ return [u, v];
225
+ };
226
+ const u_data = new Float16Array(this.grid.ni * this.grid.nj).fill(parseFloat('nan'));
227
+ const v_data = new Float16Array(this.grid.ni * this.grid.nj).fill(parseFloat('nan'));
228
+ vector_field_data.forEach(([wspd, wdir], idat) => {
229
+ if (wspd === null || wdir === null) {
230
+ return;
231
+ }
232
+ const [u, v] = vec2comp(wspd, wdir);
233
+ u_data[idat] = u;
234
+ v_data[idat] = v;
235
+ });
236
+ return new RawVectorField(this.grid, u_data, v_data, { relative_to: 'earth' });
237
+ }
129
238
  }
130
- export { RawScalarField, RawVectorField, RawProfileField };
239
+ export { RawScalarField, RawVectorField, RawProfileField, RawObsField };
@@ -0,0 +1,12 @@
1
+ import { WGLProgram } from "autumn-wgl";
2
+ import { RenderShaderData, RendererData, WebGLAnyRenderingContext } from "./AutumnTypes";
3
+ declare class ShaderProgramManager {
4
+ readonly vertex_shader_src: string;
5
+ readonly fragment_shader_src: string;
6
+ readonly shader_defines: string[];
7
+ private readonly program_map;
8
+ constructor(vertex_shader_src: string, fragment_shader_src: string, shader_defines: string[]);
9
+ getShaderProgram(gl: WebGLAnyRenderingContext, shader_data: RenderShaderData | null): WGLProgram;
10
+ getShaderUniforms(render_data: RendererData): Record<string, number | number[]>;
11
+ }
12
+ export { ShaderProgramManager };
@@ -0,0 +1,58 @@
1
+ import { WGLProgram } from "autumn-wgl";
2
+ import { mergeShaderCode } from "./utils";
3
+ class ShaderProgramManager {
4
+ constructor(vertex_shader_src, fragment_shader_src, shader_defines) {
5
+ this.vertex_shader_src = vertex_shader_src;
6
+ this.fragment_shader_src = fragment_shader_src;
7
+ this.shader_defines = shader_defines;
8
+ this.program_map = new Map();
9
+ }
10
+ getShaderProgram(gl, shader_data) {
11
+ const shader_key = shader_data === null ? 'default' : shader_data.variantName;
12
+ const cached_program = this.program_map.get(shader_key);
13
+ if (cached_program !== undefined) {
14
+ return cached_program;
15
+ }
16
+ let vertex_src = this.vertex_shader_src;
17
+ if (shader_data !== null) {
18
+ vertex_src = mergeShaderCode(shader_data.vertexShaderPrelude + '\n' + shader_data.define, vertex_src);
19
+ }
20
+ else {
21
+ const prelude = `
22
+ uniform mat4 u_projection_matrix;
23
+
24
+ vec4 projectTile(vec2 p) {
25
+ vec4 result = u_projection_matrix * vec4(p, 0.0, 1.0);
26
+ return result;
27
+ }`;
28
+ vertex_src = mergeShaderCode(prelude, vertex_src);
29
+ }
30
+ const program = new WGLProgram(gl, vertex_src, this.fragment_shader_src, { define: this.shader_defines });
31
+ this.program_map.set(shader_key, program);
32
+ return program;
33
+ }
34
+ getShaderUniforms(render_data) {
35
+ if (render_data.type == 'maplibre') {
36
+ if (render_data.shaderData.define.includes('GLOBE')) {
37
+ return {
38
+ 'u_projection_matrix': render_data.defaultProjectionData.mainMatrix,
39
+ 'u_projection_fallback_matrix': render_data.defaultProjectionData.fallbackMatrix,
40
+ 'u_projection_tile_mercator_coords': render_data.defaultProjectionData.tileMercatorCoords,
41
+ 'u_projection_clipping_plane': render_data.defaultProjectionData.clippingPlane,
42
+ 'u_projection_transition': render_data.defaultProjectionData.projectionTransition
43
+ };
44
+ }
45
+ else {
46
+ return {
47
+ 'u_projection_matrix': render_data.defaultProjectionData.mainMatrix,
48
+ };
49
+ }
50
+ }
51
+ else {
52
+ return {
53
+ 'u_projection_matrix': render_data.mainMatrix,
54
+ };
55
+ }
56
+ }
57
+ }
58
+ export { ShaderProgramManager };
@@ -0,0 +1,145 @@
1
+ import { RenderMethodArg, WebGLAnyRenderingContext } from "./AutumnTypes";
2
+ import { MapLikeType } from "./Map";
3
+ import { PlotComponent } from "./PlotComponent";
4
+ import { Grid } from "./Grid";
5
+ import { RawObsField } from "./RawField";
6
+ /**
7
+ * Positions around the station plot at which to draw the various elements
8
+ *
9
+ * | Value | Position |
10
+ * | --------|--------------|
11
+ * | `'cl'` | center-left |
12
+ * | `'ll'` | lower-left |
13
+ * | `'lc'` | lower-center |
14
+ * | `'lr'` | lower-right |
15
+ * | `'cr'` | center-right |
16
+ * | `'ur'` | upper-right |
17
+ * | `'uc'` | upper-center |
18
+ * | `'ul'` | upper-left |
19
+ * | `'c'` | center |
20
+ */
21
+ type SPPosition = 'cl' | 'll' | 'lc' | 'lr' | 'cr' | 'ur' | 'uc' | 'ul' | 'c';
22
+ interface SPNumberConfig {
23
+ type: 'number';
24
+ /**
25
+ * The position on the station plot at which to place the number
26
+ */
27
+ pos: SPPosition;
28
+ /**
29
+ * The color to use to draw the number
30
+ * @default '#000000'
31
+ */
32
+ color?: string;
33
+ /**
34
+ * A function that properly formats the number for display
35
+ * @example (val) => val === null ? '' : val.toFixed(0)
36
+ * @param val - The number to format
37
+ * @returns A string containing the formatted nubmer
38
+ */
39
+ formatter?: (val: number | null) => string;
40
+ }
41
+ interface SPStringConfig {
42
+ type: 'string';
43
+ /**
44
+ * The position on the station plot at which to place the number
45
+ */
46
+ pos: SPPosition;
47
+ /**
48
+ * The color to use to draw the number
49
+ * @default '#000000'
50
+ */
51
+ color?: string;
52
+ }
53
+ interface SPBarbConfig {
54
+ type: 'barb';
55
+ /**
56
+ * The color to use to draw the number
57
+ * @default '#000000'
58
+ */
59
+ color?: string;
60
+ /**
61
+ * A multiplier for the barb size
62
+ * @default 1
63
+ */
64
+ barb_size_multipler?: number;
65
+ }
66
+ /**
67
+ * Accepted symbol codes for sky cover and present weather symbols
68
+ */
69
+ type SPSymbol = ('0/8' | '1/8' | '2/8' | '3/8' | '4/8' | '5/8' | '6/8' | '7/8' | '8/8' | 'clr' | 'few' | 'sct' | 'bkn' | 'ovc' | 'obsc' | 'va' | 'fu' | 'hz' | 'du' | 'bldu' | 'sa' | 'blsa' | 'vcblsa' | 'vcbldu' | 'blpy' | 'po' | 'vcpo' | 'vcds' | 'vcss' | 'br' | 'bcbr' | 'bc' | 'mifg' | 'vcts' | 'virga' | 'vcsh' | 'ts' | 'thdr' | 'vctshz' | 'tsfzfg' | 'tsbr' | 'tsdz' | 'vctsup' | '-tsup' | 'tsup' | '+tsup' | 'sq' | 'fc' | '+fc' | 'ds' | 'ss' | 'drsa' | 'drdu' | '+ds' | '+ss' | 'drsn' | '+drsn' | '-blsn' | 'blsn' | '+blsn' | 'vcblsn' | 'vcfg' | 'bcfg' | 'prfg' | 'fg' | 'fzfg' | '-vctsdz' | '-dz' | '-dzbr' | 'vctsdz' | 'dz' | '+vctsdz' | '+dz' | '-fzdz' | '-fzdzsn' | 'fzdz' | '+fzdz' | 'fzdzsn' | '-dzra' | 'dzra' | '+dzra' | '-ra' | '-rabr' | 'ra' | 'rabr' | 'rafg' | 'vcra' | '+ra' | '-fzra' | '-fzrasn' | '-fzrabr' | '-fzrapl' | '-fzrasnpl' | 'tsfzrapl' | '-tsfzra' | 'fzra' | '+fzra' | 'fzrasn' | 'tsfzra' | '-dzsn' | '-rasn' | '-snra' | '-sndz' | 'rasn' | '+rasn' | 'snra' | 'dzsn' | 'sndz' | '+dzsn' | '+sndz' | '-sn' | '-snbr' | 'sn' | '+sn' | '-snsg' | 'sg' | '-sg' | 'ic' | '-fzdzpl' | '-fzdzplsn' | 'fzdzpl' | '-fzraplsn' | 'fzrapl' | '+fzrapl' | '-rapl' | '-rasnpl' | '-raplsn' | '+rapl' | 'rapl' | '-snpl' | 'snpl' | '-pl' | 'pl' | '-plsn' | '-plra' | 'plra' | '-pldz' | '+pl' | 'plsn' | 'plup' | '+plsn' | '-sh' | '-shra' | 'sh' | 'shra' | '+sh' | '+shra' | '-shrasn' | '-shsnra' | '+shrabr' | 'shrasn' | '+shrasn' | 'shsnra' | '+shsnra' | '-shsn' | 'shsn' | '+shsn' | '-gs' | '-shgs' | 'fzraplgs' | '-sngs' | 'gsplsn' | 'gspl' | 'plgssn' | 'gs' | 'shgs' | '+gs' | '+shgs' | '-gr' | '-shgr' | '-sngr' | 'gr' | 'shgr' | '+gr' | '+shgr' | '-tsrasn' | 'tsrasn' | '-tssnra' | 'tssnra' | '-vctsra' | '-tsra' | 'tsra' | '-tsdz' | 'vctsra' | 'tspl' | '-tssn' | '-tspl' | 'tssn' | '-vctssn' | 'vctssn' | 'tsplsn' | 'tssnpl' | '-tssnpl' | '-tsragr' | 'tsrags' | 'tsragr' | 'tsgs' | 'tsgr' | '+tsfzrapl' | '+vctsra' | '+tsra' | '+tsfzra' | '+tssn' | '+tspl' | '+tsplsn' | '+vctssn' | 'tssa' | 'tsds' | 'tsdu' | '+tsgs' | '+tsgr' | '+tsrags' | '+tsragr' | 'in' | '-up' | 'up' | '+up' | '-fzup' | 'fzup' | '+fzup');
70
+ interface SPSymbolConfig {
71
+ type: 'symbol';
72
+ /**
73
+ * The position on the station plot at which to place the number
74
+ */
75
+ pos: SPPosition;
76
+ /**
77
+ * The color to use to draw the number
78
+ * @default '#000000'
79
+ */
80
+ color?: string;
81
+ }
82
+ type SPConfig = SPNumberConfig | SPStringConfig | SPBarbConfig | SPSymbolConfig;
83
+ /**
84
+ * Configuration for station data plots
85
+ * @example
86
+ * spconfig : SPDataConfig<'id' | 'tmpf' | 'wind' | 'skyc'> = {
87
+ * // Add a string to the station plot (like the station ID)
88
+ * id: {type: 'string', pos: 'lr'},
89
+ *
90
+ * // Add a number to the station plot (like the temperature)
91
+ * tmpf: {type: 'number', pos: 'ul', color: '#cc0000', formatter: val => val === null ? '' : val.toFixed(0)},
92
+ *
93
+ * // Add a barb to the station plot
94
+ * wind: {type: 'barb', pos: 'c'},
95
+ *
96
+ * // Add a symbol to the station plot
97
+ * skyc: {type: 'symbol', pos: 'c'},
98
+ * }
99
+ */
100
+ type SPDataConfig<ObsFieldName extends string> = Record<ObsFieldName, SPConfig>;
101
+ interface StationPlotOptions<ObsFieldName extends string> {
102
+ config: SPDataConfig<ObsFieldName>;
103
+ /**
104
+ * Thin factor at zoom level 1 for the station plots. Should be a power of 2.
105
+ * @default 1
106
+ */
107
+ thin_fac?: number;
108
+ /**
109
+ * Font face to use for plotting text
110
+ * @default 'Trebuchet MS'
111
+ */
112
+ font_face?: string;
113
+ /**
114
+ * Size of the font to use for the text
115
+ * @default 12
116
+ */
117
+ font_size?: number;
118
+ /**
119
+ * URL template to use in retrieving the font data for the text. The default is to use the template from the map style.
120
+ */
121
+ font_url_template?: string;
122
+ }
123
+ declare class StationPlot<GridType extends Grid, MapType extends MapLikeType, ObsFieldName extends string> extends PlotComponent<MapType> {
124
+ private field;
125
+ readonly opts: Required<StationPlotOptions<ObsFieldName>>;
126
+ private gl_elems;
127
+ private text_components;
128
+ /**
129
+ *
130
+ * @param field
131
+ * @param opts
132
+ */
133
+ constructor(field: RawObsField<GridType, ObsFieldName>, opts: StationPlotOptions<ObsFieldName>);
134
+ /**
135
+ * Update the data displayed as station plots
136
+ * @param field - The new field to display as station plots
137
+ */
138
+ updateField(field: RawObsField<GridType, ObsFieldName>): Promise<void>;
139
+ /** @internal */
140
+ onAdd(map: MapType, gl: WebGLAnyRenderingContext): Promise<void>;
141
+ /** @internal */
142
+ render(gl: WebGLAnyRenderingContext, arg: RenderMethodArg): void;
143
+ }
144
+ export default StationPlot;
145
+ export type { StationPlotOptions, SPPosition, SPNumberConfig, SPStringConfig, SPBarbConfig, SPSymbolConfig, SPConfig, SPDataConfig, SPSymbol };
@@ -0,0 +1,205 @@
1
+ import { PlotComponent } from "./PlotComponent";
2
+ import { normalizeOptions } from "./utils";
3
+ import { TextCollection } from "./TextCollection";
4
+ import { Color } from "./Color";
5
+ import Barbs from "./Barbs";
6
+ const SYMBOLS = {
7
+ // Sky cover symbols
8
+ '0/8': 59658, '1/8': 59659, '2/8': 59660, '3/8': 59661, '4/8': 59662, '5/8': 59663, '6/8': 59664, '7/8': 59665, '8/8': 59666,
9
+ 'clr': 59658, 'few': 59660, 'sct': 59662, 'bkn': 59664, 'ovc': 59666, 'obsc': 59667,
10
+ // Present weather symbols
11
+ 'va': 59810, 'fu': 59810, 'hz': 59811, 'du': 59812, 'bldu': 59814, 'sa': 59814, 'blsa': 59814, 'vcblsa': 59814, 'vcbldu': 59814, 'blpy': 59814,
12
+ 'po': 59816, 'vcpo': 59816, 'vcds': 59817, 'vcss': 59817,
13
+ 'br': 59818, 'bcbr': 59818, 'bc': 59819, 'mifg': 59820,
14
+ 'vcts': 59821, 'virga': 59822, 'vcsh': 59824, 'ts': 59825, 'thdr': 59825, 'vctshz': 59825,
15
+ 'tsfzfg': 59825, 'tsbr': 59825, 'tsdz': 59825, 'vctsup': 59825,
16
+ '-tsup': 59825, 'tsup': 59825, '+tsup': 59825,
17
+ 'sq': 59826, 'fc': 59827, '+fc': 59827,
18
+ 'ds': 59839, 'ss': 59839, 'drsa': 59839, 'drdu': 59839, '+ds': 59842, '+ss': 59842,
19
+ 'drsn': 59844, '+drsn': 59845, '-blsn': 59846, 'blsn': 59846, '+blsn': 59847, 'vcblsn': 59846,
20
+ 'vcfg': 59848, 'bcfg': 59849, 'prfg': 59852, 'fg': 59853, 'fzfg': 59857,
21
+ '-vctsdz': 59859, '-dz': 59859, '-dzbr': 59859, 'vctsdz': 59861, 'dz': 59861, '+vctsdz': 59863, '+dz': 59863,
22
+ '-fzdz': 59864, '-fzdzsn': 59864, 'fzdz': 59865, '+fzdz': 59865, 'fzdzsn': 59865,
23
+ '-dzra': 59866, 'dzra': 59867, '+dzra': 59867, '-ra': 59869, '-rabr': 59869, 'ra': 59871, 'rabr': 59871, 'rafg': 59871, 'vcra': 59871, '+ra': 59873,
24
+ '-fzra': 59874, '-fzrasn': 59874, '-fzrabr': 59874, '-fzrapl': 59874, '-fzrasnpl': 59874, 'tsfzrapl': 59875, '-tsfzra': 59875,
25
+ 'fzra': 59875, '+fzra': 59875, 'fzrasn': 59875, 'tsfzra': 59875,
26
+ '-dzsn': 59876, '-rasn': 59876, '-snra': 59876, '-sndz': 59876, 'rasn': 59877, '+rasn': 59877, 'snra': 59877, 'dzsn': 59877, 'sndz': 59877, '+dzsn': 59877, '+sndz': 59877,
27
+ '-sn': 59879, '-snbr': 59879, 'sn': 59881, '+sn': 59883, '-snsg': 59885, 'sg': 59885, '-sg': 59885, 'ic': 59886,
28
+ '-fzdzpl': 59887, '-fzdzplsn': 59887, 'fzdzpl': 59887, '-fzraplsn': 59887, 'fzrapl': 59887, '+fzrapl': 59887,
29
+ '-rapl': 59887, '-rasnpl': 59887, '-raplsn': 59887, '+rapl': 59887, 'rapl': 59887, '-snpl': 59887, 'snpl': 59887,
30
+ '-pl': 59887, 'pl': 59887, '-plsn': 59887, '-plra': 59887, 'plra': 59887, '-pldz': 59887, '+pl': 59887, 'plsn': 59887, 'plup': 59887, '+plsn': 59887,
31
+ '-sh': 59888, '-shra': 59888, 'sh': 59889, 'shra': 59889, '+sh': 59889, '+shra': 59889, '-shrasn': 59891, '-shsnra': 59891, '+shrabr': 59892,
32
+ 'shrasn': 59892, '+shrasn': 59892, 'shsnra': 59892, '+shsnra': 59892, '-shsn': 59893, 'shsn': 59894, '+shsn': 59894,
33
+ '-gs': 59895, '-shgs': 59895, 'fzraplgs': 59896, '-sngs': 59896, 'gsplsn': 59896, 'gspl': 59896, 'plgssn': 59896, 'gs': 59896, 'shgs': 59896, '+gs': 59896, '+shgs': 59896,
34
+ '-gr': 59897, '-shgr': 59897, '-sngr': 59898, 'gr': 59898, 'shgr': 59898, '+gr': 59898, '+shgr': 59898,
35
+ '-tsrasn': 59907, 'tsrasn': 59907, '-tssnra': 59907, 'tssnra': 59907, '-vctsra': 59908, '-tsra': 59908, 'tsra': 59908, '-tsdz': 59908, 'vctsra': 59908,
36
+ 'tspl': 59909, '-tssn': 59909, '-tspl': 59909, 'tssn': 59909, '-vctssn': 59909, 'vctssn': 59909, 'tsplsn': 59909, 'tssnpl': 59909, '-tssnpl': 59909, '-tsragr': 59910,
37
+ 'tsrags': 59910, 'tsragr': 59910, 'tsgs': 59910, 'tsgr': 59910,
38
+ '+tsfzrapl': 59911, '+vctsra': 59912, '+tsra': 59912, '+tsfzra': 59912, '+tssn': 59913, '+tspl': 59913, '+tsplsn': 59913, '+vctssn': 59913,
39
+ 'tssa': 59914, 'tsds': 59914, 'tsdu': 59914, '+tsgs': 59915, '+tsgr': 59915, '+tsrags': 59915, '+tsragr': 59915, 'in': 59750,
40
+ '-up': 59750, 'up': 59750, '+up': 59751, '-fzup': 59756, 'fzup': 59756, '+fzup': 59757,
41
+ };
42
+ const station_plot_opts_defaults = {
43
+ config: {},
44
+ thin_fac: 1,
45
+ font_face: 'Trebuchet MS',
46
+ font_size: 12,
47
+ font_url_template: '',
48
+ };
49
+ function positionToAlignmentAndOffset(pos, off_size) {
50
+ off_size = off_size === undefined ? 10 : off_size;
51
+ let ha, va;
52
+ let xoff, yoff;
53
+ if (pos == 'll' || pos == 'cl' || pos == 'ul') {
54
+ ha = 'right';
55
+ xoff = -off_size;
56
+ }
57
+ else if (pos == 'lc' || pos == 'c' || pos == 'uc') {
58
+ ha = 'center';
59
+ xoff = 0;
60
+ }
61
+ else {
62
+ ha = 'left';
63
+ xoff = off_size;
64
+ }
65
+ if (pos == 'll' || pos == 'lc' || pos == 'lr') {
66
+ va = 'top';
67
+ yoff = -off_size;
68
+ }
69
+ else if (pos == 'cl' || pos == 'c' || pos == 'cr') {
70
+ va = 'middle';
71
+ yoff = 0;
72
+ }
73
+ else {
74
+ va = 'baseline';
75
+ yoff = off_size;
76
+ }
77
+ return { horizontal_align: ha, vertical_align: va, offset_x: xoff, offset_y: yoff };
78
+ }
79
+ class StationPlot extends PlotComponent {
80
+ /**
81
+ *
82
+ * @param field
83
+ * @param opts
84
+ */
85
+ constructor(field, opts) {
86
+ super();
87
+ this.field = field;
88
+ this.opts = normalizeOptions(opts, station_plot_opts_defaults); // Is there a way to do this without invoking `as`?
89
+ this.gl_elems = null;
90
+ this.text_components = null;
91
+ }
92
+ /**
93
+ * Update the data displayed as station plots
94
+ * @param field - The new field to display as station plots
95
+ */
96
+ async updateField(field) {
97
+ this.field = field;
98
+ if (this.gl_elems === null)
99
+ return;
100
+ const map = this.gl_elems.map;
101
+ const gl = this.gl_elems.gl;
102
+ const barb_components = this.gl_elems.barb_components;
103
+ const map_style = map.getStyle();
104
+ const font_url_template = this.opts.font_url_template == '' ? map_style.glyphs : this.opts.font_url_template;
105
+ if (font_url_template === undefined)
106
+ throw "The map style doesn't have any glyph information. Please pass the font_url_template option to StationPlot";
107
+ const font_url = font_url_template.replace('{fontstack}', this.opts.font_face);
108
+ let ibarb = 0;
109
+ const sub_component_promises = Object.entries(this.opts.config).map(async ([k_, config]) => {
110
+ const k = k_;
111
+ if (config.type == 'number' || config.type == 'string') {
112
+ const pos = config.pos;
113
+ const color_opt = config.color;
114
+ const color = color_opt === undefined ? Color.fromHex('#000000') : Color.normalizeColor(color_opt);
115
+ const coords = this.field.grid.getEarthCoords();
116
+ const zoom = this.field.grid.getMinVisibleZoom(this.opts.thin_fac);
117
+ let text_specs;
118
+ if (config.type == 'number') {
119
+ const comp = this.field.getScalar(k);
120
+ const formatter = config.formatter === undefined ? (val) => val === null ? 'null' : val.toString() : config.formatter;
121
+ text_specs = comp.map((v, i) => ({ text: formatter(v), lat: coords.lats[i], lon: coords.lons[i], min_zoom: zoom[i] }));
122
+ }
123
+ else {
124
+ const comp = this.field.getStrings(k);
125
+ text_specs = comp.map((v, i) => ({ text: v === null ? '' : v, lat: coords.lats[i], lon: coords.lons[i], min_zoom: zoom[i] }));
126
+ }
127
+ const tc_opts = {
128
+ ...positionToAlignmentAndOffset(pos),
129
+ font_size: this.opts.font_size, halo: true,
130
+ text_color: color, halo_color: Color.fromHex('#ffffff'),
131
+ };
132
+ return await TextCollection.make(gl, text_specs, font_url, tc_opts);
133
+ }
134
+ else if (config.type == 'barb') {
135
+ const comp = this.field.getVector(k);
136
+ const barb_comp = barb_components[ibarb++];
137
+ barb_comp.updateField(comp);
138
+ }
139
+ else if (config.type == 'symbol') {
140
+ const pos = config.pos;
141
+ const color_opt = config.color;
142
+ const color = color_opt === undefined ? Color.fromHex('#000000') : Color.normalizeColor(color_opt);
143
+ const comp = this.field.getStrings(k);
144
+ const coords = this.field.grid.getEarthCoords();
145
+ const zoom = this.field.grid.getMinVisibleZoom(this.opts.thin_fac);
146
+ const wxsym_font_url = font_url_template.replace('{fontstack}', 'wx_symbols');
147
+ const text_specs = comp.map((v, i) => ({ text: v === null ? '' : String.fromCharCode(SYMBOLS[v]),
148
+ lat: coords.lats[i], lon: coords.lons[i], min_zoom: zoom[i] }));
149
+ const tc_opts = {
150
+ ...positionToAlignmentAndOffset(pos),
151
+ font_size: this.opts.font_size, halo: true,
152
+ text_color: color, halo_color: Color.fromHex('#ffffff'),
153
+ };
154
+ if (tc_opts.offset_x !== undefined)
155
+ tc_opts.offset_x -= 3;
156
+ return await TextCollection.make(gl, text_specs, wxsym_font_url, tc_opts);
157
+ }
158
+ else {
159
+ throw `Unknown station plot configuration type ${config.type}`;
160
+ }
161
+ });
162
+ this.text_components = (await Promise.all(sub_component_promises)).filter((c) => c !== undefined);
163
+ map.triggerRepaint();
164
+ }
165
+ /** @internal */
166
+ async onAdd(map, gl) {
167
+ const barb_promises = Object.entries(this.opts.config).map(async ([k_, config]) => {
168
+ const k = k_;
169
+ if (config.type == 'barb') {
170
+ const comp = this.field.getVector(k);
171
+ const color = config.color === undefined ? '#000000' : config.color;
172
+ const barb_size = config.barb_size_multipler === undefined ? 1 : config.barb_size_multipler;
173
+ const barb_comp = new Barbs(comp, { thin_fac: this.opts.thin_fac, color: color, barb_size_multiplier: barb_size });
174
+ await barb_comp.onAdd(map, gl);
175
+ return barb_comp;
176
+ }
177
+ });
178
+ const barb_components = (await Promise.all(barb_promises)).filter((c) => c !== undefined);
179
+ this.gl_elems = {
180
+ map: map, gl: gl, barb_components: barb_components
181
+ };
182
+ this.updateField(this.field);
183
+ }
184
+ /** @internal */
185
+ render(gl, arg) {
186
+ if (this.gl_elems === null || this.text_components === null)
187
+ return;
188
+ const gl_elems = this.gl_elems;
189
+ const text_components = this.text_components;
190
+ const barb_components = this.gl_elems.barb_components;
191
+ const map_width = gl_elems.map.getCanvas().width;
192
+ const map_height = gl_elems.map.getCanvas().height;
193
+ const map_zoom = gl_elems.map.getZoom();
194
+ let itext = 0, ibarb = 0;
195
+ Object.values(this.opts.config).forEach(comp => {
196
+ if (comp.type == 'barb') {
197
+ barb_components[ibarb++].render(gl, arg);
198
+ }
199
+ else {
200
+ text_components[itext++].render(gl, arg, [map_width, map_height], map_zoom);
201
+ }
202
+ });
203
+ }
204
+ }
205
+ export default StationPlot;