autumnplot-gl 2.2.3 → 3.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 (49) hide show
  1. package/README.md +65 -8
  2. package/dist/110.autumnplot-gl.js +2 -0
  3. package/dist/110.autumnplot-gl.js.map +1 -0
  4. package/dist/autumnplot-gl.js +3 -0
  5. package/dist/autumnplot-gl.js.LICENSE.txt +12 -0
  6. package/dist/autumnplot-gl.js.map +1 -0
  7. package/dist/marchingsquares.wasm +0 -0
  8. package/lib/AutumnTypes.d.ts +14 -13
  9. package/lib/Barbs.d.ts +10 -5
  10. package/lib/Barbs.js +22 -5
  11. package/lib/BillboardCollection.d.ts +11 -7
  12. package/lib/BillboardCollection.js +52 -30
  13. package/lib/ColorBar.js +51 -7
  14. package/lib/Colormap.d.ts +14 -3
  15. package/lib/Colormap.js +63 -12
  16. package/lib/Contour.d.ts +73 -18
  17. package/lib/Contour.js +180 -148
  18. package/lib/ContourCreator.d.ts +18 -0
  19. package/lib/ContourCreator.js +23 -0
  20. package/lib/Fill.d.ts +18 -7
  21. package/lib/Fill.js +73 -41
  22. package/lib/Grid.d.ts +167 -0
  23. package/lib/Grid.js +339 -0
  24. package/lib/Hodographs.d.ts +13 -6
  25. package/lib/Hodographs.js +58 -71
  26. package/lib/Map.d.ts +14 -3
  27. package/lib/Map.js +9 -0
  28. package/lib/Paintball.d.ts +11 -5
  29. package/lib/Paintball.js +35 -15
  30. package/lib/PlotComponent.d.ts +5 -5
  31. package/lib/PlotLayer.d.ts +12 -12
  32. package/lib/PlotLayer.js +16 -14
  33. package/lib/PlotLayer.worker.d.ts +2 -2
  34. package/lib/PlotLayer.worker.js +105 -66
  35. package/lib/PolylineCollection.d.ts +20 -9
  36. package/lib/PolylineCollection.js +158 -32
  37. package/lib/RawField.d.ts +11 -167
  38. package/lib/RawField.js +37 -383
  39. package/lib/TextCollection.d.ts +31 -0
  40. package/lib/TextCollection.js +295 -0
  41. package/lib/cpp/marchingsquares.d.ts +6 -0
  42. package/lib/cpp/marchingsquares.js +3449 -0
  43. package/lib/cpp/marchingsquares.wasm +0 -0
  44. package/lib/cpp/marchingsquares_embind.d.ts +6 -0
  45. package/lib/index.d.ts +13 -6
  46. package/lib/index.js +12 -3
  47. package/lib/utils.d.ts +5 -3
  48. package/lib/utils.js +17 -6
  49. package/package.json +14 -9
package/lib/Grid.js ADDED
@@ -0,0 +1,339 @@
1
+ import { Float16Array } from "@petamoriken/float16";
2
+ import { WGLBuffer, WGLTexture } from "autumn-wgl";
3
+ import { lambertConformalConic, rotateSphere } from "./Map";
4
+ import { getGLFormatTypeAlignment, layer_worker } from "./PlotComponent";
5
+ import { Cache } from "./utils";
6
+ async function makeWGLDomainBuffers(gl, grid, native_grid) {
7
+ native_grid = native_grid !== undefined ? native_grid : grid;
8
+ const texcoord_margin_r = 1 / (2 * native_grid.ni);
9
+ const texcoord_margin_s = 1 / (2 * native_grid.nj);
10
+ const grid_cell_size_multiplier = (grid.ni * grid.nj) / (native_grid.ni * native_grid.nj);
11
+ const { lats: field_lats, lons: field_lons } = grid.getEarthCoords();
12
+ const domain_coords = await layer_worker.makeDomainVerticesAndTexCoords(field_lats, field_lons, grid.ni, grid.nj, texcoord_margin_r, texcoord_margin_s);
13
+ for (let icd = 0; icd < domain_coords['grid_cell_size'].length; icd++) {
14
+ domain_coords['grid_cell_size'][icd] *= grid_cell_size_multiplier;
15
+ }
16
+ const vertices = new WGLBuffer(gl, domain_coords['vertices'], 2, gl.TRIANGLE_STRIP);
17
+ const texcoords = new WGLBuffer(gl, domain_coords['tex_coords'], 2, gl.TRIANGLE_STRIP);
18
+ const grid_cell_size = new WGLBuffer(gl, domain_coords['grid_cell_size'], 1, gl.TRIANGLE_STRIP);
19
+ return { 'vertices': vertices, 'texcoords': texcoords, 'cellsize': grid_cell_size };
20
+ }
21
+ async function makeWGLBillboardBuffers(gl, grid, thin_fac, max_zoom) {
22
+ const { lats: field_lats, lons: field_lons } = grid.getEarthCoords();
23
+ const bb_elements = await layer_worker.makeBBElements(field_lats, field_lons, grid.ni, grid.nj, thin_fac, max_zoom);
24
+ const vertices = new WGLBuffer(gl, bb_elements['pts'], 3, gl.TRIANGLE_STRIP);
25
+ const texcoords = new WGLBuffer(gl, bb_elements['tex_coords'], 2, gl.TRIANGLE_STRIP);
26
+ return { 'vertices': vertices, 'texcoords': texcoords };
27
+ }
28
+ function makeVectorRotationTexture(gl, grid) {
29
+ const coords = grid.getEarthCoords();
30
+ if (!grid.is_conformal) {
31
+ // If the grid is non-conformal, we need a fully general change of basis from grid coordinates to earth coordinates. This is not supported for now, so warn about it.
32
+ console.warn('Vector rotations for non-conformal projections are not supported. The output may look incorrect.');
33
+ }
34
+ const rot_vals = new Float16Array(coords.lats.length);
35
+ for (let icd = 0; icd < coords.lats.length; icd++) {
36
+ const lon = coords.lons[icd];
37
+ const lat = coords.lats[icd];
38
+ const [x, y] = grid.transform(lon, lat);
39
+ const [x_pertlon, y_pertlon] = grid.transform(lon + 0.01, lat);
40
+ rot_vals[icd] = Math.atan2(y_pertlon - y, x_pertlon - x);
41
+ }
42
+ const { format, type, row_alignment } = getGLFormatTypeAlignment(gl, true);
43
+ const rot_img = {
44
+ format: format, type: type, row_alignment: row_alignment, image: new Uint16Array(rot_vals.buffer),
45
+ width: grid.ni, height: grid.nj, mag_filter: gl.LINEAR
46
+ };
47
+ const rot_tex = new WGLTexture(gl, rot_img);
48
+ return { 'rotation': rot_tex };
49
+ }
50
+ class Grid {
51
+ constructor(type, is_conformal, ni, nj) {
52
+ this.type = type;
53
+ this.is_conformal = is_conformal;
54
+ this.ni = ni;
55
+ this.nj = nj;
56
+ this.buffer_cache = new Cache((gl) => {
57
+ const new_ni = Math.max(Math.floor(this.ni / 50), 20);
58
+ const new_nj = Math.max(Math.floor(this.nj / 50), 20);
59
+ return makeWGLDomainBuffers(gl, this.copy({ ni: new_ni, nj: new_nj }), this);
60
+ });
61
+ this.billboard_buffer_cache = new Cache((gl, thin_fac, max_zoom) => {
62
+ return makeWGLBillboardBuffers(gl, this, thin_fac, max_zoom);
63
+ });
64
+ this.vector_rotation_cache = new Cache((gl) => {
65
+ return makeVectorRotationTexture(gl, this);
66
+ });
67
+ }
68
+ async getWGLBuffers(gl) {
69
+ return await this.buffer_cache.getValue(gl);
70
+ }
71
+ async getWGLBillboardBuffers(gl, thin_fac, max_zoom) {
72
+ return await this.billboard_buffer_cache.getValue(gl, thin_fac, max_zoom);
73
+ }
74
+ getVectorRotationTexture(gl) {
75
+ return this.vector_rotation_cache.getValue(gl);
76
+ }
77
+ }
78
+ /** A plate carree (a.k.a. lat/lon) grid with uniform grid spacing */
79
+ class PlateCarreeGrid extends Grid {
80
+ /**
81
+ * Create a plate carree grid
82
+ * @param ni - The number of grid points in the i (longitude) direction
83
+ * @param nj - The number of grid points in the j (latitude) direction
84
+ * @param ll_lon - The longitude of the lower left corner of the grid
85
+ * @param ll_lat - The latitude of the lower left corner of the grid
86
+ * @param ur_lon - The longitude of the upper right corner of the grid
87
+ * @param ur_lat - The latitude of the upper right corner of the grid
88
+ */
89
+ constructor(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat) {
90
+ super('latlon', true, ni, nj);
91
+ this.ll_lon = ll_lon;
92
+ this.ll_lat = ll_lat;
93
+ this.ur_lon = ur_lon;
94
+ this.ur_lat = ur_lat;
95
+ const dlon = (this.ur_lon - this.ll_lon) / (this.ni - 1);
96
+ const dlat = (this.ur_lat - this.ll_lat) / (this.nj - 1);
97
+ this.ll_cache = new Cache(() => {
98
+ const lons = new Float32Array(this.ni * this.nj);
99
+ const lats = new Float32Array(this.ni * this.nj);
100
+ for (let i = 0; i < this.ni; i++) {
101
+ for (let j = 0; j < this.nj; j++) {
102
+ const idx = i + j * this.ni;
103
+ lons[idx] = this.ll_lon + i * dlon;
104
+ lats[idx] = this.ll_lat + j * dlat;
105
+ }
106
+ }
107
+ return { 'lons': lons, 'lats': lats };
108
+ });
109
+ this.gc_cache = new Cache(() => {
110
+ const x = new Float32Array(this.ni);
111
+ const y = new Float32Array(this.nj);
112
+ for (let i = 0; i < this.ni; i++) {
113
+ x[i] = this.ll_lon + i * dlon;
114
+ }
115
+ for (let j = 0; j < this.nj; j++) {
116
+ y[j] = this.ll_lat + j * dlat;
117
+ }
118
+ return { x: x, y: y };
119
+ });
120
+ }
121
+ copy(opts) {
122
+ opts = opts !== undefined ? opts : {};
123
+ const ni = opts.ni !== undefined ? opts.ni : this.ni;
124
+ const nj = opts.nj !== undefined ? opts.nj : this.nj;
125
+ const ll_lon = opts.ll_lon !== undefined ? opts.ll_lon : this.ll_lon;
126
+ const ll_lat = opts.ll_lat !== undefined ? opts.ll_lat : this.ll_lat;
127
+ const ur_lon = opts.ur_lon !== undefined ? opts.ur_lon : this.ur_lon;
128
+ const ur_lat = opts.ur_lat !== undefined ? opts.ur_lat : this.ur_lat;
129
+ return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat);
130
+ }
131
+ /**
132
+ * Get a list of longitudes and latitudes on the grid (internal method)
133
+ */
134
+ getEarthCoords() {
135
+ return this.ll_cache.getValue();
136
+ }
137
+ getGridCoords() {
138
+ return this.gc_cache.getValue();
139
+ }
140
+ transform(x, y, opts) {
141
+ return [x, y];
142
+ }
143
+ getThinnedGrid(thin_x, thin_y) {
144
+ const dlon = (this.ur_lon - this.ll_lon) / this.ni;
145
+ const dlat = (this.ur_lat - this.ll_lat) / this.nj;
146
+ const ni = Math.ceil(this.ni / thin_x);
147
+ const nj = Math.ceil(this.nj / thin_y);
148
+ const ni_remove = (this.ni - 1) % thin_x;
149
+ const nj_remove = (this.nj - 1) % thin_y;
150
+ const ll_lon = this.ll_lon;
151
+ const ll_lat = this.ll_lat;
152
+ const ur_lon = this.ur_lon - ni_remove * dlon;
153
+ const ur_lat = this.ur_lat - nj_remove * dlat;
154
+ return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat);
155
+ }
156
+ }
157
+ /** A rotated lat-lon (plate carree) grid with uniform grid spacing */
158
+ class PlateCarreeRotatedGrid extends Grid {
159
+ /**
160
+ * Create a Lambert conformal conic grid
161
+ * @param ni - The number of grid points in the i (longitude) direction
162
+ * @param nj - The number of grid points in the j (latitude) direction
163
+ * @param np_lon - The longitude of the north pole for the rotated grid
164
+ * @param np_lat - The latitude of the north pole for the rotated grid
165
+ * @param lon_shift - The angle around the rotated north pole to shift the central meridian
166
+ * @param ll_lon - The longitude of the lower left corner of the grid (on the rotated earth)
167
+ * @param ll_lat - The latitude of the lower left corner of the grid (on the rotated earth)
168
+ * @param ur_lon - The longitude of the upper right corner of the grid (on the rotated earth)
169
+ * @param ur_lat - The latitude of the upper right corner of the grid (on the rotated earth)
170
+ */
171
+ constructor(ni, nj, np_lon, np_lat, lon_shift, ll_lon, ll_lat, ur_lon, ur_lat) {
172
+ super('latlonrot', true, ni, nj);
173
+ this.np_lon = np_lon;
174
+ this.np_lat = np_lat;
175
+ this.lon_shift = lon_shift;
176
+ this.ll_lon = ll_lon;
177
+ this.ll_lat = ll_lat;
178
+ this.ur_lon = ur_lon;
179
+ this.ur_lat = ur_lat;
180
+ this.llrot = rotateSphere({ np_lon: np_lon, np_lat: np_lat, lon_shift: lon_shift });
181
+ const dlon = (this.ur_lon - this.ll_lon) / (this.ni - 1);
182
+ const dlat = (this.ur_lat - this.ll_lat) / (this.nj - 1);
183
+ this.ll_cache = new Cache(() => {
184
+ const lons = new Float32Array(this.ni * this.nj);
185
+ const lats = new Float32Array(this.ni * this.nj);
186
+ for (let i = 0; i < this.ni; i++) {
187
+ const lon_p = this.ll_lon + i * dlon;
188
+ for (let j = 0; j < this.nj; j++) {
189
+ const lat_p = this.ll_lat + j * dlat;
190
+ const [lon, lat] = this.llrot(lon_p, lat_p);
191
+ const idx = i + j * this.ni;
192
+ lons[idx] = lon;
193
+ lats[idx] = lat;
194
+ }
195
+ }
196
+ return { lons: lons, lats: lats };
197
+ });
198
+ this.gc_cache = new Cache(() => {
199
+ const x = new Float32Array(this.ni);
200
+ const y = new Float32Array(this.nj);
201
+ for (let i = 0; i < this.ni; i++) {
202
+ x[i] = this.ll_lon + i * dlon;
203
+ }
204
+ for (let j = 0; j < this.nj; j++) {
205
+ y[j] = this.ll_lat + j * dlat;
206
+ }
207
+ return { x: x, y: y };
208
+ });
209
+ }
210
+ copy(opts) {
211
+ opts = opts !== undefined ? opts : {};
212
+ const ni = opts.ni !== undefined ? opts.ni : this.ni;
213
+ const nj = opts.nj !== undefined ? opts.nj : this.nj;
214
+ const ll_lon = opts.ll_lon !== undefined ? opts.ll_lon : this.ll_lon;
215
+ const ll_lat = opts.ll_lat !== undefined ? opts.ll_lat : this.ll_lat;
216
+ const ur_lon = opts.ur_lon !== undefined ? opts.ur_lon : this.ur_lon;
217
+ const ur_lat = opts.ur_lat !== undefined ? opts.ur_lat : this.ur_lat;
218
+ return new PlateCarreeRotatedGrid(ni, nj, this.np_lon, this.np_lat, this.lon_shift, ll_lon, ll_lat, ur_lon, ur_lat);
219
+ }
220
+ /**
221
+ * Get a list of longitudes and latitudes on the grid (internal method)
222
+ */
223
+ getEarthCoords() {
224
+ return this.ll_cache.getValue();
225
+ }
226
+ getGridCoords() {
227
+ return this.gc_cache.getValue();
228
+ }
229
+ transform(x, y, opts) {
230
+ opts = opts === undefined ? {} : opts;
231
+ const inverse = 'inverse' in opts ? opts.inverse : false;
232
+ return this.llrot(x, y, { inverse: !inverse });
233
+ }
234
+ getThinnedGrid(thin_x, thin_y) {
235
+ const dlon = (this.ur_lon - this.ll_lon) / this.ni;
236
+ const dlat = (this.ur_lat - this.ll_lat) / this.nj;
237
+ const ni = Math.ceil(this.ni / thin_x);
238
+ const nj = Math.ceil(this.nj / thin_y);
239
+ const ni_remove = (this.ni - 1) % thin_x;
240
+ const nj_remove = (this.nj - 1) % thin_y;
241
+ const ll_lon = this.ll_lon;
242
+ const ll_lat = this.ll_lat;
243
+ const ur_lon = this.ur_lon - ni_remove * dlon;
244
+ const ur_lat = this.ur_lat - nj_remove * dlat;
245
+ return new PlateCarreeRotatedGrid(ni, nj, this.np_lon, this.np_lat, this.lon_shift, ll_lon, ll_lat, ur_lon, ur_lat);
246
+ }
247
+ }
248
+ /** A Lambert conformal conic grid with uniform grid spacing */
249
+ class LambertGrid extends Grid {
250
+ /**
251
+ * Create a Lambert conformal conic grid
252
+ * @param ni - The number of grid points in the i (longitude) direction
253
+ * @param nj - The number of grid points in the j (latitude) direction
254
+ * @param lon_0 - The standard longitude for the projection; this is also the center longitude for the projection
255
+ * @param lat_0 - The center latitude for the projection
256
+ * @param lat_std - The standard latitudes for the projection
257
+ * @param ll_x - The x coordinate in projection space of the lower-left corner of the grid
258
+ * @param ll_y - The y coordinate in projection space of the lower-left corner of the grid
259
+ * @param ur_x - The x coordinate in projection space of the upper-right corner of the grid
260
+ * @param ur_y - The y coordinate in projection space of the upper-right corner of the grid
261
+ */
262
+ constructor(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ur_x, ur_y) {
263
+ super('lcc', true, ni, nj);
264
+ this.lon_0 = lon_0;
265
+ this.lat_0 = lat_0;
266
+ this.lat_std = lat_std;
267
+ this.ll_x = ll_x;
268
+ this.ll_y = ll_y;
269
+ this.ur_x = ur_x;
270
+ this.ur_y = ur_y;
271
+ this.lcc = lambertConformalConic({ lon_0: lon_0, lat_0: lat_0, lat_std: lat_std });
272
+ const dx = (this.ur_x - this.ll_x) / (this.ni - 1);
273
+ const dy = (this.ur_y - this.ll_y) / (this.nj - 1);
274
+ this.ll_cache = new Cache(() => {
275
+ const lons = new Float32Array(this.ni * this.nj);
276
+ const lats = new Float32Array(this.ni * this.nj);
277
+ for (let i = 0; i < this.ni; i++) {
278
+ const x = this.ll_x + i * dx;
279
+ for (let j = 0; j < this.nj; j++) {
280
+ const y = this.ll_y + j * dy;
281
+ const [lon, lat] = this.lcc(x, y, { inverse: true });
282
+ const idx = i + j * this.ni;
283
+ lons[idx] = lon;
284
+ lats[idx] = lat;
285
+ }
286
+ }
287
+ return { lons: lons, lats: lats };
288
+ });
289
+ this.gc_cache = new Cache(() => {
290
+ const x = new Float32Array(this.ni);
291
+ const y = new Float32Array(this.nj);
292
+ for (let i = 0; i < this.ni; i++) {
293
+ x[i] = this.ll_x + i * dx;
294
+ }
295
+ for (let j = 0; j < this.nj; j++) {
296
+ y[j] = this.ll_y + j * dy;
297
+ }
298
+ return { x: x, y: y };
299
+ });
300
+ }
301
+ copy(opts) {
302
+ opts = opts !== undefined ? opts : {};
303
+ const ni = opts.ni !== undefined ? opts.ni : this.ni;
304
+ const nj = opts.nj !== undefined ? opts.nj : this.nj;
305
+ const ll_x = opts.ll_x !== undefined ? opts.ll_x : this.ll_x;
306
+ const ll_y = opts.ll_y !== undefined ? opts.ll_y : this.ll_y;
307
+ const ur_x = opts.ur_x !== undefined ? opts.ur_x : this.ur_x;
308
+ const ur_y = opts.ur_y !== undefined ? opts.ur_y : this.ur_y;
309
+ return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y);
310
+ }
311
+ /**
312
+ * Get a list of longitudes and latitudes on the grid (internal method)
313
+ */
314
+ getEarthCoords() {
315
+ return this.ll_cache.getValue();
316
+ }
317
+ getGridCoords() {
318
+ return this.gc_cache.getValue();
319
+ }
320
+ transform(x, y, opts) {
321
+ opts = opts === undefined ? {} : opts;
322
+ const inverse = 'inverse' in opts ? opts.inverse : false;
323
+ return this.lcc(x, y, { inverse: inverse });
324
+ }
325
+ getThinnedGrid(thin_x, thin_y) {
326
+ const dx = (this.ur_x - this.ll_x) / this.ni;
327
+ const dy = (this.ur_y - this.ll_y) / this.nj;
328
+ const ni = Math.ceil(this.ni / thin_x);
329
+ const nj = Math.ceil(this.nj / thin_y);
330
+ const ni_remove = (this.ni - 1) % thin_x;
331
+ const nj_remove = (this.nj - 1) % thin_y;
332
+ const ll_x = this.ll_x;
333
+ const ll_y = this.ll_y;
334
+ const ur_x = this.ur_x - ni_remove * dx;
335
+ const ur_y = this.ur_y - nj_remove * dy;
336
+ return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y);
337
+ }
338
+ }
339
+ export { Grid, PlateCarreeGrid, PlateCarreeRotatedGrid, LambertGrid };
@@ -1,5 +1,5 @@
1
- /// <reference types="mapbox-gl" />
2
1
  import { PlotComponent } from "./PlotComponent";
2
+ import { MapLikeType } from "./Map";
3
3
  import { RawProfileField } from "./RawField";
4
4
  import { WebGLAnyRenderingContext } from "./AutumnTypes";
5
5
  interface HodographOptions {
@@ -15,22 +15,29 @@ interface HodographOptions {
15
15
  thin_fac?: number;
16
16
  }
17
17
  /** A class representing a a field of hodograph plots */
18
- declare class Hodographs extends PlotComponent {
19
- private readonly profile_field;
20
- readonly bgcolor: [number, number, number];
21
- readonly thin_fac: number;
18
+ declare class Hodographs<MapType extends MapLikeType> extends PlotComponent<MapType> {
19
+ private profile_field;
20
+ readonly opts: Required<HodographOptions>;
22
21
  private gl_elems;
22
+ private line_elems;
23
+ private readonly hodo_scale;
24
+ private readonly bg_size;
23
25
  /**
24
26
  * Create a field of hodographs
25
27
  * @param profile_field - The grid of profiles to plot
26
28
  * @param opts - Various options to use when creating the hodographs
27
29
  */
28
30
  constructor(profile_field: RawProfileField, opts?: HodographOptions);
31
+ /**
32
+ * Update the profiles displayed
33
+ * @param field - The new profiles to display as hodographs
34
+ */
35
+ updateField(field: RawProfileField): Promise<void>;
29
36
  /**
30
37
  * @internal
31
38
  * Add the hodographs to a map
32
39
  */
33
- onAdd(map: mapboxgl.Map, gl: WebGLAnyRenderingContext): Promise<void>;
40
+ onAdd(map: MapType, gl: WebGLAnyRenderingContext): Promise<void>;
34
41
  /**
35
42
  * @internal
36
43
  * Render the hodographs
package/lib/Hodographs.js CHANGED
@@ -1,8 +1,8 @@
1
- import { PlotComponent, layer_worker } from "./PlotComponent";
1
+ import { PlotComponent } from "./PlotComponent";
2
2
  import { PolylineCollection } from "./PolylineCollection";
3
3
  import { BillboardCollection } from "./BillboardCollection";
4
- import { getMinZoom, hex2rgba } from './utils';
5
- import { LngLat } from "./Map";
4
+ import { getMinZoom, hex2rgb, normalizeOptions } from './utils';
5
+ import { ColorMap } from "./Colormap";
6
6
  const LINE_WIDTH = 4;
7
7
  const BG_MAX_RING_MAG = 40;
8
8
  const HODO_BG_DIMS = {
@@ -40,28 +40,11 @@ function _createHodoBackgroundTexture() {
40
40
  }
41
41
  ;
42
42
  let HODO_BG_TEXTURE = null;
43
- const HODO_COLORS = [
44
- { 'bounds': [0, 1], 'color': '#ffffcc' },
45
- { 'bounds': [1, 3], 'color': '#a1dab4' },
46
- { 'bounds': [3, 6], 'color': '#41b6c4' },
47
- { 'bounds': [6, 9], 'color': '#225ea8' }
48
- ];
49
- function _createHodoHeightTexture() {
50
- let canvas = document.createElement('canvas');
51
- canvas.width = Math.max(...HODO_COLORS.map(s => Math.max(...s['bounds'])));
52
- canvas.height = 1;
53
- let ctx = canvas.getContext('2d');
54
- HODO_COLORS.forEach(stop => {
55
- if (ctx === null) {
56
- throw "Could not get rendering context for the hodograph height texture canvas";
57
- }
58
- const [clb, cub] = stop['bounds'];
59
- ctx.fillStyle = stop['color'];
60
- ctx.fillRect(clb, 0, cub - clb, 1);
61
- });
62
- return canvas;
63
- }
64
- let HODO_HEIGHT_TEXTURE = null;
43
+ const HODO_CMAP = new ColorMap([0, 1, 3, 6, 9], ['#ffffcc', '#a1dab4', '#41b6c4', '#225ea8']);
44
+ const hodograph_opt_defaults = {
45
+ bgcolor: '#000000',
46
+ thin_fac: 1
47
+ };
65
48
  /** A class representing a a field of hodograph plots */
66
49
  class Hodographs extends PlotComponent {
67
50
  /**
@@ -71,81 +54,85 @@ class Hodographs extends PlotComponent {
71
54
  */
72
55
  constructor(profile_field, opts) {
73
56
  super();
74
- opts = opts || {};
75
57
  this.profile_field = profile_field;
76
- const color = hex2rgba(opts.bgcolor || '#000000');
77
- this.bgcolor = [color[0], color[1], color[2]];
78
- this.thin_fac = opts.thin_fac || 1;
58
+ this.opts = normalizeOptions(opts, hodograph_opt_defaults);
59
+ this.hodo_scale = (HODO_BG_DIMS.BB_TEX_WIDTH - LINE_WIDTH / 2) / (HODO_BG_DIMS.BB_TEX_WIDTH * BG_MAX_RING_MAG);
60
+ this.bg_size = 140;
79
61
  this.gl_elems = null;
62
+ this.line_elems = null;
80
63
  }
81
64
  /**
82
- * @internal
83
- * Add the hodographs to a map
65
+ * Update the profiles displayed
66
+ * @param field - The new profiles to display as hodographs
84
67
  */
85
- async onAdd(map, gl) {
86
- const hodo_scale = (HODO_BG_DIMS.BB_TEX_WIDTH - LINE_WIDTH / 2) / (HODO_BG_DIMS.BB_TEX_WIDTH * BG_MAX_RING_MAG);
87
- const bg_size = 140;
88
- if (HODO_BG_TEXTURE === null) {
89
- HODO_BG_TEXTURE = _createHodoBackgroundTexture();
90
- }
91
- const bg_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'image': HODO_BG_TEXTURE, 'mag_filter': gl.NEAREST };
92
- const max_zoom = map.getMaxZoom();
93
- const bg_billboard = new BillboardCollection(gl, this.profile_field.getStormMotionGrid(), this.thin_fac, max_zoom, bg_image, HODO_BG_DIMS, this.bgcolor, bg_size * 0.004);
94
- const hodo_polyline = await layer_worker.makePolyLines(this.profile_field.profiles.map(prof => {
95
- const pt_ll = new LngLat(prof['lon'], prof['lat']).toMercatorCoord();
96
- const zoom = getMinZoom(prof['jlat'], prof['ilon'], this.thin_fac);
97
- const max_tex_z = Math.max(...HODO_COLORS.map(s => Math.max(...s['bounds'])));
68
+ async updateField(field) {
69
+ this.profile_field = field;
70
+ if (this.gl_elems === null)
71
+ return;
72
+ // XXX: This might leak VRAM
73
+ const gl = this.gl_elems.gl;
74
+ this.gl_elems.bg_billboard.updateField(field.getStormMotionGrid());
75
+ const profiles = this.profile_field.profiles;
76
+ const hodo_polyline = profiles.map(prof => {
77
+ const zoom = getMinZoom(prof['jlat'], prof['ilon'], this.opts.thin_fac);
98
78
  return {
99
- 'verts': [...prof['u']].map((u, ipt) => [u - prof['smu'], prof['v'][ipt] - prof['smv']]),
100
- 'origin': [pt_ll.x, pt_ll.y],
79
+ 'offsets': [...prof['u']].map((u, ipt) => [u - prof['smu'], prof['v'][ipt] - prof['smv']]),
80
+ 'vertices': [...prof['u']].map(u => [prof['lon'], prof['lat']]),
101
81
  'zoom': zoom,
102
- 'texcoords': [...prof['z']].map(z => [z / max_tex_z, 0.5])
82
+ 'data': [...prof['z']],
103
83
  };
104
- }));
105
- if (HODO_HEIGHT_TEXTURE === null) {
106
- HODO_HEIGHT_TEXTURE = _createHodoHeightTexture();
107
- }
108
- const height_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'image': HODO_HEIGHT_TEXTURE, 'mag_filter': gl.NEAREST };
109
- const hodo_line = new PolylineCollection(gl, hodo_polyline, height_image, 1.5, hodo_scale * bg_size);
110
- const sm_polyline = await layer_worker.makePolyLines(this.profile_field.profiles.map(prof => {
111
- const pt_ll = new LngLat(prof['lon'], prof['lat']).toMercatorCoord();
112
- let ret = {};
113
- const zoom = getMinZoom(prof['jlat'], prof['ilon'], this.thin_fac);
84
+ });
85
+ const hodo_line = await PolylineCollection.make(gl, hodo_polyline, { line_width: 2.5, cmap: HODO_CMAP, offset_scale: this.hodo_scale * this.bg_size });
86
+ const sm_polyline = profiles.map(prof => {
87
+ const zoom = getMinZoom(prof['jlat'], prof['ilon'], this.opts.thin_fac);
114
88
  const sm_mag = Math.hypot(prof['smu'], prof['smv']);
115
89
  const sm_ang = Math.PI / 2 - Math.atan2(-prof['smv'], -prof['smu']);
116
90
  const buffer = 2;
117
91
  return {
118
- 'verts': [[buffer * Math.sin(sm_ang), buffer * Math.cos(sm_ang)],
92
+ 'offsets': [[buffer * Math.sin(sm_ang), buffer * Math.cos(sm_ang)],
119
93
  [sm_mag * Math.sin(sm_ang), sm_mag * Math.cos(sm_ang)]],
120
- 'origin': [pt_ll.x, pt_ll.y],
121
- 'zoom': zoom,
122
- 'texcoords': [[0.5, 0.5], [0.5, 0.5]]
94
+ 'vertices': [[prof['lon'], prof['lat']], [prof['lon'], prof['lat']]],
95
+ 'zoom': zoom
123
96
  };
124
- }));
125
- let byte_color = this.bgcolor.map(c => c * 255);
126
- byte_color.push(255);
127
- const sm_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'width': 1, 'height': 1, 'image': new Uint8Array(byte_color),
128
- 'mag_filter': gl.NEAREST };
129
- const sm_line = new PolylineCollection(gl, sm_polyline, sm_image, 1, hodo_scale * bg_size);
97
+ });
98
+ const sm_line = await PolylineCollection.make(gl, sm_polyline, { line_width: 1.5, color: this.opts.bgcolor, offset_scale: this.hodo_scale * this.bg_size });
99
+ this.line_elems = {
100
+ hodo_line: hodo_line, sm_line: sm_line
101
+ };
102
+ }
103
+ /**
104
+ * @internal
105
+ * Add the hodographs to a map
106
+ */
107
+ async onAdd(map, gl) {
108
+ if (HODO_BG_TEXTURE === null) {
109
+ HODO_BG_TEXTURE = _createHodoBackgroundTexture();
110
+ }
111
+ const bg_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'image': HODO_BG_TEXTURE, 'mag_filter': gl.NEAREST };
112
+ const max_zoom = map.getMaxZoom();
113
+ const bg_billboard = new BillboardCollection(this.profile_field.getStormMotionGrid(), this.opts.thin_fac, max_zoom, bg_image, HODO_BG_DIMS, hex2rgb(this.opts.bgcolor), this.bg_size * 0.004);
114
+ await bg_billboard.setup(gl);
130
115
  this.gl_elems = {
131
- map: map, bg_billboard: bg_billboard, hodo_line: hodo_line, sm_line: sm_line
116
+ gl: gl, map: map, bg_billboard: bg_billboard
132
117
  };
118
+ this.updateField(this.profile_field);
133
119
  }
134
120
  /**
135
121
  * @internal
136
122
  * Render the hodographs
137
123
  */
138
124
  render(gl, matrix) {
139
- if (this.gl_elems === null)
125
+ if (this.gl_elems === null || this.line_elems === null)
140
126
  return;
141
127
  const gl_elems = this.gl_elems;
128
+ const line_elems = this.line_elems;
142
129
  const zoom = gl_elems.map.getZoom();
143
130
  const map_width = gl_elems.map.getCanvas().width;
144
131
  const map_height = gl_elems.map.getCanvas().height;
145
132
  const bearing = gl_elems.map.getBearing();
146
133
  const pitch = gl_elems.map.getPitch();
147
- gl_elems.hodo_line.render(gl, matrix, [map_width, map_height], zoom, bearing, pitch);
148
- gl_elems.sm_line.render(gl, matrix, [map_width, map_height], zoom, bearing, bearing);
134
+ line_elems.hodo_line.render(gl, matrix, [map_width, map_height], zoom, bearing, pitch);
135
+ line_elems.sm_line.render(gl, matrix, [map_width, map_height], zoom, bearing, bearing);
149
136
  gl_elems.bg_billboard.render(gl, matrix, [map_width, map_height], zoom, bearing, pitch);
150
137
  }
151
138
  }
package/lib/Map.d.ts CHANGED
@@ -1,5 +1,15 @@
1
- import mapboxgl from 'mapbox-gl';
2
- type MapType = mapboxgl.Map | maplibregl.Map;
1
+ type StyleSpecification = {
2
+ glyphs?: string;
3
+ };
4
+ type MapLikeType = {
5
+ triggerRepaint: () => void;
6
+ getCanvas: () => HTMLCanvasElement;
7
+ getStyle: () => StyleSpecification;
8
+ getZoom: () => number;
9
+ getMaxZoom: () => number;
10
+ getBearing: () => number;
11
+ getPitch: () => number;
12
+ };
3
13
  interface LambertConformalConicParameters {
4
14
  lon_0: number;
5
15
  lat_0: number;
@@ -37,6 +47,7 @@ declare class LngLat {
37
47
  x: number;
38
48
  y: number;
39
49
  };
50
+ static fromMercatorCoord(x: number, y: number): LngLat;
40
51
  }
41
52
  export { LngLat, lambertConformalConic, rotateSphere };
42
- export type { MapType };
53
+ export type { MapLikeType };
package/lib/Map.js CHANGED
@@ -124,11 +124,17 @@ function rotateSphere(params) {
124
124
  function mercatorXfromLng(lng) {
125
125
  return (180 + lng) / 360;
126
126
  }
127
+ function lngFromMercatorX(x) {
128
+ return 360 * x - 180;
129
+ }
127
130
  function mercatorYfromLat(lat) {
128
131
  const sin_lat = Math.sin(lat * Math.PI / 180);
129
132
  const y = (180 - (90 / Math.PI * Math.log((1 + sin_lat) / (1 - sin_lat)))) / 360;
130
133
  return Math.min(2, Math.max(-2, y));
131
134
  }
135
+ function latFromMercatorY(y) {
136
+ return Math.atan(Math.sinh((180 - y * 360) * Math.PI / 180)) * 180 / Math.PI;
137
+ }
132
138
  /**
133
139
  * A `LngLat` object represents a given longitude and latitude coordinate, measured in degrees.
134
140
  * These coordinates are based on the [WGS84 (EPSG:4326) standard](https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84).
@@ -156,5 +162,8 @@ class LngLat {
156
162
  toMercatorCoord() {
157
163
  return { x: mercatorXfromLng(this.lng), y: mercatorYfromLat(this.lat) };
158
164
  }
165
+ static fromMercatorCoord(x, y) {
166
+ return new LngLat(lngFromMercatorX(x), latFromMercatorY(y));
167
+ }
159
168
  }
160
169
  export { LngLat, lambertConformalConic, rotateSphere };
@@ -1,5 +1,5 @@
1
1
  import { TypedArray, WebGLAnyRenderingContext } from "./AutumnTypes";
2
- import { MapType } from "./Map";
2
+ import { MapLikeType } from "./Map";
3
3
  import { PlotComponent } from "./PlotComponent";
4
4
  import { RawScalarField } from "./RawField";
5
5
  interface PaintballOptions {
@@ -20,11 +20,12 @@ interface PaintballOptions {
20
20
  * of single-precision floats, this works for up to 24 members. (Technically speaking, I don't need the quotes around "bits", as they're bits of the
21
21
  * significand of an IEEE 754 float.)
22
22
  */
23
- declare class Paintball<ArrayType extends TypedArray> extends PlotComponent {
24
- private readonly field;
25
- readonly colors: number[];
26
- readonly opacity: number;
23
+ declare class Paintball<ArrayType extends TypedArray, MapType extends MapLikeType> extends PlotComponent<MapType> {
24
+ private field;
25
+ readonly opts: Required<PaintballOptions>;
26
+ private readonly color_components;
27
27
  private gl_elems;
28
+ private fill_texture;
28
29
  /**
29
30
  * Create a paintball plot
30
31
  * @param field - A scalar field containing the member objects encoded as "bits." The numerical value of each grid point can be constructed like
@@ -33,6 +34,11 @@ declare class Paintball<ArrayType extends TypedArray> extends PlotComponent {
33
34
  * @param opts - Options for creating the paintball plot
34
35
  */
35
36
  constructor(field: RawScalarField<ArrayType>, opts?: PaintballOptions);
37
+ /**
38
+ * Update the field displayed as a paintball plot
39
+ * @param field - The new field to display as a paintball plot
40
+ */
41
+ updateField(field: RawScalarField<ArrayType>): Promise<void>;
36
42
  /**
37
43
  * @internal
38
44
  * Add the paintball plot to a map.