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

[wip] add support for iTP curated-content/cesium endpoint #7436

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
78 changes: 62 additions & 16 deletions core/frontend/src/tile/map/CesiumTerrainProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,27 @@ enum QuantizedMeshExtensionIds {
Metadata = 4,
}

type ContentType =
| "3DTiles"
| "GLTF"
| "IMAGERY"
| "TERRAIN"
| "KML"
| "CZML"
| "GEOJSON";

interface ContentTileAttribution {
html: string;
collapsible: boolean;
}

interface CesiumContentAccessTileProps {
type: ContentType;
url: string;
accessToken: string;
attributions: ContentTileAttribution[];
}

/** Return the URL for a Cesium ION asset from its asset ID and request Key.
* @public
*/
Expand All @@ -46,13 +67,13 @@ export function getCesiumOSMBuildingsUrl(): string | undefined {
export async function getCesiumAccessTokenAndEndpointUrl(assetId: string, requestKey?: string): Promise<{ token?: string, url?: string }> {
if (undefined === requestKey) {
requestKey = IModelApp.tileAdmin.cesiumIonKey;
if (undefined === requestKey)
if (undefined === requestKey) {
notifyTerrainError(IModelApp.localization.getLocalizedString(`iModelJs:BackgroundMap.MissingCesiumToken`));
return {};
}
}

const requestTemplate = `https://api.cesium.com/v1/assets/${assetId}/endpoint?access_token={CesiumRequestToken}`;
const apiUrl: string = requestTemplate.replace("{CesiumRequestToken}", requestKey);

const apiUrl = `https://api.cesium.com/v1/assets/${assetId}/endpoint?access_token=${requestKey}`;
try {
const apiResponse = await request(apiUrl, "json");
if (undefined === apiResponse || undefined === apiResponse.url) {
Expand All @@ -77,18 +98,17 @@ function notifyTerrainError(detailedDescription?: string): void {
IModelApp.notifications.displayMessage(MessageSeverity.Information, IModelApp.localization.getLocalizedString(`iModelJs:BackgroundMap.CannotObtainTerrain`), detailedDescription);
}

/** @internal */
export async function getCesiumTerrainProvider(opts: TerrainMeshProviderOptions): Promise<TerrainMeshProvider | undefined> {
const accessTokenAndEndpointUrl = await getCesiumAccessTokenAndEndpointUrl(opts.dataSource || CesiumTerrainAssetId.Default);
if (!accessTokenAndEndpointUrl.token || !accessTokenAndEndpointUrl.url) {
notifyTerrainError(IModelApp.localization.getLocalizedString(`iModelJs:BackgroundMap.MissingCesiumToken`));
/** @beta */
export async function createCesiumTerrainProvider(opts: TerrainMeshProviderOptions & Partial<CesiumContentAccessTileProps>): Promise<TerrainMeshProvider | undefined> {
Copy link
Member Author

Choose a reason for hiding this comment

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

hoping to get the displays team input here on what theyre preference is
should we expose a helper fn like this or just change the tag on CesiumTerrainProvider?

Copy link
Member Author

Choose a reason for hiding this comment

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

@pmconne let me know if you have thoughts

Copy link
Member

Choose a reason for hiding this comment

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

Why is this beta and how is somebody expected to use it? Not every curated Cesium asset represents "terrain".

if (opts.url === undefined || opts.accessToken === undefined) {
notifyTerrainError();
return undefined;
}

let layers;
try {
const layerRequestOptions: RequestOptions = { headers: { authorization: `Bearer ${accessTokenAndEndpointUrl.token}` } };
const layerUrl = `${accessTokenAndEndpointUrl.url}layer.json`;
const layerRequestOptions: RequestOptions = { headers: { authorization: `Bearer ${opts.accessToken}` } };
const layerUrl = `${opts.url}layer.json`;
layers = await request(layerUrl, "json", layerRequestOptions);
} catch {
notifyTerrainError();
Expand Down Expand Up @@ -119,14 +139,31 @@ export async function getCesiumTerrainProvider(opts: TerrainMeshProviderOptions)
}
}

let tileUrlTemplate = accessTokenAndEndpointUrl.url + layers.tiles[0].replace("{version}", layers.version);
let tileUrlTemplate = opts.url + layers.tiles[0].replace("{version}", layers.version);
if (opts.wantNormals)
tileUrlTemplate = tileUrlTemplate.replace("?", "?extensions=octvertexnormals-watermask-metadata&");

const maxDepth = JsonUtils.asInt(layers.maxzoom, 19);

// TBD -- When we have an API extract the heights for the project from the terrain tiles - for use temporary Bing elevation.
return new CesiumTerrainProvider(opts, accessTokenAndEndpointUrl.token, tileUrlTemplate, maxDepth, tilingScheme, tileAvailability, layers.metadataAvailability);
// TBD -- When we have an API extract the heights for the project from the terrain tiles - for use temporary Bing elevation.
return new CesiumTerrainProvider({
opts,
accessToken: opts.accessToken,
tileUrlTemplate,
maxDepth,
tilingScheme,
tileAvailability,
metaDataAvailableLevel: layers.metadataAvailability,
});
}

/** @internal */
export async function getCesiumTerrainProvider(opts: TerrainMeshProviderOptions): Promise<TerrainMeshProvider | undefined> {
const accessTokenAndEndpointUrl = await getCesiumAccessTokenAndEndpointUrl(opts.dataSource || CesiumTerrainAssetId.Default);
if (!accessTokenAndEndpointUrl.token || !accessTokenAndEndpointUrl.url) {
return undefined;
}
return createCesiumTerrainProvider({ ...opts, ...accessTokenAndEndpointUrl });
}

function zigZagDecode(value: number) {
Expand Down Expand Up @@ -156,6 +193,16 @@ function zigZagDeltaDecode(uBuffer: Uint16Array, vBuffer: Uint16Array, heightBuf
}
}

interface CesiumTerrainProviderOptions {
opts: TerrainMeshProviderOptions;
accessToken: string;
tileUrlTemplate: string;
maxDepth: number;
tilingScheme: MapTilingScheme;
tileAvailability: TileAvailability | undefined;
metaDataAvailableLevel: number | undefined;
}

/** @internal */
class CesiumTerrainProvider extends TerrainMeshProvider {
private _accessToken: string;
Expand All @@ -182,8 +229,7 @@ class CesiumTerrainProvider extends TerrainMeshProvider {
return undefined !== this._metaDataAvailableLevel && mapTile.quadId.level === this._metaDataAvailableLevel && !mapTile.everLoaded;
}

constructor(opts: TerrainMeshProviderOptions, accessToken: string, tileUrlTemplate: string, maxDepth: number, tilingScheme: MapTilingScheme,
tileAvailability: TileAvailability | undefined, metaDataAvailableLevel: number | undefined) {
constructor({ opts, accessToken, tileUrlTemplate, maxDepth, tilingScheme, tileAvailability, metaDataAvailableLevel }: CesiumTerrainProviderOptions) {
super();
this._wantSkirts = opts.wantSkirts;
this._exaggeration = opts.exaggeration;
Expand Down
1 change: 1 addition & 0 deletions core/frontend/src/tile/map/MapTileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,6 +730,7 @@ class MapTreeSupplier implements TileTreeSupplier {
wantNormals: id.wantNormals,
dataSource: id.terrainDataSource,
produceGeometry: id.produceGeometry,
iTwinId: iModel.iTwinId,
};

if (id.applyTerrain) {
Expand Down
4 changes: 4 additions & 0 deletions core/frontend/src/tile/map/TerrainMeshProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export interface TerrainMeshProviderOptions {
* @beta
*/
produceGeometry?: boolean;
/** Optional id of iTwin of which the [[TerrainSettings.dataSource]] belongs to.
* @alpha
*/
iTwinId?: string;
}

/** Arguments supplied to [[TerrainMeshProvider.requestMeshData]].
Expand Down
8 changes: 7 additions & 1 deletion test-apps/display-test-app/src/frontend/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { ElectronRendererAuthorization } from "@itwin/electron-authorization/Ren
import { ITwinLocalization } from "@itwin/core-i18n";
import { getConfigurationString } from "./DisplayTestApp";
import { AddSeequentRealityModel } from "./RealityDataModel";
import { registerCesiumCuratedContentProvider } from "./CuratedCesiumContentProvider";

class DisplayTestAppAccuSnap extends AccuSnap {
private readonly _activeSnaps: SnapMode[] = [SnapMode.NearestKeypoint];
Expand Down Expand Up @@ -233,12 +234,13 @@ export class DisplayTestApp {

public static async startup(configuration: DtaConfiguration, renderSys: RenderSystem.Options, tileAdmin: TileAdmin.Props): Promise<void> {
let socketUrl = new URL(configuration.customOrchestratorUri || "http://localhost:3001");
const iTwinPlatformUrl = `https://${process.env.IMJS_URL_PREFIX ?? ""}api.bentley.com`
socketUrl = LocalhostIpcApp.buildUrlForSocket(socketUrl);
const realityDataClientOptions: RealityDataClientOptions = {
/** API Version. v1 by default */
// version?: ApiVersion;
/** API Url. Used to select environment. Defaults to "https://api.bentley.com/reality-management/reality-data" */
baseUrl: `https://${process.env.IMJS_URL_PREFIX ?? ""}api.bentley.com`,
baseUrl: iTwinPlatformUrl,
};
const opts: ElectronAppOpts | LocalHostIpcAppOpts = {
iModelApp: {
Expand Down Expand Up @@ -377,6 +379,10 @@ export class DisplayTestApp {
IModelApp.toolAdmin.defaultToolId = SVTSelectionTool.toolId;

BingTerrainMeshProvider.register();
await registerCesiumCuratedContentProvider({
iTwinId: configuration.iTwinId,
baseUrl: iTwinPlatformUrl,
});

const realityApiKey = process.env.IMJS_REALITY_DATA_KEY;
if (realityApiKey)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { CesiumTerrainAssetId } from "@itwin/core-common";
Copy link
Member

Choose a reason for hiding this comment

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

Are the contents of this file intended to represent some public open-source package we will make available for any app to use?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ideally not, it would be sample code in our test apps and maybe a sample in the sandbox, as well as our docs, of how app teams can implement this themselves

Copy link
Member

Choose a reason for hiding this comment

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

You expect every app to have to implement / copy-paste this big ugly blob of code just to be able to use the terrain that they currently get just by providing an API key? That seems like a huge downgrade and quite error-prone.

Copy link
Member Author

Choose a reason for hiding this comment

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

Granted its very ugly right now, and needs to be cleaned up, but there isn't that much code that needs to be added. App developers can customize it further to meet their needs, eg not rely on IModelApp to obtain access token, they might use an http client that additional caching, etc. Introducing another package to our already growing number of packages, doesn't seem responsible, especially as this is a beta api and very likely to change

Copy link
Member

Choose a reason for hiding this comment

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

You have effectively defined a client library for the new curated content API already. Do you really expect every consumer to write their own interfaces and enums representing types defined by that REST API? Write their own logic to rustle up an iTwin Id to pass to an API that does not give two hoots what iTwin Id you give it?

I understand not wanting to have a direct call to iTP from within itwinjs-core. But can we show a little regard for our users who will have to deal with all of this unnecessary new friction? Where's the value-add to justify it?

Copy link
Member Author

Choose a reason for hiding this comment

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

I would like to disagree. While I have added the interfaces/enums they aren't really being used (except to cast for convenience but not required), and as they are tied to the service in tech preview, they are subject to change (potentially). At the end of the day it is a single HTTP get req to the new service, the need to create a separate pkg for this isnt justified imo. The "logic to rustle up an iTwin Id" should be the responsibility of an App not iTwin.js Core or any specific client imo.

The "value add" is from a business perspective. Now, you do not need to pay for Cesium ION to take advantage of global Cesium Assets if you are already an iTwin Platform Customer. The issue is how do they take advantage of this, and it should be as simple as we expose a helper for Apps to use to populate and use in their app.

Maybe long term this could be extracted out into a separate package, or even an extension, but at this point in time for the current objectives it should be seen as out of scope.

Copy link
Member

Choose a reason for hiding this comment

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

it should be as simple

Can you please make it look simple then?

import {
createCesiumTerrainProvider,
IModelApp,
type TerrainMeshProviderOptions,
type TerrainProvider,
} from "@itwin/core-frontend";

interface ITPReq {
accessToken: string;
baseUrl?: string;
}

interface CuratedCesiumContentOptions extends ITPReq {
assetId: number;
iTwinId: string;
}

function genHeaders(accessToken: string) {
return {
headers: {
/* eslint-disable @typescript-eslint/naming-convention */
Authorization: accessToken,
Accept: "application/vnd.bentley.itwin-platform.v1+json",
Prefer: "return=representation",
/* eslint-enable */
},
};
}

async function getCuratedCesiumContentProps({
assetId,
iTwinId,
accessToken,
baseUrl,
}: CuratedCesiumContentOptions): Promise<{ token?: string; url?: string }> {
const apiUrl = `${
baseUrl ?? "https://api.bentley.com"
}/curated-content/cesium/${assetId}/tiles?iTwinId=${iTwinId}`;

try {
const res = await fetch(apiUrl, genHeaders(accessToken));
if (!res.ok) {
return {};
}
const accessTileProps = (await res.json()) as {
accessToken: string;
url: string;
};
return { token: accessTileProps.accessToken, url: accessTileProps.url };
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return {};
}
}

// https://developer.bentley.com/apis/itwins/operations/get-my-primary-account/
async function getAccountITwin({
accessToken,
baseUrl,
}: ITPReq): Promise<string> {
const apiUrl = `${
baseUrl ?? "https://api.bentley.com"
}/itwins/myprimaryaccount`;

try {
const res = await fetch(apiUrl, genHeaders(accessToken));
if (!res.ok) {
return "";
}

const { iTwin } = (await res.json()) as { iTwin: { id: string } };
return iTwin.id;
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return "";
}
}

export async function registerCesiumCuratedContentProvider({
iTwinId,
baseUrl,
}: {
iTwinId?: string;
baseUrl?: string;
}): Promise<void> {
const providerName = "CuratedCesiumContent";

const provider: TerrainProvider = {
createTerrainMeshProvider: async (options: TerrainMeshProviderOptions) => {
const accessToken = await IModelApp.authorizationClient?.getAccessToken();
if (!accessToken) {
return undefined;
}

iTwinId = iTwinId ?? (await getAccountITwin({ accessToken, baseUrl }));
aruniverse marked this conversation as resolved.
Show resolved Hide resolved

const { token, url } = await getCuratedCesiumContentProps({
assetId: +(options.dataSource || CesiumTerrainAssetId.Default),
iTwinId,
accessToken,
baseUrl,
});
return createCesiumTerrainProvider({
...options,
accessToken: token,
url,
});
},
};

IModelApp.terrainProviderRegistry.register(providerName, provider);
}
7 changes: 6 additions & 1 deletion test-apps/display-test-app/src/frontend/ViewAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,12 @@ export class ViewAttributes {
const mapSettings = this.addMapSettings();

const enableTerrain = (enable: boolean) => {
this.updateBackgroundMap({ applyTerrain: enable });
this.updateBackgroundMap({
terrainSettings: {
providerName: "CuratedCesiumContent",
aruniverse marked this conversation as resolved.
Show resolved Hide resolved
},
applyTerrain: enable,
});
terrainSettings.style.display = enable ? "block" : "none";
mapSettings.style.display = enable ? "none" : "block";
this.sync();
Expand Down
Loading