autumnplot-gl 3.2.0 → 4.0.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 (59) hide show
  1. package/README.md +11 -207
  2. package/dist/983.autumnplot-gl.js +2 -0
  3. package/dist/983.autumnplot-gl.js.map +1 -0
  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 +85 -6
  8. package/lib/AutumnTypes.js +28 -1
  9. package/lib/Barbs.d.ts +7 -5
  10. package/lib/BillboardCollection.d.ts +8 -7
  11. package/lib/BillboardCollection.js +69 -58
  12. package/lib/Color.d.ts +2 -0
  13. package/lib/Color.js +4 -0
  14. package/lib/ColorBar.d.ts +19 -0
  15. package/lib/ColorBar.js +9 -6
  16. package/lib/Colormap.d.ts +1 -0
  17. package/lib/Colormap.js +8 -8
  18. package/lib/Contour.d.ts +28 -8
  19. package/lib/Contour.js +27 -54
  20. package/lib/ContourCreator.d.ts +9 -1
  21. package/lib/ContourCreator.js +5 -4
  22. package/lib/Fill.d.ts +28 -14
  23. package/lib/Fill.js +97 -50
  24. package/lib/Grid.d.ts +132 -30
  25. package/lib/Grid.js +355 -99
  26. package/lib/Hodographs.d.ts +15 -8
  27. package/lib/Hodographs.js +48 -30
  28. package/lib/Map.d.ts +14 -1
  29. package/lib/Map.js +60 -4
  30. package/lib/Paintball.d.ts +7 -5
  31. package/lib/Paintball.js +36 -31
  32. package/lib/ParticleTracer.d.ts +19 -0
  33. package/lib/ParticleTracer.js +37 -0
  34. package/lib/PlotComponent.d.ts +7 -7
  35. package/lib/PlotComponent.js +9 -3
  36. package/lib/PlotLayer.d.ts +5 -5
  37. package/lib/PlotLayer.js +2 -2
  38. package/lib/PlotLayer.worker.d.ts +1 -2
  39. package/lib/PlotLayer.worker.js +22 -51
  40. package/lib/PolylineCollection.d.ts +5 -3
  41. package/lib/PolylineCollection.js +60 -37
  42. package/lib/RawField.d.ts +78 -23
  43. package/lib/RawField.js +147 -31
  44. package/lib/ShaderManager.d.ts +12 -0
  45. package/lib/ShaderManager.js +58 -0
  46. package/lib/StationPlot.d.ts +187 -25
  47. package/lib/StationPlot.js +209 -60
  48. package/lib/TextCollection.d.ts +9 -6
  49. package/lib/TextCollection.js +97 -62
  50. package/lib/cpp/marchingsquares.js +483 -585
  51. package/lib/cpp/marchingsquares.wasm +0 -0
  52. package/lib/cpp/marchingsquares_embind.d.ts +23 -3
  53. package/lib/index.d.ts +12 -5
  54. package/lib/index.js +8 -5
  55. package/lib/utils.d.ts +4 -1
  56. package/lib/utils.js +12 -1
  57. package/package.json +3 -3
  58. package/dist/110.autumnplot-gl.js +0 -2
  59. package/dist/110.autumnplot-gl.js.map +0 -1
package/lib/Grid.js CHANGED
@@ -1,45 +1,60 @@
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
+ const WGS84_SEMIMAJOR = 6378137.0;
8
+ const WGS84_SEMIMINOR = 6356752.314245;
9
+ function argMin(ary) {
10
+ if (ary.length === 0) {
11
+ return -1;
12
+ }
13
+ let min = ary[0];
14
+ let minIndex = 0;
15
+ for (let i = 1; i < ary.length; i++) {
16
+ if (ary[i] < min) {
17
+ minIndex = i;
18
+ min = ary[i];
19
+ }
15
20
  }
21
+ return minIndex;
22
+ }
23
+ async function makeWGLDomainBuffers(gl, grid, simplify_ni, simplify_nj) {
24
+ const texcoord_margin_r = 1 / (2 * grid.ni);
25
+ const texcoord_margin_s = 1 / (2 * grid.nj);
26
+ const { lats: field_lats, lons: field_lons } = grid.getEarthCoords(simplify_ni, simplify_nj);
27
+ const domain_coords = await layer_worker.makeDomainVerticesAndTexCoords(field_lats, field_lons, simplify_ni, simplify_nj, texcoord_margin_r, texcoord_margin_s);
16
28
  const vertices = new WGLBuffer(gl, domain_coords['vertices'], 2, gl.TRIANGLE_STRIP);
17
29
  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 };
30
+ return { 'vertices': vertices, 'texcoords': texcoords };
20
31
  }
21
- async function makeWGLBillboardBuffers(gl, grid, thin_fac, max_zoom) {
32
+ async function makeWGLBillboardBuffers(gl, grid, thin_fac, map_max_zoom) {
22
33
  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);
34
+ const min_zoom = grid.getMinVisibleZoom(thin_fac);
35
+ const bb_elements = await layer_worker.makeBBElements(field_lats, field_lons, min_zoom, grid.ni, grid.nj, map_max_zoom);
36
+ const vertices = new WGLBuffer(gl, bb_elements['pts'], 2, gl.POINTS, { per_instance: true });
37
+ const texcoords = new WGLBuffer(gl, bb_elements['tex_coords'], 2, gl.POINTS, { per_instance: true });
26
38
  return { 'vertices': vertices, 'texcoords': texcoords };
27
39
  }
28
- function makeVectorRotationTexture(gl, grid) {
40
+ function makeVectorRotationTexture(gl, grid, data_are_earth_relative) {
29
41
  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);
42
+ const rot_vals = new Float16Array(grid.ni * grid.nj).fill(parseFloat('nan'));
43
+ if (data_are_earth_relative) {
44
+ rot_vals.fill(0);
45
+ }
46
+ else {
47
+ if (!grid.is_conformal) {
48
+ // 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.
49
+ console.warn('Vector rotations for non-conformal projections are not supported. The output may look incorrect.');
50
+ }
51
+ for (let icd = 0; icd < coords.lats.length; icd++) {
52
+ const lon = coords.lons[icd];
53
+ const lat = coords.lats[icd];
54
+ rot_vals[icd] = grid.getVectorRotationAtPoint(lon, lat);
55
+ }
56
+ }
57
+ const { format, type, row_alignment } = getGLFormatTypeAlignment(gl, 'float16');
43
58
  const rot_img = {
44
59
  format: format, type: type, row_alignment: row_alignment, image: new Uint16Array(rot_vals.buffer),
45
60
  width: grid.ni, height: grid.nj, mag_filter: gl.LINEAR
@@ -47,36 +62,100 @@ function makeVectorRotationTexture(gl, grid) {
47
62
  const rot_tex = new WGLTexture(gl, rot_img);
48
63
  return { 'rotation': rot_tex };
49
64
  }
65
+ /** The base class for grid types */
50
66
  class Grid {
51
67
  constructor(type, is_conformal, ni, nj) {
52
68
  this.type = type;
53
69
  this.is_conformal = is_conformal;
54
70
  this.ni = ni;
55
71
  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
72
  this.billboard_buffer_cache = new Cache((gl, thin_fac, max_zoom) => {
62
73
  return makeWGLBillboardBuffers(gl, this, thin_fac, max_zoom);
63
74
  });
64
- this.vector_rotation_cache = new Cache((gl) => {
65
- return makeVectorRotationTexture(gl, this);
75
+ this.vector_rotation_cache = new Cache((gl, data_are_earth_relative) => {
76
+ return makeVectorRotationTexture(gl, this, data_are_earth_relative);
66
77
  });
67
78
  }
68
- async getWGLBuffers(gl) {
69
- return await this.buffer_cache.getValue(gl);
70
- }
71
79
  async getWGLBillboardBuffers(gl, thin_fac, max_zoom) {
72
80
  return await this.billboard_buffer_cache.getValue(gl, thin_fac, max_zoom);
73
81
  }
74
- getVectorRotationTexture(gl) {
75
- return this.vector_rotation_cache.getValue(gl);
82
+ getVectorRotationAtPoint(lon, lat) {
83
+ const [x, y] = this.transform(lon, lat);
84
+ const [x_pertlon, y_pertlon] = this.transform(lon + 0.01, lat);
85
+ return Math.atan2(y_pertlon - y, x_pertlon - x);
86
+ }
87
+ getVectorRotationTexture(gl, data_are_earth_relative) {
88
+ return this.vector_rotation_cache.getValue(gl, data_are_earth_relative);
89
+ }
90
+ }
91
+ /** A structured grid (in this case meaning a cartesian grid with i and j coordinates) */
92
+ class StructuredGrid extends Grid {
93
+ constructor(type, is_conformal, ni, nj, thin_x, thin_y) {
94
+ super(type, is_conformal, ni, nj);
95
+ this.thin_x = thin_x === undefined ? 1 : thin_x;
96
+ this.thin_y = thin_y === undefined ? 1 : thin_y;
97
+ this.buffer_cache = new Cache((gl) => {
98
+ const new_ni = Math.max(Math.floor(this.ni / 20), 20);
99
+ const new_nj = Math.max(Math.floor(this.nj / 20), 20);
100
+ return makeWGLDomainBuffers(gl, this, new_ni, new_nj);
101
+ });
102
+ }
103
+ /** @internal */
104
+ xyThinFromMaxZoom(thin_fac, map_max_zoom) {
105
+ const n_density_tiers = Math.log2(thin_fac);
106
+ const n_inaccessible_tiers = Math.max(n_density_tiers + 1 - map_max_zoom, 0);
107
+ const xy_thin = Math.pow(2, n_inaccessible_tiers);
108
+ return [xy_thin, xy_thin];
109
+ }
110
+ /** @internal */
111
+ getMinVisibleZoom(thin_fac) {
112
+ const min_zoom = new Uint8Array(this.ni * this.nj);
113
+ const zoom_thin_fac = thin_fac / Math.max(this.thin_x, this.thin_y);
114
+ for (let ilat = 0; ilat < this.nj * this.thin_y; ilat++) {
115
+ for (let ilon = 0; ilon < this.ni * this.thin_x; ilon++) {
116
+ const idx = ilat * this.ni + ilon;
117
+ min_zoom[idx] = getMinZoom(ilat, ilon, zoom_thin_fac);
118
+ }
119
+ }
120
+ return min_zoom;
121
+ }
122
+ /** @internal */
123
+ thinDataArray(original_grid, ary) {
124
+ const arrayType = getArrayConstructor(ary);
125
+ const new_data = new arrayType(this.ni * this.nj);
126
+ for (let i = 0; i < this.ni; i++) {
127
+ for (let j = 0; j < this.nj; j++) {
128
+ const idx_old = i * this.thin_x + original_grid.ni * j * this.thin_y;
129
+ const idx = i + this.ni * j;
130
+ new_data[idx] = ary[idx_old];
131
+ }
132
+ }
133
+ return new_data;
134
+ }
135
+ async getWGLBuffers(gl) {
136
+ return await this.buffer_cache.getValue(gl);
137
+ }
138
+ sampleNearestGridPoint(lon, lat, ary) {
139
+ const [x, y] = this.transform(lon, lat);
140
+ const { x: xs, y: ys } = this.getGridCoords();
141
+ const ll_x = xs[0];
142
+ const ur_x = xs[xs.length - 1];
143
+ const dx = xs[1] - xs[0];
144
+ const ll_y = ys[0];
145
+ const ur_y = ys[ys.length - 1];
146
+ const dy = ys[1] - ys[0];
147
+ 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) {
148
+ return { sample: NaN, sample_lon: NaN, sample_lat: NaN };
149
+ }
150
+ const i_min = argMin(xs.map(xv => Math.abs(xv - x)));
151
+ const j_min = argMin(ys.map(yv => Math.abs(yv - y)));
152
+ const idx = i_min + j_min * this.ni;
153
+ const [lon_min, lat_min] = this.transform(xs[i_min], ys[j_min], { inverse: true });
154
+ return { sample: ary[idx], sample_lon: lon_min, sample_lat: lat_min };
76
155
  }
77
156
  }
78
157
  /** A plate carree (a.k.a. lat/lon) grid with uniform grid spacing */
79
- class PlateCarreeGrid extends Grid {
158
+ class PlateCarreeGrid extends StructuredGrid {
80
159
  /**
81
160
  * Create a plate carree grid
82
161
  * @param ni - The number of grid points in the i (longitude) direction
@@ -86,22 +165,24 @@ class PlateCarreeGrid extends Grid {
86
165
  * @param ur_lon - The longitude of the upper right corner of the grid
87
166
  * @param ur_lat - The latitude of the upper right corner of the grid
88
167
  */
89
- constructor(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat) {
90
- super('latlon', true, ni, nj);
168
+ constructor(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat, thin_x, thin_y) {
169
+ super('latlon', true, ni, nj, thin_x, thin_y);
91
170
  this.ll_lon = ll_lon;
92
171
  this.ll_lat = ll_lat;
93
172
  this.ur_lon = ur_lon;
94
173
  this.ur_lat = ur_lat;
95
174
  const dlon = (this.ur_lon - this.ll_lon) / (this.ni - 1);
96
175
  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;
176
+ this.ll_cache = new Cache((ni, nj) => {
177
+ const lons = new Float32Array(ni * nj);
178
+ const lats = new Float32Array(ni * nj);
179
+ const dlon_req = (this.ni - 1) / (ni - 1) * dlon;
180
+ const dlat_req = (this.nj - 1) / (nj - 1) * dlat;
181
+ for (let i = 0; i < ni; i++) {
182
+ for (let j = 0; j < nj; j++) {
183
+ const idx = i + j * ni;
184
+ lons[idx] = this.ll_lon + i * dlon_req;
185
+ lats[idx] = this.ll_lat + j * dlat_req;
105
186
  }
106
187
  }
107
188
  return { 'lons': lons, 'lats': lats };
@@ -118,6 +199,7 @@ class PlateCarreeGrid extends Grid {
118
199
  return { x: x, y: y };
119
200
  });
120
201
  }
202
+ /** @internal */
121
203
  copy(opts) {
122
204
  opts = opts !== undefined ? opts : {};
123
205
  const ni = opts.ni !== undefined ? opts.ni : this.ni;
@@ -129,18 +211,25 @@ class PlateCarreeGrid extends Grid {
129
211
  return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat);
130
212
  }
131
213
  /**
214
+ * @internal
132
215
  * Get a list of longitudes and latitudes on the grid (internal method)
133
216
  */
134
- getEarthCoords() {
135
- return this.ll_cache.getValue();
217
+ getEarthCoords(ni, nj) {
218
+ ni = ni === undefined ? this.ni : ni;
219
+ nj = nj === undefined ? this.nj : nj;
220
+ return this.ll_cache.getValue(ni, nj);
136
221
  }
222
+ /** @internal */
137
223
  getGridCoords() {
138
224
  return this.gc_cache.getValue();
139
225
  }
226
+ /** @internal */
140
227
  transform(x, y, opts) {
141
228
  return [x, y];
142
229
  }
143
- getThinnedGrid(thin_x, thin_y) {
230
+ /** @internal */
231
+ getThinnedGrid(thin_fac, map_max_zoom) {
232
+ const [thin_x, thin_y] = this.xyThinFromMaxZoom(thin_fac, map_max_zoom);
144
233
  const dlon = (this.ur_lon - this.ll_lon) / this.ni;
145
234
  const dlat = (this.ur_lat - this.ll_lat) / this.nj;
146
235
  const ni = Math.ceil(this.ni / thin_x);
@@ -151,11 +240,11 @@ class PlateCarreeGrid extends Grid {
151
240
  const ll_lat = this.ll_lat;
152
241
  const ur_lon = this.ur_lon - ni_remove * dlon;
153
242
  const ur_lat = this.ur_lat - nj_remove * dlat;
154
- return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat);
243
+ return new PlateCarreeGrid(ni, nj, ll_lon, ll_lat, ur_lon, ur_lat, this.thin_x * thin_x, this.thin_y * thin_y);
155
244
  }
156
245
  }
157
246
  /** A rotated lat-lon (plate carree) grid with uniform grid spacing */
158
- class PlateCarreeRotatedGrid extends Grid {
247
+ class PlateCarreeRotatedGrid extends StructuredGrid {
159
248
  /**
160
249
  * Create a Lambert conformal conic grid
161
250
  * @param ni - The number of grid points in the i (longitude) direction
@@ -168,8 +257,8 @@ class PlateCarreeRotatedGrid extends Grid {
168
257
  * @param ur_lon - The longitude of the upper right corner of the grid (on the rotated earth)
169
258
  * @param ur_lat - The latitude of the upper right corner of the grid (on the rotated earth)
170
259
  */
171
- constructor(ni, nj, np_lon, np_lat, lon_shift, ll_lon, ll_lat, ur_lon, ur_lat) {
172
- super('latlonrot', true, ni, nj);
260
+ constructor(ni, nj, np_lon, np_lat, lon_shift, ll_lon, ll_lat, ur_lon, ur_lat, thin_x, thin_y) {
261
+ super('latlonrot', true, ni, nj, thin_x, thin_y);
173
262
  this.np_lon = np_lon;
174
263
  this.np_lat = np_lat;
175
264
  this.lon_shift = lon_shift;
@@ -180,15 +269,17 @@ class PlateCarreeRotatedGrid extends Grid {
180
269
  this.llrot = rotateSphere({ np_lon: np_lon, np_lat: np_lat, lon_shift: lon_shift });
181
270
  const dlon = (this.ur_lon - this.ll_lon) / (this.ni - 1);
182
271
  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;
272
+ this.ll_cache = new Cache((ni, nj) => {
273
+ const lons = new Float32Array(ni * nj);
274
+ const lats = new Float32Array(ni * nj);
275
+ const dlon_req = (this.ni - 1) / (ni - 1) * dlon;
276
+ const dlat_req = (this.nj - 1) / (nj - 1) * dlat;
277
+ for (let i = 0; i < ni; i++) {
278
+ const lon_p = this.ll_lon + i * dlon_req;
279
+ for (let j = 0; j < nj; j++) {
280
+ const lat_p = this.ll_lat + j * dlat_req;
190
281
  const [lon, lat] = this.llrot(lon_p, lat_p);
191
- const idx = i + j * this.ni;
282
+ const idx = i + j * ni;
192
283
  lons[idx] = lon;
193
284
  lats[idx] = lat;
194
285
  }
@@ -207,6 +298,7 @@ class PlateCarreeRotatedGrid extends Grid {
207
298
  return { x: x, y: y };
208
299
  });
209
300
  }
301
+ /** @internal */
210
302
  copy(opts) {
211
303
  opts = opts !== undefined ? opts : {};
212
304
  const ni = opts.ni !== undefined ? opts.ni : this.ni;
@@ -218,20 +310,27 @@ class PlateCarreeRotatedGrid extends Grid {
218
310
  return new PlateCarreeRotatedGrid(ni, nj, this.np_lon, this.np_lat, this.lon_shift, ll_lon, ll_lat, ur_lon, ur_lat);
219
311
  }
220
312
  /**
221
- * Get a list of longitudes and latitudes on the grid (internal method)
313
+ * @internal
314
+ * Get a list of longitudes and latitudes on the grid
222
315
  */
223
- getEarthCoords() {
224
- return this.ll_cache.getValue();
316
+ getEarthCoords(ni, nj) {
317
+ ni = ni === undefined ? this.ni : ni;
318
+ nj = nj === undefined ? this.nj : nj;
319
+ return this.ll_cache.getValue(ni, nj);
225
320
  }
321
+ /** @internal */
226
322
  getGridCoords() {
227
323
  return this.gc_cache.getValue();
228
324
  }
325
+ /** @internal */
229
326
  transform(x, y, opts) {
230
327
  opts = opts === undefined ? {} : opts;
231
328
  const inverse = 'inverse' in opts ? opts.inverse : false;
232
329
  return this.llrot(x, y, { inverse: !inverse });
233
330
  }
234
- getThinnedGrid(thin_x, thin_y) {
331
+ /** @internal */
332
+ getThinnedGrid(thin_fac, map_max_zoom) {
333
+ const [thin_x, thin_y] = this.xyThinFromMaxZoom(thin_fac, map_max_zoom);
235
334
  const dlon = (this.ur_lon - this.ll_lon) / this.ni;
236
335
  const dlat = (this.ur_lat - this.ll_lat) / this.nj;
237
336
  const ni = Math.ceil(this.ni / thin_x);
@@ -242,13 +341,13 @@ class PlateCarreeRotatedGrid extends Grid {
242
341
  const ll_lat = this.ll_lat;
243
342
  const ur_lon = this.ur_lon - ni_remove * dlon;
244
343
  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);
344
+ 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
345
  }
247
346
  }
248
347
  /** A Lambert conformal conic grid with uniform grid spacing */
249
- class LambertGrid extends Grid {
348
+ class LambertGrid extends StructuredGrid {
250
349
  /**
251
- * Create a Lambert conformal conic grid
350
+ * Create a Lambert conformal conic grid from the lower-left and upper-right corner x/y values.
252
351
  * @param ni - The number of grid points in the i (longitude) direction
253
352
  * @param nj - The number of grid points in the j (latitude) direction
254
353
  * @param lon_0 - The standard longitude for the projection; this is also the center longitude for the projection
@@ -258,9 +357,11 @@ class LambertGrid extends Grid {
258
357
  * @param ll_y - The y coordinate in projection space of the lower-left corner of the grid
259
358
  * @param ur_x - The x coordinate in projection space of the upper-right corner of the grid
260
359
  * @param ur_y - The y coordinate in projection space of the upper-right corner of the grid
360
+ * @param a - The semimajor axis of the assumed shape of Earth in meters
361
+ * @param b - The semiminor axis of the assumed shape of Earth in meters
261
362
  */
262
- constructor(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ur_x, ur_y) {
263
- super('lcc', true, ni, nj);
363
+ constructor(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ur_x, ur_y, a, b, thin_x, thin_y) {
364
+ super('lcc', true, ni, nj, thin_x, thin_y);
264
365
  this.lon_0 = lon_0;
265
366
  this.lat_0 = lat_0;
266
367
  this.lat_std = lat_std;
@@ -268,18 +369,22 @@ class LambertGrid extends Grid {
268
369
  this.ll_y = ll_y;
269
370
  this.ur_x = ur_x;
270
371
  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;
372
+ this.a = a === undefined ? WGS84_SEMIMAJOR : a;
373
+ this.b = b === undefined ? WGS84_SEMIMINOR : b;
374
+ this.lcc = lambertConformalConic({ lon_0: lon_0, lat_0: lat_0, lat_std: lat_std, a: this.a, b: this.b });
375
+ const dx = (this.ur_x - this.ll_x) / this.ni;
376
+ const dy = (this.ur_y - this.ll_y) / this.nj;
377
+ this.ll_cache = new Cache((ni, nj) => {
378
+ const lons = new Float32Array(ni * nj);
379
+ const lats = new Float32Array(ni * nj);
380
+ const dx_req = (this.ni - 1) / (ni - 1) * dx;
381
+ const dy_req = (this.nj - 1) / (nj - 1) * dy;
382
+ for (let i = 0; i < ni; i++) {
383
+ const x = this.ll_x + i * dx_req;
384
+ for (let j = 0; j < nj; j++) {
385
+ const y = this.ll_y + j * dy_req;
281
386
  const [lon, lat] = this.lcc(x, y, { inverse: true });
282
- const idx = i + j * this.ni;
387
+ const idx = i + j * ni;
283
388
  lons[idx] = lon;
284
389
  lats[idx] = lat;
285
390
  }
@@ -298,11 +403,29 @@ class LambertGrid extends Grid {
298
403
  return { x: x, y: y };
299
404
  });
300
405
  }
301
- static fromLLCornerLonLat(ni, nj, lon_0, lat_0, lat_std, ll_lon, ll_lat, dx, dy) {
302
- const lcc = lambertConformalConic({ lon_0: lon_0, lat_0: lat_0, lat_std: lat_std });
406
+ /**
407
+ * Create a Lambert conformal conic grid from the lower-left grid point coordinate and a dx and dy.
408
+ * @param ni - The number of grid points in the i (longitude) direction
409
+ * @param nj - The number of grid points in the j (latitude) direction
410
+ * @param lon_0 - The standard longitude for the projection; this is also the center longitude for the projection
411
+ * @param lat_0 - The center latitude for the projection
412
+ * @param lat_std - The standard latitudes for the projection
413
+ * @param ll_lon - The longitude of the lower-left corner of the grid
414
+ * @param ll_lat - The latitude of the lower-left corner of the grid
415
+ * @param dx - The grid dx in meters
416
+ * @param dy - The grid dy in meters
417
+ * @param a - The semimajor axis of the assumed shape of Earth in meters
418
+ * @param b - The semiminor axis of the assumed shape of Earth in meters
419
+ * @returns
420
+ */
421
+ static fromLLCornerLonLat(ni, nj, lon_0, lat_0, lat_std, ll_lon, ll_lat, dx, dy, a, b) {
422
+ a = a === undefined ? WGS84_SEMIMAJOR : a;
423
+ b = b === undefined ? WGS84_SEMIMINOR : b;
424
+ const lcc = lambertConformalConic({ lon_0: lon_0, lat_0: lat_0, lat_std: lat_std, a: a, b: b });
303
425
  const [ll_x, ll_y] = lcc(ll_lon, ll_lat);
304
- return new LambertGrid(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ll_x + ni * dx, ll_y + nj * dy);
426
+ return new LambertGrid(ni, nj, lon_0, lat_0, lat_std, ll_x, ll_y, ll_x + ni * dx, ll_y + nj * dy, a, b);
305
427
  }
428
+ /** @internal */
306
429
  copy(opts) {
307
430
  opts = opts !== undefined ? opts : {};
308
431
  const ni = opts.ni !== undefined ? opts.ni : this.ni;
@@ -311,23 +434,30 @@ class LambertGrid extends Grid {
311
434
  const ll_y = opts.ll_y !== undefined ? opts.ll_y : this.ll_y;
312
435
  const ur_x = opts.ur_x !== undefined ? opts.ur_x : this.ur_x;
313
436
  const ur_y = opts.ur_y !== undefined ? opts.ur_y : this.ur_y;
314
- return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y);
437
+ return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y, this.a, this.b);
315
438
  }
316
439
  /**
317
- * Get a list of longitudes and latitudes on the grid (internal method)
440
+ * @internal
441
+ * Get a list of longitudes and latitudes on the grid
318
442
  */
319
- getEarthCoords() {
320
- return this.ll_cache.getValue();
443
+ getEarthCoords(ni, nj) {
444
+ ni = ni === undefined ? this.ni : ni;
445
+ nj = nj === undefined ? this.nj : nj;
446
+ return this.ll_cache.getValue(ni, nj);
321
447
  }
448
+ /** @internal */
322
449
  getGridCoords() {
323
450
  return this.gc_cache.getValue();
324
451
  }
452
+ /** @internal */
325
453
  transform(x, y, opts) {
326
454
  opts = opts === undefined ? {} : opts;
327
455
  const inverse = opts.inverse === undefined ? false : opts.inverse;
328
456
  return this.lcc(x, y, { inverse: inverse });
329
457
  }
330
- getThinnedGrid(thin_x, thin_y) {
458
+ /** @internal */
459
+ getThinnedGrid(thin_fac, map_max_zoom) {
460
+ const [thin_x, thin_y] = this.xyThinFromMaxZoom(thin_fac, map_max_zoom);
331
461
  const dx = (this.ur_x - this.ll_x) / this.ni;
332
462
  const dy = (this.ur_y - this.ll_y) / this.nj;
333
463
  const ni = Math.ceil(this.ni / thin_x);
@@ -338,7 +468,133 @@ class LambertGrid extends Grid {
338
468
  const ll_y = this.ll_y;
339
469
  const ur_x = this.ur_x - ni_remove * dx;
340
470
  const ur_y = this.ur_y - nj_remove * dy;
341
- return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y);
471
+ return new LambertGrid(ni, nj, this.lon_0, this.lat_0, this.lat_std, ll_x, ll_y, ur_x, ur_y, this.a, this.b, this.thin_x * thin_x, this.thin_y * thin_y);
472
+ }
473
+ }
474
+ /** An unstructured grid defined by a list of latitudes and longitudes */
475
+ class UnstructuredGrid extends Grid {
476
+ /**
477
+ * Create an unstructured grid
478
+ * @param coords - The lat/lon coordinates of the grid points
479
+ */
480
+ constructor(coords, zoom) {
481
+ const MAX_DIM = 4096;
482
+ super('unstructured', true, Math.min(coords.length, MAX_DIM), Math.floor(coords.length / MAX_DIM) + 1);
483
+ this.coords = coords;
484
+ this.zoom_arg = zoom === undefined ? null : zoom;
485
+ this.zoom_cache = new Cache((thin_fac) => {
486
+ const MAP_MAX_ZOOM = 24;
487
+ const offset = Math.log2(thin_fac);
488
+ const kd_nodes = this.coords.map(c => ({ ...new LngLat(c.lon, c.lat).toMercatorCoord(), min_zoom: MAP_MAX_ZOOM }));
489
+ const tree = new kdTree([...kd_nodes], (a, b) => Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y)), ['x', 'y']);
490
+ const recursiveThin = (x, y, depth) => {
491
+ const size = Math.pow(0.5, depth + 1);
492
+ const nodes = tree.nearest({ x: x, y: y, min_zoom: 0 }, 2, size);
493
+ if (nodes.length > 0) {
494
+ const [node, dist] = nodes.sort((a, b) => a[1] - b[1])[0];
495
+ if (node.min_zoom == MAP_MAX_ZOOM) {
496
+ node.min_zoom = Math.max(depth - offset, 0);
497
+ }
498
+ }
499
+ if (nodes.length > 1 && depth < MAP_MAX_ZOOM + offset) {
500
+ recursiveThin(x - size / 2, y - size / 2, depth + 1);
501
+ recursiveThin(x + size / 2, y - size / 2, depth + 1);
502
+ recursiveThin(x - size / 2, y + size / 2, depth + 1);
503
+ recursiveThin(x + size / 2, y + size / 2, depth + 1);
504
+ }
505
+ };
506
+ recursiveThin(0.5, 0.5, 0);
507
+ return new Uint8Array(kd_nodes.map(n => n.min_zoom));
508
+ });
509
+ }
510
+ /** @internal */
511
+ copy() {
512
+ return new UnstructuredGrid(this.coords);
513
+ }
514
+ /** @internal */
515
+ getEarthCoords() {
516
+ return { lons: new Float32Array(this.coords.map(c => c.lon)), lats: new Float32Array(this.coords.map(c => c.lat)) };
517
+ }
518
+ /** @internal */
519
+ getGridCoords() {
520
+ const { lons, lats } = this.getEarthCoords();
521
+ return { x: lons, y: lats };
522
+ }
523
+ /** @internal */
524
+ transform(x, y, opts) {
525
+ return [x, y];
526
+ }
527
+ /** @internal */
528
+ getMinVisibleZoom(thin_fac) {
529
+ if (this.zoom_arg !== null)
530
+ return this.zoom_arg;
531
+ return this.zoom_cache.getValue(thin_fac);
532
+ }
533
+ /** @internal */
534
+ getThinnedGrid(thin_fac, map_max_zoom) {
535
+ const min_zoom = this.getMinVisibleZoom(thin_fac);
536
+ return new UnstructuredGrid(this.coords.filter((ll, ill) => min_zoom[ill] <= map_max_zoom), min_zoom.filter(ll => ll <= map_max_zoom));
537
+ }
538
+ /** @internal */
539
+ thinDataArray(original_grid, ary) {
540
+ let i_new = 0;
541
+ const arrayType = getArrayConstructor(ary);
542
+ const new_data = new arrayType(this.ni * this.nj);
543
+ for (let i = 0; i < original_grid.coords.length; i++) {
544
+ if (this.coords[i_new].lat == original_grid.coords[i].lat && this.coords[i_new].lon == original_grid.coords[i].lon) {
545
+ new_data[i_new++] = ary[i];
546
+ if (i_new >= this.coords.length)
547
+ break;
548
+ }
549
+ }
550
+ return new_data;
551
+ }
552
+ sampleNearestGridPoint(lon, lat, ary) {
553
+ // TAS: This is gonna be slow. Need to think about using the kdTree here.
554
+ const idx = argMin(this.coords.map(c => (c.lon - lon) * (c.lon - lon) + (c.lat - lat) * (c.lat - lat)));
555
+ return { sample: ary[idx], sample_lon: this.coords[idx].lon, sample_lat: this.coords[idx].lat };
342
556
  }
343
557
  }
344
- export { Grid, PlateCarreeGrid, PlateCarreeRotatedGrid, LambertGrid };
558
+ // class GeostationaryImage extends Grid {
559
+ // public readonly satellite_lon: number;
560
+ // public readonly satellite_lat: number;
561
+ // private readonly vpp: (a: number, b: number, opts?: {inverse: boolean}) => [number, number];
562
+ // constructor(ni: number, nj: number, satellite_lon: number, satellite_lat: number) {
563
+ // super('geostationary', false, ni, nj);
564
+ // this.satellite_lon = satellite_lon;
565
+ // this.satellite_lat = satellite_lat;
566
+ // this.vpp = verticalPerspective({lon_0: satellite_lon, lat_0: satellite_lat, alt: 35786000.});
567
+ // }
568
+ // public getEarthCoords(): EarthCoords {}
569
+ // public getGridCoords(): GridCoords {}
570
+ // public transform(x: number, y: number, opts?: {inverse?: boolean}): [number, number] {
571
+ // opts = opts === undefined ? {}: opts;
572
+ // const inverse = opts.inverse === undefined ? false : opts.inverse;
573
+ // return this.vpp(x, y, {inverse: inverse});
574
+ // }
575
+ // public sampleNearestGridPoint(lon: number, lat: number, ary: TypedArray): {sample: number, sample_lon: number, sample_lat: number} {
576
+ // const [x, y] = this.transform(lon, lat);
577
+ // const {x: xs, y: ys} = this.getGridCoords();
578
+ // const ll_x = xs[0];
579
+ // const ur_x = xs[xs.length - 1];
580
+ // const dx = xs[1] - xs[0];
581
+ // const ll_y = ys[0];
582
+ // const ur_y = ys[ys.length - 1];
583
+ // const dy = ys[1] - ys[0];
584
+ // 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) {
585
+ // return {sample: NaN, sample_lon: NaN, sample_lat: NaN};
586
+ // }
587
+ // const i_min = argMin(xs.map(xv => Math.abs(xv - x)));
588
+ // const j_min = argMin(ys.map(yv => Math.abs(yv - y)));
589
+ // const idx = i_min + j_min * this.ni;
590
+ // const [lon_min, lat_min] = this.transform(xs[i_min], ys[j_min], {inverse: true});
591
+ // return {sample: ary[idx], sample_lon: lon_min, sample_lat: lat_min};
592
+ // }
593
+ // public getThinnedGrid(thin_fac: number, map_max_zoom: number): Grid {}
594
+ // public thinDataArray<ArrayType extends TypedArray>(original_grid: Grid, ary: ArrayType): ArrayType {}
595
+ // public getMinVisibleZoom(thin_fac: number): Uint8Array {}
596
+ // public copy() {
597
+ // return new GeostationaryImage(this.ni, this.nj, this.satellite_lon, this.satellite_lat);
598
+ // }
599
+ // }
600
+ export { Grid, StructuredGrid, PlateCarreeGrid, PlateCarreeRotatedGrid, LambertGrid, UnstructuredGrid };