itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
1,430 lines (1,126 loc) • 48.4 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = exports.CONTROL_EVENTS = void 0;
var THREE = _interopRequireWildcard(require("three"));
var _AnimationPlayer = _interopRequireWildcard(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"));
// private members
var EPS = 0.000001; // 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(); // 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 player = null;
var dampingFactor = 0.25;
var dampingMove = new THREE.Quaternion(0, 0, 0, 1);
var animationDampingMove = new _AnimationPlayer.Animation({
duration: 120,
name: 'damping-move'
});
var animationDampingOrbital = new _AnimationPlayer.Animation({
duration: 60,
name: 'damping-orbit'
}); // 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 = {};
// current downed key
var currentKey;
/**
* 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 previous;
/**
* GlobeControls is a camera controller
*
* @class GlobeControls
* @param {GlobeView} view the view where the control will be used
* @param {Coordinates} targetCoordinate the target looked by camera, at initialization
* @param {number} range distance between the target looked and camera, at initialization
* @param {number} globeRadius The globe's radius
* @param {object} options
* @param {number} options.zoomSpeed Speed zoom with mouse
* @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 {bool} enabled enable control
* @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 {bool} 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.
*/
function GlobeControls(view, targetCoordinate, range, globeRadius) {
var _this = this;
var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {};
player = new _AnimationPlayer["default"]();
this._view = view;
this.camera = view.camera.camera3D;
this.domElement = view.mainLoop.gfxEngine.renderer.domElement; // State control
var state;
var states = new _StateControl["default"]();
state = states.NONE;
this.getStates = function () {
return states;
};
this.isPaused = function () {
return state == states.NONE;
}; // Set to false to disable this control
this.enabled = true; // This option actually enables dollying in and out; left as "zoom" for
// backwards compatibility
this.zoomSpeed = options.zoomSpeed || 2.0; // Limits to how far you can dolly in and out ( PerspectiveCamera only )
this.minDistance = options.minDistance || 250;
this.maxDistance = options.maxDistance || globeRadius * 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.Math.degToRad(0.5); // radians
this.maxPolarAngle = THREE.Math.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; // Set to true to disable use of the keys
this.enableKeys = true; // Enable Damping
this.enableDamping = true;
this.startEvent = {
type: 'start'
};
this.endEvent = {
type: 'end'
};
this.getDollyScale = function () {
return Math.pow(0.95, this.zoomSpeed);
};
this.rotateLeft = function () {
var angle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
sphericalDelta.theta -= angle;
};
this.rotateUp = function () {
var angle = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
sphericalDelta.phi -= angle;
}; // pass in distance in world space to move left
this.panLeft = function (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
this.panUp = function (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
this._mouseToPan = function (deltaX, deltaY) {
var gfx = view.mainLoop.gfxEngine;
state = states.PAN;
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.Math.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);
}
};
this.dollyIn = function (dollyScale) {
if (dollyScale === undefined) {
dollyScale = this.getDollyScale();
}
if (this.camera.isPerspectiveCamera) {
orbitScale /= dollyScale;
} else if (this.camera.isOrthographicCamera) {
this.camera.zoom = THREE.Math.clamp(this.camera.zoom * dollyScale, this.minZoom, this.maxZoom);
this.camera.updateProjectionMatrix();
view.notifyChange(this.camera);
}
};
this.dollyOut = function (dollyScale) {
if (dollyScale === undefined) {
dollyScale = this.getDollyScale();
}
if (this.camera.isPerspectiveCamera) {
orbitScale *= dollyScale;
} else if (this.camera.isOrthographicCamera) {
this.camera.zoom = THREE.Math.clamp(this.camera.zoom / dollyScale, this.minZoom, this.maxZoom);
this.camera.updateProjectionMatrix();
view.notifyChange(this.camera);
}
};
var quaterPano = new THREE.Quaternion();
var quaterAxis = new THREE.Quaternion();
var axisX = new THREE.Vector3(1, 0, 0);
var minDistanceZ = Infinity;
var getMinDistanceCameraBoundingSphereObbsUp = function (tile) {
if (tile.level > 10 && tile.children.length == 1 && tile.geometry) {
var obb = tile.obb;
var sphereCamera = {
position: _this.camera.position.clone(),
radius: _this.minDistanceCollision
};
if (obb.isSphereAboveXYBox(sphereCamera)) {
minDistanceZ = Math.min(sphereCamera.position.z - obb.box3D.max.z, minDistanceZ);
}
}
};
var lastNormalizedIntersection = new THREE.Vector3();
var normalizedIntersection = new THREE.Vector3();
var update = function () {
// We compute distance between camera's bounding sphere and geometry's obb up face
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 (view.tileLayer) {
minDistanceZ = Infinity;
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = view.tileLayer.level0Nodes[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var tile = _step.value;
tile.traverse(getMinDistanceCameraBoundingSphereObbsUp);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator["return"] != null) {
_iterator["return"]();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
}
}
switch (state) {
// MOVE_GLOBE Rotate globe with mouse
case 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 states.PAN:
_this.camera.position.add(panVector);
cameraTarget.position.add(panVector);
break;
// PANORAMIC Move target camera
case states.PANORAMIC:
{
_this.camera.worldToLocal(cameraTarget.position);
var normal = _this.camera.position.clone().normalize().applyQuaternion(_this.camera.quaternion.clone().inverse());
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 (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.Math.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.Math.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 - dampingFactor;
sphericalDelta.phi *= 1 - dampingFactor;
moveAroundGlobe.slerp(dampingMove, dampingFactor * 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) {
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 === states.ORBIT && player.isStopped() && (sphericalDelta.theta > EPS || sphericalDelta.phi > EPS)) {
player.playLater(animationDampingOrbital, 2);
}
};
this.update = update; // Update helper
var updateHelper = enableTargetHelper ? function (position, helper) {
positionObject(position, helper);
view.notifyChange(this.camera);
} : function () {};
var raycaster = new THREE.Raycaster();
function onMouseMove(event) {
if (player.isPlaying()) {
player.stop();
}
if (this.enabled === false) {
return;
}
event.preventDefault();
var coords = view.eventToViewCoords(event);
switch (state) {
case states.ORBIT:
case states.PANORAMIC:
{
rotateEnd.copy(coords);
rotateDelta.subVectors(rotateEnd, rotateStart);
var gfx = view.mainLoop.gfxEngine;
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);
break;
}
case states.DOLLY:
dollyEnd.copy(coords);
dollyDelta.subVectors(dollyEnd, dollyStart);
if (dollyDelta.y > 0) {
this.dollyIn();
} else if (dollyDelta.y < 0) {
this.dollyOut();
}
dollyStart.copy(dollyEnd);
break;
case states.PAN:
panEnd.copy(coords);
panDelta.subVectors(panEnd, panStart);
this._mouseToPan(panDelta.x, panDelta.y);
panStart.copy(panEnd);
break;
case states.MOVE_GLOBE:
{
var normalized = view.viewToNormalizedCoords(coords);
raycaster.setFromCamera(normalized, this.camera);
raycaster.ray.intersectSphere(pickSphere, intersection); // If there's intersection then move globe else we stop the move
if (intersection) {
normalizedIntersection.copy(intersection).normalize();
moveAroundGlobe.setFromUnitVectors(normalizedIntersection, lastNormalizedIntersection);
lastTimeMouseMove = Date.now();
} else {
onMouseUp.bind(this)();
}
break;
}
default:
}
if (state !== states.NONE) {
update();
}
}
var targetPosition = new THREE.Vector3();
var pickedPosition = new THREE.Vector3();
var updateTarget = function () {
// Update camera's target position
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.getInverse(cameraTarget.matrixWorld);
targetPosition.copy(_this.camera.position);
targetPosition.applyMatrix4(cameraTarget.matrixWorldInverse);
spherical.setFromVector3(targetPosition);
};
var _onMouseMoveListener = onMouseMove.bind(this);
var _onMouseUpListener = onMouseUp.bind(this);
function onMouseDown(event) {
var _this2 = this;
_CameraUtils["default"].stop(view, this.camera);
return player.stop().then(function () {
if (_this2.enabled === false) {
return;
}
event.preventDefault();
updateTarget();
previous = _CameraUtils["default"].getTransformCameraLookingAtTarget(view, _this2.camera);
state = states.inputToState(event.button, currentKey);
var coords = view.eventToViewCoords(event);
switch (state) {
case states.ORBIT:
case states.PANORAMIC:
rotateStart.copy(coords);
break;
case states.MOVE_GLOBE:
{
// update picking on sphere
if (view.getPickingPositionFromDepth(coords, pickingPoint)) {
pickSphere.radius = pickingPoint.length();
lastNormalizedIntersection.copy(pickingPoint).normalize();
updateHelper.bind(_this2)(pickingPoint, helpers.picking);
} else {
state = states.NONE;
}
break;
}
case states.DOLLY:
dollyStart.copy(coords);
break;
case states.PAN:
panStart.copy(coords);
break;
default:
}
if (state != states.NONE) {
_this2.domElement.addEventListener('mousemove', _onMouseMoveListener, false);
_this2.domElement.addEventListener('mouseup', _onMouseUpListener, false);
_this2.domElement.addEventListener('mouseleave', _onMouseUpListener, false);
_this2.dispatchEvent(_this2.startEvent);
}
});
}
function ondblclick(event) {
var _this3 = this;
if (this.enabled === false || currentKey) {
return;
}
return player.stop().then(function () {
var point = view.getPickingPositionFromDepth(view.eventToViewCoords(event));
var range = _this3.getRange();
if (point && range > _this3.minDistance) {
_this3.lookAtCoordinate({
coord: new _Coordinates["default"]('EPSG:4978', point),
range: range * 0.6,
time: 1500
});
}
});
}
this._handlingEvent = function (current) {
current = current || _CameraUtils["default"].getTransformCameraLookingAtTarget(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);
}
}
};
this._onEndingMove = function (current) {
state = states.NONE;
_this._handlingEvent(current);
};
function onMouseUp() {
if (this.enabled === false) {
return;
}
this.domElement.removeEventListener('mousemove', _onMouseMoveListener, false);
this.domElement.removeEventListener('mouseup', _onMouseUpListener, false);
this.domElement.removeEventListener('mouseleave', _onMouseUpListener, false);
this.dispatchEvent(this.endEvent);
player.stop(); // Launch damping movement for :
// * states.ORBIT
// * states.MOVE_GLOBE
if (this.enableDamping) {
if (state === states.ORBIT && (sphericalDelta.theta > EPS || sphericalDelta.phi > EPS)) {
player.play(animationDampingOrbital).then(this._onEndingMove);
} else if (state === states.MOVE_GLOBE && Date.now() - lastTimeMouseMove < 50) {
// animation since mouse up event occurs less than 50ms after the last mouse move
player.play(animationDampingMove).then(this._onEndingMove);
} else {
this._onEndingMove();
}
} else {
this._onEndingMove();
}
}
function onMouseWheel(event) {
var _this4 = this;
return player.stop().then(function () {
if (!_this4.enabled || !states.DOLLY.enable) {
return;
}
_CameraUtils["default"].stop(view, _this4.camera);
event.preventDefault();
event.stopPropagation();
updateTarget();
var delta = 0; // WebKit / Opera / Explorer 9
if (event.wheelDelta !== undefined) {
delta = event.wheelDelta; // Firefox
} else if (event.detail !== undefined) {
delta = -event.detail;
}
if (delta > 0) {
_this4.dollyOut();
} else if (delta < 0) {
_this4.dollyIn();
}
var previousRange = _this4.getRange();
update();
var newRange = _this4.getRange();
if (Math.abs(newRange - previousRange) / previousRange > 0.001) {
_this4.dispatchEvent({
type: CONTROL_EVENTS.RANGE_CHANGED,
previous: previousRange,
"new": newRange
});
}
_this4.dispatchEvent(_this4.startEvent);
_this4.dispatchEvent(_this4.endEvent);
});
}
function onKeyUp() {
if (this.enabled === false || this.enableKeys === false) {
return;
}
currentKey = undefined;
}
function onKeyDown(event) {
var _this5 = this;
return player.stop().then(function () {
if (_this5.enabled === false || _this5.enableKeys === false) {
return;
}
currentKey = event.keyCode;
switch (event.keyCode) {
case states.PAN.up:
_this5._mouseToPan(0, _this5.keyPanSpeed);
state = states.PAN;
update();
break;
case states.PAN.bottom:
_this5._mouseToPan(0, -_this5.keyPanSpeed);
state = states.PAN;
update();
break;
case states.PAN.left:
_this5._mouseToPan(_this5.keyPanSpeed, 0);
state = states.PAN;
update();
break;
case states.PAN.right:
_this5._mouseToPan(-_this5.keyPanSpeed, 0);
state = states.PAN;
update();
break;
default:
}
});
}
function onTouchStart(event) {
var _this6 = this;
// CameraUtils.stop(view);
return player.stop().then(function () {
if (_this6.enabled === false) {
return;
}
state = states.touchToState(event.touches.length);
updateTarget();
if (state !== states.NONE) {
switch (state) {
case states.MOVE_GLOBE:
{
var coords = view.eventToViewCoords(event);
if (view.getPickingPositionFromDepth(coords, pickingPoint)) {
pickSphere.radius = pickingPoint.length();
lastNormalizedIntersection.copy(pickingPoint).normalize();
updateHelper.bind(_this6)(pickingPoint, helpers.picking);
} else {
state = states.NONE;
}
break;
}
case states.ORBIT:
case 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 states.PAN:
panStart.set(event.touches[0].pageX, event.touches[0].pageY);
break;
default:
}
_this6.dispatchEvent(_this6.startEvent);
}
});
}
function onTouchMove(event) {
if (player.isPlaying()) {
player.stop();
}
if (this.enabled === false) {
return;
}
event.preventDefault();
event.stopPropagation();
switch (event.touches.length) {
case states.MOVE_GLOBE.finger:
{
var coords = view.eventToViewCoords(event);
var normalized = view.viewToNormalizedCoords(coords);
raycaster.setFromCamera(normalized, this.camera);
raycaster.ray.intersectSphere(pickSphere, intersection); // If there's intersection then move globe else we stop the move
if (intersection) {
normalizedIntersection.copy(intersection).normalize();
moveAroundGlobe.setFromUnitVectors(normalizedIntersection, lastNormalizedIntersection);
lastTimeMouseMove = Date.now();
} else {
onMouseUp.bind(this)();
}
break;
}
case states.ORBIT.finger:
case states.DOLLY.finger:
{
var gfx = 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);
if (dollyDelta.y > 0) {
this.dollyOut();
} else if (dollyDelta.y < 0) {
this.dollyIn();
}
dollyStart.copy(dollyEnd);
break;
}
case 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:
state = states.NONE;
}
if (state !== states.NONE) {
update();
}
}
var _onMouseDownListener = onMouseDown.bind(this);
var _onMouseWheelListener = onMouseWheel.bind(this);
var _ondblclickListener = ondblclick.bind(this);
var _onTouchStartListener = onTouchStart.bind(this);
var _onTouchEndListener = function ()
/* event */
{
onMouseUp.bind(this)();
}.bind(this);
var _onTouchMoveListener = onTouchMove.bind(this);
var _onKeyDownListener = onKeyDown.bind(this);
var _onKeyUpListener = onKeyUp.bind(this);
var _onContextMenuListener = function (event) {
event.preventDefault();
};
var _updateListener = this.update;
var _onBlurListener = function () {
onKeyUp.bind(_this)();
onMouseUp.bind(_this)();
};
this.dispose = function () {
this.domElement.removeEventListener('contextmenu', _onContextMenuListener, false);
this.domElement.removeEventListener('mousedown', _onMouseDownListener, false);
this.domElement.removeEventListener('mousemove', _onMouseMoveListener, false);
this.domElement.removeEventListener('mousewheel', _onMouseWheelListener, false);
this.domElement.removeEventListener('DOMMouseScroll', _onMouseWheelListener, false); // firefox
this.domElement.removeEventListener('mouseup', _onMouseUpListener, false);
this.domElement.removeEventListener('mouseleave', _onMouseUpListener, false);
this.domElement.removeEventListener('dblclick', _ondblclickListener, false);
this.domElement.removeEventListener('touchstart', _onTouchStartListener, false);
this.domElement.removeEventListener('touchend', _onTouchEndListener, false);
this.domElement.removeEventListener('touchmove', _onTouchMoveListener, false);
player.removeEventListener('animation-frame', _updateListener);
window.removeEventListener('keydown', _onKeyDownListener, false);
window.removeEventListener('keyup', _onKeyUpListener, false);
window.removeEventListener('blur', _onBlurListener);
this.dispatchEvent({
type: 'dispose'
});
}; // Instance all
this.domElement.addEventListener('contextmenu', _onContextMenuListener, false);
this.domElement.addEventListener('mousedown', _onMouseDownListener, false);
this.domElement.addEventListener('mousewheel', _onMouseWheelListener, false);
this.domElement.addEventListener('dblclick', _ondblclickListener, false);
this.domElement.addEventListener('DOMMouseScroll', _onMouseWheelListener, false); // firefox
this.domElement.addEventListener('touchstart', _onTouchStartListener, false);
this.domElement.addEventListener('touchend', _onTouchEndListener, false);
this.domElement.addEventListener('touchmove', _onTouchMoveListener, false); // refresh control for each animation's frame
player.addEventListener('animation-frame', _updateListener); // TODO: Why windows
window.addEventListener('keydown', _onKeyDownListener, false);
window.addEventListener('keyup', _onKeyUpListener, false); // Reset key/mouse when window loose focus
window.addEventListener('blur', _onBlurListener);
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);
}
positionObject(targetCoordinate.as('EPSG:4978', xyz), cameraTarget);
this.lookAtCoordinate({
coord: targetCoordinate,
tilt: 89.5,
heading: 0,
range: range
}, false);
}
GlobeControls.prototype = Object.create(THREE.EventDispatcher.prototype);
GlobeControls.prototype.constructor = GlobeControls;
function getRangeFromScale(scale, pitch, fov, height) {
// Screen pitch, in millimeters
pitch = (pitch || 0.28) / 1000;
fov = THREE.Math.degToRad(fov); // Invert one unit projection (see getDollyScale)
var range = pitch * height / (scale * 2 * Math.tan(fov * 0.5));
return range;
}
/**
* Changes the tilt of the current camera, in degrees.
* @param {number} tilt
* @param {boolean} isAnimated
* @return {Promise<void>}
*/
GlobeControls.prototype.setTilt = function (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>}
*/
GlobeControls.prototype.setHeading = function (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>}
*/
GlobeControls.prototype.setRange = function (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
*/
GlobeControls.prototype.getCameraTargetPosition = function () {
return cameraTarget.position;
};
/**
* Returns the "range": the distance in meters between the camera and the current central point on the screen.
* @return {number} number
*/
GlobeControls.prototype.getRange = function () {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this._view, this.camera).range;
};
/**
* Returns the tilt of the current camera in degrees.
* @return {Angle} number - The angle of the rotation in degrees.
*/
GlobeControls.prototype.getTilt = function () {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this._view, this.camera).tilt;
};
/**
* Returns the heading of the current camera in degrees.
* @return {Angle} number - The angle of the rotation in degrees.
*/
GlobeControls.prototype.getHeading = function () {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this._view, this.camera).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}
*/
GlobeControls.prototype.pan = function (pVector) {
this._mouseToPan(pVector.x, pVector.y);
this.update();
return Promise.resolve();
};
/**
* Returns the orientation angles of the current camera, in degrees.
* @return {Array<number>}
*/
GlobeControls.prototype.getCameraOrientation = function () {
return [this.getTilt(), this.getHeading()];
};
/**
* Returns the camera location projected on the ground in lat,lon. See {@linkcode Coordinates} for conversion.
* @return {Coordinates} position
*/
GlobeControls.prototype.getCameraCoordinate = function () {
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
*/
GlobeControls.prototype.getLookAtCoordinate = function () {
return _CameraUtils["default"].getTransformCameraLookingAtTarget(this._view, this.camera).coord;
};
/**
* Sets the animation enabled.
* @param {boolean} enable enable
*/
GlobeControls.prototype.setAnimationEnabled = function (enable) {
enableAnimation = enable;
};
/**
* Determines if animation enabled.
* @return {boolean} True if animation enabled, False otherwise.
*/
GlobeControls.prototype.isAnimationEnabled = function () {
return enableAnimation;
};
/**
* Returns the actual zoom. The zoom will always be between the [getMinZoom(), getMaxZoom()].
* @return {number} The zoom .
*/
GlobeControls.prototype.getZoom = function () {
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}
*/
GlobeControls.prototype.setZoom = function (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.
*/
GlobeControls.prototype.getScale = function (pitch) {
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.
*/
GlobeControls.prototype.pixelsToMeters = function (pixels) {
var pixelPitch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.28;
console.warn('Deprecated use View#getPixelsToMeters instead.');
var scaled = this.getScale(pixelPitch);
return pixels * pixelPitch / scaled / 1000;
};
/**
* To convert the projection a number of horizontal pixels of screen to longitude degree WGS84 on the globe
* @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 degree on globe
*
* @deprecated Use `View#getPixelsToMeters` and `GlobeControls#metersToDegrees`
* instead.
*/
GlobeControls.prototype.pixelsToDegrees = function (pixels) {
var pixelPitch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.28;
console.warn('Deprecated, use View#getPixelsToMeters and GlobeControls#getMetersToDegrees instead.');
var chord = this.pixelsToMeters(pixels, pixelPitch);
return THREE.Math.radToDeg(2 * Math.asin(chord / (2 * _Ellipsoid.ellipsoidSizes.x)));
};
/**
* Projection on screen in pixels of length in meter on globe
* @param {number} value Length in meter on globe
* @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 pixels on screen
*
* @deprecated Use `View#getMetersToPixels` instead.
*/
GlobeControls.prototype.metersToPixels = function (value) {
var pixelPitch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0.28;
console.warn('Deprecated, use View#getMetersToPixels instead.');
var scaled = this.getScale(pixelPitch);
pixelPitch /= 1000;
return value * scaled / pixelPitch;
};
/**
* Changes the zoom of the central point of screen so that screen acts as a map with a specified scale.
* The view flies to the desired zoom scale;
* @param {number} scale The scale
* @param {number} pitch The pitch
* @param {boolean} isAnimated Indicates if animated
* @return {Promise}
*/
GlobeControls.prototype.setScale = function (scale, pitch, isAnimated) {
return this.lookAtCoordinate({
scale: scale,
pitch: pitch
}, isAnimated);
};
/**
* Changes the center of the scene on screen to the specified in lat, lon. See {@linkcode Coordinates} for conversion.
* This function allows to change the central position, the zoom, the range, the scale and the camera orientation at the same time.
* The zoom has to be between the [getMinZoom(), getMaxZoom()].
* Zoom parameter is ignored if range is set
* The tilt's interval is between 4 and 89.5 degree
*
* @param {CameraUtils~CameraTransformOptions} params camera transformation to apply
* @param {number} [params.zoom] zoom
* @param {number} [params.scale] scale
* @param {boolean} isAnimated Indicates if animated
* @return {Promise} A promise that resolves when transformation is oppered
*/
GlobeControls.prototype.lookAtCoordinate = function () {
var _this7 = this;
var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
var isAnimated = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : this.isAnimationEnabled();
if (params.zoom) {
params.range = this._view.tileLayer.computeDistanceCameraFromTileZoom(params.zoom, this._view.camera);
} else if (params.scale) {
var gfx = this._view.mainLoop.gfxEngine;
params.range = getRangeFromScale(params.scale, params.pitch, this.camera.fov, gfx.height);
if (params.range < this.minDistance || params.range > this.maxDistance) {
// eslint-disable-next-line no-console
console.warn("This scale ".concat(params.scale, " can not be reached"));
params.range = THREE.Math.clamp(params.range, this.minDistance, this.maxDistance);
}
}
if (params.tilt !== undefined) {
var minTilt = 90 - THREE.Math.radToDeg(this.maxPolarAngle);
var maxTilt = 90 - THREE.Math.radToDeg(this.minPolarAngle);
if (params.tilt < minTilt || params.tilt > maxTilt) {
params.tilt = THREE.Math.clamp(params.tilt, minTilt, maxTilt); // eslint-disable-next-line no-console
console.warn('Tilt was clamped to ', params.tilt, " the interval is between ".concat(minTilt, " and ").concat(maxTilt, " degree"));
}
}
previous = _CameraUtils["default"].getTransformCameraLookingAtTarget(this._view, this.camera);
if (isAnimated) {
params.callback = function (r) {
return cameraTarget.position.copy(r.targetWorldPosition);
};
this.dispatchEvent({
type: 'animation-started'
});
return _CameraUtils["default"].animateCameraToLookAtTarget(this._view, this.camera, params).then(function (result) {
_this7.dispatchEvent({
type: 'animation-ended'
});
_this7._handlingEvent(result);
return result;
});
} else {
return _CameraUtils["default"].transformCameraToLookAtTarget(this._view, this.camera, params).then(function (result) {
cameraTarget.position.copy(result.targetWorldPosition);
_this7._handlingEvent(result);
return result;
});
}
};
/**
* Pick a position on the globe at the given position in lat,lon. See {@linkcode Coordinates} for conversion.
* @param {Vector2} windowCoords - window coordinates
* @param {number=} y - The y-position inside the Globe element.
* @return {Coordinates} position
*/
GlobeControls.prototype.pickGeoPosition = function (windowCoords) {
var pickedPosition = this._view.getPickingPositionFromDepth(windowCoords);
if (!pickedPosition) {
return;
}
return new _Coordinates["default"]('EPSG:4978', pickedPosition).as('EPSG:4326');
};
var _default = GlobeControls;
exports["default"] = _default;