UNPKG

@itwin/core-frontend

Version:
727 lines • 36.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, 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