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

Obtain Base Url for Texture Requests within Reality Tile Loader #7450

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
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
9 changes: 9 additions & 0 deletions common/api/core-frontend.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2382,6 +2382,9 @@ export function createOrbitGtTileTreeReference(props: OrbitGtTileTree.ReferenceP
// @internal (undocumented)
export function createPrimaryTileTreeReference(view: ViewState, model: GeometricModelState): PrimaryTreeReference;

// @internal
export function createReaderPropsWithBaseUrl(streamBuffer: ByteStream | Uint8Array, yAxisUp: boolean, baseUrl?: string): GltfReaderProps | undefined;

// @internal (undocumented)
export function createRealityTileTreeReference(props: RealityModelTileTree.ReferenceProps): RealityModelTileTree.Reference;

Expand Down Expand Up @@ -9097,6 +9100,8 @@ export namespace RealityDataSource {
export function createOrbitGtBlobPropsFromKey(rdSourceKey: RealityDataSourceKey): OrbitGtBlobProps | undefined;
// @alpha
export function fromKey(key: RealityDataSourceKey, iTwinId: GuidString | undefined): Promise<RealityDataSource | undefined>;
// @internal
export function getTilesetUrlFromTilesetUrlImpl(rdSource: RealityDataSource): string | undefined;
}

// @alpha
Expand Down Expand Up @@ -9525,6 +9530,8 @@ export class RealityTileRegion {
export class RealityTileTree extends TileTree {
// @internal
constructor(params: RealityTileTreeParams);
// @internal (undocumented)
readonly baseUrl?: string;
// @beta
get batchTableProperties(): BatchTableProperties | undefined;
// @internal (undocumented)
Expand Down Expand Up @@ -9591,6 +9598,8 @@ export class RealityTileTree extends TileTree {

// @internal (undocumented)
export interface RealityTileTreeParams extends TileTreeParams {
// (undocumented)
readonly baseUrl?: string;
// (undocumented)
readonly gcsConverterAvailable: boolean;
// (undocumented)
Expand Down
2 changes: 2 additions & 0 deletions common/api/summary/core-frontend.exports.csv
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ internal;function;createMaskTreeReference
internal;function;createModelMapLayerTileTreeReference
internal;function;createOrbitGtTileTreeReference
internal;function;createPrimaryTileTreeReference
internal;function;createReaderPropsWithBaseUrl
internal;function;createRealityTileTreeReference
beta;interface;CreateRenderInstancesParamsBuilderArgs
public;interface;CreateRenderMaterialArgs
Expand Down Expand Up @@ -594,6 +595,7 @@ alpha;function;createKeyFromBlobUrl
alpha;function;createKeyFromOrbitGtBlobProps
alpha;function;createOrbitGtBlobPropsFromKey
alpha;function;fromKey
internal;function;getTilesetUrlFromTilesetUrlImpl
alpha;interface;RealityDataSourceProvider
alpha;class;RealityDataSourceProviderRegistry
internal;interface;RealityMeshGraphicParams
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@itwin/core-frontend",
"comment": "",
"type": "none"
}
],
"packageName": "@itwin/core-frontend"
}
7 changes: 7 additions & 0 deletions core/frontend/src/RealityDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ export namespace RealityDataSource {

return provider.createRealityDataSource(key, iTwinId);
}

/** Returns the source's tileset url if the source is an instance of RealityDataSourceTilesetUrlImpl.
* @internal
*/
export function getTilesetUrlFromTilesetUrlImpl(rdSource: RealityDataSource): string | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

Delete this, add an optional method on the RealityDataSource interface, and implement it in RealityDataSourceTilesetUrlImpl. That will allow any other implementations of RealityDataSource to implement it themselves if they so choose.

return (rdSource instanceof RealityDataSourceTilesetUrlImpl) ? rdSource.tilesetUrl : undefined;
};
}

/** A named supplier of [RealityDataSource]]s.
Expand Down
3 changes: 3 additions & 0 deletions core/frontend/src/RealityDataSourceTilesetUrlImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export class RealityDataSourceTilesetUrlImpl implements RealityDataSource {
return undefined;
}

public get tilesetUrl(): string | undefined {
return this._tilesetUrl;
}
// This is to set the root url from the provided root document path.
// If the root document is stored on PW Context Share then the root document property of the Reality Data is provided,
// otherwise the full path to root document is given.
Expand Down
96 changes: 96 additions & 0 deletions core/frontend/src/test/tile/RealityTileLoader.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { describe, expect, it } from "vitest";
import { createReaderPropsWithBaseUrl } from "../../tile/RealityTileLoader";
import { GltfV2ChunkTypes, GltfVersions, TileFormat } from "@itwin/core-common";

const minimalBin = new Uint8Array([12, 34, 0xfe, 0xdc]);
const minimalJson = { asset: { version: "02.00" }, meshes: [] };

function jsonToBytes(json: object, alignment = 4): Uint8Array {
let str = JSON.stringify(json);
while (str.length % alignment !== 0)
str += " ";

const bytes = new TextEncoder().encode(str);
expect(bytes.length).toEqual(str.length); // pure ASCII
return bytes;
}

function setHeader(data: Uint8Array | DataView, length: number, format = TileFormat.Gltf, version = GltfVersions.Version2): void {
if (data instanceof Uint8Array)
data = new DataView(data.buffer);

data.setUint32(0, format, true);
data.setUint32(4, version, true);
data.setUint32(8, length, true);
}

interface Chunk {
len?: number;
type: number;
data: Uint8Array;
}

interface Header {
len?: number;
format: number;
version: number;
}
function glbFromChunks(chunks: Chunk[], header?: Header): Uint8Array {
let numBytes = 12;
for (const chunk of chunks)
numBytes += 8 + (chunk.len ?? chunk.data.length);

const glb = new Uint8Array(numBytes);
const view = new DataView(glb.buffer);

header = header ?? { format: TileFormat.Gltf, version: GltfVersions.Version2 };
setHeader(view, header.len ?? numBytes, header.format, header.version);

let chunkStart = 12;
for (const chunk of chunks) {
view.setUint32(chunkStart + 0, chunk.len ?? chunk.data.length, true);
view.setUint32(chunkStart + 4, chunk.type, true);
glb.set(chunk.data, chunkStart + 8);
chunkStart += chunk.data.length + 8;
}

return glb;
}

function makeGlb(json: object | undefined, binary?: Uint8Array, header?: Header): Uint8Array {
const chunks = [];
if (json)
chunks.push({ type: GltfV2ChunkTypes.JSON, data: jsonToBytes(json) });

if (binary)
chunks.push({ type: GltfV2ChunkTypes.Binary, data: binary });

return glbFromChunks(chunks, header);
}

describe("createReaderPropsWithBaseUrl", () => {
Copy link
Member

Choose a reason for hiding this comment

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

These tests don't prove much. A much more valuable test would confirm that you successfully resolve a resource inside the glTF with a relative URL where prior to your change you would have failed to do so.

const glb = makeGlb(minimalJson, minimalBin);
it("should add a valid base url to the reader props", {}, () => {
let props = createReaderPropsWithBaseUrl(glb, false, "http://localhost:8080/tileset.json");
expect(props?.baseUrl?.toString()).to.equal("http://localhost:8080/tileset.json");

props = createReaderPropsWithBaseUrl(glb, false, "https://some-blob-storage.com/tileset.json");
expect(props?.baseUrl?.toString()).to.equal("https://some-blob-storage.com/tileset.json");

props = createReaderPropsWithBaseUrl(glb, false, "https://some-blob-storage.com/tileset.json?with-some-query-params");
expect(props?.baseUrl?.toString()).to.equal("https://some-blob-storage.com/tileset.json?with-some-query-params");
});

it("should not add an invalid base url to the reader props", {}, () => {
let props = createReaderPropsWithBaseUrl(glb, false, "");
expect(props?.baseUrl).to.be.undefined;

props = createReaderPropsWithBaseUrl(glb, false, "some-invalid-url");
expect(props?.baseUrl).to.be.undefined;
});
});

8 changes: 6 additions & 2 deletions core/frontend/src/tile/RealityModelTileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,13 @@ class RealityModelTileTreeParams implements RealityTileTreeParams {
public is3d = true;
public loader: RealityModelTileLoader;
public rootTile: RealityTileParams;
public baseUrl?: string;

public get location() { return this.loader.tree.location; }
public get yAxisUp() { return this.loader.tree.yAxisUp; }
public get priority() { return this.loader.priority; }

public constructor(tileTreeId: string, iModel: IModelConnection, modelId: Id64String, loader: RealityModelTileLoader, public readonly gcsConverterAvailable: boolean, public readonly rootToEcef: Transform | undefined) {
public constructor(tileTreeId: string, iModel: IModelConnection, modelId: Id64String, loader: RealityModelTileLoader, public readonly gcsConverterAvailable: boolean, public readonly rootToEcef: Transform | undefined, baseUrl?: string) {
this.loader = loader;
this.id = tileTreeId;
this.modelId = modelId;
Expand All @@ -287,6 +288,7 @@ class RealityModelTileTreeParams implements RealityTileTreeParams {
additiveRefinement: undefined !== refine ? "ADD" === refine : undefined,
usesGeometricError: loader.tree.rdSource.usesGeometricError,
});
this.baseUrl = baseUrl;
}
}

Expand Down Expand Up @@ -711,7 +713,9 @@ export namespace RealityModelTileTree {
const props = await getTileTreeProps(rdSource, tilesetToDb, iModel);
const loader = new RealityModelTileLoader(props, new BatchedTileIdMap(iModel), opts);
const gcsConverterAvailable = await getGcsConverterAvailable(iModel);
const params = new RealityModelTileTreeParams(tileTreeId, iModel, modelId, loader, gcsConverterAvailable, props.tilesetToEcef);
//The full tileset url is needed so that it includes the url's search parameters if any are present
const baseUrl = RealityDataSource.getTilesetUrlFromTilesetUrlImpl(rdSource);
const params = new RealityModelTileTreeParams(tileTreeId, iModel, modelId, loader, gcsConverterAvailable, props.tilesetToEcef, baseUrl);
return new RealityModelTileTree(params);
}
return undefined;
Expand Down
27 changes: 24 additions & 3 deletions core/frontend/src/tile/RealityTileLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ScreenViewport, Viewport } from "../Viewport";
import { GltfWrapMode } from "../common/gltf/GltfSchema";
import {
B3dmReader, BatchedTileIdMap, createDefaultViewFlagOverrides, GltfGraphicsReader, GltfReader, GltfReaderProps, I3dmReader, ImdlReader, readPointCloudTileContent,
RealityTile, RealityTileContent, Tile, TileContent, TileDrawArgs, TileLoadPriority, TileRequest, TileRequestChannel, TileUser,
RealityTile, RealityTileContent, RealityTileTree, Tile, TileContent, TileDrawArgs, TileLoadPriority, TileRequest, TileRequestChannel, TileUser,
} from "./internal";

const defaultViewFlagOverrides = createDefaultViewFlagOverrides({});
Expand Down Expand Up @@ -143,7 +143,9 @@ export abstract class RealityTileLoader {
reader = I3dmReader.create(streamBuffer, iModel, modelId, is3d, tile.contentRange, system, yAxisUp, tile.isLeaf, isCanceled, undefined, this.wantDeduplicatedVertices);
break;
case TileFormat.Gltf:
const props = GltfReaderProps.create(streamBuffer.nextBytes(streamBuffer.arrayBuffer.byteLength), yAxisUp);
const tree = tile.tree as RealityTileTree;
const baseUrl = tree.baseUrl;
const props = createReaderPropsWithBaseUrl(streamBuffer, yAxisUp, baseUrl);
if (props) {
reader = new GltfGraphicsReader(props, {
iModel,
Expand All @@ -155,7 +157,6 @@ export abstract class RealityTileLoader {
idMap: this.getBatchIdMap(),
});
}

break;
case TileFormat.Cmpt:
const header = new CompositeTileHeader(streamBuffer);
Expand Down Expand Up @@ -242,3 +243,23 @@ export abstract class RealityTileLoader {
return minDistance;
}
}

/** Exposed strictly for testing purposes.
* @internal
*/
export function createReaderPropsWithBaseUrl(streamBuffer: ByteStream | Uint8Array, yAxisUp: boolean, baseUrl?: string): GltfReaderProps | undefined {
if(streamBuffer instanceof ByteStream) {
return (!baseUrl || !isValidURL(baseUrl)) ? GltfReaderProps.create(streamBuffer.nextBytes(streamBuffer.arrayBuffer.byteLength), yAxisUp) : GltfReaderProps.create(streamBuffer.nextBytes(streamBuffer.arrayBuffer.byteLength), yAxisUp, new URL(baseUrl));
}
return (!baseUrl || !isValidURL(baseUrl)) ? GltfReaderProps.create(streamBuffer, yAxisUp) : GltfReaderProps.create(streamBuffer, yAxisUp, new URL(baseUrl));

}

function isValidURL(url: string){
try {
new URL(url);
} catch {
return false;
}
return true;
}
andremig-bentley marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 4 additions & 0 deletions core/frontend/src/tile/RealityTileTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export interface RealityTileTreeParams extends TileTreeParams {
readonly rootTile: RealityTileParams;
readonly rootToEcef?: Transform;
readonly gcsConverterAvailable: boolean;
readonly baseUrl?: string;
}

/** Base class for a [[TileTree]] representing a reality model (e.g., a point cloud or photogrammetry mesh) or 3d terrain with map imagery.
Expand All @@ -189,6 +190,8 @@ export class RealityTileTree extends TileTree {
protected _rootToEcef?: Transform;
/** @internal */
protected _ecefToDb?: Transform;
/** @internal */
public readonly baseUrl?: string;

/** @internal */
public constructor(params: RealityTileTreeParams) {
Expand All @@ -207,6 +210,7 @@ export class RealityTileTree extends TileTree {
this._ecefToDb = dbToEcef.inverse();
}
}
this.baseUrl = params.baseUrl;
}

/** The mapping of per-feature JSON properties from this tile tree's batch table, if one is defined.
Expand Down
Loading