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

iTwin Reality Data integration #12334

Merged
merged 13 commits into from
Nov 26, 2024
22 changes: 16 additions & 6 deletions Apps/Sandcastle/gallery/iTwin Demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,18 +120,28 @@
scene.primitives.add(surroundingArea);
scene.primitives.add(station);

// Create tileset of the reality data mesh
const iTwinId = "535a24a3-9b29-4e23-bb5d-9cedb524c743";
const realityMeshId = "85897090-3bcc-470b-bec7-20bb639cc1b9";
const realityMesh = await Cesium.ITwinData.createTilesetForRealityDataId(
iTwinId,
realityMeshId,
);
scene.primitives.add(realityMesh);

Sandcastle.addToolbarButton(
"Toggle Surrounding Area",
function () {
surroundingArea.show = !surroundingArea.show;
},
() => (surroundingArea.show = !surroundingArea.show),
"layers",
);
Sandcastle.addToolbarButton(
"Toggle Station Model",
function () {
station.show = !station.show;
},
() => (station.show = !station.show),
"layers",
);
Sandcastle.addToolbarButton(
"Toggle Reality Mesh",
() => (realityMesh.show = !realityMesh.show),
"layers",
);

Expand Down
Binary file modified Apps/Sandcastle/gallery/iTwin Demo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
201 changes: 201 additions & 0 deletions packages/engine/Source/Core/ITwinPlatform.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,57 @@ ITwinPlatform.ExportType = Object.freeze({
"3DTILES": "3DTILES",
});

/**
* Types of Reality data
* @see https://developer.bentley.com/apis/reality-management/rm-rd-details/#types
* @enum {string}
*/
ITwinPlatform.RealityDataType = Object.freeze({
Cesium3DTiles: "Cesium3DTiles",
PNTS: "PNTS",
OPC: "OPC",
RealityMesh3DTiles: "RealityMesh3DTiles",
Terrain3DTiles: "Terrain3DTiles",
"3MX": "3MX",
"3SM": "3SM",
CCCloudProject: "CCCloudProject",
CCImageCollection: "CCImageCollection",
CCOrientations: "CCOrientations",
ContextCaptureInputs: "ContextCaptureInputs",
ContextDetector: "ContextDetector",
ContextScene: "ContextScene",
DAE: "DAE",
DGN: "DGN",
DSM: "DSM",
FBX: "FBX",
GLB: "GLB",
GLTF: "GLTF",
KML: "KML",
LAS: "LAS",
LAZ: "LAZ",
LOD: "LOD",
LodTree: "LodTree",
OBJ: "OBJ",
OMI: "OMI",
OMR: "OMR",
Orthophoto: "Orthophoto",
OrthophotoDSM: "OrthophotoDSM",
OSGB: "OSGB",
OVF: "OVF",
OBT: "OBT",
PLY: "PLY",
PointCloud: "PointCloud",
S3C: "S3C",
ScanCollection: "ScanCollection",
SHP: "SHP",
SLPK: "SLPK",
SpaceEyes3D: "SpaceEyes3D",
STL: "STL",
TSM: "TSM",
Unstructured: "Unstructured",
Other: "Other",
});

/**
* Gets or sets the default iTwin access token. This token should have the <code>itwin-platform</code> scope.
*
Expand Down Expand Up @@ -155,4 +206,154 @@ ITwinPlatform.getExports = async function (iModelId) {
}
};

/**
* @typedef {Object} RealityDataExtent
* @private
* @property {{latitude: number, longitude: number}} southWest
* @property {{latitude: number, longitude: number}} northEast
*/

/**
* @typedef {Object} RealityDataRepresentation
* @private
* @property {string} id "95d8dccd-d89e-4287-bb5f-3219acbc71ae",
* @property {string} displayName "Name of reality data",
* @property {string} dataset "Dataset",
* @property {string} group "73d09423-28c3-4fdb-ab4a-03a47a5b04f8",
* @property {string} description "Description of reality data",
* @property {string} rootDocument "Directory/SubDirectory/realityData.3mx",
* @property {number} size 6521212,
* @property {string} classification "Model",
* @property {ITwinPlatform.RealityDataType} type "3MX",
* @property {{startDateTime: string, endDateTime: string, acquirer: string}} acquisition
* @property {RealityDataExtent} extent
* @property {boolean} authoring false,
* @property {string} dataCenterLocation "North Europe",
* @property {string} modifiedDateTime "2021-04-09T19:03:12Z",
* @property {string} lastAccessedDateTime "2021-04-09T00:00:00Z",
* @property {string} createdDateTime "2021-02-22T20:03:40Z",
* @property {string} ownerId "f1d49cc7-f9b3-494f-9c67-563ea5597063",
*/

/**
* Load the full metadata for the given iTwin id and reality data id.
*
* @private
*
* @param {string} iTwinId The id of the iTwin to load data from
* @param {string} realityDataId The id of the reality data to load
* @returns {Promise<RealityDataRepresentation>}
*/
ITwinPlatform.getRealityDataMetadata = async function (iTwinId, realityDataId) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("iTwinId", iTwinId);
Check.typeOf.string("realityDataId", realityDataId);
if (!defined(ITwinPlatform.defaultAccessToken)) {
throw new DeveloperError("Must set ITwinPlatform.defaultAccessToken first");
}
//>>includeEnd('debug')

const resource = new Resource({
url: `${ITwinPlatform.apiEndpoint}reality-management/reality-data/${realityDataId}`,
headers: {
Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`,
Accept: "application/vnd.bentley.itwin-platform.v1+json",
},
queryParameters: { iTwinId: iTwinId },
});

try {
const response = await resource.fetchJson();
return response.realityData;
} catch (error) {
const result = JSON.parse(error.response);
if (error.statusCode === 401) {
throw new RuntimeError(
`Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`,
);
} else if (error.statusCode === 403) {
console.error(result.error.code, result.error.message);
throw new RuntimeError("Not allowed, forbidden");
} else if (error.statusCode === 404) {
throw new RuntimeError(
`Reality data not found: ${iTwinId}, ${realityDataId}`,
);
} else if (error.statusCode === 422) {
throw new RuntimeError(
`Unprocessable Entity:${result.error.code} ${result.error.message}`,
);
} else if (error.statusCode === 429) {
throw new RuntimeError("Too many requests");
}
throw new RuntimeError(`Unknown request failure ${error.statusCode}`);
}
};

/**
* Request the access url for the given iTwin id, reality data id and root document.
* The root document can be requested from the list using <code>return=representation</code>
* or the metadata route from {@link ITwinPlatform.getRealityDataMetadata}
*
* @private
*
* @param {string} iTwinId The id of the iTwin to load data from
* @param {string} realityDataId The id of the reality data to load
* @param {string} rootDocument The path of the root document for this reality data
* @returns {Promise<string>}
*/
ITwinPlatform.getRealityDataURL = async function (
iTwinId,
realityDataId,
rootDocument,
) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("iTwinId", iTwinId);
Check.typeOf.string("realityDataId", realityDataId);
Check.typeOf.string("rootDocument", rootDocument);
if (!defined(ITwinPlatform.defaultAccessToken)) {
throw new DeveloperError("Must set ITwinPlatform.defaultAccessToken first");
}
//>>includeEnd('debug')

const resource = new Resource({
url: `${ITwinPlatform.apiEndpoint}reality-management/reality-data/${realityDataId}/readaccess`,
headers: {
Authorization: `Bearer ${ITwinPlatform.defaultAccessToken}`,
Accept: "application/vnd.bentley.itwin-platform.v1+json",
},
queryParameters: { iTwinId: iTwinId },
});

try {
const result = await resource.fetchJson();

const containerUrl = result._links.containerUrl.href;
const tilesetUrl = new URL(containerUrl);
tilesetUrl.pathname = `${tilesetUrl.pathname}/${rootDocument}`;

return tilesetUrl.toString();
} catch (error) {
const result = JSON.parse(error.response);
if (error.statusCode === 401) {
throw new RuntimeError(
`Unauthorized, bad token, wrong scopes or headers bad. ${result.error.details[0].code}`,
);
} else if (error.statusCode === 403) {
console.error(result.error.code, result.error.message);
throw new RuntimeError("Not allowed, forbidden");
} else if (error.statusCode === 404) {
throw new RuntimeError(
`Reality data not found: ${iTwinId}, ${realityDataId}`,
);
} else if (error.statusCode === 422) {
throw new RuntimeError(
`Unprocessable Entity:${result.error.code} ${result.error.message}`,
);
} else if (error.statusCode === 429) {
throw new RuntimeError("Too many requests");
}
throw new RuntimeError(`Unknown request failure ${error.statusCode}`);
}
};

export default ITwinPlatform;
64 changes: 64 additions & 0 deletions packages/engine/Source/Scene/ITwinData.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import defined from "../Core/defined.js";
import Resource from "../Core/Resource.js";
import ITwinPlatform from "../Core/ITwinPlatform.js";
import RuntimeError from "../Core/RuntimeError.js";
import Check from "../Core/Check.js";

/**
* Methods for loading iTwin platform data into CesiumJS
Expand Down Expand Up @@ -71,4 +72,67 @@ ITwinData.createTilesetFromIModelId = async function (iModelId, options) {
return Cesium3DTileset.fromUrl(resource, options);
};

/**
* Create a tileset for the specified reality data id. This function only works
* with 3D Tiles meshes and point clouds.
*
* If the <code>type</code> or <code>rootDocument</code> are not provided this function
* will first request the full metadata for the specified reality data to fill these values.
*
jjspace marked this conversation as resolved.
Show resolved Hide resolved
* @experimental This feature is not final and is subject to change without Cesium's standard deprecation policy.
*
* @param {string} iTwinId The id of the iTwin to load data from
* @param {string} realityDataId The id of the reality data to load
* @param {ITwinPlatform.RealityDataType} [type] The type of this reality data
* @param {string} [rootDocument] The path of the root document for this reality data
* @returns {Promise<Cesium3DTileset>}
*/
ITwinData.createTilesetForRealityDataId = async function (
iTwinId,
realityDataId,
type,
rootDocument,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe type and rootDocument should be extracted from the reality data metadata to ensure that they are consistent with what actually exist in the storage container... or maybe this is our way to avoid a call to get metadata since they were already extracted and assumed consistent?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or maybe this is our way to avoid a call to get metadata since they were already extracted and assumed consistent?

@matmarchand You are correct. If either of these values is missing we will fetch the metadata needed to load the tileset. This will likely only the rootDocument commonly but we grab and update both from the new request anyway.

However if the application using this function already has these values we can skip that network call. This could happen if the app loads the list of reality data using return=representation instead of return=minimal. Or if the app already called the individual metadata route themselves. No need to waste extra time and bandwidth if the info is already accessible

) {
//>>includeStart('debug', pragmas.debug);
Check.typeOf.string("iTwinId", iTwinId);
Check.typeOf.string("realityDataId", realityDataId);
if (defined(type)) {
Check.typeOf.string("type", type);
}
if (defined(rootDocument)) {
Check.typeOf.string("rootDocument", rootDocument);
}
//>>includeEnd('debug')

if (!defined(type) || !defined(rootDocument)) {
jjspace marked this conversation as resolved.
Show resolved Hide resolved
const metadata = await ITwinPlatform.getRealityDataMetadata(
iTwinId,
realityDataId,
);
rootDocument = metadata.rootDocument;
type = metadata.type;
}

const supportedRealityDataTypes = [
ITwinPlatform.RealityDataType.Cesium3DTiles,
ITwinPlatform.RealityDataType.PNTS,
ITwinPlatform.RealityDataType.RealityMesh3DTiles,
ITwinPlatform.RealityDataType.Terrain3DTiles,
];

if (!supportedRealityDataTypes.includes(type)) {
throw new RuntimeError(`Reality data type is not a mesh type: ${type}`);
}

const tilesetAccessUrl = await ITwinPlatform.getRealityDataURL(
iTwinId,
realityDataId,
rootDocument,
);

return Cesium3DTileset.fromUrl(tilesetAccessUrl, {
maximumScreenSpaceError: 4,
});
};

export default ITwinData;
Loading