@deck.gl/core
Version:
deck.gl core library
245 lines (208 loc) • 8.4 kB
text/typescript
// deck.gl
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors
import {Matrix4} from '@math.gl/core';
import Viewport from './viewport';
import {PROJECTION_MODE} from '../lib/constants';
import {altitudeToFovy, fovyToAltitude} from '@math.gl/web-mercator';
import {MAX_LATITUDE} from '@math.gl/web-mercator';
import {vec3, vec4} from '@math.gl/core';
const DEGREES_TO_RADIANS = Math.PI / 180;
const RADIANS_TO_DEGREES = 180 / Math.PI;
const EARTH_RADIUS = 6370972;
const GLOBE_RADIUS = 256;
function getDistanceScales() {
const unitsPerMeter = GLOBE_RADIUS / EARTH_RADIUS;
const unitsPerDegree = (Math.PI / 180) * GLOBE_RADIUS;
return {
unitsPerMeter: [unitsPerMeter, unitsPerMeter, unitsPerMeter],
unitsPerMeter2: [0, 0, 0],
metersPerUnit: [1 / unitsPerMeter, 1 / unitsPerMeter, 1 / unitsPerMeter],
unitsPerDegree: [unitsPerDegree, unitsPerDegree, unitsPerMeter],
unitsPerDegree2: [0, 0, 0],
degreesPerUnit: [1 / unitsPerDegree, 1 / unitsPerDegree, 1 / unitsPerMeter]
};
}
export type GlobeViewportOptions = {
/** Name of the viewport */
id?: string;
/** Left offset from the canvas edge, in pixels */
x?: number;
/** Top offset from the canvas edge, in pixels */
y?: number;
/** Viewport width in pixels */
width?: number;
/** Viewport height in pixels */
height?: number;
/** Longitude in degrees */
longitude?: number;
/** Latitude in degrees */
latitude?: number;
/** Camera altitude relative to the viewport height, used to control the FOV. Default `1.5` */
altitude?: number;
/* Meter offsets of the viewport center from lng, lat, elevation */
position?: number[];
/** Zoom level */
zoom?: number;
/** Use orthographic projection */
orthographic?: boolean;
/** Camera fovy in degrees. If provided, overrides `altitude` */
fovy?: number;
/** Scaler for the near plane, 1 unit equals to the height of the viewport. Default `0.5` */
nearZMultiplier?: number;
/** Scaler for the far plane, 1 unit equals to the distance from the camera to the edge of the screen. Default `1` */
farZMultiplier?: number;
/** Optionally override the near plane position. `nearZMultiplier` is ignored if `nearZ` is supplied. */
nearZ?: number;
/** Optionally override the far plane position. `farZMultiplier` is ignored if `farZ` is supplied. */
farZ?: number;
/** The resolution at which to turn flat features into 3D meshes, in degrees. Smaller numbers will generate more detailed mesh. Default `10` */
resolution?: number;
};
export default class GlobeViewport extends Viewport {
longitude!: number;
latitude!: number;
resolution!: number;
constructor(opts: GlobeViewportOptions = {}) {
const {
longitude = 0,
zoom = 0,
// Matches Maplibre defaults
// https://github.com/maplibre/maplibre-gl-js/blob/f8ab4b48d59ab8fe7b068b102538793bbdd4c848/src/geo/projection/globe_transform.ts#L632-L633
nearZMultiplier = 0.5,
farZMultiplier = 1,
resolution = 10
} = opts;
let {latitude = 0, height, altitude = 1.5, fovy} = opts;
// Clamp to web mercator limit to prevent bad inputs
latitude = Math.max(Math.min(latitude, MAX_LATITUDE), -MAX_LATITUDE);
height = height || 1;
if (fovy) {
altitude = fovyToAltitude(fovy);
} else {
fovy = altitudeToFovy(altitude);
}
// Exagerate distance by latitude to match the Web Mercator distortion
// The goal is that globe and web mercator projection results converge at high zoom
// https://github.com/maplibre/maplibre-gl-js/blob/f8ab4b48d59ab8fe7b068b102538793bbdd4c848/src/geo/projection/globe_transform.ts#L575-L577
const scaleAdjust = 1 / Math.PI / Math.cos((latitude * Math.PI) / 180);
const scale = Math.pow(2, zoom) * scaleAdjust;
const nearZ = opts.nearZ ?? nearZMultiplier;
const farZ = opts.farZ ?? (altitude + (GLOBE_RADIUS * 2 * scale) / height) * farZMultiplier;
// Calculate view matrix
const viewMatrix = new Matrix4().lookAt({eye: [0, -altitude, 0], up: [0, 0, 1]});
viewMatrix.rotateX(latitude * DEGREES_TO_RADIANS);
viewMatrix.rotateZ(-longitude * DEGREES_TO_RADIANS);
viewMatrix.scale(scale / height);
super({
...opts,
// x, y, width,
height,
// view matrix
viewMatrix,
longitude,
latitude,
zoom,
// projection matrix parameters
distanceScales: getDistanceScales(),
fovy,
focalDistance: altitude,
near: nearZ,
far: farZ
});
this.scale = scale;
this.latitude = latitude;
this.longitude = longitude;
this.resolution = resolution;
}
get projectionMode() {
return PROJECTION_MODE.GLOBE;
}
getDistanceScales() {
return this.distanceScales;
}
getBounds(options: {z?: number} = {}): [number, number, number, number] {
const unprojectOption = {targetZ: options.z || 0};
const left = this.unproject([0, this.height / 2], unprojectOption);
const top = this.unproject([this.width / 2, 0], unprojectOption);
const right = this.unproject([this.width, this.height / 2], unprojectOption);
const bottom = this.unproject([this.width / 2, this.height], unprojectOption);
if (right[0] < this.longitude) right[0] += 360;
if (left[0] > this.longitude) left[0] -= 360;
return [
Math.min(left[0], right[0], top[0], bottom[0]),
Math.min(left[1], right[1], top[1], bottom[1]),
Math.max(left[0], right[0], top[0], bottom[0]),
Math.max(left[1], right[1], top[1], bottom[1])
];
}
unproject(
xyz: number[],
{topLeft = true, targetZ}: {topLeft?: boolean; targetZ?: number} = {}
): number[] {
const [x, y, z] = xyz;
const y2 = topLeft ? y : this.height - y;
const {pixelUnprojectionMatrix} = this;
let coord;
if (Number.isFinite(z)) {
// Has depth component
coord = transformVector(pixelUnprojectionMatrix, [x, y2, z, 1]);
} else {
// since we don't know the correct projected z value for the point,
// unproject two points to get a line and then find the point on that line that intersects with the sphere
const coord0 = transformVector(pixelUnprojectionMatrix, [x, y2, -1, 1]);
const coord1 = transformVector(pixelUnprojectionMatrix, [x, y2, 1, 1]);
const lt = ((targetZ || 0) / EARTH_RADIUS + 1) * GLOBE_RADIUS;
const lSqr = vec3.sqrLen(vec3.sub([], coord0, coord1));
const l0Sqr = vec3.sqrLen(coord0);
const l1Sqr = vec3.sqrLen(coord1);
const sSqr = (4 * l0Sqr * l1Sqr - (lSqr - l0Sqr - l1Sqr) ** 2) / 16;
const dSqr = (4 * sSqr) / lSqr;
const r0 = Math.sqrt(l0Sqr - dSqr);
const dr = Math.sqrt(Math.max(0, lt * lt - dSqr));
const t = (r0 - dr) / Math.sqrt(lSqr);
coord = vec3.lerp([], coord0, coord1, t);
}
const [X, Y, Z] = this.unprojectPosition(coord);
if (Number.isFinite(z)) {
return [X, Y, Z];
}
return Number.isFinite(targetZ) ? [X, Y, targetZ as number] : [X, Y];
}
projectPosition(xyz: number[]): [number, number, number] {
const [lng, lat, Z = 0] = xyz;
const lambda = lng * DEGREES_TO_RADIANS;
const phi = lat * DEGREES_TO_RADIANS;
const cosPhi = Math.cos(phi);
const D = (Z / EARTH_RADIUS + 1) * GLOBE_RADIUS;
return [Math.sin(lambda) * cosPhi * D, -Math.cos(lambda) * cosPhi * D, Math.sin(phi) * D];
}
unprojectPosition(xyz: number[]): [number, number, number] {
const [x, y, z] = xyz;
const D = vec3.len(xyz);
const phi = Math.asin(z / D);
const lambda = Math.atan2(x, -y);
const lng = lambda * RADIANS_TO_DEGREES;
const lat = phi * RADIANS_TO_DEGREES;
const Z = (D / GLOBE_RADIUS - 1) * EARTH_RADIUS;
return [lng, lat, Z];
}
projectFlat(xyz: number[]): [number, number] {
return xyz as [number, number];
}
unprojectFlat(xyz: number[]): [number, number] {
return xyz as [number, number];
}
panByPosition(coords: number[], pixel: number[]): GlobeViewportOptions {
const fromPosition = this.unproject(pixel);
return {
longitude: coords[0] - fromPosition[0] + this.longitude,
latitude: coords[1] - fromPosition[1] + this.latitude
};
}
}
function transformVector(matrix: number[], vector: number[]): number[] {
const result = vec4.transformMat4([], vector, matrix);
vec4.scale(result, result, 1 / result[3]);
return result;
}