@itwin/unified-selection
Version:
Package for managing unified selection in iTwin.js applications.
342 lines • 15.2 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 { 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