UNPKG

@itwin/core-frontend

Version:
930 lines • 50.7 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 Tiles */ import { assert, compareBooleans, compareBooleansOrUndefined, compareNumbers, compareStrings, compareStringsOrUndefined, CompressedId64Set, expectDefined } from "@itwin/core-bentley"; import { Cartographic, FeatureAppearance, GeoCoordStatus, GlobeMode, ModelMapLayerDrapeTarget, ModelMapLayerSettings, PlanarClipMaskPriority, TerrainHeightOriginMode, } from "@itwin/core-common"; import { Angle, AngleSweep, Constant, Ellipsoid, EllipsoidPatch, Point3d, Range1d, Range3d, Ray3d, Transform, Vector3d, } from "@itwin/core-geometry"; import { ApproximateTerrainHeights } from "../../ApproximateTerrainHeights"; import { IModelApp } from "../../IModelApp"; import { PlanarClipMaskState } from "../../PlanarClipMaskState"; import { FeatureSymbology } from "../../render/FeatureSymbology"; import { BingElevationProvider, createDefaultViewFlagOverrides, EllipsoidTerrainProvider, ImageryMapTileTree, LayerTileTreeHandler, LayerTileTreeReferenceHandler, MapCartoRectangle, MapLayerTileTreeReference, MapTile, MapTileLoader, PlanarTilePatch, QuadId, RealityTileDrawArgs, RealityTileTree, TileTreeReference, UpsampledMapTile, WebMercatorTilingScheme, } from "../internal"; const scratchPoint = Point3d.create(); const scratchCorners = [Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero()]; const scratchCorner = Point3d.createZero(); const scratchZNormal = Vector3d.create(0, 0, 1); /** Map tile tree scale range visibility values. * @beta */ export var MapTileTreeScaleRangeVisibility; (function (MapTileTreeScaleRangeVisibility) { /** state is currently unknown (i.e. never been displayed) */ MapTileTreeScaleRangeVisibility[MapTileTreeScaleRangeVisibility["Unknown"] = 0] = "Unknown"; /** all currently selected tree tiles are visible (i.e within the scale range) */ MapTileTreeScaleRangeVisibility[MapTileTreeScaleRangeVisibility["Visible"] = 1] = "Visible"; /** all currently selected tree tiles are hidden (i.e outside the scale range) */ MapTileTreeScaleRangeVisibility[MapTileTreeScaleRangeVisibility["Hidden"] = 2] = "Hidden"; /** currently selected tree tiles are partially visible (i.e some tiles are within the scale range, and some are outside.) */ MapTileTreeScaleRangeVisibility[MapTileTreeScaleRangeVisibility["Partial"] = 3] = "Partial"; })(MapTileTreeScaleRangeVisibility || (MapTileTreeScaleRangeVisibility = {})); /** A [quad tree](https://en.wikipedia.org/wiki/Quadtree) consisting of [[MapTile]]s representing the map imagery draped onto the surface of the Earth. * A `MapTileTree` enables display of a globe or planar map with [map imagery](https://en.wikipedia.org/wiki/Tiled_web_map) obtained from any number of sources, such as * [Bing](https://learn.microsoft.com/en-us/bingmaps/), [OpenStreetMap](https://wiki.openstreetmap.org/wiki/API), and [GIS servers](https://wiki.openstreetmap.org/wiki/API). * The specific imagery displayed is determined by a [[Viewport]]'s [MapImagerySettings]($common) and [BackgroundMapSettings]($common). * * The map or globe may be smooth, or feature 3d geometry supplied by a [[TerrainProvider]]. * The terrain displayed in a [[Viewport]] is determined by its [TerrainSettings]($common). * @public */ export class MapTileTree extends RealityTileTree { /** @internal */ ecefToDb; /** @internal */ bimElevationBias; /** @internal */ geodeticOffset; /** @internal */ sourceTilingScheme; /** @internal */ _mercatorFractionToDb; /** @internal */ earthEllipsoid; /** @internal */ minEarthEllipsoid; /** @internal */ maxEarthEllipsoid; /** Determines whether the map displays as a plane or an ellipsoid. */ globeMode; /** @internal */ globeOrigin; /** @internal */ _mercatorTilingScheme; /** @internal */ useDepthBuffer; /** @internal */ isOverlay; /** @internal */ terrainExaggeration; /** @internal */ baseColor; /** @internal */ baseTransparent; /** @internal */ mapTransparent; /** @internal */ produceGeometry; /** @internal */ layerImageryTrees = []; _layerHandler; /** @internal */ get layerHandler() { return this._layerHandler; } /** @internal */ constructor(params, ecefToDb, bimElevationBias, geodeticOffset, sourceTilingScheme, id, applyTerrain) { super(params); this.ecefToDb = ecefToDb; this.bimElevationBias = bimElevationBias; this.geodeticOffset = geodeticOffset; this.sourceTilingScheme = sourceTilingScheme; this._mercatorTilingScheme = new WebMercatorTilingScheme(); this._mercatorFractionToDb = this._mercatorTilingScheme.computeMercatorFractionToDb(ecefToDb, bimElevationBias, params.iModel, applyTerrain); const quadId = new QuadId(sourceTilingScheme.rootLevel, 0, 0); this.globeOrigin = this.ecefToDb.getOrigin().clone(); this.earthEllipsoid = Ellipsoid.createCenterMatrixRadii(this.globeOrigin, this.ecefToDb.matrix, Constant.earthRadiusWGS84.equator, Constant.earthRadiusWGS84.equator, Constant.earthRadiusWGS84.polar); const globalHeightRange = applyTerrain ? ApproximateTerrainHeights.instance.globalHeightRange : Range1d.createXX(0, 0); const globalRectangle = MapCartoRectangle.createMaximum(); this.globeMode = id.globeMode; this.isOverlay = id.isOverlay; this.useDepthBuffer = id.useDepthBuffer; this.terrainExaggeration = id.terrainExaggeration; this.baseColor = id.baseColor; this.baseTransparent = id.baseTransparent; this.mapTransparent = id.mapTransparent; if (applyTerrain) { this.minEarthEllipsoid = Ellipsoid.createCenterMatrixRadii(this.globeOrigin, this.ecefToDb.matrix, Constant.earthRadiusWGS84.equator + globalHeightRange.low, Constant.earthRadiusWGS84.equator + globalHeightRange.low, Constant.earthRadiusWGS84.polar + globalHeightRange.low); this.maxEarthEllipsoid = Ellipsoid.createCenterMatrixRadii(this.globeOrigin, this.ecefToDb.matrix, Constant.earthRadiusWGS84.equator + globalHeightRange.high, Constant.earthRadiusWGS84.equator + globalHeightRange.high, Constant.earthRadiusWGS84.polar + globalHeightRange.high); this.produceGeometry = id.produceGeometry; } else { this.minEarthEllipsoid = this.earthEllipsoid; this.maxEarthEllipsoid = this.earthEllipsoid; } const rootPatch = EllipsoidPatch.createCapture(this.maxEarthEllipsoid, AngleSweep.createStartSweepRadians(0, Angle.pi2Radians), AngleSweep.createStartSweepRadians(-Angle.piOver2Radians, Angle.piRadians)); let range; if (this.globeMode === GlobeMode.Ellipsoid) { range = rootPatch.range(); } else { const corners = this.getFractionalTileCorners(quadId); this._mercatorFractionToDb.multiplyPoint3dArrayInPlace(corners); range = Range3d.createArray(MapTile.computeRangeCorners(corners, Vector3d.create(0, 0, 1), 0, scratchCorners, globalHeightRange)); } this._rootTile = this.createGlobeChild({ contentId: quadId.contentId, maximumSize: 0, range }, quadId, range.corners(), globalRectangle, rootPatch, undefined); this._layerHandler = new LayerTileTreeHandler(this); } /** @internal */ get parentsAndChildrenExclusive() { // If we are not depth buffering we force parents and exclusive to false to cause the map tiles to be sorted // by depth so that painters algorithm will approximate correct depth display. return this.useDepthBuffer ? this.loader.parentsAndChildrenExclusive : false; } /** Return the imagery tile tree state of matching the provided imagery tree id. * @internal */ getImageryTreeState(imageryTreeId) { return this._layerHandler.imageryTreeState.get(imageryTreeId); } /** Return a cloned dictionary of the imagery tile tree states * @internal */ cloneImageryTreeState() { const clone = new Map(); for (const [treeId, state] of this._layerHandler.imageryTreeState) { clone.set(treeId, state.clone()); } return clone; } /** @internal */ tileFromQuadId(quadId) { return this._rootTile.tileFromQuadId(quadId); } collectClassifierGraphics(args, selectedTiles) { super.collectClassifierGraphics(args, selectedTiles); this._layerHandler.collectClassifierGraphics(args, selectedTiles); } /** @internal */ clearImageryTreesAndClassifiers() { this._layerHandler.layerImageryTrees.length = 0; this._layerHandler.layerSettings.clear(); this._layerHandler.modelIdToIndex.clear(); this._layerHandler.layerClassifiers.clear(); } /** @internal */ get isTransparent() { return this.mapTransparent || this.baseTransparent; } /** @internal */ get maxDepth() { let maxDepth = this.loader.maxDepth; /** NB: need to use local layerImageTrees because of the order super() is called (before layerHandler exists on this class)! */ this.layerImageryTrees?.forEach((layerImageryTree) => maxDepth = Math.max(maxDepth, layerImageryTree.tree.maxDepth)); return maxDepth; } /** @internal */ createPlanarChild(params, quadId, corners, normal, rectangle, chordHeight, heightRange) { const childAvailable = this.mapLoader.isTileAvailable(quadId); if (!childAvailable && this.produceGeometry) return undefined; const patch = new PlanarTilePatch(corners, normal, chordHeight); const cornerNormals = this.getCornerRays(rectangle); if (childAvailable) return new MapTile(params, this, quadId, patch, rectangle, heightRange, cornerNormals); assert(params.parent instanceof MapTile); let loadableTile = params.parent; while (loadableTile?.isUpsampled) loadableTile = loadableTile.parent; assert(undefined !== loadableTile); return new UpsampledMapTile(params, this, quadId, patch, rectangle, heightRange, cornerNormals, loadableTile); } /** @internal */ createGlobeChild(params, quadId, _rangeCorners, rectangle, ellipsoidPatch, heightRange) { return new MapTile(params, this, quadId, ellipsoidPatch, rectangle, heightRange, this.getCornerRays(rectangle)); } /** @internal */ getChildHeightRange(quadId, rectangle, parent) { return this.mapLoader.getChildHeightRange(quadId, rectangle, parent); } /** Reprojection does not work with very large tiles so just do linear transform. * @internal */ static minReprojectionDepth = 8; /** @internal */ static maxGlobeDisplayDepth = 8; /** @internal */ static minDisplayableDepth = 3; /** @internal */ get mapLoader() { return this.loader; } /** @internal */ getBaseRealityDepth(sceneContext) { // If the view has ever had global scope then preload low level (global) tiles. return (sceneContext.viewport.view.maxGlobalScopeFactor > 1) ? MapTileTree.minDisplayableDepth : -1; } /** @internal */ doCreateGlobeChildren(tile) { if (this.globeMode !== GlobeMode.Ellipsoid) return false; const childDepth = tile.depth + 1; if (childDepth < MapTileTree.maxGlobeDisplayDepth) // If the depth is too low (tile is too large) display as globe. return true; return false; // Display as globe if more than 100 KM from project. } /** @internal */ doReprojectChildren(tile) { if (this._gcsConverter === undefined) return false; const childDepth = tile.depth + 1; if (childDepth < MapTileTree.minReprojectionDepth) // If the depth is too low (tile is too large) omit reprojection. return false; return this.cartesianRange.intersectsRange(tile.range); } /** @internal */ getCornerRays(rectangle) { const rays = new Array(); if (this.globeMode === GlobeMode.Ellipsoid) { rays.push(expectDefined(this.earthEllipsoid.radiansToUnitNormalRay(rectangle.low.x, Cartographic.parametricLatitudeFromGeodeticLatitude(rectangle.high.y)))); rays.push(expectDefined(this.earthEllipsoid.radiansToUnitNormalRay(rectangle.high.x, Cartographic.parametricLatitudeFromGeodeticLatitude(rectangle.high.y)))); rays.push(expectDefined(this.earthEllipsoid.radiansToUnitNormalRay(rectangle.low.x, Cartographic.parametricLatitudeFromGeodeticLatitude(rectangle.low.y)))); rays.push(expectDefined(this.earthEllipsoid.radiansToUnitNormalRay(rectangle.high.x, Cartographic.parametricLatitudeFromGeodeticLatitude(rectangle.low.y)))); } else { const mercatorFractionRange = rectangle.getTileFractionRange(this._mercatorTilingScheme); rays.push(Ray3d.createCapture(this._mercatorFractionToDb.multiplyXYZ(mercatorFractionRange.low.x, mercatorFractionRange.high.y), scratchZNormal)); rays.push(Ray3d.createCapture(this._mercatorFractionToDb.multiplyXYZ(mercatorFractionRange.high.x, mercatorFractionRange.high.y), scratchZNormal)); rays.push(Ray3d.createCapture(this._mercatorFractionToDb.multiplyXYZ(mercatorFractionRange.low.x, mercatorFractionRange.low.y), scratchZNormal)); rays.push(Ray3d.createCapture(this._mercatorFractionToDb.multiplyXYZ(mercatorFractionRange.high.x, mercatorFractionRange.low.y), scratchZNormal)); } return rays; } /** @internal */ pointAboveEllipsoid(point) { return expectDefined(this.earthEllipsoid.worldToLocal(point, scratchPoint)).magnitude() > 1; } getMercatorFractionChildGridPoints(tile, columnCount, rowCount) { const gridPoints = []; const quadId = tile.quadId; const deltaX = 1.0 / columnCount, deltaY = 1.0 / rowCount; for (let row = 0; row <= rowCount; row++) { for (let column = 0; column <= columnCount; column++) { const xFraction = this.sourceTilingScheme.tileXToFraction(quadId.column + column * deltaX, quadId.level); const yFraction = this.sourceTilingScheme.tileYToFraction(quadId.row + row * deltaY, quadId.level); gridPoints.push(Point3d.create(xFraction, yFraction, 0)); } } // If not mercator already need to remap latitude... if (!(this.sourceTilingScheme instanceof WebMercatorTilingScheme)) for (const gridPoint of gridPoints) gridPoint.y = this._mercatorTilingScheme.latitudeToYFraction(this.sourceTilingScheme.yFractionToLatitude(gridPoint.y)); return gridPoints; } getChildCornersFromGridPoints(gridPoints, columnCount, rowCount) { const childCorners = new Array(); for (let row = 0; row < rowCount; row++) { for (let column = 0; column < columnCount; column++) { const index0 = column + row * (columnCount + 1); const index1 = index0 + (columnCount + 1); childCorners.push([gridPoints[index0], gridPoints[index0 + 1], gridPoints[index1], gridPoints[index1 + 1]]); } } return childCorners; } /** @internal */ getCachedReprojectedPoints(gridPoints) { const requestProps = []; for (const gridPoint of gridPoints) requestProps.push({ x: this._mercatorTilingScheme.xFractionToLongitude(gridPoint.x) * Angle.degreesPerRadian, y: this._mercatorTilingScheme.yFractionToLatitude(gridPoint.y) * Angle.degreesPerRadian, z: this.bimElevationBias, }); const iModelCoordinates = expectDefined(this._gcsConverter).getCachedIModelCoordinatesFromGeoCoordinates(requestProps); if (iModelCoordinates.missing) return undefined; return iModelCoordinates.result.map((result) => !result || result.s ? undefined : Point3d.fromJSON(result.p)); } /** Minimize reprojection requests by requesting this corners tile and a grid that will include all points for 4 levels of descendants. * This greatly reduces the number of reprojection requests which currently require a roundtrip through the backend. * @internal */ async loadReprojectionCache(tile) { const quadId = tile.quadId; const xRange = Range1d.createXX(this.sourceTilingScheme.tileXToFraction(quadId.column, quadId.level), this.sourceTilingScheme.tileXToFraction(quadId.column + 1, quadId.level)); const yRange = Range1d.createXX(this.sourceTilingScheme.tileYToFraction(quadId.row, quadId.level), this.sourceTilingScheme.tileYToFraction(quadId.row + 1, quadId.level)); const cacheDepth = 4, cacheDimension = 2 ** cacheDepth; const delta = 1.0 / cacheDimension; const requestProps = []; for (let row = 0; row <= cacheDimension; row++) { for (let column = 0; column <= cacheDimension; column++) { let yFraction = yRange.fractionToPoint(row * delta); if (!(this.sourceTilingScheme instanceof WebMercatorTilingScheme)) yFraction = this._mercatorTilingScheme.latitudeToYFraction(this.sourceTilingScheme.yFractionToLatitude(yFraction)); requestProps.push({ x: this._mercatorTilingScheme.xFractionToLongitude(xRange.fractionToPoint(column * delta)) * Angle.degreesPerRadian, y: this._mercatorTilingScheme.yFractionToLatitude(yFraction) * Angle.degreesPerRadian, z: this.bimElevationBias, }); } } await expectDefined(this._gcsConverter).getIModelCoordinatesFromGeoCoordinates(requestProps); } static _scratchCarto = Cartographic.createZero(); /** Get the corners for planar children. * This generally will resolve immediately, but may require an asynchronous request for reprojecting the corners. * @internal */ getPlanarChildCorners(tile, columnCount, rowCount, resolve) { const resolveCorners = (points, reprojected = undefined) => { for (let i = 0; i < points.length; i++) { const gridPoint = points[i]; this._mercatorFractionToDb.multiplyPoint3d(gridPoint, scratchCorner); if (this.globeMode !== GlobeMode.Ellipsoid || this.cartesianRange.containsPoint(scratchCorner)) { if (reprojected !== undefined && reprojected[i] !== undefined) // eslint-disable-next-line @typescript-eslint/no-non-null-assertion reprojected[i].clone(gridPoint); else scratchCorner.clone(gridPoint); } else { this._mercatorTilingScheme.fractionToCartographic(gridPoint.x, gridPoint.y, MapTileTree._scratchCarto); this.earthEllipsoid.radiansToPoint(MapTileTree._scratchCarto.longitude, Cartographic.parametricLatitudeFromGeodeticLatitude(MapTileTree._scratchCarto.latitude), gridPoint); const cartesianDistance = this.cartesianRange.distanceToPoint(scratchCorner); if (cartesianDistance < this.cartesianTransitionDistance) scratchCorner.interpolate(cartesianDistance / this.cartesianTransitionDistance, gridPoint, gridPoint); } } resolve(this.getChildCornersFromGridPoints(points, columnCount, rowCount)); }; let reprojectedPoints; const gridPoints = this.getMercatorFractionChildGridPoints(tile, columnCount, rowCount); if (this.doReprojectChildren(tile)) { reprojectedPoints = this.getCachedReprojectedPoints(gridPoints); if (reprojectedPoints) { // If the reprojected corners are in the cache, resolve immediately. resolveCorners(gridPoints, reprojectedPoints); } else { // If the reprojected corners are not in cache request them - but also request reprojection of a grid that will include descendent corners to ensure they can // be reloaded without expensive reprojection requests. this.loadReprojectionCache(tile).then(() => { const reprojected = this.getCachedReprojectedPoints(gridPoints); assert(reprojected !== undefined); // We just cached them... they better be there now. resolveCorners(gridPoints, reprojected); }).catch((_error) => { resolveCorners(gridPoints); }); } } else { resolveCorners(gridPoints); } } /** Scan the list of currently selected reality tiles, and fire the viewport's 'onMapLayerScaleRangeVisibilityChanged ' event * if any scale range visibility change is detected for one more map-layer definition. * @internal */ reportTileVisibility(args, selected) { const debugControl = args.context.target.debugControl; const layersVisibilityBefore = this.cloneImageryTreeState(); const changes = new Array(); if (!layersVisibilityBefore) return; for (const [treeId] of layersVisibilityBefore) { const treeVisibility = this.getImageryTreeState(treeId); if (treeVisibility) { treeVisibility.reset(); } } for (const selectedTile of selected) { if (selectedTile instanceof MapTile) { let selectedImageryTiles = selectedTile.imageryTiles; if (selectedTile.hiddenImageryTiles) { selectedImageryTiles = selectedImageryTiles ? [...selectedImageryTiles, ...selectedTile.hiddenImageryTiles] : selectedTile.hiddenImageryTiles; } const leafTiles = selectedTile.highResolutionReplacementTiles; if (leafTiles) { for (const tile of leafTiles) { const treeState = this.getImageryTreeState(tile.tree.id); treeState?.setScaleRangeVisibility(false); } } if (selectedImageryTiles) { for (const selectedImageryTile of selectedImageryTiles) { const treeState = this.getImageryTreeState(selectedImageryTile.tree.id); if (treeState) { if (selectedImageryTile.isOutOfLodRange) { treeState.setScaleRangeVisibility(false); } else { treeState.setScaleRangeVisibility(true); } } } } } } for (const [treeId, prevState] of layersVisibilityBefore) { const newState = this.getImageryTreeState(treeId); if (newState) { const prevVisibility = prevState.getScaleRangeVisibility(); const visibility = newState.getScaleRangeVisibility(); if (prevVisibility !== visibility) { if (debugControl && debugControl.logRealityTiles) { // eslint-disable-next-line no-console console.log(`ImageryTileTree '${treeId}' changed prev state: '${MapTileTreeScaleRangeVisibility[prevVisibility]}' new state: '${MapTileTreeScaleRangeVisibility[visibility]}'`); } const mapLayersIndexes = args.context.viewport.getMapLayerIndexesFromIds(this.id, treeId); for (const index of mapLayersIndexes) { changes.push({ index: index.index, isOverlay: index.isOverlay, visibility }); } } } } if (changes.length !== 0) { args.context.viewport.onMapLayerScaleRangeVisibilityChanged.raiseEvent(changes); } } /** @internal */ getFractionalTileCorners(quadId) { const corners = []; corners.push(Point3d.create(this.sourceTilingScheme.tileXToFraction(quadId.column, quadId.level), this.sourceTilingScheme.tileYToFraction(quadId.row, quadId.level), 0.0)); corners.push(Point3d.create(this.sourceTilingScheme.tileXToFraction(quadId.column + 1, quadId.level), this.sourceTilingScheme.tileYToFraction(quadId.row, quadId.level), 0.0)); corners.push(Point3d.create(this.sourceTilingScheme.tileXToFraction(quadId.column, quadId.level), this.sourceTilingScheme.tileYToFraction(quadId.row + 1, quadId.level), 0.0)); corners.push(Point3d.create(this.sourceTilingScheme.tileXToFraction(quadId.column + 1, quadId.level), this.sourceTilingScheme.tileYToFraction(quadId.row + 1, quadId.level), 0.0)); return corners; } /** @internal */ getTileRectangle(quadId) { return this.sourceTilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level); } /** @internal */ getLayerIndex(imageryTreeId) { const index = this._layerHandler.modelIdToIndex.get(imageryTreeId); return index === undefined ? -1 : index; } /** @internal */ getLayerTransparency(imageryTreeId) { const layerSettings = this._layerHandler.layerSettings.get(imageryTreeId); assert(undefined !== layerSettings); return undefined === layerSettings || !layerSettings.transparency ? 0.0 : layerSettings.transparency; } } /** @internal */ class MapTileTreeProps { gcsConverterAvailable; id; modelId; location = Transform.createIdentity(); yAxisUp = true; is3d = true; rootTile = { contentId: "", range: Range3d.createNull(), maximumSize: 0 }; loader; iModel; get priority() { return this.loader.priority; } constructor(modelId, loader, iModel, gcsConverterAvailable) { this.gcsConverterAvailable = gcsConverterAvailable; this.id = this.modelId = modelId; this.loader = loader; this.iModel = iModel; } } function createViewFlagOverrides(wantLighting, wantThematic) { return createDefaultViewFlagOverrides({ clipVolume: false, lighting: wantLighting, thematic: wantThematic }); } class MapTreeSupplier { isEcefDependent = true; compareTileTreeIds(lhs, rhs) { let cmp = compareNumbers(lhs.tileUserId, rhs.tileUserId); if (0 === cmp) { cmp = compareStringsOrUndefined(lhs.maskModelIds, rhs.maskModelIds); if (0 === cmp) { cmp = compareBooleans(lhs.isOverlay, rhs.isOverlay); if (0 === cmp) { cmp = compareBooleans(lhs.wantSkirts, rhs.wantSkirts); if (0 === cmp) { cmp = compareBooleans(lhs.wantNormals, rhs.wantNormals); if (0 === cmp) { cmp = compareNumbers(lhs.globeMode, rhs.globeMode); if (0 === cmp) { cmp = compareNumbers(lhs.baseColor ? lhs.baseColor.tbgr : -1, rhs.baseColor ? rhs.baseColor.tbgr : -1); if (0 === cmp) { cmp = compareBooleans(lhs.baseTransparent, rhs.baseTransparent); if (0 === cmp) { cmp = compareBooleans(lhs.mapTransparent, rhs.mapTransparent); if (0 === cmp) { cmp = compareBooleans(lhs.applyTerrain, rhs.applyTerrain); if (0 === cmp) { if (lhs.applyTerrain) { // Terrain-only settings. cmp = compareStrings(lhs.terrainProviderName, rhs.terrainProviderName); if (0 === cmp) { cmp = compareStringsOrUndefined(lhs.terrainDataSource, rhs.terrainDataSource); if (0 === cmp) { cmp = compareNumbers(lhs.terrainHeightOrigin, rhs.terrainHeightOrigin); if (0 === cmp) { cmp = compareNumbers(lhs.terrainHeightOriginMode, rhs.terrainHeightOriginMode); if (0 === cmp) { cmp = compareNumbers(lhs.terrainExaggeration, rhs.terrainExaggeration); if (0 === cmp) cmp = compareBooleansOrUndefined(lhs.produceGeometry, rhs.produceGeometry); } } } } } else { // Non-Terrain (flat) settings. cmp = compareNumbers(lhs.mapGroundBias, rhs.mapGroundBias); if (0 === cmp) cmp = compareBooleans(lhs.useDepthBuffer, rhs.useDepthBuffer); } } } } } } } } } } } return cmp; } async computeHeightBias(heightOrigin, heightOriginMode, exaggeration, iModel, elevationProvider) { const projectCenter = iModel.projectExtents.center; switch (heightOriginMode) { case TerrainHeightOriginMode.Ground: return heightOrigin + exaggeration * (await elevationProvider.getHeightValue(projectCenter, iModel, true)); case TerrainHeightOriginMode.Geodetic: return heightOrigin; case TerrainHeightOriginMode.Geoid: return heightOrigin + await elevationProvider.getGeodeticToSeaLevelOffset(projectCenter, iModel); } } async createTileTree(id, iModel) { let bimElevationBias = 0, terrainProvider, geodeticOffset = 0; let applyTerrain = id.applyTerrain; const modelId = iModel.transientIds.getNext(); const gcsConverterAvailable = await getGcsConverterAvailable(iModel); const terrainOpts = { wantSkirts: id.wantSkirts, exaggeration: id.terrainExaggeration, wantNormals: id.wantNormals, dataSource: id.terrainDataSource, produceGeometry: id.produceGeometry, }; if (id.applyTerrain) { await ApproximateTerrainHeights.instance.initialize(); const elevationProvider = new BingElevationProvider(); bimElevationBias = -await this.computeHeightBias(id.terrainHeightOrigin, id.terrainHeightOriginMode, id.terrainExaggeration, iModel, elevationProvider); geodeticOffset = await elevationProvider.getGeodeticToSeaLevelOffset(iModel.projectExtents.center, iModel); const provider = IModelApp.terrainProviderRegistry.find(id.terrainProviderName); if (provider) terrainProvider = await provider.createTerrainMeshProvider(terrainOpts); if (!terrainProvider) { applyTerrain = false; geodeticOffset = 0; } } if (!terrainProvider) { terrainProvider = new EllipsoidTerrainProvider(terrainOpts); bimElevationBias = id.mapGroundBias; } const loader = new MapTileLoader(iModel, modelId, bimElevationBias, terrainProvider); const ecefToDb = iModel.getMapEcefToDb(bimElevationBias); if (id.maskModelIds) await iModel.models.load(CompressedId64Set.decompressSet(id.maskModelIds)); const treeProps = new MapTileTreeProps(modelId, loader, iModel, gcsConverterAvailable); return new MapTileTree(treeProps, ecefToDb, bimElevationBias, geodeticOffset, terrainProvider.tilingScheme, id, applyTerrain); } } const mapTreeSupplier = new MapTreeSupplier(); /** Specialization of tile tree that represents background map. * @internal */ export class MapTileTreeReference extends TileTreeReference { _isDrape; _overrideTerrainDisplay; _tileUserId; _settings; _symbologyOverrides; _planarClipMask; _layerRefHandler; iModel; get layerRefHandler() { return this._layerRefHandler; } shouldDrapeLayer(layerTreeRef) { const mapLayerSettings = layerTreeRef?.layerSettings; if (mapLayerSettings && mapLayerSettings instanceof ModelMapLayerSettings) return ModelMapLayerDrapeTarget.Globe === mapLayerSettings.drapeTarget; return true; // catch-all for other cases (skip reality models, though). } constructor(settings, baseLayerSettings, layerSettings, iModel, tileUserId, isOverlay, _isDrape, _overrideTerrainDisplay) { super(); this._isDrape = _isDrape; this._overrideTerrainDisplay = _overrideTerrainDisplay; this.iModel = iModel; this._tileUserId = tileUserId; this._settings = settings; if (this._settings.planarClipMask && this._settings.planarClipMask.isValid) this._planarClipMask = PlanarClipMaskState.create(this._settings.planarClipMask); if (this._overrideTerrainDisplay && this._overrideTerrainDisplay()?.produceGeometry) this.collectTileGeometry = (collector) => this._collectTileGeometry(collector); this._layerRefHandler = new LayerTileTreeReferenceHandler(this, isOverlay, baseLayerSettings, layerSettings, true); } forEachLayerTileTreeRef(func) { for (const layerTree of this._layerRefHandler.layerTrees) { assert(layerTree instanceof MapLayerTileTreeReference); func(layerTree); } } get isGlobal() { return true; } get baseColor() { return this.baseColor; } get planarClipMaskPriority() { return PlanarClipMaskPriority.BackgroundMap; } _createGeometryTreeReference() { if (!this._settings.applyTerrain || this._isDrape) return undefined; // Don't bother generating non-terrain (flat) geometry. const ref = new MapTileTreeReference(this._settings, undefined, [], this.iModel, this._tileUserId, false, false, () => { return { produceGeometry: true }; }); assert(undefined !== ref.collectTileGeometry); return ref; } /** Terrain tiles do not contribute to the range used by "fit view". */ unionFitRange(_range) { } get settings() { return this._settings; } set settings(settings) { this._settings = settings; this._planarClipMask = settings.planarClipMask ? PlanarClipMaskState.create(settings.planarClipMask) : undefined; } get layerSettings() { return this.layerRefHandler.layerSettings; } get castsShadows() { return false; } get _isLoadingComplete() { // Wait until drape tree is fully loaded too. for (const drapeTree of this._layerRefHandler.layerTrees) if (drapeTree && !drapeTree.isLoadingComplete) return false; return super._isLoadingComplete; } get useDepthBuffer() { return !this._layerRefHandler.isOverlay && (this.settings.applyTerrain || this.settings.useDepthBuffer); } get treeOwner() { let wantSkirts = (this.settings.applyTerrain || this.useDepthBuffer) && !this.settings.transparency && !this._layerRefHandler.baseTransparent; if (wantSkirts) { const maskTrans = this._planarClipMask?.settings.transparency; wantSkirts = (undefined === maskTrans || maskTrans <= 0); } const id = { tileUserId: this._tileUserId, applyTerrain: this.settings.applyTerrain && !this._isDrape, terrainProviderName: this.settings.terrainSettings.providerName, terrainDataSource: this.settings.terrainSettings.dataSource, terrainHeightOrigin: this.settings.terrainSettings.heightOrigin, terrainHeightOriginMode: this.settings.terrainSettings.heightOriginMode, terrainExaggeration: this.settings.terrainSettings.exaggeration, mapGroundBias: this.settings.groundBias, wantSkirts, // Can set to this.settings.terrainSettings.applyLighting if we want to ever apply lighting to terrain again so that normals are retrieved when lighting is on. wantNormals: false, globeMode: this._isDrape ? GlobeMode.Plane : this.settings.globeMode, isOverlay: this._layerRefHandler.isOverlay, useDepthBuffer: this.useDepthBuffer, baseColor: this._layerRefHandler.baseColor, baseTransparent: this._layerRefHandler.baseTransparent, mapTransparent: Number(this.settings.transparency) > 0, maskModelIds: this._planarClipMask?.settings.compressedModelIds, produceGeometry: false, }; if (undefined !== this._overrideTerrainDisplay) { const ovr = this._overrideTerrainDisplay(); if (undefined !== ovr) { id.wantSkirts = ovr.wantSkirts ?? id.wantSkirts; id.wantNormals = ovr.wantNormals ?? id.wantNormals; id.produceGeometry = ovr.produceGeometry === true; } } return this.iModel.tiles.getTileTreeOwner(id, mapTreeSupplier); } getLayerImageryTreeRef(index) { const baseLayerIndex = this._layerRefHandler.baseImageryLayerIncluded ? 1 : 0; const treeIndex = index + baseLayerIndex; return index < 0 || treeIndex >= this._layerRefHandler.layerTrees.length ? undefined : this._layerRefHandler.layerTrees[treeIndex]; } /** Return the map-layer scale range visibility for the provided map-layer index. * @internal */ getMapLayerScaleRangeVisibility(index) { const tree = this.treeOwner.tileTree; if (undefined !== tree) { const tileTreeRef = this.getLayerImageryTreeRef(index); const treeId = tileTreeRef?.treeOwner.tileTree?.id; if (treeId !== undefined) { const treeState = tree.getImageryTreeState(treeId); if (treeState !== undefined) return treeState.getScaleRangeVisibility(); } } return MapTileTreeScaleRangeVisibility.Unknown; } /** Adds this reference's graphics to the scene. By default this invokes [[TileTree.drawScene]] on the referenced TileTree, if it is loaded. */ addToScene(context) { if (!context.viewFlags.backgroundMap) return; const tree = this.treeOwner.load(); if (undefined === tree || !this._layerRefHandler.initializeLayers(context)) return; // Not loaded yet. if (this._planarClipMask && this._planarClipMask.settings.isValid) context.addPlanarClassifier(tree.modelId, undefined, this._planarClipMask); const nonLocatable = this.settings.locatable ? undefined : true; const transparency = this.settings.transparency ? this.settings.transparency : undefined; this._symbologyOverrides = new FeatureSymbology.Overrides(); if (nonLocatable || transparency) { this._symbologyOverrides.override({ modelId: tree.modelId, appearance: FeatureAppearance.fromJSON({ transparency, nonLocatable }), }); } const args = this.createDrawArgs(context); if (undefined !== args) tree.draw(args); tree.clearImageryTreesAndClassifiers(); } createDrawArgs(context) { const args = super.createDrawArgs(context); if (undefined === args) return undefined; const tree = this.treeOwner.load(); return new RealityTileDrawArgs(args, args.worldToViewMap, args.frustumPlanes, undefined, tree?.layerHandler.layerClassifiers); } getViewFlagOverrides(_tree) { return createViewFlagOverrides(false, this._settings.applyTerrain ? undefined : false); } getSymbologyOverrides(_tree) { return this._symbologyOverrides; } discloseTileTrees(trees) { super.discloseTileTrees(trees); this._layerRefHandler.discloseTileTrees(trees); if (this._planarClipMask) this._planarClipMask.discloseTileTrees(trees); } imageryTreeFromTreeModelIds(mapTreeModelId, layerTreeModelId) { const imageryTrees = []; const tree = this.treeOwner.tileTree; if (undefined === tree || tree.modelId !== mapTreeModelId) return imageryTrees; for (const imageryTree of this._layerRefHandler.layerTrees) if (imageryTree && imageryTree.treeOwner.tileTree && imageryTree.treeOwner.tileTree.modelId === layerTreeModelId) imageryTrees.push(imageryTree); return imageryTrees; } layerFromTreeModelIds(mapTreeModelId, layerTreeModelId) { const imageryTree = this.imageryTreeFromTreeModelIds(mapTreeModelId, layerTreeModelId); return imageryTree.map((tree) => { const isBaseLayer = (this._layerRefHandler.baseImageryLayerIncluded && tree.layerIndex === 0); return { isBaseLayer, index: isBaseLayer ? undefined : { isOverlay: this._layerRefHandler.isOverlay, index: this._layerRefHandler.baseImageryLayerIncluded ? tree.layerIndex - 1 : tree.layerIndex }, settings: tree.layerSettings, provider: tree.imageryProvider, }; }); } // Utility method that execute the provided function for every *imagery* tiles under a given HitDetail object. async forEachImageryTileHit(hit, func) { const tree = this.treeOwner.tileTree; if (undefined === tree || hit.iModel !== tree.iModel || tree.modelId !== hit.modelId || !hit.viewport || !hit.viewport.view.is3d) return undefined; const backgroundMapGeometry = hit.viewport.displayStyle.getBackgroundMapGeometry(); if (undefined === backgroundMapGeometry) return undefined; const worldPoint = hit.hitPoint.clone(); let cartoGraphic; try { cartoGraphic = (await backgroundMapGeometry.dbToWGS84CartographicFromGcs([worldPoint]))[0]; } catch { } if (!cartoGraphic) { return undefined; } const imageryTreeRef = this.imageryTreeFromTreeModelIds(hit.modelId, hit.sourceId); if (imageryTreeRef.length > 0) { if (hit.tileId !== undefined) { const terrainQuadId = QuadId.createFromContentId(hit.tileId); const terrainTile = tree.tileFromQuadId(terrainQuadId); for (const treeRef of imageryTreeRef) { const processedTileIds = []; if (terrainTile && terrainTile.imageryTiles) { const imageryTree = treeRef.treeOwner.tileTree; if (imageryTree) { for (const imageryTile of terrainTile.imageryTiles) { if (!processedTileIds.includes(imageryTile.contentId) && imageryTree === imageryTile.imageryTree && imageryTile.rectangle.containsCartographic(cartoGraphic)) { processedTileIds.push(imageryTile.contentId); try { await func(treeRef, imageryTile.quadId, cartoGraphic, imageryTree); } catch { // continue iterating even though we got a failure. } } } } } } } } } canSupplyToolTip(hit) { const tree = this.treeOwner.tileTree; return undefined !== tree && tree.modelId === hit.modelId; } async getToolTip(hit) { const tree = this.treeOwner.tileTree; if (undefined === tree || tree.modelId !== hit.modelId) return undefined; let carto; const strings = []; const getTooltipFunc = async (imageryTreeRef, quadId, cartoGraphic, imageryTree) => { strings.push(`${IModelApp.localization.getLocalizedString("iModelJs:MapLayers.ImageryLayer")}: ${imageryTreeRef.layerSettings.name}`); carto = cartoGraphic; await imageryTree.imageryLoader.getToolTip(strings, quadId, cartoGraphic, imageryTree); }; try { await this.forEachImageryTileHit(hit, getTooltipFunc); } catch { // No results added } if (carto) { strings.push(`${IModelApp.localization.getLocalizedString("iModelJs:MapLayers.Latitude")}: ${carto.latitudeDegrees.toFixed(4)}`); strings.push(`${IModelApp.localization.getLocalizedString("iModelJs:MapLayers.Longitude")}: ${carto.longitudeDegrees.toFixed(4)}`); if (this.settings.applyTerrain && tree.terrainExaggeration !== 0.0) { const geodeticHeight = (carto.height - tree.bimElevationBias) / tree.terrainExaggeration; strings.push(`${IModelApp.localization.getLocalizedString("iModelJs:MapLayers.Height")}: ${geodeticHeight.toFixed(1)} ${IModelApp.localization.getLocalizedString("iModelJs:MapLayers.SeaLevel")}: ${(geodeticHeight - tree.geodeticOffset).toFixed(1)}`); } } const div = document.createElement("div"); div.innerHTML = strings.join("<br>"); return div; } async getMapFeatureInfo(hit, options) { const tree = this.treeOwner.tileTree; if (undefined === tree || hit.iModel !== tree.iModel || tree.modelId !== hit.modelId || !hit.viewport || !hit.viewport.view.is3d) return undefined; const info = []; const imageryTreeRef = this.imageryTreeFromTreeModelIds(hit.modelId, hit.sourceId); if (imageryTreeRef !== undefined) { const getFeatureInfoFunc = async (_imageryTreeRef, quadId, cartoGraphic, imageryTree) => { try { await imageryTree.imageryLoader.getMapFeatureInfo(info, quadId, cartoGraphic, imageryTree, hit, options); } catch { } }; try { await this.forEachImageryTileHit(hit, getFeatureInfoFunc); } catch { // No results added } } return info; } /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */ addLogoCards(cards, vp) { const tree = this.treeOwner.tileTree; if (tree) { // eslint-disable-next-line @typescript-eslint/no-deprecated tree.mapLoader.terrainProvider.addLogoCards(cards, vp); for (const imageryTreeRef of this._layerRefHandler.layerTrees) { if (imageryTreeRef?.layerSettings.visible) { const imageryTree = imageryTreeRef.treeOwner.tileTree; if (imageryTree instanceof ImageryMapTileTree) // eslint-disable-next-line @typescript-eslint/no-deprecated imageryTree.addLogoCards(cards, vp); } } } } /** Add logo cards to logo div. */ async addAttributions(cards, vp) { const tree = this.treeOwner.tileTree; if (tree) { const promises = [tree.mapLoader.terrainProvider.addAttributions(cards, vp)]; for (const imageryTreeRef of this._layerRefHandler.layerTrees) { if (imageryTreeRef?.layerSettings.visible) { const imageryTree = imageryTreeRef.treeOwner.tileTree; if (imageryTree instanceof ImageryMapTileTree) promises.push(imageryTree.addAttributions(cards, vp)); } } await Promise.all(promises); } } decorate(context) { for (const layerTree of this._layerRefHandler.layerTrees) { if (layerTree) layerTree.decorate(context); } } } /** Returns whether a GCS converter is available. * @internal */ ex