diff --git a/Apps/Sandcastle/gallery/iTwin Demo.html b/Apps/Sandcastle/gallery/iTwin Demo.html index 19e0156a005..e78ab95f4c1 100644 --- a/Apps/Sandcastle/gallery/iTwin Demo.html +++ b/Apps/Sandcastle/gallery/iTwin Demo.html @@ -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", ); diff --git a/Apps/Sandcastle/gallery/iTwin Demo.jpg b/Apps/Sandcastle/gallery/iTwin Demo.jpg index 98316311825..ec74f8279e2 100644 Binary files a/Apps/Sandcastle/gallery/iTwin Demo.jpg and b/Apps/Sandcastle/gallery/iTwin Demo.jpg differ diff --git a/packages/engine/Source/Core/ITwinPlatform.js b/packages/engine/Source/Core/ITwinPlatform.js index ca6f8e63063..c6e9e44ba4a 100644 --- a/packages/engine/Source/Core/ITwinPlatform.js +++ b/packages/engine/Source/Core/ITwinPlatform.js @@ -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 itwin-platform scope. * @@ -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} + */ +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 return=representation + * 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} + */ +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; diff --git a/packages/engine/Source/Scene/ITwinData.js b/packages/engine/Source/Scene/ITwinData.js index 1e970ef1a3e..1b53f27e4c9 100644 --- a/packages/engine/Source/Scene/ITwinData.js +++ b/packages/engine/Source/Scene/ITwinData.js @@ -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 @@ -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 type or rootDocument are not provided this function + * will first request the full metadata for the specified reality data to fill these values. + * + * @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} + */ +ITwinData.createTilesetForRealityDataId = async function ( + iTwinId, + realityDataId, + type, + rootDocument, +) { + //>>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)) { + 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; diff --git a/packages/engine/Specs/Core/ITwinPlatformSpec.js b/packages/engine/Specs/Core/ITwinPlatformSpec.js index eefb376a6b2..b43089c3f88 100644 --- a/packages/engine/Specs/Core/ITwinPlatformSpec.js +++ b/packages/engine/Specs/Core/ITwinPlatformSpec.js @@ -1,8 +1,9 @@ -import DeveloperError from "../../Source/Core/DeveloperError.js"; -import ITwinPlatform from "../../Source/Core/ITwinPlatform.js"; -import RequestErrorEvent from "../../Source/Core/RequestErrorEvent.js"; -import Resource from "../../Source/Core/Resource.js"; -import RuntimeError from "../../Source/Core/RuntimeError.js"; +import { + ITwinPlatform, + RequestErrorEvent, + Resource, + RuntimeError, +} from "../../index.js"; describe("ITwinPlatform", () => { let previousAccessToken; @@ -22,10 +23,10 @@ describe("ITwinPlatform", () => { }); it("rejects with no iModelId", async () => { - // @ts-expect-error - await expectAsync(ITwinPlatform.getExports()).toBeRejectedWithError( - DeveloperError, - /iModelId/, + await expectAsync( + ITwinPlatform.getExports(undefined), + ).toBeRejectedWithDeveloperError( + "Expected iModelId to be typeof string, actual typeof was undefined", ); }); @@ -33,9 +34,8 @@ describe("ITwinPlatform", () => { ITwinPlatform.defaultAccessToken = undefined; await expectAsync( ITwinPlatform.getExports("imodel-id-1"), - ).toBeRejectedWithError( - DeveloperError, - /Must set ITwinPlatform.defaultAccessToken/, + ).toBeRejectedWithDeveloperError( + "Must set ITwinPlatform.defaultAccessToken first", ); }); @@ -99,7 +99,7 @@ describe("ITwinPlatform", () => { let resource; requestSpy.and.callFake(function () { resource = this; - return JSON.stringify({ exports: [] }); + return { exports: [] }; }); await ITwinPlatform.getExports("imodel-id-1"); expect(resource).toBeDefined(); @@ -119,4 +119,296 @@ describe("ITwinPlatform", () => { expect(resource.url).toContain("imodel-id-1"); }); }); + + describe("getRealityDataMetadata", () => { + let requestSpy; + beforeEach(() => { + requestSpy = spyOn(Resource.prototype, "fetchJson"); + }); + + it("rejects with no iTwinId", async () => { + await expectAsync( + ITwinPlatform.getRealityDataMetadata(undefined), + ).toBeRejectedWithDeveloperError( + "Expected iTwinId to be typeof string, actual typeof was undefined", + ); + }); + + it("rejects with no realityDataId", async () => { + await expectAsync( + ITwinPlatform.getRealityDataMetadata("itwin-id-1", undefined), + ).toBeRejectedWithDeveloperError( + "Expected realityDataId to be typeof string, actual typeof was undefined", + ); + }); + + it("rejects with no default access token set", async () => { + ITwinPlatform.defaultAccessToken = undefined; + await expectAsync( + ITwinPlatform.getRealityDataMetadata("itwin-id-1", "reality-data-id-1"), + ).toBeRejectedWithDeveloperError( + "Must set ITwinPlatform.defaultAccessToken first", + ); + }); + + it("rejects for API 401 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 401, + JSON.stringify({ + error: { message: "failed", details: [{ code: "InvalidToken" }] }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataMetadata("itwin-id-1", "reality-data-id-1"), + ).toBeRejectedWithError(RuntimeError, /Unauthorized/); + }); + + it("rejects for API 403 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 403, + JSON.stringify({ + error: { message: "failed", code: "Forbidden" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataMetadata("itwin-id-1", "reality-data-id-1"), + ).toBeRejectedWithError(RuntimeError, /forbidden/); + }); + + it("rejects for API 404 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 404, + JSON.stringify({ + error: { message: "" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataMetadata("itwin-id-1", "reality-data-id-1"), + ).toBeRejectedWithError(RuntimeError, /not found/); + }); + + it("rejects for API 422 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 422, + JSON.stringify({ + error: { message: "failed", code: "BadEntity" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataMetadata("itwin-id-1", "reality-data-id-1"), + ).toBeRejectedWithError(RuntimeError, /Unprocessable/); + }); + + it("rejects for API 429 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 429, + JSON.stringify({ + error: { message: "" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataMetadata("itwin-id-1", "reality-data-id-1"), + ).toBeRejectedWithError(RuntimeError, /Too many/); + }); + + it("uses the default access token for the API request", async () => { + let resource; + requestSpy.and.callFake(function () { + resource = this; + return { realityData: {} }; + }); + await ITwinPlatform.getRealityDataMetadata( + "itwin-id-1", + "reality-data-id-1", + ); + expect(resource).toBeDefined(); + expect(resource.headers["Authorization"]).toEqual( + "Bearer default-access-token", + ); + }); + }); + + describe("getRealityDataURL", () => { + let requestSpy; + beforeEach(() => { + requestSpy = spyOn(Resource.prototype, "fetchJson"); + }); + + it("rejects with no iTwinId", async () => { + await expectAsync( + ITwinPlatform.getRealityDataURL(undefined), + ).toBeRejectedWithDeveloperError( + "Expected iTwinId to be typeof string, actual typeof was undefined", + ); + }); + + it("rejects with no realityDataId", async () => { + await expectAsync( + ITwinPlatform.getRealityDataURL("itwin-id-1", undefined), + ).toBeRejectedWithDeveloperError( + "Expected realityDataId to be typeof string, actual typeof was undefined", + ); + }); + it("rejects with no rootDocument", async () => { + await expectAsync( + ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + undefined, + ), + ).toBeRejectedWithDeveloperError( + "Expected rootDocument to be typeof string, actual typeof was undefined", + ); + }); + + it("rejects with no default access token set", async () => { + ITwinPlatform.defaultAccessToken = undefined; + await expectAsync( + ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ), + ).toBeRejectedWithDeveloperError( + "Must set ITwinPlatform.defaultAccessToken first", + ); + }); + + it("rejects for API 401 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 401, + JSON.stringify({ + error: { message: "failed", details: [{ code: "InvalidToken" }] }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ), + ).toBeRejectedWithError(RuntimeError, /Unauthorized/); + }); + + it("rejects for API 403 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 403, + JSON.stringify({ + error: { message: "failed", code: "Forbidden" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ), + ).toBeRejectedWithError(RuntimeError, /forbidden/); + }); + + it("rejects for API 404 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 404, + JSON.stringify({ + error: { message: "" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ), + ).toBeRejectedWithError(RuntimeError, /not found/); + }); + + it("rejects for API 422 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 422, + JSON.stringify({ + error: { message: "failed", code: "BadEntity" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ), + ).toBeRejectedWithError(RuntimeError, /Unprocessable/); + }); + + it("rejects for API 429 errors", async () => { + requestSpy.and.rejectWith( + new RequestErrorEvent( + 429, + JSON.stringify({ + error: { message: "" }, + }), + ), + ); + await expectAsync( + ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ), + ).toBeRejectedWithError(RuntimeError, /Too many/); + }); + + it("uses the default access token for the API request", async () => { + let resource; + requestSpy.and.callFake(function () { + resource = this; + return { + _links: { + containerUrl: { href: "https://example.com/base/path?auth=token" }, + }, + }; + }); + await ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ); + expect(resource).toBeDefined(); + expect(resource.headers["Authorization"]).toEqual( + "Bearer default-access-token", + ); + }); + + it("combines the rootDocument with the access url", async () => { + requestSpy.and.resolveTo({ + _links: { + containerUrl: { href: "https://example.com/base/path?auth=token" }, + }, + }); + const tilesetUrl = await ITwinPlatform.getRealityDataURL( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ); + expect(tilesetUrl).toEqual( + "https://example.com/base/path/root/document/path.json?auth=token", + ); + }); + }); }); diff --git a/packages/engine/Specs/Scene/ITwinDataSpec.js b/packages/engine/Specs/Scene/ITwinDataSpec.js index f407483eec0..8eb4322c577 100644 --- a/packages/engine/Specs/Scene/ITwinDataSpec.js +++ b/packages/engine/Specs/Scene/ITwinDataSpec.js @@ -116,4 +116,109 @@ describe("ITwinData", () => { expect(tilesetSpy.calls.mostRecent().args[1]).toEqual(tilesetOptions); }); }); + + describe("createTilesetForRealityDataId", () => { + let getMetadataSpy; + let getUrlSpy; + let tilesetSpy; + beforeEach(() => { + getMetadataSpy = spyOn(ITwinPlatform, "getRealityDataMetadata"); + getUrlSpy = spyOn(ITwinPlatform, "getRealityDataURL"); + tilesetSpy = spyOn(Cesium3DTileset, "fromUrl"); + }); + + it("rejects if the type is not supported", async () => { + await expectAsync( + ITwinData.createTilesetForRealityDataId( + "imodel-id-1", + "reality-data-id-1", + ITwinPlatform.RealityDataType.DGN, + "root/path.json", + ), + ).toBeRejectedWithError(RuntimeError, /type is not/); + }); + + it("does not fetch metadata if type and rootDocument are defined", async () => { + await ITwinData.createTilesetForRealityDataId( + "itwin-id-1", + "reality-data-id-1", + ITwinPlatform.RealityDataType.Cesium3DTiles, + "root/document/path.json", + ); + + expect(getMetadataSpy).not.toHaveBeenCalled(); + expect(getUrlSpy).toHaveBeenCalledOnceWith( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ); + }); + + it("fetches metadata if type is undefined", async () => { + getMetadataSpy.and.resolveTo({ + iModelId: "itwin-id-1", + id: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.Cesium3DTiles, + rootDocument: "root/document/path.json", + }); + await ITwinData.createTilesetForRealityDataId( + "itwin-id-1", + "reality-data-id-1", + undefined, + "root/document/path.json", + ); + + expect(getMetadataSpy).toHaveBeenCalledOnceWith( + "itwin-id-1", + "reality-data-id-1", + ); + expect(getUrlSpy).toHaveBeenCalledOnceWith( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ); + }); + + it("fetches metadata if rootDocument is undefined", async () => { + getMetadataSpy.and.resolveTo({ + iModelId: "itwin-id-1", + id: "reality-data-id-1", + type: ITwinPlatform.RealityDataType.Cesium3DTiles, + rootDocument: "root/document/path.json", + }); + await ITwinData.createTilesetForRealityDataId( + "itwin-id-1", + "reality-data-id-1", + ITwinPlatform.RealityDataType.Cesium3DTiles, + undefined, + ); + + expect(getMetadataSpy).toHaveBeenCalledOnceWith( + "itwin-id-1", + "reality-data-id-1", + ); + expect(getUrlSpy).toHaveBeenCalledOnceWith( + "itwin-id-1", + "reality-data-id-1", + "root/document/path.json", + ); + }); + + it("creates a tileset from the constructed blob url", async () => { + const tilesetUrl = + "https://example.com/root/document/path.json?auth=token"; + getUrlSpy.and.resolveTo(tilesetUrl); + + await ITwinData.createTilesetForRealityDataId( + "itwin-id-1", + "reality-data-id-1", + ITwinPlatform.RealityDataType.Cesium3DTiles, + "root/document/path.json", + ); + + expect(tilesetSpy).toHaveBeenCalledOnceWith(tilesetUrl, { + maximumScreenSpaceError: 4, + }); + }); + }); });