diff --git a/specs/data/combineTilesets/externalInSameDirectory/README.md b/specs/data/combineTilesets/externalInSameDirectory/README.md new file mode 100644 index 0000000..865db03 --- /dev/null +++ b/specs/data/combineTilesets/externalInSameDirectory/README.md @@ -0,0 +1,18 @@ +A regression test for https://github.com/CesiumGS/3d-tiles-tools/issues/138 + +``` +tileset.json refers to + sub0/externalA.json + +sub0/externalA.json refers to + tileA.b3dm + externalB.json + +sub0/externalB.json refers to + tileB.b3dm +``` + +The content URI for `tileB.b3dm` of the combined result should be `sub0/tileB.b3dm` + + + \ No newline at end of file diff --git a/specs/data/combineTilesets/externalInSameDirectory/sub0/externalA.json b/specs/data/combineTilesets/externalInSameDirectory/sub0/externalA.json new file mode 100644 index 0000000..dedb655 --- /dev/null +++ b/specs/data/combineTilesets/externalInSameDirectory/sub0/externalA.json @@ -0,0 +1,32 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "children": [ + { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 1.0, + "content": { + "uri": "tileA.b3dm" + } + }, + { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 1.0, + "content": { + "uri": "externalB.json" + } + } + ] + } +} diff --git a/specs/data/combineTilesets/externalInSameDirectory/sub0/externalB.json b/specs/data/combineTilesets/externalInSameDirectory/sub0/externalB.json new file mode 100644 index 0000000..ce6f5bb --- /dev/null +++ b/specs/data/combineTilesets/externalInSameDirectory/sub0/externalB.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "children": [ + { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 1.0, + "content": { + "uri": "tileB.b3dm" + } + } + ] + } +} diff --git a/specs/data/combineTilesets/externalInSameDirectory/sub0/tileA.b3dm b/specs/data/combineTilesets/externalInSameDirectory/sub0/tileA.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalInSameDirectory/sub0/tileB.b3dm b/specs/data/combineTilesets/externalInSameDirectory/sub0/tileB.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalInSameDirectory/tileset.json b/specs/data/combineTilesets/externalInSameDirectory/tileset.json new file mode 100644 index 0000000..5324623 --- /dev/null +++ b/specs/data/combineTilesets/externalInSameDirectory/tileset.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "children": [ + { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 1.0, + "content": { + "uri": "sub0/externalA.json" + } + } + ] + } +} diff --git a/specs/data/combineTilesets/externalMultipleContents/README.md b/specs/data/combineTilesets/externalMultipleContents/README.md new file mode 100644 index 0000000..715b22c --- /dev/null +++ b/specs/data/combineTilesets/externalMultipleContents/README.md @@ -0,0 +1,28 @@ +A test for the handling of multiple contents in the `combine` command +(Brought up in https://github.com/CesiumGS/3d-tiles-tools/issues/138 ) + +The cases that are differentiated here: + +- A tile refers to a SINGLE content, which is an external tileset + - In this case, the properties of the tile will be "replaced" with the properties of the external root +- A tile refers to an multiple contents, including external tilesets + - In this case, the external roots will be added as children to the tile + +--- + +- The `tileset.json` root + - No children + - Multiple contents: `tile.b3dm`, `/subA/externalA.json`, `/subB/externalB.json` +- The result root should have one content and two children + +- The `/subA/externalA.json` root: + - Multiple contents: `tileA0.b3dm`, `tileA1.b3dm` + - No content +- This should be the first child of the result root + +- The `/subB/externalB.json` root: + - A single content: `externalB0.json` + - No children +- The `externalB0.json` contains two contents, `tileB0x.b3dm` and `tileB0y.b3dm`. +- These should be put into root of the `/subB/externalB.json` +- This will be the second child of the result root diff --git a/specs/data/combineTilesets/externalMultipleContents/subA/externalA.json b/specs/data/combineTilesets/externalMultipleContents/subA/externalA.json new file mode 100644 index 0000000..e1ce060 --- /dev/null +++ b/specs/data/combineTilesets/externalMultipleContents/subA/externalA.json @@ -0,0 +1,20 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "contents": [ + { + "uri": "tileA0.b3dm" + }, + { + "uri": "tileA1.b3dm" + } + ] + } +} diff --git a/specs/data/combineTilesets/externalMultipleContents/subA/tileA0.b3dm b/specs/data/combineTilesets/externalMultipleContents/subA/tileA0.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalMultipleContents/subA/tileA1.b3dm b/specs/data/combineTilesets/externalMultipleContents/subA/tileA1.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalMultipleContents/subB/externalB.json b/specs/data/combineTilesets/externalMultipleContents/subB/externalB.json new file mode 100644 index 0000000..18498c9 --- /dev/null +++ b/specs/data/combineTilesets/externalMultipleContents/subB/externalB.json @@ -0,0 +1,15 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "content": { + "uri": "externalB0.json" + } + } +} diff --git a/specs/data/combineTilesets/externalMultipleContents/subB/externalB0.json b/specs/data/combineTilesets/externalMultipleContents/subB/externalB0.json new file mode 100644 index 0000000..886e0a7 --- /dev/null +++ b/specs/data/combineTilesets/externalMultipleContents/subB/externalB0.json @@ -0,0 +1,20 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "contents": [ + { + "uri": "tileB0x.b3dm" + }, + { + "uri": "tileB0y.b3dm" + } + ] + } +} diff --git a/specs/data/combineTilesets/externalMultipleContents/subB/tileB0x.b3dm b/specs/data/combineTilesets/externalMultipleContents/subB/tileB0x.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalMultipleContents/subB/tileB0y.b3dm b/specs/data/combineTilesets/externalMultipleContents/subB/tileB0y.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalMultipleContents/tile.b3dm b/specs/data/combineTilesets/externalMultipleContents/tile.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalMultipleContents/tileset.json b/specs/data/combineTilesets/externalMultipleContents/tileset.json new file mode 100644 index 0000000..0470808 --- /dev/null +++ b/specs/data/combineTilesets/externalMultipleContents/tileset.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "1.1" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "contents": [ + { + "uri": "tile.b3dm" + }, + { + "uri": "subA/externalA.json" + }, + { + "uri": "subB/externalB.json" + } + ] + } +} diff --git a/specs/data/combineTilesets/externalWithUrl/README.md b/specs/data/combineTilesets/externalWithUrl/README.md new file mode 100644 index 0000000..e896c30 --- /dev/null +++ b/specs/data/combineTilesets/externalWithUrl/README.md @@ -0,0 +1,5 @@ +A test for the `combine` functionality based on a tileset that uses +the (legacy) `url` property for its contents. + +See https://github.com/CesiumGS/3d-tiles-tools/issues/43 + diff --git a/specs/data/combineTilesets/externalWithUrl/external.json b/specs/data/combineTilesets/externalWithUrl/external.json new file mode 100644 index 0000000..fce558a --- /dev/null +++ b/specs/data/combineTilesets/externalWithUrl/external.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "1.0" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "children": [ + { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 1.0, + "content": { + "url": "tile.b3dm" + } + } + ] + } +} diff --git a/specs/data/combineTilesets/externalWithUrl/tile.b3dm b/specs/data/combineTilesets/externalWithUrl/tile.b3dm new file mode 100644 index 0000000..e69de29 diff --git a/specs/data/combineTilesets/externalWithUrl/tileset.json b/specs/data/combineTilesets/externalWithUrl/tileset.json new file mode 100644 index 0000000..f701527 --- /dev/null +++ b/specs/data/combineTilesets/externalWithUrl/tileset.json @@ -0,0 +1,23 @@ +{ + "asset": { + "version": "1.0" + }, + "geometricError": 4.0, + "root": { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 2.0, + "children": [ + { + "boundingVolume": { + "box": [0.5, 0.5, 0.5, 0.5, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, 0.5] + }, + "geometricError": 1.0, + "content": { + "url": "external.json" + } + } + ] + } +} diff --git a/specs/tools/tilesetProcessing/TilesetCombinerSpec.ts b/specs/tools/tilesetProcessing/TilesetCombinerSpec.ts index f4d4860..a6f0e71 100644 --- a/specs/tools/tilesetProcessing/TilesetCombinerSpec.ts +++ b/specs/tools/tilesetProcessing/TilesetCombinerSpec.ts @@ -7,17 +7,9 @@ import { TilesetOperations } from "../../../src/tools"; import { SpecHelpers } from "../../SpecHelpers"; const SPECS_DATA_BASE_DIRECTORY = SpecHelpers.getSpecsDataBaseDirectory(); - -const nestedExteralInput = - SPECS_DATA_BASE_DIRECTORY + "/combineTilesets/nestedExternal"; -const nestedExteralOutput = - SPECS_DATA_BASE_DIRECTORY + "/output/combineTilesets/nestedExternal"; - -const externalsWithTransformInput = - SPECS_DATA_BASE_DIRECTORY + "/combineTilesets/externalsWithTransform"; -const externalsWithTransformOutput = - SPECS_DATA_BASE_DIRECTORY + "/output/combineTilesets/externalsWithTransform"; - +const INPUT_BASE_DIRECTORY = SPECS_DATA_BASE_DIRECTORY + "/combineTilesets/"; +const OUTPUT_BASE_DIRECTORY = + SPECS_DATA_BASE_DIRECTORY + "/output/combineTilesets/"; const overwrite = true; describe("TilesetCombiner", function () { @@ -27,17 +19,16 @@ describe("TilesetCombiner", function () { ); }); - it("combines external tilesets into a single tileset", async function () { - await TilesetOperations.combine( - nestedExteralInput, - nestedExteralOutput, - overwrite - ); + it("combines external tilesets into a single tileset in nestedExternal", async function () { + const testDirectoryName = "nestedExternal"; + const input = INPUT_BASE_DIRECTORY + testDirectoryName; + const output = OUTPUT_BASE_DIRECTORY + testDirectoryName; + + await TilesetOperations.combine(input, output, overwrite); // Ensure that the output directory contains the expected files: // All files of the input, except for the external tileset JSON files - const actualRelativeFiles = - SpecHelpers.collectRelativeFileNames(nestedExteralOutput); + const actualRelativeFiles = SpecHelpers.collectRelativeFileNames(output); actualRelativeFiles.sort(); const expectedRelativeFiles = [ "README.md", @@ -54,7 +45,7 @@ describe("TilesetCombiner", function () { // Ensure that the single 'tileset.json' contains the // proper content URIs for the combined output const tilesetJsonBuffer = fs.readFileSync( - Paths.join(nestedExteralOutput, "tileset.json") + Paths.join(output, "tileset.json") ); const tileset = JSON.parse(tilesetJsonBuffer.toString()); const actualContentUris = await SpecHelpers.collectExplicitContentUris( @@ -73,17 +64,17 @@ describe("TilesetCombiner", function () { expect(actualContentUris).toEqual(expectedContentUris); }); - it("retains the transforms of root nodes of external tilesets", async function () { - await TilesetOperations.combine( - externalsWithTransformInput, - externalsWithTransformOutput, - overwrite - ); + it("retains the transforms of root nodes of external tilesets in externalsWithTransform", async function () { + const testDirectoryName = "externalsWithTransform"; + const input = INPUT_BASE_DIRECTORY + testDirectoryName; + const output = OUTPUT_BASE_DIRECTORY + testDirectoryName; + + await TilesetOperations.combine(input, output, overwrite); // Ensure that the resulting tileset JSON contains the // proper content transforms and bounding volumes const tilesetJsonBuffer = fs.readFileSync( - Paths.join(externalsWithTransformOutput, "tileset.json") + Paths.join(output, "tileset.json") ); const tileset = JSON.parse(tilesetJsonBuffer.toString()); @@ -122,4 +113,110 @@ describe("TilesetCombiner", function () { const childTransformB = tileset.root.children[2].transform; expect(childTransformB).toEqual(expectedTransformB); }); + + it("generates proper content URIs for externalInSameDirectory", async function () { + const testDirectoryName = "externalInSameDirectory"; + const input = INPUT_BASE_DIRECTORY + testDirectoryName; + const output = OUTPUT_BASE_DIRECTORY + testDirectoryName; + + await TilesetOperations.combine(input, output, overwrite); + + // Ensure that the output directory contains the expected files: + // All files of the input, except for the external tileset JSON files + const actualRelativeFiles = SpecHelpers.collectRelativeFileNames(output); + actualRelativeFiles.sort(); + const expectedRelativeFiles = [ + "README.md", + "sub0/tileA.b3dm", + "sub0/tileB.b3dm", + "tileset.json", + ]; + expect(actualRelativeFiles).toEqual(expectedRelativeFiles); + + // Ensure that the single 'tileset.json' contains the + // proper content URIs for the combined output + const tilesetJsonBuffer = fs.readFileSync( + Paths.join(output, "tileset.json") + ); + const tileset = JSON.parse(tilesetJsonBuffer.toString()); + const actualContentUris = await SpecHelpers.collectExplicitContentUris( + tileset.root + ); + actualContentUris.sort(); + + const expectedContentUris = ["sub0/tileA.b3dm", "sub0/tileB.b3dm"]; + expect(actualContentUris).toEqual(expectedContentUris); + }); + + it("handles multiple contents with external tilesets in externalMultipleContents", async function () { + const testDirectoryName = "externalMultipleContents"; + const input = INPUT_BASE_DIRECTORY + testDirectoryName; + const output = OUTPUT_BASE_DIRECTORY + testDirectoryName; + + await TilesetOperations.combine(input, output, overwrite); + + // Ensure that the output directory contains the expected files: + // All files of the input, except for the external tileset JSON files + const actualRelativeFiles = SpecHelpers.collectRelativeFileNames(output); + actualRelativeFiles.sort(); + const expectedRelativeFiles = [ + "README.md", + "subA/tileA0.b3dm", + "subA/tileA1.b3dm", + "subB/tileB0x.b3dm", + "subB/tileB0y.b3dm", + "tile.b3dm", + "tileset.json", + ]; + expect(actualRelativeFiles).toEqual(expectedRelativeFiles); + + // Ensure that the single 'tileset.json' contains the + // proper content URIs for the combined output + const tilesetJsonBuffer = fs.readFileSync( + Paths.join(output, "tileset.json") + ); + const tileset = JSON.parse(tilesetJsonBuffer.toString()); + const actualContentUris = await SpecHelpers.collectExplicitContentUris( + tileset.root + ); + actualContentUris.sort(); + + const expectedContentUris = [ + "subA/tileA0.b3dm", + "subA/tileA1.b3dm", + "subB/tileB0x.b3dm", + "subB/tileB0y.b3dm", + "tile.b3dm", + ]; + expect(actualContentUris).toEqual(expectedContentUris); + }); + + it("handles legacy 'url' properties for content", async function () { + const testDirectoryName = "externalWithUrl"; + const input = INPUT_BASE_DIRECTORY + testDirectoryName; + const output = OUTPUT_BASE_DIRECTORY + testDirectoryName; + + await TilesetOperations.combine(input, output, overwrite); + + // Ensure that the output directory contains the expected files: + // All files of the input, except for the external tileset JSON files + const actualRelativeFiles = SpecHelpers.collectRelativeFileNames(output); + actualRelativeFiles.sort(); + const expectedRelativeFiles = ["README.md", "tile.b3dm", "tileset.json"]; + expect(actualRelativeFiles).toEqual(expectedRelativeFiles); + + // Ensure that the single 'tileset.json' contains the + // proper content URIs for the combined output + const tilesetJsonBuffer = fs.readFileSync( + Paths.join(output, "tileset.json") + ); + const tileset = JSON.parse(tilesetJsonBuffer.toString()); + const actualContentUris = await SpecHelpers.collectExplicitContentUris( + tileset.root + ); + actualContentUris.sort(); + + const expectedContentUris = ["tile.b3dm"]; + expect(actualContentUris).toEqual(expectedContentUris); + }); }); diff --git a/src/cli/main.ts b/src/cli/main.ts index a5137ac..bd2dc99 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -447,6 +447,8 @@ async function run() { await runCommand(command, parsedToolArgs, optionArgs); const afterMs = performance.now(); logger.info(`Total: ${(afterMs - beforeMs).toFixed(3)} ms`); + // This is not part of the Logger API!: + logger.flush(); } async function runCommand(command: string, toolArgs: any, optionArgs: any) { diff --git a/src/tools/tilesetProcessing/TilesetCombiner.ts b/src/tools/tilesetProcessing/TilesetCombiner.ts index 541a815..3a17426 100644 --- a/src/tools/tilesetProcessing/TilesetCombiner.ts +++ b/src/tools/tilesetProcessing/TilesetCombiner.ts @@ -17,6 +17,9 @@ import { TilesetTargets } from "../../tilesets"; import { Tiles } from "../../tilesets"; import { Tilesets } from "../../tilesets"; +import { Loggers } from "../../base"; +const logger = Loggers.get("tilesetProcessing"); + /** * A class for combining external tileset of a given tileset, to * create a new, combined tileset. @@ -78,6 +81,10 @@ export class TilesetCombiner { tilesetTargetName: string, overwrite: boolean ): Promise { + logger.debug(`Running combine`); + logger.debug(` tilesetSourceName: ${tilesetSourceName}`); + logger.debug(` tilesetTargetName: ${tilesetTargetName}`); + const tilesetSource = TilesetSources.createAndOpen(tilesetSourceName); const tilesetTarget = TilesetTargets.createAndBegin( tilesetTargetName, @@ -105,6 +112,8 @@ export class TilesetCombiner { this.tilesetSource = undefined; this.tilesetTarget = undefined; + + logger.debug(`Running combine DONE`); } /** @@ -140,7 +149,7 @@ export class TilesetCombiner { const tileset = JSON.parse(tilesetJsonBuffer.toString()) as Tileset; this.externalTilesetFileNames.length = 0; - await this.combineTilesetsInternal(".", tileset, undefined); + await this.combineTilesetsInternal(".", tileset); this.copyResources(tilesetTargetJsonFileName); @@ -164,26 +173,36 @@ export class TilesetCombiner { * * @param currentDirectory - The current directory * @param tileset - The current tileset - * @param parentTile - The optional parent tile */ private async combineTilesetsInternal( currentDirectory: string, - tileset: Tileset, - parentTile: Tile | undefined + tileset: Tileset ): Promise { + // Traverse the tileset, depth-first, and call `combineTileInternal` + // on each tile. If the tile contains a content with an external + // tileset, then this external tileset will be "inlined". Note + // that depending on the structure of the tileset, this may cause + // new children to be added to the tile. This has to be taken + // into account here (and this is the reason why this cannot + // be implemented with `Tiles.traverseExplicit`). const root = tileset.root; - if (parentTile) { - parentTile.content = root.content; - parentTile.contents = root.contents; - parentTile.children = root.children; - parentTile.boundingVolume = root.boundingVolume; - parentTile.transform = root.transform; - } - await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { - const tile = tilePath[tilePath.length - 1]; + const stack: Tile[] = []; + stack.push(root); + while (stack.length > 0) { + const tile = stack[stack.length - 1]; + stack.pop(); + + // Create a copy of the children array, to handle the + // case that new children are added to the tile while + // "inlining" the external tilesets. + const children = tile.children?.slice(); await this.combineTileInternal(currentDirectory, tile); - return true; - }); + if (children) { + for (const child of children) { + stack.push(child); + } + } + } } /** @@ -201,44 +220,71 @@ export class TilesetCombiner { tile: Tile ): Promise { if (tile.content) { - await this.combineContentInternal(currentDirectory, tile, tile.content); + await this.combineSingleContentInternal(currentDirectory, tile); } else if (tile.contents) { - for (const content of tile.contents) { - await this.combineContentInternal(currentDirectory, tile, content); - } + await this.combineMultipleContentsInternal(currentDirectory, tile); } } /** - * This is called for each content of each tile of the source tileset and - * all of its external tilesets. + * Processes an external tileset that was found during the traversal. + * This will be called recursively on all external tilesets. It will + * return the external tileset, AFTER it has itself been combined + * by passing it to `combineTilesetsInternal` * - * If the given content points to an external tileset, it is inlined - * by calling `combineTilesetsInternal` with the directory and JSON of the - * external tileset. + * @param externalFileName - The full file name of the external tileset + * @param externalFileBuffer - The buffer containing the JSON of the + * external tileset + * @returns The processed external tileset + */ + private async processExternalTileset( + externalFileName: string, + externalFileBuffer: Buffer + ) { + this.externalTilesetFileNames.push(externalFileName); + const externalTilesetDirectory = path.dirname(externalFileName); + const externalTileset = JSON.parse( + externalFileBuffer.toString() + ) as Tileset; + await this.combineTilesetsInternal( + externalTilesetDirectory, + externalTileset + ); + return externalTileset; + } + + /** + * This is called for each tile of the source tileset that has a single + * content. + * + * If the content points to an external tileset, then + * - The external tileset is 'combined' by calling `processExternalTileset` + * - The properties of the given tile are replaced with the properties of + * the root of the (combined) external tileset. * * Otherwise, the URL of the content is updated to be relative to * the root of the resulting combined tileset. * * @param currentDirectory - The current directory (see `combineTilesetsInternal`) * @param tile - The current tile - * @param content - The current tile content */ - private async combineContentInternal( + private async combineSingleContentInternal( currentDirectory: string, - tile: Tile, - content: Content + tile: Tile ): Promise { if (!this.tilesetSource || !this.tilesetTarget) { throw new DeveloperError("The source and target must be defined"); } - - const contentUri = content.uri; - if (!contentUri) { - // This is the case for legacy data (including some of the - // original spec data), so handle this case explicitly here. - throw new TilesetError("Content does not have a URI"); + const content = tile.content; + if (!content) { + throw new DeveloperError( + "This should only be called for tiles with a single content" + ); } + + // Obtain the data of the content, and determine whether it is + // an external tileset + const contentUri = TilesetCombiner.obtainContentUri(content); const externalFileName = Paths.join(currentDirectory, contentUri); const externalFileBuffer = this.tilesetSource.getValue(externalFileName); if (!externalFileBuffer) { @@ -252,29 +298,149 @@ export class TilesetCombiner { // will end up in const newUri = Paths.relativize(".", externalFileName); content.uri = newUri; - } else { - // When the data is an external tileset, recursively combine - // ("inline") that tileset, and insert its content, contents - // and children into the current tile - this.externalTilesetFileNames.push(externalFileName); - const externalTilesetDirectory = path.dirname(externalFileName); - const externalTilesetBuffer = - this.tilesetSource.getValue(externalFileName); - if (!externalTilesetBuffer) { - throw new TilesetError( - `Could not obtain data for external ` + - `tileset file ${externalFileName}` - ); + return; + } + + // The data is an external tileset. Process (combine) this + // tileset, recursively + const externalTileset = await this.processExternalTileset( + externalFileName, + externalFileBuffer + ); + const externalRoot = externalTileset.root; + + // All relevant properties of the tile are replaced with + // the properties of the external tileset root + tile.geometricError = externalRoot.geometricError; + tile.content = externalRoot.content; + tile.contents = externalRoot.contents; + tile.children = externalRoot.children; + tile.boundingVolume = externalRoot.boundingVolume; + tile.transform = externalRoot.transform; + } + + /** + * This is called for each tile of the source tileset that has multiple contents. + * + * It will process each content: + * + * If the content points to an external tileset, then + * - The external tileset is 'combined' by calling `processExternalTileset` + * - The root of this (combined) external tileset is added as a child to the give tile + * + * Otherwise, the URL of the content is updated to be relative to + * the root of the resulting combined tileset. + * + * @param currentDirectory - The current directory (see `combineTilesetsInternal`) + * @param tile - The current tile + */ + private async combineMultipleContentsInternal( + currentDirectory: string, + tile: Tile + ): Promise { + if (!this.tilesetSource || !this.tilesetTarget) { + throw new DeveloperError("The source and target must be defined"); + } + const contents = tile.contents; + if (!contents) { + throw new DeveloperError( + "This should only be called for tiles with multiple contents" + ); + } + + // The new contents (omitting the external tilesets), + // and the new children (containing tiles for external tileset roots) + const newContents: Content[] = []; + let newChildren: Tile[] | undefined = undefined; + + for (const content of contents) { + // Obtain the data of the content, and determine whether it is + // an external tileset + const contentUri = TilesetCombiner.obtainContentUri(content); + const externalFileName = Paths.join(currentDirectory, contentUri); + const externalFileBuffer = this.tilesetSource.getValue(externalFileName); + if (!externalFileBuffer) { + throw new TilesetError(`No data found for ${externalFileName}`); } - const externalTileset = JSON.parse( - externalTilesetBuffer.toString() - ) as Tileset; - await this.combineTilesetsInternal( - externalTilesetDirectory, - externalTileset, - tile + const contentData = new BufferedContentData( + contentUri, + externalFileBuffer ); + const isTileset = await this.externalTilesetDetector(contentData); + + if (!isTileset) { + // When the data is not an external tileset, then just update + // the content URI to point to the path that the content data + // will end up in + const newUri = Paths.relativize(".", externalFileName); + content.uri = newUri; + newContents.push(content); + continue; + } + + // The data is an external tileset. Process (combine) this + // tileset, recursively + const externalTileset = await this.processExternalTileset( + externalFileName, + externalFileBuffer + ); + const externalRoot = externalTileset.root; + + // Add a tile (that has the properties from the external tileset + // root) as a new child + const newChild: Tile = { + geometricError: externalRoot.geometricError, + content: externalRoot.content, + contents: externalRoot.contents, + children: externalRoot.children, + boundingVolume: externalRoot.boundingVolume, + transform: externalRoot.transform, + }; + if (newChildren === undefined) { + newChildren = [newChild]; + } else { + newChildren.push(newChild); + } } + Tiles.setContents(tile, newContents); + tile.children = newChildren; + } + + /** + * Returns the URI of the given content, handling the case that it + * might be stored as the (legacy) `url` property. + * + * If the given content contains the (legacy) `url` property, then + * it will be updated, in place: The `uri` property will be set to + * the value of the `url` property, and the `url` property will + * be deleted. + * + * @param content - The `Content` + * @returns The content URI + */ + private static obtainContentUri(content: Content): string { + let contentUri = content.uri; + if (contentUri !== undefined) { + return contentUri; + } + // This is the case for legacy data (including some of the + // original spec data), so handle this case explicitly here. + const legacyContent = content as any; + if (legacyContent.url === undefined) { + // This should never be the case: + throw new TilesetError( + "Content does neither contain a 'uri' nor a (legacy) 'url' property" + ); + } + // Remove the legacy content.url, and set the URL + // as the content.uri instead + logger.warn( + "The 'url' property of tile content is deprecated. Using 'uri' in combined result." + ); + contentUri = legacyContent.url; + delete legacyContent.url; + legacyContent.uri = contentUri; + return contentUri; } /**