diff --git a/src/issues/JsonValidationIssues.ts b/src/issues/JsonValidationIssues.ts index d5d9ffe6..c5966af9 100644 --- a/src/issues/JsonValidationIssues.ts +++ b/src/issues/JsonValidationIssues.ts @@ -235,6 +235,23 @@ export class JsonValidationIssues { return issue; } + /** + * Indicates that the the value of a string property does not meet + * the necessary requirements. This refers to constraints about + * the structure of the string value, e.g. when it has to be + * a valid ISO8601 string. + * + * @param path - The path for the `ValidationIssue` + * @param message - The message for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static STRING_VALUE_INVALID(path: string, message: string) { + const type = "STRING_VALUE_INVALID"; + const severity = ValidationIssueSeverity.ERROR; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } + /** * Indicates that multiple properties have been defined, when * only one of them should have been defined. diff --git a/src/validation/BasicValidator.ts b/src/validation/BasicValidator.ts index 4a196ff1..f49fe9f1 100644 --- a/src/validation/BasicValidator.ts +++ b/src/validation/BasicValidator.ts @@ -20,39 +20,6 @@ import { ValidationIssueUtils } from "../issues/ValidationIssueUtils"; * @internal */ export class BasicValidator { - /** - * Validate that the given string is a valid identifier string, - * as defined in the 3D Metadata Specification. - * - * @param path - The path for the `ValidationIssue` message - * @param name - The name for the `ValidationIssue` message - * @param value - The value - * @param context - The `ValidationContext` to add the issue to - * @returns Whether the given value is an identifier string - */ - static validateIdentifierString( - path: string, - name: string, - value: string, - context: ValidationContext - ): boolean { - if (!BasicValidator.validateDefined(path, name, value, context)) { - return false; - } - const idRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; - if (!idRegex.test(value)) { - const issue = JsonValidationIssues.STRING_PATTERN_MISMATCH( - path, - name, - value, - idRegex.toString() - ); - context.addIssue(issue); - return false; - } - return true; - } - /** * Validate that the specified value has the type `"string"`, * if it is present. diff --git a/src/validation/StringValidator.ts b/src/validation/StringValidator.ts new file mode 100644 index 00000000..4cee02eb --- /dev/null +++ b/src/validation/StringValidator.ts @@ -0,0 +1,184 @@ +import { ValidationContext } from "./ValidationContext"; + +import { JsonValidationIssues } from "../issues/JsonValidationIssues"; +import { BasicValidator } from "./BasicValidator"; + +/** + * A class for the validation of strings that must follow a + * certain pattern. + * + * @internal + */ +export class StringValidator { + /** + * Validate that the given string is a valid identifier string, + * as defined in the 3D Metadata Specification. + * + * If the given value is not defined, then a `PROPERTY_MISSING` + * validation issue will be added to the given validation + * context, and `false` is returned. + * + * If the given value is not a string, then a `TYPE_MISMATCH` + * validation issue will be added to the given validation + * context, and `false` is returned. + * + * If the given string does not match the regex for a valid + * identifier string, then a `STRING_PATTERN_MISMATCH` + * validation issue will be added to the given validation + * context, and `false` is returned. + * + * Otherwise, `true` is returned. + * + * @param path - The path for the `ValidationIssue` message + * @param name - The name for the `ValidationIssue` message + * @param value - The value + * @param context - The `ValidationContext` to add the issue to + * @returns Whether the given value is an identifier string + */ + static validateIdentifierString( + path: string, + name: string, + value: string, + context: ValidationContext + ): boolean { + if (!BasicValidator.validateString(path, name, value, context)) { + return false; + } + const idRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + if (!idRegex.test(value)) { + const issue = JsonValidationIssues.STRING_PATTERN_MISMATCH( + path, + name, + value, + idRegex.toString() + ); + context.addIssue(issue); + return false; + } + return true; + } + + /** + * Validate that the given string is a valid ISO8601 string. + * + * If the given value is not defined, then a `PROPERTY_MISSING` + * validation issue will be added to the given validation + * context, and `false` is returned. + * + * If the given value is not a string, then a `TYPE_MISMATCH` + * validation issue will be added to the given validation + * context, and `false` is returned. + * + * If the given string is not a valid ISO8601 string, then a + * `STRING_PATTERN_MISMATCH` validation issue will be added + * to the given validation context, and `false` is returned. + * + * Otherwise, `true` is returned. + * + * @param path - The path for the `ValidationIssue` message + * @param name - The name for the `ValidationIssue` message + * @param value - The value + * @param context - The `ValidationContext` to add the issue to + * @returns Whether the given value is an ISO8601 string + */ + static validateIso8601String( + path: string, + name: string, + value: string, + context: ValidationContext + ): boolean { + if (!BasicValidator.validateString(path, name, value, context)) { + return false; + } + if (!StringValidator.isValidIso8601String(value)) { + const message = + `The string property ${name} must be a valid ISO8601 string, ` + + `but is '${value}'`; + const issue = JsonValidationIssues.STRING_VALUE_INVALID(path, message); + context.addIssue(issue); + return false; + } + return true; + } + + /** + * Returns whether the given string is a valid ISO8601 string. + * + * Extracted from the "validator.js" library. + * + * @param str - The string + * @returns Whether the string is a valid ISO8601 string + */ + private static isValidIso8601String(str: string) { + // The following is extracted from the "validator.js" library, at + // https://github.com/validatorjs/validator.js/blob/ + // f54599c8fbd43b1febb2cbc18190107417fbdd5e/src/lib/isISO8601.js + // (with minor adjustments for linting) + // + // The copyright header of this library: + // + // Copyright (c) 2018 Chris O'Hara + // + // Permission is hereby granted, free of charge, to any person obtaining + // a copy of this software and associated documentation files (the + // "Software"), to deal in the Software without restriction, including + // without limitation the rights to use, copy, modify, merge, publish, + // distribute, sublicense, and/or sell copies of the Software, and to + // permit persons to whom the Software is furnished to do so, subject to + // the following conditions: + // + // The above copyright notice and this permission notice shall be + // included in all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + // from http://goo.gl/0ejHHW + const iso8601 = + /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-3])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; + const check = iso8601.test(str); + if (!check) { + return false; + } + + // str must have passed the ISO8601 check + // this check is meant to catch invalid dates + // like 2009-02-31 + // first check for ordinal dates + const ordinalMatch = str.match(/^(\d{4})-?(\d{3})([ T]{1}\.*|$)/); + if (ordinalMatch) { + const oYear = Number(ordinalMatch[1]); + const oDay = Number(ordinalMatch[2]); + // if is leap year + if ((oYear % 4 === 0 && oYear % 100 !== 0) || oYear % 400 === 0) + return oDay <= 366; + return oDay <= 365; + } + const matches = str.match(/(\d{4})-?(\d{0,2})-?(\d*)/); + if (!matches) { + return false; + } + const match = matches.map(Number); + const year = match[1]; + const month = match[2]; + const day = match[3]; + const monthString = month ? `0${month}`.slice(-2) : month; + const dayString = day ? `0${day}`.slice(-2) : day; + + // create a date object and compare + const d = new Date(`${year}-${monthString || "01"}-${dayString || "01"}`); + if (month && day) { + return ( + d.getUTCFullYear() === year && + d.getUTCMonth() + 1 === month && + d.getUTCDate() === day + ); + } + return true; + } +} diff --git a/src/validation/extensions/NgaGpmValidator.ts b/src/validation/extensions/NgaGpmValidator.ts index 6846af51..6140ad93 100644 --- a/src/validation/extensions/NgaGpmValidator.ts +++ b/src/validation/extensions/NgaGpmValidator.ts @@ -3,6 +3,7 @@ import { defined } from "3d-tiles-tools"; import { Validator } from "../Validator"; import { ValidationContext } from "../ValidationContext"; import { BasicValidator } from "../BasicValidator"; +import { StringValidator } from "../StringValidator"; import { RootPropertyValidator } from "../RootPropertyValidator"; import { ExtendedObjectsValidators } from "../ExtendedObjectsValidators"; @@ -26,6 +27,24 @@ enum EpsgEcefCodes { Generic = 4978, } +/** + * The epsilon for the validation of the length of unit vectors. + * + * When the length (magnitude) of a unit vector deviates by more + * than this epsilon from 1.0, then this is considered to be + * a validation error. + */ +const UNIT_VECTOR_LENGTH_EPSILON = 0.00001; + +/** + * The epsilon for the validation orthogonality unit vectors. + * + * When the absolute dot product between two unit vectors + * is greater than this epsilon, then this is considered + * to be a validation error. + */ +const ORTHOGONAL_VECTORS_DOT_PRODUCT_EPSILON = 0.0005; + /** * A class for the validation of `NGA_gpm` extension objects * @@ -973,9 +992,8 @@ export class NgaGpmValidator implements Validator { } /** - * Validate the given array of unitVector objects. - * - * In this context, this array must always have 3 elements. + * Validate the given array of three unitVector objects that form + * an orthogonal basis. * * @param path - The path for validation issues * @param name - The name of the object @@ -1020,9 +1038,89 @@ export class NgaGpmValidator implements Validator { result = false; } } + + // If the basic structure of the vectors has been valid until now, + // validate that they form an orthonormal basis. + if (result) { + const unitVector0 = unitVectors[0]; + const unitVector1 = unitVectors[1]; + const unitVector2 = unitVectors[2]; + + const dot01 = NgaGpmValidator.computeDotProduct(unitVector0, unitVector1); + if (Math.abs(dot01) > ORTHOGONAL_VECTORS_DOT_PRODUCT_EPSILON) { + const message = + `The vectors ${name}[0] and ${name}[1] are not orthogonal, ` + + `their dot product is ${dot01}`; + const issue = NgaGpmValidationIssues.VECTORS_NOT_ORTHOGONAL( + path, + message + ); + context.addIssue(issue); + result = false; + } + + const dot12 = NgaGpmValidator.computeDotProduct(unitVector1, unitVector2); + if (Math.abs(dot12) > ORTHOGONAL_VECTORS_DOT_PRODUCT_EPSILON) { + const message = + `The vectors ${name}[1] and ${name}[2] are not orthogonal, ` + + `their dot product is ${dot12}`; + const issue = NgaGpmValidationIssues.VECTORS_NOT_ORTHOGONAL( + path, + message + ); + context.addIssue(issue); + result = false; + } + + const dot20 = NgaGpmValidator.computeDotProduct(unitVector2, unitVector0); + if (Math.abs(dot20) > ORTHOGONAL_VECTORS_DOT_PRODUCT_EPSILON) { + const message = + `The vectors ${name}[2] and ${name}[0] are not orthogonal, ` + + `their dot product is ${dot20}`; + const issue = NgaGpmValidationIssues.VECTORS_NOT_ORTHOGONAL( + path, + message + ); + context.addIssue(issue); + result = false; + } + } + return result; } + /** + * Computes the dot product between the given vectors + * + * @param v0 - The first vector + * @param v1 - The second vector + * @returns The dot product + */ + private static computeDotProduct( + v0: [number, number, number], + v1: [number, number, number] + ): number { + const x0 = v0[0]; + const y0 = v0[1]; + const z0 = v0[2]; + const x1 = v1[0]; + const y1 = v1[1]; + const z1 = v1[2]; + const dot = x0 * x1 + y0 * y1 + z0 * z1; + return dot; + } + + /** + * Computes the length (magnitude) of the given vector + * + * @param v - The vector + * @returns The length + */ + private static computeLength(v: [number, number, number]): number { + const dot = NgaGpmValidator.computeDotProduct(v, v); + return Math.sqrt(dot); + } + /** * Validate the given unitVector * @@ -1077,7 +1175,22 @@ export class NgaGpmValidator implements Validator { } } - // TODO Could check for unit-length here, with a sensible epsilon + // If the basic structure of the vector was valid until now, + // validate that it has unit length + if (result) { + const length = NgaGpmValidator.computeLength(unitVector); + if (Math.abs(length - 1.0) > UNIT_VECTOR_LENGTH_EPSILON) { + const message = + `The vector ${name} must have unit length, but has a length ` + + `of ${length}`; + const issue = NgaGpmValidationIssues.VECTOR_NOT_UNIT_LENGTH( + path, + message + ); + context.addIssue(issue); + result = false; + } + } return result; } @@ -1124,17 +1237,15 @@ export class NgaGpmValidator implements Validator { const referenceDateTime = idInformation.referenceDateTime; const referenceDateTimePath = path + "/referenceDateTime"; - // The referenceDateTime MUST be a string + // The referenceDateTime MUST be an ISO8601 string if ( - !BasicValidator.validateString( + !StringValidator.validateIso8601String( referenceDateTimePath, "referenceDateTime", referenceDateTime, context ) ) { - // TODO It should be an ISO8601 string. This could/should - // be checked with the "validator.js" npm library. result = false; } @@ -1543,9 +1654,9 @@ export class NgaGpmValidator implements Validator { const referenceDateTime = collectionUnitRecord.referenceDateTime; const referenceDateTimePath = path + "/referenceDateTime"; - // The referenceDateTime MUST be a string + // The referenceDateTime MUST be an ISO8601 string if ( - !BasicValidator.validateString( + !StringValidator.validateIso8601String( referenceDateTimePath, "referenceDateTime", referenceDateTime, @@ -2255,6 +2366,31 @@ export class NgaGpmValidator implements Validator { result = false; } } + + // If the basic structure was valid until now, then validate that + // the values of the `ppeMetadata[i].source` entries are unique. + if (result) { + const sourceValues: string[] = []; + for (let i = 0; i < ppeManifest.length; i++) { + const ppeMetadata = ppeManifest[i]; + const source = ppeMetadata.source; + sourceValues.push(source); + } + const sourceValueSet = new Set(...sourceValues); + if (sourceValueSet.size != ppeManifest.length) { + const message = + `The sources of PPE metadata entries must be unique, ` + + `but are ${sourceValues} `; + const issue = + NgaGpmValidationIssues.PER_POINT_ERROR_SOURCE_VALUES_NOT_UNIQUE( + path, + message + ); + context.addIssue(issue); + result = false; + } + } + return result; } diff --git a/src/validation/extensions/gpm/NgaGpmValidationIssues.ts b/src/validation/extensions/gpm/NgaGpmValidationIssues.ts index 547318f7..7f4a0865 100644 --- a/src/validation/extensions/gpm/NgaGpmValidationIssues.ts +++ b/src/validation/extensions/gpm/NgaGpmValidationIssues.ts @@ -29,4 +29,54 @@ export class NgaGpmValidationIssues { const issue = new ValidationIssue(type, path, message, severity); return issue; } + + /** + * Indicates that the length (magnitude) of a 3D vector that is + * supposed to be a `unitVector` deviated by more than a certain + * epsilon from 1.0. + * + * @param path - The path for the `ValidationIssue` + * @param message - The message for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static VECTOR_NOT_UNIT_LENGTH(path: string, message: string) { + const type = "VECTOR_NOT_UNIT_LENGTH"; + const severity = ValidationIssueSeverity.ERROR; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } + + /** + * Indicates that two vectors that are supposed to be part of an + * orthonormal basis (as part of `lsrAxisUnitVectors`) are not + * orthogonal. + * + * @param path - The path for the `ValidationIssue` + * @param message - The message for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static VECTORS_NOT_ORTHOGONAL(path: string, message: string) { + const type = "VECTORS_NOT_ORTHOGONAL"; + const severity = ValidationIssueSeverity.ERROR; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } + + /** + * Indicates that the `source` values of a set of PPE metadata + * objects have not been unique. + * + * @param path - The path for the `ValidationIssue` + * @param message - The message for the `ValidationIssue` + * @returns The `ValidationIssue` + */ + static PER_POINT_ERROR_SOURCE_VALUES_NOT_UNIQUE( + path: string, + message: string + ) { + const type = "PER_POINT_ERROR_SOURCE_VALUES_NOT_UNIQUE"; + const severity = ValidationIssueSeverity.ERROR; + const issue = new ValidationIssue(type, path, message, severity); + return issue; + } } diff --git a/src/validation/gltf/meshFeatures/FeatureIdValidator.ts b/src/validation/gltf/meshFeatures/FeatureIdValidator.ts index eb9e4acb..f58df4f8 100644 --- a/src/validation/gltf/meshFeatures/FeatureIdValidator.ts +++ b/src/validation/gltf/meshFeatures/FeatureIdValidator.ts @@ -3,6 +3,7 @@ import { defined } from "3d-tiles-tools"; import { ValidationContext } from "../../ValidationContext"; import { ValidatedElement } from "../../ValidatedElement"; import { BasicValidator } from "../../BasicValidator"; +import { StringValidator } from "../../StringValidator"; import { GltfData } from "../GltfData"; @@ -83,7 +84,7 @@ export class FeatureIdValidator { result = false; } else { if ( - !BasicValidator.validateIdentifierString( + !StringValidator.validateIdentifierString( labelPath, "label", label, diff --git a/src/validation/metadata/MetadataClassValidator.ts b/src/validation/metadata/MetadataClassValidator.ts index 84b3c7f8..91f6bd4f 100644 --- a/src/validation/metadata/MetadataClassValidator.ts +++ b/src/validation/metadata/MetadataClassValidator.ts @@ -2,6 +2,7 @@ import { defined } from "3d-tiles-tools"; import { ValidationContext } from "../ValidationContext"; import { BasicValidator } from "../BasicValidator"; +import { StringValidator } from "../StringValidator"; import { RootPropertyValidator } from "../RootPropertyValidator"; import { ExtendedObjectsValidators } from "../ExtendedObjectsValidators"; @@ -129,7 +130,7 @@ export class MetadataClassValidator { // Each property name MUST match the ID regex if ( - !BasicValidator.validateIdentifierString( + !StringValidator.validateIdentifierString( propertyPath, propertyName, propertyName, diff --git a/src/validation/metadata/SchemaValidator.ts b/src/validation/metadata/SchemaValidator.ts index d76143c7..47ca4e15 100644 --- a/src/validation/metadata/SchemaValidator.ts +++ b/src/validation/metadata/SchemaValidator.ts @@ -4,6 +4,7 @@ import { Schema } from "3d-tiles-tools"; import { ValidationContext } from "../ValidationContext"; import { Validator } from "../Validator"; import { BasicValidator } from "../BasicValidator"; +import { StringValidator } from "../StringValidator"; import { RootPropertyValidator } from "../RootPropertyValidator"; import { ExtendedObjectsValidators } from "../ExtendedObjectsValidators"; @@ -116,7 +117,7 @@ export class SchemaValidator implements Validator { // The id MUST be defined // The id MUST be a string // The id MUST be a valid identifier - if (!BasicValidator.validateIdentifierString(idPath, "id", id, context)) { + if (!StringValidator.validateIdentifierString(idPath, "id", id, context)) { result = false; } @@ -162,7 +163,7 @@ export class SchemaValidator implements Validator { // Each class name MUST match the ID regex if ( - !BasicValidator.validateIdentifierString( + !StringValidator.validateIdentifierString( metadataClassPath, className, className, @@ -199,7 +200,7 @@ export class SchemaValidator implements Validator { // Each enum name MUST match the ID regex if ( - !BasicValidator.validateIdentifierString( + !StringValidator.validateIdentifierString( metadataEnumPath, enumName, enumName,