@itwin/core-frontend
Version:
iTwin.js frontend components
930 lines • 50.7 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 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