Changed Elements API V2 Tutorial

Tutorial result example

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 v2 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 Get started with iTwin Platform first. This tutorial expects that you have a registered application as shown in the quick start tutorial.

1.1 Required materials

Node.js (18.x 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-v2 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 v2 branch contains the finished tutorial application.

Clone starting point for tutorial


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

Finished tutorial application


bash
git clone https://github.com/iTwin/changed-elements-tutorial -b v2

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.

You can also try it out in the API documentation page. Each operation has a try it out button to play with it.

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 Create Comparison Job

This operation creates a comparison job. The comparison job will be queued and will progress to completion. To use this operation, you must provide the start and end changeset Ids that you want to obtain results for. Here is an example request and response. ensure to replace the iTwin Id, iModel Id, start & end changesetId, and authorization token with your own.

see Create Comparison Job API for more details. To get changeset Ids, you can use Get iModel Changesets API.

Once comparison job is ready, you can use Get Comparison Job to retrieve the comparison result.

Note: changeset ranges are inclusive, also see Get Comparison Job for more details.

Example HTTP Request for Create Comparison Job Operation


http
POST https://api.bentley.com/changedelements/comparisonjob HTTP/1.1 
Authorization: Bearer Your_Access_Token
Accept: application/vnd.bentley.itwin-platform.v2+json
Content-Type: application/json
{
"iTwinId": "myItwinId",
"iModelId": "myIModelId",
"startChangesetId": "myStartChangesetId",
"endChangesetId": "myEndChangesetId"
}

Example result from the Create Comparison Operation


json
{
      "comparisonJob": {
          "status": "Queued",
          "jobId": "myStartChangesetId-myEndChangesetId",
          "iTwinId": "myItwinId",
          "iModelId": "myIModelId",
          "startChangesetId": "myStartChangesetId",
          "endChangesetId": "myEndChangesetId"
      }
    }

2.2 Delete Comparison Job

This operation deletes an existing job based on the job Id, iTwin Id, and iModel Id. All stored data relating to comparison will be deleted.

Job Id is normally comprised of start changeset id and end changeset id, in the following format: startChangesetId-endChangesetId.

A successful deletion returns 204 (No Content Response). see Delete Comparison Job API for more details.

Example HTTP Request for Delete Comparison Job Operation


http
DELETE https://api.bentley.com/changedelements/comparisonjob/myjobId/itwin/myiTwinId/imodel/myiModelId HTTP/1.1
Authorization: Bearer Your_Access_Token
Accept: application/vnd.bentley.itwin-platform.v2+json

2.3 Get Comparison Job

This operation retrieves the status of a comparison job. If the processing is complete, it provides a link to the resulting comparison summary. To use this operation, a Job Id must be provided (startChangesetId-endChangesetId). see Get Comparison Job API for more details.

Here is an example request that shows how to get the changed elements between two changesets. This operation will return an href that contains which elements have changed between the two given changeset Ids.

You can also see an example of a successful response. The data in the href corresponds to the ChangedElements interface.

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.

Example HTTP Request for Get Comparison Operation


http
GET https://api.bentley.com/changedelements/comparisonjob/myjobId/itwin/myiTwinId/imodel/myiModelId HTTP/1.1
Authorization: Bearer Your_Access_Token
Accept: application/vnd.bentley.itwin-platform.v2+json

Example result from the Get Comparison Operation


json
{
    "comparisonJob": {
        "comparison": {
            "href": "https://api.bentley.com/path1/path2/id"
        },
        "status": "Completed",
        "jobId": "16063aa71dfbcee75d32a7c5a31ca40e9bb2b094-8968f5c4449d26c0dababf37aed17dcc49d7059f",
        "iTwinId": "1036c64d-7fbe-47fd-b03c-4ed7ad7fc829",
        "iModelId": "0db82dc1-e871-4209-b40a-6753c6a68c19",
        "startChangesetId": "16063aa71dfbcee75d32a7c5a31ca40e9bb2b094",
        "endChangesetId": "8968f5c4449d26c0dababf37aed17dcc49d7059f",
        "currentProgress": 10,
        "maxProgress": 10
      }
    }

3. Putting it to work

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

As mentioned in set up environment section, pull from start-v2 branch. Get started with iTwin Platform will give you the directions on how to set up the .env file.

Now we will implement a simple widget that will allow us to use the three operations of the changed elements API. It will have a dropdown to pick a named version, a button to create a comparison job, a button to delete a comparison job and a button to visualize the differences between the current version and the one selected in the dropdown.

3.1 Creating the Changed Elements Client

After cloning the start-v2 branch of the repository, create a new .tsx file and call it ChangedElementClient.tsx. This file will contain all the API interactions.

We will create 2 functions first: getAuthorization for getting the access token, fetchVisibleNamedVersions for fetching all visible named versions from current imodel. The named versions object also contains the Id of the changesets, which we will use later on to do the API calls.

ChangedElementsClient.ts


typescript
import { IModelApp } from "@itwin/core-frontend"
import { Authorization, IModelsClient, NamedVersion, NamedVersionState, toArray } from "@itwin/imodels-client-management";

export class ChangedElementClient {
  public static async getAuthorization(): Promise<Authorization> {
      if (!IModelApp.authorizationClient)
        throw new Error("AuthorizationClient is not defined. Most likely IModelApp.startup was not called yet.");
    
      const token = await IModelApp.authorizationClient.getAccessToken();
      const parts = token.split(" ");
      return parts.length === 2
        ? { scheme: parts[0], token: parts[1] }
        : { scheme: "Bearer", token };
  }

  static async fetchVisibleNamedVersions(iModelId: string): Promise<NamedVersion[]> {
      const client = new IModelsClient();
      const iModelIterator = client.namedVersions.getRepresentationList({
        urlParams: { $top: 10 },
        iModelId,
        authorization: () => ChangedElementClient.getAuthorization(),
      });
    
      const versions = (await toArray(iModelIterator)).filter(
        (v) => v.state === NamedVersionState.Visible
      );
      return versions;
    }
}

3.2 Creating the Changed Elements Widget

Then we can create another new .tsx file and call it ChangedElementsWidget.tsx. This file will contain all the UI for our custom widget.

we will have a useEffect that fetch versions using the ChangedElementClient.fetchVisibleNamedVersions we just wrote. Then display those versions in a drop down wrapper.

In this way, we have populated the dropdown list to select our version to compare against.

In our widget component, there should also be a button for create comparison, a button for delete comparison, a button for visualize change and a progress on comparison job status. We will write those implementation 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.

ChangedElementsWidget.tsx


typescript
import { IModelConnection } from "@itwin/core-frontend";
import { useState, useEffect } from "react";
import { NamedVersion } from "@itwin/imodels-client-management";
import { ChangedElementClient } from "./changeElementsClient";
import { Button, LabeledSelect, toaster, Text } from "@itwin/itwinui-react";
import "./ChangedElementsWidget.scss";

export interface ChangedElementsWidgetProps {
  iModel: IModelConnection | undefined;
}

export function ChangedElementsWidget(props: ChangedElementsWidgetProps){
  const [namedVersions, setNamedVersions] = useState<NamedVersion[]>([]);
  const [selectedVersionIndex, setSelectedVersionIndex] = useState<number>(0);
  const namedVersionsOptions = namedVersions.map((version, index) => ({ 
    value: index, 
    label: version.displayName.toString() 
  }));

  // fetch named versions 
  useEffect(() => {
      const fetchVersions = async () => {
          if (!props.iModel?.iModelId) return;
          const versionsArray = await ChangedElementClient.fetchVisibleNamedVersions(props.iModel.iModelId);

          setNamedVersions(versionsArray);
          };
  
      fetchVersions();
  }, [props.iModel]);


  return (
      <div  className="widget-container">
          <Text>Changed Element Widget</Text> 
          <LabeledSelect
              label="Select Version"
              displayStyle="inline"
              options={namedVersionsOptions}
              value={selectedVersionIndex}
              onChange={(value)=> {setSelectedVersionIndex(value)}}
          ></LabeledSelect>
          <Text className="widget-progress-text">Comparison Progress: NA</Text> 

          <Button onClick={()=>{}}>Create Comparison</Button>
          <Button onClick={()=>{}}>Visualize Comparison</Button>
          <Button onClick={()=>{}}>Delete Comparison</Button>
      </div>
  );
}

ChangedElementsWidget.scss


scss
.widget-container {
  margin: 8px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.widget-label {
  color: white;
  font-size: 14px;
}

.widget-progress-text {
  font-size: 14px;
  font-weight: bold;
}

3.3 Adding the widget to the application

Now that we have the component ready, import ChangedElementsWidget to App.tsx, create an UiItemsProvider and name it changedElementsWidgetProvider in App.tsx that contains our just created ChangedElementsWidget. Add this changedElementsWidgetProvider to the list of uiProvider in the Viewer component being returned by App.tsx.

Then you should be able to see the new widget!

Check App.tsx to see if you have done it correctly.

Create an UI Items Provider


typescript
const changedElementsWidgetProvider: UiItemsProvider = {
  id: "example:Provider",
  getWidgets: () => [
    {
      id: "example:Widget",
      content: <ChangedElementsWidget iModel={UiFramework.getIModelConnection()}/>,
      layouts: {
        standard: {
          location: StagePanelLocation.Right,
          section: StagePanelSection.Start,
        },
      },
    },
  ],
};

Add to Viewer in App.tsx


typescript
uiProviders={[
        changedElementsWidgetProvider,
        new ViewerNavigationToolsProvider(),
        ... rest of code]}

3.3 Writing a client for the API

For the 3 operations mentioned in Overview of Changed Elements API. let's add functions for calling the API for each operation.

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

We are capturing the error message in this case so that we can log the error messages out with toaster later.

Ccreate, Delete, and Get API calls in changeElementsClient.tsx


typescript
public static async createComparisonJob(iModel:IModelConnection, startChangesetId: string | null) {
      const iModelId = iModel.iModelId;
      const iTwinId = iModel.iTwinId;
      const endChangesetId = iModel.changeset.id;
      
      if (iModelId === undefined || iTwinId === undefined) {
          throw new Error("IModel is not properly defined");
      }
      if (startChangesetId === null) {
          throw new Error("start Changeset ID is not properly defined");
      }

      const authorization = await this.getAuthorization();

      const url = "https://api.bentley.com/changedelements/comparisonjob";
      const body = {
          iTwinId,
          iModelId,
          startChangesetId,
          endChangesetId
      };

      const options = {
          method: "POST",
          headers: {
          Authorization: authorization.scheme.toString() + " " + authorization.token.toString(),
          "Content-Type": "application/json",
          Accept: "application/vnd.bentley.itwin-platform.v2+json"
          },
          body: JSON.stringify(body)
      };

      try {
          const response = await fetch(url, options);
          if (!response.ok) {
            const errBody = await response.json()
            throw new Error(errBody?.error?.message.toString());
          }
          const data = await response.json();
          return data?.comparisonJob;
      } catch (error) {
          throw error;
      }
  }

  public static async deleteComparisonJob(iModel:IModelConnection, startChangesetId: string | null) {
      const iModelId = iModel.iModelId;
      const iTwinId = iModel.iTwinId;
      const endChangesetId = iModel.changeset.id;

      if (iModelId === undefined || iTwinId === undefined) {
          throw new Error("IModel is not properly defined");
      }

      if (startChangesetId === null) {
        throw new Error("start Changeset ID is not properly defined");
      }

      const jobId = startChangesetId.toString() + "-" + endChangesetId.toString();

      const authorization = await this.getAuthorization();
      const url = "https://api.bentley.com/changedelements/comparisonjob/" + jobId.toString() + "/itwin/" + iTwinId.toString() + "/imodel/" + iModelId.toString();
      const options = {
        method: "DELETE",
        headers: {
          Authorization: authorization.scheme.toString() + " " + authorization.token.toString(),
          Accept: "application/vnd.bentley.itwin-platform.v2+json",
        },
      };
  
      try {
        const response = await fetch(url, options);
        if (!response.ok) {
          const errBody = await response.json()
          throw new Error(errBody?.error?.message.toString());
        }
        // If successful, it returns 204 No Content
        return true;
      } catch (error) {
        throw error;
      }
  }


  public static async getComparisonJob(iModel:IModelConnection, startChangesetId: string | null){
      const iModelId = iModel.iModelId;
      const iTwinId = iModel.iTwinId;
      const endChangesetId = iModel.changeset.id;
      
      if (iModelId === undefined || iTwinId === undefined) {
          throw new Error("IModel is not properly defined");
      }

      if (startChangesetId === null) {
        throw new Error("start Changeset ID is not properly defined");
      }

      const authorization = await this.getAuthorization();
      const jobId = startChangesetId.toString() + "-" + endChangesetId.toString();

      const url = "https://api.bentley.com/changedelements/comparisonjob/" + jobId.toString() + "/itwin/" + iTwinId.toString() + "/imodel/" + iModelId.toString();

      const options = {
          method: "GET",
          headers: {
              Authorization: authorization.scheme.toString() + " " + authorization.token.toString(),
              Accept: "application/vnd.bentley.itwin-platform.v2+json",
          },
      };

      try {
          const response = await fetch(url, options);
          if (response.status === 404) {
              return null;  // job not found is expected since it gets triggered in interval
          }
          if (!response.ok) {
            const errBody = await response.json()
            throw new Error(errBody?.error?.message.toString());
          }
          const data = await response.json();

          return data;
      } catch (error) {
          throw error;
      }
  }
  

3.3.1 Getting Changed Element from Href

Since the comparison result is contained in an href returned by get comparison, we would need an extra function in changeElementsClient.ts to handle that. This method would simply retrieve the changedElements from the link and return that data.

Getting Changed Elements from Href


typescript
public static async getChangedElementsFromHref(href: string): Promise<ChangedElements | undefined> {
      const options = {
        method: "GET",
      };
  
      try {
        const response = await fetch(href, options);
        if (!response.ok) {
          throw new Error(response.statusText);
        }
        const data = await response.json();
        return data?.changedElements as ChangedElements;
      } catch (error) {
        throw error;
      }
  }
  

3.4 Adding the Post, Delete, Get Operations to Widget.

Now, we must implement the onClick handlers of the widget buttons so that they use the client and call the necessary API endpoints. Each button onClick method basically encapsulate the corresponding client method and display any messages with the toaster. Remember to add them to each Button's component.

The handleVisualizeComparison method is missing a step on displaying the changeElements visually, we will cover that in the next section.

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

handle button onClick in ChangedElementsWidget


typescript
  
  // functions for on click events
  const handleCreateComparison = async () => {
      if (!props.iModel) return; 
      try {
          await ChangedElementClient.createComparisonJob(
            props.iModel, 
            namedVersions[selectedVersionIndex].changesetId
          );
          toaster.positive(<Text>Comparison job created successfully.</Text>);
      } catch (error) {
          toaster.negative(
          <>
              <Text>Failed to create comparison</Text>
              <Text variant="small">{(error as Error)?.message ?? "Error creating comparison"}</Text>
          </>
          );
      }
      };
      
  const handleVisualizeComparison = async () => {
      if (!props.iModel) return; 
      try {
          const comparisonData = await ChangedElementClient.getComparisonJob(
              props.iModel, 
              namedVersions[selectedVersionIndex].changesetId
          );
          if (!comparisonData) {
              toaster.negative(<Text>Comparison job not found</Text>);
              return;
          }
          const href = comparisonData?.comparisonJob?.comparison?.href;
          if (!href) {
              toaster.negative(<Text>Comparison job not ready</Text>);
              return;
          }
          const changedElements = await ChangedElementClient.getChangedElementsFromHref(href);
          
          // visualize change here
          
          } catch (error) {
              toaster.negative(
              <>
                  <Text>Failed to visualize comparison</Text>
                  <Text variant="small">{(error as Error)?.message ?? "Error getting comparison"}</Text>
              </>
              );
      }
  };
  
  const handleDeleteComparison = async () => {
      if (!props.iModel) return;
      try {
          await ChangedElementClient.deleteComparisonJob(
              props.iModel, 
              namedVersions[selectedVersionIndex].changesetId
              );
  
      } catch (error) {
          toaster.negative(
          <>
              <Text>Error deleting comparison job</Text>
              <Text variant="small">{(error as Error)?.message ?? "Error deleting comparison"}</Text>
          </>
          );
      }
  };

Button Component


html
<Button onClick={handleCreateComparison}>Create Comparison</Button>
<Button onClick={handleVisualizeComparison}>Visualize Comparison</Button>
<Button onClick={handleDeleteComparison}>Delete Comparison</Button>

3.6 Change Visualization

Now lets visualize the changed elements retrieved from the href link. This will involve creating a feature override provider to modify viewport appearance according to changed elements. Then we just need to process and categorize changed elements to feed into this provider.

First lets create a ComparisonProvider that colors the changed elements by overriding the appearance of elements in the viewport. The provider's constructor finds which elements are inserted or updated based on their DbOpcode.

It will have 3 key methods:

  • setComparison: Applies the overrides to the class.
  • dropComparison: Removes existing overrides. it'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.
  • addFeatureOverrides: Colors inserted elements green and updated elements blue.

Then lets create a visualizeChange class that takes in the changed elements and categorizes them base on their operation codes, see DbOpcode.

Here is what the VisualizeChange.tsx looks like when you are done with this section.

VisualizeChange.tsx


typescript
class ComparisonProvider implements FeatureOverrideProvider {
  private _insertOp: Id64Array = [];
  private _updateOp: Id64Array = [];
  private static _provider: ComparisonProvider | undefined;

  /** Creates and applies a FeatureOverrideProvider to highlight the inserted and updated element Ids */
  public static setComparison(viewport: Viewport, insertOp: Id64Array, updateOp: Id64Array): ComparisonProvider {
    ComparisonProvider.dropComparison(viewport);
    ComparisonProvider._provider = new ComparisonProvider(insertOp, updateOp);
    viewport.addFeatureOverrideProvider(ComparisonProvider._provider);
    return ComparisonProvider._provider;
  }

  /** Removes the previous provider. */
  public static dropComparison(viewport: Viewport) {
    if (ComparisonProvider._provider !== undefined)
      viewport.dropFeatureOverrideProvider(ComparisonProvider._provider);
    ComparisonProvider._provider = undefined;
  }

  private constructor(insertOp: Id64Array, updateOp: Id64Array) {
    this._insertOp = insertOp;
    this._updateOp = updateOp;
  }

  /** Tells the viewport how to override the elements appearance. */
  public addFeatureOverrides(overrides: FeatureSymbology.Overrides, viewport: Viewport) {
    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,
    });

    overrides.setDefaultOverrides(defaultAppearance);

    const insertFeature = FeatureAppearance.fromRgb(ColorDef.green);
    const updateFeature = FeatureAppearance.fromRgb(ColorDef.blue);

    this._insertOp.forEach((id) => overrides.override({ elementId: id, appearance: insertFeature }));
    this._updateOp.forEach((id) => overrides.override({ elementId: id, appearance: updateFeature }));
  }
}

export class VisualizeChange {
  public static async visualizeComparison(changedElements: ChangedElements){
      const vp = IModelApp.viewManager.selectedView;
      if (vp === undefined)
          return;

      const elementIds = changedElements?.elements;
      const opcodes = changedElements?.opcodes;
      const deleteOp: Id64Array = [];
      const insertOp: Id64Array = [];
      const updateOp: Id64Array = [];
      let msgBrief = "";
      let msgDetail = "";
  
      if (
        // Tests if response has valid changes
        elementIds === undefined || elementIds.length <= 0 ||
        opcodes === undefined || opcodes.length <= 0 ||
        elementIds.length !== opcodes.length
      ) {
        msgBrief = "No elements changed";
        msgDetail = "There were 0 elements changed between change sets.";
      } else {
        msgBrief = elementIds.length + " elements changed";
        msgDetail = "There were " + elementIds.length + " elements changed between change sets.";
        for (let i = 0; i < elementIds.length; i += 1) {
          switch (opcodes[i]) {
            case DbOpcode.Delete:
              // Deleted elements will not be represented in this sample.
              deleteOp.push(elementIds[i]);
              break;
            case DbOpcode.Insert:
              insertOp.push(elementIds[i]);
              break;
            case DbOpcode.Update:
              updateOp.push(elementIds[i]);
              break;
          }
        }
      }
  
      toaster.informational(
        <>
          <Text>{msgBrief}</Text>
          <Text variant="small">{msgDetail}</Text>
        </>);
      
      ComparisonProvider.setComparison(vp, insertOp, updateOp);
      return { elementIds, opcodes };
  }
}

3.6.1 adding visualize change to the button's onClick

Remeber to call VisualizeChange.visualizeComparison after retrieving the href in handleVisualizeComparison back in ChangedElementsWidget.tsx and we are all set!

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:

handleVisualizeComparison method in ChangedElementsWidget.tsx


typescript
const changedElements = await ChangedElementClient.getChangedElementsFromHref(href);
if (changedElements) {
    VisualizeChange.visualizeComparison(changedElements);
}

3.7 Getting Status Update of the Comparison Job (optional)

It would be nice to see the progress of the comparison job. For this, let's add fetchProgress function in changeElementsClient.ts which essencially calls getComparisonJob method. If a valid result is being returned from getComparisonJob, divide currentProgress by maxProgress and convert that to a percentage amount. This percentage amount will indicate the progress of our comparison job.

Back to ChangedElementsWidget.tsx, create an interval that fetches progress every couple seconds. Have the selectedVersionIndex in useEffect dependencies so it triggers everytime a different namedVersion is selected.

We can have that setComparisonActive variable be called in handleCreateComparison and handleDeleteComparison, so that fetchProgress also get triggered when those button are clicked.

Reference this ChangeElementsWidget to see where setComparisonActive is added.

Remember to show this progress as Text in the returned component. Now we should be able to see the progress on job status!

Fetch Progress in changeElementsClient.ts


typescript
public static async fetchProgress(iModel: IModelConnection, startChangesetId: string | null): Promise<string> {
    const comparisonData = await ChangedElementClient.getComparisonJob(iModel, startChangesetId);
    if (comparisonData === null) {
        return "Job not found";
    }
    return comparisonData?.comparisonJob?.currentProgress && comparisonData?.comparisonJob?.maxProgress
        ? ((comparisonData.comparisonJob.currentProgress / comparisonData.comparisonJob.maxProgress) * 100).toFixed(2) + "%"
        : "0%";
  }

use FetchProgress with an interval in ChangedElementsWidget.tsx


typescript
const [comparisonActive, setComparisonActive] = useState<boolean>(false); 
  const [progress, setProgress] = useState<string>("0%");
    // fetch progress every 3 seconds 
  useEffect(() => {
      let interval: NodeJS.Timeout;
  
      const fetchProgress = async () => {
          if (!props.iModel || !namedVersions[selectedVersionIndex]) return;
          try {
              const progressPercentage = await ChangedElementClient.fetchProgress(
                  props.iModel,
                  namedVersions[selectedVersionIndex].changesetId
              );
              setProgress(progressPercentage);
          } catch (error: any) {
              toaster.negative(
                  <>
                  <Text>Failed to fetch comparison progress</Text>
                  <Text variant="small">{(error as Error)?.message ?? "Error fetching progress"}</Text>
                  </>
              );
          }
      };

      
      fetchProgress();
      interval = setInterval(fetchProgress, 3000); // Fetch every 3 seconds
  
      return () => clearInterval(interval);
  }, [props.iModel, namedVersions[selectedVersionIndex], selectedVersionIndex, comparisonActive]);

html component for text progress changeElementsClient.ts


html
<Text className="widget-progress-text">Comparison Progress: {progress}</Text> 
<Button onClick={handleCreateComparison}>Create Comparison</Button>

4. Making sense of Changed Elements data

The returned data is in an href that contains the changed elements info. For the json data in that link, each of those 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

Was this page helpful?