UNPKG

@itwin/core-frontend

Version:
453 lines • 24.2 kB
/*--------------------------------------------------------------------------------------------- * 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