gridviz 2.1.13 → 2.1.15

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