diff --git a/src/index.html b/src/index.html
index 3670874..5d47e03 100644
--- a/src/index.html
+++ b/src/index.html
@@ -5,18 +5,7 @@
     
Планирование маршрутов
     
     
-    
     
-    
-    
-    
-    
-
-    
-    
-    
-    
-    
     
 
 
diff --git a/src/js/cldr.min.js b/src/js/cldr.min.js
deleted file mode 100644
index c0ffad5..0000000
--- a/src/js/cldr.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/*!
- * CLDR JavaScript Library v0.5.4 2020-10-22T15:56Z MIT license © Rafael Xavier
- * http://git.io/h4lmVg
- */
-!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof module&&"object"==typeof module.exports?module.exports=t():e.Cldr=t()}(this,(function(){var e,t=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)},n=function(e,n){if(t(e)&&(e=e.join("/")),"string"!=typeof e)throw new Error('invalid path "'+e+'"');return(e=(e=e.replace(/^\//,"").replace(/^cldr\//,"")).replace(/{[a-zA-Z]+}/g,(function(e){return e=e.replace(/^{([^}]*)}$/,"$1"),n[e]}))).split("/")},r=function(e,t){var n,r;if(e.some)return e.some(t);for(n=0,r=e.length;n4, 2=>8, 3=>16)
-        const cardinal = cardinals[Math.round(bearing*n/360)%n * 16/n];
-
-        return cardinal;
-    }
-
-
-    /**
-     * Constrain degrees to range -90..+90 (for latitude); e.g. -91 => -89, 91 => 89.
-     *
-     * @private
-     * @param {number} degrees
-     * @returns degrees within range -90..+90.
-     */
-    static wrap90(degrees) {
-        if (-90<=degrees && degrees<=90) return degrees; // avoid rounding due to arithmetic ops if within range
-
-        // latitude wrapping requires a triangle wave function; a general triangle wave is
-        //     f(x) = 4a/p ⋅ | (x-p/4)%p - p/2 | - a
-        // where a = amplitude, p = period, % = modulo; however, JavaScript '%' is a remainder operator
-        // not a modulo operator - for modulo, replace 'x%n' with '((x%n)+n)%n'
-        const x = degrees, a = 90, p = 360;
-        return 4*a/p * Math.abs((((x-p/4)%p)+p)%p - p/2) - a;
-    }
-
-    /**
-     * Constrain degrees to range -180..+180 (for longitude); e.g. -181 => 179, 181 => -179.
-     *
-     * @private
-     * @param {number} degrees
-     * @returns degrees within range -180..+180.
-     */
-    static wrap180(degrees) {
-        if (-180<=degrees && degrees<=180) return degrees; // avoid rounding due to arithmetic ops if within range
-
-        // longitude wrapping requires a sawtooth wave function; a general sawtooth wave is
-        //     f(x) = (2ax/p - p/2) % p - a
-        // where a = amplitude, p = period, % = modulo; however, JavaScript '%' is a remainder operator
-        // not a modulo operator - for modulo, replace 'x%n' with '((x%n)+n)%n'
-        const x = degrees, a = 180, p = 360;
-        return (((2*a*x/p - p/2)%p)+p)%p - a;
-    }
-
-    /**
-     * Constrain degrees to range 0..360 (for bearings); e.g. -1 => 359, 361 => 1.
-     *
-     * @private
-     * @param {number} degrees
-     * @returns degrees within range 0..360.
-     */
-    static wrap360(degrees) {
-        if (0<=degrees && degrees<360) return degrees; // avoid rounding due to arithmetic ops if within range
-
-        // bearing wrapping requires a sawtooth wave function with a vertical offset equal to the
-        // amplitude and a corresponding phase shift; this changes the general sawtooth wave function from
-        //     f(x) = (2ax/p - p/2) % p - a
-        // to
-        //     f(x) = (2ax/p) % p
-        // where a = amplitude, p = period, % = modulo; however, JavaScript '%' is a remainder operator
-        // not a modulo operator - for modulo, replace 'x%n' with '((x%n)+n)%n'
-        const x = degrees, a = 180, p = 360;
-        return (((2*a*x/p)%p)+p)%p;
-    }
-
-}
-
-
-// Extend Number object with methods to convert between degrees & radians
-Number.prototype.toRadians = function() { return this * Math.PI / 180; };
-Number.prototype.toDegrees = function() { return this * 180 / Math.PI; };
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export default Dms;
diff --git a/src/js/geo/latlon-ellipsoidal-datum.js b/src/js/geo/latlon-ellipsoidal-datum.js
deleted file mode 100644
index 3cc4c13..0000000
--- a/src/js/geo/latlon-ellipsoidal-datum.js
+++ /dev/null
@@ -1,402 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Geodesy tools for conversions between (historical) datums          (c) Chris Veness 2005-2022  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong-convert-coords.html                                     */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-ellipsoidal-datum                  */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import LatLonEllipsoidal, { Cartesian, Dms } from './latlon-ellipsoidal.js';
-
-
-/**
- * Historical geodetic datums: a latitude/longitude point defines a geographic location on or
- * above/below the  earth’s surface, measured in degrees from the equator & the International
- * Reference Meridian and metres above the ellipsoid, and based on a given datum. The datum is
- * based on a reference ellipsoid and tied to geodetic survey reference points.
- *
- * Modern geodesy is generally based on the WGS84 datum (as used for instance by GPS systems), but
- * previously various reference ellipsoids and datum references were used.
- *
- * This module extends the core latlon-ellipsoidal module to include ellipsoid parameters and datum
- * transformation parameters, and methods for converting between different (generally historical)
- * datums.
- *
- * It can be used for UK Ordnance Survey mapping (OS National Grid References are still based on the
- * otherwise historical OSGB36 datum), as well as for historical purposes.
- *
- * q.v. Ordnance Survey ‘A guide to coordinate systems in Great Britain’ Section 6,
- * www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf, and also
- * www.ordnancesurvey.co.uk/blog/2014/12/2.
- *
- * @module latlon-ellipsoidal-datum
- */
-
-
-/*
- * Ellipsoid parameters; exposed through static getter below.
- */
-const ellipsoids = {
-    WGS84:         { a: 6378137,     b: 6356752.314245, f: 1/298.257223563 },
-    Airy1830:      { a: 6377563.396, b: 6356256.909,    f: 1/299.3249646   },
-    AiryModified:  { a: 6377340.189, b: 6356034.448,    f: 1/299.3249646   },
-    Bessel1841:    { a: 6377397.155, b: 6356078.962822, f: 1/299.15281285  },
-    Clarke1866:    { a: 6378206.4,   b: 6356583.8,      f: 1/294.978698214 },
-    Clarke1880IGN: { a: 6378249.2,   b: 6356515.0,      f: 1/293.466021294 },
-    GRS80:         { a: 6378137,     b: 6356752.314140, f: 1/298.257222101 },
-    Intl1924:      { a: 6378388,     b: 6356911.946128, f: 1/297           }, // aka Hayford
-    WGS72:         { a: 6378135,     b: 6356750.52,     f: 1/298.26        },
-};
-
-
-/*
- * Datums; exposed through static getter below.
- */
-const datums = {
-    // transforms: t in metres, s in ppm, r in arcseconds              tx       ty        tz       s        rx        ry        rz
-    ED50:       { ellipsoid: ellipsoids.Intl1924,      transform: [   89.5,    93.8,    123.1,    -1.2,     0.0,      0.0,      0.156    ] }, // epsg.io/1311
-    ETRS89:     { ellipsoid: ellipsoids.GRS80,         transform: [    0,       0,        0,       0,       0,        0,        0        ] }, // epsg.io/1149; @ 1-metre level
-    Irl1975:    { ellipsoid: ellipsoids.AiryModified,  transform: [ -482.530, 130.596, -564.557,  -8.150,   1.042,    0.214,    0.631    ] }, // epsg.io/1954
-    NAD27:      { ellipsoid: ellipsoids.Clarke1866,    transform: [    8,    -160,     -176,       0,       0,        0,        0        ] },
-    NAD83:      { ellipsoid: ellipsoids.GRS80,         transform: [    0.9956, -1.9103,  -0.5215, -0.00062, 0.025915, 0.009426, 0.011599 ] },
-    NTF:        { ellipsoid: ellipsoids.Clarke1880IGN, transform: [  168,      60,     -320,       0,       0,        0,        0        ] },
-    OSGB36:     { ellipsoid: ellipsoids.Airy1830,      transform: [ -446.448, 125.157, -542.060,  20.4894, -0.1502,  -0.2470,  -0.8421   ] }, // epsg.io/1314
-    Potsdam:    { ellipsoid: ellipsoids.Bessel1841,    transform: [ -582,    -105,     -414,      -8.3,     1.04,     0.35,    -3.08     ] },
-    TokyoJapan: { ellipsoid: ellipsoids.Bessel1841,    transform: [  148,    -507,     -685,       0,       0,        0,        0        ] },
-    WGS72:      { ellipsoid: ellipsoids.WGS72,         transform: [    0,       0,       -4.5,    -0.22,    0,        0,        0.554    ] },
-    WGS84:      { ellipsoid: ellipsoids.WGS84,         transform: [    0.0,     0.0,      0.0,     0.0,     0.0,      0.0,      0.0      ] },
-};
-/* sources:
- * - ED50:       www.gov.uk/guidance/oil-and-gas-petroleum-operations-notices#pon-4
- * - Irl1975:    www.osi.ie/wp-content/uploads/2015/05/transformations_booklet.pdf
- * - NAD27:      en.wikipedia.org/wiki/Helmert_transformation
- * - NAD83:      www.uvm.edu/giv/resources/WGS84_NAD83.pdf [strictly, WGS84(G1150) -> NAD83(CORS96) @ epoch 1997.0]
- *               (note NAD83(1986) ≡ WGS84(Original); confluence.qps.nl/pages/viewpage.action?pageId=29855173)
- * - NTF:        Nouvelle Triangulation Francaise geodesie.ign.fr/contenu/fichiers/Changement_systeme_geodesique.pdf
- * - OSGB36:     www.ordnancesurvey.co.uk/docs/support/guide-coordinate-systems-great-britain.pdf
- * - Potsdam:    kartoweb.itc.nl/geometrics/Coordinate%20transformations/coordtrans.html
- * - TokyoJapan: www.geocachingtoolbox.com?page=datumEllipsoidDetails
- * - WGS72:      www.icao.int/safety/pbn/documentation/eurocontrol/eurocontrol wgs 84 implementation manual.pdf
- *
- * more transform parameters are available from earth-info.nga.mil/GandG/coordsys/datums/NATO_DT.pdf,
- * www.fieldenmaps.info/cconv/web/cconv_params.js
- */
-/* note:
- * - ETRS89 reference frames are coincident with WGS-84 at epoch 1989.0 (ie null transform) at the one metre level.
- */
-
-
-// freeze static properties
-Object.keys(ellipsoids).forEach(e => Object.freeze(ellipsoids[e]));
-Object.keys(datums).forEach(d => { Object.freeze(datums[d]); Object.freeze(datums[d].transform); });
-
-
-/* LatLon - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Latitude/longitude points on an ellipsoidal model earth, with ellipsoid parameters and methods
- * for converting between datums and to geocentric (ECEF) cartesian coordinates.
- *
- * @extends LatLonEllipsoidal
- */
-class LatLonEllipsoidal_Datum extends LatLonEllipsoidal {
-
-    /**
-     * Creates a geodetic latitude/longitude point on an ellipsoidal model earth using given datum.
-     *
-     * @param {number} lat - Latitude (in degrees).
-     * @param {number} lon - Longitude (in degrees).
-     * @param {number} [height=0] - Height above ellipsoid in metres.
-     * @param {LatLon.datums} datum - Datum this point is defined within.
-     *
-     * @example
-     *   import LatLon from '/js/geodesy/latlon-ellipsoidal-datum.js';
-     *   const p = new LatLon(53.3444, -6.2577, 17, LatLon.datums.Irl1975);
-     */
-    constructor(lat, lon, height=0, datum=datums.WGS84) {
-        if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);
-
-        super(lat, lon, height);
-
-        this._datum = datum;
-    }
-
-
-    /**
-     * Datum this point is defined within.
-     */
-    get datum() {
-        return this._datum;
-    }
-
-
-    /**
-     * Ellipsoids with their parameters; semi-major axis (a), semi-minor axis (b), and flattening (f).
-     *
-     * Flattening f = (a−b)/a; at least one of these parameters is derived from defining constants.
-     *
-     * @example
-     *   const a = LatLon.ellipsoids.Airy1830.a; // 6377563.396
-     */
-    static get ellipsoids() {
-        return ellipsoids;
-    }
-
-
-    /**
-     * Datums; with associated ellipsoid, and Helmert transform parameters to convert from WGS-84
-     * into given datum.
-     *
-     * Note that precision of various datums will vary, and WGS-84 (original) is not defined to be
-     * accurate to better than ±1 metre. No transformation should be assumed to be accurate to
-     * better than a metre, for many datums somewhat less.
-     *
-     * This is a small sample of commoner datums from a large set of historical datums. I will add
-     * new datums on request.
-     *
-     * @example
-     *   const a = LatLon.datums.OSGB36.ellipsoid.a;                    // 6377563.396
-     *   const tx = LatLon.datums.OSGB36.transform;                     // [ tx, ty, tz, s, rx, ry, rz ]
-     *   const availableDatums = Object.keys(LatLon.datums).join(', '); // ED50, Irl1975, NAD27, ...
-     */
-    static get datums() {
-        return datums;
-    }
-
-
-    // note instance datum getter/setters are in LatLonEllipsoidal
-
-
-    /**
-     * Parses a latitude/longitude point from a variety of formats.
-     *
-     * Latitude & longitude (in degrees) can be supplied as two separate parameters, as a single
-     * comma-separated lat/lon string, or as a single object with { lat, lon } or GeoJSON properties.
-     *
-     * The latitude/longitude values may be numeric or strings; they may be signed decimal or
-     * deg-min-sec (hexagesimal) suffixed by compass direction (NSEW); a variety of separators are
-     * accepted. Examples -3.62, '3 37 12W', '3°37′12″W'.
-     *
-     * Thousands/decimal separators must be comma/dot; use Dms.fromLocale to convert locale-specific
-     * thousands/decimal separators.
-     *
-     * @param   {number|string|Object} lat|latlon - Geodetic Latitude (in degrees) or comma-separated lat/lon or lat/lon object.
-     * @param   {number}               [lon] - Longitude in degrees.
-     * @param   {number}               [height=0] - Height above ellipsoid in metres.
-     * @param   {LatLon.datums}        [datum=WGS84] - Datum this point is defined within.
-     * @returns {LatLon} Latitude/longitude point on ellipsoidal model earth using given datum.
-     * @throws  {TypeError} Unrecognised datum.
-     *
-     * @example
-     *   const p = LatLon.parse('51.47736, 0.0000', 0, LatLon.datums.OSGB36);
-     */
-    static parse(...args) {
-        let datum = datums.WGS84;
-
-        // if the last argument is a datum, use that, otherwise use default WGS-84
-        if (args.length==4 || (args.length==3 && typeof args[2] == 'object')) datum = args.pop();
-
-        if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);
-
-        const point = super.parse(...args);
-
-        point._datum = datum;
-
-        return point;
-    }
-
-
-    /**
-     * Converts ‘this’ lat/lon coordinate to new coordinate system.
-     *
-     * @param   {LatLon.datums} toDatum - Datum this coordinate is to be converted to.
-     * @returns {LatLon} This point converted to new datum.
-     * @throws  {TypeError} Unrecognised datum.
-     *
-     * @example
-     *   const pWGS84 = new LatLon(51.47788, -0.00147, 0, LatLon.datums.WGS84);
-     *   const pOSGB = pWGS84.convertDatum(LatLon.datums.OSGB36); // 51.4773°N, 000.0001°E
-     */
-    convertDatum(toDatum) {
-        if (!toDatum || toDatum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${toDatum}’`);
-
-        const oldCartesian = this.toCartesian();                 // convert geodetic to cartesian
-        const newCartesian = oldCartesian.convertDatum(toDatum); // convert datum
-        const newLatLon = newCartesian.toLatLon();               // convert cartesian back to geodetic
-
-        return newLatLon;
-    }
-
-
-    /**
-     * Converts ‘this’ point from (geodetic) latitude/longitude coordinates to (geocentric) cartesian
-     * (x/y/z) coordinates, based on the same datum.
-     *
-     * Shadow of LatLonEllipsoidal.toCartesian(), returning Cartesian augmented with
-     * LatLonEllipsoidal_Datum methods/properties.
-     *
-     * @returns {Cartesian} Cartesian point equivalent to lat/lon point, with x, y, z in metres from
-     *   earth centre, augmented with reference frame conversion methods and properties.
-     */
-    toCartesian() {
-        const cartesian = super.toCartesian();
-        const cartesianDatum = new Cartesian_Datum(cartesian.x, cartesian.y, cartesian.z, this.datum);
-        return cartesianDatum;
-    }
-
-}
-
-
-/* Cartesian  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Augments Cartesian with datum the cooordinate is based on, and methods to convert between datums
- * (using Helmert 7-parameter transforms) and to convert cartesian to geodetic latitude/longitude
- * point.
- *
- * @extends Cartesian
- */
-class Cartesian_Datum extends Cartesian {
-
-    /**
-     * Creates cartesian coordinate representing ECEF (earth-centric earth-fixed) point, on a given
-     * datum. The datum will identify the primary meridian (for the x-coordinate), and is also
-     * useful in transforming to/from geodetic (lat/lon) coordinates.
-     *
-     * @param  {number} x - X coordinate in metres (=> 0°N,0°E).
-     * @param  {number} y - Y coordinate in metres (=> 0°N,90°E).
-     * @param  {number} z - Z coordinate in metres (=> 90°N).
-     * @param  {LatLon.datums} [datum] - Datum this coordinate is defined within.
-     * @throws {TypeError} Unrecognised datum.
-     *
-     * @example
-     *   import { Cartesian } from '/js/geodesy/latlon-ellipsoidal-datum.js';
-     *   const coord = new Cartesian(3980581.210, -111.159, 4966824.522);
-     */
-    constructor(x, y, z, datum=undefined) {
-        if (datum && datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);
-
-        super(x, y, z);
-
-        if (datum) this._datum = datum;
-    }
-
-
-    /**
-     * Datum this point is defined within.
-     */
-    get datum() {
-        return this._datum;
-    }
-    set datum(datum) {
-        if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);
-        this._datum = datum;
-    }
-
-
-    /**
-     * Converts ‘this’ (geocentric) cartesian (x/y/z) coordinate to (geodetic) latitude/longitude
-     * point (based on the same datum, or WGS84 if unset).
-     *
-     * Shadow of Cartesian.toLatLon(), returning LatLon augmented with LatLonEllipsoidal_Datum
-     * methods convertDatum, toCartesian, etc.
-     *
-     * @returns {LatLon} Latitude/longitude point defined by cartesian coordinates.
-     * @throws  {TypeError} Unrecognised datum
-     *
-     * @example
-     *   const c = new Cartesian(4027893.924, 307041.993, 4919474.294);
-     *   const p = c.toLatLon(); // 50.7978°N, 004.3592°E
-     */
-    toLatLon(deprecatedDatum=undefined) {
-        if (deprecatedDatum) {
-            console.info('datum parameter to Cartesian_Datum.toLatLon is deprecated: set datum before calling toLatLon()');
-            this.datum = deprecatedDatum;
-        }
-        const datum = this.datum || datums.WGS84;
-        if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ‘${datum}’`);
-
-        const latLon = super.toLatLon(datum.ellipsoid); // TODO: what if datum is not geocentric?
-        const point = new LatLonEllipsoidal_Datum(latLon.lat, latLon.lon, latLon.height, this.datum);
-        return point;
-    }
-
-
-    /**
-     * Converts ‘this’ cartesian coordinate to new datum using Helmert 7-parameter transformation.
-     *
-     * @param   {LatLon.datums} toDatum - Datum this coordinate is to be converted to.
-     * @returns {Cartesian} This point converted to new datum.
-     * @throws  {Error} Undefined datum.
-     *
-     * @example
-     *   const c = new Cartesian(3980574.247, -102.127, 4966830.065, LatLon.datums.OSGB36);
-     *   c.convertDatum(LatLon.datums.Irl1975); // [??,??,??]
-     */
-    convertDatum(toDatum) {
-        // TODO: what if datum is not geocentric?
-        if (!toDatum || toDatum.ellipsoid == undefined) throw new TypeError(`unrecognised datum ‘${toDatum}’`);
-        if (!this.datum) throw new TypeError('cartesian coordinate has no datum');
-
-        let oldCartesian = null;
-        let transform = null;
-
-        if (this.datum == undefined || this.datum == datums.WGS84) {
-            // converting from WGS 84
-            oldCartesian = this;
-            transform = toDatum.transform;
-        }
-        if (toDatum == datums.WGS84) {
-            // converting to WGS 84; use inverse transform
-            oldCartesian = this;
-            transform = this.datum.transform.map(p => -p);
-        }
-        if (transform == null) {
-            // neither this.datum nor toDatum are WGS84: convert this to WGS84 first
-            oldCartesian = this.convertDatum(datums.WGS84);
-            transform = toDatum.transform;
-        }
-
-        const newCartesian = oldCartesian.applyTransform(transform);
-        newCartesian.datum = toDatum;
-
-        return newCartesian;
-    }
-
-
-    /**
-     * Applies Helmert 7-parameter transformation to ‘this’ coordinate using transform parameters t.
-     *
-     * This is used in converting datums (geodetic->cartesian, apply transform, cartesian->geodetic).
-     *
-     * @private
-     * @param   {number[]} t - Transformation to apply to this coordinate.
-     * @returns {Cartesian} Transformed point.
-     */
-    applyTransform(t)   {
-        // this point
-        const { x: x1, y: y1, z: z1 } = this;
-
-        // transform parameters
-        const tx = t[0];                    // x-shift in metres
-        const ty = t[1];                    // y-shift in metres
-        const tz = t[2];                    // z-shift in metres
-        const s  = t[3]/1e6 + 1;            // scale: normalise parts-per-million to (s+1)
-        const rx = (t[4]/3600).toRadians(); // x-rotation: normalise arcseconds to radians
-        const ry = (t[5]/3600).toRadians(); // y-rotation: normalise arcseconds to radians
-        const rz = (t[6]/3600).toRadians(); // z-rotation: normalise arcseconds to radians
-
-        // apply transform
-        const x2 = tx + x1*s  - y1*rz + z1*ry;
-        const y2 = ty + x1*rz + y1*s  - z1*rx;
-        const z2 = tz - x1*ry + y1*rx + z1*s;
-
-        return new Cartesian_Datum(x2, y2, z2);
-    }
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { LatLonEllipsoidal_Datum as default, Cartesian_Datum as Cartesian, datums, Dms };
diff --git a/src/js/geo/latlon-ellipsoidal-referenceframe-txparams.js b/src/js/geo/latlon-ellipsoidal-referenceframe-txparams.js
deleted file mode 100644
index 0da6656..0000000
--- a/src/js/geo/latlon-ellipsoidal-referenceframe-txparams.js
+++ /dev/null
@@ -1,148 +0,0 @@
-/* Helmert transform parameters         tx(mm)   ty(mm)   tz(mm) s(ppb)  rx(mas)  ry(mas)  rz(mas) */
-export default {
-    /* eslint-disable key-spacing, indent */
-    'ITRF2014→ITRF2008': { epoch: '2010.0',
-                           params: [    1.6,     1.9,     2.4,  -0.02,   0.00,    0.00,    0.00   ],
-                           rates:  [    0.0,     0.0,    -0.1,   0.03,   0.00,    0.00,    0.00   ] },
-    'ITRF2014→ITRF2005': { epoch: '2010.0',
-                           params: [    2.6,     1.0,    -2.3,   0.92,   0.00,    0.00,    0.00   ],
-                           rates:  [    0.3,     0.0,    -0.1,   0.03,   0.00,    0.00,    0.00   ] },
-    'ITRF2014→ITRF2000': { epoch: '2010.0',
-                           params: [    0.7,     1.2,   -26.1,   2.12,   0.00,    0.00,    0.00   ],
-                           rates:  [    0.1,     0.1,    -1.9,   0.11,   0.00,    0.00,    0.00   ] },
-    'ITRF2014→ITRF97':   { epoch: '2010.0',
-                           params: [    7.4,    -0.5,   -62.8,   3.80,   0.00,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-    'ITRF2014→ITRF96':   { epoch: '2010.0',
-                           params: [    7.4,    -0.5,   -62.8,   3.80,   0.00,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-    'ITRF2014→ITRF94':   { epoch: '2010.0',
-                           params: [    7.4,    -0.5,   -62.8,   3.80,   0.00,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-    'ITRF2014→ITRF93':   { epoch: '2010.0',
-                           params: [  -50.4,     3.3,   -60.2,   4.29,  -2.81,   -3.38,    0.40   ],
-                           rates:  [   -2.8,    -0.1,    -2.5,   0.12,  -0.11,   -0.19,    0.07   ] },
-    'ITRF2014→ITRF92':   { epoch: '2010.0',
-                           params: [   15.4,     1.5,   -70.8,   3.09,   0.00,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-    'ITRF2014→ITRF91':   { epoch: '2010.0',
-                           params: [   27.4,    15.5,   -76.8,   4.49,   0.00,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-    'ITRF2014→ITRF90':   { epoch: '2010.0',
-                           params: [   25.4,    11.5,   -92.8,   4.79,   0.00,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-    'ITRF2014→ITRF89':   { epoch: '2010.0',
-                           params: [   30.4,    35.5,  -130.8,   8.19,   0.00,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-    'ITRF2014→ITRF88':   { epoch: '2010.0',
-                           params: [   25.4,    -0.5,  -154.8,  11.29,   0.10,    0.00,    0.26   ],
-                           rates:  [    0.1,    -0.5,    -3.3,   0.12,   0.00,    0.00,    0.02   ] },
-
-    'ITRF2008→ITRF2005': { epoch: '2000.0',
-                           params: [   -2.0,    -0.9,    -4.7,   0.94,   0.00,    0.00,    0.00   ],
-                           rates:  [    0.3,     0.0,     0.0,   0.00,   0.00,    0.00,    0.00   ] },
-    'ITRF2008→ITRF2000': { epoch: '2000.0',
-                           params: [   -1.9,    -1.7,   -10.5,   1.34,   0.00,    0.00,    0.00   ],
-                           rates:  [    0.1,     0.1,    -1.8,   0.08,   0.00,    0.00,    0.00   ] },
-    'ITRF2008→ITRF97':   { epoch: '2000.0',
-                           params: [    4.8,     2.6,   -33.2,   2.92,   0.00,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-    'ITRF2008→ITRF96':   { epoch: '2000.0',
-                           params: [    4.8,     2.6,   -33.2,   2.92,   0.00,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-    'ITRF2008→ITRF94':   { epoch: '2000.0',
-                           params: [    4.8,     2.6,   -33.2,   2.92,   0.00,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-    'ITRF2008→ITRF93':   { epoch: '2000.0',
-                           params: [  -24.0,     2.4,   -38.6,   3.41,  -1.71,   -1.48,   -0.30   ],
-                           rates:  [   -2.8,    -0.1,    -2.4,   0.09,  -0.11,   -0.19,    0.07   ] },
-    'ITRF2008→ITRF92':   { epoch: '2000.0',
-                           params: [   12.8,     4.6,   -41.2,   2.21,   0.00,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-    'ITRF2008→ITRF91':   { epoch: '2000.0',
-                           params: [   24.8,    18.6,   -47.2,   3.61,   0.00,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-    'ITRF2008→ITRF90':   { epoch: '2000.0',
-                           params: [   22.8,    14.6,   -63.2,   3.91,   0.00,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-    'ITRF2008→ITRF89':   { epoch: '2000.0',
-                           params: [   27.8,    38.6,  -101.2,   7.31,   0.00,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-    'ITRF2008→ITRF88':   { epoch: '2000.0',
-                           params: [   22.8,     2.6,  -125.2,  10.41,   0.10,    0.00,    0.06   ],
-                           rates:  [    0.1,    -0.5,    -3.2,   0.09,   0.00,    0.00,    0.02   ] },
-
-    'ITRF2005→ITRF2000': { epoch: '2000.0',
-                           params: [    0.1,    -0.8,    -5.8,   0.40,   0.000,   0.000,   0.000  ],
-                           rates:  [   -0.2,     0.1,    -1.8,   0.08,   0.000,   0.000,   0.000  ] },
-
-    'ITRF2000→ITRF97':   { epoch: '1997.0',
-                           params: [   0.67,     0.61,   -1.85,  1.55,   0.00,    0.00,    0.00   ],
-                           rates:  [   0.00,    -0.06,   -0.14,  0.01,   0.00,    0.00,    0.02   ] },
-    'ITRF2000→ITRF96':   { epoch: '1997.0',
-                           params: [   0.67,     0.61,   -1.85,  1.55,   0.00,    0.00,    0.00   ],
-                           rates:  [   0.00,    -0.06,   -0.14,  0.01,   0.00,    0.00,    0.02   ] },
-    'ITRF2000→ITRF94':   { epoch: '1997.0',
-                           params: [   0.67,     0.61,   -1.85,  1.55,   0.00,    0.00,    0.00   ],
-                           rates:  [   0.00,    -0.06,   -0.14,  0.01,   0.00,    0.00,    0.02   ] },
-    'ITRF2000→ITRF93':   { epoch: '1988.0',
-                           params: [   12.7,     6.5,   -20.9,   1.95,  -0.39,    0.80,   -1.14   ],
-                           rates:  [   -2.9,    -0.2,    -0.6,   0.01,  -0.11,   -0.19,    0.07   ] },
-    'ITRF2000→ITRF92':   { epoch: '1988.0',
-                           params: [   1.47,     1.35,   -1.39,  0.75,   0.00,    0.00,   -0.18   ],
-                           rates:  [   0.00,    -0.06,   -0.14,  0.01,   0.00,    0.00,    0.02   ] },
-    'ITRF2000→ITRF91':   { epoch: '1988.0',
-                           params: [   26.7,    27.5,   -19.9,   2.15,   0.00,    0.00,   -0.18   ],
-                           rates:  [    0.0,    -0.6,    -1.4,   0.01,   0.00,    0.00,    0.02   ] },
-    'ITRF2000→ITRF90':   { epoch: '1988.0',
-                           params: [   2.47,     2.35,   -3.59,  2.45,   0.00,    0.00,   -0.18   ],
-                           rates:  [   0.00,    -0.06,   -0.14,  0.01,   0.00,    0.00,    0.02   ] },
-    'ITRF2000→ITRF89':   { epoch: '1988.0',
-                           params: [   2.97,     4.75,   -7.39,  5.85,   0.00,    0.00,   -0.18   ],
-                           rates:  [   0.00,    -0.06,   -0.14,  0.01,   0.00,    0.00,    0.02   ] },
-    'ITRF2000→ITRF88':   { epoch: '1988.0',
-                           params: [   2.47,     1.15,   -9.79,  8.95,   0.10,    0.00,   -0.18   ],
-                           rates:  [   0.00,    -0.06,   -0.14,  0.01,   0.00,    0.00,    0.02   ] },
-
-    'ITRF2000→NAD83':    { epoch: '1997.0', // note NAD83(CORS96)
-                           params: [  995.6, -1901.3,  -521.5,   0.62,  25.915,   9.426,  11.599  ],
-                           rates:  [    0.7,    -0.7,     0.5,  -0.18,   0.067,  -0.757,  -0.051  ] },
-
-    'ITRF2014→ETRF2000': { epoch: '2000.0',
-                           params: [   53.7,    51.2,   -55.1,   1.02,   0.891,   5.390,  -8.712  ],
-                           rates:  [    0.1,     0.1,    -1.9,   0.11,   0.081,   0.490,  -0.792  ] },
-    'ITRF2008→ETRF2000': { epoch: '2000.0',
-                           params: [   52.1,    49.3,   -58.5,   1.34,   0.891,   5.390,  -8.712  ],
-                           rates:  [    0.1,     0.1,    -1.8,   0.08,   0.081,   0.490,  -0.792  ] },
-    'ITRF2005→ETRF2000': { epoch: '2000.0',
-                           params: [   54.1,    50.2,   -53.8,   0.40,   0.891,   5.390,  -8.712  ],
-                           rates:  [   -0.2,     0.1,    -1.8,   0.08,   0.081,   0.490,  -0.792  ] },
-    'ITRF2000→ETRF2000': { epoch: '2000.0',
-                           params: [   54.0,    51.0,   -48.0,   0.00,   0.891,   5.390,  -8.712  ],
-                           rates:  [    0.0,     0.0,     0.0,   0.00,   0.081,   0.490,  -0.792  ] },
-
-    'ITRF2008→GDA94':    { epoch: '1994.0',
-                           params: [  -84.68,  -19.42,   32.01,  9.710, -0.4254,  2.2578,  2.4015 ],
-                           rates:  [    1.42,    1.34,    0.90,  0.109,  1.5461,  1.1820,  1.1551 ] },
-    'ITRF2005→GDA94':    { epoch: '1994.0',
-                           params: [  -79.73,   -6.86,   38.03,  6.636,  0.0351, -2.1211, -2.1411 ],
-                           rates:  [    2.25,   -0.62,   -0.56,  0.294, -1.4707, -1.1443, -1.1701 ] },
-    'ITRF2000→GDA94':    { epoch: '1994.0',
-                           params: [  -45.91,  -29.85,  -20.37,  7.070, -1.6705,  0.4594,  1.9356 ],
-                           rates:  [   -4.66,    3.55,   11.24,  0.249,  1.7454,  1.4868,  1.2240 ] },
-};
-/* Note WGS84(G730/G873/G1150) are coincident with ITRF at 10-centimetre level; WGS84(G1674) and
- * ITRF20014 / ITRF2008 ‘are likely to agree at the centimeter level’ (QPS).
- *
- * sources:
- * - ITRS: itrf.ensg.ign.fr/trans_para.php
- * - NAD83: Transforming Positions and Velocities between the International Terrestrial Reference
- *     Frame of 2000 and North American Datum of 1983, Soler & Snay, 2004;
- *     www.ngs.noaa.gov/CORS/Articles/SolerSnayASCE.pdf
- * - ETRS: etrs89.ensg.ign.fr/memo-V8.pdf / www.euref.eu/symposia/2016SanSebastian/01-02-Altamimi.pdf
- * - GDA:  ITRF to GDA94 coordinate transformations, Dawson & Woods, 2010
- *     (note sign of rotations for GDA94 reversed from Dawson & Woods 2010 as “Australia assumes rotation
- *     to be of coordinate axes” rather than the more conventional “position around the coordinate axes”)
- * more are available at:
- * confluence.qps.nl/qinsy/files/en/29856813/45482834/2/1453459502000/ITRF_Transformation_Parameters.xlsx
- */
diff --git a/src/js/geo/latlon-ellipsoidal-referenceframe.js b/src/js/geo/latlon-ellipsoidal-referenceframe.js
deleted file mode 100644
index 1aa2773..0000000
--- a/src/js/geo/latlon-ellipsoidal-referenceframe.js
+++ /dev/null
@@ -1,533 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Geodesy tools for conversions between reference frames             (c) Chris Veness 2016-2019  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong-convert-coords.html                                     */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-ellipsoidal-referenceframe          */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import LatLonEllipsoidal, { Cartesian, Dms } from './latlon-ellipsoidal.js';
-
-
-/**
- * Modern geodetic reference frames: a latitude/longitude point defines a geographic location on or
- * above/below the earth’s surface, measured in degrees from the equator and the International
- * Reference Meridian and metres above the ellipsoid within a given terrestrial reference frame at a
- * given epoch.
- *
- * This module extends the core latlon-ellipsoidal module to include methods for converting between
- * different reference frames.
- *
- * This is scratching the surface of complexities involved in high precision geodesy, but may be of
- * interest and/or value to those with less demanding requirements.
- *
- * Note that ITRF solutions do not directly use an ellipsoid, but are specified by cartesian
- * coordinates; the GRS80 ellipsoid is recommended for transformations to geographical coordinates
- * (itrf.ensg.ign.fr).
- *
- * @module latlon-ellipsoidal-referenceframe
- */
-
-
-/*
- * Sources:
- *
- * - Soler & Snay, “Transforming Positions and Velocities between the International Terrestrial Refer-
- *   ence Frame of 2000 and North American Datum of 1983”, Journal of Surveying Engineering May 2004;
- *   www.ngs.noaa.gov/CORS/Articles/SolerSnayASCE.pdf.
- *
- * - Dawson & Woods, “ITRF to GDA94 coordinate transformations”, Journal of Applied Geodesy 4 (2010);
- *   www.ga.gov.au/webtemp/image_cache/GA19050.pdf.
- */
-
-/* eslint-disable key-spacing, indent */
-
-/*
- * Ellipsoid parameters; exposed through static getter below.
- */
-const ellipsoids = {
-    WGS84: { a: 6378137, b: 6356752.314245, f: 1/298.257223563 },
-    GRS80: { a: 6378137, b: 6356752.314140, f: 1/298.257222101 },
-};
-
-/*
- * Reference frames; exposed through static getter below.
- */
-const referenceFrames = {
-    ITRF2014:   { name: 'ITRF2014',   epoch: 2010.0, ellipsoid: ellipsoids.GRS80 },
-    ITRF2008:   { name: 'ITRF2008',   epoch: 2005.0, ellipsoid: ellipsoids.GRS80 },
-    ITRF2005:   { name: 'ITRF2005',   epoch: 2000.0, ellipsoid: ellipsoids.GRS80 },
-    ITRF2000:   { name: 'ITRF2000',   epoch: 1997.0, ellipsoid: ellipsoids.GRS80 },
-    ITRF93:     { name: 'ITRF93',     epoch: 1988.0, ellipsoid: ellipsoids.GRS80 },
-    ITRF91:     { name: 'ITRF91',     epoch: 1988.0, ellipsoid: ellipsoids.GRS80 },
-    WGS84g1762: { name: 'WGS84g1762', epoch: 2005.0, ellipsoid: ellipsoids.WGS84 },
-    WGS84g1674: { name: 'WGS84g1674', epoch: 2005.0, ellipsoid: ellipsoids.WGS84 },
-    WGS84g1150: { name: 'WGS84g1150', epoch: 2001.0, ellipsoid: ellipsoids.WGS84 },
-    ETRF2000:   { name: 'ETRF2000',   epoch: 2005.0, ellipsoid: ellipsoids.GRS80 }, // ETRF2000(R08)
-    NAD83:      { name: 'NAD83',      epoch: 1997.0, ellipsoid: ellipsoids.GRS80 }, // CORS96
-    GDA94:      { name: 'GDA94',      epoch: 1994.0, ellipsoid: ellipsoids.GRS80 },
-};
-
-/*
- * Transform parameters; exposed through static getter below.
- */
-import txParams from './latlon-ellipsoidal-referenceframe-txparams.js';
-
-
-// freeze static properties
-Object.keys(ellipsoids).forEach(e => Object.freeze(ellipsoids[e]));
-Object.keys(referenceFrames).forEach(trf => Object.freeze(referenceFrames[trf]));
-Object.keys(txParams).forEach(tx => { Object.freeze(txParams[tx]); Object.freeze(txParams[tx].params); Object.freeze(txParams[tx].rates); });
-
-/* eslint-enable key-spacing, indent */
-
-
-/* LatLon - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Latitude/longitude points on an ellipsoidal model earth, with ellipsoid parameters and methods
- * for converting between reference frames and to geocentric (ECEF) cartesian coordinates.
- *
- * @extends LatLonEllipsoidal
- */
-class LatLonEllipsoidal_ReferenceFrame extends LatLonEllipsoidal {
-
-    /**
-     * Creates geodetic latitude/longitude point on an ellipsoidal model earth using using a
-     * specified reference frame.
-     *
-     * Note that while the epoch defaults to the frame reference epoch, the accuracy of ITRF
-     * realisations is meaningless without knowing the observation epoch.
-     *
-     * @param  {number} lat - Geodetic latitude in degrees.
-     * @param  {number} lon - Geodetic longitude in degrees.
-     * @param  {number} [height=0] - Height above ellipsoid in metres.
-     * @param  {LatLon.referenceFrames} [referenceFrame=ITRF2014] - Reference frame this point is defined within.
-     * @param  {number} [epoch=referenceFrame.epoch] - date of observation of coordinate (decimal year).
-     *   defaults to reference epoch t₀ of reference frame.
-     * @throws {TypeError} Unrecognised reference frame.
-     *
-     * @example
-     *   import LatLon from '/js/geodesy/latlon-ellipsoidal-referenceframe.js';
-     *   const p = new LatLon(51.47788, -0.00147, 0, LatLon.referenceFrames.ITRF2000);
-     */
-    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}’`);
-
-        super(lat, lon, height);
-
-        this._referenceFrame = referenceFrame;
-        if (epoch) this._epoch = Number(epoch);
-    }
-
-
-    /**
-     * Reference frame this point is defined within.
-     */
-    get referenceFrame() {
-        return this._referenceFrame;
-    }
-
-
-    /**
-     * Point’s observed epoch.
-     */
-    get epoch() {
-        return this._epoch || this.referenceFrame.epoch;
-    }
-
-
-    /**
-     * Ellipsoid parameters; semi-major axis (a), semi-minor axis (b), and flattening (f).
-     *
-     * The only ellipsoids used in modern geodesy are WGS-84 and GRS-80 (while based on differing
-     * defining parameters, the only effective difference is a 0.1mm variation in the minor axis b).
-     *
-     * @example
-     *   const availableEllipsoids = Object.keys(LatLon.ellipsoids).join(); // WGS84,GRS80
-     *   const a = LatLon.ellipsoids.Airy1830.a;                            // 6377563.396
-     */
-    static get ellipsoids() {
-        return ellipsoids;
-    }
-
-
-    /**
-     * Reference frames, with their base ellipsoids and reference epochs.
-     *
-     * @example
-     *   const availableReferenceFrames = Object.keys(LatLon.referenceFrames).join(); // ITRF2014,ITRF2008, ...
-     */
-    static get referenceFrames() {
-        return referenceFrames;
-    }
-
-
-    /**
-     * 14-parameter Helmert transformation parameters between (dynamic) ITRS frames, and from ITRS
-     * frames to (static) regional TRFs NAD83, ETRF2000, and GDA94.
-     *
-     * This is a limited set of transformations; e.g. ITRF frames prior to ITRF2000 are not included.
-     * More transformations could be added on request.
-     *
-     * Many conversions are direct; for NAD83, successive ITRF transformations are chained back to
-     * ITRF2000.
-     */
-    static get transformParameters() {
-        return txParams;
-    }
-
-
-    /**
-     * Parses a latitude/longitude point from a variety of formats.
-     *
-     * Latitude & longitude (in degrees) can be supplied as two separate parameters, as a single
-     * comma-separated lat/lon string, or as a single object with { lat, lon } or GeoJSON properties.
-     *
-     * The latitude/longitude values may be numeric or strings; they may be signed decimal or
-     * deg-min-sec (hexagesimal) suffixed by compass direction (NSEW); a variety of separators are
-     * accepted. Examples -3.62, '3 37 12W', '3°37′12″W'.
-     *
-     * Thousands/decimal separators must be comma/dot; use Dms.fromLocale to convert locale-specific
-     * thousands/decimal separators.
-     *
-     * @param   {number|string|Object}   lat|latlon - Geodetic Latitude (in degrees) or comma-separated lat/lon or lat/lon object.
-     * @param   {number}                 [lon] - Longitude in degrees.
-     * @param   {number}                 height - Height above ellipsoid in metres.
-     * @param   {LatLon.referenceFrames} referenceFrame - Reference frame this point is defined within.
-     * @param   {number} [epoch=referenceFrame.epoch] - date of observation of coordinate (decimal year).
-     * @returns {LatLon} Latitude/longitude point on ellipsoidal model earth using given reference frame.
-     * @throws {TypeError} Unrecognised reference frame.
-     *
-     * @example
-     *   const p1 = LatLon.parse(51.47788, -0.00147, 17, LatLon.referenceFrames.ETRF2000);          // numeric pair
-     *   const p2 = LatLon.parse('51°28′40″N, 000°00′05″W', 17, LatLon.referenceFrames.ETRF2000);   // dms string + height
-     *   const p3 = LatLon.parse({ lat: 52.205, lon: 0.119 }, 17, LatLon.referenceFrames.ETRF2000); // { lat, lon } object numeric
-     */
-    static parse(...args) {
-        if (args.length == 0) throw new TypeError('invalid (empty) point');
-
-        let referenceFrame = null, epoch = null;
-
-        if (!isNaN(args[1]) && typeof args[2] == 'object') { // latlon, height, referenceFrame, [epoch]
-            [ referenceFrame ] = args.splice(2, 1);
-            [ epoch ] = args.splice(2, 1);
-        }
-
-        if (!isNaN(args[2]) && typeof args[3] == 'object') { // lat, lon, height, referenceFrame, [epoch]
-            [ referenceFrame ] = args.splice(3, 1);
-            [ epoch ] = args.splice(3, 1);
-        }
-
-        if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('unrecognised reference frame');
-
-        // args is now lat, lon, height or latlon, height as taken by LatLonEllipsoidal .parse()
-
-        const point = super.parse(...args); // note super.parse() also invokes this.constructor()
-
-        point._referenceFrame = referenceFrame;
-        if (epoch) point._epoch = Number(epoch);
-
-        return point;
-    }
-
-
-    /**
-     * Converts ‘this’ lat/lon coordinate to new coordinate system.
-     *
-     * @param   {LatLon.referenceFrames} toReferenceFrame - Reference frame this coordinate is to be converted to.
-     * @returns {LatLon} This point converted to new reference frame.
-     * @throws  {Error}  Undefined reference frame, Transformation not available.
-     *
-     * @example
-     *   const pEtrf = new LatLon(51.47788000, -0.00147000, 0, LatLon.referenceFrames.ITRF2000);
-     *   const pItrf = pEtrf.convertReferenceFrame(LatLon.referenceFrames.ETRF2000); // 51.47787826°N, 000.00147125°W
-     */
-    convertReferenceFrame(toReferenceFrame) {
-        if (!toReferenceFrame || toReferenceFrame.epoch == undefined) throw new TypeError('unrecognised reference frame');
-
-        const oldCartesian = this.toCartesian();                                   // convert geodetic to cartesian
-        const newCartesian = oldCartesian.convertReferenceFrame(toReferenceFrame); // convert TRF
-        const newLatLon = newCartesian.toLatLon();                                 // convert cartesian back to to geodetic
-
-        return newLatLon;
-    }
-
-
-    /**
-     * Converts ‘this’ point from (geodetic) latitude/longitude coordinates to (geocentric) cartesian
-     * (x/y/z) coordinates, based on same reference frame.
-     *
-     * Shadow of LatLonEllipsoidal.toCartesian(), returning Cartesian augmented with
-     * LatLonEllipsoidal_ReferenceFrame methods/properties.
-     *
-     * @returns {Cartesian} Cartesian point equivalent to lat/lon point, with x, y, z in metres from
-     *   earth centre, augmented with reference frame conversion methods and properties.
-     */
-    toCartesian() {
-        const cartesian = super.toCartesian();
-        const cartesianReferenceFrame = new Cartesian_ReferenceFrame(cartesian.x, cartesian.y, cartesian.z, this.referenceFrame, this.epoch);
-        return cartesianReferenceFrame;
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ point, formatted as degrees, degrees+minutes, or
-     * degrees+minutes+seconds.
-     *
-     * @param   {string} [format=d] - Format point as 'd', 'dm', 'dms'.
-     * @param   {number} [dp=4|2|0] - Number of decimal places to use: default 4 for d, 2 for dm, 0 for dms.
-     * @param   {number} [dpHeight=null] - Number of decimal places to use for height; default (null) is no height display.
-     * @param   {boolean} [referenceFrame=false] - Whether to show reference frame point is defined on.
-     * @returns {string} Comma-separated formatted latitude/longitude.
-     *
-     * @example
-     *   new LatLon(51.47788, -0.00147, 0, LatLon.referenceFrames.ITRF2014).toString();             // 51.4778°N, 000.0015°W
-     *   new LatLon(51.47788, -0.00147, 0, LatLon.referenceFrames.ITRF2014).toString('dms');        // 51°28′40″N, 000°00′05″W
-     *   new LatLon(51.47788, -0.00147, 42, LatLon.referenceFrames.ITRF2014).toString('dms', 0, 0); // 51°28′40″N, 000°00′05″W +42m
-     */
-    toString(format='d', dp=undefined, dpHeight=null, referenceFrame=false) {
-        const ll = super.toString(format, dp, dpHeight);
-
-        const epochFmt = { useGrouping: false, minimumFractionDigits: 1, maximumFractionDigits: 20 };
-        const epoch = this.referenceFrame && this.epoch != this.referenceFrame.epoch ? this.epoch.toLocaleString('en', epochFmt) : '';
-
-        const trf = referenceFrame ? ` (${this.referenceFrame.name}${epoch?'@'+epoch:''})` : '';
-
-        return ll + trf;
-    }
-
-}
-
-
-/* Cartesian  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Augments Cartesian with reference frame and observation epoch the cooordinate is based on, and
- * methods to convert between reference frames (using Helmert 14-parameter transforms) and to
- * convert cartesian to geodetic latitude/longitude point.
- *
- * @extends Cartesian
- */
-class Cartesian_ReferenceFrame extends Cartesian {
-
-    /**
-     * Creates cartesian coordinate representing ECEF (earth-centric earth-fixed) point, on a given
-     * reference frame. The reference frame will identify the primary meridian (for the x-coordinate),
-     * and is also useful in transforming to/from geodetic (lat/lon) coordinates.
-     *
-     * @param  {number} x - X coordinate in metres (=> 0°N,0°E).
-     * @param  {number} y - Y coordinate in metres (=> 0°N,90°E).
-     * @param  {number} z - Z coordinate in metres (=> 90°N).
-     * @param  {LatLon.referenceFrames} [referenceFrame] - Reference frame this coordinate is defined within.
-     * @param  {number} [epoch=referenceFrame.epoch] - date of observation of coordinate (decimal year).
-     * @throws {TypeError} Unrecognised reference frame, Invalid epoch.
-     *
-     * @example
-     *   import { Cartesian } from '/js/geodesy/latlon-ellipsoidal-referenceframe.js';
-     *   const coord = new Cartesian(3980581.210, -111.159, 4966824.522);
-     */
-    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}’`);
-
-        super(x, y, z);
-
-        if (referenceFrame) this._referenceFrame = referenceFrame;
-        if (epoch) this._epoch = epoch;
-    }
-
-
-    /**
-     * Reference frame this point is defined within.
-     */
-    get referenceFrame() {
-        return this._referenceFrame;
-    }
-    set referenceFrame(referenceFrame) {
-        if (!referenceFrame || referenceFrame.epoch==undefined) throw new TypeError('unrecognised reference frame');
-        this._referenceFrame = referenceFrame;
-    }
-
-    /**
-     * Point’s observed epoch.
-     */
-    get epoch() {
-        return this._epoch ? this._epoch : (this._referenceFrame ? this._referenceFrame.epoch : undefined);
-    }
-    set epoch(epoch) {
-        if (isNaN(Number(epoch))) throw new TypeError(`invalid epoch ’${epoch}’`);
-        if (this._epoch != this._referenceFrame.epoch) this._epoch = Number(epoch);
-    }
-
-
-    /**
-     * Converts ‘this’ (geocentric) cartesian (x/y/z) coordinate to (geodetic) latitude/longitude
-     * point (based on the same reference frame).
-     *
-     * Shadow of Cartesian.toLatLon(), returning LatLon augmented with LatLonEllipsoidal_ReferenceFrame
-     * methods convertReferenceFrame, toCartesian, etc.
-     *
-     * @returns {LatLon} Latitude/longitude point defined by cartesian coordinates, in given reference frame.
-     * @throws  {Error} No reference frame defined.
-     *
-     * @example
-     *   const c = new Cartesian(4027893.924, 307041.993, 4919474.294, LatLon.referenceFrames.ITRF2000);
-     *   const p = c.toLatLon(); // 50.7978°N, 004.3592°E
-     */
-    toLatLon() {
-        if (!this.referenceFrame) throw new Error('cartesian reference frame not defined');
-
-        const latLon = super.toLatLon(this.referenceFrame.ellipsoid);
-        const point = new LatLonEllipsoidal_ReferenceFrame(latLon.lat, latLon.lon, latLon.height, this.referenceFrame, this.epoch);
-        return point;
-    }
-
-
-    /**
-     * Converts ‘this’ cartesian coordinate to new reference frame using Helmert 14-parameter
-     * transformation. The observation epoch is unchanged.
-     *
-     * Note that different conversions have different tolerences; refer to the literature if
-     * tolerances are significant.
-     *
-     * @param   {LatLon.referenceFrames} toReferenceFrame - Reference frame this coordinate is to be converted to.
-     * @returns {Cartesian} This point converted to new reference frame.
-     * @throws  {Error} Undefined reference frame.
-     *
-     * @example
-     *   const c = new Cartesian(3980574.247, -102.127, 4966830.065, LatLon.referenceFrames.ITRF2000);
-     *   c.convertReferenceFrame(LatLon.referenceFrames.ETRF2000); // [3980574.395,-102.214,4966829.941](ETRF2000@1997.0)
-     */
-    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 (this.referenceFrame.name == toReferenceFrame.name) return this; // no-op!
-
-        const oldTrf = this.referenceFrame;
-        const newTrf = toReferenceFrame;
-
-        // WGS84(G730/G873/G1150) are coincident with ITRF at 10-centimetre level; WGS84(G1674) and
-        // ITRF20014 / ITRF2008 ‘are likely to agree at the centimeter level’ (QPS)
-        if (oldTrf.name.startsWith('ITRF') && newTrf.name.startsWith('WGS84')) return this;
-        if (oldTrf.name.startsWith('WGS84') && newTrf.name.startsWith('ITRF')) return this;
-
-        const oldC = this;
-        let newC = null;
-
-        // is requested transformation available in single step?
-        const txFwd = txParams[oldTrf.name+'→'+newTrf.name];
-        const txRev = txParams[newTrf.name+'→'+oldTrf.name];
-
-        if (txFwd || txRev) {
-            // yes, single step available (either forward or reverse)
-            const tx = txFwd? txFwd : reverseTransform(txRev);
-            const t = this.epoch || this.referenceFrame.epoch;
-            const t0 = tx.epoch;//epoch || newTrf.epoch;
-            newC = oldC.applyTransform(tx.params, tx.rates, t-t0); // ...apply transform...
-        } else {
-            // find intermediate transform common to old & new to chain though; this is pretty yucky,
-            // but since with current transform params we can transform in no more than 2 steps, it works!
-            // TODO: find cleaner method!
-            const txAvailFromOld = Object.keys(txParams).filter(tx => tx.split('→')[0] == oldTrf.name).map(tx => tx.split('→')[1]);
-            const txAvailToNew = Object.keys(txParams).filter(tx => tx.split('→')[1] == newTrf.name).map(tx => tx.split('→')[0]);
-            const txIntermediateFwd = txAvailFromOld.filter(tx => txAvailToNew.includes(tx))[0];
-            const txAvailFromNew = Object.keys(txParams).filter(tx => tx.split('→')[0] == newTrf.name).map(tx => tx.split('→')[1]);
-            const txAvailToOld = Object.keys(txParams).filter(tx => tx.split('→')[1] == oldTrf.name).map(tx => tx.split('→')[0]);
-            const txIntermediateRev = txAvailFromNew.filter(tx => txAvailToOld.includes(tx))[0];
-            const txFwd1 = txParams[oldTrf.name+'→'+txIntermediateFwd];
-            const txFwd2 = txParams[txIntermediateFwd+'→'+newTrf.name];
-            const txRev1 = txParams[newTrf.name+'→'+txIntermediateRev];
-            const txRev2 = txParams[txIntermediateRev+'→'+oldTrf.name];
-            const tx1 = txIntermediateFwd ? txFwd1 : reverseTransform(txRev2);
-            const tx2 = txIntermediateFwd ? txFwd2 : reverseTransform(txRev1);
-            const t = this.epoch || this.referenceFrame.epoch;
-            newC = oldC.applyTransform(tx1.params, tx1.rates, t-tx1.epoch); // ...apply transform 1...
-            newC = newC.applyTransform(tx2.params, tx2.rates, t-tx2.epoch); // ...apply transform 2...
-        }
-
-        newC.referenceFrame = toReferenceFrame;
-        newC.epoch = oldC.epoch;
-
-        return newC;
-
-        function reverseTransform(tx) {
-            return { epoch: tx.epoch, params: tx.params.map(p => -p), rates: tx.rates.map(r => -r) };
-        }
-    }
-
-
-    /**
-     * Applies Helmert 14-parameter transformation to ‘this’ coordinate using supplied transform
-     * parameters and annual rates of change, with the secular variation given by the difference
-     * between the reference epoch t0 and the observation epoch tc.
-     *
-     * This is used in converting reference frames.
-     *
-     * See e.g. 3D Coordinate Transformations, Deakin, 1998.
-     *
-     * @private
-     * @param   {number[]} params - Transform parameters tx, ty, tz, s, rx, ry, rz..
-     * @param   {number[]} rates - Rate of change of transform parameters ṫx, ṫy, ṫz, ṡ, ṙx, ṙy, ṙz.
-     * @param   {number}   δt - Period between reference and observed epochs, t − t₀.
-     * @returns {Cartesian} Transformed point (without reference frame).
-     */
-    applyTransform(params, rates, δt)   {
-        // this point
-        const x1 = this.x, y1 = this.y, z1 = this.z;
-
-        // base parameters
-        const tx = params[0]/1000;                    // x-shift: normalise millimetres to metres
-        const ty = params[1]/1000;                    // y-shift: normalise millimetres to metres
-        const tz = params[2]/1000;                    // z-shift: normalise millimetres to metres
-        const s  = params[3]/1e9;                     // scale: normalise parts-per-billion
-        const rx = (params[4]/3600/1000).toRadians(); // x-rotation: normalise milliarcseconds to radians
-        const ry = (params[5]/3600/1000).toRadians(); // y-rotation: normalise milliarcseconds to radians
-        const rz = (params[6]/3600/1000).toRadians(); // z-rotation: normalise milliarcseconds to radians
-
-        // rate parameters
-        const ṫx = rates[0]/1000;                     // x-shift: normalise millimetres to metres
-        const ṫy = rates[1]/1000;                     // y-shift: normalise millimetres to metres
-        const ṫz = rates[2]/1000;                     // z-shift: normalise millimetres to metres
-        const ṡ  = rates[3]/1e9;                      // scale: normalise parts-per-billion
-        const ṙx = (rates[4]/3600/1000).toRadians();  // x-rotation: normalise milliarcseconds to radians
-        const ṙy = (rates[5]/3600/1000).toRadians();  // y-rotation: normalise milliarcseconds to radians
-        const ṙz = (rates[6]/3600/1000).toRadians();  // z-rotation: normalise milliarcseconds to radians
-
-        // combined (normalised) parameters
-        const T = { x: tx + ṫx*δt, y: ty + ṫy*δt, z: tz + ṫz*δt };
-        const R = { x: rx + ṙx*δt, y: ry + ṙy*δt, z: rz + ṙz*δt };
-        const S = 1 + s + ṡ*δt;
-
-        // apply transform (shift, scale, rotate)
-        const x2 = T.x + x1*S   - y1*R.z + z1*R.y;
-        const y2 = T.y + x1*R.z + y1*S   - z1*R.x;
-        const z2 = T.z - x1*R.y + y1*R.x + z1*S;
-
-        return new Cartesian_ReferenceFrame(x2, y2, z2);
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ cartesian point. TRF is shown if set, and
-     * observation epoch if different from reference epoch.
-     *
-     * @param   {number} [dp=0] - Number of decimal places to use.
-     * @returns {string} Comma-separated latitude/longitude.
-     */
-    toString(dp=0) {
-        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.toFixed(dp)},${y.toFixed(dp)},${z.toFixed(dp)}]${trf}`;
-    }
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { LatLonEllipsoidal_ReferenceFrame as default, Cartesian_ReferenceFrame as Cartesian, Dms };
diff --git a/src/js/geo/latlon-ellipsoidal-vincenty.js b/src/js/geo/latlon-ellipsoidal-vincenty.js
deleted file mode 100644
index cec22d6..0000000
--- a/src/js/geo/latlon-ellipsoidal-vincenty.js
+++ /dev/null
@@ -1,331 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Vincenty Direct and Inverse Solution of Geodesics on the Ellipsoid (c) Chris Veness 2002-2022  */
-/*                                                                                   MIT Licence  */
-/* www.ngs.noaa.gov/PUBS_LIB/inverse.pdf                                                          */
-/* www.movable-type.co.uk/scripts/latlong-vincenty.html                                           */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-ellipsoidal-vincenty                */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import LatLonEllipsoidal, { Dms } from './latlon-ellipsoidal.js';
-
-const π = Math.PI;
-const ε = Number.EPSILON;
-
-
-/**
- * Distances & bearings between points, and destination points given start points & initial bearings,
- * calculated on an ellipsoidal earth model using ‘direct and inverse solutions of geodesics on the
- * ellipsoid’ devised by Thaddeus Vincenty.
- *
- * From: T Vincenty, "Direct and Inverse Solutions of Geodesics on the Ellipsoid with application of
- * nested equations", Survey Review, vol XXIII no 176, 1975. www.ngs.noaa.gov/PUBS_LIB/inverse.pdf.
- *
- * @module latlon-ellipsoidal-vincenty
- */
-
-/* LatLonEllipsoidal_Vincenty - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-/**
- * Extends LatLonEllipsoidal with methods for calculating distances and bearings between points, and
- * destination points given distances and initial bearings, accurate to within 0.5mm distance,
- * 0.000015″ bearing.
- *
- * By default, these calculations are made on a WGS-84 ellipsoid. For geodesic calculations on other
- * ellipsoids, monkey-patch the LatLon point by setting the datum of ‘this’ point to make it appear
- * as a LatLonEllipsoidal_Datum or LatLonEllipsoidal_ReferenceFrame point: e.g.
- *
- *     import LatLon, { Dms } from '../latlon-ellipsoidal-vincenty.js';
- *     import { datums }      from '../latlon-ellipsoidal-datum.js';
- *     const le = new LatLon(50.065716, -5.713824);  // in OSGB-36
- *     const jog = new LatLon(58.644399, -3.068521); // in OSGB-36
- *     le.datum = datums.OSGB36;     // source point determines ellipsoid to use
- *     const d = le.distanceTo(jog); // = 969982.014; 27.848m more than on WGS-84 ellipsoid
- *
- * @extends LatLonEllipsoidal
- */
-class LatLonEllipsoidal_Vincenty extends LatLonEllipsoidal {
-
-    /**
-     * Returns the distance between ‘this’ point and destination point along a geodesic on the
-     * surface of the ellipsoid, using Vincenty inverse solution.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @returns {number} Distance in metres between points or NaN if failed to converge.
-     *
-     * @example
-     *   const p1 = new LatLon(50.06632, -5.71475);
-     *   const p2 = new LatLon(58.64402, -3.07009);
-     *   const d = p1.distanceTo(p2); // 969,954.166 m
-     */
-    distanceTo(point) {
-        try {
-            const dist = this.inverse(point).distance;
-            return Number(dist.toFixed(3)); // round to 1mm precision
-        } catch (e) {
-            if (e instanceof EvalError) return NaN; // λ > π or failed to converge
-            throw e;
-        }
-    }
-
-
-    /**
-     * Returns the initial bearing to travel along a geodesic from ‘this’ point to the given point,
-     * using Vincenty inverse solution.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @returns {number} Initial bearing in degrees from north (0°..360°) or NaN if failed to converge.
-     *
-     * @example
-     *   const p1 = new LatLon(50.06632, -5.71475);
-     *   const p2 = new LatLon(58.64402, -3.07009);
-     *   const b1 = p1.initialBearingTo(p2); // 9.1419°
-     */
-    initialBearingTo(point) {
-        try {
-            const brng = this.inverse(point).initialBearing;
-            return Number(brng.toFixed(7)); // round to 0.001″ precision
-        } catch (e) {
-            if (e instanceof EvalError) return NaN; // λ > π or failed to converge
-            throw e;
-        }
-    }
-
-
-    /**
-     * Returns the final bearing having travelled along a geodesic from ‘this’ point to the given
-     * point, using Vincenty inverse solution.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @returns {number} Final bearing in degrees from north (0°..360°) or NaN if failed to converge.
-     *
-     * @example
-     *   const p1 = new LatLon(50.06632, -5.71475);
-     *   const p2 = new LatLon(58.64402, -3.07009);
-     *   const b2 = p1.finalBearingTo(p2); // 11.2972°
-     */
-    finalBearingTo(point) {
-        try {
-            const brng = this.inverse(point).finalBearing;
-            return Number(brng.toFixed(7)); // round to 0.001″ precision
-        } catch (e) {
-            if (e instanceof EvalError) return NaN; // λ > π or failed to converge
-            throw e;
-        }
-    }
-
-
-    /**
-     * Returns the destination point having travelled the given distance along a geodesic given by
-     * initial bearing from ‘this’ point, using Vincenty direct solution.
-     *
-     * @param   {number} distance - Distance travelled along the geodesic in metres.
-     * @param   {number} initialBearing - Initial bearing in degrees from north.
-     * @returns {LatLon} Destination point.
-     *
-     * @example
-     *   const p1 = new LatLon(-37.95103, 144.42487);
-     *   const p2 = p1.destinationPoint(54972.271, 306.86816); // 37.6528°S, 143.9265°E
-     */
-    destinationPoint(distance, initialBearing) {
-        return this.direct(Number(distance), Number(initialBearing)).point;
-    }
-
-
-    /**
-     * Returns the final bearing having travelled along a geodesic given by initial bearing for a
-     * given distance from ‘this’ point, using Vincenty direct solution.
-     * TODO: arg order? (this is consistent with destinationPoint, but perhaps less intuitive)
-     *
-     * @param   {number} distance - Distance travelled along the geodesic in metres.
-     * @param   {LatLon} initialBearing - Initial bearing in degrees from north.
-     * @returns {number} Final bearing in degrees from north (0°..360°).
-     *
-     * @example
-     *   const p1 = new LatLon(-37.95103, 144.42487);
-     *   const b2 = p1.finalBearingOn(54972.271, 306.86816); // 307.1736°
-     */
-    finalBearingOn(distance, initialBearing) {
-        const brng = this.direct(Number(distance), Number(initialBearing)).finalBearing;
-        return Number(brng.toFixed(7)); // round to 0.001″ precision
-    }
-
-
-    /**
-     * Returns the point at given fraction between ‘this’ point and given point.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @param   {number} fraction - Fraction between the two points (0 = this point, 1 = specified point).
-     * @returns {LatLon} Intermediate point between this point and destination point.
-     *
-     * @example
-     *   const p1 = new LatLon(50.06632, -5.71475);
-     *   const p2 = new LatLon(58.64402, -3.07009);
-     *   const pInt = p1.intermediatePointTo(p2, 0.5); // 54.3639°N, 004.5304°W
-     */
-    intermediatePointTo(point, fraction) {
-        if (fraction == 0) return this;
-        if (fraction == 1) return point;
-
-        const inverse = this.inverse(point);
-        const dist = inverse.distance;
-        const brng = inverse.initialBearing;
-        return isNaN(brng) ? this : this.destinationPoint(dist*fraction, brng);
-    }
-
-
-    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-
-    /**
-     * Vincenty direct calculation.
-     *
-     * Ellipsoid parameters are taken from datum of 'this' point. Height is ignored.
-     *
-     * @private
-     * @param   {number} distance - Distance along bearing in metres.
-     * @param   {number} initialBearing - Initial bearing in degrees from north.
-     * @returns (Object} Object including point (destination point), finalBearing.
-     * @throws  {RangeError} Point must be on surface of ellipsoid.
-     * @throws  {EvalError}  Formula failed to converge.
-     */
-    direct(distance, initialBearing) {
-        if (isNaN(distance)) throw new TypeError(`invalid distance ${distance}`);
-        if (distance == 0) return { point: this, finalBearing: NaN, iterations: 0 };
-        if (isNaN(initialBearing)) throw new TypeError(`invalid bearing ${initialBearing}`);
-        if (this.height != 0) throw new RangeError('point must be on the surface of the ellipsoid');
-
-        const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();
-        const α1 = Number(initialBearing).toRadians();
-        const s = Number(distance);
-
-        // allow alternative ellipsoid to be specified
-        const ellipsoid = this.datum ? this.datum.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
-        const { a, b, f } = ellipsoid;
-
-        const sinα1 = Math.sin(α1);
-        const cosα1 = Math.cos(α1);
-
-        const tanU1 = (1-f) * Math.tan(φ1), cosU1 = 1 / Math.sqrt((1 + tanU1*tanU1)), sinU1 = tanU1 * cosU1;
-        const σ1 = Math.atan2(tanU1, cosα1); // σ1 = angular distance on the sphere from the equator to P1
-        const sinα = cosU1 * sinα1;          // α = azimuth of the geodesic at the equator
-        const cosSqα = 1 - sinα*sinα;
-        const uSq = cosSqα * (a*a - b*b) / (b*b);
-        const A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
-        const B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
-
-        let σ = s / (b*A), sinσ = null, cosσ = null; // σ = angular distance P₁ P₂ on the sphere
-        let cos2σₘ = null; // σₘ = angular distance on the sphere from the equator to the midpoint of the line
-
-        let σʹ = null, iterations = 0;
-        do {
-            cos2σₘ = Math.cos(2*σ1 + σ);
-            sinσ = Math.sin(σ);
-            cosσ = Math.cos(σ);
-            const Δσ = B*sinσ*(cos2σₘ+B/4*(cosσ*(-1+2*cos2σₘ*cos2σₘ)-B/6*cos2σₘ*(-3+4*sinσ*sinσ)*(-3+4*cos2σₘ*cos2σₘ)));
-            σʹ = σ;
-            σ = s / (b*A) + Δσ;
-        } while (Math.abs(σ-σʹ) > 1e-12 && ++iterations<100); // TV: 'iterate until negligible change in λ' (≈0.006mm)
-        if (iterations >= 100) throw new EvalError('Vincenty formula failed to converge'); // not possible?
-
-        const x = sinU1*sinσ - cosU1*cosσ*cosα1;
-        const φ2 = Math.atan2(sinU1*cosσ + cosU1*sinσ*cosα1, (1-f)*Math.sqrt(sinα*sinα + x*x));
-        const λ = Math.atan2(sinσ*sinα1, cosU1*cosσ - sinU1*sinσ*cosα1);
-        const C = f/16*cosSqα*(4+f*(4-3*cosSqα));
-        const L = λ - (1-C) * f * sinα * (σ + C*sinσ*(cos2σₘ+C*cosσ*(-1+2*cos2σₘ*cos2σₘ)));
-        const λ2 = λ1 + L;
-
-        const α2 = Math.atan2(sinα, -x);
-
-        const destinationPoint = new LatLonEllipsoidal_Vincenty(φ2.toDegrees(), λ2.toDegrees(), 0, this.datum);
-
-        return {
-            point:        destinationPoint,
-            finalBearing: Dms.wrap360(α2.toDegrees()),
-            iterations:   iterations,
-        };
-    }
-
-
-    /**
-     * Vincenty inverse calculation.
-     *
-     * Ellipsoid parameters are taken from datum of 'this' point. Height is ignored.
-     *
-     * @private
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @returns {Object} Object including distance, initialBearing, finalBearing.
-     * @throws  {TypeError}  Invalid point.
-     * @throws  {RangeError} Points must be on surface of ellipsoid.
-     * @throws  {EvalError}  Formula failed to converge.
-     */
-    inverse(point) {
-        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');
-
-        const p1 = this, p2 = point;
-        const φ1 = p1.lat.toRadians(), λ1 = p1.lon.toRadians();
-        const φ2 = p2.lat.toRadians(), λ2 = p2.lon.toRadians();
-
-        // allow alternative ellipsoid to be specified
-        const ellipsoid = this.datum ? this.datum.ellipsoid : LatLonEllipsoidal.ellipsoids.WGS84;
-        const { a, b, f } = ellipsoid;
-
-        const L = λ2 - λ1; // L = difference in longitude, U = reduced latitude, defined by tan U = (1-f)·tanφ.
-        const tanU1 = (1-f) * Math.tan(φ1), cosU1 = 1 / Math.sqrt((1 + tanU1*tanU1)), sinU1 = tanU1 * cosU1;
-        const tanU2 = (1-f) * Math.tan(φ2), cosU2 = 1 / Math.sqrt((1 + tanU2*tanU2)), sinU2 = tanU2 * cosU2;
-
-        const antipodal = Math.abs(L) > π/2 || Math.abs(φ2-φ1) > π/2;
-
-        let λ = L, sinλ = null, cosλ = null; // λ = difference in longitude on an auxiliary sphere
-        let σ = antipodal ? π : 0, sinσ = 0, cosσ = antipodal ? -1 : 1, sinSqσ = null; // σ = angular distance P₁ P₂ on the sphere
-        let cos2σₘ = 1;                      // σₘ = angular distance on the sphere from the equator to the midpoint of the line
-        let cosSqα = 1;                      // α = azimuth of the geodesic at the equator
-
-        let λʹ = null, iterations = 0;
-        do {
-            sinλ = Math.sin(λ);
-            cosλ = Math.cos(λ);
-            sinSqσ = (cosU2*sinλ)**2 + (cosU1*sinU2-sinU1*cosU2*cosλ)**2;
-            if (Math.abs(sinSqσ) < 1e-24) break;  // co-incident/antipodal points (σ < ≈0.006mm)
-            sinσ = Math.sqrt(sinSqσ);
-            cosσ = sinU1*sinU2 + cosU1*cosU2*cosλ;
-            σ = Math.atan2(sinσ, cosσ);
-            const sinα = cosU1 * cosU2 * sinλ / sinσ;
-            cosSqα = 1 - sinα*sinα;
-            cos2σₘ = (cosSqα != 0) ? (cosσ - 2*sinU1*sinU2/cosSqα) : 0; // on equatorial line cos²α = 0 (§6)
-            const C = f/16*cosSqα*(4+f*(4-3*cosSqα));
-            λʹ = λ;
-            λ = L + (1-C) * f * sinα * (σ + C*sinσ*(cos2σₘ+C*cosσ*(-1+2*cos2σₘ*cos2σₘ)));
-            const iterationCheck = antipodal ? Math.abs(λ)-π : Math.abs(λ);
-            if (iterationCheck > π) throw new EvalError('λ > π');
-        } while (Math.abs(λ-λʹ) > 1e-12 && ++iterations<1000); // TV: 'iterate until negligible change in λ' (≈0.006mm)
-        if (iterations >= 1000) throw new EvalError('Vincenty formula failed to converge');
-
-        const uSq = cosSqα * (a*a - b*b) / (b*b);
-        const A = 1 + uSq/16384*(4096+uSq*(-768+uSq*(320-175*uSq)));
-        const B = uSq/1024 * (256+uSq*(-128+uSq*(74-47*uSq)));
-        const Δσ = B*sinσ*(cos2σₘ+B/4*(cosσ*(-1+2*cos2σₘ*cos2σₘ)-B/6*cos2σₘ*(-3+4*sinσ*sinσ)*(-3+4*cos2σₘ*cos2σₘ)));
-
-        const s = b*A*(σ-Δσ); // s = length of the geodesic
-
-        // note special handling of exactly antipodal points where sin²σ = 0 (due to discontinuity
-        // atan2(0, 0) = 0 but atan2(ε, 0) = π/2 / 90°) - in which case bearing is always meridional,
-        // due north (or due south!)
-        // α = azimuths of the geodesic; α2 the direction P₁ P₂ produced
-        const α1 = Math.abs(sinSqσ) < ε ? 0 : Math.atan2(cosU2*sinλ,  cosU1*sinU2-sinU1*cosU2*cosλ);
-        const α2 = Math.abs(sinSqσ) < ε ? π : Math.atan2(cosU1*sinλ, -sinU1*cosU2+cosU1*sinU2*cosλ);
-
-        return {
-            distance:       s,
-            initialBearing: Math.abs(s) < ε ? NaN : Dms.wrap360(α1.toDegrees()),
-            finalBearing:   Math.abs(s) < ε ? NaN : Dms.wrap360(α2.toDegrees()),
-            iterations:     iterations,
-        };
-    }
-
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { LatLonEllipsoidal_Vincenty as default, Dms };
diff --git a/src/js/geo/latlon-ellipsoidal.js b/src/js/geo/latlon-ellipsoidal.js
deleted file mode 100644
index 46873f6..0000000
--- a/src/js/geo/latlon-ellipsoidal.js
+++ /dev/null
@@ -1,429 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Geodesy tools for an ellipsoidal earth model                       (c) Chris Veness 2005-2022  */
-/*                                                                                   MIT Licence  */
-/* Core class for latlon-ellipsoidal-datum & latlon-ellipsoidal-referenceframe.                   */
-/*                                                                                                */
-/* www.movable-type.co.uk/scripts/latlong-convert-coords.html                                     */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-ellipsoidal                         */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import Dms      from './dms.js';
-import Vector3d from './vector3d.js';
-
-
-/**
- * A latitude/longitude point defines a geographic location on or above/below the earth’s surface,
- * measured in degrees from the equator & the International Reference Meridian and in metres above
- * the ellipsoid, and based on a given datum.
- *
- * As so much modern geodesy is based on WGS-84 (as used by GPS), this module includes WGS-84
- * ellipsoid parameters, and it has methods for converting geodetic (latitude/longitude) points to/from
- * geocentric cartesian points; the latlon-ellipsoidal-datum and latlon-ellipsoidal-referenceframe
- * modules provide transformation parameters for converting between historical datums and between
- * modern reference frames.
- *
- * This module is used for both trigonometric geodesy (eg latlon-ellipsoidal-vincenty) and n-vector
- * geodesy (eg latlon-nvector-ellipsoidal), and also for UTM/MGRS mapping.
- *
- * @module latlon-ellipsoidal
- */
-
-
-/*
- * Ellipsoid parameters; exposed through static getter below.
- *
- * The only ellipsoid defined is WGS84, for use in utm/mgrs, vincenty, nvector.
- */
-const ellipsoids = {
-    WGS84: { a: 6378137, b: 6356752.314245, f: 1/298.257223563 },
-};
-
-
-/*
- * Datums; exposed through static getter below.
- *
- * The only datum defined is WGS84, for use in utm/mgrs, vincenty, nvector.
- */
-const datums = {
-    WGS84: { ellipsoid: ellipsoids.WGS84 },
-};
-
-
-// freeze static properties
-Object.freeze(ellipsoids.WGS84);
-Object.freeze(datums.WGS84);
-
-
-/* LatLonEllipsoidal - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-
-/**
- * Latitude/longitude points on an ellipsoidal model earth, with ellipsoid parameters and methods
- * for converting points to/from cartesian (ECEF) coordinates.
- *
- * This is the core class, which will usually be used via LatLonEllipsoidal_Datum or
- * LatLonEllipsoidal_ReferenceFrame.
- */
-class LatLonEllipsoidal {
-
-    /**
-     * Creates a geodetic latitude/longitude point on a (WGS84) ellipsoidal model earth.
-     *
-     * @param  {number} lat - Latitude (in degrees).
-     * @param  {number} lon - Longitude (in degrees).
-     * @param  {number} [height=0] - Height above ellipsoid in metres.
-     * @throws {TypeError} Invalid lat/lon/height.
-     *
-     * @example
-     *   import LatLon from '/js/geodesy/latlon-ellipsoidal.js';
-     *   const p = new LatLon(51.47788, -0.00147, 17);
-     */
-    constructor(lat, lon, height=0) {
-        if (isNaN(lat) || lat == null) throw new TypeError(`invalid lat ‘${lat}’`);
-        if (isNaN(lon) || lon == null) throw new TypeError(`invalid lon ‘${lon}’`);
-        if (isNaN(height) || height == null) throw new TypeError(`invalid height ‘${height}’`);
-
-        this._lat = Dms.wrap90(Number(lat));
-        this._lon = Dms.wrap180(Number(lon));
-        this._height = Number(height);
-    }
-
-
-    /**
-     * Latitude in degrees north from equator (including aliases lat, latitude): can be set as
-     * numeric or hexagesimal (deg-min-sec); returned as numeric.
-     */
-    get lat()       { return this._lat; }
-    get latitude()  { return this._lat; }
-    set lat(lat) {
-        this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(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}’`);
-    }
-
-    /**
-     * Longitude in degrees east from international reference meridian (including aliases lon, lng,
-     * longitude): can be set as numeric or hexagesimal (deg-min-sec); returned as numeric.
-     */
-    get lon()       { return this._lon; }
-    get lng()       { return this._lon; }
-    get longitude() { return this._lon; }
-    set lon(lon) {
-        this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(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}’`);
-    }
-    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}’`);
-    }
-
-    /**
-     * Height in metres above ellipsoid.
-     */
-    get height()       { return this._height; }
-    set height(height) { this._height = Number(height); if (isNaN(this._height)) throw new TypeError(`invalid height ‘${height}’`); }
-
-
-    /**
-     * Datum.
-     *
-     * Note this is replicated within LatLonEllipsoidal in order that a LatLonEllipsoidal object can
-     * be monkey-patched to look like a LatLonEllipsoidal_Datum, for Vincenty calculations on
-     * different ellipsoids.
-     *
-     * @private
-     */
-    get datum()      { return this._datum; }
-    set datum(datum) { this._datum = datum; }
-
-
-    /**
-     * Ellipsoids with their parameters; this module only defines WGS84 parameters a = 6378137, b =
-     * 6356752.314245, f = 1/298.257223563.
-     *
-     * @example
-     *   const a = LatLon.ellipsoids.WGS84.a; // 6378137
-     */
-    static get ellipsoids() {
-        return ellipsoids;
-    }
-
-    /**
-     * Datums; this module only defines WGS84 datum, hence no datum transformations.
-     *
-     * @example
-     *   const a = LatLon.datums.WGS84.ellipsoid.a; // 6377563.396
-     */
-    static get datums() {
-        return datums;
-    }
-
-
-    /**
-     * Parses a latitude/longitude point from a variety of formats.
-     *
-     * Latitude & longitude (in degrees) can be supplied as two separate parameters, as a single
-     * comma-separated lat/lon string, or as a single object with { lat, lon } or GeoJSON properties.
-     *
-     * The latitude/longitude values may be numeric or strings; they may be signed decimal or
-     * deg-min-sec (hexagesimal) suffixed by compass direction (NSEW); a variety of separators are
-     * accepted. Examples -3.62, '3 37 12W', '3°37′12″W'.
-     *
-     * Thousands/decimal separators must be comma/dot; use Dms.fromLocale to convert locale-specific
-     * thousands/decimal separators.
-     *
-     * @param   {number|string|Object} lat|latlon - Latitude (in degrees), or comma-separated lat/lon, or lat/lon object.
-     * @param   {number}               [lon]      - Longitude (in degrees).
-     * @param   {number}               [height=0] - Height above ellipsoid in metres.
-     * @returns {LatLon} Latitude/longitude point on WGS84 ellipsoidal model earth.
-     * @throws  {TypeError} Invalid coordinate.
-     *
-     * @example
-     *   const p1 = LatLon.parse(51.47788, -0.00147);              // numeric pair
-     *   const p2 = LatLon.parse('51°28′40″N, 000°00′05″W', 17);   // dms string + height
-     *   const p3 = LatLon.parse({ lat: 52.205, lon: 0.119 }, 17); // { lat, lon } object numeric + height
-     */
-    static parse(...args) {
-        if (args.length == 0) throw new TypeError('invalid (empty) point');
-
-        let lat=undefined, lon=undefined, height=undefined;
-
-        // single { lat, lon } object
-        if (typeof args[0]=='object' && (args.length==1 || !isNaN(parseFloat(args[1])))) {
-            const ll = args[0];
-            if (ll.type == 'Point' && Array.isArray(ll.coordinates)) { // GeoJSON
-                [ lon, lat, height ] = ll.coordinates;
-                height = height || 0;
-            } else { // regular { lat, lon } object
-                if (ll.latitude  != undefined) lat = ll.latitude;
-                if (ll.lat       != undefined) lat = ll.lat;
-                if (ll.longitude != undefined) lon = ll.longitude;
-                if (ll.lng       != undefined) lon = ll.lng;
-                if (ll.lon       != undefined) lon = ll.lon;
-                if (ll.height    != undefined) height = ll.height;
-                lat = Dms.wrap90(Dms.parse(lat));
-                lon = Dms.wrap180(Dms.parse(lon));
-            }
-            if (args[1] != undefined) height = args[1];
-            if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${JSON.stringify(args[0])}’`);
-        }
-
-        // single comma-separated lat/lon
-        if (typeof args[0] == 'string' && args[0].split(',').length == 2) {
-            [ lat, lon ] = args[0].split(',');
-            lat = Dms.wrap90(Dms.parse(lat));
-            lon = Dms.wrap180(Dms.parse(lon));
-            height = args[1] || 0;
-            if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args[0]}’`);
-        }
-
-        // regular (lat, lon) arguments
-        if (lat==undefined && lon==undefined) {
-            [ lat, lon ] = args;
-            lat = Dms.wrap90(Dms.parse(lat));
-            lon = Dms.wrap180(Dms.parse(lon));
-            height = args[2] || 0;
-            if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`);
-        }
-
-        return new this(lat, lon, height); // 'new this' as may return subclassed types
-    }
-
-
-    /**
-     * Converts ‘this’ point from (geodetic) latitude/longitude coordinates to (geocentric)
-     * cartesian (x/y/z) coordinates.
-     *
-     * @returns {Cartesian} Cartesian point equivalent to lat/lon point, with x, y, z in metres from
-     *   earth centre.
-     */
-    toCartesian() {
-        // x = (ν+h)⋅cosφ⋅cosλ, y = (ν+h)⋅cosφ⋅sinλ, z = (ν⋅(1-e²)+h)⋅sinφ
-        // where ν = a/√(1−e²⋅sinφ⋅sinφ), e² = (a²-b²)/a² or (better conditioned) 2⋅f-f²
-        const ellipsoid = this.datum
-            ? this.datum.ellipsoid
-            : this.referenceFrame ? this.referenceFrame.ellipsoid : ellipsoids.WGS84;
-
-        const φ = this.lat.toRadians();
-        const λ = this.lon.toRadians();
-        const h = this.height;
-        const { a, f } = ellipsoid;
-
-        const sinφ = Math.sin(φ), cosφ = Math.cos(φ);
-        const sinλ = Math.sin(λ), cosλ = Math.cos(λ);
-
-        const eSq = 2*f - f*f;                      // 1st eccentricity squared ≡ (a²-b²)/a²
-        const ν = a / Math.sqrt(1 - eSq*sinφ*sinφ); // radius of curvature in prime vertical
-
-        const x = (ν+h) * cosφ * cosλ;
-        const y = (ν+h) * cosφ * sinλ;
-        const z = (ν*(1-eSq)+h) * sinφ;
-
-        return new Cartesian(x, y, z);
-    }
-
-
-    /**
-     * Checks if another point is equal to ‘this’ point.
-     *
-     * @param   {LatLon} point - Point to be compared against this point.
-     * @returns {bool} True if points have identical latitude, longitude, height, and datum/referenceFrame.
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(52.205, 0.119);
-     *   const equal = p1.equals(p2); // true
-     */
-    equals(point) {
-        if (!(point instanceof LatLonEllipsoidal)) throw new TypeError(`invalid point ‘${point}’`);
-
-        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;
-        if (this.referenceFrame != point.referenceFrame) return false;
-        if (this.epoch != point.epoch) return false;
-
-        return true;
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ point, formatted as degrees, degrees+minutes, or
-     * degrees+minutes+seconds.
-     *
-     * @param   {string} [format=d] - Format point as 'd', 'dm', 'dms', or 'n' for signed numeric.
-     * @param   {number} [dp=4|2|0] - Number of decimal places to use: default 4 for d, 2 for dm, 0 for dms.
-     * @param   {number} [dpHeight=null] - Number of decimal places to use for height; default is no height display.
-     * @returns {string} Comma-separated formatted latitude/longitude.
-     * @throws  {RangeError} Invalid format.
-     *
-     * @example
-     *   const greenwich = new LatLon(51.47788, -0.00147, 46);
-     *   const d = greenwich.toString();                        // 51.4779°N, 000.0015°W
-     *   const dms = greenwich.toString('dms', 2);              // 51°28′40″N, 000°00′05″W
-     *   const [lat, lon] = greenwich.toString('n').split(','); // 51.4779, -0.0015
-     *   const dmsh = greenwich.toString('dms', 0, 0);          // 51°28′40″N, 000°00′06″W +46m
-     */
-    toString(format='d', dp=undefined, dpHeight=null) {
-        // 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}’`);
-
-        const height = (this.height>=0 ? ' +' : ' ') + this.height.toFixed(dpHeight) + 'm';
-        if (format == 'n') { // signed numeric degrees
-            if (dp == undefined) dp = 4;
-            const lat = this.lat.toFixed(dp);
-            const lon = this.lon.toFixed(dp);
-            return `${lat}, ${lon}${dpHeight==null ? '' : height}`;
-        }
-
-        const lat = Dms.toLat(this.lat, format, dp);
-        const lon = Dms.toLon(this.lon, format, dp);
-
-        return `${lat}, ${lon}${dpHeight==null ? '' : height}`;
-    }
-
-}
-
-
-/* Cartesian  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * ECEF (earth-centered earth-fixed) geocentric cartesian coordinates.
- *
- * @extends Vector3d
- */
-class Cartesian extends Vector3d {
-
-    /**
-     * Creates cartesian coordinate representing ECEF (earth-centric earth-fixed) point.
-     *
-     * @param {number} x - X coordinate in metres (=> 0°N,0°E).
-     * @param {number} y - Y coordinate in metres (=> 0°N,90°E).
-     * @param {number} z - Z coordinate in metres (=> 90°N).
-     *
-     * @example
-     *   import { Cartesian } from '/js/geodesy/latlon-ellipsoidal.js';
-     *   const coord = new Cartesian(3980581.210, -111.159, 4966824.522);
-     */
-    constructor(x, y, z) {
-        super(x, y, z); // arguably redundant constructor, but specifies units & axes
-    }
-
-
-    /**
-     * Converts ‘this’ (geocentric) cartesian (x/y/z) coordinate to (geodetic) latitude/longitude
-     * point on specified ellipsoid.
-     *
-     * Uses Bowring’s (1985) formulation for μm precision in concise form; ‘The accuracy of geodetic
-     * latitude and height equations’, B R Bowring, Survey Review vol 28, 218, Oct 1985.
-     *
-     * @param   {LatLon.ellipsoids} [ellipsoid=WGS84] - Ellipsoid to use when converting point.
-     * @returns {LatLon} Latitude/longitude point defined by cartesian coordinates, on given ellipsoid.
-     * @throws  {TypeError} Invalid ellipsoid.
-     *
-     * @example
-     *   const c = new Cartesian(4027893.924, 307041.993, 4919474.294);
-     *   const p = c.toLatLon(); // 50.7978°N, 004.3592°E
-     */
-    toLatLon(ellipsoid=ellipsoids.WGS84) {
-        // note ellipsoid is available as a parameter for when toLatLon gets subclassed to
-        // Ellipsoidal_Datum / Ellipsoidal_Referenceframe.
-        if (!ellipsoid || !ellipsoid.a) throw new TypeError(`invalid ellipsoid ‘${ellipsoid}’`);
-
-        const { x, y, z } = this;
-        const { a, b, f } = ellipsoid;
-
-        const e2 = 2*f - f*f;           // 1st eccentricity squared ≡ (a²−b²)/a²
-        const ε2 = e2 / (1-e2);         // 2nd eccentricity squared ≡ (a²−b²)/b²
-        const p = Math.sqrt(x*x + y*y); // distance from minor axis
-        const R = Math.sqrt(p*p + z*z); // polar radius
-
-        // parametric latitude (Bowring eqn.17, replacing tanβ = z·a / p·b)
-        const tanβ = (b*z)/(a*p) * (1+ε2*b/R);
-        const sinβ = tanβ / Math.sqrt(1+tanβ*tanβ);
-        const cosβ = sinβ / tanβ;
-
-        // geodetic latitude (Bowring eqn.18: tanφ = z+ε²⋅b⋅sin³β / p−e²⋅cos³β)
-        const φ = isNaN(cosβ) ? 0 : Math.atan2(z + ε2*b*sinβ*sinβ*sinβ, p - e2*a*cosβ*cosβ*cosβ);
-
-        // longitude
-        const λ = Math.atan2(y, x);
-
-        // height above ellipsoid (Bowring eqn.7)
-        const sinφ = Math.sin(φ), cosφ = Math.cos(φ);
-        const ν = a / Math.sqrt(1-e2*sinφ*sinφ); // length of the normal terminated by the minor axis
-        const h = p*cosφ + z*sinφ - (a*a/ν);
-
-        const point = new LatLonEllipsoidal(φ.toDegrees(), λ.toDegrees(), h);
-
-        return point;
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ cartesian point.
-     *
-     * @param   {number} [dp=0] - Number of decimal places to use.
-     * @returns {string} Comma-separated latitude/longitude.
-     */
-    toString(dp=0) {
-        const x = this.x.toFixed(dp), y = this.y.toFixed(dp), z = this.z.toFixed(dp);
-        return `[${x},${y},${z}]`;
-    }
-
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { LatLonEllipsoidal as default, Cartesian, Vector3d, Dms };
diff --git a/src/js/geo/latlon-nvector-ellipsoidal.js b/src/js/geo/latlon-nvector-ellipsoidal.js
deleted file mode 100644
index c31378a..0000000
--- a/src/js/geo/latlon-nvector-ellipsoidal.js
+++ /dev/null
@@ -1,445 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Vector-based ellipsoidal geodetic (latitude/longitude) functions   (c) Chris Veness 2015-2021  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong-vectors.html                                            */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-nvector-ellipsoidal                 */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import LatLonEllipsoidal, { Cartesian, Vector3d, Dms } from './latlon-ellipsoidal.js';
-
-
-/**
- * Tools for working with points on (ellipsoidal models of) the earth’s surface using a vector-based
- * approach using ‘n-vectors’ (rather than the more common spherical trigonometry).
- *
- * Based on Kenneth Gade’s ‘Non-singular Horizontal Position Representation’.
- *
- * Note that these formulations take x => 0°N,0°E, y => 0°N,90°E, z => 90°N (in order that n-vector
- * = cartesian vector at 0°N,0°E); Gade uses x => 90°N, y => 0°N,90°E, z => 0°N,0°E.
- *
- * @module latlon-nvector-ellipsoidal
- */
-
-
-/* LatLon_NvectorEllipsoidal  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Latitude/longitude points on an ellipsoidal model earth augmented with methods for calculating
- * delta vectors between points, and converting to n-vectors.
- *
- * @extends LatLonEllipsoidal
- */
-class LatLon_NvectorEllipsoidal extends LatLonEllipsoidal {
-
-    /**
-     * Calculates delta from ‘this’ point to supplied point.
-     *
-     * The delta is given as a north-east-down NED vector. Note that this is a linear delta,
-     * unrelated to a geodesic on the ellipsoid.
-     *
-     * Points need not be defined on the same datum.
-     *
-     * @param   {LatLon} point - Point delta is to be determined to.
-     * @returns {Ned} Delta from ‘this’ point to supplied point in local tangent plane of this point.
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const a = new LatLon(49.66618, 3.45063, 99);
-     *   const b = new LatLon(48.88667, 2.37472, 64);
-     *   const delta = a.deltaTo(b);   // [N:-86127,E:-78901,D:1104]
-     *   const dist = delta.length;    // 116809.178 m
-     *   const brng = delta.bearing;   // 222.493°
-     *   const elev = delta.elevation; //  -0.5416°
-     */
-    deltaTo(point) {
-        if (!(point instanceof LatLonEllipsoidal)) throw new TypeError(`invalid point ‘${point}’`);
-
-        // get delta in cartesian frame
-        const c1 = this.toCartesian();
-        const c2 = point.toCartesian();
-        const δc = c2.minus(c1);
-
-        // get local (n-vector) coordinate frame
-        const n1 = this.toNvector();
-        const a = new Vector3d(0, 0, 1); // axis vector pointing to 90°N
-        const d = n1.negate();           // down (pointing opposite to n-vector)
-        const e = a.cross(n1).unit();    // east (pointing perpendicular to the plane)
-        const n = e.cross(d);            // north (by right hand rule)
-
-        // rotation matrix is built from n-vector coordinate frame axes (using row vectors)
-        const r = [
-            [ n.x, n.y, n.z ],
-            [ e.x, e.y, e.z ],
-            [ d.x, d.y, d.z ],
-        ];
-
-        // apply rotation to δc to get delta in n-vector reference frame
-        const δn = new Cartesian(
-            r[0][0]*δc.x + r[0][1]*δc.y + r[0][2]*δc.z,
-            r[1][0]*δc.x + r[1][1]*δc.y + r[1][2]*δc.z,
-            r[2][0]*δc.x + r[2][1]*δc.y + r[2][2]*δc.z,
-        );
-
-        return new Ned(δn.x, δn.y, δn.z);
-    }
-
-
-    /**
-     * Calculates destination point using supplied delta from ‘this’ point.
-     *
-     * The delta is given as a north-east-down NED vector. Note that this is a linear delta,
-     * unrelated to a geodesic on the ellipsoid.
-     *
-     * @param   {Ned}    delta - Delta from ‘this’ point to supplied point in local tangent plane of this point.
-     * @returns {LatLon} Destination point.
-     *
-     * @example
-     *   const a = new LatLon(49.66618, 3.45063, 99);
-     *   const delta = Ned.fromDistanceBearingElevation(116809.178, 222.493, -0.5416); // [N:-86127,E:-78901,D:1104]
-     *   const b = a.destinationPoint(delta);                                          // 48.8867°N, 002.3747°E
-     */
-    destinationPoint(delta) {
-        if (!(delta instanceof Ned)) throw new TypeError('delta is not Ned object');
-
-        // convert North-East-Down delta to standard x/y/z vector in coordinate frame of n-vector
-        const δn = new Vector3d(delta.north, delta.east, delta.down);
-
-        // get local (n-vector) coordinate frame
-        const n1 = this.toNvector();
-        const a = new Vector3d(0, 0, 1); // axis vector pointing to 90°N
-        const d = n1.negate();           // down (pointing opposite to n-vector)
-        const e = a.cross(n1).unit();    // east (pointing perpendicular to the plane)
-        const n = e.cross(d);            // north (by right hand rule)
-
-        // rotation matrix is built from n-vector coordinate frame axes (using column vectors)
-        const r = [
-            [ n.x, e.x, d.x ],
-            [ n.y, e.y, d.y ],
-            [ n.z, e.z, d.z ],
-        ];
-
-        // apply rotation to δn to get delta in cartesian (ECEF) coordinate reference frame
-        const δc = new Cartesian(
-            r[0][0]*δn.x + r[0][1]*δn.y + r[0][2]*δn.z,
-            r[1][0]*δn.x + r[1][1]*δn.y + r[1][2]*δn.z,
-            r[2][0]*δn.x + r[2][1]*δn.y + r[2][2]*δn.z,
-        );
-
-        // apply (cartesian) delta to c1 to obtain destination point as cartesian coordinate
-        const c1 = this.toCartesian();              // convert this LatLon to Cartesian
-        const v2 = c1.plus(δc);                     // the plus() gives us a plain vector,..
-        const c2 = new Cartesian(v2.x, v2.y, v2.z); // ... need to convert it to Cartesian to get LatLon
-
-        // return destination cartesian coordinate as latitude/longitude
-        return c2.toLatLon();
-    }
-
-
-    /**
-     * Converts ‘this’ lat/lon point to n-vector (normal to the earth's surface).
-     *
-     * @returns {Nvector} N-vector representing lat/lon point.
-     *
-     * @example
-     *   const p = new LatLon(45, 45);
-     *   const n = p.toNvector(); // [0.5000,0.5000,0.7071]
-     */
-    toNvector() { // note: replicated in LatLonNvectorSpherical
-        const φ = this.lat.toRadians();
-        const λ = this.lon.toRadians();
-
-        const sinφ = Math.sin(φ), cosφ = Math.cos(φ);
-        const sinλ = Math.sin(λ), cosλ = Math.cos(λ);
-
-        // right-handed vector: x -> 0°E,0°N; y -> 90°E,0°N, z -> 90°N
-        const x = cosφ * cosλ;
-        const y = cosφ * sinλ;
-        const z = sinφ;
-
-        return new NvectorEllipsoidal(x, y, z, this.h, this.datum);
-    }
-
-
-    /**
-     * Converts ‘this’ point from (geodetic) latitude/longitude coordinates to (geocentric) cartesian
-     * (x/y/z) coordinates.
-     *
-     * @returns {Cartesian} Cartesian point equivalent to lat/lon point, with x, y, z in metres from
-     *   earth centre.
-     */
-    toCartesian() {
-        const c = super.toCartesian();  // c is 'Cartesian'
-
-        // return Cartesian_Nvector to have toNvector() available as method of exported LatLon
-        return new Cartesian_Nvector(c.x, c.y, c.z);
-    }
-
-}
-
-
-/* Nvector - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-
-/**
- * An n-vector is a position representation using a (unit) vector normal to the Earth ellipsoid.
- * Unlike latitude/longitude points, n-vectors have no singularities or discontinuities.
- *
- * For many applications, n-vectors are more convenient to work with than other position
- * representations such as latitude/longitude, earth-centred earth-fixed (ECEF) vectors, UTM
- * coordinates, etc.
- *
- * @extends Vector3d
- */
-class NvectorEllipsoidal extends Vector3d {
-
-    // note commonality with latlon-nvector-spherical
-
-    /**
-     * Creates a 3d n-vector normal to the Earth's surface.
-     *
-     * @param {number} x - X component of n-vector (towards 0°N, 0°E).
-     * @param {number} y - Y component of n-vector (towards 0°N, 90°E).
-     * @param {number} z - Z component of n-vector (towards 90°N).
-     * @param {number} [h=0] - Height above ellipsoid surface in metres.
-     * @param {LatLon.datums} [datum=WGS84] - Datum this n-vector is defined within.
-     */
-    constructor(x, y, z, h=0, datum=LatLonEllipsoidal.datums.WGS84) {
-        const u = new Vector3d(x, y, z).unit(); // n-vectors are always normalised
-
-        super(u.x, u.y, u.z);
-
-        this.h = Number(h);
-        this.datum = datum;
-    }
-
-
-    /**
-     * Converts ‘this’ n-vector to latitude/longitude point.
-     *
-     * @returns {LatLon} Latitude/longitude point equivalent to this n-vector.
-     *
-     * @example
-     *   const p = new Nvector(0.500000, 0.500000, 0.707107).toLatLon(); // 45.0000°N, 045.0000°E
-     */
-    toLatLon() {
-        // tanφ = z / √(x²+y²), tanλ = y / x (same as spherical calculation)
-
-        const { x, y, z } = this;
-
-        const φ = Math.atan2(z, Math.sqrt(x*x + y*y));
-        const λ = Math.atan2(y, x);
-
-        return new LatLon_NvectorEllipsoidal(φ.toDegrees(), λ.toDegrees(), this.h, this.datum);
-    }
-
-
-    /**
-     * Converts ‘this’ n-vector to cartesian coordinate.
-     *
-     * qv Gade 2010 ‘A Non-singular Horizontal Position Representation’ eqn 22
-     *
-     * @returns {Cartesian} Cartesian coordinate equivalent to this n-vector.
-     *
-     * @example
-     *   const c = new Nvector(0.500000, 0.500000, 0.707107).toCartesian(); // [3194419,3194419,4487349]
-     *   const p = c.toLatLon();                                            // 45.0000°N, 045.0000°E
-     */
-    toCartesian() {
-        const { b, f } = this.datum.ellipsoid;
-        const { x, y, z, h } = this;
-
-        const m = (1-f) * (1-f); // (1−f)² = b²/a²
-        const n = b / Math.sqrt(x*x/m + y*y/m + z*z);
-
-        const xʹ = n * x / m + x*h;
-        const yʹ = n * y / m + y*h;
-        const zʹ = n * z     + z*h;
-
-        return new Cartesian_Nvector(xʹ, yʹ, zʹ);
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ (unit) n-vector. Height component is only shown if
-     * dpHeight is specified.
-     *
-     * @param   {number} [dp=3] - Number of decimal places to display.
-     * @param   {number} [dpHeight=null] - Number of decimal places to use for height; default is no height display.
-     * @returns {string} Comma-separated x, y, z, h values.
-     *
-     * @example
-     *   new Nvector(0.5000, 0.5000, 0.7071).toString();        // [0.500,0.500,0.707]
-     *   new Nvector(0.5000, 0.5000, 0.7071, 1).toString(6, 0); // [0.500002,0.500002,0.707103+1m]
-     */
-    toString(dp=3, dpHeight=null) {
-        const { x, y, z } = this;
-        const h = `${this.h>=0 ? '+' : ''}${this.h.toFixed(dpHeight)}m`;
-
-        return `[${x.toFixed(dp)},${y.toFixed(dp)},${z.toFixed(dp)}${dpHeight==null ? '' : h}]`;
-    }
-
-}
-
-
-/* Cartesian  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Cartesian_Nvector extends Cartesian with method to convert cartesian coordinates to n-vectors.
- *
- * @extends Cartesian
- */
-class Cartesian_Nvector extends Cartesian {
-
-
-    /**
-     * Converts ‘this’ cartesian coordinate to an n-vector.
-     *
-     * qv Gade 2010 ‘A Non-singular Horizontal Position Representation’ eqn 23
-     *
-     * @param   {LatLon.datums} [datum=WGS84] - Datum to use for conversion.
-     * @returns {Nvector} N-vector equivalent to this cartesian coordinate.
-     *
-     * @example
-     *   const c = new Cartesian(3980581, 97, 4966825);
-     *   const n = c.toNvector(); // { x: 0.6228, y: 0.0000, z: 0.7824, h: 0.0000 }
-     */
-    toNvector(datum=LatLonEllipsoidal.datums.WGS84) {
-        const { a, f } = datum.ellipsoid;
-        const { x, y, z } = this;
-
-        const e2 = 2*f - f*f; // e² = 1st eccentricity squared ≡ (a²-b²)/a²
-        const e4 = e2*e2;     // e⁴
-
-        const p = (x*x + y*y) / (a*a);
-        const q = z*z * (1-e2) / (a*a);
-        const r = (p + q - e4) / 6;
-        const s = (e4*p*q) / (4*r*r*r);
-        const t = Math.cbrt(1 + s + Math.sqrt(2*s+s*s));
-        const u = r * (1 + t + 1/t);
-        const v = Math.sqrt(u*u + e4*q);
-        const w = e2 * (u + v - q) / (2*v);
-        const k = Math.sqrt(u + v + w*w) - w;
-        const d = k * Math.sqrt(x*x + y*y) / (k + e2);
-
-        const tmp = 1 / Math.sqrt(d*d + z*z);
-        const xʹ = tmp * k/(k+e2) * x;
-        const yʹ = tmp * k/(k+e2) * y;
-        const zʹ = tmp * z;
-        const h = (k + e2 - 1)/k * Math.sqrt(d*d + z*z);
-
-        const n = new NvectorEllipsoidal(xʹ, yʹ, zʹ, h, datum);
-
-        return n;
-    }
-
-}
-
-
-/* Ned  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * North-east-down (NED), also known as local tangent plane (LTP), is a vector in the local
- * coordinate frame of a body.
- */
-class Ned {
-
-    /**
-     * Creates North-East-Down vector.
-     *
-     * @param {number} north - North component in metres.
-     * @param {number} east - East component in metres.
-     * @param {number} down - Down component (normal to the surface of the ellipsoid) in metres.
-     *
-     * @example
-     *   import { Ned } from '/js/geodesy/latlon-nvector-ellipsoidal.js';
-     *   const delta = new Ned(110569, 111297, 1936); // [N:110569,E:111297,D:1936]
-     */
-    constructor(north, east, down) {
-        this.north = north;
-        this.east = east;
-        this.down = down;
-    }
-
-
-    /**
-     * Length of NED vector.
-     *
-     * @returns {number} Length of NED vector in metres.
-     */
-    get length() {
-        const { north, east, down } = this;
-
-        return Math.sqrt(north*north + east*east + down*down);
-    }
-
-
-    /**
-     * Bearing of NED vector.
-     *
-     * @returns {number} Bearing of NED vector in degrees from north.
-     */
-    get bearing() {
-        const θ = Math.atan2(this.east, this.north);
-
-        return Dms.wrap360(θ.toDegrees()); // normalise to range 0..360°
-    }
-
-
-    /**
-     * Elevation of NED vector.
-     *
-     * @returns {number} Elevation of NED vector in degrees from horizontal (ie tangent to ellipsoid surface).
-     */
-    get elevation() {
-        const α = Math.asin(this.down/this.length);
-
-        return -α.toDegrees();
-    }
-
-
-    /**
-     * Creates North-East-Down vector from distance, bearing, & elevation (in local coordinate system).
-     *
-     * @param   {number} dist - Length of NED vector in metres.
-     * @param   {number} brng - Bearing (in degrees from north) of NED vector .
-     * @param   {number} elev - Elevation (in degrees from local coordinate frame horizontal) of NED vector.
-     * @returns {Ned} North-East-Down vector equivalent to distance, bearing, elevation.
-     *
-     * @example
-     *   const delta = Ned.fromDistanceBearingElevation(116809.178, 222.493, -0.5416); // [N:-86127,E:-78901,D:1104]
-     */
-    static fromDistanceBearingElevation(dist, brng, elev) {
-        const θ = Number(brng).toRadians();
-        const α = Number(elev).toRadians();
-        dist = Number(dist);
-
-        const sinθ = Math.sin(θ), cosθ = Math.cos(θ);
-        const sinα = Math.sin(α), cosα = Math.cos(α);
-
-        const n = cosθ * dist*cosα;
-        const e = sinθ * dist*cosα;
-        const d = -sinα * dist;
-
-        return new Ned(n, e, d);
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ NED vector.
-     *
-     * @param   {number} [dp=0] - Number of decimal places to display.
-     * @returns {string} Comma-separated (labelled) n, e, d values.
-     */
-    toString(dp=0) {
-        return `[N:${this.north.toFixed(dp)},E:${this.east.toFixed(dp)},D:${this.down.toFixed(dp)}]`;
-    }
-
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { LatLon_NvectorEllipsoidal as default, NvectorEllipsoidal as Nvector, Cartesian_Nvector as Cartesian, Ned, Dms };
diff --git a/src/js/geo/latlon-nvector-spherical.js b/src/js/geo/latlon-nvector-spherical.js
deleted file mode 100644
index 4d94c43..0000000
--- a/src/js/geo/latlon-nvector-spherical.js
+++ /dev/null
@@ -1,1021 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Vector-based spherical geodetic (latitude/longitude) functions     (c) Chris Veness 2011-2022  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong-vectors.html                                            */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-nvector-spherical                   */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import Vector3d from './vector3d.js';
-import Dms      from './dms.js';
-
-const π = Math.PI;
-
-
-/**
- * Tools for working with points and paths on (a spherical model of) the earth’s surface using a
- * vector-based approach using ‘n-vectors’. In contrast to the more common spherical trigonometry,
- * a vector-based approach makes many calculations much simpler, and easier to follow.
- *
- * Based on Kenneth Gade’s ‘Non-singular Horizontal Position Representation’.
- *
- * Note that these formulations take x => 0°N,0°E, y => 0°N,90°E, z => 90°N; Gade uses x => 90°N,
- * y => 0°N,90°E, z => 0°N,0°E.
- *
- * Note also that on a spherical model earth, an n-vector is equivalent to a normalised version of
- * an (ECEF) cartesian coordinate.
- *
- * @module latlon-nvector-spherical
- */
-
-
-/* LatLonNvectorSpherical - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Latitude/longitude points on an spherical model earth, and methods for calculating distances,
- * bearings, destinations, etc on great circle paths.
- */
-class LatLonNvectorSpherical {
-
-    /**
-     * Creates a latitude/longitude point on the earth’s surface, using a spherical model earth.
-     *
-     * @param  {number} lat - Latitude (in degrees).
-     * @param  {number} lon - Longitude (in degrees).
-     * @throws {TypeError} Invalid lat/lon.
-     *
-     * @example
-     *   import LatLon from '/js/geodesy/latlon-nvector-spherical.js';
-     *   const p = new LatLon(52.205, 0.119);
-     */
-    constructor(lat, lon) {
-        if (isNaN(lat)) throw new TypeError(`invalid lat ‘${lat}’`);
-        if (isNaN(lon)) throw new TypeError(`invalid lon ‘${lon}’`);
-
-        this._lat = Dms.wrap90(Number(lat));
-        this._lon = Dms.wrap180(Number(lon));
-    }
-
-
-    /**
-     * Latitude in degrees north from equator (including aliases lat, latitude): can be set as
-     * numeric or hexagesimal (deg-min-sec); returned as numeric.
-     */
-    get lat()       { return this._lat; }
-    get latitude()  { return this._lat; }
-    set lat(lat) {
-        this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(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}’`);
-    }
-
-    /**
-     * Longitude in degrees east from international reference meridian (including aliases lon, lng,
-     * longitude): can be set as numeric or hexagesimal (deg-min-sec); returned as numeric.
-     */
-    get lon()       { return this._lon; }
-    get lng()       { return this._lon; }
-    get longitude() { return this._lon; }
-    set lon(lon) {
-        this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(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}’`);
-    }
-    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}’`);
-    }
-
-
-    /** Conversion factors; 1000 * LatLon.metresToKm gives 1. */
-    static get metresToKm()            { return 1/1000; }
-    /** Conversion factors; 1000 * LatLon.metresToMiles gives 0.621371192237334. */
-    static get metresToMiles()         { return 1/1609.344; }
-    /** Conversion factors; 1000 * LatLon.metresToMiles gives 0.5399568034557236. */
-    static get metresToNauticalMiles() { return 1/1852; }
-
-
-    // TODO: is it worth LatLon.parse() for the n-vector version?
-
-
-    /**
-     * Converts ‘this’ latitude/longitude point to an n-vector (normal to earth's surface).
-     *
-     * @returns {Nvector} Normalised n-vector representing lat/lon point.
-     *
-     * @example
-     *   const p = new LatLon(45, 45);
-     *   const v = p.toNvector();      // [0.5000,0.5000,0.7071]
-     */
-    toNvector() { // note: replicated in LatLon_NvectorEllipsoidal
-        const φ = this.lat.toRadians();
-        const λ = this.lon.toRadians();
-
-        const sinφ = Math.sin(φ), cosφ = Math.cos(φ);
-        const sinλ = Math.sin(λ), cosλ = Math.cos(λ);
-
-        // right-handed vector: x -> 0°E,0°N; y -> 90°E,0°N, z -> 90°N
-        const x = cosφ * cosλ;
-        const y = cosφ * sinλ;
-        const z = sinφ;
-
-        return new NvectorSpherical(x, y, z);
-    }
-
-
-    /**
-     * Vector normal to great circle obtained by heading on given bearing from ‘this’ point.
-     *
-     * Direction of vector is such that initial bearing vector b = c × n, where n is an n-vector
-     * representing ‘this’ (start) point.
-     *
-     * @private
-     * @param   {number}   bearing - Compass bearing in degrees.
-     * @returns {Vector3d} Normalised vector representing great circle.
-     *
-     * @example
-     *   const p1 = new LatLon(53.3206, -1.7297);
-     *   const gc = p1.greatCircle(96.0);         // [-0.794,0.129,0.594]
-     */
-    greatCircle(bearing) {
-        const φ = this.lat.toRadians();
-        const λ = this.lon.toRadians();
-        const θ = Number(bearing).toRadians();
-
-        const x =  Math.sin(λ) * Math.cos(θ) - Math.sin(φ) * Math.cos(λ) * Math.sin(θ);
-        const y = -Math.cos(λ) * Math.cos(θ) - Math.sin(φ) * Math.sin(λ) * Math.sin(θ);
-        const z =  Math.cos(φ) * Math.sin(θ);
-
-        return new Vector3d(x, y, z);
-    }
-
-
-    /**
-     * Returns the distance on the surface of the sphere from ‘this’ point to destination point.
-     *
-     * @param   {LatLon}    point - Latitude/longitude of destination point.
-     * @param   {number}    [radius=6371e3] - Radius of earth (defaults to mean radius in metres).
-     * @returns {number}    Distance between this point and destination point, in same units as radius.
-     * @throws  {TypeError} Invalid point/radius.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const d = p1.distanceTo(p2);          // 404.3 km
-     */
-    distanceTo(point, radius=6371e3) {
-        if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
-        if (isNaN(radius)) throw new TypeError(`invalid radius ‘${radius}’`);
-
-        const R = Number(radius);
-
-        const n1 = this.toNvector();
-        const n2 = point.toNvector();
-
-        const sinθ = n1.cross(n2).length;
-        const cosθ = n1.dot(n2);
-        const δ = Math.atan2(sinθ, cosθ); // tanδ = |n₁×n₂| / n₁⋅n₂
-
-        return δ * R;
-    }
-
-
-    /**
-     * Returns the initial bearing from ‘this’ point to destination point.
-     *
-     * @param   {LatLon}    point - Latitude/longitude of destination point.
-     * @returns {number}    Initial bearing in degrees from north (0°..360°).
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const b1 = p1.initialBearingTo(p2);   // 156.2°
-     */
-    initialBearingTo(point) {
-        if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
-        if (this.equals(point)) return NaN; // coincident points
-
-        const p1 = this.toNvector();
-        const p2 = point.toNvector();
-
-        const N = new NvectorSpherical(0, 0, 1); // n-vector representing north pole
-
-        const c1 = p1.cross(p2); // great circle through p1 & p2
-        const c2 = p1.cross(N);  // great circle through p1 & north pole
-
-        const θ = c1.angleTo(c2, p1); // bearing is (signed) angle between c1 & c2
-
-        return Dms.wrap360(θ.toDegrees()); // normalise to range 0..360°
-    }
-
-
-    /**
-     * Returns final bearing arriving at destination point from ‘this’ point; the final bearing will
-     * differ from the initial bearing by varying degrees according to distance and latitude.
-     *
-     * @param   {LatLon}    point - Latitude/longitude of destination point.
-     * @returns {number}    Final bearing in degrees from north (0°..360°).
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const b2 = p1.finalBearingTo(p2); // 157.9°
-     */
-    finalBearingTo(point) {
-        if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
-
-        // get initial bearing from destination point to this point & reverse it by adding 180°
-        return Dms.wrap360(point.initialBearingTo(this) + 180);
-    }
-
-
-    /**
-     * Returns the midpoint between ‘this’ point and destination point.
-     *
-     * @param   {LatLon}    point - Latitude/longitude of destination point.
-     * @returns {LatLon}    Midpoint between this point and destination point.
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const pMid = p1.midpointTo(p2);       // 50.5363°N, 001.2746°E
-     */
-    midpointTo(point) {
-        if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
-
-        const n1 = this.toNvector();
-        const n2 = point.toNvector();
-
-        const mid = n1.plus(n2);
-
-        return new NvectorSpherical(mid.x, mid.y, mid.z).toLatLon();
-    }
-
-
-    /**
-     * Returns the point at given fraction between ‘this’ point and given point.
-     *
-     * @param   {LatLon}    point - Latitude/longitude of destination point.
-     * @param   {number}    fraction - Fraction between the two points (0 = this point, 1 = specified point).
-     * @returns {LatLon}    Intermediate point between this point and destination point.
-     * @throws  {TypeError} Invalid point/fraction.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const pInt = p1.intermediatePointTo(p2, 0.25); // 51.3721°N, 000.7072°E
-     */
-    intermediatePointTo(point, fraction) {
-        if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
-        if (isNaN(fraction)) throw new TypeError(`invalid fraction ‘${fraction}’`);
-
-        // angular distance between points; tanδ = |n₁×n₂| / n₁⋅n₂
-        const n1 = this.toNvector();
-        const n2 = point.toNvector();
-        const sinθ = n1.cross(n2).length;
-        const cosθ = n1.dot(n2);
-        const δ = Math.atan2(sinθ, cosθ);
-
-        // interpolated angular distance on straight line between points
-        const δi = δ * Number(fraction);
-        const sinδi = Math.sin(δi);
-        const cosδi = Math.cos(δi);
-
-        // direction vector (perpendicular to n1 in plane of n2)
-        const d = n1.cross(n2).unit().cross(n1); // unit(n₁×n₂) × n₁
-
-        // interpolated position
-        const int = n1.times(cosδi).plus(d.times(sinδi)); // n₁⋅cosδᵢ + d⋅sinδᵢ
-
-        return new NvectorSpherical(int.x, int.y, int.z).toLatLon();
-    }
-
-
-    /**
-     * Returns the latitude/longitude point projected from the point at given fraction on a straight
-     * line between between ‘this’ point and given point.
-     *
-     * @param   {LatLon}    point - Latitude/longitude of destination point.
-     * @param   {number}    fraction - Fraction between the two points (0 = this point, 1 = specified point).
-     * @returns {LatLon}    Intermediate point between this point and destination point.
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const pInt = p1.intermediatePointTo(p2, 0.25); // 51.3723°N, 000.7072°E
-     */
-    intermediatePointOnChordTo(point, fraction) {
-        if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
-
-        const n1 = this.toNvector();
-        const n2 = point.toNvector();
-
-        const int = n1.plus(n2.minus(n1).times(Number(fraction))); // n₁ + (n₂−n₁)·f ≡ n₁·(1-f) + n₂·f
-
-        const n = new NvectorSpherical(int.x, int.y, int.z);
-
-        return n.toLatLon();
-    }
-
-
-    /**
-     * Returns the destination point from ‘this’ point having travelled the given distance on the
-     * given initial bearing (bearing normally varies around path followed).
-     *
-     * @param   {number} distance - Distance travelled, in same units as earth radius (default: metres).
-     * @param   {number} bearing - Initial bearing in degrees from north.
-     * @param   {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {LatLon} Destination point.
-     *
-     * @example
-     *   const p1 = new LatLon(51.47788, -0.00147);
-     *   const p2 = p1.destinationPoint(7794, 300.7); // 51.5136°N, 000.0983°W
-     */
-    destinationPoint(distance, bearing, radius=6371e3) {
-        const n1 = this.toNvector();           // Gade's n_EA_E
-        const δ = distance / radius;           // angular distance in radians
-        const θ = Number(bearing).toRadians(); // initial bearing in radians
-
-        const N = new NvectorSpherical(0, 0, 1);     // north pole
-
-        const de = N.cross(n1).unit();               // east direction vector @ n1 (Gade's k_e_E)
-        const dn = n1.cross(de);                     // north direction vector @ n1 (Gade's (k_n_E)
-
-        const deSinθ = de.times(Math.sin(θ));
-        const dnCosθ = dn.times(Math.cos(θ));
-
-        const d = dnCosθ.plus(deSinθ);               // direction vector @ n1 (≡ C×n1; C = great circle)
-
-        const x = n1.times(Math.cos(δ));             // component of n2 parallel to n1
-        const y = d.times(Math.sin(δ));              // component of n2 perpendicular to n1
-
-        const n2 = x.plus(y);                        // Gade's n_EB_E
-
-        return new NvectorSpherical(n2.x, n2.y, n2.z).toLatLon();
-    }
-
-
-    /**
-     * Returns the point of intersection of two paths each defined by point pairs or start point and bearing.
-     *
-     * @param   {LatLon}        path1start - Start point of first path.
-     * @param   {LatLon|number} path1brngEnd - End point of first path or initial bearing from first start point.
-     * @param   {LatLon}        path2start - Start point of second path.
-     * @param   {LatLon|number} path2brngEnd - End point of second path or initial bearing from second start point.
-     * @returns {LatLon}        Destination point (null if no unique intersection defined)
-     * @throws  {TypeError}     Invalid parameter.
-     *
-     * @example
-     *   const p1 = new LatLon(51.8853, 0.2545), brng1 = 108.55;
-     *   const p2 = new LatLon(49.0034, 2.5735), brng2 =  32.44;
-     *   const pInt = LatLon.intersection(p1, brng1, p2, brng2); // 50.9076°N, 004.5086°E
-     */
-    static intersection(path1start, path1brngEnd, path2start, path2brngEnd) {
-        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),
-        // then candidate intersections are simply c1 × c2 & c2 × c1; most of the work is deciding correct
-        // intersection point to select! if bearing is given, that determines which intersection, if both
-        // paths are defined by start/end points, take closer intersection
-
-        const p1 = path1start.toNvector();
-        const p2 = path2start.toNvector();
-
-        let c1 = null, c2 = null, path1def = null, path2def = null;
-        // c1 & c2 are vectors defining great circles through start & end points; p × c gives initial bearing vector
-
-        if (path1brngEnd instanceof LatLonNvectorSpherical) { // path 1 defined by endpoint
-            c1 = p1.cross(path1brngEnd.toNvector());
-            path1def = 'endpoint';
-        } else {                              // path 1 defined by initial bearing
-            c1 = path1start.greatCircle(path1brngEnd);
-            path1def = 'bearing';
-        }
-        if (path2brngEnd instanceof LatLonNvectorSpherical) { // path 2 defined by endpoint
-            c2 = p2.cross(path2brngEnd.toNvector());
-            path2def = 'endpoint';
-        } else {                              // path 2 defined by initial bearing
-            c2 = path2start.greatCircle(path2brngEnd);
-            path2def = 'bearing';
-        }
-
-        // there are two (antipodal) candidate intersection points; we have to choose which to return
-        const i1 = c1.cross(c2);
-        const i2 = c2.cross(c1);
-
-        // TODO am I making heavy weather of this? is there a simpler way to do it?
-
-        // selection of intersection point depends on how paths are defined (bearings or endpoints)
-        let intersection = null, dir1 = null, dir2 = null;
-        switch (path1def + '+' + path2def) {
-            case 'bearing+bearing':
-                // if c×p⋅i1 is +ve, the initial bearing is towards i1, otherwise towards antipodal i2
-                dir1 = Math.sign(c1.cross(p1).dot(i1)); // c1×p1⋅i1 +ve means p1 bearing points to i1
-                dir2 = Math.sign(c2.cross(p2).dot(i1)); // c2×p2⋅i1 +ve means p2 bearing points to i1
-
-                switch (dir1 + dir2) {
-                    case  2: // dir1, dir2 both +ve, 1 & 2 both pointing to i1
-                        intersection = i1;
-                        break;
-                    case -2: // dir1, dir2 both -ve, 1 & 2 both pointing to i2
-                        intersection = i2;
-                        break;
-                    case  0: // dir1, dir2 opposite; intersection is at further-away intersection point
-                        // take opposite intersection from mid-point of p1 & p2 [is this always true?]
-                        intersection = p1.plus(p2).dot(i1) > 0 ? i2 : i1;
-                        break;
-                }
-                break;
-            case 'bearing+endpoint': // use bearing c1 × p1
-                dir1 = Math.sign(c1.cross(p1).dot(i1)); // c1×p1⋅i1 +ve means p1 bearing points to i1
-                intersection = dir1 > 0 ? i1 : i2;
-                break;
-            case 'endpoint+bearing': // use bearing c2 × p2
-                dir2 = Math.sign(c2.cross(p2).dot(i1)); // c2×p2⋅i1 +ve means p2 bearing points to i1
-                intersection = dir2 > 0 ? i1 : i2;
-                break;
-            case 'endpoint+endpoint': // select nearest intersection to mid-point of all points
-                const mid = p1.plus(p2).plus(path1brngEnd.toNvector()).plus(path2brngEnd.toNvector()); // eslint-disable-line no-case-declarations
-                intersection = mid.dot(i1) > 0 ? i1 : i2;
-                break;
-        }
-
-        return new NvectorSpherical(intersection.x, intersection.y, intersection.z).toLatLon();
-    }
-
-
-    /**
-     * Returns (signed) distance from ‘this’ point to great circle defined by start-point and end-point/bearing.
-     *
-     * @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 to great circle (-ve if to left, +ve if to right of path).
-     * @throws  {TypeError}     Invalid parameter.
-     *
-     * @example
-     *   const pCurrent = new LatLon(53.2611, -0.7972);
-     *
-     *   const p1 = new LatLon(53.3206, -1.7297), brng = 96.0;
-     *   const d = pCurrent.crossTrackDistanceTo(p1, brng); // Number(d.toPrecision(4)): -305.7
-     *
-     *   const p1 = new LatLon(53.3206, -1.7297), p2 = new LatLon(53.1887, 0.1334);
-     *   const d = pCurrent.crossTrackDistanceTo(p1, p2);   // Number(d.toPrecision(4)): -307.5
-     */
-    crossTrackDistanceTo(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}’`);
-
-        if (this.equals(pathStart)) return 0;
-
-        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 α = gc.angleTo(p) - π/2; // angle between point & great-circle
-
-        return α * R;
-    }
-
-
-    /**
-     * 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;
-    }
-
-
-    /**
-     * Returns closest point on great circle segment between point1 & point2 to ‘this’ point.
-     *
-     * If this point is ‘within’ the extent of the segment, the point is on the segment between point1 &
-     * point2; otherwise, it is the closer of the endpoints defining the segment.
-     *
-     * @param   {LatLon} point1 - Start point of great circle segment.
-     * @param   {LatLon} point2 - End point of great circle segment.
-     * @returns {LatLon} Closest point on segment.
-     *
-     * @example
-     *   const p1 = new LatLon(51.0, 1.0);
-     *   const p2 = new LatLon(51.0, 2.0);
-     *
-     *   const p0 = new LatLon(51.0, 1.9);
-     *   const p = p0.nearestPointOnSegment(p1, p2); // 51.0004°N, 001.9000°E
-     *   const d = p.distanceTo(p);                  // 42.71 m
-     *
-     *   const p0 = new LatLon(51.0, 2.1);
-     *   const p = p0.nearestPointOnSegment(p1, p2); // 51.0000°N, 002.0000°E
-     */
-    nearestPointOnSegment(point1, point2) {
-        let p = null;
-
-        if (this.isWithinExtent(point1, point2) && !point1.equals(point2)) {
-            // closer to segment than to its endpoints, find closest point on segment
-            const n0 = this.toNvector(), n1 = point1.toNvector(), n2 = point2.toNvector();
-            const c1 = n1.cross(n2); // n1×n2 = vector representing great circle through p1, p2
-            const c2 = n0.cross(c1); // n0×c1 = vector representing great circle through p0 normal to c1
-            const n = c1.cross(c2);  // c2×c1 = nearest point on c1 to n0
-            p = new NvectorSpherical(n.x, n.y, n.z).toLatLon();
-        } else {
-            // beyond segment extent, take closer endpoint
-            const d1 = this.distanceTo(point1);
-            const d2 = this.distanceTo(point2);
-            const pCloser = d1p1, p0->p2, p1->p2, p2->p1
-        const δ10 = n0.minus(n1), δ12 = n2.minus(n1);
-        const δ20 = n0.minus(n2), δ21 = n1.minus(n2);
-
-        // dot product δ10⋅δ12 tells us if p0 is on p2 side of p1, similarly for δ20⋅δ21
-        const extent1 = δ10.dot(δ12);
-        const extent2 = δ20.dot(δ21);
-
-        const isSameHemisphere = n0.dot(n1)>=0 && n0.dot(n2)>=0;
-
-        return extent1>=0 && extent2>=0 && isSameHemisphere;
-    }
-
-
-    /**
-     * Locates a point given two known locations and bearings from those locations.
-     *
-     * @param   {LatLon} point1 - First reference point.
-     * @param   {number} bearing1 - Bearing (in degrees from north) from first reference point.
-     * @param   {LatLon} point2 - Second reference point.
-     * @param   {number} bearing2 - Bearing (in degrees from north) from second reference point.
-     * @returns {LatLon} Triangulated point.
-     *
-     * @example
-     *   const p1 = new LatLon(50.7175,1.65139), p2 = new LatLon(50.9250,1.7094);
-     *   const p = LatLon.triangulate(p1, 333.3508, p2, 310.1414); // 51.1297°N, 001.3214°E
-     */
-    static triangulate(point1, bearing1, point2, bearing2) {
-        const n1 = point1.toNvector(), θ1 = Number(bearing1).toRadians();
-        const n2 = point2.toNvector(), θ2 = Number(bearing2).toRadians();
-
-        const N = new NvectorSpherical(0, 0, 1); // north pole
-
-        const de1 = N.cross(n1).unit();          // east vector @ n1
-        const dn1 = n1.cross(de1);               // north vector @ n1
-        const de1Sinθ = de1.times(Math.sin(θ1));
-        const dn1Cosθ = dn1.times(Math.cos(θ1));
-        const d1 = dn1Cosθ.plus(de1Sinθ);        // direction vector @ n1
-
-        const c1 = n1.cross(d1);                 // great circle p1 + bearing1
-
-        const de2 = N.cross(n2).unit();          // east vector @ n2
-        const dn2 = n2.cross(de2);               // north vector @ n2
-        const de2Sinθ = de2.times(Math.sin(θ2));
-        const dn2Cosθ = dn2.times(Math.cos(θ2));
-        const d2 = dn2Cosθ.plus(de2Sinθ);        // direction vector @ n2
-
-        const c2 = n2.cross(d2);                 // great circle p2 + bearing2
-
-        const ni = c1.cross(c2);                 // n-vector of intersection point
-
-        return new NvectorSpherical(ni.x, ni.y, ni.z).toLatLon();
-    }
-
-
-    /**
-     * Locates a latitude/longitude point at given distances from three other points.
-     *
-     * @param   {LatLon} point1 - First reference point.
-     * @param   {number} distance1 - Distance to first reference point (same units as radius).
-     * @param   {LatLon} point2 - Second reference point.
-     * @param   {number} distance2 - Distance to second reference point (same units as radius).
-     * @param   {LatLon} point3 - Third reference point.
-     * @param   {number} distance3 - Distance to third reference point (same units as radius).
-     * @param   {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {LatLon} Trilaterated point.
-     *
-     * @example
-     *   LatLon.trilaterate(new LatLon(0, 0), 157e3, new LatLon(0, 1), 111e3, new LatLon(1, 0), 111e3); // 00.9985°N, 000.9986°E
-     */
-    static trilaterate(point1, distance1, point2, distance2, point3, distance3, radius=6371e3) {
-        // from en.wikipedia.org/wiki/Trilateration
-
-        const n1 = point1.toNvector(), δ1 = Number(distance1)/Number(radius);
-        const n2 = point2.toNvector(), δ2 = Number(distance2)/Number(radius);
-        const n3 = point3.toNvector(), δ3 = Number(distance3)/Number(radius);
-
-        // the following uses x,y coordinate system with origin at n1, x axis n1->n2
-        const eX = n2.minus(n1).unit();                        // unit vector in x direction n1->n2
-        const i = eX.dot(n3.minus(n1));                        // signed magnitude of x component of n1->n3
-        const eY = n3.minus(n1).minus(eX.times(i)).unit();     // unit vector in y direction
-        const d = n2.minus(n1).length;                         // distance n1->n2
-        const j = eY.dot(n3.minus(n1));                        // signed magnitude of y component of n1->n3
-        const x = (δ1*δ1 - δ2*δ2 + d*d) / (2*d);               // x component of n1 -> intersection
-        const y = (δ1*δ1 - δ3*δ3 + i*i + j*j) / (2*j) - x*i/j; // y component of n1 -> intersection
-        // const eZ = eX.cross(eY);                            // unit vector perpendicular to plane
-        // const z = Math.sqrt(δ1*δ1 - x*x - y*y);             // z will be NaN for no intersections
-
-        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
-
-        return new NvectorSpherical(n.x, n.y, n.z).toLatLon();
-    }
-
-
-
-    /**
-     * Tests whether ‘this’ point is enclosed by the polygon defined by a set of points.
-     *
-     * @param   {LatLon[]} polygon - Ordered array of points defining vertices of polygon.
-     * @returns {bool}     Whether this point is enclosed by polygon.
-     *
-     * @example
-     *   const bounds = [ new LatLon(45,1), new LatLon(45,2), new LatLon(46,2), new LatLon(46,1) ];
-     *   const p = new LatLon(45.1, 1.1);
-     *   const inside = p.isEnclosedBy(bounds); // true
-     */
-    isEnclosedBy(polygon) {
-        // this method uses angle summation test; on a plane, angles for an enclosed point will sum
-        // to 360°, angles for an exterior point will sum to 0°. On a sphere, enclosed point angles
-        // will sum to less than 360° (due to spherical excess), exterior point angles will be small
-        // but non-zero. TODO: are any winding number optimisations applicable to spherical surface?
-
-        if (!(polygon instanceof Array)) throw new TypeError(`isEnclosedBy: polygon must be Array (not ${classOf(polygon)})`);
-        if (!(polygon[0] instanceof LatLonNvectorSpherical)) throw new TypeError(`isEnclosedBy: polygon must be Array of LatLon (not ${classOf(polygon[0])})`);
-        if (polygon.length < 3) return false; // or throw?
-
-        const nVertices = polygon.length;
-
-        const p = this.toNvector();
-
-        // get vectors from p to each vertex
-        const vectorToVertex = [];
-        for (let v=0; v π;
-    }
-
-
-    /**
-     * Calculates the area of a spherical polygon where the sides of the polygon are great circle
-     * arcs joining the vertices.
-     *
-     * Uses Girard’s theorem: A = [Σθᵢ − (n−2)·π]·R²
-     *
-     * @param   {LatLon[]} polygon - Array of points defining vertices of the polygon.
-     * @param   {number}   [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {number}   The area of the polygon in the same units as radius.
-     *
-     * @example
-     *   const polygon = [ new LatLon(0,0), new LatLon(1,0), new LatLon(0,1) ];
-     *   const area = LatLon.areaOf(polygon); // 6.18e9 m²
-     */
-    static areaOf(polygon, radius=6371e3) {
-        const R = Number(radius);
-
-        // get great-circle vector representing each segment
-        const c = [];
-        for (let v=0; v π/2) centreV = centreV.negate();
-
-        const centreP = new NvectorSpherical(centreV.x, centreV.y, centreV.z).toLatLon();
-
-        return centreP;
-    }
-    static centerOf(polygon) { return LatLonNvectorSpherical.centreOf(polygon); } // for en-us American English
-
-
-    /**
-     * Returns point representing geographic mean of supplied points.
-     *
-     * @param   {LatLon[]} points - Array of points to be averaged.
-     * @returns {LatLon}   Point at the geographic mean of the supplied points.
-     *
-     * @example
-     *   const p = LatLon.meanOf([ new LatLon(1, 1), new LatLon(4, 2), new LatLon(1, 3) ]); // 02.0001°N, 002.0000°E
-     */
-    static meanOf(points) {
-        let m = new NvectorSpherical(0, 0, 0); // null vector
-
-        // add all vectors
-        for (let p = 0; p < points.length; p++) {
-            m = m.plus(points[p].toNvector());
-        }
-        // m is now geographic mean
-
-        return new NvectorSpherical(m.x, m.y, m.z).toLatLon();
-    }
-
-
-    /**
-     * Checks if another point is equal to ‘this’ point.
-     *
-     * @param   {LatLon}    point - Point to be compared against this point.
-     * @returns {bool}      True if points have identical latitude and longitude values.
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(52.205, 0.119);
-     *   const equal = p1.equals(p2); // true
-     */
-    equals(point) {
-        if (!(point instanceof LatLonNvectorSpherical)) throw new TypeError(`invalid point ‘${point}’`);
-
-        if (Math.abs(this.lat - point.lat) > Number.EPSILON) return false;
-        if (Math.abs(this.lon - point.lon) > Number.EPSILON) return false;
-
-        return true;
-    }
-
-
-    /**
-     * Converts ‘this’ point to a GeoJSON object.
-     *
-     * @returns {Object} this point as a GeoJSON ‘Point’ object.
-     */
-    toGeoJSON() {
-        return { type: 'Point', coordinates: [ this.lon, this.lat ] };
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ point, formatted as degrees, degrees+minutes, or
-     * degrees+minutes+seconds.
-     *
-     * @param   {string} [format=d] - Format point as 'd', 'dm', 'dms', or 'n' for signed numeric.
-     * @param   {number} [dp=4|2|0] - Number of decimal places to use: default 4 for d, 2 for dm, 0 for dms.
-     * @returns {string} Comma-separated formatted latitude/longitude.
-     *
-     * @example
-     *   const greenwich = new LatLon(51.47788, -0.00147);
-     *   const d = greenwich.toString();                        // 51.4778°N, 000.0015°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
-     */
-    toString(format='d', dp=undefined) {
-        // 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 (format == 'n') { // signed numeric degrees
-            if (dp == undefined) dp = 4;
-            return `${this.lat.toFixed(dp)},${this.lon.toFixed(dp)}`;
-        }
-        const lat = Dms.toLat(this.lat, format, dp);
-        const lon = Dms.toLon(this.lon, format, dp);
-        return `${lat}, ${lon}`;
-    }
-
-}
-
-
-/* Nvector - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-
-/**
- * An n-vector is a (unit) vector normal to the Earth's surface (a non-singular position
- * representation).
- *
- * For many applications, n-vectors are more convenient to work with than other position
- * representations such as latitude/longitude, UTM coordinates, etc.
- *
- * On a spherical model earth, an n-vector is equivalent to a (normalised) earth-centred earth-fixed
- * (ECEF) vector.
- *
- * @extends Vector3d
- */
-class NvectorSpherical extends Vector3d {
-
-    // note commonality with latlon-nvector-ellipsoidal
-
-    /**
-     * Creates a 3d n-vector normal to the Earth’s surface.
-     *
-     * @param {number} x - X component of n-vector (towards 0°N, 0°E).
-     * @param {number} y - Y component of n-vector (towards 0°N, 90°E).
-     * @param {number} z - Z component of n-vector (towards 90°N).
-     *
-     * @example
-     *   import { Nvector } from '/js/geodesy/latlon-nvector-spherical.js';
-     *   const n = new Nvector(0.5000, 0.5000, 0.7071);
-     */
-    constructor(x, y, z) {
-        const u = new Vector3d(x, y, z).unit(); // n-vectors are always normalised
-
-        super(u.x, u.y, u.z);
-    }
-
-
-    /**
-     * Converts ‘this’ n-vector to latitude/longitude point.
-     *
-     * @returns  {LatLon} Latitude/longitude point vector points to.
-     *
-     * @example
-     *   const n = new Nvector(0.5000, 0.5000, 0.7071);
-     *   const p = n.toLatLon(); // 45.0°N, 045.0°E
-     */
-    toLatLon() {
-        // tanφ = z / √(x²+y²), tanλ = y / x (same as ellipsoidal calculation)
-
-        const x = this.x, y = this.y, z = this.z;
-
-        const φ = Math.atan2(z, Math.sqrt(x*x + y*y));
-        const λ = Math.atan2(y, x);
-
-        return new LatLonNvectorSpherical(φ.toDegrees(), λ.toDegrees());
-    }
-
-
-    /**
-     * Vector normal to great circle obtained by heading on given bearing from point given by
-     * ‘this’ n-vector.
-     *
-     * Direction of vector is such that initial bearing vector b = c × n, where n is an n-vector
-     * representing ‘this’ (start) point.
-     *
-     * @private
-     * @param   {number}   bearing - Compass bearing in degrees.
-     * @returns {Vector3d} Normalised vector representing great circle.
-     *
-     * @example
-     *   const n1 = new LatLon(53.3206, -1.7297).toNvector();
-     *   const gc = n1.greatCircle(96.0); // [-0.794,0.129,0.594]
-     */
-    greatCircle(bearing) {
-        const θ = Number(bearing).toRadians();
-
-        const N = new Vector3d(0, 0, 1); // n-vector representing north pole
-        const e = N.cross(this);         // easting
-        const n = this.cross(e);         // northing
-        const eʹ = e.times(Math.cos(θ)/e.length);
-        const nʹ = n.times(Math.sin(θ)/n.length);
-        const c = nʹ.minus(eʹ);
-
-        return c;
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ n-vector.
-     *
-     * @param   {number} [dp=3] - Number of decimal places to display.
-     * @returns {string} Comma-separated x, y, z, h values.
-     *
-     * @example
-     *   const v = new Nvector(0.5000, 0.5000, 0.7071).toString(); // [0.500,0.500,0.707]
-     */
-    toString(dp=3) {
-        const x = this.x.toFixed(dp);
-        const y = this.y.toFixed(dp);
-        const z = this.z.toFixed(dp);
-
-        return `[${x},${y},${z}]`;
-    }
-
-}
-
-
-/**
- * Return class of supplied argument; javascriptweblog.wordpress.com/2011/08/08.
- *
- * @param   {any} thing - Object whose class is to be determined.
- * @returns {string} Class of supplied object.
- */
-function classOf(thing) {
-    return ({}).toString.call(thing).match(/\s([a-zA-Z0-9]+)/)[1];
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { LatLonNvectorSpherical as default, NvectorSpherical as Nvector, Dms };
diff --git a/src/js/geo/latlon-spherical.js b/src/js/geo/latlon-spherical.js
deleted file mode 100644
index 67ba244..0000000
--- a/src/js/geo/latlon-spherical.js
+++ /dev/null
@@ -1,865 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Latitude/longitude spherical geodesy tools                         (c) Chris Veness 2002-2022  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong.html                                                    */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#latlon-spherical                           */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import Dms from './dms.js';
-
-const π = Math.PI;
-
-
-/**
- * Library of geodesy functions for operations on a spherical earth model.
- *
- * Includes distances, bearings, destinations, etc, for both great circle paths and rhumb lines,
- * and other related functions.
- *
- * All calculations are done using simple spherical trigonometric formulae.
- *
- * @module latlon-spherical
- */
-
-// note greek letters (e.g. φ, λ, θ) are used for angles in radians to distinguish from angles in
-// degrees (e.g. lat, lon, brng)
-
-
-/* LatLonSpherical - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-
-/**
- * Latitude/longitude points on a spherical model earth, and methods for calculating distances,
- * bearings, destinations, etc on (orthodromic) great-circle paths and (loxodromic) rhumb lines.
- */
-class LatLonSpherical {
-
-    /**
-     * Creates a latitude/longitude point on the earth’s surface, using a spherical model earth.
-     *
-     * @param  {number} lat - Latitude (in degrees).
-     * @param  {number} lon - Longitude (in degrees).
-     * @throws {TypeError} Invalid lat/lon.
-     *
-     * @example
-     *   import LatLon from '/js/geodesy/latlon-spherical.js';
-     *   const p = new LatLon(52.205, 0.119);
-     */
-    constructor(lat, lon) {
-        if (isNaN(lat)) throw new TypeError(`invalid lat ‘${lat}’`);
-        if (isNaN(lon)) throw new TypeError(`invalid lon ‘${lon}’`);
-
-        this._lat = Dms.wrap90(Number(lat));
-        this._lon = Dms.wrap180(Number(lon));
-    }
-
-
-    /**
-     * Latitude in degrees north from equator (including aliases lat, latitude): can be set as
-     * numeric or hexagesimal (deg-min-sec); returned as numeric.
-     */
-    get lat()       { return this._lat; }
-    get latitude()  { return this._lat; }
-    set lat(lat) {
-        this._lat = isNaN(lat) ? Dms.wrap90(Dms.parse(lat)) : Dms.wrap90(Number(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}’`);
-    }
-
-    /**
-     * Longitude in degrees east from international reference meridian (including aliases lon, lng,
-     * longitude): can be set as numeric or hexagesimal (deg-min-sec); returned as numeric.
-     */
-    get lon()       { return this._lon; }
-    get lng()       { return this._lon; }
-    get longitude() { return this._lon; }
-    set lon(lon) {
-        this._lon = isNaN(lon) ? Dms.wrap180(Dms.parse(lon)) : Dms.wrap180(Number(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}’`);
-    }
-    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}’`);
-    }
-
-
-    /** Conversion factors; 1000 * LatLon.metresToKm gives 1. */
-    static get metresToKm()            { return 1/1000; }
-    /** Conversion factors; 1000 * LatLon.metresToMiles gives 0.621371192237334. */
-    static get metresToMiles()         { return 1/1609.344; }
-    /** Conversion factors; 1000 * LatLon.metresToMiles gives 0.5399568034557236. */
-    static get metresToNauticalMiles() { return 1/1852; }
-
-
-    /**
-     * Parses a latitude/longitude point from a variety of formats.
-     *
-     * Latitude & longitude (in degrees) can be supplied as two separate parameters, as a single
-     * comma-separated lat/lon string, or as a single object with { lat, lon } or GeoJSON properties.
-     *
-     * The latitude/longitude values may be numeric or strings; they may be signed decimal or
-     * deg-min-sec (hexagesimal) suffixed by compass direction (NSEW); a variety of separators are
-     * accepted. Examples -3.62, '3 37 12W', '3°37′12″W'.
-     *
-     * Thousands/decimal separators must be comma/dot; use Dms.fromLocale to convert locale-specific
-     * thousands/decimal separators.
-     *
-     * @param   {number|string|Object} lat|latlon - Latitude (in degrees) or comma-separated lat/lon or lat/lon object.
-     * @param   {number|string}        [lon]      - Longitude (in degrees).
-     * @returns {LatLon} Latitude/longitude point.
-     * @throws  {TypeError} Invalid point.
-     *
-     * @example
-     *   const p1 = LatLon.parse(52.205, 0.119);                                    // numeric pair (≡ new LatLon)
-     *   const p2 = LatLon.parse('52.205', '0.119');                                // numeric string pair (≡ new LatLon)
-     *   const p3 = LatLon.parse('52.205, 0.119');                                  // single string numerics
-     *   const p4 = LatLon.parse('52°12′18.0″N', '000°07′08.4″E');                  // DMS pair
-     *   const p5 = LatLon.parse('52°12′18.0″N, 000°07′08.4″E');                    // single string DMS
-     *   const p6 = LatLon.parse({ lat: 52.205, lon: 0.119 });                      // { lat, lon } object numeric
-     *   const p7 = LatLon.parse({ lat: '52°12′18.0″N', lng: '000°07′08.4″E' });    // { lat, lng } object DMS
-     *   const p8 = LatLon.parse({ type: 'Point', coordinates: [ 0.119, 52.205] }); // GeoJSON
-     */
-    static parse(...args) {
-        if (args.length == 0) throw new TypeError('invalid (empty) point');
-        if (args[0]===null || args[1]===null) throw new TypeError('invalid (null) point');
-
-        let lat=undefined, lon=undefined;
-
-        if (args.length == 2) { // regular (lat, lon) arguments
-            [ lat, lon ] = args;
-            lat = Dms.wrap90(Dms.parse(lat));
-            lon = Dms.wrap180(Dms.parse(lon));
-            if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`);
-        }
-
-        if (args.length == 1 && typeof args[0] == 'string') { // single comma-separated lat,lon string
-            [ lat, lon ] = args[0].split(',');
-            lat = Dms.wrap90(Dms.parse(lat));
-            lon = Dms.wrap180(Dms.parse(lon));
-            if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args[0]}’`);
-        }
-
-        if (args.length == 1 && typeof args[0] == 'object') { // single { lat, lon } object
-            const ll = args[0];
-            if (ll.type == 'Point' && Array.isArray(ll.coordinates)) { // GeoJSON
-                [ lon, lat ] = ll.coordinates;
-            } else { // regular { lat, lon } object
-                if (ll.latitude  != undefined) lat = ll.latitude;
-                if (ll.lat       != undefined) lat = ll.lat;
-                if (ll.longitude != undefined) lon = ll.longitude;
-                if (ll.lng       != undefined) lon = ll.lng;
-                if (ll.lon       != undefined) lon = ll.lon;
-                lat = Dms.wrap90(Dms.parse(lat));
-                lon = Dms.wrap180(Dms.parse(lon));
-            }
-            if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${JSON.stringify(args[0])}’`);
-        }
-
-        if (isNaN(lat) || isNaN(lon)) throw new TypeError(`invalid point ‘${args.toString()}’`);
-
-        return new LatLonSpherical(lat, lon);
-    }
-
-
-    /**
-     * Returns the distance along the surface of the earth from ‘this’ point to destination point.
-     *
-     * Uses haversine formula: a = sin²(Δφ/2) + cosφ1·cosφ2 · sin²(Δλ/2); d = 2 · atan2(√a, √(a-1)).
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @param   {number} [radius=6371e3] - Radius of earth (defaults to mean radius in metres).
-     * @returns {number} Distance between this point and destination point, in same units as radius.
-     * @throws  {TypeError} Invalid radius.
-     *
-     * @example
-     *   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
-     *   const m = p1.distanceTo(p2, 3959); // 251.2 miles
-     */
-    distanceTo(point, radius=6371e3) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-        if (isNaN(radius)) throw new TypeError(`invalid radius ‘${radius}’`);
-
-        // a = sin²(Δφ/2) + cos(φ1)⋅cos(φ2)⋅sin²(Δλ/2)
-        // δ = 2·atan2(√(a), √(1−a))
-        // see mathforum.org/library/drmath/view/51879.html for derivation
-
-        const R = radius;
-        const φ1 = this.lat.toRadians(),  λ1 = this.lon.toRadians();
-        const φ2 = point.lat.toRadians(), λ2 = point.lon.toRadians();
-        const Δφ = φ2 - φ1;
-        const Δλ = λ2 - λ1;
-
-        const a = Math.sin(Δφ/2)*Math.sin(Δφ/2) + Math.cos(φ1)*Math.cos(φ2) * Math.sin(Δλ/2)*Math.sin(Δλ/2);
-        const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
-        const d = R * c;
-
-        return d;
-    }
-
-
-    /**
-     * Returns the initial bearing from ‘this’ point to destination point.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @returns {number} Initial bearing in degrees from north (0°..360°).
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const b1 = p1.initialBearingTo(p2); // 156.2°
-     */
-    initialBearingTo(point) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-        if (this.equals(point)) return NaN; // coincident points
-
-        // tanθ = sinΔλ⋅cosφ2 / cosφ1⋅sinφ2 − sinφ1⋅cosφ2⋅cosΔλ
-        // see mathforum.org/library/drmath/view/55417.html for derivation
-
-        const φ1 = this.lat.toRadians();
-        const φ2 = point.lat.toRadians();
-        const Δλ = (point.lon - this.lon).toRadians();
-
-        const x = Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);
-        const y = Math.sin(Δλ) * Math.cos(φ2);
-        const θ = Math.atan2(y, x);
-
-        const bearing = θ.toDegrees();
-
-        return Dms.wrap360(bearing);
-    }
-
-
-    /**
-     * Returns final bearing arriving at destination point from ‘this’ point; the final bearing will
-     * differ from the initial bearing by varying degrees according to distance and latitude.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @returns {number} Final bearing in degrees from north (0°..360°).
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const b2 = p1.finalBearingTo(p2); // 157.9°
-     */
-    finalBearingTo(point) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-
-        // get initial bearing from destination point to this point & reverse it by adding 180°
-
-        const bearing = point.initialBearingTo(this) + 180;
-
-        return Dms.wrap360(bearing);
-    }
-
-
-    /**
-     * Returns the midpoint between ‘this’ point and destination point.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @returns {LatLon} Midpoint between this point and destination point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const pMid = p1.midpointTo(p2); // 50.5363°N, 001.2746°E
-     */
-    midpointTo(point) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-
-        // φm = atan2( sinφ1 + sinφ2, √( (cosφ1 + cosφ2⋅cosΔλ)² + cos²φ2⋅sin²Δλ ) )
-        // λm = λ1 + atan2(cosφ2⋅sinΔλ, cosφ1 + cosφ2⋅cosΔλ)
-        // midpoint is sum of vectors to two points: mathforum.org/library/drmath/view/51822.html
-
-        const φ1 = this.lat.toRadians();
-        const λ1 = this.lon.toRadians();
-        const φ2 = point.lat.toRadians();
-        const Δλ = (point.lon - this.lon).toRadians();
-
-        // get cartesian coordinates for the two points
-        const A = { x: Math.cos(φ1), y: 0, z: Math.sin(φ1) }; // place point A on prime meridian y=0
-        const B = { x: Math.cos(φ2)*Math.cos(Δλ), y: Math.cos(φ2)*Math.sin(Δλ), z: Math.sin(φ2) };
-
-        // vector to midpoint is sum of vectors to two points (no need to normalise)
-        const C = { x: A.x + B.x, y: A.y + B.y, z: A.z + B.z };
-
-        const φm = Math.atan2(C.z, Math.sqrt(C.x*C.x + C.y*C.y));
-        const λm = λ1 + Math.atan2(C.y, C.x);
-
-        const lat = φm.toDegrees();
-        const lon = λm.toDegrees();
-
-        return new LatLonSpherical(lat, lon);
-    }
-
-
-    /**
-     * Returns the point at given fraction between ‘this’ point and given point.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @param   {number} fraction - Fraction between the two points (0 = this point, 1 = specified point).
-     * @returns {LatLon} Intermediate point between this point and destination point.
-     *
-     * @example
-     *   const p1 = new LatLon(52.205, 0.119);
-     *   const p2 = new LatLon(48.857, 2.351);
-     *   const pInt = p1.intermediatePointTo(p2, 0.25); // 51.3721°N, 000.7073°E
-     */
-    intermediatePointTo(point, fraction) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-        if (this.equals(point)) return new LatLonSpherical(this.lat, this.lon); // coincident points
-
-        const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();
-        const φ2 = point.lat.toRadians(), λ2 = point.lon.toRadians();
-
-        // distance between points
-        const Δφ = φ2 - φ1;
-        const Δλ = λ2 - λ1;
-        const a = Math.sin(Δφ/2) * Math.sin(Δφ/2)
-            + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2);
-        const δ = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
-
-        const A = Math.sin((1-fraction)*δ) / Math.sin(δ);
-        const B = Math.sin(fraction*δ) / Math.sin(δ);
-
-        const x = A * Math.cos(φ1) * Math.cos(λ1) + B * Math.cos(φ2) * Math.cos(λ2);
-        const y = A * Math.cos(φ1) * Math.sin(λ1) + B * Math.cos(φ2) * Math.sin(λ2);
-        const z = A * Math.sin(φ1) + B * Math.sin(φ2);
-
-        const φ3 = Math.atan2(z, Math.sqrt(x*x + y*y));
-        const λ3 = Math.atan2(y, x);
-
-        const lat = φ3.toDegrees();
-        const lon = λ3.toDegrees();
-
-        return new LatLonSpherical(lat, lon);
-    }
-
-
-    /**
-     * Returns the destination point from ‘this’ point having travelled the given distance on the
-     * given initial bearing (bearing normally varies around path followed).
-     *
-     * @param   {number} distance - Distance travelled, in same units as earth radius (default: metres).
-     * @param   {number} bearing - Initial bearing in degrees from north.
-     * @param   {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {LatLon} Destination point.
-     * @throws  {TypeError} Invalid distance/bearing/radius.
-     *
-     * @example
-     *   const p1 = new LatLon(51.47788, -0.00147);
-     *   const p2 = p1.destinationPoint(7794, 300.7); // 51.5136°N, 000.0983°W
-     */
-    destinationPoint(distance, bearing, radius=6371e3) {
-        if (isNaN(distance)) throw new TypeError(`invalid distance ‘${distance}’`);
-        if (isNaN(bearing)) throw new TypeError(`invalid bearing ‘${bearing}’`);
-        if (isNaN(radius)) throw new TypeError(`invalid radius ‘${radius}’`);
-
-        // sinφ2 = sinφ1⋅cosδ + cosφ1⋅sinδ⋅cosθ
-        // tanΔλ = sinθ⋅sinδ⋅cosφ1 / cosδ−sinφ1⋅sinφ2
-        // see mathforum.org/library/drmath/view/52049.html for derivation
-
-        const δ = distance / radius; // angular distance in radians
-        const θ = Number(bearing).toRadians();
-
-        const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();
-
-        const sinφ2 = Math.sin(φ1) * Math.cos(δ) + Math.cos(φ1) * Math.sin(δ) * Math.cos(θ);
-        const φ2 = Math.asin(sinφ2);
-        const y = Math.sin(θ) * Math.sin(δ) * Math.cos(φ1);
-        const x = Math.cos(δ) - Math.sin(φ1) * sinφ2;
-        const λ2 = λ1 + Math.atan2(y, x);
-
-        const lat = φ2.toDegrees();
-        const lon = λ2.toDegrees();
-
-        return new LatLonSpherical(lat, lon);
-    }
-
-
-    /**
-     * Returns the point of intersection of two paths defined by point and bearing.
-     *
-     * @param   {LatLon}      p1 - First point.
-     * @param   {number}      brng1 - Initial bearing from first point.
-     * @param   {LatLon}      p2 - Second point.
-     * @param   {number}      brng2 - Initial bearing from second point.
-     * @returns {LatLon|null} Destination point (null if no unique intersection defined).
-     *
-     * @example
-     *   const p1 = new LatLon(51.8853, 0.2545), brng1 = 108.547;
-     *   const p2 = new LatLon(49.0034, 2.5735), brng2 =  32.435;
-     *   const pInt = LatLon.intersection(p1, brng1, p2, brng2); // 50.9078°N, 004.5084°E
-     */
-    static intersection(p1, brng1, p2, brng2) {
-        if (!(p1 instanceof LatLonSpherical)) p1 = LatLonSpherical.parse(p1); // allow literal forms
-        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}’`);
-
-        // see www.edwilliams.org/avform.htm#Intersection
-
-        const φ1 = p1.lat.toRadians(), λ1 = p1.lon.toRadians();
-        const φ2 = p2.lat.toRadians(), λ2 = p2.lon.toRadians();
-        const θ13 = Number(brng1).toRadians(), θ23 = Number(brng2).toRadians();
-        const Δφ = φ2 - φ1, Δλ = λ2 - λ1;
-
-        // angular distance p1-p2
-        const δ12 = 2 * Math.asin(Math.sqrt(Math.sin(Δφ/2) * Math.sin(Δφ/2)
-            + Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ/2) * Math.sin(Δλ/2)));
-        if (Math.abs(δ12) < Number.EPSILON) return new LatLonSpherical(p1.lat, p1.lon); // coincident points
-
-        // initial/final bearings between points
-        const cosθa = (Math.sin(φ2) - Math.sin(φ1)*Math.cos(δ12)) / (Math.sin(δ12)*Math.cos(φ1));
-        const cosθb = (Math.sin(φ1) - Math.sin(φ2)*Math.cos(δ12)) / (Math.sin(δ12)*Math.cos(φ2));
-        const θa = Math.acos(Math.min(Math.max(cosθa, -1), 1)); // protect against rounding errors
-        const θb = Math.acos(Math.min(Math.max(cosθb, -1), 1)); // protect against rounding errors
-
-        const θ12 = Math.sin(λ2-λ1)>0 ? θa : 2*π-θa;
-        const θ21 = Math.sin(λ2-λ1)>0 ? 2*π-θb : θb;
-
-        const α1 = θ13 - θ12; // angle 2-1-3
-        const α2 = θ21 - θ23; // angle 1-2-3
-
-        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 (antipodal/360°)
-
-        const cosα3 = -Math.cos(α1)*Math.cos(α2) + Math.sin(α1)*Math.sin(α2)*Math.cos(δ12);
-
-        const δ13 = Math.atan2(Math.sin(δ12)*Math.sin(α1)*Math.sin(α2), Math.cos(α2) + Math.cos(α1)*cosα3);
-
-        const φ3 = Math.asin(Math.min(Math.max(Math.sin(φ1)*Math.cos(δ13) + Math.cos(φ1)*Math.sin(δ13)*Math.cos(θ13), -1), 1));
-
-        const Δλ13 = Math.atan2(Math.sin(θ13)*Math.sin(δ13)*Math.cos(φ1), Math.cos(δ13) - Math.sin(φ1)*Math.sin(φ3));
-        const λ3 = λ1 + Δλ13;
-
-        const lat = φ3.toDegrees();
-        const lon = λ3.toDegrees();
-
-        return new LatLonSpherical(lat, lon);
-    }
-
-
-    /**
-     * Returns (signed) distance from ‘this’ point to great circle defined by start-point and
-     * end-point.
-     *
-     * @param   {LatLon} pathStart - Start point of great circle path.
-     * @param   {LatLon} pathEnd - End point of great circle path.
-     * @param   {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {number} Distance to great circle (-ve if to left, +ve if to right of path).
-     *
-     * @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.crossTrackDistanceTo(p1, p2);  // -307.5 m
-     */
-    crossTrackDistanceTo(pathStart, pathEnd, radius=6371e3) {
-        if (!(pathStart instanceof LatLonSpherical)) pathStart = LatLonSpherical.parse(pathStart); // allow literal forms
-        if (!(pathEnd instanceof LatLonSpherical)) pathEnd = LatLonSpherical.parse(pathEnd);       // allow literal forms
-        const R = radius;
-
-        if (this.equals(pathStart)) return 0;
-
-        const δ13 = pathStart.distanceTo(this, R) / R;
-        const θ13 = pathStart.initialBearingTo(this).toRadians();
-        const θ12 = pathStart.initialBearingTo(pathEnd).toRadians();
-
-        const δxt = Math.asin(Math.sin(δ13) * Math.sin(θ13 - θ12));
-
-        return δxt * R;
-    }
-
-
-    /**
-     * Returns how far ‘this’ point is along a path from from start-point, heading 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} pathEnd - End point of great circle path.
-     * @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, pathEnd, radius=6371e3) {
-        if (!(pathStart instanceof LatLonSpherical)) pathStart = LatLonSpherical.parse(pathStart); // allow literal forms
-        if (!(pathEnd instanceof LatLonSpherical)) pathEnd = LatLonSpherical.parse(pathEnd);       // allow literal forms
-        const R = radius;
-
-        if (this.equals(pathStart)) return 0;
-
-        const δ13 = pathStart.distanceTo(this, R) / R;
-        const θ13 = pathStart.initialBearingTo(this).toRadians();
-        const θ12 = pathStart.initialBearingTo(pathEnd).toRadians();
-
-        const δxt = Math.asin(Math.sin(δ13) * Math.sin(θ13-θ12));
-
-        const δat = Math.acos(Math.cos(δ13) / Math.abs(Math.cos(δxt)));
-
-        return δat*Math.sign(Math.cos(θ12-θ13)) * R;
-    }
-
-
-    /**
-     * Returns maximum latitude reached when travelling on a great circle on given bearing from
-     * ‘this’ point (‘Clairaut’s formula’). Negate the result for the minimum latitude (in the
-     * southern hemisphere).
-     *
-     * The maximum latitude is independent of longitude; it will be the same for all points on a
-     * given latitude.
-     *
-     * @param   {number} bearing - Initial bearing.
-     * @returns {number} Maximum latitude reached.
-     */
-    maxLatitude(bearing) {
-        const θ = Number(bearing).toRadians();
-
-        const φ = this.lat.toRadians();
-
-        const φMax = Math.acos(Math.abs(Math.sin(θ) * Math.cos(φ)));
-
-        return φMax.toDegrees();
-    }
-
-
-    /**
-     * Returns the pair of meridians at which a great circle defined by two points crosses the given
-     * latitude. If the great circle doesn't reach the given latitude, null is returned.
-     *
-     * @param   {LatLon}      point1 - First point defining great circle.
-     * @param   {LatLon}      point2 - Second point defining great circle.
-     * @param   {number}      latitude - Latitude crossings are to be determined for.
-     * @returns {Object|null} Object containing { lon1, lon2 } or null if given latitude not reached.
-     */
-    static crossingParallels(point1, point2, latitude) {
-        if (point1.equals(point2)) return null; // coincident points
-
-        const φ = Number(latitude).toRadians();
-
-        const φ1 = point1.lat.toRadians();
-        const λ1 = point1.lon.toRadians();
-        const φ2 = point2.lat.toRadians();
-        const λ2 = point2.lon.toRadians();
-
-        const Δλ = λ2 - λ1;
-
-        const x = Math.sin(φ1) * Math.cos(φ2) * Math.cos(φ) * Math.sin(Δλ);
-        const y = Math.sin(φ1) * Math.cos(φ2) * Math.cos(φ) * Math.cos(Δλ) - Math.cos(φ1) * Math.sin(φ2) * Math.cos(φ);
-        const z = Math.cos(φ1) * Math.cos(φ2) * Math.sin(φ) * Math.sin(Δλ);
-
-        if (z * z > x * x + y * y) return null; // great circle doesn't reach latitude
-
-        const λm = Math.atan2(-y, x);               // longitude at max latitude
-        const Δλi = Math.acos(z / Math.sqrt(x*x + y*y)); // Δλ from λm to intersection points
-
-        const λi1 = λ1 + λm - Δλi;
-        const λi2 = λ1 + λm + Δλi;
-
-        const lon1 = λi1.toDegrees();
-        const lon2 = λi2.toDegrees();
-
-        return {
-            lon1: Dms.wrap180(lon1),
-            lon2: Dms.wrap180(lon2),
-        };
-    }
-
-
-    /* Rhumb - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-
-    /**
-     * Returns the distance travelling from ‘this’ point to destination point along a rhumb line.
-     *
-     * @param   {LatLon} point - Latitude/longitude of destination point.
-     * @param   {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {number} Distance in km between this point and destination point (same units as radius).
-     *
-     * @example
-     *   const p1 = new LatLon(51.127, 1.338);
-     *   const p2 = new LatLon(50.964, 1.853);
-     *   const d = p1.distanceTo(p2); //  40.31 km
-     */
-    rhumbDistanceTo(point, radius=6371e3) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-
-        // see www.edwilliams.org/avform.htm#Rhumb
-
-        const R = radius;
-        const φ1 = this.lat.toRadians();
-        const φ2 = point.lat.toRadians();
-        const Δφ = φ2 - φ1;
-        let Δλ = Math.abs(point.lon - this.lon).toRadians();
-        // if dLon over 180° take shorter rhumb line across the anti-meridian:
-        if (Math.abs(Δλ) > π) Δλ = Δλ > 0 ? -(2 * π - Δλ) : (2 * π + Δλ);
-
-        // on Mercator projection, longitude distances shrink by latitude; q is the 'stretch factor'
-        // q becomes ill-conditioned along E-W line (0/0); use empirical tolerance to avoid it (note ε is too small)
-        const Δψ = Math.log(Math.tan(φ2 / 2 + π / 4) / Math.tan(φ1 / 2 + π / 4));
-        const q = Math.abs(Δψ) > 10e-12 ? Δφ / Δψ : Math.cos(φ1);
-
-        // distance is pythagoras on 'stretched' Mercator projection, √(Δφ² + q²·Δλ²)
-        const δ = Math.sqrt(Δφ*Δφ + q*q * Δλ*Δλ); // angular distance in radians
-        const d = δ * R;
-
-        return d;
-    }
-
-
-    /**
-     * Returns the bearing from ‘this’ point to destination point along a rhumb line.
-     *
-     * @param   {LatLon}    point - Latitude/longitude of destination point.
-     * @returns {number}    Bearing in degrees from north.
-     *
-     * @example
-     *   const p1 = new LatLon(51.127, 1.338);
-     *   const p2 = new LatLon(50.964, 1.853);
-     *   const d = p1.rhumbBearingTo(p2); // 116.7°
-     */
-    rhumbBearingTo(point) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-        if (this.equals(point)) return NaN; // coincident points
-
-        const φ1 = this.lat.toRadians();
-        const φ2 = point.lat.toRadians();
-        let Δλ = (point.lon - this.lon).toRadians();
-        // if dLon over 180° take shorter rhumb line across the anti-meridian:
-        if (Math.abs(Δλ) > π) Δλ = Δλ > 0 ? -(2 * π - Δλ) : (2 * π + Δλ);
-
-        const Δψ = Math.log(Math.tan(φ2 / 2 + π / 4) / Math.tan(φ1 / 2 + π / 4));
-
-        const θ = Math.atan2(Δλ, Δψ);
-
-        const bearing = θ.toDegrees();
-
-        return Dms.wrap360(bearing);
-    }
-
-
-    /**
-     * Returns the destination point having travelled along a rhumb line from ‘this’ point the given
-     * distance on the given bearing.
-     *
-     * @param   {number} distance - Distance travelled, in same units as earth radius (default: metres).
-     * @param   {number} bearing - Bearing in degrees from north.
-     * @param   {number} [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {LatLon} Destination point.
-     *
-     * @example
-     *   const p1 = new LatLon(51.127, 1.338);
-     *   const p2 = p1.rhumbDestinationPoint(40300, 116.7); // 50.9642°N, 001.8530°E
-     */
-    rhumbDestinationPoint(distance, bearing, radius=6371e3) {
-        const φ1 = this.lat.toRadians(), λ1 = this.lon.toRadians();
-        const θ = Number(bearing).toRadians();
-
-        const δ = distance / radius; // angular distance in radians
-
-        const Δφ = δ * Math.cos(θ);
-        let φ2 = φ1 + Δφ;
-
-        // check for some daft bugger going past the pole, normalise latitude if so
-        if (Math.abs(φ2) > π / 2) φ2 = φ2 > 0 ? π - φ2 : -π - φ2;
-
-        const Δψ = Math.log(Math.tan(φ2 / 2 + π / 4) / Math.tan(φ1 / 2 + π / 4));
-        const q = Math.abs(Δψ) > 10e-12 ? Δφ / Δψ : Math.cos(φ1); // E-W course becomes ill-conditioned with 0/0
-
-        const Δλ = δ * Math.sin(θ) / q;
-        const λ2 = λ1 + Δλ;
-
-        const lat = φ2.toDegrees();
-        const lon = λ2.toDegrees();
-
-        return new LatLonSpherical(lat, lon);
-    }
-
-
-    /**
-     * Returns the loxodromic midpoint (along a rhumb line) between ‘this’ point and second point.
-     *
-     * @param   {LatLon} point - Latitude/longitude of second point.
-     * @returns {LatLon} Midpoint between this point and second point.
-     *
-     * @example
-     *   const p1 = new LatLon(51.127, 1.338);
-     *   const p2 = new LatLon(50.964, 1.853);
-     *   const pMid = p1.rhumbMidpointTo(p2); // 51.0455°N, 001.5957°E
-     */
-    rhumbMidpointTo(point) {
-        if (!(point instanceof LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
-
-        // see mathforum.org/kb/message.jspa?messageID=148837
-
-        const φ1 = this.lat.toRadians(); let λ1 = this.lon.toRadians();
-        const φ2 = point.lat.toRadians(), λ2 = point.lon.toRadians();
-
-        if (Math.abs(λ2 - λ1) > π) λ1 += 2 * π; // crossing anti-meridian
-
-        const φ3 = (φ1 + φ2) / 2;
-        const f1 = Math.tan(π / 4 + φ1 / 2);
-        const f2 = Math.tan(π / 4 + φ2 / 2);
-        const f3 = Math.tan(π / 4 + φ3 / 2);
-        let λ3 = ((λ2 - λ1) * Math.log(f3) + λ1 * Math.log(f2) - λ2 * Math.log(f1)) / Math.log(f2 / f1);
-
-        if (!isFinite(λ3)) λ3 = (λ1 + λ2) / 2; // parallel of latitude
-
-        const lat = φ3.toDegrees();
-        const lon = λ3.toDegrees();
-
-        return new LatLonSpherical(lat, lon);
-    }
-
-
-    /* Area - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-    /**
-     * Calculates the area of a spherical polygon where the sides of the polygon are great circle
-     * arcs joining the vertices.
-     *
-     * @param   {LatLon[]} polygon - Array of points defining vertices of the polygon.
-     * @param   {number}   [radius=6371e3] - (Mean) radius of earth (defaults to radius in metres).
-     * @returns {number}   The area of the polygon in the same units as radius.
-     *
-     * @example
-     *   const polygon = [new LatLon(0,0), new LatLon(1,0), new LatLon(0,1)];
-     *   const area = LatLon.areaOf(polygon); // 6.18e9 m²
-     */
-    static areaOf(polygon, radius=6371e3) {
-        // 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(φ₁/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)
-
-        const R = radius;
-
-        // close polygon so that last point equals first point
-        const closed = polygon[0].equals(polygon[polygon.length-1]);
-        if (!closed) polygon.push(polygon[0]);
-
-        const nVertices = polygon.length - 1;
-
-        let S = 0; // spherical excess in steradians
-        for (let v=0; v Number.EPSILON) return false;
-        if (Math.abs(this.lon - point.lon) > Number.EPSILON) return false;
-
-        return true;
-    }
-
-
-    /**
-     * Converts ‘this’ point to a GeoJSON object.
-     *
-     * @returns {Object} this point as a GeoJSON ‘Point’ object.
-     */
-    toGeoJSON() {
-        return { type: 'Point', coordinates: [ this.lon, this.lat ] };
-    }
-
-
-    /**
-     * Returns a string representation of ‘this’ point, formatted as degrees, degrees+minutes, or
-     * degrees+minutes+seconds.
-     *
-     * @param   {string} [format=d] - Format point as 'd', 'dm', 'dms', or 'n' for signed numeric.
-     * @param   {number} [dp=4|2|0] - Number of decimal places to use: default 4 for d, 2 for dm, 0 for dms.
-     * @returns {string} Comma-separated formatted latitude/longitude.
-     * @throws  {RangeError} Invalid format.
-     *
-     * @example
-     *   const greenwich = new LatLon(51.47788, -0.00147);
-     *   const d = greenwich.toString();                        // 51.4779°N, 000.0015°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.4779, -0.0015
-     */
-    toString(format='d', dp=undefined) {
-        // 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 (format == 'n') { // signed numeric degrees
-            if (dp == undefined) dp = 4;
-            return `${this.lat.toFixed(dp)},${this.lon.toFixed(dp)}`;
-        }
-        const lat = Dms.toLat(this.lat, format, dp);
-        const lon = Dms.toLon(this.lon, format, dp);
-        return `${lat}, ${lon}`;
-    }
-
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { LatLonSpherical as default, Dms };
diff --git a/src/js/geo/mgrs.js b/src/js/geo/mgrs.js
deleted file mode 100644
index a0030f5..0000000
--- a/src/js/geo/mgrs.js
+++ /dev/null
@@ -1,305 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* MGRS / UTM Conversion Functions                                    (c) Chris Veness 2014-2022  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong-utm-mgrs.html                                           */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#mgrs                                       */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import Utm, { LatLon as LatLonEllipsoidal, Dms } from './utm.js';
-
-
-/**
- * Military Grid Reference System (MGRS/NATO) grid references provides geocoordinate references
- * covering the entire globe, based on UTM projections.
- *
- * MGRS references comprise a grid zone designator, a 100km square identification, and an easting
- * and northing (in metres); e.g. ‘31U DQ 48251 11932’.
- *
- * Depending on requirements, some parts of the reference may be omitted (implied), and
- * eastings/northings may be given to varying resolution.
- *
- * qv www.fgdc.gov/standards/projects/FGDC-standards-projects/usng/fgdc_std_011_2001_usng.pdf
- *
- * @module mgrs
- */
-
-
-/*
- * Latitude bands C..X 8° each, covering 80°S to 84°N
- */
-const latBands = 'CDEFGHJKLMNPQRSTUVWXX'; // X is repeated for 80-84°N
-
-
-/*
- * 100km grid square column (‘e’) letters repeat every third zone
- */
-const e100kLetters = [ 'ABCDEFGH', 'JKLMNPQR', 'STUVWXYZ' ];
-
-
-/*
- * 100km grid square row (‘n’) letters repeat every other zone
- */
-const n100kLetters = [ 'ABCDEFGHJKLMNPQRSTUV', 'FGHJKLMNPQRSTUVABCDE' ];
-
-
-/* Mgrs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Military Grid Reference System (MGRS/NATO) grid references, with methods to parse references, and
- * to convert to UTM coordinates.
- */
-class Mgrs {
-
-    /**
-     * Creates an Mgrs grid reference object.
-     *
-     * @param  {number} zone - 6° longitudinal zone (1..60 covering 180°W..180°E).
-     * @param  {string} band - 8° latitudinal band (C..X covering 80°S..84°N).
-     * @param  {string} e100k - First letter (E) of 100km grid square.
-     * @param  {string} n100k - Second letter (N) of 100km grid square.
-     * @param  {number} easting - Easting in metres within 100km grid square.
-     * @param  {number} northing - Northing in metres within 100km grid square.
-     * @param  {LatLon.datums} [datum=WGS84] - Datum UTM coordinate is based on.
-     * @throws {RangeError}  Invalid MGRS grid reference.
-     *
-     * @example
-     *   import Mgrs from '/js/geodesy/mgrs.js';
-     *   const mgrsRef = new Mgrs(31, 'U', 'D', 'Q', 48251, 11932); // 31U DQ 48251 11932
-     */
-    constructor(zone, band, e100k, n100k, easting, northing, datum=LatLonEllipsoidal.datums.WGS84) {
-        if (!(1<=zone && zone<=60)) throw new RangeError(`invalid MGRS zone ‘${zone}’`);
-        if (zone != parseInt(zone)) throw new RangeError(`invalid MGRS zone ‘${zone}’`);
-        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 (Number(easting) < 0 || Number(easting) > 99999) errors.push(`invalid MGRS easting ‘${easting}’`);
-        if (Number(northing) < 0 || Number(northing) > 99999) errors.push(`invalid MGRS northing ‘${northing}’`);
-        if (!datum || datum.ellipsoid==undefined) errors.push(`unrecognised datum ‘${datum}’`);
-        if (errors.length > 0) throw new RangeError(errors.join(', '));
-
-        this.zone = Number(zone);
-        this.band = band;
-        this.e100k = e100k;
-        this.n100k = n100k;
-        this.easting = Math.floor(easting);
-        this.northing = Math.floor(northing);
-        this.datum = datum;
-    }
-
-
-    /**
-     * Converts MGRS grid reference to UTM coordinate.
-     *
-     * 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 = Mgrs.parse('31U DQ 48251 11932');
-     *   const utmCoord = mgrsRef.toUtm(); // 31 N 448251 5411932
-     */
-    toUtm() {
-        const hemisphere = this.band>='N' ? 'N' : 'S';
-
-        // get easting specified by e100k (note +1 because eastings start at 166e3 due to 500km false origin)
-        const col = e100kLetters[(this.zone-1)%3].indexOf(this.e100k) + 1;
-        const e100kNum = col * 100e3; // e100k in metres
-
-        // get northing specified by n100k
-        const row = n100kLetters[(this.zone-1)%2].indexOf(this.n100k);
-        const n100kNum = row * 100e3; // n100k in metres
-
-        // get latitude of (bottom of) band (10 bands above the equator, 8°latitude each)
-        const latBand = (latBands.indexOf(this.band)-10)*8;
-
-        // get southern-most northing of bottom of band, using floor() to extend to include entirety
-        // of bottom-most 100km square - note in northern hemisphere, centre of zone will be furthest
-        // south; in southern hemisphere extremity of zone will be furthest south, so use 3°E / 0°E
-        const lon = this.band >= 'N' ? 3 : 0;
-        const nBand = Math.floor(new LatLonEllipsoidal(latBand, lon).toUtm().northing/100e3)*100e3;
-
-        // 100km grid square row letters repeat every 2,000km north; add enough 2,000km blocks to
-        // get into required band
-        let n2M = 0; // northing of 2,000km block
-        while (n2M + n100kNum + this.northing < nBand) n2M += 2000e3;
-
-        return new Utm_Mgrs(this.zone, hemisphere, e100kNum+this.easting, n2M+n100kNum+this.northing, this.datum);
-    }
-
-
-    /**
-     * Parses string representation of MGRS grid reference.
-     *
-     * An MGRS grid reference comprises (space-separated)
-     *  - grid zone designator (GZD)
-     *  - 100km grid square letter-pair
-     *  - easting
-     *  - northing.
-     *
-     * @param   {string} mgrsGridRef - String representation of MGRS grid reference.
-     * @returns {Mgrs}   Mgrs grid reference object.
-     * @throws  {Error}  Invalid MGRS grid reference.
-     *
-     * @example
-     *   const mgrsRef = Mgrs.parse('31U DQ 48251 11932');
-     *   const mgrsRef = Mgrs.parse('31UDQ4825111932'); // military style no separators
-     *   //  mgrsRef: { zone:31, band:'U', e100k:'D', n100k:'Q', easting:48251, northing:11932 }
-     */
-    static parse(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/)) { // convert mgrsGridRef to standard space-separated format
-            const ref = mgrsGridRef.match(/(\d\d?[A-Z])([A-Z]{2})([0-9]{2,10})/i);
-            if (!ref) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);
-
-            const [ , gzd, en100k, en ] = ref;  // split grid ref into gzd, en100k, en
-            const [ easting, northing ] = [ en.slice(0, en.length/2), en.slice(-en.length/2) ];
-            mgrsGridRef = `${gzd} ${en100k} ${easting} ${northing}`;
-        }
-
-        // match separate elements (separated by whitespace)
-        const ref = mgrsGridRef.match(/\S+/g); // returns [ gzd, e100k, easting, northing ]
-        if (ref==null || ref.length!=4) throw new Error(`invalid MGRS grid reference ‘${mgrsGridRef}’`);
-
-        const [ gzd, en100k, e, n ] = ref;                     // split grid ref into gzd, en100k, e, n
-        const [ , zone, band ] = gzd.match(/(\d\d?)([A-Z])/i); // split gzd into zone, band
-        const [ e100k, n100k ] = en100k.split('');             // split 100km letter-pair into e, n
-
-        // standardise to 10-digit refs - ie metres) (but only if < 10-digit refs, to allow decimals)
-        const easting = e.length>=5 ?  e : e.padEnd(5, '0');
-        const northing = n.length>=5 ?  n : n.padEnd(5, '0');
-
-        return new Mgrs(zone, band, e100k, n100k, easting, northing);
-    }
-
-
-    /**
-     * Returns a string representation of an MGRS grid reference.
-     *
-     * To distinguish from civilian UTM coordinate representations, no space is included within the
-     * zone/band grid zone designator.
-     *
-     * Components are separated by spaces: for a military-style unseparated string, use
-     *   Mgrs.toString().replace(/ /g, '');
-     *
-     * 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).
-     *
-     * @param   {number}     [digits=10] - Precision of returned grid reference (eg 4 = km, 10 = m).
-     * @returns {string}     This grid reference in standard format.
-     * @throws  {RangeError} Invalid precision.
-     *
-     * @example
-     *   const mgrsStr = new Mgrs(31, 'U', 'D', 'Q', 48251, 11932).toString(); // 31U DQ 48251 11932
-     */
-    toString(digits=10) {
-        if (![ 2, 4, 6, 8, 10 ].includes(Number(digits))) throw new RangeError(`invalid precision ‘${digits}’`);
-
-        const { zone, band, e100k, n100k, easting, northing } = this;
-
-        // truncate to required precision
-        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 zPadded = zone.toString().padStart(2, '0');
-        const ePadded = eRounded.toString().padStart(digits/2, '0');
-        const nPadded = nRounded.toString().padStart(digits/2, '0');
-
-        return `${zPadded}${band} ${e100k}${n100k} ${ePadded} ${nPadded}`;
-    }
-}
-
-
-/* Utm_Mgrs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Extends Utm with method to convert UTM coordinate to MGRS reference.
- *
- * @extends Utm
- */
-class Utm_Mgrs extends Utm {
-
-    /**
-     * Converts UTM coordinate to MGRS reference.
-     *
-     * @returns {Mgrs}
-     * @throws  {TypeError} Invalid UTM coordinate.
-     *
-     * @example
-     *   const utmCoord = new Utm(31, 'N', 448251, 5411932);
-     *   const mgrsRef = utmCoord.toMgrs(); // 31U DQ 48251 11932
-     */
-    toMgrs() {
-        // MGRS zone is same as UTM zone
-        const zone = this.zone;
-
-        // convert UTM to lat/long to get latitude to determine band
-        const latlong = this.toLatLon();
-        // grid zones are 8° tall, 0°N is 10th band
-        const band = latBands.charAt(Math.floor(latlong.lat.toFixed(12)/8+10)); // latitude band
-
-        // columns in zone 1 are A-H, zone 2 J-R, zone 3 S-Z, then repeating every 3rd zone
-        const col = Math.floor(this.easting / 100e3);
-        // (note -1 because eastings start at 166e3 due to 500km false origin)
-        const e100k = e100kLetters[(zone-1)%3].charAt(col-1);
-
-        // rows in even zones are A-V, in odd zones are F-E
-        const row = Math.floor(this.northing / 100e3) % 20;
-        const n100k = n100kLetters[(zone-1)%2].charAt(row);
-
-        // truncate easting/northing to within 100km grid square & round to 1-metre precision
-        const easting = Math.floor(this.easting % 100e3);
-        const northing = Math.floor(this.northing % 100e3);
-
-        return new Mgrs(zone, band, e100k, n100k, easting, northing);
-    }
-
-}
-
-
-/**
- * Extends LatLonEllipsoidal adding toMgrs() method to the Utm object returned by LatLon.toUtm().
- *
- * @extends LatLonEllipsoidal
- */
-class Latlon_Utm_Mgrs extends LatLonEllipsoidal {
-
-    /**
-     * Converts latitude/longitude to UTM coordinate.
-     *
-     * Shadow of LatLon.toUtm, returning Utm augmented with toMgrs() method.
-     *
-     * @param   {number} [zoneOverride] - Use specified zone rather than zone within which point lies;
-     *          note overriding the UTM zone has the potential to result in negative eastings, and
-     *          perverse results within Norway/Svalbard exceptions (this is unlikely to be relevant
-     *          for MGRS, but is needed as Mgrs passes through the Utm class).
-     * @returns {Utm}   UTM coordinate.
-     * @throws  {Error} If point not valid, if point outside latitude range.
-     *
-     * @example
-     *   const latlong = new LatLon(48.8582, 2.2945);
-     *   const utmCoord = latlong.toUtm(); // 31 N 448252 5411933
-     */
-    toUtm(zoneOverride=undefined) {
-        const utm = super.toUtm(zoneOverride);
-        return new Utm_Mgrs(utm.zone, utm.hemisphere, utm.easting, utm.northing, utm.datum, utm.convergence, utm.scale);
-    }
-
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { Mgrs as default, Utm_Mgrs as Utm, Latlon_Utm_Mgrs as LatLon, Dms };
diff --git a/src/js/geo/osgridref.js b/src/js/geo/osgridref.js
deleted file mode 100644
index f73c24e..0000000
--- a/src/js/geo/osgridref.js
+++ /dev/null
@@ -1,348 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Ordnance Survey Grid Reference functions                           (c) Chris Veness 2005-2021  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong-gridref.html                                            */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#osgridref                                  */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-import LatLonEllipsoidal, { Dms } from './latlon-ellipsoidal-datum.js';
-
-
-/**
- * Ordnance Survey OSGB grid references provide geocoordinate references for UK mapping purposes.
- *
- * Formulation implemented here due to Thomas, Redfearn, etc is as published by OS, but is inferior
- * to Krüger as used by e.g. Karney 2011.
- *
- * www.ordnancesurvey.co.uk/documents/resources/guide-coordinate-systems-great-britain.pdf.
- *
- * Note OSGB grid references cover Great Britain only; Ireland and the Channel Islands have their
- * own references.
- *
- * Note that these formulae are based on ellipsoidal calculations, and according to the OS are
- * accurate to about 4–5 metres – for greater accuracy, a geoid-based transformation (OSTN15) must
- * be used.
- */
-
-/*
- * Converted 2015 to work with WGS84 by default, OSGB36 as option;
- * www.ordnancesurvey.co.uk/blog/2014/12/confirmation-on-changes-to-latitude-and-longitude
- */
-
-
-/* OsGridRef  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-const nationalGrid = {
-    trueOrigin:  { lat: 49, lon: -2 },                 // true origin of grid 49°N,2°W on OSGB36 datum
-    falseOrigin: { easting: -400e3, northing: 100e3 }, // easting & northing of false origin, metres from true origin
-    scaleFactor: 0.9996012717,                         // scale factor on central meridian
-    ellipsoid:   LatLonEllipsoidal.ellipsoids.Airy1830,
-};
-// note Irish National Grid uses t/o 53°30′N, 8°W, f/o 200kmW, 250kmS, scale factor 1.000035, on Airy 1830 Modified ellipsoid
-
-
-/**
- * OS Grid References with methods to parse and convert them to latitude/longitude points.
- */
-class OsGridRef {
-
-    /**
-     * Creates an OsGridRef object.
-     *
-     * @param {number} easting - Easting in metres from OS Grid false origin.
-     * @param {number} northing - Northing in metres from OS Grid false origin.
-     *
-     * @example
-     *   import OsGridRef from '/js/geodesy/osgridref.js';
-     *   const gridref = new OsGridRef(651409, 313177);
-     */
-    constructor(easting, northing) {
-        this.easting = Number(easting);
-        this.northing = Number(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}’`);
-    }
-
-
-    /**
-     * Converts ‘this’ Ordnance Survey Grid Reference easting/northing coordinate to latitude/longitude
-     * (SW corner of grid square).
-     *
-     * While OS Grid References are based on OSGB-36, the Ordnance Survey have deprecated the use of
-     * OSGB-36 for latitude/longitude coordinates (in favour of WGS-84), hence this function returns
-     * WGS-84 by default, with OSGB-36 as an option. See www.ordnancesurvey.co.uk/blog/2014/12/2.
-     *
-     * Note formulation implemented here due to Thomas, Redfearn, etc is as published by OS, but is
-     * inferior to Krüger as used by e.g. Karney 2011.
-     *
-     * @param   {LatLon.datum} [datum=WGS84] - Datum to convert grid reference into.
-     * @returns {LatLon}       Latitude/longitude of supplied grid reference.
-     *
-     * @example
-     *   const gridref = new OsGridRef(651409.903, 313177.270);
-     *   const pWgs84 = gridref.toLatLon();                    // 52°39′28.723″N, 001°42′57.787″E
-     *   // to obtain (historical) OSGB36 lat/lon point:
-     *   const pOsgb = gridref.toLatLon(LatLon.datums.OSGB36); // 52°39′27.253″N, 001°43′04.518″E
-     */
-    toLatLon(datum=LatLonEllipsoidal.datums.WGS84) {
-        const { easting: E, northing: N } = this;
-
-        const { a, b } = nationalGrid.ellipsoid;            // a = 6377563.396, b = 6356256.909
-        const φ0 = nationalGrid.trueOrigin.lat.toRadians(); // latitude of true origin, 49°N
-        const λ0 = nationalGrid.trueOrigin.lon.toRadians(); // longitude of true origin, 2°W
-        const E0 = -nationalGrid.falseOrigin.easting;       // easting of true origin, 400km
-        const N0 = -nationalGrid.falseOrigin.northing;      // northing of true origin, -100km
-        const F0 = nationalGrid.scaleFactor;                // 0.9996012717
-
-        const e2 = 1 - (b*b)/(a*a);                         // eccentricity squared
-        const n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n;        // n, n², n³
-
-        let φ=φ0, M=0;
-        do {
-            φ = (N-N0-M)/(a*F0) + φ;
-
-            const Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (φ-φ0);
-            const Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(φ-φ0) * Math.cos(φ+φ0);
-            const Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(φ-φ0)) * Math.cos(2*(φ+φ0));
-            const Md = (35/24)*n3 * Math.sin(3*(φ-φ0)) * Math.cos(3*(φ+φ0));
-            M = b * F0 * (Ma - Mb + Mc - Md);                // meridional arc
-
-        } while (Math.abs(N-N0-M) >= 0.00001);  // ie until < 0.01mm
-
-        const cosφ = Math.cos(φ), sinφ = Math.sin(φ);
-        const ν = a*F0/Math.sqrt(1-e2*sinφ*sinφ);            // nu = transverse radius of curvature
-        const ρ = a*F0*(1-e2)/Math.pow(1-e2*sinφ*sinφ, 1.5); // rho = meridional radius of curvature
-        const η2 = ν/ρ-1;                                    // eta = ?
-
-        const tanφ = Math.tan(φ);
-        const tan2φ = tanφ*tanφ, tan4φ = tan2φ*tan2φ, tan6φ = tan4φ*tan2φ;
-        const secφ = 1/cosφ;
-        const ν3 = ν*ν*ν, ν5 = ν3*ν*ν, ν7 = ν5*ν*ν;
-        const VII = tanφ/(2*ρ*ν);
-        const VIII = tanφ/(24*ρ*ν3)*(5+3*tan2φ+η2-9*tan2φ*η2);
-        const IX = tanφ/(720*ρ*ν5)*(61+90*tan2φ+45*tan4φ);
-        const X = secφ/ν;
-        const XI = secφ/(6*ν3)*(ν/ρ+2*tan2φ);
-        const XII = secφ/(120*ν5)*(5+28*tan2φ+24*tan4φ);
-        const XIIA = secφ/(5040*ν7)*(61+662*tan2φ+1320*tan4φ+720*tan6φ);
-
-        const dE = (E-E0), dE2 = dE*dE, dE3 = dE2*dE, dE4 = dE2*dE2, dE5 = dE3*dE2, dE6 = dE4*dE2, dE7 = dE5*dE2;
-        φ = φ - VII*dE2 + VIII*dE4 - IX*dE6;
-        const λ = λ0 + X*dE - XI*dE3 + XII*dE5 - XIIA*dE7;
-
-        let point = new LatLon_OsGridRef(φ.toDegrees(), λ.toDegrees(), 0, LatLonEllipsoidal.datums.OSGB36);
-
-        if (datum != LatLonEllipsoidal.datums.OSGB36) {
-            // if point is required in datum other than OSGB36, convert it
-            point = point.convertDatum(datum);
-            // convertDatum() gives us a LatLon: convert to LatLon_OsGridRef which includes toOsGrid()
-            point = new LatLon_OsGridRef(point.lat, point.lon, point.height, point.datum);
-        }
-
-        return point;
-    }
-
-
-    /**
-     * Parses grid reference to OsGridRef object.
-     *
-     * Accepts standard grid references (eg 'SU 387 148'), with or without whitespace separators, from
-     * two-digit references up to 10-digit references (1m × 1m square), or fully numeric comma-separated
-     * references in metres (eg '438700,114800').
-     *
-     * @param   {string}    gridref - Standard format OS Grid Reference.
-     * @returns {OsGridRef} Numeric version of grid reference in metres from false origin (SW corner of
-     *   supplied grid square).
-     * @throws  {Error}     Invalid grid reference.
-     *
-     * @example
-     *   const grid = OsGridRef.parse('TG 51409 13177'); // grid: { easting: 651409, northing: 313177 }
-     */
-    static parse(gridref) {
-        gridref = String(gridref).trim();
-
-        // check for fully numeric comma-separated gridref format
-        let match = gridref.match(/^(\d+),\s*(\d+)$/);
-        if (match) return new OsGridRef(match[1], match[2]);
-
-        // validate format
-        match = gridref.match(/^[HNST][ABCDEFGHJKLMNOPQRSTUVWXYZ]\s*[0-9]+\s*[0-9]+$/i);
-        if (!match) throw new Error(`invalid grid reference ‘${gridref}’`);
-
-        // get numeric values of letter references, mapping A->0, B->1, C->2, etc:
-        let l1 = gridref.toUpperCase().charCodeAt(0) - 'A'.charCodeAt(0); // 500km square
-        let l2 = gridref.toUpperCase().charCodeAt(1) - 'A'.charCodeAt(0); // 100km square
-        // shuffle down letters after 'I' since 'I' is not used in grid:
-        if (l1 > 7) l1--;
-        if (l2 > 7) l2--;
-
-        // convert grid letters into 100km-square indexes from false origin (grid square SV):
-        const e100km = ((l1 - 2) % 5) * 5 + (l2 % 5);
-        const n100km = (19 - Math.floor(l1 / 5) * 5) - Math.floor(l2 / 5);
-
-        // skip grid letters to get numeric (easting/northing) part of ref
-        let en = gridref.slice(2).trim().split(/\s+/);
-        // if e/n not whitespace separated, split half way
-        if (en.length == 1) en = [ en[0].slice(0, en[0].length / 2), en[0].slice(en[0].length / 2) ];
-
-        // validation
-        if (en[0].length != en[1].length) throw new Error(`invalid grid reference ‘${gridref}’`);
-
-        // standardise to 10-digit refs (metres)
-        en[0] = en[0].padEnd(5, '0');
-        en[1] = en[1].padEnd(5, '0');
-
-        const e = e100km + en[0];
-        const n = n100km + en[1];
-
-        return new OsGridRef(e, n);
-    }
-
-
-    /**
-     * Converts ‘this’ numeric grid reference to standard OS Grid Reference.
-     *
-     * @param   {number} [digits=10] - Precision of returned grid reference (10 digits = metres);
-     *   digits=0 will return grid reference in numeric format.
-     * @returns {string} This grid reference in standard format.
-     *
-     * @example
-     *   const gridref = new OsGridRef(651409, 313177).toString(8); // 'TG 5140 1317'
-     *   const gridref = new OsGridRef(651409, 313177).toString(0); // '651409,313177'
-     */
-    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
-
-        let { easting: e, northing: n } = this;
-
-        // use digits = 0 to return numeric format (in metres) - note northing may be >= 1e7
-        if (digits == 0) {
-            const format = { useGrouping: false,  minimumIntegerDigits: 6, maximumFractionDigits: 3 };
-            const ePad = e.toLocaleString('en', format);
-            const nPad = n.toLocaleString('en', format);
-            return `${ePad},${nPad}`;
-        }
-
-        // get the 100km-grid indices
-        const e100km = Math.floor(e / 100000), n100km = Math.floor(n / 100000);
-
-        // translate those into numeric equivalents of the grid letters
-        let l1 = (19 - n100km) - (19 - n100km) % 5 + Math.floor((e100km + 10) / 5);
-        let l2 = (19 - n100km) * 5 % 25 + e100km % 5;
-
-        // compensate for skipped 'I' and build grid letter-pairs
-        if (l1 > 7) l1++;
-        if (l2 > 7) l2++;
-        const letterPair = String.fromCharCode(l1 + 'A'.charCodeAt(0), l2 + 'A'.charCodeAt(0));
-
-        // strip 100km-grid indices from easting & northing, and reduce precision
-        e = Math.floor((e % 100000) / Math.pow(10, 5 - digits / 2));
-        n = Math.floor((n % 100000) / Math.pow(10, 5 - digits / 2));
-
-        // pad eastings & northings with leading zeros
-        e = e.toString().padStart(digits/2, '0');
-        n = n.toString().padStart(digits/2, '0');
-
-        return `${letterPair} ${e} ${n}`;
-    }
-
-}
-
-
-/* LatLon_OsGridRef - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Extends LatLon class with method to convert LatLon point to OS Grid Reference.
- *
- * @extends LatLonEllipsoidal
- */
-class LatLon_OsGridRef extends LatLonEllipsoidal {
-
-    /**
-     * Converts latitude/longitude to Ordnance Survey grid reference easting/northing coordinate.
-     *
-     * @returns {OsGridRef} OS Grid Reference easting/northing.
-     *
-     * @example
-     *   const grid = new LatLon(52.65798, 1.71605).toOsGrid(); // TG 51409 13177
-     *   // for conversion of (historical) OSGB36 latitude/longitude point:
-     *   const grid = new LatLon(52.65798, 1.71605).toOsGrid(LatLon.datums.OSGB36);
-     */
-    toOsGrid() {
-        // if necessary convert to OSGB36 first
-        const point = this.datum == LatLonEllipsoidal.datums.OSGB36
-            ? this
-            : this.convertDatum(LatLonEllipsoidal.datums.OSGB36);
-
-        const φ = point.lat.toRadians();
-        const λ = point.lon.toRadians();
-
-        const { a, b } = nationalGrid.ellipsoid;            // a = 6377563.396, b = 6356256.909
-        const φ0 = nationalGrid.trueOrigin.lat.toRadians(); // latitude of true origin, 49°N
-        const λ0 = nationalGrid.trueOrigin.lon.toRadians(); // longitude of true origin, 2°W
-        const E0 = -nationalGrid.falseOrigin.easting;       // easting of true origin, 400km
-        const N0 = -nationalGrid.falseOrigin.northing;      // northing of true origin, -100km
-        const F0 = nationalGrid.scaleFactor;                // 0.9996012717
-
-        const e2 = 1 - (b*b)/(a*a);                          // eccentricity squared
-        const n = (a-b)/(a+b), n2 = n*n, n3 = n*n*n;         // n, n², n³
-
-        const cosφ = Math.cos(φ), sinφ = Math.sin(φ);
-        const ν = a*F0/Math.sqrt(1-e2*sinφ*sinφ);            // nu = transverse radius of curvature
-        const ρ = a*F0*(1-e2)/Math.pow(1-e2*sinφ*sinφ, 1.5); // rho = meridional radius of curvature
-        const η2 = ν/ρ-1;                                    // eta = ?
-
-        const Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (φ-φ0);
-        const Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(φ-φ0) * Math.cos(φ+φ0);
-        const Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(φ-φ0)) * Math.cos(2*(φ+φ0));
-        const Md = (35/24)*n3 * Math.sin(3*(φ-φ0)) * Math.cos(3*(φ+φ0));
-        const M = b * F0 * (Ma - Mb + Mc - Md);              // meridional arc
-
-        const cos3φ = cosφ*cosφ*cosφ;
-        const cos5φ = cos3φ*cosφ*cosφ;
-        const tan2φ = Math.tan(φ)*Math.tan(φ);
-        const tan4φ = tan2φ*tan2φ;
-
-        const I = M + N0;
-        const II = (ν/2)*sinφ*cosφ;
-        const III = (ν/24)*sinφ*cos3φ*(5-tan2φ+9*η2);
-        const IIIA = (ν/720)*sinφ*cos5φ*(61-58*tan2φ+tan4φ);
-        const IV = ν*cosφ;
-        const V = (ν/6)*cos3φ*(ν/ρ-tan2φ);
-        const VI = (ν/120) * cos5φ * (5 - 18*tan2φ + tan4φ + 14*η2 - 58*tan2φ*η2);
-
-        const Δλ = λ-λ0;
-        const Δλ2 = Δλ*Δλ, Δλ3 = Δλ2*Δλ, Δλ4 = Δλ3*Δλ, Δλ5 = Δλ4*Δλ, Δλ6 = Δλ5*Δλ;
-
-        let N = I + II*Δλ2 + III*Δλ4 + IIIA*Δλ6;
-        let E = E0 + IV*Δλ + V*Δλ3 + VI*Δλ5;
-
-        N = Number(N.toFixed(3)); // round to mm precision
-        E = Number(E.toFixed(3));
-
-        try {
-            return new OsGridRef(E, N); // note: gets truncated to SW corner of 1m grid square
-        } catch (e) {
-            throw new Error(`${e.message} from (${point.lat.toFixed(6)},${point.lon.toFixed(6)}).toOsGrid()`);
-        }
-    }
-
-
-    /**
-     * Override LatLonEllipsoidal.convertDatum() with version which returns LatLon_OsGridRef.
-     */
-    convertDatum(toDatum) {
-        const osgbED = super.convertDatum(toDatum); // returns LatLonEllipsoidal_Datum
-        const osgbOSGR = new LatLon_OsGridRef(osgbED.lat, osgbED.lon, osgbED.height, osgbED.datum);
-        return osgbOSGR;
-    }
-
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { OsGridRef as default, LatLon_OsGridRef as LatLon, Dms };
diff --git a/src/js/geo/utm.js b/src/js/geo/utm.js
deleted file mode 100644
index 6a6c625..0000000
--- a/src/js/geo/utm.js
+++ /dev/null
@@ -1,379 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* UTM / WGS-84 Conversion Functions                                  (c) Chris Veness 2014-2022  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/latlong-utm-mgrs.html                                           */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#utm                                        */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-/* eslint-disable indent */
-
-import LatLonEllipsoidal, { Dms } from './latlon-ellipsoidal-datum.js';
-
-
-/**
- * The Universal Transverse Mercator (UTM) system is a 2-dimensional Cartesian coordinate system
- * providing locations on the surface of the Earth.
- *
- * UTM is a set of 60 transverse Mercator projections, normally based on the WGS-84 ellipsoid.
- * Within each zone, coordinates are represented as eastings and northings, measures in metres; e.g.
- * ‘31 N 448251 5411932’.
- *
- * This method based on Karney 2011 ‘Transverse Mercator with an accuracy of a few nanometers’,
- * building on Krüger 1912 ‘Konforme Abbildung des Erdellipsoids in der Ebene’.
- *
- * @module utm
- */
-
-
-/* Utm  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * UTM coordinates, with functions to parse them and convert them to LatLon points.
- */
-class Utm {
-
-    /**
-     * Creates a Utm coordinate object comprising zone, hemisphere, easting, northing on a given
-     * datum (normally WGS84).
-     *
-     * @param  {number}        zone - UTM 6° longitudinal zone (1..60 covering 180°W..180°E).
-     * @param  {string}        hemisphere - N for northern hemisphere, S for southern hemisphere.
-     * @param  {number}        easting - Easting in metres from false easting (-500km from central meridian).
-     * @param  {number}        northing - Northing in metres from equator (N) or from false northing -10,000km (S).
-     * @param  {LatLon.datums} [datum=WGS84] - Datum UTM coordinate is based on.
-     * @param  {number}        [convergence=null] - Meridian convergence (bearing of grid north
-     *                         clockwise from true north), in degrees.
-     * @param  {number}        [scale=null] - Grid scale factor.
-     * @params {boolean=true}  verifyEN - Check easting/northing is within 'normal' values (may be
-     *                         suppressed for extended coherent coordinates or alternative datums
-     *                         e.g. ED50 (epsg.io/23029).
-     * @throws {TypeError} Invalid UTM coordinate.
-     *
-     * @example
-     *   import Utm from '/js/geodesy/utm.js';
-     *   const utmCoord = new Utm(31, 'N', 448251, 5411932);
-     */
-    constructor(zone, hemisphere, easting, northing, datum=LatLonEllipsoidal.datums.WGS84, convergence=null, scale=null, verifyEN=true) {
-        if (!(1<=zone && zone<=60)) throw new RangeError(`invalid UTM zone ‘${zone}’`);
-        if (zone != parseInt(zone)) throw new RangeError(`invalid UTM zone ‘${zone}’`);
-        if (typeof hemisphere != 'string' || !hemisphere.match(/[NS]/i)) throw new RangeError(`invalid UTM hemisphere ‘${hemisphere}’`);
-        if (verifyEN) { // (rough) range-check of E/N values
-            if (!(0<=easting && easting<=1000e3)) throw new RangeError(`invalid UTM easting ‘${easting}’`);
-            if (hemisphere.toUpperCase()=='N' && !(0<=northing && northing<9329006)) throw new RangeError(`invalid UTM northing ‘${northing}’`);
-            if (hemisphere.toUpperCase()=='S' && !(1116914 1e-12); // using IEEE 754 δτi -> 0 after 2-3 iterations
-        // note relatively large convergence test as δτi toggles on ±1.12e-16 for eg 31 N 400000 5000000
-        const τ = τi;
-
-        const φ = Math.atan(τ);
-
-        let λ = Math.atan2(sinhηʹ, cosξʹ);
-
-        // ---- convergence: Karney 2011 Eq 26, 27
-
-        let p = 1;
-        for (let j=1; j<=6; j++) p -= 2*j*β[j] * Math.cos(2*j*ξ) * Math.cosh(2*j*η);
-        let q = 0;
-        for (let j=1; j<=6; j++) q += 2*j*β[j] * Math.sin(2*j*ξ) * Math.sinh(2*j*η);
-
-        const γʹ = Math.atan(Math.tan(ξʹ) * Math.tanh(ηʹ));
-        const γʺ = Math.atan2(q, p);
-
-        const γ = γʹ + γʺ;
-
-        // ---- scale: Karney 2011 Eq 28
-
-        const sinφ = Math.sin(φ);
-        const kʹ = Math.sqrt(1 - e*e*sinφ*sinφ) * Math.sqrt(1 + τ*τ) * Math.sqrt(sinhηʹ*sinhηʹ + cosξʹ*cosξʹ);
-        const kʺ = A / a / Math.sqrt(p*p + q*q);
-
-        const k = k0 * kʹ * kʺ;
-
-        // ------------
-
-        const λ0 = ((z-1)*6 - 180 + 3).toRadians(); // longitude of central meridian
-        λ += λ0; // move λ from zonal to global coordinates
-
-        // round to reasonable precision
-        const lat = Number(φ.toDegrees().toFixed(14)); // nm precision (1nm = 10^-14°)
-        const lon = Number(λ.toDegrees().toFixed(14)); // (strictly lat rounding should be φ⋅cosφ!)
-        const convergence = Number(γ.toDegrees().toFixed(9));
-        const scale = Number(k.toFixed(12));
-
-        const latLong = new LatLon_Utm(lat, lon, 0, this.datum);
-        // ... and add the convergence and scale into the LatLon object ... wonderful JavaScript!
-        latLong.convergence = convergence;
-        latLong.scale = scale;
-
-        return latLong;
-    }
-
-
-    /**
-     * Parses string representation of UTM coordinate.
-     *
-     * A UTM coordinate comprises (space-separated)
-     *  - zone
-     *  - hemisphere
-     *  - easting
-     *  - northing.
-     *
-     * @param   {string} utmCoord - UTM coordinate (WGS 84).
-     * @param   {Datum}  [datum=WGS84] - Datum coordinate is defined in (default WGS 84).
-     * @returns {Utm} Parsed UTM coordinate.
-     * @throws  {TypeError} Invalid UTM coordinate.
-     *
-     * @example
-     *   const utmCoord = Utm.parse('31 N 448251 5411932');
-     *   // utmCoord: {zone: 31, hemisphere: 'N', easting: 448251, northing: 5411932 }
-     */
-    static parse(utmCoord, datum=LatLonEllipsoidal.datums.WGS84) {
-        // match separate elements (separated by whitespace)
-        utmCoord = utmCoord.trim().match(/\S+/g);
-
-        if (utmCoord==null || utmCoord.length!=4) throw new Error(`invalid UTM coordinate ‘${utmCoord}’`);
-
-        const zone = utmCoord[0], hemisphere = utmCoord[1], easting = utmCoord[2], northing = utmCoord[3];
-
-        return new this(zone, hemisphere, easting, northing, datum); // 'new this' as may return subclassed types
-    }
-
-
-    /**
-     * Returns a string representation of a UTM coordinate.
-     *
-     * To distinguish from MGRS grid zone designators, a space is left between the zone and the
-     * hemisphere.
-     *
-     * Note that UTM coordinates get rounded, not truncated (unlike MGRS grid references).
-     *
-     * @param   {number} [digits=0] - Number of digits to appear after the decimal point (3 ≡ mm).
-     * @returns {string} A string representation of the coordinate.
-     *
-     * @example
-     *   const utm = new Utm('31', 'N', 448251, 5411932).toString(4);  // 31 N 448251.0000 5411932.0000
-     */
-    toString(digits=0) {
-
-        const z = this.zone.toString().padStart(2, '0');
-        const h = this.hemisphere;
-        const e = this.easting.toFixed(digits);
-        const n = this.northing.toFixed(digits);
-
-        return `${z} ${h} ${e} ${n}`;
-    }
-
-}
-
-
-/* LatLon_Utm - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Extends LatLon with method to convert LatLon points to UTM coordinates.
- *
- * @extends LatLon
- */
-class LatLon_Utm extends LatLonEllipsoidal {
-
-    /**
-     * Converts latitude/longitude to UTM coordinate.
-     *
-     * Implements Karney’s method, using Krüger series to order n⁶, giving results accurate to 5nm
-     * for distances up to 3900km from the central meridian.
-     *
-     * @param   {number} [zoneOverride] - Use specified zone rather than zone within which point lies;
-     *          note overriding the UTM zone has the potential to result in negative eastings, and
-     *          perverse results within Norway/Svalbard exceptions.
-     * @returns {Utm} UTM coordinate.
-     * @throws  {TypeError} Latitude outside UTM limits.
-     *
-     * @example
-     *   const latlong = new LatLon(48.8582, 2.2945);
-     *   const utmCoord = latlong.toUtm(); // 31 N 448252 5411933
-     */
-    toUtm(zoneOverride=undefined) {
-        if (!(-80<=this.lat && this.lat<=84)) throw new RangeError(`latitude ‘${this.lat}’ outside UTM limits`);
-
-        const falseEasting = 500e3, falseNorthing = 10000e3;
-
-        let zone = zoneOverride || Math.floor((this.lon+180)/6) + 1; // longitudinal zone
-        let λ0 = ((zone-1)*6 - 180 + 3).toRadians(); // longitude of central meridian
-
-        // ---- handle Norway/Svalbard exceptions
-        // grid zones are 8° tall; 0°N is offset 10 into latitude bands array
-        const mgrsLatBands = 'CDEFGHJKLMNPQRSTUVWXX'; // X is repeated for 80-84°N
-        const latBand = mgrsLatBands.charAt(Math.floor(this.lat/8+10));
-        // adjust zone & central meridian for Norway
-        if (zone==31 && latBand=='V' && this.lon>= 3) { zone++; λ0 += (6).toRadians(); }
-        // adjust zone & central meridian for Svalbard
-        if (zone==32 && latBand=='X' && this.lon<  9) { zone--; λ0 -= (6).toRadians(); }
-        if (zone==32 && latBand=='X' && this.lon>= 9) { zone++; λ0 += (6).toRadians(); }
-        if (zone==34 && latBand=='X' && this.lon< 21) { zone--; λ0 -= (6).toRadians(); }
-        if (zone==34 && latBand=='X' && this.lon>=21) { zone++; λ0 += (6).toRadians(); }
-        if (zone==36 && latBand=='X' && this.lon< 33) { zone--; λ0 -= (6).toRadians(); }
-        if (zone==36 && latBand=='X' && this.lon>=33) { zone++; λ0 += (6).toRadians(); }
-
-        const φ = this.lat.toRadians();      // latitude ± from equator
-        const λ = this.lon.toRadians() - λ0; // longitude ± from central meridian
-
-        // 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;
-
-        const k0 = 0.9996; // UTM scale on the central meridian
-
-        // ---- easting, northing: Karney 2011 Eq 7-14, 29, 35:
-
-        const e = Math.sqrt(f*(2-f)); // eccentricity
-        const n = f / (2 - f);        // 3rd flattening
-        const n2 = n*n, n3 = n*n2, n4 = n*n3, n5 = n*n4, n6 = n*n5;
-
-        const cosλ = Math.cos(λ), sinλ = Math.sin(λ), tanλ = Math.tan(λ);
-
-        const τ = Math.tan(φ); // τ ≡ tanφ, τʹ ≡ tanφʹ; prime (ʹ) indicates angles on the conformal sphere
-        const σ = Math.sinh(e*Math.atanh(e*τ/Math.sqrt(1+τ*τ)));
-
-        const τʹ = τ*Math.sqrt(1+σ*σ) - σ*Math.sqrt(1+τ*τ);
-
-        const ξʹ = Math.atan2(τʹ, cosλ);
-        const ηʹ = Math.asinh(sinλ / Math.sqrt(τʹ*τʹ + cosλ*cosλ));
-
-        const A = a/(1+n) * (1 + 1/4*n2 + 1/64*n4 + 1/256*n6); // 2πA is the circumference of a meridian
-
-        const α = [ null, // note α is one-based array (6th order Krüger expressions)
-            1/2*n - 2/3*n2 + 5/16*n3 +   41/180*n4 -     127/288*n5 +      7891/37800*n6,
-                  13/48*n2 -  3/5*n3 + 557/1440*n4 +     281/630*n5 - 1983433/1935360*n6,
-                           61/240*n3 -  103/140*n4 + 15061/26880*n5 +   167603/181440*n6,
-                                   49561/161280*n4 -     179/168*n5 + 6601661/7257600*n6,
-                                                     34729/80640*n5 - 3418889/1995840*n6,
-                                                                  212378941/319334400*n6 ];
-
-        let ξ = ξʹ;
-        for (let j=1; j<=6; j++) ξ += α[j] * Math.sin(2*j*ξʹ) * Math.cosh(2*j*ηʹ);
-
-        let η = ηʹ;
-        for (let j=1; j<=6; j++) η += α[j] * Math.cos(2*j*ξʹ) * Math.sinh(2*j*ηʹ);
-
-        let x = k0 * A * η;
-        let y = k0 * A * ξ;
-
-        // ---- convergence: Karney 2011 Eq 23, 24
-
-        let pʹ = 1;
-        for (let j=1; j<=6; j++) pʹ += 2*j*α[j] * Math.cos(2*j*ξʹ) * Math.cosh(2*j*ηʹ);
-        let qʹ = 0;
-        for (let j=1; j<=6; j++) qʹ += 2*j*α[j] * Math.sin(2*j*ξʹ) * Math.sinh(2*j*ηʹ);
-
-        const γʹ = Math.atan(τʹ / Math.sqrt(1+τʹ*τʹ)*tanλ);
-        const γʺ = Math.atan2(qʹ, pʹ);
-
-        const γ = γʹ + γʺ;
-
-        // ---- scale: Karney 2011 Eq 25
-
-        const sinφ = Math.sin(φ);
-        const kʹ = Math.sqrt(1 - e*e*sinφ*sinφ) * Math.sqrt(1 + τ*τ) / Math.sqrt(τʹ*τʹ + cosλ*cosλ);
-        const kʺ = A / a * Math.sqrt(pʹ*pʹ + qʹ*qʹ);
-
-        const k = k0 * kʹ * kʺ;
-
-        // ------------
-
-        // shift x/y to false origins
-        x = x + falseEasting;             // make x relative to false easting
-        if (y < 0) y = y + falseNorthing; // make y in southern hemisphere relative to false northing
-
-        // round to reasonable precision
-        x = Number(x.toFixed(9)); // nm precision
-        y = Number(y.toFixed(9)); // nm precision
-        const convergence = Number(γ.toDegrees().toFixed(9));
-        const scale = Number(k.toFixed(12));
-
-        const h = this.lat>=0 ? 'N' : 'S'; // hemisphere
-
-        return new Utm(zone, h, x, y, this.datum, convergence, scale, !!zoneOverride);
-    }
-}
-
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export { Utm as default, LatLon_Utm as LatLon, Dms };
diff --git a/src/js/geo/vector3d.js b/src/js/geo/vector3d.js
deleted file mode 100644
index 08ddce6..0000000
--- a/src/js/geo/vector3d.js
+++ /dev/null
@@ -1,256 +0,0 @@
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-/* Vector handling functions                                          (c) Chris Veness 2011-2019  */
-/*                                                                                   MIT Licence  */
-/* www.movable-type.co.uk/scripts/geodesy-library.html#vector3d                                   */
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-
-/**
- * Library of 3-d vector manipulation routines.
- *
- * @module vector3d
- */
-
-
-/* Vector3d - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
-
-
-/**
- * Functions for manipulating generic 3-d vectors.
- *
- * Functions return vectors as return results, so that operations can be chained.
- *
- * @example
- *   const v = v1.cross(v2).dot(v3) // ≡ v1×v2⋅v3
- */
-class Vector3d {
-
-    /**
-     * Creates a 3-d vector.
-     *
-     * @param {number} x - X component of vector.
-     * @param {number} y - Y component of vector.
-     * @param {number} z - Z component of vector.
-     *
-     * @example
-     *   import Vector3d from '/js/geodesy/vector3d.js';
-     *   const v = new Vector3d(0.267, 0.535, 0.802);
-     */
-    constructor(x, y, z) {
-        if (isNaN(x) || isNaN(y) || isNaN(z)) throw new TypeError(`invalid vector [${x},${y},${z}]`);
-
-        this.x = Number(x);
-        this.y = Number(y);
-        this.z = Number(z);
-    }
-
-
-    /**
-     * Length (magnitude or norm) of ‘this’ vector.
-     *
-     * @returns {number} Magnitude of this vector.
-     */
-    get length() {
-        return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z);
-    }
-
-
-    /**
-     * Adds supplied vector to ‘this’ vector.
-     *
-     * @param   {Vector3d} v - Vector to be added to this vector.
-     * @returns {Vector3d} Vector representing sum of this and v.
-     */
-    plus(v) {
-        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');
-
-        return new Vector3d(this.x + v.x, this.y + v.y, this.z + v.z);
-    }
-
-
-    /**
-     * Subtracts supplied vector from ‘this’ vector.
-     *
-     * @param   {Vector3d} v - Vector to be subtracted from this vector.
-     * @returns {Vector3d} Vector representing difference between this and v.
-     */
-    minus(v) {
-        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');
-
-        return new Vector3d(this.x - v.x, this.y - v.y, this.z - v.z);
-    }
-
-
-    /**
-     * Multiplies ‘this’ vector by a scalar value.
-     *
-     * @param   {number}   x - Factor to multiply this vector by.
-     * @returns {Vector3d} Vector scaled by x.
-     */
-    times(x) {
-        if (isNaN(x)) throw new TypeError(`invalid scalar value ‘${x}’`);
-
-        return new Vector3d(this.x * x, this.y * x, this.z * x);
-    }
-
-
-    /**
-     * Divides ‘this’ vector by a scalar value.
-     *
-     * @param   {number}   x - Factor to divide this vector by.
-     * @returns {Vector3d} Vector divided by x.
-     */
-    dividedBy(x) {
-        if (isNaN(x)) throw new TypeError(`invalid scalar value ‘${x}’`);
-
-        return new Vector3d(this.x / x, this.y / x, this.z / x);
-    }
-
-
-    /**
-     * Multiplies ‘this’ vector by the supplied vector using dot (scalar) product.
-     *
-     * @param   {Vector3d} v - Vector to be dotted with this vector.
-     * @returns {number}   Dot product of ‘this’ and v.
-     */
-    dot(v) {
-        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');
-
-        return this.x * v.x + this.y * v.y + this.z * v.z;
-    }
-
-
-    /**
-     * Multiplies ‘this’ vector by the supplied vector using cross (vector) product.
-     *
-     * @param   {Vector3d} v - Vector to be crossed with this vector.
-     * @returns {Vector3d} Cross product of ‘this’ and v.
-     */
-    cross(v) {
-        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');
-
-        const x = this.y * v.z - this.z * v.y;
-        const y = this.z * v.x - this.x * v.z;
-        const z = this.x * v.y - this.y * v.x;
-
-        return new Vector3d(x, y, z);
-    }
-
-
-    /**
-     * Negates a vector to point in the opposite direction.
-     *
-     * @returns {Vector3d} Negated vector.
-     */
-    negate() {
-        return new Vector3d(-this.x, -this.y, -this.z);
-    }
-
-
-    /**
-     * Normalizes a vector to its unit vector
-     * – if the vector is already unit or is zero magnitude, this is a no-op.
-     *
-     * @returns {Vector3d} Normalised version of this vector.
-     */
-    unit() {
-        const norm = this.length;
-        if (norm == 1) return this;
-        if (norm == 0) return this;
-
-        const x = this.x / norm;
-        const y = this.y / norm;
-        const z = this.z / norm;
-
-        return new Vector3d(x, y, z);
-    }
-
-
-    /**
-     * 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 - 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).
-     */
-    angleTo(v, n=undefined) {
-        if (!(v instanceof Vector3d)) throw new TypeError('v is not Vector3d object');
-        if (!(n instanceof Vector3d || n == undefined)) throw new TypeError('n is not Vector3d object');
-
-        // 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;
-        const cosθ = this.dot(v);
-
-        return Math.atan2(sinθ, cosθ);
-    }
-
-
-    /**
-     * Rotates ‘this’ point around an axis by a specified angle.
-     *
-     * @param   {Vector3d} axis - The axis being rotated around.
-     * @param   {number}   angle - The angle of rotation (in degrees).
-     * @returns {Vector3d} The rotated point.
-     */
-    rotateAround(axis, angle) {
-        if (!(axis instanceof Vector3d)) throw new TypeError('axis is not Vector3d object');
-
-        const θ = angle.toRadians();
-
-        // en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle
-        // en.wikipedia.org/wiki/Quaternions_and_spatial_rotation#Quaternion-derived_rotation_matrix
-        const p = this.unit();
-        const a = axis.unit();
-
-        const s = Math.sin(θ);
-        const c = Math.cos(θ);
-        const t = 1-c;
-        const x = a.x, y = a.y, z = a.z;
-
-        const r = [ // rotation matrix for rotation about supplied axis
-            [ t*x*x + c,   t*x*y - s*z, t*x*z + s*y ],
-            [ t*x*y + s*z, t*y*y + c,   t*y*z - s*x ],
-            [ t*x*z - s*y, t*y*z + s*x, t*z*z + c   ],
-        ];
-
-        // multiply r × p
-        const rp = [
-            r[0][0]*p.x + r[0][1]*p.y + r[0][2]*p.z,
-            r[1][0]*p.x + r[1][1]*p.y + r[1][2]*p.z,
-            r[2][0]*p.x + r[2][1]*p.y + r[2][2]*p.z,
-        ];
-        const p2 = new Vector3d(rp[0], rp[1], rp[2]);
-
-        return p2;
-        // qv en.wikipedia.org/wiki/Rodrigues'_rotation_formula...
-    }
-
-
-    /**
-     * String representation of vector.
-     *
-     * @param   {number} [dp=3] - Number of decimal places to be used.
-     * @returns {string} Vector represented as [x,y,z].
-     */
-    toString(dp=3) {
-        return `[${this.x.toFixed(dp)},${this.y.toFixed(dp)},${this.z.toFixed(dp)}]`;
-    }
-
-}
-
-
-// Extend Number object with methods to convert between degrees & radians
-Number.prototype.toRadians = function() { return this * Math.PI / 180; };
-Number.prototype.toDegrees = function() { return this * 180 / Math.PI; };
-
-/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
-
-export default Vector3d;
diff --git a/src/js/globalize.min.js b/src/js/globalize.min.js
deleted file mode 100644
index d60d8db..0000000
--- a/src/js/globalize.min.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/*!
- * Globalize v1.7.0 2021-08-02T11:53Z Released under the MIT license
- * http://git.io/TrdQbw
- */
-!function(e,n){"function"==typeof define&&define.amd?define(["cldr","cldr/event"],n):"object"==typeof exports?module.exports=n(require("cldrjs")):e.Globalize=n(e.Cldr)}(this,(function(e){var n=function(e,n){return e=e.replace(/{[0-9a-zA-Z-_. ]+}/g,(function(e){return e=e.replace(/^{([^}]*)}$/,"$1"),"string"==typeof(t=n[e])?t:"number"==typeof t?""+t:JSON.stringify(t);var t}))},t=function(){var e=arguments[0],n=[].slice.call(arguments,1);return n.forEach((function(n){var t;for(t in n)e[t]=n[t]})),e},r=function(e,r,a){var i;return r=e+(r?": "+n(r,a):""),(i=new Error(r)).code=e,t(i,a),i},a=function(e,n,t){e.length&&e[e.length-1].type===n?e[e.length-1].value+=t:e.push({type:n,value:t})},i=function(e){return JSON.stringify(e,(function(e,n){return n&&n.runtimeKey?n.runtimeKey:n}))},u=function(e,n,t,a){if(!t)throw r(e,n,a)},o=function(e){return Array.isArray(e)?e:e?[e]:[]},c=function(e,n,t){var r;r=o((t=t||{}).skip).some((function(n){return n.test(e)})),u("E_MISSING_CLDR","Missing required CLDR content `{path}`.",n||r,{path:e})},l=function(e,n){u("E_MISSING_PARAMETER","Missing required parameter `{name}`.",void 0!==e,{name:n})},f=function(e,n,t,r){u("E_INVALID_PAR_TYPE","Invalid `{name}` parameter ({value}). {expected} expected.",t,{expected:r,name:n,value:e})},s=function(n,t){f(n,t,void 0===n||"string"==typeof n||n instanceof e,"String or Cldr instance")},d=function(e){return null!==e&&""+e=="[object Object]"},m=function(n){return n instanceof e?n:new e(n)};function v(e){e.once("get",c),e.get("supplemental/likelySubtags")}function _(e){if(!(this instanceof _))return new _(e);l(e,"locale"),s(e,"locale"),this.cldr=m(e),v(this.cldr)}return _.load=function(){e.load.apply(e,arguments)},_.locale=function(e){return s(e,"locale"),arguments.length&&(this.cldr=m(e),v(this.cldr)),this.cldr},_._alwaysArray=o,_._createError=r,_._formatMessage=n,_._formatMessageToParts=function(e,n){var t=0,r=[];return e.replace(/{[0-9a-zA-Z-_. ]+}/g,(function(i,u){var o=i.slice(1,-1);a(r,"literal",e.slice(t,u)),a(r,"variable",n[o]),r[r.length-1].name=o,t+=u+i.length})),r.filter((function(e){return""!==e.value}))},_._isPlainObject=d,_._objectExtend=t,_._partsJoin=function(e){return e.map((function(e){return e.value})).join("")},_._partsPush=a,_._regexpEscape=function(e){return e.replace(/([.*+?^=!:${}()|\[\]\/\\])/g,"\\$1")},_._runtimeBind=function(e,n,t,r){var a=i(e),u=function(e){if(void 0!==e.name)return e.name;var n=/^function\s+([\w\$]+)\s*\(/.exec(e.toString());return n&&n.length>0?n[1]:void 0}(t),o=n.locale;return u?(t.runtimeKey=function(e,n,t,r){var a,u;return r=r||i(t),u=e+n+r,(a=[].reduce.call(u,(function(e,n){return 0|(e=(e<<5)-e+n.charCodeAt(0))}),0))>0?"a"+a:"b"+Math.abs(a)}(u,o,null,a),t.generatorString=function(){return'Globalize("'+o+'").'+u+"("+a.slice(1,-1)+")"},t.runtimeArgs=r,t):t},_._stringPad=function(e,n,t){var r;for("string"!=typeof e&&(e=String(e)),r=e.length;r=t&&e<=r,{maximum:r,minimum:t,name:n,value:e})},_._validateParameterTypePlainObject=function(e,n){f(e,n,void 0===e||d(e),"Plain Object")},_._validateParameterType=f,_}));
diff --git a/src/js/jquery.js b/src/js/jquery.js
deleted file mode 100644
index b86de89..0000000
--- a/src/js/jquery.js
+++ /dev/null
@@ -1,10993 +0,0 @@
-/*!
- * jQuery JavaScript Library v3.6.3
- * https://jquery.com/
- *
- * Includes Sizzle.js
- * https://sizzlejs.com/
- *
- * Copyright OpenJS Foundation and other contributors
- * Released under the MIT license
- * https://jquery.org/license
- *
- * Date: 2022-12-20T21:28Z
- */
-( function( global, factory ) {
-
-	"use strict";
-
-	if ( typeof module === "object" && typeof module.exports === "object" ) {
-
-		// For CommonJS and CommonJS-like environments where a proper `window`
-		// is present, execute the factory and get jQuery.
-		// For environments that do not have a `window` with a `document`
-		// (such as Node.js), expose a factory as module.exports.
-		// This accentuates the need for the creation of a real `window`.
-		// e.g. var jQuery = require("jquery")(window);
-		// See ticket trac-14549 for more info.
-		module.exports = global.document ?
-			factory( global, true ) :
-			function( w ) {
-				if ( !w.document ) {
-					throw new Error( "jQuery requires a window with a document" );
-				}
-				return factory( w );
-			};
-	} else {
-		factory( global );
-	}
-
-// Pass this if window is not defined yet
-} )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
-
-// Edge <= 12 - 13+, Firefox <=18 - 45+, IE 10 - 11, Safari 5.1 - 9+, iOS 6 - 9.1
-// throw exceptions when non-strict code (e.g., ASP.NET 4.5) accesses strict mode
-// arguments.callee.caller (trac-13335). But as of jQuery 3.0 (2016), strict mode should be common
-// enough that all such attempts are guarded in a try block.
-"use strict";
-
-var arr = [];
-
-var getProto = Object.getPrototypeOf;
-
-var slice = arr.slice;
-
-var flat = arr.flat ? function( array ) {
-	return arr.flat.call( array );
-} : function( array ) {
-	return arr.concat.apply( [], array );
-};
-
-
-var push = arr.push;
-
-var indexOf = arr.indexOf;
-
-var class2type = {};
-
-var toString = class2type.toString;
-
-var hasOwn = class2type.hasOwnProperty;
-
-var fnToString = hasOwn.toString;
-
-var ObjectFunctionString = fnToString.call( Object );
-
-var support = {};
-
-var isFunction = function isFunction( obj ) {
-
-		// Support: Chrome <=57, Firefox <=52
-		// In some browsers, typeof returns "function" for HTML