"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var THREE = _interopRequireWildcard(require("three"));
var _GeometryLayer = _interopRequireDefault(require("./GeometryLayer"));
var _PointsMaterial = _interopRequireWildcard(require("../Renderer/PointsMaterial"));
var _Picking = _interopRequireDefault(require("../Core/Picking"));
function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (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; }
const point = new THREE.Vector3();
const bboxMesh = new THREE.Mesh();
const box3 = new THREE.Box3();
bboxMesh.geometry.boundingBox = box3;
function initBoundingBox(elt, layer) {
  elt.tightbbox.getSize(box3.max);
  box3.max.multiplyScalar(0.5);
  box3.min.copy(box3.max).negate();
  elt.obj.boxHelper = new THREE.BoxHelper(bboxMesh);
  elt.obj.boxHelper.geometry = elt.obj.boxHelper.geometry.toNonIndexed();
  elt.obj.boxHelper.computeLineDistances();
  elt.obj.boxHelper.material = elt.childrenBitField ? new THREE.LineDashedMaterial({
    dashSize: 0.25,
    gapSize: 0.25
  }) : new THREE.LineBasicMaterial();
  elt.obj.boxHelper.material.color.setHex(0);
  elt.obj.boxHelper.material.linewidth = 2;
  elt.obj.boxHelper.frustumCulled = false;
  elt.obj.boxHelper.position.copy(elt.tightbbox.min).add(box3.max);
  elt.obj.boxHelper.autoUpdateMatrix = false;
  layer.bboxes.add(elt.obj.boxHelper);
  elt.obj.boxHelper.updateMatrix();
  elt.obj.boxHelper.updateMatrixWorld();
}
function computeSSEPerspective(context, pointSize, spacing, elt, distance) {
  if (distance <= 0) {
    return Infinity;
  }
  const pointSpacing = spacing / 2 ** elt.depth;
  // Estimate the onscreen distance between 2 points
  const onScreenSpacing = context.camera.preSSE * pointSpacing / distance;
  // [  P1  ]--------------[   P2   ]
  //     <--------------------->      = pointsSpacing (in world coordinates)
  //                                  ~ onScreenSpacing (in pixels)
  // <------>                         = pointSize (in pixels)
  return Math.max(0.0, onScreenSpacing - pointSize);
}
function computeSSEOrthographic(context, pointSize, spacing, elt) {
  const pointSpacing = spacing / 2 ** elt.depth;

  // Given an identity view matrix, project pointSpacing from world space to
  // clip space. v' = vVP = vP
  const v = new THREE.Vector4(pointSpacing);
  v.applyMatrix4(context.camera.camera3D.projectionMatrix);

  // We map v' to the screen space and calculate the distance to the origin.
  const dx = v.x * 0.5 * context.camera.width;
  const dy = v.y * 0.5 * context.camera.height;
  const distance = Math.sqrt(dx * dx + dy * dy);
  return Math.max(0.0, distance - pointSize);
}
function computeScreenSpaceError(context, pointSize, spacing, elt, distance) {
  if (context.camera.camera3D.isOrthographicCamera) {
    return computeSSEOrthographic(context, pointSize, spacing, elt);
  }
  return computeSSEPerspective(context, pointSize, spacing, elt, distance);
}
function markForDeletion(elt) {
  if (elt.obj) {
    elt.obj.visible = false;
  }
  if (!elt.notVisibleSince) {
    elt.notVisibleSince = Date.now();
    // Set .sse to an invalid value
    elt.sse = -1;
  }
  for (const child of elt.children) {
    markForDeletion(child);
  }
}
function changeIntensityRange(layer) {
  if (layer.material.intensityRange) {
    layer.material.intensityRange.set(layer.minIntensityRange, layer.maxIntensityRange);
  }
}

/**
 * The basis for all point clouds related layers.
 *
 * @property {boolean} isPointCloudLayer - Used to checkout whether this layer
 * is a PointCloudLayer. Default is `true`. You should not change this, as it is
 * used internally for optimisation.
 * @property {THREE.Group|THREE.Object3D} group - Contains the created
 * `THREE.Points` meshes, usually with an instance of a `THREE.Points` per node.
 * @property {THREE.Group|THREE.Object3D} bboxes - Contains the bounding boxes
 * (`THREE.Box3`) of the tree, usually one per node.
 * @property {number} octreeDepthLimit - The depth limit at which to stop
 * browsing the octree. Can be used to limit the browsing, without having to
 * edit manually the source of the point cloud. No limit by default (`-1`).
 * @property {number} [pointBudget=2000000] - Maximum number of points to
 * display at the same time. This influences the performance of rendering.
 * Default to two millions points.
 * @property {number} [sseThreshold=2] - Threshold of the **S**creen **S**pace
 * **E**rror. Default to `2`.
 * @property {number} [pointSize=4] - The size (in pixels) of the points.
 * Default to `4`.
 * @property {THREE.Material|PointsMaterial} [material=new PointsMaterial] - The
 * material to use to display the points of the cloud. Be default it is a new
 * `PointsMaterial`.
 * @property {number} [mode=PNTS_MODE.COLOR] - The displaying mode of the points.
 * Values are specified in `PointsMaterial`.
 * @property {number} [minIntensityRange=0] - The minimal intensity of the
 * layer. Changing this value will affect the material, if it has the
 * corresponding uniform. The value is normalized between 0 and 1.
 * @property {number} [maxIntensityRange=1] - The maximal intensity of the
 * layer. Changing this value will affect the material, if it has the
 * corresponding uniform. The value is normalized between 0 and 1.
 */
class PointCloudLayer extends _GeometryLayer.default {
  /**
   * Constructs a new instance of point cloud layer.
   * Constructs a new instance of a Point Cloud Layer. This should not be used
   * directly, but rather implemented using `extends`.
   *
   * @constructor
   * @extends GeometryLayer
   *
   * @param {string} id - The id of the layer, that should be unique. It is
   * not mandatory, but an error will be emitted if this layer is added a
   * {@link View} that already has a layer going by that id.
   * @param {Object} [config] - Optional configuration, all elements in it
   * will be merged as is in the layer. For example, if the configuration
   * contains three elements `name, protocol, extent`, these elements will be
   * available using `layer.name` or something else depending on the property
   * name. See the list of properties to know which one can be specified.
   */
  constructor(id) {
    let config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    super(id, new THREE.Group(), config);
    this.isPointCloudLayer = true;
    this.protocol = 'pointcloud';
    this.group = config.group || new THREE.Group();
    this.object3d.add(this.group);
    this.bboxes = config.bboxes || new THREE.Group();
    this.bboxes.visible = false;
    this.object3d.add(this.bboxes);
    this.group.updateMatrixWorld();

    // default config
    this.octreeDepthLimit = config.octreeDepthLimit || -1;
    this.pointBudget = config.pointBudget || 2000000;
    this.pointSize = config.pointSize === 0 || !isNaN(config.pointSize) ? config.pointSize : 4;
    this.sseThreshold = config.sseThreshold || 2;
    this.defineLayerProperty('minIntensityRange', config.minIntensityRange || 0, changeIntensityRange);
    this.defineLayerProperty('maxIntensityRange', config.maxIntensityRange || 1, changeIntensityRange);
    this.material = config.material || {};
    if (!this.material.isMaterial) {
      config.material = config.material || {};
      config.material.intensityRange = new THREE.Vector2(this.minIntensityRange, this.maxIntensityRange);
      this.material = new _PointsMaterial.default(config.material);
    }
    this.material.defines = this.material.defines || {};
    this.mode = config.mode || _PointsMaterial.PNTS_MODE.COLOR;
  }
  preUpdate(context, changeSources) {
    // See https://cesiumjs.org/hosted-apps/massiveworlds/downloads/Ring/WorldScaleTerrainRendering.pptx
    // slide 17
    context.camera.preSSE = context.camera.height / (2 * Math.tan(THREE.MathUtils.degToRad(context.camera.camera3D.fov) * 0.5));
    if (this.material) {
      this.material.visible = this.visible;
      this.material.opacity = this.opacity;
      this.material.transparent = this.opacity < 1;
      this.material.size = this.pointSize;
      this.material.scale = context.camera.preSSE;
      if (this.material.updateUniforms) {
        this.material.updateUniforms();
      }
    }

    // lookup lowest common ancestor of changeSources
    let commonAncestor;
    for (const source of changeSources.values()) {
      if (source.isCamera || source == this) {
        // if the change is caused by a camera move, no need to bother
        // to find common ancestor: we need to update the whole tree:
        // some invisible tiles may now be visible
        return [this.root];
      }
      if (source.obj === undefined) {
        continue;
      }
      // filter sources that belong to our layer
      if (source.obj.isPoints && source.obj.layer == this) {
        if (!commonAncestor) {
          commonAncestor = source;
        } else {
          commonAncestor = source.findCommonAncestor(commonAncestor);
          if (!commonAncestor) {
            return [this.root];
          }
        }
      }
    }
    if (commonAncestor) {
      return [commonAncestor];
    }

    // Start updating from hierarchy root
    return [this.root];
  }
  update(context, layer, elt) {
    elt.visible = false;
    if (this.octreeDepthLimit >= 0 && this.octreeDepthLimit < elt.depth) {
      markForDeletion(elt);
      return;
    }

    // pick the best bounding box
    const bbox = elt.tightbbox ? elt.tightbbox : elt.bbox;
    elt.visible = context.camera.isBox3Visible(bbox, this.object3d.matrixWorld);
    if (!elt.visible) {
      markForDeletion(elt);
      return;
    }
    elt.notVisibleSince = undefined;
    point.copy(context.camera.camera3D.position).sub(this.object3d.position);

    // only load geometry if this elements has points
    if (elt.numPoints !== 0) {
      if (elt.obj) {
        elt.obj.visible = true;
      } else if (!elt.promise) {
        const distance = Math.max(0.001, bbox.distanceToPoint(point));
        // Increase priority of nearest node
        const priority = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / distance;
        elt.promise = context.scheduler.execute({
          layer,
          requester: elt,
          view: context.view,
          priority,
          redraw: true,
          earlyDropFunction: cmd => !cmd.requester.visible || !this.visible
        }).then(pts => {
          if (this.onPointsCreated) {
            this.onPointsCreated(layer, pts);
          }
          elt.obj = pts;
          // store tightbbox to avoid ping-pong (bbox = larger => visible, tight => invisible)
          elt.tightbbox = pts.tightbbox;

          // make sure to add it here, otherwise it might never
          // be added nor cleaned
          this.group.add(elt.obj);
          elt.obj.updateMatrixWorld(true);
          elt.promise = null;
        }, err => {
          if (err.isCancelledCommandException) {
            elt.promise = null;
          }
        });
      }
    }
    if (elt.children && elt.children.length) {
      const distance = bbox.distanceToPoint(point);
      elt.sse = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / this.sseThreshold;
      if (elt.sse >= 1) {
        return elt.children;
      } else {
        for (const child of elt.children) {
          markForDeletion(child);
        }
      }
    }
  }
  postUpdate() {
    this.displayedCount = 0;
    for (const pts of this.group.children) {
      if (pts.visible) {
        const count = pts.geometry.attributes.position.count;
        pts.geometry.setDrawRange(0, count);
        this.displayedCount += count;
      }
    }
    if (this.displayedCount > this.pointBudget) {
      // 2 different point count limit implementation, depending on the potree source
      if (this.supportsProgressiveDisplay) {
        // In this format, points are evenly distributed within a node,
        // so we can draw a percentage of each node and still get a correct
        // representation
        const reduction = this.pointBudget / this.displayedCount;
        for (const pts of this.group.children) {
          if (pts.visible) {
            const count = Math.floor(pts.geometry.drawRange.count * reduction);
            if (count > 0) {
              pts.geometry.setDrawRange(0, count);
            } else {
              pts.visible = false;
            }
          }
        }
        this.displayedCount *= reduction;
      } else {
        // This format doesn't require points to be evenly distributed, so
        // we're going to sort the nodes by "importance" (= on screen size)
        // and display only the first N nodes
        this.group.children.sort((p1, p2) => p2.userData.node.sse - p1.userData.node.sse);
        let limitHit = false;
        this.displayedCount = 0;
        for (const pts of this.group.children) {
          const count = pts.geometry.attributes.position.count;
          if (limitHit || this.displayedCount + count > this.pointBudget) {
            pts.visible = false;
            limitHit = true;
          } else {
            this.displayedCount += count;
          }
        }
      }
    }
    const now = Date.now();
    for (let i = this.group.children.length - 1; i >= 0; i--) {
      const obj = this.group.children[i];
      if (!obj.visible && now - obj.userData.node.notVisibleSince > 10000) {
        // remove from group
        this.group.children.splice(i, 1);

        // no need to dispose obj.material, as it is shared by all objects of this layer
        obj.geometry.dispose();
        obj.material = null;
        obj.geometry = null;
        obj.userData.node.obj = null;
      }
    }
  }
  pickObjectsAt(view, mouse, radius) {
    let target = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
    return _Picking.default.pickPointsAt(view, mouse, radius, this, target);
  }
  getObjectToUpdateForAttachedLayers(meta) {
    if (meta.obj) {
      const p = meta.parent;
      if (p && p.obj) {
        return {
          element: meta.obj,
          parent: p.obj
        };
      } else {
        return {
          element: meta.obj
        };
      }
    }
  }
}
var _default = PointCloudLayer;
exports.default = _default;