@itwin/measure-tools-react
Version:
Frontend framework and tools for measurements
171 lines • 8.73 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 "./MeasurementPropertyWidget.scss";
import * as React from "react";
import { IModelApp, SelectionSetEventType } from "@itwin/core-frontend";
import { PropertyRecord, PropertyValueFormat } from "@itwin/appui-abstract";
import { useActiveFrontstageDef, useActiveIModelConnection, WidgetState } from "@itwin/appui-react";
import { SimplePropertyDataProvider, VirtualizedPropertyGridWithDataProvider } from "@itwin/components-react";
import { Orientation, ResizableContainerObserver } from "@itwin/core-react";
import { MeasurementSelectionSet } from "../api/MeasurementSelectionSet.js";
import { MeasurementUIEvents } from "../api/MeasurementUIEvents.js";
import { MeasureTools } from "../MeasureTools.js";
import { useCallback, useState } from "react";
import { SvgCopy } from "@itwin/itwinui-icons-react";
import { IconButton } from "@itwin/itwinui-react";
export function useSpecificWidgetDef(id) {
const frontstageDef = useActiveFrontstageDef();
return frontstageDef?.findWidgetDef(id);
}
export const MeasurementPropertyWidgetId = "measure-tools-property-widget";
// eslint-disable-next-line @typescript-eslint/naming-convention
export const MeasurementPropertyWidget = () => {
const activeIModelConnection = useActiveIModelConnection();
const [dataProvider] = React.useState(new SimplePropertyDataProvider());
const [lastSelectedCount, setLastSelectedCount] = React.useState(MeasurementSelectionSet.global.measurements.length);
const [{ width, height }, setSize] = useState({ width: 0, height: 0 });
const createPropertyRecord = (displayLabel, value) => {
const propValue = { valueFormat: PropertyValueFormat.Primitive, displayValue: value, value };
const propDescription = { displayLabel, name: displayLabel, typename: "string" };
const record = new PropertyRecord(propValue, propDescription);
record.isDisabled = false;
record.isReadonly = true;
return record;
};
const addAggregateProperties = (data) => {
const orderedAggrPropEntries = new Array();
const aggregateIndices = new Map();
let hasAtLeastTwoInstances = false;
// Scan through all the measurement data, collect any properties that we can aggregate
for (const dataEntry of data) {
for (const prop of dataEntry.properties) {
if (!prop.aggregatableValue) {
continue;
}
const aggrIndex = aggregateIndices.get(prop.name);
if (aggrIndex === undefined) {
aggregateIndices.set(prop.name, orderedAggrPropEntries.length);
orderedAggrPropEntries.push({ label: prop.label, prop: { ...prop.aggregatableValue } });
}
else {
const aggrProp = orderedAggrPropEntries[aggrIndex];
if (aggrProp.prop.formatSpec === prop.aggregatableValue.formatSpec) {
aggrProp.prop.value += prop.aggregatableValue.value;
hasAtLeastTwoInstances = true;
}
}
}
}
// Want to show if either: two types or one type with at least two instances
if (data.length > 1 &&
(orderedAggrPropEntries.length >= 2 || (orderedAggrPropEntries.length === 1 && hasAtLeastTwoInstances))) {
const catIndex = dataProvider.addCategory({
expand: true,
label: MeasureTools.localization.getLocalizedString("MeasureTools:Generic.cumulativeTotals"),
name: "cumulativeTotals",
});
for (const entry of orderedAggrPropEntries) {
const label = entry.label;
const aggrProp = entry.prop;
const formattedValue = IModelApp.quantityFormatter.formatQuantity(aggrProp.value, aggrProp.formatSpec);
dataProvider.addProperty(createPropertyRecord(label, formattedValue), catIndex);
}
return true;
}
return false;
};
const getData = async (collapseAll = false) => {
dataProvider.categories = [];
dataProvider.records = {};
let data = [];
let transientIds = [];
for (const measurement of MeasurementSelectionSet.global) {
const mData = await measurement.getDataForMeasurementWidget();
if (mData?.properties.length && measurement.transientId) {
data.push(mData);
transientIds.push(measurement.transientId);
}
}
// addProperty will raise onDataChanged. If we have no data, raise it ourselves.
if (!data.length) {
dataProvider.onDataChanged.raiseEvent();
return;
}
// Reverse the order. Last selected measurement should display up top.
data = data.reverse();
transientIds = transientIds.reverse();
addAggregateProperties(data);
for (let i = 0; i < data.length; ++i) {
const isExpanded = collapseAll ? false : i === 0;
const catIndex = dataProvider.addCategory({ expand: isExpanded, label: data[i].title, name: transientIds[i] });
data[i].properties.map((kv) => dataProvider.addProperty(createPropertyRecord(kv.label, kv.value), catIndex));
}
};
const onSelectionChanged = async (args) => {
// Only collapse if we are adding/removing more than one at once
let collapseAll;
if (Array.isArray(args)) {
// Property value update only
collapseAll = false;
}
else {
// Selection event
switch (args.type) {
case SelectionSetEventType.Add:
case SelectionSetEventType.Replace:
collapseAll = args.added.length !== 1;
break;
case SelectionSetEventType.Remove:
collapseAll = args.removed.length !== 1;
break;
default:
collapseAll = true;
}
}
const selectCount = MeasurementSelectionSet.global.measurements.length;
setLastSelectedCount(selectCount);
await getData(collapseAll);
};
React.useLayoutEffect(() => {
const remover1 = MeasurementSelectionSet.global.onChanged.addListener(onSelectionChanged);
const remover2 = MeasurementUIEvents.onMeasurementPropertiesChanged.addListener(onSelectionChanged);
getData(lastSelectedCount >= 2).catch(() => {
/* no op */
});
return () => {
remover1();
remover2();
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
React.useEffect(() => {
if (!activeIModelConnection)
return;
return activeIModelConnection.onGlobalOriginChanged.addListener(async () => {
await getData(false);
});
}, [activeIModelConnection]); // eslint-disable-line react-hooks/exhaustive-deps
const widgetDef = useSpecificWidgetDef(MeasurementPropertyWidgetId);
React.useEffect(() => {
if (lastSelectedCount) {
widgetDef?.setWidgetState(WidgetState.Open);
}
else {
widgetDef?.setWidgetState(WidgetState.Hidden);
}
}, [widgetDef, lastSelectedCount]);
const handleResize = useCallback((w, h) => {
setSize({ width: w, height: h });
}, []);
const copyButton = React.useCallback((props) => props.isPropertyHovered && (React.createElement(IconButton, { styleType: "borderless", onClick: () => {
const value = props.property.value;
if (value !== undefined && value.hasOwnProperty("displayValue"))
navigator.clipboard.writeText(value.displayValue ?? "").catch((_) => { });
} },
React.createElement(SvgCopy, null))), []);
return (React.createElement("div", { className: "measure-tools-property-widget-container" },
React.createElement(ResizableContainerObserver, { onResize: handleResize },
React.createElement(VirtualizedPropertyGridWithDataProvider, { dataProvider: dataProvider, orientation: Orientation.Vertical, height: height, width: width, isPropertyHoverEnabled: true, actionButtonRenderers: [copyButton] }))));
};
//# sourceMappingURL=MeasurementPropertyWidget.js.map