maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
311 lines (277 loc) • 15.3 kB
text/typescript
import {EXTENT} from '../../data/extent';
import {projectTileCoordinatesToSphere} from './globe_utils';
import {BoundingVolumeCache} from '../../util/primitives/bounding_volume_cache';
import {coveringZoomLevel, type CoveringTilesOptions} from './covering_tiles';
import {vec3, type vec4} from 'gl-matrix';
import type {IReadonlyTransform} from '../transform_interface';
import type {MercatorCoordinate} from '../mercator_coordinate';
import type {CoveringTilesDetailsProvider} from './covering_tiles_details_provider';
import {OverscaledTileID} from '../../source/tile_id';
import {earthRadius} from '../lng_lat';
import {ConvexVolume} from '../../util/primitives/convex_volume';
import {threePlaneIntersection} from '../../util/util';
/**
* Computes distance of a point to a tile in an arbitrary axis.
* World is assumed to have size 1, distance returned is to the nearer tile edge.
* @param point - Point position.
* @param tile - Tile position.
* @param tileSize - Tile size.
*/
function distanceToTileSimple(point: number, tile: number, tileSize: number): number {
const delta = point - tile;
return (delta < 0) ? -delta : Math.max(0, delta - tileSize);
}
function distanceToTileWrapX(pointX: number, pointY: number, tileCornerX: number, tileCornerY: number, tileSize: number): number {
const tileCornerToPointX = pointX - tileCornerX;
let distanceX: number;
if (tileCornerToPointX < 0) {
// Point is left of tile
distanceX = Math.min(-tileCornerToPointX, 1.0 + tileCornerToPointX - tileSize);
} else if (tileCornerToPointX > 1) {
// Point is right of tile
distanceX = Math.min(Math.max(tileCornerToPointX - tileSize, 0), 1.0 - tileCornerToPointX);
} else {
// Point is inside tile in the X axis.
distanceX = 0;
}
return Math.max(distanceX, distanceToTileSimple(pointY, tileCornerY, tileSize));
}
export class GlobeCoveringTilesDetailsProvider implements CoveringTilesDetailsProvider {
private _boundingVolumeCache: BoundingVolumeCache<ConvexVolume> = new BoundingVolumeCache(this._computeTileBoundingVolume);
/**
* Prepares the internal bounding volume cache for the next frame.
*/
prepareNextFrame() {
this._boundingVolumeCache.swapBuffers();
}
/**
* Returns the distance of a point to a square tile. If the point is inside the tile, returns 0.
* Assumes the world to be of size 1.
* Handles distances on a sphere correctly: X is wrapped when crossing the antimeridian,
* when crossing the poles Y is mirrored and X is shifted by half world size.
*/
distanceToTile2d(pointX: number, pointY: number, tileID: {x: number; y: number; z: number}, _bv: ConvexVolume): number {
const scale = 1 << tileID.z;
const tileMercatorSize = 1.0 / scale;
const tileCornerX = tileID.x / scale; // In range 0..1
const tileCornerY = tileID.y / scale; // In range 0..1
const worldSize = 1.0;
const halfWorld = 0.5 * worldSize;
let smallestDistance = 2.0 * worldSize;
// Original tile
smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX, tileCornerY, tileMercatorSize));
// Up
smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX + halfWorld, -tileCornerY - tileMercatorSize, tileMercatorSize));
// Down
smallestDistance = Math.min(smallestDistance, distanceToTileWrapX(pointX, pointY, tileCornerX + halfWorld, worldSize + worldSize - tileCornerY - tileMercatorSize, tileMercatorSize));
return smallestDistance;
}
/**
* Returns the wrap value for a given tile, computed so that tiles will remain loaded when crossing the antimeridian.
*/
getWrap(centerCoord: MercatorCoordinate, tileID: {x: number; y: number; z: number}, _parentWrap: number): number {
const scale = 1 << tileID.z;
const tileMercatorSize = 1.0 / scale;
const tileX = tileID.x / scale; // In range 0..1
const distanceCurrent = distanceToTileSimple(centerCoord.x, tileX, tileMercatorSize);
const distanceLeft = distanceToTileSimple(centerCoord.x, tileX - 1.0, tileMercatorSize);
const distanceRight = distanceToTileSimple(centerCoord.x, tileX + 1.0, tileMercatorSize);
const distanceSmallest = Math.min(distanceCurrent, distanceLeft, distanceRight);
if (distanceSmallest === distanceRight) {
return 1;
}
if (distanceSmallest === distanceLeft) {
return -1;
}
return 0;
}
allowVariableZoom(transform: IReadonlyTransform, options: CoveringTilesOptions): boolean {
return coveringZoomLevel(transform, options) > 4;
}
allowWorldCopies(): boolean {
return false;
}
getTileBoundingVolume(tileID: { x: number; y: number; z: number }, wrap: number, elevation: number, options: CoveringTilesOptions) {
return this._boundingVolumeCache.getTileBoundingVolume(tileID, wrap, elevation, options);
}
private _computeTileBoundingVolume(tileID: {x: number; y: number; z: number}, wrap: number, elevation: number, options: CoveringTilesOptions): ConvexVolume {
let minElevation = elevation;
let maxElevation = elevation;
if (options?.terrain) {
const overscaledTileID = new OverscaledTileID(tileID.z, wrap, tileID.z, tileID.x, tileID.y);
const minMax = options.terrain.getMinMaxElevation(overscaledTileID);
minElevation = minMax.minElevation ?? elevation;
maxElevation = minMax.maxElevation ?? elevation;
}
// Convert elevation to distances from center of a unit sphere planet (so that 1 is surface)
minElevation /= earthRadius;
maxElevation /= earthRadius;
minElevation += 1;
maxElevation += 1;
if (tileID.z <= 0) {
// Tile covers the entire sphere.
return ConvexVolume.fromAabb( // We return an AABB in this case.
[-maxElevation, -maxElevation, -maxElevation],
[maxElevation, maxElevation, maxElevation]
);
} else if (tileID.z === 1) {
// Tile covers a quarter of the sphere.
// X is 1 at lng=E90°
// Y is 1 at **north** pole
// Z is 1 at null island
return ConvexVolume.fromAabb( // We also just use AABBs for this zoom level.
[tileID.x === 0 ? -maxElevation : 0, tileID.y === 0 ? 0 : -maxElevation, -maxElevation],
[tileID.x === 0 ? 0 : maxElevation, tileID.y === 0 ? maxElevation : 0, maxElevation]
);
} else {
const corners = [
projectTileCoordinatesToSphere(0, 0, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(EXTENT, 0, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(EXTENT, EXTENT, tileID.x, tileID.y, tileID.z),
projectTileCoordinatesToSphere(0, EXTENT, tileID.x, tileID.y, tileID.z),
];
const extremesPoints = [];
for (const c of corners) {
extremesPoints.push(vec3.scale([] as any, c, maxElevation));
}
if (maxElevation !== minElevation) {
// Only add additional points if terrain is enabled and is not flat.
for (const c of corners) {
extremesPoints.push(vec3.scale([] as any, c, minElevation));
}
}
// Special handling of poles - we need to extend the tile AABB
// to include the pole for tiles that border mercator north/south edge.
if (tileID.y === 0) {
extremesPoints.push([0, 1, 0]); // North pole
}
if (tileID.y === (1 << tileID.z) - 1) {
extremesPoints.push([0, -1, 0]); // South pole
}
// Compute a best-fit AABB for the frustum rejection test
const aabbMin: vec3 = [1, 1, 1];
const aabbMax: vec3 = [-1, -1, -1];
for (const c of extremesPoints) {
for (let i = 0; i < 3; i++) {
aabbMin[i] = Math.min(aabbMin[i], c[i]);
aabbMax[i] = Math.max(aabbMax[i], c[i]);
}
}
// Now we compute the actual bounding volume.
// The up/down plane will be normal to the tile's center.
// The north/south plane will be used for the tile's north and south edge and will be orthogonal to the up/down plane.
// The left and right planes will be determined by the tile's east/west edges and will differ slightly - we are not creating a box!
// We will find the min and max extents for the up/down and north/south planes using the set of points
// where the extremes are likely to lie.
// Vector "center" (from planet center to tile center) will be our up/down axis.
const center = projectTileCoordinatesToSphere(EXTENT / 2, EXTENT / 2, tileID.x, tileID.y, tileID.z);
// Vector to the east of "center".
const centerEast = vec3.cross([] as any, [0, 1, 0], center);
vec3.normalize(centerEast, centerEast);
// Vector to the north of "center" will be our north/south axis.
const north = vec3.cross([] as any, center, centerEast);
vec3.normalize(north, north);
// Axes for the east and west edge of our bounding volume.
// These axes are NOT opposites of each other, they differ!
// They are also not orthogonal to the up/down and north/south axes.
const axisEast = vec3.cross([] as any, corners[2], corners[1]);
vec3.normalize(axisEast, axisEast);
const axisWest = vec3.cross([] as any, corners[0], corners[3]);
vec3.normalize(axisWest, axisWest);
// Now we will expand the extremes point set for bounding volume creation.
// We will also include the tile center point, since it will always be an extreme for the "center" axis.
extremesPoints.push(vec3.scale([] as any, center, maxElevation));
// No need to include a minElevation-scaled center, since we already have minElevation corners in the set and these will always lie lower than the center.
// The extremes might also lie on the midpoint of the north or south edge.
// For tiles in the north hemisphere, only the south edge can contain an extreme,
// since when we imagine the tile's actual shape projected onto the plane normal to "center" vector,
// the tile's north edge will curve towards the tile center, thus its extremes are accounted for by the
// corners, however the south edge will curve away from the center point, extending beyond the tile's edges,
// thus it must be included.
// The poles are an exception - they must always be included in the extremes, if the tile touches the north/south mercator range edge.
//
// A tile's exaggerated shape on the northern hemisphere, projected onto the normal plane of "center".
// The "c" is the tile's center point. The "m" is the edge mid point we are looking for.
//
// /-- --\
// / ------- \
// / \
// / c \
// / \
// /-- --\
// ----- -----
// ---m---
if (tileID.y >= (1 << tileID.z) / 2) {
// South hemisphere - include the tile's north edge midpoint
extremesPoints.push(vec3.scale([] as any, projectTileCoordinatesToSphere(EXTENT / 2, 0, tileID.x, tileID.y, tileID.z), maxElevation));
// No need to include minElevation variant of this point, for the same reason why we don't include minElevation center.
}
if (tileID.y < (1 << tileID.z) / 2) {
// North hemisphere - include the tile's south edge midpoint
extremesPoints.push(vec3.scale([] as any, projectTileCoordinatesToSphere(EXTENT / 2, EXTENT, tileID.x, tileID.y, tileID.z), maxElevation));
// No need to include minElevation variant of this point, for the same reason why we don't include minElevation center.
}
// Find the min and max extends and the midpoints along each axis,
// using the set of extreme points.
const upDownMinMax = findAxisMinMax(center, extremesPoints);
const northSouthMinMax = findAxisMinMax(north, extremesPoints);
const planeUp = [-center[0], -center[1], -center[2], upDownMinMax.max] as vec4;
const planeDown = [center[0], center[1], center[2], -upDownMinMax.min] as vec4;
const planeNorth = [-north[0], -north[1], -north[2], northSouthMinMax.max] as vec4;
const planeSouth = [north[0], north[1], north[2], -northSouthMinMax.min] as vec4;
const planeEast = [...axisEast, 0] as vec4;
const planeWest = [...axisWest, 0] as vec4;
const points: vec3[] = [];
// North points
if (tileID.y === 0) {
// If the tile borders a pole, then
points.push(
threePlaneIntersection(planeWest, planeEast, planeUp),
threePlaneIntersection(planeWest, planeEast, planeDown),
);
} else {
points.push(
threePlaneIntersection(planeNorth, planeEast, planeUp),
threePlaneIntersection(planeNorth, planeEast, planeDown),
threePlaneIntersection(planeNorth, planeWest, planeUp),
threePlaneIntersection(planeNorth, planeWest, planeDown)
);
}
// South points
if (tileID.y === (1 << tileID.z) - 1) {
points.push(
threePlaneIntersection(planeWest, planeEast, planeUp),
threePlaneIntersection(planeWest, planeEast, planeDown),
);
} else {
points.push(
threePlaneIntersection(planeSouth, planeEast, planeUp),
threePlaneIntersection(planeSouth, planeEast, planeDown),
threePlaneIntersection(planeSouth, planeWest, planeUp),
threePlaneIntersection(planeSouth, planeWest, planeDown)
);
}
return new ConvexVolume(points, [
planeUp,
planeDown,
planeNorth,
planeSouth,
planeEast,
planeWest
], aabbMin, aabbMax);
}
}
}
function findAxisMinMax(axis: vec3, points: vec3[]) {
let min = +Infinity;
let max = -Infinity;
for (const c of points) {
const dot = vec3.dot(axis, c);
min = Math.min(min, dot);
max = Math.max(max, dot);
}
return {
min,
max
};
}