gridviz 2.1.9 → 2.1.11

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