diff --git a/demos/validationOptionsDemo.ts b/demos/validationOptionsDemo.ts index 06081420..b864abdf 100644 --- a/demos/validationOptionsDemo.ts +++ b/demos/validationOptionsDemo.ts @@ -1,3 +1,4 @@ +import { ContentDataTypes } from "3d-tiles-tools"; import { Validators, ValidationOptions, @@ -10,7 +11,7 @@ async function runWithIncluded() { const options = ValidationOptions.fromJson({ validateContentData: true, // The default includeContentTypes: [ - "CONTENT_TYPE_B3DM", // Explicitly included here + ContentDataTypes.CONTENT_TYPE_B3DM, // Explicitly included here ], excludeContentTypes: undefined, // The default }); @@ -30,7 +31,7 @@ async function runWithoutIncluded() { const options = ValidationOptions.fromJson({ validateContentData: true, // The default includeContentTypes: [ - //"CONTENT_TYPE_B3DM", // Not included here! + //ContentDataTypes.CONTENT_TYPE_B3DM, // Not included here! ], excludeContentTypes: undefined, // The default }); @@ -51,7 +52,7 @@ async function runWithExcluded() { validateContentData: true, // The default includeContentTypes: undefined, // The default excludeContentTypes: [ - "CONTENT_TYPE_B3DM" // Explicitly excluded here + ContentDataTypes.CONTENT_TYPE_B3DM // Explicitly excluded here ] }); const tilesetFile = "specs/data/tilesets/validTilesetWithInvalidB3dm.json"; @@ -71,7 +72,7 @@ async function runWithoutExcluded() { validateContentData: true, // The default includeContentTypes: undefined, // The default excludeContentTypes: [ - // "CONTENT_TYPE_B3DM" // NOT Excluded here + // ContentDataTypes.CONTENT_TYPE_B3DM // NOT Excluded here ] }); const tilesetFile = "specs/data/tilesets/validTilesetWithInvalidB3dm.json"; diff --git a/specs/TilesetValidationSpec.ts b/specs/TilesetValidationSpec.ts index dae93d93..0f0a32d4 100644 --- a/specs/TilesetValidationSpec.ts +++ b/specs/TilesetValidationSpec.ts @@ -936,7 +936,30 @@ describe("Tileset validation", function () { expect(result.get(0).type).toEqual("CONTENT_VALIDATION_INFO"); }); - it("detects no issues in validTilesetWithInvalidI3dm", async function () { + it("detects no issues in validTilesetWithValid3tz", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/validTilesetWithValid3tz.json" + ); + expect(result.length).toEqual(0); + }); + + it("detects issues in validTilesetWith3tzWithError", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/validTilesetWith3tzWithError.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("CONTENT_VALIDATION_ERROR"); + }); + + it("detects issues in validTilesetWithInvalid3tz", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/tilesets/validTilesetWithInvalid3tz.json" + ); + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("CONTENT_VALIDATION_ERROR"); + }); + + it("detects issues in validTilesetWithInvalidI3dm", async function () { const result = await Validators.validateTilesetFile( "specs/data/tilesets/validTilesetWithInvalidI3dm.json" ); @@ -944,7 +967,7 @@ describe("Tileset validation", function () { expect(result.get(0).type).toEqual("CONTENT_VALIDATION_ERROR"); }); - it("detects no issues in validTilesetWithInvalidPnts", async function () { + it("detects issues in validTilesetWithInvalidPnts", async function () { const result = await Validators.validateTilesetFile( "specs/data/tilesets/validTilesetWithInvalidPnts.json" ); diff --git a/specs/data/extensions/maxarContentGeojson/lineString.geojson b/specs/data/extensions/maxarContentGeojson/lineString.geojson new file mode 100644 index 00000000..ac7af70d --- /dev/null +++ b/specs/data/extensions/maxarContentGeojson/lineString.geojson @@ -0,0 +1,37 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "name": "UL", + "code": 12 + }, + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -75.61209430782448, + 40.042530611425896 + ], + [ + -75.61219430782448, + 40.042530611425896 + ], + [ + -75.61219430782448, + 40.042630611425896 + ], + [ + -75.61209430782448, + 40.042630611425896 + ], + [ + -75.61209430782448, + 40.042530611425896 + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/specs/data/extensions/maxarContentGeojson/validTilesetWithGeojson.json b/specs/data/extensions/maxarContentGeojson/validTilesetWithGeojson.json new file mode 100644 index 00000000..78ed5c10 --- /dev/null +++ b/specs/data/extensions/maxarContentGeojson/validTilesetWithGeojson.json @@ -0,0 +1,16 @@ +{ + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "content": { + "uri": "lineString.geojson" + }, + "refine": "REPLACE", + "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 + } +} \ No newline at end of file diff --git a/specs/data/extensions/maxarContentGeojson/validTilesetWithMaxarContentGeojson.json b/specs/data/extensions/maxarContentGeojson/validTilesetWithMaxarContentGeojson.json new file mode 100644 index 00000000..363bba0f --- /dev/null +++ b/specs/data/extensions/maxarContentGeojson/validTilesetWithMaxarContentGeojson.json @@ -0,0 +1,66 @@ +{ + "extras" : { + "info": { + "note": "The MAXAR_content_geojson example has been taken from the specification README.md" + } + }, + "extensionsUsed": [ + "MAXAR_content_geojson" + ], + "extensionsRequired": [ + "MAXAR_content_geojson" + ], + "asset" : { + "version" : "1.1" + }, + "schema": { + "id": "EXAMPLE_SCHEMA_ID", + "classes": { + "tileset": { + "properties": { + "content_type": { + "type": "STRING" + }, + "geometry_model": { + "type": "STRING" + }, + "name": { + "type": "STRING" + }, + "schema": { + "type": "STRING" + }, + "wff_version": { + "type": "STRING" + } + } + } + } + }, + "metadata": { + "class": "tileset", + "properties": { + "content_type": "VECTOR", + "geometry_model": "OBJECTS", + "name": "Vegetation Layer", + "schema": "wff/15", + "wff_version": "1.5" + }, + "extensions": { + "MAXAR_content_geojson": { + "propertiesSchemaUri": "vegetation_schema.json" + } + } + }, + "geometricError" : 2.0, + "root" : { + "content": { + "uri": "lineString.geojson" + }, + "refine": "REPLACE", + "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 + } +} \ No newline at end of file diff --git a/specs/data/extensions/maxarGrid/validTilesetWithMaxarGrid.json b/specs/data/extensions/maxarGrid/validTilesetWithMaxarGrid.json new file mode 100644 index 00000000..17dca256 --- /dev/null +++ b/specs/data/extensions/maxarGrid/validTilesetWithMaxarGrid.json @@ -0,0 +1,51 @@ +{ + "extras" : { + "info": { + "note": "The MAXAR_grid example has been taken from the specification README.md" + } + }, + "extensionsUsed": [ + "MAXAR_grid" + ], + "extensions": { + "MAXAR_grid": { + "type": "quad", + "center": [3097202.3706942615, 500000.00000000122], + "size": [2088960.0, 2088960.0], + "srs": { + "referenceSystem": "ITRF2008", + "epoch": "2005.0", + "coordinateSystem": "UTM14N", + "elevation": "ELLIPSOID" + } + } + }, + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "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, + "extensions": { + "MAXAR_grid": { + "boundingBox": [ + 3425906.396465421, + 500000.0000000012, + 3546738.395635767, + 694559.9877318252, + -581.1745009114966, + 445.7444586344063 + ], + "index": [ + 5, + 4 + ], + "level": 3 + } + } + } +} \ No newline at end of file diff --git a/specs/data/extensions/maxarGrid/validTilesetWithVriconGrid.json b/specs/data/extensions/maxarGrid/validTilesetWithVriconGrid.json new file mode 100644 index 00000000..0d01a443 --- /dev/null +++ b/specs/data/extensions/maxarGrid/validTilesetWithVriconGrid.json @@ -0,0 +1,51 @@ +{ + "extras" : { + "info": { + "note": "The VRICON_grid is a legacy name for MAXAR_grid. The example here has been taken from the MAXAR_grid specification README.md" + } + }, + "extensionsUsed": [ + "VRICON_grid" + ], + "extensions": { + "VRICON_grid": { + "type": "quad", + "center": [3097202.3706942615, 500000.00000000122], + "size": [2088960.0, 2088960.0], + "srs": { + "referenceSystem": "ITRF2008", + "epoch": "2005.0", + "coordinateSystem": "UTM14N", + "elevation": "ELLIPSOID" + } + } + }, + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "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, + "extensions": { + "VRICON_grid": { + "boundingBox": [ + 3425906.396465421, + 500000.0000000012, + 3546738.395635767, + 694559.9877318252, + -581.1745009114966, + 445.7444586344063 + ], + "index": [ + 5, + 4 + ], + "level": 3 + } + } + } +} \ No newline at end of file diff --git a/specs/data/extensions/vriconClass/validTilesetWithVriconClass.json b/specs/data/extensions/vriconClass/validTilesetWithVriconClass.json new file mode 100644 index 00000000..1f1956a9 --- /dev/null +++ b/specs/data/extensions/vriconClass/validTilesetWithVriconClass.json @@ -0,0 +1,89 @@ +{ + "extras" : { + "info": { + "note": "The VRICON_class example has been taken from the specification README.md" + } + }, + "extensionsUsed": [ + "VRICON_class" + ], + "extensions": { + "VRICON_class": { + "geometry": [ + { + "name": "dsm", + "class": 1 + } + ], + "texture": [ + { + "name": "r3dm::cdm_difference", + "class": 18 + }, + { + "name": "r3dm::date_avg", + "class": 98 + }, + { + "name": "r3dm::date_first", + "class": 96 + }, + { + "name": "r3dm::date_last", + "class": 97 + }, + { + "name": "r3dm::image_count", + "class": 32 + }, + { + "name": "r3dm::ndvi", + "class": 2314 + }, + { + "name": "r3dm::ndvi_cnt", + "class": 2317 + }, + { + "name": "r3dm::ndvi_max", + "class": 2315 + }, + { + "name": "r3dm::ndvi_min", + "class": 2316 + }, + { + "name": "r3dm::security", + "class": 48 + }, + { + "name": "r3dm::uncertainty_ce90", + "class": 49 + }, + { + "name": "r3dm::uncertainty_le90", + "class": 50 + }, + { + "name": "r3dm::universal_mix", + "class": 86 + }, + { + "name": "texture", + "class": 1 + } + ] + } + }, + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "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 + } +} \ No newline at end of file diff --git a/specs/data/tilesets/tiles/3tz/invalid.3tz b/specs/data/tilesets/tiles/3tz/invalid.3tz new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/specs/data/tilesets/tiles/3tz/invalid.3tz @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/specs/data/tilesets/tiles/3tz/simple.3tz b/specs/data/tilesets/tiles/3tz/simple.3tz index 924bc59f..dd5b48bb 100644 Binary files a/specs/data/tilesets/tiles/3tz/simple.3tz and b/specs/data/tilesets/tiles/3tz/simple.3tz differ diff --git a/specs/data/tilesets/tiles/3tz/withError.3tz b/specs/data/tilesets/tiles/3tz/withError.3tz new file mode 100644 index 00000000..924bc59f Binary files /dev/null and b/specs/data/tilesets/tiles/3tz/withError.3tz differ diff --git a/specs/data/tilesets/validTilesetWith3tzWithError.json b/specs/data/tilesets/validTilesetWith3tzWithError.json new file mode 100644 index 00000000..448c921b --- /dev/null +++ b/specs/data/tilesets/validTilesetWith3tzWithError.json @@ -0,0 +1,22 @@ +{ + "extensionsUsed": [ + "MAXAR_content_3tz" + ], + "extensionsRequired" : [ + "MAXAR_content_3tz" + ], + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "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": "tiles/3tz/withError.3tz" + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/validTilesetWithInvalid3tz.json b/specs/data/tilesets/validTilesetWithInvalid3tz.json new file mode 100644 index 00000000..687f1e30 --- /dev/null +++ b/specs/data/tilesets/validTilesetWithInvalid3tz.json @@ -0,0 +1,22 @@ +{ + "extensionsUsed": [ + "MAXAR_content_3tz" + ], + "extensionsRequired" : [ + "MAXAR_content_3tz" + ], + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "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": "tiles/3tz/invalid.3tz" + } + } +} \ No newline at end of file diff --git a/specs/data/tilesets/validTilesetWithValid3tz.json b/specs/data/tilesets/validTilesetWithValid3tz.json new file mode 100644 index 00000000..348fac8e --- /dev/null +++ b/specs/data/tilesets/validTilesetWithValid3tz.json @@ -0,0 +1,22 @@ +{ + "extensionsUsed": [ + "MAXAR_content_3tz" + ], + "extensionsRequired" : [ + "MAXAR_content_3tz" + ], + "asset" : { + "version" : "1.1" + }, + "geometricError" : 2.0, + "root" : { + "refine": "REPLACE", + "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": "tiles/3tz/simple.3tz" + } + } +} \ No newline at end of file diff --git a/specs/extensions/MaxarContentGeojonValidationSpec.ts b/specs/extensions/MaxarContentGeojonValidationSpec.ts new file mode 100644 index 00000000..57ff9eb1 --- /dev/null +++ b/specs/extensions/MaxarContentGeojonValidationSpec.ts @@ -0,0 +1,24 @@ +import { Validators } from "../../src/validation/Validators"; + +describe("Tileset MAXAR_content_geojson extension validation", function () { + it("detects issues in validTilesetWithGeojson", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/extensions/maxarContentGeojson/validTilesetWithGeojson.json" + ); + // Expect one warning for skipping the GeoJSON validation + // and one for the missing declaration of the + // MAXAR_content_geojson usage in the extensionsUsed + expect(result.length).toEqual(2); + expect(result.get(0).type).toEqual("CONTENT_VALIDATION_WARNING"); + expect(result.get(1).type).toEqual("EXTENSION_FOUND_BUT_NOT_USED"); + }); + + it("detects issues in validTilesetWithMaxarContentGeojson", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/extensions/maxarContentGeojson/validTilesetWithMaxarContentGeojson.json" + ); + // Expect one warning for skipping the GeoJSON validation + expect(result.length).toEqual(1); + expect(result.get(0).type).toEqual("CONTENT_VALIDATION_WARNING"); + }); +}); diff --git a/specs/extensions/MaxarGridValidationSpec.ts b/specs/extensions/MaxarGridValidationSpec.ts new file mode 100644 index 00000000..caa40587 --- /dev/null +++ b/specs/extensions/MaxarGridValidationSpec.ts @@ -0,0 +1,18 @@ +import { Validators } from "../../src/validation/Validators"; + +describe("Tileset MAXAR_grid extension validation", function () { + it("detects no issues in validTilesetWithMaxarGrid", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/extensions/maxarGrid/validTilesetWithMaxarGrid.json" + ); + expect(result.length).toEqual(0); + }); + + // Note: VRICON_grid is just a legacy name of MAXAR_grid + it("detects no issues in validTilesetWithVriconGrid", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/extensions/maxarGrid/validTilesetWithVriconGrid.json" + ); + expect(result.length).toEqual(0); + }); +}); diff --git a/specs/extensions/VriconClassValidationSpec.ts b/specs/extensions/VriconClassValidationSpec.ts new file mode 100644 index 00000000..a88e0eea --- /dev/null +++ b/specs/extensions/VriconClassValidationSpec.ts @@ -0,0 +1,10 @@ +import { Validators } from "../../src/validation/Validators"; + +describe("Tileset VRICON_class extension validation", function () { + it("detects no issues in validTilesetWithVriconClass", async function () { + const result = await Validators.validateTilesetFile( + "specs/data/extensions/vriconClass/validTilesetWithVriconClass.json" + ); + expect(result.length).toEqual(0); + }); +}); diff --git a/src/validation/ContentDataValidator.ts b/src/validation/ContentDataValidator.ts index 73ecf9fe..20f74d44 100644 --- a/src/validation/ContentDataValidator.ts +++ b/src/validation/ContentDataValidator.ts @@ -1,22 +1,23 @@ import paths from "path"; -import { defined } from "3d-tiles-tools"; - import { Uris } from "3d-tiles-tools"; - -import { ValidationContext } from "./ValidationContext"; -import { ContentDataTypeRegistry } from "3d-tiles-tools"; +import { defined } from "3d-tiles-tools"; import { ContentData } from "3d-tiles-tools"; +import { ContentDataTypes } from "3d-tiles-tools"; +import { ContentDataTypeRegistry } from "3d-tiles-tools"; import { LazyContentData } from "3d-tiles-tools"; -import { ContentDataValidators } from "./ContentDataValidators"; - import { Content } from "3d-tiles-tools"; +import { ValidationContext } from "./ValidationContext"; +import { ContentDataValidators } from "./ContentDataValidators"; + import { IoValidationIssues } from "../issues/IoValidationIssue"; import { ContentValidationIssues } from "../issues/ContentValidationIssues"; import { ValidationOptionChecks } from "./ValidationOptionChecks"; import { ValidationIssueFilters } from "./ValidationIssueFilters"; import { ValidationIssueSeverity } from "./ValidationIssueSeverity"; +import { ValidationResult } from "./ValidationResult"; +import { Validator } from "./Validator"; /** * A class for validation of the data that is pointed to by a `content.uri`. @@ -137,28 +138,64 @@ export class ContentDataValidator { const contentDataType = await ContentDataTypeRegistry.findContentDataType( contentData ); - const isTileset = contentDataType === "CONTENT_TYPE_TILESET"; + const isTileset = contentDataType === ContentDataTypes.CONTENT_TYPE_TILESET; + const is3tz = contentDataType === ContentDataTypes.CONTENT_TYPE_3TZ; - // If the content is an external tileset, then add its - // resolved URI to the context as an "activeTilesetUri", - // to detect cycles - const resolvedContentUri = context.resolveUri(contentUri); if (isTileset) { - if (context.isActiveTilesetUri(resolvedContentUri)) { - const message = `External tileset content ${contentUri} creates a cycle`; - const issue = ContentValidationIssues.CONTENT_VALIDATION_ERROR( - contentPath, - message - ); - context.addIssue(issue); - return false; - } - context.addActiveTilesetUri(resolvedContentUri); + const result = await ContentDataValidator.validateExternalTilesetContent( + contentPath, + contentUri, + contentData, + dataValidator, + context + ); + return result; + } + const result = await ContentDataValidator.validateSimpleContent( + contentUri, + contentData, + dataValidator, + is3tz, + context + ); + return result; + } + + /** + * Implementation of `validateContentDataInternal` for the case that + * the content is an external tileset. + * + * @param contentPath - The path for the `ValidationIssue` instances. + * @param contentUri - The URI of the content + * @param context - The `ValidationContext` + * @param contentData - The content data + * @param dataValidator - The validator for the content data + * @returns A promise that resolves when the validation is finished + */ + private static async validateExternalTilesetContent( + contentPath: string, + contentUri: string, + contentData: ContentData, + dataValidator: Validator, + context: ValidationContext + ): Promise { + // Add the resolved URI of the external tileset to the context as + // an "activeTilesetUri", to detect cycles + const resolvedTilesetContentUri = context.resolveUri(contentUri); + if (context.isActiveTilesetUri(resolvedTilesetContentUri)) { + const message = `External tileset content ${contentUri} creates a cycle`; + const issue = ContentValidationIssues.CONTENT_VALIDATION_ERROR( + contentPath, + message + ); + context.addIssue(issue); + return false; } + context.addActiveTilesetUri(resolvedTilesetContentUri); // Create a new context to collect the issues that are found in - // the data. If there are issues, then they will be stored as - // the 'causes' of a single content validation issue. + // the external tileset. If there are issues, then they will be + // stored as the 'causes' of a single content validation issue. const dirName = paths.dirname(contentData.uri); const derivedContext = context.deriveFromUri(dirName); const result = await dataValidator.validateObject( @@ -168,59 +205,113 @@ export class ContentDataValidator { ); const derivedResult = derivedContext.getResult(); - // Add all extensions that have been found in the content - // to the current context. They also have to appear in - // the 'extensionsUsed' of the containing tileset. + // Add all extensions that have been found in the external + // tileset to the current context. They also have to appear + // in the 'extensionsUsed' of the containing tileset. const derivedExtensionsFound = derivedContext.getExtensionsFound(); for (const derivedExtensionFound of derivedExtensionsFound) { context.addExtensionFound(derivedExtensionFound); } - if (isTileset) { - const issue = ContentValidationIssues.createForExternalTileset( - contentUri, - derivedResult - ); - if (issue) { - context.addIssue(issue); - } - } else { - const includedSeverities: ValidationIssueSeverity[] = []; - if ( - options.contentValidationIssueSeverity == ValidationIssueSeverity.ERROR - ) { - includedSeverities.push(ValidationIssueSeverity.ERROR); - } else if ( - options.contentValidationIssueSeverity == - ValidationIssueSeverity.WARNING - ) { - includedSeverities.push(ValidationIssueSeverity.WARNING); - includedSeverities.push(ValidationIssueSeverity.ERROR); - } else { - includedSeverities.push(ValidationIssueSeverity.INFO); - includedSeverities.push(ValidationIssueSeverity.WARNING); - includedSeverities.push(ValidationIssueSeverity.ERROR); - } - const filter = ValidationIssueFilters.byIncludedSeverities( - ...includedSeverities - ); - const filteredDerivedResult = derivedResult.filter(filter); - const issue = ContentValidationIssues.createForContent( - contentUri, - filteredDerivedResult - ); - if (issue) { - context.addIssue(issue); - } + const issue = ContentValidationIssues.createForExternalTileset( + contentUri, + derivedResult + ); + if (issue) { + context.addIssue(issue); } - if (isTileset) { - context.removeActiveTilesetUri(resolvedContentUri); - } + context.removeActiveTilesetUri(resolvedTilesetContentUri); return result; } + /** + * Implementation of `validateContentDataInternal` for the case that + * the content is NOT an external tileset. + * + * @param contentUri - The URI of the content + * @param context - The `ValidationContext` + * @param contentData - The content data + * @param dataValidator - The validator for the content data + * @param is3tz - Whether the content is a 3TZ package + * @returns A promise that resolves when the validation is finished + */ + private static async validateSimpleContent( + contentUri: string, + contentData: ContentData, + dataValidator: Validator, + is3tz: boolean, + context: ValidationContext + ): Promise { + // Special handling for 3TZ: + // + // The context usually refers to the directory that the content + // is contained in (for example, a URI like `../images/image.png` + // that is used in a glTF file like `example/directory/file.gltf` + // has to be resolved to `example/images/image.png`). + // + // But for 3TZ, it has to determine the absolute (!) path from + // the content URI to even be able to open the 3TZ (because 3TZ + // can only be a file in the local file system), and there are no + // resources to be resolved FROM the 3TZ that are NOT part of + // the 3TZ. + let dirName = "."; + if (!is3tz) { + dirName = paths.dirname(contentData.uri); + } + const derivedContext = context.deriveFromUri(dirName); + const result = await dataValidator.validateObject( + contentUri, + contentData, + derivedContext + ); + const derivedResult = derivedContext.getResult(); + const options = context.getOptions(); + const filteredDerivedResult = ContentDataValidator.filterResult( + derivedResult, + options.contentValidationIssueSeverity + ); + const issue = ContentValidationIssues.createForContent( + contentUri, + filteredDerivedResult + ); + if (issue) { + context.addIssue(issue); + } + return result; + } + + /** + * Filter the given result, and return a new result that only contains + * validation issues that are at least as severe as the given severity. + * + * @param result - The validation result + * @param severity - The highest validation issue severity that should be included + * @returns The filtered result + */ + private static filterResult( + result: ValidationResult, + severity: ValidationIssueSeverity + ) { + const includedSeverities: ValidationIssueSeverity[] = []; + if (severity == ValidationIssueSeverity.ERROR) { + includedSeverities.push(ValidationIssueSeverity.ERROR); + } else if (severity == ValidationIssueSeverity.WARNING) { + includedSeverities.push(ValidationIssueSeverity.WARNING); + includedSeverities.push(ValidationIssueSeverity.ERROR); + } else { + includedSeverities.push(ValidationIssueSeverity.INFO); + includedSeverities.push(ValidationIssueSeverity.WARNING); + includedSeverities.push(ValidationIssueSeverity.ERROR); + } + const filter = ValidationIssueFilters.byIncludedSeverities( + ...includedSeverities + ); + const filteredResult = result.filter(filter); + return filteredResult; + } + /** * Track the extensions that are used, and which only refer to * allowing certain content data types. @@ -236,21 +327,17 @@ export class ContentDataValidator { contentData: ContentData, context: ValidationContext ) { - const magic = await contentData.getMagic(); - const isGlb = magic.toString("ascii") === "glTF"; - if (isGlb) { - context.addExtensionFound("3DTILES_content_gltf"); - } const contentDataType = await ContentDataTypeRegistry.findContentDataType( contentData ); - const isGltf = contentDataType === "CONTENT_TYPE_GLTF"; - if (isGltf) { + if (contentDataType === ContentDataTypes.CONTENT_TYPE_GLB) { context.addExtensionFound("3DTILES_content_gltf"); - } - const isProbably3tz = contentData.extension === ".3tz"; - if (isProbably3tz) { + } else if (contentDataType === ContentDataTypes.CONTENT_TYPE_GLTF) { + context.addExtensionFound("3DTILES_content_gltf"); + } else if (contentDataType === ContentDataTypes.CONTENT_TYPE_3TZ) { context.addExtensionFound("MAXAR_content_3tz"); + } else if (contentDataType === ContentDataTypes.CONTENT_TYPE_GEOJSON) { + context.addExtensionFound("MAXAR_content_geojson"); } } } diff --git a/src/validation/ContentDataValidators.ts b/src/validation/ContentDataValidators.ts index 453053ef..1330f575 100644 --- a/src/validation/ContentDataValidators.ts +++ b/src/validation/ContentDataValidators.ts @@ -1,9 +1,11 @@ -import { ContentDataTypeRegistry, defined } from "3d-tiles-tools"; +import { defined } from "3d-tiles-tools"; +import { ContentData } from "3d-tiles-tools"; +import { ContentDataTypes } from "3d-tiles-tools"; +import { ContentDataTypeRegistry } from "3d-tiles-tools"; import { Validators } from "./Validators"; import { Validator } from "./Validator"; import { ValidationContext } from "./ValidationContext"; -import { ContentData } from "3d-tiles-tools"; import { TilesetPackageValidator } from "./TilesetPackageValidator"; import { B3dmValidator } from "../tileFormats/B3dmValidator"; @@ -53,36 +55,28 @@ export class ContentDataValidators { return; } - // The keys that are used here are the strings that are - // returned by the `ContentDataTypeRegistry`, for a - // given `ContentData`. - // THESE STRINGS ARE NOT SPECIFIED. - // Using them here is relying on an implementation - // detail. Whether or not these strings should be - // public and/or specified has to be decided. - ContentDataValidators.registerForBuffer( - "CONTENT_TYPE_GLB", + ContentDataTypes.CONTENT_TYPE_GLB, new GltfValidator() ); ContentDataValidators.registerForBuffer( - "CONTENT_TYPE_B3DM", + ContentDataTypes.CONTENT_TYPE_B3DM, new B3dmValidator() ); ContentDataValidators.registerForBuffer( - "CONTENT_TYPE_I3DM", + ContentDataTypes.CONTENT_TYPE_I3DM, new I3dmValidator() ); ContentDataValidators.registerForBuffer( - "CONTENT_TYPE_CMPT", + ContentDataTypes.CONTENT_TYPE_CMPT, new CmptValidator() ); ContentDataValidators.registerForBuffer( - "CONTENT_TYPE_PNTS", + ContentDataTypes.CONTENT_TYPE_PNTS, new PntsValidator() ); @@ -107,21 +101,30 @@ export class ContentDataValidators { ); } - ContentDataValidators.register("CONTENT_TYPE_GEOM", geomValidator); - ContentDataValidators.register("CONTENT_TYPE_VCTR", vctrValidator); - ContentDataValidators.register("CONTENT_TYPE_GEOJSON", geojsonValidator); + ContentDataValidators.register( + ContentDataTypes.CONTENT_TYPE_GEOM, + geomValidator + ); + ContentDataValidators.register( + ContentDataTypes.CONTENT_TYPE_VCTR, + vctrValidator + ); + ContentDataValidators.register( + ContentDataTypes.CONTENT_TYPE_GEOJSON, + geojsonValidator + ); ContentDataValidators.register( - "CONTENT_TYPE_3TZ", + ContentDataTypes.CONTENT_TYPE_3TZ, ContentDataValidators.createPackageValidator() ); ContentDataValidators.register( - "CONTENT_TYPE_TILESET", + ContentDataTypes.CONTENT_TYPE_TILESET, ContentDataValidators.createTilesetValidator() ); ContentDataValidators.register( - "CONTENT_TYPE_GLTF", + ContentDataTypes.CONTENT_TYPE_GLTF, ContentDataValidators.createGltfJsonValidator() ); diff --git a/src/validation/TilesetPackageValidator.ts b/src/validation/TilesetPackageValidator.ts index cbdb4aa1..0c1d1fc0 100644 --- a/src/validation/TilesetPackageValidator.ts +++ b/src/validation/TilesetPackageValidator.ts @@ -49,7 +49,6 @@ export class TilesetPackageValidator implements Validator { resolvedUri: string, context: ValidationContext ): Promise { - //console.log("TilesetPackageValidator resolvedUri is " + resolvedUri); const isContent = true; const result = TilesetPackageValidator.validatePackageFileInternal( resolvedUri, @@ -97,6 +96,42 @@ export class TilesetPackageValidator implements Validator { uri: string, isContent: boolean, context: ValidationContext + ): Promise { + try { + const result = await TilesetPackageValidator.validatePackageFileUnchecked( + uri, + isContent, + context + ); + return result; + } catch (error) { + const message = `Failed to open ${uri}. Input file is invalid.`; + const issue = IoValidationIssues.IO_ERROR(uri, message); + context.addIssue(issue); + return false; + } + } + + /** + * Validates the tileset that is contained in the package that is + * pointed to by the given URI (assuming that it is a file in + * the local file system). + * + * @param uri - The full URI of the package file + * @param isContent - Whether the given package was found as a tile + * content. If this is the case, then the issues that are found + * in the package will be summarized in a `CONTENT_VALIDATION_` + * issue. Otherwise, they will be added directly to the given context. + * @param context - The `ValidationContext` + * @returns A promise that indicates whether the package contained + * a valid tileset. + * @throws Error if opening the tileset source causes an unhandled + * error. + */ + private static async validatePackageFileUnchecked( + uri: string, + isContent: boolean, + context: ValidationContext ): Promise { // Create the tileset source for the package from the given URI // (i.e. the full package file name). If the source cannot diff --git a/src/validation/TilesetTraversingValidator.ts b/src/validation/TilesetTraversingValidator.ts index 35db0e47..d6c60caa 100644 --- a/src/validation/TilesetTraversingValidator.ts +++ b/src/validation/TilesetTraversingValidator.ts @@ -99,7 +99,7 @@ export class TilesetTraversingValidator { ); } catch (error) { // There may be different kinds of errors that are thrown - // during the traveral of the tileset and its validation. + // during the traversal of the tileset and its validation. // An `ImplicitTilingError` indicates that an implicit // tileset was invalid (e.g. a missing subtree file or // one of its buffers). The `ImplicitTilingError` is diff --git a/src/validation/Validators.ts b/src/validation/Validators.ts index db2c1655..da087c85 100644 --- a/src/validation/Validators.ts +++ b/src/validation/Validators.ts @@ -478,23 +478,6 @@ export class Validators { ); } - // Register an empty validator for NGA_gpm - { - const emptyValidator = Validators.createEmptyValidator(); - const override = false; - ExtendedObjectsValidators.register("NGA_gpm", emptyValidator, override); - } - - // Register an empty validator for MAXAR_content_3tz - { - const emptyValidator = Validators.createEmptyValidator(); - const override = false; - ExtendedObjectsValidators.register( - "MAXAR_content_3tz", - emptyValidator, - override - ); - } // Register an empty validator for MAXAR_content_geojson { const emptyValidator = Validators.createEmptyValidator(); @@ -505,6 +488,7 @@ export class Validators { override ); } + // Register an empty validator for MAXAR_extent { const emptyValidator = Validators.createEmptyValidator(); @@ -515,6 +499,7 @@ export class Validators { override ); } + // Register an empty validator for MAXAR_grid { const emptyValidator = Validators.createEmptyValidator(); @@ -525,6 +510,7 @@ export class Validators { override ); } + // Register an empty validator for VRICON_class { const emptyValidator = Validators.createEmptyValidator(); @@ -535,6 +521,7 @@ export class Validators { override ); } + // Register an empty validator for VRICON_grid { const emptyValidator = Validators.createEmptyValidator();