gridviz 3.1.0 → 3.1.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/README.md +18 -15
- package/dist/gridviz.js +3829 -2623
- package/dist/gridviz.min.js +1 -1
- package/package.json +5 -3
- package/src/button/Button.js +77 -68
- package/src/button/FullscreenButton.js +123 -123
- package/src/button/ZoomButtons.js +109 -104
- package/src/core/GeoCanvas.js +15 -0
- package/src/core/Legend.js +267 -245
- package/src/core/Map.js +819 -670
- package/src/core/Tooltip.js +191 -185
- package/src/index.js +82 -79
- package/src/layer/GridLayer.js +4 -3
- package/src/layer/LabelLayer.js +7 -4
- package/src/legend/ColorCategoryLegend.js +127 -125
- package/src/legend/ColorDiscreteLegend.js +116 -114
- package/src/legend/ColorLegend.js +33 -11
- package/src/legend/OrientationLegend.js +94 -92
- package/src/legend/SizeLegend.js +8 -1
- package/src/legend/TernaryLegend.js +317 -314
- package/src/legend/VennLegend.js +202 -0
- package/src/style/CircleColorSizeWebGLStyle.js +249 -0
- package/src/style/CompositionStyle.js +1 -1
- package/src/style/DotDensityStyle.js +8 -4
- package/src/style/ImageStyle.js +1 -1
- package/src/style/Interpolator.js +283 -259
- package/src/style/IsoFenceStyle.js +1 -1
- package/src/style/LegoStyle.js +6 -5
- package/src/style/MosaicStyle.js +1 -1
- package/src/style/NinjaStarStyle.js +1 -1
- package/src/style/PillarStyle.js +1 -1
- package/src/style/RandomWalkInterpolator.js +213 -0
- package/src/style/ShadingRayStyle.js +16 -2
- package/src/style/ShadingStyle.js +80 -72
- package/src/style/ShapeColorSizeStyle.js +18 -2
- package/src/style/SideCategoryStyle.js +1 -1
- package/src/style/SideStyle.js +1 -1
- package/src/style/SideTanakaStyle.js +1 -1
- package/src/style/SquareColorCategoryWebGLStyle.js +71 -20
- package/src/style/SquareColorWebGLStyle.js +7 -4
- package/src/style/StrokeStyle.js +1 -1
- package/src/style/TextStyle.js +1 -1
- package/src/style/TimeSeriesStyle.js +14 -7
- package/src/utils/WebGLSquareColoringAdvanced.js +47 -9
- package/src/utils/scale.js +20 -0
- package/src/utils/webGLUtils.js +52 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { Legend } from '../core/Legend.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A legend for the colors of a map which shows two or three variables as overlapping layers, one color for each,
|
|
8
|
+
* blended by multiplication. It is a Venn diagram: a circle for each variable, in its color, and the colors of the overlaps are the mix of the colors
|
|
9
|
+
* of the circles.
|
|
10
|
+
*
|
|
11
|
+
* It is interactive, like the other legends: when the pointer is over a part of the diagram, only the cells drawn in the color(s) of the circles
|
|
12
|
+
* at this place are shown on the map. For a part of a single circle, it is the cells of its color. For an overlap, it is the cells of the colors
|
|
13
|
+
* of all the circles which overlap. Hovering the label of a circle shows the cells of its color.
|
|
14
|
+
*
|
|
15
|
+
* By default, all the styles of the layer the legend is in are highlighted, so the legend should be put on one of the styles of the layer,
|
|
16
|
+
* and the styles which draw the variables should be the ones of the layer. The `styles` option sets the styles to highlight otherwise.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* style.legends = [
|
|
20
|
+
* new gridviz.VennLegend({
|
|
21
|
+
* title: 'Crops',
|
|
22
|
+
* items: [
|
|
23
|
+
* { label: 'Fruit trees', color: 'rgb(0,255,255)' },
|
|
24
|
+
* { label: 'Olive groves', color: 'rgb(245,210,0)' },
|
|
25
|
+
* { label: 'Vineyards', color: 'rgb(255,20,147)' },
|
|
26
|
+
* ],
|
|
27
|
+
* }),
|
|
28
|
+
* ]
|
|
29
|
+
*
|
|
30
|
+
* @module legend
|
|
31
|
+
* @author Joseph Davies
|
|
32
|
+
*/
|
|
33
|
+
export class VennLegend extends Legend {
|
|
34
|
+
/**
|
|
35
|
+
* @param {Object} opts The options of the legend (see Legend for the common ones: title, labelFontSize, labelFormat, interactive, className...).
|
|
36
|
+
* @param {Array.<{label:string, color:string}>} opts.items The two or three variables: the label shown beside its circle, and its color. The color should be the one the style which draws the variable uses (it is matched with it to highlight the cells).
|
|
37
|
+
* @param {number} [opts.radius] The radius of the circles, in pixels. Default: 45.
|
|
38
|
+
* @param {number} [opts.opacity] The opacity of the circles, within [0,1]. Default: 1.
|
|
39
|
+
* @param {Array.<import('../core/Style.js').Style>} [opts.styles] The styles to highlight, which draw the variables. Default: all the styles of the layer of the legend.
|
|
40
|
+
*/
|
|
41
|
+
constructor(opts) {
|
|
42
|
+
super(opts)
|
|
43
|
+
opts = opts || {}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The variables: label and color.
|
|
47
|
+
* @type {Array.<{label:string, color:string}>} */
|
|
48
|
+
this.items = (opts.items || []).slice(0, 3)
|
|
49
|
+
if (this.items.length < 2) throw new Error('A Venn legend needs two or three items')
|
|
50
|
+
|
|
51
|
+
/** The radius of the circles, in pixels.
|
|
52
|
+
* @type {number} */
|
|
53
|
+
this.radius = opts.radius || 45
|
|
54
|
+
|
|
55
|
+
/** The opacity of the circles.
|
|
56
|
+
* @type {number} */
|
|
57
|
+
this.opacity = opts.opacity === undefined ? 1 : opts.opacity
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The styles to highlight, if not the ones of the layer.
|
|
61
|
+
* @type {Array.<import('../core/Style.js').Style>|undefined} */
|
|
62
|
+
this.styles = opts.styles
|
|
63
|
+
|
|
64
|
+
/** The parts of the diagram being hovered, as a key. @private @type {string|null} */
|
|
65
|
+
this._hovered = null
|
|
66
|
+
|
|
67
|
+
this.setType('venn')
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The styles to highlight: the ones given, else all the ones of the layer.
|
|
72
|
+
* @returns {Array.<import('../core/Style.js').Style>}
|
|
73
|
+
* @override
|
|
74
|
+
* @protected
|
|
75
|
+
*/
|
|
76
|
+
highlightedStyles() {
|
|
77
|
+
if (this.styles) return this.styles
|
|
78
|
+
return this.layerStyles && this.layerStyles.length ? this.layerStyles : super.highlightedStyles()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The centers of the circles, relative to the center of the diagram. @private */
|
|
82
|
+
centers() {
|
|
83
|
+
const d = this.items.length === 2 ? 1.1 * this.radius : 1.3 * this.radius
|
|
84
|
+
return this.items.length === 2
|
|
85
|
+
? [
|
|
86
|
+
[-d / 2, 0],
|
|
87
|
+
[d / 2, 0],
|
|
88
|
+
]
|
|
89
|
+
: [
|
|
90
|
+
[-d / 2, 0],
|
|
91
|
+
[d / 2, 0],
|
|
92
|
+
[0, (d * Math.sqrt(3)) / 2],
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Draw the legend.
|
|
98
|
+
*/
|
|
99
|
+
update() {
|
|
100
|
+
//clear
|
|
101
|
+
this.div.selectAll('*').remove()
|
|
102
|
+
|
|
103
|
+
//title
|
|
104
|
+
this.makeTitle()
|
|
105
|
+
|
|
106
|
+
const R = this.radius
|
|
107
|
+
const centers = this.centers()
|
|
108
|
+
const n = this.items.length
|
|
109
|
+
|
|
110
|
+
// the size of the labels, which stand on the sides of the circles, and below the third one
|
|
111
|
+
const fontSize = parseFloat(this.labelFontSize) || 14
|
|
112
|
+
const unit = /px$/.test(this.labelFontSize) || !isNaN(+this.labelFontSize) ? 1 : 16
|
|
113
|
+
const fontPx = fontSize * unit
|
|
114
|
+
const textWidth = (/** @type {string} */ label) => label.length * fontPx * 0.56
|
|
115
|
+
const gap = 7
|
|
116
|
+
|
|
117
|
+
// the position of the circles, in the svg: the labels of the first two are on the sides, the one of the third is below
|
|
118
|
+
const leftWidth = textWidth(this.items[0].label) + gap
|
|
119
|
+
const rightWidth = textWidth(this.items[1].label) + gap
|
|
120
|
+
const spread = centers[1][0] - centers[0][0]
|
|
121
|
+
const cx = centers.map((c) => leftWidth + R + (c[0] - centers[0][0]))
|
|
122
|
+
const cy = centers.map((c) => R + c[1])
|
|
123
|
+
const w = Math.ceil(leftWidth + 2 * R + spread + rightWidth)
|
|
124
|
+
const height = Math.ceil(n === 2 ? 2 * R : centers[2][1] + 2 * R + gap + fontPx)
|
|
125
|
+
|
|
126
|
+
const svg = this.div
|
|
127
|
+
.append('svg')
|
|
128
|
+
.classed('gridviz-legend-graphic', true)
|
|
129
|
+
.attr('width', w)
|
|
130
|
+
.attr('height', height)
|
|
131
|
+
// a description for the screen readers: the diagram is a picture, made of the variables it shows
|
|
132
|
+
.attr('role', 'img')
|
|
133
|
+
.attr('aria-label', (this.title ? this.title + ': ' : '') + this.items.map((item) => item.label).join(', '))
|
|
134
|
+
.style('overflow', 'visible')
|
|
135
|
+
// the circles blend with each other only, not with what is behind the legend
|
|
136
|
+
.style('isolation', 'isolate')
|
|
137
|
+
|
|
138
|
+
//the circles
|
|
139
|
+
const circles = this.items.map((item, i) =>
|
|
140
|
+
svg
|
|
141
|
+
.append('circle')
|
|
142
|
+
.classed('gridviz-legend-swatch', true)
|
|
143
|
+
.attr('cx', cx[i])
|
|
144
|
+
.attr('cy', cy[i])
|
|
145
|
+
.attr('r', R)
|
|
146
|
+
.style('fill', item.color)
|
|
147
|
+
.style('fill-opacity', this.opacity)
|
|
148
|
+
.style('mix-blend-mode', 'multiply')
|
|
149
|
+
.style('transition', 'opacity 0.1s')
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
//the labels: on the left of the first circle, on the right of the second one, below the third one
|
|
153
|
+
const labels = this.items.map((item, i) => {
|
|
154
|
+
const anchor = i === 0 ? 'end' : i === 1 ? 'start' : 'middle'
|
|
155
|
+
const x = i === 0 ? cx[0] - R - gap : i === 1 ? cx[1] + R + gap : cx[2]
|
|
156
|
+
const y = i === 2 ? cy[2] + R + fontPx : cy[i]
|
|
157
|
+
const t = svg
|
|
158
|
+
.append('text')
|
|
159
|
+
.classed('gridviz-legend-label', true)
|
|
160
|
+
.attr('x', x)
|
|
161
|
+
.attr('y', y)
|
|
162
|
+
.attr('text-anchor', anchor)
|
|
163
|
+
.attr('dominant-baseline', i === 2 ? 'auto' : 'central')
|
|
164
|
+
.style('font-size', this.labelFontSizeCSS())
|
|
165
|
+
.style('fill', this.labelColorCSS())
|
|
166
|
+
this.setText(t, item.label)
|
|
167
|
+
return t
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (!this.interactive) return
|
|
171
|
+
|
|
172
|
+
// The parts hovered: the circles under the pointer. The others are faded, and only the cells of the colors of the hovered circles are shown
|
|
173
|
+
const hover = (/** @type {number[]} */ hit) => {
|
|
174
|
+
const key = hit.join(',')
|
|
175
|
+
if (key === this._hovered) return
|
|
176
|
+
this._hovered = key || null
|
|
177
|
+
circles.forEach((c, i) =>
|
|
178
|
+
c
|
|
179
|
+
.classed('gridviz-legend-swatch--active', hit.includes(i))
|
|
180
|
+
.style('opacity', hit.length && !hit.includes(i) ? 0.25 : 1)
|
|
181
|
+
)
|
|
182
|
+
this.highlight(hit.length ? hit.map((i) => this.items[i].color) : null)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// over the diagram: the circles which contain the pointer
|
|
186
|
+
svg.style('cursor', 'pointer')
|
|
187
|
+
.on('mousemove', (/** @type {MouseEvent} */ event) => {
|
|
188
|
+
const box = /** @type {SVGSVGElement} */ (svg.node()).getBoundingClientRect()
|
|
189
|
+
const x = event.clientX - box.left
|
|
190
|
+
const y = event.clientY - box.top
|
|
191
|
+
const hit = []
|
|
192
|
+
for (let i = 0; i < n; i++) if (Math.hypot(x - cx[i], y - cy[i]) <= R) hit.push(i)
|
|
193
|
+
// not over a circle (over a label, for example): the label takes it
|
|
194
|
+
if (hit.length || !(/** @type {Element} */ (event.target).classList.contains('gridviz-legend-label')))
|
|
195
|
+
hover(hit)
|
|
196
|
+
})
|
|
197
|
+
.on('mouseleave', () => hover([]))
|
|
198
|
+
|
|
199
|
+
// over a label: the circle of the label
|
|
200
|
+
labels.forEach((t, i) => t.style('cursor', 'pointer').on('mouseenter', () => hover([i])))
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
import { color as parseColor } from 'd3-color'
|
|
5
|
+
import { Style } from '../core/Style.js'
|
|
6
|
+
import { getBuffer, getProgram, getSharedWebGLCanvas } from '../utils/webGLUtils.js'
|
|
7
|
+
|
|
8
|
+
// One instance per circle: the position of its center relative to the view center (geographical unit), its radius (idem), and its color.
|
|
9
|
+
const FLOATS_PER_CIRCLE = 7
|
|
10
|
+
|
|
11
|
+
const VERTEX_SHADER = `#version 300 es
|
|
12
|
+
layout(location = 0) in vec2 corner;
|
|
13
|
+
layout(location = 1) in vec3 circle;
|
|
14
|
+
layout(location = 2) in vec4 col;
|
|
15
|
+
uniform vec2 canvasSize;
|
|
16
|
+
uniform float z;
|
|
17
|
+
out vec4 vCol;
|
|
18
|
+
out vec2 vLocal;
|
|
19
|
+
out float vRadius;
|
|
20
|
+
void main() {
|
|
21
|
+
float radius = circle.z / z;
|
|
22
|
+
// the quad is a pixel bigger than the circle, for its anti-aliased edge
|
|
23
|
+
vec2 local = corner * (radius + 1.0);
|
|
24
|
+
vec2 pix = circle.xy / z + local;
|
|
25
|
+
gl_Position = vec4(pix * 2.0 / canvasSize, 0.0, 1.0);
|
|
26
|
+
vCol = col;
|
|
27
|
+
vLocal = local;
|
|
28
|
+
vRadius = radius;
|
|
29
|
+
}
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
const FRAGMENT_SHADER = `#version 300 es
|
|
33
|
+
precision mediump float;
|
|
34
|
+
in vec4 vCol;
|
|
35
|
+
in vec2 vLocal;
|
|
36
|
+
in float vRadius;
|
|
37
|
+
out vec4 outColor;
|
|
38
|
+
void main() {
|
|
39
|
+
float a = clamp(vRadius + 0.5 - length(vLocal), 0.0, 1.0) * vCol.a;
|
|
40
|
+
outColor = vec4(vCol.rgb * a, a);
|
|
41
|
+
}
|
|
42
|
+
`
|
|
43
|
+
|
|
44
|
+
/** The colors already parsed, to not parse a color string for each cell. @type {Map<string, Array<number>|null>} */
|
|
45
|
+
const parsedColors = new Map()
|
|
46
|
+
/** @param {string} c @returns {Array<number>|null} r, g, b, a within [0,1] */
|
|
47
|
+
const rgba = (c) => {
|
|
48
|
+
let v = parsedColors.get(c)
|
|
49
|
+
if (v === undefined) {
|
|
50
|
+
const p = parseColor(c)
|
|
51
|
+
v = p ? [p.r / 255, p.g / 255, p.b / 255, p.opacity] : null
|
|
52
|
+
if (parsedColors.size > 10000) parsedColors.clear()
|
|
53
|
+
parsedColors.set(c, v)
|
|
54
|
+
}
|
|
55
|
+
return v
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The data of the circles, reused from draw to draw. */
|
|
59
|
+
let instances = new Float32Array(FLOATS_PER_CIRCLE * 4096)
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A style which shows grid cells as circles, with a specific color and size, drawn with WebGL.
|
|
63
|
+
*
|
|
64
|
+
* It is made for maps with many circles. The circles of the style are drawn by the GPU at once, instead of one by one,
|
|
65
|
+
* and then copied on the map in a single operation. It makes a big difference when there are many circles, and when the style
|
|
66
|
+
* has a blend operation such as "multiply", which is then done once for the style and not for each circle.
|
|
67
|
+
*
|
|
68
|
+
* As a consequence, the circles of one style do not blend with each other: they are all drawn first, then blended with what is below.
|
|
69
|
+
* It is the same as ShapeColorSizeStyle (with the 'circle' shape) in all other aspects.
|
|
70
|
+
* It needs WebGL 2. Without it, the circles are drawn one by one.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* new gridviz.CircleColorSizeWebGLStyle({
|
|
74
|
+
* color: (cell) => (cell.value > 100 ? 'red' : 'blue'),
|
|
75
|
+
* size: (cell, resolution) => resolution * 0.5,
|
|
76
|
+
* blendOperation: () => 'multiply',
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* @module style
|
|
80
|
+
* @author Joseph Davies
|
|
81
|
+
*/
|
|
82
|
+
export class CircleColorSizeWebGLStyle extends Style {
|
|
83
|
+
/**
|
|
84
|
+
* @param {object} opts The options of the style (see Style for the common ones: filter, offset, alpha, blendOperation, visible, viewScale).
|
|
85
|
+
* @param {(function(import('../core/Dataset.js').Cell, number, number, object):string) | string} [opts.color] The color of the circle of a cell: a CSS color, or a function of the cell, the resolution, the zoom level and the view scale. A cell without color is not drawn. Default: '#EA6BAC'.
|
|
86
|
+
* @param {(function(import('../core/Dataset.js').Cell, number, number, object):number) | number} [opts.size] The diameter of the circle of a cell, in geographical unit: a number, or a function of the cell, the resolution, the zoom level and the view scale. A cell without size (0) is not drawn. Default: the resolution.
|
|
87
|
+
*/
|
|
88
|
+
constructor(opts) {
|
|
89
|
+
super(opts)
|
|
90
|
+
opts = opts || {}
|
|
91
|
+
/**
|
|
92
|
+
* The color of the circle of a cell.
|
|
93
|
+
* @type {(function(import('../core/Dataset.js').Cell, number, number, object):string) | string} */
|
|
94
|
+
this.color = opts.color || '#EA6BAC'
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The diameter of the circle of a cell, in geographical unit. A cell without size (0) is not drawn.
|
|
98
|
+
* @type {(function(import('../core/Dataset.js').Cell, number, number, object):number) | number} */
|
|
99
|
+
this.size = opts.size || ((cell, resolution) => resolution)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {Array.<import("../core/Dataset.js").Cell>} cells
|
|
104
|
+
* @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
|
|
105
|
+
* @param {number} resolution
|
|
106
|
+
* @override
|
|
107
|
+
*/
|
|
108
|
+
draw(cells, geoCanvas, resolution) {
|
|
109
|
+
//filter
|
|
110
|
+
if (this.filter) cells = cells.filter(this.filter)
|
|
111
|
+
|
|
112
|
+
const z = geoCanvas.view.z
|
|
113
|
+
const viewScale = this.viewScale ? this.viewScale(cells, resolution, z) : undefined
|
|
114
|
+
const r2 = resolution * 0.5
|
|
115
|
+
|
|
116
|
+
// the legend swatch hovered, if any: only the cells of its colour are drawn
|
|
117
|
+
const highlight = this.highlightColors
|
|
118
|
+
|
|
119
|
+
const colorFunction = typeof this.color === 'function' ? this.color : null
|
|
120
|
+
const sizeFunction = typeof this.size === 'function' ? this.size : null
|
|
121
|
+
|
|
122
|
+
//the circles are positioned relative to the center of the view, to keep the precision of the 32 bits floats
|
|
123
|
+
const vx = geoCanvas.view.x
|
|
124
|
+
const vy = geoCanvas.view.y
|
|
125
|
+
|
|
126
|
+
//fill the data of the circles
|
|
127
|
+
if (instances.length < FLOATS_PER_CIRCLE * cells.length)
|
|
128
|
+
instances = new Float32Array(
|
|
129
|
+
FLOATS_PER_CIRCLE * Math.max(cells.length, 2 * (instances.length / FLOATS_PER_CIRCLE))
|
|
130
|
+
)
|
|
131
|
+
let n = 0
|
|
132
|
+
for (const c of cells) {
|
|
133
|
+
//@ts-ignore
|
|
134
|
+
const col = colorFunction ? colorFunction(c, resolution, z, viewScale) : this.color
|
|
135
|
+
if (!col || col === 'none') continue
|
|
136
|
+
if (highlight && !this.isHighlighted(col)) continue
|
|
137
|
+
|
|
138
|
+
//@ts-ignore
|
|
139
|
+
const size = sizeFunction ? sizeFunction(c, resolution, z, viewScale) : this.size
|
|
140
|
+
if (!size) continue
|
|
141
|
+
|
|
142
|
+
const v = rgba(col)
|
|
143
|
+
if (!v) continue
|
|
144
|
+
|
|
145
|
+
const offset = this.offset(c, resolution, z)
|
|
146
|
+
const i = n * FLOATS_PER_CIRCLE
|
|
147
|
+
instances[i] = c.x + r2 + offset.dx - vx
|
|
148
|
+
instances[i + 1] = c.y + r2 + offset.dy - vy
|
|
149
|
+
instances[i + 2] = size * 0.5
|
|
150
|
+
instances[i + 3] = v[0]
|
|
151
|
+
instances[i + 4] = v[1]
|
|
152
|
+
instances[i + 5] = v[2]
|
|
153
|
+
instances[i + 6] = v[3]
|
|
154
|
+
n++
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (n > 0) {
|
|
158
|
+
if (!this.drawWebGL(geoCanvas, n, z)) this.drawCanvas(geoCanvas, n)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
//update legends
|
|
162
|
+
this.updateLegends({ viewScale: viewScale, resolution: resolution, z: z, cells: cells })
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Draw the circles with webGL, on the shared canvas, and copy it on the map.
|
|
167
|
+
* @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
|
|
168
|
+
* @param {number} n
|
|
169
|
+
* @param {number} z
|
|
170
|
+
* @returns {boolean} false if webGL 2 is not available.
|
|
171
|
+
* @private
|
|
172
|
+
*/
|
|
173
|
+
drawWebGL(geoCanvas, n, z) {
|
|
174
|
+
let cv
|
|
175
|
+
try {
|
|
176
|
+
cv = getSharedWebGLCanvas(geoCanvas.w, geoCanvas.h, { version: 2 })
|
|
177
|
+
} catch (e) {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
const gl = /** @type {WebGL2RenderingContext} */ (/** @type {unknown} */ (cv.gl))
|
|
181
|
+
if (!gl.drawArraysInstanced) return false
|
|
182
|
+
|
|
183
|
+
const program = getProgram(gl, VERTEX_SHADER, FRAGMENT_SHADER)
|
|
184
|
+
gl.useProgram(program)
|
|
185
|
+
gl.uniform2f(gl.getUniformLocation(program, 'canvasSize'), geoCanvas.w, geoCanvas.h)
|
|
186
|
+
gl.uniform1f(gl.getUniformLocation(program, 'z'), z)
|
|
187
|
+
|
|
188
|
+
//the corners of the quad of a circle, the same for all circles
|
|
189
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, 'CircleColorSizeWebGLStyle.corner'))
|
|
190
|
+
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW)
|
|
191
|
+
gl.enableVertexAttribArray(0)
|
|
192
|
+
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0)
|
|
193
|
+
gl.vertexAttribDivisor(0, 0)
|
|
194
|
+
|
|
195
|
+
//the circles: one instance each
|
|
196
|
+
const stride = FLOATS_PER_CIRCLE * 4
|
|
197
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, getBuffer(gl, 'CircleColorSizeWebGLStyle.circles'))
|
|
198
|
+
gl.bufferData(gl.ARRAY_BUFFER, instances.subarray(0, n * FLOATS_PER_CIRCLE), gl.DYNAMIC_DRAW)
|
|
199
|
+
gl.enableVertexAttribArray(1)
|
|
200
|
+
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, stride, 0)
|
|
201
|
+
gl.vertexAttribDivisor(1, 1)
|
|
202
|
+
gl.enableVertexAttribArray(2)
|
|
203
|
+
gl.vertexAttribPointer(2, 4, gl.FLOAT, false, stride, 12)
|
|
204
|
+
gl.vertexAttribDivisor(2, 1)
|
|
205
|
+
|
|
206
|
+
gl.clearColor(0, 0, 0, 0)
|
|
207
|
+
gl.clear(gl.COLOR_BUFFER_BIT)
|
|
208
|
+
//colors are premultiplied by the fragment shader
|
|
209
|
+
gl.enable(gl.BLEND)
|
|
210
|
+
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
|
|
211
|
+
gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, n)
|
|
212
|
+
gl.disable(gl.BLEND)
|
|
213
|
+
|
|
214
|
+
//leave the context as found
|
|
215
|
+
gl.vertexAttribDivisor(1, 0)
|
|
216
|
+
gl.vertexAttribDivisor(2, 0)
|
|
217
|
+
gl.disableVertexAttribArray(0)
|
|
218
|
+
gl.disableVertexAttribArray(1)
|
|
219
|
+
gl.disableVertexAttribArray(2)
|
|
220
|
+
|
|
221
|
+
//copy on the map. The alpha and blend operation of the style are the ones of the canvas, set by the layer.
|
|
222
|
+
geoCanvas.initCanvasTransform()
|
|
223
|
+
geoCanvas.offscreenCtx.drawImage(cv.canvas, 0, 0)
|
|
224
|
+
return true
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Draw the circles one by one, without webGL.
|
|
229
|
+
* @param {import("../core/GeoCanvas.js").GeoCanvas} geoCanvas
|
|
230
|
+
* @param {number} n
|
|
231
|
+
* @private
|
|
232
|
+
*/
|
|
233
|
+
drawCanvas(geoCanvas, n) {
|
|
234
|
+
const ctx = geoCanvas.offscreenCtx
|
|
235
|
+
const vx = geoCanvas.view.x
|
|
236
|
+
const vy = geoCanvas.view.y
|
|
237
|
+
geoCanvas.setCanvasTransform()
|
|
238
|
+
for (let k = 0; k < n; k++) {
|
|
239
|
+
const i = k * FLOATS_PER_CIRCLE
|
|
240
|
+
ctx.fillStyle =
|
|
241
|
+
'rgba(' +
|
|
242
|
+
[255 * instances[i + 3], 255 * instances[i + 4], 255 * instances[i + 5], instances[i + 6]].join(',') +
|
|
243
|
+
')'
|
|
244
|
+
ctx.beginPath()
|
|
245
|
+
ctx.arc(instances[i] + vx, instances[i + 1] + vy, instances[i + 2], 0, 2 * Math.PI)
|
|
246
|
+
ctx.fill()
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -286,6 +286,6 @@ export class CompositionStyle extends Style {
|
|
|
286
286
|
}
|
|
287
287
|
|
|
288
288
|
//update legends
|
|
289
|
-
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
|
|
289
|
+
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale, cells: cells })
|
|
290
290
|
}
|
|
291
291
|
}
|
|
@@ -74,6 +74,10 @@ export class DotDensityStyle extends Style {
|
|
|
74
74
|
|
|
75
75
|
const r2 = resolution / 2
|
|
76
76
|
|
|
77
|
+
//the positions are relative to the center of the view, see GeoCanvas.getWebGLTransformCentered
|
|
78
|
+
const vx = geoCanvas.view.x
|
|
79
|
+
const vy = geoCanvas.view.y
|
|
80
|
+
|
|
77
81
|
for (let cell of cells) {
|
|
78
82
|
//get color
|
|
79
83
|
const col = this.color(cell, resolution, z, viewScale)
|
|
@@ -86,8 +90,8 @@ export class DotDensityStyle extends Style {
|
|
|
86
90
|
const offset = this.offset(cell, resolution, z)
|
|
87
91
|
|
|
88
92
|
//cell center
|
|
89
|
-
const cx = cell.x + offset.dx + r2
|
|
90
|
-
const cy = cell.y + offset.dy + r2
|
|
93
|
+
const cx = cell.x + offset.dx + r2 - vx
|
|
94
|
+
const cy = cell.y + offset.dy + r2 - vy
|
|
91
95
|
|
|
92
96
|
//convert color
|
|
93
97
|
const cc = color(col)
|
|
@@ -99,7 +103,7 @@ export class DotDensityStyle extends Style {
|
|
|
99
103
|
}
|
|
100
104
|
|
|
101
105
|
//draw
|
|
102
|
-
prog.draw(geoCanvas.
|
|
106
|
+
prog.draw(geoCanvas.getWebGLTransformCentered())
|
|
103
107
|
|
|
104
108
|
//draw in canvas geo
|
|
105
109
|
geoCanvas.initCanvasTransform()
|
|
@@ -128,6 +132,6 @@ export class DotDensityStyle extends Style {
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
//update legends
|
|
131
|
-
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
|
|
135
|
+
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale, cells: cells })
|
|
132
136
|
}
|
|
133
137
|
}
|
package/src/style/ImageStyle.js
CHANGED
|
@@ -95,6 +95,6 @@ export class ImageStyle extends Style {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
//update legends
|
|
98
|
-
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale })
|
|
98
|
+
this.updateLegends({ style: this, resolution: resolution, z: z, viewScale: viewScale, cells: cells })
|
|
99
99
|
}
|
|
100
100
|
}
|