gridviz 2.1.13 → 2.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +124 -125
  2. package/dist/gridviz.js +8341 -8339
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +77 -77
  5. package/src/App.js +758 -758
  6. package/src/BackgroundLayer.js +182 -182
  7. package/src/BackgroundLayerWMS.js +131 -131
  8. package/src/Dataset.js +76 -76
  9. package/src/DatasetComponent.js +113 -113
  10. package/src/GeoCanvas.js +420 -420
  11. package/src/LabelLayer.js +190 -190
  12. package/src/Layer.js +103 -103
  13. package/src/Legend.js +86 -86
  14. package/src/LineLayer.js +150 -150
  15. package/src/Style.js +131 -129
  16. package/src/Tooltip.js +180 -180
  17. package/src/button/Button.js +54 -54
  18. package/src/button/FullscreenButton.js +98 -98
  19. package/src/button/ZoomButtons.js +72 -72
  20. package/src/dataset/COGeo.js +24 -24
  21. package/src/dataset/CSVGrid.js +104 -104
  22. package/src/dataset/GeoTIFF.js +127 -127
  23. package/src/dataset/GridTile.js +41 -41
  24. package/src/dataset/LGrid.js +54 -54
  25. package/src/dataset/TiledGrid.js +264 -264
  26. package/src/index.js +71 -71
  27. package/src/legend/ColorCategoryLegend.js +124 -124
  28. package/src/legend/ColorDiscreteLegend.js +103 -103
  29. package/src/legend/ColorLegend.js +128 -128
  30. package/src/legend/SegmentOrientationLegend.js +87 -87
  31. package/src/legend/SegmentWidthLegend.js +101 -101
  32. package/src/legend/SizeLegend.js +141 -141
  33. package/src/style/CompositionStyle.js +300 -300
  34. package/src/style/ContourStyle.js +54 -54
  35. package/src/style/DotDensityStyle.js +156 -156
  36. package/src/style/IsoFenceStyle.js +262 -262
  37. package/src/style/JoyPlotStyle.js +135 -135
  38. package/src/style/LegoStyle.js +112 -112
  39. package/src/style/MosaicStyle.js +107 -107
  40. package/src/style/NinjaStarStyle.js +125 -125
  41. package/src/style/PillarStyle.js +230 -230
  42. package/src/style/SegmentStyle.js +142 -142
  43. package/src/style/ShapeColorSizeStyle.js +152 -152
  44. package/src/style/SideCatStyle.js +172 -172
  45. package/src/style/SideStyle.js +216 -216
  46. package/src/style/SquareColorCatWGLStyle.js +120 -120
  47. package/src/style/SquareColorWGLStyle.js +146 -146
  48. package/src/style/StrokeStyle.js +131 -131
  49. package/src/style/TanakaStyle.js +160 -160
  50. package/src/style/TextStyle.js +132 -132
  51. package/src/style/TimeSeriesStyle.js +259 -259
  52. package/src/utils/Utils.js +35 -35
  53. package/src/utils/WebGLRectangleColoring.js +106 -106
  54. package/src/utils/WebGLSquareColoring.js +118 -118
  55. package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
  56. package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
  57. package/src/utils/WebGLSquareColoringKS.js +153 -153
  58. package/src/utils/stretching.js +131 -131
  59. package/src/utils/webGLUtils.js +71 -71
  60. 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
+ }