@itwin/core-frontend
Version:
iTwin.js frontend components
857 lines • 69.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 IModelConnection
*/
import { assert, BeEvent, expectDefined, GeoServiceStatus, Id64, IModelStatus, Logger, OneAtATimeAction, OpenMode, TransientIdSequence, } from "@itwin/core-bentley";
import { Cartographic, CodeScopeSpec, CodeSpec, EcefLocation, ECSqlReader, FontMap, GeoCoordStatus, IModel, IModelError, IModelReadRpcInterface, mapToGeoServiceStatus, Placement2d, Placement3d, QueryBinder, QueryRowFormat, RpcManager, SnapshotIModelRpcInterface, ViewStoreRpc, } from "@itwin/core-common";
import { Point3d, Range3d, Transform } from "@itwin/core-geometry";
import { FrontendLoggerCategory } from "./common/FrontendLoggerCategory";
import { GeoServices } from "./GeoServices";
import { IModelApp } from "./IModelApp";
import { IModelRoutingContext } from "./IModelRoutingContext";
import { ModelState } from "./ModelState";
import { HiliteSet, SelectionSet } from "./SelectionSet";
import { SubCategoriesCache } from "./SubCategoriesCache";
import { BingElevationProvider } from "./tile/internal";
import { Tiles } from "./Tiles";
import { ViewState } from "./ViewState";
import { _requestSnap } from "./common/internal/Symbols";
import { IpcApp } from "./IpcApp";
import { SchemaContext } from "@itwin/ecschema-metadata";
import { ECSchemaRpcLocater, RpcIncrementalSchemaLocater } from '@itwin/ecschema-rpcinterface-common';
const loggerCategory = FrontendLoggerCategory.IModelConnection;
/** A connection to a [IModelDb]($backend) hosted on the backend.
* @public
* @extensions
*/
export class IModelConnection extends IModel {
/** The [[ModelState]]s in this IModelConnection. */
models;
/** The [[ElementState]]s in this IModelConnection. */
elements;
/** The [[CodeSpec]]s in this IModelConnection. */
codeSpecs;
/** The [[ViewState]]s in this IModelConnection. */
views;
/** The set of currently hilited elements for this IModelConnection. */
hilited;
/** The set of currently selected elements for this IModelConnection. */
selectionSet;
/** The set of Tiles for this IModelConnection. */
tiles;
/** The set of [Category]($backend)'s in this IModelConnection. */
categories;
/** A cache of information about SubCategories chiefly used for rendering.
* @internal
*/
get subcategories() { return this.categories.cache; }
/** Generator for unique Ids of transient graphics for this IModelConnection. */
transientIds = new TransientIdSequence();
/** The Geographic location services available for this iModelConnection. */
geoServices;
/** @internal Whether GCS has been disabled for this iModelConnection. */
_gcsDisabled = false;
/** @internal Return true if a GCS is not defined for this iModelConnection; also returns true if GCS is defined but disabled. */
get noGcsDefined() { return this._gcsDisabled || undefined === this.geographicCoordinateSystem; }
/** @internal */
disableGCS(disable) { this._gcsDisabled = disable; }
/** The maximum time (in milliseconds) to wait before timing out the request to open a connection to a new iModel */
static connectionTimeout = 10 * 60 * 1000;
/** The RPC routing for this connection. */
routingContext = IModelRoutingContext.default;
/** Type guard for instanceof [[BriefcaseConnection]] */
isBriefcaseConnection() { return false; }
/** Type guard for instanceof [[CheckpointConnection]]
* @beta
*/
isCheckpointConnection() { return false; }
/** Type guard for instanceof [[SnapshotConnection]] */
isSnapshotConnection() { return false; }
/** Type guard for instanceof [[BlankConnection]] */
isBlankConnection() { return false; }
/** Returns `true` if this is a briefcase copy of an iModel that is synchronized with iModelHub. */
get isBriefcase() { return this.isBriefcaseConnection(); }
/** Returns `true` if this is a *snapshot* iModel.
* @see [[SnapshotConnection.openSnapshot]]
*/
get isSnapshot() { return this.isSnapshotConnection(); }
/** True if this is a [Blank Connection]($docs/learning/frontend/BlankConnection). */
get isBlank() { return this.isBlankConnection(); }
/** Check the [[openMode]] of this IModelConnection to see if it was opened read-only. */
get isReadonly() { return this.openMode === OpenMode.Readonly; }
/** Check if the IModelConnection is open (i.e. it has a *connection* to a backend server).
* Returns false for [[BlankConnection]] instances and after [[IModelConnection.close]] has been called.
* @note no RPC operations are valid on this IModelConnection if this method returns false.
*/
get isOpen() { return !this.isClosed; }
/** Event raised immediately before *any* IModelConnection is [[close]]d.
* @note This static event is raised when *any* IModelConnection is closed, and the specific IModelConnection is passed as its argument. To
* monitor closing a specific IModelConnection, listen for the `onClose` instance event instead.
* @note Be careful not to perform any asynchronous operations on the IModelConnection because it will close before they are processed.
*/
static onClose = new BeEvent();
/** Event called immediately after *any* IModelConnection is opened. */
static onOpen = new BeEvent();
/** Event raised immediately before this IModelConnection is [[close]]d.
* @note This event is raised only for this specific IModelConnection. To monitor *all* IModelConnections, listen for the static `onClose` event instead.
* @note Be careful not to perform any asynchronous operations on the IModelConnection because it will close before they are processed.
*/
onClose = new BeEvent();
/** The font map for this IModelConnection. Only valid after calling #loadFontMap and waiting for the returned promise to be fulfilled.
* @deprecated in 5.0.0 - will not be removed until after 2026-06-13. If you need font Ids on the front-end for some reason, write an Ipc method that queries [IModelDb.fonts]($backend).
*/
fontMap; // eslint-disable-line @typescript-eslint/no-deprecated
_schemaContext;
/** Load the FontMap for this IModelConnection.
* @returns Returns a Promise<FontMap> that is fulfilled when the FontMap member of this IModelConnection is valid.
* @deprecated in 5.0.0 - will not be removed until after 2026-06-13. If you need font Ids on the front-end for some reason, write an Ipc method that queries [IModelDb.fonts]($backend).
*/
async loadFontMap() {
if (undefined === this.fontMap) { // eslint-disable-line @typescript-eslint/no-deprecated
this.fontMap = new FontMap(); // eslint-disable-line @typescript-eslint/no-deprecated
if (this.isOpen) {
const fontProps = await IModelReadRpcInterface.getClientForRouting(this.routingContext.token).readFontJson(this.getRpcProps());
this.fontMap.addFonts(fontProps.fonts); // eslint-disable-line @typescript-eslint/no-deprecated
}
}
return this.fontMap; // eslint-disable-line @typescript-eslint/no-deprecated
}
/** Find the first registered base class of the given EntityState className. This class will "handle" the State for the supplied className.
* @param className The full name of the class of interest.
* @param defaultClass If no base class of the className is registered, return this value.
* @note this method is async since it may have to query the server to get the class hierarchy.
*/
async findClassFor(className, defaultClass) {
let ctor = IModelApp.lookupEntityClass(className);
if (undefined !== ctor)
return ctor;
// it's not registered, we need to query its class hierarchy.
// wait until we get the full list of base classes from backend
if (this.isOpen) {
const baseClasses = await IModelReadRpcInterface.getClientForRouting(this.routingContext.token).getClassHierarchy(this.getRpcProps(), className);
// Make sure some other async code didn't register this class while we were await-ing above
ctor = IModelApp.lookupEntityClass(className);
if (undefined !== ctor)
return ctor;
// walk through the list until we find a registered base class
baseClasses.some((baseClass) => {
const test = IModelApp.lookupEntityClass(baseClass);
if (test === undefined)
return false; // nope, not registered
ctor = test; // found it, save it
IModelApp.registerEntityState(className, ctor); // and register the fact that our starting class is handled by this subclass.
return true; // stop
});
}
return ctor ?? defaultClass; // either the baseClass handler or defaultClass if we didn't find a registered baseClass
}
/** @internal */
constructor(iModelProps) {
super(iModelProps);
super.initialize(iModelProps.name ?? "<undefined>", iModelProps);
this.models = new IModelConnection.Models(this);
this.elements = new IModelConnection.Elements(this);
this.codeSpecs = new IModelConnection.CodeSpecs(this);
this.views = new IModelConnection.Views(this);
this.categories = new IModelConnection.Categories(this);
this.selectionSet = new SelectionSet(this);
this.hilited = new HiliteSet(this);
this.tiles = new Tiles(this);
this.geoServices = GeoServices.createForIModel(this);
this.hilited.onModelSubCategoryModeChanged.addListener(() => {
IModelApp.viewManager.onSelectionSetChanged(this);
});
}
/** Called prior to connection closing. Raises close events and calls tiles.dispose.
* @internal
*/
beforeClose() {
this.onClose.raiseEvent(this); // event for this connection
IModelConnection.onClose.raiseEvent(this); // event for all connections
this.tiles[Symbol.dispose]();
this.subcategories.onIModelConnectionClose();
}
/** Allow to execute query and read results along with meta data. The result are streamed.
*
* See also:
* - [ECSQL Overview]($docs/learning/frontend/ExecutingECSQL)
* - [Code Examples]($docs/learning/frontend/ECSQLCodeExamples)
* - [ECSQL Row Format]($docs/learning/ECSQLRowFormat)
*
* @param params The values to bind to the parameters (if the ECSQL has any).
* @param config Allow to specify certain flags which control how query is executed.
* @returns Returns an [ECSqlReader]($common) which helps iterate over the result set and also give access to metadata.
* @public
* */
createQueryReader(ecsql, params, config) {
const executor = {
execute: async (request) => {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).queryRows(this.getRpcProps(), request);
},
};
return new ECSqlReader(executor, ecsql, params, config);
}
/**
* queries the BisCore.SubCategory table for the entries that are children of the passed categoryIds
* @param compressedCategoryIds compressed category Ids
* @returns array of SubCategoryResultRow
* @internal
*/
async querySubCategories(compressedCategoryIds) {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).querySubCategories(this.getRpcProps(), compressedCategoryIds);
}
/**
* queries the BisCore.SubCategory table for entries that are children of used spatial categories and 3D elements.
* @returns array of SubCategoryResultRow
* @internal
*/
async queryAllUsedSpatialSubCategories() {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).queryAllUsedSpatialSubCategories(this.getRpcProps());
}
/** Query for a set of element ids that satisfy the supplied query params
* @param params The query parameters. The `limit` and `offset` members should be used to page results.
* @throws [IModelError]($common) If the generated statement is invalid or would return too many rows.
*/
async queryEntityIds(params) {
return new Set(this.isOpen ? await IModelReadRpcInterface.getClientForRouting(this.routingContext.token).queryEntityIds(this.getRpcProps(), params) : undefined);
}
_snapRpc = new OneAtATimeAction(async (props) => IModelReadRpcInterface.getClientForRouting(this.routingContext.token).requestSnap(this.getRpcProps(), IModelApp.sessionId, props));
/** Request a snap from the backend.
* @note callers must gracefully handle Promise rejected with AbandonedError
* @internal
*/
async [_requestSnap](props) {
return this.isOpen ? this._snapRpc.request(props) : { status: 2 };
}
/** @internal
* @deprecated in 4.8 - will not be removed until after 2026-06-13. Use AccuSnap.doSnapRequest.
*/
async requestSnap(props) {
return this[_requestSnap](props);
}
_toolTipRpc = new OneAtATimeAction(async (id) => IModelReadRpcInterface.getClientForRouting(this.routingContext.token).getToolTipMessage(this.getRpcProps(), id));
/** Request a tooltip from the backend.
* @note If another call to this method occurs before preceding call(s) return, all preceding calls will be abandoned - only the most recent will resolve. Therefore callers must gracefully handle Promise rejected with AbandonedError.
*/
async getToolTipMessage(id) {
return this.isOpen ? this._toolTipRpc.request(id) : [];
}
/** Request element clip containment status from the backend. */
async getGeometryContainment(requestProps) { return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).getGeometryContainment(this.getRpcProps(), requestProps); }
/** Obtain a summary of the geometry belonging to one or more [GeometricElement]($backend)s suitable for debugging and diagnostics.
* @param requestProps Specifies the elements to query and options for how to format the output.
* @returns A string containing the summary, typically consisting of multiple lines.
* @note Trying to parse the output to programmatically inspect an element's geometry is not recommended.
* @see [GeometryStreamIterator]($common) to more directly inspect a geometry stream.
*/
async getGeometrySummary(requestProps) {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).getGeometrySummary(this.getRpcProps(), requestProps);
}
/** Request a named texture image from the backend.
* @param textureLoadProps The texture load properties which must contain a name property (a valid 64-bit integer identifier). It optionally can contain the maximum texture size supported by the client.
* @see [[Id64]]
* @public
*/
async queryTextureData(textureLoadProps) {
if (this.isOpen) {
const rpcClient = IModelReadRpcInterface.getClientForRouting(this.routingContext.token);
const img = rpcClient.queryTextureData(this.getRpcProps(), textureLoadProps);
return img;
}
return undefined;
}
/** Request element mass properties from the backend. */
async getMassProperties(requestProps) {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).getMassProperties(this.getRpcProps(), requestProps);
}
/** Request mass properties for multiple elements from the backend.
* @deprecated in 4.11 - will not be removed until after 2026-06-13. Use [[IModelConnection.getMassProperties]].
*/
async getMassPropertiesPerCandidate(requestProps) {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).getMassPropertiesPerCandidate(this.getRpcProps(), requestProps);
}
/** Produce encoded [Polyface]($core-geometry)s from the geometry stream of a [GeometricElement]($backend).
* A polyface is produced for each geometric entry in the element's geometry stream, excluding geometry like open curves that can't be converted into polyfaces.
* The polyfaces can be decoded using [readElementMeshes]($common).
* Symbology, UV parameters, and normal vectors are not included in the result.
* @param requestProps A description of how to produce the polyfaces and from which element to obtain them.
* @returns an encoded list of polyfaces that can be decoded by [readElementMeshes]($common).
* @throws Error if [ElementMeshRequestProps.source]($common) does not refer to a [GeometricElement]($backend).
* @note This function is intended to support limited analysis of an element's geometry as a mesh. It is not intended for producing graphics.
* @see [[TileAdmin.requestElementGraphics]] to obtain meshes appropriate for display.
* @beta
*/
async generateElementMeshes(requestProps) {
return IModelReadRpcInterface.getClientForRouting(this.routingContext.token).generateElementMeshes(this.getRpcProps(), requestProps);
}
/** Convert a point in this iModel's Spatial coordinates to a [[Cartographic]] using the Geographic location services for this IModelConnection.
* @param spatial A point in the iModel's spatial coordinates
* @param result If defined, use this for output
* @returns A Cartographic location (Horizontal datum depends on iModel's GCS)
* @throws IModelError if [[isGeoLocated]] is false or point could not be converted.
* @see [[cartographicFromSpatial]] if you have more than one point to convert, or you don't know whether the iModel has a GCS.
*/
async spatialToCartographicFromGcs(spatial, result) {
if (!this.isGeoLocated && this.noGcsDefined)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
const geoConverter = expectDefined(this.geoServices.getConverter());
const coordResponse = await geoConverter.getGeoCoordinatesFromIModelCoordinates([spatial]);
if (1 !== coordResponse.geoCoords.length || GeoCoordStatus.NoGCSDefined === coordResponse.geoCoords[0].s)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (GeoCoordStatus.Success !== coordResponse.geoCoords[0].s) {
const geoServiceStatus = mapToGeoServiceStatus(coordResponse.geoCoords[0].s);
throw new IModelError(geoServiceStatus, "Error converting spatial to cartographic");
}
const longLatHeight = Point3d.fromJSON(coordResponse.geoCoords[0].p); // x is longitude in degrees, y is latitude in degrees, z is height in meters...
return Cartographic.fromDegrees({ longitude: longLatHeight.x, latitude: longLatHeight.y, height: longLatHeight.z }, result);
}
/** Convert a point in this iModel's Spatial coordinates to a [[Cartographic]] using the Geographic location services for this IModelConnection or [[IModel.ecefLocation]].
* @param spatial A point in the iModel's spatial coordinates
* @param result If defined, use this for output
* @returns A Cartographic location (Horizontal datum depends on iModel's GCS)
* @throws IModelError if [[isGeoLocated]] is false or point could not be converted.
* @see [[cartographicFromSpatial]] to convert multiple points at once.
* @see [[spatialToCartographicFromEcef]] to synchronously convert points using the iModel's ECEF transform.
*/
async spatialToCartographic(spatial, result) {
return (this.noGcsDefined ? this.spatialToCartographicFromEcef(spatial, result) : this.spatialToCartographicFromGcs(spatial, result));
}
/** Convert points in this iModel's spatial coordinate system to [Cartographic]($common) coordinates using either a [[GeoConverter]] or the iModel's [EcefLocation]($common).
* @param spatial Coordinates to be converted from the iModel's spatial coordinate system
* @returns The `spatial` coordinates converted to cartographic coordinates, of the same length and order as the `spatial`.
* @throws IModelError if [[isGeoLocated]] is false or any point could not be converted.
* @see [[spatialFromCartographic]] to perform the inverse conversion.
* @see [[spatialToCartographicFromEcef]] to synchronously convert points using the iModel's ECEF transform.
*/
async cartographicFromSpatial(spatial) {
return this.cartographicFromSpatialWithGcs(spatial);
}
/** Convert points in this iModel's spatial coordinate system to [Cartographic]($common) coordinates using either a [[GeoConverter]] or the iModel's [EcefLocation]($common).
* @param spatial Coordinates to be converted from the iModel's spatial coordinate system
* @returns The `spatial` coordinates converted to cartographic coordinates (WGS84 horizontal datum), of the same length and order as the `spatial`.
* @throws IModelError if [[isGeoLocated]] is false or any point could not be converted.
* @see [[cartographicFromSpatial]] to perform conversion using iModel's GCS horizontal datum
* @beta
*/
async wgs84CartographicFromSpatial(spatial) {
return this.cartographicFromSpatialWithGcs(spatial, "WGS84");
}
/** @internal */
async cartographicFromSpatialWithGcs(spatial, datumOrGCRS) {
if (this.noGcsDefined)
return spatial.map((p) => this.spatialToCartographicFromEcef(p));
if (!this.isGeoLocated)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (!this.isOpen)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not open");
if (spatial.length === 0)
return [];
const geoConverter = this.geoServices.getConverter(datumOrGCRS);
assert(undefined !== geoConverter);
const coordResponse = await geoConverter.getGeoCoordinatesFromIModelCoordinates(spatial);
if (coordResponse.geoCoords.length !== spatial.length)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
return coordResponse.geoCoords.map((coord) => {
switch (coord.s) {
case GeoCoordStatus.NoGCSDefined:
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
case GeoCoordStatus.Success:
const llh = Point3d.fromJSON(coord.p);
return Cartographic.fromDegrees({ longitude: llh.x, latitude: llh.y, height: llh.z });
default:
throw new IModelError(mapToGeoServiceStatus(coord.s), "Error converting spatial to cartographic");
}
});
}
/** Convert a [Cartographic]($common) to a point in this iModel's spatial coordinate system using a [[GeoConverter]].
* @param cartographic A cartographic location
* @param result If defined, use this for output
* @returns A point in this iModel's spatial coordinates
* @throws IModelError if [[isGeoLocated]] is false or cartographic location could not be converted.
* @see [[spatialFromCartographic]] to convert multiple points at once, or you don't know whether the iModel has a GCS.
*/
async cartographicToSpatialFromGcs(cartographic, result) {
if (!this.isGeoLocated && this.noGcsDefined)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
const geoConverter = expectDefined(this.geoServices.getConverter());
const geoCoord = Point3d.create(cartographic.longitudeDegrees, cartographic.latitudeDegrees, cartographic.height); // x is longitude in degrees, y is latitude in degrees, z is height in meters...
const coordResponse = await geoConverter.getIModelCoordinatesFromGeoCoordinates([geoCoord]);
if (1 !== coordResponse.iModelCoords.length || GeoCoordStatus.NoGCSDefined === coordResponse.iModelCoords[0].s)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (GeoCoordStatus.Success !== coordResponse.iModelCoords[0].s) {
const geoServiceStatus = mapToGeoServiceStatus(coordResponse.iModelCoords[0].s);
throw new IModelError(geoServiceStatus, "Error converting cartographic to spatial");
}
result = result ? result : Point3d.createZero();
result.setFromJSON(coordResponse.iModelCoords[0].p);
return result;
}
/** Convert a [Cartographic]($common) to a point in this iModel's Spatial coordinates using a [[GeoConverter]] or[[IModel.ecefLocation]($common).
* @param cartographic A cartographic location
* @param result If defined, use this for output
* @returns A point in this iModel's spatial coordinates
* @throws IModelError if [[isGeoLocated]] is false or cartographic location could not be converted.
* @see [[spatialFromCartographic]] to convert multiple points at once.
* @see [[cartographicToSpatialFromEcef]] to synchronously convert points using the iModel's ECEF transform.
*/
async cartographicToSpatial(cartographic, result) {
return (this.noGcsDefined ? this.cartographicToSpatialFromEcef(cartographic, result) : this.cartographicToSpatialFromGcs(cartographic, result));
}
/** Convert [Cartographic]($common) coordinates into points in this iModel's spatial coordinate system using a [[GeoConverter]] or the iModel's [EcefLocation]($common).
* @param cartographic Coordinates to be converted to the iModel's spatial coordinate system.
* @returns The `cartographic` coordinates converted to spatial coordinates, of the same length and order as `cartographic`.
* @throws IModelError if [[isGeoLocated]] is false or any point could not be converted.
* @see [[cartographicFromSpatial]] to perform the inverse conversion.
*/
async spatialFromCartographic(cartographic) {
if (this.noGcsDefined)
return cartographic.map((p) => this.cartographicToSpatialFromEcef(p));
const geoCoords = cartographic.map((p) => Point3d.create(p.longitudeDegrees, p.latitudeDegrees, p.height));
return this.toSpatialFromGcs(geoCoords);
}
/** Convert geographic coordinates into points in this iModel's spatial coordinate system using a [[GeoConverter]] or the iModel's [EcefLocation]($common).
* @param geoCoords Coordinates to be converted are in the coordinate system described by the `datumOrGCRS` parameter. Defaults iModel's spatial coordinate system otherwise.
* @param datumOrGCRS Datum name or Geographic CRS object definition to use for the conversion.
* @returns The `geographics` coordinates converted to spatial coordinates, of the same length and order as `geographics`.
* @throws IModelError if [[isGeoLocated]] is false or any point could not be converted.
* @beta
*/
async toSpatialFromGcs(geoCoords, datumOrGCRS) {
if (!this.isGeoLocated)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (!this.isOpen)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not open");
if (geoCoords.length === 0)
return [];
const geoConverter = this.geoServices.getConverter(datumOrGCRS);
assert(undefined !== geoConverter);
const coordResponse = await geoConverter.getIModelCoordinatesFromGeoCoordinates(geoCoords);
if (coordResponse.iModelCoords.length !== geoCoords.length)
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
return coordResponse.iModelCoords.map((coord) => {
switch (coord.s) {
case GeoCoordStatus.NoGCSDefined:
throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
case GeoCoordStatus.Success:
return Point3d.fromJSON(coord.p);
default:
throw new IModelError(mapToGeoServiceStatus(coord.s), "Error converting cartographic to spatial");
}
});
}
/** @internal */
getMapEcefToDb(bimElevationBias) {
if (!this.ecefLocation)
return Transform.createIdentity();
const mapEcefToDb = this.ecefLocation.getTransform().inverse();
if (!mapEcefToDb) {
assert(false);
return Transform.createIdentity();
}
mapEcefToDb.origin.z += bimElevationBias;
return mapEcefToDb;
}
_geodeticToSeaLevel;
_projectCenterAltitude;
/** Event called immediately after map elevation request is completed. This occurs only in the case where background map terrain is displayed
* with either geoid or ground offset. These require a query to BingElevation and therefore synching the view may be required
* when the request is completed.
* @internal
*/
onMapElevationLoaded = new BeEvent();
/** The offset between sea level and the geodetic ellipsoid. This will return undefined only if the request for the offset to Bing Elevation
* is required, and in this case the [[onMapElevationLoaded]] event is raised when the request is completed.
* @internal
*/
get geodeticToSeaLevel() {
if (undefined === this._geodeticToSeaLevel) {
const elevationProvider = new BingElevationProvider();
this._geodeticToSeaLevel = elevationProvider.getGeodeticToSeaLevelOffset(this.projectExtents.center, this);
this._geodeticToSeaLevel.then((geodeticToSeaLevel) => {
this._geodeticToSeaLevel = geodeticToSeaLevel;
this.onMapElevationLoaded.raiseEvent(this);
}).catch((_error) => this._geodeticToSeaLevel = 0.0);
}
return ("number" === typeof this._geodeticToSeaLevel) ? this._geodeticToSeaLevel : undefined;
}
/** The altitude (geodetic) at the project center. This will return undefined only if the request for the offset to Bing Elevation
* is required, and in this case the [[onMapElevationLoaded]] event is raised when the request is completed.
* @internal
*/
get projectCenterAltitude() {
if (undefined === this._projectCenterAltitude) {
const elevationProvider = new BingElevationProvider();
this._projectCenterAltitude = elevationProvider.getHeightValue(this.projectExtents.center, this);
this._projectCenterAltitude.then((projectCenterAltitude) => {
this._projectCenterAltitude = projectCenterAltitude;
this.onMapElevationLoaded.raiseEvent(this);
}).catch((_error) => this._projectCenterAltitude = 0.0);
}
return ("number" === typeof this._projectCenterAltitude) ? this._projectCenterAltitude : undefined;
}
/**
* Gets the context that allows accessing the metadata (see `@itwin/ecschema-metadata` package) of this iModel.
* The context is created lazily when this property is accessed for the first time, with an `ECSchemaRpcLocater` registered as a fallback locater, enabling users to register their own locater that'd take more priority.
* This means to correctly access schema context, client-side applications must register `ECSchemaRpcInterface` following instructions for [RPC configuration]($docs/learning/rpcinterface/#client-side-configuration).
* Server-side applications would also [configure RPC]($docs/learning/rpcinterface/#server-side-configuration) as needed.
*
* @note While a `BlankConnection` returns a valid `schemaContext`, it has an invalid locater registered by default, and will throw an error when trying to call it's methods.
* @beta
*/
get schemaContext() {
if (this._schemaContext === undefined) {
const context = new SchemaContext();
// While incremental schema loading is the prefered way to load schemas on the frontend, there might be cases where clients
// would want to use their own locaters, so if incremenal schema loading is disabled, the locater is not registered.
if (IModelApp.isIncrementalSchemaLoadingEnabled) {
context.addLocater(new RpcIncrementalSchemaLocater(this._getRpcProps()));
}
context.addFallbackLocater(new ECSchemaRpcLocater(this._getRpcProps()));
this._schemaContext = context;
}
return this._schemaContext;
}
}
/** A connection that exists without an iModel. Useful for connecting to Reality Data services.
* @note This class exists because our display system requires an IModelConnection type even if only reality data is drawn.
* @public
*/
export class BlankConnection extends IModelConnection {
isBlankConnection() { return true; }
/** The Guid that identifies the iTwin for this BlankConnection.
* @note This can also be set via the [[create]] method using [[BlankConnectionProps.iTwinId]].
*/
get iTwinId() { return this._iTwinId; }
set iTwinId(iTwinId) { this._iTwinId = iTwinId; }
/** A BlankConnection does not have an associated iModel, so its `iModelId` is alway `undefined`. */
get iModelId() { return undefined; } // GuidString | undefined for the superclass, but always undefined for BlankConnection
/** A BlankConnection is always considered closed because it does not have a specific backend nor associated iModel.
* @returns `true` is always returned since RPC operations and iModel queries are not valid.
* @note Even though true is always returned, it is still valid to call [[close]] to dispose frontend resources.
*/
get isClosed() { return true; }
/** Create a new [Blank IModelConnection]($docs/learning/frontend/BlankConnection).
* @param props The properties to use for the new BlankConnection.
*/
static create(props) {
const connection = new BlankConnection({
name: props.name,
rootSubject: { name: props.name },
projectExtents: props.extents,
globalOrigin: props.globalOrigin,
ecefLocation: props.location instanceof Cartographic ? EcefLocation.createFromCartographicOrigin(props.location) : props.location,
key: "",
iTwinId: props.iTwinId,
});
IModelConnection.onOpen.raiseEvent(connection);
return connection;
}
/** There are no connections to the backend to close in the case of a BlankConnection.
* However, there are frontend resources (like the tile cache) that can be disposed.
* @note A BlankConnection should not be used after calling `close`.
*/
async close() {
this.beforeClose();
}
/** @internal */
closeSync() {
this.beforeClose();
}
}
/** A connection to a [SnapshotDb]($backend) hosted on a backend.
* @public
*/
export class SnapshotConnection extends IModelConnection {
/** Type guard for instanceof [[SnapshotConnection]] */
isSnapshotConnection() { return true; }
/** The Guid that identifies this iModel. */
get iModelId() { return expectDefined(super.iModelId); } // GuidString | undefined for the superclass, but required for SnapshotConnection
/** Returns `true` if [[close]] has already been called. */
get isClosed() { return this._isClosed ? true : false; }
_isClosed;
/** Returns `true` if this is a connection to a remote snapshot iModel resolved by the backend.
* @see [[openRemote]]
*/
get isRemote() { return this._isRemote ? true : false; }
_isRemote;
/** Open an IModelConnection to a read-only snapshot iModel from a file name.
* @note This method is intended for desktop or mobile applications and is not available for web applications.
*/
static async openFile(filePath) {
if (!IpcApp.isValid)
throw new Error("IPC required to open a snapshot");
Logger.logTrace(loggerCategory, "SnapshotConnection.openFile", () => ({ filePath }));
const connectionProps = await IpcApp.appFunctionIpc.openSnapshot(filePath);
const connection = new SnapshotConnection(connectionProps);
IModelConnection.onOpen.raiseEvent(connection);
return connection;
}
/** Open an IModelConnection to a remote read-only snapshot iModel from a key that will be resolved by the backend.
* @note This method is intended for web applications.
* @deprecated in 4.10 - will not be removed until after 2026-06-13. Use [[CheckpointConnection.openRemote]].
*/
static async openRemote(fileKey) {
const routingContext = IModelRoutingContext.current || IModelRoutingContext.default;
RpcManager.setIModel({ iModelId: "undefined", key: fileKey });
const openResponse = await SnapshotIModelRpcInterface.getClientForRouting(routingContext.token).openRemote(fileKey); // eslint-disable-line @typescript-eslint/no-deprecated
Logger.logTrace(loggerCategory, "SnapshotConnection.openRemote", () => ({ fileKey }));
const connection = new SnapshotConnection(openResponse);
connection.routingContext = routingContext;
connection._isRemote = true;
IModelConnection.onOpen.raiseEvent(connection);
return connection;
}
/** Close this SnapshotConnection.
* @note For local snapshot files, `close` closes the connection and the underlying [SnapshotDb]($backend) database file.
* For remote snapshots, `close` only closes the connection and frees any frontend resources allocated to the connection.
* @see [[openFile]], [[openRemote]]
*/
async close() {
if (this.isClosed)
return;
this.beforeClose();
try {
if (!this.isRemote) {
await IpcApp.appFunctionIpc.closeIModel(this.key);
}
}
finally {
this._isClosed = true;
}
}
}
/** @public */
(function (IModelConnection) {
/** The collection of loaded ModelState objects for an [[IModelConnection]]. */
class Models {
_iModel;
_modelExtentsQuery = `
SELECT
Model.Id AS ECInstanceId,
iModel_bbox_union(
iModel_placement_aabb(
iModel_placement(
iModel_point(Origin.X, Origin.Y, 0),
iModel_angles(Rotation, 0, 0),
iModel_bbox(
BBoxLow.X, BBoxLow.Y, -1,
BBoxHigh.X, BBoxHigh.Y, 1
)
)
)
) AS bbox
FROM bis.GeometricElement2d
WHERE InVirtualSet(:ids64, Model.Id) AND Origin.X IS NOT NULL
GROUP BY Model.Id
UNION
SELECT
ge.Model.Id AS ECInstanceId,
iModel_bbox(
min(i.MinX), min(i.MinY), min(i.MinZ),
max(i.MaxX), max(i.MaxY), max(i.MaxZ)
) AS bbox
FROM bis.SpatialIndex AS i, bis.GeometricElement3d AS ge, bis.GeometricModel3d AS gm
WHERE InVirtualSet(:ids64, ge.Model.Id) AND ge.ECInstanceId=i.ECInstanceId AND InVirtualSet(:ids64, gm.ECInstanceId) AND (gm.$->isNotSpatiallyLocated?=false OR gm.$->isNotSpatiallyLocated? IS NULL)
GROUP BY ge.Model.Id
UNION
SELECT
ge.Model.Id AS ECInstanceId,
iModel_bbox_union(
iModel_placement_aabb(
iModel_placement(
iModel_point(ge.Origin.X, ge.Origin.Y, ge.Origin.Z),
iModel_angles(ge.Yaw, ge.Pitch, ge.Roll),
iModel_bbox(
ge.BBoxLow.X, ge.BBoxLow.Y, ge.BBoxLow.Z,
ge.BBoxHigh.X, ge.BBoxHigh.Y, ge.BBoxHigh.Z
)
)
)
) AS bbox
FROM bis.GeometricElement3d AS ge, bis.GeometricModel3d as gm
WHERE InVirtualSet(:ids64, ge.Model.Id) AND ge.Origin.X IS NOT NULL AND InVirtualSet(:ids64, gm.ECInstanceId) AND gm.$->isNotSpatiallyLocated?=true
GROUP BY ge.Model.Id`;
_modelExistenceQuery = `
WITH
GeometricModels AS(
SELECT
ECInstanceId
FROM bis.GeometricModel
WHERE InVirtualSet(: ids64, ECInstanceId)
)
SELECT
ECInstanceId,
true AS isGeometricModel
FROM GeometricModels
UNION ALL
SELECT
ECInstanceId,
false AS isGeometricModel
FROM bis.Model
WHERE InVirtualSet(: ids64, ECInstanceId)
AND ECInstanceId NOT IN(SELECT ECInstanceId FROM GeometricModels)`;
_loadedExtents = [];
_geometryChangedListener;
_loaded = new Map();
/** @internal */
get loaded() { return this._loaded; }
/** An iterator over all currently-loaded models. */
[Symbol.iterator]() {
return this._loaded.values()[Symbol.iterator]();
}
/** @internal */
constructor(_iModel) {
this._iModel = _iModel;
IModelConnection.onOpen.addListener(() => {
if (this._iModel.isBriefcaseConnection()) {
this._geometryChangedListener = (changes) => {
this._loadedExtents = this._loadedExtents.filter((extent) => !changes.some((change) => change.id === extent.id));
};
this._iModel.txns.onModelGeometryChanged.addListener(this._geometryChangedListener);
}
});
IModelConnection.onClose.addListener(() => {
if (this._iModel.isBriefcaseConnection() && this._geometryChangedListener) {
this._iModel.txns.onModelGeometryChanged.removeListener(this._geometryChangedListener);
this._geometryChangedListener = undefined;
}
});
}
/** The Id of the [RepositoryModel]($backend). */
get repositoryModelId() { return "0x1"; }
/** @internal */
async getDictionaryModel() {
const res = await this._iModel.models.queryProps({ from: "bis.DictionaryModel", wantPrivate: true });
if (res.length !== 1 || res[0].id === undefined)
throw new IModelError(IModelStatus.BadModel, "bis.DictionaryModel");
return res[0].id;
}
/** Get a batch of [[ModelProps]] given a list of Model ids. */
async getProps(modelIds) {
const iModel = this._iModel;
return iModel.isOpen ? IModelReadRpcInterface.getClientForRouting(iModel.routingContext.token).getModelProps(iModel.getRpcProps(), [...Id64.toIdSet(modelIds)]) : [];
}
/** Find a ModelState in the set of loaded Models by ModelId. */
getLoaded(id) {
return this._loaded.get(id);
}
/** Given a set of modelIds, return the subset of corresponding models that are not currently loaded.
* @param modelIds The set of model Ids
* @returns The subset of the supplied Ids corresponding to models that are not currently loaded, or undefined if all of the specified models are loaded.
*/
filterLoaded(modelIds) {
let unloaded;
for (const id of Id64.iterable(modelIds)) {
if (undefined === this.getLoaded(id)) {
if (undefined === unloaded)
unloaded = new Set();
unloaded.add(id);
}
}
return unloaded;
}
/** load a set of Models by Ids. After the returned Promise resolves, you may get the ModelState objects by calling getLoadedModel. */
async load(modelIds) {
const notLoaded = this.filterLoaded(modelIds);
if (undefined === notLoaded)
return; // all requested models are already loaded
try {
const propArray = await this.getProps(notLoaded);
await this.updateLoadedWithModelProps(propArray);
}
catch {
// ignore error, we had nothing to do.
}
}
/** Given an array of modelProps, find the class for each model and construct it. save it in the iModelConnection's loaded set. */
async updateLoadedWithModelProps(modelProps) {
try {
for (const props of modelProps) {
const ctor = await this._iModel.findClassFor(props.classFullName, ModelState);
if (undefined !== ctor && undefined !== props.id && undefined === this.getLoaded(props.id)) { // do not overwrite if someone else loads it while we await
const modelState = new ctor(props, this._iModel); // create a new instance of the appropriate ModelState subclass
this._loaded.set(modelState.id, modelState); // save it in loaded set
}
}
}
catch {
// ignore error, we had nothing to do.
}
}
/** Remove a model from the set of loaded models. Used internally by BriefcaseConnection in response to txn events.
* @internal
*/
unload(modelId) {
this._loaded.delete(modelId);
}
/** Query for a set of model ranges by ModelIds.
* @param modelIds the Id or Ids of the [GeometricModel]($backend)s for which to query the ranges.
* @returns An array containing the range of each model of each unique model Id, omitting the range for any Id which did no identify a GeometricModel.
* @note The contents of the returned array do not follow a deterministic order.
* @throws [IModelError]($common) if exactly one model Id is specified and that Id does not identify a GeometricModel.
* @see [[queryExtents]] for a similar function that does not throw and produces a deterministically-ordered result.
*/
async queryModelRanges(modelIds) {
const results = await this.queryExtents([...Id64.toIdSet(modelIds)]);
if (results.length === 1 && results[0].status !== IModelStatus.Success) {
throw new IModelError(results[0].status, "error querying model range");
}
return results.filter((x) => x.status === IModelStatus.Success).map((x) => x.extents);
}
/** For each [GeometricModel]($backend) specified by Id, attempts to obtain the union of the volumes of all geometric elements within that model.
* @param modelIds The Id or Ids of the geometric models for which to obtain the extents.
* @returns An array of results, one per supplied Id, in the order in which the Ids were supplied. If the extents could not be obtained, the
* corresponding results entry's `extents` will be a "null" range (@see [Range3d.isNull]($geometry) and its `status` will indicate
* why the extents could not be obtained (e.g., because the Id did not identify a [GeometricModel]($backend)).
*/
async queryExtents(modelIds) {
if (!this._iModel.isOpen)
return [];
if (typeof modelIds === "string")
modelIds = [modelIds];
const modelExtents = [];
for (const modelId of modelIds) {
if (!Id64.isValidId64(modelId)) {
modelExtents.push({ id: modelId, extents: Range3d.createNull(), status: IModelStatus.InvalidId });
}
}
const getUnloadedModelIds = () => modelIds.filter((modelId) => !modelExtents.some((loadedExtent) => loadedExtent.id === modelId));
let remainingModelIds = getUnloadedModelIds();
for (const modelId of remainingModelIds) {
const modelExtent = this._loadedExtents.find((extent) => modelId === extent.id);
if (modelExtent) {
modelExtents.push(modelExtent);
}
}
remainingModelIds = getUnloadedModelIds();
if (remainingModelIds.length > 0) {
const params = new QueryBinder();
params.bindIdSet("ids64", remainingModelIds);
const extentsQueryReader = this._iModel.createQueryReader(this._modelExtentsQuery, params, {
rowFormat: QueryRowFormat.UseECSqlPropertyNames,
});
for await (const row of extentsQueryReader) {
const byteArray = new Uint8Array(Object.values(row.bbox));
const extents = Range3d.fromArrayBuffer(byteArray.buffer);
const extent = { id: row.ECInstanceId, extents, status: IModelStatus.Success };
modelExtents.push(extent);
this._loadedExtents.push(extent);
}
}
remainingModelIds = getUnloadedModelIds();
if (remainingModelIds.length > 0) {
const params = new QueryBinder();
params.bindIdSet("ids64", remainingModelIds);
const modelExistenceQueryReader = this._iModel.createQueryReader(this._modelExistenceQuery, params, {
rowFormat: QueryRowFormat.UseECSqlPropertyNames,
});
for await (const row of modelExistenceQueryReader) {
let extent;
if (row.isGeometricModel) {
extent = { id: row.ECInstanceId, extents: Range3d.createNull(), status: IModelStatus.Success };
}
else {
exten