Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add support for iTwin Geospatial Features API #12449

Merged
merged 6 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions Apps/Sandcastle/gallery/iTwin Feature Service.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,17 @@
viewer.scene.camera.flyTo(birdsEyeView);

// Load feature service geojson files
const points = await Cesium.ITwinData.createDataSourceForRealityDataId(
const points = await Cesium.ITwinData.loadGeospatialFeatures(
iTwinId,
"57b975f6-fd92-42ba-8014-79911ed606d1",
"2380dc1b-1dac-4709-aa5c-f6cb38c4e9f5",
);
const lines = await Cesium.ITwinData.createDataSourceForRealityDataId(
const lines = await Cesium.ITwinData.loadGeospatialFeatures(
iTwinId,
"1099c53f-c568-48a3-a57c-0230a6f37229",
"613d2310-4d01-43b7-bc92-873a2ca4a4a0",
);
const areas = await Cesium.ITwinData.createDataSourceForRealityDataId(
const areas = await Cesium.ITwinData.loadGeospatialFeatures(
iTwinId,
"21eaf0d0-ab90-400f-97cf-adc455b29a78",
"93e7ef51-5210-49f2-92a3-c7f6685e102f",
);

// Add some styling to the lines and points to differentiate types
Expand All @@ -86,7 +86,7 @@
Arrow_Marking: { color: Cesium.Color.YELLOW, icon: "car" },
Road_Sign: { color: Cesium.Color.ORANGE, icon: "triangle" },
};
const type = entity.properties.Type?.getValue();
const type = entity.properties.type?.getValue();
if (Cesium.defined(type) && Cesium.defined(styleByType[type])) {
const { color, icon } = styleByType[type];
const canvas = await pinBuilder.fromMakiIconId(icon, color, 48);
Expand All @@ -103,7 +103,7 @@
Turning_pocket: Cesium.Color.DEEPPINK,
Yellow_Box: Cesium.Color.GOLD,
};
const type = entity.properties.Type?.getValue();
const type = entity.properties.type?.getValue();
if (Cesium.defined(type) && Cesium.defined(lineColorsByType[type])) {
entity.polyline.material = lineColorsByType[type];
}
Expand Down
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

- Changed behavior of `DataSourceDisplay.ready` to always stay `true` once it is initially set to `true`. [#12429](https://github.com/CesiumGS/cesium/pull/12429)

#### Additions :tada:

- Add `ITwinData.loadGeospatialFeatures(iTwinId, collectionId)` function to load data from the [Geospatial Features API](https://developer.bentley.com/apis/geospatial-features/operations/get-features/) [#12449](https://github.com/CesiumGS/cesium/pull/12449)

#### Fixes :wrench:

- Fixed error when resetting `Cesium3DTileset.modelMatrix` to its initial value. [#12409](https://github.com/CesiumGS/cesium/pull/12409)
Expand Down
48 changes: 47 additions & 1 deletion packages/engine/Source/Scene/ITwinData.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RuntimeError from "../Core/RuntimeError.js";
import Check from "../Core/Check.js";
import KmlDataSource from "../DataSources/KmlDataSource.js";
import GeoJsonDataSource from "../DataSources/GeoJsonDataSource.js";
import DeveloperError from "../Core/DeveloperError.js";

/**
* Methods for loading iTwin platform data into CesiumJS
Expand Down Expand Up @@ -154,7 +155,7 @@ ITwinData.createTilesetForRealityDataId = async function (
*
* @throws {RuntimeError} if the type of reality data is not supported by this function
*/
ITwinData.createDataSourceForRealityDataId = async function loadRealityData(
ITwinData.createDataSourceForRealityDataId = async function (
iTwinId,
realityDataId,
type,
Expand Down Expand Up @@ -205,4 +206,49 @@ ITwinData.createDataSourceForRealityDataId = async function loadRealityData(
return KmlDataSource.load(tilesetAccessUrl);
};

/**
* Load data from the Geospatial Features API as GeoJSON.
*
* @param {string} iTwinId The id of the iTwin to load data from
* @param {string} collectionId The id of the data collection to load
* @param {number} [limit=10000] number of items per page, must be between 1 and 10,000 inclusive
* @returns {Promise<GeoJsonDataSource>}
*/
ITwinData.loadGeospatialFeatures = async function (
iTwinId,
collectionId,
limit,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("iTwinId", iTwinId);
Check.typeOf.string("collectionId", collectionId);
if (defined(limit)) {
Check.typeOf.number("limit", limit);
Check.typeOf.number.lessThanOrEquals("limit", limit, 10000);
Check.typeOf.number.greaterThanOrEquals("limit", limit, 1);
}
if (!defined(ITwinPlatform.defaultAccessToken)) {
throw new DeveloperError("Must set ITwinPlatform.defaultAccessToken first");
}
//>>includeEnd('debug')

const pageLimit = limit ?? 10000;

const tilesetUrl = `${ITwinPlatform.apiEndpoint}geospatial-features/itwins/${iTwinId}/ogc/collections/${collectionId}/items`;

const resource = new Resource({
url: tilesetUrl,
headers: {
Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`,
Accept: "application/vnd.bentley.itwin-platform.v1+json",
},
queryParameters: {
limit: pageLimit,
client: "CesiumJS",
},
});

return GeoJsonDataSource.load(resource);
};

export default ITwinData;
63 changes: 63 additions & 0 deletions packages/engine/Specs/Scene/ITwinDataSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,4 +346,67 @@ describe("ITwinData", () => {
expect(geojsonSpy).not.toHaveBeenCalled();
});
});

describe("loadGeospatialFeatures", () => {
let geojsonSpy;
beforeEach(() => {
geojsonSpy = spyOn(GeoJsonDataSource, "load");
});

it("rejects with no iTwinId", async () => {
await expectAsync(
// @ts-expect-error
ITwinData.loadGeospatialFeatures(undefined),
).toBeRejectedWithDeveloperError(
"Expected iTwinId to be typeof string, actual typeof was undefined",
);
});

it("rejects with no collectionId", async () => {
await expectAsync(
// @ts-expect-error
ITwinData.loadGeospatialFeatures("itwin-id-1", undefined),
).toBeRejectedWithDeveloperError(
"Expected collectionId to be typeof string, actual typeof was undefined",
);
});

it("rejects with limit < 1", async () => {
await expectAsync(
ITwinData.loadGeospatialFeatures("itwin-id-1", "collection-id-1", 0),
).toBeRejectedWithDeveloperError(
"Expected limit to be greater than or equal to 1, actual value was 0",
);
});

it("rejects with limit > 10000", async () => {
await expectAsync(
ITwinData.loadGeospatialFeatures(
"itwin-id-1",
"collection-id-1",
20000,
),
).toBeRejectedWithDeveloperError(
"Expected limit to be less than or equal to 10000, actual value was 20000",
);
});

it("rejects with no default access token set", async () => {
ITwinPlatform.defaultAccessToken = undefined;
await expectAsync(
ITwinData.loadGeospatialFeatures("itwin-id-1", "collection-id-1"),
).toBeRejectedWithDeveloperError(
"Must set ITwinPlatform.defaultAccessToken first",
);
});

it("creates a GeoJsonDataSource from the constructed blob url if the type is GeoJSON", async () => {
await ITwinData.loadGeospatialFeatures("itwin-id-1", "collection-id-1");

expect(geojsonSpy).toHaveBeenCalledTimes(1);
expect(geojsonSpy.calls.mostRecent().args[0].url).toEqual(
"https://api.bentley.com/geospatial-features/itwins/itwin-id-1/ogc/collections/collection-id-1/items?limit=10000&client=CesiumJS",
);
});
});
});
Loading