"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.keys = exports["default"] = exports.STATE = exports.PLANAR_CONTROL_EVENT = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _assertThisInitialized2 = _interopRequireDefault(require("@babel/runtime/helpers/assertThisInitialized"));
var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));
var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));
var THREE = _interopRequireWildcard(require("three"));
var _MainLoop = require("../Core/MainLoop");
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function () { var Super = (0, _getPrototypeOf2["default"])(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return (0, _possibleConstructorReturn2["default"])(this, result); }; }
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
// event keycode
var keys = {
  CTRL: 17,
  SPACE: 32,
  T: 84,
  Y: 89
};
exports.keys = keys;
var mouseButtons = {
  LEFTCLICK: THREE.MOUSE.LEFT,
  MIDDLECLICK: THREE.MOUSE.MIDDLE,
  RIGHTCLICK: THREE.MOUSE.RIGHT
};
var currentPressedButton;

// starting camera position and orientation target
var startPosition = new THREE.Vector3();
var startQuaternion = new THREE.Quaternion();
// camera initial zoom value if orthographic
var cameraInitialZoom = 0;

// point under the cursor
var pointUnderCursor = new THREE.Vector3();

// control state
var STATE = {
  NONE: -1,
  DRAG: 0,
  PAN: 1,
  ROTATE: 2,
  TRAVEL: 3,
  ORTHO_ZOOM: 4
};

// cursor shape linked to control state
exports.STATE = STATE;
var cursor = {
  "default": 'auto',
  drag: 'move',
  pan: 'cell',
  travel: 'wait',
  rotate: 'move',
  ortho_zoom: 'wait'
};
var vectorZero = new THREE.Vector3();

// mouse movement
var mousePosition = new THREE.Vector2();
var lastMousePosition = new THREE.Vector2();
var deltaMousePosition = new THREE.Vector2(0, 0);

// drag movement
var dragStart = new THREE.Vector3();
var dragEnd = new THREE.Vector3();
var dragDelta = new THREE.Vector3();

// camera focus point : ground point at screen center
var centerPoint = new THREE.Vector3(0, 0, 0);

// camera rotation
var phi = 0.0;

// displacement and rotation vectors
var vect = new THREE.Vector3();
var quat = new THREE.Quaternion();
var vect2 = new THREE.Vector2();

// animated travel
var travelEndPos = new THREE.Vector3();
var travelStartPos = new THREE.Vector3();
var travelStartRot = new THREE.Quaternion();
var travelEndRot = new THREE.Quaternion();
var travelAlpha = 0;
var travelDuration = 0;
var travelUseRotation = false;
var travelUseSmooth = false;

// zoom changes (for orthographic camera)
var startZoom = 0;
var endZoom = 0;

// ray caster for drag movement
var rayCaster = new THREE.Raycaster();
var plane = new THREE.Plane(new THREE.Vector3(0, 0, -1));

// default parameters :
var defaultOptions = {
  enabled: true,
  enableRotation: true,
  rotateSpeed: 2.0,
  minPanSpeed: 0.05,
  maxPanSpeed: 15,
  zoomTravelTime: 0.2,
  // must be a number
  zoomFactor: 2,
  maxResolution: 1 / Infinity,
  minResolution: Infinity,
  maxAltitude: 50000000,
  groundLevel: 200,
  autoTravelTimeMin: 1.5,
  autoTravelTimeMax: 4,
  autoTravelTimeDist: 50000,
  smartTravelHeightMin: 75,
  smartTravelHeightMax: 500,
  instantTravel: false,
  minZenithAngle: 0,
  maxZenithAngle: 82.5,
  handleCollision: true,
  minDistanceCollision: 30,
  enableSmartTravel: true,
  enablePan: true
};
var PLANAR_CONTROL_EVENT = {
  MOVED: 'moved'
};

/**
 * Planar controls is a camera controller adapted for a planar view, with animated movements.
 * Usage is as follow :
 * <ul>
 *     <li><b>Left mouse button:</b> drag the camera (translation on the (xy) world plane).</li>
 *     <li><b>Right mouse button:</b> pan the camera (translation on the vertical (z) axis of the world plane).</li>
 *     <li><b>CTRL + Left mouse button:</b> rotate the camera around the focus point.</li>
 *     <li><b>Wheel scrolling:</b> zoom toward the cursor position.</li>
 *     <li><b>Wheel clicking:</b> smart zoom toward the cursor position (animated).</li>
 *     <li><b>Y key:</b> go to the starting view (animated).</li>
 *     <li><b>T key:</b> go to the top view (animated).</li>
 * </ul>
 *
 * @class   PlanarControls
 * @param   {PlanarView}    view                                the view where the controls will be used
 * @param   {object}        options
 * @param   {boolean}       [options.enabled=true]              Set to false to disable this control
 * @param   {boolean}       [options.enableRotation=true]       Enable the rotation with the `CTRL + Left mouse button`
 * and in animations, like the smart zoom.
 * @param   {boolean}       [options.enableSmartTravel=true]    Enable smart travel with the `wheel-click / space-bar`.
 * @param   {boolean}       [options.enablePan=true]            Enable pan movements with the `right-click`.
 * @param   {number}        [options.rotateSpeed=2.0]           Rotate speed.
 * @param   {number}        [options.maxPanSpeed=15]            Pan speed when close to maxAltitude.
 * @param   {number}        [options.minPanSpeed=0.05]          Pan speed when close to the ground.
 * @param   {number}        [options.zoomTravelTime=0.2]        Animation time when zooming.
 * @param   {number}        [options.zoomFactor=2]              The factor the scale is multiplied by when zooming
 * in and divided by when zooming out. This factor can't be null.
 * @param   {number}        [options.maxResolution=0]           The smallest size in meters a pixel at the center of the
 * view can represent.
 * @param   {number}        [options.minResolution=Infinity]    The biggest size in meters a pixel at the center of the
 * view can represent.
 * @param   {number}        [options.maxAltitude=12000]         Maximum altitude reachable when panning or zooming out.
 * @param   {number}        [options.groundLevel=200]           Minimum altitude reachable when panning.
 * @param   {number}        [options.autoTravelTimeMin=1.5]     Minimum duration for animated travels with the `auto`
 * parameter.
 * @param   {number}        [options.autoTravelTimeMax=4]       Maximum duration for animated travels with the `auto`
 * parameter.
 * @param   {number}        [options.autoTravelTimeDist=20000]  Maximum travel distance for animated travel with the
 * `auto` parameter.
 * @param   {number}        [options.smartTravelHeightMin=75]     Minimum height above ground reachable after a smart
 * travel.
 * @param   {number}        [options.smartTravelHeightMax=500]    Maximum height above ground reachable after a smart
 * travel.
 * @param   {boolean}       [options.instantTravel=false]       If set to true, animated travels will have no duration.
 * @param   {number}        [options.minZenithAngle=0]          The minimum reachable zenith angle for a camera
 * rotation, in degrees.
 * @param   {number}        [options.maxZenithAngle=82.5]       The maximum reachable zenith angle for a camera
 * rotation, in degrees.
 * @param   {boolean}       [options.handleCollision=true]
 */
exports.PLANAR_CONTROL_EVENT = PLANAR_CONTROL_EVENT;
var PlanarControls = /*#__PURE__*/function (_THREE$EventDispatche) {
  (0, _inherits2["default"])(PlanarControls, _THREE$EventDispatche);
  var _super = _createSuper(PlanarControls);
  function PlanarControls(view) {
    var _this;
    var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    (0, _classCallCheck2["default"])(this, PlanarControls);
    _this = _super.call(this);
    _this.view = view;
    _this.camera = view.camera.camera3D;

    // Set to false to disable this control
    _this.enabled = typeof options.enabled == 'boolean' ? options.enabled : defaultOptions.enabled;
    if (_this.camera.isOrthographicCamera) {
      cameraInitialZoom = _this.camera.zoom;

      // enable rotation movements
      _this.enableRotation = false;

      // enable pan movements
      _this.enablePan = false;

      // Camera altitude is clamped under maxAltitude.
      // This is not relevant for an orthographic camera (since the orthographic camera altitude won't change).
      // Therefore, neutralizing by default the maxAltitude limit allows zooming out with an orthographic camera,
      // no matter its initial position.
      _this.maxAltitude = Infinity;

      // the zoom travel time (stored in `this.zoomTravelTime`) can't be `auto` with an orthographic camera
      _this.zoomTravelTime = typeof options.zoomTravelTime === 'number' ? options.zoomTravelTime : defaultOptions.zoomTravelTime;
    } else {
      // enable rotation movements
      _this.enableRotation = options.enableRotation === undefined ? defaultOptions.enableRotation : options.enableRotation;
      _this.rotateSpeed = options.rotateSpeed || defaultOptions.rotateSpeed;

      // enable pan movements
      _this.enablePan = options.enablePan === undefined ? defaultOptions.enablePan : options.enablePan;

      // minPanSpeed when close to the ground, maxPanSpeed when close to maxAltitude
      _this.minPanSpeed = options.minPanSpeed || defaultOptions.minPanSpeed;
      _this.maxPanSpeed = options.maxPanSpeed || defaultOptions.maxPanSpeed;

      // camera altitude is clamped under maxAltitude
      _this.maxAltitude = options.maxAltitude || defaultOptions.maxAltitude;

      // animation duration for the zoom
      _this.zoomTravelTime = options.zoomTravelTime || defaultOptions.zoomTravelTime;
    }

    // zoom movement is equal to the distance to the zoom target, multiplied by zoomFactor
    if (options.zoomInFactor) {
      console.warn('Controls zoomInFactor parameter is deprecated. Use zoomFactor instead.');
      options.zoomFactor = options.zoomFactor || options.zoomInFactor;
    }
    if (options.zoomOutFactor) {
      console.warn('Controls zoomOutFactor parameter is deprecated. Use zoomFactor instead.');
      options.zoomFactor = options.zoomFactor || options.zoomInFactor || 1 / options.zoomOutFactor;
    }
    if (options.zoomFactor === 0) {
      console.warn('Controls zoomFactor parameter can not be equal to 0. Its value will be set to default.');
      options.zoomFactor = defaultOptions.zoomFactor;
    }
    _this.zoomInFactor = options.zoomFactor || defaultOptions.zoomFactor;
    _this.zoomOutFactor = 1 / (options.zoomFactor || defaultOptions.zoomFactor);

    // the maximum and minimum size (in meters) a pixel at the center of the view can represent
    _this.maxResolution = options.maxResolution || defaultOptions.maxResolution;
    _this.minResolution = options.minResolution || defaultOptions.minResolution;

    // approximate ground altitude value. Camera altitude is clamped above groundLevel
    _this.groundLevel = options.groundLevel || defaultOptions.groundLevel;

    // min and max duration in seconds, for animated travels with `auto` parameter
    _this.autoTravelTimeMin = options.autoTravelTimeMin || defaultOptions.autoTravelTimeMin;
    _this.autoTravelTimeMax = options.autoTravelTimeMax || defaultOptions.autoTravelTimeMax;

    // max travel duration is reached for this travel distance (empirical smoothing value)
    _this.autoTravelTimeDist = options.autoTravelTimeDist || defaultOptions.autoTravelTimeDist;

    // after a smartZoom, camera height above ground will be between these two values
    if (options.smartZoomHeightMin) {
      console.warn('Controls smartZoomHeightMin parameter is deprecated. Use smartTravelHeightMin instead.');
      options.smartTravelHeightMin = options.smartTravelHeightMin || options.smartZoomHeightMin;
    }
    if (options.smartZoomHeightMax) {
      console.warn('Controls smartZoomHeightMax parameter is deprecated. Use smartTravelHeightMax instead.');
      options.smartTravelHeightMax = options.smartTravelHeightMax || options.smartZoomHeightMax;
    }
    _this.smartTravelHeightMin = options.smartTravelHeightMin || defaultOptions.smartTravelHeightMin;
    _this.smartTravelHeightMax = options.smartTravelHeightMax || defaultOptions.smartTravelHeightMax;

    // if set to true, animated travels have 0 duration
    _this.instantTravel = options.instantTravel || defaultOptions.instantTravel;

    // the zenith angle for a camera rotation will be between these two values
    _this.minZenithAngle = (options.minZenithAngle || defaultOptions.minZenithAngle) * Math.PI / 180;
    // max value should be less than 90 deg (90 = parallel to the ground)
    _this.maxZenithAngle = (options.maxZenithAngle || defaultOptions.maxZenithAngle) * Math.PI / 180;

    // focus policy options
    if (options.focusOnMouseOver) {
      console.warn('Planar controls \'focusOnMouseOver\' optional parameter has been removed.');
    }
    if (options.focusOnMouseClick) {
      console.warn('Planar controls \'focusOnMouseClick\' optional parameter has been removed.');
    }

    // set collision options
    _this.handleCollision = options.handleCollision === undefined ? defaultOptions.handleCollision : options.handleCollision;
    _this.minDistanceCollision = defaultOptions.minDistanceCollision;

    // enable smart travel
    _this.enableSmartTravel = options.enableSmartTravel === undefined ? defaultOptions.enableSmartTravel : options.enableSmartTravel;
    startPosition.copy(_this.camera.position);
    startQuaternion.copy(_this.camera.quaternion);

    // control state
    _this.state = STATE.NONE;
    _this.cursor = cursor;
    if (_this.view.controls) {
      // esLint-disable-next-line no-console
      console.warn('Deprecated use of PlanarControls. See examples to correct PlanarControls implementation.');
      _this.view.controls.dispose();
    }
    _this.view.controls = (0, _assertThisInitialized2["default"])(_this);

    // eventListeners handlers
    _this._handlerOnKeyDown = _this.onKeyDown.bind((0, _assertThisInitialized2["default"])(_this));
    _this._handlerOnMouseDown = _this.onMouseDown.bind((0, _assertThisInitialized2["default"])(_this));
    _this._handlerOnMouseUp = _this.onMouseUp.bind((0, _assertThisInitialized2["default"])(_this));
    _this._handlerOnMouseMove = _this.onMouseMove.bind((0, _assertThisInitialized2["default"])(_this));
    _this._handlerOnMouseWheel = _this.onMouseWheel.bind((0, _assertThisInitialized2["default"])(_this));
    _this._handlerContextMenu = _this.onContextMenu.bind((0, _assertThisInitialized2["default"])(_this));
    _this._handlerUpdate = _this.update.bind((0, _assertThisInitialized2["default"])(_this));

    // add this PlanarControl instance to the view's frameRequesters
    // with this, PlanarControl.update() will be called each frame
    _this.view.addFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE, _this._handlerUpdate);

    // event listeners for user input (to activate the controls)
    _this.addInputListeners();
    return _this;
  }
  (0, _createClass2["default"])(PlanarControls, [{
    key: "dispose",
    value: function dispose() {
      this.removeInputListeners();
      this.view.removeFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.AFTER_CAMERA_UPDATE, this._handlerUpdate);
    }

    /**
     * update the view and camera if needed, and handles the animated travel
     * @param   {number}    dt                  the delta time between two updates in millisecond
     * @param   {boolean}   updateLoopRestarted true if we just started rendering
     * @ignore
     */
  }, {
    key: "update",
    value: 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;
      }
      var onMovement = this.state !== STATE.NONE;
      switch (this.state) {
        case STATE.TRAVEL:
          this.handleTravel(dt);
          this.view.notifyChange(this.camera);
          break;
        case STATE.ORTHO_ZOOM:
          this.handleZoomOrtho(dt);
          this.view.notifyChange(this.camera);
          break;
        case STATE.DRAG:
          this.handleDragMovement();
          this.view.notifyChange(this.camera);
          break;
        case STATE.ROTATE:
          this.handleRotation();
          this.view.notifyChange(this.camera);
          break;
        case STATE.PAN:
          this.handlePanMovement();
          this.view.notifyChange(this.camera);
          break;
        case STATE.NONE:
        default:
          break;
      }
      // We test if camera collides to the geometry layer or is too close to the ground, and adjust its altitude in
      // case
      if (this.handleCollision) {
        // check distance to the ground/surface geometry (could be another geometry layer)
        this.view.camera.adjustAltitudeToAvoidCollisionWithLayer(this.view, this.view.tileLayer, this.minDistanceCollision);
      }
      if (onMovement) {
        this.view.dispatchEvent({
          type: PLANAR_CONTROL_EVENT.MOVED
        });
      }
      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 user to 'grab' a world point and drag it to move.
     *
     * @ignore
     */
  }, {
    key: "initiateDrag",
    value: 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]{@link PlanarControls#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.
     *
     * @ignore
     */
  }, {
    key: "handleDragMovement",
    value: function handleDragMovement() {
      // the world point under the current mouse cursor position, at same altitude than dragStart
      this.getWorldPointFromMathPlaneAtScreenXY(mousePosition, dragStart.z, dragEnd);

      // the difference between start and end cursor position
      dragDelta.subVectors(dragStart, dragEnd);

      // update the camera position
      this.camera.position.add(dragDelta);
      dragDelta.set(0, 0, 0);
    }

    /**
     * Initiate a pan movement (local translation on (xz) plane).
     *
     * @ignore
     */
  }, {
    key: "initiatePan",
    value: function initiatePan() {
      this.state = STATE.PAN;
    }

    /**
     * Handle the pan movement (translation on local x / world z plane) when user moves the mouse while
     * STATE.PAN. The drag movement is previously initiated by [initiatePan]{@link PlanarControls#initiatePan}.
     * Compute the pan value and update the camera controls.
     *
     * @ignore
     */
  }, {
    key: "handlePanMovement",
    value: function handlePanMovement() {
      vect.set(-deltaMousePosition.x, deltaMousePosition.y, 0);
      this.camera.localToWorld(vect);
      this.camera.position.copy(vect);
    }

    /**
     * Initiate a rotate (orbit) movement.
     *
     * @ignore
     */
  }, {
    key: "initiateRotation",
    value: function initiateRotation() {
      this.state = STATE.ROTATE;
      centerPoint.copy(this.getWorldPointAtScreenXY(new THREE.Vector2(0.5 * this.view.mainLoop.gfxEngine.width, 0.5 * this.view.mainLoop.gfxEngine.height)));
      var radius = this.camera.position.distanceTo(centerPoint);
      phi = Math.acos((this.camera.position.z - centerPoint.z) / radius);
    }

    /**
     * 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]{@link PlanarControls#initiateRotation}.
     * Compute the new position value and update the camera controls.
     *
     * @ignore
     */
  }, {
    key: "handleRotation",
    value: function handleRotation() {
      // angle deltas
      // deltaMousePosition is computed in onMouseMove / onMouseDowns
      var thetaDelta = -this.rotateSpeed * deltaMousePosition.x / this.view.mainLoop.gfxEngine.width;
      var phiDelta = -this.rotateSpeed * deltaMousePosition.y / this.view.mainLoop.gfxEngine.height;

      // the vector from centerPoint (focus point) to camera position
      var 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;
          vect.set(0, 0, 1);
          quat.setFromUnitVectors(this.camera.up, vect);
          offset.applyQuaternion(quat);
          vect.setFromMatrixColumn(this.camera.matrix, 0);
          quat.setFromAxisAngle(vect, phiDelta);
          offset.applyQuaternion(quat);
          vect.set(0, 0, 1);
          quat.setFromUnitVectors(this.camera.up, vect).invert();
          offset.applyQuaternion(quat);
        }
        if (thetaDelta !== 0) {
          // rotation around Z (azimuth)
          vect.set(0, 0, 1);
          quat.setFromAxisAngle(vect, thetaDelta);
          offset.applyQuaternion(quat);
        }
      }
      this.camera.position.copy(offset);
      // TODO : lookAt calls an updateMatrixWorld(). It should be replaced by a new method that does not.
      this.camera.lookAt(vectorZero);
      this.camera.position.add(centerPoint);
      this.camera.updateMatrixWorld();
    }

    /**
     * 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]{@link PlanarControls#initiateTravel} function).
     *
     * @param   {Event} event   the mouse wheel event.
     * @ignore
     */
  }, {
    key: "initiateZoom",
    value: function initiateZoom(event) {
      var delta = -event.deltaY;
      pointUnderCursor.copy(this.getWorldPointAtScreenXY(mousePosition));
      var newPos = new THREE.Vector3();
      if (delta > 0 || delta < 0 && this.maxAltitude > this.camera.position.z) {
        var zoomFactor = delta > 0 ? this.zoomInFactor : this.zoomOutFactor;

        // do not zoom if the resolution after the zoom is outside resolution limits
        var endResolution = this.view.getPixelsToMeters() / zoomFactor;
        if (this.maxResolution > endResolution || endResolution > this.minResolution) {
          return;
        }

        // change the camera field of view if the camera is orthographic
        if (this.camera.isOrthographicCamera) {
          // switch state to STATE.ZOOM
          this.state = STATE.ORTHO_ZOOM;
          this.view.notifyChange(this.camera);

          // camera zoom at the beginning of zoom movement
          startZoom = this.camera.zoom;
          // camera zoom at the end of zoom movement
          endZoom = startZoom * zoomFactor;
          // the altitude of the target must be the same as camera's
          pointUnderCursor.z = this.camera.position.z;
          travelAlpha = 0;
          travelDuration = this.zoomTravelTime;
          this.updateMouseCursorType();
        } else {
          // target position
          newPos.lerpVectors(this.camera.position, pointUnderCursor, 1 - 1 / zoomFactor);
          // initiate travel
          this.initiateTravel(newPos, this.zoomTravelTime, null, false);
        }
      }
    }

    /**
     * Handle the animated zoom change for an orthographic camera, when state is `ZOOM`.
     *
     * @param   {number}    dt  the delta time between two updates in milliseconds
     * @ignore
     */
  }, {
    key: "handleZoomOrtho",
    value: function handleZoomOrtho(dt) {
      travelAlpha = Math.min(travelAlpha + dt / 1000 / travelDuration, 1);

      // new zoom
      var zoom = startZoom + travelAlpha * (endZoom - startZoom);
      if (this.camera.zoom !== zoom) {
        // zoom has changed
        this.camera.zoom = zoom;
        this.camera.updateProjectionMatrix();

        // current world coordinates under the mouse
        this.view.viewToNormalizedCoords(mousePosition, vect);
        vect.z = 0;
        vect.unproject(this.camera);

        // new camera position
        this.camera.position.x += pointUnderCursor.x - vect.x;
        this.camera.position.y += pointUnderCursor.y - vect.y;
        this.camera.updateMatrixWorld(true);
      }

      // completion test
      this.testAnimationEnd();
    }

    /**
     * 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.
     *
     * @ignore
     */
  }, {
    key: "initiateSmartTravel",
    value: function initiateSmartTravel() {
      var pointUnderCursor = this.getWorldPointAtScreenXY(mousePosition);

      // direction of the movement, projected on xy plane and normalized
      var dir = new THREE.Vector3();
      dir.copy(pointUnderCursor).sub(this.camera.position);
      dir.z = 0;
      dir.normalize();
      var distanceToPoint = this.camera.position.distanceTo(pointUnderCursor);

      // camera height (altitude above ground) at the end of the travel, 5000 is an empirical smoothing distance
      var targetHeight = THREE.MathUtils.lerp(this.smartTravelHeightMin, this.smartTravelHeightMax, Math.min(distanceToPoint / 5000, 1));

      // camera position at the end of the travel
      var moveTarget = new THREE.Vector3();
      moveTarget.copy(pointUnderCursor);
      if (this.enableRotation) {
        moveTarget.add(dir.multiplyScalar(-targetHeight * 2));
      }
      moveTarget.z = pointUnderCursor.z + targetHeight;
      if (this.camera.isOrthographicCamera) {
        startZoom = this.camera.zoom;
        // camera zoom at the end of the travel, 5000 is an empirical smoothing distance
        endZoom = startZoom * (1 + Math.min(distanceToPoint / 5000, 1));
        moveTarget.z = this.camera.position.z;
      }

      // initiate the travel
      this.initiateTravel(moveTarget, 'auto', pointUnderCursor, true);
    }

    /**
     * Triggers an animated movement and rotation for the camera.
     *
     * @param   {THREE.Vector3} targetPos   The target position of the camera (reached at the end).
     * @param   {number|string}        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 :
     * <ul>
     *     <li>if targetOrientation is a world point (Vector3) : the camera will lookAt() this point</li>
     *     <li>if targetOrientation is a quaternion : this quaternion will define the final camera orientation </li>
     *     <li>if targetOrientation is neither a world point nor a quaternion : the camera will keep its starting
     *     orientation</li>
     * </ul>
     * @param   {boolean}       useSmooth   animation is smoothed using the `smooth(value)` function (slower
     * at start and finish).
     *
     * @ignore
     */
  }, {
    key: "initiateTravel",
    value: function initiateTravel(targetPos, travelTime, targetOrientation, useSmooth) {
      this.state = STATE.TRAVEL;
      this.view.notifyChange(this.camera);
      // the progress of the travel (animation alpha)
      travelAlpha = 0;
      // update cursor
      this.updateMouseCursorType();
      travelUseRotation = this.enableRotation && targetOrientation && (targetOrientation.isQuaternion || targetOrientation.isVector3);
      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 :
      if (travelUseRotation) {
        if (targetOrientation.isQuaternion) {
          // case where targetOrientation is a quaternion
          travelEndRot.copy(targetOrientation);
        } else if (targetOrientation.isVector3) {
          // case where targetOrientation is a 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;
      } else if (travelTime === 'auto') {
        // case where travelTime is set to `auto` : travelDuration will be a value between autoTravelTimeMin and
        // autoTravelTimeMax depending on travel distance and travel angular difference

        // a value between 0 and 1 according to the travel distance. Adjusted by autoTravelTimeDist parameter
        var normalizedDistance = Math.min(1, targetPos.distanceTo(this.camera.position) / this.autoTravelTimeDist);
        travelDuration = THREE.MathUtils.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 exceed autoTravelTimeMax
        if (travelUseRotation) {
          // value is normalized between 0 and 1
          var angularDifference = 0.5 - 0.5 * travelEndRot.normalize().dot(this.camera.quaternion.normalize());
          travelDuration *= 1 + 2 * angularDifference;
          travelDuration = Math.min(travelDuration, this.autoTravelTimeMax);
        }
      } else {
        // case where travelTime !== `auto` : travelTime is a duration in seconds given as parameter
        travelDuration = travelTime;
      }
    }

    /**
     * Handle the animated movement and rotation of the camera in `travel` state.
     *
     * @param   {number}    dt  the delta time between two updates in milliseconds
     * @ignore
     */
  }, {
    key: "handleTravel",
    value: function handleTravel(dt) {
      travelAlpha = Math.min(travelAlpha + dt / 1000 / travelDuration, 1);

      // the animation alpha, between 0 (start) and 1 (finish)
      var alpha = travelUseSmooth ? this.smooth(travelAlpha) : travelAlpha;

      // new position
      this.camera.position.lerpVectors(travelStartPos, travelEndPos, alpha);
      var zoom = startZoom + alpha * (endZoom - startZoom);
      // new zoom
      if (this.camera.isOrthographicCamera && this.camera.zoom !== zoom) {
        this.camera.zoom = zoom;
        this.camera.updateProjectionMatrix();
      }

      // new rotation
      if (travelUseRotation === true) {
        this.camera.quaternion.slerpQuaternions(travelStartRot, travelEndRot, alpha);
      }

      // completion test
      this.testAnimationEnd();
    }

    /**
     * Test if the currently running animation is finished (travelAlpha reached 1).
     * If it is, reset controls to state NONE.
     *
     * @ignore
     */
  }, {
    key: "testAnimationEnd",
    value: function testAnimationEnd() {
      if (travelAlpha === 1) {
        // Resume normal behaviour after animation is completed
        this.state = STATE.NONE;
        this.updateMouseCursorType();
      }
    }

    /**
     * Triggers an animated movement (travel) to set the camera to top view, above the focus point,
     * at altitude = distanceToFocusPoint.
     *
     * @ignore
     */
  }, {
    key: "goToTopView",
    value: function goToTopView() {
      var topViewPos = new THREE.Vector3();
      var targetQuat = new THREE.Quaternion();

      // the top view position is above the camera focus point, at an altitude = distanceToPoint
      topViewPos.copy(this.getWorldPointAtScreenXY(new THREE.Vector2(0.5 * this.view.mainLoop.gfxEngine.width, 0.5 * this.view.mainLoop.gfxEngine.height)));
      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
     *
     * @ignore
     */
  }, {
    key: "goToStartView",
    value: function goToStartView() {
      // if startZoom and endZoom have not been set yet, give them neutral values
      if (this.camera.isOrthographicCamera) {
        startZoom = this.camera.zoom;
        endZoom = cameraInitialZoom;
      }
      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 (does not us actual geometry).
     *
     * @param   {THREE.Vector2} posXY       the mouse position in screen space (unit : pixel)
     * @param   {number}        altitude    the altitude (z) of the mathematical plane
     * @param   {THREE.Vector3} target      the target vector3
     * @return  {THREE.Vector3}
     * @ignore
     */
  }, {
    key: "getWorldPointFromMathPlaneAtScreenXY",
    value: function getWorldPointFromMathPlaneAtScreenXY(posXY, altitude) {
      var target = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : new THREE.Vector3();
      vect2.copy(this.view.viewToNormalizedCoords(posXY));
      rayCaster.setFromCamera(vect2, this.camera);
      plane.constant = altitude;
      rayCaster.ray.intersectPlane(plane, target);
      return target;
    }

    /**
     * Returns the world point (xyz) under the posXY screen point. If geometry is under the cursor, the point is
     * obtained with getPickingPositionFromDepth. If no geometry is under the cursor, the point is obtained with
     * [getWorldPointFromMathPlaneAtScreenXY]{@link PlanarControls#getWorldPointFromMathPlaneAtScreenXY}.
     *
     * @param   {THREE.Vector2} posXY   the mouse position in screen space (unit : pixel)
     * @param   {THREE.Vector3} target  the target World coordinates.
     * @return  {THREE.Vector3}
     * @ignore
     */
  }, {
    key: "getWorldPointAtScreenXY",
    value: function getWorldPointAtScreenXY(posXY) {
      var target = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : new THREE.Vector3();
      // check if there is a valid geometry under cursor
      if (this.view.getPickingPositionFromDepth(posXY, target)) {
        return target;
      } else {
        // if not, we use the mathematical plane at altitude = groundLevel
        this.getWorldPointFromMathPlaneAtScreenXY(posXY, this.groundLevel, target);
        return target;
      }
    }

    /**
     * Add all the input event listeners (activate the controls).
     *
     * @ignore
     */
  }, {
    key: "addInputListeners",
    value: function addInputListeners() {
      this.view.domElement.addEventListener('keydown', this._handlerOnKeyDown, false);
      this.view.domElement.addEventListener('mousedown', this._handlerOnMouseDown, false);
      this.view.domElement.addEventListener('mouseup', this._handlerOnMouseUp, false);
      this.view.domElement.addEventListener('mouseleave', this._handlerOnMouseUp, false);
      this.view.domElement.addEventListener('mousemove', this._handlerOnMouseMove, false);
      this.view.domElement.addEventListener('wheel', this._handlerOnMouseWheel, false);
      // prevent the default context menu from appearing when right-clicking
      // this allows to use right-click for input without the menu appearing
      this.view.domElement.addEventListener('contextmenu', this._handlerContextMenu, false);
    }

    /**
     * Removes all the input listeners (deactivate the controls).
     *
     * @ignore
     */
  }, {
    key: "removeInputListeners",
    value: function removeInputListeners() {
      this.view.domElement.removeEventListener('keydown', this._handlerOnKeyDown, true);
      this.view.domElement.removeEventListener('mousedown', this._handlerOnMouseDown, false);
      this.view.domElement.removeEventListener('mouseup', this._handlerOnMouseUp, false);
      this.view.domElement.removeEventListener('mouseleave', this._handlerOnMouseUp, false);
      this.view.domElement.removeEventListener('mousemove', this._handlerOnMouseMove, false);
      this.view.domElement.removeEventListener('wheel', this._handlerOnMouseWheel, false);
      this.view.domElement.removeEventListener('contextmenu', this._handlerContextMenu, false);
    }

    /**
     * Update the cursor image according to the control state.
     *
     * @ignore
     */
  }, {
    key: "updateMouseCursorType",
    value: function updateMouseCursorType() {
      switch (this.state) {
        case STATE.NONE:
          this.view.domElement.style.cursor = this.cursor["default"];
          break;
        case STATE.DRAG:
          this.view.domElement.style.cursor = this.cursor.drag;
          break;
        case STATE.PAN:
          this.view.domElement.style.cursor = this.cursor.pan;
          break;
        case STATE.TRAVEL:
          this.view.domElement.style.cursor = this.cursor.travel;
          break;
        case STATE.ORTHO_ZOOM:
          this.view.domElement.style.cursor = this.cursor.ortho_zoom;
          break;
        case STATE.ROTATE:
          this.view.domElement.style.cursor = this.cursor.rotate;
          break;
        default:
          break;
      }
    }
  }, {
    key: "updateMousePositionAndDelta",
    value: function updateMousePositionAndDelta(event) {
      this.view.eventToViewCoords(event, mousePosition);
      deltaMousePosition.copy(mousePosition).sub(lastMousePosition);
      lastMousePosition.copy(mousePosition);
    }

    /**
     * cursor modification for a specifique state.
     *
     * @param   {string} state   the state in which we want to change the cursor ('default', 'drag', 'pan', 'travel', 'rotate').
     * @param   {string} newCursor   the css cursor we want to have for the specified state.
     * @ignore
     */
  }, {
    key: "setCursor",
    value: function setCursor(state, newCursor) {
      this.cursor[state] = newCursor;
      this.updateMouseCursorType();
    }

    /**
     * Catch and manage the event when a touch on the mouse is downs.
     *
     * @param   {Event} event   the current event (mouse left or right button clicked, mouse wheel button actioned).
     * @ignore
     */
  }, {
    key: "onMouseDown",
    value: function onMouseDown(event) {
      if (!this.enabled) {
        return;
      }
      event.preventDefault();
      this.view.domElement.focus();
      if (STATE.NONE !== this.state) {
        return;
      }
      currentPressedButton = event.button;
      this.updateMousePositionAndDelta(event);
      if (mouseButtons.LEFTCLICK === event.button) {
        if (event.ctrlKey) {
          if (this.enableRotation) {
            this.initiateRotation();
          } else {
            return;
          }
        } else {
          this.initiateDrag();
        }
      } else if (mouseButtons.MIDDLECLICK === event.button) {
        if (this.enableSmartTravel) {
          this.initiateSmartTravel();
        } else {
          return;
        }
      } else if (mouseButtons.RIGHTCLICK === event.button) {
        if (this.enablePan) {
          this.initiatePan();
        } else {
          return;
        }
      }
      this.updateMouseCursorType();
    }

    /**
     * Catch and manage the event when a touch on the mouse is released.
     *
     * @param   {Event} event   the current event
     * @ignore
     */
  }, {
    key: "onMouseUp",
    value: function onMouseUp(event) {
      event.preventDefault();

      // Does not interrupt ongoing camera action if state is TRAVEL or CAMERA_OTHO. This prevents interrupting a zoom
      // movement or a smart travel by pressing any movement key.
      // The camera action is also uninterrupted if the released button does not match the button triggering the
      // ongoing action. This prevents for instance exiting drag mode when right-clicking while dragging the view.
      if (STATE.TRAVEL !== this.state && STATE.ORTHO_ZOOM !== this.state && currentPressedButton === event.button) {
        this.state = STATE.NONE;
      }
      this.updateMouseCursorType();
    }

    /**
     * Catch and manage the event when the mouse is moved.
     *
     * @param   {Event} event   the current event.
     * @ignore
     */
  }, {
    key: "onMouseMove",
    value: function onMouseMove(event) {
      if (!this.enabled) {
        return;
      }
      event.preventDefault();
      this.updateMousePositionAndDelta(event);

      // notify change if moving
      if (STATE.NONE !== this.state) {
        this.view.notifyChange();
      }
    }

    /**
     * Catch and manage the event when a key is down.
     *
     * @param   {Event} event   the current event
     * @ignore
     */
  }, {
    key: "onKeyDown",
    value: function onKeyDown(event) {
      if (STATE.NONE !== this.state || !this.enabled) {
        return;
      }
      switch (event.keyCode) {
        case keys.T:
          // going to top view is not relevant for an orthographic camera, since it is always top view
          if (!this.camera.isOrthographicCamera) {
            this.goToTopView();
          }
          break;
        case keys.Y:
          this.goToStartView();
          break;
        case keys.SPACE:
          if (this.enableSmartTravel) {
            this.initiateSmartTravel(event);
          }
          break;
        default:
          break;
      }
    }

    /**
     * Catch and manage the event when the mouse wheel is rolled.
     *
     * @param   {Event} event   the current event
     * @ignore
     */
  }, {
    key: "onMouseWheel",
    value: function onMouseWheel(event) {
      if (!this.enabled) {
        return;
      }
      event.preventDefault();
      event.stopPropagation();
      if (STATE.NONE === this.state) {
        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
     * @ignore
     */
  }, {
    key: "onContextMenu",
    value: function onContextMenu(event) {
      event.preventDefault();
    }

    /**
     * Smoothing function (sigmoid) : based on h01 Hermite function.
     *
     * @param   {number}    value   the value to be smoothed, between 0 and 1.
     * @return  {number}            a value between 0 and 1.
     * @ignore
     */
  }, {
    key: "smooth",
    value: function smooth(value) {
      // p between 1.0 and 1.5 (empirical)

      return Math.pow(Math.pow(value, 2) * (3 - 2 * value), 1.20);
    }
  }]);
  return PlanarControls;
}(THREE.EventDispatcher);
var _default = PlanarControls;
exports["default"] = _default;