@itwin/core-backend
Version:
iTwin.js backend components
434 lines • 21.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElementSubTreeDeleter = exports.ElementTreeDeleter = exports.ElementTreeBottomUp = exports.ElementTreeWalkerScope = void 0;
exports.deleteElementTree = deleteElementTree;
exports.deleteElementSubTrees = deleteElementSubTrees;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Elements
*/
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const BackendLoggerCategory_1 = require("./BackendLoggerCategory");
const Element_1 = require("./Element");
const IModelDb_1 = require("./IModelDb");
const Model_1 = require("./Model");
const loggerCategory = `${BackendLoggerCategory_1.BackendLoggerCategory.IModelDb}.ElementTreeWalker`;
function sortChildrenBeforeParents(iModel, ids) {
const children = [];
const parents = [];
for (const eid of ids) {
const parentId = iModel.elements.queryParent(eid);
if (parentId !== undefined && ids.includes(parentId))
children.push(eid);
else
parents.push(eid);
}
if (children.length === 0)
return [parents];
return [...sortChildrenBeforeParents(iModel, children), parents];
}
function isModelEmpty(iModel, modelId) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
return iModel.withPreparedStatement(`select count(*) from ${Element_1.Element.classFullName} where Model.Id = ?`, (stmt) => {
stmt.bindId(1, modelId);
stmt.step();
return stmt.getValue(0).getInteger() === 0;
});
}
function isDefinitionModel(model) {
return (model.id !== core_common_1.IModel.repositoryModelId) && (model instanceof Model_1.DefinitionModel);
}
var ElementPruningClassification;
(function (ElementPruningClassification) {
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Normal"] = 0] = "PRUNING_CLASS_Normal";
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Subject"] = 1] = "PRUNING_CLASS_Subject";
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_Definition"] = 2] = "PRUNING_CLASS_Definition";
ElementPruningClassification[ElementPruningClassification["PRUNING_CLASS_DefinitionPartition"] = 3] = "PRUNING_CLASS_DefinitionPartition";
})(ElementPruningClassification || (ElementPruningClassification = {}));
function classifyElementForPruning(iModel, elementId) {
const el = iModel.elements.getElement(elementId);
// DefinitionContainer is submodeled by a DefinitionModel and so it must be classified as PRUNING_CLASS_DefinitionPartition for tree-walking purposes.
// Since DefinitionContainer is-a DefinitionElement the (el instanceof DefinitionElement) case below would classify it as PRUNING_CLASS_Definition.
// That is why we special-case it here.
if (el instanceof Element_1.DefinitionContainer)
return ElementPruningClassification.PRUNING_CLASS_DefinitionPartition;
return (el instanceof Element_1.Subject) ? ElementPruningClassification.PRUNING_CLASS_Subject :
(el instanceof Element_1.DefinitionElement) ? ElementPruningClassification.PRUNING_CLASS_Definition :
(el instanceof Element_1.DefinitionPartition) ? ElementPruningClassification.PRUNING_CLASS_DefinitionPartition :
ElementPruningClassification.PRUNING_CLASS_Normal;
}
/** Records the path that a tree search took to reach an element or model. This object is immutable.
* @beta
*/
class ElementTreeWalkerScope {
topElement = "";
/** path of parent elements and enclosing models */
path = [];
/** cached info about the immediately enclosing model (i.e., the last model in path) */
enclosingModelInfo;
constructor(arg1, arg2) {
if (typeof arg1 === "string") {
// normal constructor
(0, core_bentley_1.assert)(arg2 instanceof Model_1.Model);
this.topElement = arg1;
this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) });
}
else if (arg1 instanceof ElementTreeWalkerScope) {
// copy-like constructor
this.topElement = arg1.topElement;
this.path = [...arg1.path];
if (typeof arg2 === "string") {
// with new parent
this.path.push(arg2);
this.enclosingModelInfo = arg1.enclosingModelInfo;
}
else {
// with new enclosing model
this.path.push(this.enclosingModelInfo = { model: arg2, isDefinitionModel: isDefinitionModel(arg2) });
}
}
else {
throw new Error("invalid constructor signature");
}
}
get enclosingModel() { return this.enclosingModelInfo.model; }
get inDefinitionModel() { return this.enclosingModelInfo.isDefinitionModel; } // NB: this will return false for the RepositoryModel!
get inRepositoryModel() { return this.enclosingModelInfo.model.id === IModelDb_1.IModelDb.repositoryModelId; }
static createTopScope(iModel, topElementId) {
const topElement = iModel.elements.getElement(topElementId);
const topElementModel = iModel.models.getModel(topElement.model);
return new ElementTreeWalkerScope(topElementId, topElementModel);
}
fmtItem(v) {
if (typeof v === "string")
return `element ${v}`;
return `model ${v.model.id} ${v.isDefinitionModel ? "(DEFN)" : ""}`;
}
toString() {
return `[ ${this.path.map((v) => this.fmtItem(v)).join(" / ")} ]`;
}
}
exports.ElementTreeWalkerScope = ElementTreeWalkerScope;
function fmtElement(iModel, elementId) {
const el = iModel.elements.getElement(elementId);
return `${el.id} ${el.classFullName} ${el.getDisplayLabel()}`;
}
function fmtModel(model) {
return `${model.id} ${model.classFullName} ${model.name}`;
}
let isTraceEnabledChecked = -1;
function isTraceEnabled() {
if (isTraceEnabledChecked === -1)
isTraceEnabledChecked = core_bentley_1.Logger.isEnabled(loggerCategory, core_bentley_1.LogLevel.Trace) ? 1 : 0;
return isTraceEnabledChecked === 1;
}
function logElement(op, iModel, elementId, scope, logChildren) {
if (!isTraceEnabled())
return;
core_bentley_1.Logger.logTrace(loggerCategory, `${op} ${fmtElement(iModel, elementId)} ${scope ? scope.toString() : ""}`);
if (logChildren)
iModel.elements.queryChildren(elementId).forEach((c) => logElement(" - ", iModel, c, undefined, true));
}
function logModel(op, iModel, modelId, scope, logElements) {
if (!isTraceEnabled())
return;
const model = iModel.models.getModel(modelId);
core_bentley_1.Logger.logTrace(loggerCategory, `${op} ${fmtModel(model)} ${scope ? scope.toString() : ""}`);
if (logElements) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
iModel.withPreparedStatement(`select ecinstanceid from ${Element_1.Element.classFullName} where Model.Id = ?`, (stmt) => {
stmt.bindId(1, modelId);
while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
logElement(" - ", iModel, stmt.getValue(0).getId());
}
});
}
}
/** Does a depth-first search on the tree defined by an element and its sub-models and children.
* Sub-models are visited before their modeled elements, and children are visited before their parents.
*
* The following callbacks allow the subclass to exclude elements and sub-trees from the search:
* * [[ElementTreeBottomUp.shouldExploreModel]], [[ElementTreeBottomUp.shouldExploreChildren]]
* * [[ElementTreeBottomUp.shouldVisitElement]], [[ElementTreeBottomUp.shouldVisitModel]]
*
* The [[ElementTreeBottomUp.visitElement]] and [[ElementTreeBottomUp.visitModel]] callbacks allow
* the subclass to process the elements and models that are encountered in the search.
* @beta
*/
class ElementTreeBottomUp {
_iModel;
constructor(_iModel) {
this._iModel = _iModel;
}
/** Return true if the search should recurse into this model */
shouldExploreModel(_model, _scope) { return true; }
/** Return true if the search should recurse into the children (if any) of this element */
shouldExploreChildren(_parentId, _scope) { return true; }
/** Return true if the search should visit this element */
shouldVisitElement(_elementId, _scope) { return true; }
/** Return true if the search should visit this model */
shouldVisitModel(_model, _scope) { return true; }
/** The main tree-walking function */
processElementTree(element, scope) {
const subModel = this._iModel.models.tryGetModel(element);
if (subModel !== undefined) {
if (this.shouldExploreModel(subModel, scope))
this._processSubModel(subModel, scope);
if (this.shouldVisitModel(subModel, scope))
this.visitModel(subModel, scope);
}
if (this.shouldExploreChildren(element, scope))
this._processChildren(element, scope);
if (this.shouldVisitElement(element, scope))
this.visitElement(element, scope);
}
/** process the children of the specified parent element */
_processChildren(parentElement, parentScope) {
const children = this._iModel.elements.queryChildren(parentElement);
if (children.length === 0)
return;
const childrenScope = new ElementTreeWalkerScope(parentScope, parentElement);
for (const childElement of children)
this.processElementTree(childElement, childrenScope);
}
/** process the elements in the specified model */
_processSubModel(model, parenScope) {
const scope = new ElementTreeWalkerScope(parenScope, model);
// Visit only the top-level parents. processElementTree will visit their children (bottom-up).
// eslint-disable-next-line @typescript-eslint/no-deprecated
model.iModel.withPreparedStatement(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (stmt) => {
stmt.bindId(1, model.id);
while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
const elementId = stmt.getValue(0).getId();
this.processElementTree(elementId, scope);
}
});
}
}
exports.ElementTreeBottomUp = ElementTreeBottomUp;
/** Helper class that manages the deletion of definitions and subjects */
class SpecialElements {
definitionModels = [];
definitions = [];
subjects = [];
recordSpecialElement(iModel, elementId) {
// Defer Definitions and Subjects
const cls = classifyElementForPruning(iModel, elementId);
if (cls === ElementPruningClassification.PRUNING_CLASS_Subject) {
this.subjects.push(elementId);
return true;
}
else if (cls === ElementPruningClassification.PRUNING_CLASS_Definition) {
this.definitions.push(elementId);
return true;
}
else if (cls === ElementPruningClassification.PRUNING_CLASS_DefinitionPartition) {
this.definitionModels.push(elementId);
return true;
}
return false; // not a special element
}
/** Delete special elements - This calls Elements.deleteDefinitionElements to process the collected definition elements as a group and then
* calls Models.deleteModel on collected definition models. It then deletes all collected Subjects by calling Element.deleteElement on them.
* @note Caller must ensure that the special elements were recorded in a depth-first search.
*/
deleteSpecialElements(iModel) {
// It's dangerous to pass a mixture of SubCategories and Categories to deleteDefinitionElements.
// That function will delete the Categories first, which automatically deletes all their child
// SubCategories (in native code). If a SubCategory in the list is one of those children, then
// deleteDefinitionElements will try and fail with an exception to delete that SubCategory in a subsequent step.
// To work around this, we delete the SubCategories first, then everything else.
// A similar problem occurs when you pass other kinds of elements to deleteDefinitionElements, where some are
// children and others are parents. deleteDefinitionElements does not preserve the order that you specify,
// and it does not process children before parents.
for (const definitions of sortChildrenBeforeParents(iModel, this.definitions)) {
if (isTraceEnabled())
definitions.forEach((e) => logElement("try delete", iModel, e));
iModel.elements.deleteDefinitionElements(definitions); // will not delete definitions that are still in use.
}
for (const m of this.definitionModels) {
if (!isModelEmpty(iModel, m)) {
logModel("Model not empty - cannot delete - may contain Definitions that are still in use", iModel, m, undefined, true);
}
else {
logModel("delete", iModel, m);
iModel.models.deleteModel(m);
iModel.elements.deleteElement(m);
}
}
for (const e of this.subjects) {
if (iModel.elements.queryChildren(e).length !== 0) {
logElement("Subject still has children - cannot delete - may have child DefinitionModels", iModel, e, undefined, true);
}
else {
logElement("delete", iModel, e);
iModel.elements.deleteElement(e);
}
}
}
}
/** Deletes an entire element tree, including sub-models and child elements.
* Items are deleted in bottom-up order. Definitions and Subjects are deleted after normal elements.
* Call deleteNormalElements on each tree. Then call deleteSpecialElements.
* @see deleteElementTree for a simple way to use this class.
* @beta
*/
class ElementTreeDeleter extends ElementTreeBottomUp {
_special = new SpecialElements();
shouldExploreModel(_model) { return true; }
shouldVisitElement(_elementId) { return true; }
visitModel(model, _scope) {
if (isDefinitionModel(model))
return; // we recorded definition models in visitElement when we encountered the DefinitionPartition elements.
// visitElement has already deleted the elements in the model. So, now it's safe to delete the model itself.
logModel("delete", this._iModel, model.id, _scope);
model.delete();
}
visitElement(elementId, _scope) {
if (!this._special.recordSpecialElement(this._iModel, elementId)) {
logElement("delete", this._iModel, elementId, _scope);
this._iModel.elements.deleteElement(elementId);
}
}
/**
* Delete the "normal" elements and record the special elements for deferred processing.
* @param topElement The parent of the sub-tree to be deleted. Top element itself is also deleted.
* @param scope How the parent was found
* @see deleteSpecialElements
*/
deleteNormalElements(topElement, scope) {
const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this._iModel, topElement);
this.processElementTree(topElement, topScope); //
}
/** Delete all special elements that were found and deferred by deleteNormalElements. Call this
* function once after all element trees are processed by deleteNormalElements.
*/
deleteSpecialElements() {
this._special.deleteSpecialElements(this._iModel);
}
}
exports.ElementTreeDeleter = ElementTreeDeleter;
/** Does a breadth-first search on the tree defined by an element and its sub-models and children.
* Parents are visited first, then children, then sub-models.
* The subclass can "prune" sub-trees from the search. When a sub-tree is "pruned" the search does *not* recurse into it.
* If a sub-tree is not pruned, then the search does recurse into it.
* @beta
*/
class ElementTreeTopDown {
_iModel;
constructor(_iModel) {
this._iModel = _iModel;
}
/** Should the search *not* recurse into this sub-tree? */
shouldPrune(_elementId, _scope) { return false; }
processElementTree(element, scope) {
if (this.shouldPrune(element, scope)) {
this.prune(element, scope);
return;
}
this._processChildren(element, scope);
const subModel = this._iModel.models.tryGetModel(element);
if (subModel !== undefined) {
this._processSubModel(subModel, scope);
}
}
_processChildren(element, scope) {
let parentScope;
for (const childElement of this._iModel.elements.queryChildren(element)) {
if (parentScope === undefined)
parentScope = new ElementTreeWalkerScope(scope, element);
this.processElementTree(childElement, parentScope);
}
}
_processSubModel(subModel, scope) {
const subModelScope = new ElementTreeWalkerScope(scope, subModel);
// Visit only the top-level parents. processElementTree will recurse into their children.
// eslint-disable-next-line @typescript-eslint/no-deprecated
this._iModel.withPreparedStatement(`select ECInstanceId from bis:Element where Model.id=? and Parent.Id is null`, (stmt) => {
stmt.bindId(1, subModel.id);
while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
const elementId = stmt.getValue(0).getId();
this.processElementTree(elementId, subModelScope);
}
});
}
}
/** Performs a breadth-first search to visit elements in top-down order.
* When the supplied filter function chooses an element, ElementTreeDeleter is used to delete it and its sub-tree.
* @beta
*/
class ElementSubTreeDeleter extends ElementTreeTopDown {
_treeDeleter;
_shouldPruneCb;
/** Construct an ElementSubTreeDeleter.
* @param iModel The iModel
* @param topElement Where to start the search.
* @param shouldPruneCb Callback that selects sub-trees that should be deleted.
* @see deleteElementSubTrees for a simple way to use this class.
*/
constructor(iModel, shouldPruneCb) {
super(iModel);
this._treeDeleter = new ElementTreeDeleter(this._iModel);
this._shouldPruneCb = shouldPruneCb;
}
shouldPrune(elementId, scope) { return this._shouldPruneCb(elementId, scope); }
prune(elementId, scope) {
this._treeDeleter.deleteNormalElements(elementId, scope);
}
/** Traverses the tree of elements beginning with the top element, and deletes all selected sub-trees.
* Normal elements are deleted. Any special elements that are encountered are deferred.
* Call deleteSpecialElementSubTrees after all top elements have been processed. */
deleteNormalElementSubTrees(topElement, scope) {
const topScope = scope ?? ElementTreeWalkerScope.createTopScope(this._iModel, topElement);
this.processElementTree(topElement, topScope); // deletes normal elements and their sub-trees, defers special elements
}
/** Delete all special elements and their sub-trees that were found in the course of processing.
* The sub-trees were already expanded by ElementTreeDeleter.deleteNormalElements.
*/
deleteSpecialElementSubTrees() {
this._treeDeleter.deleteSpecialElements();
}
}
exports.ElementSubTreeDeleter = ElementSubTreeDeleter;
/** @internal */
function deleteElementTree(arg0, arg1) {
let maxPasses;
let iModel;
let topElement;
if (arg0 instanceof IModelDb_1.IModelDb) {
(0, core_bentley_1.assert)(typeof arg1 === "string");
iModel = arg0;
topElement = arg1;
}
else {
iModel = arg0.iModel;
topElement = arg0.topElement;
maxPasses = arg0.maxPasses;
}
maxPasses = maxPasses ?? 5;
let pass = 0;
do {
const del = new ElementTreeDeleter(iModel);
del.deleteNormalElements(topElement);
del.deleteSpecialElements();
} while ((iModel.elements.tryGetElement(topElement) !== undefined) && (++pass < maxPasses));
}
/** Deletes all element sub-trees that are selected by the supplied filter. Uses ElementSubTreeDeleter.
* If the filter selects the top element itself, then the entire tree (including the top element) is deleted.
* That has the same effect as calling [[deleteElementTree]] on the top element.
* @note The caller may have to call this function multiple times if there are multiple layers of dependencies among definition elements.
* @param iModel The iModel
* @param topElement Where to start the search.
* @param filter Callback that selects sub-trees that should be deleted.
* @beta
*/
function deleteElementSubTrees(iModel, topElement, filter) {
const del = new ElementSubTreeDeleter(iModel, filter);
del.deleteNormalElementSubTrees(topElement);
del.deleteSpecialElementSubTrees();
}
//# sourceMappingURL=ElementTreeWalker.js.map