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
@@ -1,73 +1,222 @@
1
- import { kdTree } from "kd-tree-javascript";
2
- import { LngLat } from "./Map";
3
1
  import { PlotComponent } from "./PlotComponent";
4
- import { getMinZoom, normalizeOptions } from "./utils";
5
- class UnstructuredGrid {
6
- constructor(coords) {
7
- this.coords = coords;
2
+ import { normalizeOptions } from "./utils";
3
+ import { TextCollection } from "./TextCollection";
4
+ import { Color } from "./Color";
5
+ import Barbs from "./Barbs";
6
+ const SYMBOLS = {
7
+ // Sky cover symbols
8
+ '0/8': 59658, '1/8': 59659, '2/8': 59660, '3/8': 59661, '4/8': 59662, '5/8': 59663, '6/8': 59664, '7/8': 59665, '8/8': 59666,
9
+ 'clr': 59658, 'few': 59660, 'sct': 59662, 'bkn': 59664, 'ovc': 59666, 'obsc': 59667,
10
+ // Present weather symbols
11
+ 'va': 59810, 'fu': 59810, 'hz': 59811, 'du': 59812, 'bldu': 59814, 'sa': 59814, 'blsa': 59814, 'vcblsa': 59814, 'vcbldu': 59814, 'blpy': 59814,
12
+ 'po': 59816, 'vcpo': 59816, 'vcds': 59817, 'vcss': 59817,
13
+ 'br': 59818, 'bcbr': 59818, 'bc': 59819, 'mifg': 59820,
14
+ 'vcts': 59821, 'virga': 59822, 'vcsh': 59824, 'ts': 59825, 'thdr': 59825, 'vctshz': 59825,
15
+ 'tsfzfg': 59825, 'tsbr': 59825, 'tsdz': 59825, 'vctsup': 59825,
16
+ '-tsup': 59825, 'tsup': 59825, '+tsup': 59825,
17
+ 'sq': 59826, 'fc': 59827, '+fc': 59827,
18
+ 'ds': 59839, 'ss': 59839, 'drsa': 59839, 'drdu': 59839, '+ds': 59842, '+ss': 59842,
19
+ 'drsn': 59844, '+drsn': 59845, '-blsn': 59846, 'blsn': 59846, '+blsn': 59847, 'vcblsn': 59846,
20
+ 'vcfg': 59848, 'bcfg': 59849, 'prfg': 59852, 'fg': 59853, 'fzfg': 59857,
21
+ '-vctsdz': 59859, '-dz': 59859, '-dzbr': 59859, 'vctsdz': 59861, 'dz': 59861, '+vctsdz': 59863, '+dz': 59863,
22
+ '-fzdz': 59864, '-fzdzsn': 59864, 'fzdz': 59865, '+fzdz': 59865, 'fzdzsn': 59865,
23
+ '-dzra': 59866, 'dzra': 59867, '+dzra': 59867, '-ra': 59869, '-rabr': 59869, 'ra': 59871, 'rabr': 59871, 'rafg': 59871, 'vcra': 59871, '+ra': 59873,
24
+ '-fzra': 59874, '-fzrasn': 59874, '-fzrabr': 59874, '-fzrapl': 59874, '-fzrasnpl': 59874, 'tsfzrapl': 59875, '-tsfzra': 59875,
25
+ 'fzra': 59875, '+fzra': 59875, 'fzrasn': 59875, 'tsfzra': 59875,
26
+ '-dzsn': 59876, '-rasn': 59876, '-snra': 59876, '-sndz': 59876, 'rasn': 59877, '+rasn': 59877, 'snra': 59877, 'dzsn': 59877, 'sndz': 59877, '+dzsn': 59877, '+sndz': 59877,
27
+ '-sn': 59879, '-snbr': 59879, 'sn': 59881, '+sn': 59883, '-snsg': 59885, 'sg': 59885, '-sg': 59885, 'ic': 59886,
28
+ '-fzdzpl': 59887, '-fzdzplsn': 59887, 'fzdzpl': 59887, '-fzraplsn': 59887, 'fzrapl': 59887, '+fzrapl': 59887,
29
+ '-rapl': 59887, '-rasnpl': 59887, '-raplsn': 59887, '+rapl': 59887, 'rapl': 59887, '-snpl': 59887, 'snpl': 59887,
30
+ '-pl': 59887, 'pl': 59887, '-plsn': 59887, '-plra': 59887, 'plra': 59887, '-pldz': 59887, '+pl': 59887, 'plsn': 59887, 'plup': 59887, '+plsn': 59887,
31
+ '-sh': 59888, '-shra': 59888, 'sh': 59889, 'shra': 59889, '+sh': 59889, '+shra': 59889, '-shrasn': 59891, '-shsnra': 59891, '+shrabr': 59892,
32
+ 'shrasn': 59892, '+shrasn': 59892, 'shsnra': 59892, '+shsnra': 59892, '-shsn': 59893, 'shsn': 59894, '+shsn': 59894,
33
+ '-gs': 59895, '-shgs': 59895, 'fzraplgs': 59896, '-sngs': 59896, 'gsplsn': 59896, 'gspl': 59896, 'plgssn': 59896, 'gs': 59896, 'shgs': 59896, '+gs': 59896, '+shgs': 59896,
34
+ '-gr': 59897, '-shgr': 59897, '-sngr': 59898, 'gr': 59898, 'shgr': 59898, '+gr': 59898, '+shgr': 59898,
35
+ '-tsrasn': 59907, 'tsrasn': 59907, '-tssnra': 59907, 'tssnra': 59907, '-vctsra': 59908, '-tsra': 59908, 'tsra': 59908, '-tsdz': 59908, 'vctsra': 59908,
36
+ 'tspl': 59909, '-tssn': 59909, '-tspl': 59909, 'tssn': 59909, '-vctssn': 59909, 'vctssn': 59909, 'tsplsn': 59909, 'tssnpl': 59909, '-tssnpl': 59909, '-tsragr': 59910,
37
+ 'tsrags': 59910, 'tsragr': 59910, 'tsgs': 59910, 'tsgr': 59910,
38
+ '+tsfzrapl': 59911, '+vctsra': 59912, '+tsra': 59912, '+tsfzra': 59912, '+tssn': 59913, '+tspl': 59913, '+tsplsn': 59913, '+vctssn': 59913,
39
+ 'tssa': 59914, 'tsds': 59914, 'tsdu': 59914, '+tsgs': 59915, '+tsgr': 59915, '+tsrags': 59915, '+tsragr': 59915, 'in': 59750,
40
+ '-up': 59750, 'up': 59750, '+up': 59751, '-fzup': 59756, 'fzup': 59756, '+fzup': 59757,
41
+ };
42
+ const station_plot_opts_defaults = {
43
+ config: {},
44
+ thin_fac: 1,
45
+ font_face: 'Trebuchet MS',
46
+ font_size: 12,
47
+ font_url_template: '',
48
+ };
49
+ function positionToAlignmentAndOffset(pos, off_size) {
50
+ off_size = off_size === undefined ? 10 : off_size;
51
+ let ha, va;
52
+ let xoff, yoff;
53
+ if (pos == 'll' || pos == 'cl' || pos == 'ul') {
54
+ ha = 'right';
55
+ xoff = -off_size;
8
56
  }
9
- getZoomThinning(thin_fac, map_max_zoom) {
10
- let min_label_lon = null, min_label_lat = null, max_label_lon = null, max_label_lat = null;
11
- this.coords.forEach(coord => {
12
- if (min_label_lon === null || coord.lon < min_label_lon)
13
- min_label_lon = coord.lon;
14
- if (max_label_lon === null || coord.lon > max_label_lon)
15
- max_label_lon = coord.lon;
16
- if (min_label_lat === null || coord.lat < min_label_lat)
17
- min_label_lat = coord.lat;
18
- if (max_label_lat === null || coord.lat > max_label_lat)
19
- max_label_lat = coord.lat;
20
- });
21
- if (min_label_lon === null || min_label_lat === null || max_label_lon === null || max_label_lat === null)
22
- return [];
23
- const kd_nodes = this.coords.map(c => ({ lon: c.lon, lat: c.lat, min_zoom: map_max_zoom }));
24
- const tree = new kdTree(kd_nodes, (a, b) => Math.hypot(a.lon - b.lon, a.lat - b.lat), ['lon', 'lat']);
25
- const { x: min_label_x, y: max_label_y } = new LngLat(min_label_lon, min_label_lat).toMercatorCoord();
26
- const { x: max_label_x, y: min_label_y } = new LngLat(max_label_lon, max_label_lat).toMercatorCoord();
27
- const thin_grid_width = max_label_x - min_label_x;
28
- const thin_grid_height = max_label_y - min_label_y;
29
- const ni_thin_grid = Math.round(thin_grid_width * thin_fac); // thin_fac was 4 / contour_label_spacing for the contour labels
30
- const nj_thin_grid = Math.round(thin_grid_height * thin_fac);
31
- const thin_grid_xs = [];
32
- const thin_grid_ys = [];
33
- for (let idx = 0; idx < ni_thin_grid; idx++) {
34
- thin_grid_xs.push(min_label_x + (idx / ni_thin_grid) * thin_grid_width);
35
- }
36
- for (let jdy = 0; jdy < nj_thin_grid; jdy++) {
37
- thin_grid_ys.push(min_label_y + (jdy / nj_thin_grid) * thin_grid_height);
38
- }
39
- for (let idx = 0; idx < ni_thin_grid; idx++) {
40
- for (let jdy = 0; jdy < nj_thin_grid; jdy++) {
41
- const zoom = getMinZoom(jdy, idx, Math.pow(2, map_max_zoom));
42
- const grid_x = thin_grid_xs[idx];
43
- const grid_y = thin_grid_ys[jdy];
44
- const ll = LngLat.fromMercatorCoord(grid_x, grid_y);
45
- const [label, dist] = tree.nearest({ lon: ll.lng, lat: ll.lat, min_zoom: 0 }, 1)[0];
46
- label.min_zoom = zoom;
47
- }
48
- }
49
- return kd_nodes.map(n => n.min_zoom);
57
+ else if (pos == 'lc' || pos == 'c' || pos == 'uc') {
58
+ ha = 'center';
59
+ xoff = 0;
50
60
  }
51
- }
52
- class ObsField {
53
- constructor(data) {
54
- this.grid = new UnstructuredGrid(data.map(d => d.coord));
55
- this.data = data;
61
+ else {
62
+ ha = 'left';
63
+ xoff = off_size;
64
+ }
65
+ if (pos == 'll' || pos == 'lc' || pos == 'lr') {
66
+ va = 'top';
67
+ yoff = -off_size;
68
+ }
69
+ else if (pos == 'cl' || pos == 'c' || pos == 'cr') {
70
+ va = 'middle';
71
+ yoff = 0;
56
72
  }
73
+ else {
74
+ va = 'baseline';
75
+ yoff = off_size;
76
+ }
77
+ return { horizontal_align: ha, vertical_align: va, offset_x: xoff, offset_y: yoff };
57
78
  }
58
- const station_plot_opts_defaults = {};
79
+ /**
80
+ * Station model plots for observed data
81
+ * @example
82
+ * // Specify how to set up the station plot
83
+ * const station_plot_locs = {
84
+ * tmpf: {type: 'number', pos: 'ul', color: '#cc0000', formatter: val => val === null ? '' : val.toFixed(0)},
85
+ * dwpf: {type: 'number', pos: 'll', color: '#00aa00', formatter: val => val === null ? '' : val.toFixed(0)},
86
+ * wind: {type: 'barb', pos: 'c'},
87
+ * preswx: {type: 'symbol', pos: 'cl', color: '#ff00ff'},
88
+ * skyc: {type: 'symbol', pos: 'c'},
89
+ * };
90
+ *
91
+ * // Create the station plot
92
+ * const station_plot = new StationPlot(obs_field, {config: station_plot_locs, thin_fac: 8, font_size: 14});
93
+ */
59
94
  class StationPlot extends PlotComponent {
60
- constructor(field, data_positions, opts) {
95
+ /**
96
+ * Create station plots
97
+ * @param field - A field containing the observed data
98
+ * @param opts - Various options for the station plots
99
+ */
100
+ constructor(field, opts) {
61
101
  super();
62
102
  this.field = field;
63
- this.data_positions = data_positions;
64
- this.opts = normalizeOptions(opts, station_plot_opts_defaults);
103
+ this.opts = normalizeOptions(opts, station_plot_opts_defaults); // Is there a way to do this without invoking `as`?
104
+ this.gl_elems = null;
105
+ this.text_components = null;
106
+ }
107
+ /**
108
+ * Update the data displayed as station plots
109
+ * @param field - The new field to display as station plots
110
+ */
111
+ async updateField(field) {
112
+ this.field = field;
113
+ if (this.gl_elems === null)
114
+ return;
115
+ const map = this.gl_elems.map;
116
+ const gl = this.gl_elems.gl;
117
+ const barb_components = this.gl_elems.barb_components;
118
+ const map_style = map.getStyle();
119
+ const font_url_template = this.opts.font_url_template == '' ? map_style.glyphs : this.opts.font_url_template;
120
+ if (font_url_template === undefined)
121
+ throw "The map style doesn't have any glyph information. Please pass the font_url_template option to StationPlot";
122
+ const font_url = font_url_template.replace('{fontstack}', this.opts.font_face);
123
+ let ibarb = 0;
124
+ const sub_component_promises = Object.entries(this.opts.config).map(async ([k_, config]) => {
125
+ const k = k_;
126
+ if (config.type == 'number' || config.type == 'string') {
127
+ const pos = config.pos;
128
+ const color = config.color === undefined ? Color.fromHex('#000000') : Color.normalizeColor(config.color);
129
+ const halo_color = config.halo_color === undefined ? Color.fromHex('#ffffff') : Color.normalizeColor(config.halo_color);
130
+ const halo = config.halo === undefined ? true : config.halo;
131
+ const coords = this.field.grid.getEarthCoords();
132
+ const zoom = this.field.grid.getMinVisibleZoom(this.opts.thin_fac);
133
+ let text_specs;
134
+ if (config.type == 'number') {
135
+ const comp = this.field.getScalar(k);
136
+ const formatter = config.formatter === undefined ? (val) => val === null ? 'null' : val.toString() : config.formatter;
137
+ text_specs = comp.map((v, i) => ({ text: formatter(v), lat: coords.lats[i], lon: coords.lons[i], min_zoom: zoom[i] }));
138
+ }
139
+ else {
140
+ const comp = this.field.getStrings(k);
141
+ text_specs = comp.map((v, i) => ({ text: v === null ? '' : v, lat: coords.lats[i], lon: coords.lons[i], min_zoom: zoom[i] }));
142
+ }
143
+ const tc_opts = {
144
+ ...positionToAlignmentAndOffset(pos),
145
+ font_size: this.opts.font_size, halo: halo,
146
+ text_color: color, halo_color: halo_color,
147
+ };
148
+ return await TextCollection.make(gl, text_specs, font_url, tc_opts);
149
+ }
150
+ else if (config.type == 'barb') {
151
+ const comp = this.field.getVector(k);
152
+ const barb_comp = barb_components[ibarb++];
153
+ barb_comp.updateField(comp);
154
+ }
155
+ else if (config.type == 'symbol') {
156
+ const pos = config.pos;
157
+ const color = config.color === undefined ? Color.fromHex('#000000') : Color.normalizeColor(config.color);
158
+ const halo_color = config.halo_color === undefined ? Color.fromHex('#ffffff') : Color.normalizeColor(config.halo_color);
159
+ const halo = config.halo === undefined ? true : config.halo;
160
+ const comp = this.field.getStrings(k);
161
+ const coords = this.field.grid.getEarthCoords();
162
+ const zoom = this.field.grid.getMinVisibleZoom(this.opts.thin_fac);
163
+ const wxsym_font_url = font_url_template.replace('{fontstack}', 'wx_symbols');
164
+ const text_specs = comp.map((v, i) => ({ text: v === null ? '' : String.fromCharCode(SYMBOLS[v]),
165
+ lat: coords.lats[i], lon: coords.lons[i], min_zoom: zoom[i] }));
166
+ const tc_opts = {
167
+ ...positionToAlignmentAndOffset(pos),
168
+ font_size: this.opts.font_size, halo: halo,
169
+ text_color: color, halo_color: halo_color,
170
+ };
171
+ if (tc_opts.offset_x !== undefined)
172
+ tc_opts.offset_x -= 3;
173
+ return await TextCollection.make(gl, text_specs, wxsym_font_url, tc_opts);
174
+ }
175
+ else {
176
+ throw `Unknown station plot configuration type ${config.type}`;
177
+ }
178
+ });
179
+ this.text_components = (await Promise.all(sub_component_promises)).filter((c) => c !== undefined);
180
+ map.triggerRepaint();
65
181
  }
182
+ /** @internal */
66
183
  async onAdd(map, gl) {
184
+ const barb_promises = Object.entries(this.opts.config).map(async ([k_, config]) => {
185
+ const k = k_;
186
+ if (config.type == 'barb') {
187
+ const comp = this.field.getVector(k);
188
+ const color = config.color === undefined ? '#000000' : config.color;
189
+ const barb_size = config.barb_size_multipler === undefined ? 1 : config.barb_size_multipler;
190
+ const barb_comp = new Barbs(comp, { thin_fac: this.opts.thin_fac, color: color, barb_size_multiplier: barb_size });
191
+ await barb_comp.onAdd(map, gl);
192
+ return barb_comp;
193
+ }
194
+ });
195
+ const barb_components = (await Promise.all(barb_promises)).filter((c) => c !== undefined);
196
+ this.gl_elems = {
197
+ map: map, gl: gl, barb_components: barb_components
198
+ };
199
+ this.updateField(this.field);
67
200
  }
68
- render(gl, matrix) {
69
- if (matrix instanceof Float32Array)
70
- matrix = [...matrix];
201
+ /** @internal */
202
+ render(gl, arg) {
203
+ if (this.gl_elems === null || this.text_components === null)
204
+ return;
205
+ const gl_elems = this.gl_elems;
206
+ const text_components = this.text_components;
207
+ const barb_components = this.gl_elems.barb_components;
208
+ const map_width = gl_elems.map.getCanvas().width;
209
+ const map_height = gl_elems.map.getCanvas().height;
210
+ const map_zoom = gl_elems.map.getZoom();
211
+ let itext = 0, ibarb = 0;
212
+ Object.values(this.opts.config).forEach(comp => {
213
+ if (comp.type == 'barb') {
214
+ barb_components[ibarb++].render(gl, arg);
215
+ }
216
+ else {
217
+ text_components[itext++].render(gl, arg, [map_width, map_height], map_zoom);
218
+ }
219
+ });
71
220
  }
72
221
  }
73
- export { StationPlot };
222
+ export default StationPlot;
@@ -1,6 +1,7 @@
1
- import { WebGLAnyRenderingContext } from "./AutumnTypes";
1
+ import { RenderMethodArg, WebGLAnyRenderingContext } from "./AutumnTypes";
2
2
  import { Color } from "./Color";
3
- import { WGLBuffer, WGLProgram, WGLTexture } from "autumn-wgl";
3
+ import { ShaderProgramManager } from "./ShaderManager";
4
+ import { WGLBuffer, WGLTexture } from "autumn-wgl";
4
5
  interface TextSpec {
5
6
  lat: number;
6
7
  lon: number;
@@ -16,17 +17,19 @@ interface TextCollectionOptions {
16
17
  text_color?: Color;
17
18
  halo_color?: Color;
18
19
  halo?: boolean;
20
+ offset_x?: number;
21
+ offset_y?: number;
19
22
  }
20
23
  declare class TextCollection {
21
- readonly program: WGLProgram;
24
+ readonly shader_manager: ShaderProgramManager;
22
25
  readonly anchors: WGLBuffer;
23
26
  readonly offsets: WGLBuffer;
24
27
  readonly texcoords: WGLBuffer;
25
28
  readonly texture: WGLTexture;
26
29
  readonly opts: Required<TextCollectionOptions>;
27
30
  private constructor();
28
- static make(gl: WebGLAnyRenderingContext, text_locs: TextSpec[], fontstack_url: string, opts?: TextCollectionOptions): Promise<TextCollection>;
29
- render(gl: WebGLAnyRenderingContext, matrix: number[], [map_width, map_height]: [number, number], map_zoom: number): void;
31
+ static make(gl: WebGLAnyRenderingContext, text_locs: TextSpec[], fontstack_url_template: string, opts?: TextCollectionOptions): Promise<TextCollection>;
32
+ render(gl: WebGLAnyRenderingContext, arg: RenderMethodArg, [map_width, map_height]: [number, number], map_zoom: number): void;
30
33
  }
31
34
  export { TextCollection };
32
- export type { TextSpec, TextCollectionOptions };
35
+ export type { TextSpec, TextCollectionOptions, HorizontalAlign, VerticalAlign };
@@ -1,23 +1,24 @@
1
- import { isWebGL2Ctx } from "./AutumnTypes";
1
+ import { getRendererData, isWebGL2Ctx } from "./AutumnTypes";
2
2
  import { Color } from "./Color";
3
3
  import { LngLat } from "./Map";
4
+ import { ShaderProgramManager } from "./ShaderManager";
4
5
  import { Cache, normalizeOptions } from "./utils";
5
- import { WGLBuffer, WGLProgram, WGLTexture } from "autumn-wgl";
6
+ import { WGLBuffer, WGLTexture } from "autumn-wgl";
6
7
  import Protobuf from 'pbf';
7
8
  import potpack from "potpack";
8
- const text_vertex_shader_src = `
9
- uniform mat4 u_matrix;
9
+ const text_vertex_shader_src = `#version 300 es
10
+
10
11
  uniform int u_offset;
11
12
  uniform highp float u_map_width;
12
13
  uniform highp float u_map_height;
13
14
  uniform highp float u_map_zoom;
14
15
  uniform highp float u_font_size;
15
16
 
16
- attribute vec3 a_pos;
17
- attribute vec2 a_offset;
18
- attribute vec2 a_tex_coord;
17
+ in vec3 a_pos;
18
+ in vec2 a_offset;
19
+ in vec2 a_tex_coord;
19
20
 
20
- varying highp vec2 v_tex_coord;
21
+ out highp vec2 v_tex_coord;
21
22
 
22
23
  mat4 scalingMatrix(float x_scale, float y_scale, float z_scale) {
23
24
  return mat4(x_scale, 0.0, 0.0, 0.0,
@@ -38,11 +39,12 @@ void main() {
38
39
 
39
40
  mat4 map_stretch_matrix = scalingMatrix(u_map_height / u_map_width, 1., 1.);
40
41
 
41
- gl_Position = u_matrix * vec4(a_pos.xy + globe_offset, 0.0, 1.0) + u_font_size / 12. * 1.5 * map_stretch_matrix * vec4(offset, 0., 0.);
42
+ gl_Position = projectTile(a_pos.xy + globe_offset) + u_font_size / 12. * 1.5 * map_stretch_matrix * vec4(offset, 0., 0.);
42
43
  v_tex_coord = a_tex_coord;
43
44
  }`
44
- const text_fragment_shader_src = `
45
- varying highp vec2 v_tex_coord;
45
+ const text_fragment_shader_src = `#version 300 es
46
+
47
+ in highp vec2 v_tex_coord;
46
48
  uniform sampler2D u_sdf_sampler;
47
49
  uniform int u_is_halo;
48
50
 
@@ -52,8 +54,10 @@ uniform lowp vec4 u_halo_color;
52
54
  #define SDF_FILL 0.75
53
55
  #define SDF_HALO 0.45
54
56
 
57
+ out highp vec4 fragColor;
58
+
55
59
  void main() {
56
- highp float sdf_val = texture2D(u_sdf_sampler, v_tex_coord).r;
60
+ highp float sdf_val = texture(u_sdf_sampler, v_tex_coord).r;
57
61
 
58
62
  lowp float step_width = 0.08;
59
63
  lowp float alpha = smoothstep(SDF_FILL - step_width, SDF_FILL + step_width, sdf_val);
@@ -65,9 +69,8 @@ void main() {
65
69
  }
66
70
 
67
71
  color.a *= alpha;
68
- gl_FragColor = color;
72
+ fragColor = color;
69
73
  }`
70
- const program_cache = new Cache((gl) => new WGLProgram(gl, text_vertex_shader_src, text_fragment_shader_src));
71
74
  const PADDING = 3;
72
75
  function parseFontPBF(data) {
73
76
  const readGlyph = (tag, glyph, pbf) => {
@@ -125,6 +128,7 @@ function createAtlas(pbf_glyphs) {
125
128
  const { w: img_width, h: img_height } = potpack(bins);
126
129
  const atlas_data = new Uint8Array(img_width * img_height);
127
130
  const glyphs = {};
131
+ let max_glyph_height = 0;
128
132
  glyph_bins.forEach(glyph_bin => {
129
133
  const { bin, glyph } = glyph_bin;
130
134
  if (bin.x === undefined || bin.y === undefined)
@@ -133,6 +137,7 @@ function createAtlas(pbf_glyphs) {
133
137
  id: glyph.id, width: glyph.width, height: glyph.height, left: glyph.left, top: glyph.top,
134
138
  atlas_i: bin.x, atlas_j: bin.y, advance: glyph.advance
135
139
  };
140
+ max_glyph_height = Math.max(max_glyph_height, glyph.height);
136
141
  for (let i = 0; i < glyph.width; i++) {
137
142
  for (let j = 0; j < glyph.height; j++) {
138
143
  const glyph_idx = i + glyph.width * j;
@@ -142,38 +147,49 @@ function createAtlas(pbf_glyphs) {
142
147
  }
143
148
  });
144
149
  const glyph_M = glyphs['M'.charCodeAt(0)];
145
- const baseline = glyph_M.height - glyph_M.top;
146
- const top = -glyph_M.top;
150
+ const baseline = glyph_M === undefined ? max_glyph_height : glyph_M.height - glyph_M.top;
151
+ const top = glyph_M === undefined ? 0 : -glyph_M.top;
147
152
  return { atlas: atlas_data, atlas_width: img_width, atlas_height: img_height, baseline: baseline, top: top, glyph_info: glyphs };
148
153
  }
149
- async function getFontAtlas(url) {
150
- const resp = await fetch(url);
151
- const blob = await resp.blob();
152
- const data_buffer = await blob.arrayBuffer();
153
- // Parse the PBF and get the glyph data
154
- const glyphs = parseFontPBF(new Uint8Array(data_buffer));
154
+ const FONT_ATLAS_CACHE = new Cache(async (urls) => {
155
+ const promises = urls.map(async (url) => {
156
+ const resp = await fetch(url);
157
+ if (resp.status != 200) {
158
+ throw `Error ${resp.status} retrieving font pbf from '${url}'`;
159
+ }
160
+ const blob = await resp.blob();
161
+ const data_buffer = await blob.arrayBuffer();
162
+ // Parse the PBF and get the glyph data
163
+ return parseFontPBF(new Uint8Array(data_buffer));
164
+ });
165
+ const glyphs = (await Promise.all(promises)).flat();
155
166
  // Create an atlas for the glyphs
156
167
  return createAtlas(glyphs);
157
- }
168
+ });
158
169
  const text_collection_opt_defaults = {
159
170
  horizontal_align: 'left',
160
171
  vertical_align: 'baseline',
161
172
  font_size: 12,
162
173
  text_color: new Color([0, 0, 0, 1]),
163
174
  halo_color: new Color([0, 0, 0, 1]),
164
- halo: false
175
+ halo: false,
176
+ offset_x: 0,
177
+ offset_y: 0
165
178
  };
166
179
  class TextCollection {
167
180
  constructor(gl, text_locs, font_atlas, opts) {
168
- this.program = program_cache.getValue(gl);
169
181
  this.opts = normalizeOptions(opts, text_collection_opt_defaults);
170
182
  const is_webgl2 = isWebGL2Ctx(gl);
171
183
  const format = is_webgl2 ? gl.R8 : gl.LUMINANCE;
172
184
  const type = gl.UNSIGNED_BYTE;
173
185
  const row_alignment = 1;
186
+ const empty_atlas = font_atlas.atlas_width == 0 || font_atlas.atlas_height == 0 || font_atlas.atlas.length == 0;
187
+ const atlas_width = empty_atlas ? 1 : font_atlas.atlas_width;
188
+ const atlas_height = empty_atlas ? 1 : font_atlas.atlas_height;
189
+ const atlas_data = empty_atlas ? new Uint8Array([0, 0, 0, 0]) : font_atlas.atlas;
174
190
  const image = {
175
- 'format': format, 'type': type, 'width': font_atlas.atlas_width, 'height': font_atlas.atlas_height,
176
- 'image': font_atlas.atlas, 'row_alignment': row_alignment, 'mag_filter': gl.LINEAR
191
+ 'format': format, 'type': type, 'width': atlas_width, 'height': atlas_height,
192
+ 'image': atlas_data, 'row_alignment': row_alignment, 'mag_filter': gl.LINEAR
177
193
  };
178
194
  this.texture = new WGLTexture(gl, image);
179
195
  const n_verts = text_locs.map(tl => tl.text.length).reduce((a, b) => a + b, 0) * 6;
@@ -185,7 +201,8 @@ class TextCollection {
185
201
  const { lat, lon, text } = loc;
186
202
  const min_zoom = loc.min_zoom === undefined ? 0 : loc.min_zoom;
187
203
  const { x: anchor_x, y: anchor_y } = new LngLat(lon, lat).toMercatorCoord();
188
- let x_offset = 0;
204
+ let x_offset = this.opts.offset_x;
205
+ let y_offset = this.opts.offset_y;
189
206
  const init_i_off = i_off;
190
207
  for (let i = 0; i < text.length; i++) {
191
208
  const glyph_code = text.charCodeAt(i);
@@ -214,17 +231,17 @@ class TextCollection {
214
231
  anchor_data[i_anch++] = anchor_y;
215
232
  anchor_data[i_anch++] = min_zoom;
216
233
  offset_data[i_off++] = x_offset;
217
- offset_data[i_off++] = font_atlas.baseline + glyph_info.top - glyph_info.height;
234
+ offset_data[i_off++] = y_offset + font_atlas.baseline + glyph_info.top - glyph_info.height;
218
235
  offset_data[i_off++] = x_offset;
219
- offset_data[i_off++] = font_atlas.baseline + glyph_info.top - glyph_info.height;
236
+ offset_data[i_off++] = y_offset + font_atlas.baseline + glyph_info.top - glyph_info.height;
220
237
  offset_data[i_off++] = x_offset + glyph_info.width;
221
- offset_data[i_off++] = font_atlas.baseline + glyph_info.top - glyph_info.height;
238
+ offset_data[i_off++] = y_offset + font_atlas.baseline + glyph_info.top - glyph_info.height;
222
239
  offset_data[i_off++] = x_offset;
223
- offset_data[i_off++] = font_atlas.baseline + glyph_info.top;
240
+ offset_data[i_off++] = y_offset + font_atlas.baseline + glyph_info.top;
224
241
  offset_data[i_off++] = x_offset + glyph_info.width;
225
- offset_data[i_off++] = font_atlas.baseline + glyph_info.top;
242
+ offset_data[i_off++] = y_offset + font_atlas.baseline + glyph_info.top;
226
243
  offset_data[i_off++] = x_offset + glyph_info.width;
227
- offset_data[i_off++] = font_atlas.baseline + glyph_info.top;
244
+ offset_data[i_off++] = y_offset + font_atlas.baseline + glyph_info.top;
228
245
  tc_data[i_tc++] = glyph_info.atlas_i / font_atlas.atlas_width;
229
246
  tc_data[i_tc++] = (glyph_info.atlas_j + glyph_info.height) / font_atlas.atlas_height;
230
247
  tc_data[i_tc++] = glyph_info.atlas_i / font_atlas.atlas_width;
@@ -241,12 +258,12 @@ class TextCollection {
241
258
  }
242
259
  if (this.opts.horizontal_align == 'center') {
243
260
  for (let i = init_i_off; i < init_i_off + text.length * 12; i += 2) {
244
- offset_data[i] -= x_offset / 2;
261
+ offset_data[i] -= (x_offset - this.opts.offset_x) / 2;
245
262
  }
246
263
  }
247
264
  else if (this.opts.horizontal_align == 'right') {
248
265
  for (let i = init_i_off; i < init_i_off + text.length * 12; i += 2) {
249
- offset_data[i] -= x_offset;
266
+ offset_data[i] -= (x_offset - this.opts.offset_x);
250
267
  }
251
268
  }
252
269
  if (this.opts.vertical_align == 'top') {
@@ -260,45 +277,63 @@ class TextCollection {
260
277
  }
261
278
  }
262
279
  });
280
+ this.shader_manager = new ShaderProgramManager(text_vertex_shader_src, text_fragment_shader_src, []);
263
281
  this.anchors = new WGLBuffer(gl, anchor_data, 3, gl.TRIANGLE_STRIP);
264
282
  this.offsets = new WGLBuffer(gl, offset_data, 2, gl.TRIANGLE_STRIP);
265
283
  this.texcoords = new WGLBuffer(gl, tc_data, 2, gl.TRIANGLE_STRIP);
266
284
  }
267
- static async make(gl, text_locs, fontstack_url, opts) {
268
- const atlas = await getFontAtlas(fontstack_url);
285
+ static async make(gl, text_locs, fontstack_url_template, opts) {
286
+ const FONT_GROUP_SIZE = 256;
287
+ const characters = text_locs.map(tl => [...tl.text]).flat().map(c => c.charCodeAt(0)).filter((c, i, ary) => ary.indexOf(c) == i);
288
+ const char_code_min = Math.min(...characters);
289
+ const char_code_max = Math.max(...characters);
290
+ const stack_start = Math.floor(char_code_min / FONT_GROUP_SIZE) * FONT_GROUP_SIZE;
291
+ const stack_end = Math.floor(char_code_max / FONT_GROUP_SIZE) * FONT_GROUP_SIZE;
292
+ const fontstack_urls = [];
293
+ for (let istack = stack_start; istack <= stack_end; istack += FONT_GROUP_SIZE) {
294
+ fontstack_urls.push(fontstack_url_template.replace('{range}', `${istack}-${istack + FONT_GROUP_SIZE - 1}`));
295
+ }
296
+ const atlas = await FONT_ATLAS_CACHE.getValue(fontstack_urls);
297
+ if (atlas.atlas_height == 0 || atlas.atlas_width == 0 || atlas.atlas.length == 0)
298
+ console.warn(`No font data from '${fontstack_url_template}'`);
269
299
  return new TextCollection(gl, text_locs, atlas, opts);
270
300
  }
271
- render(gl, matrix, [map_width, map_height], map_zoom) {
301
+ render(gl, arg, [map_width, map_height], map_zoom) {
302
+ const render_data = getRendererData(arg);
303
+ const program = this.shader_manager.getShaderProgram(gl, render_data.shaderData);
272
304
  const uniforms = {
273
- 'u_matrix': matrix, 'u_map_width': map_width, 'u_map_height': map_height, 'u_map_zoom': map_zoom, 'u_font_size': this.opts.font_size,
274
- 'u_text_color': this.opts.text_color.toRGBATuple(), 'u_halo_color': this.opts.halo_color.toRGBATuple(), 'u_offset': 0
305
+ 'u_map_width': map_width, 'u_map_height': map_height, 'u_map_zoom': map_zoom, 'u_font_size': this.opts.font_size,
306
+ 'u_text_color': this.opts.text_color.toRGBATuple(), 'u_halo_color': this.opts.halo_color.toRGBATuple(), 'u_offset': 0,
307
+ ...this.shader_manager.getShaderUniforms(render_data)
275
308
  };
276
309
  uniforms['u_is_halo'] = this.opts.halo ? 1 : 0;
277
- this.program.use({ 'a_pos': this.anchors, 'a_offset': this.offsets, 'a_tex_coord': this.texcoords }, uniforms, { 'u_sdf_sampler': this.texture });
310
+ program.use({ 'a_pos': this.anchors, 'a_offset': this.offsets, 'a_tex_coord': this.texcoords }, uniforms, { 'u_sdf_sampler': this.texture });
278
311
  gl.enable(gl.BLEND);
279
312
  gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
280
- this.program.draw();
313
+ program.draw();
281
314
  if (this.opts.halo) {
282
- this.program.setUniforms({ 'u_is_halo': 0 });
283
- this.program.draw();
315
+ program.setUniforms({ 'u_is_halo': 0 });
316
+ program.draw();
284
317
  }
285
- this.program.setUniforms({ 'u_offset': -2, 'u_is_halo': this.opts.halo ? 1 : 0 });
286
- this.program.draw();
287
- if (this.opts.halo) {
288
- this.program.setUniforms({ 'u_is_halo': 0 });
289
- this.program.draw();
290
- }
291
- this.program.setUniforms({ 'u_offset': -1, 'u_is_halo': this.opts.halo ? 1 : 0 });
292
- this.program.draw();
293
- if (this.opts.halo) {
294
- this.program.setUniforms({ 'u_is_halo': 0 });
295
- this.program.draw();
296
- }
297
- this.program.setUniforms({ 'u_offset': 1, 'u_is_halo': this.opts.halo ? 1 : 0 });
298
- this.program.draw();
299
- if (this.opts.halo) {
300
- this.program.setUniforms({ 'u_is_halo': 0 });
301
- this.program.draw();
318
+ if (render_data.type != 'maplibre' || !render_data.shaderData.define.includes('GLOBE')) {
319
+ program.setUniforms({ 'u_offset': -2, 'u_is_halo': this.opts.halo ? 1 : 0 });
320
+ program.draw();
321
+ if (this.opts.halo) {
322
+ program.setUniforms({ 'u_is_halo': 0 });
323
+ program.draw();
324
+ }
325
+ program.setUniforms({ 'u_offset': -1, 'u_is_halo': this.opts.halo ? 1 : 0 });
326
+ program.draw();
327
+ if (this.opts.halo) {
328
+ program.setUniforms({ 'u_is_halo': 0 });
329
+ program.draw();
330
+ }
331
+ program.setUniforms({ 'u_offset': 1, 'u_is_halo': this.opts.halo ? 1 : 0 });
332
+ program.draw();
333
+ if (this.opts.halo) {
334
+ program.setUniforms({ 'u_is_halo': 0 });
335
+ program.draw();
336
+ }
302
337
  }
303
338
  }
304
339
  }