diff --git a/modules/geo-layers/src/tile-layer/tile-layer.ts b/modules/geo-layers/src/tile-layer/tile-layer.ts index de5e6b3a8fb..173fe69277c 100644 --- a/modules/geo-layers/src/tile-layer/tile-layer.ts +++ b/modules/geo-layers/src/tile-layer/tile-layer.ts @@ -52,7 +52,9 @@ const defaultProps: DefaultProps = { debounceTime: 0, zoomOffset: 0, visibleMinZoom: null, - visibleMaxZoom: null + visibleMaxZoom: null, + prefetchZoomDelta: 0, + prefetchTileRadius: 0 }; /** All props supported by the TileLayer */ @@ -167,6 +169,20 @@ type _TileLayerProps = { * @default null */ visibleMaxZoom?: number | null; + + /** + * Number of lower zoom levels to preload around selected tiles. + * + * @default 0 + */ + prefetchZoomDelta?: number; + + /** + * Number of selected-zoom tile rings to include when prefetching lower zoom tiles. + * + * @default 0 + */ + prefetchTileRadius?: number; }; export type TileLayerPickingInfo< @@ -268,7 +284,9 @@ export default class TileLayer extends debounceTime, zoomOffset, visibleMinZoom, - visibleMaxZoom + visibleMaxZoom, + prefetchZoomDelta, + prefetchTileRadius } = this.props; return { @@ -284,6 +302,8 @@ export default class TileLayer extends zoomOffset, visibleMinZoom, visibleMaxZoom, + prefetchZoomDelta, + prefetchTileRadius, getTileData: this.getTileData.bind(this), onTileLoad: this._onTileLoad.bind(this), diff --git a/modules/geo-layers/src/tileset-2d/tile-2d-header.ts b/modules/geo-layers/src/tileset-2d/tile-2d-header.ts index 8375a165c28..6e4e5f662eb 100644 --- a/modules/geo-layers/src/tileset-2d/tile-2d-header.ts +++ b/modules/geo-layers/src/tileset-2d/tile-2d-header.ts @@ -19,6 +19,7 @@ export class Tile2DHeader { index: TileIndex; isVisible: boolean; isSelected: boolean; + isPrefetch: boolean; parent: Tile2DHeader | null; children: Tile2DHeader[] | null; content: DataT | null; @@ -42,6 +43,7 @@ export class Tile2DHeader { this.index = index; this.isVisible = false; this.isSelected = false; + this.isPrefetch = false; this.parent = null; this.children = []; diff --git a/modules/geo-layers/src/tileset-2d/tileset-2d.ts b/modules/geo-layers/src/tileset-2d/tileset-2d.ts index 8a317db9ad7..fed6423a57c 100644 --- a/modules/geo-layers/src/tileset-2d/tileset-2d.ts +++ b/modules/geo-layers/src/tileset-2d/tileset-2d.ts @@ -85,6 +85,10 @@ export type Tileset2DProps = { visibleMinZoom?: number | null; /** The maximum zoom level at which tiles are visible. @default null */ visibleMaxZoom?: number | null; + /** Number of lower zoom levels to preload around selected tiles. @default 0 */ + prefetchZoomDelta?: number; + /** Number of selected-zoom tile rings to include when prefetching lower zoom tiles. @default 0 */ + prefetchTileRadius?: number; /** Called when a tile successfully loads. */ onTileLoad?: (tile: Tile2DHeader) => void; /** Called when a tile is cleared from cache. */ @@ -114,6 +118,8 @@ export const DEFAULT_TILESET2D_PROPS: Omit, 'getTileDat zoomOffset: 0, visibleMinZoom: null, visibleMaxZoom: null, + prefetchZoomDelta: 0, + prefetchTileRadius: 0, // onTileLoad: (tile: Tile2DHeader) => void, // onTileUnload: (tile: Tile2DHeader) => void, // onTileError: (error: any, tile: Tile2DHeader) => void, /** Called when all tiles in the current viewport are loaded. */ // onViewportLoad: ((tiles: Tile2DHeader[]) => void) | null, @@ -137,6 +143,7 @@ export class Tileset2D { private _viewport: Viewport | null; private _zRange: ZRange | null; private _selectedTiles: Tile2DHeader[] | null; + private _prefetchTiles: Tile2DHeader[] | null; private _frameNumber: number; private _modelMatrix: Matrix4; private _modelMatrixInverse: Matrix4; @@ -178,6 +185,7 @@ export class Tileset2D { this._viewport = null; this._zRange = null; this._selectedTiles = null; + this._prefetchTiles = null; this._frameNumber = 0; this._modelMatrix = new Matrix4(); @@ -223,6 +231,7 @@ export class Tileset2D { this._cache.clear(); this._tiles = []; this._selectedTiles = null; + this._prefetchTiles = null; } reloadAll(): void { @@ -269,6 +278,9 @@ export class Tileset2D { modelMatrixInverse: this._modelMatrixInverse }); this._selectedTiles = tileIndices.map(index => this._getTile(index, true)); + this._prefetchTiles = this._getPrefetchTileIndices(tileIndices).map(index => + this._getTile(index, true) + ); if (this._dirty) { // Some new tiles are added @@ -277,6 +289,7 @@ export class Tileset2D { // Check for needed reloads explicitly even if the view/matrix has not changed. } else if (this.needsReload) { this._selectedTiles = this._selectedTiles!.map(tile => this._getTile(tile.index, true)); + this._prefetchTiles = this._prefetchTiles!.map(tile => this._getTile(tile.index, true)); } // Update tile states @@ -410,12 +423,18 @@ export class Tileset2D { visibilities[i++] = tile.isVisible; tile.isSelected = false; tile.isVisible = false; + tile.isPrefetch = false; } // @ts-expect-error called only when _selectedTiles is already defined for (const tile of this._selectedTiles) { tile.isSelected = true; tile.isVisible = true; } + for (const tile of this._prefetchTiles || []) { + if (!tile.isSelected) { + tile.isPrefetch = true; + } + } // Strategy-specific state logic (typeof refinementStrategy === 'function' @@ -444,6 +463,9 @@ export class Tileset2D { if (tile.isVisible) { return 1e6 + this._getTileDistanceToViewportCenter(tile); } + if (tile.isPrefetch) { + return 2e6 + this._getTileDistanceToViewportCenter(tile); + } return -1; } @@ -471,6 +493,53 @@ export class Tileset2D { return Number.MAX_SAFE_INTEGER; } + private _getPrefetchTileIndices(tileIndices: TileIndex[]): TileIndex[] { + const {prefetchZoomDelta, prefetchTileRadius} = this.opts; + const zoomDelta = Math.floor(prefetchZoomDelta); + const radius = Math.max(0, Math.floor(prefetchTileRadius)); + + if (!zoomDelta || tileIndices.length === 0) { + return []; + } + + const ids = new Set(tileIndices.map(index => this.getTileId(index))); + const indices: TileIndex[] = []; + + for (const tileIndex of tileIndices) { + const tileZoom = this.getTileZoom(tileIndex); + const targetZoom = Math.max(this._minZoom ?? 0, tileZoom - zoomDelta); + const parentDelta = tileZoom - targetZoom; + if (parentDelta <= 0) { + continue; + } + + const scale = 2 ** parentDelta; + const worldSize = 2 ** tileZoom; + for (let xOffset = -radius; xOffset <= radius; xOffset++) { + for (let yOffset = -radius; yOffset <= radius; yOffset++) { + const x = tileIndex.x + xOffset; + const y = tileIndex.y + yOffset; + if (y < 0 || y >= worldSize) { + continue; + } + + const wrappedX = ((x % worldSize) + worldSize) % worldSize; + const prefetchIndex = { + x: Math.floor(wrappedX / scale), + y: Math.floor(y / scale), + z: targetZoom + }; + const id = this.getTileId(prefetchIndex); + if (!ids.has(id)) { + ids.add(id); + indices.push(prefetchIndex); + } + } + } + } + return indices; + } + private _pruneRequests(): void { const {maxRequests = 0} = this.opts; diff --git a/test/modules/geo-layers/tileset-2d/tile-2d-header.spec.ts b/test/modules/geo-layers/tileset-2d/tile-2d-header.spec.ts index a7a7e2124a1..08027b7c921 100644 --- a/test/modules/geo-layers/tileset-2d/tile-2d-header.spec.ts +++ b/test/modules/geo-layers/tileset-2d/tile-2d-header.spec.ts @@ -6,7 +6,7 @@ import {test, expect} from 'vitest'; import {_Tile2DHeader as Tile2DHeader} from '@deck.gl/geo-layers'; import {RequestScheduler} from '@loaders.gl/loader-utils'; -const getPriority = tile => (tile.isSelected ? 1 : -1); +const getPriority = tile => (tile.isSelected || tile.isPrefetch ? 1 : -1); test('Tile2DHeader', async () => { let onTileLoadCalled = false; diff --git a/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts b/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts index 1f14df8e444..8dcf631cce7 100644 --- a/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts +++ b/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts @@ -55,6 +55,41 @@ test('Tileset2D#update', () => { expect(tileset.tiles[0].bbox, 'tile has metadata').toBeTruthy(); }); +test('Tileset2D#prefetch parent tiles', () => { + const tileset = new Tileset2D({ + getTileData, + prefetchZoomDelta: 1, + prefetchTileRadius: 0, + onTileLoad: () => {} + }); + tileset.update(testViewport); + + expect(tileset._cache.get('1171-1566-12').isSelected, 'selected tile is selected').toBeTruthy(); + expect(tileset._cache.get('585-783-11').isPrefetch, 'parent tile is prefetched').toBeTruthy(); + expect(tileset._cache.get('585-783-11').isVisible, 'prefetch tile is hidden').toBeFalsy(); +}); + +test('Tileset2D#prefetch parent tile ring', () => { + const tileset = new Tileset2D({ + getTileData, + prefetchZoomDelta: 1, + prefetchTileRadius: 1, + onTileLoad: () => {} + }); + tileset.update(testViewport); + + const prefetchTileIds = Array.from(tileset._cache.values()) + .filter(tile => tile.isPrefetch) + .map(tile => tile.id) + .sort(); + expect(prefetchTileIds, 'prefetches the lower-zoom ring around selected tiles').toEqual([ + '585-782-11', + '585-783-11', + '586-782-11', + '586-783-11' + ]); +}); + test('Tileset2D#updateOnModelMatrix', () => { const tileset = new Tileset2D({ getTileData,