UNPKG

@itwin/core-frontend

Version:
857 lines • 69.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, 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