Skip to content

Commit

Permalink
Merge pull request #2 from javagl/tools-ts-implicit
Browse files Browse the repository at this point in the history
Support for implicit tileset traversal (and metadata handling)
  • Loading branch information
javagl committed Mar 27, 2023
2 parents 1f47eda + 8fa605d commit a06814c
Show file tree
Hide file tree
Showing 101 changed files with 19,185 additions and 85 deletions.
35 changes: 32 additions & 3 deletions IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,51 @@ Parts of the current implementation may still change. This page is only a short
- `./src/base`: Generic, low-level utility functions.
Most of the functions here are grouped into classes.
- `Buffers`: Padding, detecting GZIPpedness, obtaining "magic" bytes...
- `DataError`: An error indicating that input data was invalid on a low level (e.g. invalid binary data, or unparseable JSON)
- `DeveloperError`: An error that was caused by the _developer_ (i.e. someone used the API in a wrong way)
- `Iterables`: Iterating over files, filtering, mapping...
- `Paths`: Resolving, checking/changing file extensions, ...
- `Uris`: Detect data URIs or absolute URIs
- Special cases: `defined` and `defaultValue`. These have some documentation explaining why they should rarely be used in TypeScript.

- `./src/contentOperations`: Operations that are applied to tile content
- `ContentOps`: Functions like ZIP/unZIP, conversions like `glbToB3dm`, _always_ operating on buffers: "Buffer in - Buffer out"
- `./src/contentProcessing`: Operations that are applied to tile content
- `ContentOps`: Functions like `glbToB3dm`, _always_ operating on buffers: "Buffer in - Buffer out"
- `GltfUtilites`/`GltfPipelineLegacy`: Wrappers around `gltf-pipeline` (e.g. for optimizing/upgrading the GLB in a B3DM)

- `./src/contentTypes`: Classes for determining the type of content data
- `ContentData` as the main interface, implemented as `BufferedContentData` (to be created from a buffer that already exists in memory), or `LazyContentData` (that resolves "as little data as possible" to determine the content type)
- `ContentDataTypeRegistry`: Receives a `ContentData` object and returns a string that indicates the type, like `CONTENT_TYPE_B3DM` or `CONTENT_TYPE_TILESET`.
- `ContentDataTypes`: A set of strings representing different content data types, like `CONTENT_TYPE_B3DM` or `CONTENT_TYPE_TILESET`.
- `ContentDataTypeRegistry`: Receives a `ContentData` object and returns one of the `ContentDataTypes` strings
- `ContentDataTypeChecks`: Offers methods to create predicates that check for certain `included/excluded` content types

- `./src/implicitTiling/`: Classes that represent the structure and information of implicit tilesets
- `AvailabilityInfo`: A simple interface for representing information about the availability of tiles, content, or child subtrees in implicit tiling. This is accessed with an _index_. Instances of classes implementing this interface can be created with the `AvailabilityInfos` class.
- `SubtreeInfo`: A structure that combines the `AvailabilityInfo` for tiles, content, and child subtrees (as it is defined in the input data). Instances of this structure can be created from a subtree JSON file or from binary subtree data, using the `SubtreeInfos` class.
- `BinarySubtreeData`: A simple structure from which the `SubtreeInfo` is created. It combines the data that represents a 'subtree' in implicit tiling, in its 'raw' form: It contains the `Subtree` JSON object, as well as a `BinaryBufferStructure`/`BinaryBufferData` that was created from the `buffers/bufferViews` and the resolved binary data
- `BinarySubtreeDataResolver`: A class that receives a `Subtree` JSON object, and returns the `BinarySubtreeData`, resolving all external `buffer.uri` references
- `ImplicitTilingError`: An error indicating that implicit tiling data was structurally invalid
- `ImplicitTilings`: Methods that try to hide the difference between `QUADTREE` and `OCTREE` tilings. They usually receive a `TileImplicitTiling` JSON object, and perform operations that _depend_ on the `subdivisionScheme`, but can be applied _agnostically_ of the subvision scheme. (Note: Some of this could be done in a cleaner and more generic way, involving ... generics (sic). This _does_ already exist (in a different language), but carrying type parameters along, as in `Availability<TreeCoordinates<Octree>>` can look obscure and "overengineered" at the first glance. I only hope that the current solution here does not turn out to be _underengineered_ ...)
- `TemplateUris`: Internal method to substitute quadtree- or octree coordinates into template URIs

- `./src/io`: Classes for "loading data" in a very generic way, from different sources
- `ResourceResolver` is the main interface, with the core functionality of receiving a URI and returning the data for that URI, with implementations to obtain that data e.g. from a file system or from a 3D Tiles Package.

- `./src/metadata/`: Classes for an implementation of the 3D Metadata Specification
- Utilities for dealing with the JSON representations of metadata objects `ClassProperties`/`MetadataTypes`/`MetadataComponentTypes`...
- Internal utilities for processing metadata values (e.g. normalization, `offset` and `scale` etc.), in `MetadataValues` and `ArrayValues`.
- The `PropertyTableModel`, `MetadataEntityModel` and `PropertyModel` interfaces offer a very thin and simple abstraction layer for 3D Metadata. The structure of these classes is shown here:
![PropertyTable](figures/PropertyTable.png)
- Implementations of these interfaces exist:
- For the JSON-based representation of metadata entities, metadata entity model instances can be created with `MetadataEntityModels`
- `./src/metadata/binary` contains implementations of the metadata interfaces for _binary_ data, with `BinaryPropertyTableModel` being the top-level class, implementing the `PropertyTableModel` interface.

- `./src/packages`: Classes for reading or creating 3D Tiles Package files
- These are implementations of the `TilesetSource` and `TilesetTarget` interface (see `./src/tilesetData`), based on 3TZ or 3DTILES

- `./src/pipelines`: **Preliminary** classes for modeling "processing pipelines" for tilesets

- `./src/spatial`: Basic classes for dealing with tree structures, specifically with quadtrees and octrees

- `./src/structure`: Plain old data objects for the elements of a Tileset JSON
- E.g. `Tileset`, `Tile`, `Content`, ...
- The goal is to have a _typed_ representation of a tileset JSON (assuming that the input JSON was indeed structurally valid - there are no validations or actual type checks during deserialization)
Expand All @@ -56,6 +78,11 @@ Parts of the current implementation may still change. This page is only a short
- `Tiles` for traversing (explicit!) tile hierarchies
- `Tilesets` offering convenience functions for `merge/combine/upgrade`

- `./src/traversal`: Classes for traversing tilesets
- NOTE: The `SubtreeModel`/`SubtreeMetadataModel` interfaces _might_ at some point be moved into `implicitTiling`, but are currently tailored for the use in the traversal classes, and should be considered to be an "implementation detail" here.
- The `TilesetTraverser` class is the entry point for the traversal. It allows traversing a tileset, and offer each traversed tile as a `TraversedTile` instance. The `TraversedTile` describes a tile during traversal (e.g. with a parent, and semantic-based overrides)


## Demos

The `./demos/` folder contains demos that show how to use various parts of the API
Expand All @@ -71,6 +98,8 @@ The `./demos/` folder contains demos that show how to use various parts of the A
- `TileFormatsDemoConversions`: Demos showing how to use the `TileFormats` class for conversions (like extracting GLB from B3DM etc.)
- `TilesetProcessingDemos`: Demos for the `combine/merge/upgrade` functions
- `TilesetUpgraderDemos`: More fine-grained demos for the upgrade functionality
- `TraversalDemo`: A basic example showing how to traverse a tileset
- `TraversalStatsDemo`: A basic example showing how to traverse a tileset and collect statistical information in the process.

The `./demos/benchmarks` folder contains very basic benchmarks for creating/reading different 3D Tiles package formats.

Expand Down
2 changes: 1 addition & 1 deletion ThirdParty.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"license": [
"Apache-2.0"
],
"version": "1.102.0",
"version": "1.103.0",
"url": "https://www.npmjs.com/package/cesium"
},
{
Expand Down
2 changes: 1 addition & 1 deletion demos/TileFormatsDemoConversions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import fs from "fs";

import { Paths } from "../src/base/Paths";

import { GltfUtilities } from "../src/contentOperations/GtlfUtilities";
import { GltfUtilities } from "../src/contentProcessing/GtlfUtilities";
import { TileFormats } from "../src/tileFormats/TileFormats";

function glbToB3dm(inputFileName: string, outputFileName: string) {
Expand Down
43 changes: 43 additions & 0 deletions demos/TraversalDemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import path from "path";

import { readJsonUnchecked } from "./readJsonUnchecked";

import { ResourceResolvers } from "../src/io/ResourceResolvers";
import { TilesetTraverser } from "../src/traversal/TilesetTraverser";

async function tilesetTraversalDemo(filePath: string) {
const directory = path.dirname(filePath);
const resourceResolver =
ResourceResolvers.createFileResourceResolver(directory);
const tileset = await readJsonUnchecked(filePath);
// Note: External schemas are not considered here
const schema = tileset.schema;
const depthFirst = false;
console.log("Traversing tileset");
await TilesetTraverser.traverse(
tileset,
schema,
resourceResolver,
async (traversedTile) => {
const contentUris = traversedTile.getFinalContents().map((c) => c.uri);
const geometricError = traversedTile.asFinalTile().geometricError;
console.log(
` Traversed tile: ${traversedTile}, ` +
`path: ${traversedTile.path}, ` +
`contents [${contentUris}], ` +
`geometricError ${geometricError}`
);
return true;
},
depthFirst
);
console.log("Traversing tileset DONE");
}

async function runDemo() {
const tilesetFileName =
"../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json";
await tilesetTraversalDemo(tilesetFileName);
}

runDemo();
217 changes: 217 additions & 0 deletions demos/TraversalStatsDemo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import fs from "fs";
import path from "path";

import { readJsonUnchecked } from "./readJsonUnchecked";

import { ResourceResolvers } from "../src/io/ResourceResolvers";

import { TilesetTraverser } from "../src/traversal/TilesetTraverser";
import { TraversedTile } from "../src/traversal/TraversedTile";

// A small demo that traverses a tileset, passes all
// traversed tiles to a "StatsCollector" (defined below),
// and creates a short JSON summary of some statistics.

async function tilesetTraversalDemo(filePath: string) {
// Read the tileset from the input path
const directory = path.dirname(filePath);
const resourceResolver =
ResourceResolvers.createFileResourceResolver(directory);
const tileset = await readJsonUnchecked(filePath);
// Note: External schemas are not considered here
const schema = tileset.schema;

// Traverse the tileset, and pass each tile to
// the StatsCollector
console.log("Traversing tileset");
const tilesetStatsCollector = new TilesetStatsCollector();
const depthFirst = false;
await TilesetTraverser.traverse(
tileset,
schema,
resourceResolver,
async (traversedTile) => {
tilesetStatsCollector.accept(traversedTile);
return true;
},
depthFirst
);
console.log("Traversing tileset DONE");

// Print the statistics summary to the console
console.log("Stats:");
const json = tilesetStatsCollector.createJson();
const jsonString = JSON.stringify(json, null, 2);
console.log(jsonString);
}

// A simple class to collect statistical information
class StatsCollector {
// A mapping from value names to counters
private readonly counters: {
[key: string]: Counter;
} = {};

// A mapping from value names to statistical summaries
private readonly summaries: {
[key: string]: Summary;
} = {};

// Add one entry to a summary, creating it when necessary
acceptEntry(name: string, value: number) {
let summary = this.summaries[name];
if (!summary) {
summary = new Summary();
this.summaries[name] = summary;
}
summary.accept(value);
}

// Increment a counter, creating it when necessary
increment(name: string) {
let counter = this.counters[name];
if (!counter) {
counter = new Counter();
this.counters[name] = counter;
}
counter.increment();
}

// Create a short JSON representation of the collected data
createJson(): any {
const json: any = {};
for (const key of Object.keys(this.counters)) {
const counter = this.counters[key];
json[key] = counter.getCount();
}
for (const key of Object.keys(this.summaries)) {
const summary = this.summaries[key];
json[key] = {
count: summary.getCount(),
sum: summary.getSum(),
min: summary.getMinimum(),
max: summary.getMaximum(),
avg: summary.getMean(),
stdDev: summary.getStandardDeviation(),
};
}
return json;
}
}

/**
* A class that serves as a counter in the `StatsCollector`
*/
class Counter {
private count: number;

public constructor() {
this.count = 0;
}

increment() {
this.count++;
}

getCount() {
return this.count;
}
}

/**
* A class that can accept numbers, and collects statistical
* information for these numbers.
*/
class Summary {
private count: number;
private sum: number;
private min: number;
private max: number;
private varianceTracker: number;

public constructor() {
this.count = 0;
this.sum = 0.0;
this.min = Number.POSITIVE_INFINITY;
this.max = Number.NEGATIVE_INFINITY;
this.varianceTracker = 0.0;
}

accept(value: number) {
const deviation = value - this.getMean();
this.sum += value;
this.min = Math.min(this.min, value);
this.max = Math.max(this.max, value);
this.count++;
if (this.count > 1) {
this.varianceTracker +=
(deviation * deviation * (this.count - 1)) / this.count;
}
}

getCount() {
return this.count;
}

getSum() {
return this.sum;
}

getMinimum() {
return this.min;
}

getMaximum() {
return this.max;
}

getMean() {
return this.sum / this.count;
}

getStandardDeviation() {
return Math.sqrt(this.varianceTracker / this.count);
}
}

// A specialization of the `StatsColleror` that collects
// information about the tiles that are traversed with
// a TilesetTraverser
class TilesetStatsCollector extends StatsCollector {
// Accept the given tile during traversal, and collect
// statistical information
accept(traversedTile: TraversedTile) {
this.increment("totalNumberOfTiles");

// NOTE: This is a means of checking whether a tile
// is the root of an implicit tileset. This may be
// refactored at some point.
if (traversedTile.getImplicitTiling()) {
this.increment("totalNumberOfSubtres");
} else {
// Obtain all content URIs, resolve them, and obtain
// the sizes of the corresponding files, storing them
// in the "tileFileSize" summary
const contentUris = traversedTile.getFinalContents().map((c) => c.uri);
for (const contentUri of contentUris) {
const resolvedContentUri = traversedTile.resolveUri(contentUri);
const stats = fs.statSync(resolvedContentUri);
const tileFileSizeInBytes = stats.size;
this.acceptEntry("tileFileSize", tileFileSizeInBytes);
}
}

// Store the geometric error in the "geometricError" summary
const finalTile = traversedTile.asFinalTile();
const geometricError = finalTile.geometricError;
this.acceptEntry("geometricError", geometricError);
}
}

async function runDemo() {
const tilesetFileName =
"../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json";
await tilesetTraversalDemo(tilesetFileName);
}

runDemo();
27 changes: 27 additions & 0 deletions demos/readJsonUnchecked.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import fs from "fs";

/**
* Only for internal use and basic tests:
*
* Reads a JSON file, parses it, and returns the result.
* If the file cannot be read or parsed, then an error
* message will be printed and `undefined` is returned.
*
* @param filePath - The path to the file
* @returns A promise that resolves with the result or `undefined`
*/
export async function readJsonUnchecked(filePath: string): Promise<any> {
try {
const data = fs.readFileSync(filePath);
if (!data) {
console.error("Could not read " + filePath);
return undefined;
}
const jsonString = data.toString();
const result = JSON.parse(jsonString);
return result;
} catch (error) {
console.error("Could not parse JSON", error);
return undefined;
}
}

0 comments on commit a06814c

Please sign in to comment.