@itwin/core-frontend
Version:
iTwin.js frontend components
476 lines • 27.5 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 Views
*/
import { assert } from "@itwin/core-bentley";
import { Angle, ClipPlane, ClipPlaneContainment, Constant, Ellipsoid, GrowableXYZArray, Matrix3d, Plane3dByOriginAndUnitNormal, Point2d, Point3d, Point4d, Range1d, Range3d, Ray3d, Transform, Vector3d } from "@itwin/core-geometry";
import { Cartographic, ColorByName, ColorDef, GeoCoordStatus, GlobeMode, LinePixels } from "@itwin/core-common";
import { WebMercatorTilingScheme } from "./tile/internal";
const scratchRange = Range3d.createNull();
const scratchZeroPoint = Point3d.createZero();
const scratchPoint = Point3d.create();
const scratchVector = Vector3d.create();
const scratchCenterPoint = Point3d.createZero();
const scratchIntersectRay = Ray3d.create(Point3d.create(), Vector3d.create());
const scratchEyePoint = Point3d.createZero();
const scratchViewRotation = Matrix3d.createIdentity();
const scratchSilhouetteNormal = Vector3d.create();
const scratchCartoRectangle = new GrowableXYZArray();
const scratchWorkArray = new GrowableXYZArray();
function accumulateDepthRange(point, viewRotation, range) {
viewRotation.multiplyXYZtoXYZ(point, scratchPoint);
range.extend(scratchPoint);
}
function accumulateFrustumPlaneDepthRange(frustum, plane, viewRotation, range, eyePoint) {
let includeHorizon = false;
for (let i = 0; i < 4; i++) {
const frustumRay = Ray3d.createStartEnd(eyePoint ? eyePoint : frustum.points[i + 4], frustum.points[i]);
const thisFraction = frustumRay.intersectionWithPlane(plane, scratchPoint);
if (undefined !== thisFraction && (!eyePoint || thisFraction > 0))
accumulateDepthRange(scratchPoint, viewRotation, range);
else
includeHorizon = true;
}
if (includeHorizon) {
if (eyePoint !== undefined) {
const eyeHeight = plane.altitude(eyePoint);
if (eyeHeight < 0)
accumulateDepthRange(eyePoint, viewRotation, range);
else {
const viewZ = viewRotation.getRow(2);
const horizonDistance = Math.sqrt(eyeHeight * eyeHeight + 2 * eyeHeight * Constant.earthRadiusWGS84.equator);
accumulateDepthRange(eyePoint.plusScaled(viewZ, -horizonDistance, scratchPoint), viewRotation, range);
}
}
}
}
/** @internal */
export function getFrustumPlaneIntersectionDepthRange(frustum, plane) {
const eyePoint = frustum.getEyePoint(scratchEyePoint);
const viewRotation = frustum.getRotation(scratchViewRotation);
const intersectRange = Range3d.createNull();
accumulateFrustumPlaneDepthRange(frustum, plane, viewRotation, intersectRange, eyePoint);
return intersectRange.isNull ? Range1d.createNull() : Range1d.createXX(intersectRange.low.z, intersectRange.high.z);
}
/** Geometry of background map - either an ellipsoid or a plane as defined by GlobeMode.
* @internal
*/
export class BackgroundMapGeometry {
_bimElevationBias;
_iModel;
globeMode;
geometry;
globeOrigin;
globeMatrix;
cartesianRange;
cartesianTransitionRange;
cartesianPlane;
cartesianDiagonal;
cartesianChordHeight;
maxGeometryChordHeight;
_mercatorFractionToDb;
_mercatorTilingScheme;
_ecefToDb;
static maxCartesianDistance = 1E4; // If globe is 3D we still consider the map geometry flat within this distance of the project extents.
static _transitionDistanceMultiplier = .25; // In the transition range which extends beyond the cartesian range we interpolate between cartesian and ellipsoid.
static _scratchRayFractions = new Array();
static _scratchRayAngles = new Array();
static _scratchPoint = Point3d.createZero();
constructor(_bimElevationBias, globeMode, _iModel) {
this._bimElevationBias = _bimElevationBias;
this._iModel = _iModel;
this._ecefToDb = _iModel.getMapEcefToDb(_bimElevationBias);
this.globeMode = globeMode;
this.cartesianRange = BackgroundMapGeometry.getCartesianRange(_iModel);
this.cartesianTransitionRange = this.cartesianRange.clone();
this.cartesianTransitionRange.expandInPlace(BackgroundMapGeometry.getCartesianTransitionDistance(_iModel));
this.cartesianDiagonal = this.cartesianRange.diagonal().magnitudeXY();
const earthRadius = Constant.earthRadiusWGS84.equator;
this.globeOrigin = this._ecefToDb.origin.cloneAsPoint3d();
this.globeMatrix = this._ecefToDb.matrix.clone();
this.cartesianChordHeight = Math.sqrt(this.cartesianDiagonal * this.cartesianDiagonal + earthRadius * earthRadius) - earthRadius; // Maximum chord height deviation of the cartesian area.
const halfChordAngle = Angle.piOver2Radians / 10;
this.maxGeometryChordHeight = (1 - Math.cos(halfChordAngle)) * earthRadius;
this.cartesianPlane = this.getPlane();
this.geometry = (globeMode === GlobeMode.Ellipsoid) ? this.getEarthEllipsoid() : this.cartesianPlane;
this._mercatorTilingScheme = new WebMercatorTilingScheme();
this._mercatorFractionToDb = this._mercatorTilingScheme.computeMercatorFractionToDb(this._ecefToDb, _bimElevationBias, _iModel, false);
}
static getCartesianRange(iModel, result) {
const cartesianRange = Range3d.createFrom(iModel.projectExtents, result);
cartesianRange.expandInPlace(BackgroundMapGeometry.maxCartesianDistance);
return cartesianRange;
}
static getCartesianTransitionDistance(iModel) {
return BackgroundMapGeometry.getCartesianRange(iModel, scratchRange).diagonal().magnitudeXY() * BackgroundMapGeometry._transitionDistanceMultiplier;
}
async dbToCartographicFromGcs(db) {
const scratch = new Point3d();
const promises = db.map(async (p) => {
return this.cartesianRange.containsPoint(Point3d.createFrom(p, scratch)) ? this._iModel.spatialToCartographic(p) : this.dbToCartographic(p);
});
return Promise.all(promises);
}
async dbToWGS84CartographicFromGcs(db) {
if (db.length === 0)
return [];
const result = Array(db.length);
const reproject = [];
const reprojectIdx = [];
const scratch = new Point3d();
for (let i = 0; i < db.length; i++) {
Point3d.createFrom(db[i], scratch);
if (this.cartesianRange.containsPoint(scratch)) {
reprojectIdx.push(i);
reproject.push(db[i]);
}
else {
result[i] = this.dbToCartographic(db[i]);
}
}
if (reproject.length === 0)
return result;
const reprojectPromise = this._iModel.wgs84CartographicFromSpatial(reproject);
return reprojectPromise.then((reprojected) => {
if (reprojected.length === reprojectIdx.length) { // reprojected array size must match our index array, otherwise something is OFF
for (let i = 0; i < reprojected.length; i++) {
result[reprojectIdx[i]] = reprojected[i]; // Insert the reprojected values at their original index
}
}
return result;
});
}
dbToCartographic(db, result) {
if (undefined === result)
result = Cartographic.createZero();
if (this.globeMode === GlobeMode.Plane) {
const mercatorFraction = this._mercatorFractionToDb.multiplyInversePoint3d(db);
return this._mercatorTilingScheme.fractionToCartographic(mercatorFraction.x, mercatorFraction.y, result, mercatorFraction.z);
}
else {
const ecef = this._ecefToDb.multiplyInversePoint3d(db);
return Cartographic.fromEcef(ecef, result);
}
}
async cartographicToDbFromGcs(cartographic) {
let db;
if (this.globeMode === GlobeMode.Plane) {
const fraction = Point2d.create(0, 0);
db = cartographic.map((p) => {
this._mercatorTilingScheme.cartographicToFraction(p.latitude, p.longitude, fraction);
return this._mercatorFractionToDb.multiplyXYZ(fraction.x, fraction.y, p.height);
});
}
else {
db = cartographic.map((p) => this._ecefToDb.multiplyPoint3d(p.toEcef()));
}
if (this._iModel.noGcsDefined)
return db;
const promises = db.map(async (p, i) => {
return this.cartesianRange.containsPoint(p) ? this._iModel.cartographicToSpatialFromGcs(cartographic[i]) : p;
});
return Promise.all(promises);
}
async cartographicToDbFromWgs84Gcs(cartographic) {
let db;
if (this.globeMode === GlobeMode.Plane) {
const fraction = Point2d.create(0, 0);
db = cartographic.map((p) => {
this._mercatorTilingScheme.cartographicToFraction(p.latitude, p.longitude, fraction);
return this._mercatorFractionToDb.multiplyXYZ(fraction.x, fraction.y, p.height);
});
}
else {
db = cartographic.map((p) => this._ecefToDb.multiplyPoint3d(p.toEcef()));
}
if (this._iModel.noGcsDefined)
return db;
const toReprojectCoords = [];
const toReprojectIdx = [];
db.forEach(async (p, i) => {
if (this.cartesianRange.containsPoint(p)) {
toReprojectCoords.push({ x: cartographic[i].longitudeDegrees, y: cartographic[i].latitudeDegrees, z: cartographic[i].height });
toReprojectIdx.push(i);
}
});
const spatialPoints = await this._iModel.toSpatialFromGcs(toReprojectCoords, { horizontalCRS: { epsg: 4326 }, verticalCRS: { id: "ELLIPSOID" } });
return db.map((p, i) => {
const reprojectedIdx = toReprojectIdx.findIndex((value) => value === i);
return (reprojectedIdx === -1 ? p : spatialPoints[reprojectedIdx]);
});
}
cartographicToDb(cartographic, result) {
if (this.globeMode === GlobeMode.Plane) {
const fraction = Point2d.create(0, 0);
this._mercatorTilingScheme.cartographicToFraction(cartographic.latitude, cartographic.longitude, fraction);
return this._mercatorFractionToDb.multiplyXYZ(fraction.x, fraction.y, cartographic.height, result);
}
else {
return this._ecefToDb.multiplyPoint3d(cartographic.toEcef());
}
}
getEarthEllipsoid(radiusOffset = 0) {
const equatorRadius = Constant.earthRadiusWGS84.equator + radiusOffset, polarRadius = Constant.earthRadiusWGS84.polar + radiusOffset;
return Ellipsoid.createCenterMatrixRadii(this.globeOrigin, this.globeMatrix, equatorRadius, equatorRadius, polarRadius);
}
getPlane(offset = 0) {
return Plane3dByOriginAndUnitNormal.create(Point3d.create(0, 0, this._bimElevationBias + offset), Vector3d.create(0, 0, 1));
}
getRayIntersection(ray, positiveOnly) {
let intersect;
if (this.globeMode === GlobeMode.Ellipsoid) {
const ellipsoid = this.geometry;
BackgroundMapGeometry._scratchRayAngles.length = 0;
BackgroundMapGeometry._scratchRayFractions.length = 0;
const count = ellipsoid.intersectRay(ray, BackgroundMapGeometry._scratchRayFractions, undefined, BackgroundMapGeometry._scratchRayAngles);
let intersectDistance;
for (let i = 0; i < count; i++) {
const thisFraction = BackgroundMapGeometry._scratchRayFractions[i];
if ((!positiveOnly || thisFraction > 0) && (undefined === intersectDistance || thisFraction < intersectDistance)) {
intersectDistance = thisFraction;
intersect = scratchIntersectRay;
ellipsoid.radiansToUnitNormalRay(BackgroundMapGeometry._scratchRayAngles[i].longitudeRadians, BackgroundMapGeometry._scratchRayAngles[i].latitudeRadians, intersect);
if (intersect.direction.dotProduct(ray.direction) < 0) {
if (this.cartesianRange.containsPoint(intersect.origin)) { // If we're in the cartesian range, correct to planar intersection.
const planeFraction = ray.intersectionWithPlane(this.cartesianPlane, scratchIntersectRay.origin);
if (undefined !== planeFraction && (!positiveOnly || planeFraction > 0)) {
intersect.direction.setFromVector3d(this.cartesianPlane.getNormalRef());
}
}
}
}
}
}
else {
const plane = this.geometry;
const thisFraction = ray.intersectionWithPlane(plane, scratchIntersectRay.origin);
if (undefined !== thisFraction && (!positiveOnly || thisFraction > 0)) {
intersect = scratchIntersectRay;
intersect.direction.setFromVector3d(plane.getNormalRef());
}
}
return intersect;
}
getPointHeight(point) {
if (this.globeMode === GlobeMode.Ellipsoid) {
const ellipsoid = this.geometry;
const projected = ellipsoid.projectPointToSurface(point);
if (undefined === projected)
return undefined;
const distance = ellipsoid.radiansToPoint(projected.longitudeRadians, projected.latitudeRadians).distance(point);
const ellipsePoint = ellipsoid.transformRef.multiplyInversePoint3d(point, BackgroundMapGeometry._scratchPoint);
return ellipsePoint.magnitude() < 1 ? -distance : distance;
}
else {
const plane = this.geometry;
return plane.altitude(point);
}
}
/** @internal */
getFrustumIntersectionDepthRange(frustum, bimRange, heightRange, gridPlane, doGlobalScope) {
const clipPlanes = frustum.getRangePlanes(false, false, 0);
const eyePoint = frustum.getEyePoint(scratchEyePoint);
const viewRotation = frustum.getRotation(scratchViewRotation);
if (undefined === viewRotation)
return Range1d.createNull(); // Degenerate frustum...
const viewZ = viewRotation.getRow(2);
const cartoRange = this.cartesianTransitionRange;
const intersectRange = Range3d.createNull();
const doAccumulate = ((point) => accumulateDepthRange(point, viewRotation, intersectRange));
if (gridPlane)
accumulateFrustumPlaneDepthRange(frustum, gridPlane, viewRotation, intersectRange, eyePoint);
if (this.geometry instanceof Plane3dByOriginAndUnitNormal) {
// Intersection with a planar background projection...
const heights = heightRange ? [heightRange.low, heightRange.high] : [0];
for (const height of heights) {
accumulateFrustumPlaneDepthRange(frustum, this.getPlane(height), viewRotation, intersectRange, eyePoint);
}
}
else {
const minOffset = heightRange ? heightRange.low : 0, maxOffset = (heightRange ? heightRange.high : 0) + this.cartesianChordHeight;
const radiusOffsets = [minOffset, maxOffset];
// If we are doing global scope then include minimum ellipsoid that represents the chordal approximation of the low level tiles.
// this substantially expands the frustum so don't do it for non-global views, but this clipping out the low level tiles.
if (doGlobalScope)
radiusOffsets.push(minOffset - this.maxGeometryChordHeight);
const toView = Transform.createRefs(Point3d.createZero(), viewRotation);
const eyePoint4d = eyePoint ? Point4d.createFromPointAndWeight(eyePoint, 1) : Point4d.createFromPointAndWeight(viewZ, 0);
for (const radiusOffset of radiusOffsets) {
const ellipsoid = this.getEarthEllipsoid(radiusOffset);
const isInside = eyePoint && ellipsoid.worldToLocal(eyePoint).magnitude() < 1.0;
const center = ellipsoid.localToWorld(scratchZeroPoint, scratchCenterPoint);
const clipPlaneCount = clipPlanes.planes.length;
// Extrema...
let angles, extremaPoint;
if (undefined !== (angles = ellipsoid.surfaceNormalToAngles(viewZ)) &&
undefined !== (extremaPoint = ellipsoid.radiansToPoint(angles.longitudeRadians, angles.latitudeRadians)) &&
(eyePoint === undefined || viewZ.dotProductStartEnd(extremaPoint, eyePoint) > 0) &&
clipPlanes.classifyPointContainment([extremaPoint], false) !== ClipPlaneContainment.StronglyOutside)
doAccumulate(extremaPoint);
if (isInside) {
if (eyePoint)
doAccumulate(eyePoint);
}
else {
const silhouette = ellipsoid.silhouetteArc(eyePoint4d);
if (silhouette !== undefined) {
silhouette.perpendicularVector.clone(scratchSilhouetteNormal);
// Push the silhouette plane as clip so that we do not include geometry at other side of ellipsoid.
// First make sure that it is pointing in the right direction.
if (eyePoint) {
// Clip toward eye.
if (scratchSilhouetteNormal.dotProduct(viewZ) < 0)
scratchSilhouetteNormal.negate(scratchSilhouetteNormal);
}
else {
/* If parallel projection - clip toward side of ellipsoid with BIM geometry */
if (Vector3d.createStartEnd(silhouette.center, bimRange.center).dotProduct(scratchSilhouetteNormal) < 0)
scratchSilhouetteNormal.negate(scratchSilhouetteNormal);
}
clipPlanes.planes.push(ClipPlane.createNormalAndDistance(scratchSilhouetteNormal, scratchSilhouetteNormal.dotProduct(silhouette.center)));
}
else {
clipPlanes.planes.push(ClipPlane.createNormalAndPoint(viewZ, center));
}
}
if (!isInside || radiusOffset === radiusOffsets[0]) {
// Intersections of ellipsoid with frustum planes...
const viewingInside = eyePoint !== undefined && viewZ.dotProduct(Vector3d.createStartEnd(center, eyePoint)) < 0;
if (eyePoint === undefined || !isInside || viewingInside) {
for (const clipPlane of clipPlanes.planes) {
const plane = clipPlane.getPlane3d();
const arc = ellipsoid.createPlaneSection(plane);
if (undefined !== arc) {
arc.announceClipIntervals(clipPlanes, (a0, a1, cp) => {
if (Math.abs(a1 - a0) < 1.0E-8) {
doAccumulate(cp.fractionToPoint(a0)); // Tiny sweep - avoid problem with rangeMethod (not worth doing anyway).
}
else {
const segment = cp.clonePartialCurve(a0, a1);
if (segment !== undefined)
segment.extendRange(intersectRange, toView);
}
});
}
}
}
// Intersections of the cartesian region with frustum planes.
scratchCartoRectangle.resize(0);
scratchCartoRectangle.push({ x: cartoRange.low.x, y: cartoRange.low.y, z: radiusOffset });
scratchCartoRectangle.push({ x: cartoRange.high.x, y: cartoRange.low.y, z: radiusOffset });
scratchCartoRectangle.push({ x: cartoRange.high.x, y: cartoRange.high.y, z: radiusOffset });
scratchCartoRectangle.push({ x: cartoRange.low.x, y: cartoRange.high.y, z: radiusOffset });
scratchCartoRectangle.push({ x: cartoRange.low.x, y: cartoRange.low.y, z: radiusOffset });
clipPlanes.clipConvexPolygonInPlace(scratchCartoRectangle, scratchWorkArray);
for (let i = 0; i < scratchCartoRectangle.length; i++)
doAccumulate(scratchCartoRectangle.getPoint3dAtUncheckedPointIndex(i));
while (clipPlanes.planes.length > clipPlaneCount) // Remove pushed silhouette plane.
clipPlanes.planes.pop();
}
}
}
if (intersectRange.zLength() < 5) {
// For the case where the fitted depth is small (less than 5 meters) we must be viewing planar projection or the
// planar portion of the iModel in plan view. In this case use a constant (arbitrarily 100 meters) depth so that the frustum
// Z is doesn't change and cause nearly planar geometry to jitter in Z buffer.
const zCenter = (intersectRange.low.z + intersectRange.high.z) / 2;
const zExpand = 50;
return Range1d.createXX(zCenter - zExpand, zCenter + zExpand);
}
else {
const diagonal = intersectRange.diagonal(scratchVector).magnitudeXY();
const expansion = diagonal * .01;
return Range1d.createXX(intersectRange.low.z - expansion, intersectRange.high.z + expansion);
}
}
addFrustumDecorations(builder, frustum) {
if (this.geometry instanceof Ellipsoid) {
const ellipsoid = this.geometry;
const clipPlanes = frustum.getRangePlanes(false, false, 0);
const viewRotation = frustum.getRotation();
const eyePoint = frustum.getEyePoint(scratchEyePoint);
const viewZ = viewRotation.getRow(2);
const eyePoint4d = eyePoint ? Point4d.createFromPointAndWeight(eyePoint, 1) : Point4d.createFromPointAndWeight(viewZ, 0);
const isInside = eyePoint && ellipsoid.worldToLocal(eyePoint).magnitude() < 1.0;
const center = ellipsoid.localToWorld(scratchZeroPoint, scratchCenterPoint);
const cartoRange = this.cartesianTransitionRange;
if (!isInside) {
const silhouette = ellipsoid.silhouetteArc(eyePoint4d);
if (silhouette !== undefined) {
silhouette.perpendicularVector.clone(scratchSilhouetteNormal);
if (scratchSilhouetteNormal.dotProduct(viewZ) < 0)
scratchSilhouetteNormal.negate(scratchSilhouetteNormal);
clipPlanes.planes.push(ClipPlane.createNormalAndDistance(scratchSilhouetteNormal, scratchSilhouetteNormal.dotProduct(silhouette.center)));
}
else {
clipPlanes.planes.push(ClipPlane.createNormalAndPoint(viewZ, center));
}
const ellipsoidColor = ColorDef.create(ColorByName.yellow);
builder.setSymbology(ellipsoidColor, ellipsoidColor, 1, LinePixels.Code2);
for (const clipPlane of clipPlanes.planes) {
const plane = clipPlane.getPlane3d();
const arc = ellipsoid.createPlaneSection(plane);
if (undefined !== arc) {
arc.announceClipIntervals(clipPlanes, (a0, a1, cp) => {
const segment = cp.clonePartialCurve(a0, a1);
if (segment !== undefined)
builder.addArc(segment, false, false);
});
}
// Intersections of the cartesian region with frustum planes.
scratchCartoRectangle.resize(0);
scratchCartoRectangle.push({ x: cartoRange.low.x, y: cartoRange.low.y, z: 0 });
scratchCartoRectangle.push({ x: cartoRange.high.x, y: cartoRange.low.y, z: 0 });
scratchCartoRectangle.push({ x: cartoRange.high.x, y: cartoRange.high.y, z: 0 });
scratchCartoRectangle.push({ x: cartoRange.low.x, y: cartoRange.high.y, z: 0 });
scratchCartoRectangle.push({ x: cartoRange.low.x, y: cartoRange.low.y, z: 0 });
clipPlanes.clipConvexPolygonInPlace(scratchCartoRectangle, scratchWorkArray);
if (scratchCartoRectangle.length > 0) {
builder.addLineString(scratchCartoRectangle.getPoint3dArray());
}
}
}
}
}
}
/** Calculate the ECEF to database (IModel) coordinate transform at a provided location, using the GCS of the iModel.
* The transform will exactly represent the GCS at the provided location.
* @public
*/
export async function calculateEcefToDbTransformAtLocation(originIn, iModel) {
const geoConverter = iModel.noGcsDefined ? undefined : iModel.geoServices.getConverter("WGS84");
if (geoConverter === undefined)
return undefined;
const origin = Point3d.create(originIn.x, originIn.y, 0); // Always Test at zero.
const eastPoint = origin.plusXYZ(1, 0, 0);
const northPoint = origin.plusXYZ(0, 1, 0);
const response = await geoConverter.getGeoCoordinatesFromIModelCoordinates([origin, northPoint, eastPoint]);
if (response.geoCoords[0].s !== GeoCoordStatus.Success || response.geoCoords[1].s !== GeoCoordStatus.Success || response.geoCoords[2].s !== GeoCoordStatus.Success)
return undefined;
const geoOrigin = Point3d.fromJSON(response.geoCoords[0].p);
const geoNorth = Point3d.fromJSON(response.geoCoords[1].p);
const geoEast = Point3d.fromJSON(response.geoCoords[2].p);
const ecefOrigin = Cartographic.fromDegrees({ longitude: geoOrigin.x, latitude: geoOrigin.y, height: geoOrigin.z }).toEcef();
const ecefNorth = Cartographic.fromDegrees({ longitude: geoNorth.x, latitude: geoNorth.y, height: geoNorth.z }).toEcef();
const ecefEast = Cartographic.fromDegrees({ longitude: geoEast.x, latitude: geoEast.y, height: geoEast.z }).toEcef();
const xVector = Vector3d.createStartEnd(ecefOrigin, ecefEast);
const yVector = Vector3d.createStartEnd(ecefOrigin, ecefNorth);
const zVector = xVector.unitCrossProduct(yVector);
if (undefined === zVector) {
assert(false); // Should never occur.
return undefined;
}
const matrix = Matrix3d.createColumns(xVector, yVector, zVector);
if (matrix === undefined)
return undefined;
const inverse = matrix.inverse();
if (inverse === undefined) {
assert(false); // Should never occur.
return undefined;
}
return Transform.createMatrixPickupPutdown(matrix, origin, ecefOrigin).inverse();
}
//# sourceMappingURL=BackgroundMapGeometry.js.map