UNPKG

@vizlabdev/geodesy

Version:
446 lines (356 loc) 16.2 kB
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Vector-based ellipsoidal geodetic (latitude/longitude) functions (c) Chris Veness 2015-2021 */ /* MIT Licence */ /* www.movable-type.co.uk/scripts/latlong-vectors.html */ /* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-nvector-ellipsoidal */ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ import LatLonEllipsoidal, { Cartesian, Vector3d, Dms } from './latlon-ellipsoidal.js'; /** * Tools for working with points on (ellipsoidal models of) the earth’s surface using a vector-based * approach using ‘n-vectors’ (rather than the more common spherical trigonometry). * * Based on Kenneth Gade’s ‘Non-singular Horizontal Position Representation’. * * Note that these formulations take x => 0°N,0°E, y => 0°N,90°E, z => 90°N (in order that n-vector * = cartesian vector at 0°N,0°E); Gade uses x => 90°N, y => 0°N,90°E, z => 0°N,0°E. * * @module latlon-nvector-ellipsoidal */ /* LatLon_NvectorEllipsoidal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * Latitude/longitude points on an ellipsoidal model earth augmented with methods for calculating * delta vectors between points, and converting to n-vectors. * * @extends LatLonEllipsoidal */ class LatLon_NvectorEllipsoidal extends LatLonEllipsoidal { /** * Calculates delta from ‘this’ point to supplied point. * * The delta is given as a north-east-down NED vector. Note that this is a linear delta, * unrelated to a geodesic on the ellipsoid. * * Points need not be defined on the same datum. * * @param {LatLon} point - Point delta is to be determined to. * @returns {Ned} Delta from ‘this’ point to supplied point in local tangent plane of this point. * @throws {TypeError} Invalid point. * * @example * const a = new LatLon(49.66618, 3.45063, 99); * const b = new LatLon(48.88667, 2.37472, 64); * const delta = a.deltaTo(b); // [N:-86127,E:-78901,D:1104] * const dist = delta.length; // 116809.178 m * const brng = delta.bearing; // 222.493° * const elev = delta.elevation; // -0.5416° */ deltaTo(point) { if (!(point instanceof LatLonEllipsoidal)) throw new TypeError(`invalid point ‘${point}’`); // get delta in cartesian frame const c1 = this.toCartesian(); const c2 = point.toCartesian(); const δc = c2.minus(c1); // get local (n-vector) coordinate frame const n1 = this.toNvector(); const a = new Vector3d(0, 0, 1); // axis vector pointing to 90°N const d = n1.negate(); // down (pointing opposite to n-vector) const e = a.cross(n1).unit(); // east (pointing perpendicular to the plane) const n = e.cross(d); // north (by right hand rule) // rotation matrix is built from n-vector coordinate frame axes (using row vectors) const r = [ [ n.x, n.y, n.z ], [ e.x, e.y, e.z ], [ d.x, d.y, d.z ], ]; // apply rotation to δc to get delta in n-vector reference frame const δn = new Cartesian( r[0][0]*δc.x + r[0][1]*δc.y + r[0][2]*δc.z, r[1][0]*δc.x + r[1][1]*δc.y + r[1][2]*δc.z, r[2][0]*δc.x + r[2][1]*δc.y + r[2][2]*δc.z, ); return new Ned(δn.x, δn.y, δn.z); } /** * Calculates destination point using supplied delta from ‘this’ point. * * The delta is given as a north-east-down NED vector. Note that this is a linear delta, * unrelated to a geodesic on the ellipsoid. * * @param {Ned} delta - Delta from ‘this’ point to supplied point in local tangent plane of this point. * @returns {LatLon} Destination point. * * @example * const a = new LatLon(49.66618, 3.45063, 99); * const delta = Ned.fromDistanceBearingElevation(116809.178, 222.493, -0.5416); // [N:-86127,E:-78901,D:1104] * const b = a.destinationPoint(delta); // 48.8867°N, 002.3747°E */ destinationPoint(delta) { if (!(delta instanceof Ned)) throw new TypeError('delta is not Ned object'); // convert North-East-Down delta to standard x/y/z vector in coordinate frame of n-vector const δn = new Vector3d(delta.north, delta.east, delta.down); // get local (n-vector) coordinate frame const n1 = this.toNvector(); const a = new Vector3d(0, 0, 1); // axis vector pointing to 90°N const d = n1.negate(); // down (pointing opposite to n-vector) const e = a.cross(n1).unit(); // east (pointing perpendicular to the plane) const n = e.cross(d); // north (by right hand rule) // rotation matrix is built from n-vector coordinate frame axes (using column vectors) const r = [ [ n.x, e.x, d.x ], [ n.y, e.y, d.y ], [ n.z, e.z, d.z ], ]; // apply rotation to δn to get delta in cartesian (ECEF) coordinate reference frame const δc = new Cartesian( r[0][0]*δn.x + r[0][1]*δn.y + r[0][2]*δn.z, r[1][0]*δn.x + r[1][1]*δn.y + r[1][2]*δn.z, r[2][0]*δn.x + r[2][1]*δn.y + r[2][2]*δn.z, ); // apply (cartesian) delta to c1 to obtain destination point as cartesian coordinate const c1 = this.toCartesian(); // convert this LatLon to Cartesian const v2 = c1.plus(δc); // the plus() gives us a plain vector,.. const c2 = new Cartesian(v2.x, v2.y, v2.z); // ... need to convert it to Cartesian to get LatLon // return destination cartesian coordinate as latitude/longitude return c2.toLatLon(); } /** * Converts ‘this’ lat/lon point to n-vector (normal to the earth's surface). * * @returns {Nvector} N-vector representing lat/lon point. * * @example * const p = new LatLon(45, 45); * const n = p.toNvector(); // [0.5000,0.5000,0.7071] */ toNvector() { // note: replicated in LatLonNvectorSpherical const φ = this.lat.toRadians(); const λ = this.lon.toRadians(); const sinφ = Math.sin(φ), cosφ = Math.cos(φ); const sinλ = Math.sin(λ), cosλ = Math.cos(λ); // right-handed vector: x -> 0°E,0°N; y -> 90°E,0°N, z -> 90°N const x = cosφ * cosλ; const y = cosφ * sinλ; const z = sinφ; return new NvectorEllipsoidal(x, y, z, this.h, this.datum); } /** * Converts ‘this’ point from (geodetic) latitude/longitude coordinates to (geocentric) cartesian * (x/y/z) coordinates. * * @returns {Cartesian} Cartesian point equivalent to lat/lon point, with x, y, z in metres from * earth centre. */ toCartesian() { const c = super.toCartesian(); // c is 'Cartesian' // return Cartesian_Nvector to have toNvector() available as method of exported LatLon return new Cartesian_Nvector(c.x, c.y, c.z); } } /* Nvector - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * An n-vector is a position representation using a (unit) vector normal to the Earth ellipsoid. * Unlike latitude/longitude points, n-vectors have no singularities or discontinuities. * * For many applications, n-vectors are more convenient to work with than other position * representations such as latitude/longitude, earth-centred earth-fixed (ECEF) vectors, UTM * coordinates, etc. * * @extends Vector3d */ class NvectorEllipsoidal extends Vector3d { // note commonality with latlon-nvector-spherical /** * Creates a 3d n-vector normal to the Earth's surface. * * @param {number} x - X component of n-vector (towards 0°N, 0°E). * @param {number} y - Y component of n-vector (towards 0°N, 90°E). * @param {number} z - Z component of n-vector (towards 90°N). * @param {number} [h=0] - Height above ellipsoid surface in metres. * @param {LatLon.datums} [datum=WGS84] - Datum this n-vector is defined within. */ constructor(x, y, z, h=0, datum=LatLonEllipsoidal.datums.WGS84) { const u = new Vector3d(x, y, z).unit(); // n-vectors are always normalised super(u.x, u.y, u.z); this.h = Number(h); this.datum = datum; } /** * Converts ‘this’ n-vector to latitude/longitude point. * * @returns {LatLon} Latitude/longitude point equivalent to this n-vector. * * @example * const p = new Nvector(0.500000, 0.500000, 0.707107).toLatLon(); // 45.0000°N, 045.0000°E */ toLatLon() { // tanφ = z / √(x²+y²), tanλ = y / x (same as spherical calculation) const { x, y, z } = this; const φ = Math.atan2(z, Math.sqrt(x*x + y*y)); const λ = Math.atan2(y, x); return new LatLon_NvectorEllipsoidal(φ.toDegrees(), λ.toDegrees(), this.h, this.datum); } /** * Converts ‘this’ n-vector to cartesian coordinate. * * qv Gade 2010 ‘A Non-singular Horizontal Position Representation’ eqn 22 * * @returns {Cartesian} Cartesian coordinate equivalent to this n-vector. * * @example * const c = new Nvector(0.500000, 0.500000, 0.707107).toCartesian(); // [3194419,3194419,4487349] * const p = c.toLatLon(); // 45.0000°N, 045.0000°E */ toCartesian() { const { b, f } = this.datum.ellipsoid; const { x, y, z, h } = this; const m = (1-f) * (1-f); // (1−f)² = b²/a² const n = b / Math.sqrt(x*x/m + y*y/m + z*z); const xʹ = n * x / m + x*h; const yʹ = n * y / m + y*h; const zʹ = n * z + z*h; return new Cartesian_Nvector(xʹ, yʹ, zʹ); } /** * Returns a string representation of ‘this’ (unit) n-vector. Height component is only shown if * dpHeight is specified. * * @param {number} [dp=3] - Number of decimal places to display. * @param {number} [dpHeight=null] - Number of decimal places to use for height; default is no height display. * @returns {string} Comma-separated x, y, z, h values. * * @example * new Nvector(0.5000, 0.5000, 0.7071).toString(); // [0.500,0.500,0.707] * new Nvector(0.5000, 0.5000, 0.7071, 1).toString(6, 0); // [0.500002,0.500002,0.707103+1m] */ toString(dp=3, dpHeight=null) { const { x, y, z } = this; const h = `${this.h>=0 ? '+' : ''}${this.h.toFixed(dpHeight)}m`; return `[${x.toFixed(dp)},${y.toFixed(dp)},${z.toFixed(dp)}${dpHeight==null ? '' : h}]`; } } /* Cartesian - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * Cartesian_Nvector extends Cartesian with method to convert cartesian coordinates to n-vectors. * * @extends Cartesian */ class Cartesian_Nvector extends Cartesian { /** * Converts ‘this’ cartesian coordinate to an n-vector. * * qv Gade 2010 ‘A Non-singular Horizontal Position Representation’ eqn 23 * * @param {LatLon.datums} [datum=WGS84] - Datum to use for conversion. * @returns {Nvector} N-vector equivalent to this cartesian coordinate. * * @example * const c = new Cartesian(3980581, 97, 4966825); * const n = c.toNvector(); // { x: 0.6228, y: 0.0000, z: 0.7824, h: 0.0000 } */ toNvector(datum=LatLonEllipsoidal.datums.WGS84) { const { a, f } = datum.ellipsoid; const { x, y, z } = this; const e2 = 2*f - f*f; // e² = 1st eccentricity squared ≡ (a²-b²)/a² const e4 = e2*e2; // e⁴ const p = (x*x + y*y) / (a*a); const q = z*z * (1-e2) / (a*a); const r = (p + q - e4) / 6; const s = (e4*p*q) / (4*r*r*r); const t = Math.cbrt(1 + s + Math.sqrt(2*s+s*s)); const u = r * (1 + t + 1/t); const v = Math.sqrt(u*u + e4*q); const w = e2 * (u + v - q) / (2*v); const k = Math.sqrt(u + v + w*w) - w; const d = k * Math.sqrt(x*x + y*y) / (k + e2); const tmp = 1 / Math.sqrt(d*d + z*z); const xʹ = tmp * k/(k+e2) * x; const yʹ = tmp * k/(k+e2) * y; const zʹ = tmp * z; const h = (k + e2 - 1)/k * Math.sqrt(d*d + z*z); const n = new NvectorEllipsoidal(xʹ, yʹ, zʹ, h, datum); return n; } } /* Ned - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /** * North-east-down (NED), also known as local tangent plane (LTP), is a vector in the local * coordinate frame of a body. */ class Ned { /** * Creates North-East-Down vector. * * @param {number} north - North component in metres. * @param {number} east - East component in metres. * @param {number} down - Down component (normal to the surface of the ellipsoid) in metres. * * @example * import { Ned } from '/js/geodesy/latlon-nvector-ellipsoidal.js'; * const delta = new Ned(110569, 111297, 1936); // [N:110569,E:111297,D:1936] */ constructor(north, east, down) { this.north = north; this.east = east; this.down = down; } /** * Length of NED vector. * * @returns {number} Length of NED vector in metres. */ get length() { const { north, east, down } = this; return Math.sqrt(north*north + east*east + down*down); } /** * Bearing of NED vector. * * @returns {number} Bearing of NED vector in degrees from north. */ get bearing() { const θ = Math.atan2(this.east, this.north); return Dms.wrap360(θ.toDegrees()); // normalise to range 0..360° } /** * Elevation of NED vector. * * @returns {number} Elevation of NED vector in degrees from horizontal (ie tangent to ellipsoid surface). */ get elevation() { const α = Math.asin(this.down/this.length); return -α.toDegrees(); } /** * Creates North-East-Down vector from distance, bearing, & elevation (in local coordinate system). * * @param {number} dist - Length of NED vector in metres. * @param {number} brng - Bearing (in degrees from north) of NED vector . * @param {number} elev - Elevation (in degrees from local coordinate frame horizontal) of NED vector. * @returns {Ned} North-East-Down vector equivalent to distance, bearing, elevation. * * @example * const delta = Ned.fromDistanceBearingElevation(116809.178, 222.493, -0.5416); // [N:-86127,E:-78901,D:1104] */ static fromDistanceBearingElevation(dist, brng, elev) { const θ = Number(brng).toRadians(); const α = Number(elev).toRadians(); dist = Number(dist); const sinθ = Math.sin(θ), cosθ = Math.cos(θ); const sinα = Math.sin(α), cosα = Math.cos(α); const n = cosθ * dist*cosα; const e = sinθ * dist*cosα; const d = -sinα * dist; return new Ned(n, e, d); } /** * Returns a string representation of ‘this’ NED vector. * * @param {number} [dp=0] - Number of decimal places to display. * @returns {string} Comma-separated (labelled) n, e, d values. */ toString(dp=0) { return `[N:${this.north.toFixed(dp)},E:${this.east.toFixed(dp)},D:${this.down.toFixed(dp)}]`; } } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ export { LatLon_NvectorEllipsoidal as default, NvectorEllipsoidal as Nvector, Cartesian_Nvector as Cartesian, Ned, Dms };