From da22e47d58195bfebff99fced0fa920b4ccc1890 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 19 Mar 2023 16:25:27 +0100 Subject: [PATCH 01/60] Added subtree content type Let's not be too picky about whether this really is a "content" type... --- specs/ContentDataTypesSpec.ts | 7 +++++++ specs/data/contentTypes/README.md | 1 + specs/data/contentTypes/content.subtree | Bin 0 -> 352 bytes src/contentTypes/ContentDataTypeRegistry.ts | 4 ++++ src/contentTypes/ContentDataTypes.ts | 1 + 5 files changed, 13 insertions(+) create mode 100644 specs/data/contentTypes/content.subtree diff --git a/specs/ContentDataTypesSpec.ts b/specs/ContentDataTypesSpec.ts index 0aa97435..6cc461f4 100644 --- a/specs/ContentDataTypesSpec.ts +++ b/specs/ContentDataTypesSpec.ts @@ -52,6 +52,13 @@ describe("ContentDataTypeRegistry.findContentDataType", function () { expect(type).toEqual(ContentDataTypes.CONTENT_TYPE_VCTR); }); + it("detects SUBT", async function () { + const contentUri = "specs/data/contentTypes/content.subtree"; + const c = BufferedContentData.create(contentUri); + const type = await ContentDataTypeRegistry.findContentDataType(c); + expect(type).toEqual(ContentDataTypes.CONTENT_TYPE_SUBT); + }); + it("detects GEOJSON", async function () { const contentUri = "specs/data/contentTypes/content.geojson"; const c = BufferedContentData.create(contentUri); diff --git a/specs/data/contentTypes/README.md b/specs/data/contentTypes/README.md index d3694305..94580ce7 100644 --- a/specs/data/contentTypes/README.md +++ b/specs/data/contentTypes/README.md @@ -4,6 +4,7 @@ Tile content files used in the specs. - The `content.3tz` is a 3TZ file with a minimal, valid `tileset.json` - The `content.gltf` is the (embedded) `Triangle` sample model from https://github.com/KhronosGroup/glTF-Sample-Models/blob/8e9a5a6ad1a2790e2333e3eb48a1ee39f9e0e31b/2.0/ - The `content.glb` is the `Box` sample model from https://github.com/KhronosGroup/glTF-Sample-Models/blob/8e9a5a6ad1a2790e2333e3eb48a1ee39f9e0e31b/2.0/ +- The `content.subt` is the subtree file from https://github.com/CesiumGS/3d-tiles-samples/blob/902ea3dca1821a9ef9d23d141f800c68627c452b/1.1/SparseImplicitQuadtree/subtrees/0.0.0.subtree The other files are taken from https://github.com/CesiumGS/cesium/tree/c0ec95713b6cde5a91eea320795c84408159dcad/Apps/SampleData/Cesium3DTiles diff --git a/specs/data/contentTypes/content.subtree b/specs/data/contentTypes/content.subtree new file mode 100644 index 0000000000000000000000000000000000000000..b91203997f56120a4c58c5a343565b36da1dfd52 GIT binary patch literal 352 zcmaKoJqyAx5QgL8CioL1bB9(zLpO0190V5;n%0XA#1@*XQY8P~OTRz`Z^#Gl^WK}B zty7#Z#)My}=H13#Jgt?2%JTS3=LTU9#SL>FM{9QpdiA8?*%K|zUyWj{zZO{NKBZ=_joQUCw| literal 0 HcmV?d00001 diff --git a/src/contentTypes/ContentDataTypeRegistry.ts b/src/contentTypes/ContentDataTypeRegistry.ts index 1e8b4b7a..3d3eb7a8 100644 --- a/src/contentTypes/ContentDataTypeRegistry.ts +++ b/src/contentTypes/ContentDataTypeRegistry.ts @@ -77,6 +77,10 @@ export class ContentDataTypeRegistry { ContentDataTypeRegistry.byMagic("vctr"), ContentDataTypes.CONTENT_TYPE_VCTR ); + ContentDataTypeRegistry.register( + ContentDataTypeRegistry.byMagic("subt"), + ContentDataTypes.CONTENT_TYPE_SUBT + ); ContentDataTypeRegistry.register( ContentDataTypeRegistry.byExtension(".geojson"), ContentDataTypes.CONTENT_TYPE_GEOJSON diff --git a/src/contentTypes/ContentDataTypes.ts b/src/contentTypes/ContentDataTypes.ts index 3ee0d88c..5c7d5a13 100644 --- a/src/contentTypes/ContentDataTypes.ts +++ b/src/contentTypes/ContentDataTypes.ts @@ -15,6 +15,7 @@ export class ContentDataTypes { static readonly CONTENT_TYPE_PNTS = "CONTENT_TYPE_PNTS"; static readonly CONTENT_TYPE_GEOM = "CONTENT_TYPE_GEOM"; static readonly CONTENT_TYPE_VCTR = "CONTENT_TYPE_VCTR"; + static readonly CONTENT_TYPE_SUBT = "CONTENT_TYPE_SUBT"; static readonly CONTENT_TYPE_GEOJSON = "CONTENT_TYPE_GEOJSON"; static readonly CONTENT_TYPE_3TZ = "CONTENT_TYPE_3TZ"; From 5c7fd0b6bd116cda2d87c4ff74925fb108e762d7 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 19 Mar 2023 17:14:22 +0100 Subject: [PATCH 02/60] Formatting --- src/implicitTiling/AvailabilityInfo.ts | 2 +- src/implicitTiling/ImplicitTilingError.ts | 2 +- src/implicitTiling/OctreeCoordinates.ts | 2 +- src/implicitTiling/Octrees.ts | 2 +- src/implicitTiling/QuadtreeCoordinates.ts | 2 +- src/implicitTiling/Quadtrees.ts | 2 +- src/implicitTiling/SubtreeInfo.ts | 2 +- src/implicitTiling/SubtreeInfos.ts | 2 +- src/implicitTiling/TemplateUris.ts | 2 +- src/implicitTiling/TreeCoordinates.ts | 2 +- src/index.ts | 1 - src/io/TilesetSourceResourceResolver.ts | 5 +---- src/metadata/ArrayValues.ts | 2 +- src/metadata/ClassProperties.ts | 2 +- src/metadata/MetadataError.ts | 2 +- src/traversal/SubtreeModel.ts | 2 +- src/traversal/SubtreeModels.ts | 2 +- 17 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/implicitTiling/AvailabilityInfo.ts b/src/implicitTiling/AvailabilityInfo.ts index 082fb6b0..5680ea23 100644 --- a/src/implicitTiling/AvailabilityInfo.ts +++ b/src/implicitTiling/AvailabilityInfo.ts @@ -2,7 +2,7 @@ * An interface that describes the availability information * in a subtree. This is used for tile, content, and child * subtree availability. - * + * * @internal */ export interface AvailabilityInfo { diff --git a/src/implicitTiling/ImplicitTilingError.ts b/src/implicitTiling/ImplicitTilingError.ts index 8c38c4e2..b3453f93 100644 --- a/src/implicitTiling/ImplicitTilingError.ts +++ b/src/implicitTiling/ImplicitTilingError.ts @@ -4,7 +4,7 @@ * * This may be thrown by methods that create the convenience classes * for this package, when the given resources are not valid. - * + * * @internal */ export class ImplicitTilingError extends Error { diff --git a/src/implicitTiling/OctreeCoordinates.ts b/src/implicitTiling/OctreeCoordinates.ts index 5c38e3c3..27dfaf23 100644 --- a/src/implicitTiling/OctreeCoordinates.ts +++ b/src/implicitTiling/OctreeCoordinates.ts @@ -4,7 +4,7 @@ import { TreeCoordinates } from "./TreeCoordinates"; /** * An implementation of `TreeCoordinates` for octrees - * + * * @internal */ export class OctreeCoordinates implements TreeCoordinates { diff --git a/src/implicitTiling/Octrees.ts b/src/implicitTiling/Octrees.ts index 70707e4f..e2f0012d 100644 --- a/src/implicitTiling/Octrees.ts +++ b/src/implicitTiling/Octrees.ts @@ -2,7 +2,7 @@ import { OctreeCoordinates } from "./OctreeCoordinates"; /** * Methods related to octrees - * + * * @internal */ export class Octrees { diff --git a/src/implicitTiling/QuadtreeCoordinates.ts b/src/implicitTiling/QuadtreeCoordinates.ts index aeea721f..5292a45e 100644 --- a/src/implicitTiling/QuadtreeCoordinates.ts +++ b/src/implicitTiling/QuadtreeCoordinates.ts @@ -4,7 +4,7 @@ import { TreeCoordinates } from "./TreeCoordinates"; /** * An implementation of `TreeCoordinates` for octrees - * + * * @internal */ export class QuadtreeCoordinates implements TreeCoordinates { diff --git a/src/implicitTiling/Quadtrees.ts b/src/implicitTiling/Quadtrees.ts index 826069e5..4a6a8e98 100644 --- a/src/implicitTiling/Quadtrees.ts +++ b/src/implicitTiling/Quadtrees.ts @@ -2,7 +2,7 @@ import { QuadtreeCoordinates } from "./QuadtreeCoordinates"; /** * Methods related to quadtrees. - * + * * @internal */ export class Quadtrees { diff --git a/src/implicitTiling/SubtreeInfo.ts b/src/implicitTiling/SubtreeInfo.ts index 02bc3a6a..0887d619 100644 --- a/src/implicitTiling/SubtreeInfo.ts +++ b/src/implicitTiling/SubtreeInfo.ts @@ -6,7 +6,7 @@ import { AvailabilityInfo } from "./AvailabilityInfo"; * * It offers the availability information for tiles, child * subtrees, and contents, as `AvailabilityInfo` objects. - * + * * @internal */ export interface SubtreeInfo { diff --git a/src/implicitTiling/SubtreeInfos.ts b/src/implicitTiling/SubtreeInfos.ts index ca8555fc..906fbea1 100644 --- a/src/implicitTiling/SubtreeInfos.ts +++ b/src/implicitTiling/SubtreeInfos.ts @@ -10,7 +10,7 @@ import { TileImplicitTiling } from "../structure/TileImplicitTiling"; /** * Methods to create `SubtreeInfo` instances. - * + * * @internal */ export class SubtreeInfos { diff --git a/src/implicitTiling/TemplateUris.ts b/src/implicitTiling/TemplateUris.ts index a70e9c71..e1f80130 100644 --- a/src/implicitTiling/TemplateUris.ts +++ b/src/implicitTiling/TemplateUris.ts @@ -3,7 +3,7 @@ import { QuadtreeCoordinates } from "./QuadtreeCoordinates"; /** * Method related to template URIs for implicit tiling. - * + * * @internal */ export class TemplateUris { diff --git a/src/implicitTiling/TreeCoordinates.ts b/src/implicitTiling/TreeCoordinates.ts index 041abfee..50018d41 100644 --- a/src/implicitTiling/TreeCoordinates.ts +++ b/src/implicitTiling/TreeCoordinates.ts @@ -1,6 +1,6 @@ /** * An interface for coordinates within a tree structure. - * + * * @internal */ export interface TreeCoordinates { diff --git a/src/index.ts b/src/index.ts index 3b722941..259b4b11 100644 --- a/src/index.ts +++ b/src/index.ts @@ -115,4 +115,3 @@ export * from "./traversal/SubtreeModels"; export * from "./traversal/TilesetTraverser"; export * from "./traversal/TraversedTile"; export * from "./traversal/TraversalCallback"; - diff --git a/src/io/TilesetSourceResourceResolver.ts b/src/io/TilesetSourceResourceResolver.ts index fd059166..4aa44548 100644 --- a/src/io/TilesetSourceResourceResolver.ts +++ b/src/io/TilesetSourceResourceResolver.ts @@ -16,10 +16,7 @@ export class TilesetSourceResourceResolver implements ResourceResolver { private readonly _basePath: string; private readonly _tilesetSource: TilesetSource; - constructor( - basePath: string, - tilesetSource: TilesetSource - ) { + constructor(basePath: string, tilesetSource: TilesetSource) { this._basePath = basePath; this._tilesetSource = tilesetSource; } diff --git a/src/metadata/ArrayValues.ts b/src/metadata/ArrayValues.ts index 03327422..a218a4ba 100644 --- a/src/metadata/ArrayValues.ts +++ b/src/metadata/ArrayValues.ts @@ -36,7 +36,7 @@ type NumberND = NumberScalar | NumberND[]; * that the values have the same structure, i.e. they are * both numeric/numbers or arrays with the same length. If this * is not the case, then a `MetadataError` will be thrown. - * + * * @internal */ export class ArrayValues { diff --git a/src/metadata/ClassProperties.ts b/src/metadata/ClassProperties.ts index 35355aca..91913ca8 100644 --- a/src/metadata/ClassProperties.ts +++ b/src/metadata/ClassProperties.ts @@ -7,7 +7,7 @@ import { ClassProperty } from "../structure/Metadata/ClassProperty"; /** * Utility methods related to `ClassProperty` objects - * + * * @internal */ export class ClassProperties { diff --git a/src/metadata/MetadataError.ts b/src/metadata/MetadataError.ts index 2acbefee..40da61c3 100644 --- a/src/metadata/MetadataError.ts +++ b/src/metadata/MetadataError.ts @@ -3,7 +3,7 @@ * * This may be thrown by methods that create the convenience classes * for this package, when the given inputs are not valid. - * + * * @internal */ export class MetadataError extends Error { diff --git a/src/traversal/SubtreeModel.ts b/src/traversal/SubtreeModel.ts index 6bfc1aea..b29719b1 100644 --- a/src/traversal/SubtreeModel.ts +++ b/src/traversal/SubtreeModel.ts @@ -3,7 +3,7 @@ import { SubtreeMetadataModel } from "./SubtreeMetadataModel"; /** * An interface that summarizes the information for a subtree. - * + * * @internal */ export interface SubtreeModel { diff --git a/src/traversal/SubtreeModels.ts b/src/traversal/SubtreeModels.ts index 2837075a..1b40ff15 100644 --- a/src/traversal/SubtreeModels.ts +++ b/src/traversal/SubtreeModels.ts @@ -25,7 +25,7 @@ import { SubtreeMetadataModels } from "./SubtreeMetadataModels"; * The methods will resolve the data for a subtree, based on the template * URI from the implicit tiling and the root coordinates of the subtree, * and offer this information as `SubtreeModel` objects. - * + * * @internal */ export class SubtreeModels { From 5e7870dd8dcccf445e8cdfc6404319e8a024a62b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 19 Mar 2023 17:15:15 +0100 Subject: [PATCH 03/60] Generalize content types. Basic image types support. --- specs/ContentDataTypesSpec.ts | 14 +++++ specs/TileFormatsSpec.ts | 6 +- specs/data/contentTypes/README.md | 2 + specs/data/contentTypes/content.jpg | Bin 0 -> 723 bytes specs/data/contentTypes/content.png | Bin 0 -> 119 bytes src/base/Buffers.ts | 36 ++++++++--- src/contentTypes/BufferedContentData.ts | 17 ++--- src/contentTypes/ContentData.ts | 9 ++- src/contentTypes/ContentDataTypeRegistry.ts | 65 ++++++++++++++++---- src/contentTypes/ContentDataTypes.ts | 2 + src/contentTypes/LazyContentData.ts | 19 +++--- src/tileFormats/TileFormats.ts | 6 +- 12 files changed, 129 insertions(+), 47 deletions(-) create mode 100644 specs/data/contentTypes/content.jpg create mode 100644 specs/data/contentTypes/content.png diff --git a/specs/ContentDataTypesSpec.ts b/specs/ContentDataTypesSpec.ts index 6cc461f4..1f151681 100644 --- a/specs/ContentDataTypesSpec.ts +++ b/specs/ContentDataTypesSpec.ts @@ -59,6 +59,20 @@ describe("ContentDataTypeRegistry.findContentDataType", function () { expect(type).toEqual(ContentDataTypes.CONTENT_TYPE_SUBT); }); + it("detects PNG", async function () { + const contentUri = "specs/data/contentTypes/content.png"; + const c = BufferedContentData.create(contentUri); + const type = await ContentDataTypeRegistry.findContentDataType(c); + expect(type).toEqual(ContentDataTypes.CONTENT_TYPE_PNG); + }); + + it("detects JPEG", async function () { + const contentUri = "specs/data/contentTypes/content.jpg"; + const c = BufferedContentData.create(contentUri); + const type = await ContentDataTypeRegistry.findContentDataType(c); + expect(type).toEqual(ContentDataTypes.CONTENT_TYPE_JPEG); + }); + it("detects GEOJSON", async function () { const contentUri = "specs/data/contentTypes/content.geojson"; const c = BufferedContentData.create(contentUri); diff --git a/specs/TileFormatsSpec.ts b/specs/TileFormatsSpec.ts index bcb2e12f..376a4123 100644 --- a/specs/TileFormatsSpec.ts +++ b/specs/TileFormatsSpec.ts @@ -271,7 +271,7 @@ describe("TileFormats", function () { ]); const cmpt = TileFormats.createCompositeTileDataBuffer(cmptTileData); - const magic = Buffers.getMagic(cmpt); + const magic = Buffers.getMagicString(cmpt); const version = cmpt.readUInt32LE(4); const byteLength = cmpt.readUInt32LE(8); const tilesLength = cmpt.readUInt32LE(12); @@ -288,12 +288,12 @@ describe("TileFormats", function () { expect(byteLength).toBe(expectedByteLength); expect(tilesLength).toBe(2); - const b3dmMagic = Buffers.getMagic(cmpt, headerByteLength); + const b3dmMagic = Buffers.getMagicString(cmpt, headerByteLength); const b3dmByteLength = cmpt.readUInt32LE(headerByteLength + 8); expect(b3dmMagic).toBe("b3dm"); expect(b3dmByteLength % 8 === 0).toBe(true); // b3dm is aligned - const i3dmMagic = Buffers.getMagic(cmpt, headerByteLength + b3dmByteLength); + const i3dmMagic = Buffers.getMagicString(cmpt, headerByteLength + b3dmByteLength); const i3dmByteLength = cmpt.readUInt32LE( headerByteLength + b3dmByteLength + 8 ); diff --git a/specs/data/contentTypes/README.md b/specs/data/contentTypes/README.md index 94580ce7..2dad00a7 100644 --- a/specs/data/contentTypes/README.md +++ b/specs/data/contentTypes/README.md @@ -5,6 +5,8 @@ Tile content files used in the specs. - The `content.gltf` is the (embedded) `Triangle` sample model from https://github.com/KhronosGroup/glTF-Sample-Models/blob/8e9a5a6ad1a2790e2333e3eb48a1ee39f9e0e31b/2.0/ - The `content.glb` is the `Box` sample model from https://github.com/KhronosGroup/glTF-Sample-Models/blob/8e9a5a6ad1a2790e2333e3eb48a1ee39f9e0e31b/2.0/ - The `content.subt` is the subtree file from https://github.com/CesiumGS/3d-tiles-samples/blob/902ea3dca1821a9ef9d23d141f800c68627c452b/1.1/SparseImplicitQuadtree/subtrees/0.0.0.subtree +- The `content.png` is a 1x1 PNG image +- The `content.jpg` is a 1x1 JPEG image The other files are taken from https://github.com/CesiumGS/cesium/tree/c0ec95713b6cde5a91eea320795c84408159dcad/Apps/SampleData/Cesium3DTiles diff --git a/specs/data/contentTypes/content.jpg b/specs/data/contentTypes/content.jpg new file mode 100644 index 0000000000000000000000000000000000000000..dfee859bf19dac7787499b25925f20f79cb42805 GIT binary patch literal 723 zcmex=}T+5Ri6|E+FFJVCMj-APxLK zz#zy0bSxt?qY?v?AS1INnAuRebI{N?Mn?>~P20{M%Pff?d0xX;l1B?$Bv6EF@~*g^hcWGV+@WL##!{0xPx-^*o-vj`uean6T literal 0 HcmV?d00001 diff --git a/specs/data/contentTypes/content.png b/specs/data/contentTypes/content.png new file mode 100644 index 0000000000000000000000000000000000000000..818c71d03f435db011069584cda25c1f66af1a85 GIT binary patch literal 119 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2s6ii6yp7}lMWc?smOq&xaLGB9lH z=l+w(3gmMZctjR6Fz_7)VaDV6D^h@hJf1F&Arj%qKmPx>XJGxu^l!OoV+&B6!PC{x JWt~$(69DNq9##MV literal 0 HcmV?d00001 diff --git a/src/base/Buffers.ts b/src/base/Buffers.ts index cdc82c47..0bda103c 100644 --- a/src/base/Buffers.ts +++ b/src/base/Buffers.ts @@ -4,7 +4,6 @@ import { defined } from "./defined"; import { DataError } from "./DataError"; - /** * Methods related to buffers. * @@ -53,21 +52,45 @@ export class Buffers { } /** - * Obtains the magic header from the given buffer. + * Obtains the magic header bytes from the given buffer. + * + * This returns up to `byteLength` bytes of the given buffer, + * starting at the given byte offset. If the buffer length + * is too small, then a shorter buffer may be returned. + * + * @param buffer - The buffer + * @param byteOffset - The byte offset, usually 0 + * @param byteLength - The byte length + * @returns The magic header. + */ + static getMagicBytes( + buffer: Buffer, + byteOffset: number, + byteLength: number + ): Buffer { + const start = Math.min(buffer.length, byteOffset); + const end = Math.min(buffer.length, start + byteLength); + return buffer.subarray(start, end); + } + + /** + * Obtains the magic header from the given buffer, interpreted + * as an ASCII string * * This returns up to 4 bytes of the given buffer, starting at - * the given byte offset, converted to a string. If the buffer - * length is too small, then a shorter string may be returned. + * the given byte offset, converted to an ASCII string. If the + * buffer length is too small, then a shorter string may be + * returned. * * @param buffer - The buffer * @param byteOffset - The optional byte offset, defaulting to 0 * @returns The magic header. */ - static getMagic(buffer: Buffer, byteOffset?: number): string { + static getMagicString(buffer: Buffer, byteOffset?: number): string { const magicLength = 4; const start = Math.min(buffer.length, byteOffset ?? 0); const end = Math.min(buffer.length, start + magicLength); - return buffer.toString("utf8", start, end); + return buffer.toString("ascii", start, end); } /** @@ -135,7 +158,6 @@ export class Buffers { const message = `Could not parse JSON from buffer: ${e}`; throw new DataError(message); } - } /** diff --git a/src/contentTypes/BufferedContentData.ts b/src/contentTypes/BufferedContentData.ts index 2b43c3a3..47713bb3 100644 --- a/src/contentTypes/BufferedContentData.ts +++ b/src/contentTypes/BufferedContentData.ts @@ -50,12 +50,12 @@ export class BufferedContentData implements ContentData { private readonly _extension: string; /** - * The "magic string" from the content data. This is - * a string consisting of the first (up to) 4 bytes - * of the content data, or the empty string if the - * content data could not be resolved. + * The "magic header bytes" from the content data. These + * are the first (up to) 4 bytes of the content data, + * or the empty buffer if the content data could not + * be resolved. */ - private readonly _magic: string; + private readonly _magic: Buffer; /** * The content data, or `null` if the data could not @@ -87,9 +87,10 @@ export class BufferedContentData implements ContentData { this._uri = uri; this._extension = path.extname(uri).toLowerCase(); if (data) { - this._magic = Buffers.getMagic(data); + const magicHeaderLength = 4; + this._magic = Buffers.getMagicBytes(data, 0, magicHeaderLength); } else { - this._magic = ""; + this._magic = Buffer.alloc(0); } this._data = data; this._parsedObject = undefined; @@ -112,7 +113,7 @@ export class BufferedContentData implements ContentData { } /** {@inheritDoc ContentData.magic} */ - async getMagic(): Promise { + async getMagic(): Promise { return this._magic; } diff --git a/src/contentTypes/ContentData.ts b/src/contentTypes/ContentData.ts index 2ec6bb4c..d4afbc7b 100644 --- a/src/contentTypes/ContentData.ts +++ b/src/contentTypes/ContentData.ts @@ -26,13 +26,12 @@ export interface ContentData { exists(): Promise; /** - * Returns a string that consists of the first 4 bytes - * of the buffer data (or fewer, if the buffer contains - * fewer than 4 bytes) + * Returns the first 4 bytes of the buffer data (or fewer, if the + * buffer contains fewer than 4 bytes) * - * @returns The magic string + * @returns The magic bytes */ - getMagic(): Promise; + getMagic(): Promise; /** * Returns the actual content data that was read from the URI. diff --git a/src/contentTypes/ContentDataTypeRegistry.ts b/src/contentTypes/ContentDataTypeRegistry.ts index 3d3eb7a8..f7a8d9c7 100644 --- a/src/contentTypes/ContentDataTypeRegistry.ts +++ b/src/contentTypes/ContentDataTypeRegistry.ts @@ -45,42 +45,57 @@ export class ContentDataTypeRegistry { // In the future, there might be a mechanism for 'overriding' a // previously registered type. ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("glTF"), + ContentDataTypeRegistry.byMagicString("glTF"), ContentDataTypes.CONTENT_TYPE_GLB ); ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("b3dm"), + ContentDataTypeRegistry.byMagicString("b3dm"), ContentDataTypes.CONTENT_TYPE_B3DM ); ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("i3dm"), + ContentDataTypeRegistry.byMagicString("i3dm"), ContentDataTypes.CONTENT_TYPE_I3DM ); ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("cmpt"), + ContentDataTypeRegistry.byMagicString("cmpt"), ContentDataTypes.CONTENT_TYPE_CMPT ); ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("pnts"), + ContentDataTypeRegistry.byMagicString("pnts"), ContentDataTypes.CONTENT_TYPE_PNTS ); ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("geom"), + ContentDataTypeRegistry.byMagicString("geom"), ContentDataTypes.CONTENT_TYPE_GEOM ); + ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("vctr"), + ContentDataTypeRegistry.byMagicString("vctr"), ContentDataTypes.CONTENT_TYPE_VCTR ); + ContentDataTypeRegistry.register( - ContentDataTypeRegistry.byMagic("subt"), + ContentDataTypeRegistry.byMagicString("subt"), ContentDataTypes.CONTENT_TYPE_SUBT ); + + const pngMagicHeader = [0x89, 0x50, 0x4e, 0x47]; + ContentDataTypeRegistry.register( + ContentDataTypeRegistry.byMagicBytes(pngMagicHeader), + ContentDataTypes.CONTENT_TYPE_PNG + ); + + const jpegMagicHeader = [0xff, 0xd8, 0xff]; + ContentDataTypeRegistry.register( + ContentDataTypeRegistry.byMagicBytes(jpegMagicHeader), + ContentDataTypes.CONTENT_TYPE_JPEG + ); + ContentDataTypeRegistry.register( ContentDataTypeRegistry.byExtension(".geojson"), ContentDataTypes.CONTENT_TYPE_GEOJSON @@ -131,16 +146,42 @@ export class ContentDataTypeRegistry { /** * Creates a predicate that checks whether the magic header of - * a ContentData matches the given magic header string. + * a ContentData (interpreted as ASCII characters) starts with + * the given magic header string. * * @param magic - The magic header string * @returns The predicate */ - private static byMagic( + private static byMagicString( magic: string ): (contentData: ContentData) => Promise { - const predicate = async (contentData: ContentData) => - (await contentData.getMagic()) === magic; + const predicate = async (contentData: ContentData) => { + const contentMagic = await contentData.getMagic(); + const contentMagicString = contentMagic.toString("ascii"); + return contentMagicString.startsWith(magic); + }; + return predicate; + } + + /** + * Creates a predicate that checks whether the magic header of + * a ContentData starts with the given bytes + * + * @param magic - The magic bytes + * @returns The predicate + */ + private static byMagicBytes( + magic: number[] + ): (contentData: ContentData) => Promise { + const predicate = async (contentData: ContentData) => { + const contentMagic = await contentData.getMagic(); + for (let i = 0; i < magic.length; i++) { + if (contentMagic[i] != magic[i]) { + return false; + } + } + return true; + }; return predicate; } diff --git a/src/contentTypes/ContentDataTypes.ts b/src/contentTypes/ContentDataTypes.ts index 5c7d5a13..47d0ad97 100644 --- a/src/contentTypes/ContentDataTypes.ts +++ b/src/contentTypes/ContentDataTypes.ts @@ -16,6 +16,8 @@ export class ContentDataTypes { static readonly CONTENT_TYPE_GEOM = "CONTENT_TYPE_GEOM"; static readonly CONTENT_TYPE_VCTR = "CONTENT_TYPE_VCTR"; static readonly CONTENT_TYPE_SUBT = "CONTENT_TYPE_SUBT"; + static readonly CONTENT_TYPE_PNG = "CONTENT_TYPE_PNG"; + static readonly CONTENT_TYPE_JPEG = "CONTENT_TYPE_JPEG"; static readonly CONTENT_TYPE_GEOJSON = "CONTENT_TYPE_GEOJSON"; static readonly CONTENT_TYPE_3TZ = "CONTENT_TYPE_3TZ"; diff --git a/src/contentTypes/LazyContentData.ts b/src/contentTypes/LazyContentData.ts index 7e2da7fd..ea89e697 100644 --- a/src/contentTypes/LazyContentData.ts +++ b/src/contentTypes/LazyContentData.ts @@ -42,12 +42,12 @@ export class LazyContentData implements ContentData { private _exists: boolean | undefined; /** - * The "magic string" from the content data. This is - * a string consisting of the first (up to) 4 bytes - * of the content data, or the empty string if the - * content data could not be resolved. + * The "magic header bytes" from the content data. These + * are the first (up to) 4 bytes of the content data, + * or the empty buffer if the content data could not + * be resolved. */ - private _magic: string | undefined; + private _magic: Buffer | undefined; /** * The content data, or `null` if the data could not @@ -117,19 +117,20 @@ export class LazyContentData implements ContentData { } /** {@inheritDoc ContentData.getMagic} */ - async getMagic(): Promise { + async getMagic(): Promise { if (defined(this._magic)) { return this._magic; } + const magicHeaderLength = 4; const partialData = await this._resourceResolver.resolveDataPartial( this._uri, - 4 + magicHeaderLength ); if (partialData) { - this._magic = Buffers.getMagic(partialData); + this._magic = Buffers.getMagicBytes(partialData, 0, magicHeaderLength); this._exists = true; } else { - this._magic = ""; + this._magic = Buffer.alloc(0); this._exists = false; } return this._magic; diff --git a/src/tileFormats/TileFormats.ts b/src/tileFormats/TileFormats.ts index 2b4da9d5..5d0f47dd 100644 --- a/src/tileFormats/TileFormats.ts +++ b/src/tileFormats/TileFormats.ts @@ -19,7 +19,7 @@ export class TileFormats { * @returns Whether the given buffer contains composite tile data */ static isComposite(buffer: Buffer): boolean { - const magic = Buffers.getMagic(buffer); + const magic = Buffers.getMagicString(buffer); return magic === "cmpt"; } @@ -37,7 +37,7 @@ export class TileFormats { */ static readCompositeTileData(buffer: Buffer): CompositeTileData { // Basic checks for magic number, length and version - const magic = Buffers.getMagic(buffer); + const magic = Buffers.getMagicString(buffer); if (magic !== "cmpt") { throw new TileFormatError(`Expected magic "cmpt", but found "${magic}"`); } @@ -106,7 +106,7 @@ export class TileFormats { */ static readTileData(buffer: Buffer): TileData { // Basic checks for magic number, length and version - const magic = Buffers.getMagic(buffer); + const magic = Buffers.getMagicString(buffer); if (magic !== "b3dm" && magic !== "pnts" && magic !== "i3dm") { throw new TileFormatError( `Expected magic "b3dm", "i3dm", or "pnts", but found "${magic}"` From 99e5685dbaaab98b10714303bfdcb4f11b4e1e98 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 19 Mar 2023 17:15:25 +0100 Subject: [PATCH 04/60] Formatting --- src/base/DataError.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/base/DataError.ts b/src/base/DataError.ts index 083afffe..7583154f 100644 --- a/src/base/DataError.ts +++ b/src/base/DataError.ts @@ -1,11 +1,11 @@ /** * An error that may be thrown to indicate that input - * data was invalid. - * + * data was invalid. + * * This may refer to buffers that have been expected to * contain a certain type of data, but did not. For * example, a buffer that looked like a GZIpped buffer, - * but turned out to be invalid, or a buffer that + * but turned out to be invalid, or a buffer that * looked like it contained valid JSON, but did not. * * @internal From ff708ac50354fd33252e7bb5cff88149c0316b5a Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 19 Mar 2023 17:16:59 +0100 Subject: [PATCH 05/60] Update method name from previous changes --- src/traversal/SubtreeModels.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traversal/SubtreeModels.ts b/src/traversal/SubtreeModels.ts index 1b40ff15..aaa6550e 100644 --- a/src/traversal/SubtreeModels.ts +++ b/src/traversal/SubtreeModels.ts @@ -111,7 +111,7 @@ export class SubtreeModels { // For SUBT (binary subtree data), create the SubtreeModel // from the whole buffer - const isSubt = Buffers.getMagic(subtreeData) === "subt"; + const isSubt = Buffers.getMagicString(subtreeData) === "subt"; if (isSubt) { const binarySubtreeData = await BinarySubtreeDataResolver.resolveFromBuffer( From 088e59fa56227ab8af3bf4d99a5f2f4ce8cd3aa6 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 19 Mar 2023 17:51:25 +0100 Subject: [PATCH 06/60] First draft of a generic tileset processor --- src/tilesetProcessing/TilesetProcessor.ts | 597 ++++++++++++++++++++++ 1 file changed, 597 insertions(+) create mode 100644 src/tilesetProcessing/TilesetProcessor.ts diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetProcessor.ts new file mode 100644 index 00000000..861c3983 --- /dev/null +++ b/src/tilesetProcessing/TilesetProcessor.ts @@ -0,0 +1,597 @@ +import { Buffers } from "../base/Buffers"; +import { DeveloperError } from "../base/DeveloperError"; + +import { TilesetSourceResourceResolver } from "../io/TilesetSourceResourceResolver"; + +import { BufferedContentData } from "../contentTypes/BufferedContentData"; +import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; + +import { Tile } from "../structure/Tile"; +import { Tileset } from "../structure/Tileset"; +import { Content } from "../structure/Content"; +import { Schema } from "../structure/Metadata/Schema"; + +import { TilesetError } from "../tilesetData/TilesetError"; +import { TilesetSource } from "../tilesetData/TilesetSource"; +import { TilesetTarget } from "../tilesetData/TilesetTarget"; +import { TilesetTargets } from "../tilesetData/TilesetTargets"; +import { TilesetSources } from "../tilesetData/TilesetSources"; +import { TilesetEntry } from "../tilesetData/TilesetEntry"; + +import { TilesetTraverser } from "../traversal/TilesetTraverser"; + +import { Tiles } from "../tilesets/Tiles"; +import { Tilesets } from "../tilesets/Tilesets"; + + +/** + */ +export class TilesetProcessor { + /** + * A function that will receive log messages + */ + private readonly logCallback: (message: any) => void; + + /** + * The tileset source for the input + */ + private tilesetSource: TilesetSource | undefined; + + /** + * The tileset target for the output. + */ + private tilesetTarget: TilesetTarget | undefined; + + /** + * The set of keys (file names) from the current tileset source + * that have already been processed. + */ + private processedSourceKeys: { [key: string]: boolean } = {}; + + /** + * Creates a new instance + * + * @param quiet - Whether log messages should be omitted + */ + constructor(quiet?: boolean) { + if (quiet !== true) { + this.logCallback = (message: any) => console.log(message); + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + this.logCallback = (message: any) => {}; + } + } + + private log(message: any): void { + this.logCallback(message); + } + + /** + * Process the specified source tileset, and write it to the given + * target. + * + * @param tilesetSourceName - The tileset source name + * @param tilesetTargetName - The tileset target name + * @param overwrite Whether the target should be overwritten if + * it already exists + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed, + * or when the output already exists and `overwrite` was `false`. + */ + async process( + tilesetSourceName: string, + tilesetTargetName: string, + overwrite: boolean + ): Promise { + // TODO Somehow ensure that the source is closed + // if the target throws up (try-with-resources FTW) + const tilesetSource = TilesetSources.createAndOpen(tilesetSourceName); + const tilesetTarget = TilesetTargets.createAndBegin( + tilesetTargetName, + overwrite + ); + + this.tilesetSource = tilesetSource; + this.tilesetTarget = tilesetTarget; + + const tilesetSourceJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetSourceName); + + const tilesetTargetJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetTargetName); + + await this.processInternal( + tilesetSourceJsonFileName, + tilesetTargetJsonFileName + ); + + tilesetSource.close(); + await tilesetTarget.end(); + + this.tilesetSource = undefined; + this.tilesetTarget = undefined; + Object.keys(this.processedSourceKeys).forEach( + (key) => delete this.processedSourceKeys[key] + ); + } + + /** + * Internal (top-level) method for the processing. + * + * It reads the tileset JSON from the specified source, passes + * it to `processTileset`, and writes the tileset JSON to the + * specified target. + * + * Any operations that affect files other than the tileset JSON + * file are part of `processTileset` + * + * @param tilesetSourceName - The tileset source name + * @param tilesetTargetName - The tileset target name + * @returns A promise that resolves when the process is finished + * @throws DeveloperError When the source or target is not opened + * @throws TilesetError When the input could not be processed + */ + private async processInternal( + tilesetSourceJsonFileName: string, + tilesetTargetJsonFileName: string + ): Promise { + if (!this.tilesetSource || !this.tilesetTarget) { + throw new DeveloperError("The source and target must be defined"); + } + + // Obtain the tileset object from the tileset JSON file + const parsedTileset = this.parseSourceValue( + tilesetSourceJsonFileName + ); + + // Resolve the schema, either from the `tileset.schema` + // or the `tileset.schemaUri` + const schema = this.resolveSchema(parsedTileset.result); + + // Process the actual tileset + await this.processTileset(parsedTileset.result, schema); + + // Store the resulting tileset as JSON + this.storeTargetValue( + tilesetTargetJsonFileName, + parsedTileset.wasZipped, + parsedTileset.result + ); + } + + /** + * It parses the JSON from the value with the given key (file name), + * and returns the parsed result, AND information of whether the + * input was zipped. + * + * This is mainly a convenience function to emulate the behavior of the + * "legacy" tools in terms of handling the tileset JSON: When writing + * the tileset JSON data to the target, then it should zip that JSON + * data if and only if it was zipped in the input. + * + * See `storeTargetValue` for the counterpart of this method. + * + * In the future, there might be mechanisms for a more fine-grained + * control over whether certain files should be zipped or not... + * + * @param key - The key (file name) + * @returns A structure containing the `wasZipped` information, and + * the parsed result + * @throws TilesetError If the source is not opened, the specified + * entry cannot be found, or the entry data could not be unzipped, + * or its contents could not be parsed as JSON. + */ + private parseSourceValue(key: string): { wasZipped: boolean; result: T } { + let value = this.getSourceValue(key); + let wasZipped = false; + if (Buffers.isGzipped(value)) { + wasZipped = true; + try { + value = Buffers.gunzip(value); + } catch (e) { + const message = `Could not unzip ${key}: ${e}`; + throw new TilesetError(message); + } + } + try { + const result = JSON.parse(value.toString()) as T; + return { + wasZipped: wasZipped, + result: result, + }; + } catch (e) { + const message = `Could not parse ${key}: ${e}`; + throw new TilesetError(message); + } + } + + /** + * Convert the given object into a JSON string, put it into a buffer, + * zip it (based on the `doZip` flag), and put the result into the + * tileset target. + * + * This is only intended for the "legacy" handling of the tileset + * JSON data, and is the counterpart of `parseSourceValue`. See + * `parseSourceValue` for details. + * + * @param key - The key (file name) + * @param doZip - Whether the output should be zipped + * @param object - The object for which the JSON should be stored + * @throws DeveloperError When the target is not opened + */ + private storeTargetValue(key: string, doZip: boolean, object: object) { + if (!this.tilesetTarget) { + throw new DeveloperError("The target must be defined"); + } + const jsonString = JSON.stringify(object, null, 2); + let jsonBuffer = Buffer.from(jsonString); + if (doZip) { + jsonBuffer = Buffers.gzip(jsonBuffer); + } + this.tilesetTarget.addEntry(key, jsonBuffer); + } + + /** + * Obtains the value for the given key from the current tileset source, + * throwing an error if the source is not opened, or when the + * given key cannot be found. + * + * @param key - The key (file name) + * @returns The value (file contents) + * @throws DeveloperError When the source is not opened + * @throws TilesetError When the given key cannot be found + */ + private getSourceValue(key: string): Buffer { + if (!this.tilesetSource) { + throw new DeveloperError("The source must be defined"); + } + const buffer = this.tilesetSource.getValue(key); + if (!buffer) { + const message = `No ${key} found in input`; + throw new TilesetError(message); + } + return buffer; + } + + /** + * Resolve the `Schema` for the given tileset. + * + * This is either the `tileset.schema`, or the schema that is + * obtained from the `tileset.schemaUri`, or `undefined` if + * neither of them are present. + * + * @param tileset - The tileset + * @returns The `Schema`, or `undefined` if there is none + * @throws DeveloperError If the source is not opened + * @throws TilesetError If the schema from the `schemaUri` + * could not be resolved or parsed. + */ + private resolveSchema(tileset: Tileset): Schema | undefined { + if (!this.tilesetSource) { + throw new DeveloperError("The source must be defined"); + } + if (tileset.schema) { + return tileset.schema; + } + if (tileset.schemaUri) { + const parsedSchema = this.parseSourceValue(tileset.schemaUri); + return parsedSchema.result; + } + return undefined; + } + + /** + * Process the given tileset. + * + * This will process... + * - all explicit tile content entries + * - all tile content entries (including implicit ones) + * - all entries (including all of the above) + * if they haven't been processed yet. + * + * All these operations will eventually end up in the + * `processEntryImpl` method - see this method for details. + * + * @param tileset - The tileset + * @param schema - The optional metadata schema for the tileset + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processTileset( + tileset: Tileset, + schema: Schema | undefined + ): Promise { + this.log(`Processing explicit tiles`); + await this.processExplicitTilesContentEntries(tileset); + + // TODO Something like "processImplicitTemplateUris" + // and maybe something explicit for the subtrees + // as well... + + this.log(`Processing all tiles`); + await this.processAllTilesContentEntries(tileset, schema); + + this.log(`Processing all entries`); + await this.processAllEntries(); + } + + /** + * Process all entries that are tile content of explicit tiles. + * + * Each entry will eventually be processed with the + * `processEntryImpl` method - see this method for details. + * + * @param tileset - The tileset + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processExplicitTilesContentEntries( + tileset: Tileset + ): Promise { + const root = tileset.root; + await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { + const tile = tilePath[tilePath.length - 1]; + await this.processExplicitTileContentEntries(tile); + return true; + }); + } + + /** + * Process all entries that are content of the given tile. + * + * Each entry will eventually be processed with the + * `processEntryImpl` method - see this method for details. + * + * @param tile - The tile + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processExplicitTileContentEntries(tile: Tile): Promise { + // For roots of implicit tilesets, the content URI + // is a template URI (i.e. they are not explicit, + // and therefore not considered here) + if (tile.implicitTiling) { + return; + } + if (tile.content) { + const content = tile.content; + const targetEntry = await this.processEntry(content.uri); + if (targetEntry) { + content.uri = targetEntry.key; + } else { + tile.content = undefined; + } + } else if (tile.contents) { + const newContents: Content[] = []; + for (const content of tile.contents) { + const targetEntry = await this.processEntry(content.uri); + if (targetEntry) { + content.uri = targetEntry.key; + newContents.push(content); + } + } + if (newContents.length > 0) { + tile.contents = newContents; + } else { + tile.contents = undefined; + } + } + } + + /** + * Process all entries that are tile content (both of explicit + * and implicit tiles). + * + * Each entry will eventually be processed with the + * `processEntryImpl` method - see this method for details. + * + * @param tileset - The tileset + * @param schema - The optional metadata schema for the tileset + * @returns A promise that resolves when the process is finished + * @throws DeveloperError When the source is not opened + * @throws TilesetError When the input could not be processed + */ + private async processAllTilesContentEntries( + tileset: Tileset, + schema: Schema | undefined + ): Promise { + if (!this.tilesetSource) { + throw new DeveloperError("The source must be defined"); + } + const resourceResolver = new TilesetSourceResourceResolver( + ".", + this.tilesetSource + ); + const depthFirst = false; + await TilesetTraverser.traverse( + tileset, + schema, + resourceResolver, + async (traversedTile) => { + // 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()) { + const contentUris = traversedTile + .getFinalContents() + .map((c) => c.uri); + for (const contentUri of contentUris) { + this.processEntry(contentUri); + } + } + return true; + }, + depthFirst + ); + } + + /** + * Process all entries that are contained in the current + * tileset source. + * + * Each entry will eventually be processed with the + * `processEntryImpl` method - see this method for details. + * + * @param tileset - The tileset + * @returns A promise that resolves when the process is finished + * @throws DeveloperError When the source or target is not opened + * @throws TilesetError When the input could not be processed + */ + private async processAllEntries(): Promise { + if (!this.tilesetSource || !this.tilesetTarget) { + throw new DeveloperError("The source and target must be defined"); + } + const entries = TilesetSources.getEntries(this.tilesetSource); + for (const entry of entries) { + const key = entry.key; + await this.processEntry(key); + } + } + + /** + * Process the specified entry. + * + * If the entry with the specified entry was already processed, + * then this method does nothing. + * + * Otherwise, the specified entry will be looked up in the tileset + * source. Its content type will be determined. The source entry + * will be passed to `processEntryImpl`, which returns a target + * entry. If the target entry is defined, then it is inserted + * into the tileset target. + * + * This is the "staging" method for `processEntryImpl` (see this + * method for further details) + * + * @param tileset - The tileset + * @returns A promise that resolves when the process is finished, + * containing either the new entry that was put into the tileset + * target, or `undefined` when the entry was supposed to be + * omitted in the target. + * @throws DeveloperError When the source or target is not opened + * @throws TilesetError When the input could not be processed + */ + private async processEntry(key: string): Promise { + if (!this.tilesetSource || !this.tilesetTarget) { + throw new DeveloperError("The source and target must be defined"); + } + + const sourceKey = key; + if (this.processedSourceKeys[sourceKey] === true) { + return; + } + this.processedSourceKeys[sourceKey] = true; + + const sourceValue = this.tilesetSource.getValue(sourceKey); + if (!sourceValue) { + const message = `No ${sourceKey} found in input`; + throw new TilesetError(message); + } + const sourceEntry: TilesetEntry = { + key: sourceKey, + value: sourceValue, + }; + const type = await this.determineContentDataType(sourceKey, sourceValue); + const targetEntry = await this.processEntryImpl(sourceEntry, type); + + if (targetEntry) { + this.tilesetTarget.addEntry(targetEntry.key, targetEntry.value); + } + return targetEntry; + } + + /** + * TODO Consider this, for example, for "inlining" references to + * external PNGs into a GLB, and then say + * markAsProcessed("./images/referredToByGlb.png"); + * to omit it in the output... + * + * A method that can be called by implementations, to mark a certain + * file as already being processed, and no longer be considered in + * subsequent steps. + * + * @param key - The key (file name) + */ + markAsProcessed(key: string) { + this.processedSourceKeys[key] = true; + } + + /** + * Process an entry. + * + * This will be called ONCE for each entry of the tileset source, + * and return an entry that is supposed to be put into the tileset + * target. + * + * It receives the source entry, which may represent a content + * of an (explicit) tile, a content of an implicit tile, or just + * one entry of the tileset source (i.e. a "file" that is not + * a tile content). + * + * It returns the "processed" entry that is supposed to put into + * the tileset target. If the returned entry is `undefined`, then + * this means that the entry should be omitted in the target. + * + * Otherwise, the returned entry may have a different `key` + * (file name), and/or a modified `value` (file data). This + * entry will be put into the tileset target. + * + * Note that a modification of the `key` have different implications: + * + * - For explicit tile content, changes in the `key` will automatically + * be taken into account, by updating the `content.uri` accordingly + * - For implicit tile content, changes in the `key` have to be taken + * into account by updating template URIs. + * - For files, changes in the `key` have to be taken into account by + * domain-specific knowledge about what these files actually are. + * + * @param sourceEntry - The source entry + * @param type - The type of the entry data (see `ContentDataTypes`), + * or `undefined` if the type could not be determined. + * @returns A promise that resolves when the process is finished, + * containing either the new entry, or `undefined` when the entry + * was supposed to be removed (i.e. omitted in the target). + * @throws TilesetError When the input could not be processed + */ + async processEntryImpl( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise { + const sourceKey = sourceEntry.key; + const sourceValue = sourceEntry.value; + + this.log(`Processing source: ${sourceKey} with type ${type}`); + + // TODO This is a no-op: + const targetKey = sourceKey; + const targetValue = sourceValue; + + this.log(` target: ${targetKey}`); + + const targetEntry = { + key: targetKey, + value: targetValue, + }; + return targetEntry; + } + + /** + * Determine the type of the given content data. + * + * The string will either be one of the `ContentDataTypes` strings, + * or `undefined` if the type cannot be determined. + * + * @param key - The key (file name) + * @param value - The value (file contents) + * @returns A promise with the content data type string + */ + private async determineContentDataType( + key: string, + value: Buffer + ): Promise { + const contentData = new BufferedContentData(key, value); + const type = await ContentDataTypeRegistry.findContentDataType(contentData); + return type; + } +} + From ee74c736ef8bf4d82a00fc99c14025a03c4b4aef Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 20 Mar 2023 19:47:33 +0100 Subject: [PATCH 07/60] Cleanups for TraversedTile --- demos/TraversalStatsDemo.ts | 9 ++- src/traversal/ExplicitTraversedTile.ts | 47 +++++++++----- src/traversal/ImplicitTraversedTile.ts | 28 +++------ src/traversal/TraversedTile.ts | 87 ++++++++++++++++++++------ 4 files changed, 114 insertions(+), 57 deletions(-) diff --git a/demos/TraversalStatsDemo.ts b/demos/TraversalStatsDemo.ts index fad50e03..4b28a7ad 100644 --- a/demos/TraversalStatsDemo.ts +++ b/demos/TraversalStatsDemo.ts @@ -183,12 +183,11 @@ class TilesetStatsCollector extends StatsCollector { 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()) { + const subtreeUri = traversedTile.getSubtreeUri(); + if (subtreeUri !== undefined) { this.increment("totalNumberOfSubtres"); - } else { + } + if (!traversedTile.isImplicitTilesetRoot()) { // Obtain all content URIs, resolve them, and obtain // the sizes of the corresponding files, storing them // in the "tileFileSize" summary diff --git a/src/traversal/ExplicitTraversedTile.ts b/src/traversal/ExplicitTraversedTile.ts index 5d014ab7..9dedfd28 100644 --- a/src/traversal/ExplicitTraversedTile.ts +++ b/src/traversal/ExplicitTraversedTile.ts @@ -73,6 +73,28 @@ export class ExplicitTraversedTile implements TraversedTile { this._resourceResolver = resourceResolver; } + /** + * Returns the `metadata` from the input JSON that defines the + * `MetadataEntity` that is associated with this tile, or + * `undefined` if the input did not contain a metadata entity. + * + * @returns The `MetadataEntity` object, or `undefined` + */ + getMetadata(): MetadataEntity | undefined { + return this._tile.metadata; + } + + /** + * Returns the `implicitTiling` from the input JSON that defines the + * `TileImplicitTiling` that is associated with this tile, or + * `undefined` if this tile does not define an implicit tiling. + * + * @returns The `TileImplicitTiling` object + */ + getImplicitTiling(): TileImplicitTiling | undefined { + return this._tile.implicitTiling; + } + /** {@inheritDoc TraversedTile.asRawTile} */ asRawTile(): Tile { return this._tile; @@ -198,6 +220,16 @@ export class ExplicitTraversedTile implements TraversedTile { return finalContents; } + /** {@inheritDoc TraversedTile.resolveUri} */ + resolveUri(uri: string): string { + return this._resourceResolver.resolveUri(uri); + } + + /** {@inheritDoc TraversedTile.isImplicitTilesetRoot} */ + isImplicitTilesetRoot() : boolean { + return this._tile.implicitTiling !== undefined; + } + /** {@inheritDoc TraversedTile.getSubtreeUri} */ getSubtreeUri(): string | undefined { const implicitTiling = this._tile.implicitTiling; @@ -214,21 +246,6 @@ export class ExplicitTraversedTile implements TraversedTile { return subtreeUri; } - /** {@inheritDoc TraversedTile.getImplicitTiling} */ - getImplicitTiling(): TileImplicitTiling | undefined { - return this._tile.implicitTiling; - } - - /** {@inheritDoc TraversedTile.getMetadata} */ - getMetadata(): MetadataEntity | undefined { - return this._tile.metadata; - } - - /** {@inheritDoc TraversedTile.resolveUri} */ - resolveUri(uri: string): string { - return this._resourceResolver.resolveUri(uri); - } - // TODO For debugging toString = (): string => { return `ExplicitTraversedTile, level ${this.level}, path ${this.path}`; diff --git a/src/traversal/ImplicitTraversedTile.ts b/src/traversal/ImplicitTraversedTile.ts index f7fecdd5..dc9fc8bc 100644 --- a/src/traversal/ImplicitTraversedTile.ts +++ b/src/traversal/ImplicitTraversedTile.ts @@ -377,6 +377,16 @@ export class ImplicitTraversedTile implements TraversedTile { return contents; } + /** {@inheritDoc TraversedTile.resolveUri} */ + resolveUri(uri: string): string { + return this._resourceResolver.resolveUri(uri); + } + + /** {@inheritDoc TraversedTile.isImplicitTilesetRoot} */ + isImplicitTilesetRoot() : boolean { + return false; + } + /** {@inheritDoc TraversedTile.getSubtreeUri} */ getSubtreeUri(): string | undefined { const localCoordinate = this._localCoordinate; @@ -393,24 +403,6 @@ export class ImplicitTraversedTile implements TraversedTile { return undefined; } - /** {@inheritDoc TraversedTile.getImplicitTiling} */ - getImplicitTiling(): TileImplicitTiling | undefined { - const localCoordinate = this._localCoordinate; - if (localCoordinate.level === 0) { - return this._implicitTiling; - } - } - - /** {@inheritDoc TraversedTile.getMetadata} */ - getMetadata(): MetadataEntity | undefined { - return undefined; - } - - /** {@inheritDoc TraversedTile.resolveUri} */ - resolveUri(uri: string): string { - return this._resourceResolver.resolveUri(uri); - } - // TODO For debugging toString = (): string => { return ( diff --git a/src/traversal/TraversedTile.ts b/src/traversal/TraversedTile.ts index f9a34591..1f9cfefb 100644 --- a/src/traversal/TraversedTile.ts +++ b/src/traversal/TraversedTile.ts @@ -1,7 +1,5 @@ import { Tile } from "../structure/Tile"; import { Content } from "../structure/Content"; -import { MetadataEntity } from "../structure/MetadataEntity"; -import { TileImplicitTiling } from "../structure/TileImplicitTiling"; /** * An interface that summarizes context information for @@ -15,15 +13,13 @@ export interface TraversedTile { * of the tile. This is just a plain data structure corresponding * to the tile. * - * The returned object reflects the "raw" state of the tile that + * The returned object reflects the "raw" state of the object that * is either contained in the tileset JSON, or derived from the - * subdivision rules of implicit tiles. - * - * Specifically: This is the state BEFORE any semantic-based - * overrides have been applied. When there is metadata - * associated with the tile, and this metadata has semantics - * that override certain tile properties, then these overrides - * are NOT reflected in the returned tile. + * subdivision rules of implicit tiles. Specifically: This is the + * state BEFORE any semantic-based overrides have been applied. + * When there is metadata associated with the object, and this + * metadata has semantics that override certain properties, then + * these overrides are NOT reflected in the returned object. * * In order to obtain a tile where the semantic-based overrides * are applied, `asFinalTile` can be used. @@ -109,21 +105,74 @@ export interface TraversedTile { * content), or a single-element array (when the tile has a * single `tile.content` object), or an array that resembles * the `tile.contents` array. + * + * Note that the returned content objects may contain + * template URIs for tiles that are roots of implicit + * tilesets. Use `isImplicitTilesetRoot` to detect + * whether this tile is the root of an implicit tileset, + * and the content URIs may be template URIs. + * + * The returned content objects reflect the state BEFORE + * any semantic-based overrides have been applied. + * See `asRawTile` for details about the semantic-based + * overrides. * * @returns The contents */ getRawContents(): Content[]; - // TODO Document or improve this - the same difference as between - // asRawTile and asFinalTile + /** + * Returns the `Content` objects of the tile. + * + * The returned objects correspond to the ones returned by + * `getRawContents`, but in a state where semantic-based + * overrides have been applied. + * + * See `asRawTile` and `asFinalTile` for details about the + * semantic-based overrides. + * + * @returns The contents + */ getFinalContents(): Content[]; - // TODO Some information has to be exposed here solely - // for the validation. This should preferably not be - // visible in this interface. The traversal might be - // refactored to hide this information here. - getSubtreeUri(): string | undefined; - getImplicitTiling(): TileImplicitTiling | undefined; - getMetadata(): MetadataEntity | undefined; + /** + * Resolves the given URI against the context in which this + * tile appears. + * + * This is primarily intended for (relative) content URIs. + * It will usually just resolve the given URI against the + * path that contained the tileset, resulting in an absolute + * URI that can be used to access the content. + * + * @param uri - The (content) uri + * @returns The resolved URI + */ resolveUri(uri: string): string; + + /** + * Returns whether this tile is the root of an implicit tileset. + * + * This is `true` for tiles that appear in the explicit + * tile hierarchy of a tileset JSON, and which have a + * `tile.implicitTiling` property. + * + * For these tiles, the `content.uri` properties do not define + * actual URIs, but *template* URIs. + * + * @returns Whether this is an implicit tileset root + */ + isImplicitTilesetRoot() : boolean; + + /** + * Returns the URI of the subtree file for this tile, or + * `undefined` if this is not the root of a subtree. + * + * If this tile is the root of a subtree in an implicit tileset, then + * the returned URI will contain the actual subtree URI that was + * created by substituting the coordinates of this tile into the + * `implicitTiling.subtrees.uri` template URI. + * + * @returns The subtree URI, or `undefined` + */ + getSubtreeUri(): string | undefined; } From 6fe6d8cc8e457299e6fcf58ae2c0e285010bbe1e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 20 Mar 2023 19:48:28 +0100 Subject: [PATCH 08/60] Formatting --- specs/TileFormatsSpec.ts | 5 ++- src/traversal/ExplicitTraversedTile.ts | 10 ++--- src/traversal/ImplicitTraversedTile.ts | 2 +- src/traversal/TraversedTile.ts | 54 +++++++++++++------------- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/specs/TileFormatsSpec.ts b/specs/TileFormatsSpec.ts index 376a4123..ec7784ec 100644 --- a/specs/TileFormatsSpec.ts +++ b/specs/TileFormatsSpec.ts @@ -293,7 +293,10 @@ describe("TileFormats", function () { expect(b3dmMagic).toBe("b3dm"); expect(b3dmByteLength % 8 === 0).toBe(true); // b3dm is aligned - const i3dmMagic = Buffers.getMagicString(cmpt, headerByteLength + b3dmByteLength); + const i3dmMagic = Buffers.getMagicString( + cmpt, + headerByteLength + b3dmByteLength + ); const i3dmByteLength = cmpt.readUInt32LE( headerByteLength + b3dmByteLength + 8 ); diff --git a/src/traversal/ExplicitTraversedTile.ts b/src/traversal/ExplicitTraversedTile.ts index 9dedfd28..fbc87180 100644 --- a/src/traversal/ExplicitTraversedTile.ts +++ b/src/traversal/ExplicitTraversedTile.ts @@ -75,9 +75,9 @@ export class ExplicitTraversedTile implements TraversedTile { /** * Returns the `metadata` from the input JSON that defines the - * `MetadataEntity` that is associated with this tile, or + * `MetadataEntity` that is associated with this tile, or * `undefined` if the input did not contain a metadata entity. - * + * * @returns The `MetadataEntity` object, or `undefined` */ getMetadata(): MetadataEntity | undefined { @@ -86,9 +86,9 @@ export class ExplicitTraversedTile implements TraversedTile { /** * Returns the `implicitTiling` from the input JSON that defines the - * `TileImplicitTiling` that is associated with this tile, or + * `TileImplicitTiling` that is associated with this tile, or * `undefined` if this tile does not define an implicit tiling. - * + * * @returns The `TileImplicitTiling` object */ getImplicitTiling(): TileImplicitTiling | undefined { @@ -226,7 +226,7 @@ export class ExplicitTraversedTile implements TraversedTile { } /** {@inheritDoc TraversedTile.isImplicitTilesetRoot} */ - isImplicitTilesetRoot() : boolean { + isImplicitTilesetRoot(): boolean { return this._tile.implicitTiling !== undefined; } diff --git a/src/traversal/ImplicitTraversedTile.ts b/src/traversal/ImplicitTraversedTile.ts index dc9fc8bc..c0e78bb2 100644 --- a/src/traversal/ImplicitTraversedTile.ts +++ b/src/traversal/ImplicitTraversedTile.ts @@ -383,7 +383,7 @@ export class ImplicitTraversedTile implements TraversedTile { } /** {@inheritDoc TraversedTile.isImplicitTilesetRoot} */ - isImplicitTilesetRoot() : boolean { + isImplicitTilesetRoot(): boolean { return false; } diff --git a/src/traversal/TraversedTile.ts b/src/traversal/TraversedTile.ts index 1f9cfefb..daee0144 100644 --- a/src/traversal/TraversedTile.ts +++ b/src/traversal/TraversedTile.ts @@ -15,10 +15,10 @@ export interface TraversedTile { * * The returned object reflects the "raw" state of the object that * is either contained in the tileset JSON, or derived from the - * subdivision rules of implicit tiles. Specifically: This is the - * state BEFORE any semantic-based overrides have been applied. - * When there is metadata associated with the object, and this - * metadata has semantics that override certain properties, then + * subdivision rules of implicit tiles. Specifically: This is the + * state BEFORE any semantic-based overrides have been applied. + * When there is metadata associated with the object, and this + * metadata has semantics that override certain properties, then * these overrides are NOT reflected in the returned object. * * In order to obtain a tile where the semantic-based overrides @@ -105,15 +105,15 @@ export interface TraversedTile { * content), or a single-element array (when the tile has a * single `tile.content` object), or an array that resembles * the `tile.contents` array. - * + * * Note that the returned content objects may contain * template URIs for tiles that are roots of implicit - * tilesets. Use `isImplicitTilesetRoot` to detect + * tilesets. Use `isImplicitTilesetRoot` to detect * whether this tile is the root of an implicit tileset, * and the content URIs may be template URIs. - * + * * The returned content objects reflect the state BEFORE - * any semantic-based overrides have been applied. + * any semantic-based overrides have been applied. * See `asRawTile` for details about the semantic-based * overrides. * @@ -125,10 +125,10 @@ export interface TraversedTile { * Returns the `Content` objects of the tile. * * The returned objects correspond to the ones returned by - * `getRawContents`, but in a state where semantic-based - * overrides have been applied. - * - * See `asRawTile` and `asFinalTile` for details about the + * `getRawContents`, but in a state where semantic-based + * overrides have been applied. + * + * See `asRawTile` and `asFinalTile` for details about the * semantic-based overrides. * * @returns The contents @@ -138,12 +138,12 @@ export interface TraversedTile { /** * Resolves the given URI against the context in which this * tile appears. - * - * This is primarily intended for (relative) content URIs. - * It will usually just resolve the given URI against the + * + * This is primarily intended for (relative) content URIs. + * It will usually just resolve the given URI against the * path that contained the tileset, resulting in an absolute * URI that can be used to access the content. - * + * * @param uri - The (content) uri * @returns The resolved URI */ @@ -151,28 +151,28 @@ export interface TraversedTile { /** * Returns whether this tile is the root of an implicit tileset. - * + * * This is `true` for tiles that appear in the explicit * tile hierarchy of a tileset JSON, and which have a * `tile.implicitTiling` property. - * + * * For these tiles, the `content.uri` properties do not define * actual URIs, but *template* URIs. - * + * * @returns Whether this is an implicit tileset root */ - isImplicitTilesetRoot() : boolean; + isImplicitTilesetRoot(): boolean; /** - * Returns the URI of the subtree file for this tile, or + * Returns the URI of the subtree file for this tile, or * `undefined` if this is not the root of a subtree. - * - * If this tile is the root of a subtree in an implicit tileset, then - * the returned URI will contain the actual subtree URI that was + * + * If this tile is the root of a subtree in an implicit tileset, then + * the returned URI will contain the actual subtree URI that was * created by substituting the coordinates of this tile into the - * `implicitTiling.subtrees.uri` template URI. - * - * @returns The subtree URI, or `undefined` + * `implicitTiling.subtrees.uri` template URI. + * + * @returns The subtree URI, or `undefined` */ getSubtreeUri(): string | undefined; } From a05bd3712526d99b0e6b8990fee25101a1204c9e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 20 Mar 2023 19:48:54 +0100 Subject: [PATCH 09/60] Tileset content processor drafts --- demos/TilesetContentProcessorDrafts.ts | 36 +++ ...rocessor.ts => TilesetContentProcessor.ts} | 283 +++++++++++++----- 2 files changed, 242 insertions(+), 77 deletions(-) create mode 100644 demos/TilesetContentProcessorDrafts.ts rename src/tilesetProcessing/{TilesetProcessor.ts => TilesetContentProcessor.ts} (69%) diff --git a/demos/TilesetContentProcessorDrafts.ts b/demos/TilesetContentProcessorDrafts.ts new file mode 100644 index 00000000..9628a920 --- /dev/null +++ b/demos/TilesetContentProcessorDrafts.ts @@ -0,0 +1,36 @@ +import { Paths } from "../src/base/Paths"; +import { ContentOps } from "../src/contentOperations/ContentOps"; +import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; +import { TilesetContentProcessor } from "../src/tilesetProcessing/TilesetContentProcessor"; + +async function runB3dmToGlbTest() { + const tilesetSourceName = + "../3d-tiles-samples/1.0/TilesetWithDiscreteLOD/tileset.json"; + const tilesetTargetName = "./output/TilesetWithDiscreteLOD/tileset.json"; + const overwrite = true; + + const quiet = true; + const tilesetContentProcessor = new TilesetContentProcessor(quiet); + + tilesetContentProcessor.setProcessEntryCallback( + async (sourceEntry: TilesetEntry, type: string | undefined) => { + if (type !== "CONTENT_TYPE_B3DM") { + return sourceEntry; + } + const targetEntry = { + key: Paths.replaceExtension(sourceEntry.key, ".glb"), + value: ContentOps.b3dmToGlbBuffer(sourceEntry.value), + }; + console.log("Updated " + sourceEntry.key + " to " + targetEntry.key); + return targetEntry; + } + ); + + await tilesetContentProcessor.process( + tilesetSourceName, + tilesetTargetName, + overwrite + ); +} + +runB3dmToGlbTest(); diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetContentProcessor.ts similarity index 69% rename from src/tilesetProcessing/TilesetProcessor.ts rename to src/tilesetProcessing/TilesetContentProcessor.ts index 861c3983..13a03696 100644 --- a/src/tilesetProcessing/TilesetProcessor.ts +++ b/src/tilesetProcessing/TilesetContentProcessor.ts @@ -23,10 +23,64 @@ import { TilesetTraverser } from "../traversal/TilesetTraverser"; import { Tiles } from "../tilesets/Tiles"; import { Tilesets } from "../tilesets/Tilesets"; +/** + * A function that can process one entry of a tileset dataset. + * + * This will be called ONCE for each entry of the tileset source, + * and return an entry that is supposed to be put into the tileset + * target. + * + * It receives the source entry, which may represent a content + * of an (explicit) tile, a content of an implicit tile, or just + * one entry of the tileset source (i.e. a "file" that is not + * a tile content). + * + * It returns the "processed" entry that is supposed to put into + * the tileset target. If the returned entry is `undefined`, then + * this means that the entry should be omitted in the target. + * + * Otherwise, the returned entry may have a different `key` + * (file name), and/or a modified `value` (file data). This + * entry will be put into the tileset target. + * + * Note that a modification of the `key` have different implications: + * + * - For explicit tile content, changes in the `key` will automatically + * be taken into account, by updating the `content.uri` accordingly + * - For implicit tile content, changes in the `key` have to be taken + * into account by updating template URIs. + * - For files, changes in the `key` have to be taken into account by + * domain-specific knowledge about what these files actually are. + * + * @param sourceEntry - The source entry + * @param type - The type of the entry data (see `ContentDataTypes`), + * or `undefined` if the type could not be determined. + * @returns A promise that resolves when the process is finished, + * containing either the new entry, or `undefined` when the entry + * was supposed to be removed (i.e. omitted in the target). + * @throws TilesetError When the input could not be processed + * + */ +export type ProcessEntryCallback = ( + sourceEntry: TilesetEntry, + type: string | undefined +) => Promise; + +/** + * A callback that will be called ONCE for each content + * that is contained in a tile that is a root of an + * implicit tileset. + * + * Specifically, these are the contents where the `content.uri` + * is a template URI. + */ +export type ProcessImplicitTilesetRootContentCallback = ( + content: Content +) => Promise; /** */ -export class TilesetProcessor { +export class TilesetContentProcessor { /** * A function that will receive log messages */ @@ -43,10 +97,29 @@ export class TilesetProcessor { private tilesetTarget: TilesetTarget | undefined; /** - * The set of keys (file names) from the current tileset source - * that have already been processed. + * The set of keys (file names) that have already been processed. + * This includes the original keys, as well as new keys that + * have been assigned to entries in the `processEntryCallback`. + */ + private processedKeys: { [key: string]: boolean } = {}; + + /** + * The callback that will be called for each entry. + * + * See `ProcessEntryCallback` for details. + */ + private processEntryCallback: ProcessEntryCallback | undefined = + this.processEntryNoOp.bind(this); + + /** + * The callback that will be called for each content of a tile + * that is the root of an implicit tileset. + * + * See `ProcessImplicitTilesetRootContentCallback` for details. */ - private processedSourceKeys: { [key: string]: boolean } = {}; + private processImplicitTilesetRootContentCallback: + | ProcessImplicitTilesetRootContentCallback + | undefined = this.processImplicitTilesetRootContentNoOp.bind(this); /** * Creates a new instance @@ -62,6 +135,34 @@ export class TilesetProcessor { } } + /** + * Set the callback that will be called for each entry. + * + * See `ProcessEntryCallback` for details. + * + * @param callback - The callback + */ + setProcessEntryCallback(callback: ProcessEntryCallback | undefined) { + this.processEntryCallback = callback; + } + + /** + * Set the callback that will be called for each content of a tile + * that is the root of an implicit tileset. + * + * See `ProcessImplicitTilesetRootContentCallback` for details. + */ + setProcessImplicitTilesetRootContentCallback( + callback: ProcessImplicitTilesetRootContentCallback | undefined + ) { + this.processImplicitTilesetRootContentCallback = callback; + } + + /** + * Internal method to just call the log callback + * + * @param message - The message + */ private log(message: any): void { this.logCallback(message); } @@ -110,8 +211,8 @@ export class TilesetProcessor { this.tilesetSource = undefined; this.tilesetTarget = undefined; - Object.keys(this.processedSourceKeys).forEach( - (key) => delete this.processedSourceKeys[key] + Object.keys(this.processedKeys).forEach( + (key) => delete this.processedKeys[key] ); } @@ -160,7 +261,7 @@ export class TilesetProcessor { } /** - * It parses the JSON from the value with the given key (file name), + * Parses the JSON from the value with the given key (file name), * and returns the parsed result, AND information of whether the * input was zipped. * @@ -290,7 +391,7 @@ export class TilesetProcessor { * if they haven't been processed yet. * * All these operations will eventually end up in the - * `processEntryImpl` method - see this method for details. + * `processEntryCallback` - see this field for details. * * @param tileset - The tileset * @param schema - The optional metadata schema for the tileset @@ -304,22 +405,21 @@ export class TilesetProcessor { this.log(`Processing explicit tiles`); await this.processExplicitTilesContentEntries(tileset); - // TODO Something like "processImplicitTemplateUris" - // and maybe something explicit for the subtrees - // as well... - this.log(`Processing all tiles`); await this.processAllTilesContentEntries(tileset, schema); this.log(`Processing all entries`); await this.processAllEntries(); + + this.log(`Processing all implicit tileset roots`); + await this.processImplicitTilesetRoots(tileset); } /** * Process all entries that are tile content of explicit tiles. * * Each entry will eventually be processed with the - * `processEntryImpl` method - see this method for details. + * `processEntryCallback` - see this field for details. * * @param tileset - The tileset * @returns A promise that resolves when the process is finished @@ -340,7 +440,7 @@ export class TilesetProcessor { * Process all entries that are content of the given tile. * * Each entry will eventually be processed with the - * `processEntryImpl` method - see this method for details. + * `processEntryCallback` - see this field for details. * * @param tile - The tile * @returns A promise that resolves when the process is finished @@ -378,12 +478,69 @@ export class TilesetProcessor { } } + /** + * Process all tiles that are roots of implicit tilesets. + * + * @param tileset - The tileset + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processImplicitTilesetRoots(tileset: Tileset): Promise { + const root = tileset.root; + await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { + const tile = tilePath[tilePath.length - 1]; + if (tile.implicitTiling) { + await this.processImplicitTilesetRoot(tile); + } + return true; + }); + } + + /** + * Process the given tile, which is a root of an implicit tileset. + * + * @param tile - The tile + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processImplicitTilesetRoot(tile: Tile): Promise { + const callback = + this.processImplicitTilesetRootContentCallback ?? + this.processImplicitTilesetRootContentNoOp; + if (tile.content) { + const content = tile.content; + await callback(content); + } else if (tile.contents) { + for (const content of tile.contents) { + await callback(content); + } + } + } + + /** + * Process the given content, which is the content of a root of an + * implicit tileset, doing nothing. + * + * @param content - The content + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processImplicitTilesetRootContentNoOp( + content: Content + ): Promise { + console.log( + "Performing no-op on implicit tileset root content with URI " + + content.uri + ); + return content; + } + /** * Process all entries that are tile content (both of explicit * and implicit tiles). * * Each entry will eventually be processed with the - * `processEntryImpl` method - see this method for details. + * `processEntryCallback` - see this field for details. * * @param tileset - The tileset * @param schema - The optional metadata schema for the tileset @@ -398,6 +555,10 @@ export class TilesetProcessor { if (!this.tilesetSource) { throw new DeveloperError("The source must be defined"); } + + // Create the resource resolver that will be used for + // resolving ".subtree" files of implicit tilesets + // during the traversal const resourceResolver = new TilesetSourceResourceResolver( ".", this.tilesetSource @@ -408,15 +569,12 @@ export class TilesetProcessor { schema, resourceResolver, async (traversedTile) => { - // 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()) { + if (!traversedTile.isImplicitTilesetRoot()) { const contentUris = traversedTile .getFinalContents() .map((c) => c.uri); for (const contentUri of contentUris) { - this.processEntry(contentUri); + await this.processEntry(contentUri); } } return true; @@ -430,7 +588,7 @@ export class TilesetProcessor { * tileset source. * * Each entry will eventually be processed with the - * `processEntryImpl` method - see this method for details. + * `processEntryCallback` - see this field for details. * * @param tileset - The tileset * @returns A promise that resolves when the process is finished @@ -451,17 +609,17 @@ export class TilesetProcessor { /** * Process the specified entry. * - * If the entry with the specified entry was already processed, + * If the entry with the specified key was already processed, * then this method does nothing. * * Otherwise, the specified entry will be looked up in the tileset * source. Its content type will be determined. The source entry - * will be passed to `processEntryImpl`, which returns a target + * will be passed to `processEntryCallback`, which returns a target * entry. If the target entry is defined, then it is inserted * into the tileset target. * - * This is the "staging" method for `processEntryImpl` (see this - * method for further details) + * This is the "staging" method for `processEntryCallback` + * (see this field for further details) * * @param tileset - The tileset * @returns A promise that resolves when the process is finished, @@ -477,10 +635,10 @@ export class TilesetProcessor { } const sourceKey = key; - if (this.processedSourceKeys[sourceKey] === true) { + if (this.processedKeys[sourceKey] === true) { return; } - this.processedSourceKeys[sourceKey] = true; + this.processedKeys[sourceKey] = true; const sourceValue = this.tilesetSource.getValue(sourceKey); if (!sourceValue) { @@ -492,82 +650,54 @@ export class TilesetProcessor { value: sourceValue, }; const type = await this.determineContentDataType(sourceKey, sourceValue); - const targetEntry = await this.processEntryImpl(sourceEntry, type); + + this.log(`Processing source: ${sourceKey} with type ${type}`); + + const callback = this.processEntryCallback ?? this.processEntryNoOp; + const targetEntry = await callback(sourceEntry, type); + + this.log(` to target: ${targetEntry?.key}`); if (targetEntry) { this.tilesetTarget.addEntry(targetEntry.key, targetEntry.value); + this.processedKeys[targetEntry.key] = true; } return targetEntry; } /** - * TODO Consider this, for example, for "inlining" references to - * external PNGs into a GLB, and then say + * TODO Consider something like this, for example, for "inlining" + * references to external PNGs into a GLB, and then say * markAsProcessed("./images/referredToByGlb.png"); * to omit it in the output... - * + * * A method that can be called by implementations, to mark a certain - * file as already being processed, and no longer be considered in - * subsequent steps. - * + * file as already having been processed, and no longer be considered + * in subsequent steps. + * * @param key - The key (file name) */ markAsProcessed(key: string) { - this.processedSourceKeys[key] = true; + this.processedKeys[key] = true; } /** - * Process an entry. - * - * This will be called ONCE for each entry of the tileset source, - * and return an entry that is supposed to be put into the tileset - * target. - * - * It receives the source entry, which may represent a content - * of an (explicit) tile, a content of an implicit tile, or just - * one entry of the tileset source (i.e. a "file" that is not - * a tile content). - * - * It returns the "processed" entry that is supposed to put into - * the tileset target. If the returned entry is `undefined`, then - * this means that the entry should be omitted in the target. - * - * Otherwise, the returned entry may have a different `key` - * (file name), and/or a modified `value` (file data). This - * entry will be put into the tileset target. - * - * Note that a modification of the `key` have different implications: - * - * - For explicit tile content, changes in the `key` will automatically - * be taken into account, by updating the `content.uri` accordingly - * - For implicit tile content, changes in the `key` have to be taken - * into account by updating template URIs. - * - For files, changes in the `key` have to be taken into account by - * domain-specific knowledge about what these files actually are. - * - * @param sourceEntry - The source entry - * @param type - The type of the entry data (see `ContentDataTypes`), - * or `undefined` if the type could not be determined. - * @returns A promise that resolves when the process is finished, - * containing either the new entry, or `undefined` when the entry - * was supposed to be removed (i.e. omitted in the target). - * @throws TilesetError When the input could not be processed + * Process an entry, doing nothing, for the case + * that no `processEntryCallback` is defined. */ - async processEntryImpl( + private async processEntryNoOp( sourceEntry: TilesetEntry, + // eslint-disable-next-line @typescript-eslint/no-unused-vars type: string | undefined ): Promise { + this.log(`Performing no-op on ${sourceEntry.key}`); + const sourceKey = sourceEntry.key; const sourceValue = sourceEntry.value; - this.log(`Processing source: ${sourceKey} with type ${type}`); - - // TODO This is a no-op: const targetKey = sourceKey; const targetValue = sourceValue; - this.log(` target: ${targetKey}`); - const targetEntry = { key: targetKey, value: targetValue, @@ -594,4 +724,3 @@ export class TilesetProcessor { return type; } } - From c072b8985b8bfdaadf5087878026eb35aaa9e146 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 21 Mar 2023 17:08:41 +0100 Subject: [PATCH 10/60] Basic support for external tileset traversal --- .../TilesetContentProcessor.ts | 41 +++--- src/traversal/ImplicitTraversedTile.ts | 1 - src/traversal/TilesetTraverser.ts | 128 +++++++++++++++++- 3 files changed, 139 insertions(+), 31 deletions(-) diff --git a/src/tilesetProcessing/TilesetContentProcessor.ts b/src/tilesetProcessing/TilesetContentProcessor.ts index 13a03696..68709117 100644 --- a/src/tilesetProcessing/TilesetContentProcessor.ts +++ b/src/tilesetProcessing/TilesetContentProcessor.ts @@ -119,7 +119,7 @@ export class TilesetContentProcessor { */ private processImplicitTilesetRootContentCallback: | ProcessImplicitTilesetRootContentCallback - | undefined = this.processImplicitTilesetRootContentNoOp.bind(this); + | undefined = undefined; /** * Creates a new instance @@ -411,8 +411,10 @@ export class TilesetContentProcessor { this.log(`Processing all entries`); await this.processAllEntries(); - this.log(`Processing all implicit tileset roots`); - await this.processImplicitTilesetRoots(tileset); + if (this.processImplicitTilesetRootContentCallback) { + this.log(`Processing all implicit tileset roots`); + await this.processImplicitTilesetRoots(tileset); + } } /** @@ -501,12 +503,17 @@ export class TilesetContentProcessor { * * @param tile - The tile * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `processImplicitTilesetRootContentCallback` + * is not defined * @throws TilesetError When the input could not be processed */ private async processImplicitTilesetRoot(tile: Tile): Promise { - const callback = - this.processImplicitTilesetRootContentCallback ?? - this.processImplicitTilesetRootContentNoOp; + const callback = this.processImplicitTilesetRootContentCallback; + if (!callback) { + throw new DeveloperError( + "No callback for implicit tileset root contents" + ); + } if (tile.content) { const content = tile.content; await callback(content); @@ -517,24 +524,6 @@ export class TilesetContentProcessor { } } - /** - * Process the given content, which is the content of a root of an - * implicit tileset, doing nothing. - * - * @param content - The content - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processImplicitTilesetRootContentNoOp( - content: Content - ): Promise { - console.log( - "Performing no-op on implicit tileset root content with URI " + - content.uri - ); - return content; - } - /** * Process all entries that are tile content (both of explicit * and implicit tiles). @@ -564,6 +553,7 @@ export class TilesetContentProcessor { this.tilesetSource ); const depthFirst = false; + const traverseExternalTilesets = false; await TilesetTraverser.traverse( tileset, schema, @@ -579,7 +569,8 @@ export class TilesetContentProcessor { } return true; }, - depthFirst + depthFirst, + traverseExternalTilesets ); } diff --git a/src/traversal/ImplicitTraversedTile.ts b/src/traversal/ImplicitTraversedTile.ts index c0e78bb2..2cf93afb 100644 --- a/src/traversal/ImplicitTraversedTile.ts +++ b/src/traversal/ImplicitTraversedTile.ts @@ -8,7 +8,6 @@ import { SubtreeModels } from "./SubtreeModels"; import { Tile } from "../structure/Tile"; import { Content } from "../structure/Content"; -import { MetadataEntity } from "../structure/MetadataEntity"; import { TileImplicitTiling } from "../structure/TileImplicitTiling"; import { TreeCoordinates } from "../implicitTiling/TreeCoordinates"; diff --git a/src/traversal/TilesetTraverser.ts b/src/traversal/TilesetTraverser.ts index fb3ac562..a38b3878 100644 --- a/src/traversal/TilesetTraverser.ts +++ b/src/traversal/TilesetTraverser.ts @@ -6,7 +6,12 @@ import { Schema } from "../structure/Metadata/Schema"; import { TraversedTile } from "./TraversedTile"; import { ExplicitTraversedTile } from "./ExplicitTraversedTile"; import { TraversalCallback } from "./TraversalCallback"; + import { DeveloperError } from "../base/DeveloperError"; +import { DataError } from "../base/DataError"; + +import { LazyContentData } from "../contentTypes/LazyContentData"; +import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; /** * A class that can traverse the tiles of a tileset. @@ -26,9 +31,12 @@ export class TilesetTraverser { * `tileset.schemaUri`. If this is defined, then it is assumed * to be a valid schema definition. * @param resourceResolver - The `ResourceResolver` that is used to - * resolve resources for implicit tilesets (subtree files) + * resolve resources for implicit tilesets (subtree files) or + * external tilesets. * @param traversalCallback - The `TraversalCallback` * @param depthFirst - Whether the traversal should be depth-first + * @param traverseExternalTilesets - Whether external tileset should be + * traversed * @returns A Promise that resolves when the traversal finished */ static async traverse( @@ -36,7 +44,8 @@ export class TilesetTraverser { schema: Schema | undefined, resourceResolver: ResourceResolver, traversalCallback: TraversalCallback, - depthFirst: boolean + depthFirst: boolean, + traverseExternalTilesets: boolean ): Promise { const root = tileset.root; if (!root) { @@ -65,11 +74,120 @@ export class TilesetTraverser { if (traverseChildren) { const children = await traversedTile.getChildren(); const length = children.length; - for (let i = 0; i < length; i++) { - const traversedChild = children[i]; - stack.push(traversedChild); + + if (length !== 0) { + // When there are children, traverse them directly + for (let i = 0; i < length; i++) { + const traversedChild = children[i]; + stack.push(traversedChild); + } + } else if (traverseExternalTilesets) { + // When there are no children, but external tilesets should + // be traversed, determine the roots of external tilesets + // and put them on the traversal stack + const externalRoots = + await TilesetTraverser.createExternalTilesetRoots( + traversedTile, + resourceResolver + ); + for (let i = 0; i < externalRoots.length; i++) { + const externalRoot = externalRoots[i]; + stack.push(externalRoot); + } } } } } + + /** + * + * @param traversedTile - The `TraversedTile` + * @param resourceResolver The `ResourceResolver` for the + * external tileset JSON and related files + * @returns The external tileset roots + * @throws DataError If one of the externa tilesets or + * its associated files could not be resolved. + */ + private static async createExternalTilesetRoots( + traversedTile: TraversedTile, + resourceResolver: ResourceResolver + ) { + if (traversedTile.isImplicitTilesetRoot()) { + return []; + } + const contents = traversedTile.getRawContents(); + if (contents.length === 0) { + return []; + } + const externalRoots: TraversedTile[] = []; + for (const content of contents) { + // Check if the the content is an external tileset + const contentUri = content.uri; + const contentData = new LazyContentData(contentUri, resourceResolver); + const contentDataType = await ContentDataTypeRegistry.findContentDataType( + contentData + ); + const isTileset = contentDataType === "CONTENT_TYPE_TILESET"; + + if (isTileset) { + // If an external tileset was found, parse it, derive + // a resource resolver for its base directory, obtain + // its metadata schema, and create an explicit traversed + // tile for its root. + const externalTileset = await contentData.getParsedObject(); + const derivedResourceResolver = resourceResolver.derive(contentUri); + const externalSchema = await TilesetTraverser.resolveSchema( + externalTileset, + derivedResourceResolver + ); + const externalRoot = new ExplicitTraversedTile( + externalTileset.root, + traversedTile.path + `/[external:${contentUri}]/root`, + traversedTile.level + 1, + traversedTile, + externalSchema, + derivedResourceResolver + ); + externalRoots.push(externalRoot); + } + } + return externalRoots; + } + + /** + * Resolve the `Schema` for the given tileset. + * + * This is either the `tileset.schema`, or the schema that is + * obtained from the `tileset.schemaUri`, or `undefined` if + * neither of them are present. + * + * @param tileset - The tileset + * @param resourceResolver - The `ResourceResolver` for loading + * the schema from the `schemaUri` if necessary + * @returns The `Schema`, or `undefined` if there is none + * @throws DataError If the schema from the `schemaUri` + * could not be resolved or parsed. + */ + private static async resolveSchema( + tileset: Tileset, + resourceResolver: ResourceResolver + ): Promise { + if (tileset.schema) { + return tileset.schema; + } + if (tileset.schemaUri) { + const uri = tileset.schemaUri; + const schemaData = await resourceResolver.resolveData(uri); + if (!schemaData) { + throw new DataError(`Could not resolve ${uri}`); + } + try { + const schema = JSON.parse(schemaData.toString("utf-8")); + return schema; + } catch (e) { + throw new DataError(`Could not parse schema from ${uri}`); + } + } + return undefined; + } } From f9a0f4d26aef101f68fdcab119d5564112d5858b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 21 Mar 2023 17:15:05 +0100 Subject: [PATCH 11/60] Update and extend traversal demo. --- demos/TraversalDemo.ts | 25 ++++- specs/data/TilesetWithUris/ll.b3dm | Bin 0 -> 14833 bytes specs/data/TilesetWithUris/lr.b3dm | Bin 0 -> 14833 bytes specs/data/TilesetWithUris/parent.b3dm | Bin 0 -> 14809 bytes specs/data/TilesetWithUris/tileset.json | 118 ++++++++++++++++++++++++ specs/data/TilesetWithUris/ul.b3dm | Bin 0 -> 14813 bytes specs/data/TilesetWithUris/ur.b3dm | Bin 0 -> 14821 bytes 7 files changed, 138 insertions(+), 5 deletions(-) create mode 100644 specs/data/TilesetWithUris/ll.b3dm create mode 100644 specs/data/TilesetWithUris/lr.b3dm create mode 100644 specs/data/TilesetWithUris/parent.b3dm create mode 100644 specs/data/TilesetWithUris/tileset.json create mode 100644 specs/data/TilesetWithUris/ul.b3dm create mode 100644 specs/data/TilesetWithUris/ur.b3dm diff --git a/demos/TraversalDemo.ts b/demos/TraversalDemo.ts index d64624bd..0e37e171 100644 --- a/demos/TraversalDemo.ts +++ b/demos/TraversalDemo.ts @@ -6,6 +6,9 @@ import { ResourceResolvers } from "../src/io/ResourceResolvers"; import { TilesetTraverser } from "../src/traversal/TilesetTraverser"; async function tilesetTraversalDemo(filePath: string) { + + console.log(`Traversing tileset ${filePath}`); + const directory = path.dirname(filePath); const resourceResolver = ResourceResolvers.createFileResourceResolver(directory); @@ -13,7 +16,7 @@ async function tilesetTraversalDemo(filePath: string) { // Note: External schemas are not considered here const schema = tileset.schema; const depthFirst = false; - console.log("Traversing tileset"); + const traverseExternalTilesets = true; await TilesetTraverser.traverse( tileset, schema, @@ -29,15 +32,27 @@ async function tilesetTraversalDemo(filePath: string) { ); return true; }, - depthFirst + depthFirst, + traverseExternalTilesets ); console.log("Traversing tileset DONE"); } -async function runDemo() { +async function runBasicDemo() { + const tilesetFileName = + "./specs/data/TilesetWithUris/tileset.json"; + await tilesetTraversalDemo(tilesetFileName); +} + +async function runExternalDemo() { const tilesetFileName = - "../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json"; + "./specs/data/TilesetOfTilesetsWithUris/tileset.json"; await tilesetTraversalDemo(tilesetFileName); } -runDemo(); +async function runDemo() { + await runBasicDemo(); + await runExternalDemo(); +} + +runDemo(); \ No newline at end of file diff --git a/specs/data/TilesetWithUris/ll.b3dm b/specs/data/TilesetWithUris/ll.b3dm new file mode 100644 index 0000000000000000000000000000000000000000..dbd0d2142ca9d7a3525e00fe00a79718658c3c67 GIT binary patch literal 14833 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 literal 0 HcmV?d00001 diff --git a/specs/data/TilesetWithUris/lr.b3dm b/specs/data/TilesetWithUris/lr.b3dm new file mode 100644 index 0000000000000000000000000000000000000000..f052e259f0789b35b85323ae4d2567f7323efe0a GIT binary patch literal 14833 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 LkTIOu(sBO_hJBp2 literal 0 HcmV?d00001 diff --git a/specs/data/TilesetWithUris/tileset.json b/specs/data/TilesetWithUris/tileset.json new file mode 100644 index 00000000..ef60cd7b --- /dev/null +++ b/specs/data/TilesetWithUris/tileset.json @@ -0,0 +1,118 @@ +{ + "asset": { + "version": "1.0", + "tilesetVersion": "1.2.3" + }, + "properties": { + "id": { + "minimum": 0, + "maximum": 9 + }, + "Longitude": { + "minimum": -1.3197192952275933, + "maximum": -1.319644104024109 + }, + "Latitude": { + "minimum": 0.698848878034009, + "maximum": 0.6989046192460953 + }, + "Height": { + "minimum": 6.161747192963958, + "maximum": 84.83180232718587 + } + }, + "geometricError": 240, + "root": { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.3196390408203893, + 0.6989055782, + 0, + 88 + ] + }, + "geometricError": 70, + "refine": "ADD", + "content": { + "uri": "parent.b3dm", + "boundingVolume": { + "region": [ + -1.3197004795898053, + 0.6988582109, + -1.3196595204101946, + 0.6988897891, + 0, + 88 + ] + } + }, + "children": [ + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.6988424218, + -1.31968, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "ll.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.6988424218, + -1.3196390408203893, + 0.698874, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "lr.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.31968, + 0.698874, + -1.3196390408203893, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "ur.b3dm" + } + }, + { + "boundingVolume": { + "region": [ + -1.3197209591796106, + 0.698874, + -1.31968, + 0.6989055782, + 0, + 20 + ] + }, + "geometricError": 0, + "content": { + "uri": "ul.b3dm" + } + } + ] + } +} diff --git a/specs/data/TilesetWithUris/ul.b3dm b/specs/data/TilesetWithUris/ul.b3dm new file mode 100644 index 0000000000000000000000000000000000000000..a386047150bb0dfa5c6ebec88aef3dd4b73e3f77 GIT binary patch literal 14813 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 S=gWO=@4sqR8i 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 P9Q5${dl|!(EiCvSSa?#e literal 0 HcmV?d00001 From 2f8cfff8ca574165b67b8897a57d6a702ab57b83 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 21 Mar 2023 17:39:37 +0100 Subject: [PATCH 12/60] Revived demos from validator Demos for the parts of the code that have been moved from the validator to the tools --- demos/BinaryMetadataDemos.ts | 207 ++++++++++++++++++ demos/ImplicitTilingDemos.ts | 74 +++++++ demos/MetadataDemos.ts | 156 +++++++++++++ demos/SubtreeInfoDemos.ts | 58 +++++ demos/TraversalDemo.ts | 9 +- specs/data/subtrees/validBuffer.bin | Bin 0 -> 16 bytes specs/data/subtrees/validSubtree.json | 10 + .../validSubtreeImplicitTiling.json.input | 8 + 8 files changed, 516 insertions(+), 6 deletions(-) create mode 100644 demos/BinaryMetadataDemos.ts create mode 100644 demos/ImplicitTilingDemos.ts create mode 100644 demos/MetadataDemos.ts create mode 100644 demos/SubtreeInfoDemos.ts create mode 100644 specs/data/subtrees/validBuffer.bin create mode 100644 specs/data/subtrees/validSubtree.json create mode 100644 specs/data/subtrees/validSubtreeImplicitTiling.json.input diff --git a/demos/BinaryMetadataDemos.ts b/demos/BinaryMetadataDemos.ts new file mode 100644 index 00000000..adac650e --- /dev/null +++ b/demos/BinaryMetadataDemos.ts @@ -0,0 +1,207 @@ +import { ClassProperty } from "../src/structure/Metadata/ClassProperty"; +import { BinaryPropertyTables } from "../src/metadata/binary/BinaryPropertyTables"; +import { BinaryPropertyTableModel } from "../src/metadata/binary/BinaryPropertyTableModel"; + +/** + * A test for the `BinaryPropertyTableModel` class. + * + * It creates a (binary) property table that contains a single + * property with the given structure and the given values. + * From that, it creates a `BinaryPropertyTable`, and goes + * through all its rows (as `MetadataEntityModel` instances), + * and prints the values that this entity model has for + * the given property. + * + * (These should be the same as the given input property values) + * + * @param name - A name for log messages + * @param classProperty - The `ClassProperty` + * @param propertyValues - The property values + */ +function runPropertyTableModelTest( + name: string, + classProperty: ClassProperty, + propertyValues: any +) { + const count = propertyValues.length; + const arrayOffsetType = "UINT32"; + const stringOffsetType = "UINT32"; + const binaryPropertyTable = + BinaryPropertyTables.createBinaryPropertyTableFromProperty( + "testProperty", + classProperty, + propertyValues, + arrayOffsetType, + stringOffsetType, + undefined + ); + const propertyTableModel = new BinaryPropertyTableModel(binaryPropertyTable); + console.log("For " + name); + console.log(" Original values: " + JSON.stringify(propertyValues)); + for (let i = 0; i < count; i++) { + const entity0 = propertyTableModel.getMetadataEntityModel(i); + const value0 = entity0.getPropertyValue("testProperty"); + console.log(` Value from MetadataEntity ${i}: ` + JSON.stringify(value0)); + } +} + +/** + * Calls `runPropertyTableModelTest` for various class property types + */ +function runPropertyTableModelTests() { + const example_variable_length_FLOAT32_SCALAR_array = { + type: "SCALAR", + componentType: "FLOAT32", + array: true, + }; + const example_fixed_length_FLOAT32_SCALAR_array = { + type: "SCALAR", + componentType: "FLOAT32", + array: true, + count: 5, + }; + const example_STRING = { + type: "STRING", + }; + const example_variable_length_STRING_array = { + type: "STRING", + array: true, + }; + const example_fixed_length_STRING_array = { + type: "STRING", + array: true, + count: 5, + }; + const example_BOOLEAN = { + type: "BOOLEAN", + }; + const example_variable_length_BOOLEAN_array = { + type: "BOOLEAN", + array: true, + }; + const example_fixed_length_BOOLEAN_array = { + type: "BOOLEAN", + array: true, + count: 5, + }; + + const example_variable_length_UINT32_VEC2_array = { + type: "VEC2", + componentType: "UINT32", + array: true, + }; + const example_fixed_length_UINT32_VEC2_array = { + type: "VEC2", + componentType: "UINT32", + array: true, + count: 5, + }; + + const example_variable_length_FLOAT32_SCALAR_array_values = [ + [-1.0, -0.5, 0.0, 0.5, 1.0], + [-1.0, 0.0, 1.0], + ]; + const example_fixed_length_FLOAT32_SCALAR_array_values = [ + [-1.0, -0.5, 0.0, 0.5, 1.0], + [1.0, 2.0, 3.0, 4.0, 5.0], + ]; + const example_STRING_values = ["This is a test", "This is another test"]; + const example_variable_length_STRING_array_values = [ + ["This", "is", "a", "test"], + ["Another", "test"], + ]; + const example_fixed_length_STRING_array_values = [ + ["zero", "one", "two", "three", "four"], + ["A", "B", "C", "D", "E"], + ]; + const example_BOOLEAN_values = [true, false]; + const example_variable_length_BOOLEAN_array_values = [ + [true, false, true, false], + [false, true, false], + ]; + const example_fixed_length_BOOLEAN_array_values = [ + [true, false, true, false, true], + [false, true, false, true, false], + ]; + + const example_variable_length_UINT32_VEC2_array_values = [ + [ + [0, 1], + [2, 3], + [4, 5], + ], + [ + [6, 7], + [8, 9], + ], + ]; + const example_fixed_length_UINT32_VEC2_array_values = [ + [ + [0, 1], + [2, 3], + [4, 5], + [6, 7], + [8, 9], + ], + [ + [10, 11], + [12, 13], + [14, 15], + [16, 17], + [18, 19], + ], + ]; + + runPropertyTableModelTest( + "example_fixed_length_STRING_array", + example_fixed_length_STRING_array, + example_fixed_length_STRING_array_values + ); + runPropertyTableModelTest( + "example_variable_length_BOOLEAN_array", + example_variable_length_BOOLEAN_array, + example_variable_length_BOOLEAN_array_values + ); + runPropertyTableModelTest( + "example_fixed_length_UINT32_VEC2_array", + example_fixed_length_UINT32_VEC2_array, + example_fixed_length_UINT32_VEC2_array_values + ); + runPropertyTableModelTest( + "example_variable_length_STRING_array", + example_variable_length_STRING_array, + example_variable_length_STRING_array_values + ); + runPropertyTableModelTest( + "example_fixed_length_FLOAT32_SCALAR_array", + example_fixed_length_FLOAT32_SCALAR_array, + example_fixed_length_FLOAT32_SCALAR_array_values + ); + runPropertyTableModelTest( + "example_STRING", + example_STRING, + example_STRING_values + ); + runPropertyTableModelTest( + "example_variable_length_FLOAT32_SCALAR_array", + example_variable_length_FLOAT32_SCALAR_array, + example_variable_length_FLOAT32_SCALAR_array_values + ); + runPropertyTableModelTest( + "example_BOOLEAN", + example_BOOLEAN, + example_BOOLEAN_values + ); + runPropertyTableModelTest( + "example_fixed_length_BOOLEAN_array", + example_fixed_length_BOOLEAN_array, + example_fixed_length_BOOLEAN_array_values + ); + runPropertyTableModelTest( + "example_variable_length_UINT32_VEC2_array", + example_variable_length_UINT32_VEC2_array, + example_variable_length_UINT32_VEC2_array_values + ); +} + +runPropertyTableModelTests(); diff --git a/demos/ImplicitTilingDemos.ts b/demos/ImplicitTilingDemos.ts new file mode 100644 index 00000000..816e8a6a --- /dev/null +++ b/demos/ImplicitTilingDemos.ts @@ -0,0 +1,74 @@ +import { Quadtrees } from "../src/implicitTiling/Quadtrees"; +import { TemplateUris } from "../src/implicitTiling/TemplateUris"; +import { QuadtreeCoordinates } from "../src/implicitTiling/QuadtreeCoordinates"; +import { OctreeCoordinates } from "../src/implicitTiling/OctreeCoordinates"; + +/** + * A basic demo of the `QuadtreeCoordinates.children` method + */ +function testQuadtreeChildren() { + const r = new QuadtreeCoordinates(0, 0, 0); + console.log("Children of " + r + ":"); + for (const c of r.children()) { + console.log(" " + c + " index " + c.toIndex() + " parent " + c.parent()); + } +} + +/** + * A basic demo of the `QuadtreeCoordinates.descendants` method + */ +function testQuadtreeDescendants() { + const r = new QuadtreeCoordinates(0, 0, 0); + const maxLevelInclusive = 3; + const depthFirst = true; + console.log("Descendants of " + r + " up to " + maxLevelInclusive + ":"); + for (const c of r.descendants(maxLevelInclusive, depthFirst)) { + console.log(" " + c + " index " + c.toIndex() + " parent " + c.parent()); + } +} + +/** + * A basic demo of the `Quadtrees.coordinatesForLevel` method + */ +function testQuadtreeLevel() { + const level = 3; + const coords = Quadtrees.coordinatesForLevel(3); + console.log("Coordinates in level " + level + ":"); + for (const c of coords) { + console.log(" " + c); + } +} + +/** + * A basic demo for the `TemplateUris.substituteQuadtree` method + */ +function testSubstituteQuadtree() { + const uri = "test-{level}-{x}-{y}"; + const c = new QuadtreeCoordinates(3, 4, 5); + const s = TemplateUris.substituteQuadtree(uri, c); + console.log("uri : " + uri); + console.log("coordinates: " + c); + console.log("result : " + s); +} + +/** + * A basic demo for the `TemplateUris.substituteOctree` method + */ +function testSubstituteOctree() { + const uri = "test-{level}-{x}-{y}-{z}"; + const c = new OctreeCoordinates(3, 4, 5, 6); + const s = TemplateUris.substituteOctree(uri, c); + console.log("uri : " + uri); + console.log("coordinates: " + c); + console.log("result : " + s); +} + +async function runDemos() { + testQuadtreeChildren(); + testQuadtreeDescendants(); + testQuadtreeLevel(); + testSubstituteQuadtree(); + testSubstituteOctree(); +} + +runDemos(); diff --git a/demos/MetadataDemos.ts b/demos/MetadataDemos.ts new file mode 100644 index 00000000..0e730157 --- /dev/null +++ b/demos/MetadataDemos.ts @@ -0,0 +1,156 @@ +import { defaultValue } from "../src/base/defaultValue"; +import { readJsonUnchecked } from "./readJsonUnchecked"; + +import { MetadataClass } from "../src/structure/Metadata/MetadataClass"; +import { Tileset } from "../src/structure/Tileset"; + +import { MetadataEntityModels } from "../src/metadata/MetadataEntityModels"; + +/** + * Test the `MetadataEntityModels` class for the type + * "exampleScalarInt32" + */ +function testExampleScalarInt32() { + console.log("exampleScalarInt32:"); + const metadataClass: MetadataClass = { + properties: { + testProperty: { + type: "SCALAR", + componentType: "INT32", + }, + }, + }; + const entityJson = { + testProperty: 1234, + }; + const entity = MetadataEntityModels.createFromClass( + metadataClass, + entityJson + ); + + const value = entity.getPropertyValue("testProperty"); + console.log(" Property value: " + value); +} + +/** + * Test the `MetadataEntityModels` class for the type + * "exampleArrayInt16WithDefault" + */ +function testExampleArrayInt16WithDefault() { + console.log("exampleArrayInt16WithDefault:"); + const metadataClass: MetadataClass = { + properties: { + testProperty: { + array: true, + type: "SCALAR", + componentType: "INT16", + required: false, + noData: [], + default: [1, 1, 1], + }, + }, + }; + const entityJson = { + testProperty: undefined, + }; + const entity = MetadataEntityModels.createFromClass( + metadataClass, + entityJson + ); + const value = entity.getPropertyValue("testProperty"); + console.log(" Property value: " + value); +} + +/** + * Test the `MetadataEntityModels` class for the type + * "exampleVec3Uint16Normalized" + */ +function testExampleVec3Uint16Normalized() { + console.log("exampleVec3Uint16Normalized:"); + const metadataClass: MetadataClass = { + properties: { + testProperty: { + type: "VEC3", + componentType: "UINT16", + normalized: true, + }, + }, + }; + const entityJson = { + testProperty: [0, 32767, 65535], + }; + const entity = MetadataEntityModels.createFromClass( + metadataClass, + entityJson + ); + const value = entity.getPropertyValue("testProperty"); + console.log(" Property value: " + value); +} + +/** + * Creates a string for a (metadata property) value. + * The exact format is not specified. This is only + * intended for the demo. But the string will be + * exhaustive. + * + * @param value - The value + * @returns The string + */ +function createValueString(value: any) { + let result = ""; + if (Array.isArray(value)) { + result += "["; + for (let i = 0; i < value.length; i++) { + if (i > 0) { + result += ", "; + } + result += createValueString(value[i]); + } + result += "]"; + return result; + } + if (typeof value === "string") { + return '"' + value + '"'; + } + return value.toString(); +} + +/** + * Prints all properties of the `TilesetWithFullMetadata` sample + * data, as they are obtained via a `MetadataEntityModel`. + */ +async function testTilesetWithFullMetadata() { + // Note: This is making some assumptions about + // the input, and only intended for the basic + // demo of the `MetadataEntityModels` class + const tileset: Tileset = await readJsonUnchecked( + "./specs/data/TilesetWithFullMetadata/tileset.json" + ); + const metadataSchema = tileset.schema; + const metadataEntity = tileset.metadata; + if (!metadataSchema || !metadataEntity) { + console.log("Test input was invalid"); + return; + } + const entity = MetadataEntityModels.create(metadataSchema, metadataEntity); + + console.log("Metadata property values:"); + const metadataClasses = defaultValue(metadataSchema.classes, {}); + const metadataClass = metadataClasses["exampleClass"]; + const properties = metadataClass.properties ?? {}; + for (const propertyName of Object.keys(properties)) { + const nameString = propertyName.padStart(60); + const value = entity.getPropertyValue(propertyName); + const valueString = createValueString(value); + console.log(` Property value of ${nameString}: ${valueString}`); + } +} + +function runDemos() { + testExampleScalarInt32(); + testExampleArrayInt16WithDefault(); + testExampleVec3Uint16Normalized(); + testTilesetWithFullMetadata(); +} + +runDemos(); diff --git a/demos/SubtreeInfoDemos.ts b/demos/SubtreeInfoDemos.ts new file mode 100644 index 00000000..f83b6d0e --- /dev/null +++ b/demos/SubtreeInfoDemos.ts @@ -0,0 +1,58 @@ +import path from "path"; + +import { readJsonUnchecked } from "./readJsonUnchecked"; + +import { ResourceResolvers } from "../src/io/ResourceResolvers"; + +import { QuadtreeCoordinates } from "../src/implicitTiling/QuadtreeCoordinates"; +import { SubtreeInfos } from "../src/implicitTiling/SubtreeInfos"; + +async function testSubtreeInfo() { + // Create a `SubtreeInfo` for a valid subtree, from + // the specs data directory + const subtreeFilePath = "./specs/data/subtrees/validSubtree.json"; + const implcitTilingFilePath = + "specs/data/subtrees/validSubtreeImplicitTiling.json.input"; + const implicitTiling = await readJsonUnchecked(implcitTilingFilePath); + const subtree = await readJsonUnchecked(subtreeFilePath); + const directory = path.dirname(subtreeFilePath); + const resourceResolver = + ResourceResolvers.createFileResourceResolver(directory); + const subtreeInfo = await SubtreeInfos.createFromJson( + subtree, + implicitTiling, + resourceResolver + ); + if (!subtreeInfo) { + console.log("Could not resolve subtree data"); + return; + } + + // Print the tile availability information, accessing it by index + console.log("Tile availability from indices:"); + const tileAvailabilityInfo = subtreeInfo.tileAvailabilityInfo; + for (let i = 0; i < tileAvailabilityInfo.length; i++) { + const available = tileAvailabilityInfo.isAvailable(i); + console.log(" at index " + i + " available :" + available); + } + + // Print the tile availability information, accessing it with + // the index that is computed from QuadtreeCoordinates + console.log("Tile availability from coordinates:"); + const r = new QuadtreeCoordinates(0, 0, 0); + const depthFirst = false; + const maxLevelInclusive = implicitTiling.subtreeLevels - 1; + for (const c of r.descendants(maxLevelInclusive, depthFirst)) { + const index = c.toIndex(); + const available = tileAvailabilityInfo.isAvailable(index); + console.log( + " " + c + " index " + c.toIndex() + " available: " + available + ); + } +} + +async function runDemos() { + await testSubtreeInfo(); +} + +runDemos(); diff --git a/demos/TraversalDemo.ts b/demos/TraversalDemo.ts index 0e37e171..c3b70dc5 100644 --- a/demos/TraversalDemo.ts +++ b/demos/TraversalDemo.ts @@ -6,7 +6,6 @@ import { ResourceResolvers } from "../src/io/ResourceResolvers"; import { TilesetTraverser } from "../src/traversal/TilesetTraverser"; async function tilesetTraversalDemo(filePath: string) { - console.log(`Traversing tileset ${filePath}`); const directory = path.dirname(filePath); @@ -39,14 +38,12 @@ async function tilesetTraversalDemo(filePath: string) { } async function runBasicDemo() { - const tilesetFileName = - "./specs/data/TilesetWithUris/tileset.json"; + const tilesetFileName = "./specs/data/TilesetWithUris/tileset.json"; await tilesetTraversalDemo(tilesetFileName); } async function runExternalDemo() { - const tilesetFileName = - "./specs/data/TilesetOfTilesetsWithUris/tileset.json"; + const tilesetFileName = "./specs/data/TilesetOfTilesetsWithUris/tileset.json"; await tilesetTraversalDemo(tilesetFileName); } @@ -55,4 +52,4 @@ async function runDemo() { await runExternalDemo(); } -runDemo(); \ No newline at end of file +runDemo(); diff --git a/specs/data/subtrees/validBuffer.bin b/specs/data/subtrees/validBuffer.bin new file mode 100644 index 0000000000000000000000000000000000000000..694af87617666264ebc93f540f5559c28085a6e2 GIT binary patch literal 16 Scmd-zVq|~-wgk2W1_l5J7y%6c literal 0 HcmV?d00001 diff --git a/specs/data/subtrees/validSubtree.json b/specs/data/subtrees/validSubtree.json new file mode 100644 index 00000000..eba990ae --- /dev/null +++ b/specs/data/subtrees/validSubtree.json @@ -0,0 +1,10 @@ +{ + "buffers": [{ "uri": "validBuffer.bin", "byteLength": 16 }], + "bufferViews": [ + { "buffer": 0, "byteOffset": 0, "byteLength": 3 }, + { "buffer": 0, "byteOffset": 8, "byteLength": 8 } + ], + "tileAvailability": { "bitstream": 0, "availableCount": 7 }, + "contentAvailability": [{ "availableCount": 0, "constant": 0 }], + "childSubtreeAvailability": { "bitstream": 1, "availableCount": 8 } +} diff --git a/specs/data/subtrees/validSubtreeImplicitTiling.json.input b/specs/data/subtrees/validSubtreeImplicitTiling.json.input new file mode 100644 index 00000000..61869d4d --- /dev/null +++ b/specs/data/subtrees/validSubtreeImplicitTiling.json.input @@ -0,0 +1,8 @@ +{ + "subdivisionScheme": "QUADTREE", + "subtreeLevels": 3, + "availableLevels": 6, + "subtrees": { + "uri": "subtrees/{level}.{x}.{y}.subtree" + } +} From c5e16fbf67f56d91cbdab65d81a53b0c048553fe Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 21 Mar 2023 21:36:48 +0100 Subject: [PATCH 13/60] Added missing parameter in demo --- demos/TraversalStatsDemo.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/demos/TraversalStatsDemo.ts b/demos/TraversalStatsDemo.ts index 4b28a7ad..4beca888 100644 --- a/demos/TraversalStatsDemo.ts +++ b/demos/TraversalStatsDemo.ts @@ -26,6 +26,7 @@ async function tilesetTraversalDemo(filePath: string) { console.log("Traversing tileset"); const tilesetStatsCollector = new TilesetStatsCollector(); const depthFirst = false; + const traverseExternalTilesets = false; await TilesetTraverser.traverse( tileset, schema, @@ -34,7 +35,8 @@ async function tilesetTraversalDemo(filePath: string) { tilesetStatsCollector.accept(traversedTile); return true; }, - depthFirst + depthFirst, + traverseExternalTilesets ); console.log("Traversing tileset DONE"); From 98abd904ecfde14ed047eb4d0e622d97f026b1e1 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 22 Mar 2023 18:49:59 +0100 Subject: [PATCH 14/60] Update for refactoring in base branch --- demos/TilesetContentProcessorDrafts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/TilesetContentProcessorDrafts.ts b/demos/TilesetContentProcessorDrafts.ts index 9628a920..e145a4e1 100644 --- a/demos/TilesetContentProcessorDrafts.ts +++ b/demos/TilesetContentProcessorDrafts.ts @@ -1,5 +1,5 @@ import { Paths } from "../src/base/Paths"; -import { ContentOps } from "../src/contentOperations/ContentOps"; +import { ContentOps } from "../src/contentProcessing/ContentOps"; import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; import { TilesetContentProcessor } from "../src/tilesetProcessing/TilesetContentProcessor"; From d54bca773d05232c3e91f15af3ad932105e84a71 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 22 Mar 2023 19:09:15 +0100 Subject: [PATCH 15/60] Minor cleanup in TilesetTraverser --- src/traversal/TilesetTraverser.ts | 57 ++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/src/traversal/TilesetTraverser.ts b/src/traversal/TilesetTraverser.ts index a38b3878..fab35a10 100644 --- a/src/traversal/TilesetTraverser.ts +++ b/src/traversal/TilesetTraverser.ts @@ -90,16 +90,18 @@ export class TilesetTraverser { traversedTile, resourceResolver ); - for (let i = 0; i < externalRoots.length; i++) { - const externalRoot = externalRoots[i]; - stack.push(externalRoot); - } + stack.push(...externalRoots); } } } } /** + * Create the nodes that are the roots of external tilesets + * that are referred to by the given traversed tile. + * + * If the given tile does not have any contents or none of + * them refers to a tileset, then an empty array is returned. * * @param traversedTile - The `TraversedTile` * @param resourceResolver The `ResourceResolver` for the @@ -111,7 +113,7 @@ export class TilesetTraverser { private static async createExternalTilesetRoots( traversedTile: TraversedTile, resourceResolver: ResourceResolver - ) { + ): Promise { if (traversedTile.isImplicitTilesetRoot()) { return []; } @@ -121,20 +123,18 @@ export class TilesetTraverser { } const externalRoots: TraversedTile[] = []; for (const content of contents) { - // Check if the the content is an external tileset const contentUri = content.uri; - const contentData = new LazyContentData(contentUri, resourceResolver); - const contentDataType = await ContentDataTypeRegistry.findContentDataType( - contentData + + // Try to obtain an external tileset from the content + const externalTileset = await TilesetTraverser.resolveExternalTileset( + contentUri, + resourceResolver ); - const isTileset = contentDataType === "CONTENT_TYPE_TILESET"; - if (isTileset) { - // If an external tileset was found, parse it, derive - // a resource resolver for its base directory, obtain - // its metadata schema, and create an explicit traversed - // tile for its root. - const externalTileset = await contentData.getParsedObject(); + if (externalTileset) { + // If an external tileset was found, derive a resource resolver + // for its base directory, obtain its metadata schema, and + // create an explicit traversed tile for its root. const derivedResourceResolver = resourceResolver.derive(contentUri); const externalSchema = await TilesetTraverser.resolveSchema( externalTileset, @@ -154,6 +154,31 @@ export class TilesetTraverser { return externalRoots; } + /** + * Fetch the external tileset from the given URI. If the given + * URI does not refer to an external tileset, then `undefined` + * is returned. + * + * @param uri - The URI + * @param resourceResolver - The `ResourceResolver` + * @returns The tileset + */ + private static async resolveExternalTileset( + uri: string, + resourceResolver: ResourceResolver + ): Promise { + const contentData = new LazyContentData(uri, resourceResolver); + const contentDataType = await ContentDataTypeRegistry.findContentDataType( + contentData + ); + const isTileset = contentDataType === "CONTENT_TYPE_TILESET"; + if (isTileset) { + const externalTileset = await contentData.getParsedObject(); + return externalTileset; + } + return undefined; + } + /** * Resolve the `Schema` for the given tileset. * From 4a06861012bf577de22047f7749e69ea51555829 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 22 Mar 2023 19:12:18 +0100 Subject: [PATCH 16/60] Fix typo in comment --- src/traversal/TraversedTile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/traversal/TraversedTile.ts b/src/traversal/TraversedTile.ts index daee0144..9511bf2b 100644 --- a/src/traversal/TraversedTile.ts +++ b/src/traversal/TraversedTile.ts @@ -39,7 +39,7 @@ export interface TraversedTile { /** * Returns a `Tile` object that contains the "JSON"-representation * of the tile. This is just a plain data structure corresponding - * the tile. + * to the tile. * * In contrast to `asRawTile`, this method returns a `Tile` object * where semantic-based overrides have already been applied. When From fad56db47034d91665d09c170367eca33af23269 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 22 Mar 2023 19:20:16 +0100 Subject: [PATCH 17/60] Removed unnecessary type assertions --- src/traversal/MetadataSemanticOverrides.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/traversal/MetadataSemanticOverrides.ts b/src/traversal/MetadataSemanticOverrides.ts index fde547fb..b64e75cd 100644 --- a/src/traversal/MetadataSemanticOverrides.ts +++ b/src/traversal/MetadataSemanticOverrides.ts @@ -21,7 +21,7 @@ export class MetadataSemanticOverrides { // TODO There are far too few error checks (e.g. for invalid // indices) here. This COULD be delegated to the assumption // that the input is "valid" (as determined by the validator), - // but the error handling here should still be improvev. + // but the error handling here should still be improved. /** * Perform the overrides of the properties of the given tile that @@ -195,7 +195,7 @@ export class MetadataSemanticOverrides { const semanticGeometricError = metadataEntityModel.getPropertyValueBySemantic("TILE_GEOMETRIC_ERROR"); if (defined(semanticGeometricError)) { - tile.geometricError = semanticGeometricError as number; + tile.geometricError = semanticGeometricError; } const semanticRefine = @@ -255,13 +255,13 @@ export class MetadataSemanticOverrides { const semanticUri = metadataEntityModel.getPropertyValueBySemantic("CONTENT_URI"); if (defined(semanticUri)) { - content.uri = semanticUri as string; + content.uri = semanticUri; } const semanticGroupId = metadataEntityModel.getPropertyValueBySemantic("CONTENT_GROUP_ID"); if (defined(semanticGroupId)) { - content.group = semanticGroupId as number; + content.group = semanticGroupId; } } } From f7f61626a6e188713b8665ec2d3f96fdda5f7f8a Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 23 Mar 2023 14:11:50 +0100 Subject: [PATCH 18/60] Add GIF content data type --- specs/ContentDataTypesSpec.ts | 7 +++++++ specs/data/contentTypes/README.md | 1 + specs/data/contentTypes/content.gif | Bin 0 -> 807 bytes src/contentTypes/ContentDataTypeRegistry.ts | 5 +++++ src/contentTypes/ContentDataTypes.ts | 1 + 5 files changed, 14 insertions(+) create mode 100644 specs/data/contentTypes/content.gif diff --git a/specs/ContentDataTypesSpec.ts b/specs/ContentDataTypesSpec.ts index 1f151681..e58fdc3a 100644 --- a/specs/ContentDataTypesSpec.ts +++ b/specs/ContentDataTypesSpec.ts @@ -73,6 +73,13 @@ describe("ContentDataTypeRegistry.findContentDataType", function () { expect(type).toEqual(ContentDataTypes.CONTENT_TYPE_JPEG); }); + it("detects GIF", async function () { + const contentUri = "specs/data/contentTypes/content.gif"; + const c = BufferedContentData.create(contentUri); + const type = await ContentDataTypeRegistry.findContentDataType(c); + expect(type).toEqual(ContentDataTypes.CONTENT_TYPE_GIF); + }); + it("detects GEOJSON", async function () { const contentUri = "specs/data/contentTypes/content.geojson"; const c = BufferedContentData.create(contentUri); diff --git a/specs/data/contentTypes/README.md b/specs/data/contentTypes/README.md index 2dad00a7..5bdbeaa4 100644 --- a/specs/data/contentTypes/README.md +++ b/specs/data/contentTypes/README.md @@ -7,6 +7,7 @@ Tile content files used in the specs. - The `content.subt` is the subtree file from https://github.com/CesiumGS/3d-tiles-samples/blob/902ea3dca1821a9ef9d23d141f800c68627c452b/1.1/SparseImplicitQuadtree/subtrees/0.0.0.subtree - The `content.png` is a 1x1 PNG image - The `content.jpg` is a 1x1 JPEG image +- The `content.gif` is a 1x1 GIF image The other files are taken from https://github.com/CesiumGS/cesium/tree/c0ec95713b6cde5a91eea320795c84408159dcad/Apps/SampleData/Cesium3DTiles diff --git a/specs/data/contentTypes/content.gif b/specs/data/contentTypes/content.gif new file mode 100644 index 0000000000000000000000000000000000000000..c71e11fb16f6947a12fe82fb09265dc751eed30d GIT binary patch literal 807 zcmZ?wbhEHbWMp7u_|Cxa|Nno6Q7{?;BQ*pRf3kqRt^*=Ld4ipTg@KWUg~1vCp_&Im literal 0 HcmV?d00001 diff --git a/src/contentTypes/ContentDataTypeRegistry.ts b/src/contentTypes/ContentDataTypeRegistry.ts index f7a8d9c7..67e24f54 100644 --- a/src/contentTypes/ContentDataTypeRegistry.ts +++ b/src/contentTypes/ContentDataTypeRegistry.ts @@ -96,6 +96,11 @@ export class ContentDataTypeRegistry { ContentDataTypes.CONTENT_TYPE_JPEG ); + ContentDataTypeRegistry.register( + ContentDataTypeRegistry.byMagicString("GIF8"), + ContentDataTypes.CONTENT_TYPE_GIF + ); + ContentDataTypeRegistry.register( ContentDataTypeRegistry.byExtension(".geojson"), ContentDataTypes.CONTENT_TYPE_GEOJSON diff --git a/src/contentTypes/ContentDataTypes.ts b/src/contentTypes/ContentDataTypes.ts index 47d0ad97..95e07f65 100644 --- a/src/contentTypes/ContentDataTypes.ts +++ b/src/contentTypes/ContentDataTypes.ts @@ -18,6 +18,7 @@ export class ContentDataTypes { static readonly CONTENT_TYPE_SUBT = "CONTENT_TYPE_SUBT"; static readonly CONTENT_TYPE_PNG = "CONTENT_TYPE_PNG"; static readonly CONTENT_TYPE_JPEG = "CONTENT_TYPE_JPEG"; + static readonly CONTENT_TYPE_GIF = "CONTENT_TYPE_GIF"; static readonly CONTENT_TYPE_GEOJSON = "CONTENT_TYPE_GEOJSON"; static readonly CONTENT_TYPE_3TZ = "CONTENT_TYPE_3TZ"; From 6fce88c2a5f644b91f653f18851614d30d4f42a9 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 23 Mar 2023 15:04:03 +0100 Subject: [PATCH 19/60] Draft for omitting callbacks --- demos/TilesetContentProcessorDrafts.ts | 57 ++++++- .../TilesetContentProcessor.ts | 139 ++++++------------ 2 files changed, 98 insertions(+), 98 deletions(-) diff --git a/demos/TilesetContentProcessorDrafts.ts b/demos/TilesetContentProcessorDrafts.ts index e145a4e1..dc067f37 100644 --- a/demos/TilesetContentProcessorDrafts.ts +++ b/demos/TilesetContentProcessorDrafts.ts @@ -1,5 +1,7 @@ import { Paths } from "../src/base/Paths"; import { ContentOps } from "../src/contentProcessing/ContentOps"; +import { GltfUtilities } from "../src/contentProcessing/GtlfUtilities"; +import { Content } from "../src/structure/Content"; import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; import { TilesetContentProcessor } from "../src/tilesetProcessing/TilesetContentProcessor"; @@ -9,11 +11,12 @@ async function runB3dmToGlbTest() { const tilesetTargetName = "./output/TilesetWithDiscreteLOD/tileset.json"; const overwrite = true; - const quiet = true; - const tilesetContentProcessor = new TilesetContentProcessor(quiet); - - tilesetContentProcessor.setProcessEntryCallback( - async (sourceEntry: TilesetEntry, type: string | undefined) => { + const quiet = false; + const tilesetContentProcessor = new (class extends TilesetContentProcessor { + override async processExplicitTileContentEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise { if (type !== "CONTENT_TYPE_B3DM") { return sourceEntry; } @@ -21,10 +24,47 @@ async function runB3dmToGlbTest() { key: Paths.replaceExtension(sourceEntry.key, ".glb"), value: ContentOps.b3dmToGlbBuffer(sourceEntry.value), }; - console.log("Updated " + sourceEntry.key + " to " + targetEntry.key); + console.log(" Updated " + sourceEntry.key + " to " + targetEntry.key); return targetEntry; } + })(quiet); + + await tilesetContentProcessor.process( + tilesetSourceName, + tilesetTargetName, + overwrite ); +} + + +async function runOptimizeTest() { + const tilesetSourceName = + "../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json"; + const tilesetTargetName = "./output/SparseImplicitQuadtree-optimized/tileset.json"; + const overwrite = true; + + const quiet = false; + const tilesetContentProcessor = new (class extends TilesetContentProcessor { + override async processTileContentEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise { + if (type !== "CONTENT_TYPE_GLB") { + return sourceEntry; + } + const targetEntry = { + key: "optimized/" + sourceEntry.key, + value: await GltfUtilities.optimizeGlb(sourceEntry.value, {}), + }; + console.log(" Optimized " + sourceEntry.key + " to " + targetEntry.key); + return targetEntry; + } + + override async processImplicitTilesetRootContent(content: Content): Promise { + content.uri = "optimized/" + content.uri + return content; + } + })(quiet); await tilesetContentProcessor.process( tilesetSourceName, @@ -33,4 +73,7 @@ async function runB3dmToGlbTest() { ); } -runB3dmToGlbTest(); + + +//runB3dmToGlbTest(); +runOptimizeTest(); diff --git a/src/tilesetProcessing/TilesetContentProcessor.ts b/src/tilesetProcessing/TilesetContentProcessor.ts index 68709117..d5d852ef 100644 --- a/src/tilesetProcessing/TilesetContentProcessor.ts +++ b/src/tilesetProcessing/TilesetContentProcessor.ts @@ -24,6 +24,8 @@ import { Tiles } from "../tilesets/Tiles"; import { Tilesets } from "../tilesets/Tilesets"; /** + * TODO Adjust this comment for the process* methods: + * * A function that can process one entry of a tileset dataset. * * This will be called ONCE for each entry of the tileset source, @@ -61,23 +63,11 @@ import { Tilesets } from "../tilesets/Tilesets"; * @throws TilesetError When the input could not be processed * */ -export type ProcessEntryCallback = ( +type ProcessEntryCallback = ( sourceEntry: TilesetEntry, type: string | undefined ) => Promise; -/** - * A callback that will be called ONCE for each content - * that is contained in a tile that is a root of an - * implicit tileset. - * - * Specifically, these are the contents where the `content.uri` - * is a template URI. - */ -export type ProcessImplicitTilesetRootContentCallback = ( - content: Content -) => Promise; - /** */ export class TilesetContentProcessor { @@ -99,28 +89,10 @@ export class TilesetContentProcessor { /** * The set of keys (file names) that have already been processed. * This includes the original keys, as well as new keys that - * have been assigned to entries in the `processEntryCallback`. + * have been assigned to entries while they have been processed. */ private processedKeys: { [key: string]: boolean } = {}; - /** - * The callback that will be called for each entry. - * - * See `ProcessEntryCallback` for details. - */ - private processEntryCallback: ProcessEntryCallback | undefined = - this.processEntryNoOp.bind(this); - - /** - * The callback that will be called for each content of a tile - * that is the root of an implicit tileset. - * - * See `ProcessImplicitTilesetRootContentCallback` for details. - */ - private processImplicitTilesetRootContentCallback: - | ProcessImplicitTilesetRootContentCallback - | undefined = undefined; - /** * Creates a new instance * @@ -135,29 +107,6 @@ export class TilesetContentProcessor { } } - /** - * Set the callback that will be called for each entry. - * - * See `ProcessEntryCallback` for details. - * - * @param callback - The callback - */ - setProcessEntryCallback(callback: ProcessEntryCallback | undefined) { - this.processEntryCallback = callback; - } - - /** - * Set the callback that will be called for each content of a tile - * that is the root of an implicit tileset. - * - * See `ProcessImplicitTilesetRootContentCallback` for details. - */ - setProcessImplicitTilesetRootContentCallback( - callback: ProcessImplicitTilesetRootContentCallback | undefined - ) { - this.processImplicitTilesetRootContentCallback = callback; - } - /** * Internal method to just call the log callback * @@ -390,9 +339,6 @@ export class TilesetContentProcessor { * - all entries (including all of the above) * if they haven't been processed yet. * - * All these operations will eventually end up in the - * `processEntryCallback` - see this field for details. - * * @param tileset - The tileset * @param schema - The optional metadata schema for the tileset * @returns A promise that resolves when the process is finished @@ -411,18 +357,13 @@ export class TilesetContentProcessor { this.log(`Processing all entries`); await this.processAllEntries(); - if (this.processImplicitTilesetRootContentCallback) { - this.log(`Processing all implicit tileset roots`); - await this.processImplicitTilesetRoots(tileset); - } + this.log(`Processing all implicit tileset roots`); + await this.processImplicitTilesetRoots(tileset); } /** * Process all entries that are tile content of explicit tiles. * - * Each entry will eventually be processed with the - * `processEntryCallback` - see this field for details. - * * @param tileset - The tileset * @returns A promise that resolves when the process is finished * @throws TilesetError When the input could not be processed @@ -441,9 +382,6 @@ export class TilesetContentProcessor { /** * Process all entries that are content of the given tile. * - * Each entry will eventually be processed with the - * `processEntryCallback` - see this field for details. - * * @param tile - The tile * @returns A promise that resolves when the process is finished * @throws TilesetError When the input could not be processed @@ -457,7 +395,10 @@ export class TilesetContentProcessor { } if (tile.content) { const content = tile.content; - const targetEntry = await this.processEntry(content.uri); + const targetEntry = await this.processEntry( + content.uri, + this.processExplicitTileContentEntry + ); if (targetEntry) { content.uri = targetEntry.key; } else { @@ -466,7 +407,10 @@ export class TilesetContentProcessor { } else if (tile.contents) { const newContents: Content[] = []; for (const content of tile.contents) { - const targetEntry = await this.processEntry(content.uri); + const targetEntry = await this.processEntry( + content.uri, + this.processExplicitTileContentEntry + ); if (targetEntry) { content.uri = targetEntry.key; newContents.push(content); @@ -503,23 +447,18 @@ export class TilesetContentProcessor { * * @param tile - The tile * @returns A promise that resolves when the process is finished - * @throws DeveloperError If `processImplicitTilesetRootContentCallback` - * is not defined * @throws TilesetError When the input could not be processed */ private async processImplicitTilesetRoot(tile: Tile): Promise { - const callback = this.processImplicitTilesetRootContentCallback; - if (!callback) { - throw new DeveloperError( - "No callback for implicit tileset root contents" - ); - } if (tile.content) { const content = tile.content; - await callback(content); + const newContent = await this.processImplicitTilesetRootContent(content); + tile.content = newContent; } else if (tile.contents) { - for (const content of tile.contents) { - await callback(content); + for (let i=0; i c.uri); for (const contentUri of contentUris) { - await this.processEntry(contentUri); + await this.processEntry(contentUri, this.processTileContentEntry); } } return true; @@ -578,9 +514,6 @@ export class TilesetContentProcessor { * Process all entries that are contained in the current * tileset source. * - * Each entry will eventually be processed with the - * `processEntryCallback` - see this field for details. - * * @param tileset - The tileset * @returns A promise that resolves when the process is finished * @throws DeveloperError When the source or target is not opened @@ -593,7 +526,7 @@ export class TilesetContentProcessor { const entries = TilesetSources.getEntries(this.tilesetSource); for (const entry of entries) { const key = entry.key; - await this.processEntry(key); + await this.processEntry(key, this.processGenericEntry); } } @@ -620,7 +553,10 @@ export class TilesetContentProcessor { * @throws DeveloperError When the source or target is not opened * @throws TilesetError When the input could not be processed */ - private async processEntry(key: string): Promise { + private async processEntry( + key: string, + callback: ProcessEntryCallback + ): Promise { if (!this.tilesetSource || !this.tilesetTarget) { throw new DeveloperError("The source and target must be defined"); } @@ -644,7 +580,6 @@ export class TilesetContentProcessor { this.log(`Processing source: ${sourceKey} with type ${type}`); - const callback = this.processEntryCallback ?? this.processEntryNoOp; const targetEntry = await callback(sourceEntry, type); this.log(` to target: ${targetEntry?.key}`); @@ -656,6 +591,28 @@ export class TilesetContentProcessor { return targetEntry; } + async processExplicitTileContentEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise { + return sourceEntry; + } + async processTileContentEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise { + return sourceEntry; + } + async processGenericEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise { + return sourceEntry; + } + async processImplicitTilesetRootContent(content: Content): Promise { + return content; + } + /** * TODO Consider something like this, for example, for "inlining" * references to external PNGs into a GLB, and then say From 5afd909b9a44a4dc0d128887e7940f05d4518ac6 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 23 Mar 2023 15:09:09 +0100 Subject: [PATCH 20/60] Update for changes in base branch --- src/tileFormats/TileDataLayouts.ts | 2 +- src/tileFormats/TileFormats.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/tileFormats/TileDataLayouts.ts b/src/tileFormats/TileDataLayouts.ts index 53548f9e..d80a3d3c 100644 --- a/src/tileFormats/TileDataLayouts.ts +++ b/src/tileFormats/TileDataLayouts.ts @@ -45,7 +45,7 @@ export interface TileDataLayout { export class TileDataLayouts { static create(buffer: Buffer): TileDataLayout { // Basic checks for magic number, length and version - const magic = Buffers.getMagic(buffer); + const magic = Buffers.getMagicString(buffer); if (magic !== "b3dm" && magic !== "pnts" && magic !== "i3dm") { throw new TileFormatError( `Expected magic "b3dm", "i3dm", or "pnts", but found "${magic}"` diff --git a/src/tileFormats/TileFormats.ts b/src/tileFormats/TileFormats.ts index 2da25080..d2678521 100644 --- a/src/tileFormats/TileFormats.ts +++ b/src/tileFormats/TileFormats.ts @@ -2,7 +2,8 @@ import { Buffers } from "../base/Buffers"; import { CompositeTileData } from "./CompositeTileData"; import { TileData } from "./TileData"; -import { TileDataLayout, TileDataLayouts } from "./TileDataLayouts"; +import { TileDataLayout } from "./TileDataLayouts"; +import { TileDataLayouts } from "./TileDataLayouts"; import { TileFormatError } from "./TileFormatError"; /** From 6f2e1e0b54e4fbe391d4c7b3258c2e52a82daddc Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 23 Mar 2023 15:09:51 +0100 Subject: [PATCH 21/60] Formatting --- demos/TilesetContentProcessorDrafts.ts | 16 +- src/main.ts | 196 +++++++++--------- .../TilesetContentProcessor.ts | 6 +- 3 files changed, 111 insertions(+), 107 deletions(-) diff --git a/demos/TilesetContentProcessorDrafts.ts b/demos/TilesetContentProcessorDrafts.ts index dc067f37..264223f6 100644 --- a/demos/TilesetContentProcessorDrafts.ts +++ b/demos/TilesetContentProcessorDrafts.ts @@ -36,11 +36,11 @@ async function runB3dmToGlbTest() { ); } - async function runOptimizeTest() { const tilesetSourceName = "../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json"; - const tilesetTargetName = "./output/SparseImplicitQuadtree-optimized/tileset.json"; + const tilesetTargetName = + "./output/SparseImplicitQuadtree-optimized/tileset.json"; const overwrite = true; const quiet = false; @@ -56,12 +56,16 @@ async function runOptimizeTest() { key: "optimized/" + sourceEntry.key, value: await GltfUtilities.optimizeGlb(sourceEntry.value, {}), }; - console.log(" Optimized " + sourceEntry.key + " to " + targetEntry.key); + console.log( + " Optimized " + sourceEntry.key + " to " + targetEntry.key + ); return targetEntry; } - override async processImplicitTilesetRootContent(content: Content): Promise { - content.uri = "optimized/" + content.uri + override async processImplicitTilesetRootContent( + content: Content + ): Promise { + content.uri = "optimized/" + content.uri; return content; } })(quiet); @@ -73,7 +77,5 @@ async function runOptimizeTest() { ); } - - //runB3dmToGlbTest(); runOptimizeTest(); diff --git a/src/main.ts b/src/main.ts index 5bd5f510..45c77cc5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -54,125 +54,125 @@ const inputArrayDefinition: any = { */ function parseToolArgs(a: string[]) { const args = yargs(a) - .usage("Usage: $0 [options]") - .help("h") - .alias("h", "help") - .options({ - o: { - alias: "output", - description: "Output path for the command.", - global: true, - normalize: true, - type: "string", - demandOption: true, - }, - f: { - alias: "force", - default: false, - description: "Output can be overwritten if it already exists.", - global: true, - type: "boolean", - }, - }) - .command("tilesetToDatabase", "Create a sqlite database for a tileset.", { - i: inputStringDefinition, - }) - .command( - "databaseToTileset", - "Unpack a tileset database to a tileset folder.", - { i: inputStringDefinition } - ) - .command( - "glbToB3dm", - "Repackage the input glb as a b3dm with a basic header.", - { i: inputStringDefinition } - ) - .command( - "glbToI3dm", - "Repackage the input glb as a i3dm with a basic header.", - { i: inputStringDefinition } - ) + .usage("Usage: $0 [options]") + .help("h") + .alias("h", "help") + .options({ + o: { + alias: "output", + description: "Output path for the command.", + global: true, + normalize: true, + type: "string", + demandOption: true, + }, + f: { + alias: "force", + default: false, + description: "Output can be overwritten if it already exists.", + global: true, + type: "boolean", + }, + }) + .command("tilesetToDatabase", "Create a sqlite database for a tileset.", { + i: inputStringDefinition, + }) + .command( + "databaseToTileset", + "Unpack a tileset database to a tileset folder.", + { i: inputStringDefinition } + ) + .command( + "glbToB3dm", + "Repackage the input glb as a b3dm with a basic header.", + { i: inputStringDefinition } + ) + .command( + "glbToI3dm", + "Repackage the input glb as a i3dm with a basic header.", + { i: inputStringDefinition } + ) .command( "b3dmToGlb", "Extract the binary glTF asset from the input b3dm.", { - i: inputStringDefinition, + i: inputStringDefinition, } ) .command( "i3dmToGlb", "Extract the binary glTF asset from the input i3dm.", { - i: inputStringDefinition, + i: inputStringDefinition, } ) .command( "cmptToGlb", "Extract the binary glTF assets from the input cmpt.", { - i: inputStringDefinition, + i: inputStringDefinition, } ) - .command( - "optimizeB3dm", - "Pass the input b3dm through gltf-pipeline. To pass options to gltf-pipeline, place them after --options. (--options -h for gltf-pipeline help)", - { + .command( + "optimizeB3dm", + "Pass the input b3dm through gltf-pipeline. To pass options to gltf-pipeline, place them after --options. (--options -h for gltf-pipeline help)", + { i: inputStringDefinition, - options: { - description: - "All arguments after this flag will be passed to gltf-pipeline as command line options.", - }, - } - ) - .command( - "optimizeI3dm", - "Pass the input i3dm through gltf-pipeline. To pass options to gltf-pipeline, place them after --options. (--options -h for gltf-pipeline help)", - { + options: { + description: + "All arguments after this flag will be passed to gltf-pipeline as command line options.", + }, + } + ) + .command( + "optimizeI3dm", + "Pass the input i3dm through gltf-pipeline. To pass options to gltf-pipeline, place them after --options. (--options -h for gltf-pipeline help)", + { + i: inputStringDefinition, + options: { + description: + "All arguments after this flag will be passed to gltf-pipeline as command line options.", + }, + } + ) + .command("gzip", "Gzips the input tileset directory.", { i: inputStringDefinition, - options: { - description: - "All arguments after this flag will be passed to gltf-pipeline as command line options.", + t: { + alias: "tilesOnly", + default: false, + description: "Only tile content files should be gzipped.", + type: "boolean", }, - } - ) - .command("gzip", "Gzips the input tileset directory.", { - i: inputStringDefinition, - t: { - alias: "tilesOnly", - default: false, - description: "Only tile content files should be gzipped.", - type: "boolean", - }, - }) - .command("ungzip", "Ungzips the input tileset directory.", { - i: inputStringDefinition, - }) - .command( - "combine", - "Combines all external tilesets into a single tileset.json file.", - { i: inputStringDefinition } - ) - .command( - "merge", - "Merge any number of tilesets together into a single tileset.", - { i: inputArrayDefinition } - ) - .command( - "upgrade", - "Upgrades the input tileset to the latest version of the 3D Tiles spec. Embedded glTF models will be upgraded to glTF 2.0.", - { i: inputStringDefinition } - ) - .command( - "analyze", - "Analyze the input file, and write the results to the output directory. " + - "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", - { i: inputStringDefinition } - ) - .demandCommand(1) - .strict(); + }) + .command("ungzip", "Ungzips the input tileset directory.", { + i: inputStringDefinition, + }) + .command( + "combine", + "Combines all external tilesets into a single tileset.json file.", + { i: inputStringDefinition } + ) + .command( + "merge", + "Merge any number of tilesets together into a single tileset.", + { i: inputArrayDefinition } + ) + .command( + "upgrade", + "Upgrades the input tileset to the latest version of the 3D Tiles spec. Embedded glTF models will be upgraded to glTF 2.0.", + { i: inputStringDefinition } + ) + .command( + "analyze", + "Analyze the input file, and write the results to the output directory. " + + "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", + { i: inputStringDefinition } + ) + .demandCommand(1) + .strict(); return args.argv as any; } diff --git a/src/tilesetProcessing/TilesetContentProcessor.ts b/src/tilesetProcessing/TilesetContentProcessor.ts index d5d852ef..1564e94d 100644 --- a/src/tilesetProcessing/TilesetContentProcessor.ts +++ b/src/tilesetProcessing/TilesetContentProcessor.ts @@ -455,9 +455,11 @@ export class TilesetContentProcessor { const newContent = await this.processImplicitTilesetRootContent(content); tile.content = newContent; } else if (tile.contents) { - for (let i=0; i Date: Sun, 26 Mar 2023 15:51:24 +0200 Subject: [PATCH 22/60] Fix method name for base branch update --- src/contentProcessing/GtlfUtilities.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/contentProcessing/GtlfUtilities.ts b/src/contentProcessing/GtlfUtilities.ts index 1ed71bcc..ff0f2a1e 100644 --- a/src/contentProcessing/GtlfUtilities.ts +++ b/src/contentProcessing/GtlfUtilities.ts @@ -37,7 +37,7 @@ export class GltfUtilities { * @throws TileFormatError If the input does not contain valid GLB data. */ static extractJsonFromGlb(glbBuffer: Buffer): Buffer { - const magic = Buffers.getMagic(glbBuffer); + const magic = Buffers.getMagicString(glbBuffer); if (magic !== "glTF") { throw new TileFormatError( `Expected magic header to be 'gltf', but found ${magic}` From 49cf568d31d40cebfc70ae2b85c5f5fdc3443bee Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 26 Mar 2023 15:52:26 +0200 Subject: [PATCH 23/60] Preliminarily unify tileset processing --- demos/TilesetContentProcessorDrafts.ts | 41 +- src/tilesetProcessing/TilesetCombiner.ts | 28 +- .../TilesetContentProcessor.ts | 591 ++---------------- .../TilesetEntryProcessor.ts | 31 + .../TilesetExplicitContentProcessor.ts | 173 +++++ src/tilesetProcessing/TilesetMerger.ts | 38 +- src/tilesetProcessing/TilesetProcessor.ts | 481 ++++++++++++++ 7 files changed, 812 insertions(+), 571 deletions(-) create mode 100644 src/tilesetProcessing/TilesetEntryProcessor.ts create mode 100644 src/tilesetProcessing/TilesetExplicitContentProcessor.ts create mode 100644 src/tilesetProcessing/TilesetProcessor.ts diff --git a/demos/TilesetContentProcessorDrafts.ts b/demos/TilesetContentProcessorDrafts.ts index 264223f6..9ea553f4 100644 --- a/demos/TilesetContentProcessorDrafts.ts +++ b/demos/TilesetContentProcessorDrafts.ts @@ -3,7 +3,9 @@ import { ContentOps } from "../src/contentProcessing/ContentOps"; import { GltfUtilities } from "../src/contentProcessing/GtlfUtilities"; import { Content } from "../src/structure/Content"; import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; + import { TilesetContentProcessor } from "../src/tilesetProcessing/TilesetContentProcessor"; +import { TilesetExplicitContentProcessor } from "../src/tilesetProcessing/TilesetExplicitContentProcessor"; async function runB3dmToGlbTest() { const tilesetSourceName = @@ -12,22 +14,25 @@ async function runB3dmToGlbTest() { const overwrite = true; const quiet = false; - const tilesetContentProcessor = new (class extends TilesetContentProcessor { - override async processExplicitTileContentEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise { - if (type !== "CONTENT_TYPE_B3DM") { - return sourceEntry; + const tilesetContentProcessor = + new (class extends TilesetExplicitContentProcessor { + override async processExplicitTileContentEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise { + if (type !== "CONTENT_TYPE_B3DM") { + return [sourceEntry]; + } + const targetEntry = { + key: Paths.replaceExtension(sourceEntry.key, ".glb"), + value: ContentOps.b3dmToGlbBuffer(sourceEntry.value), + }; + console.log( + " Updated " + sourceEntry.key + " to " + targetEntry.key + ); + return [targetEntry]; } - const targetEntry = { - key: Paths.replaceExtension(sourceEntry.key, ".glb"), - value: ContentOps.b3dmToGlbBuffer(sourceEntry.value), - }; - console.log(" Updated " + sourceEntry.key + " to " + targetEntry.key); - return targetEntry; - } - })(quiet); + })(quiet); await tilesetContentProcessor.process( tilesetSourceName, @@ -48,9 +53,9 @@ async function runOptimizeTest() { override async processTileContentEntry( sourceEntry: TilesetEntry, type: string | undefined - ): Promise { + ): Promise { if (type !== "CONTENT_TYPE_GLB") { - return sourceEntry; + return [sourceEntry]; } const targetEntry = { key: "optimized/" + sourceEntry.key, @@ -59,7 +64,7 @@ async function runOptimizeTest() { console.log( " Optimized " + sourceEntry.key + " to " + targetEntry.key ); - return targetEntry; + return [targetEntry]; } override async processImplicitTilesetRootContent( diff --git a/src/tilesetProcessing/TilesetCombiner.ts b/src/tilesetProcessing/TilesetCombiner.ts index fc6ab4db..10306bf8 100644 --- a/src/tilesetProcessing/TilesetCombiner.ts +++ b/src/tilesetProcessing/TilesetCombiner.ts @@ -87,13 +87,17 @@ export class TilesetCombiner { this.tilesetSource = tilesetSource; this.tilesetTarget = tilesetTarget; - const tilesetJsonFileName = + const tilesetSourceJsonFileName = Tilesets.determineTilesetJsonFileName(tilesetSourceName); + const tilesetTargetJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetTargetName); + await this.combineInternal( tilesetSource, - tilesetJsonFileName, - tilesetTarget + tilesetSourceJsonFileName, + tilesetTarget, + tilesetTargetJsonFileName ); tilesetSource.close(); @@ -111,9 +115,11 @@ export class TilesetCombiner { * source and target. * * @param tilesetSource The tileset source - * @param tilesetJsonFileName The name of the top-level tileset in + * @param tilesetSourceJsonFileName The name of the top-level tileset in * the given source (usually `tileset.json`). * @param tilesetTarget The tileset target + * @param tilesetTargetJsonFileName The name of the top-level tileset in + * the given target (usually `tileset.json`). * @returns A promise that resolves when the process is finished. * @throws TilesetError When the input tileset file can not be * found @@ -122,12 +128,13 @@ export class TilesetCombiner { */ private async combineInternal( tilesetSource: TilesetSource, - tilesetJsonFileName: string, - tilesetTarget: TilesetTarget + tilesetSourceJsonFileName: string, + tilesetTarget: TilesetTarget, + tilesetTargetJsonFileName: string ): Promise { - const tilesetJsonBuffer = tilesetSource.getValue(tilesetJsonFileName); + const tilesetJsonBuffer = tilesetSource.getValue(tilesetSourceJsonFileName); if (!tilesetJsonBuffer) { - const message = `No ${tilesetJsonFileName} found in input`; + const message = `No ${tilesetSourceJsonFileName} found in input`; throw new TilesetError(message); } const tileset = JSON.parse(tilesetJsonBuffer.toString()) as Tileset; @@ -139,7 +146,10 @@ export class TilesetCombiner { const combinedTilesetJsonString = JSON.stringify(tileset, null, 2); const combinedTilesetJsonBuffer = Buffer.from(combinedTilesetJsonString); - tilesetTarget.addEntry("tileset.json", combinedTilesetJsonBuffer); + tilesetTarget.addEntry( + tilesetTargetJsonFileName, + combinedTilesetJsonBuffer + ); } /** diff --git a/src/tilesetProcessing/TilesetContentProcessor.ts b/src/tilesetProcessing/TilesetContentProcessor.ts index 1564e94d..4d1733e8 100644 --- a/src/tilesetProcessing/TilesetContentProcessor.ts +++ b/src/tilesetProcessing/TilesetContentProcessor.ts @@ -1,427 +1,62 @@ -import { Buffers } from "../base/Buffers"; import { DeveloperError } from "../base/DeveloperError"; import { TilesetSourceResourceResolver } from "../io/TilesetSourceResourceResolver"; -import { BufferedContentData } from "../contentTypes/BufferedContentData"; -import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; - import { Tile } from "../structure/Tile"; import { Tileset } from "../structure/Tileset"; import { Content } from "../structure/Content"; import { Schema } from "../structure/Metadata/Schema"; -import { TilesetError } from "../tilesetData/TilesetError"; -import { TilesetSource } from "../tilesetData/TilesetSource"; -import { TilesetTarget } from "../tilesetData/TilesetTarget"; -import { TilesetTargets } from "../tilesetData/TilesetTargets"; -import { TilesetSources } from "../tilesetData/TilesetSources"; import { TilesetEntry } from "../tilesetData/TilesetEntry"; import { TilesetTraverser } from "../traversal/TilesetTraverser"; import { Tiles } from "../tilesets/Tiles"; -import { Tilesets } from "../tilesets/Tilesets"; + +import { TilesetProcessor } from "./TilesetProcessor"; /** - * TODO Adjust this comment for the process* methods: - * - * A function that can process one entry of a tileset dataset. - * - * This will be called ONCE for each entry of the tileset source, - * and return an entry that is supposed to be put into the tileset - * target. - * - * It receives the source entry, which may represent a content - * of an (explicit) tile, a content of an implicit tile, or just - * one entry of the tileset source (i.e. a "file" that is not - * a tile content). - * - * It returns the "processed" entry that is supposed to put into - * the tileset target. If the returned entry is `undefined`, then - * this means that the entry should be omitted in the target. + * A base class for classes that can process the content of tiles of + * tilesets. * - * Otherwise, the returned entry may have a different `key` - * (file name), and/or a modified `value` (file data). This - * entry will be put into the tileset target. + * It defines two abstract methods: The `processTileContentEntry` method + * may be overridden by subclasses to process each entry as necessary. * - * Note that a modification of the `key` have different implications: + * The `processImplicitTilesetRootContent` method may be called to + * update the `content.uri` (i.e. the template URI) of tiles that are + * the roots of implicit tilesets, so that they reflect the modifications + * of key (file names) that are done in `processTileContentEntry`. * - * - For explicit tile content, changes in the `key` will automatically - * be taken into account, by updating the `content.uri` accordingly - * - For implicit tile content, changes in the `key` have to be taken - * into account by updating template URIs. - * - For files, changes in the `key` have to be taken into account by - * domain-specific knowledge about what these files actually are. - * - * @param sourceEntry - The source entry - * @param type - The type of the entry data (see `ContentDataTypes`), - * or `undefined` if the type could not be determined. - * @returns A promise that resolves when the process is finished, - * containing either the new entry, or `undefined` when the entry - * was supposed to be removed (i.e. omitted in the target). - * @throws TilesetError When the input could not be processed - * - */ -type ProcessEntryCallback = ( - sourceEntry: TilesetEntry, - type: string | undefined -) => Promise; - -/** + * Entries that are no tile content will be processed with the + * `processEntry` method, which is implemented as a no-op by + * default. */ -export class TilesetContentProcessor { - /** - * A function that will receive log messages - */ - private readonly logCallback: (message: any) => void; - - /** - * The tileset source for the input - */ - private tilesetSource: TilesetSource | undefined; - - /** - * The tileset target for the output. - */ - private tilesetTarget: TilesetTarget | undefined; - - /** - * The set of keys (file names) that have already been processed. - * This includes the original keys, as well as new keys that - * have been assigned to entries while they have been processed. - */ - private processedKeys: { [key: string]: boolean } = {}; - +export abstract class TilesetContentProcessor extends TilesetProcessor { /** * Creates a new instance * * @param quiet - Whether log messages should be omitted */ constructor(quiet?: boolean) { - if (quiet !== true) { - this.logCallback = (message: any) => console.log(message); - } else { - // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - this.logCallback = (message: any) => {}; - } - } - - /** - * Internal method to just call the log callback - * - * @param message - The message - */ - private log(message: any): void { - this.logCallback(message); - } - - /** - * Process the specified source tileset, and write it to the given - * target. - * - * @param tilesetSourceName - The tileset source name - * @param tilesetTargetName - The tileset target name - * @param overwrite Whether the target should be overwritten if - * it already exists - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed, - * or when the output already exists and `overwrite` was `false`. - */ - async process( - tilesetSourceName: string, - tilesetTargetName: string, - overwrite: boolean - ): Promise { - // TODO Somehow ensure that the source is closed - // if the target throws up (try-with-resources FTW) - const tilesetSource = TilesetSources.createAndOpen(tilesetSourceName); - const tilesetTarget = TilesetTargets.createAndBegin( - tilesetTargetName, - overwrite - ); - - this.tilesetSource = tilesetSource; - this.tilesetTarget = tilesetTarget; - - const tilesetSourceJsonFileName = - Tilesets.determineTilesetJsonFileName(tilesetSourceName); - - const tilesetTargetJsonFileName = - Tilesets.determineTilesetJsonFileName(tilesetTargetName); - - await this.processInternal( - tilesetSourceJsonFileName, - tilesetTargetJsonFileName - ); - - tilesetSource.close(); - await tilesetTarget.end(); - - this.tilesetSource = undefined; - this.tilesetTarget = undefined; - Object.keys(this.processedKeys).forEach( - (key) => delete this.processedKeys[key] - ); - } - - /** - * Internal (top-level) method for the processing. - * - * It reads the tileset JSON from the specified source, passes - * it to `processTileset`, and writes the tileset JSON to the - * specified target. - * - * Any operations that affect files other than the tileset JSON - * file are part of `processTileset` - * - * @param tilesetSourceName - The tileset source name - * @param tilesetTargetName - The tileset target name - * @returns A promise that resolves when the process is finished - * @throws DeveloperError When the source or target is not opened - * @throws TilesetError When the input could not be processed - */ - private async processInternal( - tilesetSourceJsonFileName: string, - tilesetTargetJsonFileName: string - ): Promise { - if (!this.tilesetSource || !this.tilesetTarget) { - throw new DeveloperError("The source and target must be defined"); - } - - // Obtain the tileset object from the tileset JSON file - const parsedTileset = this.parseSourceValue( - tilesetSourceJsonFileName - ); - - // Resolve the schema, either from the `tileset.schema` - // or the `tileset.schemaUri` - const schema = this.resolveSchema(parsedTileset.result); - - // Process the actual tileset - await this.processTileset(parsedTileset.result, schema); - - // Store the resulting tileset as JSON - this.storeTargetValue( - tilesetTargetJsonFileName, - parsedTileset.wasZipped, - parsedTileset.result - ); - } - - /** - * Parses the JSON from the value with the given key (file name), - * and returns the parsed result, AND information of whether the - * input was zipped. - * - * This is mainly a convenience function to emulate the behavior of the - * "legacy" tools in terms of handling the tileset JSON: When writing - * the tileset JSON data to the target, then it should zip that JSON - * data if and only if it was zipped in the input. - * - * See `storeTargetValue` for the counterpart of this method. - * - * In the future, there might be mechanisms for a more fine-grained - * control over whether certain files should be zipped or not... - * - * @param key - The key (file name) - * @returns A structure containing the `wasZipped` information, and - * the parsed result - * @throws TilesetError If the source is not opened, the specified - * entry cannot be found, or the entry data could not be unzipped, - * or its contents could not be parsed as JSON. - */ - private parseSourceValue(key: string): { wasZipped: boolean; result: T } { - let value = this.getSourceValue(key); - let wasZipped = false; - if (Buffers.isGzipped(value)) { - wasZipped = true; - try { - value = Buffers.gunzip(value); - } catch (e) { - const message = `Could not unzip ${key}: ${e}`; - throw new TilesetError(message); - } - } - try { - const result = JSON.parse(value.toString()) as T; - return { - wasZipped: wasZipped, - result: result, - }; - } catch (e) { - const message = `Could not parse ${key}: ${e}`; - throw new TilesetError(message); - } - } - - /** - * Convert the given object into a JSON string, put it into a buffer, - * zip it (based on the `doZip` flag), and put the result into the - * tileset target. - * - * This is only intended for the "legacy" handling of the tileset - * JSON data, and is the counterpart of `parseSourceValue`. See - * `parseSourceValue` for details. - * - * @param key - The key (file name) - * @param doZip - Whether the output should be zipped - * @param object - The object for which the JSON should be stored - * @throws DeveloperError When the target is not opened - */ - private storeTargetValue(key: string, doZip: boolean, object: object) { - if (!this.tilesetTarget) { - throw new DeveloperError("The target must be defined"); - } - const jsonString = JSON.stringify(object, null, 2); - let jsonBuffer = Buffer.from(jsonString); - if (doZip) { - jsonBuffer = Buffers.gzip(jsonBuffer); - } - this.tilesetTarget.addEntry(key, jsonBuffer); - } - - /** - * Obtains the value for the given key from the current tileset source, - * throwing an error if the source is not opened, or when the - * given key cannot be found. - * - * @param key - The key (file name) - * @returns The value (file contents) - * @throws DeveloperError When the source is not opened - * @throws TilesetError When the given key cannot be found - */ - private getSourceValue(key: string): Buffer { - if (!this.tilesetSource) { - throw new DeveloperError("The source must be defined"); - } - const buffer = this.tilesetSource.getValue(key); - if (!buffer) { - const message = `No ${key} found in input`; - throw new TilesetError(message); - } - return buffer; + super(quiet); } - /** - * Resolve the `Schema` for the given tileset. - * - * This is either the `tileset.schema`, or the schema that is - * obtained from the `tileset.schemaUri`, or `undefined` if - * neither of them are present. - * - * @param tileset - The tileset - * @returns The `Schema`, or `undefined` if there is none - * @throws DeveloperError If the source is not opened - * @throws TilesetError If the schema from the `schemaUri` - * could not be resolved or parsed. - */ - private resolveSchema(tileset: Tileset): Schema | undefined { - if (!this.tilesetSource) { - throw new DeveloperError("The source must be defined"); - } - if (tileset.schema) { - return tileset.schema; - } - if (tileset.schemaUri) { - const parsedSchema = this.parseSourceValue(tileset.schemaUri); - return parsedSchema.result; - } - return undefined; - } - - /** - * Process the given tileset. - * - * This will process... - * - all explicit tile content entries - * - all tile content entries (including implicit ones) - * - all entries (including all of the above) - * if they haven't been processed yet. - * - * @param tileset - The tileset - * @param schema - The optional metadata schema for the tileset - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processTileset( + /** {@inheritDoc TilesetProcessor.processTilesetInternal} */ + override async processTilesetInternal( tileset: Tileset, schema: Schema | undefined ): Promise { - this.log(`Processing explicit tiles`); - await this.processExplicitTilesContentEntries(tileset); - this.log(`Processing all tiles`); await this.processAllTilesContentEntries(tileset, schema); this.log(`Processing all entries`); - await this.processAllEntries(); + await this.processEntries(); this.log(`Processing all implicit tileset roots`); await this.processImplicitTilesetRoots(tileset); - } - - /** - * Process all entries that are tile content of explicit tiles. - * - * @param tileset - The tileset - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processExplicitTilesContentEntries( - tileset: Tileset - ): Promise { - const root = tileset.root; - await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { - const tile = tilePath[tilePath.length - 1]; - await this.processExplicitTileContentEntries(tile); - return true; - }); - } - /** - * Process all entries that are content of the given tile. - * - * @param tile - The tile - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processExplicitTileContentEntries(tile: Tile): Promise { - // For roots of implicit tilesets, the content URI - // is a template URI (i.e. they are not explicit, - // and therefore not considered here) - if (tile.implicitTiling) { - return; - } - if (tile.content) { - const content = tile.content; - const targetEntry = await this.processEntry( - content.uri, - this.processExplicitTileContentEntry - ); - if (targetEntry) { - content.uri = targetEntry.key; - } else { - tile.content = undefined; - } - } else if (tile.contents) { - const newContents: Content[] = []; - for (const content of tile.contents) { - const targetEntry = await this.processEntry( - content.uri, - this.processExplicitTileContentEntry - ); - if (targetEntry) { - content.uri = targetEntry.key; - newContents.push(content); - } - } - if (newContents.length > 0) { - tile.contents = newContents; - } else { - tile.contents = undefined; - } - } + this.log(`Processing tileset JSON`); + await this.processTilesetJson(tileset, schema); } /** @@ -479,7 +114,8 @@ export class TilesetContentProcessor { tileset: Tileset, schema: Schema | undefined ): Promise { - if (!this.tilesetSource) { + const tilesetSource = this.getTilesetSource(); + if (!tilesetSource) { throw new DeveloperError("The source must be defined"); } @@ -488,7 +124,7 @@ export class TilesetContentProcessor { // during the traversal const resourceResolver = new TilesetSourceResourceResolver( ".", - this.tilesetSource + tilesetSource ); const depthFirst = false; const traverseExternalTilesets = false; @@ -502,7 +138,10 @@ export class TilesetContentProcessor { .getFinalContents() .map((c) => c.uri); for (const contentUri of contentUris) { - await this.processEntry(contentUri, this.processTileContentEntry); + await this.processEntryInternal( + contentUri, + this.processTileContentEntry + ); } } return true; @@ -512,165 +151,49 @@ export class TilesetContentProcessor { ); } - /** - * Process all entries that are contained in the current - * tileset source. - * - * @param tileset - The tileset - * @returns A promise that resolves when the process is finished - * @throws DeveloperError When the source or target is not opened - * @throws TilesetError When the input could not be processed - */ - private async processAllEntries(): Promise { - if (!this.tilesetSource || !this.tilesetTarget) { - throw new DeveloperError("The source and target must be defined"); - } - const entries = TilesetSources.getEntries(this.tilesetSource); - for (const entry of entries) { - const key = entry.key; - await this.processEntry(key, this.processGenericEntry); - } - } - - /** - * Process the specified entry. - * - * If the entry with the specified key was already processed, - * then this method does nothing. - * - * Otherwise, the specified entry will be looked up in the tileset - * source. Its content type will be determined. The source entry - * will be passed to `processEntryCallback`, which returns a target - * entry. If the target entry is defined, then it is inserted - * into the tileset target. - * - * This is the "staging" method for `processEntryCallback` - * (see this field for further details) - * - * @param tileset - The tileset - * @returns A promise that resolves when the process is finished, - * containing either the new entry that was put into the tileset - * target, or `undefined` when the entry was supposed to be - * omitted in the target. - * @throws DeveloperError When the source or target is not opened - * @throws TilesetError When the input could not be processed - */ - private async processEntry( - key: string, - callback: ProcessEntryCallback - ): Promise { - if (!this.tilesetSource || !this.tilesetTarget) { - throw new DeveloperError("The source and target must be defined"); - } - - const sourceKey = key; - if (this.processedKeys[sourceKey] === true) { - return; - } - this.processedKeys[sourceKey] = true; - - const sourceValue = this.tilesetSource.getValue(sourceKey); - if (!sourceValue) { - const message = `No ${sourceKey} found in input`; - throw new TilesetError(message); - } - const sourceEntry: TilesetEntry = { - key: sourceKey, - value: sourceValue, - }; - const type = await this.determineContentDataType(sourceKey, sourceValue); - - this.log(`Processing source: ${sourceKey} with type ${type}`); - - const targetEntry = await callback(sourceEntry, type); - - this.log(` to target: ${targetEntry?.key}`); - - if (targetEntry) { - this.tilesetTarget.addEntry(targetEntry.key, targetEntry.value); - this.processedKeys[targetEntry.key] = true; - } - return targetEntry; - } - - async processExplicitTileContentEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise { - return sourceEntry; - } - async processTileContentEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise { - return sourceEntry; - } - async processGenericEntry( + /** {@inheritDoc TilesetProcessor.processEntry} */ + protected override async processEntry( sourceEntry: TilesetEntry, + // eslint-disable-next-line @typescript-eslint/no-unused-vars type: string | undefined - ): Promise { - return sourceEntry; - } - async processImplicitTilesetRootContent(content: Content): Promise { - return content; + ): Promise { + return [sourceEntry]; } /** - * TODO Consider something like this, for example, for "inlining" - * references to external PNGs into a GLB, and then say - * markAsProcessed("./images/referredToByGlb.png"); - * to omit it in the output... + * Process a single entry that represents the content of a tile. * - * A method that can be called by implementations, to mark a certain - * file as already having been processed, and no longer be considered - * in subsequent steps. + * This is the main configuration point for this class: Implementors + * may override this method, perform arbitrary operations on the + * given entry, and return the result. * - * @param key - The key (file name) - */ - markAsProcessed(key: string) { - this.processedKeys[key] = true; - } - - /** - * Process an entry, doing nothing, for the case - * that no `processEntryCallback` is defined. + * (A no-op implementation of this method would be to just return an + * array that contains the given source entry as its only element) + * + * @param sourceEntry - The source entry + * @param type The content data type (see `ContentDataTypes`) + * @returns The target entries */ - private async processEntryNoOp( + abstract processTileContentEntry( sourceEntry: TilesetEntry, - // eslint-disable-next-line @typescript-eslint/no-unused-vars type: string | undefined - ): Promise { - this.log(`Performing no-op on ${sourceEntry.key}`); - - const sourceKey = sourceEntry.key; - const sourceValue = sourceEntry.value; - - const targetKey = sourceKey; - const targetValue = sourceValue; - - const targetEntry = { - key: targetKey, - value: targetValue, - }; - return targetEntry; - } + ): Promise; /** - * Determine the type of the given content data. + * Process the `content` of a tile that is the root of an implicit tileset. * - * The string will either be one of the `ContentDataTypes` strings, - * or `undefined` if the type cannot be determined. + * Implementors may override this method, and modify the given content + * as necessary, to reflect modifications that may have been done in + * `processTileContentEntry`. * - * @param key - The key (file name) - * @param value - The value (file contents) - * @returns A promise with the content data type string + * (A no-op implementation of this method would be to just return + * the given content, as it is) + * + * @param sourceEntry - The source entry + * @param type The content data type (see `ContentDataTypes`) + * @returns The target entries */ - private async determineContentDataType( - key: string, - value: Buffer - ): Promise { - const contentData = new BufferedContentData(key, value); - const type = await ContentDataTypeRegistry.findContentDataType(contentData); - return type; - } + abstract processImplicitTilesetRootContent( + content: Content + ): Promise; } diff --git a/src/tilesetProcessing/TilesetEntryProcessor.ts b/src/tilesetProcessing/TilesetEntryProcessor.ts new file mode 100644 index 00000000..fd28d572 --- /dev/null +++ b/src/tilesetProcessing/TilesetEntryProcessor.ts @@ -0,0 +1,31 @@ +import { TilesetEntry } from "../tilesetData/TilesetEntry"; + +/** + * A function that can process one `TilesetEntry` that is part + * of a tileset dataset. + * + * This is used as the type for the functions that process one + * entry in a `TilesetProcessor`. It will be called ONCE for each + * entry of the tileset source, and return entries that are + * supposed to be put into the tileset target. + * + * It receives the source entry, which may represent a content + * of an (explicit) tile, a content of any (possibly implicit) + * tile, or just one entry of the tileset source (i.e. a "file" + * that is not a tile content). + * + * It returns the "processed" entries that are supposed to put into + * the tileset target (which may be an empty array, causing the + * corresponding entry to be omitted in the target) + * + * @param sourceEntry - The source entry + * @param type - The type of the entry data (see `ContentDataTypes`), + * or `undefined` if the type could not be determined. + * @returns A promise that resolves when the process is finished, + * containing the resulting entries + * @throws TilesetError When the input could not be processed + */ +export type TilesetEntryProcessor = ( + sourceEntry: TilesetEntry, + type: string | undefined +) => Promise; diff --git a/src/tilesetProcessing/TilesetExplicitContentProcessor.ts b/src/tilesetProcessing/TilesetExplicitContentProcessor.ts new file mode 100644 index 00000000..2f4b5bae --- /dev/null +++ b/src/tilesetProcessing/TilesetExplicitContentProcessor.ts @@ -0,0 +1,173 @@ +import { Tile } from "../structure/Tile"; +import { Tileset } from "../structure/Tileset"; +import { Content } from "../structure/Content"; +import { Schema } from "../structure/Metadata/Schema"; + +import { TilesetEntry } from "../tilesetData/TilesetEntry"; + +import { Tiles } from "../tilesets/Tiles"; +import { TilesetProcessor } from "./TilesetProcessor"; + +/** + * A base class for classes that can process the content of + * explicit tiles of tilesets. + * + * The abstract `processExplicitTileContentEntry` method is the main + * configuration point: It may be overridden by subclasses to process + * each entry as necessary. + * + * Entries that are not explicit tile content will be processed with + * the `processEntry` method, which is implemented as a no-op by + * default. + */ +export abstract class TilesetExplicitContentProcessor extends TilesetProcessor { + /** + * Creates a new instance + * + * @param quiet - Whether log messages should be omitted + */ + constructor(quiet?: boolean) { + super(quiet); + } + + /** {@inheritDoc TilesetProcessor.processTilesetInternal} */ + override async processTilesetInternal( + tileset: Tileset, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + schema: Schema | undefined + ): Promise { + this.log(`Processing explicit tile content entries`); + await this.processExplicitTilesContentEntries(tileset); + + this.log(`Processing all entries`); + await this.processEntries(); + + this.log(`Processing tileset JSON`); + await this.processTilesetJson(tileset, schema); + } + + /** + * Process all entries that are tile content of explicit tiles. + * + * @param tileset - The tileset + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processExplicitTilesContentEntries( + tileset: Tileset + ): Promise { + const root = tileset.root; + await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { + const tile = tilePath[tilePath.length - 1]; + await this.processExplicitTileContentEntries(tile); + return true; + }); + } + + /** + * Process all entries that are content of the given tile. + * + * @param tile - The tile + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + private async processExplicitTileContentEntries(tile: Tile): Promise { + // For roots of implicit tilesets, the content URI + // is a template URI (i.e. they are not explicit, + // and therefore not considered here) + if (tile.implicitTiling) { + return; + } + if (tile.content) { + const content = tile.content; + const targetEntries = await this.processEntryInternal( + content.uri, + this.processExplicitTileContentEntry + ); + this.updateTileContent(tile, targetEntries); + } else if (tile.contents) { + const allTargetEntries = []; + for (const content of tile.contents) { + const targetEntries = await this.processEntryInternal( + content.uri, + this.processExplicitTileContentEntry + ); + allTargetEntries.push(...targetEntries); + } + this.updateTileContent(tile, allTargetEntries); + } + } + + /** + * Update the content of the given tile to reflect the given entries. + * + * When the given entries are empty, then the `content` and `contents` + * of the given tile will be deleted. + * + * When there is one entry, then the `content` of the given tile will + * receive the `key` (file name) of this entry as the content `uri`. + * + * When there are multiple entries, the tile will receive `contents` + * where each content `uri` is one `key` file name of the entries. + * + * @param tile - The tile + * @param targetEntries - The target entries + */ + private updateTileContent(tile: Tile, targetEntries: TilesetEntry[]) { + if (targetEntries.length === 0) { + delete tile.content; + delete tile.contents; + return; + } + if (targetEntries.length === 1) { + const targetEntry = targetEntries[0]; + if (tile.content) { + tile.content.uri = targetEntry.key; + } else { + const content = { + uri: targetEntry.key, + }; + tile.content = content; + delete tile.contents; + } + } + + const newContents: Content[] = []; + for (const targetEntry of targetEntries) { + const content = { + uri: targetEntry.key, + }; + newContents.push(content); + } + tile.contents = newContents; + delete tile.content; + } + + /** {@inheritDoc TilesetProcessor.processEntry} */ + protected override async processEntry( + sourceEntry: TilesetEntry, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + type: string | undefined + ): Promise { + return [sourceEntry]; + } + + /** + * Process a single entry that represents the content of an explicit tile. + * + * This is the main configuration point for this class: Implementors + * may override this method, perform arbitrary operations on the + * given entry, and return the result. + * + * (A no-op implementation of this method would be to just return an + * array that contains the given source entry as its only element) + * + * @param sourceEntry - The source entry + * @param type The content data type (see `ContentDataTypes`) + * @returns The target entries + */ + abstract processExplicitTileContentEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise; +} diff --git a/src/tilesetProcessing/TilesetMerger.ts b/src/tilesetProcessing/TilesetMerger.ts index 36ac0329..5c8776f3 100644 --- a/src/tilesetProcessing/TilesetMerger.ts +++ b/src/tilesetProcessing/TilesetMerger.ts @@ -39,7 +39,7 @@ export class TilesetMerger { * If the inputs are directories or files that do not end with ".json", * then these names will default to "tileset.json" */ - private tilesetJsonFileNames: string[]; + private tilesetSourceJsonFileNames: string[]; /** * Identifiers for the external tilesets. These will usually @@ -54,12 +54,18 @@ export class TilesetMerger { */ private tilesetTarget: TilesetTarget | undefined; + /** + * The name of the tileset JSON file in the target. + * (Usually `tileset.json`) + */ + private tilesetTargetJsonFileName: string | undefined; + /** * Creates a new instance */ constructor() { this.tilesetSources = []; - this.tilesetJsonFileNames = []; + this.tilesetSourceJsonFileNames = []; this.tilesetSourceIdentifiers = []; } @@ -84,7 +90,7 @@ export class TilesetMerger { // Create the sources and target for (const tilesetSourceName of tilesetSourceNames) { // Determine the name of the file that contains the tileset JSON data - const tilesetJsonFileName = + const tilesetSourceJsonFileName = Tilesets.determineTilesetJsonFileName(tilesetSourceName); // Determine an "identifier" for the tileset source @@ -104,10 +110,12 @@ export class TilesetMerger { const tilesetSource = TilesetSources.createAndOpen(tilesetSourceName); this.tilesetSources.push(tilesetSource); - this.tilesetJsonFileNames.push(tilesetJsonFileName); + this.tilesetSourceJsonFileNames.push(tilesetSourceJsonFileName); this.tilesetSourceIdentifiers.push(tilesetSourceIdentifier); } + this.tilesetTargetJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetTargetName); this.tilesetTarget = TilesetTargets.createAndBegin( tilesetTargetName, overwrite @@ -125,13 +133,18 @@ export class TilesetMerger { this.tilesetSources.length = 0; this.tilesetSourceIdentifiers.length = 0; this.tilesetTarget = undefined; + this.tilesetTargetJsonFileName = undefined; } /** * Internal method for `merge` */ private mergeInternal() { - if (this.tilesetSources.length == 0 || !this.tilesetTarget) { + if ( + this.tilesetSources.length == 0 || + !this.tilesetTarget || + !this.tilesetTargetJsonFileName + ) { throw new DeveloperError("The sources and target must be defined"); } @@ -140,10 +153,12 @@ export class TilesetMerger { const length = this.tilesetSources.length; for (let i = 0; i < length; ++i) { const tilesetSource = this.tilesetSources[i]; - const tilesetJsonFileName = this.tilesetJsonFileNames[i]; - const tilesetJsonBuffer = tilesetSource.getValue(tilesetJsonFileName); + const tilesetSourceJsonFileName = this.tilesetSourceJsonFileNames[i]; + const tilesetJsonBuffer = tilesetSource.getValue( + tilesetSourceJsonFileName + ); if (!tilesetJsonBuffer) { - const message = `No ${tilesetJsonFileName} found in input`; + const message = `No ${tilesetSourceJsonFileName} found in input`; throw new TilesetError(message); } const tileset = JSON.parse(tilesetJsonBuffer.toString()) as Tileset; @@ -156,7 +171,7 @@ export class TilesetMerger { const children = TilesetMerger.getChildren( tilesets, this.tilesetSourceIdentifiers, - this.tilesetJsonFileNames + this.tilesetSourceJsonFileNames ); const mergedTileset = { asset: { @@ -176,7 +191,10 @@ export class TilesetMerger { // Write the merged tileset into the target const mergedTilesetJson = JSON.stringify(mergedTileset, null, 2); const mergedTilesetBuffer = Buffer.from(mergedTilesetJson); - this.tilesetTarget.addEntry("tileset.json", mergedTilesetBuffer); + this.tilesetTarget.addEntry( + this.tilesetTargetJsonFileName, + mergedTilesetBuffer + ); // Copy the resources from the sources to the target this.copyResources(); diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetProcessor.ts new file mode 100644 index 00000000..d83ea602 --- /dev/null +++ b/src/tilesetProcessing/TilesetProcessor.ts @@ -0,0 +1,481 @@ +import { Buffers } from "../base/Buffers"; +import { DeveloperError } from "../base/DeveloperError"; + +import { BufferedContentData } from "../contentTypes/BufferedContentData"; +import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; + +import { Tileset } from "../structure/Tileset"; +import { Schema } from "../structure/Metadata/Schema"; + +import { TilesetError } from "../tilesetData/TilesetError"; +import { TilesetSource } from "../tilesetData/TilesetSource"; +import { TilesetTarget } from "../tilesetData/TilesetTarget"; +import { TilesetTargets } from "../tilesetData/TilesetTargets"; +import { TilesetSources } from "../tilesetData/TilesetSources"; +import { TilesetEntry } from "../tilesetData/TilesetEntry"; + +import { Tilesets } from "../tilesets/Tilesets"; + +import { TilesetEntryProcessor } from "./TilesetEntryProcessor"; + +/** + * A base class for classes that can process tilesets. + * + * This class offers a `process` method that receives a name + * of a `TilesetSource` and a `TilesetTarget`. It will open + * the source and the target, process all entries of the + * source, and put the results into the target. + * + * The abstract `processEntry` method is the main configuration + * point: It may be overridden by subclasses to process each + * entry as necessary. + */ +export abstract class TilesetProcessor { + /** + * A function that will receive log messages + */ + private readonly logCallback: (message: any) => void; + + /** + * The tileset source for the input + */ + private tilesetSource: TilesetSource | undefined; + + /** + * The tileset target for the output. + */ + private tilesetTarget: TilesetTarget | undefined; + + /** + * The set of keys (file names) that have already been processed. + * This includes the original keys, as well as new keys that + * have been assigned to entries while they have been processed. + */ + private processedKeys: { [key: string]: boolean } = {}; + + /** + * Creates a new instance + * + * @param quiet - Whether log messages should be omitted + */ + constructor(quiet?: boolean) { + if (quiet !== true) { + this.logCallback = (message: any) => console.log(message); + } else { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function + this.logCallback = (message: any) => {}; + } + } + + /** + * Returns the tileset source, or undefined when no source + * has been opened. + * + * @returns - The `TilesetSource` + */ + protected getTilesetSource(): TilesetSource | undefined { + return this.tilesetSource; + } + + /** + * Internal method to just call the log callback + * + * @param message - The message + */ + protected log(message: any): void { + this.logCallback(message); + } + + /** + * Process the specified source tileset, and write it to the given + * target. + * + * @param tilesetSourceName - The tileset source name + * @param tilesetTargetName - The tileset target name + * @param overwrite Whether the target should be overwritten if + * it already exists + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed, + * or when the output already exists and `overwrite` was `false`. + */ + async process( + tilesetSourceName: string, + tilesetTargetName: string, + overwrite: boolean + ): Promise { + // TODO Somehow ensure that the source is closed + // if the target throws up (try-with-resources FTW) + const tilesetSource = TilesetSources.createAndOpen(tilesetSourceName); + const tilesetTarget = TilesetTargets.createAndBegin( + tilesetTargetName, + overwrite + ); + + this.tilesetSource = tilesetSource; + this.tilesetTarget = tilesetTarget; + + const tilesetSourceJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetSourceName); + + const tilesetTargetJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetTargetName); + + await this.processInternal( + tilesetSourceJsonFileName, + tilesetTargetJsonFileName + ); + + tilesetSource.close(); + await tilesetTarget.end(); + + this.tilesetSource = undefined; + this.tilesetTarget = undefined; + Object.keys(this.processedKeys).forEach( + (key) => delete this.processedKeys[key] + ); + } + + /** + * Internal (top-level) method for the processing. + * + * It reads the tileset JSON from the specified source, passes + * it to `processTileset`, and writes the tileset JSON to the + * specified target. + * + * Any operations that affect files other than the tileset JSON + * file are part of `processTileset` + * + * @param tilesetSourceName - The tileset source name + * @param tilesetTargetName - The tileset target name + * @returns A promise that resolves when the process is finished + * @throws DeveloperError When the source or target is not opened + * @throws TilesetError When the input could not be processed + */ + private async processInternal( + tilesetSourceJsonFileName: string, + tilesetTargetJsonFileName: string + ): Promise { + if (!this.tilesetSource || !this.tilesetTarget) { + throw new DeveloperError("The source and target must be defined"); + } + + // Obtain the tileset object from the tileset JSON file + const parsedTileset = this.parseSourceValue( + tilesetSourceJsonFileName + ); + + // Resolve the schema, either from the `tileset.schema` + // or the `tileset.schemaUri` + const schema = this.resolveSchema(parsedTileset.result); + + // Process the actual tileset + await this.processTilesetInternal(parsedTileset.result, schema); + + // Store the resulting tileset as JSON + this.storeTargetValue( + tilesetTargetJsonFileName, + parsedTileset.wasZipped, + parsedTileset.result + ); + } + + /** + * Process the given tileset. + * + * This will just call `processEntries` and `processTileset`, + * where the latter serves as a point where implementors may + * perform modifications to the tileset JSON. + * + * @param tileset - The tileset + * @param schema - The optional metadata schema for the tileset + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + protected async processTilesetInternal( + tileset: Tileset, + schema: Schema | undefined + ): Promise { + await this.processEntries(); + await this.processTilesetJson(tileset, schema); + } + + /** + * Process the given tileset. + * + * Implementors may modify the given `Tileset`. The result + * will be written into the target, after all entries have + * been processed. + * + * @param tileset - The tileset + * @param schema - The optional metadata schema for the tileset + * @returns A promise that resolves when the process is finished + * @throws TilesetError When the input could not be processed + */ + protected async processTilesetJson( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + tileset: Tileset, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + schema: Schema | undefined + ): Promise { + // No-op + } + + /** + * Process all entries that are contained in the current + * tileset source. + * + * @returns A promise that resolves when the process is finished + * @throws DeveloperError When the source or target is not opened + * @throws TilesetError When the input could not be processed + */ + protected async processEntries(): Promise { + if (!this.tilesetSource || !this.tilesetTarget) { + throw new DeveloperError("The source and target must be defined"); + } + const entries = TilesetSources.getEntries(this.tilesetSource); + for (const entry of entries) { + const key = entry.key; + await this.processEntryInternal(key, this.processEntry); + } + } + + /** + * Process the specified entry. + * + * If the entry with the specified key was already processed, + * then this method does nothing. + * + * Otherwise, the specified entry will be looked up in the tileset + * source. Its content type will be determined. The source entry + * will be passed to `processEntry`, which returns the target + * entries that will be inserted into the tileset target. + * + * @param key - The key (file name) of the entry + * @param entryProcessor - The `TilesetEntryProcessor` that will + * be called to process the actual entry. + * @returns A promise that resolves when the process is finished, + * containing either the resulting entries + * @throws DeveloperError When the source or target is not opened + * @throws TilesetError When the input could not be processed + */ + protected async processEntryInternal( + key: string, + entryProcessor: TilesetEntryProcessor + ): Promise { + if (!this.tilesetSource || !this.tilesetTarget) { + throw new DeveloperError("The source and target must be defined"); + } + + const sourceKey = key; + if (this.isProcessed(sourceKey)) { + return []; + } + this.markAsProcessed(sourceKey); + + const sourceValue = this.tilesetSource.getValue(sourceKey); + if (!sourceValue) { + const message = `No ${sourceKey} found in input`; + throw new TilesetError(message); + } + const sourceEntry: TilesetEntry = { + key: sourceKey, + value: sourceValue, + }; + const type = await this.determineContentDataType(sourceKey, sourceValue); + + this.log(`Processing source : ${sourceKey} with type ${type}`); + + const targetEntries = await entryProcessor(sourceEntry, type); + + this.log(` to targets: ${targetEntries?.map((t) => t.key)}`); + + if (targetEntries) { + for (const targetEntry of targetEntries) { + this.tilesetTarget.addEntry(targetEntry.key, targetEntry.value); + this.markAsProcessed(targetEntry.key); + } + } + return targetEntries; + } + + /** + * Process a single entry. + * + * This is the main configuration point for this class: Implementors + * may override this method, perform arbitrary operations on the + * given entry, and return the result. + * + * (A no-op implementation of this method would be to just return an + * array that contains the given source entry as its only element) + * + * @param sourceEntry - The source entry + * @param type The content data type (see `ContentDataTypes`) + * @returns The target entries + */ + protected abstract processEntry( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise; + + /** + * A method that can be called by implementations, to mark a certain + * file as already having been processed, and no longer be considered + * in subsequent steps. + * + * @param key - The key (file name) + */ + protected markAsProcessed(key: string) { + this.processedKeys[key] = true; + } + + /** + * Returns whether the entry with the given key (file name) was + * already processed. + * + * @param key - The key (file name) + * @returns Whether the entry was already processed + */ + protected isProcessed(key: string) { + return this.processedKeys[key] === true; + } + + /** + * Determine the type of the given content data. + * + * The string will either be one of the `ContentDataTypes` strings, + * or `undefined` if the type cannot be determined. + * + * @param key - The key (file name) + * @param value - The value (file contents) + * @returns A promise with the content data type string + */ + private async determineContentDataType( + key: string, + value: Buffer + ): Promise { + const contentData = new BufferedContentData(key, value); + const type = await ContentDataTypeRegistry.findContentDataType(contentData); + return type; + } + + /** + * Parses the JSON from the value with the given key (file name), + * and returns the parsed result, AND information of whether the + * input was zipped. + * + * This is mainly a convenience function to emulate the behavior of the + * "legacy" tools in terms of handling the tileset JSON: When writing + * the tileset JSON data to the target, then it should zip that JSON + * data if and only if it was zipped in the input. + * + * See `storeTargetValue` for the counterpart of this method. + * + * In the future, there might be mechanisms for a more fine-grained + * control over whether certain files should be zipped or not... + * + * @param key - The key (file name) + * @returns A structure containing the `wasZipped` information, and + * the parsed result + * @throws TilesetError If the source is not opened, the specified + * entry cannot be found, or the entry data could not be unzipped, + * or its contents could not be parsed as JSON. + */ + private parseSourceValue(key: string): { wasZipped: boolean; result: T } { + let value = this.getSourceValue(key); + let wasZipped = false; + if (Buffers.isGzipped(value)) { + wasZipped = true; + try { + value = Buffers.gunzip(value); + } catch (e) { + const message = `Could not unzip ${key}: ${e}`; + throw new TilesetError(message); + } + } + try { + const result = JSON.parse(value.toString()) as T; + return { + wasZipped: wasZipped, + result: result, + }; + } catch (e) { + const message = `Could not parse ${key}: ${e}`; + throw new TilesetError(message); + } + } + + /** + * Convert the given object into a JSON string, put it into a buffer, + * zip it (based on the `doZip` flag), and put the result into the + * tileset target. + * + * This is only intended for the "legacy" handling of the tileset + * JSON data, and is the counterpart of `parseSourceValue`. See + * `parseSourceValue` for details. + * + * @param key - The key (file name) + * @param doZip - Whether the output should be zipped + * @param object - The object for which the JSON should be stored + * @throws DeveloperError When the target is not opened + */ + private storeTargetValue(key: string, doZip: boolean, object: object) { + if (!this.tilesetTarget) { + throw new DeveloperError("The target must be defined"); + } + const jsonString = JSON.stringify(object, null, 2); + let jsonBuffer = Buffer.from(jsonString); + if (doZip) { + jsonBuffer = Buffers.gzip(jsonBuffer); + } + this.tilesetTarget.addEntry(key, jsonBuffer); + } + + /** + * Obtains the value for the given key from the current tileset source, + * throwing an error if the source is not opened, or when the + * given key cannot be found. + * + * @param key - The key (file name) + * @returns The value (file contents) + * @throws DeveloperError When the source is not opened + * @throws TilesetError When the given key cannot be found + */ + private getSourceValue(key: string): Buffer { + if (!this.tilesetSource) { + throw new DeveloperError("The source must be defined"); + } + const buffer = this.tilesetSource.getValue(key); + if (!buffer) { + const message = `No ${key} found in input`; + throw new TilesetError(message); + } + return buffer; + } + + /** + * Resolve the `Schema` for the given tileset. + * + * This is either the `tileset.schema`, or the schema that is + * obtained from the `tileset.schemaUri`, or `undefined` if + * neither of them are present. + * + * @param tileset - The tileset + * @returns The `Schema`, or `undefined` if there is none + * @throws DeveloperError If the source is not opened + * @throws TilesetError If the schema from the `schemaUri` + * could not be resolved or parsed. + */ + private resolveSchema(tileset: Tileset): Schema | undefined { + if (!this.tilesetSource) { + throw new DeveloperError("The source must be defined"); + } + if (tileset.schema) { + return tileset.schema; + } + if (tileset.schemaUri) { + const parsedSchema = this.parseSourceValue(tileset.schemaUri); + return parsedSchema.result; + } + return undefined; + } +} From 25d051cc6164e8ce2fb0d473c12805facf680468 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 26 Mar 2023 15:52:52 +0200 Subject: [PATCH 24/60] Utility methods for extensions --- src/tilesets/Extensions.ts | 135 +++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 src/tilesets/Extensions.ts diff --git a/src/tilesets/Extensions.ts b/src/tilesets/Extensions.ts new file mode 100644 index 00000000..eaf27351 --- /dev/null +++ b/src/tilesets/Extensions.ts @@ -0,0 +1,135 @@ +import { RootProperty } from "../structure/RootProperty"; +import { Tileset } from "../structure/Tileset"; + +/** + * Utility methods for handling extensions + */ +export class Extensions { + /** + * Returns whether the given object contains the given extension. + * + * That is, whether the `object.extensions` contains a key + * that is the given extension name. + * + * @param rootProperty - The object that may contain the extension + * @param extension The extension (i.e. its name as a string) + * @returns Whether the object contains the extension + */ + static contains(rootProperty: RootProperty, extension: string) { + if (!rootProperty.extensions) { + return false; + } + return Object.keys(rootProperty.extensions).includes(extension); + } + + /** + * Add the given extension to the `extensionsUsed` of the given tileset. + * + * The extension will be added if it was not yet contained in the + * array, creating the array of necessary. + * + * @param tileset - The tileset + * @param extension - The extension name + */ + static addExtensionUsed(tileset: Tileset, extension: string) { + tileset.extensionsUsed = Extensions.addUnique( + tileset.extensionsUsed, + extension + ); + } + + /** + * Remove the given extension from the `extensionsUsed` of the given tileset. + * + * The array will be set to `undefined` if it becomes empty, and the + * extension will also be removed from `extensionsRequired`. + * + * @param tileset - The tileset + * @param extension - The extension name + */ + static removeExtensionUsed(tileset: Tileset, extension: string) { + tileset.extensionsUsed = Extensions.removeUnique( + tileset.extensionsUsed, + extension + ); + Extensions.removeExtensionRequired(tileset, extension); + } + + /** + * Add the given extension to the `extensionsRequired` of the given tileset. + * + * The extension will be added if it was not yet contained in the + * array, creating the array of necessary. This will also add + * the extension to `extensionsUsed`. + * + * @param tileset - The tileset + * @param extension - The extension name + */ + static addExtensionRequired(tileset: Tileset, extension: string) { + tileset.extensionsRequired = Extensions.addUnique( + tileset.extensionsRequired, + extension + ); + Extensions.addExtensionUsed(tileset, extension); + } + + /** + * Remove the given extension to the `extensionsUsed` of the given tileset. + * + * The array will be set to `undefined` if it becomes empty. + * + * This will *not* remove the extension from the `extensionsUsed`! + * + * @param tileset - The tileset + * @param extension - The extension name + */ + static removeExtensionRequired(tileset: Tileset, extension: string) { + tileset.extensionsRequired = Extensions.removeUnique( + tileset.extensionsRequired, + extension + ); + } + + /** + * Adds the given element to the given array and returns the + * array, creating a new array if the array was `undefined`. + * + * @param array - The array + * @param element - The element + * @returns The new array + */ + private static addUnique(array: T[] | undefined, element: T): T[] { + if (!array) { + array = []; + } + if (!array.includes(element)) { + array.push(element); + } + return array; + } + + /** + * Remove the given element from the given array and returns the + * array. If the array becomes empty, this method returns `undefined`. + * + * @param array - The array + * @param element - The element + * @returns The new array + */ + private static removeUnique( + array: T[] | undefined, + element: T + ): T[] | undefined { + if (!array) { + return undefined; + } + const index = array.indexOf(element); + if (index !== -1) { + array.splice(index, 1); + } + if (array.length === 0) { + return undefined; + } + return array; + } +} From 5857a59c63e15cc48e06debe418d6e3e930e94c4 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 26 Mar 2023 16:01:16 +0200 Subject: [PATCH 25/60] Update extension declarations in TilesetUpgrader --- specs/TilesetUpgraderSpec.ts | 18 ++++++++++++++++++ src/tilesetProcessing/TilesetUpgrader.ts | 9 ++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/specs/TilesetUpgraderSpec.ts b/specs/TilesetUpgraderSpec.ts index 420ba627..91baa252 100644 --- a/specs/TilesetUpgraderSpec.ts +++ b/specs/TilesetUpgraderSpec.ts @@ -20,10 +20,18 @@ const unitBoundingBox = { // - The `refine` value must be given in uppercase // - The `content.url` must be a `content.uri` // (checked for both `content` and `contents`) +// - The `extensionsUsed` and `extensionsRequired` should no +// longer contain `3DTILES_content_gltf` const inputTilesetJsonRaw: unknown = { asset: { version: "0.0", }, + extensionsUsed: [ + "3DTILES_content_gltf", + "EXAMPLE_extension_A", + "EXAMPLE_extension_B", + ], + extensionsRequired: ["3DTILES_content_gltf", "EXAMPLE_extension_A"], geometricError: 2.0, root: { boundingVolume: unitBoundingBox, @@ -85,4 +93,14 @@ describe("TilesetUpgrader", function () { expect(tileset.root.children![0].refine).toBe("ADD"); expect(tileset.root.children![1].refine).toBeUndefined(); }); + + it("removes the unnecessary extension declaration for 3DTILES_content_gltf", async function () { + const tilesetUpgrader = new TilesetUpgrader(quiet); + + const tileset = JSON.parse(inputTilesetJsonString) as Tileset; + await tilesetUpgrader.upgradeTileset(tileset); + + expect(tileset.extensionsUsed).not.toContain("3DTILES_content_gltf"); + expect(tileset.extensionsRequired).not.toContain("3DTILES_content_gltf"); + }); }); diff --git a/src/tilesetProcessing/TilesetUpgrader.ts b/src/tilesetProcessing/TilesetUpgrader.ts index eb7c1309..feae6184 100644 --- a/src/tilesetProcessing/TilesetUpgrader.ts +++ b/src/tilesetProcessing/TilesetUpgrader.ts @@ -20,6 +20,7 @@ import { Tilesets } from "../tilesets/Tilesets"; import { TileFormats } from "../tileFormats/TileFormats"; import { GltfUtilities } from "../contentProcessing/GtlfUtilities"; +import { Extensions } from "../tilesets/Extensions"; /** * The options for the upgrade. This is only used internally, @@ -35,6 +36,7 @@ type UpgradeOptions = { upgradeContentUrlToUri: boolean; upgradeB3dmGltf1ToGltf2: boolean; upgradeI3dmGltf1ToGltf2: boolean; + upgradeExtensionDeclarations: boolean; }; /** @@ -83,6 +85,7 @@ export class TilesetUpgrader { upgradeContentUrlToUri: true, upgradeB3dmGltf1ToGltf2: true, upgradeI3dmGltf1ToGltf2: true, + upgradeExtensionDeclarations: true, }; } @@ -191,7 +194,7 @@ export class TilesetUpgrader { async upgradeTileset(tileset: Tileset): Promise { if (this.upgradeOptions.upgradeAssetVersionNumber) { this.logCallback(`Upgrading asset version number`); - await this.upgradeAssetVersionNumber(tileset); + this.upgradeAssetVersionNumber(tileset); } if (this.upgradeOptions.upgradeRefineCase) { this.logCallback(`Upgrading refine to be in uppercase`); @@ -201,6 +204,10 @@ export class TilesetUpgrader { this.logCallback(`Upgrading content.url to content.uri`); await this.upgradeEachContentUrlToUri(tileset); } + if (this.upgradeOptions.upgradeExtensionDeclarations) { + this.logCallback(`Upgrading extension declarations`); + Extensions.removeExtensionUsed(tileset, "3DTILES_content_gltf"); + } } /** From f5759d220b19497ce44db0ff7b89fccd19e7afea Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 29 Mar 2023 14:40:52 +0200 Subject: [PATCH 26/60] Fix default method argument --- src/base/Iterables.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/base/Iterables.ts b/src/base/Iterables.ts index 5c2e09ac..bd6ddd13 100644 --- a/src/base/Iterables.ts +++ b/src/base/Iterables.ts @@ -15,14 +15,13 @@ export class Iterables { * `recurse` is `true`. * * @param directory - The directory - * @param recurse - [true] Whether the files should + * @param recurse - Whether the files should * be listed recursively * @returns The generator for path strings */ static *overFiles( directory: string | PathLike, - // eslint-disable-next-line @typescript-eslint/no-inferrable-types - recurse: boolean = true + recurse: boolean ): IterableIterator { const fileNames = fs.readdirSync(directory); for (const fileName of fileNames) { From d6c83f1926585850be54bd9767caec5fa35a013d Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 29 Mar 2023 14:41:31 +0200 Subject: [PATCH 27/60] Update for renamed method in base branch Building, linting and running it several times did not catch this.... --- src/ToolsMain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ToolsMain.ts b/src/ToolsMain.ts index d34b75ae..e63f530e 100644 --- a/src/ToolsMain.ts +++ b/src/ToolsMain.ts @@ -177,7 +177,7 @@ export class ToolsMain { }; // Read the buffer and its magic header - const magic = Buffers.getMagic(inputBuffer, 0); + const magic = Buffers.getMagicString(inputBuffer, 0); if (magic === "b3dm" || magic === "i3dm" || magic === "pnts") { // Handle the basic legacy tile formats From dc76982f60f6ced18527d6f59963ca75926c12da Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 29 Mar 2023 19:10:47 +0200 Subject: [PATCH 28/60] Refactor TilesetTraverser Converted static traversal method into instance method on TilesetTraverser. Very basic support for traversing tileset packages. --- demos/TilesetContentProcessorDrafts.ts | 5 +- demos/TraversalDemo.ts | 20 +- demos/TraversalStatsDemo.ts | 117 ++++---- specs/LazyContentDataSpec.ts | 4 - src/io/FileResourceResolver.ts | 3 +- src/io/ResourceResolver.ts | 9 - src/io/TilesetSourceResourceResolver.ts | 8 - src/io/UnzippingResourceResolver.ts | 5 - .../TilesetContentProcessor.ts | 13 +- src/tilesetProcessing/TilesetUpgrader.ts | 7 +- src/traversal/ExplicitTraversedTile.ts | 44 ++- src/traversal/ImplicitTraversedTile.ts | 13 +- src/traversal/TilesetTraverser.ts | 269 ++++++++---------- src/traversal/TilesetTraversers.ts | 223 +++++++++++++++ src/traversal/TraversedTile.ts | 16 +- 15 files changed, 483 insertions(+), 273 deletions(-) create mode 100644 src/traversal/TilesetTraversers.ts diff --git a/demos/TilesetContentProcessorDrafts.ts b/demos/TilesetContentProcessorDrafts.ts index 9ea553f4..79554871 100644 --- a/demos/TilesetContentProcessorDrafts.ts +++ b/demos/TilesetContentProcessorDrafts.ts @@ -1,6 +1,7 @@ import { Paths } from "../src/base/Paths"; import { ContentOps } from "../src/contentProcessing/ContentOps"; import { GltfUtilities } from "../src/contentProcessing/GtlfUtilities"; +import { ContentDataTypes } from "../src/contentTypes/ContentDataTypes"; import { Content } from "../src/structure/Content"; import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; @@ -20,7 +21,7 @@ async function runB3dmToGlbTest() { sourceEntry: TilesetEntry, type: string | undefined ): Promise { - if (type !== "CONTENT_TYPE_B3DM") { + if (type !== ContentDataTypes.CONTENT_TYPE_B3DM) { return [sourceEntry]; } const targetEntry = { @@ -54,7 +55,7 @@ async function runOptimizeTest() { sourceEntry: TilesetEntry, type: string | undefined ): Promise { - if (type !== "CONTENT_TYPE_GLB") { + if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { return [sourceEntry]; } const targetEntry = { diff --git a/demos/TraversalDemo.ts b/demos/TraversalDemo.ts index c3b70dc5..86d76ce6 100644 --- a/demos/TraversalDemo.ts +++ b/demos/TraversalDemo.ts @@ -4,6 +4,7 @@ import { readJsonUnchecked } from "./readJsonUnchecked"; import { ResourceResolvers } from "../src/io/ResourceResolvers"; import { TilesetTraverser } from "../src/traversal/TilesetTraverser"; +import { TraversedTile } from "../src/traversal/TraversedTile"; async function tilesetTraversalDemo(filePath: string) { console.log(`Traversing tileset ${filePath}`); @@ -12,15 +13,14 @@ async function tilesetTraversalDemo(filePath: string) { const resourceResolver = ResourceResolvers.createFileResourceResolver(directory); const tileset = await readJsonUnchecked(filePath); - // Note: External schemas are not considered here - const schema = tileset.schema; - const depthFirst = false; - const traverseExternalTilesets = true; - await TilesetTraverser.traverse( + + const tilesetTraverser = new TilesetTraverser(directory, resourceResolver, { + depthFirst: false, + traverseExternalTilesets: true, + }); + await tilesetTraverser.traverse( tileset, - schema, - resourceResolver, - async (traversedTile) => { + async (traversedTile: TraversedTile) => { const contentUris = traversedTile.getFinalContents().map((c) => c.uri); const geometricError = traversedTile.asFinalTile().geometricError; console.log( @@ -30,9 +30,7 @@ async function tilesetTraversalDemo(filePath: string) { `geometricError ${geometricError}` ); return true; - }, - depthFirst, - traverseExternalTilesets + } ); console.log("Traversing tileset DONE"); } diff --git a/demos/TraversalStatsDemo.ts b/demos/TraversalStatsDemo.ts index 4beca888..d3774e2a 100644 --- a/demos/TraversalStatsDemo.ts +++ b/demos/TraversalStatsDemo.ts @@ -7,42 +7,88 @@ import { ResourceResolvers } from "../src/io/ResourceResolvers"; import { TilesetTraverser } from "../src/traversal/TilesetTraverser"; import { TraversedTile } from "../src/traversal/TraversedTile"; +import { BufferedContentData } from "../src/contentTypes/BufferedContentData"; +import { ContentDataTypeRegistry } from "../src/contentTypes/ContentDataTypeRegistry"; +import { ContentDataTypeChecks } from "../src/contentTypes/ContentDataTypeChecks"; +import { ContentDataTypes } from "../src/contentTypes/ContentDataTypes"; // 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) { + const statsCollector = new StatsCollector(); + + // Create a check that determines whether content + // data should count as "tile content" + const isTileFileContent = ContentDataTypeChecks.createIncludedCheck( + ContentDataTypes.CONTENT_TYPE_GLB, + ContentDataTypes.CONTENT_TYPE_B3DM, + ContentDataTypes.CONTENT_TYPE_I3DM, + ContentDataTypes.CONTENT_TYPE_CMPT, + ContentDataTypes.CONTENT_TYPE_PNTS, + ContentDataTypes.CONTENT_TYPE_GEOM, + ContentDataTypes.CONTENT_TYPE_VCTR, + ContentDataTypes.CONTENT_TYPE_GEOJSON, + ContentDataTypes.CONTENT_TYPE_GLTF + ); + + // A `TraversalCallback` that will be passed to the + // tileset traverser, and store information about the + // traversed tiles in the `StatsCollector` + const statsTraversalCallback = async (traversedTile: TraversedTile) => { + statsCollector.increment("totalNumberOfTiles"); + const subtreeUri = traversedTile.getSubtreeUri(); + if (subtreeUri !== undefined) { + statsCollector.increment("totalNumberOfSubtres"); + } + if (!traversedTile.isImplicitTilesetRoot()) { + // Obtain all content URIs, resolve the associated data, + // and store the size in the "tileFileSize" summary if + // the data is one of the known tile content types + const contentUris = traversedTile.getFinalContents().map((c) => c.uri); + const tileResourceResolver = traversedTile.getResourceResolver(); + for (const contentUri of contentUris) { + const data = await tileResourceResolver.resolveData(contentUri); + const contentData = new BufferedContentData(contentUri, data); + const isTileFile = await isTileFileContent(contentData); + if (isTileFile) { + if (data) { + statsCollector.acceptEntry("tileFileSize", data.length); + } else { + statsCollector.increment("unresolvableContents"); + } + } + } + } + + // Store the geometric error in the "geometricError" summary + const finalTile = traversedTile.asFinalTile(); + const geometricError = finalTile.geometricError; + statsCollector.acceptEntry("geometricError", geometricError); + return true; + }; + // 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 + // Create the TilesetTraverser and traverse the tileset, + // passing each tile to the callback that stores the + // information in the StatsCollector console.log("Traversing tileset"); - const tilesetStatsCollector = new TilesetStatsCollector(); - const depthFirst = false; - const traverseExternalTilesets = false; - await TilesetTraverser.traverse( - tileset, - schema, - resourceResolver, - async (traversedTile) => { - tilesetStatsCollector.accept(traversedTile); - return true; - }, - depthFirst, - traverseExternalTilesets - ); + const tilesetTraverser = new TilesetTraverser(directory, resourceResolver, { + depthFirst: false, + traverseExternalTilesets: true, + }); + await tilesetTraverser.traverse(tileset, statsTraversalCallback); console.log("Traversing tileset DONE"); // Print the statistics summary to the console console.log("Stats:"); - const json = tilesetStatsCollector.createJson(); + const json = statsCollector.createJson(); const jsonString = JSON.stringify(json, null, 2); console.log(jsonString); } @@ -176,39 +222,6 @@ class Summary { } } -// 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"); - - const subtreeUri = traversedTile.getSubtreeUri(); - if (subtreeUri !== undefined) { - this.increment("totalNumberOfSubtres"); - } - if (!traversedTile.isImplicitTilesetRoot()) { - // 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"; diff --git a/specs/LazyContentDataSpec.ts b/specs/LazyContentDataSpec.ts index 5d45ae29..fc80b228 100644 --- a/specs/LazyContentDataSpec.ts +++ b/specs/LazyContentDataSpec.ts @@ -9,10 +9,6 @@ class SpecResourceResolver implements ResourceResolver { this.dataMap[uri] = buffer; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - resolveUri(uri: string): string { - throw new DeveloperError("Not supposed to be called."); - } async resolveData(uri: string): Promise { const data = this.dataMap[uri] as Buffer; if (!data) { diff --git a/src/io/FileResourceResolver.ts b/src/io/FileResourceResolver.ts index 6211ed1d..5f4b473e 100644 --- a/src/io/FileResourceResolver.ts +++ b/src/io/FileResourceResolver.ts @@ -17,8 +17,7 @@ export class FileResourceResolver implements ResourceResolver { this._basePath = basePath; } - /** {@inheritDoc ResourceResolver.resolveUri} */ - resolveUri(uri: string): string { + private resolveUri(uri: string): string { let resolved = path.resolve(this._basePath, decodeURIComponent(uri)); resolved = resolved.replace(/\\/g, "/"); return resolved; diff --git a/src/io/ResourceResolver.ts b/src/io/ResourceResolver.ts index 96817451..26d787c1 100644 --- a/src/io/ResourceResolver.ts +++ b/src/io/ResourceResolver.ts @@ -5,15 +5,6 @@ * @internal */ export interface ResourceResolver { - /** - * Returns the URI that results from resolving the given - * URI against the base URI of this resource resolver. - * - * @param uri - The URI - * @returns The resolved URI - */ - resolveUri(uri: string): string; - /** * Resolve the data from the given URI. * diff --git a/src/io/TilesetSourceResourceResolver.ts b/src/io/TilesetSourceResourceResolver.ts index 4aa44548..79804735 100644 --- a/src/io/TilesetSourceResourceResolver.ts +++ b/src/io/TilesetSourceResourceResolver.ts @@ -1,5 +1,3 @@ -import path from "path"; - import { Paths } from "../base/Paths"; import { Uris } from "../base/Uris"; @@ -21,12 +19,6 @@ export class TilesetSourceResourceResolver implements ResourceResolver { this._tilesetSource = tilesetSource; } - /** {@inheritDoc ResourceResolver.resolveUri} */ - resolveUri(uri: string): string { - const resolved = path.resolve(this._basePath, decodeURIComponent(uri)); - return resolved; - } - /** {@inheritDoc ResourceResolver.resolveData} */ async resolveData(uri: string): Promise { if (Uris.isDataUri(uri)) { diff --git a/src/io/UnzippingResourceResolver.ts b/src/io/UnzippingResourceResolver.ts index 2676d28b..9b3eb6da 100644 --- a/src/io/UnzippingResourceResolver.ts +++ b/src/io/UnzippingResourceResolver.ts @@ -17,11 +17,6 @@ export class UnzippingResourceResolver implements ResourceResolver { this._delegate = delegate; } - /** {@inheritDoc ResourceResolver.resolveUri} */ - resolveUri(uri: string): string { - return this._delegate.resolveUri(uri); - } - /** {@inheritDoc ResourceResolver.resolveData} */ async resolveData(uri: string): Promise { const delegateData = await this._delegate.resolveData(uri); diff --git a/src/tilesetProcessing/TilesetContentProcessor.ts b/src/tilesetProcessing/TilesetContentProcessor.ts index 4d1733e8..377b31b1 100644 --- a/src/tilesetProcessing/TilesetContentProcessor.ts +++ b/src/tilesetProcessing/TilesetContentProcessor.ts @@ -126,12 +126,13 @@ export abstract class TilesetContentProcessor extends TilesetProcessor { ".", tilesetSource ); - const depthFirst = false; - const traverseExternalTilesets = false; - await TilesetTraverser.traverse( + const tilesetTraverser = new TilesetTraverser(".", resourceResolver, { + depthFirst: false, + traverseExternalTilesets: true, + }); + await tilesetTraverser.traverseWithSchema( tileset, schema, - resourceResolver, async (traversedTile) => { if (!traversedTile.isImplicitTilesetRoot()) { const contentUris = traversedTile @@ -145,9 +146,7 @@ export abstract class TilesetContentProcessor extends TilesetProcessor { } } return true; - }, - depthFirst, - traverseExternalTilesets + } ); } diff --git a/src/tilesetProcessing/TilesetUpgrader.ts b/src/tilesetProcessing/TilesetUpgrader.ts index feae6184..8f5de525 100644 --- a/src/tilesetProcessing/TilesetUpgrader.ts +++ b/src/tilesetProcessing/TilesetUpgrader.ts @@ -3,6 +3,7 @@ import { DeveloperError } from "../base/DeveloperError"; import { BufferedContentData } from "../contentTypes/BufferedContentData"; import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; +import { ContentDataTypes } from "../contentTypes/ContentDataTypes"; import { Tile } from "../structure/Tile"; import { Tileset } from "../structure/Tileset"; @@ -16,11 +17,11 @@ import { TilesetSources } from "../tilesetData/TilesetSources"; import { Tiles } from "../tilesets/Tiles"; import { Tilesets } from "../tilesets/Tilesets"; +import { Extensions } from "../tilesets/Extensions"; import { TileFormats } from "../tileFormats/TileFormats"; import { GltfUtilities } from "../contentProcessing/GtlfUtilities"; -import { Extensions } from "../tilesets/Extensions"; /** * The options for the upgrade. This is only used internally, @@ -343,14 +344,14 @@ export class TilesetUpgrader { value: Buffer, type: string | undefined ): Promise { - if (type === "CONTENT_TYPE_B3DM") { + if (type === ContentDataTypes.CONTENT_TYPE_B3DM) { if (this.upgradeOptions.upgradeB3dmGltf1ToGltf2) { this.logCallback(` Upgrading GLB in ${key}`); value = await TilesetUpgrader.upgradeB3dmGltf1ToGltf2(value); } else { this.logCallback(` Not upgrading GLB in ${key} (disabled via option)`); } - } else if (type === "CONTENT_TYPE_I3DM") { + } else if (type === ContentDataTypes.CONTENT_TYPE_I3DM) { if (this.upgradeOptions.upgradeI3dmGltf1ToGltf2) { this.logCallback(` Upgrading GLB in ${key}`); value = await TilesetUpgrader.upgradeI3dmGltf1ToGltf2(value); diff --git a/src/traversal/ExplicitTraversedTile.ts b/src/traversal/ExplicitTraversedTile.ts index fbc87180..06742d5f 100644 --- a/src/traversal/ExplicitTraversedTile.ts +++ b/src/traversal/ExplicitTraversedTile.ts @@ -57,6 +57,44 @@ export class ExplicitTraversedTile implements TraversedTile { */ private readonly _resourceResolver; + /** + * Convenience function to create the root tile for a tile + * traversal. + * + * @param root - The root tile from the tileset + * @param schema - The optional metadata schema + * @param resourceResolver - The `ResourceResolver` for + * external references (like subtree files) + * @returns The root `TraversedTile` + */ + static createRoot( + root: Tile, + schema: Schema | undefined, + resourceResolver: ResourceResolver + ): TraversedTile { + const traversedRoot = new ExplicitTraversedTile( + root, + "/root", + 0, + undefined, + schema, + resourceResolver + ); + return traversedRoot; + } + + /** + * Creates a new instance + * + * @param tile - The `Tile` from the tileset JSON + * @param path - A JSON-path-like string describing this tile + * @param level - The level, referring to the root of the + * traversal, starting at 0 + * @param parent The optional parent tile + * @param schema The optional metadata schema + * @param resourceResolver - The `ResourceResolver` for + * external references (like subtree files) + */ constructor( tile: Tile, path: string, @@ -220,9 +258,9 @@ export class ExplicitTraversedTile implements TraversedTile { return finalContents; } - /** {@inheritDoc TraversedTile.resolveUri} */ - resolveUri(uri: string): string { - return this._resourceResolver.resolveUri(uri); + /** {@inheritDoc TraversedTile.getResourceResolver} */ + getResourceResolver(): ResourceResolver { + return this._resourceResolver; } /** {@inheritDoc TraversedTile.isImplicitTilesetRoot} */ diff --git a/src/traversal/ImplicitTraversedTile.ts b/src/traversal/ImplicitTraversedTile.ts index 5d261a93..496eddfc 100644 --- a/src/traversal/ImplicitTraversedTile.ts +++ b/src/traversal/ImplicitTraversedTile.ts @@ -329,10 +329,9 @@ export class ImplicitTraversedTile implements TraversedTile { const contents = []; const subtreeInfo = this._subtreeModel.subtreeInfo; const contentAvailabilityInfos = subtreeInfo.contentAvailabilityInfos; + const tileIndex = this._localCoordinate.toIndex(); for (const contentAvailabilityInfo of contentAvailabilityInfos) { - const available = contentAvailabilityInfo.isAvailable( - this._localCoordinate.toIndex() - ); + const available = contentAvailabilityInfo.isAvailable(tileIndex); if (available) { // TODO The existence of the root content URI should // have been validated. So this could also throw @@ -364,9 +363,9 @@ export class ImplicitTraversedTile implements TraversedTile { if (!subtreeMetadataModel) { return contents; } + const tileIndex = this._localCoordinate.toIndex(); for (let i = 0; i < contents.length; i++) { const content = contents[i]; - const tileIndex = this._localCoordinate.toIndex(); MetadataSemanticOverrides.applyImplicitContentMetadataSemanticOverrides( content, i, @@ -377,9 +376,9 @@ export class ImplicitTraversedTile implements TraversedTile { return contents; } - /** {@inheritDoc TraversedTile.resolveUri} */ - resolveUri(uri: string): string { - return this._resourceResolver.resolveUri(uri); + /** {@inheritDoc TraversedTile.getResourceResolver} */ + getResourceResolver(): ResourceResolver { + return this._resourceResolver; } /** {@inheritDoc TraversedTile.isImplicitTilesetRoot} */ diff --git a/src/traversal/TilesetTraverser.ts b/src/traversal/TilesetTraverser.ts index fab35a10..a2167092 100644 --- a/src/traversal/TilesetTraverser.ts +++ b/src/traversal/TilesetTraverser.ts @@ -6,12 +6,25 @@ import { Schema } from "../structure/Metadata/Schema"; import { TraversedTile } from "./TraversedTile"; import { ExplicitTraversedTile } from "./ExplicitTraversedTile"; import { TraversalCallback } from "./TraversalCallback"; +import { TilesetTraversers } from "./TilesetTraversers"; import { DeveloperError } from "../base/DeveloperError"; -import { DataError } from "../base/DataError"; -import { LazyContentData } from "../contentTypes/LazyContentData"; -import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; +/** + * A collection of configuration options for the traversal. + */ +export type TraversalOptions = { + /** + * Whether the traversal should be depth-first (in contrast + * to the default breadth-first order) + */ + depthFirst?: boolean; + + /** + * Whether external tilesets should be traversed + */ + traverseExternalTilesets?: boolean; +}; /** * A class that can traverse the tiles of a tileset. @@ -19,6 +32,53 @@ import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry * @internal */ export class TilesetTraverser { + /** + * The base URI against which content URIs are resolved + * when they refer to 3D Tiles Packages. + * (The current implementations of 3D Tiles Package based + * `TilesetSource`, specifically `TilesetSource3tz`, + * require an absolute URI) + */ + private readonly baseUri: string; + + /** + * The `ResourceResolver` that is used to resolve resources like + * external metadata schema files, subtree files for implicit + * tilesets, or external tilesets. + */ + private readonly resourceResolver: ResourceResolver; + + /** + * The `TraversalOptions` + */ + private readonly options: TraversalOptions; + + /** + * Creates a new instance. + * + * NOTE: The exact set of traversal options is not yet specified. + * + * @param baseUri - The URI against which content URI are resolved + * in order to obtain an absolute URI. This is only used for traversing + * package (3TZ or 3DTILES) content + * @param resourceResolver - The `ResourceResolver` that is used to + * resolve resources like external metadata schema files, subtree + * files for implicit tilesets, or external tilesets. + * @param options Options for the traveral process. + */ + constructor( + baseUri: string, + resourceResolver: ResourceResolver, + options?: TraversalOptions + ) { + this.baseUri = baseUri; + this.resourceResolver = resourceResolver; + this.options = { + depthFirst: options?.depthFirst === true, + traverseExternalTilesets: options?.traverseExternalTilesets === true, + }; + } + /** * Traverses the tiles in the given tileset. * @@ -27,39 +87,51 @@ export class TilesetTraverser { * as `TraversedTile` instances. * * @param tileset - The `Tileset` + * @param traversalCallback - The `TraversalCallback` + * @returns A Promise that resolves when the traversal finished + */ + async traverse( + tileset: Tileset, + traversalCallback: TraversalCallback + ): Promise { + const schema = await TilesetTraversers.resolveSchema( + tileset, + this.resourceResolver + ); + return this.traverseWithSchema(tileset, schema, traversalCallback); + } + + /** + * Traverses the tiles in the given tileset. + * + * This is only the implementation of `traverse`, with the + * option to pass in a `Schema` object that already has + * been resolved. + * + * @param tileset - The `Tileset` * @param schema - The schema from the `tileset.schema` or the - * `tileset.schemaUri`. If this is defined, then it is assumed - * to be a valid schema definition. - * @param resourceResolver - The `ResourceResolver` that is used to - * resolve resources for implicit tilesets (subtree files) or - * external tilesets. + * `tileset.schemaUri`, or `undefined` if the tileset does + * not have an associated schema. * @param traversalCallback - The `TraversalCallback` - * @param depthFirst - Whether the traversal should be depth-first - * @param traverseExternalTilesets - Whether external tileset should be - * traversed * @returns A Promise that resolves when the traversal finished */ - static async traverse( + async traverseWithSchema( tileset: Tileset, schema: Schema | undefined, - resourceResolver: ResourceResolver, - traversalCallback: TraversalCallback, - depthFirst: boolean, - traverseExternalTilesets: boolean + traversalCallback: TraversalCallback ): Promise { const root = tileset.root; if (!root) { return; } + const depthFirst = this.options.depthFirst; + const stack: TraversedTile[] = []; - const traversedRoot = new ExplicitTraversedTile( + const traversedRoot = ExplicitTraversedTile.createRoot( root, - "/root", - 0, - undefined, schema, - resourceResolver + this.resourceResolver ); stack.push(traversedRoot); @@ -72,147 +144,44 @@ export class TilesetTraverser { const traverseChildren = await traversalCallback(traversedTile); if (traverseChildren) { - const children = await traversedTile.getChildren(); - const length = children.length; - - if (length !== 0) { - // When there are children, traverse them directly - for (let i = 0; i < length; i++) { - const traversedChild = children[i]; - stack.push(traversedChild); - } - } else if (traverseExternalTilesets) { - // When there are no children, but external tilesets should - // be traversed, determine the roots of external tilesets - // and put them on the traversal stack - const externalRoots = - await TilesetTraverser.createExternalTilesetRoots( - traversedTile, - resourceResolver - ); - stack.push(...externalRoots); - } + const children = await this.createChildren(traversedTile); + stack.push(...children); } } } /** - * Create the nodes that are the roots of external tilesets - * that are referred to by the given traversed tile. + * Create the children for the traversal for the given tile. * - * If the given tile does not have any contents or none of - * them refers to a tileset, then an empty array is returned. + * If the given `TraversedTile` has children, then they will + * be returned. + * Otherwise, if `options.traverseExternalTilesets` was set, + * then this will be the roots of external tilesets. + * Otherwise, it will be the empty array. * * @param traversedTile - The `TraversedTile` - * @param resourceResolver The `ResourceResolver` for the - * external tileset JSON and related files - * @returns The external tileset roots - * @throws DataError If one of the externa tilesets or - * its associated files could not be resolved. + * @returns The children */ - private static async createExternalTilesetRoots( - traversedTile: TraversedTile, - resourceResolver: ResourceResolver + private async createChildren( + traversedTile: TraversedTile ): Promise { - if (traversedTile.isImplicitTilesetRoot()) { - return []; - } - const contents = traversedTile.getRawContents(); - if (contents.length === 0) { - return []; - } - const externalRoots: TraversedTile[] = []; - for (const content of contents) { - const contentUri = content.uri; - - // Try to obtain an external tileset from the content - const externalTileset = await TilesetTraverser.resolveExternalTileset( - contentUri, - resourceResolver - ); + const traverseExternalTilesets = this.options.traverseExternalTilesets; + const children = await traversedTile.getChildren(); + const length = children.length; - if (externalTileset) { - // If an external tileset was found, derive a resource resolver - // for its base directory, obtain its metadata schema, and - // create an explicit traversed tile for its root. - const derivedResourceResolver = resourceResolver.derive(contentUri); - const externalSchema = await TilesetTraverser.resolveSchema( - externalTileset, - derivedResourceResolver - ); - const externalRoot = new ExplicitTraversedTile( - externalTileset.root, - traversedTile.path + `/[external:${contentUri}]/root`, - traversedTile.level + 1, - traversedTile, - externalSchema, - derivedResourceResolver - ); - externalRoots.push(externalRoot); - } + if (length !== 0) { + return children; } - return externalRoots; - } - - /** - * Fetch the external tileset from the given URI. If the given - * URI does not refer to an external tileset, then `undefined` - * is returned. - * - * @param uri - The URI - * @param resourceResolver - The `ResourceResolver` - * @returns The tileset - */ - private static async resolveExternalTileset( - uri: string, - resourceResolver: ResourceResolver - ): Promise { - const contentData = new LazyContentData(uri, resourceResolver); - const contentDataType = await ContentDataTypeRegistry.findContentDataType( - contentData - ); - const isTileset = contentDataType === "CONTENT_TYPE_TILESET"; - if (isTileset) { - const externalTileset = await contentData.getParsedObject(); - return externalTileset; - } - return undefined; - } - - /** - * Resolve the `Schema` for the given tileset. - * - * This is either the `tileset.schema`, or the schema that is - * obtained from the `tileset.schemaUri`, or `undefined` if - * neither of them are present. - * - * @param tileset - The tileset - * @param resourceResolver - The `ResourceResolver` for loading - * the schema from the `schemaUri` if necessary - * @returns The `Schema`, or `undefined` if there is none - * @throws DataError If the schema from the `schemaUri` - * could not be resolved or parsed. - */ - private static async resolveSchema( - tileset: Tileset, - resourceResolver: ResourceResolver - ): Promise { - if (tileset.schema) { - return tileset.schema; - } - if (tileset.schemaUri) { - const uri = tileset.schemaUri; - const schemaData = await resourceResolver.resolveData(uri); - if (!schemaData) { - throw new DataError(`Could not resolve ${uri}`); - } - try { - const schema = JSON.parse(schemaData.toString("utf-8")); - return schema; - } catch (e) { - throw new DataError(`Could not parse schema from ${uri}`); - } + if (traverseExternalTilesets) { + // When there are no children, but external tilesets should + // be traversed, determine the roots of external tilesets + // and put them on the traversal stack + const externalRoots = await TilesetTraversers.createExternalTilesetRoots( + this.baseUri, + traversedTile + ); + return externalRoots; } - return undefined; + return []; } } diff --git a/src/traversal/TilesetTraversers.ts b/src/traversal/TilesetTraversers.ts new file mode 100644 index 00000000..e1d9e207 --- /dev/null +++ b/src/traversal/TilesetTraversers.ts @@ -0,0 +1,223 @@ +import path from "path"; + +import { ResourceResolver } from "../io/ResourceResolver"; +import { TilesetSourceResourceResolver } from "../io/TilesetSourceResourceResolver"; + +import { Tileset } from "../structure/Tileset"; +import { Schema } from "../structure/Metadata/Schema"; + +import { TraversedTile } from "./TraversedTile"; +import { ExplicitTraversedTile } from "./ExplicitTraversedTile"; + +import { DataError } from "../base/DataError"; + +import { LazyContentData } from "../contentTypes/LazyContentData"; +import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; +import { ContentDataTypes } from "../contentTypes/ContentDataTypes"; + +import { TilesetSource3tz } from "../packages/TilesetSource3tz"; +import { Paths } from "../base/Paths"; + +/** + * Internal utility methods for tileset traversal, used for + * the `TilesetTraverser` implementation. + * + * @internal + */ +export class TilesetTraversers { + /** + * Create the nodes that are the roots of external tilesets + * that are referred to by the given traversed tile. + * + * If the given tile does not have any contents or none of + * them refers to a tileset, then an empty array is returned. + * + * @param baseUri - The URI against which content URI are resolved + * in order to obtain an absolute URI. This is only used for the case + * of package (3TZ or 3DTILES) content, to create a `TilesetSource` + * from the absolute URI. + * @param traversedTile - The `TraversedTile` + * @returns The external tileset roots + * @throws DataError If one of the externa tilesets or + * its associated files could not be resolved. + */ + static async createExternalTilesetRoots( + baseUri: string, + traversedTile: TraversedTile + ): Promise { + if (traversedTile.isImplicitTilesetRoot()) { + return []; + } + const contents = traversedTile.getRawContents(); + if (contents.length === 0) { + return []; + } + + const resourceResolver = traversedTile.getResourceResolver(); + + const externalRoots: TraversedTile[] = []; + for (const content of contents) { + const contentUri = content.uri; + + // Try to obtain an external tileset from the content + const externalTilesetContext = + await TilesetTraversers.resolveExternalTilesetContext( + baseUri, + contentUri, + resourceResolver + ); + + if (externalTilesetContext) { + const externalTileset = externalTilesetContext.tileset; + const externalResourceResolver = + externalTilesetContext.resourceResolver; + + // If an external tileset was found, resolve its schema, + // and create an explicit traversed tile for its root. + const externalSchema = await TilesetTraversers.resolveSchema( + externalTileset, + externalResourceResolver + ); + const externalRoot = new ExplicitTraversedTile( + externalTileset.root, + traversedTile.path + `/[external:${contentUri}]/root`, + traversedTile.level + 1, + traversedTile, + externalSchema, + externalResourceResolver + ); + externalRoots.push(externalRoot); + } + } + return externalRoots; + } + + /** + * Fetch the information that is required for creating the root + * nodes of external tilesets from the given URI. + * + * If the given URI does not refer to an external tileset, + * then `undefined` is returned. + * + * Otherwise, it will return the parsed `Tileset` object, + * and the `ResourceResolver` that can be used to resolve + * resources from this tileset. + * + * @param baseUri - The URI against which the given URI is resolved + * in order to obtain an absolute URI. This is only used for the case + * of package (3TZ or 3DTILES) content, to create a `TilesetSource` + * from the absolute URI. + * @param uri - The URI + * @param resourceResolver - The `ResourceResolver` + * @returns The tileset + * @throws DataError If an external tileset could not be + * resolved or parsed. + */ + private static async resolveExternalTilesetContext( + baseUri: string, + uri: string, + resourceResolver: ResourceResolver + ): Promise< + { tileset: Tileset; resourceResolver: ResourceResolver } | undefined + > { + const contentData = new LazyContentData(uri, resourceResolver); + const contentDataType = await ContentDataTypeRegistry.findContentDataType( + contentData + ); + const isTileset = contentDataType === ContentDataTypes.CONTENT_TYPE_TILESET; + + // For external tileset JSON files, just return the parsed + // tileset and a resource resolver that resolves against + // the base directory of the tileset JSON file + if (isTileset) { + const externalTileset = await contentData.getParsedObject(); + const basePath = path.dirname(uri); + const externalResourceResolver = resourceResolver.derive(basePath); + const result = { + tileset: externalTileset, + resourceResolver: externalResourceResolver, + }; + return result; + } + + // For tileset packages, create a `TilesetSource`, extract + // the `Tileset` object from its `tileset.json` file, + // and return the `Tileset` and a resource resolver that + // resolves against the tileset source. + const isPackage = contentDataType === ContentDataTypes.CONTENT_TYPE_3TZ; + if (isPackage) { + const absoluteUri = Paths.join(baseUri, uri); + const externalTilesetSource = new TilesetSource3tz(); + const tilesetJsonFileName = "tileset.json"; + // XXX TODO There is no matching 'close' call for this! + try { + externalTilesetSource.open(absoluteUri); + } catch (e) { + console.warn( + `Could not open external tileset from ${absoluteUri} - ignoring` + ); + return undefined; + } + let externalTileset; + const tilesetJsonData = + externalTilesetSource.getValue(tilesetJsonFileName); + if (!tilesetJsonData) { + throw new DataError(`Could not resolve ${tilesetJsonFileName}`); + } + try { + externalTileset = JSON.parse(tilesetJsonData.toString("utf-8")); + } catch (e) { + throw new DataError( + `Could not parse tileset from ${tilesetJsonFileName}` + ); + } + const externalResourceResolver = new TilesetSourceResourceResolver( + ".", + externalTilesetSource + ); + const result = { + tileset: externalTileset, + resourceResolver: externalResourceResolver, + }; + return result; + } + return undefined; + } + + /** + * Resolve the `Schema` for the given tileset. + * + * This is either the `tileset.schema`, or the schema that is + * obtained from the `tileset.schemaUri`, or `undefined` if + * neither of them are present. + * + * @param tileset - The tileset + * @param resourceResolver - The `ResourceResolver` for loading + * the schema from the `schemaUri` if necessary + * @returns The `Schema`, or `undefined` if there is none + * @throws DataError If the schema from the `schemaUri` + * could not be resolved or parsed. + */ + static async resolveSchema( + tileset: Tileset, + resourceResolver: ResourceResolver + ): Promise { + if (tileset.schema) { + return tileset.schema; + } + if (tileset.schemaUri) { + const uri = tileset.schemaUri; + const schemaData = await resourceResolver.resolveData(uri); + if (!schemaData) { + throw new DataError(`Could not resolve ${uri}`); + } + try { + const schema = JSON.parse(schemaData.toString("utf-8")); + return schema; + } catch (e) { + throw new DataError(`Could not parse schema from ${uri}`); + } + } + return undefined; + } +} diff --git a/src/traversal/TraversedTile.ts b/src/traversal/TraversedTile.ts index 9511bf2b..c5abf608 100644 --- a/src/traversal/TraversedTile.ts +++ b/src/traversal/TraversedTile.ts @@ -1,5 +1,6 @@ import { Tile } from "../structure/Tile"; import { Content } from "../structure/Content"; +import { ResourceResolver } from "../io/ResourceResolver"; /** * An interface that summarizes context information for @@ -136,18 +137,13 @@ export interface TraversedTile { getFinalContents(): Content[]; /** - * Resolves the given URI against the context in which this - * tile appears. + * Returns the `ResourceResolver` that can be used for + * resolving the data that appears as `content.uri` in + * this tile. * - * This is primarily intended for (relative) content URIs. - * It will usually just resolve the given URI against the - * path that contained the tileset, resulting in an absolute - * URI that can be used to access the content. - * - * @param uri - The (content) uri - * @returns The resolved URI + * @returns The `ResourceResolver` */ - resolveUri(uri: string): string; + getResourceResolver(): ResourceResolver; /** * Returns whether this tile is the root of an implicit tileset. From 815f9969ba9ea4693cf55662077c9f585d6353d5 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 1 Apr 2023 23:22:57 +0200 Subject: [PATCH 29/60] Minor fix for exmample in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b1589c0a..3e5491db 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ npx ts-node ./src/main.ts combine -i ./specs/data/combineTilesets/input -o ./spe Merge multiple tilesets into a single one that refers to the input tilesets as external tilesets. ``` -npx ts-node ./src/main.ts merge -i ./specs/data/mergeTilesets/input/TilesetA -i ./specs/data/mergeTilesets/input/sub/TilesetA -o ./specs/data/mergeTilesets/output +npx ts-node ./src/main.ts merge -i ./specs/data/mergeTilesets/TilesetA -i ./specs/data/mergeTilesets/sub/TilesetA -o ./specs/data/mergeTilesets/output ``` From daef0bb876debb9f71049acb7bbc4c5608c98474 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sat, 1 Apr 2023 23:24:30 +0200 Subject: [PATCH 30/60] Cleanups and polishing for pipelines and processing --- demos/PipelineDrafts.ts | 4 +- demos/PipelineExperiments.ts | 62 +++ demos/TilesetContentProcessorDrafts.ts | 87 ---- demos/TilesetProcessorExamples.ts | 104 +++++ demos/TraversalStatsDemo.ts | 37 +- src/contentProcessing/GltfPipelineLegacy.ts | 40 +- src/pipelines/ContentStage.ts | 20 + src/pipelines/ContentStageExecutor.ts | 239 +++++++++-- src/pipelines/ContentStages.ts | 1 + src/pipelines/Pipeline.ts | 29 ++ src/pipelines/PipelineExecutor.ts | 41 +- src/pipelines/Pipelines.ts | 32 +- src/pipelines/Stage.ts | 9 +- src/pipelines/TilesetEntries.ts | 27 -- src/pipelines/TilesetStage.ts | 11 + src/pipelines/TilesetStageExecutor.ts | 108 +++-- src/pipelines/TilesetStages.ts | 18 +- .../BasicTilesetProcessor.ts | 303 ++++++++++++++ .../TilesetContentProcessor.ts | 198 --------- .../TilesetExplicitContentProcessor.ts | 173 -------- src/tilesetProcessing/TilesetProcessor.ts | 386 +++++++++--------- .../TilesetProcessorContext.ts | 59 +++ src/tilesets/Extensions.ts | 63 +-- 23 files changed, 1176 insertions(+), 875 deletions(-) create mode 100644 demos/PipelineExperiments.ts delete mode 100644 demos/TilesetContentProcessorDrafts.ts create mode 100644 demos/TilesetProcessorExamples.ts delete mode 100644 src/pipelines/TilesetEntries.ts create mode 100644 src/tilesetProcessing/BasicTilesetProcessor.ts delete mode 100644 src/tilesetProcessing/TilesetContentProcessor.ts delete mode 100644 src/tilesetProcessing/TilesetExplicitContentProcessor.ts create mode 100644 src/tilesetProcessing/TilesetProcessorContext.ts diff --git a/demos/PipelineDrafts.ts b/demos/PipelineDrafts.ts index d8d6e4f5..68175735 100644 --- a/demos/PipelineDrafts.ts +++ b/demos/PipelineDrafts.ts @@ -5,8 +5,8 @@ import { Pipelines } from "../src/pipelines/Pipelines"; function createPipelineDraftJson() { const pipelineJson = { - input: "./data/Tileset", - output: "./data/testTileset_final", + input: "./specs/data/TilesetWithUris", + output: "./output/pipelineDrafts", tilesetStages: [ { name: "unzip-and-zip-tiles-only", diff --git a/demos/PipelineExperiments.ts b/demos/PipelineExperiments.ts new file mode 100644 index 00000000..12e50406 --- /dev/null +++ b/demos/PipelineExperiments.ts @@ -0,0 +1,62 @@ +import { PipelineExecutor } from "../src/pipelines/PipelineExecutor"; +import { Pipelines } from "../src/pipelines/Pipelines"; + +// Notes: (See ContentStageExecutor) +// - The differentiation between explicit and implicit content +// operations (and which content operations require an update +// of the template URI) still has to be finalized. +// - The (public!) TilesetProcessor.storeTargetEntry method +// should be reviewed. Maybe returning multiple entries +// for content operations should NOT automatically update +// the contents of the input tile, but there should be +// a convenience method for doing this. + +function createPipelineExperimentsJson() { + const optimizeGlbOptions = { + dracoOptions: { + compressionLevel: 10, + }, + }; + + const pipelineJson = { + input: "./specs/data/TilesetWithUris/tileset.json", + output: "./output/result.3tz", + tilesetStages: [ + { + name: "Tileset stage for b3dmToGlb", + contentStages: ["b3dmToGlb"], + }, + { + name: "Tileset stage for optimizeGlb", + contentStages: [ + { + name: "optimizeGlb", + options: optimizeGlbOptions, + }, + ], + }, + { + name: "Tileset stage for separateGltf", + contentStages: [ + { + name: "separateGltf", + }, + ], + }, + { + // This is not necessary, but done for the experiment + name: "Tileset stage for 3TZ", + }, + ], + }; + return pipelineJson; +} + +async function runPipelineDrafts() { + const pipelineJson = createPipelineExperimentsJson(); + const pipeline = Pipelines.createPipeline(pipelineJson); + const overwrite = true; + await PipelineExecutor.executePipeline(pipeline, overwrite); +} + +runPipelineDrafts(); diff --git a/demos/TilesetContentProcessorDrafts.ts b/demos/TilesetContentProcessorDrafts.ts deleted file mode 100644 index 79554871..00000000 --- a/demos/TilesetContentProcessorDrafts.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { Paths } from "../src/base/Paths"; -import { ContentOps } from "../src/contentProcessing/ContentOps"; -import { GltfUtilities } from "../src/contentProcessing/GtlfUtilities"; -import { ContentDataTypes } from "../src/contentTypes/ContentDataTypes"; -import { Content } from "../src/structure/Content"; -import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; - -import { TilesetContentProcessor } from "../src/tilesetProcessing/TilesetContentProcessor"; -import { TilesetExplicitContentProcessor } from "../src/tilesetProcessing/TilesetExplicitContentProcessor"; - -async function runB3dmToGlbTest() { - const tilesetSourceName = - "../3d-tiles-samples/1.0/TilesetWithDiscreteLOD/tileset.json"; - const tilesetTargetName = "./output/TilesetWithDiscreteLOD/tileset.json"; - const overwrite = true; - - const quiet = false; - const tilesetContentProcessor = - new (class extends TilesetExplicitContentProcessor { - override async processExplicitTileContentEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise { - if (type !== ContentDataTypes.CONTENT_TYPE_B3DM) { - return [sourceEntry]; - } - const targetEntry = { - key: Paths.replaceExtension(sourceEntry.key, ".glb"), - value: ContentOps.b3dmToGlbBuffer(sourceEntry.value), - }; - console.log( - " Updated " + sourceEntry.key + " to " + targetEntry.key - ); - return [targetEntry]; - } - })(quiet); - - await tilesetContentProcessor.process( - tilesetSourceName, - tilesetTargetName, - overwrite - ); -} - -async function runOptimizeTest() { - const tilesetSourceName = - "../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json"; - const tilesetTargetName = - "./output/SparseImplicitQuadtree-optimized/tileset.json"; - const overwrite = true; - - const quiet = false; - const tilesetContentProcessor = new (class extends TilesetContentProcessor { - override async processTileContentEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise { - if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { - return [sourceEntry]; - } - const targetEntry = { - key: "optimized/" + sourceEntry.key, - value: await GltfUtilities.optimizeGlb(sourceEntry.value, {}), - }; - console.log( - " Optimized " + sourceEntry.key + " to " + targetEntry.key - ); - return [targetEntry]; - } - - override async processImplicitTilesetRootContent( - content: Content - ): Promise { - content.uri = "optimized/" + content.uri; - return content; - } - })(quiet); - - await tilesetContentProcessor.process( - tilesetSourceName, - tilesetTargetName, - overwrite - ); -} - -//runB3dmToGlbTest(); -runOptimizeTest(); diff --git a/demos/TilesetProcessorExamples.ts b/demos/TilesetProcessorExamples.ts new file mode 100644 index 00000000..6e518a61 --- /dev/null +++ b/demos/TilesetProcessorExamples.ts @@ -0,0 +1,104 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { Content } from "../src/structure/Content"; +import { Schema } from "../src/structure/Metadata/Schema"; +import { Tile } from "../src/structure/Tile"; +import { Tileset } from "../src/structure/Tileset"; + +import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; + +import { BasicTilesetProcessor } from "../src/tilesetProcessing/BasicTilesetProcessor"; + +import { TraversedTile } from "../src/traversal/TraversedTile"; + +async function example() { + const tilesetSourceName = + "../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json"; + const tilesetTargetName = + "./output/SparseImplicitQuadtree-result/tileset.json"; + const overwrite = true; + + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(tilesetSourceName, tilesetTargetName, overwrite); + + // Apply a callback to each `TraversedTile` + await tilesetProcessor.forEachTile( + async (traversedTile: TraversedTile): Promise => { + console.log("In forEachTile"); + return; + } + ); + + // Apply a callback to each (explicit) `Tile + await tilesetProcessor.forEachExplicitTile( + async (tile: Tile): Promise => { + console.log("In forEachExplicitTile"); + return; + } + ); + + // Create a callback that receives a `Tile` and + // applies a callback to each content + const contentCallback = BasicTilesetProcessor.callbackForEachContent( + async (content: Content): Promise => { + console.log("In contentCallback for implicit tiling roots", content.uri); + return; + } + ); + + // Apply a callback to each content of an (explicit) `Tile` + await tilesetProcessor.forEachExplicitTile( + async (tile: Tile): Promise => { + console.log("In forEachExplicitTile "); + if (tile.implicitTiling) { + contentCallback(tile); + } + } + ); + + // Apply a callback to each entry that is the content + // of an explicit `Tile` + await tilesetProcessor.forEachExplicitTileContentEntry( + async ( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise => { + console.log("In forEachExplicitTileContentEntry"); + return [sourceEntry]; + } + ); + + // Apply a callback to each entry that is the content + // of a tile + await tilesetProcessor.forEachTileContentEntry( + async ( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise => { + console.log("In forEachTileContentEntry"); + return [sourceEntry]; + } + ); + + // Apply a callback to each entry + await tilesetProcessor.forEachEntry( + async ( + sourceEntry: TilesetEntry, + type: string | undefined + ): Promise => { + console.log("In forEachEntry"); + return [sourceEntry]; + } + ); + + // Apply a callback to the tileset and its schema + await tilesetProcessor.forTileset( + async (tileset: Tileset, schema: Schema | undefined): Promise => { + console.log("In forTileset"); + return; + } + ); + + await tilesetProcessor.end(); +} + +example(); diff --git a/demos/TraversalStatsDemo.ts b/demos/TraversalStatsDemo.ts index d3774e2a..704a3beb 100644 --- a/demos/TraversalStatsDemo.ts +++ b/demos/TraversalStatsDemo.ts @@ -1,4 +1,3 @@ -import fs from "fs"; import path from "path"; import { readJsonUnchecked } from "./readJsonUnchecked"; @@ -8,7 +7,6 @@ import { ResourceResolvers } from "../src/io/ResourceResolvers"; import { TilesetTraverser } from "../src/traversal/TilesetTraverser"; import { TraversedTile } from "../src/traversal/TraversedTile"; import { BufferedContentData } from "../src/contentTypes/BufferedContentData"; -import { ContentDataTypeRegistry } from "../src/contentTypes/ContentDataTypeRegistry"; import { ContentDataTypeChecks } from "../src/contentTypes/ContentDataTypeChecks"; import { ContentDataTypes } from "../src/contentTypes/ContentDataTypes"; @@ -37,6 +35,21 @@ async function tilesetTraversalDemo(filePath: string) { // tileset traverser, and store information about the // traversed tiles in the `StatsCollector` const statsTraversalCallback = async (traversedTile: TraversedTile) => { + { + const indent = " ".repeat(traversedTile.level); + const contentUris = traversedTile.getFinalContents().map((c) => c.uri); + const geometricError = traversedTile.asFinalTile().geometricError; + const message = + indent + + "Level " + + traversedTile.level + + ", geometricError " + + geometricError + + ", contents " + + contentUris; + console.log(message); + } + statsCollector.increment("totalNumberOfTiles"); const subtreeUri = traversedTile.getSubtreeUri(); if (subtreeUri !== undefined) { @@ -50,13 +63,17 @@ async function tilesetTraversalDemo(filePath: string) { const tileResourceResolver = traversedTile.getResourceResolver(); for (const contentUri of contentUris) { const data = await tileResourceResolver.resolveData(contentUri); - const contentData = new BufferedContentData(contentUri, data); - const isTileFile = await isTileFileContent(contentData); - if (isTileFile) { - if (data) { + if (!data) { + statsCollector.increment("unresolvableContents"); + } else { + const contentData = new BufferedContentData(contentUri, data); + const isTileFile = await isTileFileContent(contentData); + if (isTileFile) { statsCollector.acceptEntry("tileFileSize", data.length); - } else { - statsCollector.increment("unresolvableContents"); + statsCollector.acceptEntry( + "tileFileSize_" + traversedTile.level, + data.length + ); } } } @@ -66,6 +83,10 @@ async function tilesetTraversalDemo(filePath: string) { const finalTile = traversedTile.asFinalTile(); const geometricError = finalTile.geometricError; statsCollector.acceptEntry("geometricError", geometricError); + statsCollector.acceptEntry( + "geometricError_" + traversedTile.level, + geometricError + ); return true; }; diff --git a/src/contentProcessing/GltfPipelineLegacy.ts b/src/contentProcessing/GltfPipelineLegacy.ts index 822f6932..00ddda1e 100644 --- a/src/contentProcessing/GltfPipelineLegacy.ts +++ b/src/contentProcessing/GltfPipelineLegacy.ts @@ -1,6 +1,7 @@ import { defaultValue } from "../base/defaultValue"; import { Cartesian3, defined, DeveloperError, Ellipsoid } from "cesium"; +import { Extensions } from "../tilesets/Extensions"; /** * Methods and fragments ported from a legacy version of gltf-pipeline. @@ -57,8 +58,7 @@ export class GltfPipelineLegacy { extensions.CESIUM_RTC = { center: positionArray, }; - GltfPipelineLegacy.addExtensionsRequired(gltf, "CESIUM_RTC"); - GltfPipelineLegacy.addExtensionsUsed(gltf, "CESIUM_RTC"); + Extensions.addExtensionRequired(gltf, "CESIUM_RTC"); } private static fixBatchIdSemantic(gltf: any) { @@ -92,40 +92,4 @@ export class GltfPipelineLegacy { } } } - - private static addExtensionsRequired(gltf: any, extension: string) { - let extensionsRequired = gltf.extensionsRequired; - if (!defined(extensionsRequired)) { - extensionsRequired = []; - gltf.extensionsRequired = extensionsRequired; - } - GltfPipelineLegacy.addToArray(extensionsRequired, extension, true); - GltfPipelineLegacy.addExtensionsUsed(gltf, extension); - } - - private static addExtensionsUsed(gltf: any, extension: string) { - let extensionsUsed = gltf.extensionsUsed; - if (!defined(extensionsUsed)) { - extensionsUsed = []; - gltf.extensionsUsed = extensionsUsed; - } - GltfPipelineLegacy.addToArray(extensionsUsed, extension, true); - } - - private static addToArray( - array: string[], - element: string, - checkDuplicates: boolean - ) { - checkDuplicates = defaultValue(checkDuplicates, false); - if (checkDuplicates) { - const index = array.indexOf(element); - if (index > -1) { - return index; - } - } - - array.push(element); - return array.length - 1; - } } diff --git a/src/pipelines/ContentStage.ts b/src/pipelines/ContentStage.ts index b557307c..2e507f6f 100644 --- a/src/pipelines/ContentStage.ts +++ b/src/pipelines/ContentStage.ts @@ -1,6 +1,26 @@ import { Stage } from "./Stage"; + import { TilesetEntry } from "../tilesetData/TilesetEntry"; +/** + * An interface that describes an operation that may be + * applied to one `TilesetEntry` during the execution + * of a pipeline. + * + * Instances of this are created with the `ContentStages` + * class, and contained within a `TilesetStage`. + */ export interface ContentStage extends Stage { + /** + * An optional predicate that determines whether this stage + * should be applied to a certain entry. + */ condition: ((e: TilesetEntry) => Promise) | undefined; + + /** + * Arbitrary options that may have been given in the + * input JSON, and will be passed to implementations + * that may support these options (e.g. `gltf-pipeline`). + */ + options: any; } diff --git a/src/pipelines/ContentStageExecutor.ts b/src/pipelines/ContentStageExecutor.ts index b1b07990..b754bafc 100644 --- a/src/pipelines/ContentStageExecutor.ts +++ b/src/pipelines/ContentStageExecutor.ts @@ -1,71 +1,228 @@ +import GltfPipeline from "gltf-pipeline"; + +import { Paths } from "../base/Paths"; +import { Buffers } from "../base/Buffers"; + +import { ContentDataTypes } from "../contentTypes/ContentDataTypes"; + import { TilesetEntry } from "../tilesetData/TilesetEntry"; -import { TilesetSource } from "../tilesetData/TilesetSource"; -import { TilesetSources } from "../tilesetData/TilesetSources"; -import { TilesetTarget } from "../tilesetData/TilesetTarget"; -import { TilesetTargets } from "../tilesetData/TilesetTargets"; import { ContentStage } from "./ContentStage"; -import { TilesetEntries } from "./TilesetEntries"; +import { BasicTilesetProcessor } from "../tilesetProcessing/BasicTilesetProcessor"; + +import { GltfUtilities } from "../contentProcessing/GtlfUtilities"; +import { ContentOps } from "../contentProcessing/ContentOps"; + +/** + * Methods to execute `ContentStage` objects. + */ export class ContentStageExecutor { + /** + * Execute the given `ContentStage`. + * + * @param contentStage - The `ContentStage` object + * @param tilesetProcessor The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws TilesetError If one of the processing steps causes + * an error. + */ static async executeContentStage( - tilesetSource: TilesetSource, - tilesetTarget: TilesetTarget, - contentStage: ContentStage + contentStage: ContentStage, + tilesetProcessor: BasicTilesetProcessor ) { if (contentStage.name === "gzip") { await ContentStageExecutor.executeGzip( - tilesetSource, - tilesetTarget, + tilesetProcessor, contentStage.condition ); } else if (contentStage.name === "ungzip") { - await ContentStageExecutor.executeGunzip(tilesetSource, tilesetTarget); + await ContentStageExecutor.executeGunzip(tilesetProcessor); + } else if (contentStage.name === "b3dmToGlb") { + await ContentStageExecutor.executeB3dmToGlb(tilesetProcessor); + } else if (contentStage.name === "optimizeGlb") { + const options = contentStage.options; + await ContentStageExecutor.executeOptimizeGlb(tilesetProcessor, options); + } else if (contentStage.name === "separateGltf") { + await ContentStageExecutor.executeSeparateGltf(tilesetProcessor); } else { - // TODO Review and document this - const message = - ` Unknown contentStage name: ${contentStage.name} ` + - `- performing no-op`; + const message = ` Unknown contentStage name: ${contentStage.name}`; console.log(message); - await ContentStageExecutor.executeNoOp(tilesetSource, tilesetTarget); } } + /** + * Performs the 'gzip' content stage with the given processor. + * + * This will process all entries of the source tileset. The + * data of entries that match the given condition will be + * compressed with gzip. Other entries remain unaffected. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @param condition The condition from the `ContentStage` + * @returns A promise that resolves when the process is finished + * @throws TilesetError If one of the processing steps causes + * an error. + */ private static async executeGzip( - tilesetSource: TilesetSource, - tilesetTarget: TilesetTarget, + tilesetProcessor: BasicTilesetProcessor, condition: ((e: TilesetEntry) => Promise) | undefined ): Promise { - const inputEntries = TilesetSources.getEntries(tilesetSource); - for (const inputEntry of inputEntries) { - let included = true; - if (condition) { - included = await condition(inputEntry); - } - let outputEntry = inputEntry; - if (included) { - outputEntry = TilesetEntries.gzip(inputEntry); + await tilesetProcessor.forEachEntry( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (sourceEntry: TilesetEntry, type: string | undefined) => { + let targetValue = sourceEntry.value; + if (condition) { + const shouldZip = await condition(sourceEntry); + if (shouldZip) { + targetValue = Buffers.gzip(sourceEntry.value); + } + } + const targetEntry = { + key: sourceEntry.key, + value: targetValue, + }; + return [targetEntry]; } - tilesetTarget.addEntry(outputEntry.key, outputEntry.value); - } + ); } + /** + * Performs the 'gunzip' content stage with the given processor. + * + * This will process all entries of the source tileset. The + * data of entries that is compressed with gzip will be + * uncompressed. Other entries remain unaffected. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws TilesetError If one of the processing steps causes + * an error. + */ private static async executeGunzip( - tilesetSource: TilesetSource, - tilesetTarget: TilesetTarget + tilesetProcessor: BasicTilesetProcessor ): Promise { - const inputEntries = TilesetSources.getEntries(tilesetSource); - for (const inputEntry of inputEntries) { - const outputEntry = TilesetEntries.gunzip(inputEntry); - tilesetTarget.addEntry(outputEntry.key, outputEntry.value); - } + await tilesetProcessor.forEachEntry( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (sourceEntry: TilesetEntry, type: string | undefined) => { + const targetEntry = { + key: sourceEntry.key, + value: Buffers.gunzip(sourceEntry.value), + }; + return [targetEntry]; + } + ); + } + + private static async executeB3dmToGlb( + tilesetProcessor: BasicTilesetProcessor + ): Promise { + await tilesetProcessor.forEachExplicitTileContentEntry( + async (sourceEntry: TilesetEntry, type: string | undefined) => { + if (type !== ContentDataTypes.CONTENT_TYPE_B3DM) { + return [sourceEntry]; + } + const sourceKey = sourceEntry.key; + const targetKey = Paths.replaceExtension(sourceKey, ".glb"); + const sourceValue = sourceEntry.value; + const targetValue = ContentOps.b3dmToGlbBuffer(sourceValue); + const targetEntry = { + key: targetKey, + value: targetValue, + }; + return [targetEntry]; + } + ); + } + + private static async executeOptimizeGlb( + tilesetProcessor: BasicTilesetProcessor, + options: any + ): Promise { + await tilesetProcessor.forEachExplicitTileContentEntry( + async (sourceEntry: TilesetEntry, type: string | undefined) => { + if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { + return [sourceEntry]; + } + const sourceValue = sourceEntry.value; + const targetValue = await GltfUtilities.optimizeGlb( + sourceValue, + options + ); + const targetEntry = { + key: sourceEntry.key, + value: targetValue, + }; + return [targetEntry]; + } + ); } - private static async executeNoOp( - tilesetSource: TilesetSource, - tilesetTarget: TilesetTarget + /** + * Internal experiments. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws TilesetError If one of the processing steps causes + * an error. + */ + private static async executeSeparateGltf( + tilesetProcessor: BasicTilesetProcessor ): Promise { - const inputEntries = TilesetSources.getEntries(tilesetSource); - TilesetTargets.putEntries(tilesetTarget, inputEntries); + await tilesetProcessor.forEachExplicitTileContentEntry( + async (sourceEntry: TilesetEntry, type: string | undefined) => { + if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { + return [sourceEntry]; + } + const options = { + separate: true, + name: sourceEntry.key, + }; + const gltfPipelineResults = await GltfPipeline.glbToGltf( + sourceEntry.value, + options + ); + const targetKey = Paths.replaceExtension(sourceEntry.key, ".gltf"); + const targetValue = Buffer.from( + JSON.stringify(gltfPipelineResults.gltf) + ); + const targetEntry = { + key: targetKey, + value: targetValue, + }; + for (const resourceKey of Object.keys( + gltfPipelineResults.separateResources + )) { + const resourceValue = + gltfPipelineResults.separateResources[resourceKey]; + const resourceTargetEntry = { + key: resourceKey, + value: resourceValue, + }; + tilesetProcessor.storeTargetEntry(resourceTargetEntry); + } + return [targetEntry]; + } + ); + + // TODO This has to be done for the implicit case: + /* + const templateUriUpdateCallback = + BasicTilesetProcessor.callbackForEachContent( + async (content: Content): Promise => { + if (content.uri.toLowerCase().endsWith(".glb")) { + content.uri = Paths.replaceExtension(content.uri, ".gltf"); + } + } + ); + + await tilesetProcessor.forEachExplicitTile( + async (tile: Tile): Promise => { + if (tile.implicitTiling) { + templateUriUpdateCallback(tile); + } + } + ); + */ } } diff --git a/src/pipelines/ContentStages.ts b/src/pipelines/ContentStages.ts index 4d7d46bc..62f697bd 100644 --- a/src/pipelines/ContentStages.ts +++ b/src/pipelines/ContentStages.ts @@ -14,6 +14,7 @@ export class ContentStages { const contentStage: ContentStage = { name: contentStageJson, condition: undefined, + options: undefined, }; return contentStage; } diff --git a/src/pipelines/Pipeline.ts b/src/pipelines/Pipeline.ts index e3975b11..47905b40 100644 --- a/src/pipelines/Pipeline.ts +++ b/src/pipelines/Pipeline.ts @@ -1,7 +1,36 @@ import { TilesetStage } from "./TilesetStage"; +/** + * An interface that describes a pipeline of operations + * that should be applied to tileset data. + * + * It consists of the input- and output definition, and a + * set of `TilesetStage` steps that should be executed. + * Instances of this are created with the `Pipelines` + * class, and executed with a `PipelineExecutor`. + */ export interface Pipeline { + /** + * The name of the input tileset. + * + * This may be a path to a tileset JSON file, or a directory + * name (which is then assumed to contain a `tileset.json`), + * or a tileset package, as indicated by the file extension + * being `.3tz` or `.3dtiles`. + */ input: string; + + /** + * The name of the output tileset. + * + * (See `input` for details) + */ output: string; + + /** + * The array of `TilesetStage` objects that will be + * applied to the input data to eventually generate + * the output data. + */ tilesetStages: TilesetStage[]; } diff --git a/src/pipelines/PipelineExecutor.ts b/src/pipelines/PipelineExecutor.ts index 3beee044..2748148a 100644 --- a/src/pipelines/PipelineExecutor.ts +++ b/src/pipelines/PipelineExecutor.ts @@ -1,7 +1,24 @@ +import fs from "fs"; +import os from "os"; +import path from "path"; + import { Pipeline } from "./Pipeline"; import { TilesetStageExecutor } from "./TilesetStageExecutor"; +/** + * Methods to execute `Pipeline` objects. + */ export class PipelineExecutor { + /** + * Executes the given `Pipeline`. + * + * @param pipeline - The `Pipeline` object + * @param overwrite - Whether outputs should be overwritten if + * they already exist + * @returns A promise that resolves when the process is finished + * @throws TilesetError If one of the processing steps causes + * an error. + */ static async executePipeline(pipeline: Pipeline, overwrite: boolean) { console.log("Executing pipeline"); @@ -10,6 +27,26 @@ export class PipelineExecutor { let currentOverwrite = true; const tilesetStages = pipeline.tilesetStages; + + // Create a temporary directory for the intermediate + // processing steps (if there are more than one) + // TODO: This is not cleaned up at the end... + let tempBasePath = ""; + + // TODO Store locally for experiments... + const EXPERIMENTS = true; + if (EXPERIMENTS) { + tempBasePath = "./output/TEMP"; + console.warn("Using temp path for experiments: " + tempBasePath); + } else { + if (tilesetStages.length > 1) { + tempBasePath = fs.mkdtempSync( + path.join(os.tmpdir(), "3d-tiles-tools-pipeline-") + ); + } + } + + // Execute each `TilesetStage` for (let t = 0; t < tilesetStages.length; t++) { const tilesetStage = tilesetStages[t]; @@ -22,9 +59,7 @@ export class PipelineExecutor { currentOutput = pipeline.output; currentOverwrite = overwrite; } else { - // TODO Use proper OS-specific temp directory here. - // (And maybe even clean up afterwards...) - currentOutput = "./data/TEMP/tilesetStage-" + t; + currentOutput = `${tempBasePath}/tilesetStage-${t}`; currentOverwrite = true; } diff --git a/src/pipelines/Pipelines.ts b/src/pipelines/Pipelines.ts index feabd492..74d27531 100644 --- a/src/pipelines/Pipelines.ts +++ b/src/pipelines/Pipelines.ts @@ -4,17 +4,33 @@ import { Pipeline } from "./Pipeline"; import { TilesetStage } from "./TilesetStage"; import { TilesetStages } from "./TilesetStages"; +/** + * Methods to create `Pipeline` objects from JSON input. + */ export class Pipelines { + /** + * Creates a `Pipeline` object from the given (untyped) JSON. + * + * @param pipelineJson - The JSON object + * @returns The `Pipeline` object + * @throws DeveloperError When the input was not valid + */ static createPipeline(pipelineJson: any): Pipeline { - const tilesetStages: TilesetStage[] = []; - if (!pipelineJson.tilesetStages) { - throw new DeveloperError( - "The pipeline JSON does not define tilesetStages" - ); + if (!pipelineJson.input) { + throw new DeveloperError("The pipeline JSON does not define an input"); + } + if (!pipelineJson.output) { + throw new DeveloperError("The pipeline JSON does not define an output"); } - for (const tilesetStageJson of pipelineJson.tilesetStages) { - const tilesetStage = TilesetStages.createTilesetStage(tilesetStageJson); - tilesetStages.push(tilesetStage); + + // The tilesetStages may be undefined, resulting + // in an empty array here + const tilesetStages: TilesetStage[] = []; + if (pipelineJson.tilesetStages) { + for (const tilesetStageJson of pipelineJson.tilesetStages) { + const tilesetStage = TilesetStages.createTilesetStage(tilesetStageJson); + tilesetStages.push(tilesetStage); + } } const pipeline: Pipeline = { input: pipelineJson.input, diff --git a/src/pipelines/Stage.ts b/src/pipelines/Stage.ts index 39463e02..acee6af2 100644 --- a/src/pipelines/Stage.ts +++ b/src/pipelines/Stage.ts @@ -1,4 +1,11 @@ +/** + * Common base interface for a stage in a pipeline. + * + * Specializations are `TilesetStage` and `ContentStage`. + */ export interface Stage { + /** + * The name of this stage. + */ name: string; - [option: string]: any; } diff --git a/src/pipelines/TilesetEntries.ts b/src/pipelines/TilesetEntries.ts deleted file mode 100644 index d7beae81..00000000 --- a/src/pipelines/TilesetEntries.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Buffers } from "../base/Buffers"; - -import { TilesetEntry } from "../tilesetData/TilesetEntry"; - -export class TilesetEntries { - static gzip(inputEntry: TilesetEntry): TilesetEntry { - const inputKey = inputEntry.key; - const inputValue = inputEntry.value; - const outputKey = inputKey; - const outputValue = Buffers.gzip(inputValue); - return { - key: outputKey, - value: outputValue, - }; - } - - static gunzip(inputEntry: TilesetEntry): TilesetEntry { - const inputKey = inputEntry.key; - const inputValue = inputEntry.value; - const outputKey = inputKey; - const outputValue = Buffers.gunzip(inputValue); - return { - key: outputKey, - value: outputValue, - }; - } -} diff --git a/src/pipelines/TilesetStage.ts b/src/pipelines/TilesetStage.ts index 0107782b..c48d91c2 100644 --- a/src/pipelines/TilesetStage.ts +++ b/src/pipelines/TilesetStage.ts @@ -1,6 +1,17 @@ import { ContentStage } from "./ContentStage"; import { Stage } from "./Stage"; +/** + * Interface for a stage within a `Pipeline` of + * operations that should be applied to tileset data. + * + * Instances of this class are created with `TilesetStages` + * and executed with a `TilesetStageExecutor`. + */ export interface TilesetStage extends Stage { + /** + * The `ContentStage` steps representing the sequence of + * operations that should be applied to content. + */ contentStages: ContentStage[]; } diff --git a/src/pipelines/TilesetStageExecutor.ts b/src/pipelines/TilesetStageExecutor.ts index ef6ab372..31a1247d 100644 --- a/src/pipelines/TilesetStageExecutor.ts +++ b/src/pipelines/TilesetStageExecutor.ts @@ -1,28 +1,60 @@ -import { TilesetSource } from "../tilesetData/TilesetSource"; -import { TilesetSources } from "../tilesetData/TilesetSources"; -import { TilesetTarget } from "../tilesetData/TilesetTarget"; -import { TilesetTargets } from "../tilesetData/TilesetTargets"; - import { TilesetStage } from "./TilesetStage"; import { ContentStageExecutor } from "./ContentStageExecutor"; +import { BasicTilesetProcessor } from "../tilesetProcessing/BasicTilesetProcessor"; + +/** + * Methods to execute `TilesetStage` objects. + */ export class TilesetStageExecutor { - static async executeTilesetStageInternal( - tilesetSource: TilesetSource, - tilesetTarget: TilesetTarget, - tilesetStage: TilesetStage + /** + * Executes the given `TilesetStage`. + * + * @param tilesetStage - The `TilesetStage` object + * @param currentInput - The current input name, or a temporary + * name for intermediate steps (see `Pipeline.input` for details) + * @param currentOutput - The current output name, or a temporary + * name for intermediate steps (see `Pipeline.input` for details) + * @param overwrite - Whether outputs should be overwritten if + * they already exist + * @returns A promise that resolves when the process is finished + * @throws TilesetError If one of the processing steps causes + * an error. + */ + static async executeTilesetStage( + tilesetStage: TilesetStage, + currentInput: string, + currentOutput: string, + overwrite: boolean ) { - const contentStages = tilesetStage.contentStages; + console.log(` Executing tilesetStage : ${tilesetStage.name}`); + console.log(` currentInput: ${currentInput}`); + console.log(` currentOutput: ${currentOutput}`); + + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(currentInput, currentOutput, overwrite); + await TilesetStageExecutor.executeTilesetStageInternal( + tilesetStage, + tilesetProcessor + ); + await tilesetProcessor.end(); + } + /** + * Implementation for `executeTilesetStage`. + * + * @param tilesetStage - The `TilesetStage` object + * @param tilesetProcessor The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws TilesetError If one of the processing steps causes + * an error. + */ + private static async executeTilesetStageInternal( + tilesetStage: TilesetStage, + tilesetProcessor: BasicTilesetProcessor + ) { + const contentStages = tilesetStage.contentStages; if (contentStages.length === 0) { - // TODO This should probably not cause a message. - // A TilesetStage might be something like `databaseToTileset` - // that is self-contained and "atomic", and does not have - // any content stages. - const message = ` No contentStages - performing no-op`; - console.log(message); - const inputEntries = TilesetSources.getEntries(tilesetSource); - TilesetTargets.putEntries(tilesetTarget, inputEntries); return; } @@ -35,45 +67,9 @@ export class TilesetStageExecutor { console.log(message); await ContentStageExecutor.executeContentStage( - tilesetSource, - tilesetTarget, - contentStage + contentStage, + tilesetProcessor ); } } - - static async executeTilesetStage( - tilesetStage: TilesetStage, - currentInput: string, - currentOutput: string, - overwrite: boolean - ) { - console.log(` Executing tilesetStage : ${tilesetStage.name}`); - console.log(` currentInput: ${currentInput}`); - console.log(` currentOutput: ${currentOutput}`); - - let tilesetSource; - let tilesetTarget; - try { - tilesetSource = TilesetSources.createAndOpen(currentInput); - tilesetTarget = TilesetTargets.createAndBegin(currentOutput, overwrite); - - await TilesetStageExecutor.executeTilesetStageInternal( - tilesetSource, - tilesetTarget, - tilesetStage - ); - - tilesetSource.close(); - await tilesetTarget.end(); - } catch (error) { - if (tilesetSource) { - tilesetSource.close(); - } - if (tilesetTarget) { - await tilesetTarget.end(); - } - throw error; - } - } } diff --git a/src/pipelines/TilesetStages.ts b/src/pipelines/TilesetStages.ts index 011e02f9..2d969679 100644 --- a/src/pipelines/TilesetStages.ts +++ b/src/pipelines/TilesetStages.ts @@ -5,19 +5,33 @@ import { ContentStage } from "./ContentStage"; import { TilesetStage } from "./TilesetStage"; import { ContentStages } from "./ContentStages"; +/** + * Methods to create `TilesetStage` objects from JSON input. + */ export class TilesetStages { + /** + * Creates a `TilesetStage` object from the given (untyped) JSON. + * + * @param tilesetStageJson - The JSON object + * @returns The `TilesetStage` object + * @throws DeveloperError When the input was not valid + */ static createTilesetStage(tilesetStageJson: any): TilesetStage { - const contentStages: ContentStage[] = []; if (typeof tilesetStageJson === "string") { const tilesetStage: TilesetStage = { name: tilesetStageJson, - contentStages: contentStages, + contentStages: [], }; return tilesetStage; } + if (!defined(tilesetStageJson.name)) { throw new DeveloperError("The tilesetStage JSON does not define a name"); } + + // The contentStages may be undefined, resulting + // in an empty array here: + const contentStages: ContentStage[] = []; if (tilesetStageJson.contentStages) { for (const contentStageJson of tilesetStageJson.contentStages) { const contentStage = ContentStages.createContentStage(contentStageJson); diff --git a/src/tilesetProcessing/BasicTilesetProcessor.ts b/src/tilesetProcessing/BasicTilesetProcessor.ts new file mode 100644 index 00000000..e0993717 --- /dev/null +++ b/src/tilesetProcessing/BasicTilesetProcessor.ts @@ -0,0 +1,303 @@ +import { Tile } from "../structure/Tile"; +import { Tileset } from "../structure/Tileset"; +import { Content } from "../structure/Content"; +import { Schema } from "../structure/Metadata/Schema"; + +import { TilesetSourceResourceResolver } from "../io/TilesetSourceResourceResolver"; + +import { TilesetEntry } from "../tilesetData/TilesetEntry"; +import { TilesetSources } from "../tilesetData/TilesetSources"; + +import { Tiles } from "../tilesets/Tiles"; + +import { TilesetProcessor } from "./TilesetProcessor"; +import { TilesetEntryProcessor } from "./TilesetEntryProcessor"; + +import { TraversedTile } from "../traversal/TraversedTile"; +import { TilesetTraverser } from "../traversal/TilesetTraverser"; + +/** + * Implementation of a `TilesetProcessor` that offers methods for + * common operations on tileset data. + * + * The operations are applied by callbacks on certain elements + * of the tileset data: + * + * - Tiles and their content + * - Explicit tiles and their content + * - Unspecified entries (files) in the tileset + * - The tileset (and its schema) itself + * + * Each entry that is processed with one of the `forEach*Entry` + * methods will be processed only _once_, and then marked as + * already having been processed. Entries that have not been + * processed when `TilesetProcessor.end` is called will be + * moved to the tileset target as they are. + */ +export class BasicTilesetProcessor extends TilesetProcessor { + /** + * Creates a new instance + * + * @param quiet - Whether log messages should be omitted + */ + constructor(quiet?: boolean) { + super(quiet); + } + + /** + * Apply the given callback to all `TraversedTile` instances + * that result from traversing the tileset. + * + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When an error is thrown during processing + */ + async forEachTile( + callback: (traversedTile: TraversedTile) => Promise + ): Promise { + const context = this.getContext(); + const tilesetSource = context.tilesetSource; + const tileset = context.tileset; + const schema = context.schema; + + // Create the resource resolver that will be used for + // resolving ".subtree" files of implicit tilesets + // during the traversal + const resourceResolver = new TilesetSourceResourceResolver( + ".", + tilesetSource + ); + const tilesetTraverser = new TilesetTraverser(".", resourceResolver, { + depthFirst: false, + traverseExternalTilesets: true, + }); + await tilesetTraverser.traverseWithSchema( + tileset, + schema, + async (traversedTile) => { + await callback(traversedTile); + return true; + } + ); + } + + /** + * Apply the given callback to all entries that represent tile + * content, if they have not been processed yet. + * + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When an error is thrown during processing + */ + async forEachTileContentEntry( + callback: TilesetEntryProcessor + ): Promise { + await this.forEachTile( + async (traversedTile: TraversedTile): Promise => { + if (!traversedTile.isImplicitTilesetRoot()) { + const contentUris = traversedTile + .getFinalContents() + .map((c) => c.uri); + for (const contentUri of contentUris) { + await this.processEntryInternal(contentUri, callback); + } + } + } + ); + } + + /** + * Apply the given callback to each tile that appears as `Tile` + * object in the tileset JSON + * + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When an error is thrown during processing + */ + async forEachExplicitTile( + callback: (tile: Tile) => Promise + ): Promise { + const context = this.getContext(); + const tileset = context.tileset; + const root = tileset.root; + await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { + const tile = tilePath[tilePath.length - 1]; + await callback(tile); + return true; + }); + } + + /** + * Apply the given callback to all entries that represent the content + * of explicit tiles (i.e. tiles that appear as `Tile` objects in + * the tileset JSON), if they have not been processed yet. + * + * The tileset JSON will automatically be updated to take into account + * the result of the given callback: When the callback receives an + * entry with a certain `key` (file name), and returns an entry with + * a different `key`, then the `content.uri` will be updated + * accordingly (also taking into account whether the content was + * deleted to split into multiple contents). + * + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When an error is thrown during processing + */ + async forEachExplicitTileContentEntry( + callback: TilesetEntryProcessor + ): Promise { + await this.forEachExplicitTile(async (tile: Tile) => { + await this.processExplicitTileContentEntries(tile, callback); + }); + } + + /** + * Process all entries that are content of the given tile, and + * update the tile content to reflect the changes from the + * callback (see `forEachExplicitTileContentEntry` and + * `updateTileContent`) + * + * @param tile - The tile + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When the input could not be processed + */ + private async processExplicitTileContentEntries( + tile: Tile, + callback: TilesetEntryProcessor + ): Promise { + // For roots of implicit tilesets, the content URI + // is a template URI (i.e. they are not explicit, + // and therefore not considered here) + if (tile.implicitTiling) { + return; + } + if (tile.content) { + const content = tile.content; + const targetEntries = await this.processEntryInternal( + content.uri, + callback + ); + BasicTilesetProcessor.updateTileContent(tile, targetEntries); + } else if (tile.contents) { + const allTargetEntries = []; + for (const content of tile.contents) { + const targetEntries = await this.processEntryInternal( + content.uri, + callback + ); + allTargetEntries.push(...targetEntries); + } + BasicTilesetProcessor.updateTileContent(tile, allTargetEntries); + } + } + + /** + * Update the content of the given tile to reflect the given entries. + * + * When the given entries are empty, then the `content` and `contents` + * of the given tile will be deleted. + * + * When there is one entry, then the `content` of the given tile will + * receive the `key` (file name) of this entry as the content `uri`. + * + * When there are multiple entries, the tile will receive `contents` + * where each content `uri` is one `key` file name of the entries. + * + * @param tile - The tile + * @param targetEntries - The target entries + */ + private static updateTileContent(tile: Tile, targetEntries: TilesetEntry[]) { + if (targetEntries.length === 0) { + delete tile.content; + delete tile.contents; + return; + } + if (targetEntries.length === 1) { + const targetEntry = targetEntries[0]; + if (tile.content) { + tile.content.uri = targetEntry.key; + } else { + const content = { + uri: targetEntry.key, + }; + tile.content = content; + delete tile.contents; + } + } + + const newContents: Content[] = []; + for (const targetEntry of targetEntries) { + const content = { + uri: targetEntry.key, + }; + newContents.push(content); + } + tile.contents = newContents; + delete tile.content; + } + + /** + * Applies the given callback to each `TilesetEntry` that has not + * yet been processed. + * + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When the input could not be processed + */ + async forEachEntry(callback: TilesetEntryProcessor) { + const context = this.getContext(); + const tilesetSource = context.tilesetSource; + const entries = TilesetSources.getEntries(tilesetSource); + for (const entry of entries) { + const key = entry.key; + await this.processEntryInternal(key, callback); + } + } + + /** + * Apply the given callback to the `Tileset` and the metadata + * schema. + * + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When an error is thrown during processing + */ + async forTileset( + callback: (tileset: Tileset, schema: Schema | undefined) => Promise + ) { + const context = this.getContext(); + const tileset = context.tileset; + const schema = context.schema; + await callback(tileset, schema); + } + + /** + * Creates a callback that receives a `Tile` object, and calls + * the given callback on each of its `Content` objects. + * + * @param callback - The callback for the content + * @returns The callback for the tile + */ + static callbackForEachContent( + callback: (content: Content) => Promise + ): (tile: Tile) => Promise { + return async (tile: Tile) => { + if (tile.content) { + const content = tile.content; + await callback(content); + } + if (tile.contents) { + for (const content of tile.contents) { + await callback(content); + } + } + }; + } +} diff --git a/src/tilesetProcessing/TilesetContentProcessor.ts b/src/tilesetProcessing/TilesetContentProcessor.ts deleted file mode 100644 index 377b31b1..00000000 --- a/src/tilesetProcessing/TilesetContentProcessor.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { DeveloperError } from "../base/DeveloperError"; - -import { TilesetSourceResourceResolver } from "../io/TilesetSourceResourceResolver"; - -import { Tile } from "../structure/Tile"; -import { Tileset } from "../structure/Tileset"; -import { Content } from "../structure/Content"; -import { Schema } from "../structure/Metadata/Schema"; - -import { TilesetEntry } from "../tilesetData/TilesetEntry"; - -import { TilesetTraverser } from "../traversal/TilesetTraverser"; - -import { Tiles } from "../tilesets/Tiles"; - -import { TilesetProcessor } from "./TilesetProcessor"; - -/** - * A base class for classes that can process the content of tiles of - * tilesets. - * - * It defines two abstract methods: The `processTileContentEntry` method - * may be overridden by subclasses to process each entry as necessary. - * - * The `processImplicitTilesetRootContent` method may be called to - * update the `content.uri` (i.e. the template URI) of tiles that are - * the roots of implicit tilesets, so that they reflect the modifications - * of key (file names) that are done in `processTileContentEntry`. - * - * Entries that are no tile content will be processed with the - * `processEntry` method, which is implemented as a no-op by - * default. - */ -export abstract class TilesetContentProcessor extends TilesetProcessor { - /** - * Creates a new instance - * - * @param quiet - Whether log messages should be omitted - */ - constructor(quiet?: boolean) { - super(quiet); - } - - /** {@inheritDoc TilesetProcessor.processTilesetInternal} */ - override async processTilesetInternal( - tileset: Tileset, - schema: Schema | undefined - ): Promise { - this.log(`Processing all tiles`); - await this.processAllTilesContentEntries(tileset, schema); - - this.log(`Processing all entries`); - await this.processEntries(); - - this.log(`Processing all implicit tileset roots`); - await this.processImplicitTilesetRoots(tileset); - - this.log(`Processing tileset JSON`); - await this.processTilesetJson(tileset, schema); - } - - /** - * Process all tiles that are roots of implicit tilesets. - * - * @param tileset - The tileset - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processImplicitTilesetRoots(tileset: Tileset): Promise { - const root = tileset.root; - await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { - const tile = tilePath[tilePath.length - 1]; - if (tile.implicitTiling) { - await this.processImplicitTilesetRoot(tile); - } - return true; - }); - } - - /** - * Process the given tile, which is a root of an implicit tileset. - * - * @param tile - The tile - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processImplicitTilesetRoot(tile: Tile): Promise { - if (tile.content) { - const content = tile.content; - const newContent = await this.processImplicitTilesetRootContent(content); - tile.content = newContent; - } else if (tile.contents) { - for (let i = 0; i < tile.contents.length; i++) { - const content = tile.contents[i]; - const newContent = await this.processImplicitTilesetRootContent( - content - ); - tile.contents[i] = newContent; - } - } - } - - /** - * Process all entries that are tile content (both of explicit - * and implicit tiles). - * - * @param tileset - The tileset - * @param schema - The optional metadata schema for the tileset - * @returns A promise that resolves when the process is finished - * @throws DeveloperError When the source is not opened - * @throws TilesetError When the input could not be processed - */ - private async processAllTilesContentEntries( - tileset: Tileset, - schema: Schema | undefined - ): Promise { - const tilesetSource = this.getTilesetSource(); - if (!tilesetSource) { - throw new DeveloperError("The source must be defined"); - } - - // Create the resource resolver that will be used for - // resolving ".subtree" files of implicit tilesets - // during the traversal - const resourceResolver = new TilesetSourceResourceResolver( - ".", - tilesetSource - ); - const tilesetTraverser = new TilesetTraverser(".", resourceResolver, { - depthFirst: false, - traverseExternalTilesets: true, - }); - await tilesetTraverser.traverseWithSchema( - tileset, - schema, - async (traversedTile) => { - if (!traversedTile.isImplicitTilesetRoot()) { - const contentUris = traversedTile - .getFinalContents() - .map((c) => c.uri); - for (const contentUri of contentUris) { - await this.processEntryInternal( - contentUri, - this.processTileContentEntry - ); - } - } - return true; - } - ); - } - - /** {@inheritDoc TilesetProcessor.processEntry} */ - protected override async processEntry( - sourceEntry: TilesetEntry, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type: string | undefined - ): Promise { - return [sourceEntry]; - } - - /** - * Process a single entry that represents the content of a tile. - * - * This is the main configuration point for this class: Implementors - * may override this method, perform arbitrary operations on the - * given entry, and return the result. - * - * (A no-op implementation of this method would be to just return an - * array that contains the given source entry as its only element) - * - * @param sourceEntry - The source entry - * @param type The content data type (see `ContentDataTypes`) - * @returns The target entries - */ - abstract processTileContentEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise; - - /** - * Process the `content` of a tile that is the root of an implicit tileset. - * - * Implementors may override this method, and modify the given content - * as necessary, to reflect modifications that may have been done in - * `processTileContentEntry`. - * - * (A no-op implementation of this method would be to just return - * the given content, as it is) - * - * @param sourceEntry - The source entry - * @param type The content data type (see `ContentDataTypes`) - * @returns The target entries - */ - abstract processImplicitTilesetRootContent( - content: Content - ): Promise; -} diff --git a/src/tilesetProcessing/TilesetExplicitContentProcessor.ts b/src/tilesetProcessing/TilesetExplicitContentProcessor.ts deleted file mode 100644 index 2f4b5bae..00000000 --- a/src/tilesetProcessing/TilesetExplicitContentProcessor.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Tile } from "../structure/Tile"; -import { Tileset } from "../structure/Tileset"; -import { Content } from "../structure/Content"; -import { Schema } from "../structure/Metadata/Schema"; - -import { TilesetEntry } from "../tilesetData/TilesetEntry"; - -import { Tiles } from "../tilesets/Tiles"; -import { TilesetProcessor } from "./TilesetProcessor"; - -/** - * A base class for classes that can process the content of - * explicit tiles of tilesets. - * - * The abstract `processExplicitTileContentEntry` method is the main - * configuration point: It may be overridden by subclasses to process - * each entry as necessary. - * - * Entries that are not explicit tile content will be processed with - * the `processEntry` method, which is implemented as a no-op by - * default. - */ -export abstract class TilesetExplicitContentProcessor extends TilesetProcessor { - /** - * Creates a new instance - * - * @param quiet - Whether log messages should be omitted - */ - constructor(quiet?: boolean) { - super(quiet); - } - - /** {@inheritDoc TilesetProcessor.processTilesetInternal} */ - override async processTilesetInternal( - tileset: Tileset, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - schema: Schema | undefined - ): Promise { - this.log(`Processing explicit tile content entries`); - await this.processExplicitTilesContentEntries(tileset); - - this.log(`Processing all entries`); - await this.processEntries(); - - this.log(`Processing tileset JSON`); - await this.processTilesetJson(tileset, schema); - } - - /** - * Process all entries that are tile content of explicit tiles. - * - * @param tileset - The tileset - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processExplicitTilesContentEntries( - tileset: Tileset - ): Promise { - const root = tileset.root; - await Tiles.traverseExplicit(root, async (tilePath: Tile[]) => { - const tile = tilePath[tilePath.length - 1]; - await this.processExplicitTileContentEntries(tile); - return true; - }); - } - - /** - * Process all entries that are content of the given tile. - * - * @param tile - The tile - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - private async processExplicitTileContentEntries(tile: Tile): Promise { - // For roots of implicit tilesets, the content URI - // is a template URI (i.e. they are not explicit, - // and therefore not considered here) - if (tile.implicitTiling) { - return; - } - if (tile.content) { - const content = tile.content; - const targetEntries = await this.processEntryInternal( - content.uri, - this.processExplicitTileContentEntry - ); - this.updateTileContent(tile, targetEntries); - } else if (tile.contents) { - const allTargetEntries = []; - for (const content of tile.contents) { - const targetEntries = await this.processEntryInternal( - content.uri, - this.processExplicitTileContentEntry - ); - allTargetEntries.push(...targetEntries); - } - this.updateTileContent(tile, allTargetEntries); - } - } - - /** - * Update the content of the given tile to reflect the given entries. - * - * When the given entries are empty, then the `content` and `contents` - * of the given tile will be deleted. - * - * When there is one entry, then the `content` of the given tile will - * receive the `key` (file name) of this entry as the content `uri`. - * - * When there are multiple entries, the tile will receive `contents` - * where each content `uri` is one `key` file name of the entries. - * - * @param tile - The tile - * @param targetEntries - The target entries - */ - private updateTileContent(tile: Tile, targetEntries: TilesetEntry[]) { - if (targetEntries.length === 0) { - delete tile.content; - delete tile.contents; - return; - } - if (targetEntries.length === 1) { - const targetEntry = targetEntries[0]; - if (tile.content) { - tile.content.uri = targetEntry.key; - } else { - const content = { - uri: targetEntry.key, - }; - tile.content = content; - delete tile.contents; - } - } - - const newContents: Content[] = []; - for (const targetEntry of targetEntries) { - const content = { - uri: targetEntry.key, - }; - newContents.push(content); - } - tile.contents = newContents; - delete tile.content; - } - - /** {@inheritDoc TilesetProcessor.processEntry} */ - protected override async processEntry( - sourceEntry: TilesetEntry, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - type: string | undefined - ): Promise { - return [sourceEntry]; - } - - /** - * Process a single entry that represents the content of an explicit tile. - * - * This is the main configuration point for this class: Implementors - * may override this method, perform arbitrary operations on the - * given entry, and return the result. - * - * (A no-op implementation of this method would be to just return an - * array that contains the given source entry as its only element) - * - * @param sourceEntry - The source entry - * @param type The content data type (see `ContentDataTypes`) - * @returns The target entries - */ - abstract processExplicitTileContentEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise; -} diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetProcessor.ts index d83ea602..d7009566 100644 --- a/src/tilesetProcessing/TilesetProcessor.ts +++ b/src/tilesetProcessing/TilesetProcessor.ts @@ -8,8 +8,6 @@ import { Tileset } from "../structure/Tileset"; import { Schema } from "../structure/Metadata/Schema"; import { TilesetError } from "../tilesetData/TilesetError"; -import { TilesetSource } from "../tilesetData/TilesetSource"; -import { TilesetTarget } from "../tilesetData/TilesetTarget"; import { TilesetTargets } from "../tilesetData/TilesetTargets"; import { TilesetSources } from "../tilesetData/TilesetSources"; import { TilesetEntry } from "../tilesetData/TilesetEntry"; @@ -17,18 +15,20 @@ import { TilesetEntry } from "../tilesetData/TilesetEntry"; import { Tilesets } from "../tilesets/Tilesets"; import { TilesetEntryProcessor } from "./TilesetEntryProcessor"; +import { TilesetProcessorContext } from "./TilesetProcessorContext"; +import { TilesetSource } from "../tilesetData/TilesetSource"; +import { TilesetTarget } from "../tilesetData/TilesetTarget"; /** * A base class for classes that can process tilesets. * - * This class offers a `process` method that receives a name - * of a `TilesetSource` and a `TilesetTarget`. It will open - * the source and the target, process all entries of the - * source, and put the results into the target. + * This class offers the infrastructure for opening a `TilesetSource` + * and a `TilesetTarget`, parsing the `Tileset` object from the + * source, and performing operations on `Tileset` and the + * `TilesetEntry` objects. * - * The abstract `processEntry` method is the main configuration - * point: It may be overridden by subclasses to process each - * entry as necessary. + * Subclasses will offer predefined sets of operations that can + * be performed on the `Tileset` and the `TilesetEntry` objects. */ export abstract class TilesetProcessor { /** @@ -37,21 +37,9 @@ export abstract class TilesetProcessor { private readonly logCallback: (message: any) => void; /** - * The tileset source for the input + * The context that was created in `begin` */ - private tilesetSource: TilesetSource | undefined; - - /** - * The tileset target for the output. - */ - private tilesetTarget: TilesetTarget | undefined; - - /** - * The set of keys (file names) that have already been processed. - * This includes the original keys, as well as new keys that - * have been assigned to entries while they have been processed. - */ - private processedKeys: { [key: string]: boolean } = {}; + private context: TilesetProcessorContext | undefined; /** * Creates a new instance @@ -68,175 +56,161 @@ export abstract class TilesetProcessor { } /** - * Returns the tileset source, or undefined when no source - * has been opened. + * Internal method to just call the log callback * - * @returns - The `TilesetSource` + * @param message - The message */ - protected getTilesetSource(): TilesetSource | undefined { - return this.tilesetSource; + protected log(message: any): void { + this.logCallback(message); } /** - * Internal method to just call the log callback + * Returns the `TilesetProcessorContext` that contains all + * elements that are required for processing the tileset * - * @param message - The message + * @returns The `TilesetProcessorContext` + * @throws DeveloperError If `begin` was not called yet */ - protected log(message: any): void { - this.logCallback(message); + protected getContext(): TilesetProcessorContext { + if (!this.context) { + throw new DeveloperError( + "The processor was not initialized. Call 'begin' first." + ); + } + return this.context; } /** - * Process the specified source tileset, and write it to the given - * target. + * Prepare processing the given tileset source and writing + * the results into the given tileset target. * * @param tilesetSourceName - The tileset source name * @param tilesetTargetName - The tileset target name * @param overwrite Whether the target should be overwritten if * it already exists - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed, + * @returns A promise that resolves when this processor has been + * initialized + * @throws TilesetError When the input could not be opened, * or when the output already exists and `overwrite` was `false`. */ - async process( + async begin( tilesetSourceName: string, tilesetTargetName: string, overwrite: boolean ): Promise { - // TODO Somehow ensure that the source is closed - // if the target throws up (try-with-resources FTW) - const tilesetSource = TilesetSources.createAndOpen(tilesetSourceName); - const tilesetTarget = TilesetTargets.createAndBegin( - tilesetTargetName, - overwrite - ); - - this.tilesetSource = tilesetSource; - this.tilesetTarget = tilesetTarget; - - const tilesetSourceJsonFileName = - Tilesets.determineTilesetJsonFileName(tilesetSourceName); - - const tilesetTargetJsonFileName = - Tilesets.determineTilesetJsonFileName(tilesetTargetName); - - await this.processInternal( - tilesetSourceJsonFileName, - tilesetTargetJsonFileName - ); - - tilesetSource.close(); - await tilesetTarget.end(); - - this.tilesetSource = undefined; - this.tilesetTarget = undefined; - Object.keys(this.processedKeys).forEach( - (key) => delete this.processedKeys[key] - ); + let tilesetSource; + let tilesetTarget; + try { + tilesetSource = TilesetSources.createAndOpen(tilesetSourceName); + tilesetTarget = TilesetTargets.createAndBegin( + tilesetTargetName, + overwrite + ); + + const tilesetSourceJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetSourceName); + + const tilesetTargetJsonFileName = + Tilesets.determineTilesetJsonFileName(tilesetTargetName); + + // Obtain the tileset object from the tileset JSON file + const parsedTileset = TilesetProcessor.parseSourceValue( + tilesetSource, + tilesetSourceJsonFileName + ); + + // Resolve the schema, either from the `tileset.schema` + // or the `tileset.schemaUri` + const schema = TilesetProcessor.resolveSchema( + tilesetSource, + parsedTileset.result + ); + + // If nothing has thrown up to this point, then + // a `TilesetProcessorContext` with a valid + // state can be created: + this.context = { + tilesetSource: tilesetSource, + tilesetSourceJsonFileName: tilesetSourceJsonFileName, + tileset: parsedTileset.result, + tilesetJsonWasZipped: parsedTileset.wasZipped, + schema: schema, + tilesetTarget: tilesetTarget, + tilesetTargetJsonFileName: tilesetTargetJsonFileName, + processedKeys: {}, + }; + } catch (error) { + if (tilesetSource) { + try { + tilesetSource.close(); + } catch (e) { + // Error already about to be re-thrown + } + } + if (tilesetTarget) { + try { + await tilesetTarget.end(); + } catch (e) { + // Error already about to be re-thrown + } + } + delete this.context; + throw error; + } } /** - * Internal (top-level) method for the processing. - * - * It reads the tileset JSON from the specified source, passes - * it to `processTileset`, and writes the tileset JSON to the - * specified target. - * - * Any operations that affect files other than the tileset JSON - * file are part of `processTileset` + * Finish processing the source tileset and write all entries + * that have not been processed yet into the target. * - * @param tilesetSourceName - The tileset source name - * @param tilesetTargetName - The tileset target name - * @returns A promise that resolves when the process is finished - * @throws DeveloperError When the source or target is not opened - * @throws TilesetError When the input could not be processed + * @returns A promise that resolves when the operation finished + * @throws TilesetError When there was an error while processing + * or storing the entries. */ - private async processInternal( - tilesetSourceJsonFileName: string, - tilesetTargetJsonFileName: string - ): Promise { - if (!this.tilesetSource || !this.tilesetTarget) { - throw new DeveloperError("The source and target must be defined"); + async end() { + const context = this.getContext(); + const tilesetSource = context.tilesetSource; + const tilesetTarget = context.tilesetTarget; + + // Perform a no-op on all entries that have not yet + // been marked as processed + const entries = TilesetSources.getEntries(tilesetSource); + for (const entry of entries) { + const key = entry.key; + await this.processEntryInternal( + key, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (sourceEntry: TilesetEntry, type: string | undefined) => { + return [sourceEntry]; + } + ); } - // Obtain the tileset object from the tileset JSON file - const parsedTileset = this.parseSourceValue( - tilesetSourceJsonFileName - ); - - // Resolve the schema, either from the `tileset.schema` - // or the `tileset.schemaUri` - const schema = this.resolveSchema(parsedTileset.result); - - // Process the actual tileset - await this.processTilesetInternal(parsedTileset.result, schema); + const tilesetTargetJsonFileName = context.tilesetTargetJsonFileName; + const tileset = context.tileset; + const tilesetJsonWasZipped = context.tilesetJsonWasZipped; // Store the resulting tileset as JSON - this.storeTargetValue( + TilesetProcessor.storeTargetValue( + tilesetTarget, tilesetTargetJsonFileName, - parsedTileset.wasZipped, - parsedTileset.result + tilesetJsonWasZipped, + tileset ); - } - - /** - * Process the given tileset. - * - * This will just call `processEntries` and `processTileset`, - * where the latter serves as a point where implementors may - * perform modifications to the tileset JSON. - * - * @param tileset - The tileset - * @param schema - The optional metadata schema for the tileset - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - protected async processTilesetInternal( - tileset: Tileset, - schema: Schema | undefined - ): Promise { - await this.processEntries(); - await this.processTilesetJson(tileset, schema); - } - - /** - * Process the given tileset. - * - * Implementors may modify the given `Tileset`. The result - * will be written into the target, after all entries have - * been processed. - * - * @param tileset - The tileset - * @param schema - The optional metadata schema for the tileset - * @returns A promise that resolves when the process is finished - * @throws TilesetError When the input could not be processed - */ - protected async processTilesetJson( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - tileset: Tileset, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - schema: Schema | undefined - ): Promise { - // No-op - } - /** - * Process all entries that are contained in the current - * tileset source. - * - * @returns A promise that resolves when the process is finished - * @throws DeveloperError When the source or target is not opened - * @throws TilesetError When the input could not be processed - */ - protected async processEntries(): Promise { - if (!this.tilesetSource || !this.tilesetTarget) { - throw new DeveloperError("The source and target must be defined"); - } - const entries = TilesetSources.getEntries(this.tilesetSource); - for (const entry of entries) { - const key = entry.key; - await this.processEntryInternal(key, this.processEntry); + // Clean up by closing the source and the target + delete this.context; + try { + tilesetSource.close(); + } catch (error) { + try { + await tilesetTarget.end(); + } catch (e) { + // Error already about to be re-thrown + } + throw error; } + await tilesetTarget.end(); } /** @@ -247,14 +221,14 @@ export abstract class TilesetProcessor { * * Otherwise, the specified entry will be looked up in the tileset * source. Its content type will be determined. The source entry - * will be passed to `processEntry`, which returns the target + * will be passed to the given processor, which returns the target * entries that will be inserted into the tileset target. * * @param key - The key (file name) of the entry * @param entryProcessor - The `TilesetEntryProcessor` that will * be called to process the actual entry. * @returns A promise that resolves when the process is finished, - * containing either the resulting entries + * containing the resulting entries * @throws DeveloperError When the source or target is not opened * @throws TilesetError When the input could not be processed */ @@ -262,9 +236,9 @@ export abstract class TilesetProcessor { key: string, entryProcessor: TilesetEntryProcessor ): Promise { - if (!this.tilesetSource || !this.tilesetTarget) { - throw new DeveloperError("The source and target must be defined"); - } + const context = this.getContext(); + const tilesetSource = context.tilesetSource; + const tilesetTarget = context.tilesetTarget; const sourceKey = key; if (this.isProcessed(sourceKey)) { @@ -272,7 +246,7 @@ export abstract class TilesetProcessor { } this.markAsProcessed(sourceKey); - const sourceValue = this.tilesetSource.getValue(sourceKey); + const sourceValue = tilesetSource.getValue(sourceKey); if (!sourceValue) { const message = `No ${sourceKey} found in input`; throw new TilesetError(message); @@ -291,7 +265,7 @@ export abstract class TilesetProcessor { if (targetEntries) { for (const targetEntry of targetEntries) { - this.tilesetTarget.addEntry(targetEntry.key, targetEntry.value); + tilesetTarget.addEntry(targetEntry.key, targetEntry.value); this.markAsProcessed(targetEntry.key); } } @@ -299,33 +273,26 @@ export abstract class TilesetProcessor { } /** - * Process a single entry. - * - * This is the main configuration point for this class: Implementors - * may override this method, perform arbitrary operations on the - * given entry, and return the result. - * - * (A no-op implementation of this method would be to just return an - * array that contains the given source entry as its only element) + * Store the given entry in the current target * - * @param sourceEntry - The source entry - * @param type The content data type (see `ContentDataTypes`) - * @returns The target entries + * @param targetEntry - The target entry */ - protected abstract processEntry( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise; + storeTargetEntry(targetEntry: TilesetEntry) { + const context = this.getContext(); + const tilesetTarget = context.tilesetTarget; + tilesetTarget.addEntry(targetEntry.key, targetEntry.value); + this.markAsProcessed(targetEntry.key); + } /** - * A method that can be called by implementations, to mark a certain - * file as already having been processed, and no longer be considered - * in subsequent steps. + * Mark a certain entry (file) as already having been processed, + * and no longer be considered in subsequent steps. * * @param key - The key (file name) */ protected markAsProcessed(key: string) { - this.processedKeys[key] = true; + const context = this.getContext(); + context.processedKeys[key] = true; } /** @@ -335,8 +302,9 @@ export abstract class TilesetProcessor { * @param key - The key (file name) * @returns Whether the entry was already processed */ - protected isProcessed(key: string) { - return this.processedKeys[key] === true; + protected isProcessed(key: string): boolean { + const context = this.getContext(); + return context.processedKeys[key] === true; } /** @@ -373,6 +341,7 @@ export abstract class TilesetProcessor { * In the future, there might be mechanisms for a more fine-grained * control over whether certain files should be zipped or not... * + * @param tilesetSource - The `TilesetSource` * @param key - The key (file name) * @returns A structure containing the `wasZipped` information, and * the parsed result @@ -380,8 +349,11 @@ export abstract class TilesetProcessor { * entry cannot be found, or the entry data could not be unzipped, * or its contents could not be parsed as JSON. */ - private parseSourceValue(key: string): { wasZipped: boolean; result: T } { - let value = this.getSourceValue(key); + private static parseSourceValue( + tilesetSource: TilesetSource, + key: string + ): { wasZipped: boolean; result: T } { + let value = TilesetProcessor.getSourceValue(tilesetSource, key); let wasZipped = false; if (Buffers.isGzipped(value)) { wasZipped = true; @@ -413,21 +385,24 @@ export abstract class TilesetProcessor { * JSON data, and is the counterpart of `parseSourceValue`. See * `parseSourceValue` for details. * + * @param tilesetTarget - The `TilesetTarget` * @param key - The key (file name) * @param doZip - Whether the output should be zipped * @param object - The object for which the JSON should be stored * @throws DeveloperError When the target is not opened */ - private storeTargetValue(key: string, doZip: boolean, object: object) { - if (!this.tilesetTarget) { - throw new DeveloperError("The target must be defined"); - } + private static storeTargetValue( + tilesetTarget: TilesetTarget, + key: string, + doZip: boolean, + object: object + ) { const jsonString = JSON.stringify(object, null, 2); let jsonBuffer = Buffer.from(jsonString); if (doZip) { jsonBuffer = Buffers.gzip(jsonBuffer); } - this.tilesetTarget.addEntry(key, jsonBuffer); + tilesetTarget.addEntry(key, jsonBuffer); } /** @@ -435,16 +410,17 @@ export abstract class TilesetProcessor { * throwing an error if the source is not opened, or when the * given key cannot be found. * + * @param tilesetSource - The `TilesetSource` * @param key - The key (file name) * @returns The value (file contents) * @throws DeveloperError When the source is not opened * @throws TilesetError When the given key cannot be found */ - private getSourceValue(key: string): Buffer { - if (!this.tilesetSource) { - throw new DeveloperError("The source must be defined"); - } - const buffer = this.tilesetSource.getValue(key); + private static getSourceValue( + tilesetSource: TilesetSource, + key: string + ): Buffer { + const buffer = tilesetSource.getValue(key); if (!buffer) { const message = `No ${key} found in input`; throw new TilesetError(message); @@ -459,21 +435,25 @@ export abstract class TilesetProcessor { * obtained from the `tileset.schemaUri`, or `undefined` if * neither of them are present. * + * @param tilesetSource - The `TilesetSource` * @param tileset - The tileset * @returns The `Schema`, or `undefined` if there is none * @throws DeveloperError If the source is not opened * @throws TilesetError If the schema from the `schemaUri` * could not be resolved or parsed. */ - private resolveSchema(tileset: Tileset): Schema | undefined { - if (!this.tilesetSource) { - throw new DeveloperError("The source must be defined"); - } + private static resolveSchema( + tilesetSource: TilesetSource, + tileset: Tileset + ): Schema | undefined { if (tileset.schema) { return tileset.schema; } if (tileset.schemaUri) { - const parsedSchema = this.parseSourceValue(tileset.schemaUri); + const parsedSchema = TilesetProcessor.parseSourceValue( + tilesetSource, + tileset.schemaUri + ); return parsedSchema.result; } return undefined; diff --git a/src/tilesetProcessing/TilesetProcessorContext.ts b/src/tilesetProcessing/TilesetProcessorContext.ts new file mode 100644 index 00000000..97fd4ffe --- /dev/null +++ b/src/tilesetProcessing/TilesetProcessorContext.ts @@ -0,0 +1,59 @@ +import { Schema } from "../structure/Metadata/Schema"; +import { Tileset } from "../structure/Tileset"; + +import { TilesetSource } from "../tilesetData/TilesetSource"; +import { TilesetTarget } from "../tilesetData/TilesetTarget"; + +/** + * A class summarizing the data that a `TilesetProcessor` is operating on. + * + * This is initialized during the `TilesetProcessor.begin` call, if all + * the source- and target information could be resolved, and is supposed + * to represent a consistent, properly initialized state to work on. + */ +export interface TilesetProcessorContext { + /** + * The tileset source for the input + */ + tilesetSource: TilesetSource; + + /** + * The name of the file that contains the tileset JSON + * data in the source (usually `tileset.json`) + */ + tilesetSourceJsonFileName: string; + + /** + * The tileset that was parsed from the input + */ + tileset: Tileset; + + /** + * Whether the tileset JSON was zipped (a legacy feature, + * see `TilesetProcessor.parseSourceValue` for details) + */ + tilesetJsonWasZipped: boolean; + + /** + * The optional metadata schema associated with the tileset + */ + schema: Schema | undefined; + + /** + * The tileset target for the output. + */ + tilesetTarget: TilesetTarget; + + /** + * The name of the file that contains the tileset JSON + * data in the target (usually `tileset.json`) + */ + tilesetTargetJsonFileName: string; + + /** + * The set of keys (file names) that have already been processed. + * This includes the original keys, as well as new keys that + * have been assigned to entries while they have been processed. + */ + processedKeys: { [key: string]: boolean }; +} diff --git a/src/tilesets/Extensions.ts b/src/tilesets/Extensions.ts index eaf27351..9f01d677 100644 --- a/src/tilesets/Extensions.ts +++ b/src/tilesets/Extensions.ts @@ -1,5 +1,12 @@ -import { RootProperty } from "../structure/RootProperty"; -import { Tileset } from "../structure/Tileset"; +/** + * A type for objects that can contain extensions + */ +type Extended = { extensions?: object }; + +/** + * A type for objects that can contain extension declarations + */ +type Extensible = { extensionsUsed?: any; extensionsRequired?: any }; /** * Utility methods for handling extensions @@ -11,81 +18,81 @@ export class Extensions { * That is, whether the `object.extensions` contains a key * that is the given extension name. * - * @param rootProperty - The object that may contain the extension + * @param extensible - The object that may contain the extension * @param extension The extension (i.e. its name as a string) * @returns Whether the object contains the extension */ - static contains(rootProperty: RootProperty, extension: string) { - if (!rootProperty.extensions) { + static contains(extended: Extended, extension: string) { + if (!extended.extensions) { return false; } - return Object.keys(rootProperty.extensions).includes(extension); + return Object.keys(extended.extensions).includes(extension); } /** - * Add the given extension to the `extensionsUsed` of the given tileset. + * Add the given extension to the `extensionsUsed` of the given object. * * The extension will be added if it was not yet contained in the * array, creating the array of necessary. * - * @param tileset - The tileset + * @param extensible - The object * @param extension - The extension name */ - static addExtensionUsed(tileset: Tileset, extension: string) { - tileset.extensionsUsed = Extensions.addUnique( - tileset.extensionsUsed, + static addExtensionUsed(extensible: Extensible, extension: string) { + extensible.extensionsUsed = Extensions.addUnique( + extensible.extensionsUsed, extension ); } /** - * Remove the given extension from the `extensionsUsed` of the given tileset. + * Remove the given extension from the `extensionsUsed` of the given object. * * The array will be set to `undefined` if it becomes empty, and the * extension will also be removed from `extensionsRequired`. * - * @param tileset - The tileset + * @param extensible - The object * @param extension - The extension name */ - static removeExtensionUsed(tileset: Tileset, extension: string) { - tileset.extensionsUsed = Extensions.removeUnique( - tileset.extensionsUsed, + static removeExtensionUsed(extensible: Extensible, extension: string) { + extensible.extensionsUsed = Extensions.removeUnique( + extensible.extensionsUsed, extension ); - Extensions.removeExtensionRequired(tileset, extension); + Extensions.removeExtensionRequired(extensible, extension); } /** - * Add the given extension to the `extensionsRequired` of the given tileset. + * Add the given extension to the `extensionsRequired` of the given object. * * The extension will be added if it was not yet contained in the * array, creating the array of necessary. This will also add * the extension to `extensionsUsed`. * - * @param tileset - The tileset + * @param extensible - The object * @param extension - The extension name */ - static addExtensionRequired(tileset: Tileset, extension: string) { - tileset.extensionsRequired = Extensions.addUnique( - tileset.extensionsRequired, + static addExtensionRequired(extensible: Extensible, extension: string) { + extensible.extensionsRequired = Extensions.addUnique( + extensible.extensionsRequired, extension ); - Extensions.addExtensionUsed(tileset, extension); + Extensions.addExtensionUsed(extensible, extension); } /** - * Remove the given extension to the `extensionsUsed` of the given tileset. + * Remove the given extension to the `extensionsUsed` of the given object. * * The array will be set to `undefined` if it becomes empty. * * This will *not* remove the extension from the `extensionsUsed`! * - * @param tileset - The tileset + * @param extensible - The object * @param extension - The extension name */ - static removeExtensionRequired(tileset: Tileset, extension: string) { - tileset.extensionsRequired = Extensions.removeUnique( - tileset.extensionsRequired, + static removeExtensionRequired(extensible: Extensible, extension: string) { + extensible.extensionsRequired = Extensions.removeUnique( + extensible.extensionsRequired, extension ); } From 1248d5b07a01315e5854eeaca22afcfdc923068d Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 2 Apr 2023 15:27:27 +0200 Subject: [PATCH 31/60] Fix minor typos in demos --- demos/TilesetProcessorExamples.ts | 2 +- demos/TraversalStatsDemo.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/demos/TilesetProcessorExamples.ts b/demos/TilesetProcessorExamples.ts index 6e518a61..eb14ccc1 100644 --- a/demos/TilesetProcessorExamples.ts +++ b/demos/TilesetProcessorExamples.ts @@ -28,7 +28,7 @@ async function example() { } ); - // Apply a callback to each (explicit) `Tile + // Apply a callback to each (explicit) `Tile` await tilesetProcessor.forEachExplicitTile( async (tile: Tile): Promise => { console.log("In forEachExplicitTile"); diff --git a/demos/TraversalStatsDemo.ts b/demos/TraversalStatsDemo.ts index 704a3beb..c636bfb2 100644 --- a/demos/TraversalStatsDemo.ts +++ b/demos/TraversalStatsDemo.ts @@ -53,7 +53,7 @@ async function tilesetTraversalDemo(filePath: string) { statsCollector.increment("totalNumberOfTiles"); const subtreeUri = traversedTile.getSubtreeUri(); if (subtreeUri !== undefined) { - statsCollector.increment("totalNumberOfSubtres"); + statsCollector.increment("totalNumberOfSubtrees"); } if (!traversedTile.isImplicitTilesetRoot()) { // Obtain all content URIs, resolve the associated data, From faddadd047cd2910c6aef1e817164808612346e8 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 2 Apr 2023 15:42:52 +0200 Subject: [PATCH 32/60] Renamed ImplicitTilingDemos to SpatialDemos --- demos/{ImplicitTilingDemos.ts => SpatialDemos.ts} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename demos/{ImplicitTilingDemos.ts => SpatialDemos.ts} (90%) diff --git a/demos/ImplicitTilingDemos.ts b/demos/SpatialDemos.ts similarity index 90% rename from demos/ImplicitTilingDemos.ts rename to demos/SpatialDemos.ts index 816e8a6a..5c4e620b 100644 --- a/demos/ImplicitTilingDemos.ts +++ b/demos/SpatialDemos.ts @@ -1,7 +1,8 @@ -import { Quadtrees } from "../src/implicitTiling/Quadtrees"; +import { Quadtrees } from "../src/spatial/Quadtrees"; +import { QuadtreeCoordinates } from "../src/spatial/QuadtreeCoordinates"; +import { OctreeCoordinates } from "../src/spatial/OctreeCoordinates"; + import { TemplateUris } from "../src/implicitTiling/TemplateUris"; -import { QuadtreeCoordinates } from "../src/implicitTiling/QuadtreeCoordinates"; -import { OctreeCoordinates } from "../src/implicitTiling/OctreeCoordinates"; /** * A basic demo of the `QuadtreeCoordinates.children` method From 364a40295e1c2d766e6bea84423b9b909ef3da20 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 2 Apr 2023 15:46:02 +0200 Subject: [PATCH 33/60] Update demo for renamed directory --- demos/SubtreeInfoDemos.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/SubtreeInfoDemos.ts b/demos/SubtreeInfoDemos.ts index f83b6d0e..2116426e 100644 --- a/demos/SubtreeInfoDemos.ts +++ b/demos/SubtreeInfoDemos.ts @@ -4,7 +4,7 @@ import { readJsonUnchecked } from "./readJsonUnchecked"; import { ResourceResolvers } from "../src/io/ResourceResolvers"; -import { QuadtreeCoordinates } from "../src/implicitTiling/QuadtreeCoordinates"; +import { QuadtreeCoordinates } from "../src/spatial/QuadtreeCoordinates"; import { SubtreeInfos } from "../src/implicitTiling/SubtreeInfos"; async function testSubtreeInfo() { From f95f152505226cd769ac819de2debd53aab99a97 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 3 Apr 2023 20:05:03 +0200 Subject: [PATCH 34/60] Refactoring for pipelines with TilesetProcessor More fine-grained functions in TilesetProcessor and BasicTilesetProcessor, with some convenience functions on top of them, used to implement the content stage execution of pipelines. --- src/pipelines/ContentStageExecutor.ts | 277 +++++++++++----- src/pipelines/PipelineError.ts | 19 ++ src/pipelines/PipelineExecutor.ts | 2 +- src/pipelines/TilesetStageExecutor.ts | 4 +- .../BasicTilesetProcessor.ts | 304 ++++++++++-------- .../TilesetEntryProcessor.ts | 12 +- src/tilesetProcessing/TilesetProcessor.ts | 139 +++++--- src/tilesets/Tiles.ts | 34 +- src/traversal/TilesetTraverser.ts | 46 ++- 9 files changed, 547 insertions(+), 290 deletions(-) create mode 100644 src/pipelines/PipelineError.ts diff --git a/src/pipelines/ContentStageExecutor.ts b/src/pipelines/ContentStageExecutor.ts index b754bafc..8c77b7d3 100644 --- a/src/pipelines/ContentStageExecutor.ts +++ b/src/pipelines/ContentStageExecutor.ts @@ -8,12 +8,18 @@ import { ContentDataTypes } from "../contentTypes/ContentDataTypes"; import { TilesetEntry } from "../tilesetData/TilesetEntry"; import { ContentStage } from "./ContentStage"; +import { PipelineError } from "./PipelineError"; import { BasicTilesetProcessor } from "../tilesetProcessing/BasicTilesetProcessor"; import { GltfUtilities } from "../contentProcessing/GtlfUtilities"; import { ContentOps } from "../contentProcessing/ContentOps"; +import { Tile } from "../structure/Tile"; + +import { TraversedTile } from "../traversal/TraversedTile"; +import { Tiles } from "../tilesets/Tiles"; + /** * Methods to execute `ContentStage` objects. */ @@ -24,12 +30,35 @@ export class ContentStageExecutor { * @param contentStage - The `ContentStage` object * @param tilesetProcessor The `BasicTilesetProcessor` * @returns A promise that resolves when the process is finished - * @throws TilesetError If one of the processing steps causes + * @throws PipelineError If one of the processing steps causes * an error. */ static async executeContentStage( contentStage: ContentStage, tilesetProcessor: BasicTilesetProcessor + ) { + try { + await ContentStageExecutor.executeContentStageInternal( + contentStage, + tilesetProcessor + ); + } catch (e) { + throw new PipelineError(`${e}`); + } + } + + /** + * Execute the given `ContentStage`. + * + * @param contentStage - The `ContentStage` object + * @param tilesetProcessor The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ + private static async executeContentStageInternal( + contentStage: ContentStage, + tilesetProcessor: BasicTilesetProcessor ) { if (contentStage.name === "gzip") { await ContentStageExecutor.executeGzip( @@ -61,7 +90,7 @@ export class ContentStageExecutor { * @param tilesetProcessor - The `BasicTilesetProcessor` * @param condition The condition from the `ContentStage` * @returns A promise that resolves when the process is finished - * @throws TilesetError If one of the processing steps causes + * @throws Error If one of the processing steps causes * an error. */ private static async executeGzip( @@ -82,7 +111,7 @@ export class ContentStageExecutor { key: sourceEntry.key, value: targetValue, }; - return [targetEntry]; + return targetEntry; } ); } @@ -96,7 +125,7 @@ export class ContentStageExecutor { * * @param tilesetProcessor - The `BasicTilesetProcessor` * @returns A promise that resolves when the process is finished - * @throws TilesetError If one of the processing steps causes + * @throws Error If one of the processing steps causes * an error. */ private static async executeGunzip( @@ -109,53 +138,128 @@ export class ContentStageExecutor { key: sourceEntry.key, value: Buffers.gunzip(sourceEntry.value), }; - return [targetEntry]; + return targetEntry; } ); } + /** + * Performs the 'b3dmToGlb' content stage with the given processor. + * + * This will process all tile contents entries of the source tileset + * that have the `CONTENT_TYPE_B3DM`. These entries will be replaced + * by entries that contain the GLB data from the B3DM. + * + * If the entries have names that end in `.b3dm`, then these + * extensions will be changed to `.glb`. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ private static async executeB3dmToGlb( tilesetProcessor: BasicTilesetProcessor ): Promise { - await tilesetProcessor.forEachExplicitTileContentEntry( - async (sourceEntry: TilesetEntry, type: string | undefined) => { - if (type !== ContentDataTypes.CONTENT_TYPE_B3DM) { - return [sourceEntry]; + // Define the rule for updating the key (file name) of + // the entries, as well as possible template URIs of + // implicit tileset roots. + const updateUri = (uri: string) => { + if (Paths.hasExtension(uri, ".b3dm")) { + return Paths.replaceExtension(uri, ".glb"); + } + return uri; + }; + + // Define the `TilesetEntryProcessor` that generates an + // entry with GLB data from an entry with B3DM data. + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_B3DM) { + return sourceEntry; + } + const targetEntry = { + key: updateUri(sourceEntry.key), + value: ContentOps.b3dmToGlbBuffer(sourceEntry.value), + }; + return targetEntry; + }; + + // Traverse the (explicit) tiles of the input tileset + await tilesetProcessor.forEachExplicitTile( + async (tile: Tile): Promise => { + // When the tile is not an implicit tiling root, + // then just update the entries that correspond + // to the tile contents. + if (!tile.implicitTiling) { + tilesetProcessor.processTileContentEntries(tile, entryProcessor); + } else { + // For implicit tiling roots, traverse the implicit tile hierarchy + // that starts at this tile, and process each entry that corresponds + // to the content of one of the implicit tiles. + await tilesetProcessor.forEachTileAt( + tile, + async (traversedTile: TraversedTile) => { + await tilesetProcessor.processTraversedTileContentEntries( + traversedTile, + entryProcessor + ); + } + ); + + // After the traversal, update the content URIs of the + // implicit tiling root (which are template URIs) + const contents = Tiles.getContents(tile); + for (const content of contents) { + content.uri = updateUri(content.uri); + } } - const sourceKey = sourceEntry.key; - const targetKey = Paths.replaceExtension(sourceKey, ".glb"); - const sourceValue = sourceEntry.value; - const targetValue = ContentOps.b3dmToGlbBuffer(sourceValue); - const targetEntry = { - key: targetKey, - value: targetValue, - }; - return [targetEntry]; } ); } + /** + * Performs the 'optimizeGlb' content stage with the given processor. + * + * This will process all tile contents entries of the source tileset + * that have the `CONTENT_TYPE_GLB`, and apply the `gltf-pipeline` + * optimization with the given options to them. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @param options - The options for `gltf-pipeline` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ private static async executeOptimizeGlb( tilesetProcessor: BasicTilesetProcessor, options: any ): Promise { - await tilesetProcessor.forEachExplicitTileContentEntry( - async (sourceEntry: TilesetEntry, type: string | undefined) => { - if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { - return [sourceEntry]; - } - const sourceValue = sourceEntry.value; - const targetValue = await GltfUtilities.optimizeGlb( - sourceValue, - options - ); - const targetEntry = { - key: sourceEntry.key, - value: targetValue, - }; - return [targetEntry]; + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { + return sourceEntry; } - ); + const targetValue = await GltfUtilities.optimizeGlb( + sourceEntry.value, + options + ); + const targetEntry = { + key: sourceEntry.key, + value: targetValue, + }; + return targetEntry; + }; + await tilesetProcessor.forEachTile(async (traversedTile: TraversedTile) => { + await tilesetProcessor.processTraversedTileContentEntries( + traversedTile, + entryProcessor + ); + }); } /** @@ -163,66 +267,77 @@ export class ContentStageExecutor { * * @param tilesetProcessor - The `BasicTilesetProcessor` * @returns A promise that resolves when the process is finished - * @throws TilesetError If one of the processing steps causes + * @throws PipelineError If one of the processing steps causes * an error. */ private static async executeSeparateGltf( tilesetProcessor: BasicTilesetProcessor ): Promise { - await tilesetProcessor.forEachExplicitTileContentEntry( - async (sourceEntry: TilesetEntry, type: string | undefined) => { - if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { - return [sourceEntry]; - } - const options = { - separate: true, - name: sourceEntry.key, - }; - const gltfPipelineResults = await GltfPipeline.glbToGltf( - sourceEntry.value, - options - ); - const targetKey = Paths.replaceExtension(sourceEntry.key, ".gltf"); - const targetValue = Buffer.from( - JSON.stringify(gltfPipelineResults.gltf) - ); - const targetEntry = { - key: targetKey, - value: targetValue, - }; - for (const resourceKey of Object.keys( - gltfPipelineResults.separateResources - )) { - const resourceValue = - gltfPipelineResults.separateResources[resourceKey]; - const resourceTargetEntry = { - key: resourceKey, - value: resourceValue, - }; - tilesetProcessor.storeTargetEntry(resourceTargetEntry); - } - return [targetEntry]; + const updateUri = (uri: string) => { + if (Paths.hasExtension(uri, ".glb")) { + return Paths.replaceExtension(uri, ".gltf"); } - ); + return uri; + }; - // TODO This has to be done for the implicit case: - /* - const templateUriUpdateCallback = - BasicTilesetProcessor.callbackForEachContent( - async (content: Content): Promise => { - if (content.uri.toLowerCase().endsWith(".glb")) { - content.uri = Paths.replaceExtension(content.uri, ".gltf"); - } - } + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { + return sourceEntry; + } + const options = { + separate: true, + name: Paths.replaceExtension(sourceEntry.key, ""), + }; + const gltfPipelineResults = await GltfPipeline.glbToGltf( + sourceEntry.value, + options ); + const targetValue = Buffer.from(JSON.stringify(gltfPipelineResults.gltf)); + const targetEntry = { + key: updateUri(sourceEntry.key), + value: targetValue, + }; + + const separateResources = gltfPipelineResults.separateResources; + const resourceKeys = Object.keys(separateResources); + for (const resourceKey of resourceKeys) { + const resourceValue = separateResources[resourceKey]; + const resourceTargetEntry = { + key: resourceKey, + value: resourceValue, + }; + tilesetProcessor.storeTargetEntries(resourceTargetEntry); + tilesetProcessor.markAsProcessed(resourceKey); + } + return targetEntry; + }; await tilesetProcessor.forEachExplicitTile( async (tile: Tile): Promise => { - if (tile.implicitTiling) { - templateUriUpdateCallback(tile); + if (!tile.implicitTiling) { + await tilesetProcessor.processTileContentEntries( + tile, + entryProcessor + ); + } else { + await tilesetProcessor.forEachTileAt( + tile, + async (traversedTile: TraversedTile) => { + await tilesetProcessor.processTraversedTileContentEntries( + traversedTile, + entryProcessor + ); + } + ); + const contents = Tiles.getContents(tile); + for (const content of contents) { + content.uri = updateUri(content.uri); + } } } ); - */ } } diff --git a/src/pipelines/PipelineError.ts b/src/pipelines/PipelineError.ts new file mode 100644 index 00000000..f6f94a6e --- /dev/null +++ b/src/pipelines/PipelineError.ts @@ -0,0 +1,19 @@ +/** + * An error that may be thrown to indicate that a pipeline + * was invalid (for example, due to unknown stage names), + * or one of the stages caused an error during execution. + * + * @internal + */ +export class PipelineError extends Error { + constructor(message: string) { + super(message); + // See https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes + // #extending-built-ins-like-error-array-and-map-may-no-longer-work + Object.setPrototypeOf(this, PipelineError.prototype); + } + + override toString = (): string => { + return `${this.name}: ${this.message}`; + }; +} diff --git a/src/pipelines/PipelineExecutor.ts b/src/pipelines/PipelineExecutor.ts index 2748148a..86f13132 100644 --- a/src/pipelines/PipelineExecutor.ts +++ b/src/pipelines/PipelineExecutor.ts @@ -16,7 +16,7 @@ export class PipelineExecutor { * @param overwrite - Whether outputs should be overwritten if * they already exist * @returns A promise that resolves when the process is finished - * @throws TilesetError If one of the processing steps causes + * @throws PipelineError If one of the processing steps causes * an error. */ static async executePipeline(pipeline: Pipeline, overwrite: boolean) { diff --git a/src/pipelines/TilesetStageExecutor.ts b/src/pipelines/TilesetStageExecutor.ts index 31a1247d..a5ee48e1 100644 --- a/src/pipelines/TilesetStageExecutor.ts +++ b/src/pipelines/TilesetStageExecutor.ts @@ -18,7 +18,7 @@ export class TilesetStageExecutor { * @param overwrite - Whether outputs should be overwritten if * they already exist * @returns A promise that resolves when the process is finished - * @throws TilesetError If one of the processing steps causes + * @throws PipelineError If one of the processing steps causes * an error. */ static async executeTilesetStage( @@ -46,7 +46,7 @@ export class TilesetStageExecutor { * @param tilesetStage - The `TilesetStage` object * @param tilesetProcessor The `BasicTilesetProcessor` * @returns A promise that resolves when the process is finished - * @throws TilesetError If one of the processing steps causes + * @throws PipelineError If one of the processing steps causes * an error. */ private static async executeTilesetStageInternal( diff --git a/src/tilesetProcessing/BasicTilesetProcessor.ts b/src/tilesetProcessing/BasicTilesetProcessor.ts index e0993717..dadcce81 100644 --- a/src/tilesetProcessing/BasicTilesetProcessor.ts +++ b/src/tilesetProcessing/BasicTilesetProcessor.ts @@ -23,16 +23,10 @@ import { TilesetTraverser } from "../traversal/TilesetTraverser"; * The operations are applied by callbacks on certain elements * of the tileset data: * - * - Tiles and their content - * - Explicit tiles and their content - * - Unspecified entries (files) in the tileset + * - All tiles (as `TraversedTile` instances) + * - Explicit tiles (as `Tile` instances) + * - Unspecified entries (files) in the tileset (as `TilesetEntry` objects) * - The tileset (and its schema) itself - * - * Each entry that is processed with one of the `forEach*Entry` - * methods will be processed only _once_, and then marked as - * already having been processed. Entries that have not been - * processed when `TilesetProcessor.end` is called will be - * moved to the tileset target as they are. */ export class BasicTilesetProcessor extends TilesetProcessor { /** @@ -57,8 +51,30 @@ export class BasicTilesetProcessor extends TilesetProcessor { callback: (traversedTile: TraversedTile) => Promise ): Promise { const context = this.getContext(); - const tilesetSource = context.tilesetSource; const tileset = context.tileset; + await this.forEachTileAt(tileset.root, callback); + } + + /** + * Apply the given callback to all `TraversedTile` instances + * that result from traversing the tile hierarchy, starting + * at the given tile. + * + * The given tile is assumed to be an explicit tile in the + * current tileset. + * + * @param tile The tile where to start the traversal + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When an error is thrown during processing + */ + async forEachTileAt( + tile: Tile, + callback: (traversedTile: TraversedTile) => Promise + ): Promise { + const context = this.getContext(); + const tilesetSource = context.tilesetSource; const schema = context.schema; // Create the resource resolver that will be used for @@ -72,8 +88,8 @@ export class BasicTilesetProcessor extends TilesetProcessor { depthFirst: false, traverseExternalTilesets: true, }); - await tilesetTraverser.traverseWithSchema( - tileset, + await tilesetTraverser.traverseWithSchemaAt( + tile, schema, async (traversedTile) => { await callback(traversedTile); @@ -82,32 +98,6 @@ export class BasicTilesetProcessor extends TilesetProcessor { ); } - /** - * Apply the given callback to all entries that represent tile - * content, if they have not been processed yet. - * - * @param callback - The callback - * @returns A promise that resolves when the process is finished - * @throws DeveloperError If `begin` was not called yet - * @throws TilesetError When an error is thrown during processing - */ - async forEachTileContentEntry( - callback: TilesetEntryProcessor - ): Promise { - await this.forEachTile( - async (traversedTile: TraversedTile): Promise => { - if (!traversedTile.isImplicitTilesetRoot()) { - const contentUris = traversedTile - .getFinalContents() - .map((c) => c.uri); - for (const contentUri of contentUris) { - await this.processEntryInternal(contentUri, callback); - } - } - } - ); - } - /** * Apply the given callback to each tile that appears as `Tile` * object in the tileset JSON @@ -131,69 +121,159 @@ export class BasicTilesetProcessor extends TilesetProcessor { } /** - * Apply the given callback to all entries that represent the content - * of explicit tiles (i.e. tiles that appear as `Tile` objects in - * the tileset JSON), if they have not been processed yet. + * Applies the given callback to each `TilesetEntry` that has not + * yet been processed. * - * The tileset JSON will automatically be updated to take into account - * the result of the given callback: When the callback receives an - * entry with a certain `key` (file name), and returns an entry with - * a different `key`, then the `content.uri` will be updated - * accordingly (also taking into account whether the content was - * deleted to split into multiple contents). + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When the input could not be processed + */ + async forEachEntry(callback: TilesetEntryProcessor) { + const context = this.getContext(); + const tilesetSource = context.tilesetSource; + const entries = TilesetSources.getEntries(tilesetSource); + for (const entry of entries) { + await this.processEntry(entry, callback); + } + } + + /** + * Apply the given callback to the `Tileset` and the metadata + * schema. * * @param callback - The callback * @returns A promise that resolves when the process is finished * @throws DeveloperError If `begin` was not called yet * @throws TilesetError When an error is thrown during processing */ - async forEachExplicitTileContentEntry( - callback: TilesetEntryProcessor - ): Promise { - await this.forEachExplicitTile(async (tile: Tile) => { - await this.processExplicitTileContentEntries(tile, callback); - }); + async forTileset( + callback: (tileset: Tileset, schema: Schema | undefined) => Promise + ) { + const context = this.getContext(); + const tileset = context.tileset; + const schema = context.schema; + await callback(tileset, schema); } /** - * Process all entries that are content of the given tile, and - * update the tile content to reflect the changes from the - * callback (see `forEachExplicitTileContentEntry` and - * `updateTileContent`) + * Process all entries that correspond to content of the given tile. + * + * This determines the entries in the tileset source that represent + * content of the given tile, calls `processEntry` for each of them, + * and stores the resulting entries. + * + * The `tile.content.uri` or `tile.contents[i].uri` of the given tile + * will be updated to reflect possible changes of the keys (file + * names) that pare performed by the `entryProcessor`. * * @param tile - The tile + * @param entryProcessor The `TilesetEntryProcessor` * @returns A promise that resolves when the process is finished - * @throws DeveloperError If `begin` was not called yet - * @throws TilesetError When the input could not be processed */ - private async processExplicitTileContentEntries( + async processTileContentEntries( tile: Tile, - callback: TilesetEntryProcessor + entryProcessor: TilesetEntryProcessor ): Promise { - // For roots of implicit tilesets, the content URI - // is a template URI (i.e. they are not explicit, - // and therefore not considered here) - if (tile.implicitTiling) { - return; + const sourceEntries = await this.fetchTileContentEntries(tile); + const targetEntries = await this.processEntries( + sourceEntries, + entryProcessor + ); + BasicTilesetProcessor.updateTileContent(tile, targetEntries); + this.storeTargetEntries(...targetEntries); + } + + /** + * Process all entries that correspond to content of the given traversed tile. + * + * This determines the entries in the tileset source that represent + * content of the given tile, calls `processEntry` for each of them, + * and stores the resulting entries. + * + * @param traversedTile - The traversed tile + * @param entryProcessor The `TilesetEntryProcessor` + * @returns A promise that resolves when the process is finished + */ + async processTraversedTileContentEntries( + traversedTile: TraversedTile, + entryProcessor: TilesetEntryProcessor + ): Promise { + const sourceEntries = await this.fetchTraversedTileContentEntries( + traversedTile + ); + const targetEntries = await this.processEntries( + sourceEntries, + entryProcessor + ); + this.storeTargetEntries(...targetEntries); + } + + /** + * Fetch all entries from the tileset source that correspond to the + * contents of the given tile. + * + * @param tile - The tile + * @returns A promise with the entries + */ + private async fetchTileContentEntries(tile: Tile): Promise { + const contents = BasicTilesetProcessor.getTileContents(tile); + const entries = await this.fetchContentEntries(contents); + return entries; + } + + /** + * Fetch all entries from the tileset source that correspond to the + * contents of the given traversed tile. + * + * @param traversedTile - The traversed tile + * @returns A promise with the entries + */ + private async fetchTraversedTileContentEntries( + traversedTile: TraversedTile + ): Promise { + if (traversedTile.isImplicitTilesetRoot()) { + return []; } - if (tile.content) { - const content = tile.content; - const targetEntries = await this.processEntryInternal( - content.uri, - callback - ); - BasicTilesetProcessor.updateTileContent(tile, targetEntries); - } else if (tile.contents) { - const allTargetEntries = []; - for (const content of tile.contents) { - const targetEntries = await this.processEntryInternal( - content.uri, - callback - ); - allTargetEntries.push(...targetEntries); + const contents = traversedTile.getFinalContents(); + const entries = await this.fetchContentEntries(contents); + return entries; + } + + /** + * Fetch all entries from the tileset source that correspond to the + * given contents. + * + * @param contents - The contents + * @returns A promise with the entries + */ + private async fetchContentEntries( + contents: Content[] + ): Promise { + const entries = []; + for (const content of contents) { + const entry = await this.fetchSourceEntry(content.uri); + if (entry) { + entries.push(entry); } - BasicTilesetProcessor.updateTileContent(tile, allTargetEntries); } + return entries; + } + + /** + * Returns an array with all contents of the give tile. + * + * @param contents - The contents + * @returns A promise with the entries + */ + private static getTileContents(tile: Tile): Content[] { + if (tile.content) { + return [tile.content]; + } + if (tile.contents) { + return tile.contents; + } + return []; } /** @@ -240,64 +320,4 @@ export class BasicTilesetProcessor extends TilesetProcessor { tile.contents = newContents; delete tile.content; } - - /** - * Applies the given callback to each `TilesetEntry` that has not - * yet been processed. - * - * @param callback - The callback - * @returns A promise that resolves when the process is finished - * @throws DeveloperError If `begin` was not called yet - * @throws TilesetError When the input could not be processed - */ - async forEachEntry(callback: TilesetEntryProcessor) { - const context = this.getContext(); - const tilesetSource = context.tilesetSource; - const entries = TilesetSources.getEntries(tilesetSource); - for (const entry of entries) { - const key = entry.key; - await this.processEntryInternal(key, callback); - } - } - - /** - * Apply the given callback to the `Tileset` and the metadata - * schema. - * - * @param callback - The callback - * @returns A promise that resolves when the process is finished - * @throws DeveloperError If `begin` was not called yet - * @throws TilesetError When an error is thrown during processing - */ - async forTileset( - callback: (tileset: Tileset, schema: Schema | undefined) => Promise - ) { - const context = this.getContext(); - const tileset = context.tileset; - const schema = context.schema; - await callback(tileset, schema); - } - - /** - * Creates a callback that receives a `Tile` object, and calls - * the given callback on each of its `Content` objects. - * - * @param callback - The callback for the content - * @returns The callback for the tile - */ - static callbackForEachContent( - callback: (content: Content) => Promise - ): (tile: Tile) => Promise { - return async (tile: Tile) => { - if (tile.content) { - const content = tile.content; - await callback(content); - } - if (tile.contents) { - for (const content of tile.contents) { - await callback(content); - } - } - }; - } } diff --git a/src/tilesetProcessing/TilesetEntryProcessor.ts b/src/tilesetProcessing/TilesetEntryProcessor.ts index fd28d572..6d4a8024 100644 --- a/src/tilesetProcessing/TilesetEntryProcessor.ts +++ b/src/tilesetProcessing/TilesetEntryProcessor.ts @@ -5,27 +5,25 @@ import { TilesetEntry } from "../tilesetData/TilesetEntry"; * of a tileset dataset. * * This is used as the type for the functions that process one - * entry in a `TilesetProcessor`. It will be called ONCE for each - * entry of the tileset source, and return entries that are - * supposed to be put into the tileset target. + * entry in a `TilesetProcessor`. * * It receives the source entry, which may represent a content * of an (explicit) tile, a content of any (possibly implicit) * tile, or just one entry of the tileset source (i.e. a "file" * that is not a tile content). * - * It returns the "processed" entries that are supposed to put into - * the tileset target (which may be an empty array, causing the + * It returns the "processed" entry that is supposed to put into + * the tileset target (which may be `undefined`, causing the * corresponding entry to be omitted in the target) * * @param sourceEntry - The source entry * @param type - The type of the entry data (see `ContentDataTypes`), * or `undefined` if the type could not be determined. * @returns A promise that resolves when the process is finished, - * containing the resulting entries + * containing the resulting entry * @throws TilesetError When the input could not be processed */ export type TilesetEntryProcessor = ( sourceEntry: TilesetEntry, type: string | undefined -) => Promise; +) => Promise; diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetProcessor.ts index d7009566..4a81f71a 100644 --- a/src/tilesetProcessing/TilesetProcessor.ts +++ b/src/tilesetProcessing/TilesetProcessor.ts @@ -177,13 +177,16 @@ export abstract class TilesetProcessor { const entries = TilesetSources.getEntries(tilesetSource); for (const entry of entries) { const key = entry.key; - await this.processEntryInternal( + const targetEntry = await this.processSourceEntry( key, // eslint-disable-next-line @typescript-eslint/no-unused-vars async (sourceEntry: TilesetEntry, type: string | undefined) => { - return [sourceEntry]; + return sourceEntry; } ); + if (targetEntry) { + this.storeTargetEntries(targetEntry); + } } const tilesetTargetJsonFileName = context.tilesetTargetJsonFileName; @@ -220,9 +223,7 @@ export abstract class TilesetProcessor { * then this method does nothing. * * Otherwise, the specified entry will be looked up in the tileset - * source. Its content type will be determined. The source entry - * will be passed to the given processor, which returns the target - * entries that will be inserted into the tileset target. + * source, and passed to `processEntry`. * * @param key - The key (file name) of the entry * @param entryProcessor - The `TilesetEntryProcessor` that will @@ -232,56 +233,114 @@ export abstract class TilesetProcessor { * @throws DeveloperError When the source or target is not opened * @throws TilesetError When the input could not be processed */ - protected async processEntryInternal( + private async processSourceEntry( key: string, entryProcessor: TilesetEntryProcessor - ): Promise { - const context = this.getContext(); - const tilesetSource = context.tilesetSource; - const tilesetTarget = context.tilesetTarget; - + ): Promise { const sourceKey = key; if (this.isProcessed(sourceKey)) { - return []; + return undefined; } - this.markAsProcessed(sourceKey); - - const sourceValue = tilesetSource.getValue(sourceKey); - if (!sourceValue) { + const sourceEntry = await this.fetchSourceEntry(key); + if (!sourceEntry) { const message = `No ${sourceKey} found in input`; throw new TilesetError(message); } - const sourceEntry: TilesetEntry = { - key: sourceKey, - value: sourceValue, - }; - const type = await this.determineContentDataType(sourceKey, sourceValue); + const targetEntry = await this.processEntry(sourceEntry, entryProcessor); + return targetEntry; + } + + /** + * Process the given source entry, and return the processed result. + * + * This will determine the content type of the given entry, pass + * it together with its type information to the `entryProcessor`, + * and mark the entry (and the possible target entry) as "processed". + * + * This will *not* store the returned target entry in the tileset + * target. To do so, `storeTargetEntries` has to be called with + * the result. + * + * @param sourceEntry - The source entry + * @param entryProcessor The `TilesetEntryProcessor` + * @returns The target entry + */ + async processEntry( + sourceEntry: TilesetEntry, + entryProcessor: TilesetEntryProcessor + ): Promise { + const type = await this.determineContentDataType(sourceEntry); + + this.log(`Processing source: ${sourceEntry.key} with type ${type}`); - this.log(`Processing source : ${sourceKey} with type ${type}`); + const targetEntry = await entryProcessor(sourceEntry, type); - const targetEntries = await entryProcessor(sourceEntry, type); + this.log(` to target: ${targetEntry?.key}`); - this.log(` to targets: ${targetEntries?.map((t) => t.key)}`); + this.markAsProcessed(sourceEntry.key); + if (targetEntry) { + this.markAsProcessed(targetEntry.key); + } + return targetEntry; + } - if (targetEntries) { - for (const targetEntry of targetEntries) { - tilesetTarget.addEntry(targetEntry.key, targetEntry.value); - this.markAsProcessed(targetEntry.key); + /** + * Calls `processEntry` on each input, and returns the results. + + * @param sourceEntries - The source entries + * @param entryProcessor The `TilesetEntryProcessor` + * @returns The target entries + */ + async processEntries( + sourceEntries: TilesetEntry[], + entryProcessor: TilesetEntryProcessor + ): Promise { + const targetEntries = []; + for (const sourceEntry of sourceEntries) { + const targetEntry = await this.processEntry(sourceEntry, entryProcessor); + if (targetEntry) { + targetEntries.push(targetEntry); } } return targetEntries; } /** - * Store the given entry in the current target + * Fetch the entry for the specified key from the current tileset + * source. If there is no entry for the given key, then `undefined` + * is returned. + * + * @param key - The key (file name) + * @returns The object containing the entry and its type + */ + async fetchSourceEntry(key: string): Promise { + const context = this.getContext(); + const tilesetSource = context.tilesetSource; + + const sourceKey = key; + const sourceValue = tilesetSource.getValue(sourceKey); + if (!sourceValue) { + console.warn("No input found for " + sourceKey); + return undefined; + } + const sourceEntry: TilesetEntry = { + key: sourceKey, + value: sourceValue, + }; + return sourceEntry; + } + + /** + * Store the given entries in the current target * - * @param targetEntry - The target entry + * @param targetEntries - The target entries */ - storeTargetEntry(targetEntry: TilesetEntry) { + storeTargetEntries(...targetEntries: TilesetEntry[]) { const context = this.getContext(); const tilesetTarget = context.tilesetTarget; - tilesetTarget.addEntry(targetEntry.key, targetEntry.value); - this.markAsProcessed(targetEntry.key); + for (const targetEntry of targetEntries) { + tilesetTarget.addEntry(targetEntry.key, targetEntry.value); + } } /** @@ -290,7 +349,7 @@ export abstract class TilesetProcessor { * * @param key - The key (file name) */ - protected markAsProcessed(key: string) { + markAsProcessed(key: string) { const context = this.getContext(); context.processedKeys[key] = true; } @@ -302,26 +361,24 @@ export abstract class TilesetProcessor { * @param key - The key (file name) * @returns Whether the entry was already processed */ - protected isProcessed(key: string): boolean { + isProcessed(key: string): boolean { const context = this.getContext(); return context.processedKeys[key] === true; } /** - * Determine the type of the given content data. + * Determine the type of the given entry * * The string will either be one of the `ContentDataTypes` strings, * or `undefined` if the type cannot be determined. * - * @param key - The key (file name) - * @param value - The value (file contents) + * @param entry - The entry * @returns A promise with the content data type string */ private async determineContentDataType( - key: string, - value: Buffer + entry: TilesetEntry ): Promise { - const contentData = new BufferedContentData(key, value); + const contentData = new BufferedContentData(entry.key, entry.value); const type = await ContentDataTypeRegistry.findContentDataType(contentData); return type; } diff --git a/src/tilesets/Tiles.ts b/src/tilesets/Tiles.ts index d242c124..662ff8eb 100644 --- a/src/tilesets/Tiles.ts +++ b/src/tilesets/Tiles.ts @@ -1,3 +1,4 @@ +import { Content } from "../structure/Content"; import { Tile } from "../structure/Tile"; import { Contents } from "./Contents"; import { TileTraversalCallback } from "./TileTraversalCallback"; @@ -6,6 +7,27 @@ import { TileTraversalCallback } from "./TileTraversalCallback"; * Utility methods related to tiles, given as `Tile` objects. */ export class Tiles { + /** + * Obtains the contents from the given tile. + * + * This will either return a single-element array, when the tile + * defined `tile.content`, or a multi-element array, when the tile + * defined `tile.contents`, or an empty array, when the tile does + * not have contents. + * + * @param tile The `Tile` + * @returns The content URIs + */ + static getContents(tile: Tile): Content[] { + if (tile.content) { + return [tile.content]; + } + if (tile.contents) { + return tile.contents; + } + return []; + } + /** * Obtains the content URIs from the given tile. * @@ -20,19 +42,13 @@ export class Tiles { * @returns The content URIs */ static getContentUris(tile: Tile): string[] { + const contents = Tiles.getContents(tile); const contentUris = []; - if (tile.content) { - const uri = Contents.getUri(tile.content); + for (const content of contents) { + const uri = Contents.getUri(content); if (uri) { contentUris.push(uri); } - } else if (tile.contents) { - for (const content of tile.contents) { - const uri = Contents.getUri(content); - if (uri) { - contentUris.push(uri); - } - } } return contentUris; } diff --git a/src/traversal/TilesetTraverser.ts b/src/traversal/TilesetTraverser.ts index a2167092..006e9c96 100644 --- a/src/traversal/TilesetTraverser.ts +++ b/src/traversal/TilesetTraverser.ts @@ -9,6 +9,7 @@ import { TraversalCallback } from "./TraversalCallback"; import { TilesetTraversers } from "./TilesetTraversers"; import { DeveloperError } from "../base/DeveloperError"; +import { Tile } from "../structure/Tile"; /** * A collection of configuration options for the traversal. @@ -120,20 +121,51 @@ export class TilesetTraverser { schema: Schema | undefined, traversalCallback: TraversalCallback ): Promise { - const root = tileset.root; - if (!root) { - return; - } + await this.traverseInternal(tileset.root, schema, traversalCallback); + } + + /** + * Traverses the hierarchy of tiles, starting at the + * given tile. + * + * @param tile - The `Tile` where the traversal should start + * @param schema - The schema from the `tileset.schema` or the + * `tileset.schemaUri`, or `undefined` if the tileset does + * not have an associated schema. + * @param traversalCallback - The `TraversalCallback` + * @returns A Promise that resolves when the traversal finished + */ + async traverseWithSchemaAt( + tile: Tile, + schema: Schema | undefined, + traversalCallback: TraversalCallback + ): Promise { + await this.traverseInternal(tile, schema, traversalCallback); + } + + /** + * Actual implementation of the traversal. + * + * @param tile - The `Tile` where to start the traversal + * @param tileset - The `Tileset` + * @param traversalCallback - The `TraversalCallback` + * @returns A Promise that resolves when the traversal finished + */ + private async traverseInternal( + tile: Tile, + schema: Schema | undefined, + traversalCallback: TraversalCallback + ) { const depthFirst = this.options.depthFirst; const stack: TraversedTile[] = []; - const traversedRoot = ExplicitTraversedTile.createRoot( - root, + const traversalRoot = ExplicitTraversedTile.createRoot( + tile, schema, this.resourceResolver ); - stack.push(traversedRoot); + stack.push(traversalRoot); while (stack.length > 0) { const traversedTile = depthFirst ? stack.pop() : stack.shift(); From 9c9d703617d7567a213d4f6f027312fd5e5f01d1 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 4 Apr 2023 19:07:42 +0200 Subject: [PATCH 35/60] Add function for checking content type strings --- src/contentTypes/ContentDataTypeChecks.ts | 32 ++++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/contentTypes/ContentDataTypeChecks.ts b/src/contentTypes/ContentDataTypeChecks.ts index fa398e3b..79a05be5 100644 --- a/src/contentTypes/ContentDataTypeChecks.ts +++ b/src/contentTypes/ContentDataTypeChecks.ts @@ -19,6 +19,33 @@ export class ContentDataTypeChecks { includedContentDataTypes: string[] | undefined, excludedContentDataTypes: string[] | undefined ): (contentData: ContentData) => Promise { + const typeCheck = ContentDataTypeChecks.createTypeCheck( + includedContentDataTypes, + excludedContentDataTypes + ); + return async (contentData: ContentData) => { + const contentDataType = await ContentDataTypeRegistry.findContentDataType( + contentData + ); + const result = typeCheck(contentDataType); + return result; + }; + } + + /** + * Creates a predicate that checks whether a given string + * (that represents one of the `ContentDataTypes`) is + * contained in the given included types, and NOT + * contained in the given excluded types. + * + * @param includedContentDataTypes - The included types + * @param excludedContentDataTypes - The excluded types + * @returns The predicate + */ + static createTypeCheck( + includedContentDataTypes: string[] | undefined, + excludedContentDataTypes: string[] | undefined + ): (contentDataType: string | undefined) => boolean { const localIncluded: string[] = []; if (includedContentDataTypes) { localIncluded.push(...includedContentDataTypes); @@ -27,10 +54,7 @@ export class ContentDataTypeChecks { if (excludedContentDataTypes) { localExcluded.push(...excludedContentDataTypes); } - return async (contentData: ContentData) => { - const contentDataType = await ContentDataTypeRegistry.findContentDataType( - contentData - ); + return (contentDataType: string | undefined) => { if (!contentDataType) { return false; } From 8cc1b15083da22158cf90bd2ef2f5fa4b61ef6ec Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 4 Apr 2023 19:15:28 +0200 Subject: [PATCH 36/60] Basic tileset stages. Clean up content stages. --- demos/PipelineExperimentsTilesetStages.ts | 43 ++++ src/pipelines/ContentStage.ts | 26 ++- src/pipelines/ContentStageExecutor.ts | 192 ++++++++---------- src/pipelines/ContentStages.ts | 91 ++++++--- src/pipelines/PipelineExecutor.ts | 3 +- src/pipelines/Stage.ts | 8 + src/pipelines/TilesetStage.ts | 2 +- src/pipelines/TilesetStageExecutor.ts | 107 +++++++--- src/pipelines/TilesetStages.ts | 37 +++- .../BasicTilesetProcessor.ts | 83 ++++++-- src/tilesetProcessing/TilesetProcessor.ts | 20 +- src/tilesetProcessing/TilesetUpgrader.ts | 37 ++-- 12 files changed, 439 insertions(+), 210 deletions(-) create mode 100644 demos/PipelineExperimentsTilesetStages.ts diff --git a/demos/PipelineExperimentsTilesetStages.ts b/demos/PipelineExperimentsTilesetStages.ts new file mode 100644 index 00000000..8d293fe3 --- /dev/null +++ b/demos/PipelineExperimentsTilesetStages.ts @@ -0,0 +1,43 @@ +import { Pipeline } from "../src/pipelines/Pipeline"; +import { TilesetStages } from "../src/pipelines/TilesetStages"; +import { ContentStages } from "../src/pipelines/ContentStages"; +import { PipelineExecutor } from "../src/pipelines/PipelineExecutor"; + +async function example() { + const input = "./specs/data/TilesetOfTilesets"; + const output = "./output/result.3tz"; + const overwrite = true; + const optimizeGlbOptions = { + dracoOptions: { + compressionLevel: 10, + }, + }; + + const tilesetStages = [ + TilesetStages.createUpgrade(), + TilesetStages.createCombine(), + TilesetStages.create("B3DM to GLB", "Convert B3DM to GLB", [ + ContentStages.createB3dmToGlb(), + ]), + TilesetStages.create("Optimize GLB", "Optimize GLB", [ + ContentStages.createOptimizeGlb(optimizeGlbOptions), + ]), + TilesetStages.create("Separate glTF", "Separate glTF", [ + ContentStages.createSeparateGltf(), + ]), + TilesetStages.create("Dummy", "Dummy", []), + ]; + + const pipeline: Pipeline = { + input: input, + output: output, + tilesetStages: tilesetStages, + }; + + const pipelineJsonString = JSON.stringify(pipeline, null, 2); + console.log("Executing pipeline:\n" + pipelineJsonString); + + await PipelineExecutor.executePipeline(pipeline, overwrite); +} + +example(); diff --git a/src/pipelines/ContentStage.ts b/src/pipelines/ContentStage.ts index 2e507f6f..8ed249d7 100644 --- a/src/pipelines/ContentStage.ts +++ b/src/pipelines/ContentStage.ts @@ -1,7 +1,5 @@ import { Stage } from "./Stage"; -import { TilesetEntry } from "../tilesetData/TilesetEntry"; - /** * An interface that describes an operation that may be * applied to one `TilesetEntry` during the execution @@ -12,15 +10,31 @@ import { TilesetEntry } from "../tilesetData/TilesetEntry"; */ export interface ContentStage extends Stage { /** - * An optional predicate that determines whether this stage - * should be applied to a certain entry. + * An optional array of `ContentDataType` strings that + * indicates which content types this stage should be + * applied to. + * + * The stage will be applied to types that are contained + * in the `includedContentTypes`, but NOT contained in + * the `excludedContentTypes` + */ + includedContentTypes?: string[]; + + /** + * An optional array of `ContentDataType` strings that + * indicates which content types this stage should be + * applied to. + * + * The stage will be applied to types that are contained + * in the `includedContentTypes`, but NOT contained in + * the `excludedContentTypes` */ - condition: ((e: TilesetEntry) => Promise) | undefined; + excludedContentTypes?: string[]; /** * Arbitrary options that may have been given in the * input JSON, and will be passed to implementations * that may support these options (e.g. `gltf-pipeline`). */ - options: any; + options?: any; } diff --git a/src/pipelines/ContentStageExecutor.ts b/src/pipelines/ContentStageExecutor.ts index 8c77b7d3..d930d250 100644 --- a/src/pipelines/ContentStageExecutor.ts +++ b/src/pipelines/ContentStageExecutor.ts @@ -1,9 +1,11 @@ +import path from "path"; import GltfPipeline from "gltf-pipeline"; import { Paths } from "../base/Paths"; import { Buffers } from "../base/Buffers"; import { ContentDataTypes } from "../contentTypes/ContentDataTypes"; +import { ContentDataTypeChecks } from "../contentTypes/ContentDataTypeChecks"; import { TilesetEntry } from "../tilesetData/TilesetEntry"; @@ -14,11 +16,7 @@ import { BasicTilesetProcessor } from "../tilesetProcessing/BasicTilesetProcesso import { GltfUtilities } from "../contentProcessing/GtlfUtilities"; import { ContentOps } from "../contentProcessing/ContentOps"; - -import { Tile } from "../structure/Tile"; - -import { TraversedTile } from "../traversal/TraversedTile"; -import { Tiles } from "../tilesets/Tiles"; +import { ContentStages } from "./ContentStages"; /** * Methods to execute `ContentStage` objects. @@ -60,19 +58,22 @@ export class ContentStageExecutor { contentStage: ContentStage, tilesetProcessor: BasicTilesetProcessor ) { - if (contentStage.name === "gzip") { - await ContentStageExecutor.executeGzip( - tilesetProcessor, - contentStage.condition + if (contentStage.name === ContentStages.CONTENT_STAGE_GZIP) { + const condition = ContentDataTypeChecks.createTypeCheck( + contentStage.includedContentTypes, + contentStage.excludedContentTypes ); - } else if (contentStage.name === "ungzip") { + await ContentStageExecutor.executeGzip(tilesetProcessor, condition); + } else if (contentStage.name === ContentStages.CONTENT_STAGE_UNGZIP) { await ContentStageExecutor.executeGunzip(tilesetProcessor); - } else if (contentStage.name === "b3dmToGlb") { + } else if (contentStage.name === ContentStages.CONTENT_STAGE_B3DM_TO_GLB) { await ContentStageExecutor.executeB3dmToGlb(tilesetProcessor); - } else if (contentStage.name === "optimizeGlb") { + } else if (contentStage.name === ContentStages.CONTENT_STAGE_OPTIMIZE_GLB) { const options = contentStage.options; await ContentStageExecutor.executeOptimizeGlb(tilesetProcessor, options); - } else if (contentStage.name === "separateGltf") { + } else if ( + contentStage.name === ContentStages.CONTENT_STAGE_SEPARATE_GLTF + ) { await ContentStageExecutor.executeSeparateGltf(tilesetProcessor); } else { const message = ` Unknown contentStage name: ${contentStage.name}`; @@ -88,32 +89,39 @@ export class ContentStageExecutor { * compressed with gzip. Other entries remain unaffected. * * @param tilesetProcessor - The `BasicTilesetProcessor` - * @param condition The condition from the `ContentStage` + * @param condition The condition that was created from + * the included- and excluded types that have been defined + * in the `ContentStage` * @returns A promise that resolves when the process is finished * @throws Error If one of the processing steps causes * an error. */ private static async executeGzip( tilesetProcessor: BasicTilesetProcessor, - condition: ((e: TilesetEntry) => Promise) | undefined + condition: ((type: string | undefined) => boolean) | undefined ): Promise { - await tilesetProcessor.forEachEntry( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async (sourceEntry: TilesetEntry, type: string | undefined) => { - let targetValue = sourceEntry.value; - if (condition) { - const shouldZip = await condition(sourceEntry); - if (shouldZip) { - targetValue = Buffers.gzip(sourceEntry.value); - } + // The entry processor receives the source entry, and + // returns a target entry where the `value` is zipped + // if the source entry matches the given condition. + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + let targetValue = sourceEntry.value; + if (condition) { + const shouldZip = condition(type); + if (shouldZip) { + targetValue = Buffers.gzip(sourceEntry.value); } - const targetEntry = { - key: sourceEntry.key, - value: targetValue, - }; - return targetEntry; } - ); + const targetEntry = { + key: sourceEntry.key, + value: targetValue, + }; + return targetEntry; + }; + + await tilesetProcessor.processAllEntries(entryProcessor); } /** @@ -131,16 +139,23 @@ export class ContentStageExecutor { private static async executeGunzip( tilesetProcessor: BasicTilesetProcessor ): Promise { - await tilesetProcessor.forEachEntry( + // The entry processor receives the source entry, and + // returns a target entry where the `value` is unzipped + // (If the data was not zipped, then `Buffers.gunzip` + // returns an unmodified result) + const entryProcessor = async ( + sourceEntry: TilesetEntry, // eslint-disable-next-line @typescript-eslint/no-unused-vars - async (sourceEntry: TilesetEntry, type: string | undefined) => { - const targetEntry = { - key: sourceEntry.key, - value: Buffers.gunzip(sourceEntry.value), - }; - return targetEntry; - } - ); + type: string | undefined + ) => { + const targetEntry = { + key: sourceEntry.key, + value: Buffers.gunzip(sourceEntry.value), + }; + return targetEntry; + }; + + await tilesetProcessor.processAllEntries(entryProcessor); } /** @@ -164,7 +179,7 @@ export class ContentStageExecutor { // Define the rule for updating the key (file name) of // the entries, as well as possible template URIs of // implicit tileset roots. - const updateUri = (uri: string) => { + const uriProcessor = (uri: string) => { if (Paths.hasExtension(uri, ".b3dm")) { return Paths.replaceExtension(uri, ".glb"); } @@ -181,42 +196,14 @@ export class ContentStageExecutor { return sourceEntry; } const targetEntry = { - key: updateUri(sourceEntry.key), + key: uriProcessor(sourceEntry.key), value: ContentOps.b3dmToGlbBuffer(sourceEntry.value), }; return targetEntry; }; - - // Traverse the (explicit) tiles of the input tileset - await tilesetProcessor.forEachExplicitTile( - async (tile: Tile): Promise => { - // When the tile is not an implicit tiling root, - // then just update the entries that correspond - // to the tile contents. - if (!tile.implicitTiling) { - tilesetProcessor.processTileContentEntries(tile, entryProcessor); - } else { - // For implicit tiling roots, traverse the implicit tile hierarchy - // that starts at this tile, and process each entry that corresponds - // to the content of one of the implicit tiles. - await tilesetProcessor.forEachTileAt( - tile, - async (traversedTile: TraversedTile) => { - await tilesetProcessor.processTraversedTileContentEntries( - traversedTile, - entryProcessor - ); - } - ); - - // After the traversal, update the content URIs of the - // implicit tiling root (which are template URIs) - const contents = Tiles.getContents(tile); - for (const content of contents) { - content.uri = updateUri(content.uri); - } - } - } + await tilesetProcessor.processTileContentEntries( + uriProcessor, + entryProcessor ); } @@ -237,6 +224,10 @@ export class ContentStageExecutor { tilesetProcessor: BasicTilesetProcessor, options: any ): Promise { + // The entry processor receives the source entry, and + // returns a target entry where the the `value` contains + // GLB data that was optimized with `gltf-pipeline` + // and the given options const entryProcessor = async ( sourceEntry: TilesetEntry, type: string | undefined @@ -254,16 +245,14 @@ export class ContentStageExecutor { }; return targetEntry; }; - await tilesetProcessor.forEachTile(async (traversedTile: TraversedTile) => { - await tilesetProcessor.processTraversedTileContentEntries( - traversedTile, - entryProcessor - ); - }); + await tilesetProcessor.processTileContentEntries( + (uri: string) => uri, + entryProcessor + ); } /** - * Internal experiments. + * Performs the 'separateGltf' content stage with the given processor. * * @param tilesetProcessor - The `BasicTilesetProcessor` * @returns A promise that resolves when the process is finished @@ -273,13 +262,21 @@ export class ContentStageExecutor { private static async executeSeparateGltf( tilesetProcessor: BasicTilesetProcessor ): Promise { - const updateUri = (uri: string) => { + // Define the rule for updating the key (file name) of + // the entries, as well as possible template URIs of + // implicit tileset roots. + const uriProcessor = (uri: string) => { if (Paths.hasExtension(uri, ".glb")) { return Paths.replaceExtension(uri, ".gltf"); } return uri; }; + // The entry processor receives the source entry, and + // returns a target entry where the the `value` contains + // the glTF data that was generated with `gltf-pipeline`. + // The additional external resources will be passed to + // the tileset processor, to be stored in the target. const entryProcessor = async ( sourceEntry: TilesetEntry, type: string | undefined @@ -287,9 +284,11 @@ export class ContentStageExecutor { if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { return sourceEntry; } + const dirname = path.dirname(sourceEntry.key); + const prefix = Paths.replaceExtension(path.basename(sourceEntry.key), ""); const options = { separate: true, - name: Paths.replaceExtension(sourceEntry.key, ""), + name: prefix, }; const gltfPipelineResults = await GltfPipeline.glbToGltf( sourceEntry.value, @@ -297,7 +296,7 @@ export class ContentStageExecutor { ); const targetValue = Buffer.from(JSON.stringify(gltfPipelineResults.gltf)); const targetEntry = { - key: updateUri(sourceEntry.key), + key: uriProcessor(sourceEntry.key), value: targetValue, }; @@ -306,7 +305,7 @@ export class ContentStageExecutor { for (const resourceKey of resourceKeys) { const resourceValue = separateResources[resourceKey]; const resourceTargetEntry = { - key: resourceKey, + key: Paths.join(dirname, resourceKey), value: resourceValue, }; tilesetProcessor.storeTargetEntries(resourceTargetEntry); @@ -314,30 +313,9 @@ export class ContentStageExecutor { } return targetEntry; }; - - await tilesetProcessor.forEachExplicitTile( - async (tile: Tile): Promise => { - if (!tile.implicitTiling) { - await tilesetProcessor.processTileContentEntries( - tile, - entryProcessor - ); - } else { - await tilesetProcessor.forEachTileAt( - tile, - async (traversedTile: TraversedTile) => { - await tilesetProcessor.processTraversedTileContentEntries( - traversedTile, - entryProcessor - ); - } - ); - const contents = Tiles.getContents(tile); - for (const content of contents) { - content.uri = updateUri(content.uri); - } - } - } + await tilesetProcessor.processTileContentEntries( + uriProcessor, + entryProcessor ); } } diff --git a/src/pipelines/ContentStages.ts b/src/pipelines/ContentStages.ts index 62f697bd..0f26cbe4 100644 --- a/src/pipelines/ContentStages.ts +++ b/src/pipelines/ContentStages.ts @@ -1,20 +1,75 @@ import { defined } from "../base/defined"; import { DeveloperError } from "../base/DeveloperError"; -import { TilesetEntry } from "../tilesetData/TilesetEntry"; - -import { ContentDataTypeChecks } from "../contentTypes/ContentDataTypeChecks"; -import { BufferedContentData } from "../contentTypes/BufferedContentData"; - import { ContentStage } from "./ContentStage"; +/** + * Methods to create `ContentStage` objects from JSON input. + */ export class ContentStages { + public static readonly CONTENT_STAGE_GZIP = "gzip"; + public static readonly CONTENT_STAGE_UNGZIP = "ungzip"; + public static readonly CONTENT_STAGE_B3DM_TO_GLB = "b3dmToGlb"; + public static readonly CONTENT_STAGE_OPTIMIZE_GLB = "optimizeGlb"; + public static readonly CONTENT_STAGE_SEPARATE_GLTF = "separateGltf"; + + public static createGzip( + includedContentTypes: string[] | undefined + ): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_GZIP, + description: "Compresses each entry with GZIP", + includedContentTypes: includedContentTypes, + }; + return contentStage; + } + + public static createUngzip(): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_UNGZIP, + description: "Uncompress each entry that was compressed with GZIP", + }; + return contentStage; + } + + public static createB3dmToGlb(): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_B3DM_TO_GLB, + description: "Convert each B3DM content into GLB", + }; + return contentStage; + } + + public static createOptimizeGlb(options: any): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_OPTIMIZE_GLB, + description: + "Apply gltf-pipeline to each GLB content, with the given options", + options: options, + }; + return contentStage; + } + + public static createSeparateGltf(): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_SEPARATE_GLTF, + description: + "Convert each GLB content into a .gltf file with separate resources", + }; + return contentStage; + } + + /** + * Creates a `ContentStage` object from the given (untyped) JSON. + * + * @param contentStageJson - The JSON object + * @returns The `ContentStage` object + * @throws DeveloperError When the input was not valid + */ static createContentStage(contentStageJson: any): ContentStage { if (typeof contentStageJson === "string") { const contentStage: ContentStage = { name: contentStageJson, - condition: undefined, - options: undefined, }; return contentStage; } @@ -23,28 +78,6 @@ export class ContentStages { if (!defined(contentStage.name)) { throw new DeveloperError("The contentStage JSON does not define a name"); } - contentStage.condition = - ContentStages.createContentStageCondition(contentStageJson); return contentStage; } - - static createContentStageCondition( - contentStageJson: any - ): ((e: TilesetEntry) => Promise) | undefined { - const included = contentStageJson.includedContentTypes; - const excluded = contentStageJson.excludedContentTypes; - if (included || excluded) { - const contentDataCheck = ContentDataTypeChecks.createCheck( - included, - excluded - ); - const condition = async (entry: TilesetEntry) => { - const contentData = new BufferedContentData(entry.key, entry.value); - const matches = await contentDataCheck(contentData); - return matches; - }; - return condition; - } - return undefined; - } } diff --git a/src/pipelines/PipelineExecutor.ts b/src/pipelines/PipelineExecutor.ts index 86f13132..4eb9452f 100644 --- a/src/pipelines/PipelineExecutor.ts +++ b/src/pipelines/PipelineExecutor.ts @@ -59,7 +59,8 @@ export class PipelineExecutor { currentOutput = pipeline.output; currentOverwrite = overwrite; } else { - currentOutput = `${tempBasePath}/tilesetStage-${t}`; + const nameSuffix = tilesetStage.name.replace(/[^\w\s]/gi, ""); + currentOutput = `${tempBasePath}/tilesetStage-${t}-${nameSuffix}`; currentOverwrite = true; } diff --git a/src/pipelines/Stage.ts b/src/pipelines/Stage.ts index acee6af2..db8a5c8d 100644 --- a/src/pipelines/Stage.ts +++ b/src/pipelines/Stage.ts @@ -8,4 +8,12 @@ export interface Stage { * The name of this stage. */ name: string; + + /** + * An optional description. + * + * This should be a single-line, human-readable description that + * summarizes what the stage is doing. + */ + description?: string; } diff --git a/src/pipelines/TilesetStage.ts b/src/pipelines/TilesetStage.ts index c48d91c2..df502696 100644 --- a/src/pipelines/TilesetStage.ts +++ b/src/pipelines/TilesetStage.ts @@ -13,5 +13,5 @@ export interface TilesetStage extends Stage { * The `ContentStage` steps representing the sequence of * operations that should be applied to content. */ - contentStages: ContentStage[]; + contentStages?: ContentStage[]; } diff --git a/src/pipelines/TilesetStageExecutor.ts b/src/pipelines/TilesetStageExecutor.ts index a5ee48e1..755a204b 100644 --- a/src/pipelines/TilesetStageExecutor.ts +++ b/src/pipelines/TilesetStageExecutor.ts @@ -1,7 +1,14 @@ import { TilesetStage } from "./TilesetStage"; import { ContentStageExecutor } from "./ContentStageExecutor"; +import { PipelineError } from "./PipelineError"; +import { TilesetStages } from "./TilesetStages"; import { BasicTilesetProcessor } from "../tilesetProcessing/BasicTilesetProcessor"; +import { TilesetUpgrader } from "../tilesetProcessing/TilesetUpgrader"; +import { TilesetCombiner } from "../tilesetProcessing/TilesetCombiner"; + +import { ContentDataTypeChecks } from "../contentTypes/ContentDataTypeChecks"; +import { ContentDataTypes } from "../contentTypes/ContentDataTypes"; /** * Methods to execute `TilesetStage` objects. @@ -31,45 +38,99 @@ export class TilesetStageExecutor { console.log(` currentInput: ${currentInput}`); console.log(` currentOutput: ${currentOutput}`); - const tilesetProcessor = new BasicTilesetProcessor(); - await tilesetProcessor.begin(currentInput, currentOutput, overwrite); - await TilesetStageExecutor.executeTilesetStageInternal( - tilesetStage, - tilesetProcessor - ); - await tilesetProcessor.end(); + try { + await TilesetStageExecutor.executeTilesetStageInternal( + tilesetStage, + currentInput, + currentOutput, + overwrite + ); + } catch (e) { + throw new PipelineError(`${e}`); + } } /** * Implementation for `executeTilesetStage`. * + * For details about the arguments, see `executeTilesetStage`. + * * @param tilesetStage - The `TilesetStage` object - * @param tilesetProcessor The `BasicTilesetProcessor` + * @param currentInput - The current input name + * @param currentOutput - The current output name + * @param overwrite - Whether outputs should be overwritten * @returns A promise that resolves when the process is finished - * @throws PipelineError If one of the processing steps causes + * @throws Error If one of the processing steps causes * an error. */ private static async executeTilesetStageInternal( tilesetStage: TilesetStage, - tilesetProcessor: BasicTilesetProcessor + currentInput: string, + currentOutput: string, + overwrite: boolean ) { - const contentStages = tilesetStage.contentStages; - if (contentStages.length === 0) { - return; + if (tilesetStage.name === TilesetStages.TILESET_STAGE_UPGRADE) { + const quiet = false; + const tilesetUpgrader = new TilesetUpgrader(quiet); + await tilesetUpgrader.upgrade(currentInput, currentOutput, overwrite); + } else if (tilesetStage.name === TilesetStages.TILESET_STAGE_COMBINE) { + const externalTilesetDetector = ContentDataTypeChecks.createIncludedCheck( + ContentDataTypes.CONTENT_TYPE_TILESET + ); + const tilesetCombiner = new TilesetCombiner(externalTilesetDetector); + await tilesetCombiner.combine(currentInput, currentOutput, overwrite); + } else { + await TilesetStageExecutor.executeTilesetContentStages( + tilesetStage, + currentInput, + currentOutput, + overwrite + ); } + } - for (let c = 0; c < contentStages.length; c++) { - const contentStage = contentStages[c]; + /** + * Execute all `ContentStage` objects in the given `TilesetStage`. + * + * For details about the arguments, see `executeTilesetStage`. + * + * @param tilesetStage - The `TilesetStage` object + * @param currentInput - The current input name + * @param currentOutput - The current output name + * @param overwrite - Whether outputs should be overwritten + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ + private static async executeTilesetContentStages( + tilesetStage: TilesetStage, + currentInput: string, + currentOutput: string, + overwrite: boolean + ) { + try { + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(currentInput, currentOutput, overwrite); - const message = - ` Executing contentStage ${c} of ` + - `${contentStages.length}: ${contentStage.name}`; - console.log(message); + const contentStages = tilesetStage.contentStages; + if (contentStages) { + for (let c = 0; c < contentStages.length; c++) { + const contentStage = contentStages[c]; - await ContentStageExecutor.executeContentStage( - contentStage, - tilesetProcessor - ); + const message = + ` Executing contentStage ${c} of ` + + `${contentStages.length}: ${contentStage.name}`; + console.log(message); + + await ContentStageExecutor.executeContentStage( + contentStage, + tilesetProcessor + ); + } + } + await tilesetProcessor.end(); + } catch (e) { + throw new PipelineError(`${e}`); } } } diff --git a/src/pipelines/TilesetStages.ts b/src/pipelines/TilesetStages.ts index 2d969679..0972ddfe 100644 --- a/src/pipelines/TilesetStages.ts +++ b/src/pipelines/TilesetStages.ts @@ -9,6 +9,38 @@ import { ContentStages } from "./ContentStages"; * Methods to create `TilesetStage` objects from JSON input. */ export class TilesetStages { + public static readonly TILESET_STAGE_UPGRADE = "upgrade"; + public static readonly TILESET_STAGE_COMBINE = "combine"; + + public static createUpgrade(): TilesetStage { + const tilesetStage: TilesetStage = { + name: TilesetStages.TILESET_STAGE_UPGRADE, + description: "Upgrade the input tileset to the latest version", + }; + return tilesetStage; + } + + public static createCombine(): TilesetStage { + const tilesetStage: TilesetStage = { + name: TilesetStages.TILESET_STAGE_COMBINE, + description: "Combine all external tilesets into one", + }; + return tilesetStage; + } + + public static create( + name: string, + description: string, + contentStages: ContentStage[] + ): TilesetStage { + const tilesetStage: TilesetStage = { + name: name, + description: description, + contentStages: contentStages, + }; + return tilesetStage; + } + /** * Creates a `TilesetStage` object from the given (untyped) JSON. * @@ -29,10 +61,9 @@ export class TilesetStages { throw new DeveloperError("The tilesetStage JSON does not define a name"); } - // The contentStages may be undefined, resulting - // in an empty array here: - const contentStages: ContentStage[] = []; + let contentStages: ContentStage[] | undefined = undefined; if (tilesetStageJson.contentStages) { + contentStages = []; for (const contentStageJson of tilesetStageJson.contentStages) { const contentStage = ContentStages.createContentStage(contentStageJson); contentStages.push(contentStage); diff --git a/src/tilesetProcessing/BasicTilesetProcessor.ts b/src/tilesetProcessing/BasicTilesetProcessor.ts index dadcce81..cbdb9623 100644 --- a/src/tilesetProcessing/BasicTilesetProcessor.ts +++ b/src/tilesetProcessing/BasicTilesetProcessor.ts @@ -69,7 +69,7 @@ export class BasicTilesetProcessor extends TilesetProcessor { * @throws DeveloperError If `begin` was not called yet * @throws TilesetError When an error is thrown during processing */ - async forEachTileAt( + private async forEachTileAt( tile: Tile, callback: (traversedTile: TraversedTile) => Promise ): Promise { @@ -120,6 +120,24 @@ export class BasicTilesetProcessor extends TilesetProcessor { }); } + /** + * Apply the given callback to the `Tileset` and the metadata + * schema. + * + * @param callback - The callback + * @returns A promise that resolves when the process is finished + * @throws DeveloperError If `begin` was not called yet + * @throws TilesetError When an error is thrown during processing + */ + async forTileset( + callback: (tileset: Tileset, schema: Schema | undefined) => Promise + ) { + const context = this.getContext(); + const tileset = context.tileset; + const schema = context.schema; + await callback(tileset, schema); + } + /** * Applies the given callback to each `TilesetEntry` that has not * yet been processed. @@ -129,31 +147,60 @@ export class BasicTilesetProcessor extends TilesetProcessor { * @throws DeveloperError If `begin` was not called yet * @throws TilesetError When the input could not be processed */ - async forEachEntry(callback: TilesetEntryProcessor) { + async processAllEntries(entryProcessor: TilesetEntryProcessor) { const context = this.getContext(); const tilesetSource = context.tilesetSource; const entries = TilesetSources.getEntries(tilesetSource); for (const entry of entries) { - await this.processEntry(entry, callback); + await this.processEntry(entry, entryProcessor); } } /** - * Apply the given callback to the `Tileset` and the metadata - * schema. + * Process all entries that are tile content. * - * @param callback - The callback + * This will process all tile content entries of the source tileset + * with the given `TilesetEntryProcessor`. The given `uriProcessor` + * will be used for updating the `key` (file name) of the entries, + * as well as possible template URIs at the roots of implicit + * tilesets. + * + * @param uriProcessor - The processor that updates keys and URIs + * @param entryProcessor - The `TilesetEntryProcessor` * @returns A promise that resolves when the process is finished - * @throws DeveloperError If `begin` was not called yet - * @throws TilesetError When an error is thrown during processing + * @throws Error If one of the processing steps causes + * an error. */ - async forTileset( - callback: (tileset: Tileset, schema: Schema | undefined) => Promise - ) { - const context = this.getContext(); - const tileset = context.tileset; - const schema = context.schema; - await callback(tileset, schema); + async processTileContentEntries( + uriProcessor: (uri: string) => string, + entryProcessor: TilesetEntryProcessor + ): Promise { + // Traverse the (explicit) tiles of the input tileset + await this.forEachExplicitTile(async (tile: Tile): Promise => { + // When the tile is not an implicit tiling root, + // then just update the entries that correspond + // to the tile contents. + if (!tile.implicitTiling) { + await this.processExplicitTileContentEntries(tile, entryProcessor); + } else { + // For implicit tiling roots, traverse the implicit tile hierarchy + // that starts at this tile, and process each entry that corresponds + // to the content of one of the implicit tiles. + await this.forEachTileAt(tile, async (traversedTile: TraversedTile) => { + await this.processTraversedTileContentEntries( + traversedTile, + entryProcessor + ); + }); + + // After the traversal, update the content URIs of the + // implicit tiling root (which are template URIs) + const contents = Tiles.getContents(tile); + for (const content of contents) { + content.uri = uriProcessor(content.uri); + } + } + }); } /** @@ -165,13 +212,13 @@ export class BasicTilesetProcessor extends TilesetProcessor { * * The `tile.content.uri` or `tile.contents[i].uri` of the given tile * will be updated to reflect possible changes of the keys (file - * names) that pare performed by the `entryProcessor`. + * names) that are performed by the `entryProcessor`. * * @param tile - The tile * @param entryProcessor The `TilesetEntryProcessor` * @returns A promise that resolves when the process is finished */ - async processTileContentEntries( + private async processExplicitTileContentEntries( tile: Tile, entryProcessor: TilesetEntryProcessor ): Promise { @@ -195,7 +242,7 @@ export class BasicTilesetProcessor extends TilesetProcessor { * @param entryProcessor The `TilesetEntryProcessor` * @returns A promise that resolves when the process is finished */ - async processTraversedTileContentEntries( + private async processTraversedTileContentEntries( traversedTile: TraversedTile, entryProcessor: TilesetEntryProcessor ): Promise { diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetProcessor.ts index 4a81f71a..b818b068 100644 --- a/src/tilesetProcessing/TilesetProcessor.ts +++ b/src/tilesetProcessing/TilesetProcessor.ts @@ -171,21 +171,25 @@ export abstract class TilesetProcessor { const context = this.getContext(); const tilesetSource = context.tilesetSource; const tilesetTarget = context.tilesetTarget; + const tilesetSourceJsonFileName = context.tilesetSourceJsonFileName; // Perform a no-op on all entries that have not yet // been marked as processed const entries = TilesetSources.getEntries(tilesetSource); for (const entry of entries) { const key = entry.key; - const targetEntry = await this.processSourceEntry( - key, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async (sourceEntry: TilesetEntry, type: string | undefined) => { - return sourceEntry; + // The tileset JSON file will be added explicitly below + if (key !== tilesetSourceJsonFileName) { + const targetEntry = await this.processSourceEntry( + key, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async (sourceEntry: TilesetEntry, type: string | undefined) => { + return sourceEntry; + } + ); + if (targetEntry) { + this.storeTargetEntries(targetEntry); } - ); - if (targetEntry) { - this.storeTargetEntries(targetEntry); } } diff --git a/src/tilesetProcessing/TilesetUpgrader.ts b/src/tilesetProcessing/TilesetUpgrader.ts index 8f5de525..5e1ca087 100644 --- a/src/tilesetProcessing/TilesetUpgrader.ts +++ b/src/tilesetProcessing/TilesetUpgrader.ts @@ -37,6 +37,7 @@ type UpgradeOptions = { upgradeContentUrlToUri: boolean; upgradeB3dmGltf1ToGltf2: boolean; upgradeI3dmGltf1ToGltf2: boolean; + upgradeExternalTilesets: boolean; upgradeExtensionDeclarations: boolean; }; @@ -86,6 +87,7 @@ export class TilesetUpgrader { upgradeContentUrlToUri: true, upgradeB3dmGltf1ToGltf2: true, upgradeI3dmGltf1ToGltf2: true, + upgradeExternalTilesets: true, upgradeExtensionDeclarations: true, }; } @@ -122,9 +124,12 @@ export class TilesetUpgrader { const tilesetTargetJsonFileName = Tilesets.determineTilesetJsonFileName(tilesetTargetName); - await this.upgradeInternal( - tilesetSourceJsonFileName, - tilesetTargetJsonFileName + const upgradedTilesetJsonBuffer = await this.upgradeInternal( + tilesetSourceJsonFileName + ); + this.tilesetTarget.addEntry( + tilesetTargetJsonFileName, + upgradedTilesetJsonBuffer ); await this.upgradeResources(tilesetSourceJsonFileName); @@ -138,19 +143,17 @@ export class TilesetUpgrader { /** * Internal method for the actual upgrade. * - * It justo obtains the tileset JSON data from the source, passes - * it to `upgradeTileset`, and writes the result under the given - * name into the target. + * It just obtains the tileset JSON data from the source, passes + * it to `upgradeTileset`, and returns the buffer containing the + * JSON data of the upgraded result. * * @param tilesetSourceJsonFileName - The name of the tileset JSON in the source - * @param tilesetTargetJsonFileName - The name of the tileset JSON in the target * @returns A promise that resolves when the process is finished * @throws TilesetError When the input could not be processed */ private async upgradeInternal( - tilesetSourceJsonFileName: string, - tilesetTargetJsonFileName: string - ): Promise { + tilesetSourceJsonFileName: string + ): Promise { if (!this.tilesetSource || !this.tilesetTarget) { throw new DeveloperError("The source and target must be defined"); } @@ -181,10 +184,7 @@ export class TilesetUpgrader { if (tilesetJsonBufferWasZipped) { resultTilesetJsonBuffer = Buffers.gzip(resultTilesetJsonBuffer); } - this.tilesetTarget.addEntry( - tilesetTargetJsonFileName, - resultTilesetJsonBuffer - ); + return resultTilesetJsonBuffer; } /** @@ -358,6 +358,15 @@ export class TilesetUpgrader { } else { this.logCallback(` Not upgrading GLB in ${key} (disabled via option)`); } + } else if (type == ContentDataTypes.CONTENT_TYPE_TILESET) { + if (this.upgradeOptions.upgradeExternalTilesets) { + this.logCallback(` Upgrading external tileset in ${key}`); + value = await this.upgradeInternal(key); + } else { + this.logCallback( + ` Not upgrading external tileset in ${key} (disabled via option)` + ); + } } else { this.logCallback(` No upgrade operation to perform for ${key}`); } From ebe9d91f8ee7e56e6f3a37839f9c03f9577eb45e Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 5 Apr 2023 20:10:34 +0200 Subject: [PATCH 37/60] Delegate both public methods to an internal one --- src/io/TilesetSourceResourceResolver.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/io/TilesetSourceResourceResolver.ts b/src/io/TilesetSourceResourceResolver.ts index 79804735..48924522 100644 --- a/src/io/TilesetSourceResourceResolver.ts +++ b/src/io/TilesetSourceResourceResolver.ts @@ -21,6 +21,22 @@ export class TilesetSourceResourceResolver implements ResourceResolver { /** {@inheritDoc ResourceResolver.resolveData} */ async resolveData(uri: string): Promise { + return this.resolveDataInternal(uri); + } + + /** {@inheritDoc ResourceResolver.resolveDataPartial} */ + async resolveDataPartial( + uri: string, + maxBytes: number + ): Promise { + const buffer = await this.resolveDataInternal(uri); + if (!buffer) { + return null; + } + return buffer.subarray(0, maxBytes); + } + + private async resolveDataInternal(uri: string): Promise { if (Uris.isDataUri(uri)) { const data = Buffer.from(uri.split(",")[1], "base64"); return data; @@ -36,15 +52,6 @@ export class TilesetSourceResourceResolver implements ResourceResolver { return value; } - /** {@inheritDoc ResourceResolver.resolveDataPartial} */ - async resolveDataPartial( - uri: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - maxBytes: number - ): Promise { - return await this.resolveData(uri); - } - /** {@inheritDoc ResourceResolver.derive} */ derive(uri: string): ResourceResolver { const resolved = Paths.join(this._basePath, decodeURIComponent(uri)); From 7b20706ee95548dbb95be6f0e50f603503a6987b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 5 Apr 2023 20:10:51 +0200 Subject: [PATCH 38/60] Very basic comments for ContentOps --- src/contentProcessing/ContentOps.ts | 74 +++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/contentProcessing/ContentOps.ts b/src/contentProcessing/ContentOps.ts index b7e22e67..c00dd7e5 100644 --- a/src/contentProcessing/ContentOps.ts +++ b/src/contentProcessing/ContentOps.ts @@ -2,23 +2,66 @@ import { GltfUtilities } from "./GtlfUtilities"; import { TileFormats } from "../tileFormats/TileFormats"; +/** + * Low-level operations on tile content. + * + * The methods in this class are supposed to represent basic + * operations that receive a buffer with tile content data, + * and return a buffer with tile content data. + * + * They are used for implementing some of the command line + * functionalities (like `b3dmToGlb`), as well as serving + * as building blocks for the tileset content processing + * in pipelines. + */ export class ContentOps { + /** + * Extracts the GLB buffer from the given B3DM buffer. + * + * @param inputBuffer - The input buffer + * @returns The resulting buffer + */ static b3dmToGlbBuffer(inputBuffer: Buffer): Buffer { const inputTileData = TileFormats.readTileData(inputBuffer); const outputBuffer = inputTileData.payload; return outputBuffer; } + /** + * Extracts the GLB buffer from the given I3DM buffer. + * + * @param inputBuffer - The input buffer + * @returns The resulting buffer + */ static i3dmToGlbBuffer(inputBuffer: Buffer): Buffer { const inputTileData = TileFormats.readTileData(inputBuffer); const outputBuffer = inputTileData.payload; return outputBuffer; } + /** + * Extracts all GLB buffers from the given CMPT buffer. + * + * This will recursively resolve all inner tiles. If they + * are B3DM or I3DM tiles, then their GLBs will be added + * to the results array, in unspecified order. + * + * @param inputBuffer - The input buffer + * @returns The resulting buffers + */ static cmptToGlbBuffers(inputBuffer: Buffer): Buffer[] { return TileFormats.extractGlbBuffers(inputBuffer); } + /** + * Creates a B3DM buffer from the given GLB buffer. + * + * This will create a B3DM that contains the minimum required + * default feature table, and the given GLB as its payload. + * + * @param inputBuffer - The input buffer + * @returns The resulting buffer + */ static glbToB3dmBuffer(inputBuffer: Buffer): Buffer { const outputTileData = TileFormats.createDefaultB3dmTileDataFromGlb(inputBuffer); @@ -26,6 +69,15 @@ export class ContentOps { return outputBuffer; } + /** + * Creates a I3DM buffer from the given GLB buffer. + * + * This will create an I3DM that contains the minimum required + * default feature table, and the given GLB as its payload. + * + * @param inputBuffer - The input buffer + * @returns The resulting buffer + */ static glbToI3dmBuffer(inputBuffer: Buffer): Buffer { const outputTileData = TileFormats.createDefaultI3dmTileDataFromGlb(inputBuffer); @@ -33,6 +85,17 @@ export class ContentOps { return outputBuffer; } + /** + * Optimize the GLB that is contained in the given B3DM. + * + * This will optimize the GLB in the given B3DM, using `gltf-pipeline` + * with the given options, and create a new B3DM from the result. + * The result will have the same feature- and batch table data + * as the given input. + * + * @param inputBuffer - The input buffer + * @returns The resulting buffer + */ static async optimizeB3dmBuffer( inputBuffer: Buffer, options: any @@ -51,6 +114,17 @@ export class ContentOps { return outputBuffer; } + /** + * Optimize the GLB that is contained in the given I3DM. + * + * This will optimize the GLB in the given I3DM, using `gltf-pipeline` + * with the given options, and create a new I3DM from the result. + * The result will have the same feature- and batch table data + * as the given input. + * + * @param inputBuffer - The input buffer + * @returns The resulting buffer + */ static async optimizeI3dmBuffer( inputBuffer: Buffer, options: any From 5bfdc0a9a300af9571f01c394e60429ce30572b9 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 5 Apr 2023 20:11:10 +0200 Subject: [PATCH 39/60] Fix comment in TilesetUpgrader --- src/tilesetProcessing/TilesetUpgrader.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tilesetProcessing/TilesetUpgrader.ts b/src/tilesetProcessing/TilesetUpgrader.ts index 5e1ca087..1d190fae 100644 --- a/src/tilesetProcessing/TilesetUpgrader.ts +++ b/src/tilesetProcessing/TilesetUpgrader.ts @@ -231,8 +231,8 @@ export class TilesetUpgrader { * * This will examine each `tile.content` in the explicit representation * of the tile hierarchy in the given tileset. If any content does not - * define a `uri`, but a (legacy) `url` property, then a warning is - * printed and the `url` is renamed to `uri`. + * define a `uri`, but a (legacy) `url` property, then the `url` is + * renamed to `uri`. * * @param tileset - The tileset */ From bd38374c13aee12a4c1095cb354eec85682cfe07 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 5 Apr 2023 20:12:02 +0200 Subject: [PATCH 40/60] Add RTC handling to GltfUtilities --- specs/GltfUtilitiesSpec.ts | 96 ++++++++++++++++++++++++++ src/contentProcessing/GtlfUtilities.ts | 69 ++++++++++++++++++ src/tilesets/Extensions.ts | 86 +++++++++++++---------- 3 files changed, 214 insertions(+), 37 deletions(-) create mode 100644 specs/GltfUtilitiesSpec.ts diff --git a/specs/GltfUtilitiesSpec.ts b/specs/GltfUtilitiesSpec.ts new file mode 100644 index 00000000..3ffcfccc --- /dev/null +++ b/specs/GltfUtilitiesSpec.ts @@ -0,0 +1,96 @@ +import GltfPipeline from "gltf-pipeline"; + +import { GltfUtilities } from "../src/contentProcessing/GtlfUtilities"; + +// A glTF that uses CESIUM_RTC. +// It defines two scenes +// - scene 0 with nodes 0 and 1 +// - scene 1 with nodes 2 and 3 +const inputGltfWithCesiumRtc: any = { + asset: { + version: "2.0", + }, + extensionsUsed: ["CESIUM_RTC"], + extensionsRequired: ["CESIUM_RTC"], + extensions: { + CESIUM_RTC: { + center: [123.456, 234.567, 345.678], + }, + }, + scene: 0, + scenes: [ + { + nodes: [0, 1], + }, + { + nodes: [2, 3], + }, + ], + nodes: [ + { + name: "node0", + }, + { + name: "node1", + }, + { + name: "node2", + }, + { + name: "node3", + }, + { + name: "node4", + }, + { + name: "node5", + }, + ], +}; + +describe("GltfUtilities", function () { + it("replaceCesiumRtcExtension replaces the CESIUM_RTC extension", async function () { + const inputGltf = inputGltfWithCesiumRtc; + const rtcTranslation = inputGltf.extensions["CESIUM_RTC"].center; + const options = { + keepUnusedElements: true, + }; + + // Create a GLB from the input glTF + const glbResults = await GltfPipeline.gltfToGlb(inputGltf, options); + const inputGlb = glbResults.glb; + + // Remove the RTC extension + const outputGlb = await GltfUtilities.replaceCesiumRtcExtension(inputGlb); + + // Create a glTF from the resulting GLB + const gltfResults = await GltfPipeline.glbToGltf(outputGlb, options); + const outputGltf = gltfResults.gltf; + + // There are 10 nodes, namely the 6 existing ones, plus 4 new roots + expect(outputGltf.nodes.length).toBe(10); + + // The former roots of scene 0 (nodes 0 and 1) have + // been re-parented to nodes 6 and 7 + expect(outputGltf.scenes[0].nodes).toEqual([6, 7]); + expect(outputGltf.nodes[6].children).toEqual([0]); + expect(outputGltf.nodes[7].children).toEqual([1]); + + // The former roots of scene 0 (nodes 2 and 3) have + // been re-parented to nodes 6 and 7 + expect(outputGltf.scenes[1].nodes).toEqual([8, 9]); + expect(outputGltf.nodes[8].children).toEqual([2]); + expect(outputGltf.nodes[9].children).toEqual([3]); + + // All new nodes have the RTC center as their translation + expect(outputGltf.nodes[8].translation).toEqual(rtcTranslation); + expect(outputGltf.nodes[9].translation).toEqual(rtcTranslation); + expect(outputGltf.nodes[6].translation).toEqual(rtcTranslation); + expect(outputGltf.nodes[7].translation).toEqual(rtcTranslation); + + // The extensions object and declarations have been removed + expect(outputGltf.extensions).toBeUndefined(); + expect(outputGltf.extensionsUsed).toBeUndefined(); + expect(outputGltf.extensionsRequired).toBeUndefined(); + }); +}); diff --git a/src/contentProcessing/GtlfUtilities.ts b/src/contentProcessing/GtlfUtilities.ts index ff0f2a1e..a423cb9d 100644 --- a/src/contentProcessing/GtlfUtilities.ts +++ b/src/contentProcessing/GtlfUtilities.ts @@ -1,6 +1,11 @@ import GltfPipeline from "gltf-pipeline"; + import { Buffers } from "../base/Buffers"; + import { TileFormatError } from "../tileFormats/TileFormatError"; + +import { Extensions } from "../tilesets/Extensions"; + import { GltfPipelineLegacy } from "./GltfPipelineLegacy"; /** @@ -130,4 +135,68 @@ export class GltfUtilities { const result = await GltfPipeline.processGlb(glbBuffer, options); return result.glb; } + + /** + * Given an input buffer containing a binary glTF asset, remove + * its use of the `CESIUM_RTC` extension by inserting new nodes + * (above the former root nodes) that contain the RTC center as + * their translation. + * + * @param glbBuffer The buffer containing the binary glTF. + * @returns A promise that resolves to the resulting binary glTF. + */ + static async replaceCesiumRtcExtension(glbBuffer: Buffer) { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const customStage = (gltf: any, options: any) => { + GltfUtilities.replaceCesiumRtcExtensionInternal(gltf); + return gltf; + }; + const options = { + customStages: [customStage], + keepUnusedElements: true, + }; + const result = await GltfPipeline.processGlb(glbBuffer, options); + return result.glb; + } + + /** + * Replaces the `CESIUM_RTC` extension in the given glTF object. + * + * This will insert a new parent node above each root node of + * a scene. These new parent nodes will have a `translation` + * that is directly taken from the `CESIUM_RTC` `center`. + * + * The `CESIUM_RTC` extension object and its used/required + * usage declarations will be removed. + * + * @param gltf - The glTF object + */ + private static replaceCesiumRtcExtensionInternal(gltf: any) { + const rtcExtension = gltf.extensions["CESIUM_RTC"]; + if (!rtcExtension) { + return; + } + const rtcTranslation = rtcExtension.center; + const scenes = gltf.scenes; + if (!scenes) { + return; + } + for (const scene of scenes) { + const sceneNodes = scene.nodes; + if (sceneNodes) { + for (let i = 0; i < sceneNodes.length; i++) { + const nodeIndex = sceneNodes[i]; + const newParent = { + translation: rtcTranslation, + children: [nodeIndex], + }; + const newParentIndex = gltf.nodes.length; + gltf.nodes.push(newParent); + sceneNodes[i] = newParentIndex; + } + } + } + Extensions.removeExtensionUsed(gltf, "CESIUM_RTC"); + Extensions.removeExtension(gltf, "CESIUM_RTC"); + } } diff --git a/src/tilesets/Extensions.ts b/src/tilesets/Extensions.ts index 9f01d677..1a94dd02 100644 --- a/src/tilesets/Extensions.ts +++ b/src/tilesets/Extensions.ts @@ -1,7 +1,7 @@ /** * A type for objects that can contain extensions */ -type Extended = { extensions?: object }; +type Extended = { extensions?: { [key: string]: any } }; /** * A type for objects that can contain extension declarations @@ -18,17 +18,35 @@ export class Extensions { * That is, whether the `object.extensions` contains a key * that is the given extension name. * - * @param extensible - The object that may contain the extension + * @param extended - The object that may contain the extension * @param extension The extension (i.e. its name as a string) * @returns Whether the object contains the extension */ - static contains(extended: Extended, extension: string) { + static containsExtension(extended: Extended, extension: string) { if (!extended.extensions) { return false; } return Object.keys(extended.extensions).includes(extension); } + /** + * Remove the specified extension from the `extensions` dictionary + * of the given object, deleting the `extensions` if they become + * empty. + * + * @param extended - The extended object + * @param extension The extension (i.e. its name as a string) + */ + static removeExtension(extended: Extended, extension: string) { + if (!extended.extensions) { + return; + } + delete extended.extensions[extension]; + if (Object.keys(extended.extensions).length === 0) { + delete extended.extensions; + } + } + /** * Add the given extension to the `extensionsUsed` of the given object. * @@ -39,26 +57,20 @@ export class Extensions { * @param extension - The extension name */ static addExtensionUsed(extensible: Extensible, extension: string) { - extensible.extensionsUsed = Extensions.addUnique( - extensible.extensionsUsed, - extension - ); + Extensions.addUniqueTo(extensible, "extensionsUsed", extension); } /** * Remove the given extension from the `extensionsUsed` of the given object. * - * The array will be set to `undefined` if it becomes empty, and the + * The array will be deleted if it becomes empty, and the * extension will also be removed from `extensionsRequired`. * * @param extensible - The object * @param extension - The extension name */ static removeExtensionUsed(extensible: Extensible, extension: string) { - extensible.extensionsUsed = Extensions.removeUnique( - extensible.extensionsUsed, - extension - ); + Extensions.removeFrom(extensible, "extensionsUsed", extension); Extensions.removeExtensionRequired(extensible, extension); } @@ -73,17 +85,14 @@ export class Extensions { * @param extension - The extension name */ static addExtensionRequired(extensible: Extensible, extension: string) { - extensible.extensionsRequired = Extensions.addUnique( - extensible.extensionsRequired, - extension - ); + Extensions.addUniqueTo(extensible, "extensionsRequired", extension); Extensions.addExtensionUsed(extensible, extension); } /** * Remove the given extension to the `extensionsUsed` of the given object. * - * The array will be set to `undefined` if it becomes empty. + * The array will be deleted if it becomes empty. * * This will *not* remove the extension from the `extensionsUsed`! * @@ -91,52 +100,55 @@ export class Extensions { * @param extension - The extension name */ static removeExtensionRequired(extensible: Extensible, extension: string) { - extensible.extensionsRequired = Extensions.removeUnique( - extensible.extensionsRequired, - extension - ); + Extensions.removeFrom(extensible, "extensionsRequired", extension); } /** - * Adds the given element to the given array and returns the - * array, creating a new array if the array was `undefined`. + * Adds the given element to the specified array if it was not + * contained yet, creating a new array if it did not exist yet. * - * @param array - The array + * @param object - The object containing the array + * @param key - The name of the array property * @param element - The element - * @returns The new array */ - private static addUnique(array: T[] | undefined, element: T): T[] { + private static addUniqueTo( + object: { [key: string]: T[] | undefined }, + key: string, + element: T + ) { + let array = object[key]; if (!array) { array = []; + object[key] = array; } if (!array.includes(element)) { array.push(element); } - return array; } /** - * Remove the given element from the given array and returns the - * array. If the array becomes empty, this method returns `undefined`. + * Remove the given element from the specified array. If the array + * becomes empty, it is deleted. * - * @param array - The array + * @param object - The object containing the array + * @param key - The name of the array property * @param element - The element - * @returns The new array */ - private static removeUnique( - array: T[] | undefined, + private static removeFrom( + object: { [key: string]: T[] | undefined }, + key: string, element: T - ): T[] | undefined { + ) { + const array = object[key]; if (!array) { - return undefined; + return; } const index = array.indexOf(element); if (index !== -1) { array.splice(index, 1); } if (array.length === 0) { - return undefined; + delete object[key]; } - return array; } } From 501d90c582e3f0db9cf37b27a72a0a5a4e6c822c Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Wed, 5 Apr 2023 20:12:31 +0200 Subject: [PATCH 41/60] Added test data for upgrading with external tilesets --- .../content.b3dm | Bin 0 -> 14833 bytes .../external.json | 22 ++++++++++++++++++ .../tileset.json | 22 ++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 specs/data/upgradeTileset/tilesetWithExternalTilesetWithUrls/content.b3dm create mode 100644 specs/data/upgradeTileset/tilesetWithExternalTilesetWithUrls/external.json create mode 100644 specs/data/upgradeTileset/tilesetWithExternalTilesetWithUrls/tileset.json diff --git a/specs/data/upgradeTileset/tilesetWithExternalTilesetWithUrls/content.b3dm b/specs/data/upgradeTileset/tilesetWithExternalTilesetWithUrls/content.b3dm new file mode 100644 index 0000000000000000000000000000000000000000..f052e259f0789b35b85323ae4d2567f7323efe0a GIT binary patch literal 14833 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 Date: Wed, 5 Apr 2023 20:13:02 +0200 Subject: [PATCH 42/60] Created in-memory tileset source and target for tests --- specs/LazyContentDataSpec.ts | 53 ++++----------- specs/TilesetSourceSpec.ts | 25 +++++-- specs/TilesetTargetSpec.ts | 17 +++-- src/tilesetData/TilesetInMemory.ts | 106 +++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 48 deletions(-) create mode 100644 src/tilesetData/TilesetInMemory.ts diff --git a/specs/LazyContentDataSpec.ts b/specs/LazyContentDataSpec.ts index fc80b228..9406a6d1 100644 --- a/specs/LazyContentDataSpec.ts +++ b/specs/LazyContentDataSpec.ts @@ -1,40 +1,24 @@ -import { DeveloperError } from "../src/base/DeveloperError"; +/* eslint-disable @typescript-eslint/no-unused-vars */ + import { LazyContentData } from "../src/contentTypes/LazyContentData"; import { ResourceResolver } from "../src/io/ResourceResolver"; -class SpecResourceResolver implements ResourceResolver { - private readonly dataMap: { [key: string]: Buffer } = {}; - - putData(uri: string, buffer: Buffer) { - this.dataMap[uri] = buffer; - } +import { TilesetSourceResourceResolver } from "../src/io/TilesetSourceResourceResolver"; +import { TilesetInMemory } from "../src/tilesetData/TilesetInMemory"; - async resolveData(uri: string): Promise { - const data = this.dataMap[uri] as Buffer; - if (!data) { - return null; - } - return data; - } - async resolveDataPartial( - uri: string, - maxBytes: number - ): Promise { - const data = this.dataMap[uri] as Buffer; - if (!data) { - return null; - } - return data.subarray(0, maxBytes); - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - derive(uri: string): ResourceResolver { - throw new DeveloperError("Not supposed to be called."); - } +function createTestResourceResolver(): ResourceResolver { + const tilesetSource = new TilesetInMemory(); + tilesetSource.open(""); + const resourceResolver = new TilesetSourceResourceResolver( + ".", + tilesetSource + ); + return resourceResolver; } describe("LazyContentData", function () { it("does not read data at construction", function () { - const resourceResolver = new SpecResourceResolver(); + const resourceResolver = createTestResourceResolver(); const resolveDataSpy = spyOn( resourceResolver, "resolveData" @@ -56,20 +40,18 @@ describe("LazyContentData", function () { jasmine.any(Number) ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const magic = contentData.getMagic(); expect(resolveDataSpy).toHaveBeenCalledTimes(0); expect(resolveDataPartialSpy).toHaveBeenCalledTimes(2); expect(resolveDataPartialSpy).toHaveBeenCalledWith("example.glb", 4); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const data = contentData.getData(); expect(resolveDataSpy).toHaveBeenCalledTimes(1); expect(resolveDataPartialSpy).toHaveBeenCalledTimes(2); }); it("reads only a few bytes for getMagic", function () { - const resourceResolver = new SpecResourceResolver(); + const resourceResolver = createTestResourceResolver(); const resolveDataSpy = spyOn( resourceResolver, "resolveData" @@ -83,7 +65,6 @@ describe("LazyContentData", function () { expect(resolveDataSpy).toHaveBeenCalledTimes(0); expect(resolveDataPartialSpy).toHaveBeenCalledTimes(0); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const magic = contentData.getMagic(); expect(resolveDataSpy).toHaveBeenCalledTimes(0); expect(resolveDataPartialSpy).toHaveBeenCalledTimes(1); @@ -91,7 +72,7 @@ describe("LazyContentData", function () { }); it("reads the data only once", async function () { - const resourceResolver = new SpecResourceResolver(); + const resourceResolver = createTestResourceResolver(); const resolveDataSpy = spyOn( resourceResolver, "resolveData" @@ -105,13 +86,9 @@ describe("LazyContentData", function () { expect(resolveDataSpy).toHaveBeenCalledTimes(0); expect(resolveDataPartialSpy).toHaveBeenCalledTimes(0); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const data0 = await contentData.getData(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const object0 = await contentData.getParsedObject(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const data1 = await contentData.getData(); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const object1 = await contentData.getParsedObject(); expect(resolveDataSpy).toHaveBeenCalledTimes(1); diff --git a/specs/TilesetSourceSpec.ts b/specs/TilesetSourceSpec.ts index 01ef567c..c73512ea 100644 --- a/specs/TilesetSourceSpec.ts +++ b/specs/TilesetSourceSpec.ts @@ -1,24 +1,39 @@ import { TilesetSource } from "../src/tilesetData/TilesetSource"; import { TilesetSourceFs } from "../src/tilesetData/TilesetSourceFs"; +import { TilesetInMemory } from "../src/tilesetData/TilesetInMemory"; + import { TilesetSource3tz } from "../src/packages/TilesetSource3tz"; import { TilesetSource3dtiles } from "../src/packages/TilesetSource3dtiles"; +async function createTilesetInMemory() { + const tileset = new TilesetInMemory(); + tileset.begin("", true); + tileset.addEntry("tileset.json", Buffer.alloc(0)); + await tileset.end(); + return tileset; +} + // The basic contract that is established by the `TilesetSource` // interface is checked for these implementations: const testCases = [ { description: "TilesetSourceFs", - constructorFunction: TilesetSourceFs, + creationFunction: () => new TilesetSourceFs(), sourceName: "./specs/data/Tileset/", }, { description: "TilesetSource3tz", - constructorFunction: TilesetSource3tz, + creationFunction: () => new TilesetSource3tz(), sourceName: "./specs/data/tileset.3tz", }, { description: "TilesetSource3dtiles", - constructorFunction: TilesetSource3dtiles, + creationFunction: () => new TilesetSource3dtiles(), + sourceName: "./specs/data/tileset.3dtiles", + }, + { + description: "TilesetInMemory", + creationFunction: createTilesetInMemory, sourceName: "./specs/data/tileset.3dtiles", }, ]; @@ -28,8 +43,8 @@ for (const testCase of testCases) { let tilesetSource: TilesetSource; let sourceName: string; - beforeEach(function () { - tilesetSource = new testCase.constructorFunction(); + beforeEach(async function () { + tilesetSource = await testCase.creationFunction(); sourceName = testCase.sourceName; }); diff --git a/specs/TilesetTargetSpec.ts b/specs/TilesetTargetSpec.ts index beffca7c..ef478542 100644 --- a/specs/TilesetTargetSpec.ts +++ b/specs/TilesetTargetSpec.ts @@ -1,5 +1,7 @@ import { TilesetTarget } from "../src/tilesetData/TilesetTarget"; import { TilesetTargetFs } from "../src/tilesetData/TilesetTargetFs"; +import { TilesetInMemory } from "../src/tilesetData/TilesetInMemory"; + import { TilesetTarget3tz } from "../src/packages/TilesetTarget3tz"; import { TilesetTarget3dtiles } from "../src/packages/TilesetTarget3dtiles"; @@ -8,19 +10,24 @@ import { TilesetTarget3dtiles } from "../src/packages/TilesetTarget3dtiles"; const testCases = [ { description: "TilesetTargetFs", - constructorFunction: TilesetTargetFs, + creationFunction: () => new TilesetTargetFs(), targetName: "./specs/data/output/Tileset/", }, { description: "TilesetTarget3tz", - constructorFunction: TilesetTarget3tz, + creationFunction: () => new TilesetTarget3tz(), targetName: "./specs/data/output/tileset.3tz", }, { description: "TilesetTarget3dtiles", - constructorFunction: TilesetTarget3dtiles, + creationFunction: () => new TilesetTarget3dtiles(), targetName: "./specs/data/output/tileset.3dtiles", }, + { + description: "TilesetInMemory", + creationFunction: () => new TilesetInMemory(), + targetName: "", + }, ]; for (const testCase of testCases) { @@ -28,8 +35,8 @@ for (const testCase of testCases) { let tilesetTarget: TilesetTarget; let targetName: string; - beforeEach(function () { - tilesetTarget = new testCase.constructorFunction(); + beforeEach(async function () { + tilesetTarget = testCase.creationFunction(); targetName = testCase.targetName; }); diff --git a/src/tilesetData/TilesetInMemory.ts b/src/tilesetData/TilesetInMemory.ts new file mode 100644 index 00000000..75f50a97 --- /dev/null +++ b/src/tilesetData/TilesetInMemory.ts @@ -0,0 +1,106 @@ +import { TilesetSource } from "./TilesetSource"; +import { TilesetError } from "./TilesetError"; +import { TilesetTarget } from "./TilesetTarget"; + +/** + * Implementation of a TilesetSource and TilesetTarget that + * stores the data in memory. + * + * This is mainly intended for tests and debugging. + * + * @internal + */ +export class TilesetInMemory implements TilesetSource, TilesetTarget { + /** + * The mapping from keys to the actual data + */ + private readonly dataMap: { [key: string]: Buffer } = {}; + + /** + * Whether this source has already been opened + */ + private sourceIsOpen: boolean; + + /** + * Whether this target has already been opened + */ + private targetIsOpen: boolean; + + /** + * The overwrite flag for the target + */ + private overwrite: boolean; + + /** + * Default constructor + */ + constructor() { + this.sourceIsOpen = false; + this.targetIsOpen = false; + this.overwrite = false; + } + + /** {@inheritDoc TilesetSource.open} */ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + open(fullInputName: string) { + if (this.sourceIsOpen) { + throw new TilesetError("Source already opened"); + } + this.sourceIsOpen = true; + } + + /** {@inheritDoc TilesetSource.getKeys} */ + getKeys() { + if (!this.sourceIsOpen) { + throw new TilesetError("Source is not opened. Call 'open' first."); + } + return Object.keys(this.dataMap).values(); + } + + /** {@inheritDoc TilesetSource.getValue} */ + getValue(key: string): Buffer | undefined { + if (!this.sourceIsOpen) { + throw new TilesetError("Source is not opened. Call 'open' first."); + } + return this.dataMap[key]; + } + + /** {@inheritDoc TilesetSource.close} */ + close() { + if (!this.sourceIsOpen) { + throw new TilesetError("Source is not opened. Call 'open' first."); + } + this.sourceIsOpen = false; + } + + /** {@inheritDoc TilesetTarget.begin} */ + begin(fullOutputName: string, overwrite: boolean) { + if (this.targetIsOpen) { + throw new TilesetError("Target already opened"); + } + this.targetIsOpen = true; + this.overwrite = overwrite; + } + + /** {@inheritDoc TilesetTarget.addEntry} */ + addEntry(key: string, content: Buffer) { + if (!this.targetIsOpen) { + throw new TilesetError("Target is not opened. Call 'begin' first."); + } + if (this.dataMap[key]) { + if (!this.overwrite) { + throw new TilesetError("Entry already exists: " + key); + } + } + this.dataMap[key] = content; + } + + /** {@inheritDoc TilesetTarget.end} */ + async end() { + if (!this.targetIsOpen) { + throw new TilesetError("Target is not opened. Call 'begin' first."); + } + this.targetIsOpen = false; + this.overwrite = false; + } +} From 40fe9cce948c149f23f38d0b7a5fdfe4f5374368 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Thu, 6 Apr 2023 15:27:41 +0200 Subject: [PATCH 43/60] Updated legacy specs for tileset combination --- specs/SpecHelpers.ts | 73 +++++++++++++++++++ specs/TileFormatsSpec.ts | 2 +- specs/TilesetCombinerSpec.ts | 57 +++++++++++++++ .../{ => nestedExternal}/README.md | 0 .../{ => nestedExternal}/externalA.json | 0 .../{ => nestedExternal}/sub0/externalB.json | 0 .../sub0/sub01/tileD.b3dm | 0 .../{ => nestedExternal}/sub0/tileC.b3dm | 0 .../{ => nestedExternal}/sub1/externalC.json | 0 .../sub1/sub10/tileF.b3dm | 0 .../{ => nestedExternal}/sub1/tileE.b3dm | 0 .../{ => nestedExternal}/tileA.b3dm | 0 .../{ => nestedExternal}/tileB.b3dm | 0 .../{ => nestedExternal}/tileset.json | 0 specs/legacy/README.md | 5 -- specs/legacy/SpecHelpers.ts | 46 ------------ specs/legacy/combineTilesetSpec.ts | 36 --------- 17 files changed, 131 insertions(+), 88 deletions(-) create mode 100644 specs/SpecHelpers.ts create mode 100644 specs/TilesetCombinerSpec.ts rename specs/data/combineTilesets/{ => nestedExternal}/README.md (100%) rename specs/data/combineTilesets/{ => nestedExternal}/externalA.json (100%) rename specs/data/combineTilesets/{ => nestedExternal}/sub0/externalB.json (100%) rename specs/data/combineTilesets/{ => nestedExternal}/sub0/sub01/tileD.b3dm (100%) rename specs/data/combineTilesets/{ => nestedExternal}/sub0/tileC.b3dm (100%) rename specs/data/combineTilesets/{ => nestedExternal}/sub1/externalC.json (100%) rename specs/data/combineTilesets/{ => nestedExternal}/sub1/sub10/tileF.b3dm (100%) rename specs/data/combineTilesets/{ => nestedExternal}/sub1/tileE.b3dm (100%) rename specs/data/combineTilesets/{ => nestedExternal}/tileA.b3dm (100%) rename specs/data/combineTilesets/{ => nestedExternal}/tileB.b3dm (100%) rename specs/data/combineTilesets/{ => nestedExternal}/tileset.json (100%) delete mode 100644 specs/legacy/README.md delete mode 100644 specs/legacy/SpecHelpers.ts delete mode 100644 specs/legacy/combineTilesetSpec.ts diff --git a/specs/SpecHelpers.ts b/specs/SpecHelpers.ts new file mode 100644 index 00000000..676d85db --- /dev/null +++ b/specs/SpecHelpers.ts @@ -0,0 +1,73 @@ +import fs from "fs"; + +import { Iterables } from "../src/base/Iterables"; +import { Paths } from "../src/base/Paths"; +import { Tile } from "../src/structure/Tile"; +import { Tiles } from "../src/tilesets/Tiles"; + +/** + * Utility methods for the specs + */ +export class SpecHelpers { + /** + * Returns the given byte length, padded if necessary to + * be a multiple of 8 + * + * @param byteLength - The byte length + * @returns The padded byte length + */ + static getPaddedByteLength(byteLength: number): number { + const boundary = 8; + const remainder = byteLength % boundary; + const padding = remainder === 0 ? 0 : boundary - remainder; + return byteLength + padding; + } + + /** + * Forcefully deletes the given directory and all its contents + * and subdirectories. Be careful. + * + * @param directory - The directory to delete + */ + static forceDeleteDirectory(directory: string) { + fs.rmSync(directory, { + force: true, + recursive: true, + }); + } + + /** + * Returns an array that contains the names of all files in + * the given directory and its subdirectories, relative to + * the given directory (with `/` as the path separator), + * in unspecified order. + * + * @param directory - The directory + * @returns The relative file names + */ + static collectRelativeFileNames(directory: string): string[] { + const allFiles = Iterables.overFiles(directory, true); + const relativeFiles = Iterables.map(allFiles, (file: string) => + Paths.relativize(directory, file) + ); + return Array.from(relativeFiles); + } + + /** + * Collect all content URIs that appear in the given tile or + * any of its descendants, in unspecified order. + * + * @param startTile - The start tile + * @returns A promise to all content URIs + */ + static async collectExplicitContentUris(startTile: Tile) { + const allContentUris: string[] = []; + await Tiles.traverseExplicit(startTile, async (tiles: Tile[]) => { + const tile = tiles[tiles.length - 1]; + const contentUris = Tiles.getContentUris(tile); + allContentUris.push(...contentUris); + return true; + }); + return allContentUris; + } +} diff --git a/specs/TileFormatsSpec.ts b/specs/TileFormatsSpec.ts index ec7784ec..45017fdb 100644 --- a/specs/TileFormatsSpec.ts +++ b/specs/TileFormatsSpec.ts @@ -2,7 +2,7 @@ import fs from "fs"; import { Buffers } from "../src/base/Buffers"; import { TileFormats } from "../src/tileFormats/TileFormats"; -import { SpecHelpers } from "./legacy/SpecHelpers"; +import { SpecHelpers } from "./SpecHelpers"; describe("TileFormats", function () { it("reads B3DM (deprecated 1) from a buffer", function () { diff --git a/specs/TilesetCombinerSpec.ts b/specs/TilesetCombinerSpec.ts new file mode 100644 index 00000000..47df7663 --- /dev/null +++ b/specs/TilesetCombinerSpec.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import fs from "fs"; + +import { Paths } from "../src/base/Paths"; + +import { Tilesets } from "../src/tilesets/Tilesets"; + +import { SpecHelpers } from "./SpecHelpers"; + +describe("TilesetCombiner", function () { + it("combines external tilesets into a single tileset", async function () { + const tilesetSourceName = "./specs/data/combineTilesets/nestedExternal"; + const tilesetTargetName = + "./specs/data/output/combineTilesets/nestedExternal"; + const overwrite = true; + + await Tilesets.combine(tilesetSourceName, tilesetTargetName, 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(tilesetTargetName); + actualRelativeFiles.sort(); + const expectedRelativeFiles = [ + "README.md", + "sub0/sub01/tileD.b3dm", + "sub0/tileC.b3dm", + "sub1/sub10/tileF.b3dm", + "sub1/tileE.b3dm", + "tileA.b3dm", + "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(tilesetTargetName, "tileset.json") + ); + const tileset = JSON.parse(tilesetJsonBuffer.toString()); + const actualContentUris = await SpecHelpers.collectExplicitContentUris( + tileset.root + ); + actualContentUris.sort(); + + const expectedContentUris = [ + "sub0/sub01/tileD.b3dm", + "sub0/tileC.b3dm", + "sub1/sub10/tileF.b3dm", + "sub1/tileE.b3dm", + "tileA.b3dm", + "tileB.b3dm", + ]; + expect(actualContentUris).toEqual(expectedContentUris); + }); +}); diff --git a/specs/data/combineTilesets/README.md b/specs/data/combineTilesets/nestedExternal/README.md similarity index 100% rename from specs/data/combineTilesets/README.md rename to specs/data/combineTilesets/nestedExternal/README.md diff --git a/specs/data/combineTilesets/externalA.json b/specs/data/combineTilesets/nestedExternal/externalA.json similarity index 100% rename from specs/data/combineTilesets/externalA.json rename to specs/data/combineTilesets/nestedExternal/externalA.json diff --git a/specs/data/combineTilesets/sub0/externalB.json b/specs/data/combineTilesets/nestedExternal/sub0/externalB.json similarity index 100% rename from specs/data/combineTilesets/sub0/externalB.json rename to specs/data/combineTilesets/nestedExternal/sub0/externalB.json diff --git a/specs/data/combineTilesets/sub0/sub01/tileD.b3dm b/specs/data/combineTilesets/nestedExternal/sub0/sub01/tileD.b3dm similarity index 100% rename from specs/data/combineTilesets/sub0/sub01/tileD.b3dm rename to specs/data/combineTilesets/nestedExternal/sub0/sub01/tileD.b3dm diff --git a/specs/data/combineTilesets/sub0/tileC.b3dm b/specs/data/combineTilesets/nestedExternal/sub0/tileC.b3dm similarity index 100% rename from specs/data/combineTilesets/sub0/tileC.b3dm rename to specs/data/combineTilesets/nestedExternal/sub0/tileC.b3dm diff --git a/specs/data/combineTilesets/sub1/externalC.json b/specs/data/combineTilesets/nestedExternal/sub1/externalC.json similarity index 100% rename from specs/data/combineTilesets/sub1/externalC.json rename to specs/data/combineTilesets/nestedExternal/sub1/externalC.json diff --git a/specs/data/combineTilesets/sub1/sub10/tileF.b3dm b/specs/data/combineTilesets/nestedExternal/sub1/sub10/tileF.b3dm similarity index 100% rename from specs/data/combineTilesets/sub1/sub10/tileF.b3dm rename to specs/data/combineTilesets/nestedExternal/sub1/sub10/tileF.b3dm diff --git a/specs/data/combineTilesets/sub1/tileE.b3dm b/specs/data/combineTilesets/nestedExternal/sub1/tileE.b3dm similarity index 100% rename from specs/data/combineTilesets/sub1/tileE.b3dm rename to specs/data/combineTilesets/nestedExternal/sub1/tileE.b3dm diff --git a/specs/data/combineTilesets/tileA.b3dm b/specs/data/combineTilesets/nestedExternal/tileA.b3dm similarity index 100% rename from specs/data/combineTilesets/tileA.b3dm rename to specs/data/combineTilesets/nestedExternal/tileA.b3dm diff --git a/specs/data/combineTilesets/tileB.b3dm b/specs/data/combineTilesets/nestedExternal/tileB.b3dm similarity index 100% rename from specs/data/combineTilesets/tileB.b3dm rename to specs/data/combineTilesets/nestedExternal/tileB.b3dm diff --git a/specs/data/combineTilesets/tileset.json b/specs/data/combineTilesets/nestedExternal/tileset.json similarity index 100% rename from specs/data/combineTilesets/tileset.json rename to specs/data/combineTilesets/nestedExternal/tileset.json diff --git a/specs/legacy/README.md b/specs/legacy/README.md deleted file mode 100644 index 2b3aefdd..00000000 --- a/specs/legacy/README.md +++ /dev/null @@ -1,5 +0,0 @@ -The tests in this directory are largely "ported" from -https://github.com/CesiumGS/3d-tiles-tools/tree/98b5d1e5369d5b2c2c6860b9c0ba3890e886ba25 - -When the respective functionality is covered with new, dedicated -tests, these may be removed. diff --git a/specs/legacy/SpecHelpers.ts b/specs/legacy/SpecHelpers.ts deleted file mode 100644 index 932caecd..00000000 --- a/specs/legacy/SpecHelpers.ts +++ /dev/null @@ -1,46 +0,0 @@ -import fs from "fs"; -import path from "path"; - -import { Iterables } from "../../src/base/Iterables"; - -export class SpecHelpers { - static getPaddedByteLength(byteLength: number): number { - const boundary = 8; - const remainder = byteLength % boundary; - const padding = remainder === 0 ? 0 : boundary - remainder; - return byteLength + padding; - } - - static forceDeleteDirectory(p: string) { - fs.rmSync(p, { - force: true, - recursive: true, - }); - } - - static isJson(file: string): boolean { - return path.extname(file) === ".json"; - } - - static getContentUris(string: string): string[] { - const regex = new RegExp('"uri"\\s?:\\s?"([^"]*)"', "g"); - const matches = string.matchAll(regex); - const uris: string[] = []; - for (const match of matches) { - uris.push(match[1]); - } - return uris; - } - - static getNumberOfTilesets(directory: string): number { - const recurse = true; - const files = Iterables.overFiles(directory, recurse); - let numberOfJsonFiles = 0; - for (const file of files) { - if (SpecHelpers.isJson(file)) { - numberOfJsonFiles++; - } - } - return numberOfJsonFiles; - } -} diff --git a/specs/legacy/combineTilesetSpec.ts b/specs/legacy/combineTilesetSpec.ts deleted file mode 100644 index 32383976..00000000 --- a/specs/legacy/combineTilesetSpec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import fs from "fs"; - -import { Tilesets } from "../../src/tilesets/Tilesets"; -import { SpecHelpers } from "./SpecHelpers"; - -const tilesetDirectory = "./specs/data/TilesetOfTilesetsWithUris/"; -const combinedDirectory = - "./specs/data/output/TilesetOfTilesetsWithUris-combined"; -const combinedJson = - "./specs/data/output/TilesetOfTilesetsWithUris-combined/tileset.json"; - -describe("combineTileset", function () { - afterEach(async function () { - //SpecHelpers.forceDeleteDirectory(combinedDirectory); - }); - - it("combines external tilesets into a single tileset", async function () { - const overwrite = true; - await Tilesets.combine(tilesetDirectory, combinedDirectory, overwrite); - - const numberOfTilesets = SpecHelpers.getNumberOfTilesets(combinedDirectory); - - const combinedJsonBuffer = fs.readFileSync(combinedJson); - const combinedJsonString = combinedJsonBuffer.toString(); - const contentUris = SpecHelpers.getContentUris(combinedJsonString); - - expect(numberOfTilesets).toBe(1); - expect(contentUris).toEqual([ - "parent.b3dm", - "tileset3/ll.b3dm", - "lr.b3dm", - "ur.b3dm", - "ul.b3dm", - ]); - }); -}); From c519f666df1cedd0b23b44ad65eb9043ee4f0a66 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 9 Apr 2023 04:33:32 +0200 Subject: [PATCH 44/60] Test for TilesetMerger and TilesetProcessor --- specs/BasicTilesetProcessorSpec.ts | 203 ++++++++++++++++++ specs/TilesetCombinerSpec.ts | 1 - specs/TilesetMergerSpec.ts | 61 ++++++ specs/TilesetProcessorSpec.ts | 79 +++++++ .../{ => basicMerge}/TilesetA/ll.b3dm | Bin .../{ => basicMerge}/TilesetA/lr.b3dm | Bin .../{ => basicMerge}/TilesetA/parent.b3dm | Bin .../{ => basicMerge}/TilesetA/tileset.json | 0 .../{ => basicMerge}/TilesetA/ul.b3dm | Bin .../{ => basicMerge}/TilesetA/ur.b3dm | Bin .../{ => basicMerge}/sub/TilesetA/ll.b3dm | Bin .../{ => basicMerge}/sub/TilesetA/lr.b3dm | Bin .../{ => basicMerge}/sub/TilesetA/parent.b3dm | Bin .../sub/TilesetA/tileset.json | 0 .../{ => basicMerge}/sub/TilesetA/ul.b3dm | Bin .../{ => basicMerge}/sub/TilesetA/ur.b3dm | Bin .../basicProcessing/README.md | 19 ++ .../basicProcessing/sub/tileB.b3dm | 0 .../basicProcessing/tileA.b3dm | 0 .../basicProcessing/tileB.b3dm | 0 .../basicProcessing/tileC.b3dm | 0 .../basicProcessing/tileset.json | 47 ++++ .../BasicTilesetProcessor.ts | 134 +++++------- src/tilesetProcessing/TilesetProcessor.ts | 130 ++++++----- .../TilesetProcessorContext.ts | 7 + 25 files changed, 548 insertions(+), 133 deletions(-) create mode 100644 specs/BasicTilesetProcessorSpec.ts create mode 100644 specs/TilesetMergerSpec.ts create mode 100644 specs/TilesetProcessorSpec.ts rename specs/data/mergeTilesets/{ => basicMerge}/TilesetA/ll.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/TilesetA/lr.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/TilesetA/parent.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/TilesetA/tileset.json (100%) rename specs/data/mergeTilesets/{ => basicMerge}/TilesetA/ul.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/TilesetA/ur.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/sub/TilesetA/ll.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/sub/TilesetA/lr.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/sub/TilesetA/parent.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/sub/TilesetA/tileset.json (100%) rename specs/data/mergeTilesets/{ => basicMerge}/sub/TilesetA/ul.b3dm (100%) rename specs/data/mergeTilesets/{ => basicMerge}/sub/TilesetA/ur.b3dm (100%) create mode 100644 specs/data/tilesetProcessing/basicProcessing/README.md create mode 100644 specs/data/tilesetProcessing/basicProcessing/sub/tileB.b3dm create mode 100644 specs/data/tilesetProcessing/basicProcessing/tileA.b3dm create mode 100644 specs/data/tilesetProcessing/basicProcessing/tileB.b3dm create mode 100644 specs/data/tilesetProcessing/basicProcessing/tileC.b3dm create mode 100644 specs/data/tilesetProcessing/basicProcessing/tileset.json diff --git a/specs/BasicTilesetProcessorSpec.ts b/specs/BasicTilesetProcessorSpec.ts new file mode 100644 index 00000000..d23a297f --- /dev/null +++ b/specs/BasicTilesetProcessorSpec.ts @@ -0,0 +1,203 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import fs from "fs"; + +import { SpecHelpers } from "./SpecHelpers"; + +import { Paths } from "../src/base/Paths"; + +import { BasicTilesetProcessor } from "../src/tilesetProcessing/BasicTilesetProcessor"; + +import { Tiles } from "../src/tilesets/Tiles"; + +import { Tile } from "../src/structure/Tile"; + +import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; + +const basicInput = "./specs/data/tilesetProcessing/basicProcessing"; +const basicOutput = "./specs/data/output/tilesetProcessing/basicProcessing"; +const overwrite = true; + +// Utility class that offers methods for a "dummy" modification +// of the URIs (file names) and content processing, and stores +// all processed entry names +class SpecProcessor { + processedKeys: string[] = []; + + processUri = (uri: string) => { + return "PROCESSED_" + uri; + }; + + processEntry = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + this.processedKeys.push(sourceEntry.key); + return { + key: this.processUri(sourceEntry.key), + value: sourceEntry.value, + }; + }; +} + +describe("BasicTilesetProcessor", function () { + afterEach(function () { + //SpecHelpers.forceDeleteDirectory(basicOutput); + }); + + it("forEachExplicitTile covers all tiles", async function () { + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + + const actualContentUris: string[][] = []; + await tilesetProcessor.forEachExplicitTile(async (tile: Tile) => { + const contentUris = Tiles.getContentUris(tile); + actualContentUris.push(contentUris); + }); + await tilesetProcessor.end(); + + const expectedContentUris = [ + ["tileA.b3dm"], + ["tileB.b3dm", "sub/tileB.b3dm"], + ["tileC.b3dm"], + ["tileA.b3dm"], + ]; + expect(actualContentUris).toEqual(expectedContentUris); + }); + + it("processAllEntries processes all entries exactly once", async function () { + // XXX DEBUG + SpecHelpers.forceDeleteDirectory(basicOutput); + + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + const specProcessor = new SpecProcessor(); + await tilesetProcessor.processAllEntries(specProcessor.processEntry); + await tilesetProcessor.end(); + + // Expect ALL files to have been processed + // (except for 'tileset.json') + const expectedProcessedKeys = [ + "README.md", + "sub/tileB.b3dm", + "tileA.b3dm", + "tileB.b3dm", + "tileC.b3dm", + ]; + const actualProcessedKeys = specProcessor.processedKeys; + actualProcessedKeys.sort(); + expect(actualProcessedKeys).toEqual(expectedProcessedKeys); + + // Expect the names of ALL files to have been modified + // (except for 'tileset.json') + const expectedOutputFiles = [ + "PROCESSED_README.md", + "PROCESSED_sub/tileB.b3dm", + "PROCESSED_tileA.b3dm", + "PROCESSED_tileB.b3dm", + "PROCESSED_tileC.b3dm", + "tileset.json", + ]; + const actualOutputFiles = SpecHelpers.collectRelativeFileNames(basicOutput); + actualOutputFiles.sort(); + expect(actualOutputFiles).toEqual(expectedOutputFiles); + }); + + it("processTileContentEntries processes the tile content entries", async function () { + // XXX DEBUG + SpecHelpers.forceDeleteDirectory(basicOutput); + + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + const specProcessor = new SpecProcessor(); + await tilesetProcessor.processTileContentEntries( + specProcessor.processUri, + specProcessor.processEntry + ); + await tilesetProcessor.end(); + + // Expect the content files to have been processed + const expectedProcessedKeys = [ + "sub/tileB.b3dm", + "tileA.b3dm", + "tileB.b3dm", + "tileC.b3dm", + ]; + const actualProcessedKeys = specProcessor.processedKeys; + actualProcessedKeys.sort(); + expect(actualProcessedKeys).toEqual(expectedProcessedKeys); + + // Expect the names of content files to have been modified + const expectedOutputFiles = [ + "PROCESSED_sub/tileB.b3dm", + "PROCESSED_tileA.b3dm", + "PROCESSED_tileB.b3dm", + "PROCESSED_tileC.b3dm", + "README.md", + "tileset.json", + ]; + const actualOutputFiles = SpecHelpers.collectRelativeFileNames(basicOutput); + actualOutputFiles.sort(); + expect(actualOutputFiles).toEqual(expectedOutputFiles); + }); + + it("processTileContentEntries updates the content URIs", async function () { + // XXX DEBUG + SpecHelpers.forceDeleteDirectory(basicOutput); + + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + const specProcessor = new SpecProcessor(); + await tilesetProcessor.processTileContentEntries( + specProcessor.processUri, + specProcessor.processEntry + ); + await tilesetProcessor.end(); + + // Ensure that the 'tileset.json' contains the + // proper content URIs for the processed output + const tilesetJsonBuffer = fs.readFileSync( + Paths.join(basicOutput, "tileset.json") + ); + const tileset = JSON.parse(tilesetJsonBuffer.toString()); + const actualContentUris = await SpecHelpers.collectExplicitContentUris( + tileset.root + ); + actualContentUris.sort(); + + const expectedContentUris = [ + "PROCESSED_sub/tileB.b3dm", + "PROCESSED_tileA.b3dm", + "PROCESSED_tileA.b3dm", + "PROCESSED_tileB.b3dm", + "PROCESSED_tileC.b3dm", + ]; + expect(actualContentUris).toEqual(expectedContentUris); + }); + + it("processAllEntries only processes unprocessed entries", async function () { + // XXX DEBUG + SpecHelpers.forceDeleteDirectory(basicOutput); + + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + + // First, process all content entries + const contentsSpecProcessor = new SpecProcessor(); + await tilesetProcessor.processTileContentEntries( + contentsSpecProcessor.processUri, + contentsSpecProcessor.processEntry + ); + + // Now, process all remaining entries + const specProcessor = new SpecProcessor(); + await tilesetProcessor.processAllEntries(specProcessor.processEntry); + await tilesetProcessor.end(); + + // Expect only the non-content entries to have been processed + // in processAllEntries + const expectedProcessedKeys = ["README.md"]; + const actualProcessedKeys = specProcessor.processedKeys; + actualProcessedKeys.sort(); + expect(actualProcessedKeys).toEqual(expectedProcessedKeys); + }); +}); diff --git a/specs/TilesetCombinerSpec.ts b/specs/TilesetCombinerSpec.ts index 47df7663..82ed7ea9 100644 --- a/specs/TilesetCombinerSpec.ts +++ b/specs/TilesetCombinerSpec.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-non-null-assertion */ import fs from "fs"; import { Paths } from "../src/base/Paths"; diff --git a/specs/TilesetMergerSpec.ts b/specs/TilesetMergerSpec.ts new file mode 100644 index 00000000..931a3658 --- /dev/null +++ b/specs/TilesetMergerSpec.ts @@ -0,0 +1,61 @@ +import fs from "fs"; + +import { Paths } from "../src/base/Paths"; + +import { Tilesets } from "../src/tilesets/Tilesets"; + +import { SpecHelpers } from "./SpecHelpers"; + +describe("TilesetMerger", function () { + it("merges tilesets into a single tileset", async function () { + const tilesetSourceNames = [ + "./specs/data/mergeTilesets/basicMerge/TilesetA/tileset.json", + "./specs/data/mergeTilesets/basicMerge/sub/TilesetA/tileset.json", + ]; + const tilesetTargetName = "./specs/data/output/mergeTilesets/basicMerge"; + const overwrite = true; + + await Tilesets.merge(tilesetSourceNames, tilesetTargetName, overwrite); + + // Ensure that the output directory contains the expected files: + // All files of the input, disambiguated for the same base name + // (i.e. "TilesetA" and "TilesetA-0" - this is not specified, + // but has to be assumed here) + const actualRelativeFiles = + SpecHelpers.collectRelativeFileNames(tilesetTargetName); + actualRelativeFiles.sort(); + const expectedRelativeFiles = [ + "TilesetA-0/ll.b3dm", + "TilesetA-0/lr.b3dm", + "TilesetA-0/parent.b3dm", + "TilesetA-0/tileset.json", + "TilesetA-0/ul.b3dm", + "TilesetA-0/ur.b3dm", + "TilesetA/ll.b3dm", + "TilesetA/lr.b3dm", + "TilesetA/parent.b3dm", + "TilesetA/tileset.json", + "TilesetA/ul.b3dm", + "TilesetA/ur.b3dm", + "tileset.json", + ]; + expect(actualRelativeFiles).toEqual(expectedRelativeFiles); + + // Ensure that the single 'tileset.json' contains the + // proper content URIs for the external tilesets: + const tilesetJsonBuffer = fs.readFileSync( + Paths.join(tilesetTargetName, "tileset.json") + ); + const tileset = JSON.parse(tilesetJsonBuffer.toString()); + const actualContentUris = await SpecHelpers.collectExplicitContentUris( + tileset.root + ); + actualContentUris.sort(); + + const expectedContentUris = [ + "TilesetA-0/tileset.json", + "TilesetA/tileset.json", + ]; + expect(actualContentUris).toEqual(expectedContentUris); + }); +}); diff --git a/specs/TilesetProcessorSpec.ts b/specs/TilesetProcessorSpec.ts new file mode 100644 index 00000000..4118af97 --- /dev/null +++ b/specs/TilesetProcessorSpec.ts @@ -0,0 +1,79 @@ +import { BasicTilesetProcessor } from "../src/tilesetProcessing/BasicTilesetProcessor"; + +import { SpecHelpers } from "./SpecHelpers"; + +const basicInput = "./specs/data/tilesetProcessing/basicProcessing"; +const basicOutput = "./specs/data/output/tilesetProcessing/basicProcessing"; +const overwrite = true; + +describe("TilesetProcessor", function () { + afterEach(function () { + SpecHelpers.forceDeleteDirectory(basicOutput); + }); + + it("throws when trying to call 'begin' with invalid path", async function () { + const tilesetProcessor = new BasicTilesetProcessor(); + await expectAsync( + (async function () { + await tilesetProcessor.begin( + basicInput + "_INVALID", + basicOutput, + overwrite + ); + })() + //^ This () is important to really CALL the anonymous function + // and return a promise. + ).toBeRejectedWithError(); + }); + + it("throws when trying to call 'begin' twice", async function () { + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + await expectAsync( + (async function () { + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + })() + //^ This () is important to really CALL the anonymous function + // and return a promise. + ).toBeRejectedWithError(); + }); + + it("throws when trying to call 'end' without 'begin'", async function () { + const tilesetProcessor = new BasicTilesetProcessor(); + await expectAsync( + (async function () { + await tilesetProcessor.end(); + })() + //^ This () is important to really CALL the anonymous function + // and return a promise. + ).toBeRejectedWithError(); + }); + + it("throws when trying to call 'end' twice", async function () { + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + await tilesetProcessor.end(); + await expectAsync( + (async function () { + await tilesetProcessor.end(); + })() + //^ This () is important to really CALL the anonymous function + // and return a promise. + ).toBeRejectedWithError(); + }); + + it("performs a 'no-op' of just copying the data when when no other functions are called", async function () { + const tilesetProcessor = new BasicTilesetProcessor(); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); + await tilesetProcessor.end(); + + // Ensure that the output directory contains exactly the + // input files + const relativeInputFiles = SpecHelpers.collectRelativeFileNames(basicInput); + relativeInputFiles.sort(); + const relativeOutputFiles = + SpecHelpers.collectRelativeFileNames(basicOutput); + relativeOutputFiles.sort(); + expect(relativeOutputFiles).toEqual(relativeInputFiles); + }); +}); diff --git a/specs/data/mergeTilesets/TilesetA/ll.b3dm b/specs/data/mergeTilesets/basicMerge/TilesetA/ll.b3dm similarity index 100% rename from specs/data/mergeTilesets/TilesetA/ll.b3dm rename to specs/data/mergeTilesets/basicMerge/TilesetA/ll.b3dm diff --git a/specs/data/mergeTilesets/TilesetA/lr.b3dm b/specs/data/mergeTilesets/basicMerge/TilesetA/lr.b3dm similarity index 100% rename from specs/data/mergeTilesets/TilesetA/lr.b3dm rename to specs/data/mergeTilesets/basicMerge/TilesetA/lr.b3dm diff --git a/specs/data/mergeTilesets/TilesetA/parent.b3dm b/specs/data/mergeTilesets/basicMerge/TilesetA/parent.b3dm similarity index 100% rename from specs/data/mergeTilesets/TilesetA/parent.b3dm rename to specs/data/mergeTilesets/basicMerge/TilesetA/parent.b3dm diff --git a/specs/data/mergeTilesets/TilesetA/tileset.json b/specs/data/mergeTilesets/basicMerge/TilesetA/tileset.json similarity index 100% rename from specs/data/mergeTilesets/TilesetA/tileset.json rename to specs/data/mergeTilesets/basicMerge/TilesetA/tileset.json diff --git a/specs/data/mergeTilesets/TilesetA/ul.b3dm b/specs/data/mergeTilesets/basicMerge/TilesetA/ul.b3dm similarity index 100% rename from specs/data/mergeTilesets/TilesetA/ul.b3dm rename to specs/data/mergeTilesets/basicMerge/TilesetA/ul.b3dm diff --git a/specs/data/mergeTilesets/TilesetA/ur.b3dm b/specs/data/mergeTilesets/basicMerge/TilesetA/ur.b3dm similarity index 100% rename from specs/data/mergeTilesets/TilesetA/ur.b3dm rename to specs/data/mergeTilesets/basicMerge/TilesetA/ur.b3dm diff --git a/specs/data/mergeTilesets/sub/TilesetA/ll.b3dm b/specs/data/mergeTilesets/basicMerge/sub/TilesetA/ll.b3dm similarity index 100% rename from specs/data/mergeTilesets/sub/TilesetA/ll.b3dm rename to specs/data/mergeTilesets/basicMerge/sub/TilesetA/ll.b3dm diff --git a/specs/data/mergeTilesets/sub/TilesetA/lr.b3dm b/specs/data/mergeTilesets/basicMerge/sub/TilesetA/lr.b3dm similarity index 100% rename from specs/data/mergeTilesets/sub/TilesetA/lr.b3dm rename to specs/data/mergeTilesets/basicMerge/sub/TilesetA/lr.b3dm diff --git a/specs/data/mergeTilesets/sub/TilesetA/parent.b3dm b/specs/data/mergeTilesets/basicMerge/sub/TilesetA/parent.b3dm similarity index 100% rename from specs/data/mergeTilesets/sub/TilesetA/parent.b3dm rename to specs/data/mergeTilesets/basicMerge/sub/TilesetA/parent.b3dm diff --git a/specs/data/mergeTilesets/sub/TilesetA/tileset.json b/specs/data/mergeTilesets/basicMerge/sub/TilesetA/tileset.json similarity index 100% rename from specs/data/mergeTilesets/sub/TilesetA/tileset.json rename to specs/data/mergeTilesets/basicMerge/sub/TilesetA/tileset.json diff --git a/specs/data/mergeTilesets/sub/TilesetA/ul.b3dm b/specs/data/mergeTilesets/basicMerge/sub/TilesetA/ul.b3dm similarity index 100% rename from specs/data/mergeTilesets/sub/TilesetA/ul.b3dm rename to specs/data/mergeTilesets/basicMerge/sub/TilesetA/ul.b3dm diff --git a/specs/data/mergeTilesets/sub/TilesetA/ur.b3dm b/specs/data/mergeTilesets/basicMerge/sub/TilesetA/ur.b3dm similarity index 100% rename from specs/data/mergeTilesets/sub/TilesetA/ur.b3dm rename to specs/data/mergeTilesets/basicMerge/sub/TilesetA/ur.b3dm diff --git a/specs/data/tilesetProcessing/basicProcessing/README.md b/specs/data/tilesetProcessing/basicProcessing/README.md new file mode 100644 index 00000000..305feaca --- /dev/null +++ b/specs/data/tilesetProcessing/basicProcessing/README.md @@ -0,0 +1,19 @@ + +A basic tileset consisting of dummy data, with a structure +suitable for testing various aspects of the tileset +processing. + +A tile with content and multiple children, where one child +contains multiple contents (with the same file name, but +located in different directories), one content that appears +twice in different tiles (even though that may not make +sense - it is intended for tests), and a README.md that is +unrelated to the tileset itself. + +root.content.uri = tileA.b3dm +root.children[0].contents[0].uri = tileB.b3dm +root.children[0].contents[1].uri = sub/tileB.b3dm +root.children[1].content.uri = tileC.b3dm +root.children[2].content.uri = tileA.b3dm + + diff --git a/specs/data/tilesetProcessing/basicProcessing/sub/tileB.b3dm b/specs/data/tilesetProcessing/basicProcessing/sub/tileB.b3dm new file mode 100644 index 00000000..e69de29b diff --git a/specs/data/tilesetProcessing/basicProcessing/tileA.b3dm b/specs/data/tilesetProcessing/basicProcessing/tileA.b3dm new file mode 100644 index 00000000..e69de29b diff --git a/specs/data/tilesetProcessing/basicProcessing/tileB.b3dm b/specs/data/tilesetProcessing/basicProcessing/tileB.b3dm new file mode 100644 index 00000000..e69de29b diff --git a/specs/data/tilesetProcessing/basicProcessing/tileC.b3dm b/specs/data/tilesetProcessing/basicProcessing/tileC.b3dm new file mode 100644 index 00000000..e69de29b diff --git a/specs/data/tilesetProcessing/basicProcessing/tileset.json b/specs/data/tilesetProcessing/basicProcessing/tileset.json new file mode 100644 index 00000000..b8bf3b4a --- /dev/null +++ b/specs/data/tilesetProcessing/basicProcessing/tileset.json @@ -0,0 +1,47 @@ +{ + "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 ] + }, + "content": { + "uri": "tileA.b3dm" + }, + "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, + "contents": [ + { + "uri": "tileB.b3dm" + }, + { + "uri": "sub/tileB.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": "tileC.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": "tileA.b3dm" + } + } + ] + } +} \ No newline at end of file diff --git a/src/tilesetProcessing/BasicTilesetProcessor.ts b/src/tilesetProcessing/BasicTilesetProcessor.ts index cbdb9623..b6c780dc 100644 --- a/src/tilesetProcessing/BasicTilesetProcessor.ts +++ b/src/tilesetProcessing/BasicTilesetProcessor.ts @@ -5,9 +5,6 @@ import { Schema } from "../structure/Metadata/Schema"; import { TilesetSourceResourceResolver } from "../io/TilesetSourceResourceResolver"; -import { TilesetEntry } from "../tilesetData/TilesetEntry"; -import { TilesetSources } from "../tilesetData/TilesetSources"; - import { Tiles } from "../tilesets/Tiles"; import { TilesetProcessor } from "./TilesetProcessor"; @@ -139,10 +136,10 @@ export class BasicTilesetProcessor extends TilesetProcessor { } /** - * Applies the given callback to each `TilesetEntry` that has not - * yet been processed. + * Applies the given entry processor to each `TilesetEntry` that + * has not yet been processed * - * @param callback - The callback + * @param entryProcessor - The callback * @returns A promise that resolves when the process is finished * @throws DeveloperError If `begin` was not called yet * @throws TilesetError When the input could not be processed @@ -150,9 +147,12 @@ export class BasicTilesetProcessor extends TilesetProcessor { async processAllEntries(entryProcessor: TilesetEntryProcessor) { const context = this.getContext(); const tilesetSource = context.tilesetSource; - const entries = TilesetSources.getEntries(tilesetSource); - for (const entry of entries) { - await this.processEntry(entry, entryProcessor); + const tilesetSourceJsonFileName = context.tilesetSourceJsonFileName; + const sourceKeys = tilesetSource.getKeys(); + for (const sourceKey of sourceKeys) { + if (sourceKey !== tilesetSourceJsonFileName) { + await this.processEntry(sourceKey, entryProcessor); + } } } @@ -206,10 +206,6 @@ export class BasicTilesetProcessor extends TilesetProcessor { /** * Process all entries that correspond to content of the given tile. * - * This determines the entries in the tileset source that represent - * content of the given tile, calls `processEntry` for each of them, - * and stores the resulting entries. - * * The `tile.content.uri` or `tile.contents[i].uri` of the given tile * will be updated to reflect possible changes of the keys (file * names) that are performed by the `entryProcessor`. @@ -222,20 +218,19 @@ export class BasicTilesetProcessor extends TilesetProcessor { tile: Tile, entryProcessor: TilesetEntryProcessor ): Promise { - const sourceEntries = await this.fetchTileContentEntries(tile); - const targetEntries = await this.processEntries( - sourceEntries, + const contents = BasicTilesetProcessor.getTileContents(tile); + const targetContentUris = await this.processContentEntries( + contents, entryProcessor ); - BasicTilesetProcessor.updateTileContent(tile, targetEntries); - this.storeTargetEntries(...targetEntries); + BasicTilesetProcessor.updateTileContent(tile, targetContentUris); } /** * Process all entries that correspond to content of the given traversed tile. * * This determines the entries in the tileset source that represent - * content of the given tile, calls `processEntry` for each of them, + * content of the given tile, calls `processEntries` on them, * and stores the resulting entries. * * @param traversedTile - The traversed tile @@ -246,65 +241,40 @@ export class BasicTilesetProcessor extends TilesetProcessor { traversedTile: TraversedTile, entryProcessor: TilesetEntryProcessor ): Promise { - const sourceEntries = await this.fetchTraversedTileContentEntries( - traversedTile - ); - const targetEntries = await this.processEntries( - sourceEntries, - entryProcessor - ); - this.storeTargetEntries(...targetEntries); - } - - /** - * Fetch all entries from the tileset source that correspond to the - * contents of the given tile. - * - * @param tile - The tile - * @returns A promise with the entries - */ - private async fetchTileContentEntries(tile: Tile): Promise { - const contents = BasicTilesetProcessor.getTileContents(tile); - const entries = await this.fetchContentEntries(contents); - return entries; - } - - /** - * Fetch all entries from the tileset source that correspond to the - * contents of the given traversed tile. - * - * @param traversedTile - The traversed tile - * @returns A promise with the entries - */ - private async fetchTraversedTileContentEntries( - traversedTile: TraversedTile - ): Promise { if (traversedTile.isImplicitTilesetRoot()) { - return []; + return; } const contents = traversedTile.getFinalContents(); - const entries = await this.fetchContentEntries(contents); - return entries; + await this.processContentEntries(contents, entryProcessor); } /** - * Fetch all entries from the tileset source that correspond to the - * given contents. + * Process all entries that correspond to the given contents. * - * @param contents - The contents - * @returns A promise with the entries + * @param tile - The tile + * @param entryProcessor The `TilesetEntryProcessor` + * @returns A promise that resolves when the process is finished, + * containing the new names that the entries have after processing */ - private async fetchContentEntries( - contents: Content[] - ): Promise { - const entries = []; + private async processContentEntries( + contents: Content[], + entryProcessor: TilesetEntryProcessor + ): Promise { + const targetContentUris: string[] = []; for (const content of contents) { - const entry = await this.fetchSourceEntry(content.uri); - if (entry) { - entries.push(entry); + const sourceContentUri = content.uri; + let targetContentUri; + if (this.isProcessed(sourceContentUri)) { + targetContentUri = this.getTargetKey(sourceContentUri); + } else { + await this.processEntry(sourceContentUri, entryProcessor); + targetContentUri = this.getTargetKey(sourceContentUri); + } + if (targetContentUri) { + targetContentUris.push(targetContentUri); } } - return entries; + return targetContentUris; } /** @@ -324,33 +294,33 @@ export class BasicTilesetProcessor extends TilesetProcessor { } /** - * Update the content of the given tile to reflect the given entries. + * Update the content of the given tile to reflect the given URIs. * - * When the given entries are empty, then the `content` and `contents` + * When the given array is empty, then the `content` and `contents` * of the given tile will be deleted. * - * When there is one entry, then the `content` of the given tile will - * receive the `key` (file name) of this entry as the content `uri`. + * When there is one element, then the `content` of the given tile will + * receive this element as the content `uri`. * - * When there are multiple entries, the tile will receive `contents` - * where each content `uri` is one `key` file name of the entries. + * When there are multiple elements, the tile will receive `contents` + * where each content `uri` is one element of the array. * * @param tile - The tile - * @param targetEntries - The target entries + * @param contentUris - The content URIs */ - private static updateTileContent(tile: Tile, targetEntries: TilesetEntry[]) { - if (targetEntries.length === 0) { + private static updateTileContent(tile: Tile, contentUris: string[]) { + if (contentUris.length === 0) { delete tile.content; delete tile.contents; return; } - if (targetEntries.length === 1) { - const targetEntry = targetEntries[0]; + if (contentUris.length === 1) { + const contentUri = contentUris[0]; if (tile.content) { - tile.content.uri = targetEntry.key; + tile.content.uri = contentUri; } else { const content = { - uri: targetEntry.key, + uri: contentUri, }; tile.content = content; delete tile.contents; @@ -358,9 +328,9 @@ export class BasicTilesetProcessor extends TilesetProcessor { } const newContents: Content[] = []; - for (const targetEntry of targetEntries) { + for (const contentUri of contentUris) { const content = { - uri: targetEntry.key, + uri: contentUri, }; newContents.push(content); } diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetProcessor.ts index b818b068..7eb6caea 100644 --- a/src/tilesetProcessing/TilesetProcessor.ts +++ b/src/tilesetProcessing/TilesetProcessor.ts @@ -98,6 +98,9 @@ export abstract class TilesetProcessor { tilesetTargetName: string, overwrite: boolean ): Promise { + if (this.context) { + throw new TilesetError("Processing has already begun"); + } let tilesetSource; let tilesetTarget; try { @@ -138,6 +141,7 @@ export abstract class TilesetProcessor { tilesetTarget: tilesetTarget, tilesetTargetJsonFileName: tilesetTargetJsonFileName, processedKeys: {}, + targetKeys: {}, }; } catch (error) { if (tilesetSource) { @@ -175,21 +179,26 @@ export abstract class TilesetProcessor { // Perform a no-op on all entries that have not yet // been marked as processed - const entries = TilesetSources.getEntries(tilesetSource); - for (const entry of entries) { - const key = entry.key; - // The tileset JSON file will be added explicitly below - if (key !== tilesetSourceJsonFileName) { - const targetEntry = await this.processSourceEntry( - key, + const sourceKeys = tilesetSource.getKeys(); + for (const sourceKey of sourceKeys) { + if (sourceKey !== tilesetSourceJsonFileName) { + await this.processEntry( + sourceKey, // eslint-disable-next-line @typescript-eslint/no-unused-vars async (sourceEntry: TilesetEntry, type: string | undefined) => { return sourceEntry; } ); - if (targetEntry) { - this.storeTargetEntries(targetEntry); - } + } + } + + const entries = TilesetSources.getEntries(tilesetSource); + for (const entry of entries) { + const key = entry.key; + // The tileset JSON file will be added explicitly below + if (!this.isProcessed(key) && key !== tilesetSourceJsonFileName) { + this.markAsProcessed(key); + this.storeTargetEntries(entry); } } @@ -227,31 +236,46 @@ export abstract class TilesetProcessor { * then this method does nothing. * * Otherwise, the specified entry will be looked up in the tileset - * source, and passed to `processEntry`. + * source, and passed to the given entry processor, together with + * its type information. + * + * The resulting target entry (if any) will be stored in the + * tileset target, and both the source and the target will + * be marked as 'processed' * - * @param key - The key (file name) of the entry + * @param sourceKey - The key (file name) of the entry * @param entryProcessor - The `TilesetEntryProcessor` that will * be called to process the actual entry. - * @returns A promise that resolves when the process is finished, - * containing the resulting entries + * @returns A promise that resolves when the process is finished * @throws DeveloperError When the source or target is not opened * @throws TilesetError When the input could not be processed */ - private async processSourceEntry( - key: string, + protected async processEntry( + sourceKey: string, entryProcessor: TilesetEntryProcessor - ): Promise { - const sourceKey = key; + ): Promise { if (this.isProcessed(sourceKey)) { - return undefined; + return; } - const sourceEntry = await this.fetchSourceEntry(key); + const sourceEntry = await this.fetchSourceEntry(sourceKey); if (!sourceEntry) { + this.markAsProcessed(sourceKey); const message = `No ${sourceKey} found in input`; - throw new TilesetError(message); + //throw new TilesetError(message); + console.warn(message); + return; + } + const targetEntry = await this.processEntryInternal( + sourceEntry, + entryProcessor + ); + + this.markAsProcessed(sourceEntry.key); + if (targetEntry) { + this.putTargetKey(sourceEntry.key, targetEntry.key); + this.markAsProcessed(targetEntry.key); + this.storeTargetEntries(targetEntry); } - const targetEntry = await this.processEntry(sourceEntry, entryProcessor); - return targetEntry; } /** @@ -269,7 +293,7 @@ export abstract class TilesetProcessor { * @param entryProcessor The `TilesetEntryProcessor` * @returns The target entry */ - async processEntry( + private async processEntryInternal( sourceEntry: TilesetEntry, entryProcessor: TilesetEntryProcessor ): Promise { @@ -281,34 +305,9 @@ export abstract class TilesetProcessor { this.log(` to target: ${targetEntry?.key}`); - this.markAsProcessed(sourceEntry.key); - if (targetEntry) { - this.markAsProcessed(targetEntry.key); - } return targetEntry; } - /** - * Calls `processEntry` on each input, and returns the results. - - * @param sourceEntries - The source entries - * @param entryProcessor The `TilesetEntryProcessor` - * @returns The target entries - */ - async processEntries( - sourceEntries: TilesetEntry[], - entryProcessor: TilesetEntryProcessor - ): Promise { - const targetEntries = []; - for (const sourceEntry of sourceEntries) { - const targetEntry = await this.processEntry(sourceEntry, entryProcessor); - if (targetEntry) { - targetEntries.push(targetEntry); - } - } - return targetEntries; - } - /** * Fetch the entry for the specified key from the current tileset * source. If there is no entry for the given key, then `undefined` @@ -317,7 +316,9 @@ export abstract class TilesetProcessor { * @param key - The key (file name) * @returns The object containing the entry and its type */ - async fetchSourceEntry(key: string): Promise { + protected async fetchSourceEntry( + key: string + ): Promise { const context = this.getContext(); const tilesetSource = context.tilesetSource; @@ -370,6 +371,35 @@ export abstract class TilesetProcessor { return context.processedKeys[key] === true; } + /** + * Stores the new key (file name) that the the entry with the + * given key received during processing. + * + * @param sourceKey - The key (file name) + * @returns The target key, or `undefined` + */ + protected putTargetKey(sourceKey: string, targetKey: string) { + const context = this.getContext(); + context.targetKeys[sourceKey] = targetKey; + } + + /** + * Returns the new key (file name) that the the entry with the + * given key received during processing. + * + * When this is `undefined`, then this may either mean that + * the entry was removed during processing, or that it has + * not been procesed yet. The latter can be checked with + * `isProcessed`. + * + * @param sourceKey - The key (file name) + * @returns The target key, or `undefined` + */ + protected getTargetKey(sourceKey: string): string | undefined { + const context = this.getContext(); + return context.targetKeys[sourceKey]; + } + /** * Determine the type of the given entry * diff --git a/src/tilesetProcessing/TilesetProcessorContext.ts b/src/tilesetProcessing/TilesetProcessorContext.ts index 97fd4ffe..baee1e53 100644 --- a/src/tilesetProcessing/TilesetProcessorContext.ts +++ b/src/tilesetProcessing/TilesetProcessorContext.ts @@ -52,8 +52,15 @@ export interface TilesetProcessorContext { /** * The set of keys (file names) that have already been processed. + * * This includes the original keys, as well as new keys that * have been assigned to entries while they have been processed. */ processedKeys: { [key: string]: boolean }; + + /** + * A mapping from source keys (file names) to the target names + * that they received during processing. + */ + targetKeys: { [key: string]: string }; } From d09f18c6e02745fdb0a720b53c2ac2ba699ac77c Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 9 Apr 2023 16:30:03 +0200 Subject: [PATCH 45/60] Extended tests, mainly implicit tileset processing --- demos/TilesetProcessorExamples.ts | 51 +-- specs/BasicTilesetProcessorSpec.ts | 80 +++-- specs/ImplicitTilesetProcessorSpec.ts | 132 ++++++++ specs/SpecEntryProcessor.ts | 32 ++ specs/SpecHelpers.ts | 62 ++++ specs/TilesetProcessorSpec.ts | 11 +- .../basicProcessing/sub/tileB.b3dm | Bin 0 -> 1864 bytes .../basicProcessing/tileA.b3dm | Bin 0 -> 1864 bytes .../basicProcessing/tileB.b3dm | Bin 0 -> 1864 bytes .../basicProcessing/tileC.b3dm | Bin 0 -> 1864 bytes .../implicitProcessing/ImplicitProcessing.gif | Bin 0 -> 192569 bytes .../implicitProcessing/README.md | 24 ++ .../implicitProcessing/SandcastleCode.js | 31 ++ .../content/content_0__0_0.glb | Bin 0 -> 1200 bytes .../content/content_1__0_0.glb | Bin 0 -> 1200 bytes .../content/content_1__0_1.glb | Bin 0 -> 1200 bytes .../content/content_1__1_0.glb | Bin 0 -> 1200 bytes .../content/content_1__1_1.glb | Bin 0 -> 1200 bytes .../content/content_2__1_1.glb | Bin 0 -> 1204 bytes .../content/content_2__1_2.glb | Bin 0 -> 1204 bytes .../content/content_2__2_1.glb | Bin 0 -> 1204 bytes .../content/content_2__2_2.glb | Bin 0 -> 1204 bytes .../content/content_3__2_3.glb | Bin 0 -> 1208 bytes .../content/content_3__2_4.glb | Bin 0 -> 1208 bytes .../content/content_3__3_2.glb | Bin 0 -> 1208 bytes .../content/content_3__3_3.glb | Bin 0 -> 1204 bytes .../content/content_3__3_4.glb | Bin 0 -> 1204 bytes .../content/content_3__3_5.glb | Bin 0 -> 1208 bytes .../content/content_3__4_2.glb | Bin 0 -> 1208 bytes .../content/content_3__4_3.glb | Bin 0 -> 1204 bytes .../content/content_3__4_4.glb | Bin 0 -> 1204 bytes .../content/content_3__4_5.glb | Bin 0 -> 1208 bytes .../content/content_3__5_3.glb | Bin 0 -> 1208 bytes .../content/content_3__5_4.glb | Bin 0 -> 1208 bytes .../implicitProcessing/subtreeInfo.md | 296 ++++++++++++++++++ .../implicitProcessing/subtrees/0.0.0.subtree | Bin 0 -> 296 bytes .../implicitProcessing/subtrees/2.1.1.subtree | Bin 0 -> 352 bytes .../implicitProcessing/subtrees/2.1.2.subtree | Bin 0 -> 352 bytes .../implicitProcessing/subtrees/2.2.1.subtree | Bin 0 -> 352 bytes .../implicitProcessing/subtrees/2.2.2.subtree | Bin 0 -> 352 bytes .../implicitProcessing/tileset.json | 24 ++ src/contentTypes/ContentDataTypeRegistry.ts | 24 ++ src/tilesetProcessing/TilesetProcessor.ts | 30 +- 43 files changed, 692 insertions(+), 105 deletions(-) create mode 100644 specs/ImplicitTilesetProcessorSpec.ts create mode 100644 specs/SpecEntryProcessor.ts create mode 100644 specs/data/tilesetProcessing/implicitProcessing/ImplicitProcessing.gif create mode 100644 specs/data/tilesetProcessing/implicitProcessing/README.md create mode 100644 specs/data/tilesetProcessing/implicitProcessing/SandcastleCode.js create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_0__0_0.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_1__0_0.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_1__0_1.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_1__1_0.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_1__1_1.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_2__1_1.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_2__1_2.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_2__2_1.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_2__2_2.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__2_3.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__2_4.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_2.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_3.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_4.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_5.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_2.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_3.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_4.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_5.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__5_3.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/content/content_3__5_4.glb create mode 100644 specs/data/tilesetProcessing/implicitProcessing/subtreeInfo.md create mode 100644 specs/data/tilesetProcessing/implicitProcessing/subtrees/0.0.0.subtree create mode 100644 specs/data/tilesetProcessing/implicitProcessing/subtrees/2.1.1.subtree create mode 100644 specs/data/tilesetProcessing/implicitProcessing/subtrees/2.1.2.subtree create mode 100644 specs/data/tilesetProcessing/implicitProcessing/subtrees/2.2.1.subtree create mode 100644 specs/data/tilesetProcessing/implicitProcessing/subtrees/2.2.2.subtree create mode 100644 specs/data/tilesetProcessing/implicitProcessing/tileset.json diff --git a/demos/TilesetProcessorExamples.ts b/demos/TilesetProcessorExamples.ts index eb14ccc1..8857bc81 100644 --- a/demos/TilesetProcessorExamples.ts +++ b/demos/TilesetProcessorExamples.ts @@ -36,57 +36,40 @@ async function example() { } ); - // Create a callback that receives a `Tile` and - // applies a callback to each content - const contentCallback = BasicTilesetProcessor.callbackForEachContent( - async (content: Content): Promise => { - console.log("In contentCallback for implicit tiling roots", content.uri); - return; - } - ); - - // Apply a callback to each content of an (explicit) `Tile` + // Apply a callback to each `Tile` await tilesetProcessor.forEachExplicitTile( async (tile: Tile): Promise => { - console.log("In forEachExplicitTile "); - if (tile.implicitTiling) { - contentCallback(tile); - } + console.log("In forEachExplicitTile"); } ); - // Apply a callback to each entry that is the content - // of an explicit `Tile` - await tilesetProcessor.forEachExplicitTileContentEntry( - async ( - sourceEntry: TilesetEntry, - type: string | undefined - ): Promise => { - console.log("In forEachExplicitTileContentEntry"); - return [sourceEntry]; + // Apply a callback to each `TraversedTile` + await tilesetProcessor.forEachTile( + async (traversedTile: TraversedTile): Promise => { + console.log("In forEachTile"); } ); - // Apply a callback to each entry that is the content - // of a tile - await tilesetProcessor.forEachTileContentEntry( + // Process all entries + await tilesetProcessor.processAllEntries( async ( sourceEntry: TilesetEntry, type: string | undefined - ): Promise => { - console.log("In forEachTileContentEntry"); - return [sourceEntry]; + ): Promise => { + console.log("In processAllEntries"); + return sourceEntry; } ); - // Apply a callback to each entry - await tilesetProcessor.forEachEntry( + // Process all entries + await tilesetProcessor.processTileContentEntries( + (uri: string) => uri, async ( sourceEntry: TilesetEntry, type: string | undefined - ): Promise => { - console.log("In forEachEntry"); - return [sourceEntry]; + ): Promise => { + console.log("In processTileContentEntries"); + return sourceEntry; } ); diff --git a/specs/BasicTilesetProcessorSpec.ts b/specs/BasicTilesetProcessorSpec.ts index d23a297f..7dd0c728 100644 --- a/specs/BasicTilesetProcessorSpec.ts +++ b/specs/BasicTilesetProcessorSpec.ts @@ -2,6 +2,7 @@ import fs from "fs"; import { SpecHelpers } from "./SpecHelpers"; +import { SpecProcessor } from "./SpecEntryProcessor"; import { Paths } from "../src/base/Paths"; @@ -11,41 +12,20 @@ import { Tiles } from "../src/tilesets/Tiles"; import { Tile } from "../src/structure/Tile"; -import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; +import { TraversedTile } from "../src/traversal/TraversedTile"; const basicInput = "./specs/data/tilesetProcessing/basicProcessing"; const basicOutput = "./specs/data/output/tilesetProcessing/basicProcessing"; +const quiet = true; const overwrite = true; -// Utility class that offers methods for a "dummy" modification -// of the URIs (file names) and content processing, and stores -// all processed entry names -class SpecProcessor { - processedKeys: string[] = []; - - processUri = (uri: string) => { - return "PROCESSED_" + uri; - }; - - processEntry = async ( - sourceEntry: TilesetEntry, - type: string | undefined - ) => { - this.processedKeys.push(sourceEntry.key); - return { - key: this.processUri(sourceEntry.key), - value: sourceEntry.value, - }; - }; -} - -describe("BasicTilesetProcessor", function () { +describe("BasicTilesetProcessor on explicit input", function () { afterEach(function () { - //SpecHelpers.forceDeleteDirectory(basicOutput); + SpecHelpers.forceDeleteDirectory(basicOutput); }); - it("forEachExplicitTile covers all tiles", async function () { - const tilesetProcessor = new BasicTilesetProcessor(); + it("forEachExplicitTile covers all explicit tiles", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); const actualContentUris: string[][] = []; @@ -55,6 +35,9 @@ describe("BasicTilesetProcessor", function () { }); await tilesetProcessor.end(); + // NOTE: The order is actually not specified. + // This should be sorted lexographically for + // the comparison... const expectedContentUris = [ ["tileA.b3dm"], ["tileB.b3dm", "sub/tileB.b3dm"], @@ -64,11 +47,31 @@ describe("BasicTilesetProcessor", function () { expect(actualContentUris).toEqual(expectedContentUris); }); - it("processAllEntries processes all entries exactly once", async function () { - // XXX DEBUG - SpecHelpers.forceDeleteDirectory(basicOutput); + it("forEachTile covers all tiles", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(basicInput, basicOutput, overwrite); - const tilesetProcessor = new BasicTilesetProcessor(); + const actualContentUris: string[][] = []; + await tilesetProcessor.forEachTile(async (traversedTile: TraversedTile) => { + const contentUris = traversedTile.getFinalContents().map((c) => c.uri); + actualContentUris.push(contentUris); + }); + await tilesetProcessor.end(); + + // NOTE: The order is actually not specified. + // This should be sorted lexographically for + // the comparison... + const expectedContentUris = [ + ["tileA.b3dm"], + ["tileB.b3dm", "sub/tileB.b3dm"], + ["tileC.b3dm"], + ["tileA.b3dm"], + ]; + expect(actualContentUris).toEqual(expectedContentUris); + }); + + it("processAllEntries processes all entries exactly once", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); const specProcessor = new SpecProcessor(); await tilesetProcessor.processAllEntries(specProcessor.processEntry); @@ -103,10 +106,7 @@ describe("BasicTilesetProcessor", function () { }); it("processTileContentEntries processes the tile content entries", async function () { - // XXX DEBUG - SpecHelpers.forceDeleteDirectory(basicOutput); - - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); const specProcessor = new SpecProcessor(); await tilesetProcessor.processTileContentEntries( @@ -141,10 +141,7 @@ describe("BasicTilesetProcessor", function () { }); it("processTileContentEntries updates the content URIs", async function () { - // XXX DEBUG - SpecHelpers.forceDeleteDirectory(basicOutput); - - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); const specProcessor = new SpecProcessor(); await tilesetProcessor.processTileContentEntries( @@ -175,10 +172,7 @@ describe("BasicTilesetProcessor", function () { }); it("processAllEntries only processes unprocessed entries", async function () { - // XXX DEBUG - SpecHelpers.forceDeleteDirectory(basicOutput); - - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); // First, process all content entries diff --git a/specs/ImplicitTilesetProcessorSpec.ts b/specs/ImplicitTilesetProcessorSpec.ts new file mode 100644 index 00000000..1c0c50d9 --- /dev/null +++ b/specs/ImplicitTilesetProcessorSpec.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import fs from "fs"; + +import { SpecHelpers } from "./SpecHelpers"; +import { SpecProcessor } from "./SpecEntryProcessor"; + +import { BasicTilesetProcessor } from "../src/tilesetProcessing/BasicTilesetProcessor"; + +import { Tiles } from "../src/tilesets/Tiles"; + +import { Tile } from "../src/structure/Tile"; + +import { TraversedTile } from "../src/traversal/TraversedTile"; + +import { TilesetSources } from "../src/tilesetData/TilesetSources"; + +const implicitInput = "./specs/data/tilesetProcessing/implicitProcessing"; +const implicitOutput = + "./specs/data/output/tilesetProcessing/implicitProcessing"; +const quiet = true; +const overwrite = true; + +describe("BasicTilesetProcessor on implicit input", function () { + afterEach(function () { + SpecHelpers.forceDeleteDirectory(implicitOutput); + }); + + it("forEachExplicitTile covers all explicit tiles", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(implicitInput, implicitOutput, overwrite); + + const actualContentUris: string[][] = []; + await tilesetProcessor.forEachExplicitTile(async (tile: Tile) => { + const contentUris = Tiles.getContentUris(tile); + actualContentUris.push(contentUris); + }); + await tilesetProcessor.end(); + + // NOTE: The order is actually not specified. + // This should be sorted lexographically for + // the comparison... + const expectedContentUris = [["content/content_{level}__{x}_{y}.glb"]]; + expect(actualContentUris).toEqual(expectedContentUris); + }); + + it("forEachTile covers all tiles", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(implicitInput, implicitOutput, overwrite); + + const actualContentUris: string[][] = []; + await tilesetProcessor.forEachTile(async (traversedTile: TraversedTile) => { + const contentUris = traversedTile.getFinalContents().map((c) => c.uri); + actualContentUris.push(contentUris); + }); + await tilesetProcessor.end(); + + // Just check the number of content URIs from visited tiles: + // - 1 for the implicit tiling root (with template URI as content URI) + // - 1 for the root + // - 4 for the tiles at level 1 + // - 4 for the tiles at level 2 + // - 12 for the tiles at level 3 + expect(actualContentUris.length).toEqual(22); + }); + + it("processAllEntries processes all entries exactly once", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(implicitInput, implicitOutput, overwrite); + const specProcessor = new SpecProcessor(); + await tilesetProcessor.processAllEntries(specProcessor.processEntry); + await tilesetProcessor.end(); + + const actualProcessedKeys = specProcessor.processedKeys; + const actualOutputFiles = + SpecHelpers.collectRelativeFileNames(implicitOutput); + + // Just check the number of processed entries: It should be the same + // as the number of output files, minus 1 for the 'tileset.json' + expect(actualProcessedKeys.length).toEqual(actualOutputFiles.length - 1); + }); + + it("processTileContentEntries processes the tile content entries", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(implicitInput, implicitOutput, overwrite); + const specProcessor = new SpecProcessor(); + await tilesetProcessor.processTileContentEntries( + specProcessor.processUri, + specProcessor.processEntry + ); + await tilesetProcessor.end(); + + const actualProcessedKeys = specProcessor.processedKeys; + + // Just check the number of processed entries: It should be the same + // as the number of tiles in the input + // - 1 for the root + // - 4 for the tiles at level 1 + // - 4 for the tiles at level 2 + // - 12 for the tiles at level 3 + expect(actualProcessedKeys.length).toEqual(21); + }); + + it("processTileContentEntries updates the content URIs", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(implicitInput, implicitOutput, overwrite); + const specProcessor = new SpecProcessor(); + await tilesetProcessor.processTileContentEntries( + specProcessor.processUri, + specProcessor.processEntry + ); + await tilesetProcessor.end(); + + // Collect all content URIs from the output tileset + const outputTilesetSource = TilesetSources.createAndOpen(implicitOutput); + const outputTileset = SpecHelpers.parseTileset(outputTilesetSource); + const actualContentUris = await SpecHelpers.collectContentUris( + outputTileset, + outputTilesetSource + ); + outputTilesetSource.close(); + + // Ensure that all content URIs have been updated + for (const contentUri of actualContentUris) { + expect(contentUri.startsWith("PROCESSED")).toBeTrue(); + } + + // Ensure that the template URI was updated + const templateUri = outputTileset.root.content?.uri; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(templateUri!.startsWith("PROCESSED")).toBeTrue(); + }); +}); diff --git a/specs/SpecEntryProcessor.ts b/specs/SpecEntryProcessor.ts new file mode 100644 index 00000000..177c6266 --- /dev/null +++ b/specs/SpecEntryProcessor.ts @@ -0,0 +1,32 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +import { TilesetEntry } from "../src/tilesetData/TilesetEntry"; + +/** + * Utility class for processing tileset entries for the specs. + * + * It offers "dummy" methods for + * - modification of the URIs (file names) + * - content processing (just changing the file name) + * and stores all processed source entry names so that + * the exact set of processed entries may be checked + * in the tests. + */ +export class SpecProcessor { + processedKeys: string[] = []; + + processUri = (uri: string) => { + return "PROCESSED_" + uri; + }; + + processEntry = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + this.processedKeys.push(sourceEntry.key); + return { + key: this.processUri(sourceEntry.key), + value: sourceEntry.value, + }; + }; +} diff --git a/specs/SpecHelpers.ts b/specs/SpecHelpers.ts index 676d85db..36e8203d 100644 --- a/specs/SpecHelpers.ts +++ b/specs/SpecHelpers.ts @@ -2,9 +2,19 @@ import fs from "fs"; import { Iterables } from "../src/base/Iterables"; import { Paths } from "../src/base/Paths"; +import { DeveloperError } from "../src/base/DeveloperError"; + import { Tile } from "../src/structure/Tile"; +import { Tileset } from "../src/structure/Tileset"; + import { Tiles } from "../src/tilesets/Tiles"; +import { TilesetSourceResourceResolver } from "../src/io/TilesetSourceResourceResolver"; + +import { TilesetTraverser } from "../src/traversal/TilesetTraverser"; + +import { TilesetSource } from "../src/tilesetData/TilesetSource"; + /** * Utility methods for the specs */ @@ -70,4 +80,56 @@ export class SpecHelpers { }); return allContentUris; } + + /** + * Collect all content URIs (excluding possible template URIs in + * implicit tiling roots) that appear in the given tileset, in + * unspecified order. + * + * @param tileset - The tileset + * @param tilesetSource - The tileset source + * @returns A promise to all content URIs + */ + static async collectContentUris( + tileset: Tileset, + tilesetSource: TilesetSource + ) { + const resourceResolver = new TilesetSourceResourceResolver( + ".", + tilesetSource + ); + const tilesetTraverser = new TilesetTraverser(".", resourceResolver, { + depthFirst: false, + traverseExternalTilesets: true, + }); + const allContentUris: string[] = []; + await tilesetTraverser.traverse(tileset, async (traversedTile) => { + if (!traversedTile.isImplicitTilesetRoot()) { + const contentUris = traversedTile.getFinalContents().map((c) => c.uri); + allContentUris.push(...contentUris); + } + return true; + }); + return allContentUris; + } + + /** + * Parse the tileset from the 'tileset.json' in the given source + * + * @param tilesetSource - The tileset source + * @returns The tileset + * @throws DeveloperError if the tileset could not be read + */ + static parseTileset(tilesetSource: TilesetSource) { + const tilesetJsonBuffer = tilesetSource.getValue("tileset.json"); + if (!tilesetJsonBuffer) { + throw new DeveloperError("No tileset.json found in input"); + } + try { + const tileset = JSON.parse(tilesetJsonBuffer.toString()) as Tileset; + return tileset; + } catch (e) { + throw new DeveloperError(`${e}`); + } + } } diff --git a/specs/TilesetProcessorSpec.ts b/specs/TilesetProcessorSpec.ts index 4118af97..0ed2861f 100644 --- a/specs/TilesetProcessorSpec.ts +++ b/specs/TilesetProcessorSpec.ts @@ -4,6 +4,7 @@ import { SpecHelpers } from "./SpecHelpers"; const basicInput = "./specs/data/tilesetProcessing/basicProcessing"; const basicOutput = "./specs/data/output/tilesetProcessing/basicProcessing"; +const quiet = true; const overwrite = true; describe("TilesetProcessor", function () { @@ -12,7 +13,7 @@ describe("TilesetProcessor", function () { }); it("throws when trying to call 'begin' with invalid path", async function () { - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await expectAsync( (async function () { await tilesetProcessor.begin( @@ -27,7 +28,7 @@ describe("TilesetProcessor", function () { }); it("throws when trying to call 'begin' twice", async function () { - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); await expectAsync( (async function () { @@ -39,7 +40,7 @@ describe("TilesetProcessor", function () { }); it("throws when trying to call 'end' without 'begin'", async function () { - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await expectAsync( (async function () { await tilesetProcessor.end(); @@ -50,7 +51,7 @@ describe("TilesetProcessor", function () { }); it("throws when trying to call 'end' twice", async function () { - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); await tilesetProcessor.end(); await expectAsync( @@ -63,7 +64,7 @@ describe("TilesetProcessor", function () { }); it("performs a 'no-op' of just copying the data when when no other functions are called", async function () { - const tilesetProcessor = new BasicTilesetProcessor(); + const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(basicInput, basicOutput, overwrite); await tilesetProcessor.end(); diff --git a/specs/data/tilesetProcessing/basicProcessing/sub/tileB.b3dm b/specs/data/tilesetProcessing/basicProcessing/sub/tileB.b3dm index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..98ed93971021021a4150aed90830a8d0bdcea5a1 100644 GIT binary patch literal 1864 zcmcIkOK;jh5O&}1ciLXjo_e4GY?7wFR0JhWA_8fkxkRX1o5gHpY_wj}MhGeYtbd_D zsWZziY{)~&p_az;*qQmpGvf`s-td;=I6Ds=3^^Efk`- zvwa(!R>1$mUB}sLx0(||!;ocJDl+09Pe_pW`%D~h_6uT)Bsh_*)$eCaDm6@pqcmZO z?3|1k@z*NuT9Jflp1`rUUL;bPNxNQa)DB6J4C!yk>Uv!ec>-d&J1w3CsayjDGHVdG zItMx0s(W*Y%4)f&X@Lc{@{G=*y8jLJBbG6Lb*E4c%~by;Bw`5@RHlOXWDloN=!eKN8|G*rz!w$=gH0yF7A)5Qrsdb7BOsKYX#FYh=RnVZP=2BHm%v{Gy#hk{jn?sgdk6Fu2<7)$2WCB|Ult0=%Q~JK zo8ikg6K8Xi)x_9mwru?DoR`^5ob9`8yGS?rt~xV5Hb4Ed(5@vf_0mKmu6gVl(6)SL R`_1JW)^Y8;c3n5Z_y^dNhhzW% literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/basicProcessing/tileA.b3dm b/specs/data/tilesetProcessing/basicProcessing/tileA.b3dm index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..98ed93971021021a4150aed90830a8d0bdcea5a1 100644 GIT binary patch literal 1864 zcmcIkOK;jh5O&}1ciLXjo_e4GY?7wFR0JhWA_8fkxkRX1o5gHpY_wj}MhGeYtbd_D zsWZziY{)~&p_az;*qQmpGvf`s-td;=I6Ds=3^^Efk`- zvwa(!R>1$mUB}sLx0(||!;ocJDl+09Pe_pW`%D~h_6uT)Bsh_*)$eCaDm6@pqcmZO z?3|1k@z*NuT9Jflp1`rUUL;bPNxNQa)DB6J4C!yk>Uv!ec>-d&J1w3CsayjDGHVdG zItMx0s(W*Y%4)f&X@Lc{@{G=*y8jLJBbG6Lb*E4c%~by;Bw`5@RHlOXWDloN=!eKN8|G*rz!w$=gH0yF7A)5Qrsdb7BOsKYX#FYh=RnVZP=2BHm%v{Gy#hk{jn?sgdk6Fu2<7)$2WCB|Ult0=%Q~JK zo8ikg6K8Xi)x_9mwru?DoR`^5ob9`8yGS?rt~xV5Hb4Ed(5@vf_0mKmu6gVl(6)SL R`_1JW)^Y8;c3n5Z_y^dNhhzW% literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/basicProcessing/tileB.b3dm b/specs/data/tilesetProcessing/basicProcessing/tileB.b3dm index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..98ed93971021021a4150aed90830a8d0bdcea5a1 100644 GIT binary patch literal 1864 zcmcIkOK;jh5O&}1ciLXjo_e4GY?7wFR0JhWA_8fkxkRX1o5gHpY_wj}MhGeYtbd_D zsWZziY{)~&p_az;*qQmpGvf`s-td;=I6Ds=3^^Efk`- zvwa(!R>1$mUB}sLx0(||!;ocJDl+09Pe_pW`%D~h_6uT)Bsh_*)$eCaDm6@pqcmZO z?3|1k@z*NuT9Jflp1`rUUL;bPNxNQa)DB6J4C!yk>Uv!ec>-d&J1w3CsayjDGHVdG zItMx0s(W*Y%4)f&X@Lc{@{G=*y8jLJBbG6Lb*E4c%~by;Bw`5@RHlOXWDloN=!eKN8|G*rz!w$=gH0yF7A)5Qrsdb7BOsKYX#FYh=RnVZP=2BHm%v{Gy#hk{jn?sgdk6Fu2<7)$2WCB|Ult0=%Q~JK zo8ikg6K8Xi)x_9mwru?DoR`^5ob9`8yGS?rt~xV5Hb4Ed(5@vf_0mKmu6gVl(6)SL R`_1JW)^Y8;c3n5Z_y^dNhhzW% literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/basicProcessing/tileC.b3dm b/specs/data/tilesetProcessing/basicProcessing/tileC.b3dm index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..98ed93971021021a4150aed90830a8d0bdcea5a1 100644 GIT binary patch literal 1864 zcmcIkOK;jh5O&}1ciLXjo_e4GY?7wFR0JhWA_8fkxkRX1o5gHpY_wj}MhGeYtbd_D zsWZziY{)~&p_az;*qQmpGvf`s-td;=I6Ds=3^^Efk`- zvwa(!R>1$mUB}sLx0(||!;ocJDl+09Pe_pW`%D~h_6uT)Bsh_*)$eCaDm6@pqcmZO z?3|1k@z*NuT9Jflp1`rUUL;bPNxNQa)DB6J4C!yk>Uv!ec>-d&J1w3CsayjDGHVdG zItMx0s(W*Y%4)f&X@Lc{@{G=*y8jLJBbG6Lb*E4c%~by;Bw`5@RHlOXWDloN=!eKN8|G*rz!w$=gH0yF7A)5Qrsdb7BOsKYX#FYh=RnVZP=2BHm%v{Gy#hk{jn?sgdk6Fu2<7)$2WCB|Ult0=%Q~JK zo8ikg6K8Xi)x_9mwru?DoR`^5ob9`8yGS?rt~xV5Hb4Ed(5@vf_0mKmu6gVl(6)SL R`_1JW)^Y8;c3n5Z_y^dNhhzW% literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/ImplicitProcessing.gif b/specs/data/tilesetProcessing/implicitProcessing/ImplicitProcessing.gif new file mode 100644 index 0000000000000000000000000000000000000000..720b6c3aad591a71dad9a69da9f6257f43b8444c GIT binary patch literal 192569 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

ywDYX=;COs6*n-DkF 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

  • bPQ=ir|+I$hlB2|fz?CGo!X@$ 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

    ifzg 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 NcXpfdqv!wG{sr$tO|k$0 literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_1__0_1.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_1__0_1.glb new file mode 100644 index 0000000000000000000000000000000000000000..0ed74c5b375fa8a0de9c46adf1c6570f153f151d GIT binary patch literal 1200 zcmb_cO>dh(5TzeKqlakD6k}|sEtkr*63LNm65Gus@?n7mwgT2#7AKK~)SuL!nA!DW zlccSBC=%M4VP@aXd$XkZ`tq%1S-;*})(3pAR>5KqB#LNNaZ!PPw}-G!5-M&Py$9d5 zA$*iHNRo<5?MA%V@{*Qv{kWyzkNV!ohKScCem=KB>L{!x$x^#1_@H#QK%BgE~+WN-vPyxRHb#<)4D5*4Y zdq`>87$_)Iz=PJaLd|f$zC#6!hE%p?#M5IdP!SKuZ;KbOgQkC1S9e#dZ>;0wn zlVQHaeO;@lB$5h7@YpL8lTnDS^dKxTH8ygnEaz37ckL&;|V}@5wGut+Ma5Vg|*(jJ2odXKy2&OKQ znQ`4^+pStZc0Y9OZW6}L;Soy#R1prP#?{sVt3}0|x@jKpU-50au-@US{u}CfgRk;C X&sSrswYAaw+1j`sZIjyd|7<@2SDa5V literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_1__1_0.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_1__1_0.glb new file mode 100644 index 0000000000000000000000000000000000000000..ed46cf43bbb5d3c8d346fb39add84fa9b7e9136e GIT binary patch literal 1200 zcmb_cQE!_t5N7Ls#vZDBW(grlxA8J%Q7ffNLYriLsq$dJfG5Dnn5&F|*dW++F>#vVM!jM)K7ZrH-M+oXTrs9s#Z{RsL z1do#Xaa>WU-H_)cFK8imk0k|fI&!BrguE_r`oadOqp+RNma`4mkdp^obi7B46=*s8 z#(oYl`UePHxPzEHLEK%=$4>yGfnz&^DGuiZ)%rU2E2#TFKoLwWZX>t>dWtm|C6y-b z2nj8yAd(C4;5tDW&SSyz^Hj>R@`l5N$+Qjz$nNkfIglh9>Lfhifr^T8(gn4;w)*A$P~Ph>nOyNe5RbglTs<$7rTmBA`+$Yc=GXLd}8cHsme8LCm){Ix__yC z-!b3gzN}SL5=jLkS$lvo5GyK4ma%Zd>m)6(SHZ(3A{CwUjEgH0YF_URL3&ZT!ahay z35b3bBZ|C|tW3#@N7&Wi-^^~mTq64suY-(kSwti7q^PN)IHy%gyUhufGszB}OQaMm zsHJj`aN}rbox@5~9)Hj^;kZ62`{Kow$BYFur#SUoD<)P3`SZ^h@u+B(C(dNC{ZqDJ9nfqt^Ct##cF#rGn literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_1__1_1.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_1__1_1.glb new file mode 100644 index 0000000000000000000000000000000000000000..33674c6a5c4eefaf23c7bbbec9eb5b3f02121ac5 GIT binary patch literal 1200 zcmb_cO^@0z5KTXRMh~kwrw{^c+gvJ&b|qFowm|o?;@~7s;wr?*apss>v%3ruj8GdIz#knUV{4n0~9x=%?Y>#dWsz|$|^(L z9x`5X#k5oqz+HwiT%}5Gu5zunRnYBbLg#hVL3X>}*^XsJ*CgQucU)CMB6r|-JUHON zjXqW@6lcoIOy>~ve2)-M^0npsla!=vQ|uG5Ol72UfAHzLf0F5tm0!?NOj9%!|P*9kVX_+*rZU8 zfNFMWMUhulY;!h~33fGv_oJ`hZjk*%)=|NiBH;-HTGgB=-ta2t_HasUgcdvNGOd+} zYEAAQ?!)C|H3{dajSKT|F&j-GKz2ZsiNGgkm~Ae5bPW99uqK!iog)h7C_xX&_PA-g z=C<^G-(G0j?Ieub!y^^}R53QC!8O(qt3ky_vpziGzv9be?!3V-{Woa5!b!dzpYcl* W$Iq5cG2`2KW1GY#*E;RBbNe^Q?ohA* literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_2__1_1.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_2__1_1.glb new file mode 100644 index 0000000000000000000000000000000000000000..4eccfdb306c68cf826ea8a209401baf79745f412 GIT binary patch literal 1204 zcmb_c-*1~R5N1FA5OmL!5R$YT51X>6l~N_4OR~OHc`#tW6Ty)&S*s{g|ET?eI~&|k zWt;XeONP7S&fod_Y_ei~^VYJgpYJWp#(BF67Y875Osk5E3WA3NM0J`{anI-v2s{U( zCrQIJt*F#)%(pvU(o(LUb`*k9-yb;;^SZ>(mkvlBh1Fy{8!y3uEqTO62OG3liI%rH zI3N+YE)UQ^B`(F6FKYdcBOvypCMtxA&dwNmlf95^ix%Ma4L>2fpjU5gXSE zwQ8X}Gg@XchoJ9!3h^XhSJ2;yskAl4zM__hOzAure!Lu@@8(upFkq}b(T&J#S>5Z;Zye!E8Y6JAFJU9p5F5J*u|Me&waIc=L0Y|A9uw=R)V zu&9>GJ-}VKny#ne0=02q5iaNB83f1`DE)D`X+7}yA3^e{6E{j0Oy!dU;qFB literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_2__1_2.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_2__1_2.glb new file mode 100644 index 0000000000000000000000000000000000000000..0d5eeca7a42f9c97c1d7225394d49abbd41e0fb0 GIT binary patch literal 1204 zcmb_cO^@0z5KTXRNHpgZLV(>ihqe^$O00lvf$n9+!3j>{D#XZf=&n$t{Zaja9Xk#L z(N;Y)QsSBMjNkY@J4v>>eQjCR&$pIk<9oLZ<_92AM2mup0{q7Vgk>C4@xbU0@LdPO zXGw!NE~wOQ#J4-1(_F5ecNF|#&l@@r@iNEhD+i>G!g4&Cjuzm+mOSC2{WV%FN6TGT z&N)QyA0TjH12H&4Jlu@?M*z1obllFh`nV^o4)#{hT*7+)2i8@HPVWr$6sxcoQfTT9 zkkFh8BDnw`?h+Kjjs@S^sg%3I?{*U=(=zNJzuoU-Pm-*wlW>cBDhkGtJ@7g%RM@yy z$f$+#Olh9T6#Sm&D#Vk3Z9#u0rqZ@4_7t^5BueN0;KNn_B-BNz%5B#U-k;Zcf2qAT z%s04~wMsxDsbD0lH?RxEj7pMaEL!j~Npn1{;NvkO1s(H@i(3+DULTbhMvz7nQ`n@S zR1Sz{l~xpaCD|?|Grqxd4Z;2B>$e+Zf5Xc#qf54-8}OwlsiJsGilh%!9>jGzA~o0hVtVUOLTev)S6w@PlftU~+V76wDD!T_x3j zv&;7Qu07GV+esKV%_EiqC?Y(R8dqC2R)dO{W?k9#6aFv0Oy<@b9M!)=J+JUpe*L_B Z#PO4H)M#uK|J;fnZIk-a%%T5h`xnHbQ0D*u literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_2__2_1.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_2__2_1.glb new file mode 100644 index 0000000000000000000000000000000000000000..553217a1db7524649f0bb01ea384d699fd9659f8 GIT binary patch literal 1204 zcmb_cO^@0z5KTXR2%2*WA;4~%LtBb=C00PTK=-oZ;3Q7sD#XZf=&le_e^h^9XPjgS z-L~qXmMqUqX8gwQ*-5hX^-IgLe!RA<4$hmEzc>I3LtYe872w?;ASmOQt2@EJf#=u| zJZkR8aly55L%H3_oacJ|xZ~iB`tHbvP?kAAnvXvgC_u|dpWY5E=}izWqj<9g1^GL{|DBg&F%gf>M3@_URYtM zJ3zv7u9%hzJh(|vh)%5Jwv%eTE4*GW5jrh{F7n&^%JwYDdL{{vxaX=664?W{>%b8k zHwx8ip*%C5CprbM?>dBd60j}k@5Cf+n_`!!Whx<^2g7$4gA-%VN-4KpJA8Xy>;9$o zvSz-)eO^lfGR>7>S#^M2pyph&EEC~UmPwl9Z3Pdn5i9saW>Q_V(C~UsV$^~(qSS>= z3QA>zYIbQwkyn=OQZ|-?Trp?RFBz>*f)Q0E!SVrNK2;h1H zta-TE6kHoOmfM}od9IhwI}X91>knLrWtrpjl?&QLVKEv`hI4RX%bsx2-U=;|qvfqC z_a#L4A0TjH3(-G8Jlu?WCjhT~J#g*EJA<_;Nc|Gl|39!EE$(&^;B-Rt>sW`R#mTdzNG!lY~dyb5#h5?1A6*pu&wC zg{)dA&xGfRP9f;}9wD9uYzq22F-hCH*e7b4N=WBk|HD=9EYw9Qe zDi>6}N+XKAvTT>Ksa)f|hVXv)iuW3g#$5Pl@(l zpRzf=Yfd!nW)jBj<`IhkiWo1Y!8O(qt475~v#MIHC;VT09?zUNIMTm^+AHkj+x8Jh Y>exOtY>FA*#%tRo;^&90A^*?zFQ~UrZU6uP literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__2_3.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__2_3.glb new file mode 100644 index 0000000000000000000000000000000000000000..1ba18599f356599e22b35b12047270bc1aad2034 GIT binary patch literal 1208 zcmb_cO^@0z5OqI(2paX6LP%h@&7l=VyAmrPTcCScad3i3TqQAb9ClYI(*CR-`_nph zk|l_?>Y_wfLf&D&S^Dz>&@g0qE@Teg{Q-hvyJhl@vj^~O z3xX%b{OvYp%DACOcOqk%T0QL;c*DLkv>+5khSL`oC=-R{cr+a?z=D)M;->BzEuNue zuS@F`qW=#NxN!|Jc!9XP8oMt5cGtZeT6R}=`v}&>;+m;*Was}N+ZxmApWq&2C00Xo z!`=ZRmN7|{kl?{hgj#gAQly<&sa@{%dJ$J~5p zShx|X77P`dvMf?Dczws#h(`_En*L5tEpLMT{SZRZX?oaQ=z z$-S(ZZ*b3ZwS-hL$!Ss@U>C?4Q#46p1yr!d0xcs|X>_E&tES4hVcgf>$Sf;0trJ}6kHVG-5nt)c??GrFKKU+w+{}a`UjK;Enq3$byiUIZlr)kU9$M>0~}xfdxBqkBbhrXt5kE zXIok45dD9Ez=a!#;S%K9?8dR36P&FBb?{eY_x~U}3eoDHp`KzD z_CgBH-VqX-Q$Zvb;K6l*T9~omJ2RDXUwFM&1 zcA&z>wMdO%sL-6|iA=%kyN*IUY1q~DcX}#$+hSL-OGKii9t_`)2Pej!m8#rs?eN`s zt^1eSi-!3Y_j#=nkVq;R$?6U41F@izWEl%ryiC#@4=i|il1M?PJmcbugqqhoB}OAi zD@s$?rl3?7h<24u6nQ1tJ|znt;n4>FX7c&VC9)s!GRWwfMKl6WijpddceF@p)0|*C zCfT8JiIjo`rBv<_Zv6FZGxL|IjRi}8wV2GoLw11W5yN|@nQb>)I~smiZ52$8PK|;& zf~ni2`fqpH9p82*x^_1S<4yC3r2vW$52eP{R*lu7;&t6trfJ^eoAGJ3G+yDT-VgP? gz+d_G_xJ(FkG1`yW2?B?iMMuBW;`}?%Kx+d6S^K!=l}o! literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_2.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_2.glb new file mode 100644 index 0000000000000000000000000000000000000000..6f543033376ebe2ef09a041d9080a7a446ce1d41 GIT binary patch literal 1208 zcmb_cO^@0z5KTXR2paX6LI|+i=Fp0wU5OQtEzrHJI5>$(T!k1p4&4<(+Mm^9e_Cgp z1VXp1dZ;BQGm{y=@q2dSbb0;KvaIi~E$a==n}t8y0}Df*=Thb1-R~hN)@!cr1pfw} zV?*$$xxZfLTpKr(o2|@vrk9Uf4&I>a4r~Zzk>TeH8?=eSVla6f60cG|s5dO9btDuOLi_*Z22{~$ZGxZOR&J;e^# z4a*ICdx&|)71L6I2RAWl(ON6HX(d{3bFb5hg-(i~jTCpjvK@=lj!D8@?zqZ@L;}HW zJ8;0pjYySXsL+IGu};A2x(*?pG;C`6J3UF>y4WRlnTkp3UjO|?@5I=%Qp#=C_TQb? zx__y?teCHHpVyLvOmihzT5ez)s43ShO+`4DMVw@KV8O$a#Bx58sZ`f2G`!xE7?mKk zC{PO_>Qx$1s>bAZ<`;Qb~6d%Rr82N0C|Xq(%>4a#Hvy8(yR`xR_g)(8K1^8>lMD~|3Tvg fPV&p~jRRI+;&aU_(J3ana!xEmorC zZjbg$h`~QV;KD7$=o#YfYC3!daGkN;O!oxk3@=r*d7cA%{|DGb2-hAsUEovfi0zO{ zlXrlWmQ)bQ1^952q7=PE@S>MVxvTtsKV>qjLkAh|eF!=eTgh z#YpL7=+y?8}W)>__8yl9vYB8CEkL&;|V}{pGGix^6I2wL*+$xw7odyMS1XGtu z^>23BeZT8|=-S;RjJM4rmIA0EJd_$&TMd>$#Y?k2KH~r4^K5Co!MFN%sPPJ?@|*GV Zqh+b7$ETC6W9XZ>W$ZTez2pDc{sp{wQjh=u literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_4.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__3_4.glb new file mode 100644 index 0000000000000000000000000000000000000000..78457fcb3d69b9a0f184323fa2ee0c4fc2bbcf32 GIT binary patch literal 1204 zcmb_c&5oNe5T-v563v+s0!g;z&?=2~rL2;$P13!raxmb4D}s?R>8>KAK2#r|kJd4m zgml}ghaw@*jA#6fzt47@Zf;&0hVlKiVf667UHi);kT9f0!9@Yy!x4gVx1-{o(XZgy zCInBC`nz30W$lK1pYx1na`Ti^@J0h?WJ1Wx45!B?$T|w^>0~}xfeCx^h>N;gv{;6g zy{*hkh`~QV;KCYWc!IdUp1LOh+Zvg+HO5|S%RYly?v;u((O-d`{{w6*NON!jdyZAu z3@K{zju6v~3L?1x4{l?WqPG)#-%F&-3$NdgnM}&SLWcWa$brOZzfQtU9;hf7NB+RE zY^bpDS|hC&3N)u#EEDhsj;#>S5_Tp1ot#SDw%AeR5)mt<-Ql~jduHresmkrv4&PqZ zI)ACX(#*HG&uf)}L{h;>+H4>X#DYqarYv0XGEOo)ui)W9A_blDl#3e@*1SF_F|;79 zD7vssL8(j-?JAup@=DS?AqyVi$p-&!^6B$6vLEp>Na>nIGy+eGk}8V#v`DCKPOv?b z?4VsDrC>oRm3xFce?8mG{3U8*!qQ(YCUfwR9bj3+@Y>bPwwrC78h%)96-07Zz0vc|2g2CGHI+q$hD@xAysTNYWQh!#D`_p#p zKtj50)kBfMGvgV*@q0E&wz_(07{<@nhVcg9>!m;60|`S~6kHVG-R&VLc??GrFKKU+47v`a`mvK;0=1tz=DvMIZh8PkU9#>@n||)fCU?JkBho%v{;Uo zy{@cth~7Uy;KDUT{|Iq=Id+c#c4uJOoeTA`PgosSM~2v%c zV?%|FYmsWfP@yT!6Pbe7b8Llp(y*=R@AOpiHpPx&mxx43?e^af-4kQaN>y&Vw*T(D z*7-~AMa_JJ`@B|ZNF)`EWQPrG12LnLWEl$=yiC#@4=i|il1M?vJmcbugqqhoB}Oes zBT8M^q@Yw5h-Q^m6nQ1tHYGD2;n4>FX7uIjC9)s!GRWwXMKl6WijpddH?&A;-JD<> zCfTlbiIjo`rBv=7Zv5qBHSy=DjRkXmF&j<6Lw11W5yN|@nQbc81#dwkoTXxr^1jMvR0mI5e3Jd_$&TL-KL6)(-YGEMUy{~4brbK@0`>i?mh h7x*gw@H~Dn4D(~{;MmwI{;3r|+I8xWW)A&7+fTD?Qf~kN literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_2.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_2.glb new file mode 100644 index 0000000000000000000000000000000000000000..707b8c03f034d51a4436cf5de652ba6cd8efde8b GIT binary patch literal 1208 zcmb_cO^@0z5KTXRNHpp(g^;jpn?oy#b|qFowm|o?;@||6xJqK=IP9(v(*CR-`_nph z;w6Z->YHLwb@W{$LIqD zt^?7Nq~T^$QmNgT=UZOTLN1@S6oQMMci}+Hs{*G74oDq^#b`Jg&cT74JmRAM3N2Qk z<*p9SDMardAaLOZqW=PMcQx`~0Nl<1`*1t{rFy$ZxQ_Dm)H$;Ee~?{;==4rdkFf*x zLQ2iv9uitmK_nLtz)gZ$*c-ufJC$-<2HkGLWLiZXq`3Q?>`0PzbrSA!M@7jv5(r+$ zg#$LOMQQ{?g(kE}WC}sga~0xI!?vcs(^JW7iao_H5s8x8?|&TlN3G6ERc^bs|KYUO z`%CR*!@R*guT=sPNd+TWy@72crc{zFWAU6WGW|!^n`}RcJZYN>9X&$i@KpEqq)VSKJu?#9+n$^L!?MM7)d>PNIH~3Zm4>exl eqx@!kwyft*wQaX-3+Nb5c2&sAsw7It6*^e8bz!ug)G6pKR{T;F_U+keFfij zAbeCTh~tte<3=LiiGmes{kUV`U-Z2T2O?1w_<7`jGErDf#G!*ZfqfjPY`!klfe^!+Z|yaZdXtD1ZMofWgYz$*!w@gu7-5_XRxQ(5u2f< zA@2YQE10B8Nbun%K`A=16nQ6AYFGNbUcyycgq zHf}VsdZ9owRwOC~zwfyk@g!ke(%;Fc)oqGBO)iy*R(ddeHyWH6dseD*+qJ{D=e6En zYA-GG4es+=tsqrQa+=i#*oAV=6wNXoEk%{21>RTi@gmWZO++T-HH{3f_gV}qNF$0Z zY*J7v2V}ELD~i0*Y?sox*y7EG;CB4!^A)nc6;+tA72mQg_)1nxQ=GFhWwtrVbFTQ_ zx>PC2!%AuQ0Jp(vx}F9L)W(5Du$+%);3GT0i!H}zXP9jcTRR4Rc-&~10-YKKb0pV~ zN%!BJvc0}(e`wq7B#hhU5laJ<5nf7zYpfcpLB-d)IX>Y3;^TBdUg4_$9eTXLseL;> d5c1=FZFgEWLEB7RbiA?aOhUftm>qw%e*#L-QBVK? literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_4.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_4.glb new file mode 100644 index 0000000000000000000000000000000000000000..3bbe3fd57fb08c5f4f743ca81a97852f57779b2d GIT binary patch literal 1204 zcmb_cO^@0z5KTXR2%2*WA+WT~rJ`t8VzrPh(7miUIKd>YLW~@T?g}CGNA(AG#@LV* zZPi09Ii4BM_}TB-$%@6zYtQq3zV*Bo_S<Soawqse*F-dsH9aoi*$RC9501miu zqmk7M1-j#9rgMn8VL*sy37eAsPEJzS6^BGFQyD4U>wg^d&Qe{JQf{-h|KVkA_?O!E zmU)MJSxZzh&6Qxq@c`RIO}J)7A<~(wv%JLn3K3o;R`HQ6q`G0L;q{S-x8yrfY@tg* zseDjwl|~eKWyLmU6S=~h4e|Z(%hzjUeb=2{{vFg_VJF|VPdHM? W)@s-kGro;G+a%)Whpi$1&-O3Vs8A39 literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_5.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__4_5.glb new file mode 100644 index 0000000000000000000000000000000000000000..f91ae5e5c4c4e6528920eb0db195bd4c766e39bb GIT binary patch literal 1208 zcmb_c-*4J55cbynA<=1%8H7OTmY0p8R-yvZ0_#h~gA+{RDa6QeXe)%YKWl&9?((Aq zu}yoJCC7L6oxk(<*@=?H&0EIU&v%Ty$9_Bar+eUGD6&lH47|rZ1o?U`^n(;n;5jw~ zFGl$5btX*VhHA4_sYuP@Wh=nD?6{XUgep&Qxo?9hW??=Yj0ZEYVZ)zs)9w;2nWE(^ z5B4cU=N}+&;|ikp3h{6~?7jjxtv=S_w65sm9MM|jmrLi!?*Bn{2-5DHz#iugSPah! z_Vy5oRA_FL1`lo{)WTY8wXtGjwwc#%N7BT3&_asa-}#P5NxKNbQ|^S$q(TD0Z8>nj z!V8fq!BC;GNFx)2*Kr*}JZjj~^mlraymhim>~bBE)ZN~ve)p)=St`Xgb9*08bKSq> zURBK3xaYZ~AU8rwo|FgJ26`e4PZAl%-`+n7((3a>W!cZ2(H*U0`#6T}ga*1+(>GOQ(SE9hL->qEn(^jF$A8 zsQ&tt&GAEXp=mdRaK0)Yu?Qdw@lqDJg;iqJsQ74>2g|aa@t^T^G-Ve!(*J|n8|>s) d?K5K*c4{=%HbeVMW4^Y_NXC9tYvljg{slp}Q7-@h literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__5_3.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__5_3.glb new file mode 100644 index 0000000000000000000000000000000000000000..ff5f4ec2c856671e52cdca27d6037650e83deaef GIT binary patch literal 1208 zcmb_cO^@0z5KTXRNHpp(4dHX!9NN;=uEYw+7U*799GqYhS0P4@!|nC zS%PS*9%{+S%-G{Me$RHCF0Nl%mi6PcWxc_EGY_VFU{S>LT&f)W`#prkdd=0H;1A$? z4unsd2kUjtwQ(c4*~*M(dhxX7;P=~|z5|ggG8`T_piLC!m&5UJ1`cf4BQDxqq9ro4 zyyd|;g=qf+1TI`b^qwK^t}eUJ0G@l%cRZK6cf@iBSe)l}&Dc3|=l>vk1nRU;fRC{Q zmcw$x-X3C}amBP$;KNOfTG(qPH+G`+HuqbtSm>k(U8K16jqO;Rw#+2l<&LXdNF)$C zt_KG!+=x^Oh6;^&7V8B3cE=;cqlQgQf2SwOTPJsjU8Z7^y4!m{=pMB?OQpHZ+}^v> z+|FNeFDmA1-1A)0kZG<2OUn&xLp9-=rKyN!vWSxm4=nh2l330!Wh&J*iwv*#Bt|7j zElQPGPeG|1Q1vQ}DDukEZNesUg-08L+u@h5SIGWK7GcWgV#QbBYgKTfc*FCASH&r@ z5nAjjmuam;SZH$ha2w1=i%~E|Z5)^ev&nD_KC%NMTM4{(hS_?vrDNcGhb6&e=#(fp zM+v%3RDZq8=KpPTplLTJVZ16Hu?QfK@K73DW0hDnDqfo9!M5#3d^0|erq(O`)B8c~ j1$OewW`DG-pC3y5Q^TgXMI9T(Yug0y^Sv4)|IhX>y82RY literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/content/content_3__5_4.glb b/specs/data/tilesetProcessing/implicitProcessing/content/content_3__5_4.glb new file mode 100644 index 0000000000000000000000000000000000000000..0f592c0cbd01b8058ff87b6fe327379310a3db3e GIT binary patch literal 1208 zcmb_cU2mH(6lEVj1YPZsC4_u*<7LaDR!WtGHp%)@<-rgKJOM_=WUZn|`&s*Wy*8#H z>o)CSmW=PczSqY-$0knKSFbJ0`uWzf-eJF91&afaD57P_MG5}>0m5pxqvDRy@8G)* zgpZO2yIo18b|an@Jg2!_KNb}HVb2>n5b-L<>5&6cM`1M?&&Eq|AR`aBXn%ti%h7T- zN9P=(_YV-buz?snL)=|X`p*Dv=VIu%oe^%v?YJj26RD%WB76S_*;SBE?+o@7JK|NW|Mle)pM)O!E;P*UNA)YjBYx+AqmAp-{r`RPTR#Nu|A4mNYV=qcoZo78y;k?%S zOYMbWzQKK2s}v-X3P#fU07WR~RFX7h(UMnjlH-L1A8!&V>4c|TT#-ogdauMVf;6I- z!X^c!azHezw4%r>NsENc`4+D>1UKW`ub0UFmRDg)S8PkS;7d_aMR7*UgqqC>mNCip z#wAh;7FJTZ2e=7V)Acl1pf(OHg5`WX10UG|mTwupJI!o!*xJ$XgX2cQ&!Nu{u!6u*}qQ zr~ z0ri>cC?)6T6_+G}IEJ-gUP)?R393Gz!w6^uT9=%WnUfL>^kq>hhP6qVCB;DT#9XjT Q(akcctyNI)5P}2<01V4tHUIzs literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.1.1.subtree b/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.1.1.subtree new file mode 100644 index 0000000000000000000000000000000000000000..f42a55fc515499477066986cf947742dd81207fc GIT binary patch literal 352 zcmZXQ%L>9U5Jlr3_ydAuoka^GB|C8|xDecki?mKO5L0NTN|F4#lRnYTA`k98bD8mV zjtjdF| z@u&^ftRp8YrJ%8}dnsd8ZmhwEOTrC$tUx{64yscv?919w;3f!Oln%k+FBqqo$W-t- hNrA<~*xQMAQo$_vAD~ew$)k$7NeJs$tA2mO{RNo>Zg2nq literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.1.2.subtree b/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.1.2.subtree new file mode 100644 index 0000000000000000000000000000000000000000..3e714c26c9795096800991747bd7640324a59a0a GIT binary patch literal 352 zcmZXQJqyAx5QgJFaC4Q+8Cno2*~C?F5M0C|trrc%6q>72B>&w@KhfTh58n5=mpfnQ zxL}NB@t|6E7yIzM)*33$<%Ql2!U5bJA6%x}a236JHt_Bpt((h)Br+eh_JHGeWP=zQ zkJ@0(I&!j73K|Q$moir6#u{w6B;26K3e>ahpgPsUzN{SuZi3)N=@1i0L?Uzl`mWB>pF literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.2.1.subtree b/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.2.1.subtree new file mode 100644 index 0000000000000000000000000000000000000000..cebc9edb398d00e0f04b3283986b8589f3771eca GIT binary patch literal 352 zcmZXQJqyAx5QgJFaCec+8Cno2*~C?F5M0C|trrc%6q>72B>&w@KhfTh58n5=mpfnQ zxL}NB@t|6E7yIzM)*33$<%Ql2!U5bJA6%x}a236JHt_Bpt((h)Br+eh_JHGeWP=zQ zkJ@0(I&!j73K|Q$moir6#u{w6B;26K3e>ahpgPsUzN{SuZi3)N=@1i0L?UzpTxYybcN literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.2.2.subtree b/specs/data/tilesetProcessing/implicitProcessing/subtrees/2.2.2.subtree new file mode 100644 index 0000000000000000000000000000000000000000..33f029ef176653b38ba6325f33e6d014fa46e830 GIT binary patch literal 352 zcmZXQ%L>9U5Jlq`=qE_lS+pQhvJahpgPsUzN{SuZi3)N=@1 { + const contentData = new BufferedContentData(uri, data); + const contentDataType = await this.findContentDataType(contentData); + return contentDataType; + } + /** * Tries to find the string that describes the given content data * type. If the type of the content data cannot be determined, diff --git a/src/tilesetProcessing/TilesetProcessor.ts b/src/tilesetProcessing/TilesetProcessor.ts index 7eb6caea..99dd2e07 100644 --- a/src/tilesetProcessing/TilesetProcessor.ts +++ b/src/tilesetProcessing/TilesetProcessor.ts @@ -1,7 +1,6 @@ import { Buffers } from "../base/Buffers"; import { DeveloperError } from "../base/DeveloperError"; -import { BufferedContentData } from "../contentTypes/BufferedContentData"; import { ContentDataTypeRegistry } from "../contentTypes/ContentDataTypeRegistry"; import { Tileset } from "../structure/Tileset"; @@ -281,9 +280,8 @@ export abstract class TilesetProcessor { /** * Process the given source entry, and return the processed result. * - * This will determine the content type of the given entry, pass - * it together with its type information to the `entryProcessor`, - * and mark the entry (and the possible target entry) as "processed". + * This will determine the content type of the given entry, and pass + * it together with its type information to the `entryProcessor`. * * This will *not* store the returned target entry in the tileset * target. To do so, `storeTargetEntries` has to be called with @@ -297,7 +295,10 @@ export abstract class TilesetProcessor { sourceEntry: TilesetEntry, entryProcessor: TilesetEntryProcessor ): Promise { - const type = await this.determineContentDataType(sourceEntry); + const type = await ContentDataTypeRegistry.findType( + sourceEntry.key, + sourceEntry.value + ); this.log(`Processing source: ${sourceEntry.key} with type ${type}`); @@ -316,7 +317,7 @@ export abstract class TilesetProcessor { * @param key - The key (file name) * @returns The object containing the entry and its type */ - protected async fetchSourceEntry( + private async fetchSourceEntry( key: string ): Promise { const context = this.getContext(); @@ -400,23 +401,6 @@ export abstract class TilesetProcessor { return context.targetKeys[sourceKey]; } - /** - * Determine the type of the given entry - * - * The string will either be one of the `ContentDataTypes` strings, - * or `undefined` if the type cannot be determined. - * - * @param entry - The entry - * @returns A promise with the content data type string - */ - private async determineContentDataType( - entry: TilesetEntry - ): Promise { - const contentData = new BufferedContentData(entry.key, entry.value); - const type = await ContentDataTypeRegistry.findContentDataType(contentData); - return type; - } - /** * Parses the JSON from the value with the given key (file name), * and returns the parsed result, AND information of whether the From 953e02d457eb2496f012e9ba030bb7e75a0d21bf Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 9 Apr 2023 16:37:57 +0200 Subject: [PATCH 46/60] Removed duplicate call in demo --- demos/TilesetProcessorExamples.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/demos/TilesetProcessorExamples.ts b/demos/TilesetProcessorExamples.ts index 8857bc81..cac54698 100644 --- a/demos/TilesetProcessorExamples.ts +++ b/demos/TilesetProcessorExamples.ts @@ -36,13 +36,6 @@ async function example() { } ); - // Apply a callback to each `Tile` - await tilesetProcessor.forEachExplicitTile( - async (tile: Tile): Promise => { - console.log("In forEachExplicitTile"); - } - ); - // Apply a callback to each `TraversedTile` await tilesetProcessor.forEachTile( async (traversedTile: TraversedTile): Promise => { From b6dfb5c77adb088a0dfb2de9a4e0fda9883af3c6 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Sun, 9 Apr 2023 16:39:48 +0200 Subject: [PATCH 47/60] Removed another duplicate method in demo --- demos/TilesetProcessorExamples.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/demos/TilesetProcessorExamples.ts b/demos/TilesetProcessorExamples.ts index cac54698..6c4bc678 100644 --- a/demos/TilesetProcessorExamples.ts +++ b/demos/TilesetProcessorExamples.ts @@ -20,14 +20,6 @@ async function example() { const tilesetProcessor = new BasicTilesetProcessor(); await tilesetProcessor.begin(tilesetSourceName, tilesetTargetName, overwrite); - // Apply a callback to each `TraversedTile` - await tilesetProcessor.forEachTile( - async (traversedTile: TraversedTile): Promise => { - console.log("In forEachTile"); - return; - } - ); - // Apply a callback to each (explicit) `Tile` await tilesetProcessor.forEachExplicitTile( async (tile: Tile): Promise => { From 1ecc983153c6fb4946354914fdc97545fd5f4340 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 10 Apr 2023 18:46:22 +0200 Subject: [PATCH 48/60] Cleanups for pipeline experiments demo --- demos/PipelineExperiments.ts | 77 ++++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/demos/PipelineExperiments.ts b/demos/PipelineExperiments.ts index 12e50406..deedda09 100644 --- a/demos/PipelineExperiments.ts +++ b/demos/PipelineExperiments.ts @@ -1,16 +1,6 @@ import { PipelineExecutor } from "../src/pipelines/PipelineExecutor"; import { Pipelines } from "../src/pipelines/Pipelines"; -// Notes: (See ContentStageExecutor) -// - The differentiation between explicit and implicit content -// operations (and which content operations require an update -// of the template URI) still has to be finalized. -// - The (public!) TilesetProcessor.storeTargetEntry method -// should be reviewed. Maybe returning multiple entries -// for content operations should NOT automatically update -// the contents of the input tile, but there should be -// a convenience method for doing this. - function createPipelineExperimentsJson() { const optimizeGlbOptions = { dracoOptions: { @@ -18,37 +8,58 @@ function createPipelineExperimentsJson() { }, }; - const pipelineJson = { - input: "./specs/data/TilesetWithUris/tileset.json", - output: "./output/result.3tz", - tilesetStages: [ + const b3dmToGlbJson = { + name: "B3DM to GLB", + description: "Convert B3DM to GLB", + contentStages: [ { - name: "Tileset stage for b3dmToGlb", - contentStages: ["b3dmToGlb"], - }, - { - name: "Tileset stage for optimizeGlb", - contentStages: [ - { - name: "optimizeGlb", - options: optimizeGlbOptions, - }, - ], + name: "b3dmToGlb", + description: "Convert each B3DM content into GLB", }, + ], + }; + + const optimizeGlbJson = { + name: "Optimize GLB", + description: "Optimize GLB", + contentStages: [ { - name: "Tileset stage for separateGltf", - contentStages: [ - { - name: "separateGltf", - }, - ], + name: "optimizeGlb", + description: + "Apply gltf-pipeline to each GLB content, with the given options", + options: optimizeGlbOptions, }, + ], + }; + + const separateGltfJson = { + name: "Separate glTF", + description: "Separate glTF", + contentStages: [ { - // This is not necessary, but done for the experiment - name: "Tileset stage for 3TZ", + name: "separateGltf", + description: + "Convert each GLB content into a .gltf file with separate resources", }, ], }; + + const dummyJson = { + name: "Dummy", + description: + "Dummy (to have the final output in a directory before writing it into the package)", + }; + + const pipelineJson = { + input: "./specs/data/TilesetWithUris/tileset.json", + output: "./output/result.3tz", + tilesetStages: [ + b3dmToGlbJson, + optimizeGlbJson, + separateGltfJson, + dummyJson, + ], + }; return pipelineJson; } From dd556205c56172d160fca1f8e73bf1002a488954 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 10 Apr 2023 18:46:38 +0200 Subject: [PATCH 49/60] Minor clarification in demo comment --- demos/TilesetProcessorExamples.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demos/TilesetProcessorExamples.ts b/demos/TilesetProcessorExamples.ts index 6c4bc678..c11412a9 100644 --- a/demos/TilesetProcessorExamples.ts +++ b/demos/TilesetProcessorExamples.ts @@ -46,7 +46,7 @@ async function example() { } ); - // Process all entries + // Process all entries that are tile content await tilesetProcessor.processTileContentEntries( (uri: string) => uri, async ( From 89d90281d37917f6c42cefeb428342a89f686490 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 10 Apr 2023 18:47:11 +0200 Subject: [PATCH 50/60] Basic specs for packages as inputs and outputs --- specs/BasicTilesetProcessorSpec.ts | 5 + specs/ImplicitTilesetProcessorSpec.ts | 6 + specs/PackageTilesetProcessorSpec.ts | 145 ++++++++++++++++++ specs/SpecHelpers.ts | 91 +++++++++++ specs/TilesetProcessorSpec.ts | 5 + .../tilesetProcessing/basicProcessing.3dtiles | Bin 0 -> 24576 bytes .../tilesetProcessing/basicProcessing.3tz | Bin 0 -> 10568 bytes .../implicitProcessing.3dtiles | Bin 0 -> 245760 bytes .../tilesetProcessing/implicitProcessing.3tz | Bin 0 -> 230409 bytes 9 files changed, 252 insertions(+) create mode 100644 specs/PackageTilesetProcessorSpec.ts create mode 100644 specs/data/tilesetProcessing/basicProcessing.3dtiles create mode 100644 specs/data/tilesetProcessing/basicProcessing.3tz create mode 100644 specs/data/tilesetProcessing/implicitProcessing.3dtiles create mode 100644 specs/data/tilesetProcessing/implicitProcessing.3tz diff --git a/specs/BasicTilesetProcessorSpec.ts b/specs/BasicTilesetProcessorSpec.ts index 7dd0c728..0ad257e1 100644 --- a/specs/BasicTilesetProcessorSpec.ts +++ b/specs/BasicTilesetProcessorSpec.ts @@ -19,6 +19,11 @@ const basicOutput = "./specs/data/output/tilesetProcessing/basicProcessing"; const quiet = true; const overwrite = true; +/** + * Tests that verify that the `forEach...` and `process...` methods + * of the BasicTilesetProcessor visit and process the correct + * elements on explicit tilesets + */ describe("BasicTilesetProcessor on explicit input", function () { afterEach(function () { SpecHelpers.forceDeleteDirectory(basicOutput); diff --git a/specs/ImplicitTilesetProcessorSpec.ts b/specs/ImplicitTilesetProcessorSpec.ts index 1c0c50d9..b8f82238 100644 --- a/specs/ImplicitTilesetProcessorSpec.ts +++ b/specs/ImplicitTilesetProcessorSpec.ts @@ -20,6 +20,11 @@ const implicitOutput = const quiet = true; const overwrite = true; +/** + * Tests that verify that the `forEach...` and `process...` methods + * of the BasicTilesetProcessor visit and process the correct + * elements on implicit tilesets + */ describe("BasicTilesetProcessor on implicit input", function () { afterEach(function () { SpecHelpers.forceDeleteDirectory(implicitOutput); @@ -29,6 +34,7 @@ describe("BasicTilesetProcessor on implicit input", function () { const tilesetProcessor = new BasicTilesetProcessor(quiet); await tilesetProcessor.begin(implicitInput, implicitOutput, overwrite); + // There is only one explicit tile in the 'implicitProcessing' data const actualContentUris: string[][] = []; await tilesetProcessor.forEachExplicitTile(async (tile: Tile) => { const contentUris = Tiles.getContentUris(tile); diff --git a/specs/PackageTilesetProcessorSpec.ts b/specs/PackageTilesetProcessorSpec.ts new file mode 100644 index 00000000..c684ef80 --- /dev/null +++ b/specs/PackageTilesetProcessorSpec.ts @@ -0,0 +1,145 @@ +import { SpecHelpers } from "./SpecHelpers"; + +import { BasicTilesetProcessor } from "../src/tilesetProcessing/BasicTilesetProcessor"; + +const basicInput = "./specs/data/tilesetProcessing/basicProcessing"; +const basicInput3tz = "./specs/data/tilesetProcessing/basicProcessing.3tz"; +const basicInput3dtiles = + "./specs/data/tilesetProcessing/basicProcessing.3dtiles"; + +const implicitInput = "./specs/data/tilesetProcessing/implicitProcessing"; +const implicitInput3tz = + "./specs/data/tilesetProcessing/implicitProcessing.3tz"; +const implicitInput3dtiles = + "./specs/data/tilesetProcessing/implicitProcessing.3dtiles"; + +const basicOutput = "./specs/data/output/tilesetProcessing/basicProcessing"; +const basicOutput3tz = + "./specs/data/output/tilesetProcessing/basicProcessing.3tz"; +const basicOutput3dtiles = + "./specs/data/output/tilesetProcessing/basicProcessing.3dtiles"; + +const implicitOutput = + "./specs/data/output/tilesetProcessing/implicitProcessing"; +const implicitOutput3tz = + "./specs/data/output/tilesetProcessing/implicitProcessing.3tz"; +const implicitOutput3dtiles = + "./specs/data/output/tilesetProcessing/implicitProcessing.3dtiles"; + +const quiet = true; +const overwrite = true; + +/** + * Tests that verify that the BasicTilesetProcessor operates properly + * when using packages as input or output + */ +describe("BasicTilesetProcessor on packages", function () { + afterEach(function () { + SpecHelpers.forceDeleteDirectory(basicOutput); + SpecHelpers.forceDeleteDirectory(implicitOutput); + }); + + it("writes basic from directories to 3TZ", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(basicInput, basicOutput3tz, overwrite); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + basicInput3tz, + basicOutput3tz + ); + expect(difference).toBeUndefined(); + }); + + it("writes basic from 3TZ to directories", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(basicInput3tz, basicOutput, overwrite); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + basicInput, + basicOutput + ); + expect(difference).toBeUndefined(); + }); + + it("writes basic from directories to 3DTILES", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(basicInput, basicOutput3dtiles, overwrite); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + basicInput3dtiles, + basicOutput3dtiles + ); + expect(difference).toBeUndefined(); + }); + + it("writes basic from 3DTILES to directories", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(basicInput3dtiles, basicOutput, overwrite); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + basicInput, + basicOutput + ); + expect(difference).toBeUndefined(); + }); + + it("writes implicit from directories to 3TZ", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(implicitInput, implicitOutput3tz, overwrite); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + implicitInput3tz, + implicitOutput3tz + ); + expect(difference).toBeUndefined(); + }); + + it("writes implicit from 3TZ to directories", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin(implicitInput3tz, implicitOutput, overwrite); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + implicitInput, + implicitOutput + ); + expect(difference).toBeUndefined(); + }); + + it("writes implicit from directories to 3DTILES", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin( + implicitInput, + implicitOutput3dtiles, + overwrite + ); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + implicitInput3dtiles, + implicitOutput3dtiles + ); + expect(difference).toBeUndefined(); + }); + + it("writes implicit from 3DTILES to directories", async function () { + const tilesetProcessor = new BasicTilesetProcessor(quiet); + await tilesetProcessor.begin( + implicitInput3dtiles, + implicitOutput, + overwrite + ); + await tilesetProcessor.end(); + + const difference = SpecHelpers.computePackageDifference( + implicitInput, + implicitOutput + ); + expect(difference).toBeUndefined(); + }); +}); diff --git a/specs/SpecHelpers.ts b/specs/SpecHelpers.ts index 36e8203d..3b7dde70 100644 --- a/specs/SpecHelpers.ts +++ b/specs/SpecHelpers.ts @@ -14,6 +14,7 @@ import { TilesetSourceResourceResolver } from "../src/io/TilesetSourceResourceRe import { TilesetTraverser } from "../src/traversal/TilesetTraverser"; import { TilesetSource } from "../src/tilesetData/TilesetSource"; +import { TilesetSources } from "../src/tilesetData/TilesetSources"; /** * Utility methods for the specs @@ -132,4 +133,94 @@ export class SpecHelpers { throw new DeveloperError(`${e}`); } } + + /** + * Returns whether the specified packages are equal. + * + * This means that they contain the same keys, and the + * keys are mapped to the same values. + * + * @param nameA - The first package name + * @param nameB - The second package name + * @returns A string describing the difference, or `undefined` + * if there is no difference. + */ + static computePackageDifference( + nameA: string, + nameB: string + ): string | undefined { + const tilesetSourceA = TilesetSources.createAndOpen(nameA); + const tilesetSourceB = TilesetSources.createAndOpen(nameB); + const result = SpecHelpers.computePackageDifferenceInternal( + nameA, + tilesetSourceA, + nameB, + tilesetSourceB + ); + tilesetSourceA.close(); + tilesetSourceB.close(); + return result; + } + + /** + * Returns whether the specified packages are equal. + * + * This means that they contain the same keys, and the + * keys are mapped to the same values. + * + * Entries that end in `.json` will be parsed and strigified + * for the comparison (to handle formatting differences), + * whereas other entries will be treated as "binary", and + * their values will be compared byte-wise. + * + * @param nameA - The first package name + * @param tilesetSourceA - The first package + * @param nameB - The second package name + * @param tilesetSourceB - The second package + * @returns A string describing the difference, or `undefined` + * if there is no difference. + */ + static computePackageDifferenceInternal( + nameA: string, + tilesetSourceA: TilesetSource, + nameB: string, + tilesetSourceB: TilesetSource + ): string | undefined { + const keysA = [...tilesetSourceA.getKeys()]; + const keysB = [...tilesetSourceB.getKeys()]; + + if (keysA.length != keysB.length) { + return `There are ${keysA.length} keys in ${nameA} and ${keysB.length} keys in ${nameB}`; + } + for (let i = 0; i < keysA.length; i++) { + if (keysA[i] != keysB[i]) { + return `Key ${i} is ${keysA[i]} in ${nameA} and ${keysA[i]} in ${nameB}`; + } + } + for (let i = 0; i < keysA.length; i++) { + const valueA = tilesetSourceA.getValue(keysA[i]); + const valueB = tilesetSourceB.getValue(keysB[i]); + if (valueA && valueB) { + if (keysA[i].endsWith(".json")) { + const jsonA = JSON.parse(valueA.toString()); + const jsonB = JSON.parse(valueB.toString()); + const stringA = JSON.stringify(jsonA); + const stringB = JSON.stringify(jsonB); + if (stringA !== stringB) { + return `Value ${keysA[i]} has different JSON contents in ${nameA} and in ${nameB}`; + } + } else { + if (valueA?.length != valueB?.length) { + return `Value ${keysA[i]} has ${valueA?.length} bytes in ${nameA} and ${valueB?.length} bytes in ${nameB}`; + } + const n = valueA.length; + for (let j = 0; j < n; j++) { + if (valueA[i] != valueB[i]) { + return `Value ${keysA[i]} has ${valueA[i]} at index ${j} in ${nameA} but ${valueB[i]} in ${nameB}`; + } + } + } + } + } + } } diff --git a/specs/TilesetProcessorSpec.ts b/specs/TilesetProcessorSpec.ts index 0ed2861f..4db24b70 100644 --- a/specs/TilesetProcessorSpec.ts +++ b/specs/TilesetProcessorSpec.ts @@ -7,6 +7,11 @@ const basicOutput = "./specs/data/output/tilesetProcessing/basicProcessing"; const quiet = true; const overwrite = true; +/** + * Tests for the base functionality of the (abstract) TilesetProcessor + * base class, using its only concrete implementation, namely the + * BasicTilesetProcessor + */ describe("TilesetProcessor", function () { afterEach(function () { SpecHelpers.forceDeleteDirectory(basicOutput); diff --git a/specs/data/tilesetProcessing/basicProcessing.3dtiles b/specs/data/tilesetProcessing/basicProcessing.3dtiles new file mode 100644 index 0000000000000000000000000000000000000000..6d2d10904ac528e544a5e1505c96b3910d1d7c6b GIT binary patch literal 24576 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 literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/implicitProcessing.3tz b/specs/data/tilesetProcessing/implicitProcessing.3tz new file mode 100644 index 0000000000000000000000000000000000000000..14b9bcdf5262d44cf473e9a02fb8d47c8118ebac GIT binary patch literal 230409 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�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�OMYBp^HdN;Z@6Cy`4V>jV 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 Date: Mon, 10 Apr 2023 18:47:29 +0200 Subject: [PATCH 51/60] Add option to set temp directory --- src/pipelines/PipelineExecutor.ts | 33 +++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/pipelines/PipelineExecutor.ts b/src/pipelines/PipelineExecutor.ts index 4eb9452f..b2783a45 100644 --- a/src/pipelines/PipelineExecutor.ts +++ b/src/pipelines/PipelineExecutor.ts @@ -9,6 +9,29 @@ import { TilesetStageExecutor } from "./TilesetStageExecutor"; * Methods to execute `Pipeline` objects. */ export class PipelineExecutor { + /** + * The directory to store temporary files in. + * + * If this is `undefined`, then a directory in the + * default system temp directory will be used. + */ + private static tempBaseDirectory: string | undefined; + + /** + * Set the directory to store temporary files in. + * + * If this is `undefined`, then a directory in the + * default system temp directory will be used. + * + * This is primarily intended for testing, demos, and + * debugging. + * + * @param directory - The directory + */ + static setTempBaseDirectory(directory: string | undefined) { + PipelineExecutor.tempBaseDirectory = directory; + } + /** * Executes the given `Pipeline`. * @@ -31,14 +54,8 @@ export class PipelineExecutor { // Create a temporary directory for the intermediate // processing steps (if there are more than one) // TODO: This is not cleaned up at the end... - let tempBasePath = ""; - - // TODO Store locally for experiments... - const EXPERIMENTS = true; - if (EXPERIMENTS) { - tempBasePath = "./output/TEMP"; - console.warn("Using temp path for experiments: " + tempBasePath); - } else { + let tempBasePath = PipelineExecutor.tempBaseDirectory; + if (!tempBasePath) { if (tilesetStages.length > 1) { tempBasePath = fs.mkdtempSync( path.join(os.tmpdir(), "3d-tiles-tools-pipeline-") From 98ca36fd90de038f34068cedabe780a5d9f6324d Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 10 Apr 2023 18:54:39 +0200 Subject: [PATCH 52/60] Cleanups for demos --- demos/PackagesDemo.ts | 2 +- demos/TilesetProcessingDemos.ts | 10 +++++----- demos/TraversalStatsDemo.ts | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/demos/PackagesDemo.ts b/demos/PackagesDemo.ts index 5921c49a..6e4314bb 100644 --- a/demos/PackagesDemo.ts +++ b/demos/PackagesDemo.ts @@ -51,7 +51,7 @@ async function readPackageExample(fileName: string) { async function run() { console.log("Running test"); - const directory = "./data/"; + const directory = "./output/"; if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { recursive: true }); } diff --git a/demos/TilesetProcessingDemos.ts b/demos/TilesetProcessingDemos.ts index 3a0e96bc..63bc5a6b 100644 --- a/demos/TilesetProcessingDemos.ts +++ b/demos/TilesetProcessingDemos.ts @@ -53,16 +53,16 @@ async function tilesetProcessingDemos() { ); await combineTilesets( - "./specs/data/combineTilesets/tileset.json", - "./specs/data/output/combineTilesets" + "./specs/data/combineTilesets/nestedExternal/tileset.json", + "./specs/data/output/nestedExternal/combineTilesets" ); await mergeTilesets( [ - "./specs/data/mergeTilesets/TilesetA/tileset.json", - "./specs/data/mergeTilesets/sub/TilesetA/tileset.json", + "./specs/data/mergeTilesets/basicMerge/TilesetA/tileset.json", + "./specs/data/mergeTilesets/basicMerge/sub/TilesetA/tileset.json", ], - "./specs/data/output/mergeTilesets/merged.3tz" + "./specs/data/output/mergeTilesets/basicMerge.3tz" ); } diff --git a/demos/TraversalStatsDemo.ts b/demos/TraversalStatsDemo.ts index c636bfb2..7979c904 100644 --- a/demos/TraversalStatsDemo.ts +++ b/demos/TraversalStatsDemo.ts @@ -245,7 +245,7 @@ class Summary { async function runDemo() { const tilesetFileName = - "../3d-tiles-samples/1.1/SparseImplicitQuadtree/tileset.json"; + "./specs/data/tilesetProcessing/implicitProcessing/tileset.json"; await tilesetTraversalDemo(tilesetFileName); } From 3c7f8dab4d1ad04c96a61848f67834afa0c16948 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Mon, 10 Apr 2023 21:05:03 +0200 Subject: [PATCH 53/60] More content operations. Update of CLI. --- demos/PackageConversion.ts | 4 +- demos/PipelineExperimentsContentStages.ts | 91 +++++++ .../contentProcessing/README.md | 12 + .../contentProcessing/b3dmContent.b3dm | Bin 0 -> 9680 bytes .../contentProcessing/glbContent.glb | Bin 0 -> 1664 bytes .../contentProcessing/i3dmContent.i3dm | Bin 0 -> 3780 bytes .../contentProcessing/tileset.json | 39 +++ src/ToolsMain.ts | 87 ++---- src/main.ts | 68 +++-- {demos => src/packages}/ZipToPackage.ts | 2 +- src/pipelines/ContentStageExecutor.ts | 253 +++++++++++++++++- src/pipelines/ContentStages.ts | 49 ++++ 12 files changed, 525 insertions(+), 80 deletions(-) create mode 100644 demos/PipelineExperimentsContentStages.ts create mode 100644 specs/data/tilesetProcessing/contentProcessing/README.md create mode 100644 specs/data/tilesetProcessing/contentProcessing/b3dmContent.b3dm create mode 100644 specs/data/tilesetProcessing/contentProcessing/glbContent.glb create mode 100644 specs/data/tilesetProcessing/contentProcessing/i3dmContent.i3dm create mode 100644 specs/data/tilesetProcessing/contentProcessing/tileset.json rename {demos => src/packages}/ZipToPackage.ts (95%) diff --git a/demos/PackageConversion.ts b/demos/PackageConversion.ts index a55ae399..7775484d 100644 --- a/demos/PackageConversion.ts +++ b/demos/PackageConversion.ts @@ -7,7 +7,7 @@ import minimist from "minimist"; import { TilesetTargets } from "../src/tilesetData/TilesetTargets"; import { TilesetSources } from "../src/tilesetData/TilesetSources"; -import { ZipToPackage } from "./ZipToPackage"; +import { ZipToPackage } from "../src/packages/ZipToPackage"; /** * Print the help message showing the command line options @@ -73,7 +73,7 @@ async function tilesetPackageConversion(options: any) { const inputExtension = path.extname(input).toLowerCase(); if (inputExtension === ".zip") { - ZipToPackage.convert(input, output, overwrite); + await ZipToPackage.convert(input, output, overwrite); } else { const tilesetSource = TilesetSources.createAndOpen(input); const tilesetTarget = TilesetTargets.createAndBegin(output, overwrite); diff --git a/demos/PipelineExperimentsContentStages.ts b/demos/PipelineExperimentsContentStages.ts new file mode 100644 index 00000000..4ed6555d --- /dev/null +++ b/demos/PipelineExperimentsContentStages.ts @@ -0,0 +1,91 @@ +import { Pipeline } from "../src/pipelines/Pipeline"; +import { TilesetStages } from "../src/pipelines/TilesetStages"; +import { ContentStages } from "../src/pipelines/ContentStages"; +import { PipelineExecutor } from "../src/pipelines/PipelineExecutor"; +import { TilesetStage } from "../src/pipelines/TilesetStage"; + +function createPipeline(tilesetStage: TilesetStage) { + const nameSuffix = tilesetStage.name.replace(/[^\w\s]/gi, ""); + const input = "./specs/data/tilesetProcessing/contentProcessing"; + const output = + "./specs/data/output/tilesetProcessing/contentProcessing/output-" + + nameSuffix; + const pipeline: Pipeline = { + input: input, + output: output, + tilesetStages: [tilesetStage], + }; + return pipeline; +} + +async function example() { + const overwrite = true; + const optimizeGlbOptions = { + dracoOptions: { + compressionLevel: 10, + }, + }; + + const tilesetStageB3dmToGlb = TilesetStages.create( + "B3DM to GLB", + "Convert B3DM to GLB", + [ContentStages.createB3dmToGlb()] + ); + + const tilesetStageI3dmToGlb = TilesetStages.create( + "I3DM to GLB", + "Convert I3DM to GLB", + [ContentStages.createI3dmToGlb()] + ); + + const tilesetStageGlbToB3dm = TilesetStages.create( + "GLB to B3DM", + "Convert GLB to B3DM", + [ContentStages.createGlbToB3dm()] + ); + + const tilesetStageGlbToI3dm = TilesetStages.create( + "GLB to I3DM", + "Convert GLB to I3DM", + [ContentStages.createGlbToI3dm()] + ); + + const tilesetStageOptimizeB3dm = TilesetStages.create( + "Optimize B3DM", + "Optimize the GLB part of B3DM", + [ContentStages.createOptimizeB3dm(optimizeGlbOptions)] + ); + + const tilesetStageOptimizeI3dm = TilesetStages.create( + "Optimize I3DM", + "Optimize the GLB part of I3DM", + [ContentStages.createOptimizeI3dm(optimizeGlbOptions)] + ); + + await PipelineExecutor.executePipeline( + createPipeline(tilesetStageB3dmToGlb), + overwrite + ); + await PipelineExecutor.executePipeline( + createPipeline(tilesetStageI3dmToGlb), + overwrite + ); + await PipelineExecutor.executePipeline( + createPipeline(tilesetStageGlbToB3dm), + overwrite + ); + await PipelineExecutor.executePipeline( + createPipeline(tilesetStageGlbToI3dm), + overwrite + ); + await PipelineExecutor.executePipeline( + createPipeline(tilesetStageOptimizeB3dm), + overwrite + ); + await PipelineExecutor.executePipeline( + createPipeline(tilesetStageOptimizeI3dm), + overwrite + ); +} + +example(); diff --git a/specs/data/tilesetProcessing/contentProcessing/README.md b/specs/data/tilesetProcessing/contentProcessing/README.md new file mode 100644 index 00000000..a6d603e3 --- /dev/null +++ b/specs/data/tilesetProcessing/contentProcessing/README.md @@ -0,0 +1,12 @@ + +A dummy tileset for the processing tests. + +It contains B3DM, I3DM and GLB content, and is used for basic +tests of the content processing stages, namely + +- `b3dmToGlb` +- `i3dmToGlb` +- `glbToB3dm` +- `glbToI3dm` +- `optimizeB3dm` +- `optimizeI3dm` diff --git a/specs/data/tilesetProcessing/contentProcessing/b3dmContent.b3dm b/specs/data/tilesetProcessing/contentProcessing/b3dmContent.b3dm new file mode 100644 index 0000000000000000000000000000000000000000..8cb958955234e1ecaac3837ecbf0162b8fd47bf8 GIT binary patch literal 9680 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 aG@BF-M}eelL&cP`;k#FhA6#TIO#BN27j!HD literal 0 HcmV?d00001 diff --git a/specs/data/tilesetProcessing/contentProcessing/tileset.json b/specs/data/tilesetProcessing/contentProcessing/tileset.json new file mode 100644 index 00000000..a4be4137 --- /dev/null +++ b/specs/data/tilesetProcessing/contentProcessing/tileset.json @@ -0,0 +1,39 @@ +{ + "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": "b3dmContent.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": "i3dmContent.i3dm" + } + }, { + "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": "glbContent.glb" + } + } + ] + } +} \ No newline at end of file diff --git a/src/ToolsMain.ts b/src/ToolsMain.ts index e63f530e..fb3f3683 100644 --- a/src/ToolsMain.ts +++ b/src/ToolsMain.ts @@ -17,6 +17,10 @@ import { PipelineExecutor } from "./pipelines/PipelineExecutor"; import { Pipelines } from "./pipelines/Pipelines"; import { Buffers } from "./base/Buffers"; import { TileDataLayouts } from "./tileFormats/TileDataLayouts"; +import { Pipeline } from "./pipelines/Pipeline"; +import { ZipToPackage } from "./packages/ZipToPackage"; +import { TilesetSources } from "./tilesetData/TilesetSources"; +import { TilesetTargets } from "./tilesetData/TilesetTargets"; /** * Functions that directly correspond to the command line functionality. @@ -327,66 +331,26 @@ export class ToolsMain { await PipelineExecutor.executePipeline(pipeline, force); } - private static createTilesetToDatabasePipeline( - input: string, - output: string - ) { - const pipelineJson = { - input: input, - output: output, - tilesetStages: [ - { - name: "tilesetToDatabase", - }, - ], - }; - return pipelineJson; - } - - static async tilesetToDatabase( - input: string, - output: string, - force: boolean - ) { + static async convert(input: string, output: string, force: boolean) { ToolsMain.ensureCanWrite(output, force); + const inputExtension = path.extname(input).toLowerCase(); - const pipelineJson = ToolsMain.createTilesetToDatabasePipeline( - input, - output - ); - const pipeline = Pipelines.createPipeline(pipelineJson); - await PipelineExecutor.executePipeline(pipeline, force); - } - - private static createDatabaseToTilesetPipeline( - input: string, - output: string - ) { - const pipelineJson = { - input: input, - output: output, - tilesetStages: [ - { - name: "databaseToTileset", - }, - ], - }; - return pipelineJson; - } - - static async databaseToTileset( - input: string, - output: string, - force: boolean - ) { - ToolsMain.ensureCanWrite(output, force); - - const pipelineJson = ToolsMain.createDatabaseToTilesetPipeline( - input, - output - ); - const pipeline = Pipelines.createPipeline(pipelineJson); - await PipelineExecutor.executePipeline(pipeline, force); + if (inputExtension === ".zip") { + await ZipToPackage.convert(input, output, force); + } else { + const tilesetSource = TilesetSources.createAndOpen(input); + const tilesetTarget = TilesetTargets.createAndBegin(output, force); + + const keys = tilesetSource.getKeys(); + for (const key of keys) { + const content = tilesetSource.getValue(key); + if (content) { + tilesetTarget.addEntry(key, content); + } + } + tilesetSource.close(); + await tilesetTarget.end(); + } } static async combine(input: string, output: string, force: boolean) { @@ -404,6 +368,13 @@ export class ToolsMain { await Tilesets.merge(inputs, output, force); } + static async pipeline(input: string, force: boolean) { + const pipelineJsonBuffer = fs.readFileSync(input); + const pipelineJson = JSON.parse(pipelineJsonBuffer.toString()); + const pipeline = Pipelines.createPipeline(pipelineJson); + await PipelineExecutor.executePipeline(pipeline, force); + } + /** * Returns whether the specified file can be written. * diff --git a/src/main.ts b/src/main.ts index 45c77cc5..52e97043 100644 --- a/src/main.ts +++ b/src/main.ts @@ -45,6 +45,15 @@ const inputArrayDefinition: any = { demandOption: true, }; +const outputStringDefinition: any = { + alias: "output", + description: "Output path for the command.", + global: true, + normalize: true, + type: "string", + demandOption: true, +}; + /** * Parses the arguments that are intended for the actual 3D Tiles tools * (ignoring the option arguments), and returns the result. @@ -58,14 +67,6 @@ function parseToolArgs(a: string[]) { .help("h") .alias("h", "help") .options({ - o: { - alias: "output", - description: "Output path for the command.", - global: true, - normalize: true, - type: "string", - demandOption: true, - }, f: { alias: "force", default: false, @@ -76,27 +77,39 @@ function parseToolArgs(a: string[]) { }) .command("tilesetToDatabase", "Create a sqlite database for a tileset.", { i: inputStringDefinition, + o: outputStringDefinition, }) .command( "databaseToTileset", "Unpack a tileset database to a tileset folder.", - { i: inputStringDefinition } + { i: inputStringDefinition, o: outputStringDefinition } + ) + .command( + "convert", + "Convert between tilesets and tileset package formats. " + + "The input and output can be paths to tileset JSON files, " + + "'.3tz', or '.3dtiles' files.", + { + i: inputStringDefinition, + o: outputStringDefinition, + } ) .command( "glbToB3dm", "Repackage the input glb as a b3dm with a basic header.", - { i: inputStringDefinition } + { i: inputStringDefinition, o: outputStringDefinition } ) .command( "glbToI3dm", "Repackage the input glb as a i3dm with a basic header.", - { i: inputStringDefinition } + { i: inputStringDefinition, o: outputStringDefinition } ) .command( "b3dmToGlb", "Extract the binary glTF asset from the input b3dm.", { i: inputStringDefinition, + o: outputStringDefinition, } ) .command( @@ -104,6 +117,7 @@ function parseToolArgs(a: string[]) { "Extract the binary glTF asset from the input i3dm.", { i: inputStringDefinition, + o: outputStringDefinition, } ) .command( @@ -111,6 +125,7 @@ function parseToolArgs(a: string[]) { "Extract the binary glTF assets from the input cmpt.", { i: inputStringDefinition, + o: outputStringDefinition, } ) .command( @@ -118,6 +133,7 @@ function parseToolArgs(a: string[]) { "Pass the input b3dm through gltf-pipeline. To pass options to gltf-pipeline, place them after --options. (--options -h for gltf-pipeline help)", { i: inputStringDefinition, + o: outputStringDefinition, options: { description: "All arguments after this flag will be passed to gltf-pipeline as command line options.", @@ -129,6 +145,7 @@ function parseToolArgs(a: string[]) { "Pass the input i3dm through gltf-pipeline. To pass options to gltf-pipeline, place them after --options. (--options -h for gltf-pipeline help)", { i: inputStringDefinition, + o: outputStringDefinition, options: { description: "All arguments after this flag will be passed to gltf-pipeline as command line options.", @@ -137,6 +154,7 @@ function parseToolArgs(a: string[]) { ) .command("gzip", "Gzips the input tileset directory.", { i: inputStringDefinition, + o: outputStringDefinition, t: { alias: "tilesOnly", default: false, @@ -146,22 +164,26 @@ function parseToolArgs(a: string[]) { }) .command("ungzip", "Ungzips the input tileset directory.", { i: inputStringDefinition, + o: outputStringDefinition, }) .command( "combine", "Combines all external tilesets into a single tileset.json file.", - { i: inputStringDefinition } + { i: inputStringDefinition, o: outputStringDefinition } ) .command( "merge", "Merge any number of tilesets together into a single tileset.", - { i: inputArrayDefinition } + { i: inputArrayDefinition, o: outputStringDefinition } ) .command( "upgrade", "Upgrades the input tileset to the latest version of the 3D Tiles spec. Embedded glTF models will be upgraded to glTF 2.0.", - { i: inputStringDefinition } + { i: inputStringDefinition, o: outputStringDefinition } ) + .command("pipeline", "Execute a pipeline that is provided as a JSON file", { + i: inputStringDefinition, + }) .command( "analyze", "Analyze the input file, and write the results to the output directory. " + @@ -169,7 +191,7 @@ function parseToolArgs(a: string[]) { "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", - { i: inputStringDefinition } + { i: inputStringDefinition, o: outputStringDefinition } ) .demandCommand(1) .strict(); @@ -249,17 +271,27 @@ async function runCommand(command: string, toolArgs: any, optionArgs: any) { } else if (command === "ungzip") { await ToolsMain.ungzip(input, output, force); } else if (command === "tilesetToDatabase") { - await ToolsMain.tilesetToDatabase(input, output, force); + console.log( + `The 'tilesetToDatabase' command is deprecated. Use 'convert' instead.` + ); + await ToolsMain.convert(input, output, force); } else if (command === "databaseToTileset") { - await ToolsMain.databaseToTileset(input, output, force); + console.log( + `The 'databaseToTileset' command is deprecated. Use 'convert' instead.` + ); + await ToolsMain.convert(input, output, force); + } else if (command === "convert") { + await ToolsMain.convert(input, output, force); } else if (command === "combine") { await ToolsMain.combine(input, output, force); } else if (command === "upgrade") { await ToolsMain.upgrade(input, output, force); } else if (command === "merge") { await ToolsMain.merge(inputs, output, force); + } else if (command === "pipeline") { + await ToolsMain.pipeline(input, force); } else if (command === "analyze") { - await ToolsMain.analyze(input, output, force); + ToolsMain.analyze(input, output, force); } else { throw new DeveloperError(`Invalid command: ${command}`); } diff --git a/demos/ZipToPackage.ts b/src/packages/ZipToPackage.ts similarity index 95% rename from demos/ZipToPackage.ts rename to src/packages/ZipToPackage.ts index 25c3ff87..63a82837 100644 --- a/demos/ZipToPackage.ts +++ b/src/packages/ZipToPackage.ts @@ -1,6 +1,6 @@ import StreamZip from "node-stream-zip"; -import { TilesetTargets } from "../src/tilesetData/TilesetTargets"; +import { TilesetTargets } from "../tilesetData/TilesetTargets"; /** * Methods for converting ZIP files into 3D Tiles packages. diff --git a/src/pipelines/ContentStageExecutor.ts b/src/pipelines/ContentStageExecutor.ts index d930d250..36e45342 100644 --- a/src/pipelines/ContentStageExecutor.ts +++ b/src/pipelines/ContentStageExecutor.ts @@ -10,13 +10,13 @@ import { ContentDataTypeChecks } from "../contentTypes/ContentDataTypeChecks"; import { TilesetEntry } from "../tilesetData/TilesetEntry"; import { ContentStage } from "./ContentStage"; +import { ContentStages } from "./ContentStages"; import { PipelineError } from "./PipelineError"; import { BasicTilesetProcessor } from "../tilesetProcessing/BasicTilesetProcessor"; import { GltfUtilities } from "../contentProcessing/GtlfUtilities"; import { ContentOps } from "../contentProcessing/ContentOps"; -import { ContentStages } from "./ContentStages"; /** * Methods to execute `ContentStage` objects. @@ -66,8 +66,24 @@ export class ContentStageExecutor { await ContentStageExecutor.executeGzip(tilesetProcessor, condition); } else if (contentStage.name === ContentStages.CONTENT_STAGE_UNGZIP) { await ContentStageExecutor.executeGunzip(tilesetProcessor); + } else if (contentStage.name === ContentStages.CONTENT_STAGE_GLB_TO_B3DM) { + await ContentStageExecutor.executeGlbToB3dm(tilesetProcessor); + } else if (contentStage.name === ContentStages.CONTENT_STAGE_GLB_TO_I3DM) { + await ContentStageExecutor.executeGlbToI3dm(tilesetProcessor); } else if (contentStage.name === ContentStages.CONTENT_STAGE_B3DM_TO_GLB) { await ContentStageExecutor.executeB3dmToGlb(tilesetProcessor); + } else if (contentStage.name === ContentStages.CONTENT_STAGE_I3DM_TO_GLB) { + await ContentStageExecutor.executeI3dmToGlb(tilesetProcessor); + } else if ( + contentStage.name === ContentStages.CONTENT_STAGE_OPTIMIZE_B3DM + ) { + const options = contentStage.options; + await ContentStageExecutor.executeOptimizeB3dm(tilesetProcessor, options); + } else if ( + contentStage.name === ContentStages.CONTENT_STAGE_OPTIMIZE_I3DM + ) { + const options = contentStage.options; + await ContentStageExecutor.executeOptimizeI3dm(tilesetProcessor, options); } else if (contentStage.name === ContentStages.CONTENT_STAGE_OPTIMIZE_GLB) { const options = contentStage.options; await ContentStageExecutor.executeOptimizeGlb(tilesetProcessor, options); @@ -158,6 +174,104 @@ export class ContentStageExecutor { await tilesetProcessor.processAllEntries(entryProcessor); } + /** + * Performs the 'glbToB3dm' content stage with the given processor. + * + * This will process all tile contents entries of the source tileset + * that have the `CONTENT_TYPE_GLB`. These entries will be replaced + * by entries that contain the B3DM data that was created from the GLB. + * + * If the entries have names that end in `.glb`, then these + * extensions will be changed to `.b3dm`. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ + private static async executeGlbToB3dm( + tilesetProcessor: BasicTilesetProcessor + ): Promise { + // Define the rule for updating the key (file name) of + // the entries, as well as possible template URIs of + // implicit tileset roots. + const uriProcessor = (uri: string) => { + if (Paths.hasExtension(uri, ".glb")) { + return Paths.replaceExtension(uri, ".b3dm"); + } + return uri; + }; + + // Define the `TilesetEntryProcessor` that generates an + // entry with B3DM data from an entry with GLB data. + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { + return sourceEntry; + } + const targetEntry = { + key: uriProcessor(sourceEntry.key), + value: ContentOps.glbToB3dmBuffer(sourceEntry.value), + }; + return targetEntry; + }; + await tilesetProcessor.processTileContentEntries( + uriProcessor, + entryProcessor + ); + } + + /** + * Performs the 'glbToI3dm' content stage with the given processor. + * + * This will process all tile contents entries of the source tileset + * that have the `CONTENT_TYPE_GLB`. These entries will be replaced + * by entries that contain the I3DM data that was created from the GLB. + * + * If the entries have names that end in `.glb`, then these + * extensions will be changed to `.i3dm`. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ + private static async executeGlbToI3dm( + tilesetProcessor: BasicTilesetProcessor + ): Promise { + // Define the rule for updating the key (file name) of + // the entries, as well as possible template URIs of + // implicit tileset roots. + const uriProcessor = (uri: string) => { + if (Paths.hasExtension(uri, ".glb")) { + return Paths.replaceExtension(uri, ".i3dm"); + } + return uri; + }; + + // Define the `TilesetEntryProcessor` that generates an + // entry with I3DM data from an entry with GLB data. + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_GLB) { + return sourceEntry; + } + const targetEntry = { + key: uriProcessor(sourceEntry.key), + value: ContentOps.glbToI3dmBuffer(sourceEntry.value), + }; + return targetEntry; + }; + await tilesetProcessor.processTileContentEntries( + uriProcessor, + entryProcessor + ); + } + /** * Performs the 'b3dmToGlb' content stage with the given processor. * @@ -207,6 +321,143 @@ export class ContentStageExecutor { ); } + /** + * Performs the 'i3dmToGlb' content stage with the given processor. + * + * This will process all tile contents entries of the source tileset + * that have the `CONTENT_TYPE_I3DM`. These entries will be replaced + * by entries that contain the GLB data from the I3DM. + * + * If the entries have names that end in `.i3dm`, then these + * extensions will be changed to `.glb`. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ + private static async executeI3dmToGlb( + tilesetProcessor: BasicTilesetProcessor + ): Promise { + // Define the rule for updating the key (file name) of + // the entries, as well as possible template URIs of + // implicit tileset roots. + const uriProcessor = (uri: string) => { + if (Paths.hasExtension(uri, ".i3dm")) { + return Paths.replaceExtension(uri, ".glb"); + } + return uri; + }; + + // Define the `TilesetEntryProcessor` that generates an + // entry with GLB data from an entry with I3DM data. + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_I3DM) { + return sourceEntry; + } + const targetEntry = { + key: uriProcessor(sourceEntry.key), + value: ContentOps.i3dmToGlbBuffer(sourceEntry.value), + }; + return targetEntry; + }; + await tilesetProcessor.processTileContentEntries( + uriProcessor, + entryProcessor + ); + } + + /** + * Performs the 'optimizeB3dm' content stage with the given processor. + * + * This will process all tile contents entries of the source tileset + * that have the `CONTENT_TYPE_B3DM`, and apply the `gltf-pipeline` + * optimization with the given options to their GLB data. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @param options - The options for `gltf-pipeline` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ + private static async executeOptimizeB3dm( + tilesetProcessor: BasicTilesetProcessor, + options: any + ): Promise { + // The entry processor receives the source entry, and + // returns a target entry where the the `value` contains + // GLB data that was optimized with `gltf-pipeline` + // and the given options + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_B3DM) { + return sourceEntry; + } + const targetValue = await ContentOps.optimizeB3dmBuffer( + sourceEntry.value, + options + ); + const targetEntry = { + key: sourceEntry.key, + value: targetValue, + }; + return targetEntry; + }; + await tilesetProcessor.processTileContentEntries( + (uri: string) => uri, + entryProcessor + ); + } + + /** + * Performs the 'optimizeI3dm' content stage with the given processor. + * + * This will process all tile contents entries of the source tileset + * that have the `CONTENT_TYPE_I3DM`, and apply the `gltf-pipeline` + * optimization with the given options to their GLB data. + * + * @param tilesetProcessor - The `BasicTilesetProcessor` + * @param options - The options for `gltf-pipeline` + * @returns A promise that resolves when the process is finished + * @throws Error If one of the processing steps causes + * an error. + */ + private static async executeOptimizeI3dm( + tilesetProcessor: BasicTilesetProcessor, + options: any + ): Promise { + // The entry processor receives the source entry, and + // returns a target entry where the the `value` contains + // GLB data that was optimized with `gltf-pipeline` + // and the given options + const entryProcessor = async ( + sourceEntry: TilesetEntry, + type: string | undefined + ) => { + if (type !== ContentDataTypes.CONTENT_TYPE_I3DM) { + return sourceEntry; + } + const targetValue = await ContentOps.optimizeI3dmBuffer( + sourceEntry.value, + options + ); + const targetEntry = { + key: sourceEntry.key, + value: targetValue, + }; + return targetEntry; + }; + await tilesetProcessor.processTileContentEntries( + (uri: string) => uri, + entryProcessor + ); + } + /** * Performs the 'optimizeGlb' content stage with the given processor. * diff --git a/src/pipelines/ContentStages.ts b/src/pipelines/ContentStages.ts index 0f26cbe4..9b949663 100644 --- a/src/pipelines/ContentStages.ts +++ b/src/pipelines/ContentStages.ts @@ -9,7 +9,12 @@ import { ContentStage } from "./ContentStage"; export class ContentStages { public static readonly CONTENT_STAGE_GZIP = "gzip"; public static readonly CONTENT_STAGE_UNGZIP = "ungzip"; + public static readonly CONTENT_STAGE_GLB_TO_B3DM = "glbToB3dm"; + public static readonly CONTENT_STAGE_GLB_TO_I3DM = "glbToI3dm"; public static readonly CONTENT_STAGE_B3DM_TO_GLB = "b3dmToGlb"; + public static readonly CONTENT_STAGE_I3DM_TO_GLB = "i3dmToGlb"; + public static readonly CONTENT_STAGE_OPTIMIZE_B3DM = "optimizeB3dm"; + public static readonly CONTENT_STAGE_OPTIMIZE_I3DM = "optimizeI3dm"; public static readonly CONTENT_STAGE_OPTIMIZE_GLB = "optimizeGlb"; public static readonly CONTENT_STAGE_SEPARATE_GLTF = "separateGltf"; @@ -32,6 +37,22 @@ export class ContentStages { return contentStage; } + public static createGlbToB3dm(): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_GLB_TO_B3DM, + description: "Convert each GLB into a default B3DM", + }; + return contentStage; + } + + public static createGlbToI3dm(): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_GLB_TO_I3DM, + description: "Convert each GLB into a default I3DM", + }; + return contentStage; + } + public static createB3dmToGlb(): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_B3DM_TO_GLB, @@ -40,6 +61,34 @@ export class ContentStages { return contentStage; } + public static createI3dmToGlb(): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_I3DM_TO_GLB, + description: "Convert each I3DM content into GLB", + }; + return contentStage; + } + + public static createOptimizeB3dm(options: any): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_OPTIMIZE_B3DM, + description: + "Apply gltf-pipeline to the GLB part of each B3DM content, with the given options", + options: options, + }; + return contentStage; + } + + public static createOptimizeI3dm(options: any): ContentStage { + const contentStage: ContentStage = { + name: ContentStages.CONTENT_STAGE_OPTIMIZE_B3DM, + description: + "Apply gltf-pipeline to the GLB part of each I3DM content, with the given options", + options: options, + }; + return contentStage; + } + public static createOptimizeGlb(options: any): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_OPTIMIZE_GLB, From 74c7344a448ba583c98c2964400f8f36aa3957c8 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Apr 2023 18:27:42 +0200 Subject: [PATCH 54/60] Update README and implementation notes --- IMPLEMENTATION.md | 8 ++++ README.md | 107 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 109 insertions(+), 6 deletions(-) diff --git a/IMPLEMENTATION.md b/IMPLEMENTATION.md index caf4601a..8472c655 100644 --- a/IMPLEMENTATION.md +++ b/IMPLEMENTATION.md @@ -51,6 +51,11 @@ Parts of the current implementation may still change. This page is only a short - 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 + - The `Pipeline` class describes the pipeline with its input and output, and contains one or more `TilesetStage` objects + - The `TilesetStage` describes an operation that is applied to the tileset as a whole, usually focussing on modifications of the tileset JSON object. It may contain one or more `ContentStage` objects + - The `ContentStage` is an operation that may be applied to tile content (i.e. "files") that are part of the tileset + - Instances of these classes may be created with the `Pipelines`, `TilesetStages`, and `ContentStages` classes, respectively + - A pipeline may be executed by a `PipelineExecutor`. - `./src/spatial`: Basic classes for dealing with tree structures, specifically with quadtrees and octrees @@ -73,10 +78,13 @@ Parts of the current implementation may still change. This page is only a short - `TilesetCombiner`: Used to "inline" external tilesets into a single one - `TilesetMerger`: Used to create one tileset that refers to others as external tilesets - `TilesetUpgrader`: Upgrade a tileset to a newer version (many aspects unspecified here) + - The (abstract) `TilesetProcessor` class and the (concrete) `BasicTilesetProcessor` class offer an infrastructure for generic operations on the tilesets and their content. These classes serve as the basis for the implementation of the pipeline execution functionality. - `./src/tilesets`: Utility functions for tileset operations - `Tiles` for traversing (explicit!) tile hierarchies - `Tilesets` offering convenience functions for `merge/combine/upgrade` + - `Contents` with utility functions related to tile `content` objects + - `Extensions` for handling extensions and extension declarations in tilesets (and glTF objects) - `./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. diff --git a/README.md b/README.md index 3e5491db..228b2cb5 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,40 @@ Merge multiple tilesets into a single one that refers to the input tilesets as e npx ts-node ./src/main.ts merge -i ./specs/data/mergeTilesets/TilesetA -i ./specs/data/mergeTilesets/sub/TilesetA -o ./specs/data/mergeTilesets/output ``` +#### upgrade + +Upgrade a tileset to the latest 3D Tiles version. +``` +npx ts-node ./src/main.ts upgrade -i ./specs/data/TilesetOfTilesets/tileset.json -o ./output/upgraded +``` +The exact behavior of the upgrade operation is not yet specified. But when B3DM- and I3DM tile content in the input tileset uses glTF 1.0 assets, then the upgrade step will try to upgrade these assets to glTF 2.0. + +#### convert + +(This replaces the `databaseToTileset` and `tilesetToDatabase` commands) + +Convert between tilesets and tileset package formats. +``` +npx ts-node ./src/main.ts upgrade -i ./specs/data/TilesetOfTilesets/tileset.json -o ./output/TilesetOfTilesets.3tz +``` + +The input- and output arguments for this command may be + +- The name of a directory that contains a `tileset.json` file (or the full path to a tileset JSON file) +- The name of a `.3tz` file +- The name of a `.3dtiles` file + +The input may also be a `.zip` file that contains a `tileset.json` file. + +#### databaseToTileset + +Deprecated. This functionality is now offered via the `convert` command. + +#### tilesetToDatabase + +Deprecated. This functionality is now offered via the `convert` command. + + ### Command line tools for tile content @@ -116,7 +150,7 @@ npx ts-node ./src/main.ts optimizeB3dm -i ./specs/data/Textured/batchedTextured. This example optimizes the b3dm and compresses the meshes using Draco, with a high compression level. -### optimizeI3dm +#### optimizeI3dm Optimize a i3dm using [gltf-pipeline](https://github.com/CesiumGS/gltf-pipeline/blob/main/README.md). ``` @@ -125,18 +159,79 @@ npx ts-node ./src/main.ts optimizeI3dm -i ./specs/data/instancedWithBatchTableBi See [optimizeB3dm](#optimizeb3dm) for further examples. -### upgrade +### Pipeline -Upgrade a tileset to the latest 3D Tiles version. +Execute a sequence of operations that are described in a JSON file. + +> **Note:** The pipeline execution feature is preliminary. Many aspects of the pipeline definition, including the JSON representation and the exact set of operations that are supported as parts of pipelines may change in future releases. + + The basic structure of a pipeline JSON file is summarized here: + +- A pipeline has an `input` and `output`, which are the names of a tileset directory or package +- A pipeline has an array of 'tileset stages' +- A tileset stage has a `name` and a `description` +- A tileset stage has an array of 'content stages' +- A content stage has a `name` and a `description` +- A content stage can carry information about the content types that it is applied to + +A simple example pipline may therefore look like this: ``` -npx ts-node ./src/main.ts upgrade -i ./specs/data/TilesetOfTilesets/tileset.json -o ./output/upgraded +{ + "input": "./specs/data/TilesetOfTilesetsWithUris", + "output": "./output/TilesetOfTilesetsWithUris.3tz", + "tilesetStages": [ + { + "name": "_b3dmToGlb", + "description": "Convert B3DM to GLB", + "contentStages": [ + { + "name": "b3dmToGlb", + "description": "Convert each B3DM content into GLB" + } + ] + } + ] +} ``` -The exact behavior of the upgrade operation is not yet specified. But when B3DM- and I3DM tile content in the input tileset uses glTF 1.0 assets, then the upgrade step will try to upgrade these assets to glTF 2.0. + +The `name` of a tileset- or content stage can refer to a predefined set of operations that can be executed. If a `name` is not one of the known operations, it should start with an `_` underscore. + +The `description` of a tileset- or content stage is intended as a human-readable summary, to be shown as log output. + +The predefined operations largely correspond to the command-line functionality. + +The known tileset stages are: + +- `upgrade`: Upgrade the input tileset to the latest version. Details about what that means are omitted here. +- `combine`: Combine all external tilesets of the input tileset, to create a single tileset + +The known content stages are: + +- Compression: + - `gzip`: Apply GZIP compression to all files (with optional filters) + - `ungzip`: Uncompress all files that are compressed with GZIP +- Conversion: + - `glbToB3dm`: Convert all GLB tile contents into B3DM + - `glbToI3dm`: Convert all GLB tile contents into I3DM (with the GLB being the only instance) + - `b3dmToGlb`: Convert all B3DM tile contents into GLB (assuming that the B3DM is only a wrapper around GLB) + - `i3dmToGlb`: Convert all I3DM tile contents into GLB (assuming that the I3DM is only a wrapper around GLB) + - `separateGltf`: Convert all GLB tile contents into `.gltf` files with external resources +- Optimization: + + These operations receive an `options` object, which is an untyped object carrying the options that are passed to `gltf-pipeline` for the optimization. + - `optimizeGlb`: Optimize GLB tile content, using `gltf-pipeline` + - `optimizeB3dm`: Optimize the GLB payload of a B3DM tile content, using `gltf-pipeline` + - `optimizeI3dm`: Optimize the GLB payload of a I3DM tile content, using `gltf-pipeline` + +An example of a pipeline that combines a sequence of multiple operations is shown in [`examplePipeline.json`](./specs/data/pipelines/examplePipeline.json). + --- -**Draft** demos for the library usage: +## Demos + +The `demos` folder contains some examples of how the functionality of the tools may be used as a library. This is intended as a preview. The functionality is not yet exposed as a public API. ### General tool functions From f97245d1716d0764456c839fcab70a786d1df92b Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Apr 2023 18:30:01 +0200 Subject: [PATCH 55/60] Minor cleanups in demos --- demos/PipelineExperimentsContentStages.ts | 14 +++++++------- demos/PipelineExperimentsTilesetStages.ts | 11 +++++++---- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/demos/PipelineExperimentsContentStages.ts b/demos/PipelineExperimentsContentStages.ts index 4ed6555d..e194fe03 100644 --- a/demos/PipelineExperimentsContentStages.ts +++ b/demos/PipelineExperimentsContentStages.ts @@ -19,7 +19,6 @@ function createPipeline(tilesetStage: TilesetStage) { } async function example() { - const overwrite = true; const optimizeGlbOptions = { dracoOptions: { compressionLevel: 10, @@ -27,41 +26,42 @@ async function example() { }; const tilesetStageB3dmToGlb = TilesetStages.create( - "B3DM to GLB", + "_b3dmToGlb", "Convert B3DM to GLB", [ContentStages.createB3dmToGlb()] ); const tilesetStageI3dmToGlb = TilesetStages.create( - "I3DM to GLB", + "_i3dmToGlb", "Convert I3DM to GLB", [ContentStages.createI3dmToGlb()] ); const tilesetStageGlbToB3dm = TilesetStages.create( - "GLB to B3DM", + "_glbToB3dm", "Convert GLB to B3DM", [ContentStages.createGlbToB3dm()] ); const tilesetStageGlbToI3dm = TilesetStages.create( - "GLB to I3DM", + "_glbToI3dm", "Convert GLB to I3DM", [ContentStages.createGlbToI3dm()] ); const tilesetStageOptimizeB3dm = TilesetStages.create( - "Optimize B3DM", + "_optimizeB3dm", "Optimize the GLB part of B3DM", [ContentStages.createOptimizeB3dm(optimizeGlbOptions)] ); const tilesetStageOptimizeI3dm = TilesetStages.create( - "Optimize I3DM", + "_optimizeI3dm", "Optimize the GLB part of I3DM", [ContentStages.createOptimizeI3dm(optimizeGlbOptions)] ); + const overwrite = true; await PipelineExecutor.executePipeline( createPipeline(tilesetStageB3dmToGlb), overwrite diff --git a/demos/PipelineExperimentsTilesetStages.ts b/demos/PipelineExperimentsTilesetStages.ts index 8d293fe3..9e726d0a 100644 --- a/demos/PipelineExperimentsTilesetStages.ts +++ b/demos/PipelineExperimentsTilesetStages.ts @@ -2,6 +2,7 @@ import { Pipeline } from "../src/pipelines/Pipeline"; import { TilesetStages } from "../src/pipelines/TilesetStages"; import { ContentStages } from "../src/pipelines/ContentStages"; import { PipelineExecutor } from "../src/pipelines/PipelineExecutor"; +import { ContentDataTypes } from "../src/contentTypes/ContentDataTypes"; async function example() { const input = "./specs/data/TilesetOfTilesets"; @@ -16,16 +17,18 @@ async function example() { const tilesetStages = [ TilesetStages.createUpgrade(), TilesetStages.createCombine(), - TilesetStages.create("B3DM to GLB", "Convert B3DM to GLB", [ + TilesetStages.create("_b3dmToGlb", "Convert B3DM to GLB", [ ContentStages.createB3dmToGlb(), ]), - TilesetStages.create("Optimize GLB", "Optimize GLB", [ + TilesetStages.create("_optimizeGlb", "Optimize GLB", [ ContentStages.createOptimizeGlb(optimizeGlbOptions), ]), - TilesetStages.create("Separate glTF", "Separate glTF", [ + TilesetStages.create("_separateGltf", "Separate glTF", [ ContentStages.createSeparateGltf(), ]), - TilesetStages.create("Dummy", "Dummy", []), + TilesetStages.create("_gzip", "Gzip (glTF only)", [ + ContentStages.createGzip([ContentDataTypes.CONTENT_TYPE_GLTF]), + ]), ]; const pipeline: Pipeline = { From 97dd025fcf7a151d7761870a29d655565eb782ec Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Apr 2023 18:30:37 +0200 Subject: [PATCH 56/60] Clean up imports --- src/ToolsMain.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ToolsMain.ts b/src/ToolsMain.ts index fb3f3683..fc1363ed 100644 --- a/src/ToolsMain.ts +++ b/src/ToolsMain.ts @@ -3,10 +3,12 @@ import path from "path"; import { Paths } from "./base/Paths"; import { DeveloperError } from "./base/DeveloperError"; +import { Buffers } from "./base/Buffers"; import { Tilesets } from "./tilesets/Tilesets"; import { TileFormats } from "./tileFormats/TileFormats"; +import { TileDataLayouts } from "./tileFormats/TileDataLayouts"; import { ContentOps } from "./contentProcessing/ContentOps"; import { GltfUtilities } from "./contentProcessing/GtlfUtilities"; @@ -15,10 +17,9 @@ import { ContentDataTypes } from "./contentTypes/ContentDataTypes"; import { PipelineExecutor } from "./pipelines/PipelineExecutor"; import { Pipelines } from "./pipelines/Pipelines"; -import { Buffers } from "./base/Buffers"; -import { TileDataLayouts } from "./tileFormats/TileDataLayouts"; -import { Pipeline } from "./pipelines/Pipeline"; + import { ZipToPackage } from "./packages/ZipToPackage"; + import { TilesetSources } from "./tilesetData/TilesetSources"; import { TilesetTargets } from "./tilesetData/TilesetTargets"; From 86776d49615bd01432978b63c6afbe861804b2fe Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Apr 2023 18:31:10 +0200 Subject: [PATCH 57/60] Minor documentation updates --- src/contentTypes/ContentDataTypeRegistry.ts | 8 +- src/pipelines/ContentStages.ts | 94 ++++++++++++++++++++- src/pipelines/TilesetStages.ts | 27 +++++- src/traversal/ExplicitTraversedTile.ts | 4 +- src/traversal/TilesetTraverser.ts | 2 + 5 files changed, 127 insertions(+), 8 deletions(-) diff --git a/src/contentTypes/ContentDataTypeRegistry.ts b/src/contentTypes/ContentDataTypeRegistry.ts index d0997bce..92235f50 100644 --- a/src/contentTypes/ContentDataTypeRegistry.ts +++ b/src/contentTypes/ContentDataTypeRegistry.ts @@ -8,10 +8,10 @@ import { BufferedContentData } from "./BufferedContentData"; /** * A class for determining the type of data that a URI points to. * - * The only public methods (for now) are `registerDefaults`, - * which registers all known content data types, and - * `findContentDataType`, which returns the string that - * describes the type of a `ContentData` object. + * The only public methods (for now) are `findType`, which + * determines the type of data that is given as a URI and + * a buffer, and `findContentDataType`, which returns the + * string that describes the type of a `ContentData` object. * * @internal */ diff --git a/src/pipelines/ContentStages.ts b/src/pipelines/ContentStages.ts index 9b949663..f60d2664 100644 --- a/src/pipelines/ContentStages.ts +++ b/src/pipelines/ContentStages.ts @@ -4,20 +4,67 @@ import { DeveloperError } from "../base/DeveloperError"; import { ContentStage } from "./ContentStage"; /** - * Methods to create `ContentStage` objects from JSON input. + * Methods to create `ContentStage` objects */ export class ContentStages { + /** + * The `name` that identifies the "gzip" content stage + */ public static readonly CONTENT_STAGE_GZIP = "gzip"; + + /** + * The `name` that identifies the "ungzip" content stage + */ public static readonly CONTENT_STAGE_UNGZIP = "ungzip"; + + /** + * The `name` that identifies the "glbToB3dm" content stage + */ public static readonly CONTENT_STAGE_GLB_TO_B3DM = "glbToB3dm"; + + /** + * The `name` that identifies the "glbToI3dm" content stage + */ public static readonly CONTENT_STAGE_GLB_TO_I3DM = "glbToI3dm"; + + /** + * The `name` that identifies the "b3dmToGlb" content stage + */ public static readonly CONTENT_STAGE_B3DM_TO_GLB = "b3dmToGlb"; + + /** + * The `name` that identifies the "i3dmToGlb" content stage + */ public static readonly CONTENT_STAGE_I3DM_TO_GLB = "i3dmToGlb"; + + /** + * The `name` that identifies the "optimizeB3dm" content stage + */ public static readonly CONTENT_STAGE_OPTIMIZE_B3DM = "optimizeB3dm"; + + /** + * The `name` that identifies the "optimizeI3dm" content stage + */ public static readonly CONTENT_STAGE_OPTIMIZE_I3DM = "optimizeI3dm"; + + /** + * The `name` that identifies the "optimizeGlb" content stage + */ public static readonly CONTENT_STAGE_OPTIMIZE_GLB = "optimizeGlb"; + + /** + * The `name` that identifies the "separateGltf" content stage + */ public static readonly CONTENT_STAGE_SEPARATE_GLTF = "separateGltf"; + /** + * Creates a content stage that performs the "gzip" operation + * + * @param - The array of `ContentDataType` strings that the operation + * should be applied to (or `undefined` if it should be applied to + * all data types) + * @returns The content stage + */ public static createGzip( includedContentTypes: string[] | undefined ): ContentStage { @@ -29,6 +76,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "ungzip" operation + * + * @returns The content stage + */ public static createUngzip(): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_UNGZIP, @@ -37,6 +89,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "glbToB3dm" operation + * + * @returns The content stage + */ public static createGlbToB3dm(): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_GLB_TO_B3DM, @@ -45,6 +102,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "glbToI3dm" operation + * + * @returns The content stage + */ public static createGlbToI3dm(): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_GLB_TO_I3DM, @@ -53,6 +115,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "b3dmToGlb" operation + * + * @returns The content stage + */ public static createB3dmToGlb(): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_B3DM_TO_GLB, @@ -61,6 +128,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "i3dmToGlb" operation + * + * @returns The content stage + */ public static createI3dmToGlb(): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_I3DM_TO_GLB, @@ -69,6 +141,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "optimizeGlb" operation + * + * @returns The content stage + */ public static createOptimizeB3dm(options: any): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_OPTIMIZE_B3DM, @@ -79,6 +156,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "optimizeI3dm" operation + * + * @returns The content stage + */ public static createOptimizeI3dm(options: any): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_OPTIMIZE_B3DM, @@ -89,6 +171,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "optimizeGlb" operation + * + * @returns The content stage + */ public static createOptimizeGlb(options: any): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_OPTIMIZE_GLB, @@ -99,6 +186,11 @@ export class ContentStages { return contentStage; } + /** + * Creates a content stage that performs the "separateGlb" operation + * + * @returns The content stage + */ public static createSeparateGltf(): ContentStage { const contentStage: ContentStage = { name: ContentStages.CONTENT_STAGE_SEPARATE_GLTF, diff --git a/src/pipelines/TilesetStages.ts b/src/pipelines/TilesetStages.ts index 0972ddfe..c84ce07c 100644 --- a/src/pipelines/TilesetStages.ts +++ b/src/pipelines/TilesetStages.ts @@ -6,12 +6,24 @@ import { TilesetStage } from "./TilesetStage"; import { ContentStages } from "./ContentStages"; /** - * Methods to create `TilesetStage` objects from JSON input. + * Methods to create `TilesetStage` objects. */ export class TilesetStages { + /** + * The `name` that identifies the "upgrade" tileset stage + */ public static readonly TILESET_STAGE_UPGRADE = "upgrade"; + + /** + * The `name` that identifies the "combine" tileset stage + */ public static readonly TILESET_STAGE_COMBINE = "combine"; + /** + * Creates a tileset stage that performs the "upgrade" operation + * + * @returns The tileset stage + */ public static createUpgrade(): TilesetStage { const tilesetStage: TilesetStage = { name: TilesetStages.TILESET_STAGE_UPGRADE, @@ -20,6 +32,11 @@ export class TilesetStages { return tilesetStage; } + /** + * Creates a tileset stage that performs the "combine" operation + * + * @returns The tileset stage + */ public static createCombine(): TilesetStage { const tilesetStage: TilesetStage = { name: TilesetStages.TILESET_STAGE_COMBINE, @@ -28,6 +45,14 @@ export class TilesetStages { return tilesetStage; } + /** + * Creates a tileset stage from the given parameters. + * + * @param name - The `name` of the tileset stage + * @param description - The `description` of the tileset stage + * @param contentStages - The content stages + * @returns The tileset stage + */ public static create( name: string, description: string, diff --git a/src/traversal/ExplicitTraversedTile.ts b/src/traversal/ExplicitTraversedTile.ts index 06742d5f..28745522 100644 --- a/src/traversal/ExplicitTraversedTile.ts +++ b/src/traversal/ExplicitTraversedTile.ts @@ -90,8 +90,8 @@ export class ExplicitTraversedTile implements TraversedTile { * @param path - A JSON-path-like string describing this tile * @param level - The level, referring to the root of the * traversal, starting at 0 - * @param parent The optional parent tile - * @param schema The optional metadata schema + * @param parent - The optional parent tile + * @param schema - The optional metadata schema * @param resourceResolver - The `ResourceResolver` for * external references (like subtree files) */ diff --git a/src/traversal/TilesetTraverser.ts b/src/traversal/TilesetTraverser.ts index 006e9c96..ad21ffa4 100644 --- a/src/traversal/TilesetTraverser.ts +++ b/src/traversal/TilesetTraverser.ts @@ -13,6 +13,8 @@ import { Tile } from "../structure/Tile"; /** * A collection of configuration options for the traversal. + * + * @internal */ export type TraversalOptions = { /** From 3c6314e66bb6f82e35527e530ed069823dde6334 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Apr 2023 18:31:39 +0200 Subject: [PATCH 58/60] Basic specs for implicit tilings --- specs/ImplicitTilingsSpec.ts | 309 ++++++++++++++++++++++++++ specs/SpecHelpers.ts | 43 ++++ src/implicitTiling/ImplicitTilings.ts | 19 +- 3 files changed, 368 insertions(+), 3 deletions(-) create mode 100644 specs/ImplicitTilingsSpec.ts diff --git a/specs/ImplicitTilingsSpec.ts b/specs/ImplicitTilingsSpec.ts new file mode 100644 index 00000000..48f01dce --- /dev/null +++ b/specs/ImplicitTilingsSpec.ts @@ -0,0 +1,309 @@ +import { ImplicitTilings } from "../src/implicitTiling/ImplicitTilings"; + +import { OctreeCoordinates } from "../src/spatial/OctreeCoordinates"; +import { QuadtreeCoordinates } from "../src/spatial/QuadtreeCoordinates"; + +import { SpecHelpers } from "./SpecHelpers"; + +function createQuadtreeImplicitTiling(subtreeLevels: number) { + const implicitTiling = { + subdivisionScheme: "QUADTREE", + subtreeLevels: subtreeLevels, + availableLevels: subtreeLevels * 2, + subtrees: { + uri: "SPEC_SUBTREES_URI", + }, + }; + return implicitTiling; +} + +function createOctreeImplicitTiling(subtreeLevels: number) { + const implicitTiling = { + subdivisionScheme: "OCTREE", + subtreeLevels: subtreeLevels, + availableLevels: subtreeLevels * 2, + subtrees: { + uri: "SPEC_SUBTREES_URI", + }, + }; + return implicitTiling; +} + +describe("ImplicitTilings", function () { + it("creates a proper iterator for QUADTREE with createSubtreeCoordinatesIterator", function () { + const implicitTiling = createQuadtreeImplicitTiling(3); + const iterator = + ImplicitTilings.createSubtreeCoordinatesIterator(implicitTiling); + const actualCoordinates = [...iterator].map((c) => c.toArray()); + + SpecHelpers.sortLexicographically(actualCoordinates); + const expectedCoordinates = [ + [0, 0, 0], + [1, 0, 0], + [1, 1, 0], + [1, 0, 1], + [1, 1, 1], + [2, 0, 0], + [2, 1, 0], + [2, 0, 1], + [2, 1, 1], + [2, 2, 0], + [2, 3, 0], + [2, 2, 1], + [2, 3, 1], + [2, 0, 2], + [2, 1, 2], + [2, 0, 3], + [2, 1, 3], + [2, 2, 2], + [2, 3, 2], + [2, 2, 3], + [2, 3, 3], + ]; + SpecHelpers.sortLexicographically(expectedCoordinates); + expect(actualCoordinates).toEqual(expectedCoordinates); + }); + + it("creates a proper iterator for OCTREE with createSubtreeCoordinatesIterator", function () { + const implicitTiling = createOctreeImplicitTiling(3); + const iterator = + ImplicitTilings.createSubtreeCoordinatesIterator(implicitTiling); + const actualCoordinates = [...iterator].map((c) => c.toArray()); + + SpecHelpers.sortLexicographically(actualCoordinates); + const expectedCoordinates = [ + [0, 0, 0, 0], + [1, 0, 0, 0], + [1, 1, 0, 0], + [1, 0, 1, 0], + [1, 1, 1, 0], + [1, 0, 0, 1], + [1, 1, 0, 1], + [1, 0, 1, 1], + [1, 1, 1, 1], + [2, 0, 0, 0], + [2, 1, 0, 0], + [2, 0, 1, 0], + [2, 1, 1, 0], + [2, 0, 0, 1], + [2, 1, 0, 1], + [2, 0, 1, 1], + [2, 1, 1, 1], + [2, 2, 0, 0], + [2, 3, 0, 0], + [2, 2, 1, 0], + [2, 3, 1, 0], + [2, 2, 0, 1], + [2, 3, 0, 1], + [2, 2, 1, 1], + [2, 3, 1, 1], + [2, 0, 2, 0], + [2, 1, 2, 0], + [2, 0, 3, 0], + [2, 1, 3, 0], + [2, 0, 2, 1], + [2, 1, 2, 1], + [2, 0, 3, 1], + [2, 1, 3, 1], + [2, 2, 2, 0], + [2, 3, 2, 0], + [2, 2, 3, 0], + [2, 3, 3, 0], + [2, 2, 2, 1], + [2, 3, 2, 1], + [2, 2, 3, 1], + [2, 3, 3, 1], + [2, 0, 0, 2], + [2, 1, 0, 2], + [2, 0, 1, 2], + [2, 1, 1, 2], + [2, 0, 0, 3], + [2, 1, 0, 3], + [2, 0, 1, 3], + [2, 1, 1, 3], + [2, 2, 0, 2], + [2, 3, 0, 2], + [2, 2, 1, 2], + [2, 3, 1, 2], + [2, 2, 0, 3], + [2, 3, 0, 3], + [2, 2, 1, 3], + [2, 3, 1, 3], + [2, 0, 2, 2], + [2, 1, 2, 2], + [2, 0, 3, 2], + [2, 1, 3, 2], + [2, 0, 2, 3], + [2, 1, 2, 3], + [2, 0, 3, 3], + [2, 1, 3, 3], + [2, 2, 2, 2], + [2, 3, 2, 2], + [2, 2, 3, 2], + [2, 3, 3, 2], + [2, 2, 2, 3], + [2, 3, 2, 3], + [2, 2, 3, 3], + [2, 3, 3, 3], + ]; + SpecHelpers.sortLexicographically(expectedCoordinates); + expect(actualCoordinates).toEqual(expectedCoordinates); + }); + + it("throws an error for non-positive subtree levels in createSubtreeCoordinatesIterator", function () { + const implicitTiling = createQuadtreeImplicitTiling(0); + expect(function () { + ImplicitTilings.createSubtreeCoordinatesIterator(implicitTiling); + }).toThrowError(); + }); + + it("computes the right number of nodes for QUADTREE with computeNumberOfNodesPerSubtree", function () { + const implicitTiling1 = createQuadtreeImplicitTiling(1); + const implicitTiling2 = createQuadtreeImplicitTiling(2); + const implicitTiling4 = createQuadtreeImplicitTiling(4); + const implicitTiling8 = createQuadtreeImplicitTiling(8); + + const actualNumberOfNodes1 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling1); + const actualNumberOfNodes2 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling2); + const actualNumberOfNodes4 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling4); + const actualNumberOfNodes8 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling8); + + const expectedNumberOfNodes1 = 1; + const expectedNumberOfNodes2 = 5; + const expectedNumberOfNodes4 = 85; + const expectedNumberOfNodes8 = 21845; + + expect(actualNumberOfNodes1).toEqual(expectedNumberOfNodes1); + expect(actualNumberOfNodes2).toEqual(expectedNumberOfNodes2); + expect(actualNumberOfNodes4).toEqual(expectedNumberOfNodes4); + expect(actualNumberOfNodes8).toEqual(expectedNumberOfNodes8); + }); + it("computes the right number of nodes for OCTREE with computeNumberOfNodesPerSubtree", function () { + const implicitTiling1 = createOctreeImplicitTiling(1); + const implicitTiling2 = createOctreeImplicitTiling(2); + const implicitTiling4 = createOctreeImplicitTiling(4); + const implicitTiling8 = createOctreeImplicitTiling(8); + + const actualNumberOfNodes1 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling1); + const actualNumberOfNodes2 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling2); + const actualNumberOfNodes4 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling4); + const actualNumberOfNodes8 = + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling8); + + const expectedNumberOfNodes1 = 1; + const expectedNumberOfNodes2 = 9; + const expectedNumberOfNodes4 = 585; + const expectedNumberOfNodes8 = 2396745; + + expect(actualNumberOfNodes1).toEqual(expectedNumberOfNodes1); + expect(actualNumberOfNodes2).toEqual(expectedNumberOfNodes2); + expect(actualNumberOfNodes4).toEqual(expectedNumberOfNodes4); + expect(actualNumberOfNodes8).toEqual(expectedNumberOfNodes8); + }); + + it("throws an error for non-positive subtree levels in computeNumberOfNodesPerSubtree", function () { + const implicitTiling = createQuadtreeImplicitTiling(0); + expect(function () { + ImplicitTilings.computeNumberOfNodesPerSubtree(implicitTiling); + }).toThrowError(); + }); + + it("computes the right number of nodes for QUADTREE with computeNumberOfNodesInLevel", function () { + const implicitTiling = createQuadtreeImplicitTiling(8); + + const actualNumberOfNodes0 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 0 + ); + const actualNumberOfNodes1 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 1 + ); + const actualNumberOfNodes2 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 2 + ); + const actualNumberOfNodes4 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 4 + ); + + const expectedNumberOfNodes0 = 1; + const expectedNumberOfNodes1 = 4; + const expectedNumberOfNodes2 = 16; + const expectedNumberOfNodes4 = 256; + + expect(actualNumberOfNodes0).toEqual(expectedNumberOfNodes0); + expect(actualNumberOfNodes1).toEqual(expectedNumberOfNodes1); + expect(actualNumberOfNodes2).toEqual(expectedNumberOfNodes2); + expect(actualNumberOfNodes4).toEqual(expectedNumberOfNodes4); + }); + it("computes the right number of nodes for OCTREE with computeNumberOfNodesInLevel", function () { + const implicitTiling = createOctreeImplicitTiling(8); + + const actualNumberOfNodes0 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 0 + ); + const actualNumberOfNodes1 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 1 + ); + const actualNumberOfNodes2 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 2 + ); + const actualNumberOfNodes4 = ImplicitTilings.computeNumberOfNodesInLevel( + implicitTiling, + 4 + ); + + const expectedNumberOfNodes0 = 1; + const expectedNumberOfNodes1 = 8; + const expectedNumberOfNodes2 = 64; + const expectedNumberOfNodes4 = 4096; + + expect(actualNumberOfNodes0).toEqual(expectedNumberOfNodes0); + expect(actualNumberOfNodes1).toEqual(expectedNumberOfNodes1); + expect(actualNumberOfNodes2).toEqual(expectedNumberOfNodes2); + expect(actualNumberOfNodes4).toEqual(expectedNumberOfNodes4); + }); + + it("throws an error for negative level in computeNumberOfNodesInLevel", function () { + const implicitTiling = createQuadtreeImplicitTiling(3); + expect(function () { + ImplicitTilings.computeNumberOfNodesInLevel(implicitTiling, -1); + }).toThrowError(); + }); + + it("computes the right coordinates for QUADTREE in globalizeCoordinates", function () { + const implicitTiling = createQuadtreeImplicitTiling(3); + const actualCoordinates = ImplicitTilings.globalizeCoordinates( + implicitTiling, + new QuadtreeCoordinates(2, 1, 0), + new QuadtreeCoordinates(1, 2, 1) + ); + + const expectedCoordinates = new QuadtreeCoordinates(3, 4, 1); + expect(actualCoordinates.toArray()).toEqual(expectedCoordinates.toArray()); + }); + + it("computes the right coordinates for QUADTREE in globalizeCoordinates", function () { + const implicitTiling = createOctreeImplicitTiling(3); + const actualCoordinates = ImplicitTilings.globalizeCoordinates( + implicitTiling, + new OctreeCoordinates(2, 1, 0, 1), + new OctreeCoordinates(1, 2, 1, 2) + ); + + const expectedCoordinates = new OctreeCoordinates(3, 4, 1, 4); + expect(actualCoordinates.toArray()).toEqual(expectedCoordinates.toArray()); + }); +}); diff --git a/specs/SpecHelpers.ts b/specs/SpecHelpers.ts index 3b7dde70..bff97517 100644 --- a/specs/SpecHelpers.ts +++ b/specs/SpecHelpers.ts @@ -223,4 +223,47 @@ export class SpecHelpers { } } } + + /** + * Compares two arrays lexicographically. + * + * When the arrays have different lengths, then the shorter + * one will be "padded" with elements that are smaller than + * all other elements in the other array. + * + * @param a - The first array + * @param b - The second array + * @returns The result of the comparison + */ + private static compareLexicographically(a: number[], b: number[]) { + const n = Math.min(a.length, b.length); + for (let i = 0; i < n; i++) { + const d = a[i] - b[i]; + if (d !== 0) { + return d; + } + } + if (a.length < b.length) { + return -1; + } + if (a.length > b.length) { + return 1; + } + return 0; + } + + /** + * Sorts a 2D array lexicographically, in place. + * + * When two elements have different lengths, then the shorter + * one will be "padded" with elements that are smaller than + * all other elements in the other array. + * + * @param array - The array + * @returns The array + */ + static sortLexicographically(array: number[][]) { + array.sort(SpecHelpers.compareLexicographically); + return array; + } } diff --git a/src/implicitTiling/ImplicitTilings.ts b/src/implicitTiling/ImplicitTilings.ts index 6f3f7595..1dd52a56 100644 --- a/src/implicitTiling/ImplicitTilings.ts +++ b/src/implicitTiling/ImplicitTilings.ts @@ -38,14 +38,21 @@ export class ImplicitTilings { * @param implicitTiling - The `TileImplicitTiling` object * @returns The generator * @throws ImplicitTilingError if the given object does not - * have a valid `subdivisionScheme`. + * have a valid `subdivisionScheme`, or the number of subtree + * levels is not positive */ static createSubtreeCoordinatesIterator( implicitTiling: TileImplicitTiling ): IterableIterator { + const levels = implicitTiling.subtreeLevels; + if (levels < 1) { + throw new ImplicitTilingError( + `Invalid number of subtree levels: ${levels}` + ); + } const r = this.createRootCoordinates(implicitTiling); const depthFirst = false; - return r.descendants(implicitTiling.subtreeLevels - 1, depthFirst); + return r.descendants(levels - 1, depthFirst); } /** @@ -55,12 +62,18 @@ export class ImplicitTilings { * @param implicitTiling - The `TileImplicitTiling` object * @returns The number of nodes * @throws ImplicitTilingError if the given object does not - * have a valid `subdivisionScheme`. + * have a valid `subdivisionScheme`, or the number of subtree + * levels is not positive */ static computeNumberOfNodesPerSubtree( implicitTiling: TileImplicitTiling ): number { const levels = implicitTiling.subtreeLevels; + if (levels < 1) { + throw new ImplicitTilingError( + `Invalid number of subtree levels: ${levels}` + ); + } if (implicitTiling.subdivisionScheme === "QUADTREE") { return Quadtrees.computeNumberOfNodesForLevels(levels); } From 33624efbc0d22ee315df72108b1da5730c519aa5 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Apr 2023 18:31:49 +0200 Subject: [PATCH 59/60] Update API definition file --- etc/3d-tiles-tools.api.md | 56 +++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/etc/3d-tiles-tools.api.md b/etc/3d-tiles-tools.api.md index b0acc469..07f4d317 100644 --- a/etc/3d-tiles-tools.api.md +++ b/etc/3d-tiles-tools.api.md @@ -203,7 +203,8 @@ export class Buffers { static getBufferPadded(buffer: Buffer, byteOffset?: number): Buffer; static getJson(buffer: Buffer): any; static getJsonBufferPadded(json: any, byteOffset?: number): Buffer; - static getMagic(buffer: Buffer, byteOffset?: number): string; + static getMagicBytes(buffer: Buffer, byteOffset: number, byteLength: number): Buffer; + static getMagicString(buffer: Buffer, byteOffset?: number): string; static getUnicodeBOMDescription(buffer: Buffer): string | undefined; static gunzip(inputBuffer: Buffer): Buffer; static gzip(inputBuffer: Buffer): Buffer; @@ -292,7 +293,7 @@ export interface ContentData { exists(): Promise; get extension(): string; getData(): Promise; - getMagic(): Promise; + getMagic(): Promise; getParsedObject(): Promise; get uri(): string; } @@ -302,6 +303,7 @@ export interface ContentData { // @internal export class ContentDataTypeRegistry { static findContentDataType(contentData: ContentData): Promise; + static findType(uri: string, data: Buffer): Promise; } // Warning: (ae-internal-missing-underscore) The name "DefaultMetadataEntityModel" should be prefixed with an underscore because the declaration is marked as @internal @@ -353,22 +355,19 @@ export class ExplicitTraversedTile implements TraversedTile { constructor(tile: Tile, path: string, level: number, parent: TraversedTile | undefined, schema: Schema | undefined, resourceResolver: ResourceResolver); asFinalTile(): Tile; asRawTile(): Tile; + static createRoot(root: Tile, schema: Schema | undefined, resourceResolver: ResourceResolver): TraversedTile; getChildren(): Promise; - // (undocumented) getFinalContents(): Content[]; - // (undocumented) getImplicitTiling(): TileImplicitTiling | undefined; - // (undocumented) getMetadata(): MetadataEntity | undefined; getParent(): TraversedTile | undefined; getRawContents(): Content[]; - // (undocumented) + getResourceResolver(): ResourceResolver; getSubtreeUri(): string | undefined; + isImplicitTilesetRoot(): boolean; get level(): number; get path(): string; // (undocumented) - resolveUri(uri: string): string; - // (undocumented) toString: () => string; } @@ -387,7 +386,6 @@ export class FileResourceResolver implements ResourceResolver { derive(uri: string): ResourceResolver; resolveData(uri: string): Promise; resolveDataPartial(uri: string, maxBytes: number): Promise; - resolveUri(uri: string): string; } // Warning: (ae-internal-missing-underscore) The name "Group" should be prefixed with an underscore because the declaration is marked as @internal @@ -426,23 +424,17 @@ export class ImplicitTraversedTile implements TraversedTile { asFinalTile(): Tile; asRawTile(): Tile; getChildren(): Promise; - // (undocumented) getFinalContents(): Content[]; getGlobalCoordinate(): TreeCoordinates; - // (undocumented) - getImplicitTiling(): TileImplicitTiling | undefined; getLocalCoordinate(): TreeCoordinates; - // (undocumented) - getMetadata(): MetadataEntity | undefined; getParent(): TraversedTile | undefined; getRawContents(): Content[]; - // (undocumented) + getResourceResolver(): ResourceResolver; getSubtreeUri(): string | undefined; + isImplicitTilesetRoot(): boolean; get level(): number; get path(): string; // (undocumented) - resolveUri(uri: string): string; - // (undocumented) toString: () => string; } @@ -480,7 +472,7 @@ export class Iterables { next(): IteratorResult; }; static map(iterable: IterableIterator, mapper: (element: S) => T): IterableIterator; - static overFiles(directory: string | PathLike, recurse?: boolean): IterableIterator; + static overFiles(directory: string | PathLike, recurse: boolean): IterableIterator; } // Warning: (ae-internal-missing-underscore) The name "LazyContentData" should be prefixed with an underscore because the declaration is marked as @internal @@ -491,7 +483,7 @@ export class LazyContentData implements ContentData { exists(): Promise; get extension(): string; getData(): Promise; - getMagic(): Promise; + getMagic(): Promise; getParsedObject(): Promise; get uri(): string; } @@ -761,7 +753,6 @@ export interface ResourceResolver { derive(uri: string): ResourceResolver; resolveData(uri: string): Promise; resolveDataPartial(uri: string, maxBytes: number): Promise; - resolveUri(uri: string): string; } // Warning: (ae-internal-missing-underscore) The name "ResourceResolvers" should be prefixed with an underscore because the declaration is marked as @internal @@ -1082,11 +1073,10 @@ export class TilesetSourceFs implements TilesetSource { // // @internal export class TilesetSourceResourceResolver implements ResourceResolver { - constructor(basePath: string, tilesetSourceFileName: string, tilesetSource: TilesetSource); + constructor(basePath: string, tilesetSource: TilesetSource); derive(uri: string): ResourceResolver; resolveData(uri: string): Promise; resolveDataPartial(uri: string, maxBytes: number): Promise; - resolveUri(uri: string): string; } // Warning: (ae-internal-missing-underscore) The name "TilesetSources" should be prefixed with an underscore because the declaration is marked as @internal @@ -1150,7 +1140,10 @@ export class TilesetTargets { // // @internal export class TilesetTraverser { - static traverse(tileset: Tileset, schema: Schema | undefined, resourceResolver: ResourceResolver, traversalCallback: TraversalCallback, depthFirst: boolean): Promise; + constructor(baseUri: string, resourceResolver: ResourceResolver, options?: TraversalOptions); + traverse(tileset: Tileset, traversalCallback: TraversalCallback): Promise; + traverseWithSchema(tileset: Tileset, schema: Schema | undefined, traversalCallback: TraversalCallback): Promise; + traverseWithSchemaAt(tile: Tile, schema: Schema | undefined, traversalCallback: TraversalCallback): Promise; } // Warning: (ae-internal-missing-underscore) The name "TraversalCallback" should be prefixed with an underscore because the declaration is marked as @internal @@ -1160,6 +1153,12 @@ export interface TraversalCallback { (traversedTile: TraversedTile): Promise; } +// @public +export type TraversalOptions = { + depthFirst?: boolean; + traverseExternalTilesets?: boolean; +}; + // Warning: (ae-internal-missing-underscore) The name "TraversedTile" should be prefixed with an underscore because the declaration is marked as @internal // // @internal @@ -1167,20 +1166,14 @@ export interface TraversedTile { asFinalTile(): Tile; asRawTile(): Tile; getChildren(): Promise; - // (undocumented) getFinalContents(): Content[]; - // (undocumented) - getImplicitTiling(): TileImplicitTiling | undefined; - // (undocumented) - getMetadata(): MetadataEntity | undefined; getParent(): TraversedTile | undefined; getRawContents(): Content[]; - // (undocumented) + getResourceResolver(): ResourceResolver; getSubtreeUri(): string | undefined; + isImplicitTilesetRoot(): boolean; get level(): number; get path(): string; - // (undocumented) - resolveUri(uri: string): string; } // Warning: (ae-internal-missing-underscore) The name "TreeCoordinates" should be prefixed with an underscore because the declaration is marked as @internal @@ -1204,7 +1197,6 @@ export class UnzippingResourceResolver implements ResourceResolver { derive(uri: string): ResourceResolver; resolveData(uri: string): Promise; resolveDataPartial(uri: string, maxBytes: number): Promise; - resolveUri(uri: string): string; } // Warning: (ae-internal-missing-underscore) The name "Uris" should be prefixed with an underscore because the declaration is marked as @internal From 384fe41f92078d16481574411fa7d1a3d2605398 Mon Sep 17 00:00:00 2001 From: Marco Hutter Date: Tue, 11 Apr 2023 18:31:58 +0200 Subject: [PATCH 60/60] Add example pipelines --- specs/data/pipelines/examplePipeline.json | 62 +++++++++++++++++++++++ specs/data/pipelines/simplePipeline.json | 16 ++++++ 2 files changed, 78 insertions(+) create mode 100644 specs/data/pipelines/examplePipeline.json create mode 100644 specs/data/pipelines/simplePipeline.json diff --git a/specs/data/pipelines/examplePipeline.json b/specs/data/pipelines/examplePipeline.json new file mode 100644 index 00000000..2418f382 --- /dev/null +++ b/specs/data/pipelines/examplePipeline.json @@ -0,0 +1,62 @@ +{ + "input": "./specs/data/TilesetOfTilesets", + "output": "./output/result", + "tilesetStages": [ + { + "name": "upgrade", + "description": "Upgrade the input tileset to the latest version" + }, + { + "name": "combine", + "description": "Combine all external tilesets into one" + }, + { + "name": "_b3dmToGlb", + "description": "Convert B3DM to GLB", + "contentStages": [ + { + "name": "b3dmToGlb", + "description": "Convert each B3DM content into GLB" + } + ] + }, + { + "name": "_optimizeGlb", + "description": "Optimize GLB", + "contentStages": [ + { + "name": "optimizeGlb", + "description": "Apply gltf-pipeline to each GLB content, with the given options", + "options": { + "dracoOptions": { + "compressionLevel": 10 + } + } + } + ] + }, + { + "name": "_separateGltf", + "description": "Separate glTF", + "contentStages": [ + { + "name": "separateGltf", + "description": "Convert each GLB content into a .gltf file with separate resources" + } + ] + }, + { + "name": "_gzip", + "description": "Gzip (glTF only)", + "contentStages": [ + { + "name": "gzip", + "description": "Compresses each entry with GZIP", + "includedContentTypes": [ + "CONTENT_TYPE_GLTF" + ] + } + ] + } + ] +} diff --git a/specs/data/pipelines/simplePipeline.json b/specs/data/pipelines/simplePipeline.json new file mode 100644 index 00000000..e8dbaf95 --- /dev/null +++ b/specs/data/pipelines/simplePipeline.json @@ -0,0 +1,16 @@ +{ + "input": "./specs/data/TilesetOfTilesetsWithUris", + "output": "./output/TilesetOfTilesetsWithUris.3tz", + "tilesetStages": [ + { + "name": "B3DM to GLB", + "description": "Convert B3DM to GLB", + "contentStages": [ + { + "name": "b3dmToGlb", + "description": "Convert each B3DM content into GLB" + } + ] + } + ] +} \ No newline at end of file