mapbox-gl
Version:
A WebGL interactive maps library
191 lines (155 loc) • 8.23 kB
JavaScript
// @flow
import {mat4, vec3, vec4} from 'gl-matrix';
import {Ray} from '../../util/primitives.js';
import EXTENT from '../../data/extent.js';
import LngLat from '../lng_lat.js';
import {degToRad, radToDeg, getColumn, shortestAngle, clamp} from '../../util/util.js';
import MercatorCoordinate, {
lngFromMercatorX,
latFromMercatorY,
mercatorZfromAltitude,
mercatorXfromLng,
mercatorYfromLat
} from '../mercator_coordinate.js';
import Mercator from './mercator.js';
import Point from '@mapbox/point-geometry';
import {farthestPixelDistanceOnPlane, farthestPixelDistanceOnSphere} from './far_z.js';
import {number as interpolate} from '../../style-spec/util/interpolate.js';
import {
GLOBE_RADIUS,
latLngToECEF,
globeTileBounds,
globeNormalizeECEF,
globeDenormalizeECEF,
globeECEFUnitsToPixelScale,
globeECEFNormalizationScale,
globeToMercatorTransition
} from './globe_util.js';
import type Transform from '../transform.js';
import type {ElevationScale} from './projection.js';
import type {Vec3} from 'gl-matrix';
import type {ProjectionSpecification} from '../../style-spec/types.js';
import type {CanonicalTileID, UnwrappedTileID} from '../../source/tile_id.js';
export const GLOBE_METERS_TO_ECEF = mercatorZfromAltitude(1, 0.0) * 2.0 * GLOBE_RADIUS * Math.PI;
export default class Globe extends Mercator {
constructor(options: ProjectionSpecification) {
super(options);
this.requiresDraping = true;
this.supportsWorldCopies = false;
this.supportsFog = false;
this.zAxisUnit = "pixels";
this.unsupportedLayers = ['debug', 'custom'];
}
projectTilePoint(x: number, y: number, id: CanonicalTileID): {x: number, y: number, z: 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);
const bounds = globeTileBounds(id);
const normalizationMatrix = globeNormalizeECEF(bounds);
vec3.transformMat4(pos, pos, normalizationMatrix);
return {x: pos[0], y: pos[1], z: pos[2]};
}
locationPoint(tr: Transform, lngLat: LngLat): Point {
const pos = latLngToECEF(lngLat.lat, lngLat.lng);
const up = vec3.normalize([], pos);
const elevation = tr.elevation ?
tr.elevation.getAtPointOrZero(tr.locationCoordinate(lngLat), tr._centerAltitude) :
tr._centerAltitude;
const upScale = mercatorZfromAltitude(1, 0) * EXTENT * elevation;
vec3.scaleAndAdd(pos, pos, up, upScale);
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]);
}
pixelsPerMeter(lat: number, worldSize: number): number {
return mercatorZfromAltitude(1, 0) * worldSize;
}
createTileMatrix(tr: Transform, worldSize: number, id: UnwrappedTileID): Float64Array {
const decode = globeDenormalizeECEF(globeTileBounds(id.canonical));
return mat4.multiply(new Float64Array(16), tr.globeMatrix, decode);
}
createInversionMatrix(tr: Transform, id: CanonicalTileID): Float32Array {
const {center, worldSize} = tr;
const ecefUnitsToPixels = globeECEFUnitsToPixelScale(worldSize);
const matrix = mat4.identity(new Float64Array(16));
const encode = globeNormalizeECEF(globeTileBounds(id));
mat4.multiply(matrix, matrix, encode);
mat4.rotateY(matrix, matrix, degToRad(center.lng));
mat4.rotateX(matrix, matrix, degToRad(center.lat));
mat4.scale(matrix, matrix, [1.0 / ecefUnitsToPixels, 1.0 / ecefUnitsToPixels, 1.0]);
const ecefUnitsToMercatorPixels = tr.pixelsPerMeter / mercatorZfromAltitude(1.0, center.lat) / EXTENT;
mat4.scale(matrix, matrix, [ecefUnitsToMercatorPixels, ecefUnitsToMercatorPixels, 1.0]);
return Float32Array.from(matrix);
}
pointCoordinate(tr: Transform, x: number, y: number, _: number): 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 this scenario we'll "clamp" the ray
// to the surface of the sphere, ie. 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) {
// 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);
}
farthestPixelDistance(tr: Transform): number {
const pixelsPerMeter = this.pixelsPerMeter(tr.center.lat, tr.worldSize);
const globePixelDistance = farthestPixelDistanceOnSphere(tr, pixelsPerMeter);
const t = globeToMercatorTransition(tr.zoom);
if (t > 0.0) {
const mercatorPixelsPerMeter = mercatorZfromAltitude(1, tr.center.lat) * tr.worldSize;
const mercatorPixelDistance = farthestPixelDistanceOnPlane(tr, mercatorPixelsPerMeter);
return interpolate(globePixelDistance, mercatorPixelDistance, t);
}
return globePixelDistance;
}
upVector(id: CanonicalTileID, x: number, y: number): Vec3 {
const tiles = 1 << id.z;
const mercX = (x / EXTENT + id.x) / tiles;
const mercY = (y / EXTENT + id.y) / tiles;
return latLngToECEF(latFromMercatorY(mercY), lngFromMercatorX(mercX), 1.0);
}
upVectorScale(id: CanonicalTileID, latitude: number, worldSize: number): ElevationScale {
const pixelsPerMeterAtLat = mercatorZfromAltitude(1, latitude) * worldSize;
return {metersToTile: GLOBE_METERS_TO_ECEF * globeECEFNormalizationScale(globeTileBounds(id)), metersToLabelSpace: pixelsPerMeterAtLat};
}
}