import Cartographic from "../Core/Cartographic.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import Ellipsoid from "../Core/Ellipsoid.js";
import HeadingPitchRoll from "../Core/HeadingPitchRoll.js";
import CesiumMath from "../Core/Math.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 I3SFeature from "./I3SFeature.js";
import I3SField from "./I3SField.js";
import I3SGeometry from "./I3SGeometry.js";
/**
* This class implements an I3S Node. In CesiumJS each I3SNode creates a Cesium3DTile.
*
* Do not construct this directly, instead access tiles through {@link I3SLayer}.
*
* @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;
}
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,
this._dataProvider._traceFetches
);
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)) {
that._resource = that._parent.resource.getDerivedResource({ url: uri });
}
processData();
};
/**
* Loads the node fields.
* @returns {Promise} 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 that = this;
function createAndLoadField(fields, index) {
const newField = new I3SField(that, fields[index]);
that._fields[newField._storageInfo.name] = newField;
return newField.load();
}
const promises = [];
if (defined(fields)) {
for (let i = 0; i < fields.length; i++) {
promises.push(createAndLoadField(fields, i));
}
}
return Promise.all(promises);
};
/**
* 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,
defaultValue(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._createI3SDecoderTask = async function (
decodeI3STaskProcessor,
data
) {
// Prepare the data to send to the worker
const parentData = data.geometryData._parent._data;
const parentRotationInverseMatrix =
data.geometryData._parent._inverseRotationMatrix;
let longitude = 0.0;
let latitude = 0.0;
let height = 0.0;
if (defined(parentData.obb)) {
longitude = parentData.obb.center[0];
latitude = parentData.obb.center[1];
height = parentData.obb.center[2];
} else if (defined(parentData.mbs)) {
longitude = parentData.mbs[0];
latitude = parentData.mbs[1];
height = parentData.mbs[2];
}
const axisFlipRotation = Matrix3.fromRotationX(-CesiumMath.PI_OVER_TWO);
const parentRotation = new Matrix3();
Matrix3.multiply(
axisFlipRotation,
parentRotationInverseMatrix,
parentRotation
);
const cartographicCenter = Cartographic.fromDegrees(
longitude,
latitude,
height
);
const cartesianCenter = Ellipsoid.WGS84.cartographicToCartesian(
cartographicCenter
);
const payload = {
binaryData: data.geometryData._data,
featureData:
defined(data.featureData) && defined(data.featureData[0])
? data.featureData[0].data
: undefined,
schema: data.defaultGeometrySchema,
bufferInfo: data.geometryData._geometryBufferInfo,
ellipsoidRadiiSquare: Ellipsoid.WGS84.radiiSquared,
url: data.url,
geoidDataList: data.geometryData._dataProvider._geoidDataList,
cartographicCenter: cartographicCenter,
cartesianCenter: cartesianCenter,
parentRotation: parentRotation,
};
const transferrableObjects = [];
return decodeI3STaskProcessor.scheduleTask(payload, transferrableObjects);
};
/**
* @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",
},
};
const decodeI3STaskProcessor = await this._dataProvider.getDecoderTaskProcessor();
// Load the geometry data
const dataPromises = [this._loadGeometryData()];
if (this._dataProvider.legacyVersion16) {
dataPromises.push(this._loadFeatureData());
}
const that = this;
return Promise.all(dataPromises).then(function () {
// Binary glTF
let generateGltfPromise = Promise.resolve();
if (defined(that._geometryData) && that._geometryData.length > 0) {
const parameters = {
geometryData: that._geometryData[0],
featureData: that._featureData,
defaultGeometrySchema: that._layer._data.store.defaultGeometrySchema,
url: that._geometryData[0].resource.url,
tile: that._tile,
};
const promise = that._createI3SDecoderTask(
decodeI3STaskProcessor,
parameters
);
if (!defined(promise)) {
// Postponed
return;
}
generateGltfPromise = promise.then(function (result) {
rawGltf = parameters.geometryData._generateGltf(
result.meshData.nodesInScene,
result.meshData.nodes,
result.meshData.meshes,
result.meshData.buffers,
result.meshData.bufferViews,
result.meshData.accessors
);
that._geometryData[0]._customAttributes =
result.meshData._customAttributes;
});
}
return generateGltfPromise.then(function () {
const binaryGltfData = that._dataProvider._binarizeGltf(rawGltf);
const glbDataBlob = new Blob([binaryGltfData], {
type: "application/binary",
});
return URL.createObjectURL(glbDataBlob);
});
});
};
// 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.
*
* The request may not be made if the Cesium Request Scheduler can't prioritize it.
*
*
* @return {Promise|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;
return this._i3sNode
._createContentURL()
.then((url) => {
if (!defined(url)) {
this._isLoading = false;
return;
}
this._contentResource = new Resource({ url: url });
return this._hookedRequestContent();
})
.then((content) => {
this._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;