gridviz 3.0.25 → 3.0.27

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