@loaders.gl/tiles
Version:
Common components for different tiles loaders.
129 lines (128 loc) • 6.02 kB
JavaScript
import { Vector3 } from '@math.gl/core';
import { CullingVolume, Plane } from '@math.gl/culling';
import { Ellipsoid } from '@math.gl/geospatial';
const scratchVector = new Vector3();
const scratchPosition = new Vector3();
const cullingVolume = new CullingVolume([
new Plane(),
new Plane(),
new Plane(),
new Plane(),
new Plane(),
new Plane()
]);
// Extracts a frame state appropriate for tile culling from a deck.gl viewport
// TODO - this could likely be generalized and merged back into deck.gl for other culling scenarios
export function getFrameState(viewport, frameNumber) {
// Traverse and and request. Update _selectedTiles so that we know what to render.
// Traverse and and request. Update _selectedTiles so that we know what to render.
const { cameraDirection, cameraUp, height } = viewport;
const { metersPerUnit } = viewport.distanceScales;
// TODO - Ellipsoid.eastNorthUpToFixedFrame() breaks on raw array, create a Vector.
// TODO - Ellipsoid.eastNorthUpToFixedFrame() takes a cartesian, is that intuitive?
const viewportCenterCartesian = worldToCartesian(viewport, viewport.center);
const enuToFixedTransform = Ellipsoid.WGS84.eastNorthUpToFixedFrame(viewportCenterCartesian);
const cameraPositionCartographic = viewport.unprojectPosition(viewport.cameraPosition);
const cameraPositionCartesian = Ellipsoid.WGS84.cartographicToCartesian(cameraPositionCartographic, new Vector3());
// These should still be normalized as the transform has scale 1 (goes from meters to meters)
const cameraDirectionCartesian = new Vector3(
// @ts-ignore
enuToFixedTransform.transformAsVector(new Vector3(cameraDirection).scale(metersPerUnit))).normalize();
const cameraUpCartesian = new Vector3(
// @ts-ignore
enuToFixedTransform.transformAsVector(new Vector3(cameraUp).scale(metersPerUnit))).normalize();
commonSpacePlanesToWGS84(viewport);
const ViewportClass = viewport.constructor;
const { longitude, latitude, width, bearing, zoom } = viewport;
// @ts-ignore
const topDownViewport = new ViewportClass({
longitude,
latitude,
height,
width,
bearing,
zoom,
pitch: 0
});
// TODO: make a file/class for frameState and document what needs to be attached to this so that traversal can function
return {
camera: {
position: cameraPositionCartesian,
direction: cameraDirectionCartesian,
up: cameraUpCartesian
},
viewport,
topDownViewport,
height,
cullingVolume,
frameNumber, // TODO: This can be the same between updates, what number is unique for between updates?
sseDenominator: 1.15 // Assumes fovy = 60 degrees
};
}
/**
* Limit `tiles` array length with `maximumTilesSelected` number.
* The criteria for this filtering is distance of a tile center
* to the `frameState.viewport`'s longitude and latitude
* @param tiles - tiles array to filter
* @param frameState - frameState to calculate distances
* @param maximumTilesSelected - maximal amount of tiles in the output array
* @returns new tiles array
*/
export function limitSelectedTiles(tiles, frameState, maximumTilesSelected) {
if (maximumTilesSelected === 0 || tiles.length <= maximumTilesSelected) {
return [tiles, []];
}
// Accumulate distances in couples array: [tileIndex: number, distanceToViewport: number]
const tuples = [];
const { longitude: viewportLongitude, latitude: viewportLatitude } = frameState.viewport;
for (const [index, tile] of tiles.entries()) {
const [longitude, latitude] = tile.header.mbs;
const deltaLon = Math.abs(viewportLongitude - longitude);
const deltaLat = Math.abs(viewportLatitude - latitude);
const distance = Math.sqrt(deltaLat * deltaLat + deltaLon * deltaLon);
tuples.push([index, distance]);
}
const tuplesSorted = tuples.sort((a, b) => a[1] - b[1]);
const selectedTiles = [];
for (let i = 0; i < maximumTilesSelected; i++) {
selectedTiles.push(tiles[tuplesSorted[i][0]]);
}
const unselectedTiles = [];
for (let i = maximumTilesSelected; i < tuplesSorted.length; i++) {
unselectedTiles.push(tiles[tuplesSorted[i][0]]);
}
return [selectedTiles, unselectedTiles];
}
function commonSpacePlanesToWGS84(viewport) {
// Extract frustum planes based on current view.
const frustumPlanes = viewport.getFrustumPlanes();
// Get the near/far plane centers
const nearCenterCommon = closestPointOnPlane(frustumPlanes.near, viewport.cameraPosition);
const nearCenterCartesian = worldToCartesian(viewport, nearCenterCommon);
const cameraCartesian = worldToCartesian(viewport, viewport.cameraPosition, scratchPosition);
let i = 0;
cullingVolume.planes[i++].fromPointNormal(nearCenterCartesian, scratchVector.copy(nearCenterCartesian).subtract(cameraCartesian));
for (const dir in frustumPlanes) {
if (dir === 'near') {
continue; // eslint-disable-line no-continue
}
const plane = frustumPlanes[dir];
const posCommon = closestPointOnPlane(plane, nearCenterCommon, scratchPosition);
const cartesianPos = worldToCartesian(viewport, posCommon, scratchPosition);
cullingVolume.planes[i++].fromPointNormal(cartesianPos,
// Want the normal to point into the frustum since that's what culling expects
scratchVector.copy(nearCenterCartesian).subtract(cartesianPos));
}
}
function closestPointOnPlane(plane, refPoint, out = new Vector3()) {
const distanceToRef = plane.normal.dot(refPoint);
out
.copy(plane.normal)
.scale(plane.distance - distanceToRef)
.add(refPoint);
return out;
}
function worldToCartesian(viewport, point, out = new Vector3()) {
const cartographicPos = viewport.unprojectPosition(point);
return Ellipsoid.WGS84.cartographicToCartesian(cartographicPos, out);
}