gridviz 2.0.37 → 2.0.39

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 (58) hide show
  1. package/README.md +119 -119
  2. package/dist/gridviz.js +33256 -33296
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +78 -78
  5. package/src/App.js +698 -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 +103 -103
  10. package/src/GeoCanvas.js +419 -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 -166
  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/ParquetGrid.js +122 -122
  22. package/src/dataset/TiledGrid.js +296 -296
  23. package/src/index.js +177 -177
  24. package/src/legend/ColorCategoryLegend.js +124 -124
  25. package/src/legend/ColorDiscreteLegend.js +105 -105
  26. package/src/legend/ColorLegend.js +125 -125
  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/JoyPlotStyle.js +135 -135
  34. package/src/style/KernelSmoothingOldStyle.js +221 -221
  35. package/src/style/KernelSmoothingStyle.js +115 -115
  36. package/src/style/KernelSmoothingWGLStyle.js +130 -130
  37. package/src/style/LegoStyle.js +112 -112
  38. package/src/style/MosaicStyle.js +107 -107
  39. package/src/style/NinjaStarStyle.js +124 -124
  40. package/src/style/PillarStyle.js +230 -230
  41. package/src/style/SegmentStyle.js +142 -142
  42. package/src/style/ShapeColorSizeStyle.js +126 -126
  43. package/src/style/SideCatStyle.js +172 -172
  44. package/src/style/SideStyle.js +218 -218
  45. package/src/style/SquareColorCatWGLStyle.js +120 -120
  46. package/src/style/SquareColorWGLStyle.js +146 -146
  47. package/src/style/StrokeStyle.js +131 -131
  48. package/src/style/TanakaStyle.js +160 -160
  49. package/src/style/TextStyle.js +132 -132
  50. package/src/utils/Utils.js +35 -35
  51. package/src/utils/WebGLRectangleColoring.js +106 -106
  52. package/src/utils/WebGLSquareColoring.js +118 -118
  53. package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
  54. package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
  55. package/src/utils/WebGLSquareColoringKS.js +153 -153
  56. package/src/utils/stretching.js +131 -131
  57. package/src/utils/webGLUtils.js +71 -71
  58. package/src/utils/zoomUtils.js +142 -142
package/src/GeoCanvas.js CHANGED
@@ -1,419 +1,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.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.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
+ }