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/LICENSE +674 -0
- package/README.md +156 -0
- package/lib/AutumnTypes.d.ts +37 -0
- package/lib/AutumnTypes.js +4 -0
- package/lib/Barbs.d.ts +56 -0
- package/lib/Barbs.js +155 -0
- package/lib/BillboardCollection.d.ts +17 -0
- package/lib/BillboardCollection.js +149 -0
- package/lib/ColorBar.d.ts +70 -0
- package/lib/ColorBar.js +183 -0
- package/lib/Colormap.d.ts +70 -0
- package/lib/Colormap.js +137 -0
- package/lib/Contour.d.ts +71 -0
- package/lib/Contour.js +172 -0
- package/lib/ContourFill.d.ts +58 -0
- package/lib/ContourFill.js +125 -0
- package/lib/Hodographs.d.ts +51 -0
- package/lib/Hodographs.js +152 -0
- package/lib/Map.d.ts +34 -0
- package/lib/Map.js +120 -0
- package/lib/Paintball.d.ts +56 -0
- package/lib/Paintball.js +104 -0
- package/lib/PlotComponent.d.ts +20 -0
- package/lib/PlotComponent.js +6 -0
- package/lib/PlotLayer.d.ts +96 -0
- package/lib/PlotLayer.js +131 -0
- package/lib/PlotLayer.worker.d.ts +18 -0
- package/lib/PlotLayer.worker.js +343 -0
- package/lib/PolylineCollection.d.ts +17 -0
- package/lib/PolylineCollection.js +92 -0
- package/lib/RawField.d.ts +194 -0
- package/lib/RawField.js +340 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +20 -0
- package/lib/utils.d.ts +9 -0
- package/lib/utils.js +103 -0
- package/package.json +37 -0
|
@@ -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
|
+
* ¶m field - The field to create filled contours from
|
|
51
|
+
* ¶m 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 };
|