@itwin/core-frontend
Version:
iTwin.js frontend components
830 lines • 72.3 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SnapshotConnection = exports.BlankConnection = exports.IModelConnection = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const core_geometry_1 = require("@itwin/core-geometry");
const FrontendLoggerCategory_1 = require("./common/FrontendLoggerCategory");
const GeoServices_1 = require("./GeoServices");
const IModelApp_1 = require("./IModelApp");
const IModelRoutingContext_1 = require("./IModelRoutingContext");
const ModelState_1 = require("./ModelState");
const SelectionSet_1 = require("./SelectionSet");
const SubCategoriesCache_1 = require("./SubCategoriesCache");
const internal_1 = require("./tile/internal");
const Tiles_1 = require("./Tiles");
const ViewState_1 = require("./ViewState");
const Symbols_1 = require("./common/internal/Symbols");
const IpcApp_1 = require("./IpcApp");
const ecschema_metadata_1 = require("@itwin/ecschema-metadata");
const ecschema_rpcinterface_common_1 = require("@itwin/ecschema-rpcinterface-common");
const loggerCategory = FrontendLoggerCategory_1.FrontendLoggerCategory.IModelConnection;
/** A connection to a [IModelDb]($backend) hosted on the backend.
* @public
* @extensions
*/
class IModelConnection extends core_common_1.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 core_bentley_1.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_1.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 === core_bentley_1.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 core_bentley_1.BeEvent();
/** Event called immediately after *any* IModelConnection is opened. */
static onOpen = new core_bentley_1.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 core_bentley_1.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 core_common_1.FontMap(); // eslint-disable-line @typescript-eslint/no-deprecated
if (this.isOpen) {
const fontProps = await core_common_1.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_1.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 core_common_1.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_1.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_1.IModelApp.lookupEntityClass(baseClass);
if (test === undefined)
return false; // nope, not registered
ctor = test; // found it, save it
IModelApp_1.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_1.SelectionSet(this);
this.hilited = new SelectionSet_1.HiliteSet(this);
this.tiles = new Tiles_1.Tiles(this);
this.geoServices = GeoServices_1.GeoServices.createForIModel(this);
this.hilited.onModelSubCategoryModeChanged.addListener(() => {
IModelApp_1.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 core_common_1.IModelReadRpcInterface.getClientForRouting(this.routingContext.token).queryRows(this.getRpcProps(), request);
},
};
return new core_common_1.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 core_common_1.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 core_common_1.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 core_common_1.IModelReadRpcInterface.getClientForRouting(this.routingContext.token).queryEntityIds(this.getRpcProps(), params) : undefined);
}
_snapRpc = new core_bentley_1.OneAtATimeAction(async (props) => core_common_1.IModelReadRpcInterface.getClientForRouting(this.routingContext.token).requestSnap(this.getRpcProps(), IModelApp_1.IModelApp.sessionId, props));
/** Request a snap from the backend.
* @note callers must gracefully handle Promise rejected with AbandonedError
* @internal
*/
async [Symbols_1._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[Symbols_1._requestSnap](props);
}
_toolTipRpc = new core_bentley_1.OneAtATimeAction(async (id) => core_common_1.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 core_common_1.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 core_common_1.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 = core_common_1.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 core_common_1.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 core_common_1.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 core_common_1.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 core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
const geoConverter = (0, core_bentley_1.expectDefined)(this.geoServices.getConverter());
const coordResponse = await geoConverter.getGeoCoordinatesFromIModelCoordinates([spatial]);
if (1 !== coordResponse.geoCoords.length || core_common_1.GeoCoordStatus.NoGCSDefined === coordResponse.geoCoords[0].s)
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (core_common_1.GeoCoordStatus.Success !== coordResponse.geoCoords[0].s) {
const geoServiceStatus = (0, core_common_1.mapToGeoServiceStatus)(coordResponse.geoCoords[0].s);
throw new core_common_1.IModelError(geoServiceStatus, "Error converting spatial to cartographic");
}
const longLatHeight = core_geometry_1.Point3d.fromJSON(coordResponse.geoCoords[0].p); // x is longitude in degrees, y is latitude in degrees, z is height in meters...
return core_common_1.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 core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (!this.isOpen)
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not open");
if (spatial.length === 0)
return [];
const geoConverter = this.geoServices.getConverter(datumOrGCRS);
(0, core_bentley_1.assert)(undefined !== geoConverter);
const coordResponse = await geoConverter.getGeoCoordinatesFromIModelCoordinates(spatial);
if (coordResponse.geoCoords.length !== spatial.length)
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
return coordResponse.geoCoords.map((coord) => {
switch (coord.s) {
case core_common_1.GeoCoordStatus.NoGCSDefined:
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
case core_common_1.GeoCoordStatus.Success:
const llh = core_geometry_1.Point3d.fromJSON(coord.p);
return core_common_1.Cartographic.fromDegrees({ longitude: llh.x, latitude: llh.y, height: llh.z });
default:
throw new core_common_1.IModelError((0, core_common_1.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 core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
const geoConverter = (0, core_bentley_1.expectDefined)(this.geoServices.getConverter());
const geoCoord = core_geometry_1.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 || core_common_1.GeoCoordStatus.NoGCSDefined === coordResponse.iModelCoords[0].s)
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (core_common_1.GeoCoordStatus.Success !== coordResponse.iModelCoords[0].s) {
const geoServiceStatus = (0, core_common_1.mapToGeoServiceStatus)(coordResponse.iModelCoords[0].s);
throw new core_common_1.IModelError(geoServiceStatus, "Error converting cartographic to spatial");
}
result = result ? result : core_geometry_1.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) => core_geometry_1.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 core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
if (!this.isOpen)
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not open");
if (geoCoords.length === 0)
return [];
const geoConverter = this.geoServices.getConverter(datumOrGCRS);
(0, core_bentley_1.assert)(undefined !== geoConverter);
const coordResponse = await geoConverter.getIModelCoordinatesFromGeoCoordinates(geoCoords);
if (coordResponse.iModelCoords.length !== geoCoords.length)
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
return coordResponse.iModelCoords.map((coord) => {
switch (coord.s) {
case core_common_1.GeoCoordStatus.NoGCSDefined:
throw new core_common_1.IModelError(core_bentley_1.GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated");
case core_common_1.GeoCoordStatus.Success:
return core_geometry_1.Point3d.fromJSON(coord.p);
default:
throw new core_common_1.IModelError((0, core_common_1.mapToGeoServiceStatus)(coord.s), "Error converting cartographic to spatial");
}
});
}
/** @internal */
getMapEcefToDb(bimElevationBias) {
if (!this.ecefLocation)
return core_geometry_1.Transform.createIdentity();
const mapEcefToDb = this.ecefLocation.getTransform().inverse();
if (!mapEcefToDb) {
(0, core_bentley_1.assert)(false);
return core_geometry_1.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 core_bentley_1.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 internal_1.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 internal_1.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 ecschema_metadata_1.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_1.IModelApp.isIncrementalSchemaLoadingEnabled) {
context.addLocater(new ecschema_rpcinterface_common_1.RpcIncrementalSchemaLocater(this._getRpcProps()));
}
context.addFallbackLocater(new ecschema_rpcinterface_common_1.ECSchemaRpcLocater(this._getRpcProps()));
this._schemaContext = context;
}
return this._schemaContext;
}
}
exports.IModelConnection = IModelConnection;
/** 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
*/
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 core_common_1.Cartographic ? core_common_1.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();
}
}
exports.BlankConnection = BlankConnection;
/** A connection to a [SnapshotDb]($backend) hosted on a backend.
* @public
*/
class SnapshotConnection extends IModelConnection {
/** Type guard for instanceof [[SnapshotConnection]] */
isSnapshotConnection() { return true; }
/** The Guid that identifies this iModel. */
get iModelId() { return (0, core_bentley_1.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_1.IpcApp.isValid)
throw new Error("IPC required to open a snapshot");
core_bentley_1.Logger.logTrace(loggerCategory, "SnapshotConnection.openFile", () => ({ filePath }));
const connectionProps = await IpcApp_1.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_1.IModelRoutingContext.current || IModelRoutingContext_1.IModelRoutingContext.default;
core_common_1.RpcManager.setIModel({ iModelId: "undefined", key: fileKey });
const openResponse = await core_common_1.SnapshotIModelRpcInterface.getClientForRouting(routingContext.token).openRemote(fileKey); // eslint-disable-line @typescript-eslint/no-deprecated
core_bentley_1.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_1.IpcApp.appFunctionIpc.closeIModel(this.key);
}
}
finally {
this._isClosed = true;
}
}
}
exports.SnapshotConnection = SnapshotConnection;
/** @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 core_common_1.IModelError(core_bentley_1.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 ? core_common_1.IModelReadRpcInterface.getClientForRouting(iModel.routingContext.token).getModelProps(iModel.getRpcProps(), [...core_bentley_1.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 core_bentley_1.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_1.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([...core_bentley_1.Id64.toIdSet(modelIds)]);
if (results.length === 1 && results[0].status !== core_bentley_1.IModelStatus.Success) {
throw new core_common_1.IModelError(results[0].status, "error querying model range");
}
return results.filter((x) => x.status === core_bentley_1.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 (!core_bentley_1.Id64.isValidId64(modelId)) {
modelExtents.push({ id: modelId, extents: core_geometry_1.Range3d.createNull(), status: core_bentley_1.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(