@loaders.gl/i3s
Version:
i3s .
280 lines • 11.8 kB
JavaScript
import { load } from '@loaders.gl/core';
import { I3SNodePageLoader } from "../../i3s-node-page-loader.js";
import { normalizeTileNonUrlData } from "../parsers/parse-i3s.js";
import { getUrlWithToken, generateTilesetAttributeUrls } from "../utils/url-utils.js";
const BROWSER_PREFIXES = ['', 'WEBKIT_', 'MOZ_'];
const WEBGL_EXTENSIONS = {
/* eslint-disable camelcase */
WEBGL_compressed_texture_s3tc: 'dxt',
WEBGL_compressed_texture_s3tc_srgb: 'dxt-srgb',
WEBGL_compressed_texture_etc1: 'etc1',
WEBGL_compressed_texture_etc: 'etc2',
WEBGL_compressed_texture_pvrtc: 'pvrtc',
WEBGL_compressed_texture_atc: 'atc',
WEBGL_compressed_texture_astc: 'astc',
EXT_texture_compression_rgtc: 'rgtc'
/* eslint-enable camelcase */
};
/**
* class I3SNodePagesTiles - loads nodePages and form i3s tiles from them
*/
export default class I3SNodePagesTiles {
tileset;
nodePages = [];
pendingNodePages = [];
nodesPerPage;
options;
lodSelectionMetricType;
textureDefinitionsSelectedFormats = [];
nodesInNodePages;
url;
textureLoaderOptions = {};
/**
* @constructs
* Create a I3SNodePagesTiles instance.
* @param tileset - i3s tileset header ('layers/0')
* @param url - tileset url
* @param options - i3s loader options
*/
constructor(tileset, url = '', options) {
this.tileset = { ...tileset }; // spread the tileset to avoid circular reference
this.url = url;
this.nodesPerPage = tileset.nodePages?.nodesPerPage || 64;
this.lodSelectionMetricType = tileset.nodePages?.lodSelectionMetricType;
this.options = options;
this.nodesInNodePages = 0;
this.initSelectedFormatsForTextureDefinitions(tileset);
}
/**
* Loads some nodePage and return a particular node from it
* @param id - id of node through all node pages
*/
async getNodeById(id) {
const pageIndex = Math.floor(id / this.nodesPerPage);
if (!this.nodePages[pageIndex] && !this.pendingNodePages[pageIndex]) {
const nodePageUrl = getUrlWithToken(`${this.url}/nodepages/${pageIndex}`,
// @ts-expect-error this.options is not properly typed
this.options.i3s?.token);
this.pendingNodePages[pageIndex] = {
status: 'Pending',
promise: load(nodePageUrl, I3SNodePageLoader, this.options)
};
this.nodePages[pageIndex] = await this.pendingNodePages[pageIndex].promise;
this.nodesInNodePages += this.nodePages[pageIndex].nodes.length;
this.pendingNodePages[pageIndex].status = 'Done';
}
if (this.pendingNodePages[pageIndex].status === 'Pending') {
this.nodePages[pageIndex] = await this.pendingNodePages[pageIndex].promise;
}
const nodeIndex = id % this.nodesPerPage;
return this.nodePages[pageIndex].nodes[nodeIndex];
}
/**
* Forms tile header using node and tileset data
* @param id - id of node through all node pages
*/
// eslint-disable-next-line complexity, max-statements
async formTileFromNodePages(id) {
const node = await this.getNodeById(id);
const children = [];
const childNodesPromises = [];
for (const child of node.children || []) {
childNodesPromises.push(this.getNodeById(child));
}
const childNodes = await Promise.all(childNodesPromises);
for (const childNode of childNodes) {
children.push({
id: childNode.index.toString(),
obb: childNode.obb
});
}
let contentUrl;
let textureUrl;
let materialDefinition;
let textureFormat = 'jpg';
let attributeUrls = [];
let isDracoGeometry = false;
if (node && node.mesh) {
// Get geometry resource URL and type (compressed / non-compressed)
const { url, isDracoGeometry: isDracoGeometryResult } = (node.mesh.geometry &&
this.getContentUrl(node.mesh.geometry)) || { isDracoGeometry: false };
contentUrl = url;
isDracoGeometry = isDracoGeometryResult;
const { textureData, materialDefinition: nodeMaterialDefinition } = this.getInformationFromMaterial(node.mesh.material);
materialDefinition = nodeMaterialDefinition;
textureFormat = textureData.format || textureFormat;
if (textureData.name) {
textureUrl = `${this.url}/nodes/${node.mesh.material.resource}/textures/${textureData.name}`;
}
if (this.tileset.attributeStorageInfo) {
attributeUrls = generateTilesetAttributeUrls(this.tileset, this.url, node.mesh.attribute.resource);
}
}
const lodSelection = this.getLodSelection(node);
return normalizeTileNonUrlData({
id: id.toString(),
lodSelection,
obb: node.obb,
contentUrl,
textureUrl,
attributeUrls,
materialDefinition,
textureFormat,
textureLoaderOptions: this.textureLoaderOptions,
children,
isDracoGeometry
});
}
/**
* Forms url and type of geometry resource by nodepage's data and `geometryDefinitions` in the tileset
* @param - data about the node's mesh from the nodepage
* @returns -
* {string} url - url to the geometry resource
* {boolean} isDracoGeometry - whether the geometry resource contain DRACO compressed geometry
*/
getContentUrl(meshGeometryData) {
let result = null;
// @ts-ignore
const geometryDefinition = this.tileset.geometryDefinitions[meshGeometryData.definition];
let geometryIndex = -1;
// Try to find DRACO geometryDefinition of `useDracoGeometry` option is set
const i3sOptions = this.options.i3s;
if (i3sOptions && typeof i3sOptions === 'object' && i3sOptions.useDracoGeometry) {
geometryIndex = geometryDefinition.geometryBuffers.findIndex((buffer) => buffer.compressedAttributes && buffer.compressedAttributes.encoding === 'draco');
}
// If DRACO geometry is not applicable try to select non-compressed geometry
if (geometryIndex === -1) {
geometryIndex = geometryDefinition.geometryBuffers.findIndex((buffer) => !buffer.compressedAttributes);
}
if (geometryIndex !== -1) {
const isDracoGeometry = Boolean(geometryDefinition.geometryBuffers[geometryIndex].compressedAttributes);
result = {
url: `${this.url}/nodes/${meshGeometryData.resource}/geometries/${geometryIndex}`,
isDracoGeometry
};
}
return result;
}
/**
* Forms 1.6 compatible LOD selection object from a nodepage's node data
* @param node - a node from nodepage
* @returns- Array of LodSelection
*/
getLodSelection(node) {
const lodSelection = [];
if (this.lodSelectionMetricType === 'maxScreenThresholdSQ') {
lodSelection.push({
metricType: 'maxScreenThreshold',
// @ts-ignore
maxError: Math.sqrt(node.lodThreshold / (Math.PI * 0.25))
});
}
lodSelection.push({
metricType: this.lodSelectionMetricType,
// @ts-ignore
maxError: node.lodThreshold
});
return lodSelection;
}
/**
* Returns information about texture and material from `materialDefinitions`
* @param material - material data from nodepage
* @returns - Couple {textureData, materialDefinition}
* {string} textureData.name - path name of the texture
* {string} textureData.format - format of the texture
* materialDefinition - PBR-like material definition from `materialDefinitions`
*/
getInformationFromMaterial(material) {
const informationFromMaterial = { textureData: { name: null } };
if (material) {
const materialDefinition = this.tileset.materialDefinitions?.[material.definition];
if (materialDefinition) {
informationFromMaterial.materialDefinition = materialDefinition;
const textureSetDefinitionIndex = materialDefinition?.pbrMetallicRoughness?.baseColorTexture?.textureSetDefinitionId;
if (typeof textureSetDefinitionIndex === 'number') {
informationFromMaterial.textureData =
this.textureDefinitionsSelectedFormats[textureSetDefinitionIndex] ||
informationFromMaterial.textureData;
}
}
}
return informationFromMaterial;
}
/**
* Sets preferable and supported format for each textureDefinition of the tileset
* @param tileset - I3S layer data
* @returns
*/
initSelectedFormatsForTextureDefinitions(tileset) {
this.textureDefinitionsSelectedFormats = [];
const possibleI3sFormats = this.getSupportedTextureFormats();
const textureSetDefinitions = tileset.textureSetDefinitions || [];
for (const textureSetDefinition of textureSetDefinitions) {
const formats = (textureSetDefinition && textureSetDefinition.formats) || [];
let selectedFormat = null;
for (const i3sFormat of possibleI3sFormats) {
const format = formats.find((value) => value.format === i3sFormat);
if (format) {
selectedFormat = format;
break;
}
}
// For I3S 1.8 need to define basis target format to decode
if (selectedFormat && selectedFormat.format === 'ktx2') {
this.textureLoaderOptions.basis = {
containerFormat: 'ktx2',
module: 'encoder'
};
}
this.textureDefinitionsSelectedFormats.push(selectedFormat);
}
}
/**
* Returns the array of supported texture format
* @returns list of format strings
*/
getSupportedTextureFormats() {
const formats = [];
const i3sOptions = this.options.i3s;
if (!i3sOptions || i3sOptions.useCompressedTextures) {
// I3S 1.7 selection
const supportedCompressedFormats = getSupportedGPUTextureFormats();
// List of possible in i3s formats:
// https://github.com/Esri/i3s-spec/blob/master/docs/1.7/textureSetDefinitionFormat.cmn.md
if (supportedCompressedFormats.has('etc2')) {
formats.push('ktx-etc2');
}
if (supportedCompressedFormats.has('dxt')) {
formats.push('dds');
}
// I3S 1.8 selection
// ktx2 wraps basis texture which at the edge case can be decoded as uncompressed image
formats.push('ktx2');
}
formats.push('jpg');
formats.push('png');
return formats;
}
}
function getSupportedGPUTextureFormats(gl) {
const formats = new Set();
gl = gl || getWebGLContext() || undefined;
for (const prefix of BROWSER_PREFIXES) {
for (const extension in WEBGL_EXTENSIONS) {
if (gl && gl.getExtension(`${prefix}${extension}`)) {
formats.add(WEBGL_EXTENSIONS[extension]);
}
}
}
return formats;
}
function getWebGLContext() {
try {
const canvas = document.createElement('canvas');
return canvas.getContext('webgl');
}
catch {
return null;
}
}
//# sourceMappingURL=i3s-nodepages-tiles.js.map