gridviz 3.0.18 → 3.0.20

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 (56) hide show
  1. package/README.md +128 -127
  2. package/dist/gridviz.js +7862 -7819
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +74 -74
  5. package/src/button/ZoomButtons.js +101 -101
  6. package/src/core/Dataset.js +154 -154
  7. package/src/core/Drawable.js +60 -60
  8. package/src/core/GeoCanvas.js +430 -419
  9. package/src/core/Layer.js +23 -23
  10. package/src/core/Legend.js +108 -108
  11. package/src/core/Map.js +516 -503
  12. package/src/core/MultiResolutionDataset.js +93 -93
  13. package/src/core/Style.js +98 -98
  14. package/src/core/Tooltip.js +1 -0
  15. package/src/dataset/CSVGrid.js +112 -112
  16. package/src/dataset/GeoTIFF.js +122 -122
  17. package/src/dataset/JSGrid.js +55 -55
  18. package/src/dataset/TiledGrid.js +302 -289
  19. package/src/dataset/worker.js +18 -18
  20. package/src/index.js +77 -77
  21. package/src/layer/BackgroundLayer.js +162 -162
  22. package/src/layer/BackgroundLayerImage.js +81 -81
  23. package/src/layer/BackgroundLayerWMS.js +104 -104
  24. package/src/layer/GeoJSONLayer.js +214 -214
  25. package/src/layer/GridLayer.js +148 -148
  26. package/src/layer/LabelLayer.js +194 -194
  27. package/src/legend/ColorCategoryLegend.js +109 -109
  28. package/src/legend/ColorDiscreteLegend.js +100 -100
  29. package/src/legend/ColorLegend.js +115 -115
  30. package/src/legend/OrientationLegend.js +89 -89
  31. package/src/legend/SizeLegend.js +224 -224
  32. package/src/legend/TernaryLegend.js +302 -302
  33. package/src/style/CompositionStyle.js +291 -291
  34. package/src/style/DotDensityStyle.js +133 -133
  35. package/src/style/ImageStyle.js +100 -100
  36. package/src/style/IsoFenceStyle.js +217 -217
  37. package/src/style/JoyPlotStyle.js +127 -127
  38. package/src/style/LegoStyle.js +110 -110
  39. package/src/style/MosaicStyle.js +99 -99
  40. package/src/style/NinjaStarStyle.js +102 -102
  41. package/src/style/PillarStyle.js +207 -207
  42. package/src/style/SegmentStyle.js +105 -105
  43. package/src/style/ShapeColorSizeStyle.js +147 -147
  44. package/src/style/SideCategoryStyle.js +112 -112
  45. package/src/style/SideStyle.js +212 -212
  46. package/src/style/SquareColorCategoryWebGLStyle.js +107 -107
  47. package/src/style/SquareColorWebGLStyle.js +126 -126
  48. package/src/style/StrokeStyle.js +135 -135
  49. package/src/style/TanakaStyle.js +69 -69
  50. package/src/style/TextStyle.js +110 -110
  51. package/src/style/TimeSeriesStyle.js +222 -222
  52. package/src/utils/scale.js +218 -218
  53. package/src/utils/stretching.js +120 -120
  54. package/src/utils/ternary.js +208 -203
  55. package/src/utils/utils.js +58 -58
  56. package/src/utils/webGLUtils.js +72 -72
@@ -1,419 +1,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 zoom for pan/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 f = tP.k / t.k
90
-
91
- // Get container's bounding rect to adjust for offsets
92
- const containerRect = this.canvas.getBoundingClientRect()
93
-
94
- // Adjust for container's offset and zoom center
95
- const offsetX = e.sourceEvent.offsetX - containerRect.left
96
- const offsetY = e.sourceEvent.offsetY - containerRect.top
97
-
98
- if (f === 1) {
99
- // Pan logic
100
- const dx = tP.x - t.x
101
- const dy = tP.y - t.y
102
- this.pan(dx * this.view.z, -dy * this.view.z)
103
- } else {
104
- const se = e.sourceEvent
105
-
106
- if (se instanceof WheelEvent) {
107
- // Zoom at mouse position, adjusted by container offset
108
- this.zoom(f, this.pixToGeoX(offsetX), this.pixToGeoY(offsetY))
109
- } else if (se instanceof TouchEvent) {
110
- if (!se.targetTouches.length) return
111
-
112
- // Compute average position of the touches
113
- let tx = 0,
114
- ty = 0
115
- for (let tt of se.targetTouches) {
116
- tx += tt.clientX
117
- ty += tt.clientY
118
- }
119
- tx /= se.targetTouches.length
120
- ty /= se.targetTouches.length
121
-
122
- // Adjust for container's offset
123
- tx -= containerRect.left
124
- ty -= containerRect.top
125
-
126
- // Zoom at the average touch position
127
- this.zoom(f, this.pixToGeoX(tx), this.pixToGeoY(ty))
128
- }
129
- }
130
- tP = t
131
-
132
- if (this.onZoomFun) this.onZoomFun(e)
133
- })
134
- .on('start', (e) => {
135
- // start of zoom event
136
- // save the current canvas state to keep onscreen during pan/zoom before redrawing
137
- this.canvasSave.c = document.createElement('canvas')
138
- this.canvasSave.c.setAttribute('width', '' + this.w)
139
- this.canvasSave.c.setAttribute('height', '' + this.h)
140
- this.canvasSave.c.getContext('2d')?.drawImage(this.canvas, 0, 0)
141
- this.canvasSave.dx = 0
142
- this.canvasSave.dy = 0
143
- this.canvasSave.f = 1
144
- if (this.onZoomStartFun) this.onZoomStartFun(e)
145
- })
146
- .on('end', (e) => {
147
- // end of zoom event
148
- this.redraw()
149
- this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
150
-
151
- if (this.onZoomEndFun) this.onZoomEndFun(e)
152
- })
153
- z(select(this.canvas))
154
- }
155
-
156
- //center extent
157
- /** @type {number|undefined} */
158
- this.xMin = opts.centerExtent ? opts.centerExtent[0] : undefined
159
- /** @type {number|undefined} */
160
- this.yMin = opts.centerExtent ? opts.centerExtent[1] : undefined
161
- /** @type {number|undefined} */
162
- this.xMax = opts.centerExtent ? opts.centerExtent[2] : undefined
163
- /** @type {number|undefined} */
164
- this.yMax = opts.centerExtent ? opts.centerExtent[3] : undefined
165
-
166
- /** Zoom extent, to limit zoom in and out
167
- * @type {Array.<number>} */
168
- this.zoomExtent = opts.zoomExtent || [0, Infinity]
169
-
170
- /** Canvas state, to be used to avoid unnecessary redraws on zoom/pan
171
- * @type {{c:HTMLCanvasElement|null,dx:number,dy:number,f:number}} */
172
- this.canvasSave = { c: null, dx: 0, dy: 0, f: 1 }
173
- }
174
-
175
- /** @returns {View} */
176
- getView() {
177
- return this.view
178
- }
179
-
180
- /** @param {Array.<number>} v */
181
- setCenterExtent(v) {
182
- this.xMin = v[0]
183
- this.yMin = v[1]
184
- this.xMax = v[2]
185
- this.yMax = v[3]
186
- }
187
- /** @returns {Array.<number|undefined>} */
188
- getCenterExtent() {
189
- return [this.xMin, this.yMin, this.xMax, this.yMax]
190
- }
191
-
192
- /** @param {Array.<number>} v */
193
- setZoomExtent(v) {
194
- this.zoomExtent = v
195
- }
196
- /** @returns {Array.<number>} */
197
- getZoomExtent() {
198
- return this.zoomExtent
199
- }
200
-
201
- /** Initialise canvas transform with identity transformation. */
202
- initCanvasTransform() {
203
- this.ctx.setTransform(1, 0, 0, 1, 0, 0)
204
- this.offscreenCtx.setTransform(1, 0, 0, 1, 0, 0)
205
- }
206
-
207
- /** Initialise canvas transform with geo to screen transformation, so that geo objects can be drawn directly in geo coordinates. */
208
- setCanvasTransform() {
209
- const k = 1 / this.view.z
210
- const tx = -this.view.x / this.view.z + this.w * 0.5
211
- const ty = this.view.y / this.view.z + this.h * 0.5
212
- this.ctx.setTransform(k, 0, 0, -k, tx, ty)
213
- this.offscreenCtx.setTransform(k, 0, 0, -k, tx, ty)
214
- }
215
-
216
- /** Get the transformation matrix to webGL screen coordinates, within [-1,1]*[-1,1] */
217
- getWebGLTransform() {
218
- const kx = 2.0 / (this.w * this.view.z)
219
- const ky = 2.0 / (this.h * this.view.z)
220
- return [kx, 0.0, 0.0, 0.0, ky, 0.0, -kx * this.view.x, -ky * this.view.y, 1.0]
221
- }
222
-
223
- /** The function specifying how to draw the map. */
224
- redraw() {
225
- throw new Error('Method redraw not implemented.')
226
- }
227
-
228
- /**
229
- * Clear. To be used before a redraw for example.
230
- * @param {string} color
231
- */
232
- clear(color = 'white') {
233
- if (this.opts.transparentBackground) {
234
- this.ctx.clearRect(0, 0, this.w, this.h)
235
- this.offscreenCtx.clearRect(0, 0, this.w, this.h)
236
- } else {
237
- if (this.ctx) this.ctx.fillStyle = color
238
- if (this.offscreenCtx) this.offscreenCtx.fillStyle = color
239
- this.ctx.fillRect(0, 0, this.w, this.h)
240
- this.offscreenCtx.fillRect(0, 0, this.w, this.h)
241
- }
242
- }
243
-
244
- /**
245
- * @param {number} dxGeo
246
- * @param {number} dyGeo
247
- */
248
- pan(dxGeo = 0, dyGeo = 0) {
249
- //ensures x/y extent
250
- if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
251
- if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
252
- if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
253
- if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
254
-
255
- //pan
256
- this.view.x += dxGeo
257
- this.view.y += dyGeo
258
- this.updateExtentGeo()
259
-
260
- if (this.canvasSave.c) {
261
- const scale = 1 / this.view.z
262
-
263
- // Update saved canvas offset
264
- this.canvasSave.dx -= dxGeo * scale
265
- this.canvasSave.dy += dyGeo * scale
266
-
267
- // clear canvas
268
- this.clear(this.backgroundColor)
269
-
270
- // this doesnt work on mobile https://github.com/eurostat/gridviz/issues/98
271
- //this.ctx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
272
- this.offscreenCtx.drawImage(this.canvasSave.c, this.canvasSave.dx, this.canvasSave.dy)
273
-
274
- // Render the offscreen canvas to the visible context
275
- // this.clear(this.backgroundColor)
276
- this.ctx.drawImage(this.offscreenCtx.canvas, 0, 0)
277
- } else {
278
- console.log('no canvas save')
279
- }
280
- }
281
-
282
- /**
283
- * Zoom.
284
- * @param {number} f The zoom factor, within ]0, Infinity]. 1 is for no change. <1 to zoom-in, >1 to zoom-out.
285
- * @param {number} xGeo The x geo position fixed in the screen.
286
- * @param {number} yGeo The y geo position fixed in the screen.
287
- */
288
- zoom(f = 1, xGeo = this.view.x, yGeo = this.view.y) {
289
- //TODO force geo extend to remain
290
-
291
- //trying to zoom in/out beyond limit
292
- if (this.zoomExtent[0] == this.view.z && f <= 1) return
293
- if (this.zoomExtent[1] == this.view.z && f >= 1) return
294
-
295
- //ensure zoom extent preserved
296
- const newZf = f * this.view.z
297
- if (newZf < this.zoomExtent[0]) f = this.zoomExtent[0] / this.view.z
298
- if (newZf > this.zoomExtent[1]) f = this.zoomExtent[1] / this.view.z
299
-
300
- this.view.z *= f
301
-
302
- //compute pan
303
- let dxGeo = (xGeo - this.view.x) * (1 - f)
304
- let dyGeo = (yGeo - this.view.y) * (1 - f)
305
-
306
- //ensures x/y extent
307
- if (this.xMin != undefined && this.view.x + dxGeo < this.xMin) dxGeo = this.xMin - this.view.x
308
- if (this.yMin != undefined && this.view.y + dyGeo < this.yMin) dyGeo = this.yMin - this.view.y
309
- if (this.xMax != undefined && this.view.x + dxGeo > this.xMax) dxGeo = this.xMax - this.view.x
310
- if (this.yMax != undefined && this.view.y + dyGeo > this.yMax) dyGeo = this.yMax - this.view.y
311
-
312
- //pan
313
- this.view.x += dxGeo
314
- this.view.y += dyGeo
315
- this.updateExtentGeo()
316
-
317
- // zoom in on the current canvas state
318
- if (this.canvasSave.c) {
319
- this.clear(this.backgroundColor)
320
- this.canvasSave.f /= f
321
- this.canvasSave.dx = this.geoToPixX(xGeo) * (1 - this.canvasSave.f)
322
- this.canvasSave.dy = this.geoToPixY(yGeo) * (1 - this.canvasSave.f)
323
- this.offscreenCtx.drawImage(
324
- this.canvasSave.c,
325
- this.canvasSave.dx,
326
- this.canvasSave.dy,
327
- this.canvasSave.f * this.canvasSave.c.width,
328
- this.canvasSave.f * this.canvasSave.c.height
329
- )
330
- this.ctx.drawImage(
331
- this.offscreenCanvas, // Use offscreen canvas as the source
332
- 0,
333
- 0, // Position the offscreen canvas at the top-left corner of the main canvas
334
- this.canvas.width, // The width of the visible canvas
335
- this.canvas.height // The height of the visible canvas
336
- )
337
- }
338
- }
339
-
340
- /**
341
- * @param {number} marginPx
342
- * @returns {Envelope} The envelope of the view, in geo coordinates.
343
- */
344
- updateExtentGeo(marginPx = 20) {
345
- this.extGeo = {
346
- xMin: this.pixToGeoX(-marginPx),
347
- xMax: this.pixToGeoX(this.w + marginPx),
348
- yMin: this.pixToGeoY(this.h + marginPx),
349
- yMax: this.pixToGeoY(-marginPx),
350
- }
351
- return this.extGeo
352
- }
353
-
354
- /**
355
- * Check if the object has to be drawn
356
- *
357
- * @param {{x:number,y:number}} obj
358
- */
359
- toDraw(obj) {
360
- if (obj.x < this.extGeo.xMin) return false
361
- if (obj.x > this.extGeo.xMax) return false
362
- if (obj.y < this.extGeo.yMin) return false
363
- if (obj.y > this.extGeo.yMax) return false
364
- return true
365
- }
366
-
367
- //conversion functions
368
- /**
369
- * @param {number} xGeo Geo x coordinate, in m.
370
- * @returns {number} Screen x coordinate, in pix.
371
- */
372
- geoToPixX(xGeo) {
373
- return (xGeo - this.view.x) / this.view.z + this.w * 0.5
374
- }
375
- /**
376
- * @param {number} yGeo Geo y coordinate, in m.
377
- * @returns {number} Screen y coordinate, in pix.
378
- */
379
- geoToPixY(yGeo) {
380
- return -(yGeo - this.view.y) / this.view.z + this.h * 0.5
381
- }
382
- /**
383
- * @param {number} x Screen x coordinate, in pix.
384
- * @returns {number} Geo x coordinate, in m.
385
- */
386
- pixToGeoX(x) {
387
- return (x - this.w * 0.5) * this.view.z + this.view.x
388
- }
389
- /**
390
- * @param {number} y Screen y coordinate, in pix.
391
- * @returns {number} Geo y coordinate, in m.
392
- */
393
- pixToGeoY(y) {
394
- return -(y - this.h * 0.5) * this.view.z + this.view.y
395
- }
396
-
397
- /** Get x,y,z elements from URL and assign them to the view. */
398
- setViewFromURL() {
399
- const x = GeoCanvas.getParameterByName('x'),
400
- y = GeoCanvas.getParameterByName('y'),
401
- z = GeoCanvas.getParameterByName('z')
402
- if (x != null && x != undefined && !isNaN(+x)) this.view.x = +x
403
- if (y != null && y != undefined && !isNaN(+y)) this.view.y = +y
404
- if (z != null && z != undefined && !isNaN(+z)) this.view.z = +z
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
+ /**
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
+ }