UNPKG

@cesium/engine

Version:

CesiumJS is a JavaScript library for creating 3D globes and 2D maps in a web browser without a plugin.

928 lines (839 loc) 24.1 kB
import Cartographic from "../Core/Cartographic.js"; import Check from "../Core/Check.js"; import defined from "../Core/defined.js"; import Ellipsoid from "../Core/Ellipsoid.js"; import HeadingPitchRoll from "../Core/HeadingPitchRoll.js"; import Matrix3 from "../Core/Matrix3.js"; import Matrix4 from "../Core/Matrix4.js"; import Resource from "../Core/Resource.js"; import Quaternion from "../Core/Quaternion.js"; import Transforms from "../Core/Transforms.js"; import Cesium3DTile from "./Cesium3DTile.js"; import I3SDataProvider from "./I3SDataProvider.js"; import I3SDecoder from "./I3SDecoder.js"; import I3SFeature from "./I3SFeature.js"; import I3SField from "./I3SField.js"; import I3SGeometry from "./I3SGeometry.js"; /** * @typedef {object} I3SNode.AttributeFilter * * A filter given by an attribute name and values. * The 3D feature object should be hidden if its value for the attribute name is not specified in the collection of values. * * @property {string} name The name of the attribute * @property {string[]|number[]} values The collection of values */ /** * This class implements an I3S Node. In CesiumJS each I3SNode creates a Cesium3DTile. * <p> * Do not construct this directly, instead access tiles through {@link I3SLayer}. * </p> * @alias I3SNode * @internalConstructor */ function I3SNode(parent, ref, isRoot) { let level; let layer; let nodeIndex; let resource; if (isRoot) { level = 0; layer = parent; } else { level = parent._level + 1; layer = parent._layer; } if (typeof ref === "number") { nodeIndex = ref; } else { resource = parent.resource.getDerivedResource({ url: `${ref}/`, }); } this._parent = parent; this._dataProvider = parent._dataProvider; this._isRoot = isRoot; this._level = level; this._layer = layer; this._nodeIndex = nodeIndex; this._resource = resource; this._isLoading = false; this._tile = undefined; this._data = undefined; this._geometryData = []; this._featureData = []; this._fields = {}; this._children = []; this._childrenReadyPromise = undefined; this._globalTransform = undefined; this._inverseGlobalTransform = undefined; this._inverseRotationMatrix = undefined; this._symbologyData = undefined; } Object.defineProperties(I3SNode.prototype, { /** * Gets the resource for the node. * @memberof I3SNode.prototype * @type {Resource} * @readonly */ resource: { get: function () { return this._resource; }, }, /** * Gets the parent layer. * @memberof I3SNode.prototype * @type {I3SLayer} * @readonly */ layer: { get: function () { return this._layer; }, }, /** * Gets the parent node. * @memberof I3SNode.prototype * @type {I3SNode|undefined} * @readonly */ parent: { get: function () { return this._parent; }, }, /** * Gets the children nodes. * @memberof I3SNode.prototype * @type {I3SNode[]} * @readonly */ children: { get: function () { return this._children; }, }, /** * Gets the collection of geometries. * @memberof I3SNode.prototype * @type {I3SGeometry[]} * @readonly */ geometryData: { get: function () { return this._geometryData; }, }, /** * Gets the collection of features. * @memberof I3SNode.prototype * @type {I3SFeature[]} * @readonly */ featureData: { get: function () { return this._featureData; }, }, /** * Gets the collection of fields. * @memberof I3SNode.prototype * @type {I3SField[]} * @readonly */ fields: { get: function () { return this._fields; }, }, /** * Gets the Cesium3DTile for this node. * @memberof I3SNode.prototype * @type {Cesium3DTile} * @readonly */ tile: { get: function () { return this._tile; }, }, /** * Gets the I3S data for this object. * @memberof I3SNode.prototype * @type {object} * @readonly */ data: { get: function () { return this._data; }, }, }); /** * @private */ I3SNode.prototype.load = async function () { const that = this; function processData() { if (!that._isRoot) { // Create a new tile const tileDefinition = that._create3DTileDefinition(); that._tile = new Cesium3DTile( that._layer._tileset, that._dataProvider.resource, tileDefinition, that._parent._tile, ); that._tile._i3sNode = that; } } // If we don't have a nodepage index load from json if (!defined(this._nodeIndex)) { const data = await I3SDataProvider.loadJson(this._resource); that._data = data; processData(); return; } const node = await this._layer._getNodeInNodePages(this._nodeIndex); that._data = node; let uri; if (that._isRoot) { uri = "nodes/root/"; } else if (defined(node.mesh)) { const uriIndex = node.mesh.geometry.resource; uri = `../${uriIndex}/`; } if (defined(uri) && defined(that._parent.resource)) { that._resource = that._parent.resource.getDerivedResource({ url: uri }); } processData(); }; function createAndLoadField(node, storageInfo) { const newField = new I3SField(node, storageInfo); node._fields[storageInfo.name] = newField; return newField.load(); } /** * Loads the node fields. * @returns {Promise<void>} A promise that is resolved when the I3S Node fields are loaded */ I3SNode.prototype.loadFields = function () { // Check if we must load fields const fields = this._layer._data.attributeStorageInfo; const promises = []; if (defined(fields)) { for (let i = 0; i < fields.length; i++) { const storageInfo = fields[i]; const field = this._fields[storageInfo.name]; if (defined(field)) { promises.push(field.load()); } else { promises.push(createAndLoadField(this, storageInfo)); } } } return Promise.all(promises); }; /** * Loads the node field. * @param {string} name The field name * @returns {Promise<void>} A promise that is resolved when the I3S Node field is loaded */ I3SNode.prototype.loadField = function (name) { //>>includeStart('debug', pragmas.debug); Check.defined("name", name); //>>includeEnd('debug'); const field = this._fields[name]; if (defined(field)) { return field.load(); } const fields = this._layer._data.attributeStorageInfo; if (defined(fields)) { for (let i = 0; i < fields.length; i++) { const storageInfo = fields[i]; if (storageInfo.name === name) { return createAndLoadField(this, storageInfo); } } } return Promise.resolve(); }; /** * Returns the fields for a given picked position * @param {Cartesian3} pickedPosition The picked position * @returns {object} Object containing field names and their values */ I3SNode.prototype.getFieldsForPickedPosition = function (pickedPosition) { const geometry = this.geometryData[0]; if (!defined(geometry.customAttributes.featureIndex)) { return {}; } const location = geometry.getClosestPointIndexOnTriangle( pickedPosition.x, pickedPosition.y, pickedPosition.z, ); if ( location.index === -1 || location.index > geometry.customAttributes.featureIndex.length ) { return {}; } const featureIndex = geometry.customAttributes.featureIndex[location.index]; return this.getFieldsForFeature(featureIndex); }; /** * Returns the fields for a given feature * @param {number} featureIndex Index of the feature whose attributes we want to get * @returns {object} Object containing field names and their values */ I3SNode.prototype.getFieldsForFeature = function (featureIndex) { const featureFields = {}; for (const fieldName in this.fields) { if (this.fields.hasOwnProperty(fieldName)) { const field = this.fields[fieldName]; if (featureIndex >= 0 && featureIndex < field.values.length) { featureFields[field.name] = field.values[featureIndex]; } } } return featureFields; }; /** * @private */ I3SNode.prototype._loadChildren = function () { const that = this; // If the promise for loading the children was already created, just return it if (defined(this._childrenReadyPromise)) { return this._childrenReadyPromise; } const childPromises = []; if (defined(that._data.children)) { for ( let childIndex = 0; childIndex < that._data.children.length; childIndex++ ) { const child = that._data.children[childIndex]; const newChild = new I3SNode(that, child.href ?? child, false); that._children.push(newChild); childPromises.push(newChild.load()); } } this._childrenReadyPromise = Promise.all(childPromises).then(function () { for (let i = 0; i < that._children.length; i++) { that._tile.children.push(that._children[i]._tile); } }); return this._childrenReadyPromise; }; /** * @private */ I3SNode.prototype._loadGeometryData = function () { const geometryPromises = []; // To debug decoding for a specific tile, add a condition // that wraps this if/else to match the tile uri if (defined(this._data.geometryData)) { for ( let geomIndex = 0; geomIndex < this._data.geometryData.length; geomIndex++ ) { const curGeometryData = new I3SGeometry( this, this._data.geometryData[geomIndex].href, ); this._geometryData.push(curGeometryData); geometryPromises.push(curGeometryData.load()); } } else if (defined(this._data.mesh)) { const geometryDefinition = this._layer._findBestGeometryBuffers( this._data.mesh.geometry.definition, ["position", "uv0"], ); const geometryURI = `./geometries/${geometryDefinition.bufferIndex}/`; const newGeometryData = new I3SGeometry(this, geometryURI); newGeometryData._geometryDefinitions = geometryDefinition.definition; newGeometryData._geometryBufferInfo = geometryDefinition.geometryBufferInfo; this._geometryData.push(newGeometryData); geometryPromises.push(newGeometryData.load()); } return Promise.all(geometryPromises); }; /** * @private */ I3SNode.prototype._loadFeatureData = function () { const featurePromises = []; // To debug decoding for a specific tile, add a condition // that wraps this if/else to match the tile uri if (defined(this._data.featureData)) { for ( let featureIndex = 0; featureIndex < this._data.featureData.length; featureIndex++ ) { const newFeatureData = new I3SFeature( this, this._data.featureData[featureIndex].href, ); this._featureData.push(newFeatureData); featurePromises.push(newFeatureData.load()); } } return Promise.all(featurePromises); }; /** * @private */ I3SNode.prototype._clearGeometryData = function () { this._geometryData = []; }; /** * @private */ I3SNode.prototype._create3DTileDefinition = function () { const obb = this._data.obb; const mbs = this._data.mbs; if (!defined(obb) && !defined(mbs)) { console.error("Failed to load I3S node. Bounding volume is required."); return undefined; } let geoPosition; if (defined(obb)) { geoPosition = Cartographic.fromDegrees( obb.center[0], obb.center[1], obb.center[2], ); } else { geoPosition = Cartographic.fromDegrees(mbs[0], mbs[1], mbs[2]); } // Offset bounding box position if we have a geoid service defined if (defined(this._dataProvider._geoidDataList) && defined(geoPosition)) { for (let i = 0; i < this._dataProvider._geoidDataList.length; i++) { const tile = this._dataProvider._geoidDataList[i]; const projectedPos = tile.projection.project(geoPosition); if ( projectedPos.x > tile.nativeExtent.west && projectedPos.x < tile.nativeExtent.east && projectedPos.y > tile.nativeExtent.south && projectedPos.y < tile.nativeExtent.north ) { geoPosition.height += sampleGeoid(projectedPos.x, projectedPos.y, tile); break; } } } let boundingVolume = {}; let position; let span = 0; if (defined(obb)) { boundingVolume = { box: [ 0, 0, 0, obb.halfSize[0], 0, 0, 0, obb.halfSize[1], 0, 0, 0, obb.halfSize[2], ], }; span = Math.max( Math.max(this._data.obb.halfSize[0], this._data.obb.halfSize[1]), this._data.obb.halfSize[2], ); position = Ellipsoid.WGS84.cartographicToCartesian(geoPosition); } else { boundingVolume = { sphere: [0, 0, 0, mbs[3]], }; position = Ellipsoid.WGS84.cartographicToCartesian(geoPosition); span = this._data.mbs[3]; } span *= 2; // Compute the geometric error let metersPerPixel = Infinity; // Get the meters/pixel density required to pop the next LOD if (defined(this._data.lodThreshold)) { if ( this._layer._data.nodePages.lodSelectionMetricType === "maxScreenThresholdSQ" ) { const maxScreenThreshold = Math.sqrt( this._data.lodThreshold / (Math.PI * 0.25), ); metersPerPixel = span / maxScreenThreshold; } else if ( this._layer._data.nodePages.lodSelectionMetricType === "maxScreenThreshold" ) { const maxScreenThreshold = this._data.lodThreshold; metersPerPixel = span / maxScreenThreshold; } else { // Other LOD selection types can only be used for point cloud data console.error("Invalid lodSelectionMetricType in Layer"); } } else if (defined(this._data.lodSelection)) { for ( let lodIndex = 0; lodIndex < this._data.lodSelection.length; lodIndex++ ) { if ( this._data.lodSelection[lodIndex].metricType === "maxScreenThreshold" ) { metersPerPixel = span / this._data.lodSelection[lodIndex].maxError; } } } if (metersPerPixel === Infinity) { metersPerPixel = 100000; } // Calculate the length of 16 pixels in order to trigger the screen space error const geometricError = metersPerPixel * 16; // Transformations const hpr = new HeadingPitchRoll(0, 0, 0); let orientation = Transforms.headingPitchRollQuaternion(position, hpr); if (defined(this._data.obb)) { orientation = new Quaternion( this._data.obb.quaternion[0], this._data.obb.quaternion[1], this._data.obb.quaternion[2], this._data.obb.quaternion[3], ); } const rotationMatrix = Matrix3.fromQuaternion(orientation); const inverseRotationMatrix = Matrix3.inverse(rotationMatrix, new Matrix3()); const globalTransform = new Matrix4( rotationMatrix[0], rotationMatrix[1], rotationMatrix[2], 0, rotationMatrix[3], rotationMatrix[4], rotationMatrix[5], 0, rotationMatrix[6], rotationMatrix[7], rotationMatrix[8], 0, position.x, position.y, position.z, 1, ); const inverseGlobalTransform = Matrix4.inverse( globalTransform, new Matrix4(), ); const localTransform = Matrix4.clone(globalTransform); if (defined(this._parent._globalTransform)) { Matrix4.multiply( globalTransform, this._parent._inverseGlobalTransform, localTransform, ); } this._globalTransform = globalTransform; this._inverseGlobalTransform = inverseGlobalTransform; this._inverseRotationMatrix = inverseRotationMatrix; // get children definition const childrenDefinition = []; for (let childIndex = 0; childIndex < this._children.length; childIndex++) { childrenDefinition.push( this._children[childIndex]._create3DTileDefinition(), ); } // Create a tile set const inPlaceTileDefinition = { children: childrenDefinition, refine: "REPLACE", boundingVolume: boundingVolume, transform: [ localTransform[0], localTransform[4], localTransform[8], localTransform[12], localTransform[1], localTransform[5], localTransform[9], localTransform[13], localTransform[2], localTransform[6], localTransform[10], localTransform[14], localTransform[3], localTransform[7], localTransform[11], localTransform[15], ], content: { uri: defined(this._resource) ? this._resource.url : undefined, }, geometricError: geometricError, }; return inPlaceTileDefinition; }; /** * @private */ I3SNode.prototype._loadSymbology = async function () { if (!defined(this._symbologyData) && defined(this._layer._symbology)) { this._symbologyData = await this._layer._symbology._getSymbology(this); } }; /** * @private */ I3SNode.prototype._createContentURL = async function () { let rawGltf = { scene: 0, scenes: [ { nodes: [0], }, ], nodes: [ { name: "singleNode", }, ], meshes: [], buffers: [], bufferViews: [], accessors: [], materials: [], textures: [], images: [], samplers: [], asset: { version: "2.0", }, }; // Load the geometry data const dataPromises = [this._loadGeometryData()]; if (this._dataProvider.legacyVersion16) { dataPromises.push(this._loadFeatureData()); } await Promise.all(dataPromises); // Binary glTF if (defined(this._geometryData) && this._geometryData.length > 0) { if (this._dataProvider._applySymbology) { await this._loadSymbology(); } const url = this._geometryData[0].resource.url; const geometrySchema = this._layer._data.store.defaultGeometrySchema; const geometryData = this._geometryData[0]; const result = await I3SDecoder.decode( url, geometrySchema, geometryData, this._featureData[0], this._symbologyData, ); if (!defined(result)) { // Postponed return; } rawGltf = geometryData._generateGltf( result.meshData.nodesInScene, result.meshData.nodes, result.meshData.meshes, result.meshData.buffers, result.meshData.bufferViews, result.meshData.accessors, result.meshData.rootExtensions, result.meshData.extensionsUsed, ); this._geometryData[0]._customAttributes = result.meshData._customAttributes; } const binaryGltfData = this._dataProvider._binarizeGltf(rawGltf); const glbDataBlob = new Blob([binaryGltfData], { type: "application/binary", }); return URL.createObjectURL(glbDataBlob); }; async function loadFilters(node) { const filters = node._layer._filters; const promises = []; for (let i = 0; i < filters.length; i++) { const promise = node.loadField(filters[i].name); promises.push(promise); } await Promise.all(promises); return filters; } function checkFeatureValue(featureIndex, field, filter) { if (!defined(filter.values) || filter.values.length === 0) { return false; } const fieldValues = defined(field) ? field.values : []; let featureValue; if (featureIndex < fieldValues.length) { featureValue = fieldValues[featureIndex]; } let matches = false; for (let i = 0; i < filter.values.length; i++) { if (filter.values[i] === featureValue) { matches = true; break; } } return matches; } async function filterFeatures(node, contentModel) { const batchTable = node._tile.content.batchTable; if (defined(batchTable) && batchTable.featuresLength > 0) { batchTable.setAllShow(true); const filters = await loadFilters(node); if (filters.length > 0) { for ( let featureIndex = 0; featureIndex < batchTable.featuresLength; featureIndex++ ) { for (let filterIndex = 0; filterIndex < filters.length; filterIndex++) { const filter = filters[filterIndex]; if ( !checkFeatureValue(featureIndex, node._fields[filter.name], filter) ) { batchTable.setShow(featureIndex, false); break; } } } } } contentModel.show = true; } /** * @private */ I3SNode.prototype._filterFeatures = function () { const promises = []; // Forced filtering is required for loaded nodes only for (let i = 0; i < this._children.length; i++) { const promise = this._children[i]._filterFeatures(); promises.push(promise); } // Filters are applied for nodes with geometry data only const contentModel = this._tile?.content?._model; if ( defined(this._geometryData) && this._geometryData.length > 0 && defined(contentModel) && contentModel.ready ) { // The model needs to be hidden till the filters are applied contentModel.show = false; const promise = filterFeatures(this, contentModel); promises.push(promise); } return Promise.all(promises); }; // Reimplement Cesium3DTile.prototype.requestContent so that // We get a chance to load our own gltf from I3S data Cesium3DTile.prototype._hookedRequestContent = Cesium3DTile.prototype.requestContent; /** * Requests the tile's content. * <p> * The request may not be made if the Cesium Request Scheduler can't prioritize it. * </p> * * @return {Promise<Cesium3DTileContent>|undefined} A promise that resolves when the request completes, or undefined if there is no request needed, or the request cannot be scheduled. * @private */ Cesium3DTile.prototype.requestContent = function () { if (!this.tileset._isI3STileSet) { return this._hookedRequestContent(); } if (!this._isLoading) { this._isLoading = true; const that = this; return this._i3sNode ._createContentURL() .then((url) => { if (!defined(url)) { that._isLoading = false; return; } that._contentResource = new Resource({ url: url }); return that._hookedRequestContent(); }) .then((content) => { // Filters are applied for nodes with geometry data only const contentModel = content?._model; if ( defined(that._i3sNode._geometryData) && that._i3sNode._geometryData.length > 0 && defined(contentModel) ) { // The model needs to be hidden till the filters are applied contentModel.show = false; contentModel.readyEvent.addEventListener(() => { filterFeatures(that._i3sNode, contentModel); }); } that._isLoading = false; return content; }); } }; function bilinearInterpolate(tx, ty, h00, h10, h01, h11) { const a = h00 * (1 - tx) + h10 * tx; const b = h01 * (1 - tx) + h11 * tx; return a * (1 - ty) + b * ty; } function sampleMap(u, v, width, data) { const address = u + v * width; return data[address]; } function sampleGeoid(sampleX, sampleY, geoidData) { const extent = geoidData.nativeExtent; let x = ((sampleX - extent.west) / (extent.east - extent.west)) * (geoidData.width - 1); let y = ((sampleY - extent.south) / (extent.north - extent.south)) * (geoidData.height - 1); const xi = Math.floor(x); let yi = Math.floor(y); x -= xi; y -= yi; const xNext = xi < geoidData.width ? xi + 1 : xi; let yNext = yi < geoidData.height ? yi + 1 : yi; yi = geoidData.height - 1 - yi; yNext = geoidData.height - 1 - yNext; const h00 = sampleMap(xi, yi, geoidData.width, geoidData.buffer); const h10 = sampleMap(xNext, yi, geoidData.width, geoidData.buffer); const h01 = sampleMap(xi, yNext, geoidData.width, geoidData.buffer); const h11 = sampleMap(xNext, yNext, geoidData.width, geoidData.buffer); let finalHeight = bilinearInterpolate(x, y, h00, h10, h01, h11); finalHeight = finalHeight * geoidData.scale + geoidData.offset; return finalHeight; } Object.defineProperties(Cesium3DTile.prototype, { /** * Gets the I3S Node for the tile. * @memberof Cesium3DTile.prototype * @type {string} */ i3sNode: { get: function () { return this._i3sNode; }, }, }); export default I3SNode;