mapbox-gl
Version:
A WebGL interactive maps library
348 lines (291 loc) • 13.3 kB
JavaScript
// @flow
import MercatorCoordinate, {mercatorZfromAltitude, latFromMercatorY} from '../geo/mercator_coordinate.js';
import {degToRad, wrap, getColumn, setColumn} from '../util/util.js';
import {vec3, quat, mat4} from 'gl-matrix';
import type {Elevation} from '../terrain/elevation.js';
import type {LngLatLike} from '../geo/lng_lat.js';
import type {Mat4, Vec3, Vec4, Quat} from 'gl-matrix';
function updateTransformOrientation(matrix: Mat4, orientation: Quat) {
// Take temporary copy of position to prevent it from being overwritten
const position: Vec4 = getColumn(matrix, 3);
// Convert quaternion to rotation matrix
mat4.fromQuat(matrix, orientation);
setColumn(matrix, 3, position);
}
function updateTransformPosition(matrix: Mat4, position: Vec3) {
setColumn(matrix, 3, [position[0], position[1], position[2], 1.0]);
}
function orientationFromPitchBearing(pitch: number, bearing: number): Quat {
// Both angles are considered to define CW rotation around their respective axes.
// Values have to be negated to achieve the proper quaternion in left handed coordinate space
const orientation = quat.identity([]);
quat.rotateZ(orientation, orientation, -bearing);
quat.rotateX(orientation, orientation, -pitch);
return orientation;
}
export function orientationFromFrame(forward: Vec3, up: Vec3): ?Quat {
// Find right-vector of the resulting coordinate frame. Up-vector has to be
// sanitized first in order to remove the roll component from the orientation
const xyForward = [forward[0], forward[1], 0];
const xyUp = [up[0], up[1], 0];
const epsilon = 1e-15;
if (vec3.length(xyForward) >= epsilon) {
// Roll rotation can be seen as the right vector not being on the xy-plane, ie. right[2] != 0.0.
// It can be negated by projecting the up vector on top of the forward vector.
const xyDir = vec3.normalize([], xyForward);
vec3.scale(xyUp, xyDir, vec3.dot(xyUp, xyDir));
up[0] = xyUp[0];
up[1] = xyUp[1];
}
const right = vec3.cross([], up, forward);
if (vec3.len(right) < epsilon) {
return null;
}
const bearing = Math.atan2(-right[1], right[0]);
const pitch = Math.atan2(Math.sqrt(forward[0] * forward[0] + forward[1] * forward[1]), -forward[2]);
return orientationFromPitchBearing(pitch, bearing);
}
/**
* Options for accessing physical properties of the underlying camera entity.
* Direct access to these properties allows more flexible and precise controlling of the camera.
* These options are also fully compatible and interchangeable with CameraOptions. All fields are optional.
* See {@link Map#setFreeCameraOptions} and {@link Map#getFreeCameraOptions}.
*
* @param {MercatorCoordinate} position Position of the camera in slightly modified web mercator coordinates.
- The size of 1 unit is the width of the projected world instead of the "mercator meter".
Coordinate [0, 0, 0] is the north-west corner and [1, 1, 0] is the south-east corner.
- Z coordinate is conformal and must respect minimum and maximum zoom values.
- Zoom is automatically computed from the altitude (z).
* @param {quat} orientation Orientation of the camera represented as a unit quaternion [x, y, z, w] in a left-handed coordinate space.
Direction of the rotation is clockwise around the respective axis.
The default pose of the camera is such that the forward vector is looking up the -Z axis.
The up vector is aligned with north orientation of the map:
forward: [0, 0, -1]
up: [0, -1, 0]
right [1, 0, 0]
Orientation can be set freely but certain constraints still apply:
- Orientation must be representable with only pitch and bearing.
- Pitch has an upper limit
* @example
* const camera = map.getFreeCameraOptions();
*
* const position = [138.72649, 35.33974];
* const altitude = 3000;
*
* camera.position = mapboxgl.MercatorCoordinate.fromLngLat(position, altitude);
* camera.lookAtPoint([138.73036, 35.36197]);
*
* map.setFreeCameraOptions(camera);
* @see [Example: Animate the camera around a point in 3D terrain](https://docs.mapbox.com/mapbox-gl-js/example/free-camera-point/)
* @see [Example: Animate the camera along a path](https://docs.mapbox.com/mapbox-gl-js/example/free-camera-path/)
*/
class FreeCameraOptions {
orientation: ?Quat;
_position: ?MercatorCoordinate;
_elevation: ?Elevation;
_renderWorldCopies: boolean;
constructor(position: ?MercatorCoordinate, orientation: ?Quat) {
this.position = position;
this.orientation = orientation;
}
get position(): ?MercatorCoordinate {
return this._position;
}
set position(position: ?MercatorCoordinate | Vec3) {
if (!position) {
this._position = null;
} else {
const mercatorCoordinate = position instanceof MercatorCoordinate ? position : new MercatorCoordinate(position[0], position[1], position[2]);
if (this._renderWorldCopies) {
mercatorCoordinate.x = wrap(mercatorCoordinate.x, 0, 1);
}
this._position = mercatorCoordinate;
}
}
/**
* Helper function for setting orientation of the camera by defining a focus point
* on the map.
*
* @param {LngLatLike} location Location of the focus point on the map.
* @param {vec3?} up Up vector of the camera is necessary in certain scenarios where bearing can't be deduced
* from the viewing direction.
* @example
* const camera = map.getFreeCameraOptions();
*
* const position = [138.72649, 35.33974];
* const altitude = 3000;
*
* camera.position = mapboxgl.MercatorCoordinate.fromLngLat(position, altitude);
* camera.lookAtPoint([138.73036, 35.36197]);
* // Apply camera changes
* map.setFreeCameraOptions(camera);
*/
lookAtPoint(location: LngLatLike, up?: Vec3) {
this.orientation = null;
if (!this.position) {
return;
}
const pos: MercatorCoordinate = this.position;
const altitude = this._elevation ? this._elevation.getAtPointOrZero(MercatorCoordinate.fromLngLat(location)) : 0;
const target = MercatorCoordinate.fromLngLat(location, altitude);
const forward = [target.x - pos.x, target.y - pos.y, target.z - pos.z];
if (!up)
up = [0, 0, 1];
// flip z-component if the up vector is pointing downwards
up[2] = Math.abs(up[2]);
this.orientation = orientationFromFrame(forward, up);
}
/**
* Helper function for setting the orientation of the camera as a pitch and a bearing.
*
* @param {number} pitch Pitch angle in degrees.
* @param {number} bearing Bearing angle in degrees.
* @example
* const camera = map.getFreeCameraOptions();
*
* // Update camera pitch and bearing
* camera.setPitchBearing(80, 90);
* // Apply changes
* map.setFreeCameraOptions(camera);
*/
setPitchBearing(pitch: number, bearing: number) {
this.orientation = orientationFromPitchBearing(degToRad(pitch), degToRad(-bearing));
}
}
/**
* While using the free camera API the outcome value of isZooming, isMoving and isRotating
* is not a result of the free camera API.
* If the user sets the map.interactive to true, there will be conflicting behaviors while
* interacting with map via zooming or moving using mouse or/and keyboard which will result
* in isZooming, isMoving and isRotating to return true while using free camera API. In order
* to prevent the confilicting behavior please set map.interactive to false which will result
* in muting the following events: zoom, zoomend, zoomstart, rotate, rotateend, rotatestart,
* move, moveend, movestart, pitch, pitchend, pitchstart.
*/
class FreeCamera {
_transform: Mat4;
_orientation: Quat;
constructor(position: ?Vec3, orientation: ?Quat) {
this._transform = mat4.identity([]);
this.orientation = orientation;
this.position = position;
}
get mercatorPosition(): MercatorCoordinate {
const pos = this.position;
return new MercatorCoordinate(pos[0], pos[1], pos[2]);
}
get position(): Vec3 {
const col: Vec4 = getColumn(this._transform, 3);
return [col[0], col[1], col[2]];
}
set position(value: ?Vec3) {
if (value) {
updateTransformPosition(this._transform, value);
}
}
get orientation(): Quat {
return this._orientation;
}
set orientation(value: ?Quat) {
this._orientation = value || quat.identity([]);
if (value) {
updateTransformOrientation(this._transform, this._orientation);
}
}
getPitchBearing(): {pitch: number, bearing: number} {
const f = this.forward();
const r = this.right();
return {
bearing: Math.atan2(-r[1], r[0]),
pitch: Math.atan2(Math.sqrt(f[0] * f[0] + f[1] * f[1]), -f[2])
};
}
setPitchBearing(pitch: number, bearing: number) {
this._orientation = orientationFromPitchBearing(pitch, bearing);
updateTransformOrientation(this._transform, this._orientation);
}
forward(): Vec3 {
const col: Vec4 = getColumn(this._transform, 2);
// Forward direction is towards the negative Z-axis
return [-col[0], -col[1], -col[2]];
}
up(): Vec3 {
const col: Vec4 = getColumn(this._transform, 1);
// Up direction has to be flipped to point towards north
return [-col[0], -col[1], -col[2]];
}
right(): Vec3 {
const col: Vec4 = getColumn(this._transform, 0);
return [col[0], col[1], col[2]];
}
getCameraToWorld(worldSize: number, pixelsPerMeter: number): Float64Array {
const cameraToWorld = new Float64Array(16);
mat4.invert(cameraToWorld, this.getWorldToCamera(worldSize, pixelsPerMeter));
return cameraToWorld;
}
getCameraToWorldMercator(): Mat4 {
return this._transform;
}
getWorldToCameraPosition(worldSize: number, pixelsPerMeter: number, uniformScale: number): Float64Array {
const invPosition = this.position;
vec3.scale(invPosition, invPosition, -worldSize);
const matrix = new Float64Array(16);
mat4.fromScaling(matrix, [uniformScale, uniformScale, uniformScale]);
mat4.translate(matrix, matrix, invPosition);
// Adjust scale on z (3rd column 3rd row)
matrix[10] *= pixelsPerMeter;
return matrix;
}
getWorldToCamera(worldSize: number, pixelsPerMeter: number): Float64Array {
// transformation chain from world space to camera space:
// 1. Height value (z) of renderables is in meters. Scale z coordinate by pixelsPerMeter
// 2. Transform from pixel coordinates to camera space with cameraMatrix^-1
// 3. flip Y if required
// worldToCamera: flip * cam^-1 * zScale
// cameraToWorld: (flip * cam^-1 * zScale)^-1 => (zScale^-1 * cam * flip^-1)
const matrix = new Float64Array(16);
// Compute inverse of camera matrix and post-multiply negated translation
const invOrientation = new Float64Array(4);
const invPosition = this.position;
quat.conjugate(invOrientation, this._orientation);
vec3.scale(invPosition, invPosition, -worldSize);
mat4.fromQuat(matrix, invOrientation);
mat4.translate(matrix, matrix, invPosition);
// Pre-multiply y (2nd row)
matrix[1] *= -1.0;
matrix[5] *= -1.0;
matrix[9] *= -1.0;
matrix[13] *= -1.0;
// Post-multiply z (3rd column)
matrix[8] *= pixelsPerMeter;
matrix[9] *= pixelsPerMeter;
matrix[10] *= pixelsPerMeter;
matrix[11] *= pixelsPerMeter;
return matrix;
}
getCameraToClipPerspective(fovy: number, aspectRatio: number, nearZ: number, farZ: number): Float64Array {
const matrix = new Float64Array(16);
mat4.perspective(matrix, fovy, aspectRatio, nearZ, farZ);
return matrix;
}
getCameraToClipOrthographic(left: number, right: number, bottom: number, top: number, nearZ: number, farZ: number): Float64Array {
const matrix = new Float64Array(16);
mat4.ortho(matrix, left, right, bottom, top, nearZ, farZ);
return matrix;
}
// The additional parameter needs to be removed. This was introduced because originally
// the value returned by this function was incorrect. Fixing it would break the fog visuals and needs to be
// communicated carefully first. Also see transform.cameraWorldSizeForFog.
getDistanceToElevation(elevationMeters: number, convert: boolean = false): number {
const z0 = elevationMeters === 0 ? 0 : mercatorZfromAltitude(elevationMeters, convert ? latFromMercatorY(this.position[1]) : this.position[1]);
const f = this.forward();
return (z0 - this.position[2]) / f[2];
}
clone(): FreeCamera {
return new FreeCamera([...this.position], [...this.orientation]);
}
}
export {
FreeCamera,
FreeCameraOptions
};