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/Grid.js CHANGED
@@ -1,45 +1,58 @@
1
1
  import { Float16Array } from "@petamoriken/float16";
2
2
  import { WGLBuffer, WGLTexture } from "autumn-wgl";
3
- import { lambertConformalConic, rotateSphere } from "./Map";
3
+ import { LngLat, lambertConformalConic, rotateSphere } from "./Map";
4
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;
5
+ import { Cache, getArrayConstructor, getMinZoom } from "./utils";
6
+ import { kdTree } from "kd-tree-javascript";
7
+ function argMin(ary) {
8
+ if (ary.length === 0) {
9
+ return -1;
10
+ }
11
+ let min = ary[0];
12
+ let minIndex = 0;
13
+ for (let i = 1; i < ary.length; i++) {
14
+ if (ary[i] < min) {
15
+ minIndex = i;
16
+ min = ary[i];
17
+ }
15
18
  }
19
+ return minIndex;
20
+ }
21
+ async function makeWGLDomainBuffers(gl, grid, simplify_ni, simplify_nj) {
22
+ const texcoord_margin_r = 1 / (2 * grid.ni);
23
+ const texcoord_margin_s = 1 / (2 * grid.nj);
24
+ const { lats: field_lats, lons: field_lons } = grid.getEarthCoords(simplify_ni, simplify_nj);
25
+ const domain_coords = await layer_worker.makeDomainVerticesAndTexCoords(field_lats, field_lons, simplify_ni, simplify_nj, texcoord_margin_r, texcoord_margin_s);
16
26
  const vertices = new WGLBuffer(gl, domain_coords['vertices'], 2, gl.TRIANGLE_STRIP);
17
27
  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 };
28
+ return { 'vertices': vertices, 'texcoords': texcoords };
20
29
  }
21
- async function makeWGLBillboardBuffers(gl, grid, thin_fac, max_zoom) {
30
+ async function makeWGLBillboardBuffers(gl, grid, thin_fac, map_max_zoom) {
22
31
  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);
32
+ const min_zoom = grid.getMinVisibleZoom(thin_fac);
33
+ const bb_elements = await layer_worker.makeBBElements(field_lats, field_lons, min_zoom, grid.ni, grid.nj, map_max_zoom);
34
+ const vertices = new WGLBuffer(gl, bb_elements['pts'], 2, gl.POINTS, { per_instance: true });
35
+ const texcoords = new WGLBuffer(gl, bb_elements['tex_coords'], 2, gl.POINTS, { per_instance: true });
26
36
  return { 'vertices': vertices, 'texcoords': texcoords };
27
37
  }
28
- function makeVectorRotationTexture(gl, grid) {
38
+ function makeVectorRotationTexture(gl, grid, data_are_earth_relative) {
29
39
  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);
40
+ const rot_vals = new Float16Array(grid.ni * grid.nj).fill(parseFloat('nan'));
41
+ if (data_are_earth_relative) {
42
+ rot_vals.fill(0);
43
+ }
44
+ else {
45
+ if (!grid.is_conformal) {
46
+ // 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.
47
+ console.warn('Vector rotations for non-conformal projections are not supported. The output may look incorrect.');
48
+ }
49
+ for (let icd = 0; icd < coords.lats.length; icd++) {
50
+ const lon = coords.lons[icd];
51
+ const lat = coords.lats[icd];
52
+ rot_vals[icd] = grid.getVectorRotationAtPoint(lon, lat);
53
+ }
54
+ }
55
+ const { format, type, row_alignment } = getGLFormatTypeAlignment(gl, 'float16');
43
56
  const rot_img = {
44
57
  format: format, type: type, row_alignment: row_alignment, image: new Uint16Array(rot_vals.buffer),
45
58
  width: grid.ni, height: grid.nj, mag_filter: gl.LINEAR
@@ -53,30 +66,93 @@ class Grid {
53
66
  this.is_conformal = is_conformal;
54
67
  this.ni = ni;
55
68
  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
69
  this.billboard_buffer_cache = new Cache((gl, thin_fac, max_zoom) => {
62
70
  return makeWGLBillboardBuffers(gl, this, thin_fac, max_zoom);
63
71
  });
64
- this.vector_rotation_cache = new Cache((gl) => {
65
- return makeVectorRotationTexture(gl, this);
72
+ this.vector_rotation_cache = new Cache((gl, data_are_earth_relative) => {
73
+ return makeVectorRotationTexture(gl, this, data_are_earth_relative);
66
74
  });
67
75
  }
68
- async getWGLBuffers(gl) {
69
- return await this.buffer_cache.getValue(gl);
70
- }
71
76
  async getWGLBillboardBuffers(gl, thin_fac, max_zoom) {
72
77
  return await this.billboard_buffer_cache.getValue(gl, thin_fac, max_zoom);
73
78
  }
74
- getVectorRotationTexture(gl) {
75
- return this.vector_rotation_cache.getValue(gl);
79
+ getVectorRotationAtPoint(lon, lat) {
80
+ const [x, y] = this.transform(lon, lat);
81
+ const [x_pertlon, y_pertlon] = this.transform(lon + 0.01, lat);
82
+ return Math.atan2(y_pertlon - y, x_pertlon - x);
83
+ }
84
+ getVectorRotationTexture(gl, data_are_earth_relative) {
85
+ return this.vector_rotation_cache.getValue(gl, data_are_earth_relative);
86
+ }
87
+ }
88
+ /** A structured grid (in this case meaning a cartesian grid with i and j coordinates) */
89
+ class StructuredGrid extends Grid {
90
+ constructor(type, is_conformal, ni, nj, thin_x, thin_y) {
91
+ super(type, is_conformal, ni, nj);
92
+ this.thin_x = thin_x === undefined ? 1 : thin_x;
93
+ this.thin_y = thin_y === undefined ? 1 : thin_y;
94
+ this.buffer_cache = new Cache((gl) => {
95
+ const new_ni = Math.max(Math.floor(this.ni / 20), 20);
96
+ const new_nj = Math.max(Math.floor(this.nj / 20), 20);
97
+ return makeWGLDomainBuffers(gl, this, new_ni, new_nj);
98
+ });
99
+ }
100
+ /** @internal */
101
+ xyThinFromMaxZoom(thin_fac, map_max_zoom) {
102
+ const n_density_tiers = Math.log2(thin_fac);
103
+ const n_inaccessible_tiers = Math.max(n_density_tiers + 1 - map_max_zoom, 0);
104
+ const xy_thin = Math.pow(2, n_inaccessible_tiers);
105
+ return [xy_thin, xy_thin];
106
+ }
107
+ /** @internal */
108
+ getMinVisibleZoom(thin_fac) {
109
+ const min_zoom = new Uint8Array(this.ni * this.nj);
110
+ const zoom_thin_fac = thin_fac / Math.max(this.thin_x, this.thin_y);
111
+ for (let ilat = 0; ilat < this.nj * this.thin_y; ilat++) {
112
+ for (let ilon = 0; ilon < this.ni * this.thin_x; ilon++) {
113
+ const idx = ilat * this.ni + ilon;
114
+ min_zoom[idx] = getMinZoom(ilat, ilon, zoom_thin_fac);
115
+ }
116
+ }
117
+ return min_zoom;
118
+ }
119
+ /** @internal */
120
+ thinDataArray(original_grid, ary) {
121
+ const arrayType = getArrayConstructor(ary);
122
+ const new_data = new arrayType(this.ni * this.nj);
123
+ for (let i = 0; i < this.ni; i++) {
124
+ for (let j = 0; j < this.nj; j++) {
125
+ const idx_old = i * this.thin_x + original_grid.ni * j * this.thin_y;
126
+ const idx = i + this.ni * j;
127
+ new_data[idx] = ary[idx_old];
128
+ }
129
+ }
130
+ return new_data;
131
+ }
132
+ async getWGLBuffers(gl) {
133
+ return await this.buffer_cache.getValue(gl);
134
+ }
135
+ sampleNearestGridPoint(lon, lat, ary) {
136
+ const [x, y] = this.transform(lon, lat);
137
+ const { x: xs, y: ys } = this.getGridCoords();
138
+ const ll_x = xs[0];
139
+ const ur_x = xs[xs.length - 1];
140
+ const dx = xs[1] - xs[0];
141
+ const ll_y = ys[0];
142
+ const ur_y = ys[ys.length - 1];
143
+ const dy = ys[1] - ys[0];
144
+ if (x < ll_x - 0.5 * dx || x > ur_x + 0.5 * dx || y < ll_y - 0.5 * dy || y > ur_y + 0.5 * dy) {
145
+ return { sample: NaN, sample_lon: NaN, sample_lat: NaN };
146
+ }
147
+ const i_min = argMin(xs.map(xv => Math.abs(xv - x)));
148
+ const j_min = argMin(ys.map(yv => Math.abs(yv - y)));
149
+ const idx = i_min + j_min * this.ni;
150
+ const [lon_min, lat_min] = this.transform(xs[i_min], ys[j_min], { inverse: true });
151
+ return { sample: ary[idx], sample_lon: lon_min, sample_lat: lat_min };
76
152
  }
77
153
  }
78
154
  /** A plate carree (a.k.a. lat/lon) grid with uniform grid spacing */
79
- class PlateCarreeGrid extends Grid {
155
+ class PlateCarreeGrid extends StructuredGrid {
80
156
  /**
81
157
  * Create a plate carree grid
82
158
  * @param ni - The number of grid points in the i (longitude) direction
@@ -86,22 +162,24 @@ class PlateCarreeGrid extends Grid {
86
162
  * @param ur_lon - The longitude of the upper right corner of the grid
87
163
  * @param ur_lat - The latitude of the upper right corner of the grid
88
164
  */
89
- constructor(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat) {
90
- super('latlon', true, ni, nj);
165
+ constructor(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat, thin_x, thin_y) {
166
+ super('latlon', true, ni, nj, thin_x, thin_y);
91
167
  this.ll_lon = ll_lon;
92
168
  this.ll_lat = ll_lat;
93
169
  this.ur_lon = ur_lon;
94
170
  this.ur_lat = ur_lat;
95
171
  const dlon = (this.ur_lon - this.ll_lon) / (this.ni - 1);
96
172
  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;
173
+ this.ll_cache = new Cache((ni, nj) => {
174
+ const lons = new Float32Array(ni * nj);
175
+ const lats = new Float32Array(ni * nj);
176
+ const dlon_req = (this.ni - 1) / (ni - 1) * dlon;
177
+ const dlat_req = (this.nj - 1) / (nj - 1) * dlat;
178
+ for (let i = 0; i < ni; i++) {
179
+ for (let j = 0; j < nj; j++) {
180
+ const idx = i + j * ni;
181
+ lons[idx] = this.ll_lon + i * dlon_req;
182
+ lats[idx] = this.ll_lat + j * dlat_req;
105
183
  }
106
184
  }
107
185
  return { 'lons': lons, 'lats': lats };
@@ -118,6 +196,7 @@ class PlateCarreeGrid extends Grid {
118
196
  return { x: x, y: y };
119
197
  });
120
198
  }
199
+ /** @internal */
121
200
  copy(opts) {
122
201
  opts = opts !== undefined ? opts : {};
123
202
  const ni = opts.ni !== undefined ? opts.ni : this.ni;
@@ -129,18 +208,25 @@ class PlateCarreeGrid extends Grid {
129
208
  return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat);
130
209
  }
131
210
  /**
211
+ * @internal
132
212
  * Get a list of longitudes and latitudes on the grid (internal method)
133
213
  */
134
- getEarthCoords() {
135
- return this.ll_cache.getValue();
214
+ getEarthCoords(ni, nj) {
215
+ ni = ni === undefined ? this.ni : ni;
216
+ nj = nj === undefined ? this.nj : nj;
217
+ return this.ll_cache.getValue(ni, nj);
136
218
  }
219
+ /** @internal */
137
220
  getGridCoords() {
138
221
  return this.gc_cache.getValue();
139
222
  }
223
+ /** @internal */
140
224
  transform(x, y, opts) {
141
225
  return [x, y];
142
226
  }
143
- getThinnedGrid(thin_x, thin_y) {
227
+ /** @internal */
228
+ getThinnedGrid(thin_fac, map_max_zoom) {
229
+ const [thin_x, thin_y] = this.xyThinFromMaxZoom(thin_fac, map_max_zoom);
144
230
  const dlon = (this.ur_lon - this.ll_lon) / this.ni;
145
231
  const dlat = (this.ur_lat - this.ll_lat) / this.nj;
146
232
  const ni = Math.ceil(this.ni / thin_x);
@@ -151,11 +237,11 @@ class PlateCarreeGrid extends Grid {
151
237
  const ll_lat = this.ll_lat;
152
238
  const ur_lon = this.ur_lon - ni_remove * dlon;
153
239
  const ur_lat = this.ur_lat - nj_remove * dlat;
154
- return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat);
240
+ return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat, this.thin_x * thin_x, this.thin_y * thin_y);
155
241
  }
156
242
  }
157
243
  /** A rotated lat-lon (plate carree) grid with uniform grid spacing */
158
- class PlateCarreeRotatedGrid extends Grid {
244
+ class PlateCarreeRotatedGrid extends StructuredGrid {
159
245
  /**
160
246
  * Create a Lambert conformal conic grid
161
247
  * @param ni - The number of grid points in the i (longitude) direction
@@ -168,8 +254,8 @@ class PlateCarreeRotatedGrid extends Grid {
168
254
  * @param ur_lon - The longitude of the upper right corner of the grid (on the rotated earth)
169
255
  * @param ur_lat - The latitude of the upper right corner of the grid (on the rotated earth)
170
256
  */
171
- constructor(ni, nj, np_lon, np_lat, lon_shift, ll_lon, ll_lat, ur_lon, ur_lat) {
172
- super('latlonrot', true, ni, nj);
257
+ constructor(ni, nj, np_lon, np_lat, lon_shift, ll_lon, ll_lat, ur_lon, ur_lat, thin_x, thin_y) {
258
+ super('latlonrot', true, ni, nj, thin_x, thin_y);
173
259
  this.np_lon = np_lon;
174
260
  this.np_lat = np_lat;
175
261
  this.lon_shift = lon_shift;
@@ -180,15 +266,17 @@ class PlateCarreeRotatedGrid extends Grid {
180
266
  this.llrot = rotateSphere({ np_lon: np_lon, np_lat: np_lat, lon_shift: lon_shift });
181
267
  const dlon = (this.ur_lon - this.ll_lon) / (this.ni - 1);
182
268
  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;
269
+ this.ll_cache = new Cache((ni, nj) => {
270
+ const lons = new Float32Array(ni * nj);
271
+ const lats = new Float32Array(ni * nj);
272
+ const dlon_req = (this.ni - 1) / (ni - 1) * dlon;
273
+ const dlat_req = (this.nj - 1) / (nj - 1) * dlat;
274
+ for (let i = 0; i < ni; i++) {
275
+ const lon_p = this.ll_lon + i * dlon_req;
276
+ for (let j = 0; j < nj; j++) {
277
+ const lat_p = this.ll_lat + j * dlat_req;
190
278
  const [lon, lat] = this.llrot(lon_p, lat_p);
191
- const idx = i + j * this.ni;
279
+ const idx = i + j * ni;
192
280
  lons[idx] = lon;
193
281
  lats[idx] = lat;
194
282
  }
@@ -207,6 +295,7 @@ class PlateCarreeRotatedGrid extends Grid {
207
295
  return { x: x, y: y };
208
296
  });
209
297
  }
298
+ /** @internal */
210
299
  copy(opts) {
211
300
  opts = opts !== undefined ? opts : {};
212
301
  const ni = opts.ni !== undefined ? opts.ni : this.ni;
@@ -218,20 +307,27 @@ class PlateCarreeRotatedGrid extends Grid {
218
307
  return new PlateCarreeRotatedGrid(ni, nj, this.np_lon, this.np_lat, this.lon_shift, ll_lon, ll_lat, ur_lon, ur_lat);
219
308
  }
220
309
  /**
221
- * Get a list of longitudes and latitudes on the grid (internal method)
310
+ * @internal
311
+ * Get a list of longitudes and latitudes on the grid
222
312
  */
223
- getEarthCoords() {
224
- return this.ll_cache.getValue();
313
+ getEarthCoords(ni, nj) {
314
+ ni = ni === undefined ? this.ni : ni;
315
+ nj = nj === undefined ? this.nj : nj;
316
+ return this.ll_cache.getValue(ni, nj);
225
317
  }
318
+ /** @internal */
226
319
  getGridCoords() {
227
320
  return this.gc_cache.getValue();
228
321
  }
322
+ /** @internal */
229
323
  transform(x, y, opts) {
230
324
  opts = opts === undefined ? {} : opts;
231
325
  const inverse = 'inverse' in opts ? opts.inverse : false;
232
326
  return this.llrot(x, y, { inverse: !inverse });
233
327
  }
234
- getThinnedGrid(thin_x, thin_y) {
328
+ /** @internal */
329
+ getThinnedGrid(thin_fac, map_max_zoom) {
330
+ const [thin_x, thin_y] = this.xyThinFromMaxZoom(thin_fac, map_max_zoom);
235
331
  const dlon = (this.ur_lon - this.ll_lon) / this.ni;
236
332
  const dlat = (this.ur_lat - this.ll_lat) / this.nj;
237
333
  const ni = Math.ceil(this.ni / thin_x);
@@ -242,13 +338,13 @@ class PlateCarreeRotatedGrid extends Grid {
242
338
  const ll_lat = this.ll_lat;
243
339
  const ur_lon = this.ur_lon - ni_remove * dlon;
244
340
  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);
341
+ return new PlateCarreeRotatedGrid(ni, nj, this.np_lon, this.np_lat, this.lon_shift, ll_lon, ll_lat, ur_lon, ur_lat, this.thin_x * thin_x, this.thin_y * thin_y);
246
342
  }
247
343
  }
248
344
  /** A Lambert conformal conic grid with uniform grid spacing */
249
- class LambertGrid extends Grid {
345
+ class LambertGrid extends StructuredGrid {
250
346
  /**
251
- * Create a Lambert conformal conic grid
347
+ * Create a Lambert conformal conic grid from the lower-left and upper-right corner x/y values.
252
348
  * @param ni - The number of grid points in the i (longitude) direction
253
349
  * @param nj - The number of grid points in the j (latitude) direction
254
350
  * @param lon_0 - The standard longitude for the projection; this is also the center longitude for the projection
@@ -259,8 +355,8 @@ class LambertGrid extends Grid {
259
355
  * @param ur_x - The x coordinate in projection space of the upper-right corner of the grid
260
356
  * @param ur_y - The y coordinate in projection space of the upper-right corner of the grid
261
357
  */
262
- constructor(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ur_x, ur_y) {
263
- super('lcc', true, ni, nj);
358
+ constructor(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ur_x, ur_y, thin_x, thin_y) {
359
+ super('lcc', true, ni, nj, thin_x, thin_y);
264
360
  this.lon_0 = lon_0;
265
361
  this.lat_0 = lat_0;
266
362
  this.lat_std = lat_std;
@@ -269,17 +365,19 @@ class LambertGrid extends Grid {
269
365
  this.ur_x = ur_x;
270
366
  this.ur_y = ur_y;
271
367
  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;
368
+ const dx = (this.ur_x - this.ll_x) / this.ni;
369
+ const dy = (this.ur_y - this.ll_y) / this.nj;
370
+ this.ll_cache = new Cache((ni, nj) => {
371
+ const lons = new Float32Array(ni * nj);
372
+ const lats = new Float32Array(ni * nj);
373
+ const dx_req = (this.ni - 1) / (ni - 1) * dx;
374
+ const dy_req = (this.nj - 1) / (nj - 1) * dy;
375
+ for (let i = 0; i < ni; i++) {
376
+ const x = this.ll_x + i * dx_req;
377
+ for (let j = 0; j < nj; j++) {
378
+ const y = this.ll_y + j * dy_req;
281
379
  const [lon, lat] = this.lcc(x, y, { inverse: true });
282
- const idx = i + j * this.ni;
380
+ const idx = i + j * ni;
283
381
  lons[idx] = lon;
284
382
  lats[idx] = lat;
285
383
  }
@@ -298,6 +396,25 @@ class LambertGrid extends Grid {
298
396
  return { x: x, y: y };
299
397
  });
300
398
  }
399
+ /**
400
+ * Create a Lambert conformal conic grid from the lower-left grid point coordinate and a dx and dy.
401
+ * @param ni - The number of grid points in the i (longitude) direction
402
+ * @param nj - The number of grid points in the j (latitude) direction
403
+ * @param lon_0 - The standard longitude for the projection; this is also the center longitude for the projection
404
+ * @param lat_0 - The center latitude for the projection
405
+ * @param lat_std - The standard latitudes for the projection
406
+ * @param ll_lon - The longitude of the lower-left corner of the grid
407
+ * @param ll_lat - The latitude of the lower-left corner of the grid
408
+ * @param dx - The grid dx in meters
409
+ * @param dy - The grid dy in meters
410
+ * @returns
411
+ */
412
+ static fromLLCornerLonLat(ni, nj, lon_0, lat_0, lat_std, ll_lon, ll_lat, dx, dy) {
413
+ const lcc = lambertConformalConic({ lon_0: lon_0, lat_0: lat_0, lat_std: lat_std });
414
+ const [ll_x, ll_y] = lcc(ll_lon, ll_lat);
415
+ return new LambertGrid(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ll_x + ni * dx, ll_y + nj * dy);
416
+ }
417
+ /** @internal */
301
418
  copy(opts) {
302
419
  opts = opts !== undefined ? opts : {};
303
420
  const ni = opts.ni !== undefined ? opts.ni : this.ni;
@@ -309,20 +426,27 @@ class LambertGrid extends Grid {
309
426
  return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y);
310
427
  }
311
428
  /**
312
- * Get a list of longitudes and latitudes on the grid (internal method)
429
+ * @internal
430
+ * Get a list of longitudes and latitudes on the grid
313
431
  */
314
- getEarthCoords() {
315
- return this.ll_cache.getValue();
432
+ getEarthCoords(ni, nj) {
433
+ ni = ni === undefined ? this.ni : ni;
434
+ nj = nj === undefined ? this.nj : nj;
435
+ return this.ll_cache.getValue(ni, nj);
316
436
  }
437
+ /** @internal */
317
438
  getGridCoords() {
318
439
  return this.gc_cache.getValue();
319
440
  }
441
+ /** @internal */
320
442
  transform(x, y, opts) {
321
443
  opts = opts === undefined ? {} : opts;
322
- const inverse = 'inverse' in opts ? opts.inverse : false;
444
+ const inverse = opts.inverse === undefined ? false : opts.inverse;
323
445
  return this.lcc(x, y, { inverse: inverse });
324
446
  }
325
- getThinnedGrid(thin_x, thin_y) {
447
+ /** @internal */
448
+ getThinnedGrid(thin_fac, map_max_zoom) {
449
+ const [thin_x, thin_y] = this.xyThinFromMaxZoom(thin_fac, map_max_zoom);
326
450
  const dx = (this.ur_x - this.ll_x) / this.ni;
327
451
  const dy = (this.ur_y - this.ll_y) / this.nj;
328
452
  const ni = Math.ceil(this.ni / thin_x);
@@ -333,7 +457,91 @@ class LambertGrid extends Grid {
333
457
  const ll_y = this.ll_y;
334
458
  const ur_x = this.ur_x - ni_remove * dx;
335
459
  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);
460
+ return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y, this.thin_x * thin_x, this.thin_y * thin_y);
461
+ }
462
+ }
463
+ /** An unstructured grid */
464
+ class UnstructuredGrid extends Grid {
465
+ /**
466
+ * Create an unstructured grid
467
+ * @param coords - The lat/lon coordinates of the grid points
468
+ */
469
+ constructor(coords, zoom) {
470
+ const MAX_DIM = 4096;
471
+ super('unstructured', true, Math.min(coords.length, MAX_DIM), Math.floor(coords.length / MAX_DIM) + 1);
472
+ this.coords = coords;
473
+ this.zoom_arg = zoom === undefined ? null : zoom;
474
+ this.zoom_cache = new Cache((thin_fac) => {
475
+ const MAP_MAX_ZOOM = 24;
476
+ const offset = Math.log2(thin_fac);
477
+ const kd_nodes = this.coords.map(c => ({ ...new LngLat(c.lon, c.lat).toMercatorCoord(), min_zoom: MAP_MAX_ZOOM }));
478
+ const tree = new kdTree([...kd_nodes], (a, b) => Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y)), ['x', 'y']);
479
+ const recursiveThin = (x, y, depth) => {
480
+ const size = Math.pow(0.5, depth + 1);
481
+ const nodes = tree.nearest({ x: x, y: y, min_zoom: 0 }, 2, size);
482
+ if (nodes.length > 0) {
483
+ const [node, dist] = nodes.sort((a, b) => a[1] - b[1])[0];
484
+ if (node.min_zoom == MAP_MAX_ZOOM) {
485
+ node.min_zoom = Math.max(depth - offset, 0);
486
+ }
487
+ }
488
+ if (nodes.length > 1 && depth < MAP_MAX_ZOOM + offset) {
489
+ recursiveThin(x - size / 2, y - size / 2, depth + 1);
490
+ recursiveThin(x + size / 2, y - size / 2, depth + 1);
491
+ recursiveThin(x - size / 2, y + size / 2, depth + 1);
492
+ recursiveThin(x + size / 2, y + size / 2, depth + 1);
493
+ }
494
+ };
495
+ recursiveThin(0.5, 0.5, 0);
496
+ return new Uint8Array(kd_nodes.map(n => n.min_zoom));
497
+ });
498
+ }
499
+ /** @internal */
500
+ copy() {
501
+ return new UnstructuredGrid(this.coords);
502
+ }
503
+ /** @internal */
504
+ getEarthCoords() {
505
+ return { lons: new Float32Array(this.coords.map(c => c.lon)), lats: new Float32Array(this.coords.map(c => c.lat)) };
506
+ }
507
+ /** @internal */
508
+ getGridCoords() {
509
+ const { lons, lats } = this.getEarthCoords();
510
+ return { x: lons, y: lats };
511
+ }
512
+ /** @internal */
513
+ transform(x, y, opts) {
514
+ return [x, y];
515
+ }
516
+ /** @internal */
517
+ getMinVisibleZoom(thin_fac) {
518
+ if (this.zoom_arg !== null)
519
+ return this.zoom_arg;
520
+ return this.zoom_cache.getValue(thin_fac);
521
+ }
522
+ /** @internal */
523
+ getThinnedGrid(thin_fac, map_max_zoom) {
524
+ const min_zoom = this.getMinVisibleZoom(thin_fac);
525
+ return new UnstructuredGrid(this.coords.filter((ll, ill) => min_zoom[ill] <= map_max_zoom), min_zoom.filter(ll => ll <= map_max_zoom));
526
+ }
527
+ /** @internal */
528
+ thinDataArray(original_grid, ary) {
529
+ let i_new = 0;
530
+ const arrayType = getArrayConstructor(ary);
531
+ const new_data = new arrayType(this.ni * this.nj);
532
+ for (let i = 0; i < original_grid.coords.length; i++) {
533
+ if (this.coords[i_new].lat == original_grid.coords[i].lat && this.coords[i_new].lon == original_grid.coords[i].lon) {
534
+ new_data[i_new++] = ary[i];
535
+ if (i_new >= this.coords.length)
536
+ break;
537
+ }
538
+ }
539
+ return new_data;
540
+ }
541
+ sampleNearestGridPoint(lon, lat, ary) {
542
+ // TAS: This is gonna be slow. Need to think about using the kdTree here.
543
+ const idx = argMin(this.coords.map(c => (c.lon - lon) * (c.lon - lon) + (c.lat - lat) * (c.lat - lat)));
544
+ return { sample: ary[idx], sample_lon: this.coords[idx].lon, sample_lat: this.coords[idx].lat };
337
545
  }
338
546
  }
339
- export { Grid, PlateCarreeGrid, PlateCarreeRotatedGrid, LambertGrid };
547
+ export { Grid, StructuredGrid, PlateCarreeGrid, PlateCarreeRotatedGrid, LambertGrid, UnstructuredGrid };
@@ -1,10 +1,13 @@
1
1
  import { PlotComponent } from "./PlotComponent";
2
2
  import { MapLikeType } from "./Map";
3
3
  import { RawProfileField } from "./RawField";
4
- import { WebGLAnyRenderingContext } from "./AutumnTypes";
4
+ import { RenderMethodArg, WebGLAnyRenderingContext } from "./AutumnTypes";
5
+ import { ColorMap } from "./Colormap";
6
+ import { Grid } from "./Grid";
5
7
  interface HodographOptions {
6
8
  /**
7
9
  * The color of the hodograph plot background as a hex string
10
+ * @default '#000000'
8
11
  */
9
12
  bgcolor?: string;
10
13
  /**
@@ -13,13 +16,28 @@ interface HodographOptions {
13
16
  * @default 1
14
17
  */
15
18
  thin_fac?: number;
19
+ /**
20
+ * The width of the hodograph line in pixels
21
+ * @default 2.5
22
+ */
23
+ hodo_line_width?: number;
24
+ /**
25
+ * The width of the lines on the background in pixels
26
+ * @default 1.5
27
+ */
28
+ background_line_width?: number;
29
+ /**
30
+ * The colormap to use for the heights on the hodograph. Default is a yellow-blue colormap.
31
+ */
32
+ height_cmap?: ColorMap;
16
33
  }
17
- /** A class representing a a field of hodograph plots */
18
- declare class Hodographs<MapType extends MapLikeType> extends PlotComponent<MapType> {
34
+ /** A class representing a field of hodograph plots */
35
+ declare class Hodographs<GridType extends Grid, MapType extends MapLikeType> extends PlotComponent<MapType> {
19
36
  private profile_field;
20
37
  readonly opts: Required<HodographOptions>;
21
38
  private gl_elems;
22
39
  private line_elems;
40
+ private hodo_bg_texture;
23
41
  private readonly hodo_scale;
24
42
  private readonly bg_size;
25
43
  /**
@@ -27,12 +45,12 @@ declare class Hodographs<MapType extends MapLikeType> extends PlotComponent<MapT
27
45
  * @param profile_field - The grid of profiles to plot
28
46
  * @param opts - Various options to use when creating the hodographs
29
47
  */
30
- constructor(profile_field: RawProfileField, opts?: HodographOptions);
48
+ constructor(profile_field: RawProfileField<GridType>, opts?: HodographOptions);
31
49
  /**
32
50
  * Update the profiles displayed
33
51
  * @param field - The new profiles to display as hodographs
34
52
  */
35
- updateField(field: RawProfileField): Promise<void>;
53
+ updateField(field: RawProfileField<GridType>): Promise<void>;
36
54
  /**
37
55
  * @internal
38
56
  * Add the hodographs to a map
@@ -42,7 +60,7 @@ declare class Hodographs<MapType extends MapLikeType> extends PlotComponent<MapT
42
60
  * @internal
43
61
  * Render the hodographs
44
62
  */
45
- render(gl: WebGLAnyRenderingContext, matrix: number[] | Float32Array): void;
63
+ render(gl: WebGLAnyRenderingContext, arg: RenderMethodArg): void;
46
64
  }
47
65
  export default Hodographs;
48
66
  export type { HodographOptions };