gridviz 3.0.14 → 3.0.16

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