Skip to content

Commit

Permalink
Load all subcategories when creating a default view. (iTwin#6882)
Browse files Browse the repository at this point in the history
Co-authored-by: Paul Connelly <[email protected]>
  • Loading branch information
hl662 and pmconne authored Jul 1, 2024
1 parent d7bda38 commit 8fba91d
Show file tree
Hide file tree
Showing 15 changed files with 143 additions and 13 deletions.
2 changes: 2 additions & 0 deletions common/api/core-backend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3110,6 +3110,8 @@ export abstract class IModelDb extends IModel {
prepareStatement(sql: string, logErrors?: boolean): ECSqlStatement;
// @deprecated
query(ecsql: string, params?: QueryBinder, options?: QueryOptions): AsyncIterableIterator<any>;
// @internal
queryAllUsedSpatialSubCategories(): Promise<SubCategoryResultRow[]>;
queryEntityIds(params: EntityQueryParams): Id64Set;
queryFilePropertyBlob(prop: FilePropertyProps): Uint8Array | undefined;
queryFilePropertyString(prop: FilePropertyProps): string | undefined;
Expand Down
2 changes: 2 additions & 0 deletions common/api/core-common.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4963,6 +4963,8 @@ export abstract class IModelReadRpcInterface extends RpcInterface {
// (undocumented)
loadElementProps(_iModelToken: IModelRpcProps, _elementIdentifier: Id64String | GuidString | CodeProps, _options?: ElementLoadOptions): Promise<ElementProps | undefined>;
// (undocumented)
queryAllUsedSpatialSubCategories(_iModelToken: IModelRpcProps): Promise<SubCategoryResultRow[]>;
// (undocumented)
queryBlob(_iModelToken: IModelRpcProps, _request: DbBlobRequest): Promise<DbBlobResponse>;
// (undocumented)
queryElementProps(_iModelToken: IModelRpcProps, _params: EntityQueryParams): Promise<ElementProps[]>;
Expand Down
5 changes: 4 additions & 1 deletion common/api/core-frontend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -7043,6 +7043,8 @@ export abstract class IModelConnection extends IModel {
get projectCenterAltitude(): number | undefined;
// @deprecated
query(ecsql: string, params?: QueryBinder, options?: QueryOptions): AsyncIterableIterator<any>;
// @internal
queryAllUsedSpatialSubCategories(): Promise<SubCategoryResultRow[]>;
queryEntityIds(params: EntityQueryParams): Promise<Id64Set>;
// @deprecated
queryRowCount(ecsql: string, params?: QueryBinder): Promise<number>;
Expand Down Expand Up @@ -13406,7 +13408,7 @@ export class StrokesPrimitivePointLists extends Array<StrokesPrimitivePointList>
// @internal
export class SubCategoriesCache {
constructor(imodel: IModelConnection);
add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance): void;
add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance, override: boolean): void;
// (undocumented)
clear(): void;
// (undocumented)
Expand All @@ -13416,6 +13418,7 @@ export class SubCategoriesCache {
// (undocumented)
getSubCategoryInfo(categoryId: Id64String, inputSubCategoryIds: Id64String | Iterable<Id64String>): Promise<Map<Id64String, IModelConnection.Categories.SubCategoryInfo>>;
load(categoryIds: Id64Arg): SubCategoriesRequest | undefined;
loadAllUsedSpatialSubCategories(): Promise<void>;
// (undocumented)
onIModelConnectionClose(): void;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-backend",
"comment": "Add RPC method queryAllUsedSpatialSubCategories() to fetch all subcategories of used spatial categories and 3D elements.",
"type": "none"
}
],
"packageName": "@itwin/core-backend"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-common",
"comment": "Add RPC method queryAllUsedSpatialSubCategories() to fetch all subcategories of used spatial categories and 3D elements.",
"type": "none"
}
],
"packageName": "@itwin/core-common"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-frontend",
"comment": "Load up front all subcategories of used spatial categories and 3D elements when creating a default view.",
"type": "none"
}
],
"packageName": "@itwin/core-frontend"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/rpcinterface-full-stack-tests",
"comment": "",
"type": "none"
}
],
"packageName": "@itwin/rpcinterface-full-stack-tests"
}
27 changes: 26 additions & 1 deletion core/backend/src/IModelDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,32 @@ export abstract class IModelDb extends IModel {
}

/**
* queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds
* queries the BisCore.SubCategory table for entries that are children of used spatial categories and 3D elements.
* @returns array of SubCategoryResultRow
* @internal
*/
public async queryAllUsedSpatialSubCategories(): Promise<SubCategoryResultRow[]> {
const result: SubCategoryResultRow[] = [];
const parentCategoriesQuery = `SELECT DISTINCT Category.Id AS id FROM BisCore.GeometricElement3d WHERE Category.Id IN (SELECT ECInstanceId FROM BisCore.SpatialCategory)`;
const parentCategories: Id64Array = [];
for await (const row of this.createQueryReader(parentCategoriesQuery)) {
parentCategories.push(row.id);
};
const where = [...parentCategories].join(",");
const query = `SELECT ECInstanceId as id, Parent.Id as parentId, Properties as appearance FROM BisCore.SubCategory WHERE Parent.Id IN (${where})`;

try {
for await (const row of this.createQueryReader(query, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) {
result.push(row.toRow() as SubCategoryResultRow);
}
} catch {
// We can ignore the error here, and just return whatever we were able to query.
}
return result;
}

/**
* queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds.
* @param categoryIds categoryIds to query
* @returns array of SubCategoryResultRow
* @internal
Expand Down
5 changes: 5 additions & 0 deletions core/backend/src/rpc-impl/IModelReadRpcImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,11 @@ export class IModelReadRpcImpl extends RpcInterface implements IModelReadRpcInte
return viewHydrater.getHydrateResponseProps(options);
}

public async queryAllUsedSpatialSubCategories(tokenProps: IModelRpcProps): Promise<SubCategoryResultRow[]> {
const iModelDb = await getIModelForRpc(tokenProps);
return iModelDb.queryAllUsedSpatialSubCategories();
}

public async querySubCategories(tokenProps: IModelRpcProps, compressedCategoryIds: CompressedId64Set): Promise<SubCategoryResultRow[]> {
const iModelDb = await getIModelForRpc(tokenProps);
const decompressedIds = CompressedId64Set.decompressArray(compressedCategoryIds);
Expand Down
4 changes: 3 additions & 1 deletion core/common/src/rpc/IModelReadRpcInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // eslint-di
public static readonly interfaceName = "IModelReadRpcInterface";

/** The semantic version of the interface. */
public static interfaceVersion = "3.6.0";
public static interfaceVersion = "3.7.0";

/*===========================================================================================
NOTE: Any add/remove/change to the methods below requires an update of the interface version.
Expand All @@ -95,6 +95,8 @@ export abstract class IModelReadRpcInterface extends RpcInterface { // eslint-di
public async queryRows(_iModelToken: IModelRpcProps, _request: DbQueryRequest): Promise<DbQueryResponse> { return this.forward(arguments); }
@RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation
public async querySubCategories(_iModelToken: IModelRpcProps, _categoryIds: CompressedId64Set): Promise<SubCategoryResultRow[]> { return this.forward(arguments); }
@RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation
public async queryAllUsedSpatialSubCategories(_iModelToken: IModelRpcProps): Promise<SubCategoryResultRow[]> { return this.forward(arguments); }
public async queryBlob(_iModelToken: IModelRpcProps, _request: DbBlobRequest): Promise<DbBlobResponse> { return this.forward(arguments); }
@RpcOperation.allowResponseCaching(RpcResponseCacheControl.Immutable) // eslint-disable-line deprecation/deprecation
public async getModelProps(_iModelToken: IModelRpcProps, _modelIds: Id64String[]): Promise<ModelProps[]> { return this.forward(arguments); }
Expand Down
9 changes: 9 additions & 0 deletions core/frontend/src/IModelConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ export abstract class IModelConnection extends IModel {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).querySubCategories(this.getRpcProps(), compressedCategoryIds);
}

/**
* queries the BisCore.SubCategory table for entries that are children of used spatial categories and 3D elements.
* @returns array of SubCategoryResultRow
* @internal
*/
public async queryAllUsedSpatialSubCategories(): Promise<SubCategoryResultRow[]> {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).queryAllUsedSpatialSubCategories(this.getRpcProps());
}

/** Execute a query and stream its results
* The result of the query is async iterator over the rows. The iterator will get next page automatically once rows in current page has been read.
* [ECSQL row]($docs/learning/ECSQLRowFormat).
Expand Down
25 changes: 19 additions & 6 deletions core/frontend/src/SubCategoriesCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,14 +58,25 @@ export class SubCategoriesCache {

return !request.wasCanceled;
});

return {
missingCategoryIds: missing,
promise,
cancel: () => request.cancel(),
};
}

/** Load all subcategories that come from used spatial categories of the iModel into the cache. */
public async loadAllUsedSpatialSubCategories(): Promise<void> {
try {
const results = await this._imodel.queryAllUsedSpatialSubCategories();
if (undefined !== results){
this.processResults(results, new Set<string>(), false);
}
} catch (e) {
// In case of a truncated response, gracefully handle the error and exit.
}

}
/** Given categoryIds, return which of these are not cached. */
private getMissing(categoryIds: Id64Arg): Id64Set | undefined {
let missing: Id64Set | undefined;
Expand Down Expand Up @@ -98,9 +109,10 @@ export class SubCategoriesCache {
return new SubCategoryAppearance(props);
}

private processResults(result: SubCategoriesCache.Result, missing: Id64Set): void {
for (const row of result)
this.add(row.parentId, row.id, SubCategoriesCache.createSubCategoryAppearance(row.appearance));
private processResults(result: SubCategoriesCache.Result, missing: Id64Set, override: boolean = true): void {
for (const row of result){
this.add(row.parentId, row.id, SubCategoriesCache.createSubCategoryAppearance(row.appearance), override);
}

// Ensure that any category Ids which returned no results (e.g., non-existent category, invalid Id, etc) are still recorded so they are not repeatedly re-requested
for (const id of missing)
Expand All @@ -111,13 +123,14 @@ export class SubCategoriesCache {
/** Exposed strictly for tests.
* @internal
*/
public add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance) {
public add(categoryId: string, subCategoryId: string, appearance: SubCategoryAppearance, override: boolean) {
let set = this._byCategoryId.get(categoryId);
if (undefined === set)
this._byCategoryId.set(categoryId, set = new Set<string>());

set.add(subCategoryId);
this._appearances.set(subCategoryId, appearance);
if (override)
this._appearances.set(subCategoryId, appearance);
}

public async getCategoryInfo(inputCategoryIds: Id64String | Iterable<Id64String>): Promise<Map<Id64String, IModelConnection.Categories.CategoryInfo>> {
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/ViewCreator3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export class ViewCreator3d {

const viewState = SpatialViewState.createFromProps(props, this._imodel);
try {
await viewState.iModel.subcategories.loadAllUsedSpatialSubCategories();
await viewState.load();
} catch {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { expect } from "chai";
import { SubCategoryAppearance } from "@itwin/core-common";
import { IModelConnection, ScreenViewport, SnapshotConnection, ViewCreator3d} from "@itwin/core-frontend";
import { TestUtility } from "../TestUtility";
import sinon = require("sinon");

describe("ViewCreator3d", async () => {
let imodel: IModelConnection;
Expand Down Expand Up @@ -41,8 +42,8 @@ describe("ViewCreator3d", async () => {
// In a real scenario the visibility would be obtained from the persistent subcategory appearance - our test iModels don't contain any
// invisible subcategories to test with.
// The iModel contains one spatial category 0x17 with one subcategory 0x18. We're adding a second pretend subcategory 0x20.
imodel.subcategories.add("0x17", "0x18", new SubCategoryAppearance());
imodel.subcategories.add("0x17", "0x20", new SubCategoryAppearance());
imodel.subcategories.add("0x17", "0x18", new SubCategoryAppearance(), true);
imodel.subcategories.add("0x17", "0x20", new SubCategoryAppearance(), true);

const creator = new ViewCreator3d(imodel);
let view = await creator.createDefaultView();
Expand All @@ -55,7 +56,7 @@ describe("ViewCreator3d", async () => {
expectVisible(true, true);

const invisibleAppearance = new SubCategoryAppearance({ invisible: true });
imodel.subcategories.add("0x17", "0x18", invisibleAppearance);
imodel.subcategories.add("0x17", "0x18", invisibleAppearance, true);

view = await creator.createDefaultView();
expectVisible(false, true);
Expand All @@ -66,12 +67,33 @@ describe("ViewCreator3d", async () => {
view = await creator.createDefaultView({ allSubCategoriesVisible: true });
expectVisible(true, true);

imodel.subcategories.add("0x17", "0x20", invisibleAppearance);
imodel.subcategories.add("0x17", "0x20", invisibleAppearance, true);
view = await creator.createDefaultView({ allSubCategoriesVisible: true });
expectVisible(true, true);

view = await creator.createDefaultView();
expectVisible(false, false);
});

it("when internal logic of loadAllSubCategories throws, should fall back to loading all subcategories through standard paging", async () => {
imodel.subcategories.add("0x17", "0x18", new SubCategoryAppearance(), true);
imodel.subcategories.add("0x17", "0x20", new SubCategoryAppearance(), true);

const loadSpy = sinon.spy(imodel.subcategories, "load");
const queryStub = sinon.stub(imodel, "queryAllUsedSpatialSubCategories").rejects(new Error("Internal Server Error"));

const creator = new ViewCreator3d(imodel);
const view = await creator.createDefaultView();
function expectVisible(subcat18Vis: boolean, subcat20Vis: boolean): void {
expect(view.isSubCategoryVisible("0x18")).to.equal(subcat18Vis);
expect(view.isSubCategoryVisible("0x20")).to.equal(subcat20Vis);
}

expect(Array.from(view.categorySelector.categories)).to.deep.equal(["0x17"]);
expectVisible(true, true);
expect(loadSpy).to.be.calledOnce;
loadSpy.restore();
queryStub.restore();
});
});

Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,12 @@ describe("IModelReadRpcInterface Methods from an IModelConnection", () => {
expect(candidate1Result).to.deep.eq({ ...expectedCandidate1Result, candidate: candidates[0] });
expect(candidate2Result).to.deep.eq({ ...expectedCandidate2Result, candidate: candidates[1] });
});

it("queryAllUsedSpatialSubCategories should find subcategories coming from spatial categories of 3d Elements", async () => {
const result = await iModel.queryAllUsedSpatialSubCategories();
expect(result).to.not.be.null;
expect(result.length).to.not.be.equal(0);
});
});

describe("Snapping", () => {
Expand Down

0 comments on commit 8fba91d

Please sign in to comment.