UNPKG

@itwin/measure-tools-react

Version:
402 lines 17.6 kB
/*--------------------------------------------------------------------------------------------- * 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