@itwin/core-frontend
Version:
iTwin.js frontend components
453 lines • 24.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module TileTreeSupplier
*/
import { assert, BeTimePoint, compareStringsOrUndefined, expectDefined, Id64 } from "@itwin/core-bentley";
import { BatchType, Cartographic, ColorDef, Feature, FeatureTable, Frustum, FrustumPlanes, GeoCoordStatus, PackedFeatureTable, QParams3d, Quantization, RealityDataFormat, RealityDataProvider, } from "@itwin/core-common";
import { Point3d, Range3d, Transform, Vector3d } from "@itwin/core-geometry";
import { CRSManager, Downloader, DownloaderXhr, OnlineEngine, OPCReader, OrbitGtAList, OrbitGtBounds, OrbitGtCoordinate, OrbitGtDataManager, OrbitGtFrameData, OrbitGtIViewRequest, OrbitGtTileLoadSorter, PageCachedFile, PointDataRaw, UrlFS, } from "@itwin/core-orbitgt";
import { calculateEcefToDbTransformAtLocation } from "../../BackgroundMapGeometry";
import { DisplayStyleState } from "../../DisplayStyleState";
import { IModelApp } from "../../IModelApp";
import { RealityDataSource } from "../../RealityDataSource";
import { Mesh } from "../../common/internal/render/MeshPrimitives";
import { LayerTileTreeHandler, RealityModelTileTree, Tile, TileLoadPriority, TileTree, TileUsageMarker, } from "../../tile/internal";
const scratchRange = Range3d.create();
const scratchWorldFrustum = new Frustum();
function compareSourceKeys(lhs, rhs) {
return compareStringsOrUndefined(lhs.id, rhs.id) || compareStringsOrUndefined(lhs.format, rhs.format) || compareStringsOrUndefined(lhs.iTwinId, rhs.iTwinId);
}
class OrbitGtTreeSupplier {
getOwner(treeId, iModel) {
return iModel.tiles.getTileTreeOwner(treeId, this);
}
async createTileTree(treeId, iModel) {
return OrbitGtTileTree.createOrbitGtTileTree(treeId.rdSourceKey, iModel, treeId.modelId);
}
compareTileTreeIds(lhs, rhs) {
return compareStringsOrUndefined(lhs.modelId, rhs.modelId) || compareSourceKeys(lhs.rdSourceKey, rhs.rdSourceKey);
}
findCompatibleContextRealityModelId(sourceKey, style) {
const owners = style.iModel.tiles.getTreeOwnersForSupplier(this);
for (const owner of owners) {
// Find an existing tree with the same reality data source key.
if (0 === compareSourceKeys(sourceKey, owner.id.rdSourceKey)) {
const modelId = owner.id.modelId;
assert(undefined !== modelId);
// If the model Id is unused by any other context reality model in the view and does not identify a persistent reality model, use it.
if (Id64.isTransientId64(modelId) && !style.contextRealityModelStates.some((model) => model.modelId === modelId))
return modelId;
}
}
return undefined;
}
}
const orbitGtTreeSupplier = new OrbitGtTreeSupplier();
function transformFromOrbitGt(ogtTransform, result) {
if (undefined === result)
result = Transform.createIdentity();
result.matrix.setRowValues(ogtTransform.getElement(0, 0), ogtTransform.getElement(0, 1), ogtTransform.getElement(0, 2), ogtTransform.getElement(1, 0), ogtTransform.getElement(1, 1), ogtTransform.getElement(1, 2), ogtTransform.getElement(2, 0), ogtTransform.getElement(2, 1), ogtTransform.getElement(2, 2));
result.origin.x = ogtTransform.getElement(0, 3);
result.origin.y = ogtTransform.getElement(1, 3);
result.origin.z = ogtTransform.getElement(2, 3);
return result;
}
function pointFromOrbitGt(ogtCoordinate, result) {
if (undefined === result)
result = Point3d.create();
result.x = ogtCoordinate.x;
result.y = ogtCoordinate.y;
result.z = ogtCoordinate.z;
return result;
}
function rangeFromOrbitGt(ogtBounds, result) {
if (undefined === result)
result = Range3d.create();
pointFromOrbitGt(ogtBounds.min, result.low);
pointFromOrbitGt(ogtBounds.max, result.high);
return result;
}
export function createOrbitGtTileTreeReference(props) {
return new OrbitGtTreeReference(props);
}
class OrbitGtTileTreeParams {
location;
id;
modelId;
iModel;
get priority() { return TileLoadPriority.Context; }
constructor(rdSourceKey, iModel, modelId, location) {
this.location = location;
const key = rdSourceKey;
this.id = `${key.provider}:${key.format}:${key.id}:${key.iTwinId}`;
this.modelId = modelId;
this.iModel = iModel;
}
}
class OrbitGtRootTile extends Tile {
_loadChildren(_resolve, _reject) { }
async requestContent(_isCanceled) { return undefined; }
get channel() { return IModelApp.tileAdmin.channels.getForHttp("itwinjs-orbitgit"); }
async readContent(_data, _system, _isCanceled) { return {}; }
freeMemory() { }
constructor(params, tree) { super(params, tree); }
}
class OrbitGtViewRequest extends OrbitGtIViewRequest {
_tileDrawArgs;
_centerOffset;
_tileToIModelTransform;
constructor(_tileDrawArgs, _centerOffset) {
super();
this._tileDrawArgs = _tileDrawArgs;
this._centerOffset = _centerOffset;
this._tileToIModelTransform = _tileDrawArgs.location.multiplyTransformTransform(Transform.createTranslation(_centerOffset));
}
isVisibleBox(bounds) {
const box = Frustum.fromRange(rangeFromOrbitGt(bounds, scratchRange));
const worldBox = box.transformBy(this._tileToIModelTransform, scratchWorldFrustum);
return FrustumPlanes.Containment.Outside !== this._tileDrawArgs.frustumPlanes.computeFrustumContainment(worldBox, undefined);
}
getFrameTime() {
return this._tileDrawArgs.now.milliseconds;
}
shouldSplit(level, tile) {
// get the world size of the tile voxels
const tileCenter = level.getTileGrid().getCellCenter(tile.gridIndex);
tileCenter.x += this._centerOffset.x;
tileCenter.y += this._centerOffset.y;
tileCenter.z += this._centerOffset.z;
const worldCenter = this._tileDrawArgs.location.multiplyXYZ(tileCenter.x, tileCenter.y, tileCenter.z);
const worldCenter2 = this._tileDrawArgs.location.multiplyXYZ(tileCenter.x, tileCenter.y, tileCenter.z + level.getTileGrid().size.z);
const voxelSize = worldCenter2.distance(worldCenter) / 64;
// get the world size of a screen pixel at the tile center
const viewPt = this._tileDrawArgs.worldToViewMap.transform0.multiplyPoint3dQuietNormalize(worldCenter);
const viewPt2 = new Point3d(viewPt.x + 1.0, viewPt.y, viewPt.z);
const pixelSizeAtCenter = this._tileDrawArgs.worldToViewMap.transform1.multiplyPoint3dQuietNormalize(viewPt).distance(this._tileDrawArgs.worldToViewMap.transform1.multiplyPoint3dQuietNormalize(viewPt2));
// stop splitting if the voxel size of the children becomes too small to improve quality
const split = (0.5 * voxelSize > 2.0 * pixelSizeAtCenter);
return split;
}
}
class TileSortProjector {
_sortTransform;
constructor(iModelTransform, viewingSpace, centerOffset) {
const rotation = viewingSpace.rotation;
let origin;
if (undefined === viewingSpace.eyePoint) {
origin = Vector3d.createFrom(viewingSpace.viewOrigin);
const viewDelta = viewingSpace.viewDelta;
const eyeDelta = Vector3d.createFrom({ x: viewDelta.x / 2, y: viewDelta.y / 2, z: viewDelta.z * 10 });
rotation.multiplyVector(eyeDelta, eyeDelta);
origin.addInPlace(eyeDelta);
}
else {
origin = Vector3d.createFrom(viewingSpace.eyePoint);
}
rotation.multiplyVector(origin);
origin.scaleInPlace(-1);
const toViewTransform = Transform.createOriginAndMatrix(origin, rotation);
const tileToIModelTransform = iModelTransform.multiplyTransformTransform(Transform.createTranslation(centerOffset));
this._sortTransform = toViewTransform.multiplyTransformTransform(tileToIModelTransform);
}
projectToViewForSort(coordinate) {
const point = pointFromOrbitGt(coordinate);
this._sortTransform.multiplyPoint3d(point, point);
coordinate.x = point.x;
coordinate.y = point.y;
coordinate.z = point.z;
}
}
class OrbitGtTileGraphic extends TileUsageMarker {
graphic;
constructor(graphic, viewport, time) {
super();
this.graphic = graphic;
this.mark(viewport, time);
}
[Symbol.dispose]() {
this.graphic[Symbol.dispose]();
}
}
export class OrbitGtTileTree extends TileTree {
_dataManager;
_centerOffset;
_ecefTransform;
_tileParams;
rootTile;
viewFlagOverrides = {};
_tileGraphics = new Map();
_layerHandler;
layerImageryTrees = [];
get layerHandler() { return this._layerHandler; }
constructor(treeParams, _dataManager, cloudRange, _centerOffset, _ecefTransform) {
super(treeParams);
this._dataManager = _dataManager;
this._centerOffset = _centerOffset;
this._ecefTransform = _ecefTransform;
this._layerHandler = new LayerTileTreeHandler(this);
this._tileParams = { contentId: "0", range: cloudRange, maximumSize: 256 };
this.rootTile = new OrbitGtRootTile(this._tileParams, this);
}
async getEcefTransform() {
return this._ecefTransform;
}
[Symbol.dispose]() {
if (this.isDisposed)
return;
for (const graphic of this._tileGraphics.values())
graphic[Symbol.dispose]();
this._tileGraphics.clear();
super[Symbol.dispose]();
}
_selectTiles(_args) { return []; }
get is3d() { return true; }
get isContentUnbounded() { return false; }
get maxDepth() { return undefined; }
_doPrune(olderThan) {
for (const [key, graphic] of this._tileGraphics)
if (graphic.isExpired(olderThan)) {
graphic[Symbol.dispose]();
this._tileGraphics.delete(key);
}
}
prune() {
const olderThan = BeTimePoint.now().minus(this.expirationTime);
this._doPrune(olderThan);
}
collectStatistics(stats) {
for (const tileGraphic of this._tileGraphics)
tileGraphic[1].graphic.collectStatistics(stats);
}
draw(args) {
const debugControl = args.context.target.debugControl;
const debugBuilder = (debugControl && debugControl.displayRealityTileRanges) ? args.context.createSceneGraphicBuilder() : undefined;
const doLogging = (debugControl && debugControl.logRealityTiles);
const viewRequest = new OrbitGtViewRequest(args, this._centerOffset);
const levelsInView = new OrbitGtAList();
const blocksInView = new OrbitGtAList();
const tilesInView = new OrbitGtAList();
const frameData = new OrbitGtFrameData();
this._dataManager.getViewTree().renderView3D(viewRequest, levelsInView, blocksInView, tilesInView, frameData.tilesToRender);
this._dataManager.filterLoadList(levelsInView, blocksInView, tilesInView, frameData.levelsToLoad, frameData.blocksToLoad, frameData.tilesToLoad);
tilesInView.sort(new OrbitGtTileLoadSorter(this._dataManager.getViewTree(), new TileSortProjector(this.iModelTransform, args.context.viewingSpace, this._centerOffset)));
let totalPointCount = 0;
const tileCount = frameData.tilesToRender.size();
// Inform TileAdmin about tiles we are handling ourselves...
IModelApp.tileAdmin.addExternalTilesForUser(args.context.viewport, { requested: frameData.tilesToLoad.size() + (frameData.hasMissingData() ? 1 : 0), selected: tileCount, ready: tileCount });
if (debugBuilder)
debugBuilder.setSymbology(ColorDef.red, ColorDef.red, 1);
let minLevel = 100, maxLevel = -100;
for (let t = 0; t < tileCount; t++) {
const tile = frameData.tilesToRender.get(t);
minLevel = Math.min(minLevel, tile.tileIndex.level);
maxLevel = Math.max(maxLevel, tile.tileIndex.level);
totalPointCount += tile.tileIndex.pointCount;
const key = tile.tileIndex.key;
const cachedGraphic = this._tileGraphics.get(key);
if (undefined !== cachedGraphic) {
cachedGraphic.mark(args.context.viewport, args.now);
args.graphics.add(cachedGraphic.graphic);
}
else {
const range = rangeFromOrbitGt(tile.bounds);
range.low.addInPlace(this._centerOffset);
range.high.addInPlace(this._centerOffset);
const qParams = QParams3d.fromRange(range, undefined, (tile.points8 != null) ? Quantization.rangeScale8 : Quantization.rangeScale16);
const featureTable = new FeatureTable(1, this.modelId, BatchType.Primary);
const features = new Mesh.Features(featureTable);
const system = IModelApp.renderSystem;
const voxelSize = (range.high.x - range.low.x) / 64;
features.add(new Feature(this.modelId), 1);
const tilePoints = (tile.points8 != null) ? tile.points8.toNativeBuffer() : tile.points16.toNativeBuffer();
let renderGraphic = system.createPointCloud({
positions: tilePoints,
qparams: qParams,
colors: tile.colors.toNativeBuffer(),
features: features.toFeatureIndex(),
voxelSize,
colorFormat: "bgr",
}, this.iModel);
renderGraphic = system.createBatch(expectDefined(renderGraphic), PackedFeatureTable.pack(featureTable), range);
args.graphics.add(renderGraphic);
this._tileGraphics.set(key, new OrbitGtTileGraphic(renderGraphic, args.context.viewport, args.now));
}
if (debugBuilder)
debugBuilder.addRangeBox(rangeFromOrbitGt(tile.bounds));
}
if (debugBuilder)
args.graphics.add(debugBuilder.finish());
if (doLogging) {
// eslint-disable-next-line no-console
console.log(`Total OrbitGtTiles: ${tileCount} MinLevel: ${minLevel} MaxLevel: ${maxLevel} Total Points: ${totalPointCount}`);
}
args.drawGraphics();
if (frameData.hasMissingData()) {
this._dataManager.loadData(frameData).then(() => IModelApp.tileAdmin.onTileLoad.raiseEvent(this.rootTile)).catch((_err) => undefined);
}
}
}
(function (OrbitGtTileTree) {
function isValidSASToken(downloadUrl) {
// Create fake URL for and parameter parsing and SAS token URI parsing
if (!downloadUrl.startsWith("http"))
downloadUrl = `http://x.com/x?${downloadUrl}`;
const sasUrl = new URL(downloadUrl);
const se = sasUrl.searchParams.get("se");
if (se) {
const expiryUTC = new Date(se);
const now = new Date();
const currentUTC = new Date(now?.toUTCString());
return expiryUTC >= currentUTC;
}
return false;
}
function isValidOrbitGtBlobProps(props) {
// Check main OrbitGtBlobProps fields are defined
if (!props.accountName || !props.containerName || !props.blobFileName || !props.sasToken)
return false;
// Check SAS token is valid
return isValidSASToken(props.sasToken);
}
async function createOrbitGtTileTree(rdSourceKey, iModel, modelId) {
const rdSource = await RealityDataSource.fromKey(rdSourceKey, iModel.iTwinId);
const isContextShare = rdSourceKey.provider === RealityDataProvider.ContextShare;
const isTilestUrl = rdSourceKey.provider === RealityDataProvider.TilesetUrl;
let blobStringUrl;
if (isContextShare) {
const realityData = rdSource ? rdSource.realityData : undefined;
if (rdSource === undefined || realityData === undefined)
return undefined;
const docRootName = realityData.rootDocument;
if (!docRootName)
return undefined;
const token = await IModelApp.getAccessToken();
const blobUrl = await realityData.getBlobUrl(token, docRootName);
blobStringUrl = blobUrl.toString();
}
else if (isTilestUrl) {
blobStringUrl = rdSourceKey.id;
}
else {
const orbitGtBlobProps = RealityDataSource.createOrbitGtBlobPropsFromKey(rdSourceKey);
if (orbitGtBlobProps === undefined)
return undefined;
if (!isValidOrbitGtBlobProps(orbitGtBlobProps))
return undefined;
const { accountName, containerName, blobFileName, sasToken } = orbitGtBlobProps;
blobStringUrl = blobFileName;
if (accountName.length > 0)
blobStringUrl = UrlFS.getAzureBlobSasUrl(accountName, containerName, blobFileName, sasToken);
}
if (Downloader.INSTANCE == null)
Downloader.INSTANCE = new DownloaderXhr();
if (CRSManager.ENGINE == null)
CRSManager.ENGINE = await OnlineEngine.create();
// wrap a caching layer (16 MB) around the blob file
const urlFS = new UrlFS();
const blobFileSize = await urlFS.getFileLength(blobStringUrl);
const cacheKilobytes = 128;
const cachedBlobFile = new PageCachedFile(urlFS, blobStringUrl, blobFileSize, cacheKilobytes * 1024 /* pageSize*/, 128 /* maxPageCount*/);
const pointCloudReader = await OPCReader.openFile(cachedBlobFile, blobStringUrl, true /* lazyLoading*/);
let pointCloudCRS = pointCloudReader.getFileCRS();
if (pointCloudCRS == null)
pointCloudCRS = "";
const dataManager = new OrbitGtDataManager(pointCloudReader, pointCloudCRS, PointDataRaw.TYPE);
const pointCloudBounds = dataManager.getPointCloudBounds();
const pointCloudRange = rangeFromOrbitGt(pointCloudBounds);
const pointCloudCenter = expectDefined(pointCloudRange.localXYZToWorld(.5, .5, .5));
const addCloudCenter = Transform.createTranslation(pointCloudCenter);
const ecefTransform = Transform.createIdentity();
let pointCloudCenterToDb = addCloudCenter;
if (pointCloudCRS.length > 0) {
await CRSManager.ENGINE.prepareForArea(pointCloudCRS, pointCloudBounds);
const wgs84CRS = "4978";
await CRSManager.ENGINE.prepareForArea(wgs84CRS, new OrbitGtBounds());
const pointCloudToEcef = transformFromOrbitGt(CRSManager.createTransform(pointCloudCRS, new OrbitGtCoordinate(pointCloudCenter.x, pointCloudCenter.y, pointCloudCenter.z), wgs84CRS));
const pointCloudCenterToEcef = pointCloudToEcef.multiplyTransformTransform(addCloudCenter);
ecefTransform.setFrom(pointCloudCenterToEcef);
let ecefToDb = iModel.getMapEcefToDb(0);
// In initial publishing version the iModel ecef Transform was used to locate the reality model.
// This would work well only for tilesets published from that iModel but for iModels the ecef transform is calculated
// at the center of the project extents and the reality model location may differ greatly, and the curvature of the earth
// could introduce significant errors.
// The publishing was modified to calculate the ecef transform at the reality model range center and at the same time the "iModelPublishVersion"
// member was added to the root object.
const ecefOrigin = pointCloudCenterToEcef.getOrigin();
const dbOrigin = ecefToDb.multiplyPoint3d(ecefOrigin);
const realityOriginToProjectDistance = iModel.projectExtents.distanceToPoint(dbOrigin);
const maxProjectDistance = 1E5; // Only use the project GCS projection if within 100KM of the project. Don't attempt to use GCS if global reality model or in another locale - Results will be unreliable.
if (realityOriginToProjectDistance < maxProjectDistance) {
const cartographicOrigin = Cartographic.fromEcef(ecefOrigin);
const geoConverter = iModel.noGcsDefined ? undefined : iModel.geoServices.getConverter("WGS84");
if (cartographicOrigin !== undefined && geoConverter !== undefined) {
const geoOrigin = Point3d.create(cartographicOrigin.longitudeDegrees, cartographicOrigin.latitudeDegrees, cartographicOrigin.height);
const response = await geoConverter.getIModelCoordinatesFromGeoCoordinates([geoOrigin]);
if (response.iModelCoords[0].s === GeoCoordStatus.Success) {
const ecefToDbOrigin = await calculateEcefToDbTransformAtLocation(Point3d.fromJSON(response.iModelCoords[0].p), iModel);
if (ecefToDbOrigin)
ecefToDb = ecefToDbOrigin;
}
}
}
pointCloudCenterToDb = ecefToDb.multiplyTransformTransform(pointCloudCenterToEcef);
}
const params = new OrbitGtTileTreeParams(rdSourceKey, iModel, modelId, pointCloudCenterToDb);
// We use a RTC transform to avoid jitter from large cloud coordinates.
const centerOffset = Vector3d.create(-pointCloudCenter.x, -pointCloudCenter.y, -pointCloudCenter.z);
pointCloudRange.low.addInPlace(centerOffset);
pointCloudRange.high.addInPlace(centerOffset);
return new OrbitGtTileTree(params, dataManager, pointCloudRange, centerOffset, ecefTransform);
}
OrbitGtTileTree.createOrbitGtTileTree = createOrbitGtTileTree;
})(OrbitGtTileTree || (OrbitGtTileTree = {}));
/** Supplies a reality data [[TileTree]] from a URL. May be associated with a persistent [[GeometricModelState]], or attached at run-time via a [[ContextOrbitGtState]].
* Exported strictly for tests.
*/
export class OrbitGtTreeReference extends RealityModelTileTree.Reference {
treeOwner;
_rdSourceKey;
_modelId;
get castsShadows() { return false; }
get modelId() { return this._modelId; }
constructor(props) {
super(props);
// Create rdSourceKey if not provided
if (props.rdSourceKey) {
this._rdSourceKey = props.rdSourceKey;
}
else if (props.orbitGtBlob) {
this._rdSourceKey = RealityDataSource.createKeyFromOrbitGtBlobProps(props.orbitGtBlob);
}
else {
// TODO: Maybe we should throw an exception
this._rdSourceKey = RealityDataSource.createKeyFromBlobUrl("", RealityDataProvider.OrbitGtBlob, RealityDataFormat.OPC);
}
// ###TODO find compatible model Id
let modelId = props.modelId;
if (undefined === modelId && this._source instanceof DisplayStyleState)
modelId = orbitGtTreeSupplier.findCompatibleContextRealityModelId(this._rdSourceKey, this._source);
this._modelId = modelId ?? props.iModel.transientIds.getNext();
const ogtTreeId = { rdSourceKey: this._rdSourceKey, modelId: this.modelId };
this.treeOwner = orbitGtTreeSupplier.getOwner(ogtTreeId, props.iModel);
}
canSupplyToolTip(hit) {
const tree = this.treeOwner.tileTree;
return undefined !== tree && hit.iModel === tree.iModel;
}
async getToolTip(hit) {
const tree = this.treeOwner.tileTree;
if (undefined === tree || hit.iModel !== tree.iModel)
return undefined;
const strings = [];
strings.push(IModelApp.localization.getLocalizedString("iModelJs:RealityModelTypes.OrbitGTPointCloud"));
if (this._name)
strings.push(`${IModelApp.localization.getLocalizedString("iModelJs:TooltipInfo.Name")} ${this._name}`);
const div = document.createElement("div");
div.innerHTML = strings.join("<br>");
return div;
}
}
//# sourceMappingURL=OrbitGtTileTree.js.map