Changed Elements API Tutorial

Introduction

The Changed Elements API is a REST API that allows the consumer to inspect what elements have changed in an iModel between two versions of the iModel. In this tutorial, we will explore using the API operations and we will write an iTwin.js application to visualize change.

Info

Skill level:

Basic

Duration:

45 minutes

1. Set up your environment

To do this tutorial, it is recommended that you do the Web Application Quick Start tutorial first. This tutorial expects that you have a registered application as shown in the quick start tutorial.

1.1 Required materials


Node.js (14.x LTS version)

This tool provides the backend JavaScript runtime necessary for your computer to read and render code appropriately. It also allows you to run NPM command line (required for every iTwin project).

Git

This is the source code control system for the iTwin repositories.

Tutorial Repository

This is the github repository that you will use in this tutorial. You should clone the start branch as a starting point to follow along. If at any point you are unsure on how to setup the code demonstrated in the snippets, or just want to take a shortcut, the repository’s main branch contains the finished tutorial application.

Clone starting point for tutorial


1 git clone https://github.com/iTwin/changed-elements-tutorial -b start

Finished tutorial application


1 git clone https://github.com/iTwin/changed-elements-tutorial

1.2 Suggested materials


Google Chrome

This software can help you to develop and debug frontend JavaScript problems.

Visual Studio Code

This is our recommended editor and debugger tool for developing iTwin.js applications. It is free, open source and includes a GUI for working with GIT.

Postman

If you want to test the REST API calls directly, you can use Postman or any other solution capable of sending HTTP requests. If you do it this way, you will require an authorization token for the requests to work.

To learn more about how authentication and authorization works in an iTwin powered application, check out the full documentation on how to obtain the token.

2. Overview of Changed Elements API

Before diving into writing code to leverage the changed elements API with an iTwin.js application, let’s take a look at the operations that are available in the API first

2.1 Enabling Change Tracking


This is the first operation of the API and is used to enable change tracking of an iModel.

The API requires the changedelements:modify scope to be added to your App. It can be added to your apps in the Developer Portal.

2.1.1 What is change tracking and why do we need it?


An iModel evolves over time. Every time a design file is changed and the iModel is synchronized, new Changesets are published to the iModel, updating the iModel data.

An iModel may contain graphical elements that are related to business data. For example, let’s think about a piece of equipment:

This equipment may be displayed in an iModel with a bare-bones Geometric Element, however, business data that relates to it (like the pump diameter in the image above), exists under the properties of a different non-graphical element.

A changeset may contain changes to the equipment’s related pump diameter property, but the geometric counterpart of the equipment will not contain a change. To be able to find which elements relate to which change, you can use the Changed Elements API.

The Changed Elements API will track the iModel for changes, and when a Named Version is created, the API will find all related elements that have changed in each of the changesets and store a summary of them in the cloud.

2.1.2 How do I enable tracking in the Changed Elements API?


The API has an operation to enable the changed elements feature for an iModel. As mentioned before, for this operation to work, your App needs to have the scope: changedelements:modify.

Here is an example request that shows how to enable the API’s change tracking for an iModel, you would just need to replace the context Id, iModel Id and authorization token with your own.

The body should contain a JSON object with a enable property, and it must be either true or false as shown in the example above.

Here is the documentation for this operation.

Example HTTP Request for Enable Change Tracking Operation


1 2 3 4 5 PUT https://api.bentley.com/changedelements/v1/tracking?contextId=myContextId&iModelId=myIModelId Authorization: Bearer Your_Token_Here Content-Type: application/json { "enable": true }

2.1.3 My iModel is being tracked for change, now what?


Once change tracking is enabled for your iModel, whenever a new Named Version gets created, either by a design application or a Connector, the API will process the changesets and store the results of what elements have changed in your iModel.

This operation can take time, and the time it takes is dependent on data complexity and size. It is recommended to use a small iModel to do this tutorial if you are following along, as a very large iModel may take hours to process if it has never been processed and has a lot of data.

2.2 Getting Changed Elements


The next operation allows us to query the API to get the elements that have changed after processing is ready. To use this operation, you must provide the start and end changeset Ids that you want to obtain results for. To get changeset Ids, you can use Get iModel Changesets API.

The API requires the changedelements:read scope to be added to your App. It can be added to your apps in the Developer Portal

2.2.1 Using the API to get changed elements


Here is an example request that shows how to get the changed elements between two changesets, ensure to replace the context Id, iModel Id, start changeset Id, end changeset Id and authorization token with your own.

This operation will return a JSON object that contains which elements have changed between the two given changeset Ids. The returned JSON corresponds to the ChangedElements interface. We will explore the format a bit more in the Changed Elements JSON Section. Here’s an example of the data returned for a single element that changed:

The results are inclusive to both start and end changesets. This means that the changes found in both changesets will be contained in the results. This is important to think about because if you have an iModel with 2 Named Versions, A and B, and you want to get what has changed between A and B, you should *not* include A's changeset in the query. Consider the following example:

In the example above, even though Changeset 2 is the changeset related to Named Version A, to get what has changed between A and B, start changeset should be Changeset 3 and end changeset should be Changeset 4, as Changeset 2 is already applied to the iModel in Named Version A.

Another thing to keep in mind is that if you want to obtain changed elements for a single changeset, since the range is inclusive, you can provide the same changeset Id for start and end changesets, and it will return the elements for the given changeset.

Here’s the documentation for this operation.

Example HTTP Request for Get Comparison Operation


1 2 3 GET https://api.bentley.com/changedelements/v1/comparison?contextId=myContextId&iModelId=myIModelId&startChangesetId=myStartChangesetId&endChangesetId=myEndChangesetId Authorization: Bearer Your_Token_Here Content-Type: application/json

Example result from the Get Comparison Operation


1 2 3 4 5 6 7 8 9 10 11 12 13 { "changedElements": { "elements":["0x30000000f69"], "classIds":["0x670"], "opcodes":[23], "modelIds":["0x20000000002"], "type":[1], "properties":[["UserLabel"]], "oldChecksums":[[1448094486]], "newChecksums":[[362149254]], "parentIds":["0"], "parentClassIds":["0"] }

3. Putting it to work

Now that we have covered how the API works, we will use it to create a simple application that can visualize change.

We will implement a simple widget that will allow us to use both operations of the changed elements API. It will have a button to enable change tracking, a dropdown that will allow us to pick a named version and another button to visualize the differences between the current version and the one selected in the dropdown.

3.1 Creating the widget


After cloning the main branch of the repository, create a new .tsx file and call it ChangedElementsWidget.tsx. We are going to write a react hook that will allow us to query the named versions of the iModel, so that we can populate the dropdown list to select our version to compare against.

The named versions object also contains the Id of the changesets, which we will use later on to do the API calls.

Now that we have a working react hook for that purpose, we can write our widget component. The widget will have a label, a react select dropdown and two buttons. We will write the button’s onClick handlers later on in the tutorial, so we will leave them empty for now.

To stylize the UI component a little bit, create a ChangedElementsWidget.scss file and add the styles shown in the code snippet.

We can now proceed to add the widget to the viewer application.

Necessary imports


1 2 3 4 5 6 7 import { Version } from "@bentley/imodelhub-client"; import { AuthorizedFrontendRequestContext, IModelApp, IModelConnection } from "@bentley/imodeljs-frontend"; import { Button } from "@bentley/ui-core"; import React, { useEffect, useState } from "react"; import { useCallback } from "react"; import Select from "react-select"; import "./ChangedElementsWidget.scss";

Write a react hook for getting named versions


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function useNamedVersions(props: { iModel: IModelConnection | undefined }) { const [versions, setVersions] = useState<Version[]>(); // Load named versions that can be used for comparison useEffect(() => { const loadChangesets = async () => { // Ensure we have a proper iModel with an iModel Id if (props.iModel?.iModelId === undefined) { console.error("iModel is not valid"); return; } // Create request context for querying named versions const requestContext = await AuthorizedFrontendRequestContext.create(); // Get the versions and set them to our state setVersions(await IModelApp.iModelClient.versions.get(requestContext, props.iModel.iModelId)); }; // Call the asynchronous function to load named versions loadChangesets(); }, [props.iModel]); return versions; }

Write the widget UI component


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 export interface ChangedElementsWidgetProps { iModel: IModelConnection | undefined; } export function ChangedElementsWidget(props: ChangedElementsWidgetProps) { // Named versions of the iModel const versions = useNamedVersions(props); // Named version selected in dropdown const [selectedVersion, setSelectedVersion] = useState<Version | undefined>(); // Callback for when clicking the 'Visualize Changed Elements' button const onVisualizeChangedElements = useCallback(async () => { // We will implement this later in the tutorial }, []); // Callback for when clicking the 'Enable Change Tracking' button const onEnableTracking = useCallback(async () => { // We will implement this later in the tutorial }, []); const selectOptions = []; if (versions) { for (const version of versions) { selectOptions.push({ value: version, label: version.name ?? "Unknown Named Version" }); } } // On react select change set the new selected version const onReactSelectChange = (option: { value: Version | undefined, label: string } | null) => { setSelectedVersion(option?.value); }; return ( <div className="widget-container"> <div className="widget-label">Select Named Version:</div> <Select value={{ value: selectedVersion, label: selectedVersion?.name ?? "" }} options={selectOptions} onChange={onReactSelectChange} /> <Button className={"widget-button"} onClick={onVisualizeChangedElements}> Visualize Changed Elements </Button> <Button className={"widget-button"} onClick={onEnableTracking}> Enable Change Tracking </Button> </div> ); }

SCSS for the UI Component


1 2 3 4 5 6 7 8 9 10 11 12 .widget-container { margin: 8px; } .widget-label { color: white; font-size: 14px; } .widget-button { width: 100%; }

3.2 Adding the widget to the application


Now that we have the component ready, we need to create a UiItemsProvider that will feed the our widget to the viewer. Create a new file ChangedElementsUiProvider.tsx to put the code in. Then, import the ChangedElementsUiProvider in our App.tsx file, and add the provider to the uiProviders array prop of the Viewer react component.

If you want a more in-depth explanation on the usage of the provider, see iTwin Viewer Hello World tutorial.

Then, we must pass the provider to the viewer’s react component, and it should now show in right panel like so:

If you want to verify that you have added the code in the right place, you can check the final results for App.tsx, ChangedElementsWidget.tsx and ChangedElementsUiProvider.tsx. Keep in mind that the ChangedElementsWidget.tsx code in the repository is already in its final state, containing button handlers that we will be adding later on in this tutorial.

Create a UI Items Provider


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 import { AbstractWidgetProps, StagePanelLocation, StagePanelSection, UiItemsProvider, } from '@bentley/ui-abstract'; import { UiFramework } from '@bentley/ui-framework'; import * as React from 'react'; import { ChangedElementsWidget } from './ChangedElementsWidget'; export class ChangedElementsUiProvider implements UiItemsProvider { public readonly id = 'ChangedElementsProviderId'; public provideWidgets( stageId: string, stageUsage: string, location: StagePanelLocation, section?: StagePanelSection ): ReadonlyArray<AbstractWidgetProps> { const widgets: AbstractWidgetProps[] = []; if ( location === StagePanelLocation.Right && section === StagePanelSection.Start ) { const changedElementsWidget: AbstractWidgetProps = { id: 'ChangedElementsWidget', label: 'Changed Elements', getWidgetContent() { return ( <ChangedElementsWidget iModel={UiFramework.getIModelConnection()} /> ); }, }; widgets.push(changedElementsWidget); } return widgets; } }

Import the ChangedElementsUiProvider in App.tsx


1 import { ChangedElementsUiProvider } from "./ChangedElementsUiProvider";

Add the UI Items Provider to the viewer in App.tsx


1 2 3 4 5 6 7 8 <Viewer contextId={contextId} iModelId={iModelId} authConfig={{ config: authConfig }} onIModelAppInit={onIModelAppInit} backend={{ buddiRegion: 103 }} uiProviders={[new ChangedElementsUiProvider()]} />

3.3 Writing a client for the API


To use the API in our viewer, we will need to create a client class that interfaces with the API and gives us the proper results.

Create a new file called ChangedElementsClient.ts and create a class like shown in the code snippet. We are first going to write functions to create the correct URLs for our operations based on some input parameters, like our iModel’s Id and changeset Ids.

Now that we have the scaffolding for the client class, let’s add functions for calling the API for each operation.

The getComparison function will use the Get Comparison endpoint to get the ChangedElements that were found between the given changesets, for a given iModel.

The enableChangeTracking function will use the Enable Change Tracking endpoint to enable or disable tracking for an iModel.

Both operations require an authorization token. The repository comes with a simple AuthorizationClient class that will get the necessary tokens as long as your .env file is properly setup as explained in the README of the repo

Here’s what the ChangedElementsClient.ts file should look like when you are done with this section.

Changed Elements Client


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 import { ChangedElements } from "@bentley/imodeljs-common"; import { IModelConnection } from "@bentley/imodeljs-frontend"; import { AccessToken, AuthorizedClientRequestContext, IncludePrefix, request, RequestOptions } from "@bentley/itwin-client"; import { AuthorizationClient } from "./AuthorizationClient"; /** * Class for using the Changed Elements API */ export class ChangedElementsClient { /** * Get base URL for changed elements API * @returns URL for changed elements API */ public getUrl() { return "https://api.bentley.com/changedelements"; } /** * Function to form the URL for the comparison operation of changed elements API * @param iModelId iModel Id to query for * @param contextId Project Id of the iModel * @param startChangesetId Start changeset for comparison data * @param endChangesetId End changeset for comparison data * @returns Url for querying changed elements from the changed elements API */ public getComparisonOperationUrl( iModelId: string, contextId: string, startChangesetId: string, endChangesetId: string ) { return this.getUrl() + "/comparison?iModelId=" + iModelId + "&contextId=" + contextId + "&startChangesetId=" + startChangesetId + "&endChangesetId=" + endChangesetId; } /** * Function to form the URL for the enable change tracking operation of changed elements API * @param iModelId iModel Id to query for * @param contextId Project Id of the iModel * @returns Url for enabling/disabling change tracking */ public getEnableChangeTrackingUrl( iModelId: string, contextId: string, ) { return this.getUrl() + "/tracking?iModelId=" + iModelId + "&contextId=" + contextId; } /** * Tries to get an access token from the authorization client * Should work if your .env is setup properly and your application's client * is setup correctly * @returns AccessToken or undefined */ private async getAccessToken(): Promise<AccessToken | undefined> { try { return await AuthorizationClient.apimClient.getAccessToken(); } catch(e) { console.error(e); return undefined; } } /** * Headers for requests * @param requestContext * @returns */ public getHeaderOptions(accessToken: AccessToken) { return { Authorization: accessToken.toTokenString(IncludePrefix.Yes), } } /** * Gets the changed elements between two changesets using the changed elements API * This results in a GET request to the comparison endpoint * @param iModel iModel to test * @param startChangesetId Start changeset Id * @param endChangesetId End changeset Id * @returns ChangedElements object or undefined */ public async getComparison( iModel: IModelConnection, startChangesetId: string, endChangesetId: string ): Promise<ChangedElements | undefined> { const accessToken = await this.getAccessToken(); if (accessToken === undefined) { throw new Error("Could not get access token"); } // Create a request context const requestContext = new AuthorizedClientRequestContext(accessToken); // Parse out iModel Id and Context Id const iModelId = iModel.iModelId; const contextId = iModel.contextId; // Ensure they are properly defined if (iModelId === undefined || contextId === undefined) { throw new Error("IModel is not properly defined"); } // Get the request URL for the comparison operation const url: string = this.getComparisonOperationUrl(iModelId, contextId, startChangesetId, endChangesetId); // Options for the request const options: RequestOptions = { method: "GET", headers: this.getHeaderOptions(accessToken) }; try { // Execute the request const response = await request(requestContext, url, options); // Ensure we got a proper response if (response.status === 200 && response.body?.changedElements !== undefined) { // If so, cast the changedElements object of the body as a ChangedElements type return response.body.changedElements as ChangedElements; } // Something went wrong, log it to console console.error("Could not get changed elements. Status: " + response.status + ". Body: " + response.body); } catch (e) { console.error("Error obtaining changed elements: " + e); } // We did not get a proper response, return undefined return undefined; } /** * Enable or disable change tracking for an iModel * This will cause the iModel to be monitored for named versions * Whenever a named version gets created, the changed elements API will process the changesets * so that a comparison operation can be made against the new named versions * @param iModel IModel to track change for * @param value true for enabling, false for disabling * @returns true if successful, false if failed */ public async enableChangeTracking( iModel: IModelConnection, value: boolean, ): Promise<boolean> { const accessToken = await this.getAccessToken(); if (accessToken === undefined) { throw new Error("Could not get access token"); } // Create a request context const requestContext = new AuthorizedClientRequestContext(accessToken); // Parse out iModel Id and Context Id const iModelId = iModel.iModelId; const contextId = iModel.contextId; // Ensure they are properly defined if (iModelId === undefined || contextId === undefined) { throw new Error("IModel is not properly defined"); } // Get the request URL for the comparison operation const url: string = this.getEnableChangeTrackingUrl(iModelId, contextId); // Options for the request const options: RequestOptions = { method: "PUT", headers: this.getHeaderOptions(accessToken), body: { enable: value } }; try { // Execute the request const response = await request(requestContext, url, options); // Ensure we get a proper response if (response.status === 202) { return true; } // Something went wrong, log it to console console.error("Could not enable change tracking. Status: " + response.status + ". Body: " + response.body); } catch (e) { console.error("Error change tracking: " + e); } // We did not get a proper response, return undefined return false; } }

3.4 Using the client


Now, we must implement the onClick handlers of the widget buttons so that they use the client and call the necessary API endpoints. First, ensure you import the ChangedElementsClient to be used in the ChangedElementsWidget.tsx file.

For enabling change tracking, all we need to do is use the client’s enableChangeTracking method we wrote earlier. Paste the code in the onEnableTracking callback that we left blank earlier in Section 3.1. For this to work, we just need to pass the iModel object and true to the client’s function. If you have not enabled change tracking on your iModel yet, do so now. Keep in mind that as mentioned in section 2.1.3, processing may take a while, so take a break and get some coffee to ensure that the API processes your iModel.

For visualizing changed elements, we are going to need to use the client’s getComparison method. This requires us to pass the iModel object, startChangesetId and endChangesetId. We can obtain the iModel from the passed props of the widget. The endChangesetId we can obtain by looking at the iModel’s current changeset. For startChangesetId, we need to use the selectedVersion variable we setup in the widget that should contain the changeset Id of the named version that got selected by the user. Paste the code in the onVisualizeChangedElements callback that we left blank earlier in Section 3.1. If you get a 404 response from the Get Comparison operation, it means the API has not yet processed your iModel, and you must wait.

If authorization has been properly setup, you should obtain results from the API. To display the results in the viewport, let’s emphasize the elements that have changed on the screen. We can use the EmphasizeElements class, which is a FeatureOverrideProvider that will highlight the elements on the viewport.

The changedElements.elements is an array that contains the element Ids of all elements that have changed in the iModel between the changesets that we are looking at. Here’s more information about the ChangedElements result.

Here’s what the ChangedElementsWidget.tsx file should look like when you are done with this section.

Import the ChangedElementsClient class in ChangedElementsWidget


1 import { ChangedElementsClient } from "./ChangedElementsClient";

Enable Change Tracking Button Handler


1 2 3 4 5 6 7 8 9 10 11 // Callback for when clicking the 'Enable Change Tracking' button const onEnableTracking = useCallback(async () => { const iModel = props.iModel; // Ensure our iModel is defined if (iModel) { // Create a changed elements client object const client = new ChangedElementsClient(); // Enable change tracking for the iModel await client.enableChangeTracking(iModel, true); } }, [props.iModel]);

Visualize Changed Elements Button Handler


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // Callback for when clicking the 'Visualize Changed Elements' button const onVisualizeChangedElements = useCallback(async () => { const iModel = props.iModel; if (iModel === undefined || iModel.changeSetId === undefined) { console.error("iModel is not valid"); return; } if (selectedVersion?.changeSetId === undefined) { console.error("Selected version is not defined"); return; } const client = new ChangedElementsClient(); const endChangesetId = iModel.changeSetId; const startChangesetId = selectedVersion?.changeSetId; const changedElements = await client.getComparison( iModel, startChangesetId, endChangesetId ); // Log the results to console to inspect them console.log(changedElements); const viewport = IModelApp.viewManager.selectedView; if (changedElements && viewport) { // Emphasize the changed elements in the view EmphasizeElements.getOrCreate(viewport).emphasizeElements( changedElements.elements, viewport ); } }, [selectedVersion, props.iModel]);

3.5 Enhancing the change visualization


By now, you should have changed elements being emphasized in your view, but it would be better if we colorize them by their operation codes, see DbOpcode.

To be able to colorize the elements, we will implement our own FeatureOverrideProvider. Create a new file ChangedElementsFeatureOverrides.ts and follow the code snippet to the right.

The provider’s constructor finds which elements are inserted or updated based on their DbOpcode. Then, it implements the addFeatureOverrides function that will colorize inserted elements as green, updated elements as blue and make everything else transparent gray.

Displaying deleted elements is not as straightforward because the deleted elements are not present in the current iModel that we are displaying in the viewport. This can be done by implementing a TiledGraphicsProvider, but this is out of the scope for this tutorial.

Now that we have a feature override provider that will colorize our elements properly, let’s use it in our onVisualizeChangedElements button callback. Import the class in ChangedElementsWidget.tsx.

Before adding a provider to the viewport, t’s important to drop any feature override providers from the viewport before we add one to ensure we start with a clean viewport each time.

You should now be able to see the elements colorized, showing inserted elements as green and updated elements as blue whenever we click the visualize button:

Here’s what the ChangedElementsFeatureOverrides.ts file should look like when you are done with this section.

Changed Elements Feature Override Provider


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 import { DbOpcode, Id64String } from "@bentley/bentleyjs-core"; import { ChangedElements, FeatureAppearance } from "@bentley/imodeljs-common"; import { FeatureOverrideProvider, FeatureSymbology, Viewport } from "@bentley/imodeljs-frontend"; /** * Feature Override Provider to visualize changed elements and colorize them * in the viewport */ export class ChangedElementsFeatureOverrides implements FeatureOverrideProvider { // Array of inserted element Ids private _insertedElements: Id64String[] = []; // Array of updated element Ids private _updatedElements: Id64String[] = []; /** * Constructor * @param changedElements Changed Elements to visualize */ public constructor(changedElements: ChangedElements) { // Go over all changed elements array, all arrays are guaranteed to have same size for (let i = 0; i < changedElements.elements.length; ++i) { // Element Id of the changed element const elementId: Id64String = changedElements.elements[i]; // Operation code of the changed element const opcode: DbOpcode = changedElements.opcodes[i]; // Add the element Id to the proper list switch (opcode) { case DbOpcode.Delete: // Deleted elements do not exist in the current version of the iModel // Displaying non-iModel elements in the same view is out of scope for this tutorial break; case DbOpcode.Update: this._updatedElements.push(elementId); break; case DbOpcode.Insert: this._insertedElements.push(elementId); break; } } } /** * Adds the colorization and emphasis of the elements we care about * @param overrides Overrides to be updated with our changed elements * @param viewport Viewport we are overriding features on */ public addFeatureOverrides(overrides: FeatureSymbology.Overrides, viewport: Viewport): void { // Create a default appearance for non-changed elements, set it to transparent light gray const defaultAppearance = FeatureAppearance.fromJSON({ rgb: {r: 200, g: 200, b: 200}, transparency: 0.9, // Make unchanged elements non-locatable // This is to allow selecting changed elements that are behind unchanged elements in the view nonLocatable: true, }); // Override the default coloring for all other elements overrides.setDefaultOverrides(defaultAppearance); // Create an appearance with the color green for inserted elements and emphasize them const insertedAppearance = FeatureAppearance.fromJSON({ rgb: {r: 0, g: 255, b: 0}, emphasized: true, }); // Override the inserted elements with the appearance this._insertedElements.forEach((elementId: string) => { overrides.overrideElement(elementId, insertedAppearance); }); // Create an appearance with the color blue for updated elements const updatedAppearance = FeatureAppearance.fromJSON({ rgb: {r: 0, g: 0, b: 255}, emphasized: true }); // Override the updated elements with the appearance this._updatedElements.forEach((elementId: string) => { overrides.overrideElement(elementId, updatedAppearance); }); } }

Updated Visualize Changed Elements Button Callback


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 // Updated callback for when clicking the 'Visualize Changed Elements' button const onVisualizeChangedElements = useCallback(async () => { const iModel = props.iModel; if (iModel === undefined || iModel.changeSetId === undefined) { console.error("iModel is not valid"); return; } if (selectedVersion?.changeSetId === undefined) { console.error("Selected version is not defined"); return; } const client = new ChangedElementsClient(); const endChangesetId = iModel.changeSetId; const startChangesetId = selectedVersion?.changeSetId; const changedElements = await client.getComparison( iModel, startChangesetId, endChangesetId ); // Log the results to console to inspect them console.log(changedElements); const viewport = IModelApp.viewManager.selectedView; if (changedElements && viewport) { // Ensure we are not currently visualizing changed elements const oldProvider = viewport.findFeatureOverrideProviderOfType(ChangedElementsFeatureOverrides); if (oldProvider) { // If we are, drop the override provider so that we start with a clean viewport viewport.dropFeatureOverrideProvider(oldProvider); } // Create our feature override provider object const overrideProvider = new ChangedElementsFeatureOverrides(changedElements); // Add it to the viewport viewport.addFeatureOverrideProvider(overrideProvider); // Store changed elements in component setChangedElements(changedElements); } }, [selectedVersion]);

3.6 About changed properties


We will cover working with properties from Changed Elements API in a different tutorial, but here’s an overview of what’s provided in the API:

The API returns a properties array that contains the EC Property Access Strings of any properties that had changes between the two versions being queried. The properties array is 2-dimensional, the first index corresponds to the element you are looking at, the second index will allow you to iterate through all properties that changed in the element.

As explained in the Enabling Change Tracking Section, the properties of an element may not live in the element itself, so we can’t simply query for its value using ECSQL, the element Id and the property access string.

To properly inspect an elements properties, including properties that do not exist on the element, we need to use iTwin.js Presentation Library, which is out of scope for this tutorial.

The change data also contains the newChecksums and oldChecksums arrays. Each property of an element will have a matching new and old checksum. These arrays are useful to quickly check if the property value indeed has changed, or if it has flipped back and forth to the same value between versions.

4. Making sense of Changed Elements data

The returned data is a JSON object that contains arrays of data. Each of these arrays are of the same length. This length is the number of changed elements that were found during processing.

4.1 Changed Elements JSON


ChangedElements class is defined in the @bentley/imodeljs-common package.

As mentioned before, this interface contains different arrays. Let’s go over each of the arrays and what they are:

elements

Contains the element Ids of the changed elements. This is useful if you want to query the iModel for more information about the element.

classIds

Contains the ECClass Ids of the changed elements. This is useful if you want to access specific properties of the element that are in its ECClass.

opcodes

Contains the operation codes that tells us if the element was inserted, updated or deleted in the change. See DbOpcode

type

Contains the type of change that occurred to the element. This number is a bitflag, and can be used to know whether the element had property changes, geometric changes, placement changes, indirect changes and/or hidden property changes.

modelIds

Contains the model Ids of the changed elements. This is useful for visualization purposes, like ensuring the model the changed element resides in is displayed in the viewport.

properties

Contains the property accessor string names of changed properties an element may have. This property accessor string can be used in conjunction with the element’s class Id to obtain the property value.

oldChecksums

Contains the old checksum for the property value of the index that matches the property array. This is useful to determine using newChecksums array, whether the property value has indeed changed in the before and after states of the iModel. This is useful because there are cases in which a property may be flipped back and forth, and you may still want to know it was touched, but you can determine whether the change is valuable using a simple checksum comparison.

newChecksums

Contains the new checksum for the property value of the index that matches the property array

parentIds

Contains the parent Id of the element. If the element does not have a parent, this id will be “0”

parentClassIds

Contains the ECClass Id of the parent of the element. If the element does not have a parent, this id will be “0”

Conclusion

Congratulations on completing this tutorial, at this point you should have been able to visualize change using the API! In conclusion, the Changed Elements API can help you understand what elements have changed in your iModel and how they have changed between the given changesets. You could use this API to generate reports, visualize change using an iTwin.js application like we did in this tutorial, or review properties that have changed on the elements that you find relevant in your iModel.

More resources that you may like

Create React App

Set up a modern web app by running one command.

iTwin Viewer React

The iTwin Viewer is a configurable iTwin.js viewer that offers basic tooling and widgets out-of-the-box and can be further extended through the use of iModel.js extensions. This package contains the Viewer as a React component and some additional Typescript API’s.

iTwin Viewer Create React App Template

This is a template for applications that are based on the iTwin Viewer for Create React App.

Bentley React Scripts

This is the iTwin.js fork of react-scripts.