gridviz 2.0.41 → 2.0.42

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