gridviz 3.0.35 → 3.0.37

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 +21056 -17487
  3. package/dist/gridviz.min.js +2 -2
  4. package/package.json +74 -74
  5. package/src/button/Button.js +67 -67
  6. package/src/button/FullscreenButton.js +121 -121
  7. package/src/button/ZoomButtons.js +103 -103
  8. package/src/core/Dataset.js +232 -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 +650 -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 +311 -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 +311 -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 +114 -118
  60. package/src/utils/WebGLSquareColoringAdvanced.js +223 -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 +178 -73
  67. package/src/utils/zoomUtils.js +142 -142
package/src/core/Map.js CHANGED
@@ -1,568 +1,650 @@
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
- this.geoCanvas.cancelCurrentRequests = () => {
62
- // when the zoom level changes, avoid drawing outdated tiles, and ensure that requests are properly aborted when necessary
63
- for (const layer of this.layers) {
64
- //multires
65
- if (layer.dataset?.datasets) {
66
- for (const dataset of layer.dataset?.datasets) {
67
- if (dataset?.cancelCurrentRequests) dataset.cancelCurrentRequests()
68
- }
69
- }
70
- //single res
71
- if (layer.dataset?.cancelCurrentRequests) layer.dataset?.cancelCurrentRequests()
72
- }
73
- }
74
-
75
- // legend div
76
- this.legend = opts.legendContainer
77
- ? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
78
- : null
79
- this._legendProvidedByUser = !!opts.legendContainer
80
- if (!this.legend) this.initialiseLegend()
81
-
82
- //tooltip
83
-
84
- // set App container as default parent element for tooltip
85
- if (!opts.tooltip) opts.tooltip = {}
86
- if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
87
-
88
- /**
89
- * @private
90
- * @type {Tooltip} */
91
- this.tooltip = new Tooltip(opts.tooltip)
92
-
93
- /**
94
- * Set to true so that the tooltip appears for the first layer starting from top where a cell is found.
95
- * Otherwise, it shows a tooltip only for the first layer from the top with a cellInfoHTML property, even if there is no cell at the mouse position for this layer.
96
- * @type {boolean} */
97
- this.digForTooltip = opts.digForTooltip || false
98
-
99
- // add event listeners to container
100
- this.mouseOverHandler = (e) => this.focusCell(e)
101
- this.mouseMoveHandler = (e) => this.focusCell(e)
102
- this.mouseOutHandler = (e) => this.tooltip.hide()
103
- this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
104
- this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
105
- this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
106
-
107
- // listen for resize events on the App's container and handle them
108
- this.defineResizeObserver()
109
-
110
- // add extra logic to onZoomStartFun
111
- this.geoCanvas.onZoomStartFun = (e) => {
112
- if (opts.onZoomStartFun) opts.onZoomStartFun(e)
113
- this.tooltip.hide()
114
- }
115
-
116
- //for mouse over
117
- /**
118
- * @private
119
- * @type {HTMLCanvasElement|null} */
120
- this.canvasSave = null
121
-
122
- this.selectionRectangleColor = opts.selectionRectangleColor || '#FF6347'
123
- this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,z) => {}
124
-
125
- // transparent background (e.g. leaflet) 'red painting' fix
126
- this.transparentBackground = opts.transparentBackground
127
-
128
- //set default globalCompositeOperation
129
- this.defaultGlobalCompositeOperation =
130
- opts.defaultGlobalCompositeOperation || this.geoCanvas.ctx.globalCompositeOperation
131
- }
132
-
133
- /**
134
- * @protected
135
- * @returns {HTMLCanvasElement}
136
- */
137
- initialiseCanvas() {
138
- const canvas = document.createElement('canvas')
139
- canvas.setAttribute('width', '' + this.w)
140
- canvas.setAttribute('height', '' + this.h)
141
- this.container.appendChild(canvas)
142
- return canvas
143
- }
144
-
145
- initialiseLegend() {
146
- this.legend = select(this.container)
147
- .append('div') // Create a new container
148
- .attr('id', 'gridviz-legend')
149
- .style('position', 'absolute')
150
- .style('width', 'auto')
151
- .style('height', 'auto')
152
- .style('background', '#FFFFFF')
153
- //.style("padding", this.padding)
154
- .style('border', '0px')
155
- //.style('border-radius', '5px')
156
- .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
157
- .style('font-family', 'Helvetica, Arial, sans-serif')
158
- .style('bottom', '15px')
159
- .style('right', '15px')
160
- //hide
161
- //.style("visibility", "hidden")
162
- }
163
-
164
- /**
165
- * Set/get layer stack.
166
- *
167
- * @param {undefined|import("./Layer.js").Layer|import("./Layer.js").Layer[]} layers
168
- * @returns { this | import("./Layer.js").Layer[] }
169
- */
170
- layers_(layers) {
171
- if (arguments.length === 0) return this.layers
172
- if (arguments.length === 1)
173
- if (Array.isArray(layers)) this.layers = layers
174
- else this.layers = [layers]
175
- else this.layers = arguments
176
- return this
177
- }
178
-
179
- /** @returns {this} */
180
- redraw() {
181
- //remove legend elements
182
- if (this.legend) this.legend.selectAll('*').remove()
183
-
184
- //clear
185
- this.geoCanvas.initCanvasTransform()
186
- this.geoCanvas.clear(this.geoCanvas.backgroundColor)
187
-
188
- const z = this.geoCanvas.view.z
189
- this.updateExtentGeo()
190
-
191
- const ctx = this.geoCanvas.offscreenCtx
192
-
193
- //go through the layers
194
- for (const layer of this.layers) {
195
- //check if layer is visible
196
- if (layer.visible && !layer.visible(z)) continue
197
-
198
- //set layer alpha and blend mode
199
- if (layer.alpha || layer.blendOperation) {
200
- ctx.save()
201
- if (layer.alpha) ctx.globalAlpha = layer.alpha(z)
202
- if (layer.blendOperation) ctx.globalCompositeOperation = layer.blendOperation(z)
203
- }
204
-
205
- //set affin transform to draw with geographical coordinates
206
- this.geoCanvas.setCanvasTransform()
207
-
208
- //draw layer
209
- layer.draw(this.geoCanvas, this.legend)
210
-
211
- //draw layer filter
212
- if (layer.filterColor) layer.drawFilter(this.geoCanvas)
213
-
214
- //restore default alpha and blend operation
215
- if (layer.alpha || layer.blendOperation) ctx.restore()
216
- }
217
-
218
- // one drawImage call: draw the offscreen canvas to the main canvas
219
- this.geoCanvas.initCanvasTransform()
220
- this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
221
-
222
- this.canvasSave = null
223
-
224
- return this
225
- }
226
-
227
- /**
228
- * @param {number} marginPx
229
- * @returns {import('./GeoCanvas.js').Envelope}
230
- * @public
231
- */
232
- updateExtentGeo(marginPx = 20) {
233
- return this.geoCanvas.updateExtentGeo(marginPx)
234
- }
235
-
236
- /** @param {MouseEvent} e */
237
- focusCell(e) {
238
- // Don’t process hover events during active zoom
239
- if (this.geoCanvas._isZooming) return;
240
-
241
- //compute mouse geo position
242
- const mousePositionGeo = {
243
- x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
244
- y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
245
- }
246
- /** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
247
- const focus = this.getCellFocusInfo(mousePositionGeo)
248
-
249
- // Transparent background (Leaflet): redraw base, then draw focus rect directly on the visible canvas
250
- if (this.transparentBackground) {
251
- // Always restore the base (offscreen) into the visible canvas
252
- this.geoCanvas.initCanvasTransform();
253
- this.geoCanvas.ctx.clearRect(0, 0, this.w, this.h);
254
- this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0);
255
-
256
- if (focus) {
257
- this.tooltip.html(focus.html);
258
- this.tooltip.setPosition(e);
259
- this.tooltip.show();
260
-
261
- const rectWPix = this.selectionRectangleWidthPix
262
- ? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
263
- : 4;
264
-
265
- const ctx = this.geoCanvas.ctx; // draw directly on visible canvas
266
- ctx.save();
267
- ctx.strokeStyle = this.selectionRectangleColor;
268
- ctx.lineWidth = rectWPix;
269
- ctx.beginPath();
270
- // compute in pixel space (no geo transform on ctx)
271
- const xPix = this.geoCanvas.geoToPixX(focus.cell.x);
272
- const yPix = this.geoCanvas.geoToPixY(focus.cell.y);
273
- const wPix = focus.resolution / this.geoCanvas.view.z;
274
- const hPix = -wPix; // y axis inverted in geoToPix
275
- ctx.rect(
276
- xPix - rectWPix / 2,
277
- yPix + rectWPix / 2,
278
- wPix + rectWPix,
279
- hPix - rectWPix
280
- );
281
- ctx.stroke();
282
- ctx.restore();
283
- } else {
284
- this.tooltip.hide();
285
- }
286
- return; // handled
287
- }
288
-
289
- if (focus) {
290
- this.geoCanvas.canvas.style.cursor = 'pointer';
291
- this.tooltip.html(focus.html)
292
- this.tooltip.setPosition(e)
293
- this.tooltip.show()
294
-
295
- //show cell position as a rectangle
296
- if (!this.canvasSave) {
297
- this.canvasSave = document.createElement('canvas')
298
- this.canvasSave.setAttribute('width', '' + this.w)
299
- this.canvasSave.setAttribute('height', '' + this.h)
300
- this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
301
- } else {
302
- this.geoCanvas.offscreenCtx.drawImage(this.canvasSave, 0, 0)
303
- }
304
-
305
- //draw image saved + draw rectangle
306
- const rectWPix = this.selectionRectangleWidthPix
307
- ? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
308
- : 4
309
- this.geoCanvas.initCanvasTransform()
310
- const ctx = this.geoCanvas.offscreenCtx
311
- ctx.strokeStyle = this.selectionRectangleColor
312
- ctx.lineWidth = rectWPix
313
- ctx.beginPath()
314
-
315
- ctx.rect(
316
- this.geoCanvas.geoToPixX(focus.cell.x) - rectWPix / 2,
317
- this.geoCanvas.geoToPixY(focus.cell.y) + rectWPix / 2,
318
- focus.resolution / this.geoCanvas.view.z + rectWPix,
319
- -focus.resolution / this.geoCanvas.view.z - rectWPix
320
- )
321
- ctx.stroke()
322
- this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
323
- } else {
324
- this.geoCanvas.canvas.style.cursor = 'default';
325
- this.tooltip.hide()
326
- if (this.canvasSave) this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
327
- }
328
- }
329
-
330
- /**
331
- * Return the cell HTML info at a given geo position.
332
- * This is usefull for user interactions, to show this info where the user clicks for example.
333
- *
334
- * @param {{x:number,y:number}} posGeo
335
- * @returns {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined}
336
- * @protected
337
- */
338
- getCellFocusInfo(posGeo) {
339
-
340
- // go through the layers, starting from top one
341
- const z = this.geoCanvas.view.z
342
- for (let i = this.layers.length - 1; i >= 0; i--) {
343
-
344
- /** @type {import("./Layer.js").Layer} */
345
- const layer = this.layers[i]
346
-
347
- if (layer.visible && !layer.visible(z)) continue
348
- if (layer.cellInfoHTML === 'none') continue
349
- if (!layer.cellInfoHTML) continue
350
- if (!layer.getDataset) continue
351
-
352
- // get dataset
353
- const dsc = layer.getDataset(z)
354
- if (!dsc) continue
355
-
356
- //get cell at mouse position
357
- /** @type {import('./Dataset.js').Cell|undefined} */
358
- const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
359
-
360
- // no cell found at theis position for this layer
361
- if (!cell)
362
- // dig to the next layer below
363
- if (this.digForTooltip) continue
364
- // no tooltip to show
365
- else return undefined
366
-
367
- //rare case for a dataset with mixed resolutions
368
- if (dsc.mixedResolution) {
369
- const r = +dsc.mixedResolution(cell)
370
- const html = layer.cellInfoHTML(cell, r)
371
- if (!html) return undefined
372
- return { cell: cell, html: html, resolution: r }
373
- }
374
-
375
- const html = layer.cellInfoHTML(cell, dsc.getResolution())
376
- if (!html) return undefined
377
- return { cell: cell, html: html, resolution: dsc.getResolution() }
378
- }
379
- }
380
-
381
- /**
382
- * @param {number} x
383
- * @param {number} y
384
- * @param {number|undefined} z
385
- */
386
- setView(x, y, z = undefined) {
387
- this.geoCanvas.view.x = x
388
- this.geoCanvas.view.y = y
389
- if (z != undefined) this.geoCanvas.view.z = z
390
- return this
391
- }
392
-
393
- /** @returns {import('./GeoCanvas.js').View} */
394
- getView() {
395
- return this.geoCanvas.view
396
- }
397
-
398
- /** @returns {number} */
399
- getZoom() {
400
- return this.geoCanvas.view.z
401
- }
402
- /** @param {number} z @returns {this} */
403
- setZoom(z) {
404
- this.geoCanvas.view.z = z
405
- return this
406
- }
407
-
408
- /** @returns {Array.<number|undefined>} */
409
- getCenterExtent() {
410
- return this.geoCanvas.getCenterExtent()
411
- }
412
- /** @param {Array.<number>} val @returns {this} */
413
- setCenterExtent(val) {
414
- this.geoCanvas.setCenterExtent(val)
415
- return this
416
- }
417
-
418
- /** @returns {Array.<number>} */
419
- getZoomExtent() {
420
- return this.geoCanvas.getZoomExtent()
421
- }
422
- /** @param {Array.<number>} val @returns {this} */
423
- setZoomExtent(val) {
424
- this.geoCanvas.setZoomExtent(val)
425
- return this
426
- }
427
-
428
- /** @returns {string} */
429
- getBackgroundColor() {
430
- return this.geoCanvas.backgroundColor
431
- }
432
- /** @param {string} val @returns {this} */
433
- setBackgroundColor(val) {
434
- this.geoCanvas.backgroundColor = val
435
- return this
436
- }
437
-
438
- /**
439
- * Adds a set of zoom buttons to the map
440
- *
441
- * @param {object} opts
442
- * @returns {this}
443
- */
444
- addZoomButtons(opts) {
445
- // * opts.id
446
- // * opts.onZoom - custom event handler function
447
- // * opts.x
448
- // * opts.y
449
- // * opts.delta - zoom delta applied on each click
450
-
451
- this.zoomButtons = new ZoomButtons({
452
- map: this,
453
- id: opts?.id || 'gridviz-zoom-buttons-' + this.container.id,
454
- class: opts?.class,
455
- x: opts?.x,
456
- y: opts?.y,
457
- onZoom: opts?.onZoom,
458
- delta: opts?.delta || 0.2,
459
- })
460
-
461
- return this
462
- }
463
-
464
- /**
465
- * Adds a fullscreen toggle button to the app
466
- *
467
- * @param {object} opts
468
- * @returns {this}
469
- */
470
- addFullscreenButton(opts) {
471
- // * opts.map - the gridviz map
472
- // * opts.id
473
- // * opts.x
474
- // * opts.y
475
-
476
- this.fullscreenButton = new FullscreenButton({
477
- map: this,
478
- id: opts?.id || 'gridviz-fullscreen-button',
479
- class: opts?.class,
480
- x: opts?.x,
481
- y: opts?.y,
482
- })
483
-
484
- return this
485
- }
486
-
487
- /** @returns {this} */
488
- setViewFromURL() {
489
- this.geoCanvas.setViewFromURL()
490
- return this
491
- }
492
-
493
- /**
494
- * @description Add a resize event observer to the Apps container and update the canvas accordingly
495
- * @memberof App
496
- */
497
- defineResizeObserver() {
498
- // Track whether the observer is currently processing a resize event
499
- let resizePending = false
500
-
501
- const resizeObserver = new ResizeObserver((entries) => {
502
- if (!Array.isArray(entries) || !entries.length) return
503
-
504
- let container = this.container
505
-
506
- // Ensure the container has valid dimensions
507
- if (container.clientWidth > 0 && container.clientHeight > 0) {
508
- if (!resizePending) {
509
- resizePending = true // Prevent overlapping resize triggers
510
-
511
- window.requestAnimationFrame(() => {
512
- resizePending = false // Reset the flag after processing
513
-
514
- // Check for size changes
515
- if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
516
- this.h = container.clientHeight
517
- this.w = container.clientWidth
518
-
519
- // Update geoCanvas sizes
520
- this.geoCanvas.h = this.h
521
- this.geoCanvas.w = this.w
522
- this.geoCanvas.canvas.setAttribute('width', String(this.w))
523
- this.geoCanvas.canvas.setAttribute('height', String(this.h))
524
- this.geoCanvas.offscreenCanvas.setAttribute('width', String(this.w))
525
- this.geoCanvas.offscreenCanvas.setAttribute('height', String(this.h))
526
-
527
- this.redraw()
528
-
529
- // Optionally reposition UI elements
530
- // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px';
531
- // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px';
532
- }
533
- })
534
- }
535
- }
536
- })
537
-
538
- resizeObserver.observe(this.container)
539
- }
540
-
541
- /**
542
- * @description Destroy the map and it's event listeners
543
- * This should significantly reduce the memory used when creating and destroying gridviz map instances (for example in leaflet-gridviz)
544
- * @memberof App
545
- */
546
- destroy() {
547
- // clear layers
548
- this.layers = []
549
- this.bgLayers = []
550
-
551
- // remove event listeners from container
552
- this.container.removeEventListener('mouseover', this.mouseOverHandler)
553
- this.container.removeEventListener('mousemove', this.mouseMoveHandler)
554
- this.container.removeEventListener('mouseout', this.mouseOutHandler)
555
-
556
- // remove canvas
557
- this.geoCanvas.canvas.remove()
558
-
559
- // remove legend
560
- // only remove legend if gridviz created it
561
- if (!this._legendProvidedByUser) {
562
- this.legend?.remove()
563
- }
564
-
565
- // remove tooltip
566
- this.tooltip.tooltip?.remove()
567
- }
568
- }
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
+ /**
34
+ * When true, redraw() schedules one redraw for the next animation frame instead of
35
+ * drawing at once, so the many redraw requests of a busy moment (every tile that
36
+ * arrives, every info.json, every setting change) are drawn once. redrawNow() still
37
+ * draws at once. Off by default: redraw() stays synchronous, as it always was.
38
+ * @type {boolean} */
39
+ this.asyncRedraw = !!opts.asyncRedraw
40
+ /** @private @type {number|null} */
41
+ this._redrawFrame = null
42
+
43
+ //get container element
44
+ this.container = container || document.getElementById('gridviz')
45
+ if (!this.container) {
46
+ console.error('Cannot find gridviz container element.')
47
+ return
48
+ }
49
+
50
+ //https://css-tricks.com/absolute-positioning-inside-relative-positioning/
51
+ this.container.style.position = 'relative' // container element must have relative positioning
52
+
53
+ //set dimensions
54
+ /** @type {number} */
55
+ this.w = opts.w || this.container.offsetWidth
56
+ /** @type {number} */
57
+ this.h = opts.h || this.container.offsetHeight
58
+
59
+ // Create the main canvas (for rendering to screen)
60
+ /** @type {HTMLCanvasElement} */
61
+ this._canvas = opts.canvas || this.initialiseCanvas()
62
+
63
+ /** Initialize GeoCanvas
64
+ * @type {GeoCanvas}
65
+ * @private */
66
+ this.geoCanvas = new GeoCanvas(this._canvas, opts.x, opts.y, opts.z, opts)
67
+
68
+ this.geoCanvas.redraw = () => {
69
+ this.redraw()
70
+ }
71
+ this.geoCanvas.cancelCurrentRequests = () => {
72
+ // when the zoom level changes, avoid drawing outdated tiles, and ensure that requests are properly aborted when necessary
73
+ for (const layer of this.layers) {
74
+ //multires
75
+ if (layer.dataset?.datasets) {
76
+ for (const dataset of layer.dataset?.datasets) {
77
+ if (dataset?.cancelCurrentRequests) dataset.cancelCurrentRequests()
78
+ }
79
+ }
80
+ //single res
81
+ if (layer.dataset?.cancelCurrentRequests) layer.dataset?.cancelCurrentRequests()
82
+ }
83
+ }
84
+
85
+ // legend div
86
+ this.legend = opts.legendContainer
87
+ ? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
88
+ : null
89
+ this._legendProvidedByUser = !!opts.legendContainer
90
+ if (!this.legend) this.initialiseLegend()
91
+
92
+ //tooltip
93
+
94
+ // set App container as default parent element for tooltip
95
+ if (!opts.tooltip) opts.tooltip = {}
96
+ if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
97
+
98
+ /**
99
+ * @private
100
+ * @type {Tooltip} */
101
+ this.tooltip = new Tooltip(opts.tooltip)
102
+
103
+ /**
104
+ * Set to true so that the tooltip appears for the first layer starting from top where a cell is found.
105
+ * Otherwise, it shows a tooltip only for the first layer from the top with a cellInfoHTML property, even if there is no cell at the mouse position for this layer.
106
+ * @type {boolean} */
107
+ this.digForTooltip = opts.digForTooltip || false
108
+
109
+ // add event listeners to the canvas. Mouse moves are handled at most once per
110
+ // animation frame (the latest one): finding the cell and redrawing the highlight
111
+ // for every intermediate event only delays the one that's shown.
112
+ /** @private */
113
+ this._focusFrame = null
114
+ /** @private */
115
+ this._focusEvent = null
116
+ const scheduleFocus = (e) => {
117
+ this._focusEvent = e
118
+ if (this._focusFrame) return
119
+ this._focusFrame = requestAnimationFrame(() => {
120
+ this._focusFrame = null
121
+ const ev = this._focusEvent
122
+ this._focusEvent = null
123
+ if (ev) this.focusCell(ev)
124
+ })
125
+ }
126
+ this.mouseOverHandler = scheduleFocus
127
+ this.mouseMoveHandler = scheduleFocus
128
+ this.mouseOutHandler = (e) => {
129
+ if (this._focusFrame) cancelAnimationFrame(this._focusFrame)
130
+ this._focusFrame = null
131
+ this._focusEvent = null
132
+ this.tooltip.hide()
133
+ }
134
+ this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
135
+ this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
136
+ this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
137
+
138
+ // listen for resize events on the App's container and handle them
139
+ this.defineResizeObserver()
140
+
141
+ // add extra logic to onZoomStartFun
142
+ this.geoCanvas.onZoomStartFun = (e) => {
143
+ if (opts.onZoomStartFun) opts.onZoomStartFun(e)
144
+ this.tooltip.hide()
145
+ }
146
+
147
+ //for mouse over
148
+ /**
149
+ * @private
150
+ * @type {HTMLCanvasElement|null} */
151
+ this.canvasSave = null
152
+
153
+ this.selectionRectangleColor = opts.selectionRectangleColor || '#FF6347'
154
+ this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,z) => {}
155
+
156
+ // transparent background (e.g. leaflet) 'red painting' fix
157
+ this.transparentBackground = opts.transparentBackground
158
+
159
+ //set default globalCompositeOperation
160
+ this.defaultGlobalCompositeOperation =
161
+ opts.defaultGlobalCompositeOperation || this.geoCanvas.ctx.globalCompositeOperation
162
+ }
163
+
164
+ /**
165
+ * @protected
166
+ * @returns {HTMLCanvasElement}
167
+ */
168
+ initialiseCanvas() {
169
+ const canvas = document.createElement('canvas')
170
+ canvas.setAttribute('width', '' + this.w)
171
+ canvas.setAttribute('height', '' + this.h)
172
+ this.container.appendChild(canvas)
173
+ return canvas
174
+ }
175
+
176
+ initialiseLegend() {
177
+ this.legend = select(this.container)
178
+ .append('div') // Create a new container
179
+ .attr('id', 'gridviz-legend')
180
+ .style('position', 'absolute')
181
+ .style('width', 'auto')
182
+ .style('height', 'auto')
183
+ .style('background', '#FFFFFF')
184
+ //.style("padding", this.padding)
185
+ .style('border', '0px')
186
+ //.style('border-radius', '5px')
187
+ .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
188
+ .style('font-family', 'Helvetica, Arial, sans-serif')
189
+ .style('bottom', '15px')
190
+ .style('right', '15px')
191
+ //hide
192
+ //.style("visibility", "hidden")
193
+ }
194
+
195
+ /**
196
+ * Set/get layer stack.
197
+ *
198
+ * @param {undefined|import("./Layer.js").Layer|import("./Layer.js").Layer[]} layers
199
+ * @returns { this | import("./Layer.js").Layer[] }
200
+ */
201
+ layers_(layers) {
202
+ if (arguments.length === 0) return this.layers
203
+ if (arguments.length === 1)
204
+ if (Array.isArray(layers)) this.layers = layers
205
+ else this.layers = [layers]
206
+ else this.layers = arguments
207
+ return this
208
+ }
209
+
210
+ /**
211
+ * Redraws the map: at once, or, with the asyncRedraw option, once at the next
212
+ * animation frame however many times it's called before then.
213
+ * @returns {this} */
214
+ redraw() {
215
+ if (this.asyncRedraw) {
216
+ if (!this._redrawFrame && !this._destroyed)
217
+ this._redrawFrame = requestAnimationFrame(() => {
218
+ this._redrawFrame = null
219
+ this.redrawNow()
220
+ })
221
+ return this
222
+ }
223
+ return this.redrawNow()
224
+ }
225
+
226
+ /**
227
+ * Redraws the map at once, whatever the asyncRedraw option (and cancels a scheduled redraw).
228
+ * @returns {this} */
229
+ redrawNow() {
230
+ if (this._redrawFrame) cancelAnimationFrame(this._redrawFrame)
231
+ this._redrawFrame = null
232
+ // a destroyed map can still be asked to redraw by a dataset it shared (see destroy)
233
+ if (this._destroyed) return this
234
+
235
+ //remove legend elements
236
+ if (this.legend) this.legend.selectAll('*').remove()
237
+
238
+ //clear
239
+ this.geoCanvas.initCanvasTransform()
240
+ this.geoCanvas.clear(this.geoCanvas.backgroundColor)
241
+
242
+ const z = this.geoCanvas.view.z
243
+ this.updateExtentGeo()
244
+
245
+ const ctx = this.geoCanvas.offscreenCtx
246
+
247
+ //go through the layers
248
+ for (const layer of this.layers) {
249
+ //check if layer is visible
250
+ if (layer.visible && !layer.visible(z)) continue
251
+
252
+ //set layer alpha and blend mode
253
+ if (layer.alpha || layer.blendOperation) {
254
+ ctx.save()
255
+ if (layer.alpha) ctx.globalAlpha = layer.alpha(z)
256
+ if (layer.blendOperation) ctx.globalCompositeOperation = layer.blendOperation(z)
257
+ }
258
+
259
+ //set affin transform to draw with geographical coordinates
260
+ this.geoCanvas.setCanvasTransform()
261
+
262
+ //draw layer
263
+ layer.draw(this.geoCanvas, this.legend)
264
+
265
+ //draw layer filter
266
+ if (layer.filterColor) layer.drawFilter(this.geoCanvas)
267
+
268
+ //restore default alpha and blend operation
269
+ if (layer.alpha || layer.blendOperation) ctx.restore()
270
+ }
271
+
272
+ // one drawImage call: draw the offscreen canvas to the main canvas
273
+ this.geoCanvas.initCanvasTransform()
274
+ this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
275
+
276
+ this.canvasSave = null
277
+ // the canvas holds no focus rectangle any more
278
+ this._focusedCell = undefined
279
+
280
+ return this
281
+ }
282
+
283
+ /**
284
+ * @param {number} marginPx
285
+ * @returns {import('./GeoCanvas.js').Envelope}
286
+ * @public
287
+ */
288
+ updateExtentGeo(marginPx = 20) {
289
+ return this.geoCanvas.updateExtentGeo(marginPx)
290
+ }
291
+
292
+ /** @param {MouseEvent} e */
293
+ focusCell(e) {
294
+ // Don’t process hover events during active zoom
295
+ if (this.geoCanvas._isZooming) return;
296
+
297
+ //compute mouse geo position
298
+ const mousePositionGeo = {
299
+ x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
300
+ y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
301
+ }
302
+ /** @type {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined} */
303
+ const focus = this.getCellFocusInfo(mousePositionGeo)
304
+
305
+ // Same cell as the one already highlighted (or still no cell): the canvas is right
306
+ // as it is, only the tooltip follows the mouse. Saves a full-canvas copy per move.
307
+ const focusedCell = focus ? focus.cell : null
308
+ if (focusedCell === this._focusedCell) {
309
+ if (focus) {
310
+ this.tooltip.setPosition(e)
311
+ this.tooltip.show()
312
+ } else this.tooltip.hide()
313
+ return
314
+ }
315
+ this._focusedCell = focusedCell
316
+
317
+ // Transparent background (Leaflet): redraw base, then draw focus rect directly on the visible canvas
318
+ if (this.transparentBackground) {
319
+ // Always restore the base (offscreen) into the visible canvas
320
+ this.geoCanvas.initCanvasTransform();
321
+ this.geoCanvas.ctx.clearRect(0, 0, this.w, this.h);
322
+ this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0);
323
+
324
+ if (focus) {
325
+ this.tooltip.html(focus.html);
326
+ this.tooltip.setPosition(e);
327
+ this.tooltip.show();
328
+
329
+ const rectWPix = this.selectionRectangleWidthPix
330
+ ? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
331
+ : 4;
332
+
333
+ const ctx = this.geoCanvas.ctx; // draw directly on visible canvas
334
+ ctx.save();
335
+ ctx.strokeStyle = this.selectionRectangleColor;
336
+ ctx.lineWidth = rectWPix;
337
+ ctx.beginPath();
338
+ // compute in pixel space (no geo transform on ctx)
339
+ const xPix = this.geoCanvas.geoToPixX(focus.cell.x);
340
+ const yPix = this.geoCanvas.geoToPixY(focus.cell.y);
341
+ const wPix = focus.resolution / this.geoCanvas.view.z;
342
+ const hPix = -wPix; // y axis inverted in geoToPix
343
+ ctx.rect(
344
+ xPix - rectWPix / 2,
345
+ yPix + rectWPix / 2,
346
+ wPix + rectWPix,
347
+ hPix - rectWPix
348
+ );
349
+ ctx.stroke();
350
+ ctx.restore();
351
+ } else {
352
+ this.tooltip.hide();
353
+ }
354
+ return; // handled
355
+ }
356
+
357
+ if (focus) {
358
+ this.geoCanvas.canvas.style.cursor = 'pointer';
359
+ this.tooltip.html(focus.html)
360
+ this.tooltip.setPosition(e)
361
+ this.tooltip.show()
362
+
363
+ //show cell position as a rectangle
364
+ if (!this.canvasSave) {
365
+ this.canvasSave = document.createElement('canvas')
366
+ this.canvasSave.setAttribute('width', '' + this.w)
367
+ this.canvasSave.setAttribute('height', '' + this.h)
368
+ this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
369
+ } else {
370
+ this.geoCanvas.offscreenCtx.drawImage(this.canvasSave, 0, 0)
371
+ }
372
+
373
+ //draw image saved + draw rectangle
374
+ const rectWPix = this.selectionRectangleWidthPix
375
+ ? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
376
+ : 4
377
+ this.geoCanvas.initCanvasTransform()
378
+ const ctx = this.geoCanvas.offscreenCtx
379
+ ctx.strokeStyle = this.selectionRectangleColor
380
+ ctx.lineWidth = rectWPix
381
+ ctx.beginPath()
382
+
383
+ ctx.rect(
384
+ this.geoCanvas.geoToPixX(focus.cell.x) - rectWPix / 2,
385
+ this.geoCanvas.geoToPixY(focus.cell.y) + rectWPix / 2,
386
+ focus.resolution / this.geoCanvas.view.z + rectWPix,
387
+ -focus.resolution / this.geoCanvas.view.z - rectWPix
388
+ )
389
+ ctx.stroke()
390
+ this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
391
+ } else {
392
+ this.geoCanvas.canvas.style.cursor = 'default';
393
+ this.tooltip.hide()
394
+ if (this.canvasSave) this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
395
+ }
396
+ }
397
+
398
+ /**
399
+ * Return the cell HTML info at a given geo position.
400
+ * This is usefull for user interactions, to show this info where the user clicks for example.
401
+ *
402
+ * @param {{x:number,y:number}} posGeo
403
+ * @returns {{cell:import('./Dataset.js').Cell,html:string,resolution:number} | undefined}
404
+ * @protected
405
+ */
406
+ getCellFocusInfo(posGeo) {
407
+
408
+ // go through the layers, starting from top one
409
+ const z = this.geoCanvas.view.z
410
+ for (let i = this.layers.length - 1; i >= 0; i--) {
411
+
412
+ /** @type {import("./Layer.js").Layer} */
413
+ const layer = this.layers[i]
414
+
415
+ if (layer.visible && !layer.visible(z)) continue
416
+ if (layer.cellInfoHTML === 'none') continue
417
+ if (!layer.cellInfoHTML) continue
418
+ if (!layer.getDataset) continue
419
+
420
+ // get dataset
421
+ const dsc = layer.getDataset(z)
422
+ if (!dsc) continue
423
+
424
+ //get cell at mouse position
425
+ /** @type {import('./Dataset.js').Cell|undefined} */
426
+ const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
427
+
428
+ // no cell found at theis position for this layer
429
+ if (!cell)
430
+ // dig to the next layer below
431
+ if (this.digForTooltip) continue
432
+ // no tooltip to show
433
+ else return undefined
434
+
435
+ //rare case for a dataset with mixed resolutions
436
+ if (dsc.mixedResolution) {
437
+ const r = +dsc.mixedResolution(cell)
438
+ const html = layer.cellInfoHTML(cell, r)
439
+ if (!html) return undefined
440
+ return { cell: cell, html: html, resolution: r }
441
+ }
442
+
443
+ const html = layer.cellInfoHTML(cell, dsc.getResolution())
444
+ if (!html) return undefined
445
+ return { cell: cell, html: html, resolution: dsc.getResolution() }
446
+ }
447
+ }
448
+
449
+ /**
450
+ * @param {number} x
451
+ * @param {number} y
452
+ * @param {number|undefined} z
453
+ */
454
+ setView(x, y, z = undefined) {
455
+ this.geoCanvas.view.x = x
456
+ this.geoCanvas.view.y = y
457
+ if (z != undefined) this.geoCanvas.view.z = z
458
+ return this
459
+ }
460
+
461
+ /** @returns {import('./GeoCanvas.js').View} */
462
+ getView() {
463
+ return this.geoCanvas.view
464
+ }
465
+
466
+ /** @returns {number} */
467
+ getZoom() {
468
+ return this.geoCanvas.view.z
469
+ }
470
+ /** @param {number} z @returns {this} */
471
+ setZoom(z) {
472
+ this.geoCanvas.view.z = z
473
+ return this
474
+ }
475
+
476
+ /** @returns {Array.<number|undefined>} */
477
+ getCenterExtent() {
478
+ return this.geoCanvas.getCenterExtent()
479
+ }
480
+ /** @param {Array.<number>} val @returns {this} */
481
+ setCenterExtent(val) {
482
+ this.geoCanvas.setCenterExtent(val)
483
+ return this
484
+ }
485
+
486
+ /** @returns {Array.<number>} */
487
+ getZoomExtent() {
488
+ return this.geoCanvas.getZoomExtent()
489
+ }
490
+ /** @param {Array.<number>} val @returns {this} */
491
+ setZoomExtent(val) {
492
+ this.geoCanvas.setZoomExtent(val)
493
+ return this
494
+ }
495
+
496
+ /** @returns {string} */
497
+ getBackgroundColor() {
498
+ return this.geoCanvas.backgroundColor
499
+ }
500
+ /** @param {string} val @returns {this} */
501
+ setBackgroundColor(val) {
502
+ this.geoCanvas.backgroundColor = val
503
+ return this
504
+ }
505
+
506
+ /**
507
+ * Adds a set of zoom buttons to the map
508
+ *
509
+ * @param {object} opts
510
+ * @returns {this}
511
+ */
512
+ addZoomButtons(opts) {
513
+ // * opts.id
514
+ // * opts.onZoom - custom event handler function
515
+ // * opts.x
516
+ // * opts.y
517
+ // * opts.delta - zoom delta applied on each click
518
+
519
+ this.zoomButtons = new ZoomButtons({
520
+ map: this,
521
+ id: opts?.id || 'gridviz-zoom-buttons-' + this.container.id,
522
+ class: opts?.class,
523
+ x: opts?.x,
524
+ y: opts?.y,
525
+ onZoom: opts?.onZoom,
526
+ delta: opts?.delta || 0.2,
527
+ })
528
+
529
+ return this
530
+ }
531
+
532
+ /**
533
+ * Adds a fullscreen toggle button to the app
534
+ *
535
+ * @param {object} opts
536
+ * @returns {this}
537
+ */
538
+ addFullscreenButton(opts) {
539
+ // * opts.map - the gridviz map
540
+ // * opts.id
541
+ // * opts.x
542
+ // * opts.y
543
+
544
+ this.fullscreenButton = new FullscreenButton({
545
+ map: this,
546
+ id: opts?.id || 'gridviz-fullscreen-button',
547
+ class: opts?.class,
548
+ x: opts?.x,
549
+ y: opts?.y,
550
+ })
551
+
552
+ return this
553
+ }
554
+
555
+ /** @returns {this} */
556
+ setViewFromURL() {
557
+ this.geoCanvas.setViewFromURL()
558
+ return this
559
+ }
560
+
561
+ /**
562
+ * @description Add a resize event observer to the Apps container and update the canvas accordingly
563
+ * @memberof App
564
+ */
565
+ defineResizeObserver() {
566
+ // Track whether the observer is currently processing a resize event
567
+ let resizePending = false
568
+
569
+ const resizeObserver = new ResizeObserver((entries) => {
570
+ if (!Array.isArray(entries) || !entries.length) return
571
+
572
+ let container = this.container
573
+
574
+ // Ensure the container has valid dimensions
575
+ if (container.clientWidth > 0 && container.clientHeight > 0) {
576
+ if (!resizePending) {
577
+ resizePending = true // Prevent overlapping resize triggers
578
+
579
+ window.requestAnimationFrame(() => {
580
+ resizePending = false // Reset the flag after processing
581
+
582
+ // Check for size changes
583
+ if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
584
+ this.h = container.clientHeight
585
+ this.w = container.clientWidth
586
+
587
+ // Update geoCanvas sizes
588
+ this.geoCanvas.h = this.h
589
+ this.geoCanvas.w = this.w
590
+ this.geoCanvas.canvas.setAttribute('width', String(this.w))
591
+ this.geoCanvas.canvas.setAttribute('height', String(this.h))
592
+ this.geoCanvas.offscreenCanvas.setAttribute('width', String(this.w))
593
+ this.geoCanvas.offscreenCanvas.setAttribute('height', String(this.h))
594
+
595
+ this.redraw()
596
+
597
+ // Optionally reposition UI elements
598
+ // if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px';
599
+ // if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px';
600
+ }
601
+ })
602
+ }
603
+ }
604
+ })
605
+
606
+ resizeObserver.observe(this.container)
607
+ /** @private */
608
+ this._resizeObserver = resizeObserver
609
+ }
610
+
611
+ /**
612
+ * @description Destroy the map and it's event listeners
613
+ * This should significantly reduce the memory used when creating and destroying gridviz map instances (for example in leaflet-gridviz)
614
+ * @memberof App
615
+ */
616
+ destroy() {
617
+ this._destroyed = true
618
+
619
+ // clear layers
620
+ this.layers = []
621
+ this.bgLayers = []
622
+
623
+ // cancel scheduled work
624
+ if (this._redrawFrame) cancelAnimationFrame(this._redrawFrame)
625
+ this._redrawFrame = null
626
+ if (this._focusFrame) cancelAnimationFrame(this._focusFrame)
627
+ this._focusFrame = null
628
+
629
+ // stop observing the container: the observer would otherwise keep this map alive
630
+ if (this._resizeObserver) this._resizeObserver.disconnect()
631
+ this._resizeObserver = null
632
+
633
+ // remove event listeners, from the canvas they were added to
634
+ this.geoCanvas.canvas.removeEventListener('mouseover', this.mouseOverHandler)
635
+ this.geoCanvas.canvas.removeEventListener('mousemove', this.mouseMoveHandler)
636
+ this.geoCanvas.canvas.removeEventListener('mouseout', this.mouseOutHandler)
637
+
638
+ // remove canvas
639
+ this.geoCanvas.canvas.remove()
640
+
641
+ // remove legend
642
+ // only remove legend if gridviz created it
643
+ if (!this._legendProvidedByUser) {
644
+ this.legend?.remove()
645
+ }
646
+
647
+ // remove tooltip
648
+ this.tooltip.tooltip?.remove()
649
+ }
650
+ }