-
Notifications
You must be signed in to change notification settings - Fork 390
Expand file tree
/
Copy pathTilesRendererBase.js
More file actions
1842 lines (1266 loc) · 46.5 KB
/
TilesRendererBase.js
File metadata and controls
1842 lines (1266 loc) · 46.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
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
41
42
43
44
45
46
47
48
49
50
51
52
53
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
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
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
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
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import { getUrlExtension } from '../utilities/urlExtension.js';
import { LRUCache } from '../utilities/LRUCache.js';
import { PriorityQueue } from '../utilities/PriorityQueue.js';
import { runTraversal as optimizedRunTraversal } from './optimizedTraverseFunctions.js';
import { runTraversal } from './traverseFunctions.js';
import { UNLOADED, QUEUED, LOADING, PARSING, LOADED, FAILED } from '../constants.js';
import { throttle } from '../utilities/throttle.js';
import { traverseSet } from '../utilities/TraversalUtils.js';
/**
* @callback TileBeforeCallback
* @param {Tile} tile
* @param {Tile|null} parent
* @param {number} depth
* @returns {boolean}
*/
/**
* @callback TileAfterCallback
* @param {Tile} tile
* @param {Tile|null} parent
* @param {number} depth
*/
/**
* @callback EventCallback
* @param {Object} event
*/
const PLUGIN_REGISTERED = Symbol( 'PLUGIN_REGISTERED' );
const regionErrorTarget = {
inView: true,
error: 0,
distance: Infinity,
};
// priority queue sort function that takes two tiles to compare. Returning 1 means
// "tile a" is loaded first.
const defaultPriorityCallback = ( a, b ) => {
const aPriority = a.priority || 0;
const bPriority = b.priority || 0;
if ( aPriority !== bPriority ) {
// lower priority value sorts first
return aPriority > bPriority ? 1 : - 1;
} else if ( ! a.traversal || ! b.traversal ) {
return 0;
} else if ( a.traversal.used !== b.traversal.used ) {
// load tiles that have been used
return a.traversal.used ? 1 : - 1;
} else if ( a.traversal.error !== b.traversal.error ) {
// load the tile with the higher error
return a.traversal.error > b.traversal.error ? 1 : - 1;
} else if ( a.traversal.distanceFromCamera !== b.traversal.distanceFromCamera ) {
// and finally visible tiles which have equal error (ex: if geometricError === 0)
// should prioritize based on distance.
return a.traversal.distanceFromCamera > b.traversal.distanceFromCamera ? - 1 : 1;
} else if ( a.internal.depthFromRenderedParent !== b.internal.depthFromRenderedParent ) {
return a.internal.depthFromRenderedParent > b.internal.depthFromRenderedParent ? - 1 : 1;
}
return 0;
};
// Optimized priority callback - prioritizes distance over error for better user experience
const optimizedPriorityCallback = ( a, b ) => {
const aPriority = a.priority || 0;
const bPriority = b.priority || 0;
if ( aPriority !== bPriority ) {
// lower priority value sorts first
return aPriority > bPriority ? 1 : - 1;
} else if ( ! a.traversal || ! b.traversal ) {
return 0;
} else if ( a.traversal.used !== b.traversal.used ) {
// load tiles that have been used
return a.traversal.used ? 1 : - 1;
} else if ( a.traversal.inFrustum !== b.traversal.inFrustum ) {
// load tiles that have are in the frustum
return a.traversal.inFrustum ? 1 : - 1;
} else if ( a.internal.hasUnrenderableContent !== b.internal.hasUnrenderableContent ) {
// load internal tile sets first
return a.internal.hasUnrenderableContent ? 1 : - 1;
} else if ( a.traversal.distanceFromCamera !== b.traversal.distanceFromCamera ) {
// load closer tiles first
return a.traversal.distanceFromCamera > b.traversal.distanceFromCamera ? - 1 : 1;
}
return 0;
};
// lru cache unload callback that takes two tiles to compare. Returning 1 means "tile a"
// is unloaded first.
const lruPriorityCallback = ( a, b ) => {
const aPriority = a.priority || 0;
const bPriority = b.priority || 0;
if ( aPriority !== bPriority ) {
// lower priority value sorts first
return aPriority > bPriority ? 1 : - 1;
} else if ( ! a.traversal || ! b.traversal ) {
return 0;
} else if ( a.traversal.lastFrameVisited !== b.traversal.lastFrameVisited ) {
// dispose of least recent tiles first
return a.traversal.lastFrameVisited > b.traversal.lastFrameVisited ? - 1 : 1;
} else if ( a.internal.depthFromRenderedParent !== b.internal.depthFromRenderedParent ) {
// dispose of deeper tiles first so parents are not disposed before children
return a.internal.depthFromRenderedParent > b.internal.depthFromRenderedParent ? 1 : - 1;
} else if ( a.internal.loadingState !== b.internal.loadingState ) {
// dispose of tiles that are earlier along in the loading process first
return a.internal.loadingState > b.internal.loadingState ? - 1 : 1;
} else if ( a.internal.hasUnrenderableContent !== b.internal.hasUnrenderableContent ) {
// dispose of external tilesets last
return a.internal.hasUnrenderableContent ? - 1 : 1;
} else if ( a.traversal.error !== b.traversal.error ) {
// unload the tile with lower error
return a.traversal.error > b.traversal.error ? - 1 : 1;
}
return 0;
};
// Internal Tile Type Definitions
/**
* Internal renderer state added to each tile during preprocessing.
* @typedef {Object} TileInternalData
* @property {boolean} hasContent - Whether the tile has a content URI.
* @property {boolean} hasRenderableContent - Whether the tile content is a renderable model (not an external tileset).
* @property {boolean} hasUnrenderableContent - Whether the tile content is an external tileset JSON.
* @property {number} loadingState - Current loading state constant (UNLOADED, QUEUED, LOADING, PARSING, LOADED, or FAILED).
* @property {string} basePath - Base URL used to resolve relative content URIs.
* @property {number} depth - Depth of this tile in the full tile hierarchy.
* @property {number} depthFromRenderedParent - Depth from the nearest ancestor with renderable content.
* @property {boolean} isVirtual - Whether this tile was synthetically generated by a plugin.
* @property {number} virtualChildCount - Number of virtual children appended to this tile by plugins.
*/
/**
* Per-frame traversal state updated on each tile during `TilesRendererBase.update`.
* @typedef {Object} TileTraversalData
* @property {number} distanceFromCamera - Distance from the tile bounds to the nearest active camera.
* @property {number} error - Screen space error computed for this tile.
* @property {boolean} inFrustum - Whether the tile was within the camera frustum on the last update.
* @property {boolean} isLeaf - Whether this tile is a leaf node in the used tile tree.
* @property {boolean} used - Whether this tile was visited during the last update traversal.
* @property {boolean} usedLastFrame - Whether this tile was visited in the previous frame.
* @property {boolean} visible - Whether this tile is currently visible (loaded, in frustum, meets SSE).
*/
/**
* A 3D Tiles tile with both spec fields (from tileset JSON) and renderer-managed state.
* @typedef {Object} Tile
* @property {Object} boundingVolume - Bounding volume. Has either a `box` (12-element array) or `sphere` (4-element array) field.
* @property {number} geometricError - Error in meters introduced if this tile is not rendered.
* @property {Tile|null} parent - Parent tile, or null for the root.
* @property {Tile[]} [children] - Child tiles.
* @property {Object} [content] - Loadable content URI reference.
* @property {'REPLACE'|'ADD'} [refine] - Refinement strategy; inherited from the parent if omitted.
* @property {number[]} [transform] - Optional 4x4 column-major transform matrix.
* @property {Object} [extensions] - Extension-specific objects.
* @property {Object} [extras] - Extra application-specific data.
* @property {TileInternalData} internal - Internal renderer state.
* @property {TileTraversalData} traversal - Per-frame traversal state.
*/
/**
* A loaded 3D Tiles tileset JSON object.
* @typedef {Object} Tileset
* @property {Object} asset - Metadata about the tileset. Contains `version` (string) and optional `tilesetVersion` (string).
* @property {number} geometricError - Error in meters for the entire tileset.
* @property {Tile} root - The root tile.
* @property {string[]} [extensionsUsed] - Names of extensions used somewhere in the tileset.
* @property {string[]} [extensionsRequired] - Names of extensions required to load the tileset.
* @property {Object} [properties] - Metadata about per-feature properties.
* @property {Object} [extensions] - Extension-specific objects.
* @property {Object} [extras] - Extra application-specific data.
*/
//
// TilesRendererBase Event Definitions
/**
* Fired when the renderer determines a new render is required — e.g. after a tile loads.
* @event TilesRendererBase#needs-update
*/
/**
* Fired when any tile content (model or external tileset) finishes loading.
* @event TilesRendererBase#load-content
*/
/**
* Fired when any tileset JSON finishes loading.
* @event TilesRendererBase#load-tileset
* @property {Tileset} tileset - The loaded tileset object.
* @property {string} url - The URL from which the tileset was loaded.
*/
/**
* Fired when the root tileset JSON finishes loading.
* @event TilesRendererBase#load-root-tileset
* @property {Tileset} tileset - The loaded root tileset object.
* @property {string} url - The URL from which the tileset was loaded.
*/
/**
* Fired when tile downloads begin after a period of inactivity.
* @event TilesRendererBase#tiles-load-start
*/
/**
* Fired when all pending tile downloads and parses have completed.
* @event TilesRendererBase#tiles-load-end
*/
/**
* Fired when a tile content download begins.
* @event TilesRendererBase#tile-download-start
* @property {Tile} tile - The tile being downloaded.
* @property {string} uri - The URI being fetched.
*/
/**
* Fired when a tile's renderable content (model/scene) is created.
* The `scene` type is engine-specific (e.g. `THREE.Group` in three.js).
* @event TilesRendererBase#load-model
* @property {Object} scene - The engine-specific scene object created for this tile.
* @property {Tile} tile - The tile the scene belongs to.
* @property {string} url - The URL the content was loaded from.
*/
/**
* Fired when a tile's renderable content is about to be removed and destroyed.
* The `scene` type is engine-specific (e.g. `THREE.Group` in three.js).
* @event TilesRendererBase#dispose-model
* @property {Object} scene - The engine-specific scene object being disposed.
* @property {Tile} tile - The tile the scene belonged to.
*/
/**
* Fired when a tile transitions between visible and hidden.
* The `scene` type is engine-specific (e.g. `THREE.Group` in three.js).
* @event TilesRendererBase#tile-visibility-change
* @property {Object} scene - The engine-specific scene object.
* @property {Tile} tile - The tile whose visibility changed.
* @property {boolean} visible - Whether the tile is now visible.
*/
/**
* Fired at the start of each `update()` call, before traversal begins.
* @event TilesRendererBase#update-before
*/
/**
* Fired at the end of each `update()` call, after traversal completes.
* @event TilesRendererBase#update-after
*/
/**
* Fired when a tile or tileset fails to load.
* @event TilesRendererBase#load-error
* @property {Tile|null} tile - The tile that failed, or null if a root tileset failed.
* @property {Error} error - The error that occurred.
* @property {string|URL} url - The URL that failed to load.
*/
/**
* Base class for 3D Tiles renderers. Manages tile loading, caching, traversal,
* and a plugin system for extending rendering behavior. Engine-specific renderers
* extend this class to add camera projection, scene management, and tile display.
*/
export class TilesRendererBase {
/**
* Root tile of the loaded root tileset, or null if not yet loaded.
* @type {Tile|null}
* @readonly
*/
get root() {
const tileset = this.rootTileset;
return tileset ? tileset.root : null;
}
get rootTileSet() {
console.warn( 'TilesRenderer: "rootTileSet" has been deprecated. Use "rootTileset" instead.' );
return this.rootTileset;
}
/**
* Fraction of tiles loaded since the last idle state, from 0 (nothing loaded) to 1 (all loaded).
* @type {number}
* @readonly
*/
get loadProgress() {
const { stats, isLoading } = this;
const loading = stats.queued + stats.downloading + stats.parsing;
const total = stats.inCacheSinceLoad + ( isLoading ? 1 : 0 );
return total === 0 ? 1.0 : 1.0 - loading / total;
}
get errorThreshold() {
return this._errorThreshold;
}
set errorThreshold( v ) {
console.warn( 'TilesRenderer: The "errorThreshold" option has been deprecated.' );
this._errorThreshold = v;
}
/**
* @param {string} [url] - URL of the root tileset JSON to load.
*/
constructor( url = null ) {
// state
this.rootLoadingState = UNLOADED;
/**
* The loaded root tileset object, or null if not yet loaded.
* @type {Tileset|null}
* @readonly
*/
this.rootTileset = null;
this.rootURL = url;
/**
* Options passed to `fetch` when loading tile and tileset resources.
* @type {RequestInit}
*/
this.fetchOptions = {};
this.plugins = [];
this.queuedTiles = [];
this.cachedSinceLoadComplete = new Set();
this.isLoading = false;
const lruCache = new LRUCache();
lruCache.unloadPriorityCallback = lruPriorityCallback;
const downloadQueue = new PriorityQueue();
downloadQueue.maxJobs = 25;
downloadQueue.priorityCallback = defaultPriorityCallback;
const parseQueue = new PriorityQueue();
parseQueue.maxJobs = 5;
parseQueue.priorityCallback = defaultPriorityCallback;
const processNodeQueue = new PriorityQueue();
processNodeQueue.maxJobs = 25;
processNodeQueue.priorityCallback = ( a, b ) => {
const aParent = a.parent;
const bParent = b.parent;
if ( aParent === bParent ) {
return 0;
} else if ( ! aParent ) {
return 1;
} else if ( ! bParent ) {
return - 1;
} else {
// fall back to the priority used for tile loads and parsing
return downloadQueue.priorityCallback( aParent, bParent );
}
};
this.processedTiles = new WeakSet();
/**
* Set of all tiles that are currently visible.
* @type {Set<Tile>}
* @readonly
*/
this.visibleTiles = new Set();
/**
* Set of all tiles that are currently active (displayed as a stand-in while children load).
* @type {Set<Tile>}
* @readonly
*/
this.activeTiles = new Set();
this.usedSet = new Set();
this.loadingTiles = new Set();
/**
* LRU cache managing loaded tile lifecycle and memory eviction.
* @note Cannot be replaced once `update()` has been called for the first time.
* @type {LRUCache}
*/
this.lruCache = lruCache;
/**
* Priority queue controlling concurrent tile downloads. Max jobs defaults to `25`.
* @note Cannot be replaced once `update()` has been called for the first time.
* @type {PriorityQueue}
*/
this.downloadQueue = downloadQueue;
/**
* Priority queue controlling concurrent tile parsing. Max jobs defaults to `5`.
* @note Cannot be modified once `update()` has been called for the first time.
* @type {PriorityQueue}
*/
this.parseQueue = parseQueue;
/**
* Priority queue for expanding and initializing tiles for traversal. Max jobs defaults to `25`.
* @note Cannot be replaced once `update()` has been called for the first time.
* @type {PriorityQueue}
*/
this.processNodeQueue = processNodeQueue;
/**
* Loading and rendering statistics updated each frame. Fields:
* - `inCache` — tiles currently in the LRU cache
* - `queued` — tiles queued for download
* - `downloading` — tiles currently downloading
* - `parsing` — tiles currently being parsed
* - `loaded` — tiles that have finished loading
* - `failed` — tiles that failed to load
* - `inFrustum` — tiles inside the camera frustum after the last update
* - `used` — tiles visited during the last traversal
* - `active` — tiles currently set as active
* - `visible` — tiles currently visible
* @type {Object}
*/
this.stats = {
inCacheSinceLoad: 0,
inCache: 0,
queued: 0,
downloading: 0,
parsing: 0,
loaded: 0,
failed: 0,
inFrustum: 0,
used: 0,
active: 0,
visible: 0,
tilesProcessed: 0,
};
this.frameCount = 0;
// callbacks
this._dispatchNeedsUpdateEvent = throttle( () => {
this.dispatchEvent( { type: 'needs-update' } );
} );
// options
/**
* Target screen-space error in pixels to aim for when updating the geometry. Tiles will
* not render if they are below this level of screen-space error. See the
* {@link https://github.com/CesiumGS/3d-tiles/tree/master/specification#geometric-error geometric error section}
* of the 3D Tiles specification for more information.
* @type {number}
*/
this.errorTarget = 16.0;
this._errorThreshold = Infinity;
/**
* "Active tiles" are those that are loaded and available but not necessarily visible.
* These tiles are useful for raycasting off-camera or for casting shadows. Active tiles
* not currently in a camera frustum are removed from the scene as an optimization.
* Setting this to `true` keeps them in the scene so they can be rendered from an outside
* camera view not accounted for by the tiles renderer.
* @type {boolean}
*/
this.displayActiveTiles = false;
/**
* Maximum depth in the tile hierarchy to traverse. Tiles deeper than this are skipped.
* @type {number}
*/
this.maxDepth = Infinity;
/**
* **Experimental.** Enables an optimized tile loading strategy that loads only the tiles
* needed for the current view, reducing memory usage and improving initial load times.
* Tiles are loaded independently based on screen-space error without requiring all parent
* tiles to load first. Prevents visual gaps and flashing during camera movement.
*
* Based in part on {@link https://cesium.com/learn/cesium-native/ref-doc/selection-algorithm-details.html Cesium Native tile selection}.
*
* Default is `false`, which uses the previous approach of loading all parent and sibling
* tiles for guaranteed smooth transitions.
* @warn Setting is currently incompatible with plugins that split tiles and on-the-fly generate and
* dispose of child tiles including the `ImageOverlayPlugin` `enableTileSplitting` setting,
* `QuantizedMeshPlugin`, & `ImageFormatPlugin` subclasses (XYZ, TMS, etc). Any tile sets
* that share caches or queues must also use the same setting.
* @type {boolean}
*/
this.optimizedLoadStrategy = false;
/**
* **Experimental.** When `true`, sibling tiles are loaded together to prevent gaps during
* camera movement. When `false`, only visible tiles are loaded, minimizing memory but
* potentially causing brief gaps during rapid movement.
*
* Only applies when `optimizedLoadStrategy` is enabled.
* @type {boolean}
*/
this.loadSiblings = true;
/**
* The number of tiles to process immediately when traversing the tile set to determine
* what to render. Lower numbers prevent frame hiccups caused by processing too many tiles
* at once when a new tile set is available, while higher values process more tiles
* immediately so data can be downloaded and displayed sooner.
* @type {number}
*/
this.maxTilesProcessed = 250;
}
// Plugins
/**
* Registers a plugin with this renderer. Plugins are inserted in priority order and
* receive lifecycle callbacks throughout the tile loading and rendering process.
* A plugin instance may only be registered to one renderer at a time.
* @param {Object} plugin
*/
registerPlugin( plugin ) {
if ( plugin[ PLUGIN_REGISTERED ] === true ) {
throw new Error( 'TilesRendererBase: A plugin can only be registered to a single tileset' );
}
// warn if plugin implements deprecated loadRootTileSet method
if ( plugin.loadRootTileSet && ! plugin.loadRootTileset ) {
console.warn( 'TilesRendererBase: Plugin implements deprecated "loadRootTileSet" method. Please rename to "loadRootTileset".' );
plugin.loadRootTileset = plugin.loadRootTileSet;
}
if ( plugin.preprocessTileSet && ! plugin.preprocessTileset ) {
console.warn( 'TilesRendererBase: Plugin implements deprecated "preprocessTileSet" method. Please rename to "preprocessTileset".' );
plugin.preprocessTileset = plugin.preprocessTileSet;
}
// insert the plugin based on the priority registered on the plugin
const plugins = this.plugins;
const priority = plugin.priority || 0;
let insertionPoint = plugins.length;
for ( let i = 0; i < plugins.length; i ++ ) {
const otherPriority = plugins[ i ].priority || 0;
if ( otherPriority > priority ) {
insertionPoint = i;
break;
}
}
plugins.splice( insertionPoint, 0, plugin );
plugin[ PLUGIN_REGISTERED ] = true;
if ( plugin.init ) {
plugin.init( this );
}
}
/**
* Removes a registered plugin. Calls `plugin.dispose()` if defined.
* Accepts either the plugin instance or its string name.
* Returns true if the plugin was found and removed.
* @param {Object|string} plugin
* @returns {boolean}
*/
unregisterPlugin( plugin ) {
const plugins = this.plugins;
if ( typeof plugin === 'string' ) {
plugin = this.getPluginByName( plugin );
}
if ( plugins.includes( plugin ) ) {
const index = plugins.indexOf( plugin );
plugins.splice( index, 1 );
if ( plugin.dispose ) {
plugin.dispose();
}
return true;
}
return false;
}
/**
* Returns the first registered plugin whose `name` property matches, or null.
* @param {string} name
* @returns {Object|null}
*/
getPluginByName( name ) {
return this.plugins.find( p => p.name === name ) || null;
}
invokeOnePlugin( func ) {
const plugins = [ ...this.plugins, this ];
for ( let i = 0; i < plugins.length; i ++ ) {
const result = func( plugins[ i ] );
if ( result ) {
return result;
}
}
return null;
}
invokeAllPlugins( func ) {
const plugins = [ ...this.plugins, this ];
const pending = [];
for ( let i = 0; i < plugins.length; i ++ ) {
const result = func( plugins[ i ] );
if ( result ) {
pending.push( result );
}
}
return pending.length === 0 ? null : Promise.all( pending );
}
// Public API
/**
* Iterates over all tiles in the loaded hierarchy. `beforecb` is called before
* descending into a tile's children; returning true from it skips the subtree.
* `aftercb` is called after all children have been visited.
* @param {TileBeforeCallback|null} [beforecb]
* @param {TileAfterCallback|null} [aftercb]
*/
traverse( beforecb, aftercb, ensureFullyProcessed = true ) {
if ( ! this.root ) return;
traverseSet( this.root, ( tile, ...args ) => {
if ( ensureFullyProcessed ) {
this.ensureChildrenArePreprocessed( tile, true );
}
return beforecb ? beforecb( tile, ...args ) : false;
}, aftercb );
}
/**
* Collects attribution data from all registered plugins into `target` and returns it.
* @param {Array<{type: string, value: any}>} [target]
* @returns {Array<{type: string, value: any}>}
*/
getAttributions( target = [] ) {
this.invokeAllPlugins( plugin => plugin !== this && plugin.getAttributions && plugin.getAttributions( target ) );
return target;
}
/**
* Runs the tile traversal and update loop. Should be called once per frame after
* camera matrices have been updated. Triggers tile loading, visibility updates,
* and LRU cache eviction.
*/
update() {
// load root
const { lruCache, usedSet, stats, root, downloadQueue, parseQueue, processNodeQueue, optimizedLoadStrategy } = this;
if ( this.rootLoadingState === UNLOADED ) {
this.rootLoadingState = LOADING;
this.invokeOnePlugin( plugin => plugin.loadRootTileset && plugin.loadRootTileset() )
.then( root => {
let processedUrl = this.rootURL;
if ( processedUrl !== null ) {
this.invokeAllPlugins( plugin => processedUrl = plugin.preprocessURL ? plugin.preprocessURL( processedUrl, null ) : processedUrl );
}
this.rootLoadingState = LOADED;
this.rootTileset = root;
this.dispatchEvent( { type: 'needs-update' } );
this.dispatchEvent( { type: 'load-content' } );
this.dispatchEvent( {
type: 'load-tileset',
tileset: root,
url: processedUrl,
} );
this.dispatchEvent( {
type: 'load-root-tileset',
tileset: root,
url: processedUrl,
} );
} )
.catch( error => {
this.rootLoadingState = FAILED;
console.error( error );
this.rootTileset = null;
this.dispatchEvent( {
type: 'load-error',
tile: null,
error,
url: this.rootURL,
} );
} );
}
if ( ! root ) {
return;
}
// check if the plugins that can block the tile updates require it
let needsUpdate = null;
this.invokeAllPlugins( plugin => {
if ( plugin.doTilesNeedUpdate ) {
const res = plugin.doTilesNeedUpdate();
if ( needsUpdate === null ) {
needsUpdate = res;
} else {
needsUpdate = Boolean( needsUpdate || res );
}
}
} );
if ( needsUpdate === false ) {
this.dispatchEvent( { type: 'update-before' } );
this.dispatchEvent( { type: 'update-after' } );
return;
}
// follow through with the update
this.dispatchEvent( { type: 'update-before' } );
//
stats.inFrustum = 0;
stats.used = 0;
stats.active = 0;
stats.visible = 0;
stats.tilesProcessed = 0;
this.frameCount ++;
usedSet.forEach( tile => lruCache.markUnused( tile ) );
usedSet.clear();
// assign the correct callbacks
const priorityCallback = optimizedLoadStrategy ? optimizedPriorityCallback : defaultPriorityCallback;
downloadQueue.priorityCallback = priorityCallback;
parseQueue.priorityCallback = priorityCallback;
// prepare for traversal
this.prepareForTraversal();
// run traversal
if ( optimizedLoadStrategy ) {
optimizedRunTraversal( root, this );
} else {
runTraversal( root, this );
}
// remove any tiles that are loading but no longer used
this.removeUnusedPendingTiles();
// TODO: This will only sort for one tileset. We may want to store this queue on the
// LRUCache so multiple tilesets can use it at once
// start the downloads of the tiles as needed
const queuedTiles = this.queuedTiles;
queuedTiles.sort( lruCache.unloadPriorityCallback );
for ( let i = 0, l = queuedTiles.length; i < l && ! lruCache.isFull(); i ++ ) {
this.requestTileContents( queuedTiles[ i ] );
}
queuedTiles.length = 0;
// start the downloads
lruCache.scheduleUnload();
// if all tasks have finished and we've been marked as actively loading then fire the completion event
const runningTasks = downloadQueue.running || parseQueue.running || processNodeQueue.running;
if ( runningTasks === false && this.isLoading === true ) {
this.cachedSinceLoadComplete.clear();
stats.inCacheSinceLoad = 0;
this.dispatchEvent( { type: 'tiles-load-end' } );
this.isLoading = false;
}
this.dispatchEvent( { type: 'update-after' } );
}
/**
* Resets any tiles that previously failed to load so they will be retried on the next `update`.
*/
resetFailedTiles() {
// reset the root tile if it's finished but never loaded
if ( this.rootLoadingState === FAILED ) {
this.rootLoadingState = UNLOADED;
}
const stats = this.stats;
if ( stats.failed === 0 ) {
return;
}
this.traverse( tile => {
if ( tile.internal.loadingState === FAILED ) {
tile.internal.loadingState = UNLOADED;
}
}, null, false );
stats.failed = 0;
}
calculateTileViewErrorWithPlugin( tile, target ) {
// calculate camera view error
this.calculateTileViewError( tile, target );
// TODO: this logic is extremely complex. It may be more simple to have the plugin
// return a "should mask" field that indicates its "false" values should be respected
// rather than the function returning a "no-op" boolean.
// check the plugin visibility - each plugin will mask between themselves
let inRegion = null;
let inRegionError = 0;
let inRegionDistance = Infinity;
this.invokeAllPlugins( plugin => {
if ( plugin !== this && plugin.calculateTileViewError ) {
// if function returns false it means "no operation"
regionErrorTarget.inView = true;
regionErrorTarget.error = 0;
regionErrorTarget.distance = Infinity;
if ( plugin.calculateTileViewError( tile, regionErrorTarget ) ) {
if ( inRegion === null ) {
inRegion = true;
}
// Plugins can set "inView" to false in order to mask the visible tiles
inRegion = inRegion && regionErrorTarget.inView;
if ( regionErrorTarget.inView ) {
inRegionDistance = Math.min( inRegionDistance, regionErrorTarget.distance );
inRegionError = Math.max( inRegionError, regionErrorTarget.error );
}
}
}
} );
if ( target.inView && inRegion !== false ) {
// if the tile is in camera view and we haven't encountered a region (null) or
// the region is in view (true). regionInView === false means the tile is masked out.
target.error = Math.max( target.error, inRegionError );