"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.$3dTilesCulling = $3dTilesCulling;
exports.$3dTilesSubdivisionControl = $3dTilesSubdivisionControl;
exports.computeNodeSSE = computeNodeSSE;
exports.init3dTilesLayer = init3dTilesLayer;
exports.pre3dTilesUpdate = pre3dTilesUpdate;
exports.process3dTilesNode = process3dTilesNode;
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var THREE = _interopRequireWildcard(require("three"));
var _Extent = _interopRequireDefault(require("../Core/Geographic/Extent"));
var _ObjectRemovalHelper = _interopRequireDefault(require("./ObjectRemovalHelper"));
var _C3DTilesLayer = require("../Layer/C3DTilesLayer");
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 _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"]; if (!it) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = it.call(o); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; }
function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
/** @module 3dTilesProcessing
*/

function requestNewTile(view, scheduler, geometryLayer, metadata, parent, redraw) {
  var command = {
    /* mandatory */
    view: view,
    requester: parent,
    layer: geometryLayer,
    priority: parent ? 1.0 / (parent.distance + 1) : 100,
    /* specific params */
    metadata: metadata,
    redraw: redraw
  };
  geometryLayer.dispatchEvent({
    type: _C3DTilesLayer.C3DTILES_LAYER_EVENTS.ON_TILE_REQUESTED,
    metadata: metadata
  });
  return scheduler.execute(command);
}
function getChildTiles(tile) {
  // only keep children that have the same layer and a valid tileId
  return tile.children.filter(function (n) {
    return n.layer == tile.layer && n.tileId;
  });
}
function subdivideNode(context, layer, node, cullingTest) {
  if (node.additiveRefinement) {
    // Additive refinement can only fetch visible children.
    _subdivideNodeAdditive(context, layer, node, cullingTest);
  } else {
    // Substractive refinement on the other hand requires to replace
    // node with all of its children
    _subdivideNodeSubstractive(context, layer, node);
  }
}
var tmpBox3 = new THREE.Box3();
var tmpSphere = new THREE.Sphere();
function boundingVolumeToExtent(crs, volume, transform) {
  if (volume.region) {
    var box = tmpBox3.copy(volume.region.box3D).applyMatrix4(volume.region.matrixWorld);
    return _Extent["default"].fromBox3(crs, box);
  } else if (volume.box) {
    var _box = tmpBox3.copy(volume.box).applyMatrix4(transform);
    return _Extent["default"].fromBox3(crs, _box);
  } else {
    var sphere = tmpSphere.copy(volume.sphere).applyMatrix4(transform);
    var _box2 = sphere.getBoundingBox(tmpBox3);
    return _Extent["default"].fromBox3(crs, _box2);
  }
}
var tmpMatrix = new THREE.Matrix4();
function _subdivideNodeAdditive(context, layer, node, cullingTest) {
  var _iterator = _createForOfIteratorHelper(layer.tileset.tiles[node.tileId].children),
    _step;
  try {
    var _loop = function _loop() {
      var child = _step.value;
      // child being downloaded => skip
      if (child.promise || child.loaded) {
        return "continue";
      }

      // 'child' is only metadata (it's *not* a THREE.Object3D). 'cullingTest' needs
      // a matrixWorld, so we compute it: it's node's matrixWorld x child's transform
      var overrideMatrixWorld = node.matrixWorld;
      if (child.transform) {
        overrideMatrixWorld = tmpMatrix.multiplyMatrices(node.matrixWorld, child.transform);
      }
      var isVisible = cullingTest ? !cullingTest(layer, context.camera, child, overrideMatrixWorld) : true;

      // child is not visible => skip
      if (!isVisible) {
        return "continue";
      }
      child.promise = requestNewTile(context.view, context.scheduler, layer, child, node, true).then(function (tile) {
        node.add(tile);
        tile.updateMatrixWorld();

        // The extent is calculated but it's never used in 3D tiles process
        var extent = boundingVolumeToExtent(layer.extent.crs, tile.boundingVolume, tile.matrixWorld);
        tile.traverse(function (obj) {
          obj.extent = extent;
        });
        layer.onTileContentLoaded(tile);
        context.view.notifyChange(child);
        child.loaded = true;
        delete child.promise;
      });
    };
    for (_iterator.s(); !(_step = _iterator.n()).done;) {
      var _ret = _loop();
      if (_ret === "continue") continue;
    }
  } catch (err) {
    _iterator.e(err);
  } finally {
    _iterator.f();
  }
}
function _subdivideNodeSubstractive(context, layer, node) {
  if (!node.pendingSubdivision && getChildTiles(node).length == 0) {
    var childrenTiles = layer.tileset.tiles[node.tileId].children;
    if (childrenTiles === undefined || childrenTiles.length === 0) {
      return;
    }
    node.pendingSubdivision = true;
    var promises = [];
    var _loop2 = function _loop2(i) {
      promises.push(requestNewTile(context.view, context.scheduler, layer, childrenTiles[i], node, false).then(function (tile) {
        childrenTiles[i].loaded = true;
        node.add(tile);
        tile.updateMatrixWorld();
        if (node.additiveRefinement) {
          context.view.notifyChange(node);
        }
        layer.tileset.tiles[tile.tileId].loaded = true;
        layer.onTileContentLoaded(tile);
      }));
    };
    for (var i = 0; i < childrenTiles.length; i++) {
      _loop2(i);
    }
    Promise.all(promises).then(function () {
      node.pendingSubdivision = false;
      context.view.notifyChange(node);
    });
  }
}

/**
 * Check if the node is visible in the camera.
 *
 * @param      {C3DTilesLayer} layer       node 3D tiles layer
 * @param      {Camera}   camera           camera
 * @param      {THREE.Object3D}   node             The 3d tile node to check.
 * @param      {THREE.Matrix4}   tileMatrixWorld  The node matrix world
 * @return     {boolean}  return true if the node is visible
 */
function $3dTilesCulling(layer, camera, node, tileMatrixWorld) {
  // For viewer Request Volume
  // https://github.com/AnalyticalGraphicsInc/3d-tiles-samples/tree/master/tilesets/TilesetWithRequestVolume
  if (node.viewerRequestVolume && node.viewerRequestVolume.viewerRequestVolumeCulling(camera, tileMatrixWorld)) {
    return true;
  }

  // For bounding volume
  if (node.boundingVolume && node.boundingVolume.boundingVolumeCulling(camera, tileMatrixWorld)) {
    return true;
  }
  return false;
}

// Cleanup all 3dtiles|three.js starting from a given node n.
// n's children can be of 2 types:
//   - have a 'content' attribute -> it's a tileset and must
//     be cleaned with cleanup3dTileset()
//   - doesn't have 'content' -> it's a raw Object3D object,
//     and must be cleaned with _cleanupObject3D()
function cleanup3dTileset(layer, n) {
  var depth = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0;
  // If this layer is not using additive refinement, we can only
  // clean a tile if all its neighbours are cleaned as well because
  // a tile can only be in 2 states:
  //   - displayed and no children displayed
  //   - hidden and all of its children displayed
  // So here we implement a conservative measure: if T is cleanable
  // we actually only clean its children tiles.
  var canCleanCompletely = n.additiveRefinement || depth > 0;
  for (var i = 0; i < n.children.length; i++) {
    // skip non-tiles elements
    if (!n.children[i].content) {
      if (canCleanCompletely) {
        _ObjectRemovalHelper["default"].removeChildrenAndCleanupRecursively(n.children[i].layer, n.children[i]);
      }
    } else {
      cleanup3dTileset(layer, n.children[i], depth + 1);
    }
  }
  if (canCleanCompletely) {
    if (n.dispose) {
      n.dispose();
    }
    delete n.content;
    layer.tileset.tiles[n.tileId].loaded = false;
    n.remove.apply(n, (0, _toConsumableArray2["default"])(n.children));

    // and finally remove from parent
    if (depth == 0 && n.parent) {
      n.parent.remove(n);
    }
  } else {
    var tiles = getChildTiles(n);
    n.remove.apply(n, (0, _toConsumableArray2["default"])(tiles));
  }
}

// this is a layer
function pre3dTilesUpdate() {
  if (!this.visible) {
    return [];
  }

  // Elements removed are added in the layer._cleanableTiles list.
  // Since we simply push in this array, the first item is always
  // the oldest one.
  var now = Date.now();
  if (this._cleanableTiles.length && now - this._cleanableTiles[0].cleanableSince > this.cleanupDelay) {
    // Make sure we don't clean root tile
    this.root.cleanableSince = undefined;
    var i = 0;
    for (; i < this._cleanableTiles.length; i++) {
      var elt = this._cleanableTiles[i];
      if (now - elt.cleanableSince > this.cleanupDelay) {
        cleanup3dTileset(this, elt);
      } else {
        // later entries are younger
        break;
      }
    }
    // remove deleted elements from _cleanableTiles
    this._cleanableTiles.splice(0, i);
  }
  return [this.root];
}
var boundingVolumeBox = new THREE.Box3();
var boundingVolumeSphere = new THREE.Sphere();
function computeNodeSSE(camera, node) {
  node.distance = 0;
  if (node.boundingVolume.region) {
    boundingVolumeBox.copy(node.boundingVolume.region.box3D);
    boundingVolumeBox.applyMatrix4(node.boundingVolume.region.matrixWorld);
    node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position);
  } else if (node.boundingVolume.box) {
    // boundingVolume.box is affected by matrixWorld
    boundingVolumeBox.copy(node.boundingVolume.box);
    boundingVolumeBox.applyMatrix4(node.matrixWorld);
    node.distance = boundingVolumeBox.distanceToPoint(camera.camera3D.position);
  } else if (node.boundingVolume.sphere) {
    // boundingVolume.sphere is affected by matrixWorld
    boundingVolumeSphere.copy(node.boundingVolume.sphere);
    boundingVolumeSphere.applyMatrix4(node.matrixWorld);
    // TODO: see https://github.com/iTowns/itowns/issues/800
    node.distance = Math.max(0.0, boundingVolumeSphere.distanceToPoint(camera.camera3D.position));
  } else {
    return Infinity;
  }
  if (node.distance === 0) {
    // This test is needed in case geometricError = distance = 0
    return Infinity;
  }
  return camera._preSSE * (node.geometricError / node.distance);
}
function init3dTilesLayer(view, scheduler, layer, rootTile) {
  return requestNewTile(view, scheduler, layer, rootTile, undefined, true).then(function (tile) {
    layer.object3d.add(tile);
    tile.updateMatrixWorld();
    layer.tileset.tiles[tile.tileId].loaded = true;
    layer.root = tile;
    // The extent is calculated but it's never used in 3D tiles process
    layer.extent = boundingVolumeToExtent(layer.crs || view.referenceCrs, tile.boundingVolume, tile.matrixWorld);
    layer.onTileContentLoaded(tile);
  });
}
function setDisplayed(node, display) {
  // The geometry of the tile is not in node, but in node.content
  // To change the display state, we change node.content.visible instead of
  // node.material.visible
  if (node.content) {
    node.content.visible = display;
  }
}
function markForDeletion(layer, elt) {
  if (!elt.cleanableSince) {
    elt.cleanableSince = Date.now();
    layer._cleanableTiles.push(elt);
  }
}

/**
 * This funcion builds the method to update 3d tiles node.
 *
 * The returned method checks the 3d tile visibility with `cullingTest` function.
 * It subdivises visible node if `subdivisionTest` return `true`.
 *
 * @param      {Function}  [cullingTest=$3dTilesCulling]                 The culling test method.
 * @param      {Function}  [subdivisionTest=$3dTilesSubdivisionControl]  The subdivision test method.
 * @return     {Function}    { description_of_the_return_value }
 */
function process3dTilesNode() {
  var cullingTest = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : $3dTilesCulling;
  var subdivisionTest = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : $3dTilesSubdivisionControl;
  return function _process3dTilesNodes(context, layer, node) {
    // early exit if parent's subdivision is in progress
    if (node.parent.pendingSubdivision && !node.parent.additiveRefinement) {
      node.visible = false;
      return undefined;
    }

    // do proper culling
    var isVisible = cullingTest ? !cullingTest(layer, context.camera, node, node.matrixWorld) : true;
    node.visible = isVisible;
    if (isVisible) {
      if (node.cleanableSince) {
        layer._cleanableTiles.splice(layer._cleanableTiles.indexOf(node), 1);
        node.cleanableSince = undefined;
      }
      var returnValue;
      if (node.pendingSubdivision || subdivisionTest(context, layer, node)) {
        subdivideNode(context, layer, node, cullingTest);
        // display iff children aren't ready
        setDisplayed(node, node.pendingSubdivision || node.additiveRefinement);
        returnValue = getChildTiles(node);
      } else {
        setDisplayed(node, true);
        var _iterator2 = _createForOfIteratorHelper(getChildTiles(node)),
          _step2;
        try {
          for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
            var n = _step2.value;
            n.visible = false;
            markForDeletion(layer, n);
          }
        } catch (err) {
          _iterator2.e(err);
        } finally {
          _iterator2.f();
        }
      }
      return returnValue;
    }
    markForDeletion(layer, node);
  };
}

/**
 *
 *
 * the method returns true if the `node` should be subivised.
 *
 * @param      {object}   context  The current context
 * @param      {Camera}   context.camera  The current camera
 * @param      {C3DTilesLayer}   layer  The 3d tile layer
 * @param      {THREE.Object3D}  node  The 3d tile node
 * @return     {boolean}
 */
function $3dTilesSubdivisionControl(context, layer, node) {
  if (layer.tileset.tiles[node.tileId].children === undefined) {
    return false;
  }
  if (layer.tileset.tiles[node.tileId].isTileset) {
    return true;
  }
  var sse = computeNodeSSE(context.camera, node);
  return sse > layer.sseThreshold;
}