UNPKG

@itwin/core-backend

Version:
184 lines • 10.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { IModelHost, SnapshotDb } from "../../core-backend"; import { Format, KindOfQuantity, SchemaUnitProvider, Unit } from "@itwin/ecschema-metadata"; import { QueryBinder, QueryRowFormat } from "@itwin/core-common"; import { assert, expect } from "chai"; import * as path from "path"; import { KnownTestLocations } from "../KnownTestLocations"; import { TestUtils } from "../TestUtils"; /** * Example-style tests showing how to access KindOfQuantity presentation formats * through SchemaView and resolve the alias-qualified format/unit names * into full schema items via ecschema-metadata or ECSQL. * * Uses sim-master.bim which contains schemas with rich KoQ definitions * (AecUnits, RoadRailUnits, LinearReferencing, etc.). */ describe("SchemaView KindOfQuantity presentation formats", () => { let iModel; let schemaView; before(async () => { if (!IModelHost.isValid) await TestUtils.startBackend(); iModel = SnapshotDb.openFile(path.join(KnownTestLocations.assetsDir, "sim-master.bim")); schemaView = await iModel.getSchemaView(); }); after(() => { iModel.close(); }); /** * Example: get a property's KoQ from SchemaView, then resolve the * alias-qualified format and unit names to full ecschema-metadata objects via * IModelDb.schemaContext. * * SchemaView uses alias-qualified names (e.g. "f:DefaultRealU", "u:M") * because the Units and Formats schemas are excluded from the SchemaView blob. * The alias ("f" -> "Formats", "u" -> "Units") is stored in ec_Schema, so we * can resolve it using the schema context that has the full schema set. */ it("should resolve KoQ format and unit names via ecschema-metadata", async () => { // --- Step 1: find a property with a KoQ via SchemaView --- const rClass = schemaView.findClass("LinearReferencing:DistanceExpression"); assert.isDefined(rClass, "DistanceExpression class not found"); const rProp = rClass.getProperty("DistanceAlongFromStart"); assert.isDefined(rProp, "DistanceAlongFromStart property not found"); const prop = rProp; if (!prop.isPrimitive()) throw new Error("expected primitive"); const rKoq = prop.kindOfQuantity; assert.isDefined(rKoq, "property should have a KoQ"); // --- Step 2: inspect parsed presentation formats --- const formats = rKoq.presentationFormats; expect(formats.length).to.be.greaterThan(0, "KoQ should have presentation formats"); // LinearReferencing:LENGTH has: f:DefaultRealU(2)[u:M], f:DefaultRealU(2)[u:FT] const first = formats[0]; expect(first.name).to.equal("f:DefaultRealU"); expect(first.precision).to.equal(2); expect(first.unitAndLabels).to.have.length(1); expect(first.unitAndLabels[0][0]).to.equal("u:M"); // --- Step 3: resolve alias names to full schema items via schemaContext --- // The Formats and Units schemas are excluded from SchemaView, // but IModelDb.schemaContext (ecschema-metadata) has the full set. // Alias "f" -> "Formats", alias "u" -> "Units" - these are standard BIS aliases. // Resolve the format: "f:DefaultRealU" -> "Formats.DefaultRealU" const formatFullName = `Formats.${first.name.split(":")[1]}`; const metaFormat = iModel.schemaContext.getSchemaItemSync(formatFullName, Format); assert.isDefined(metaFormat, `Format '${formatFullName}' not found in ecschema-metadata`); expect(metaFormat.precision).to.be.a("number"); // Resolve the unit: "u:M" -> "Units.M" const unitFullName = `Units.${first.unitAndLabels[0][0].split(":")[1]}`; const metaUnit = iModel.schemaContext.getSchemaItemSync(unitFullName, Unit); assert.isDefined(metaUnit, `Unit '${unitFullName}' not found in ecschema-metadata`); expect(metaUnit.fullName).to.equal("Units.M"); // Access unit details like label and unit system const unitSystem = await metaUnit.unitSystem; assert.isDefined(unitSystem, "unit should have a unit system"); expect(unitSystem.name).to.equal("SI"); // --- Step 4: cross-validate against the KoQ in ecschema-metadata --- const metaKoq = iModel.schemaContext.getSchemaItemSync("LinearReferencing", "LENGTH", KindOfQuantity); assert.isDefined(metaKoq, "KoQ not found in ecschema-metadata"); expect(metaKoq.relativeError).to.equal(rKoq.relativeError); const metaFormats = metaKoq.presentationFormats; expect(metaFormats.length).to.equal(formats.length); // ecschema-metadata resolves the formats fully - cross-check the format names. // OverrideFormat.fullName includes override syntax (e.g. "Formats.DefaultRealU(2)[Units.M]"), // so strip the override portion before extracting the base name. for (let i = 0; i < formats.length; i++) { const rtItemName = formats[i].name.split(":")[1]; const metaBaseFullName = metaFormats[i].fullName.split(/[([]/)[0]; const metaItemName = metaBaseFullName.split(".").pop(); expect(rtItemName).to.equal(metaItemName, `format name mismatch at index ${i}`); } // The SchemaUnitProvider bridges ecschema-metadata units to the @itwin/core-quantity package. // Use it to build a FormatterSpec for quantity formatting: const unitsProvider = new SchemaUnitProvider(iModel.schemaContext); const persistenceUnitProps = await unitsProvider.findUnitByName("Units.M"); expect(persistenceUnitProps.name).to.equal("Units.M"); }); /** * Example: get a KoQ from SchemaView, then query ECDbMeta via ECSQL * to resolve the referenced format and unit definitions. * * This approach avoids loading the full ecschema-metadata schema set and works * well for targeted lookups - e.g. "I have a format name, give me its NumericSpec." */ it("should resolve KoQ format and unit names via ECSQL", async () => { // --- Step 1: find a KoQ via SchemaView --- const rKoq = schemaView.findKindOfQuantity("LinearReferencing:LENGTH"); assert.isDefined(rKoq, "KoQ not found in SchemaView"); const formats = rKoq.presentationFormats; expect(formats.length).to.be.greaterThan(0); // --- Step 2: resolve the format name via meta.FormatDef --- // "f:DefaultRealU" -> format name is "DefaultRealU" in the "Formats" schema const first = formats[0]; const formatItemName = first.name.split(":")[1]; // "DefaultRealU" // Query FormatDef to get the format's NumericSpec (= FormatProps JSON) let formatNumericSpec; for await (const row of iModel.createQueryReader(`SELECT f.NumericSpec, f.CompositeSpec FROM meta.FormatDef f JOIN meta.ECSchemaDef s USING meta.SchemaOwnsFormats WHERE f.Name = ? AND s.Name = 'Formats'`, QueryBinder.from([formatItemName]), { // eslint-disable-next-line @typescript-eslint/no-deprecated rowFormat: QueryRowFormat.UseJsPropertyNames, })) { formatNumericSpec = row.toRow(); } assert.isDefined(formatNumericSpec, `FormatDef '${formatItemName}' not found via ECSQL`); // NumericSpec is a JSON string with the format properties (type, precision, traits, etc.) const numericSpec = JSON.parse(formatNumericSpec.numericSpec); expect(numericSpec.type).to.equal("Decimal"); expect(numericSpec.precision).to.be.a("number"); // The presentation format can override precision - e.g. the override "(2)" means precision 2 if (first.precision !== undefined) expect(first.precision).to.equal(2); // our override // --- Step 3: resolve the unit name via meta.UnitDef --- // "u:M" -> unit name is "M" in the "Units" schema const unitItemName = first.unitAndLabels[0][0].split(":")[1]; // "M" let unitRow; for await (const row of iModel.createQueryReader(`SELECT u.Name, u.DisplayLabel, us.Name AS unitSystemName FROM meta.UnitDef u JOIN meta.ECSchemaDef s USING meta.SchemaOwnsUnits JOIN meta.UnitSystemDef us USING meta.UnitSystemHasUnits WHERE u.Name = ? AND s.Name = 'Units'`, QueryBinder.from([unitItemName]), { // eslint-disable-next-line @typescript-eslint/no-deprecated rowFormat: QueryRowFormat.UseJsPropertyNames, })) { unitRow = row.toRow(); } assert.isDefined(unitRow, `UnitDef '${unitItemName}' not found via ECSQL`); expect(unitRow.name).to.equal("M"); expect(unitRow.unitSystemName).to.equal("SI"); // --- Step 4: query the format's composite units (base format units, before overrides) --- // DefaultRealU has no composite units (it's a single-unit format), but AngleDMS does. // Let's also check the second KoQ format which might have composite units. const angleKoq = schemaView.findKindOfQuantity("RoadRailUnits:ANGLE"); if (angleKoq) { const angleFormats = angleKoq.presentationFormats; // The AngleDMS format has composite units: ARC_DEG, ARC_MINUTE, ARC_SECOND const dmsFormat = angleFormats.find((f) => f.name.split(":")[1] === "AngleDMS"); if (dmsFormat) { const compositeUnits = []; for await (const row of iModel.createQueryReader(`SELECT cu.Ordinal, u.Name AS unitName, cu.Label FROM meta.FormatCompositeUnitDef cu JOIN meta.FormatDef f USING meta.FormatOwnsCompositeUnits JOIN meta.ECSchemaDef s USING meta.SchemaOwnsFormats JOIN meta.UnitDef u USING meta.CompositeUnitRefersToUnit WHERE f.Name = 'AngleDMS' AND s.Name = 'Formats' ORDER BY cu.Ordinal`, undefined, { // eslint-disable-next-line @typescript-eslint/no-deprecated rowFormat: QueryRowFormat.UseJsPropertyNames, })) { compositeUnits.push(row.toRow()); } expect(compositeUnits.length).to.equal(3, "AngleDMS should have 3 composite units"); expect(compositeUnits[0].unitName).to.equal("ARC_DEG"); expect(compositeUnits[1].unitName).to.equal("ARC_MINUTE"); expect(compositeUnits[2].unitName).to.equal("ARC_SECOND"); } } }); }); //# sourceMappingURL=SchemaViewKoQ.test.js.map