gridviz 2.0.45 → 2.0.47

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 (54) hide show
  1. package/README.md +119 -119
  2. package/dist/gridviz.js +7559 -7549
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +78 -76
  5. package/src/App.js +668 -668
  6. package/src/BackgroundLayer.js +182 -182
  7. package/src/BackgroundLayerWMS.js +131 -131
  8. package/src/Dataset.js +76 -76
  9. package/src/DatasetComponent.js +101 -101
  10. package/src/GeoCanvas.js +420 -420
  11. package/src/LabelLayer.js +190 -190
  12. package/src/Layer.js +92 -92
  13. package/src/Legend.js +86 -86
  14. package/src/LineLayer.js +150 -150
  15. package/src/Style.js +114 -114
  16. package/src/Tooltip.js +172 -172
  17. package/src/dataset/COGeo.js +24 -24
  18. package/src/dataset/CSVGrid.js +104 -104
  19. package/src/dataset/GeoTIFF.js +127 -127
  20. package/src/dataset/GridTile.js +41 -41
  21. package/src/dataset/TiledGrid.js +264 -264
  22. package/src/index.js +177 -176
  23. package/src/legend/ColorCategoryLegend.js +124 -124
  24. package/src/legend/ColorDiscreteLegend.js +103 -103
  25. package/src/legend/ColorLegend.js +124 -124
  26. package/src/legend/SegmentOrientationLegend.js +87 -87
  27. package/src/legend/SegmentWidthLegend.js +97 -97
  28. package/src/legend/SizeLegend.js +141 -141
  29. package/src/style/CompositionStyle.js +300 -300
  30. package/src/style/ContourStyle.js +54 -54
  31. package/src/style/DotDensityStyle.js +156 -156
  32. package/src/style/JoyPlotStyle.js +135 -135
  33. package/src/style/LegoStyle.js +112 -112
  34. package/src/style/MosaicStyle.js +107 -107
  35. package/src/style/NinjaStarStyle.js +124 -124
  36. package/src/style/PillarStyle.js +230 -230
  37. package/src/style/SegmentStyle.js +142 -142
  38. package/src/style/ShapeColorSizeStyle.js +126 -126
  39. package/src/style/SideCatStyle.js +172 -172
  40. package/src/style/SideStyle.js +218 -218
  41. package/src/style/SquareColorCatWGLStyle.js +120 -120
  42. package/src/style/SquareColorWGLStyle.js +146 -146
  43. package/src/style/StrokeStyle.js +131 -131
  44. package/src/style/TanakaStyle.js +160 -160
  45. package/src/style/TextStyle.js +132 -132
  46. package/src/utils/Utils.js +35 -35
  47. package/src/utils/WebGLRectangleColoring.js +106 -106
  48. package/src/utils/WebGLSquareColoring.js +118 -118
  49. package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
  50. package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
  51. package/src/utils/WebGLSquareColoringKS.js +153 -153
  52. package/src/utils/stretching.js +131 -131
  53. package/src/utils/webGLUtils.js +71 -71
  54. package/src/utils/zoomUtils.js +142 -142
package/src/App.js CHANGED
@@ -1,668 +1,668 @@
1
- //@ts-check
2
- 'use strict'
3
-
4
- // internal imports
5
- import { GeoCanvas } from './GeoCanvas.js'
6
- import { Layer } from './Layer.js'
7
- import { Dataset } from './Dataset.js'
8
- import { Tooltip } from './Tooltip.js'
9
- import { CSVGrid } from './dataset/CSVGrid.js'
10
- import { TiledGrid } from './dataset/TiledGrid.js'
11
- import { BackgroundLayer } from './BackgroundLayer.js'
12
- import { BackgroundLayerWMS } from './BackgroundLayerWMS.js'
13
- import { LabelLayer } from './LabelLayer.js'
14
- import { LineLayer } from './LineLayer.js'
15
- import { monitor, monitorDuration } from './utils/Utils.js'
16
-
17
- // external imports
18
- import { select } from 'd3-selection'
19
-
20
- /**
21
- * A gridviz application.
22
- *
23
- * @author Joseph Davies, Julien Gaffuri
24
- */
25
- export class App {
26
- /**
27
- * @param {HTMLDivElement} container
28
- * @param {object} opts
29
- */
30
- constructor(container, opts) {
31
- opts = opts || {}
32
-
33
- /**
34
- * The layers.
35
- * @type {Array.<Layer>}
36
- * */
37
- this.layers = []
38
-
39
- //get container element
40
- this.container = container || document.getElementById('gridviz')
41
- if (!this.container) {
42
- console.error('Cannot find gridviz container element.')
43
- return
44
- }
45
-
46
- //set dimensions
47
- /** @type {number} */
48
- this.w = opts.w || this.container.offsetWidth
49
- /** @type {number} */
50
- this.h = opts.h || this.container.offsetHeight
51
-
52
- //create canvas element if user doesnt specify one
53
- /** @type {HTMLCanvasElement} */
54
- let canvas = opts.canvas || null
55
- if (!canvas) {
56
- canvas = document.createElement('canvas')
57
- canvas.setAttribute('width', '' + this.w)
58
- canvas.setAttribute('height', '' + this.h)
59
- this.container.appendChild(canvas)
60
- }
61
-
62
- /** Make geo canvas
63
- * @type {GeoCanvas}
64
- * @private */
65
- this.cg = new GeoCanvas(canvas, undefined, 1, opts)
66
- this.cg.redraw = (strong = true) => {
67
- if (monitor) monitorDuration('Start redraw')
68
- //console.log("?x=" + this.cg.getCenter().x + "&y=" + this.cg.getCenter().y + "&z=" + this.cg.getZf())
69
-
70
- //remove legend elements
71
- if (this.legend && strong) this.legend.selectAll('*').remove()
72
-
73
- //clear
74
- this.cg.initCanvasTransform()
75
- this.cg.clear(this.cg.backgroundColor)
76
-
77
- const zf = this.getZoomFactor()
78
- this.updateExtentGeo()
79
-
80
- //go through the background layers
81
- if (this.showBgLayers)
82
- for (const layer of this.bgLayers) {
83
- //check if layer is visible
84
- if (!layer.visible) continue
85
- if (zf > layer.maxZoom) continue
86
- if (zf < layer.minZoom) continue
87
-
88
- //draw layer
89
- layer.draw(this.cg)
90
- }
91
-
92
- //go through the layers
93
- for (const layer of this.layers) {
94
- //check if layer is visible
95
- if (!layer.visible) continue
96
- if (zf > layer.maxZoom) continue
97
- if (zf < layer.minZoom) continue
98
-
99
- //get layer dataset component
100
- /** @type {import('./DatasetComponent').DatasetComponent|undefined} */
101
- const dsc = layer.getDatasetComponent(zf)
102
- if (!dsc) continue
103
-
104
- //launch data download, if necessary
105
- if (strong)
106
- dsc.getData(this.cg.extGeo, () => {
107
- this.cg.redraw()
108
- })
109
-
110
- //update dataset view cache
111
- if (strong) dsc.updateViewCache(this.cg.extGeo)
112
-
113
- //draw cells, style by style
114
- if (strong)
115
- for (const s of layer.styles) {
116
- if (zf > s.maxZoom) continue
117
- if (zf < s.minZoom) continue
118
- s.draw(dsc.getViewCache(), dsc.getResolution(), this.cg)
119
- }
120
-
121
- //add legend element
122
- if (this.legend && strong)
123
- for (const s of layer.styles) {
124
- if (zf > s.maxZoom) continue
125
- if (zf < s.minZoom) continue
126
- for (const lg of s.legends) {
127
- //console.log(s, lg)
128
- //this.legend.append(lg.div)
129
- //s1.node().appendChild(s2.node())
130
- this.legend.node().append(lg.div.node())
131
- }
132
-
133
- //case for styles of styles, like kernel smoothing
134
- //TODO do better
135
- if (s['styles']) {
136
- for (const s2 of s.styles) {
137
- if (zf > s2.maxZoom) continue
138
- if (zf < s2.minZoom) continue
139
- for (const lg of s2.legends) {
140
- //console.log(s, lg)
141
- //this.legend.append(lg.div)
142
- //s1.node().appendChild(s2.node())
143
- this.legend.node().append(lg.div.node())
144
- }
145
- }
146
- }
147
- }
148
- }
149
-
150
- //draw boundary layer
151
- //if (strong)
152
- if (this.showBoundaries && this.boundaryLayer) this.boundaryLayer.draw(this.cg)
153
-
154
- //draw label layer
155
- //if (strong)
156
- if (this.showLabels && this.labelLayer) this.labelLayer.draw(this.cg)
157
-
158
- //
159
- this.canvasSave = null
160
-
161
- if (monitor) monitorDuration('End redraw')
162
-
163
- // listen for resize events on the App's container and handle them
164
- this.defineResizeObserver(this.container, canvas)
165
-
166
- return this
167
- }
168
-
169
- /** @type {Array.<BackgroundLayer|BackgroundLayerWMS>} */
170
- this.bgLayers = []
171
- /** @type {boolean} */
172
- this.showBgLayers = true
173
-
174
- /** @type {LabelLayer | undefined} */
175
- this.labelLayer = undefined
176
- /** @type {boolean} */
177
- this.showLabels = true
178
-
179
- /** @type {LineLayer | undefined} */
180
- this.boundaryLayer = undefined
181
- /** @type {boolean} */
182
- this.showBoundaries = true
183
-
184
- //legend div
185
-
186
- this.legendDivId = opts.legendDivId || 'gvizLegend'
187
- this.legend = select('#' + this.legendDivId)
188
- if (this.legend.empty()) {
189
- this.legend = select(
190
- this.container.id && this.container.id != '' ? '#' + this.container.id : 'body'
191
- )
192
- .append('div')
193
- .attr('id', this.legendDivId)
194
- .style('position', 'absolute')
195
- .style('width', 'auto')
196
- .style('height', 'auto')
197
- .style('background', '#FFFFFFCC')
198
- //.style("padding", this.padding)
199
- .style('border', '0px')
200
- .style('border-radius', '5px')
201
- .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
202
- .style('font-family', 'Helvetica, Arial, sans-serif')
203
- .style('top', '20px')
204
- .style('right', '20px')
205
- //hide
206
- //.style("visibility", "hidden")
207
- }
208
-
209
- //tooltip
210
-
211
- // set App container as default parent element for tooltip
212
- if (!opts.tooltip) opts.tooltip = {}
213
- if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
214
-
215
- /**
216
- * @private
217
- * @type {Tooltip} */
218
- this.tooltip = new Tooltip(opts.tooltip)
219
-
220
- /** @param {MouseEvent} e */
221
- const focusCell = (e) => {
222
- //compute mouse geo position
223
- const mousePositionGeo = {
224
- x: this.cg.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
225
- y: this.cg.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
226
- }
227
- /** @type {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined} */
228
- const focus = this.getCellFocusInfo(mousePositionGeo)
229
-
230
- // transparent background (e.g. leaflet) 'red painting' fix
231
- if (opts.transparentBackground) {
232
- if (focus) {
233
- this.tooltip.setPosition(e)
234
- this.tooltip.show()
235
- this.tooltip.html(focus.html)
236
- } else {
237
- this.tooltip.hide()
238
- }
239
- this.canvasSave = document.createElement('canvas')
240
- this.canvasSave.setAttribute('width', '' + this.w)
241
- this.canvasSave.setAttribute('height', '' + this.h)
242
- this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
243
- this.cg.initCanvasTransform()
244
- return
245
- }
246
-
247
- if (focus) {
248
- this.tooltip.setPosition(e)
249
- this.tooltip.show()
250
- this.tooltip.html(focus.html)
251
-
252
- //show cell position as a rectangle
253
- if (!this.canvasSave) {
254
- this.canvasSave = document.createElement('canvas')
255
- this.canvasSave.setAttribute('width', '' + this.w)
256
- this.canvasSave.setAttribute('height', '' + this.h)
257
- this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
258
- } else {
259
- this.cg.ctx.drawImage(this.canvasSave, 0, 0)
260
- }
261
-
262
- //draw image saved + draw rectangle
263
- const rectWPix = this.selectionRectangleWidthPix
264
- ? this.selectionRectangleWidthPix(focus.resolution, this.getZoomFactor())
265
- : 4
266
- this.cg.initCanvasTransform()
267
- this.cg.ctx.strokeStyle = this.selectionRectangleColor
268
- this.cg.ctx.lineWidth = rectWPix
269
- this.cg.ctx.beginPath()
270
-
271
- this.cg.ctx.rect(
272
- this.cg.geoToPixX(focus.cell.x) - rectWPix / 2,
273
- this.cg.geoToPixY(focus.cell.y) + rectWPix / 2,
274
- focus.resolution / this.getZoomFactor() + rectWPix,
275
- -focus.resolution / this.getZoomFactor() - rectWPix
276
- )
277
- this.cg.ctx.stroke()
278
- } else {
279
- this.tooltip.hide()
280
- if (this.canvasSave) this.cg.ctx.drawImage(this.canvasSave, 0, 0)
281
- }
282
- }
283
-
284
- // add event listeners to container
285
- this.mouseOverHandler = (e) => focusCell(e)
286
- this.mouseMoveHandler = (e) => focusCell(e)
287
- this.mouseOutHandler = (e) => this.tooltip.hide()
288
- this.container.addEventListener('mouseover', this.mouseOverHandler)
289
- this.container.addEventListener('mousemove', this.mouseMoveHandler)
290
- this.container.addEventListener('mouseout', this.mouseOutHandler)
291
-
292
- // add extra logic to onZoomStartFun
293
- this.cg.onZoomStartFun = (e) => {
294
- if (opts.onZoomStartFun) opts.onZoomStartFun(e)
295
- this.tooltip.hide()
296
- }
297
-
298
- //for mouse over
299
- /**
300
- * @private
301
- * @type {HTMLCanvasElement|null} */
302
- this.canvasSave = null
303
-
304
- this.selectionRectangleColor = opts.selectionRectangleColor || 'red'
305
- this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,zf) => {}
306
-
307
- //
308
- //canvas.addEventListener("keydown", e => { console.log(arguments) });
309
- }
310
-
311
- /**
312
- * @param {number} marginPx
313
- * @returns {import('./Dataset').Envelope}
314
- * @public
315
- */
316
- updateExtentGeo(marginPx = 20) {
317
- return this.cg.updateExtentGeo(marginPx)
318
- }
319
-
320
- /**
321
- * Return the cell HTML info at a given geo position.
322
- * This is usefull for user interactions, to show this info where the user clicks for example.
323
- *
324
- * @param {{x:number,y:number}} posGeo
325
- * @returns {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined}
326
- * @protected
327
- */
328
- getCellFocusInfo(posGeo) {
329
- //go through the layers, starting from top
330
- const zf = this.getZoomFactor()
331
- for (let i = this.layers.length - 1; i >= 0; i--) {
332
- /** @type {Layer} */
333
- const layer = this.layers[i]
334
- if (!layer.visible) continue
335
- if (!layer.cellInfoHTML) continue
336
- if (layer.cellInfoHTML === 'none') continue
337
- const dsc = layer.getDatasetComponent(zf)
338
- if (!dsc) continue
339
-
340
- //get cell at mouse position
341
- /** @type {import('./Dataset').Cell|undefined} */
342
- const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
343
- if (!cell) return undefined
344
- const html = layer.cellInfoHTML(cell, dsc.getResolution())
345
- if (!html) return undefined
346
- return { cell: cell, html: html, resolution: dsc.getResolution() }
347
- }
348
- }
349
-
350
- //getters and setters
351
-
352
- /** @returns {{x:number,y:number}} */
353
- getGeoCenter() {
354
- return this.cg.getCenter()
355
- }
356
- /** @param {{x:number,y:number}} val @returns {this} */
357
- setGeoCenter(val) {
358
- this.cg.setCenter(val)
359
- return this
360
- }
361
-
362
- /** @returns {number} */
363
- getZoomFactor() {
364
- return this.cg.getZf()
365
- }
366
- /** @param {number} val @returns {this} */
367
- setZoomFactor(val) {
368
- this.cg.setZf(val)
369
- return this
370
- }
371
-
372
- /** @returns {Array.<number>} */
373
- getZoomFactorExtent() {
374
- return this.cg.getZfExtent()
375
- }
376
- /** @param {Array.<number>} val @returns {this} */
377
- setZoomFactorExtent(val) {
378
- this.cg.setZfExtent(val)
379
- return this
380
- }
381
-
382
- /** @returns {string} */
383
- getBackgroundColor() {
384
- return this.cg.backgroundColor
385
- }
386
- /** @param {string} val @returns {this} */
387
- setBackgroundColor(val) {
388
- this.cg.backgroundColor = val
389
- return this
390
- }
391
-
392
- /** @returns {LineLayer | undefined} */
393
- getBoundaryLayer() {
394
- return this.boundaryLayer
395
- }
396
- /** @param {object} opts @returns {this} */
397
- setBoundaryLayer(opts) {
398
- this.boundaryLayer = new LineLayer(opts)
399
- return this
400
- }
401
-
402
- /** @returns {LabelLayer | undefined} */
403
- getLabelLayer() {
404
- return this.labelLayer
405
- }
406
- /** @param {object} opts @returns {this} */
407
- setLabelLayer(opts) {
408
- this.labelLayer = new LabelLayer(opts)
409
- return this
410
- }
411
-
412
- /** @returns {this} */
413
- redraw() {
414
- this.cg.redraw()
415
- return this
416
- }
417
-
418
- /**
419
- * Add a layer to the app.
420
- *
421
- * @param {Dataset} dataset The dataset of the layer
422
- * @param {Array.<import('./Style').Style>} styles The styles of the layer
423
- * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string}} opts The layer options.
424
- * @returns {this}
425
- */
426
- addLayerFromDataset(dataset, styles, opts) {
427
- const lay = new Layer(dataset, styles, opts)
428
- this.layers.push(lay)
429
- return this
430
- }
431
-
432
- //dataset creation
433
-
434
- /**
435
- * Make a CSV grid dataset.
436
- *
437
- * @param {string} url The URL of the dataset.
438
- * @param {number} resolution The dataset resolution in geographical unit.
439
- * @param {object=} opts The parameters of the dataset.
440
- * @returns {Dataset}
441
- */
442
- makeCSVGridDataset(url, resolution, opts) {
443
- return new Dataset(
444
- [
445
- new CSVGrid(url, resolution, opts).getData(undefined, () => {
446
- this.cg.redraw()
447
- }),
448
- ],
449
- [],
450
- opts
451
- )
452
- }
453
-
454
- /**
455
- * Make a tiled grid dataset.
456
- *
457
- * @param {string} url
458
- * @param {{preprocess?:function(import('./Dataset').Cell):boolean}} opts
459
- * @returns {Dataset}
460
- */
461
- makeTiledGridDataset(url, opts) {
462
- return new Dataset(
463
- [
464
- new TiledGrid(url, this, opts).loadInfo(() => {
465
- this.cg.redraw()
466
- }),
467
- ],
468
- [],
469
- opts
470
- )
471
- }
472
-
473
- //multi scale dataset creation
474
-
475
- /**
476
- * Make a multi scale CSV grid dataset.
477
- *
478
- * @param {Array.<number>} resolutions
479
- * @param {function(number):string} resToURL
480
- * @param {{preprocess?:function(import('./Dataset').Cell):boolean}} opts
481
- * @returns {Dataset}
482
- */
483
- makeMultiScaleCSVGridDataset(resolutions, resToURL, opts) {
484
- return Dataset.make(
485
- resolutions,
486
- (res) =>
487
- new CSVGrid(resToURL(res), res, opts).getData(undefined, () => {
488
- this.cg.redraw()
489
- }),
490
- opts
491
- )
492
- }
493
-
494
- //tiled multiscale
495
-
496
- /**
497
- * Make a multi scale tiled grid dataset.
498
- *
499
- * @param {Array.<number>} resolutions
500
- * @param {function(number):string} resToURL
501
- * @param {{preprocess?:function(import('./Dataset').Cell):boolean}} opts
502
- * @returns {Dataset}
503
- */
504
- makeMultiScaleTiledGridDataset(resolutions, resToURL, opts) {
505
- return Dataset.make(
506
- resolutions,
507
- (res) =>
508
- new TiledGrid(resToURL(res), this, opts).loadInfo(() => {
509
- this.cg.redraw()
510
- }),
511
- opts
512
- )
513
- }
514
-
515
- // direct layer creation
516
-
517
- /**
518
- * Add a layer from a CSV grid dataset.
519
- *
520
- * @param {string} url The URL of the dataset.
521
- * @param {number} resolution The dataset resolution in geographical unit.
522
- * @param {Array.<import('./Style').Style>} styles The styles, ordered in drawing order.
523
- * @param {object=} opts The parameters of the dataset and layer.
524
- * @returns {this}
525
- */
526
- addCSVGridLayer(url, resolution, styles, opts) {
527
- const ds = this.makeCSVGridDataset(url, resolution, opts)
528
- return this.addLayerFromDataset(ds, styles, opts)
529
- }
530
-
531
- /**
532
- *
533
- * @param {string} url
534
- * @param {Array.<import('./Style').Style>} styles
535
- * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string, preprocess?:function(import('./Dataset').Cell):boolean}} opts
536
- * @returns {this}
537
- */
538
- addTiledGridLayer(url, styles, opts) {
539
- const ds = this.makeTiledGridDataset(url, opts)
540
- return this.addLayerFromDataset(ds, styles, opts)
541
- }
542
-
543
- /**
544
- * Add a layer from a CSV grid dataset.
545
- *
546
- * @param {Array.<number>} resolutions
547
- * @param {function(number):string} resToURL
548
- * @param {Array.<import('./Style').Style>} styles The styles, ordered in drawing order.
549
- * @param {object=} opts The parameters of the dataset and layer.
550
- * @returns {this}
551
- */
552
- addMultiScaleCSVGridLayer(resolutions, resToURL, styles, opts) {
553
- const ds = this.makeMultiScaleCSVGridDataset(resolutions, resToURL, opts)
554
- return this.addLayerFromDataset(ds, styles, opts)
555
- }
556
-
557
- /**
558
- * @param {Array.<number>} resolutions
559
- * @param {function(number):string} resToURL
560
- * @param {Array.<import('./Style').Style>} styles
561
- * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string, preprocess?:function(import('./Dataset').Cell):boolean}} opts
562
- * @returns {this}
563
- */
564
- addMultiScaleTiledGridLayer(resolutions, resToURL, styles, opts) {
565
- const ds = this.makeMultiScaleTiledGridDataset(resolutions, resToURL, opts)
566
- return this.addLayerFromDataset(ds, styles, opts)
567
- }
568
-
569
- /**
570
- * Add a background layer to the app.
571
- *
572
- * @param {object} opts
573
- * @returns {this}
574
- */
575
- addBackgroundLayer(opts) {
576
- this.bgLayers.push(new BackgroundLayer(opts))
577
- this.redraw()
578
- return this
579
- }
580
-
581
- /**
582
- * Add a WMS background layer to the app.
583
- *
584
- * @param {object} opts
585
- * @returns {this}
586
- */
587
- addBackgroundLayerWMS(opts) {
588
- this.bgLayers.push(new BackgroundLayerWMS(opts))
589
- this.redraw()
590
- return this
591
- }
592
-
593
- /**
594
- *
595
- * @param {string} id
596
- * @param {object} opts
597
- * @returns {this}
598
- */
599
- addZoomSlider(id, opts) {
600
- this.cg.addZoomSlider(id, opts)
601
- return this
602
- }
603
-
604
- /** @returns {this} */
605
- setViewFromURL() {
606
- this.cg.setViewFromURL()
607
- return this
608
- }
609
-
610
- /**
611
- * @description Add a resize event observer to the Apps container and update the canvas accordingly
612
- * @param {HTMLDivElement} container The App's container element
613
- * @param {HTMLCanvasElement} canvas The App canvas element
614
- * @memberof App
615
- */
616
- defineResizeObserver(container, canvas) {
617
- // listen to resize events
618
- const resizeObserver = new ResizeObserver((entries) => {
619
- // make sure canvas has been built
620
- if (container.clientWidth > 0 && container.clientHeight > 0) {
621
- // make sure we dont exceed loop limit first
622
- // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
623
- window.requestAnimationFrame(() => {
624
- if (!Array.isArray(entries) || !entries.length) {
625
- return
626
- }
627
- // update the app and canvas size
628
- if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
629
- this.h = container.clientHeight
630
- this.w = container.clientWidth
631
- this.cg.h = container.clientHeight
632
- this.cg.w = container.clientWidth
633
- canvas.setAttribute('width', '' + this.w)
634
- canvas.setAttribute('height', '' + this.h)
635
- this.redraw()
636
- }
637
- })
638
- }
639
- })
640
-
641
- resizeObserver.observe(container)
642
- }
643
-
644
- /**
645
- * @description Destroy the app and it's event listeners
646
- * This should significantly reduce the memory used when creating and destroying gridviz app instances (for example in leaflet-gridviz)
647
- * @memberof App
648
- */
649
- destroy() {
650
- // clear layers
651
- this.layers = []
652
- this.bgLayers = []
653
-
654
- // remove event listeners from container
655
- this.container.removeEventListener('mouseover', this.mouseOverHandler)
656
- this.container.removeEventListener('mousemove', this.mouseMoveHandler)
657
- this.container.removeEventListener('mouseout', this.mouseOutHandler)
658
-
659
- // remove canvas
660
- this.cg.canvas.remove()
661
-
662
- // remove legend
663
- this.legend?.remove()
664
-
665
- // remove tooltip
666
- this.tooltip.tooltip?.remove()
667
- }
668
- }
1
+ //@ts-check
2
+ 'use strict'
3
+
4
+ // internal imports
5
+ import { GeoCanvas } from './GeoCanvas.js'
6
+ import { Layer } from './Layer.js'
7
+ import { Dataset } from './Dataset.js'
8
+ import { Tooltip } from './Tooltip.js'
9
+ import { CSVGrid } from './dataset/CSVGrid.js'
10
+ import { TiledGrid } from './dataset/TiledGrid.js'
11
+ import { BackgroundLayer } from './BackgroundLayer.js'
12
+ import { BackgroundLayerWMS } from './BackgroundLayerWMS.js'
13
+ import { LabelLayer } from './LabelLayer.js'
14
+ import { LineLayer } from './LineLayer.js'
15
+ import { monitor, monitorDuration } from './utils/Utils.js'
16
+
17
+ // external imports
18
+ import { select } from 'd3-selection'
19
+
20
+ /**
21
+ * A gridviz application.
22
+ *
23
+ * @author Joseph Davies, Julien Gaffuri
24
+ */
25
+ export class App {
26
+ /**
27
+ * @param {HTMLDivElement} container
28
+ * @param {object} opts
29
+ */
30
+ constructor(container, opts) {
31
+ opts = opts || {}
32
+
33
+ /**
34
+ * The layers.
35
+ * @type {Array.<Layer>}
36
+ * */
37
+ this.layers = []
38
+
39
+ //get container element
40
+ this.container = container || document.getElementById('gridviz')
41
+ if (!this.container) {
42
+ console.error('Cannot find gridviz container element.')
43
+ return
44
+ }
45
+
46
+ //set dimensions
47
+ /** @type {number} */
48
+ this.w = opts.w || this.container.offsetWidth
49
+ /** @type {number} */
50
+ this.h = opts.h || this.container.offsetHeight
51
+
52
+ //create canvas element if user doesnt specify one
53
+ /** @type {HTMLCanvasElement} */
54
+ let canvas = opts.canvas || null
55
+ if (!canvas) {
56
+ canvas = document.createElement('canvas')
57
+ canvas.setAttribute('width', '' + this.w)
58
+ canvas.setAttribute('height', '' + this.h)
59
+ this.container.appendChild(canvas)
60
+ }
61
+
62
+ /** Make geo canvas
63
+ * @type {GeoCanvas}
64
+ * @private */
65
+ this.cg = new GeoCanvas(canvas, undefined, 1, opts)
66
+ this.cg.redraw = (strong = true) => {
67
+ if (monitor) monitorDuration('Start redraw')
68
+ //console.log("?x=" + this.cg.getCenter().x + "&y=" + this.cg.getCenter().y + "&z=" + this.cg.getZf())
69
+
70
+ //remove legend elements
71
+ if (this.legend && strong) this.legend.selectAll('*').remove()
72
+
73
+ //clear
74
+ this.cg.initCanvasTransform()
75
+ this.cg.clear(this.cg.backgroundColor)
76
+
77
+ const zf = this.getZoomFactor()
78
+ this.updateExtentGeo()
79
+
80
+ //go through the background layers
81
+ if (this.showBgLayers)
82
+ for (const layer of this.bgLayers) {
83
+ //check if layer is visible
84
+ if (!layer.visible) continue
85
+ if (zf > layer.maxZoom) continue
86
+ if (zf < layer.minZoom) continue
87
+
88
+ //draw layer
89
+ layer.draw(this.cg)
90
+ }
91
+
92
+ //go through the layers
93
+ for (const layer of this.layers) {
94
+ //check if layer is visible
95
+ if (!layer.visible) continue
96
+ if (zf > layer.maxZoom) continue
97
+ if (zf < layer.minZoom) continue
98
+
99
+ //get layer dataset component
100
+ /** @type {import('./DatasetComponent').DatasetComponent|undefined} */
101
+ const dsc = layer.getDatasetComponent(zf)
102
+ if (!dsc) continue
103
+
104
+ //launch data download, if necessary
105
+ if (strong)
106
+ dsc.getData(this.cg.extGeo, () => {
107
+ this.cg.redraw()
108
+ })
109
+
110
+ //update dataset view cache
111
+ if (strong) dsc.updateViewCache(this.cg.extGeo)
112
+
113
+ //draw cells, style by style
114
+ if (strong)
115
+ for (const s of layer.styles) {
116
+ if (zf > s.maxZoom) continue
117
+ if (zf < s.minZoom) continue
118
+ s.draw(dsc.getViewCache(), dsc.getResolution(), this.cg)
119
+ }
120
+
121
+ //add legend element
122
+ if (this.legend && strong)
123
+ for (const s of layer.styles) {
124
+ if (zf > s.maxZoom) continue
125
+ if (zf < s.minZoom) continue
126
+ for (const lg of s.legends) {
127
+ //console.log(s, lg)
128
+ //this.legend.append(lg.div)
129
+ //s1.node().appendChild(s2.node())
130
+ this.legend.node().append(lg.div.node())
131
+ }
132
+
133
+ //case for styles of styles, like kernel smoothing
134
+ //TODO do better
135
+ if (s['styles']) {
136
+ for (const s2 of s.styles) {
137
+ if (zf > s2.maxZoom) continue
138
+ if (zf < s2.minZoom) continue
139
+ for (const lg of s2.legends) {
140
+ //console.log(s, lg)
141
+ //this.legend.append(lg.div)
142
+ //s1.node().appendChild(s2.node())
143
+ this.legend.node().append(lg.div.node())
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+
150
+ //draw boundary layer
151
+ //if (strong)
152
+ if (this.showBoundaries && this.boundaryLayer) this.boundaryLayer.draw(this.cg)
153
+
154
+ //draw label layer
155
+ //if (strong)
156
+ if (this.showLabels && this.labelLayer) this.labelLayer.draw(this.cg)
157
+
158
+ //
159
+ this.canvasSave = null
160
+
161
+ if (monitor) monitorDuration('End redraw')
162
+
163
+ // listen for resize events on the App's container and handle them
164
+ this.defineResizeObserver(this.container, canvas)
165
+
166
+ return this
167
+ }
168
+
169
+ /** @type {Array.<BackgroundLayer|BackgroundLayerWMS>} */
170
+ this.bgLayers = []
171
+ /** @type {boolean} */
172
+ this.showBgLayers = true
173
+
174
+ /** @type {LabelLayer | undefined} */
175
+ this.labelLayer = undefined
176
+ /** @type {boolean} */
177
+ this.showLabels = true
178
+
179
+ /** @type {LineLayer | undefined} */
180
+ this.boundaryLayer = undefined
181
+ /** @type {boolean} */
182
+ this.showBoundaries = true
183
+
184
+ //legend div
185
+
186
+ this.legendDivId = opts.legendDivId || 'gvizLegend'
187
+ this.legend = select('#' + this.legendDivId)
188
+ if (this.legend.empty()) {
189
+ this.legend = select(
190
+ this.container.id && this.container.id != '' ? '#' + this.container.id : 'body'
191
+ )
192
+ .append('div')
193
+ .attr('id', this.legendDivId)
194
+ .style('position', 'absolute')
195
+ .style('width', 'auto')
196
+ .style('height', 'auto')
197
+ .style('background', '#FFFFFFCC')
198
+ //.style("padding", this.padding)
199
+ .style('border', '0px')
200
+ .style('border-radius', '5px')
201
+ .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
202
+ .style('font-family', 'Helvetica, Arial, sans-serif')
203
+ .style('top', '20px')
204
+ .style('right', '20px')
205
+ //hide
206
+ //.style("visibility", "hidden")
207
+ }
208
+
209
+ //tooltip
210
+
211
+ // set App container as default parent element for tooltip
212
+ if (!opts.tooltip) opts.tooltip = {}
213
+ if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
214
+
215
+ /**
216
+ * @private
217
+ * @type {Tooltip} */
218
+ this.tooltip = new Tooltip(opts.tooltip)
219
+
220
+ /** @param {MouseEvent} e */
221
+ const focusCell = (e) => {
222
+ //compute mouse geo position
223
+ const mousePositionGeo = {
224
+ x: this.cg.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
225
+ y: this.cg.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
226
+ }
227
+ /** @type {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined} */
228
+ const focus = this.getCellFocusInfo(mousePositionGeo)
229
+
230
+ // transparent background (e.g. leaflet) 'red painting' fix
231
+ if (opts.transparentBackground) {
232
+ if (focus) {
233
+ this.tooltip.setPosition(e)
234
+ this.tooltip.show()
235
+ this.tooltip.html(focus.html)
236
+ } else {
237
+ this.tooltip.hide()
238
+ }
239
+ this.canvasSave = document.createElement('canvas')
240
+ this.canvasSave.setAttribute('width', '' + this.w)
241
+ this.canvasSave.setAttribute('height', '' + this.h)
242
+ this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
243
+ this.cg.initCanvasTransform()
244
+ return
245
+ }
246
+
247
+ if (focus) {
248
+ this.tooltip.setPosition(e)
249
+ this.tooltip.show()
250
+ this.tooltip.html(focus.html)
251
+
252
+ //show cell position as a rectangle
253
+ if (!this.canvasSave) {
254
+ this.canvasSave = document.createElement('canvas')
255
+ this.canvasSave.setAttribute('width', '' + this.w)
256
+ this.canvasSave.setAttribute('height', '' + this.h)
257
+ this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
258
+ } else {
259
+ this.cg.ctx.drawImage(this.canvasSave, 0, 0)
260
+ }
261
+
262
+ //draw image saved + draw rectangle
263
+ const rectWPix = this.selectionRectangleWidthPix
264
+ ? this.selectionRectangleWidthPix(focus.resolution, this.getZoomFactor())
265
+ : 4
266
+ this.cg.initCanvasTransform()
267
+ this.cg.ctx.strokeStyle = this.selectionRectangleColor
268
+ this.cg.ctx.lineWidth = rectWPix
269
+ this.cg.ctx.beginPath()
270
+
271
+ this.cg.ctx.rect(
272
+ this.cg.geoToPixX(focus.cell.x) - rectWPix / 2,
273
+ this.cg.geoToPixY(focus.cell.y) + rectWPix / 2,
274
+ focus.resolution / this.getZoomFactor() + rectWPix,
275
+ -focus.resolution / this.getZoomFactor() - rectWPix
276
+ )
277
+ this.cg.ctx.stroke()
278
+ } else {
279
+ this.tooltip.hide()
280
+ if (this.canvasSave) this.cg.ctx.drawImage(this.canvasSave, 0, 0)
281
+ }
282
+ }
283
+
284
+ // add event listeners to container
285
+ this.mouseOverHandler = (e) => focusCell(e)
286
+ this.mouseMoveHandler = (e) => focusCell(e)
287
+ this.mouseOutHandler = (e) => this.tooltip.hide()
288
+ this.container.addEventListener('mouseover', this.mouseOverHandler)
289
+ this.container.addEventListener('mousemove', this.mouseMoveHandler)
290
+ this.container.addEventListener('mouseout', this.mouseOutHandler)
291
+
292
+ // add extra logic to onZoomStartFun
293
+ this.cg.onZoomStartFun = (e) => {
294
+ if (opts.onZoomStartFun) opts.onZoomStartFun(e)
295
+ this.tooltip.hide()
296
+ }
297
+
298
+ //for mouse over
299
+ /**
300
+ * @private
301
+ * @type {HTMLCanvasElement|null} */
302
+ this.canvasSave = null
303
+
304
+ this.selectionRectangleColor = opts.selectionRectangleColor || 'red'
305
+ this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,zf) => {}
306
+
307
+ //
308
+ //canvas.addEventListener("keydown", e => { console.log(arguments) });
309
+ }
310
+
311
+ /**
312
+ * @param {number} marginPx
313
+ * @returns {import('./Dataset').Envelope}
314
+ * @public
315
+ */
316
+ updateExtentGeo(marginPx = 20) {
317
+ return this.cg.updateExtentGeo(marginPx)
318
+ }
319
+
320
+ /**
321
+ * Return the cell HTML info at a given geo position.
322
+ * This is usefull for user interactions, to show this info where the user clicks for example.
323
+ *
324
+ * @param {{x:number,y:number}} posGeo
325
+ * @returns {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined}
326
+ * @protected
327
+ */
328
+ getCellFocusInfo(posGeo) {
329
+ //go through the layers, starting from top
330
+ const zf = this.getZoomFactor()
331
+ for (let i = this.layers.length - 1; i >= 0; i--) {
332
+ /** @type {Layer} */
333
+ const layer = this.layers[i]
334
+ if (!layer.visible) continue
335
+ if (!layer.cellInfoHTML) continue
336
+ if (layer.cellInfoHTML === 'none') continue
337
+ const dsc = layer.getDatasetComponent(zf)
338
+ if (!dsc) continue
339
+
340
+ //get cell at mouse position
341
+ /** @type {import('./Dataset').Cell|undefined} */
342
+ const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
343
+ if (!cell) return undefined
344
+ const html = layer.cellInfoHTML(cell, dsc.getResolution())
345
+ if (!html) return undefined
346
+ return { cell: cell, html: html, resolution: dsc.getResolution() }
347
+ }
348
+ }
349
+
350
+ //getters and setters
351
+
352
+ /** @returns {{x:number,y:number}} */
353
+ getGeoCenter() {
354
+ return this.cg.getCenter()
355
+ }
356
+ /** @param {{x:number,y:number}} val @returns {this} */
357
+ setGeoCenter(val) {
358
+ this.cg.setCenter(val)
359
+ return this
360
+ }
361
+
362
+ /** @returns {number} */
363
+ getZoomFactor() {
364
+ return this.cg.getZf()
365
+ }
366
+ /** @param {number} val @returns {this} */
367
+ setZoomFactor(val) {
368
+ this.cg.setZf(val)
369
+ return this
370
+ }
371
+
372
+ /** @returns {Array.<number>} */
373
+ getZoomFactorExtent() {
374
+ return this.cg.getZfExtent()
375
+ }
376
+ /** @param {Array.<number>} val @returns {this} */
377
+ setZoomFactorExtent(val) {
378
+ this.cg.setZfExtent(val)
379
+ return this
380
+ }
381
+
382
+ /** @returns {string} */
383
+ getBackgroundColor() {
384
+ return this.cg.backgroundColor
385
+ }
386
+ /** @param {string} val @returns {this} */
387
+ setBackgroundColor(val) {
388
+ this.cg.backgroundColor = val
389
+ return this
390
+ }
391
+
392
+ /** @returns {LineLayer | undefined} */
393
+ getBoundaryLayer() {
394
+ return this.boundaryLayer
395
+ }
396
+ /** @param {object} opts @returns {this} */
397
+ setBoundaryLayer(opts) {
398
+ this.boundaryLayer = new LineLayer(opts)
399
+ return this
400
+ }
401
+
402
+ /** @returns {LabelLayer | undefined} */
403
+ getLabelLayer() {
404
+ return this.labelLayer
405
+ }
406
+ /** @param {object} opts @returns {this} */
407
+ setLabelLayer(opts) {
408
+ this.labelLayer = new LabelLayer(opts)
409
+ return this
410
+ }
411
+
412
+ /** @returns {this} */
413
+ redraw() {
414
+ this.cg.redraw()
415
+ return this
416
+ }
417
+
418
+ /**
419
+ * Add a layer to the app.
420
+ *
421
+ * @param {Dataset} dataset The dataset of the layer
422
+ * @param {Array.<import('./Style').Style>} styles The styles of the layer
423
+ * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string}} opts The layer options.
424
+ * @returns {this}
425
+ */
426
+ addLayerFromDataset(dataset, styles, opts) {
427
+ const lay = new Layer(dataset, styles, opts)
428
+ this.layers.push(lay)
429
+ return this
430
+ }
431
+
432
+ //dataset creation
433
+
434
+ /**
435
+ * Make a CSV grid dataset.
436
+ *
437
+ * @param {string} url The URL of the dataset.
438
+ * @param {number} resolution The dataset resolution in geographical unit.
439
+ * @param {object=} opts The parameters of the dataset.
440
+ * @returns {Dataset}
441
+ */
442
+ makeCSVGridDataset(url, resolution, opts) {
443
+ return new Dataset(
444
+ [
445
+ new CSVGrid(url, resolution, opts).getData(undefined, () => {
446
+ this.cg.redraw()
447
+ }),
448
+ ],
449
+ [],
450
+ opts
451
+ )
452
+ }
453
+
454
+ /**
455
+ * Make a tiled grid dataset.
456
+ *
457
+ * @param {string} url
458
+ * @param {{preprocess?:function(import('./Dataset').Cell):boolean}} opts
459
+ * @returns {Dataset}
460
+ */
461
+ makeTiledGridDataset(url, opts) {
462
+ return new Dataset(
463
+ [
464
+ new TiledGrid(url, this, opts).loadInfo(() => {
465
+ this.cg.redraw()
466
+ }),
467
+ ],
468
+ [],
469
+ opts
470
+ )
471
+ }
472
+
473
+ //multi scale dataset creation
474
+
475
+ /**
476
+ * Make a multi scale CSV grid dataset.
477
+ *
478
+ * @param {Array.<number>} resolutions
479
+ * @param {function(number):string} resToURL
480
+ * @param {{preprocess?:function(import('./Dataset').Cell):boolean}} opts
481
+ * @returns {Dataset}
482
+ */
483
+ makeMultiScaleCSVGridDataset(resolutions, resToURL, opts) {
484
+ return Dataset.make(
485
+ resolutions,
486
+ (res) =>
487
+ new CSVGrid(resToURL(res), res, opts).getData(undefined, () => {
488
+ this.cg.redraw()
489
+ }),
490
+ opts
491
+ )
492
+ }
493
+
494
+ //tiled multiscale
495
+
496
+ /**
497
+ * Make a multi scale tiled grid dataset.
498
+ *
499
+ * @param {Array.<number>} resolutions
500
+ * @param {function(number):string} resToURL
501
+ * @param {{preprocess?:function(import('./Dataset').Cell):boolean}} opts
502
+ * @returns {Dataset}
503
+ */
504
+ makeMultiScaleTiledGridDataset(resolutions, resToURL, opts) {
505
+ return Dataset.make(
506
+ resolutions,
507
+ (res) =>
508
+ new TiledGrid(resToURL(res), this, opts).loadInfo(() => {
509
+ this.cg.redraw()
510
+ }),
511
+ opts
512
+ )
513
+ }
514
+
515
+ // direct layer creation
516
+
517
+ /**
518
+ * Add a layer from a CSV grid dataset.
519
+ *
520
+ * @param {string} url The URL of the dataset.
521
+ * @param {number} resolution The dataset resolution in geographical unit.
522
+ * @param {Array.<import('./Style').Style>} styles The styles, ordered in drawing order.
523
+ * @param {object=} opts The parameters of the dataset and layer.
524
+ * @returns {this}
525
+ */
526
+ addCSVGridLayer(url, resolution, styles, opts) {
527
+ const ds = this.makeCSVGridDataset(url, resolution, opts)
528
+ return this.addLayerFromDataset(ds, styles, opts)
529
+ }
530
+
531
+ /**
532
+ *
533
+ * @param {string} url
534
+ * @param {Array.<import('./Style').Style>} styles
535
+ * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string, preprocess?:function(import('./Dataset').Cell):boolean}} opts
536
+ * @returns {this}
537
+ */
538
+ addTiledGridLayer(url, styles, opts) {
539
+ const ds = this.makeTiledGridDataset(url, opts)
540
+ return this.addLayerFromDataset(ds, styles, opts)
541
+ }
542
+
543
+ /**
544
+ * Add a layer from a CSV grid dataset.
545
+ *
546
+ * @param {Array.<number>} resolutions
547
+ * @param {function(number):string} resToURL
548
+ * @param {Array.<import('./Style').Style>} styles The styles, ordered in drawing order.
549
+ * @param {object=} opts The parameters of the dataset and layer.
550
+ * @returns {this}
551
+ */
552
+ addMultiScaleCSVGridLayer(resolutions, resToURL, styles, opts) {
553
+ const ds = this.makeMultiScaleCSVGridDataset(resolutions, resToURL, opts)
554
+ return this.addLayerFromDataset(ds, styles, opts)
555
+ }
556
+
557
+ /**
558
+ * @param {Array.<number>} resolutions
559
+ * @param {function(number):string} resToURL
560
+ * @param {Array.<import('./Style').Style>} styles
561
+ * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string, preprocess?:function(import('./Dataset').Cell):boolean}} opts
562
+ * @returns {this}
563
+ */
564
+ addMultiScaleTiledGridLayer(resolutions, resToURL, styles, opts) {
565
+ const ds = this.makeMultiScaleTiledGridDataset(resolutions, resToURL, opts)
566
+ return this.addLayerFromDataset(ds, styles, opts)
567
+ }
568
+
569
+ /**
570
+ * Add a background layer to the app.
571
+ *
572
+ * @param {object} opts
573
+ * @returns {this}
574
+ */
575
+ addBackgroundLayer(opts) {
576
+ this.bgLayers.push(new BackgroundLayer(opts))
577
+ this.redraw()
578
+ return this
579
+ }
580
+
581
+ /**
582
+ * Add a WMS background layer to the app.
583
+ *
584
+ * @param {object} opts
585
+ * @returns {this}
586
+ */
587
+ addBackgroundLayerWMS(opts) {
588
+ this.bgLayers.push(new BackgroundLayerWMS(opts))
589
+ this.redraw()
590
+ return this
591
+ }
592
+
593
+ /**
594
+ *
595
+ * @param {string} id
596
+ * @param {object} opts
597
+ * @returns {this}
598
+ */
599
+ addZoomSlider(id, opts) {
600
+ this.cg.addZoomSlider(id, opts)
601
+ return this
602
+ }
603
+
604
+ /** @returns {this} */
605
+ setViewFromURL() {
606
+ this.cg.setViewFromURL()
607
+ return this
608
+ }
609
+
610
+ /**
611
+ * @description Add a resize event observer to the Apps container and update the canvas accordingly
612
+ * @param {HTMLDivElement} container The App's container element
613
+ * @param {HTMLCanvasElement} canvas The App canvas element
614
+ * @memberof App
615
+ */
616
+ defineResizeObserver(container, canvas) {
617
+ // listen to resize events
618
+ const resizeObserver = new ResizeObserver((entries) => {
619
+ // make sure canvas has been built
620
+ if (container.clientWidth > 0 && container.clientHeight > 0) {
621
+ // make sure we dont exceed loop limit first
622
+ // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
623
+ window.requestAnimationFrame(() => {
624
+ if (!Array.isArray(entries) || !entries.length) {
625
+ return
626
+ }
627
+ // update the app and canvas size
628
+ if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
629
+ this.h = container.clientHeight
630
+ this.w = container.clientWidth
631
+ this.cg.h = container.clientHeight
632
+ this.cg.w = container.clientWidth
633
+ canvas.setAttribute('width', '' + this.w)
634
+ canvas.setAttribute('height', '' + this.h)
635
+ this.redraw()
636
+ }
637
+ })
638
+ }
639
+ })
640
+
641
+ resizeObserver.observe(container)
642
+ }
643
+
644
+ /**
645
+ * @description Destroy the app and it's event listeners
646
+ * This should significantly reduce the memory used when creating and destroying gridviz app instances (for example in leaflet-gridviz)
647
+ * @memberof App
648
+ */
649
+ destroy() {
650
+ // clear layers
651
+ this.layers = []
652
+ this.bgLayers = []
653
+
654
+ // remove event listeners from container
655
+ this.container.removeEventListener('mouseover', this.mouseOverHandler)
656
+ this.container.removeEventListener('mousemove', this.mouseMoveHandler)
657
+ this.container.removeEventListener('mouseout', this.mouseOutHandler)
658
+
659
+ // remove canvas
660
+ this.cg.canvas.remove()
661
+
662
+ // remove legend
663
+ this.legend?.remove()
664
+
665
+ // remove tooltip
666
+ this.tooltip.tooltip?.remove()
667
+ }
668
+ }