import Cartesian2 from "../../Core/Cartesian2.js"; import Cartesian3 from "../../Core/Cartesian3.js"; import defined from "../../Core/defined.js"; import Frozen from "../../Core/Frozen.js"; import getTimestamp from "../../Core/getTimestamp.js"; import CesiumMath from "../../Core/Math.js"; import ScreenSpaceEventHandler from "../../Core/ScreenSpaceEventHandler.js"; import TimeConstants from "../../Core/TimeConstants.js"; import defaultPickWorldPosition from "./defaultPickWorldPosition.js"; import ScreenSpaceInputBindings from "./ScreenSpaceInputBindings.js"; import MouseButton from "./MouseButton.js"; /** * @typedef {object} ControllerOptions * @memberof ScreenSpaceMapCameraController * @property {ScreenSpaceInputBindings.InputBinding[]} [dragInputs] The drag input bindings that control panning. */ /** * A camera controller that allows panning the camera tangential to the ellipsoid in screen space * by clicking and dragging the mouse. * @class * @implements Controller * @example * viewer.scene.screenSpaceCameraController.enableInputs = false; * * const mapCameraController = new Cesium.ScreenSpaceMapCameraController(); * viewer.addController(mapCameraController); * * @example * // Configure the controller to use the right mouse button for panning instead of the default left mouse button. * const mapCameraController = new Cesium.ScreenSpaceMapCameraController({ * dragInputs: [{ button: Cesium.MouseButton.RIGHT}] * }); * viewer.addController(mapCameraController); */ class ScreenSpaceMapCameraController { /** * @private * @returns {ScreenSpaceInputBindings.InputBinding[]} The default drag input bindings. */ static _getDefaultDragInputs() { return [ Object.freeze({ button: MouseButton.LEFT, }), ]; } /** * Creates an instance of a ScreenSpaceMapCameraController. * @param {ScreenSpaceMapCameraController.ControllerOptions} [options] The options for configuring the controller. */ constructor(options = Frozen.EMPTY_OBJECT) { this._enabled = true; this._handler = undefined; this._lastUpdateTime = undefined; /** * The drag input bindings that map panning. Each binding is a combination of the mouse button * and an optional keyboard modifier. * @type {ScreenSpaceInputBindings.InputBinding[]} * @see ScreenSpaceEventHandler */ this.dragInputs = options.dragInputs ?? ScreenSpaceMapCameraController._getDefaultDragInputs(); this._dragInputState = undefined; this._panDelta = new Cartesian2(); this._panPosition = new Cartesian2(); /** * A callback function used to pick the world position from which to pan. The function is called with {@link Scene}, the {@link Cartesian2} screen space position, and a {@link Cartesian3} instance to store the result. The function should return the {@link Cartesian3} world position from which to pan, or undefined if no position could be picked. If undefined is returned, the camera will pan relative to the ellipsoid surface below the camera. * @type {Function(Scene, Cartesian2, Cartesian3): Cartesian3|undefined} * @default defaultPickWorldPosition * @example * const mapCameraController = new Cesium.ScreenSpaceMapCameraController(); * mapCameraController.pickWorldPosition = function (scene, windowPosition, result) { * // Pick the world position from the depth buffer * return scene.pickPosition(windowPosition, result); * }; * viewer.addController(mapCameraController); */ this.pickWorldPosition = defaultPickWorldPosition; this._ellipsoidNormal = new Cartesian3(); this._ellipsoidSurfacePosition = new Cartesian3(); this._panDirectionX = new Cartesian3(); this._panDirectionY = new Cartesian3(); this._pixelSize = new Cartesian2(); this._panVelocity = new Cartesian2(); /** * The speed in meters per pixel at which the camera pans. * @type {number} * @default 1.0 */ this.panSpeed = 1.0; /** * Enable or disable inertia when panning. When enabled, the camera will continue to move after the user stops dragging, gradually slowing down based on {@link ScreenSpaceMapCameraController#inertialDecay}. * @type {boolean} * @default true */ this.inertiaEnabled = true; /** * The rate at which the camera's pan velocity decays over time. * @type {number} * @default 6.0 */ this.inertialDecay = 6.0; /** * A parameter in the range [0, 1) used to limit the range * of inputs to a percentage of the window width/height per animation frame. * This helps keep the camera under control in low-frame-rate situations. * @type {number} * @default 0.1 */ this.maximumMovementRatio = 0.1; } /** * @inheritdoc */ get enabled() { return this._enabled; } set enabled(value) { this._enabled = value; if (value) { this._panDelta.x = 0; this._panDelta.y = 0; } else if (defined(this._dragInputState)) { this._dragInputState.isDragging = false; } } /** * @private * @type {boolean} */ get isDragging() { return defined(this._dragInputState) && this._dragInputState.isDragging; } /** * @inheritdoc * @param {HTMLElement} element The DOM element containing the Cesium scene. */ connectedCallback(element) { const handler = new ScreenSpaceEventHandler(element); this._handler = handler; this._dragInputState = ScreenSpaceInputBindings.registerDragInputBindings( handler, this.dragInputs, { start: this._handleStartPan.bind(this), change: this._handlePan.bind(this), }, ); } /** * @inheritdoc * @param {HTMLElement} element The DOM element containing the Cesium scene. */ disconnectedCallback(element) { const handler = this._handler; if (defined(handler) && !handler.isDestroyed()) { handler.destroy(); } } /** * @inheritdoc */ firstUpdate() { this._lastUpdateTime = getTimestamp(); this._panDelta.x = 0; this._panDelta.y = 0; } /** * @inheritdoc * @param {any} scene */ update(scene) { const dt = (getTimestamp() - this._lastUpdateTime) * TimeConstants.SECONDS_PER_MILLISECOND; let dx = -this._panDelta.x; let dy = this._panDelta.y; if (this.inertiaEnabled && !this.isDragging) { const damping = Math.exp(-this.inertialDecay * dt); this._panVelocity.x *= damping; this._panVelocity.y *= damping; dx = this._panVelocity.x * dt; dy = this._panVelocity.y * dt; } const { camera, ellipsoid, canvas } = scene; const { clientWidth, clientHeight } = canvas; if ( dt === 0 || clientWidth === 0 || clientHeight === 0 || (Math.abs(dx) <= CesiumMath.EPSILON3 && Math.abs(dy) <= CesiumMath.EPSILON3) ) { // Reset for next frame this._lastUpdateTime = getTimestamp(); this._panDelta.x = 0; this._panDelta.y = 0; return; } const windowPosition = this._panPosition; let surface = this.pickWorldPosition( scene, windowPosition, this._ellipsoidSurfacePosition, ); if (!defined(surface)) { surface = ellipsoid.scaleToGeodeticSurface( camera.positionWC, this._ellipsoidSurfacePosition, ); } const zAxis = ellipsoid.geodeticSurfaceNormal( surface, this._ellipsoidNormal, ); let xAxis = Cartesian3.clone(camera.rightWC, this._panDirectionX); xAxis = Cartesian3.normalize(xAxis, this._panDirectionX); // If z-axis is parallel to camera forward, we use the camera up vector to compute the y-axis. Otherwise, we use the z-axis and x-axis to compute the y-axis. let yAxis = Cartesian3.clone(camera.upWC, this._panDirectionY); const theta = Math.abs(Cartesian3.dot(zAxis, camera.directionWC)); if (CesiumMath.lessThan(theta, 1.0, CesiumMath.EPSILON6)) { yAxis = Cartesian3.cross(zAxis, xAxis, this._panDirectionY); } yAxis = Cartesian3.normalize(yAxis, this._panDirectionY); const distance = Cartesian3.distance(camera.positionWC, surface); const { drawingBufferWidth, drawingBufferHeight, pixelRatio } = scene; const pixelSize = camera.frustum.getPixelDimensions( drawingBufferWidth, drawingBufferHeight, distance, pixelRatio, this._pixelSize, ); const maxPixels = this.maximumMovementRatio * Math.max(clientWidth, clientHeight); dx = CesiumMath.clamp(dx, -maxPixels, maxPixels); this._panVelocity.x = dx / dt; dx *= this.panSpeed * pixelSize.x; dy = CesiumMath.clamp(dy, -maxPixels, maxPixels); this._panVelocity.y = dy / dt; dy *= this.panSpeed * pixelSize.y; camera.move(xAxis, dx); camera.move(yAxis, dy); // Reset for next frame this._lastUpdateTime = getTimestamp(); this._panDelta.x = 0; this._panDelta.y = 0; } /** * @private * @param {Event} event */ _handleStartPan(event) { if (!this.enabled) { return; } this._panDelta.x = 0; this._panDelta.y = 0; } /** * @private */ _handlePan(event) { this._panDelta.x += event.endPosition.x - event.startPosition.x; this._panDelta.y += event.endPosition.y - event.startPosition.y; this._panPosition.x = event.endPosition.x; this._panPosition.y = event.endPosition.y; } } export default ScreenSpaceMapCameraController;