diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index c820647199..0cad7f921f 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -16,6 +16,11 @@ The following people have contributed to iTowns 2.
* [AtolCD](http://www.atolcd.com)
* [Thomas Broyer](https://github.com/tbroyer)
+
+* [LIRIS](https://liris.cnrs.fr/)
+ * [Nicolas Saul](https://github.com/NikoSaul)
+ * [Emmanuel Schmück](https://github.com/EmmanuelSchmuck/)
+ * [Marie Lamure](https://github.com/mlamure)
The following organizations supported iTowns2 :
* IGN ( http://www.ign.fr )
diff --git a/examples/planar.html b/examples/planar.html
index 1560ee875a..18f8457d4d 100644
--- a/examples/planar.html
+++ b/examples/planar.html
@@ -59,10 +59,13 @@
Key bindings
- - ↑ ↓ : forward / backward
- - ← → : strafe left / right
- - ⇞ (PageUp), ⇟ (PageDown) : move up / down
- - mouse Clic + drag : camera rotation
+ - Left-Click: camera translation (drag)
+ - Right-Click: camera translation (pan)
+ - Ctrl + Left-Click: camera rotation (orbit)
+ - Spacebar / Wheel-Click: smart zoom
+ - Mouse Wheel: zoom in/out
+ - T: orient camera to a top view
+ - Y: move camera to start position
diff --git a/examples/planar.js b/examples/planar.js
index 36ced7b730..d04683dd11 100644
--- a/examples/planar.js
+++ b/examples/planar.js
@@ -67,7 +67,7 @@ view.camera.camera3D.lookAt(extent.center().xyz());
// instanciate controls
// eslint-disable-next-line no-new
-new itowns.FirstPersonControls(view, { focusOnClick: true, moveSpeed: 1000 });
+new itowns.PlanarControls(view, {});
// Request redraw
view.notifyChange(true);
diff --git a/src/Main.js b/src/Main.js
index 01d7f63111..113f8ae3b0 100644
--- a/src/Main.js
+++ b/src/Main.js
@@ -23,6 +23,7 @@ export { default as PointsMaterial } from './Renderer/PointsMaterial';
export { default as PointCloudProcessing } from './Process/PointCloudProcessing';
export { default as FlyControls } from './Renderer/ThreeExtended/FlyControls';
export { default as FirstPersonControls } from './Renderer/ThreeExtended/FirstPersonControls';
+export { default as PlanarControls } from './Renderer/ThreeExtended/PlanarControls';
export { default as GeoJSON2Three } from './Renderer/ThreeExtended/GeoJSON2Three';
export { CONTROL_EVENTS } from './Renderer/ThreeExtended/GlobeControls';
export { default as DEMUtils } from './utils/DEMUtils';
diff --git a/src/Renderer/ThreeExtended/PlanarControls.js b/src/Renderer/ThreeExtended/PlanarControls.js
new file mode 100644
index 0000000000..5af535b936
--- /dev/null
+++ b/src/Renderer/ThreeExtended/PlanarControls.js
@@ -0,0 +1,739 @@
+/** Description: Camera controls adapted for a planar view, with animated movements
+* Left mouse button : "drag" the ground, translating the camera on the (xy) world plane.
+* Right mouse button : translate the camera on local x and world z axis (pan)
+* Ctrl + left mouse : rotate (orbit) around the camera's focus point.
+* Scroll wheel : zooms toward cursor position (animated).
+* Middle mouse button (wheel click) : 'smart zoom' at cursor location (animated).
+* Y : go to start view (animated)
+* T : go to top view (animated)
+* How to use : instanciate PlanarControls after camera setup (setPosition and lookAt)
+*/
+
+import * as THREE from 'three';
+
+// event keycode
+const keys = {
+ CTRL: 17,
+ SPACE: 32,
+ T: 84,
+ Y: 89,
+};
+
+const mouseButtons = {
+ LEFTCLICK: THREE.MOUSE.LEFT,
+ MIDDLECLICK: THREE.MOUSE.MIDDLE,
+ RIGHTCLICK: THREE.MOUSE.RIGHT,
+};
+
+// control state
+const STATE = {
+ NONE: -1,
+ DRAG: 0,
+ PAN: 1,
+ ROTATE: 2,
+ TRAVEL: 3,
+};
+
+/**
+* PlanarControls Constructor
+* Numerical values have been adjusted for the example provided in examples/planar.html
+* Most of them can be changed with the options parameter
+* @param {PlanarView} view : the itowns view (planar view)
+* @param {options} options : optional parameters.
+*/
+function PlanarControls(view, options = {}) {
+ this.view = view;
+ this.camera = view.camera.camera3D;
+ this.domElement = view.mainLoop.gfxEngine.renderer.domElement;
+
+ this.rotateSpeed = options.rotateSpeed || 2.0;
+
+ // minPanSpeed when close to the ground, maxPanSpeed when close to maxAltitude
+ this.maxPanSpeed = options.maxPanSpeed || 15;
+ this.minPanSpeed = options.minPanSpeed || 0.05;
+
+ // animation duration for the zoom
+ this.zoomTravelTime = options.zoomTravelTime || 0.2;
+
+ // zoom movement is equal to the distance to the zoom target, multiplied by zoomFactor
+ this.zoomInFactor = options.zoomInFactor || 0.25;
+ this.zoomOutFactor = options.zoomOutFactor || 0.4;
+
+ // pan movement is clamped between maxAltitude and groundLevel
+ this.maxAltitude = options.maxAltitude || 12000;
+
+ // approximate ground altitude value
+ this.groundLevel = options.groundLevel || 200;
+
+ // min and max duration in seconds, for animated travels with 'auto' parameter
+ this.autoTravelTimeMin = options.autoTravelTimeMin || 1.5;
+ this.autoTravelTimeMax = options.autoTravelTimeMax || 4;
+
+ // max travel duration is reached for this travel distance (empirical smoothing value)
+ this.autoTravelTimeDist = options.autoTravelTimeDist || 20000;
+
+ // after a smartZoom, camera height above ground will be between these two values
+ this.smartZoomHeightMin = options.smartZoomHeightMin || 75;
+ this.smartZoomHeightMax = options.smartZoomHeightMax || 500;
+
+ // if set to true, animated travels have 0 duration
+ this.instantTravel = options.instantTravel || false;
+
+ this.minZenithAngle = options.minZenithAngle || 0 * Math.PI / 180;
+
+ // should be less than 90 deg (90 = parallel to the ground)
+ this.maxZenithAngle = (options.maxZenithAngle || 82.5) * Math.PI / 180;
+
+ // focus policy options
+ this.focusOnMouseOver = options.focusOnMouseOver || true;
+ this.focusOnMouseClick = options.focusOnMouseClick || true;
+
+ // starting camera position and orientation target are setup before instanciating PlanarControls
+ // using: view.camera.setPosition() and view.camera.lookAt()
+ // startPosition and startQuaternion are stored to be able to return to the start view
+ const startPosition = this.camera.position.clone();
+ const startQuaternion = this.camera.quaternion.clone();
+
+ // control state
+ this.state = STATE.NONE;
+
+ // mouse movement
+ const mousePosition = new THREE.Vector2();
+ const lastMousePosition = new THREE.Vector2();
+ const deltaMousePosition = new THREE.Vector2(0, 0);
+
+ // drag movement
+ const dragStart = new THREE.Vector3();
+ const dragEnd = new THREE.Vector3();
+ const dragDelta = new THREE.Vector3();
+
+ // camera focus point : ground point at screen center
+ const centerPoint = new THREE.Vector3(0, 0, 0);
+
+ // camera rotation
+ let phi = 0.0;
+
+ // animated travel
+ const travelEndPos = new THREE.Vector3();
+ const travelStartPos = new THREE.Vector3();
+ const travelStartRot = new THREE.Quaternion();
+ const travelEndRot = new THREE.Quaternion();
+ let travelAlpha = 0;
+ let travelDuration = 0;
+ let travelUseRotation = false;
+ let travelUseSmooth = false;
+
+ // eventListeners handlers
+ const _handlerOnKeyDown = onKeyDown.bind(this);
+ const _handlerOnMouseDown = onMouseDown.bind(this);
+ const _handlerOnMouseUp = onMouseUp.bind(this);
+ const _handlerOnMouseMove = onMouseMove.bind(this);
+ const _handlerOnMouseWheel = onMouseWheel.bind(this);
+
+ // focus policy
+ if (this.focusOnMouseOver) {
+ this.domElement.addEventListener('mouseover', () => this.domElement.focus());
+ }
+ if (this.focusOnClick) {
+ this.domElement.addEventListener('click', () => this.domElement.focus());
+ }
+
+ // prevent the default contextmenu from appearing when right-clicking
+ // this allows to use right-click for input without the menu appearing
+ this.domElement.addEventListener('contextmenu', onContextMenu.bind(this), false);
+
+ // add this PlanarControl instance to the view's framerequesters
+ // with this, PlanarControl.update() will be called each frame
+ this.view.addFrameRequester(this);
+
+ // Updates the view and camera if needed, and handles the animated travel
+ this.update = function update(dt, updateLoopRestarted) {
+ // dt will not be relevant when we just started rendering, we consider a 1-frame move in this case
+ if (updateLoopRestarted) {
+ dt = 16;
+ }
+ if (this.state === STATE.TRAVEL) {
+ this.handleTravel(dt);
+ this.view.notifyChange(true);
+ }
+ if (this.state === STATE.DRAG) {
+ this.handleDragMovement();
+ }
+ if (this.state === STATE.ROTATE) {
+ this.handleRotation();
+ }
+ if (this.state === STATE.PAN) {
+ this.handlePanMovement();
+ }
+ deltaMousePosition.set(0, 0);
+ };
+
+ /**
+ * Initiate a drag movement (translation on xy plane)
+ * The movement value is derived from the actual world point under the mouse cursor
+ * This allows the user to 'grab' a world point and drag it to move (eg : google map)
+ */
+ this.initiateDrag = function initiateDrag() {
+ this.state = STATE.DRAG;
+
+ // the world point under mouse cursor when the drag movement is started
+ dragStart.copy(this.getWorldPointAtScreenXY(mousePosition));
+
+ // the difference between start and end cursor position
+ dragDelta.set(0, 0, 0);
+ };
+
+ /**
+ * Handle the drag movement (translation on xy plane) when user moves the mouse while in STATE.DRAG
+ * The drag movement is previously initiated by initiateDrag()
+ * Compute the drag value and update the camera controls.
+ * The movement value is derived from the actual world point under the mouse cursor
+ * This allows the user to 'grab' a world point and drag it to move (eg : google map)
+ */
+ this.handleDragMovement = function handleDragMovement() {
+ // the world point under the current mouse cursor position, at same altitude than dragStart
+ dragEnd.copy(this.getWorldPointFromMathPlaneAtScreenXY(mousePosition, dragStart.z));
+
+ // the difference between start and end cursor position
+ dragDelta.subVectors(dragStart, dragEnd);
+
+ this.camera.position.add(dragDelta);
+
+ dragDelta.set(0, 0, 0);
+ };
+
+ /**
+ * Initiate a pan movement (local translation on xz plane)
+ */
+ this.initiatePan = function initiatePan() {
+ this.state = STATE.PAN;
+ };
+
+ /**
+ * Handle the pan movement (translation on local x / world z plane) when user moves the mouse while in STATE.PAN
+ * The drag movement is previously initiated by initiatePan()
+ * Compute the pan value and update the camera controls.
+ */
+ this.handlePanMovement = (() => {
+ const vec = new THREE.Vector3();
+
+ return () => {
+ // normalized (betwwen 0 and 1) distance between groundLevel and maxAltitude
+ const distToGround = THREE.Math.clamp((this.camera.position.z - this.groundLevel) / this.maxAltitude, 0, 1);
+
+ // pan movement speed, adujsted according to altitude
+ const panSpeed = THREE.Math.lerp(this.minPanSpeed, this.maxPanSpeed, distToGround);
+
+ // lateral movement (local x axis)
+ vec.set(panSpeed * -1 * deltaMousePosition.x, 0, 0);
+ this.camera.position.copy(this.camera.localToWorld(vec));
+
+ // vertical movement (world z axis)
+ const newAltitude = this.camera.position.z + panSpeed * deltaMousePosition.y;
+
+ // check if altitude is valid
+ if (newAltitude < this.maxAltitude && newAltitude > this.groundLevel) {
+ this.camera.position.z = newAltitude;
+ }
+ };
+ })();
+
+ /**
+ * Initiate a rotate (orbit) movement
+ */
+ this.initiateRotation = function initiateRotation() {
+ this.state = STATE.ROTATE;
+
+ centerPoint.copy(this.getWorldPointAtScreenXY({ x: 0.5 * this.domElement.clientWidth, y: 0.5 * this.domElement.clientHeight }));
+
+ const r = this.camera.position.distanceTo(centerPoint);
+ phi = Math.acos((this.camera.position.z - centerPoint.z) / r);
+ };
+
+ /**
+ * Handle the rotate movement (orbit) when user moves the mouse while in STATE.ROTATE
+ * the movement is an orbit around 'centerPoint', the camera focus point (ground point at screen center)
+ * The rotate movement is previously initiated in initiateRotation()
+ * Compute the new position value and update the camera controls.
+ */
+ this.handleRotation = (() => {
+ const vec = new THREE.Vector3();
+ const quat = new THREE.Quaternion();
+
+ return () => {
+ // angle deltas
+ // deltaMousePosition is computed in onMouseMove / onMouseDown s
+ const thetaDelta = -this.rotateSpeed * deltaMousePosition.x / this.domElement.clientWidth;
+ const phiDelta = -this.rotateSpeed * deltaMousePosition.y / this.domElement.clientHeight;
+
+ // the vector from centerPoint (focus point) to camera position
+ const offset = this.camera.position.clone().sub(centerPoint);
+
+ if (thetaDelta !== 0 || phiDelta !== 0) {
+ if ((phi + phiDelta >= this.minZenithAngle)
+ && (phi + phiDelta <= this.maxZenithAngle)
+ && phiDelta !== 0) {
+ // rotation around X (altitude)
+ phi += phiDelta;
+
+ vec.set(0, 0, 1);
+ quat.setFromUnitVectors(this.camera.up, vec);
+ offset.applyQuaternion(quat);
+
+ vec.setFromMatrixColumn(this.camera.matrix, 0);
+ quat.setFromAxisAngle(vec, phiDelta);
+ offset.applyQuaternion(quat);
+
+ vec.set(0, 0, 1);
+ quat.setFromUnitVectors(this.camera.up, vec).inverse();
+ offset.applyQuaternion(quat);
+ }
+ if (thetaDelta !== 0) {
+ // rotation around Z (azimuth)
+ vec.set(0, 0, 1);
+ quat.setFromAxisAngle(vec, thetaDelta);
+ offset.applyQuaternion(quat);
+ }
+ }
+
+ this.camera.position.copy(offset).add(centerPoint);
+
+ this.camera.lookAt(centerPoint);
+ };
+ })();
+
+ /**
+ * Triggers a Zoom animated movement (travel) toward / away from the world point under the mouse cursor
+ * The zoom intensity varies according to the distance between the camera and the point.
+ * The closer to the ground, the lower the intensity
+ * Orientation will not change (null parameter in the call to initiateTravel function)
+ * @param {event} event : the mouse wheel event.
+ */
+ this.initiateZoom = function initiateZoom(event) {
+ let delta;
+
+ // mousewheel delta
+ if (event.wheelDelta !== undefined) {
+ delta = event.wheelDelta;
+ } else if (event.detail !== undefined) {
+ delta = -event.detail;
+ }
+
+ const pointUnderCursor = this.getWorldPointAtScreenXY(mousePosition);
+ const newPos = new THREE.Vector3();
+
+ // Zoom IN
+ if (delta > 0) {
+ // target position
+ newPos.lerpVectors(this.camera.position, pointUnderCursor, this.zoomInFactor);
+ // initiate travel
+ this.initiateTravel(newPos, this.zoomTravelTime, null, false);
+ }
+ // Zoom OUT
+ else if (delta < 0 && this.camera.position.z < this.maxAltitude) {
+ // target position
+ newPos.lerpVectors(this.camera.position, pointUnderCursor, -1 * this.zoomOutFactor);
+ // initiate travel
+ this.initiateTravel(newPos, this.zoomTravelTime, null, false);
+ }
+ };
+
+ /**
+ * Triggers a 'smart zoom' animated movement (travel) toward the point under mouse cursor
+ * The camera will be smoothly moved and oriented close to the target, at a determined height and distance
+ */
+ this.initiateSmartZoom = function initiateSmartZoom() {
+ // point under mouse cursor
+ const pointUnderCursor = new THREE.Vector3();
+
+ // check if there is valid geometry under cursor
+ if (typeof this.view.getPickingPositionFromDepth(mousePosition) !== 'undefined') {
+ pointUnderCursor.copy(this.view.getPickingPositionFromDepth(mousePosition));
+ }
+ else {
+ return;
+ }
+
+ // direction of the movement, projected on xy plane and normalized
+ const dir = new THREE.Vector3();
+ dir.copy(pointUnderCursor).sub(this.camera.position);
+ dir.z = 0;
+ dir.normalize();
+
+ const distanceToPoint = this.camera.position.distanceTo(pointUnderCursor);
+
+ // camera height (altitude above ground) at the end of the travel, 5000 is an empirical smoothing distance
+ const targetHeight = THREE.Math.lerp(this.smartZoomHeightMin, this.smartZoomHeightMax, Math.min(distanceToPoint / 5000, 1));
+
+ // camera position at the end of the travel
+ const moveTarget = new THREE.Vector3();
+
+ moveTarget.copy(pointUnderCursor).add(dir.multiplyScalar(-targetHeight * 2));
+ moveTarget.z = pointUnderCursor.z + targetHeight;
+
+ // initiate the travel
+ this.initiateTravel(moveTarget, 'auto', pointUnderCursor, true);
+ };
+
+
+ /**
+ * Triggers an animated movement & rotation for the camera
+ * @param {THREE.Vector3} targetPos : the target position of the camera (reached at the end)
+ * @param {number} travelTime : set to 'auto', or set to a duration in seconds.
+ * If set to auto : travel time will be set to a duration between autoTravelTimeMin and autoTravelTimeMax
+ * according to the distance and the angular difference between start and finish.
+ * @param {(string|THREE.Vector3|THREE.Quaternion)} targetOrientation : define the target rotation of the camera
+ * if targetOrientation is a world point (Vector3) : the camera will lookAt() this point
+ * if targetOrientation is a quaternion : this quaternion will define the final camera orientation
+ * if targetOrientation is neither a quaternion nor a world point : the camera will keep its starting orientation
+ * @param {boolean} useSmooth : animation is smoothed using the 'smooth(value)' function (slower at start and finish)
+ */
+ this.initiateTravel = function initiateTravel(targetPos, travelTime, targetOrientation, useSmooth) {
+ this.state = STATE.TRAVEL;
+ this.view.notifyChange(true);
+ // the progress of the travel (animation alpha)
+ travelAlpha = 0;
+ // update cursor
+ this.updateMouseCursorType();
+
+ travelUseRotation = (targetOrientation instanceof THREE.Quaternion || targetOrientation instanceof THREE.Vector3);
+ travelUseSmooth = useSmooth;
+
+ // start position (current camera position)
+ travelStartPos.copy(this.camera.position);
+
+ // start rotation (current camera rotation)
+ travelStartRot.copy(this.camera.quaternion);
+
+ // setup the end rotation :
+
+ // case where targetOrientation is a quaternion
+ if (targetOrientation instanceof THREE.Quaternion) {
+ travelEndRot.copy(targetOrientation);
+ }
+ // case where targetOrientation is a vector3
+ else if (targetOrientation instanceof THREE.Vector3) {
+ if (targetPos === targetOrientation) {
+ this.camera.lookAt(targetOrientation);
+ travelEndRot.copy(this.camera.quaternion);
+ this.camera.quaternion.copy(travelStartRot);
+ }
+ else {
+ this.camera.position.copy(targetPos);
+ this.camera.lookAt(targetOrientation);
+ travelEndRot.copy(this.camera.quaternion);
+ this.camera.quaternion.copy(travelStartRot);
+ this.camera.position.copy(travelStartPos);
+ }
+ }
+
+ // end position
+ travelEndPos.copy(targetPos);
+
+ // beginning of the travel duration setup
+
+ if (this.instantTravel) {
+ travelDuration = 0;
+ }
+ // case where travelTime is set to 'auto' : travelDuration will be a value between autoTravelTimeMin and autoTravelTimeMax
+ // depending on travel distance and travel angular difference
+ else if (travelTime === 'auto') {
+ // a value between 0 and 1 according to the travel distance. Adjusted by autoTravelTimeDist parameter
+ const normalizedDistance = Math.min(1, targetPos.distanceTo(this.camera.position) / this.autoTravelTimeDist);
+
+ travelDuration = THREE.Math.lerp(this.autoTravelTimeMin, this.autoTravelTimeMax, normalizedDistance);
+
+ // if travel changes camera orientation, travel duration is adjusted according to angularDifference
+ // this allows for a smoother travel (more time for the camera to rotate)
+ // final duration will not excede autoTravelTimeMax
+ if (travelUseRotation) {
+ // value is normalized between 0 and 1
+ const angularDifference = 0.5 - 0.5 * (travelEndRot.normalize().dot(this.camera.quaternion.normalize()));
+
+ travelDuration *= 1 + 2 * angularDifference;
+ travelDuration = Math.min(travelDuration, this.autoTravelTimeMax);
+ }
+ }
+ // case where traveltime !== 'auto' : travelTime is a duration in seconds given as parameter
+ else {
+ travelDuration = travelTime;
+ }
+ };
+
+ /**
+ * Resume normal behavior after a travel is completed
+ */
+ this.endTravel = function endTravel() {
+ this.camera.position.copy(travelEndPos);
+
+ if (travelUseRotation) {
+ this.camera.quaternion.copy(travelEndRot);
+ }
+
+ this.state = STATE.NONE;
+
+ this.updateMouseCursorType();
+ };
+
+ /**
+ * Handle the animated movement and rotation of the camera in 'travel' state
+ * @param {number} dt : the deltatime between two updates in milliseconds
+ */
+ this.handleTravel = function handleTravel(dt) {
+ travelAlpha += (dt / 1000) / travelDuration;
+
+ // the animation alpha, between 0 (start) and 1 (finish)
+ const alpha = (travelUseSmooth) ? smooth(travelAlpha) : travelAlpha;
+
+ // new position
+ this.camera.position.lerpVectors(travelStartPos, travelEndPos, alpha);
+
+ // new rotation
+ if (travelUseRotation === true) {
+ THREE.Quaternion.slerp(travelStartRot, travelEndRot, this.camera.quaternion, alpha);
+ }
+ // completion test
+ if (travelAlpha > 1) {
+ this.endTravel();
+ }
+ };
+
+ /**
+ * Triggers an animated movement (travel) to set the camera to top view, above the focus point, at altitude=distanceToFocusPoint
+ */
+ this.goToTopView = function goToTopView() {
+ const topViewPos = new THREE.Vector3();
+ const targetQuat = new THREE.Quaternion();
+
+ // the top view position is above the camera focus point, at an altitude = distanceToPoint
+ topViewPos.copy(this.getWorldPointAtScreenXY({ x: 0.5 * this.domElement.clientWidth, y: 0.5 * this.domElement.clientHeight }));
+ topViewPos.z += Math.min(this.maxAltitude, this.camera.position.distanceTo(topViewPos));
+
+ targetQuat.setFromAxisAngle(new THREE.Vector3(1, 0, 0), 0);
+
+ // initiate the travel
+ this.initiateTravel(topViewPos, 'auto', targetQuat, true);
+ };
+
+ /**
+ * Triggers an animated movement (travel) to set the camera to starting view
+ */
+ this.goToStartView = function goToStartView() {
+ this.initiateTravel(startPosition, 'auto', startQuaternion, true);
+ };
+
+ /**
+ * returns the world point (xyz) under the posXY screen point
+ * the point belong to an abstract mathematical plane of specified altitude (doesnt use actual geometry)
+ * @param {THREE.Vector2} posXY : the mouse position in screen space (unit : pixel)
+ * @param {number} altitude : the altitude (z) of the mathematical plane
+ * @returns {THREE.Vector3}
+ */
+ this.getWorldPointFromMathPlaneAtScreenXY = (() => {
+ const vector = new THREE.Vector3();
+ return (posXY, altitude) => {
+ vector.set((posXY.x / this.domElement.clientWidth) * 2 - 1, -(posXY.y / this.domElement.clientHeight) * 2 + 1, 0.5);
+ vector.unproject(this.camera);
+ // dir = direction toward the point on the plane
+ const dir = vector.sub(this.camera.position).normalize();
+ // distance from camera to point on the plane
+ const distance = (altitude - this.camera.position.z) / dir.z;
+
+ return this.camera.position.clone().add(dir.multiplyScalar(distance));
+ };
+ })();
+
+ /**
+ * returns the world point (xyz) under the posXY screen point
+ * if geometry is under the cursor, the point in obtained with getPickingPositionFromDepth
+ * if no geometry is under the cursor, the point is obtained with getWorldPointFromMathPlaneAtScreenXY
+ * @param {THREE.Vector2} posXY : the mouse position in screen space (unit : pixel)
+ * @returns {THREE.Vector3}
+ */
+ this.getWorldPointAtScreenXY = function getWorldPointAtScreenXY(posXY) {
+ const pointUnderCursor = this.view.getPickingPositionFromDepth(posXY);
+ // check if there is valid geometry under cursor
+ if (pointUnderCursor) {
+ return pointUnderCursor;
+ }
+ // if not, we use the mathematical plane at altitude = groundLevel
+ else {
+ return this.getWorldPointFromMathPlaneAtScreenXY(posXY, this.groundLevel);
+ }
+ };
+
+ this.updateMousePositionAndDelta = function updateMousePositionAndDelta(event) {
+ mousePosition.set(event.clientX, event.clientY);
+
+ deltaMousePosition.copy(mousePosition).sub(lastMousePosition);
+
+ lastMousePosition.copy(mousePosition);
+ };
+
+ /**
+ * Adds all the input event listeners (activate the controls)
+ */
+ this.addInputListeners = function addInputListeners() {
+ this.domElement.addEventListener('keydown', _handlerOnKeyDown, true);
+ this.domElement.addEventListener('mousedown', _handlerOnMouseDown, false);
+ this.domElement.addEventListener('mouseup', _handlerOnMouseUp, false);
+ this.domElement.addEventListener('mousemove', _handlerOnMouseMove, false);
+ this.domElement.addEventListener('mousewheel', _handlerOnMouseWheel, false);
+ // For firefox
+ this.domElement.addEventListener('MozMousePixelScroll', _handlerOnMouseWheel, false);
+ };
+
+ /**
+ * removes all the input event listeners (desactivate the controls)
+ */
+ this.removeInputListeners = function removeInputListeners() {
+ this.domElement.removeEventListener('keydown', _handlerOnKeyDown, true);
+ this.domElement.removeEventListener('mousedown', _handlerOnMouseDown, false);
+ this.domElement.removeEventListener('mouseup', _handlerOnMouseUp, false);
+ this.domElement.removeEventListener('mousemove', _handlerOnMouseMove, false);
+ this.domElement.removeEventListener('mousewheel', _handlerOnMouseWheel, false);
+ // For firefox
+ this.domElement.removeEventListener('MozMousePixelScroll', _handlerOnMouseWheel, false);
+ };
+
+ /**
+ * update the cursor image according to the control state
+ */
+ this.updateMouseCursorType = function updateMouseCursorType() {
+ switch (this.state) {
+ case STATE.NONE:
+ this.domElement.style.cursor = 'auto';
+ break;
+ case STATE.DRAG:
+ this.domElement.style.cursor = 'move';
+ break;
+ case STATE.PAN:
+ this.domElement.style.cursor = 'cell';
+ break;
+ case STATE.TRAVEL:
+ this.domElement.style.cursor = 'wait';
+ break;
+ case STATE.ROTATE:
+ this.domElement.style.cursor = 'move';
+ break;
+ default:
+ break;
+ }
+ };
+
+ // event listeners for user input (to activate the controls)
+ this.addInputListeners();
+}
+// ===== end of PlanarControls constructor =====
+
+/**
+* Catch and manage the event when a touch on the mouse is down.
+* @param {event} event : the current event (mouse left button clicked or mouse wheel button actionned)
+*/
+var onMouseDown = function onMouseDown(event) {
+ event.preventDefault();
+
+ if (this.state === STATE.TRAVEL) {
+ return;
+ }
+
+ this.updateMousePositionAndDelta(event);
+
+ if (event.button === mouseButtons.LEFTCLICK) {
+ if (event.ctrlKey) {
+ this.initiateRotation();
+ } else {
+ this.initiateDrag();
+ }
+ } else if (event.button === mouseButtons.MIDDLECLICK) {
+ this.initiateSmartZoom(event);
+ } else if (event.button === mouseButtons.RIGHTCLICK) {
+ this.initiatePan();
+ }
+
+ this.updateMouseCursorType();
+};
+
+/**
+* Catch the event when a touch on the mouse is uped.
+* @param {event} event : the current event
+*/
+var onMouseUp = function onMouseUp(event) {
+ event.preventDefault();
+
+ if (this.state !== STATE.TRAVEL) {
+ this.state = STATE.NONE;
+ }
+
+ this.updateMouseCursorType();
+};
+
+/**
+* Catch and manage the event when the mouse is moved
+* @param {event} event : the current event
+*/
+var onMouseMove = function onMouseMove(event) {
+ event.preventDefault();
+
+ this.updateMousePositionAndDelta(event);
+
+ // notify change if moving
+ if (this.state !== STATE.NONE) {
+ this.view.notifyChange(true);
+ }
+};
+
+/**
+* Catch and manage the event when a key is down.
+* @param {event} event : the current event
+*/
+var onKeyDown = function onKeyDown(event) {
+ if (this.state === STATE.TRAVEL) {
+ return;
+ }
+ if (event.keyCode === keys.T) {
+ this.goToTopView();
+ }
+ if (event.keyCode === keys.Y) {
+ this.goToStartView();
+ }
+ if (event.keyCode === keys.SPACE) {
+ this.initiateSmartZoom(event);
+ }
+};
+
+/**
+* Catch and manage the event when the mouse wheel is rolled.
+* @param {event} event : the current event
+*/
+var onMouseWheel = function onMouseWheel(event) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ if (this.state === STATE.NONE) {
+ this.initiateZoom(event);
+ }
+};
+
+/**
+* Catch and manage the event when the context menu is called (by a right click on the window).
+* We use this to prevent the context menu from appearing, so we can use right click for other inputs.
+* @param {event} event : the current event
+*/
+var onContextMenu = function onContextMenu(event) {
+ event.preventDefault();
+};
+
+/**
+* smoothing function (sigmoid) : based on h01 Hermite function
+* returns a value between 0 and 1
+* @param {number} value : the value to be smoothed, between 0 and 1
+* @returns {number}
+*/
+var smooth = function smooth(value) {
+ // p between 1.0 and 1.5 (empirical)
+ const p = 1.20;
+ return Math.pow((value * value * (3 - 2 * value)), p);
+};
+
+export default PlanarControls;