gridviz 2.1.7 → 2.1.9

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 (57) hide show
  1. package/README.md +125 -124
  2. package/dist/gridviz.js +8021 -7991
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +77 -77
  5. package/src/App.js +705 -705
  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 +101 -101
  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 +129 -129
  16. package/src/Tooltip.js +180 -172
  17. package/src/dataset/COGeo.js +24 -24
  18. package/src/dataset/CSVGrid.js +104 -104
  19. package/src/dataset/GeoTIFF.js +127 -127
  20. package/src/dataset/GridTile.js +41 -41
  21. package/src/dataset/LGrid.js +54 -54
  22. package/src/dataset/TiledGrid.js +264 -264
  23. package/src/index.js +71 -71
  24. package/src/legend/ColorCategoryLegend.js +124 -124
  25. package/src/legend/ColorDiscreteLegend.js +103 -103
  26. package/src/legend/ColorLegend.js +124 -124
  27. package/src/legend/SegmentOrientationLegend.js +87 -87
  28. package/src/legend/SegmentWidthLegend.js +97 -97
  29. package/src/legend/SizeLegend.js +141 -141
  30. package/src/style/CompositionStyle.js +300 -300
  31. package/src/style/ContourStyle.js +54 -54
  32. package/src/style/DotDensityStyle.js +156 -156
  33. package/src/style/IsoFenceStyle.js +262 -240
  34. package/src/style/JoyPlotStyle.js +135 -135
  35. package/src/style/LegoStyle.js +112 -112
  36. package/src/style/MosaicStyle.js +107 -107
  37. package/src/style/NinjaStarStyle.js +125 -125
  38. package/src/style/PillarStyle.js +230 -230
  39. package/src/style/SegmentStyle.js +142 -142
  40. package/src/style/ShapeColorSizeStyle.js +126 -126
  41. package/src/style/SideCatStyle.js +172 -172
  42. package/src/style/SideStyle.js +216 -216
  43. package/src/style/SquareColorCatWGLStyle.js +120 -120
  44. package/src/style/SquareColorWGLStyle.js +146 -146
  45. package/src/style/StrokeStyle.js +131 -131
  46. package/src/style/TanakaStyle.js +160 -160
  47. package/src/style/TextStyle.js +132 -132
  48. package/src/style/TimeSeriesStyle.js +250 -250
  49. package/src/utils/Utils.js +35 -35
  50. package/src/utils/WebGLRectangleColoring.js +106 -106
  51. package/src/utils/WebGLSquareColoring.js +118 -118
  52. package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
  53. package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
  54. package/src/utils/WebGLSquareColoringKS.js +153 -153
  55. package/src/utils/stretching.js +131 -131
  56. package/src/utils/webGLUtils.js +71 -71
  57. 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
+ }