"use strict";

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports["default"] = exports.VIEW_EVENTS = void 0;

var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));

var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));

var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));

var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));

var _getPrototypeOf2 = _interopRequireDefault(require("@babel/runtime/helpers/getPrototypeOf"));

var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits"));

var THREE = _interopRequireWildcard(require("three"));

var _Camera = _interopRequireDefault(require("../Renderer/Camera"));

var _MainLoop = _interopRequireWildcard(require("./MainLoop"));

var _ColorLayersOrdering = require("../Renderer/ColorLayersOrdering");

var _c3DEngine = _interopRequireDefault(require("../Renderer/c3DEngine"));

var _RenderMode = _interopRequireDefault(require("../Renderer/RenderMode"));

var _Crs = _interopRequireDefault(require("./Geographic/Crs"));

var _LayeredMaterial = require("../Renderer/LayeredMaterial");

var _Scheduler = _interopRequireDefault(require("./Scheduler/Scheduler"));

var _Picking = _interopRequireDefault(require("./Picking"));

var VIEW_EVENTS = {
  /**
   * Fires when all the layers of the view are considered initialized.
   * Initialized in this context means: all layers are ready to be
   * displayed (no pending network access, no visual improvement to be
   * expected, ...).
   * If you add new layers, the event will be fired again when all
   * layers are ready.
   * @event View#layers-initialized
   * @property type {string} layers-initialized
   */
  LAYERS_INITIALIZED: 'layers-initialized',
  LAYER_REMOVED: 'layer-removed',
  LAYER_ADDED: 'layer-added',
  INITIALIZED: 'initialized',
  COLOR_LAYERS_ORDER_CHANGED: _ColorLayersOrdering.COLOR_LAYERS_ORDER_CHANGED
};
exports.VIEW_EVENTS = VIEW_EVENTS;

var _syncGeometryLayerVisibility = function (layer, view) {
  if (layer.object3d) {
    layer.object3d.visible = layer.visible;
  }

  if (layer.threejsLayer) {
    if (layer.visible) {
      view.camera.camera3D.layers.enable(layer.threejsLayer);
    } else {
      view.camera.camera3D.layers.disable(layer.threejsLayer);
    }
  }
};

function _preprocessLayer(view, layer, provider, parentLayer) {
  var source = layer.source;

  if (parentLayer && !layer.extent) {
    layer.extent = parentLayer.extent;

    if (source && !source.extent) {
      source.extent = parentLayer.extent;
    }
  }

  if (layer.isGeometryLayer) {
    if (parentLayer) {
      // layer.threejsLayer *must* be assigned before preprocessing,
      // because TileProvider.preprocessDataLayer function uses it.
      layer.threejsLayer = view.mainLoop.gfxEngine.getUniqueThreejsLayer();
    }

    layer.defineLayerProperty('visible', true, function () {
      return _syncGeometryLayerVisibility(layer, view);
    });

    _syncGeometryLayerVisibility(layer, view); // Find projection layer, this is projection destination


    layer.projection = view.referenceCrs;
  } else if (parentLayer.tileMatrixSets.includes(_Crs["default"].formatToTms(source.projection))) {
    layer.projection = source.projection;
  } else {
    layer.projection = parentLayer.extent.crs;
  }

  if (!layer.whenReady) {
    if (provider && provider.preprocessDataLayer) {
      layer.whenReady = provider.preprocessDataLayer(layer, view, view.mainLoop.scheduler, parentLayer);
    } else if (source && source.whenReady) {
      layer.whenReady = source.whenReady;
    } else {
      layer.whenReady = Promise.resolve();
    }
  }

  layer.whenReady = layer.whenReady.then(function () {
    layer.ready = true;
    return layer;
  });
  return layer;
}

var _eventCoords = new THREE.Vector2();

var matrix = new THREE.Matrix4();
var screen = new THREE.Vector2();
var ray = new THREE.Ray();
var direction = new THREE.Vector3();

var View =
/*#__PURE__*/
function (_THREE$EventDispatche) {
  (0, _inherits2["default"])(View, _THREE$EventDispatche);

  /**
   * Constructs an Itowns View instance
   *
   * @param {string} crs - The default CRS of Three.js coordinates. Should be a cartesian CRS.
   * @param {HTMLElement} viewerDiv - Where to instanciate the Three.js scene in the DOM
   * @param {Object=} options - Optional properties.
   * @param {?MainLoop} options.mainLoop - {@link MainLoop} instance to use, otherwise a default one will be constructed
   * @param {?(WebGLRenderer|object)} options.renderer - {@link WebGLRenderer} instance to use, otherwise
   * a default one will be constructed. In this case, if options.renderer is an object, it will be used to
   * configure the renderer (see {@link c3DEngine}.  If not present, a new &lt;canvas> will be created and
   * added to viewerDiv (mutually exclusive with mainLoop)
   * @param {?Scene} options.scene3D - {@link Scene} instance to use, otherwise a default one will be constructed
   * @constructor
   */
  function View(crs, viewerDiv) {
    var _this;

    var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
    (0, _classCallCheck2["default"])(this, View);

    if (!viewerDiv) {
      throw new Error('Invalid viewerDiv parameter (must non be null/undefined)');
    }

    _this = (0, _possibleConstructorReturn2["default"])(this, (0, _getPrototypeOf2["default"])(View).call(this));
    _this.referenceCrs = crs;
    var engine; // options.renderer can be 2 separate things:
    //   - an actual renderer (in this case we don't use viewerDiv)
    //   - options for the renderer to be created

    if (options.renderer && options.renderer.domElement) {
      engine = new _c3DEngine["default"](options.renderer);
    } else {
      engine = new _c3DEngine["default"](viewerDiv, options.renderer);
    }

    _this.mainLoop = options.mainLoop || new _MainLoop["default"](new _Scheduler["default"](), engine);
    _this.scene = options.scene3D || new THREE.Scene();

    if (!options.scene3D) {
      _this.scene.autoUpdate = false;
    }

    _this.camera = new _Camera["default"](_this.referenceCrs, _this.mainLoop.gfxEngine.getWindowSize().x, _this.mainLoop.gfxEngine.getWindowSize().y, options);
    _this._frameRequesters = {};
    _this._layers = [];
    window.addEventListener('resize', function () {
      // If the user gave us a container (<div>) then itowns' size is
      // the container's size. Otherwise we use window' size.
      _this.mainLoop.gfxEngine.onWindowResize(viewerDiv.clientWidth, viewerDiv.clientHeight);

      _this.camera.resize(viewerDiv.clientWidth, viewerDiv.clientHeight);

      _this.notifyChange(_this.camera.camera3D);
    }, false);
    _this._changeSources = new Set();
    _this._delayedFrameRequesterRemoval = [];

    _this._allLayersAreReadyCallback = function () {
      // all layers must be ready
      var allReady = _this.getLayers().every(function (layer) {
        return layer.ready;
      });

      if (allReady && _this.mainLoop.scheduler.commandsWaitingExecutionCount() == 0 && _this.mainLoop.renderingState == _MainLoop.RENDERING_PAUSED) {
        _this.dispatchEvent({
          type: VIEW_EVENTS.LAYERS_INITIALIZED
        });

        _this.removeFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END, _this._allLayersAreReadyCallback);
      }
    };

    _this.camera.resize(viewerDiv.clientWidth, viewerDiv.clientHeight);

    var fn = function () {
      _this.removeEventListener(VIEW_EVENTS.LAYERS_INITIALIZED, fn);

      _this.dispatchEvent({
        type: VIEW_EVENTS.INITIALIZED
      });
    };

    _this.addEventListener(VIEW_EVENTS.LAYERS_INITIALIZED, fn);

    _this._fullSizeDepthBuffer = null;

    _this.addFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.BEFORE_RENDER, function () {
      if (_this._fullSizeDepthBuffer != null && _this._fullSizeDepthBuffer.needsUpdate) {
        // clean depth buffer
        _this._fullSizeDepthBuffer = null;
      }
    }); // Focus needed to capture some key events.


    viewerDiv.focus();
    return _this;
  }
  /**
   * Add layer in viewer.
   * The layer id must be unique.
   *
   * This function calls `preprocessDataLayer` of the relevant provider with this
   * layer and set `layer.whenReady` to a promise that resolves when
   * the preprocessing operation is done. This promise is also returned by
   * `addLayer` allowing to chain call.
   *
   * @param {LayerOptions|Layer|GeometryLayer} layer
   * @param {Layer=} parentLayer
   * @return {Promise} a promise resolved with the new layer object when it is fully initialized or rejected if any error occurred.
   */


  (0, _createClass2["default"])(View, [{
    key: "addLayer",
    value: function addLayer(layer, parentLayer) {
      var _this2 = this;

      return new Promise(function (resolve, reject) {
        if (!layer) {
          reject(new Error('layer is undefined'));
          return;
        }

        var duplicate = _this2.getLayerById(layer.id);

        if (duplicate) {
          reject(new Error("Invalid id '".concat(layer.id, "': id already used")));
          return;
        }

        var protocol = layer.source ? layer.source.protocol : layer.protocol;

        var provider = _this2.mainLoop.scheduler.getProtocolProvider(protocol);

        if (layer.protocol && !provider) {
          reject(new Error("".concat(layer.protocol, " is not a recognized protocol name.")));
          return;
        }

        layer = _preprocessLayer(_this2, layer, provider, parentLayer);

        if (parentLayer) {
          if (layer.isColorLayer) {
            var layerColors = _this2.getLayers(function (l) {
              return l.isColorLayer;
            });

            var sumColorLayers = parentLayer.countColorLayersTextures.apply(parentLayer, (0, _toConsumableArray2["default"])(layerColors).concat([layer]));

            if (sumColorLayers <= (0, _LayeredMaterial.getMaxColorSamplerUnitsCount)()) {
              parentLayer.attach(layer);
            } else {
              reject(new Error("Cant add color layer ".concat(layer.id, ": the maximum layer is reached")));
              return;
            }
          } else {
            parentLayer.attach(layer);
          }
        } else {
          if (typeof layer.update !== 'function') {
            reject(new Error('Cant add GeometryLayer: missing a update function'));
            return;
          }

          if (typeof layer.preUpdate !== 'function') {
            reject(new Error('Cant add GeometryLayer: missing a preUpdate function'));
            return;
          }

          _this2._layers.push(layer);
        }

        if (layer.object3d && !layer.object3d.parent && layer.object3d !== _this2.scene) {
          _this2.scene.add(layer.object3d);
        }

        layer.whenReady.then(function (layer) {
          _this2.notifyChange(parentLayer || layer, false);

          if (!_this2._frameRequesters[_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END] || !_this2._frameRequesters[_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END].includes(_this2._allLayersAreReadyCallback)) {
            _this2.addFrameRequester(_MainLoop.MAIN_LOOP_EVENTS.UPDATE_END, _this2._allLayersAreReadyCallback);
          }

          resolve(layer);
        });

        _this2.dispatchEvent({
          type: VIEW_EVENTS.LAYER_ADDED,
          layerId: layer.id
        });
      });
    }
    /**
     * Removes a specific imagery layer from the current layer list. This removes layers inserted with attach().
     * @example
     * view.removeLayer('layerId');
     * @param      {string}   layerId      The identifier
     * @return     {boolean}
     */

  }, {
    key: "removeLayer",
    value: function removeLayer(layerId) {
      var layer = this.getLayerById(layerId);

      if (layer) {
        var parentLayer = layer.parent; // Remove and dispose all nodes

        layer["delete"](); // Detach layer if it's attached

        if (parentLayer && !parentLayer.detach(layer)) {
          throw new Error("Error to detach ".concat(layerId, " from ").concat(parentLayer.id));
        } else if (parentLayer == undefined) {
          // Remove layer from viewer
          this._layers.splice(this._layers.findIndex(function (l) {
            return l.id == layerId;
          }), 1);
        }

        if (layer.isColorLayer) {
          // Update color layers sequence
          var imageryLayers = this.getLayers(function (l) {
            return l.isColorLayer;
          });
          var _iteratorNormalCompletion = true;
          var _didIteratorError = false;
          var _iteratorError = undefined;

          try {
            for (var _iterator = imageryLayers[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
              var color = _step.value;

              if (color.sequence > layer.sequence) {
                color.sequence--;
              }
            }
          } catch (err) {
            _didIteratorError = true;
            _iteratorError = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion && _iterator["return"] != null) {
                _iterator["return"]();
              }
            } finally {
              if (_didIteratorError) {
                throw _iteratorError;
              }
            }
          }
        }

        this.notifyChange(this.camera);
        this.dispatchEvent({
          type: VIEW_EVENTS.LAYER_REMOVED,
          layerId: layerId
        });
        return true;
      } else {
        throw new Error("".concat(layerId, " doesn't exist"));
      }
    }
    /**
     * Notifies the scene it needs to be updated due to changes exterior to the
     * scene itself (e.g. camera movement).
     * non-interactive events (e.g: texture loaded)
     * @param {*} changeSource
     * @param {boolean} needsRedraw - indicates if notified change requires a full scene redraw.
     */

  }, {
    key: "notifyChange",
    value: function notifyChange() {
      var changeSource = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : undefined;
      var needsRedraw = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;

      if (changeSource) {
        this._changeSources.add(changeSource);

        if ((changeSource.isTileMesh || changeSource.isCamera) && this._fullSizeDepthBuffer) {
          this._fullSizeDepthBuffer.needsUpdate = true;
        }
      }

      this.mainLoop.scheduleViewUpdate(this, needsRedraw);
    }
    /**
     * Get all layers, with an optionnal filter applied.
     * The filter method will be called with 2 args:
     *   - 1st: current layer
     *   - 2nd: (optional) the geometry layer to which the current layer is attached
     * @example
     * // get all layers
     * view.getLayers();
     * // get all color layers
     * view.getLayers(layer => layer.isColorLayer);
     * // get all elevation layers
     * view.getLayers(layer => layer.isElevationLayer);
     * // get all geometry layers
     * view.getLayers(layer => layer.isGeometryLayer);
     * // get one layer with id
     * view.getLayers(layer => layer.id === 'itt');
     * @param {function(Layer):boolean} filter
     * @returns {Array<Layer>}
     */

  }, {
    key: "getLayers",
    value: function getLayers(filter) {
      var result = [];
      var _iteratorNormalCompletion2 = true;
      var _didIteratorError2 = false;
      var _iteratorError2 = undefined;

      try {
        for (var _iterator2 = this._layers[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
          var layer = _step2.value;

          if (!filter || filter(layer)) {
            result.push(layer);
          }

          var _iteratorNormalCompletion3 = true;
          var _didIteratorError3 = false;
          var _iteratorError3 = undefined;

          try {
            for (var _iterator3 = layer.attachedLayers[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
              var attached = _step3.value;

              if (!filter || filter(attached, layer)) {
                result.push(attached);
              }
            }
          } catch (err) {
            _didIteratorError3 = true;
            _iteratorError3 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) {
                _iterator3["return"]();
              }
            } finally {
              if (_didIteratorError3) {
                throw _iteratorError3;
              }
            }
          }
        }
      } catch (err) {
        _didIteratorError2 = true;
        _iteratorError2 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) {
            _iterator2["return"]();
          }
        } finally {
          if (_didIteratorError2) {
            throw _iteratorError2;
          }
        }
      }

      return result;
    }
    /**
     * Gets the layer by identifier.
     *
     * @param {String}  layerId  The layer identifier
     * @return {Layer}  The layer by identifier.
     */

  }, {
    key: "getLayerById",
    value: function getLayerById(layerId) {
      var layers = this.getLayers(function (l) {
        return l.id === layerId;
      });

      if (layers.length) {
        return layers[0];
      }
    }
    /**
     * @param {Layer} layer
     * @returns {GeometryLayer} the parent layer of the given layer or undefined.
     */

  }, {
    key: "getParentLayer",
    value: function getParentLayer(layer) {
      var _iteratorNormalCompletion4 = true;
      var _didIteratorError4 = false;
      var _iteratorError4 = undefined;

      try {
        for (var _iterator4 = this._layers[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
          var geometryLayer = _step4.value;
          var _iteratorNormalCompletion5 = true;
          var _didIteratorError5 = false;
          var _iteratorError5 = undefined;

          try {
            for (var _iterator5 = geometryLayer.attachedLayers[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
              var attached = _step5.value;

              if (attached === layer) {
                return geometryLayer;
              }
            }
          } catch (err) {
            _didIteratorError5 = true;
            _iteratorError5 = err;
          } finally {
            try {
              if (!_iteratorNormalCompletion5 && _iterator5["return"] != null) {
                _iterator5["return"]();
              }
            } finally {
              if (_didIteratorError5) {
                throw _iteratorError5;
              }
            }
          }
        }
      } catch (err) {
        _didIteratorError4 = true;
        _iteratorError4 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) {
            _iterator4["return"]();
          }
        } finally {
          if (_didIteratorError4) {
            throw _iteratorError4;
          }
        }
      }
    }
    /**
     * @name FrameRequester
     * @function
     *
     * @description
     * Method that will be called each time the `MainLoop` updates. This function
     * will be given as parameter the delta (in ms) between this update and the
     * previous one, and whether or not we just started to render again. This update
     * is considered as the "next" update if `view.notifyChange` was called during a
     * precedent update. If `view.notifyChange` has been called by something else
     * (other micro/macrotask, UI events etc...), then this update is considered as
     * being the "first". It can also receive optional arguments, depending on the
     * attach point of this function. Currently only `BEFORE_LAYER_UPDATE /
     * AFTER_LAYER_UPDATE` attach points provide an additional argument: the layer
     * being updated.
     * <br><br>
     *
     * This means that if a `frameRequester` function wants to animate something, it
     * should keep on calling `view.notifyChange` until its task is done.
     * <br><br>
     *
     * Implementors of `frameRequester` should keep in mind that this function will
     * be potentially called at each frame, thus care should be given about
     * performance.
     * <br><br>
     *
     * Typical frameRequesters are controls, module wanting to animate moves or UI
     * elements etc... Basically anything that would want to call
     * requestAnimationFrame.
     *
     * @param {number} dt
     * @param {boolean} updateLoopRestarted
     * @param {...*} args
     */

    /**
     * Add a frame requester to this view.
     *
     * FrameRequesters can activate the MainLoop update by calling view.notifyChange.
     *
     * @param {String} when - decide when the frameRequester should be called during
     * the update cycle. Can be any of {@link MAIN_LOOP_EVENTS}.
     * @param {FrameRequester} frameRequester - this function will be called at each
     * MainLoop update with the time delta between last update, or 0 if the MainLoop
     * has just been relaunched.
     */

  }, {
    key: "addFrameRequester",
    value: function addFrameRequester(when, frameRequester) {
      if (typeof frameRequester !== 'function') {
        throw new Error('frameRequester must be a function');
      }

      if (!this._frameRequesters[when]) {
        this._frameRequesters[when] = [frameRequester];
      } else {
        this._frameRequesters[when].push(frameRequester);
      }
    }
    /**
     * Remove a frameRequester.
     * The effective removal will happen either later; at worst it'll be at
     * the beginning of the next frame.
     *
     * @param {String} when - attach point of this requester. Can be any of
     * {@link MAIN_LOOP_EVENTS}.
     * @param {FrameRequester} frameRequester
     */

  }, {
    key: "removeFrameRequester",
    value: function removeFrameRequester(when, frameRequester) {
      if (this._frameRequesters[when].includes(frameRequester)) {
        this._delayedFrameRequesterRemoval.push({
          when: when,
          frameRequester: frameRequester
        });
      } else {
        console.error('Invalid call to removeFrameRequester: frameRequester isn\'t registered');
      }
    }
  }, {
    key: "_executeFrameRequestersRemovals",
    value: function _executeFrameRequestersRemovals() {
      var _iteratorNormalCompletion6 = true;
      var _didIteratorError6 = false;
      var _iteratorError6 = undefined;

      try {
        for (var _iterator6 = this._delayedFrameRequesterRemoval[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) {
          var toDelete = _step6.value;

          var index = this._frameRequesters[toDelete.when].indexOf(toDelete.frameRequester);

          if (index >= 0) {
            this._frameRequesters[toDelete.when].splice(index, 1);
          } else {
            console.warn('FrameReq has already been removed');
          }
        }
      } catch (err) {
        _didIteratorError6 = true;
        _iteratorError6 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion6 && _iterator6["return"] != null) {
            _iterator6["return"]();
          }
        } finally {
          if (_didIteratorError6) {
            throw _iteratorError6;
          }
        }
      }

      this._delayedFrameRequesterRemoval.length = 0;
    }
    /**
     * Execute a frameRequester.
     *
     * @param {String} when - attach point of this (these) requester(s). Can be any
     * of {@link MAIN_LOOP_EVENTS}.
     * @param {Number} dt - delta between this update and the previous one
     * @param {boolean} updateLoopRestarted
     * @param {...*} args - optional arguments
     */

  }, {
    key: "execFrameRequesters",
    value: function execFrameRequesters(when, dt, updateLoopRestarted) {
      if (!this._frameRequesters[when]) {
        return;
      }

      if (this._delayedFrameRequesterRemoval.length > 0) {
        this._executeFrameRequestersRemovals();
      }

      for (var _len = arguments.length, args = new Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) {
        args[_key - 3] = arguments[_key];
      }

      var _iteratorNormalCompletion7 = true;
      var _didIteratorError7 = false;
      var _iteratorError7 = undefined;

      try {
        for (var _iterator7 = this._frameRequesters[when][Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) {
          var frameRequester = _step7.value;

          if (frameRequester.update) {
            frameRequester.update(dt, updateLoopRestarted, args);
          } else {
            frameRequester(dt, updateLoopRestarted, args);
          }
        }
      } catch (err) {
        _didIteratorError7 = true;
        _iteratorError7 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion7 && _iterator7["return"] != null) {
            _iterator7["return"]();
          }
        } finally {
          if (_didIteratorError7) {
            throw _iteratorError7;
          }
        }
      }
    }
    /**
     * Extract view coordinates from a mouse-event / touch-event
     * @param {event} event - event can be a MouseEvent or a TouchEvent
     * @param {number} touchIdx - finger index when using a TouchEvent (default: 0)
     * @return {THREE.Vector2} - view coordinates (in pixels, 0-0 = top-left of the View)
     */

  }, {
    key: "eventToViewCoords",
    value: function eventToViewCoords(event) {
      var touchIdx = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

      if (event.touches === undefined || !event.touches.length) {
        return _eventCoords.set(event.offsetX, event.offsetY);
      } else {
        var br = this.mainLoop.gfxEngine.renderer.domElement.getBoundingClientRect();
        return _eventCoords.set(event.touches[touchIdx].clientX - br.x, event.touches[touchIdx].clientY - br.y);
      }
    }
    /**
     * Extract normalized coordinates (NDC) from a mouse-event / touch-event
     * @param {event} event - event can be a MouseEvent or a TouchEvent
     * @param {number} touchIdx - finger index when using a TouchEvent (default: 0)
     * @return {THREE.Vector2} - NDC coordinates (x and y are [-1, 1])
     */

  }, {
    key: "eventToNormalizedCoords",
    value: function eventToNormalizedCoords(event) {
      var touchIdx = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;
      return this.viewToNormalizedCoords(this.eventToViewCoords(event, touchIdx));
    }
    /**
     * Convert view coordinates to normalized coordinates (NDC)
     * @param {THREE.Vector2} viewCoords (in pixels, 0-0 = top-left of the View)
     * @return {THREE.Vector2} - NDC coordinates (x and y are [-1, 1])
     */

  }, {
    key: "viewToNormalizedCoords",
    value: function viewToNormalizedCoords(viewCoords) {
      _eventCoords.x = 2 * (viewCoords.x / this.camera.width) - 1;
      _eventCoords.y = -2 * (viewCoords.y / this.camera.height) + 1;
      return _eventCoords;
    }
    /**
     * Convert NDC coordinates to view coordinates
     * @param {THREE.Vector2} ndcCoords
     * @return {THREE.Vector2} - view coordinates (in pixels, 0-0 = top-left of the View)
     */

  }, {
    key: "normalizedToViewCoords",
    value: function normalizedToViewCoords(ndcCoords) {
      _eventCoords.x = (ndcCoords.x + 1) * 0.5 * this.camera.width;
      _eventCoords.y = (ndcCoords.y - 1) * -0.5 * this.camera.height;
      return _eventCoords;
    }
    /**
     * Return objects from some layers/objects3d under the mouse in this view.
     *
     * @param {Object} mouseOrEvt - mouse position in window coordinates (0, 0 = top-left)
     * or MouseEvent or TouchEvent
     * @param {number} radius - picking will happen in a circle centered on mouseOrEvt. Radius
     * is the radius of this circle, in pixels
     * @param {...*} where - where to look for objects. Can be either: empty (= look
     * in all layers with type == 'geometry'), layer ids or layers or a mix of all
     * the above.
     * @return {Array} - an array of objects. Each element contains at least an object
     * property which is the Object3D under the cursor. Then depending on the queried
     * layer/source, there may be additionnal properties (coming from THREE.Raycaster
     * for instance).
     *
     * @example
     * view.pickObjectsAt({ x, y })
     * view.pickObjectsAt({ x, y }, 1, 'wfsBuilding')
     * view.pickObjectsAt({ x, y }, 3, 'wfsBuilding', myLayer)
     */

  }, {
    key: "pickObjectsAt",
    value: function pickObjectsAt(mouseOrEvt, radius) {
      var results = [];

      for (var _len2 = arguments.length, where = new Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
        where[_key2 - 2] = arguments[_key2];
      }

      var sources = where.length == 0 ? this.getLayers(function (l) {
        return l.isGeometryLayer;
      }) : [].concat(where);
      var mouse = mouseOrEvt instanceof Event ? this.eventToViewCoords(mouseOrEvt) : mouseOrEvt;
      radius = radius || 0;
      var _iteratorNormalCompletion8 = true;
      var _didIteratorError8 = false;
      var _iteratorError8 = undefined;

      try {
        for (var _iterator8 = sources[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) {
          var source = _step8.value;

          if (source.isLayer || typeof source === 'string') {
            var layer = typeof source === 'string' ? this.getLayerById(source) : source;

            if (!layer || !layer.ready) {
              console.warn('view.pickObjectAt : layer is not ready : ', source);
              continue;
            }

            var parentLayer = this.getParentLayer(layer);

            if (!parentLayer) {
              var sp = layer.pickObjectsAt(this, mouse, radius); // warning: sp might be very large, so we can't use '...sp' (we'll hit
              // 'javascript maximum call stack size exceeded' error) nor
              // Array.prototype.push.apply(result, sp)

              for (var i = 0; i < sp.length; i++) {
                results.push(sp[i]);
              }
            } else {
              // raycast using parent layer object3d
              var obj = _Picking["default"].pickObjectsAt(this, mouse, radius, parentLayer.object3d); // then filter the results


              var _iteratorNormalCompletion9 = true;
              var _didIteratorError9 = false;
              var _iteratorError9 = undefined;

              try {
                for (var _iterator9 = obj[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) {
                  var o = _step9.value;

                  if (o.layer === layer) {
                    results.push(o);
                  }
                }
              } catch (err) {
                _didIteratorError9 = true;
                _iteratorError9 = err;
              } finally {
                try {
                  if (!_iteratorNormalCompletion9 && _iterator9["return"] != null) {
                    _iterator9["return"]();
                  }
                } finally {
                  if (_didIteratorError9) {
                    throw _iteratorError9;
                  }
                }
              }
            }
          } else if (source.isObject3D) {
            _Picking["default"].pickObjectsAt(this, mouse, radius, source, results);
          } else {
            throw new Error("Invalid where arg (value = ".concat(where, "). Expected layers, layer ids or Object3Ds"));
          }
        }
      } catch (err) {
        _didIteratorError8 = true;
        _iteratorError8 = err;
      } finally {
        try {
          if (!_iteratorNormalCompletion8 && _iterator8["return"] != null) {
            _iterator8["return"]();
          }
        } finally {
          if (_didIteratorError8) {
            throw _iteratorError8;
          }
        }
      }

      return results;
    }
  }, {
    key: "readDepthBuffer",
    value: function readDepthBuffer(x, y, width, height) {
      var g = this.mainLoop.gfxEngine;
      var currentWireframe = this.tileLayer.wireframe;
      var currentOpacity = this.tileLayer.opacity;
      var currentVisibility = this.tileLayer.visible;

      if (currentWireframe) {
        this.tileLayer.wireframe = false;
      }

      if (currentOpacity < 1.0) {
        this.tileLayer.opacity = 1.0;
      }

      if (!currentVisibility) {
        this.tileLayer.visible = true;
      }

      var restore = this.tileLayer.level0Nodes.map(function (n) {
        return _RenderMode["default"].push(n, _RenderMode["default"].MODES.DEPTH);
      });
      var buffer = g.renderViewToBuffer({
        camera: this.camera,
        scene: this.tileLayer.object3d
      }, {
        x: x,
        y: y,
        width: width,
        height: height
      });
      restore.forEach(function (r) {
        return r();
      });

      if (this.tileLayer.wireframe !== currentWireframe) {
        this.tileLayer.wireframe = currentWireframe;
      }

      if (this.tileLayer.opacity !== currentOpacity) {
        this.tileLayer.opacity = currentOpacity;
      }

      if (this.tileLayer.visible !== currentVisibility) {
        this.tileLayer.visible = currentVisibility;
      }

      return buffer;
    }
    /**
     * Returns the world position (view's crs: referenceCrs) under view coordinates.
     * This position is computed with depth buffer.
     *
     * @param      {THREE.Vector2}  mouse  position in view coordinates (in pixel), if it's null so it's view's center.
     * @param      {THREE.Vector3}  [target=THREE.Vector3()] target. the result will be copied into this Vector3. If not present a new one will be created.
     * @return     {THREE.Vector3}  the world position in view's crs: referenceCrs.
     */

  }, {
    key: "getPickingPositionFromDepth",
    value: function getPickingPositionFromDepth(mouse) {
      var target = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : new THREE.Vector3();

      if (!this.tileLayer || this.tileLayer.level0Nodes.length == 0 || !this.tileLayer.level0Nodes[0]) {
        target = undefined;
        return;
      }

      var l = this.mainLoop;

      var viewPaused = l.scheduler.commandsWaitingExecutionCount() == 0 && l.renderingState == _MainLoop.RENDERING_PAUSED;

      var g = l.gfxEngine;
      var dim = g.getWindowSize();
      var camera = this.camera.camera3D;
      mouse = mouse || dim.clone().multiplyScalar(0.5);
      mouse.x = Math.floor(mouse.x);
      mouse.y = Math.floor(mouse.y); // Prepare state

      var prev = camera.layers.mask;
      camera.layers.mask = 1 << this.tileLayer.threejsLayer; // Render/Read to buffer

      var buffer;

      if (viewPaused) {
        this._fullSizeDepthBuffer = this._fullSizeDepthBuffer || this.readDepthBuffer(0, 0, dim.x, dim.y);
        var id = ((dim.y - mouse.y - 1) * dim.x + mouse.x) * 4;
        buffer = this._fullSizeDepthBuffer.slice(id, id + 4);
      } else {
        buffer = this.readDepthBuffer(mouse.x, mouse.y, 1, 1);
      }

      screen.x = mouse.x / dim.x * 2 - 1;
      screen.y = -(mouse.y / dim.y) * 2 + 1; // Origin

      ray.origin.copy(camera.position); // Direction

      ray.direction.set(screen.x, screen.y, 0.5); // Unproject

      matrix.multiplyMatrices(camera.matrixWorld, matrix.getInverse(camera.projectionMatrix));
      ray.direction.applyMatrix4(matrix);
      ray.direction.sub(ray.origin);
      direction.set(0, 0, 1.0);
      direction.applyMatrix4(matrix);
      direction.sub(ray.origin);
      var angle = direction.angleTo(ray.direction);
      var orthoZ = g.depthBufferRGBAValueToOrthoZ(buffer, camera);
      var length = orthoZ / Math.cos(angle);
      target.addVectors(camera.position, ray.direction.setLength(length));
      camera.layers.mask = prev;

      if (target.length() > 10000000) {
        return undefined;
      }

      return target;
    }
  }]);
  return View;
}(THREE.EventDispatcher);

var _default = View;
exports["default"] = _default;