@itwin/core-common
Version:
iTwin.js components common to frontend and backend
344 lines • 18.3 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 Geometry
*/
import { Angle, Constant, Point3d, Range1d, Range2d, Vector3d } from "@itwin/core-geometry";
import { assert } from "@itwin/core-bentley";
/** A position on the earth defined by longitude, latitude, and height above the [WGS84](https://en.wikipedia.org/wiki/World_Geodetic_System) ellipsoid.
* @public
*/
export class Cartographic {
longitude;
latitude;
height;
/**
* @param longitude longitude, in radians.
* @param latitude latitude, in radians.
* @param height The height, in meters, above the ellipsoid.
*/
constructor(longitude = 0, latitude = 0, height = 0) {
this.longitude = longitude;
this.latitude = latitude;
this.height = height;
}
/** Create a Cartographic object with longitude, latitude, and height of zero. */
static createZero() {
return new Cartographic(0, 0, 0);
}
/** Create a new Cartographic from longitude and latitude specified in radians.
* @param args an object containing a longitude, latitude, and an optional height property. The longitude and latitude properties are numbers specified in radians. The height property, if specified, is a number which contains the height in meters above the ellipsoid; if undefined, this height will default to zero.
* @param result The object onto which to store the result.
*/
static fromRadians(args, result) {
if (!result)
return new Cartographic(args.longitude, args.latitude, args.height);
result.longitude = args.longitude;
result.latitude = args.latitude;
result.height = args.height !== undefined ? args.height : 0;
return result;
}
/** Create a JSON representation of a Cartographic object. */
toJSON() {
return {
latitude: this.latitude,
longitude: this.longitude,
height: this.height,
};
}
/** Freeze this Cartographic */
freeze() {
return Object.freeze(this);
}
/** longitude, in degrees */
get longitudeDegrees() {
return Angle.radiansToDegrees(this.longitude);
}
/** latitude, in degrees */
get latitudeDegrees() {
return Angle.radiansToDegrees(this.latitude);
}
static _oneMinusF = 1 - (Constant.earthRadiusWGS84.equator - Constant.earthRadiusWGS84.polar) / Constant.earthRadiusWGS84.equator;
static _equatorOverPolar = Constant.earthRadiusWGS84.equator / Constant.earthRadiusWGS84.polar;
/** return the geocentric latitude angle for the input geodetic latitude angle (both in radians).
* @param geodeticLatitude geodetic latitude angle in radians
*/
static geocentricLatitudeFromGeodeticLatitude(geodeticLatitude) {
return Math.atan(Cartographic._oneMinusF * Cartographic._oneMinusF * Math.tan(geodeticLatitude));
}
/** return the parametric latitude angle for the input geodetic latitude angle (both in radians). The parametric latitude
* is appropriate for input to the Ellipsoid methods.
* @param geodeticLatitude geodetic latitude angle in radians
*/
static parametricLatitudeFromGeodeticLatitude(geodeticLatitude) {
return Math.atan(Cartographic._oneMinusF * Cartographic._oneMinusF * Cartographic._equatorOverPolar * Math.tan(geodeticLatitude));
}
/** Create a new Cartographic from longitude and latitude specified in degrees. The values in the resulting object will be in radians.
* @param args an object containing a longitude, latitude, and an optional height property. The longitude and latitude properties are numbers specified in degrees. The height property, if specified, is a number which contains the height in meters above the ellipsoid; if undefined, this height will default to zero.
* @param result The object onto which to store the result.
*/
static fromDegrees(args, result) {
return Cartographic.fromRadians({ longitude: Angle.degreesToRadians(args.longitude), latitude: Angle.degreesToRadians(args.latitude), height: args.height }, result);
}
/** Create a new Cartographic from longitude and latitude in [Angle]($geometry)s. The values in the resulting object will be in radians.
* @param args an object containing a longitude, latitude, and an optional height property. The longitude and latitude properties are Angle objects. The height property, if specified, is a number which contains the height in meters above the ellipsoid; if undefined, this height will default to zero.
* @param result The object into which to store the result (optional)
*/
static fromAngles(args, result) {
return Cartographic.fromRadians({ longitude: args.longitude.radians, latitude: args.latitude.radians, height: args.height }, result);
}
static _cartesianToCartographicN = new Point3d();
static _cartesianToCartographicP = new Point3d();
static _cartesianToCartographicH = new Vector3d();
static _wgs84OneOverRadii = new Point3d(1.0 / 6378137.0, 1.0 / 6378137.0, 1.0 / 6356752.3142451793);
static _wgs84OneOverRadiiSquared = new Point3d(1.0 / (6378137.0 * 6378137.0), 1.0 / (6378137.0 * 6378137.0), 1.0 / (6356752.3142451793 * 6356752.3142451793));
static _wgs84RadiiSquared = new Point3d(6378137.0 * 6378137.0, 6378137.0 * 6378137.0, 6356752.3142451793 * 6356752.3142451793);
static _wgs84CenterToleranceSquared = 0.1;
static _scratchN = new Vector3d();
static _scratchK = new Vector3d();
/** Creates a new Cartographic from an [ECEF](https://en.wikipedia.org/wiki/ECEF) position.
* @param cartesian The position, in ECEF, to convert to cartographic representation.
* @param [result] The object onto which to store the result.
* @returns The modified result parameter, new Cartographic instance if none was provided, or undefined if the cartesian is at the center of the ellipsoid.
*/
static fromEcef(cartesian, result) {
const oneOverRadiiSquared = Cartographic._wgs84OneOverRadiiSquared;
const p = Cartographic.scalePointToGeodeticSurface(cartesian, Cartographic._cartesianToCartographicP);
if (!p)
return undefined;
const n = Cartographic._cartesianToCartographicN;
Cartographic.multiplyComponents(p, oneOverRadiiSquared, n);
Cartographic.normalize(n, n);
const h = p.vectorTo(cartesian, Cartographic._cartesianToCartographicH);
const longitude = Math.atan2(n.y, n.x);
const latitude = Math.asin(n.z);
const height = Math.sign(h.dotProduct(cartesian)) * h.magnitude();
if (!result)
return new Cartographic(longitude, latitude, height);
result.longitude = longitude;
result.latitude = latitude;
result.height = height;
return result;
}
/** Scale point to geodetic surface
* @param point in ECEF to scale to the surface
* @param [result] The object onto which to store the result.
* @returns a point on the geodetic surface
*/
static scalePointToGeodeticSurface(point, result) {
const oneOverRadii = Cartographic._wgs84OneOverRadii;
const oneOverRadiiSquared = Cartographic._wgs84OneOverRadiiSquared;
const centerToleranceSquared = Cartographic._wgs84CenterToleranceSquared;
return Cartographic._scaleToGeodeticSurface(point, oneOverRadii, oneOverRadiiSquared, centerToleranceSquared, result);
}
/** Duplicates a Cartographic. */
clone(result) {
if (!result)
return new Cartographic(this.longitude, this.latitude, this.height);
result.longitude = this.longitude;
result.latitude = this.latitude;
result.height = this.height;
return result;
}
/** Return true if this Cartographic is the same as right */
equals(right) {
return (this === right) ||
((this.longitude === right.longitude) &&
(this.latitude === right.latitude) &&
(this.height === right.height));
}
/** Compares this Cartographic component-wise and returns true if they are within the provided epsilon, */
equalsEpsilon(right, epsilon) {
return (this === right) ||
((Math.abs(this.longitude - right.longitude) <= epsilon) &&
(Math.abs(this.latitude - right.latitude) <= epsilon) &&
(Math.abs(this.height - right.height) <= epsilon));
}
static normalize(cartesian, result) {
const magnitude = cartesian.magnitude();
result.x = cartesian.x / magnitude;
result.y = cartesian.y / magnitude;
result.z = cartesian.z / magnitude;
}
static multiplyComponents(left, right, result) {
result.x = left.x * right.x;
result.y = left.y * right.y;
result.z = left.z * right.z;
}
static scalePoint(cartesian, scalar, result) {
result.x = cartesian.x * scalar;
result.y = cartesian.y * scalar;
result.z = cartesian.z * scalar;
}
static addPoints(left, right, result) {
result.x = left.x + right.x;
result.y = left.y + right.y;
result.z = left.z + right.z;
}
/** Create a string representing this cartographic in the format '(longitude, latitude, height)'. */
toString() { return `(${this.longitude}, ${this.latitude}, ${this.height})`; }
static _scaleToGeodeticSurfaceIntersection = new Point3d();
static _scaleToGeodeticSurfaceGradient = new Point3d();
static _scaleToGeodeticSurface(cartesian, oneOverRadii, oneOverRadiiSquared, centerToleranceSquared, result) {
const positionX = cartesian.x;
const positionY = cartesian.y;
const positionZ = cartesian.z;
const oneOverRadiiX = oneOverRadii.x;
const oneOverRadiiY = oneOverRadii.y;
const oneOverRadiiZ = oneOverRadii.z;
const x2 = positionX * positionX * oneOverRadiiX * oneOverRadiiX;
const y2 = positionY * positionY * oneOverRadiiY * oneOverRadiiY;
const z2 = positionZ * positionZ * oneOverRadiiZ * oneOverRadiiZ;
// Compute the squared ellipsoid norm.
const squaredNorm = x2 + y2 + z2;
const ratio = Math.sqrt(1.0 / squaredNorm);
// As an initial approximation, assume that the radial intersection is the projection point.
const intersection = Cartographic._scaleToGeodeticSurfaceIntersection;
Cartographic.scalePoint(cartesian, ratio, intersection);
// If the position is near the center, the iteration will not converge.
if (squaredNorm < centerToleranceSquared) {
return !isFinite(ratio) ? undefined : Point3d.createFrom(intersection, result);
}
const oneOverRadiiSquaredX = oneOverRadiiSquared.x;
const oneOverRadiiSquaredY = oneOverRadiiSquared.y;
const oneOverRadiiSquaredZ = oneOverRadiiSquared.z;
// Use the gradient at the intersection point in place of the true unit normal.
// The difference in magnitude will be absorbed in the multiplier.
const gradient = Cartographic._scaleToGeodeticSurfaceGradient;
gradient.x = intersection.x * oneOverRadiiSquaredX * 2.0;
gradient.y = intersection.y * oneOverRadiiSquaredY * 2.0;
gradient.z = intersection.z * oneOverRadiiSquaredZ * 2.0;
// Compute the initial guess at the normal vector multiplier, lambda.
let lambda = (1.0 - ratio) * cartesian.magnitude() / (0.5 * gradient.magnitude());
let correction = 0.0;
let func;
let denominator;
let xMultiplier;
let yMultiplier;
let zMultiplier;
let xMultiplier2;
let yMultiplier2;
let zMultiplier2;
let xMultiplier3;
let yMultiplier3;
let zMultiplier3;
do {
lambda -= correction;
xMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquaredX);
yMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquaredY);
zMultiplier = 1.0 / (1.0 + lambda * oneOverRadiiSquaredZ);
xMultiplier2 = xMultiplier * xMultiplier;
yMultiplier2 = yMultiplier * yMultiplier;
zMultiplier2 = zMultiplier * zMultiplier;
xMultiplier3 = xMultiplier2 * xMultiplier;
yMultiplier3 = yMultiplier2 * yMultiplier;
zMultiplier3 = zMultiplier2 * zMultiplier;
func = x2 * xMultiplier2 + y2 * yMultiplier2 + z2 * zMultiplier2 - 1.0;
// "denominator" here refers to the use of this expression in the velocity and acceleration
// computations in the sections to follow.
denominator = x2 * xMultiplier3 * oneOverRadiiSquaredX + y2 * yMultiplier3 * oneOverRadiiSquaredY + z2 * zMultiplier3 * oneOverRadiiSquaredZ;
const derivative = -2.0 * denominator;
correction = func / derivative;
} while (Math.abs(func) > 0.01);
if (!result)
return new Point3d(positionX * xMultiplier, positionY * yMultiplier, positionZ * zMultiplier);
result.x = positionX * xMultiplier;
result.y = positionY * yMultiplier;
result.z = positionZ * zMultiplier;
return result;
}
/** Return an ECEF point from a Cartographic point */
toEcef(result) {
const cosLatitude = Math.cos(this.latitude);
const scratchN = Cartographic._scratchN;
const scratchK = Cartographic._scratchK;
scratchN.x = cosLatitude * Math.cos(this.longitude);
scratchN.y = cosLatitude * Math.sin(this.longitude);
scratchN.z = Math.sin(this.latitude);
Cartographic.normalize(scratchN, scratchN);
Cartographic.multiplyComponents(Cartographic._wgs84RadiiSquared, scratchN, scratchK);
const gamma = Math.sqrt(scratchN.dotProduct(scratchK));
Cartographic.scalePoint(scratchK, 1.0 / gamma, scratchK);
Cartographic.scalePoint(scratchN, this.height, scratchN);
result = result ? result : new Point3d();
Cartographic.addPoints(scratchK, scratchN, result);
return result;
}
}
/** A cartographic range representing a rectangular region if low longitude/latitude > high then area crossing seam is indicated.
* @public
*/
export class CartographicRange {
_ranges = [];
// These following are used to preserve the min/max latitude and longitudes.
// The longitudes are raw values and may cross over the -PI or 2PI boundaries.
_minLongitude = 0;
_maxLongitude = 0;
_minLatitude = 0;
_maxLatitude = 0;
constructor(spatialRange, spatialToEcef) {
// Compute 8 corners in spatial coordinate system before converting to ECEF
// We want a box oriented in the spatial coordinate system and not in the ECEF coordinate system
const spatialCorners = spatialRange.corners();
const ecefCorners = spatialToEcef.multiplyPoint3dArray(spatialCorners);
let low, high;
for (const ecefCorner of ecefCorners) {
const geoPt = Cartographic.fromEcef(ecefCorner);
if (!geoPt)
continue;
if (undefined === low || undefined === high) {
low = geoPt;
high = geoPt.clone();
continue;
}
low.latitude = Math.min(low.latitude, geoPt.latitude);
low.longitude = Math.min(low.longitude, geoPt.longitude);
high.latitude = Math.max(high.latitude, geoPt.latitude);
high.longitude = Math.max(high.longitude, geoPt.longitude);
}
if (!low || !high) {
assert(false);
return;
}
const longitudeRanges = [];
this._minLongitude = Math.min(low.longitude, high.longitude), this._maxLongitude = Math.max(low.longitude, high.longitude);
if (this._maxLongitude - this._minLongitude > Angle.piRadians) {
longitudeRanges.push(Range1d.createXX(0.0, this._minLongitude));
longitudeRanges.push(Range1d.createXX(this._maxLongitude, Angle.pi2Radians));
}
else {
longitudeRanges.push(Range1d.createXX(this._minLongitude, this._maxLongitude));
}
for (const longitudeRange of longitudeRanges) {
this._minLatitude = Math.min(low.latitude, high.latitude), this._maxLatitude = Math.max(low.latitude, high.latitude);
if (this._maxLatitude - this._minLatitude > Angle.piOver2Radians) {
this._ranges.push(Range2d.createXYXY(longitudeRange.low, 0.0, longitudeRange.high, this._minLatitude));
this._ranges.push(Range2d.createXYXY(longitudeRange.low, this._maxLatitude, longitudeRange.high, Angle.piRadians));
}
else {
this._ranges.push(Range2d.createXYXY(longitudeRange.low, this._minLatitude, longitudeRange.high, this._maxLatitude));
}
}
}
intersectsRange(other) {
for (const range of this._ranges)
for (const otherRange of other._ranges)
if (range.intersectsRange(otherRange))
return true;
return false;
}
/** This method returns the raw latitude / longitude for the range in a Range2d object.
* The X value represents the longitude and the Y value the latitudes.
* Y values are kept between -PI and +PI while
* longitude values can be expressed in any range between -2PI to +2PI
* given the minimum longitude is always smaller numerically than the maximum longitude.
* Note that usually the longitudes are usually by convention in the range of -PI to PI except
* for ranges that overlap the -PI/+PI frontier in which case either representation is acceptable.
*/
getLongitudeLatitudeBoundingBox() {
return Range2d.createXYXY(this._minLongitude, this._minLatitude, this._maxLongitude, this._maxLatitude);
}
}
//# sourceMappingURL=Cartographic.js.map