@itwin/core-frontend
Version:
iTwin.js frontend components
727 lines • 36.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, dispose, expectDefined } from "@itwin/core-bentley";
import { ColorByName, ColorDef, FrustumPlanes, GlobeMode, PackedFeatureTable } from "@itwin/core-common";
import { AxisOrder, BilinearPatch, ClipPlane, ClipPrimitive, ClipShape, ClipVector, Constant, ConvexClipPlaneSet, EllipsoidPatch, LongitudeLatitudeNumber, Matrix3d, Point3d, PolygonOps, Range1d, Range2d, Range3d, Ray3d, Transform, Vector2d, Vector3d } from "@itwin/core-geometry";
import { IModelApp } from "../../IModelApp";
import { RealityMeshParams } from "../../render/RealityMeshParams";
import { upsampleRealityMeshParams } from "../../internal/render/UpsampleRealityMeshParams";
import { TerrainTexture } from "../../internal/render/RenderTerrain";
import { MapCartoRectangle, MapTileTree, QuadId, RealityTile, TileGraphicType, TileLoadStatus, TileTreeLoadStatus, } from "../internal";
/** @internal */
export class PlanarTilePatch {
corners;
normal;
_chordHeight;
constructor(corners, normal, _chordHeight) {
this.corners = corners;
this.normal = normal;
this._chordHeight = _chordHeight;
}
getRangeCorners(heightRange, result) {
let index = 0;
for (const corner of this.corners)
corner.plusScaled(this.normal, heightRange.low - this._chordHeight, result[index++]);
for (const corner of this.corners)
corner.plusScaled(this.normal, heightRange.high + this._chordHeight, result[index++]);
return result;
}
getClipShape() {
return [this.corners[0], this.corners[1], this.corners[3], this.corners[2]];
}
}
/** Projects points within the rectangular region of a [[MapTile]] into 3d space.
* @see [[MapTile.getProjection]] to obtain the projection for a [[MapTile]].
* @public
*/
export class MapTileProjection {
/** @alpha */
get ellipsoidPatch() { return undefined; }
/** @alpha */
getGlobalPoint(u, v, z, result) {
const point = this.getPoint(u, v, z, result);
return this.transformFromLocal.multiplyPoint3d(point, point);
}
}
/** @alpha */
class EllipsoidProjection extends MapTileProjection {
_patch;
transformFromLocal = Transform.createIdentity();
localRange;
constructor(_patch, heightRange) {
super();
this._patch = _patch;
this.localRange = _patch.range();
this.localRange.expandInPlace(heightRange ? (heightRange.high - heightRange.low) : 0);
}
static _scratchAngles = LongitudeLatitudeNumber.createZero();
static _scratchRay = Ray3d.createZero();
getPoint(u, v, height, result) {
const angles = this._patch.uvFractionToAngles(u, v, height, EllipsoidProjection._scratchAngles);
const ray = this._patch.anglesToUnitNormalRay(angles, EllipsoidProjection._scratchRay);
return Point3d.createFrom(expectDefined(ray).origin, result);
}
get ellipsoidPatch() { return this._patch; }
}
/** @alpha */
export class PlanarProjection extends MapTileProjection {
_bilinearPatch;
transformFromLocal;
localRange;
constructor(patch, heightRange) {
super();
this.transformFromLocal = Transform.createOriginAndMatrix(patch.corners[0], Matrix3d.createRigidHeadsUp(patch.normal, AxisOrder.ZYX));
const planeCorners = expectDefined(this.transformFromLocal.multiplyInversePoint3dArray([patch.corners[0], patch.corners[1], patch.corners[2], patch.corners[3]]));
this.localRange = Range3d.createArray(planeCorners);
this.localRange.low.z += heightRange ? heightRange.low : 0;
this.localRange.high.z += heightRange ? heightRange.high : 0;
this._bilinearPatch = new BilinearPatch(planeCorners[0], planeCorners[1], planeCorners[2], planeCorners[3]);
}
getPoint(u, v, z, result) {
result = this._bilinearPatch.uvFractionToPoint(u, v, result);
result.z += z;
return result;
}
}
const scratchNormal = Vector3d.create();
const scratchViewZ = Vector3d.create();
const scratchPoint = Point3d.create();
const scratchClipPlanes = [ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint), ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint), ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint), ClipPlane.createNormalAndPoint(scratchNormal, scratchPoint)];
const scratchCorners = [Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero(), Point3d.createZero()];
/** A [[Tile]] belonging to a [[MapTileTree]] representing a rectangular region of a map of the Earth.
* @public
*/
export class MapTile extends RealityTile {
static _maxParentHeightDepth = 4;
_imageryTiles;
_hiddenTiles;
_highResolutionReplacementTiles;
/** @internal */
everLoaded = false; // If the tile is only required for availability metadata, load it once and then allow it to be unloaded.
/** @internal */
_heightRange;
/** @internal */
_renderGeometry;
/** @internal */
_mesh; // Primitive retained on leaves only for upsampling.
/** @internal */
get isReady() {
return super.isReady && this.baseImageryIsReady;
}
/** @internal */
get hasGraphics() {
return this._renderGeometry !== undefined;
}
/** @internal */
get renderGeometry() { return this._renderGeometry; }
/** @internal */
get mesh() { return this._mesh; }
/** @internal */
get loadableTerrainTile() { return this.loadableTile; }
/** @internal */
get isPlanar() { return this._patch instanceof PlanarTilePatch; }
/** @internal */
get imageryTiles() { return this._imageryTiles; }
/** List of selected tiles but are currently in hidden state (i.e. scale range visibility)
* @internal
*/
get hiddenImageryTiles() { return this._hiddenTiles; }
/** List of leafs tiles that have been selected as a replacement for missing high resolution tiles.
* When this list is non-empty this means we are past the maximum LOD available of the tile tree.
* By using those tiles, you are likely to get a display where tiles looks pixelated..
* in some cases this is preferred to have no tile at all.
* @internal
*/
get highResolutionReplacementTiles() { return this._highResolutionReplacementTiles; }
/** The [[MapTileTree]] to which this tile belongs. */
mapTree;
/** Uniquely identifies this tile within its [[mapTree]]. */
quadId;
_patch;
/** The area of the surface of the Earth that this tile represents. */
rectangle;
/** @internal */
_cornerRays;
/** @internal */
constructor(params, mapTree, quadId, patch, rectangle, heightRange, cornerRays) {
super(params, mapTree);
this.mapTree = mapTree;
this.quadId = quadId;
this._patch = patch;
this.rectangle = rectangle;
this._cornerRays = cornerRays;
this._heightRange = heightRange?.clone();
}
/** @internal */
getRangeCorners(result) {
return this._patch instanceof PlanarTilePatch ? this._patch.getRangeCorners(expectDefined(this.heightRange), result) : this.range.corners(result);
}
/** @internal */
getSizeProjectionCorners() {
// Use only the first 4 corners -- On terrain tiles the height is initially exagerated to world height range which can cause excessive tile loading.
const rangeCorners = this.getRangeCorners(scratchCorners);
return rangeCorners.slice(0, 4);
}
/** @internal */
markUsed(args) {
super.markUsed(args);
if (this._imageryTiles)
for (const imageryTile of this._imageryTiles)
imageryTile.markUsed(args);
}
/** @internal */
get graphicType() {
if (this.mapTree.isOverlay)
return TileGraphicType.Overlay;
return (this.mapTree.useDepthBuffer || this._forceDepthBuffer) ? TileGraphicType.Scene : TileGraphicType.BackgroundMap;
}
/** @internal */
get mapLoader() { return this.realityRoot.loader; }
/** @internal */
get isUpsampled() { return false; }
/** @internal */
tileFromQuadId(quadId) {
if (0 === quadId.compare(this.quadId))
return this;
assert(quadId.level > this.quadId.level);
if (quadId.level <= this.quadId.level)
return undefined;
if (this.children) {
const shift = quadId.level - this.quadId.level - 1;
const childRow = quadId.row >> shift;
const childColumn = quadId.column >> shift;
for (const child of this.children) {
const mapChild = child;
const childQuadId = mapChild.quadId;
if (childQuadId.row === childRow && childQuadId.column === childColumn)
return mapChild.tileFromQuadId(quadId);
}
}
return undefined;
}
get _forceDepthBuffer() {
// For large ellipsoidal globe tile force the depth buffer on to avoid anomalies at horizon.
return this.mapTree.globeMode === GlobeMode.Ellipsoid && this.depth < 8;
}
/** @internal */
addBoundingGraphic(builder, color) {
if (!this.isDisplayable)
return;
const heightRange = (this.heightRange === undefined) ? Range1d.createXX(-1, 1) : this.heightRange;
const lows = [], highs = [], reorder = [0, 1, 3, 2, 0];
const cornerRays = expectDefined(this._cornerRays);
if (this._patch instanceof PlanarTilePatch) {
const normal = this._patch.normal;
for (let i = 0; i < 5; i++) {
const corner = this._patch.corners[reorder[i]];
lows.push(corner.plusScaled(normal, heightRange.low));
highs.push(corner.plusScaled(normal, heightRange.high));
}
}
else {
for (let i = 0; i < 5; i++) {
const cornerRay = cornerRays[reorder[i]];
lows.push(cornerRay.origin.plusScaled(cornerRay.direction, heightRange.low));
highs.push(cornerRay.origin.plusScaled(cornerRay.direction, heightRange.high));
}
}
builder.setSymbology(color, color, 1);
builder.addLineString(lows);
builder.addLineString(highs);
for (let i = 0; i < 4; i++)
builder.addLineString([lows[i], highs[i]]);
const inColor = ColorDef.create(ColorByName.cornflowerBlue);
const outColor = ColorDef.create(ColorByName.chartreuse);
const transitionColor = ColorDef.create(ColorByName.aquamarine);
const inPoints = [], outPoints = [], transitionPoints = [];
for (const point of highs)
if (this.mapTree.cartesianRange.containsPoint(point))
inPoints.push(point);
else if (this.mapTree.cartesianRange.distanceToPoint(point) < this.mapTree.cartesianTransitionDistance)
transitionPoints.push(point);
else
outPoints.push(point);
builder.setSymbology(inColor, inColor, 15);
builder.addPointString(inPoints);
builder.setSymbology(outColor, outColor, 15);
builder.addPointString(outPoints);
builder.setSymbology(transitionColor, transitionColor, 31);
builder.addPointString(transitionPoints);
}
/** @internal */
getContentClip() {
const points = this.getClipShape();
if (points.length < 3)
return undefined;
if (this.mapTree.globeMode === GlobeMode.Ellipsoid) {
const normal = PolygonOps.areaNormal(points);
const globeOrigin = this.mapTree.globeOrigin;
const globeNormal = Vector3d.createStartEnd(globeOrigin, points[0]);
const negate = normal.dotProduct(globeNormal) < 0;
const clipPlanes = [];
for (let i = 0; i < 4; i++) {
const point = points[i];
const clipNormal = globeOrigin.crossProductToPoints(point, points[(i + 1) % 4], scratchNormal);
if (negate)
clipNormal.negate(clipNormal);
const clipPlane = ClipPlane.createNormalAndPoint(clipNormal, point, false, false, scratchClipPlanes[i]);
if (clipPlane !== undefined) // Undefined at pole tiles...
clipPlanes.push(clipPlane);
}
const planeSet = ConvexClipPlaneSet.createPlanes(clipPlanes);
const clipPrimitive = ClipPrimitive.createCapture(planeSet);
return ClipVector.createCapture([clipPrimitive]);
}
else {
return ClipVector.createCapture([expectDefined(ClipShape.createShape(points))]);
}
}
/** @internal */
setNotFound() {
super.setNotFound();
// For map tiles assume that an unfound tile implies descendants and siblings will also be unfound.
if (undefined !== this.parent)
this.parent.setLeaf();
}
/** @internal */
getGraphic(_system, _texture) {
return undefined;
}
/** For globe tiles displaying less then depth 2 appears distorted
* @internal
*/
get isDisplayable() {
return this.mapTree.globeMode === GlobeMode.Ellipsoid ? (this.depth >= MapTileTree.minDisplayableDepth) : super.isDisplayable;
}
/** @internal */
isOccluded(viewingSpace) {
if (undefined === this._cornerRays || this.mapTree.globeMode !== GlobeMode.Ellipsoid)
return false;
if (viewingSpace.eyePoint !== undefined) {
if (!this.mapTree.pointAboveEllipsoid(viewingSpace.eyePoint))
return false;
for (const cornerNormal of this._cornerRays) {
const eyeNormal = Vector3d.createStartEnd(viewingSpace.eyePoint, cornerNormal.origin, scratchNormal);
eyeNormal.normalizeInPlace();
if (eyeNormal.dotProduct(cornerNormal.direction) < .01)
return false;
}
}
else {
const viewZ = viewingSpace.rotation.getRow(2, scratchViewZ);
for (const cornerNormal of this._cornerRays)
if (cornerNormal.direction.dotProduct(viewZ) > 0)
return false;
}
return true;
}
/** @internal */
_loadChildren(resolve, _reject) {
const mapTree = this.mapTree;
const childLevel = this.quadId.level + 1;
const rowCount = mapTree.sourceTilingScheme.getNumberOfYChildrenAtLevel(childLevel);
const columnCount = mapTree.sourceTilingScheme.getNumberOfXChildrenAtLevel(childLevel);
const resolveChildren = (children) => {
const childrenRange = Range3d.createNull();
for (const child of children)
childrenRange.extendRange(child.range);
if (!this.range.containsRange(childrenRange))
this.range.extendRange(childrenRange);
resolve(children);
};
if (mapTree.doCreateGlobeChildren(this)) {
this.createGlobeChildren(columnCount, rowCount, resolveChildren);
return;
}
const resolvePlanarChildren = (childCorners) => {
const level = this.quadId.level + 1;
const column = this.quadId.column * 2;
const row = this.quadId.row * 2;
const children = [];
const childrenAreLeaves = (this.depth + 1) === mapTree.loader.maxDepth;
const globeMode = this.mapTree.globeMode;
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < columnCount; i++) {
const quadId = new QuadId(level, column + i, row + j);
const corners = childCorners[j * columnCount + i];
const rectangle = mapTree.getTileRectangle(quadId);
const normal = PolygonOps.areaNormal([corners[0], corners[1], corners[3], corners[2]]);
normal.normalizeInPlace();
const heightRange = this.mapTree.getChildHeightRange(quadId, rectangle, this);
const diagonal = Math.max(corners[0].distance(corners[3]), corners[1].distance(corners[2])) / 2.0;
const chordHeight = globeMode === GlobeMode.Ellipsoid ? Math.sqrt(diagonal * diagonal + Constant.earthRadiusWGS84.equator * Constant.earthRadiusWGS84.equator) - Constant.earthRadiusWGS84.equator : 0.0;
const rangeCorners = MapTile.computeRangeCorners(corners, normal, chordHeight, undefined, heightRange);
const range = Range3d.createArray(rangeCorners);
const child = this.mapTree.createPlanarChild({ contentId: quadId.contentId, maximumSize: 512, range, parent: this, isLeaf: childrenAreLeaves }, quadId, corners, normal, rectangle, chordHeight, heightRange);
if (child)
children.push(child);
}
}
resolveChildren(children);
};
mapTree.getPlanarChildCorners(this, columnCount, rowCount, resolvePlanarChildren);
}
createGlobeChildren(columnCount, rowCount, resolve) {
const level = this.quadId.level + 1;
const column = this.quadId.column * 2;
const row = this.quadId.row * 2;
const mapTree = this.mapTree;
const children = [];
for (let j = 0; j < rowCount; j++) {
for (let i = 0; i < columnCount; i++) {
const quadId = new QuadId(level, column + i, row + j);
const angleSweep = quadId.getAngleSweep(mapTree.sourceTilingScheme);
const ellipsoidPatch = EllipsoidPatch.createCapture(this.mapTree.earthEllipsoid, angleSweep.longitude, angleSweep.latitude);
const range = ellipsoidPatch.range();
const rectangle = mapTree.getTileRectangle(quadId);
const heightRange = this.mapTree.getChildHeightRange(quadId, rectangle, this);
if (undefined !== heightRange)
range.expandInPlace(heightRange.high - heightRange.low);
children.push(this.mapTree.createGlobeChild({ contentId: quadId.contentId, maximumSize: 512, range, parent: this, isLeaf: false }, quadId, range.corners(), rectangle, ellipsoidPatch, heightRange));
}
}
resolve(children);
return children;
}
/** @internal */
static computeRangeCorners(corners, normal, chordHeight, result, heightRange) {
if (result === undefined) {
result = [];
for (let i = 0; i < 8; i++)
result.push(Point3d.create());
}
let index = 0;
assert(corners.length === 4);
const deltaLow = normal.scale(-chordHeight + (heightRange ? heightRange.low : 0));
const deltaHigh = normal.scale(chordHeight + (heightRange ? heightRange.high : 0));
for (const corner of corners)
corner.plus(deltaLow, result[index++]);
for (const corner of corners)
corner.plus(deltaHigh, result[index++]);
return result;
}
/** @internal */
isRegionCulled(args) {
return this.isContentCulled(args);
}
/** @internal */
isContentCulled(args) {
return FrustumPlanes.Containment.Outside === args.frustumPlanes.computeContainment(this.getRangeCorners(scratchCorners));
}
clearImageryTiles() {
if (this._imageryTiles) {
this._imageryTiles.forEach((tile) => tile.releaseMapTileUsage());
this._imageryTiles = undefined;
}
if (this._hiddenTiles) {
this._hiddenTiles = undefined;
}
if (this._highResolutionReplacementTiles) {
this._highResolutionReplacementTiles = undefined;
}
}
/** @internal */
produceGraphics() {
if (undefined !== this._graphic && this.imageryIsReady)
return this._graphic;
const geometry = this.renderGeometry;
if (undefined === geometry)
return undefined;
const textures = this.getDrapeTextures();
const { baseColor, baseTransparent } = this.mapTree;
const layerClassifiers = this.mapTree.layerHandler.layerClassifiers;
const graphic = IModelApp.renderSystem.createRealityMeshGraphic({ realityMesh: geometry, projection: this.getProjection(), tileRectangle: this.rectangle, featureTable: PackedFeatureTable.pack(this.mapLoader.featureTable), tileId: this.contentId, baseColor, baseTransparent, textures, layerClassifiers, disableClipStyle: true }, true);
// If there are no layer classifiers then we can save this graphic for re-use. If layer classifiers exist they are regenerated based on view and we must collate them with the imagery.
if (this.imageryIsReady && 0 === this.mapTree.layerHandler.layerClassifiers.size)
this._graphic = graphic;
return graphic;
}
/** @internal */
getClipShape() {
if (undefined === this._cornerRays)
throw new Error("MapTile.getClipShape called before corner rays were set");
return (this._patch instanceof PlanarTilePatch) ? this._patch.getClipShape() : [this._cornerRays[0].origin, this._cornerRays[1].origin, this._cornerRays[3].origin, this._cornerRays[2].origin];
}
/** @internal */
_collectStatistics(stats) {
super._collectStatistics(stats);
this._renderGeometry?.collectStatistics(stats);
if (this._mesh) {
stats.addTerrain(this._mesh.indices.byteLength
+ this._mesh.positions.points.byteLength
+ this._mesh.uvs.points.byteLength
+ (this._mesh.normals ? this._mesh.normals.byteLength : 0));
}
}
/** Height range is along with the tile corners to detect if tile intersects view frustum.
* Range will be single value fo ron-terrain tiles -- if terrain tile is not loaded it will
* inherit height from ancestors.
* @internal
*/
get heightRange() {
if (undefined !== this._heightRange)
return this._heightRange;
for (let parent = this.parent; undefined !== parent; parent = parent.parent) {
const mapParent = parent;
if (undefined !== mapParent._heightRange)
return mapParent._heightRange;
}
assert(false);
return Range1d.createNull();
}
/** @internal */
get mapTilingScheme() {
return this.mapTree.sourceTilingScheme;
}
/** Adjust the minimum and maximum elevations of the terrain within this tile. */
adjustHeights(minHeight, maxHeight) {
if (undefined === this._heightRange)
this._heightRange = Range1d.createXX(minHeight, maxHeight);
else {
this._heightRange.low = Math.max(expectDefined(this.heightRange).low, minHeight);
this._heightRange.high = Math.min(expectDefined(this.heightRange).high, maxHeight);
}
if (this.rangeCorners && this._patch instanceof PlanarTilePatch)
this._patch.getRangeCorners(expectDefined(this.heightRange), this.rangeCorners);
}
/** Obtain a [[MapTileProjection]] to project positions within this tile's area into 3d space. */
getProjection(heightRange) {
return this._patch instanceof PlanarTilePatch ? new PlanarProjection(this._patch, heightRange) : new EllipsoidProjection(this._patch, heightRange);
}
/** @internal */
get baseImageryIsReady() {
if (undefined !== this.mapTree.baseColor || 0 === this.mapTree.layerHandler.layerImageryTrees.length)
return true;
if (undefined === this._imageryTiles)
return false;
const baseTreeId = this.mapTree.layerHandler.layerImageryTrees[0].tree.modelId;
return this._imageryTiles.every((imageryTile) => imageryTile.imageryTree.modelId !== baseTreeId || imageryTile.isReady);
}
/** @internal */
get imageryIsReady() {
if (undefined === this._imageryTiles)
return 0 === this.mapTree.layerHandler.layerImageryTrees.length;
return this._imageryTiles.every((tile) => tile.isReady);
}
/** Select secondary (imagery) tiles
* @internal
*/
selectSecondaryTiles(args, context) {
if (0 === this.mapTree.layerHandler.layerImageryTrees.length || this.imageryIsReady)
return;
this.clearImageryTiles();
this._imageryTiles = new Array();
this._hiddenTiles = new Array();
this._highResolutionReplacementTiles = new Array();
for (const layerImageryTree of this.mapTree.layerHandler.layerImageryTrees) {
let tmpTiles = new Array();
const tmpLeafTiles = new Array();
if (TileTreeLoadStatus.Loaded !== layerImageryTree.tree.selectCartoDrapeTiles(tmpTiles, tmpLeafTiles, this, args)) {
this._imageryTiles = undefined;
return;
}
// When the base layer is zoomed-in beyond it's max resolution,
// we display leaf tiles and stretched them if needed.
// We don't want the same behavior non-base layers, in the case,
// the layer will simply disappear past its max resolution.
// Note: Replacement leaf tiles are kept as a mean to determine which
// imagery tree has reached it's maximum zoom level.
if (layerImageryTree.baseImageryLayer) {
tmpTiles = [...tmpTiles, ...tmpLeafTiles];
}
else {
this._highResolutionReplacementTiles = [...this._highResolutionReplacementTiles, ...tmpLeafTiles];
}
// MapTileTree might include a non-visible imagery tree, we need to check for that.
if (layerImageryTree.settings.visible && !layerImageryTree.settings.allSubLayersInvisible) {
for (const imageryTile of tmpTiles) {
imageryTile.markMapTileUsage();
if (imageryTile.isReady)
args.markReady(imageryTile);
else
context.missing.push(imageryTile);
this._imageryTiles.push(imageryTile);
}
}
else {
// Even though those selected imagery tile are not visible,
// we keep track of them for scale range reporting.
for (const imageryTile of tmpTiles) {
this._hiddenTiles.push(imageryTile);
}
}
}
}
static _scratchRectangle1 = MapCartoRectangle.createZero();
static _scratchRectangle2 = MapCartoRectangle.createZero();
/** The height range for terrain tiles is not known until the tiles are unloaded. We use "ApproximateTerrainHeight" for first 6 levels but below
* that the tiles inherit height range from parents. This is problematic as tiles with large height range will be unnecessarily selected as
* they apparently intersect view frustum. To avoid this force loading of terrain tiles if they exceed "_maxParentHightDepth".
* @internal
*/
forceSelectRealityTile() {
let parentHeightDepth = 0;
// eslint-disable-next-line @typescript-eslint/no-this-alias
for (let parent = this; parent !== undefined && parent._heightRange === undefined; parent = parent.parent)
parentHeightDepth++;
return parentHeightDepth > MapTile._maxParentHeightDepth;
}
/** @internal */
minimumVisibleFactor() {
// if minimumVisibleFactor is more than 0, it stops parents from loading when children are not ready, to fill in gaps
return 0.0;
}
static _scratchThisDiagonal = Vector2d.create();
static _scratchDrapeDiagonal = Vector2d.create();
/** @internal */
getDrapeTextures() {
if (undefined === this._imageryTiles)
return undefined;
const drapeTextures = [];
const thisRectangle = this.loadableTerrainTile.rectangle;
const thisDiagonal = thisRectangle.diagonal(MapTile._scratchThisDiagonal);
const bordersNorthPole = this.quadId.bordersNorthPole(this.mapTree.sourceTilingScheme);
const bordersSouthPole = this.quadId.bordersSouthPole(this.mapTree.sourceTilingScheme);
for (const imageryTile of this._imageryTiles) {
if (imageryTile.texture) {
drapeTextures.push(this.computeDrapeTexture(thisRectangle, thisDiagonal, imageryTile, imageryTile.rectangle));
if ((bordersNorthPole && imageryTile.quadId.bordersNorthPole(imageryTile.tilingScheme) && imageryTile.rectangle.high.y < thisRectangle.high.y) ||
(bordersSouthPole && imageryTile.quadId.bordersSouthPole(imageryTile.tilingScheme) && imageryTile.rectangle.low.y > thisRectangle.low.y)) {
// Add separate texture stretching last sliver of tile imagery to cover pole.
const sliverRectangle = imageryTile.rectangle.clone(MapTile._scratchRectangle1);
const clipRectangle = thisRectangle.clone(MapTile._scratchRectangle2);
const sliverHeight = sliverRectangle.high.y - sliverRectangle.low.y;
if (bordersSouthPole) {
clipRectangle.high.y = sliverRectangle.low.y;
sliverRectangle.low.y = thisRectangle.low.y;
sliverRectangle.high.y += 1 / sliverHeight;
}
else {
clipRectangle.low.y = sliverRectangle.high.y;
sliverRectangle.high.y = thisRectangle.high.y;
sliverRectangle.low.y -= 1 / sliverHeight;
}
drapeTextures.push(this.computeDrapeTexture(thisRectangle, thisDiagonal, imageryTile, sliverRectangle, clipRectangle));
}
}
else {
for (let parent = imageryTile.parent; undefined !== parent; parent = parent.parent) {
const mapTile = parent;
if (mapTile.texture) {
drapeTextures.push(this.computeDrapeTexture(thisRectangle, thisDiagonal, mapTile, mapTile.rectangle, imageryTile.rectangle));
break;
}
}
}
}
return drapeTextures.length > 0 ? drapeTextures : undefined;
}
static _scratchIntersectRange = Range2d.createNull();
computeDrapeTexture(thisRectangle, thisDiagonal, imageryTile, drapeRectangle, clipRectangle) {
assert(imageryTile.texture !== undefined);
// Compute transformation from the terrain tile texture coordinates (0-1) to the drape tile texture coordinates.
const drapeDiagonal = drapeRectangle.diagonal(MapTile._scratchDrapeDiagonal);
const translate = Vector2d.create((thisRectangle.low.x - drapeRectangle.low.x) / drapeDiagonal.x, (thisRectangle.low.y - drapeRectangle.low.y) / drapeDiagonal.y);
const scale = Vector2d.create(thisDiagonal.x / drapeDiagonal.x, thisDiagonal.y / drapeDiagonal.y);
const featureIndex = this.mapLoader.getFeatureIndex(imageryTile.imageryTree.modelId);
let clipRect;
if (undefined !== clipRectangle) {
const intersect = clipRectangle.intersect(drapeRectangle, MapTile._scratchIntersectRange);
assert(!intersect.isNull);
clipRect = Range2d.createXYXY((intersect.low.x - drapeRectangle.low.x) / drapeDiagonal.x, (intersect.low.y - drapeRectangle.low.y) / drapeDiagonal.y, (intersect.high.x - drapeRectangle.low.x) / drapeDiagonal.x, (intersect.high.y - drapeRectangle.low.y) / drapeDiagonal.y);
}
const imageryModelId = imageryTile.tree.modelId;
return new TerrainTexture(imageryTile.texture, featureIndex, scale, translate, drapeRectangle, this.mapTree.getLayerIndex(imageryModelId), this.mapTree.getLayerTransparency(imageryModelId), clipRect);
}
/** @internal */
setContent(content) {
if (this.quadId.level < this.maxDepth) {
const childIds = this.quadId.getChildIds();
for (const childId of childIds) {
if (!this.mapLoader.isTileAvailable(childId)) {
this._mesh = content.terrain?.mesh; // If a child is unavailable retain mesh for upsampling.
break;
}
}
}
if (this.mapTree.produceGeometry) {
const iModelTransform = this.mapTree.iModelTransform;
const geometryTransform = content.terrain?.renderGeometry?.transform;
const transform = geometryTransform ? iModelTransform.multiplyTransformTransform(geometryTransform) : iModelTransform;
const polyface = content.terrain?.mesh ? RealityMeshParams.toPolyface(content.terrain.mesh, { transform }) : undefined;
this._geometry = polyface ? { polyfaces: [polyface] } : undefined;
}
else {
dispose(this._renderGeometry);
this._renderGeometry = content.terrain?.renderGeometry;
}
this.everLoaded = true;
if (undefined !== content.contentRange)
this._contentRange = content.contentRange;
this.setIsReady();
}
/** @internal */
freeMemory() {
// ###TODO MapTiles and ImageryMapTiles share resources and don't currently interact well with TileAdmin.freeMemory(). Opt out for now.
}
/** @internal */
disposeContents() {
super.disposeContents();
this._renderGeometry = dispose(this._renderGeometry);
this.clearImageryTiles();
// Note - don't dispose of mesh - these should only ever exist on terrain leaf tile and are required by children. Let garbage collector handle them.
}
}
/** A child tile that has no content of its own available. It instead produces content by up-sampling the content of an ancestor tile.
* @internal
*/
export class UpsampledMapTile extends MapTile {
/** The ancestor tile whose content will be up-sampled. */
_loadableTile;
constructor(params, mapTree, quadId, patch, rectangle, heightRange, cornerRays, loadableTile) {
super(params, mapTree, quadId, patch, rectangle, heightRange, cornerRays);
this._loadableTile = loadableTile;
}
get isUpsampled() { return true; }
get isEmpty() { return false; }
get loadableTile() { return this._loadableTile; }
upsampleFromParent() {
const parent = this.loadableTerrainTile;
const parentMesh = parent.mesh;
if (undefined === parentMesh) {
return undefined;
}
const thisId = this.quadId, parentId = parent.quadId;
const levelDelta = thisId.level - parentId.level;
const thisColumn = thisId.column - (parentId.column << levelDelta);
const thisRow = thisId.row - (parentId.row << levelDelta);
const scale = 1.0 / (1 << levelDelta);
const parentParameterRange = Range2d.createXYXY(scale * thisColumn, scale * thisRow, scale * (thisColumn + 1), scale * (thisRow + 1));
const upsample = upsampleRealityMeshParams(parentMesh, parentParameterRange);
this.adjustHeights(upsample.heightRange.low, upsample.heightRange.high);
return upsample;
}
get renderGeometry() {
if (undefined === this._renderGeometry) {
const upsample = this.upsampleFromParent();
const projection = this.loadableTerrainTile.getProjection(this.heightRange);
if (upsample)
this._renderGeometry = IModelApp.renderSystem.createTerrainMesh(upsample.mesh, projection.transformFromLocal, true);
}
return this._renderGeometry;
}
get isLoading() { return this.loadableTile.isLoading; }
get isQueued() { return this.loadableTile.isQueued; }
get isNotFound() { return this.loadableTile.isNotFound; }
get isReady() { return (this._renderGeometry !== undefined || this.loadableTile.loadStatus === TileLoadStatus.Ready) && this.baseImageryIsReady; }
markUsed(args) {
args.markUsed(this);
args.markUsed(this.loadableTile);
}
}
//# sourceMappingURL=MapTile.js.map