diff --git a/common/api/ecschema-metadata.api.md b/common/api/ecschema-metadata.api.md index ad4521c32d47..193542584c86 100644 --- a/common/api/ecschema-metadata.api.md +++ b/common/api/ecschema-metadata.api.md @@ -707,6 +707,8 @@ export class InvertedUnit extends SchemaItem { get invertsUnit(): LazyLoadedUnit | undefined; // (undocumented) protected _invertsUnit?: LazyLoadedUnit; + // @alpha (undocumented) + static isInvertedUnit(object: any): object is InvertedUnit; // (undocumented) readonly schemaItemType: SchemaItemType.InvertedUnit; // @alpha diff --git a/common/changes/@itwin/ecschema-metadata/rob-invertedUnitsFix_2024-10-25-12-34.json b/common/changes/@itwin/ecschema-metadata/rob-invertedUnitsFix_2024-10-25-12-34.json new file mode 100644 index 000000000000..b3c48345ffff --- /dev/null +++ b/common/changes/@itwin/ecschema-metadata/rob-invertedUnitsFix_2024-10-25-12-34.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@itwin/ecschema-metadata", + "comment": "Support inverted units through SchemaUnitsConverter", + "type": "none" + } + ], + "packageName": "@itwin/ecschema-metadata" +} \ No newline at end of file diff --git a/core/ecschema-metadata/src/Metadata/InvertedUnit.ts b/core/ecschema-metadata/src/Metadata/InvertedUnit.ts index a2184fb19717..a2e27f1e0680 100644 --- a/core/ecschema-metadata/src/Metadata/InvertedUnit.ts +++ b/core/ecschema-metadata/src/Metadata/InvertedUnit.ts @@ -35,6 +35,13 @@ export class InvertedUnit extends SchemaItem { public get invertsUnit(): LazyLoadedUnit | undefined { return this._invertsUnit; } public get unitSystem(): LazyLoadedUnitSystem | undefined { return this._unitSystem; } + /** + * @alpha + */ + public static isInvertedUnit(object: any): object is InvertedUnit { + return SchemaItem.isSchemaItem(object) && object.schemaItemType === SchemaItemType.InvertedUnit; + } + /** * Save this InvertedUnit's properties to an object for serializing to JSON. * @param standalone Serialization includes only this object (as opposed to the full schema). diff --git a/core/ecschema-metadata/src/UnitProvider/SchemaUnitProvider.ts b/core/ecschema-metadata/src/UnitProvider/SchemaUnitProvider.ts index 114eb978eeb2..ce7c2a4deb88 100644 --- a/core/ecschema-metadata/src/UnitProvider/SchemaUnitProvider.ts +++ b/core/ecschema-metadata/src/UnitProvider/SchemaUnitProvider.ts @@ -3,13 +3,14 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { BentleyError, BentleyStatus } from "@itwin/core-bentley"; -import { UnitConversionProps, UnitExtraData, UnitProps, UnitsProvider } from "@itwin/core-quantity"; +import { BadUnit, UnitConversionInvert, UnitConversionProps, UnitExtraData, UnitProps, UnitsProvider } from "@itwin/core-quantity"; import { ISchemaLocater, SchemaContext } from "../Context"; import { SchemaItem } from "../Metadata/SchemaItem"; import { SchemaItemKey, SchemaKey } from "../SchemaKey"; import { Unit } from "../Metadata/Unit"; import { SchemaItemType } from "../ECObjects"; import { UnitConverter } from "../UnitConversion/UnitConverter"; +import { InvertedUnit } from "../Metadata/InvertedUnit"; /** * Class used to find Units in SchemaContext by attributes such as Phenomenon and DisplayLabel. @@ -26,7 +27,7 @@ export class SchemaUnitProvider implements UnitsProvider { * created and the locater will be added. * @param _unitExtraData Additional data like alternate display label not found in Units Schema to match with Units; Defaults to empty array. */ - constructor(contextOrLocater: ISchemaLocater, private _unitExtraData: UnitExtraData[] = []){ + constructor(contextOrLocater: ISchemaLocater, private _unitExtraData: UnitExtraData[] = []) { if (contextOrLocater instanceof SchemaContext) { this._context = contextOrLocater; } else { @@ -42,8 +43,26 @@ export class SchemaUnitProvider implements UnitsProvider { * @returns UnitProps interface from @itwin/core-quantity whose name matches unitName. */ public async findUnitByName(unitName: string): Promise { - const unit = await this.findECUnitByName(unitName); - return this.getUnitsProps(unit); + // Check if schema exists and unit exists in schema + const [schemaName, schemaItemName] = SchemaItem.parseFullName(unitName); + const schemaKey = new SchemaKey(schemaName); + const schema = await this._context.getSchema(schemaKey); + + if (!schema) { + return new BadUnit(); // return BadUnit if schema does not exist + } + + const itemKey = new SchemaItemKey(schemaItemName, schema.schemaKey); + const unit = await this._context.getSchemaItem(itemKey); + if (unit && unit.schemaItemType === SchemaItemType.Unit) + return this.getUnitsProps(unit); + + const invertedUnit = await this._context.getSchemaItem(itemKey); + if (invertedUnit && invertedUnit.schemaItemType === SchemaItemType.InvertedUnit) { + return this.getUnitsProps(invertedUnit); + } + + return new BadUnit(); } /** @@ -83,9 +102,18 @@ export class SchemaUnitProvider implements UnitsProvider { if (Unit.isUnit(value)) { const foundPhenomenon = await value.phenomenon; if (foundPhenomenon && foundPhenomenon.key.matchesFullName(phenomenon)) { - const unitProps = this.getUnitsProps(value); + const unitProps = await this.getUnitsProps(value); filteredUnits.push(unitProps); } + } else if (InvertedUnit.isInvertedUnit(value) && value.invertsUnit) { + const invertsUnit = await value.invertsUnit; + if (invertsUnit) { + const foundPhenomenon = await invertsUnit.phenomenon; + if (foundPhenomenon && foundPhenomenon.key.matchesFullName(phenomenon)) { + const unitProps = await this.getUnitsProps(value); + filteredUnits.push(unitProps); + } + } } ({ value, done } = schemaItems.next()); } @@ -124,22 +152,13 @@ export class SchemaUnitProvider implements UnitsProvider { const findSchema = schemaName ? schemaName.toLowerCase() : undefined; const findPhenomenon = phenomenon ? phenomenon.toLowerCase() : undefined; const findUnitSystem = unitSystem ? unitSystem.toLowerCase() : undefined; - let foundUnit: Unit | undefined; - - try { - try { - foundUnit = await this.findUnitByDisplayLabel(findLabel, findSchema, findPhenomenon, findUnitSystem); - } catch { - // If there is no Unit with display label that matches label, then check for alternate display labels that may match - foundUnit = await this.findUnitByAltDisplayLabel(findLabel, findSchema, findPhenomenon, findUnitSystem); - } - } catch { - throw new BentleyError(BentleyStatus.ERROR, "Cannot find unit with label", () => { - return { unitLabel }; - }); - } - return this.getUnitsProps(foundUnit); + const foundUnit: UnitProps = await this.findUnitByDisplayLabel(findLabel, findSchema, findPhenomenon, findUnitSystem); + if (foundUnit.isValid) + return foundUnit; + + // If there is no Unit with display label that matches label, then check for alternate display labels that may match + return this.findUnitByAltDisplayLabel(findLabel, findSchema, findPhenomenon, findUnitSystem); } /** @@ -149,43 +168,47 @@ export class SchemaUnitProvider implements UnitsProvider { * @returns The UnitConversionProps interface from the @itwin/core-quantity package. */ public async getConversion(fromUnit: UnitProps, toUnit: UnitProps): Promise { - const conversion = await this._unitConverter.calculateConversion(fromUnit.name, toUnit.name); - return { + // need to check if either side is an inverted unit. The UnitConverter can only handle Units + if (!fromUnit.isValid || !toUnit.isValid) + throw new BentleyError(BentleyStatus.ERROR, "Both provided units must be valid.", () => { + return { fromUnit, toUnit }; + }); + + const { unitName: fromUnitName, isInverted: fromIsInverted } = await this.checkUnitPropsForConversion(fromUnit, this._context); + const { unitName: toUnitName, isInverted: toIsInverted } = await this.checkUnitPropsForConversion(toUnit, this._context); + + const conversion = await this._unitConverter.calculateConversion(fromUnitName, toUnitName); + const result: UnitConversionProps = { factor: conversion.factor, offset: conversion.offset, }; + if (fromIsInverted && !toIsInverted) + result.inversion = UnitConversionInvert.InvertPreConversion; + else if (!fromIsInverted && toIsInverted) + result.inversion = UnitConversionInvert.InvertPostConversion; + + return result; } - /** - * Find unit in a schema that has unitName. - * @param unitName Full name of unit. - * @returns Unit whose full name matches unitName. - */ - private async findECUnitByName(unitName: string): Promise { - // Check if schema exists and unit exists in schema - const [schemaName, schemaItemName] = SchemaItem.parseFullName(unitName); + private async checkUnitPropsForConversion(input: UnitProps, context: SchemaContext): Promise<{ unitName: string, isInverted: boolean }> { + const [schemaName, schemaItemName] = SchemaItem.parseFullName(input.name); const schemaKey = new SchemaKey(schemaName); - const schema = await this._context.getSchema(schemaKey); + const schema = await context.getSchema(schemaKey); if (!schema) { - throw new BentleyError(BentleyStatus.ERROR, "Cannot find schema for unit", () => { - return { schema: schemaName, unit: unitName }; + throw new BentleyError(BentleyStatus.ERROR, "Could not obtain schema for unit.", () => { + return { name: input.name }; }); } const itemKey = new SchemaItemKey(schemaItemName, schema.schemaKey); - const item = await this._context.getSchemaItem(itemKey); - if (!item) - throw new BentleyError(BentleyStatus.ERROR, "Cannot find schema item/unit", () => { - return { item: schemaItemName, schema: schemaName }; - }); - - if (item.schemaItemType === SchemaItemType.Unit) - return item; + const invertedUnit = await context.getSchemaItem(itemKey); + // Check if we found an item, the item is an inverted unit, and it has its invertsUnit property set + if (invertedUnit && InvertedUnit.isInvertedUnit(invertedUnit) && invertedUnit.invertsUnit) { + return { unitName: invertedUnit.invertsUnit.fullName, isInverted: true }; + } - throw new BentleyError(BentleyStatus.ERROR, "Item is not a unit", () => { - return { itemType: item.key.fullName }; - }); + return { unitName: input.name, isInverted: false }; } /** @@ -193,11 +216,25 @@ export class SchemaUnitProvider implements UnitsProvider { * @param unit The Unit to convert. * @returns UnitProps interface from @itwin/core. */ - private getUnitsProps(unit: Unit): UnitProps { + private async getUnitsProps(unit: Unit | InvertedUnit): Promise { + if (Unit.isUnit(unit)) { + return { + name: unit.fullName, + label: unit.label ?? "", + phenomenon: unit.phenomenon ? unit.phenomenon.fullName : "", + isValid: true, + system: unit.unitSystem === undefined ? "" : unit.unitSystem.fullName, + }; + } + + const invertsUnit = await unit.invertsUnit; + if (!invertsUnit) + return new BadUnit(); + return { name: unit.fullName, label: unit.label ?? "", - phenomenon: unit.phenomenon ? unit.phenomenon.fullName : "", + phenomenon: invertsUnit.phenomenon ? invertsUnit.phenomenon.fullName : "", isValid: true, system: unit.unitSystem === undefined ? "" : unit.unitSystem.fullName, }; @@ -207,48 +244,60 @@ export class SchemaUnitProvider implements UnitsProvider { * Finds Unit by displayLabel and that it belongs to schemaName, phenomenon, and unitSystem if defined. * @internal */ - private async findUnitByDisplayLabel(displayLabel: string, schemaName?: string, phenomenon?: string, unitSystem?: string): Promise { + private async findUnitByDisplayLabel(displayLabel: string, schemaName?: string, phenomenon?: string, unitSystem?: string): Promise { + // TODO: Known bug: This only looks through loaded schemas. If schema name is provided, we can attempt to load that schema const schemaItems = this._context.getSchemaItems(); let { value, done } = schemaItems.next(); while (!done) { if (Unit.isUnit(value) && value.label?.toLowerCase() === displayLabel) { + // TODO: this can be optimized. We don't have to await these if we don't want to check for them const currPhenomenon = await value.phenomenon; const currUnitSystem = await value.unitSystem; if (!schemaName || value.schema.name.toLowerCase() === schemaName) if (!phenomenon || (currPhenomenon && currPhenomenon.key.matchesFullName(phenomenon))) if (!unitSystem || (currUnitSystem && currUnitSystem.key.matchesFullName(unitSystem))) - return value; + return this.getUnitsProps(value); + } else if (InvertedUnit.isInvertedUnit(value) && value.label?.toLowerCase() === displayLabel && value.invertsUnit) { + const invertsUnit = await value.invertsUnit; + if (invertsUnit) { + const currPhenomenon = await invertsUnit.phenomenon; + const currUnitSystem = await invertsUnit.unitSystem; + if (!schemaName || value.schema.name.toLowerCase() === schemaName) + if (!phenomenon || (currPhenomenon && currPhenomenon.key.matchesFullName(phenomenon))) + if (!unitSystem || (currUnitSystem && currUnitSystem.key.matchesFullName(unitSystem))) + return this.getUnitsProps(value); + } } ({ value, done } = schemaItems.next()); } - throw new BentleyError(BentleyStatus.ERROR, "Cannot find unit with display label", () => { - return { displayLabel }; - }); + return new BadUnit(); } /** * Finds Unit by altDisplayLabel and that it belongs to schemaName, phenomenon, and unitSystem if defined. * @internal */ - private async findUnitByAltDisplayLabel(altDisplayLabel: string, schemaName?: string, phenomenon?: string, unitSystem?: string): Promise { + private async findUnitByAltDisplayLabel(altDisplayLabel: string, schemaName?: string, phenomenon?: string, unitSystem?: string): Promise { for (const entry of this._unitExtraData) { if (entry.altDisplayLabels && entry.altDisplayLabels.length > 0) { if (entry.altDisplayLabels.findIndex((ref: string) => ref.toLowerCase() === altDisplayLabel) !== -1) { // Found altDisplayLabel that matches label to find - const unit = await this.findECUnitByName(entry.name); - const foundPhenomenon = await unit.phenomenon; - const foundUnitSystem = await unit.unitSystem; - if (!schemaName || unit.schema.name.toLowerCase() === schemaName) - if (!phenomenon || (foundPhenomenon && foundPhenomenon.key.matchesFullName(phenomenon))) - if (!unitSystem || (foundUnitSystem && foundUnitSystem.key.matchesFullName(unitSystem))) - return unit; + const unitProps = await this.findUnitByName(entry.name); + // If no schemaName, phenomenon, or unitSystem are provided, return unitProps + if (!schemaName && !phenomenon && !unitSystem) + return unitProps; + + // Check if the provided values match unitProps + const schemaNameMatches = !schemaName || unitProps.name.toLowerCase().startsWith(schemaName); + const phenomenonMatches = !phenomenon || unitProps.phenomenon.toLowerCase() === phenomenon; + const unitSystemMatches = !unitSystem || unitProps.system.toLowerCase() === unitSystem; + // If all provided values match, return unitProps + if (schemaNameMatches && phenomenonMatches && unitSystemMatches) + return unitProps; } } } - - throw new BentleyError(BentleyStatus.ERROR, "Cannot find unit with alternate display label", () => { - return { altDisplayLabel }; - }); + return new BadUnit(); } } diff --git a/core/ecschema-metadata/src/test/Metadata/Quantity.test.ts b/core/ecschema-metadata/src/test/UnitConversion/Quantity.test.ts similarity index 52% rename from core/ecschema-metadata/src/test/Metadata/Quantity.test.ts rename to core/ecschema-metadata/src/test/UnitConversion/Quantity.test.ts index 609f08476d10..d8e9adc607e3 100644 --- a/core/ecschema-metadata/src/test/Metadata/Quantity.test.ts +++ b/core/ecschema-metadata/src/test/UnitConversion/Quantity.test.ts @@ -3,22 +3,93 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { assert } from "chai"; +import { assert, expect } from "chai"; import { SchemaContext } from "../../Context"; import { SchemaItemFormatProps } from "../../Deserialization/JsonProps"; import { Format } from "../../Metadata/Format"; import { MutableSchema, Schema } from "../../Metadata/Schema"; -import { Formatter, FormatterSpec, Parser, ParserSpec, Format as QFormat, UnitProps } from "@itwin/core-quantity"; +import { Formatter, FormatterSpec, Parser, ParserSpec, Format as QFormat, Quantity, UnitProps } from "@itwin/core-quantity"; import { deserializeXmlSync } from "../TestUtils/DeserializationHelpers"; import { SchemaKey, SchemaMatchType, SchemaUnitProvider } from "../../ecschema-metadata"; import * as fs from "fs"; import * as path from "path"; describe("Quantity", () => { - let schema: Schema; - let testFormat: Format; + describe("Conversions", () => { + let context: SchemaContext; + let provider: SchemaUnitProvider; + + before(() => { + context = new SchemaContext(); + + const schemaFile = path.join(__dirname, "..", "..", "..", "..", "node_modules", "@bentley", "units-schema", "Units.ecschema.xml"); + const schemaXml = fs.readFileSync(schemaFile, "utf-8"); + deserializeXmlSync(schemaXml, context); + provider = new SchemaUnitProvider(context); + }); + + it("Convert between inverted and base units", async () => { + const invertedUnit = await provider.findUnitByName("Units.HORIZONTAL_PER_VERTICAL"); + assert.isTrue(invertedUnit.isValid); + const baseUnit = await provider.findUnitByName("Units.VERTICAL_PER_HORIZONTAL"); + assert.isTrue(baseUnit.isValid); + + const invertedValue = 2.0; + const baseValue = 0.5; + + const baseQuantity = new Quantity(baseUnit, baseValue); + const invertedQuantity = new Quantity(invertedUnit, invertedValue); + + const toInvertedConversion = await provider.getConversion(baseUnit, invertedUnit); + const invertedResult = baseQuantity.convertTo(invertedUnit, toInvertedConversion); + expect(invertedResult).to.not.be.undefined; + if (invertedResult) { + expect(invertedResult.magnitude).to.equal(invertedValue); + expect(invertedResult.unit.name).to.equal(invertedUnit.name); + } + + const toBaseConversion = await provider.getConversion(invertedUnit, baseUnit); + const baseResult = invertedQuantity.convertTo(baseUnit, toBaseConversion); + expect(baseResult).to.not.be.undefined; + if (baseResult) { + expect(baseResult.magnitude).to.equal(baseValue); + expect(baseResult.unit.name).to.equal(baseUnit.name); + } + }); + + it("Convert between meters and feet", async () => { + const metersUnit = await provider.findUnitByName("Units.M"); + assert.isTrue(metersUnit.isValid); + const feetUnit = await provider.findUnitByName("Units.FT"); + assert.isTrue(feetUnit.isValid); + + const metersValue = 1.0; + const feetValue = 3.28084; + + const metersQuantity = new Quantity(metersUnit, metersValue); + const feetQuantity = new Quantity(feetUnit, feetValue); + + const toFeetConversion = await provider.getConversion(metersUnit, feetUnit); + const feetResult = metersQuantity.convertTo(feetUnit, toFeetConversion); + expect(feetResult).to.not.be.undefined; + if (feetResult) { + expect(feetResult.magnitude).to.be.closeTo(feetValue, 0.00001); + expect(feetResult.unit.name).to.equal(feetUnit.name); + } + + const toMetersConversion = await provider.getConversion(feetUnit, metersUnit); + const metersResult = feetQuantity.convertTo(metersUnit, toMetersConversion); + expect(metersResult).to.not.be.undefined; + if (metersResult) { + expect(metersResult.magnitude).to.be.closeTo(metersValue, 0.00001); + expect(metersResult.unit.name).to.equal(metersUnit.name); + } + }); + }); describe("Format and Parse Bearing", () => { + let schema: Schema; + let testFormat: Format; const formatProps: SchemaItemFormatProps = { schemaItemType: "Format", label: "MyCustomFormat", @@ -76,7 +147,6 @@ describe("Quantity", () => { const formatterResult = Formatter.formatQuantity(value, bearingDMSFormatter); assert.equal(formatterResult, inputString); - }); }); diff --git a/core/ecschema-metadata/src/test/UnitProvider/UnitData.ts b/core/ecschema-metadata/src/test/UnitProvider/UnitData.ts index 5117b9edc352..4e388cd6dbc5 100644 --- a/core/ecschema-metadata/src/test/UnitProvider/UnitData.ts +++ b/core/ecschema-metadata/src/test/UnitProvider/UnitData.ts @@ -48,4 +48,8 @@ export const UNIT_EXTRA_DATA = [ { name: "Units.CUB_US_SURVEY_FT", altDisplayLabels: ["cf"] }, { name: "Units.CUB_YRD", altDisplayLabels: ["cy"] }, { name: "Units.CUB_M", altDisplayLabels: ["cm"] }, + // slope, example for an inverted unit + { name: "Units.HORIZONTAL_PER_VERTICAL", altDisplayLabels: ["hpv"] }, + // to test the same label with a different system/phenomenon + { name: "Units.FT_H2O", altDisplayLabels: ["hpv"] }, ]; diff --git a/core/ecschema-metadata/src/test/UnitProvider/UnitProvider.test.ts b/core/ecschema-metadata/src/test/UnitProvider/UnitProvider.test.ts index 48f84e34f218..5e9e77e6d002 100644 --- a/core/ecschema-metadata/src/test/UnitProvider/UnitProvider.test.ts +++ b/core/ecschema-metadata/src/test/UnitProvider/UnitProvider.test.ts @@ -322,5 +322,52 @@ describe("Unit Provider tests", () => { const unit2 = await provider.findUnitByName("Units.KM_PER_HR"); expect(unit2.name === "Units.KM_PER_HR", `Unit name should be Units.KM_PER_HR and not ${unit2.name}`).to.be.true; }); + + it("should find VERTICAL_PER_HORIZONTAL by unit name", async () => { + const unit = await provider.findUnitByName("Units.VERTICAL_PER_HORIZONTAL"); + expect(unit.name === "Units.VERTICAL_PER_HORIZONTAL", `Unit name should be Units.VERTICAL_PER_HORIZONTAL and not ${unit.name}`).to.be.true; + }); + + it("should find inverted unit HORIZONTAL_PER_VERTICAL by unit name", async () => { + const unit = await provider.findUnitByName("Units.HORIZONTAL_PER_VERTICAL"); + expect(unit.isValid).to.be.true; + expect(unit.name === "Units.HORIZONTAL_PER_VERTICAL", `Unit name should be Units.HORIZONTAL_PER_VERTICAL and not ${unit.name}`).to.be.true; + expect(unit.label).to.equal(""); + expect(unit.phenomenon).to.equal("Units.SLOPE"); + expect(unit.system).to.equal("Units.INTERNATIONAL"); + }); + + it("should find slope units by family", async () => { + const slopeUnits = await provider.getUnitsByFamily("Units.SLOPE"); + expect(slopeUnits).to.have.lengthOf(14); + // find unit of name Units.HORIZONTAL_PER_VERTICAL in the array + const hpv = slopeUnits.find((u) => u.name === "Units.HORIZONTAL_PER_VERTICAL"); + expect(hpv).to.not.be.undefined; + expect(hpv!.phenomenon).to.equal("Units.SLOPE"); + }); + + it("should find HORZONTAL_PER_VERTICAL unit by alt label", async () => { + let unit = await provider.findUnit("hpv"); + expect(unit.isValid).to.be.true; + expect(unit.name).to.equal("Units.HORIZONTAL_PER_VERTICAL"); + + unit = await provider.findUnit("hpv", "UnknownSchema"); + expect(unit.isValid).to.be.false; + + unit = await provider.findUnit("hpv", "Units"); + expect(unit.isValid).to.be.true; + expect(unit.name).to.equal("Units.HORIZONTAL_PER_VERTICAL"); + + unit = await provider.findUnit("hpv", undefined, "Units.PRESSURE"); + expect(unit.isValid).to.be.true; + expect(unit.name).to.equal("Units.FT_H2O"); + + unit = await provider.findUnit("hpv", undefined, undefined, "Units.USCUSTOM"); + expect(unit.isValid).to.be.true; + expect(unit.name).to.equal("Units.FT_H2O"); + + unit = await provider.findUnit("hpv", undefined, "Units.SLOPE", "Units.USCUSTOM"); + expect(unit.isValid).to.be.false; + }); }); });