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 32406256cbc..8375a165c28 100644 --- a/modules/geo-layers/src/tileset-2d/tile-2d-header.ts +++ b/modules/geo-layers/src/tileset-2d/tile-2d-header.ts @@ -10,6 +10,7 @@ import type {Layer} from '@deck.gl/core'; export type TileLoadDataProps = { requestScheduler: RequestScheduler; getData: (props: TileLoadProps) => Promise; + getPriority: (tile: Tile2DHeader) => number; onLoad: (tile: Tile2DHeader) => void; onError: (error: any, tile: Tile2DHeader) => void; }; @@ -106,6 +107,7 @@ export class Tile2DHeader { /* eslint-disable max-statements */ private async _loadData({ getData, + getPriority, requestScheduler, onLoad, onError @@ -116,10 +118,8 @@ export class Tile2DHeader { this._abortController = new AbortController(); const {signal} = this._abortController; - // @ts-expect-error (2345) Argument of type '(tile: any) => 1 | -1' is not assignable ... - const requestToken = await requestScheduler.scheduleRequest(this, tile => { - return tile.isSelected ? 1 : -1; - }); + // @ts-expect-error (2345) loaders.gl's RequestScheduler callback type is too narrow. + const requestToken = await requestScheduler.scheduleRequest(this, getPriority); if (!requestToken) { this._isCancelled = true; diff --git a/modules/geo-layers/src/tileset-2d/tileset-2d.ts b/modules/geo-layers/src/tileset-2d/tileset-2d.ts index e0752ec13a8..d4d72164f3f 100644 --- a/modules/geo-layers/src/tileset-2d/tileset-2d.ts +++ b/modules/geo-layers/src/tileset-2d/tileset-2d.ts @@ -437,6 +437,43 @@ export class Tileset2D { private _getCullBounds = memoize(getCullBounds); + private _getRequestPriority(tile: Tile2DHeader): number { + // RequestScheduler loads lower priority values first. + const distance = this._getTileDistanceSquared(tile); + if (tile.isSelected) { + return distance; + } + if (tile.isVisible) { + return 1e6 + distance; + } + return -1; + } + + private _getTileDistanceSquared(tile: Tile2DHeader): number { + const {width, height} = this._viewport || {}; + if (!this._viewport || !width || !height) { + return 0; + } + + const {bbox} = tile; + const center = + 'west' in bbox + ? ([(bbox.west + bbox.east) / 2, (bbox.south + bbox.north) / 2] as [number, number]) + : ([(bbox.left + bbox.right) / 2, (bbox.top + bbox.bottom) / 2] as [number, number]); + + try { + const [x, y] = this._viewport.project(center); + if (Number.isFinite(x) && Number.isFinite(y)) { + const dx = x - width / 2; + const dy = y - height / 2; + return dx * dx + dy * dy; + } + } catch { + // Some viewport/tile combinations are not projectable. Keep them valid but lowest priority. + } + return Number.MAX_SAFE_INTEGER; + } + private _pruneRequests(): void { const {maxRequests = 0} = this.opts; @@ -542,6 +579,7 @@ export class Tileset2D { // eslint-disable-next-line @typescript-eslint/no-floating-promises tile.loadData({ getData: this.opts.getTileData, + getPriority: this._getRequestPriority.bind(this), requestScheduler: this._requestScheduler, onLoad: this.onTileLoad, onError: this.opts.onTileError 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 578ea3b595d..a7a7e2124a1 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,6 +6,8 @@ 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); + test('Tile2DHeader', async () => { let onTileLoadCalled = false; let onTileErrorCalled = false; @@ -14,6 +16,7 @@ test('Tile2DHeader', async () => { let tile2d = new Tile2DHeader({}); await tile2d.loadData({ requestScheduler, + getPriority, getData: () => 'loaded data', onLoad: () => (onTileLoadCalled = true), onError: () => (onTileErrorCalled = true) @@ -26,6 +29,7 @@ test('Tile2DHeader', async () => { tile2d = new Tile2DHeader({}); await tile2d.loadData({ requestScheduler, + getPriority, getData: () => { throw new Error('getTileData error'); }, @@ -44,6 +48,7 @@ test('Tile2DHeader#Cancel request if not selected', async () => { const requestScheduler = new RequestScheduler({throttleRequests: true, maxRequests: 1}); const opts = { requestScheduler, + getPriority, getData: () => tileRequestCount++, onLoad: () => onTileLoadCalled++, onError: () => onTileErrorCalled++ @@ -66,6 +71,35 @@ test('Tile2DHeader#Cancel request if not selected', async () => { expect(onTileLoadCalled === 1 && onTileErrorCalled === 0, 'Callbacks invoked').toBeTruthy(); }); +test('Tile2DHeader#request priority', async () => { + const requestOrder: string[] = []; + const requestScheduler = new RequestScheduler({throttleRequests: true, maxRequests: 1}); + const opts = { + requestScheduler, + getPriority: tile => (tile.id === 'center' ? 0 : 10), + getData: ({id}) => { + requestOrder.push(id); + return id; + }, + onLoad: () => {}, + onError: () => {} + }; + + const edgeTile = new Tile2DHeader({}); + edgeTile.id = 'edge'; + edgeTile.isSelected = true; + const centerTile = new Tile2DHeader({}); + centerTile.id = 'center'; + centerTile.isSelected = true; + + const edgeLoader = edgeTile.loadData(opts); + const centerLoader = centerTile.loadData(opts); + await edgeLoader; + await centerLoader; + + expect(requestOrder, 'lower request priority values load first').toEqual(['center', 'edge']); +}); + test('Tile2DHeader#abort', async () => { const requestScheduler = new RequestScheduler({throttleRequests: true, maxRequests: 1}); let onTileLoadCalled = false; @@ -73,6 +107,7 @@ test('Tile2DHeader#abort', async () => { const opts = { requestScheduler, + getPriority, getData: () => null, onLoad: () => (onTileLoadCalled = true), onError: () => (onTileErrorCalled = true) @@ -104,6 +139,7 @@ test('Tile2DHeader#reload', async () => { let onTileErrorCalled = 0; const opts = { requestScheduler, + getPriority, onLoad: () => onTileLoadCalled++, onError: () => onTileErrorCalled++ };