gridviz 2.1.13 → 2.1.14

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 (60) hide show
  1. package/README.md +124 -125
  2. package/dist/gridviz.js +8341 -8339
  3. package/dist/gridviz.min.js +1 -1
  4. package/package.json +77 -77
  5. package/src/App.js +758 -758
  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 -113
  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 +131 -129
  16. package/src/Tooltip.js +180 -180
  17. package/src/button/Button.js +54 -54
  18. package/src/button/FullscreenButton.js +98 -98
  19. package/src/button/ZoomButtons.js +72 -72
  20. package/src/dataset/COGeo.js +24 -24
  21. package/src/dataset/CSVGrid.js +104 -104
  22. package/src/dataset/GeoTIFF.js +127 -127
  23. package/src/dataset/GridTile.js +41 -41
  24. package/src/dataset/LGrid.js +54 -54
  25. package/src/dataset/TiledGrid.js +264 -264
  26. package/src/index.js +71 -71
  27. package/src/legend/ColorCategoryLegend.js +124 -124
  28. package/src/legend/ColorDiscreteLegend.js +103 -103
  29. package/src/legend/ColorLegend.js +128 -128
  30. package/src/legend/SegmentOrientationLegend.js +87 -87
  31. package/src/legend/SegmentWidthLegend.js +101 -101
  32. package/src/legend/SizeLegend.js +141 -141
  33. package/src/style/CompositionStyle.js +300 -300
  34. package/src/style/ContourStyle.js +54 -54
  35. package/src/style/DotDensityStyle.js +156 -156
  36. package/src/style/IsoFenceStyle.js +262 -262
  37. package/src/style/JoyPlotStyle.js +135 -135
  38. package/src/style/LegoStyle.js +112 -112
  39. package/src/style/MosaicStyle.js +107 -107
  40. package/src/style/NinjaStarStyle.js +125 -125
  41. package/src/style/PillarStyle.js +230 -230
  42. package/src/style/SegmentStyle.js +142 -142
  43. package/src/style/ShapeColorSizeStyle.js +152 -152
  44. package/src/style/SideCatStyle.js +172 -172
  45. package/src/style/SideStyle.js +216 -216
  46. package/src/style/SquareColorCatWGLStyle.js +120 -120
  47. package/src/style/SquareColorWGLStyle.js +146 -146
  48. package/src/style/StrokeStyle.js +131 -131
  49. package/src/style/TanakaStyle.js +160 -160
  50. package/src/style/TextStyle.js +132 -132
  51. package/src/style/TimeSeriesStyle.js +259 -259
  52. package/src/utils/Utils.js +35 -35
  53. package/src/utils/WebGLRectangleColoring.js +106 -106
  54. package/src/utils/WebGLSquareColoring.js +118 -118
  55. package/src/utils/WebGLSquareColoringAdvanced.js +230 -230
  56. package/src/utils/WebGLSquareColoringCatAdvanced.js +133 -133
  57. package/src/utils/WebGLSquareColoringKS.js +153 -153
  58. package/src/utils/stretching.js +131 -131
  59. package/src/utils/webGLUtils.js +71 -71
  60. package/src/utils/zoomUtils.js +142 -142
package/src/App.js CHANGED
@@ -1,758 +1,758 @@
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
- import { ZoomButtons } from './button/ZoomButtons.js'
18
- import { FullscreenButton } from './button/FullscreenButton.js'
19
-
20
- // external imports
21
- import { select } from 'd3-selection'
22
-
23
- /**
24
- * A gridviz application.
25
- *
26
- * @author Joseph Davies, Julien Gaffuri
27
- */
28
- export class App {
29
- /**
30
- * @param {HTMLDivElement} container
31
- * @param {object} opts
32
- */
33
- constructor(container, opts) {
34
- opts = opts || {}
35
-
36
- /**
37
- * The layers.
38
- * @type {Array.<Layer>}
39
- * */
40
- this.layers = []
41
-
42
- //get container element
43
- this.container = container || document.getElementById('gridviz')
44
- if (!this.container) {
45
- console.error('Cannot find gridviz container element.')
46
- return
47
- }
48
-
49
- //set dimensions
50
- /** @type {number} */
51
- this.w = opts.w || this.container.offsetWidth
52
- /** @type {number} */
53
- this.h = opts.h || this.container.offsetHeight
54
-
55
- //create canvas element if user doesnt specify one
56
- /** @type {HTMLCanvasElement} */
57
- let canvas = opts.canvas || null
58
- if (!canvas) {
59
- canvas = document.createElement('canvas')
60
- canvas.setAttribute('width', '' + this.w)
61
- canvas.setAttribute('height', '' + this.h)
62
- this.container.appendChild(canvas)
63
- }
64
-
65
- /** Make geo canvas
66
- * @type {GeoCanvas}
67
- * @private */
68
- this.cg = new GeoCanvas(canvas, undefined, 1, opts)
69
- this.cg.redraw = (strong = true) => {
70
- if (monitor) monitorDuration('Start redraw')
71
- //console.log("?x=" + this.cg.getCenter().x + "&y=" + this.cg.getCenter().y + "&z=" + this.cg.getZf())
72
-
73
- //remove legend elements
74
- if (this.legend && strong) this.legend.selectAll('*').remove()
75
-
76
- //clear
77
- this.cg.initCanvasTransform()
78
- this.cg.clear(this.cg.backgroundColor)
79
-
80
- const zf = this.getZoomFactor()
81
- this.updateExtentGeo()
82
-
83
- //go through the background layers
84
- if (this.showBgLayers)
85
- for (const layer of this.bgLayers) {
86
- //check if layer is visible
87
- if (!layer.visible) continue
88
- if (zf > layer.maxZoom) continue
89
- if (zf < layer.minZoom) continue
90
-
91
- //draw layer
92
- layer.draw(this.cg)
93
- }
94
-
95
- //go through the layers
96
- for (const layer of this.layers) {
97
- //check if layer is visible
98
- if (!layer.visible) continue
99
- if (zf > layer.maxZoom) continue
100
- if (zf < layer.minZoom) continue
101
-
102
- //get layer dataset component
103
- /** @type {import('./DatasetComponent').DatasetComponent|undefined} */
104
- const dsc = layer.getDatasetComponent(zf)
105
- if (!dsc) continue
106
-
107
- //launch data download, if necessary
108
- if (strong)
109
- dsc.getData(this.cg.extGeo, () => {
110
- this.cg.redraw()
111
- })
112
-
113
- //update dataset view cache
114
- if (strong) dsc.updateViewCache(this.cg.extGeo)
115
-
116
- //set layer alpha and blend mode
117
- this.cg.ctx.globalAlpha = layer.alpha ? layer.alpha(zf) : 1.0
118
- this.cg.ctx.globalCompositeOperation = layer.blendOperation(zf)
119
-
120
- //draw cells, style by style
121
- if (strong)
122
- for (const s of layer.styles) {
123
- //check if style is visible
124
- if (!s.visible) continue
125
- if (zf > s.maxZoom) continue
126
- if (zf < s.minZoom) continue
127
-
128
- //set style alpha and blend mode
129
- //TODO: multiply by layer alpha ?
130
- this.cg.ctx.globalAlpha = s.alpha ? s.alpha(zf) : 1.0
131
- this.cg.ctx.globalCompositeOperation = s.blendOperation(zf)
132
-
133
- s.draw(dsc.getViewCache(), dsc.getResolution(), this.cg)
134
- }
135
-
136
- //add legend element
137
- if (this.legend && strong) {
138
- for (const s of layer.styles) {
139
- if (zf > s.maxZoom) continue
140
- if (zf < s.minZoom) continue
141
- for (const lg of s.legends) {
142
- //console.log(s, lg)
143
- //this.legend.append(lg.div)
144
- //s1.node().appendChild(s2.node())
145
- this.legend.node().append(lg.div.node())
146
- }
147
-
148
- //case for styles of styles, like kernel smoothing
149
- //TODO do better
150
- if (s['styles']) {
151
- for (const s2 of s.styles) {
152
- if (zf > s2.maxZoom) continue
153
- if (zf < s2.minZoom) continue
154
- for (const lg of s2.legends) {
155
- //console.log(s, lg)
156
- //this.legend.append(lg.div)
157
- //s1.node().appendChild(s2.node())
158
- this.legend.node().append(lg.div.node())
159
- }
160
- }
161
- }
162
- }
163
- }
164
-
165
- //restore default alpha and blend operation
166
- this.cg.ctx.globalAlpha = 1.0
167
- this.cg.ctx.globalCompositeOperation = this.defaultGlobalCompositeOperation
168
- }
169
-
170
- //draw boundary layer
171
- //if (strong)
172
- if (this.showBoundaries && this.boundaryLayer) this.boundaryLayer.draw(this.cg)
173
-
174
- //draw label layer
175
- //if (strong)
176
- if (this.showLabels && this.labelLayer) this.labelLayer.draw(this.cg)
177
-
178
- //
179
- this.canvasSave = null
180
-
181
- if (monitor) monitorDuration('End redraw')
182
-
183
- // listen for resize events on the App's container and handle them
184
- this.defineResizeObserver(this.container, canvas)
185
-
186
- return this
187
- }
188
-
189
- /** @type {Array.<BackgroundLayer|BackgroundLayerWMS>} */
190
- this.bgLayers = []
191
- /** @type {boolean} */
192
- this.showBgLayers = true
193
-
194
- /** @type {LabelLayer | undefined} */
195
- this.labelLayer = undefined
196
- /** @type {boolean} */
197
- this.showLabels = true
198
-
199
- /** @type {LineLayer | undefined} */
200
- this.boundaryLayer = undefined
201
- /** @type {boolean} */
202
- this.showBoundaries = true
203
-
204
- //legend div
205
-
206
- this.legendDivId = opts.legendDivId || 'gvizLegend'
207
- this.legend = select('#' + this.legendDivId)
208
- if (this.legend.empty()) {
209
- this.legend = select(
210
- this.container.id && this.container.id != '' ? '#' + this.container.id : 'body'
211
- )
212
- .append('div')
213
- .attr('id', this.legendDivId)
214
- .style('position', 'absolute')
215
- .style('width', 'auto')
216
- .style('height', 'auto')
217
- .style('background', '#FFFFFFCC')
218
- //.style("padding", this.padding)
219
- .style('border', '0px')
220
- .style('border-radius', '5px')
221
- .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
222
- .style('font-family', 'Helvetica, Arial, sans-serif')
223
- .style('top', '20px')
224
- .style('right', '20px')
225
- //hide
226
- //.style("visibility", "hidden")
227
- }
228
-
229
- //tooltip
230
-
231
- // set App container as default parent element for tooltip
232
- if (!opts.tooltip) opts.tooltip = {}
233
- if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
234
-
235
- /**
236
- * @private
237
- * @type {Tooltip} */
238
- this.tooltip = new Tooltip(opts.tooltip)
239
-
240
- /** @param {MouseEvent} e */
241
- const focusCell = (e) => {
242
- //compute mouse geo position
243
- const mousePositionGeo = {
244
- x: this.cg.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
245
- y: this.cg.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
246
- }
247
- /** @type {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined} */
248
- const focus = this.getCellFocusInfo(mousePositionGeo)
249
-
250
- // transparent background (e.g. leaflet) 'red painting' fix
251
- if (opts.transparentBackground) {
252
- if (focus) {
253
- this.tooltip.html(focus.html)
254
- this.tooltip.setPosition(e)
255
- this.tooltip.show()
256
- } else {
257
- this.tooltip.hide()
258
- }
259
- this.canvasSave = document.createElement('canvas')
260
- this.canvasSave.setAttribute('width', '' + this.w)
261
- this.canvasSave.setAttribute('height', '' + this.h)
262
- this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
263
- this.cg.initCanvasTransform()
264
- return
265
- }
266
-
267
- if (focus) {
268
- this.tooltip.html(focus.html)
269
- this.tooltip.setPosition(e)
270
- this.tooltip.show()
271
-
272
- //show cell position as a rectangle
273
- if (!this.canvasSave) {
274
- this.canvasSave = document.createElement('canvas')
275
- this.canvasSave.setAttribute('width', '' + this.w)
276
- this.canvasSave.setAttribute('height', '' + this.h)
277
- this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
278
- } else {
279
- this.cg.ctx.drawImage(this.canvasSave, 0, 0)
280
- }
281
-
282
- //draw image saved + draw rectangle
283
- const rectWPix = this.selectionRectangleWidthPix
284
- ? this.selectionRectangleWidthPix(focus.resolution, this.getZoomFactor())
285
- : 4
286
- this.cg.initCanvasTransform()
287
- this.cg.ctx.strokeStyle = this.selectionRectangleColor
288
- this.cg.ctx.lineWidth = rectWPix
289
- this.cg.ctx.beginPath()
290
-
291
- this.cg.ctx.rect(
292
- this.cg.geoToPixX(focus.cell.x) - rectWPix / 2,
293
- this.cg.geoToPixY(focus.cell.y) + rectWPix / 2,
294
- focus.resolution / this.getZoomFactor() + rectWPix,
295
- -focus.resolution / this.getZoomFactor() - rectWPix
296
- )
297
- this.cg.ctx.stroke()
298
- } else {
299
- this.tooltip.hide()
300
- if (this.canvasSave) this.cg.ctx.drawImage(this.canvasSave, 0, 0)
301
- }
302
- }
303
-
304
- // add event listeners to container
305
- this.mouseOverHandler = (e) => focusCell(e)
306
- this.mouseMoveHandler = (e) => focusCell(e)
307
- this.mouseOutHandler = (e) => this.tooltip.hide()
308
- this.container.addEventListener('mouseover', this.mouseOverHandler)
309
- this.container.addEventListener('mousemove', this.mouseMoveHandler)
310
- this.container.addEventListener('mouseout', this.mouseOutHandler)
311
-
312
- // add extra logic to onZoomStartFun
313
- this.cg.onZoomStartFun = (e) => {
314
- if (opts.onZoomStartFun) opts.onZoomStartFun(e)
315
- this.tooltip.hide()
316
- }
317
-
318
- //for mouse over
319
- /**
320
- * @private
321
- * @type {HTMLCanvasElement|null} */
322
- this.canvasSave = null
323
-
324
- this.selectionRectangleColor = opts.selectionRectangleColor || 'red'
325
- this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,zf) => {}
326
-
327
- //
328
- //canvas.addEventListener("keydown", e => { console.log(arguments) });
329
-
330
- //set default globalCompositeOperation
331
- this.defaultGlobalCompositeOperation =
332
- opts.defaultGlobalCompositeOperation || this.cg.ctx.globalCompositeOperation
333
- }
334
-
335
- /**
336
- * @param {number} marginPx
337
- * @returns {import('./Dataset').Envelope}
338
- * @public
339
- */
340
- updateExtentGeo(marginPx = 20) {
341
- return this.cg.updateExtentGeo(marginPx)
342
- }
343
-
344
- /**
345
- * Return the cell HTML info at a given geo position.
346
- * This is usefull for user interactions, to show this info where the user clicks for example.
347
- *
348
- * @param {{x:number,y:number}} posGeo
349
- * @returns {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined}
350
- * @protected
351
- */
352
- getCellFocusInfo(posGeo) {
353
- //go through the layers, starting from top
354
- const zf = this.getZoomFactor()
355
- for (let i = this.layers.length - 1; i >= 0; i--) {
356
- /** @type {Layer} */
357
- const layer = this.layers[i]
358
- if (!layer.visible) continue
359
- if (!layer.cellInfoHTML) continue
360
- //if (layer.cellInfoHTML === 'none') continue
361
- const dsc = layer.getDatasetComponent(zf)
362
- if (!dsc) continue
363
-
364
- //get cell at mouse position
365
- /** @type {import('./Dataset').Cell|undefined} */
366
- const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
367
- //console.log(cell, dsc.resolution)
368
- if (!cell) return undefined
369
- const html = layer.cellInfoHTML(cell, dsc.getResolution())
370
- if (!html) return undefined
371
- return { cell: cell, html: html, resolution: dsc.getResolution() }
372
- }
373
- }
374
-
375
- //getters and setters
376
-
377
- /** @returns {{x:number,y:number}} */
378
- getGeoCenter() {
379
- return this.cg.getCenter()
380
- }
381
- /** @param {{x:number,y:number}} val @returns {this} */
382
- setGeoCenter(val) {
383
- this.cg.setCenter(val)
384
- return this
385
- }
386
-
387
- /** @returns {number} */
388
- getZoomFactor() {
389
- return this.cg.getZf()
390
- }
391
- /** @param {number} val @returns {this} */
392
- setZoomFactor(val) {
393
- this.cg.setZf(val)
394
- return this
395
- }
396
-
397
- /** @returns {Array.<number>} */
398
- getZoomFactorExtent() {
399
- return this.cg.getZfExtent()
400
- }
401
- /** @param {Array.<number>} val @returns {this} */
402
- setZoomFactorExtent(val) {
403
- this.cg.setZfExtent(val)
404
- return this
405
- }
406
-
407
- /** @returns {string} */
408
- getBackgroundColor() {
409
- return this.cg.backgroundColor
410
- }
411
- /** @param {string} val @returns {this} */
412
- setBackgroundColor(val) {
413
- this.cg.backgroundColor = val
414
- return this
415
- }
416
-
417
- /** @returns {LineLayer | undefined} */
418
- getBoundaryLayer() {
419
- return this.boundaryLayer
420
- }
421
- /** @param {object} opts @returns {this} */
422
- setBoundaryLayer(opts) {
423
- this.boundaryLayer = new LineLayer(opts)
424
- return this
425
- }
426
-
427
- /** @returns {LabelLayer | undefined} */
428
- getLabelLayer() {
429
- return this.labelLayer
430
- }
431
- /** @param {object} opts @returns {this} */
432
- setLabelLayer(opts) {
433
- this.labelLayer = new LabelLayer(opts)
434
- return this
435
- }
436
-
437
- /** @returns {this} */
438
- redraw() {
439
- this.cg.redraw()
440
- return this
441
- }
442
-
443
- /**
444
- * Add a layer to the app.
445
- *
446
- * @param {Dataset} dataset The dataset of the layer
447
- * @param {Array.<import('./Style').Style>} styles The styles of the layer
448
- * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string}} opts The layer options.
449
- * @returns {this}
450
- */
451
- addLayerFromDataset(dataset, styles, opts) {
452
- const lay = new Layer(dataset, styles, opts)
453
- this.layers.push(lay)
454
- return this
455
- }
456
-
457
- //dataset creation
458
-
459
- /**
460
- * Make a local grid dataset.
461
- *
462
- * @param {number} resolution The dataset resolution in geographical unit.
463
- * @param {Array} cells The cells.
464
- * @param {object=} opts The parameters of the dataset.
465
- * @returns {Dataset}
466
- */
467
- makeLGridDataset(resolution, cells, opts) {
468
- return new Dataset([new LGrid(resolution, cells)], [], opts)
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
- /**
642
- * Adds a set of zoom buttons to the app
643
- *
644
- * @param {object} opts
645
- * @returns {this}
646
- */
647
- addZoomButtons(opts) {
648
- // * opts.id
649
- // * opts.onZoom - custom event handler function
650
- // * opts.x
651
- // * opts.y
652
- // * opts.delta - zoom delta applied on each click
653
-
654
- this.zoomButtons = new ZoomButtons({
655
- app: this,
656
- id: opts?.id || 'gridviz-zoom-buttons',
657
- class: opts?.class,
658
- x: opts?.x || this.w - 50,
659
- y: opts?.y || 10,
660
- onZoom: opts?.onZoom,
661
- delta: opts?.delta || 0.2
662
- })
663
-
664
- return this
665
- }
666
-
667
- /**
668
- * Adds a fullscreen toggle button to the app
669
- *
670
- * @param {object} opts
671
- * @returns {this}
672
- */
673
- addFullscreenButton(opts) {
674
- // * opts.app - the gridviz app
675
- // * opts.id
676
- // * opts.x
677
- // * opts.y
678
-
679
- this.fullscreenButton = new FullscreenButton({
680
- app: this,
681
- id: opts?.id || 'gridviz-fullscreen-button',
682
- class: opts?.class,
683
- x: opts?.x || this.w - 50,
684
- y: opts?.y || 85,
685
- })
686
-
687
- return this
688
- }
689
-
690
- /** @returns {this} */
691
- setViewFromURL() {
692
- this.cg.setViewFromURL()
693
- return this
694
- }
695
-
696
- /**
697
- * @description Add a resize event observer to the Apps container and update the canvas accordingly
698
- * @param {HTMLDivElement} container The App's container element
699
- * @param {HTMLCanvasElement} canvas The App canvas element
700
- * @memberof App
701
- */
702
- defineResizeObserver(container, canvas) {
703
- // listen to resize events
704
- const resizeObserver = new ResizeObserver((entries) => {
705
- // make sure canvas has been built
706
- if (container.clientWidth > 0 && container.clientHeight > 0) {
707
- // make sure we dont exceed loop limit first
708
- // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
709
- window.requestAnimationFrame(() => {
710
- if (!Array.isArray(entries) || !entries.length) {
711
- return
712
- }
713
- // update the app and canvas size
714
- if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
715
- this.h = container.clientHeight
716
- this.w = container.clientWidth
717
- this.cg.h = container.clientHeight
718
- this.cg.w = container.clientWidth
719
- canvas.setAttribute('width', '' + this.w)
720
- canvas.setAttribute('height', '' + this.h)
721
- this.redraw()
722
-
723
- //update button positions
724
- if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
725
- if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
726
- }
727
- })
728
- }
729
- })
730
-
731
- resizeObserver.observe(container)
732
- }
733
-
734
- /**
735
- * @description Destroy the app and it's event listeners
736
- * This should significantly reduce the memory used when creating and destroying gridviz app instances (for example in leaflet-gridviz)
737
- * @memberof App
738
- */
739
- destroy() {
740
- // clear layers
741
- this.layers = []
742
- this.bgLayers = []
743
-
744
- // remove event listeners from container
745
- this.container.removeEventListener('mouseover', this.mouseOverHandler)
746
- this.container.removeEventListener('mousemove', this.mouseMoveHandler)
747
- this.container.removeEventListener('mouseout', this.mouseOutHandler)
748
-
749
- // remove canvas
750
- this.cg.canvas.remove()
751
-
752
- // remove legend
753
- this.legend?.remove()
754
-
755
- // remove tooltip
756
- this.tooltip.tooltip?.remove()
757
- }
758
- }
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
+ import { ZoomButtons } from './button/ZoomButtons.js'
18
+ import { FullscreenButton } from './button/FullscreenButton.js'
19
+
20
+ // external imports
21
+ import { select } from 'd3-selection'
22
+
23
+ /**
24
+ * A gridviz application.
25
+ *
26
+ * @author Joseph Davies, Julien Gaffuri
27
+ */
28
+ export class App {
29
+ /**
30
+ * @param {HTMLDivElement} container
31
+ * @param {object} opts
32
+ */
33
+ constructor(container, opts) {
34
+ opts = opts || {}
35
+
36
+ /**
37
+ * The layers.
38
+ * @type {Array.<Layer>}
39
+ * */
40
+ this.layers = []
41
+
42
+ //get container element
43
+ this.container = container || document.getElementById('gridviz')
44
+ if (!this.container) {
45
+ console.error('Cannot find gridviz container element.')
46
+ return
47
+ }
48
+
49
+ //set dimensions
50
+ /** @type {number} */
51
+ this.w = opts.w || this.container.offsetWidth
52
+ /** @type {number} */
53
+ this.h = opts.h || this.container.offsetHeight
54
+
55
+ //create canvas element if user doesnt specify one
56
+ /** @type {HTMLCanvasElement} */
57
+ let canvas = opts.canvas || null
58
+ if (!canvas) {
59
+ canvas = document.createElement('canvas')
60
+ canvas.setAttribute('width', '' + this.w)
61
+ canvas.setAttribute('height', '' + this.h)
62
+ this.container.appendChild(canvas)
63
+ }
64
+
65
+ /** Make geo canvas
66
+ * @type {GeoCanvas}
67
+ * @private */
68
+ this.cg = new GeoCanvas(canvas, undefined, 1, opts)
69
+ this.cg.redraw = (strong = true) => {
70
+ if (monitor) monitorDuration('Start redraw')
71
+ //console.log("?x=" + this.cg.getCenter().x + "&y=" + this.cg.getCenter().y + "&z=" + this.cg.getZf())
72
+
73
+ //remove legend elements
74
+ if (this.legend && strong) this.legend.selectAll('*').remove()
75
+
76
+ //clear
77
+ this.cg.initCanvasTransform()
78
+ this.cg.clear(this.cg.backgroundColor)
79
+
80
+ const zf = this.getZoomFactor()
81
+ this.updateExtentGeo()
82
+
83
+ //go through the background layers
84
+ if (this.showBgLayers)
85
+ for (const layer of this.bgLayers) {
86
+ //check if layer is visible
87
+ if (!layer.visible) continue
88
+ if (zf > layer.maxZoom) continue
89
+ if (zf < layer.minZoom) continue
90
+
91
+ //draw layer
92
+ layer.draw(this.cg)
93
+ }
94
+
95
+ //go through the layers
96
+ for (const layer of this.layers) {
97
+ //check if layer is visible
98
+ if (!layer.visible) continue
99
+ if (zf > layer.maxZoom) continue
100
+ if (zf < layer.minZoom) continue
101
+
102
+ //get layer dataset component
103
+ /** @type {import('./DatasetComponent').DatasetComponent|undefined} */
104
+ const dsc = layer.getDatasetComponent(zf)
105
+ if (!dsc) continue
106
+
107
+ //launch data download, if necessary
108
+ if (strong)
109
+ dsc.getData(this.cg.extGeo, () => {
110
+ this.cg.redraw()
111
+ })
112
+
113
+ //update dataset view cache
114
+ if (strong) dsc.updateViewCache(this.cg.extGeo)
115
+
116
+ //set layer alpha and blend mode
117
+ this.cg.ctx.globalAlpha = layer.alpha ? layer.alpha(zf) : 1.0
118
+ this.cg.ctx.globalCompositeOperation = layer.blendOperation(zf)
119
+
120
+ //draw cells, style by style
121
+ if (strong)
122
+ for (const s of layer.styles) {
123
+ //check if style is visible
124
+ if (!s.visible) continue
125
+ if (zf > s.maxZoom) continue
126
+ if (zf < s.minZoom) continue
127
+
128
+ //set style alpha and blend mode
129
+ //TODO: multiply by layer alpha ?
130
+ this.cg.ctx.globalAlpha = s.alpha ? s.alpha(zf) : 1.0
131
+ this.cg.ctx.globalCompositeOperation = s.blendOperation(zf)
132
+
133
+ s.draw(dsc.getViewCache(), dsc.getResolution(), this.cg)
134
+ }
135
+
136
+ //add legend element
137
+ if (this.legend && strong) {
138
+ for (const s of layer.styles) {
139
+ if (zf > s.maxZoom) continue
140
+ if (zf < s.minZoom) continue
141
+ for (const lg of s.legends) {
142
+ //console.log(s, lg)
143
+ //this.legend.append(lg.div)
144
+ //s1.node().appendChild(s2.node())
145
+ this.legend.node().append(lg.div.node())
146
+ }
147
+
148
+ //case for styles of styles, like kernel smoothing
149
+ //TODO do better
150
+ if (s['styles']) {
151
+ for (const s2 of s.styles) {
152
+ if (zf > s2.maxZoom) continue
153
+ if (zf < s2.minZoom) continue
154
+ for (const lg of s2.legends) {
155
+ //console.log(s, lg)
156
+ //this.legend.append(lg.div)
157
+ //s1.node().appendChild(s2.node())
158
+ this.legend.node().append(lg.div.node())
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ //restore default alpha and blend operation
166
+ this.cg.ctx.globalAlpha = 1.0
167
+ this.cg.ctx.globalCompositeOperation = this.defaultGlobalCompositeOperation
168
+ }
169
+
170
+ //draw boundary layer
171
+ //if (strong)
172
+ if (this.showBoundaries && this.boundaryLayer) this.boundaryLayer.draw(this.cg)
173
+
174
+ //draw label layer
175
+ //if (strong)
176
+ if (this.showLabels && this.labelLayer) this.labelLayer.draw(this.cg)
177
+
178
+ //
179
+ this.canvasSave = null
180
+
181
+ if (monitor) monitorDuration('End redraw')
182
+
183
+ // listen for resize events on the App's container and handle them
184
+ this.defineResizeObserver(this.container, canvas)
185
+
186
+ return this
187
+ }
188
+
189
+ /** @type {Array.<BackgroundLayer|BackgroundLayerWMS>} */
190
+ this.bgLayers = []
191
+ /** @type {boolean} */
192
+ this.showBgLayers = true
193
+
194
+ /** @type {LabelLayer | undefined} */
195
+ this.labelLayer = undefined
196
+ /** @type {boolean} */
197
+ this.showLabels = true
198
+
199
+ /** @type {LineLayer | undefined} */
200
+ this.boundaryLayer = undefined
201
+ /** @type {boolean} */
202
+ this.showBoundaries = true
203
+
204
+ //legend div
205
+
206
+ this.legendDivId = opts.legendDivId || 'gvizLegend'
207
+ this.legend = select('#' + this.legendDivId)
208
+ if (this.legend.empty()) {
209
+ this.legend = select(
210
+ this.container.id && this.container.id != '' ? '#' + this.container.id : 'body'
211
+ )
212
+ .append('div')
213
+ .attr('id', this.legendDivId)
214
+ .style('position', 'absolute')
215
+ .style('width', 'auto')
216
+ .style('height', 'auto')
217
+ .style('background', '#FFFFFFCC')
218
+ //.style("padding", this.padding)
219
+ .style('border', '0px')
220
+ .style('border-radius', '5px')
221
+ .style('box-shadow', '3px 3px 3px grey, -3px -3px 3px #ddd')
222
+ .style('font-family', 'Helvetica, Arial, sans-serif')
223
+ .style('top', '20px')
224
+ .style('right', '20px')
225
+ //hide
226
+ //.style("visibility", "hidden")
227
+ }
228
+
229
+ //tooltip
230
+
231
+ // set App container as default parent element for tooltip
232
+ if (!opts.tooltip) opts.tooltip = {}
233
+ if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
234
+
235
+ /**
236
+ * @private
237
+ * @type {Tooltip} */
238
+ this.tooltip = new Tooltip(opts.tooltip)
239
+
240
+ /** @param {MouseEvent} e */
241
+ const focusCell = (e) => {
242
+ //compute mouse geo position
243
+ const mousePositionGeo = {
244
+ x: this.cg.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
245
+ y: this.cg.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
246
+ }
247
+ /** @type {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined} */
248
+ const focus = this.getCellFocusInfo(mousePositionGeo)
249
+
250
+ // transparent background (e.g. leaflet) 'red painting' fix
251
+ if (opts.transparentBackground) {
252
+ if (focus) {
253
+ this.tooltip.html(focus.html)
254
+ this.tooltip.setPosition(e)
255
+ this.tooltip.show()
256
+ } else {
257
+ this.tooltip.hide()
258
+ }
259
+ this.canvasSave = document.createElement('canvas')
260
+ this.canvasSave.setAttribute('width', '' + this.w)
261
+ this.canvasSave.setAttribute('height', '' + this.h)
262
+ this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
263
+ this.cg.initCanvasTransform()
264
+ return
265
+ }
266
+
267
+ if (focus) {
268
+ this.tooltip.html(focus.html)
269
+ this.tooltip.setPosition(e)
270
+ this.tooltip.show()
271
+
272
+ //show cell position as a rectangle
273
+ if (!this.canvasSave) {
274
+ this.canvasSave = document.createElement('canvas')
275
+ this.canvasSave.setAttribute('width', '' + this.w)
276
+ this.canvasSave.setAttribute('height', '' + this.h)
277
+ this.canvasSave.getContext('2d').drawImage(this.cg.canvas, 0, 0)
278
+ } else {
279
+ this.cg.ctx.drawImage(this.canvasSave, 0, 0)
280
+ }
281
+
282
+ //draw image saved + draw rectangle
283
+ const rectWPix = this.selectionRectangleWidthPix
284
+ ? this.selectionRectangleWidthPix(focus.resolution, this.getZoomFactor())
285
+ : 4
286
+ this.cg.initCanvasTransform()
287
+ this.cg.ctx.strokeStyle = this.selectionRectangleColor
288
+ this.cg.ctx.lineWidth = rectWPix
289
+ this.cg.ctx.beginPath()
290
+
291
+ this.cg.ctx.rect(
292
+ this.cg.geoToPixX(focus.cell.x) - rectWPix / 2,
293
+ this.cg.geoToPixY(focus.cell.y) + rectWPix / 2,
294
+ focus.resolution / this.getZoomFactor() + rectWPix,
295
+ -focus.resolution / this.getZoomFactor() - rectWPix
296
+ )
297
+ this.cg.ctx.stroke()
298
+ } else {
299
+ this.tooltip.hide()
300
+ if (this.canvasSave) this.cg.ctx.drawImage(this.canvasSave, 0, 0)
301
+ }
302
+ }
303
+
304
+ // add event listeners to container
305
+ this.mouseOverHandler = (e) => focusCell(e)
306
+ this.mouseMoveHandler = (e) => focusCell(e)
307
+ this.mouseOutHandler = (e) => this.tooltip.hide()
308
+ this.container.addEventListener('mouseover', this.mouseOverHandler)
309
+ this.container.addEventListener('mousemove', this.mouseMoveHandler)
310
+ this.container.addEventListener('mouseout', this.mouseOutHandler)
311
+
312
+ // add extra logic to onZoomStartFun
313
+ this.cg.onZoomStartFun = (e) => {
314
+ if (opts.onZoomStartFun) opts.onZoomStartFun(e)
315
+ this.tooltip.hide()
316
+ }
317
+
318
+ //for mouse over
319
+ /**
320
+ * @private
321
+ * @type {HTMLCanvasElement|null} */
322
+ this.canvasSave = null
323
+
324
+ this.selectionRectangleColor = opts.selectionRectangleColor || 'red'
325
+ this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,zf) => {}
326
+
327
+ //
328
+ //canvas.addEventListener("keydown", e => { console.log(arguments) });
329
+
330
+ //set default globalCompositeOperation
331
+ this.defaultGlobalCompositeOperation =
332
+ opts.defaultGlobalCompositeOperation || this.cg.ctx.globalCompositeOperation
333
+ }
334
+
335
+ /**
336
+ * @param {number} marginPx
337
+ * @returns {import('./Dataset').Envelope}
338
+ * @public
339
+ */
340
+ updateExtentGeo(marginPx = 20) {
341
+ return this.cg.updateExtentGeo(marginPx)
342
+ }
343
+
344
+ /**
345
+ * Return the cell HTML info at a given geo position.
346
+ * This is usefull for user interactions, to show this info where the user clicks for example.
347
+ *
348
+ * @param {{x:number,y:number}} posGeo
349
+ * @returns {{cell:import('./Dataset').Cell,html:string,resolution:number} | undefined}
350
+ * @protected
351
+ */
352
+ getCellFocusInfo(posGeo) {
353
+ //go through the layers, starting from top
354
+ const zf = this.getZoomFactor()
355
+ for (let i = this.layers.length - 1; i >= 0; i--) {
356
+ /** @type {Layer} */
357
+ const layer = this.layers[i]
358
+ if (!layer.visible) continue
359
+ if (!layer.cellInfoHTML) continue
360
+ //if (layer.cellInfoHTML === 'none') continue
361
+ const dsc = layer.getDatasetComponent(zf)
362
+ if (!dsc) continue
363
+
364
+ //get cell at mouse position
365
+ /** @type {import('./Dataset').Cell|undefined} */
366
+ const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
367
+ //console.log(cell, dsc.resolution)
368
+ if (!cell) return undefined
369
+ const html = layer.cellInfoHTML(cell, dsc.getResolution())
370
+ if (!html) return undefined
371
+ return { cell: cell, html: html, resolution: dsc.getResolution() }
372
+ }
373
+ }
374
+
375
+ //getters and setters
376
+
377
+ /** @returns {{x:number,y:number}} */
378
+ getGeoCenter() {
379
+ return this.cg.getCenter()
380
+ }
381
+ /** @param {{x:number,y:number}} val @returns {this} */
382
+ setGeoCenter(val) {
383
+ this.cg.setCenter(val)
384
+ return this
385
+ }
386
+
387
+ /** @returns {number} */
388
+ getZoomFactor() {
389
+ return this.cg.getZf()
390
+ }
391
+ /** @param {number} val @returns {this} */
392
+ setZoomFactor(val) {
393
+ this.cg.setZf(val)
394
+ return this
395
+ }
396
+
397
+ /** @returns {Array.<number>} */
398
+ getZoomFactorExtent() {
399
+ return this.cg.getZfExtent()
400
+ }
401
+ /** @param {Array.<number>} val @returns {this} */
402
+ setZoomFactorExtent(val) {
403
+ this.cg.setZfExtent(val)
404
+ return this
405
+ }
406
+
407
+ /** @returns {string} */
408
+ getBackgroundColor() {
409
+ return this.cg.backgroundColor
410
+ }
411
+ /** @param {string} val @returns {this} */
412
+ setBackgroundColor(val) {
413
+ this.cg.backgroundColor = val
414
+ return this
415
+ }
416
+
417
+ /** @returns {LineLayer | undefined} */
418
+ getBoundaryLayer() {
419
+ return this.boundaryLayer
420
+ }
421
+ /** @param {object} opts @returns {this} */
422
+ setBoundaryLayer(opts) {
423
+ this.boundaryLayer = new LineLayer(opts)
424
+ return this
425
+ }
426
+
427
+ /** @returns {LabelLayer | undefined} */
428
+ getLabelLayer() {
429
+ return this.labelLayer
430
+ }
431
+ /** @param {object} opts @returns {this} */
432
+ setLabelLayer(opts) {
433
+ this.labelLayer = new LabelLayer(opts)
434
+ return this
435
+ }
436
+
437
+ /** @returns {this} */
438
+ redraw() {
439
+ this.cg.redraw()
440
+ return this
441
+ }
442
+
443
+ /**
444
+ * Add a layer to the app.
445
+ *
446
+ * @param {Dataset} dataset The dataset of the layer
447
+ * @param {Array.<import('./Style').Style>} styles The styles of the layer
448
+ * @param {{visible?:boolean,minZoom?:number,maxZoom?:number,pixNb?:number,cellInfoHTML?:function(import('./Dataset').Cell):string}} opts The layer options.
449
+ * @returns {this}
450
+ */
451
+ addLayerFromDataset(dataset, styles, opts) {
452
+ const lay = new Layer(dataset, styles, opts)
453
+ this.layers.push(lay)
454
+ return this
455
+ }
456
+
457
+ //dataset creation
458
+
459
+ /**
460
+ * Make a local grid dataset.
461
+ *
462
+ * @param {number} resolution The dataset resolution in geographical unit.
463
+ * @param {Array} cells The cells.
464
+ * @param {object=} opts The parameters of the dataset.
465
+ * @returns {Dataset}
466
+ */
467
+ makeLGridDataset(resolution, cells, opts) {
468
+ return new Dataset([new LGrid(resolution, cells)], [], opts)
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
+ /**
642
+ * Adds a set of zoom buttons to the app
643
+ *
644
+ * @param {object} opts
645
+ * @returns {this}
646
+ */
647
+ addZoomButtons(opts) {
648
+ // * opts.id
649
+ // * opts.onZoom - custom event handler function
650
+ // * opts.x
651
+ // * opts.y
652
+ // * opts.delta - zoom delta applied on each click
653
+
654
+ this.zoomButtons = new ZoomButtons({
655
+ app: this,
656
+ id: opts?.id || 'gridviz-zoom-buttons',
657
+ class: opts?.class,
658
+ x: opts?.x || this.w - 50,
659
+ y: opts?.y || 10,
660
+ onZoom: opts?.onZoom,
661
+ delta: opts?.delta || 0.2
662
+ })
663
+
664
+ return this
665
+ }
666
+
667
+ /**
668
+ * Adds a fullscreen toggle button to the app
669
+ *
670
+ * @param {object} opts
671
+ * @returns {this}
672
+ */
673
+ addFullscreenButton(opts) {
674
+ // * opts.app - the gridviz app
675
+ // * opts.id
676
+ // * opts.x
677
+ // * opts.y
678
+
679
+ this.fullscreenButton = new FullscreenButton({
680
+ app: this,
681
+ id: opts?.id || 'gridviz-fullscreen-button',
682
+ class: opts?.class,
683
+ x: opts?.x || this.w - 50,
684
+ y: opts?.y || 85,
685
+ })
686
+
687
+ return this
688
+ }
689
+
690
+ /** @returns {this} */
691
+ setViewFromURL() {
692
+ this.cg.setViewFromURL()
693
+ return this
694
+ }
695
+
696
+ /**
697
+ * @description Add a resize event observer to the Apps container and update the canvas accordingly
698
+ * @param {HTMLDivElement} container The App's container element
699
+ * @param {HTMLCanvasElement} canvas The App canvas element
700
+ * @memberof App
701
+ */
702
+ defineResizeObserver(container, canvas) {
703
+ // listen to resize events
704
+ const resizeObserver = new ResizeObserver((entries) => {
705
+ // make sure canvas has been built
706
+ if (container.clientWidth > 0 && container.clientHeight > 0) {
707
+ // make sure we dont exceed loop limit first
708
+ // see: https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
709
+ window.requestAnimationFrame(() => {
710
+ if (!Array.isArray(entries) || !entries.length) {
711
+ return
712
+ }
713
+ // update the app and canvas size
714
+ if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
715
+ this.h = container.clientHeight
716
+ this.w = container.clientWidth
717
+ this.cg.h = container.clientHeight
718
+ this.cg.w = container.clientWidth
719
+ canvas.setAttribute('width', '' + this.w)
720
+ canvas.setAttribute('height', '' + this.h)
721
+ this.redraw()
722
+
723
+ //update button positions
724
+ if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px'
725
+ if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px'
726
+ }
727
+ })
728
+ }
729
+ })
730
+
731
+ resizeObserver.observe(container)
732
+ }
733
+
734
+ /**
735
+ * @description Destroy the app and it's event listeners
736
+ * This should significantly reduce the memory used when creating and destroying gridviz app instances (for example in leaflet-gridviz)
737
+ * @memberof App
738
+ */
739
+ destroy() {
740
+ // clear layers
741
+ this.layers = []
742
+ this.bgLayers = []
743
+
744
+ // remove event listeners from container
745
+ this.container.removeEventListener('mouseover', this.mouseOverHandler)
746
+ this.container.removeEventListener('mousemove', this.mouseMoveHandler)
747
+ this.container.removeEventListener('mouseout', this.mouseOutHandler)
748
+
749
+ // remove canvas
750
+ this.cg.canvas.remove()
751
+
752
+ // remove legend
753
+ this.legend?.remove()
754
+
755
+ // remove tooltip
756
+ this.tooltip.tooltip?.remove()
757
+ }
758
+ }