UNPKG

terriajs

Version:

Geospatial data visualization platform.

230 lines 13.2 kB
import Cartesian3 from "terriajs-cesium/Source/Core/Cartesian3"; import Cartographic from "terriajs-cesium/Source/Core/Cartographic"; import DeveloperError from "terriajs-cesium/Source/Core/DeveloperError"; import Ellipsoid from "terriajs-cesium/Source/Core/Ellipsoid"; import HeadingPitchRange from "terriajs-cesium/Source/Core/HeadingPitchRange"; import HeadingPitchRoll from "terriajs-cesium/Source/Core/HeadingPitchRoll"; import CesiumMath from "terriajs-cesium/Source/Core/Math"; import Matrix3 from "terriajs-cesium/Source/Core/Matrix3"; import Matrix4 from "terriajs-cesium/Source/Core/Matrix4"; import Quaternion from "terriajs-cesium/Source/Core/Quaternion"; import Rectangle from "terriajs-cesium/Source/Core/Rectangle"; import Transforms from "terriajs-cesium/Source/Core/Transforms"; import { isJsonNumber, isJsonObject } from "../Core/Json"; import TerriaError from "../Core/TerriaError"; /** * Holds a camera's view parameters, expressed as a rectangular extent and/or as a camera position, direction, * and up vector. */ export default class CameraView { /** * Gets the rectangular extent of the view. If {@link CameraView#position}, {@link CameraView#direction}, * and {@link CameraView#up} are specified, this property will be ignored for viewers that support those parameters * (e.g. Cesium). This property must always be supplied, however, for the benefit of viewers that do not understand * these parameters (e.g. Leaflet). */ rectangle; /** * Gets the position of the camera in the Earth-centered Fixed frame. */ position; /** * Gets the look direction of the camera in the Earth-centered Fixed frame. */ direction; /** * Gets the up vector direction of the camera in the Earth-centered Fixed frame. */ up; constructor(rectangle, position, direction, up) { this.rectangle = Rectangle.clone(rectangle); if (position !== undefined || direction !== undefined || up !== undefined) { if (position === undefined || direction === undefined || up === undefined) { throw new DeveloperError("If any of position, direction, or up are specified, all must be specified."); } this.position = Cartesian3.clone(position); this.direction = Cartesian3.clone(direction); this.up = Cartesian3.clone(up); } } toJson() { const result = { west: CesiumMath.toDegrees(this.rectangle.west), south: CesiumMath.toDegrees(this.rectangle.south), east: CesiumMath.toDegrees(this.rectangle.east), north: CesiumMath.toDegrees(this.rectangle.north) }; function vectorToJson(vector) { return { x: vector.x, y: vector.y, z: vector.z }; } if (this.position && this.direction && this.up) { result.position = vectorToJson(this.position); result.direction = vectorToJson(this.direction); result.up = vectorToJson(this.up); } return result; } /** * Constructs a {@link CameraView} from json. All angles must be specified in degrees. * If neither json.lookAt nor json.positionHeadingPitchRoll is present, then json should have the keys position, direction, up, west, south, east, north. * @param {Object} json The JSON description. The JSON should be in the form of an object literal, not a string. * @param {Object} [json.lookAt] If present, must include keys targetLongitude, targetLatitude, targetHeight, heading, pitch, range. * @param {Object} [json.positionHeadingPitchRoll] If present, must include keys cameraLongitude, cameraLatitude, cameraHeight, heading, pitch, roll. * @return {CameraView} The camera view. */ static fromJson(json) { const lookAt = json.lookAt; const positionHeadingPitchRoll = json.positionHeadingPitchRoll; if (isJsonObject(lookAt)) { if (!isJsonNumber(lookAt.targetLongitude) || !isJsonNumber(lookAt.targetLatitude) || !isJsonNumber(lookAt.targetHeight) || !isJsonNumber(lookAt.heading) || !isJsonNumber(lookAt.pitch) || !isJsonNumber(lookAt.range)) { throw new TerriaError({ sender: CameraView, title: "Invalid CameraView", message: "`lookAt` must have `targetLongitude`, `targetLatitude`, " + "`targetHeight`, `heading`, `pitch`, and `range` properties, " + "and all must be numbers." }); } const targetPosition = Cartographic.fromDegrees(lookAt.targetLongitude, lookAt.targetLatitude, lookAt.targetHeight); const headingPitchRange = new HeadingPitchRange(CesiumMath.toRadians(lookAt.heading), CesiumMath.toRadians(lookAt.pitch), lookAt.range); return CameraView.fromLookAt(targetPosition, headingPitchRange); } else if (isJsonObject(positionHeadingPitchRoll)) { if (!isJsonNumber(positionHeadingPitchRoll.cameraLongitude) || !isJsonNumber(positionHeadingPitchRoll.cameraLatitude) || !isJsonNumber(positionHeadingPitchRoll.cameraHeight) || !isJsonNumber(positionHeadingPitchRoll.heading) || !isJsonNumber(positionHeadingPitchRoll.pitch) || !isJsonNumber(positionHeadingPitchRoll.roll)) { throw new TerriaError({ sender: CameraView, title: "Invalid CameraView", message: "`positionHeadingPitchRoll` must have `cameraLongitude`, " + "`cameraLatitude`, `cameraHeight`, `heading`, `pitch`, and " + "`roll` properties, and all must be numbers." }); } const cameraPosition = Cartographic.fromDegrees(positionHeadingPitchRoll.cameraLongitude, positionHeadingPitchRoll.cameraLatitude, positionHeadingPitchRoll.cameraHeight); return CameraView.fromPositionHeadingPitchRoll(cameraPosition, CesiumMath.toRadians(positionHeadingPitchRoll.heading), CesiumMath.toRadians(positionHeadingPitchRoll.pitch), CesiumMath.toRadians(positionHeadingPitchRoll.roll)); } else { if (!isJsonNumber(json.west) || !isJsonNumber(json.south) || !isJsonNumber(json.east) || !isJsonNumber(json.north)) { throw new TerriaError({ sender: CameraView, title: "Invalid CameraView", message: "The `west`, `south`, `east`, and `north` properties are " + "required and must be numbers, specified in degrees." }); } const rectangle = Rectangle.fromDegrees(json.west, json.south, json.east, json.north); if (isVector(json.position) && isVector(json.direction) && isVector(json.up)) { return new CameraView(rectangle, new Cartesian3(json.position.x, json.position.y, json.position.z), new Cartesian3(json.direction.x, json.direction.y, json.direction.z), new Cartesian3(json.up.x, json.up.y, json.up.z)); } else { return new CameraView(rectangle); } } } /** * Constructs a {@link CameraView} from a "look at" description. * @param targetPosition The position to look at. * @param headingPitchRange The offset of the camera from the target position. * @return The camera view. */ static fromLookAt = function (targetPosition, headingPitchRange) { const positionENU = offsetFromHeadingPitchRange(headingPitchRange.heading, -headingPitchRange.pitch, headingPitchRange.range, scratchPosition); const directionENU = Cartesian3.normalize(Cartesian3.negate(positionENU, scratchDirection), scratchDirection); const rightENU = Cartesian3.cross(directionENU, Cartesian3.UNIT_Z, scratchRight); if (Cartesian3.magnitudeSquared(rightENU) < CesiumMath.EPSILON10) { Cartesian3.clone(Cartesian3.UNIT_X, rightENU); } Cartesian3.normalize(rightENU, rightENU); const upENU = Cartesian3.cross(rightENU, directionENU, scratchUp); Cartesian3.normalize(upENU, upENU); const targetCartesian = Ellipsoid.WGS84.cartographicToCartesian(targetPosition, scratchTarget); const transform = Transforms.eastNorthUpToFixedFrame(targetCartesian, Ellipsoid.WGS84, scratchMatrix4); const offsetECF = Matrix4.multiplyByPointAsVector(transform, positionENU, scratchOffset); const position = Cartesian3.add(targetCartesian, offsetECF, new Cartesian3()); const direction = Cartesian3.normalize(Cartesian3.negate(offsetECF, new Cartesian3()), new Cartesian3()); const up = Matrix4.multiplyByPointAsVector(transform, upENU, new Cartesian3()); // Estimate a rectangle for this view. const fieldOfViewHalfAngle = CesiumMath.toRadians(30); const groundDistance = Math.tan(fieldOfViewHalfAngle) * (headingPitchRange.range + targetPosition.height); const angle = groundDistance / Ellipsoid.WGS84.minimumRadius; const extent = new Rectangle(targetPosition.longitude - angle, targetPosition.latitude - angle, targetPosition.longitude + angle, targetPosition.latitude + angle); return new CameraView(extent, position, direction, up); }; /** * Constructs a {@link CameraView} from a camera position and heading, pitch, and roll angles for the camera. * @param cameraPosition The position of the camera. * @param heading The heading of the camera in radians measured from North toward East. * @param pitch The pitch of the camera in radians measured from the local horizontal. Positive angles look up, negative angles look down. * @param roll The roll of the camera in radians counterclockwise. */ static fromPositionHeadingPitchRoll(cameraPosition, heading, pitch, roll) { const hpr = new HeadingPitchRoll(heading - CesiumMath.PI_OVER_TWO, pitch, roll); const rotQuat = Quaternion.fromHeadingPitchRoll(hpr, scratchQuaternion); const rotMat = Matrix3.fromQuaternion(rotQuat, scratchMatrix3); const directionENU = Matrix3.getColumn(rotMat, 0, scratchDirection); const upENU = Matrix3.getColumn(rotMat, 2, scratchUp); const positionECF = Ellipsoid.WGS84.cartographicToCartesian(cameraPosition, scratchTarget); const transform = Transforms.eastNorthUpToFixedFrame(positionECF, Ellipsoid.WGS84, scratchMatrix4); const directionECF = Matrix4.multiplyByPointAsVector(transform, directionENU, new Cartesian3()); const upECF = Matrix4.multiplyByPointAsVector(transform, upENU, new Cartesian3()); // Estimate a rectangle for this view. const fieldOfViewHalfAngle = CesiumMath.toRadians(30); const groundDistance = Math.tan(fieldOfViewHalfAngle) * cameraPosition.height; const angle = groundDistance / Ellipsoid.WGS84.minimumRadius; const extent = new Rectangle(cameraPosition.longitude - angle, cameraPosition.latitude - angle, cameraPosition.longitude + angle, cameraPosition.latitude + angle); return new CameraView(extent, positionECF, directionECF, upECF); } } function isVector(value) { return (isJsonObject(value) && isJsonNumber(value.x) && isJsonNumber(value.y) && isJsonNumber(value.z)); } const scratchPosition = new Cartesian3(); const scratchOffset = new Cartesian3(); const scratchDirection = new Cartesian3(); const scratchRight = new Cartesian3(); const scratchUp = new Cartesian3(); const scratchTarget = new Cartesian3(); const scratchMatrix4 = new Matrix4(); const scratchQuaternion = new Quaternion(); const scratchMatrix3 = new Matrix3(); const scratchLookAtHeadingPitchRangeQuaternion1 = new Quaternion(); const scratchLookAtHeadingPitchRangeQuaternion2 = new Quaternion(); const scratchHeadingPitchRangeMatrix3 = new Matrix3(); function offsetFromHeadingPitchRange(heading, pitch, range, result) { 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, result); Matrix3.multiplyByVector(rotMatrix, offset, offset); Cartesian3.negate(offset, offset); Cartesian3.multiplyByScalar(offset, range, offset); return offset; } //# sourceMappingURL=CameraView.js.map