import { Batched3DModel3DTileContent, Cartesian3, Cesium3DTile, Cesium3DTileRefine, Cesium3DTileset, Ellipsoid, HeadingPitchRange, Implicit3DTileContent, ImplicitSubdivisionScheme, ImplicitTileCoordinates, ImplicitTileset, Matrix3, Matrix4, MetadataClass, GroupMetadata, Multiple3DTileContent, Resource, TileBoundingSphere, TileBoundingS2Cell, } from "../../Source/Cesium.js"; import CesiumMath from "../../Source/Core/Math.js"; import ImplicitTilingTester from "../ImplicitTilingTester.js"; import Cesium3DTilesTester from "../Cesium3DTilesTester.js"; import createScene from "../createScene.js"; describe( "Scene/Implicit3DTileContent", function () { var tilesetResource = new Resource({ url: "https://example.com/tileset.json", }); var mockTileset = { modelMatrix: Matrix4.IDENTITY, }; var metadataSchema; // intentionally left undefined var tileJson = { geometricError: 800, refine: "ADD", boundingVolume: { box: [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256], }, content: { uri: "https://example.com/{level}/{x}/{y}.b3dm", extras: { author: "Cesium", }, }, extensions: { "3DTILES_implicit_tiling": { subdivisionScheme: "QUADTREE", subtreeLevels: 2, maximumLevel: 1, subtrees: { uri: "https://example.com/{level}/{x}/{y}.subtree", }, }, }, extras: { year: "2021", }, }; var implicitTileset = new ImplicitTileset( tilesetResource, tileJson, metadataSchema ); var quadtreeBuffer = ImplicitTilingTester.generateSubtreeBuffers({ tileAvailability: { descriptor: "11010", bitLength: 5, isInternal: true, }, contentAvailability: [ { descriptor: "01010", bitLength: 5, isInternal: true, }, ], childSubtreeAvailability: { descriptor: "1111000011110000", bitLength: 16, isInternal: true, }, }).subtreeBuffer; var rootCoordinates = new ImplicitTileCoordinates({ subdivisionScheme: implicitTileset.subdivisionScheme, subtreeLevels: implicitTileset.subtreeLevels, level: 0, x: 0, y: 0, z: 0, }); function gatherTilesPreorder(tile, minLevel, maxLevel, result) { var level = tile.implicitCoordinates.level; if (minLevel <= level && level <= maxLevel) { result.push(tile); } for (var i = 0; i < tile.children.length; i++) { gatherTilesPreorder(tile.children[i], minLevel, maxLevel, result); } } function getBoundingBoxArray(tile) { var boundingBox = tile.boundingVolume.boundingVolume; var box = new Array(12); Cartesian3.pack(boundingBox.center, box); Matrix3.pack(boundingBox.halfAxes, box, 3); return box; } var scene; // This scene is the same as Composite/Composite, just rephrased // using 3DTILES_multiple_contents var centerLongitude = -1.31968; var centerLatitude = 0.698874; beforeAll(function () { scene = createScene(); }); afterAll(function () { scene.destroyForSpecs(); }); var mockPlaceholderTile; beforeEach(function () { mockPlaceholderTile = new Cesium3DTile(mockTileset, tilesetResource, { geometricError: 400, boundingVolume: { box: [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256], }, }); mockPlaceholderTile.implicitCoordinates = rootCoordinates; mockPlaceholderTile.implicitTileset = implicitTileset; // One item in each data set is always located in the center, so point the camera there var center = Cartesian3.fromRadians(centerLongitude, centerLatitude); scene.camera.lookAt(center, new HeadingPitchRange(0.0, -1.57, 26.0)); }); afterEach(function () { scene.primitives.removeAll(); }); it("expands subtree", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { var expectedChildrenCounts = [2, 4, 0, 0, 0, 0, 4, 0, 0, 0, 0]; var tiles = []; var subtreeRootTile = mockPlaceholderTile.children[0]; gatherTilesPreorder(subtreeRootTile, 0, 2, tiles); expect(expectedChildrenCounts.length).toEqual(tiles.length); for (var i = 0; i < tiles.length; i++) { expect(tiles[i].children.length).toEqual(expectedChildrenCounts[i]); } }); }); it("sets tile coordinates on each tile", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { var expectedCoordinates = [ [0, 0, 0], [1, 0, 0], [2, 0, 0], [2, 1, 0], [2, 0, 1], [2, 1, 1], [1, 0, 1], [2, 0, 2], [2, 1, 2], [2, 0, 3], [2, 1, 3], ]; var tiles = []; var subtreeRootTile = mockPlaceholderTile.children[0]; gatherTilesPreorder(subtreeRootTile, 0, 2, tiles); for (var i = 0; i < tiles.length; i++) { var expected = expectedCoordinates[i]; var coordinates = new ImplicitTileCoordinates({ subdivisionScheme: implicitTileset.subdivisionScheme, subtreeLevels: implicitTileset.subtreeLevels, level: expected[0], x: expected[1], y: expected[2], }); expect(tiles[i].implicitCoordinates).toEqual(coordinates); } }); }); it("handles deeper subtrees correctly", function () { mockPlaceholderTile.implicitCoordinates = new ImplicitTileCoordinates({ subdivisionScheme: implicitTileset.subdivisionScheme, subtreeLevels: implicitTileset.subtreeLevels, level: 2, x: 2, y: 1, }); var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); var refine = implicitTileset.refine === "ADD" ? Cesium3DTileRefine.ADD : Cesium3DTileRefine.REPLACE; var parentCoordinates = mockPlaceholderTile.implicitCoordinates; var childCoordinates = parentCoordinates.getChildCoordinates(0); var parentGeometricError = implicitTileset.geometricError / 4; var childGeometricError = implicitTileset.geometricError / 8; var rootBoundingVolume = [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256]; var parentBox = Implicit3DTileContent._deriveBoundingBox( rootBoundingVolume, parentCoordinates.level, parentCoordinates.x, parentCoordinates.y ); var childBox = Implicit3DTileContent._deriveBoundingBox( rootBoundingVolume, childCoordinates.level, childCoordinates.x, childCoordinates.y ); return content.readyPromise.then(function () { var subtreeRootTile = mockPlaceholderTile.children[0]; var childTile = subtreeRootTile.children[0]; expect(subtreeRootTile.implicitCoordinates).toEqual(parentCoordinates); expect(childTile.implicitCoordinates).toEqual(childCoordinates); expect(subtreeRootTile.refine).toEqual(refine); expect(childTile.refine).toEqual(refine); expect(subtreeRootTile.geometricError).toEqual(parentGeometricError); expect(childTile.geometricError).toEqual(childGeometricError); expect(getBoundingBoxArray(subtreeRootTile)).toEqual(parentBox); expect(getBoundingBoxArray(childTile)).toEqual(childBox); }); }); it("puts the root tile inside the placeholder tile", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { expect(mockPlaceholderTile.children.length).toEqual(1); }); }); it("preserves tile extras", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { expect(mockPlaceholderTile.children[0].extras).toEqual(tileJson.extras); }); }); it("stores a reference to the subtree in each transcoded tile", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { expect(mockPlaceholderTile.implicitSubtree).not.toBeDefined(); var subtreeRootTile = mockPlaceholderTile.children[0]; var subtree = subtreeRootTile.implicitSubtree; expect(subtree).toBeDefined(); var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 1, tiles); for (var i = 0; i < tiles.length; i++) { expect(tiles[i].implicitSubtree).toBe(subtree); } }); }); it("does not store references to subtrees in placeholder tiles", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { expect(mockPlaceholderTile.implicitSubtree).not.toBeDefined(); var subtreeRootTile = mockPlaceholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 2, 2, tiles); for (var i = 0; i < tiles.length; i++) { expect(tiles[i].implicitSubtree).not.toBeDefined(); } }); }); it("destroys", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { var subtree = content._implicitSubtree; expect(content.isDestroyed()).toBe(false); expect(subtree.isDestroyed()).toBe(false); content.destroy(); expect(content.isDestroyed()).toBe(true); expect(subtree.isDestroyed()).toBe(true); }); }); it("returns default values for most Cesium3DTileContent properties", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); expect(content.featurePropertiesDirty).toBe(false); expect(content.featuresLength).toBe(0); expect(content.pointsLength).toBe(0); expect(content.trianglesLength).toBe(0); expect(content.geometryByteLength).toBe(0); expect(content.texturesByteLength).toBe(0); expect(content.batchTableByteLength).toBe(0); expect(content.innerContents).not.toBeDefined(); expect(content.tileset).toBe(mockTileset); expect(content.tile).toBe(mockPlaceholderTile); expect(content.batchTable).not.toBeDefined(); }); it("url returns the subtree url", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); expect(content.url).toBe("https://example.com/0/0/0.subtree"); }); it("templates content URIs for each tile with content", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { var expectedCoordinates = [ [0, 0, 0], [1, 0, 0], [1, 0, 1], ]; var contentAvailability = [false, true, true]; var templateUri = implicitTileset.contentUriTemplates[0]; var subtreeRootTile = mockPlaceholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 1, tiles); expect(expectedCoordinates.length).toEqual(tiles.length); for (var i = 0; i < tiles.length; i++) { var expected = expectedCoordinates[i]; var coordinates = new ImplicitTileCoordinates({ subdivisionScheme: implicitTileset.subdivisionScheme, subtreeLevels: implicitTileset.subtreeLevels, level: expected[0], x: expected[1], y: expected[2], }); var expectedResource = templateUri.getDerivedResource({ templateValues: coordinates.getTemplateValues(), }); if (contentAvailability[i]) { expect(tiles[i]._contentResource.url).toEqual(expectedResource.url); } else { expect(tiles[i]._contentResource).not.toBeDefined(); } } }); }); it("constructs placeholder tiles for child subtrees", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { var expectedCoordinates = [ [2, 0, 0], [2, 1, 0], [2, 0, 1], [2, 1, 1], [2, 0, 2], [2, 1, 2], [2, 0, 3], [2, 1, 3], ]; var templateUri = implicitTileset.subtreeUriTemplate; var subtreeRootTile = mockPlaceholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 2, 2, tiles); expect(expectedCoordinates.length).toEqual(tiles.length); for (var i = 0; i < tiles.length; i++) { var expected = expectedCoordinates[i]; var coordinates = new ImplicitTileCoordinates({ subdivisionScheme: implicitTileset.subdivisionScheme, subtreeLevels: implicitTileset.subtreeLevels, level: expected[0], x: expected[1], y: expected[2], }); var expectedResource = templateUri.getDerivedResource({ templateValues: coordinates.getTemplateValues(), }); var placeholderTile = tiles[i]; expect(placeholderTile._contentResource.url).toEqual( expectedResource.url ); expect(placeholderTile.implicitTileset).toBeDefined(); expect(placeholderTile.implicitCoordinates).toBeDefined(); } }); }); it("propagates refine down the tree", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); var refine = implicitTileset.refine === "ADD" ? Cesium3DTileRefine.ADD : Cesium3DTileRefine.REPLACE; return content.readyPromise.then(function () { var subtreeRootTile = mockPlaceholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 2, tiles); for (var i = 0; i < tiles.length; i++) { expect(tiles[i].refine).toEqual(refine); } }); }); it("divides the geometricError by 2 for each level of the tree", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); var rootGeometricError = implicitTileset.geometricError; return content.readyPromise.then(function () { var subtreeRootTile = mockPlaceholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 2, tiles); for (var i = 0; i < tiles.length; i++) { var level = tiles[i].implicitCoordinates.level; expect(tiles[i].geometricError).toEqual( rootGeometricError / Math.pow(2, level) ); } }); }); it("subdivides bounding volumes for each tile", function () { var content = new Implicit3DTileContent( mockTileset, mockPlaceholderTile, tilesetResource, quadtreeBuffer, 0 ); return content.readyPromise.then(function () { var expectedCoordinates = [ [0, 0, 0], [1, 0, 0], [2, 0, 0], [2, 1, 0], [2, 0, 1], [2, 1, 1], [1, 0, 1], [2, 0, 2], [2, 1, 2], [2, 0, 3], [2, 1, 3], ]; var rootBoundingVolume = [0, 0, 0, 256, 0, 0, 0, 256, 0, 0, 0, 256]; var subtreeRootTile = mockPlaceholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 2, tiles); expect(expectedCoordinates.length).toEqual(tiles.length); for (var i = 0; i < tiles.length; i++) { var coordinates = expectedCoordinates[i]; var boundingBox = tiles[i].boundingVolume.boundingVolume; var childBox = new Array(12); Cartesian3.pack(boundingBox.center, childBox); Matrix3.pack(boundingBox.halfAxes, childBox, 3); var expectedBounds = Implicit3DTileContent._deriveBoundingBox( rootBoundingVolume, coordinates[0], coordinates[1], coordinates[2] ); expect(childBox).toEqual(expectedBounds); } }); }); describe("_deriveBoundingVolumeS2", function () { var deriveBoundingVolumeS2 = Implicit3DTileContent._deriveBoundingVolumeS2; var simpleBoundingVolumeS2 = { token: "1", minimumHeight: 0, maximumHeight: 10, }; var simpleBoundingVolumeS2Cell = new TileBoundingS2Cell( simpleBoundingVolumeS2 ); var implicitTilesetS2 = { boundingVolume: { extensions: { "3DTILES_bounding_volume_S2": simpleBoundingVolumeS2, }, }, subdivisionScheme: ImplicitSubdivisionScheme.QUADTREE, }; it("throws if parentIsPlaceholderTile is undefined", function () { expect(function () { deriveBoundingVolumeS2(undefined, {}, 0, 0, 0, 0, 0); }).toThrowDeveloperError(); }); it("throws if parentTile is undefined", function () { expect(function () { deriveBoundingVolumeS2(false, undefined, 0, 0, 0, 0, 0); }).toThrowDeveloperError(); }); it("throws if childIndex is undefined", function () { expect(function () { deriveBoundingVolumeS2(false, {}, undefined, 0, 0, 0, 0); }).toThrowDeveloperError(); }); it("throws if level is undefined", function () { expect(function () { deriveBoundingVolumeS2(false, {}, 0, undefined, 0, 0, 0); }).toThrowDeveloperError(); }); it("throws if x is undefined", function () { expect(function () { deriveBoundingVolumeS2(false, {}, 0, 0, undefined, 0, 0); }).toThrowDeveloperError(); }); it("throws if y is undefined", function () { expect(function () { deriveBoundingVolumeS2(false, {}, 0, 0, 0, undefined, 0); }).toThrowDeveloperError(); }); it("throws if z is defined but not a number", function () { expect(function () { deriveBoundingVolumeS2(false, {}, 0, 0, 0, 0, ""); }).toThrowDeveloperError(); }); it("returns implicit tileset boundingVolume if parentIsPlaceholderTile is true", function () { var placeholderTile = { _boundingVolume: simpleBoundingVolumeS2Cell, }; var result = deriveBoundingVolumeS2( true, placeholderTile, 0, 0, 0, 0, 0 ); expect(result).toEqual(implicitTilesetS2.boundingVolume); expect(result).not.toBe(implicitTilesetS2.boundingVolume); }); it("subdivides correctly using QUADTREE", function () { var parentTile = { _boundingVolume: simpleBoundingVolumeS2Cell, }; var expected = { token: "04", minimumHeight: 0, maximumHeight: 10, }; var result = deriveBoundingVolumeS2(false, parentTile, 0, 1, 0, 0); expect(result).toEqual({ extensions: { "3DTILES_bounding_volume_S2": expected, }, }); parentTile._boundingVolume = new TileBoundingS2Cell({ token: "3", minimumHeight: 0, maximumHeight: 10, }); expected = { token: "24", minimumHeight: 0, maximumHeight: 10, }; result = deriveBoundingVolumeS2(false, parentTile, 0, 1, 0, 0); expect(result).toEqual({ extensions: { "3DTILES_bounding_volume_S2": expected, }, }); }); it("subdivides correctly using OCTREE", function () { implicitTilesetS2.subdivisionScheme = ImplicitSubdivisionScheme.OCTREE; var parentTile = { _boundingVolume: simpleBoundingVolumeS2Cell, }; var expected0 = { token: "04", minimumHeight: 0, maximumHeight: 5, }; var expected1 = { token: "04", minimumHeight: 5, maximumHeight: 10, }; var result0 = deriveBoundingVolumeS2(false, parentTile, 0, 1, 0, 0, 0); expect(result0).toEqual({ extensions: { "3DTILES_bounding_volume_S2": expected0, }, }); var result1 = deriveBoundingVolumeS2(false, parentTile, 4, 1, 0, 0, 0); expect(result1).toEqual({ extensions: { "3DTILES_bounding_volume_S2": expected1, }, }); }); }); describe("_deriveBoundingBox", function () { var deriveBoundingBox = Implicit3DTileContent._deriveBoundingBox; var simpleBoundingBox = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]; it("throws if rootBox is undefined", function () { expect(function () { deriveBoundingBox(undefined, 0, 0, 0); }).toThrowDeveloperError(); }); it("throws if level is undefined", function () { expect(function () { deriveBoundingBox(simpleBoundingBox, undefined, 0, 0); }).toThrowDeveloperError(); }); it("throws if x is undefined", function () { expect(function () { deriveBoundingBox(simpleBoundingBox, 1, undefined, 0); }).toThrowDeveloperError(); }); it("throws if y is undefined", function () { expect(function () { deriveBoundingBox(simpleBoundingBox, 1, 0, undefined); }).toThrowDeveloperError(); }); it("subdivides like a quadtree if z is not given", function () { var tile = [1, 3, 0, 4, 0, 0, 0, 2, 0, 0, 0, 1]; var expected = [0, 1.5, 0, 1, 0, 0, 0, 0.5, 0, 0, 0, 1]; var result = deriveBoundingBox(tile, 2, 1, 0); expect(result).toEqual(expected); }); it("subdivides like an octree if z is given", function () { var tile = [1, 3, 0, 4, 0, 0, 0, 2, 0, 0, 0, 1]; var expected = [0, 1.5, 0.25, 1, 0, 0, 0, 0.5, 0, 0, 0, 0.25]; var result = deriveBoundingBox(tile, 2, 1, 0, 2); expect(result).toEqual(expected); }); it("handles rotation and non-uniform scaling correctly", function () { var tile = [1, 2, 3, 0, 4, 0, 0, 0, 2, 1, 0, 0]; var expected = [1.25, 1, 1.5, 0, 1, 0, 0, 0, 0.5, 0.25, 0, 0]; var result = deriveBoundingBox(tile, 2, 1, 0, 2); expect(result).toEqual(expected); }); }); describe("_deriveBoundingRegion", function () { var deriveBoundingRegion = Implicit3DTileContent._deriveBoundingRegion; var simpleRegion = [ 0, 0, CesiumMath.PI_OVER_FOUR, CesiumMath.PI_OVER_FOUR, 0, 100, ]; it("throws if rootRegion is undefined", function () { expect(function () { deriveBoundingRegion(undefined, 1, 0, 0); }).toThrowDeveloperError(); }); it("throws if level is undefined", function () { expect(function () { deriveBoundingRegion(simpleRegion, undefined, 0, 0); }).toThrowDeveloperError(); }); it("throws if x is undefined", function () { expect(function () { deriveBoundingRegion(simpleRegion, 1, undefined, 0); }).toThrowDeveloperError(); }); it("throws if y is undefined", function () { expect(function () { deriveBoundingRegion(simpleRegion, 1, 0, undefined); }).toThrowDeveloperError(); }); function makeRectangle( westDegrees, southDegrees, eastDegrees, northDegrees, minimumHeight, maximumHeight ) { return [ CesiumMath.toRadians(westDegrees), CesiumMath.toRadians(southDegrees), CesiumMath.toRadians(eastDegrees), CesiumMath.toRadians(northDegrees), minimumHeight, maximumHeight, ]; } it("subdivides like a quadtree if z is not given", function () { // 4x4 degree rectangle so the subdivisions at level 2 will // be 1 degree each var tile = makeRectangle(-1, -2, 3, 2, 0, 20); var expected = makeRectangle(0, 1, 1, 2, 0, 20); var result = deriveBoundingRegion(tile, 2, 1, 3); expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON9); }); it("deriveVolume subdivides like an octree if z is given", function () { // 4x4 degree rectangle so the subdivisions at level 2 will // be 1 degree each var tile = makeRectangle(-1, -2, 3, 2, 0, 20); var expected = makeRectangle(0, 1, 1, 2, 10, 15); var result = deriveBoundingRegion(tile, 2, 1, 3, 2); expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON9); }); it("handles the IDL", function () { var tile = makeRectangle(90, -45, -90, 45, 0, 20); var expected = makeRectangle(180, -45, -90, 0, 0, 20); var result = deriveBoundingRegion(tile, 1, 1, 0); expect(result).toEqualEpsilon(expected, CesiumMath.EPSILON9); }); }); describe("3DTILES_multiple_contents", function () { var implicitMultipleContentsUrl = "Data/Cesium3DTiles/Implicit/ImplicitMultipleContents/tileset.json"; it("a single content is transcoded as a regular tile", function () { return Cesium3DTilesTester.loadTileset( scene, implicitMultipleContentsUrl ).then(function (tileset) { // The root tile of this tileset only has one available content var transcodedRoot = tileset.root.children[0]; var transcodedRootHeader = transcodedRoot._header; expect(transcodedRoot.content).toBeInstanceOf( Batched3DModel3DTileContent ); expect(transcodedRootHeader.content).toEqual({ uri: "ground/0/0/0.b3dm", }); expect(transcodedRootHeader.extensions).not.toBeDefined(); }); }); it("multiple contents are transcoded to a tile with a 3DTILES_multiple_contents extension", function () { return Cesium3DTilesTester.loadTileset( scene, implicitMultipleContentsUrl ).then(function (tileset) { var childTiles = tileset.root.children[0].children; for (var i = 0; i < childTiles.length; i++) { var childTile = childTiles[i]; var content = childTile.content; expect(content).toBeInstanceOf(Multiple3DTileContent); var childTileHeader = childTile._header; expect(childTileHeader.content).not.toBeDefined(); } }); }); it("passes extensions through correctly", function () { var originalLoadJson = Cesium3DTileset.loadJson; var metadataExtension = { group: "buildings", }; var otherExtension = { someKey: "someValue", }; spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) { return originalLoadJson(tilesetUrl).then(function (tilesetJson) { var multiContent = tilesetJson.root.extensions["3DTILES_multiple_contents"]; multiContent.content.forEach(function (content) { content.extensions = { "3DTILES_metadata": metadataExtension, }; }); tilesetJson.root.extensions["3DTILES_extension"] = otherExtension; return tilesetJson; }); }); return Cesium3DTilesTester.loadTileset( scene, implicitMultipleContentsUrl ).then(function (tileset) { // the placeholder tile does not have any extensions. var placeholderTile = tileset.root; var placeholderHeader = placeholderTile._header; expect(placeholderHeader.extensions).not.toBeDefined(); expect(placeholderHeader.content.extensions).not.toBeDefined(); var transcodedRoot = placeholderTile.children[0]; var transcodedRootHeader = transcodedRoot._header; expect(transcodedRootHeader.extensions).toEqual({ "3DTILES_extension": otherExtension, }); expect(transcodedRootHeader.content.extensions).toEqual({ "3DTILES_metadata": metadataExtension, }); var childTiles = transcodedRoot.children; for (var i = 0; i < childTiles.length; i++) { var childTile = childTiles[i]; var childTileHeader = childTile._header; expect(childTileHeader.extensions["3DTILES_extension"]).toEqual( otherExtension ); var innerContentHeaders = childTileHeader.extensions["3DTILES_multiple_contents"].content; innerContentHeaders.forEach(function (header) { expect(header.extensions).toEqual({ "3DTILES_metadata": metadataExtension, }); }); } }); }); }); describe("3DTILES_metadata", function () { var implicitTilesetUrl = "Data/Cesium3DTiles/Implicit/ImplicitTileset/tileset.json"; var implicitGroupMetadataUrl = "Data/Cesium3DTiles/Metadata/ImplicitGroupMetadata/tileset.json"; var implicitHeightSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/tileset.json"; var implicitS2HeightSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitHeightSemantics/s2-tileset.json"; var implicitTileBoundingVolumeSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitTileBoundingVolumeSemantics/tileset.json"; var implicitHeightAndSphereSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitHeightAndSphereSemantics/tileset.json"; var implicitHeightAndRegionSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitHeightAndRegionSemantics/tileset.json"; var implicitContentBoundingVolumeSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitContentBoundingVolumeSemantics/tileset.json"; var implicitContentHeightSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitContentHeightSemantics/tileset.json"; var implicitContentHeightAndRegionSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitContentHeightAndRegionSemantics/tileset.json"; var implicitGeometricErrorSemanticsUrl = "Data/Cesium3DTiles/Metadata/ImplicitGeometricErrorSemantics/tileset.json"; var metadataClass = new MetadataClass({ id: "test", class: { properties: { name: { componentType: "STRING", }, height: { componentType: "FLOAT32", }, }, }, }); var groupMetadata = new GroupMetadata({ id: "testGroup", group: { properties: { name: "Test Group", height: 35.6, }, }, class: metadataClass, }); it("assigns groupMetadata", function () { return Cesium3DTilesTester.loadTileset(scene, implicitTilesetUrl).then( function (tileset) { var content = tileset.root.content; content.groupMetadata = groupMetadata; expect(content.groupMetadata).toBe(groupMetadata); } ); }); it("group metadata gets transcoded correctly", function () { return Cesium3DTilesTester.loadTileset( scene, implicitGroupMetadataUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 2, tiles); var groups = tileset.metadata.groups; var ground = groups.ground; expect(ground.getProperty("color")).toEqual( new Cartesian3(120, 68, 32) ); expect(ground.getProperty("priority")).toBe(0); var sky = groups.sky; expect(sky.getProperty("color")).toEqual( new Cartesian3(206, 237, 242) ); expect(sky.getProperty("priority")).toBe(1); tiles.forEach(function (tile) { if (tile.hasMultipleContents) { // child tiles have multiple contents var contents = tile.content.innerContents; expect(contents[0].groupMetadata).toBe(ground); expect(contents[1].groupMetadata).toBe(sky); } else { // parent tile is a single B3DM tile expect(tile.content.groupMetadata).toBe(ground); } }); }); }); // view (lon, lat, height) = (0, 0, 0) from height meters above function viewCartographicOrigin(height) { var center = Cartesian3.fromDegrees(0.0, 0.0); var offset = new Cartesian3(0, 0, height); scene.camera.lookAt(center, offset); } it("uses height semantics to adjust region bounding volumes", function () { viewCartographicOrigin(10000); return Cesium3DTilesTester.loadTileset( scene, implicitHeightSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var implicitRegion = placeholderTile.implicitTileset.boundingVolume.region; var minimumHeight = implicitRegion[4]; var maximumHeight = implicitRegion[5]; // This tileset uses TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT // to set tighter bounding volumes var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); for (var i = 0; i < tiles.length; i++) { var tileRegion = tiles[i].boundingVolume; expect(tileRegion.minimumHeight).toBeGreaterThan(minimumHeight); expect(tileRegion.maximumHeight).toBeLessThan(maximumHeight); } }); }); it("uses height semantics to adjust S2 bounding volumes", function () { viewCartographicOrigin(10000); return Cesium3DTilesTester.loadTileset( scene, implicitS2HeightSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var implicitS2Volume = placeholderTile.implicitTileset.boundingVolume.extensions[ "3DTILES_bounding_volume_S2" ]; var minimumHeight = implicitS2Volume.minimumHeight; var maximumHeight = implicitS2Volume.maximumHeight; // This tileset uses TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT // to set tighter bounding volumes var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); for (var i = 0; i < tiles.length; i++) { var tileS2Volume = tiles[i].boundingVolume; expect(tileS2Volume.minimumHeight).toBeGreaterThan(minimumHeight); expect(tileS2Volume.maximumHeight).toBeLessThan(maximumHeight); } }); }); // get half the bounding cube width from the bounding box's // halfAxes matrix function getHalfWidth(boundingBox) { return boundingBox.boundingVolume.halfAxes[0]; } it("ignores height semantics if the implicit volume is a box", function () { var cameraHeight = 100; var rootHalfWidth = 10; var originalLoadJson = Cesium3DTileset.loadJson; spyOn(Cesium3DTileset, "loadJson").and.callFake(function (tilesetUrl) { return originalLoadJson(tilesetUrl).then(function (tilesetJson) { tilesetJson.root.boundingVolume = { box: [ Ellipsoid.WGS84.radii.x + cameraHeight, 0, 0, rootHalfWidth, 0, 0, 0, rootHalfWidth, 0, 0, 0, rootHalfWidth, ], }; return tilesetJson; }); }); viewCartographicOrigin(cameraHeight); return Cesium3DTilesTester.loadTileset( scene, implicitHeightSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); // TILE_MINIMUM_HEIGHT and TILE_MAXIMUM_HEIGHT only apply to // regions, so this will check that they are not used. tiles.forEach(function (tile) { var level = tile.implicitCoordinates.level; var halfWidth = getHalfWidth(tile.boundingVolume); // Even for floats, divide by 2 operations are exact as long // as there is no overflow. expect(halfWidth).toEqual(rootHalfWidth / Math.pow(2, level)); }); }); }); it("uses tile bounding box from metadata semantics if present", function () { viewCartographicOrigin(124000); return Cesium3DTilesTester.loadTileset( scene, implicitTileBoundingVolumeSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root.children[0]; var subtreeRootTile = placeholderTile.children[0]; var rootHalfWidth = 2048; expect(getHalfWidth(subtreeRootTile.boundingVolume)).toBe( rootHalfWidth ); for (var level = 1; level < 4; level++) { var halfWidthAtLevel = rootHalfWidth / (1 << level); var tiles = []; gatherTilesPreorder(subtreeRootTile, level, level, tiles); for (var i = 0; i < tiles.length; i++) { // In this tileset, each tile's TILE_BOUNDING_BOX is // smaller than the implicit tile bounds. Make sure // this is true. var tile = tiles[i]; var halfWidth = getHalfWidth(tile.boundingVolume); expect(halfWidth).toBeLessThan(halfWidthAtLevel); } } }); }); it("prioritizes height semantics over bounding volume semantics", function () { viewCartographicOrigin(10000); return Cesium3DTilesTester.loadTileset( scene, implicitHeightAndSphereSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var implicitRegion = placeholderTile.implicitTileset.boundingVolume.region; var minimumHeight = implicitRegion[4]; var maximumHeight = implicitRegion[5]; // This tileset uses TILE_BOUNDING_SPHERE, TILE_MINIMUM_HEIGHT, and // TILE_MAXIMUM_HEIGHT but TILE_BOUNDING_SPHERE is ignored var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); for (var i = 0; i < tiles.length; i++) { var tileRegion = tiles[i].boundingVolume; expect(tileRegion.minimumHeight).toBeDefined(); expect(tileRegion.maximumHeight).toBeDefined(); expect(tileRegion.minimumHeight).toBeGreaterThan(minimumHeight); expect(tileRegion.maximumHeight).toBeLessThan(maximumHeight); } }); }); it("uses height semantics to adjust region semantic", function () { viewCartographicOrigin(10000); return Cesium3DTilesTester.loadTileset( scene, implicitHeightAndRegionSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var implicitRegion = placeholderTile.implicitTileset.boundingVolume.region; var west = implicitRegion[0]; var south = implicitRegion[1]; var east = implicitRegion[2]; var north = implicitRegion[3]; var minimumHeight = implicitRegion[4]; var maximumHeight = implicitRegion[5]; // This tileset uses TILE_BOUNDING_REGION, TILE_MINIMUM_HEIGHT, and // TILE_MAXIMUM_HEIGHT to set tighter bounding volumes var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); for (var i = 0; i < tiles.length; i++) { var tileRegion = tiles[i].boundingVolume; expect(tileRegion.minimumHeight).toBeGreaterThan(minimumHeight); expect(tileRegion.maximumHeight).toBeLessThan(maximumHeight); // Check that the bounding volume is using the explicit tile regions // which are shrunken compared to the implicit tile regions expect(tileRegion.rectangle.west).toBeGreaterThan(west); expect(tileRegion.rectangle.south).toBeGreaterThan(south); expect(tileRegion.rectangle.east).toBeLessThan(east); expect(tileRegion.rectangle.north).toBeLessThan(north); } }); }); it("uses content bounding box from metadata semantics if present", function () { viewCartographicOrigin(124000); return Cesium3DTilesTester.loadTileset( scene, implicitContentBoundingVolumeSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root.children[0]; var subtreeRootTile = placeholderTile.children[0]; // This tileset defines the content bounding spheres in a // property with metadata semantic CONTENT_BOUNDING_SPHERE. // Check that each tile has a content bounding volume. var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); tiles.forEach(function (tile) { expect( tile.contentBoundingVolume instanceof TileBoundingSphere ).toBe(true); expect(tile.contentBoundingVolume).not.toBe(tile.boundingVolume); }); }); }); it("uses content height semantics to adjust implicit region", function () { viewCartographicOrigin(10000); return Cesium3DTilesTester.loadTileset( scene, implicitContentHeightSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var implicitRegion = placeholderTile.implicitTileset.boundingVolume.region; var minimumHeight = implicitRegion[4]; var maximumHeight = implicitRegion[5]; // This tileset uses CONTENT_MINIMUM_HEIGHT and CONTENT_MAXIMUM_HEIGHT // to set tighter bounding volumes var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); for (var i = 0; i < tiles.length; i++) { var contentRegion = tiles[i].contentBoundingVolume; expect(contentRegion.minimumHeight).toBeGreaterThan(minimumHeight); expect(contentRegion.maximumHeight).toBeLessThan(maximumHeight); } }); }); it("uses content height semantics to adjust content region semantic", function () { viewCartographicOrigin(10000); return Cesium3DTilesTester.loadTileset( scene, implicitContentHeightAndRegionSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; var implicitRegion = placeholderTile.implicitTileset.boundingVolume.region; var west = implicitRegion[0]; var south = implicitRegion[1]; var east = implicitRegion[2]; var north = implicitRegion[3]; var minimumHeight = implicitRegion[4]; var maximumHeight = implicitRegion[5]; // This tileset uses CONTENT_BOUNDING_REGION, CONTENT_MINIMUM_HEIGHT, and // CONTENT_MAXIMUM_HEIGHT to set tighter bounding volumes var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); for (var i = 0; i < tiles.length; i++) { var contentRegion = tiles[i].contentBoundingVolume; expect(contentRegion.minimumHeight).toBeGreaterThan(minimumHeight); expect(contentRegion.maximumHeight).toBeLessThan(maximumHeight); // Check that the content bounding volume is using the explicit tile // regions which are shrunken compared to the implicit tile regions expect(contentRegion.rectangle.west).toBeGreaterThan(west); expect(contentRegion.rectangle.south).toBeGreaterThan(south); expect(contentRegion.rectangle.east).toBeLessThan(east); expect(contentRegion.rectangle.north).toBeLessThan(north); } }); }); it("uses geometric error from metadata semantics if present", function () { viewCartographicOrigin(10000); return Cesium3DTilesTester.loadTileset( scene, implicitGeometricErrorSemanticsUrl ).then(function (tileset) { var placeholderTile = tileset.root; var subtreeRootTile = placeholderTile.children[0]; // This tileset defines the geometric error in a // property with metadata semantic TILE_GEOMETRIC_ERROR. // Check that each tile has the right geometric error. var tiles = []; gatherTilesPreorder(subtreeRootTile, 0, 3, tiles); var geometricErrors = tiles.map(function (tile) { return tile.geometricError; }); // prettier-ignore var expectedGeometricErrors = [ 300, 203, 112, 113, 114, 115, 201, 104, 105, 106, 107, 202, 108, 109, 110, 111, 200, 103, 101, 102, 100 ]; expect(geometricErrors).toEqual(expectedGeometricErrors); }); }); }); }, "WebGL" );