itowns
Version:
A JS/WebGL framework for 3D geospatial data visualization
502 lines (404 loc) • 20.3 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _typeof = require("@babel/runtime/helpers/typeof");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
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 _GeometryLayer2 = _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 _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; } }
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; }
var point = new THREE.Vector3();
var bboxMesh = new THREE.Mesh();
var 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;
elt.obj.boxHelper.layers.mask = layer.bboxes.layers.mask;
layer.bboxes.add(elt.obj.boxHelper);
elt.obj.boxHelper.updateMatrix();
elt.obj.boxHelper.updateMatrixWorld();
}
function computeScreenSpaceError(context, pointSize, spacing, elt, distance) {
if (distance <= 0) {
return Infinity;
}
var pointSpacing = spacing / Math.pow(2, elt.depth); // Estimate the onscreen distance between 2 points
var 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 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;
}
var _iterator = _createForOfIteratorHelper(elt.children),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var child = _step.value;
markForDeletion(child);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
}
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=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.
*/
var PointCloudLayer = /*#__PURE__*/function (_GeometryLayer) {
(0, _inherits2["default"])(PointCloudLayer, _GeometryLayer);
var _super = _createSuper(PointCloudLayer);
/**
* 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.
*/
function PointCloudLayer(id) {
var _this;
var config = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
(0, _classCallCheck2["default"])(this, PointCloudLayer);
_this = _super.call(this, 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.MODE.COLOR;
return _this;
}
(0, _createClass2["default"])(PointCloudLayer, [{
key: "preUpdate",
value: function 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;
if (this.material.updateUniforms) {
this.material.updateUniforms();
}
} // lookup lowest common ancestor of changeSources
var commonAncestor;
var _iterator2 = _createForOfIteratorHelper(changeSources.values()),
_step2;
try {
for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
var source = _step2.value;
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];
}
}
}
}
} catch (err) {
_iterator2.e(err);
} finally {
_iterator2.f();
}
if (commonAncestor) {
return [commonAncestor];
} // Start updating from hierarchy root
return [this.root];
}
}, {
key: "update",
value: function update(context, layer, elt) {
var _this2 = this;
elt.visible = false;
if (this.octreeDepthLimit >= 0 && this.octreeDepthLimit < elt.depth) {
markForDeletion(elt);
return;
} // pick the best bounding box
var 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) {
var distance = Math.max(0.001, bbox.distanceToPoint(point)); // Increase priority of nearest node
var priority = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, distance) / distance;
elt.promise = context.scheduler.execute({
layer: layer,
requester: elt,
view: context.view,
priority: priority,
redraw: true,
earlyDropFunction: function earlyDropFunction(cmd) {
return !cmd.requester.visible || !_this2.visible;
}
}).then(function (pts) {
if (_this2.onPointsCreated) {
_this2.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
_this2.group.add(elt.obj);
elt.obj.updateMatrixWorld(true);
elt.promise = null;
}, function (err) {
if (err.isCancelledCommandException) {
elt.promise = null;
}
});
}
}
if (elt.children && elt.children.length) {
var _distance = bbox.distanceToPoint(point);
elt.sse = computeScreenSpaceError(context, layer.pointSize, layer.spacing, elt, _distance) / this.sseThreshold;
if (elt.sse >= 1) {
return elt.children;
} else {
var _iterator3 = _createForOfIteratorHelper(elt.children),
_step3;
try {
for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
var child = _step3.value;
markForDeletion(child);
}
} catch (err) {
_iterator3.e(err);
} finally {
_iterator3.f();
}
}
}
}
}, {
key: "postUpdate",
value: function postUpdate() {
this.displayedCount = 0;
var _iterator4 = _createForOfIteratorHelper(this.group.children),
_step4;
try {
for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
var _pts2 = _step4.value;
if (_pts2.visible) {
var _count2 = _pts2.geometry.attributes.position.count;
_pts2.geometry.setDrawRange(0, _count2);
this.displayedCount += _count2;
}
}
} catch (err) {
_iterator4.e(err);
} finally {
_iterator4.f();
}
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
var reduction = this.pointBudget / this.displayedCount;
var _iterator5 = _createForOfIteratorHelper(this.group.children),
_step5;
try {
for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
var pts = _step5.value;
if (pts.visible) {
var count = Math.floor(pts.geometry.drawRange.count * reduction);
if (count > 0) {
pts.geometry.setDrawRange(0, count);
} else {
pts.visible = false;
}
}
}
} catch (err) {
_iterator5.e(err);
} finally {
_iterator5.f();
}
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(function (p1, p2) {
return p2.userData.node.sse - p1.userData.node.sse;
});
var limitHit = false;
this.displayedCount = 0;
var _iterator6 = _createForOfIteratorHelper(this.group.children),
_step6;
try {
for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
var _pts = _step6.value;
var _count = _pts.geometry.attributes.position.count;
if (limitHit || this.displayedCount + _count > this.pointBudget) {
_pts.visible = false;
limitHit = true;
} else {
this.displayedCount += _count;
}
}
} catch (err) {
_iterator6.e(err);
} finally {
_iterator6.f();
}
}
}
var now = Date.now();
for (var i = this.group.children.length - 1; i >= 0; i--) {
var 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;
}
}
}
}, {
key: "pickObjectsAt",
value: function pickObjectsAt(view, mouse, radius) {
var target = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : [];
return _Picking["default"].pickPointsAt(view, mouse, radius, this, target);
}
}, {
key: "getObjectToUpdateForAttachedLayers",
value: function getObjectToUpdateForAttachedLayers(meta) {
if (meta.obj) {
var p = meta.parent;
if (p && p.obj) {
return {
element: meta.obj,
parent: p.obj
};
} else {
return {
element: meta.obj
};
}
}
}
}]);
return PointCloudLayer;
}(_GeometryLayer2["default"]);
var _default = PointCloudLayer;
exports["default"] = _default;