gridviz 2.1.9 → 2.1.11
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 +125 -125
- package/dist/gridviz.js +8036 -8024
- package/dist/gridviz.min.js +1 -1
- package/package.json +77 -77
- package/src/App.js +705 -705
- package/src/BackgroundLayer.js +182 -182
- package/src/BackgroundLayerWMS.js +131 -131
- package/src/Dataset.js +76 -76
- package/src/DatasetComponent.js +113 -101
- package/src/GeoCanvas.js +420 -420
- package/src/LabelLayer.js +190 -190
- package/src/Layer.js +103 -103
- package/src/Legend.js +86 -86
- package/src/LineLayer.js +150 -150
- package/src/Style.js +129 -129
- package/src/Tooltip.js +180 -180
- package/src/dataset/COGeo.js +24 -24
- package/src/dataset/CSVGrid.js +104 -104
- package/src/dataset/GeoTIFF.js +127 -127
- package/src/dataset/GridTile.js +41 -41
- package/src/dataset/LGrid.js +54 -54
- package/src/dataset/TiledGrid.js +264 -264
- package/src/index.js +71 -71
- package/src/legend/ColorCategoryLegend.js +124 -124
- package/src/legend/ColorDiscreteLegend.js +103 -103
- package/src/legend/ColorLegend.js +124 -124
- package/src/legend/SegmentOrientationLegend.js +87 -87
- package/src/legend/SegmentWidthLegend.js +97 -97
- package/src/legend/SizeLegend.js +141 -141
- package/src/style/CompositionStyle.js +300 -300
- package/src/style/ContourStyle.js +54 -54
- package/src/style/DotDensityStyle.js +156 -156
- package/src/style/IsoFenceStyle.js +262 -262
- package/src/style/JoyPlotStyle.js +135 -135
- package/src/style/LegoStyle.js +112 -112
- package/src/style/MosaicStyle.js +107 -107
- package/src/style/NinjaStarStyle.js +125 -125
- package/src/style/PillarStyle.js +230 -230
- package/src/style/SegmentStyle.js +142 -142
- package/src/style/ShapeColorSizeStyle.js +126 -126
- package/src/style/SideCatStyle.js +172 -172
- package/src/style/SideStyle.js +216 -216
- package/src/style/SquareColorCatWGLStyle.js +120 -120
- package/src/style/SquareColorWGLStyle.js +146 -146
- package/src/style/StrokeStyle.js +131 -131
- package/src/style/TanakaStyle.js +160 -160
- package/src/style/TextStyle.js +132 -132
- package/src/style/TimeSeriesStyle.js +250 -250
- package/src/utils/Utils.js +35 -35
- package/src/utils/WebGLRectangleColoring.js +106 -106
- package/src/utils/WebGLSquareColoring.js +118 -118
- package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
- package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
- package/src/utils/WebGLSquareColoringKS.js +153 -153
- package/src/utils/stretching.js +131 -131
- package/src/utils/webGLUtils.js +71 -71
- package/src/utils/zoomUtils.js +142 -142
package/src/GeoCanvas.js
CHANGED
|
@@ -1,420 +1,420 @@
|
|
|
1
|
-
//@ts-check
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
/** @typedef { {xMin: number, xMax: number, yMin: number, yMax: number} } Envelope */
|
|
5
|
-
|
|
6
|
-
import { select } from 'd3-selection'
|
|
7
|
-
import { zoom, zoomIdentity } from 'd3-zoom'
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A HTML canvas for geo data display, enhanced with zoom and pan capabilities.
|
|
11
|
-
*
|
|
12
|
-
* @author Joseph Davies, Julien Gaffuri
|
|
13
|
-
*/
|
|
14
|
-
export class GeoCanvas {
|
|
15
|
-
/**
|
|
16
|
-
* @constructor
|
|
17
|
-
* @param {HTMLCanvasElement} canvas
|
|
18
|
-
* @param {object} center Geographical coordinates of the center
|
|
19
|
-
* @param {number} zf The zoom factor (pixel size, in ground m)
|
|
20
|
-
* @param {object} opts
|
|
21
|
-
*/
|
|
22
|
-
constructor(canvas, center, zf, opts) {
|
|
23
|
-
this.opts = opts || {}
|
|
24
|
-
|
|
25
|
-
/** @type {HTMLCanvasElement} */
|
|
26
|
-
this.canvas = canvas
|
|
27
|
-
|
|
28
|
-
/** @type {number} */
|
|
29
|
-
this.w = this.canvas.offsetWidth
|
|
30
|
-
/** @type {number} */
|
|
31
|
-
this.h = this.canvas.offsetHeight
|
|
32
|
-
|
|
33
|
-
this.canvas.width = this.w
|
|
34
|
-
this.canvas.height = this.h
|
|
35
|
-
|
|
36
|
-
const ctx = this.canvas.getContext('2d')
|
|
37
|
-
if (!ctx) throw 'Impossible to create canvas 2D context'
|
|
38
|
-
/**@type {CanvasRenderingContext2D} */
|
|
39
|
-
this.ctx = ctx
|
|
40
|
-
|
|
41
|
-
// set geo coordinates of the center
|
|
42
|
-
this.center = center || { x: this.w * 0.5, y: this.h * 0.5 }
|
|
43
|
-
|
|
44
|
-
// zoom factor: pixel size, in m/pix
|
|
45
|
-
/** @type {number} */
|
|
46
|
-
this.zf = zf
|
|
47
|
-
|
|
48
|
-
/** Background color.
|
|
49
|
-
* @type {string} */
|
|
50
|
-
this.backgroundColor = opts.backgroundColor || 'white'
|
|
51
|
-
|
|
52
|
-
/** @type {function():void} */
|
|
53
|
-
this.onZoomStartFun = opts.onZoomStartFun
|
|
54
|
-
|
|
55
|
-
/** @type {function():void} */
|
|
56
|
-
this.onZoomEndFun = opts.onZoomEndFun
|
|
57
|
-
|
|
58
|
-
/** @type {function():void} */
|
|
59
|
-
this.onZoomFun = opts.onZoomFun
|
|
60
|
-
|
|
61
|
-
//current extent
|
|
62
|
-
/** @type {Envelope} */
|
|
63
|
-
this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
|
|
64
|
-
this.updateExtentGeo()
|
|
65
|
-
|
|
66
|
-
//rely on d3 zoom for pan/zoom
|
|
67
|
-
if (!opts.disableZoom) {
|
|
68
|
-
let tP = zoomIdentity
|
|
69
|
-
const z = zoom()
|
|
70
|
-
//to make the zooming a bit faster
|
|
71
|
-
.wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
|
|
72
|
-
.on('zoom', (e) => {
|
|
73
|
-
const t = e.transform
|
|
74
|
-
const f = tP.k / t.k
|
|
75
|
-
if (f == 1) {
|
|
76
|
-
//pan
|
|
77
|
-
const dx = tP.x - t.x
|
|
78
|
-
const dy = tP.y - t.y
|
|
79
|
-
this.pan(dx * this.getZf(), -dy * this.getZf())
|
|
80
|
-
} else {
|
|
81
|
-
const se = e.sourceEvent
|
|
82
|
-
if (se instanceof WheelEvent) {
|
|
83
|
-
//zoom at the mouse position
|
|
84
|
-
this.zoom(
|
|
85
|
-
f,
|
|
86
|
-
this.pixToGeoX(e.sourceEvent.offsetX),
|
|
87
|
-
this.pixToGeoY(e.sourceEvent.offsetY)
|
|
88
|
-
)
|
|
89
|
-
} else if (se instanceof TouchEvent) {
|
|
90
|
-
//compute average position of the touches
|
|
91
|
-
let tx = 0,
|
|
92
|
-
ty = 0
|
|
93
|
-
for (let tt of se.targetTouches) {
|
|
94
|
-
tx += tt.clientX
|
|
95
|
-
ty += tt.clientY
|
|
96
|
-
}
|
|
97
|
-
tx /= se.targetTouches.length
|
|
98
|
-
ty /= se.targetTouches.length
|
|
99
|
-
//zoom at this average position
|
|
100
|
-
this.zoom(f, this.pixToGeoX(tx), this.pixToGeoY(ty))
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
tP = t
|
|
104
|
-
|
|
105
|
-
if (this.onZoomFun) this.onZoomFun(e)
|
|
106
|
-
})
|
|
107
|
-
.on('start', (e) => {
|
|
108
|
-
this.canvasSave.c = document.createElement('canvas')
|
|
109
|
-
this.canvasSave.c.setAttribute('width', '' + this.w)
|
|
110
|
-
this.canvasSave.c.setAttribute('height', '' + this.h)
|
|
111
|
-
this.canvasSave.c.getContext('2d').drawImage(this.canvas, 0, 0)
|
|
112
|
-
this.canvasSave.dx = 0
|
|
113
|
-
this.canvasSave.dy = 0
|
|
114
|
-
this.canvasSave.f = 1
|
|
115
|
-
|
|
116
|
-
if (this.onZoomStartFun) this.onZoomStartFun(e)
|
|
117
|
-
})
|
|
118
|
-
.on('end', (e) => {
|
|
119
|
-
this.redraw(true)
|
|
120
|
-
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
121
|
-
|
|
122
|
-
if (this.onZoomEndFun) this.onZoomEndFun(e)
|
|
123
|
-
})
|
|
124
|
-
z(select(this.canvas))
|
|
125
|
-
}
|
|
126
|
-
//select(this.canvas).call(z);
|
|
127
|
-
|
|
128
|
-
/** Zoom extent, to limit zoom in and out
|
|
129
|
-
* @type {Array.<number>} */
|
|
130
|
-
this.zfExtent = [0, Infinity]
|
|
131
|
-
|
|
132
|
-
/** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
|
|
133
|
-
* @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
|
|
134
|
-
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** @param {{x:number,y:number}} v Geographical coordinates of the center */
|
|
138
|
-
setCenter(v) {
|
|
139
|
-
this.center = v
|
|
140
|
-
}
|
|
141
|
-
/** @returns {{x:number,y:number}} Geographical coordinates of the center */
|
|
142
|
-
getCenter() {
|
|
143
|
-
return this.center
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/** @param {number} v The zoom factor (pixel size, in ground m) */
|
|
147
|
-
setZf(v) {
|
|
148
|
-
this.zf = v
|
|
149
|
-
if (this.slider) this.slider.attr('value', +this.zf)
|
|
150
|
-
}
|
|
151
|
-
/** @returns {number} The zoom factor (pixel size, in ground m) */
|
|
152
|
-
getZf() {
|
|
153
|
-
return this.zf
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** @param {Array.<number>} v */
|
|
157
|
-
setZfExtent(v) {
|
|
158
|
-
this.zfExtent = v
|
|
159
|
-
}
|
|
160
|
-
/** @returns {Array.<number>} */
|
|
161
|
-
getZfExtent() {
|
|
162
|
-
return this.zfExtent
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/** Initialise canvas transform with identity transformation. */
|
|
166
|
-
initCanvasTransform() {
|
|
167
|
-
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Initialise canvas transform with geo to screen transformation, so that geo objects can be drawn directly in geo coordinates. */
|
|
171
|
-
setCanvasTransform() {
|
|
172
|
-
const k = 1 / this.getZf()
|
|
173
|
-
const tx = -this.center.x / this.getZf() + this.w * 0.5
|
|
174
|
-
const ty = this.center.y / this.getZf() + this.h * 0.5
|
|
175
|
-
this.ctx.setTransform(k, 0, 0, -k, tx, ty)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/** Get the transformation matrix to webGL screen coordinates, within [-1,1]*[-1,1] */
|
|
179
|
-
getWebGLTransform() {
|
|
180
|
-
const kx = 2.0 / (this.w * this.getZf())
|
|
181
|
-
const ky = 2.0 / (this.h * this.getZf())
|
|
182
|
-
return [kx, 0.0, 0.0, 0.0, ky, 0.0, -kx * this.center.x, -ky * this.center.y, 1.0]
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** The function specifying how to draw the map.
|
|
186
|
-
* @param {boolean} strong */
|
|
187
|
-
redraw(strong = true) {
|
|
188
|
-
throw new Error('Method redraw not implemented.')
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Clear. To be used before a redraw for example.
|
|
193
|
-
* @param {string} color
|
|
194
|
-
*/
|
|
195
|
-
clear(color = 'white') {
|
|
196
|
-
if (this.opts.transparentBackground) {
|
|
197
|
-
this.ctx.clearRect(0, 0, this.w, this.h)
|
|
198
|
-
} else {
|
|
199
|
-
if (this.ctx) this.ctx.fillStyle = color
|
|
200
|
-
this.ctx.fillRect(0, 0, this.w, this.h)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* @param {number} dxGeo
|
|
206
|
-
* @param {number} dyGeo
|
|
207
|
-
*/
|
|
208
|
-
pan(dxGeo = 0, dyGeo = 0) {
|
|
209
|
-
//TODO force extend to remain
|
|
210
|
-
this.center.x += dxGeo
|
|
211
|
-
this.center.y += dyGeo
|
|
212
|
-
this.updateExtentGeo()
|
|
213
|
-
|
|
214
|
-
if (this.canvasSave.c) {
|
|
215
|
-
this.canvasSave.dx -= dxGeo / this.getZf()
|
|
216
|
-
this.canvasSave.dy += dyGeo / this.getZf()
|
|
217
|
-
this.clear(this.backgroundColor)
|
|
218
|
-
// this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
|
|
219
|
-
this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Zoom.
|
|
225
|
-
* @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
|
|
226
|
-
* @param {number} xGeo The x geo position fixed in the screen.
|
|
227
|
-
* @param {number} yGeo The y geo position fixed in the screen.
|
|
228
|
-
*/
|
|
229
|
-
zoom(f = 1, xGeo = this.center.x, yGeo = this.center.y) {
|
|
230
|
-
//TODO force geo extend to remain
|
|
231
|
-
|
|
232
|
-
//trying to zoom in/out beyond limit
|
|
233
|
-
if (this.zfExtent[0] == this.getZf() && f <= 1) return
|
|
234
|
-
if (this.zfExtent[1] == this.getZf() && f >= 1) return
|
|
235
|
-
|
|
236
|
-
//ensure zoom extent preserved
|
|
237
|
-
const newZf = f * this.getZf()
|
|
238
|
-
if (newZf < this.zfExtent[0]) f = this.zfExtent[0] / this.getZf()
|
|
239
|
-
if (newZf > this.zfExtent[1]) f = this.zfExtent[1] / this.getZf()
|
|
240
|
-
|
|
241
|
-
this.setZf(f * this.getZf())
|
|
242
|
-
const dxGeo = (xGeo - this.center.x) * (1 - f)
|
|
243
|
-
this.center.x += dxGeo
|
|
244
|
-
const dyGeo = (yGeo - this.center.y) * (1 - f)
|
|
245
|
-
this.center.y += dyGeo
|
|
246
|
-
this.updateExtentGeo()
|
|
247
|
-
|
|
248
|
-
//TODO
|
|
249
|
-
//this.redraw(false)
|
|
250
|
-
if (this.canvasSave.c) {
|
|
251
|
-
this.clear(this.backgroundColor)
|
|
252
|
-
this.canvasSave.f /= f
|
|
253
|
-
this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
|
|
254
|
-
this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
|
|
255
|
-
this.clear(this.backgroundColor)
|
|
256
|
-
this.ctx.drawImage(
|
|
257
|
-
this.canvasSave.c,
|
|
258
|
-
this.canvasSave.dx,
|
|
259
|
-
this.canvasSave.dy,
|
|
260
|
-
this.canvasSave.f * this.canvasSave.c.width,
|
|
261
|
-
this.canvasSave.f * this.canvasSave.c.height
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* @param {number} marginPx
|
|
268
|
-
* @returns {Envelope} The envelope of the view, in geo coordinates.
|
|
269
|
-
*/
|
|
270
|
-
updateExtentGeo(marginPx = 20) {
|
|
271
|
-
this.extGeo = {
|
|
272
|
-
xMin: this.pixToGeoX(-marginPx),
|
|
273
|
-
xMax: this.pixToGeoX(this.w + marginPx),
|
|
274
|
-
yMin: this.pixToGeoY(this.h + marginPx),
|
|
275
|
-
yMax: this.pixToGeoY(-marginPx),
|
|
276
|
-
}
|
|
277
|
-
return this.extGeo
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Check if the object has to be drawn
|
|
282
|
-
*
|
|
283
|
-
* @param {{x:number,y:number}} obj
|
|
284
|
-
*/
|
|
285
|
-
toDraw(obj) {
|
|
286
|
-
if (obj.x < this.extGeo.xMin) return false
|
|
287
|
-
if (obj.x > this.extGeo.xMax) return false
|
|
288
|
-
if (obj.y < this.extGeo.yMin) return false
|
|
289
|
-
if (obj.y > this.extGeo.yMax) return false
|
|
290
|
-
return true
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
//conversion functions
|
|
294
|
-
/**
|
|
295
|
-
* @param {number} xGeo Geo x coordinate, in m.
|
|
296
|
-
* @returns {number} Screen x coordinate, in pix.
|
|
297
|
-
*/
|
|
298
|
-
geoToPixX(xGeo) {
|
|
299
|
-
return (xGeo - this.center.x) / this.getZf() + this.w * 0.5
|
|
300
|
-
}
|
|
301
|
-
/**
|
|
302
|
-
* @param {number} yGeo Geo y coordinate, in m.
|
|
303
|
-
* @returns {number} Screen y coordinate, in pix.
|
|
304
|
-
*/
|
|
305
|
-
geoToPixY(yGeo) {
|
|
306
|
-
return -(yGeo - this.center.y) / this.getZf() + this.h * 0.5
|
|
307
|
-
}
|
|
308
|
-
/**
|
|
309
|
-
* @param {number} x Screen x coordinate, in pix.
|
|
310
|
-
* @returns {number} Geo x coordinate, in m.
|
|
311
|
-
*/
|
|
312
|
-
pixToGeoX(x) {
|
|
313
|
-
return (x - this.w * 0.5) * this.getZf() + this.center.x
|
|
314
|
-
}
|
|
315
|
-
/**
|
|
316
|
-
* @param {number} y Screen y coordinate, in pix.
|
|
317
|
-
* @returns {number} Geo y coordinate, in m.
|
|
318
|
-
*/
|
|
319
|
-
pixToGeoY(y) {
|
|
320
|
-
return -(y - this.h * 0.5) * this.getZf() + this.center.y
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** Get x,y,z elements from URL and assign them to the view center and zoom level. */
|
|
324
|
-
setViewFromURL() {
|
|
325
|
-
const x = GeoCanvas.getParameterByName('x'),
|
|
326
|
-
y = GeoCanvas.getParameterByName('y'),
|
|
327
|
-
z = GeoCanvas.getParameterByName('z')
|
|
328
|
-
const c = this.getCenter()
|
|
329
|
-
if (x != null && x != undefined && !isNaN(+x)) c.x = +x
|
|
330
|
-
if (y != null && y != undefined && !isNaN(+y)) c.y = +y
|
|
331
|
-
if (z != null && z != undefined && !isNaN(+z)) this.setZf(+z)
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
*
|
|
336
|
-
* @param {string} id
|
|
337
|
-
* @param {object} opts
|
|
338
|
-
* @returns {this}
|
|
339
|
-
*/
|
|
340
|
-
addZoomSlider(id, opts) {
|
|
341
|
-
opts = opts || {}
|
|
342
|
-
opts.width = opts.width || '30px'
|
|
343
|
-
opts.height = opts.height || '300px'
|
|
344
|
-
|
|
345
|
-
//the div element
|
|
346
|
-
const div = select('#' + id)
|
|
347
|
-
if (div.empty()) {
|
|
348
|
-
console.error('Could not find div element to build zoom slider. Id: ' + id)
|
|
349
|
-
return this
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const th = this
|
|
353
|
-
/** */
|
|
354
|
-
this.slider = div
|
|
355
|
-
.append('input')
|
|
356
|
-
.attr('type', 'range')
|
|
357
|
-
.attr('min', this.getZfExtent()[0])
|
|
358
|
-
.attr('max', this.getZfExtent()[1])
|
|
359
|
-
.attr('value', this.getZf())
|
|
360
|
-
.on('input', function (d) {
|
|
361
|
-
if (!this || !this.value) return
|
|
362
|
-
const v = +this.value
|
|
363
|
-
select(this).attr('value', v)
|
|
364
|
-
th.setZf(v)
|
|
365
|
-
//redraw
|
|
366
|
-
th.redraw()
|
|
367
|
-
})
|
|
368
|
-
.style('width', opts.width)
|
|
369
|
-
.style('height', opts.height)
|
|
370
|
-
.style('opacity', 0.7)
|
|
371
|
-
.on('mouseover', function (d) {
|
|
372
|
-
select(this).style('opacity', 1)
|
|
373
|
-
})
|
|
374
|
-
.on('mouseout', function (d) {
|
|
375
|
-
select(this).style('opacity', 0.7)
|
|
376
|
-
})
|
|
377
|
-
.style('-webkit-appearance', 'slider-vertical') //for chrome
|
|
378
|
-
.style('writing-mode', 'bt-lr') //for IE/edge
|
|
379
|
-
.attr('orient', 'vertical') //for firefox
|
|
380
|
-
.style('background', 'lightgray')
|
|
381
|
-
.style('outline', 'none')
|
|
382
|
-
.style('-webkit-transition', '.2s')
|
|
383
|
-
.style('transition', 'opacity .2s')
|
|
384
|
-
|
|
385
|
-
//TODO
|
|
386
|
-
/*select(".slider::-webkit-slider-thumb")
|
|
387
|
-
.style("-webkit-appearance", "none")
|
|
388
|
-
.style("appearance", "none")
|
|
389
|
-
.style("width", "30px")
|
|
390
|
-
.style("height", "40px")
|
|
391
|
-
.style("background", "black")
|
|
392
|
-
.style("cursor", "pointer")*/
|
|
393
|
-
|
|
394
|
-
/*select("slider::-moz-range-thumb")
|
|
395
|
-
.style("-webkit-appearance", "none")
|
|
396
|
-
.style("width", "30px")
|
|
397
|
-
.style("height", "40px")
|
|
398
|
-
.style("background", "#04AA6D")
|
|
399
|
-
.style("cursor", "pointer")*/
|
|
400
|
-
/*
|
|
401
|
-
.slider::ms-thumb,
|
|
402
|
-
.slider::-moz-range-thumb {
|
|
403
|
-
}*/
|
|
404
|
-
|
|
405
|
-
return this
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Get a URL parameter by name.
|
|
410
|
-
*
|
|
411
|
-
* @param {string} name
|
|
412
|
-
* @returns {string | null}
|
|
413
|
-
*/
|
|
414
|
-
static getParameterByName(name) {
|
|
415
|
-
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
|
|
416
|
-
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
|
|
417
|
-
results = regex.exec(location.search)
|
|
418
|
-
return !results ? null : decodeURIComponent(results[1].replace(/\+/g, ' '))
|
|
419
|
-
}
|
|
420
|
-
}
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/** @typedef { {xMin: number, xMax: number, yMin: number, yMax: number} } Envelope */
|
|
5
|
+
|
|
6
|
+
import { select } from 'd3-selection'
|
|
7
|
+
import { zoom, zoomIdentity } from 'd3-zoom'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A HTML canvas for geo data display, enhanced with zoom and pan capabilities.
|
|
11
|
+
*
|
|
12
|
+
* @author Joseph Davies, Julien Gaffuri
|
|
13
|
+
*/
|
|
14
|
+
export class GeoCanvas {
|
|
15
|
+
/**
|
|
16
|
+
* @constructor
|
|
17
|
+
* @param {HTMLCanvasElement} canvas
|
|
18
|
+
* @param {object} center Geographical coordinates of the center
|
|
19
|
+
* @param {number} zf The zoom factor (pixel size, in ground m)
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
*/
|
|
22
|
+
constructor(canvas, center, zf, opts) {
|
|
23
|
+
this.opts = opts || {}
|
|
24
|
+
|
|
25
|
+
/** @type {HTMLCanvasElement} */
|
|
26
|
+
this.canvas = canvas
|
|
27
|
+
|
|
28
|
+
/** @type {number} */
|
|
29
|
+
this.w = this.canvas.offsetWidth
|
|
30
|
+
/** @type {number} */
|
|
31
|
+
this.h = this.canvas.offsetHeight
|
|
32
|
+
|
|
33
|
+
this.canvas.width = this.w
|
|
34
|
+
this.canvas.height = this.h
|
|
35
|
+
|
|
36
|
+
const ctx = this.canvas.getContext('2d')
|
|
37
|
+
if (!ctx) throw 'Impossible to create canvas 2D context'
|
|
38
|
+
/**@type {CanvasRenderingContext2D} */
|
|
39
|
+
this.ctx = ctx
|
|
40
|
+
|
|
41
|
+
// set geo coordinates of the center
|
|
42
|
+
this.center = center || { x: this.w * 0.5, y: this.h * 0.5 }
|
|
43
|
+
|
|
44
|
+
// zoom factor: pixel size, in m/pix
|
|
45
|
+
/** @type {number} */
|
|
46
|
+
this.zf = zf
|
|
47
|
+
|
|
48
|
+
/** Background color.
|
|
49
|
+
* @type {string} */
|
|
50
|
+
this.backgroundColor = opts.backgroundColor || 'white'
|
|
51
|
+
|
|
52
|
+
/** @type {function():void} */
|
|
53
|
+
this.onZoomStartFun = opts.onZoomStartFun
|
|
54
|
+
|
|
55
|
+
/** @type {function():void} */
|
|
56
|
+
this.onZoomEndFun = opts.onZoomEndFun
|
|
57
|
+
|
|
58
|
+
/** @type {function():void} */
|
|
59
|
+
this.onZoomFun = opts.onZoomFun
|
|
60
|
+
|
|
61
|
+
//current extent
|
|
62
|
+
/** @type {Envelope} */
|
|
63
|
+
this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
|
|
64
|
+
this.updateExtentGeo()
|
|
65
|
+
|
|
66
|
+
//rely on d3 zoom for pan/zoom
|
|
67
|
+
if (!opts.disableZoom) {
|
|
68
|
+
let tP = zoomIdentity
|
|
69
|
+
const z = zoom()
|
|
70
|
+
//to make the zooming a bit faster
|
|
71
|
+
.wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
|
|
72
|
+
.on('zoom', (e) => {
|
|
73
|
+
const t = e.transform
|
|
74
|
+
const f = tP.k / t.k
|
|
75
|
+
if (f == 1) {
|
|
76
|
+
//pan
|
|
77
|
+
const dx = tP.x - t.x
|
|
78
|
+
const dy = tP.y - t.y
|
|
79
|
+
this.pan(dx * this.getZf(), -dy * this.getZf())
|
|
80
|
+
} else {
|
|
81
|
+
const se = e.sourceEvent
|
|
82
|
+
if (se instanceof WheelEvent) {
|
|
83
|
+
//zoom at the mouse position
|
|
84
|
+
this.zoom(
|
|
85
|
+
f,
|
|
86
|
+
this.pixToGeoX(e.sourceEvent.offsetX),
|
|
87
|
+
this.pixToGeoY(e.sourceEvent.offsetY)
|
|
88
|
+
)
|
|
89
|
+
} else if (se instanceof TouchEvent) {
|
|
90
|
+
//compute average position of the touches
|
|
91
|
+
let tx = 0,
|
|
92
|
+
ty = 0
|
|
93
|
+
for (let tt of se.targetTouches) {
|
|
94
|
+
tx += tt.clientX
|
|
95
|
+
ty += tt.clientY
|
|
96
|
+
}
|
|
97
|
+
tx /= se.targetTouches.length
|
|
98
|
+
ty /= se.targetTouches.length
|
|
99
|
+
//zoom at this average position
|
|
100
|
+
this.zoom(f, this.pixToGeoX(tx), this.pixToGeoY(ty))
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
tP = t
|
|
104
|
+
|
|
105
|
+
if (this.onZoomFun) this.onZoomFun(e)
|
|
106
|
+
})
|
|
107
|
+
.on('start', (e) => {
|
|
108
|
+
this.canvasSave.c = document.createElement('canvas')
|
|
109
|
+
this.canvasSave.c.setAttribute('width', '' + this.w)
|
|
110
|
+
this.canvasSave.c.setAttribute('height', '' + this.h)
|
|
111
|
+
this.canvasSave.c.getContext('2d').drawImage(this.canvas, 0, 0)
|
|
112
|
+
this.canvasSave.dx = 0
|
|
113
|
+
this.canvasSave.dy = 0
|
|
114
|
+
this.canvasSave.f = 1
|
|
115
|
+
|
|
116
|
+
if (this.onZoomStartFun) this.onZoomStartFun(e)
|
|
117
|
+
})
|
|
118
|
+
.on('end', (e) => {
|
|
119
|
+
this.redraw(true)
|
|
120
|
+
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
121
|
+
|
|
122
|
+
if (this.onZoomEndFun) this.onZoomEndFun(e)
|
|
123
|
+
})
|
|
124
|
+
z(select(this.canvas))
|
|
125
|
+
}
|
|
126
|
+
//select(this.canvas).call(z);
|
|
127
|
+
|
|
128
|
+
/** Zoom extent, to limit zoom in and out
|
|
129
|
+
* @type {Array.<number>} */
|
|
130
|
+
this.zfExtent = [0, Infinity]
|
|
131
|
+
|
|
132
|
+
/** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
|
|
133
|
+
* @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
|
|
134
|
+
this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {{x:number,y:number}} v Geographical coordinates of the center */
|
|
138
|
+
setCenter(v) {
|
|
139
|
+
this.center = v
|
|
140
|
+
}
|
|
141
|
+
/** @returns {{x:number,y:number}} Geographical coordinates of the center */
|
|
142
|
+
getCenter() {
|
|
143
|
+
return this.center
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** @param {number} v The zoom factor (pixel size, in ground m) */
|
|
147
|
+
setZf(v) {
|
|
148
|
+
this.zf = v
|
|
149
|
+
if (this.slider) this.slider.attr('value', +this.zf)
|
|
150
|
+
}
|
|
151
|
+
/** @returns {number} The zoom factor (pixel size, in ground m) */
|
|
152
|
+
getZf() {
|
|
153
|
+
return this.zf
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @param {Array.<number>} v */
|
|
157
|
+
setZfExtent(v) {
|
|
158
|
+
this.zfExtent = v
|
|
159
|
+
}
|
|
160
|
+
/** @returns {Array.<number>} */
|
|
161
|
+
getZfExtent() {
|
|
162
|
+
return this.zfExtent
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Initialise canvas transform with identity transformation. */
|
|
166
|
+
initCanvasTransform() {
|
|
167
|
+
this.ctx.setTransform(1, 0, 0, 1, 0, 0)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Initialise canvas transform with geo to screen transformation, so that geo objects can be drawn directly in geo coordinates. */
|
|
171
|
+
setCanvasTransform() {
|
|
172
|
+
const k = 1 / this.getZf()
|
|
173
|
+
const tx = -this.center.x / this.getZf() + this.w * 0.5
|
|
174
|
+
const ty = this.center.y / this.getZf() + this.h * 0.5
|
|
175
|
+
this.ctx.setTransform(k, 0, 0, -k, tx, ty)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Get the transformation matrix to webGL screen coordinates, within [-1,1]*[-1,1] */
|
|
179
|
+
getWebGLTransform() {
|
|
180
|
+
const kx = 2.0 / (this.w * this.getZf())
|
|
181
|
+
const ky = 2.0 / (this.h * this.getZf())
|
|
182
|
+
return [kx, 0.0, 0.0, 0.0, ky, 0.0, -kx * this.center.x, -ky * this.center.y, 1.0]
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The function specifying how to draw the map.
|
|
186
|
+
* @param {boolean} strong */
|
|
187
|
+
redraw(strong = true) {
|
|
188
|
+
throw new Error('Method redraw not implemented.')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Clear. To be used before a redraw for example.
|
|
193
|
+
* @param {string} color
|
|
194
|
+
*/
|
|
195
|
+
clear(color = 'white') {
|
|
196
|
+
if (this.opts.transparentBackground) {
|
|
197
|
+
this.ctx.clearRect(0, 0, this.w, this.h)
|
|
198
|
+
} else {
|
|
199
|
+
if (this.ctx) this.ctx.fillStyle = color
|
|
200
|
+
this.ctx.fillRect(0, 0, this.w, this.h)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {number} dxGeo
|
|
206
|
+
* @param {number} dyGeo
|
|
207
|
+
*/
|
|
208
|
+
pan(dxGeo = 0, dyGeo = 0) {
|
|
209
|
+
//TODO force extend to remain
|
|
210
|
+
this.center.x += dxGeo
|
|
211
|
+
this.center.y += dyGeo
|
|
212
|
+
this.updateExtentGeo()
|
|
213
|
+
|
|
214
|
+
if (this.canvasSave.c) {
|
|
215
|
+
this.canvasSave.dx -= dxGeo / this.getZf()
|
|
216
|
+
this.canvasSave.dy += dyGeo / this.getZf()
|
|
217
|
+
this.clear(this.backgroundColor)
|
|
218
|
+
// this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
|
|
219
|
+
this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Zoom.
|
|
225
|
+
* @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
|
|
226
|
+
* @param {number} xGeo The x geo position fixed in the screen.
|
|
227
|
+
* @param {number} yGeo The y geo position fixed in the screen.
|
|
228
|
+
*/
|
|
229
|
+
zoom(f = 1, xGeo = this.center.x, yGeo = this.center.y) {
|
|
230
|
+
//TODO force geo extend to remain
|
|
231
|
+
|
|
232
|
+
//trying to zoom in/out beyond limit
|
|
233
|
+
if (this.zfExtent[0] == this.getZf() && f <= 1) return
|
|
234
|
+
if (this.zfExtent[1] == this.getZf() && f >= 1) return
|
|
235
|
+
|
|
236
|
+
//ensure zoom extent preserved
|
|
237
|
+
const newZf = f * this.getZf()
|
|
238
|
+
if (newZf < this.zfExtent[0]) f = this.zfExtent[0] / this.getZf()
|
|
239
|
+
if (newZf > this.zfExtent[1]) f = this.zfExtent[1] / this.getZf()
|
|
240
|
+
|
|
241
|
+
this.setZf(f * this.getZf())
|
|
242
|
+
const dxGeo = (xGeo - this.center.x) * (1 - f)
|
|
243
|
+
this.center.x += dxGeo
|
|
244
|
+
const dyGeo = (yGeo - this.center.y) * (1 - f)
|
|
245
|
+
this.center.y += dyGeo
|
|
246
|
+
this.updateExtentGeo()
|
|
247
|
+
|
|
248
|
+
//TODO
|
|
249
|
+
//this.redraw(false)
|
|
250
|
+
if (this.canvasSave.c) {
|
|
251
|
+
this.clear(this.backgroundColor)
|
|
252
|
+
this.canvasSave.f /= f
|
|
253
|
+
this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
|
|
254
|
+
this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
|
|
255
|
+
this.clear(this.backgroundColor)
|
|
256
|
+
this.ctx.drawImage(
|
|
257
|
+
this.canvasSave.c,
|
|
258
|
+
this.canvasSave.dx,
|
|
259
|
+
this.canvasSave.dy,
|
|
260
|
+
this.canvasSave.f * this.canvasSave.c.width,
|
|
261
|
+
this.canvasSave.f * this.canvasSave.c.height
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* @param {number} marginPx
|
|
268
|
+
* @returns {Envelope} The envelope of the view, in geo coordinates.
|
|
269
|
+
*/
|
|
270
|
+
updateExtentGeo(marginPx = 20) {
|
|
271
|
+
this.extGeo = {
|
|
272
|
+
xMin: this.pixToGeoX(-marginPx),
|
|
273
|
+
xMax: this.pixToGeoX(this.w + marginPx),
|
|
274
|
+
yMin: this.pixToGeoY(this.h + marginPx),
|
|
275
|
+
yMax: this.pixToGeoY(-marginPx),
|
|
276
|
+
}
|
|
277
|
+
return this.extGeo
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check if the object has to be drawn
|
|
282
|
+
*
|
|
283
|
+
* @param {{x:number,y:number}} obj
|
|
284
|
+
*/
|
|
285
|
+
toDraw(obj) {
|
|
286
|
+
if (obj.x < this.extGeo.xMin) return false
|
|
287
|
+
if (obj.x > this.extGeo.xMax) return false
|
|
288
|
+
if (obj.y < this.extGeo.yMin) return false
|
|
289
|
+
if (obj.y > this.extGeo.yMax) return false
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
//conversion functions
|
|
294
|
+
/**
|
|
295
|
+
* @param {number} xGeo Geo x coordinate, in m.
|
|
296
|
+
* @returns {number} Screen x coordinate, in pix.
|
|
297
|
+
*/
|
|
298
|
+
geoToPixX(xGeo) {
|
|
299
|
+
return (xGeo - this.center.x) / this.getZf() + this.w * 0.5
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* @param {number} yGeo Geo y coordinate, in m.
|
|
303
|
+
* @returns {number} Screen y coordinate, in pix.
|
|
304
|
+
*/
|
|
305
|
+
geoToPixY(yGeo) {
|
|
306
|
+
return -(yGeo - this.center.y) / this.getZf() + this.h * 0.5
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* @param {number} x Screen x coordinate, in pix.
|
|
310
|
+
* @returns {number} Geo x coordinate, in m.
|
|
311
|
+
*/
|
|
312
|
+
pixToGeoX(x) {
|
|
313
|
+
return (x - this.w * 0.5) * this.getZf() + this.center.x
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* @param {number} y Screen y coordinate, in pix.
|
|
317
|
+
* @returns {number} Geo y coordinate, in m.
|
|
318
|
+
*/
|
|
319
|
+
pixToGeoY(y) {
|
|
320
|
+
return -(y - this.h * 0.5) * this.getZf() + this.center.y
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** Get x,y,z elements from URL and assign them to the view center and zoom level. */
|
|
324
|
+
setViewFromURL() {
|
|
325
|
+
const x = GeoCanvas.getParameterByName('x'),
|
|
326
|
+
y = GeoCanvas.getParameterByName('y'),
|
|
327
|
+
z = GeoCanvas.getParameterByName('z')
|
|
328
|
+
const c = this.getCenter()
|
|
329
|
+
if (x != null && x != undefined && !isNaN(+x)) c.x = +x
|
|
330
|
+
if (y != null && y != undefined && !isNaN(+y)) c.y = +y
|
|
331
|
+
if (z != null && z != undefined && !isNaN(+z)) this.setZf(+z)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
*
|
|
336
|
+
* @param {string} id
|
|
337
|
+
* @param {object} opts
|
|
338
|
+
* @returns {this}
|
|
339
|
+
*/
|
|
340
|
+
addZoomSlider(id, opts) {
|
|
341
|
+
opts = opts || {}
|
|
342
|
+
opts.width = opts.width || '30px'
|
|
343
|
+
opts.height = opts.height || '300px'
|
|
344
|
+
|
|
345
|
+
//the div element
|
|
346
|
+
const div = select('#' + id)
|
|
347
|
+
if (div.empty()) {
|
|
348
|
+
console.error('Could not find div element to build zoom slider. Id: ' + id)
|
|
349
|
+
return this
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const th = this
|
|
353
|
+
/** */
|
|
354
|
+
this.slider = div
|
|
355
|
+
.append('input')
|
|
356
|
+
.attr('type', 'range')
|
|
357
|
+
.attr('min', this.getZfExtent()[0])
|
|
358
|
+
.attr('max', this.getZfExtent()[1])
|
|
359
|
+
.attr('value', this.getZf())
|
|
360
|
+
.on('input', function (d) {
|
|
361
|
+
if (!this || !this.value) return
|
|
362
|
+
const v = +this.value
|
|
363
|
+
select(this).attr('value', v)
|
|
364
|
+
th.setZf(v)
|
|
365
|
+
//redraw
|
|
366
|
+
th.redraw()
|
|
367
|
+
})
|
|
368
|
+
.style('width', opts.width)
|
|
369
|
+
.style('height', opts.height)
|
|
370
|
+
.style('opacity', 0.7)
|
|
371
|
+
.on('mouseover', function (d) {
|
|
372
|
+
select(this).style('opacity', 1)
|
|
373
|
+
})
|
|
374
|
+
.on('mouseout', function (d) {
|
|
375
|
+
select(this).style('opacity', 0.7)
|
|
376
|
+
})
|
|
377
|
+
.style('-webkit-appearance', 'slider-vertical') //for chrome
|
|
378
|
+
.style('writing-mode', 'bt-lr') //for IE/edge
|
|
379
|
+
.attr('orient', 'vertical') //for firefox
|
|
380
|
+
.style('background', 'lightgray')
|
|
381
|
+
.style('outline', 'none')
|
|
382
|
+
.style('-webkit-transition', '.2s')
|
|
383
|
+
.style('transition', 'opacity .2s')
|
|
384
|
+
|
|
385
|
+
//TODO
|
|
386
|
+
/*select(".slider::-webkit-slider-thumb")
|
|
387
|
+
.style("-webkit-appearance", "none")
|
|
388
|
+
.style("appearance", "none")
|
|
389
|
+
.style("width", "30px")
|
|
390
|
+
.style("height", "40px")
|
|
391
|
+
.style("background", "black")
|
|
392
|
+
.style("cursor", "pointer")*/
|
|
393
|
+
|
|
394
|
+
/*select("slider::-moz-range-thumb")
|
|
395
|
+
.style("-webkit-appearance", "none")
|
|
396
|
+
.style("width", "30px")
|
|
397
|
+
.style("height", "40px")
|
|
398
|
+
.style("background", "#04AA6D")
|
|
399
|
+
.style("cursor", "pointer")*/
|
|
400
|
+
/*
|
|
401
|
+
.slider::ms-thumb,
|
|
402
|
+
.slider::-moz-range-thumb {
|
|
403
|
+
}*/
|
|
404
|
+
|
|
405
|
+
return this
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get a URL parameter by name.
|
|
410
|
+
*
|
|
411
|
+
* @param {string} name
|
|
412
|
+
* @returns {string | null}
|
|
413
|
+
*/
|
|
414
|
+
static getParameterByName(name) {
|
|
415
|
+
name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
|
|
416
|
+
var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
|
|
417
|
+
results = regex.exec(location.search)
|
|
418
|
+
return !results ? null : decodeURIComponent(results[1].replace(/\+/g, ' '))
|
|
419
|
+
}
|
|
420
|
+
}
|