itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
1,219 lines (1,022 loc) • 56.5 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = exports.CONTROL_EVENTS = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var THREE = _interopRequireWildcard(require("three"));
var _AnimationPlayer = _interopRequireDefault(require("../Core/AnimationPlayer"));
var _Coordinates = _interopRequireDefault(require("../Core/Geographic/Coordinates"));
var _Ellipsoid = require("../Core/Math/Ellipsoid");
var _CameraUtils = _interopRequireDefault(require("../Utils/CameraUtils"));
var _StateControl = _interopRequireDefault(require("./StateControl"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = (0, _getPrototypeOf2["default"])(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2["default"])(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
// private members
var EPS = 0.000001;
var direction = {
up: new THREE.Vector2(0, 1),
bottom: new THREE.Vector2(0, -1),
left: new THREE.Vector2(1, 0),
right: new THREE.Vector2(-1, 0)
}; // Orbit
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var spherical = new THREE.Spherical(1.0, 0.01, 0);
var sphericalDelta = new THREE.Spherical(1.0, 0, 0);
var orbitScale = 1.0; // Pan
var panStart = new THREE.Vector2();
var panEnd = new THREE.Vector2();
var panDelta = new THREE.Vector2();
var panOffset = new THREE.Vector3(); // Dolly
var dollyStart = new THREE.Vector2();
var dollyEnd = new THREE.Vector2();
var dollyDelta = new THREE.Vector2();
var dollyScale; // Globe move
var moveAroundGlobe = new THREE.Quaternion();
var cameraTarget = new THREE.Object3D();
cameraTarget.matrixWorldInverse = new THREE.Matrix4();
var xyz = new _Coordinates["default"]('EPSG:4978', 0, 0, 0);
var c = new _Coordinates["default"]('EPSG:4326', 0, 0, 0); // Position object on globe
function positionObject(newPosition, object) {
xyz.setFromVector3(newPosition).as('EPSG:4326', c);
object.position.copy(newPosition);
object.lookAt(c.geodesicNormal.add(newPosition));
object.rotateX(Math.PI * 0.5);
object.updateMatrixWorld(true);
} // Save the last time of mouse move for damping
var lastTimeMouseMove = 0; // Animations and damping
var enableAnimation = true;
var dampingFactorDefault = 0.25;
var dampingMove = new THREE.Quaternion(0, 0, 0, 1);
var durationDampingMove = 120;
var durationDampingOrbital = 60; // Pan Move
var panVector = new THREE.Vector3(); // Save last transformation
var lastPosition = new THREE.Vector3();
var lastQuaternion = new THREE.Quaternion(); // Tangent sphere to ellipsoid
var pickSphere = new THREE.Sphere();
var pickingPoint = new THREE.Vector3(); // Sphere intersection
var intersection = new THREE.Vector3(); // Set to true to enable target helper
var enableTargetHelper = false;
var helpers = {};
/**
* Globe control pan event. Fires after camera pan
* @event GlobeControls#pan-changed
* @property target {GlobeControls} dispatched on controls
* @property type {string} orientation-changed
*/
/**
* Globe control orientation event. Fires when camera's orientation change
* @event GlobeControls#orientation-changed
* @property new {object}
* @property new.tilt {number} the new value of the tilt of the camera
* @property new.heading {number} the new value of the heading of the camera
* @property previous {object}
* @property previous.tilt {number} the previous value of the tilt of the camera
* @property previous.heading {number} the previous value of the heading of the camera
* @property target {GlobeControls} dispatched on controls
* @property type {string} orientation-changed
*/
/**
* Globe control range event. Fires when camera's range to target change
* @event GlobeControls#range-changed
* @property new {number} the new value of the range
* @property previous {number} the previous value of the range
* @property target {GlobeControls} dispatched on controls
* @property type {string} range-changed
*/
/**
* Globe control camera's target event. Fires when camera's target change
* @event GlobeControls#camera-target-changed
* @property new {object}
* @property new {Coordinates} the new camera's target coordinates
* @property previous {Coordinates} the previous camera's target coordinates
* @property target {GlobeControls} dispatched on controls
* @property type {string} camera-target-changed
*/
/**
* globe controls events
* @property PAN_CHANGED {string} Fires after camera pan
* @property ORIENTATION_CHANGED {string} Fires when camera's orientation change
* @property RANGE_CHANGED {string} Fires when camera's range to target change
* @property CAMERA_TARGET_CHANGED {string} Fires when camera's target change
*/
var CONTROL_EVENTS = {
PAN_CHANGED: 'pan-changed',
ORIENTATION_CHANGED: 'orientation-changed',
RANGE_CHANGED: 'range-changed',
CAMERA_TARGET_CHANGED: 'camera-target-changed'
};
exports.CONTROL_EVENTS = CONTROL_EVENTS;
var quaterPano = new THREE.Quaternion();
var quaterAxis = new THREE.Quaternion();
var axisX = new THREE.Vector3(1, 0, 0);
var minDistanceZ = Infinity;
var lastNormalizedIntersection = new THREE.Vector3();
var normalizedIntersection = new THREE.Vector3();
var raycaster = new THREE.Raycaster();
var targetPosition = new THREE.Vector3();
var pickedPosition = new THREE.Vector3();
var sphereCamera = new THREE.Sphere();
var previous;
/**
* GlobeControls is a camera controller
*
* @class GlobeControls
* @param {GlobeView} view the view where the control will be used
* @param {CameraTransformOptions|Extent} placement the {@link CameraTransformOptions} to apply to view's camera
* or the extent it must display at initialisation, see {@link CameraTransformOptions} in {@link CameraUtils}.
* @param {object} options
* @param {number} [options.zoomFactor=2] The factor the scale is multiplied by when dollying (zooming) in or
* divided by when dollying out.
* @param {number} options.rotateSpeed Speed camera rotation in orbit and panoramic mode
* @param {number} options.minDistance Minimum distance between ground and camera
* @param {number} options.maxDistance Maximum distance between ground and camera
* @param {bool} options.handleCollision enable collision camera with ground
* @property {number} minDistance Minimum distance between ground and camera
* @property {number} maxDistance Maximum distance between ground and camera
* @property {number} zoomSpeed Speed zoom with mouse
* @property {number} rotateSpeed Speed camera rotation in orbit and panoramic mode
* @property {number} minDistanceCollision Minimum distance collision between ground and camera
* @property {boolean} enableDamping enable camera damping, if it's disabled the camera immediately when the mouse button is released.
* If it's enabled, the camera movement is decelerate.
*/
var GlobeControls = /*#__PURE__*/function (_THREE$EventDispatche) {
(0, _inherits2["default"])(GlobeControls, _THREE$EventDispatche);
var _super = _createSuper(GlobeControls);
function GlobeControls(view, placement) {
var _this;
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
(0, _classCallCheck2["default"])(this, GlobeControls);
_this = _super.call(this);
_this.player = new _AnimationPlayer["default"]();
_this.view = view;
_this.camera = view.camera.camera3D; // State control
_this.states = new _StateControl["default"](_this.view); // this.enabled property has moved to StateControl
Object.defineProperty((0, _assertThisInitialized2["default"])(_this), 'enabled', {
get: function get() {
return _this.states.enabled;
},
set: function set(value) {
console.warn('GlobeControls.enabled property is deprecated. Use StateControl.enabled instead ' + '- which you can access with GlobeControls.states.enabled.');
_this.states.enabled = value;
}
}); // These options actually enables dollying in and out; left as "zoom" for
// backwards compatibility
if (options.zoomSpeed) {
console.warn('Controls zoomSpeed parameter is deprecated. Use zoomInFactor and zoomOutFactor instead.');
options.zoomFactor = options.zoomFactor || options.zoomSpeed;
}
_this.zoomFactor = options.zoomFactor || 1.25; // Limits to how far you can dolly in and out ( PerspectiveCamera only )
_this.minDistance = options.minDistance || 250;
_this.maxDistance = options.maxDistance || _Ellipsoid.ellipsoidSizes.x * 8.0; // Limits to how far you can zoom in and out ( OrthographicCamera only )
_this.minZoom = 0;
_this.maxZoom = Infinity; // Set to true to disable this control
_this.rotateSpeed = options.rotateSpeed || 0.25; // Set to true to disable this control
_this.keyPanSpeed = 7.0; // pixels moved per arrow key push
// How far you can orbit vertically, upper and lower limits.
// Range is 0 to Math.PI radians.
// TODO Warning minPolarAngle = 0.01 -> it isn't possible to be perpendicular on Globe
_this.minPolarAngle = THREE.MathUtils.degToRad(0.5); // radians
_this.maxPolarAngle = THREE.MathUtils.degToRad(86); // radians
// How far you can orbit horizontally, upper and lower limits.
// If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ].
_this.minAzimuthAngle = -Infinity; // radians
_this.maxAzimuthAngle = Infinity; // radians
// Set collision options
_this.handleCollision = typeof options.handleCollision !== 'undefined' ? options.handleCollision : true;
_this.minDistanceCollision = 60; // this.enableKeys property has moved to StateControl
Object.defineProperty((0, _assertThisInitialized2["default"])(_this), 'enableKeys', {
get: function get() {
return _this.states.enableKeys;
},
set: function set(value) {
console.warn('GlobeControls.enableKeys property is deprecated. Use StateControl.enableKeys instead ' + '- which you can access with GlobeControls.states.enableKeys.');
_this.states.enableKeys = value;
}
}); // Enable Damping
_this.enableDamping = true;
_this.dampingMoveFactor = options.dampingMoveFactor != undefined ? options.dampingMoveFactor : dampingFactorDefault;
_this.startEvent = {
type: 'start'
};
_this.endEvent = {
type: 'end'
}; // Update helper
_this.updateHelper = enableTargetHelper ? function (position, helper) {
positionObject(position, helper);
view.notifyChange(_this.camera);
} : function () {};
_this._onEndingMove = null;
_this._onTravel = _this.travel.bind((0, _assertThisInitialized2["default"])(_this));
_this._onTouchStart = _this.onTouchStart.bind((0, _assertThisInitialized2["default"])(_this));
_this._onTouchEnd = _this.onTouchEnd.bind((0, _assertThisInitialized2["default"])(_this));
_this._onTouchMove = _this.onTouchMove.bind((0, _assertThisInitialized2["default"])(_this));
_this._onStateChange = _this.onStateChange.bind((0, _assertThisInitialized2["default"])(_this));
_this._onRotation = _this.handleRotation.bind((0, _assertThisInitialized2["default"])(_this));
_this._onDrag = _this.handleDrag.bind((0, _assertThisInitialized2["default"])(_this));
_this._onDolly = _this.handleDolly.bind((0, _assertThisInitialized2["default"])(_this));
_this._onPan = _this.handlePan.bind((0, _assertThisInitialized2["default"])(_this));
_this._onPanoramic = _this.handlePanoramic.bind((0, _assertThisInitialized2["default"])(_this));
_this._onZoom = _this.handleZoom.bind((0, _assertThisInitialized2["default"])(_this));
_this.states.addEventListener('state-changed', _this._onStateChange, false);
_this.states.addEventListener(_this.states.ORBIT._event, _this._onRotation, false);
_this.states.addEventListener(_this.states.MOVE_GLOBE._event, _this._onDrag, false);
_this.states.addEventListener(_this.states.DOLLY._event, _this._onDolly, false);
_this.states.addEventListener(_this.states.PAN._event, _this._onPan, false);
_this.states.addEventListener(_this.states.PANORAMIC._event, _this._onPanoramic, false);
_this.states.addEventListener('zoom', _this._onZoom, false);
_this.view.domElement.addEventListener('touchstart', _this._onTouchStart, false);
_this.view.domElement.addEventListener('touchend', _this._onTouchEnd, false);
_this.view.domElement.addEventListener('touchmove', _this._onTouchMove, false);
_this.states.addEventListener(_this.states.TRAVEL_IN._event, _this._onTravel, false);
_this.states.addEventListener(_this.states.TRAVEL_OUT._event, _this._onTravel, false);
view.scene.add(cameraTarget);
if (enableTargetHelper) {
cameraTarget.add(helpers.target);
view.scene.add(helpers.picking);
var layerTHREEjs = view.mainLoop.gfxEngine.getUniqueThreejsLayer();
helpers.target.layers.set(layerTHREEjs);
helpers.picking.layers.set(layerTHREEjs);
_this.camera.layers.enable(layerTHREEjs);
}
if (placement.isExtent) {
placement.center().as('EPSG:4978', xyz);
} else {
placement.coord.as('EPSG:4978', xyz);
placement.tilt = placement.tilt || 89.5;
placement.heading = placement.heading || 0;
}
positionObject(xyz, cameraTarget);
_this.lookAtCoordinate(placement, false);
return _this;
}
(0, _createClass2["default"])(GlobeControls, [{
key: "dollyInScale",
get: function get() {
return this.zoomFactor;
}
}, {
key: "dollyOutScale",
get: function get() {
return 1 / this.zoomFactor;
}
}, {
key: "isPaused",
get: function get() {
// TODO : also check if CameraUtils is performing an animation
return this.states.currentState === this.states.NONE && !this.player.isPlaying();
}
}, {
key: "onEndingMove",
value: function onEndingMove(current) {
if (this._onEndingMove) {
this.player.removeEventListener('animation-stopped', this._onEndingMove);
this._onEndingMove = null;
}
this.handlingEvent(current);
}
}, {
key: "rotateLeft",
value: function rotateLeft() {
var angle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
sphericalDelta.theta -= angle;
}
}, {
key: "rotateUp",
value: function rotateUp() {
var angle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
sphericalDelta.phi -= angle;
} // pass in distance in world space to move left
}, {
key: "panLeft",
value: function panLeft(distance) {
var te = this.camera.matrix.elements; // get X column of matrix
panOffset.fromArray(te);
panOffset.multiplyScalar(-distance);
panVector.add(panOffset);
} // pass in distance in world space to move up
}, {
key: "panUp",
value: function panUp(distance) {
var te = this.camera.matrix.elements; // get Y column of matrix
panOffset.fromArray(te, 4);
panOffset.multiplyScalar(distance);
panVector.add(panOffset);
} // pass in x,y of change desired in pixel space,
// right and down are positive
}, {
key: "mouseToPan",
value: function mouseToPan(deltaX, deltaY) {
var gfx = this.view.mainLoop.gfxEngine;
if (this.camera.isPerspectiveCamera) {
var targetDistance = this.camera.position.distanceTo(this.getCameraTargetPosition()); // half of the fov is center to top of screen
targetDistance *= 2 * Math.tan(THREE.MathUtils.degToRad(this.camera.fov * 0.5)); // we actually don't use screenWidth, since perspective camera is fixed to screen height
this.panLeft(deltaX * targetDistance / gfx.width * this.camera.aspect);
this.panUp(deltaY * targetDistance / gfx.height);
} else if (this.camera.isOrthographicCamera) {
// orthographic
this.panLeft(deltaX * (this.camera.right - this.camera.left) / gfx.width);
this.panUp(deltaY * (this.camera.top - this.camera.bottom) / gfx.height);
}
}
}, {
key: "dolly",
value: function dolly(delta) {
if (delta === 0) {
return;
}
dollyScale = delta > 0 ? this.dollyInScale : this.dollyOutScale;
if (this.camera.isPerspectiveCamera) {
orbitScale /= dollyScale;
} else if (this.camera.isOrthographicCamera) {
this.camera.zoom = THREE.MathUtils.clamp(this.camera.zoom * dollyScale, this.minZoom, this.maxZoom);
this.camera.updateProjectionMatrix();
this.view.notifyChange(this.camera);
}
}
}, {
key: "getMinDistanceCameraBoundingSphereObbsUp",
value: function getMinDistanceCameraBoundingSphereObbsUp(tile) {
if (tile.level > 10 && tile.children.length == 1 && tile.geometry) {
var obb = tile.obb;
sphereCamera.center.copy(this.camera.position);
sphereCamera.radius = this.minDistanceCollision;
if (obb.isSphereAboveXYBox(sphereCamera)) {
minDistanceZ = Math.min(sphereCamera.center.z - obb.box3D.max.z, minDistanceZ);
}
}
}
}, {
key: "update",
value: function update() {
var _this2 = this;
var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.states.currentState;
// We compute distance between camera's bounding sphere and geometry's obb up face
minDistanceZ = Infinity;
if (this.handleCollision) {
// We check distance to the ground/surface geometry
// add minDistanceZ between camera's bounding and tiles's oriented bounding box (up face only)
// Depending on the distance of the camera with obbs, we add a slowdown or constrain to the movement.
// this constraint or deceleration is suitable for two types of movement MOVE_GLOBE and ORBIT.
// This constraint or deceleration inversely proportional to the camera/obb distance
if (this.view.tileLayer) {
var _iterator = _createForOfIteratorHelper(this.view.tileLayer.level0Nodes),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var tile = _step.value;
tile.traverse(this.getMinDistanceCameraBoundingSphereObbsUp.bind(this));
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
}
switch (state) {
// MOVE_GLOBE Rotate globe with mouse
case this.states.MOVE_GLOBE:
if (minDistanceZ < 0) {
cameraTarget.translateY(-minDistanceZ);
this.camera.position.setLength(this.camera.position.length() - minDistanceZ);
} else if (minDistanceZ < this.minDistanceCollision) {
var translate = this.minDistanceCollision * (1.0 - minDistanceZ / this.minDistanceCollision);
cameraTarget.translateY(translate);
this.camera.position.setLength(this.camera.position.length() + translate);
}
lastNormalizedIntersection.copy(normalizedIntersection).applyQuaternion(moveAroundGlobe);
cameraTarget.position.applyQuaternion(moveAroundGlobe);
this.camera.position.applyQuaternion(moveAroundGlobe);
break;
// PAN Move camera in projection plan
case this.states.PAN:
this.camera.position.add(panVector);
cameraTarget.position.add(panVector);
break;
// PANORAMIC Move target camera
case this.states.PANORAMIC:
{
this.camera.worldToLocal(cameraTarget.position);
var normal = this.camera.position.clone().normalize().applyQuaternion(this.camera.quaternion.clone().invert());
quaterPano.setFromAxisAngle(normal, sphericalDelta.theta).multiply(quaterAxis.setFromAxisAngle(axisX, sphericalDelta.phi));
cameraTarget.position.applyQuaternion(quaterPano);
this.camera.localToWorld(cameraTarget.position);
break;
}
// ZOOM/ORBIT Move Camera around the target camera
default:
{
// get camera position in local space of target
this.camera.position.applyMatrix4(cameraTarget.matrixWorldInverse); // angle from z-axis around y-axis
if (sphericalDelta.theta || sphericalDelta.phi) {
spherical.setFromVector3(this.camera.position);
} // far underground
var dynamicRadius = spherical.radius * Math.sin(this.minPolarAngle);
var slowdownLimit = dynamicRadius * 8;
var contraryLimit = dynamicRadius * 2;
var minContraintPhi = -0.01;
if (this.handleCollision) {
if (minDistanceZ < slowdownLimit && minDistanceZ > contraryLimit && sphericalDelta.phi > 0) {
// slowdown zone : slowdown sphericalDelta.phi
var slowdownZone = slowdownLimit - contraryLimit; // the deeper the camera is in this zone, the bigger the factor is
var slowdownFactor = 1 - (slowdownZone - (minDistanceZ - contraryLimit)) / slowdownZone; // apply slowdown factor on tilt mouvement
sphericalDelta.phi *= slowdownFactor * slowdownFactor;
} else if (minDistanceZ < contraryLimit && minDistanceZ > -contraryLimit && sphericalDelta.phi > minContraintPhi) {
// contraint zone : contraint sphericalDelta.phi
// calculation of the angle of rotation which allows to leave this zone
var contraryPhi = -Math.asin((contraryLimit - minDistanceZ) * 0.25 / spherical.radius); // clamp contraryPhi to make a less brutal exit
contraryPhi = THREE.MathUtils.clamp(contraryPhi, minContraintPhi, 0); // the deeper the camera is in this zone, the bigger the factor is
var contraryFactor = 1 - (contraryLimit - minDistanceZ) / (2 * contraryLimit);
sphericalDelta.phi = THREE.MathUtils.lerp(sphericalDelta.phi, contraryPhi, contraryFactor);
minDistanceZ -= Math.sin(sphericalDelta.phi) * spherical.radius;
}
}
spherical.theta += sphericalDelta.theta;
spherical.phi += sphericalDelta.phi; // restrict spherical.theta to be between desired limits
spherical.theta = Math.max(this.minAzimuthAngle, Math.min(this.maxAzimuthAngle, spherical.theta)); // restrict spherical.phi to be between desired limits
spherical.phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, spherical.phi));
spherical.radius = this.camera.position.length() * orbitScale; // restrict spherical.phi to be betwee EPS and PI-EPS
spherical.makeSafe(); // restrict radius to be between desired limits
spherical.radius = Math.max(this.minDistance, Math.min(this.maxDistance, spherical.radius));
this.camera.position.setFromSpherical(spherical); // if camera is underground, so move up camera
if (minDistanceZ < 0) {
this.camera.position.y -= minDistanceZ;
spherical.setFromVector3(this.camera.position);
sphericalDelta.phi = 0;
}
cameraTarget.localToWorld(this.camera.position);
}
}
this.camera.up.copy(cameraTarget.position).normalize();
this.camera.lookAt(cameraTarget.position);
if (!this.enableDamping) {
sphericalDelta.theta = 0;
sphericalDelta.phi = 0;
moveAroundGlobe.set(0, 0, 0, 1);
} else {
sphericalDelta.theta *= 1 - dampingFactorDefault;
sphericalDelta.phi *= 1 - dampingFactorDefault;
moveAroundGlobe.slerp(dampingMove, this.dampingMoveFactor * 0.2);
}
orbitScale = 1;
panVector.set(0, 0, 0); // update condition is:
// min(camera displacement, camera rotation in radians)^2 > EPS
// using small-angle approximation cos(x/2) = 1 - x^2 / 8
if (lastPosition.distanceToSquared(this.camera.position) > EPS || 8 * (1 - lastQuaternion.dot(this.camera.quaternion)) > EPS) {
this.view.notifyChange(this.camera);
lastPosition.copy(this.camera.position);
lastQuaternion.copy(this.camera.quaternion);
} // Launch animationdamping if mouse stops these movements
if (this.enableDamping && state === this.states.ORBIT && this.player.isStopped() && (sphericalDelta.theta > EPS || sphericalDelta.phi > EPS)) {
this.player.setCallback(function () {
_this2.update(_this2.states.ORBIT);
});
this.player.playLater(durationDampingOrbital, 2);
}
}
}, {
key: "onStateChange",
value: function onStateChange(event) {
// If the state changed to NONE, end the movement associated to the previous state.
if (this.states.currentState === this.states.NONE) {
this.handleEndMovement(event);
return;
} // Stop CameraUtils ongoing animations, which can for instance be triggered with `this.travel` or
// `this.lookAtCoordinate` methods.
_CameraUtils["default"].stop(this.view, this.camera); // Dispatch events which specify if changes occurred in camera transform options.
this.onEndingMove(); // Stop eventual damping movement.
this.player.stop(); // Update camera transform options.
this.updateTarget();
previous = _CameraUtils["default"].getTransformCameraLookingAtTarget(this.view, this.camera, pickedPosition); // Initialize rotation and panoramic movements.
rotateStart.copy(event.viewCoords); // Initialize drag movement.
if (this.view.getPickingPositionFromDepth(event.viewCoords, pickingPoint)) {
pickSphere.radius = pickingPoint.length();
lastNormalizedIntersection.copy(pickingPoint).normalize();
this.updateHelper(pickingPoint, helpers.picking);
} // Initialize dolly movement.
dollyStart.copy(event.viewCoords); // Initialize pan movement.
panStart.copy(event.viewCoords);
}
}, {
key: "handleRotation",
value: function handleRotation(event) {
// Stop player if needed. Player can be playing while moving mouse in the case of rotation. This is due to the
// fact that a damping move can occur while rotating (without the need of releasing the mouse button)
this.player.stop();
this.handlePanoramic(event);
}
}, {
key: "handleDrag",
value: function handleDrag(event) {
var normalized = this.view.viewToNormalizedCoords(event.viewCoords); // An updateMatrixWorld on the camera prevents camera jittering when moving globe on a zoomed out view, with
// devtools open in web browser.
this.camera.updateMatrixWorld();
raycaster.setFromCamera(normalized, this.camera); // If there's intersection then move globe else we stop the move
if (raycaster.ray.intersectSphere(pickSphere, intersection)) {
normalizedIntersection.copy(intersection).normalize();
moveAroundGlobe.setFromUnitVectors(normalizedIntersection, lastNormalizedIntersection);
lastTimeMouseMove = Date.now();
this.update();
} else {
this.states.onPointerUp();
}
}
}, {
key: "handleDolly",
value: function handleDolly(event) {
dollyEnd.copy(event.viewCoords);
dollyDelta.subVectors(dollyEnd, dollyStart);
this.dolly(-dollyDelta.y);
dollyStart.copy(dollyEnd);
this.update();
}
}, {
key: "handlePan",
value: function handlePan(event) {
if (event.viewCoords) {
panEnd.copy(event.viewCoords);
panDelta.subVectors(panEnd, panStart);
panStart.copy(panEnd);
} else if (event.direction) {
panDelta.copy(direction[event.direction]).multiplyScalar(this.keyPanSpeed);
}
this.mouseToPan(panDelta.x, panDelta.y);
this.update(this.states.PAN);
}
}, {
key: "handlePanoramic",
value: function handlePanoramic(event) {
rotateEnd.copy(event.viewCoords);
rotateDelta.subVectors(rotateEnd, rotateStart);
var gfx = this.view.mainLoop.gfxEngine;
sphericalDelta.theta -= 2 * Math.PI * rotateDelta.x / gfx.width * this.rotateSpeed; // rotating up and down along whole screen attempts to go 360, but limited to 180
sphericalDelta.phi -= 2 * Math.PI * rotateDelta.y / gfx.height * this.rotateSpeed;
rotateStart.copy(rotateEnd);
this.update();
}
}, {
key: "handleEndMovement",
value: function handleEndMovement() {
var _this3 = this;
var event = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
this.dispatchEvent(this.endEvent);
this.player.stop(); // Launch damping movement for :
// * this.states.ORBIT
// * this.states.MOVE_GLOBE
if (this.enableDamping) {
if (event.previous === this.states.ORBIT && (sphericalDelta.theta > EPS || sphericalDelta.phi > EPS)) {
this.player.setCallback(function () {
_this3.update(_this3.states.ORBIT);
});
this.player.play(durationDampingOrbital);
this._onEndingMove = function () {
return _this3.onEndingMove();
};
this.player.addEventListener('animation-stopped', this._onEndingMove);
} else if (event.previous === this.states.MOVE_GLOBE && Date.now() - lastTimeMouseMove < 50) {
this.player.setCallback(function () {
_this3.update(_this3.states.MOVE_GLOBE);
}); // animation since mouse up event occurs less than 50ms after the last mouse move
this.player.play(durationDampingMove);
this._onEndingMove = function () {
return _this3.onEndingMove();
};
this.player.addEventListener('animation-stopped', this._onEndingMove);
} else {
this.onEndingMove();
}
} else {
this.onEndingMove();
}
}
}, {
key: "updateTarget",
value: function updateTarget() {
// Update camera's target position
this.view.getPickingPositionFromDepth(null, pickedPosition);
var distance = !isNaN(pickedPosition.x) ? this.camera.position.distanceTo(pickedPosition) : 100;
targetPosition.set(0, 0, -distance);
this.camera.localToWorld(targetPosition); // set new camera target on globe
positionObject(targetPosition, cameraTarget);
cameraTarget.matrixWorldInverse.copy(cameraTarget.matrixWorld).invert();
targetPosition.copy(this.camera.position);
targetPosition.applyMatrix4(cameraTarget.matrixWorldInverse);
spherical.setFromVector3(targetPosition);
}
}, {
key: "handlingEvent",
value: function handlingEvent(current) {
current = current || _CameraUtils["default"].getTransformCameraLookingAtTarget(this.view, this.camera);
var diff = _CameraUtils["default"].getDiffParams(previous, current);
if (diff) {
if (diff.range) {
this.dispatchEvent({
type: CONTROL_EVENTS.RANGE_CHANGED,
previous: diff.range.previous,
"new": diff.range["new"]
});
}
if (diff.coord) {
this.dispatchEvent({
type: CONTROL_EVENTS.CAMERA_TARGET_CHANGED,
previous: diff.coord.previous,
"new": diff.coord["new"]
});
}
if (diff.tilt || diff.heading) {
var event = {
type: CONTROL_EVENTS.ORIENTATION_CHANGED
};
if (diff.tilt) {
event.previous = {
tilt: diff.tilt.previous
};
event["new"] = {
tilt: diff.tilt["new"]
};
}
if (diff.heading) {
event.previous = event.previous || {};
event["new"] = event["new"] || {};
event["new"].heading = diff.heading["new"];
event.previous.heading = diff.heading.previous;
}
this.dispatchEvent(event);
}
}
}
}, {
key: "travel",
value: function travel(event) {
this.player.stop();
var point = this.view.getPickingPositionFromDepth(event.viewCoords);
var range = this.getRange(point);
if (point && range > this.minDistance) {
return this.lookAtCoordinate({
coord: new _Coordinates["default"]('EPSG:4978', point),
range: range * (event.direction === 'out' ? 1 / 0.6 : 0.6),
time: 1500
});
}
}
}, {
key: "handleZoom",
value: function handleZoom(event) {
this.player.stop();
_CameraUtils["default"].stop(this.view, this.camera);
this.updateTarget();
var delta = -event.delta;
this.dolly(delta);
var previousRange = this.getRange(pickedPosition);
this.update();
var newRange = this.getRange(pickedPosition);
if (Math.abs(newRange - previousRange) / previousRange > 0.001) {
this.dispatchEvent({
type: CONTROL_EVENTS.RANGE_CHANGED,
previous: previousRange,
"new": newRange
});
}
this.dispatchEvent(this.startEvent);
this.dispatchEvent(this.endEvent);
}
}, {
key: "onTouchStart",
value: function onTouchStart(event) {
// CameraUtils.stop(view);
this.player.stop(); // TODO : this.states.enabled check should be removed when moving touch events management to StateControl
if (this.states.enabled === false) {
return;
}
this.state = this.states.touchToState(event.touches.length);
this.updateTarget();
if (this.state !== this.states.NONE) {
switch (this.state) {
case this.states.MOVE_GLOBE:
{
var coords = this.view.eventToViewCoords(event);
if (this.view.getPickingPositionFromDepth(coords, pickingPoint)) {
pickSphere.radius = pickingPoint.length();
lastNormalizedIntersection.copy(pickingPoint).normalize();
this.updateHelper(pickingPoint, helpers.picking);
} else {
this.state = this.states.NONE;
}
break;
}
case this.states.ORBIT:
case this.states.DOLLY:
{
var x = event.touches[0].pageX;
var y = event.touches[0].pageY;
var dx = x - event.touches[1].pageX;
var dy = y - event.touches[1].pageY;
var distance = Math.sqrt(dx * dx + dy * dy);
dollyStart.set(0, distance);
rotateStart.set(x, y);
break;
}
case this.states.PAN:
panStart.set(event.touches[0].pageX, event.touches[0].pageY);
break;
default:
}
this.dispatchEvent(this.startEvent);
}
}
}, {
key: "onTouchMove",
value: function onTouchMove(event) {
if (this.player.isPlaying()) {
this.player.stop();
} // TODO : this.states.enabled check should be removed when moving touch events management to StateControl
if (this.states.enabled === false) {
return;
}
event.preventDefault();
event.stopPropagation();
switch (event.touches.length) {
case this.states.MOVE_GLOBE.finger:
{
var coords = this.view.eventToViewCoords(event);
var normalized = this.view.viewToNormalizedCoords(coords); // An updateMatrixWorld on the camera prevents camera jittering when moving globe on a zoomed out view, with
// devtools open in web browser.
this.camera.updateMatrixWorld();
raycaster.setFromCamera(normalized, this.camera); // If there's intersection then move globe else we stop the move
if (raycaster.ray.intersectSphere(pickSphere, intersection)) {
normalizedIntersection.copy(intersection).normalize();
moveAroundGlobe.setFromUnitVectors(normalizedIntersection, lastNormalizedIntersection);
lastTimeMouseMove = Date.now();
} else {
this.onTouchEnd();
}
break;
}
case this.states.ORBIT.finger:
case this.states.DOLLY.finger:
{
var gfx = this.view.mainLoop.gfxEngine;
rotateEnd.set(event.touches[0].pageX, event.touches[0].pageY);
rotateDelta.subVectors(rotateEnd, rotateStart); // rotating across whole screen goes 360 degrees around
this.rotateLeft(2 * Math.PI * rotateDelta.x / gfx.width * this.rotateSpeed); // rotating up and down along whole screen attempts to go 360, but limited to 180
this.rotateUp(2 * Math.PI * rotateDelta.y / gfx.height * this.rotateSpeed);
rotateStart.copy(rotateEnd);
var dx = event.touches[0].pageX - event.touches[1].pageX;
var dy = event.touches[0].pageY - event.touches[1].pageY;
var distance = Math.sqrt(dx * dx + dy * dy);
dollyEnd.set(0, distance);
dollyDelta.subVectors(dollyEnd, dollyStart);
this.dolly(dollyDelta.y);
dollyStart.copy(dollyEnd);
break;
}
case this.states.PAN.finger:
panEnd.set(event.touches[0].pageX, event.touches[0].pageY);
panDelta.subVectors(panEnd, panStart);
this.mouseToPan(panDelta.x, panDelta.y);
panStart.copy(panEnd);
break;
default:
this.state = this.states.NONE;
}
if (this.state !== this.states.NONE) {
this.update(this.state);
}
}
}, {
key: "onTouchEnd",
value: function onTouchEnd() {
this.handleEndMovement({
previous: this.state
});
this.state = this.states.NONE;
}
}, {
key: "dispose",
value: function dispose() {
this.view.domElement.removeEventListener('touchstart', this._onTouchStart, false);
this.view.domElement.removeEventListener('touchend', this._onTouchEnd, false);
this.view.domElement.removeEventListener('touchmove', this._onTouchMove, false);
this.states.dispose();
this.states.removeEventListener('state-changed', this._onStateChange, false);
this.states.removeEventListener(this.states.ORBIT._event, this._onRotation, false);
this.states.removeEventListener(this.states.MOVE_GLOBE._event, this._onDrag, false);
this.states.removeEventListener(this.states.DOLLY._event, this._onDolly, false);
this.states.removeEventListener(this.states.PAN._event, this._onPan, false);
this.states.removeEventListener(this.states.PANORAMIC._event, this._onPanoramic, false);
this.states.removeEventListener('zoom', this._onZoom, false);
this.states.removeEventListener(this.states.TRAVEL_IN._event, this._onTravel, false);
this.states.removeEventListener(this.states.TRAVEL_OUT._event, this._onTravel, false);
this.dispatchEvent({
type: 'dispose'
});
}
/**
* Changes the tilt of the current camera, in degrees.
* @param {number} tilt
* @param {boolean} isAnimated
* @return {Promise<void>}
*/
}, {
key: "setTilt",
value: function setTilt(tilt, isAnimated) {
return this.lookAtCoordinate({
tilt: tilt
}, isAnimated);
}
/**
* Changes the heading of the current camera, in degrees.
* @param {number} heading
* @param {boolean} isAnimated
* @return {Promise<void>}
*/
}, {
key: "setHeading",
value: function setHeading(heading, isAnimated) {
return this.lookAtCoordinate({
heading: heading
}, isAnimated);
}
/**
* Sets the "range": the distance in meters between the camera and the current central point on the screen.
* @param {number} range
* @param {boolean} isAnimated
* @return {Promise<void>}
*/
}, {
key: "setRange",
value: function setRange(range, isAnimated) {
return this.lookAtCoordinate({
range: range
}, isAnimated);
}
/**
* Returns the {@linkcode Coordinates} of the globe point targeted by the camera in EPSG:4978 projection. See {@linkcode Coordinates} for conversion
* @return {THREE.Vector3} position
*/
}, {
key: "getCameraTargetPosition",
value: function getCameraTargetPosition() {
return cameraTarget.position;
}
/**
* Returns the "range": the distance in meters between the camera and the current central point on the screen.
* @param {THREE.Vector3} [position] - The position to consider as picked on
* the ground.
* @return {number} number
*/
}, {
key: "getRange",
value: function getRange(position) {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this.view, this.camera, position).range;
}
/**
* Returns the tilt of the current camera in degrees.
* @param {THREE.Vector3} [position] - The position to consider as picked on
* the ground.
* @return {number} The angle of the rotation in degrees.
*/
}, {
key: "getTilt",
value: function getTilt(position) {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this.view, this.camera, position).tilt;
}
/**
* Returns the heading of the current camera in degrees.
* @param {THREE.Vector3} [position] - The position to consider as picked on
* the ground.
* @return {number} The angle of the rotation in degrees.
*/
}, {
key: "getHeading",
value: function getHeading(position) {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this.view, this.camera, position).heading;
}
/**
* Displaces the central point to a specific amount of pixels from its current position.
* The view flies to the desired coordinate, i.e.is not teleported instantly. Note : The results can be strange in some cases, if ever possible, when e.g.the camera looks horizontally or if the displaced center would not pick the ground once displaced.
* @param {vector} pVector The vector
* @return {Promise}
*/
}, {
key: "pan",
value: function pan(pVector) {
this.mouseToPan(pVector.x, pVector.y);
this.update(this.states.PAN);
return Promise.resolve();
}
/**
* Returns the orientation angles of the current camera, in degrees.
* @return {Array<number>}
*/
}, {
key: "getCameraOrientation",
value: function getCameraOrientation() {
this.view.getPickingPositionFromDepth(null, pickedPosition);
return [this.getTilt(pickedPosition), this.getHeading(pickedPosition)];
}
/**
* Returns the camera location projected on the ground in lat,lon. See {@linkcode Coordinates} for conversion.
* @return {Coordinates} position
*/
}, {
key: "getCameraCoordinate",
value: function getCameraCoordinate() {
return new _Coordinates["default"]('EPSG:4978', this.camera.position).as('EPSG:4326');
}
/**
* Returns the {@linkcode Coordinates} of the central point on screen in lat,lon. See {@linkcode Coordinates} for conversion.
* @return {Coordinates} coordinate
*/
}, {
key: "getLookAtCoordinate",
value: function getLookAtCoordinate() {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this.view, this.camera).coord;
}
/**
* Sets the animation enabled.
* @param {boolean} enable enable
*/
}, {
key: "setAnimationEnabled",
value: function setAnimationEnabled(enable) {
enableAnimation = enable;
}
/**
* Determines if animation enabled.
* @return {boolean} True if animation enabled, False otherwise.
*/
}, {
key: "isAnimationEnabled",
value: function isAnimationEnabled() {
return enableAnimation;
}
/**
* Returns the actual zoom. The zoom will always be between the [getMinZoom(), getMaxZoom()].
* @return {number} The zoom .
*/
}, {
key: "getZoom",
value: function getZoom() {
return this.view.tileLayer.computeTileZoomFromDistanceCamera(this.getRange(), this.view.camera);
}
/**
* Sets the current zoom, which is an index in the logical scales predefined for the application.
* The higher the zoom, the closer to the ground.
* The zoom is always in the [getMinZoom(), getMaxZoom()] range.
* @param {number} zoom The zoom
* @param {boolean} isAnimated Indicates if animated
* @return {Promise}
*/
}, {
key: "setZoom",
value: function setZoom(zoom, isAnimated) {
return this.lookAtCoordinate({
zoom: zoom
}, isAnimated);
}
/**
* Return the current zoom scale at the central point of the view.
* This function compute the scale of a map
* @param {number} pitch Screen pitch, in millimeters ; 0.28 by default
* @return {number} The zoom scale.
*
* @deprecated Use View#getScale instead.
*/
}, {
key: "getScale",
value: function getScale(pitch)
/* istanbul ignore next */
{
console.warn('Deprecated, use View#getScale instead.');
return this.view.getScale(pitch);
}
/**
* To convert the projection in meters on the globe of a number of pixels of screen
* @param {number} pixels count pixels to project
* @param {number} pixelPitch Screen pixel pitch, in millimeters (default = 0.28 mm / standard pixel size of 0.28 millimeters as defined by the OGC)
* @return {number} projection in meters on globe
*
* @deprecated Use `View#getPixelsToMeters` instead.
*/
}, {
key: "pixelsToMeters",
value: function pixelsToMeters(pixels)
/* istanbul ignore next */
{
var pixelPitch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.28;