mapbox-gl
Version:
A WebGL interactive maps library
934 lines (770 loc) • 39.1 kB
JavaScript
// @flow
import MercatorCoordinate, {
lngFromMercatorX,
latFromMercatorY,
mercatorZfromAltitude,
mercatorXfromLng,
mercatorYfromLat,
MAX_MERCATOR_LATITUDE
} from '../mercator_coordinate.js';
import EXTENT from '../../style-spec/data/extent.js';
import {number as interpolate} from '../../style-spec/util/interpolate.js';
import {degToRad, radToDeg, clamp, smoothstep, getColumn, shortestAngle} from '../../util/util.js';
import {vec3, vec4, mat3, mat4} from 'gl-matrix';
import SegmentVector from '../../data/segment.js';
import {members as globeLayoutAttributes} from '../../terrain/globe_attributes.js';
import posAttributes from '../../data/pos_attributes.js';
import {TriangleIndexArray, GlobeVertexArray, PosArray} from '../../data/array_types.js';
import {Aabb, Ray} from '../../util/primitives.js';
import LngLat, {earthRadius} from '../lng_lat.js';
import LngLatBounds from '../lng_lat_bounds.js';
import Point from '@mapbox/point-geometry';
import assert from 'assert';
import type Painter from '../../render/painter.js';
import type {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id.js';
import type Context from '../../gl/context.js';
import type {Vec3, Mat4} 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_ZOOM_THRESHOLD_MIN = 5;
export const GLOBE_ZOOM_THRESHOLD_MAX = 6;
// At low zoom levels the globe gets rendered so that the scale at this
// latitude matches it's scale in a mercator map. The choice of latitude is
// a bit arbitrary. Different choices will match mercator more closely in different
// views. 45 is a good enough choice because:
// - it's half way from the pole to the equator
// - matches most middle latitudes reasonably well
// - biases towards increasing size rather than decreasing
// - makes the globe slightly larger at very low zoom levels, where it already
// covers less pixels than mercator (due to the curved surface)
//
// Changing this value will change how large a globe is rendered and could affect
// end users. This should only be done of the tradeoffs between change and improvement
// are carefully considered.
export const GLOBE_SCALE_MATCH_LATITUDE = 45;
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;
export const GLOBE_VERTEX_GRID_SIZE = 64;
const GLOBE_LATITUDINAL_GRID_LOD_TABLE = [GLOBE_VERTEX_GRID_SIZE, GLOBE_VERTEX_GRID_SIZE / 2, GLOBE_VERTEX_GRID_SIZE / 4];
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 function globeMetersToEcef(d: number): number {
return d * GLOBE_RADIUS / earthRadius;
}
export function globePointCoordinate(tr: Transform, x: number, y: number, clampToHorizon: boolean = true): ?MercatorCoordinate {
const point0 = vec3.scale([], tr._camera.position, tr.worldSize);
const point1 = [x, y, 1, 1];
vec4.transformMat4(point1, point1, tr.pixelMatrixInverse);
vec4.scale(point1, point1, 1 / point1[3]);
const p0p1 = vec3.sub([], point1, point0);
const dir = vec3.normalize([], p0p1);
// Find closest point on the sphere to the ray. This is a bit more involving operation
// if the ray is not intersecting with the sphere, in which case we "clamp" the ray
// to the surface of the sphere, i.e. find a tangent vector that originates from the camera position
const m = tr.globeMatrix;
const globeCenter = [m[12], m[13], m[14]];
const p0toCenter = vec3.sub([], globeCenter, point0);
const p0toCenterDist = vec3.length(p0toCenter);
const centerDir = vec3.normalize([], p0toCenter);
const radius = tr.worldSize / (2.0 * Math.PI);
const cosAngle = vec3.dot(centerDir, dir);
const origoTangentAngle = Math.asin(radius / p0toCenterDist);
const origoDirAngle = Math.acos(cosAngle);
if (origoTangentAngle < origoDirAngle) {
if (!clampToHorizon) return null;
// Find the tangent vector by interpolating between camera-to-globe and camera-to-click vectors.
// First we'll find a point P1 on the clicked ray that forms a right-angled triangle with the camera position
// and the center of the globe. Angle of the tanget vector is then used as the interpolation factor
const clampedP1 = [], origoToP1 = [];
vec3.scale(clampedP1, dir, p0toCenterDist / cosAngle);
vec3.normalize(origoToP1, vec3.sub(origoToP1, clampedP1, p0toCenter));
vec3.normalize(dir, vec3.add(dir, p0toCenter, vec3.scale(dir, origoToP1, Math.tan(origoTangentAngle) * p0toCenterDist)));
}
const pointOnGlobe = [];
const ray = new Ray(point0, dir);
ray.closestPointOnSphere(globeCenter, radius, pointOnGlobe);
// Transform coordinate axes to find lat & lng of the position
const xa = vec3.normalize([], getColumn(m, 0));
const ya = vec3.normalize([], getColumn(m, 1));
const za = vec3.normalize([], getColumn(m, 2));
const xp = vec3.dot(xa, pointOnGlobe);
const yp = vec3.dot(ya, pointOnGlobe);
const zp = vec3.dot(za, pointOnGlobe);
const lat = radToDeg(Math.asin(-yp / radius));
let lng = radToDeg(Math.atan2(xp, zp));
// Check that the returned longitude angle is not wrapped
lng = tr.center.lng + shortestAngle(tr.center.lng, lng);
const mx = mercatorXfromLng(lng);
const my = clamp(mercatorYfromLat(lat), 0, 1);
return new MercatorCoordinate(mx, my);
}
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 bounds = tileCornersToBounds(id);
const corners = boundsToECEF(bounds);
return Aabb.fromPoints(corners);
}
export function interpolateVec3(from: Vec3, to: Vec3, phase: number): Vec3 {
vec3.scale(from, from, 1 - phase);
return vec3.scaleAndAdd(from, from, to, phase);
}
// Similar to globeTileBounds() but accounts for globe to Mercator transition.
export function transitionTileAABBinECEF(id: CanonicalTileID, tr: Transform): Aabb {
const phase = globeToMercatorTransition(tr.zoom);
if (phase === 0) {
return globeTileBounds(id);
}
const bounds = tileCornersToBounds(id);
const corners = boundsToECEF(bounds);
const w = mercatorXfromLng(bounds.getWest()) * tr.worldSize;
const e = mercatorXfromLng(bounds.getEast()) * tr.worldSize;
const n = mercatorYfromLat(bounds.getNorth()) * tr.worldSize;
const s = mercatorYfromLat(bounds.getSouth()) * tr.worldSize;
// Mercator bounds globeCorners in world/pixel space
const nw = [w, n, 0];
const ne = [e, n, 0];
const sw = [w, s, 0];
const se = [e, s, 0];
// Transform Mercator globeCorners to ECEF
const worldToECEFMatrix = mat4.invert([], tr.globeMatrix);
vec3.transformMat4(nw, nw, worldToECEFMatrix);
vec3.transformMat4(ne, ne, worldToECEFMatrix);
vec3.transformMat4(sw, sw, worldToECEFMatrix);
vec3.transformMat4(se, se, worldToECEFMatrix);
// Interpolate Mercator corners and globe corners
corners[0] = interpolateVec3(corners[0], sw, phase);
corners[1] = interpolateVec3(corners[1], se, phase);
corners[2] = interpolateVec3(corners[2], ne, phase);
corners[3] = interpolateVec3(corners[3], nw, phase);
return Aabb.fromPoints(corners);
}
function transformPoints(corners: Array<Vec3>, globeMatrix: Mat4, scale: number) {
for (const corner of corners) {
vec3.transformMat4(corner, corner, globeMatrix);
vec3.scale(corner, corner, scale);
}
}
// Returns AABB in world/camera space scaled by numTiles / tr.worldSize
// extendToPoles - extend tiles neighboring to north / south pole segments with the north/south pole point
export function aabbForTileOnGlobe(tr: Transform, numTiles: number, tileId: CanonicalTileID, extendToPoles: boolean): Aabb {
const scale = numTiles / tr.worldSize;
const m = tr.globeMatrix;
if (tileId.z <= 1) {
// Compute world/pixel space AABB that fully encapsulates
// transformed corners of the ECEF AABB
const corners = globeTileBounds(tileId).getCorners();
transformPoints(corners, m, scale);
return Aabb.fromPoints(corners);
}
// 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 bounds = tileCornersToBounds(tileId, extendToPoles);
const corners = boundsToECEF(bounds);
// Transform the corners to world space
transformPoints(corners, m, scale);
const mx = Number.MAX_VALUE;
const cornerMax = [-mx, -mx, -mx];
const cornerMin = [mx, mx, mx];
// Extend the aabb by including the center point. There are some corner cases where center point is inside the
// tile but due to curvature aabb computed from corner points does not cover the curved area.
if (bounds.contains(tr.center)) {
for (const corner of corners) {
vec3.min(cornerMin, cornerMin, corner);
vec3.max(cornerMax, cornerMax, corner);
}
cornerMax[2] = 0.0;
const point = tr.point;
const center = [point.x * scale, point.y * scale, 0];
vec3.min(cornerMin, cornerMin, center);
vec3.max(cornerMax, cornerMax, center);
return new Aabb(cornerMin, cornerMax);
}
// Compute arcs describing edges of the tile on the globe surface.
// Vertical edges revolves around the globe origin whereas horizontal edges revolves around the y-axis.
const arcCenter = [m[12] * scale, m[13] * scale, m[14] * scale];
const tileCenter = bounds.getCenter();
const centerLat = clamp(tr.center.lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE);
const tileCenterLat = clamp(tileCenter.lat, -MAX_MERCATOR_LATITUDE, MAX_MERCATOR_LATITUDE);
const camX = mercatorXfromLng(tr.center.lng);
const camY = mercatorYfromLat(centerLat);
let dx = camX - mercatorXfromLng(tileCenter.lng);
const dy = camY - mercatorYfromLat(tileCenterLat);
// Shortest distance might be across the antimeridian
if (dx > .5) {
dx -= 1;
} else if (dx < -.5) {
dx += 1;
}
// Here we determine the arc which is closest to the map center point.
// Horizontal arcs origin = globe center
// Vertical arcs origin = globe center + yAxis * shift.
// Where `shift` is determined by latitude.
let closestArcIdx = 0;
if (Math.abs(dx) > Math.abs(dy)) {
closestArcIdx = dx >= 0 ? 1 : 3;
} else {
closestArcIdx = dy >= 0 ? 0 : 2;
const yAxis = [m[4] * scale, m[5] * scale, m[6] * scale];
const shift = -Math.sin(degToRad(dy >= 0 ? bounds.getSouth() : bounds.getNorth())) * GLOBE_RADIUS;
vec3.scaleAndAdd(arcCenter, arcCenter, yAxis, shift);
}
const arcStart = corners[closestArcIdx];
const arcEnd = corners[(closestArcIdx + 1) % 4];
const closestArc = new Arc(arcStart, arcEnd, arcCenter);
const arcExtremum = [
(localExtremum(closestArc, 0) || arcStart[0]),
(localExtremum(closestArc, 1) || arcStart[1]),
(localExtremum(closestArc, 2) || arcStart[2])];
const phase = globeToMercatorTransition(tr.zoom);
if (phase > 0.0) {
const mercatorCorners = mercatorTileCornersInCameraSpace(tileId, numTiles, tr._pixelsPerMercatorPixel, camX, camY);
// Interpolate the four corners towards their world space location in mercator projection during transition.
for (let i = 0; i < corners.length; i++) {
interpolateVec3(corners[i], mercatorCorners[i], phase);
}
// Calculate the midpoint of the closest edge midpoint in Mercator
const mercatorMidpoint = vec3.add([], mercatorCorners[closestArcIdx], mercatorCorners[(closestArcIdx + 1) % 4]);
vec3.scale(mercatorMidpoint, mercatorMidpoint, .5);
// Interpolate globe extremum toward Mercator midpoint
interpolateVec3(arcExtremum, mercatorMidpoint, phase);
}
for (const corner of corners) {
vec3.min(cornerMin, cornerMin, corner);
vec3.max(cornerMax, cornerMax, corner);
}
// 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(arcStart[2], arcEnd[2]);
vec3.min(cornerMin, cornerMin, arcExtremum);
vec3.max(cornerMax, cornerMax, arcExtremum);
return new Aabb(cornerMin, cornerMax);
}
export function tileCornersToBounds({x, y, z}: CanonicalTileID, extendToPoles: boolean = false): LngLatBounds {
const s = 1.0 / (1 << z);
const sw = new LngLat(lngFromMercatorX(x * s), y === (1 << z) - 1 && extendToPoles ? -90 : latFromMercatorY((y + 1) * s));
const ne = new LngLat(lngFromMercatorX((x + 1) * s), y === 0 && extendToPoles ? 90 : latFromMercatorY(y * s));
return new LngLatBounds(sw, ne);
}
function mercatorTileCornersInCameraSpace({x, y, z}: CanonicalTileID, numTiles: number, mercatorScale: number, camX: number, camY: number): Array<Vec3> {
const tileScale = 1.0 / (1 << z);
// Values in Mercator coordinates (0 - 1)
let w = x * tileScale;
let e = w + tileScale;
let n = y * tileScale;
let s = n + tileScale;
// // Ensure that the tile viewed is the nearest when across the antimeridian
let wrap = 0;
const tileCenterXFromCamera = (w + e) / 2 - camX;
if (tileCenterXFromCamera > .5) {
wrap = -1;
} else if (tileCenterXFromCamera < -.5) {
wrap = 1;
}
camX *= numTiles;
camY *= numTiles;
// Transform Mercator coordinates to points on the plane tangent to the globe at cameraCenter.
w = ((w + wrap) * numTiles - camX) * mercatorScale + camX;
e = ((e + wrap) * numTiles - camX) * mercatorScale + camX;
n = (n * numTiles - camY) * mercatorScale + camY;
s = (s * numTiles - camY) * mercatorScale + camY;
return [[w, s, 0],
[e, s, 0],
[e, n, 0],
[w, n, 0]];
}
function boundsToECEF(bounds: LngLatBounds): Array<Vec3> {
const ny = degToRad(bounds.getNorth());
const sy = degToRad(bounds.getSouth());
const cosN = Math.cos(ny);
const cosS = Math.cos(sy);
const sinN = Math.sin(ny);
const sinS = Math.sin(sy);
const w = bounds.getWest();
const e = bounds.getEast();
return [
csLatLngToECEF(cosS, sinS, w),
csLatLngToECEF(cosS, sinS, e),
csLatLngToECEF(cosN, sinN, e),
csLatLngToECEF(cosN, sinN, w)
];
}
function csLatLngToECEF(cosLat: number, sinLat: number, lng: number, radius: number = GLOBE_RADIUS): Vec3 {
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 ecefToLatLng([x, y, z]: Array<number>): LngLat {
const radius = Math.hypot(x, y, z);
const lng = Math.atan2(x, z);
const lat = Math.PI * 0.5 - Math.acos(-y / radius);
return new LngLat(radToDeg(lng), radToDeg(lat));
}
export function latLngToECEF(lat: number, lng: number, radius?: number): Vec3 {
assert(lat <= 90 && lat >= -90, 'Lattitude must be between -90 and 90');
return csLatLngToECEF(Math.cos(degToRad(lat)), Math.sin(degToRad(lat)), lng, radius);
}
export function tileCoordToECEF(x: number, y: number, id: CanonicalTileID, radius?: number): Vec3 {
const tileCount = 1 << id.z;
const mercatorX = (x / EXTENT + id.x) / tileCount;
const mercatorY = (y / EXTENT + id.y) / tileCount;
const lat = latFromMercatorY(mercatorY);
const lng = lngFromMercatorX(mercatorX);
const pos = latLngToECEF(lat, lng, radius);
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({min, max}: Aabb): number {
return GLOBE_NORMALIZATION_MASK / Math.max(max[0] - min[0], max[1] - min[1], max[2] - min[2]);
}
// avoid redundant allocations by sharing the same typed array for normalization/denormalization matrices;
// we never use multiple instances of these at the same time, but this might change, so let's be careful here!
const tempMatrix = new Float64Array(16);
export function globeNormalizeECEF(bounds: Aabb): Float64Array {
const scale = globeECEFNormalizationScale(bounds);
const m = mat4.fromScaling(tempMatrix, [scale, scale, scale]);
return mat4.translate(m, m, vec3.negate([], bounds.min));
}
export function globeDenormalizeECEF(bounds: Aabb): Float64Array {
const m = mat4.fromTranslation(tempMatrix, bounds.min);
const scale = 1.0 / globeECEFNormalizationScale(bounds);
return mat4.scale(m, m, [scale, scale, scale]);
}
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: number, y: number, worldSize: number, lng: number, lat: number): 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 {x, y} = tr.point;
// Map aligned label space for globe view is the non-rotated globe itself in pixel coordinates.
// 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(x, y, tr.worldSize / tr._pixelsPerMercatorPixel, 0, 0);
return mat4.multiply(m, m, globeDenormalizeECEF(globeTileBounds(id)));
}
export function calculateGlobeMercatorMatrix(tr: Transform): Float32Array {
const zScale = tr.pixelsPerMeter;
const ws = zScale / mercatorZfromAltitude(1, tr.center.lat);
const posMatrix = mat4.identity(new Float64Array(16));
mat4.translate(posMatrix, posMatrix, [tr.point.x, tr.point.y, 0.0]);
mat4.scale(posMatrix, posMatrix, [ws, ws, zScale]);
return Float32Array.from(posMatrix);
}
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));
// Rotate the pole triangle fan to the correct location
const numTiles = 1 << z;
const xOffsetAngle = (x / numTiles - 0.5) * Math.PI * 2.0;
mat4.rotateY(poleMatrix, tr.globeMatrix, xOffsetAngle);
return Float32Array.from(poleMatrix);
}
export function globeUseCustomAntiAliasing(painter: Painter, context: Context, transform: Transform): boolean {
const transitionT = globeToMercatorTransition(transform.zoom);
const useContextAA = painter.style.map._antialias;
const disabled = context.options.extStandardDerivativesForceOff || (painter.terrain && painter.terrain.exaggeration() > 0.0);
return transitionT === 0.0 && !useContextAA && !disabled;
}
export function getGridMatrix(id: CanonicalTileID, bounds: LngLatBounds, latitudinalLod: number, worldSize: number): Mat4 {
const n = bounds.getNorth();
const s = bounds.getSouth();
const w = bounds.getWest();
const e = bounds.getEast();
// Construct transformation matrix for converting tile coordinates into LatLngs
const tiles = 1 << id.z;
const tileWidth = e - w;
const tileHeight = n - s;
const tileToLng = tileWidth / GLOBE_VERTEX_GRID_SIZE;
const tileToLat = -tileHeight / GLOBE_LATITUDINAL_GRID_LOD_TABLE[latitudinalLod];
const matrix = [0, tileToLng, 0, tileToLat, 0, 0, n, w, 0];
if (id.z > 0) {
// Add slight padding to patch seams between tiles.
// This is done by extruding vertices by a fixed amount. Pixel padding
// is first converted to degrees and then to tile units before being
// applied to the final transformation matrix.
const pixelPadding = 0.5;
const padding = pixelPadding * 360.0 / worldSize;
const xScale = padding / tileWidth + 1;
const yScale = padding / tileHeight + 1;
const padMatrix = [xScale, 0, 0, 0, yScale, 0, -0.5 * padding / tileToLng, 0.5 * padding / tileToLat, 1];
mat3.multiply(matrix, matrix, padMatrix);
}
// Embed additional variables to the last row of the matrix
matrix[2] = tiles;
matrix[5] = id.x;
matrix[8] = id.y;
return matrix;
}
export function getLatitudinalLod(lat: number): number {
const UPPER_LATITUDE = MAX_MERCATOR_LATITUDE - 5.0;
lat = clamp(lat, -UPPER_LATITUDE, UPPER_LATITUDE) / UPPER_LATITUDE * 90.0;
// const t = Math.pow(1.0 - Math.cos(degToRad(lat)), 2);
const t = Math.pow(Math.abs(Math.sin(degToRad(lat))), 3);
const lod = Math.round(t * (GLOBE_LATITUDINAL_GRID_LOD_TABLE.length - 1));
return lod;
}
export function globeCenterToScreenPoint(tr: Transform): Point {
const pos = [0, 0, 0];
const matrix = mat4.identity(new Float64Array(16));
mat4.multiply(matrix, tr.pixelMatrix, tr.globeMatrix);
vec3.transformMat4(pos, pos, matrix);
return new Point(pos[0], pos[1]);
}
function cameraPositionInECEF(tr: Transform): Array<number> {
// Here "center" is the center of the globe. We refer to transform._center
// (the surface of the map on the center of the screen) as "pivot" to avoid confusion.
const centerToPivot = latLngToECEF(tr._center.lat, tr._center.lng);
// Set axis to East-West line tangent to sphere at pivot
const south = vec3.fromValues(0, 1, 0);
let axis = vec3.cross([], south, centerToPivot);
// Rotate axis around pivot by bearing
const rotation = mat4.fromRotation([], -tr.angle, centerToPivot);
axis = vec3.transformMat4(axis, axis, rotation);
// Rotate camera around axis by pitch
mat4.fromRotation(rotation, -tr._pitch, axis);
const pivotToCamera = vec3.normalize([], centerToPivot);
vec3.scale(pivotToCamera, pivotToCamera, globeMetersToEcef(tr.cameraToCenterDistance / tr.pixelsPerMeter));
vec3.transformMat4(pivotToCamera, pivotToCamera, rotation);
return vec3.add([], centerToPivot, pivotToCamera);
}
// Return the angle of the normal vector at a point on the globe relative to the camera.
// i.e. how much to tilt map-aligned markers.
export function globeTiltAtLngLat(tr: Transform, lngLat: LngLat): number {
const centerToPoint = latLngToECEF(lngLat.lat, lngLat.lng);
const centerToCamera = cameraPositionInECEF(tr);
const pointToCamera = vec3.subtract([], centerToCamera, centerToPoint);
return vec3.angle(pointToCamera, centerToPoint);
}
export function isLngLatBehindGlobe(tr: Transform, lngLat: LngLat): boolean {
// We consider 1% past the horizon not occluded, this allows popups to be dragged around the globe edge without fading.
return (globeTiltAtLngLat(tr, lngLat) > Math.PI / 2 * 1.01);
}
/**
* Check if poles are visible inside the current viewport
*
* @param {Transform} transform The current map transform.
* @returns {[boolean, boolean]} A tuple of booleans [northInViewport, southInViewport]
*/
export function polesInViewport(tr: Transform): [boolean, boolean] {
// Create matrix from ECEF to screen coordinates
const ecefToScreenMatrix = mat4.identity(new Float64Array(16));
mat4.multiply(ecefToScreenMatrix, tr.pixelMatrix, tr.globeMatrix);
const north = [0, GLOBE_MIN, 0];
const south = [0, GLOBE_MAX, 0];
// Translate the poles from ECEF to screen coordinates
vec3.transformMat4(north, north, ecefToScreenMatrix);
vec3.transformMat4(south, south, ecefToScreenMatrix);
// Check if the poles are inside the viewport and not behind the globe surface
const northInViewport =
north[0] > 0 && north[0] <= tr.width &&
north[1] > 0 && north[1] <= tr.height &&
!isLngLatBehindGlobe(tr, new LngLat(tr.center.lat, 90));
const southInViewport =
south[0] > 0 && south[0] <= tr.width &&
south[1] > 0 && south[1] <= tr.height &&
!isLngLatBehindGlobe(tr, new LngLat(tr.center.lat, -90));
return [northInViewport, southInViewport];
}
const POLE_RAD = degToRad(85.0);
const POLE_COS = Math.cos(POLE_RAD);
const POLE_SIN = Math.sin(POLE_RAD);
// Generate terrain grid with embedded skirts
const EMBED_SKIRTS = true;
type GridLodSegments = {
withoutSkirts: SegmentVector,
withSkirts: SegmentVector
};
type GridWithLods = {
vertices: PosArray,
indices: TriangleIndexArray,
segments: Array<GridLodSegments>
};
export class GlobeSharedBuffers {
_poleNorthVertexBuffer: VertexBuffer;
_poleSouthVertexBuffer: VertexBuffer;
_texturedPoleNorthVertexBuffer: VertexBuffer;
_texturedPoleSouthVertexBuffer: VertexBuffer;
_poleIndexBuffer: IndexBuffer;
_poleSegments: Array<SegmentVector>;
_gridBuffer: VertexBuffer;
_gridIndexBuffer: IndexBuffer;
_gridSegments: Array<GridLodSegments>;
constructor(context: Context) {
this._createGrid(context);
this._createPoles(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();
for (const segments of this._gridSegments) {
segments.withSkirts.destroy();
segments.withoutSkirts.destroy();
}
}
// Generate terrain grid vertices and indices for all LOD's
//
// Grid vertices memory layout:
//
// First line Skirt
// ┌───────────────┐
// │┌─────────────┐│
// Left ││┼┼┼┼┼┼┼┼┼┼┼┼┼││ Right
// Border ││┼┼┼┼┼┼┼┼┼┼┼┼┼││ Border
// Skirt │├─────────────┤│ Skirt
// ││ Main Grid ││
// │├─────────────┤│
// ││┼┼┼┼┼┼┼┼┼┼┼┼┼││
// ││┼┼┼┼┼┼┼┼┼┼┼┼┼││
// │└─────────────┘│
// ├───────────────┤
// ├───────────────┤
// └───────────────┘
// Bottom Skirt = Number of LOD's
//
_fillGridMeshWithLods(longitudinalCellsCount: number, latitudinalLods: number[]): GridWithLods {
const vertices = new PosArray();
const indices = new TriangleIndexArray();
const segments: Array<GridLodSegments> = [];
const xVertices = longitudinalCellsCount + 1 + 2 * (EMBED_SKIRTS ? 1 : 0);
const yVerticesHighLodNoStrip = latitudinalLods[0] + 1;
const yVerticesHighLodWithStrip = latitudinalLods[0] + 1 + (EMBED_SKIRTS ? 1 + latitudinalLods.length : 0);
// Index adjustment, used to make strip (x, y) vertex input attribute data
// to match same data on ordinary grid edges
const prepareVertex = (x: number, y: number, isSkirt: boolean) => {
if (!EMBED_SKIRTS) return [x, y];
let adjustedX = (() => {
if (x === xVertices - 1) {
return x - 2;
} else if (x === 0) {
return x;
} else {
return x - 1;
}
})();
// Skirt factor is introduces as an offset to the .x coordinate, similar to how it's done for mercator grids
const skirtOffset = 24575;
adjustedX += isSkirt ? skirtOffset : 0;
return [adjustedX, y];
};
// Add first horizontal strip if present
if (EMBED_SKIRTS) {
for (let x = 0; x < xVertices; ++x) {
vertices.emplaceBack(...prepareVertex(x, 0, true));
}
}
// Add main grid part with vertices strips embedded
for (let y = 0; y < yVerticesHighLodNoStrip; ++y) {
for (let x = 0; x < xVertices; ++x) {
const isSideBorder = (x === 0 || x === xVertices - 1);
vertices.emplaceBack(...prepareVertex(x, y, isSideBorder && EMBED_SKIRTS));
}
}
// Add bottom strips for each LOD
if (EMBED_SKIRTS) {
for (let lodIdx = 0; lodIdx < latitudinalLods.length; ++lodIdx) {
const lastYRowForLod = latitudinalLods[lodIdx];
for (let x = 0; x < xVertices; ++x) {
vertices.emplaceBack(...prepareVertex(x, lastYRowForLod, true));
}
}
}
// Fill triangles
for (let lodIdx = 0; lodIdx < latitudinalLods.length; ++lodIdx) {
const indexOffset = indices.length;
const yVerticesLod = latitudinalLods[lodIdx] + 1 + 2 * (EMBED_SKIRTS ? 1 : 0);
const skirtsOnlyIndices = new TriangleIndexArray();
for (let y = 0; y < yVerticesLod - 1; y++) {
const isLastLine = (y === yVerticesLod - 2);
const offsetToNextRow =
(isLastLine && EMBED_SKIRTS ?
(xVertices * (yVerticesHighLodWithStrip - latitudinalLods.length + lodIdx - y)) :
xVertices);
for (let x = 0; x < xVertices - 1; x++) {
const idx = y * xVertices + x;
const isSkirt = EMBED_SKIRTS && (y === 0 || isLastLine || x === 0 || x === xVertices - 2);
if (isSkirt) {
skirtsOnlyIndices.emplaceBack(idx + 1, idx, idx + offsetToNextRow);
skirtsOnlyIndices.emplaceBack(idx + offsetToNextRow, idx + offsetToNextRow + 1, idx + 1);
} else {
indices.emplaceBack(idx + 1, idx, idx + offsetToNextRow);
indices.emplaceBack(idx + offsetToNextRow, idx + offsetToNextRow + 1, idx + 1);
}
}
}
// Segments grid only
const withoutSkirts = SegmentVector.simpleSegment(0, indexOffset, vertices.length, indices.length - indexOffset);
for (let i = 0; i < skirtsOnlyIndices.uint16.length; i += 3) {
indices.emplaceBack(skirtsOnlyIndices.uint16[i], skirtsOnlyIndices.uint16[i + 1], skirtsOnlyIndices.uint16[i + 2]);
}
// Segments grid + skirts only
const withSkirts = SegmentVector.simpleSegment(0, indexOffset, vertices.length, indices.length - indexOffset);
segments.push({withoutSkirts, withSkirts});
}
return {vertices, indices, segments};
}
_createGrid(context: Context) {
const gridWithLods = this._fillGridMeshWithLods(GLOBE_VERTEX_GRID_SIZE, GLOBE_LATITUDINAL_GRID_LOD_TABLE);
this._gridSegments = gridWithLods.segments;
this._gridBuffer = context.createVertexBuffer(gridWithLods.vertices, posAttributes.members);
this._gridIndexBuffer = context.createIndexBuffer(gridWithLods.indices, true);
}
_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 texturedNorthVertices = new GlobeVertexArray();
const texturedSouthVertices = 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 endAngle = 360.0 / tiles;
northVertices.emplaceBack(0, -GLOBE_RADIUS, 0, 0.5, 0); // place the tip
southVertices.emplaceBack(0, -GLOBE_RADIUS, 0, 0.5, 1);
texturedNorthVertices.emplaceBack(0, -GLOBE_RADIUS, 0, 0.5, 0.5);
texturedSouthVertices.emplaceBack(0, -GLOBE_RADIUS, 0, 0.5, 0.5);
for (let i = 0; i <= GLOBE_VERTEX_GRID_SIZE; i++) {
let uvX = i / GLOBE_VERTEX_GRID_SIZE;
let uvY = 0.0;
const angle = interpolate(0, endAngle, uvX);
const [gx, gy, gz] = csLatLngToECEF(POLE_COS, POLE_SIN, angle, GLOBE_RADIUS);
northVertices.emplaceBack(gx, gy, gz, uvX, uvY);
southVertices.emplaceBack(gx, gy, gz, uvX, 1.0 - uvY);
const rad = degToRad(angle);
uvX = 0.5 + 0.5 * Math.sin(rad);
uvY = 0.5 + 0.5 * Math.cos(rad);
texturedNorthVertices.emplaceBack(gx, gy, gz, uvX, uvY);
texturedSouthVertices.emplaceBack(gx, gy, gz, uvX, 1.0 - uvY);
}
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);
this._texturedPoleNorthVertexBuffer = context.createVertexBuffer(texturedNorthVertices, globeLayoutAttributes, false);
this._texturedPoleSouthVertexBuffer = context.createVertexBuffer(texturedSouthVertices, globeLayoutAttributes, false);
}
getGridBuffers(latitudinalLod: number, withSkirts: boolean): [VertexBuffer, IndexBuffer, SegmentVector] {
return [this._gridBuffer, this._gridIndexBuffer, withSkirts ? this._gridSegments[latitudinalLod].withSkirts : this._gridSegments[latitudinalLod].withoutSkirts];
}
getPoleBuffers(z: number, textured: boolean): [VertexBuffer, VertexBuffer, IndexBuffer, SegmentVector] {
return [
textured ? this._texturedPoleNorthVertexBuffer : this._poleNorthVertexBuffer,
textured ? this._texturedPoleSouthVertexBuffer : this._poleSouthVertexBuffer,
this._poleIndexBuffer,
this._poleSegments[z]
];
}
}