Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

geodesy

Package Overview
Dependencies
Maintainers
1
Versions
14
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

geodesy - npm Package Compare versions

Comparing version 2.0.0 to 2.0.1

14

CHANGELOG.md
# Changelog
## [2.0.1] - 2019-04-10
### Fixed
- Add missing n-vector spherical alongTrackDistanceTo() method
- Add missing .toUtm() method to LatLon object returned by Utm.toLatLon()
- Fix n-vector spherical isWithinExtent() for point in different hemisphere
- Fix vector3d angleTo() for case where plane normal n is in the plane
- Rationalise/harmonise exception messages
### Added
- README ‘docs’ badge with link to documentation
## [2.0.0] - 2019-02-14

@@ -4,0 +18,0 @@

2

dms.js

@@ -270,3 +270,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

static compassPoint(bearing, precision=3) {
if (![ 1,2,3 ].includes(Number(precision))) throw new RangeError('‘precision’ must be 1, 2 or 3'); // eslint-disable-line comma-spacing
if (![ 1, 2, 3 ].includes(Number(precision))) throw new RangeError(`invalid precision ‘${precision}’`);
// note precision could be extended to 4 for quarter-winds (eg NbNW), but I think they are little used

@@ -273,0 +273,0 @@

@@ -118,3 +118,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

constructor(lat, lon, height=0, datum=datums.WGS84) {
if (!datum || datum.transform==undefined) throw new TypeError(`Unrecognised datum ‘${datum}’`);
if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);

@@ -201,3 +201,3 @@ super(lat, lon, height);

if (!datum || datum.transform==undefined) throw new TypeError(`Unrecognised datum ‘${datum}’`);
if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);

@@ -224,3 +224,3 @@ const point = super.parse(...args);

convertDatum(toDatum) {
if (toDatum == undefined || toDatum.transform == undefined) throw new TypeError('Unrecognised datum');
if (!toDatum || toDatum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${toDatum}’`);

@@ -294,3 +294,3 @@ let oldLatLon = this;

toLatLon(datum=datums.WGS84) {
if (!datum) throw new TypeError('Unrecognised datum');
if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);
const latLon = super.toLatLon(datum.ellipsoid);

@@ -312,3 +312,3 @@ return new LatLonEllipsoidal_Datum(latLon.lat, latLon.lon, latLon.height, datum);

// this point
const x1 = this.x, y1 = this.y, z1 = this.z;
const { x: x1, y: y1, z: z1 } = this;

@@ -315,0 +315,0 @@ // transform parameters

@@ -115,4 +115,4 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

constructor(lat, lon, height=0, referenceFrame=referenceFrames.ITRF2014, epoch=undefined) {
if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('Unrecognised reference frame');
if (epoch != undefined && isNaN(Number(epoch))) throw new TypeError(`Invalid epoch ’${epoch}’`);
if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('unrecognised reference frame');
if (epoch != undefined && isNaN(Number(epoch))) throw new TypeError(`invalid epoch ’${epoch}’`);

@@ -210,3 +210,3 @@ super(lat, lon, height);

static parse(...args) {
if (args.length == 0) throw new TypeError('Invalid (empty) coordinate');
if (args.length == 0) throw new TypeError('invalid (empty) point');

@@ -225,3 +225,3 @@ let referenceFrame = null, epoch = null;

if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('Unrecognised reference frame');
if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('unrecognised reference frame');

@@ -251,3 +251,3 @@ // args is now lat, lon, height or latlon, height as taken by LatLonEllipsoidal .parse()

convertReferenceFrame(toReferenceFrame) {
if (!toReferenceFrame || toReferenceFrame.epoch == undefined) throw new TypeError('Unrecognised reference frame');
if (!toReferenceFrame || toReferenceFrame.epoch == undefined) throw new TypeError('unrecognised reference frame');

@@ -335,4 +335,4 @@ const oldCartesian = this.toCartesian(); // convert geodetic to cartesian

constructor(x, y, z, referenceFrame=undefined, epoch=undefined) {
if (referenceFrame!=undefined && referenceFrame.epoch==undefined) throw new TypeError('Unrecognised reference frame');
if (epoch!=undefined && isNaN(Number(epoch))) throw new TypeError(`Invalid epoch ’${epoch}’`);
if (referenceFrame!=undefined && referenceFrame.epoch==undefined) throw new TypeError('unrecognised reference frame');
if (epoch!=undefined && isNaN(Number(epoch))) throw new TypeError(`invalid epoch ’${epoch}’`);

@@ -353,3 +353,3 @@ super(x, y, z);

set referenceFrame(referenceFrame) {
if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('Unrecognised reference frame');
if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('unrecognised reference frame');
this._referenceFrame = referenceFrame;

@@ -365,3 +365,3 @@ }

set epoch(epoch) {
if (isNaN(Number(epoch))) throw new TypeError(`Invalid epoch ’${epoch}’`);
if (isNaN(Number(epoch))) throw new TypeError(`invalid epoch ’${epoch}’`);
if (this._epoch != this._referenceFrame.epoch) this._epoch = Number(epoch);

@@ -386,3 +386,3 @@ }

toLatLon() {
if (!this.referenceFrame) throw new Error('Cartesian reference frame not defined');
if (!this.referenceFrame) throw new Error('cartesian reference frame not defined');

@@ -412,4 +412,4 @@ const latLon = super.toLatLon(this.referenceFrame.ellipsoid);

convertReferenceFrame(toReferenceFrame) {
if (!toReferenceFrame || toReferenceFrame.epoch == undefined) throw new TypeError('Unrecognised reference frame');
if (!this.referenceFrame) throw new TypeError('Cartesian coordinate has no reference frame');
if (!toReferenceFrame || toReferenceFrame.epoch == undefined) throw new TypeError('unrecognised reference frame');
if (!this.referenceFrame) throw new TypeError('cartesian coordinate has no reference frame');

@@ -530,7 +530,7 @@ if (this.referenceFrame.name == toReferenceFrame.name) return this; // no-op!

toString(dp=0) {
const x = this.x.toFixed(dp), y = this.y.toFixed(dp), z = this.z.toFixed(dp);
const { x, y, z } = this;
const epochFmt = { useGrouping: false, minimumFractionDigits: 1, maximumFractionDigits: 20 };
const epoch = this.referenceFrame && this.epoch != this.referenceFrame.epoch ? this.epoch.toLocaleString('en', epochFmt) : '';
const trf = this.referenceFrame ? `(${this.referenceFrame.name}${epoch?'@'+epoch:''})` : '';
return `[${x},${y},${z}]${trf}`;
return `[${x.toFixed(dp)},${y.toFixed(dp)},${z.toFixed(dp)}]${trf}`;
}

@@ -537,0 +537,0 @@ }

@@ -164,3 +164,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

direct(distance, initialBearing) {
if (this.height != 0) throw new RangeError('Point must be on the surface of the ellipsoid');
if (this.height != 0) throw new RangeError('point must be on the surface of the ellipsoid');

@@ -171,5 +171,5 @@ const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();

const ellipsoid = this.datum ? this.datum.ellipsoid :
this.referenceFrame ? this.referenceFrame.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const a = ellipsoid.a, b = ellipsoid.b, f = ellipsoid.f;
// allow alternative ellipsoid to be specified
const ellipsoid = this.datum ? this.datum.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const { a, b, f } = ellipsoid;

@@ -210,3 +210,3 @@ const sinα1 = Math.sin(α1);

const destinationPoint = new LatLonEllipsoidal_Vincenty(φ2.toDegrees(), Dms.wrap180(λ2.toDegrees()), 0, this.datum);
const destinationPoint = new LatLonEllipsoidal_Vincenty(φ2.toDegrees(), λ2.toDegrees(), 0, this.datum);

@@ -229,3 +229,3 @@ return {

* @returns {Object} Object including distance, initialBearing, finalBearing.
* @throws {TypeError} Point not LatLon object.
* @throws {TypeError} Invalid point.
* @throws {RangeError} Points must be on surface of ellipsoid.

@@ -235,4 +235,4 @@ * @throws {EvalError} Formula failed to converge.

inverse(point) {
if (!(point instanceof LatLonEllipsoidal)) throw new TypeError('‘point’ is not (Ellipsoidal) LatLon object');
if (this.height!=0 || point.height!=0) throw new RangeError('Point must be on the surface of the ellipsoid');
if (!(point instanceof LatLonEllipsoidal)) throw new TypeError(`invalid point ‘${point}’`);
if (this.height!=0 || point.height!=0) throw new RangeError('point must be on the surface of the ellipsoid');

@@ -243,5 +243,5 @@ const p1 = this, p2 = point;

const ellipsoid = this.datum ? this.datum.ellipsoid :
this.referenceFrame ? this.referenceFrame.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const a = ellipsoid.a, b = ellipsoid.b, f = ellipsoid.f;
// allow alternative ellipsoid to be specified
const ellipsoid = this.datum ? this.datum.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const { a, b, f } = ellipsoid;

@@ -261,3 +261,3 @@ const L = λ2 - λ1;

sinSqσ = (cosU2*sinλ) * (cosU2*sinλ) + (cosU1*sinU2-sinU1*cosU2*cosλ) * (cosU1*sinU2-sinU1*cosU2*cosλ);
if (sinSqσ == 0) break; // co-incident points
if (Math.abs(sinSqσ) < Number.EPSILON) break; // co-incident points
sinσ = Math.sqrt(sinSqσ);

@@ -290,4 +290,4 @@ cosσ = sinU1*sinU2 + cosU1*cosU2*cosλ;

distance: s,
initialBearing: s==0 ? NaN : Dms.wrap360(α1.toDegrees()),
finalBearing: s==0 ? NaN : Dms.wrap360(α2.toDegrees()),
initialBearing: Math.abs(s) < Number.EPSILON ? NaN : Dms.wrap360(α1.toDegrees()),
finalBearing: Math.abs(s) < Number.EPSILON ? NaN : Dms.wrap360(α2.toDegrees()),
iterations: iterations,

@@ -294,0 +294,0 @@ };

@@ -76,5 +76,5 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

constructor(lat, lon, height=0) {
if (isNaN(lat)) throw new TypeError(`Invalid lat ‘${lat}’`);
if (isNaN(lon)) throw new TypeError(`Invalid lon ‘${lon}’`);
if (isNaN(height)) throw new TypeError(`Invalid height ‘${height}’`);
if (isNaN(lat)) throw new TypeError(`invalid lat ‘${lat}’`);
if (isNaN(lon)) throw new TypeError(`invalid lon ‘${lon}’`);
if (isNaN(height)) throw new TypeError(`invalid height ‘${height}’`);

@@ -95,7 +95,7 @@ this._lat = Dms.wrap90(Number(lat));

this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(lat));
if (isNaN(this._lat)) throw new TypeError(`Invalid lat ‘${lat}’`);
if (isNaN(this._lat)) throw new TypeError(`invalid lat ‘${lat}’`);
}
set latitude(lat) {
this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(lat));
if (isNaN(this._lat)) throw new TypeError(`Invalid latitude ‘${lat}’`);
if (isNaN(this._lat)) throw new TypeError(`invalid latitude ‘${lat}’`);
}

@@ -112,11 +112,11 @@

this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon));
if (isNaN(this._lon)) throw new TypeError(`Invalid lon ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid lon ‘${lon}’`);
}
set lng(lon) {
this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon));
if (isNaN(this._lon)) throw new TypeError(`Invalid lng ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid lng ‘${lon}’`);
}
set longitude(lon) {
this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon));
if (isNaN(this._lon)) throw new TypeError(`Invalid longitude ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid longitude ‘${lon}’`);
}

@@ -128,3 +128,3 @@

get height() { return this._height; }
set height(height) { this._height = Number(height); if (isNaN(this._height)) throw new TypeError(`Invalid height ‘${height}’`); }
set height(height) { this._height = Number(height); if (isNaN(this._height)) throw new TypeError(`invalid height ‘${height}’`); }

@@ -192,3 +192,3 @@

static parse(...args) {
if (args.length == 0) throw new TypeError('Invalid (empty) coordinate');
if (args.length == 0) throw new TypeError('invalid (empty) point');

@@ -214,3 +214,3 @@ let lat=undefined, lon=undefined, height=undefined;

if (args[1] != undefined) height = args[1];
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`Invalid coordinate ‘${JSON.stringify(args[0])}’`);
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${JSON.stringify(args[0])}’`);
}

@@ -224,3 +224,3 @@

height = args[1] || 0;
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`Invalid coordinate ‘${args[0]}’`);
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args[0]}’`);
}

@@ -234,3 +234,3 @@

height = args[2] || 0;
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`Invalid coordinate ‘${args.toString()}’`);
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`);
}

@@ -258,4 +258,3 @@

const h = this.height;
const a = ellipsoid.a;
const f = ellipsoid.f;
const { a, f } = ellipsoid;

@@ -281,3 +280,3 @@ const sinφ = Math.sin(φ), cosφ = Math.cos(φ);

* @returns {bool} True if points have identical latitude, longitude, height, and datum/referenceFrame.
* @throws {TypeError} Point is not LatLon object.
* @throws {TypeError} Invalid point.
*

@@ -290,7 +289,7 @@ * @example

equals(point) {
if (!(point instanceof LatLonEllipsoidal)) throw new TypeError('‘point’ is not LatLon object');
if (!(point instanceof LatLonEllipsoidal)) throw new TypeError(`invalid point ‘${point}’`);
if (this.lat != point.lat) return false;
if (this.lon != point.lon) return false;
if (this.height != point.height) return false;
if (Math.abs(this.lat - point.lat) > Number.EPSILON) return false;
if (Math.abs(this.lon - point.lon) > Number.EPSILON) return false;
if (Math.abs(this.height - point.height) > Number.EPSILON) return false;
if (this.datum != point.datum) return false;

@@ -323,3 +322,3 @@ if (this.referenceFrame != point.referenceFrame) return false;

// note: explicitly set dp to undefined for passing through to toLat/toLon
if (![ 'd', 'dm', 'dms', 'n' ].includes(format)) throw new RangeError(`Invalid format ‘${format}’`);
if (![ 'd', 'dm', 'dms', 'n' ].includes(format)) throw new RangeError(`invalid format ‘${format}’`);

@@ -386,6 +385,6 @@ const height = (this.height>=0 ? ' +' : ' ') + this.height.toFixed(dpHeight) + 'm';

// note ellipsoid is available as a parameter for when toLatLon gets subclassed.
if (!ellipsoid || !ellipsoid.a) throw new TypeError('Invalid ellipsoid');
if (!ellipsoid || !ellipsoid.a) throw new TypeError('invalid ellipsoid');
const x = this.x, y = this.y, z = this.z;
const a = ellipsoid.a, b = ellipsoid.b, f = ellipsoid.f;
const { x, y, z } = this;
const { a, b, f } = ellipsoid;

@@ -392,0 +391,0 @@ const e2 = 2*f - f*f; // 1st eccentricity squared ≡ (a²−b²)/a²

@@ -47,3 +47,4 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

* @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.
* @returns {Ned} Delta from ‘this’ point to supplied point in local tangent plane of this point.
* @throws {TypeError} Invalid point.
*

@@ -59,3 +60,3 @@ * @example

deltaTo(point) {
if (!(point instanceof LatLonEllipsoidal)) throw new TypeError('‘point’ is not (Ellipsoidal) LatLon object');
if (!(point instanceof LatLonEllipsoidal)) throw new TypeError(`invalid point ‘${point}’`);

@@ -232,3 +233,3 @@ // get delta in cartesian frame

const x = this.x, y = this.y, z = this.z;
const { x, y, z } = this;

@@ -254,13 +255,5 @@ const φ = Math.atan2(z, Math.sqrt(x*x + y*y));

toCartesian() {
const ellipsoid = this.datum ? this.datum.ellipsoid :
this.referenceFrame ? this.referenceFrame.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const { b, f } = this.datum.ellipsoid;
const { x, y, z, h } = this;
const b = ellipsoid.b;
const f = ellipsoid.f;
const x = this.x;
const y = this.y;
const z = this.z;
const h = this.h;
const m = (1-f) * (1-f); // (1−f)² = b²/a²

@@ -290,8 +283,6 @@ const n = b / Math.sqrt(x*x/m + y*y/m + z*z);

toString(dp=3, dpHeight=null) {
const x = this.x.toFixed(dp);
const y = this.y.toFixed(dp);
const z = this.z.toFixed(dp);
const { x, y, z } = this;
const h = `${this.h>=0 ? '+' : ''}${this.h.toFixed(dpHeight)}m`;
return `[${x},${y},${z}${dpHeight==null ? '' : h}]`;
return `[${x.toFixed(dp)},${y.toFixed(dp)},${z.toFixed(dp)}${dpHeight==null ? '' : h}]`;
}

@@ -326,9 +317,5 @@

toNvector(datum=LatLonEllipsoidal.datums.WGS84) {
const a = datum.ellipsoid.a;
const f = datum.ellipsoid.f;
const { a, f } = datum.ellipsoid;
const { x, y, z } = this;
const x = this.x;
const y = this.y;
const z = this.z;
const e2 = 2*f - f*f; // e² = 1st eccentricity squared ≡ (a²-b²)/a²

@@ -395,5 +382,5 @@ const e4 = e2*e2; // e⁴

get length() {
const n = this.north, e = this.east, d = this.down;
const { north, east, down } = this;
return Math.sqrt(n*n + e*e + d*d);
return Math.sqrt(north*north + east*east + down*down);
}

@@ -400,0 +387,0 @@

@@ -45,3 +45,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

* @param {number} lon - Longitude (in degrees).
* @throws {TypeError} Non-numeric lat/lon.
* @throws {TypeError} Invalid lat/lon.
*

@@ -53,4 +53,4 @@ * @example

constructor(lat, lon) {
if (isNaN(lat)) throw new TypeError(`Invalid lat ‘${lat}’`);
if (isNaN(lon)) throw new TypeError(`Invalid lon ‘${lon}’`);
if (isNaN(lat)) throw new TypeError(`invalid lat ‘${lat}’`);
if (isNaN(lon)) throw new TypeError(`invalid lon ‘${lon}’`);

@@ -70,7 +70,7 @@ this._lat = Dms.wrap90(Number(lat));

this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(lat));
if (isNaN(this._lat)) throw new TypeError(`Invalid lat ‘${lat}’`);
if (isNaN(this._lat)) throw new TypeError(`invalid lat ‘${lat}’`);
}
set latitude(lat) {
this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(lat));
if (isNaN(this._lat)) throw new TypeError(`Invalid latitude ‘${lat}’`);
if (isNaN(this._lat)) throw new TypeError(`invalid latitude ‘${lat}’`);
}

@@ -87,11 +87,11 @@

this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon));
if (isNaN(this._lon)) throw new TypeError(`Invalid lon ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid lon ‘${lon}’`);
}
set lng(lon) {
this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon));
if (isNaN(this._lon)) throw new TypeError(`Invalid lng ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid lng ‘${lon}’`);
}
set longitude(lon) {
this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(lon));
if (isNaN(this._lon)) throw new TypeError(`Invalid longitude ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid longitude ‘${lon}’`);
}

@@ -169,3 +169,3 @@

* @returns {number} Distance between this point and destination point, in same units as radius.
* @throws {TypeError} Point is not LatLon object, Radius is not a number.
* @throws {TypeError} Invalid point/radius.
*

@@ -178,4 +178,4 @@ * @example

distanceTo(point, radius=6371e3) {
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError('‘point’ is not (NvectorSpherical) LatLon object');
if (isNaN(radius)) throw new TypeError('Radius is not a number');
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
if (isNaN(radius)) throw new TypeError(`invalid radius ‘${radius}’`);

@@ -200,3 +200,3 @@ const R = Number(radius);

* @returns {number} Initial bearing in degrees from north (0°..360°).
* @throws {TypeError} Point is not LatLon object.
* @throws {TypeError} Invalid point.
*

@@ -209,3 +209,4 @@ * @example

initialBearingTo(point) {
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError('‘point’ is not (NvectorSpherical) LatLon object');
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
if (this.equals(point)) return NaN; // coincident points

@@ -232,3 +233,3 @@ const p1 = this.toNvector();

* @returns {number} Final bearing in degrees from north (0°..360°).
* @throws {TypeError} Point is not LatLon object.
* @throws {TypeError} Invalid point.
*

@@ -241,3 +242,3 @@ * @example

finalBearingTo(point) {
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError('‘point’ is not (NvectorSpherical) LatLon object');
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);

@@ -254,3 +255,3 @@ // get initial bearing from destination point to this point & reverse it by adding 180°

* @returns {LatLon} Midpoint between this point and destination point.
* @throws {TypeError} Point is not LatLon object.
* @throws {TypeError} Invalid point.
*

@@ -263,3 +264,3 @@ * @example

midpointTo(point) {
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError('‘point’ is not (NvectorSpherical) LatLon object');
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);

@@ -281,3 +282,3 @@ const n1 = this.toNvector();

* @returns {LatLon} Intermediate point between this point and destination point.
* @throws {TypeError} Point is not LatLon object.
* @throws {TypeError} Invalid point/fraction.
*

@@ -290,3 +291,4 @@ * @example

intermediatePointTo(point, fraction) {
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError('‘point’ is not (NvectorSpherical) LatLon object');
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
if (isNaN(fraction)) throw new TypeError(`invalid fraction ‘${fraction}’`);

@@ -322,3 +324,3 @@ // angular distance between points; tanδ = |n₁×n₂| / n₁⋅n₂

* @returns {LatLon} Intermediate point between this point and destination point.
* @throws {TypeError} Point is not LatLon object.
* @throws {TypeError} Invalid point.
*

@@ -331,3 +333,3 @@ * @example

intermediatePointOnChordTo(point, fraction) {
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError('‘point’ is not (NvectorSpherical) LatLon object');
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);

@@ -390,3 +392,3 @@ const n1 = this.toNvector();

* @returns {LatLon} Destination point (null if no unique intersection defined)
* @throws {TypeError} Parameter is not LatLon object.
* @throws {TypeError} Invalid parameter.
*

@@ -399,7 +401,9 @@ * @example

static intersection(path1start, path1brngEnd, path2start, path2brngEnd) {
if (!(path1start instanceof LatLonNvectorSpherical)) throw new TypeError('‘path1start’ is not LatLon object');
if (!(path2start instanceof LatLonNvectorSpherical)) throw new TypeError('‘path2start’ is not LatLon object');
if (!(path1brngEnd instanceof LatLonNvectorSpherical) && isNaN(path1brngEnd)) throw new TypeError('‘path1brngEnd’ is not LatLon object or bearing');
if (!(path2brngEnd instanceof LatLonNvectorSpherical) && isNaN(path2brngEnd)) throw new TypeError('‘path2brngEnd’ is not LatLon object or bearing');
if (!(path1start instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid path1start ‘${path1start}’`);
if (!(path2start instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid path2start ‘${path2start}’`);
if (!(path1brngEnd instanceof LatLonNvectorSpherical) && isNaN(path1brngEnd)) throw new TypeError(`invalid path1brngEnd ‘${path1brngEnd}’`);
if (!(path2brngEnd instanceof LatLonNvectorSpherical) && isNaN(path2brngEnd)) throw new TypeError(`invalid path2brngEnd ‘${path2brngEnd}’`);
if (path1start.equals(path2start)) return new LatLonNvectorSpherical(path1start.lat, path2start.lon); // coincident points
// if c1 & c2 are great circles through start and end points (or defined by start point + bearing),

@@ -420,3 +424,3 @@ // then candidate intersections are simply c1 × c2 & c2 × c1; most of the work is deciding correct

} else { // path 1 defined by initial bearing
c1 = path1start.greatCircle(Number(path1brngEnd));
c1 = path1start.greatCircle(path1brngEnd);
path1def = 'bearing';

@@ -428,3 +432,3 @@ }

} else { // path 2 defined by initial bearing
c2 = path2start.greatCircle(Number(path2brngEnd));
c2 = path2start.greatCircle(path2brngEnd);
path2def = 'bearing';

@@ -485,3 +489,3 @@ }

* @returns {number} Distance to great circle (-ve if to left, +ve if to right of path).
* @throws {TypeError} Parameter is not LatLon object.
* @throws {TypeError} Invalid parameter.
*

@@ -498,4 +502,7 @@ * @example

crossTrackDistanceTo(pathStart, pathBrngEnd, radius=6371e3) {
if (!(pathStart instanceof LatLonNvectorSpherical)) throw new TypeError('‘pathStart’ is not (NvectorSpherical) LatLon object');
if (!(pathStart instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid pathStart ‘${pathStart}’`);
if (!(pathBrngEnd instanceof LatLonNvectorSpherical || !isNaN(pathBrngEnd))) throw new TypeError(`invalid pathBrngEnd ‘${pathBrngEnd}’`);
if (this.equals(pathStart)) return NaN; // coincident points
const p = this.toNvector();

@@ -506,3 +513,3 @@ const R = Number(radius);

? pathStart.toNvector().cross(pathBrngEnd.toNvector()) // great circle defined by two points
: pathStart.greatCircle(Number(pathBrngEnd)); // great circle defined by point + bearing
: pathStart.greatCircle(pathBrngEnd); // great circle defined by point + bearing

@@ -515,5 +522,38 @@ const α = gc.angleTo(p) - π/2; // angle between point & great-circle

// TODO: alongTrackDistanceTo
/**
* Returns how far ‘this’ point is along a path from from start-point, heading on bearing or towards
* end-point. That is, if a perpendicular is drawn from ‘this’ point to the (great circle) path, the
* along-track distance is the distance from the start point to where the perpendicular crosses the
* path.
*
* @param {LatLon} pathStart - Start point of great circle path.
* @param {LatLon|number} pathBrngEnd - End point of great circle path or initial bearing from great circle start point.
* @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
* @returns {number} Distance along great circle to point nearest ‘this’ point.
*
* @example
* const pCurrent = new LatLon(53.2611, -0.7972);
* const p1 = new LatLon(53.3206, -1.7297);
* const p2 = new LatLon(53.1887, 0.1334);
* const d = pCurrent.alongTrackDistanceTo(p1, p2); // 62.331 km
*/
alongTrackDistanceTo(pathStart, pathBrngEnd, radius=6371e3) {
if (!(pathStart instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid pathStart ‘${pathStart}’`);
if (!(pathBrngEnd instanceof LatLonNvectorSpherical || !isNaN(pathBrngEnd))) throw new TypeError(`invalid pathBrngEnd ‘${pathBrngEnd}’`);
const p = this.toNvector();
const R = Number(radius);
const gc = pathBrngEnd instanceof LatLonNvectorSpherical // (note JavaScript is not good at method overloading)
? pathStart.toNvector().cross(pathBrngEnd.toNvector()) // great circle defined by two points
: pathStart.greatCircle(pathBrngEnd); // great circle defined by point + bearing
const pat = gc.cross(p).cross(gc); // along-track point c × p × c
const α = pathStart.toNvector().angleTo(pat, gc); // angle between start point and along-track point
return α * R;
}
/**

@@ -543,3 +583,3 @@ * Returns closest point on great circle segment between point1 & point2 to ‘this’ point.

if (this.isWithinExtent(point1, point2)) {
if (this.isWithinExtent(point1, point2) && !point1.equals(point2)) {
// closer to segment than to its endpoints, find closest point on segment

@@ -555,3 +595,4 @@ const n0 = this.toNvector(), n1 = point1.toNvector(), n2 = point2.toNvector();

const d2 = this.distanceTo(point2);
p = d1<d2 ? point1 : point2;
const pCloser = d1<d2 ? point1 : point2;
p = new LatLonNvectorSpherical(pCloser.lat, pCloser.lon);
}

@@ -567,3 +608,4 @@

* If this point is not on the great circle defined by point1 & point 2, returns whether it is
* within the area bound by perpendiculars to the great circle at each point.
* within the area bound by perpendiculars to the great circle at each point (in the same
* hemisphere).
*

@@ -580,2 +622,4 @@ * @param {LatLon} point1 - First point defining segment.

isWithinExtent(point1, point2) {
if (point1.equals(point2)) return this.equals(point1); // null segment
const n0 = this.toNvector(), n1 = point1.toNvector(), n2 = point2.toNvector(); // n-vectors

@@ -591,3 +635,5 @@

return extent1>=0 && extent2>=0;
const isSameHemisphere = n0.dot(n1)>=0 && n0.dot(n2)>=0;
return extent1>=0 && extent2>=0 && isSameHemisphere;
}

@@ -670,2 +716,4 @@

if (!isFinite(x) || !isFinite(y)) return null; // coincident points?
const n = n1.plus(eX.times(x)).plus(eY.times(y)); // note don't use z component; assume points at same height

@@ -724,2 +772,4 @@

*
* Uses Girard’s theorem: A = [Σθᵢ − (n−2)·π]·R²
*
* @param {LatLon[]} polygon - Array of points defining vertices of the polygon.

@@ -734,4 +784,2 @@ * @param {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).

static areaOf(polygon, radius=6371e3) {
// uses Girard’s theorem: A = [Σθᵢ − (n−2)·π]·R²
const R = Number(radius);

@@ -745,3 +793,3 @@

// get great-circle vector for each vertex
// get great-circle vector for each segment
const c = [];

@@ -764,2 +812,6 @@ for (let v=0; v<n; v++) {

// note: angle between two sides of a spherical triangle is acos(c₁·c₂) where cₙ is the
// plane normal vector to the great circle representing the triangle side - use this instead
// of angleTo()?
const E = (Σθ - (n-2)*π); // spherical excess (in steradians)

@@ -801,3 +853,3 @@ const A = E * R*R; // area in units of R²

* @returns {bool} True if points have identical latitude and longitude values.
* @throws {TypeError} Point is not LatLon object.
* @throws {TypeError} Invalid point.
*

@@ -810,6 +862,6 @@ * @example

equals(point) {
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError('‘point’ is not (NvectorSpherical) LatLon object');
if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
if (this.lat != point.lat) return false;
if (this.lon != point.lon) return false;
if (Math.abs(this.lat - point.lat) > Number.EPSILON) return false;
if (Math.abs(this.lon - point.lon) > Number.EPSILON) return false;

@@ -841,3 +893,3 @@ return true;

* const d = greenwich.toString(); // 51.4778°N, 000.0015°W
* const dms = greenwich.toString('dms', 2); // 51°28′40″N, 000°00′05″W
* const dms = greenwich.toString('dms', 2); // 51°28′40.37″N, 000°00′05.29″W
* const [lat, lon] = greenwich.toString('n').split(','); // 51.4778, -0.0015

@@ -847,3 +899,3 @@ */

// note: explicitly set dp to undefined for passing through to toLat/toLon
if (![ 'd', 'dm', 'dms', 'n' ].includes(format)) throw new RangeError(`Invalid format ‘${format}’`);
if (![ 'd', 'dm', 'dms', 'n' ].includes(format)) throw new RangeError(`invalid format ‘${format}’`);

@@ -850,0 +902,0 @@ if (format == 'n') { // signed numeric degrees

@@ -42,3 +42,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

* @param {number} lon - Longitude (in degrees).
* @throws {TypeError} Non-numeric lat/lon.
* @throws {TypeError} Invalid lat/lon.
*

@@ -50,4 +50,4 @@ * @example

constructor(lat, lon) {
if (isNaN(lat)) throw new TypeError(`Invalid lat ‘${lat}’`);
if (isNaN(lon)) throw new TypeError(`Invalid lon ‘${lon}’`);
if (isNaN(lat)) throw new TypeError(`invalid lat ‘${lat}’`);
if (isNaN(lon)) throw new TypeError(`invalid lon ‘${lon}’`);

@@ -67,7 +67,7 @@ this._lat = Dms.wrap90(lat);

this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(lat);
if (isNaN(this._lat)) throw new TypeError(`Invalid lat ‘${lat}’`);
if (isNaN(this._lat)) throw new TypeError(`invalid lat ‘${lat}’`);
}
set latitude(lat) {
this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(lat);
if (isNaN(this._lat)) throw new TypeError(`Invalid latitude ‘${lat}’`);
if (isNaN(this._lat)) throw new TypeError(`invalid latitude ‘${lat}’`);
}

@@ -84,11 +84,11 @@

this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(lon);
if (isNaN(this._lon)) throw new TypeError(`Invalid lon ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid lon ‘${lon}’`);
}
set lng(lon) {
this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(lon);
if (isNaN(this._lon)) throw new TypeError(`Invalid lng ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid lng ‘${lon}’`);
}
set longitude(lon) {
this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(lon);
if (isNaN(this._lon)) throw new TypeError(`Invalid longitude ‘${lon}’`);
if (isNaN(this._lon)) throw new TypeError(`invalid longitude ‘${lon}’`);
}

@@ -121,3 +121,3 @@

* @returns {LatLon} Latitude/longitude point.
* @throws {TypeError} Invalid coordinate.
* @throws {TypeError} Invalid point.
*

@@ -135,4 +135,4 @@ * @example

static parse(...args) {
if (args.length == 0) throw new TypeError('Invalid (empty) coordinate');
if (args[0]===null || args[1]===null) throw new TypeError('Invalid (null) coordinate');
if (args.length == 0) throw new TypeError('invalid (empty) point');
if (args[0]===null || args[1]===null) throw new TypeError('invalid (null) point');

@@ -145,3 +145,3 @@ let lat=undefined, lon=undefined;

lon = Dms.wrap180(Dms.parse(lon));
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`Invalid coordinate ‘${args.toString()}’`);
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`);
}

@@ -153,3 +153,3 @@

lon = Dms.wrap180(Dms.parse(lon));
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`Invalid coordinate ‘${args[0]}’`);
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args[0]}’`);
}

@@ -170,6 +170,6 @@

}
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`Invalid coordinate ‘${JSON.stringify(args[0])}’`);
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${JSON.stringify(args[0])}’`);
}
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`Invalid coordinate ‘${args.toString()}’`);
if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`);

@@ -188,3 +188,3 @@ return new LatLonSpherical(lat, lon);

* @returns {number} Distance between this point and destination point, in same units as radius.
* @throws {TypeError} Radius is not a number.
* @throws {TypeError} Invalid radius.
*

@@ -199,3 +199,3 @@ * @example

if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
if (isNaN(radius)) throw new TypeError('Radius is not a number');
if (isNaN(radius)) throw new TypeError(`invalid radius ‘${radius}’`);

@@ -233,2 +233,3 @@ // a = sin²(Δφ/2) + cos(φ1)⋅cos(φ2)⋅sin²(Δλ/2)

if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
if (this.equals(point)) return NaN; // coincident points

@@ -308,3 +309,3 @@ // tanθ = sinΔλ⋅cosφ2 / cosφ1⋅sinφ2 − sinφ1⋅cosφ2⋅cosΔλ

return new LatLonSpherical(lat, Dms.wrap180(lon));
return new LatLonSpherical(lat, lon);
}

@@ -327,2 +328,3 @@

if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
if (this.equals(point)) return new LatLonSpherical(this.lat, this.lon); // coincident points

@@ -352,3 +354,3 @@ const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();

return new LatLonSpherical(lat, Dms.wrap180(lon));
return new LatLonSpherical(lat, lon);
}

@@ -389,3 +391,3 @@

return new LatLonSpherical(lat, Dms.wrap180(lon));
return new LatLonSpherical(lat, lon);
}

@@ -411,2 +413,4 @@

if (!(p2 instanceof LatLonSpherical)) p2 = LatLonSpherical.parse(p2); // allow literal forms
if (isNaN(brng1)) throw new TypeError(`invalid brng1 ‘${brng1}’`);
if (isNaN(brng2)) throw new TypeError(`invalid brng2 ‘${brng2}’`);

@@ -423,3 +427,3 @@ // see www.edwilliams.org/avform.htm#Intersection

+ Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2)));
if (δ12 == 0) return null;
if (Math.abs(δ12) < Number.EPSILON) return new LatLonSpherical(p1.lat, p1.lon); // coincident points

@@ -439,3 +443,3 @@ // initial/final bearings between points

if (Math.sin(α1) == 0 && Math.sin(α2) == 0) return null; // infinite intersections
if (Math.sin(α1) * Math.sin(α2) < 0) return null; // ambiguous intersection
if (Math.sin(α1) * Math.sin(α2) < 0) return null; // ambiguous intersection (antipodal?)

@@ -454,3 +458,3 @@ const cosα3 = -Math.cos(α1)*Math.cos(α2) + Math.sin(α1)*Math.sin(α2)*Math.cos(δ12);

return new LatLonSpherical(lat, Dms.wrap180(lon));
return new LatLonSpherical(lat, lon);
}

@@ -555,2 +559,4 @@

static crossingParallels(point1, point2, latitude) {
if (point1.equals(point2)) return null; // coincident points
const φ = Number(latitude).toRadians();

@@ -632,3 +638,2 @@

* @returns {number} Bearing in degrees from north.
* @throws {TypeError} Invalid coordinate.
*

@@ -642,2 +647,3 @@ * @example

if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
if (this.equals(point)) return NaN; // coincident points

@@ -693,3 +699,3 @@ const φ1 = this.lat.toRadians(), φ2 = point.lat.toRadians();

return new LatLonSpherical(lat, Dms.wrap180(lon));
return new LatLonSpherical(lat, lon);
}

@@ -730,7 +736,7 @@

return new LatLonSpherical(lat, Dms.wrap180(lon));
return new LatLonSpherical(lat, lon);
}
/* Area - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Area - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

@@ -752,4 +758,5 @@

// uses method due to Karney: osgeo-org.1560.x6.nabble.com/Area-of-a-spherical-polygon-td3841625.html;
// for each edge of the polygon, tan(E/2) = tan(Δλ/2)·(tan(φ1/2) + tan(φ2/2)) / (1 + tan(φ1/2)·tan(φ2/2))
// for each edge of the polygon, tan(E/2) = tan(Δλ/2)·(tan(φ₁/2)+tan(φ₂/2)) / (1+tan(φ₁/2)·tan(φ₂/2))
// where E is the spherical excess of the trapezium obtained by extending the edge to the equator
// (Karney's method is probably more efficient than the more widely known L’Huilier’s Theorem)

@@ -820,4 +827,4 @@ const R = radius;

if (this.lat != point.lat) return false;
if (this.lon != point.lon) return false;
if (Math.abs(this.lat - point.lat) > Number.EPSILON) return false;
if (Math.abs(this.lon - point.lon) > Number.EPSILON) return false;

@@ -855,3 +862,3 @@ return true;

// note: explicitly set dp to undefined for passing through to toLat/toLon
if (![ 'd', 'dm', 'dms', 'n' ].includes(format)) throw new RangeError(`Invalid format ‘${format}’`);
if (![ 'd', 'dm', 'dms', 'n' ].includes(format)) throw new RangeError(`invalid format ‘${format}’`);

@@ -858,0 +865,0 @@ if (format == 'n') { // signed numeric degrees

@@ -73,7 +73,7 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

const errors = []; // check & report all other possible errors rather than reporting one-by-one
if (band.length!=1 || latBands.indexOf(band) == -1) errors.push(`Invalid MGRS band ‘${band}’`);
if (e100k.length!=1 || e100kLetters[(zone-1)%3].indexOf(e100k) == -1) errors.push(`Invalid MGRS 100km grid square column ‘${e100k}’ for zone ${zone}`);
if (n100k.length!=1 || n100kLetters[0].indexOf(n100k) == -1) errors.push(`Invalid MGRS 100km grid square row ‘${n100k}’`);
if (isNaN(Number(easting))) errors.push(`Invalid MGRS easting ‘${easting}’`);
if (isNaN(Number(northing))) errors.push(`Invalid MGRS northing ‘${northing}’`);
if (band.length!=1 || latBands.indexOf(band) == -1) errors.push(`invalid MGRS band ‘${band}’`);
if (e100k.length!=1 || e100kLetters[(zone-1)%3].indexOf(e100k) == -1) errors.push(`invalid MGRS 100km grid square column ‘${e100k}’ for zone ${zone}`);
if (n100k.length!=1 || n100kLetters[0].indexOf(n100k) == -1) errors.push(`invalid MGRS 100km grid square row ‘${n100k}’`);
if (isNaN(Number(easting))) errors.push(`invalid MGRS easting ‘${easting}’`);
if (isNaN(Number(northing))) errors.push(`invalid MGRS northing ‘${northing}’`);
if (errors.length > 0) throw new RangeError(errors.join(', '));

@@ -94,6 +94,10 @@

*
* @returns {Utm} UTM coordinate equivalent to this MGRS grid reference
* Grid references refer to squares rather than points (with the size of the square indicated
* by the precision of the reference); this conversion will return the UTM coordinate of the SW
* corner of the grid reference square.
*
* @returns {Utm} UTM coordinate of SW corner of this MGRS grid reference.
*
* @example
* const mgrsRef = new Mgrs(31, 'U', 'D', 'Q', 48251, 11932);
* const mgrsRef = Mgrs.parse('31U DQ 48251 11932');
* const utmCoord = mgrsRef.toUtm(); // 31 N 448251 5411932

@@ -146,7 +150,7 @@ */

static parse(mgrsGridRef) {
if (!mgrsGridRef) throw new Error(`Invalid MGRS grid reference ‘${mgrsGridRef}’`);
if (!mgrsGridRef) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);
// check for military-style grid reference with no separators
if (!mgrsGridRef.trim().match(/\s/)) {
if (!Number(mgrsGridRef.slice(0, 2))) throw new Error(`Invalid MGRS grid reference ‘${mgrsGridRef}’`);
if (!Number(mgrsGridRef.slice(0, 2))) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);
let en = mgrsGridRef.trim().slice(5); // get easting/northing following zone/band/100ksq

@@ -160,3 +164,3 @@ en = en.slice(0, en.length/2)+' '+en.slice(-en.length/2); // separate easting/northing

if (ref==null || ref.length!=4) throw new Error(`Invalid MGRS grid reference ‘${mgrsGridRef}’`);
if (ref==null || ref.length!=4) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);

@@ -192,3 +196,7 @@ // split gzd into zone/band

*
* Note that MGRS grid references get truncated, not rounded (unlike UTM coordinates).
* Note that MGRS grid references get truncated, not rounded (unlike UTM coordinates); grid
* references indicate a bounding square, rather than a point, with the size of the square
* indicated by the precision - a precision of 10 indicates a 1-metre square, a precision of 4
* indicates a 1,000-metre square (hence 31U DQ 48 11 indicates a 1km square with SW corner at
* 31 N 448000 5411000, which would include the 1m square 31U DQ 48251 11932).
*

@@ -203,19 +211,16 @@ * @param {number} [digits=10] - Precision of returned grid reference (eg 4 = km, 10 = m).

toString(digits=10) {
if (![ 2,4,6,8,10 ].includes(Number(digits))) throw new RangeError(`Invalid precision ‘${digits}’`); // eslint-disable-line comma-spacing
if (![ 2, 4, 6, 8, 10 ].includes(Number(digits))) throw new RangeError(`invalid precision ‘${digits}’`);
const zone = this.zone.toString().padStart(2, '0'); // ensure leading zero
const band = this.band;
const { zone, band, e100k, n100k, easting, northing } = this;
const e100k = this.e100k;
const n100k = this.n100k;
// truncate to required precision
const eRounded = Math.floor(this.easting/Math.pow(10, 5-digits/2));
const nRounded = Math.floor(this.northing/Math.pow(10, 5-digits/2));
const eRounded = Math.floor(easting/Math.pow(10, 5-digits/2));
const nRounded = Math.floor(northing/Math.pow(10, 5-digits/2));
// ensure leading zeros
const easting = eRounded.toString().padStart(digits/2, '0');
const northing = nRounded.toString().padStart(digits/2, '0');
const zPadded = zone.toString().padStart(2, '0');
const ePadded = eRounded.toString().padStart(digits/2, '0');
const nPadded = nRounded.toString().padStart(digits/2, '0');
return `${zone}${band} ${e100k}${n100k} ${easting} ${northing}`;
return `${zPadded}${band} ${e100k}${n100k} ${ePadded} ${nPadded}`;
}

@@ -222,0 +227,0 @@ }

@@ -56,4 +56,4 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

if (isNaN(easting) || this.easting<0 || this.easting>700e3) throw new RangeError(`Invalid easting ‘${easting}’`);
if (isNaN(northing) || this.northing<0 || this.northing>1300e3) throw new RangeError(`Invalid northing ‘${northing}’`);
if (isNaN(easting) || this.easting<0 || this.easting>700e3) throw new RangeError(`invalid easting ‘${easting}’`);
if (isNaN(northing) || this.northing<0 || this.northing>1300e3) throw new RangeError(`invalid northing ‘${northing}’`);
}

@@ -83,4 +83,3 @@

toLatLon(datum=LatLonEllipsoidal.datums.WGS84) {
const E = this.easting;
const N = this.northing;
const { easting: E, northing: N } = this;

@@ -164,3 +163,3 @@ const a = 6377563.396, b = 6356256.909; // Airy 1830 major & minor semi-axes

match = gridref.match(/^[A-Z]{2}\s*[0-9]+\s*[0-9]+$/i);
if (!match) throw new Error(`Invalid grid reference ‘${gridref}’`);
if (!match) throw new Error(`invalid grid reference ‘${gridref}’`);

@@ -175,3 +174,3 @@ // get numeric values of letter references, mapping A->0, B->1, C->2, etc:

// sanity check
if (l1<8 || l1 > 18) throw new Error(`Invalid grid reference ‘${gridref}’`);
if (l1<8 || l1 > 18) throw new Error(`invalid grid reference ‘${gridref}’`);

@@ -188,3 +187,3 @@ // convert grid letters into 100km-square indexes from false origin (grid square SV):

// validation
if (en[0].length != en[1].length) throw new Error(`Invalid grid reference ‘${gridref}’`);
if (en[0].length != en[1].length) throw new Error(`invalid grid reference ‘${gridref}’`);

@@ -214,6 +213,5 @@ // standardise to 10-digit refs (metres)

toString(digits=10) {
if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`Invalid precision ‘${digits}’`); // eslint-disable-line comma-spacing
if (![ 0,2,4,6,8,10,12,14,16 ].includes(Number(digits))) throw new RangeError(`invalid precision ‘${digits}’`); // eslint-disable-line comma-spacing
let e = this.easting;
let n = this.northing;
let { easting: e, northing: n } = this;

@@ -220,0 +218,0 @@ // use digits = 0 to return numeric format (in metres) - note northing may be >= 1e7

@@ -11,3 +11,3 @@ {

"author": "Chris Veness",
"version": "2.0.0",
"version": "2.0.1",
"license": "MIT",

@@ -27,7 +27,8 @@ "engines": {

"chai": "^4.2.0",
"eslint": "^5.13.0",
"esm": "^3.2.4",
"coveralls": "^3.0.3",
"eslint": "^5.16.0",
"esm": "^3.2.22",
"jsdoc": "^3.5.5",
"mocha": "^5.2.0",
"nyc": "^13.2.0"
"mocha": "^6.1.2",
"nyc": "^13.3.0"
},

@@ -34,0 +35,0 @@ "eslintConfig": {

Geodesy functions
=================
[![Build Status](https://travis-ci.org/chrisveness/geodesy.svg?branch=v2.0.0)
](https://travis-ci.org/chrisveness/geodesy)
[![Coverage Status](https://coveralls.io/repos/github/chrisveness/geodesy/bad
ge.svg?branch=master)](https://coveralls.io/github/chrisveness/geodesy?branch
=v2.0.0)
[![Build Status](https://travis-ci.org/chrisveness/geodesy.svg?branch=master)](https://travis-ci.org/chrisveness/geodesy)
[![Coverage Status](https://coveralls.io/repos/github/chrisveness/geodesy/badge.svg?branch=master)](https://coveralls.io/github/chrisveness/geodesy?branch=master)
[![Documentation](https://img.shields.io/badge/docs-www.movable--type.co.uk%2Fscripts%2Fgeodesy--library.html-lightgrey.svg)](https://www.movable-type.co.uk/scripts/geodesy-library.html)

@@ -63,8 +61,8 @@ These libraries started life (a long time ago) as simple ‘latitude/longitude’ code fragments

````
import LatLon from '/js/geodesy/latlon-spherical.js
```javascript
import LatLon from '/js/geodesy/latlon-spherical.js';
const p1 = new LatLon(52.205, 0.119);
const p2 = new LatLon(48.857, 2.351);
const d = p1.distanceTo(p2); // 404.3×10³ m
````
```

@@ -74,4 +72,4 @@ - or to find the destination point for a given distance and initial bearing on an ellipsoidal model

````
import LatLon from '/js/geodesy/latlon-ellipsoidal-vincency.js
```javascript
import LatLon from '/js/geodesy/latlon-ellipsoidal-vincency.js';
const p1 = new LatLon(-37.95103, 144.42487);

@@ -81,6 +79,7 @@ const dist = 54972.271;

const p2 = p1.destinationPoint(dist, brng); // 37.6528°S, 143.9265°E
````
```
Full documentation is available at [www.movable-type.co.uk/scripts/geodesy-library.html](https://www.movable-type.co.uk/scripts/geodesy-library.html),
and there is a full [test suite](https://www.movable-type.co.uk/scripts/test/geodesy-test.html).
and tests in the [browser](https://www.movable-type.co.uk/scripts/test/geodesy-test.html) as well as
[Travis CI](https://travis-ci.org/chrisveness/geodesy).

@@ -98,29 +97,33 @@ Usage

<!doctype html><title>geodesy example</title><meta charset="utf-8">
<script type="module">
import LatLon from 'https://cdn.jsdelivr.net/gh/chrisveness/geodesy@2.0.0/latlon-spherical.min.js';
```html
<!doctype html><title>geodesy example</title><meta charset="utf-8">
<script type="module">
import LatLon from 'https://cdn.jsdelivr.net/gh/chrisveness/geodesy@2.0.1/latlon-spherical.min.js';
const p1 = new LatLon(50.06632, -5.71475);
const p2 = new LatLon(58.64402, -3.07009);
const p1 = new LatLon(50.06632, -5.71475);
const p2 = new LatLon(58.64402, -3.07009);
const d = p1.distanceTo(p2);
console.assert(d.toFixed(3) == '968874.704');
const d = p1.distanceTo(p2);
console.assert(d.toFixed(3) == '968874.704');
const mid = p1.midpointTo(p2);
console.assert(mid.toString('dms') == '54°21′44″N, 004°31′51″W');
</script>
const mid = p1.midpointTo(p2);
console.assert(mid.toString('dms') == '54° 21′ 44″ N, 004° 31′ 51″ W');
</script>
```
### Usage in Node.js
The library can be used in a Node.js app from npm:
The library can be used in a Node.js app from [npm](https://www.npmjs.com/package/geodesy):
$ npm install geodesy
$ node -r esm
> import LatLon from 'geodesy/latlon-spherical';
> const p1 = new LatLon(50.06632, -5.71475);
> const p2 = new LatLon(58.64402, -3.07009);
> const d = p1.distanceTo(p2);
> console.assert(d.toFixed(3) == '968874.704');
> const mid = p1.midpointTo(p2);
> console.assert(mid.toString() == '54°21′44″N, 004°31′51″W');
```shell
$ npm install geodesy
$ node -r esm
> import LatLon from 'geodesy/latlon-spherical';
> const p1 = new LatLon(50.06632, -5.71475);
> const p2 = new LatLon(58.64402, -3.07009);
> const d = p1.distanceTo(p2);
> console.assert(d.toFixed(3) == '968874.704');
> const mid = p1.midpointTo(p2);
> console.assert(mid.toString('dms') == '54° 21′ 44″ N, 004° 31′ 51″ W');
```

@@ -133,74 +136,87 @@ ### Other examples

import LatLon from 'geodesy/latlon-ellipsoidal-vincenty.js';
```javascript
import LatLon from 'geodesy/latlon-ellipsoidal-vincenty.js';
const p1 = new LatLon(50.06632, -5.71475);
const p2 = new LatLon(58.64402, -3.07009);
const p1 = new LatLon(50.06632, -5.71475);
const p2 = new LatLon(58.64402, -3.07009);
const d = p1.distanceTo(p2);
console.assert(d.toFixed(3) == '969954.166');
const d = p1.distanceTo(p2);
console.assert(d.toFixed(3) == '969954.166');
```
e.g. for UTM conversions:
import Utm from 'geodesy/utm.js';
```javascript
import Utm from 'geodesy/utm.js';
const utm = Utm.parse('48 N 377298.745 1483034.794');
const latlon = utm.toLatLon();
console.assert(latlon.toString('dms', 2) == '13°24′45.00″N, 103°52′00.00″E');
const utm = Utm.parse('48 N 377298.745 1483034.794');
const latlon = utm.toLatLon();
console.assert(latlon.toUtm().toString() == '48 N 377298.745 1483034.794';
console.assert(latlon.toString('dms', 2) == '13° 24′ 45.00″ N, 103° 52′ 00.00″ E');
console.assert(latlon.toUtm().toString() == '48 N 377298.745 1483034.794';
```
e.g. for MGRS/NATO map references:
import Mgrs, { LatLon } from 'geodesy/mgrs.js';
```javascript
import Mgrs, { LatLon } from 'geodesy/mgrs.js';
const mgrs = Mgrs.parse('31U DQ 48251 11932');
const latlon = mgrs.toUtm().toLatLon();
console.assert(latlon.toString('dms', 2) == '48°51′29.50″N, 002°17′40.16″E');
const mgrs = Mgrs.parse('31U DQ 48251 11932');
const latlon = mgrs.toUtm().toLatLon();
console.assert(latlon.toString('dms', 2) == '48° 51′ 29.50″ N, 002° 17′ 40.16″ E');
const p = LatLon.parse('51°28′40.37″N, 000°00′05.29″W');
const ref = p.toUtm().toMgrs();
console.assert(ref.toString() == '30U YC 08215 07233');
const p = LatLon.parse('51°28′40.37″N, 000°00′05.29″W');
const ref = p.toUtm().toMgrs();
console.assert(ref.toString() == '30U YC 08215 07233');
```
e.g. for OS grid references:
import OsGridRef, { LatLon } from 'geodesy/osgridref.js';
```javascript
import OsGridRef, { LatLon } from 'geodesy/osgridref.js';
const gridref = new OsGridRef(651409.903, 313177.270);
const gridref = new OsGridRef(651409.903, 313177.270);
const pWgs84 = OsGridRef.osGridToLatLon(gridref);
console.assert(pWgs84.toString('dms', 4) == '52°39′28.7230″N, 001°42′57.7870″E');
const pWgs84 = gridref.toLatLon();
console.assert(pWgs84.toString('dms', 4) == '52° 39′ 28.7230″ N, 001° 42′ 57.7870″ E');
const pOsgb = OsGridRef.osGridToLatLon(gridref, LatLon.datums.OSGB36);
console.assert(pOsgb.toString('dms', 4) == '52°39′27.2531″N, 001°43′04.5177″E');
const pOsgb = gridref.toLatLon(LatLon.datums.OSGB36);
console.assert(pOsgb.toString('dms', 4) == '52° 39′ 27.2531″ N, 001° 43′ 04.5177″ E');
```
e.g. for testing if a point is enclosed within a polygon:
import LatLon from 'geodesy/latlon-nvector-spherical.js';
```javascript
import LatLon from 'geodesy/latlon-nvector-spherical.js';
const polygon = [ new LatLon(48,2), new LatLon(49,2), new LatLon(49,3), new LatLon(48,3) ];
const polygon = [ new LatLon(48,2), new LatLon(49,2), new LatLon(49,3), new LatLon(48,3) ];
const enclosed = new LatLon(48.9,2.4).isEnclosedBy(polygon)
const enclosed = new LatLon(48.9,2.4).isEnclosedBy(polygon);
console.assert(enclosed == true);
```
console.assert(enclosed == true);
e.g. greater parsing & presentation control:
import LatLon from 'geodesy/latlon-spherical.js';
```javascript
import LatLon from 'geodesy/latlon-spherical.js';
Dms.separator = ' '; // full-space separator between degrees-minutes-seconds
const p1 = LatLon.parse('50°03′59″N, 005°42′53″W'));
const p2 = LatLon.parse('58°38′38″N, 003°04′12″W'));
const p1 = LatLon.parse({ lat: '50:03:59N', lng: '005:42:53W' });
const p2 = LatLon.parse({ lat: '58:38:38N', lng: '003:04:12W' });
const mid = p1.midpointTo(p2);
Dms.separator = ' '; // full-space separator between degrees-minutes-seconds
console.assert(mid.toString('dms') == '54° 21′ 44″N, 004° 31′ 51″W');
const mid = p1.midpointTo(p2);
console.assert(mid.toString('dms') == '54° 21′ 44″ N, 004° 31′ 50″ W');
```
e.g. datum conversions:
import LatLon from 'geodesy/latlon-ellipsoidal-datum.js';
```javascript
import LatLon from 'geodesy/latlon-ellipsoidal-datum.js';
const pWgs84 = new LatLon('53.3498° N, 6.2603° W');
const pIrl1975 = pWgs84.convertDatum(LatLon.datums.Irl1975);
const pWgs84 = new LatLon(53.3444, -6.2577);
console.assert(pIrl1975.toString() == '53.3497°N, 006.2589°W');
const pIrl1975 = pWgs84.convertDatum(LatLon.datums.Irl1975);
console.assert(pIrl1975.toString() == '53.3442° N, 006.2567° W');
```
(The format of the import statements will vary according to deployment).

@@ -28,4 +28,4 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

describe('valid datum', function() {
test('constructor', () => should.Throw(function() { new LatLon(0, 0, 0, null); }, TypeError, 'Unrecognised datum ‘null’'));
test('parse', () => should.Throw(function() { LatLon.parse('0, 0', 0, null); }, TypeError, 'Unrecognised datum ‘null’'));
test('constructor', () => should.Throw(function() { new LatLon(0, 0, 0, null); }, TypeError, 'unrecognised datum ‘null’'));
test('parse', () => should.Throw(function() { LatLon.parse('0, 0', 0, null); }, TypeError, 'unrecognised datum ‘null’'));
});

@@ -43,3 +43,3 @@

test('convert round-trip', () => greenwichOSGB36.convertDatum(LatLon.datums.WGS84).toString('d', 5).should.equal('51.47788°N, 000.00147°W'));
test('convert fails', () => should.Throw(function() { new LatLon(51, 0).convertDatum(null); }, TypeError, 'Unrecognised datum'));
test('convert fails', () => should.Throw(function() { new LatLon(51, 0).convertDatum(null); }, TypeError, 'unrecognised datum ‘null’'));
});

@@ -64,3 +64,3 @@

test('LL neq (datum)', () => p1.equals(new LatLon(51.47788, -0.00147, 1, LatLon.datums.Irl1975)).should.be.false);
test('equals (fail)', () => should.Throw(function() { p1.equals(null); }, TypeError, '‘point’ is not LatLon object'));
test('equals (fail)', () => should.Throw(function() { p1.equals(null); }, TypeError, 'invalid point ‘null’'));
});

@@ -73,5 +73,5 @@

test('toLatLon', () => c.toLatLon().toString().should.equal('45.0000°N, 045.0000°E'));
test('toLatLon fail', () => should.Throw(function() { c.toLatLon(null); }, TypeError, 'Unrecognised datum'));
test('toLatLon fail', () => should.Throw(function() { c.toLatLon('xx'); }, TypeError, 'Unrecognised datum ‘xx’'));
test('toLatLon fail', () => should.Throw(function() { c.toLatLon(null); }, TypeError, 'unrecognised datum ‘null’'));
test('toLatLon fail', () => should.Throw(function() { c.toLatLon('xx'); }, TypeError, 'unrecognised datum ‘xx’'));
});
});

@@ -20,4 +20,4 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

test('TRF', () => new LatLon(0, 0, 0, LatLon.referenceFrames.ITRF2014, 2000.0).toString().should.equal('00.0000°N, 000.0000°E'));
test('bad TRF fail', () => should.Throw(function() { new LatLon(0, 0, 0, null); }, TypeError, 'Unrecognised reference frame'));
test('bad epoch fail', () => should.Throw(function() { new LatLon(0, 0, 0, LatLon.referenceFrames.ITRF2014, 'xxx'); }, TypeError, 'Invalid epoch ’xxx’'));
test('bad TRF fail', () => should.Throw(function() { new LatLon(0, 0, 0, null); }, TypeError, 'unrecognised reference frame'));
test('bad epoch fail', () => should.Throw(function() { new LatLon(0, 0, 0, LatLon.referenceFrames.ITRF2014, 'xxx'); }, TypeError, 'invalid epoch ’xxx’'));
});

@@ -55,26 +55,26 @@

describe('parse fail', function() {
test('empty', () => should.Throw(function() { LatLon.parse(); }, TypeError, 'Invalid (empty) coordinate'));
test('l,l bad TRF', () => should.Throw(function() { LatLon.parse(0, 0, 0, 0); }, TypeError, 'Unrecognised reference frame'));
test('l,l bad TRF', () => should.Throw(function() { LatLon.parse(0, 0, 0, null); }, TypeError, 'Unrecognised reference frame'));
test('l/l bad TRF', () => should.Throw(function() { LatLon.parse('0, 0', 0, 0); }, TypeError, 'Unrecognised reference frame'));
test('l/l bad TRF', () => should.Throw(function() { LatLon.parse('0, 0', 0, null); }, TypeError, 'Unrecognised reference frame'));
test('empty', () => should.Throw(function() { LatLon.parse(); }, TypeError, 'invalid (empty) point'));
test('l,l bad TRF', () => should.Throw(function() { LatLon.parse(0, 0, 0, 0); }, TypeError, 'unrecognised reference frame'));
test('l,l bad TRF', () => should.Throw(function() { LatLon.parse(0, 0, 0, null); }, TypeError, 'unrecognised reference frame'));
test('l/l bad TRF', () => should.Throw(function() { LatLon.parse('0, 0', 0, 0); }, TypeError, 'unrecognised reference frame'));
test('l/l bad TRF', () => should.Throw(function() { LatLon.parse('0, 0', 0, null); }, TypeError, 'unrecognised reference frame'));
});
describe('convertReferenceFrame fail', function() {
test('no TRF', () => should.Throw(function() { new LatLon(0, 0).convertReferenceFrame('ITRF2014'); }, TypeError, 'Unrecognised reference frame'));
test('no TRF', () => should.Throw(function() { new Cartesian(1, 2, 3).convertReferenceFrame('ITRF2014'); }, TypeError, 'Unrecognised reference frame'));
test('no TRF', () => should.Throw(function() { new LatLon(0, 0).convertReferenceFrame('ITRF2014'); }, TypeError, 'unrecognised reference frame'));
test('no TRF', () => should.Throw(function() { new Cartesian(1, 2, 3).convertReferenceFrame('ITRF2014'); }, TypeError, 'unrecognised reference frame'));
});
describe('Cartesian constructor fail', function() {
test('empty', () => should.Throw(function() { new Cartesian(4027893.924, 307041.993, 4919474.294, 'ITRF2000'); }, Error, 'Unrecognised reference frame'));
test('empty', () => should.Throw(function() { new Cartesian(4027893.924, 307041.993, 4919474.294, LatLon.referenceFrames.ITRF2000, 'last year'); }, Error, 'Invalid epoch ’last year’'));
test('empty', () => should.Throw(function() { new Cartesian(4027893.924, 307041.993, 4919474.294, 'ITRF2000'); }, Error, 'unrecognised reference frame'));
test('empty', () => should.Throw(function() { new Cartesian(4027893.924, 307041.993, 4919474.294, LatLon.referenceFrames.ITRF2000, 'last year'); }, Error, 'invalid epoch ’last year’'));
});
describe('Cartesian setter fail', function() {
test('bad TRF', () => should.Throw(function() { new Cartesian(1, 2, 3).referenceFrame = 'ITRF2014'; }, TypeError, 'Unrecognised reference frame'));
test('bad epoch', () => should.Throw(function() { new Cartesian(1, 2, 3).epoch = 'last year'; }, TypeError, 'Invalid epoch ’last year’'));
test('bad TRF', () => should.Throw(function() { new Cartesian(1, 2, 3).referenceFrame = 'ITRF2014'; }, TypeError, 'unrecognised reference frame'));
test('bad epoch', () => should.Throw(function() { new Cartesian(1, 2, 3).epoch = 'last year'; }, TypeError, 'invalid epoch ’last year’'));
});
describe('Cartesian.toLatLon fail', function() {
test('empty', () => should.Throw(function() { new Cartesian(4027893.924, 307041.993, 4919474.294).toLatLon(null); }, Error, 'Cartesian reference frame not defined'));
test('empty', () => should.Throw(function() { new Cartesian(4027893.924, 307041.993, 4919474.294).toLatLon(null); }, Error, 'cartesian reference frame not defined'));
});

@@ -81,0 +81,0 @@

@@ -20,5 +20,5 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

test('@example', () => new LatLon(51.47788, -0.00147, 17).toString('d', 4, 2).should.equal('51.4779°N, 000.0015°W +17.00m'));
test('non-numeric lat fail', () => should.Throw(function() { new LatLon('x', 0, 0); }, TypeError, 'Invalid lat ‘x’'));
test('non-numeric lon fail', () => should.Throw(function() { new LatLon(0, 'x', 0); }, TypeError, 'Invalid lon ‘x’'));
test('non-numeric height fail', () => should.Throw(function() { new LatLon(0, 0, 'x'); }, TypeError, 'Invalid height ‘x’'));
test('non-numeric lat fail', () => should.Throw(function() { new LatLon('x', 0, 0); }, TypeError, 'invalid lat ‘x’'));
test('non-numeric lon fail', () => should.Throw(function() { new LatLon(0, 'x', 0); }, TypeError, 'invalid lon ‘x’'));
test('non-numeric height fail', () => should.Throw(function() { new LatLon(0, 0, 'x'); }, TypeError, 'invalid height ‘x’'));
});

@@ -57,12 +57,12 @@

describe('parse fail', function() {
test('empty', () => should.Throw(function() { LatLon.parse(); }, TypeError, 'Invalid (empty) coordinate'));
test('single arg num', () => should.Throw(function() { LatLon.parse(1); }, TypeError, 'Invalid coordinate ‘1’'));
test('single arg str', () => should.Throw(function() { LatLon.parse('London'); }, TypeError, 'Invalid coordinate ‘London’'));
test('single arg str + h', () => should.Throw(function() { LatLon.parse('London', 99); }, TypeError, 'Invalid coordinate ‘London,99’'));
test('invalid comma arg', () => should.Throw(function() { LatLon.parse('London,UK'); }, TypeError, 'Invalid coordinate ‘London,UK’'));
test('invalid comma arg + h', () => should.Throw(function() { LatLon.parse('London,UK', 99); }, TypeError, 'Invalid coordinate ‘London,UK’'));
test('empty object', () => should.Throw(function() { LatLon.parse({}); }, TypeError, 'Invalid coordinate ‘{}’'));
test('invalid object 1', () => should.Throw(function() { LatLon.parse({ y: 51.47788, x: -0.00147 }); }, TypeError, 'Invalid coordinate ‘{"y":51.47788,"x":-0.00147}’'));
test('invalid object 2', () => should.Throw(function() { LatLon.parse({ lat: 'y', lon: 'x' }); }, TypeError, 'Invalid coordinate ‘{"lat":"y","lon":"x"}’'));
test('invalid lat,lon', () => should.Throw(function() { LatLon.parse(null, null); }, TypeError, 'Invalid coordinate ‘,’'));
test('empty', () => should.Throw(function() { LatLon.parse(); }, TypeError, 'invalid (empty) point'));
test('single arg num', () => should.Throw(function() { LatLon.parse(1); }, TypeError, 'invalid point ‘1’'));
test('single arg str', () => should.Throw(function() { LatLon.parse('London'); }, TypeError, 'invalid point ‘London’'));
test('single arg str + h', () => should.Throw(function() { LatLon.parse('London', 99); }, TypeError, 'invalid point ‘London,99’'));
test('invalid comma arg', () => should.Throw(function() { LatLon.parse('London,UK'); }, TypeError, 'invalid point ‘London,UK’'));
test('invalid comma arg + h', () => should.Throw(function() { LatLon.parse('London,UK', 99); }, TypeError, 'invalid point ‘London,UK’'));
test('empty object', () => should.Throw(function() { LatLon.parse({}); }, TypeError, 'invalid point ‘{}’'));
test('invalid object 1', () => should.Throw(function() { LatLon.parse({ y: 51.47788, x: -0.00147 }); }, TypeError, 'invalid point ‘{"y":51.47788,"x":-0.00147}’'));
test('invalid object 2', () => should.Throw(function() { LatLon.parse({ lat: 'y', lon: 'x' }); }, TypeError, 'invalid point ‘{"lat":"y","lon":"x"}’'));
test('invalid lat,lon', () => should.Throw(function() { LatLon.parse(null, null); }, TypeError, 'invalid point ‘,’'));
});

@@ -110,8 +110,8 @@

test('get ellipsoids', () => LatLon.ellipsoids.should.deep.equal({ WGS84: { a: 6378137, b: 6356752.314245, f: 1/298.257223563 } }));
test('set lat fail', () => should.Throw(function() { p.lat = 'x'; }, TypeError, 'Invalid lat ‘x’'));
test('set latitude fail', () => should.Throw(function() { p.latitude = 'x'; }, TypeError, 'Invalid latitude ‘x’'));
test('set lon fail', () => should.Throw(function() { p.lon = 'x'; }, TypeError, 'Invalid lon ‘x’'));
test('set lng fail', () => should.Throw(function() { p.lng = 'x'; }, TypeError, 'Invalid lng ‘x’'));
test('set longitude fail', () => should.Throw(function() { p.longitude = 'x'; }, TypeError, 'Invalid longitude ‘x’'));
test('set height fail', () => should.Throw(function() { p.height = 'x'; }, TypeError, 'Invalid height ‘x’'));
test('set lat fail', () => should.Throw(function() { p.lat = 'x'; }, TypeError, 'invalid lat ‘x’'));
test('set latitude fail', () => should.Throw(function() { p.latitude = 'x'; }, TypeError, 'invalid latitude ‘x’'));
test('set lon fail', () => should.Throw(function() { p.lon = 'x'; }, TypeError, 'invalid lon ‘x’'));
test('set lng fail', () => should.Throw(function() { p.lng = 'x'; }, TypeError, 'invalid lng ‘x’'));
test('set longitude fail', () => should.Throw(function() { p.longitude = 'x'; }, TypeError, 'invalid longitude ‘x’'));
test('set height fail', () => should.Throw(function() { p.height = 'x'; }, TypeError, 'invalid height ‘x’'));
});

@@ -127,3 +127,3 @@

test('LL neq h', () => (p1.equals(new LatLon(51.47788, -0.00147, 99))).should.equal(false));
test('equals fail', () => should.Throw(function() { p1.equals(null); }, TypeError, '‘point’ is not LatLon object'));
test('equals fail', () => should.Throw(function() { p1.equals(null); }, TypeError, 'invalid point ‘null’'));
});

@@ -136,4 +136,4 @@

test('toLatLon', () => c.toLatLon().toString().should.equal('45.0000°N, 045.0000°E'));
test('toLatLon fail', () => should.Throw(function() { c.toLatLon(null); }, TypeError, 'Invalid ellipsoid'));
test('toLatLon fail', () => should.Throw(function() { c.toLatLon(null); }, TypeError, 'invalid ellipsoid'));
});
});

@@ -27,5 +27,5 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

test('direct final bearing', () => le.finalBearingOn(dist, brngInit).should.equal(brngFinal));
test('inverse distance (fail)', () => should.Throw(function() { le.distanceTo(null); }, TypeError, '‘point’ is not (Ellipsoidal) LatLon object'));
test('inverse init brng (fail)', () => should.Throw(function() { le.initialBearingTo(null); }, TypeError, '‘point’ is not (Ellipsoidal) LatLon object'));
test('inverse final brng (fail)', () => should.Throw(function() { le.finalBearingTo(null); }, TypeError, '‘point’ is not (Ellipsoidal) LatLon object'));
test('inverse distance (fail)', () => should.Throw(function() { le.distanceTo(null); }, TypeError, 'invalid point ‘null’'));
test('inverse init brng (fail)', () => should.Throw(function() { le.initialBearingTo(null); }, TypeError, 'invalid point ‘null’'));
test('inverse final brng (fail)', () => should.Throw(function() { le.finalBearingTo(null); }, TypeError, 'invalid point ‘null’'));
});

@@ -86,11 +86,12 @@

const le = new LatLon(50.06632, -5.71475, 1), jog = new LatLon(58.64402, -3.07009);
test('distanceTo (fail)', () => should.Throw(function() { le.distanceTo(jog); }, RangeError, 'Point must be on the surface of the ellipsoid'));
test('initialBearingTo (fail)', () => should.Throw(function() { le.initialBearingTo(jog); }, RangeError, 'Point must be on the surface of the ellipsoid'));
test('finalBearingTo (fail)', () => should.Throw(function() { le.finalBearingTo(jog); }, RangeError, 'Point must be on the surface of the ellipsoid'));
test('destinationPoint (fail)', () => should.Throw(function() { le.destinationPoint(0, 0); }, RangeError, 'Point must be on the surface of the ellipsoid'));
test('finalBearingOn (fail)', () => should.Throw(function() { le.finalBearingOn(0, 0); }, RangeError, 'Point must be on the surface of the ellipsoid'));
test('distanceTo (fail)', () => should.Throw(function() { le.distanceTo(jog); }, RangeError, 'point must be on the surface of the ellipsoid'));
test('initialBearingTo (fail)', () => should.Throw(function() { le.initialBearingTo(jog); }, RangeError, 'point must be on the surface of the ellipsoid'));
test('finalBearingTo (fail)', () => should.Throw(function() { le.finalBearingTo(jog); }, RangeError, 'point must be on the surface of the ellipsoid'));
test('destinationPoint (fail)', () => should.Throw(function() { le.destinationPoint(0, 0); }, RangeError, 'point must be on the surface of the ellipsoid'));
test('finalBearingOn (fail)', () => should.Throw(function() { le.finalBearingOn(0, 0); }, RangeError, 'point must be on the surface of the ellipsoid'));
});
describe('convergence', function() {
test('vincenty antipodal convergence failure', () => new LatLon(0, 0).distanceTo(new LatLon(0.5, 179.7)).should.be.NaN);
test('vincenty antipodal λ > π', () => new LatLon(0.0, 0.0).distanceTo(new LatLon(0.5, 179.7)).should.be.NaN);
test('vincenty antipodal convergence', () => new LatLon(5.0, 0.0).distanceTo(new LatLon(-5.1, 179.4)).should.be.NaN);
});

@@ -109,5 +110,8 @@

const dist = 969982.014; // 27.848m more than on WGS-84 ellipsoid; Airy1830 has a smaller flattening, hence larger distance at higher latitudes
const brngInit = 9.1428517;
test('inverse distance', () => le.distanceTo(jog).should.equal(dist));
test('inverse bearing', () => le.initialBearingTo(jog).should.equal(brngInit));
test('direct destination', () => le.destinationPoint(dist, brngInit).toString('d', 6).should.equal('58.644399°N, 003.068521°W'));
});
});

@@ -137,3 +137,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

test('fail', () => should.Throw(function() { new LatLon(0, 0).deltaTo(null); }, TypeError, '‘point’ is not (Ellipsoidal) LatLon object'));
test('fail', () => should.Throw(function() { new LatLon(0, 0).deltaTo(null); }, TypeError, 'invalid point ‘null’'));
});

@@ -140,0 +140,0 @@

@@ -13,36 +13,43 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

describe('latlon-nvector-spherical', function() {
const test = it; // just an alias
Dms.separator = ''; // tests are easier without any DMS separator
const R = 6371e3;
const π = Math.PI;
const ε = Number.EPSILON;
describe('@examples', function() {
test('constructor', () => new LatLon(52.205, 0.119).toString().should.equal('52.2050°N, 000.1190°E'));
test('toNvector', () => new LatLon(45, 45).toNvector().toString().should.equal('[0.500,0.500,0.707]'));
test('greatCircle', () => new LatLon(53.3206, -1.7297).greatCircle(96.0).toString().should.equal('[-0.794,0.129,0.594]'));
test('distanceTo d', () => new LatLon(52.205, 0.119).distanceTo(new LatLon(48.857, 2.351)).toFixed().should.equal('404279'));
test('distanceTo m', () => new LatLon(52.205, 0.119).distanceTo(new LatLon(48.857, 2.351), 3959).toFixed(1).should.equal('251.2'));
test('initialBearingTo', () => new LatLon(52.205, 0.119).initialBearingTo(new LatLon(48.857, 2.351)).toFixed(1).should.equal('156.2'));
test('finalBearingTo', () => new LatLon(52.205, 0.119).finalBearingTo(new LatLon(48.857, 2.351)).toFixed(1).should.equal('157.9'));
test('midpointTo', () => new LatLon(52.205, 0.119).midpointTo(new LatLon(48.857, 2.351)).toString().should.equal('50.5363°N, 001.2746°E'));
test('intermediatePointTo', () => new LatLon(52.205, 0.119).intermediatePointTo(new LatLon(48.857, 2.351), 0.25).toString().should.equal('51.3721°N, 000.7073°E'));
test('intermediatePointOnChordTo', () => new LatLon(52.205, 0.119).intermediatePointOnChordTo(new LatLon(48.857, 2.351), 0.25).toString().should.equal('51.3723°N, 000.7072°E'));
test('destinationPoint', () => new LatLon(51.47788, -0.00147).destinationPoint(7794, 300.7).toString().should.equal('51.5136°N, 000.0983°W'));
test('intersection', () => LatLon.intersection(new LatLon(51.8853, 0.2545), 108.547, new LatLon(49.0034, 2.5735), 32.435).toString().should.equal('50.9078°N, 004.5084°E'));
test('crossTrackDistanceTo', () => new LatLon(53.2611, -0.7972).crossTrackDistanceTo(new LatLon(53.3206, -1.7297), new LatLon(53.1887, 0.1334)).toFixed(1).should.equal('-307.5'));
test('nearestPointOnSegment 1', () => new LatLon(51.0, 1.9).nearestPointOnSegment(new LatLon(51.0, 1.0), new LatLon(51.0, 2.0)).toString().should.equal('51.0004°N, 001.9000°E'));
test('nearestPointOnSegment 2', () => new LatLon(51.0, 2.1).nearestPointOnSegment(new LatLon(51.0, 1.0), new LatLon(51.0, 2.0)).toString().should.equal('51.0000°N, 002.0000°E'));
test('isWithinExtent 1', () => new LatLon(52, 1).isWithinExtent(new LatLon(51, 1), new LatLon(52, 2)).should.be.true);
test('isWithinExtent 2', () => new LatLon(51, 0).isWithinExtent(new LatLon(51, 1), new LatLon(52, 2)).should.be.false);
test('triangulate', () => LatLon.triangulate(new LatLon(50.7175, 1.65139), 333.3508, new LatLon(50.9250, 1.7094), 310.1414).toString().should.equal('51.1297°N, 001.3214°E'));
test('trilaterate', () => LatLon.trilaterate(new LatLon(0, 0), 157e3, new LatLon(0, 1), 111e3, new LatLon(1, 0), 111e3).toString().should.equal('00.9985°N, 000.9986°E'));
test('constructor', () => new LatLon(52.205, 0.119).toString().should.equal('52.2050°N, 000.1190°E'));
test('toNvector', () => new LatLon(45, 45).toNvector().toString().should.equal('[0.500,0.500,0.707]'));
test('greatCircle', () => new LatLon(53.3206, -1.7297).greatCircle(96.0).toString().should.equal('[-0.794,0.129,0.594]'));
test('distanceTo d', () => new LatLon(52.205, 0.119).distanceTo(new LatLon(48.857, 2.351)).toFixed().should.equal('404279'));
test('distanceTo m', () => new LatLon(52.205, 0.119).distanceTo(new LatLon(48.857, 2.351), 3959).toFixed(1).should.equal('251.2'));
test('initialBearingTo', () => new LatLon(52.205, 0.119).initialBearingTo(new LatLon(48.857, 2.351)).toFixed(1).should.equal('156.2'));
test('finalBearingTo', () => new LatLon(52.205, 0.119).finalBearingTo(new LatLon(48.857, 2.351)).toFixed(1).should.equal('157.9'));
test('midpointTo', () => new LatLon(52.205, 0.119).midpointTo(new LatLon(48.857, 2.351)).toString().should.equal('50.5363°N, 001.2746°E'));
test('intermediatePointTo', () => new LatLon(52.205, 0.119).intermediatePointTo(new LatLon(48.857, 2.351), 0.25).toString().should.equal('51.3721°N, 000.7073°E'));
test('intermediatePointOnChordTo', () => new LatLon(52.205, 0.119).intermediatePointOnChordTo(new LatLon(48.857, 2.351), 0.25).toString().should.equal('51.3723°N, 000.7072°E'));
test('destinationPoint', () => new LatLon(51.47788, -0.00147).destinationPoint(7794, 300.7).toString().should.equal('51.5136°N, 000.0983°W'));
test('intersection', () => LatLon.intersection(new LatLon(51.8853, 0.2545), 108.547, new LatLon(49.0034, 2.5735), 32.435).toString().should.equal('50.9078°N, 004.5084°E'));
test('crossTrackDistanceTo', () => new LatLon(53.2611, -0.7972).crossTrackDistanceTo(new LatLon(53.3206, -1.7297), new LatLon(53.1887, 0.1334)).toFixed(1).should.equal('-307.5'));
test('alongTrackDistanceTo', () => new LatLon(53.2611, -0.7972).alongTrackDistanceTo(new LatLon(53.3206, -1.7297), new LatLon(53.1887, 0.1334)).toFixed().should.equal('62331'));
test('nearestPointOnSegment 1', () => new LatLon(51.0, 1.9).nearestPointOnSegment(new LatLon(51.0, 1.0), new LatLon(51.0, 2.0)).toString().should.equal('51.0004°N, 001.9000°E'));
test('nearestPointOnSegment 2', () => new LatLon(51.0, 2.1).nearestPointOnSegment(new LatLon(51.0, 1.0), new LatLon(51.0, 2.0)).toString().should.equal('51.0000°N, 002.0000°E'));
test('nearestPointOnSegment antip', () => new LatLon(10, -140).nearestPointOnSegment(new LatLon(0, 20), new LatLon(0, 40)).toString().should.equal('00.0000°N, 020.0000°E'));
test('isWithinExtent 1', () => new LatLon(52, 1).isWithinExtent(new LatLon(51, 1), new LatLon(52, 2)).should.be.true);
test('isWithinExtent 2', () => new LatLon(51, 0).isWithinExtent(new LatLon(51, 1), new LatLon(52, 2)).should.be.false);
test('isWithinExtent coincident', () => new LatLon(51, 0).isWithinExtent(new LatLon(51, 1), new LatLon(51, 1)).should.be.false);
test('triangulate', () => LatLon.triangulate(new LatLon(50.7175, 1.65139), 333.3508, new LatLon(50.9250, 1.7094), 310.1414).toString().should.equal('51.1297°N, 001.3214°E'));
test('trilaterate', () => LatLon.trilaterate(new LatLon(0, 0), 157e3, new LatLon(0, 1), 111e3, new LatLon(1, 0), 111e3).toString().should.equal('00.9985°N, 000.9986°E'));
const bounds = [ new LatLon(45, 1), new LatLon(45, 2), new LatLon(46, 2), new LatLon(46, 1) ];
test('isEnclosedBy', () => new LatLon(45.1, 1.1).isEnclosedBy(bounds).should.be.true);
const polygon = [ new LatLon(0, 0), new LatLon(1, 0), new LatLon(0, 1) ];
test('areaOf', () => LatLon.areaOf(polygon).toExponential(2).should.equal('6.18e+9'));
test('meanOf', () => LatLon.meanOf([ new LatLon(1, 1), new LatLon(4, 2), new LatLon(1, 3) ]).toString().should.equal('02.0001°N, 002.0000°E'));
test('equals', () => new LatLon(52.205, 0.119).equals(new LatLon(52.205, 0.119)).should.be.true);
test('isEnclosedBy', () => new LatLon(45.1, 1.1).isEnclosedBy(bounds).should.be.true);
test('areaOf cw', () => LatLon.areaOf([ new LatLon(0, 0), new LatLon(1, 0), new LatLon(0, 1) ]).toExponential(2).should.equal('6.18e+9'));
test('areaOf ccw', () => LatLon.areaOf([ new LatLon(0, 0), new LatLon(0, 1), new LatLon(1, 0) ]).toExponential(2).should.equal('6.18e+9'));
test('meanOf', () => LatLon.meanOf([ new LatLon(1, 1), new LatLon(4, 2), new LatLon(1, 3) ]).toString().should.equal('02.0001°N, 002.0000°E'));
test('equals', () => new LatLon(52.205, 0.119).equals(new LatLon(52.205, 0.119)).should.be.true);
const greenwich = new LatLon(51.47788, -0.00147);
test('toString d', () => greenwich.toString().should.equal('51.4779°N, 000.0015°W'));
test('toString dms', () => greenwich.toString('dms').should.equal('51°28′40″N, 000°00′05″W'));
test('toString lat,lon', () => greenwich.toString('n').split(',').should.deep.equal([ '51.4779', '-0.0015' ]));
test('toString d', () => greenwich.toString().should.equal('51.4779°N, 000.0015°W'));
test('toString dms', () => greenwich.toString('dms', 2).should.equal('51°28′40.37″N, 000°00′05.29″W'));
test('toString lat,lon', () => greenwich.toString('n').split(',').should.deep.equal([ '51.4779', '-0.0015' ]));
});

@@ -56,9 +63,5 @@

describe('constructor fail', function() {
test('non-numeric lat fail', () => should.Throw(function() { new LatLon('x', 0, 0); }, TypeError, 'Invalid lat ‘x’'));
test('non-numeric lon fail', () => should.Throw(function() { new LatLon(0, 'x', 0); }, TypeError, 'Invalid lon ‘x’'));
test('non-numeric lat fail', () => should.Throw(function() { new LatLon('x', 0, 0); }, TypeError, 'invalid lat ‘x’'));
test('non-numeric lon fail', () => should.Throw(function() { new LatLon(0, 'x', 0); }, TypeError, 'invalid lon ‘x’'));
});
describe('constructor fail', function() {
test('non-numeric lat fail', () => should.Throw(function() { new LatLon('x', 0, 0); }, TypeError, 'Invalid lat ‘x’'));
test('non-numeric lon fail', () => should.Throw(function() { new LatLon(0, 'x', 0); }, TypeError, 'Invalid lon ‘x’'));
});

@@ -88,7 +91,7 @@ describe('getters/setters', function() {

const camb = new LatLon(0, 0);
test('lat', () => should.Throw(function() { camb.lat = 'xxx'; }, TypeError, 'Invalid lat ‘xxx’'));
test('latitude', () => should.Throw(function() { camb.latitude = 'xxx'; }, TypeError, 'Invalid latitude ‘xxx’'));
test('lon', () => should.Throw(function() { camb.lon = 'xxx'; }, TypeError, 'Invalid lon ‘xxx’'));
test('lgn', () => should.Throw(function() { camb.lng = 'xxx'; }, TypeError, 'Invalid lng ‘xxx’'));
test('longitude', () => should.Throw(function() { camb.longitude = 'xxx'; }, TypeError, 'Invalid longitude ‘xxx’'));
test('lat', () => should.Throw(function() { camb.lat = 'xxx'; }, TypeError, 'invalid lat ‘xxx’'));
test('latitude', () => should.Throw(function() { camb.latitude = 'xxx'; }, TypeError, 'invalid latitude ‘xxx’'));
test('lon', () => should.Throw(function() { camb.lon = 'xxx'; }, TypeError, 'invalid lon ‘xxx’'));
test('lgn', () => should.Throw(function() { camb.lng = 'xxx'; }, TypeError, 'invalid lng ‘xxx’'));
test('longitude', () => should.Throw(function() { camb.longitude = 'xxx'; }, TypeError, 'invalid longitude ‘xxx’'));
});

@@ -104,3 +107,3 @@

test('n,6', () => btTower.toString('n', 6).should.equal('51.521470,-0.138833'));
test('bad fmt', () => should.Throw(function() { btTower.toString('x', 6); }, RangeError, 'Invalid format ‘x’'));
test('bad fmt', () => should.Throw(function() { btTower.toString('x', 6); }, RangeError, 'invalid format ‘x’'));
});

@@ -113,3 +116,3 @@

describe('geodesics', function() {
describe('dist/brng/dest', function() {
const cambg = new LatLon(52.205, 0.119), paris = new LatLon(48.857, 2.351);

@@ -120,17 +123,25 @@ test('distance', () => cambg.distanceTo(paris).toPrecision(4).should.equal('4.043e+5'));

test('final bearing', () => cambg.finalBearingTo(paris).toFixed(1).should.equal('157.9'));
test('initial brng coinc', () => cambg.initialBearingTo(cambg).should.be.NaN);
test('final brng coinc', () => cambg.finalBearingTo(cambg).should.be.NaN);
test('bearing (reverse)', () => paris.initialBearingTo(cambg).toFixed(1).should.equal('337.9'));
test('midpoint', () => cambg.midpointTo(paris).toString().should.equal('50.5363°N, 001.2746°E'));
test('int.point', () => cambg.intermediatePointTo(paris, 0.25).toString().should.equal('51.3721°N, 000.7073°E'));
test('destination', () => new LatLon(51.47788, -0.00147).destinationPoint(7794, 300.7).toString().should.equal('51.5136°N, 000.0983°W'));
test('int.point coinc', () => cambg.intermediatePointTo(cambg, 0.25).toString().should.equal('52.2050°N, 000.1190°E'));
test('int.pt', () => new LatLon(90, 0).intermediatePointTo(new LatLon(0, 90), 0.75).toString().should.equal('22.5000°N, 090.0000°E'));
test('int.pt chord', () => new LatLon(90, 0).intermediatePointOnChordTo(new LatLon(0, 90), 0.75).toString().should.equal('18.4349°N, 090.0000°E'));
const greenwich = new LatLon(51.47788, -0.00147), dist = 7794, brng = 300.7;
test('destination', () => greenwich.destinationPoint(dist, brng).toString().should.equal('51.5136°N, 000.0983°W'));
test('destination r', () => greenwich.destinationPoint(dist, brng, 6371e3).toString().should.equal('51.5136°N, 000.0983°W'));
});
describe('geodesics fails', function() {
describe('dist/brng/dest fails', function() {
const cambg = new LatLon(52.205, 0.119), paris = new LatLon(48.857, 2.351);
test('distance (fail p)', () => should.Throw(function() { cambg.distanceTo('paris'); }, TypeError, '‘point’ is not (NvectorSpherical) LatLon object'));
test('distance (fail r)', () => should.Throw(function() { cambg.distanceTo(paris, 'xxx'); }, TypeError, 'Radius is not a number'));
test('init brng (fail)', () => should.Throw(function() { cambg.initialBearingTo('paris'); }, TypeError, '‘point’ is not (NvectorSpherical) LatLon object'));
test('final brng (fail)', () => should.Throw(function() { cambg.finalBearingTo('paris'); }, TypeError, '‘point’ is not (NvectorSpherical) LatLon object'));
test('midpoint (fail)', () => should.Throw(function() { cambg.midpointTo('paris'); }, TypeError, '‘point’ is not (NvectorSpherical) LatLon object'));
test('int.point (fail)', () => should.Throw(function() { cambg.intermediatePointTo('paris', 0.5); }, TypeError, '‘point’ is not (NvectorSpherical) LatLon object'));
test('int.point.ch (fail)', () => should.Throw(function() { cambg.intermediatePointOnChordTo('paris', 0.5); }, TypeError, '‘point’ is not (NvectorSpherical) LatLon object'));
test('distance (fail p)', () => should.Throw(function() { cambg.distanceTo({}); }, TypeError, 'invalid point ‘[object Object]’'));
test('distance (fail p)', () => should.Throw(function() { cambg.distanceTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('distance (fail r)', () => should.Throw(function() { cambg.distanceTo(paris, 'xxx'); }, TypeError, 'invalid radius ‘xxx’'));
test('init brng (fail)', () => should.Throw(function() { cambg.initialBearingTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('final brng (fail)', () => should.Throw(function() { cambg.finalBearingTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('midpoint (fail)', () => should.Throw(function() { cambg.midpointTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('int.point (fail)', () => should.Throw(function() { cambg.intermediatePointTo('paris', 0.5); }, TypeError, 'invalid point ‘paris’'));
test('int.point.ch (fail)', () => should.Throw(function() { cambg.intermediatePointOnChordTo('paris', 0.5); }, TypeError, 'invalid point ‘paris’'));
});

@@ -160,19 +171,51 @@

test('coincident', () => LatLon.intersection(new LatLon(1, 1), N, new LatLon(1, 1), E).toString().should.equal('01.0000°N, 001.0000°E'));
const stn = new LatLon(51.8853, 0.2545), cdg = new LatLon(49.0034, 2.5735);
test('stn-cdg-bxl', () => LatLon.intersection(stn, 108.547, cdg, 32.435).toString().should.equal('50.9078°N, 004.5084°E'));
test('int’n (fail 1)', () => should.Throw(function() { LatLon.intersection(null, 'n', null, 's'); }, TypeError, '‘path1start’ is not LatLon object'));
test('int’n (fail 2)', () => should.Throw(function() { LatLon.intersection(stn, 'n', null, 's'); }, TypeError, '‘path2start’ is not LatLon object'));
test('int’n (fail 3)', () => should.Throw(function() { LatLon.intersection(stn, 'n', cdg, 's'); }, TypeError, '‘path1brngEnd’ is not LatLon object'));
test('int’n (fail 4)', () => should.Throw(function() { LatLon.intersection(stn, 'n', cdg, 's'); }, TypeError, '‘path1brngEnd’ is not LatLon object'));
test('int’n (fail 5)', () => should.Throw(function() { LatLon.intersection(stn, 0, cdg, 's'); }, TypeError, '‘path2brngEnd’ is not LatLon object'));
test('bad point 1', () => should.Throw(function() { LatLon.intersection(false, N, new LatLon(1, 0), E); }, TypeError, 'invalid path1start ‘false’'));
test('bad point 2', () => should.Throw(function() { LatLon.intersection(new LatLon(0, 1), N, false, E); }, TypeError, 'invalid path2start ‘false’'));
test('int’n (fail 1)', () => should.Throw(function() { LatLon.intersection(null, 'n', null, 's'); }, TypeError, 'invalid path1start ‘null’'));
test('int’n (fail 2)', () => should.Throw(function() { LatLon.intersection(stn, 'n', null, 's'); }, TypeError, 'invalid path2start ‘null’'));
test('int’n (fail 3)', () => should.Throw(function() { LatLon.intersection(stn, 'n', cdg, 's'); }, TypeError, 'invalid path1brngEnd ‘n’'));
test('int’n (fail 4)', () => should.Throw(function() { LatLon.intersection(stn, 0, cdg, 's'); }, TypeError, 'invalid path2brngEnd ‘s’'));
test('rounding errors', () => LatLon.intersection(new LatLon(51, 0), 120, new LatLon(50, 0), 60).toString().should.equal('50.4921°N, 001.3612°E'));
});
describe('cross-track', function() {
test('cross-track b', () => new LatLon(10, 0).crossTrackDistanceTo(new LatLon(0, 0), 90).toPrecision(4).should.equal('-1.112e+6'));
test('cross-track p', () => new LatLon(10, 1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+6'));
test('cross-track -', () => new LatLon(10, 0).crossTrackDistanceTo(new LatLon(0, 0), 270).toPrecision(4).should.equal('1.112e+6'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(0, 0).crossTrackDistanceTo(null, 0); }, TypeError, '‘pathStart’ is not (NvectorSpherical) LatLon object'));
describe('cross-track / along-track', function() {
test('cross-track end-p', () => new LatLon(10, 1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+6'));
test('cross-track brng', () => new LatLon(10, 0).crossTrackDistanceTo(new LatLon(0, 0), 90).toPrecision(4).should.equal('-1.112e+6'));
test('cross-track -', () => new LatLon(10, 0).crossTrackDistanceTo(new LatLon(0, 0), 270).toPrecision(4).should.equal('1.112e+6'));
const bradwell = new LatLon(53.3206, -1.7297), dunham = new LatLon(53.2611, -0.7972), partney = new LatLon(53.1887, 0.1334);
test('cross-track', () => dunham.crossTrackDistanceTo(bradwell, partney).toPrecision(4).should.equal('-307.5'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(10, 1).crossTrackDistanceTo(null, new LatLon(0, 2)); }, TypeError, 'invalid pathStart ‘null’'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(10, 1).crossTrackDistanceTo(new LatLon(0, 0), 'x'); }, TypeError, 'invalid pathBrngEnd ‘x’'));
test('along-track end-p', () => dunham.alongTrackDistanceTo(bradwell, partney).toPrecision(4).should.equal('6.233e+4'));
test('along-track brng', () => dunham.alongTrackDistanceTo(bradwell, 96.0).toPrecision(4).should.equal('6.233e+4'));
test('cross-track NE', () => new LatLon(1, 1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5'));
test('cross-track SE', () => new LatLon(-1, 1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5'));
test('cross-track SW?', () => new LatLon(-1, -1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5'));
test('cross-track NW?', () => new LatLon( 1, -1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5')); // eslint-disable-line space-in-parens
test('along-track NE', () => new LatLon( 1, 1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5')); // eslint-disable-line space-in-parens
test('along-track SE', () => new LatLon(-1, 1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5'));
test('along-track SW', () => new LatLon(-1, -1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5'));
test('along-track NW', () => new LatLon( 1, -1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5')); // eslint-disable-line space-in-parens
test('cross-track brng w-e', () => new LatLon(1, 0).crossTrackDistanceTo(new LatLon(0, 0), 90).toPrecision(4).should.equal('-1.112e+5'));
test('cross-track brng e-w', () => new LatLon(1, 0).crossTrackDistanceTo(new LatLon(0, 0), 270).toPrecision(4).should.equal('1.112e+5'));
test('cross-track coinc', () => new LatLon(10, 0).crossTrackDistanceTo(new LatLon(10, 0), 0).should.be.NaN);
test('cross-track (fail)', () => should.Throw(function() { new LatLon(0, 0).crossTrackDistanceTo(null, 0); }, TypeError, 'invalid pathStart ‘null’'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(0, 0).crossTrackDistanceTo(new LatLon(0, 0), 'x'); }, TypeError, 'invalid pathBrngEnd ‘x’'));
test('along-track (fail)', () => should.Throw(function() { new LatLon(0, 0).alongTrackDistanceTo(null, 0); }, TypeError, 'invalid pathStart ‘null’'));
test('along-track (fail)', () => should.Throw(function() { new LatLon(0, 0).alongTrackDistanceTo(new LatLon(0, 0), 'x'); }, TypeError, 'invalid pathBrngEnd ‘x’'));
});
describe('triangulate', function() {
// TODO
});
describe('trilaterate', function() { // http://gis.stackexchange.com/a/415/41129

@@ -183,6 +226,13 @@ const p1 = new LatLon(37.418436, -121.963477), d1 = 265.710701754;

test('gis.stackexchange', () => LatLon.trilaterate(p1, d1, p2, d2, p3, d3).toString('d', 6).should.equal('37.419078°N, 121.960579°W'));
test('coincident', () => should.equal(LatLon.trilaterate(p1, d1, p1, d2, p1, d3), null));
});
describe('area', function() {
const polyHemi = [ new LatLon(0, 1), new LatLon(45, 0), new LatLon(89, 90), new LatLon(45, 180), new LatLon(0, 179), new LatLon(-45, 180), new LatLon(-89, 90), new LatLon(-45, 0) ];
describe('area / enclosed (polygon-based)', function() {
const polyTriangle = [ new LatLon(1, 1), new LatLon(2, 1), new LatLon(1, 2) ];
const polySquareCw = [ new LatLon(1, 1), new LatLon(2, 1), new LatLon(2, 2), new LatLon(1, 2) ];
const polySquareCcw = [ new LatLon(1, 1), new LatLon(1, 2), new LatLon(2, 2), new LatLon(2, 1) ];
const polyOctant = [ new LatLon(0, ε), new LatLon(90, 0), new LatLon(0, 90-ε) ];
const polyOctantS = [ new LatLon(-ε, ε), new LatLon(90, 0), new LatLon(-ε, 90-ε) ];
// const polyQuadrant = [ new LatLon(ε, ε), new LatLon(90, ε), new LatLon(ε, 180-ε), new LatLon(ε, 90) ];
const polyHemiE = [ new LatLon(ε, ε), new LatLon(90-ε, 0), new LatLon(90-ε, 180), new LatLon(ε, 180), new LatLon(-ε, 180), new LatLon(-90+ε, 180), new LatLon(-90+ε, 0), new LatLon(-ε, ε) ];
const polyGc = [ new LatLon(10, 0), new LatLon(10, 90), new LatLon(0, 45) ];

@@ -192,4 +242,17 @@ const polyPole = [ new LatLon(89, 0), new LatLon(89, 120), new LatLon(89, -120) ];

const polyConcave = [ new LatLon(1, 1), new LatLon(5, 1), new LatLon(5, 3), new LatLon(1, 3), new LatLon(3, 2) ];
test('hemisphere enclosed y', () => new LatLon(22.5, 0.59).isEnclosedBy(polyHemi).should.be.true);
test('hemisphere enclosed n', () => new LatLon(22.5, 0.58).isEnclosedBy(polyHemi).should.be.false);
test('triangle area', () => LatLon.areaOf(polyTriangle).toFixed(0).should.equal('6181527888'));
test('triangle area radius', () => LatLon.areaOf(polyTriangle, 6371e3).toFixed(0).should.equal('6181527888'));
test('triangle area closed', () => LatLon.areaOf(polyTriangle.concat(polyTriangle[0])).toFixed(0).should.equal('6181527888'));
test('square cw area', () => LatLon.areaOf(polySquareCw).toFixed(0).should.equal('12360230987'));
test('square ccw area', () => LatLon.areaOf(polySquareCcw).toFixed(0).should.equal('12360230987'));
test('octant area', () => LatLon.areaOf(polyOctant).should.equal(π*R*R/2));
test('super-octant area', () => LatLon.areaOf(polyOctantS).should.equal(π*R*R/2));
// TODO: fails: test('quadrant area', () => LatLon.areaOf(polyQuadrant).should.equal(π*R*R));
test('hemisphere area', () => LatLon.areaOf(polyHemiE).toFixed(1).should.equal((2*π*R*R).toFixed(1)));
test('pole area', () => LatLon.areaOf(polyPole).toFixed(0).should.equal('16063139192'));
test('concave area', () => LatLon.areaOf(polyConcave).toFixed(0).should.equal('74042699236'));
test('hemisphere enclosed y', () => new LatLon(45, 1).isEnclosedBy(polyHemiE).should.be.true);
// TODO: fails: test('hemisphere enclosed n', () => new LatLon(45, -1).isEnclosedBy(polyHemiE).should.be.false);
test('gc enclosed y', () => new LatLon(14, 45).isEnclosedBy(polyGc).should.be.true);

@@ -201,10 +264,25 @@ test('gc enclosed n', () => new LatLon(15, 45).isEnclosedBy(polyGc).should.be.false);

test('concave enclosed n', () => new LatLon(2, 2).isEnclosedBy(polyConcave).should.be.false);
});
test('int.pt normal', () => new LatLon(90, 0).intermediatePointTo(new LatLon(0, 90), 0.75).toString().should.equal('22.5000°N, 090.0000°E'));
test('int.pt direct', () => new LatLon(90, 0).intermediatePointOnChordTo(new LatLon(0, 90), 0.75).toString().should.equal('18.4349°N, 090.0000°E'));
describe('Ed Williams', function() { // www.edwilliams.org/avform.htm
const lax = new LatLon(Dms.parse('33° 57′N'), Dms.parse('118° 24′W'));
const jfk = new LatLon(Dms.parse('40° 38′N'), Dms.parse('073° 47′W'));
const r = 180*60/π; // earth radius in nautical miles
test('distance nm', () => lax.distanceTo(jfk, r).toPrecision(4).should.equal('2144'));
test('bearing', () => lax.initialBearingTo(jfk).toPrecision(2).should.equal('66'));
test('intermediate', () => lax.intermediatePointTo(jfk, 100/2144).toString('dm', 0).should.equal('34°37′N, 116°33′W'));
const d = new LatLon(Dms.parse('34:30N'), Dms.parse('116:30W'));
test('cross-track', () => d.crossTrackDistanceTo(lax, jfk, r).toPrecision(5).should.equal('7.4523'));
test('along-track', () => d.alongTrackDistanceTo(lax, jfk, r).toPrecision(5).should.equal('99.588'));
test('intermediate', () => lax.intermediatePointTo(jfk, 0.4).toString('dm', 3).should.equal('38°40.167′N, 101°37.570′W'));
const reo = new LatLon(Dms.parse('42.600N'), Dms.parse('117.866W'));
const bke = new LatLon(Dms.parse('44.840N'), Dms.parse('117.806W'));
test('intersection', () => LatLon.intersection(reo, 51, bke, 137).toString('d', 3).should.equal('43.572°N, 116.189°W'));
});
describe('misc', function() {
test('equals (fail)', () => should.Throw(function() { new LatLon(52.205, 0.119).equals('cambg'); }, TypeError, '‘point’ is not (NvectorSpherical) LatLon object'));
test('equals true', () => new LatLon(52.205, 0.119).equals(new LatLon(52.205, 0.119)).should.be.true);
test('equals false', () => new LatLon(52.206, 0.119).equals(new LatLon(52.205, 0.119)).should.be.false);
test('equals (fail)', () => should.Throw(function() { new LatLon(52.205, 0.119).equals('cambg'); }, TypeError, 'invalid point ‘cambg’'));
test('toGeoJSON', () => new LatLon(52.205, 0.119).toGeoJSON().should.deep.equal({ type: 'Point', coordinates: [ 0.119, 52.205 ] }));
});

@@ -211,0 +289,0 @@ });

@@ -18,2 +18,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

const π = Math.PI;
const ε = Number.EPSILON;

@@ -23,12 +24,11 @@ Dms.separator = ''; // tests are easier without any DMS separator

describe('@examples', function() {
const cambg = '52.2050°N, 000.1190°E';
test('constructor', () => new LatLon(52.205, 0.119).toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p1', () => LatLon.parse(52.205, 0.119).toString().should.equal(cambg));
test('parse p2', () => LatLon.parse('52.205', '0.119').toString().should.equal(cambg));
test('parse p3', () => LatLon.parse('52.205, 0.119').toString().should.equal(cambg));
test('parse p4', () => LatLon.parse('52°12′18.0″N', '000°07′08.4″E').toString().should.equal(cambg));
test('parse p5', () => LatLon.parse('52°12′18.0″N, 000°07′08.4″E').toString().should.equal(cambg));
test('parse p6', () => LatLon.parse({ lat: 52.205, lon: 0.119 }).toString().should.equal(cambg));
test('parse p7', () => LatLon.parse({ lat: '52°12′18.0″N', lng: '000°07′08.4″E' }).toString().should.equal(cambg));
test('parse p8', () => LatLon.parse({ type: 'Point', coordinates: [ 0.119, 52.205 ] }).toString().should.equal(cambg));
test('parse p1', () => LatLon.parse(52.205, 0.119).toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p2', () => LatLon.parse('52.205', '0.119').toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p3', () => LatLon.parse('52.205, 0.119').toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p4', () => LatLon.parse('52°12′18.0″N', '000°07′08.4″E').toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p5', () => LatLon.parse('52°12′18.0″N, 000°07′08.4″E').toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p6', () => LatLon.parse({ lat: 52.205, lon: 0.119 }).toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p7', () => LatLon.parse({ lat: '52°12′18.0″N', lng: '000°07′08.4″E' }).toString().should.equal('52.2050°N, 000.1190°E'));
test('parse p8', () => LatLon.parse({ type: 'Point', coordinates: [ 0.119, 52.205 ] }).toString().should.equal('52.2050°N, 000.1190°E'));
test('distanceTo d', () => new LatLon(52.205, 0.119).distanceTo(new LatLon(48.857, 2.351)).toFixed().should.equal('404279'));

@@ -50,35 +50,25 @@ test('distanceTo m', () => new LatLon(52.205, 0.119).distanceTo(new LatLon(48.857, 2.351), 3959).toFixed(1).should.equal('251.2'));

test('equals', () => new LatLon(52.205, 0.119).equals(new LatLon(52.205, 0.119)).should.be.true);
test('toString 1', () => new LatLon(51.47788, -0.00147).toString().should.equal('51.4779°N, 000.0015°W'));
test('toString 2', () => new LatLon(51.47788, -0.00147).toString('dms', 2).should.equal('51°28′40.37″N, 000°00′05.29″W'));
test('toString 3', () => new LatLon(51.47788, -0.00147).toString('n').should.equal('51.4779,-0.0015'));
const greenwich = new LatLon(51.47788, -0.00147);
test('toString d', () => greenwich.toString().should.equal('51.4779°N, 000.0015°W'));
test('toString dms', () => greenwich.toString('dms', 2).should.equal('51°28′40.37″N, 000°00′05.29″W'));
test('toString lat,lon', () => greenwich.toString('n').split(',').should.deep.equal([ '51.4779', '-0.0015' ]));
});
describe('constructor fail', function() {
test('non-numeric lat fail', () => should.Throw(function() { new LatLon('x', 0, 0); }, TypeError, 'Invalid lat ‘x’'));
test('non-numeric lon fail', () => should.Throw(function() { new LatLon(0, 'x', 0); }, TypeError, 'Invalid lon ‘x’'));
test('non-numeric lat fail', () => should.Throw(function() { new LatLon('x', 0, 0); }, TypeError, 'invalid lat ‘x’'));
test('non-numeric lon fail', () => should.Throw(function() { new LatLon(0, 'x', 0); }, TypeError, 'invalid lon ‘x’'));
});
describe('parse fail', function() {
test('empty', () => should.Throw(function() { LatLon.parse(); }, TypeError, 'Invalid (empty) coordinate'));
test('single arg num', () => should.Throw(function() { LatLon.parse(1); }, TypeError, 'Invalid coordinate ‘1’'));
test('invalid comma arg', () => should.Throw(function() { LatLon.parse('cam', 'bridge'); }, TypeError, 'Invalid coordinate ‘cam,bridge’'));
test('single arg str', () => should.Throw(function() { LatLon.parse('cambridge'); }, TypeError, 'Invalid coordinate ‘cambridge’'));
test('invalid object 1', () => should.Throw(function() { LatLon.parse({ y: 51.47788, x: -0.00147 }); }, TypeError, 'Invalid coordinate ‘{"y":51.47788,"x":-0.00147}’'));
test('invalid object 2', () => should.Throw(function() { LatLon.parse({ lat: 'y', lon: 'x' }); }, TypeError, 'Invalid coordinate ‘{"lat":"y","lon":"x"}’'));
test('null 1', () => should.Throw(function() { LatLon.parse(null); }, TypeError, 'Invalid (null) coordinate'));
test('null 2', () => should.Throw(function() { LatLon.parse(null, null); }, TypeError, 'Invalid (null) coordinate'));
test('null 3', () => should.Throw(function() { LatLon.parse(1.23, null); }, TypeError, 'Invalid (null) coordinate'));
test('empty', () => should.Throw(function() { LatLon.parse(); }, TypeError, 'invalid (empty) point'));
test('single arg num', () => should.Throw(function() { LatLon.parse(1); }, TypeError, 'invalid point ‘1’'));
test('invalid comma arg', () => should.Throw(function() { LatLon.parse('cam', 'bridge'); }, TypeError, 'invalid point ‘cam,bridge’'));
test('single arg str', () => should.Throw(function() { LatLon.parse('cambridge'); }, TypeError, 'invalid point ‘cambridge’'));
test('invalid object 1', () => should.Throw(function() { LatLon.parse({ y: 51.47788, x: -0.00147 }); }, TypeError, 'invalid point ‘{"y":51.47788,"x":-0.00147}’'));
test('invalid object 2', () => should.Throw(function() { LatLon.parse({ lat: 'y', lon: 'x' }); }, TypeError, 'invalid point ‘{"lat":"y","lon":"x"}’'));
test('null 1', () => should.Throw(function() { LatLon.parse(null); }, TypeError, 'invalid (null) point'));
test('null 2', () => should.Throw(function() { LatLon.parse(null, null); }, TypeError, 'invalid (null) point'));
test('null 3', () => should.Throw(function() { LatLon.parse(1.23, null); }, TypeError, 'invalid (null) point'));
});
describe('toString', function() {
const btTower = new LatLon(51.521470, -0.138833);
test('default', () => btTower.toString().should.equal('51.5215°N, 000.1388°W'));
test('d,6', () => btTower.toString('d', 6).should.equal('51.521470°N, 000.138833°W'));
test('dm,4', () => btTower.toString('dm', 4).should.equal('51°31.2882′N, 000°08.3300′W'));
test('dms,2', () => btTower.toString('dms', 2).should.equal('51°31′17.29″N, 000°08′19.80″W'));
test('n', () => btTower.toString('n').should.equal('51.5215,-0.1388'));
test('n,6', () => btTower.toString('n', 6).should.equal('51.521470,-0.138833'));
test('bad fmt', () => should.Throw(function() { btTower.toString('x', 6); }, RangeError, 'Invalid format ‘x’'));
});
describe('getters/setters', function() {

@@ -107,10 +97,21 @@ const camb = new LatLon(0, 0);

const camb = new LatLon(0, 0);
test('lat', () => should.Throw(function() { camb.lat = 'xxx'; }, TypeError, 'Invalid lat ‘xxx’'));
test('latitude', () => should.Throw(function() { camb.latitude = 'xxx'; }, TypeError, 'Invalid latitude ‘xxx’'));
test('lon', () => should.Throw(function() { camb.lon = 'xxx'; }, TypeError, 'Invalid lon ‘xxx’'));
test('lgn', () => should.Throw(function() { camb.lng = 'xxx'; }, TypeError, 'Invalid lng ‘xxx’'));
test('longitude', () => should.Throw(function() { camb.longitude = 'xxx'; }, TypeError, 'Invalid longitude ‘xxx’'));
test('lat', () => should.Throw(function() { camb.lat = 'xxx'; }, TypeError, 'invalid lat ‘xxx’'));
test('latitude', () => should.Throw(function() { camb.latitude = 'xxx'; }, TypeError, 'invalid latitude ‘xxx’'));
test('lon', () => should.Throw(function() { camb.lon = 'xxx'; }, TypeError, 'invalid lon ‘xxx’'));
test('lgn', () => should.Throw(function() { camb.lng = 'xxx'; }, TypeError, 'invalid lng ‘xxx’'));
test('longitude', () => should.Throw(function() { camb.longitude = 'xxx'; }, TypeError, 'invalid longitude ‘xxx’'));
});
describe('alternate formats', function() {
describe('toString', function() {
const btTower = new LatLon(51.521470, -0.138833);
test('default', () => btTower.toString().should.equal('51.5215°N, 000.1388°W'));
test('d,6', () => btTower.toString('d', 6).should.equal('51.521470°N, 000.138833°W'));
test('dm,4', () => btTower.toString('dm', 4).should.equal('51°31.2882′N, 000°08.3300′W'));
test('dms,2', () => btTower.toString('dms', 2).should.equal('51°31′17.29″N, 000°08′19.80″W'));
test('n', () => btTower.toString('n').should.equal('51.5215,-0.1388'));
test('n,6', () => btTower.toString('n', 6).should.equal('51.521470,-0.138833'));
test('bad fmt', () => should.Throw(function() { btTower.toString('x', 6); }, RangeError, 'invalid format ‘x’'));
});
describe('alternate point formats', function() {
const cambg = LatLon.parse({ lat: 52.205, lon: 0.119 });

@@ -120,6 +121,11 @@ test('distance {lat,lon}', () => cambg.distanceTo({ lat: 48.857, lon: 2.351 }).toPrecision(4).should.equal('4.043e+5'));

test('distance {latitude,longitude}', () => cambg.distanceTo({ latitude: 48.857, longitude: 2.351 }).toPrecision(4).should.equal('4.043e+5'));
test('distance {x,y} fails', () => should.Throw(function() { cambg.distanceTo(({ x: 48.857, y: 2.351 })); }, TypeError, 'Invalid coordinate ‘{"x":48.857,"y":2.351}’'));
test('distance GeoJSON', () => cambg.distanceTo({ type: 'Point', coordinates: [ 2.351, 48.857 ] }).toPrecision(4).should.equal('4.043e+5'));
test('distance {x,y} fails', () => should.Throw(function() { cambg.distanceTo(({ x: 48.857, y: 2.351 })); }, TypeError, 'invalid point ‘{"x":48.857,"y":2.351}’'));
test('distance "d"', () => cambg.distanceTo('48.857, 2.351').toPrecision(4).should.equal('4.043e+5'));
test('distance "dms"', () => cambg.distanceTo('48°51′25.2″N, 002°21′03.6″E').toPrecision(4).should.equal('4.043e+5'));
test('object (fail)', () => should.Throw(function() { cambg.distanceTo({ x: 48.857, y: 2.351 }); }, TypeError, 'invalid point ‘{"x":48.857,"y":2.351}’'));
test('string (fail)', () => should.Throw(function() { cambg.distanceTo('paris'); }, TypeError, 'invalid point ‘paris’'));
});
describe('geodesics', function() {
describe('dist/brng/dest', function() {
const cambg = new LatLon(52.205, 0.119), paris = new LatLon(48.857, 2.351);

@@ -130,27 +136,83 @@ test('distance', () => cambg.distanceTo(paris).toPrecision(4).should.equal('4.043e+5'));

test('final bearing', () => cambg.finalBearingTo(paris).toFixed(1).should.equal('157.9'));
test('initial brng coinc', () => cambg.initialBearingTo(cambg).should.be.NaN);
test('final brng coinc', () => cambg.finalBearingTo(cambg).should.be.NaN);
test('bearing (reverse)', () => paris.initialBearingTo(cambg).toFixed(1).should.equal('337.9'));
test('midpoint', () => cambg.midpointTo(paris).toString().should.equal('50.5363°N, 001.2746°E'));
test('int.point', () => cambg.intermediatePointTo(paris, 0.25).toString().should.equal('51.3721°N, 000.7073°E'));
test('distance (fail)', () => should.Throw(function() { cambg.distanceTo('paris'); }, TypeError, 'Invalid coordinate ‘paris’'));
test('distance (fail)', () => should.Throw(function() { cambg.distanceTo(paris, 'xxx'); }, TypeError, 'Radius is not a number'));
test('init brng (fail)', () => should.Throw(function() { cambg.initialBearingTo('paris'); }, TypeError, 'Invalid coordinate ‘paris’'));
test('final brng (fail)', () => should.Throw(function() { cambg.finalBearingTo('paris'); }, TypeError, 'Invalid coordinate ‘paris’'));
test('midpoint (fail)', () => should.Throw(function() { cambg.midpointTo('paris'); }, TypeError, 'Invalid coordinate ‘paris’'));
test('int.point (fail)', () => should.Throw(function() { cambg.intermediatePointTo('paris', 0.5); }, TypeError, 'Invalid coordinate ‘paris’'));
test('int.point coinc', () => cambg.intermediatePointTo(cambg, 0.25).toString().should.equal('52.2050°N, 000.1190°E'));
const greenwich = new LatLon(51.47788, -0.00147), dist = 7794, brng = 300.7;
test('dest’n', () => greenwich.destinationPoint(dist, brng).toString().should.equal('51.5136°N, 000.0983°W'));
test('dest’n r', () => greenwich.destinationPoint(dist, brng, 6371e3).toString().should.equal('51.5136°N, 000.0983°W'));
test('destination', () => greenwich.destinationPoint(dist, brng).toString().should.equal('51.5136°N, 000.0983°W'));
test('destination r', () => greenwich.destinationPoint(dist, brng, 6371e3).toString().should.equal('51.5136°N, 000.0983°W'));
});
describe('dist/brng/dest fails', function() {
const cambg = new LatLon(52.205, 0.119), paris = new LatLon(48.857, 2.351);
test('distance (fail p)', () => should.Throw(function() { cambg.distanceTo({}); }, TypeError, 'invalid point ‘{}’'));
test('distance (fail p)', () => should.Throw(function() { cambg.distanceTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('distance (fail r)', () => should.Throw(function() { cambg.distanceTo(paris, 'xxx'); }, TypeError, 'invalid radius ‘xxx’'));
test('init brng (fail)', () => should.Throw(function() { cambg.initialBearingTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('final brng (fail)', () => should.Throw(function() { cambg.finalBearingTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('midpoint (fail)', () => should.Throw(function() { cambg.midpointTo('paris'); }, TypeError, 'invalid point ‘paris’'));
test('int.point (fail)', () => should.Throw(function() { cambg.intermediatePointTo('paris', 0.5); }, TypeError, 'invalid point ‘paris’'));
});
describe('intersection', function() {
const N = 0, E = 90, S = 180, W = 270;
test('toward 1,1 N,E nearest', () => LatLon.intersection(new LatLon(0, 1), N, new LatLon(1, 0), E).toString().should.equal('00.9998°N, 001.0000°E'));
test('toward 1,1 E,N nearest', () => LatLon.intersection(new LatLon(1, 0), E, new LatLon(0, 1), N).toString().should.equal('00.9998°N, 001.0000°E'));
test('toward 1,1 N,E antipodal', () => should.equal(LatLon.intersection(new LatLon(2, 1), N, new LatLon(1, 0), E), null));
test('toward/away 1,1 N,W antipodal', () => should.equal(LatLon.intersection(new LatLon(0, 1), N, new LatLon(1, 0), W), null));
test('toward/away 1,1 W,N antipodal', () => should.equal(LatLon.intersection(new LatLon(1, 0), W, new LatLon(0, 1), N), null));
test('toward/away 1,1 S,E antipodal', () => should.equal(LatLon.intersection(new LatLon(0, 1), S, new LatLon(1, 0), E), null));
test('toward/away 1,1 E,S antipodal', () => should.equal(LatLon.intersection(new LatLon(1, 0), E, new LatLon(0, 1), S), null));
test('away 1,1 S,W antipodal', () => LatLon.intersection(new LatLon(0, 1), S, new LatLon(1, 0), W).toString().should.equal('00.9998°S, 179.0000°W'));
test('away 1,1 W,S antipodal', () => LatLon.intersection(new LatLon(1, 0), W, new LatLon(0, 1), S).toString().should.equal('00.9998°S, 179.0000°W'));
test('1E/90E N,E antipodal', () => should.equal(LatLon.intersection(new LatLon(0, 1), N, new LatLon(1, 90), E), null));
test('1E/90E N,E nearest', () => LatLon.intersection(new LatLon(0, 1), N, new LatLon(1, 92), E).toString().should.equal('00.0175°N, 179.0000°W'));
test('brng+end 1a', () => should.Throw(function() { LatLon.intersection(new LatLon(1, 0), new LatLon(1, 3), new LatLon(2, 2), S); }, TypeError, 'invalid brng1 ‘01.0000°N, 003.0000°E’'));
test('brng+end 1b', () => should.Throw(function() { LatLon.intersection(new LatLon(2, 2), S, new LatLon(1, 0), new LatLon(1, 3)); }, TypeError, 'invalid brng2 ‘01.0000°N, 003.0000°E’'));
test('coincident', () => LatLon.intersection(new LatLon(1, 1), N, new LatLon(1, 1), E).toString().should.equal('01.0000°N, 001.0000°E'));
const stn = new LatLon(51.8853, 0.2545), cdg = new LatLon(49.0034, 2.5735);
test('intersec’n', () => LatLon.intersection(stn, 108.547, cdg, 32.435).toString().should.equal('50.9078°N, 004.5084°E'));
test('stn-cdg-bxl', () => LatLon.intersection(stn, 108.547, cdg, 32.435).toString().should.equal('50.9078°N, 004.5084°E'));
const bradwell = new LatLon(53.3206, -1.7297);
test('cross-track', () => new LatLon(53.2611, -0.7972).crossTrackDistanceTo(bradwell, new LatLon(53.1887, 0.1334)).toPrecision(4).should.equal('-307.5'));
test('bad point 1', () => should.Throw(function() { LatLon.intersection(false, N, new LatLon(1, 0), E); }, TypeError, 'invalid point ‘false’'));
test('bad point 2', () => should.Throw(function() { LatLon.intersection(new LatLon(0, 1), N, false, E); }, TypeError, 'invalid point ‘false’'));
test('int’n (fail 1)', () => should.Throw(function() { LatLon.intersection(null, 'n', null, 's'); }, TypeError, 'invalid (null) point'));
test('int’n (fail 2)', () => should.Throw(function() { LatLon.intersection(stn, 'n', null, 's'); }, TypeError, 'invalid (null) point'));
test('int’n (fail 3)', () => should.Throw(function() { LatLon.intersection(stn, 'n', cdg, 's'); }, TypeError, 'invalid brng1 ‘n’'));
test('int’n (fail 4)', () => should.Throw(function() { LatLon.intersection(stn, 0, cdg, 's'); }, TypeError, 'invalid brng2 ‘s’'));
test('rounding errors', () => LatLon.intersection(new LatLon(51, 0), 120, new LatLon(50, 0), 60).toString().should.equal('50.4921°N, 001.3612°E'));
});
describe('cross-track / along-track', function() {
test('cross-track p', () => new LatLon(10, 1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+6'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(10, 1).crossTrackDistanceTo(null, new LatLon(0, 2)); }, TypeError, 'Invalid (null) coordinate'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(10, 1).crossTrackDistanceTo(new LatLon(0, 0), null); }, TypeError, 'Invalid (null) coordinate'));
test('along-track', () => new LatLon(53.2611, -0.7972).alongTrackDistanceTo(bradwell, new LatLon(53.1887, 0.1334)).toPrecision(4).should.equal('6.233e+4'));
test('along-track', () => new LatLon(53.2611, -0.7972).alongTrackDistanceTo('53.3206, -1.7297', '53.1887, 0.1334').toPrecision(4).should.equal('6.233e+4'));
const bradwell = new LatLon(53.3206, -1.7297), dunham = new LatLon(53.2611, -0.7972), partney = new LatLon(53.1887, 0.1334);
test('cross-track', () => dunham.crossTrackDistanceTo(bradwell, partney).toPrecision(4).should.equal('-307.5'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(10, 1).crossTrackDistanceTo(null, new LatLon(0, 2)); }, TypeError, 'invalid (null) point'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(10, 1).crossTrackDistanceTo(new LatLon(0, 0), null); }, TypeError, 'invalid (null) point'));
test('along-track', () => dunham.alongTrackDistanceTo(bradwell, partney).toPrecision(4).should.equal('6.233e+4'));
test('cross-track NE', () => new LatLon(1, 1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5'));
test('cross-track SE', () => new LatLon(-1, 1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5'));
test('cross-track SW?', () => new LatLon(-1, -1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5'));
test('cross-track NW?', () => new LatLon( 1, -1).crossTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5')); // eslint-disable-line space-in-parens
test('along-track NE', () => new LatLon( 1, 1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5')); // eslint-disable-line space-in-parens
test('along-track SE', () => new LatLon(-1, 1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('1.112e+5'));
test('along-track SW', () => new LatLon(-1, -1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5'));
test('along-track NW', () => new LatLon( 1, -1).alongTrackDistanceTo(new LatLon(0, 0), new LatLon(0, 2)).toPrecision(4).should.equal('-1.112e+5')); // eslint-disable-line space-in-parens
test('cross-track coinc', () => new LatLon(10, 0).crossTrackDistanceTo(new LatLon(10, 0), new LatLon(0, 2)).should.be.NaN);
test('cross-track (fail)', () => should.Throw(function() { new LatLon(0, 0).crossTrackDistanceTo(null, 0); }, TypeError, 'invalid (null) point'));
test('cross-track (fail)', () => should.Throw(function() { new LatLon(0, 0).crossTrackDistanceTo(new LatLon(0, 0), 'x'); }, TypeError, 'invalid point ‘x’'));
test('along-track (fail)', () => should.Throw(function() { new LatLon(0, 0).alongTrackDistanceTo(null, 0); }, TypeError, 'invalid (null) point'));
test('along-track (fail)', () => should.Throw(function() { new LatLon(0, 0).alongTrackDistanceTo(new LatLon(0, 0), 'x'); }, TypeError, 'invalid point ‘x’'));
});
describe('misc', function() {
test('Clairaut 0°', () => new LatLon(0, 0).maxLatitude(0).should.equal(90));

@@ -164,4 +226,29 @@ test('Clairaut 1°', () => new LatLon(0, 0).maxLatitude(1).should.equal(89));

test('parallels -', () => should.equal(LatLon.crossingParallels(new LatLon(0, 0), new LatLon(10, 60), 60), null));
test('parallels coinc', () => should.equal(LatLon.crossingParallels(new LatLon(0, 0), new LatLon(0, 0), 0), null));
});
describe('area (polygon-based)', function() {
const polyTriangle = [ new LatLon(1, 1), new LatLon(2, 1), new LatLon(1, 2) ];
const polySquareCw = [ new LatLon(1, 1), new LatLon(2, 1), new LatLon(2, 2), new LatLon(1, 2) ];
const polySquareCcw = [ new LatLon(1, 1), new LatLon(1, 2), new LatLon(2, 2), new LatLon(2, 1) ];
const polyOctant = [ new LatLon(0, ε), new LatLon(90, 0), new LatLon(0, 90-ε) ];
const polyOctantS = [ new LatLon(-ε, ε), new LatLon(90, 0), new LatLon(-ε, 90-ε) ];
const polyQuadrant = [ new LatLon(ε, ε), new LatLon(90, ε), new LatLon(ε, 180-ε), new LatLon(ε, 90) ];
const polyHemiE = [ new LatLon(ε, ε), new LatLon(90-ε, 0), new LatLon(90-ε, 180), new LatLon(ε, 180), new LatLon(-ε, 180), new LatLon(-90+ε, 180), new LatLon(-90+ε, 0), new LatLon(-ε, ε) ];
const polyPole = [ new LatLon(89, 0), new LatLon(89, 120), new LatLon(89, -120) ];
const polyConcave = [ new LatLon(1, 1), new LatLon(5, 1), new LatLon(5, 3), new LatLon(1, 3), new LatLon(3, 2) ];
test('triangle area', () => LatLon.areaOf(polyTriangle).toFixed(0).should.equal('6181527888'));
test('triangle area radius', () => LatLon.areaOf(polyTriangle, 6371e3).toFixed(0).should.equal('6181527888'));
test('triangle area closed', () => LatLon.areaOf(polyTriangle.concat(polyTriangle[0])).toFixed(0).should.equal('6181527888'));
test('square cw area', () => LatLon.areaOf(polySquareCw).toFixed(0).should.equal('12360230987'));
test('square ccw area', () => LatLon.areaOf(polySquareCcw).toFixed(0).should.equal('12360230987'));
test('octant area', () => LatLon.areaOf(polyOctant).toFixed(1).should.equal((π*R*R/2).toFixed(1)));
test('super-octant area', () => LatLon.areaOf(polyOctantS).toFixed(1).should.equal((π*R*R/2).toFixed(1)));
test('quadrant area', () => LatLon.areaOf(polyQuadrant).should.equal(π*R*R));
test('hemisphere area', () => LatLon.areaOf(polyHemiE).toFixed(1).should.equal((2*π*R*R).toFixed(1)));
test('pole area', () => LatLon.areaOf(polyPole).toFixed(0).should.equal('16063139192'));
test('concave area', () => LatLon.areaOf(polyConcave).toFixed(0).should.equal('74042699236'));
});
describe('Ed Williams', function() { // www.edwilliams.org/avform.htm

@@ -183,39 +270,2 @@ const lax = new LatLon(Dms.parse('33° 57′N'), Dms.parse('118° 24′W'));

describe('intersections', function() {
const N = 0, E = 90, S = 180, W = 270;
test('toward 1,1 N,E nearest', () => LatLon.intersection(new LatLon(0, 1), N, new LatLon(1, 0), E).toString('d').should.equal('00.9998°N, 001.0000°E'));
test('toward 1,1 E,N nearest', () => LatLon.intersection(new LatLon(1, 0), E, new LatLon(0, 1), N).toString('d').should.equal('00.9998°N, 001.0000°E'));
test('away 1,1 S,W antipodal', () => LatLon.intersection(new LatLon(0, 1), S, new LatLon(1, 0), W).toString('d').should.equal('00.9998°S, 179.0000°W'));
test('away 1,1 W,S antipodal', () => LatLon.intersection(new LatLon(1, 0), W, new LatLon(0, 1), S).toString('d').should.equal('00.9998°S, 179.0000°W'));
test('1E/90E N,E nearest', () => LatLon.intersection(new LatLon(0, 1), N, new LatLon(1, 92), E).toString('d').should.equal('00.0175°N, 179.0000°W'));
const stn = new LatLon(51.8853, 0.2545), cdg = new LatLon(49.0034, 2.5735);
test('stn-cdg-bxl', () => LatLon.intersection(stn, 108.547, cdg, 32.435).toString('d').should.equal('50.9078°N, 004.5084°E'));
test('bad point 1', () => should.Throw(function() { LatLon.intersection(false, N, new LatLon(1, 0), E); }, TypeError, 'Invalid coordinate ‘false’'));
test('bad point 2', () => should.Throw(function() { LatLon.intersection(new LatLon(0, 1), N, false, E); }, TypeError, 'Invalid coordinate ‘false’'));
test('coincident points', () => should.equal(LatLon.intersection(new LatLon(0, 1), N, new LatLon(0, 1), E), null));
test('rounding errors', () => LatLon.intersection(new LatLon(51, 0), 120, new LatLon(50, 0), 60).toString('d').should.equal('50.4921°N, 001.3612°E'));
});
describe('area', function() {
const polyTriangle = [ new LatLon(1, 1), new LatLon(2, 1), new LatLon(1, 2) ];
const polySquareCw = [ new LatLon(1, 1), new LatLon(2, 1), new LatLon(2, 2), new LatLon(1, 2) ];
const polySquareCcw = [ new LatLon(1, 1), new LatLon(1, 2), new LatLon(2, 2), new LatLon(2, 1) ];
const polyQuadrant = [ new LatLon(0, 1e-99), new LatLon(0, 180), new LatLon(90, 0) ];
const polyHemi = [ new LatLon(0, 1), new LatLon(45, 0), new LatLon(89, 90), new LatLon(45, 180), new LatLon(0, 179), new LatLon(-45, 180), new LatLon(-89, 90), new LatLon(-45, 0) ];
const polyPole = [ new LatLon(89, 0), new LatLon(89, 120), new LatLon(89, -120) ];
const polyConcave = [ new LatLon(1, 1), new LatLon(5, 1), new LatLon(5, 3), new LatLon(1, 3), new LatLon(3, 2) ];
test('triangle area', () => LatLon.areaOf(polyTriangle).toFixed(0).should.equal('6181527888'));
test('triangle area radius', () => LatLon.areaOf(polyTriangle, 6371e3).toFixed(0).should.equal('6181527888'));
test('triangle area closed', () => LatLon.areaOf(polyTriangle.concat(polyTriangle[0])).toFixed(0).should.equal('6181527888'));
test('square cw area', () => LatLon.areaOf(polySquareCw).toFixed(0).should.equal('12360230987'));
test('square ccw area', () => LatLon.areaOf(polySquareCcw).toFixed(0).should.equal('12360230987'));
test('quadrant area', () => LatLon.areaOf(polyQuadrant).should.equal(π*R*R));
test('hemisphere area', () => LatLon.areaOf(polyHemi).toFixed(0).should.equal('252684679676459'));
test('pole area', () => LatLon.areaOf(polyPole).toFixed(0).should.equal('16063139192'));
test('concave area', () => LatLon.areaOf(polyConcave).toFixed(0).should.equal('74042699236'));
});
describe('rhumb lines', function() {

@@ -226,32 +276,25 @@ const dov = new LatLon(51.127, 1.338), cal = new LatLon(50.964, 1.853);

test('distance dateline E-W', () => new LatLon(1, -179).rhumbDistanceTo(new LatLon(1, 179)).toFixed(6).should.equal(new LatLon(1, 1).rhumbDistanceTo(new LatLon(1, -1)).toFixed(6)));
test('distance err', () => should.Throw(function() { dov.rhumbDistanceTo(false); }, TypeError, 'Invalid coordinate ‘false’'));
test('distance err', () => should.Throw(function() { dov.rhumbDistanceTo(false); }, TypeError, 'invalid point ‘false’'));
test('bearing', () => dov.rhumbBearingTo(cal).toFixed(1).should.equal('116.7'));
test('bearing dateline', () => new LatLon(1, -179).rhumbBearingTo(new LatLon(1, 179)).should.equal(270));
test('bearing dateline', () => new LatLon(1, 179).rhumbBearingTo(new LatLon(1, -179)).should.equal(90));
test('bearing err', () => should.Throw(function() { dov.rhumbBearingTo(false); }, TypeError, 'Invalid coordinate ‘false’'));
test('dest’n', () => dov.rhumbDestinationPoint(40310, 116.7).toString('d').should.equal('50.9641°N, 001.8531°E'));
test('dest’n', () => dov.rhumbDestinationPoint(40310, 116.7, 6371e3).toString('d').should.equal('50.9641°N, 001.8531°E'));
test('dest’n', () => new LatLon(1, 1).rhumbDestinationPoint(111178, 90).toString('d').should.equal('01.0000°N, 002.0000°E'));
test('dest’n dateline', () => new LatLon(1, 179).rhumbDestinationPoint(222356, 90).toString('d').should.equal('01.0000°N, 179.0000°W'));
test('dest’n dateline', () => new LatLon(1, -179).rhumbDestinationPoint(222356, 270).toString('d').should.equal('01.0000°N, 179.0000°E'));
test('midpoint', () => dov.rhumbMidpointTo(cal).toString('d').should.equal('51.0455°N, 001.5957°E'));
test('midpoint dateline', () => new LatLon(1, -179).rhumbMidpointTo(new LatLon(1, 178)).toString('d').should.equal('01.0000°N, 179.5000°E'));
test('midpoint err', () => should.Throw(function() { dov.rhumbMidpointTo(false); }, TypeError, 'Invalid coordinate ‘false’'));
test('bearing coincident', () => dov.rhumbBearingTo(dov).should.be.NaN);
test('bearing err', () => should.Throw(function() { dov.rhumbBearingTo(false); }, TypeError, 'invalid point ‘false’'));
test('dest’n', () => dov.rhumbDestinationPoint(40310, 116.7).toString().should.equal('50.9641°N, 001.8531°E'));
test('dest’n', () => dov.rhumbDestinationPoint(40310, 116.7, 6371e3).toString().should.equal('50.9641°N, 001.8531°E'));
test('dest’n', () => new LatLon(1, 1).rhumbDestinationPoint(111178, 90).toString().should.equal('01.0000°N, 002.0000°E'));
test('dest’n dateline', () => new LatLon(1, 179).rhumbDestinationPoint(222356, 90).toString().should.equal('01.0000°N, 179.0000°W'));
test('dest’n dateline', () => new LatLon(1, -179).rhumbDestinationPoint(222356, 270).toString().should.equal('01.0000°N, 179.0000°E'));
test('midpoint', () => dov.rhumbMidpointTo(cal).toString().should.equal('51.0455°N, 001.5957°E'));
test('midpoint dateline', () => new LatLon(1, -179).rhumbMidpointTo(new LatLon(1, 178)).toString().should.equal('01.0000°N, 179.5000°E'));
test('midpoint err', () => should.Throw(function() { dov.rhumbMidpointTo(false); }, TypeError, 'invalid point ‘false’'));
});
describe('point param as literal', function() { // is this being over-flexible?
const cambg = new LatLon(52.205, 0.119);
test('string', () => cambg.distanceTo('48.857, 2.351').toPrecision(4).should.equal('4.043e+5'));
test('object', () => cambg.distanceTo({ lat: 48.857, lon: 2.351 }).toPrecision(4).should.equal('4.043e+5'));
test('string (fail)', () => should.Throw(function() { cambg.distanceTo('paris'); }, TypeError, 'Invalid coordinate ‘paris’'));
test('object (fail)', () => should.Throw(function() { cambg.distanceTo({ x: 48.857, y: 2.351 }); }, TypeError, 'Invalid coordinate ‘{"x":48.857,"y":2.351}’'));
});
describe('misc', function() {
test('equals true', () => new LatLon(52.205, 0.119).equals(new LatLon(52.205, 0.119)).should.be.true);
test('equals false', () => new LatLon(52.206, 0.119).equals(new LatLon(52.205, 0.119)).should.be.false);
test('equals error', () => should.Throw(function() { new LatLon(52.206, 0.119).equals(false); }, TypeError, 'Invalid coordinate ‘false’'));
test('toGeoJSON', () => new LatLon(52.205, 0.119).toGeoJSON().should.deep.equal({ type: 'Point', coordinates: [ 0.119, 52.205 ] }));
test('equals true', () => new LatLon(52.205, 0.119).equals(new LatLon(52.205, 0.119)).should.be.true);
test('equals false', () => new LatLon(52.206, 0.119).equals(new LatLon(52.205, 0.119)).should.be.false);
test('equals (fail)', () => should.Throw(function() { new LatLon(52.205, 0.119).equals('cambg'); }, TypeError, 'invalid point ‘cambg’'));
test('toGeoJSON', () => new LatLon(52.205, 0.119).toGeoJSON().should.deep.equal({ type: 'Point', coordinates: [ 0.119, 52.205 ] }));
});
});

@@ -32,15 +32,15 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

describe('constructor fail', function() {
test('Invalid northing', () => should.Throw(function() { new OsGridRef(0, 1301e3); }, Error, 'Invalid northing ‘1301000’'));
test('Invalid easting', () => should.Throw(function() { new OsGridRef(701e3, 0); }, Error, 'Invalid easting ‘701000’'));
test('texts', () => should.Throw(function() { new OsGridRef('e', 'n'); }, Error, 'Invalid easting ‘e’'));
test('Invalid northing', () => should.Throw(function() { new OsGridRef(0, 1301e3); }, Error, 'invalid northing ‘1301000’'));
test('Invalid easting', () => should.Throw(function() { new OsGridRef(701e3, 0); }, Error, 'invalid easting ‘701000’'));
test('texts', () => should.Throw(function() { new OsGridRef('e', 'n'); }, Error, 'invalid easting ‘e’'));
});
describe('parse fail', function() {
test('text', () => should.Throw(function() { OsGridRef.parse('Cambridge'); }, Error, 'Invalid grid reference ‘Cambridge’'));
test('outside range', () => should.Throw(function() { OsGridRef.parse('AA 1 2'); }, Error, 'Invalid grid reference ‘AA 1 2’'));
test('unbalanced numerics', () => should.Throw(function() { OsGridRef.parse('SV 1 20'); }, Error, 'Invalid grid reference ‘SV 1 20’'));
test('text', () => should.Throw(function() { OsGridRef.parse('Cambridge'); }, Error, 'invalid grid reference ‘Cambridge’'));
test('outside range', () => should.Throw(function() { OsGridRef.parse('AA 1 2'); }, Error, 'invalid grid reference ‘AA 1 2’'));
test('unbalanced numerics', () => should.Throw(function() { OsGridRef.parse('SV 1 20'); }, Error, 'invalid grid reference ‘SV 1 20’'));
});
describe('toString fail', function() {
test('1bad precision', () => should.Throw(function() { new OsGridRef(651409, 313177).toString(20); }, Error, 'Invalid precision ‘20’'));
test('1bad precision', () => should.Throw(function() { new OsGridRef(651409, 313177).toString(20); }, Error, 'invalid precision ‘20’'));
});

@@ -47,0 +47,0 @@

@@ -26,2 +26,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

test('toString', () => new Utm('31', 'N', 448251, 5411932).toString(4).should.equal('31 N 448251.0000 5411932.0000'));
test('README', () => Utm.parse('48 N 377298.745 1483034.794').toLatLon().toUtm().toString(3).should.equal('48 N 377298.745 1483034.794'));
});

@@ -31,3 +32,3 @@

test('constructor', () => new Mgrs(31, 'U', 'D', 'Q', 48251, 11932).toString().should.equal('31U DQ 48251 11932'));
test('toUtm', () => new Mgrs(31, 'U', 'D', 'Q', 48251, 11932).toUtm().toString().should.equal('31 N 448251 5411932'));
test('toUtm', () => Mgrs.parse('31U DQ 48251 11932').toUtm().toString().should.equal('31 N 448251 5411932'));
test('parse', () => Mgrs.parse('31U DQ 48251 11932').toString().should.equal('31U DQ 48251 11932'));

@@ -42,5 +43,8 @@ test('parse military-style', () => Mgrs.parse('31UDQ4825111932').toString().should.equal('31U DQ 48251 11932'));

describe('UTM constructor fail', function() {
test('zone fail', () => should.Throw(function() { new Utm(0, 'N', 0, 0); }, RangeError, 'Invalid UTM zone ‘0’'));
test('zone fail', () => should.Throw(function() { new Utm(61, 'N', 0, 0); }, RangeError, 'Invalid UTM zone ‘61’'));
test('hemisphere fail', () => should.Throw(function() { new Utm(1, 'E', 0, 0); }, RangeError, 'Invalid UTM hemisphere ‘E’'));
test('zone fail', () => should.Throw(function() { new Utm(0, 'N', 0, 0); }, RangeError, 'invalid UTM zone ‘0’'));
test('zone fail', () => should.Throw(function() { new Utm(61, 'N', 0, 0); }, RangeError, 'invalid UTM zone ‘61’'));
test('hemisphere fail', () => should.Throw(function() { new Utm(1, 'E', 0, 0); }, RangeError, 'invalid UTM hemisphere ‘E’'));
test('easting fail', () => should.Throw(function() { new Utm(1, 'N', 1001e3, 0); }, RangeError, 'invalid UTM easting ‘1001000’'));
test('northing N fail', () => should.Throw(function() { new Utm(1, 'N', 0, 9329e3); }, RangeError, 'invalid UTM northing ‘9329000’'));
test('northing S fail', () => should.Throw(function() { new Utm(1, 'S', 0, 1118e3); }, RangeError, 'invalid UTM northing ‘1118000’'));
});

@@ -50,24 +54,24 @@

test('bad zone', () => should.Throw(function() { new Mgrs(0, 'C', 'A', 'A', 0, 0); }, RangeError, 'MGRS zone ‘0’ out of range'));
test('bad band', () => should.Throw(function() { new Mgrs(1, 'A', 'A', 'A', 0, 0); }, RangeError, 'Invalid MGRS band ‘A’'));
test('bad grid sq easting', () => should.Throw(function() { new Mgrs(1, 'C', 'I', 'A', 0, 0); }, RangeError, 'Invalid MGRS 100km grid square column ‘I’ for zone 1'));
test('bad grid sq northing', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'I', 0, 0); }, RangeError, 'Invalid MGRS 100km grid square row ‘I’'));
test('invalid grid sq e', () => should.Throw(function() { new Mgrs(2, 'C', 'A', 'A', 0, 0); }, RangeError, 'Invalid MGRS 100km grid square column ‘A’ for zone 2'));
test('bad easting', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 'x', 0); }, RangeError, 'Invalid MGRS easting ‘x’'));
test('bad northing', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 0, 'x'); }, RangeError, 'Invalid MGRS northing ‘x’'));
test('too far north', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 0, 'x'); }, RangeError, 'Invalid MGRS northing ‘x’'));
test('bad multiples', () => should.Throw(function() { new Mgrs(1, 'A', 'A', 'I', 0, 0); }, RangeError, 'Invalid MGRS band ‘A’, Invalid MGRS 100km grid square row ‘I’'));
test('bad band', () => should.Throw(function() { new Mgrs(1, 'A', 'A', 'A', 0, 0); }, RangeError, 'invalid MGRS band ‘A’'));
test('bad grid sq easting', () => should.Throw(function() { new Mgrs(1, 'C', 'I', 'A', 0, 0); }, RangeError, 'invalid MGRS 100km grid square column ‘I’ for zone 1'));
test('bad grid sq northing', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'I', 0, 0); }, RangeError, 'invalid MGRS 100km grid square row ‘I’'));
test('invalid grid sq e', () => should.Throw(function() { new Mgrs(2, 'C', 'A', 'A', 0, 0); }, RangeError, 'invalid MGRS 100km grid square column ‘A’ for zone 2'));
test('bad easting', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 'x', 0); }, RangeError, 'invalid MGRS easting ‘x’'));
test('bad northing', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 0, 'x'); }, RangeError, 'invalid MGRS northing ‘x’'));
test('too far north', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 0, 'x'); }, RangeError, 'invalid MGRS northing ‘x’'));
test('bad multiples', () => should.Throw(function() { new Mgrs(1, 'A', 'A', 'I', 0, 0); }, RangeError, 'invalid MGRS band ‘A’, invalid MGRS 100km grid square row ‘I’'));
});
describe('UTM parse', function() {
test('parse fail', () => should.Throw(function() { Utm.parse('Cambridge'); }, Error, 'Invalid UTM coordinate ‘Cambridge’'));
test('parse fail', () => should.Throw(function() { Utm.parse('Cambridge'); }, Error, 'invalid UTM coordinate ‘Cambridge’'));
});
describe('toString', function() {
test('toString fail', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 0, 0).toString(3); }, Error, 'Invalid precision ‘3’'));
test('toString fail', () => should.Throw(function() { new Mgrs(1, 'C', 'A', 'A', 0, 0).toString(3); }, Error, 'invalid precision ‘3’'));
});
describe('MGRS parse', function() {
test('parse fail 1', () => should.Throw(function() { Mgrs.parse(null); }, Error, 'Invalid MGRS grid reference ‘null’'));
test('parse fail 2', () => should.Throw(function() { Mgrs.parse('Cambridge'); }, Error, 'Invalid MGRS grid reference ‘Cambridge’'));
test('parse fail 3', () => should.Throw(function() { Mgrs.parse('New York'); }, Error, 'Invalid MGRS grid reference ‘New York’'));
test('parse fail 1', () => should.Throw(function() { Mgrs.parse(null); }, Error, 'invalid MGRS grid reference ‘null’'));
test('parse fail 2', () => should.Throw(function() { Mgrs.parse('Cambridge'); }, Error, 'invalid MGRS grid reference ‘Cambridge’'));
test('parse fail 3', () => should.Throw(function() { Mgrs.parse('New York'); }, Error, 'invalid MGRS grid reference ‘New York’'));
});

@@ -157,3 +161,3 @@

describe('UTM fail', function() {
test('zone fail', () => should.Throw(function() { new LatLon(85, 0).toUtm(); }, RangeError, 'Latitude outside UTM limits'));
test('zone fail', () => should.Throw(function() { new LatLon(85, 0).toUtm(); }, RangeError, 'latitude ‘85’ outside UTM limits'));
});

@@ -160,0 +164,0 @@

@@ -21,3 +21,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

describe('constructor fail', function() {
test('texts', () => should.Throw(function() { new Vector3d('x', 'y', 'z'); }, TypeError, 'Invalid vector'));
test('texts', () => should.Throw(function() { new Vector3d('x', 'y', 'z'); }, TypeError, 'invalid vector [x,y,z]'));
});

@@ -31,2 +31,3 @@

test('times', () => v123.times(2).should.deep.equal(new Vector3d(2, 4, 6)));
test('times str', () => v123.times('2').should.deep.equal(new Vector3d(2, 4, 6)));
test('dividedBy', () => v123.dividedBy(2).should.deep.equal(new Vector3d(0.5, 1, 1.5)));

@@ -38,3 +39,6 @@ test('dot', () => v123.dot(v321).should.equal(10));

test('unit', () => v123.unit().toString().should.equal('[0.267,0.535,0.802]'));
test('angleTo', () => (v123.angleTo(v321).toDegrees().toFixed(3)).should.equal('44.415'));
test('angleTo', () => v123.angleTo(v321).toDegrees().toFixed(3).should.equal('44.415'));
test('angleTo +', () => v123.angleTo(v321, v123.cross(v321)).toDegrees().toFixed(3).should.equal('44.415'));
test('angleTo -', () => v123.angleTo(v321, v321.cross(v123)).toDegrees().toFixed(3).should.equal('-44.415'));
test('angleTo 0', () => v123.angleTo(v321, v123).toDegrees().toFixed(3).should.equal('44.415'));
test('rotateAround', () => v123.rotateAround(new Vector3d(0, 0, 1), 90).toString().should.equal('[-0.535,0.267,0.802]'));

@@ -50,2 +54,4 @@ test('toString', () => v123.toString().should.equal('[1.000,2.000,3.000]'));

test('minus', () => should.Throw(function() { v123.minus(1); }, TypeError, 'v is not Vector3d object'));
test('times', () => should.Throw(function() { v123.times('x'); }, TypeError, 'invalid scalar value ‘x’'));
test('dividedBy', () => should.Throw(function() { v123.dividedBy('x'); }, TypeError, 'invalid scalar value ‘x’'));
test('dot', () => should.Throw(function() { v123.dot(1); }, TypeError, 'v is not Vector3d object'));

@@ -52,0 +58,0 @@ test('cross', () => should.Throw(function() { v123.cross(1); }, TypeError, 'v is not Vector3d object'));

@@ -45,3 +45,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

* @param {number} [convergence=null] - Meridian convergence (bearing of grid north
* clockwise from true north), in degrees.
* clockwise from true north), in degrees.
* @param {number} [scale=null] - Grid scale factor.

@@ -55,7 +55,7 @@ * @throws {TypeError} Invalid UTM coordinate.

constructor(zone, hemisphere, easting, northing, datum=LatLonEllipsoidal.datums.WGS84, convergence=null, scale=null) {
if (!(1<=zone && zone<=60)) throw new RangeError(`Invalid UTM zone ‘${zone}’`);
if (!hemisphere.match(/[NS]/i)) throw new RangeError(`Invalid UTM hemisphere ‘${hemisphere}’`);
// range-check easting/northing (with 40km overlap between zones) - TODO is this worthwhile?
//if (!(120e3<=easting && easting<=880e3)) throw new Error('Invalid UTM easting '+ easting);
//if (!(0<=northing && northing<=10000e3)) throw new Error('Invalid UTM northing '+ northing);
if (!(1<=zone && zone<=60)) throw new RangeError(`invalid UTM zone ‘${zone}’`);
if (!hemisphere.match(/[NS]/i)) throw new RangeError(`invalid UTM hemisphere ‘${hemisphere}’`);
if (!(0<=easting && easting<=1000e3)) throw new RangeError(`invalid UTM easting ‘${easting}’`);
if (hemisphere=='N' && !(0<=northing && northing<9328094)) throw new RangeError(`invalid UTM northing ‘${northing}’`);
if (hemisphere=='S' && !(1118414<northing && northing<=10000e3)) throw new RangeError(`invalid UTM northing ‘${northing}’`);

@@ -83,9 +83,7 @@ this.zone = Number(zone);

toLatLon() {
const z = this.zone;
const h = this.hemisphere;
const { zone: z, hemisphere: h } = this;
const falseEasting = 500e3, falseNorthing = 10000e3;
const a = this.datum.ellipsoid.a, f = this.datum.ellipsoid.f;
// WGS-84: a = 6378137, b = 6356752.314245, f = 1/298.257223563;
const { a, f } = this.datum.ellipsoid; // WGS-84: a = 6378137, f = 1/298.257223563;

@@ -174,3 +172,3 @@ const k0 = 0.9996; // UTM scale on the central meridian

const latLong = new LatLonEllipsoidal(lat, lon, 0, this.datum);
const latLong = new LatLon_Utm(lat, lon, 0, this.datum);
// ... and add the convergence and scale into the LatLon object ... wonderful JavaScript!

@@ -206,3 +204,3 @@ latLong.convergence = convergence;

if (utmCoord==null || utmCoord.length!=4) throw new Error(`Invalid UTM coordinate ‘${utmCoord}’`);
if (utmCoord==null || utmCoord.length!=4) throw new Error(`invalid UTM coordinate ‘${utmCoord}’`);

@@ -266,3 +264,3 @@ const zone = utmCoord[0], hemisphere = utmCoord[1], easting = utmCoord[2], northing = utmCoord[3];

toUtm() {
if (!(-80<=this.lat && this.lat<=84)) throw new RangeError('Latitude outside UTM limits');
if (!(-80<=this.lat && this.lat<=84)) throw new RangeError(`latitude ‘${this.lat}’ outside UTM limits`);

@@ -291,5 +289,5 @@ const falseEasting = 500e3, falseNorthing = 10000e3;

const ellipsoid = this.datum ? this.datum.ellipsoid : this.referenceFrame ? this.referenceFrame.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const a = ellipsoid.a, f = ellipsoid.f;
// WGS-84: a = 6378137, b = 6356752.314245, f = 1/298.257223563;
// allow alternative ellipsoid to be specified
const ellipsoid = this.datum ? this.datum.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
const { a, f } = ellipsoid; // WGS-84: a = 6378137, f = 1/298.257223563;

@@ -296,0 +294,0 @@ const k0 = 0.9996; // UTM scale on the central meridian

@@ -40,3 +40,3 @@ /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */

constructor(x, y, z) {
if (isNaN(x) || isNaN(x) || isNaN(x)) throw new TypeError('Invalid vector');
if (isNaN(x) || isNaN(x) || isNaN(x)) throw new TypeError(`invalid vector [${x},${y},${z}]`);

@@ -92,3 +92,3 @@ this.x = Number(x);

times(x) {
x = Number(x);
if (isNaN(x)) throw new TypeError(`invalid scalar value ‘${x}’`);

@@ -106,3 +106,3 @@ return new Vector3d(this.x * x, this.y * x, this.z * x);

dividedBy(x) {
x = Number(x);
if (isNaN(x)) throw new TypeError(`invalid scalar value ‘${x}’`);

@@ -173,8 +173,10 @@ return new Vector3d(this.x / x, this.y / x, this.z / x);

/**
* Calculates the angle between ‘this’ vector and supplied vector atan2(|p₁×p₂|, p₁·p₂).
* Calculates the angle between ‘this’ vector and supplied vector atan2(|p₁×p₂|, p₁·p₂) (or if
* (extra-planar) ‘n’ supplied then atan2(n·p₁×p₂, p₁·p₂).
*
* @param {Vector3d} v
* @param {Vector3d} [n] - Plane normal: if supplied, angle is -π..+π, signed +ve if this->v is
* clockwise looking along n, -ve in opposite direction (if not supplied, angle is always 0..π).
* @returns {number} Angle (in radians) between this vector and supplied vector.
* @param {Vector3d} v - Vector whose angle is to be determined from ‘this’ vector.
* @param {Vector3d} [n] - Plane normal: if supplied, angle is signed +ve if this->v is
* clockwise looking along n, -ve in opposite direction.
* @returns {number} Angle (in radians) between this vector and supplied vector (in range 0..π
* if n not supplied, range -π..+π if n supplied).
*/

@@ -185,3 +187,8 @@ angleTo(v, n=undefined) {

const sign = n==undefined ? 1 : Math.sign(this.cross(v).dot(n));
// q.v. stackoverflow.com/questions/14066933#answer-16544330, but n·p₁×p₂ is numerically
// ill-conditioned, so just calculate sign to apply to |p₁×p₂|
// if n·p₁×p₂ is -ve, negate |p₁×p₂|
const sign = n==undefined || this.cross(v).dot(n)>=0 ? 1 : -1;
const sinθ = this.cross(v).length * sign;

@@ -188,0 +195,0 @@ const cosθ = this.dot(v);

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc