UNPKG

itowns

Version:

A JS/WebGL framework for 3D geospatial data visualization

594 lines (488 loc) 24.3 kB
"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 _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray")); 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 _GeometryLayer2 = _interopRequireDefault(require("./GeometryLayer")); var _InfoLayer = require("./InfoLayer"); var _Picking = _interopRequireDefault(require("../Core/Picking")); var _convertToTile = _interopRequireDefault(require("../Converter/convertToTile")); var _ObjectRemovalHelper = _interopRequireDefault(require("../Process/ObjectRemovalHelper")); var _LayeredMaterialNodeProcessing = require("../Process/LayeredMaterialNodeProcessing"); var _Layer = require("./Layer"); var _Cache = require("../Core/Scheduler/Cache"); 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; } 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; } } var subdivisionVector = new THREE.Vector3(); var boundingSphereCenter = new THREE.Vector3(); /** * @property {InfoTiledGeometryLayer} info - Status information of layer * @property {boolean} isTiledGeometryLayer - Used to checkout whether this * layer is a TiledGeometryLayer. Default is true. You should not change this, * as it is used internally for optimisation. * */ var TiledGeometryLayer = /*#__PURE__*/function (_GeometryLayer) { (0, _inherits2["default"])(TiledGeometryLayer, _GeometryLayer); var _super = _createSuper(TiledGeometryLayer); /** * A layer extending the {@link GeometryLayer}, but with a tiling notion. * * `TiledGeometryLayer` is the ground where `ColorLayer` and `ElevationLayer` are attached. * `TiledGeometryLayer` is a quadtree data structure. At zoom 0, * there is a single tile for the whole earth. At zoom level 1, * the single tile splits into 4 tiles (2x2 tile square). * Each zoom level quadtree divides the geometry tiles of the one before it. * The camera distance determines how the tiles are subdivided for optimal data display. * * Some `GeometryLayer` can also be attached to the `TiledGeometryLayer` if they want to take advantage of the quadtree. * * ![tiled geometry](/docs/static/images/tiledGeometry.jpeg) * _In `GlobeView`, **red lines** represents the **WGS84 grid** and **orange lines** the **Pseudo-mercator grid**._ * _In this picture, there are tiles with 3 different zoom/levels._ * * The zoom/level is based on [tiled web map](https://en.wikipedia.org/wiki/Tiled_web_map). * It corresponds at meters by pixel. If the projection tile exceeds a certain pixel size (on screen) * then it is subdivided into 4 tiles with a zoom greater than 1. * * @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 {THREE.Object3d} object3d - The object3d used to contain the * geometry of the TiledGeometryLayer. It is usually a `THREE.Group`, but it * can be anything inheriting from a `THREE.Object3d`. * @param {Array} schemeTile - extents Array of root tiles * @param {Object} builder - builder geometry object * @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. * @param {Source} [config.source] - Description and options of the source. * * @throws {Error} `object3d` must be a valid `THREE.Object3d`. */ function TiledGeometryLayer(id, object3d, schemeTile, builder, config) { var _this; (0, _classCallCheck2["default"])(this, TiledGeometryLayer); // cacheLifeTime = CACHE_POLICIES.INFINITE because the cache is handled by the builder config.cacheLifeTime = _Cache.CACHE_POLICIES.INFINITE; config.source = false; _this = _super.call(this, id, object3d, config); _this.isTiledGeometryLayer = true; _this.protocol = 'tile'; _this.sseSubdivisionThreshold = _this.sseSubdivisionThreshold || 1.0; _this.schemeTile = schemeTile; _this.builder = builder; _this.info = new _InfoLayer.InfoTiledGeometryLayer((0, _assertThisInitialized2["default"])(_this)); if (!_this.schemeTile) { throw new Error("Cannot init tiled layer without schemeTile for layer ".concat(_this.id)); } if (!_this.builder) { throw new Error("Cannot init tiled layer without builder for layer ".concat(_this.id)); } _this.level0Nodes = []; var promises = []; var _iterator = _createForOfIteratorHelper(_this.schemeTile), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var root = _step.value; promises.push(_this.convert(undefined, root)); } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } _this._promises.push(Promise.all(promises).then(function (level0s) { var _this$object3d; _this.level0Nodes = level0s; (_this$object3d = _this.object3d).add.apply(_this$object3d, (0, _toConsumableArray2["default"])(level0s)); _this.object3d.updateMatrixWorld(); })); return _this; } /** * Picking method for this layer. It uses the {@link Picking#pickTilesAt} * method. * * @param {View} view - The view instance. * @param {Object} coordinates - The coordinates to pick in the view. It * should have at least `x` and `y` properties. * @param {number} radius - Radius of the picking circle. * @param {Array} target - Array to push picking result. * * @return {Array} An array containing all targets picked under the * specified coordinates. */ (0, _createClass2["default"])(TiledGeometryLayer, [{ key: "pickObjectsAt", value: function pickObjectsAt(view, coordinates) { var radius = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.options.defaultPickingRadius; var target = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : []; return _Picking["default"].pickTilesAt(view, coordinates, radius, this, target); } /** * Does pre-update work on the context: * <ul> * <li>update the `colorLayers` and `elevationLayers`</li> * <li>update the `maxElevationLevel`</li> * </ul> * * Once this work is done, it returns a list of nodes to update. Depending * on the origin of `sources`, it can return a few things: * <ul> * <li>if `sources` is empty, it returns the first node of the layer * (stored as `level0Nodes`), which will trigger the update of the whole * tree</li> * <li>if the update is triggered by a camera move, the whole tree is * returned too</li> * <li>if `source.layer` is this layer, it means that `source` is a node; a * common ancestor will be found if there are multiple sources, with the * default common ancestor being the first source itself</li> * <li>else it returns the whole tree</li> * </ul> * * @param {Object} context - The context of the update; see the {@link * MainLoop} for more informations. * @param {Set<GeometryLayer|TileMesh>} sources - A list of sources to * generate a list of nodes to update. * * @return {TileMesh[]} The array of nodes to update. */ }, { key: "preUpdate", value: function preUpdate(context, sources) { var _this2 = this; if (sources.has(undefined) || sources.size == 0) { return this.level0Nodes; } context.colorLayers = context.view.getLayers(function (l, a) { return a && a.id == _this2.id && l.isColorLayer; }); context.elevationLayers = context.view.getLayers(function (l, a) { return a && a.id == _this2.id && l.isElevationLayer; }); context.maxElevationLevel = -1; var _iterator2 = _createForOfIteratorHelper(context.elevationLayers), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var e = _step2.value; context.maxElevationLevel = Math.max(e.source.zoom.max, context.maxElevationLevel); } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } if (context.maxElevationLevel == -1) { context.maxElevationLevel = Infinity; } // Prepare ColorLayer sequence order // In this moment, there is only one color layers sequence, because they are attached to tileLayer. // In future, the sequence must be returned by parent geometry layer. this.colorLayersOrder = _Layer.ImageryLayers.getColorLayersIdOrderedBySequence(context.colorLayers); var commonAncestor; var _iterator3 = _createForOfIteratorHelper(sources.values()), _step3; try { for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { var source = _step3.value; if (source.isCamera) { // 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.level0Nodes; } if (source.layer === this) { if (!commonAncestor) { commonAncestor = source; } else { commonAncestor = source.findCommonAncestor(commonAncestor); if (!commonAncestor) { return this.level0Nodes; } } if (commonAncestor.material == null) { commonAncestor = undefined; } } } } catch (err) { _iterator3.e(err); } finally { _iterator3.f(); } if (commonAncestor) { return [commonAncestor]; } else { return this.level0Nodes; } } /** * Update a node of this layer. The node will not be updated if: * <ul> * <li>it does not have a parent, then it is removed</li> * <li>its parent is being subdivided</li> * <li>is not visible in the camera</li> * </ul> * * @param {Object} context - The context of the update; see the {@link * MainLoop} for more informations. * @param {Layer} layer - Parameter to be removed once all update methods * have been aligned. * @param {TileMesh} node - The node to update. * * @returns {Object} */ }, { key: "update", value: function update(context, layer, node) { var _this3 = this; if (!node.parent) { return _ObjectRemovalHelper["default"].removeChildrenAndCleanup(this, node); } // early exit if parent' subdivision is in progress if (node.parent.pendingSubdivision) { node.visible = false; node.material.visible = false; this.info.update(node); return undefined; } // do proper culling node.visible = !this.culling(node, context.camera); if (node.visible) { var requestChildrenUpdate = false; node.material.visible = true; this.info.update(node); if (node.pendingSubdivision || TiledGeometryLayer.hasEnoughTexturesToSubdivide(context, node) && this.subdivision(context, this, node)) { this.subdivideNode(context, node); // display iff children aren't ready node.material.visible = node.pendingSubdivision; this.info.update(node); requestChildrenUpdate = true; } if (node.material.visible) { if (!requestChildrenUpdate) { return _ObjectRemovalHelper["default"].removeChildren(this, node); } } return requestChildrenUpdate ? node.children.filter(function (n) { return n.layer == _this3; }) : undefined; } node.material.visible = false; this.info.update(node); return _ObjectRemovalHelper["default"].removeChildren(this, node); } }, { key: "convert", value: function convert(requester, extent) { return _convertToTile["default"].convert(requester, extent, this); } }, { key: "countColorLayersTextures", value: function countColorLayersTextures() { return arguments.length; } // eslint-disable-next-line }, { key: "culling", value: function culling(node, camera) { return !camera.isBox3Visible(node.obb.box3D, node.obb.matrixWorld); } /** * Tell if a node has enough elevation or color textures to subdivide. * Subdivision is prevented if: * <ul> * <li>the node is covered by at least one elevation layer and if the node * doesn't have an elevation texture yet</li> * <li>a color texture is missing</li> * </ul> * * @param {Object} context - The context of the update; see the {@link * MainLoop} for more informations. * @param {TileMesh} node - The node to subdivide. * * @returns {boolean} False if the node can not be subdivided, true * otherwise. */ }, { key: "subdivideNode", value: /** * Subdivides a node of this layer. If the node is currently in the process * of subdivision, it will not do anything here. The subdivision of a node * will occur in four part, to create a quadtree. The extent of the node * will be divided in four parts: north-west, north-east, south-west and * south-east. Once all four nodes are created, they will be added to the * current node and the view of the context will be notified of this change. * * @param {Object} context - The context of the update; see the {@link * MainLoop} for more informations. * @param {TileMesh} node - The node to subdivide. * @return {Promise} { description_of_the_return_value } */ function subdivideNode(context, node) { var _this4 = this; if (!node.pendingSubdivision && !node.children.some(function (n) { return n.layer == _this4; })) { var extents = node.extent.subdivision(); // TODO: pendingSubdivision mechanism is fragile, get rid of it node.pendingSubdivision = true; var command = { /* mandatory */ view: context.view, requester: node, layer: this, priority: 10000, /* specific params */ extentsSource: extents, redraw: false }; return context.scheduler.execute(command).then(function (children) { var _iterator4 = _createForOfIteratorHelper(children), _step4; try { for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) { var child = _step4.value; node.add(child); child.updateMatrixWorld(true); } } catch (err) { _iterator4.e(err); } finally { _iterator4.f(); } node.pendingSubdivision = false; context.view.notifyChange(node, false); }, function (err) { node.pendingSubdivision = false; if (!err.isCancelledCommandException) { throw new Error(err); } }); } } /** * Test the subdvision of a node, compared to this layer. * * @param {Object} context - The context of the update; see the {@link * MainLoop} for more informations. * @param {PlanarLayer} layer - This layer, parameter to be removed. * @param {TileMesh} node - The node to test. * * @return {boolean} - True if the node is subdivisable, otherwise false. */ }, { key: "subdivision", value: function subdivision(context, layer, node) { if (node.level < this.minSubdivisionLevel) { return true; } if (this.maxSubdivisionLevel <= node.level) { return false; } // Prevent to subdivise the node if the current elevation level // we must avoid a tile, with level 20, inherits a level 3 elevation texture. // The induced geometric error is much too large and distorts the SSE var nodeLayer = node.material.getElevationLayer(); if (nodeLayer) { var currentTexture = nodeLayer.textures[0]; if (currentTexture && currentTexture.extent) { var offsetScale = nodeLayer.offsetScales[0]; var ratio = offsetScale.z; // ratio is node size / texture size if (ratio < 1 / Math.pow(2, this.maxDeltaElevationLevel)) { return false; } } } subdivisionVector.setFromMatrixScale(node.matrixWorld); boundingSphereCenter.copy(node.boundingSphere.center).applyMatrix4(node.matrixWorld); var distance = Math.max(0.0, context.camera.camera3D.position.distanceTo(boundingSphereCenter) - node.boundingSphere.radius * subdivisionVector.x); // Size projection on pixel of bounding if (context.camera.camera3D.isOrthographicCamera) { var camera3D = context.camera.camera3D; var preSSE = context.camera._preSSE * 2 * camera3D.zoom / (camera3D.top - camera3D.bottom); node.screenSize = preSSE * node.boundingSphere.radius * subdivisionVector.x; } else { node.screenSize = context.camera._preSSE * (2 * node.boundingSphere.radius * subdivisionVector.x) / distance; } // The screen space error is calculated to have a correct texture display. // For the projection of a texture's texel to be less than or equal to one pixel var sse = node.screenSize / (_LayeredMaterialNodeProcessing.SIZE_DIAGONAL_TEXTURE * 2); return this.sseSubdivisionThreshold < sse; } }], [{ key: "hasEnoughTexturesToSubdivide", value: function hasEnoughTexturesToSubdivide(context, node) { var layerUpdateState = node.layerUpdateState || {}; var nodeLayer = node.material.getElevationLayer(); var _iterator5 = _createForOfIteratorHelper(context.elevationLayers), _step5; try { for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) { var e = _step5.value; var extents = node.getExtentsByProjection(e.crs); var zoom = extents[0].zoom; if (zoom > e.zoom.max || zoom < e.zoom.min) { continue; } if (!e.frozen && e.ready && e.source.extentInsideLimit(node.extent, zoom) && (!nodeLayer || nodeLayer.level < 0)) { // no stop subdivision in the case of a loading error if (layerUpdateState[e.id] && layerUpdateState[e.id].inError()) { continue; } return false; } } } catch (err) { _iterator5.e(err); } finally { _iterator5.f(); } var _iterator6 = _createForOfIteratorHelper(context.colorLayers), _step6; try { for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) { var c = _step6.value; if (c.frozen || !c.visible || !c.ready) { continue; } var _extents = node.getExtentsByProjection(c.crs); var _zoom = _extents[0].zoom; if (_zoom > c.zoom.max || _zoom < c.zoom.min) { continue; } // no stop subdivision in the case of a loading error if (layerUpdateState[c.id] && layerUpdateState[c.id].inError()) { continue; } nodeLayer = node.material.getLayer(c.id); if (c.source.extentInsideLimit(node.extent, _zoom) && (!nodeLayer || nodeLayer.level < 0)) { return false; } } } catch (err) { _iterator6.e(err); } finally { _iterator6.f(); } return true; } }]); return TiledGeometryLayer; }(_GeometryLayer2["default"]); var _default = TiledGeometryLayer; exports["default"] = _default;