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;