UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

397 lines • 19.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 iModels */ import { assert, BeEvent, expectDefined, GeoServiceStatus, Id64, IModelStatus, OpenMode } from "@itwin/core-bentley"; import { AxisIndex, AxisOrder, Constant, Geometry, Matrix3d, Point3d, Range3d, Transform, Vector3d, YawPitchRollAngles, } from "@itwin/core-geometry"; import { Cartographic } from "./geometry/Cartographic"; import { GeographicCRS } from "./geometry/CoordinateReferenceSystem"; import { IModelError } from "./IModelError"; /** The position and orientation of an iModel on the earth in [ECEF](https://en.wikipedia.org/wiki/ECEF) (Earth Centered Earth Fixed) coordinates * @note This is an immutable type - all of its properties are frozen. * @see [GeoLocation of iModels]($docs/learning/GeoLocation.md) * @public */ export class EcefLocation { /** The origin of the ECEF transform. */ origin; /** The orientation of the ECEF transform */ orientation; /** Optional position on the earth used to establish the ECEF origin and orientation. */ cartographicOrigin; /** Optional X column vector used with [[yVector]] to calculate potentially non-rigid transform if a projection is present. */ xVector; /** Optional Y column vector used with [[xVector]] to calculate potentially non-rigid transform if a projection is present. */ yVector; /** The transform from iModel Spatial coordinates to ECEF from this EcefLocation */ _transform = Transform.createIdentity(); /** Get the transform from iModel Spatial coordinates to ECEF from this EcefLocation */ getTransform() { return this._transform; } /** Construct a new EcefLocation. Once constructed, it is frozen and cannot be modified. */ constructor(props) { this.origin = Point3d.fromJSON(props.origin).freeze(); this.orientation = YawPitchRollAngles.fromJSON(props.orientation).freeze(); if (props.cartographicOrigin) this.cartographicOrigin = Cartographic.fromRadians({ longitude: props.cartographicOrigin.longitude, latitude: props.cartographicOrigin.latitude, height: props.cartographicOrigin.height }).freeze(); if (props.xVector && props.yVector) { this.xVector = Vector3d.fromJSON(props.xVector).freeze(); this.yVector = Vector3d.fromJSON(props.yVector).freeze(); } if (props.transform) { this._transform.setFromJSON(props.transform); this._transform.freeze(); } else { let matrix; if (this.xVector && this.yVector) { const zVector = this.xVector.crossProduct(this.yVector); if (zVector.normalizeInPlace()) matrix = Matrix3d.createColumns(this.xVector, this.yVector, zVector); } if (!matrix) matrix = this.orientation.toMatrix3d(); this._transform = Transform.createOriginAndMatrix(this.origin, matrix); this._transform.freeze(); } } /** Returns true if this EcefLocation is not located at the center of the Earth. * @alpha are locations very close to the center considered valid? What are the specific criteria? */ get isValid() { return !this.origin.isZero; } /** Construct ECEF Location from cartographic origin with optional known point and angle. */ static createFromCartographicOrigin(origin, point, angle) { const ecefOrigin = origin.toEcef(); const deltaRadians = 10 / Constant.earthRadiusWGS84.polar; const northCarto = Cartographic.fromRadians({ longitude: origin.longitude, latitude: origin.latitude + deltaRadians, height: origin.height }); const eastCarto = Cartographic.fromRadians({ longitude: origin.longitude + deltaRadians, latitude: origin.latitude, height: origin.height }); const ecefNorth = northCarto.toEcef(); const ecefEast = eastCarto.toEcef(); const xVector = expectDefined(Vector3d.createStartEnd(ecefOrigin, ecefEast).normalize()); const yVector = expectDefined(Vector3d.createStartEnd(ecefOrigin, ecefNorth).normalize()); const matrix = expectDefined(Matrix3d.createRigidFromColumns(xVector, yVector, AxisOrder.XYZ)); if (angle !== undefined) { const north = Matrix3d.createRotationAroundAxisIndex(AxisIndex.Z, angle); matrix.multiplyMatrixMatrix(north, matrix); } if (point !== undefined) { const delta = matrix.multiplyVector(Vector3d.create(-point.x, -point.y, -point.z)); ecefOrigin.addInPlace(delta); } return new EcefLocation({ origin: ecefOrigin, orientation: expectDefined(YawPitchRollAngles.createFromMatrix3d(matrix)), cartographicOrigin: origin }); } /** Construct ECEF Location from transform with optional position on the earth used to establish the ECEF origin and orientation. */ static createFromTransform(transform) { const ecefOrigin = transform.getOrigin(); const angleFromInput = YawPitchRollAngles.createDegrees(0, 0, 0); const locationOrientationFromInputT = YawPitchRollAngles.createFromMatrix3d(transform.getMatrix(), angleFromInput); const transformProps = transform.toJSON(); return new EcefLocation({ origin: ecefOrigin, orientation: locationOrientationFromInputT ?? angleFromInput, transform: transformProps }); } /** Get the location center of the earth in the iModel coordinate system. */ get earthCenter() { const matrix = this.orientation.toMatrix3d(); return Point3d.createFrom(matrix.multiplyTransposeXYZ(-this.origin.x, -this.origin.y, -this.origin.z)); } /** Return true if this location is equivalent to another location within a small tolerance. */ isAlmostEqual(other) { if (!this.origin.isAlmostEqual(other.origin) || !this.orientation.isAlmostEqual(other.orientation)) return false; if ((this.xVector === undefined) !== (other.xVector === undefined) || (this.yVector === undefined) !== (other.yVector === undefined)) return false; if (this.xVector !== undefined && other.xVector !== undefined && !this.xVector.isAlmostEqual(other.xVector)) return false; if (this.yVector !== undefined && other.yVector !== undefined && !this.yVector.isAlmostEqual(other.yVector)) return false; if (!this.getTransform().isAlmostEqual(other.getTransform())) return false; const thisCarto = this.cartographicOrigin; const otherCarto = other.cartographicOrigin; if (undefined === thisCarto || undefined === otherCarto) return undefined === thisCarto && undefined === otherCarto; return thisCarto.equalsEpsilon(otherCarto, Geometry.smallMetricDistance); } toJSON() { const props = { origin: this.origin.toJSON(), orientation: this.orientation.toJSON(), transform: this.getTransform().toJSON(), }; if (this.cartographicOrigin) props.cartographicOrigin = this.cartographicOrigin.toJSON(); if (this.xVector) props.xVector = this.xVector.toJSON(); if (this.yVector) props.yVector = this.yVector.toJSON(); return props; } } /** Represents an iModel in JavaScript. * @see [GeoLocation of iModels]($docs/learning/GeoLocation.md) * @public */ export class IModel { _projectExtents; _name; _rootSubject; _globalOrigin; _ecefLocation; _geographicCoordinateSystem; _iModelId; _changeset; /** The Id of the repository model. */ static repositoryModelId = "0x1"; /** The Id of the root subject element. */ static rootSubjectId = "0x1"; /** The Id of the dictionary model. */ static dictionaryId = "0x10"; /** Event raised after [[name]] changes. */ onNameChanged = new BeEvent(); /** Event raised after [[rootSubject]] changes. */ onRootSubjectChanged = new BeEvent(); /** Event raised after [[projectExtents]] changes. */ onProjectExtentsChanged = new BeEvent(); /** Event raised after [[globalOrigin]] changes. */ onGlobalOriginChanged = new BeEvent(); /** Event raised after [[ecefLocation]] changes. */ onEcefLocationChanged = new BeEvent(); /** Event raised after [[geographicCoordinateSystem]] changes. */ onGeographicCoordinateSystemChanged = new BeEvent(); /** Event raised after [[changeset]] changes. */ onChangesetChanged = new BeEvent(); /** Name of the iModel */ get name() { assert(this._name !== undefined); return this._name; } set name(name) { if (name !== this._name) { const old = this._name; this._name = name; if (undefined !== old) this.onNameChanged.raiseEvent(old); } } /** The name and description of the root subject of this iModel */ get rootSubject() { assert(this._rootSubject !== undefined); return this._rootSubject; } set rootSubject(subject) { if (undefined === this._rootSubject || this._rootSubject.name !== subject.name || this._rootSubject.description !== subject.description) { const old = this._rootSubject; this._rootSubject = subject; if (old) this.onRootSubjectChanged.raiseEvent(old); } } /** * The volume, in spatial coordinates, inside which the entire project is contained. * @note The object returned from this method is frozen. You *must* make a copy before you do anything that might attempt to modify it. */ get projectExtents() { assert(undefined !== this._projectExtents); return this._projectExtents; } set projectExtents(extents) { // Don't allow any axis of the project extents to be less than 1 meter. const projectExtents = extents.clone(); projectExtents.ensureMinLengths(1.0); if (!this._projectExtents || !this._projectExtents.isAlmostEqual(projectExtents)) { const old = this._projectExtents; projectExtents.freeze(); this._projectExtents = projectExtents; if (old) this.onProjectExtentsChanged.raiseEvent(old); } } /** An offset to be applied to all spatial coordinates. */ get globalOrigin() { assert(this._globalOrigin !== undefined); return this._globalOrigin; } set globalOrigin(org) { if (!this._globalOrigin || !this._globalOrigin.isAlmostEqual(org)) { const old = this._globalOrigin; org.freeze(); this._globalOrigin = org; if (old) this.onGlobalOriginChanged.raiseEvent(old); } } /** The [EcefLocation]($docs/learning/glossary#ecefLocation) of the iModel in Earth Centered Earth Fixed coordinates. * If the iModel property geographicCoordinateSystem is not defined then the ecefLocation provides a geolocation by defining a * 3D coordinate system relative to the Earth model WGS84. Refer to additional documentation for details. If the geographicCoordinateSystem * property is defined then the ecefLocation must be used with care. When the geographicCoordinateSystem is defined it indicates the * iModel cartesian space is the result of a cartographic projection. This implies a flattening of the Earth surface process that * results in scale, angular or area distortion. The ecefLocation is then an approximation calculated at the center of the project extent. * If the project is more than 2 kilometer in size, the ecefLocation may represent a poor approximation of the effective * cartographic projection used and a linear transformation should then be calculated at the exact origin of the data * it must position. * @see [GeoLocation of iModels]($docs/learning/GeoLocation.md) */ get ecefLocation() { return this._ecefLocation; } set ecefLocation(ecefLocation) { const old = this._ecefLocation; if (!old && !ecefLocation) return; else if (old && ecefLocation && old.isAlmostEqual(ecefLocation)) return; this._ecefLocation = ecefLocation; this.onEcefLocationChanged.raiseEvent(old); } /** Set the [EcefLocation]($docs/learning/glossary#ecefLocation) for this iModel. */ setEcefLocation(ecef) { this.ecefLocation = new EcefLocation(ecef); } /** The geographic coordinate reference system of the iModel. */ get geographicCoordinateSystem() { return this._geographicCoordinateSystem; } set geographicCoordinateSystem(geoCRS) { const old = this._geographicCoordinateSystem; if (!old && !geoCRS) return; else if (old && geoCRS && old.equals(geoCRS)) return; this._geographicCoordinateSystem = geoCRS; this.onGeographicCoordinateSystemChanged.raiseEvent(old); } /** Sets the geographic coordinate reference system from GeographicCRSProps. */ setGeographicCoordinateSystem(geoCRS) { this.geographicCoordinateSystem = new GeographicCRS(geoCRS); } /** @internal */ getConnectionProps() { return { name: this.name, rootSubject: this.rootSubject, projectExtents: this.projectExtents.toJSON(), globalOrigin: this.globalOrigin.toJSON(), ecefLocation: this.ecefLocation, geographicCoordinateSystem: this.geographicCoordinateSystem?.toJSON(), ...this._getRpcProps(), }; } /** Convert this iModel to a JSON representation. */ toJSON() { return this.getConnectionProps(); } /** A key used to identify this iModel in RPC calls from frontend to backend. * @internal */ _fileKey; /** Get the key that was used to open this iModel. This is the value used for Rpc and Ipc communications. */ get key() { return this._fileKey; } /** @internal */ _iTwinId; /** The Guid that identifies the iTwin that owns this iModel. */ get iTwinId() { return this._iTwinId; } /** The Guid that identifies this iModel. */ get iModelId() { return this._iModelId; } /** @public */ get changeset() { return { ...this._changeset }; } set changeset(changeset) { const prev = this._changeset; if (prev.id !== changeset.id || prev.index !== changeset.index) { this._changeset = { id: changeset.id, index: changeset.index }; this.onChangesetChanged.raiseEvent(prev); } } _openMode = OpenMode.Readonly; /** The [[OpenMode]] used for this IModel. */ get openMode() { return this._openMode; } /** Return a token for RPC operations. * @throws IModelError if the iModel is not open. */ getRpcProps() { if (!this.isOpen) throw new IModelError(IModelStatus.BadRequest, "IModel is not open for rpc"); return this._getRpcProps(); } /** Returns the iModel's RPC properties. * @note It is an error to attempt to use these properties as a token for RPC operations if the iModel is not open. * @internal */ _getRpcProps() { return { key: this._fileKey, iTwinId: this.iTwinId, iModelId: this.iModelId, changeset: this.changeset, }; } /** @internal */ constructor(tokenProps) { this._changeset = tokenProps?.changeset ?? { id: "", index: 0 }; this._fileKey = ""; if (tokenProps) { this._fileKey = tokenProps.key; this._iTwinId = tokenProps.iTwinId; this._iModelId = tokenProps.iModelId; } } /** @internal */ initialize(name, props) { this.name = name; this.rootSubject = props.rootSubject; this.projectExtents = Range3d.fromJSON(props.projectExtents); this.globalOrigin = Point3d.fromJSON(props.globalOrigin); const ecefLocation = props.ecefLocation ? new EcefLocation(props.ecefLocation) : undefined; this.ecefLocation = ecefLocation?.isValid ? ecefLocation : undefined; this.geographicCoordinateSystem = props.geographicCoordinateSystem ? new GeographicCRS(props.geographicCoordinateSystem) : undefined; } /** Get the default subCategoryId for the supplied categoryId */ static getDefaultSubCategoryId(categoryId) { return Id64.isValid(categoryId) ? Id64.fromLocalAndBriefcaseIds(Id64.getLocalId(categoryId) + 1, Id64.getBriefcaseId(categoryId)) : Id64.invalid; } /** True if this iModel has an [EcefLocation]($docs/learning/glossary#ecefLocation). */ get isGeoLocated() { return undefined !== this._ecefLocation; } /** Get the Transform from this iModel's Spatial coordinates to ECEF coordinates using its [[IModel.ecefLocation]]. * @throws IModelError if [[isGeoLocated]] is false. */ getEcefTransform() { if (undefined === this._ecefLocation) throw new IModelError(GeoServiceStatus.NoGeoLocation, "iModel is not GeoLocated"); return this._ecefLocation.getTransform(); } /** Convert a point in this iModel's Spatial coordinates to an ECEF point using its [[IModel.ecefLocation]]. * @param spatial A point in the iModel's spatial coordinates * @param result If defined, use this for output * @returns A Point3d in ECEF coordinates * @throws IModelError if [[isGeoLocated]] is false. */ spatialToEcef(spatial, result) { return this.getEcefTransform().multiplyPoint3d(spatial, result); } /** Convert a point in ECEF coordinates to a point in this iModel's Spatial coordinates using its [[ecefLocation]]. * @param ecef A point in ECEF coordinates * @param result If defined, use this for output * @returns A Point3d in this iModel's spatial coordinates * @throws IModelError if [[isGeoLocated]] is false. * @note The resultant point will only be meaningful if the ECEF coordinate is close on the earth to the iModel. */ ecefToSpatial(ecef, result) { return expectDefined(this.getEcefTransform().multiplyInversePoint3d(ecef, result)); } /** Convert a point in this iModel's Spatial coordinates to a [[Cartographic]] using its [[IModel.ecefLocation]]. * @param spatial A point in the iModel's spatial coordinates * @param result If defined, use this for output * @returns A Cartographic location * @throws IModelError if [[isGeoLocated]] is false. */ spatialToCartographicFromEcef(spatial, result) { return expectDefined(Cartographic.fromEcef(this.spatialToEcef(spatial), result)); } /** Convert a [[Cartographic]] to a point in this iModel's Spatial coordinates using its [[IModel.ecefLocation]]. * @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. * @note The resultant point will only be meaningful if the ECEF coordinate is close on the earth to the iModel. */ cartographicToSpatialFromEcef(cartographic, result) { return this.ecefToSpatial(cartographic.toEcef(result), result); } } //# sourceMappingURL=IModel.js.map