Repository: NASA-AMMOS/3DTilesRendererJS
Pull request #1183 (2025-06-22 03:43:35)

ImageOverlayPlugin: Add download queue
Related to #1155 

Adds a queue to limit the number of downloads occurring at once and prevent out of memory issues.


Patch:
diff --git a/src/plugins/three/images/ImageOverlayPlugin.js b/src/plugins/three/images/ImageOverlayPlugin.js
index bdea96b..516e2a0 100644
--- a/src/plugins/three/images/ImageOverlayPlugin.js
+++ b/src/plugins/three/images/ImageOverlayPlugin.js
@@ -4,6 +4,7 @@ import { XYZImageSource } from './sources/XYZImageSource.js';
 import { TMSImageSource } from './sources/TMSImageSource.js';
 import { forEachTileInBounds, getMeshesCartographicRange } from './overlays/utils.js';
 import { CesiumIonAuth } from '../../base/auth/CesiumIonAuth.js';
+import { PriorityQueue } from '../../../utilities/PriorityQueue.js';
 import { wrapOverlaysMaterial } from './overlays/wrapOverlaysMaterial.js';
 
 // function for marking and releasing images in the given overlay
@@ -83,6 +84,9 @@ export class ImageOverlayPlugin {
 		this.usedTextures = new Set();
 		this.meshParams = new WeakMap();
 		this.pendingTiles = new Map();
+		this.processQueue = null;
+		this._onUpdateAfter = null;
+		this._onTileDownloadStart = null;
 		this._scheduled = false;
 
 		overlays.forEach( overlay => {
@@ -97,10 +101,34 @@ export class ImageOverlayPlugin {
 	init( tiles ) {
 
 		const tileComposer = new TiledTextureComposer( this.renderer );
+		const processQueue = new PriorityQueue();
+		processQueue.priorityCallback = ( a, b ) => {
+
+			const tileA = a.tile;
+			const tileB = b.tile;
+
+			const visibleA = tiles.visibleTiles.has( tileA );
+			const visibleB = tiles.visibleTiles.has( tileB );
+			if ( visibleA !== visibleB ) {
+
+				// load visible tiles first
+				return visibleA ? 1 : - 1;
+
+			} else {
+
+				// the fallback to the download queue tile priority
+				return tiles.downloadQueue.priorityCallback( tileA, tileB );
+
+			}
+
+			// TODO: we could prioritize by overlay order here to ensure consistency
+
+		};
 
 		// save variables
 		this.tiles = tiles;
 		this.tileComposer = tileComposer;
+		this.processQueue = processQueue;
 
 		// init all existing tiles
 		tiles.forEachLoadedModel( ( scene, tile ) => {
@@ -169,7 +197,7 @@ export class ImageOverlayPlugin {
 
 	disposeTile( tile ) {
 
-		const { overlayInfo, tileControllers } = this;
+		const { overlayInfo, tileControllers, processQueue } = this;
 
 		// Cancel any ongoing tasks. If a tile is cancelled while downloading
 		// this will not have been created, yet.
@@ -185,16 +213,16 @@ export class ImageOverlayPlugin {
 
 			if ( tileInfo.has( tile ) ) {
 
-				const { meshInfo, range, meshRange, level, target } = tileInfo.get( tile );
+				const { meshInfo, range, meshRange, level, target, meshRangeMarked, rangeMarked } = tileInfo.get( tile );
 
 				// release the ranges
-				if ( meshRange !== null ) {
+				if ( meshRange !== null && meshRangeMarked ) {
 
 					markOverlayImages( meshRange, level, overlay, true );
 
 				}
 
-				if ( range !== null ) {
+				if ( range !== null && rangeMarked ) {
 
 					markOverlayImages( range, level, overlay, true );
 
@@ -214,6 +242,13 @@ export class ImageOverlayPlugin {
 
 		} ) );
 
+		// Remove any items that reference the tile being disposed
+		processQueue.removeByFilter( item => {
+
+			return item.tile === tile;
+
+		} );
+
 	}
 
 	processTileModel( scene, tile ) {
@@ -329,7 +364,7 @@ export class ImageOverlayPlugin {
 
 	deleteOverlay( overlay ) {
 
-		const { overlays, overlayInfo } = this;
+		const { overlays, overlayInfo, processQueue } = this;
 		const index = overlays.indexOf( overlay );
 		if ( index !== - 1 ) {
 
@@ -352,6 +387,14 @@ export class ImageOverlayPlugin {
 
 			overlay.dispose();
 			overlays.splice( index, 1 );
+
+			// Remove any items that reference the overlay being disposed
+			processQueue.removeByFilter( item => {
+
+				return item.overlay === overlay;
+
+			} );
+
 			this.needsUpdate = true;
 
 		}
@@ -424,7 +467,7 @@ export class ImageOverlayPlugin {
 
 		// This function is resilient to multiple calls in case an overlay is added after a tile starts loading
 		// and before it is loaded, meaning this function needs to be called twice to ensure it's initialized.
-		const { overlayInfo } = this;
+		const { overlayInfo, processQueue } = this;
 		if ( overlayInfo.get( overlay ).tileInfo.has( tile ) ) {
 
 			return;
@@ -438,6 +481,9 @@ export class ImageOverlayPlugin {
 			level: level,
 			target: null,
 			meshInfo: new Map(),
+
+			rangeMarked: false,
+			meshRangeMarked: false,
 		};
 
 		overlayInfo
@@ -452,7 +498,18 @@ export class ImageOverlayPlugin {
 			const range = [ minLon, minLat, maxLon, maxLat ];
 			info.range = range;
 
-			markOverlayImages( range, level, overlay, false );
+			processQueue
+				.add( { tile, overlay }, () => {
+
+					info.rangeMarked = true;
+					return markOverlayImages( range, level, overlay, false );
+
+				} )
+				.catch( () => {
+
+					// the queue throws an error if a task is removed early
+
+				} );
 
 		}
 
@@ -467,12 +524,22 @@ export class ImageOverlayPlugin {
 
 		}
 
-		const { tiles, overlayInfo, resolution, tileComposer, tileControllers, usedTextures } = this;
+		const { tiles, overlayInfo, resolution, tileComposer, tileControllers, usedTextures, processQueue } = this;
 		const { ellipsoid } = tiles;
 		const { controller, tileInfo } = overlayInfo.get( overlay );
 		const tileController = tileControllers.get( tile );
 
-		// find all meshes to project on
+		// wait for the overlay to be completely loaded so projection and tiling are available
+		await overlay.whenReady();
+
+		// check if the overlay or tile have been disposed since starting this function
+		if ( controller.signal.aborted || tileController.signal.aborted ) {
+
+			return;
+
+		}
+
+		// find all meshes to project on and ensure matrices are up to date
 		const meshes = [];
 		scene.updateMatrixWorld();
 		scene.traverse( c => {
@@ -485,16 +552,6 @@ export class ImageOverlayPlugin {
 
 		} );
 
-		// wait for the overlay to be completely loaded so projection and tiling are available
-		await overlay.whenReady();
-
-		// check if the overlay or tile have been disposed since starting this function
-		if ( controller.signal.aborted || tileController.signal.aborted ) {
-
-			return;
-
-		}
-
 		const rootMatrix = scene.parent !== null ? tiles.group.matrixWorldInverse : null;
 		const { tiling, projection, imageSource } = overlay;
 		const { range, uvs } = getMeshesCartographicRange( meshes, ellipsoid, rootMatrix, projection );
@@ -525,7 +582,18 @@ export class ImageOverlayPlugin {
 
 		} );
 
-		await markOverlayImages( range, info.level, overlay, false );
+		await processQueue
+			.add( { tile, overlay }, () => {
+
+				info.meshRangeMarked = true;
+				return markOverlayImages( range, info.level, overlay, false );
+
+			} )
+			.catch( () => {
+
+				// the queue throws an error if a task is removed early
+
+			} );
 
 		// check if the overlay has been disposed since starting this function
 		if ( controller.signal.aborted || tileController.signal.aborted ) {
diff --git a/src/utilities/PriorityQueue.d.ts b/src/utilities/PriorityQueue.d.ts
index 2bf24df..f796332 100644
--- a/src/utilities/PriorityQueue.d.ts
+++ b/src/utilities/PriorityQueue.d.ts
@@ -9,6 +9,7 @@ export class PriorityQueue {
 	sort() : void;
 	add( item : any, callback : ( item : any ) => any ) : Promise< any >;
 	remove( item : any ) : void;
+	removeByFilter( filter : ( item : any ) => boolean ) : void;
 
 	tryRunJobs() : void;
 	scheduleJobRun() : void;
diff --git a/src/utilities/PriorityQueue.js b/src/utilities/PriorityQueue.js
index 0f57cde..c0a15c2 100644
--- a/src/utilities/PriorityQueue.js
+++ b/src/utilities/PriorityQueue.js
@@ -108,6 +108,22 @@ class PriorityQueue {
 
 	}
 
+	removeByFilter( filter ) {
+
+		const { items } = this;
+		for ( let i = 0; i < items.length; i ++ ) {
+
+			const item = items[ i ];
+			if ( filter( item ) ) {
+
+				this.remove( item );
+
+			}
+
+		}
+
+	}
+
 	tryRunJobs() {
 
 		this.sort();


Test patch:
diff --git a/test/PriorityQueue.test.js b/test/PriorityQueue.test.js
index 27ee677..8f647fa 100644
--- a/test/PriorityQueue.test.js
+++ b/test/PriorityQueue.test.js
@@ -206,7 +206,7 @@ describe( 'PriorityQueue', () => {
 		let thrown = false;
 		const key = {};
 		const promise = queue
-			.add( key, async () => {} )
+			.add( key, () => {} )
 			.catch( () => thrown = true );
 
 		queue.remove( key );
@@ -217,4 +217,27 @@ describe( 'PriorityQueue', () => {
 
 	} );
 
+	it( 'should be able to filter items and dispose this removed.', async () => {
+
+		const queue = new PriorityQueue();
+		queue.priorityCallback = () => 0;
+		queue.autoUpdate = false;
+		queue.maxJobs = 1;
+
+		queue.add( 1, () => {} );
+		queue.add( 2, () => {} );
+
+		let thrown;
+		const promise = queue.add( 3, () => {} ).catch( () => thrown = true );
+
+		expect( queue.items ).toHaveLength( 3 );
+		queue.removeByFilter( i => i === 3 );
+		expect( queue.items ).toHaveLength( 2 );
+
+		await promise;
+
+		expect( thrown ).toEqual( true );
+
+	} );
+
 } );