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/dist/gridviz.js +6506 -2937
- package/dist/gridviz.min.js +1 -1
- package/package.json +2 -2
- package/src/core/Dataset.js +78 -0
- package/src/core/Map.js +91 -9
- package/src/dataset/TiledGrid.js +24 -15
- package/src/style/DotDensityStyle.js +3 -3
- package/src/style/SquareColorCategoryWebGLStyle.js +123 -119
- package/src/style/SquareColorWebGLStyle.js +5 -5
- package/src/utils/WebGLSquareColoring.js +28 -32
- package/src/utils/WebGLSquareColoringAdvanced.js +26 -30
- package/src/utils/webGLUtils.js +105 -0
package/package.json
CHANGED
package/src/core/Dataset.js
CHANGED
|
@@ -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
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
552
|
-
this.
|
|
553
|
-
this.
|
|
554
|
-
this.
|
|
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()
|
package/src/dataset/TiledGrid.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
66
|
-
const cvWGL =
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
112
|
-
|
|
113
|
-
gl.
|
|
114
|
-
gl.
|
|
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,
|
|
138
|
-
|
|
139
|
-
const
|
|
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
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
gl.
|
|
158
|
-
gl.
|
|
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.
|
|
161
|
-
|
|
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
|
-
//
|
|
207
|
-
|
|
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
|
-
//
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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()
|