Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

presentation/default units #657

Merged
merged 14 commits into from
Feb 5, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { expect } from "chai";
import { ClientRequestContext, Guid } from "@bentley/bentleyjs-core";
import { IModelDb, SnapshotDb } from "@bentley/imodeljs-backend";
import { PresentationManager } from "@bentley/presentation-backend";
import { UnitSystemFormat } from "@bentley/presentation-backend/lib/presentation-backend/PresentationManager";
import {
ContentSpecificationTypes, DisplayValuesArray, DisplayValuesMap, KeySet, PresentationUnitSystem, Ruleset, RuleTypes,
ContentSpecificationTypes, DisplayValuesArray, DisplayValuesMap, findFieldByLabel, KeySet, PresentationUnitSystem, Ruleset, RuleTypes,
} from "@bentley/presentation-common";
import { findFieldByLabel } from "../frontend/Content.test";
import { initialize, terminate } from "../IntegrationTests";

describe("PresentationManager", () => {
Expand Down Expand Up @@ -38,93 +38,73 @@ describe("PresentationManager", () => {
const keys = KeySet.fromJSON({ instanceKeys: [["Generic:PhysicalObject", ["0x74"]]], nodeKeys: [] });

it("formats property with default kind of quantity format when it doesn't have format for requested unit system", async () => {
const manager: PresentationManager = new PresentationManager();
const descriptor = await manager.getContentDescriptor({
imodel,
rulesetOrId: ruleset,
keys,
displayType: "Grid",
requestContext: new ClientRequestContext(),
unitSystem: PresentationUnitSystem.BritishImperial,
});
expect(descriptor).to.not.be.undefined;
const field = findFieldByLabel(descriptor!.fields, "cm2")!;
expect(field).not.to.be.undefined;
const content = await manager.getContent({ imodel, rulesetOrId: ruleset, keys, descriptor: descriptor!, requestContext: new ClientRequestContext(), unitSystem: PresentationUnitSystem.BritishImperial });
const displayValues = content!.contentSet[0].displayValues.rc_generic_PhysicalObject_ncc_MyProp_areaElementAspect as DisplayValuesArray;
expect(displayValues.length).is.eq(1);
expect(((displayValues[0] as DisplayValuesMap).displayValues as DisplayValuesMap)[field.name]!).to.eq("150.1235 cm²");
await checkExpectedAreaDisplayValue("150.1235 cm²", PresentationUnitSystem.BritishImperial, imodel, keys, ruleset);
});

it("formats property using default format when it doesn't have format for requested unit system", async () => {
const formatProps = {
composite:
{includeZero:true,
composite: {
includeZero:true,
spacer:" ",
units:[{label:"ft²",name:"SQ_FT"}],
units: [
{ label:"ft²", name:"SQ_FT" },
],
},
formatTraits:"KeepSingleZero|KeepDecimalPoint|ShowUnitLabel",
precision:4,
type:"Decimal",
uomSeparator:""};
uomSeparator:"",
};

const map = {
const defaultFormats = {
// eslint-disable-next-line @typescript-eslint/naming-convention
AREA: {unitSystems: [PresentationUnitSystem.BritishImperial], format: formatProps},
AREA: { unitSystems: [PresentationUnitSystem.BritishImperial], format: formatProps },
};

const manager: PresentationManager = new PresentationManager({defaultFormats: map});
const descriptor = await manager.getContentDescriptor({
imodel,
rulesetOrId: ruleset,
keys,
displayType: "Grid",
requestContext: new ClientRequestContext(),
unitSystem: PresentationUnitSystem.BritishImperial,
});
expect(descriptor).to.not.be.undefined;
const field = findFieldByLabel(descriptor!.fields, "cm2")!;
expect(field).not.to.be.undefined;
const content = await manager.getContent({ imodel, rulesetOrId: ruleset, keys, descriptor: descriptor!, requestContext: new ClientRequestContext(), unitSystem: PresentationUnitSystem.BritishImperial });
const displayValues = content!.contentSet[0].displayValues.rc_generic_PhysicalObject_ncc_MyProp_areaElementAspect as DisplayValuesArray;
expect(displayValues.length).is.eq(1);
expect(((displayValues[0] as DisplayValuesMap).displayValues as DisplayValuesMap)[field.name]!).to.eq("0.1616 ft²");
await checkExpectedAreaDisplayValue("0.1616 ft²", PresentationUnitSystem.BritishImperial, imodel, keys, ruleset, defaultFormats);
});

grigasp marked this conversation as resolved.
Show resolved Hide resolved
it("formats property using provided format when it has provided format and default format for requested unit system", async () => {
const formatProps = {
composite:
{includeZero:true,
composite: {
includeZero:true,
spacer:" ",
units:[{label:"ft²",name:"SQ_FT"}],
units: [
{ label:"ft²", name:"SQ_FT" },
],
},
formatTraits:"KeepSingleZero|KeepDecimalPoint|ShowUnitLabel",
precision:4,
type:"Decimal",
uomSeparator:""};
uomSeparator:"",
};

const map = {
const defaultFormats = {
// eslint-disable-next-line @typescript-eslint/naming-convention
AREA: {unitSystems: [PresentationUnitSystem.Metric], format: formatProps},
AREA: { unitSystems: [PresentationUnitSystem.Metric], format: formatProps },
};

const manager: PresentationManager = new PresentationManager({defaultFormats: map});
const descriptor = await manager.getContentDescriptor({
imodel,
rulesetOrId: ruleset,
keys,
displayType: "Grid",
requestContext: new ClientRequestContext(),
unitSystem: PresentationUnitSystem.Metric,
});
expect(descriptor).to.not.be.undefined;
const field = findFieldByLabel(descriptor!.fields, "cm2")!;
expect(field).not.to.be.undefined;
const content = await manager.getContent({ imodel, rulesetOrId: ruleset, keys, descriptor: descriptor!, requestContext: new ClientRequestContext(), unitSystem: PresentationUnitSystem.Metric });
const displayValues = content!.contentSet[0].displayValues.rc_generic_PhysicalObject_ncc_MyProp_areaElementAspect as DisplayValuesArray;
expect(displayValues.length).is.eq(1);
expect(((displayValues[0] as DisplayValuesMap).displayValues as DisplayValuesMap)[field.name]!).to.eq("150.1235 cm²");
await checkExpectedAreaDisplayValue("150.1235 cm²", PresentationUnitSystem.Metric, imodel, keys, ruleset, defaultFormats);
});
});

});

async function checkExpectedAreaDisplayValue(value: string, unitSystem: PresentationUnitSystem, imodel: IModelDb, keys: KeySet, ruleset: Ruleset, defaultFormats?: {[phenomenon: string]: UnitSystemFormat} ){
grigasp marked this conversation as resolved.
Show resolved Hide resolved
const manager: PresentationManager = new PresentationManager({defaultFormats});
const descriptor = await manager.getContentDescriptor({
imodel,
rulesetOrId: ruleset,
keys,
displayType: "Grid",
requestContext: new ClientRequestContext(),
unitSystem,
});
expect(descriptor).to.not.be.undefined;
const field = findFieldByLabel(descriptor!.fields, "cm2")!;
expect(field).not.to.be.undefined;
const content = await manager.getContent({ imodel, rulesetOrId: ruleset, keys, descriptor: descriptor!, requestContext: new ClientRequestContext(), unitSystem });
const displayValues = content!.contentSet[0].displayValues.rc_generic_PhysicalObject_ncc_MyProp_areaElementAspect as DisplayValuesArray;
expect(displayValues.length).is.eq(1);
expect(((displayValues[0] as DisplayValuesMap).displayValues as DisplayValuesMap)[field.name]!).to.eq(value);
}
25 changes: 1 addition & 24 deletions full-stack-tests/presentation/src/frontend/Content.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { expect } from "chai";
import { Guid, Id64, Id64String } from "@bentley/bentleyjs-core";
import { IModelConnection, SnapshotConnection } from "@bentley/imodeljs-frontend";
import {
ContentSpecificationTypes, DefaultContentDisplayTypes, Descriptor, DisplayValueGroup, Field, FieldDescriptor, InstanceKey, KeySet,
ContentSpecificationTypes, DefaultContentDisplayTypes, Descriptor, DisplayValueGroup, Field, FieldDescriptor, findFieldByLabel, InstanceKey, KeySet,
NestedContentField, PresentationError, PresentationStatus, RelationshipDirection, Ruleset, RuleTypes,
} from "@bentley/presentation-common";
import { Presentation } from "@bentley/presentation-frontend";
Expand Down Expand Up @@ -574,26 +574,3 @@ class ECClassHierarchy {
};
}
}

export function findFieldByLabel(fields: Field[], label: string, allFields?: Field[]): Field | undefined {
const isTopLevel = (undefined === allFields);
if (!allFields)
allFields = new Array<Field>();
for (const field of fields) {
if (field.label === label)
return field;

if (field.isNestedContentField()) {
const nestedMatchingField = findFieldByLabel(field.nestedFields, label, allFields);
if (nestedMatchingField)
return nestedMatchingField;
}

allFields.push(field);
}
if (isTopLevel) {
// eslint-disable-next-line no-console
console.error(`Field '${label}' not found. Available fields: [${allFields.map((f) => `"${f.label}"`).join(", ")}]`);
}
return undefined;
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const createDefaultNativePlatform = (props: DefaultNativePlatformProps):
const mode = (props.mode === PresentationManagerMode.ReadOnly) ? IModelJsNative.ECPresentationManagerMode.ReadOnly : IModelJsNative.ECPresentationManagerMode.ReadWrite;
const cacheConfig = props.cacheConfig ?? { mode: HierarchyCacheMode.Disk, directory: "" };
const defaultFormats = props.defaultFormats ? this.getSerializedDefaultFormatsMap(props.defaultFormats) : {};
this._nativeAddon = new IModelHost.platform.ECPresentationManager({ ...props, mode, cacheConfig, defaultFormats});
this._nativeAddon = new IModelHost.platform.ECPresentationManager({ ...props, mode, cacheConfig, defaultFormats });
}
private getStatus(responseStatus: IModelJsNative.ECPresentationStatus): PresentationStatus {
switch (responseStatus) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,8 @@ export interface HybridCacheConfig extends HierarchyCacheConfigBase {
}

/**
* Map for setting up format to a number of UnitSystems.
* It is used in [[PresentationManager]] to setup DefaultFormats.
* DefaultFormats are constructed by mapping a phenomenon with UnitSystemFormat.
* A data structure that associates some unit systems with a format. The associations are used for
* assigning default unit formats for specific phenomenons (see [[PresentationManagerProps.defaultFormats]])
* @alpha
*/
export interface UnitSystemFormat {
Expand Down Expand Up @@ -259,7 +258,8 @@ export interface PresentationManagerProps {
contentCacheSize?: number;

/**
* A map for setting up default formats.
* A map of default unit formats to use for formatting properties that don't have a presentation format
* in requested unit system.
* @alpha */
defaultFormats?: {
[phenomenon: string]: UnitSystemFormat;
Expand Down
19 changes: 18 additions & 1 deletion presentation/backend/src/test/PresentationManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ describe("PresentationManager", () => {
const cacheConfig = {
mode: HierarchyCacheMode.Memory,
};
const formatProps = {
composite: {
includeZero:true,
spacer:" ",
units: [{label:"'",name:"IN"}],
},
formatTraits:"KeepSingleZero|KeepDecimalPoint|ShowUnitLabel",
precision:4,
type:"Decimal",
uomSeparator:"",
};
const defaultFormats = {
// eslint-disable-next-line @typescript-eslint/naming-convention
LENGTH: { unitSystems: [PresentationUnitSystem.BritishImperial], format: formatProps },
grigasp marked this conversation as resolved.
Show resolved Hide resolved
};
const props: PresentationManagerProps = {
id: faker.random.uuid(),
presentationAssetsRoot: "/test",
Expand All @@ -114,6 +129,7 @@ describe("PresentationManager", () => {
updatesPollInterval: 1,
cacheConfig,
contentCacheSize: 999,
defaultFormats,
};
const expectedCacheConfig = {
mode: HierarchyCacheMode.Memory,
Expand All @@ -128,7 +144,8 @@ describe("PresentationManager", () => {
isChangeTrackingEnabled: true,
cacheConfig: expectedCacheConfig,
contentCacheSize: 999,
defaultFormats: {},
// eslint-disable-next-line @typescript-eslint/naming-convention
defaultFormats: {LENGTH: {unitSystems: [PresentationUnitSystem.BritishImperial], serializedFormat: JSON.stringify(formatProps)}},
});
});
});
Expand Down
1 change: 1 addition & 0 deletions presentation/common/src/presentation-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export * from "./presentation-common/AsyncTasks";
export * from "./presentation-common/Diagnostics";
export * from "./presentation-common/EC";
export * from "./presentation-common/Error";
export * from "./presentation-common/findFieldByLabel";
export * from "./presentation-common/KeySet";
export * from "./presentation-common/LabelDefinition";
export * from "./presentation-common/Logging";
Expand Down
35 changes: 35 additions & 0 deletions presentation/common/src/presentation-common/findFieldByLabel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Core
*/

import { Field } from "../presentation-common";

/** Returns field by given label.
* @internal
*/
export function findFieldByLabel(fields: Field[], label: string, allFields?: Field[]): Field | undefined {
const isTopLevel = (undefined === allFields);
if (!allFields)
allFields = new Array<Field>();
for (const field of fields) {
if (field.label === label)
return field;

if (field.isNestedContentField()) {
const nestedMatchingField = findFieldByLabel(field.nestedFields, label, allFields);
if (nestedMatchingField)
return nestedMatchingField;
}

allFields.push(field);
}
if (isTopLevel) {
// eslint-disable-next-line no-console
console.error(`Field '${label}' not found. Available fields: [${allFields.map((f) => `"${f.label}"`).join(", ")}]`);
}
return undefined;
}