Skip to main content

Working with the Asset Elements API

When you tile a design file such as RVT, IFC, or DWG with the BIM/CAD Tiler with Database, or add a model from Bentley Infrastructure Cloud, Cesium ion keeps the full property set of every element. In CesiumJS, Cesium.IonAssetElementService reads those properties, queries elements by property value, and locates them in the 3D view.

You'll learn how to:

  • Read the properties of an element you click, with getElementId and getElement.
  • Query elements with JSON filters and page through the results.
  • Highlight query results with 3D Tiles styling and fly to any result.

The query Sandcastle with doors and windows highlighted green and the result list on the left.

The query Sandcastle with doors and windows highlighted green and the result list on the left.

Prerequisites

  • Familiarity with CesiumJS basics (Viewer, 3D Tiles, entities)
  • Completion of Working with BIM/CAD Tileset Metadata is recommended but not required
  • Familiarity with Cesium Sandcastle
  • This tutorial uses the Snowdon Towers sample models: the Structural RVT model in the inspect Sandcastle, and the Architectural IFC model in the query Sandcastle. The Sandcastles use the default Cesium ion access token, so you don't need a Cesium ion account to follow along.

What you'll build

Two Sandcastles, one for inspecting elements and one for queries, that together:

  • Highlight an element when you click it and show its full property set
  • Highlight elements as you hover over them
  • Run JSON filter queries and highlight the matching elements
  • List results as each page loads
  • Fly the camera to a result when you click it in the list

Introduction to the Asset Elements API

The Asset Elements API returns detailed properties for individual elements in your design model. Working with BIM/CAD Tileset Metadata describes the model as a whole. The Elements API looks up any single element and returns its full property set from the source file.

In CesiumJS, Cesium.IonAssetElementService calls the API for you. Create one service per asset:

const elementService = Cesium.IonAssetElementService.fromAssetId(ASSET_ID);

The service uses Cesium.Ion.defaultAccessToken and Cesium.Ion.defaultServer unless you pass the accessToken or server options. This tutorial uses these methods:

  • IonAssetElementService.getElementId(feature) reads the element id from a picked feature.
  • getElement(id) returns one element with its properties.
  • Query with listElements, one page at a time.
  • IonAssetElementService.getBoundingSphere(element) returns a bounding sphere to fly the camera to.

See the IonAssetElementService reference and the Asset Elements REST API documentation.

IonAssetElementService is experimental, so its API may change without the standard CesiumJS deprecation policy.

What is an "element"? An element represents a single object from your source design file, such as a wall, door, or column. While a tileset may split an element across multiple rendered features for efficient streaming, the element ID links them all back to the same source object and its complete property set.

Information

The Elements API makes engineering data easier to explore, query, and use in AI-powered workflows. The more consistently your data is structured, the more meaningful and reliable the results you can unlock. 

Not seeing the results you expected? We'd love to know more. Tell us more on the Cesium Community Forum.

How to follow this tutorial

Each step shows one concept and its code. The snippets use the same names as the Sandcastles, so a function from one step may call a function from another. Part 1 (Steps 1 to 3) follows the aec-elements-api-inspect Sandcastle. Part 2 (Steps 4 and 5) follows the aec-elements-api-query Sandcastle, and its HTML tab holds the markup for the result panel. Each part ends with a link to its complete Sandcastle.

Part 1: Inspect an element

1Load the design model

Load the tileset and create the element service. The service is bound to one asset, and Steps 2 and 3 call it. The query Sandcastle in Step 4 creates its own viewer, service, and tileset for its model.

const ASSET_ID = 6093105; // Snowdon Towers Sample Structural.rvt

// A service bound to the asset that talks to the ion Asset Elements API.
const elementService = Cesium.IonAssetElementService.fromAssetId(ASSET_ID);

Then create the viewer:

const viewer = new Cesium.Viewer("cesiumContainer", {
timeline: false,
animation: false,
baseLayerPicker: false,
});

The Sandcastle also sets the time of day for the lighting. Then load the tileset:

const tileset = viewer.scene.primitives.add(
await Cesium.Cesium3DTileset.fromIonAssetId(ASSET_ID),
);
await viewer.zoomTo(tileset);

To use your own asset, set Cesium.Ion.defaultAccessToken to a token with the assets:read scope and access to the appropriate assets in your Cesium ion account. Set it before you call fromAssetId, because the service reads the default token when you create it.

BIM/CAD Tiler with Database required: The Asset Elements API works with models tiled with the BIM/CAD Tiler with Database, or added from Bentley Infrastructure Cloud. If tileset.metadata is undefined, or getElement rejects with a 404 for a picked element, re-upload your model with the BIM/CAD Tiler with Database.

Information

The BIM/CAD Tiler with Database is currently in Technology Preview. APIs for working with this tiler and the data it produces might change prior to General Availability.

2Pick a feature and fetch its properties

Each rendered feature carries an element property that identifies its source element. IonAssetElementService.getElementId(feature) reads it, and returns undefined for anything without one, such as the globe. Pass the id to getElement to get the element's label, location, and full property set.

getElement returns an object like this one, trimmed to three properties:

{
id: "3298534883926",
label: 'Concrete 10" [3-GM]',
location: {
center: { longitude: -79.887, latitude: 40.0223, height: 213.98 },
radius: 11.8,
},
properties: {
Category: { value: "3298534883472", displayValue: "Walls", units: null },
Height: { value: 23.3934, displayValue: "23.3934 m", units: "Units:M" },
"Net Volume": {
value: 18.2696,
displayValue: "18.2696 m³",
units: "Units:CUB_M",
},
},
}

Each property has a raw value, a displayValue formatted for people with its units included, and units. For a reference such as Category, value is an id and displayValue is the name. location.center is in degrees and meters above the ellipsoid, and location.radius is in meters. Elements without a location have no location field.

Element ID typing

getElementId returns the element id as a decimal string, such as "3298534883926", and getElement takes that string. Elements, tree nodes, and tree path entries from the service also have decimal string ids, so ids from the tileset and from the service compare with === and match in a Set.

Click handler

Add a hidden entity to hold the InfoBox content, and a function that turns the properties into a table with each property's displayValue. The function sets each name and value with textContent, so the InfoBox shows them as text and never runs them as HTML.

// An entity used only to display the fetched properties in the InfoBox.
const infoEntity = viewer.entities.add(new Cesium.Entity({ show: false }));

function propertiesTable(properties) {
const rows = Object.entries(properties)
.map(([key, property]) => {
// displayValue already includes formatted units.
const text = property?.displayValue ?? property?.value ?? property;
const row = document.createElement("tr");
const nameCell = row.insertCell();
const name = document.createElement("b");
name.textContent = key;
nameCell.appendChild(name);
row.insertCell().textContent = String(text);
return row.outerHTML;
})
.join("");
return `<table class="cesium-infoBox-defaultTable"><tbody>${rows}</tbody></table>`;
}

The click handler reads the element id, highlights the element with the style from Step 3, and shows "Loading..." in the InfoBox while it fetches the element. Clicking anything without an element id, such as the globe, clears the selection. If the user clicks again before the response arrives, the activeSelection counter drops the older response. If the request fails, the InfoBox shows the error. Setting the action on viewer.screenSpaceEventHandler replaces the viewer's default left click behavior.

// On click, read the element id off the picked feature and fetch its metadata.
viewer.screenSpaceEventHandler.setInputAction(async function (movement) {
const selection = ++activeSelection;
const feature = viewer.scene.pick(movement.position);

// getElementId reads the element id a rendered feature belongs to.
const elementId = Cesium.IonAssetElementService.getElementId(feature);
if (!Cesium.defined(elementId)) {
selectedElementId = undefined;
tileset.makeStyleDirty();
infoEntity.show = false;
viewer.selectedEntity = undefined;
return;
}

selectedElementId = elementId;
tileset.makeStyleDirty();
infoEntity.name = `Element ${elementId}`;
infoEntity.description = "Loading...";
infoEntity.show = true;
viewer.selectedEntity = infoEntity;

try {
const element = await elementService.getElement(elementId);
if (selection !== activeSelection) {
return;
}
infoEntity.description = propertiesTable(element?.properties ?? {});
} catch (error) {
if (selection === activeSelection) {
infoEntity.description = `<p style="color:#ff6b6b;">${error.message}</p>`;
}
}
}, Cesium.ScreenSpaceEventType.LEFT_CLICK);

Feature properties vs API properties: The properties stored in tiles, which you read with feature.getProperty(), are a small subset that keeps tiles small for streaming. The API returns the full property set from the source model on demand.

3Highlight the selected element

The click handler in Step 2 stores the clicked element's id in selectedElementId. A style with an evaluateColor callback reads each feature's id with getElementId and colors the selected element green. After selectedElementId changes, the handler calls tileset.makeStyleDirty() so CesiumJS evaluates the style again.

let selectedElementId;
let activeSelection = 0;
tileset.style = new Cesium.Cesium3DTileStyle({
color: {
evaluateColor(feature, result) {
const id = Cesium.IonAssetElementService.getElementId(feature);
const color =
Cesium.defined(selectedElementId) && id === selectedElementId
? Cesium.Color.LIME
: Cesium.Color.WHITE;
return Cesium.Color.clone(color, result);
},
},
});

Open the complete inspect Sandcastle and click any element to see its properties.

The inspect Sandcastle with a selected wall highlighted green and its properties in the InfoBox.

The inspect Sandcastle with a selected wall highlighted green and its properties in the InfoBox.

Part 2: Query elements

4Query and filter elements

Dependency: This step calls appendResultItems() from Step 5.

Pass a JSON filter to listElements to get the elements whose properties match. Filter keys are the property names that getElement returns, such as IFC Class Name, and they are case sensitive. The special key @label matches the element's label.

The query Sandcastle uses the Snowdon Towers Sample Architectural IFC model, so its filters use IFC property names:

const ASSET_ID = 6093184; // Snowdon Towers Sample Architectural.ifc

BIM content in the API: The elements returned by the API correspond to BIM objects from your source file. For IFC models, this includes classes like ifcwall, ifccolumn, ifcspace, ifcdoor, ifcwindow, ifcslab, and many more. Each element carries its source properties (dimensions, materials, type marks, etc.). Not all source data may be preserved; coverage depends on the source format and the ingestion pipeline's support for that format's object types.

JSON filter examples

FilterMatches
{ "IFC Class Name": "ifcwall" }Elements whose IFC class is ifcwall
{ "IFC Class Name": "ifcwall", "Pset_WallCommon.IsExternal": true }Exterior walls. Keys in one object must all match
{ "$and": [{ "IFC Class Name": "ifcspace" }, { "FootprintArea": { "$gt": 41.8 } }] }Spaces with a footprint area above 41.8 (square meters in the sample model)
{ "IFC Class Name": { "$in": ["ifcdoor", "ifcwindow"] } }Doors or windows
{ "$or": [{ "@label": { "$like": "%Retail%" } }, { "@label": { "$like": "%Office%" } }] }Elements whose label contains "Retail" or "Office"

The filters are MongoDB style, but not every MongoDB operator is supported. Use $like for text patterns. $like ignores case, while keys and plain values are case sensitive.

Numeric filters compare raw numeric values, so $gt works on numbers. Reference properties such as Category hold an id in value, so filter on a text property such as IFC Class Name instead.

For aggregate statistics using the same JSON filters, see Aggregate BIM/CAD element properties.

Page through results

Define the queries the toolbar offers. Each one has a label and a JSON filter.

// JSON filters match element property names using operators such as $in and $like.
const QUERIES = [
{
label: "All Walls",
filter: { "IFC Class Name": "ifcwall" },
},
{
// Multiple keys in one object are combined with AND.
label: "Exterior Walls",
filter: { "IFC Class Name": "ifcwall", "Pset_WallCommon.IsExternal": true },
},
{
label: "Large Spaces > 450 sq ft",
filter: {
// FootprintArea is in m²; 450 sq ft ≈ 41.8 m².
$and: [
{ "IFC Class Name": "ifcspace" },
{ FootprintArea: { $gt: 41.8 } },
],
},
},
{
label: "Doors and Windows",
filter: { "IFC Class Name": { $in: ["ifcdoor", "ifcwindow"] } },
},
{
label: "Leasable Spaces (Retail, Office, or Commercial)",
filter: {
"IFC Class Name": "ifcspace",
$or: [
{ "@label": { $like: "%Retail%" } },
{ "@label": { $like: "%Office%" } },
{ "@label": { $like: "%Commercial%" } },
],
},
},
];

listElements returns one page with items, total, and a continuationToken when more results remain. total counts matches across all pages, or is null when the total is unavailable. top sets the page size and defaults to 100. The Sandcastle requests 1000 per page. To get the next page, pass the token back with the same filter. queryElements loops until there is no continuationToken, and passes each page to an onPage callback so results appear while the query loads:

async function queryElements(filter, options = {}) {
const { signal, onPage } = options;

let totalFetched = 0;
let total;
let continuationToken;

do {
const page = await elementService.listElements({
filter,
top: 1000,
continuationToken,
});
// listElements is not cancelable, so drop pages for superseded queries.
if (signal?.aborted) {
return { aborted: true };
}

totalFetched += page.items.length;
total = page.total;
if (onPage) {
onPage(page.items, totalFetched, total);
}

continuationToken = page.continuationToken;
} while (Cesium.defined(continuationToken));

return { total: total ?? totalFetched };
}

On the sample model, the presets match 1078, 590, 128, 200, and 18 elements, so All Walls takes two pages.

Running a query

Get the result panel elements from the Sandcastle's HTML tab:

const resultPanel = document.getElementById("resultPanel");
const resultHeader = document.getElementById("resultHeader");
const resultsList = document.getElementById("resultsList");

Keep the selected element, the hovered element, and the ids of the query matches. The query Sandcastle is separate from the inspect Sandcastle, so it declares its own selection state and style. Ids from getElementId and from the service are both decimal strings, so the comparisons and Set lookups match.

// Element ids are decimal strings, as returned by IonAssetElementService.
let selectedElementId = null;
let hoveredElementId = null;
const matchedElementIds = new Set(); // query results
let activeQueryController = null;

The style colors the selected element yellow, the hovered element cyan, and query matches green. While a selection or query is active, everything else fades. A MOUSE_MOVE handler in the Sandcastle sets hoveredElementId.

function applyStyle() {
const style = new Cesium.Cesium3DTileStyle();
style.color = {
evaluateColor: function (feature, result) {
const id = Cesium.IonAssetElementService.getElementId(feature);

// Selected: yellow
if (id === selectedElementId) {
return Cesium.Color.clone(Cesium.Color.YELLOW, result);
}
// Hovered: cyan
if (id === hoveredElementId) {
return Cesium.Color.clone(Cesium.Color.CYAN.withAlpha(0.7), result);
}
// Query match: green
if (matchedElementIds.size > 0 && matchedElementIds.has(id)) {
return Cesium.Color.clone(Cesium.Color.LIME, result);
}
// Faded when selection or query is active
if (selectedElementId || matchedElementIds.size > 0) {
return Cesium.Color.clone(Cesium.Color.WHITE.withAlpha(0.02), result);
}
// Default
return Cesium.Color.clone(Cesium.Color.WHITE, result);
},
};
tileset.style = style;
}

After the state changes, call tileset.makeStyleDirty() so CesiumJS evaluates the style again. In the Sandcastle, initialize loads the tileset, zooms to it, and then calls applyStyle.

Why evaluateColor instead of style conditions? Declarative style conditions work for a few elements. A query can match thousands, and checking a long list of conditions for every feature gets slow. A Set lookup in evaluateColor stays fast. tileset.makeStyleDirty() re-evaluates the existing style without parsing a new one.

The query Sandcastle loads world terrain and turns on globe translucency, so runQuery can fade the globe while results show:

viewer.scene.setTerrain(Cesium.Terrain.fromWorldTerrain());
viewer.scene.globe.depthTestAgainstTerrain = true;
viewer.scene.globe.translucency.enabled = true;

runQuery stops any running query with an AbortController, clears the previous results, and calls queryElements. Each page adds its ids to the highlight set and its items to the result list, and the header shows the count so far. If a request fails, the header shows the error.

async function runQuery(query) {
// Cancel any in-flight query
if (activeQueryController) {
activeQueryController.abort();
}
activeQueryController = new AbortController();
const { signal } = activeQueryController;

// Clear previous state
selectedElementId = null;
matchedElementIds.clear();
if (tileset) {
tileset.makeStyleDirty();
}
viewer.selectedEntity = undefined;
resultsList.innerHTML = "";

resultHeader.textContent = `${query.label} — loading...`;
resultPanel.style.display = "block";
viewer.scene.globe.translucency.frontFaceAlpha = 0.25;

try {
const result = await queryElements(query.filter, {
signal,
onPage(pageItems, totalSoFar, totalElements) {
for (const el of pageItems) {
matchedElementIds.add(el.id);
}
tileset.makeStyleDirty();
appendResultItems(pageItems);

const totalLabel =
totalElements !== undefined ? ` of ${totalElements}` : "";
resultHeader.textContent = `${query.label} — ${totalSoFar}${totalLabel} results`;
},
});
if (result.aborted) {
return;
}

resultHeader.textContent = `${query.label} — ${result.total} result${result.total !== 1 ? "s" : ""}`;
} catch (err) {
if (signal.aborted) {
return;
}
resultHeader.textContent = `Error: ${err.message}`;
}
}

Clearing results

clearResults stops any running query, clears the selection and highlights, hides the result panel, and makes the globe opaque again.

function clearResults() {
if (activeQueryController) {
activeQueryController.abort();
}
selectedElementId = null;
matchedElementIds.clear();
resultsList.innerHTML = "";
resultPanel.style.display = "none";
viewer.scene.globe.translucency.frontFaceAlpha = 1.0;
viewer.selectedEntity = undefined;
if (tileset) {
tileset.makeStyleDirty();
}
}

The toolbar menu runs a query, or clears the results with its first entry. The Clear button clears the results and resets the menu.

Sandcastle.addToolbarMenu(
[
{ text: "Select a query...", onselect: clearResults },
...QUERIES.map((query) => ({
text: query.label,
onselect: () => runQuery(query),
})),
],
"toolbar",
);

// Get a reference to the dropdown so the Clear button can reset it
const queryDropdown = document.querySelector("#toolbar select");

Sandcastle.addToolbarButton("Clear", function () {
clearResults();
if (queryDropdown) {
queryDropdown.selectedIndex = 0;
}
});

5Display results and navigate to elements

Show each result in a list. Clicking an item highlights it yellow, flies the camera to it, and shows its properties in the InfoBox.

Building the result list

Each list item has an id and may include a label and location. The list shows the label, or the id when there is no label, with the element id underneath.

function appendResultItems(items) {
for (const el of items) {
const li = document.createElement("li");

const label = document.createElement("div");
label.className = "result-label";
label.textContent = el.label ?? el.id;
li.appendChild(label);

const detail = document.createElement("div");
detail.className = "result-detail";
detail.textContent = `Element ${el.id}`;
li.appendChild(detail);

li.addEventListener("click", () => selectElement(el));
resultsList.appendChild(li);
}
}

List items may omit properties. To show properties in a table, you need one getElement call per row, so keep such tables to a few rows.

Selecting and flying to an element

selectElement highlights the element, flies to it, and shows its properties in the InfoBox. It fetches the full element with getElement, and drops the response if the user has selected another element in the meantime. In the query Sandcastle, clicking an element in the 3D view also calls selectElement, with flyTo: false.

The query Sandcastle has its own InfoBox entity and table function, named differently from Step 2:

function buildDescription(properties) {
const rows = Object.entries(properties)
.map(([key, property]) => {
// displayValue already includes formatted units.
const text = property.displayValue ?? property.value;
return `<tr><td><b>${key}</b></td><td>${text}</td></tr>`;
})
.join("");
return `<table class="cesium-infoBox-defaultTable"><tbody>${rows}</tbody></table>`;
}
const elementPropertiesEntity = new Cesium.Entity({
name: "Element",
show: false,
});
viewer.entities.add(elementPropertiesEntity);
async function selectElement(element, options = {}) {
const { flyTo = true } = options;
const elementId = element.id;
selectedElementId = elementId;
tileset.makeStyleDirty();
viewer.scene.globe.translucency.frontFaceAlpha = 0.25;

if (flyTo) {
flyToElement(element);
}

// Fetch element properties and show in InfoBox
elementPropertiesEntity.name = element.label ?? `Element ${elementId}`;
elementPropertiesEntity.description = "Loading...";
elementPropertiesEntity.show = true;
viewer.selectedEntity = elementPropertiesEntity;

try {
const details = await elementService.getElement(elementId);
if (elementId !== selectedElementId) {
return;
}
elementPropertiesEntity.name = details.label ?? `Element ${elementId}`;
elementPropertiesEntity.description = buildDescription(
details.properties ?? {},
);
} catch (err) {
elementPropertiesEntity.description = `<p style="color:#ff6b6b;">Error: ${err.message}</p>`;
}

viewer.selectedEntity = undefined;
viewer.selectedEntity = elementPropertiesEntity;
}

flyToElement builds a bounding sphere with IonAssetElementService.getBoundingSphere(element). It returns undefined when the element has no valid location, and the Sandcastle then zooms to the whole tileset. Small elements get a minimum radius so the camera does not end up inside them.

function flyToElement(element) {
const boundingSphere =
Cesium.IonAssetElementService.getBoundingSphere(element);
if (!Cesium.defined(boundingSphere)) {
viewer.zoomTo(tileset);
return;
}
boundingSphere.radius = Math.max(boundingSphere.radius, 5) * 1.5;
viewer.camera.flyToBoundingSphere(boundingSphere, {
offset: new Cesium.HeadingPitchRange(
0,
Cesium.Math.toRadians(-30),
0, // auto-compute range to fit sphere
),
duration: 2,
});
}

Open the complete query Sandcastle to pick a query from the toolbar menu, see the matches highlighted, and click a result to fly to it.

What's next

  • Bring your own data: You need a Cesium ion account, an access token with the assets:read scope, and a model tiled with the BIM/CAD Tiler with Database, or added from Bentley Infrastructure Cloud. If you upload a file, georeference it with an EPSG code and vertical datum so elements get a location. Then set Cesium.Ion.defaultAccessToken to your token and replace ASSET_ID with your asset ID.
  • Aggregate BIM/CAD element properties covers aggregate statistics using the same JSON filters, such as totals for a dashboard.
  • Navigate a BIM/CAD model tree shows how to browse an asset's elements as a tree and find where a picked element sits in it.
  • Cross-model queries: Create one service per asset to run the same filter against several models from one project, for example structural columns and architectural spaces.

Complete code

Open the query Sandcastle in a new tab.

Open the inspect Sandcastle to click any element and see its properties. Open the query Sandcastle to pick a query from the toolbar menu, see the matches highlighted, and click a result to fly to it.

Troubleshooting

SymptomCauseFix
getElement rejects with a 404The asset has no element with that ID, or the model was not tiled with the BIM/CAD Tiler with DatabaseCheck the asset ID, or re-upload with the BIM/CAD Tiler with Database
Requests fail with 401Invalid or expired access tokenCreate a new token with the assets:read scope
tileset.metadata is undefinedLegacy pipeline uploadRe-upload with the BIM/CAD Tiler with Database
getBoundingSphere returns undefinedThe element's location data is missing or invalidSkip the fly-to for that element, and check that the source model is georeferenced
Element appears far from the modelThe source model has an incorrect coordinate reference systemCheck the EPSG code and vertical datum, then re-upload
getElementId returns undefined on a model featureThe model has no element ID mappingRe-upload with the BIM/CAD Tiler with Database
A query returns no resultsFilter keys must match getElement property names exactly. Filter keys and string values are case sensitive. An unknown key returns no results, not an errorCall getElement on one element and copy its property names
Information

The Elements API makes engineering data easier to explore, query, and use in AI-powered workflows. The more consistently your data is structured, the more meaningful and reliable the results you can unlock. 

Not seeing the results you expected? We'd love to know more. Tell us more on the Cesium Community Forum.

Summary

TaskHow
Read a picked feature's element IDIonAssetElementService.getElementId(feature)
Fetch one element's propertieselementService.getElement(id)
Highlight elements by IDevaluateColor with a Set of element IDs
Query by propertylistElements({ filter }) with a JSON filter
Page through resultsPass continuationToken back to listElements
Fly to an elementIonAssetElementService.getBoundingSphere(element) with camera.flyToBoundingSphere

Content and code examples at cesium.com/learn are available under the Apache 2.0 license. You can use the code examples in your commercial or non-commercial applications.