import BoundingSphere from "../Core/BoundingSphere.js"; import Cartesian2 from "../Core/Cartesian2.js"; import Cartesian3 from "../Core/Cartesian3.js"; import Cartesian4 from "../Core/Cartesian4.js"; import Cartographic from "../Core/Cartographic.js"; import defaultValue from "../Core/defaultValue.js"; import defined from "../Core/defined.js"; import DeveloperError from "../Core/DeveloperError.js"; import EasingFunction from "../Core/EasingFunction.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import EllipsoidGeodesic from "../Core/EllipsoidGeodesic.js"; import Event from "../Core/Event.js"; import getTimestamp from "../Core/getTimestamp.js"; import HeadingPitchRange from "../Core/HeadingPitchRange.js"; import HeadingPitchRoll from "../Core/HeadingPitchRoll.js"; import Intersect from "../Core/Intersect.js"; import IntersectionTests from "../Core/IntersectionTests.js"; import CesiumMath from "../Core/Math.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import OrthographicFrustum from "../Core/OrthographicFrustum.js"; import OrthographicOffCenterFrustum from "../Core/OrthographicOffCenterFrustum.js"; import PerspectiveFrustum from "../Core/PerspectiveFrustum.js"; import Quaternion from "../Core/Quaternion.js"; import Ray from "../Core/Ray.js"; import Rectangle from "../Core/Rectangle.js"; import Transforms from "../Core/Transforms.js"; import CameraFlightPath from "./CameraFlightPath.js"; import MapMode2D from "./MapMode2D.js"; import SceneMode from "./SceneMode.js"; /** * @typedef {object} DirectionUp * * An orientation given by a pair of unit vectors * * @property {Cartesian3} direction The unit "direction" vector * @property {Cartesian3} up The unit "up" vector **/ /** * @typedef {object} HeadingPitchRollValues * * An orientation given by numeric heading, pitch, and roll * * @property {number} [heading=0.0] The heading in radians * @property {number} [pitch=-CesiumMath.PI_OVER_TWO] The pitch in radians * @property {number} [roll=0.0] The roll in meters **/ /** * The camera is defined by a position, orientation, and view frustum. *

* The orientation forms an orthonormal basis with a view, up and right = view x up unit vectors. *

* The viewing frustum is defined by 6 planes. * Each plane is represented by a {@link Cartesian4} object, where the x, y, and z components * define the unit vector normal to the plane, and the w component is the distance of the * plane from the origin/camera position. * * @alias Camera * * @constructor * * @param {Scene} scene The scene. * * @demo {@link https://sandcastle.cesium.com/index.html?src=Camera.html|Cesium Sandcastle Camera Demo} * @demo {@link https://sandcastle.cesium.com/index.html?src=Camera%20Tutorial.html|Cesium Sandcastle Camera Tutorial Example} * @demo {@link https://cesium.com/learn/cesiumjs-learn/cesiumjs-camera|Camera Tutorial} * * @example * // Create a camera looking down the negative z-axis, positioned at the origin, * // with a field of view of 60 degrees, and 1:1 aspect ratio. * const camera = new Cesium.Camera(scene); * camera.position = new Cesium.Cartesian3(); * camera.direction = Cesium.Cartesian3.negate(Cesium.Cartesian3.UNIT_Z, new Cesium.Cartesian3()); * camera.up = Cesium.Cartesian3.clone(Cesium.Cartesian3.UNIT_Y); * camera.frustum.fov = Cesium.Math.PI_OVER_THREE; * camera.frustum.near = 1.0; * camera.frustum.far = 2.0; */ function Camera(scene) { //>>includeStart('debug', pragmas.debug); if (!defined(scene)) { throw new DeveloperError("scene is required."); } //>>includeEnd('debug'); this._scene = scene; this._transform = Matrix4.clone(Matrix4.IDENTITY); this._invTransform = Matrix4.clone(Matrix4.IDENTITY); this._actualTransform = Matrix4.clone(Matrix4.IDENTITY); this._actualInvTransform = Matrix4.clone(Matrix4.IDENTITY); this._transformChanged = false; /** * The position of the camera. * * @type {Cartesian3} */ this.position = new Cartesian3(); this._position = new Cartesian3(); this._positionWC = new Cartesian3(); this._positionCartographic = new Cartographic(); this._oldPositionWC = undefined; /** * The position delta magnitude. * * @private */ this.positionWCDeltaMagnitude = 0.0; /** * The position delta magnitude last frame. * * @private */ this.positionWCDeltaMagnitudeLastFrame = 0.0; /** * How long in seconds since the camera has stopped moving * * @private */ this.timeSinceMoved = 0.0; this._lastMovedTimestamp = 0.0; /** * The view direction of the camera. * * @type {Cartesian3} */ this.direction = new Cartesian3(); this._direction = new Cartesian3(); this._directionWC = new Cartesian3(); /** * The up direction of the camera. * * @type {Cartesian3} */ this.up = new Cartesian3(); this._up = new Cartesian3(); this._upWC = new Cartesian3(); /** * The right direction of the camera. * * @type {Cartesian3} */ this.right = new Cartesian3(); this._right = new Cartesian3(); this._rightWC = new Cartesian3(); /** * The region of space in view. * * @type {PerspectiveFrustum|PerspectiveOffCenterFrustum|OrthographicFrustum} * @default PerspectiveFrustum() * * @see PerspectiveFrustum * @see PerspectiveOffCenterFrustum * @see OrthographicFrustum */ this.frustum = new PerspectiveFrustum(); this.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; this.frustum.fov = CesiumMath.toRadians(60.0); /** * The default amount to move the camera when an argument is not * provided to the move methods. * @type {number} * @default 100000.0; */ this.defaultMoveAmount = 100000.0; /** * The default amount to rotate the camera when an argument is not * provided to the look methods. * @type {number} * @default Math.PI / 60.0 */ this.defaultLookAmount = Math.PI / 60.0; /** * The default amount to rotate the camera when an argument is not * provided to the rotate methods. * @type {number} * @default Math.PI / 3600.0 */ this.defaultRotateAmount = Math.PI / 3600.0; /** * The default amount to move the camera when an argument is not * provided to the zoom methods. * @type {number} * @default 100000.0; */ this.defaultZoomAmount = 100000.0; /** * If set, the camera will not be able to rotate past this axis in either direction. * @type {Cartesian3} * @default undefined */ this.constrainedAxis = undefined; /** * The factor multiplied by the the map size used to determine where to clamp the camera position * when zooming out from the surface. The default is 1.5. Only valid for 2D and the map is rotatable. * @type {number} * @default 1.5 */ this.maximumZoomFactor = 1.5; this._moveStart = new Event(); this._moveEnd = new Event(); this._changed = new Event(); this._changedPosition = undefined; this._changedDirection = undefined; this._changedFrustum = undefined; this._changedHeading = undefined; /** * The amount the camera has to change before the changed event is raised. The value is a percentage in the [0, 1] range. * @type {number} * @default 0.5 */ this.percentageChanged = 0.5; this._viewMatrix = new Matrix4(); this._invViewMatrix = new Matrix4(); updateViewMatrix(this); this._mode = SceneMode.SCENE3D; this._modeChanged = true; const projection = scene.mapProjection; this._projection = projection; this._maxCoord = projection.project( new Cartographic(Math.PI, CesiumMath.PI_OVER_TWO) ); this._max2Dfrustum = undefined; // set default view rectangleCameraPosition3D( this, Camera.DEFAULT_VIEW_RECTANGLE, this.position, true ); let mag = Cartesian3.magnitude(this.position); mag += mag * Camera.DEFAULT_VIEW_FACTOR; Cartesian3.normalize(this.position, this.position); Cartesian3.multiplyByScalar(this.position, mag, this.position); } /** * @private */ Camera.TRANSFORM_2D = new Matrix4( 0.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0 ); /** * @private */ Camera.TRANSFORM_2D_INVERSE = Matrix4.inverseTransformation( Camera.TRANSFORM_2D, new Matrix4() ); /** * The default rectangle the camera will view on creation. * @type Rectangle */ Camera.DEFAULT_VIEW_RECTANGLE = Rectangle.fromDegrees( -95.0, -20.0, -70.0, 90.0 ); /** * A scalar to multiply to the camera position and add it back after setting the camera to view the rectangle. * A value of zero means the camera will view the entire {@link Camera#DEFAULT_VIEW_RECTANGLE}, a value greater than zero * will move it further away from the extent, and a value less than zero will move it close to the extent. * @type {number} */ Camera.DEFAULT_VIEW_FACTOR = 0.5; /** * The default heading/pitch/range that is used when the camera flies to a location that contains a bounding sphere. * @type HeadingPitchRange */ Camera.DEFAULT_OFFSET = new HeadingPitchRange( 0.0, -CesiumMath.PI_OVER_FOUR, 0.0 ); function updateViewMatrix(camera) { Matrix4.computeView( camera._position, camera._direction, camera._up, camera._right, camera._viewMatrix ); Matrix4.multiply( camera._viewMatrix, camera._actualInvTransform, camera._viewMatrix ); Matrix4.inverseTransformation(camera._viewMatrix, camera._invViewMatrix); } function updateCameraDeltas(camera) { if (!defined(camera._oldPositionWC)) { camera._oldPositionWC = Cartesian3.clone( camera.positionWC, camera._oldPositionWC ); } else { camera.positionWCDeltaMagnitudeLastFrame = camera.positionWCDeltaMagnitude; const delta = Cartesian3.subtract( camera.positionWC, camera._oldPositionWC, camera._oldPositionWC ); camera.positionWCDeltaMagnitude = Cartesian3.magnitude(delta); camera._oldPositionWC = Cartesian3.clone( camera.positionWC, camera._oldPositionWC ); // Update move timers if (camera.positionWCDeltaMagnitude > 0.0) { camera.timeSinceMoved = 0.0; camera._lastMovedTimestamp = getTimestamp(); } else { camera.timeSinceMoved = Math.max(getTimestamp() - camera._lastMovedTimestamp, 0.0) / 1000.0; } } } /** * Checks if there's a camera flight with preload for this camera. * * @returns {boolean} Whether or not this camera has a current flight with a valid preloadFlightCamera in scene. * * @private * */ Camera.prototype.canPreloadFlight = function () { return defined(this._currentFlight) && this._mode !== SceneMode.SCENE2D; }; Camera.prototype._updateCameraChanged = function () { const camera = this; updateCameraDeltas(camera); if (camera._changed.numberOfListeners === 0) { return; } const percentageChanged = camera.percentageChanged; const currentHeading = camera.heading; if (!defined(camera._changedHeading)) { camera._changedHeading = currentHeading; } let delta = Math.abs(camera._changedHeading - currentHeading) % CesiumMath.TWO_PI; delta = delta > CesiumMath.PI ? CesiumMath.TWO_PI - delta : delta; // Since delta is computed as the shortest distance between two angles // the percentage is relative to the half circle. const headingChangedPercentage = delta / Math.PI; if (headingChangedPercentage > percentageChanged) { camera._changed.raiseEvent(headingChangedPercentage); camera._changedHeading = currentHeading; } if (camera._mode === SceneMode.SCENE2D) { if (!defined(camera._changedFrustum)) { camera._changedPosition = Cartesian3.clone( camera.position, camera._changedPosition ); camera._changedFrustum = camera.frustum.clone(); return; } const position = camera.position; const lastPosition = camera._changedPosition; const frustum = camera.frustum; const lastFrustum = camera._changedFrustum; const x0 = position.x + frustum.left; const x1 = position.x + frustum.right; const x2 = lastPosition.x + lastFrustum.left; const x3 = lastPosition.x + lastFrustum.right; const y0 = position.y + frustum.bottom; const y1 = position.y + frustum.top; const y2 = lastPosition.y + lastFrustum.bottom; const y3 = lastPosition.y + lastFrustum.top; const leftX = Math.max(x0, x2); const rightX = Math.min(x1, x3); const bottomY = Math.max(y0, y2); const topY = Math.min(y1, y3); let areaPercentage; if (leftX >= rightX || bottomY >= y1) { areaPercentage = 1.0; } else { let areaRef = lastFrustum; if (x0 < x2 && x1 > x3 && y0 < y2 && y1 > y3) { areaRef = frustum; } areaPercentage = 1.0 - ((rightX - leftX) * (topY - bottomY)) / ((areaRef.right - areaRef.left) * (areaRef.top - areaRef.bottom)); } if (areaPercentage > percentageChanged) { camera._changed.raiseEvent(areaPercentage); camera._changedPosition = Cartesian3.clone( camera.position, camera._changedPosition ); camera._changedFrustum = camera.frustum.clone(camera._changedFrustum); } return; } if (!defined(camera._changedDirection)) { camera._changedPosition = Cartesian3.clone( camera.positionWC, camera._changedPosition ); camera._changedDirection = Cartesian3.clone( camera.directionWC, camera._changedDirection ); return; } const dirAngle = CesiumMath.acosClamped( Cartesian3.dot(camera.directionWC, camera._changedDirection) ); let dirPercentage; if (defined(camera.frustum.fovy)) { dirPercentage = dirAngle / (camera.frustum.fovy * 0.5); } else { dirPercentage = dirAngle; } const distance = Cartesian3.distance( camera.positionWC, camera._changedPosition ); const heightPercentage = distance / camera.positionCartographic.height; if ( dirPercentage > percentageChanged || heightPercentage > percentageChanged ) { camera._changed.raiseEvent(Math.max(dirPercentage, heightPercentage)); camera._changedPosition = Cartesian3.clone( camera.positionWC, camera._changedPosition ); camera._changedDirection = Cartesian3.clone( camera.directionWC, camera._changedDirection ); } }; function convertTransformForColumbusView(camera) { Transforms.basisTo2D( camera._projection, camera._transform, camera._actualTransform ); } const scratchCartographic = new Cartographic(); const scratchCartesian3Projection = new Cartesian3(); const scratchCartesian3 = new Cartesian3(); const scratchCartesian4Origin = new Cartesian4(); const scratchCartesian4NewOrigin = new Cartesian4(); const scratchCartesian4NewXAxis = new Cartesian4(); const scratchCartesian4NewYAxis = new Cartesian4(); const scratchCartesian4NewZAxis = new Cartesian4(); function convertTransformFor2D(camera) { const projection = camera._projection; const ellipsoid = projection.ellipsoid; const origin = Matrix4.getColumn( camera._transform, 3, scratchCartesian4Origin ); const cartographic = ellipsoid.cartesianToCartographic( origin, scratchCartographic ); const projectedPosition = projection.project( cartographic, scratchCartesian3Projection ); const newOrigin = scratchCartesian4NewOrigin; newOrigin.x = projectedPosition.z; newOrigin.y = projectedPosition.x; newOrigin.z = projectedPosition.y; newOrigin.w = 1.0; const newZAxis = Cartesian4.clone( Cartesian4.UNIT_X, scratchCartesian4NewZAxis ); const xAxis = Cartesian4.add( Matrix4.getColumn(camera._transform, 0, scratchCartesian3), origin, scratchCartesian3 ); ellipsoid.cartesianToCartographic(xAxis, cartographic); projection.project(cartographic, projectedPosition); const newXAxis = scratchCartesian4NewXAxis; newXAxis.x = projectedPosition.z; newXAxis.y = projectedPosition.x; newXAxis.z = projectedPosition.y; newXAxis.w = 0.0; Cartesian3.subtract(newXAxis, newOrigin, newXAxis); newXAxis.x = 0.0; const newYAxis = scratchCartesian4NewYAxis; if (Cartesian3.magnitudeSquared(newXAxis) > CesiumMath.EPSILON10) { Cartesian3.cross(newZAxis, newXAxis, newYAxis); } else { const yAxis = Cartesian4.add( Matrix4.getColumn(camera._transform, 1, scratchCartesian3), origin, scratchCartesian3 ); ellipsoid.cartesianToCartographic(yAxis, cartographic); projection.project(cartographic, projectedPosition); newYAxis.x = projectedPosition.z; newYAxis.y = projectedPosition.x; newYAxis.z = projectedPosition.y; newYAxis.w = 0.0; Cartesian3.subtract(newYAxis, newOrigin, newYAxis); newYAxis.x = 0.0; if (Cartesian3.magnitudeSquared(newYAxis) < CesiumMath.EPSILON10) { Cartesian4.clone(Cartesian4.UNIT_Y, newXAxis); Cartesian4.clone(Cartesian4.UNIT_Z, newYAxis); } } Cartesian3.cross(newYAxis, newZAxis, newXAxis); Cartesian3.normalize(newXAxis, newXAxis); Cartesian3.cross(newZAxis, newXAxis, newYAxis); Cartesian3.normalize(newYAxis, newYAxis); Matrix4.setColumn( camera._actualTransform, 0, newXAxis, camera._actualTransform ); Matrix4.setColumn( camera._actualTransform, 1, newYAxis, camera._actualTransform ); Matrix4.setColumn( camera._actualTransform, 2, newZAxis, camera._actualTransform ); Matrix4.setColumn( camera._actualTransform, 3, newOrigin, camera._actualTransform ); } const scratchCartesian = new Cartesian3(); function updateMembers(camera) { const mode = camera._mode; let heightChanged = false; let height = 0.0; if (mode === SceneMode.SCENE2D) { height = camera.frustum.right - camera.frustum.left; heightChanged = height !== camera._positionCartographic.height; } let position = camera._position; const positionChanged = !Cartesian3.equals(position, camera.position) || heightChanged; if (positionChanged) { position = Cartesian3.clone(camera.position, camera._position); } let direction = camera._direction; const directionChanged = !Cartesian3.equals(direction, camera.direction); if (directionChanged) { Cartesian3.normalize(camera.direction, camera.direction); direction = Cartesian3.clone(camera.direction, camera._direction); } let up = camera._up; const upChanged = !Cartesian3.equals(up, camera.up); if (upChanged) { Cartesian3.normalize(camera.up, camera.up); up = Cartesian3.clone(camera.up, camera._up); } let right = camera._right; const rightChanged = !Cartesian3.equals(right, camera.right); if (rightChanged) { Cartesian3.normalize(camera.right, camera.right); right = Cartesian3.clone(camera.right, camera._right); } const transformChanged = camera._transformChanged || camera._modeChanged; camera._transformChanged = false; if (transformChanged) { Matrix4.inverseTransformation(camera._transform, camera._invTransform); if ( camera._mode === SceneMode.COLUMBUS_VIEW || camera._mode === SceneMode.SCENE2D ) { if (Matrix4.equals(Matrix4.IDENTITY, camera._transform)) { Matrix4.clone(Camera.TRANSFORM_2D, camera._actualTransform); } else if (camera._mode === SceneMode.COLUMBUS_VIEW) { convertTransformForColumbusView(camera); } else { convertTransformFor2D(camera); } } else { Matrix4.clone(camera._transform, camera._actualTransform); } Matrix4.inverseTransformation( camera._actualTransform, camera._actualInvTransform ); camera._modeChanged = false; } const transform = camera._actualTransform; if (positionChanged || transformChanged) { camera._positionWC = Matrix4.multiplyByPoint( transform, position, camera._positionWC ); // Compute the Cartographic position of the camera. if (mode === SceneMode.SCENE3D || mode === SceneMode.MORPHING) { camera._positionCartographic = camera._projection.ellipsoid.cartesianToCartographic( camera._positionWC, camera._positionCartographic ); } else { // The camera position is expressed in the 2D coordinate system where the Y axis is to the East, // the Z axis is to the North, and the X axis is out of the map. Express them instead in the ENU axes where // X is to the East, Y is to the North, and Z is out of the local horizontal plane. const positionENU = scratchCartesian; positionENU.x = camera._positionWC.y; positionENU.y = camera._positionWC.z; positionENU.z = camera._positionWC.x; // In 2D, the camera height is always 12.7 million meters. // The apparent height is equal to half the frustum width. if (mode === SceneMode.SCENE2D) { positionENU.z = height; } camera._projection.unproject(positionENU, camera._positionCartographic); } } if (directionChanged || upChanged || rightChanged) { const det = Cartesian3.dot( direction, Cartesian3.cross(up, right, scratchCartesian) ); if (Math.abs(1.0 - det) > CesiumMath.EPSILON2) { //orthonormalize axes const invUpMag = 1.0 / Cartesian3.magnitudeSquared(up); const scalar = Cartesian3.dot(up, direction) * invUpMag; const w0 = Cartesian3.multiplyByScalar( direction, scalar, scratchCartesian ); up = Cartesian3.normalize( Cartesian3.subtract(up, w0, camera._up), camera._up ); Cartesian3.clone(up, camera.up); right = Cartesian3.cross(direction, up, camera._right); Cartesian3.clone(right, camera.right); } } if (directionChanged || transformChanged) { camera._directionWC = Matrix4.multiplyByPointAsVector( transform, direction, camera._directionWC ); Cartesian3.normalize(camera._directionWC, camera._directionWC); } if (upChanged || transformChanged) { camera._upWC = Matrix4.multiplyByPointAsVector(transform, up, camera._upWC); Cartesian3.normalize(camera._upWC, camera._upWC); } if (rightChanged || transformChanged) { camera._rightWC = Matrix4.multiplyByPointAsVector( transform, right, camera._rightWC ); Cartesian3.normalize(camera._rightWC, camera._rightWC); } if ( positionChanged || directionChanged || upChanged || rightChanged || transformChanged ) { updateViewMatrix(camera); } } function getHeading(direction, up) { let heading; if ( !CesiumMath.equalsEpsilon(Math.abs(direction.z), 1.0, CesiumMath.EPSILON3) ) { heading = Math.atan2(direction.y, direction.x) - CesiumMath.PI_OVER_TWO; } else { heading = Math.atan2(up.y, up.x) - CesiumMath.PI_OVER_TWO; } return CesiumMath.TWO_PI - CesiumMath.zeroToTwoPi(heading); } function getPitch(direction) { return CesiumMath.PI_OVER_TWO - CesiumMath.acosClamped(direction.z); } function getRoll(direction, up, right) { let roll = 0.0; if ( !CesiumMath.equalsEpsilon(Math.abs(direction.z), 1.0, CesiumMath.EPSILON3) ) { roll = Math.atan2(-right.z, up.z); roll = CesiumMath.zeroToTwoPi(roll + CesiumMath.TWO_PI); } return roll; } const scratchHPRMatrix1 = new Matrix4(); const scratchHPRMatrix2 = new Matrix4(); Object.defineProperties(Camera.prototype, { /** * Gets the camera's reference frame. The inverse of this transformation is appended to the view matrix. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @default {@link Matrix4.IDENTITY} */ transform: { get: function () { return this._transform; }, }, /** * Gets the inverse camera transform. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @default {@link Matrix4.IDENTITY} */ inverseTransform: { get: function () { updateMembers(this); return this._invTransform; }, }, /** * Gets the view matrix. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @see Camera#inverseViewMatrix */ viewMatrix: { get: function () { updateMembers(this); return this._viewMatrix; }, }, /** * Gets the inverse view matrix. * @memberof Camera.prototype * * @type {Matrix4} * @readonly * * @see Camera#viewMatrix */ inverseViewMatrix: { get: function () { updateMembers(this); return this._invViewMatrix; }, }, /** * Gets the {@link Cartographic} position of the camera, with longitude and latitude * expressed in radians and height in meters. In 2D and Columbus View, it is possible * for the returned longitude and latitude to be outside the range of valid longitudes * and latitudes when the camera is outside the map. * @memberof Camera.prototype * * @type {Cartographic} * @readonly */ positionCartographic: { get: function () { updateMembers(this); return this._positionCartographic; }, }, /** * Gets the position of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ positionWC: { get: function () { updateMembers(this); return this._positionWC; }, }, /** * Gets the view direction of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ directionWC: { get: function () { updateMembers(this); return this._directionWC; }, }, /** * Gets the up direction of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ upWC: { get: function () { updateMembers(this); return this._upWC; }, }, /** * Gets the right direction of the camera in world coordinates. * @memberof Camera.prototype * * @type {Cartesian3} * @readonly */ rightWC: { get: function () { updateMembers(this); return this._rightWC; }, }, /** * Gets the camera heading in radians. * @memberof Camera.prototype * * @type {number} * @readonly */ heading: { get: function () { if (this._mode !== SceneMode.MORPHING) { const ellipsoid = this._projection.ellipsoid; const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1); const transform = Transforms.eastNorthUpToFixedFrame( this.positionWC, ellipsoid, scratchHPRMatrix2 ); this._setTransform(transform); const heading = getHeading(this.direction, this.up); this._setTransform(oldTransform); return heading; } return undefined; }, }, /** * Gets the camera pitch in radians. * @memberof Camera.prototype * * @type {number} * @readonly */ pitch: { get: function () { if (this._mode !== SceneMode.MORPHING) { const ellipsoid = this._projection.ellipsoid; const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1); const transform = Transforms.eastNorthUpToFixedFrame( this.positionWC, ellipsoid, scratchHPRMatrix2 ); this._setTransform(transform); const pitch = getPitch(this.direction); this._setTransform(oldTransform); return pitch; } return undefined; }, }, /** * Gets the camera roll in radians. * @memberof Camera.prototype * * @type {number} * @readonly */ roll: { get: function () { if (this._mode !== SceneMode.MORPHING) { const ellipsoid = this._projection.ellipsoid; const oldTransform = Matrix4.clone(this._transform, scratchHPRMatrix1); const transform = Transforms.eastNorthUpToFixedFrame( this.positionWC, ellipsoid, scratchHPRMatrix2 ); this._setTransform(transform); const roll = getRoll(this.direction, this.up, this.right); this._setTransform(oldTransform); return roll; } return undefined; }, }, /** * Gets the event that will be raised at when the camera starts to move. * @memberof Camera.prototype * @type {Event} * @readonly */ moveStart: { get: function () { return this._moveStart; }, }, /** * Gets the event that will be raised when the camera has stopped moving. * @memberof Camera.prototype * @type {Event} * @readonly */ moveEnd: { get: function () { return this._moveEnd; }, }, /** * Gets the event that will be raised when the camera has changed by percentageChanged. * @memberof Camera.prototype * @type {Event} * @readonly */ changed: { get: function () { return this._changed; }, }, }); /** * @private */ Camera.prototype.update = function (mode) { //>>includeStart('debug', pragmas.debug); if (!defined(mode)) { throw new DeveloperError("mode is required."); } if ( mode === SceneMode.SCENE2D && !(this.frustum instanceof OrthographicOffCenterFrustum) ) { throw new DeveloperError( "An OrthographicOffCenterFrustum is required in 2D." ); } if ( (mode === SceneMode.SCENE3D || mode === SceneMode.COLUMBUS_VIEW) && !(this.frustum instanceof PerspectiveFrustum) && !(this.frustum instanceof OrthographicFrustum) ) { throw new DeveloperError( "A PerspectiveFrustum or OrthographicFrustum is required in 3D and Columbus view" ); } //>>includeEnd('debug'); let updateFrustum = false; if (mode !== this._mode) { this._mode = mode; this._modeChanged = mode !== SceneMode.MORPHING; updateFrustum = this._mode === SceneMode.SCENE2D; } if (updateFrustum) { const frustum = (this._max2Dfrustum = this.frustum.clone()); //>>includeStart('debug', pragmas.debug); if (!(frustum instanceof OrthographicOffCenterFrustum)) { throw new DeveloperError( "The camera frustum is expected to be orthographic for 2D camera control." ); } //>>includeEnd('debug'); const maxZoomOut = 2.0; const ratio = frustum.top / frustum.right; frustum.right = this._maxCoord.x * maxZoomOut; frustum.left = -frustum.right; frustum.top = ratio * frustum.right; frustum.bottom = -frustum.top; } if (this._mode === SceneMode.SCENE2D) { clampMove2D(this, this.position); } }; const setTransformPosition = new Cartesian3(); const setTransformUp = new Cartesian3(); const setTransformDirection = new Cartesian3(); Camera.prototype._setTransform = function (transform) { const position = Cartesian3.clone(this.positionWC, setTransformPosition); const up = Cartesian3.clone(this.upWC, setTransformUp); const direction = Cartesian3.clone(this.directionWC, setTransformDirection); Matrix4.clone(transform, this._transform); this._transformChanged = true; updateMembers(this); const inverse = this._actualInvTransform; Matrix4.multiplyByPoint(inverse, position, this.position); Matrix4.multiplyByPointAsVector(inverse, direction, this.direction); Matrix4.multiplyByPointAsVector(inverse, up, this.up); Cartesian3.cross(this.direction, this.up, this.right); updateMembers(this); }; const scratchAdjustOrthographicFrustumMousePosition = new Cartesian2(); const scratchPickRay = new Ray(); const scratchRayIntersection = new Cartesian3(); const scratchDepthIntersection = new Cartesian3(); function calculateOrthographicFrustumWidth(camera) { // Camera is fixed to an object, so keep frustum width constant. if (!Matrix4.equals(Matrix4.IDENTITY, camera.transform)) { return Cartesian3.magnitude(camera.position); } const scene = camera._scene; const globe = scene.globe; const mousePosition = scratchAdjustOrthographicFrustumMousePosition; mousePosition.x = scene.drawingBufferWidth / 2.0; mousePosition.y = scene.drawingBufferHeight / 2.0; let rayIntersection; if (defined(globe)) { const ray = camera.getPickRay(mousePosition, scratchPickRay); rayIntersection = globe.pickWorldCoordinates( ray, scene, true, scratchRayIntersection ); } let depthIntersection; if (scene.pickPositionSupported) { depthIntersection = scene.pickPositionWorldCoordinates( mousePosition, scratchDepthIntersection ); } let distance; if (defined(rayIntersection) || defined(depthIntersection)) { const depthDistance = defined(depthIntersection) ? Cartesian3.distance(depthIntersection, camera.positionWC) : Number.POSITIVE_INFINITY; const rayDistance = defined(rayIntersection) ? Cartesian3.distance(rayIntersection, camera.positionWC) : Number.POSITIVE_INFINITY; distance = Math.min(depthDistance, rayDistance); } else { distance = Math.max(camera.positionCartographic.height, 0.0); } return distance; } Camera.prototype._adjustOrthographicFrustum = function (zooming) { if (!(this.frustum instanceof OrthographicFrustum)) { return; } if (!zooming && this._positionCartographic.height < 150000.0) { return; } this.frustum.width = calculateOrthographicFrustumWidth(this); }; const scratchSetViewCartesian = new Cartesian3(); const scratchSetViewTransform1 = new Matrix4(); const scratchSetViewTransform2 = new Matrix4(); const scratchSetViewQuaternion = new Quaternion(); const scratchSetViewMatrix3 = new Matrix3(); const scratchSetViewCartographic = new Cartographic(); function setView3D(camera, position, hpr) { const currentTransform = Matrix4.clone( camera.transform, scratchSetViewTransform1 ); const localTransform = Transforms.eastNorthUpToFixedFrame( position, camera._projection.ellipsoid, scratchSetViewTransform2 ); camera._setTransform(localTransform); Cartesian3.clone(Cartesian3.ZERO, camera.position); hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; const rotQuat = Quaternion.fromHeadingPitchRoll( hpr, scratchSetViewQuaternion ); const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); Matrix3.getColumn(rotMat, 0, camera.direction); Matrix3.getColumn(rotMat, 2, camera.up); Cartesian3.cross(camera.direction, camera.up, camera.right); camera._setTransform(currentTransform); camera._adjustOrthographicFrustum(true); } function setViewCV(camera, position, hpr, convert) { const currentTransform = Matrix4.clone( camera.transform, scratchSetViewTransform1 ); camera._setTransform(Matrix4.IDENTITY); if (!Cartesian3.equals(position, camera.positionWC)) { if (convert) { const projection = camera._projection; const cartographic = projection.ellipsoid.cartesianToCartographic( position, scratchSetViewCartographic ); position = projection.project(cartographic, scratchSetViewCartesian); } Cartesian3.clone(position, camera.position); } hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; const rotQuat = Quaternion.fromHeadingPitchRoll( hpr, scratchSetViewQuaternion ); const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); Matrix3.getColumn(rotMat, 0, camera.direction); Matrix3.getColumn(rotMat, 2, camera.up); Cartesian3.cross(camera.direction, camera.up, camera.right); camera._setTransform(currentTransform); camera._adjustOrthographicFrustum(true); } function setView2D(camera, position, hpr, convert) { const currentTransform = Matrix4.clone( camera.transform, scratchSetViewTransform1 ); camera._setTransform(Matrix4.IDENTITY); if (!Cartesian3.equals(position, camera.positionWC)) { if (convert) { const projection = camera._projection; const cartographic = projection.ellipsoid.cartesianToCartographic( position, scratchSetViewCartographic ); position = projection.project(cartographic, scratchSetViewCartesian); } Cartesian2.clone(position, camera.position); const newLeft = -position.z * 0.5; const newRight = -newLeft; const frustum = camera.frustum; if (newRight > newLeft) { const ratio = frustum.top / frustum.right; frustum.right = newRight; frustum.left = newLeft; frustum.top = frustum.right * ratio; frustum.bottom = -frustum.top; } } if (camera._scene.mapMode2D === MapMode2D.ROTATE) { hpr.heading = hpr.heading - CesiumMath.PI_OVER_TWO; hpr.pitch = -CesiumMath.PI_OVER_TWO; hpr.roll = 0.0; const rotQuat = Quaternion.fromHeadingPitchRoll( hpr, scratchSetViewQuaternion ); const rotMat = Matrix3.fromQuaternion(rotQuat, scratchSetViewMatrix3); Matrix3.getColumn(rotMat, 2, camera.up); Cartesian3.cross(camera.direction, camera.up, camera.right); } camera._setTransform(currentTransform); } const scratchToHPRDirection = new Cartesian3(); const scratchToHPRUp = new Cartesian3(); const scratchToHPRRight = new Cartesian3(); function directionUpToHeadingPitchRoll(camera, position, orientation, result) { const direction = Cartesian3.clone( orientation.direction, scratchToHPRDirection ); const up = Cartesian3.clone(orientation.up, scratchToHPRUp); if (camera._scene.mode === SceneMode.SCENE3D) { const ellipsoid = camera._projection.ellipsoid; const transform = Transforms.eastNorthUpToFixedFrame( position, ellipsoid, scratchHPRMatrix1 ); const invTransform = Matrix4.inverseTransformation( transform, scratchHPRMatrix2 ); Matrix4.multiplyByPointAsVector(invTransform, direction, direction); Matrix4.multiplyByPointAsVector(invTransform, up, up); } const right = Cartesian3.cross(direction, up, scratchToHPRRight); result.heading = getHeading(direction, up); result.pitch = getPitch(direction); result.roll = getRoll(direction, up, right); return result; } const scratchSetViewOptions = { destination: undefined, orientation: { direction: undefined, up: undefined, heading: undefined, pitch: undefined, roll: undefined, }, convert: undefined, endTransform: undefined, }; const scratchHpr = new HeadingPitchRoll(); /** * Sets the camera position, orientation and transform. * * @param {object} options Object with the following properties: * @param {Cartesian3|Rectangle} [options.destination] The final position of the camera in WGS84 (world) coordinates or a rectangle that would be visible from a top-down view. * @param {HeadingPitchRollValues|DirectionUp} [options.orientation] An object that contains either direction and up properties or heading, pitch and roll properties. By default, the direction will point * towards the center of the frame in 3D and in the negative z direction in Columbus view. The up direction will point towards local north in 3D and in the positive * y direction in Columbus view. Orientation is not used in 2D when in infinite scrolling mode. * @param {Matrix4} [options.endTransform] Transform matrix representing the reference frame of the camera. * @param {boolean} [options.convert] Whether to convert the destination from world coordinates to scene coordinates (only relevant when not using 3D). Defaults to true. * * @example * // 1. Set position with a top-down view * viewer.camera.setView({ * destination : Cesium.Cartesian3.fromDegrees(-117.16, 32.71, 15000.0) * }); * * // 2 Set view with heading, pitch and roll * viewer.camera.setView({ * destination : cartesianPosition, * orientation: { * heading : Cesium.Math.toRadians(90.0), // east, default value is 0.0 (north) * pitch : Cesium.Math.toRadians(-90), // default value (looking down) * roll : 0.0 // default value * } * }); * * // 3. Change heading, pitch and roll with the camera position remaining the same. * viewer.camera.setView({ * orientation: { * heading : Cesium.Math.toRadians(90.0), // east, default value is 0.0 (north) * pitch : Cesium.Math.toRadians(-90), // default value (looking down) * roll : 0.0 // default value * } * }); * * * // 4. View rectangle with a top-down view * viewer.camera.setView({ * destination : Cesium.Rectangle.fromDegrees(west, south, east, north) * }); * * // 5. Set position with an orientation using unit vectors. * viewer.camera.setView({ * destination : Cesium.Cartesian3.fromDegrees(-122.19, 46.25, 5000.0), * orientation : { * direction : new Cesium.Cartesian3(-0.04231243104240401, -0.20123236049443421, -0.97862924300734), * up : new Cesium.Cartesian3(-0.47934589305293746, -0.8553216253114552, 0.1966022179118339) * } * }); */ Camera.prototype.setView = function (options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); let orientation = defaultValue( options.orientation, defaultValue.EMPTY_OBJECT ); const mode = this._mode; if (mode === SceneMode.MORPHING) { return; } if (defined(options.endTransform)) { this._setTransform(options.endTransform); } let convert = defaultValue(options.convert, true); let destination = defaultValue( options.destination, Cartesian3.clone(this.positionWC, scratchSetViewCartesian) ); if (defined(destination) && defined(destination.west)) { destination = this.getRectangleCameraCoordinates( destination, scratchSetViewCartesian ); convert = false; } if (defined(orientation.direction)) { orientation = directionUpToHeadingPitchRoll( this, destination, orientation, scratchSetViewOptions.orientation ); } scratchHpr.heading = defaultValue(orientation.heading, 0.0); scratchHpr.pitch = defaultValue(orientation.pitch, -CesiumMath.PI_OVER_TWO); scratchHpr.roll = defaultValue(orientation.roll, 0.0); if (mode === SceneMode.SCENE3D) { setView3D(this, destination, scratchHpr); } else if (mode === SceneMode.SCENE2D) { setView2D(this, destination, scratchHpr, convert); } else { setViewCV(this, destination, scratchHpr, convert); } }; const pitchScratch = new Cartesian3(); /** * Fly the camera to the home view. Use {@link Camera#.DEFAULT_VIEW_RECTANGLE} to set * the default view for the 3D scene. The home view for 2D and columbus view shows the * entire map. * * @param {number} [duration] The duration of the flight in seconds. If omitted, Cesium attempts to calculate an ideal duration based on the distance to be traveled by the flight. See {@link Camera#flyTo} */ Camera.prototype.flyHome = function (duration) { const mode = this._mode; if (mode === SceneMode.MORPHING) { this._scene.completeMorph(); } if (mode === SceneMode.SCENE2D) { this.flyTo({ destination: Camera.DEFAULT_VIEW_RECTANGLE, duration: duration, endTransform: Matrix4.IDENTITY, }); } else if (mode === SceneMode.SCENE3D) { const destination = this.getRectangleCameraCoordinates( Camera.DEFAULT_VIEW_RECTANGLE ); let mag = Cartesian3.magnitude(destination); mag += mag * Camera.DEFAULT_VIEW_FACTOR; Cartesian3.normalize(destination, destination); Cartesian3.multiplyByScalar(destination, mag, destination); this.flyTo({ destination: destination, duration: duration, endTransform: Matrix4.IDENTITY, }); } else if (mode === SceneMode.COLUMBUS_VIEW) { const maxRadii = this._projection.ellipsoid.maximumRadius; let position = new Cartesian3(0.0, -1.0, 1.0); position = Cartesian3.multiplyByScalar( Cartesian3.normalize(position, position), 5.0 * maxRadii, position ); this.flyTo({ destination: position, duration: duration, orientation: { heading: 0.0, pitch: -Math.acos(Cartesian3.normalize(position, pitchScratch).z), roll: 0.0, }, endTransform: Matrix4.IDENTITY, convert: false, }); } }; /** * Transform a vector or point from world coordinates to the camera's reference frame. * * @param {Cartesian4} cartesian The vector or point to transform. * @param {Cartesian4} [result] The object onto which to store the result. * @returns {Cartesian4} The transformed vector or point. */ Camera.prototype.worldToCameraCoordinates = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian4(); } updateMembers(this); return Matrix4.multiplyByVector(this._actualInvTransform, cartesian, result); }; /** * Transform a point from world coordinates to the camera's reference frame. * * @param {Cartesian3} cartesian The point to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed point. */ Camera.prototype.worldToCameraCoordinatesPoint = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPoint(this._actualInvTransform, cartesian, result); }; /** * Transform a vector from world coordinates to the camera's reference frame. * * @param {Cartesian3} cartesian The vector to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed vector. */ Camera.prototype.worldToCameraCoordinatesVector = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPointAsVector( this._actualInvTransform, cartesian, result ); }; /** * Transform a vector or point from the camera's reference frame to world coordinates. * * @param {Cartesian4} cartesian The vector or point to transform. * @param {Cartesian4} [result] The object onto which to store the result. * @returns {Cartesian4} The transformed vector or point. */ Camera.prototype.cameraToWorldCoordinates = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian4(); } updateMembers(this); return Matrix4.multiplyByVector(this._actualTransform, cartesian, result); }; /** * Transform a point from the camera's reference frame to world coordinates. * * @param {Cartesian3} cartesian The point to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed point. */ Camera.prototype.cameraToWorldCoordinatesPoint = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPoint(this._actualTransform, cartesian, result); }; /** * Transform a vector from the camera's reference frame to world coordinates. * * @param {Cartesian3} cartesian The vector to transform. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3} The transformed vector. */ Camera.prototype.cameraToWorldCoordinatesVector = function (cartesian, result) { //>>includeStart('debug', pragmas.debug); if (!defined(cartesian)) { throw new DeveloperError("cartesian is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Cartesian3(); } updateMembers(this); return Matrix4.multiplyByPointAsVector( this._actualTransform, cartesian, result ); }; function clampMove2D(camera, position) { const rotatable2D = camera._scene.mapMode2D === MapMode2D.ROTATE; const maxProjectedX = camera._maxCoord.x; const maxProjectedY = camera._maxCoord.y; let minX; let maxX; if (rotatable2D) { maxX = maxProjectedX; minX = -maxX; } else { maxX = position.x - maxProjectedX * 2.0; minX = position.x + maxProjectedX * 2.0; } if (position.x > maxProjectedX) { position.x = maxX; } if (position.x < -maxProjectedX) { position.x = minX; } if (position.y > maxProjectedY) { position.y = maxProjectedY; } if (position.y < -maxProjectedY) { position.y = -maxProjectedY; } } const moveScratch = new Cartesian3(); /** * Translates the camera's position by amount along direction. * * @param {Cartesian3} direction The direction to move. * @param {number} [amount] The amount, in meters, to move. Defaults to defaultMoveAmount. * * @see Camera#moveBackward * @see Camera#moveForward * @see Camera#moveLeft * @see Camera#moveRight * @see Camera#moveUp * @see Camera#moveDown */ Camera.prototype.move = function (direction, amount) { //>>includeStart('debug', pragmas.debug); if (!defined(direction)) { throw new DeveloperError("direction is required."); } //>>includeEnd('debug'); const cameraPosition = this.position; Cartesian3.multiplyByScalar(direction, amount, moveScratch); Cartesian3.add(cameraPosition, moveScratch, cameraPosition); if (this._mode === SceneMode.SCENE2D) { clampMove2D(this, cameraPosition); } this._adjustOrthographicFrustum(true); }; /** * Translates the camera's position by amount along the camera's view vector. * When in 2D mode, this will zoom in the camera instead of translating the camera's position. * * @param {number} [amount] The amount, in meters, to move. Defaults to defaultMoveAmount. * * @see Camera#moveBackward */ Camera.prototype.moveForward = function (amount) { amount = defaultValue(amount, this.defaultMoveAmount); if (this._mode === SceneMode.SCENE2D) { // 2D mode zoom2D(this, amount); } else { // 3D or Columbus view mode this.move(this.direction, amount); } }; /** * Translates the camera's position by amount along the opposite direction * of the camera's view vector. * When in 2D mode, this will zoom out the camera instead of translating the camera's position. * * @param {number} [amount] The amount, in meters, to move. Defaults to defaultMoveAmount. * * @see Camera#moveForward */ Camera.prototype.moveBackward = function (amount) { amount = defaultValue(amount, this.defaultMoveAmount); if (this._mode === SceneMode.SCENE2D) { // 2D mode zoom2D(this, -amount); } else { // 3D or Columbus view mode this.move(this.direction, -amount); } }; /** * Translates the camera's position by amount along the camera's up vector. * * @param {number} [amount] The amount, in meters, to move. Defaults to defaultMoveAmount. * * @see Camera#moveDown */ Camera.prototype.moveUp = function (amount) { amount = defaultValue(amount, this.defaultMoveAmount); this.move(this.up, amount); }; /** * Translates the camera's position by amount along the opposite direction * of the camera's up vector. * * @param {number} [amount] The amount, in meters, to move. Defaults to defaultMoveAmount. * * @see Camera#moveUp */ Camera.prototype.moveDown = function (amount) { amount = defaultValue(amount, this.defaultMoveAmount); this.move(this.up, -amount); }; /** * Translates the camera's position by amount along the camera's right vector. * * @param {number} [amount] The amount, in meters, to move. Defaults to defaultMoveAmount. * * @see Camera#moveLeft */ Camera.prototype.moveRight = function (amount) { amount = defaultValue(amount, this.defaultMoveAmount); this.move(this.right, amount); }; /** * Translates the camera's position by amount along the opposite direction * of the camera's right vector. * * @param {number} [amount] The amount, in meters, to move. Defaults to defaultMoveAmount. * * @see Camera#moveRight */ Camera.prototype.moveLeft = function (amount) { amount = defaultValue(amount, this.defaultMoveAmount); this.move(this.right, -amount); }; /** * Rotates the camera around its up vector by amount, in radians, in the opposite direction * of its right vector if not in 2D mode. * * @param {number} [amount] The amount, in radians, to rotate by. Defaults to defaultLookAmount. * * @see Camera#lookRight */ Camera.prototype.lookLeft = function (amount) { amount = defaultValue(amount, this.defaultLookAmount); // only want view of map to change in 3D mode, 2D visual is incorrect when look changes if (this._mode !== SceneMode.SCENE2D) { this.look(this.up, -amount); } }; /** * Rotates the camera around its up vector by amount, in radians, in the direction * of its right vector if not in 2D mode. * * @param {number} [amount] The amount, in radians, to rotate by. Defaults to defaultLookAmount. * * @see Camera#lookLeft */ Camera.prototype.lookRight = function (amount) { amount = defaultValue(amount, this.defaultLookAmount); // only want view of map to change in 3D mode, 2D visual is incorrect when look changes if (this._mode !== SceneMode.SCENE2D) { this.look(this.up, amount); } }; /** * Rotates the camera around its right vector by amount, in radians, in the direction * of its up vector if not in 2D mode. * * @param {number} [amount] The amount, in radians, to rotate by. Defaults to defaultLookAmount. * * @see Camera#lookDown */ Camera.prototype.lookUp = function (amount) { amount = defaultValue(amount, this.defaultLookAmount); // only want view of map to change in 3D mode, 2D visual is incorrect when look changes if (this._mode !== SceneMode.SCENE2D) { this.look(this.right, -amount); } }; /** * Rotates the camera around its right vector by amount, in radians, in the opposite direction * of its up vector if not in 2D mode. * * @param {number} [amount] The amount, in radians, to rotate by. Defaults to defaultLookAmount. * * @see Camera#lookUp */ Camera.prototype.lookDown = function (amount) { amount = defaultValue(amount, this.defaultLookAmount); // only want view of map to change in 3D mode, 2D visual is incorrect when look changes if (this._mode !== SceneMode.SCENE2D) { this.look(this.right, amount); } }; const lookScratchQuaternion = new Quaternion(); const lookScratchMatrix = new Matrix3(); /** * Rotate each of the camera's orientation vectors around axis by angle * * @param {Cartesian3} axis The axis to rotate around. * @param {number} [angle] The angle, in radians, to rotate by. Defaults to defaultLookAmount. * * @see Camera#lookUp * @see Camera#lookDown * @see Camera#lookLeft * @see Camera#lookRight */ Camera.prototype.look = function (axis, angle) { //>>includeStart('debug', pragmas.debug); if (!defined(axis)) { throw new DeveloperError("axis is required."); } //>>includeEnd('debug'); const turnAngle = defaultValue(angle, this.defaultLookAmount); const quaternion = Quaternion.fromAxisAngle( axis, -turnAngle, lookScratchQuaternion ); const rotation = Matrix3.fromQuaternion(quaternion, lookScratchMatrix); const direction = this.direction; const up = this.up; const right = this.right; Matrix3.multiplyByVector(rotation, direction, direction); Matrix3.multiplyByVector(rotation, up, up); Matrix3.multiplyByVector(rotation, right, right); }; /** * Rotate the camera counter-clockwise around its direction vector by amount, in radians. * * @param {number} [amount] The amount, in radians, to rotate by. Defaults to defaultLookAmount. * * @see Camera#twistRight */ Camera.prototype.twistLeft = function (amount) { amount = defaultValue(amount, this.defaultLookAmount); this.look(this.direction, amount); }; /** * Rotate the camera clockwise around its direction vector by amount, in radians. * * @param {number} [amount] The amount, in radians, to rotate by. Defaults to defaultLookAmount. * * @see Camera#twistLeft */ Camera.prototype.twistRight = function (amount) { amount = defaultValue(amount, this.defaultLookAmount); this.look(this.direction, -amount); }; const rotateScratchQuaternion = new Quaternion(); const rotateScratchMatrix = new Matrix3(); /** * Rotates the camera around axis by angle. The distance * of the camera's position to the center of the camera's reference frame remains the same. * * @param {Cartesian3} axis The axis to rotate around given in world coordinates. * @param {number} [angle] The angle, in radians, to rotate by. Defaults to defaultRotateAmount. * * @see Camera#rotateUp * @see Camera#rotateDown * @see Camera#rotateLeft * @see Camera#rotateRight */ Camera.prototype.rotate = function (axis, angle) { //>>includeStart('debug', pragmas.debug); if (!defined(axis)) { throw new DeveloperError("axis is required."); } //>>includeEnd('debug'); const turnAngle = defaultValue(angle, this.defaultRotateAmount); const quaternion = Quaternion.fromAxisAngle( axis, -turnAngle, rotateScratchQuaternion ); const rotation = Matrix3.fromQuaternion(quaternion, rotateScratchMatrix); Matrix3.multiplyByVector(rotation, this.position, this.position); Matrix3.multiplyByVector(rotation, this.direction, this.direction); Matrix3.multiplyByVector(rotation, this.up, this.up); Cartesian3.cross(this.direction, this.up, this.right); Cartesian3.cross(this.right, this.direction, this.up); this._adjustOrthographicFrustum(false); }; /** * Rotates the camera around the center of the camera's reference frame by angle downwards. * * @param {number} [angle] The angle, in radians, to rotate by. Defaults to defaultRotateAmount. * * @see Camera#rotateUp * @see Camera#rotate */ Camera.prototype.rotateDown = function (angle) { angle = defaultValue(angle, this.defaultRotateAmount); rotateVertical(this, angle); }; /** * Rotates the camera around the center of the camera's reference frame by angle upwards. * * @param {number} [angle] The angle, in radians, to rotate by. Defaults to defaultRotateAmount. * * @see Camera#rotateDown * @see Camera#rotate */ Camera.prototype.rotateUp = function (angle) { angle = defaultValue(angle, this.defaultRotateAmount); rotateVertical(this, -angle); }; const rotateVertScratchP = new Cartesian3(); const rotateVertScratchA = new Cartesian3(); const rotateVertScratchTan = new Cartesian3(); const rotateVertScratchNegate = new Cartesian3(); function rotateVertical(camera, angle) { const position = camera.position; if ( defined(camera.constrainedAxis) && !Cartesian3.equalsEpsilon( camera.position, Cartesian3.ZERO, CesiumMath.EPSILON2 ) ) { const p = Cartesian3.normalize(position, rotateVertScratchP); const northParallel = Cartesian3.equalsEpsilon( p, camera.constrainedAxis, CesiumMath.EPSILON2 ); const southParallel = Cartesian3.equalsEpsilon( p, Cartesian3.negate(camera.constrainedAxis, rotateVertScratchNegate), CesiumMath.EPSILON2 ); if (!northParallel && !southParallel) { const constrainedAxis = Cartesian3.normalize( camera.constrainedAxis, rotateVertScratchA ); let dot = Cartesian3.dot(p, constrainedAxis); let angleToAxis = CesiumMath.acosClamped(dot); if (angle > 0 && angle > angleToAxis) { angle = angleToAxis - CesiumMath.EPSILON4; } dot = Cartesian3.dot( p, Cartesian3.negate(constrainedAxis, rotateVertScratchNegate) ); angleToAxis = CesiumMath.acosClamped(dot); if (angle < 0 && -angle > angleToAxis) { angle = -angleToAxis + CesiumMath.EPSILON4; } const tangent = Cartesian3.cross( constrainedAxis, p, rotateVertScratchTan ); camera.rotate(tangent, angle); } else if ((northParallel && angle < 0) || (southParallel && angle > 0)) { camera.rotate(camera.right, angle); } } else { camera.rotate(camera.right, angle); } } /** * Rotates the camera around the center of the camera's reference frame by angle to the right. * * @param {number} [angle] The angle, in radians, to rotate by. Defaults to defaultRotateAmount. * * @see Camera#rotateLeft * @see Camera#rotate */ Camera.prototype.rotateRight = function (angle) { angle = defaultValue(angle, this.defaultRotateAmount); rotateHorizontal(this, -angle); }; /** * Rotates the camera around the center of the camera's reference frame by angle to the left. * * @param {number} [angle] The angle, in radians, to rotate by. Defaults to defaultRotateAmount. * * @see Camera#rotateRight * @see Camera#rotate */ Camera.prototype.rotateLeft = function (angle) { angle = defaultValue(angle, this.defaultRotateAmount); rotateHorizontal(this, angle); }; function rotateHorizontal(camera, angle) { if (defined(camera.constrainedAxis)) { camera.rotate(camera.constrainedAxis, angle); } else { camera.rotate(camera.up, angle); } } function zoom2D(camera, amount) { const frustum = camera.frustum; //>>includeStart('debug', pragmas.debug); if ( !(frustum instanceof OrthographicOffCenterFrustum) || !defined(frustum.left) || !defined(frustum.right) || !defined(frustum.bottom) || !defined(frustum.top) ) { throw new DeveloperError( "The camera frustum is expected to be orthographic for 2D camera control." ); } //>>includeEnd('debug'); let ratio; amount = amount * 0.5; if ( Math.abs(frustum.top) + Math.abs(frustum.bottom) > Math.abs(frustum.left) + Math.abs(frustum.right) ) { let newTop = frustum.top - amount; let newBottom = frustum.bottom + amount; let maxBottom = camera._maxCoord.y; if (camera._scene.mapMode2D === MapMode2D.ROTATE) { maxBottom *= camera.maximumZoomFactor; } if (newBottom > maxBottom) { newBottom = maxBottom; newTop = -maxBottom; } if (newTop <= newBottom) { newTop = 1.0; newBottom = -1.0; } ratio = frustum.right / frustum.top; frustum.top = newTop; frustum.bottom = newBottom; frustum.right = frustum.top * ratio; frustum.left = -frustum.right; } else { let newRight = frustum.right - amount; let newLeft = frustum.left + amount; let maxRight = camera._maxCoord.x; if (camera._scene.mapMode2D === MapMode2D.ROTATE) { maxRight *= camera.maximumZoomFactor; } if (newRight > maxRight) { newRight = maxRight; newLeft = -maxRight; } if (newRight <= newLeft) { newRight = 1.0; newLeft = -1.0; } ratio = frustum.top / frustum.right; frustum.right = newRight; frustum.left = newLeft; frustum.top = frustum.right * ratio; frustum.bottom = -frustum.top; } } function zoom3D(camera, amount) { camera.move(camera.direction, amount); } /** * Zooms amount along the camera's view vector. * * @param {number} [amount] The amount to move. Defaults to defaultZoomAmount. * * @see Camera#zoomOut */ Camera.prototype.zoomIn = function (amount) { amount = defaultValue(amount, this.defaultZoomAmount); if (this._mode === SceneMode.SCENE2D) { zoom2D(this, amount); } else { zoom3D(this, amount); } }; /** * Zooms amount along the opposite direction of * the camera's view vector. * * @param {number} [amount] The amount to move. Defaults to defaultZoomAmount. * * @see Camera#zoomIn */ Camera.prototype.zoomOut = function (amount) { amount = defaultValue(amount, this.defaultZoomAmount); if (this._mode === SceneMode.SCENE2D) { zoom2D(this, -amount); } else { zoom3D(this, -amount); } }; /** * Gets the magnitude of the camera position. In 3D, this is the vector magnitude. In 2D and * Columbus view, this is the distance to the map. * * @returns {number} The magnitude of the position. */ Camera.prototype.getMagnitude = function () { if (this._mode === SceneMode.SCENE3D) { return Cartesian3.magnitude(this.position); } else if (this._mode === SceneMode.COLUMBUS_VIEW) { return Math.abs(this.position.z); } else if (this._mode === SceneMode.SCENE2D) { return Math.max( this.frustum.right - this.frustum.left, this.frustum.top - this.frustum.bottom ); } }; const scratchLookAtMatrix4 = new Matrix4(); /** * Sets the camera position and orientation using a target and offset. The target must be given in * world coordinates. The offset can be either a cartesian or heading/pitch/range in the local east-north-up reference frame centered at the target. * If the offset is a cartesian, then it is an offset from the center of the reference frame defined by the transformation matrix. If the offset * is heading/pitch/range, then the heading and the pitch angles are defined in the reference frame defined by the transformation matrix. * The heading is the angle from y axis and increasing towards the x axis. Pitch is the rotation from the xy-plane. Positive pitch * angles are below the plane. Negative pitch angles are above the plane. The range is the distance from the center. * * In 2D, there must be a top down view. The camera will be placed above the target looking down. The height above the * target will be the magnitude of the offset. The heading will be determined from the offset. If the heading cannot be * determined from the offset, the heading will be north. * * @param {Cartesian3} target The target position in world coordinates. * @param {Cartesian3|HeadingPitchRange} offset The offset from the target in the local east-north-up reference frame centered at the target. * * @exception {DeveloperError} lookAt is not supported while morphing. * * @example * // 1. Using a cartesian offset * const center = Cesium.Cartesian3.fromDegrees(-98.0, 40.0); * viewer.camera.lookAt(center, new Cesium.Cartesian3(0.0, -4790000.0, 3930000.0)); * * // 2. Using a HeadingPitchRange offset * const center = Cesium.Cartesian3.fromDegrees(-72.0, 40.0); * const heading = Cesium.Math.toRadians(50.0); * const pitch = Cesium.Math.toRadians(-20.0); * const range = 5000.0; * viewer.camera.lookAt(center, new Cesium.HeadingPitchRange(heading, pitch, range)); */ Camera.prototype.lookAt = function (target, offset) { //>>includeStart('debug', pragmas.debug); if (!defined(target)) { throw new DeveloperError("target is required"); } if (!defined(offset)) { throw new DeveloperError("offset is required"); } if (this._mode === SceneMode.MORPHING) { throw new DeveloperError("lookAt is not supported while morphing."); } //>>includeEnd('debug'); const transform = Transforms.eastNorthUpToFixedFrame( target, Ellipsoid.WGS84, scratchLookAtMatrix4 ); this.lookAtTransform(transform, offset); }; const scratchLookAtHeadingPitchRangeOffset = new Cartesian3(); const scratchLookAtHeadingPitchRangeQuaternion1 = new Quaternion(); const scratchLookAtHeadingPitchRangeQuaternion2 = new Quaternion(); const scratchHeadingPitchRangeMatrix3 = new Matrix3(); function offsetFromHeadingPitchRange(heading, pitch, range) { pitch = CesiumMath.clamp( pitch, -CesiumMath.PI_OVER_TWO, CesiumMath.PI_OVER_TWO ); heading = CesiumMath.zeroToTwoPi(heading) - CesiumMath.PI_OVER_TWO; const pitchQuat = Quaternion.fromAxisAngle( Cartesian3.UNIT_Y, -pitch, scratchLookAtHeadingPitchRangeQuaternion1 ); const headingQuat = Quaternion.fromAxisAngle( Cartesian3.UNIT_Z, -heading, scratchLookAtHeadingPitchRangeQuaternion2 ); const rotQuat = Quaternion.multiply(headingQuat, pitchQuat, headingQuat); const rotMatrix = Matrix3.fromQuaternion( rotQuat, scratchHeadingPitchRangeMatrix3 ); const offset = Cartesian3.clone( Cartesian3.UNIT_X, scratchLookAtHeadingPitchRangeOffset ); Matrix3.multiplyByVector(rotMatrix, offset, offset); Cartesian3.negate(offset, offset); Cartesian3.multiplyByScalar(offset, range, offset); return offset; } /** * Sets the camera position and orientation using a target and transformation matrix. The offset can be either a cartesian or heading/pitch/range. * If the offset is a cartesian, then it is an offset from the center of the reference frame defined by the transformation matrix. If the offset * is heading/pitch/range, then the heading and the pitch angles are defined in the reference frame defined by the transformation matrix. * The heading is the angle from y axis and increasing towards the x axis. Pitch is the rotation from the xy-plane. Positive pitch * angles are below the plane. Negative pitch angles are above the plane. The range is the distance from the center. * * In 2D, there must be a top down view. The camera will be placed above the center of the reference frame. The height above the * target will be the magnitude of the offset. The heading will be determined from the offset. If the heading cannot be * determined from the offset, the heading will be north. * * @param {Matrix4} transform The transformation matrix defining the reference frame. * @param {Cartesian3|HeadingPitchRange} [offset] The offset from the target in a reference frame centered at the target. * * @exception {DeveloperError} lookAtTransform is not supported while morphing. * * @example * // 1. Using a cartesian offset * const transform = Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(-98.0, 40.0)); * viewer.camera.lookAtTransform(transform, new Cesium.Cartesian3(0.0, -4790000.0, 3930000.0)); * * // 2. Using a HeadingPitchRange offset * const transform = Cesium.Transforms.eastNorthUpToFixedFrame(Cesium.Cartesian3.fromDegrees(-72.0, 40.0)); * const heading = Cesium.Math.toRadians(50.0); * const pitch = Cesium.Math.toRadians(-20.0); * const range = 5000.0; * viewer.camera.lookAtTransform(transform, new Cesium.HeadingPitchRange(heading, pitch, range)); */ Camera.prototype.lookAtTransform = function (transform, offset) { //>>includeStart('debug', pragmas.debug); if (!defined(transform)) { throw new DeveloperError("transform is required"); } if (this._mode === SceneMode.MORPHING) { throw new DeveloperError( "lookAtTransform is not supported while morphing." ); } //>>includeEnd('debug'); this._setTransform(transform); if (!defined(offset)) { return; } let cartesianOffset; if (defined(offset.heading)) { cartesianOffset = offsetFromHeadingPitchRange( offset.heading, offset.pitch, offset.range ); } else { cartesianOffset = offset; } if (this._mode === SceneMode.SCENE2D) { Cartesian2.clone(Cartesian2.ZERO, this.position); Cartesian3.negate(cartesianOffset, this.up); this.up.z = 0.0; if (Cartesian3.magnitudeSquared(this.up) < CesiumMath.EPSILON10) { Cartesian3.clone(Cartesian3.UNIT_Y, this.up); } Cartesian3.normalize(this.up, this.up); this._setTransform(Matrix4.IDENTITY); Cartesian3.negate(Cartesian3.UNIT_Z, this.direction); Cartesian3.cross(this.direction, this.up, this.right); Cartesian3.normalize(this.right, this.right); const frustum = this.frustum; const ratio = frustum.top / frustum.right; frustum.right = Cartesian3.magnitude(cartesianOffset) * 0.5; frustum.left = -frustum.right; frustum.top = ratio * frustum.right; frustum.bottom = -frustum.top; this._setTransform(transform); return; } Cartesian3.clone(cartesianOffset, this.position); Cartesian3.negate(this.position, this.direction); Cartesian3.normalize(this.direction, this.direction); Cartesian3.cross(this.direction, Cartesian3.UNIT_Z, this.right); if (Cartesian3.magnitudeSquared(this.right) < CesiumMath.EPSILON10) { Cartesian3.clone(Cartesian3.UNIT_X, this.right); } Cartesian3.normalize(this.right, this.right); Cartesian3.cross(this.right, this.direction, this.up); Cartesian3.normalize(this.up, this.up); this._adjustOrthographicFrustum(true); }; const viewRectangle3DCartographic1 = new Cartographic(); const viewRectangle3DCartographic2 = new Cartographic(); const viewRectangle3DNorthEast = new Cartesian3(); const viewRectangle3DSouthWest = new Cartesian3(); const viewRectangle3DNorthWest = new Cartesian3(); const viewRectangle3DSouthEast = new Cartesian3(); const viewRectangle3DNorthCenter = new Cartesian3(); const viewRectangle3DSouthCenter = new Cartesian3(); const viewRectangle3DCenter = new Cartesian3(); const viewRectangle3DEquator = new Cartesian3(); const defaultRF = { direction: new Cartesian3(), right: new Cartesian3(), up: new Cartesian3(), }; let viewRectangle3DEllipsoidGeodesic; function computeD(direction, upOrRight, corner, tanThetaOrPhi) { const opposite = Math.abs(Cartesian3.dot(upOrRight, corner)); return opposite / tanThetaOrPhi - Cartesian3.dot(direction, corner); } function rectangleCameraPosition3D(camera, rectangle, result, updateCamera) { const ellipsoid = camera._projection.ellipsoid; const cameraRF = updateCamera ? camera : defaultRF; const north = rectangle.north; const south = rectangle.south; let east = rectangle.east; const west = rectangle.west; // If we go across the International Date Line if (west > east) { east += CesiumMath.TWO_PI; } // Find the midpoint latitude. // // EllipsoidGeodesic will fail if the north and south edges are very close to being on opposite sides of the ellipsoid. // Ideally we'd just call EllipsoidGeodesic.setEndPoints and let it throw when it detects this case, but sadly it doesn't // even look for this case in optimized builds, so we have to test for it here instead. // // Fortunately, this case can only happen (here) when north is very close to the north pole and south is very close to the south pole, // so handle it just by using 0 latitude as the center. It's certainliy possible to use a smaller tolerance // than one degree here, but one degree is safe and putting the center at 0 latitude should be good enough for any // rectangle that spans 178+ of the 180 degrees of latitude. const longitude = (west + east) * 0.5; let latitude; if ( south < -CesiumMath.PI_OVER_TWO + CesiumMath.RADIANS_PER_DEGREE && north > CesiumMath.PI_OVER_TWO - CesiumMath.RADIANS_PER_DEGREE ) { latitude = 0.0; } else { const northCartographic = viewRectangle3DCartographic1; northCartographic.longitude = longitude; northCartographic.latitude = north; northCartographic.height = 0.0; const southCartographic = viewRectangle3DCartographic2; southCartographic.longitude = longitude; southCartographic.latitude = south; southCartographic.height = 0.0; let ellipsoidGeodesic = viewRectangle3DEllipsoidGeodesic; if ( !defined(ellipsoidGeodesic) || ellipsoidGeodesic.ellipsoid !== ellipsoid ) { viewRectangle3DEllipsoidGeodesic = ellipsoidGeodesic = new EllipsoidGeodesic( undefined, undefined, ellipsoid ); } ellipsoidGeodesic.setEndPoints(northCartographic, southCartographic); latitude = ellipsoidGeodesic.interpolateUsingFraction( 0.5, viewRectangle3DCartographic1 ).latitude; } const centerCartographic = viewRectangle3DCartographic1; centerCartographic.longitude = longitude; centerCartographic.latitude = latitude; centerCartographic.height = 0.0; const center = ellipsoid.cartographicToCartesian( centerCartographic, viewRectangle3DCenter ); const cart = viewRectangle3DCartographic1; cart.longitude = east; cart.latitude = north; const northEast = ellipsoid.cartographicToCartesian( cart, viewRectangle3DNorthEast ); cart.longitude = west; const northWest = ellipsoid.cartographicToCartesian( cart, viewRectangle3DNorthWest ); cart.longitude = longitude; const northCenter = ellipsoid.cartographicToCartesian( cart, viewRectangle3DNorthCenter ); cart.latitude = south; const southCenter = ellipsoid.cartographicToCartesian( cart, viewRectangle3DSouthCenter ); cart.longitude = east; const southEast = ellipsoid.cartographicToCartesian( cart, viewRectangle3DSouthEast ); cart.longitude = west; const southWest = ellipsoid.cartographicToCartesian( cart, viewRectangle3DSouthWest ); Cartesian3.subtract(northWest, center, northWest); Cartesian3.subtract(southEast, center, southEast); Cartesian3.subtract(northEast, center, northEast); Cartesian3.subtract(southWest, center, southWest); Cartesian3.subtract(northCenter, center, northCenter); Cartesian3.subtract(southCenter, center, southCenter); const direction = ellipsoid.geodeticSurfaceNormal(center, cameraRF.direction); Cartesian3.negate(direction, direction); const right = Cartesian3.cross(direction, Cartesian3.UNIT_Z, cameraRF.right); Cartesian3.normalize(right, right); const up = Cartesian3.cross(right, direction, cameraRF.up); let d; if (camera.frustum instanceof OrthographicFrustum) { const width = Math.max( Cartesian3.distance(northEast, northWest), Cartesian3.distance(southEast, southWest) ); const height = Math.max( Cartesian3.distance(northEast, southEast), Cartesian3.distance(northWest, southWest) ); let rightScalar; let topScalar; const offCenterFrustum = camera.frustum._offCenterFrustum; const ratio = offCenterFrustum.right / offCenterFrustum.top; const heightRatio = height * ratio; if (width > heightRatio) { rightScalar = width; topScalar = rightScalar / ratio; } else { topScalar = height; rightScalar = heightRatio; } d = Math.max(rightScalar, topScalar); } else { const tanPhi = Math.tan(camera.frustum.fovy * 0.5); const tanTheta = camera.frustum.aspectRatio * tanPhi; d = Math.max( computeD(direction, up, northWest, tanPhi), computeD(direction, up, southEast, tanPhi), computeD(direction, up, northEast, tanPhi), computeD(direction, up, southWest, tanPhi), computeD(direction, up, northCenter, tanPhi), computeD(direction, up, southCenter, tanPhi), computeD(direction, right, northWest, tanTheta), computeD(direction, right, southEast, tanTheta), computeD(direction, right, northEast, tanTheta), computeD(direction, right, southWest, tanTheta), computeD(direction, right, northCenter, tanTheta), computeD(direction, right, southCenter, tanTheta) ); // If the rectangle crosses the equator, compute D at the equator, too, because that's the // widest part of the rectangle when projected onto the globe. if (south < 0 && north > 0) { const equatorCartographic = viewRectangle3DCartographic1; equatorCartographic.longitude = west; equatorCartographic.latitude = 0.0; equatorCartographic.height = 0.0; let equatorPosition = ellipsoid.cartographicToCartesian( equatorCartographic, viewRectangle3DEquator ); Cartesian3.subtract(equatorPosition, center, equatorPosition); d = Math.max( d, computeD(direction, up, equatorPosition, tanPhi), computeD(direction, right, equatorPosition, tanTheta) ); equatorCartographic.longitude = east; equatorPosition = ellipsoid.cartographicToCartesian( equatorCartographic, viewRectangle3DEquator ); Cartesian3.subtract(equatorPosition, center, equatorPosition); d = Math.max( d, computeD(direction, up, equatorPosition, tanPhi), computeD(direction, right, equatorPosition, tanTheta) ); } } return Cartesian3.add( center, Cartesian3.multiplyByScalar(direction, -d, viewRectangle3DEquator), result ); } const viewRectangleCVCartographic = new Cartographic(); const viewRectangleCVNorthEast = new Cartesian3(); const viewRectangleCVSouthWest = new Cartesian3(); function rectangleCameraPositionColumbusView(camera, rectangle, result) { const projection = camera._projection; if (rectangle.west > rectangle.east) { rectangle = Rectangle.MAX_VALUE; } const transform = camera._actualTransform; const invTransform = camera._actualInvTransform; const cart = viewRectangleCVCartographic; cart.longitude = rectangle.east; cart.latitude = rectangle.north; const northEast = projection.project(cart, viewRectangleCVNorthEast); Matrix4.multiplyByPoint(transform, northEast, northEast); Matrix4.multiplyByPoint(invTransform, northEast, northEast); cart.longitude = rectangle.west; cart.latitude = rectangle.south; const southWest = projection.project(cart, viewRectangleCVSouthWest); Matrix4.multiplyByPoint(transform, southWest, southWest); Matrix4.multiplyByPoint(invTransform, southWest, southWest); result.x = (northEast.x - southWest.x) * 0.5 + southWest.x; result.y = (northEast.y - southWest.y) * 0.5 + southWest.y; if (defined(camera.frustum.fovy)) { const tanPhi = Math.tan(camera.frustum.fovy * 0.5); const tanTheta = camera.frustum.aspectRatio * tanPhi; result.z = Math.max( (northEast.x - southWest.x) / tanTheta, (northEast.y - southWest.y) / tanPhi ) * 0.5; } else { const width = northEast.x - southWest.x; const height = northEast.y - southWest.y; result.z = Math.max(width, height); } return result; } const viewRectangle2DCartographic = new Cartographic(); const viewRectangle2DNorthEast = new Cartesian3(); const viewRectangle2DSouthWest = new Cartesian3(); function rectangleCameraPosition2D(camera, rectangle, result) { const projection = camera._projection; // Account for the rectangle crossing the International Date Line in 2D mode let east = rectangle.east; if (rectangle.west > rectangle.east) { if (camera._scene.mapMode2D === MapMode2D.INFINITE_SCROLL) { east += CesiumMath.TWO_PI; } else { rectangle = Rectangle.MAX_VALUE; east = rectangle.east; } } let cart = viewRectangle2DCartographic; cart.longitude = east; cart.latitude = rectangle.north; const northEast = projection.project(cart, viewRectangle2DNorthEast); cart.longitude = rectangle.west; cart.latitude = rectangle.south; const southWest = projection.project(cart, viewRectangle2DSouthWest); const width = Math.abs(northEast.x - southWest.x) * 0.5; let height = Math.abs(northEast.y - southWest.y) * 0.5; let right, top; const ratio = camera.frustum.right / camera.frustum.top; const heightRatio = height * ratio; if (width > heightRatio) { right = width; top = right / ratio; } else { top = height; right = heightRatio; } height = Math.max(2.0 * right, 2.0 * top); result.x = (northEast.x - southWest.x) * 0.5 + southWest.x; result.y = (northEast.y - southWest.y) * 0.5 + southWest.y; cart = projection.unproject(result, cart); cart.height = height; result = projection.project(cart, result); return result; } /** * Get the camera position needed to view a rectangle on an ellipsoid or map * * @param {Rectangle} rectangle The rectangle to view. * @param {Cartesian3} [result] The camera position needed to view the rectangle * @returns {Cartesian3} The camera position needed to view the rectangle */ Camera.prototype.getRectangleCameraCoordinates = function (rectangle, result) { //>>includeStart('debug', pragmas.debug); if (!defined(rectangle)) { throw new DeveloperError("rectangle is required"); } //>>includeEnd('debug'); const mode = this._mode; if (!defined(result)) { result = new Cartesian3(); } if (mode === SceneMode.SCENE3D) { return rectangleCameraPosition3D(this, rectangle, result); } else if (mode === SceneMode.COLUMBUS_VIEW) { return rectangleCameraPositionColumbusView(this, rectangle, result); } else if (mode === SceneMode.SCENE2D) { return rectangleCameraPosition2D(this, rectangle, result); } return undefined; }; const pickEllipsoid3DRay = new Ray(); function pickEllipsoid3D(camera, windowPosition, ellipsoid, result) { ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); const ray = camera.getPickRay(windowPosition, pickEllipsoid3DRay); const intersection = IntersectionTests.rayEllipsoid(ray, ellipsoid); if (!intersection) { return undefined; } const t = intersection.start > 0.0 ? intersection.start : intersection.stop; return Ray.getPoint(ray, t, result); } const pickEllipsoid2DRay = new Ray(); function pickMap2D(camera, windowPosition, projection, result) { const ray = camera.getPickRay(windowPosition, pickEllipsoid2DRay); let position = ray.origin; position = Cartesian3.fromElements(position.y, position.z, 0.0, position); const cart = projection.unproject(position); if ( cart.latitude < -CesiumMath.PI_OVER_TWO || cart.latitude > CesiumMath.PI_OVER_TWO ) { return undefined; } return projection.ellipsoid.cartographicToCartesian(cart, result); } const pickEllipsoidCVRay = new Ray(); function pickMapColumbusView(camera, windowPosition, projection, result) { const ray = camera.getPickRay(windowPosition, pickEllipsoidCVRay); const scalar = -ray.origin.x / ray.direction.x; Ray.getPoint(ray, scalar, result); const cart = projection.unproject(new Cartesian3(result.y, result.z, 0.0)); if ( cart.latitude < -CesiumMath.PI_OVER_TWO || cart.latitude > CesiumMath.PI_OVER_TWO || cart.longitude < -Math.PI || cart.longitude > Math.PI ) { return undefined; } return projection.ellipsoid.cartographicToCartesian(cart, result); } /** * Pick an ellipsoid or map. * * @param {Cartesian2} windowPosition The x and y coordinates of a pixel. * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid to pick. * @param {Cartesian3} [result] The object onto which to store the result. * @returns {Cartesian3 | undefined} If the ellipsoid or map was picked, * returns the point on the surface of the ellipsoid or map in world * coordinates. If the ellipsoid or map was not picked, returns undefined. * * @example * const canvas = viewer.scene.canvas; * const center = new Cesium.Cartesian2(canvas.clientWidth / 2.0, canvas.clientHeight / 2.0); * const ellipsoid = viewer.scene.globe.ellipsoid; * const result = viewer.camera.pickEllipsoid(center, ellipsoid); */ Camera.prototype.pickEllipsoid = function (windowPosition, ellipsoid, result) { //>>includeStart('debug', pragmas.debug); if (!defined(windowPosition)) { throw new DeveloperError("windowPosition is required."); } //>>includeEnd('debug'); const canvas = this._scene.canvas; if (canvas.clientWidth === 0 || canvas.clientHeight === 0) { return undefined; } if (!defined(result)) { result = new Cartesian3(); } ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); if (this._mode === SceneMode.SCENE3D) { result = pickEllipsoid3D(this, windowPosition, ellipsoid, result); } else if (this._mode === SceneMode.SCENE2D) { result = pickMap2D(this, windowPosition, this._projection, result); } else if (this._mode === SceneMode.COLUMBUS_VIEW) { result = pickMapColumbusView( this, windowPosition, this._projection, result ); } else { return undefined; } return result; }; const pickPerspCenter = new Cartesian3(); const pickPerspXDir = new Cartesian3(); const pickPerspYDir = new Cartesian3(); function getPickRayPerspective(camera, windowPosition, result) { const canvas = camera._scene.canvas; const width = canvas.clientWidth; const height = canvas.clientHeight; const tanPhi = Math.tan(camera.frustum.fovy * 0.5); const tanTheta = camera.frustum.aspectRatio * tanPhi; const near = camera.frustum.near; const x = (2.0 / width) * windowPosition.x - 1.0; const y = (2.0 / height) * (height - windowPosition.y) - 1.0; const position = camera.positionWC; Cartesian3.clone(position, result.origin); const nearCenter = Cartesian3.multiplyByScalar( camera.directionWC, near, pickPerspCenter ); Cartesian3.add(position, nearCenter, nearCenter); const xDir = Cartesian3.multiplyByScalar( camera.rightWC, x * near * tanTheta, pickPerspXDir ); const yDir = Cartesian3.multiplyByScalar( camera.upWC, y * near * tanPhi, pickPerspYDir ); const direction = Cartesian3.add(nearCenter, xDir, result.direction); Cartesian3.add(direction, yDir, direction); Cartesian3.subtract(direction, position, direction); Cartesian3.normalize(direction, direction); return result; } const scratchDirection = new Cartesian3(); function getPickRayOrthographic(camera, windowPosition, result) { const canvas = camera._scene.canvas; const width = canvas.clientWidth; const height = canvas.clientHeight; let frustum = camera.frustum; const offCenterFrustum = frustum.offCenterFrustum; if (defined(offCenterFrustum)) { frustum = offCenterFrustum; } let x = (2.0 / width) * windowPosition.x - 1.0; x *= (frustum.right - frustum.left) * 0.5; let y = (2.0 / height) * (height - windowPosition.y) - 1.0; y *= (frustum.top - frustum.bottom) * 0.5; const origin = result.origin; Cartesian3.clone(camera.position, origin); Cartesian3.multiplyByScalar(camera.right, x, scratchDirection); Cartesian3.add(scratchDirection, origin, origin); Cartesian3.multiplyByScalar(camera.up, y, scratchDirection); Cartesian3.add(scratchDirection, origin, origin); Cartesian3.clone(camera.directionWC, result.direction); if ( camera._mode === SceneMode.COLUMBUS_VIEW || camera._mode === SceneMode.SCENE2D ) { Cartesian3.fromElements( result.origin.z, result.origin.x, result.origin.y, result.origin ); } return result; } /** * Create a ray from the camera position through the pixel at windowPosition * in world coordinates. * * @param {Cartesian2} windowPosition The x and y coordinates of a pixel. * @param {Ray} [result] The object onto which to store the result. * @returns {Ray|undefined} Returns the {@link Cartesian3} position and direction of the ray, or undefined if the pick ray cannot be determined. */ Camera.prototype.getPickRay = function (windowPosition, result) { //>>includeStart('debug', pragmas.debug); if (!defined(windowPosition)) { throw new DeveloperError("windowPosition is required."); } //>>includeEnd('debug'); if (!defined(result)) { result = new Ray(); } const canvas = this._scene.canvas; if (canvas.clientWidth <= 0 || canvas.clientHeight <= 0) { return undefined; } const frustum = this.frustum; if ( defined(frustum.aspectRatio) && defined(frustum.fov) && defined(frustum.near) ) { return getPickRayPerspective(this, windowPosition, result); } return getPickRayOrthographic(this, windowPosition, result); }; const scratchToCenter = new Cartesian3(); const scratchProj = new Cartesian3(); /** * Return the distance from the camera to the front of the bounding sphere. * * @param {BoundingSphere} boundingSphere The bounding sphere in world coordinates. * @returns {number} The distance to the bounding sphere. */ Camera.prototype.distanceToBoundingSphere = function (boundingSphere) { //>>includeStart('debug', pragmas.debug); if (!defined(boundingSphere)) { throw new DeveloperError("boundingSphere is required."); } //>>includeEnd('debug'); const toCenter = Cartesian3.subtract( this.positionWC, boundingSphere.center, scratchToCenter ); const proj = Cartesian3.multiplyByScalar( this.directionWC, Cartesian3.dot(toCenter, this.directionWC), scratchProj ); return Math.max(0.0, Cartesian3.magnitude(proj) - boundingSphere.radius); }; const scratchPixelSize = new Cartesian2(); /** * Return the pixel size in meters. * * @param {BoundingSphere} boundingSphere The bounding sphere in world coordinates. * @param {number} drawingBufferWidth The drawing buffer width. * @param {number} drawingBufferHeight The drawing buffer height. * @returns {number} The pixel size in meters. */ Camera.prototype.getPixelSize = function ( boundingSphere, drawingBufferWidth, drawingBufferHeight ) { //>>includeStart('debug', pragmas.debug); if (!defined(boundingSphere)) { throw new DeveloperError("boundingSphere is required."); } if (!defined(drawingBufferWidth)) { throw new DeveloperError("drawingBufferWidth is required."); } if (!defined(drawingBufferHeight)) { throw new DeveloperError("drawingBufferHeight is required."); } //>>includeEnd('debug'); const distance = this.distanceToBoundingSphere(boundingSphere); const pixelSize = this.frustum.getPixelDimensions( drawingBufferWidth, drawingBufferHeight, distance, this._scene.pixelRatio, scratchPixelSize ); return Math.max(pixelSize.x, pixelSize.y); }; function createAnimationTemplateCV( camera, position, center, maxX, maxY, duration ) { const newPosition = Cartesian3.clone(position); if (center.y > maxX) { newPosition.y -= center.y - maxX; } else if (center.y < -maxX) { newPosition.y += -maxX - center.y; } if (center.z > maxY) { newPosition.z -= center.z - maxY; } else if (center.z < -maxY) { newPosition.z += -maxY - center.z; } function updateCV(value) { const interp = Cartesian3.lerp( position, newPosition, value.time, new Cartesian3() ); camera.worldToCameraCoordinatesPoint(interp, camera.position); } return { easingFunction: EasingFunction.EXPONENTIAL_OUT, startObject: { time: 0.0, }, stopObject: { time: 1.0, }, duration: duration, update: updateCV, }; } const normalScratch = new Cartesian3(); const centerScratch = new Cartesian3(); const posScratch = new Cartesian3(); const scratchCartesian3Subtract = new Cartesian3(); function createAnimationCV(camera, duration) { let position = camera.position; const direction = camera.direction; const normal = camera.worldToCameraCoordinatesVector( Cartesian3.UNIT_X, normalScratch ); const scalar = -Cartesian3.dot(normal, position) / Cartesian3.dot(normal, direction); const center = Cartesian3.add( position, Cartesian3.multiplyByScalar(direction, scalar, centerScratch), centerScratch ); camera.cameraToWorldCoordinatesPoint(center, center); position = camera.cameraToWorldCoordinatesPoint(camera.position, posScratch); const tanPhi = Math.tan(camera.frustum.fovy * 0.5); const tanTheta = camera.frustum.aspectRatio * tanPhi; const distToC = Cartesian3.magnitude( Cartesian3.subtract(position, center, scratchCartesian3Subtract) ); const dWidth = tanTheta * distToC; const dHeight = tanPhi * distToC; const mapWidth = camera._maxCoord.x; const mapHeight = camera._maxCoord.y; const maxX = Math.max(dWidth - mapWidth, mapWidth); const maxY = Math.max(dHeight - mapHeight, mapHeight); if ( position.z < -maxX || position.z > maxX || position.y < -maxY || position.y > maxY ) { const translateX = center.y < -maxX || center.y > maxX; const translateY = center.z < -maxY || center.z > maxY; if (translateX || translateY) { return createAnimationTemplateCV( camera, position, center, maxX, maxY, duration ); } } return undefined; } /** * Create an animation to move the map into view. This method is only valid for 2D and Columbus modes. * * @param {number} duration The duration, in seconds, of the animation. * @returns {object} The animation or undefined if the scene mode is 3D or the map is already ion view. * * @private */ Camera.prototype.createCorrectPositionTween = function (duration) { //>>includeStart('debug', pragmas.debug); if (!defined(duration)) { throw new DeveloperError("duration is required."); } //>>includeEnd('debug'); if (this._mode === SceneMode.COLUMBUS_VIEW) { return createAnimationCV(this, duration); } return undefined; }; const scratchFlyToDestination = new Cartesian3(); const newOptions = { destination: undefined, heading: undefined, pitch: undefined, roll: undefined, duration: undefined, complete: undefined, cancel: undefined, endTransform: undefined, maximumHeight: undefined, easingFunction: undefined, }; /** * Cancels the current camera flight and leaves the camera at its current location. * If no flight is in progress, this this function does nothing. */ Camera.prototype.cancelFlight = function () { if (defined(this._currentFlight)) { this._currentFlight.cancelTween(); this._currentFlight = undefined; } }; /** * Completes the current camera flight and moves the camera immediately to its final destination. * If no flight is in progress, this this function does nothing. */ Camera.prototype.completeFlight = function () { if (defined(this._currentFlight)) { this._currentFlight.cancelTween(); const options = { destination: undefined, orientation: { heading: undefined, pitch: undefined, roll: undefined, }, }; options.destination = newOptions.destination; options.orientation.heading = newOptions.heading; options.orientation.pitch = newOptions.pitch; options.orientation.roll = newOptions.roll; this.setView(options); if (defined(this._currentFlight.complete)) { this._currentFlight.complete(); } this._currentFlight = undefined; } }; /** * Flies the camera from its current position to a new position. * * @param {object} options Object with the following properties: * @param {Cartesian3|Rectangle} options.destination The final position of the camera in WGS84 (world) coordinates or a rectangle that would be visible from a top-down view. * @param {object} [options.orientation] An object that contains either direction and up properties or heading, pitch and roll properties. By default, the direction will point * towards the center of the frame in 3D and in the negative z direction in Columbus view. The up direction will point towards local north in 3D and in the positive * y direction in Columbus view. Orientation is not used in 2D when in infinite scrolling mode. * @param {number} [options.duration] The duration of the flight in seconds. If omitted, Cesium attempts to calculate an ideal duration based on the distance to be traveled by the flight. * @param {Camera.FlightCompleteCallback} [options.complete] The function to execute when the flight is complete. * @param {Camera.FlightCancelledCallback} [options.cancel] The function to execute if the flight is cancelled. * @param {Matrix4} [options.endTransform] Transform matrix representing the reference frame the camera will be in when the flight is completed. * @param {number} [options.maximumHeight] The maximum height at the peak of the flight. * @param {number} [options.pitchAdjustHeight] If camera flyes higher than that value, adjust pitch duiring the flight to look down, and keep Earth in viewport. * @param {number} [options.flyOverLongitude] There are always two ways between 2 points on globe. This option force camera to choose fight direction to fly over that longitude. * @param {number} [options.flyOverLongitudeWeight] Fly over the lon specifyed via flyOverLongitude only if that way is not longer than short way times flyOverLongitudeWeight. * @param {boolean} [options.convert] Whether to convert the destination from world coordinates to scene coordinates (only relevant when not using 3D). Defaults to true. * @param {EasingFunction.Callback} [options.easingFunction] Controls how the time is interpolated over the duration of the flight. * * @exception {DeveloperError} If either direction or up is given, then both are required. * * @example * // 1. Fly to a position with a top-down view * viewer.camera.flyTo({ * destination : Cesium.Cartesian3.fromDegrees(-117.16, 32.71, 15000.0) * }); * * // 2. Fly to a Rectangle with a top-down view * viewer.camera.flyTo({ * destination : Cesium.Rectangle.fromDegrees(west, south, east, north) * }); * * // 3. Fly to a position with an orientation using unit vectors. * viewer.camera.flyTo({ * destination : Cesium.Cartesian3.fromDegrees(-122.19, 46.25, 5000.0), * orientation : { * direction : new Cesium.Cartesian3(-0.04231243104240401, -0.20123236049443421, -0.97862924300734), * up : new Cesium.Cartesian3(-0.47934589305293746, -0.8553216253114552, 0.1966022179118339) * } * }); * * // 4. Fly to a position with an orientation using heading, pitch and roll. * viewer.camera.flyTo({ * destination : Cesium.Cartesian3.fromDegrees(-122.19, 46.25, 5000.0), * orientation : { * heading : Cesium.Math.toRadians(175.0), * pitch : Cesium.Math.toRadians(-35.0), * roll : 0.0 * } * }); */ Camera.prototype.flyTo = function (options) { options = defaultValue(options, defaultValue.EMPTY_OBJECT); let destination = options.destination; //>>includeStart('debug', pragmas.debug); if (!defined(destination)) { throw new DeveloperError("destination is required."); } //>>includeEnd('debug'); const mode = this._mode; if (mode === SceneMode.MORPHING) { return; } this.cancelFlight(); const isRectangle = destination instanceof Rectangle; if (isRectangle) { destination = this.getRectangleCameraCoordinates( destination, scratchFlyToDestination ); } let orientation = defaultValue( options.orientation, defaultValue.EMPTY_OBJECT ); if (defined(orientation.direction)) { orientation = directionUpToHeadingPitchRoll( this, destination, orientation, scratchSetViewOptions.orientation ); } if (defined(options.duration) && options.duration <= 0.0) { const setViewOptions = scratchSetViewOptions; setViewOptions.destination = options.destination; setViewOptions.orientation.heading = orientation.heading; setViewOptions.orientation.pitch = orientation.pitch; setViewOptions.orientation.roll = orientation.roll; setViewOptions.convert = options.convert; setViewOptions.endTransform = options.endTransform; this.setView(setViewOptions); if (typeof options.complete === "function") { options.complete(); } return; } const that = this; /* eslint-disable-next-line prefer-const */ let flightTween; newOptions.destination = destination; newOptions.heading = orientation.heading; newOptions.pitch = orientation.pitch; newOptions.roll = orientation.roll; newOptions.duration = options.duration; newOptions.complete = function () { if (flightTween === that._currentFlight) { that._currentFlight = undefined; } if (defined(options.complete)) { options.complete(); } }; newOptions.cancel = options.cancel; newOptions.endTransform = options.endTransform; newOptions.convert = isRectangle ? false : options.convert; newOptions.maximumHeight = options.maximumHeight; newOptions.pitchAdjustHeight = options.pitchAdjustHeight; newOptions.flyOverLongitude = options.flyOverLongitude; newOptions.flyOverLongitudeWeight = options.flyOverLongitudeWeight; newOptions.easingFunction = options.easingFunction; const scene = this._scene; const tweenOptions = CameraFlightPath.createTween(scene, newOptions); // If the camera doesn't actually need to go anywhere, duration // will be 0 and we can just complete the current flight. if (tweenOptions.duration === 0) { if (typeof tweenOptions.complete === "function") { tweenOptions.complete(); } return; } flightTween = scene.tweens.add(tweenOptions); this._currentFlight = flightTween; // Save the final destination view information for the PRELOAD_FLIGHT pass. let preloadFlightCamera = this._scene.preloadFlightCamera; if (this._mode !== SceneMode.SCENE2D) { if (!defined(preloadFlightCamera)) { preloadFlightCamera = Camera.clone(this); } preloadFlightCamera.setView({ destination: destination, orientation: orientation, }); this._scene.preloadFlightCullingVolume = preloadFlightCamera.frustum.computeCullingVolume( preloadFlightCamera.positionWC, preloadFlightCamera.directionWC, preloadFlightCamera.upWC ); } }; function distanceToBoundingSphere3D(camera, radius) { const frustum = camera.frustum; const tanPhi = Math.tan(frustum.fovy * 0.5); const tanTheta = frustum.aspectRatio * tanPhi; return Math.max(radius / tanTheta, radius / tanPhi); } function distanceToBoundingSphere2D(camera, radius) { let frustum = camera.frustum; const offCenterFrustum = frustum.offCenterFrustum; if (defined(offCenterFrustum)) { frustum = offCenterFrustum; } let right, top; const ratio = frustum.right / frustum.top; const heightRatio = radius * ratio; if (radius > heightRatio) { right = radius; top = right / ratio; } else { top = radius; right = heightRatio; } return Math.max(right, top) * 1.5; } const MINIMUM_ZOOM = 100.0; function adjustBoundingSphereOffset(camera, boundingSphere, offset) { offset = HeadingPitchRange.clone( defined(offset) ? offset : Camera.DEFAULT_OFFSET ); const minimumZoom = camera._scene.screenSpaceCameraController.minimumZoomDistance; const maximumZoom = camera._scene.screenSpaceCameraController.maximumZoomDistance; const range = offset.range; if (!defined(range) || range === 0.0) { const radius = boundingSphere.radius; if (radius === 0.0) { offset.range = MINIMUM_ZOOM; } else if ( camera.frustum instanceof OrthographicFrustum || camera._mode === SceneMode.SCENE2D ) { offset.range = distanceToBoundingSphere2D(camera, radius); } else { offset.range = distanceToBoundingSphere3D(camera, radius); } offset.range = CesiumMath.clamp(offset.range, minimumZoom, maximumZoom); } return offset; } /** * Sets the camera so that the current view contains the provided bounding sphere. * *

The offset is heading/pitch/range in the local east-north-up reference frame centered at the center of the bounding sphere. * The heading and the pitch angles are defined in the local east-north-up reference frame. * The heading is the angle from y axis and increasing towards the x axis. Pitch is the rotation from the xy-plane. Positive pitch * angles are below the plane. Negative pitch angles are above the plane. The range is the distance from the center. If the range is * zero, a range will be computed such that the whole bounding sphere is visible.

* *

In 2D, there must be a top down view. The camera will be placed above the target looking down. The height above the * target will be the range. The heading will be determined from the offset. If the heading cannot be * determined from the offset, the heading will be north.

* * @param {BoundingSphere} boundingSphere The bounding sphere to view, in world coordinates. * @param {HeadingPitchRange} [offset] The offset from the target in the local east-north-up reference frame centered at the target. * * @exception {DeveloperError} viewBoundingSphere is not supported while morphing. */ Camera.prototype.viewBoundingSphere = function (boundingSphere, offset) { //>>includeStart('debug', pragmas.debug); if (!defined(boundingSphere)) { throw new DeveloperError("boundingSphere is required."); } if (this._mode === SceneMode.MORPHING) { throw new DeveloperError( "viewBoundingSphere is not supported while morphing." ); } //>>includeEnd('debug'); offset = adjustBoundingSphereOffset(this, boundingSphere, offset); this.lookAt(boundingSphere.center, offset); }; const scratchflyToBoundingSphereTransform = new Matrix4(); const scratchflyToBoundingSphereDestination = new Cartesian3(); const scratchflyToBoundingSphereDirection = new Cartesian3(); const scratchflyToBoundingSphereUp = new Cartesian3(); const scratchflyToBoundingSphereRight = new Cartesian3(); const scratchFlyToBoundingSphereCart4 = new Cartesian4(); const scratchFlyToBoundingSphereQuaternion = new Quaternion(); const scratchFlyToBoundingSphereMatrix3 = new Matrix3(); /** * Flies the camera to a location where the current view contains the provided bounding sphere. * *

The offset is heading/pitch/range in the local east-north-up reference frame centered at the center of the bounding sphere. * The heading and the pitch angles are defined in the local east-north-up reference frame. * The heading is the angle from y axis and increasing towards the x axis. Pitch is the rotation from the xy-plane. Positive pitch * angles are below the plane. Negative pitch angles are above the plane. The range is the distance from the center. If the range is * zero, a range will be computed such that the whole bounding sphere is visible.

* *

In 2D and Columbus View, there must be a top down view. The camera will be placed above the target looking down. The height above the * target will be the range. The heading will be aligned to local north.

* * @param {BoundingSphere} boundingSphere The bounding sphere to view, in world coordinates. * @param {object} [options] Object with the following properties: * @param {number} [options.duration] The duration of the flight in seconds. If omitted, Cesium attempts to calculate an ideal duration based on the distance to be traveled by the flight. * @param {HeadingPitchRange} [options.offset] The offset from the target in the local east-north-up reference frame centered at the target. * @param {Camera.FlightCompleteCallback} [options.complete] The function to execute when the flight is complete. * @param {Camera.FlightCancelledCallback} [options.cancel] The function to execute if the flight is cancelled. * @param {Matrix4} [options.endTransform] Transform matrix representing the reference frame the camera will be in when the flight is completed. * @param {number} [options.maximumHeight] The maximum height at the peak of the flight. * @param {number} [options.pitchAdjustHeight] If camera flyes higher than that value, adjust pitch duiring the flight to look down, and keep Earth in viewport. * @param {number} [options.flyOverLongitude] There are always two ways between 2 points on globe. This option force camera to choose fight direction to fly over that longitude. * @param {number} [options.flyOverLongitudeWeight] Fly over the lon specifyed via flyOverLongitude only if that way is not longer than short way times flyOverLongitudeWeight. * @param {EasingFunction.Callback} [options.easingFunction] Controls how the time is interpolated over the duration of the flight. */ Camera.prototype.flyToBoundingSphere = function (boundingSphere, options) { //>>includeStart('debug', pragmas.debug); if (!defined(boundingSphere)) { throw new DeveloperError("boundingSphere is required."); } //>>includeEnd('debug'); options = defaultValue(options, defaultValue.EMPTY_OBJECT); const scene2D = this._mode === SceneMode.SCENE2D || this._mode === SceneMode.COLUMBUS_VIEW; this._setTransform(Matrix4.IDENTITY); const offset = adjustBoundingSphereOffset( this, boundingSphere, options.offset ); let position; if (scene2D) { position = Cartesian3.multiplyByScalar( Cartesian3.UNIT_Z, offset.range, scratchflyToBoundingSphereDestination ); } else { position = offsetFromHeadingPitchRange( offset.heading, offset.pitch, offset.range ); } const transform = Transforms.eastNorthUpToFixedFrame( boundingSphere.center, Ellipsoid.WGS84, scratchflyToBoundingSphereTransform ); Matrix4.multiplyByPoint(transform, position, position); let direction; let up; if (!scene2D) { direction = Cartesian3.subtract( boundingSphere.center, position, scratchflyToBoundingSphereDirection ); Cartesian3.normalize(direction, direction); up = Matrix4.multiplyByPointAsVector( transform, Cartesian3.UNIT_Z, scratchflyToBoundingSphereUp ); if (1.0 - Math.abs(Cartesian3.dot(direction, up)) < CesiumMath.EPSILON6) { const rotateQuat = Quaternion.fromAxisAngle( direction, offset.heading, scratchFlyToBoundingSphereQuaternion ); const rotation = Matrix3.fromQuaternion( rotateQuat, scratchFlyToBoundingSphereMatrix3 ); Cartesian3.fromCartesian4( Matrix4.getColumn(transform, 1, scratchFlyToBoundingSphereCart4), up ); Matrix3.multiplyByVector(rotation, up, up); } const right = Cartesian3.cross( direction, up, scratchflyToBoundingSphereRight ); Cartesian3.cross(right, direction, up); Cartesian3.normalize(up, up); } this.flyTo({ destination: position, orientation: { direction: direction, up: up, }, duration: options.duration, complete: options.complete, cancel: options.cancel, endTransform: options.endTransform, maximumHeight: options.maximumHeight, easingFunction: options.easingFunction, flyOverLongitude: options.flyOverLongitude, flyOverLongitudeWeight: options.flyOverLongitudeWeight, pitchAdjustHeight: options.pitchAdjustHeight, }); }; const scratchCartesian3_1 = new Cartesian3(); const scratchCartesian3_2 = new Cartesian3(); const scratchCartesian3_3 = new Cartesian3(); const scratchCartesian3_4 = new Cartesian3(); const horizonPoints = [ new Cartesian3(), new Cartesian3(), new Cartesian3(), new Cartesian3(), ]; function computeHorizonQuad(camera, ellipsoid) { const radii = ellipsoid.radii; const p = camera.positionWC; // Find the corresponding position in the scaled space of the ellipsoid. const q = Cartesian3.multiplyComponents( ellipsoid.oneOverRadii, p, scratchCartesian3_1 ); const qMagnitude = Cartesian3.magnitude(q); const qUnit = Cartesian3.normalize(q, scratchCartesian3_2); // Determine the east and north directions at q. let eUnit; let nUnit; if ( Cartesian3.equalsEpsilon(qUnit, Cartesian3.UNIT_Z, CesiumMath.EPSILON10) ) { eUnit = new Cartesian3(0, 1, 0); nUnit = new Cartesian3(0, 0, 1); } else { eUnit = Cartesian3.normalize( Cartesian3.cross(Cartesian3.UNIT_Z, qUnit, scratchCartesian3_3), scratchCartesian3_3 ); nUnit = Cartesian3.normalize( Cartesian3.cross(qUnit, eUnit, scratchCartesian3_4), scratchCartesian3_4 ); } // Determine the radius of the 'limb' of the ellipsoid. const wMagnitude = Math.sqrt(Cartesian3.magnitudeSquared(q) - 1.0); // Compute the center and offsets. const center = Cartesian3.multiplyByScalar( qUnit, 1.0 / qMagnitude, scratchCartesian3_1 ); const scalar = wMagnitude / qMagnitude; const eastOffset = Cartesian3.multiplyByScalar( eUnit, scalar, scratchCartesian3_2 ); const northOffset = Cartesian3.multiplyByScalar( nUnit, scalar, scratchCartesian3_3 ); // A conservative measure for the longitudes would be to use the min/max longitudes of the bounding frustum. const upperLeft = Cartesian3.add(center, northOffset, horizonPoints[0]); Cartesian3.subtract(upperLeft, eastOffset, upperLeft); Cartesian3.multiplyComponents(radii, upperLeft, upperLeft); const lowerLeft = Cartesian3.subtract(center, northOffset, horizonPoints[1]); Cartesian3.subtract(lowerLeft, eastOffset, lowerLeft); Cartesian3.multiplyComponents(radii, lowerLeft, lowerLeft); const lowerRight = Cartesian3.subtract(center, northOffset, horizonPoints[2]); Cartesian3.add(lowerRight, eastOffset, lowerRight); Cartesian3.multiplyComponents(radii, lowerRight, lowerRight); const upperRight = Cartesian3.add(center, northOffset, horizonPoints[3]); Cartesian3.add(upperRight, eastOffset, upperRight); Cartesian3.multiplyComponents(radii, upperRight, upperRight); return horizonPoints; } const scratchPickCartesian2 = new Cartesian2(); const scratchRectCartesian = new Cartesian3(); const cartoArray = [ new Cartographic(), new Cartographic(), new Cartographic(), new Cartographic(), ]; function addToResult(x, y, index, camera, ellipsoid, computedHorizonQuad) { scratchPickCartesian2.x = x; scratchPickCartesian2.y = y; const r = camera.pickEllipsoid( scratchPickCartesian2, ellipsoid, scratchRectCartesian ); if (defined(r)) { cartoArray[index] = ellipsoid.cartesianToCartographic(r, cartoArray[index]); return 1; } cartoArray[index] = ellipsoid.cartesianToCartographic( computedHorizonQuad[index], cartoArray[index] ); return 0; } /** * Computes the approximate visible rectangle on the ellipsoid. * * @param {Ellipsoid} [ellipsoid=Ellipsoid.WGS84] The ellipsoid that you want to know the visible region. * @param {Rectangle} [result] The rectangle in which to store the result * * @returns {Rectangle|undefined} The visible rectangle or undefined if the ellipsoid isn't visible at all. */ Camera.prototype.computeViewRectangle = function (ellipsoid, result) { ellipsoid = defaultValue(ellipsoid, Ellipsoid.WGS84); const cullingVolume = this.frustum.computeCullingVolume( this.positionWC, this.directionWC, this.upWC ); const boundingSphere = new BoundingSphere( Cartesian3.ZERO, ellipsoid.maximumRadius ); const visibility = cullingVolume.computeVisibility(boundingSphere); if (visibility === Intersect.OUTSIDE) { return undefined; } const canvas = this._scene.canvas; const width = canvas.clientWidth; const height = canvas.clientHeight; let successfulPickCount = 0; const computedHorizonQuad = computeHorizonQuad(this, ellipsoid); successfulPickCount += addToResult( 0, 0, 0, this, ellipsoid, computedHorizonQuad ); successfulPickCount += addToResult( 0, height, 1, this, ellipsoid, computedHorizonQuad ); successfulPickCount += addToResult( width, height, 2, this, ellipsoid, computedHorizonQuad ); successfulPickCount += addToResult( width, 0, 3, this, ellipsoid, computedHorizonQuad ); if (successfulPickCount < 2) { // If we have space non-globe in 3 or 4 corners then return the whole globe return Rectangle.MAX_VALUE; } result = Rectangle.fromCartographicArray(cartoArray, result); // Detect if we go over the poles let distance = 0; let lastLon = cartoArray[3].longitude; for (let i = 0; i < 4; ++i) { const lon = cartoArray[i].longitude; const diff = Math.abs(lon - lastLon); if (diff > CesiumMath.PI) { // Crossed the dateline distance += CesiumMath.TWO_PI - diff; } else { distance += diff; } lastLon = lon; } // We are over one of the poles so adjust the rectangle accordingly if ( CesiumMath.equalsEpsilon( Math.abs(distance), CesiumMath.TWO_PI, CesiumMath.EPSILON9 ) ) { result.west = -CesiumMath.PI; result.east = CesiumMath.PI; if (cartoArray[0].latitude >= 0.0) { result.north = CesiumMath.PI_OVER_TWO; } else { result.south = -CesiumMath.PI_OVER_TWO; } } return result; }; /** * Switches the frustum/projection to perspective. * * This function is a no-op in 2D which must always be orthographic. */ Camera.prototype.switchToPerspectiveFrustum = function () { if ( this._mode === SceneMode.SCENE2D || this.frustum instanceof PerspectiveFrustum ) { return; } const scene = this._scene; this.frustum = new PerspectiveFrustum(); this.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; this.frustum.fov = CesiumMath.toRadians(60.0); }; /** * Switches the frustum/projection to orthographic. * * This function is a no-op in 2D which will always be orthographic. */ Camera.prototype.switchToOrthographicFrustum = function () { if ( this._mode === SceneMode.SCENE2D || this.frustum instanceof OrthographicFrustum ) { return; } // This must be called before changing the frustum because it uses the previous // frustum to reconstruct the world space position from the depth buffer. const frustumWidth = calculateOrthographicFrustumWidth(this); const scene = this._scene; this.frustum = new OrthographicFrustum(); this.frustum.aspectRatio = scene.drawingBufferWidth / scene.drawingBufferHeight; this.frustum.width = frustumWidth; }; /** * @private */ Camera.clone = function (camera, result) { if (!defined(result)) { result = new Camera(camera._scene); } Cartesian3.clone(camera.position, result.position); Cartesian3.clone(camera.direction, result.direction); Cartesian3.clone(camera.up, result.up); Cartesian3.clone(camera.right, result.right); Matrix4.clone(camera._transform, result.transform); result._transformChanged = true; result.frustum = camera.frustum.clone(); return result; }; /** * A function that will execute when a flight completes. * @callback Camera.FlightCompleteCallback */ /** * A function that will execute when a flight is cancelled. * @callback Camera.FlightCancelledCallback */ export default Camera;