361 lines
9.8 KiB
JavaScript
361 lines
9.8 KiB
JavaScript
import defined from "../Core/defined.js";
|
|
import RuntimeError from "../Core/RuntimeError.js";
|
|
|
|
/**
|
|
* This class implements an I3S Field which is custom data attached
|
|
* to nodes
|
|
* @alias I3SField
|
|
* @internalConstructor
|
|
* @privateParam {I3SNode} parent The parent of that geometry
|
|
* @privateParam {object} storageInfo The structure containing the storage info of the field
|
|
*/
|
|
function I3SField(parent, storageInfo) {
|
|
this._storageInfo = storageInfo;
|
|
this._parent = parent;
|
|
this._dataProvider = parent._dataProvider;
|
|
this._loadPromise = undefined;
|
|
const uri = `attributes/${storageInfo.key}/0`;
|
|
|
|
if (defined(this._parent._nodeIndex)) {
|
|
this._resource = this._parent._layer.resource.getDerivedResource({
|
|
url: `nodes/${this._parent._data.mesh.attribute.resource}/${uri}`,
|
|
});
|
|
} else {
|
|
this._resource = this._parent.resource.getDerivedResource({ url: uri });
|
|
}
|
|
}
|
|
|
|
Object.defineProperties(I3SField.prototype, {
|
|
/**
|
|
* Gets the resource for the fields
|
|
* @memberof I3SField.prototype
|
|
* @type {Resource}
|
|
* @readonly
|
|
*/
|
|
resource: {
|
|
get: function () {
|
|
return this._resource;
|
|
},
|
|
},
|
|
/**
|
|
* Gets the header for this field.
|
|
* @memberof I3SField.prototype
|
|
* @type {object}
|
|
* @readonly
|
|
*/
|
|
header: {
|
|
get: function () {
|
|
return this._header;
|
|
},
|
|
},
|
|
/**
|
|
* Gets the values for this field.
|
|
* @memberof I3SField.prototype
|
|
* @type {object}
|
|
* @readonly
|
|
*/
|
|
values: {
|
|
get: function () {
|
|
if (defined(this._values)) {
|
|
// attribute data can be stored either as values or as object identifiers
|
|
if (defined(this._values.attributeValues)) {
|
|
return this._values.attributeValues;
|
|
}
|
|
if (defined(this._values.objectIds)) {
|
|
return this._values.objectIds;
|
|
}
|
|
}
|
|
return [];
|
|
},
|
|
},
|
|
/**
|
|
* Gets the name for the field.
|
|
* @memberof I3SField.prototype
|
|
* @type {string}
|
|
* @readonly
|
|
*/
|
|
name: {
|
|
get: function () {
|
|
return this._storageInfo.name;
|
|
},
|
|
},
|
|
});
|
|
|
|
function getNumericTypeSize(type) {
|
|
if (type === "UInt8" || type === "Int8") {
|
|
return 1;
|
|
} else if (type === "UInt16" || type === "Int16") {
|
|
return 2;
|
|
} else if (
|
|
type === "UInt32" ||
|
|
type === "Int32" ||
|
|
type === "Oid32" ||
|
|
type === "Float32"
|
|
) {
|
|
return 4;
|
|
} else if (type === "UInt64" || type === "Int64" || type === "Float64") {
|
|
return 8;
|
|
}
|
|
|
|
// Not a numeric type
|
|
return 0;
|
|
}
|
|
|
|
function getValueTypeSize(type) {
|
|
if (type === "String") {
|
|
return 1;
|
|
}
|
|
return getNumericTypeSize(type);
|
|
}
|
|
|
|
async function load(field) {
|
|
const data = await field._dataProvider._loadBinary(field._resource);
|
|
const dataView = new DataView(data);
|
|
field._data = data;
|
|
field._validateHeader(dataView);
|
|
const headerSize = field._parseHeader(dataView);
|
|
const offset = field._getBodyOffset(headerSize);
|
|
field._validateBody(dataView, offset);
|
|
field._parseBody(dataView, offset);
|
|
}
|
|
|
|
/**
|
|
* Loads the content.
|
|
* @returns {Promise<void>} A promise that is resolved when the field data is loaded
|
|
*/
|
|
I3SField.prototype.load = function () {
|
|
if (defined(this._loadPromise)) {
|
|
return this._loadPromise;
|
|
}
|
|
|
|
this._loadPromise = load(this).catch(function (error) {
|
|
console.error(error);
|
|
});
|
|
return this._loadPromise;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
I3SField.prototype._parseValue = function (dataView, type, offset) {
|
|
let value;
|
|
if (type === "UInt8") {
|
|
value = dataView.getUint8(offset);
|
|
offset += 1;
|
|
} else if (type === "Int8") {
|
|
value = dataView.getInt8(offset);
|
|
offset += 1;
|
|
} else if (type === "UInt16") {
|
|
value = dataView.getUint16(offset, true);
|
|
offset += 2;
|
|
} else if (type === "Int16") {
|
|
value = dataView.getInt16(offset, true);
|
|
offset += 2;
|
|
} else if (type === "UInt32") {
|
|
value = dataView.getUint32(offset, true);
|
|
offset += 4;
|
|
} else if (type === "Oid32") {
|
|
value = dataView.getUint32(offset, true);
|
|
offset += 4;
|
|
} else if (type === "Int32") {
|
|
value = dataView.getInt32(offset, true);
|
|
offset += 4;
|
|
} else if (type === "UInt64") {
|
|
const left = dataView.getUint32(offset, true);
|
|
const right = dataView.getUint32(offset + 4, true);
|
|
value = left + Math.pow(2, 32) * right;
|
|
offset += 8;
|
|
} else if (type === "Int64") {
|
|
const left = dataView.getUint32(offset, true);
|
|
const right = dataView.getUint32(offset + 4, true);
|
|
if (right < Math.pow(2, 31)) {
|
|
// Positive number
|
|
value = left + Math.pow(2, 32) * right;
|
|
} else {
|
|
// Negative
|
|
value = left + Math.pow(2, 32) * (right - Math.pow(2, 32));
|
|
}
|
|
|
|
offset += 8;
|
|
} else if (type === "Float32") {
|
|
value = dataView.getFloat32(offset, true);
|
|
offset += 4;
|
|
} else if (type === "Float64") {
|
|
value = dataView.getFloat64(offset, true);
|
|
offset += 8;
|
|
} else if (type === "String") {
|
|
value = String.fromCharCode(dataView.getUint8(offset));
|
|
offset += 1;
|
|
}
|
|
|
|
return {
|
|
value: value,
|
|
offset: offset,
|
|
};
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
I3SField.prototype._parseHeader = function (dataView) {
|
|
let offset = 0;
|
|
this._header = {};
|
|
for (
|
|
let itemIndex = 0;
|
|
itemIndex < this._storageInfo.header.length;
|
|
itemIndex++
|
|
) {
|
|
const item = this._storageInfo.header[itemIndex];
|
|
const parsedValue = this._parseValue(dataView, item.valueType, offset);
|
|
this._header[item.property] = parsedValue.value;
|
|
offset = parsedValue.offset;
|
|
}
|
|
return offset;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
I3SField.prototype._parseBody = function (dataView, offset) {
|
|
this._values = {};
|
|
for (
|
|
let itemIndex = 0;
|
|
itemIndex < this._storageInfo.ordering.length;
|
|
itemIndex++
|
|
) {
|
|
const orderingValue = this._storageInfo.ordering[itemIndex];
|
|
// all strings in the ordering array correspond to the property name, except ObjectIds
|
|
const item = orderingValue === "ObjectIds" ? "objectIds" : orderingValue;
|
|
const desc = this._storageInfo[item];
|
|
if (defined(desc)) {
|
|
this._values[item] = [];
|
|
for (let index = 0; index < this._header.count; ++index) {
|
|
if (desc.valueType !== "String") {
|
|
const parsedValue = this._parseValue(
|
|
dataView,
|
|
desc.valueType,
|
|
offset
|
|
);
|
|
this._values[item].push(parsedValue.value);
|
|
offset = parsedValue.offset;
|
|
} else {
|
|
const stringLen = this._values.attributeByteCounts[index];
|
|
let stringContent = "";
|
|
for (let cIndex = 0; cIndex < stringLen; ++cIndex) {
|
|
const curParsedValue = this._parseValue(
|
|
dataView,
|
|
desc.valueType,
|
|
offset
|
|
);
|
|
if (curParsedValue.value.charCodeAt(0) !== 0) {
|
|
stringContent += curParsedValue.value;
|
|
}
|
|
offset = curParsedValue.offset;
|
|
}
|
|
// We skip the last character of the string since it's a null terminator
|
|
this._values[item].push(stringContent);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
I3SField.prototype._getBodyOffset = function (headerSize) {
|
|
let valueSize = 0;
|
|
if (defined(this._storageInfo.attributeValues)) {
|
|
valueSize = getNumericTypeSize(this._storageInfo.attributeValues.valueType);
|
|
} else if (defined(this._storageInfo.objectIds)) {
|
|
valueSize = getNumericTypeSize(this._storageInfo.objectIds.valueType);
|
|
}
|
|
if (valueSize > 0) {
|
|
// Values will be padded to align the addresses with the data size
|
|
return Math.ceil(headerSize / valueSize) * valueSize;
|
|
}
|
|
return headerSize;
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
I3SField.prototype._validateHeader = function (dataView) {
|
|
let headerSize = 0;
|
|
for (
|
|
let itemIndex = 0;
|
|
itemIndex < this._storageInfo.header.length;
|
|
itemIndex++
|
|
) {
|
|
const item = this._storageInfo.header[itemIndex];
|
|
headerSize += getValueTypeSize(item.valueType);
|
|
}
|
|
if (dataView.byteLength < headerSize) {
|
|
throw new RuntimeError(
|
|
`Invalid attribute buffer size (field: ${this.name}, header: ${headerSize}, actual: ${dataView.byteLength})`
|
|
);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
I3SField.prototype._validateBody = function (dataView, offset) {
|
|
if (!defined(this._header.count)) {
|
|
throw new RuntimeError(
|
|
`Invalid attribute buffer (field: ${this.name}, count is missing)`
|
|
);
|
|
}
|
|
let attributeByteCountsOffset;
|
|
for (
|
|
let itemIndex = 0;
|
|
itemIndex < this._storageInfo.ordering.length &&
|
|
offset < dataView.byteLength;
|
|
itemIndex++
|
|
) {
|
|
const orderingValue = this._storageInfo.ordering[itemIndex];
|
|
// all strings in the ordering array correspond to the property name, except ObjectIds
|
|
const item = orderingValue === "ObjectIds" ? "objectIds" : orderingValue;
|
|
const desc = this._storageInfo[item];
|
|
if (defined(desc)) {
|
|
if (desc.valueType !== "String") {
|
|
if (item === "attributeByteCounts") {
|
|
attributeByteCountsOffset = offset;
|
|
}
|
|
const valueSize = getNumericTypeSize(desc.valueType);
|
|
offset += valueSize * this._header.count;
|
|
} else {
|
|
if (!defined(attributeByteCountsOffset)) {
|
|
throw new RuntimeError(
|
|
`Invalid attribute buffer (field: ${this.name}, attributeByteCounts is missing)`
|
|
);
|
|
}
|
|
for (
|
|
let index = 0;
|
|
index < this._header.count && offset < dataView.byteLength;
|
|
++index
|
|
) {
|
|
const parsedValue = this._parseValue(
|
|
dataView,
|
|
this._storageInfo.attributeByteCounts.valueType,
|
|
attributeByteCountsOffset
|
|
);
|
|
offset += parsedValue.value;
|
|
attributeByteCountsOffset = parsedValue.offset;
|
|
}
|
|
}
|
|
} else {
|
|
throw new RuntimeError(
|
|
`Invalid attribute buffer (field: ${this.name}, ${item} is missing)`
|
|
);
|
|
}
|
|
}
|
|
if (dataView.byteLength < offset) {
|
|
throw new RuntimeError(
|
|
`Invalid attribute buffer size (field: ${this.name}, expected: ${offset}, actual: ${dataView.byteLength})`
|
|
);
|
|
}
|
|
};
|
|
|
|
export default I3SField;
|