UNPKG

@loaders.gl/tiles

Version:

Common components for different tiles loaders.

4 lines 182 kB
{ "version": 3, "sources": ["index.js", "tileset/tileset-3d.js", "utils/doubly-linked-list-node.js", "utils/doubly-linked-list.js", "tileset/tileset-cache.js", "tileset/helpers/transform-utils.js", "tileset/helpers/frame-state.js", "tileset/helpers/zoom.js", "tileset/tile-3d.js", "constants.js", "tileset/helpers/bounding-volume.js", "tileset/helpers/tiles-3d-lod.js", "tileset/helpers/i3s-lod.js", "tileset/helpers/3d-tiles-options.js", "utils/managed-array.js", "tileset/tileset-traverser.js", "tileset/format-3d-tiles/tileset-3d-traverser.js", "tileset/format-i3s/i3s-tileset-traverser.js", "tileset/format-i3s/i3s-pending-tiles-register.js", "tileset/format-i3s/i3s-tile-manager.js"], "sourcesContent": ["// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nexport { Tileset3D } from \"./tileset/tileset-3d.js\";\nexport { Tile3D } from \"./tileset/tile-3d.js\";\nexport { TilesetTraverser } from \"./tileset/tileset-traverser.js\";\nexport { TilesetCache } from \"./tileset/tileset-cache.js\";\nexport { createBoundingVolume } from \"./tileset/helpers/bounding-volume.js\";\nexport { calculateTransformProps } from \"./tileset/helpers/transform-utils.js\";\nexport { getFrameState } from \"./tileset/helpers/frame-state.js\";\nexport { getLodStatus } from \"./tileset/helpers/i3s-lod.js\";\nexport { TILE_CONTENT_STATE, TILE_REFINEMENT, TILE_TYPE, TILESET_TYPE, LOD_METRIC_TYPE } from \"./constants.js\";\n", "// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// This file is derived from the Cesium code base under Apache 2 license\n// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md\nimport { Matrix4, Vector3 } from '@math.gl/core';\nimport { Ellipsoid } from '@math.gl/geospatial';\nimport { Stats } from '@probe.gl/stats';\nimport { RequestScheduler, path } from '@loaders.gl/loader-utils';\nimport { TilesetCache } from \"./tileset-cache.js\";\nimport { calculateTransformProps } from \"./helpers/transform-utils.js\";\nimport { getFrameState, limitSelectedTiles } from \"./helpers/frame-state.js\";\nimport { getZoomFromBoundingVolume, getZoomFromExtent, getZoomFromFullExtent } from \"./helpers/zoom.js\";\nimport { Tile3D } from \"./tile-3d.js\";\nimport { TILESET_TYPE } from \"../constants.js\";\nimport { TilesetTraverser } from \"./tileset-traverser.js\";\n// TODO - these should be moved into their respective modules\nimport { Tileset3DTraverser } from \"./format-3d-tiles/tileset-3d-traverser.js\";\nimport { I3STilesetTraverser } from \"./format-i3s/i3s-tileset-traverser.js\";\nconst DEFAULT_PROPS = {\n description: '',\n ellipsoid: Ellipsoid.WGS84,\n modelMatrix: new Matrix4(),\n throttleRequests: true,\n maxRequests: 64,\n /** Default memory values optimized for viewing mesh-based 3D Tiles on both mobile and desktop devices */\n maximumMemoryUsage: 32,\n memoryCacheOverflow: 1,\n maximumTilesSelected: 0,\n debounceTime: 0,\n onTileLoad: () => { },\n onTileUnload: () => { },\n onTileError: () => { },\n onTraversalComplete: (selectedTiles) => selectedTiles,\n contentLoader: undefined,\n viewDistanceScale: 1.0,\n maximumScreenSpaceError: 8,\n memoryAdjustedScreenSpaceError: false,\n loadTiles: true,\n updateTransforms: true,\n viewportTraversersMap: null,\n loadOptions: { fetch: {} },\n attributions: [],\n basePath: '',\n i3s: {}\n};\n// Tracked Stats\nconst TILES_TOTAL = 'Tiles In Tileset(s)';\nconst TILES_IN_MEMORY = 'Tiles In Memory';\nconst TILES_IN_VIEW = 'Tiles In View';\nconst TILES_RENDERABLE = 'Tiles To Render';\nconst TILES_LOADED = 'Tiles Loaded';\nconst TILES_LOADING = 'Tiles Loading';\nconst TILES_UNLOADED = 'Tiles Unloaded';\nconst TILES_LOAD_FAILED = 'Failed Tile Loads';\nconst POINTS_COUNT = 'Points/Vertices';\nconst TILES_GPU_MEMORY = 'Tile Memory Use';\nconst MAXIMUM_SSE = 'Maximum Screen Space Error';\n/**\n * The Tileset loading and rendering flow is as below,\n * A rendered (i.e. deck.gl `Tile3DLayer`) triggers `tileset.update()` after a `tileset` is loaded\n * `tileset` starts traversing the tile tree and update `requestTiles` (tiles of which content need\n * to be fetched) and `selectedTiles` (tiles ready for rendering under the current viewport).\n * `Tile3DLayer` will update rendering based on `selectedTiles`.\n * `Tile3DLayer` also listens to `onTileLoad` callback and trigger another round of `update and then traversal`\n * when new tiles are loaded.\n\n * As I3S tileset have stored `tileHeader` file (metadata) and tile content files (geometry, texture, ...) separately.\n * During each traversal, it issues `tilHeader` requests if that `tileHeader` is not yet fetched,\n * after the tile header is fulfilled, it will resume the traversal starting from the tile just fetched (not root).\n\n * Tile3DLayer\n * |\n * await load(tileset)\n * |\n * tileset.update()\n * | async load tileHeader\n * tileset.traverse() -------------------------- Queued\n * | resume traversal after fetched |\n * |----------------------------------------|\n * |\n * | async load tile content\n * tilset.requestedTiles ----------------------------- RequestScheduler\n * |\n * tilset.selectedTiles (ready for rendering) |\n * | Listen to |\n * Tile3DLayer ----------- onTileLoad ----------------------|\n * | | notify new tile is available\n * updateLayers |\n * tileset.update // trigger another round of update\n*/\nexport class Tileset3D {\n // props: Tileset3DProps;\n options;\n loadOptions;\n type;\n tileset;\n loader;\n url;\n basePath;\n modelMatrix;\n ellipsoid;\n lodMetricType;\n lodMetricValue;\n refine;\n root = null;\n roots = {};\n /** @todo any->unknown */\n asset = {};\n // Metadata for the entire tileset\n description = '';\n properties;\n extras = null;\n attributions = {};\n credits = {};\n stats;\n /** flags that contain information about data types in nested tiles */\n contentFormats = { draco: false, meshopt: false, dds: false, ktx2: false };\n // view props\n cartographicCenter = null;\n cartesianCenter = null;\n zoom = 1;\n boundingVolume = null;\n /** Updated based on the camera position and direction */\n dynamicScreenSpaceErrorComputedDensity = 0.0;\n // METRICS\n /**\n * The maximum amount of GPU memory (in MB) that may be used to cache tiles\n * Tiles not in view are unloaded to enforce private\n */\n maximumMemoryUsage = 32;\n /** The total amount of GPU memory in bytes used by the tileset. */\n gpuMemoryUsageInBytes = 0;\n /**\n * If loading the level of detail required by maximumScreenSpaceError\n * results in the memory usage exceeding maximumMemoryUsage (GPU), level of detail refinement\n * will instead use this (larger) adjusted screen space error to achieve the\n * best possible visual quality within the available memory.\n */\n memoryAdjustedScreenSpaceError = 0.0;\n _cacheBytes = 0;\n _cacheOverflowBytes = 0;\n /** Update tracker. increase in each update cycle. */\n _frameNumber = 0;\n _queryParams = {};\n _extensionsUsed = [];\n _tiles = {};\n /** counter for tracking tiles requests */\n _pendingCount = 0;\n /** Hold traversal results */\n selectedTiles = [];\n // TRAVERSAL\n traverseCounter = 0;\n geometricError = 0;\n lastUpdatedVieports = null;\n _requestedTiles = [];\n _emptyTiles = [];\n frameStateData = {};\n _traverser;\n _cache = new TilesetCache();\n _requestScheduler;\n // Promise tracking\n updatePromise = null;\n tilesetInitializationPromise;\n /**\n * Create a new Tileset3D\n * @param json\n * @param props\n */\n // eslint-disable-next-line max-statements\n constructor(tileset, options) {\n // PUBLIC MEMBERS\n this.options = { ...DEFAULT_PROPS, ...options };\n // raw data\n this.tileset = tileset;\n this.loader = tileset.loader;\n // could be 3d tiles, i3s\n this.type = tileset.type;\n // The url to a tileset JSON file.\n this.url = tileset.url;\n this.basePath = tileset.basePath || path.dirname(this.url);\n this.modelMatrix = this.options.modelMatrix;\n this.ellipsoid = this.options.ellipsoid;\n // Geometric error when the tree is not rendered at all\n this.lodMetricType = tileset.lodMetricType;\n this.lodMetricValue = tileset.lodMetricValue;\n this.refine = tileset.root.refine;\n this.loadOptions = this.options.loadOptions || {};\n // TRAVERSAL\n this._traverser = this._initializeTraverser();\n this._requestScheduler = new RequestScheduler({\n throttleRequests: this.options.throttleRequests,\n maxRequests: this.options.maxRequests\n });\n this.memoryAdjustedScreenSpaceError = this.options.maximumScreenSpaceError;\n this._cacheBytes = this.options.maximumMemoryUsage * 1024 * 1024;\n this._cacheOverflowBytes = this.options.memoryCacheOverflow * 1024 * 1024;\n // METRICS\n // The total amount of GPU memory in bytes used by the tileset.\n this.stats = new Stats({ id: this.url });\n this._initializeStats();\n this.tilesetInitializationPromise = this._initializeTileSet(tileset);\n }\n /** Release resources */\n destroy() {\n this._destroy();\n }\n /** Is the tileset loaded (update needs to have been called at least once) */\n isLoaded() {\n // Check that `_frameNumber !== 0` which means that update was called at least once\n return this._pendingCount === 0 && this._frameNumber !== 0 && this._requestedTiles.length === 0;\n }\n get tiles() {\n return Object.values(this._tiles);\n }\n get frameNumber() {\n return this._frameNumber;\n }\n get queryParams() {\n return new URLSearchParams(this._queryParams).toString();\n }\n setProps(props) {\n this.options = { ...this.options, ...props };\n }\n /** @deprecated */\n // setOptions(options: Tileset3DProps): void {\n // this.options = {...this.options, ...options};\n // }\n /**\n * Return a loadable tile url for a specific tile subpath\n * @param tilePath a tile subpath\n */\n getTileUrl(tilePath) {\n const isDataUrl = tilePath.startsWith('data:');\n if (isDataUrl) {\n return tilePath;\n }\n let tileUrl = tilePath;\n if (this.queryParams.length) {\n tileUrl = `${tilePath}${tilePath.includes('?') ? '&' : '?'}${this.queryParams}`;\n }\n return tileUrl;\n }\n // TODO CESIUM specific\n hasExtension(extensionName) {\n return Boolean(this._extensionsUsed.indexOf(extensionName) > -1);\n }\n /**\n * Update visible tiles relying on a list of viewports\n * @param viewports - list of viewports\n * @deprecated\n */\n update(viewports = null) {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this.tilesetInitializationPromise.then(() => {\n if (!viewports && this.lastUpdatedVieports) {\n viewports = this.lastUpdatedVieports;\n }\n else {\n this.lastUpdatedVieports = viewports;\n }\n if (viewports) {\n this.doUpdate(viewports);\n }\n });\n }\n /**\n * Update visible tiles relying on a list of viewports.\n * Do it with debounce delay to prevent update spam\n * @param viewports viewports\n * @returns Promise of new frameNumber\n */\n async selectTiles(viewports = null) {\n await this.tilesetInitializationPromise;\n if (viewports) {\n this.lastUpdatedVieports = viewports;\n }\n if (!this.updatePromise) {\n this.updatePromise = new Promise((resolve) => {\n setTimeout(() => {\n if (this.lastUpdatedVieports) {\n this.doUpdate(this.lastUpdatedVieports);\n }\n resolve(this._frameNumber);\n this.updatePromise = null;\n }, this.options.debounceTime);\n });\n }\n return this.updatePromise;\n }\n adjustScreenSpaceError() {\n if (this.gpuMemoryUsageInBytes < this._cacheBytes) {\n this.memoryAdjustedScreenSpaceError = Math.max(this.memoryAdjustedScreenSpaceError / 1.02, this.options.maximumScreenSpaceError);\n }\n else if (this.gpuMemoryUsageInBytes > this._cacheBytes + this._cacheOverflowBytes) {\n this.memoryAdjustedScreenSpaceError *= 1.02;\n }\n }\n /**\n * Update visible tiles relying on a list of viewports\n * @param viewports viewports\n */\n // eslint-disable-next-line max-statements, complexity\n doUpdate(viewports) {\n if ('loadTiles' in this.options && !this.options.loadTiles) {\n return;\n }\n if (this.traverseCounter > 0) {\n return;\n }\n const preparedViewports = viewports instanceof Array ? viewports : [viewports];\n this._cache.reset();\n this._frameNumber++;\n this.traverseCounter = preparedViewports.length;\n const viewportsToTraverse = [];\n // First loop to decrement traverseCounter\n for (const viewport of preparedViewports) {\n const id = viewport.id;\n if (this._needTraverse(id)) {\n viewportsToTraverse.push(id);\n }\n else {\n this.traverseCounter--;\n }\n }\n // Second loop to traverse\n for (const viewport of preparedViewports) {\n const id = viewport.id;\n if (!this.roots[id]) {\n this.roots[id] = this._initializeTileHeaders(this.tileset, null);\n }\n if (!viewportsToTraverse.includes(id)) {\n continue; // eslint-disable-line no-continue\n }\n const frameState = getFrameState(viewport, this._frameNumber);\n this._traverser.traverse(this.roots[id], frameState, this.options);\n }\n }\n /**\n * Check if traversal is needed for particular viewport\n * @param {string} viewportId - id of a viewport\n * @return {boolean}\n */\n _needTraverse(viewportId) {\n let traverserId = viewportId;\n if (this.options.viewportTraversersMap) {\n traverserId = this.options.viewportTraversersMap[viewportId];\n }\n if (traverserId !== viewportId) {\n return false;\n }\n return true;\n }\n /**\n * The callback to post-process tiles after traversal procedure\n * @param frameState - frame state for tile culling\n */\n _onTraversalEnd(frameState) {\n const id = frameState.viewport.id;\n if (!this.frameStateData[id]) {\n this.frameStateData[id] = { selectedTiles: [], _requestedTiles: [], _emptyTiles: [] };\n }\n const currentFrameStateData = this.frameStateData[id];\n const selectedTiles = Object.values(this._traverser.selectedTiles);\n const [filteredSelectedTiles, unselectedTiles] = limitSelectedTiles(selectedTiles, frameState, this.options.maximumTilesSelected);\n currentFrameStateData.selectedTiles = filteredSelectedTiles;\n for (const tile of unselectedTiles) {\n tile.unselect();\n }\n currentFrameStateData._requestedTiles = Object.values(this._traverser.requestedTiles);\n currentFrameStateData._emptyTiles = Object.values(this._traverser.emptyTiles);\n this.traverseCounter--;\n if (this.traverseCounter > 0) {\n return;\n }\n this._updateTiles();\n }\n /**\n * Update tiles relying on data from all traversers\n */\n _updateTiles() {\n this.selectedTiles = [];\n this._requestedTiles = [];\n this._emptyTiles = [];\n for (const frameStateKey in this.frameStateData) {\n const frameStateDataValue = this.frameStateData[frameStateKey];\n this.selectedTiles = this.selectedTiles.concat(frameStateDataValue.selectedTiles);\n this._requestedTiles = this._requestedTiles.concat(frameStateDataValue._requestedTiles);\n this._emptyTiles = this._emptyTiles.concat(frameStateDataValue._emptyTiles);\n }\n this.selectedTiles = this.options.onTraversalComplete(this.selectedTiles);\n for (const tile of this.selectedTiles) {\n this._tiles[tile.id] = tile;\n }\n this._loadTiles();\n this._unloadTiles();\n this._updateStats();\n }\n _tilesChanged(oldSelectedTiles, selectedTiles) {\n if (oldSelectedTiles.length !== selectedTiles.length) {\n return true;\n }\n const set1 = new Set(oldSelectedTiles.map((t) => t.id));\n const set2 = new Set(selectedTiles.map((t) => t.id));\n let changed = oldSelectedTiles.filter((x) => !set2.has(x.id)).length > 0;\n changed = changed || selectedTiles.filter((x) => !set1.has(x.id)).length > 0;\n return changed;\n }\n _loadTiles() {\n // Sort requests by priority before making any requests.\n // This makes it less likely this requests will be cancelled after being issued.\n // requestedTiles.sort((a, b) => a._priority - b._priority);\n for (const tile of this._requestedTiles) {\n if (tile.contentUnloaded) {\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this._loadTile(tile);\n }\n }\n }\n _unloadTiles() {\n // unload tiles from cache when hit maximumMemoryUsage\n this._cache.unloadTiles(this, (tileset, tile) => tileset._unloadTile(tile));\n }\n _updateStats() {\n let tilesRenderable = 0;\n let pointsRenderable = 0;\n for (const tile of this.selectedTiles) {\n if (tile.contentAvailable && tile.content) {\n tilesRenderable++;\n if (tile.content.pointCount) {\n pointsRenderable += tile.content.pointCount;\n }\n else {\n // Calculate vertices for non point cloud tiles.\n pointsRenderable += tile.content.vertexCount;\n }\n }\n }\n this.stats.get(TILES_IN_VIEW).count = this.selectedTiles.length;\n this.stats.get(TILES_RENDERABLE).count = tilesRenderable;\n this.stats.get(POINTS_COUNT).count = pointsRenderable;\n this.stats.get(MAXIMUM_SSE).count = this.memoryAdjustedScreenSpaceError;\n }\n async _initializeTileSet(tilesetJson) {\n if (this.type === TILESET_TYPE.I3S) {\n this.calculateViewPropsI3S();\n tilesetJson.root = await tilesetJson.root;\n }\n this.root = this._initializeTileHeaders(tilesetJson, null);\n if (this.type === TILESET_TYPE.TILES3D) {\n this._initializeTiles3DTileset(tilesetJson);\n this.calculateViewPropsTiles3D();\n }\n if (this.type === TILESET_TYPE.I3S) {\n this._initializeI3STileset();\n }\n }\n /**\n * Called during initialize Tileset to initialize the tileset's cartographic center (longitude, latitude) and zoom.\n * These metrics help apps center view on tileset\n * For I3S there is extent (<1.8 version) or fullExtent (>=1.8 version) to calculate view props\n * @returns\n */\n calculateViewPropsI3S() {\n // for I3S 1.8 try to calculate with fullExtent\n const fullExtent = this.tileset.fullExtent;\n if (fullExtent) {\n const { xmin, xmax, ymin, ymax, zmin, zmax } = fullExtent;\n this.cartographicCenter = new Vector3(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2, zmin + (zmax - zmin) / 2);\n this.cartesianCenter = new Vector3();\n Ellipsoid.WGS84.cartographicToCartesian(this.cartographicCenter, this.cartesianCenter);\n this.zoom = getZoomFromFullExtent(fullExtent, this.cartographicCenter, this.cartesianCenter);\n return;\n }\n // for I3S 1.6-1.7 try to calculate with extent\n const extent = this.tileset.store?.extent;\n if (extent) {\n const [xmin, ymin, xmax, ymax] = extent;\n this.cartographicCenter = new Vector3(xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2, 0);\n this.cartesianCenter = new Vector3();\n Ellipsoid.WGS84.cartographicToCartesian(this.cartographicCenter, this.cartesianCenter);\n this.zoom = getZoomFromExtent(extent, this.cartographicCenter, this.cartesianCenter);\n return;\n }\n // eslint-disable-next-line no-console\n console.warn('Extent is not defined in the tileset header');\n this.cartographicCenter = new Vector3();\n this.zoom = 1;\n return;\n }\n /**\n * Called during initialize Tileset to initialize the tileset's cartographic center (longitude, latitude) and zoom.\n * These metrics help apps center view on tileset.\n * For 3DTiles the root tile data is used to calculate view props.\n * @returns\n */\n calculateViewPropsTiles3D() {\n const root = this.root;\n const { center } = root.boundingVolume;\n // TODO - handle all cases\n if (!center) {\n // eslint-disable-next-line no-console\n console.warn('center was not pre-calculated for the root tile');\n this.cartographicCenter = new Vector3();\n this.zoom = 1;\n return;\n }\n // cartographic coordinates are undefined at the center of the ellipsoid\n if (center[0] !== 0 || center[1] !== 0 || center[2] !== 0) {\n this.cartographicCenter = new Vector3();\n Ellipsoid.WGS84.cartesianToCartographic(center, this.cartographicCenter);\n }\n else {\n this.cartographicCenter = new Vector3(0, 0, -Ellipsoid.WGS84.radii[0]);\n }\n this.cartesianCenter = center;\n this.zoom = getZoomFromBoundingVolume(root.boundingVolume, this.cartographicCenter);\n }\n _initializeStats() {\n this.stats.get(TILES_TOTAL);\n this.stats.get(TILES_LOADING);\n this.stats.get(TILES_IN_MEMORY);\n this.stats.get(TILES_IN_VIEW);\n this.stats.get(TILES_RENDERABLE);\n this.stats.get(TILES_LOADED);\n this.stats.get(TILES_UNLOADED);\n this.stats.get(TILES_LOAD_FAILED);\n this.stats.get(POINTS_COUNT);\n this.stats.get(TILES_GPU_MEMORY, 'memory');\n this.stats.get(MAXIMUM_SSE);\n }\n // Installs the main tileset JSON file or a tileset JSON file referenced from a tile.\n // eslint-disable-next-line max-statements\n _initializeTileHeaders(tilesetJson, parentTileHeader) {\n // A tileset JSON file referenced from a tile may exist in a different directory than the root tileset.\n // Get the basePath relative to the external tileset.\n const rootTile = new Tile3D(this, tilesetJson.root, parentTileHeader); // resource\n // If there is a parentTileHeader, add the root of the currently loading tileset\n // to parentTileHeader's children, and update its depth.\n if (parentTileHeader) {\n parentTileHeader.children.push(rootTile);\n rootTile.depth = parentTileHeader.depth + 1;\n }\n // 3DTiles knows the hierarchy beforehand\n if (this.type === TILESET_TYPE.TILES3D) {\n const stack = [];\n stack.push(rootTile);\n while (stack.length > 0) {\n const tile = stack.pop();\n this.stats.get(TILES_TOTAL).incrementCount();\n const children = tile.header.children || [];\n for (const childHeader of children) {\n const childTile = new Tile3D(this, childHeader, tile);\n // Special handling for Google\n // A session key must be used for all tile requests\n if (childTile.contentUrl?.includes('?session=')) {\n const url = new URL(childTile.contentUrl);\n const session = url.searchParams.get('session');\n // eslint-disable-next-line max-depth\n if (session) {\n this._queryParams.session = session;\n }\n }\n tile.children.push(childTile);\n childTile.depth = tile.depth + 1;\n stack.push(childTile);\n }\n }\n }\n return rootTile;\n }\n _initializeTraverser() {\n let TraverserClass;\n const type = this.type;\n switch (type) {\n case TILESET_TYPE.TILES3D:\n TraverserClass = Tileset3DTraverser;\n break;\n case TILESET_TYPE.I3S:\n TraverserClass = I3STilesetTraverser;\n break;\n default:\n TraverserClass = TilesetTraverser;\n }\n return new TraverserClass({\n basePath: this.basePath,\n onTraversalEnd: this._onTraversalEnd.bind(this)\n });\n }\n _destroyTileHeaders(parentTile) {\n this._destroySubtree(parentTile);\n }\n async _loadTile(tile) {\n let loaded;\n try {\n this._onStartTileLoading();\n loaded = await tile.loadContent();\n }\n catch (error) {\n this._onTileLoadError(tile, error instanceof Error ? error : new Error('load failed'));\n }\n finally {\n this._onEndTileLoading();\n this._onTileLoad(tile, loaded);\n }\n }\n _onTileLoadError(tile, error) {\n this.stats.get(TILES_LOAD_FAILED).incrementCount();\n const message = error.message || error.toString();\n const url = tile.url;\n // TODO - Allow for probe log to be injected instead of console?\n console.error(`A 3D tile failed to load: ${tile.url} ${message}`); // eslint-disable-line\n this.options.onTileError(tile, message, url);\n }\n _onTileLoad(tile, loaded) {\n if (!loaded) {\n return;\n }\n if (this.type === TILESET_TYPE.I3S) {\n // We can't calculate tiles total in I3S in advance so we calculate it dynamically.\n const nodesInNodePages = this.tileset?.nodePagesTile?.nodesInNodePages || 0;\n this.stats.get(TILES_TOTAL).reset();\n this.stats.get(TILES_TOTAL).addCount(nodesInNodePages);\n }\n // add coordinateOrigin and modelMatrix to tile\n if (tile && tile.content) {\n calculateTransformProps(tile, tile.content);\n }\n this.updateContentTypes(tile);\n this._addTileToCache(tile);\n this.options.onTileLoad(tile);\n }\n /**\n * Update information about data types in nested tiles\n * @param tile instance of a nested Tile3D\n */\n updateContentTypes(tile) {\n if (this.type === TILESET_TYPE.I3S) {\n if (tile.header.isDracoGeometry) {\n this.contentFormats.draco = true;\n }\n switch (tile.header.textureFormat) {\n case 'dds':\n this.contentFormats.dds = true;\n break;\n case 'ktx2':\n this.contentFormats.ktx2 = true;\n break;\n default:\n }\n }\n else if (this.type === TILESET_TYPE.TILES3D) {\n const { extensionsRemoved = [] } = tile.content?.gltf || {};\n if (extensionsRemoved.includes('KHR_draco_mesh_compression')) {\n this.contentFormats.draco = true;\n }\n if (extensionsRemoved.includes('EXT_meshopt_compression')) {\n this.contentFormats.meshopt = true;\n }\n if (extensionsRemoved.includes('KHR_texture_basisu')) {\n this.contentFormats.ktx2 = true;\n }\n }\n }\n _onStartTileLoading() {\n this._pendingCount++;\n this.stats.get(TILES_LOADING).incrementCount();\n }\n _onEndTileLoading() {\n this._pendingCount--;\n this.stats.get(TILES_LOADING).decrementCount();\n }\n _addTileToCache(tile) {\n this._cache.add(this, tile, (tileset) => tileset._updateCacheStats(tile));\n }\n _updateCacheStats(tile) {\n this.stats.get(TILES_LOADED).incrementCount();\n this.stats.get(TILES_IN_MEMORY).incrementCount();\n // TODO: Calculate GPU memory usage statistics for a tile.\n this.gpuMemoryUsageInBytes += tile.gpuMemoryUsageInBytes || 0;\n this.stats.get(TILES_GPU_MEMORY).count = this.gpuMemoryUsageInBytes;\n // Adjust SSE based on cache limits\n if (this.options.memoryAdjustedScreenSpaceError) {\n this.adjustScreenSpaceError();\n }\n }\n _unloadTile(tile) {\n this.gpuMemoryUsageInBytes -= tile.gpuMemoryUsageInBytes || 0;\n this.stats.get(TILES_IN_MEMORY).decrementCount();\n this.stats.get(TILES_UNLOADED).incrementCount();\n this.stats.get(TILES_GPU_MEMORY).count = this.gpuMemoryUsageInBytes;\n this.options.onTileUnload(tile);\n tile.unloadContent();\n }\n // Traverse the tree and destroy all tiles\n _destroy() {\n const stack = [];\n if (this.root) {\n stack.push(this.root);\n }\n while (stack.length > 0) {\n const tile = stack.pop();\n for (const child of tile.children) {\n stack.push(child);\n }\n this._destroyTile(tile);\n }\n this.root = null;\n }\n // Traverse the tree and destroy all sub tiles\n _destroySubtree(tile) {\n const root = tile;\n const stack = [];\n stack.push(root);\n while (stack.length > 0) {\n tile = stack.pop();\n for (const child of tile.children) {\n stack.push(child);\n }\n if (tile !== root) {\n this._destroyTile(tile);\n }\n }\n root.children = [];\n }\n _destroyTile(tile) {\n this._cache.unloadTile(this, tile);\n this._unloadTile(tile);\n tile.destroy();\n }\n _initializeTiles3DTileset(tilesetJson) {\n if (tilesetJson.queryString) {\n const searchParams = new URLSearchParams(tilesetJson.queryString);\n const queryParams = Object.fromEntries(searchParams.entries());\n this._queryParams = { ...this._queryParams, ...queryParams };\n }\n this.asset = tilesetJson.asset;\n if (!this.asset) {\n throw new Error('Tileset must have an asset property.');\n }\n if (this.asset.version !== '0.0' &&\n this.asset.version !== '1.0' &&\n this.asset.version !== '1.1') {\n throw new Error('The tileset must be 3D Tiles version either 0.0 or 1.0 or 1.1.');\n }\n // Note: `asset.tilesetVersion` is version of the tileset itself (not the version of the 3D TILES standard)\n // We add this version as a `v=1.0` query param to fetch the right version and not get an older cached version\n if ('tilesetVersion' in this.asset) {\n this._queryParams.v = this.asset.tilesetVersion;\n }\n // TODO - ion resources have a credits property we can use for additional attribution.\n this.credits = {\n attributions: this.options.attributions || []\n };\n this.description = this.options.description || '';\n // Gets the tileset's properties dictionary object, which contains metadata about per-feature properties.\n this.properties = tilesetJson.properties;\n this.geometricError = tilesetJson.geometricError;\n this._extensionsUsed = tilesetJson.extensionsUsed || [];\n // Returns the extras property at the top of the tileset JSON (application specific metadata).\n this.extras = tilesetJson.extras;\n }\n _initializeI3STileset() {\n // @ts-expect-error\n if (this.loadOptions.i3s && 'token' in this.loadOptions.i3s) {\n // @ts-ignore\n this._queryParams.token = this.loadOptions.i3s.token;\n }\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\n// This file is derived from the Cesium code base under Apache 2 license\n// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md\n/**\n * Doubly linked list node\n * @private\n */\nexport class DoublyLinkedListNode {\n item;\n previous;\n next;\n constructor(item, previous, next) {\n this.item = item;\n this.previous = previous;\n this.next = next;\n }\n}\n", "// This file is derived from the Cesium code base under Apache 2 license\n// See LICENSE.md and https://github.com/AnalyticalGraphicsInc/cesium/blob/master/LICENSE.md\nimport { DoublyLinkedListNode } from \"./doubly-linked-list-node.js\";\n/**\n * Doubly linked list\n * @private\n */\nexport class DoublyLinkedList {\n head = null;\n tail = null;\n _length = 0;\n get length() {\n return this._length;\n }\n /**\n * Adds the item to the end of the list\n * @param {*} [item]\n * @return {DoublyLinkedListNode}\n */\n add(item) {\n const node = new DoublyLinkedListNode(item, this.tail, null);\n if (this.tail) {\n this.tail.next = node;\n this.tail = node;\n }\n else {\n this.head = node;\n this.tail = node;\n }\n ++this._length;\n return node;\n }\n /**\n * Removes the given node from the list\n * @param {DoublyLinkedListNode} node\n */\n remove(node) {\n if (!node) {\n return;\n }\n if (node.previous && node.next) {\n node.previous.next = node.next;\n node.next.previous = node.previous;\n }\n else if (node.previous) {\n // Remove last node\n node.previous.next = null;\n this.tail = node.previous;\n }\n else if (node.next) {\n // Remove first node\n node.next.previous = null;\n this.head = node.next;\n }\n else {\n // Remove last node in the linked list\n this.head = null;\n this.tail = null;\n }\n node.next = null;\n node.previous = null;\n --this._length;\n }\n /**\n * Moves nextNode after node\n * @param {DoublyLinkedListNode} node\n * @param {DoublyLinkedListNode} nextNode\n */\n splice(node, nextNode) {\n if (node === nextNode) {\n return;\n }\n // Remove nextNode, then insert after node\n this.remove(nextNode);\n this._insert(node, nextNode);\n }\n _insert(node, nextNode) {\n const oldNodeNext = node.next;\n node.next = nextNode;\n // nextNode is the new tail\n if (this.tail === node) {\n this.tail = nextNode;\n }\n else {\n oldNodeNext.previous = nextNode;\n }\n nextNode.next = oldNodeNext;\n nextNode.previous = node;\n ++this._length;\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { DoublyLinkedList } from \"../utils/doubly-linked-list.js\";\n/**\n * Stores tiles with content loaded.\n * @private\n */\nexport class TilesetCache {\n _list;\n _sentinel;\n _trimTiles;\n constructor() {\n // [head, sentinel) -> tiles that weren't selected this frame and may be removed from the cache\n // (sentinel, tail] -> tiles that were selected this frame\n this._list = new DoublyLinkedList();\n this._sentinel = this._list.add('sentinel');\n this._trimTiles = false;\n }\n reset() {\n // Move sentinel node to the tail so, at the start of the frame, all tiles\n // may be potentially replaced. Tiles are moved to the right of the sentinel\n // when they are selected so they will not be replaced.\n this._list.splice(this._list.tail, this._sentinel);\n }\n touch(tile) {\n const node = tile._cacheNode;\n if (node) {\n this._list.splice(this._sentinel, node);\n }\n }\n add(tileset, tile, addCallback) {\n if (!tile._cacheNode) {\n tile._cacheNode = this._list.add(tile);\n if (addCallback) {\n addCallback(tileset, tile);\n }\n }\n }\n unloadTile(tileset, tile, unloadCallback) {\n const node = tile._cacheNode;\n if (!node) {\n return;\n }\n this._list.remove(node);\n tile._cacheNode = null;\n if (unloadCallback) {\n unloadCallback(tileset, tile);\n }\n }\n unloadTiles(tileset, unloadCallback) {\n const trimTiles = this._trimTiles;\n this._trimTiles = false;\n const list = this._list;\n const maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024;\n // Traverse the list only to the sentinel since tiles/nodes to the\n // right of the sentinel were used this frame.\n // The sub-list to the left of the sentinel is ordered from LRU to MRU.\n const sentinel = this._sentinel;\n let node = list.head;\n while (node !== sentinel &&\n (tileset.gpuMemoryUsageInBytes > maximumMemoryUsageInBytes || trimTiles)) {\n // @ts-expect-error\n const tile = node.item;\n // @ts-expect-error\n node = node.next;\n this.unloadTile(tileset, tile, unloadCallback);\n }\n }\n trim() {\n this._trimTiles = true;\n }\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { Ellipsoid } from '@math.gl/geospatial';\nimport { Matrix4, Vector3 } from '@math.gl/core';\nimport { assert } from '@loaders.gl/loader-utils';\nexport function calculateTransformProps(tileHeader, tile) {\n assert(tileHeader);\n assert(tile);\n const { rtcCenter, gltfUpAxis } = tile;\n const { computedTransform, boundingVolume: { center } } = tileHeader;\n let modelMatrix = new Matrix4(computedTransform);\n // Translate if appropriate\n if (rtcCenter) {\n modelMatrix.translate(rtcCenter);\n }\n // glTF models need to be rotated from Y to Z up\n // https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification#y-up-to-z-up\n switch (gltfUpAxis) {\n case 'Z':\n break;\n case 'Y':\n const rotationY = new Matrix4().rotateX(Math.PI / 2);\n modelMatrix = modelMatrix.multiplyRight(rotationY);\n break;\n case 'X':\n const rotationX = new Matrix4().rotateY(-Math.PI / 2);\n modelMatrix = modelMatrix.multiplyRight(rotationX);\n break;\n default:\n break;\n }\n // Scale/offset positions if normalized integers\n if (tile.isQuantized) {\n modelMatrix.translate(tile.quantizedVolumeOffset).scale(tile.quantizedVolumeScale);\n }\n // Option 1: Cartesian matrix and origin\n const cartesianOrigin = new Vector3(center);\n tile.cartesianModelMatrix = modelMatrix;\n tile.cartesianOrigin = cartesianOrigin;\n // Option 2: Cartographic matrix and origin\n const cartographicOrigin = Ellipsoid.WGS84.cartesianToCartographic(cartesianOrigin, new Vector3());\n const fromFixedFrameMatrix = Ellipsoid.WGS84.eastNorthUpToFixedFrame(cartesianOrigin);\n const toFixedFrameMatrix = fromFixedFrameMatrix.invert();\n tile.cartographicModelMatrix = toFixedFrameMatrix.multiplyRight(modelMatrix);\n tile.cartographicOrigin = cartographicOrigin;\n // Deprecated, drop\n if (!tile.coordinateSystem) {\n tile.modelMatrix = tile.cartographicModelMatrix;\n }\n}\n", "import { Vector3 } from '@math.gl/core';\nimport { CullingVolume, Plane } from '@math.gl/culling';\nimport { Ellipsoid } from '@math.gl/geospatial';\nconst scratchVector = new Vector3();\nconst scratchPosition = new Vector3();\nconst cullingVolume = new CullingVolume([\n new Plane(),\n new Plane(),\n new Plane(),\n new Plane(),\n new Plane(),\n new Plane()\n]);\n// Extracts a frame state appropriate for tile culling from a deck.gl viewport\n// TODO - this could likely be generalized and merged back into deck.gl for other culling scenarios\nexport function getFrameState(viewport, frameNumber) {\n // Traverse and and request. Update _selectedTiles so that we know what to render.\n // Traverse and and request. Update _selectedTiles so that we know what to render.\n const { cameraDirection, cameraUp, height } = viewport;\n const { metersPerUnit } = viewport.distanceScales;\n // TODO - Ellipsoid.eastNorthUpToFixedFrame() breaks on raw array, create a Vector.\n // TODO - Ellipsoid.eastNorthUpToFixedFrame() takes a cartesian, is that intuitive?\n const viewportCenterCartesian = worldToCartesian(viewport, viewport.center);\n const enuToFixedTransform = Ellipsoid.WGS84.eastNorthUpToFixedFrame(viewportCenterCartesian);\n const cameraPositionCartographic = viewport.unprojectPosition(viewport.cameraPosition);\n const cameraPositionCartesian = Ellipsoid.WGS84.cartographicToCartesian(cameraPositionCartographic, new Vector3());\n // These should still be normalized as the transform has scale 1 (goes from meters to meters)\n const cameraDirectionCartesian = new Vector3(\n // @ts-ignore\n enuToFixedTransform.transformAsVector(new Vector3(cameraDirection).scale(metersPerUnit))).normalize();\n const cameraUpCartesian = new Vector3(\n // @ts-ignore\n enuToFixedTransform.transformAsVector(new Vector3(cameraUp).scale(metersPerUnit))).normalize();\n commonSpacePlanesToWGS84(viewport);\n const ViewportClass = viewport.constructor;\n const { longitude, latitude, width, bearing, zoom } = viewport;\n // @ts-ignore\n const topDownViewport = new ViewportClass({\n longitude,\n latitude,\n height,\n width,\n bearing,\n zoom,\n pitch: 0\n });\n // TODO: make a file/class for frameState and document what needs to be attached to this so that traversal can function\n return {\n camera: {\n position: cameraPositionCartesian,\n direction: cameraDirectionCartesian,\n up: cameraUpCartesian\n },\n viewport,\n topDownViewport,\n height,\n cullingVolume,\n frameNumber, // TODO: This can be the same between updates, what number is unique for between updates?\n sseDenominator: 1.15 // Assumes fovy = 60 degrees\n };\n}\n/**\n * Limit `tiles` array length with `maximumTilesSelected` number.\n * The criteria for this filtering is distance of a tile center\n * to the `frameState.viewport`'s longitude and latitude\n * @param tiles - tiles array to filter\n * @param frameState - frameState to calculate distances\n * @param maximumTilesSelected - maximal amount of tiles in the output array\n * @returns new tiles array\n */\nexport function limitSelectedTiles(tiles, frameState, maximumTilesSelected) {\n if (maximumTilesSelected === 0 || tiles.length <= maximumTilesSelected) {\n return [tiles, []];\n }\n // Accumulate distances in couples array: [tileIndex: number, distanceToViewport: number]\n const tuples = [];\n const { longitude: viewportLongitude, latitude: viewportLatitude } = frameState.viewport;\n for (const [index, tile] of tiles.entries()) {\n const [longitude, latitude] = tile.header.mbs;\n const deltaLon = Math.abs(viewportLongitude - longitude);\n const deltaLat = Math.abs(viewportLatitude - latitude);\n const distance = Math.sqrt(deltaLat * deltaLat + deltaLon * deltaLon);\n tuples.push([index, distance]);\n }\n const tuplesSorted = tuples.sort((a, b) => a[1] - b[1]);\n const selectedTiles = [];\n for (let i = 0; i < maximumTilesSelected; i++) {\n selectedTiles.push(tiles[tuplesSorted[i][0]]);\n }\n const unselectedTiles = [];\n for (let i = maximumTilesSelected; i < tuplesSorted.length; i++) {\n unselectedTiles.push(tiles[tuplesSorted[i][0]]);\n }\n return [selectedTiles, unselectedTiles];\n}\nfunction commonSpacePlanesToWGS84(viewport) {\n // Extract frustum planes based on current view.\n const frustumPlanes = viewport.getFrustumPlanes();\n // Get the near/far plane centers\n const nearCenterCommon = closestPointOnPlane(frustumPlanes.near, viewport.cameraPosition);\n const nearCenterCartesian = worldToCartesian(viewport, nearCenterCommon);\n const cameraCartesian = worldToCartesian(viewport, viewport.cameraPosition, scratchPosition);\n let i = 0;\n cullingVolume.planes[i++].fromPointNormal(nearCenterCartesian, scratchVector.copy(nearCenterCartesian).subtract(cameraCartesian));\n for (const dir in frustumPlanes) {\n if (dir === 'near') {\n continue; // eslint-disable-line no-continue\n }\n const plane = frustumPlanes[dir];\n const posCommon = closestPointOnPlane(plane, nearCenterCommon, scratchPosition);\n const cartesianPos = worldToCartesian(viewport, posCommon, scratchPosition);\n cullingVolume.planes[i++].fromPointNormal(cartesianPos, \n // Want the normal to point into the frustum since that's what culling expects\n scratchVector.copy(nearCenterCartesian).subtract(cartesianPos));\n }\n}\nfunction closestPointOnPlane(plane, refPoint, out = new Vector3()) {\n const distanceToRef = plane.normal.dot(refPoint);\n out\n .copy(plane.normal)\n .scale(plane.distance - distanceToRef)\n .add(refPoint);\n return out;\n}\nfunction worldToCartesian(viewport, point, out = new Vector3()) {\n const cartographicPos = viewport.unprojectPosition(point);\n return Ellipsoid.WGS84.cartographicToCartesian(cartographicPos, out);\n}\n", "// loaders.gl\n// SPDX-License-Identifier: MIT\n// Copyright (c) vis.gl contributors\nimport { Vector3 } from '@math.gl/core';\nimport { BoundingSphere, OrientedBoundingBox } from '@math.gl/culling';\nimport { Ellipsoid } from '@math.gl/geospatial';\nconst WGS84_RADIUS_X = 6378137.0;\nconst WGS84_RADIUS_Y = 6378137.0;\nconst WGS84_RADIUS_Z = 6356752.3142451793;\nconst scratchVector = new Vector3();\n/**\n * Calculate appropriate zoom value for a particular boundingVolume\n * @param boundingVolume - the instance of bounding volume\n * @param cartorgraphicCenter - cartographic center of the bounding volume\n * @returns {number} - zoom value\n */\nexport function getZoomFromBoundingVolume(boundingVolume, cartorgraphicCenter) {\n if (boundingVolume instanceof OrientedBoundingBox) {\n // OrientedBoundingBox\n const { halfAxes } = boundingVolume;\n const obbSize = getObbSize(halfAxes);\n // Use WGS84_RADIUS_Z to allign with BoundingSphere algorithm\n // Add the tile elevation value for correct zooming to elevated tiles\n return Math.log2(WGS84_RADIUS_Z / (obbSize + cartorgraphicCenter[2]));\n }\n else if (boundingVolume instanceof BoundingSphere) {\n // BoundingSphere\n const { radius } = boundingVolume;\n // Add the tile elevation value for correct zooming to elevated tiles\n return Math.log2(WGS84_RADIUS_Z / (radius + cartorgraphicCenter[2]));\n }\n else if (boundingVolume.width && boundingVolume.height) {\n // BoundingRectangle\n const { width, height } = boundingVolume;\n const zoomX = Math.log2(WGS84_RADIUS_X / width);\n const zoomY = Math.log2(WGS84_RADIUS_Y / height);\n return (zoomX + zoomY) / 2;\n }\n return 1;\n}\n/**\n * Calculate initial zoom for the tileset from 3D `fullExtent` defined in\n * the tileset metadata\n * @param fullExtent - 3D extent of the tileset\n * @param fullExtent.xmin - minimal longitude in decimal degrees\n * @param fullExtent.xmax - maximal longitude in decimal degrees\n * @param fullExtent.ymin - minimal latitude in decimal degrees\n * @param fullExtent.ymax - maximal latitude in decimal degrees\n * @param fullExtent.zmin - minimal elevation in meters\n * @param fullExtent.zmax - maximal elevation in meters\n * @param cartorgraphicCenter - tileset center in cartographic coordinate system\n * @param cartesianCenter - tileset center in cartesian coordinate system\n * @returns - initial zoom for the tileset\n */\nexport function getZoomFromFullExtent(fullExtent, cartorgraphicCenter, cartesianCenter) {\n Ellipsoid.WGS84.cartographicToCartesian([fullExtent.xmax, fullExtent.ymax, fullExtent.zmax], scratchVector);\n const extentSize = Math.sqrt(Math.pow(scratchVector[0] - cartesianCenter[0], 2) +\n Math.pow(scratchVector[1] - cartesianCenter[1], 2) +\n Math.pow(scratchVector[2] - cartesi