diff --git a/CHANGES.md b/CHANGES.md index 6840d67..acd12f6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,11 @@ Change Log ========== +### 0.4.2 - yyyy-mm-dd + +- The `createTilesetJson` command has been extended to receive an optional cartographic position, which serves as the position of generated tileset. + + ### 0.4.1 - 2024-02-20 - The packages that have been introduced in version `0.4.0` have been merged back into a single package. diff --git a/README.md b/README.md index cf45e1f..51a41fb 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,26 @@ npx 3d-tiles-tools analyze -i ./specs/data/batchedWithBatchTableBinary.b3dm -o . This will accept B3DM, I3DM, PNTS, CMPT, and GLB files (both for glTF 1.0 and for glTF 2.0), and write files into the output directory that contain the feature table, batch table, layout information, the GLB, and the JSON of the GLB. This is primarily intended for debugging and analyzing tile data. Therefore, the exact naming and content of the generated output files are not specified. +#### createTilesetJson + +Create a tileset JSON file from a given set of tile content files. + +Additional command line options: + +| Flag | Description | Required | +| ---- | ----------- | -------- | +|`--cartographicPositionDegrees`|An array of either two or three values, which are the (longitude, latitude) or (longitude, latitude, height) of the target position. The longitude and latitude are given in degrees, and the height is given in meters.| No | + +If the input is a single file, then this will result in a single (root) tile with the input file as its tile content. If the input is a directory, then all content files in this directory will be used as tile content, recursively. The exact set of file types that are considered to be 'tile content' is not specified, but it will include GLB, B3DM, PNTS, I3DM, and CMPT files. + +Examples: + +``` +npx 3d-tiles-tools createTilesetJson -i ./input/ -o ./output/tileset.json --cartographicPositionDegrees -75.152 39.94 10 +``` +This creates the specified tileset JSON file, which will refer to all tile content files in the given input directory as its tile contents. The root node of the tileset will have a transform that will place it at the given cartographic position. + + ### Pipeline diff --git a/src/cli/ToolsMain.ts b/src/cli/ToolsMain.ts index a45af96..bc51c3c 100644 --- a/src/cli/ToolsMain.ts +++ b/src/cli/ToolsMain.ts @@ -567,6 +567,7 @@ export class ToolsMain { static async createTilesetJson( inputName: string, output: string, + cartographicPositionDegrees: number[] | undefined, force: boolean ) { logger.debug(`Executing createTilesetJson`); @@ -588,11 +589,24 @@ export class ToolsMain { Paths.relativize(inputName, fileName) ); } - logger.info(`Creating tileset.json with content URIs: ${contentUris}`); + logger.info(`Creating tileset JSON with content URIs: ${contentUris}`); const tileset = await TilesetJsonCreator.createTilesetFromContents( baseDir, contentUris ); + + if (cartographicPositionDegrees !== undefined) { + logger.info( + `Creating tileset at cartographic position: ` + + `${cartographicPositionDegrees} (in degress)` + ); + const transform = + TilesetJsonCreator.computeTransformFromCartographicPositionDegrees( + cartographicPositionDegrees + ); + tileset.root.transform = transform; + } + const tilesetJsonString = JSON.stringify(tileset, null, 2); const outputDirectory = path.dirname(output); Paths.ensureDirectoryExists(outputDirectory); diff --git a/src/cli/main.ts b/src/cli/main.ts index f2229f9..a5137ac 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -303,12 +303,23 @@ function parseToolArgs(a: string[]) { ) .command( "createTilesetJson", - "Creates a 'tileset.json' file that just refers to given GLB tile content files. " + + "Creates a tileset JSON file that just refers to given tile content files. " + "If the input is a single file, then this will result in a single (root) tile with " + - "the input file as its tile content. If the input is a directory, then all files" + - "with '.glb' file extension in this directory will be used as tile content, " + - "recursively.", - { i: inputStringDefinition, o: outputStringDefinition } + "the input file as its tile content. If the input is a directory, then all content " + + "files in this directory will be used as tile content, recursively. The exact set " + + "of file types that are considered to be 'tile content' is not specified, but it " + + "will include GLB, B3DM, PNTS, I3DM, and CMPT files.", + { + i: inputStringDefinition, + o: outputStringDefinition, + cartographicPositionDegrees: { + description: + "An array of either two or three values, which are the (longitude, latitude) " + + "or (longitude, latitude, height) of the target position. The longitude and " + + "latitude are given in degrees, and the height is given in meters.", + type: "array", + }, + } ) .demandCommand(1) .strict(); @@ -338,6 +349,63 @@ function parseOptionArgs(a: string[]) { return v; } +/** + * Ensures that the given value is an array of numbers with the given + * length constraints. + * + * If the given value is `undefined`, then no check will be performed + * and `undefined` is returned. + * + * Otherwise, this function will check the given constraints, and + * throw a `DeveloperError` if they are not met. + * + * @param value - The value + * @param minLength - The minimum length + * @param maxLength - The maximum length + * @returns The validated value + * @throws DeveloperError If the given value does not meet the given + * constraints. + */ +function validateOptionalNumberArray( + value: any, + minLength: number | undefined, + maxLength: number | undefined +): number[] | undefined { + if (value === undefined) { + return undefined; + } + if (!Array.isArray(value)) { + throw new DeveloperError(`Expected an array, but received ${value}`); + } + if (minLength !== undefined) { + if (value.length < minLength) { + throw new DeveloperError( + `Expected an array of at least length ${minLength}, ` + + `but received an array of length ${value.length}: ${value}` + ); + } + } + if (maxLength !== undefined) { + if (value.length > maxLength) { + throw new DeveloperError( + `Expected an array of at most length ${maxLength}, ` + + `but received an array of length ${value.length}: ${value}` + ); + } + } + for (let i = 0; i < value.length; i++) { + const element = value[i]; + const type = typeof element; + if (type !== "number") { + throw new DeveloperError( + `Expected an array of numbers, but element at index ${i} ` + + `has type '${type}': ${element}` + ); + } + } + return value; +} + const parsedToolArgs = parseToolArgs(toolArgs); async function run() { @@ -385,8 +453,6 @@ async function runCommand(command: string, toolArgs: any, optionArgs: any) { const inputs: string[] = toolArgs.input; const output = toolArgs.output; const force = toolArgs.force; - const recursive = toolArgs.recursive; - const tilesOnly = toolArgs.tilesOnly; const parsedOptionArgs = parseOptionArgs(optionArgs); logger.trace(`Command line call:`); @@ -394,7 +460,6 @@ async function runCommand(command: string, toolArgs: any, optionArgs: any) { logger.trace(` inputs: ${inputs}`); logger.trace(` output: ${output}`); logger.trace(` force: ${force}`); - logger.trace(` recursive: ${recursive}`); logger.trace(` optionArgs: ${optionArgs}`); logger.trace(` parsedOptionArgs: ${JSON.stringify(parsedOptionArgs)}`); @@ -407,6 +472,7 @@ async function runCommand(command: string, toolArgs: any, optionArgs: any) { } else if (command === "cmptToGlb") { await ToolsMain.cmptToGlb(input, output, force); } else if (command === "splitCmpt") { + const recursive = toolArgs.recursive === true; await ToolsMain.splitCmpt(input, output, recursive, force); } else if (command === "convertB3dmToGlb") { await ToolsMain.convertB3dmToGlb(input, output, force); @@ -423,6 +489,7 @@ async function runCommand(command: string, toolArgs: any, optionArgs: any) { } else if (command === "optimizeI3dm") { await ToolsMain.optimizeI3dm(input, output, force, parsedOptionArgs); } else if (command === "gzip") { + const tilesOnly = toolArgs.tilesOnly === true; await ToolsMain.gzip(input, output, force, tilesOnly); } else if (command === "ungzip") { await ToolsMain.ungzip(input, output, force); @@ -446,11 +513,12 @@ async function runCommand(command: string, toolArgs: any, optionArgs: any) { } else if (command === "combine") { await ToolsMain.combine(input, output, force); } else if (command === "upgrade") { + const targetVersion = toolArgs.targetVersion ?? "1.0"; await ToolsMain.upgrade( input, output, force, - toolArgs.targetVersion, + targetVersion, parsedOptionArgs ); } else if (command === "merge") { @@ -460,7 +528,17 @@ async function runCommand(command: string, toolArgs: any, optionArgs: any) { } else if (command === "analyze") { ToolsMain.analyze(input, output, force); } else if (command === "createTilesetJson") { - await ToolsMain.createTilesetJson(input, output, force); + const cartographicPositionDegrees = validateOptionalNumberArray( + toolArgs.cartographicPositionDegrees, + 2, + 3 + ); + await ToolsMain.createTilesetJson( + input, + output, + cartographicPositionDegrees, + force + ); } else { throw new DeveloperError(`Invalid command: ${command}`); } diff --git a/src/tools/tilesetProcessing/TilesetJsonCreator.ts b/src/tools/tilesetProcessing/TilesetJsonCreator.ts index d4455df..a23e5c5 100644 --- a/src/tools/tilesetProcessing/TilesetJsonCreator.ts +++ b/src/tools/tilesetProcessing/TilesetJsonCreator.ts @@ -1,6 +1,12 @@ import fs from "fs"; import path from "path"; +import { Cartographic } from "cesium"; +import { Matrix4 } from "cesium"; +import { Transforms } from "cesium"; + +import { DeveloperError } from "../../base"; + import { Tile } from "../../structure"; import { Tileset } from "../../structure"; import { BoundingVolume } from "../../structure"; @@ -258,4 +264,42 @@ export class TilesetJsonCreator { }; return tile; } + + /** + * Computes the transform for a tile to place it at the given cartographic + * position. + * + * The given position is either (longitudeDegrees, latitudeDegrees) + * or (longitudeDegrees, latitudeDegrees, heightMeters). The returned + * array will be that of a 4x4 matrix in column-major order. + * + * @param cartographicPositionDegrees - The cartographic position + * @returns The transform + * @throws DeveloperError If the given array has a length smaller than 2 + */ + static computeTransformFromCartographicPositionDegrees( + cartographicPositionDegrees: number[] + ) { + if (cartographicPositionDegrees.length < 2) { + throw new DeveloperError( + `Expected an array of at least length 2, but received an array ` + + `of length ${cartographicPositionDegrees.length}: ${cartographicPositionDegrees}` + ); + } + const lonDegrees = cartographicPositionDegrees[0]; + const latDegrees = cartographicPositionDegrees[1]; + const height = + cartographicPositionDegrees.length >= 3 + ? cartographicPositionDegrees[2] + : 0.0; + const cartographic = Cartographic.fromDegrees( + lonDegrees, + latDegrees, + height + ); + const cartesian = Cartographic.toCartesian(cartographic); + const enuMatrix = Transforms.eastNorthUpToFixedFrame(cartesian); + const transform = Matrix4.toArray(enuMatrix); + return transform; + } }