gridviz 3.1.0 → 3.1.1
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.
- package/README.md +18 -15
- package/dist/gridviz.js +3829 -2623
- package/dist/gridviz.min.js +1 -1
- package/package.json +5 -3
- package/src/button/Button.js +77 -68
- package/src/button/FullscreenButton.js +123 -123
- package/src/button/ZoomButtons.js +109 -104
- package/src/core/GeoCanvas.js +15 -0
- package/src/core/Legend.js +267 -245
- package/src/core/Map.js +819 -670
- package/src/core/Tooltip.js +191 -185
- package/src/index.js +82 -79
- package/src/layer/GridLayer.js +4 -3
- package/src/layer/LabelLayer.js +7 -4
- package/src/legend/ColorCategoryLegend.js +127 -125
- package/src/legend/ColorDiscreteLegend.js +116 -114
- package/src/legend/ColorLegend.js +33 -11
- package/src/legend/OrientationLegend.js +94 -92
- package/src/legend/SizeLegend.js +8 -1
- package/src/legend/TernaryLegend.js +317 -314
- package/src/legend/VennLegend.js +202 -0
- package/src/style/CircleColorSizeWebGLStyle.js +249 -0
- package/src/style/CompositionStyle.js +1 -1
- package/src/style/DotDensityStyle.js +8 -4
- package/src/style/ImageStyle.js +1 -1
- package/src/style/Interpolator.js +283 -259
- package/src/style/IsoFenceStyle.js +1 -1
- package/src/style/LegoStyle.js +6 -5
- package/src/style/MosaicStyle.js +1 -1
- package/src/style/NinjaStarStyle.js +1 -1
- package/src/style/PillarStyle.js +1 -1
- package/src/style/RandomWalkInterpolator.js +213 -0
- package/src/style/ShadingRayStyle.js +16 -2
- package/src/style/ShadingStyle.js +80 -72
- package/src/style/ShapeColorSizeStyle.js +18 -2
- package/src/style/SideCategoryStyle.js +1 -1
- package/src/style/SideStyle.js +1 -1
- package/src/style/SideTanakaStyle.js +1 -1
- package/src/style/SquareColorCategoryWebGLStyle.js +71 -20
- package/src/style/SquareColorWebGLStyle.js +7 -4
- package/src/style/StrokeStyle.js +1 -1
- package/src/style/TextStyle.js +1 -1
- package/src/style/TimeSeriesStyle.js +14 -7
- package/src/utils/WebGLSquareColoringAdvanced.js +47 -9
- package/src/utils/scale.js +20 -0
- package/src/utils/webGLUtils.js +52 -0
package/src/core/Map.js
CHANGED
|
@@ -1,670 +1,819 @@
|
|
|
1
|
-
//@ts-check
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
// internal imports
|
|
5
|
-
import { isVisibleAt } from './Drawable.js'
|
|
6
|
-
import { GeoCanvas } from './GeoCanvas.js'
|
|
7
|
-
import { Tooltip } from './Tooltip.js'
|
|
8
|
-
import { ZoomButtons } from '../button/ZoomButtons.js'
|
|
9
|
-
import { FullscreenButton } from '../button/FullscreenButton.js'
|
|
10
|
-
|
|
11
|
-
// external imports
|
|
12
|
-
import { select } from 'd3-selection'
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* @
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
this.
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
this.container
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
this.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
this.
|
|
144
|
-
|
|
145
|
-
this.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
*
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.
|
|
188
|
-
.
|
|
189
|
-
.
|
|
190
|
-
.
|
|
191
|
-
.
|
|
192
|
-
.
|
|
193
|
-
.
|
|
194
|
-
.
|
|
195
|
-
|
|
196
|
-
.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
this.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
this.geoCanvas.
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* @
|
|
425
|
-
*/
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
//
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
this.geoCanvas.
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
this
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
this.
|
|
669
|
-
|
|
670
|
-
}
|
|
1
|
+
//@ts-check
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// internal imports
|
|
5
|
+
import { isVisibleAt } from './Drawable.js'
|
|
6
|
+
import { GeoCanvas } from './GeoCanvas.js'
|
|
7
|
+
import { Tooltip } from './Tooltip.js'
|
|
8
|
+
import { ZoomButtons } from '../button/ZoomButtons.js'
|
|
9
|
+
import { FullscreenButton } from '../button/FullscreenButton.js'
|
|
10
|
+
|
|
11
|
+
// external imports
|
|
12
|
+
import { select } from 'd3-selection'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Tells a cell from another across redraws, which can make the cells again: by position and resolution.
|
|
16
|
+
* @param {{cell:{x:any,y:any},resolution:number}} focus
|
|
17
|
+
* @returns {string} */
|
|
18
|
+
function cellKey(focus) {
|
|
19
|
+
return focus.cell.x + ',' + focus.cell.y + ',' + focus.resolution
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A gridviz application.
|
|
24
|
+
*
|
|
25
|
+
* @module core
|
|
26
|
+
* @author Joseph Davies, Julien Gaffuri
|
|
27
|
+
*/
|
|
28
|
+
export class Map {
|
|
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.<import("./Layer.js").Layer>}
|
|
39
|
+
* */
|
|
40
|
+
this.layers = opts.layers || []
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* When true (the default), redraw() schedules one redraw for the next animation frame
|
|
44
|
+
* instead of drawing at once, so the many redraw requests of a busy moment (every tile
|
|
45
|
+
* that arrives, every info.json, every setting change) are drawn once. redrawNow() still
|
|
46
|
+
* draws at once. Set it to false for redraw() to be synchronous, as it was before 3.1.0.
|
|
47
|
+
* @type {boolean} */
|
|
48
|
+
this.asyncRedraw = opts.asyncRedraw !== false
|
|
49
|
+
/** @private @type {number|null} */
|
|
50
|
+
this._redrawFrame = null
|
|
51
|
+
|
|
52
|
+
//get container element
|
|
53
|
+
this.container = container || document.getElementById('gridviz')
|
|
54
|
+
if (!this.container) {
|
|
55
|
+
console.error('Cannot find gridviz container element.')
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
this.container.classList.add('gridviz-map')
|
|
59
|
+
|
|
60
|
+
//https://css-tricks.com/absolute-positioning-inside-relative-positioning/
|
|
61
|
+
this.container.style.position = 'relative' // container element must have relative positioning
|
|
62
|
+
|
|
63
|
+
//set dimensions
|
|
64
|
+
/** @type {number} */
|
|
65
|
+
this.w = opts.w || this.container.offsetWidth
|
|
66
|
+
/** @type {number} */
|
|
67
|
+
this.h = opts.h || this.container.offsetHeight
|
|
68
|
+
|
|
69
|
+
// Create the main canvas (for rendering to screen)
|
|
70
|
+
/** @type {HTMLCanvasElement} */
|
|
71
|
+
this._canvas = opts.canvas || this.initialiseCanvas()
|
|
72
|
+
|
|
73
|
+
/** Initialize GeoCanvas
|
|
74
|
+
* @type {GeoCanvas}
|
|
75
|
+
* @private */
|
|
76
|
+
this.geoCanvas = new GeoCanvas(this._canvas, opts.x, opts.y, opts.z, opts)
|
|
77
|
+
|
|
78
|
+
this.geoCanvas.redraw = () => {
|
|
79
|
+
this.redraw()
|
|
80
|
+
}
|
|
81
|
+
this.geoCanvas.cancelCurrentRequests = () => {
|
|
82
|
+
// when the zoom level changes, avoid drawing outdated tiles, and ensure that requests are properly aborted when necessary
|
|
83
|
+
for (const layer of this.layers) {
|
|
84
|
+
//multires
|
|
85
|
+
if (layer.dataset?.datasets) {
|
|
86
|
+
for (const dataset of layer.dataset?.datasets) {
|
|
87
|
+
if (dataset?.cancelCurrentRequests) dataset.cancelCurrentRequests()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
//single res
|
|
91
|
+
if (layer.dataset?.cancelCurrentRequests) layer.dataset?.cancelCurrentRequests()
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// legend div
|
|
96
|
+
this.legend = opts.legendContainer
|
|
97
|
+
? select(opts.legendContainer) // Wrap the provided HTML node in a D3 selection
|
|
98
|
+
: null
|
|
99
|
+
this._legendProvidedByUser = !!opts.legendContainer
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Called with the legend container (an HTML element) each time the map has drawn and its legends
|
|
103
|
+
* are in the container, for a host that needs to adjust them (fit their width, restyle...).
|
|
104
|
+
* It can also be set after the map is created.
|
|
105
|
+
* @type {function(HTMLElement):void|undefined} */
|
|
106
|
+
this.onLegendUpdate = opts.onLegendUpdate
|
|
107
|
+
if (!this.legend) this.initialiseLegend()
|
|
108
|
+
|
|
109
|
+
//tooltip
|
|
110
|
+
|
|
111
|
+
// set App container as default parent element for tooltip
|
|
112
|
+
if (!opts.tooltip) opts.tooltip = {}
|
|
113
|
+
if (!opts.tooltip.parentElement) opts.tooltip.parentElement = this.container
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @private
|
|
117
|
+
* @type {Tooltip} */
|
|
118
|
+
this.tooltip = new Tooltip(opts.tooltip)
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Set to true so that the tooltip appears for the first layer starting from top where a cell is found.
|
|
122
|
+
* Otherwise, it shows a tooltip only for the first layer from the top with a cellInfoHTML property, even if there is no cell at the mouse position for this layer.
|
|
123
|
+
* @type {boolean} */
|
|
124
|
+
this.digForTooltip = opts.digForTooltip || false
|
|
125
|
+
|
|
126
|
+
// add event listeners to the canvas. Mouse moves are handled at most once per
|
|
127
|
+
// animation frame (the latest one): finding the cell and redrawing the highlight
|
|
128
|
+
// for every intermediate event only delays the one that's shown.
|
|
129
|
+
/** @private */
|
|
130
|
+
this._focusFrame = null
|
|
131
|
+
/** @private */
|
|
132
|
+
this._focusEvent = null
|
|
133
|
+
/** The last mouse event over the canvas, null when the mouse is out of it (see redrawNow)
|
|
134
|
+
* @private
|
|
135
|
+
* @type {MouseEvent|null} */
|
|
136
|
+
this._lastMouseEvent = null
|
|
137
|
+
/** True while the mouse is on a cell, which shows its rectangle and tooltip: the map gives them back after a redraw
|
|
138
|
+
* @private */
|
|
139
|
+
this._hovering = false
|
|
140
|
+
/** The cell the hover is on (see cellKey), to tell after a redraw if the mouse is still on the same one
|
|
141
|
+
* @private
|
|
142
|
+
* @type {string|null} */
|
|
143
|
+
this._hoverKey = null
|
|
144
|
+
/** @private */
|
|
145
|
+
this._refocusing = false
|
|
146
|
+
/** The layer and the dataset of the cell which is hovered (see _hoverKey): another layer, or dataset, at the same place is
|
|
147
|
+
* another cell, whose info is not the one of the tooltip (a time slider which changes the data for example)
|
|
148
|
+
* @private
|
|
149
|
+
* @type {{layer:*, dataset:*}|null} */
|
|
150
|
+
this._hoverSource = null
|
|
151
|
+
/** The cell whose info was asked for after a redraw, so that it is not asked again at each redraw
|
|
152
|
+
* @private
|
|
153
|
+
* @type {string|null} */
|
|
154
|
+
this._triedKey = null
|
|
155
|
+
/** @private
|
|
156
|
+
* @type {{layer:*, dataset:*}|null} */
|
|
157
|
+
this._triedSource = null
|
|
158
|
+
/** The last position of the mouse in the page. The canvas does not get the mouse events while the map is dragged or
|
|
159
|
+
* scrolled (the zoom of the canvas takes them), so this one is listened to in the window: the map shows the hover again
|
|
160
|
+
* where the mouse is when a redraw ends the pan or the zoom.
|
|
161
|
+
* @private
|
|
162
|
+
* @type {{clientX:number, clientY:number}|null} */
|
|
163
|
+
this._pointer = null
|
|
164
|
+
this._pointerHandler = (e) => {
|
|
165
|
+
this._pointer = { clientX: e.clientX, clientY: e.clientY }
|
|
166
|
+
}
|
|
167
|
+
// the mouse left the page
|
|
168
|
+
this._pointerLeaveHandler = (e) => {
|
|
169
|
+
if (!e.relatedTarget) this._pointer = null
|
|
170
|
+
}
|
|
171
|
+
for (const type of ['mousemove', 'mousedown', 'wheel'])
|
|
172
|
+
window.addEventListener(type, this._pointerHandler, { capture: true, passive: true })
|
|
173
|
+
window.addEventListener('mouseout', this._pointerLeaveHandler, { capture: true, passive: true })
|
|
174
|
+
const scheduleFocus = (e) => {
|
|
175
|
+
this._lastMouseEvent = e
|
|
176
|
+
this._focusEvent = e
|
|
177
|
+
if (this._focusFrame) return
|
|
178
|
+
this._focusFrame = requestAnimationFrame(() => {
|
|
179
|
+
this._focusFrame = null
|
|
180
|
+
const ev = this._focusEvent
|
|
181
|
+
this._focusEvent = null
|
|
182
|
+
if (ev) this.focusCell(ev)
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
this.mouseOverHandler = scheduleFocus
|
|
186
|
+
this.mouseMoveHandler = scheduleFocus
|
|
187
|
+
this.mouseOutHandler = (e) => {
|
|
188
|
+
if (this._focusFrame) cancelAnimationFrame(this._focusFrame)
|
|
189
|
+
this._focusFrame = null
|
|
190
|
+
this._focusEvent = null
|
|
191
|
+
this._lastMouseEvent = null
|
|
192
|
+
this._hovering = false
|
|
193
|
+
this._hoverKey = null
|
|
194
|
+
this._hoverSource = null
|
|
195
|
+
this._triedKey = null
|
|
196
|
+
this._triedSource = null
|
|
197
|
+
this.tooltip.hide()
|
|
198
|
+
}
|
|
199
|
+
this.geoCanvas.canvas.addEventListener('mouseover', this.mouseOverHandler)
|
|
200
|
+
this.geoCanvas.canvas.addEventListener('mousemove', this.mouseMoveHandler)
|
|
201
|
+
this.geoCanvas.canvas.addEventListener('mouseout', this.mouseOutHandler)
|
|
202
|
+
|
|
203
|
+
// listen for resize events on the App's container and handle them
|
|
204
|
+
this.defineResizeObserver()
|
|
205
|
+
|
|
206
|
+
// add extra logic to onZoomStartFun
|
|
207
|
+
this.geoCanvas.onZoomStartFun = (e) => {
|
|
208
|
+
if (opts.onZoomStartFun) opts.onZoomStartFun(e)
|
|
209
|
+
this._hovering = false
|
|
210
|
+
this._hoverKey = null
|
|
211
|
+
this._hoverSource = null
|
|
212
|
+
this._triedKey = null
|
|
213
|
+
this._triedSource = null
|
|
214
|
+
this.tooltip.hide()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//for mouse over
|
|
218
|
+
/**
|
|
219
|
+
* @private
|
|
220
|
+
* @type {HTMLCanvasElement|null} */
|
|
221
|
+
this.canvasSave = null
|
|
222
|
+
|
|
223
|
+
this.selectionRectangleColor = opts.selectionRectangleColor || '#FF6347'
|
|
224
|
+
this.selectionRectangleWidthPix = opts.selectionRectangleWidthPix || (() => 4) //(r,z) => {}
|
|
225
|
+
|
|
226
|
+
// transparent background (e.g. leaflet) 'red painting' fix
|
|
227
|
+
this.transparentBackground = opts.transparentBackground
|
|
228
|
+
|
|
229
|
+
//set default globalCompositeOperation
|
|
230
|
+
this.defaultGlobalCompositeOperation =
|
|
231
|
+
opts.defaultGlobalCompositeOperation || this.geoCanvas.ctx.globalCompositeOperation
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @protected
|
|
236
|
+
* @returns {HTMLCanvasElement}
|
|
237
|
+
*/
|
|
238
|
+
initialiseCanvas() {
|
|
239
|
+
const canvas = document.createElement('canvas')
|
|
240
|
+
canvas.classList.add('gridviz-canvas')
|
|
241
|
+
canvas.setAttribute('width', '' + this.w)
|
|
242
|
+
canvas.setAttribute('height', '' + this.h)
|
|
243
|
+
this.container.appendChild(canvas)
|
|
244
|
+
return canvas
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
initialiseLegend() {
|
|
248
|
+
this.legend = select(this.container)
|
|
249
|
+
.append('div') // Create a new container
|
|
250
|
+
.attr('id', 'gridviz-legend')
|
|
251
|
+
.classed('gridviz-legends', true)
|
|
252
|
+
.style('position', 'absolute')
|
|
253
|
+
.style('width', 'auto')
|
|
254
|
+
.style('height', 'auto')
|
|
255
|
+
.style('background', '#FFFFFF')
|
|
256
|
+
//.style("padding", this.padding)
|
|
257
|
+
.style('border', '1px solid #d5d5d5')
|
|
258
|
+
.style('border-radius', '6px')
|
|
259
|
+
.style('box-shadow', '0px 4px 6px rgba(0, 0, 0, 0.1), 0px 1px 3px rgba(0, 0, 0, 0.08)')
|
|
260
|
+
.style('font-family', "'Helvetica Neue', Arial, Helvetica, sans-serif")
|
|
261
|
+
.style('bottom', '15px')
|
|
262
|
+
.style('right', '15px')
|
|
263
|
+
//hide
|
|
264
|
+
//.style("visibility", "hidden")
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Set/get layer stack.
|
|
269
|
+
*
|
|
270
|
+
* @param {undefined|import("./Layer.js").Layer|import("./Layer.js").Layer[]} layers
|
|
271
|
+
* @returns { this | import("./Layer.js").Layer[] }
|
|
272
|
+
*/
|
|
273
|
+
layers_(layers) {
|
|
274
|
+
if (arguments.length === 0) return this.layers
|
|
275
|
+
if (arguments.length === 1)
|
|
276
|
+
if (Array.isArray(layers)) this.layers = layers
|
|
277
|
+
else this.layers = [layers]
|
|
278
|
+
else this.layers = arguments
|
|
279
|
+
return this
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Redraws the map: at once, or, with the asyncRedraw option, once at the next
|
|
284
|
+
* animation frame however many times it's called before then.
|
|
285
|
+
* @returns {this} */
|
|
286
|
+
redraw() {
|
|
287
|
+
if (this.asyncRedraw) {
|
|
288
|
+
if (!this._redrawFrame && !this._destroyed)
|
|
289
|
+
this._redrawFrame = requestAnimationFrame(() => {
|
|
290
|
+
this._redrawFrame = null
|
|
291
|
+
this.redrawNow()
|
|
292
|
+
})
|
|
293
|
+
return this
|
|
294
|
+
}
|
|
295
|
+
return this.redrawNow()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Redraws the map at once, whatever the asyncRedraw option (and cancels a scheduled redraw).
|
|
300
|
+
* @returns {this} */
|
|
301
|
+
redrawNow() {
|
|
302
|
+
if (this._redrawFrame) cancelAnimationFrame(this._redrawFrame)
|
|
303
|
+
this._redrawFrame = null
|
|
304
|
+
// a destroyed map can still be asked to redraw by a dataset it shared (see destroy)
|
|
305
|
+
if (this._destroyed) return this
|
|
306
|
+
|
|
307
|
+
//remove legend elements: the legends themselves only, not what is inside them
|
|
308
|
+
//(a legend keeps its content while one of its swatches is hovered, see Style.updateLegends)
|
|
309
|
+
//except the one being hovered, which would lose its mouseleave (see Legend.highlight)
|
|
310
|
+
if (this.legend) {
|
|
311
|
+
this.legend.selectAll(':scope > *:not([data-gridviz-hovered])').remove()
|
|
312
|
+
// the legends of this draw are placed one after the other (see GridLayer.addLegends)
|
|
313
|
+
this.legend.node()._gridvizLegendCursor = null
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
//clear
|
|
317
|
+
this.geoCanvas.initCanvasTransform()
|
|
318
|
+
this.geoCanvas.clear(this.geoCanvas.backgroundColor)
|
|
319
|
+
|
|
320
|
+
const z = this.geoCanvas.view.z
|
|
321
|
+
this.updateExtentGeo()
|
|
322
|
+
|
|
323
|
+
const ctx = this.geoCanvas.offscreenCtx
|
|
324
|
+
|
|
325
|
+
//go through the layers
|
|
326
|
+
for (const layer of this.layers) {
|
|
327
|
+
//check if layer is visible
|
|
328
|
+
if (!isVisibleAt(layer, z)) continue
|
|
329
|
+
|
|
330
|
+
//set layer alpha and blend mode
|
|
331
|
+
if (layer.alpha || layer.blendOperation) {
|
|
332
|
+
ctx.save()
|
|
333
|
+
if (layer.alpha) ctx.globalAlpha = layer.alpha(z)
|
|
334
|
+
if (layer.blendOperation) ctx.globalCompositeOperation = layer.blendOperation(z)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
//set affin transform to draw with geographical coordinates
|
|
338
|
+
this.geoCanvas.setCanvasTransform()
|
|
339
|
+
|
|
340
|
+
//draw layer
|
|
341
|
+
layer.draw(this.geoCanvas, this.legend)
|
|
342
|
+
|
|
343
|
+
//draw layer filter
|
|
344
|
+
if (layer.filterColor) layer.drawFilter(this.geoCanvas)
|
|
345
|
+
|
|
346
|
+
//restore default alpha and blend operation
|
|
347
|
+
if (layer.alpha || layer.blendOperation) ctx.restore()
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// one drawImage call: draw the offscreen canvas to the main canvas
|
|
351
|
+
this.geoCanvas.initCanvasTransform()
|
|
352
|
+
this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
353
|
+
|
|
354
|
+
this.canvasSave = null
|
|
355
|
+
// the canvas holds no focus rectangle any more
|
|
356
|
+
this._focusedCell = undefined
|
|
357
|
+
|
|
358
|
+
// The mouse is on a cell: show its rectangle and its tooltip, in this very draw. The redraw cleared the rectangle,
|
|
359
|
+
// which would stay away until the next mouse move: a flicker when the hover redraws the map (#174), or when the
|
|
360
|
+
// map is scrolled or panned with the mouse over it. The cellInfoHTML of the layers is not called again for the cell
|
|
361
|
+
// which is hovered already: it can ask for a redraw (to show the cells around the one hovered for example), which
|
|
362
|
+
// would redraw for ever.
|
|
363
|
+
if (!this._refocusing && !this.geoCanvas._isZooming) {
|
|
364
|
+
const e = this.pointerEvent() || (this._hovering ? this._lastMouseEvent : null)
|
|
365
|
+
if (e) {
|
|
366
|
+
this._refocusing = true
|
|
367
|
+
try {
|
|
368
|
+
this.refocusCell(e)
|
|
369
|
+
} finally {
|
|
370
|
+
this._refocusing = false
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// the legends are in the container now
|
|
376
|
+
if (this.onLegendUpdate && this.legend) this.onLegendUpdate(this.legend.node())
|
|
377
|
+
|
|
378
|
+
return this
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {number} marginPx
|
|
383
|
+
* @returns {import('./GeoCanvas.js').Envelope}
|
|
384
|
+
* @public
|
|
385
|
+
*/
|
|
386
|
+
updateExtentGeo(marginPx = 20) {
|
|
387
|
+
return this.geoCanvas.updateExtentGeo(marginPx)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* The geographical position of a mouse event, with the offset of the tooltip.
|
|
392
|
+
* @param {MouseEvent} e
|
|
393
|
+
* @returns {{x:number,y:number}}
|
|
394
|
+
* @private */
|
|
395
|
+
mousePositionGeo(e) {
|
|
396
|
+
return {
|
|
397
|
+
x: this.geoCanvas.pixToGeoX(e.offsetX + this.tooltip.xMouseOffset),
|
|
398
|
+
y: this.geoCanvas.pixToGeoY(e.offsetY + this.tooltip.yMouseOffset),
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* The mouse event of the position of the mouse, when it is over the map: in the canvas, and not under something else of the
|
|
404
|
+
* page (a panel, a button...). Null when the position of the mouse is not known, or not over the map.
|
|
405
|
+
* @returns {{offsetX:number, offsetY:number, clientX:number, clientY:number}|null}
|
|
406
|
+
* @private */
|
|
407
|
+
pointerEvent() {
|
|
408
|
+
const p = this._pointer
|
|
409
|
+
if (!p) return null
|
|
410
|
+
const canvas = this.geoCanvas.canvas
|
|
411
|
+
const r = canvas.getBoundingClientRect()
|
|
412
|
+
if (p.clientX < r.left || p.clientX >= r.right || p.clientY < r.top || p.clientY >= r.bottom) return null
|
|
413
|
+
// (the root of the canvas is the document, or a shadow root when the map is in one)
|
|
414
|
+
const root = canvas.getRootNode ? canvas.getRootNode() : document
|
|
415
|
+
const top = (root.elementFromPoint ? root : document).elementFromPoint(p.clientX, p.clientY)
|
|
416
|
+
if (top !== canvas) return null
|
|
417
|
+
return { offsetX: p.clientX - r.left, offsetY: p.clientY - r.top, clientX: p.clientX, clientY: p.clientY }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Shows again the hover of the mouse position after a redraw cleared it. When the mouse is still on the cell it was on,
|
|
422
|
+
* its rectangle is drawn without asking its info again: the tooltip has it already, and cellInfoHTML is not called.
|
|
423
|
+
* The info of another cell is asked for once.
|
|
424
|
+
* @param {{offsetX:number, offsetY:number, clientX:number, clientY:number}} e
|
|
425
|
+
* @private */
|
|
426
|
+
refocusCell(e) {
|
|
427
|
+
if (this.geoCanvas._isZooming) return
|
|
428
|
+
const same = this.getCellFocusInfo(this.mousePositionGeo(e), false)
|
|
429
|
+
if (!same) {
|
|
430
|
+
// no cell here: nothing to show. Clear the hover which was, if any.
|
|
431
|
+
this._triedKey = null
|
|
432
|
+
this._triedSource = null
|
|
433
|
+
if (this._hovering) this.focusCell(e)
|
|
434
|
+
return
|
|
435
|
+
}
|
|
436
|
+
const key = cellKey(same)
|
|
437
|
+
const source = { layer: same.layer, dataset: same.dataset }
|
|
438
|
+
const sameSource = (o) => o && o.layer === source.layer && o.dataset === source.dataset
|
|
439
|
+
if (key === this._hoverKey && sameSource(this._hoverSource)) this.focusCell(e, same)
|
|
440
|
+
else if (!(key === this._triedKey && sameSource(this._triedSource))) {
|
|
441
|
+
// another cell (the map moved, or the cells did, or the data): its info, once. When it has none the cell is not
|
|
442
|
+
// hovered, and not asked again at the next redraw (until the mouse moves, the hover is ended by a pan, a zoom,
|
|
443
|
+
// or the data changes).
|
|
444
|
+
this.focusCell(e)
|
|
445
|
+
this._triedKey = key
|
|
446
|
+
this._triedSource = source
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* @param {MouseEvent} e
|
|
452
|
+
* @param {{cell:import('./Dataset.js').Cell,html?:string,resolution:number}} [known] The cell under the mouse, when already found.
|
|
453
|
+
* @returns {void} */
|
|
454
|
+
focusCell(e, known) {
|
|
455
|
+
// Don’t process hover events during active zoom
|
|
456
|
+
if (this.geoCanvas._isZooming) return;
|
|
457
|
+
|
|
458
|
+
// the mouse moved (or a redraw gave its cell, `known`): the info of the cells is asked again
|
|
459
|
+
if (!known && !this._refocusing) {
|
|
460
|
+
this._triedKey = null
|
|
461
|
+
this._triedSource = null
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** @type {{cell:import('./Dataset.js').Cell,html?:string,resolution:number} | undefined} */
|
|
465
|
+
const focus = known || this.getCellFocusInfo(this.mousePositionGeo(e))
|
|
466
|
+
|
|
467
|
+
// Same cell as the one already highlighted (or still no cell): the canvas is right
|
|
468
|
+
// as it is, only the tooltip follows the mouse. Saves a full-canvas copy per move.
|
|
469
|
+
const focusedCell = focus ? focus.cell : null
|
|
470
|
+
this._hovering = !!focus
|
|
471
|
+
this._hoverKey = focus ? cellKey(focus) : null
|
|
472
|
+
this._hoverSource = focus ? { layer: focus.layer, dataset: focus.dataset } : null
|
|
473
|
+
if (focusedCell === this._focusedCell) {
|
|
474
|
+
if (focus) {
|
|
475
|
+
this.tooltip.setPosition(e)
|
|
476
|
+
this.tooltip.show()
|
|
477
|
+
} else this.tooltip.hide()
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
this._focusedCell = focusedCell
|
|
481
|
+
|
|
482
|
+
// Transparent background (Leaflet): redraw base, then draw focus rect directly on the visible canvas
|
|
483
|
+
if (this.transparentBackground) {
|
|
484
|
+
// Always restore the base (offscreen) into the visible canvas
|
|
485
|
+
this.geoCanvas.initCanvasTransform();
|
|
486
|
+
this.geoCanvas.ctx.clearRect(0, 0, this.w, this.h);
|
|
487
|
+
this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0);
|
|
488
|
+
|
|
489
|
+
if (focus) {
|
|
490
|
+
if (focus.html !== undefined) this.tooltip.html(focus.html);
|
|
491
|
+
this.tooltip.setPosition(e);
|
|
492
|
+
this.tooltip.show();
|
|
493
|
+
|
|
494
|
+
const rectWPix = this.selectionRectangleWidthPix
|
|
495
|
+
? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
|
|
496
|
+
: 4;
|
|
497
|
+
|
|
498
|
+
const ctx = this.geoCanvas.ctx; // draw directly on visible canvas
|
|
499
|
+
ctx.save();
|
|
500
|
+
ctx.strokeStyle = this.selectionRectangleColor;
|
|
501
|
+
ctx.lineWidth = rectWPix;
|
|
502
|
+
ctx.beginPath();
|
|
503
|
+
// compute in pixel space (no geo transform on ctx)
|
|
504
|
+
const xPix = this.geoCanvas.geoToPixX(focus.cell.x);
|
|
505
|
+
const yPix = this.geoCanvas.geoToPixY(focus.cell.y);
|
|
506
|
+
const wPix = focus.resolution / this.geoCanvas.view.z;
|
|
507
|
+
const hPix = -wPix; // y axis inverted in geoToPix
|
|
508
|
+
ctx.rect(
|
|
509
|
+
xPix - rectWPix / 2,
|
|
510
|
+
yPix + rectWPix / 2,
|
|
511
|
+
wPix + rectWPix,
|
|
512
|
+
hPix - rectWPix
|
|
513
|
+
);
|
|
514
|
+
ctx.stroke();
|
|
515
|
+
ctx.restore();
|
|
516
|
+
} else {
|
|
517
|
+
this.tooltip.hide();
|
|
518
|
+
}
|
|
519
|
+
return; // handled
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (focus) {
|
|
523
|
+
this.geoCanvas.canvas.style.cursor = 'pointer';
|
|
524
|
+
if (focus.html !== undefined) this.tooltip.html(focus.html)
|
|
525
|
+
this.tooltip.setPosition(e)
|
|
526
|
+
this.tooltip.show()
|
|
527
|
+
|
|
528
|
+
//show cell position as a rectangle
|
|
529
|
+
if (!this.canvasSave) {
|
|
530
|
+
this.canvasSave = document.createElement('canvas')
|
|
531
|
+
this.canvasSave.setAttribute('width', '' + this.w)
|
|
532
|
+
this.canvasSave.setAttribute('height', '' + this.h)
|
|
533
|
+
this.canvasSave.getContext('2d')?.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
534
|
+
} else {
|
|
535
|
+
this.geoCanvas.offscreenCtx.drawImage(this.canvasSave, 0, 0)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
//draw image saved + draw rectangle
|
|
539
|
+
const rectWPix = this.selectionRectangleWidthPix
|
|
540
|
+
? this.selectionRectangleWidthPix(focus.resolution, this.geoCanvas.view.z)
|
|
541
|
+
: 4
|
|
542
|
+
this.geoCanvas.initCanvasTransform()
|
|
543
|
+
const ctx = this.geoCanvas.offscreenCtx
|
|
544
|
+
ctx.strokeStyle = this.selectionRectangleColor
|
|
545
|
+
ctx.lineWidth = rectWPix
|
|
546
|
+
ctx.beginPath()
|
|
547
|
+
|
|
548
|
+
ctx.rect(
|
|
549
|
+
this.geoCanvas.geoToPixX(focus.cell.x) - rectWPix / 2,
|
|
550
|
+
this.geoCanvas.geoToPixY(focus.cell.y) + rectWPix / 2,
|
|
551
|
+
focus.resolution / this.geoCanvas.view.z + rectWPix,
|
|
552
|
+
-focus.resolution / this.geoCanvas.view.z - rectWPix
|
|
553
|
+
)
|
|
554
|
+
ctx.stroke()
|
|
555
|
+
this.geoCanvas.ctx.drawImage(this.geoCanvas.offscreenCanvas, 0, 0)
|
|
556
|
+
} else {
|
|
557
|
+
this.geoCanvas.canvas.style.cursor = 'default';
|
|
558
|
+
this.tooltip.hide()
|
|
559
|
+
if (this.canvasSave) this.geoCanvas.ctx.drawImage(this.canvasSave, 0, 0)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Return the cell HTML info at a given geo position.
|
|
565
|
+
* This is usefull for user interactions, to show this info where the user clicks for example.
|
|
566
|
+
*
|
|
567
|
+
* @param {{x:number,y:number}} posGeo
|
|
568
|
+
* @param {boolean} [withHtml] False to find the cell without calling the cellInfoHTML of its layer: no html then.
|
|
569
|
+
* @returns {{cell:import('./Dataset.js').Cell,html?:string,resolution:number} | undefined}
|
|
570
|
+
* @protected
|
|
571
|
+
*/
|
|
572
|
+
getCellFocusInfo(posGeo, withHtml = true) {
|
|
573
|
+
|
|
574
|
+
// go through the layers, starting from top one
|
|
575
|
+
const z = this.geoCanvas.view.z
|
|
576
|
+
for (let i = this.layers.length - 1; i >= 0; i--) {
|
|
577
|
+
|
|
578
|
+
/** @type {import("./Layer.js").Layer} */
|
|
579
|
+
const layer = this.layers[i]
|
|
580
|
+
|
|
581
|
+
if (!isVisibleAt(layer, z)) continue
|
|
582
|
+
if (layer.cellInfoHTML === 'none') continue
|
|
583
|
+
if (!layer.cellInfoHTML) continue
|
|
584
|
+
if (!layer.getDataset) continue
|
|
585
|
+
|
|
586
|
+
// get dataset
|
|
587
|
+
const dsc = layer.getDataset(z)
|
|
588
|
+
if (!dsc) continue
|
|
589
|
+
|
|
590
|
+
//get cell at mouse position
|
|
591
|
+
/** @type {import('./Dataset.js').Cell|undefined} */
|
|
592
|
+
const cell = dsc.getCellFromPosition(posGeo, dsc.getViewCache())
|
|
593
|
+
|
|
594
|
+
// no cell found at theis position for this layer
|
|
595
|
+
if (!cell)
|
|
596
|
+
// dig to the next layer below
|
|
597
|
+
if (this.digForTooltip) continue
|
|
598
|
+
// no tooltip to show
|
|
599
|
+
else return undefined
|
|
600
|
+
|
|
601
|
+
//rare case for a dataset with mixed resolutions
|
|
602
|
+
if (dsc.mixedResolution) {
|
|
603
|
+
const r = +dsc.mixedResolution(cell)
|
|
604
|
+
const html = withHtml ? layer.cellInfoHTML(cell, r) : undefined
|
|
605
|
+
if (withHtml && !html) return undefined
|
|
606
|
+
return { cell: cell, html: html, resolution: r, layer: layer, dataset: dsc }
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const html = withHtml ? layer.cellInfoHTML(cell, dsc.getResolution()) : undefined
|
|
610
|
+
if (withHtml && !html) return undefined
|
|
611
|
+
return { cell: cell, html: html, resolution: dsc.getResolution(), layer: layer, dataset: dsc }
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* @param {number} x
|
|
617
|
+
* @param {number} y
|
|
618
|
+
* @param {number|undefined} z
|
|
619
|
+
*/
|
|
620
|
+
setView(x, y, z = undefined) {
|
|
621
|
+
this.geoCanvas.view.x = x
|
|
622
|
+
this.geoCanvas.view.y = y
|
|
623
|
+
if (z != undefined) this.geoCanvas.view.z = z
|
|
624
|
+
return this
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** @returns {import('./GeoCanvas.js').View} */
|
|
628
|
+
getView() {
|
|
629
|
+
return this.geoCanvas.view
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** @returns {number} */
|
|
633
|
+
getZoom() {
|
|
634
|
+
return this.geoCanvas.view.z
|
|
635
|
+
}
|
|
636
|
+
/** @param {number} z @returns {this} */
|
|
637
|
+
setZoom(z) {
|
|
638
|
+
this.geoCanvas.view.z = z
|
|
639
|
+
return this
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** @returns {Array.<number|undefined>} */
|
|
643
|
+
getCenterExtent() {
|
|
644
|
+
return this.geoCanvas.getCenterExtent()
|
|
645
|
+
}
|
|
646
|
+
/** @param {Array.<number>} val @returns {this} */
|
|
647
|
+
setCenterExtent(val) {
|
|
648
|
+
this.geoCanvas.setCenterExtent(val)
|
|
649
|
+
return this
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** @returns {Array.<number>} */
|
|
653
|
+
getZoomExtent() {
|
|
654
|
+
return this.geoCanvas.getZoomExtent()
|
|
655
|
+
}
|
|
656
|
+
/** @param {Array.<number>} val @returns {this} */
|
|
657
|
+
setZoomExtent(val) {
|
|
658
|
+
this.geoCanvas.setZoomExtent(val)
|
|
659
|
+
return this
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** @returns {string} */
|
|
663
|
+
getBackgroundColor() {
|
|
664
|
+
return this.geoCanvas.backgroundColor
|
|
665
|
+
}
|
|
666
|
+
/** @param {string} val @returns {this} */
|
|
667
|
+
setBackgroundColor(val) {
|
|
668
|
+
this.geoCanvas.backgroundColor = val
|
|
669
|
+
return this
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Adds a set of zoom buttons to the map
|
|
674
|
+
*
|
|
675
|
+
* @param {object} opts
|
|
676
|
+
* @returns {this}
|
|
677
|
+
*/
|
|
678
|
+
addZoomButtons(opts) {
|
|
679
|
+
// * opts.id
|
|
680
|
+
// * opts.onZoom - custom event handler function
|
|
681
|
+
// * opts.x
|
|
682
|
+
// * opts.y
|
|
683
|
+
// * opts.delta - zoom delta applied on each click
|
|
684
|
+
|
|
685
|
+
this.zoomButtons = new ZoomButtons({
|
|
686
|
+
map: this,
|
|
687
|
+
id: opts?.id || 'gridviz-zoom-buttons-' + this.container.id,
|
|
688
|
+
class: opts?.class,
|
|
689
|
+
x: opts?.x,
|
|
690
|
+
y: opts?.y,
|
|
691
|
+
onZoom: opts?.onZoom,
|
|
692
|
+
delta: opts?.delta || 0.2,
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
return this
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Adds a fullscreen toggle button to the app
|
|
700
|
+
*
|
|
701
|
+
* @param {object} opts
|
|
702
|
+
* @returns {this}
|
|
703
|
+
*/
|
|
704
|
+
addFullscreenButton(opts) {
|
|
705
|
+
// * opts.map - the gridviz map
|
|
706
|
+
// * opts.id
|
|
707
|
+
// * opts.x
|
|
708
|
+
// * opts.y
|
|
709
|
+
|
|
710
|
+
this.fullscreenButton = new FullscreenButton({
|
|
711
|
+
map: this,
|
|
712
|
+
id: opts?.id || 'gridviz-fullscreen-button',
|
|
713
|
+
class: opts?.class,
|
|
714
|
+
x: opts?.x,
|
|
715
|
+
y: opts?.y,
|
|
716
|
+
})
|
|
717
|
+
|
|
718
|
+
return this
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** @returns {this} */
|
|
722
|
+
setViewFromURL() {
|
|
723
|
+
this.geoCanvas.setViewFromURL()
|
|
724
|
+
return this
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* @description Add a resize event observer to the Apps container and update the canvas accordingly
|
|
729
|
+
* @memberof App
|
|
730
|
+
*/
|
|
731
|
+
defineResizeObserver() {
|
|
732
|
+
// Track whether the observer is currently processing a resize event
|
|
733
|
+
let resizePending = false
|
|
734
|
+
|
|
735
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
736
|
+
if (!Array.isArray(entries) || !entries.length) return
|
|
737
|
+
|
|
738
|
+
let container = this.container
|
|
739
|
+
|
|
740
|
+
// Ensure the container has valid dimensions
|
|
741
|
+
if (container.clientWidth > 0 && container.clientHeight > 0) {
|
|
742
|
+
if (!resizePending) {
|
|
743
|
+
resizePending = true // Prevent overlapping resize triggers
|
|
744
|
+
|
|
745
|
+
window.requestAnimationFrame(() => {
|
|
746
|
+
resizePending = false // Reset the flag after processing
|
|
747
|
+
|
|
748
|
+
// Check for size changes
|
|
749
|
+
if (this.h !== container.clientHeight || this.w !== container.clientWidth) {
|
|
750
|
+
this.h = container.clientHeight
|
|
751
|
+
this.w = container.clientWidth
|
|
752
|
+
|
|
753
|
+
// Update geoCanvas sizes
|
|
754
|
+
this.geoCanvas.h = this.h
|
|
755
|
+
this.geoCanvas.w = this.w
|
|
756
|
+
this.geoCanvas.canvas.setAttribute('width', String(this.w))
|
|
757
|
+
this.geoCanvas.canvas.setAttribute('height', String(this.h))
|
|
758
|
+
this.geoCanvas.offscreenCanvas.setAttribute('width', String(this.w))
|
|
759
|
+
this.geoCanvas.offscreenCanvas.setAttribute('height', String(this.h))
|
|
760
|
+
|
|
761
|
+
this.redraw()
|
|
762
|
+
|
|
763
|
+
// Optionally reposition UI elements
|
|
764
|
+
// if (this.zoomButtons) this.zoomButtons.node.style.left = this.w - 50 + 'px';
|
|
765
|
+
// if (this.fullscreenButton) this.fullscreenButton.node.style.left = this.w - 50 + 'px';
|
|
766
|
+
}
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
})
|
|
771
|
+
|
|
772
|
+
resizeObserver.observe(this.container)
|
|
773
|
+
/** @private */
|
|
774
|
+
this._resizeObserver = resizeObserver
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* @description Destroy the map and it's event listeners
|
|
779
|
+
* This should significantly reduce the memory used when creating and destroying gridviz map instances (for example in leaflet-gridviz)
|
|
780
|
+
* @memberof App
|
|
781
|
+
*/
|
|
782
|
+
destroy() {
|
|
783
|
+
this._destroyed = true
|
|
784
|
+
|
|
785
|
+
// clear layers
|
|
786
|
+
this.layers = []
|
|
787
|
+
this.bgLayers = []
|
|
788
|
+
|
|
789
|
+
// cancel scheduled work
|
|
790
|
+
if (this._redrawFrame) cancelAnimationFrame(this._redrawFrame)
|
|
791
|
+
this._redrawFrame = null
|
|
792
|
+
if (this._focusFrame) cancelAnimationFrame(this._focusFrame)
|
|
793
|
+
this._focusFrame = null
|
|
794
|
+
|
|
795
|
+
// stop observing the container: the observer would otherwise keep this map alive
|
|
796
|
+
if (this._resizeObserver) this._resizeObserver.disconnect()
|
|
797
|
+
this._resizeObserver = null
|
|
798
|
+
|
|
799
|
+
// remove event listeners, from the window and from the canvas they were added to
|
|
800
|
+
for (const type of ['mousemove', 'mousedown', 'wheel'])
|
|
801
|
+
window.removeEventListener(type, this._pointerHandler, { capture: true })
|
|
802
|
+
window.removeEventListener('mouseout', this._pointerLeaveHandler, { capture: true })
|
|
803
|
+
this.geoCanvas.canvas.removeEventListener('mouseover', this.mouseOverHandler)
|
|
804
|
+
this.geoCanvas.canvas.removeEventListener('mousemove', this.mouseMoveHandler)
|
|
805
|
+
this.geoCanvas.canvas.removeEventListener('mouseout', this.mouseOutHandler)
|
|
806
|
+
|
|
807
|
+
// remove canvas
|
|
808
|
+
this.geoCanvas.canvas.remove()
|
|
809
|
+
|
|
810
|
+
// remove legend
|
|
811
|
+
// only remove legend if gridviz created it
|
|
812
|
+
if (!this._legendProvidedByUser) {
|
|
813
|
+
this.legend?.remove()
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// remove tooltip
|
|
817
|
+
this.tooltip.tooltip?.remove()
|
|
818
|
+
}
|
|
819
|
+
}
|