311 lines
9.3 KiB
JavaScript
311 lines
9.3 KiB
JavaScript
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 <code>undefined</code> if no position could be picked. If <code>undefined</code> 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 <code>[0, 1)</code> 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;
|