UNPKG

@loaders.gl/tiles

Version:

Common components for different tiles loaders.

1,419 lines (1,402 loc) 88.5 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // dist/index.js var dist_exports = {}; __export(dist_exports, { LOD_METRIC_TYPE: () => LOD_METRIC_TYPE, TILESET_TYPE: () => TILESET_TYPE, TILE_CONTENT_STATE: () => TILE_CONTENT_STATE, TILE_REFINEMENT: () => TILE_REFINEMENT, TILE_TYPE: () => TILE_TYPE, Tile3D: () => Tile3D, Tileset3D: () => Tileset3D, TilesetCache: () => TilesetCache, TilesetTraverser: () => TilesetTraverser, calculateTransformProps: () => calculateTransformProps, createBoundingVolume: () => createBoundingVolume, getFrameState: () => getFrameState, getLodStatus: () => getLodStatus }); module.exports = __toCommonJS(dist_exports); // dist/tileset/tileset-3d.js var import_core10 = require("@math.gl/core"); var import_geospatial6 = require("@math.gl/geospatial"); var import_stats = require("@probe.gl/stats"); var import_loader_utils4 = require("@loaders.gl/loader-utils"); // dist/utils/doubly-linked-list-node.js var DoublyLinkedListNode = class { item; previous; next; constructor(item, previous, next) { this.item = item; this.previous = previous; this.next = next; } }; // dist/utils/doubly-linked-list.js var DoublyLinkedList = class { head = null; tail = null; _length = 0; get length() { return this._length; } /** * Adds the item to the end of the list * @param {*} [item] * @return {DoublyLinkedListNode} */ add(item) { const node = new DoublyLinkedListNode(item, this.tail, null); if (this.tail) { this.tail.next = node; this.tail = node; } else { this.head = node; this.tail = node; } ++this._length; return node; } /** * Removes the given node from the list * @param {DoublyLinkedListNode} node */ remove(node) { if (!node) { return; } if (node.previous && node.next) { node.previous.next = node.next; node.next.previous = node.previous; } else if (node.previous) { node.previous.next = null; this.tail = node.previous; } else if (node.next) { node.next.previous = null; this.head = node.next; } else { this.head = null; this.tail = null; } node.next = null; node.previous = null; --this._length; } /** * Moves nextNode after node * @param {DoublyLinkedListNode} node * @param {DoublyLinkedListNode} nextNode */ splice(node, nextNode) { if (node === nextNode) { return; } this.remove(nextNode); this._insert(node, nextNode); } _insert(node, nextNode) { const oldNodeNext = node.next; node.next = nextNode; if (this.tail === node) { this.tail = nextNode; } else { oldNodeNext.previous = nextNode; } nextNode.next = oldNodeNext; nextNode.previous = node; ++this._length; } }; // dist/tileset/tileset-cache.js var TilesetCache = class { _list; _sentinel; _trimTiles; constructor() { this._list = new DoublyLinkedList(); this._sentinel = this._list.add("sentinel"); this._trimTiles = false; } reset() { this._list.splice(this._list.tail, this._sentinel); } touch(tile) { const node = tile._cacheNode; if (node) { this._list.splice(this._sentinel, node); } } add(tileset, tile, addCallback) { if (!tile._cacheNode) { tile._cacheNode = this._list.add(tile); if (addCallback) { addCallback(tileset, tile); } } } unloadTile(tileset, tile, unloadCallback) { const node = tile._cacheNode; if (!node) { return; } this._list.remove(node); tile._cacheNode = null; if (unloadCallback) { unloadCallback(tileset, tile); } } unloadTiles(tileset, unloadCallback) { const trimTiles = this._trimTiles; this._trimTiles = false; const list = this._list; const maximumMemoryUsageInBytes = tileset.maximumMemoryUsage * 1024 * 1024; const sentinel = this._sentinel; let node = list.head; while (node !== sentinel && (tileset.gpuMemoryUsageInBytes > maximumMemoryUsageInBytes || trimTiles)) { const tile = node.item; node = node.next; this.unloadTile(tileset, tile, unloadCallback); } } trim() { this._trimTiles = true; } }; // dist/tileset/helpers/transform-utils.js var import_geospatial = require("@math.gl/geospatial"); var import_core = require("@math.gl/core"); var import_loader_utils = require("@loaders.gl/loader-utils"); function calculateTransformProps(tileHeader, tile) { (0, import_loader_utils.assert)(tileHeader); (0, import_loader_utils.assert)(tile); const { rtcCenter, gltfUpAxis } = tile; const { computedTransform, boundingVolume: { center } } = tileHeader; let modelMatrix = new import_core.Matrix4(computedTransform); if (rtcCenter) { modelMatrix.translate(rtcCenter); } switch (gltfUpAxis) { case "Z": break; case "Y": const rotationY = new import_core.Matrix4().rotateX(Math.PI / 2); modelMatrix = modelMatrix.multiplyRight(rotationY); break; case "X": const rotationX = new import_core.Matrix4().rotateY(-Math.PI / 2); modelMatrix = modelMatrix.multiplyRight(rotationX); break; default: break; } if (tile.isQuantized) { modelMatrix.translate(tile.quantizedVolumeOffset).scale(tile.quantizedVolumeScale); } const cartesianOrigin = new import_core.Vector3(center); tile.cartesianModelMatrix = modelMatrix; tile.cartesianOrigin = cartesianOrigin; const cartographicOrigin = import_geospatial.Ellipsoid.WGS84.cartesianToCartographic(cartesianOrigin, new import_core.Vector3()); const fromFixedFrameMatrix = import_geospatial.Ellipsoid.WGS84.eastNorthUpToFixedFrame(cartesianOrigin); const toFixedFrameMatrix = fromFixedFrameMatrix.invert(); tile.cartographicModelMatrix = toFixedFrameMatrix.multiplyRight(modelMatrix); tile.cartographicOrigin = cartographicOrigin; if (!tile.coordinateSystem) { tile.modelMatrix = tile.cartographicModelMatrix; } } // dist/tileset/helpers/frame-state.js var import_core2 = require("@math.gl/core"); var import_culling = require("@math.gl/culling"); var import_geospatial2 = require("@math.gl/geospatial"); var scratchVector = new import_core2.Vector3(); var scratchPosition = new import_core2.Vector3(); var cullingVolume = new import_culling.CullingVolume([ new import_culling.Plane(), new import_culling.Plane(), new import_culling.Plane(), new import_culling.Plane(), new import_culling.Plane(), new import_culling.Plane() ]); function getFrameState(viewport, frameNumber) { const { cameraDirection, cameraUp, height } = viewport; const { metersPerUnit } = viewport.distanceScales; const viewportCenterCartesian = worldToCartesian(viewport, viewport.center); const enuToFixedTransform = import_geospatial2.Ellipsoid.WGS84.eastNorthUpToFixedFrame(viewportCenterCartesian); const cameraPositionCartographic = viewport.unprojectPosition(viewport.cameraPosition); const cameraPositionCartesian2 = import_geospatial2.Ellipsoid.WGS84.cartographicToCartesian(cameraPositionCartographic, new import_core2.Vector3()); const cameraDirectionCartesian = new import_core2.Vector3( // @ts-ignore enuToFixedTransform.transformAsVector(new import_core2.Vector3(cameraDirection).scale(metersPerUnit)) ).normalize(); const cameraUpCartesian = new import_core2.Vector3( // @ts-ignore enuToFixedTransform.transformAsVector(new import_core2.Vector3(cameraUp).scale(metersPerUnit)) ).normalize(); commonSpacePlanesToWGS84(viewport); const ViewportClass = viewport.constructor; const { longitude, latitude, width, bearing, zoom } = viewport; const topDownViewport = new ViewportClass({ longitude, latitude, height, width, bearing, zoom, pitch: 0 }); return { camera: { position: cameraPositionCartesian2, direction: cameraDirectionCartesian, up: cameraUpCartesian }, viewport, topDownViewport, height, cullingVolume, frameNumber, // TODO: This can be the same between updates, what number is unique for between updates? sseDenominator: 1.15 // Assumes fovy = 60 degrees }; } function limitSelectedTiles(tiles, frameState, maximumTilesSelected) { if (maximumTilesSelected === 0 || tiles.length <= maximumTilesSelected) { return [tiles, []]; } const tuples = []; const { longitude: viewportLongitude, latitude: viewportLatitude } = frameState.viewport; for (const [index, tile] of tiles.entries()) { const [longitude, latitude] = tile.header.mbs; const deltaLon = Math.abs(viewportLongitude - longitude); const deltaLat = Math.abs(viewportLatitude - latitude); const distance = Math.sqrt(deltaLat * deltaLat + deltaLon * deltaLon); tuples.push([index, distance]); } const tuplesSorted = tuples.sort((a, b) => a[1] - b[1]); const selectedTiles = []; for (let i = 0; i < maximumTilesSelected; i++) { selectedTiles.push(tiles[tuplesSorted[i][0]]); } const unselectedTiles = []; for (let i = maximumTilesSelected; i < tuplesSorted.length; i++) { unselectedTiles.push(tiles[tuplesSorted[i][0]]); } return [selectedTiles, unselectedTiles]; } function commonSpacePlanesToWGS84(viewport) { const frustumPlanes = viewport.getFrustumPlanes(); const nearCenterCommon = closestPointOnPlane(frustumPlanes.near, viewport.cameraPosition); const nearCenterCartesian = worldToCartesian(viewport, nearCenterCommon); const cameraCartesian = worldToCartesian(viewport, viewport.cameraPosition, scratchPosition); let i = 0; cullingVolume.planes[i++].fromPointNormal(nearCenterCartesian, scratchVector.copy(nearCenterCartesian).subtract(cameraCartesian)); for (const dir in frustumPlanes) { if (dir === "near") { continue; } const plane = frustumPlanes[dir]; const posCommon = closestPointOnPlane(plane, nearCenterCommon, scratchPosition); const cartesianPos = worldToCartesian(viewport, posCommon, scratchPosition); cullingVolume.planes[i++].fromPointNormal( cartesianPos, // Want the normal to point into the frustum since that's what culling expects scratchVector.copy(nearCenterCartesian).subtract(cartesianPos) ); } } function closestPointOnPlane(plane, refPoint, out = new import_core2.Vector3()) { const distanceToRef = plane.normal.dot(refPoint); out.copy(plane.normal).scale(plane.distance - distanceToRef).add(refPoint); return out; } function worldToCartesian(viewport, point, out = new import_core2.Vector3()) { const cartographicPos = viewport.unprojectPosition(point); return import_geospatial2.Ellipsoid.WGS84.cartographicToCartesian(cartographicPos, out); } // dist/tileset/helpers/zoom.js var import_core3 = require("@math.gl/core"); var import_culling2 = require("@math.gl/culling"); var import_geospatial3 = require("@math.gl/geospatial"); var WGS84_RADIUS_X = 6378137; var WGS84_RADIUS_Y = 6378137; var WGS84_RADIUS_Z = 6356752314245179e-9; var scratchVector2 = new import_core3.Vector3(); function getZoomFromBoundingVolume(boundingVolume, cartorgraphicCenter) { if (boundingVolume instanceof import_culling2.OrientedBoundingBox) { const { halfAxes } = boundingVolume; const obbSize = getObbSize(halfAxes); return Math.log2(WGS84_RADIUS_Z / (obbSize + cartorgraphicCenter[2])); } else if (boundingVolume instanceof import_culling2.BoundingSphere) { const { radius } = boundingVolume; return Math.log2(WGS84_RADIUS_Z / (radius + cartorgraphicCenter[2])); } else if (boundingVolume.width && boundingVolume.height) { const { width, height } = boundingVolume; const zoomX = Math.log2(WGS84_RADIUS_X / width); const zoomY = Math.log2(WGS84_RADIUS_Y / height); return (zoomX + zoomY) / 2; } return 1; } function getZoomFromFullExtent(fullExtent, cartorgraphicCenter, cartesianCenter) { import_geospatial3.Ellipsoid.WGS84.cartographicToCartesian([fullExtent.xmax, fullExtent.ymax, fullExtent.zmax], scratchVector2); const extentSize = Math.sqrt(Math.pow(scratchVector2[0] - cartesianCenter[0], 2) + Math.pow(scratchVector2[1] - cartesianCenter[1], 2) + Math.pow(scratchVector2[2] - cartesianCenter[2], 2)); return Math.log2(WGS84_RADIUS_Z / (extentSize + cartorgraphicCenter[2])); } function getZoomFromExtent(extent, cartorgraphicCenter, cartesianCenter) { const [xmin, ymin, xmax, ymax] = extent; return getZoomFromFullExtent({ xmin, xmax, ymin, ymax, zmin: 0, zmax: 0 }, cartorgraphicCenter, cartesianCenter); } function getObbSize(halfAxes) { halfAxes.getColumn(0, scratchVector2); const axeY = halfAxes.getColumn(1); const axeZ = halfAxes.getColumn(2); const farthestVertex = scratchVector2.add(axeY).add(axeZ); const size = farthestVertex.len(); return size; } // dist/tileset/tile-3d.js var import_core7 = require("@math.gl/core"); var import_culling4 = require("@math.gl/culling"); var import_core8 = require("@loaders.gl/core"); // dist/constants.js var TILE_CONTENT_STATE = { UNLOADED: 0, // Has never been requested LOADING: 1, // Is waiting on a pending request PROCESSING: 2, // Request received. Contents are being processed for rendering. Depending on the content, it might make its own requests for external data. READY: 3, // Ready to render. EXPIRED: 4, // Is expired and will be unloaded once new content is loaded. FAILED: 5 // Request failed. }; var TILE_REFINEMENT; (function(TILE_REFINEMENT2) { TILE_REFINEMENT2[TILE_REFINEMENT2["ADD"] = 1] = "ADD"; TILE_REFINEMENT2[TILE_REFINEMENT2["REPLACE"] = 2] = "REPLACE"; })(TILE_REFINEMENT || (TILE_REFINEMENT = {})); var TILE_TYPE; (function(TILE_TYPE2) { TILE_TYPE2["EMPTY"] = "empty"; TILE_TYPE2["SCENEGRAPH"] = "scenegraph"; TILE_TYPE2["POINTCLOUD"] = "pointcloud"; TILE_TYPE2["MESH"] = "mesh"; })(TILE_TYPE || (TILE_TYPE = {})); var TILESET_TYPE; (function(TILESET_TYPE2) { TILESET_TYPE2["I3S"] = "I3S"; TILESET_TYPE2["TILES3D"] = "TILES3D"; })(TILESET_TYPE || (TILESET_TYPE = {})); var LOD_METRIC_TYPE; (function(LOD_METRIC_TYPE2) { LOD_METRIC_TYPE2["GEOMETRIC_ERROR"] = "geometricError"; LOD_METRIC_TYPE2["MAX_SCREEN_THRESHOLD"] = "maxScreenThreshold"; })(LOD_METRIC_TYPE || (LOD_METRIC_TYPE = {})); var TILE3D_OPTIMIZATION_HINT = { NOT_COMPUTED: -1, USE_OPTIMIZATION: 1, SKIP_OPTIMIZATION: 0 }; // dist/tileset/helpers/bounding-volume.js var import_core4 = require("@math.gl/core"); var import_culling3 = require("@math.gl/culling"); var import_geospatial4 = require("@math.gl/geospatial"); var import_loader_utils2 = require("@loaders.gl/loader-utils"); function defined(x) { return x !== void 0 && x !== null; } var scratchPoint = new import_core4.Vector3(); var scratchScale = new import_core4.Vector3(); var scratchNorthWest = new import_core4.Vector3(); var scratchSouthEast = new import_core4.Vector3(); var scratchCenter = new import_core4.Vector3(); var scratchXAxis = new import_core4.Vector3(); var scratchYAxis = new import_core4.Vector3(); var scratchZAxis = new import_core4.Vector3(); function createBoundingVolume(boundingVolumeHeader, transform, result) { (0, import_loader_utils2.assert)(boundingVolumeHeader, "3D Tile: boundingVolume must be defined"); if (boundingVolumeHeader.box) { return createBox(boundingVolumeHeader.box, transform, result); } if (boundingVolumeHeader.region) { return createObbFromRegion(boundingVolumeHeader.region); } if (boundingVolumeHeader.sphere) { return createSphere(boundingVolumeHeader.sphere, transform, result); } throw new Error("3D Tile: boundingVolume must contain a sphere, region, or box"); } function getCartographicBounds(boundingVolumeHeader, boundingVolume) { if (boundingVolumeHeader.box) { return orientedBoundingBoxToCartographicBounds(boundingVolume); } if (boundingVolumeHeader.region) { const [west, south, east, north, minHeight, maxHeight] = boundingVolumeHeader.region; return [ [(0, import_core4.degrees)(west), (0, import_core4.degrees)(south), minHeight], [(0, import_core4.degrees)(east), (0, import_core4.degrees)(north), maxHeight] ]; } if (boundingVolumeHeader.sphere) { return boundingSphereToCartographicBounds(boundingVolume); } throw new Error("Unkown boundingVolume type"); } function createBox(box, transform, result) { const center = new import_core4.Vector3(box[0], box[1], box[2]); transform.transform(center, center); let origin = []; if (box.length === 10) { const halfSize = box.slice(3, 6); const quaternion = new import_core4.Quaternion(); quaternion.fromArray(box, 6); const x = new import_core4.Vector3([1, 0, 0]); const y = new import_core4.Vector3([0, 1, 0]); const z = new import_core4.Vector3([0, 0, 1]); x.transformByQuaternion(quaternion); x.scale(halfSize[0]); y.transformByQuaternion(quaternion); y.scale(halfSize[1]); z.transformByQuaternion(quaternion); z.scale(halfSize[2]); origin = [...x.toArray(), ...y.toArray(), ...z.toArray()]; } else { origin = [...box.slice(3, 6), ...box.slice(6, 9), ...box.slice(9, 12)]; } const xAxis = transform.transformAsVector(origin.slice(0, 3)); const yAxis = transform.transformAsVector(origin.slice(3, 6)); const zAxis = transform.transformAsVector(origin.slice(6, 9)); const halfAxes = new import_core4.Matrix3([ xAxis[0], xAxis[1], xAxis[2], yAxis[0], yAxis[1], yAxis[2], zAxis[0], zAxis[1], zAxis[2] ]); if (defined(result)) { result.center = center; result.halfAxes = halfAxes; return result; } return new import_culling3.OrientedBoundingBox(center, halfAxes); } function createSphere(sphere, transform, result) { const center = new import_core4.Vector3(sphere[0], sphere[1], sphere[2]); transform.transform(center, center); const scale = transform.getScale(scratchScale); const uniformScale = Math.max(Math.max(scale[0], scale[1]), scale[2]); const radius = sphere[3] * uniformScale; if (defined(result)) { result.center = center; result.radius = radius; return result; } return new import_culling3.BoundingSphere(center, radius); } function createObbFromRegion(region) { const [west, south, east, north, minHeight, maxHeight] = region; const northWest = import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([(0, import_core4.degrees)(west), (0, import_core4.degrees)(north), minHeight], scratchNorthWest); const southEast = import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([(0, import_core4.degrees)(east), (0, import_core4.degrees)(south), maxHeight], scratchSouthEast); const centerInCartesian = new import_core4.Vector3().addVectors(northWest, southEast).multiplyByScalar(0.5); import_geospatial4.Ellipsoid.WGS84.cartesianToCartographic(centerInCartesian, scratchCenter); import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([(0, import_core4.degrees)(east), scratchCenter[1], scratchCenter[2]], scratchXAxis); import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([scratchCenter[0], (0, import_core4.degrees)(north), scratchCenter[2]], scratchYAxis); import_geospatial4.Ellipsoid.WGS84.cartographicToCartesian([scratchCenter[0], scratchCenter[1], maxHeight], scratchZAxis); return createBox([ ...centerInCartesian, ...scratchXAxis.subtract(centerInCartesian), ...scratchYAxis.subtract(centerInCartesian), ...scratchZAxis.subtract(centerInCartesian) ], new import_core4.Matrix4()); } function orientedBoundingBoxToCartographicBounds(boundingVolume) { const result = emptyCartographicBounds(); const { halfAxes } = boundingVolume; const xAxis = new import_core4.Vector3(halfAxes.getColumn(0)); const yAxis = new import_core4.Vector3(halfAxes.getColumn(1)); const zAxis = new import_core4.Vector3(halfAxes.getColumn(2)); for (let x = 0; x < 2; x++) { for (let y = 0; y < 2; y++) { for (let z = 0; z < 2; z++) { scratchPoint.copy(boundingVolume.center); scratchPoint.add(xAxis); scratchPoint.add(yAxis); scratchPoint.add(zAxis); addToCartographicBounds(result, scratchPoint); zAxis.negate(); } yAxis.negate(); } xAxis.negate(); } return result; } function boundingSphereToCartographicBounds(boundingVolume) { const result = emptyCartographicBounds(); const { center, radius } = boundingVolume; const point = import_geospatial4.Ellipsoid.WGS84.scaleToGeodeticSurface(center, scratchPoint); let zAxis; if (point) { zAxis = import_geospatial4.Ellipsoid.WGS84.geodeticSurfaceNormal(point); } else { zAxis = new import_core4.Vector3(0, 0, 1); } let xAxis = new import_core4.Vector3(zAxis[2], -zAxis[1], 0); if (xAxis.len() > 0) { xAxis.normalize(); } else { xAxis = new import_core4.Vector3(0, 1, 0); } const yAxis = xAxis.clone().cross(zAxis); for (const axis of [xAxis, yAxis, zAxis]) { scratchScale.copy(axis).scale(radius); for (let dir = 0; dir < 2; dir++) { scratchPoint.copy(center); scratchPoint.add(scratchScale); addToCartographicBounds(result, scratchPoint); scratchScale.negate(); } } return result; } function emptyCartographicBounds() { return [ [Infinity, Infinity, Infinity], [-Infinity, -Infinity, -Infinity] ]; } function addToCartographicBounds(target, cartesian) { import_geospatial4.Ellipsoid.WGS84.cartesianToCartographic(cartesian, scratchPoint); target[0][0] = Math.min(target[0][0], scratchPoint[0]); target[0][1] = Math.min(target[0][1], scratchPoint[1]); target[0][2] = Math.min(target[0][2], scratchPoint[2]); target[1][0] = Math.max(target[1][0], scratchPoint[0]); target[1][1] = Math.max(target[1][1], scratchPoint[1]); target[1][2] = Math.max(target[1][2], scratchPoint[2]); } // dist/tileset/helpers/tiles-3d-lod.js var import_core5 = require("@math.gl/core"); var scratchPositionNormal = new import_core5.Vector3(); var scratchCartographic = new import_core5.Vector3(); var scratchMatrix = new import_core5.Matrix4(); var scratchCenter2 = new import_core5.Vector3(); var scratchPosition2 = new import_core5.Vector3(); var scratchDirection = new import_core5.Vector3(); function fog(distanceToCamera, density) { const scalar = distanceToCamera * density; return 1 - Math.exp(-(scalar * scalar)); } function getDynamicScreenSpaceError(tileset, distanceToCamera) { if (tileset.dynamicScreenSpaceError && tileset.dynamicScreenSpaceErrorComputedDensity) { const density = tileset.dynamicScreenSpaceErrorComputedDensity; const factor = tileset.dynamicScreenSpaceErrorFactor; const dynamicError = fog(distanceToCamera, density) * factor; return dynamicError; } return 0; } function getTiles3DScreenSpaceError(tile, frameState, useParentLodMetric) { const tileset = tile.tileset; const parentLodMetricValue = tile.parent && tile.parent.lodMetricValue || tile.lodMetricValue; const lodMetricValue = useParentLodMetric ? parentLodMetricValue : tile.lodMetricValue; if (lodMetricValue === 0) { return 0; } const distance = Math.max(tile._distanceToCamera, 1e-7); const { height, sseDenominator } = frameState; const { viewDistanceScale } = tileset.options; let error = lodMetricValue * height * (viewDistanceScale || 1) / (distance * sseDenominator); error -= getDynamicScreenSpaceError(tileset, distance); return error; } // dist/tileset/helpers/i3s-lod.js var import_core6 = require("@math.gl/core"); var import_geospatial5 = require("@math.gl/geospatial"); var cameraPositionCartesian = new import_core6.Vector3(); var toEye = new import_core6.Vector3(); var cameraPositionEnu = new import_core6.Vector3(); var extraVertexEnu = new import_core6.Vector3(); var projectedOriginVector = new import_core6.Vector3(); var enuToCartesianMatrix = new import_core6.Matrix4(); var cartesianToEnuMatrix = new import_core6.Matrix4(); function getLodStatus(tile, frameState) { if (tile.lodMetricValue === 0 || isNaN(tile.lodMetricValue)) { return "DIG"; } const screenSize = 2 * getProjectedRadius(tile, frameState); if (screenSize < 2) { return "OUT"; } if (!tile.header.children || screenSize <= tile.lodMetricValue) { return "DRAW"; } else if (tile.header.children) { return "DIG"; } return "OUT"; } function getProjectedRadius(tile, frameState) { const { topDownViewport: viewport } = frameState; const mbsLat = tile.header.mbs[1]; const mbsLon = tile.header.mbs[0]; const mbsZ = tile.header.mbs[2]; const mbsR = tile.header.mbs[3]; const mbsCenterCartesian = [...tile.boundingVolume.center]; const cameraPositionCartographic = viewport.unprojectPosition(viewport.cameraPosition); import_geospatial5.Ellipsoid.WGS84.cartographicToCartesian(cameraPositionCartographic, cameraPositionCartesian); toEye.copy(cameraPositionCartesian).subtract(mbsCenterCartesian).normalize(); import_geospatial5.Ellipsoid.WGS84.eastNorthUpToFixedFrame(mbsCenterCartesian, enuToCartesianMatrix); cartesianToEnuMatrix.copy(enuToCartesianMatrix).invert(); cameraPositionEnu.copy(cameraPositionCartesian).transform(cartesianToEnuMatrix); const projection = Math.sqrt(cameraPositionEnu[0] * cameraPositionEnu[0] + cameraPositionEnu[1] * cameraPositionEnu[1]); const extraZ = projection * projection / cameraPositionEnu[2]; extraVertexEnu.copy([cameraPositionEnu[0], cameraPositionEnu[1], extraZ]); const extraVertexCartesian = extraVertexEnu.transform(enuToCartesianMatrix); const extraVectorCartesian = extraVertexCartesian.subtract(mbsCenterCartesian).normalize(); const radiusVector = toEye.cross(extraVectorCartesian).normalize().scale(mbsR); const sphereMbsBorderVertexCartesian = radiusVector.add(mbsCenterCartesian); const sphereMbsBorderVertexCartographic = import_geospatial5.Ellipsoid.WGS84.cartesianToCartographic(sphereMbsBorderVertexCartesian); const projectedOrigin = viewport.project([mbsLon, mbsLat, mbsZ]); const projectedMbsBorderVertex = viewport.project(sphereMbsBorderVertexCartographic); const projectedRadius = projectedOriginVector.copy(projectedOrigin).subtract(projectedMbsBorderVertex).magnitude(); return projectedRadius; } // dist/tileset/helpers/3d-tiles-options.js function get3dTilesOptions(tileset) { return { assetGltfUpAxis: tileset.asset && tileset.asset.gltfUpAxis || "Y" }; } // dist/utils/managed-array.js var import_loader_utils3 = require("@loaders.gl/loader-utils"); var ManagedArray = class { _map = /* @__PURE__ */ new Map(); _array; _length; constructor(length = 0) { this._array = new Array(length); this._length = length; } /** * Gets or sets the length of the array. * If the set length is greater than the length of the internal array, the internal array is resized. * * @memberof ManagedArray.prototype * @type Number */ get length() { return this._length; } set length(length) { this._length = length; if (length > this._array.length) { this._array.length = length; } } /** * Gets the internal array. * * @memberof ManagedArray.prototype * @type Array * @readonly */ get values() { return this._array; } /** * Gets the element at an index. * * @param {Number} index The index to get. */ get(index) { (0, import_loader_utils3.assert)(index < this._array.length); return this._array[index]; } /** * Sets the element at an index. Resizes the array if index is greater than the length of the array. * * @param {Number} index The index to set. * @param {*} element The element to set at index. */ set(index, element) { (0, import_loader_utils3.assert)(index >= 0); if (index >= this.length) { this.length = index + 1; } if (this._map.has(this._array[index])) { this._map.delete(this._array[index]); } this._array[index] = element; this._map.set(element, index); } delete(element) { const index = this._map.get(element); if (index >= 0) { this._array.splice(index, 1); this._map.delete(element); this.length--; } } /** * Returns the last element in the array without modifying the array. * * @returns {*} The last element in the array. */ peek() { return this._array[this._length - 1]; } /** * Push an element into the array. * * @param {*} element The element to push. */ push(element) { if (!this._map.has(element)) { const index = this.length++; this._array[index] = element; this._map.set(element, index); } } /** * Pop an element from the array. * * @returns {*} The last element in the array. */ pop() { const element = this._array[--this.length]; this._map.delete(element); return element; } /** * Resize the internal array if length > _array.length. * * @param {Number} length The length. */ reserve(length) { (0, import_loader_utils3.assert)(length >= 0); if (length > this._array.length) { this._array.length = length; } } /** * Resize the array. * * @param {Number} length The length. */ resize(length) { (0, import_loader_utils3.assert)(length >= 0); this.length = length; } /** * Trim the internal array to the specified length. Defaults to the current length. * * @param {Number} [length] The length. */ trim(length) { if (length === null || length === void 0) { length = this.length; } this._array.length = length; } reset() { this._array = []; this._map = /* @__PURE__ */ new Map(); this._length = 0; } find(target) { return this._map.has(target); } }; // dist/tileset/tileset-traverser.js var DEFAULT_PROPS = { loadSiblings: false, skipLevelOfDetail: false, updateTransforms: true, onTraversalEnd: () => { }, viewportTraversersMap: {}, basePath: "" }; var TilesetTraverser = class { options; // fulfill in traverse call root = null; // tiles should be rendered selectedTiles = {}; // tiles should be loaded from server requestedTiles = {}; // tiles does not have render content emptyTiles = {}; lastUpdate = new Date().getTime(); updateDebounceTime = 1e3; /** temporary storage to hold the traversed tiles during a traversal */ _traversalStack = new ManagedArray(); _emptyTraversalStack = new ManagedArray(); /** set in every traverse cycle */ _frameNumber = null; // RESULT traversalFinished(frameState) { return true; } // TODO nested props constructor(options) { this.options = { ...DEFAULT_PROPS, ...options }; } // tiles should be visible traverse(root, frameState, options) { this.root = root; this.options = { ...this.options, ...options }; this.reset(); this.updateTile(root, frameState); this._frameNumber = frameState.frameNumber; this.executeTraversal(root, frameState); } reset() { this.requestedTiles = {}; this.selectedTiles = {}; this.emptyTiles = {}; this._traversalStack.reset(); this._emptyTraversalStack.reset(); } /** * Execute traverse * Depth-first traversal that traverses all visible tiles and marks tiles for selection. * If skipLevelOfDetail is off then a tile does not refine until all children are loaded. * This is the traditional replacement refinement approach and is called the base traversal. * Tiles that have a greater screen space error than the base screen space error are part of the base traversal, * all other tiles are part of the skip traversal. The skip traversal allows for skipping levels of the tree * and rendering children and parent tiles simultaneously. */ /* eslint-disable-next-line complexity, max-statements */ executeTraversal(root, frameState) { const stack = this._traversalStack; root._selectionDepth = 1; stack.push(root); while (stack.length > 0) { const tile = stack.pop(); let shouldRefine = false; if (this.canTraverse(tile, frameState)) { this.updateChildTiles(tile, frameState); shouldRefine = this.updateAndPushChildren(tile, frameState, stack, tile.hasRenderContent ? tile._selectionDepth + 1 : tile._selectionDepth); } const parent = tile.parent; const parentRefines = Boolean(!parent || parent._shouldRefine); const stoppedRefining = !shouldRefine; if (!tile.hasRenderContent) { this.emptyTiles[tile.id] = tile; this.loadTile(tile, frameState); if (stoppedRefining) { this.selectTile(tile, frameState); } } else if (tile.refine === TILE_REFINEMENT.ADD) { this.loadTile(tile, frameState); this.selectTile(tile, frameState); } else if (tile.refine === TILE_REFINEMENT.REPLACE) { this.loadTile(tile, frameState); if (stoppedRefining) { this.selectTile(tile, frameState); } } this.touchTile(tile, frameState); tile._shouldRefine = shouldRefine && parentRefines; } const newTime = new Date().getTime(); if (this.traversalFinished(frameState) || newTime - this.lastUpdate > this.updateDebounceTime) { this.lastUpdate = newTime; this.options.onTraversalEnd(frameState); } } updateChildTiles(tile, frameState) { const children = tile.children; for (const child of children) { this.updateTile(child, frameState); } } /* eslint-disable complexity, max-statements */ updateAndPushChildren(tile, frameState, stack, depth) { const { loadSiblings, skipLevelOfDetail } = this.options; const children = tile.children; children.sort(this.compareDistanceToCamera.bind(this)); const checkRefines = tile.refine === TILE_REFINEMENT.REPLACE && tile.hasRenderContent && !skipLevelOfDetail; let hasVisibleChild = false; let refines = true; for (const child of children) { child._selectionDepth = depth; if (child.isVisibleAndInRequestVolume) { if (stack.find(child)) { stack.delete(child); } stack.push(child); hasVisibleChild = true; } else if (checkRefines || loadSiblings) { this.loadTile(child, frameState); this.touchTile(child, frameState); } if (checkRefines) { let childRefines; if (!child._inRequestVolume) { childRefines = false; } else if (!child.hasRenderContent) { childRefines = this.executeEmptyTraversal(child, frameState); } else { childRefines = child.contentAvailable; } refines = refines && childRefines; if (!refines) { return false; } } } if (!hasVisibleChild) { refines = false; } return refines; } /* eslint-enable complexity, max-statements */ updateTile(tile, frameState) { this.updateTileVisibility(tile, frameState); } // tile to render in the browser selectTile(tile, frameState) { if (this.shouldSelectTile(tile)) { tile._selectedFrame = frameState.frameNumber; this.selectedTiles[tile.id] = tile; } } // tile to load from server loadTile(tile, frameState) { if (this.shouldLoadTile(tile)) { tile._requestedFrame = frameState.frameNumber; tile._priority = tile._getPriority(); this.requestedTiles[tile.id] = tile; } } // cache tile touchTile(tile, frameState) { tile.tileset._cache.touch(tile); tile._touchedFrame = frameState.frameNumber; } // tile should be visible // tile should have children // tile LoD (level of detail) is not sufficient under current viewport canTraverse(tile, frameState) { if (!tile.hasChildren) { return false; } if (tile.hasTilesetContent) { return !tile.contentExpired; } return this.shouldRefine(tile, frameState); } shouldLoadTile(tile) { return tile.hasUnloadedContent || tile.contentExpired; } shouldSelectTile(tile) { return tile.contentAvailable && !this.options.skipLevelOfDetail; } /** Decide if tile LoD (level of detail) is not sufficient under current viewport */ shouldRefine(tile, frameState, useParentMetric = false) { let screenSpaceError = tile._screenSpaceError; if (useParentMetric) { screenSpaceError = tile.getScreenSpaceError(frameState, true); } return screenSpaceError > tile.tileset.memoryAdjustedScreenSpaceError; } updateTileVisibility(tile, frameState) { const viewportIds = []; if (this.options.viewportTraversersMap) { for (const key in this.options.viewportTraversersMap) { const value = this.options.viewportTraversersMap[key]; if (value === frameState.viewport.id) { viewportIds.push(key); } } } else { viewportIds.push(frameState.viewport.id); } tile.updateVisibility(frameState, viewportIds); } // UTILITIES compareDistanceToCamera(b, a) { return b._distanceToCamera - a._distanceToCamera; } anyChildrenVisible(tile, frameState) { let anyVisible = false; for (const child of tile.children) { child.updateVisibility(frameState); anyVisible = anyVisible || child.isVisibleAndInRequestVolume; } return anyVisible; } // Depth-first traversal that checks if all nearest descendants with content are loaded. // Ignores visibility. executeEmptyTraversal(root, frameState) { let allDescendantsLoaded = true; const stack = this._emptyTraversalStack; stack.push(root); while (stack.length > 0) { const tile = stack.pop(); const traverse = !tile.hasRenderContent && this.canTraverse(tile, frameState); const emptyLeaf = !tile.hasRenderContent && tile.children.length === 0; if (!traverse && !tile.contentAvailable && !emptyLeaf) { allDescendantsLoaded = false; } this.updateTile(tile, frameState); if (!tile.isVisibleAndInRequestVolume) { this.loadTile(tile, frameState); this.touchTile(tile, frameState); } if (traverse) { const children = tile.children; for (const child of children) { stack.push(child); } } } return allDescendantsLoaded; } }; // dist/tileset/tile-3d.js var scratchVector3 = new import_core7.Vector3(); function defined2(x) { return x !== void 0 && x !== null; } var Tile3D = class { tileset; header; id; url; parent; /* Specifies the type of refine that is used when traversing this tile for rendering. */ refine; type; contentUrl; /** Different refinement algorithms used by I3S and 3D tiles */ lodMetricType = "geometricError"; /** The error, in meters, introduced if this tile is rendered and its children are not. */ lodMetricValue = 0; /** @todo math.gl is not exporting BoundingVolume base type? */ boundingVolume = null; /** * The tile's content. This represents the actual tile's payload, * not the content's metadata in the tileset JSON file. */ content = null; contentState = TILE_CONTENT_STATE.UNLOADED; gpuMemoryUsageInBytes = 0; /** The tile's children - an array of Tile3D objects. */ children = []; depth = 0; viewportIds = []; transform = new import_core7.Matrix4(); extensions = null; /** TODO Cesium 3d tiles specific */ implicitTiling = null; /** Container to store application specific data */ userData = {}; computedTransform; hasEmptyContent = false; hasTilesetContent = false; traverser = new TilesetTraverser({}); /** Used by TilesetCache */ _cacheNode = null; _frameNumber = null; // TODO Cesium 3d tiles specific _expireDate = null; _expiredContent = null; _boundingBox = void 0; /** updated every frame for tree traversal and rendering optimizations: */ _distanceToCamera = 0; _screenSpaceError = 0; _visibilityPlaneMask; _visible = void 0; _contentBoundingVolume; _viewerRequestVolume; _initialTransform = new import_core7.Matrix4(); // Used by traverser, cannot be marked private _priority = 0; _selectedFrame = 0; _requestedFrame = 0; _selectionDepth = 0; _touchedFrame = 0; _centerZDepth = 0; _shouldRefine = false; _stackLength = 0; _visitedFrame = 0; _inRequestVolume = false; _lodJudge = null; // TODO i3s specific, needs to remove /** * @constructs * Create a Tile3D instance * @param tileset - Tileset3D instance * @param header - tile header - JSON loaded from a dataset * @param parentHeader - parent Tile3D instance * @param extendedId - optional ID to separate copies of a tile for different viewports. * const extendedId = `${tile.id}-${frameState.viewport.id}`; */ // eslint-disable-next-line max-statements constructor(tileset, header, parentHeader, extendedId = "") { this.header = header; this.tileset = tileset; this.id = extendedId || header.id; this.url = header.url; this.parent = parentHeader; this.refine = this._getRefine(header.refine); this.type = header.type; this.contentUrl = header.contentUrl; this._initializeLodMetric(header); this._initializeTransforms(header); this._initializeBoundingVolumes(header); this._initializeContent(header); this._initializeRenderingState(header); Object.seal(this); } destroy() { this.header = null; } isDestroyed() { return this.header === null; } get selected() { return this._selectedFrame === this.tileset._frameNumber; } get isVisible() { return this._visible; } get isVisibleAndInRequestVolume() { return this._visible && this._inRequestVolume; } /** Returns true if tile is not an empty tile and not an external tileset */ get hasRenderContent() { return !this.hasEmptyContent && !this.hasTilesetContent; } /** Returns true if tile has children */ get hasChildren() { return this.children.length > 0 || this.header.children && this.header.children.length > 0; } /** * Determines if the tile's content is ready. This is automatically `true` for * tiles with empty content. */ get contentReady() { return this.contentState === TILE_CONTENT_STATE.READY || this.hasEmptyContent; } /** * Determines if the tile has available content to render. `true` if the tile's * content is ready or if it has expired content this renders while new content loads; otherwise, */ get contentAvailable() { return Boolean(this.contentReady && this.hasRenderContent || this._expiredContent && !this.contentFailed); } /** Returns true if tile has renderable content but it's unloaded */ get hasUnloadedContent() { return this.hasRenderContent && this.contentUnloaded; } /** * Determines if the tile's content has not be requested. `true` if tile's * content has not be requested; otherwise, `false`. */ get contentUnloaded() { return this.contentState === TILE_CONTENT_STATE.UNLOADED; } /** * Determines if the tile's content is expired. `true` if tile's * content is expired; otherwise, `false`. */ get contentExpired() { return this.contentState === TILE_CONTENT_STATE.EXPIRED; } // Determines if the tile's content failed to load. `true` if the tile's // content failed to load; otherwise, `false`. get contentFailed() { return this.contentState === TILE_CONTENT_STATE.FAILED; } /** * Distance from the tile's bounding volume center to the camera */ get distanceToCamera() { return this._distanceToCamera; } /** * Screen space error for LOD selection */ get screenSpaceError() { return this._screenSpaceError; } /** * Get bounding box in cartographic coordinates * @returns [min, max] each in [longitude, latitude, altitude] */ get boundingBox() { if (!this._boundingBox) { this._boundingBox = getCartographicBounds(this.header.boundingVolume, this.boundingVolume); } return this._boundingBox; } /** Get the tile's screen space error. */ getScreenSpaceError(frameState, useParentLodMetric) { switch (this.tileset.type) { case TILESET_TYPE.I3S: return getProjectedRadius(this, frameState); case TILESET_TYPE.TILES3D: return getTiles3DScreenSpaceError(this, frameState, useParentLodMetric); default: throw new Error("Unsupported tileset type"); } } /** * Make tile unselected than means it won't be shown * but it can be still loaded in memory */ unselect() { this._selectedFrame = 0; } /** * Memory usage of tile on GPU */ _getGpuMemoryUsageInBytes() { return this.content.gpuMemoryUsageInBytes || this.content.byteLength || 0; } /* * If skipLevelOfDetail is off try to load child tiles as soon as possible so that their parent can refine sooner. * Tiles are prioritized by screen space error. */ // eslint-disable-next-line complexity _getPriority() { const traverser = this.tileset._traverser; const { skipLevelOfDetail } = traverser.options; const maySkipTile = this.refine === TILE_REFINEMENT.ADD || skipLevelOfDetail; if (maySkipTile && !this.isVisible && this._visible !== void 0) { return -1; } if (this.tileset._frameNumber - this._touchedFrame >= 1) { return -1; } if (this.contentState === TILE_CONTENT_STATE.UNLOADED) { return -1; } const parent = this.parent; const useParentScreenSpaceError = parent && (!maySkipTile || this._screenSpaceError === 0 || parent.hasTilesetContent); const screenSpaceError = useParentScreenSpaceError ? parent._screenSpaceError : this._screenSpaceError; const rootScreenSpaceError = traverser.root ? traverser.root._screenSpaceError : 0; return Math.max(rootScreenSpaceError - screenSpaceError, 0); } /** * Requests the tile's content. * The request may not be made if the Request Scheduler can't prioritize it. */ // eslint-disable-next-line max-statements, complexity async loadContent() { if (this.hasEmptyContent) { return false; } if (this.content) { return true; } const expired = this.contentExpired; if (expired) { this._expireDate = null; } this.contentState = TILE_CONTENT_STATE.LOADING; const requestToken = await this.tileset._requestScheduler.scheduleRequest(this.id, this._getPriority.bind(this)); if (!requestToken) { this.contentState = TILE_CONTENT_STATE.UNLOADED; return false; } try { const contentUrl = this.tileset.getTileUrl(this.contentUrl); const loader = this.tileset.loader; const options = { ...this.tileset.loadOptions, [loader.id]: { // @ts-expect-error ...this.tileset.loadOptions[loader.id], isTileset: this.type === "json", ...this._getLoaderSpecificOptions(loader.id) } }; this.content = await (0, import_core8.load)(contentUrl, loader, options); if (this.tileset.options.contentLoader) { await this.tileset.options.contentLoader(this); } if (this._isTileset()) { this.tileset._initializeTileHeaders(this.content, this); } this.contentState = TILE_CONTENT_STATE.READY; this._onContentLoaded(); return true; } catch (error) { this.contentState = TILE_CONTENT_STATE.FAILED; throw error; } finally { requestToken.done(); } } // Unloads the tile's content. unloadContent() { if (this.content && this.content.destroy) { this.content.destroy(); } this.content = null; if (this.header.content && this.header.content.destroy) { this.header.content.destroy(); } this.header.content = null; this.contentState = TILE_CONTENT_STATE.UNLOADED; return true; } /** * Update the tile's visibility * @param {Object} frameState - frame state for tile culling * @param {string[]} viewportIds - a list of viewport ids that show this tile * @return {void} */ updateVisibility(frameState, viewportIds) { if (this._frameNumber === frameState.frameNumber) { return; } const parent = this.parent; const parentVisibilityPlaneMask = parent ? parent._visibilityPlaneMask : import_culling4.CullingVolume.MASK_INDETERMINATE; if (this.tileset._traverser.options.updateTransforms) { const parentTransform = parent ? parent.computedTransform : this.tileset.modelMatrix; this._updateTransform(parentTransform); } this._distanceToCamera = this.distanceToTile(frameState); this._screenSpaceError = this.getScreenSpaceError(frameState, false); this._visibilityPlaneMask = this.visibility(frameState, parentVisibilityPlaneMask); this._visible = this._visibilityPlaneMask !== import_culling4.CullingVolume.MASK_OUTSIDE; this._inRequestVolume = this.insideViewerRequestVolume(frameState); this._frameNumber = frameState.frameNumber; this.viewportIds = viewportIds; } // Determines whether the tile's bounding volume intersects the culling volume. // @param {FrameState} frameState The frame state. // @param {Number} parentVisibilityPlaneMask The parent's plane mask to speed up the visibility check. // @returns {Number} A plane mask as described above in {@link CullingVolume#computeVisibilityWit