gridviz 2.0.40 → 2.0.42

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 (55) hide show
  1. package/README.md +119 -119
  2. package/dist/gridviz.js +15722 -33305
  3. package/dist/gridviz.min.js +2 -2
  4. package/package.json +76 -77
  5. package/src/App.js +642 -698
  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 -103
  10. package/src/GeoCanvas.js +420 -419
  11. package/src/LabelLayer.js +190 -190
  12. package/src/Layer.js +92 -92
  13. package/src/Legend.js +86 -86
  14. package/src/LineLayer.js +150 -150
  15. package/src/Style.js +114 -114
  16. package/src/Tooltip.js +168 -168
  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/TiledGrid.js +264 -296
  22. package/src/index.js +176 -174
  23. package/src/legend/ColorCategoryLegend.js +124 -124
  24. package/src/legend/ColorDiscreteLegend.js +103 -105
  25. package/src/legend/ColorLegend.js +124 -125
  26. package/src/legend/SegmentOrientationLegend.js +87 -87
  27. package/src/legend/SegmentWidthLegend.js +97 -97
  28. package/src/legend/SizeLegend.js +141 -141
  29. package/src/style/CompositionStyle.js +300 -300
  30. package/src/style/ContourStyle.js +54 -54
  31. package/src/style/DotDensityStyle.js +156 -156
  32. package/src/style/JoyPlotStyle.js +135 -135
  33. package/src/style/LegoStyle.js +112 -112
  34. package/src/style/MosaicStyle.js +107 -107
  35. package/src/style/NinjaStarStyle.js +124 -124
  36. package/src/style/PillarStyle.js +230 -230
  37. package/src/style/SegmentStyle.js +142 -142
  38. package/src/style/ShapeColorSizeStyle.js +126 -126
  39. package/src/style/SideCatStyle.js +172 -172
  40. package/src/style/SideStyle.js +218 -218
  41. package/src/style/SquareColorCatWGLStyle.js +120 -120
  42. package/src/style/SquareColorWGLStyle.js +146 -146
  43. package/src/style/StrokeStyle.js +131 -131
  44. package/src/style/TanakaStyle.js +160 -160
  45. package/src/style/TextStyle.js +132 -132
  46. package/src/utils/Utils.js +35 -35
  47. package/src/utils/WebGLRectangleColoring.js +106 -106
  48. package/src/utils/WebGLSquareColoring.js +118 -118
  49. package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
  50. package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
  51. package/src/utils/WebGLSquareColoringKS.js +153 -153
  52. package/src/utils/stretching.js +131 -131
  53. package/src/utils/webGLUtils.js +71 -71
  54. package/src/utils/zoomUtils.js +142 -142
  55. package/src/dataset/ParquetGrid.js +0 -122
package/src/GeoCanvas.js CHANGED
@@ -1,419 +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.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
219
- }
220
- }
221
-
222
- /**
223
- * Zoom.
224
- * @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
225
- * @param {number} xGeo The x geo position fixed in the screen.
226
- * @param {number} yGeo The y geo position fixed in the screen.
227
- */
228
- zoom(f = 1, xGeo = this.center.x, yGeo = this.center.y) {
229
- //TODO force geo extend to remain
230
-
231
- //trying to zoom in/out beyond limit
232
- if (this.zfExtent[0] == this.getZf() && f <= 1) return
233
- if (this.zfExtent[1] == this.getZf() && f >= 1) return
234
-
235
- //ensure zoom extent preserved
236
- const newZf = f * this.getZf()
237
- if (newZf < this.zfExtent[0]) f = this.zfExtent[0] / this.getZf()
238
- if (newZf > this.zfExtent[1]) f = this.zfExtent[1] / this.getZf()
239
-
240
- this.setZf(f * this.getZf())
241
- const dxGeo = (xGeo - this.center.x) * (1 - f)
242
- this.center.x += dxGeo
243
- const dyGeo = (yGeo - this.center.y) * (1 - f)
244
- this.center.y += dyGeo
245
- this.updateExtentGeo()
246
-
247
- //TODO
248
- //this.redraw(false)
249
- if (this.canvasSave.c) {
250
- this.clear(this.backgroundColor)
251
- this.canvasSave.f /= f
252
- this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
253
- this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
254
- this.clear(this.backgroundColor)
255
- this.ctx.drawImage(
256
- this.canvasSave.c,
257
- this.canvasSave.dx,
258
- this.canvasSave.dy,
259
- this.canvasSave.f * this.canvasSave.c.width,
260
- this.canvasSave.f * this.canvasSave.c.height
261
- )
262
- }
263
- }
264
-
265
- /**
266
- * @param {number} marginPx
267
- * @returns {Envelope} The envelope of the view, in geo coordinates.
268
- */
269
- updateExtentGeo(marginPx = 20) {
270
- this.extGeo = {
271
- xMin: this.pixToGeoX(-marginPx),
272
- xMax: this.pixToGeoX(this.w + marginPx),
273
- yMin: this.pixToGeoY(this.h + marginPx),
274
- yMax: this.pixToGeoY(-marginPx),
275
- }
276
- return this.extGeo
277
- }
278
-
279
- /**
280
- * Check if the object has to be drawn
281
- *
282
- * @param {{x:number,y:number}} obj
283
- */
284
- toDraw(obj) {
285
- if (obj.x < this.extGeo.xMin) return false
286
- if (obj.x > this.extGeo.xMax) return false
287
- if (obj.y < this.extGeo.yMin) return false
288
- if (obj.y > this.extGeo.yMax) return false
289
- return true
290
- }
291
-
292
- //conversion functions
293
- /**
294
- * @param {number} xGeo Geo x coordinate, in m.
295
- * @returns {number} Screen x coordinate, in pix.
296
- */
297
- geoToPixX(xGeo) {
298
- return (xGeo - this.center.x) / this.getZf() + this.w * 0.5
299
- }
300
- /**
301
- * @param {number} yGeo Geo y coordinate, in m.
302
- * @returns {number} Screen y coordinate, in pix.
303
- */
304
- geoToPixY(yGeo) {
305
- return -(yGeo - this.center.y) / this.getZf() + this.h * 0.5
306
- }
307
- /**
308
- * @param {number} x Screen x coordinate, in pix.
309
- * @returns {number} Geo x coordinate, in m.
310
- */
311
- pixToGeoX(x) {
312
- return (x - this.w * 0.5) * this.getZf() + this.center.x
313
- }
314
- /**
315
- * @param {number} y Screen y coordinate, in pix.
316
- * @returns {number} Geo y coordinate, in m.
317
- */
318
- pixToGeoY(y) {
319
- return -(y - this.h * 0.5) * this.getZf() + this.center.y
320
- }
321
-
322
- /** Get x,y,z elements from URL and assign them to the view center and zoom level. */
323
- setViewFromURL() {
324
- const x = GeoCanvas.getParameterByName('x'),
325
- y = GeoCanvas.getParameterByName('y'),
326
- z = GeoCanvas.getParameterByName('z')
327
- const c = this.getCenter()
328
- if (x != null && x != undefined && !isNaN(+x)) c.x = +x
329
- if (y != null && y != undefined && !isNaN(+y)) c.y = +y
330
- if (z != null && z != undefined && !isNaN(+z)) this.setZf(+z)
331
- }
332
-
333
- /**
334
- *
335
- * @param {string} id
336
- * @param {object} opts
337
- * @returns {this}
338
- */
339
- addZoomSlider(id, opts) {
340
- opts = opts || {}
341
- opts.width = opts.width || '30px'
342
- opts.height = opts.height || '300px'
343
-
344
- //the div element
345
- const div = select('#' + id)
346
- if (div.empty()) {
347
- console.error('Could not find div element to build zoom slider. Id: ' + id)
348
- return this
349
- }
350
-
351
- const th = this
352
- /** */
353
- this.slider = div
354
- .append('input')
355
- .attr('type', 'range')
356
- .attr('min', this.getZfExtent()[0])
357
- .attr('max', this.getZfExtent()[1])
358
- .attr('value', this.getZf())
359
- .on('input', function (d) {
360
- if (!this || !this.value) return
361
- const v = +this.value
362
- select(this).attr('value', v)
363
- th.setZf(v)
364
- //redraw
365
- th.redraw()
366
- })
367
- .style('width', opts.width)
368
- .style('height', opts.height)
369
- .style('opacity', 0.7)
370
- .on('mouseover', function (d) {
371
- select(this).style('opacity', 1)
372
- })
373
- .on('mouseout', function (d) {
374
- select(this).style('opacity', 0.7)
375
- })
376
- .style('-webkit-appearance', 'slider-vertical') //for chrome
377
- .style('writing-mode', 'bt-lr') //for IE/edge
378
- .attr('orient', 'vertical') //for firefox
379
- .style('background', 'lightgray')
380
- .style('outline', 'none')
381
- .style('-webkit-transition', '.2s')
382
- .style('transition', 'opacity .2s')
383
-
384
- //TODO
385
- /*select(".slider::-webkit-slider-thumb")
386
- .style("-webkit-appearance", "none")
387
- .style("appearance", "none")
388
- .style("width", "30px")
389
- .style("height", "40px")
390
- .style("background", "black")
391
- .style("cursor", "pointer")*/
392
-
393
- /*select("slider::-moz-range-thumb")
394
- .style("-webkit-appearance", "none")
395
- .style("width", "30px")
396
- .style("height", "40px")
397
- .style("background", "#04AA6D")
398
- .style("cursor", "pointer")*/
399
- /*
400
- .slider::ms-thumb,
401
- .slider::-moz-range-thumb {
402
- }*/
403
-
404
- return this
405
- }
406
-
407
- /**
408
- * Get a URL parameter by name.
409
- *
410
- * @param {string} name
411
- * @returns {string | null}
412
- */
413
- static getParameterByName(name) {
414
- name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
415
- var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
416
- results = regex.exec(location.search)
417
- return !results ? null : decodeURIComponent(results[1].replace(/\+/g, ' '))
418
- }
419
- }
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
+ }