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