mapbox-gl
Version:
A WebGL interactive maps library
562 lines (464 loc) • 22.2 kB
JavaScript
// @flow
import {
lngFromMercatorX,
latFromMercatorY,
mercatorZfromAltitude,
mercatorXfromLng,
mercatorYfromLat,
MAX_MERCATOR_LATITUDE
} from '../mercator_coordinate.js';
import EXTENT from '../../data/extent.js';
import {number as interpolate} from '../../style-spec/util/interpolate.js';
import {degToRad, smoothstep, clamp} from '../../util/util.js';
import {mat4, vec3} from 'gl-matrix';
import SegmentVector from '../../data/segment.js';
import {members as globeLayoutAttributes, atmosphereLayout} from '../../terrain/globe_attributes.js';
import posAttributes from '../../data/pos_attributes.js';
import {TriangleIndexArray, GlobeVertexArray, GlobeAtmosphereVertexArray, LineIndexArray, PosArray} from '../../data/array_types.js';
import {Aabb} from '../../util/primitives.js';
import LngLatBounds from '../lng_lat_bounds.js';
import type {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id.js';
import type Context from '../../gl/context.js';
import type {Mat4, Vec3} from 'gl-matrix';
import type IndexBuffer from '../../gl/index_buffer.js';
import type VertexBuffer from '../../gl/vertex_buffer.js';
import type Transform from '../transform.js';
export const GLOBE_RADIUS = EXTENT / Math.PI / 2.0;
const GLOBE_NORMALIZATION_BIT_RANGE = 15;
const GLOBE_NORMALIZATION_MASK = (1 << (GLOBE_NORMALIZATION_BIT_RANGE - 1)) - 1;
const GLOBE_VERTEX_GRID_SIZE = 64;
const TILE_SIZE = 512;
const GLOBE_MIN = -GLOBE_RADIUS;
const GLOBE_MAX = GLOBE_RADIUS;
const GLOBE_LOW_ZOOM_TILE_AABBS = [
// z == 0
new Aabb([GLOBE_MIN, GLOBE_MIN, GLOBE_MIN], [GLOBE_MAX, GLOBE_MAX, GLOBE_MAX]),
// z == 1
new Aabb([GLOBE_MIN, GLOBE_MIN, GLOBE_MIN], [0, 0, GLOBE_MAX]), // x=0, y=0
new Aabb([0, GLOBE_MIN, GLOBE_MIN], [GLOBE_MAX, 0, GLOBE_MAX]), // x=1, y=0
new Aabb([GLOBE_MIN, 0, GLOBE_MIN], [0, GLOBE_MAX, GLOBE_MAX]), // x=0, y=1
new Aabb([0, 0, GLOBE_MIN], [GLOBE_MAX, GLOBE_MAX, GLOBE_MAX]) // x=1, y=1
];
export class Arc {
constructor(p0: Vec3, p1: Vec3, center: Vec3) {
this.a = vec3.sub([], p0, center);
this.b = vec3.sub([], p1, center);
this.center = center;
const an = vec3.normalize([], this.a);
const bn = vec3.normalize([], this.b);
this.angle = Math.acos(vec3.dot(an, bn));
}
a: Vec3;
b: Vec3;
center: Vec3;
angle: number;
}
export function slerp(a: number, b: number, angle: number, t: number): number {
const sina = Math.sin(angle);
return a * (Math.sin((1.0 - t) * angle) / sina) + b * (Math.sin(t * angle) / sina);
}
// Computes local extremum point of an arc on one of the dimensions (x, y or z),
// i.e. value of a point where d/dt*f(x,y,t) == 0
export function localExtremum(arc: Arc, dim: number): ?number {
// d/dt*slerp(x,y,t) = 0
// => t = (1/a)*atan(y/(x*sin(a))-1/tan(a)), x > 0
// => t = (1/a)*(pi/2), x == 0
if (arc.angle === 0) {
return null;
}
let t: number;
if (arc.a[dim] === 0) {
t = (1.0 / arc.angle) * 0.5 * Math.PI;
} else {
t = 1.0 / arc.angle * Math.atan(arc.b[dim] / arc.a[dim] / Math.sin(arc.angle) - 1.0 / Math.tan(arc.angle));
}
if (t < 0 || t > 1) {
return null;
}
return slerp(arc.a[dim], arc.b[dim], arc.angle, clamp(t, 0.0, 1.0)) + arc.center[dim];
}
export function globeTileBounds(id: CanonicalTileID): Aabb {
if (id.z <= 1) {
return GLOBE_LOW_ZOOM_TILE_AABBS[id.z + id.y * 2 + id.x];
}
// After zoom 1 surface function is monotonic for all tile patches
// => it is enough to project corner points
const [min, max] = globeTileLatLngCorners(id);
const corners = [
latLngToECEF(min[0], min[1]),
latLngToECEF(min[0], max[1]),
latLngToECEF(max[0], min[1]),
latLngToECEF(max[0], max[1])
];
const bMin = [GLOBE_MAX, GLOBE_MAX, GLOBE_MAX];
const bMax = [GLOBE_MIN, GLOBE_MIN, GLOBE_MIN];
for (const p of corners) {
bMin[0] = Math.min(bMin[0], p[0]);
bMin[1] = Math.min(bMin[1], p[1]);
bMin[2] = Math.min(bMin[2], p[2]);
bMax[0] = Math.max(bMax[0], p[0]);
bMax[1] = Math.max(bMax[1], p[1]);
bMax[2] = Math.max(bMax[2], p[2]);
}
return new Aabb(bMin, bMax);
}
export function aabbForTileOnGlobe(tr: Transform, numTiles: number, tileId: CanonicalTileID): Aabb {
const scale = numTiles / tr.worldSize;
const to1to1Units = (cornerMin: Vec3, cornerMax: Vec3) => {
vec3.scale(cornerMin, cornerMin, scale);
vec3.scale(cornerMax, cornerMax, scale);
};
const mx = Number.MAX_VALUE;
const cornerMax = [-mx, -mx, -mx];
const cornerMin = [mx, mx, mx];
const m = calculateGlobeMatrix(tr);
if (tileId.z <= 1) {
// Compute minimum bounding box that fully encapsulates
// transformed corners of the local aabb
const aabb = globeTileBounds(tileId);
const corners = aabb.getCorners();
for (let i = 0; i < corners.length; i++) {
vec3.transformMat4(corners[i], corners[i], m);
vec3.min(cornerMin, cornerMin, corners[i]);
vec3.max(cornerMax, cornerMax, corners[i]);
}
to1to1Units(cornerMin, cornerMax);
return new Aabb(cornerMin, cornerMax);
}
// Find minimal aabb for a tile. Correct solution would be to compute bounding box that
// fully encapsulates the curved patch that represents the tile on globes surface.
// This can be simplified a bit as the globe transformation is constrained:
// 1. Camera always faces the center point on the map
// 2. Camera is always above (z-coordinate) all of the tiles
// 3. Up direction of the coordinate space (pixel space) is always +z. This means that
// the "highest" point of the map is at the center.
// 4. z-coordinate of any point in any tile descends as a function of the distance from the center
// Simplified aabb is computed by first encapsulating 4 transformed corner points of the tile.
// The resulting aabb is not complete yet as curved edges of the tile might span outside of the boundaries.
// It is enough to extend the aabb to contain only the edge that's closest to the center point.
const [nw, se] = globeTileLatLngCorners(tileId);
const bounds = new LngLatBounds();
bounds.setSouthWest([nw[1], se[0]]);
bounds.setNorthEast([se[1], nw[0]]);
const corners = [
latLngToECEF(bounds.getSouth(), bounds.getWest()),
latLngToECEF(bounds.getSouth(), bounds.getEast()),
latLngToECEF(bounds.getNorth(), bounds.getEast()),
latLngToECEF(bounds.getNorth(), bounds.getWest())
];
// Note that here we're transforming the corners to world space while finding the min/max values.
for (let i = 0; i < corners.length; i++) {
vec3.transformMat4(corners[i], corners[i], m);
vec3.min(cornerMin, cornerMin, corners[i]);
vec3.max(cornerMax, cornerMax, corners[i]);
}
if (bounds.contains(tr.center)) {
cornerMax[2] = 0.0;
to1to1Units(cornerMin, cornerMax);
return new Aabb(cornerMin, cornerMax);
}
// Compute parameters describing edges of the tile (i.e. arcs) on the globe surface.
// Vertical edges revolves around the globe origin whereas horizontal edges revolves around the y-axis.
const globeCenter = [m[12], m[13], m[14]];
const centerLng = tr.center.lng;
const centerLat = clamp(tr.center.lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE);
const center = [mercatorXfromLng(centerLng), mercatorYfromLat(centerLat)];
const tileCenterLng = bounds.getCenter().lng;
const tileCenterLat = clamp(bounds.getCenter().lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE);
const tileCenter = [mercatorXfromLng(tileCenterLng), mercatorYfromLat(tileCenterLat)];
let arcCenter = new Array(3);
let closestArcIdx = 0;
const dx = center[0] - tileCenter[0];
const dy = center[1] - tileCenter[1];
// Here we determine the arc which is closest to the map center point.
// Horizontal arcs origin = globeCenter.
// Vertical arcs origin = globeCenter + yAxis * shift.
// Where `shift` is determined by latitude.
if (Math.abs(dx) > Math.abs(dy)) {
closestArcIdx = dx >= 0 ? 1 : 3;
arcCenter = globeCenter;
} else {
closestArcIdx = dy >= 0 ? 0 : 2;
const yAxis = [m[4], m[5], m[6]];
let shift: number;
if (dy >= 0) {
shift = -Math.sin(degToRad(bounds.getSouth())) * GLOBE_RADIUS;
} else {
shift = -Math.sin(degToRad(bounds.getNorth())) * GLOBE_RADIUS;
}
arcCenter = vec3.scaleAndAdd(arcCenter, globeCenter, yAxis, shift);
}
const arcA = corners[closestArcIdx];
const arcB = corners[(closestArcIdx + 1) % 4];
const closestArc = new Arc(arcA, arcB, arcCenter);
const arcBounds = [(localExtremum(closestArc, 0) || arcA[0]),
(localExtremum(closestArc, 1) || arcA[1]),
(localExtremum(closestArc, 2) || arcA[2])];
// Reduce height of the aabb to match height of the closest arc. This reduces false positives
// of tiles farther away from the center as they would otherwise intersect with far end
// of the view frustum
cornerMin[2] = Math.min(arcA[2], arcB[2]);
vec3.min(cornerMin, cornerMin, arcBounds);
vec3.max(cornerMax, cornerMax, arcBounds);
to1to1Units(cornerMin, cornerMax);
return new Aabb(cornerMin, cornerMax);
}
export function globeTileLatLngCorners(id: CanonicalTileID): [[number, number], [number, number]] {
const tileScale = 1 << id.z;
const left = id.x / tileScale;
const right = (id.x + 1) / tileScale;
const top = id.y / tileScale;
const bottom = (id.y + 1) / tileScale;
const latLngTL = [ latFromMercatorY(top), lngFromMercatorX(left) ];
const latLngBR = [ latFromMercatorY(bottom), lngFromMercatorX(right) ];
return [latLngTL, latLngBR];
}
function csLatLngToECEF(cosLat: number, sinLat: number, lng: number, radius: number = GLOBE_RADIUS): Array<number> {
lng = degToRad(lng);
// Convert lat & lng to spherical representation. Use zoom=0 as a reference
const sx = cosLat * Math.sin(lng) * radius;
const sy = -sinLat * radius;
const sz = cosLat * Math.cos(lng) * radius;
return [sx, sy, sz];
}
export function latLngToECEF(lat: number, lng: number, radius?: number): Array<number> {
return csLatLngToECEF(Math.cos(degToRad(lat)), Math.sin(degToRad(lat)), lng, radius);
}
export function tileCoordToECEF(x: number, y: number, id: CanonicalTileID): Array<number> {
const tiles = Math.pow(2.0, id.z);
const mx = (x / EXTENT + id.x) / tiles;
const my = (y / EXTENT + id.y) / tiles;
const lat = latFromMercatorY(my);
const lng = lngFromMercatorX(mx);
const pos = latLngToECEF(lat, lng);
return pos;
}
export function globeECEFOrigin(tileMatrix: Mat4, id: UnwrappedTileID): [number, number, number] {
const origin = [0, 0, 0];
const bounds = globeTileBounds(id.canonical);
const normalizationMatrix = globeNormalizeECEF(bounds);
vec3.transformMat4(origin, origin, normalizationMatrix);
vec3.transformMat4(origin, origin, tileMatrix);
return origin;
}
export function globeECEFNormalizationScale(bounds: Aabb): number {
const maxExt = Math.max(...vec3.sub([], bounds.max, bounds.min));
return GLOBE_NORMALIZATION_MASK / maxExt;
}
export function globeNormalizeECEF(bounds: Aabb): Float64Array {
const m = mat4.identity(new Float64Array(16));
const scale = globeECEFNormalizationScale(bounds);
mat4.scale(m, m, [scale, scale, scale]);
mat4.translate(m, m, vec3.negate([], bounds.min));
return m;
}
export function globeDenormalizeECEF(bounds: Aabb): Float64Array {
const m = mat4.identity(new Float64Array(16));
const scale = 1.0 / globeECEFNormalizationScale(bounds);
mat4.translate(m, m, bounds.min);
mat4.scale(m, m, [scale, scale, scale]);
return m;
}
export function globeECEFUnitsToPixelScale(worldSize: number): number {
const localRadius = EXTENT / (2.0 * Math.PI);
const wsRadius = worldSize / (2.0 * Math.PI);
return wsRadius / localRadius;
}
export function globePixelsToTileUnits(zoom: number, id: CanonicalTileID): number {
const ecefPerPixel = EXTENT / (TILE_SIZE * Math.pow(2, zoom));
const normCoeff = globeECEFNormalizationScale(globeTileBounds(id));
return ecefPerPixel * normCoeff;
}
function calculateGlobePosMatrix(x, y, worldSize, lng, lat): Float64Array {
// transform the globe from reference coordinate space to world space
const scale = globeECEFUnitsToPixelScale(worldSize);
const offset = [x, y, -worldSize / (2.0 * Math.PI)];
const m = mat4.identity(new Float64Array(16));
mat4.translate(m, m, offset);
mat4.scale(m, m, [scale, scale, scale]);
mat4.rotateX(m, m, degToRad(-lat));
mat4.rotateY(m, m, degToRad(-lng));
return m;
}
export function calculateGlobeMatrix(tr: Transform): Float64Array {
const {x, y} = tr.point;
const {lng, lat} = tr._center;
return calculateGlobePosMatrix(x, y, tr.worldSize, lng, lat);
}
export function calculateGlobeLabelMatrix(tr: Transform, id: CanonicalTileID): Float64Array {
const {lng, lat} = tr._center;
// Camera is moved closer towards the ground near poles as part of
// compesanting the reprojection. This has to be compensated for the
// map aligned label space. Whithout this logic map aligned symbols
// would appear larger than intended.
const m = calculateGlobePosMatrix(0, 0, tr.worldSize / tr._projectionScaler, lng, lat);
return mat4.multiply(m, m, globeDenormalizeECEF(globeTileBounds(id)));
}
export function calculateGlobeMercatorMatrix(tr: Transform): Float32Array {
const worldSize = tr.worldSize;
const point = tr.point;
const mercatorZ = mercatorZfromAltitude(1, tr.center.lat) * worldSize;
const projectionScaler = mercatorZ / tr.pixelsPerMeter;
const zScale = tr.pixelsPerMeter;
const ws = worldSize / projectionScaler;
const posMatrix = mat4.identity(new Float64Array(16));
mat4.translate(posMatrix, posMatrix, [point.x, point.y, 0.0]);
mat4.scale(posMatrix, posMatrix, [ws, ws, zScale]);
return Float32Array.from(posMatrix);
}
export const GLOBE_ZOOM_THRESHOLD_MIN = 5;
export const GLOBE_ZOOM_THRESHOLD_MAX = 6;
export function globeToMercatorTransition(zoom: number): number {
return smoothstep(GLOBE_ZOOM_THRESHOLD_MIN, GLOBE_ZOOM_THRESHOLD_MAX, zoom);
}
export function globeMatrixForTile(id: CanonicalTileID, globeMatrix: Float64Array): Float32Array {
const decode = globeDenormalizeECEF(globeTileBounds(id));
return mat4.mul(mat4.create(), globeMatrix, decode);
}
export function globePoleMatrixForTile(z: number, x: number, tr: Transform): Float32Array {
const poleMatrix = mat4.identity(new Float64Array(16));
const numTiles = 1 << z;
const xOffsetAngle = (x / numTiles - 0.5) * 360;
const point = tr.point;
const ws = tr.worldSize;
const s = tr.worldSize / (tr.tileSize * numTiles);
mat4.translate(poleMatrix, poleMatrix, [point.x, point.y, -(ws / Math.PI / 2.0)]);
mat4.scale(poleMatrix, poleMatrix, [s, s, s]);
mat4.rotateX(poleMatrix, poleMatrix, degToRad(-tr._center.lat));
mat4.rotateY(poleMatrix, poleMatrix, degToRad(-tr._center.lng + xOffsetAngle));
return Float32Array.from(poleMatrix);
}
export function getGridMatrix(id: CanonicalTileID, corners: [[number, number], [number, number]]): Array<number> {
const [tl, br] = corners;
const S = 1.0 / GLOBE_VERTEX_GRID_SIZE;
const x = (br[1] - tl[1]) * S;
const y = (br[0] - tl[0]) * S;
const tileZoom = 1 << id.z;
return [0, x, tileZoom, y, 0, id.y, tl[0], tl[1], S];
}
const POLE_RAD = degToRad(85.0);
const POLE_COS = Math.cos(POLE_RAD);
const POLE_SIN = Math.sin(POLE_RAD);
export class GlobeSharedBuffers {
_poleNorthVertexBuffer: VertexBuffer;
_poleSouthVertexBuffer: VertexBuffer;
_poleIndexBuffer: IndexBuffer;
_poleSegments: Array<SegmentVector>;
_gridBuffer: VertexBuffer;
_gridIndexBuffer: IndexBuffer;
_gridSegments: SegmentVector;
atmosphereVertexBuffer: VertexBuffer;
atmosphereIndexBuffer: IndexBuffer;
atmosphereSegments: SegmentVector;
_wireframeIndexBuffer: IndexBuffer;
_wireframeSegments: SegmentVector;
constructor(context: Context) {
this._createGrid(context);
this._createPoles(context);
this._createAtmosphere(context);
}
destroy() {
this._poleIndexBuffer.destroy();
this._gridBuffer.destroy();
this._gridIndexBuffer.destroy();
this._poleNorthVertexBuffer.destroy();
this._poleSouthVertexBuffer.destroy();
for (const segments of this._poleSegments) segments.destroy();
this._gridSegments.destroy();
this.atmosphereVertexBuffer.destroy();
this.atmosphereIndexBuffer.destroy();
this.atmosphereSegments.destroy();
if (this._wireframeIndexBuffer) {
this._wireframeIndexBuffer.destroy();
this._wireframeSegments.destroy();
}
}
_createGrid(context: Context) {
const gridVertices = new PosArray();
const gridIndices = new TriangleIndexArray();
const quadExt = GLOBE_VERTEX_GRID_SIZE;
const vertexExt = quadExt + 1;
for (let j = 0; j < vertexExt; j++)
for (let i = 0; i < vertexExt; i++)
gridVertices.emplaceBack(i, j);
for (let j = 0; j < quadExt; j++) {
for (let i = 0; i < quadExt; i++) {
const index = j * vertexExt + i;
gridIndices.emplaceBack(index + 1, index, index + vertexExt);
gridIndices.emplaceBack(index + vertexExt, index + vertexExt + 1, index + 1);
}
}
const numVertices = vertexExt * vertexExt;
const numPrimitives = quadExt * quadExt * 2;
this._gridBuffer = context.createVertexBuffer(gridVertices, posAttributes.members);
this._gridIndexBuffer = context.createIndexBuffer(gridIndices, true);
this._gridSegments = SegmentVector.simpleSegment(0, 0, numVertices, numPrimitives);
}
_createPoles(context: Context) {
const poleIndices = new TriangleIndexArray();
for (let i = 0; i <= GLOBE_VERTEX_GRID_SIZE; i++) {
poleIndices.emplaceBack(0, i + 1, i + 2);
}
this._poleIndexBuffer = context.createIndexBuffer(poleIndices, true);
const northVertices = new GlobeVertexArray();
const southVertices = new GlobeVertexArray();
const polePrimitives = GLOBE_VERTEX_GRID_SIZE;
const poleVertices = GLOBE_VERTEX_GRID_SIZE + 2;
this._poleSegments = [];
for (let zoom = 0, offset = 0; zoom < GLOBE_ZOOM_THRESHOLD_MIN; zoom++) {
const tiles = 1 << zoom;
const radius = tiles * TILE_SIZE / Math.PI / 2.0;
const endAngle = 360.0 / tiles;
northVertices.emplaceBack(0, -radius, 0, 0, 0, 0.5, 0); // place the tip
southVertices.emplaceBack(0, -radius, 0, 0, 0, 0.5, 1);
for (let i = 0; i <= GLOBE_VERTEX_GRID_SIZE; i++) {
const uvX = i / GLOBE_VERTEX_GRID_SIZE;
const angle = interpolate(0, endAngle, uvX);
const [gx, gy, gz] = csLatLngToECEF(POLE_COS, POLE_SIN, angle, radius);
northVertices.emplaceBack(gx, gy, gz, 0, 0, uvX, 0);
southVertices.emplaceBack(gx, gy, gz, 0, 0, uvX, 1);
}
this._poleSegments.push(SegmentVector.simpleSegment(offset, 0, poleVertices, polePrimitives));
offset += poleVertices;
}
this._poleNorthVertexBuffer = context.createVertexBuffer(northVertices, globeLayoutAttributes, false);
this._poleSouthVertexBuffer = context.createVertexBuffer(southVertices, globeLayoutAttributes, false);
}
_createAtmosphere(context: Context) {
const atmosphereVertices = new GlobeAtmosphereVertexArray();
atmosphereVertices.emplaceBack(-1, 1, 1, 0, 0);
atmosphereVertices.emplaceBack(1, 1, 1, 1, 0);
atmosphereVertices.emplaceBack(1, -1, 1, 1, 1);
atmosphereVertices.emplaceBack(-1, -1, 1, 0, 1);
const atmosphereTriangles = new TriangleIndexArray();
atmosphereTriangles.emplaceBack(0, 1, 2);
atmosphereTriangles.emplaceBack(2, 3, 0);
this.atmosphereVertexBuffer = context.createVertexBuffer(atmosphereVertices, atmosphereLayout.members);
this.atmosphereIndexBuffer = context.createIndexBuffer(atmosphereTriangles);
this.atmosphereSegments = SegmentVector.simpleSegment(0, 0, 4, 2);
}
getGridBuffers(): [VertexBuffer, IndexBuffer, SegmentVector] {
return [this._gridBuffer, this._gridIndexBuffer, this._gridSegments];
}
getPoleBuffers(z: number): [VertexBuffer, VertexBuffer, IndexBuffer, SegmentVector] {
return [this._poleNorthVertexBuffer, this._poleSouthVertexBuffer, this._poleIndexBuffer, this._poleSegments[z]];
}
getWirefameBuffers(context: Context): [VertexBuffer, IndexBuffer, SegmentVector] {
if (!this._wireframeSegments) {
const wireframeIndices = new LineIndexArray();
const quadExt = GLOBE_VERTEX_GRID_SIZE;
const vertexExt = quadExt + 1;
for (let j = 0; j < quadExt; j++) {
for (let i = 0; i < quadExt; i++) {
const index = j * vertexExt + i;
wireframeIndices.emplaceBack(index, index + 1);
wireframeIndices.emplaceBack(index, index + vertexExt);
wireframeIndices.emplaceBack(index, index + vertexExt + 1);
}
}
this._wireframeIndexBuffer = context.createIndexBuffer(wireframeIndices);
this._wireframeSegments = SegmentVector.simpleSegment(0, 0, quadExt * quadExt, wireframeIndices.length);
}
return [this._gridBuffer, this._wireframeIndexBuffer, this._wireframeSegments];
}
}