gridviz 3.0.36 → 3.0.37

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gridviz",
3
- "version": "3.0.36",
3
+ "version": "3.0.37",
4
4
  "description": "Visualization tool for gridded statistics",
5
5
  "keywords": [
6
6
  "statistics",
@@ -71,4 +71,4 @@
71
71
  "engines": {
72
72
  "node": ">=14"
73
73
  }
74
- }
74
+ }
@@ -120,6 +120,32 @@ export class Dataset {
120
120
 
121
121
  /** @type {number} */
122
122
  const r = this.getResolution()
123
+
124
+ // Look the cell up in an index of the cells by grid position, built once per cell
125
+ // list (the view cache is a new array after each redraw) instead of scanning every
126
+ // cell on every mouse move.
127
+ const index = this.cellIndex(cells, r)
128
+ if (index) {
129
+ const fx = (posGeo.x - index.ox) / r
130
+ const fy = (posGeo.y - index.oy) / r
131
+ const ix = Math.floor(fx)
132
+ const iy = Math.floor(fy)
133
+ // a position on a cell's right/top edge also belongs to that cell (inclusive
134
+ // bounds, as in the scan below): check the cells left of and below it too
135
+ for (const [dx, dy] of [
136
+ [0, 0],
137
+ [-1, 0],
138
+ [0, -1],
139
+ [-1, -1],
140
+ ]) {
141
+ if (dx && fx !== ix) continue
142
+ if (dy && fy !== iy) continue
143
+ const cell = index.cells.get(cellKey(ix + dx, iy + dy))
144
+ if (cell) return cell
145
+ }
146
+ return undefined
147
+ }
148
+
123
149
  for (const cell of cells) {
124
150
  if (posGeo.x < cell.x) continue
125
151
  else if (cell.x + r < posGeo.x) continue
@@ -130,6 +156,47 @@ export class Dataset {
130
156
  return undefined
131
157
  }
132
158
 
159
+ /**
160
+ * An index of cells by their position on the grid of resolution r, or null when some
161
+ * cell isn't on a common grid (then the caller scans the cells). Cached for the last
162
+ * cell list given.
163
+ *
164
+ * @param {Array.<Cell>} cells
165
+ * @param {number} r
166
+ * @returns {{cells: Map<number, Cell>, ox: number, oy: number} | null}
167
+ * @protected
168
+ */
169
+ cellIndex(cells, r) {
170
+ if (this._index && this._indexCells === cells && this._indexLength === cells.length && this._indexR === r)
171
+ return this._index
172
+ let index = null
173
+ if (cells.length && r > 0) {
174
+ const ox = +cells[0].x
175
+ const oy = +cells[0].y
176
+ const map = new Map()
177
+ index = { cells: map, ox, oy }
178
+ for (const cell of cells) {
179
+ const fx = (+cell.x - ox) / r
180
+ const fy = (+cell.y - oy) / r
181
+ const ix = Math.round(fx)
182
+ const iy = Math.round(fy)
183
+ // not on the grid: no index, scan instead
184
+ if (Math.abs(fx - ix) > 1e-6 || Math.abs(fy - iy) > 1e-6) {
185
+ index = null
186
+ break
187
+ }
188
+ const key = cellKey(ix, iy)
189
+ // the first cell at a position wins, as in the scan
190
+ if (!map.has(key)) map.set(key, cell)
191
+ }
192
+ }
193
+ this._index = index
194
+ this._indexCells = cells
195
+ this._indexLength = cells.length
196
+ this._indexR = r
197
+ return index
198
+ }
199
+
133
200
  //getters and setters
134
201
 
135
202
  /** @returns {number} */
@@ -152,3 +219,14 @@ export class Dataset {
152
219
  return this
153
220
  }
154
221
  }
222
+
223
+ /**
224
+ * One number per grid position: cell positions relative to the first cell stay well
225
+ * within +-2^21, so x * 2^22 + y is exact.
226
+ * @param {number} ix
227
+ * @param {number} iy
228
+ * @returns {number}
229
+ */
230
+ function cellKey(ix, iy) {
231
+ return ix * 4194304 + iy
232
+ }
package/src/core/Map.js CHANGED
@@ -30,6 +30,16 @@ export class Map {
30
30
  * */
31
31
  this.layers = opts.layers || []
32
32
 
33
+ /**
34
+ * When true, redraw() schedules one redraw for the next animation frame instead of
35
+ * drawing at once, so the many redraw requests of a busy moment (every tile that
36
+ * arrives, every info.json, every setting change) are drawn once. redrawNow() still
37
+ * draws at once. Off by default: redraw() stays synchronous, as it always was.
38
+ * @type {boolean} */
39
+ this.asyncRedraw = !!opts.asyncRedraw
40
+ /** @private @type {number|null} */
41
+ this._redrawFrame = null
42
+
33
43
  //get container element
34
44
  this.container = container || document.getElementById('gridviz')
35
45
  if (!this.container) {
@@ -96,10 +106,31 @@ export class Map {
96
106
  * @type {boolean} */
97
107
  this.digForTooltip = opts.digForTooltip || false
98
108
 
99
- // add event listeners to container
100
- this.mouseOverHandler = (e) => this.focusCell(e)
101
- this.mouseMoveHandler = (e) => this.focusCell(e)
102
- this.mouseOutHandler = (e) => this.tooltip.hide()
109
+ // add event listeners to the canvas. Mouse moves are handled at most once per
110
+ // animation frame (the latest one): finding the cell and redrawing the highlight
111
+ // for every intermediate event only delays the one that's shown.
112
+ /** @private */
113
+ this._focusFrame = null
114
+ /** @private */
115
+ this._focusEvent = null
116
+ const scheduleFocus = (e) => {
117
+ this._focusEvent = e
118
+ if (this._focusFrame) return
119
+ this._focusFrame = requestAnimationFrame(() => {
120
+ this._focusFrame = null
121
+ const ev = this._focusEvent
122
+ this._focusEvent = null
123
+ if (ev) this.focusCell(ev)
124
+ })
125
+ }
126
+ this.mouseOverHandler = scheduleFocus
127
+ this.mouseMoveHandler = scheduleFocus
128
+ this.mouseOutHandler = (e) => {
129
+ if (this._focusFrame) cancelAnimationFrame(this._focusFrame)
130
+ this._focusFrame = null
131
+ this._focusEvent = null
132
+ this.tooltip.hide()
133
+ }
103
134
  this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
104
135
  this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
105
136
  this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
@@ -176,8 +207,31 @@ export class Map {
176
207
  return this
177
208
  }
178
209
 
179
- /** @returns {this} */
210
+ /**
211
+ * Redraws the map: at once, or, with the asyncRedraw option, once at the next
212
+ * animation frame however many times it's called before then.
213
+ * @returns {this} */
180
214
  redraw() {
215
+ if (this.asyncRedraw) {
216
+ if (!this._redrawFrame && !this._destroyed)
217
+ this._redrawFrame = requestAnimationFrame(() => {
218
+ this._redrawFrame = null
219
+ this.redrawNow()
220
+ })
221
+ return this
222
+ }
223
+ return this.redrawNow()
224
+ }
225
+
226
+ /**
227
+ * Redraws the map at once, whatever the asyncRedraw option (and cancels a scheduled redraw).
228
+ * @returns {this} */
229
+ redrawNow() {
230
+ if (this._redrawFrame) cancelAnimationFrame(this._redrawFrame)
231
+ this._redrawFrame = null
232
+ // a destroyed map can still be asked to redraw by a dataset it shared (see destroy)
233
+ if (this._destroyed) return this
234
+
181
235
  //remove legend elements
182
236
  if (this.legend) this.legend.selectAll('*').remove()
183
237
 
@@ -220,6 +274,8 @@ export class Map {
220
274
  this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
221
275
 
222
276
  this.canvasSave = null
277
+ // the canvas holds no focus rectangle any more
278
+ this._focusedCell = undefined
223
279
 
224
280
  return this
225
281
  }
@@ -246,6 +302,18 @@ export class Map {
246
302
  /** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
247
303
  const focus = this.getCellFocusInfo(mousePositionGeo)
248
304
 
305
+ // Same cell as the one already highlighted (or still no cell): the canvas is right
306
+ // as it is, only the tooltip follows the mouse. Saves a full-canvas copy per move.
307
+ const focusedCell = focus ? focus.cell : null
308
+ if (focusedCell === this._focusedCell) {
309
+ if (focus) {
310
+ this.tooltip.setPosition(e)
311
+ this.tooltip.show()
312
+ } else this.tooltip.hide()
313
+ return
314
+ }
315
+ this._focusedCell = focusedCell
316
+
249
317
  // Transparent background (Leaflet): redraw base, then draw focus rect directly on the visible canvas
250
318
  if (this.transparentBackground) {
251
319
  // Always restore the base (offscreen) into the visible canvas
@@ -536,6 +604,8 @@ export class Map {
536
604
  })
537
605
 
538
606
  resizeObserver.observe(this.container)
607
+ /** @private */
608
+ this._resizeObserver = resizeObserver
539
609
  }
540
610
 
541
611
  /**
@@ -544,14 +614,26 @@ export class Map {
544
614
  * @memberof App
545
615
  */
546
616
  destroy() {
617
+ this._destroyed = true
618
+
547
619
  // clear layers
548
620
  this.layers = []
549
621
  this.bgLayers = []
550
622
 
551
- // remove event listeners from container
552
- this.container.removeEventListener('mouseover', this.mouseOverHandler)
553
- this.container.removeEventListener('mousemove', this.mouseMoveHandler)
554
- this.container.removeEventListener('mouseout', this.mouseOutHandler)
623
+ // cancel scheduled work
624
+ if (this._redrawFrame) cancelAnimationFrame(this._redrawFrame)
625
+ this._redrawFrame = null
626
+ if (this._focusFrame) cancelAnimationFrame(this._focusFrame)
627
+ this._focusFrame = null
628
+
629
+ // stop observing the container: the observer would otherwise keep this map alive
630
+ if (this._resizeObserver) this._resizeObserver.disconnect()
631
+ this._resizeObserver = null
632
+
633
+ // remove event listeners, from the canvas they were added to
634
+ this.geoCanvas.canvas.removeEventListener('mouseover', this.mouseOverHandler)
635
+ this.geoCanvas.canvas.removeEventListener('mousemove', this.mouseMoveHandler)
636
+ this.geoCanvas.canvas.removeEventListener('mouseout', this.mouseOutHandler)
555
637
 
556
638
  // remove canvas
557
639
  this.geoCanvas.canvas.remove()
@@ -55,22 +55,43 @@ export class TiledGrid extends Dataset {
55
55
  */
56
56
  loadInfo() {
57
57
  if (!this.info && this.infoLoadingStatus === 'notLoaded') {
58
+ // marked before the request, so the redraws that happen while it's under way
59
+ // don't request it again
60
+ this.infoLoadingStatus = 'loading'
58
61
  ;(async () => {
59
62
  try {
60
63
  const data = await json(this.url + 'info.json')
61
64
  this.info = data
62
65
  this.resolution = data.resolutionGeo
63
66
  this.infoLoadingStatus = 'loaded'
64
- this.map.redraw()
67
+ // all the resolutions of a multi-resolution dataset load their info at
68
+ // once: only the one currently shown needs a redraw
69
+ if (this.isShown()) this.map.redraw()
65
70
  } catch (error) {
66
71
  //mark as failed
67
72
  this.infoLoadingStatus = 'failed'
68
73
  }
69
74
  })()
70
- } else if (this.infoLoadingStatus === 'loaded' || this.infoLoadingStatus === 'failed') this.map.redraw()
75
+ }
76
+ // (no redraw for a failed info.json: every redraw would ask again - an endless loop)
71
77
  return this
72
78
  }
73
79
 
80
+ /**
81
+ * True when a visible layer of the map shows this dataset at the current zoom.
82
+ * @returns {boolean}
83
+ */
84
+ isShown() {
85
+ if (!this.map || !this.map.layers) return false
86
+ const z = this.map.getZoom()
87
+ for (const lay of this.map.layers) {
88
+ if (lay.visible && !lay.visible(z)) continue
89
+ if (!lay.getDataset) continue
90
+ if (lay.getDataset(z) == this) return true
91
+ }
92
+ return false
93
+ }
94
+
74
95
  /**
75
96
  * Compute a tiling envelope from a geographical envelope.
76
97
  * This is the function to use to know which tiles to download for a geographical view.
@@ -197,19 +218,7 @@ export class TiledGrid extends Dataset {
197
218
  // Check if any visible layer depends on this dataset
198
219
  // check if redraw is really needed, that is if:
199
220
  // 1. the dataset belongs to a layer which is visible at the current zoom level
200
- let needsRedraw = false
201
- //go through the layers
202
- const z = this.map.getZoom()
203
- for (const lay of this.map.layers) {
204
- if (lay.visible && !lay.visible(z)) continue
205
- if (!lay.getDataset) continue
206
- if (lay.getDataset(z) != this) continue
207
- //found one layer. No need to seek more.
208
- needsRedraw = true
209
- break
210
- }
211
-
212
- if (!needsRedraw) return
221
+ if (!this.isShown()) return
213
222
 
214
223
  // Check if tile intersects the current view
215
224
  const env = this.map.updateExtentGeo()
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { Style } from '../core/Style.js'
5
5
  import { randomNormal } from 'd3-random'
6
- import { checkWebGLSupport, makeWebGLCanvas } from '../utils/webGLUtils.js'
6
+ import { checkWebGLSupport, getSharedWebGLCanvas } from '../utils/webGLUtils.js'
7
7
  import { WebGLSquareColoring } from '../utils/WebGLSquareColoring.js'
8
8
  import { color } from 'd3-color'
9
9
 
@@ -62,8 +62,8 @@ export class DotDensityStyle extends Style {
62
62
  const ctx = geoCanvas.offscreenCtx
63
63
 
64
64
  if (checkWebGLSupport()) {
65
- //create canvas and webgl renderer
66
- const cvWGL = makeWebGLCanvas(geoCanvas.w + '', geoCanvas.h + '')
65
+ //the shared webgl canvas (one context for all styles, instead of a new one per draw)
66
+ const cvWGL = getSharedWebGLCanvas(geoCanvas.w, geoCanvas.h)
67
67
  if (!cvWGL) {
68
68
  console.error('No webGL')
69
69
  return
@@ -2,9 +2,36 @@
2
2
  'use strict'
3
3
 
4
4
  import { Style } from '../core/Style.js'
5
- import { initShaderProgram, createShader, makeWebGLCanvas } from '../utils/webGLUtils.js'
5
+ import { getSharedWebGLCanvas, getProgram, getBuffer, bufferOwner } from '../utils/webGLUtils.js'
6
6
  import { color } from 'd3-color'
7
7
 
8
+ const VERTEX_SHADER = `
9
+ attribute vec2 pos;
10
+ uniform float sizePix;
11
+ uniform mat3 mat;
12
+ attribute float i;
13
+ varying float vi;
14
+ void main() {
15
+ gl_Position = vec4(mat * vec3(pos, 1.0), 1.0);
16
+ gl_PointSize = sizePix;
17
+ vi = i;
18
+ }`
19
+
20
+ const FRAGMENT_SHADER = `
21
+ precision highp float;
22
+ varying float vi;
23
+ uniform sampler2D colorLUT;
24
+ uniform float lutSize;
25
+ void main(void) {
26
+ float idx = floor(vi + 0.5);
27
+ float u = (idx + 0.5) / lutSize;
28
+ gl_FragColor = texture2D(colorLUT, vec2(u, 0.5));
29
+ }`
30
+
31
+ // names of the buffers this style fills in the shared context (getBuffer)
32
+ const POSITION_BUFFER = 'SquareColorCategoryWebGLStyle.pos'
33
+ const INDEX_BUFFER = 'SquareColorCategoryWebGLStyle.i'
34
+
8
35
  /**
9
36
  * Style based on webGL
10
37
  * To show cells as colored squares, from categories.
@@ -45,9 +72,15 @@ export class SquareColorCategoryWebGLStyle extends Style {
45
72
  * @type {function(number,number):number} */
46
73
  this.size = opts.size // (resolution, z) => ...
47
74
 
48
- /** * @type {{canvas:HTMLCanvasElement, gl:WebGLRenderingContext, width:number, height:number }}*/
75
+ /** The shared WebGL canvas this style last drew into (getSharedWebGLCanvas)
76
+ * @type {{canvas:HTMLCanvasElement, gl:WebGLRenderingContext, width:number, height:number }}*/
49
77
  this.cvWGL = undefined
50
- this.programm = undefined
78
+ /** @type {WebGLRenderingContext|undefined} */
79
+ this.gl = undefined
80
+ this.program = undefined
81
+ this.lutTexture = undefined
82
+ /** Number of points in the shared buffers, for drawArrays */
83
+ this.pointsNb = 0
51
84
 
52
85
  this.x = undefined
53
86
  this.y = undefined
@@ -55,88 +88,68 @@ export class SquareColorCategoryWebGLStyle extends Style {
55
88
  this.cellsNb = undefined
56
89
  }
57
90
 
91
+ /**
92
+ * Binds this style's program, colour lookup texture and uniforms in the shared WebGL
93
+ * context (getSharedWebGLCanvas): compiled once per context, so a new style instance
94
+ * (e.g. a setting change rebuilding the style) costs no new context or program.
95
+ * @returns {boolean} true when the context is new to this style (its data must be re-uploaded)
96
+ */
58
97
  init(w, h) {
59
- this.cvWGL = makeWebGLCanvas(w + '', h + '')
60
- if (!this.cvWGL) { console.error('No webGL'); return }
61
-
98
+ this.cvWGL = getSharedWebGLCanvas(w, h)
62
99
  const gl = this.cvWGL.gl
63
-
64
- //draw
65
- const vectorShader = `
66
- attribute vec2 pos;
67
- uniform float sizePix;
68
- uniform mat3 mat;
69
- attribute float i;
70
- varying float vi;
71
- void main() {
72
- gl_Position = vec4(mat * vec3(pos, 1.0), 1.0);
73
- gl_PointSize = sizePix;
74
- vi = i;
75
- }`
76
- /** @type {WebGLShader} */
77
- const vShader = createShader(gl, gl.VERTEX_SHADER, vectorShader)
78
-
79
- const fragmentShader = `
80
- precision highp float;
81
- varying float vi;
82
- uniform sampler2D colorLUT;
83
- uniform float lutSize;
84
- void main(void) {
85
- float idx = floor(vi + 0.5);
86
- float u = (idx + 0.5) / lutSize;
87
- gl_FragColor = texture2D(colorLUT, vec2(u, 0.5));
88
- }`
89
- /** @type {WebGLShader} */
90
- const fShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShader)
91
-
92
- /** @type {WebGLProgram} */
93
- this.program = initShaderProgram(gl, vShader, fShader)
100
+ const contextChanged = this.gl !== gl
101
+ if (contextChanged) {
102
+ this.gl = gl
103
+ this.program = getProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER)
104
+ this.lutTexture = undefined
105
+ }
94
106
  gl.useProgram(this.program)
107
+ this.bindColors()
108
+ return contextChanged
95
109
  }
96
110
 
97
111
  bindColors() {
98
112
  const gl = this.cvWGL.gl
99
- const lutSize = this.colors.length;
100
- const lutData = new Uint8Array(lutSize * 4); // RGBA for each entry
101
-
102
- // Fill lutData with your color values (e.g., rainbow, grayscale, etc.)
103
- for (let i = 0; i < lutSize; i++) {
104
- const c = color(this.colors[i])
105
- lutData[i * 4] = +c.r; // R
106
- lutData[i * 4 + 1] = c.g; // G
107
- lutData[i * 4 + 2] = c.b; // B
108
- lutData[i * 4 + 3] = c?.opacity * 255; // A
113
+ const lutSize = this.colors.length
114
+
115
+ // created once per style and context
116
+ if (!this.lutTexture) {
117
+ const lutData = new Uint8Array(lutSize * 4) // RGBA for each entry
118
+ for (let i = 0; i < lutSize; i++) {
119
+ const c = color(this.colors[i])
120
+ lutData[i * 4] = +c.r
121
+ lutData[i * 4 + 1] = c.g
122
+ lutData[i * 4 + 2] = c.b
123
+ lutData[i * 4 + 3] = c?.opacity * 255
124
+ }
125
+ this.lutTexture = gl.createTexture()
126
+ gl.bindTexture(gl.TEXTURE_2D, this.lutTexture)
127
+ gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, lutSize, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, lutData)
128
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
129
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
130
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
131
+ gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
109
132
  }
110
133
 
111
- // Create and bind texture
112
- const lutTexture = gl.createTexture();
113
- gl.bindTexture(gl.TEXTURE_2D, lutTexture);
114
- gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, lutSize, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, lutData);
115
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
116
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
117
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
118
- gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
119
-
120
- // Get uniform locations
121
- const uColorLUT = gl.getUniformLocation(this.program, 'colorLUT');
122
- const uLutSize = gl.getUniformLocation(this.program, 'lutSize');
123
-
124
- // Set uniform values
125
- gl.uniform1i(uColorLUT, 0); // Texture unit 0
126
- gl.uniform1f(uLutSize, lutSize);
127
-
128
- // Bind the texture to texture unit 0
129
- gl.activeTexture(gl.TEXTURE0);
130
- gl.bindTexture(gl.TEXTURE_2D, lutTexture);
134
+ gl.uniform1i(gl.getUniformLocation(this.program, 'colorLUT'), 0) // Texture unit 0
135
+ gl.uniform1f(gl.getUniformLocation(this.program, 'lutSize'), lutSize)
136
+ gl.activeTexture(gl.TEXTURE0)
137
+ gl.bindTexture(gl.TEXTURE_2D, this.lutTexture)
131
138
  }
132
139
 
140
+ /**
141
+ * Fills the shared position and colour index buffers with the cells to draw.
142
+ * @returns {number} the number of points uploaded (cells without a category are left out)
143
+ */
133
144
  bindVertices(cells, resolution, z, viewScale) {
134
145
  const gl = this.cvWGL.gl
135
146
  //add vertice and fragment data
136
147
  const r2 = resolution / 2
137
- let c, nb = cells.length
138
- const verticesBuffer = []
139
- const iBuffer = []
148
+ let c,
149
+ nb = cells.length
150
+ const verticesBuffer = new Float32Array(2 * nb)
151
+ const iBuffer = new Float32Array(nb)
152
+ let n = 0
140
153
  for (let i = 0; i < nb; i++) {
141
154
  c = cells[i]
142
155
  const cat = this.code(c, resolution, z, viewScale)
@@ -149,34 +162,33 @@ export class SquareColorCategoryWebGLStyle extends Style {
149
162
  console.log('Unexpected category index: ' + cat + ' ' + i_)
150
163
  continue
151
164
  }
152
- verticesBuffer.push(c.x + r2, c.y + r2)
153
- iBuffer.push(+i_)
165
+ verticesBuffer[2 * n] = c.x + r2
166
+ verticesBuffer[2 * n + 1] = c.y + r2
167
+ iBuffer[n] = +i_
168
+ n++
154
169
  }
155
170
 
156
- //bind vertice data
157
- gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
158
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(verticesBuffer), gl.STATIC_DRAW)
171
+ gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, POSITION_BUFFER))
172
+ gl.bufferData(gl.ARRAY_BUFFER, verticesBuffer.subarray(0, 2 * n), gl.DYNAMIC_DRAW)
173
+ gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, INDEX_BUFFER))
174
+ gl.bufferData(gl.ARRAY_BUFFER, iBuffer.subarray(0, n), gl.DYNAMIC_DRAW)
175
+ bufferOwner(gl, POSITION_BUFFER, this)
176
+ return n
177
+ }
178
+
179
+ /** Points the program's attributes at the shared buffers (whatever another style left bound). */
180
+ bindAttributes() {
181
+ const gl = this.cvWGL.gl
159
182
  const position = gl.getAttribLocation(this.program, 'pos')
160
- gl.vertexAttribPointer(
161
- position,
162
- 2, //numComponents
163
- gl.FLOAT, //type
164
- false, //normalise
165
- 0, //stride
166
- 0 //offset
167
- )
183
+ gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, POSITION_BUFFER))
184
+ gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0)
168
185
  gl.enableVertexAttribArray(position)
169
-
170
- //i data
171
- gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer())
172
- gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(iBuffer), gl.STATIC_DRAW)
173
186
  const i = gl.getAttribLocation(this.program, 'i')
187
+ gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, INDEX_BUFFER))
174
188
  gl.vertexAttribPointer(i, 1, gl.FLOAT, false, 0, 0)
175
189
  gl.enableVertexAttribArray(i)
176
190
  }
177
191
 
178
-
179
-
180
192
  // check if the vertices have to be bound again
181
193
  mapContentChanged(v, cellsNb) {
182
194
  if (v.x == this.x && v.y == this.y && v.z == this.z && this.cellsNb == cellsNb) return false
@@ -203,13 +215,8 @@ export class SquareColorCategoryWebGLStyle extends Style {
203
215
  //get view scale
204
216
  const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
205
217
 
206
- //create canvas and webgl renderer
207
- let contextRecreated = false
208
- if (!this.cvWGL || geoCanvas.w != this.cvWGL.width || geoCanvas.h != this.cvWGL.height) {
209
- contextRecreated = true
210
- this.init(geoCanvas.w, geoCanvas.h)
211
- this.bindColors()
212
- }
218
+ //bind this style's program in the shared WebGL context
219
+ const contextChanged = this.init(geoCanvas.w, geoCanvas.h)
213
220
  const gl = this.cvWGL.gl
214
221
  const canvas = this.cvWGL.canvas
215
222
 
@@ -217,32 +224,29 @@ export class SquareColorCategoryWebGLStyle extends Style {
217
224
  const sizePix = this.size ? this.size(resolution, z) / z : resolution / z + 0.2
218
225
  gl.uniform1f(gl.getUniformLocation(this.program, 'sizePix'), 1.0 * sizePix)
219
226
 
220
- //
221
- // mapContentChanged() is a cheap proxy for "did the content change" based
222
- // on view x/y/z and cell count alone - it has no idea the WebGL context
223
- // itself was just thrown away and recreated a few lines up (geoCanvas.w/h
224
- // changing, e.g. from a browser/devtools resize, recreates cvWGL with a
225
- // fresh, empty vertex buffer). A resize alone often leaves the geographic
226
- // view and cell count identical to the previous draw, so without forcing
227
- // a rebind here, mapContentChanged reports "unchanged" and the rebind gets
228
- // skipped - leaving the new context's vertex buffer never populated and
229
- // gl.drawArrays() drawing nothing, silently (confirmed via a resize-storm
230
- // test: reinit-without-rebind happened on 5 of 9 context recreations).
231
- const shouldRebind = this.mapContentChanged(geoCanvas.view, cells.length) || contextRecreated
232
- if (shouldRebind)
233
- //bind vertices
234
- this.bindVertices(cells, resolution, z, viewScale)
235
- //transformation
236
- gl.uniformMatrix3fv(gl.getUniformLocation(this.program, 'mat'), false, new Float32Array(geoCanvas.getWebGLTransform()))
237
-
238
- // Enable the depth test
239
- //gl.enable(gl.DEPTH_TEST);
227
+ // mapContentChanged() is a cheap proxy for "did the content change", based on view
228
+ // x/y/z and cell count alone. The upload is also needed when the shared buffers
229
+ // hold another style's data (bufferOwner), or the context is new to this style
230
+ // (e.g. replaced after being lost).
231
+ const contentChanged = this.mapContentChanged(geoCanvas.view, cells.length)
232
+ if (contentChanged || contextChanged || bufferOwner(gl, POSITION_BUFFER) !== this)
233
+ this.pointsNb = this.bindVertices(cells, resolution, z, viewScale)
234
+ this.bindAttributes()
235
+
236
+ //transformation
237
+ gl.uniformMatrix3fv(
238
+ gl.getUniformLocation(this.program, 'mat'),
239
+ false,
240
+ new Float32Array(geoCanvas.getWebGLTransform())
241
+ )
242
+
240
243
  // Clear the color buffer bit
244
+ gl.clearColor(0, 0, 0, 0)
241
245
  gl.clear(gl.COLOR_BUFFER_BIT)
242
- // Set the view port
243
- //gl.viewport(0, 0, cg.w, cg.h);
244
246
 
245
- gl.drawArrays(gl.POINTS, 0, cells.length)
247
+ // only the points actually uploaded: cells without a category are left out, and
248
+ // drawing past the end of the buffer is a WebGL error that draws nothing at all
249
+ gl.drawArrays(gl.POINTS, 0, this.pointsNb)
246
250
 
247
251
  //draw in canvas geo
248
252
  geoCanvas.initCanvasTransform()