Skip to content

Commit

Permalink
Merge pull request #105 from CesiumGS/extend-create-tileset-json
Browse files Browse the repository at this point in the history
Extend and document `createTilesetJson`
  • Loading branch information
lilleyse committed Mar 11, 2024
2 parents ac192de + e53c4e9 commit 554e2fd
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 11 deletions.
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 15 additions & 1 deletion src/cli/ToolsMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@ export class ToolsMain {
static async createTilesetJson(
inputName: string,
output: string,
cartographicPositionDegrees: number[] | undefined,
force: boolean
) {
logger.debug(`Executing createTilesetJson`);
Expand All @@ -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);
Expand Down
98 changes: 88 additions & 10 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -385,16 +453,13 @@ 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:`);
logger.trace(` command: ${command}`);
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)}`);

Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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") {
Expand All @@ -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}`);
}
Expand Down
44 changes: 44 additions & 0 deletions src/tools/tilesetProcessing/TilesetJsonCreator.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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;
}
}

0 comments on commit 554e2fd

Please sign in to comment.