Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/api-reference/geo-layers/tile-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -325,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, 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.
Expand Down
8 changes: 4 additions & 4 deletions modules/geo-layers/src/tileset-2d/tile-2d-header.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {Layer} from '@deck.gl/core';
export type TileLoadDataProps<DataT = any> = {
requestScheduler: RequestScheduler;
getData: (props: TileLoadProps) => Promise<DataT>;
getRequestPriority: (tile: Tile2DHeader<DataT>) => number;
onLoad: (tile: Tile2DHeader<DataT>) => void;
onError: (error: any, tile: Tile2DHeader<DataT>) => void;
};
Expand Down Expand Up @@ -106,6 +107,7 @@ export class Tile2DHeader<DataT = any> {
/* eslint-disable max-statements */
private async _loadData({
getData,
getRequestPriority,
requestScheduler,
onLoad,
onError
Expand All @@ -116,10 +118,8 @@ export class Tile2DHeader<DataT = any> {
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, getRequestPriority);

if (!requestToken) {
this._isCancelled = true;
Expand Down
102 changes: 101 additions & 1 deletion modules/geo-layers/src/tileset-2d/tileset-2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -48,6 +48,9 @@ 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;

const STRATEGIES = {
[STRATEGY_DEFAULT]: updateTileStateDefault,
Expand Down Expand Up @@ -437,6 +440,102 @@ export class Tileset2D {

private _getCullBounds = memoize(getCullBounds);

private _getRequestPriority(tile: Tile2DHeader): number {
if (!tile.isSelected && !tile.isVisible) {
return -1;
}

// 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;

Expand Down Expand Up @@ -542,6 +641,7 @@ export class Tileset2D {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
tile.loadData({
getData: this.opts.getTileData,
getRequestPriority: this._getRequestPriority.bind(this),
requestScheduler: this._requestScheduler,
onLoad: this.onTileLoad,
onError: this.opts.onTileError
Expand Down
36 changes: 36 additions & 0 deletions test/modules/geo-layers/tileset-2d/tile-2d-header.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 getRequestPriority = tile => (tile.isSelected ? 1 : -1);

test('Tile2DHeader', async () => {
let onTileLoadCalled = false;
let onTileErrorCalled = false;
Expand All @@ -14,6 +16,7 @@ test('Tile2DHeader', async () => {
let tile2d = new Tile2DHeader({});
await tile2d.loadData({
requestScheduler,
getRequestPriority,
getData: () => 'loaded data',
onLoad: () => (onTileLoadCalled = true),
onError: () => (onTileErrorCalled = true)
Expand All @@ -26,6 +29,7 @@ test('Tile2DHeader', async () => {
tile2d = new Tile2DHeader({});
await tile2d.loadData({
requestScheduler,
getRequestPriority,
getData: () => {
throw new Error('getTileData error');
},
Expand All @@ -44,6 +48,7 @@ test('Tile2DHeader#Cancel request if not selected', async () => {
const requestScheduler = new RequestScheduler({throttleRequests: true, maxRequests: 1});
const opts = {
requestScheduler,
getRequestPriority,
getData: () => tileRequestCount++,
onLoad: () => onTileLoadCalled++,
onError: () => onTileErrorCalled++
Expand All @@ -66,13 +71,43 @@ 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,
getRequestPriority: 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;
let onTileErrorCalled = false;

const opts = {
requestScheduler,
getRequestPriority,
getData: () => null,
onLoad: () => (onTileLoadCalled = true),
onError: () => (onTileErrorCalled = true)
Expand Down Expand Up @@ -104,6 +139,7 @@ test('Tile2DHeader#reload', async () => {
let onTileErrorCalled = 0;
const opts = {
requestScheduler,
getRequestPriority,
onLoad: () => onTileLoadCalled++,
onError: () => onTileErrorCalled++
};
Expand Down
91 changes: 91 additions & 0 deletions test/modules/geo-layers/tileset-2d/tileset-2d.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,97 @@ test('Tileset2D#update', () => {
expect(tileset.tiles[0].bbox, 'tile has metadata').toBeTruthy();
});

test('Tileset2D#getRequestPriority ranks tiles by viewport center distance', () => {
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#updateOnModelMatrix', () => {
const tileset = new Tileset2D({
getTileData,
Expand Down
Loading