From e94f041341707aa5255e3885fe327a4b59655274 Mon Sep 17 00:00:00 2001 From: Charles Richardson Date: Wed, 10 Jun 2026 16:06:59 -0400 Subject: [PATCH 1/3] Add tile request priority accessor --- docs/api-reference/geo-layers/tile-layer.md | 10 ++ modules/geo-layers/src/index.ts | 1 + .../geo-layers/src/tile-layer/tile-layer.ts | 14 +- modules/geo-layers/src/tileset-2d/index.ts | 2 +- .../src/tileset-2d/tile-2d-header.ts | 8 +- .../geo-layers/src/tileset-2d/tileset-2d.ts | 116 ++++++++++++++- .../tileset-2d/tile-2d-header.spec.ts | 36 +++++ .../geo-layers/tileset-2d/tileset-2d.spec.ts | 134 ++++++++++++++++++ 8 files changed, 314 insertions(+), 7 deletions(-) diff --git a/docs/api-reference/geo-layers/tile-layer.md b/docs/api-reference/geo-layers/tile-layer.md index f0326a28cfe..54623c3dfdb 100644 --- a/docs/api-reference/geo-layers/tile-layer.md +++ b/docs/api-reference/geo-layers/tile-layer.md @@ -227,6 +227,16 @@ getTileData: ({url, signal}) => { } ``` +#### `getPriority` (Function, optional) {#getpriority} + +- Default: `null` + +If supplied, `getPriority` is called for each relevant queued tile request and its return value controls the order in which queued requests are started. Lower non-negative values load first. Values below `0` cancel the queued request. + +If not supplied, selected tiles are requested before visible placeholder tiles, and tiles that cover the viewport center are requested before tiles farther from the center. This keeps the area the user is looking at from waiting behind edge tiles during view changes. + +This prop only affects request ordering while request throttling is active (`maxRequests > 0`). It receives a [Tile](#tile) instance. + #### `TilesetClass` (class, optional) {#tilesetclass} - Default: `Tileset2D` diff --git a/modules/geo-layers/src/index.ts b/modules/geo-layers/src/index.ts index e9b8fcaba2d..c4c3ea40d79 100644 --- a/modules/geo-layers/src/index.ts +++ b/modules/geo-layers/src/index.ts @@ -40,6 +40,7 @@ export type {GeohashLayerProps} from './geohash-layer/geohash-layer'; export type {GeoBoundingBox, NonGeoBoundingBox} from './tileset-2d/index'; export type {TileLoadProps as _TileLoadProps} from './tileset-2d/index'; +export type {TilePriorityFunction as _TilePriorityFunction} from './tileset-2d/index'; export type {Tileset2DProps as _Tileset2DProps} from './tileset-2d/index'; export {getURLFromTemplate as _getURLFromTemplate} from './tileset-2d/index'; diff --git a/modules/geo-layers/src/tile-layer/tile-layer.ts b/modules/geo-layers/src/tile-layer/tile-layer.ts index de5e6b3a8fb..6791d530bbb 100644 --- a/modules/geo-layers/src/tile-layer/tile-layer.ts +++ b/modules/geo-layers/src/tile-layer/tile-layer.ts @@ -17,7 +17,7 @@ import { import {GeoJsonLayer} from '@deck.gl/layers'; import {LayersList} from '@deck.gl/core'; -import type {TileLoadProps, ZRange} from '../tileset-2d/index'; +import type {TileLoadProps, TilePriorityFunction, ZRange} from '../tileset-2d/index'; import { Tileset2D, Tile2DHeader, @@ -34,6 +34,7 @@ const defaultProps: DefaultProps = { dataComparator: urlType.equal, renderSubLayers: {type: 'function', value: (props: any) => new GeoJsonLayer(props)}, getTileData: {type: 'function', optional: true, value: null}, + getPriority: {type: 'function', optional: true, value: null}, // TODO - change to onViewportLoad to align with Tile3DLayer onViewportLoad: {type: 'function', optional: true, value: null}, onTileLoad: {type: 'function', value: tile => {}}, @@ -81,6 +82,15 @@ type _TileLayerProps = { */ getTileData?: ((props: TileLoadProps) => Promise | DataT) | null; + /** + * Returns the request priority for queued tiles. Lower non-negative values load first; + * values below 0 cancel the queued request. If not supplied, tiles closer to the viewport + * center are requested first. + * + * @default null + */ + getPriority?: TilePriorityFunction | null; + /** Called when all tiles in the current viewport are loaded. */ onViewportLoad?: ((tiles: Tile2DHeader[]) => void) | null; @@ -261,6 +271,7 @@ export default class TileLayer extends maxCacheSize, maxCacheByteSize, refinementStrategy, + getPriority, extent, maxZoom, minZoom, @@ -278,6 +289,7 @@ export default class TileLayer extends minZoom, tileSize, refinementStrategy, + getPriority, extent, maxRequests, debounceTime, diff --git a/modules/geo-layers/src/tileset-2d/index.ts b/modules/geo-layers/src/tileset-2d/index.ts index d6ed77d3ccc..5fec2c6c077 100644 --- a/modules/geo-layers/src/tileset-2d/index.ts +++ b/modules/geo-layers/src/tileset-2d/index.ts @@ -13,7 +13,7 @@ export type { TileBoundingBox } from './types'; -export type {Tileset2DProps, RefinementStrategy} from './tileset-2d'; +export type {Tileset2DProps, RefinementStrategy, TilePriorityFunction} from './tileset-2d'; export {Tileset2D, STRATEGY_DEFAULT} from './tileset-2d'; export {Tile2DHeader} from './tile-2d-header'; 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..f0e25ac5cf4 100644 --- a/modules/geo-layers/src/tileset-2d/tileset-2d.ts +++ b/modules/geo-layers/src/tileset-2d/tileset-2d.ts @@ -10,7 +10,7 @@ import {Matrix4, equals, NumericArray} from '@math.gl/core'; import {Tile2DHeader} from './tile-2d-header'; import {getTileIndices, tileToBoundingBox, getCullBounds, transformBox} from './utils'; -import {Bounds, TileIndex, ZRange} from './types'; +import {Bounds, TileBoundingBox, TileIndex, ZRange} from './types'; import {TileLoadProps} from './types'; import {memoize} from './memoize'; @@ -48,6 +48,12 @@ export type RefinementStrategy = | RefinementStrategyFunction; const DEFAULT_CACHE_SCALE = 5; +const SELECTED_TILE_PRIORITY = 0; +const VISIBLE_TILE_PRIORITY = 1e8; +const MAX_TILE_DISTANCE_PRIORITY = VISIBLE_TILE_PRIORITY - SELECTED_TILE_PRIORITY - 1; + +/** Returns the request priority for a tile. Lower non-negative values load first. */ +export type TilePriorityFunction = (tile: Tile2DHeader) => number; const STRATEGIES = { [STRATEGY_DEFAULT]: updateTileStateDefault, @@ -73,6 +79,13 @@ export type Tileset2DProps = { maxCacheByteSize?: number | null; /** How the tile layer refines the visibility of tiles. @default 'best-available' */ refinementStrategy?: RefinementStrategy; + /** + * Returns the request priority for queued tiles. Lower non-negative values load first; + * values below 0 cancel the queued request. If not supplied, tiles closer to the viewport + * center are requested first. + * @default null + */ + getPriority?: TilePriorityFunction | null; /** Range of minimum and maximum heights in the tile. */ zRange?: ZRange | null; /** The maximum number of concurrent getTileData calls. @default 6 */ @@ -108,6 +121,7 @@ export const DEFAULT_TILESET2D_PROPS: Omit, 'getTileDat maxCacheSize: null, maxCacheByteSize: null, refinementStrategy: 'best-available', + getPriority: null, zRange: null, maxRequests: 6, debounceTime: 0, @@ -437,6 +451,105 @@ export class Tileset2D { private _getCullBounds = memoize(getCullBounds); + private _getRequestPriority(tile: Tile2DHeader): number { + if (!tile.isSelected && !tile.isVisible) { + return -1; + } + if (this.opts.getPriority) { + return this.opts.getPriority(tile); + } + + // RequestScheduler loads lower priority values first. + const distance = this._getTileDistancePriority(tile); + if (tile.isSelected) { + return SELECTED_TILE_PRIORITY + distance; + } + return VISIBLE_TILE_PRIORITY + distance; + } + + private _getTileDistancePriority(tile: Tile2DHeader): number { + const {width, height} = this._viewport || {}; + if (!this._viewport || !width || !height) { + return 0; + } + + try { + const points = this._getTileScreenCorners(tile.bbox); + const center: [number, number] = [width / 2, height / 2]; + if (points.length === 4) { + if (this._isPointInPolygon(center, points)) { + return 0; + } + const distance = points.reduce((minDistance, point, i) => { + const nextPoint = points[(i + 1) % points.length]; + return Math.min( + minDistance, + this._getPointToSegmentDistanceSquared(center, point, nextPoint) + ); + }, Number.MAX_SAFE_INTEGER); + return Math.min(distance, MAX_TILE_DISTANCE_PRIORITY); + } + } catch { + // Some viewport/tile combinations are not projectable. Keep them valid but last in tier. + } + return MAX_TILE_DISTANCE_PRIORITY; + } + + private _getTileScreenCorners(bbox: TileBoundingBox): [number, number][] { + const coordinates: [number, number][] = + 'west' in bbox + ? [ + [bbox.west, bbox.south], + [bbox.east, bbox.south], + [bbox.east, bbox.north], + [bbox.west, bbox.north] + ] + : [ + [bbox.left, bbox.top], + [bbox.right, bbox.top], + [bbox.right, bbox.bottom], + [bbox.left, bbox.bottom] + ]; + + return coordinates + .map(coordinate => this._viewport!.project(coordinate)) + .filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y)) as [number, number][]; + } + + private _isPointInPolygon(point: [number, number], polygon: [number, number][]): boolean { + let inside = false; + const [x, y] = point; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const [xi, yi] = polygon[i]; + const [xj, yj] = polygon[j]; + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) { + inside = !inside; + } + } + return inside; + } + + private _getPointToSegmentDistanceSquared( + point: [number, number], + segmentStart: [number, number], + segmentEnd: [number, number] + ): number { + const [x, y] = point; + const [x1, y1] = segmentStart; + const [x2, y2] = segmentEnd; + const dx = x2 - x1; + const dy = y2 - y1; + const lengthSquared = dx * dx + dy * dy; + const t = lengthSquared + ? Math.max(0, Math.min(1, ((x - x1) * dx + (y - y1) * dy) / lengthSquared)) + : 0; + const segmentX = x1 + t * dx; + const segmentY = y1 + t * dy; + const distanceX = x - segmentX; + const distanceY = y - segmentY; + return distanceX * distanceX + distanceY * distanceY; + } + private _pruneRequests(): void { const {maxRequests = 0} = this.opts; @@ -542,6 +655,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..dd01f4057da 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 === 'preferred' ? 0 : 10), + getData: ({id}) => { + requestOrder.push(id); + return id; + }, + onLoad: () => {}, + onError: () => {} + }; + + const edgeTile = new Tile2DHeader({}); + edgeTile.id = 'edge'; + edgeTile.isSelected = true; + const preferredTile = new Tile2DHeader({}); + preferredTile.id = 'preferred'; + preferredTile.isSelected = true; + + const edgeLoader = edgeTile.loadData(opts); + const preferredLoader = preferredTile.loadData(opts); + await edgeLoader; + await preferredLoader; + + expect(requestOrder, 'lower request priority values load first').toEqual(['preferred', '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++ }; 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..9631b2770f1 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,140 @@ test('Tileset2D#update', () => { expect(tileset.tiles[0].bbox, 'tile has metadata').toBeTruthy(); }); +test('Tileset2D#getRequestPriority ranks tiles by viewport coverage', () => { + const tileset = new Tileset2D({ + getTileData, + onTileLoad: () => {} + }); + Object.assign(tileset, { + _viewport: { + width: 100, + height: 100, + project: ([x, y]) => [x, y] + } + }); + + const selectedAtCenterEdge = { + bbox: {left: 0, top: 0, right: 50, bottom: 100}, + index: {x: 0, y: 0, z: 10}, + isSelected: true, + isVisible: true + }; + const selectedNearCenter = { + bbox: {left: 60, top: 45, right: 70, bottom: 55}, + index: {x: 1, y: 0, z: 10}, + isSelected: true, + isVisible: true + }; + const visibleAtCenter = { + bbox: {left: 0, top: 0, right: 100, bottom: 100}, + index: {x: 0, y: 0, z: 8}, + isSelected: false, + isVisible: true + }; + + expect((tileset as any)._getRequestPriority(selectedAtCenterEdge)).toBeLessThan( + (tileset as any)._getRequestPriority(selectedNearCenter) + ); + expect((tileset as any)._getRequestPriority(selectedNearCenter)).toBeLessThan( + (tileset as any)._getRequestPriority(visibleAtCenter) + ); + + tileset.finalize(); +}); + +test('Tileset2D#getRequestPriority keeps unprojectable tiles within priority tiers', () => { + const tileset = new Tileset2D({ + getTileData, + onTileLoad: () => {} + }); + Object.assign(tileset, { + _viewport: { + width: 100, + height: 100, + project: ([x, y]) => (x < 0 || y < 0 ? [Number.NaN, Number.NaN] : [x, y]) + } + }); + + const selectedUnprojectable = { + bbox: {left: -20, top: -20, right: -10, bottom: -10}, + index: {x: 0, y: 0, z: 10}, + isSelected: true, + isVisible: true + }; + const visibleAtCenter = { + bbox: {left: 0, top: 0, right: 100, bottom: 100}, + index: {x: 1, y: 0, z: 10}, + isSelected: false, + isVisible: true + }; + const visibleUnprojectable = { + bbox: {left: -20, top: -20, right: -10, bottom: -10}, + index: {x: 2, y: 0, z: 10}, + isSelected: false, + isVisible: true + }; + const staleAtCenter = { + bbox: {left: 0, top: 0, right: 100, bottom: 100}, + index: {x: 3, y: 0, z: 10}, + isSelected: false, + isVisible: false + }; + + expect((tileset as any)._getRequestPriority(selectedUnprojectable)).toBeLessThan( + (tileset as any)._getRequestPriority(visibleAtCenter) + ); + expect((tileset as any)._getRequestPriority(visibleAtCenter)).toBeLessThan( + (tileset as any)._getRequestPriority(visibleUnprojectable) + ); + expect((tileset as any)._getRequestPriority(staleAtCenter)).toBeLessThan(0); + + tileset.finalize(); +}); + +test('Tileset2D#getRequestPriority uses custom priority for relevant tiles', () => { + const tileset = new Tileset2D({ + getTileData, + getPriority: tile => (tile.id === 'edge' ? 0 : 10), + onTileLoad: () => {} + }); + Object.assign(tileset, { + _viewport: { + width: 100, + height: 100, + project: ([x, y]) => [x, y] + } + }); + + const selectedAtCenter = { + id: 'center', + bbox: {left: 0, top: 0, right: 100, bottom: 100}, + index: {x: 0, y: 0, z: 10}, + isSelected: true, + isVisible: true + }; + const selectedAtEdge = { + id: 'edge', + bbox: {left: 70, top: 70, right: 80, bottom: 80}, + index: {x: 1, y: 0, z: 10}, + isSelected: true, + isVisible: true + }; + const staleAtEdge = { + id: 'edge', + bbox: {left: 70, top: 70, right: 80, bottom: 80}, + index: {x: 2, y: 0, z: 10}, + isSelected: false, + isVisible: false + }; + + expect((tileset as any)._getRequestPriority(selectedAtEdge)).toBe(0); + expect((tileset as any)._getRequestPriority(selectedAtCenter)).toBe(10); + expect((tileset as any)._getRequestPriority(staleAtEdge)).toBeLessThan(0); + + tileset.finalize(); +}); + test('Tileset2D#updateOnModelMatrix', () => { const tileset = new Tileset2D({ getTileData, From c0d2d7172819cd13e5fc7a9f0395e714baf23521 Mon Sep 17 00:00:00 2001 From: Charles Richardson Date: Thu, 11 Jun 2026 15:51:35 -0400 Subject: [PATCH 2/3] Make tile request priority internal --- docs/api-reference/geo-layers/tile-layer.md | 12 +----- modules/geo-layers/src/index.ts | 1 - .../geo-layers/src/tile-layer/tile-layer.ts | 14 +----- modules/geo-layers/src/tileset-2d/index.ts | 2 +- .../src/tileset-2d/tile-2d-header.ts | 6 +-- .../geo-layers/src/tileset-2d/tileset-2d.ts | 16 +------ .../tileset-2d/tile-2d-header.spec.ts | 14 +++--- .../geo-layers/tileset-2d/tileset-2d.spec.ts | 43 ------------------- 8 files changed, 15 insertions(+), 93 deletions(-) diff --git a/docs/api-reference/geo-layers/tile-layer.md b/docs/api-reference/geo-layers/tile-layer.md index 54623c3dfdb..3016365a4fd 100644 --- a/docs/api-reference/geo-layers/tile-layer.md +++ b/docs/api-reference/geo-layers/tile-layer.md @@ -227,16 +227,6 @@ getTileData: ({url, signal}) => { } ``` -#### `getPriority` (Function, optional) {#getpriority} - -- Default: `null` - -If supplied, `getPriority` is called for each relevant queued tile request and its return value controls the order in which queued requests are started. Lower non-negative values load first. Values below `0` cancel the queued request. - -If not supplied, selected tiles are requested before visible placeholder tiles, and tiles that cover the viewport center are requested before tiles farther from the center. This keeps the area the user is looking at from waiting behind edge tiles during view changes. - -This prop only affects request ordering while request throttling is active (`maxRequests > 0`). It receives a [Tile](#tile) instance. - #### `TilesetClass` (class, optional) {#tilesetclass} - Default: `Tileset2D` @@ -335,6 +325,8 @@ If `<= 0`, no throttling will occur, and `getTileData` may be called an unlimite If `> 0`, a maximum of `maxRequests` instances of `getTileData` will be called concurrently. Requests may never be called if the tile wasn't visible long enough to be scheduled and started. Requests may also be aborted (through the `signal` passed to `getTileData`) if there are more than `maxRequests` ongoing requests and some of those are for tiles that are no longer visible. +When requests are queued, selected tiles are scheduled before visible placeholder tiles. Within each group, tiles closer to the viewport center are scheduled first. + If `getTileData` makes `fetch` requests against an HTTP 1 web server, then `maxRequests` should correlate to the browser's maximum number of concurrent `fetch` requests. For Chrome, the max is 6 per domain. If you use the `data` prop and specify multiple domains, you can increase this limit. For example, with Chrome and 3 domains specified, you can set `maxRequests=18`. If the web server supports HTTP/2 (Open Chrome dev tools and look for "h2" in the Protocol column), then you can make an unlimited number of concurrent requests (and can set `maxRequests=-1`). Note that this will request data for every tile, no matter how long the tile was visible, and may increase server load. diff --git a/modules/geo-layers/src/index.ts b/modules/geo-layers/src/index.ts index c4c3ea40d79..e9b8fcaba2d 100644 --- a/modules/geo-layers/src/index.ts +++ b/modules/geo-layers/src/index.ts @@ -40,7 +40,6 @@ export type {GeohashLayerProps} from './geohash-layer/geohash-layer'; export type {GeoBoundingBox, NonGeoBoundingBox} from './tileset-2d/index'; export type {TileLoadProps as _TileLoadProps} from './tileset-2d/index'; -export type {TilePriorityFunction as _TilePriorityFunction} from './tileset-2d/index'; export type {Tileset2DProps as _Tileset2DProps} from './tileset-2d/index'; export {getURLFromTemplate as _getURLFromTemplate} from './tileset-2d/index'; diff --git a/modules/geo-layers/src/tile-layer/tile-layer.ts b/modules/geo-layers/src/tile-layer/tile-layer.ts index 6791d530bbb..de5e6b3a8fb 100644 --- a/modules/geo-layers/src/tile-layer/tile-layer.ts +++ b/modules/geo-layers/src/tile-layer/tile-layer.ts @@ -17,7 +17,7 @@ import { import {GeoJsonLayer} from '@deck.gl/layers'; import {LayersList} from '@deck.gl/core'; -import type {TileLoadProps, TilePriorityFunction, ZRange} from '../tileset-2d/index'; +import type {TileLoadProps, ZRange} from '../tileset-2d/index'; import { Tileset2D, Tile2DHeader, @@ -34,7 +34,6 @@ const defaultProps: DefaultProps = { dataComparator: urlType.equal, renderSubLayers: {type: 'function', value: (props: any) => new GeoJsonLayer(props)}, getTileData: {type: 'function', optional: true, value: null}, - getPriority: {type: 'function', optional: true, value: null}, // TODO - change to onViewportLoad to align with Tile3DLayer onViewportLoad: {type: 'function', optional: true, value: null}, onTileLoad: {type: 'function', value: tile => {}}, @@ -82,15 +81,6 @@ type _TileLayerProps = { */ getTileData?: ((props: TileLoadProps) => Promise | DataT) | null; - /** - * Returns the request priority for queued tiles. Lower non-negative values load first; - * values below 0 cancel the queued request. If not supplied, tiles closer to the viewport - * center are requested first. - * - * @default null - */ - getPriority?: TilePriorityFunction | null; - /** Called when all tiles in the current viewport are loaded. */ onViewportLoad?: ((tiles: Tile2DHeader[]) => void) | null; @@ -271,7 +261,6 @@ export default class TileLayer extends maxCacheSize, maxCacheByteSize, refinementStrategy, - getPriority, extent, maxZoom, minZoom, @@ -289,7 +278,6 @@ export default class TileLayer extends minZoom, tileSize, refinementStrategy, - getPriority, extent, maxRequests, debounceTime, diff --git a/modules/geo-layers/src/tileset-2d/index.ts b/modules/geo-layers/src/tileset-2d/index.ts index 5fec2c6c077..d6ed77d3ccc 100644 --- a/modules/geo-layers/src/tileset-2d/index.ts +++ b/modules/geo-layers/src/tileset-2d/index.ts @@ -13,7 +13,7 @@ export type { TileBoundingBox } from './types'; -export type {Tileset2DProps, RefinementStrategy, TilePriorityFunction} from './tileset-2d'; +export type {Tileset2DProps, RefinementStrategy} from './tileset-2d'; export {Tileset2D, STRATEGY_DEFAULT} from './tileset-2d'; export {Tile2DHeader} from './tile-2d-header'; 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..82dc182d100 100644 --- a/modules/geo-layers/src/tileset-2d/tile-2d-header.ts +++ b/modules/geo-layers/src/tileset-2d/tile-2d-header.ts @@ -10,7 +10,7 @@ import type {Layer} from '@deck.gl/core'; export type TileLoadDataProps = { requestScheduler: RequestScheduler; getData: (props: TileLoadProps) => Promise; - getPriority: (tile: Tile2DHeader) => number; + getRequestPriority: (tile: Tile2DHeader) => number; onLoad: (tile: Tile2DHeader) => void; onError: (error: any, tile: Tile2DHeader) => void; }; @@ -107,7 +107,7 @@ export class Tile2DHeader { /* eslint-disable max-statements */ private async _loadData({ getData, - getPriority, + getRequestPriority, requestScheduler, onLoad, onError @@ -119,7 +119,7 @@ export class Tile2DHeader { const {signal} = this._abortController; // @ts-expect-error (2345) loaders.gl's RequestScheduler callback type is too narrow. - const requestToken = await requestScheduler.scheduleRequest(this, getPriority); + const requestToken = await requestScheduler.scheduleRequest(this, getRequestPriority); 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 f0e25ac5cf4..d74059b0d56 100644 --- a/modules/geo-layers/src/tileset-2d/tileset-2d.ts +++ b/modules/geo-layers/src/tileset-2d/tileset-2d.ts @@ -52,9 +52,6 @@ const SELECTED_TILE_PRIORITY = 0; const VISIBLE_TILE_PRIORITY = 1e8; const MAX_TILE_DISTANCE_PRIORITY = VISIBLE_TILE_PRIORITY - SELECTED_TILE_PRIORITY - 1; -/** Returns the request priority for a tile. Lower non-negative values load first. */ -export type TilePriorityFunction = (tile: Tile2DHeader) => number; - const STRATEGIES = { [STRATEGY_DEFAULT]: updateTileStateDefault, [STRATEGY_REPLACE]: updateTileStateReplace, @@ -79,13 +76,6 @@ export type Tileset2DProps = { maxCacheByteSize?: number | null; /** How the tile layer refines the visibility of tiles. @default 'best-available' */ refinementStrategy?: RefinementStrategy; - /** - * Returns the request priority for queued tiles. Lower non-negative values load first; - * values below 0 cancel the queued request. If not supplied, tiles closer to the viewport - * center are requested first. - * @default null - */ - getPriority?: TilePriorityFunction | null; /** Range of minimum and maximum heights in the tile. */ zRange?: ZRange | null; /** The maximum number of concurrent getTileData calls. @default 6 */ @@ -121,7 +111,6 @@ export const DEFAULT_TILESET2D_PROPS: Omit, 'getTileDat maxCacheSize: null, maxCacheByteSize: null, refinementStrategy: 'best-available', - getPriority: null, zRange: null, maxRequests: 6, debounceTime: 0, @@ -455,9 +444,6 @@ export class Tileset2D { if (!tile.isSelected && !tile.isVisible) { return -1; } - if (this.opts.getPriority) { - return this.opts.getPriority(tile); - } // RequestScheduler loads lower priority values first. const distance = this._getTileDistancePriority(tile); @@ -655,7 +641,7 @@ export class Tileset2D { // eslint-disable-next-line @typescript-eslint/no-floating-promises tile.loadData({ getData: this.opts.getTileData, - getPriority: this._getRequestPriority.bind(this), + getRequestPriority: 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 dd01f4057da..c804724af37 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 getRequestPriority = tile => (tile.isSelected ? 1 : -1); test('Tile2DHeader', async () => { let onTileLoadCalled = false; @@ -16,7 +16,7 @@ test('Tile2DHeader', async () => { let tile2d = new Tile2DHeader({}); await tile2d.loadData({ requestScheduler, - getPriority, + getRequestPriority, getData: () => 'loaded data', onLoad: () => (onTileLoadCalled = true), onError: () => (onTileErrorCalled = true) @@ -29,7 +29,7 @@ test('Tile2DHeader', async () => { tile2d = new Tile2DHeader({}); await tile2d.loadData({ requestScheduler, - getPriority, + getRequestPriority, getData: () => { throw new Error('getTileData error'); }, @@ -48,7 +48,7 @@ test('Tile2DHeader#Cancel request if not selected', async () => { const requestScheduler = new RequestScheduler({throttleRequests: true, maxRequests: 1}); const opts = { requestScheduler, - getPriority, + getRequestPriority, getData: () => tileRequestCount++, onLoad: () => onTileLoadCalled++, onError: () => onTileErrorCalled++ @@ -76,7 +76,7 @@ test('Tile2DHeader#request priority', async () => { const requestScheduler = new RequestScheduler({throttleRequests: true, maxRequests: 1}); const opts = { requestScheduler, - getPriority: tile => (tile.id === 'preferred' ? 0 : 10), + getRequestPriority: tile => (tile.id === 'preferred' ? 0 : 10), getData: ({id}) => { requestOrder.push(id); return id; @@ -107,7 +107,7 @@ test('Tile2DHeader#abort', async () => { const opts = { requestScheduler, - getPriority, + getRequestPriority, getData: () => null, onLoad: () => (onTileLoadCalled = true), onError: () => (onTileErrorCalled = true) @@ -139,7 +139,7 @@ test('Tile2DHeader#reload', async () => { let onTileErrorCalled = 0; const opts = { requestScheduler, - getPriority, + getRequestPriority, onLoad: () => onTileLoadCalled++, onError: () => onTileErrorCalled++ }; 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 9631b2770f1..74ece2ced84 100644 --- a/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts +++ b/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts @@ -146,49 +146,6 @@ test('Tileset2D#getRequestPriority keeps unprojectable tiles within priority tie tileset.finalize(); }); -test('Tileset2D#getRequestPriority uses custom priority for relevant tiles', () => { - const tileset = new Tileset2D({ - getTileData, - getPriority: tile => (tile.id === 'edge' ? 0 : 10), - onTileLoad: () => {} - }); - Object.assign(tileset, { - _viewport: { - width: 100, - height: 100, - project: ([x, y]) => [x, y] - } - }); - - const selectedAtCenter = { - id: 'center', - bbox: {left: 0, top: 0, right: 100, bottom: 100}, - index: {x: 0, y: 0, z: 10}, - isSelected: true, - isVisible: true - }; - const selectedAtEdge = { - id: 'edge', - bbox: {left: 70, top: 70, right: 80, bottom: 80}, - index: {x: 1, y: 0, z: 10}, - isSelected: true, - isVisible: true - }; - const staleAtEdge = { - id: 'edge', - bbox: {left: 70, top: 70, right: 80, bottom: 80}, - index: {x: 2, y: 0, z: 10}, - isSelected: false, - isVisible: false - }; - - expect((tileset as any)._getRequestPriority(selectedAtEdge)).toBe(0); - expect((tileset as any)._getRequestPriority(selectedAtCenter)).toBe(10); - expect((tileset as any)._getRequestPriority(staleAtEdge)).toBeLessThan(0); - - tileset.finalize(); -}); - test('Tileset2D#updateOnModelMatrix', () => { const tileset = new Tileset2D({ getTileData, From 9ecf7e2d7eab085160969f697c24b2faba79b935 Mon Sep 17 00:00:00 2001 From: Charles Richardson Date: Thu, 11 Jun 2026 18:58:30 -0400 Subject: [PATCH 3/3] Address tile priority review comments --- docs/api-reference/geo-layers/tile-layer.md | 2 +- test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api-reference/geo-layers/tile-layer.md b/docs/api-reference/geo-layers/tile-layer.md index 3016365a4fd..bc6f53f314c 100644 --- a/docs/api-reference/geo-layers/tile-layer.md +++ b/docs/api-reference/geo-layers/tile-layer.md @@ -325,7 +325,7 @@ If `<= 0`, no throttling will occur, and `getTileData` may be called an unlimite If `> 0`, a maximum of `maxRequests` instances of `getTileData` will be called concurrently. Requests may never be called if the tile wasn't visible long enough to be scheduled and started. Requests may also be aborted (through the `signal` passed to `getTileData`) if there are more than `maxRequests` ongoing requests and some of those are for tiles that are no longer visible. -When requests are queued, selected tiles are scheduled before visible placeholder tiles. Within each group, tiles closer to the viewport center are scheduled first. +When requests are queued, tiles closer to the viewport center are scheduled first. If `getTileData` makes `fetch` requests against an HTTP 1 web server, then `maxRequests` should correlate to the browser's maximum number of concurrent `fetch` requests. For Chrome, the max is 6 per domain. If you use the `data` prop and specify multiple domains, you can increase this limit. For example, with Chrome and 3 domains specified, you can set `maxRequests=18`. 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 74ece2ced84..e5b86e81768 100644 --- a/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts +++ b/test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts @@ -55,7 +55,7 @@ test('Tileset2D#update', () => { expect(tileset.tiles[0].bbox, 'tile has metadata').toBeTruthy(); }); -test('Tileset2D#getRequestPriority ranks tiles by viewport coverage', () => { +test('Tileset2D#getRequestPriority ranks tiles by viewport center distance', () => { const tileset = new Tileset2D({ getTileData, onTileLoad: () => {}