UNPKG

@itwin/core-frontend

Version:
832 lines • 62.7 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module IModelConnection */ import { assert, BeEvent, 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, 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 } 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. ctor = defaultClass; // in case we cant find a registered class that handles this class // 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; // either the baseClass handler or defaultClass if we didn't find a registered baseClass } /** @internal */ constructor(iModelProps) { super(iModelProps); super.initialize(iModelProps.name, 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 = 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 = 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(); const locater = new ECSchemaRpcLocater(this._getRpcProps()); context.addFallbackLocater(locater); 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 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; _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; } /** 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 === 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 iModel = this._iModel; return iModel.isOpen ? IModelReadRpcInterface.getClientForRouting(iModel.routingContext.token).queryModelRanges(iModel.getRpcProps(), [...Id64.toIdSet(modelIds)]) : []; } /** 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) { const iModel = this._iModel; if (!iModel.isOpen) return []; if (typeof modelIds === "string") modelIds = [modelIds]; return IModelReadRpcInterface.getClientForRouting(iModel.routingContext.token).queryModelExtents(iModel.getRpcProps(), modelIds); } /** Query for a set of ModelProps of the specified ModelQueryParams. * @param queryParams 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 props. */ async queryProps(queryParams) { const iModel = this._iModel; if (!iModel.isOpen) return []; const params = { ...queryParams }; // make a copy params.from = queryParams.from || ModelState.classFullName; // use "BisCore:Model" as default class name params.where = queryParams.where || ""; if (!queryParams.wantPrivate) { if (params.where.length > 0) params.where += " AND "; params.where += "IsPrivate=FALSE "; } if (!queryParams.wantTemplate) { if (params.where.length > 0) params.where += " AND "; params.where += "IsTemplate=FALSE "; } return IModelReadRpcInterface.getClientForRouting(iModel.routingContext.token).queryModelProps(iModel.getRpcProps(), params); } /** Asynchronously stream ModelProps using the specified ModelQueryParams. */ async *query(queryParams) { // NOTE: this implementation has the desired API signature, but its implementation must be improved to actually page results const modelPropsArray = await this.queryProps(queryParams); for (const modelProps of modelPropsArray) { yield modelProps; } } } IModelConnection.Models = Models; /** The collection of Elements for an [[IModelConnection]]. */ class Elements { _iModel; /** @internal */ constructor(_iModel) { this._iModel = _iModel; } /** The Id of the [root subject element]($docs/bis/guide/references/glossary.md#subject-root) for this iModel. */ get rootSubjectId() { return "0x1"; } /** Get a set of element ids that satisfy a query */ async queryIds(params) { return this._iModel.queryEntityIds(params); } /** Get an array of [[ElementProps]] given one or more element ids. * @note This method returns **all** of the properties of the element (excluding GeometryStream), which may be a very large amount of data - consider using * [[IModelConnection.query]] to select only those properties of interest to limit the amount of data returned. */ async getProps(arg) { const iModel = this._iModel; return iModel.isOpen ? IModelReadRpcInterface.getClientForRouting(iModel.routingContext.token).getElementProps(this._iModel.getRpcProps(), [...Id64.toIdSet(arg)]) : []; } /** Obtain the properties of a single element, optionally specifying specific properties to include or exclude. * For example, [[getProps]] and [[queryProps]] omit the [GeometryStreamProps]($common) property of [GeometricElementProps]($common) and [GeometryPartProps]($common) * because it can be quite large and is generally not useful to frontend code. The following code requests that the geometry stream be included: * ```ts * const props = await iModel.elements.loadProps(elementId, { wantGeometry: true }); * ``` * @param identifier Identifies the element by its Id, federation Guid, or [Code]($common). * @param options Optionally includes or excludes specific properties. * @returns The properties of the requested element; or `undefined` if no element exists with the specified identifier or the iModel is not open. * @throws [IModelError]($common) if the element exists but could not be loaded. */ async loadProps(identifier, options) { const imodel = this._iModel; return imodel.isOpen ? IModelReadRpcInterface.getClientForRouting(imodel.routingContext.token).loadElementProps(imodel.getRpcProps(), identifier, options) : undefined; } /** Get an array of [[ElementProps]] that satisfy a query * @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 props. */ async queryProps(params) { const iModel = this._iModel; return iModel.isOpen ? IModelReadRpcInterface.getClientForRouting(iModel.routingContext.token).queryElementProps(iModel.getRpcProps(), params) : []; } /** Obtain the [Placement]($common)s of a set of [GeometricElement]($backend)s. * @param elementIds The Ids of the elements whose placements are to be queried. * @param options Options customizing how the placements are queried. * @returns an array of placements, each having an additional `elementId` property identifying the element from which the placement was obtained. * @note Any Id that does not identify a geometric element with a valid bounding box and origin is omitted from the returned array. */ async getPlacements(elementIds, options) { let ids; if (typeof elementIds === "string") ids = [elementIds]; else if (!Array.isArray(elementIds)) ids = Array.from(elementIds); else ids = elementIds; if (ids.length === 0) return []; const select3d = ` SELECT ECInstanceId, Origin.x as x, Origin.y as y, Origin.z as z, BBoxLow.x as lx, BBoxLow.y as ly, BBoxLow.z as lz, BBoxHigh.x as hx, BBoxHigh.y as hy, BBoxHigh.z as hz, Yaw, Pitch, Roll, NULL as Rotation FROM bis.GeometricElement3d WHERE Origin IS NOT NULL AND BBoxLow IS NOT NULL AND BBoxHigh IS NOT NULL`; // Note: For the UNION ALL statement, the column alias