@itwin/measure-tools-react
Version:
Frontend framework and tools for measurements
402 lines • 17.6 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, Id64 } from "@itwin/core-bentley";
import { SelectionSetEventType } from "@itwin/core-frontend";
import { SessionStateActionId, SyncUiEventDispatcher, UiFramework } from "@itwin/appui-react";
import { Measurement } from "./Measurement.js";
import { MeasurementSyncUiEventId } from "./MeasurementEnums.js";
import { ShimFunctions } from "./ShimFunctions.js";
/**
* A selection set for measurements. Element selection sets are per imodel (generally just a single imodel opened) and are only ID sets, so this
* is intended to extend the Selection Sets by caching the actual measurements and is kept in sync with the target imodel's selection.
*/
export class MeasurementSelectionSet {
/**
* Gets the "Global" measurement selection set. Measurements do not belong to one single imodel and are transient elements that only exist on the frontend, so we do not have
* a selection set per imodel. Measurement transient ID's should be taken from the imodel that the global selection set is bound to.
* As IModels open and close the global selection tries to bind itself to any opened imodel.
*/
static get global() {
if (!MeasurementSelectionSet._global)
MeasurementSelectionSet._global = new MeasurementSelectionSet();
return MeasurementSelectionSet._global;
}
/**
* Gets the next transient ID from the current imodel. If undefined then there is no imodel.
*/
static get nextTransientId() {
const globalSS = MeasurementSelectionSet.global;
if (globalSS.imodel)
return globalSS.imodel.transientIds.getNext();
return undefined;
}
/** Gets the imodel that corresponds to this selection set. */
get imodel() {
this.ensureActiveModel();
return this._imodel;
}
/** Gets of all the currently selected measurements. This allocates a new array with each measurement. Use the iterator if you wish to iterate over the selected measurements and not create array copies. */
get measurements() {
return this._selectedMeasurements.filter((measure) => measure !== NullMeasurement.invalid);
}
/** Gets the number of selected measurements. */
get size() {
return this._idToIndexMap.size;
}
constructor() {
this._doNotModifySS = false;
this._ignoreSSEvent = false;
this._selectedMeasurements = new Array();
this._idToIndexMap = new Map();
this.onChanged = new BeUiEvent();
this.startEvents();
this.ensureActiveModel();
}
*[Symbol.iterator]() {
for (const measure of this._selectedMeasurements) {
if (measure === NullMeasurement.invalid)
continue;
yield measure;
}
}
getArray(idArg) {
const measurements = new Array();
for (const id of idArg) {
const index = this._idToIndexMap.get(id);
if (index !== undefined)
measurements.push(this._selectedMeasurements[index]);
}
return measurements;
}
get(id) {
const index = this._idToIndexMap.get(id);
if (index !== undefined)
return this._selectedMeasurements[index];
return undefined;
}
isSelected(id) {
return this._idToIndexMap.has(id);
}
add(measurements) {
if (!this.imodel)
return false;
const evtArray = [];
const idSet = new Set();
// Handle add, filter out duplicates or invalid IDs
const arr = Array.isArray(measurements) ? measurements : [measurements];
for (const measure of arr) {
if (measure && measure.transientId && !idSet.has(measure.transientId)) {
idSet.add(measure.transientId);
evtArray.push(measure);
this.addSingle(measure);
}
}
// If nothing to add, early out.
if (evtArray.length === 0)
return false;
// Add to the selection set, except when we are responding from a SS event. Make sure we ignore the SS event since we already processed the elements.
if (!this._doNotModifySS) {
this._ignoreSSEvent = true;
try {
this.imodel.selectionSet.add(idSet);
}
finally {
this._ignoreSSEvent = false;
}
}
this.sendEvent({ type: SelectionSetEventType.Add, added: evtArray, set: this });
return true;
}
// Since we want to keep measurements in an order that they were selected, if we remove measurements in the middle
// of the array, we have to put in a special null value rather than splicing it. If we have null values at the end of the array, we want
// to use those slots rather than grow the array.
addInAvailableSlotOrAppend(measurement) {
for (let i = this._selectedMeasurements.length - 1; i >= 0; i--) {
if (this._selectedMeasurements[i] === NullMeasurement.invalid) {
// Is the next entry null? If so, keep going, if not return current index
const nextI = i - 1;
if (nextI >= 0 && this._selectedMeasurements[nextI] === NullMeasurement.invalid)
continue;
// Otherwise, add it to this slot
this._selectedMeasurements[i] = measurement;
return i;
}
// If hit a regular measurement, break and append
break;
}
const index = this._selectedMeasurements.length;
this._selectedMeasurements.push(measurement);
return index;
}
addSingle(measurement) {
if (!measurement.transientId || this._idToIndexMap.has(measurement.transientId))
return false;
const index = this.addInAvailableSlotOrAppend(measurement);
this._idToIndexMap.set(measurement.transientId, index);
return true;
}
remove(measurements) {
if (!this.imodel)
return false;
const evtArray = [];
const idSet = new Set();
// Handle removes, filter out duplicates or invalid IDs
const arr = Array.isArray(measurements) ? measurements : [measurements];
for (const measure of arr) {
if (measure && measure.transientId && !idSet.has(measure.transientId)) {
idSet.add(measure.transientId);
evtArray.push(measure);
this.removeSingle(measure);
}
}
// If nothing to add, early out
if (evtArray.length === 0)
return false;
// Remove from selection set, except when we are responding from a SS event. Make sure we ignore the SS event since we already processed the elements.
if (!this._doNotModifySS) {
this._ignoreSSEvent = true;
try {
this.imodel.selectionSet.remove(idSet);
}
finally {
this._ignoreSSEvent = false;
}
}
this.sendEvent({ type: SelectionSetEventType.Remove, removed: evtArray, set: this });
return true;
}
removeSingle(measurement) {
if (!measurement.transientId)
return false;
const index = this._idToIndexMap.get(measurement.transientId);
if (index === undefined)
return false;
// Remove the ID from the map...if it's the last ID, then reset the selected measurements array to clear up holes. Otherwise,
// set a special null value. If we splice the array, then indices after this index will be invalid in the id-to-index map.
this._idToIndexMap.delete(measurement.transientId);
if (this._idToIndexMap.size === 0)
this._selectedMeasurements = [];
else
this._selectedMeasurements[index] = NullMeasurement.invalid;
return true;
}
addAndRemove(add, remove) {
if (!this.imodel)
return false;
const evtAddArray = [];
const evtRemoveArray = [];
const idAddSet = new Set();
const idRemoveSet = new Set();
// Handle adds, remove duplicates or invalid IDs
const arrAdd = Array.isArray(add) ? add : [add];
for (const measure of arrAdd) {
if (measure && measure.transientId && !idAddSet.has(measure.transientId)) {
idAddSet.add(measure.transientId);
evtAddArray.push(measure);
this.addSingle(measure);
}
}
// Handle removes, remove duplicates or invalid IDs
const arrRemove = Array.isArray(remove) ? remove : [remove];
for (const measure of arrRemove) {
if (measure && measure.transientId && !idRemoveSet.has(measure.transientId)) {
idRemoveSet.add(measure.transientId);
evtRemoveArray.push(measure);
this.removeSingle(measure);
}
}
// If no actual modification, early out
if (evtAddArray.length === 0 && evtRemoveArray.length === 0)
return false;
// Add/Remove from selection set, except when we are responding from a SS event. Make sure we ignore the SS event since we already processed the elements.
if (!this._doNotModifySS) {
this._ignoreSSEvent = true;
try {
this.imodel.selectionSet.addAndRemove(idAddSet, idRemoveSet);
}
finally {
this._ignoreSSEvent = false;
}
}
this.sendEvent({ type: SelectionSetEventType.Replace, added: evtAddArray, removed: evtRemoveArray, set: this });
return true;
}
clear() {
if (!this._imodel || this._idToIndexMap.size === 0)
return;
const removedMeasurements = this.measurements; // Returns a copy of all non-null measurements
const idArray = new Array();
for (const kv of this._idToIndexMap)
idArray.push(kv[0]);
this._idToIndexMap.clear();
this._selectedMeasurements = [];
// Remove from selection set, except when we are responding from a SS event. Make sure we ignore the SS event since we already processed the elements.
if (!this._doNotModifySS) {
this._ignoreSSEvent = true;
try {
this._imodel.selectionSet.remove(idArray);
}
finally {
this._ignoreSSEvent = false;
}
}
// Send a clear event with an array of all measurements that are no longer selected
this.sendEvent({ type: SelectionSetEventType.Clear, removed: removedMeasurements, set: this });
}
sendEvent(evt) {
this.onChanged.emit(evt);
SyncUiEventDispatcher.dispatchSyncUiEvent(MeasurementSyncUiEventId.MeasurementSelectionSetChanged);
}
handleSelectionSetChanged(ev) {
// Early out if we're in the middle of calling add/remove/etc externally (e.g. user is calling those methods and not setting to SS).
// Make sure we're responding to the right selection set.
if (this._ignoreSSEvent || !this._imodel || this._imodel.selectionSet !== ev.set)
return;
// Since we're handling a SS change event, the SS was modified with the measurements already so no need to modify it when calling the methods below
this._doNotModifySS = true;
try {
switch (ev.type) {
case SelectionSetEventType.Add:
this.handleSelectionAdd(ev);
break;
case SelectionSetEventType.Remove:
this.handleSelectionRemoved(ev);
break;
case SelectionSetEventType.Clear:
this.clear();
break;
case SelectionSetEventType.Replace:
this.handleSelectionReplaced(ev);
break;
}
}
finally {
this._doNotModifySS = false;
}
}
handleSelectionAdd(ev) {
if (!this.imodel)
return;
const addIdSet = Id64.toIdSet(ev.added, false);
const addMeasurements = new Array();
// We want to look up the measurement associated with the ID, so we need to look at all the measurements currently active...
ShimFunctions.forAllMeasurements((measurement) => {
if (measurement.transientId && addIdSet.has(measurement.transientId))
addMeasurements.push(measurement);
return true;
});
this.add(addMeasurements);
}
handleSelectionRemoved(ev) {
if (!this.imodel)
return;
const removeIdSet = Id64.toIdSet(ev.removed, false);
const removeMeasurements = new Array();
// All the measurements we want to remove should already be part of the cache, so we dont need to iterate over every measurement...
for (const removeId of removeIdSet) {
const index = this._idToIndexMap.get(removeId);
if (index !== undefined)
removeMeasurements.push(this._selectedMeasurements[index]);
}
this.remove(removeMeasurements);
}
handleSelectionReplaced(ev) {
if (!this.imodel)
return;
const addIdSet = Id64.toIdSet(ev.added, false);
const removeIdSet = Id64.toIdSet(ev.removed, false);
const addMeasurements = new Array();
const removeMeasurements = new Array();
// Find the measurements corresponding to the ID that was modified. Removes should already have the element in the cache, but we have to iterate over
// all measurements anyways...
ShimFunctions.forAllMeasurements((measurement) => {
if (measurement.transientId) {
if (addIdSet.has(measurement.transientId))
addMeasurements.push(measurement);
else if (removeIdSet.has(measurement.transientId))
removeMeasurements.push(measurement);
}
return true;
});
this.addAndRemove(addMeasurements, removeMeasurements);
}
startEvents() {
SyncUiEventDispatcher.onSyncUiEvent.addListener(this.onSyncEvent, this);
}
getUiFameworkIModel() {
// Apparently UiFramework errors if not initialized, because it's redux store hasn't been initialized,
// but it's accessor throws an exception if undefined so there doesn't seem to be an easy way to check if it's been initialized...
try {
return UiFramework.getIModelConnection();
}
catch {
return undefined;
}
}
onSyncEvent(args) {
if (args.eventIds.has(SessionStateActionId.SetIModelConnection))
this.changeActiveIModel(this.getUiFameworkIModel());
}
ensureActiveModel() {
if (this._imodel)
return;
this.changeActiveIModel(this.getUiFameworkIModel());
}
changeActiveIModel(newModel) {
// If both undefined or the same model, do nothing
if ((!this._imodel && !newModel) || (newModel === this._imodel))
return;
// Clear old model
if (this._imodel) {
this.clear();
this.resetAllTransientIds();
this._imodel.selectionSet.onChanged.removeListener(this.handleSelectionSetChanged, this);
this._imodel = undefined;
}
// Set the new model
if (newModel) {
this._imodel = newModel;
this._imodel.selectionSet.onChanged.addListener(this.handleSelectionSetChanged, this);
this.synchronizeFromSelectionSet();
}
}
resetAllTransientIds() {
ShimFunctions.forAllMeasurements((measurement) => {
measurement.transientId = undefined;
return true;
});
}
// Usually called on first init to ensure if anything is selected gets synchronized
synchronizeFromSelectionSet() {
if (!this._imodel)
return;
if (this._selectedMeasurements.length > 0) {
this._selectedMeasurements = [];
this._idToIndexMap.clear();
}
const selectedSet = this._imodel.selectionSet.elements;
ShimFunctions.forAllMeasurements((measurement) => {
if (measurement.transientId && selectedSet.has(measurement.transientId)) {
const index = this._selectedMeasurements.length;
this._selectedMeasurements.push(measurement);
this._idToIndexMap.set(measurement.transientId, index);
}
return true;
});
}
}
/** Special measurement object to null out array entries in the selected measurement array. These are skipped over and never returned in query methods. */
class NullMeasurement extends Measurement {
static get invalid() {
if (!NullMeasurement._invalid)
NullMeasurement._invalid = new NullMeasurement();
return NullMeasurement._invalid;
}
constructor() { super(); }
createNewInstance() {
return this;
}
}
//# sourceMappingURL=MeasurementSelectionSet.js.map