@itwin/presentation-hierarchies
Version:
A package for creating hierarchies based on data in iTwin.js iModels.
302 lines • 16 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { assert } from "@itwin/core-bentley";
import { createMainThreadReleaseOnTimePassedHandler, formatConcatenatedValue, getClass, TypedPrimitiveValue, } from "@itwin/presentation-shared";
import { HierarchyNode } from "../../../HierarchyNode.js";
import { HierarchyNodeKey } from "../../../HierarchyNodeKey.js";
/** @internal */
export async function createPropertyGroups(nodesToGroup, nodesAlreadyGrouped, handlerGroupingParams, valueFormatter, localizedStrings, classHierarchyInspector) {
let otherValuesGrouping;
const getOtherValuesGroupingNode = () => {
if (!otherValuesGrouping) {
const node = nodesAlreadyGrouped.find((n) => HierarchyNodeKey.isPropertyOtherValuesGrouping(n.key));
if (node) {
otherValuesGrouping = { node, new: false };
}
}
otherValuesGrouping ??= {
node: {
key: {
type: "property-grouping:other",
properties: [],
},
parentKeys: [],
groupedInstanceKeys: [],
label: localizedStrings.other,
children: [],
},
new: true,
};
return otherValuesGrouping.node;
};
const releaseMainThread = createMainThreadReleaseOnTimePassedHandler();
const groupings = { ungrouped: [], grouped: new Map() };
for (const node of nodesToGroup) {
await releaseMainThread();
const byProperties = node.processingParams?.grouping?.byProperties;
if (!byProperties) {
groupings.ungrouped.push(node);
continue;
}
if (!(await shouldCreatePropertyGroup(handlerGroupingParams, byProperties, node.key.instanceKeys[0].className, classHierarchyInspector))) {
groupings.ungrouped.push(node);
continue;
}
const currentProperty = byProperties.propertyGroups[handlerGroupingParams.previousPropertiesGroupingInfo.length];
const propertyIdentifier = {
propertyName: currentProperty.propertyName,
propertyClassName: byProperties.propertiesClassName,
};
const propertyClass = handlerGroupingParams.ecClass;
const property = await propertyClass.getProperty(currentProperty.propertyName);
if (!property?.isNavigation() && (!property?.isPrimitive() || property.primitiveType === "Binary" || property.primitiveType === "IGeometry")) {
groupings.ungrouped.push(node);
continue;
}
const extendedTypeName = "extendedTypeName" in property ? property.extendedTypeName : undefined;
const primitiveType = "primitiveType" in property ? property.primitiveType : "String";
assert(primitiveType !== "Binary" && primitiveType !== "IGeometry");
if (!currentProperty.propertyValue) {
if (byProperties.createGroupForUnspecifiedValues) {
addGroupingToMap(groupings.grouped, `${currentProperty.propertyName}:Unspecified`, {
label: localizedStrings.unspecified,
propertyGroupingNodeKey: {
type: "property-grouping:value",
...propertyIdentifier,
formattedPropertyValue: "",
},
}, node);
continue;
}
groupings.ungrouped.push(node);
continue;
}
const koqName = (await property.kindOfQuantity)?.fullName;
if (currentProperty.ranges) {
if (typeof currentProperty.propertyValue === "number") {
const propValue = currentProperty.propertyValue;
const matchingRange = currentProperty.ranges.find((range) => propValue >= range.fromValue && propValue <= range.toValue);
if (matchingRange) {
const fromValueTypedPrimitive = {
type: Number.isInteger(matchingRange.fromValue) ? "Integer" : "Double",
extendedType: extendedTypeName,
koqName,
value: matchingRange.fromValue,
};
const toValueTypedPrimitive = {
type: Number.isInteger(matchingRange.toValue) ? "Integer" : "Double",
extendedType: extendedTypeName,
koqName,
value: matchingRange.toValue,
};
const rangeLabel = matchingRange.rangeLabel ?? `${await valueFormatter(fromValueTypedPrimitive)} - ${await valueFormatter(toValueTypedPrimitive)}`;
addGroupingToMap(groupings.grouped, `${currentProperty.propertyName}:[${matchingRange.fromValue}-${matchingRange.toValue}]${matchingRange.rangeLabel ? `(${matchingRange.rangeLabel})` : ""}`, {
label: rangeLabel,
propertyGroupingNodeKey: {
...propertyIdentifier,
type: "property-grouping:range",
fromValue: matchingRange.fromValue,
toValue: matchingRange.toValue,
},
}, node);
continue;
}
}
if (byProperties.createGroupForOutOfRangeValues) {
const groupingNode = getOtherValuesGroupingNode();
assert(HierarchyNodeKey.isPropertyOtherValuesGrouping(groupingNode.key));
const thisPropertyIdentifier = {
className: propertyIdentifier.propertyClassName,
propertyName: propertyIdentifier.propertyName,
};
const hasPropertyIdentifier = groupingNode.key.properties.find((x) => x.className === thisPropertyIdentifier.className && x.propertyName === thisPropertyIdentifier.propertyName);
if (!hasPropertyIdentifier) {
groupingNode.key.properties.push(thisPropertyIdentifier);
}
node.key.instanceKeys.forEach((k) => groupingNode.groupedInstanceKeys.push(k));
groupingNode.parentKeys = node.parentKeys;
groupingNode.children.push(Object.assign(node, { parentKeys: [...node.parentKeys, groupingNode.key] }));
continue;
}
groupings.ungrouped.push(node);
continue;
}
const formattedValue = currentProperty.propertyValue instanceof Array
? await formatConcatenatedValue({ value: currentProperty.propertyValue, valueFormatter })
: await valueFormatter(TypedPrimitiveValue.create(currentProperty.propertyValue, primitiveType, koqName, extendedTypeName));
addGroupingToMap(groupings.grouped, `${currentProperty.propertyName}:${formattedValue}`, {
label: formattedValue,
propertyGroupingNodeKey: {
...propertyIdentifier,
type: "property-grouping:value",
formattedPropertyValue: formattedValue,
},
}, node);
}
return createGroupingNodes(groupings, otherValuesGrouping?.new ? otherValuesGrouping.node : undefined);
}
function addGroupingToMap(groupingMap, mapKey, propertyToAdd, node) {
let groupingInfo = groupingMap.get(mapKey);
if (!groupingInfo) {
groupingInfo = {
displayablePropertyGroupingInfo: propertyToAdd,
groupedNodes: [node],
};
groupingMap.set(mapKey, groupingInfo);
return;
}
groupingInfo.groupedNodes.push(node);
}
function createGroupingNodes(groupings, otherValuesGroupingNode) {
const groupedNodes = new Array();
groupings.grouped.forEach((entry) => {
const groupedNodeParentKeys = entry.groupedNodes[0].parentKeys;
groupedNodes.push({
label: entry.displayablePropertyGroupingInfo.label,
key: entry.displayablePropertyGroupingInfo.propertyGroupingNodeKey,
parentKeys: groupedNodeParentKeys,
groupedInstanceKeys: entry.groupedNodes.flatMap((groupedInstanceNode) => groupedInstanceNode.key.instanceKeys),
children: entry.groupedNodes.map((gn) => ({
...gn,
parentKeys: [...groupedNodeParentKeys, entry.displayablePropertyGroupingInfo.propertyGroupingNodeKey],
})),
});
});
if (otherValuesGroupingNode) {
groupedNodes.push(otherValuesGroupingNode);
}
return { grouped: groupedNodes, ungrouped: groupings.ungrouped, groupingType: "property" };
}
function createNodePropertyGroupPathMatchers(node) {
if (!HierarchyNode.isPropertyGroupingNode(node)) {
return [];
}
const keys = [...node.parentKeys, node.key];
const propertyGroupingNodeKeys = [];
for (let i = keys.length - 1; i >= 0; --i) {
const key = keys[i];
if (HierarchyNodeKey.isPropertyGrouping(key)) {
propertyGroupingNodeKeys.push(key);
}
else {
break;
}
}
propertyGroupingNodeKeys.reverse();
return propertyGroupingNodeKeys.map((key) => {
switch (key.type) {
case "property-grouping:other":
return (x) => key.properties.some((p) => p.className === x.propertiesClassName && p.propertyName === x.propertyName && !!x.isRange);
case "property-grouping:range":
return (x) => key.propertyClassName === x.propertiesClassName && key.propertyName === x.propertyName;
case "property-grouping:value":
return (x) => key.propertyClassName === x.propertiesClassName && key.propertyName === x.propertyName;
}
});
}
/** @internal */
export async function getUniquePropertiesGroupInfo(schemaProvider, parentNode, nodes) {
const releaseMainThread = createMainThreadReleaseOnTimePassedHandler();
const parentPropertyGroupPath = parentNode ? createNodePropertyGroupPathMatchers(parentNode) : [];
const uniqueProperties = new Map();
for (const node of nodes) {
await releaseMainThread();
const byProperties = node.processingParams?.grouping?.byProperties;
if (!byProperties) {
continue;
}
let propertyGroupIndex = 0;
const previousPropertiesInfo = new Array();
for (const propertyGroup of byProperties.propertyGroups) {
const mapKeyRanges = getRangesAsString(propertyGroup.ranges);
const lastKey = previousPropertiesInfo.length > 0 ? previousPropertiesInfo[previousPropertiesInfo.length - 1].propertyGroupKey : "";
const propertyGroupKey = `${lastKey}:${propertyGroup.propertyName}(${mapKeyRanges})`;
const mapKey = `${byProperties.propertiesClassName}:${propertyGroupKey}`;
let isAlreadyGrouped = false;
if (!isAlreadyGrouped && parentPropertyGroupPath.length > 0 && propertyGroupIndex < parentPropertyGroupPath.length) {
const groupMatcher = parentPropertyGroupPath[propertyGroupIndex];
isAlreadyGrouped = groupMatcher({
propertiesClassName: byProperties.propertiesClassName,
propertyName: propertyGroup.propertyName,
isRange: !!propertyGroup.ranges,
});
}
if (!isAlreadyGrouped && !uniqueProperties.get(mapKey)) {
uniqueProperties.set(mapKey, {
ecClass: await getClass(schemaProvider, byProperties.propertiesClassName),
propertyGroup: {
propertyName: propertyGroup.propertyName,
ranges: propertyGroup.ranges,
},
previousPropertiesGroupingInfo: previousPropertiesInfo.map((groupingInfo) => ({
propertiesClassName: byProperties.propertiesClassName,
propertyName: groupingInfo.propertyGroup.propertyName,
isRange: !!groupingInfo.propertyGroup.ranges,
})),
});
}
previousPropertiesInfo.push({
propertyGroup,
propertyGroupKey,
});
++propertyGroupIndex;
}
}
// Order might change in uniqueProperties, resorting to make sure that properties with fewer previous properties are returned first.
return [...uniqueProperties.values()].sort((lhs, rhs) => lhs.previousPropertiesGroupingInfo.length - rhs.previousPropertiesGroupingInfo.length);
}
function getRangesAsString(ranges) {
return ranges
? ranges
.map((range) => `${range.fromValue}-${range.toValue}(${range.rangeLabel ?? ""})`)
.sort()
.join(";")
: "";
}
async function shouldCreatePropertyGroup(handlerGroupingParams, nodePropertyGroupingParams, nodeFullClassName, classHierarchyInspector) {
if (nodePropertyGroupingParams.propertiesClassName !== handlerGroupingParams.ecClass.fullName ||
nodePropertyGroupingParams.propertyGroups.length < handlerGroupingParams.previousPropertiesGroupingInfo.length + 1) {
return false;
}
const currentProperty = nodePropertyGroupingParams.propertyGroups[handlerGroupingParams.previousPropertiesGroupingInfo.length];
if (currentProperty.propertyName !== handlerGroupingParams.propertyGroup.propertyName ||
!doRangesMatch(currentProperty.ranges, handlerGroupingParams.propertyGroup.ranges)) {
return false;
}
if (!doPreviousPropertiesMatch(handlerGroupingParams.previousPropertiesGroupingInfo, nodePropertyGroupingParams)) {
return false;
}
return classHierarchyInspector.classDerivesFrom(nodeFullClassName, handlerGroupingParams.ecClass.fullName);
}
/** @internal */
export function doPreviousPropertiesMatch(previousPropertiesGroupingInfo, nodesProperties) {
return (previousPropertiesGroupingInfo.length <= nodesProperties.propertyGroups.length &&
previousPropertiesGroupingInfo.every((groupingInfo, index) => groupingInfo.propertiesClassName === nodesProperties.propertiesClassName &&
groupingInfo.propertyName === nodesProperties.propertyGroups[index].propertyName &&
!!groupingInfo.isRange === !!nodesProperties.propertyGroups[index].ranges));
}
/** @internal */
export function doRangesMatch(ranges1, ranges2) {
if (typeof ranges1 !== typeof ranges2) {
return false;
}
if (!ranges1) {
return true;
}
assert(Array.isArray(ranges2));
if (ranges1.length !== ranges2.length) {
return false;
}
return (
// Check twice, to validate if both ranges have the same elements (elements can be in a different order)
ranges1.every((lhsRange) => ranges2.some((rhsRange) => lhsRange.fromValue === rhsRange.fromValue && lhsRange.toValue === rhsRange.toValue && lhsRange.rangeLabel === rhsRange.rangeLabel)) &&
ranges2.every((lhsRange) => ranges1.some((rhsRange) => lhsRange.fromValue === rhsRange.fromValue && lhsRange.toValue === rhsRange.toValue && lhsRange.rangeLabel === rhsRange.rangeLabel)));
}
/** @internal */
export async function createPropertiesGroupingHandlers(imodelAccess, parentNode, nodes, valueFormatter, localizedStrings) {
const propertiesGroupInfo = await getUniquePropertiesGroupInfo(imodelAccess, parentNode, nodes);
return propertiesGroupInfo.map((propertyInfo) => async (nodesToGroup, nodesAlreadyGrouped) => createPropertyGroups(nodesToGroup, nodesAlreadyGrouped, propertyInfo, valueFormatter, localizedStrings, imodelAccess));
}
//# sourceMappingURL=PropertiesGrouping.js.map