gridviz 3.0.15 → 3.0.17

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 +7958 -7861
  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 +494 -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 -204
  54. package/src/utils/utils.js +58 -63
  55. package/src/utils/webGLUtils.js +72 -70
package/src/core/Map.js CHANGED
@@ -1,495 +1,494 @@
1
- //@ts-check
2
- 'use strict'
3
-
4
- // internal imports
5
- import { GeoCanvas } from './GeoCanvas.js'
6
- import { Tooltip } from './Tooltip.js'
7
- import { ZoomButtons } from '../button/ZoomButtons.js'
8
- import { FullscreenButton } from '../button/FullscreenButton.js'
9
-
10
- // external imports
11
- import { select } from 'd3-selection'
12
-
13
- /**
14
- * A gridviz application.
15
- *
16
- * @module core
17
- * @author Joseph Davies, Julien Gaffuri
18
- */
19
- export class Map {
20
- /**
21
- * @param {HTMLDivElement} container
22
- * @param {object} opts
23
- */
24
- constructor(container, opts) {
25
- opts = opts || {}
26
-
27
- /**
28
- * The layers.
29
- * @type {Array.<import("./Layer.js").Layer>}
30
- * */
31
- this.layers = opts.layers || []
32
-
33
- //get container element
34
- this.container = container || document.getElementById('gridviz')
35
- if (!this.container) {
36
- console.error('Cannot find gridviz container element.')
37
- return
38
- }
39
-
40
- //https://css-tricks.com/absolute-positioning-inside-relative-positioning/
41
- this.container.style.position = "relative"; // container element must have relative positioning
42
-
43
- //set dimensions
44
- /** @type {number} */
45
- this.w = opts.w || this.container.offsetWidth
46
- /** @type {number} */
47
- this.h = opts.h || this.container.offsetHeight
48
-
49
- //create canvas element if not specified
50
- /** @type {HTMLCanvasElement} */
51
- this._canvas = opts.canvas || this.initialiseCanvas()
52
-
53
- /** Make geo canvas
54
- * @type {GeoCanvas}
55
- * @private */
56
- this.geoCanvas = new GeoCanvas(this._canvas, opts.x, opts.y, opts.z, opts)
57
- this.geoCanvas.redraw = () => { this.redraw() }
58
-
59
- // legend div
60
- this.legendDivId = opts.legendDivId || 'gvizLegend'
61
- this.legend = select('#' + this.legendDivId)
62
- if (this.legend.empty()) this.initialiseLegend()
63
-
64
-
65
- //tooltip
66
-
67
- // set App container as default parent element for tooltip
68
- if (!opts.tooltip) opts.tooltip = {}
69
- if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
70
-
71
- /**
72
- * @private
73
- * @type {Tooltip} */
74
- this.tooltip = new Tooltip(opts.tooltip)
75
-
76
- // add event listeners to container
77
- this.mouseOverHandler = (e) => this.focusCell(e)
78
- this.mouseMoveHandler = (e) => this.focusCell(e)
79
- this.mouseOutHandler = (e) => this.tooltip.hide()
80
- this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
81
- this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
82
- this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
83
-
84
- // add extra logic to onZoomStartFun
85
- this.geoCanvas.onZoomStartFun = (e) => {
86
- if (opts.onZoomStartFun) opts.onZoomStartFun(e)
87
- this.tooltip.hide()
88
- }
89
-
90
- //for mouse over
91
- /**
92
- * @private
93
- * @type {HTMLCanvasElement|null} */
94
- this.canvasSave = null
95
-
96
- this.selectionRectangleColor = opts.selectionRectangleColor || 'red'
97
- this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,z) => {}
98
-
99
- // transparent background (e.g. leaflet) 'red painting' fix
100
- this.transparentBackground = opts.transparentBackground
101
-
102
- //set default globalCompositeOperation
103
- this.defaultGlobalCompositeOperation =
104
- opts.defaultGlobalCompositeOperation || this.geoCanvas.ctx.globalCompositeOperation
105
- }
106
-
107
- /**
108
- * @protected
109
- * @returns {HTMLCanvasElement}
110
- */
111
- initialiseCanvas() {
112
- const canvas = document.createElement('canvas')
113
- canvas.setAttribute('width', '' + this.w)
114
- canvas.setAttribute('height', '' + this.h)
115
- this.container.appendChild(canvas)
116
- return canvas
117
- }
118
-
119
-
120
- initialiseLegend() {
121
- this.legend = select(this.container.id && this.container.id != '' ? '#' + this.container.id : 'body')
122
- .append('div')
123
- .attr('id', this.legendDivId)
124
- .style('position', 'absolute')
125
- .style('width', 'auto')
126
- .style('height', 'auto')
127
- .style('background', '#FFFFFF')
128
- //.style("padding", this.padding)
129
- .style('border', '0px')
130
- //.style('border-radius', '5px')
131
- .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
132
- .style('font-family', 'Helvetica, Arial, sans-serif')
133
- .style('bottom', '15px')
134
- .style('right', '15px')
135
- //hide
136
- //.style("visibility", "hidden")
137
- }
138
-
139
- /**
140
- * Set/get layer stack.
141
- *
142
- * @param {undefined|import("./Layer.js").Layer|import("./Layer.js").Layer[]} layers
143
- * @returns { this | import("./Layer.js").Layer[] }
144
- */
145
- layers_(layers) {
146
- if (arguments.length === 0) return this.layers
147
- if (arguments.length === 1)
148
- if (Array.isArray(layers)) this.layers = layers
149
- else this.layers = [layers]
150
- else this.layers = arguments
151
- return this
152
- }
153
-
154
- /** @returns {this} */
155
- redraw() {
156
- //remove legend elements
157
- if (this.legend) this.legend.selectAll('*').remove()
158
-
159
- //clear
160
- this.geoCanvas.initCanvasTransform()
161
- this.geoCanvas.clear(this.geoCanvas.backgroundColor)
162
-
163
- const z = this.geoCanvas.view.z
164
- this.updateExtentGeo()
165
-
166
- //go through the layers
167
- for (const layer of this.layers) {
168
-
169
- //check if layer is visible
170
- if (layer.visible && !layer.visible(z)) continue
171
-
172
- //set layer alpha and blend mode
173
- this.geoCanvas.ctx.globalAlpha = layer.alpha ? layer.alpha(z) : 1.0
174
- if (layer.blendOperation)
175
- this.geoCanvas.ctx.globalCompositeOperation = layer.blendOperation(z)
176
-
177
- //set affin transform to draw with geographical coordinates
178
- this.geoCanvas.setCanvasTransform()
179
-
180
- //draw layer
181
- layer.draw(this.geoCanvas, this.legend)
182
-
183
- //draw layer filter
184
- if (layer.filterColor)
185
- layer.drawFilter(this.geoCanvas)
186
-
187
- //restore default alpha and blend operation
188
- this.geoCanvas.ctx.globalAlpha = 1.0
189
- this.geoCanvas.ctx.globalCompositeOperation = this.defaultGlobalCompositeOperation
190
- }
191
-
192
- //
193
- this.canvasSave = null
194
-
195
- // listen for resize events on the App's container and handle them
196
- this.defineResizeObserver(this.container, this._canvas)
197
-
198
- return this
199
- }
200
-
201
-
202
- /**
203
- * @param {number} marginPx
204
- * @returns {import('./GeoCanvas.js').Envelope}
205
- * @public
206
- */
207
- updateExtentGeo(marginPx = 20) {
208
- return this.geoCanvas.updateExtentGeo(marginPx)
209
- }
210
-
211
-
212
-
213
- /** @param {MouseEvent} e */
214
- focusCell(e) {
215
- //compute mouse geo position
216
- const mousePositionGeo = {
217
- x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
218
- y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
219
- }
220
- /** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
221
- const focus = this.getCellFocusInfo(mousePositionGeo)
222
-
223
- // transparent background (e.g. leaflet) 'red painting' fix
224
- if (this.transparentBackground) {
225
- if (focus) {
226
- this.tooltip.html(focus.html)
227
- this.tooltip.setPosition(e)
228
- this.tooltip.show()
229
- } else {
230
- this.tooltip.hide()
231
- }
232
- this.canvasSave = document.createElement('canvas')
233
- this.canvasSave.setAttribute('width', '' + this.w)
234
- this.canvasSave.setAttribute('height', '' + this.h)
235
- this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.canvas, 0, 0)
236
- this.geoCanvas.initCanvasTransform()
237
- return
238
- }
239
-
240
- if (focus) {
241
- this.tooltip.html(focus.html)
242
- this.tooltip.setPosition(e)
243
- this.tooltip.show()
244
-
245
- //show cell position as a rectangle
246
- if (!this.canvasSave) {
247
- this.canvasSave = document.createElement('canvas')
248
- this.canvasSave.setAttribute('width', '' + this.w)
249
- this.canvasSave.setAttribute('height', '' + this.h)
250
- this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.canvas, 0, 0)
251
- } else {
252
- this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
253
- }
254
-
255
- //draw image saved + draw rectangle
256
- const rectWPix = this.selectionRectangleWidthPix
257
- ? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
258
- : 4
259
- this.geoCanvas.initCanvasTransform()
260
- this.geoCanvas.ctx.strokeStyle = this.selectionRectangleColor
261
- this.geoCanvas.ctx.lineWidth = rectWPix
262
- this.geoCanvas.ctx.beginPath()
263
-
264
- this.geoCanvas.ctx.rect(
265
- this.geoCanvas.geoToPixX(focus.cell.x) - rectWPix / 2,
266
- this.geoCanvas.geoToPixY(focus.cell.y) + rectWPix / 2,
267
- focus.resolution / this.geoCanvas.view.z + rectWPix,
268
- -focus.resolution / this.geoCanvas.view.z - rectWPix
269
- )
270
- this.geoCanvas.ctx.stroke()
271
- } else {
272
- this.tooltip.hide()
273
- if (this.canvasSave) this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
274
- }
275
- }
276
-
277
-
278
- /**
279
- * Return the cell HTML info at a given geo position.
280
- * This is usefull for user interactions, to show this info where the user clicks for example.
281
- *
282
- * @param {{x:number,y:number}} posGeo
283
- * @returns {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined}
284
- * @protected
285
- */
286
- getCellFocusInfo(posGeo) {
287
- //go through the layers, starting from top
288
- const z = this.geoCanvas.view.z
289
- for (let i = this.layers.length - 1; i >= 0; i--) {
290
- /** @type {import("./Layer.js").Layer} */
291
- const layer = this.layers[i]
292
- if (layer.visible && !layer.visible(z)) continue
293
- if (!layer.cellInfoHTML) continue
294
- //if (layer.cellInfoHTML === 'none') continue
295
- if (!layer.getDataset) continue
296
- const dsc = layer.getDataset(z)
297
- if (!dsc) continue
298
-
299
- //get cell at mouse position
300
- /** @type {import('./Dataset.js').Cell|undefined} */
301
- const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
302
- //console.log(cell, dsc.resolution)
303
- if (!cell) return undefined
304
-
305
- //rare case for a dataset with mixed resolutions
306
- if (dsc.mixedResolution) {
307
- const r = +dsc.mixedResolution(cell)
308
- const html = layer.cellInfoHTML(cell, r)
309
- if (!html) return undefined
310
- return { cell: cell, html: html, resolution: r }
311
- }
312
-
313
- const html = layer.cellInfoHTML(cell, dsc.getResolution())
314
- if (!html) return undefined
315
- return { cell: cell, html: html, resolution: dsc.getResolution() }
316
- }
317
- }
318
-
319
-
320
- /**
321
- * @param {number} x
322
- * @param {number} y
323
- * @param {number|undefined} z
324
- */
325
- setView(x, y, z = undefined) {
326
- this.geoCanvas.view.x = x
327
- this.geoCanvas.view.y = y
328
- if (z != undefined) this.geoCanvas.view.z = z
329
- return this
330
- }
331
-
332
- /** @returns {import('./GeoCanvas.js').View} */
333
- getView() { return this.geoCanvas.view }
334
-
335
- /** @returns {number} */
336
- getZoom() {
337
- return this.geoCanvas.view.z
338
- }
339
- /** @param {number} z @returns {this} */
340
- setZoom(z) {
341
- this.geoCanvas.view.z = z
342
- return this
343
- }
344
-
345
-
346
- /** @returns {Array.<number|undefined>} */
347
- getCenterExtent() {
348
- return this.geoCanvas.getCenterExtent()
349
- }
350
- /** @param {Array.<number>} val @returns {this} */
351
- setCenterExtent(val) {
352
- this.geoCanvas.setCenterExtent(val)
353
- return this
354
- }
355
-
356
- /** @returns {Array.<number>} */
357
- getZoomExtent() {
358
- return this.geoCanvas.getZoomExtent()
359
- }
360
- /** @param {Array.<number>} val @returns {this} */
361
- setZoomExtent(val) {
362
- this.geoCanvas.setZoomExtent(val)
363
- return this
364
- }
365
-
366
-
367
-
368
- /** @returns {string} */
369
- getBackgroundColor() {
370
- return this.geoCanvas.backgroundColor
371
- }
372
- /** @param {string} val @returns {this} */
373
- setBackgroundColor(val) {
374
- this.geoCanvas.backgroundColor = val
375
- return this
376
- }
377
-
378
- /**
379
- * Adds a set of zoom buttons to the map
380
- *
381
- * @param {object} opts
382
- * @returns {this}
383
- */
384
- addZoomButtons(opts) {
385
- // * opts.id
386
- // * opts.onZoom - custom event handler function
387
- // * opts.x
388
- // * opts.y
389
- // * opts.delta - zoom delta applied on each click
390
-
391
- this.zoomButtons = new ZoomButtons({
392
- map: this,
393
- id: opts?.id || 'gridviz-zoom-buttons',
394
- class: opts?.class,
395
- x: opts?.x,
396
- y: opts?.y,
397
- onZoom: opts?.onZoom,
398
- delta: opts?.delta || 0.2
399
- })
400
-
401
- return this
402
- }
403
-
404
- /**
405
- * Adds a fullscreen toggle button to the app
406
- *
407
- * @param {object} opts
408
- * @returns {this}
409
- */
410
- addFullscreenButton(opts) {
411
- // * opts.map - the gridviz map
412
- // * opts.id
413
- // * opts.x
414
- // * opts.y
415
-
416
- this.fullscreenButton = new FullscreenButton({
417
- map: this,
418
- id: opts?.id || 'gridviz-fullscreen-button',
419
- class: opts?.class,
420
- x: opts?.x,
421
- y: opts?.y
422
- })
423
-
424
- return this
425
- }
426
-
427
- /** @returns {this} */
428
- setViewFromURL() {
429
- this.geoCanvas.setViewFromURL()
430
- return this
431
- }
432
-
433
- /**
434
- * @description Add a resize event observer to the Apps container and update the canvas accordingly
435
- * @param {HTMLDivElement} container The App's container element
436
- * @param {HTMLCanvasElement} canvas The App canvas element
437
- * @memberof App
438
- */
439
- defineResizeObserver(container, canvas) {
440
- // listen to resize events
441
- const resizeObserver = new ResizeObserver((entries) => {
442
- // make sure canvas has been built
443
- if (container.clientWidth > 0 && container.clientHeight > 0) {
444
- // make sure we dont exceed loop limit first
445
- // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
446
- window.requestAnimationFrame(() => {
447
- if (!Array.isArray(entries) || !entries.length) {
448
- return
449
- }
450
- // update the map and canvas size
451
- if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
452
- this.h = container.clientHeight
453
- this.w = container.clientWidth
454
- this.geoCanvas.h = container.clientHeight
455
- this.geoCanvas.w = container.clientWidth
456
- canvas.setAttribute('width', '' + this.w)
457
- canvas.setAttribute('height', '' + this.h)
458
- this.redraw()
459
-
460
- //update button positions
461
- // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
462
- // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
463
- }
464
- })
465
- }
466
- })
467
-
468
- resizeObserver.observe(container)
469
- }
470
-
471
- /**
472
- * @description Destroy the map and it's event listeners
473
- * This should significantly reduce the memory used when creating and destroying gridviz map instances (for example in leaflet-gridviz)
474
- * @memberof App
475
- */
476
- destroy() {
477
- // clear layers
478
- this.layers = []
479
- this.bgLayers = []
480
-
481
- // remove event listeners from container
482
- this.container.removeEventListener('mouseover', this.mouseOverHandler)
483
- this.container.removeEventListener('mousemove', this.mouseMoveHandler)
484
- this.container.removeEventListener('mouseout', this.mouseOutHandler)
485
-
486
- // remove canvas
487
- this.geoCanvas.canvas.remove()
488
-
489
- // remove legend
490
- this.legend?.remove()
491
-
492
- // remove tooltip
493
- this.tooltip.tooltip?.remove()
494
- }
495
- }
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ // internal imports
5
+ import { GeoCanvas } from './GeoCanvas.js'
6
+ import { Tooltip } from './Tooltip.js'
7
+ import { ZoomButtons } from '../button/ZoomButtons.js'
8
+ import { FullscreenButton } from '../button/FullscreenButton.js'
9
+
10
+ // external imports
11
+ import { select } from 'd3-selection'
12
+
13
+ /**
14
+ * A gridviz application.
15
+ *
16
+ * @module core
17
+ * @author Joseph Davies, Julien Gaffuri
18
+ */
19
+ export class Map {
20
+ /**
21
+ * @param {HTMLDivElement} container
22
+ * @param {object} opts
23
+ */
24
+ constructor(container, opts) {
25
+ opts = opts || {}
26
+
27
+ /**
28
+ * The layers.
29
+ * @type {Array.<import("./Layer.js").Layer>}
30
+ * */
31
+ this.layers = opts.layers || []
32
+
33
+ //get container element
34
+ this.container = container || document.getElementById('gridviz')
35
+ if (!this.container) {
36
+ console.error('Cannot find gridviz container element.')
37
+ return
38
+ }
39
+
40
+ //https://css-tricks.com/absolute-positioning-inside-relative-positioning/
41
+ this.container.style.position = 'relative' // container element must have relative positioning
42
+
43
+ //set dimensions
44
+ /** @type {number} */
45
+ this.w = opts.w || this.container.offsetWidth
46
+ /** @type {number} */
47
+ this.h = opts.h || this.container.offsetHeight
48
+
49
+ // Create the main canvas (for rendering to screen)
50
+ /** @type {HTMLCanvasElement} */
51
+ this._canvas = opts.canvas || this.initialiseCanvas()
52
+
53
+ /** Initialize GeoCanvas
54
+ * @type {GeoCanvas}
55
+ * @private */
56
+ this.geoCanvas = new GeoCanvas(this._canvas, opts.x, opts.y, opts.z, opts)
57
+
58
+ this.geoCanvas.redraw = () => {
59
+ this.redraw()
60
+ }
61
+
62
+ // legend div
63
+ this.legendDivId = opts.legendDivId || 'gvizLegend'
64
+ this.legend = select('#' + this.legendDivId)
65
+ if (this.legend.empty()) this.initialiseLegend()
66
+
67
+ //tooltip
68
+
69
+ // set App container as default parent element for tooltip
70
+ if (!opts.tooltip) opts.tooltip = {}
71
+ if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
72
+
73
+ /**
74
+ * @private
75
+ * @type {Tooltip} */
76
+ this.tooltip = new Tooltip(opts.tooltip)
77
+
78
+ // add event listeners to container
79
+ this.mouseOverHandler = (e) => this.focusCell(e)
80
+ this.mouseMoveHandler = (e) => this.focusCell(e)
81
+ this.mouseOutHandler = (e) => this.tooltip.hide()
82
+ this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
83
+ this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
84
+ this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
85
+
86
+ // listen for resize events on the App's container and handle them
87
+ this.defineResizeObserver()
88
+
89
+ // add extra logic to onZoomStartFun
90
+ this.geoCanvas.onZoomStartFun = (e) => {
91
+ if (opts.onZoomStartFun) opts.onZoomStartFun(e)
92
+ this.tooltip.hide()
93
+ }
94
+
95
+ //for mouse over
96
+ /**
97
+ * @private
98
+ * @type {HTMLCanvasElement|null} */
99
+ this.canvasSave = null
100
+
101
+ this.selectionRectangleColor = opts.selectionRectangleColor || '#FF6347'
102
+ this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,z) => {}
103
+
104
+ // transparent background (e.g. leaflet) 'red painting' fix
105
+ this.transparentBackground = opts.transparentBackground
106
+
107
+ //set default globalCompositeOperation
108
+ this.defaultGlobalCompositeOperation =
109
+ opts.defaultGlobalCompositeOperation || this.geoCanvas.ctx.globalCompositeOperation
110
+ }
111
+
112
+ /**
113
+ * @protected
114
+ * @returns {HTMLCanvasElement}
115
+ */
116
+ initialiseCanvas() {
117
+ const canvas = document.createElement('canvas')
118
+ canvas.setAttribute('width', '' + this.w)
119
+ canvas.setAttribute('height', '' + this.h)
120
+ this.container.appendChild(canvas)
121
+ return canvas
122
+ }
123
+
124
+ initialiseLegend() {
125
+ this.legend = select(this.container.id && this.container.id != '' ? '#' + this.container.id : 'body')
126
+ .append('div')
127
+ .attr('id', this.legendDivId)
128
+ .style('position', 'absolute')
129
+ .style('width', 'auto')
130
+ .style('height', 'auto')
131
+ .style('background', '#FFFFFF')
132
+ //.style("padding", this.padding)
133
+ .style('border', '0px')
134
+ //.style('border-radius', '5px')
135
+ .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
136
+ .style('font-family', 'Helvetica, Arial, sans-serif')
137
+ .style('bottom', '15px')
138
+ .style('right', '15px')
139
+ //hide
140
+ //.style("visibility", "hidden")
141
+ }
142
+
143
+ /**
144
+ * Set/get layer stack.
145
+ *
146
+ * @param {undefined|import("./Layer.js").Layer|import("./Layer.js").Layer[]} layers
147
+ * @returns { this | import("./Layer.js").Layer[] }
148
+ */
149
+ layers_(layers) {
150
+ if (arguments.length === 0) return this.layers
151
+ if (arguments.length === 1)
152
+ if (Array.isArray(layers)) this.layers = layers
153
+ else this.layers = [layers]
154
+ else this.layers = arguments
155
+ return this
156
+ }
157
+
158
+ /** @returns {this} */
159
+ redraw() {
160
+ //remove legend elements
161
+ if (this.legend) this.legend.selectAll('*').remove()
162
+
163
+ //clear
164
+ this.geoCanvas.initCanvasTransform()
165
+ this.geoCanvas.clear(this.geoCanvas.backgroundColor)
166
+
167
+ const z = this.geoCanvas.view.z
168
+ this.updateExtentGeo()
169
+
170
+ //go through the layers
171
+ for (const layer of this.layers) {
172
+ //check if layer is visible
173
+ if (layer.visible && !layer.visible(z)) continue
174
+
175
+ //set layer alpha and blend mode
176
+ this.geoCanvas.offscreenCtx.globalAlpha = layer.alpha ? layer.alpha(z) : 1.0
177
+ if (layer.blendOperation) this.geoCanvas.offscreenCtx.globalCompositeOperation = layer.blendOperation(z)
178
+
179
+ //set affin transform to draw with geographical coordinates
180
+ this.geoCanvas.setCanvasTransform()
181
+
182
+ //draw layer
183
+ layer.draw(this.geoCanvas, this.legend)
184
+
185
+ //draw layer filter
186
+ if (layer.filterColor) layer.drawFilter(this.geoCanvas)
187
+
188
+ //restore default alpha and blend operation
189
+ this.geoCanvas.offscreenCtx.globalAlpha = 1.0
190
+ this.geoCanvas.offscreenCtx.globalCompositeOperation = this.defaultGlobalCompositeOperation
191
+ }
192
+
193
+ // one drawImage call: draw the offscreen canvas to the main canvas
194
+ this.geoCanvas.initCanvasTransform()
195
+ this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
196
+
197
+ this.canvasSave = null
198
+
199
+ return this
200
+ }
201
+
202
+ /**
203
+ * @param {number} marginPx
204
+ * @returns {import('./GeoCanvas.js').Envelope}
205
+ * @public
206
+ */
207
+ updateExtentGeo(marginPx = 20) {
208
+ return this.geoCanvas.updateExtentGeo(marginPx)
209
+ }
210
+
211
+ /** @param {MouseEvent} e */
212
+ focusCell(e) {
213
+ //compute mouse geo position
214
+ const mousePositionGeo = {
215
+ x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
216
+ y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
217
+ }
218
+ /** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
219
+ const focus = this.getCellFocusInfo(mousePositionGeo)
220
+
221
+ // transparent background (e.g. leaflet) 'red painting' fix
222
+ if (this.transparentBackground) {
223
+ if (focus) {
224
+ this.tooltip.html(focus.html)
225
+ this.tooltip.setPosition(e)
226
+ this.tooltip.show()
227
+ } else {
228
+ this.tooltip.hide()
229
+ }
230
+ this.canvasSave = document.createElement('canvas')
231
+ this.canvasSave.setAttribute('width', '' + this.w)
232
+ this.canvasSave.setAttribute('height', '' + this.h)
233
+ this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.canvas, 0, 0)
234
+ this.geoCanvas.initCanvasTransform()
235
+ return
236
+ }
237
+
238
+ if (focus) {
239
+ this.tooltip.html(focus.html)
240
+ this.tooltip.setPosition(e)
241
+ this.tooltip.show()
242
+
243
+ //show cell position as a rectangle
244
+ if (!this.canvasSave) {
245
+ this.canvasSave = document.createElement('canvas')
246
+ this.canvasSave.setAttribute('width', '' + this.w)
247
+ this.canvasSave.setAttribute('height', '' + this.h)
248
+ this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
249
+ } else {
250
+ this.geoCanvas.offscreenCtx.drawImage(this.canvasSave, 0, 0)
251
+ }
252
+
253
+ //draw image saved + draw rectangle
254
+ const rectWPix = this.selectionRectangleWidthPix
255
+ ? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
256
+ : 4
257
+ this.geoCanvas.initCanvasTransform()
258
+ const ctx = this.geoCanvas.offscreenCtx
259
+ ctx.strokeStyle = this.selectionRectangleColor
260
+ ctx.lineWidth = rectWPix
261
+ ctx.beginPath()
262
+
263
+ ctx.rect(
264
+ this.geoCanvas.geoToPixX(focus.cell.x) - rectWPix / 2,
265
+ this.geoCanvas.geoToPixY(focus.cell.y) + rectWPix / 2,
266
+ focus.resolution / this.geoCanvas.view.z + rectWPix,
267
+ -focus.resolution / this.geoCanvas.view.z - rectWPix
268
+ )
269
+ ctx.stroke()
270
+ this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
271
+ } else {
272
+ this.tooltip.hide()
273
+ if (this.canvasSave) this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Return the cell HTML info at a given geo position.
279
+ * This is usefull for user interactions, to show this info where the user clicks for example.
280
+ *
281
+ * @param {{x:number,y:number}} posGeo
282
+ * @returns {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined}
283
+ * @protected
284
+ */
285
+ getCellFocusInfo(posGeo) {
286
+ //go through the layers, starting from top
287
+ const z = this.geoCanvas.view.z
288
+ for (let i = this.layers.length - 1; i >= 0; i--) {
289
+ /** @type {import("./Layer.js").Layer} */
290
+ const layer = this.layers[i]
291
+ if (layer.visible && !layer.visible(z)) continue
292
+ if (layer.cellInfoHTML === 'none') continue // this is necessary in order to not show tooltips for layers 'on top' (e.g. population circles on top of squares)
293
+ if (!layer.cellInfoHTML) continue
294
+ if (!layer.getDataset) continue
295
+ const dsc = layer.getDataset(z)
296
+ if (!dsc) continue
297
+
298
+ //get cell at mouse position
299
+ /** @type {import('./Dataset.js').Cell|undefined} */
300
+ const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
301
+ //console.log(cell, dsc.resolution)
302
+ if (!cell) return undefined
303
+
304
+ //rare case for a dataset with mixed resolutions
305
+ if (dsc.mixedResolution) {
306
+ const r = +dsc.mixedResolution(cell)
307
+ const html = layer.cellInfoHTML(cell, r)
308
+ if (!html) return undefined
309
+ return { cell: cell, html: html, resolution: r }
310
+ }
311
+
312
+ const html = layer.cellInfoHTML(cell, dsc.getResolution())
313
+ if (!html) return undefined
314
+ return { cell: cell, html: html, resolution: dsc.getResolution() }
315
+ }
316
+ }
317
+
318
+ /**
319
+ * @param {number} x
320
+ * @param {number} y
321
+ * @param {number|undefined} z
322
+ */
323
+ setView(x, y, z = undefined) {
324
+ this.geoCanvas.view.x = x
325
+ this.geoCanvas.view.y = y
326
+ if (z != undefined) this.geoCanvas.view.z = z
327
+ return this
328
+ }
329
+
330
+ /** @returns {import('./GeoCanvas.js').View} */
331
+ getView() {
332
+ return this.geoCanvas.view
333
+ }
334
+
335
+ /** @returns {number} */
336
+ getZoom() {
337
+ return this.geoCanvas.view.z
338
+ }
339
+ /** @param {number} z @returns {this} */
340
+ setZoom(z) {
341
+ this.geoCanvas.view.z = z
342
+ return this
343
+ }
344
+
345
+ /** @returns {Array.<number|undefined>} */
346
+ getCenterExtent() {
347
+ return this.geoCanvas.getCenterExtent()
348
+ }
349
+ /** @param {Array.<number>} val @returns {this} */
350
+ setCenterExtent(val) {
351
+ this.geoCanvas.setCenterExtent(val)
352
+ return this
353
+ }
354
+
355
+ /** @returns {Array.<number>} */
356
+ getZoomExtent() {
357
+ return this.geoCanvas.getZoomExtent()
358
+ }
359
+ /** @param {Array.<number>} val @returns {this} */
360
+ setZoomExtent(val) {
361
+ this.geoCanvas.setZoomExtent(val)
362
+ return this
363
+ }
364
+
365
+ /** @returns {string} */
366
+ getBackgroundColor() {
367
+ return this.geoCanvas.backgroundColor
368
+ }
369
+ /** @param {string} val @returns {this} */
370
+ setBackgroundColor(val) {
371
+ this.geoCanvas.backgroundColor = val
372
+ return this
373
+ }
374
+
375
+ /**
376
+ * Adds a set of zoom buttons to the map
377
+ *
378
+ * @param {object} opts
379
+ * @returns {this}
380
+ */
381
+ addZoomButtons(opts) {
382
+ // * opts.id
383
+ // * opts.onZoom - custom event handler function
384
+ // * opts.x
385
+ // * opts.y
386
+ // * opts.delta - zoom delta applied on each click
387
+
388
+ this.zoomButtons = new ZoomButtons({
389
+ map: this,
390
+ id: opts?.id || 'gridviz-zoom-buttons-' + this.container.id,
391
+ class: opts?.class,
392
+ x: opts?.x,
393
+ y: opts?.y,
394
+ onZoom: opts?.onZoom,
395
+ delta: opts?.delta || 0.2,
396
+ })
397
+
398
+ return this
399
+ }
400
+
401
+ /**
402
+ * Adds a fullscreen toggle button to the app
403
+ *
404
+ * @param {object} opts
405
+ * @returns {this}
406
+ */
407
+ addFullscreenButton(opts) {
408
+ // * opts.map - the gridviz map
409
+ // * opts.id
410
+ // * opts.x
411
+ // * opts.y
412
+
413
+ this.fullscreenButton = new FullscreenButton({
414
+ map: this,
415
+ id: opts?.id || 'gridviz-fullscreen-button',
416
+ class: opts?.class,
417
+ x: opts?.x,
418
+ y: opts?.y,
419
+ })
420
+
421
+ return this
422
+ }
423
+
424
+ /** @returns {this} */
425
+ setViewFromURL() {
426
+ this.geoCanvas.setViewFromURL()
427
+ return this
428
+ }
429
+
430
+ /**
431
+ * @description Add a resize event observer to the Apps container and update the canvas accordingly
432
+ * @memberof App
433
+ */
434
+ defineResizeObserver() {
435
+ // listen to resize events
436
+ const resizeObserver = new ResizeObserver((entries) => {
437
+ let container = this.container
438
+ // make sure canvas has been built
439
+ if (container.clientWidth > 0 && container.clientHeight > 0) {
440
+ // make sure we dont exceed loop limit first
441
+ // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
442
+ window.requestAnimationFrame(() => {
443
+ if (!Array.isArray(entries) || !entries.length) {
444
+ return
445
+ }
446
+ // update the map and canvas size
447
+ if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
448
+ this.h = container.clientHeight
449
+ this.w = container.clientWidth
450
+ this.geoCanvas.h = container.clientHeight
451
+ this.geoCanvas.w = container.clientWidth
452
+ this.geoCanvas.canvas.setAttribute('width', '' + this.w)
453
+ this.geoCanvas.canvas.setAttribute('height', '' + this.h)
454
+ // offscreen canvas
455
+ this.geoCanvas.offscreenCanvas.setAttribute('width', '' + this.w)
456
+ this.geoCanvas.offscreenCanvas.setAttribute('height', '' + this.h)
457
+ this.redraw()
458
+
459
+ //update button positions
460
+ // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
461
+ // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
462
+ }
463
+ })
464
+ }
465
+ })
466
+
467
+ resizeObserver.observe(this.container)
468
+ }
469
+
470
+ /**
471
+ * @description Destroy the map and it's event listeners
472
+ * This should significantly reduce the memory used when creating and destroying gridviz map instances (for example in leaflet-gridviz)
473
+ * @memberof App
474
+ */
475
+ destroy() {
476
+ // clear layers
477
+ this.layers = []
478
+ this.bgLayers = []
479
+
480
+ // remove event listeners from container
481
+ this.container.removeEventListener('mouseover', this.mouseOverHandler)
482
+ this.container.removeEventListener('mousemove', this.mouseMoveHandler)
483
+ this.container.removeEventListener('mouseout', this.mouseOutHandler)
484
+
485
+ // remove canvas
486
+ this.geoCanvas.canvas.remove()
487
+
488
+ // remove legend
489
+ this.legend?.remove()
490
+
491
+ // remove tooltip
492
+ this.tooltip.tooltip?.remove()
493
+ }
494
+ }