gridviz 3.0.22 → 3.0.24

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 (66) hide show
  1. package/README.md +128 -128
  2. package/dist/gridviz.js +8618 -8579
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +74 -74
  5. package/src/button/Button.js +67 -67
  6. package/src/button/FullscreenButton.js +121 -121
  7. package/src/button/ZoomButtons.js +103 -101
  8. package/src/core/Dataset.js +154 -154
  9. package/src/core/Drawable.js +78 -78
  10. package/src/core/GeoCanvas.js +437 -430
  11. package/src/core/Layer.js +23 -23
  12. package/src/core/Legend.js +108 -108
  13. package/src/core/Map.js +542 -516
  14. package/src/core/MultiResolutionDataset.js +93 -93
  15. package/src/core/Style.js +98 -98
  16. package/src/core/Tooltip.js +182 -182
  17. package/src/dataset/COGeo.js +25 -25
  18. package/src/dataset/CSVGrid.js +112 -112
  19. package/src/dataset/GeoTIFF.js +122 -122
  20. package/src/dataset/JSGrid.js +55 -55
  21. package/src/dataset/TiledGrid.js +302 -302
  22. package/src/dataset/worker.js +18 -18
  23. package/src/index.js +77 -77
  24. package/src/layer/BackgroundLayer.js +173 -173
  25. package/src/layer/BackgroundLayerImage.js +81 -81
  26. package/src/layer/BackgroundLayerWMS.js +104 -104
  27. package/src/layer/GeoJSONLayer.js +214 -214
  28. package/src/layer/GridLayer.js +148 -148
  29. package/src/layer/LabelLayer.js +194 -194
  30. package/src/legend/ColorCategoryLegend.js +109 -109
  31. package/src/legend/ColorDiscreteLegend.js +100 -100
  32. package/src/legend/ColorLegend.js +115 -115
  33. package/src/legend/OrientationLegend.js +89 -89
  34. package/src/legend/SizeLegend.js +224 -224
  35. package/src/legend/TernaryLegend.js +302 -302
  36. package/src/style/CompositionStyle.js +291 -291
  37. package/src/style/ContourStyle.js +21 -21
  38. package/src/style/DotDensityStyle.js +133 -133
  39. package/src/style/ImageStyle.js +100 -100
  40. package/src/style/IsoFenceStyle.js +217 -217
  41. package/src/style/JoyPlotStyle.js +127 -127
  42. package/src/style/LegoStyle.js +110 -110
  43. package/src/style/MosaicStyle.js +99 -99
  44. package/src/style/NinjaStarStyle.js +102 -102
  45. package/src/style/PillarStyle.js +207 -207
  46. package/src/style/SegmentStyle.js +105 -105
  47. package/src/style/ShapeColorSizeStyle.js +147 -147
  48. package/src/style/SideCategoryStyle.js +112 -112
  49. package/src/style/SideStyle.js +212 -212
  50. package/src/style/SquareColorCategoryWebGLStyle.js +107 -107
  51. package/src/style/SquareColorWebGLStyle.js +126 -126
  52. package/src/style/StrokeStyle.js +135 -135
  53. package/src/style/TanakaStyle.js +69 -69
  54. package/src/style/TextStyle.js +110 -110
  55. package/src/style/TimeSeriesStyle.js +222 -222
  56. package/src/utils/WebGLRectangleColoring.js +106 -106
  57. package/src/utils/WebGLSquareColoring.js +118 -118
  58. package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
  59. package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
  60. package/src/utils/WebGLSquareColoringKS.js +153 -153
  61. package/src/utils/scale.js +218 -218
  62. package/src/utils/stretching.js +120 -120
  63. package/src/utils/ternary.js +208 -208
  64. package/src/utils/utils.js +58 -58
  65. package/src/utils/webGLUtils.js +72 -72
  66. package/src/utils/zoomUtils.js +142 -142
@@ -1,430 +1,437 @@
1
- //@ts-check
2
- 'use strict'
3
-
4
- /** @typedef { {xMin: number, xMax: number, yMin: number, yMax: number} } Envelope */
5
-
6
- /**
7
- * A viewshed.
8
- * @typedef {{x: number, y: number, z: number}} View */
9
-
10
- import { select } from 'd3-selection'
11
- import { zoom as d3zoom, zoomIdentity } from 'd3-zoom'
12
-
13
- /**
14
- * A HTML canvas for geo data display, enhanced with zoom and pan capabilities.
15
- *
16
- * @module core
17
- * @author Joseph Davies, Julien Gaffuri
18
- */
19
- export class GeoCanvas {
20
- /**
21
- * @constructor
22
- * @param {HTMLCanvasElement} canvas
23
- * @param {number} x The x coordinate of the view
24
- * @param {number} y The y coordinate of the view
25
- * @param {number} z The zoom level of the view (pixel size, in ground m)
26
- * @param {object} opts
27
- */
28
- constructor(canvas, x = 0, y = 0, z = 0, opts = undefined) {
29
- this.opts = opts || {}
30
-
31
- /** @type {HTMLCanvasElement} */
32
- this.canvas = canvas
33
-
34
- /** @type {number} */
35
- this.w = this.canvas.offsetWidth
36
- /** @type {number} */
37
- this.h = this.canvas.offsetHeight
38
-
39
- // Adjust canvas width and height based on device pixel ratio
40
- //const dpr = window.devicePixelRatio || 1 // Get the device pixel ratio
41
- //this.canvas.width = this.w * dpr // Set canvas width
42
- //\sthis.canvas.height = this.h * dpr // Set canvas height
43
-
44
- // Create offscreen canvas for drawing operations
45
- this.offscreenCanvas = document.createElement('canvas')
46
- this.offscreenCanvas.width = this.w
47
- this.offscreenCanvas.height = this.h
48
-
49
- const ctx = this.canvas.getContext('2d')
50
- const offscreenCtx = this.offscreenCanvas.getContext('2d')
51
- if (!ctx) throw 'Impossible to create canvas 2D context'
52
- if (!offscreenCtx) throw 'Impossible to create canvas 2D context'
53
- /**@type {CanvasRenderingContext2D} */
54
- this.ctx = ctx
55
- this.offscreenCtx = offscreenCtx
56
- //this.ctx.scale(dpr, dpr) // Scale the context
57
-
58
- /**
59
- * z: pixel size, in m/pix
60
- * @type {View} */
61
- this.view = { x: x, y: y, z: z }
62
-
63
- /** Background color.
64
- * @type {string} */
65
- this.backgroundColor = opts.backgroundColor || 'white'
66
-
67
- /** @type {function(object|undefined):void} */
68
- this.onZoomStartFun = opts.onZoomStartFun
69
-
70
- /** @type {function(object|undefined):void} */
71
- this.onZoomEndFun = opts.onZoomEndFun
72
-
73
- /** @type {function(object|undefined):void} */
74
- this.onZoomFun = opts.onZoomFun
75
-
76
- //current extent
77
- /** @type {Envelope} */
78
- this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
79
- this.updateExtentGeo()
80
-
81
- //rely on d3 for zoom
82
- if (!opts.disableZoom) {
83
- let tP = zoomIdentity
84
- // @ts-ignore
85
- let debounceTimeout = null // Add a debounce timeout variable
86
- const z = d3zoom()
87
- // to make the zooming a bit faster
88
- .wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
89
- .on('zoom', (e) => {
90
- const t = e.transform
91
- const zoomFactor = tP.k / t.k
92
- if (zoomFactor == 1) {
93
- //pan
94
- const dx = tP.x - t.x
95
- const dy = tP.y - t.y
96
- this.pan(dx * this.view.z, -dy * this.view.z)
97
- } else {
98
- handleZoom(e, zoomFactor)
99
- }
100
- tP = t
101
-
102
- if (this.onZoomFun) this.onZoomFun(e)
103
- })
104
- .on('start', (e) => {
105
- // start of zoom event
106
- // save the current canvas state to keep onscreen during pan/zoom before redrawing
107
- this.canvasSave.c = document.createElement('canvas')
108
- this.canvasSave.c.setAttribute('width', '' + this.w)
109
- this.canvasSave.c.setAttribute('height', '' + this.h)
110
- this.canvasSave.c.getContext('2d')?.drawImage(this.canvas, 0, 0)
111
- this.canvasSave.dx = 0
112
- this.canvasSave.dy = 0
113
- this.canvasSave.f = 1
114
- if (this.onZoomStartFun) this.onZoomStartFun(e)
115
- })
116
- .on('end', (e) => {
117
- // end of pan/zoom event
118
- this.redraw()
119
- this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
120
-
121
- if (this.onZoomEndFun) this.onZoomEndFun(e)
122
- })
123
- // @ts-ignore
124
- z(select(this.canvas))
125
-
126
- const handleZoom = (event, zoomFactor) => {
127
- // cancel ongoing data requests
128
- this.cancelCurrentRequests()
129
- const se = event.sourceEvent
130
-
131
- if (se instanceof WheelEvent) {
132
- //zoom at the mouse position
133
- this.zoom(
134
- zoomFactor,
135
- // @ts-ignore
136
- this.pixToGeoX(se.offsetX),
137
- // @ts-ignore
138
- this.pixToGeoY(se.offsetY)
139
- )
140
- } else if (se instanceof TouchEvent) {
141
- //compute average position of the touches
142
- let tx = 0,
143
- ty = 0
144
- for (let tt of se.targetTouches) {
145
- tx += tt.clientX
146
- ty += tt.clientY
147
- }
148
- tx /= se.targetTouches.length
149
- ty /= se.targetTouches.length
150
-
151
- // Adjust for container's offset
152
- // tx -= containerRect.left
153
- // ty -= containerRect.top
154
-
155
- //zoom at this average position
156
- this.zoom(zoomFactor, this.pixToGeoX(tx), this.pixToGeoY(ty))
157
- }
158
- }
159
- }
160
-
161
- //center extent
162
- /** @type {number|undefined} */
163
- this.xMin = opts.centerExtent ? opts.centerExtent[0] : undefined
164
- /** @type {number|undefined} */
165
- this.yMin = opts.centerExtent ? opts.centerExtent[1] : undefined
166
- /** @type {number|undefined} */
167
- this.xMax = opts.centerExtent ? opts.centerExtent[2] : undefined
168
- /** @type {number|undefined} */
169
- this.yMax = opts.centerExtent ? opts.centerExtent[3] : undefined
170
-
171
- /** Zoom extent, to limit zoom in and out
172
- * @type {Array.<number>} */
173
- this.zoomExtent = opts.zoomExtent || [0, Infinity]
174
-
175
- /** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
176
- * @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
177
- this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
178
- }
179
-
180
- /** @returns {View} */
181
- getView() {
182
- return this.view
183
- }
184
-
185
- /** @param {Array.<number>} v */
186
- setCenterExtent(v) {
187
- this.xMin = v[0]
188
- this.yMin = v[1]
189
- this.xMax = v[2]
190
- this.yMax = v[3]
191
- }
192
- /** @returns {Array.<number|undefined>} */
193
- getCenterExtent() {
194
- return [this.xMin, this.yMin, this.xMax, this.yMax]
195
- }
196
-
197
- /** @param {Array.<number>} v */
198
- setZoomExtent(v) {
199
- this.zoomExtent = v
200
- }
201
- /** @returns {Array.<number>} */
202
- getZoomExtent() {
203
- return this.zoomExtent
204
- }
205
-
206
- /** Initialise canvas transform with identity transformation. */
207
- initCanvasTransform() {
208
- this.ctx.setTransform(1, 0, 0, 1, 0, 0)
209
- this.offscreenCtx.setTransform(1, 0, 0, 1, 0, 0)
210
- }
211
-
212
- /** Initialise canvas transform with geo to screen transformation, so that geo objects can be drawn directly in geo coordinates. */
213
- setCanvasTransform() {
214
- const k = 1 / this.view.z
215
- const tx = -this.view.x / this.view.z + this.w * 0.5
216
- const ty = this.view.y / this.view.z + this.h * 0.5
217
- this.ctx.setTransform(k, 0, 0, -k, tx, ty)
218
- this.offscreenCtx.setTransform(k, 0, 0, -k, tx, ty)
219
- }
220
-
221
- /** Get the transformation matrix to webGL screen coordinates, within [-1,1]*[-1,1] */
222
- getWebGLTransform() {
223
- const kx = 2.0 / (this.w * this.view.z)
224
- const ky = 2.0 / (this.h * this.view.z)
225
- return [kx, 0.0, 0.0, 0.0, ky, 0.0, -kx * this.view.x, -ky * this.view.y, 1.0]
226
- }
227
-
228
- /** The function specifying how to draw the map. */
229
- redraw() {
230
- throw new Error('Method redraw not implemented.')
231
- }
232
-
233
- /** When the zoom level changes, ensures that any ongoing requests are aborted before new ones are initiated. */
234
- cancelCurrentRequests() {
235
- throw new Error('Method cancelCurrentRequests not implemented.')
236
- }
237
-
238
- /**
239
- * Clear. To be used before a redraw for example.
240
- * @param {string} color
241
- */
242
- clear(color = 'white') {
243
- if (this.opts.transparentBackground) {
244
- this.ctx.clearRect(0, 0, this.w, this.h)
245
- this.offscreenCtx.clearRect(0, 0, this.w, this.h)
246
- } else {
247
- if (this.ctx) this.ctx.fillStyle = color
248
- if (this.offscreenCtx) this.offscreenCtx.fillStyle = color
249
- this.ctx.fillRect(0, 0, this.w, this.h)
250
- this.offscreenCtx.fillRect(0, 0, this.w, this.h)
251
- }
252
- }
253
-
254
- /**
255
- * @param {number} dxGeo
256
- * @param {number} dyGeo
257
- */
258
- pan(dxGeo = 0, dyGeo = 0) {
259
- //ensures x/y extent
260
- if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
261
- if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
262
- if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
263
- if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
264
-
265
- //pan
266
- this.view.x += dxGeo
267
- this.view.y += dyGeo
268
- this.updateExtentGeo()
269
-
270
- if (this.canvasSave.c) {
271
- const scale = 1 / this.view.z
272
-
273
- // Update saved canvas offset
274
- this.canvasSave.dx -= dxGeo * scale
275
- this.canvasSave.dy += dyGeo * scale
276
-
277
- // clear canvas
278
- this.clear(this.backgroundColor)
279
-
280
- // this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
281
- //this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
282
- this.offscreenCtx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
283
-
284
- // Render the offscreen canvas to the visible context
285
- // this.clear(this.backgroundColor)
286
- this.ctx.drawImage(this.offscreenCtx.canvas, 0, 0)
287
- } else {
288
- console.log('no canvas save')
289
- }
290
- }
291
-
292
- /**
293
- * Zoom.
294
- * @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
295
- * @param {number} xGeo The x geo position fixed in the screen.
296
- * @param {number} yGeo The y geo position fixed in the screen.
297
- */
298
- zoom(f = 1, xGeo = this.view.x, yGeo = this.view.y) {
299
- //TODO force geo extend to remain
300
-
301
- //trying to zoom in/out beyond limit
302
- if (this.zoomExtent[0] == this.view.z && f <= 1) return
303
- if (this.zoomExtent[1] == this.view.z && f >= 1) return
304
-
305
- //ensure zoom extent preserved
306
- const newZf = f * this.view.z
307
- if (newZf < this.zoomExtent[0]) f = this.zoomExtent[0] / this.view.z
308
- if (newZf > this.zoomExtent[1]) f = this.zoomExtent[1] / this.view.z
309
-
310
- this.view.z *= f
311
-
312
- //compute pan
313
- let dxGeo = (xGeo - this.view.x) * (1 - f)
314
- let dyGeo = (yGeo - this.view.y) * (1 - f)
315
-
316
- //ensures x/y extent
317
- if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
318
- if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
319
- if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
320
- if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
321
-
322
- //pan
323
- this.view.x += dxGeo
324
- this.view.y += dyGeo
325
- this.updateExtentGeo()
326
-
327
- // zoom in on the current canvas state
328
- if (this.canvasSave.c) {
329
- this.clear(this.backgroundColor)
330
- this.canvasSave.f /= f
331
- this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
332
- this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
333
- this.clear(this.backgroundColor)
334
- this.offscreenCtx.drawImage(
335
- this.canvasSave.c,
336
- this.canvasSave.dx,
337
- this.canvasSave.dy,
338
- this.canvasSave.f * this.canvasSave.c.width,
339
- this.canvasSave.f * this.canvasSave.c.height
340
- )
341
- this.ctx.drawImage(
342
- this.offscreenCanvas, // Use offscreen canvas as the source
343
- 0,
344
- 0, // Position the offscreen canvas at the top-left corner of the main canvas
345
- this.canvas.width, // The width of the visible canvas
346
- this.canvas.height // The height of the visible canvas
347
- )
348
- }
349
- }
350
-
351
- /**
352
- * @param {number} marginPx
353
- * @returns {Envelope} The envelope of the view, in geo coordinates.
354
- */
355
- updateExtentGeo(marginPx = 20) {
356
- this.extGeo = {
357
- xMin: this.pixToGeoX(-marginPx),
358
- xMax: this.pixToGeoX(this.w + marginPx),
359
- yMin: this.pixToGeoY(this.h + marginPx),
360
- yMax: this.pixToGeoY(-marginPx),
361
- }
362
- return this.extGeo
363
- }
364
-
365
- /**
366
- * Check if the object has to be drawn
367
- *
368
- * @param {{x:number,y:number}} obj
369
- */
370
- toDraw(obj) {
371
- if (obj.x < this.extGeo.xMin) return false
372
- if (obj.x > this.extGeo.xMax) return false
373
- if (obj.y < this.extGeo.yMin) return false
374
- if (obj.y > this.extGeo.yMax) return false
375
- return true
376
- }
377
-
378
- //conversion functions
379
- /**
380
- * @param {number} xGeo Geo x coordinate, in m.
381
- * @returns {number} Screen x coordinate, in pix.
382
- */
383
- geoToPixX(xGeo) {
384
- return (xGeo - this.view.x) / this.view.z + this.w * 0.5
385
- }
386
- /**
387
- * @param {number} yGeo Geo y coordinate, in m.
388
- * @returns {number} Screen y coordinate, in pix.
389
- */
390
- geoToPixY(yGeo) {
391
- return -(yGeo - this.view.y) / this.view.z + this.h * 0.5
392
- }
393
- /**
394
- * @param {number} x Screen x coordinate, in pix.
395
- * @returns {number} Geo x coordinate, in m.
396
- */
397
- pixToGeoX(x) {
398
- return (x - this.w * 0.5) * this.view.z + this.view.x
399
- }
400
- /**
401
- * @param {number} y Screen y coordinate, in pix.
402
- * @returns {number} Geo y coordinate, in m.
403
- */
404
- pixToGeoY(y) {
405
- return -(y - this.h * 0.5) * this.view.z + this.view.y
406
- }
407
-
408
- /** Get x,y,z elements from URL and assign them to the view. */
409
- setViewFromURL() {
410
- const x = GeoCanvas.getParameterByName('x'),
411
- y = GeoCanvas.getParameterByName('y'),
412
- z = GeoCanvas.getParameterByName('z')
413
- if (x != null && x != undefined && !isNaN(+x)) this.view.x = +x
414
- if (y != null && y != undefined && !isNaN(+y)) this.view.y = +y
415
- if (z != null && z != undefined && !isNaN(+z)) this.view.z = +z
416
- }
417
-
418
- /**
419
- * Get a URL parameter by name.
420
- *
421
- * @param {string} name
422
- * @returns {string | null}
423
- */
424
- static getParameterByName(name) {
425
- name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
426
- var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
427
- results = regex.exec(location.search)
428
- return !results ? null : decodeURIComponent(results[1].replace(/\+/g, ' '))
429
- }
430
- }
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ /** @typedef { {xMin: number, xMax: number, yMin: number, yMax: number} } Envelope */
5
+
6
+ /**
7
+ * A viewshed.
8
+ * @typedef {{x: number, y: number, z: number}} View */
9
+
10
+ import { select } from 'd3-selection'
11
+ import { zoom as d3zoom, zoomIdentity } from 'd3-zoom'
12
+
13
+ /**
14
+ * A HTML canvas for geo data display, enhanced with zoom and pan capabilities.
15
+ *
16
+ * @module core
17
+ * @author Joseph Davies, Julien Gaffuri
18
+ */
19
+ export class GeoCanvas {
20
+ /**
21
+ * @constructor
22
+ * @param {HTMLCanvasElement} canvas
23
+ * @param {number} x The x coordinate of the view
24
+ * @param {number} y The y coordinate of the view
25
+ * @param {number} z The zoom level of the view (pixel size, in ground m)
26
+ * @param {object} opts
27
+ */
28
+ constructor(canvas, x = 0, y = 0, z = 0, opts = undefined) {
29
+ this.opts = opts || {}
30
+
31
+ /** @type {HTMLCanvasElement} */
32
+ this.canvas = canvas
33
+
34
+ /** @type {number} */
35
+ this.w = this.canvas.offsetWidth
36
+ /** @type {number} */
37
+ this.h = this.canvas.offsetHeight
38
+
39
+ // Adjust canvas width and height based on device pixel ratio
40
+ //const dpr = window.devicePixelRatio || 1 // Get the device pixel ratio
41
+ //this.canvas.width = this.w * dpr // Set canvas width
42
+ //\sthis.canvas.height = this.h * dpr // Set canvas height
43
+
44
+ // Create offscreen canvas for drawing operations
45
+ this.offscreenCanvas = document.createElement('canvas')
46
+ this.offscreenCanvas.width = this.w
47
+ this.offscreenCanvas.height = this.h
48
+
49
+ const ctx = this.canvas.getContext('2d')
50
+ const offscreenCtx = this.offscreenCanvas.getContext('2d')
51
+ if (!ctx) throw 'Impossible to create canvas 2D context'
52
+ if (!offscreenCtx) throw 'Impossible to create canvas 2D context'
53
+ /**@type {CanvasRenderingContext2D} */
54
+ this.ctx = ctx
55
+ this.offscreenCtx = offscreenCtx
56
+ //this.ctx.scale(dpr, dpr) // Scale the context
57
+
58
+ /**
59
+ * z: pixel size, in m/pix
60
+ * @type {View} */
61
+ this.view = { x: x, y: y, z: z }
62
+
63
+ /** Background color.
64
+ * @type {string} */
65
+ this.backgroundColor = opts.backgroundColor || 'white'
66
+
67
+ /** @type {function(object|undefined):void} */
68
+ this.onZoomStartFun = opts.onZoomStartFun
69
+
70
+ /** @type {function(object|undefined):void} */
71
+ this.onZoomEndFun = opts.onZoomEndFun
72
+
73
+ /** @type {function(object|undefined):void} */
74
+ this.onZoomFun = opts.onZoomFun
75
+
76
+ //current extent
77
+ /** @type {Envelope} */
78
+ this.extGeo = { xMin: NaN, xMax: NaN, yMin: NaN, yMax: NaN }
79
+ this.updateExtentGeo()
80
+
81
+ //rely on d3 for zoom
82
+ if (!opts.disableZoom) {
83
+ let tP = zoomIdentity
84
+ const z = d3zoom()
85
+ // to make the zooming a bit faster
86
+ .wheelDelta((e) => -e.deltaY * (e.deltaMode === 1 ? 0.07 : e.deltaMode ? 1 : 0.004))
87
+ .on('zoom', (e) => {
88
+ const t = e.transform
89
+ const zoomFactor = tP.k / t.k
90
+ if (zoomFactor == 1) {
91
+ //pan
92
+ const dx = tP.x - t.x
93
+ const dy = tP.y - t.y
94
+ this.applyPan(dx * this.view.z, -dy * this.view.z)
95
+ } else {
96
+ handleZoom(e, zoomFactor)
97
+ }
98
+ tP = t
99
+
100
+ if (this.onZoomFun) this.onZoomFun(e)
101
+ })
102
+ .on('start', (e) => {
103
+ // start of zoom event
104
+ this._isZooming = true;
105
+ // save the current canvas state to keep onscreen during pan/zoom before redrawing
106
+ this.canvasSave.c = document.createElement('canvas')
107
+ this.canvasSave.c.setAttribute('width', '' + this.w)
108
+ this.canvasSave.c.setAttribute('height', '' + this.h)
109
+ this.canvasSave.c.getContext('2d')?.drawImage(this.canvas, 0, 0)
110
+ this.canvasSave.dx = 0
111
+ this.canvasSave.dy = 0
112
+ this.canvasSave.f = 1
113
+ if (this.onZoomStartFun) this.onZoomStartFun(e)
114
+ })
115
+ .on('end', (e) => {
116
+ // end of pan/zoom event
117
+ this._isZooming = false;
118
+ this.redraw()
119
+ this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
120
+ if (this.onZoomEndFun) this.onZoomEndFun(e)
121
+
122
+ })
123
+ // @ts-ignore
124
+ z(select(this.canvas))
125
+
126
+ const handleZoom = (event, zoomFactor) => {
127
+ // cancel ongoing data requests
128
+ this.cancelCurrentRequests()
129
+ const se = event.sourceEvent
130
+
131
+ if (se instanceof WheelEvent) {
132
+ //zoom at the mouse position
133
+ this.applyZoom(
134
+ zoomFactor,
135
+ // @ts-ignore
136
+ this.pixToGeoX(se.offsetX),
137
+ // @ts-ignore
138
+ this.pixToGeoY(se.offsetY)
139
+ )
140
+ } else if (se instanceof TouchEvent) {
141
+ //compute average position of the touches
142
+ let tx = 0,
143
+ ty = 0
144
+ for (let tt of se.targetTouches) {
145
+ tx += tt.clientX
146
+ ty += tt.clientY
147
+ }
148
+ tx /= se.targetTouches.length
149
+ ty /= se.targetTouches.length
150
+
151
+ // Adjust for container's offset
152
+ // tx -= containerRect.left
153
+ // ty -= containerRect.top
154
+
155
+ //zoom at this average position
156
+ this.applyZoom(zoomFactor, this.pixToGeoX(tx), this.pixToGeoY(ty))
157
+ }
158
+ }
159
+ }
160
+
161
+ //center extent
162
+ /** @type {number|undefined} */
163
+ this.xMin = opts.centerExtent ? opts.centerExtent[0] : undefined
164
+ /** @type {number|undefined} */
165
+ this.yMin = opts.centerExtent ? opts.centerExtent[1] : undefined
166
+ /** @type {number|undefined} */
167
+ this.xMax = opts.centerExtent ? opts.centerExtent[2] : undefined
168
+ /** @type {number|undefined} */
169
+ this.yMax = opts.centerExtent ? opts.centerExtent[3] : undefined
170
+
171
+ /** Zoom extent, to limit zoom in and out
172
+ * @type {Array.<number>} */
173
+ this.zoomExtent = opts.zoomExtent || [0, Infinity]
174
+
175
+ /** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
176
+ * @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
177
+ this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
178
+ }
179
+
180
+ /**
181
+ * @param {number} dxGeo
182
+ * @param {number} dyGeo
183
+ */
184
+ applyPan(dxGeo = 0, dyGeo = 0) {
185
+ //ensures x/y extent
186
+ if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
187
+ if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
188
+ if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
189
+ if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
190
+
191
+ //pan
192
+ this.view.x += dxGeo
193
+ this.view.y += dyGeo
194
+ this.updateExtentGeo()
195
+
196
+ if (this.canvasSave.c) {
197
+ const scale = 1 / this.view.z
198
+
199
+ // Update saved canvas offset
200
+ this.canvasSave.dx -= dxGeo * scale
201
+ this.canvasSave.dy += dyGeo * scale
202
+
203
+ // clear canvas
204
+ this.clear(this.backgroundColor)
205
+
206
+ // this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
207
+ //this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
208
+ this.offscreenCtx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
209
+
210
+ // Render the offscreen canvas to the visible context
211
+ this.ctx.drawImage(this.offscreenCtx.canvas, 0, 0)
212
+ } else {
213
+ console.log('no canvas save')
214
+ }
215
+ }
216
+
217
+ /**
218
+ * Zoom.
219
+ * @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
220
+ * @param {number} xGeo The x geo position fixed in the screen.
221
+ * @param {number} yGeo The y geo position fixed in the screen.
222
+ */
223
+ applyZoom(f = 1, xGeo = this.view.x, yGeo = this.view.y) {
224
+ //TODO force geo extend to remain
225
+
226
+ //trying to zoom in/out beyond limit
227
+ if (this.zoomExtent[0] == this.view.z && f <= 1) return
228
+ if (this.zoomExtent[1] == this.view.z && f >= 1) return
229
+
230
+ //ensure zoom extent preserved
231
+ const newZf = f * this.view.z
232
+ if (newZf < this.zoomExtent[0]) f = this.zoomExtent[0] / this.view.z
233
+ if (newZf > this.zoomExtent[1]) f = this.zoomExtent[1] / this.view.z
234
+
235
+ this.view.z *= f
236
+
237
+ //compute pan
238
+ let dxGeo = (xGeo - this.view.x) * (1 - f)
239
+ let dyGeo = (yGeo - this.view.y) * (1 - f)
240
+
241
+ //ensures x/y extent
242
+ if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
243
+ if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
244
+ if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
245
+ if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
246
+
247
+ //pan
248
+ this.view.x += dxGeo
249
+ this.view.y += dyGeo
250
+ this.updateExtentGeo()
251
+
252
+ this._drawZoomFrame(f, xGeo, yGeo)
253
+ }
254
+
255
+ _drawZoomFrame(f, xGeo, yGeo) {
256
+ // zoom in on the current canvas state
257
+ if (this.canvasSave.c) {
258
+ this.clear(this.backgroundColor)
259
+ this.canvasSave.f /= f
260
+ this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
261
+ this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
262
+ this.clear(this.backgroundColor)
263
+ this.offscreenCtx.drawImage(
264
+ this.canvasSave.c,
265
+ this.canvasSave.dx,
266
+ this.canvasSave.dy,
267
+ this.canvasSave.f * this.canvasSave.c.width,
268
+ this.canvasSave.f * this.canvasSave.c.height
269
+ )
270
+ this.ctx.drawImage(
271
+ this.offscreenCanvas, // Use offscreen canvas as the source
272
+ 0,
273
+ 0, // Position the offscreen canvas at the top-left corner of the main canvas
274
+ this.canvas.width, // The width of the visible canvas
275
+ this.canvas.height // The height of the visible canvas
276
+ )
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Clear. To be used before a redraw for example.
282
+ * @param {string} color
283
+ */
284
+ clear(color = 'white') {
285
+ if (this.opts.transparentBackground) {
286
+ this.ctx.clearRect(0, 0, this.w, this.h)
287
+ this.offscreenCtx.clearRect(0, 0, this.w, this.h)
288
+ } else {
289
+ if (this.ctx) this.ctx.fillStyle = color
290
+ if (this.offscreenCtx) this.offscreenCtx.fillStyle = color
291
+ this.ctx.fillRect(0, 0, this.w, this.h)
292
+ this.offscreenCtx.fillRect(0, 0, this.w, this.h)
293
+ }
294
+ }
295
+
296
+ /** @returns {View} */
297
+ getView() {
298
+ return this.view
299
+ }
300
+
301
+ /** @param {Array.<number>} v */
302
+ setCenterExtent(v) {
303
+ this.xMin = v[0]
304
+ this.yMin = v[1]
305
+ this.xMax = v[2]
306
+ this.yMax = v[3]
307
+ }
308
+ /** @returns {Array.<number|undefined>} */
309
+ getCenterExtent() {
310
+ return [this.xMin, this.yMin, this.xMax, this.yMax]
311
+ }
312
+
313
+ /** @param {Array.<number>} v */
314
+ setZoomExtent(v) {
315
+ this.zoomExtent = v
316
+ }
317
+ /** @returns {Array.<number>} */
318
+ getZoomExtent() {
319
+ return this.zoomExtent
320
+ }
321
+
322
+ /** Initialise canvas transform with identity transformation. */
323
+ initCanvasTransform() {
324
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0)
325
+ this.offscreenCtx.setTransform(1, 0, 0, 1, 0, 0)
326
+ }
327
+
328
+ /** Initialise canvas transform with geo to screen transformation, so that geo objects can be drawn directly in geo coordinates. */
329
+ setCanvasTransform() {
330
+ const k = 1 / this.view.z
331
+ const tx = -this.view.x / this.view.z + this.w * 0.5
332
+ const ty = this.view.y / this.view.z + this.h * 0.5
333
+ this.ctx.setTransform(k, 0, 0, -k, tx, ty)
334
+ this.offscreenCtx.setTransform(k, 0, 0, -k, tx, ty)
335
+ }
336
+
337
+ /** Get the transformation matrix to webGL screen coordinates, within [-1,1]*[-1,1] */
338
+ getWebGLTransform() {
339
+ const kx = 2.0 / (this.w * this.view.z)
340
+ const ky = 2.0 / (this.h * this.view.z)
341
+ return [kx, 0.0, 0.0, 0.0, ky, 0.0, -kx * this.view.x, -ky * this.view.y, 1.0]
342
+ }
343
+
344
+ /** The function specifying how to draw the map. */
345
+ redraw() {
346
+ throw new Error('Method redraw not implemented.')
347
+ }
348
+
349
+ /** When the zoom level changes, ensures that any ongoing requests are aborted before new ones are initiated. */
350
+ cancelCurrentRequests() {
351
+ throw new Error('Method cancelCurrentRequests not implemented.')
352
+ }
353
+
354
+
355
+
356
+
357
+
358
+ /**
359
+ * @param {number} marginPx
360
+ * @returns {Envelope} The envelope of the view, in geo coordinates.
361
+ */
362
+ updateExtentGeo(marginPx = 20) {
363
+ this.extGeo = {
364
+ xMin: this.pixToGeoX(-marginPx),
365
+ xMax: this.pixToGeoX(this.w + marginPx),
366
+ yMin: this.pixToGeoY(this.h + marginPx),
367
+ yMax: this.pixToGeoY(-marginPx),
368
+ }
369
+ return this.extGeo
370
+ }
371
+
372
+ /**
373
+ * Check if the object has to be drawn
374
+ *
375
+ * @param {{x:number,y:number}} obj
376
+ */
377
+ toDraw(obj) {
378
+ if (obj.x < this.extGeo.xMin) return false
379
+ if (obj.x > this.extGeo.xMax) return false
380
+ if (obj.y < this.extGeo.yMin) return false
381
+ if (obj.y > this.extGeo.yMax) return false
382
+ return true
383
+ }
384
+
385
+ //conversion functions
386
+ /**
387
+ * @param {number} xGeo Geo x coordinate, in m.
388
+ * @returns {number} Screen x coordinate, in pix.
389
+ */
390
+ geoToPixX(xGeo) {
391
+ return (xGeo - this.view.x) / this.view.z + this.w * 0.5
392
+ }
393
+ /**
394
+ * @param {number} yGeo Geo y coordinate, in m.
395
+ * @returns {number} Screen y coordinate, in pix.
396
+ */
397
+ geoToPixY(yGeo) {
398
+ return -(yGeo - this.view.y) / this.view.z + this.h * 0.5
399
+ }
400
+ /**
401
+ * @param {number} x Screen x coordinate, in pix.
402
+ * @returns {number} Geo x coordinate, in m.
403
+ */
404
+ pixToGeoX(x) {
405
+ return (x - this.w * 0.5) * this.view.z + this.view.x
406
+ }
407
+ /**
408
+ * @param {number} y Screen y coordinate, in pix.
409
+ * @returns {number} Geo y coordinate, in m.
410
+ */
411
+ pixToGeoY(y) {
412
+ return -(y - this.h * 0.5) * this.view.z + this.view.y
413
+ }
414
+
415
+ /** Get x,y,z elements from URL and assign them to the view. */
416
+ setViewFromURL() {
417
+ const x = GeoCanvas.getParameterByName('x'),
418
+ y = GeoCanvas.getParameterByName('y'),
419
+ z = GeoCanvas.getParameterByName('z')
420
+ if (x != null && x != undefined && !isNaN(+x)) this.view.x = +x
421
+ if (y != null && y != undefined && !isNaN(+y)) this.view.y = +y
422
+ if (z != null && z != undefined && !isNaN(+z)) this.view.z = +z
423
+ }
424
+
425
+ /**
426
+ * Get a URL parameter by name.
427
+ *
428
+ * @param {string} name
429
+ * @returns {string | null}
430
+ */
431
+ static getParameterByName(name) {
432
+ name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]')
433
+ var regex = new RegExp('[\\?&]' + name + '=([^&#]*)'),
434
+ results = regex.exec(location.search)
435
+ return !results ? null : decodeURIComponent(results[1].replace(/\+/g, ' '))
436
+ }
437
+ }