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

Add Feature Tracking for Measure-Tools #1026

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions packages/itwin/measure-tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,72 @@ An application can further customize UI event behavior by registering override h

A concrete example of this customization is an application that has measurements organized into multiple groups. One group may be "frozen" due to some application state (state that the measure-tools library may be unaware of) and should not be cleared by the clear measurements tool.
So the application would register a custom UI event handler that would cause those measurements to be ignored when the clear measurement tool is invoked.

### Usage Tracking

This package allows consumers to track the usage of specific features.

This can be achieved by passing `onFeatureUsed` function to `MeasureToolsUiProvider`. The function is invoked with feature message as the feature is being used. The message is under this format:

`feature-{feature being used}-event-{specific event related to the feature}`

List of tracked features and their respective events:

**Measure Distance**:

- `"feature-measure-distance-event-trigger"` - when measure distance feature starts being used

- `"feature-measure-distance-event-cancel"` - when measure distance feature stops being used

- `"feature-measure-distance-event-start-point"` - when start point of the distance is registered

- `"feature-measure-distance-event-end-point"` - when end point of the distance is registered

**Clear Measurements**:

- `"feature-clear-measurements-event-trigger"` - when clear measurements is triggered and clear all measurements on the screen

**Property Widget**:

- `"feature-property-widget-event-no-data"` - when the property widget is empty / not showed on the widget panel

- `"feature-property-widget-event-update"` - when the property widget is updated with newest measurements results

**Selection Set**:

- `"feature-selection-set-event-change"` - when the selection set is updated with newly selected element(s) in the model

### Example for Usage Tracking

In this case, we create a sample [Itwin Viewer](https://www.npmjs.com/package/@itwin/web-viewer-react) and configure `MeasureToolsUiProvider`

```ts
import { MeasureToolsUiItemsProvider } from "@itwin/measure-tools-react";

const App: React.FC = () => {
// Viewer Setup here...
return (
<div className="viewer-container">
<Viewer
iTwinId={iTwinId ?? ""}
iModelId={iModelId ?? ""}
changeSetId={changesetId}
authClient={authClient}
viewCreatorOptions={viewCreatorOptions}
enablePerformanceMonitors={true}
onIModelAppInit={onIModelAppInit}
uiProviders={[
new MeasureToolsUiItemsProvider({
onFeatureUsed: (feature) => {
console.log(`MeasureTools [${feature}] used`);
},
}),
]}
/>
</div>
);
};

export default App;
```

19 changes: 17 additions & 2 deletions packages/itwin/measure-tools/src/api/MeasurementTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,16 @@ export class SelectionHolder {

/** Useful base class for tools */
export abstract class PrimitiveToolBase extends PrimitiveTool {
private _onFeatureUsed?: (feature: string) => void;

protected get feature(): Feature | undefined {
return undefined;
}

protected get onFeatureUsed(): ((feature: string) => void) | undefined {
return this._onFeatureUsed;
}

public override async onPostInstall(): Promise<void> {
await super.onPostInstall();

Expand Down Expand Up @@ -177,6 +183,15 @@ export abstract class PrimitiveToolBase extends PrimitiveTool {
);
}

protected handleFeature(feature: string) {
this.onFeatureUsed?.(feature);
}

constructor(onFeatureUsed?: (feature: string) => void) {
super();
this._onFeatureUsed = onFeatureUsed;
}

// For most of our cases we expect to draw our decorations when suspended also
public override decorateSuspended(context: DecorateContext): void {
this.decorate(context);
Expand Down Expand Up @@ -216,8 +231,8 @@ export abstract class MeasurementToolBase<
return true;
}

constructor() {
super();
constructor(onFeatureUsed?: (feature: string) => void) {
super(onFeatureUsed);

this._toolModel = this.createToolModel();
this._toolModel.synchMeasurementsWithSelectionSet = true; // Sync by default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ export class ClearMeasurementsTool extends PrimitiveToolBase {
return MeasureToolsFeatures.Tools_ClearMeasurements;
}

constructor() {
super();
constructor(onFeatureUsed?: (feature: string) => void) {
super(onFeatureUsed);
}

public override requireWriteableTarget(): boolean {
Expand All @@ -53,7 +53,7 @@ export class ClearMeasurementsTool extends PrimitiveToolBase {

public override async onPostInstall(): Promise<void> {
await super.onPostInstall();

this.handleFeature("feature-clear-measurements-event-trigger");
// NOTE: If we were laying out measurements in a tool, by virtue of how tools run, those measurements will have persisted by the time
// we install this clear tool

Expand Down
12 changes: 10 additions & 2 deletions packages/itwin/measure-tools/src/tools/MeasureDistanceTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,19 @@ MeasureDistanceToolModel
return MeasureToolsFeatures.Tools_MeasureDistance;
}

constructor(enableSheetMeasurements = false) {
super();
constructor(enableSheetMeasurements = false, onFeatureUsed?: (feature: string) => void) {
super(onFeatureUsed);
this._enableSheetMeasurements = enableSheetMeasurements;
}

public override async onPostInstall(): Promise<void> {
await super.onPostInstall();
this.handleFeature("feature-measure-distance-event-trigger");
}

public override async onCleanup(): Promise<void> {
await super.onCleanup();
this.handleFeature("feature-measure-distance-event-cancel");
}

public async onRestartTool(): Promise<void> {
Expand All @@ -89,13 +95,15 @@ MeasureDistanceToolModel
this.toolModel.currentState
) {
this.toolModel.setMeasurementViewport(viewType);
this.handleFeature("feature-measure-distance-event-start-point");
this.toolModel.setStartPoint(viewType, ev.point);
await this.sheetMeasurementsDataButtonDown(ev);
this._sendHintsToAccuDraw(ev);
this.updateToolAssistance();
} else if (
MeasureDistanceToolModel.State.SetEndPoint === this.toolModel.currentState
) {
this.handleFeature("feature-measure-distance-event-end-point");
this.toolModel.setEndPoint(viewType, ev.point, false);
await this.onReinitialize();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ export class MeasureToolDefinitions {
});
}

public static getMeasureDistanceToolCommand(enableSheetMeasurements: boolean) {
public static getMeasureDistanceToolCommand(enableSheetMeasurements: boolean, onFeatureUsed?: (feature: string) => void) {
return new ToolItemDef({
toolId: MeasureDistanceTool.toolId,
iconSpec: MeasureDistanceTool.iconSpec,
label: () => MeasureDistanceTool.flyover,
tooltip: () => MeasureDistanceTool.description,
execute: () => {
const tool = new MeasureDistanceTool(enableSheetMeasurements);
const tool = new MeasureDistanceTool(enableSheetMeasurements, onFeatureUsed);
void tool.run();
},
});
Expand Down Expand Up @@ -104,15 +104,16 @@ export class MeasureToolDefinitions {
});
}

public static get clearMeasurementsToolCommand() {
public static getClearMeasurementsToolCommand(onFeatureUsed?: (feature: string) => void) {
return new ToolItemDef({
toolId: ClearMeasurementsTool.toolId,
iconSpec: ClearMeasurementsTool.iconSpec,
isHidden: !MeasurementUIEvents.isClearMeasurementButtonVisible,
label: () => ClearMeasurementsTool.flyover,
tooltip: () => ClearMeasurementsTool.description,
execute: () => {
void IModelApp.tools.run(ClearMeasurementsTool.toolId);
const tool = new ClearMeasurementsTool(onFeatureUsed);
void tool.run();
},
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,17 @@ export interface MeasureToolsUiProviderOptions {
};
// If we check for sheet to 3d transformation when measuring in sheets
enableSheetMeasurement?: boolean;
// Callback that is invoked when a tracked feature is used.
onFeatureUsed?: (feature: string) => void;
stageUsageList?: string[];
}


export class MeasureToolsUiItemsProvider implements UiItemsProvider {
public readonly id = "MeasureToolsUiItemsProvider";
private _props: RecursiveRequired<MeasureToolsUiProviderOptions>;
private _props: Omit<RecursiveRequired<MeasureToolsUiProviderOptions>, 'onFeatureUsed'> & {
onFeatureUsed?: (feature: string) => void;
};

constructor(props?: MeasureToolsUiProviderOptions) {
this._props = {
Expand All @@ -48,6 +53,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider {
},
enableSheetMeasurement: props?.enableSheetMeasurement ?? false,
stageUsageList: props?.stageUsageList ?? [StageUsage.General],
onFeatureUsed: props?.onFeatureUsed,
};
}

Expand All @@ -61,7 +67,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider {
const featureFlags = MeasureTools.featureFlags;
const tools: ToolItemDef[] = [];
if (!featureFlags?.hideDistanceTool) {
tools.push(MeasureToolDefinitions.getMeasureDistanceToolCommand(this._props.enableSheetMeasurement));
tools.push(MeasureToolDefinitions.getMeasureDistanceToolCommand(this._props.enableSheetMeasurement, this._props?.onFeatureUsed));
}
if (!featureFlags?.hideAreaTool) {
tools.push(MeasureToolDefinitions.getMeasureAreaToolCommand(this._props.enableSheetMeasurement));
Expand Down Expand Up @@ -104,7 +110,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider {
return [
ToolbarHelper.createToolbarItemFromItemDef(
100,
MeasureToolDefinitions.clearMeasurementsToolCommand,
MeasureToolDefinitions.getClearMeasurementsToolCommand(this._props?.onFeatureUsed),
{
isHidden: new ConditionalBooleanValue(
() => isSheetViewActive() || !MeasurementUIEvents.isClearMeasurementButtonVisible,
Expand Down Expand Up @@ -137,7 +143,7 @@ export class MeasureToolsUiItemsProvider implements UiItemsProvider {
widgets.push({
id: MeasurementPropertyWidgetId,
label: MeasureTools.localization.getLocalizedString("MeasureTools:Generic.measurements"),
content: <MeasurementPropertyWidget />,
content: <MeasurementPropertyWidget onFeatureUsed={this._props?.onFeatureUsed} />,
defaultState: WidgetState.Hidden,
icon: "icon-measure",
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ import { SvgCopy } from "@itwin/itwinui-icons-react";
import { IconButton } from "@itwin/itwinui-react";
import type { PrimitiveValue } from "@itwin/appui-abstract";

/**
* Props for `MeasureTools`
* @public
*/
export interface MeasureToolsProps {
onFeatureUsed?: (feature: string) => void;
}

export function useSpecificWidgetDef(id: string) {
const frontstageDef = useActiveFrontstageDef();
return frontstageDef?.findWidgetDef(id);
Expand All @@ -30,7 +38,7 @@ export function useSpecificWidgetDef(id: string) {
export const MeasurementPropertyWidgetId = "measure-tools-property-widget";

// eslint-disable-next-line @typescript-eslint/naming-convention
export const MeasurementPropertyWidget = () => {
export const MeasurementPropertyWidget = (props: MeasureToolsProps) => {
const activeIModelConnection = useActiveIModelConnection();
const [dataProvider] = React.useState(new SimplePropertyDataProvider());
const [lastSelectedCount, setLastSelectedCount] = React.useState(MeasurementSelectionSet.global.measurements.length);
Expand Down Expand Up @@ -112,10 +120,11 @@ export const MeasurementPropertyWidget = () => {

// addProperty will raise onDataChanged. If we have no data, raise it ourselves.
if (!data.length) {
props.onFeatureUsed?.("feature-property-widget-event-no-data");
dataProvider.onDataChanged.raiseEvent();
return;
}

props.onFeatureUsed?.("feature-property-widget-event-update");
// Reverse the order. Last selected measurement should display up top.
data = data.reverse();
transientIds = transientIds.reverse();
Expand All @@ -130,6 +139,7 @@ export const MeasurementPropertyWidget = () => {
};

const onSelectionChanged = async (args: MeasurementSelectionSetEvent | Measurement[]) => {
props.onFeatureUsed?.("feature-selection-set-event-change");
// Only collapse if we are adding/removing more than one at once
let collapseAll: boolean;
if (Array.isArray(args)) {
Expand Down
Loading