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.
@@ -0,0 +1,58 @@
1
+ import { PlotComponent } from './PlotComponent';
2
+ import { ColorMap } from './Colormap';
3
+ import { WGLBuffer, WGLProgram, WGLTexture } from 'autumn-wgl';
4
+ import { RawScalarField } from './RawField';
5
+ import { MapType } from './Map';
6
+ import { WebGLAnyRenderingContext } from './AutumnTypes';
7
+ interface ContourFillOptions {
8
+ /** The color map to use when creating the fills */
9
+ cmap: ColorMap;
10
+ /**
11
+ * The opacity for the filled contours
12
+ * @default 1
13
+ */
14
+ opacity?: number;
15
+ }
16
+ interface ContourFillGLElems {
17
+ program: WGLProgram;
18
+ vertices: WGLBuffer;
19
+ fill_texture: WGLTexture;
20
+ texcoords: WGLBuffer;
21
+ cmap_texture: WGLTexture;
22
+ cmap_nonlin_texture: WGLTexture;
23
+ }
24
+ /**
25
+ * A filled contoured field
26
+ * @example
27
+ * // Create a field of filled contours with the provided color map
28
+ * const fill = new ContourFill(wind_speed_field, {cmap: color_map});
29
+ */
30
+ declare class ContourFill extends PlotComponent {
31
+ readonly field: RawScalarField;
32
+ readonly cmap: ColorMap;
33
+ readonly opacity: number;
34
+ /** @private */
35
+ readonly cmap_image: HTMLCanvasElement;
36
+ /** @private */
37
+ readonly index_map: Float32Array;
38
+ /** @private */
39
+ gl_elems: ContourFillGLElems | null;
40
+ /**
41
+ * Create a filled contoured field
42
+ * @param field - The field to create filled contours from
43
+ * @param opts - Options for creating the filled contours
44
+ */
45
+ constructor(field: RawScalarField, opts: ContourFillOptions);
46
+ /**
47
+ * @internal
48
+ * Add the filled contours to a map
49
+ */
50
+ onAdd(map: MapType, gl: WebGLAnyRenderingContext): Promise<void>;
51
+ /**
52
+ * @internal
53
+ * Render the filled contours
54
+ */
55
+ render(gl: WebGLAnyRenderingContext, matrix: number[]): void;
56
+ }
57
+ export default ContourFill;
58
+ export type { ContourFillOptions };
@@ -0,0 +1,125 @@
1
+ import { PlotComponent } from './PlotComponent';
2
+ import { makeTextureImage } from './Colormap';
3
+ import { WGLProgram, WGLTexture } from 'autumn-wgl';
4
+ import { isWebGL2Ctx } from './AutumnTypes';
5
+ const contourfill_vertex_shader_src = `uniform mat4 u_matrix;
6
+
7
+ attribute vec2 a_pos;
8
+ attribute vec2 a_tex_coord;
9
+
10
+ varying highp vec2 v_tex_coord;
11
+
12
+ void main() {
13
+ gl_Position = u_matrix * vec4(a_pos, 0.0, 1.0);
14
+ v_tex_coord = a_tex_coord;
15
+ }`
16
+ const contourfill_fragment_shader_src = `varying highp vec2 v_tex_coord;
17
+
18
+ uniform sampler2D u_fill_sampler;
19
+ uniform sampler2D u_cmap_sampler;
20
+ uniform sampler2D u_cmap_nonlin_sampler;
21
+ uniform highp float u_cmap_min;
22
+ uniform highp float u_cmap_max;
23
+ uniform highp float u_opacity;
24
+ uniform int u_n_index;
25
+
26
+ void main() {
27
+ lowp float index_buffer = 1. / (2. * float(u_n_index));
28
+ highp float fill_val = texture2D(u_fill_sampler, v_tex_coord).r;
29
+ lowp float normed_val = (fill_val - u_cmap_min) / (u_cmap_max - u_cmap_min);
30
+
31
+ if (normed_val < 0.0 || normed_val > 1.0) {
32
+ discard;
33
+ }
34
+
35
+ normed_val = index_buffer + normed_val * (1. - 2. * index_buffer);
36
+ highp float nonlin_val = texture2D(u_cmap_nonlin_sampler, vec2(normed_val, 0.5)).r;
37
+ lowp vec4 color = texture2D(u_cmap_sampler, vec2(nonlin_val, 0.5));
38
+ color.a = color.a * u_opacity;
39
+ gl_FragColor = color;
40
+ }`
41
+ /**
42
+ * A filled contoured field
43
+ * &example
44
+ * // Create a field of filled contours with the provided color map
45
+ * const fill = new ContourFill(wind_speed_field, {cmap: color_map});
46
+ */
47
+ class ContourFill extends PlotComponent {
48
+ /**
49
+ * Create a filled contoured field
50
+ * &param field - The field to create filled contours from
51
+ * &param opts - Options for creating the filled contours
52
+ */
53
+ constructor(field, opts) {
54
+ super();
55
+ this.field = field;
56
+ this.cmap = opts.cmap;
57
+ this.opacity = opts.opacity || 1.;
58
+ this.cmap_image = makeTextureImage(this.cmap);
59
+ const levels = this.cmap.levels;
60
+ const n_lev = levels.length - 1;
61
+ // Build a texture to account for nonlinear colormaps (basically inverts the relationship between
62
+ // the normalized index and the normalized level)
63
+ const n_nonlin = 101;
64
+ const map_norm = [];
65
+ for (let i = 0; i < n_nonlin; i++) {
66
+ map_norm.push(i / (n_nonlin - 1));
67
+ }
68
+ const input_norm = levels.map((lev, ilev) => ilev / n_lev);
69
+ const cmap_norm = levels.map(lev => (lev - levels[0]) / (levels[n_lev] - levels[0]));
70
+ const inv_cmap_norm = map_norm.map(lev => {
71
+ let jlev;
72
+ for (jlev = 0; !(cmap_norm[jlev] <= lev && lev <= cmap_norm[jlev + 1]); jlev++) { }
73
+ const alpha = (lev - cmap_norm[jlev]) / (cmap_norm[jlev + 1] - cmap_norm[jlev]);
74
+ return input_norm[jlev] * (1 - alpha) + input_norm[jlev + 1] * alpha;
75
+ });
76
+ this.index_map = new Float32Array(inv_cmap_norm);
77
+ this.gl_elems = null;
78
+ }
79
+ /**
80
+ * &internal
81
+ * Add the filled contours to a map
82
+ */
83
+ async onAdd(map, gl) {
84
+ // Basic procedure for the filled contours inspired by https://blog.mbq.me/webgl-weather-globe/
85
+ gl.getExtension('OES_texture_float');
86
+ gl.getExtension('OES_texture_float_linear');
87
+ const program = new WGLProgram(gl, contourfill_vertex_shader_src, contourfill_fragment_shader_src);
88
+ const { vertices: verts_buf, texcoords: tex_coords_buf } = await this.field.grid.getWGLBuffers(gl);
89
+ const vertices = verts_buf;
90
+ const texcoords = tex_coords_buf;
91
+ const format = isWebGL2Ctx(gl) ? gl.R32F : gl.LUMINANCE;
92
+ const fill_image = { 'format': format, 'type': gl.FLOAT,
93
+ 'width': this.field.grid.ni, 'height': this.field.grid.nj, 'image': this.field.data,
94
+ 'mag_filter': gl.LINEAR,
95
+ };
96
+ const fill_texture = new WGLTexture(gl, fill_image);
97
+ const cmap_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'image': this.cmap_image, 'mag_filter': gl.NEAREST };
98
+ const cmap_texture = new WGLTexture(gl, cmap_image);
99
+ const cmap_nonlin_image = { 'format': format, 'type': gl.FLOAT,
100
+ 'width': this.index_map.length, 'height': 1,
101
+ 'image': this.index_map,
102
+ 'mag_filter': gl.LINEAR
103
+ };
104
+ const cmap_nonlin_texture = new WGLTexture(gl, cmap_nonlin_image);
105
+ this.gl_elems = {
106
+ program: program, vertices: vertices, texcoords: texcoords,
107
+ fill_texture: fill_texture, cmap_texture: cmap_texture, cmap_nonlin_texture: cmap_nonlin_texture,
108
+ };
109
+ }
110
+ /**
111
+ * &internal
112
+ * Render the filled contours
113
+ */
114
+ render(gl, matrix) {
115
+ if (this.gl_elems === null)
116
+ return;
117
+ const gl_elems = this.gl_elems;
118
+ gl_elems.program.use({ 'a_pos': gl_elems.vertices, 'a_tex_coord': gl_elems.texcoords }, { 'u_cmap_min': this.cmap.levels[0], 'u_cmap_max': this.cmap.levels[this.cmap.levels.length - 1], 'u_matrix': matrix, 'u_opacity': this.opacity,
119
+ 'u_n_index': this.index_map.length }, { 'u_fill_sampler': gl_elems.fill_texture, 'u_cmap_sampler': gl_elems.cmap_texture, 'u_cmap_nonlin_sampler': gl_elems.cmap_nonlin_texture });
120
+ gl.enable(gl.BLEND);
121
+ gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
122
+ gl_elems.program.draw();
123
+ }
124
+ }
125
+ export default ContourFill;
@@ -0,0 +1,51 @@
1
+ /// <reference types="mapbox-gl" />
2
+ import { PlotComponent } from "./PlotComponent";
3
+ import { PolylineCollection } from "./PolylineCollection";
4
+ import { BillboardCollection } from "./BillboardCollection";
5
+ import { MapType } from "./Map";
6
+ import { RawProfileField } from "./RawField";
7
+ import { WebGLAnyRenderingContext } from "./AutumnTypes";
8
+ interface HodographOptions {
9
+ /**
10
+ * The color of the hodograph plot background as a hex string
11
+ */
12
+ bgcolor?: string;
13
+ /**
14
+ * How much to thin the hodographs at zoom level 1 on the map. This effectively means to plot every `n`th hodograph in the i and j directions, where `n` =
15
+ * `thin_fac`. `thin_fac` should be a power of 2.
16
+ * @default 1
17
+ */
18
+ thin_fac?: number;
19
+ }
20
+ interface HodographGLElems {
21
+ map: MapType;
22
+ bg_billboard: BillboardCollection | null;
23
+ hodo_line: PolylineCollection | null;
24
+ sm_line: PolylineCollection | null;
25
+ }
26
+ /** A class representing a a field of hodograph plots */
27
+ declare class Hodographs extends PlotComponent {
28
+ readonly profile_field: RawProfileField;
29
+ readonly bgcolor: [number, number, number];
30
+ readonly thin_fac: number;
31
+ /** @private */
32
+ gl_elems: HodographGLElems;
33
+ /**
34
+ * Create a field of hodographs
35
+ * @param profile_field - The grid of profiles to plot
36
+ * @param opts - Various options to use when creating the hodographs
37
+ */
38
+ constructor(profile_field: RawProfileField, opts?: HodographOptions);
39
+ /**
40
+ * @internal
41
+ * Add the hodographs to a map
42
+ */
43
+ onAdd(map: mapboxgl.Map, gl: WebGLAnyRenderingContext): Promise<void>;
44
+ /**
45
+ * @internal
46
+ * Render the hodographs
47
+ */
48
+ render(gl: WebGLAnyRenderingContext, matrix: number[]): void;
49
+ }
50
+ export default Hodographs;
51
+ export type { HodographOptions };
@@ -0,0 +1,152 @@
1
+ import { PlotComponent, layer_worker } from "./PlotComponent";
2
+ import { PolylineCollection } from "./PolylineCollection";
3
+ import { BillboardCollection } from "./BillboardCollection";
4
+ import { getMinZoom, hex2rgba } from './utils';
5
+ import { LngLat } from "./Map";
6
+ const LINE_WIDTH = 4;
7
+ const BG_MAX_RING_MAG = 40;
8
+ const HODO_BG_DIMS = {
9
+ BB_WIDTH: 256,
10
+ BB_HEIGHT: 256,
11
+ BB_TEX_WIDTH: 256,
12
+ BB_TEX_HEIGHT: 256,
13
+ BB_MAG_MAX: 1000,
14
+ BB_MAG_WRAP: 1000,
15
+ BB_MAG_BIN_SIZE: 1000,
16
+ };
17
+ function _createHodoBackgroundTexture() {
18
+ let canvas = document.createElement('canvas');
19
+ canvas.width = HODO_BG_DIMS.BB_TEX_WIDTH;
20
+ canvas.height = HODO_BG_DIMS.BB_TEX_HEIGHT;
21
+ let ctx = canvas.getContext('2d');
22
+ if (ctx === null) {
23
+ throw "Could not get rendering context for the hodograph background canvas";
24
+ }
25
+ ctx.lineWidth = LINE_WIDTH;
26
+ for (let irng = HODO_BG_DIMS.BB_TEX_WIDTH / 4; irng <= HODO_BG_DIMS.BB_TEX_WIDTH / 2; irng += HODO_BG_DIMS.BB_TEX_WIDTH / 4) {
27
+ ctx.beginPath();
28
+ ctx.arc(HODO_BG_DIMS.BB_TEX_WIDTH / 2, HODO_BG_DIMS.BB_TEX_WIDTH / 2, irng - LINE_WIDTH / 2, 0, 2 * Math.PI);
29
+ ctx.stroke();
30
+ }
31
+ const ctr_x = HODO_BG_DIMS.BB_TEX_WIDTH / 2, ctr_y = HODO_BG_DIMS.BB_TEX_WIDTH / 2;
32
+ const arrow_size = 20;
33
+ ctx.beginPath();
34
+ ctx.moveTo(ctr_x, ctr_y);
35
+ ctx.lineTo(ctr_x + arrow_size / 2, ctr_y + arrow_size);
36
+ ctx.lineTo(ctr_x - arrow_size / 2, ctr_y + arrow_size);
37
+ ctx.lineTo(ctr_x, ctr_y);
38
+ ctx.fill();
39
+ return canvas;
40
+ }
41
+ ;
42
+ let HODO_BG_TEXTURE = null;
43
+ const HODO_COLORS = [
44
+ { 'bounds': [0, 1], 'color': '#ffffcc' },
45
+ { 'bounds': [1, 3], 'color': '#a1dab4' },
46
+ { 'bounds': [3, 6], 'color': '#41b6c4' },
47
+ { 'bounds': [6, 9], 'color': '#225ea8' }
48
+ ];
49
+ function _createHodoHeightTexture() {
50
+ let canvas = document.createElement('canvas');
51
+ canvas.width = Math.max(...HODO_COLORS.map(s => Math.max(...s['bounds'])));
52
+ canvas.height = 1;
53
+ let ctx = canvas.getContext('2d');
54
+ HODO_COLORS.forEach(stop => {
55
+ if (ctx === null) {
56
+ throw "Could not get rendering context for the hodograph height texture canvas";
57
+ }
58
+ const [clb, cub] = stop['bounds'];
59
+ ctx.fillStyle = stop['color'];
60
+ ctx.fillRect(clb, 0, cub - clb, 1);
61
+ });
62
+ return canvas;
63
+ }
64
+ let HODO_HEIGHT_TEXTURE = null;
65
+ /** A class representing a a field of hodograph plots */
66
+ class Hodographs extends PlotComponent {
67
+ /**
68
+ * Create a field of hodographs
69
+ * @param profile_field - The grid of profiles to plot
70
+ * @param opts - Various options to use when creating the hodographs
71
+ */
72
+ constructor(profile_field, opts) {
73
+ super();
74
+ opts = opts || {};
75
+ this.profile_field = profile_field;
76
+ const color = hex2rgba(opts.bgcolor || '#000000');
77
+ this.bgcolor = [color[0], color[1], color[2]];
78
+ this.thin_fac = opts.thin_fac || 1;
79
+ this.gl_elems = null;
80
+ }
81
+ /**
82
+ * @internal
83
+ * Add the hodographs to a map
84
+ */
85
+ async onAdd(map, gl) {
86
+ const hodo_scale = (HODO_BG_DIMS.BB_TEX_WIDTH - LINE_WIDTH / 2) / (HODO_BG_DIMS.BB_TEX_WIDTH * BG_MAX_RING_MAG);
87
+ const bg_size = 140;
88
+ if (HODO_BG_TEXTURE === null) {
89
+ HODO_BG_TEXTURE = _createHodoBackgroundTexture();
90
+ }
91
+ const bg_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'image': HODO_BG_TEXTURE, 'mag_filter': gl.NEAREST };
92
+ const max_zoom = map.getMaxZoom();
93
+ const bg_billboard = new BillboardCollection(gl, this.profile_field.getStormMotionGrid(), this.thin_fac, max_zoom, bg_image, HODO_BG_DIMS, this.bgcolor, bg_size * 0.004);
94
+ const hodo_polyline = await layer_worker.makePolyLines(this.profile_field.profiles.map(prof => {
95
+ const pt_ll = new LngLat(prof['lon'], prof['lat']).toMercatorCoord();
96
+ const zoom = getMinZoom(prof['jlat'], prof['ilon'], this.thin_fac);
97
+ const max_tex_z = Math.max(...HODO_COLORS.map(s => Math.max(...s['bounds'])));
98
+ return {
99
+ 'verts': [...prof['u']].map((u, ipt) => [u - prof['smu'], prof['v'][ipt] - prof['smv']]),
100
+ 'origin': [pt_ll.x, pt_ll.y],
101
+ 'zoom': zoom,
102
+ 'texcoords': [...prof['z']].map(z => [z / max_tex_z, 0.5])
103
+ };
104
+ }));
105
+ if (HODO_HEIGHT_TEXTURE === null) {
106
+ HODO_HEIGHT_TEXTURE = _createHodoHeightTexture();
107
+ }
108
+ const height_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'image': HODO_HEIGHT_TEXTURE, 'mag_filter': gl.NEAREST };
109
+ const hodo_line = new PolylineCollection(gl, hodo_polyline, height_image, 1.5, hodo_scale * bg_size);
110
+ const sm_polyline = await layer_worker.makePolyLines(this.profile_field.profiles.map(prof => {
111
+ const pt_ll = new LngLat(prof['lon'], prof['lat']).toMercatorCoord();
112
+ let ret = {};
113
+ const zoom = getMinZoom(prof['jlat'], prof['ilon'], this.thin_fac);
114
+ const sm_mag = Math.hypot(prof['smu'], prof['smv']);
115
+ const sm_ang = Math.PI / 2 - Math.atan2(-prof['smv'], -prof['smu']);
116
+ const buffer = 2;
117
+ return {
118
+ 'verts': [[buffer * Math.sin(sm_ang), buffer * Math.cos(sm_ang)],
119
+ [sm_mag * Math.sin(sm_ang), sm_mag * Math.cos(sm_ang)]],
120
+ 'origin': [pt_ll.x, pt_ll.y],
121
+ 'zoom': zoom,
122
+ 'texcoords': [[0.5, 0.5], [0.5, 0.5]]
123
+ };
124
+ }));
125
+ let byte_color = this.bgcolor.map(c => c * 255);
126
+ byte_color.push(255);
127
+ const sm_image = { 'format': gl.RGBA, 'type': gl.UNSIGNED_BYTE, 'width': 1, 'height': 1, 'image': new Uint8Array(byte_color),
128
+ 'mag_filter': gl.NEAREST };
129
+ const sm_line = new PolylineCollection(gl, sm_polyline, sm_image, 1, hodo_scale * bg_size);
130
+ this.gl_elems = {
131
+ map: map, bg_billboard: bg_billboard, hodo_line: hodo_line, sm_line: sm_line
132
+ };
133
+ }
134
+ /**
135
+ * @internal
136
+ * Render the hodographs
137
+ */
138
+ render(gl, matrix) {
139
+ if (this.gl_elems === null)
140
+ return;
141
+ const gl_elems = this.gl_elems;
142
+ const zoom = gl_elems.map.getZoom();
143
+ const map_width = gl_elems.map.getCanvas().width;
144
+ const map_height = gl_elems.map.getCanvas().height;
145
+ const bearing = gl_elems.map.getBearing();
146
+ const pitch = gl_elems.map.getPitch();
147
+ gl_elems.hodo_line.render(gl, matrix, [map_width, map_height], zoom, bearing, pitch);
148
+ gl_elems.sm_line.render(gl, matrix, [map_width, map_height], zoom, bearing, bearing);
149
+ gl_elems.bg_billboard.render(gl, matrix, [map_width, map_height], zoom, bearing, pitch);
150
+ }
151
+ }
152
+ export default Hodographs;
package/lib/Map.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import mapboxgl from 'mapbox-gl';
2
+ type MapType = mapboxgl.Map | maplibregl.Map;
3
+ interface LambertConformalConicParameters {
4
+ lon_0: number;
5
+ lat_0: number;
6
+ lat_std: [number, number] | number;
7
+ }
8
+ declare function lambertConformalConic(params: LambertConformalConicParameters): (a: number, b: number, opts?: {
9
+ inverse: boolean;
10
+ }) => [number, number];
11
+ /**
12
+ * A `LngLat` object represents a given longitude and latitude coordinate, measured in degrees.
13
+ * These coordinates are based on the [WGS84 (EPSG:4326) standard](https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84).
14
+ *
15
+ * MapLibre GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match the
16
+ * [GeoJSON specification](https://tools.ietf.org/html/rfc7946).
17
+ *
18
+ * @param {number} lng Longitude, measured in degrees.
19
+ * @param {number} lat Latitude, measured in degrees.
20
+ * @example
21
+ * var ll = new LngLat(-123.9749, 40.7736);
22
+ * ll.lng; // = -123.9749
23
+ */
24
+ declare class LngLat {
25
+ lng: number;
26
+ lat: number;
27
+ constructor(lng: number, lat: number);
28
+ toMercatorCoord(): {
29
+ x: number;
30
+ y: number;
31
+ };
32
+ }
33
+ export { LngLat, lambertConformalConic };
34
+ export type { MapType };
package/lib/Map.js ADDED
@@ -0,0 +1,120 @@
1
+ function lambertConformalConic(params) {
2
+ // Formulas from https://pubs.usgs.gov/pp/1395/report.pdf
3
+ const compute_t = (lat) => {
4
+ const sin_lat = Math.sin(lat);
5
+ return Math.tan(Math.PI / 4 - lat / 2) * Math.pow((1 + eccen * sin_lat) / (1 - eccen * sin_lat), eccen / 2);
6
+ };
7
+ const compute_m = (lat) => {
8
+ const sin_lat = Math.sin(lat);
9
+ return Math.cos(lat) / Math.sqrt(1 - eccen * eccen * sin_lat * sin_lat);
10
+ };
11
+ // WGS 84 spheroid
12
+ const semimajor = 6378137.0;
13
+ const semiminor = 6356752.314245;
14
+ const eccen = Math.sqrt(1 - (semiminor * semiminor) / (semimajor * semimajor));
15
+ const radians = Math.PI / 180;
16
+ let { lon_0, lat_0, lat_std } = params;
17
+ lat_std = Array.isArray(lat_std) && lat_std[0] == lat_std[1] ? lat_std[0] : lat_std;
18
+ lon_0 *= radians;
19
+ lat_0 *= radians;
20
+ let F, n;
21
+ const t_0 = compute_t(lat_0);
22
+ if (Array.isArray(lat_std)) {
23
+ let [lat_1, lat_2] = lat_std;
24
+ lat_1 *= radians;
25
+ lat_2 *= radians;
26
+ const t_1 = compute_t(lat_1);
27
+ const t_2 = compute_t(lat_2);
28
+ const m_1 = compute_m(lat_1);
29
+ const m_2 = compute_m(lat_2);
30
+ n = Math.log(m_1 / m_2) / Math.log(t_1 / t_2);
31
+ F = m_1 / (n * Math.pow(t_1, n));
32
+ }
33
+ else {
34
+ let lat_1 = lat_std;
35
+ lat_1 *= radians;
36
+ const t_1 = compute_t(lat_1);
37
+ const m_1 = compute_m(lat_1);
38
+ n = Math.sin(lat_1);
39
+ F = m_1 / (n * Math.pow(t_1, n));
40
+ }
41
+ const rho_0 = semimajor * F * Math.pow(t_0, n);
42
+ const compute_lcc = (lon, lat) => {
43
+ lon *= radians;
44
+ lat *= radians;
45
+ const t = compute_t(lat);
46
+ const rho = semimajor * F * Math.pow(t, n);
47
+ const theta = n * (lon - lon_0);
48
+ const x = rho * Math.sin(theta);
49
+ const y = rho_0 - rho * Math.cos(theta);
50
+ return [x, y];
51
+ };
52
+ const eccen2 = eccen * eccen;
53
+ const eccen4 = eccen2 * eccen2;
54
+ const eccen6 = eccen4 * eccen2;
55
+ const eccen8 = eccen6 * eccen2;
56
+ /*
57
+ const A = eccen2 / 2 + 5 * eccen4 / 24 + eccen6 / 12 + 13 * eccen8 / 360;
58
+ const B = 7 * eccen4 / 48 + 29 * eccen6 / 240 + 811 * eccen8 / 11520;
59
+ const C = 7 * eccen6 / 120 + 81 * eccen8 / 1120;
60
+ const D = 4279 * eccen8 / 161280;
61
+ const Ap = A - C;
62
+ const Bp = 2 * B - 4 * D;
63
+ const Cp = 4 * C;
64
+ const Dp = 8 * D;
65
+ */
66
+ const Ap = eccen2 / 2 + 5 * eccen4 / 24 + 3 * eccen6 / 120 - 73 * eccen8 / 2016;
67
+ const Bp = 7 * eccen4 / 24 + 29 * eccen6 / 120 + 233 * eccen8 / 6720;
68
+ const Cp = 7 * eccen6 / 30 + 81 * eccen8 / 280;
69
+ const Dp = 4729 * eccen8 / 20160;
70
+ const compute_lcc_inverse = (x, y) => {
71
+ const theta = Math.atan2(x, rho_0 - y); // These arguments are backwards from what I'd expect ...
72
+ const lon = theta / n + lon_0;
73
+ const rho = Math.hypot(x, rho_0 - y) * Math.sign(n);
74
+ const t = Math.pow(rho / (semimajor * F), 1 / n);
75
+ const chi = Math.PI / 2 - 2 * Math.atan(t);
76
+ const sin_2chi = Math.sin(2 * chi);
77
+ const cos_2chi = Math.cos(2 * chi);
78
+ const lat = chi + sin_2chi * (Ap + cos_2chi * (Bp + cos_2chi * (Cp + Dp * cos_2chi)));
79
+ return [lon / radians, lat / radians];
80
+ };
81
+ return (a, b, opts) => {
82
+ opts = opts === undefined ? { inverse: false } : opts;
83
+ return opts.inverse ? compute_lcc_inverse(a, b) : compute_lcc(a, b);
84
+ };
85
+ }
86
+ function mercatorXfromLng(lng) {
87
+ return (180 + lng) / 360;
88
+ }
89
+ function mercatorYfromLat(lat) {
90
+ return (180 - (180 / Math.PI * Math.log(Math.tan(Math.PI / 4 + lat * Math.PI / 360)))) / 360;
91
+ }
92
+ /**
93
+ * A `LngLat` object represents a given longitude and latitude coordinate, measured in degrees.
94
+ * These coordinates are based on the [WGS84 (EPSG:4326) standard](https://en.wikipedia.org/wiki/World_Geodetic_System#WGS84).
95
+ *
96
+ * MapLibre GL uses longitude, latitude coordinate order (as opposed to latitude, longitude) to match the
97
+ * [GeoJSON specification](https://tools.ietf.org/html/rfc7946).
98
+ *
99
+ * @param {number} lng Longitude, measured in degrees.
100
+ * @param {number} lat Latitude, measured in degrees.
101
+ * @example
102
+ * var ll = new LngLat(-123.9749, 40.7736);
103
+ * ll.lng; // = -123.9749
104
+ */
105
+ class LngLat {
106
+ constructor(lng, lat) {
107
+ if (isNaN(lng) || isNaN(lat)) {
108
+ throw new Error(`Invalid LngLat object: (${lng}, ${lat})`);
109
+ }
110
+ this.lng = +lng;
111
+ this.lat = +lat;
112
+ if (this.lat > 90 || this.lat < -90) {
113
+ throw new Error('Invalid LngLat latitude value: must be between -90 and 90');
114
+ }
115
+ }
116
+ toMercatorCoord() {
117
+ return { x: mercatorXfromLng(this.lng), y: mercatorYfromLat(this.lat) };
118
+ }
119
+ }
120
+ export { LngLat, lambertConformalConic };
@@ -0,0 +1,56 @@
1
+ import { WebGLAnyRenderingContext } from "./AutumnTypes";
2
+ import { MapType } from "./Map";
3
+ import { PlotComponent } from "./PlotComponent";
4
+ import { RawScalarField } from "./RawField";
5
+ import { WGLBuffer, WGLProgram, WGLTexture } from "autumn-wgl";
6
+ interface PaintballOptions {
7
+ /**
8
+ * The list of colors (as hex strings) to use for each member in the paintball plot. The first color corresponds to member 1, the second to member 2, etc.
9
+ */
10
+ colors?: string[];
11
+ /**
12
+ * The opacity of the paintball plot
13
+ * @default 1
14
+ */
15
+ opacity?: number;
16
+ }
17
+ interface PaintballGLElems {
18
+ program: WGLProgram;
19
+ vertices: WGLBuffer;
20
+ fill_texture: WGLTexture;
21
+ texcoords: WGLBuffer;
22
+ }
23
+ /**
24
+ * A class representing a paintball plot, which is a plot of objects in every member of an ensemble. Objects are usually defined by a single threshold on
25
+ * a field (such as simulated reflectivity greater than 40 dBZ), but could in theory be defined by any arbitrarily complicated method. In autumnplot-gl,
26
+ * the data for the paintball plot is given as a single field with the objects from each member encoded as "bits" in the field. Because the field is made up
27
+ * of single-precision floats, this works for up to 24 members. (Technically speaking, I don't need the quotes around "bits", as they're bits of the
28
+ * significand of an IEEE 754 float.)
29
+ */
30
+ declare class Paintball extends PlotComponent {
31
+ readonly field: RawScalarField;
32
+ readonly colors: number[];
33
+ readonly opacity: number;
34
+ /** @private */
35
+ gl_elems: PaintballGLElems | null;
36
+ /**
37
+ * Create a paintball plot
38
+ * @param field - A scalar field containing the member objects encoded as "bits." The numerical value of each grid point can be constructed like
39
+ * `1.0 * M1 + 2.0 * M2 + 4.0 * M3 + 8.0 * M4 ...`, where `M1` is 1 if that grid point is in an object in member 1 and 0 otherwise,
40
+ * `M2` is the same thing for member 2, and `M3` and `M4` and up to `Mn` are the same thing for the rest of the members.
41
+ * @param opts - Options for creating the paintball plot
42
+ */
43
+ constructor(field: RawScalarField, opts?: PaintballOptions);
44
+ /**
45
+ * @internal
46
+ * Add the paintball plot to a map.
47
+ */
48
+ onAdd(map: MapType, gl: WebGLAnyRenderingContext): Promise<void>;
49
+ /**
50
+ * @internal
51
+ * Render the paintball plot
52
+ */
53
+ render(gl: WebGLAnyRenderingContext, matrix: number[]): void;
54
+ }
55
+ export default Paintball;
56
+ export type { PaintballOptions };