diff --git a/src/core/renderer/utilities/PriorityQueue.d.ts b/src/core/renderer/utilities/PriorityQueue.d.ts index 67c90f8cc..20f8951e4 100644 --- a/src/core/renderer/utilities/PriorityQueue.d.ts +++ b/src/core/renderer/utilities/PriorityQueue.d.ts @@ -9,6 +9,7 @@ export class PriorityQueue { get running(): boolean; sort() : void; + flush( item : any ) : any; has( item : any ) : boolean; add( item : any, callback : ( item : any ) => any ) : Promise< any >; remove( item : any ) : void; diff --git a/src/core/renderer/utilities/PriorityQueue.js b/src/core/renderer/utilities/PriorityQueue.js index c802364e7..fd1328534 100644 --- a/src/core/renderer/utilities/PriorityQueue.js +++ b/src/core/renderer/utilities/PriorityQueue.js @@ -305,6 +305,54 @@ export class PriorityQueue { } + /** + * Immediately runs the callback for the given item, removing it from the queue. + * Does nothing if the item is not queued. + * @param {any} item + * @returns {Promise|any} + */ + flush( item ) { + + const { items, callbacks } = this; + const index = items.indexOf( item ); + if ( ! callbacks.has( item ) ) { + + return; + + } + + const { callback, resolve, reject } = callbacks.get( item ); + callbacks.delete( item ); + items.splice( index, 1 ); + + let result; + try { + + result = callback( item ); + + } catch ( err ) { + + reject( err ); + return; + + } + + if ( result instanceof Promise ) { + + result + .then( resolve ) + .catch( reject ); + + } else { + + resolve( result ); + + } + + return result; + + } + /** * Schedules a deferred call to `tryRunJobs` via `schedulingCallback`. */ diff --git a/src/three/plugins/images/ImageOverlayPlugin.js b/src/three/plugins/images/ImageOverlayPlugin.js index caae9a92a..f8e5ce95a 100644 --- a/src/three/plugins/images/ImageOverlayPlugin.js +++ b/src/three/plugins/images/ImageOverlayPlugin.js @@ -87,6 +87,7 @@ export class ImageOverlayPlugin { this.processQueue = null; this._onUpdateAfter = null; this._onTileDownloadStart = null; + this._onTileVisibilityChange = null; this._virtualChildResetId = 0; this._bytesUsed = new WeakMap(); @@ -222,8 +223,20 @@ export class ImageOverlayPlugin { }; + this._onTileVisibilityChange = ( { tile, visible } ) => { + + this.overlayInfo.forEach( ( { tileInfo }, overlay ) => { + + const info = tileInfo.get( tile ); + overlay.setRegionVisible( info.range, visible ); + + } ); + + }; + tiles.addEventListener( 'update-after', this._onUpdateAfter ); tiles.addEventListener( 'tile-download-start', this._onTileDownloadStart ); + tiles.addEventListener( 'tile-visibility-change', this._onTileVisibilityChange ); this.overlays.forEach( overlay => { @@ -403,6 +416,8 @@ export class ImageOverlayPlugin { } ); tiles.removeEventListener( 'update-after', this._onUpdateAfter ); + tiles.removeEventListener( 'tile-download-start', this._onTileDownloadStart ); + tiles.removeEventListener( 'tile-visibility-change', this._onTileVisibilityChange ); this.resetVirtualChildren( true ); @@ -1110,6 +1125,12 @@ export class ImageOverlayPlugin { } + if ( tile.traversal.visible ) { + + overlay.setRegionVisible( info.range, true ); + + } + // if the image projection is outside the 0, 1 uvw range or there are no textures to draw in // the tiled image set the don't allocate a texture for it. let target = null; @@ -1306,6 +1327,7 @@ export class ImageOverlay { this._whenReady = null; this.isReady = false; this.isInitialized = false; + this._visibleRegionCounts = new Map(); } @@ -1383,6 +1405,32 @@ export class ImageOverlay { } + setRegionVisible( range, visible ) { + + const { _visibleRegionCounts } = this; + const key = range.join( '_' ); + let entry = _visibleRegionCounts.get( key ); + if ( ! entry ) { + + entry = { range: [ ...range ], count: 0 }; + _visibleRegionCounts.set( key, entry ); + + } + + entry.count += visible ? 1 : - 1; + + if ( entry.count < 0 ) { + + throw new Error(); + + } else if ( entry.count === 0 ) { + + _visibleRegionCounts.delete( key ); + + } + + } + } /** @@ -1682,6 +1730,10 @@ export class GeoJSONOverlay extends ImageOverlay { super( options ); this.imageSource = new GeoJSONImageSource( options ); + this._redrawQueue = new PriorityQueue(); + this._redrawQueue.maxJobs = 4; + this._redrawQueue.priorityCallback = () => 0; + } _init() { @@ -1727,9 +1779,52 @@ export class GeoJSONOverlay extends ImageOverlay { } + setRegionVisible( range, visible ) { + + super.setRegionVisible( range, visible ); + + if ( visible ) { + + const { _redrawQueue } = this; + const key = range.join( '_' ); + if ( _redrawQueue.has( key ) ) { + + _redrawQueue.flush( key ); + + } + + } + + } + redraw() { - this.imageSource.redraw(); + const { + imageSource, + _redrawQueue, + _visibleRegionCounts, + } = this; + + for ( const { range } of _visibleRegionCounts.values() ) { + + imageSource.redraw( ...range ); + + } + + imageSource.forEachItem( ( _, args ) => { + + const key = args.join( '_' ); + if ( ! _visibleRegionCounts.has( key ) && ! _redrawQueue.has( key ) ) { + + _redrawQueue.add( key, () => { + + imageSource.redraw( ...args ); + + } ); + + } + + } ); } diff --git a/src/three/plugins/images/MVTOverlay.js b/src/three/plugins/images/MVTOverlay.js index bdc53e2ac..0ed7c4fcf 100644 --- a/src/three/plugins/images/MVTOverlay.js +++ b/src/three/plugins/images/MVTOverlay.js @@ -2,6 +2,7 @@ import { ImageOverlay } from './ImageOverlayPlugin.js'; import { MVTImageSource } from './sources/MVTImageSource.js'; import { PMTilesImageSource } from './sources/PMTilesImageSource.js'; +import { PriorityQueue } from '3d-tiles-renderer/core'; /** * @callback MVTGetStyleCallback @@ -64,6 +65,10 @@ export class MVTOverlay extends ImageOverlay { super( options ); this.imageSource = options.imageSource ?? new MVTImageSource( options ); + this._redrawQueue = new PriorityQueue(); + this._redrawQueue.maxJobs = 4; + this._redrawQueue.priorityCallback = () => 0; + } _init() { @@ -140,9 +145,52 @@ export class MVTOverlay extends ImageOverlay { } + setRegionVisible( range, visible ) { + + super.setRegionVisible( range, visible ); + + if ( visible ) { + + const { _redrawQueue } = this; + const key = range.join( '_' ) + '_' + this.calculateLevel( range ); + if ( _redrawQueue.has( key ) ) { + + _redrawQueue.flush( key ); + + } + + } + + } + redraw() { - this.imageSource.redraw(); + const { + imageSource, + _redrawQueue, + _visibleRegionCounts, + } = this; + + for ( const { range } of _visibleRegionCounts.values() ) { + + imageSource.redraw( ...range, this.calculateLevel( range ) ); + + } + + imageSource.forEachItem( ( _, args ) => { + + const key = args.join( '_' ); + if ( ! _visibleRegionCounts.has( key ) && ! _redrawQueue.has( key ) ) { + + _redrawQueue.add( key, () => { + + imageSource.redraw( ...args ); + + } ); + + } + + } ); } diff --git a/src/three/plugins/images/sources/GeoJSONImageSource.js b/src/three/plugins/images/sources/GeoJSONImageSource.js index 020b99e90..e6d73feed 100644 --- a/src/three/plugins/images/sources/GeoJSONImageSource.js +++ b/src/three/plugins/images/sources/GeoJSONImageSource.js @@ -122,15 +122,17 @@ export class GeoJSONImageSource extends RegionImageSource { } - redraw() { + redraw( ...args ) { - this._updateCache( true ); - this.forEachItem( ( tex, args ) => { + const tex = this.get( ...args ); + if ( ! tex ) { - this._drawToCanvas( tex.image, args ); - tex.needsUpdate = true; + return; - } ); + } + + this._drawToCanvas( tex.image, args ); + tex.needsUpdate = true; } diff --git a/src/three/plugins/images/sources/MVTImageSource.js b/src/three/plugins/images/sources/MVTImageSource.js index dbd073d8a..abd58e371 100644 --- a/src/three/plugins/images/sources/MVTImageSource.js +++ b/src/three/plugins/images/sources/MVTImageSource.js @@ -184,35 +184,24 @@ export class MVTImageSource extends RegionImageSource { async fetchItem( [ minX, minY, maxX, maxY, level ], _signal ) { - const { resolution } = this; + const { resolution, _contentCache } = this; const canvas = document.createElement( 'canvas' ); canvas.width = resolution; canvas.height = resolution; - const ctx = canvas.getContext( '2d' ); const regionBounds = [ minX, minY, maxX, maxY ]; - const { _contentCache, _canvasRenderer } = this; const promises = []; forEachTileInBounds( regionBounds, level, _contentCache.tiling, ( tx, ty, tl ) => { - promises.push( ( async () => { - - const vectorTile = await _contentCache.lock( tx, ty, tl ); - if ( vectorTile ) { - - const tileBounds = _contentCache.tiling.getTileBounds( tx, ty, tl, true, false ); - _canvasRenderer.setFrame( ctx, tileBounds, regionBounds ); - this._renderVectorTile( vectorTile ); - - } - - } )() ); + promises.push( _contentCache.lock( tx, ty, tl ) ); } ); await Promise.all( promises ); + this._drawToCanvas( canvas, regionBounds, level ); + const tex = new CanvasTexture( canvas ); tex.colorSpace = SRGBColorSpace; tex.generateMipmaps = false; @@ -235,6 +224,48 @@ export class MVTImageSource extends RegionImageSource { } + redraw( ...args ) { + + const [ minX, minY, maxX, maxY, level ] = args; + const tex = this.get( minX, minY, maxX, maxY, level ); + if ( ! tex ) { + + return; + + } + + this._drawToCanvas( tex.image, [ minX, minY, maxX, maxY ], level ); + tex.needsUpdate = true; + + } + + dispose() { + + super.dispose(); + this._contentCache.dispose(); + + } + + _drawToCanvas( canvas, regionBounds, level ) { + + const { _contentCache, _canvasRenderer } = this; + const ctx = canvas.getContext( '2d' ); + forEachTileInBounds( regionBounds, level, _contentCache.tiling, ( tx, ty, tl ) => { + + const tileBounds = _contentCache.tiling.getTileBounds( tx, ty, tl, true, false ); + _canvasRenderer.setFrame( ctx, tileBounds, regionBounds ); + + const vectorTile = _contentCache.get( tx, ty, tl ); + if ( vectorTile ) { + + this._renderVectorTile( vectorTile ); + + } + + } ); + + } + _renderVectorTile( vectorTile ) { const { _canvasRenderer, getStyle } = this; @@ -290,40 +321,4 @@ export class MVTImageSource extends RegionImageSource { } - redraw() { - - this.forEachItem( ( tex, args ) => { - - const [ minX, minY, maxX, maxY, level ] = args; - const regionBounds = [ minX, minY, maxX, maxY ]; - const canvas = tex.image; - const ctx = canvas.getContext( '2d' ); - ctx.clearRect( 0, 0, canvas.width, canvas.height ); - - forEachTileInBounds( regionBounds, level, this._contentCache.tiling, ( tx, ty, tl ) => { - - const vectorTile = this._contentCache.get( tx, ty, tl ); - if ( vectorTile ) { - - const tileBounds = this._contentCache.tiling.getTileBounds( tx, ty, tl, true, false ); - this._canvasRenderer.setFrame( ctx, tileBounds, regionBounds ); - this._renderVectorTile( vectorTile ); - - } - - } ); - - tex.needsUpdate = true; - - } ); - - } - - dispose() { - - super.dispose(); - this._contentCache.dispose(); - - } - } diff --git a/src/three/plugins/images/sources/PMTilesImageSource.js b/src/three/plugins/images/sources/PMTilesImageSource.js index 3e17b8914..a2371ddcc 100644 --- a/src/three/plugins/images/sources/PMTilesImageSource.js +++ b/src/three/plugins/images/sources/PMTilesImageSource.js @@ -124,6 +124,7 @@ class PMTilesContentCache extends MVTContentCache { } +// TODO: this should probably be a form of proxy export class PMTilesImageSource extends RegionImageSource { get tiling() { @@ -239,16 +240,22 @@ export class PMTilesImageSource extends RegionImageSource { } - redraw() { + redraw( ...args ) { if ( this._deferredSource instanceof MVTImageSource ) { - this._deferredSource.redraw(); + this._deferredSource.redraw( ...args ); } } + forEachItem( ...args ) { + + return this._deferredSource.forEachItem( ...args ); + + } + dispose() { super.dispose(); diff --git a/test/core/PriorityQueue.test.js b/test/core/PriorityQueue.test.js index 5cbdefbbf..cc26b9c6d 100644 --- a/test/core/PriorityQueue.test.js +++ b/test/core/PriorityQueue.test.js @@ -276,4 +276,62 @@ describe( 'PriorityQueue', () => { } ); + it( 'should immediately run the callback for the flushed item.', () => { + + const queue = new PriorityQueue(); + queue.autoUpdate = false; + + const order = []; + const A = {}; + const B = {}; + const C = {}; + queue.add( A, () => order.push( 'A' ) ); + queue.add( B, () => order.push( 'B' ) ); + queue.add( C, () => order.push( 'C' ) ); + + queue.flush( B ); + + expect( order ).toEqual( [ 'B' ] ); + expect( queue.items ).toHaveLength( 2 ); + expect( queue.callbacks.size ).toEqual( 2 ); + expect( queue.items ).toHaveLength( queue.callbacks.size ); + + } ); + + it( 'should resolve the promise returned by add when flushed.', async () => { + + const queue = new PriorityQueue(); + queue.autoUpdate = false; + + const key = {}; + let resolved = false; + const promise = queue.add( key, () => 42 ).then( val => { + + expect( val ).toEqual( 42 ); + resolved = true; + + } ); + + queue.flush( key ); + + await promise; + + expect( resolved ).toEqual( true ); + + } ); + + it( 'should do nothing when flushing an item not in the queue.', () => { + + const queue = new PriorityQueue(); + queue.autoUpdate = false; + + const A = {}; + const B = {}; + queue.add( A, () => {} ); + + expect( () => queue.flush( B ) ).not.toThrow(); + expect( queue.items ).toHaveLength( 1 ); + + } ); + } );