UNPKG

@itwin/core-frontend

Version:
410 lines • 20.5 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Tiles */ import { assert, compareBooleans, compareNumbers, compareSimpleArrays, compareSimpleTypes, compareStrings, compareStringsOrUndefined, dispose, expectDefined, Logger, } from "@itwin/core-bentley"; import { Angle, Range3d, Transform } from "@itwin/core-geometry"; import { ImageSource, RenderTexture } from "@itwin/core-common"; import { IModelApp } from "../../IModelApp"; import { MapCartoRectangle, MapLayerTileTreeReference, MapTileTreeScaleRangeVisibility, QuadId, RealityTile, RealityTileLoader, RealityTileTree, TileLoadPriority, TileTreeLoadStatus, } from "../internal"; const loggerCategory = "ImageryMapTileTree"; /** @internal */ export class ImageryMapTile extends RealityTile { imageryTree; quadId; rectangle; _texture; _mapTileUsageCount = 0; _outOfLodRange; constructor(params, imageryTree, quadId, rectangle) { super(params, imageryTree); this.imageryTree = imageryTree; this.quadId = quadId; this.rectangle = rectangle; this._outOfLodRange = this.depth < imageryTree.minDepth; } get texture() { return this._texture; } get tilingScheme() { return this.imageryTree.tilingScheme; } get isDisplayable() { return (this.depth > 1) && super.isDisplayable; } get isOutOfLodRange() { return this._outOfLodRange; } setContent(content) { this._texture = content.imageryTexture; // No dispose - textures may be shared by terrain tiles so let garbage collector dispose them. if (undefined === content.imageryTexture) expectDefined(this.parent).setLeaf(); // Avoid traversing bing branches after no graphics is found. this.setIsReady(); } selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, rectangleToDrape, drapePixelSize, args) { // Base draping overlap on width rather than height so that tiling schemes with multiple root nodes overlay correctly. const isSmallerThanDrape = (this.rectangle.xLength() / this.maximumSize) < drapePixelSize; if ((this.isLeaf) // Include leaves so tiles get stretched past max LOD levels. (Only for base imagery layer) || isSmallerThanDrape || this._anyChildNotFound) { if (this.isOutOfLodRange) { drapeTiles.push(this); this.setIsReady(); } else if (this.isLeaf && !isSmallerThanDrape && !this._anyChildNotFound) { // These tiles are selected because we are beyond the max LOD of the tile tree, // might be used to display "stretched" tiles instead of having blank. highResolutionReplacementTiles.push(this); } else { drapeTiles.push(this); } return TileTreeLoadStatus.Loaded; } let status = this.loadChildren(); if (TileTreeLoadStatus.Loading === status) { args.markChildrenLoading(); } else if (TileTreeLoadStatus.Loaded === status) { if (undefined !== this.children) { for (const child of this.children) { const mapChild = child; if (mapChild.rectangle.intersectsRange(rectangleToDrape)) status = mapChild.selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, rectangleToDrape, drapePixelSize, args); if (TileTreeLoadStatus.Loaded !== status) break; } } } return status; } markMapTileUsage() { this._mapTileUsageCount++; } releaseMapTileUsage() { assert(!this._texture || this._mapTileUsageCount > 0); if (this._mapTileUsageCount) this._mapTileUsageCount--; } /** @internal */ setLeaf() { // Don't potentially re-request the children later. this.disposeChildren(); this._isLeaf = true; this._childrenLoadStatus = TileTreeLoadStatus.Loaded; } _loadChildren(resolve, _reject) { const imageryTree = this.imageryTree; const resolveChildren = (childIds) => { const children = new Array(); const childrenAreLeaves = (this.depth + 1) === imageryTree.maxDepth; // If children depth is lower than min LOD, mark them as disabled. // This is important: if those tiles are requested and the server refuse to serve them, // they will be marked as not found and their descendant will never be displayed. childIds.forEach((quadId) => { const rectangle = imageryTree.tilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level); const range = Range3d.createXYZXYZ(rectangle.low.x, rectangle.low.x, 0, rectangle.high.x, rectangle.high.y, 0); const maximumSize = imageryTree.imageryLoader.maximumScreenSize; const tile = new ImageryMapTile({ parent: this, isLeaf: childrenAreLeaves, contentId: quadId.contentId, range, maximumSize }, imageryTree, quadId, rectangle); children.push(tile); }); resolve(children); }; imageryTree.imageryLoader.generateChildIds(this, resolveChildren); } _collectStatistics(stats) { super._collectStatistics(stats); if (this._texture) stats.addTexture(this._texture.bytesUsed); } freeMemory() { // ###TODO MapTiles and ImageryMapTiles share resources and don't currently interact well with TileAdmin.freeMemory(). Opt out for now. } disposeContents() { if (0 === this._mapTileUsageCount) { super.disposeContents(); this.disposeTexture(); } } disposeTexture() { this._texture = dispose(this._texture); } [Symbol.dispose]() { this._mapTileUsageCount = 0; super[Symbol.dispose](); } } /** Object that holds various state values for an ImageryTileTree * @internal */ export class ImageryTileTreeState { _scaleRangeVis; constructor() { this._scaleRangeVis = MapTileTreeScaleRangeVisibility.Unknown; } /** Get the scale range visibility of the imagery tile tree. * @returns the scale range visibility of the imagery tile tree. */ getScaleRangeVisibility() { return this._scaleRangeVis; } /** Makes a deep copy of the current object. */ clone() { const clone = new ImageryTileTreeState(); clone._scaleRangeVis = this._scaleRangeVis; return clone; } /** Reset the scale range visibility of imagery tile tree (i.e. unknown) */ reset() { this._scaleRangeVis = MapTileTreeScaleRangeVisibility.Unknown; } /** Sets the scale range visibility of the current imagery tile tree. * The state will be derived based on the previous visibility values: * Initial state: 'Unknown' * The first call will set the state to either: 'Visible' or 'Hidden'. * If subsequent visibility values are not consistent with the first visibility state, the state become 'Partial', * meaning the imagery tree currently contains a mixed of tiles being in range and out of range. */ setScaleRangeVisibility(visible) { if (this._scaleRangeVis === MapTileTreeScaleRangeVisibility.Unknown) { this._scaleRangeVis = (visible ? MapTileTreeScaleRangeVisibility.Visible : MapTileTreeScaleRangeVisibility.Hidden); } else if ((visible && this._scaleRangeVis === MapTileTreeScaleRangeVisibility.Hidden) || (!visible && this._scaleRangeVis === MapTileTreeScaleRangeVisibility.Visible)) { this._scaleRangeVis = MapTileTreeScaleRangeVisibility.Partial; } } } /** @internal */ export class ImageryMapTileTree extends RealityTileTree { _imageryLoader; constructor(params, _imageryLoader) { super(params); this._imageryLoader = _imageryLoader; const rootQuadId = new QuadId(_imageryLoader.imageryProvider.tilingScheme.rootLevel, 0, 0); this._rootTile = new ImageryMapTile(params.rootTile, this, rootQuadId, this.getTileRectangle(rootQuadId)); } get tilingScheme() { return this._imageryLoader.imageryProvider.tilingScheme; } /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */ addLogoCards(cards, vp) { // eslint-disable-next-line @typescript-eslint/no-deprecated this._imageryLoader.addLogoCards(cards, vp); } async addAttributions(cards, vp) { return this._imageryLoader.addAttributions(cards, vp); } getTileRectangle(quadId) { return this.tilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level); } get imageryLoader() { return this._imageryLoader; } get is3d() { assert(false); return false; } get viewFlagOverrides() { assert(false); return {}; } get isContentUnbounded() { assert(false); return true; } _selectTiles(_args) { assert(false); return []; } draw(_args) { assert(false); } static _scratchDrapeRectangle = MapCartoRectangle.createZero(); static _drapeIntersectionScale = 1.0 - 1.0E-5; selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, tileToDrape, args) { const drapeRectangle = tileToDrape.rectangle.clone(ImageryMapTileTree._scratchDrapeRectangle); // Base draping overlap on width rather than height so that tiling schemes with multiple root nodes overlay correctly. const drapePixelSize = 1.05 * tileToDrape.rectangle.xLength() / tileToDrape.maximumSize; drapeRectangle.scaleAboutCenterInPlace(ImageryMapTileTree._drapeIntersectionScale); // Contract slightly to avoid draping adjacent or slivers. return this.rootTile.selectCartoDrapeTiles(drapeTiles, highResolutionReplacementTiles, drapeRectangle, drapePixelSize, args); } cartoRectangleFromQuadId(quadId) { return this.tilingScheme.tileXYToRectangle(quadId.column, quadId.row, quadId.level); } } class ImageryTileLoader extends RealityTileLoader { _imageryProvider; _iModel; constructor(_imageryProvider, _iModel) { super(); this._imageryProvider = _imageryProvider; this._iModel = _iModel; } computeTilePriority(tile) { return 25 * (this._imageryProvider.usesCachedTiles ? 2 : 1) - tile.depth; // Always cached first then descending by depth (high resolution/front first) } // Prioritized fast, cached tiles first. get maxDepth() { return this._imageryProvider.maximumZoomLevel; } get minDepth() { return this._imageryProvider.minimumZoomLevel; } get priority() { return TileLoadPriority.Map; } /** @deprecated in 5.0 - will not be removed until after 2026-06-13. Use [addAttributions] instead. */ addLogoCards(cards, vp) { // eslint-disable-next-line @typescript-eslint/no-deprecated this._imageryProvider.addLogoCards(cards, vp); } async addAttributions(cards, vp) { await this._imageryProvider.addAttributions(cards, vp); } get maximumScreenSize() { return this._imageryProvider.maximumScreenSize; } get imageryProvider() { return this._imageryProvider; } async getToolTip(strings, quadId, carto, tree) { await this._imageryProvider.getToolTip(strings, quadId, carto, tree); } async getMapFeatureInfo(featureInfos, quadId, carto, tree, hit, options) { await this._imageryProvider.getFeatureInfo(featureInfos, quadId, carto, tree, hit, options); } generateChildIds(tile, resolveChildren) { return this._imageryProvider.generateChildIds(tile, resolveChildren); } /** Load this tile's children, possibly asynchronously. Pass them to `resolve`, or an error to `reject`. */ async loadChildren(_tile) { assert(false); return undefined; } async requestTileContent(tile, _isCanceled) { const quadId = QuadId.createFromContentId(tile.contentId); return this._imageryProvider.loadTile(quadId.row, quadId.column, quadId.level); } getRequestChannel(_tile) { // ###TODO use hostname from url - but so many layers to go through to get that... return IModelApp.tileAdmin.channels.getForHttp("itwinjs-imagery"); } async loadTileContent(tile, data, system) { assert(data instanceof ImageSource); assert(tile instanceof ImageryMapTile); const content = {}; const texture = await this.loadTextureImage(data, system); if (undefined === texture) return content; content.imageryTexture = texture; return content; } async loadTextureImage(source, system) { try { return await system.createTextureFromSource({ type: RenderTexture.Type.FilteredTileSection, source, }); } catch { return undefined; } } } /** Supplies a TileTree that can load and draw tiles based on our imagery provider. * The TileTree is uniquely identified by its imagery type. */ class ImageryMapLayerTreeSupplier { /** Return a numeric value indicating how two tree IDs are ordered relative to one another. * This allows the ID to serve as a lookup key to find the corresponding TileTree. */ compareTileTreeIds(lhs, rhs) { let cmp = compareStrings(lhs.settings.formatId, rhs.settings.formatId); if (0 === cmp) { cmp = compareStrings(lhs.settings.url, rhs.settings.url); if (0 === cmp) { cmp = compareStringsOrUndefined(lhs.settings.userName, rhs.settings.userName); if (0 === cmp) { cmp = compareStringsOrUndefined(lhs.settings.password, rhs.settings.password); if (0 === cmp) { cmp = compareBooleans(lhs.settings.transparentBackground, rhs.settings.transparentBackground); if (0 === cmp) { if (lhs.settings.properties || rhs.settings.properties) { if (lhs.settings.properties && rhs.settings.properties) { const lhsKeysLength = Object.keys(lhs.settings.properties).length; const rhsKeysLength = Object.keys(rhs.settings.properties).length; if (lhsKeysLength !== rhsKeysLength) { cmp = lhsKeysLength - rhsKeysLength; } else { for (const key of Object.keys(lhs.settings.properties)) { const lhsProp = lhs.settings.properties[key]; const rhsProp = rhs.settings.properties[key]; cmp = compareStrings(typeof lhsProp, typeof rhsProp); if (0 !== cmp) break; if (Array.isArray(lhsProp) || Array.isArray(rhsProp)) { cmp = compareSimpleArrays(lhsProp, rhsProp); if (0 !== cmp) break; } else { cmp = compareSimpleTypes(lhsProp, rhsProp); if (0 !== cmp) break; } } } } else if (!lhs.settings.properties) { cmp = 1; } else { cmp = -1; } } if (0 === cmp) { cmp = compareNumbers(lhs.settings.subLayers.length, rhs.settings.subLayers.length); if (0 === cmp) { for (let i = 0; i < lhs.settings.subLayers.length && 0 === cmp; i++) { cmp = compareStrings(lhs.settings.subLayers[i].name, rhs.settings.subLayers[i].name); if (0 === cmp) { cmp = compareBooleans(lhs.settings.subLayers[i].visible, rhs.settings.subLayers[i].visible); } } } } } } } } } return cmp; } /** The first time a tree of a particular imagery type is requested, this function creates it. */ async createTileTree(id, iModel) { const imageryProvider = IModelApp.mapLayerFormatRegistry.createImageryProvider(id.settings); if (undefined === imageryProvider) { Logger.logError(loggerCategory, `Failed to create imagery provider for format '${id.settings.formatId}'`); return undefined; } try { await imageryProvider.initialize(); } catch (e) { Logger.logError(loggerCategory, `Could not initialize imagery provider for map layer '${id.settings.name}' : ${e}`); throw e; } const modelId = iModel.transientIds.getNext(); const tilingScheme = imageryProvider.tilingScheme; const rootLevel = (1 === tilingScheme.numberOfLevelZeroTilesX && 1 === tilingScheme.numberOfLevelZeroTilesY) ? 0 : -1; const rootTileId = new QuadId(rootLevel, 0, 0).contentId; const rootRange = Range3d.createXYZXYZ(-Angle.piRadians, -Angle.piOver2Radians, 0, Angle.piRadians, Angle.piOver2Radians, 0); const rootTileProps = { contentId: rootTileId, range: rootRange, maximumSize: 0 }; const loader = new ImageryTileLoader(imageryProvider, iModel); const treeProps = { rootTile: rootTileProps, id: modelId, modelId, iModel, location: Transform.createIdentity(), priority: TileLoadPriority.Map, loader, gcsConverterAvailable: false }; return new ImageryMapTileTree(treeProps, loader); } } const imageryTreeSupplier = new ImageryMapLayerTreeSupplier(); /** A reference to one of our tile trees. The specific TileTree drawn may change when the desired imagery type or target iModel changes. * @beta */ export class ImageryMapLayerTreeReference extends MapLayerTileTreeReference { /** * Constructor for an ImageryMapLayerTreeReference. * @param layerSettings Map layer settings that are applied to the ImageryMapLayerTreeReference. * @param layerIndex The index of the associated map layer. Usually passed in through [[createMapLayerTreeReference]] in [[MapTileTree]]'s constructor. * @param iModel The iModel containing the ImageryMapLayerTreeReference. */ constructor(args) { super(args.layerSettings, args.layerIndex, args.iModel); } get castsShadows() { return false; } /** Return the owner of the TileTree to draw. */ get treeOwner() { return this.iModel.tiles.getTileTreeOwner({ settings: this._layerSettings }, imageryTreeSupplier); } /* @internal */ resetTreeOwner() { return this.iModel.tiles.resetTileTreeOwner({ settings: this._layerSettings }, imageryTreeSupplier); } get imageryProvider() { const tree = this.treeOwner.load(); if (!tree || !(tree instanceof ImageryMapTileTree)) return undefined; return tree.imageryLoader.imageryProvider; } } //# sourceMappingURL=ImageryTileTree.js.map