autumnplot-gl 2.0.0-beta.1

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.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # autumnplot-gl
2
+ Hardware-accelerated geospatial data plotting in the browser
3
+
4
+ ## Links
5
+ [Github](https://github.com/tsupinie/autumnplot-gl) | [API docs](https://tsupinie.github.io/autumnplot-gl/)
6
+
7
+ ## What is this?
8
+ Lots of meteorological data web sites have a model where the data live on a central server, get plotted on the server, and then the server serves static images to the client. This creates a bottleneck where adding fields and view sectors takes exponentially more processing power for the server. One way around this is to offload the plotting to the client and to have the browser plot the data on a pan-and-zoomable map. Unfortunately, in the past, this has required developing low-level plotting code, and depending on the mapping library, the performance may be poor.
9
+
10
+ autumnplot-gl provides a solution to this problem by making hardware-accelerated data plotting in the browser easy. This was designed with meteorological data in mind, but anyone wanting to contour geospatial data on a map can use autumnplot-gl.
11
+
12
+ ## Usage
13
+ autumnplot-gl is designed to be used with either [Mapbox GL JS](https://docs.mapbox.com/mapbox-gl-js/guides/) or [MapLibre GL JS](https://maplibre.org/maplibre-gl-js-docs/) mapping libraries. Pre-built autumnplot-gl javascript files area available [here](https://tsupinie.github.io/autumnplot-gl/dist/). Adding them to your page exposes the API via the `apgl` environment variable.
14
+
15
+ ### A basic contour plot
16
+ The first step in plotting data is to create a grid. Currently, the only supported grids are PlateCarree (a.k.a. Lat/Lon) and Lambert Conformal Conic.
17
+
18
+ ```javascript
19
+ // Create a grid object that covers the continental United States
20
+ const nx = 121, ny = 61;
21
+ const grid = new apgl.PlateCarreeGrid(nx, ny, -130, 20, -65, 55);
22
+ ```
23
+
24
+ Next, create a RawScalarField with the data. autumnplot-gl doesn't care about how data get to the browser, but it should end up in a Float32Array in row-major order with the first element being at the southwest corner of the grid. A future version might include support for reading from, say, a Zarr file. Once you have your data in that format, to create the raw data field:
25
+
26
+ ```javascript
27
+ // Create the raw data field
28
+ const height_field = new apgl.RawScalarField(grid, height_data);
29
+ ```
30
+
31
+ Next, to contour the field, create a Contour object and pass it some options. At this time, a somewhat limited set of options is supported, but I do plan to expand this.
32
+
33
+ ```javascript
34
+ // Contour the data
35
+ const height_contour = new apgl.Contour(height_field, {color: '#000000', interval: 30});
36
+ ```
37
+
38
+ Next, create the actual layer that gets added to the map. The first argument (`'height-contour'` here) is an id. It doesn't mean much, but it does need to be unique between the different `PlotLayer`s you add to the map.
39
+
40
+ ```javascript
41
+ // Create the map layer
42
+ const height_layer = new apgl.PlotLayer('height-contour', height_contour);
43
+ ```
44
+
45
+ Finally, add it to the map. The interface for Mapbox and MapLibre are the same, at least currently, though there's nothing that says they'll stay that way in the future. Assuming you're using MapLibre:
46
+
47
+ ```javascript
48
+ const map = new maplibregl.Map({
49
+ container: 'map',
50
+ style: 'https://api.maptiler.com/maps/basic-v2/style.json?key=' + maptiler_api_key,
51
+ center: [-97.5, 38.5],
52
+ zoom: 4
53
+ });
54
+
55
+ map.on('load', () => {
56
+ map.addLayer(height_layer, 'railway_transit_tunnel');
57
+ });
58
+ ```
59
+
60
+ The `'railway_transit_tunnel'` argument is a layer in the map style, and this means to add your layer just below that layer on the map. This usually produces better results than just blindly slapping all your layers on top of all the map (though the map style itself may require some tweaking to produce the best results).
61
+
62
+ ### Barbs
63
+
64
+ Wind barb plotting is similar to the contours, but it requires using a `RawVectorField` with u and v data.
65
+
66
+ ```javascript
67
+ const vector_field = new apgl.RawVectorField(grid, u_data, v_data);
68
+ const barbs = new apgl.Barbs(vector_field, {color: '#000000', thin_fac: 16});
69
+ const barb_layer = new apgl.PlotLayer('barbs', barbs);
70
+
71
+ map.on('load', () => {
72
+ map.addLayer(barb_layer, 'railway_transit_tunnel');
73
+ });
74
+ ```
75
+
76
+ The wind barbs are automatically rotated based on the grid projection. Also, the density of the wind barbs is automatically varied based on the map zoom level. The `'thin_fac': 16` option means to plot every 16th wind barb in the i and j directions, and this is defined at zoom level 1. So at zoom level 2, it will plot every 8th wind barb, and at zoom level 3 every 4th wind barb, and so on. Because it divides in 2 for every deeper zoom level, `'thin_fac'` should be a power of 2.
77
+
78
+ ### Filled contours
79
+
80
+ Plotting filled contours is also similar to plotting regular contours, but there's some additional steps for the color map. A couple color maps are available by default (see [here](#built-in-color-maps) for more details), but if you have the colors you want, creating your own is (relatively) painless (hopefully). First, set up the colormap. Here, we'll just use the bluered colormap included by default.
81
+
82
+ ```javascript
83
+ const colormap = apgl.colormaps.bluered(-10, 10, 20);
84
+ const fills = new apgl.ContourFilled(height, {cmap: colormap});
85
+ const height_fill_layer = new apgl.PlotLayer('height-fill', fills);
86
+
87
+ map.on('load', () => {
88
+ map.addLayer(height_fill_layer, 'railway_transit_tunnel');
89
+ });
90
+ ```
91
+
92
+ Normally, when you have color-filled contours, you have a color bar on the plot. To create an SVG color bar:
93
+
94
+ ```javascript
95
+ const colorbar_svg = apgl.makeColorBar(colormap, {label: "Height Perturbation (m)",
96
+ ticks: [-10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10],
97
+ orientation: 'horizontal',
98
+ tick_direction: 'bottom'});
99
+
100
+ document.getElementById('colorbar-container').appendChild(colorbar_svg);
101
+ ```
102
+
103
+ ### Varying the data plots
104
+ The previous steps have gone through plotting a static dataset on a map, but in many instances, you want to view a dataset that changes, say over time. Rather than continually remove and add new layers when the user changes the time, which would get tedious, waste video RAM, and probably wouldn't perform very well, autumnplot-gl provides `MultiPlotLayer`, which allows the plotted data to easily and quickly change over time (or height or any other axis that might be relevant).
105
+
106
+ ```javascript
107
+ // Contour some data
108
+ const height_contour_f00 = new apgl.Contour(grid, height_f00);
109
+ const height_contour_f01 = new apgl.Contour(grid, height_f01);
110
+ const height_contour_f02 = new apgl.Contour(grid, height_f02);
111
+
112
+ // Create a varying map layer
113
+ const height_layer_time = new apgl.MultiPlotLayer('height-contour-time');
114
+
115
+ // Add the contoured data to it
116
+ height_layer_time.addField(height_contour_f00, '20230112_1200');
117
+ height_layer_time.addField(height_contour_f01, '20230112_1300');
118
+ height_layer_time.addField(height_contour_f02, '20230112_1400');
119
+
120
+ // Add to the map like normal
121
+ map.on('load', () => {
122
+ map.addLayer(height_layer_time, 'railway_transit_tunnel');
123
+ });
124
+ ```
125
+
126
+ The second argument to `addField()` is the key to associate with this field. This example uses the absolute time, but you could just as easily use `'f00'`, `'f01'`, ... or anything else that's relevant as long as it's unique. Now to set the active time (i.e., the time that gets plotted):
127
+
128
+ ```javascript
129
+ // Set the active field in the map layer (the map updates automatically)
130
+ height_layer.setActiveKey('20230112_1200');
131
+ ```
132
+
133
+ ## Built-in color maps
134
+ autumnplot-gl comes with several built-in color maps, accessible from `apgl.colormaps`. These are basic blue/red and red/blue diverging color maps plus a selection from [PivotalWeather](https://www.pivotalweather.com). The blue/red and red/blue are functions that take a minimum contour level, a maximum contour level, and a number of colors. For example, this creates a blue/red colormap starting at -10, ending at 10, and with 20 colors:
135
+
136
+ ```javascript
137
+ const colormap = apgl.colormaps.bluered(-10, 10, 20);
138
+ ```
139
+
140
+ Here are all the colormaps available:
141
+
142
+ ![colormaps](https://user-images.githubusercontent.com/885575/219983547-b5dd5603-f882-43f5-b57a-e19295fb4b64.png)
143
+
144
+ ## Map tiles
145
+ The above exmple uses map tiles from [Maptiler](https://www.maptiler.com/). Map tiles from Maptiler or Mapbox or others are free up to a (reasonably generous) limit, but the pricing can be a tad steep after reaching the limit. The tiles from these services are extremely detailed, and really what you're paying for there is the hardware to store, process, and serve that data. While these tiles are very nice, the detail is way overkill for a lot of uses in meteorology.
146
+
147
+ So, I've created some [less-detailed map tiles](https://tsupinie.github.io/autumnplot-gl/tiles/) that are small enough that they can be hosted without dedicated hardware. However the tradeoff is that they're only useful down to zoom level 8 or 9 on the map, such that the viewport is somewhere between half a US state and a few counties in size. If that's good enough for you, then these tiles could be useful.
148
+
149
+ ## Conspicuous absences
150
+ A few capabilities are missing from this library as of v2.0.
151
+ * Helper functions for reading from specific data formats. For instance, I'd like to add support for reading from a zarr file.
152
+ * A whole bunch of little things that ought to be fairly straightforward like tweaking the size of the wind barbs and contour thicknesses.
153
+ * Support for contour labeling. I'd like to add it, but I'm not really sure how I'd do it with the contours as I've implemented them. Any WebGL gurus, get in touch.
154
+
155
+ ## Closing thoughts
156
+ Even though autumnplot-gl is currently an extremely new package with relatively limited capability, I hope folks see potential and find it useful. Any contributions to fill out some missing features are welcome.
@@ -0,0 +1,37 @@
1
+ interface WindProfile {
2
+ lat: number;
3
+ lon: number;
4
+ jlat: number;
5
+ ilon: number;
6
+ smu: number;
7
+ smv: number;
8
+ u: Float32Array;
9
+ v: Float32Array;
10
+ z: Float32Array;
11
+ }
12
+ interface BillboardSpec {
13
+ BB_WIDTH: number;
14
+ BB_HEIGHT: number;
15
+ BB_TEX_WIDTH: number;
16
+ BB_TEX_HEIGHT: number;
17
+ BB_MAG_BIN_SIZE: number;
18
+ BB_MAG_WRAP: number;
19
+ BB_MAG_MAX: number;
20
+ }
21
+ interface PolylineSpec {
22
+ origin: Float32Array;
23
+ verts: Float32Array;
24
+ extrusion: Float32Array;
25
+ zoom: Float32Array;
26
+ texcoords: Float32Array;
27
+ }
28
+ interface LineSpec {
29
+ verts: [number, number][];
30
+ origin: [number, number];
31
+ zoom: number;
32
+ texcoords: [number, number][];
33
+ }
34
+ type WebGLAnyRenderingContext = WebGLRenderingContext | WebGL2RenderingContext;
35
+ declare function isWebGL2Ctx(gl: WebGLAnyRenderingContext): gl is WebGL2RenderingContext;
36
+ export { isWebGL2Ctx };
37
+ export type { WindProfile, BillboardSpec, PolylineSpec, LineSpec, WebGLAnyRenderingContext };
@@ -0,0 +1,4 @@
1
+ function isWebGL2Ctx(gl) {
2
+ return gl.getParameter(gl.VERSION).includes('WebGL 2.0');
3
+ }
4
+ export { isWebGL2Ctx };
package/lib/Barbs.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ import { PlotComponent } from "./PlotComponent";
2
+ import { BillboardCollection } from './BillboardCollection';
3
+ import { RawVectorField } from "./RawField";
4
+ import { MapType } from "./Map";
5
+ import { WebGLAnyRenderingContext } from "./AutumnTypes";
6
+ interface BarbsOptions {
7
+ /**
8
+ * The color to use for the barbs as a hex color string;.
9
+ * @default '#000000'
10
+ */
11
+ color?: string;
12
+ /**
13
+ * How much to thin the barbs at zoom level 1 on the map. This effectively means to plot every `n`th barb in the i and j directions, where `n` =
14
+ * `thin_fac`. `thin_fac` should be a power of 2.
15
+ * @default 1
16
+ */
17
+ thin_fac?: number;
18
+ }
19
+ interface BarbsGLElems {
20
+ map: MapType | null;
21
+ barb_billboards: BillboardCollection | null;
22
+ }
23
+ /**
24
+ * A class representing a field of wind barbs. The barbs are automatically thinned based on the zoom level on the map; the user only has to provide a
25
+ * thinning factor at zoom level 1.
26
+ * @example
27
+ * // Create a barb field with black barbs and plotting every 16th wind barb in both i and j at zoom level 1
28
+ * const vector_field = new RawVectorField(grid, u_data, v_data);
29
+ * const barbs = new Barbs(vector_field, {color: '#000000', thin_fac: 16});
30
+ */
31
+ declare class Barbs extends PlotComponent {
32
+ /** The vector field */
33
+ readonly fields: RawVectorField;
34
+ readonly color: [number, number, number];
35
+ readonly thin_fac: number;
36
+ /** @private */
37
+ gl_elems: BarbsGLElems | null;
38
+ /**
39
+ * Create a field of wind barbs
40
+ * @param fields - The vector field to plot as barbs
41
+ * @param opts - Options for creating the wind barbs
42
+ */
43
+ constructor(fields: RawVectorField, opts: BarbsOptions);
44
+ /**
45
+ * @internal
46
+ * Add the barb field to a map
47
+ */
48
+ onAdd(map: MapType, gl: WebGLAnyRenderingContext): Promise<void>;
49
+ /**
50
+ * @internal
51
+ * Render the barb field
52
+ */
53
+ render(gl: WebGLAnyRenderingContext, matrix: number[]): void;
54
+ }
55
+ export default Barbs;
56
+ export type { BarbsOptions };
package/lib/Barbs.js ADDED
@@ -0,0 +1,155 @@
1
+ import { PlotComponent } from "./PlotComponent";
2
+ import { BillboardCollection } from './BillboardCollection';
3
+ import { hex2rgba } from './utils';
4
+ const BARB_DIMS = {
5
+ BB_WIDTH: 85,
6
+ BB_HEIGHT: 256,
7
+ BB_TEX_WIDTH: 1024,
8
+ BB_TEX_HEIGHT: 1024,
9
+ BB_MAG_MAX: 235,
10
+ BB_MAG_WRAP: 60,
11
+ BB_MAG_BIN_SIZE: 5,
12
+ };
13
+ function _createBarbTexture() {
14
+ let canvas = document.createElement('canvas');
15
+ canvas.width = BARB_DIMS.BB_TEX_WIDTH;
16
+ canvas.height = BARB_DIMS.BB_TEX_HEIGHT;
17
+ function drawWindBarb(ctx, tipx, tipy, mag) {
18
+ const elem_full_size = BARB_DIMS.BB_WIDTH / 2 - 4;
19
+ const elem_spacing = elem_full_size / 2;
20
+ if (mag < 2.5) {
21
+ ctx.beginPath();
22
+ ctx.arc(tipx, tipy, elem_full_size / 2, 0, 2 * Math.PI);
23
+ ctx.stroke();
24
+ }
25
+ else {
26
+ let elem_pos = 0;
27
+ let mag_countdown = mag;
28
+ let staff_length = 0;
29
+ const n_flags = Math.floor((mag_countdown + 2.5) / 50);
30
+ staff_length += n_flags * elem_full_size / 2 + elem_spacing
31
+ + (n_flags - 1) * elem_spacing / 2;
32
+ mag_countdown -= n_flags * 50;
33
+ const n_full_barbs = Math.floor((mag_countdown + 2.5) / 10);
34
+ staff_length += n_full_barbs * elem_spacing;
35
+ mag_countdown -= n_full_barbs * 10;
36
+ const n_half_barbs = Math.floor((mag_countdown + 2.5) / 5);
37
+ staff_length += n_half_barbs * elem_spacing;
38
+ if (mag < 7.5) {
39
+ staff_length += elem_spacing;
40
+ }
41
+ staff_length = Math.max(120, staff_length);
42
+ // staff
43
+ ctx.beginPath();
44
+ ctx.moveTo(tipx, tipy);
45
+ ctx.lineTo(tipx, tipy + staff_length);
46
+ mag_countdown = mag;
47
+ elem_pos = tipy + staff_length;
48
+ let last_was_flag = false;
49
+ let first_elem = true;
50
+ while (mag_countdown > 47.5) {
51
+ if (last_was_flag)
52
+ elem_pos += elem_spacing / 2;
53
+ // flag
54
+ if (!first_elem) {
55
+ ctx.moveTo(tipx, elem_pos);
56
+ }
57
+ ctx.lineTo(tipx - elem_full_size, elem_pos);
58
+ ctx.lineTo(tipx, elem_pos - elem_full_size / 2);
59
+ elem_pos -= elem_full_size / 2 + elem_spacing;
60
+ mag_countdown -= 50;
61
+ last_was_flag = true;
62
+ first_elem = false;
63
+ }
64
+ while (mag_countdown > 7.5) {
65
+ // full barb
66
+ if (!first_elem) {
67
+ ctx.moveTo(tipx, elem_pos);
68
+ }
69
+ ctx.lineTo(tipx - elem_full_size, elem_pos + elem_full_size / 2);
70
+ elem_pos -= elem_spacing;
71
+ mag_countdown -= 10;
72
+ first_elem = false;
73
+ }
74
+ if (mag < 7.5) {
75
+ elem_pos -= elem_spacing;
76
+ }
77
+ while (mag_countdown > 2.5) {
78
+ // half barb
79
+ ctx.moveTo(tipx, elem_pos);
80
+ ctx.lineTo(tipx - elem_full_size / 2, elem_pos + elem_full_size / 4);
81
+ mag_countdown -= 5;
82
+ }
83
+ ctx.stroke();
84
+ }
85
+ }
86
+ let ctx = canvas.getContext('2d');
87
+ if (ctx === null) {
88
+ throw "Could not get rendering context for the wind barb canvas";
89
+ }
90
+ ctx.lineWidth = 8;
91
+ ctx.miterLimit = 4;
92
+ for (let ibarb = 0; ibarb <= BARB_DIMS.BB_MAG_MAX; ibarb += BARB_DIMS.BB_MAG_BIN_SIZE) {
93
+ const x_pos = (ibarb % BARB_DIMS.BB_MAG_WRAP) / BARB_DIMS.BB_MAG_BIN_SIZE * BARB_DIMS.BB_WIDTH + BARB_DIMS.BB_WIDTH / 2;
94
+ const y_pos = Math.floor(ibarb / BARB_DIMS.BB_MAG_WRAP) * BARB_DIMS.BB_HEIGHT + BARB_DIMS.BB_WIDTH / 2;
95
+ drawWindBarb(ctx, x_pos, y_pos, ibarb);
96
+ }
97
+ return canvas;
98
+ }
99
+ let BARB_TEXTURE = null;
100
+ /**
101
+ * A class representing a field of wind barbs. The barbs are automatically thinned based on the zoom level on the map; the user only has to provide a
102
+ * thinning factor at zoom level 1.
103
+ * @example
104
+ * // Create a barb field with black barbs and plotting every 16th wind barb in both i and j at zoom level 1
105
+ * const vector_field = new RawVectorField(grid, u_data, v_data);
106
+ * const barbs = new Barbs(vector_field, {color: '#000000', thin_fac: 16});
107
+ */
108
+ class Barbs extends PlotComponent {
109
+ /**
110
+ * Create a field of wind barbs
111
+ * @param fields - The vector field to plot as barbs
112
+ * @param opts - Options for creating the wind barbs
113
+ */
114
+ constructor(fields, opts) {
115
+ super();
116
+ this.fields = fields;
117
+ const color = hex2rgba(opts.color || '#000000');
118
+ this.color = [color[0], color[1], color[2]];
119
+ this.thin_fac = opts.thin_fac || 1;
120
+ this.gl_elems = null;
121
+ }
122
+ /**
123
+ * @internal
124
+ * Add the barb field to a map
125
+ */
126
+ async onAdd(map, gl) {
127
+ gl.getExtension('OES_texture_float');
128
+ gl.getExtension('OES_texture_float_linear');
129
+ const map_max_zoom = map.getMaxZoom();
130
+ if (BARB_TEXTURE === null) {
131
+ BARB_TEXTURE = _createBarbTexture();
132
+ }
133
+ const barb_image = { format: gl.RGBA, type: gl.UNSIGNED_BYTE, image: BARB_TEXTURE, mag_filter: gl.NEAREST };
134
+ const barb_billboards = new BillboardCollection(gl, this.fields, this.thin_fac, map_max_zoom, barb_image, BARB_DIMS, this.color, 0.1);
135
+ this.gl_elems = {
136
+ map: map, barb_billboards: barb_billboards
137
+ };
138
+ }
139
+ /**
140
+ * @internal
141
+ * Render the barb field
142
+ */
143
+ render(gl, matrix) {
144
+ if (this.gl_elems === null)
145
+ return;
146
+ const gl_elems = this.gl_elems;
147
+ const zoom = gl_elems.map.getZoom();
148
+ const map_width = gl_elems.map.getCanvas().width;
149
+ const map_height = gl_elems.map.getCanvas().height;
150
+ const bearing = gl_elems.map.getBearing();
151
+ const pitch = gl_elems.map.getPitch();
152
+ gl_elems.barb_billboards.render(gl, matrix, [map_width, map_height], zoom, bearing, pitch);
153
+ }
154
+ }
155
+ export default Barbs;
@@ -0,0 +1,17 @@
1
+ import { BillboardSpec, WebGLAnyRenderingContext } from "./AutumnTypes";
2
+ import { RawVectorField } from "./RawField";
3
+ import { WGLBuffer, WGLProgram, WGLTexture, WGLTextureSpec } from "autumn-wgl";
4
+ declare class BillboardCollection {
5
+ readonly spec: BillboardSpec;
6
+ readonly color: [number, number, number];
7
+ readonly size_multiplier: number;
8
+ readonly program: WGLProgram;
9
+ vertices: WGLBuffer | null;
10
+ texcoords: WGLBuffer | null;
11
+ readonly texture: WGLTexture;
12
+ readonly u_texture: WGLTexture;
13
+ readonly v_texture: WGLTexture;
14
+ constructor(gl: WebGLAnyRenderingContext, field: RawVectorField, thin_fac: number, max_zoom: number, billboard_image: WGLTextureSpec, billboard_spec: BillboardSpec, billboard_color: [number, number, number], billboard_size_mult: number);
15
+ render(gl: WebGLAnyRenderingContext, matrix: number[], [map_width, map_height]: [number, number], map_zoom: number, map_bearing: number, map_pitch: number): void;
16
+ }
17
+ export { BillboardCollection };
@@ -0,0 +1,149 @@
1
+ import { isWebGL2Ctx } from "./AutumnTypes";
2
+ import { WGLProgram, WGLTexture } from "autumn-wgl";
3
+ const billboard_vertex_shader_src = `uniform mat4 u_matrix;
4
+
5
+ attribute vec3 a_pos;
6
+ attribute vec2 a_tex_coord;
7
+ uniform lowp float u_bb_size;
8
+ uniform lowp float u_map_aspect;
9
+ uniform lowp float u_zoom;
10
+ uniform highp float u_map_bearing;
11
+ uniform lowp float u_bb_width;
12
+ uniform lowp float u_bb_height;
13
+ uniform highp float u_bb_mag_bin_size;
14
+
15
+ uniform highp float u_bb_mag_wrap;
16
+
17
+ uniform sampler2D u_u_sampler;
18
+ uniform sampler2D u_v_sampler;
19
+
20
+ varying highp vec2 v_tex_coord;
21
+
22
+ mat4 scalingMatrix(float x_scale, float y_scale, float z_scale) {
23
+ return mat4(x_scale, 0.0, 0.0, 0.0,
24
+ 0.0, y_scale, 0.0, 0.0,
25
+ 0.0, 0.0, z_scale, 0.0,
26
+ 0.0, 0.0, 0.0, 1.0);
27
+ }
28
+
29
+ mat4 rotationZMatrix(float angle) {
30
+ float s = sin(angle);
31
+ float c = cos(angle);
32
+
33
+ return mat4( c, s, 0., 0.,
34
+ -s, c, 0., 0.,
35
+ 0., 0., 1., 0.,
36
+ 0., 0., 0., 1.);
37
+ }
38
+
39
+ mat4 rotationXMatrix(float angle) {
40
+ float s = sin(angle);
41
+ float c = cos(angle);
42
+
43
+ return mat4( 1., 0., 0., 0.,
44
+ 0., c, s, 0.,
45
+ 0., -s, c, 0.,
46
+ 0., 0., 0., 1.);
47
+ }
48
+
49
+ void main() {
50
+ vec4 pivot_pos = u_matrix * vec4(a_pos.xy, 0.0, 1.0);
51
+ highp float zoom_corner = a_pos.z;
52
+ lowp float min_zoom = floor(zoom_corner / 4.0);
53
+ lowp float corner = mod(zoom_corner, 4.0);
54
+
55
+ highp float u = texture2D(u_u_sampler, a_tex_coord).r;
56
+ highp float v = texture2D(u_v_sampler, a_tex_coord).r;
57
+
58
+ lowp float bb_aspect = u_bb_width / u_bb_height;
59
+ lowp float ang = atan(v, u) - 3.141592654 / 2.0;
60
+ highp float mag = length(vec2(u, v));
61
+ mag = floor(mag / u_bb_mag_bin_size + 0.5) * u_bb_mag_bin_size;
62
+
63
+ vec4 offset = vec4(0.0, 0.0, 0.0, 0.0);
64
+ vec2 texcoord = vec2(0.0, 0.0);
65
+
66
+ if (u_zoom >= min_zoom) {
67
+ vec2 tex_loc = vec2(mod(mag, u_bb_mag_wrap) / u_bb_mag_bin_size * u_bb_width, floor(mag / u_bb_mag_wrap) * u_bb_height);
68
+
69
+ if (corner < 0.5) {
70
+ offset = vec4(-u_bb_size, u_bb_size, 0., 0.);
71
+ texcoord = tex_loc;
72
+ }
73
+ else if (corner < 1.5) {
74
+ offset = vec4(u_bb_size, u_bb_size, 0., 0.);
75
+ texcoord = tex_loc + vec2(u_bb_width, 0.0);
76
+ }
77
+ else if (corner < 2.5) {
78
+ offset = vec4(-u_bb_size, -u_bb_size * (2. / bb_aspect - 1.), 0., 0.);
79
+ texcoord = tex_loc + vec2(0.0, u_bb_height);
80
+ }
81
+ else if (corner < 3.5) {
82
+ offset = vec4(u_bb_size, -u_bb_size * (2. / bb_aspect - 1.), 0., 0.);
83
+ texcoord = tex_loc + vec2(u_bb_width, u_bb_height);
84
+ }
85
+
86
+ mat4 barb_rotation = rotationZMatrix(ang + radians(u_map_bearing));
87
+ mat4 map_stretch_matrix = scalingMatrix(1.0, 1. / u_map_aspect, 1.0);
88
+ offset = map_stretch_matrix * barb_rotation * offset;
89
+ }
90
+
91
+ gl_Position = pivot_pos + offset;
92
+ v_tex_coord = texcoord;
93
+ }`
94
+ const billboard_fragment_shader_src = `varying highp vec2 v_tex_coord;
95
+
96
+ uniform sampler2D u_sampler;
97
+ uniform lowp vec3 u_bb_color;
98
+
99
+ void main() {
100
+ lowp vec4 tex_color = texture2D(u_sampler, v_tex_coord);
101
+ gl_FragColor = vec4(u_bb_color, tex_color.a);
102
+ }`
103
+ class BillboardCollection {
104
+ constructor(gl, field, thin_fac, max_zoom, billboard_image, billboard_spec, billboard_color, billboard_size_mult) {
105
+ this.spec = billboard_spec;
106
+ this.color = billboard_color;
107
+ this.size_multiplier = billboard_size_mult;
108
+ this.program = new WGLProgram(gl, billboard_vertex_shader_src, billboard_fragment_shader_src);
109
+ this.vertices = null;
110
+ this.texcoords = null;
111
+ const n_density_tiers = Math.log2(thin_fac);
112
+ const n_inaccessible_tiers = Math.max(n_density_tiers + 1 - max_zoom, 0);
113
+ const trim_inaccessible = Math.pow(2, n_inaccessible_tiers);
114
+ const earth_relative = field.getThinnedField(trim_inaccessible, trim_inaccessible).toEarthRelative();
115
+ const u_thin = earth_relative.u;
116
+ const v_thin = earth_relative.v;
117
+ (async () => {
118
+ const { vertices, texcoords } = await earth_relative.grid.getWGLBillboardBuffers(gl, thin_fac / trim_inaccessible, max_zoom);
119
+ this.vertices = vertices;
120
+ this.texcoords = texcoords;
121
+ })();
122
+ const format = isWebGL2Ctx(gl) ? gl.R32F : gl.LUMINANCE;
123
+ const u_image = { 'format': format, 'type': gl.FLOAT,
124
+ 'width': u_thin.grid.ni, 'height': u_thin.grid.nj, 'image': u_thin.data,
125
+ 'mag_filter': gl.NEAREST,
126
+ };
127
+ const v_image = { 'format': format, 'type': gl.FLOAT,
128
+ 'width': v_thin.grid.ni, 'height': v_thin.grid.nj, 'image': v_thin.data,
129
+ 'mag_filter': gl.NEAREST,
130
+ };
131
+ this.texture = new WGLTexture(gl, billboard_image);
132
+ this.u_texture = new WGLTexture(gl, u_image);
133
+ this.v_texture = new WGLTexture(gl, v_image);
134
+ }
135
+ render(gl, matrix, [map_width, map_height], map_zoom, map_bearing, map_pitch) {
136
+ if (this.vertices === null || this.texcoords === null)
137
+ return;
138
+ const bb_size = this.spec.BB_HEIGHT * (map_height / map_width) * this.size_multiplier;
139
+ const bb_width = this.spec.BB_WIDTH / this.spec.BB_TEX_WIDTH;
140
+ const bb_height = this.spec.BB_HEIGHT / this.spec.BB_TEX_HEIGHT;
141
+ this.program.use({ 'a_pos': this.vertices, 'a_tex_coord': this.texcoords }, { 'u_bb_size': bb_size, 'u_bb_width': bb_width, 'u_bb_height': bb_height,
142
+ 'u_bb_mag_bin_size': this.spec.BB_MAG_BIN_SIZE, 'u_bb_mag_wrap': this.spec.BB_MAG_WRAP,
143
+ 'u_bb_color': this.color, 'u_matrix': matrix, 'u_map_aspect': map_height / map_width, 'u_zoom': map_zoom, 'u_map_bearing': map_bearing }, { 'u_sampler': this.texture, 'u_u_sampler': this.u_texture, 'u_v_sampler': this.v_texture });
144
+ gl.enable(gl.BLEND);
145
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
146
+ this.program.draw();
147
+ }
148
+ }
149
+ export { BillboardCollection };
@@ -0,0 +1,70 @@
1
+ import { Color, ColorMap } from "./Colormap";
2
+ type ColorbarOrientation = 'horizontal' | 'vertical';
3
+ type ColorbarTickDirection = 'top' | 'bottom' | 'left' | 'right';
4
+ interface ColorBarOptions {
5
+ /** The label to place along the color bar */
6
+ label?: string;
7
+ /**
8
+ * An array of numbers to use as the tick locations.
9
+ * @default Use all the levels in the color map provided to {@link makeColorBar}.
10
+ */
11
+ ticks?: number[];
12
+ /**
13
+ * The direction the ticks should face. Valid values are 'left' and 'right' if orientation is 'vertical' and 'top' and
14
+ * 'bottom' if orientation is 'horizontal'.
15
+ * @default 'left' if orientation is 'vertical' and 'bottom' if orientation is 'horizontal'
16
+ */
17
+ tick_direction?: ColorbarTickDirection;
18
+ /**
19
+ * The orientation for the color bar. Valid values are 'horizontal' and 'vertical'.
20
+ * @default 'vertical'
21
+ */
22
+ orientation?: ColorbarOrientation;
23
+ /**
24
+ * A font face to use for the label and tick values.
25
+ * @default 'sans-serif'
26
+ */
27
+ fontface?: string;
28
+ }
29
+ /**
30
+ * Make an SVG containing a color bar. The color bar can either be oriented horizontal or vertical, and a label can be provided.
31
+ * @param colormap - The color map to use
32
+ * @param opts - The options for creating the color bar
33
+ * @returns An SVGElement containing the color bar image.
34
+ * @example
35
+ * // Create the color bar
36
+ * const svg = makeColorBar(color_map, {label: 'Wind Speed (kts)', orientation: 'horizontal',
37
+ * fontface: 'Trebuchet MS'});
38
+ *
39
+ * // Add colorbar to the page
40
+ * document.getElementById('colorbar-container').appendChild(svg);
41
+ */
42
+ declare function makeColorBar(colormap: ColorMap, opts: ColorBarOptions): SVGElement;
43
+ interface PaintballKeyOptions {
44
+ /**
45
+ * The number of columns of entries in the key
46
+ * @default 1
47
+ */
48
+ n_cols?: number;
49
+ /**
50
+ * A font face to use for the label and tick values.
51
+ * @default 'sans-serif'
52
+ */
53
+ fontface?: string;
54
+ }
55
+ /**
56
+ * Make an SVG containing a color key for a paintball plot. The key can be split over any number of columns.
57
+ * @param colors - A list of colors
58
+ * @param labels - The labels corresponding to each color
59
+ * @param opts - The options for creating the color key
60
+ * @returns An SVGElement containing the color bar image.
61
+ * @example
62
+ * // Create the color key
63
+ * const svg = makePaintballKey(colors, labels, {n_cols: 2, fontface: 'Trebuchet MS'});
64
+ *
65
+ * // Add the color key to the page
66
+ * document.getElementById('pb-key-container').appendChild(svg);
67
+ */
68
+ declare function makePaintballKey(colors: (Color | string)[], labels: string[], opts?: PaintballKeyOptions): SVGElement;
69
+ export { makeColorBar, makePaintballKey };
70
+ export type { ColorbarOrientation, ColorbarTickDirection, ColorBarOptions, PaintballKeyOptions };