gridviz 3.0.35 → 3.0.36

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 (67) hide show
  1. package/README.md +129 -129
  2. package/dist/gridviz.js +17486 -17486
  3. package/dist/gridviz.min.js +2 -2
  4. package/package.json +73 -73
  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 -443
  11. package/src/core/Layer.js +23 -23
  12. package/src/core/Legend.js +108 -108
  13. package/src/core/Map.js +568 -568
  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 +79 -79
  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 +276 -276
  28. package/src/layer/GridLayer.js +166 -166
  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/DotDensityStyle.js +133 -133
  38. package/src/style/ImageStyle.js +100 -100
  39. package/src/style/Interpolator.js +258 -258
  40. package/src/style/IsoFenceStyle.js +217 -217
  41. package/src/style/JoyPlotStyle.js +180 -180
  42. package/src/style/LegoStyle.js +132 -132
  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/ShadingRayStyle.js +280 -280
  48. package/src/style/ShadingStyle.js +72 -72
  49. package/src/style/ShapeColorSizeStyle.js +147 -147
  50. package/src/style/SideCategoryStyle.js +112 -112
  51. package/src/style/SideStyle.js +217 -217
  52. package/src/style/SideTanakaStyle.js +154 -154
  53. package/src/style/SquareColorCategoryWebGLStyle.js +307 -294
  54. package/src/style/SquareColorWebGLStyle.js +126 -126
  55. package/src/style/StrokeStyle.js +135 -135
  56. package/src/style/TextStyle.js +110 -110
  57. package/src/style/TimeSeriesStyle.js +222 -222
  58. package/src/utils/WebGLRectangleColoring.js +106 -106
  59. package/src/utils/WebGLSquareColoring.js +118 -118
  60. package/src/utils/WebGLSquareColoringAdvanced.js +227 -227
  61. package/src/utils/WebGLSquareColoringKS.js +153 -153
  62. package/src/utils/scale.js +255 -255
  63. package/src/utils/stretching.js +120 -120
  64. package/src/utils/ternary.js +208 -208
  65. package/src/utils/utils.js +125 -125
  66. package/src/utils/webGLUtils.js +73 -73
  67. package/src/utils/zoomUtils.js +142 -142
@@ -1,443 +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
- 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
- }
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
+ }