UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

1,430 lines (1,126 loc) 48.4 kB
"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;