@itwin/core-common
Version:
iTwin.js components common to frontend and backend
397 lines • 19.7 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module 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