@itwin/measure-tools-react
Version:
Frontend framework and tools for measurements
408 lines • 19.1 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 { BeUiEvent } from "@itwin/core-bentley";
import { EventHandled, IModelApp } from "@itwin/core-frontend";
import { MeasurementPickContext } from "./Measurement.js";
import { MeasurementCachedGraphicsHandler } from "./MeasurementCachedGraphicsHandler.js";
import { MeasurementButtonHandledEvent, WellKnownViewType } from "./MeasurementEnums.js";
import { MeasurementSelectionSet } from "./MeasurementSelectionSet.js";
import { MeasurementUIEvents } from "./MeasurementUIEvents.js";
import { ShimFunctions } from "./ShimFunctions.js";
/**
* Singleton manager which maintains a list of all active measurements once they are created by a measurement tool. The manager facilitates drawing and picking of measurements
* to the appropriate viewport.
*/
export class MeasurementManager {
constructor() {
this._measurements = new Array();
/** Event that is invoked when a measurement has responded to a button event. */
this.onMeasurementButtonEvent = new BeUiEvent();
/** Event that is invoked when a measurement is added. */
this.onMeasurementsAdded = new BeUiEvent();
/** Event that is invoked when a measurement is dropped or cleared. */
this.onMeasurementsRemoved = new BeUiEvent();
}
/** Gets the manager instance. */
static get instance() {
if (!this._instance)
this._instance = new MeasurementManager();
return this._instance;
}
/** Gets a readonly array of measurements the manager owns. This does not include any transient measurements an active tool is displaying. */
get measurements() {
return this._measurements;
}
/** Gets or sets an override tooltip handler. If defined, this overrides what is returned in getDecorationToolTip. */
get overrideToolTipHandler() {
return this._overrideToolTipHandler;
}
set overrideToolTipHandler(handler) {
this._overrideToolTipHandler = handler;
}
/** Gets or sets an override geometry handler. If defined, this overrides what is returned in getDecorationGeometry. */
get overrideGeometryHandler() {
return this._overrideGeometryHandler;
}
set overrideGeometryHandler(handler) {
this._overrideGeometryHandler = handler;
}
/** Gets or sets an override hit handler. If defined, this overrides the logic in testDecorationHit. */
get overrideHitHandler() {
return this._overrideHitHandler;
}
set overrideHitHandler(handler) {
this._overrideHitHandler = handler;
}
/** Adds one or more measurements to the manager.
* @param measurement one or more measurements to add.
*/
addMeasurement(measurement) {
const arr = Array.isArray(measurement) ? measurement : [measurement];
this._measurements.push(...arr);
this.onMeasurementsAdded.emit(arr);
MeasurementUIEvents.notifyMeasurementsChanged();
this.invalidateDecorationsAllViews();
}
/**
* Iterates over all measurements owned by the manager.
* @param callback Callback to invoke for each measurement, return true to keep iterating or false to early out.
*/
forAllMeasurements(callback) {
this._measurements.every(callback);
}
/**
* Queries measurements that can be drawn in a given view type.
* @param viewType view type to find measurements for. Can be any [[WellKnownViewType]] or an app-defined one.
* @returns an array of measurements that are valid for the view type or an empty array if none were found.
*/
getMeasurementsForViewType(viewType) {
if (viewType === WellKnownViewType.Any)
return this._measurements.slice();
return this.getMeasurementsForPredicate((measurement) => {
return measurement.viewTarget.isOfViewType(viewType);
});
}
/**
* Queries measurements that can be drawn in a given viewport.
* @param vp Viewport to find measurements for.
* @returns an array of measurements that are valid for the viewport or an empty array if none were found.
*/
getMeasurementsForViewport(vp) {
return this.getMeasurementsForPredicate((measurement) => {
return measurement.viewTarget.isViewportCompatible(vp);
});
}
/** Queries measurements that belong to the group, and optionally subgroup.
* @param groupId ID of the group
* @param subgroupId Optional ID of the subgroup
* @returns an array of measurements that belong to the specified group.
*/
getMeasurementsForGroup(groupId, subgroupId) {
return this.getMeasurementsForPredicate((measurement) => {
// If subgroupId is still undefined, we want to include that as a hit
if (measurement.groupId === groupId)
return measurement.subgroupId === subgroupId;
return false;
});
}
/** Queries measurements based on a user-defined predicate.
* @param callback Defines the criteria for what the measurement needs to satisfy to be returned.
* @returns an array of measurements that satisfy the predicate.
*/
getMeasurementsForPredicate(callback) {
return this._measurements.filter(callback);
}
/** Removes one or more measurements from the manager.
* @param measurement one or more measurements to remove.
* @returns true if the measurements were dropped, false if none were found.
*/
dropMeasurement(measurement) {
const keepMeasurements = [];
const removed = [];
// For each measurement we own, find it in the drop list. If not in the drop list, add it to a new list of kept measurements
const arr = Array.isArray(measurement) ? measurement : [measurement];
for (const elem of this._measurements) {
const index = arr.indexOf(elem);
if (index > -1) {
removed.push(elem); // In list, so we found something to drop
elem.onCleanup();
}
else {
keepMeasurements.push(elem); // Not in list, add to keeps
}
}
if (0 === removed.length)
return false;
this._measurements = keepMeasurements;
this.onMeasurementsRemoved.emit(removed);
MeasurementSelectionSet.global.remove(measurement);
MeasurementUIEvents.notifyMeasurementsChanged();
this.invalidateDecorationsAllViews();
return true;
}
/**
* Removes any measurements from the manager based on the view type.
* @param viewType view type to find measurements for. Can be any [[WellKnownViewType]] or an app-defined one.
* @returns an array of measurements that were dropped, or empty if none were.
*/
dropMeasurementsForViewType(viewType) {
if (viewType === WellKnownViewType.Any) {
const dropped = this._measurements;
this.clear(true);
return dropped;
}
return this.dropMeasurementsForPredicate((measurement) => {
return measurement.viewTarget.isOfViewType(viewType);
});
}
/** Removes any measurements from the manager for a given viewport.
* @param vp Viewport to find measurements to drop for.
* @returns an array of measurements that were dropped, or empty if none were.
*/
dropMeasurementForViewport(vp) {
return this.dropMeasurementsForPredicate((measurement) => {
return measurement.viewTarget.isViewportCompatible(vp);
});
}
/** Removes one or more measurements that belong to the given group.
* @param groupId ID of the group.
* @param subgroupId Optional ID of the subgroup.
* @returns an array of measurements removed from the decorator.
*/
dropMeasurementsForGroup(groupId, subgroupId) {
return this.dropMeasurementsForPredicate((measurement) => {
// If subgroupId is still undefined, we want to include that as a hit
if (measurement.groupId === groupId)
return measurement.subgroupId === subgroupId;
return false;
});
}
/** Removes one or more measurements that satisfy the predicate.
* @param callback Defines the criteria for whether or not the measurement should be removed.
* @returns an array of measurements removed from the decorator.
*/
dropMeasurementsForPredicate(callback) {
const measurementsKept = [];
const measurementsDropped = [];
for (const elem of this._measurements) {
if (callback(elem)) {
measurementsDropped.push(elem);
elem.onCleanup();
}
else {
measurementsKept.push(elem);
}
}
this._measurements = measurementsKept;
if (measurementsDropped.length > 0) {
this.onMeasurementsRemoved.emit(measurementsDropped);
MeasurementSelectionSet.global.remove(measurementsDropped);
MeasurementUIEvents.notifyMeasurementsChanged();
this.invalidateDecorationsAllViews();
}
return measurementsDropped;
}
/** Clears measurements from the manager.
* @param clearLocked true if locked measurements should be cleared as well as non-locked, false to not clear locked measurements.
*/
clear(clearLocked = true) {
const temp = new Array();
let removed;
let count = 0;
// If clear locked, then clearing all
if (clearLocked) {
count = this._measurements.length;
removed = this._measurements;
}
else {
removed = [];
// Otherwise, clear non-locked by adding locked to the new array
for (const measurement of this._measurements) {
if (measurement.isLocked) {
temp.push(measurement);
}
else {
count++;
removed.push(measurement);
}
}
}
this._measurements = temp;
// If removed anything, notify UI
if (count > 0) {
// Call cleanup on any measurement to be removed
for (const m of removed)
m.onCleanup();
this.onMeasurementsRemoved.emit(removed);
MeasurementSelectionSet.global.clear();
MeasurementUIEvents.notifyMeasurementsChanged();
this.invalidateDecorationsAllViews();
}
}
/** Tests if the pick ID belongs to any measurement.
* @param id pick ID used by graphics the measurement generates for drawing.
* @returns true if the measurement has been picked, false otherwise.
*/
testDecorationHit(id) {
const pickContext = MeasurementPickContext.createFromSourceId(id);
if (this._overrideHitHandler) {
for (const measurement of this._measurements) {
if (measurement.isVisible && this._overrideHitHandler(measurement, pickContext))
return true;
}
}
else {
for (const measurement of this._measurements) {
if (measurement.isVisible && measurement.testDecorationHit(pickContext))
return true;
}
}
return false;
}
/** Get a geometry stream representing the pickable geometry of any measurement currently picked. Usually this is simplier geometry than what is drawn.
* @param hit Current picking context.
* @returns a geometry stream of pickable data or undefined.
*/
getDecorationGeometry(hit) {
const pickContext = MeasurementPickContext.create(hit);
for (const measurement of this._measurements) {
if (measurement.isVisible && measurement.testDecorationHit(pickContext))
return (this._overrideGeometryHandler) ? this._overrideGeometryHandler(measurement, pickContext) : measurement.getDecorationGeometry(pickContext);
}
return undefined;
}
/** Get a tooltip for any measurement currently picked.
* @param hit Current picking context.
* @returns a tooltip HTML element or string.
*/
async getDecorationToolTip(hit) {
const pickContext = MeasurementPickContext.create(hit);
for (const measurement of this._measurements) {
if (measurement.isVisible && measurement.testDecorationHit(pickContext))
return (this._overrideToolTipHandler) ? this._overrideToolTipHandler(measurement, pickContext) : measurement.getDecorationToolTip(pickContext);
}
return "";
}
/** Handles button events on any measurements that have been picked.
* @param hit Current picking context.
* @param ev Current button event.
* @returns enum whether the event has been handled or not.
*/
async onDecorationButtonEvent(hit, ev) {
const pickContext = MeasurementPickContext.create(hit, ev);
for (const measurement of this._measurements) {
if (!measurement.isVisible)
continue;
const handled = await measurement.onDecorationButtonEvent(pickContext);
// Early out if event was handled. Potentially check if we want to consume the event or not.
switch (handled) {
case MeasurementButtonHandledEvent.YesConsumeEvent:
return EventHandled.Yes;
case MeasurementButtonHandledEvent.Yes:
return EventHandled.No;
}
}
return EventHandled.No;
}
/**
* Notifies the event handler the measurement has responded to a button event.
* @param measurement Measurement that responded to the event.
* @param pickContext Current pick context.
*/
notifyMeasurementButtonEvent(measurement, pickContext) {
this.onMeasurementButtonEvent.emit({ measurement, pickContext });
}
/** Draws all valid measurements to a given viewport. Measurements that do not have the correct viewport type are not drawn to the viewport.
* @param context Decorate context for drawing to a viewport.
*/
decorate(context) {
this.tryAddGlobalOriginChangedListener(context.viewport.iModel);
for (const measurement of this._measurements) {
if (measurement.isVisible && measurement.viewTarget.isViewportCompatible(context.viewport))
measurement.decorate(context);
}
}
/** Draws all measurements that have cached graphics to a given viewport. Measurements that do not have the correct viewport type are not drawn to the viewport.
* @param context Decorate context for drawing to a viewport.
*/
decorateCached(context) {
this.tryAddGlobalOriginChangedListener(context.viewport.iModel);
for (const measurement of this._measurements) {
if (measurement.isVisible && measurement.viewTarget.isViewportCompatible(context.viewport))
measurement.decorateCached(context);
}
}
tryAddGlobalOriginChangedListener(iModel) {
if (this._iModelIdForGlobalOrigin !== iModel.iModelId) {
if (this._dropGlobalOriginChangedCallback)
this._dropGlobalOriginChangedCallback();
this._dropGlobalOriginChangedCallback = iModel.onGlobalOriginChanged.addListener(this.onActiveUnitSystemChanged, this);
this._iModelIdForGlobalOrigin = iModel.iModelId;
}
}
/** Invalidates decorations in all views, including any cached graphics measurements may be using. */
invalidateDecorationsAllViews() {
IModelApp.viewManager.invalidateDecorationsAllViews();
MeasurementCachedGraphicsHandler.instance.invalidateDecorations();
}
/** Invalidates decorations in a specified viewport. If undefined then all viewports are invalidated. This includes any cached graphics measurements may be using.
* @param vp Viewport to invalidate decorations, if undefined all viewports.
*/
invalidateDecorations(vp) {
if (vp) {
vp.invalidateDecorations();
MeasurementCachedGraphicsHandler.instance.invalidateDecorations(vp);
}
else {
this.invalidateDecorationsAllViews();
}
}
/** Adds the decorator singleton to the view manager's list of active decorators. The decorator will participate in drawing and picking operations. */
startDecorator() {
if (this._dropDecoratorCallback)
return;
MeasurementCachedGraphicsHandler.instance.setDecorateCallback(this.decorateCached.bind(this));
MeasurementCachedGraphicsHandler.instance.startDecorator();
this._dropDecoratorCallback = IModelApp.viewManager.addDecorator(this);
if (undefined === this._dropQuantityFormatterListeners) {
const unsubscribers = [IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener(this.onActiveUnitSystemChanged, this),
IModelApp.quantityFormatter.onQuantityFormatsChanged.addListener(this.onActiveUnitSystemChanged, this),
IModelApp.quantityFormatter.onUnitsProviderChanged.addListener(this.onActiveUnitSystemChanged, this)];
this._dropQuantityFormatterListeners = () => unsubscribers.forEach((unsubscriber) => {
unsubscriber();
});
}
else {
this.onActiveUnitSystemChanged();
}
}
/** Removes the decorator singleton from the view manager's list of active decorators. The decorator will still manage measurements, but will not
* participate in drawing or picking operations.
*/
stopDecorator() {
if (this._dropDecoratorCallback) {
this._dropDecoratorCallback();
this._dropDecoratorCallback = undefined;
}
if (this._dropQuantityFormatterListeners) {
this._dropQuantityFormatterListeners();
this._dropQuantityFormatterListeners = undefined;
}
MeasurementCachedGraphicsHandler.instance.setDecorateCallback(undefined);
MeasurementCachedGraphicsHandler.instance.stopDecorator();
}
onActiveUnitSystemChanged() {
for (const measurement of this._measurements) {
measurement.onDisplayUnitsChanged();
}
}
}
// Avoid circular dependency with webpack
ShimFunctions.getAllMeasurements = () => {
return MeasurementManager.instance.measurements;
};
// Avoid circular ependency with webpack
ShimFunctions.forAllMeasurements = (callback) => { MeasurementManager.instance.forAllMeasurements(callback); };
//# sourceMappingURL=MeasurementManager.js.map