UNPKG

mapbox-gl

Version:
348 lines (291 loc) 13.3 kB
// @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 };