UNPKG

@itwin/unified-selection

Version:

Package for managing unified selection in iTwin.js applications.

342 lines 15.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { EMPTY, filter, forkJoin, from, map, merge, mergeMap, scan, shareReplay, Subject, toArray } from "rxjs"; import { eachValueFrom } from "rxjs-for-await"; import { normalizeFullClassName } from "@itwin/presentation-shared"; import { formIdBindings, genericExecuteQuery, releaseMainThreadOnItemsCount } from "./Utils.js"; const HILITE_SET_EMIT_FREQUENCY = 20; /** * Creates a hilite set provider that returns a `HiliteSet` for given selectables. * @public */ export function createHiliteSetProvider(props) { return new HiliteSetProviderImpl(props); } class HiliteSetProviderImpl { _imodelAccess; // Map between a class name and its type _classRelationCache; constructor(props) { this._imodelAccess = props.imodelAccess; this._classRelationCache = new Map(); } /** * Get hilite set iterator for supplied `Selectables`. */ getHiliteSet(props) { const obs = this.getHiliteSetObservable(props); return eachValueFrom(obs); } /** * Returns a "hot" observable of hilite sets. */ getHiliteSetObservable({ selectables }) { const instancesByType = this.getInstancesByType(selectables); const observables = { models: this.getHilitedModels(instancesByType), subCategories: this.getHilitedSubCategories(instancesByType), elements: this.getHilitedElements(instancesByType), }; let hiliteSet = { models: [], subCategories: [], elements: [] }; let lastEmitTime = performance.now(); const subject = new Subject(); const subscriptions = ["models", "subCategories", "elements"].map((key) => observables[key].subscribe({ next(val) { hiliteSet[key].push(val); if (performance.now() - lastEmitTime < HILITE_SET_EMIT_FREQUENCY) { return; } subject.next(hiliteSet); hiliteSet = { models: [], subCategories: [], elements: [] }; lastEmitTime = performance.now(); }, complete() { observables[key] = undefined; if (observables.models || observables.subCategories || observables.elements) { return; } // Emit last batch before completing the observable. if (hiliteSet.elements.length || hiliteSet.models.length || hiliteSet.subCategories.length) { subject.next(hiliteSet); } subject.complete(); }, error(err) { subscriptions.forEach((x) => x.unsubscribe()); subscriptions.length = 0; subject.error(err); }, })); return subject; } async getType(key) { const normalizedClassName = normalizeFullClassName(key.className); const cachedType = this._classRelationCache.get(normalizedClassName); if (cachedType) { return cachedType; } const promise = this.getTypeImpl(normalizedClassName).then((res) => { // Update the cache with the result of the promise. this._classRelationCache.set(normalizedClassName, res); return res; }); // Add the promise to cache to prevent `getTypeImpl` being called multiple times. this._classRelationCache.set(normalizedClassName, promise); return promise; } async getTypeImpl(fullClassName) { return ((await this.checkType(fullClassName, "BisCore.Subject", "subject")) ?? (await this.checkType(fullClassName, "BisCore.Model", "model")) ?? (await this.checkType(fullClassName, "BisCore.Category", "category")) ?? (await this.checkType(fullClassName, "BisCore.SubCategory", "subCategory")) ?? (await this.checkType(fullClassName, "Functional.FunctionalElement", "functionalElement")) ?? (await this.checkType(fullClassName, "BisCore.GroupInformationElement", "groupInformationElement")) ?? (await this.checkType(fullClassName, "BisCore.GeometricElement", "geometricElement")) ?? (await this.checkType(fullClassName, "BisCore.Element", "element")) ?? "unknown"); } async checkType(keyClassName, checkClassName, type) { try { const res = this._imodelAccess.classDerivesFrom(keyClassName, checkClassName); const isOfType = typeof res === "boolean" ? res : await res; return isOfType ? type : undefined; } catch (e) { // we may be checking against a non-existing schema (e.g. Functional), in which case we should // return undefined instead of throwing an error if (e instanceof Error && e.message.match(/Schema "[\w\d_]+" not found/)) { return undefined; } throw e; } } getInstancesByType(selectables) { const keyTypeObs = merge(from(selectables.custom.values()).pipe(mergeMap((selectable) => selectable.loadInstanceKeys())), from(selectables.instanceKeys).pipe(mergeMap(([className, idSet]) => from(idSet).pipe(map((id) => ({ className, id })))))).pipe(releaseMainThreadOnItemsCount(500), // Get types for each instance key mergeMap((instanceKey) => from(this.getType(instanceKey)).pipe(map((instanceIdType) => ({ instanceId: instanceKey.id, instanceIdType })))), // Cache the results shareReplay()); return Object.fromEntries(INSTANCE_TYPES.map((type) => [ type, keyTypeObs.pipe(filter(({ instanceIdType }) => instanceIdType === type), map(({ instanceId }) => instanceId), unique()), ])); } getHilitedModels(instancesByType) { return forkJoin({ modelKeys: instancesByType.model.pipe(toArray()), subjectKeys: instancesByType.subject.pipe(toArray()), }).pipe(mergeMap(({ modelKeys, subjectKeys }) => { if (!modelKeys.length && !subjectKeys.length) { return EMPTY; } const bindings = []; const ctes = [ ` ChildSubjects(ECInstanceId, JsonProperties) AS ( SELECT ECInstanceId, JsonProperties FROM BisCore.Subject WHERE ${formIdBindings("ECInstanceId", subjectKeys, bindings)} UNION ALL SELECT r.ECInstanceId, r.JsonProperties FROM ChildSubjects s JOIN BisCore.Subject r ON r.Parent.Id = s.ECInstanceId ) `, ` Models(ECInstanceId) AS ( SELECT s.ECInstanceId AS ECInstanceId FROM BisCore.Model s WHERE ${formIdBindings("ECInstanceId", modelKeys, bindings)} ) `, ]; const ecsql = [ ` SELECT r.ECInstanceId AS ECInstanceId FROM ChildSubjects s JOIN BisCore.PhysicalPartition r ON r.Parent.Id = s.ECInstanceId OR json_extract(s.JsonProperties,'$.Subject.Model.TargetPartition') = printf('0x%x', r.ECInstanceId) `, ` SELECT ECInstanceId FROM Models `, ].join(" UNION "); return from(executeQuery(this._imodelAccess, { ctes, ecsql, bindings })); })); } getHilitedSubCategories(instancesByType) { return forkJoin({ subCategoryKeys: instancesByType.subCategory.pipe(toArray()), categoryKeys: instancesByType.category.pipe(toArray()), }).pipe(mergeMap(({ subCategoryKeys, categoryKeys }) => { if (!subCategoryKeys.length && !categoryKeys.length) { return EMPTY; } const bindings = []; const ctes = [ ` CategorySubCategories(ECInstanceId) AS ( SELECT r.ECInstanceId AS ECInstanceId FROM BisCore.Category s JOIN BisCore.SubCategory r ON r.Parent.Id = s.ECInstanceId WHERE ${formIdBindings("s.ECInstanceId", categoryKeys, bindings)} ) `, ` SubCategories(ECInstanceId) AS ( SELECT s.ECInstanceId AS ECInstanceId FROM BisCore.SubCategory s WHERE ${formIdBindings("s.ECInstanceId", subCategoryKeys, bindings)} ) `, ]; const ecsql = [`SELECT ECInstanceId FROM CategorySubCategories`, `SELECT ECInstanceId FROM SubCategories`].join(" UNION "); return from(executeQuery(this._imodelAccess, { ctes, ecsql, bindings })); })); } getHilitedElements(instancesByType) { return forkJoin({ groupInformationElementKeys: instancesByType.groupInformationElement.pipe(toArray()), geometricElementKeys: instancesByType.geometricElement.pipe(toArray()), functionalElements: instancesByType.functionalElement.pipe(toArray()), elementKeys: instancesByType.element.pipe(toArray()), }).pipe(mergeMap(({ groupInformationElementKeys, geometricElementKeys, functionalElements, elementKeys }) => { const hasFunctionalElements = !!functionalElements.length; if (!groupInformationElementKeys.length && !geometricElementKeys.length && !elementKeys.length && !hasFunctionalElements) { return EMPTY; } const bindings = []; const ctes = [ ...(hasFunctionalElements ? this.getHilitedFunctionalElementsQueryCTEs(functionalElements, bindings) : []), ` GroupMembers(ECInstanceId, ECClassId) AS ( SELECT TargetECInstanceId, TargetECClassId FROM BisCore.ElementGroupsMembers WHERE ${formIdBindings("SourceECInstanceId", groupInformationElementKeys, bindings)} ) `, ` GroupGeometricElements(ECInstanceId, ECClassId) AS ( SELECT ECInstanceId, ECClassId FROM GroupMembers UNION ALL SELECT r.ECInstanceId, r.ECClassId FROM GroupGeometricElements s JOIN BisCore.Element r ON r.Parent.Id = s.ECInstanceId ) `, ` ElementGeometricElements(ECInstanceId, ECClassId) AS ( SELECT ECInstanceId, ECClassId FROM BisCore.Element WHERE ${formIdBindings("ECInstanceId", elementKeys, bindings)} UNION ALL SELECT r.ECInstanceId, r.ECClassId FROM ElementGeometricElements s JOIN BisCore.Element r ON r.Parent.Id = s.ECInstanceId ) `, ` GeometricElementGeometricElements(ECInstanceId, ECClassId) AS ( SELECT ECInstanceId, ECClassId FROM BisCore.GeometricElement WHERE ${formIdBindings("ECInstanceId", geometricElementKeys, bindings)} UNION ALL SELECT r.ECInstanceId, r.ECClassId FROM GeometricElementGeometricElements s JOIN BisCore.Element r ON r.Parent.Id = s.ECInstanceId ) `, ]; const ecsql = [ ...(hasFunctionalElements ? ["SELECT ECInstanceId FROM FunctionalElementChildGeometricElements WHERE ECClassId IS (BisCore.GeometricElement)"] : []), "SELECT ECInstanceId FROM GeometricElementGeometricElements WHERE ECClassId IS (BisCore.GeometricElement)", "SELECT ECInstanceId FROM GroupGeometricElements WHERE ECClassId IS (BisCore.GeometricElement)", "SELECT ECInstanceId FROM ElementGeometricElements WHERE ECClassId IS (BisCore.GeometricElement)", ].join(" UNION "); return from(executeQuery(this._imodelAccess, { ctes, ecsql, bindings })); })); } getHilitedFunctionalElementsQueryCTEs(functionalElements, bindings) { return [ ` ChildFunctionalElements(ECInstanceId, ECClassId) AS ( SELECT ECInstanceId, ECClassId FROM Functional.FunctionalElement WHERE ${formIdBindings("ECInstanceId", functionalElements, bindings)} UNION ALL SELECT r.ECInstanceId, r.ECClassId FROM ChildFunctionalElements s JOIN Functional.FunctionalElement r ON r.Parent.Id = s.ECInstanceId ) `, ` PhysicalElements(ECInstanceId, ECClassId) AS ( SELECT r.SourceECInstanceId, r.SourceECClassId FROM ChildFunctionalElements s JOIN Functional.PhysicalElementFulfillsFunction r ON r.TargetECInstanceId = s.ECInstanceId ) `, ` DrawingGraphicElements(ECInstanceId, ECClassId) AS ( SELECT r.SourceECInstanceId, r.SourceECClassId FROM ChildFunctionalElements s JOIN Functional.DrawingGraphicRepresentsFunctionalElement r ON r.TargetECInstanceId = s.ECInstanceId ) `, ` PhysicalElementGeometricElements(ECInstanceId, ECClassId) AS ( SELECT ECInstanceId, ECClassId FROM PhysicalElements UNION ALL SELECT r.ECInstanceId, r.ECClassId FROM PhysicalElementGeometricElements s JOIN BisCore.Element r ON r.Parent.Id = s.ECInstanceId ) `, ` DrawingGraphicElementGeometricElements(ECInstanceId, ECClassId) AS ( SELECT ECInstanceId, ECClassId FROM DrawingGraphicElements UNION ALL SELECT r.ECInstanceId, r.ECClassId FROM DrawingGraphicElementGeometricElements s JOIN BisCore.Element r ON r.Parent.Id = s.ECInstanceId ) `, ` FunctionalElementChildGeometricElements(ECInstanceId, ECClassId) AS ( SELECT ECInstanceId, ECClassId FROM PhysicalElementGeometricElements UNION SELECT ECInstanceId, ECClassId FROM DrawingGraphicElementGeometricElements ) `, ]; } } const INSTANCE_TYPES = [ "subject", "model", "category", "subCategory", "functionalElement", "groupInformationElement", "geometricElement", "element", "unknown", ]; function unique() { return function (obs) { return obs.pipe(scan((acc, val) => { if (acc.set.has(val)) { delete acc.val; return acc; } acc.set.add(val); acc.val = val; return acc; }, { set: new Set() }), map(({ val }) => val), filter((x) => !!x)); }; } async function* executeQuery(queryExecutor, query) { yield* genericExecuteQuery(queryExecutor, query, (row) => row.ECInstanceId); } //# sourceMappingURL=HiliteSetProvider.js.map