Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend and document createTilesetJson #105

Merged
merged 2 commits into from
Mar 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
}
Loading