Skip to content

Commit 8674c3c

Browse files
authored
Adding imodel view cesium-sandcastle command (#75)
1 parent d246cb9 commit 8674c3c

8 files changed

Lines changed: 247 additions & 8 deletions

File tree

docs/_sidebar.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* [auth](auth/overview.md)
77
* [login](auth/login.md)
88
* [logout](auth/logout.md)
9+
* [api](api.md)
910
* [itwin](itwin/overview.md)
1011
* [create](itwin/create.md)
1112
* [update](itwin/update.md)
@@ -20,6 +21,8 @@
2021
* [delete](imodel/delete.md)
2122
* [info](imodel/info.md)
2223
* [list](imodel/list.md)
24+
* [view](imodel/view/overview.md)
25+
* [cesium-sandcastle](imodel/view/cesium-sandcastle.md)
2326
* [changeset](imodel/changeset/overview.md)
2427
* [named-version](imodel/named-version/overview.md)
2528
* [connection](imodel/connection/overview.md)

docs/imodel/overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Work with iModels of an iTwin.
1010
- [itp imodel delete](imodel/delete.md)
1111
- [itp imodel info](imodel/info.md)
1212
- [itp imodel list](imodel/list.md)
13+
- [itp imodel view](imodel/view/overview.md)
1314
- [itp imodel changeset](imodel/changeset/overview.md)
1415
- [itp imodel namedversion](imodel/named-version/overview.md)
1516
- [itp imodel connection](imodel/connection/overview.md)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# itp imodel view cesium-sandcastle
2+
3+
Setup iModel and get URL to view it in Cesium Sandcastle.
4+
5+
## Options
6+
7+
- **`--changeset-id`**
8+
Changeset id to be viewed in Cesium Sandcastle.
9+
**Type:** `string` **Required:** Yes
10+
11+
- **`-m, --imodel-id`**
12+
iModel id to be viewed in Cesium Sandcastle.
13+
**Type:** `string` **Required:** Yes
14+
15+
- **`--open`**
16+
Open the URL in the browser.
17+
**Type:** `boolean` **Required:** No
18+
19+
## Examples
20+
21+
```bash
22+
# Example 1: Get a link to a specific changeset of an iModel in Cesium Sandcastle
23+
itp imodel view cesium-sandcastle --imodel-id "5e19bee0-3aea-4355-a9f0-c6df9989ee7d" --changeset-id "2f3b4a8c92d747d5c8a8b2f9cde6742e5d74b3b5"
24+
25+
# Example 2: Get a link to a specific changeset of an iModel in Cesium Sandcastle and open the URL in the browser
26+
itp imodel view cesium-sandcastle --imodel-id "5e19bee0-3aea-4355-a9f0-c6df9989ee7d" --changeset-id "2f3b4a8c92d747d5c8a8b2f9cde6742e5d74b3b5" --open
27+
```
28+
29+
## API Reference
30+
31+
[Cesium Sandcastle](https://cesium.com/docs/sandcastle/)

docs/imodel/view/overview.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# itp imodel view
2+
3+
Work with views for an iModel.
4+
5+
## Available Commands
6+
7+
- [itp imodel view cesium-sandcastle](imodel/view/cesium-sandcastle.md)

package-lock.json

Lines changed: 18 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "itp",
33
"description": "Work seamlessly with the iTwin Platform",
4-
"version": "0.0.3",
4+
"version": "0.0.4",
55
"author": "Bentley systems, Incorporated",
66
"bin": {
77
"itp": "./bin/run.js"
@@ -21,14 +21,16 @@
2121
"dotenv": "^16.4.5",
2222
"fs-extra": "^8.1.0",
2323
"jwt-decode": "^4.0.0",
24-
"open": "^10.1.0"
24+
"open": "^10.1.0",
25+
"pako": "^2.1.0"
2526
},
2627
"devDependencies": {
2728
"@oclif/prettier-config": "^0.2.1",
2829
"@oclif/test": "^4",
2930
"@types/chai": "^4",
3031
"@types/mocha": "^10",
3132
"@types/node": "^18.19.64",
33+
"@types/pako": "^2.0.3",
3234
"chai": "^4",
3335
"eslint": "^8",
3436
"eslint-config-oclif": "^5",

src/commands/api/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,26 +51,26 @@ export default class ApiRequest extends BaseCommand {
5151

5252
const mappedHeaders: Record<string, string> = flags.header?.reduce((acc, header) => {
5353
const [key, value] = header.split(":");
54-
acc[key] = value;
54+
acc[key] = value.trim();
5555
return acc;
5656
}, {} as Record<string, string>) || {};
5757

5858
const query: Query[] | undefined = flags.query?.map((query) => {
5959
const [key, value] = query.split(":");
60-
return { key, value };
60+
return { key: key.trim(), value: value.trim() };
6161
}) || undefined;
6262

6363
const client = await this.getITwinApiClient();
6464

6565
const requestOptions = {
6666
apiPath: flags.path,
6767
apiVersionHeader: flags["version-header"],
68+
body: flags.body ? JSON.parse(flags.body) : undefined,
6869
headers: mappedHeaders,
6970
method: flags.method as "DELETE" | "GET" | "PATCH" | "POST" | "PUT",
7071
query
7172
};
7273

73-
7474
if (flags["empty-response"]) {
7575
await client.sendRequestNoResponse(requestOptions);
7676
return this.logAndReturnResult({result: "success"});
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3+
* See LICENSE.md in the project root for license terms and full copyright notice.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Flags } from "@oclif/core";
7+
import open from 'open';
8+
import { deflate } from "pako";
9+
10+
import BaseCommand from "../../../extensions/base-command.js";
11+
import { link, links } from "../../../services/general-models/links.js";
12+
13+
export default class CesiumSandcastle extends BaseCommand {
14+
static description = "Setup iModel and get url to view it in Cesium Sandcastle";
15+
16+
static flags = {
17+
"changeset-id": Flags.string({
18+
description: "Changeset id to be viewed in Cesium Sandcastle.",
19+
required: true
20+
}),
21+
"imodel-id": Flags.string({
22+
char: "m",
23+
description: "iModel id to be viewed in Cesium Sandcastle.",
24+
required: true
25+
}),
26+
"open": Flags.boolean({
27+
description: "Open the URL in the browser.",
28+
required: false,
29+
}),
30+
};
31+
32+
async createExport(iModelId: string, changesetId: string): Promise<ExportInfo> {
33+
const args = [
34+
"--method", "POST",
35+
"--path", "mesh-export",
36+
"--version-header", "application/vnd.bentley.itwin-platform.v1+json",
37+
"--body", JSON.stringify({
38+
changesetId,
39+
exportType: "CESIUM",
40+
iModelId
41+
}),
42+
];
43+
44+
const created = await this.runCommand<ExportCreateResponse>("api", args);
45+
return created.export;
46+
}
47+
48+
async getExports(iModelId: string) : Promise<ExportInfo[]> {
49+
const exportArgs = [
50+
"--method", "GET",
51+
"--path", "mesh-export/",
52+
"--version-header", "application/vnd.bentley.itwin-platform.v1+json",
53+
"--query", `iModelId: ${iModelId}`,
54+
"--header", "Prefer: return=representation"
55+
];
56+
const response = await this.runCommand<ExportResponse>("api", exportArgs);
57+
return response.exports;
58+
}
59+
60+
async getOrCreateExport(iModelId: string, changesetId: string): Promise<ExportInfo> {
61+
this.log(`Getting existing exports for iModel: ${iModelId} and changeset: ${changesetId}`);
62+
let existingExports = await this.getExports(iModelId);
63+
const existingExport = existingExports.find((exp) => exp.request.exportType === "CESIUM" && exp.request.changesetId === changesetId);
64+
65+
if (existingExport !== undefined) {
66+
this.log(`Found existing export with id: ${existingExport.id}`);
67+
return existingExport;
68+
}
69+
70+
this.log(`Creating new export for iModel: ${iModelId} and changeset: ${changesetId}`);
71+
let newExport = await this.createExport(iModelId, changesetId);
72+
while (newExport.status !== "Complete") {
73+
this.log(`Export status is ${newExport.status}. Waiting for export to complete...`);
74+
// eslint-disable-next-line no-await-in-loop
75+
existingExports = await this.getExports(iModelId);
76+
newExport = existingExports.find((exp) => exp.id === newExport.id)!;
77+
// eslint-disable-next-line no-await-in-loop
78+
await new Promise((resolve) => {setTimeout(resolve, 5000)});
79+
}
80+
81+
this.log(`Export completed successfully`);
82+
return newExport;
83+
}
84+
85+
async run() {
86+
const { flags } = await this.parse(CesiumSandcastle);
87+
88+
const exportInfo : ExportInfo = await this.getOrCreateExport(flags["imodel-id"], flags["changeset-id"]);
89+
90+
this.log(`Extracting tileset URL from export info`);
91+
const tilesetUrl : string = extractTileSetUrl(exportInfo);
92+
93+
const data = [
94+
jsData(tilesetUrl),
95+
htmlData(),
96+
];
97+
98+
const url = `https://sandcastle.cesium.com/#c=${makeCompressedBase64String(data)}`;
99+
100+
this.log(`Cesium Sandcastle URL:`);
101+
this.log(url);
102+
103+
if (flags.open) {
104+
this.log(`Opening URL in browser...`);
105+
open(url);
106+
}
107+
108+
return this.logAndReturnResult({ url });
109+
}
110+
}
111+
112+
113+
type ExportResponse = {
114+
_links: links
115+
exports: ExportInfo[],
116+
}
117+
118+
type ExportCreateResponse = {
119+
export: ExportInfo
120+
}
121+
122+
type ExportInfo = {
123+
_links: {
124+
mesh: link
125+
},
126+
displayName: string,
127+
error?: string,
128+
id: string,
129+
lastModified: Date,
130+
request: ExportRequest,
131+
status: "Complete" | "InProgress" | "Invalid" | "NotStarted",
132+
}
133+
134+
type ExportRequest = {
135+
changesetId: string,
136+
exportType: "3DFT" | "3DTiles" | "CESIUM" | "IMODEL",
137+
iModelId: string,
138+
}
139+
140+
function extractTileSetUrl(exportInfo: ExportInfo): string {
141+
if(exportInfo._links.mesh.href === undefined) {
142+
throw new Error(`No tileset url found for export info id: ${exportInfo.id}`);
143+
}
144+
145+
const urlParts = exportInfo._links.mesh.href.split("?");
146+
return urlParts[0] + "/tileset.json?" + urlParts[1];
147+
}
148+
149+
function makeCompressedBase64String(data: string[]) : string {
150+
let jsonString = JSON.stringify(data);
151+
jsonString = jsonString.slice(2, 2 + jsonString.length - 4);
152+
let base64String = Buffer.from(
153+
deflate(jsonString, { raw: true })
154+
).toString('base64');
155+
base64String = base64String.replace(/=+$/, ''); // remove padding
156+
157+
return base64String;
158+
}
159+
160+
function htmlData() : string {
161+
return `
162+
<style>
163+
@import url(../templates/bucket.css);
164+
</style>
165+
166+
<div id="cesiumContainer" class="fullSize"></div>
167+
`;
168+
}
169+
170+
function jsData(tilesetUrl: string) : string {
171+
return `
172+
const viewer = new Cesium.Viewer("cesiumContainer");
173+
viewer.scene.globe.show = true;
174+
viewer.scene.debugShowFramesPerSecond = true;
175+
const tilesetUrl = '${tilesetUrl}';
176+
const tileset = await Cesium.Cesium3DTileset.fromUrl(tilesetUrl);
177+
viewer.scene.primitives.add(tileset);
178+
viewer.zoomTo(tileset);
179+
`;
180+
}

0 commit comments

Comments
 (0)