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

iModel integration #12289

Merged
merged 25 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0f6db77
initial rough api
jjspace Nov 5, 2024
421e5dd
restructure api, create tileset from model id
jjspace Nov 5, 2024
4ea3908
small adjustments
jjspace Nov 6, 2024
d779bc1
oauth testing web app and service app
jjspace Nov 11, 2024
50dfe46
small cleanup and removing code we won't use
jjspace Nov 14, 2024
c953b6f
cleanup and adjustments from pr comments
jjspace Nov 15, 2024
dc79961
Merge remote-tracking branch 'origin/main' into itwin-integration
jjspace Nov 15, 2024
46bd2f0
rename namespaces
jjspace Nov 15, 2024
1757f77
partial cleanup, remove changeset ids
jjspace Nov 19, 2024
b8c1ac5
remove get export and creation routes
jjspace Nov 19, 2024
958756e
update sandcastle for exports still processing
jjspace Nov 19, 2024
7049dbe
re-organize, resource instead of fetch, clean up types
jjspace Nov 20, 2024
188e2bf
fix type generation
jjspace Nov 20, 2024
3726464
minor renaming and descriptions
jjspace Nov 20, 2024
d2055b2
pull out dev auth server
jjspace Nov 22, 2024
47e1642
condense code, update docs
jjspace Nov 22, 2024
990004b
streamline sandcastle with new imodels
jjspace Nov 22, 2024
c435e2b
Merge remote-tracking branch 'origin/main' into itwin-integration
jjspace Nov 22, 2024
fa1ef66
add tests
jjspace Nov 23, 2024
d478736
sandcastle photospheres
jjspace Nov 23, 2024
153020b
doc updates
jjspace Nov 25, 2024
d066248
switch to ion auth, remove enum comments
jjspace Nov 25, 2024
77a45d8
add enum values in comments
jjspace Nov 25, 2024
99eb928
sandcastle adjustments
jjspace Nov 25, 2024
0cc0603
Merge branch 'main' into itwin-integration
ggetz Nov 25, 2024
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
!packages/**/
!Specs/**/
!Tools/**/
!itwin-oauth-demo/**/
jjspace marked this conversation as resolved.
Show resolved Hide resolved

!**/*.js
!**/*.cjs
Expand Down
186 changes: 186 additions & 0 deletions Apps/Sandcastle/gallery/iTwin Demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"
/>
<meta
name="description"
content="Use Viewer to start building new applications or easily embed Cesium into existing applications."
jjspace marked this conversation as resolved.
Show resolved Hide resolved
/>
<meta name="cesium-sandcastle-labels" content="Beginner, Showcases" />
jjspace marked this conversation as resolved.
Show resolved Hide resolved
<title>Cesium Demo</title>
<script type="text/javascript" src="../Sandcastle-header.js"></script>
<script type="module" src="../load-cesium-es6.js"></script>
</head>
<body class="sandcastle-loading" data-sandcastle-bucket="bucket-requirejs.html">
<style>
@import url(../templates/bucket.css);
</style>
<div id="cesiumContainer" class="fullSize"></div>
<div id="loadingOverlay"><h1>Loading...</h1></div>
<div id="toolbar">
<div id="checkbox"></div>
<output id="status">Initializing</output>
</div>
<script id="cesium_sandcastle_script">
window.startup = async function (Cesium) {
"use strict";
//Sandcastle_Begin
const serviceResponse = await fetch("http://localhost:3000/service");
const { token } = await serviceResponse.json();
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking about user workflow, should this be encapsulated in ITwin.requestAccessToken or a similar function?

That way, these first few code block is simplified to:

Cesium.ITwin.defaultAccessToken = await Cesium.ITwin.requestAccessToken();

and we could make small changes if needed without needing user code to change.

Copy link
Contributor Author

@jjspace jjspace Nov 15, 2024

Choose a reason for hiding this comment

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

I could add a function like that but I don't think users should ever be running this themselves. They should do their own oauth and just set the token. This request is just for the sandcastle examples and will only provide access to the sample itwins that we add to the service account.

Copy link
Contributor

Choose a reason for hiding this comment

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

I ask as it seams a bit strange to me that we would include a direct call to fetch in a Sandcastle example.

It doesn't seem too foreign to me that we would have a function that abstracts away the relevant Cesium ion URL to get the access token under the hood that we plan on using for authentication. For example, this is similar to what IonResource.fromAssetId is doing. In the future, we could potentially allow this function to reach out to an iTwin endpoint if available, or a user-provided endpoint without having to change the flow.

But let me know what you think.

Copy link
Contributor

Choose a reason for hiding this comment

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

@jjspace Thoughts?


Cesium.ITwinPlatform.defaultAccessToken = token;
// this is the iModel in the "Hello iTwinCesium" iTwin that we should all have access to
// https://developer.bentley.com/my-itwins/b4a30036-0456-49ea-a439-3fcd9365e24e/home/
// const imodelId = "2852c3d7-00c3-4b5d-a0ce-82bbde4f061e";
const imodelId = "88673c1d-12b8-48f1-8beb-5000d0edbd0b";
const changesetId = "";
jjspace marked this conversation as resolved.
Show resolved Hide resolved

// Grabbed mapping from the iTwin Viewer
const classes = {
2199023255632: "Building Roof",
2199023255694: "Building Wall",
2199023255696: "Building Window",
};

let selectedFeature;
let picking = false;

Sandcastle.addToggleButton(
"Per-feature selection",
false,
function (checked) {
picking = checked;
if (!picking) {
unselectFeature(selectedFeature);
}
},
"checkbox",
);

// Set up viewer
const viewer = new Cesium.Viewer("cesiumContainer", {
terrain: Cesium.Terrain.fromWorldTerrain(),
});
const scene = viewer.scene;
scene.globe.show = true;
scene.debugShowFramesPerSecond = true;

// HTML overlay for showing feature name on mouseover
const nameOverlay = document.createElement("div");
viewer.container.appendChild(nameOverlay);
nameOverlay.className = "backdrop";
nameOverlay.style.display = "none";
nameOverlay.style.position = "absolute";
nameOverlay.style.bottom = "0";
nameOverlay.style.left = "0";
nameOverlay.style["pointer-events"] = "none";
nameOverlay.style.padding = "4px";
nameOverlay.style.backgroundColor = "black";
nameOverlay.style.whiteSpace = "pre-line";
nameOverlay.style.fontSize = "12px";

function selectFeature(feature, movement) {
feature.color = Cesium.Color.clone(
Cesium.Color.fromCssColorString("#eeff41"),
feature.color,
);
selectedFeature = feature;

nameOverlay.style.display = "block";
nameOverlay.style.bottom = `${
viewer.canvas.clientHeight - movement.endPosition.y
}px`;
nameOverlay.style.left = `${movement.endPosition.x}px`;
const element = feature.getProperty("element");
const subcategory = feature.getProperty("subcategory");
const message = `
Element ID: ${element}
Subcategory: ${classes[subcategory] || subcategory}
Feature ID: ${feature.featureId}`;
nameOverlay.textContent = message;
}

function unselectFeature(feature) {
if (!Cesium.defined(feature)) {
return;
}

feature.color = Cesium.Color.clone(Cesium.Color.WHITE, feature.color);
selectedFeature = undefined;
nameOverlay.style.display = "none";
}

const statusOutput = document.querySelector("#status");
async function init() {
statusOutput.innerText = "Starting export";

const start = Date.now();

statusOutput.innerText = "Creating Tileset";

let tileset = await Cesium.ITwinData.createTilesetFromModelId(
jjspace marked this conversation as resolved.
Show resolved Hide resolved
imodelId,
changesetId,
);
if (!Cesium.defined(tileset)) {
// TODO: this is temporary, we should not have to call the Start Export route ever after
jjspace marked this conversation as resolved.
Show resolved Hide resolved
// auto generation is set up
statusOutput.innerText = "Starting export";
const exportId = await Cesium.ITwinPlatform.createExportForModelId(
imodelId,
changesetId,
);
statusOutput.innerText = "Creating Tileset from export";
tileset = await Cesium.ITwinData.createTilesetFromExportId(exportId);
}
jjspace marked this conversation as resolved.
Show resolved Hide resolved

scene.primitives.add(tileset);
tileset.colorBlendMode = Cesium.Cesium3DTileColorBlendMode.REPLACE;

statusOutput.innerText = "Loaded";

viewer.zoomTo(tileset);
console.log(`Finished in ${((Date.now() - start) / 1000).toString()} seconds`);

const handler = new Cesium.ScreenSpaceEventHandler(scene.canvas);
handler.setInputAction(function (movement) {
if (!picking) {
return;
}
unselectFeature(selectedFeature);

const feature = scene.pick(movement.endPosition);

if (feature instanceof Cesium.Cesium3DTileFeature) {
selectFeature(feature, movement);
}
}, Cesium.ScreenSpaceEventType.MOUSE_MOVE);
}

init().catch((error) => {
statusOutput.style.color = "red";
if (error.message.includes("Unauthorized")) {
statusOutput.innerText = "Error: Unauthorized";
} else {
statusOutput.innerText = "Error";
}
console.error(error);
});
//Sandcastle_End
Sandcastle.finishedLoading();
};
if (typeof Cesium !== "undefined") {
window.startupCalled = true;
window.startup(Cesium).catch((error) => {
"use strict";
console.error(error);
});
}
</script>
</body>
</html>
8 changes: 8 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,12 @@ export default [
"n/no-missing-import": "off",
},
},
{
jjspace marked this conversation as resolved.
Show resolved Hide resolved
files: ["itwin-oauth-demo/*"],
languageOptions: {
...configCesium.configs.node.languageOptions,
sourceType: "module",
ecmaVersion: 2022,
},
},
];
1 change: 1 addition & 0 deletions itwin-oauth-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
config.json
34 changes: 34 additions & 0 deletions itwin-oauth-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<!doctype html>
jjspace marked this conversation as resolved.
Show resolved Hide resolved
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Auth!</title>
</head>
<body>
<script type="module">
// based on https://gist.github.com/gauravtiwari/2ae9f44aee281c759fe5a66d5c2721a2

console.log(window.location);

const code = new URL(window.location).searchParams.get("code");
console.log("code", code);

const response = await fetch(`/token?code=${code}`);
const result = await response.json();
const token = result.token;

if (!code) {
window.postMessage({ error: "Login failed" });
} else {
// this will need the sandcastle page url. The example uses `window.opener.location` but
// cross origin from inside the iframe prevents this
window.opener.postMessage(
{ auth: { code: token } },
"http://localhost:8080",
);
}
console.log("message posted");
</script>
</body>
</html>
66 changes: 66 additions & 0 deletions itwin-oauth-demo/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import express from "express";
import { readFileSync, writeFileSync } from "fs";
import { dirname, join } from "path";
import { exit } from "process";
import { fileURLToPath } from "url";

let config = {
serviceapp: {
clientId: "",
clientSecret: "",
},
port: 3000,
};

const __dirname = dirname(fileURLToPath(import.meta.url));
const configPath = join(__dirname, "./config.json");
try {
const configFile = readFileSync(configPath, { encoding: "utf-8" });
config = JSON.parse(configFile);
} catch {
console.log("config file missing, default written to", configPath);
console.log("Please update the config with the desired values");
writeFileSync(configPath, JSON.stringify(config, undefined, 2));
exit(1);
}

const app = express();
const port = config.port ?? 3000;

// eslint-disable-next-line no-unused-vars
app.get("/service", async (req, res) => {
console.log("/service request received");

const body = new URLSearchParams();
body.set("grant_type", "client_credentials");
body.set("client_id", config.serviceapp.clientId);
body.set("client_secret", config.serviceapp.clientSecret);
body.set("scope", "itwin-platform");

const response = await fetch("https://ims.bentley.com/connect/token", {
method: "POST",
body,
});

const result = await response.json();

res.setHeader("Access-Control-Allow-Origin", "*");

if (!response.ok || !result) {
console.log(" bad response/no result");
res.status(response.status).send();
return;
}
const { access_token } = result;
if (access_token) {
console.log(" token acquired, returned");
res.status(200).send({ token: access_token });
return;
}
console.log(" token not found");
res.status(404).send("token not found");
});

app.listen(port, () => {
console.log(`Server listening on port ${port}`);
});
Loading