Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load all subcategories when creating a default view. #6882

Merged
merged 16 commits into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
loadAllSubCategories(): 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 loadAllSubCategories(): Promise<void> {
hl662 marked this conversation as resolved.
Show resolved Hide resolved
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) {
pmconne marked this conversation as resolved.
Show resolved Hide resolved
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.loadAllSubCategories();
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
Loading