Refactoring & summary

main
Ogoun 4 months ago
parent 7a4a7b55ca
commit 0a05a3ed0c

@ -5,18 +5,7 @@
<title>Планирование маршрутов</title>
<link href="styles/styles.css" rel="stylesheet" />
<link href="styles/dx.material.blue.dark.compact.css" rel="stylesheet" />
<script src="js/hamiltonPath.js"></script>
<script src="js/jquery.min.js"></script>
<script src="js/knockout-latest.js"></script>
<script src="js/cldr.min.js"></script>
<script src="js/cldr/event.min.js"></script>
<script src="js/cldr/supplemental.min.js"></script>
<script src="js/globalize.min.js"></script>
<script src="js/globalize/message.min.js"></script>
<script src="js/globalize/number.min.js"></script>
<script src="js/globalize/currency.min.js"></script>
<script src="js/globalize/date.min.js"></script>
<script src="js/dx.all.js"></script>
</head>

@ -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;n<r;n++)if(t(e[n],n,e))return!0;return!1},o=function(e,t,n,o){var a,i=n[0],l=n[1],u=e.localeSep,c=n[2],f=n.slice(3,4);return o=o||{},"und"!==i&&"Zzzz"!==l&&"ZZ"!==c?[i,l,c].concat(f):void 0!==t.get("supplemental/likelySubtags")?r([[i,l,c],[i,c],[i,l],[i],["und",l]],(function(e){return a=!/\b(Zzzz|ZZ)\b/.test(e.join(u))&&t.get(["supplemental/likelySubtags",e.join(u)])}))?(a=a.split(u),["und"!==i?i:a[0],"Zzzz"!==l?l:a[1],"ZZ"!==c?c:a[2]].concat(f)):o.force?t.get("supplemental/likelySubtags/und").split(u):void 0:void 0},a=function(e,t,n){var a,i=n[0],l=n[1],u=n[2],c=n[3];return r([[[i,"Zzzz","ZZ"],[i]],[[i,"Zzzz",u],[i,u]],[[i,l,"ZZ"],[i,l]]],(function(r){var i=o(e,t,r[0]);return a=r[1],i&&i[0]===n[0]&&i[1]===n[1]&&i[2]===n[2]}))?(c&&a.push(c),a):n},i=function(e){var t,n=[];return(t=(e=e.replace(/_/,"-")).split("-u-"))[1]&&(t[1]=t[1].split("-t-"),e=t[0]+(t[1][1]?"-t-"+t[1][1]:""),n[4]=t[1][0]),null===(t=e.split("-t-")[0].match(/^(([a-z]{2,3})(-([A-Z][a-z]{3}))?(-([A-Z]{2}|[0-9]{3}))?)((-([a-zA-Z0-9]{5,8}|[0-9][a-zA-Z0-9]{3}))*)$|^(root)$/))?["und","Zzzz","ZZ"]:(n[0]=t[10]||t[2]||"und",n[1]=t[4]||"Zzzz",n[2]=t[6]||"ZZ",t[7]&&t[7].length&&(n[3]=t[7].slice(1)),n)},l=function(e,t){var n,r;if(e.forEach)return e.forEach(t);for(n=0,r=e.length;n<r;n++)t(e[n],n,e)},u=function(e,t,n){var r=e._availableBundleMap,u=e._availableBundleMapQueue;return u.length&&(l(u,(function(n,l){var c,f,p,s;if(s=i(n),void 0===(f=o(e,t,s)))throw u.splice(l,1),new Error("Could not find likelySubtags for "+n);p=(p=a(e,t,f)).join(e.localeSep),(c=r[p])&&c.length<n.length||(r[p]=n)})),e._availableBundleMapQueue=[]),r[n]||null},c=function(e,t){var n,r;return r=e+(t&&JSON?": "+JSON.stringify(t):""),(n=new Error(r)).code=e,l(function(e){var t,n=[];if(Object.keys)return Object.keys(e);for(t in e)n.push(t);return n}(t),(function(e){n[e]=t[e]})),n},f=function(e,t,n){if(!t)throw c(e,n)},p=function(e,t){f("E_MISSING_PARAMETER",void 0!==e,{name:t})},s=function(e,t,n,r){f("E_INVALID_PAR_TYPE",n,{expected:r,name:t,value:e})},d=function(e,n){s(e,n,"string"==typeof e||t(e),"String or Array")},v=function(e,t){var n;s(e,t,void 0===e||null!==(n=e)&&""+n=="[object Object]","Plain Object")},h=function(e,t){var n,r=e,o=t.length;for(n=0;n<o-1;n++)if(!(r=r[t[n]]))return;return r[t[n]]},g=function(e,t){var n,r=e._availableBundleMapQueue,o=h(t,["main"]);if(o)for(n in o)o.hasOwnProperty(n)&&"root"!==n&&-1===r.indexOf(n)&&r.push(n)},z=function(e){return t(e)?e:[e]},b=e=function(){var n={},r=[].slice.call(arguments,0);return l(r,(function(r){var o;for(o in r)o in n&&"object"==typeof n[o]&&!t(n[o])?n[o]=e(n[o],r[o]):n[o]=r[o]})),n},y=function(e,t,n){var r,o,a;for(p(n[0],"json"),r=0;r<n.length;r++)for(a=z(n[r]),o=0;o<a.length;o++)v(a[o],"json"),t=b(t,a[o]),g(e,a[o]);return t},_=function(e,t,r){var o=n(t,r);return h(e._resolved,o)},Z=function(e){this.init(e)};return Z._alwaysArray=z,Z._coreLoad=y,Z._createError=c,Z._itemGetResolved=_,Z._jsonMerge=b,Z._pathNormalize=n,Z._resourceGet=h,Z._validatePresence=p,Z._validateType=s,Z._validateTypePath=d,Z._validateTypePlainObject=v,Z._availableBundleMap={},Z._availableBundleMapQueue=[],Z._resolved={},Z.localeSep="-",Z.load=function(){Z._resolved=y(Z,Z._resolved,arguments)},Z.prototype.init=function(e){var t,n,r,l,c,f,d,v,h,g,z=Z.localeSep,b="";p(e,"locale"),s(g=e,"locale","string"==typeof g,"a string"),5===(f=i(e)).length&&(b=z+"u"+z+(v=f.pop()),f[3]||f.pop()),h=f[3],n=(r=o(Z,this,f,{force:!0})||f)[0],c=r[1],d=r[2],l=a(Z,this,r).join(z),this.attributes=t={bundle:u(Z,this,l),minLanguageId:l+b,maxLanguageId:r.join(z)+b,language:n,script:c,territory:d,region:d,variant:h},v&&("-"+v).replace(/-[a-z]{3,8}|(-[a-z]{2})-([a-z]{3,8})/g,(function(e,n,r){n?t["u"+n]=r:t["u"+e]=!0})),this.locale=e},Z.prototype.get=function(e){return p(e,"path"),d(e,"path"),_(Z,e,this.attributes)},Z.prototype.main=function(e){return p(e,"path"),d(e,"path"),f("E_MISSING_BUNDLE",null!==this.attributes.bundle,{locale:this.locale}),e=z(e),this.get(["main/{bundle}"].concat(e))},Z}));

@ -1,353 +0,0 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Geodesy representation conversion functions (c) Chris Veness 2002-2020 */
/* MIT Licence */
/* www.movable-type.co.uk/scripts/latlong.html */
/* www.movable-type.co.uk/scripts/js/geodesy/geodesy-library.html#dms */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* eslint no-irregular-whitespace: [2, { skipComments: true }] */
/**
* Latitude/longitude points may be represented as decimal degrees, or subdivided into sexagesimal
* minutes and seconds. This module provides methods for parsing and representing degrees / minutes
* / seconds.
*
* @module dms
*/
/* Degree-minutes-seconds (& cardinal directions) separator character */
let dmsSeparator = '\u202f'; // U+202F = 'narrow no-break space'
/**
* Functions for parsing and representing degrees / minutes / seconds.
*/
class Dms {
// note Unicode Degree = U+00B0. Prime = U+2032, Double prime = U+2033
/**
* Separator character to be used to separate degrees, minutes, seconds, and cardinal directions.
*
* Default separator is U+202F narrow no-break space.
*
* To change this (e.g. to empty string or full space), set Dms.separator prior to invoking
* formatting.
*
* @example
* import LatLon, { Dms } from '/js/geodesy/latlon-spherical.js';
* const p = new LatLon(51.2, 0.33).toString('dms'); // 51°1200″N, 000°1948″E
* Dms.separator = ''; // no separator
* const pʹ = new LatLon(51.2, 0.33).toString('dms'); // 51°1200″N, 000°1948″E
*/
static get separator() { return dmsSeparator; }
static set separator(char) { dmsSeparator = char; }
/**
* Parses string representing degrees/minutes/seconds into numeric degrees.
*
* This is very flexible on formats, allowing signed decimal degrees, or deg-min-sec optionally
* suffixed by compass direction (NSEW); a variety of separators are accepted. Examples -3.62,
* '3 37 12W', '3°3712″W'.
*
* Thousands/decimal separators must be comma/dot; use Dms.fromLocale to convert locale-specific
* thousands/decimal separators.
*
* @param {string|number} dms - Degrees or deg/min/sec in variety of formats.
* @returns {number} Degrees as decimal number.
*
* @example
* const lat = Dms.parse('51° 28 40.37″ N');
* const lon = Dms.parse('000° 00 05.29″ W');
* const p1 = new LatLon(lat, lon); // 51.4779°N, 000.0015°W
*/
static parse(dms) {
// check for signed decimal degrees without NSEW, if so return it directly
if (!isNaN(parseFloat(dms)) && isFinite(dms)) return Number(dms);
// strip off any sign or compass dir'n & split out separate d/m/s
const dmsParts = String(dms).trim().replace(/^-/, '').replace(/[NSEW]$/i, '').split(/[^0-9.,]+/);
if (dmsParts[dmsParts.length-1]=='') dmsParts.splice(dmsParts.length-1); // from trailing symbol
if (dmsParts == '') return NaN;
// and convert to decimal degrees...
let deg = null;
switch (dmsParts.length) {
case 3: // interpret 3-part result as d/m/s
deg = dmsParts[0]/1 + dmsParts[1]/60 + dmsParts[2]/3600;
break;
case 2: // interpret 2-part result as d/m
deg = dmsParts[0]/1 + dmsParts[1]/60;
break;
case 1: // just d (possibly decimal) or non-separated dddmmss
deg = dmsParts[0];
// check for fixed-width unseparated format eg 0033709W
//if (/[NS]/i.test(dmsParts)) deg = '0' + deg; // - normalise N/S to 3-digit degrees
//if (/[0-9]{7}/.test(deg)) deg = deg.slice(0,3)/1 + deg.slice(3,5)/60 + deg.slice(5)/3600;
break;
default:
return NaN;
}
if (/^-|[WS]$/i.test(dms.trim())) deg = -deg; // take '-', west and south as -ve
return Number(deg);
}
/**
* Converts decimal degrees to deg/min/sec format
* - degree, prime, double-prime symbols are added, but sign is discarded, though no compass
* direction is added.
* - degrees are zero-padded to 3 digits; for degrees latitude, use .slice(1) to remove leading
* zero.
*
* @private
* @param {number} deg - Degrees to be formatted as specified.
* @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
* @param {number} [dp=4|2|0] - Number of decimal places to use default 4 for d, 2 for dm, 0 for dms.
* @returns {string} Degrees formatted as deg/min/secs according to specified format.
*/
static toDms(deg, format='d', dp=undefined) {
if (isNaN(deg)) return null; // give up here if we can't make a number from deg
if (typeof deg == 'string' && deg.trim() == '') return null;
if (typeof deg == 'boolean') return null;
if (deg == Infinity) return null;
if (deg == null) return null;
// default values
if (dp === undefined) {
switch (format) {
case 'd': case 'deg': dp = 4; break;
case 'dm': case 'deg+min': dp = 2; break;
case 'dms': case 'deg+min+sec': dp = 0; break;
default: format = 'd'; dp = 4; break; // be forgiving on invalid format
}
}
deg = Math.abs(deg); // (unsigned result ready for appending compass dir'n)
let dms = null, d = null, m = null, s = null;
switch (format) {
default: // invalid format spec!
case 'd': case 'deg':
d = deg.toFixed(dp); // round/right-pad degrees
if (d<100) d = '0' + d; // left-pad with leading zeros (note may include decimals)
if (d<10) d = '0' + d;
dms = d + '°';
break;
case 'dm': case 'deg+min':
d = Math.floor(deg); // get component deg
m = ((deg*60) % 60).toFixed(dp); // get component min & round/right-pad
if (m == 60) { m = (0).toFixed(dp); d++; } // check for rounding up
d = ('000'+d).slice(-3); // left-pad with leading zeros
if (m<10) m = '0' + m; // left-pad with leading zeros (note may include decimals)
dms = d + '°'+Dms.separator + m + '';
break;
case 'dms': case 'deg+min+sec':
d = Math.floor(deg); // get component deg
m = Math.floor((deg*3600)/60) % 60; // get component min
s = (deg*3600 % 60).toFixed(dp); // get component sec & round/right-pad
if (s == 60) { s = (0).toFixed(dp); m++; } // check for rounding up
if (m == 60) { m = 0; d++; } // check for rounding up
d = ('000'+d).slice(-3); // left-pad with leading zeros
m = ('00'+m).slice(-2); // left-pad with leading zeros
if (s<10) s = '0' + s; // left-pad with leading zeros (note may include decimals)
dms = d + '°'+Dms.separator + m + ''+Dms.separator + s + '″';
break;
}
return dms;
}
/**
* Converts numeric degrees to deg/min/sec latitude (2-digit degrees, suffixed with N/S).
*
* @param {number} deg - Degrees to be formatted as specified.
* @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
* @param {number} [dp=4|2|0] - Number of decimal places to use default 4 for d, 2 for dm, 0 for dms.
* @returns {string} Degrees formatted as deg/min/secs according to specified format.
*
* @example
* const lat = Dms.toLat(-3.62, 'dms'); // 3°3712″S
*/
static toLat(deg, format, dp) {
const lat = Dms.toDms(Dms.wrap90(deg), format, dp);
return lat===null ? '' : lat.slice(1) + Dms.separator + (deg<0 ? 'S' : 'N'); // knock off initial '0' for lat!
}
/**
* Convert numeric degrees to deg/min/sec longitude (3-digit degrees, suffixed with E/W).
*
* @param {number} deg - Degrees to be formatted as specified.
* @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
* @param {number} [dp=4|2|0] - Number of decimal places to use default 4 for d, 2 for dm, 0 for dms.
* @returns {string} Degrees formatted as deg/min/secs according to specified format.
*
* @example
* const lon = Dms.toLon(-3.62, 'dms'); // 3°3712″W
*/
static toLon(deg, format, dp) {
const lon = Dms.toDms(Dms.wrap180(deg), format, dp);
return lon===null ? '' : lon + Dms.separator + (deg<0 ? 'W' : 'E');
}
/**
* Converts numeric degrees to deg/min/sec as a bearing (0°..360°).
*
* @param {number} deg - Degrees to be formatted as specified.
* @param {string} [format=d] - Return value as 'd', 'dm', 'dms' for deg, deg+min, deg+min+sec.
* @param {number} [dp=4|2|0] - Number of decimal places to use default 4 for d, 2 for dm, 0 for dms.
* @returns {string} Degrees formatted as deg/min/secs according to specified format.
*
* @example
* const lon = Dms.toBrng(-3.62, 'dms'); // 356°2248″
*/
static toBrng(deg, format, dp) {
const brng = Dms.toDms(Dms.wrap360(deg), format, dp);
return brng===null ? '' : brng.replace('360', '0'); // just in case rounding took us up to 360°!
}
/**
* Converts DMS string from locale thousands/decimal separators to JavaScript comma/dot separators
* for subsequent parsing.
*
* Both thousands and decimal separators must be followed by a numeric character, to facilitate
* parsing of single lat/long string (in which whitespace must be left after the comma separator).
*
* @param {string} str - Degrees/minutes/seconds formatted with locale separators.
* @returns {string} Degrees/minutes/seconds formatted with standard Javascript separators.
*
* @example
* const lat = Dms.fromLocale('51°2840,12″N'); // '51°2840.12″N' in France
* const p = new LatLon(Dms.fromLocale('51°2840,37″N, 000°0005,29″W'); // '51.4779°N, 000.0015°W' in France
*/
static fromLocale(str) {
const locale = (123456.789).toLocaleString();
const separator = { thousands: locale.slice(3, 4), decimal: locale.slice(7, 8) };
return str.replace(separator.thousands, '⁜').replace(separator.decimal, '.').replace('⁜', ',');
}
/**
* Converts DMS string from JavaScript comma/dot thousands/decimal separators to locale separators.
*
* Can also be used to format standard numbers such as distances.
*
* @param {string} str - Degrees/minutes/seconds formatted with standard Javascript separators.
* @returns {string} Degrees/minutes/seconds formatted with locale separators.
*
* @example
* const Dms.toLocale('123,456.789'); // '123.456,789' in France
* const Dms.toLocale('51°2840.12″N, 000°0005.31″W'); // '51°2840,12″N, 000°0005,31″W' in France
*/
static toLocale(str) {
const locale = (123456.789).toLocaleString();
const separator = { thousands: locale.slice(3, 4), decimal: locale.slice(7, 8) };
return str.replace(/,([0-9])/, '⁜$1').replace('.', separator.decimal).replace('⁜', separator.thousands);
}
/**
* Returns compass point (to given precision) for supplied bearing.
*
* @param {number} bearing - Bearing in degrees from north.
* @param {number} [precision=3] - Precision (1:cardinal / 2:intercardinal / 3:secondary-intercardinal).
* @returns {string} Compass point for supplied bearing.
*
* @example
* const point = Dms.compassPoint(24); // point = 'NNE'
* const point = Dms.compassPoint(24, 1); // point = 'N'
*/
static compassPoint(bearing, precision=3) {
if (![ 1, 2, 3 ].includes(Number(precision))) throw new RangeError(`invalid precision ${precision}`);
// note precision could be extended to 4 for quarter-winds (eg NbNW), but I think they are little used
bearing = Dms.wrap360(bearing); // normalise to range 0..360°
const cardinals = [
'N', 'NNE', 'NE', 'ENE',
'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW',
'W', 'WNW', 'NW', 'NNW' ];
const n = 4 * 2**(precision-1); // no of compass points at reqd precision (1=>4, 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;

@ -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 earths 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 = (ab)/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°3712″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 };

@ -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
*/

@ -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 earths 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;
}
/**
* Points 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°3712″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°2840″N, 000°0005″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°2840″N, 000°0005″W
* new LatLon(51.47788, -0.00147, 42, LatLon.referenceFrames.ITRF2014).toString('dms', 0, 0); // 51°2840″N, 000°0005″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;
}
/**
* Points 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 };

@ -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 };

@ -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 earths 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°3712″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°2840″N, 000°0005″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/√(1e²⋅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°2840″N, 000°0005″W
* const [lat, lon] = greenwich.toString('n').split(','); // 51.4779, -0.0015
* const dmsh = greenwich.toString('dms', 0, 0); // 51°2840″N, 000°0006″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 Bowrings (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³β / pe²⋅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 };

@ -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 earths surface using a vector-based
* approach using n-vectors (rather than the more common spherical trigonometry).
*
* Based on Kenneth Gades 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); // (1f)² = 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 };

File diff suppressed because it is too large Load Diff

@ -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 earths 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°3712″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°1218.0″N', '000°0708.4″E'); // DMS pair
* const p5 = LatLon.parse('52°1218.0″N, 000°0708.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°1218.0″N', lng: '000°0708.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), √(1a))
// 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 (Clairauts 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 LHuiliers 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<nVertices; v++) {
const φ1 = polygon[v].lat.toRadians();
const φ2 = polygon[v+1].lat.toRadians();
const Δλ = (polygon[v+1].lon - polygon[v].lon).toRadians();
const E = 2 * Math.atan2(Math.tan(Δλ/2) * (Math.tan(φ1/2)+Math.tan(φ2/2)), 1 + Math.tan(φ1/2)*Math.tan(φ2/2));
S += E;
}
if (isPoleEnclosedBy(polygon)) S = Math.abs(S) - 2*π;
const A = Math.abs(S * R*R); // area in units of R
if (!closed) polygon.pop(); // restore polygon to pristine condition
return A;
// returns whether polygon encloses pole: sum of course deltas around pole is 0° rather than
// normal ±360°: blog.element84.com/determining-if-a-spherical-polygon-contains-a-pole.html
function isPoleEnclosedBy(p) {
// TODO: any better test than this?
let ΣΔ = 0;
let prevBrng = p[0].initialBearingTo(p[1]);
for (let v=0; v<p.length-1; v++) {
const initBrng = p[v].initialBearingTo(p[v+1]);
const finalBrng = p[v].finalBearingTo(p[v+1]);
ΣΔ += (initBrng - prevBrng + 540) % 360 - 180;
ΣΔ += (finalBrng - initBrng + 540) % 360 - 180;
prevBrng = finalBrng;
}
const initBrng = p[0].initialBearingTo(p[1]);
ΣΔ += (initBrng - prevBrng + 540) % 360 - 180;
// TODO: fix (intermittant) edge crossing pole - eg (85,90), (85,0), (85,-90)
const enclosed = Math.abs(ΣΔ) < 90; // 0°-ish
return enclosed;
}
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/**
* 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.
*
* @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 LatLonSpherical)) point = LatLonSpherical.parse(point); // allow literal forms
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.
* @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°2840.37″N, 000°0005.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 };

@ -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 };

@ -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 45 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°30N, 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°3928.723″N, 001°4257.787″E
* // to obtain (historical) OSGB36 lat/lon point:
* const pOsgb = gridref.toLatLon(LatLon.datums.OSGB36); // 52°3927.253″N, 001°4304.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 };

@ -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<northing && northing<=10000e3)) throw new RangeError(`invalid UTM northing ${northing}`);
}
if (!datum || datum.ellipsoid==undefined) throw new TypeError(`unrecognised datum ${datum}`);
this.zone = Number(zone);
this.hemisphere = hemisphere.toUpperCase();
this.easting = Number(easting);
this.northing = Number(northing);
this.datum = datum;
this.convergence = convergence===null ? null : Number(convergence);
this.scale = scale===null ? null : Number(scale);
}
/**
* Converts UTM zone/easting/northing coordinate to latitude/longitude.
*
* Implements Karneys method, using Krüger series to order n⁶, giving results accurate to 5nm
* for distances up to 3900km from the central meridian.
*
* @param {Utm} utmCoord - UTM coordinate to be converted to latitude/longitude.
* @returns {LatLon} Latitude/longitude of supplied grid reference.
*
* @example
* const grid = new Utm(31, 'N', 448251.795, 5411932.678);
* const latlong = grid.toLatLon(); // 48°5129.52″N, 002°1740.20″E
*/
toLatLon() {
const { zone: z, hemisphere: h } = this;
const falseEasting = 500e3, falseNorthing = 10000e3;
const { a, f } = this.datum.ellipsoid; // WGS-84: a = 6378137, f = 1/298.257223563;
const k0 = 0.9996; // UTM scale on the central meridian
const x = this.easting - falseEasting; // make x ± relative to central meridian
const y = h=='S' ? this.northing - falseNorthing : this.northing; // make y ± relative to equator
// ---- from Karney 2011 Eq 15-22, 36:
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 A = a/(1+n) * (1 + 1/4*n2 + 1/64*n4 + 1/256*n6); // 2πA is the circumference of a meridian
const η = x / (k0*A);
const ξ = y / (k0*A);
const β = [ null, // note β is one-based array (6th order Krüger expressions)
1/2*n - 2/3*n2 + 37/96*n3 - 1/360*n4 - 81/512*n5 + 96199/604800*n6,
1/48*n2 + 1/15*n3 - 437/1440*n4 + 46/105*n5 - 1118711/3870720*n6,
17/480*n3 - 37/840*n4 - 209/4480*n5 + 5569/90720*n6,
4397/161280*n4 - 11/504*n5 - 830251/7257600*n6,
4583/161280*n5 - 108847/3991680*n6,
20648693/638668800*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*η);
const sinhηʹ = Math.sinh(ηʹ);
const sinξʹ = Math.sin(ξʹ), cosξʹ = Math.cos(ξʹ);
const τʹ = sinξʹ / Math.sqrt(sinhηʹ*sinhηʹ + cosξʹ*cosξʹ);
let δτi = null;
let τi = τʹ;
do {
const σi = Math.sinh(e*Math.atanh(e*τi/Math.sqrt(1+τi*τi)));
const τiʹ = τi * Math.sqrt(1+σi*σi) - σi * Math.sqrt(1+τi*τi);
δτi = (τʹ - τiʹ)/Math.sqrt(1+τiʹ*τiʹ)
* (1 + (1-e*e)*τi*τi) / ((1-e*e)*Math.sqrt(1+τi*τi));
τi += δτi;
} while (Math.abs(δτi) > 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 = A / a / Math.sqrt(p*p + q*q);
const k = k0 * 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 Karneys 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 = A / a * Math.sqrt(pʹ*pʹ + qʹ*qʹ);
const k = k0 * 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 };

@ -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;

@ -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<n;r+=1)e=t?e+"0":"0"+e;return e},_._validate=u,_._validateCldr=c,_._validateDefaultLocale=function(e){u("E_DEFAULT_LOCALE_NOT_DEFINED","Default locale has not been defined.",void 0!==e,{})},_._validateParameterPresence=l,_._validateParameterRange=function(e,n,t,r){u("E_PAR_OUT_OF_RANGE","Parameter `{name}` has value `{value}` out of range [{minimum}, {maximum}].",void 0===e||e>=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,_}));

10993
src/js/jquery.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,139 +0,0 @@
/*!
* Knockout JavaScript library v3.5.1
* (c) The Knockout.js team - http://knockoutjs.com/
* License: MIT (http://www.opensource.org/licenses/mit-license.php)
*/
(function() {(function(n){var A=this||(0,eval)("this"),w=A.document,R=A.navigator,v=A.jQuery,H=A.JSON;v||"undefined"===typeof jQuery||(v=jQuery);(function(n){"function"===typeof define&&define.amd?define(["exports","require"],n):"object"===typeof exports&&"object"===typeof module?n(module.exports||exports):n(A.ko={})})(function(S,T){function K(a,c){return null===a||typeof a in W?a===c:!1}function X(b,c){var d;return function(){d||(d=a.a.setTimeout(function(){d=n;b()},c))}}function Y(b,c){var d;return function(){clearTimeout(d);
d=a.a.setTimeout(b,c)}}function Z(a,c){c&&"change"!==c?"beforeChange"===c?this.pc(a):this.gb(a,c):this.qc(a)}function aa(a,c){null!==c&&c.s&&c.s()}function ba(a,c){var d=this.qd,e=d[r];e.ra||(this.Qb&&this.mb[c]?(d.uc(c,a,this.mb[c]),this.mb[c]=null,--this.Qb):e.I[c]||d.uc(c,a,e.J?{da:a}:d.$c(a)),a.Ja&&a.gd())}var a="undefined"!==typeof S?S:{};a.b=function(b,c){for(var d=b.split("."),e=a,f=0;f<d.length-1;f++)e=e[d[f]];e[d[d.length-1]]=c};a.L=function(a,c,d){a[c]=d};a.version="3.5.1";a.b("version",
a.version);a.options={deferUpdates:!1,useOnlyNativeEvents:!1,foreachHidesDestroyed:!1};a.a=function(){function b(a,b){for(var c in a)f.call(a,c)&&b(c,a[c])}function c(a,b){if(b)for(var c in b)f.call(b,c)&&(a[c]=b[c]);return a}function d(a,b){a.__proto__=b;return a}function e(b,c,d,e){var l=b[c].match(q)||[];a.a.D(d.match(q),function(b){a.a.Na(l,b,e)});b[c]=l.join(" ")}var f=Object.prototype.hasOwnProperty,g={__proto__:[]}instanceof Array,h="function"===typeof Symbol,m={},k={};m[R&&/Firefox\/2/i.test(R.userAgent)?
"KeyboardEvent":"UIEvents"]=["keyup","keydown","keypress"];m.MouseEvents="click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave".split(" ");b(m,function(a,b){if(b.length)for(var c=0,d=b.length;c<d;c++)k[b[c]]=a});var l={propertychange:!0},p=w&&function(){for(var a=3,b=w.createElement("div"),c=b.getElementsByTagName("i");b.innerHTML="\x3c!--[if gt IE "+ ++a+"]><i></i><![endif]--\x3e",c[0];);return 4<a?a:n}(),q=/\S+/g,t;return{Jc:["authenticity_token",/^__RequestVerificationToken(_.*)?$/],
D:function(a,b,c){for(var d=0,e=a.length;d<e;d++)b.call(c,a[d],d,a)},A:"function"==typeof Array.prototype.indexOf?function(a,b){return Array.prototype.indexOf.call(a,b)}:function(a,b){for(var c=0,d=a.length;c<d;c++)if(a[c]===b)return c;return-1},Lb:function(a,b,c){for(var d=0,e=a.length;d<e;d++)if(b.call(c,a[d],d,a))return a[d];return n},Pa:function(b,c){var d=a.a.A(b,c);0<d?b.splice(d,1):0===d&&b.shift()},wc:function(b){var c=[];b&&a.a.D(b,function(b){0>a.a.A(c,b)&&c.push(b)});return c},Mb:function(a,
b,c){var d=[];if(a)for(var e=0,l=a.length;e<l;e++)d.push(b.call(c,a[e],e));return d},jb:function(a,b,c){var d=[];if(a)for(var e=0,l=a.length;e<l;e++)b.call(c,a[e],e)&&d.push(a[e]);return d},Nb:function(a,b){if(b instanceof Array)a.push.apply(a,b);else for(var c=0,d=b.length;c<d;c++)a.push(b[c]);return a},Na:function(b,c,d){var e=a.a.A(a.a.bc(b),c);0>e?d&&b.push(c):d||b.splice(e,1)},Ba:g,extend:c,setPrototypeOf:d,Ab:g?d:c,P:b,Ga:function(a,b,c){if(!a)return a;var d={},e;for(e in a)f.call(a,e)&&(d[e]=
b.call(c,a[e],e,a));return d},Tb:function(b){for(;b.firstChild;)a.removeNode(b.firstChild)},Yb:function(b){b=a.a.la(b);for(var c=(b[0]&&b[0].ownerDocument||w).createElement("div"),d=0,e=b.length;d<e;d++)c.appendChild(a.oa(b[d]));return c},Ca:function(b,c){for(var d=0,e=b.length,l=[];d<e;d++){var k=b[d].cloneNode(!0);l.push(c?a.oa(k):k)}return l},va:function(b,c){a.a.Tb(b);if(c)for(var d=0,e=c.length;d<e;d++)b.appendChild(c[d])},Xc:function(b,c){var d=b.nodeType?[b]:b;if(0<d.length){for(var e=d[0],
l=e.parentNode,k=0,f=c.length;k<f;k++)l.insertBefore(c[k],e);k=0;for(f=d.length;k<f;k++)a.removeNode(d[k])}},Ua:function(a,b){if(a.length){for(b=8===b.nodeType&&b.parentNode||b;a.length&&a[0].parentNode!==b;)a.splice(0,1);for(;1<a.length&&a[a.length-1].parentNode!==b;)a.length--;if(1<a.length){var c=a[0],d=a[a.length-1];for(a.length=0;c!==d;)a.push(c),c=c.nextSibling;a.push(d)}}return a},Zc:function(a,b){7>p?a.setAttribute("selected",b):a.selected=b},Db:function(a){return null===a||a===n?"":a.trim?
a.trim():a.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},Ud:function(a,b){a=a||"";return b.length>a.length?!1:a.substring(0,b.length)===b},vd:function(a,b){if(a===b)return!0;if(11===a.nodeType)return!1;if(b.contains)return b.contains(1!==a.nodeType?a.parentNode:a);if(b.compareDocumentPosition)return 16==(b.compareDocumentPosition(a)&16);for(;a&&a!=b;)a=a.parentNode;return!!a},Sb:function(b){return a.a.vd(b,b.ownerDocument.documentElement)},kd:function(b){return!!a.a.Lb(b,a.a.Sb)},R:function(a){return a&&
a.tagName&&a.tagName.toLowerCase()},Ac:function(b){return a.onError?function(){try{return b.apply(this,arguments)}catch(c){throw a.onError&&a.onError(c),c;}}:b},setTimeout:function(b,c){return setTimeout(a.a.Ac(b),c)},Gc:function(b){setTimeout(function(){a.onError&&a.onError(b);throw b;},0)},B:function(b,c,d){var e=a.a.Ac(d);d=l[c];if(a.options.useOnlyNativeEvents||d||!v)if(d||"function"!=typeof b.addEventListener)if("undefined"!=typeof b.attachEvent){var k=function(a){e.call(b,a)},f="on"+c;b.attachEvent(f,
k);a.a.K.za(b,function(){b.detachEvent(f,k)})}else throw Error("Browser doesn't support addEventListener or attachEvent");else b.addEventListener(c,e,!1);else t||(t="function"==typeof v(b).on?"on":"bind"),v(b)[t](c,e)},Fb:function(b,c){if(!b||!b.nodeType)throw Error("element must be a DOM node when calling triggerEvent");var d;"input"===a.a.R(b)&&b.type&&"click"==c.toLowerCase()?(d=b.type,d="checkbox"==d||"radio"==d):d=!1;if(a.options.useOnlyNativeEvents||!v||d)if("function"==typeof w.createEvent)if("function"==
typeof b.dispatchEvent)d=w.createEvent(k[c]||"HTMLEvents"),d.initEvent(c,!0,!0,A,0,0,0,0,0,!1,!1,!1,!1,0,b),b.dispatchEvent(d);else throw Error("The supplied element doesn't support dispatchEvent");else if(d&&b.click)b.click();else if("undefined"!=typeof b.fireEvent)b.fireEvent("on"+c);else throw Error("Browser doesn't support triggering events");else v(b).trigger(c)},f:function(b){return a.O(b)?b():b},bc:function(b){return a.O(b)?b.v():b},Eb:function(b,c,d){var l;c&&("object"===typeof b.classList?
(l=b.classList[d?"add":"remove"],a.a.D(c.match(q),function(a){l.call(b.classList,a)})):"string"===typeof b.className.baseVal?e(b.className,"baseVal",c,d):e(b,"className",c,d))},Bb:function(b,c){var d=a.a.f(c);if(null===d||d===n)d="";var e=a.h.firstChild(b);!e||3!=e.nodeType||a.h.nextSibling(e)?a.h.va(b,[b.ownerDocument.createTextNode(d)]):e.data=d;a.a.Ad(b)},Yc:function(a,b){a.name=b;if(7>=p)try{var c=a.name.replace(/[&<>'"]/g,function(a){return"&#"+a.charCodeAt(0)+";"});a.mergeAttributes(w.createElement("<input name='"+
c+"'/>"),!1)}catch(d){}},Ad:function(a){9<=p&&(a=1==a.nodeType?a:a.parentNode,a.style&&(a.style.zoom=a.style.zoom))},wd:function(a){if(p){var b=a.style.width;a.style.width=0;a.style.width=b}},Pd:function(b,c){b=a.a.f(b);c=a.a.f(c);for(var d=[],e=b;e<=c;e++)d.push(e);return d},la:function(a){for(var b=[],c=0,d=a.length;c<d;c++)b.push(a[c]);return b},Da:function(a){return h?Symbol(a):a},Zd:6===p,$d:7===p,W:p,Lc:function(b,c){for(var d=a.a.la(b.getElementsByTagName("input")).concat(a.a.la(b.getElementsByTagName("textarea"))),
e="string"==typeof c?function(a){return a.name===c}:function(a){return c.test(a.name)},l=[],k=d.length-1;0<=k;k--)e(d[k])&&l.push(d[k]);return l},Nd:function(b){return"string"==typeof b&&(b=a.a.Db(b))?H&&H.parse?H.parse(b):(new Function("return "+b))():null},hc:function(b,c,d){if(!H||!H.stringify)throw Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");
return H.stringify(a.a.f(b),c,d)},Od:function(c,d,e){e=e||{};var l=e.params||{},k=e.includeFields||this.Jc,f=c;if("object"==typeof c&&"form"===a.a.R(c))for(var f=c.action,h=k.length-1;0<=h;h--)for(var g=a.a.Lc(c,k[h]),m=g.length-1;0<=m;m--)l[g[m].name]=g[m].value;d=a.a.f(d);var p=w.createElement("form");p.style.display="none";p.action=f;p.method="post";for(var q in d)c=w.createElement("input"),c.type="hidden",c.name=q,c.value=a.a.hc(a.a.f(d[q])),p.appendChild(c);b(l,function(a,b){var c=w.createElement("input");
c.type="hidden";c.name=a;c.value=b;p.appendChild(c)});w.body.appendChild(p);e.submitter?e.submitter(p):p.submit();setTimeout(function(){p.parentNode.removeChild(p)},0)}}}();a.b("utils",a.a);a.b("utils.arrayForEach",a.a.D);a.b("utils.arrayFirst",a.a.Lb);a.b("utils.arrayFilter",a.a.jb);a.b("utils.arrayGetDistinctValues",a.a.wc);a.b("utils.arrayIndexOf",a.a.A);a.b("utils.arrayMap",a.a.Mb);a.b("utils.arrayPushAll",a.a.Nb);a.b("utils.arrayRemoveItem",a.a.Pa);a.b("utils.cloneNodes",a.a.Ca);a.b("utils.createSymbolOrString",
a.a.Da);a.b("utils.extend",a.a.extend);a.b("utils.fieldsIncludedWithJsonPost",a.a.Jc);a.b("utils.getFormFields",a.a.Lc);a.b("utils.objectMap",a.a.Ga);a.b("utils.peekObservable",a.a.bc);a.b("utils.postJson",a.a.Od);a.b("utils.parseJson",a.a.Nd);a.b("utils.registerEventHandler",a.a.B);a.b("utils.stringifyJson",a.a.hc);a.b("utils.range",a.a.Pd);a.b("utils.toggleDomNodeCssClass",a.a.Eb);a.b("utils.triggerEvent",a.a.Fb);a.b("utils.unwrapObservable",a.a.f);a.b("utils.objectForEach",a.a.P);a.b("utils.addOrRemoveItem",
a.a.Na);a.b("utils.setTextContent",a.a.Bb);a.b("unwrap",a.a.f);Function.prototype.bind||(Function.prototype.bind=function(a){var c=this;if(1===arguments.length)return function(){return c.apply(a,arguments)};var d=Array.prototype.slice.call(arguments,1);return function(){var e=d.slice(0);e.push.apply(e,arguments);return c.apply(a,e)}});a.a.g=new function(){var b=0,c="__ko__"+(new Date).getTime(),d={},e,f;a.a.W?(e=function(a,e){var f=a[c];if(!f||"null"===f||!d[f]){if(!e)return n;f=a[c]="ko"+b++;d[f]=
{}}return d[f]},f=function(a){var b=a[c];return b?(delete d[b],a[c]=null,!0):!1}):(e=function(a,b){var d=a[c];!d&&b&&(d=a[c]={});return d},f=function(a){return a[c]?(delete a[c],!0):!1});return{get:function(a,b){var c=e(a,!1);return c&&c[b]},set:function(a,b,c){(a=e(a,c!==n))&&(a[b]=c)},Ub:function(a,b,c){a=e(a,!0);return a[b]||(a[b]=c)},clear:f,Z:function(){return b++ +c}}};a.b("utils.domData",a.a.g);a.b("utils.domData.clear",a.a.g.clear);a.a.K=new function(){function b(b,c){var d=a.a.g.get(b,e);
d===n&&c&&(d=[],a.a.g.set(b,e,d));return d}function c(c){var e=b(c,!1);if(e)for(var e=e.slice(0),k=0;k<e.length;k++)e[k](c);a.a.g.clear(c);a.a.K.cleanExternalData(c);g[c.nodeType]&&d(c.childNodes,!0)}function d(b,d){for(var e=[],l,f=0;f<b.length;f++)if(!d||8===b[f].nodeType)if(c(e[e.length]=l=b[f]),b[f]!==l)for(;f--&&-1==a.a.A(e,b[f]););}var e=a.a.g.Z(),f={1:!0,8:!0,9:!0},g={1:!0,9:!0};return{za:function(a,c){if("function"!=typeof c)throw Error("Callback must be a function");b(a,!0).push(c)},yb:function(c,
d){var f=b(c,!1);f&&(a.a.Pa(f,d),0==f.length&&a.a.g.set(c,e,n))},oa:function(b){a.u.G(function(){f[b.nodeType]&&(c(b),g[b.nodeType]&&d(b.getElementsByTagName("*")))});return b},removeNode:function(b){a.oa(b);b.parentNode&&b.parentNode.removeChild(b)},cleanExternalData:function(a){v&&"function"==typeof v.cleanData&&v.cleanData([a])}}};a.oa=a.a.K.oa;a.removeNode=a.a.K.removeNode;a.b("cleanNode",a.oa);a.b("removeNode",a.removeNode);a.b("utils.domNodeDisposal",a.a.K);a.b("utils.domNodeDisposal.addDisposeCallback",
a.a.K.za);a.b("utils.domNodeDisposal.removeDisposeCallback",a.a.K.yb);(function(){var b=[0,"",""],c=[1,"<table>","</table>"],d=[3,"<table><tbody><tr>","</tr></tbody></table>"],e=[1,"<select multiple='multiple'>","</select>"],f={thead:c,tbody:c,tfoot:c,tr:[2,"<table><tbody>","</tbody></table>"],td:d,th:d,option:e,optgroup:e},g=8>=a.a.W;a.a.ua=function(c,d){var e;if(v)if(v.parseHTML)e=v.parseHTML(c,d)||[];else{if((e=v.clean([c],d))&&e[0]){for(var l=e[0];l.parentNode&&11!==l.parentNode.nodeType;)l=l.parentNode;
l.parentNode&&l.parentNode.removeChild(l)}}else{(e=d)||(e=w);var l=e.parentWindow||e.defaultView||A,p=a.a.Db(c).toLowerCase(),q=e.createElement("div"),t;t=(p=p.match(/^(?:\x3c!--.*?--\x3e\s*?)*?<([a-z]+)[\s>]/))&&f[p[1]]||b;p=t[0];t="ignored<div>"+t[1]+c+t[2]+"</div>";"function"==typeof l.innerShiv?q.appendChild(l.innerShiv(t)):(g&&e.body.appendChild(q),q.innerHTML=t,g&&q.parentNode.removeChild(q));for(;p--;)q=q.lastChild;e=a.a.la(q.lastChild.childNodes)}return e};a.a.Md=function(b,c){var d=a.a.ua(b,
c);return d.length&&d[0].parentElement||a.a.Yb(d)};a.a.fc=function(b,c){a.a.Tb(b);c=a.a.f(c);if(null!==c&&c!==n)if("string"!=typeof c&&(c=c.toString()),v)v(b).html(c);else for(var d=a.a.ua(c,b.ownerDocument),e=0;e<d.length;e++)b.appendChild(d[e])}})();a.b("utils.parseHtmlFragment",a.a.ua);a.b("utils.setHtml",a.a.fc);a.aa=function(){function b(c,e){if(c)if(8==c.nodeType){var f=a.aa.Uc(c.nodeValue);null!=f&&e.push({ud:c,Kd:f})}else if(1==c.nodeType)for(var f=0,g=c.childNodes,h=g.length;f<h;f++)b(g[f],
e)}var c={};return{Xb:function(a){if("function"!=typeof a)throw Error("You can only pass a function to ko.memoization.memoize()");var b=(4294967296*(1+Math.random())|0).toString(16).substring(1)+(4294967296*(1+Math.random())|0).toString(16).substring(1);c[b]=a;return"\x3c!--[ko_memo:"+b+"]--\x3e"},bd:function(a,b){var f=c[a];if(f===n)throw Error("Couldn't find any memo with ID "+a+". Perhaps it's already been unmemoized.");try{return f.apply(null,b||[]),!0}finally{delete c[a]}},cd:function(c,e){var f=
[];b(c,f);for(var g=0,h=f.length;g<h;g++){var m=f[g].ud,k=[m];e&&a.a.Nb(k,e);a.aa.bd(f[g].Kd,k);m.nodeValue="";m.parentNode&&m.parentNode.removeChild(m)}},Uc:function(a){return(a=a.match(/^\[ko_memo\:(.*?)\]$/))?a[1]:null}}}();a.b("memoization",a.aa);a.b("memoization.memoize",a.aa.Xb);a.b("memoization.unmemoize",a.aa.bd);a.b("memoization.parseMemoText",a.aa.Uc);a.b("memoization.unmemoizeDomNodeAndDescendants",a.aa.cd);a.na=function(){function b(){if(f)for(var b=f,c=0,d;h<f;)if(d=e[h++]){if(h>b){if(5E3<=
++c){h=f;a.a.Gc(Error("'Too much recursion' after processing "+c+" task groups."));break}b=f}try{d()}catch(p){a.a.Gc(p)}}}function c(){b();h=f=e.length=0}var d,e=[],f=0,g=1,h=0;A.MutationObserver?d=function(a){var b=w.createElement("div");(new MutationObserver(a)).observe(b,{attributes:!0});return function(){b.classList.toggle("foo")}}(c):d=w&&"onreadystatechange"in w.createElement("script")?function(a){var b=w.createElement("script");b.onreadystatechange=function(){b.onreadystatechange=null;w.documentElement.removeChild(b);
b=null;a()};w.documentElement.appendChild(b)}:function(a){setTimeout(a,0)};return{scheduler:d,zb:function(b){f||a.na.scheduler(c);e[f++]=b;return g++},cancel:function(a){a=a-(g-f);a>=h&&a<f&&(e[a]=null)},resetForTesting:function(){var a=f-h;h=f=e.length=0;return a},Sd:b}}();a.b("tasks",a.na);a.b("tasks.schedule",a.na.zb);a.b("tasks.runEarly",a.na.Sd);a.Ta={throttle:function(b,c){b.throttleEvaluation=c;var d=null;return a.$({read:b,write:function(e){clearTimeout(d);d=a.a.setTimeout(function(){b(e)},
c)}})},rateLimit:function(a,c){var d,e,f;"number"==typeof c?d=c:(d=c.timeout,e=c.method);a.Hb=!1;f="function"==typeof e?e:"notifyWhenChangesStop"==e?Y:X;a.ub(function(a){return f(a,d,c)})},deferred:function(b,c){if(!0!==c)throw Error("The 'deferred' extender only accepts the value 'true', because it is not supported to turn deferral off once enabled.");b.Hb||(b.Hb=!0,b.ub(function(c){var e,f=!1;return function(){if(!f){a.na.cancel(e);e=a.na.zb(c);try{f=!0,b.notifySubscribers(n,"dirty")}finally{f=
!1}}}}))},notify:function(a,c){a.equalityComparer="always"==c?null:K}};var W={undefined:1,"boolean":1,number:1,string:1};a.b("extenders",a.Ta);a.ic=function(b,c,d){this.da=b;this.lc=c;this.mc=d;this.Ib=!1;this.fb=this.Jb=null;a.L(this,"dispose",this.s);a.L(this,"disposeWhenNodeIsRemoved",this.l)};a.ic.prototype.s=function(){this.Ib||(this.fb&&a.a.K.yb(this.Jb,this.fb),this.Ib=!0,this.mc(),this.da=this.lc=this.mc=this.Jb=this.fb=null)};a.ic.prototype.l=function(b){this.Jb=b;a.a.K.za(b,this.fb=this.s.bind(this))};
a.T=function(){a.a.Ab(this,D);D.qb(this)};var D={qb:function(a){a.U={change:[]};a.sc=1},subscribe:function(b,c,d){var e=this;d=d||"change";var f=new a.ic(e,c?b.bind(c):b,function(){a.a.Pa(e.U[d],f);e.hb&&e.hb(d)});e.Qa&&e.Qa(d);e.U[d]||(e.U[d]=[]);e.U[d].push(f);return f},notifySubscribers:function(b,c){c=c||"change";"change"===c&&this.Gb();if(this.Wa(c)){var d="change"===c&&this.ed||this.U[c].slice(0);try{a.u.xc();for(var e=0,f;f=d[e];++e)f.Ib||f.lc(b)}finally{a.u.end()}}},ob:function(){return this.sc},
Dd:function(a){return this.ob()!==a},Gb:function(){++this.sc},ub:function(b){var c=this,d=a.O(c),e,f,g,h,m;c.gb||(c.gb=c.notifySubscribers,c.notifySubscribers=Z);var k=b(function(){c.Ja=!1;d&&h===c&&(h=c.nc?c.nc():c());var a=f||m&&c.sb(g,h);m=f=e=!1;a&&c.gb(g=h)});c.qc=function(a,b){b&&c.Ja||(m=!b);c.ed=c.U.change.slice(0);c.Ja=e=!0;h=a;k()};c.pc=function(a){e||(g=a,c.gb(a,"beforeChange"))};c.rc=function(){m=!0};c.gd=function(){c.sb(g,c.v(!0))&&(f=!0)}},Wa:function(a){return this.U[a]&&this.U[a].length},
Bd:function(b){if(b)return this.U[b]&&this.U[b].length||0;var c=0;a.a.P(this.U,function(a,b){"dirty"!==a&&(c+=b.length)});return c},sb:function(a,c){return!this.equalityComparer||!this.equalityComparer(a,c)},toString:function(){return"[object Object]"},extend:function(b){var c=this;b&&a.a.P(b,function(b,e){var f=a.Ta[b];"function"==typeof f&&(c=f(c,e)||c)});return c}};a.L(D,"init",D.qb);a.L(D,"subscribe",D.subscribe);a.L(D,"extend",D.extend);a.L(D,"getSubscriptionsCount",D.Bd);a.a.Ba&&a.a.setPrototypeOf(D,
Function.prototype);a.T.fn=D;a.Qc=function(a){return null!=a&&"function"==typeof a.subscribe&&"function"==typeof a.notifySubscribers};a.b("subscribable",a.T);a.b("isSubscribable",a.Qc);a.S=a.u=function(){function b(a){d.push(e);e=a}function c(){e=d.pop()}var d=[],e,f=0;return{xc:b,end:c,cc:function(b){if(e){if(!a.Qc(b))throw Error("Only subscribable things can act as dependencies");e.od.call(e.pd,b,b.fd||(b.fd=++f))}},G:function(a,d,e){try{return b(),a.apply(d,e||[])}finally{c()}},qa:function(){if(e)return e.o.qa()},
Va:function(){if(e)return e.o.Va()},Ya:function(){if(e)return e.Ya},o:function(){if(e)return e.o}}}();a.b("computedContext",a.S);a.b("computedContext.getDependenciesCount",a.S.qa);a.b("computedContext.getDependencies",a.S.Va);a.b("computedContext.isInitial",a.S.Ya);a.b("computedContext.registerDependency",a.S.cc);a.b("ignoreDependencies",a.Yd=a.u.G);var I=a.a.Da("_latestValue");a.ta=function(b){function c(){if(0<arguments.length)return c.sb(c[I],arguments[0])&&(c.ya(),c[I]=arguments[0],c.xa()),this;
a.u.cc(c);return c[I]}c[I]=b;a.a.Ba||a.a.extend(c,a.T.fn);a.T.fn.qb(c);a.a.Ab(c,F);a.options.deferUpdates&&a.Ta.deferred(c,!0);return c};var F={equalityComparer:K,v:function(){return this[I]},xa:function(){this.notifySubscribers(this[I],"spectate");this.notifySubscribers(this[I])},ya:function(){this.notifySubscribers(this[I],"beforeChange")}};a.a.Ba&&a.a.setPrototypeOf(F,a.T.fn);var G=a.ta.Ma="__ko_proto__";F[G]=a.ta;a.O=function(b){if((b="function"==typeof b&&b[G])&&b!==F[G]&&b!==a.o.fn[G])throw Error("Invalid object that looks like an observable; possibly from another Knockout instance");
return!!b};a.Za=function(b){return"function"==typeof b&&(b[G]===F[G]||b[G]===a.o.fn[G]&&b.Nc)};a.b("observable",a.ta);a.b("isObservable",a.O);a.b("isWriteableObservable",a.Za);a.b("isWritableObservable",a.Za);a.b("observable.fn",F);a.L(F,"peek",F.v);a.L(F,"valueHasMutated",F.xa);a.L(F,"valueWillMutate",F.ya);a.Ha=function(b){b=b||[];if("object"!=typeof b||!("length"in b))throw Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");b=a.ta(b);a.a.Ab(b,
a.Ha.fn);return b.extend({trackArrayChanges:!0})};a.Ha.fn={remove:function(b){for(var c=this.v(),d=[],e="function"!=typeof b||a.O(b)?function(a){return a===b}:b,f=0;f<c.length;f++){var g=c[f];if(e(g)){0===d.length&&this.ya();if(c[f]!==g)throw Error("Array modified during remove; cannot remove item");d.push(g);c.splice(f,1);f--}}d.length&&this.xa();return d},removeAll:function(b){if(b===n){var c=this.v(),d=c.slice(0);this.ya();c.splice(0,c.length);this.xa();return d}return b?this.remove(function(c){return 0<=
a.a.A(b,c)}):[]},destroy:function(b){var c=this.v(),d="function"!=typeof b||a.O(b)?function(a){return a===b}:b;this.ya();for(var e=c.length-1;0<=e;e--){var f=c[e];d(f)&&(f._destroy=!0)}this.xa()},destroyAll:function(b){return b===n?this.destroy(function(){return!0}):b?this.destroy(function(c){return 0<=a.a.A(b,c)}):[]},indexOf:function(b){var c=this();return a.a.A(c,b)},replace:function(a,c){var d=this.indexOf(a);0<=d&&(this.ya(),this.v()[d]=c,this.xa())},sorted:function(a){var c=this().slice(0);
return a?c.sort(a):c.sort()},reversed:function(){return this().slice(0).reverse()}};a.a.Ba&&a.a.setPrototypeOf(a.Ha.fn,a.ta.fn);a.a.D("pop push reverse shift sort splice unshift".split(" "),function(b){a.Ha.fn[b]=function(){var a=this.v();this.ya();this.zc(a,b,arguments);var d=a[b].apply(a,arguments);this.xa();return d===a?this:d}});a.a.D(["slice"],function(b){a.Ha.fn[b]=function(){var a=this();return a[b].apply(a,arguments)}});a.Pc=function(b){return a.O(b)&&"function"==typeof b.remove&&"function"==
typeof b.push};a.b("observableArray",a.Ha);a.b("isObservableArray",a.Pc);a.Ta.trackArrayChanges=function(b,c){function d(){function c(){if(m){var d=[].concat(b.v()||[]),e;if(b.Wa("arrayChange")){if(!f||1<m)f=a.a.Pb(k,d,b.Ob);e=f}k=d;f=null;m=0;e&&e.length&&b.notifySubscribers(e,"arrayChange")}}e?c():(e=!0,h=b.subscribe(function(){++m},null,"spectate"),k=[].concat(b.v()||[]),f=null,g=b.subscribe(c))}b.Ob={};c&&"object"==typeof c&&a.a.extend(b.Ob,c);b.Ob.sparse=!0;if(!b.zc){var e=!1,f=null,g,h,m=0,
k,l=b.Qa,p=b.hb;b.Qa=function(a){l&&l.call(b,a);"arrayChange"===a&&d()};b.hb=function(a){p&&p.call(b,a);"arrayChange"!==a||b.Wa("arrayChange")||(g&&g.s(),h&&h.s(),h=g=null,e=!1,k=n)};b.zc=function(b,c,d){function l(a,b,c){return k[k.length]={status:a,value:b,index:c}}if(e&&!m){var k=[],p=b.length,g=d.length,h=0;switch(c){case "push":h=p;case "unshift":for(c=0;c<g;c++)l("added",d[c],h+c);break;case "pop":h=p-1;case "shift":p&&l("deleted",b[h],h);break;case "splice":c=Math.min(Math.max(0,0>d[0]?p+d[0]:
d[0]),p);for(var p=1===g?p:Math.min(c+(d[1]||0),p),g=c+g-2,h=Math.max(p,g),U=[],L=[],n=2;c<h;++c,++n)c<p&&L.push(l("deleted",b[c],c)),c<g&&U.push(l("added",d[n],c));a.a.Kc(L,U);break;default:return}f=k}}}};var r=a.a.Da("_state");a.o=a.$=function(b,c,d){function e(){if(0<arguments.length){if("function"===typeof f)f.apply(g.nb,arguments);else throw Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");return this}g.ra||
a.u.cc(e);(g.ka||g.J&&e.Xa())&&e.ha();return g.X}"object"===typeof b?d=b:(d=d||{},b&&(d.read=b));if("function"!=typeof d.read)throw Error("Pass a function that returns the value of the ko.computed");var f=d.write,g={X:n,sa:!0,ka:!0,rb:!1,jc:!1,ra:!1,wb:!1,J:!1,Wc:d.read,nb:c||d.owner,l:d.disposeWhenNodeIsRemoved||d.l||null,Sa:d.disposeWhen||d.Sa,Rb:null,I:{},V:0,Ic:null};e[r]=g;e.Nc="function"===typeof f;a.a.Ba||a.a.extend(e,a.T.fn);a.T.fn.qb(e);a.a.Ab(e,C);d.pure?(g.wb=!0,g.J=!0,a.a.extend(e,da)):
d.deferEvaluation&&a.a.extend(e,ea);a.options.deferUpdates&&a.Ta.deferred(e,!0);g.l&&(g.jc=!0,g.l.nodeType||(g.l=null));g.J||d.deferEvaluation||e.ha();g.l&&e.ja()&&a.a.K.za(g.l,g.Rb=function(){e.s()});return e};var C={equalityComparer:K,qa:function(){return this[r].V},Va:function(){var b=[];a.a.P(this[r].I,function(a,d){b[d.Ka]=d.da});return b},Vb:function(b){if(!this[r].V)return!1;var c=this.Va();return-1!==a.a.A(c,b)?!0:!!a.a.Lb(c,function(a){return a.Vb&&a.Vb(b)})},uc:function(a,c,d){if(this[r].wb&&
c===this)throw Error("A 'pure' computed must not be called recursively");this[r].I[a]=d;d.Ka=this[r].V++;d.La=c.ob()},Xa:function(){var a,c,d=this[r].I;for(a in d)if(Object.prototype.hasOwnProperty.call(d,a)&&(c=d[a],this.Ia&&c.da.Ja||c.da.Dd(c.La)))return!0},Jd:function(){this.Ia&&!this[r].rb&&this.Ia(!1)},ja:function(){var a=this[r];return a.ka||0<a.V},Rd:function(){this.Ja?this[r].ka&&(this[r].sa=!0):this.Hc()},$c:function(a){if(a.Hb){var c=a.subscribe(this.Jd,this,"dirty"),d=a.subscribe(this.Rd,
this);return{da:a,s:function(){c.s();d.s()}}}return a.subscribe(this.Hc,this)},Hc:function(){var b=this,c=b.throttleEvaluation;c&&0<=c?(clearTimeout(this[r].Ic),this[r].Ic=a.a.setTimeout(function(){b.ha(!0)},c)):b.Ia?b.Ia(!0):b.ha(!0)},ha:function(b){var c=this[r],d=c.Sa,e=!1;if(!c.rb&&!c.ra){if(c.l&&!a.a.Sb(c.l)||d&&d()){if(!c.jc){this.s();return}}else c.jc=!1;c.rb=!0;try{e=this.zd(b)}finally{c.rb=!1}return e}},zd:function(b){var c=this[r],d=!1,e=c.wb?n:!c.V,d={qd:this,mb:c.I,Qb:c.V};a.u.xc({pd:d,
od:ba,o:this,Ya:e});c.I={};c.V=0;var f=this.yd(c,d);c.V?d=this.sb(c.X,f):(this.s(),d=!0);d&&(c.J?this.Gb():this.notifySubscribers(c.X,"beforeChange"),c.X=f,this.notifySubscribers(c.X,"spectate"),!c.J&&b&&this.notifySubscribers(c.X),this.rc&&this.rc());e&&this.notifySubscribers(c.X,"awake");return d},yd:function(b,c){try{var d=b.Wc;return b.nb?d.call(b.nb):d()}finally{a.u.end(),c.Qb&&!b.J&&a.a.P(c.mb,aa),b.sa=b.ka=!1}},v:function(a){var c=this[r];(c.ka&&(a||!c.V)||c.J&&this.Xa())&&this.ha();return c.X},
ub:function(b){a.T.fn.ub.call(this,b);this.nc=function(){this[r].J||(this[r].sa?this.ha():this[r].ka=!1);return this[r].X};this.Ia=function(a){this.pc(this[r].X);this[r].ka=!0;a&&(this[r].sa=!0);this.qc(this,!a)}},s:function(){var b=this[r];!b.J&&b.I&&a.a.P(b.I,function(a,b){b.s&&b.s()});b.l&&b.Rb&&a.a.K.yb(b.l,b.Rb);b.I=n;b.V=0;b.ra=!0;b.sa=!1;b.ka=!1;b.J=!1;b.l=n;b.Sa=n;b.Wc=n;this.Nc||(b.nb=n)}},da={Qa:function(b){var c=this,d=c[r];if(!d.ra&&d.J&&"change"==b){d.J=!1;if(d.sa||c.Xa())d.I=null,d.V=
0,c.ha()&&c.Gb();else{var e=[];a.a.P(d.I,function(a,b){e[b.Ka]=a});a.a.D(e,function(a,b){var e=d.I[a],m=c.$c(e.da);m.Ka=b;m.La=e.La;d.I[a]=m});c.Xa()&&c.ha()&&c.Gb()}d.ra||c.notifySubscribers(d.X,"awake")}},hb:function(b){var c=this[r];c.ra||"change"!=b||this.Wa("change")||(a.a.P(c.I,function(a,b){b.s&&(c.I[a]={da:b.da,Ka:b.Ka,La:b.La},b.s())}),c.J=!0,this.notifySubscribers(n,"asleep"))},ob:function(){var b=this[r];b.J&&(b.sa||this.Xa())&&this.ha();return a.T.fn.ob.call(this)}},ea={Qa:function(a){"change"!=
a&&"beforeChange"!=a||this.v()}};a.a.Ba&&a.a.setPrototypeOf(C,a.T.fn);var N=a.ta.Ma;C[N]=a.o;a.Oc=function(a){return"function"==typeof a&&a[N]===C[N]};a.Fd=function(b){return a.Oc(b)&&b[r]&&b[r].wb};a.b("computed",a.o);a.b("dependentObservable",a.o);a.b("isComputed",a.Oc);a.b("isPureComputed",a.Fd);a.b("computed.fn",C);a.L(C,"peek",C.v);a.L(C,"dispose",C.s);a.L(C,"isActive",C.ja);a.L(C,"getDependenciesCount",C.qa);a.L(C,"getDependencies",C.Va);a.xb=function(b,c){if("function"===typeof b)return a.o(b,
c,{pure:!0});b=a.a.extend({},b);b.pure=!0;return a.o(b,c)};a.b("pureComputed",a.xb);(function(){function b(a,f,g){g=g||new d;a=f(a);if("object"!=typeof a||null===a||a===n||a instanceof RegExp||a instanceof Date||a instanceof String||a instanceof Number||a instanceof Boolean)return a;var h=a instanceof Array?[]:{};g.save(a,h);c(a,function(c){var d=f(a[c]);switch(typeof d){case "boolean":case "number":case "string":case "function":h[c]=d;break;case "object":case "undefined":var l=g.get(d);h[c]=l!==
n?l:b(d,f,g)}});return h}function c(a,b){if(a instanceof Array){for(var c=0;c<a.length;c++)b(c);"function"==typeof a.toJSON&&b("toJSON")}else for(c in a)b(c)}function d(){this.keys=[];this.values=[]}a.ad=function(c){if(0==arguments.length)throw Error("When calling ko.toJS, pass the object you want to convert.");return b(c,function(b){for(var c=0;a.O(b)&&10>c;c++)b=b();return b})};a.toJSON=function(b,c,d){b=a.ad(b);return a.a.hc(b,c,d)};d.prototype={constructor:d,save:function(b,c){var d=a.a.A(this.keys,
b);0<=d?this.values[d]=c:(this.keys.push(b),this.values.push(c))},get:function(b){b=a.a.A(this.keys,b);return 0<=b?this.values[b]:n}}})();a.b("toJS",a.ad);a.b("toJSON",a.toJSON);a.Wd=function(b,c,d){function e(c){var e=a.xb(b,d).extend({ma:"always"}),h=e.subscribe(function(a){a&&(h.s(),c(a))});e.notifySubscribers(e.v());return h}return"function"!==typeof Promise||c?e(c.bind(d)):new Promise(e)};a.b("when",a.Wd);(function(){a.w={M:function(b){switch(a.a.R(b)){case "option":return!0===b.__ko__hasDomDataOptionValue__?
a.a.g.get(b,a.c.options.$b):7>=a.a.W?b.getAttributeNode("value")&&b.getAttributeNode("value").specified?b.value:b.text:b.value;case "select":return 0<=b.selectedIndex?a.w.M(b.options[b.selectedIndex]):n;default:return b.value}},cb:function(b,c,d){switch(a.a.R(b)){case "option":"string"===typeof c?(a.a.g.set(b,a.c.options.$b,n),"__ko__hasDomDataOptionValue__"in b&&delete b.__ko__hasDomDataOptionValue__,b.value=c):(a.a.g.set(b,a.c.options.$b,c),b.__ko__hasDomDataOptionValue__=!0,b.value="number"===
typeof c?c:"");break;case "select":if(""===c||null===c)c=n;for(var e=-1,f=0,g=b.options.length,h;f<g;++f)if(h=a.w.M(b.options[f]),h==c||""===h&&c===n){e=f;break}if(d||0<=e||c===n&&1<b.size)b.selectedIndex=e,6===a.a.W&&a.a.setTimeout(function(){b.selectedIndex=e},0);break;default:if(null===c||c===n)c="";b.value=c}}}})();a.b("selectExtensions",a.w);a.b("selectExtensions.readValue",a.w.M);a.b("selectExtensions.writeValue",a.w.cb);a.m=function(){function b(b){b=a.a.Db(b);123===b.charCodeAt(0)&&(b=b.slice(1,
-1));b+="\n,";var c=[],d=b.match(e),p,q=[],h=0;if(1<d.length){for(var x=0,B;B=d[x];++x){var u=B.charCodeAt(0);if(44===u){if(0>=h){c.push(p&&q.length?{key:p,value:q.join("")}:{unknown:p||q.join("")});p=h=0;q=[];continue}}else if(58===u){if(!h&&!p&&1===q.length){p=q.pop();continue}}else if(47===u&&1<B.length&&(47===B.charCodeAt(1)||42===B.charCodeAt(1)))continue;else 47===u&&x&&1<B.length?(u=d[x-1].match(f))&&!g[u[0]]&&(b=b.substr(b.indexOf(B)+1),d=b.match(e),x=-1,B="/"):40===u||123===u||91===u?++h:
41===u||125===u||93===u?--h:p||q.length||34!==u&&39!==u||(B=B.slice(1,-1));q.push(B)}if(0<h)throw Error("Unbalanced parentheses, braces, or brackets");}return c}var c=["true","false","null","undefined"],d=/^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i,e=RegExp("\"(?:\\\\.|[^\"])*\"|'(?:\\\\.|[^'])*'|`(?:\\\\.|[^`])*`|/\\*(?:[^*]|\\*+[^*/])*\\*+/|//.*\n|/(?:\\\\.|[^/])+/w*|[^\\s:,/][^,\"'`{}()/:[\\]]*[^\\s,\"'`{}()/:[\\]]|[^\\s]","g"),f=/[\])"'A-Za-z0-9_$]+$/,g={"in":1,"return":1,"typeof":1},
h={};return{Ra:[],wa:h,ac:b,vb:function(e,f){function l(b,e){var f;if(!x){var k=a.getBindingHandler(b);if(k&&k.preprocess&&!(e=k.preprocess(e,b,l)))return;if(k=h[b])f=e,0<=a.a.A(c,f)?f=!1:(k=f.match(d),f=null===k?!1:k[1]?"Object("+k[1]+")"+k[2]:f),k=f;k&&q.push("'"+("string"==typeof h[b]?h[b]:b)+"':function(_z){"+f+"=_z}")}g&&(e="function(){return "+e+" }");p.push("'"+b+"':"+e)}f=f||{};var p=[],q=[],g=f.valueAccessors,x=f.bindingParams,B="string"===typeof e?b(e):e;a.a.D(B,function(a){l(a.key||a.unknown,
a.value)});q.length&&l("_ko_property_writers","{"+q.join(",")+" }");return p.join(",")},Id:function(a,b){for(var c=0;c<a.length;c++)if(a[c].key==b)return!0;return!1},eb:function(b,c,d,e,f){if(b&&a.O(b))!a.Za(b)||f&&b.v()===e||b(e);else if((b=c.get("_ko_property_writers"))&&b[d])b[d](e)}}}();a.b("expressionRewriting",a.m);a.b("expressionRewriting.bindingRewriteValidators",a.m.Ra);a.b("expressionRewriting.parseObjectLiteral",a.m.ac);a.b("expressionRewriting.preProcessBindings",a.m.vb);a.b("expressionRewriting._twoWayBindings",
a.m.wa);a.b("jsonExpressionRewriting",a.m);a.b("jsonExpressionRewriting.insertPropertyAccessorsIntoJson",a.m.vb);(function(){function b(a){return 8==a.nodeType&&g.test(f?a.text:a.nodeValue)}function c(a){return 8==a.nodeType&&h.test(f?a.text:a.nodeValue)}function d(d,e){for(var f=d,h=1,g=[];f=f.nextSibling;){if(c(f)&&(a.a.g.set(f,k,!0),h--,0===h))return g;g.push(f);b(f)&&h++}if(!e)throw Error("Cannot find closing comment tag to match: "+d.nodeValue);return null}function e(a,b){var c=d(a,b);return c?
0<c.length?c[c.length-1].nextSibling:a.nextSibling:null}var f=w&&"\x3c!--test--\x3e"===w.createComment("test").text,g=f?/^\x3c!--\s*ko(?:\s+([\s\S]+))?\s*--\x3e$/:/^\s*ko(?:\s+([\s\S]+))?\s*$/,h=f?/^\x3c!--\s*\/ko\s*--\x3e$/:/^\s*\/ko\s*$/,m={ul:!0,ol:!0},k="__ko_matchedEndComment__";a.h={ea:{},childNodes:function(a){return b(a)?d(a):a.childNodes},Ea:function(c){if(b(c)){c=a.h.childNodes(c);for(var d=0,e=c.length;d<e;d++)a.removeNode(c[d])}else a.a.Tb(c)},va:function(c,d){if(b(c)){a.h.Ea(c);for(var e=
c.nextSibling,f=0,k=d.length;f<k;f++)e.parentNode.insertBefore(d[f],e)}else a.a.va(c,d)},Vc:function(a,c){var d;b(a)?(d=a.nextSibling,a=a.parentNode):d=a.firstChild;d?c!==d&&a.insertBefore(c,d):a.appendChild(c)},Wb:function(c,d,e){e?(e=e.nextSibling,b(c)&&(c=c.parentNode),e?d!==e&&c.insertBefore(d,e):c.appendChild(d)):a.h.Vc(c,d)},firstChild:function(a){if(b(a))return!a.nextSibling||c(a.nextSibling)?null:a.nextSibling;if(a.firstChild&&c(a.firstChild))throw Error("Found invalid end comment, as the first child of "+
a);return a.firstChild},nextSibling:function(d){b(d)&&(d=e(d));if(d.nextSibling&&c(d.nextSibling)){var f=d.nextSibling;if(c(f)&&!a.a.g.get(f,k))throw Error("Found end comment without a matching opening comment, as child of "+d);return null}return d.nextSibling},Cd:b,Vd:function(a){return(a=(f?a.text:a.nodeValue).match(g))?a[1]:null},Sc:function(d){if(m[a.a.R(d)]){var f=d.firstChild;if(f){do if(1===f.nodeType){var k;k=f.firstChild;var h=null;if(k){do if(h)h.push(k);else if(b(k)){var g=e(k,!0);g?k=
g:h=[k]}else c(k)&&(h=[k]);while(k=k.nextSibling)}if(k=h)for(h=f.nextSibling,g=0;g<k.length;g++)h?d.insertBefore(k[g],h):d.appendChild(k[g])}while(f=f.nextSibling)}}}}})();a.b("virtualElements",a.h);a.b("virtualElements.allowedBindings",a.h.ea);a.b("virtualElements.emptyNode",a.h.Ea);a.b("virtualElements.insertAfter",a.h.Wb);a.b("virtualElements.prepend",a.h.Vc);a.b("virtualElements.setDomNodeChildren",a.h.va);(function(){a.ga=function(){this.nd={}};a.a.extend(a.ga.prototype,{nodeHasBindings:function(b){switch(b.nodeType){case 1:return null!=
b.getAttribute("data-bind")||a.j.getComponentNameForNode(b);case 8:return a.h.Cd(b);default:return!1}},getBindings:function(b,c){var d=this.getBindingsString(b,c),d=d?this.parseBindingsString(d,c,b):null;return a.j.tc(d,b,c,!1)},getBindingAccessors:function(b,c){var d=this.getBindingsString(b,c),d=d?this.parseBindingsString(d,c,b,{valueAccessors:!0}):null;return a.j.tc(d,b,c,!0)},getBindingsString:function(b){switch(b.nodeType){case 1:return b.getAttribute("data-bind");case 8:return a.h.Vd(b);default:return null}},
parseBindingsString:function(b,c,d,e){try{var f=this.nd,g=b+(e&&e.valueAccessors||""),h;if(!(h=f[g])){var m,k="with($context){with($data||{}){return{"+a.m.vb(b,e)+"}}}";m=new Function("$context","$element",k);h=f[g]=m}return h(c,d)}catch(l){throw l.message="Unable to parse bindings.\nBindings value: "+b+"\nMessage: "+l.message,l;}}});a.ga.instance=new a.ga})();a.b("bindingProvider",a.ga);(function(){function b(b){var c=(b=a.a.g.get(b,z))&&b.N;c&&(b.N=null,c.Tc())}function c(c,d,e){this.node=c;this.yc=
d;this.kb=[];this.H=!1;d.N||a.a.K.za(c,b);e&&e.N&&(e.N.kb.push(c),this.Kb=e)}function d(a){return function(){return a}}function e(a){return a()}function f(b){return a.a.Ga(a.u.G(b),function(a,c){return function(){return b()[c]}})}function g(b,c,e){return"function"===typeof b?f(b.bind(null,c,e)):a.a.Ga(b,d)}function h(a,b){return f(this.getBindings.bind(this,a,b))}function m(b,c){var d=a.h.firstChild(c);if(d){var e,f=a.ga.instance,l=f.preprocessNode;if(l){for(;e=d;)d=a.h.nextSibling(e),l.call(f,e);
d=a.h.firstChild(c)}for(;e=d;)d=a.h.nextSibling(e),k(b,e)}a.i.ma(c,a.i.H)}function k(b,c){var d=b,e=1===c.nodeType;e&&a.h.Sc(c);if(e||a.ga.instance.nodeHasBindings(c))d=p(c,null,b).bindingContextForDescendants;d&&!u[a.a.R(c)]&&m(d,c)}function l(b){var c=[],d={},e=[];a.a.P(b,function ca(f){if(!d[f]){var k=a.getBindingHandler(f);k&&(k.after&&(e.push(f),a.a.D(k.after,function(c){if(b[c]){if(-1!==a.a.A(e,c))throw Error("Cannot combine the following bindings, because they have a cyclic dependency: "+e.join(", "));
ca(c)}}),e.length--),c.push({key:f,Mc:k}));d[f]=!0}});return c}function p(b,c,d){var f=a.a.g.Ub(b,z,{}),k=f.hd;if(!c){if(k)throw Error("You cannot apply bindings multiple times to the same element.");f.hd=!0}k||(f.context=d);f.Zb||(f.Zb={});var g;if(c&&"function"!==typeof c)g=c;else{var p=a.ga.instance,q=p.getBindingAccessors||h,m=a.$(function(){if(g=c?c(d,b):q.call(p,b,d)){if(d[t])d[t]();if(d[B])d[B]()}return g},null,{l:b});g&&m.ja()||(m=null)}var x=d,u;if(g){var J=function(){return a.a.Ga(m?m():
g,e)},r=m?function(a){return function(){return e(m()[a])}}:function(a){return g[a]};J.get=function(a){return g[a]&&e(r(a))};J.has=function(a){return a in g};a.i.H in g&&a.i.subscribe(b,a.i.H,function(){var c=(0,g[a.i.H])();if(c){var d=a.h.childNodes(b);d.length&&c(d,a.Ec(d[0]))}});a.i.pa in g&&(x=a.i.Cb(b,d),a.i.subscribe(b,a.i.pa,function(){var c=(0,g[a.i.pa])();c&&a.h.firstChild(b)&&c(b)}));f=l(g);a.a.D(f,function(c){var d=c.Mc.init,e=c.Mc.update,f=c.key;if(8===b.nodeType&&!a.h.ea[f])throw Error("The binding '"+
f+"' cannot be used with virtual elements");try{"function"==typeof d&&a.u.G(function(){var a=d(b,r(f),J,x.$data,x);if(a&&a.controlsDescendantBindings){if(u!==n)throw Error("Multiple bindings ("+u+" and "+f+") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");u=f}}),"function"==typeof e&&a.$(function(){e(b,r(f),J,x.$data,x)},null,{l:b})}catch(k){throw k.message='Unable to process binding "'+f+": "+g[f]+'"\nMessage: '+k.message,
k;}})}f=u===n;return{shouldBindDescendants:f,bindingContextForDescendants:f&&x}}function q(b,c){return b&&b instanceof a.fa?b:new a.fa(b,n,n,c)}var t=a.a.Da("_subscribable"),x=a.a.Da("_ancestorBindingInfo"),B=a.a.Da("_dataDependency");a.c={};var u={script:!0,textarea:!0,template:!0};a.getBindingHandler=function(b){return a.c[b]};var J={};a.fa=function(b,c,d,e,f){function k(){var b=p?h():h,f=a.a.f(b);c?(a.a.extend(l,c),x in c&&(l[x]=c[x])):(l.$parents=[],l.$root=f,l.ko=a);l[t]=q;g?f=l.$data:(l.$rawData=
b,l.$data=f);d&&(l[d]=f);e&&e(l,c,f);if(c&&c[t]&&!a.S.o().Vb(c[t]))c[t]();m&&(l[B]=m);return l.$data}var l=this,g=b===J,h=g?n:b,p="function"==typeof h&&!a.O(h),q,m=f&&f.dataDependency;f&&f.exportDependencies?k():(q=a.xb(k),q.v(),q.ja()?q.equalityComparer=null:l[t]=n)};a.fa.prototype.createChildContext=function(b,c,d,e){!e&&c&&"object"==typeof c&&(e=c,c=e.as,d=e.extend);if(c&&e&&e.noChildContext){var f="function"==typeof b&&!a.O(b);return new a.fa(J,this,null,function(a){d&&d(a);a[c]=f?b():b},e)}return new a.fa(b,
this,c,function(a,b){a.$parentContext=b;a.$parent=b.$data;a.$parents=(b.$parents||[]).slice(0);a.$parents.unshift(a.$parent);d&&d(a)},e)};a.fa.prototype.extend=function(b,c){return new a.fa(J,this,null,function(c){a.a.extend(c,"function"==typeof b?b(c):b)},c)};var z=a.a.g.Z();c.prototype.Tc=function(){this.Kb&&this.Kb.N&&this.Kb.N.sd(this.node)};c.prototype.sd=function(b){a.a.Pa(this.kb,b);!this.kb.length&&this.H&&this.Cc()};c.prototype.Cc=function(){this.H=!0;this.yc.N&&!this.kb.length&&(this.yc.N=
null,a.a.K.yb(this.node,b),a.i.ma(this.node,a.i.pa),this.Tc())};a.i={H:"childrenComplete",pa:"descendantsComplete",subscribe:function(b,c,d,e,f){var k=a.a.g.Ub(b,z,{});k.Fa||(k.Fa=new a.T);f&&f.notifyImmediately&&k.Zb[c]&&a.u.G(d,e,[b]);return k.Fa.subscribe(d,e,c)},ma:function(b,c){var d=a.a.g.get(b,z);if(d&&(d.Zb[c]=!0,d.Fa&&d.Fa.notifySubscribers(b,c),c==a.i.H))if(d.N)d.N.Cc();else if(d.N===n&&d.Fa&&d.Fa.Wa(a.i.pa))throw Error("descendantsComplete event not supported for bindings on this node");
},Cb:function(b,d){var e=a.a.g.Ub(b,z,{});e.N||(e.N=new c(b,e,d[x]));return d[x]==e?d:d.extend(function(a){a[x]=e})}};a.Td=function(b){return(b=a.a.g.get(b,z))&&b.context};a.ib=function(b,c,d){1===b.nodeType&&a.h.Sc(b);return p(b,c,q(d))};a.ld=function(b,c,d){d=q(d);return a.ib(b,g(c,d,b),d)};a.Oa=function(a,b){1!==b.nodeType&&8!==b.nodeType||m(q(a),b)};a.vc=function(a,b,c){!v&&A.jQuery&&(v=A.jQuery);if(2>arguments.length){if(b=w.body,!b)throw Error("ko.applyBindings: could not find document.body; has the document been loaded?");
}else if(!b||1!==b.nodeType&&8!==b.nodeType)throw Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");k(q(a,c),b)};a.Dc=function(b){return!b||1!==b.nodeType&&8!==b.nodeType?n:a.Td(b)};a.Ec=function(b){return(b=a.Dc(b))?b.$data:n};a.b("bindingHandlers",a.c);a.b("bindingEvent",a.i);a.b("bindingEvent.subscribe",a.i.subscribe);a.b("bindingEvent.startPossiblyAsyncContentBinding",a.i.Cb);a.b("applyBindings",a.vc);a.b("applyBindingsToDescendants",a.Oa);
a.b("applyBindingAccessorsToNode",a.ib);a.b("applyBindingsToNode",a.ld);a.b("contextFor",a.Dc);a.b("dataFor",a.Ec)})();(function(b){function c(c,e){var k=Object.prototype.hasOwnProperty.call(f,c)?f[c]:b,l;k?k.subscribe(e):(k=f[c]=new a.T,k.subscribe(e),d(c,function(b,d){var e=!(!d||!d.synchronous);g[c]={definition:b,Gd:e};delete f[c];l||e?k.notifySubscribers(b):a.na.zb(function(){k.notifySubscribers(b)})}),l=!0)}function d(a,b){e("getConfig",[a],function(c){c?e("loadComponent",[a,c],function(a){b(a,
c)}):b(null,null)})}function e(c,d,f,l){l||(l=a.j.loaders.slice(0));var g=l.shift();if(g){var q=g[c];if(q){var t=!1;if(q.apply(g,d.concat(function(a){t?f(null):null!==a?f(a):e(c,d,f,l)}))!==b&&(t=!0,!g.suppressLoaderExceptions))throw Error("Component loaders must supply values by invoking the callback, not by returning values synchronously.");}else e(c,d,f,l)}else f(null)}var f={},g={};a.j={get:function(d,e){var f=Object.prototype.hasOwnProperty.call(g,d)?g[d]:b;f?f.Gd?a.u.G(function(){e(f.definition)}):
a.na.zb(function(){e(f.definition)}):c(d,e)},Bc:function(a){delete g[a]},oc:e};a.j.loaders=[];a.b("components",a.j);a.b("components.get",a.j.get);a.b("components.clearCachedDefinition",a.j.Bc)})();(function(){function b(b,c,d,e){function g(){0===--B&&e(h)}var h={},B=2,u=d.template;d=d.viewModel;u?f(c,u,function(c){a.j.oc("loadTemplate",[b,c],function(a){h.template=a;g()})}):g();d?f(c,d,function(c){a.j.oc("loadViewModel",[b,c],function(a){h[m]=a;g()})}):g()}function c(a,b,d){if("function"===typeof b)d(function(a){return new b(a)});
else if("function"===typeof b[m])d(b[m]);else if("instance"in b){var e=b.instance;d(function(){return e})}else"viewModel"in b?c(a,b.viewModel,d):a("Unknown viewModel value: "+b)}function d(b){switch(a.a.R(b)){case "script":return a.a.ua(b.text);case "textarea":return a.a.ua(b.value);case "template":if(e(b.content))return a.a.Ca(b.content.childNodes)}return a.a.Ca(b.childNodes)}function e(a){return A.DocumentFragment?a instanceof DocumentFragment:a&&11===a.nodeType}function f(a,b,c){"string"===typeof b.require?
T||A.require?(T||A.require)([b.require],function(a){a&&"object"===typeof a&&a.Xd&&a["default"]&&(a=a["default"]);c(a)}):a("Uses require, but no AMD loader is present"):c(b)}function g(a){return function(b){throw Error("Component '"+a+"': "+b);}}var h={};a.j.register=function(b,c){if(!c)throw Error("Invalid configuration for "+b);if(a.j.tb(b))throw Error("Component "+b+" is already registered");h[b]=c};a.j.tb=function(a){return Object.prototype.hasOwnProperty.call(h,a)};a.j.unregister=function(b){delete h[b];
a.j.Bc(b)};a.j.Fc={getConfig:function(b,c){c(a.j.tb(b)?h[b]:null)},loadComponent:function(a,c,d){var e=g(a);f(e,c,function(c){b(a,e,c,d)})},loadTemplate:function(b,c,f){b=g(b);if("string"===typeof c)f(a.a.ua(c));else if(c instanceof Array)f(c);else if(e(c))f(a.a.la(c.childNodes));else if(c.element)if(c=c.element,A.HTMLElement?c instanceof HTMLElement:c&&c.tagName&&1===c.nodeType)f(d(c));else if("string"===typeof c){var h=w.getElementById(c);h?f(d(h)):b("Cannot find element with ID "+c)}else b("Unknown element type: "+
c);else b("Unknown template value: "+c)},loadViewModel:function(a,b,d){c(g(a),b,d)}};var m="createViewModel";a.b("components.register",a.j.register);a.b("components.isRegistered",a.j.tb);a.b("components.unregister",a.j.unregister);a.b("components.defaultLoader",a.j.Fc);a.j.loaders.push(a.j.Fc);a.j.dd=h})();(function(){function b(b,e){var f=b.getAttribute("params");if(f){var f=c.parseBindingsString(f,e,b,{valueAccessors:!0,bindingParams:!0}),f=a.a.Ga(f,function(c){return a.o(c,null,{l:b})}),g=a.a.Ga(f,
function(c){var e=c.v();return c.ja()?a.o({read:function(){return a.a.f(c())},write:a.Za(e)&&function(a){c()(a)},l:b}):e});Object.prototype.hasOwnProperty.call(g,"$raw")||(g.$raw=f);return g}return{$raw:{}}}a.j.getComponentNameForNode=function(b){var c=a.a.R(b);if(a.j.tb(c)&&(-1!=c.indexOf("-")||"[object HTMLUnknownElement]"==""+b||8>=a.a.W&&b.tagName===c))return c};a.j.tc=function(c,e,f,g){if(1===e.nodeType){var h=a.j.getComponentNameForNode(e);if(h){c=c||{};if(c.component)throw Error('Cannot use the "component" binding on a custom element matching a component');
var m={name:h,params:b(e,f)};c.component=g?function(){return m}:m}}return c};var c=new a.ga;9>a.a.W&&(a.j.register=function(a){return function(b){return a.apply(this,arguments)}}(a.j.register),w.createDocumentFragment=function(b){return function(){var c=b(),f=a.j.dd,g;for(g in f);return c}}(w.createDocumentFragment))})();(function(){function b(b,c,d){c=c.template;if(!c)throw Error("Component '"+b+"' has no template");b=a.a.Ca(c);a.h.va(d,b)}function c(a,b,c){var d=a.createViewModel;return d?d.call(a,
b,c):b}var d=0;a.c.component={init:function(e,f,g,h,m){function k(){var a=l&&l.dispose;"function"===typeof a&&a.call(l);q&&q.s();p=l=q=null}var l,p,q,t=a.a.la(a.h.childNodes(e));a.h.Ea(e);a.a.K.za(e,k);a.o(function(){var g=a.a.f(f()),h,u;"string"===typeof g?h=g:(h=a.a.f(g.name),u=a.a.f(g.params));if(!h)throw Error("No component name specified");var n=a.i.Cb(e,m),z=p=++d;a.j.get(h,function(d){if(p===z){k();if(!d)throw Error("Unknown component '"+h+"'");b(h,d,e);var f=c(d,u,{element:e,templateNodes:t});
d=n.createChildContext(f,{extend:function(a){a.$component=f;a.$componentTemplateNodes=t}});f&&f.koDescendantsComplete&&(q=a.i.subscribe(e,a.i.pa,f.koDescendantsComplete,f));l=f;a.Oa(d,e)}})},null,{l:e});return{controlsDescendantBindings:!0}}};a.h.ea.component=!0})();var V={"class":"className","for":"htmlFor"};a.c.attr={update:function(b,c){var d=a.a.f(c())||{};a.a.P(d,function(c,d){d=a.a.f(d);var g=c.indexOf(":"),g="lookupNamespaceURI"in b&&0<g&&b.lookupNamespaceURI(c.substr(0,g)),h=!1===d||null===
d||d===n;h?g?b.removeAttributeNS(g,c):b.removeAttribute(c):d=d.toString();8>=a.a.W&&c in V?(c=V[c],h?b.removeAttribute(c):b[c]=d):h||(g?b.setAttributeNS(g,c,d):b.setAttribute(c,d));"name"===c&&a.a.Yc(b,h?"":d)})}};(function(){a.c.checked={after:["value","attr"],init:function(b,c,d){function e(){var e=b.checked,f=g();if(!a.S.Ya()&&(e||!m&&!a.S.qa())){var k=a.u.G(c);if(l){var q=p?k.v():k,z=t;t=f;z!==f?e&&(a.a.Na(q,f,!0),a.a.Na(q,z,!1)):a.a.Na(q,f,e);p&&a.Za(k)&&k(q)}else h&&(f===n?f=e:e||(f=n)),a.m.eb(k,
d,"checked",f,!0)}}function f(){var d=a.a.f(c()),e=g();l?(b.checked=0<=a.a.A(d,e),t=e):b.checked=h&&e===n?!!d:g()===d}var g=a.xb(function(){if(d.has("checkedValue"))return a.a.f(d.get("checkedValue"));if(q)return d.has("value")?a.a.f(d.get("value")):b.value}),h="checkbox"==b.type,m="radio"==b.type;if(h||m){var k=c(),l=h&&a.a.f(k)instanceof Array,p=!(l&&k.push&&k.splice),q=m||l,t=l?g():n;m&&!b.name&&a.c.uniqueName.init(b,function(){return!0});a.o(e,null,{l:b});a.a.B(b,"click",e);a.o(f,null,{l:b});
k=n}}};a.m.wa.checked=!0;a.c.checkedValue={update:function(b,c){b.value=a.a.f(c())}}})();a.c["class"]={update:function(b,c){var d=a.a.Db(a.a.f(c()));a.a.Eb(b,b.__ko__cssValue,!1);b.__ko__cssValue=d;a.a.Eb(b,d,!0)}};a.c.css={update:function(b,c){var d=a.a.f(c());null!==d&&"object"==typeof d?a.a.P(d,function(c,d){d=a.a.f(d);a.a.Eb(b,c,d)}):a.c["class"].update(b,c)}};a.c.enable={update:function(b,c){var d=a.a.f(c());d&&b.disabled?b.removeAttribute("disabled"):d||b.disabled||(b.disabled=!0)}};a.c.disable=
{update:function(b,c){a.c.enable.update(b,function(){return!a.a.f(c())})}};a.c.event={init:function(b,c,d,e,f){var g=c()||{};a.a.P(g,function(g){"string"==typeof g&&a.a.B(b,g,function(b){var k,l=c()[g];if(l){try{var p=a.a.la(arguments);e=f.$data;p.unshift(e);k=l.apply(e,p)}finally{!0!==k&&(b.preventDefault?b.preventDefault():b.returnValue=!1)}!1===d.get(g+"Bubble")&&(b.cancelBubble=!0,b.stopPropagation&&b.stopPropagation())}})})}};a.c.foreach={Rc:function(b){return function(){var c=b(),d=a.a.bc(c);
if(!d||"number"==typeof d.length)return{foreach:c,templateEngine:a.ba.Ma};a.a.f(c);return{foreach:d.data,as:d.as,noChildContext:d.noChildContext,includeDestroyed:d.includeDestroyed,afterAdd:d.afterAdd,beforeRemove:d.beforeRemove,afterRender:d.afterRender,beforeMove:d.beforeMove,afterMove:d.afterMove,templateEngine:a.ba.Ma}}},init:function(b,c){return a.c.template.init(b,a.c.foreach.Rc(c))},update:function(b,c,d,e,f){return a.c.template.update(b,a.c.foreach.Rc(c),d,e,f)}};a.m.Ra.foreach=!1;a.h.ea.foreach=
!0;a.c.hasfocus={init:function(b,c,d){function e(e){b.__ko_hasfocusUpdating=!0;var f=b.ownerDocument;if("activeElement"in f){var g;try{g=f.activeElement}catch(l){g=f.body}e=g===b}f=c();a.m.eb(f,d,"hasfocus",e,!0);b.__ko_hasfocusLastValue=e;b.__ko_hasfocusUpdating=!1}var f=e.bind(null,!0),g=e.bind(null,!1);a.a.B(b,"focus",f);a.a.B(b,"focusin",f);a.a.B(b,"blur",g);a.a.B(b,"focusout",g);b.__ko_hasfocusLastValue=!1},update:function(b,c){var d=!!a.a.f(c());b.__ko_hasfocusUpdating||b.__ko_hasfocusLastValue===
d||(d?b.focus():b.blur(),!d&&b.__ko_hasfocusLastValue&&b.ownerDocument.body.focus(),a.u.G(a.a.Fb,null,[b,d?"focusin":"focusout"]))}};a.m.wa.hasfocus=!0;a.c.hasFocus=a.c.hasfocus;a.m.wa.hasFocus="hasfocus";a.c.html={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.fc(b,c())}};(function(){function b(b,d,e){a.c[b]={init:function(b,c,h,m,k){var l,p,q={},t,x,n;if(d){m=h.get("as");var u=h.get("noChildContext");n=!(m&&u);q={as:m,noChildContext:u,exportDependencies:n}}x=(t=
"render"==h.get("completeOn"))||h.has(a.i.pa);a.o(function(){var h=a.a.f(c()),m=!e!==!h,u=!p,r;if(n||m!==l){x&&(k=a.i.Cb(b,k));if(m){if(!d||n)q.dataDependency=a.S.o();r=d?k.createChildContext("function"==typeof h?h:c,q):a.S.qa()?k.extend(null,q):k}u&&a.S.qa()&&(p=a.a.Ca(a.h.childNodes(b),!0));m?(u||a.h.va(b,a.a.Ca(p)),a.Oa(r,b)):(a.h.Ea(b),t||a.i.ma(b,a.i.H));l=m}},null,{l:b});return{controlsDescendantBindings:!0}}};a.m.Ra[b]=!1;a.h.ea[b]=!0}b("if");b("ifnot",!1,!0);b("with",!0)})();a.c.let={init:function(b,
c,d,e,f){c=f.extend(c);a.Oa(c,b);return{controlsDescendantBindings:!0}}};a.h.ea.let=!0;var Q={};a.c.options={init:function(b){if("select"!==a.a.R(b))throw Error("options binding applies only to SELECT elements");for(;0<b.length;)b.remove(0);return{controlsDescendantBindings:!0}},update:function(b,c,d){function e(){return a.a.jb(b.options,function(a){return a.selected})}function f(a,b,c){var d=typeof b;return"function"==d?b(a):"string"==d?a[b]:c}function g(c,d){if(x&&l)a.i.ma(b,a.i.H);else if(t.length){var e=
0<=a.a.A(t,a.w.M(d[0]));a.a.Zc(d[0],e);x&&!e&&a.u.G(a.a.Fb,null,[b,"change"])}}var h=b.multiple,m=0!=b.length&&h?b.scrollTop:null,k=a.a.f(c()),l=d.get("valueAllowUnset")&&d.has("value"),p=d.get("optionsIncludeDestroyed");c={};var q,t=[];l||(h?t=a.a.Mb(e(),a.w.M):0<=b.selectedIndex&&t.push(a.w.M(b.options[b.selectedIndex])));k&&("undefined"==typeof k.length&&(k=[k]),q=a.a.jb(k,function(b){return p||b===n||null===b||!a.a.f(b._destroy)}),d.has("optionsCaption")&&(k=a.a.f(d.get("optionsCaption")),null!==
k&&k!==n&&q.unshift(Q)));var x=!1;c.beforeRemove=function(a){b.removeChild(a)};k=g;d.has("optionsAfterRender")&&"function"==typeof d.get("optionsAfterRender")&&(k=function(b,c){g(0,c);a.u.G(d.get("optionsAfterRender"),null,[c[0],b!==Q?b:n])});a.a.ec(b,q,function(c,e,g){g.length&&(t=!l&&g[0].selected?[a.w.M(g[0])]:[],x=!0);e=b.ownerDocument.createElement("option");c===Q?(a.a.Bb(e,d.get("optionsCaption")),a.w.cb(e,n)):(g=f(c,d.get("optionsValue"),c),a.w.cb(e,a.a.f(g)),c=f(c,d.get("optionsText"),g),
a.a.Bb(e,c));return[e]},c,k);if(!l){var B;h?B=t.length&&e().length<t.length:B=t.length&&0<=b.selectedIndex?a.w.M(b.options[b.selectedIndex])!==t[0]:t.length||0<=b.selectedIndex;B&&a.u.G(a.a.Fb,null,[b,"change"])}(l||a.S.Ya())&&a.i.ma(b,a.i.H);a.a.wd(b);m&&20<Math.abs(m-b.scrollTop)&&(b.scrollTop=m)}};a.c.options.$b=a.a.g.Z();a.c.selectedOptions={init:function(b,c,d){function e(){var e=c(),f=[];a.a.D(b.getElementsByTagName("option"),function(b){b.selected&&f.push(a.w.M(b))});a.m.eb(e,d,"selectedOptions",
f)}function f(){var d=a.a.f(c()),e=b.scrollTop;d&&"number"==typeof d.length&&a.a.D(b.getElementsByTagName("option"),function(b){var c=0<=a.a.A(d,a.w.M(b));b.selected!=c&&a.a.Zc(b,c)});b.scrollTop=e}if("select"!=a.a.R(b))throw Error("selectedOptions binding applies only to SELECT elements");var g;a.i.subscribe(b,a.i.H,function(){g?e():(a.a.B(b,"change",e),g=a.o(f,null,{l:b}))},null,{notifyImmediately:!0})},update:function(){}};a.m.wa.selectedOptions=!0;a.c.style={update:function(b,c){var d=a.a.f(c()||
{});a.a.P(d,function(c,d){d=a.a.f(d);if(null===d||d===n||!1===d)d="";if(v)v(b).css(c,d);else if(/^--/.test(c))b.style.setProperty(c,d);else{c=c.replace(/-(\w)/g,function(a,b){return b.toUpperCase()});var g=b.style[c];b.style[c]=d;d===g||b.style[c]!=g||isNaN(d)||(b.style[c]=d+"px")}})}};a.c.submit={init:function(b,c,d,e,f){if("function"!=typeof c())throw Error("The value for a submit binding must be a function");a.a.B(b,"submit",function(a){var d,e=c();try{d=e.call(f.$data,b)}finally{!0!==d&&(a.preventDefault?
a.preventDefault():a.returnValue=!1)}})}};a.c.text={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.Bb(b,c())}};a.h.ea.text=!0;(function(){if(A&&A.navigator){var b=function(a){if(a)return parseFloat(a[1])},c=A.navigator.userAgent,d,e,f,g,h;(d=A.opera&&A.opera.version&&parseInt(A.opera.version()))||(h=b(c.match(/Edge\/([^ ]+)$/)))||b(c.match(/Chrome\/([^ ]+)/))||(e=b(c.match(/Version\/([^ ]+) Safari/)))||(f=b(c.match(/Firefox\/([^ ]+)/)))||(g=a.a.W||b(c.match(/MSIE ([^ ]+)/)))||
(g=b(c.match(/rv:([^ )]+)/)))}if(8<=g&&10>g)var m=a.a.g.Z(),k=a.a.g.Z(),l=function(b){var c=this.activeElement;(c=c&&a.a.g.get(c,k))&&c(b)},p=function(b,c){var d=b.ownerDocument;a.a.g.get(d,m)||(a.a.g.set(d,m,!0),a.a.B(d,"selectionchange",l));a.a.g.set(b,k,c)};a.c.textInput={init:function(b,c,k){function l(c,d){a.a.B(b,c,d)}function m(){var d=a.a.f(c());if(null===d||d===n)d="";L!==n&&d===L?a.a.setTimeout(m,4):b.value!==d&&(y=!0,b.value=d,y=!1,v=b.value)}function r(){w||(L=b.value,w=a.a.setTimeout(z,
4))}function z(){clearTimeout(w);L=w=n;var d=b.value;v!==d&&(v=d,a.m.eb(c(),k,"textInput",d))}var v=b.value,w,L,A=9==a.a.W?r:z,y=!1;g&&l("keypress",z);11>g&&l("propertychange",function(a){y||"value"!==a.propertyName||A(a)});8==g&&(l("keyup",z),l("keydown",z));p&&(p(b,A),l("dragend",r));(!g||9<=g)&&l("input",A);5>e&&"textarea"===a.a.R(b)?(l("keydown",r),l("paste",r),l("cut",r)):11>d?l("keydown",r):4>f?(l("DOMAutoComplete",z),l("dragdrop",z),l("drop",z)):h&&"number"===b.type&&l("keydown",r);l("change",
z);l("blur",z);a.o(m,null,{l:b})}};a.m.wa.textInput=!0;a.c.textinput={preprocess:function(a,b,c){c("textInput",a)}}})();a.c.uniqueName={init:function(b,c){if(c()){var d="ko_unique_"+ ++a.c.uniqueName.rd;a.a.Yc(b,d)}}};a.c.uniqueName.rd=0;a.c.using={init:function(b,c,d,e,f){var g;d.has("as")&&(g={as:d.get("as"),noChildContext:d.get("noChildContext")});c=f.createChildContext(c,g);a.Oa(c,b);return{controlsDescendantBindings:!0}}};a.h.ea.using=!0;a.c.value={init:function(b,c,d){var e=a.a.R(b),f="input"==
e;if(!f||"checkbox"!=b.type&&"radio"!=b.type){var g=[],h=d.get("valueUpdate"),m=!1,k=null;h&&("string"==typeof h?g=[h]:g=a.a.wc(h),a.a.Pa(g,"change"));var l=function(){k=null;m=!1;var e=c(),f=a.w.M(b);a.m.eb(e,d,"value",f)};!a.a.W||!f||"text"!=b.type||"off"==b.autocomplete||b.form&&"off"==b.form.autocomplete||-1!=a.a.A(g,"propertychange")||(a.a.B(b,"propertychange",function(){m=!0}),a.a.B(b,"focus",function(){m=!1}),a.a.B(b,"blur",function(){m&&l()}));a.a.D(g,function(c){var d=l;a.a.Ud(c,"after")&&
(d=function(){k=a.w.M(b);a.a.setTimeout(l,0)},c=c.substring(5));a.a.B(b,c,d)});var p;p=f&&"file"==b.type?function(){var d=a.a.f(c());null===d||d===n||""===d?b.value="":a.u.G(l)}:function(){var f=a.a.f(c()),g=a.w.M(b);if(null!==k&&f===k)a.a.setTimeout(p,0);else if(f!==g||g===n)"select"===e?(g=d.get("valueAllowUnset"),a.w.cb(b,f,g),g||f===a.w.M(b)||a.u.G(l)):a.w.cb(b,f)};if("select"===e){var q;a.i.subscribe(b,a.i.H,function(){q?d.get("valueAllowUnset")?p():l():(a.a.B(b,"change",l),q=a.o(p,null,{l:b}))},
null,{notifyImmediately:!0})}else a.a.B(b,"change",l),a.o(p,null,{l:b})}else a.ib(b,{checkedValue:c})},update:function(){}};a.m.wa.value=!0;a.c.visible={update:function(b,c){var d=a.a.f(c()),e="none"!=b.style.display;d&&!e?b.style.display="":!d&&e&&(b.style.display="none")}};a.c.hidden={update:function(b,c){a.c.visible.update(b,function(){return!a.a.f(c())})}};(function(b){a.c[b]={init:function(c,d,e,f,g){return a.c.event.init.call(this,c,function(){var a={};a[b]=d();return a},e,f,g)}}})("click");
a.ca=function(){};a.ca.prototype.renderTemplateSource=function(){throw Error("Override renderTemplateSource");};a.ca.prototype.createJavaScriptEvaluatorBlock=function(){throw Error("Override createJavaScriptEvaluatorBlock");};a.ca.prototype.makeTemplateSource=function(b,c){if("string"==typeof b){c=c||w;var d=c.getElementById(b);if(!d)throw Error("Cannot find template with ID "+b);return new a.C.F(d)}if(1==b.nodeType||8==b.nodeType)return new a.C.ia(b);throw Error("Unknown template type: "+b);};a.ca.prototype.renderTemplate=
function(a,c,d,e){a=this.makeTemplateSource(a,e);return this.renderTemplateSource(a,c,d,e)};a.ca.prototype.isTemplateRewritten=function(a,c){return!1===this.allowTemplateRewriting?!0:this.makeTemplateSource(a,c).data("isRewritten")};a.ca.prototype.rewriteTemplate=function(a,c,d){a=this.makeTemplateSource(a,d);c=c(a.text());a.text(c);a.data("isRewritten",!0)};a.b("templateEngine",a.ca);a.kc=function(){function b(b,c,d,h){b=a.m.ac(b);for(var m=a.m.Ra,k=0;k<b.length;k++){var l=b[k].key;if(Object.prototype.hasOwnProperty.call(m,
l)){var p=m[l];if("function"===typeof p){if(l=p(b[k].value))throw Error(l);}else if(!p)throw Error("This template engine does not support the '"+l+"' binding within its templates");}}d="ko.__tr_ambtns(function($context,$element){return(function(){return{ "+a.m.vb(b,{valueAccessors:!0})+" } })()},'"+d.toLowerCase()+"')";return h.createJavaScriptEvaluatorBlock(d)+c}var c=/(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'|[^>]*))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi,
d=/\x3c!--\s*ko\b\s*([\s\S]*?)\s*--\x3e/g;return{xd:function(b,c,d){c.isTemplateRewritten(b,d)||c.rewriteTemplate(b,function(b){return a.kc.Ld(b,c)},d)},Ld:function(a,f){return a.replace(c,function(a,c,d,e,l){return b(l,c,d,f)}).replace(d,function(a,c){return b(c,"\x3c!-- ko --\x3e","#comment",f)})},md:function(b,c){return a.aa.Xb(function(d,h){var m=d.nextSibling;m&&m.nodeName.toLowerCase()===c&&a.ib(m,b,h)})}}}();a.b("__tr_ambtns",a.kc.md);(function(){a.C={};a.C.F=function(b){if(this.F=b){var c=
a.a.R(b);this.ab="script"===c?1:"textarea"===c?2:"template"==c&&b.content&&11===b.content.nodeType?3:4}};a.C.F.prototype.text=function(){var b=1===this.ab?"text":2===this.ab?"value":"innerHTML";if(0==arguments.length)return this.F[b];var c=arguments[0];"innerHTML"===b?a.a.fc(this.F,c):this.F[b]=c};var b=a.a.g.Z()+"_";a.C.F.prototype.data=function(c){if(1===arguments.length)return a.a.g.get(this.F,b+c);a.a.g.set(this.F,b+c,arguments[1])};var c=a.a.g.Z();a.C.F.prototype.nodes=function(){var b=this.F;
if(0==arguments.length){var e=a.a.g.get(b,c)||{},f=e.lb||(3===this.ab?b.content:4===this.ab?b:n);if(!f||e.jd){var g=this.text();g&&g!==e.bb&&(f=a.a.Md(g,b.ownerDocument),a.a.g.set(b,c,{lb:f,bb:g,jd:!0}))}return f}e=arguments[0];this.ab!==n&&this.text("");a.a.g.set(b,c,{lb:e})};a.C.ia=function(a){this.F=a};a.C.ia.prototype=new a.C.F;a.C.ia.prototype.constructor=a.C.ia;a.C.ia.prototype.text=function(){if(0==arguments.length){var b=a.a.g.get(this.F,c)||{};b.bb===n&&b.lb&&(b.bb=b.lb.innerHTML);return b.bb}a.a.g.set(this.F,
c,{bb:arguments[0]})};a.b("templateSources",a.C);a.b("templateSources.domElement",a.C.F);a.b("templateSources.anonymousTemplate",a.C.ia)})();(function(){function b(b,c,d){var e;for(c=a.h.nextSibling(c);b&&(e=b)!==c;)b=a.h.nextSibling(e),d(e,b)}function c(c,d){if(c.length){var e=c[0],f=c[c.length-1],g=e.parentNode,h=a.ga.instance,m=h.preprocessNode;if(m){b(e,f,function(a,b){var c=a.previousSibling,d=m.call(h,a);d&&(a===e&&(e=d[0]||b),a===f&&(f=d[d.length-1]||c))});c.length=0;if(!e)return;e===f?c.push(e):
(c.push(e,f),a.a.Ua(c,g))}b(e,f,function(b){1!==b.nodeType&&8!==b.nodeType||a.vc(d,b)});b(e,f,function(b){1!==b.nodeType&&8!==b.nodeType||a.aa.cd(b,[d])});a.a.Ua(c,g)}}function d(a){return a.nodeType?a:0<a.length?a[0]:null}function e(b,e,f,h,m){m=m||{};var n=(b&&d(b)||f||{}).ownerDocument,B=m.templateEngine||g;a.kc.xd(f,B,n);f=B.renderTemplate(f,h,m,n);if("number"!=typeof f.length||0<f.length&&"number"!=typeof f[0].nodeType)throw Error("Template engine must return an array of DOM nodes");n=!1;switch(e){case "replaceChildren":a.h.va(b,
f);n=!0;break;case "replaceNode":a.a.Xc(b,f);n=!0;break;case "ignoreTargetNode":break;default:throw Error("Unknown renderMode: "+e);}n&&(c(f,h),m.afterRender&&a.u.G(m.afterRender,null,[f,h[m.as||"$data"]]),"replaceChildren"==e&&a.i.ma(b,a.i.H));return f}function f(b,c,d){return a.O(b)?b():"function"===typeof b?b(c,d):b}var g;a.gc=function(b){if(b!=n&&!(b instanceof a.ca))throw Error("templateEngine must inherit from ko.templateEngine");g=b};a.dc=function(b,c,h,m,t){h=h||{};if((h.templateEngine||g)==
n)throw Error("Set a template engine before calling renderTemplate");t=t||"replaceChildren";if(m){var x=d(m);return a.$(function(){var g=c&&c instanceof a.fa?c:new a.fa(c,null,null,null,{exportDependencies:!0}),n=f(b,g.$data,g),g=e(m,t,n,g,h);"replaceNode"==t&&(m=g,x=d(m))},null,{Sa:function(){return!x||!a.a.Sb(x)},l:x&&"replaceNode"==t?x.parentNode:x})}return a.aa.Xb(function(d){a.dc(b,c,h,d,"replaceNode")})};a.Qd=function(b,d,g,h,m){function x(b,c){a.u.G(a.a.ec,null,[h,b,u,g,r,c]);a.i.ma(h,a.i.H)}
function r(a,b){c(b,v);g.afterRender&&g.afterRender(b,a);v=null}function u(a,c){v=m.createChildContext(a,{as:z,noChildContext:g.noChildContext,extend:function(a){a.$index=c;z&&(a[z+"Index"]=c)}});var d=f(b,a,v);return e(h,"ignoreTargetNode",d,v,g)}var v,z=g.as,w=!1===g.includeDestroyed||a.options.foreachHidesDestroyed&&!g.includeDestroyed;if(w||g.beforeRemove||!a.Pc(d))return a.$(function(){var b=a.a.f(d)||[];"undefined"==typeof b.length&&(b=[b]);w&&(b=a.a.jb(b,function(b){return b===n||null===b||
!a.a.f(b._destroy)}));x(b)},null,{l:h});x(d.v());var A=d.subscribe(function(a){x(d(),a)},null,"arrayChange");A.l(h);return A};var h=a.a.g.Z(),m=a.a.g.Z();a.c.template={init:function(b,c){var d=a.a.f(c());if("string"==typeof d||"name"in d)a.h.Ea(b);else if("nodes"in d){d=d.nodes||[];if(a.O(d))throw Error('The "nodes" option must be a plain, non-observable array.');var e=d[0]&&d[0].parentNode;e&&a.a.g.get(e,m)||(e=a.a.Yb(d),a.a.g.set(e,m,!0));(new a.C.ia(b)).nodes(e)}else if(d=a.h.childNodes(b),0<d.length)e=
a.a.Yb(d),(new a.C.ia(b)).nodes(e);else throw Error("Anonymous template defined, but no template content was provided");return{controlsDescendantBindings:!0}},update:function(b,c,d,e,f){var g=c();c=a.a.f(g);d=!0;e=null;"string"==typeof c?c={}:(g="name"in c?c.name:b,"if"in c&&(d=a.a.f(c["if"])),d&&"ifnot"in c&&(d=!a.a.f(c.ifnot)),d&&!g&&(d=!1));"foreach"in c?e=a.Qd(g,d&&c.foreach||[],c,b,f):d?(d=f,"data"in c&&(d=f.createChildContext(c.data,{as:c.as,noChildContext:c.noChildContext,exportDependencies:!0})),
e=a.dc(g,d,c,b)):a.h.Ea(b);f=e;(c=a.a.g.get(b,h))&&"function"==typeof c.s&&c.s();a.a.g.set(b,h,!f||f.ja&&!f.ja()?n:f)}};a.m.Ra.template=function(b){b=a.m.ac(b);return 1==b.length&&b[0].unknown||a.m.Id(b,"name")?null:"This template engine does not support anonymous templates nested within its templates"};a.h.ea.template=!0})();a.b("setTemplateEngine",a.gc);a.b("renderTemplate",a.dc);a.a.Kc=function(a,c,d){if(a.length&&c.length){var e,f,g,h,m;for(e=f=0;(!d||e<d)&&(h=a[f]);++f){for(g=0;m=c[g];++g)if(h.value===
m.value){h.moved=m.index;m.moved=h.index;c.splice(g,1);e=g=0;break}e+=g}}};a.a.Pb=function(){function b(b,d,e,f,g){var h=Math.min,m=Math.max,k=[],l,p=b.length,q,n=d.length,r=n-p||1,v=p+n+1,u,w,z;for(l=0;l<=p;l++)for(w=u,k.push(u=[]),z=h(n,l+r),q=m(0,l-1);q<=z;q++)u[q]=q?l?b[l-1]===d[q-1]?w[q-1]:h(w[q]||v,u[q-1]||v)+1:q+1:l+1;h=[];m=[];r=[];l=p;for(q=n;l||q;)n=k[l][q]-1,q&&n===k[l][q-1]?m.push(h[h.length]={status:e,value:d[--q],index:q}):l&&n===k[l-1][q]?r.push(h[h.length]={status:f,value:b[--l],index:l}):
(--q,--l,g.sparse||h.push({status:"retained",value:d[q]}));a.a.Kc(r,m,!g.dontLimitMoves&&10*p);return h.reverse()}return function(a,d,e){e="boolean"===typeof e?{dontLimitMoves:e}:e||{};a=a||[];d=d||[];return a.length<d.length?b(a,d,"added","deleted",e):b(d,a,"deleted","added",e)}}();a.b("utils.compareArrays",a.a.Pb);(function(){function b(b,c,d,h,m){var k=[],l=a.$(function(){var l=c(d,m,a.a.Ua(k,b))||[];0<k.length&&(a.a.Xc(k,l),h&&a.u.G(h,null,[d,l,m]));k.length=0;a.a.Nb(k,l)},null,{l:b,Sa:function(){return!a.a.kd(k)}});
return{Y:k,$:l.ja()?l:n}}var c=a.a.g.Z(),d=a.a.g.Z();a.a.ec=function(e,f,g,h,m,k){function l(b){y={Aa:b,pb:a.ta(w++)};v.push(y);r||F.push(y)}function p(b){y=t[b];w!==y.pb.v()&&D.push(y);y.pb(w++);a.a.Ua(y.Y,e);v.push(y)}function q(b,c){if(b)for(var d=0,e=c.length;d<e;d++)a.a.D(c[d].Y,function(a){b(a,d,c[d].Aa)})}f=f||[];"undefined"==typeof f.length&&(f=[f]);h=h||{};var t=a.a.g.get(e,c),r=!t,v=[],u=0,w=0,z=[],A=[],C=[],D=[],F=[],y,I=0;if(r)a.a.D(f,l);else{if(!k||t&&t._countWaitingForRemove){var E=
a.a.Mb(t,function(a){return a.Aa});k=a.a.Pb(E,f,{dontLimitMoves:h.dontLimitMoves,sparse:!0})}for(var E=0,G,H,K;G=k[E];E++)switch(H=G.moved,K=G.index,G.status){case "deleted":for(;u<K;)p(u++);H===n&&(y=t[u],y.$&&(y.$.s(),y.$=n),a.a.Ua(y.Y,e).length&&(h.beforeRemove&&(v.push(y),I++,y.Aa===d?y=null:C.push(y)),y&&z.push.apply(z,y.Y)));u++;break;case "added":for(;w<K;)p(u++);H!==n?(A.push(v.length),p(H)):l(G.value)}for(;w<f.length;)p(u++);v._countWaitingForRemove=I}a.a.g.set(e,c,v);q(h.beforeMove,D);a.a.D(z,
h.beforeRemove?a.oa:a.removeNode);var M,O,P;try{P=e.ownerDocument.activeElement}catch(N){}if(A.length)for(;(E=A.shift())!=n;){y=v[E];for(M=n;E;)if((O=v[--E].Y)&&O.length){M=O[O.length-1];break}for(f=0;u=y.Y[f];M=u,f++)a.h.Wb(e,u,M)}for(E=0;y=v[E];E++){y.Y||a.a.extend(y,b(e,g,y.Aa,m,y.pb));for(f=0;u=y.Y[f];M=u,f++)a.h.Wb(e,u,M);!y.Ed&&m&&(m(y.Aa,y.Y,y.pb),y.Ed=!0,M=y.Y[y.Y.length-1])}P&&e.ownerDocument.activeElement!=P&&P.focus();q(h.beforeRemove,C);for(E=0;E<C.length;++E)C[E].Aa=d;q(h.afterMove,D);
q(h.afterAdd,F)}})();a.b("utils.setDomNodeChildrenFromArrayMapping",a.a.ec);a.ba=function(){this.allowTemplateRewriting=!1};a.ba.prototype=new a.ca;a.ba.prototype.constructor=a.ba;a.ba.prototype.renderTemplateSource=function(b,c,d,e){if(c=(9>a.a.W?0:b.nodes)?b.nodes():null)return a.a.la(c.cloneNode(!0).childNodes);b=b.text();return a.a.ua(b,e)};a.ba.Ma=new a.ba;a.gc(a.ba.Ma);a.b("nativeTemplateEngine",a.ba);(function(){a.$a=function(){var a=this.Hd=function(){if(!v||!v.tmpl)return 0;try{if(0<=v.tmpl.tag.tmpl.open.toString().indexOf("__"))return 2}catch(a){}return 1}();
this.renderTemplateSource=function(b,e,f,g){g=g||w;f=f||{};if(2>a)throw Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");var h=b.data("precompiled");h||(h=b.text()||"",h=v.template(null,"{{ko_with $item.koBindingContext}}"+h+"{{/ko_with}}"),b.data("precompiled",h));b=[e.$data];e=v.extend({koBindingContext:e},f.templateOptions);e=v.tmpl(h,b,e);e.appendTo(g.createElement("div"));v.fragments={};return e};this.createJavaScriptEvaluatorBlock=function(a){return"{{ko_code ((function() { return "+
a+" })()) }}"};this.addTemplate=function(a,b){w.write("<script type='text/html' id='"+a+"'>"+b+"\x3c/script>")};0<a&&(v.tmpl.tag.ko_code={open:"__.push($1 || '');"},v.tmpl.tag.ko_with={open:"with($1) {",close:"} "})};a.$a.prototype=new a.ca;a.$a.prototype.constructor=a.$a;var b=new a.$a;0<b.Hd&&a.gc(b);a.b("jqueryTmplTemplateEngine",a.$a)})()})})();})();

@ -1,18 +0,0 @@
module.exports = {
'Heap' : require('heap'),
'Node' : require('./core/Node'),
'Grid' : require('./core/Grid'),
'Util' : require('./core/Util'),
'DiagonalMovement' : require('./core/DiagonalMovement'),
'Heuristic' : require('./core/Heuristic'),
'AStarFinder' : require('./finders/AStarFinder'),
'BestFirstFinder' : require('./finders/BestFirstFinder'),
'BreadthFirstFinder' : require('./finders/BreadthFirstFinder'),
'DijkstraFinder' : require('./finders/DijkstraFinder'),
'BiAStarFinder' : require('./finders/BiAStarFinder'),
'BiBestFirstFinder' : require('./finders/BiBestFirstFinder'),
'BiBreadthFirstFinder' : require('./finders/BiBreadthFirstFinder'),
'BiDijkstraFinder' : require('./finders/BiDijkstraFinder'),
'IDAStarFinder' : require('./finders/IDAStarFinder'),
'JumpPointFinder' : require('./finders/JumpPointFinder'),
};

@ -1,9 +0,0 @@
/**
* ____ _ _ _____ _ _ _ _
* | _ \ __ _| |_| |__ | ___(_)_ __ __| (_)_ __ __ _ (_)___
* | |_) / _` | __| '_ \| |_ | | '_ \ / _` | | '_ \ / _` | | / __|
* | __/ (_| | |_| | | | _| | | | | | (_| | | | | | (_| |_ | \__ \
* |_| \__,_|\__|_| |_|_| |_|_| |_|\__,_|_|_| |_|\__, (_)/ |___/
* |___/ |__/
* https://github.com/qiao/PathFinding.js
*/

@ -1,8 +0,0 @@
var DiagonalMovement = {
Always: 1,
Never: 2,
IfAtMostOneObstacle: 3,
OnlyWhenNoObstacles: 4
};
module.exports = DiagonalMovement;

@ -1,247 +0,0 @@
var Node = require('./Node');
var DiagonalMovement = require('./DiagonalMovement');
/**
* The Grid class, which serves as the encapsulation of the layout of the nodes.
* @constructor
* @param {number|Array.<Array.<(number|boolean)>>} width_or_matrix Number of columns of the grid, or matrix
* @param {number} height Number of rows of the grid.
* @param {Array.<Array.<(number|boolean)>>} [matrix] - A 0-1 matrix
* representing the walkable status of the nodes(0 or false for walkable).
* If the matrix is not supplied, all the nodes will be walkable. */
function Grid(width_or_matrix, height, matrix) {
var width;
if (typeof width_or_matrix !== 'object') {
width = width_or_matrix;
} else {
height = width_or_matrix.length;
width = width_or_matrix[0].length;
matrix = width_or_matrix;
}
/**
* The number of columns of the grid.
* @type number
*/
this.width = width;
/**
* The number of rows of the grid.
* @type number
*/
this.height = height;
/**
* A 2D array of nodes.
*/
this.nodes = this._buildNodes(width, height, matrix);
}
/**
* Build and return the nodes.
* @private
* @param {number} width
* @param {number} height
* @param {Array.<Array.<number|boolean>>} [matrix] - A 0-1 matrix representing
* the walkable status of the nodes.
* @see Grid
*/
Grid.prototype._buildNodes = function(width, height, matrix) {
var i, j,
nodes = new Array(height),
row;
for (i = 0; i < height; ++i) {
nodes[i] = new Array(width);
for (j = 0; j < width; ++j) {
nodes[i][j] = new Node(j, i);
}
}
if (matrix === undefined) {
return nodes;
}
if (matrix.length !== height || matrix[0].length !== width) {
throw new Error('Matrix size does not fit');
}
for (i = 0; i < height; ++i) {
for (j = 0; j < width; ++j) {
if (matrix[i][j]) {
// 0, false, null will be walkable
// while others will be un-walkable
nodes[i][j].walkable = false;
}
}
}
return nodes;
};
Grid.prototype.getNodeAt = function(x, y) {
return this.nodes[y][x];
};
/**
* Determine whether the node at the given position is walkable.
* (Also returns false if the position is outside the grid.)
* @param {number} x - The x coordinate of the node.
* @param {number} y - The y coordinate of the node.
* @return {boolean} - The walkability of the node.
*/
Grid.prototype.isWalkableAt = function(x, y) {
return this.isInside(x, y) && this.nodes[y][x].walkable;
};
/**
* Determine whether the position is inside the grid.
* XXX: `grid.isInside(x, y)` is wierd to read.
* It should be `(x, y) is inside grid`, but I failed to find a better
* name for this method.
* @param {number} x
* @param {number} y
* @return {boolean}
*/
Grid.prototype.isInside = function(x, y) {
return (x >= 0 && x < this.width) && (y >= 0 && y < this.height);
};
/**
* Set whether the node on the given position is walkable.
* NOTE: throws exception if the coordinate is not inside the grid.
* @param {number} x - The x coordinate of the node.
* @param {number} y - The y coordinate of the node.
* @param {boolean} walkable - Whether the position is walkable.
*/
Grid.prototype.setWalkableAt = function(x, y, walkable) {
this.nodes[y][x].walkable = walkable;
};
/**
* Get the neighbors of the given node.
*
* offsets diagonalOffsets:
* +---+---+---+ +---+---+---+
* | | 0 | | | 0 | | 1 |
* +---+---+---+ +---+---+---+
* | 3 | | 1 | | | | |
* +---+---+---+ +---+---+---+
* | | 2 | | | 3 | | 2 |
* +---+---+---+ +---+---+---+
*
* When allowDiagonal is true, if offsets[i] is valid, then
* diagonalOffsets[i] and
* diagonalOffsets[(i + 1) % 4] is valid.
* @param {Node} node
* @param {DiagonalMovement} diagonalMovement
*/
Grid.prototype.getNeighbors = function(node, diagonalMovement) {
var x = node.x,
y = node.y,
neighbors = [],
s0 = false, d0 = false,
s1 = false, d1 = false,
s2 = false, d2 = false,
s3 = false, d3 = false,
nodes = this.nodes;
// ↑
if (this.isWalkableAt(x, y - 1)) {
neighbors.push(nodes[y - 1][x]);
s0 = true;
}
// →
if (this.isWalkableAt(x + 1, y)) {
neighbors.push(nodes[y][x + 1]);
s1 = true;
}
// ↓
if (this.isWalkableAt(x, y + 1)) {
neighbors.push(nodes[y + 1][x]);
s2 = true;
}
// ←
if (this.isWalkableAt(x - 1, y)) {
neighbors.push(nodes[y][x - 1]);
s3 = true;
}
if (diagonalMovement === DiagonalMovement.Never) {
return neighbors;
}
if (diagonalMovement === DiagonalMovement.OnlyWhenNoObstacles) {
d0 = s3 && s0;
d1 = s0 && s1;
d2 = s1 && s2;
d3 = s2 && s3;
} else if (diagonalMovement === DiagonalMovement.IfAtMostOneObstacle) {
d0 = s3 || s0;
d1 = s0 || s1;
d2 = s1 || s2;
d3 = s2 || s3;
} else if (diagonalMovement === DiagonalMovement.Always) {
d0 = true;
d1 = true;
d2 = true;
d3 = true;
} else {
throw new Error('Incorrect value of diagonalMovement');
}
// ↖
if (d0 && this.isWalkableAt(x - 1, y - 1)) {
neighbors.push(nodes[y - 1][x - 1]);
}
// ↗
if (d1 && this.isWalkableAt(x + 1, y - 1)) {
neighbors.push(nodes[y - 1][x + 1]);
}
// ↘
if (d2 && this.isWalkableAt(x + 1, y + 1)) {
neighbors.push(nodes[y + 1][x + 1]);
}
// ↙
if (d3 && this.isWalkableAt(x - 1, y + 1)) {
neighbors.push(nodes[y + 1][x - 1]);
}
return neighbors;
};
/**
* Get a clone of this grid.
* @return {Grid} Cloned grid.
*/
Grid.prototype.clone = function() {
var i, j,
width = this.width,
height = this.height,
thisNodes = this.nodes,
newGrid = new Grid(width, height),
newNodes = new Array(height),
row;
for (i = 0; i < height; ++i) {
newNodes[i] = new Array(width);
for (j = 0; j < width; ++j) {
newNodes[i][j] = new Node(j, i, thisNodes[i][j].walkable);
}
}
newGrid.nodes = newNodes;
return newGrid;
};
module.exports = Grid;

@ -1,48 +0,0 @@
/**
* @namespace PF.Heuristic
* @description A collection of heuristic functions.
*/
module.exports = {
/**
* Manhattan distance.
* @param {number} dx - Difference in x.
* @param {number} dy - Difference in y.
* @return {number} dx + dy
*/
manhattan: function(dx, dy) {
return dx + dy;
},
/**
* Euclidean distance.
* @param {number} dx - Difference in x.
* @param {number} dy - Difference in y.
* @return {number} sqrt(dx * dx + dy * dy)
*/
euclidean: function(dx, dy) {
return Math.sqrt(dx * dx + dy * dy);
},
/**
* Octile distance.
* @param {number} dx - Difference in x.
* @param {number} dy - Difference in y.
* @return {number} sqrt(dx * dx + dy * dy) for grids
*/
octile: function(dx, dy) {
var F = Math.SQRT2 - 1;
return (dx < dy) ? F * dx + dy : F * dy + dx;
},
/**
* Chebyshev distance.
* @param {number} dx - Difference in x.
* @param {number} dy - Difference in y.
* @return {number} max(dx, dy)
*/
chebyshev: function(dx, dy) {
return Math.max(dx, dy);
}
};

@ -1,28 +0,0 @@
/**
* A node in grid.
* This class holds some basic information about a node and custom
* attributes may be added, depending on the algorithms' needs.
* @constructor
* @param {number} x - The x coordinate of the node on the grid.
* @param {number} y - The y coordinate of the node on the grid.
* @param {boolean} [walkable] - Whether this node is walkable.
*/
function Node(x, y, walkable) {
/**
* The x coordinate of the node on the grid.
* @type number
*/
this.x = x;
/**
* The y coordinate of the node on the grid.
* @type number
*/
this.y = y;
/**
* Whether this node can be walked through.
* @type boolean
*/
this.walkable = (walkable === undefined ? true : walkable);
}
module.exports = Node;

@ -1,246 +0,0 @@
/**
* Backtrace according to the parent records and return the path.
* (including both start and end nodes)
* @param {Node} node End node
* @return {Array.<Array.<number>>} the path
*/
function backtrace(node) {
var path = [[node.x, node.y]];
while (node.parent) {
node = node.parent;
path.push([node.x, node.y]);
}
return path.reverse();
}
exports.backtrace = backtrace;
/**
* Backtrace from start and end node, and return the path.
* (including both start and end nodes)
* @param {Node}
* @param {Node}
*/
function biBacktrace(nodeA, nodeB) {
var pathA = backtrace(nodeA),
pathB = backtrace(nodeB);
return pathA.concat(pathB.reverse());
}
exports.biBacktrace = biBacktrace;
/**
* Compute the length of the path.
* @param {Array.<Array.<number>>} path The path
* @return {number} The length of the path
*/
function pathLength(path) {
var i, sum = 0, a, b, dx, dy;
for (i = 1; i < path.length; ++i) {
a = path[i - 1];
b = path[i];
dx = a[0] - b[0];
dy = a[1] - b[1];
sum += Math.sqrt(dx * dx + dy * dy);
}
return sum;
}
exports.pathLength = pathLength;
/**
* Given the start and end coordinates, return all the coordinates lying
* on the line formed by these coordinates, based on Bresenham's algorithm.
* http://en.wikipedia.org/wiki/Bresenham's_line_algorithm#Simplification
* @param {number} x0 Start x coordinate
* @param {number} y0 Start y coordinate
* @param {number} x1 End x coordinate
* @param {number} y1 End y coordinate
* @return {Array.<Array.<number>>} The coordinates on the line
*/
function interpolate(x0, y0, x1, y1) {
var abs = Math.abs,
line = [],
sx, sy, dx, dy, err, e2;
dx = abs(x1 - x0);
dy = abs(y1 - y0);
sx = (x0 < x1) ? 1 : -1;
sy = (y0 < y1) ? 1 : -1;
err = dx - dy;
while (true) {
line.push([x0, y0]);
if (x0 === x1 && y0 === y1) {
break;
}
e2 = 2 * err;
if (e2 > -dy) {
err = err - dy;
x0 = x0 + sx;
}
if (e2 < dx) {
err = err + dx;
y0 = y0 + sy;
}
}
return line;
}
exports.interpolate = interpolate;
/**
* Given a compressed path, return a new path that has all the segments
* in it interpolated.
* @param {Array.<Array.<number>>} path The path
* @return {Array.<Array.<number>>} expanded path
*/
function expandPath(path) {
var expanded = [],
len = path.length,
coord0, coord1,
interpolated,
interpolatedLen,
i, j;
if (len < 2) {
return expanded;
}
for (i = 0; i < len - 1; ++i) {
coord0 = path[i];
coord1 = path[i + 1];
interpolated = interpolate(coord0[0], coord0[1], coord1[0], coord1[1]);
interpolatedLen = interpolated.length;
for (j = 0; j < interpolatedLen - 1; ++j) {
expanded.push(interpolated[j]);
}
}
expanded.push(path[len - 1]);
return expanded;
}
exports.expandPath = expandPath;
/**
* Smoothen the give path.
* The original path will not be modified; a new path will be returned.
* @param {PF.Grid} grid
* @param {Array.<Array.<number>>} path The path
*/
function smoothenPath(grid, path) {
var len = path.length,
x0 = path[0][0], // path start x
y0 = path[0][1], // path start y
x1 = path[len - 1][0], // path end x
y1 = path[len - 1][1], // path end y
sx, sy, // current start coordinate
ex, ey, // current end coordinate
newPath,
i, j, coord, line, testCoord, blocked;
sx = x0;
sy = y0;
newPath = [[sx, sy]];
for (i = 2; i < len; ++i) {
coord = path[i];
ex = coord[0];
ey = coord[1];
line = interpolate(sx, sy, ex, ey);
blocked = false;
for (j = 1; j < line.length; ++j) {
testCoord = line[j];
if (!grid.isWalkableAt(testCoord[0], testCoord[1])) {
blocked = true;
break;
}
}
if (blocked) {
lastValidCoord = path[i - 1];
newPath.push(lastValidCoord);
sx = lastValidCoord[0];
sy = lastValidCoord[1];
}
}
newPath.push([x1, y1]);
return newPath;
}
exports.smoothenPath = smoothenPath;
/**
* Compress a path, remove redundant nodes without altering the shape
* The original path is not modified
* @param {Array.<Array.<number>>} path The path
* @return {Array.<Array.<number>>} The compressed path
*/
function compressPath(path) {
// nothing to compress
if(path.length < 3) {
return path;
}
var compressed = [],
sx = path[0][0], // start x
sy = path[0][1], // start y
px = path[1][0], // second point x
py = path[1][1], // second point y
dx = px - sx, // direction between the two points
dy = py - sy, // direction between the two points
lx, ly,
ldx, ldy,
sq, i;
// normalize the direction
sq = Math.sqrt(dx*dx + dy*dy);
dx /= sq;
dy /= sq;
// start the new path
compressed.push([sx,sy]);
for(i = 2; i < path.length; i++) {
// store the last point
lx = px;
ly = py;
// store the last direction
ldx = dx;
ldy = dy;
// next point
px = path[i][0];
py = path[i][1];
// next direction
dx = px - lx;
dy = py - ly;
// normalize
sq = Math.sqrt(dx*dx + dy*dy);
dx /= sq;
dy /= sq;
// if the direction has changed, store the point
if ( dx !== ldx || dy !== ldy ) {
compressed.push([lx,ly]);
}
}
// store the last point
compressed.push([px,py]);
return compressed;
}
exports.compressPath = compressPath;

@ -1,125 +0,0 @@
var Heap = require('heap');
var Util = require('../core/Util');
var Heuristic = require('../core/Heuristic');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* A* path-finder.
* based upon https://github.com/bgrins/javascript-astar
* @constructor
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
* @param {function} opt.heuristic Heuristic function to estimate the distance
* (defaults to manhattan).
* @param {integer} opt.weight Weight to apply to the heuristic to allow for suboptimal paths,
* in order to speed up the search.
*/
function AStarFinder(opt) {
opt = opt || {};
this.allowDiagonal = opt.allowDiagonal;
this.dontCrossCorners = opt.dontCrossCorners;
this.heuristic = opt.heuristic || Heuristic.manhattan;
this.weight = opt.weight || 1;
this.diagonalMovement = opt.diagonalMovement;
if (!this.diagonalMovement) {
if (!this.allowDiagonal) {
this.diagonalMovement = DiagonalMovement.Never;
} else {
if (this.dontCrossCorners) {
this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles;
} else {
this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle;
}
}
}
//When diagonal movement is allowed the manhattan heuristic is not admissible
//It should be octile instead
if (this.diagonalMovement === DiagonalMovement.Never) {
this.heuristic = opt.heuristic || Heuristic.manhattan;
} else {
this.heuristic = opt.heuristic || Heuristic.octile;
}
}
/**
* Find and return the the path.
* @return {Array.<[number, number]>} The path, including both start and
* end positions.
*/
AStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) {
var openList = new Heap(function(nodeA, nodeB) {
return nodeA.f - nodeB.f;
}),
startNode = grid.getNodeAt(startX, startY),
endNode = grid.getNodeAt(endX, endY),
heuristic = this.heuristic,
diagonalMovement = this.diagonalMovement,
weight = this.weight,
abs = Math.abs, SQRT2 = Math.SQRT2,
node, neighbors, neighbor, i, l, x, y, ng;
// set the `g` and `f` value of the start node to be 0
startNode.g = 0;
startNode.f = 0;
// push the start node into the open list
openList.push(startNode);
startNode.opened = true;
// while the open list is not empty
while (!openList.empty()) {
// pop the position of node which has the minimum `f` value.
node = openList.pop();
node.closed = true;
// if reached the end position, construct the path and return it
if (node === endNode) {
return Util.backtrace(endNode);
}
// get neigbours of the current node
neighbors = grid.getNeighbors(node, diagonalMovement);
for (i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];
if (neighbor.closed) {
continue;
}
x = neighbor.x;
y = neighbor.y;
// get the distance between current node and the neighbor
// and calculate the next g score
ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2);
// check if the neighbor has not been inspected yet, or
// can be reached with smaller cost from the current node
if (!neighbor.opened || ng < neighbor.g) {
neighbor.g = ng;
neighbor.h = neighbor.h || weight * heuristic(abs(x - endX), abs(y - endY));
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = node;
if (!neighbor.opened) {
openList.push(neighbor);
neighbor.opened = true;
} else {
// the neighbor can be reached with smaller cost.
// Since its f value has been updated, we have to
// update its position in the open list
openList.updateItem(neighbor);
}
}
} // end for each neighbor
} // end while not open list empty
// fail to find the path
return [];
};
module.exports = AStarFinder;

@ -1,26 +0,0 @@
var AStarFinder = require('./AStarFinder');
/**
* Best-First-Search path-finder.
* @constructor
* @extends AStarFinder
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
* @param {function} opt.heuristic Heuristic function to estimate the distance
* (defaults to manhattan).
*/
function BestFirstFinder(opt) {
AStarFinder.call(this, opt);
var orig = this.heuristic;
this.heuristic = function(dx, dy) {
return orig(dx, dy) * 1000000;
};
}
BestFirstFinder.prototype = new AStarFinder();
BestFirstFinder.prototype.constructor = BestFirstFinder;
module.exports = BestFirstFinder;

@ -1,177 +0,0 @@
var Heap = require('heap');
var Util = require('../core/Util');
var Heuristic = require('../core/Heuristic');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* A* path-finder.
* based upon https://github.com/bgrins/javascript-astar
* @constructor
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
* @param {function} opt.heuristic Heuristic function to estimate the distance
* (defaults to manhattan).
* @param {integer} opt.weight Weight to apply to the heuristic to allow for suboptimal paths,
* in order to speed up the search.
*/
function BiAStarFinder(opt) {
opt = opt || {};
this.allowDiagonal = opt.allowDiagonal;
this.dontCrossCorners = opt.dontCrossCorners;
this.diagonalMovement = opt.diagonalMovement;
this.heuristic = opt.heuristic || Heuristic.manhattan;
this.weight = opt.weight || 1;
if (!this.diagonalMovement) {
if (!this.allowDiagonal) {
this.diagonalMovement = DiagonalMovement.Never;
} else {
if (this.dontCrossCorners) {
this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles;
} else {
this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle;
}
}
}
//When diagonal movement is allowed the manhattan heuristic is not admissible
//It should be octile instead
if (this.diagonalMovement === DiagonalMovement.Never) {
this.heuristic = opt.heuristic || Heuristic.manhattan;
} else {
this.heuristic = opt.heuristic || Heuristic.octile;
}
}
/**
* Find and return the the path.
* @return {Array.<[number, number]>} The path, including both start and
* end positions.
*/
BiAStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) {
var cmp = function(nodeA, nodeB) {
return nodeA.f - nodeB.f;
},
startOpenList = new Heap(cmp),
endOpenList = new Heap(cmp),
startNode = grid.getNodeAt(startX, startY),
endNode = grid.getNodeAt(endX, endY),
heuristic = this.heuristic,
diagonalMovement = this.diagonalMovement,
weight = this.weight,
abs = Math.abs, SQRT2 = Math.SQRT2,
node, neighbors, neighbor, i, l, x, y, ng,
BY_START = 1, BY_END = 2;
// set the `g` and `f` value of the start node to be 0
// and push it into the start open list
startNode.g = 0;
startNode.f = 0;
startOpenList.push(startNode);
startNode.opened = BY_START;
// set the `g` and `f` value of the end node to be 0
// and push it into the open open list
endNode.g = 0;
endNode.f = 0;
endOpenList.push(endNode);
endNode.opened = BY_END;
// while both the open lists are not empty
while (!startOpenList.empty() && !endOpenList.empty()) {
// pop the position of start node which has the minimum `f` value.
node = startOpenList.pop();
node.closed = true;
// get neigbours of the current node
neighbors = grid.getNeighbors(node, diagonalMovement);
for (i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];
if (neighbor.closed) {
continue;
}
if (neighbor.opened === BY_END) {
return Util.biBacktrace(node, neighbor);
}
x = neighbor.x;
y = neighbor.y;
// get the distance between current node and the neighbor
// and calculate the next g score
ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2);
// check if the neighbor has not been inspected yet, or
// can be reached with smaller cost from the current node
if (!neighbor.opened || ng < neighbor.g) {
neighbor.g = ng;
neighbor.h = neighbor.h || weight * heuristic(abs(x - endX), abs(y - endY));
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = node;
if (!neighbor.opened) {
startOpenList.push(neighbor);
neighbor.opened = BY_START;
} else {
// the neighbor can be reached with smaller cost.
// Since its f value has been updated, we have to
// update its position in the open list
startOpenList.updateItem(neighbor);
}
}
} // end for each neighbor
// pop the position of end node which has the minimum `f` value.
node = endOpenList.pop();
node.closed = true;
// get neigbours of the current node
neighbors = grid.getNeighbors(node, diagonalMovement);
for (i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];
if (neighbor.closed) {
continue;
}
if (neighbor.opened === BY_START) {
return Util.biBacktrace(neighbor, node);
}
x = neighbor.x;
y = neighbor.y;
// get the distance between current node and the neighbor
// and calculate the next g score
ng = node.g + ((x - node.x === 0 || y - node.y === 0) ? 1 : SQRT2);
// check if the neighbor has not been inspected yet, or
// can be reached with smaller cost from the current node
if (!neighbor.opened || ng < neighbor.g) {
neighbor.g = ng;
neighbor.h = neighbor.h || weight * heuristic(abs(x - startX), abs(y - startY));
neighbor.f = neighbor.g + neighbor.h;
neighbor.parent = node;
if (!neighbor.opened) {
endOpenList.push(neighbor);
neighbor.opened = BY_END;
} else {
// the neighbor can be reached with smaller cost.
// Since its f value has been updated, we have to
// update its position in the open list
endOpenList.updateItem(neighbor);
}
}
} // end for each neighbor
} // end while not open list empty
// fail to find the path
return [];
};
module.exports = BiAStarFinder;

@ -1,26 +0,0 @@
var BiAStarFinder = require('./BiAStarFinder');
/**
* Bi-direcitional Best-First-Search path-finder.
* @constructor
* @extends BiAStarFinder
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
* @param {function} opt.heuristic Heuristic function to estimate the distance
* (defaults to manhattan).
*/
function BiBestFirstFinder(opt) {
BiAStarFinder.call(this, opt);
var orig = this.heuristic;
this.heuristic = function(dx, dy) {
return orig(dx, dy) * 1000000;
};
}
BiBestFirstFinder.prototype = new BiAStarFinder();
BiBestFirstFinder.prototype.constructor = BiBestFirstFinder;
module.exports = BiBestFirstFinder;

@ -1,113 +0,0 @@
var Util = require('../core/Util');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Bi-directional Breadth-First-Search path finder.
* @constructor
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
*/
function BiBreadthFirstFinder(opt) {
opt = opt || {};
this.allowDiagonal = opt.allowDiagonal;
this.dontCrossCorners = opt.dontCrossCorners;
this.diagonalMovement = opt.diagonalMovement;
if (!this.diagonalMovement) {
if (!this.allowDiagonal) {
this.diagonalMovement = DiagonalMovement.Never;
} else {
if (this.dontCrossCorners) {
this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles;
} else {
this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle;
}
}
}
}
/**
* Find and return the the path.
* @return {Array.<[number, number]>} The path, including both start and
* end positions.
*/
BiBreadthFirstFinder.prototype.findPath = function(startX, startY, endX, endY, grid) {
var startNode = grid.getNodeAt(startX, startY),
endNode = grid.getNodeAt(endX, endY),
startOpenList = [], endOpenList = [],
neighbors, neighbor, node,
diagonalMovement = this.diagonalMovement,
BY_START = 0, BY_END = 1,
i, l;
// push the start and end nodes into the queues
startOpenList.push(startNode);
startNode.opened = true;
startNode.by = BY_START;
endOpenList.push(endNode);
endNode.opened = true;
endNode.by = BY_END;
// while both the queues are not empty
while (startOpenList.length && endOpenList.length) {
// expand start open list
node = startOpenList.shift();
node.closed = true;
neighbors = grid.getNeighbors(node, diagonalMovement);
for (i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];
if (neighbor.closed) {
continue;
}
if (neighbor.opened) {
// if this node has been inspected by the reversed search,
// then a path is found.
if (neighbor.by === BY_END) {
return Util.biBacktrace(node, neighbor);
}
continue;
}
startOpenList.push(neighbor);
neighbor.parent = node;
neighbor.opened = true;
neighbor.by = BY_START;
}
// expand end open list
node = endOpenList.shift();
node.closed = true;
neighbors = grid.getNeighbors(node, diagonalMovement);
for (i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];
if (neighbor.closed) {
continue;
}
if (neighbor.opened) {
if (neighbor.by === BY_START) {
return Util.biBacktrace(neighbor, node);
}
continue;
}
endOpenList.push(neighbor);
neighbor.parent = node;
neighbor.opened = true;
neighbor.by = BY_END;
}
}
// fail to find the path
return [];
};
module.exports = BiBreadthFirstFinder;

@ -1,22 +0,0 @@
var BiAStarFinder = require('./BiAStarFinder');
/**
* Bi-directional Dijkstra path-finder.
* @constructor
* @extends BiAStarFinder
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
*/
function BiDijkstraFinder(opt) {
BiAStarFinder.call(this, opt);
this.heuristic = function(dx, dy) {
return 0;
};
}
BiDijkstraFinder.prototype = new BiAStarFinder();
BiDijkstraFinder.prototype.constructor = BiDijkstraFinder;
module.exports = BiDijkstraFinder;

@ -1,77 +0,0 @@
var Util = require('../core/Util');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Breadth-First-Search path finder.
* @constructor
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
*/
function BreadthFirstFinder(opt) {
opt = opt || {};
this.allowDiagonal = opt.allowDiagonal;
this.dontCrossCorners = opt.dontCrossCorners;
this.diagonalMovement = opt.diagonalMovement;
if (!this.diagonalMovement) {
if (!this.allowDiagonal) {
this.diagonalMovement = DiagonalMovement.Never;
} else {
if (this.dontCrossCorners) {
this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles;
} else {
this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle;
}
}
}
}
/**
* Find and return the the path.
* @return {Array.<[number, number]>} The path, including both start and
* end positions.
*/
BreadthFirstFinder.prototype.findPath = function(startX, startY, endX, endY, grid) {
var openList = [],
diagonalMovement = this.diagonalMovement,
startNode = grid.getNodeAt(startX, startY),
endNode = grid.getNodeAt(endX, endY),
neighbors, neighbor, node, i, l;
// push the start pos into the queue
openList.push(startNode);
startNode.opened = true;
// while the queue is not empty
while (openList.length) {
// take the front node from the queue
node = openList.shift();
node.closed = true;
// reached the end position
if (node === endNode) {
return Util.backtrace(endNode);
}
neighbors = grid.getNeighbors(node, diagonalMovement);
for (i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];
// skip this neighbor if it has been inspected before
if (neighbor.closed || neighbor.opened) {
continue;
}
openList.push(neighbor);
neighbor.opened = true;
neighbor.parent = node;
}
}
// fail to find the path
return [];
};
module.exports = BreadthFirstFinder;

@ -1,22 +0,0 @@
var AStarFinder = require('./AStarFinder');
/**
* Dijkstra path-finder.
* @constructor
* @extends AStarFinder
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
*/
function DijkstraFinder(opt) {
AStarFinder.call(this, opt);
this.heuristic = function(dx, dy) {
return 0;
};
}
DijkstraFinder.prototype = new AStarFinder();
DijkstraFinder.prototype.constructor = DijkstraFinder;
module.exports = DijkstraFinder;

@ -1,208 +0,0 @@
var Util = require('../core/Util');
var Heuristic = require('../core/Heuristic');
var Node = require('../core/Node');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Iterative Deeping A Star (IDA*) path-finder.
*
* Recursion based on:
* http://www.apl.jhu.edu/~hall/AI-Programming/IDA-Star.html
*
* Path retracing based on:
* V. Nageshwara Rao, Vipin Kumar and K. Ramesh
* "A Parallel Implementation of Iterative-Deeping-A*", January 1987.
* ftp://ftp.cs.utexas.edu/.snapshot/hourly.1/pub/AI-Lab/tech-reports/UT-AI-TR-87-46.pdf
*
* @author Gerard Meier (www.gerardmeier.com)
*
* @constructor
* @param {object} opt
* @param {boolean} opt.allowDiagonal Whether diagonal movement is allowed. Deprecated, use diagonalMovement instead.
* @param {boolean} opt.dontCrossCorners Disallow diagonal movement touching block corners. Deprecated, use diagonalMovement instead.
* @param {DiagonalMovement} opt.diagonalMovement Allowed diagonal movement.
* @param {function} opt.heuristic Heuristic function to estimate the distance
* (defaults to manhattan).
* @param {integer} opt.weight Weight to apply to the heuristic to allow for suboptimal paths,
* in order to speed up the search.
* @param {object} opt.trackRecursion Whether to track recursion for statistical purposes.
* @param {object} opt.timeLimit Maximum execution time. Use <= 0 for infinite.
*/
function IDAStarFinder(opt) {
opt = opt || {};
this.allowDiagonal = opt.allowDiagonal;
this.dontCrossCorners = opt.dontCrossCorners;
this.diagonalMovement = opt.diagonalMovement;
this.heuristic = opt.heuristic || Heuristic.manhattan;
this.weight = opt.weight || 1;
this.trackRecursion = opt.trackRecursion || false;
this.timeLimit = opt.timeLimit || Infinity; // Default: no time limit.
if (!this.diagonalMovement) {
if (!this.allowDiagonal) {
this.diagonalMovement = DiagonalMovement.Never;
} else {
if (this.dontCrossCorners) {
this.diagonalMovement = DiagonalMovement.OnlyWhenNoObstacles;
} else {
this.diagonalMovement = DiagonalMovement.IfAtMostOneObstacle;
}
}
}
//When diagonal movement is allowed the manhattan heuristic is not admissible
//It should be octile instead
if (this.diagonalMovement === DiagonalMovement.Never) {
this.heuristic = opt.heuristic || Heuristic.manhattan;
} else {
this.heuristic = opt.heuristic || Heuristic.octile;
}
}
/**
* Find and return the the path. When an empty array is returned, either
* no path is possible, or the maximum execution time is reached.
*
* @return {Array.<[number, number]>} The path, including both start and
* end positions.
*/
IDAStarFinder.prototype.findPath = function(startX, startY, endX, endY, grid) {
// Used for statistics:
var nodesVisited = 0;
// Execution time limitation:
var startTime = new Date().getTime();
// Heuristic helper:
var h = function(a, b) {
return this.heuristic(Math.abs(b.x - a.x), Math.abs(b.y - a.y));
}.bind(this);
// Step cost from a to b:
var cost = function(a, b) {
return (a.x === b.x || a.y === b.y) ? 1 : Math.SQRT2;
};
/**
* IDA* search implementation.
*
* @param {Node} The node currently expanding from.
* @param {number} Cost to reach the given node.
* @param {number} Maximum search depth (cut-off value).
* @param {{Array.<[number, number]>}} The found route.
* @param {number} Recursion depth.
*
* @return {Object} either a number with the new optimal cut-off depth,
* or a valid node instance, in which case a path was found.
*/
var search = function(node, g, cutoff, route, depth) {
nodesVisited++;
// Enforce timelimit:
if(this.timeLimit > 0 && new Date().getTime() - startTime > this.timeLimit * 1000) {
// Enforced as "path-not-found".
return Infinity;
}
var f = g + h(node, end) * this.weight;
// We've searched too deep for this iteration.
if(f > cutoff) {
return f;
}
if(node == end) {
route[depth] = [node.x, node.y];
return node;
}
var min, t, k, neighbour;
var neighbours = grid.getNeighbors(node, this.diagonalMovement);
// Sort the neighbours, gives nicer paths. But, this deviates
// from the original algorithm - so I left it out.
//neighbours.sort(function(a, b){
// return h(a, end) - h(b, end);
//});
/*jshint -W084 *///Disable warning: Expected a conditional expression and instead saw an assignment
for(k = 0, min = Infinity; neighbour = neighbours[k]; ++k) {
/*jshint +W084 *///Enable warning: Expected a conditional expression and instead saw an assignment
if(this.trackRecursion) {
// Retain a copy for visualisation. Due to recursion, this
// node may be part of other paths too.
neighbour.retainCount = neighbour.retainCount + 1 || 1;
if(neighbour.tested !== true) {
neighbour.tested = true;
}
}
t = search(neighbour, g + cost(node, neighbour), cutoff, route, depth + 1);
if(t instanceof Node) {
route[depth] = [node.x, node.y];
// For a typical A* linked list, this would work:
// neighbour.parent = node;
return t;
}
// Decrement count, then determine whether it's actually closed.
if(this.trackRecursion && (--neighbour.retainCount) === 0) {
neighbour.tested = false;
}
if(t < min) {
min = t;
}
}
return min;
}.bind(this);
// Node instance lookups:
var start = grid.getNodeAt(startX, startY);
var end = grid.getNodeAt(endX, endY);
// Initial search depth, given the typical heuristic contraints,
// there should be no cheaper route possible.
var cutOff = h(start, end);
var j, route, t;
// With an overflow protection.
for(j = 0; true; ++j) {
//console.log("Iteration: " + j + ", search cut-off value: " + cutOff + ", nodes visited thus far: " + nodesVisited + ".");
route = [];
// Search till cut-off depth:
t = search(start, 0, cutOff, route, 0);
// Route not possible, or not found in time limit.
if(t === Infinity) {
return [];
}
// If t is a node, it's also the end node. Route is now
// populated with a valid path to the end node.
if(t instanceof Node) {
//console.log("Finished at iteration: " + j + ", search cut-off value: " + cutOff + ", nodes visited: " + nodesVisited + ".");
return route;
}
// Try again, this time with a deeper cut-off. The t score
// is the closest we got to the end node.
cutOff = t;
}
// This _should_ never to be reached.
return [];
};
module.exports = IDAStarFinder;

@ -1,149 +0,0 @@
/**
* @author imor / https://github.com/imor
*/
var JumpPointFinderBase = require('./JumpPointFinderBase');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Path finder using the Jump Point Search algorithm which always moves
* diagonally irrespective of the number of obstacles.
*/
function JPFAlwaysMoveDiagonally(opt) {
JumpPointFinderBase.call(this, opt);
}
JPFAlwaysMoveDiagonally.prototype = new JumpPointFinderBase();
JPFAlwaysMoveDiagonally.prototype.constructor = JPFAlwaysMoveDiagonally;
/**
* Search recursively in the direction (parent -> child), stopping only when a
* jump point is found.
* @protected
* @return {Array.<[number, number]>} The x, y coordinate of the jump point
* found, or null if not found
*/
JPFAlwaysMoveDiagonally.prototype._jump = function(x, y, px, py) {
var grid = this.grid,
dx = x - px, dy = y - py;
if (!grid.isWalkableAt(x, y)) {
return null;
}
if(this.trackJumpRecursion === true) {
grid.getNodeAt(x, y).tested = true;
}
if (grid.getNodeAt(x, y) === this.endNode) {
return [x, y];
}
// check for forced neighbors
// along the diagonal
if (dx !== 0 && dy !== 0) {
if ((grid.isWalkableAt(x - dx, y + dy) && !grid.isWalkableAt(x - dx, y)) ||
(grid.isWalkableAt(x + dx, y - dy) && !grid.isWalkableAt(x, y - dy))) {
return [x, y];
}
// when moving diagonally, must check for vertical/horizontal jump points
if (this._jump(x + dx, y, x, y) || this._jump(x, y + dy, x, y)) {
return [x, y];
}
}
// horizontally/vertically
else {
if( dx !== 0 ) { // moving along x
if((grid.isWalkableAt(x + dx, y + 1) && !grid.isWalkableAt(x, y + 1)) ||
(grid.isWalkableAt(x + dx, y - 1) && !grid.isWalkableAt(x, y - 1))) {
return [x, y];
}
}
else {
if((grid.isWalkableAt(x + 1, y + dy) && !grid.isWalkableAt(x + 1, y)) ||
(grid.isWalkableAt(x - 1, y + dy) && !grid.isWalkableAt(x - 1, y))) {
return [x, y];
}
}
}
return this._jump(x + dx, y + dy, x, y);
};
/**
* Find the neighbors for the given node. If the node has a parent,
* prune the neighbors based on the jump point search algorithm, otherwise
* return all available neighbors.
* @return {Array.<[number, number]>} The neighbors found.
*/
JPFAlwaysMoveDiagonally.prototype._findNeighbors = function(node) {
var parent = node.parent,
x = node.x, y = node.y,
grid = this.grid,
px, py, nx, ny, dx, dy,
neighbors = [], neighborNodes, neighborNode, i, l;
// directed pruning: can ignore most neighbors, unless forced.
if (parent) {
px = parent.x;
py = parent.y;
// get the normalized direction of travel
dx = (x - px) / Math.max(Math.abs(x - px), 1);
dy = (y - py) / Math.max(Math.abs(y - py), 1);
// search diagonally
if (dx !== 0 && dy !== 0) {
if (grid.isWalkableAt(x, y + dy)) {
neighbors.push([x, y + dy]);
}
if (grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y]);
}
if (grid.isWalkableAt(x + dx, y + dy)) {
neighbors.push([x + dx, y + dy]);
}
if (!grid.isWalkableAt(x - dx, y)) {
neighbors.push([x - dx, y + dy]);
}
if (!grid.isWalkableAt(x, y - dy)) {
neighbors.push([x + dx, y - dy]);
}
}
// search horizontally/vertically
else {
if(dx === 0) {
if (grid.isWalkableAt(x, y + dy)) {
neighbors.push([x, y + dy]);
}
if (!grid.isWalkableAt(x + 1, y)) {
neighbors.push([x + 1, y + dy]);
}
if (!grid.isWalkableAt(x - 1, y)) {
neighbors.push([x - 1, y + dy]);
}
}
else {
if (grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y]);
}
if (!grid.isWalkableAt(x, y + 1)) {
neighbors.push([x + dx, y + 1]);
}
if (!grid.isWalkableAt(x, y - 1)) {
neighbors.push([x + dx, y - 1]);
}
}
}
}
// return all neighbors
else {
neighborNodes = grid.getNeighbors(node, DiagonalMovement.Always);
for (i = 0, l = neighborNodes.length; i < l; ++i) {
neighborNode = neighborNodes[i];
neighbors.push([neighborNode.x, neighborNode.y]);
}
}
return neighbors;
};
module.exports = JPFAlwaysMoveDiagonally;

@ -1,155 +0,0 @@
/**
* @author imor / https://github.com/imor
*/
var JumpPointFinderBase = require('./JumpPointFinderBase');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Path finder using the Jump Point Search algorithm which moves
* diagonally only when there is at most one obstacle.
*/
function JPFMoveDiagonallyIfAtMostOneObstacle(opt) {
JumpPointFinderBase.call(this, opt);
}
JPFMoveDiagonallyIfAtMostOneObstacle.prototype = new JumpPointFinderBase();
JPFMoveDiagonallyIfAtMostOneObstacle.prototype.constructor = JPFMoveDiagonallyIfAtMostOneObstacle;
/**
* Search recursively in the direction (parent -> child), stopping only when a
* jump point is found.
* @protected
* @return {Array.<[number, number]>} The x, y coordinate of the jump point
* found, or null if not found
*/
JPFMoveDiagonallyIfAtMostOneObstacle.prototype._jump = function(x, y, px, py) {
var grid = this.grid,
dx = x - px, dy = y - py;
if (!grid.isWalkableAt(x, y)) {
return null;
}
if(this.trackJumpRecursion === true) {
grid.getNodeAt(x, y).tested = true;
}
if (grid.getNodeAt(x, y) === this.endNode) {
return [x, y];
}
// check for forced neighbors
// along the diagonal
if (dx !== 0 && dy !== 0) {
if ((grid.isWalkableAt(x - dx, y + dy) && !grid.isWalkableAt(x - dx, y)) ||
(grid.isWalkableAt(x + dx, y - dy) && !grid.isWalkableAt(x, y - dy))) {
return [x, y];
}
// when moving diagonally, must check for vertical/horizontal jump points
if (this._jump(x + dx, y, x, y) || this._jump(x, y + dy, x, y)) {
return [x, y];
}
}
// horizontally/vertically
else {
if( dx !== 0 ) { // moving along x
if((grid.isWalkableAt(x + dx, y + 1) && !grid.isWalkableAt(x, y + 1)) ||
(grid.isWalkableAt(x + dx, y - 1) && !grid.isWalkableAt(x, y - 1))) {
return [x, y];
}
}
else {
if((grid.isWalkableAt(x + 1, y + dy) && !grid.isWalkableAt(x + 1, y)) ||
(grid.isWalkableAt(x - 1, y + dy) && !grid.isWalkableAt(x - 1, y))) {
return [x, y];
}
}
}
// moving diagonally, must make sure one of the vertical/horizontal
// neighbors is open to allow the path
if (grid.isWalkableAt(x + dx, y) || grid.isWalkableAt(x, y + dy)) {
return this._jump(x + dx, y + dy, x, y);
} else {
return null;
}
};
/**
* Find the neighbors for the given node. If the node has a parent,
* prune the neighbors based on the jump point search algorithm, otherwise
* return all available neighbors.
* @return {Array.<[number, number]>} The neighbors found.
*/
JPFMoveDiagonallyIfAtMostOneObstacle.prototype._findNeighbors = function(node) {
var parent = node.parent,
x = node.x, y = node.y,
grid = this.grid,
px, py, nx, ny, dx, dy,
neighbors = [], neighborNodes, neighborNode, i, l;
// directed pruning: can ignore most neighbors, unless forced.
if (parent) {
px = parent.x;
py = parent.y;
// get the normalized direction of travel
dx = (x - px) / Math.max(Math.abs(x - px), 1);
dy = (y - py) / Math.max(Math.abs(y - py), 1);
// search diagonally
if (dx !== 0 && dy !== 0) {
if (grid.isWalkableAt(x, y + dy)) {
neighbors.push([x, y + dy]);
}
if (grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y]);
}
if (grid.isWalkableAt(x, y + dy) || grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y + dy]);
}
if (!grid.isWalkableAt(x - dx, y) && grid.isWalkableAt(x, y + dy)) {
neighbors.push([x - dx, y + dy]);
}
if (!grid.isWalkableAt(x, y - dy) && grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y - dy]);
}
}
// search horizontally/vertically
else {
if(dx === 0) {
if (grid.isWalkableAt(x, y + dy)) {
neighbors.push([x, y + dy]);
if (!grid.isWalkableAt(x + 1, y)) {
neighbors.push([x + 1, y + dy]);
}
if (!grid.isWalkableAt(x - 1, y)) {
neighbors.push([x - 1, y + dy]);
}
}
}
else {
if (grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y]);
if (!grid.isWalkableAt(x, y + 1)) {
neighbors.push([x + dx, y + 1]);
}
if (!grid.isWalkableAt(x, y - 1)) {
neighbors.push([x + dx, y - 1]);
}
}
}
}
}
// return all neighbors
else {
neighborNodes = grid.getNeighbors(node, DiagonalMovement.IfAtMostOneObstacle);
for (i = 0, l = neighborNodes.length; i < l; ++i) {
neighborNode = neighborNodes[i];
neighbors.push([neighborNode.x, neighborNode.y]);
}
}
return neighbors;
};
module.exports = JPFMoveDiagonallyIfAtMostOneObstacle;

@ -1,174 +0,0 @@
/**
* @author imor / https://github.com/imor
*/
var JumpPointFinderBase = require('./JumpPointFinderBase');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Path finder using the Jump Point Search algorithm which moves
* diagonally only when there are no obstacles.
*/
function JPFMoveDiagonallyIfNoObstacles(opt) {
JumpPointFinderBase.call(this, opt);
}
JPFMoveDiagonallyIfNoObstacles.prototype = new JumpPointFinderBase();
JPFMoveDiagonallyIfNoObstacles.prototype.constructor = JPFMoveDiagonallyIfNoObstacles;
/**
* Search recursively in the direction (parent -> child), stopping only when a
* jump point is found.
* @protected
* @return {Array.<[number, number]>} The x, y coordinate of the jump point
* found, or null if not found
*/
JPFMoveDiagonallyIfNoObstacles.prototype._jump = function(x, y, px, py) {
var grid = this.grid,
dx = x - px, dy = y - py;
if (!grid.isWalkableAt(x, y)) {
return null;
}
if(this.trackJumpRecursion === true) {
grid.getNodeAt(x, y).tested = true;
}
if (grid.getNodeAt(x, y) === this.endNode) {
return [x, y];
}
// check for forced neighbors
// along the diagonal
if (dx !== 0 && dy !== 0) {
// if ((grid.isWalkableAt(x - dx, y + dy) && !grid.isWalkableAt(x - dx, y)) ||
// (grid.isWalkableAt(x + dx, y - dy) && !grid.isWalkableAt(x, y - dy))) {
// return [x, y];
// }
// when moving diagonally, must check for vertical/horizontal jump points
if (this._jump(x + dx, y, x, y) || this._jump(x, y + dy, x, y)) {
return [x, y];
}
}
// horizontally/vertically
else {
if (dx !== 0) {
if ((grid.isWalkableAt(x, y - 1) && !grid.isWalkableAt(x - dx, y - 1)) ||
(grid.isWalkableAt(x, y + 1) && !grid.isWalkableAt(x - dx, y + 1))) {
return [x, y];
}
}
else if (dy !== 0) {
if ((grid.isWalkableAt(x - 1, y) && !grid.isWalkableAt(x - 1, y - dy)) ||
(grid.isWalkableAt(x + 1, y) && !grid.isWalkableAt(x + 1, y - dy))) {
return [x, y];
}
// When moving vertically, must check for horizontal jump points
// if (this._jump(x + 1, y, x, y) || this._jump(x - 1, y, x, y)) {
// return [x, y];
// }
}
}
// moving diagonally, must make sure one of the vertical/horizontal
// neighbors is open to allow the path
if (grid.isWalkableAt(x + dx, y) && grid.isWalkableAt(x, y + dy)) {
return this._jump(x + dx, y + dy, x, y);
} else {
return null;
}
};
/**
* Find the neighbors for the given node. If the node has a parent,
* prune the neighbors based on the jump point search algorithm, otherwise
* return all available neighbors.
* @return {Array.<[number, number]>} The neighbors found.
*/
JPFMoveDiagonallyIfNoObstacles.prototype._findNeighbors = function(node) {
var parent = node.parent,
x = node.x, y = node.y,
grid = this.grid,
px, py, nx, ny, dx, dy,
neighbors = [], neighborNodes, neighborNode, i, l;
// directed pruning: can ignore most neighbors, unless forced.
if (parent) {
px = parent.x;
py = parent.y;
// get the normalized direction of travel
dx = (x - px) / Math.max(Math.abs(x - px), 1);
dy = (y - py) / Math.max(Math.abs(y - py), 1);
// search diagonally
if (dx !== 0 && dy !== 0) {
if (grid.isWalkableAt(x, y + dy)) {
neighbors.push([x, y + dy]);
}
if (grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y]);
}
if (grid.isWalkableAt(x, y + dy) && grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y + dy]);
}
}
// search horizontally/vertically
else {
var isNextWalkable;
if (dx !== 0) {
isNextWalkable = grid.isWalkableAt(x + dx, y);
var isTopWalkable = grid.isWalkableAt(x, y + 1);
var isBottomWalkable = grid.isWalkableAt(x, y - 1);
if (isNextWalkable) {
neighbors.push([x + dx, y]);
if (isTopWalkable) {
neighbors.push([x + dx, y + 1]);
}
if (isBottomWalkable) {
neighbors.push([x + dx, y - 1]);
}
}
if (isTopWalkable) {
neighbors.push([x, y + 1]);
}
if (isBottomWalkable) {
neighbors.push([x, y - 1]);
}
}
else if (dy !== 0) {
isNextWalkable = grid.isWalkableAt(x, y + dy);
var isRightWalkable = grid.isWalkableAt(x + 1, y);
var isLeftWalkable = grid.isWalkableAt(x - 1, y);
if (isNextWalkable) {
neighbors.push([x, y + dy]);
if (isRightWalkable) {
neighbors.push([x + 1, y + dy]);
}
if (isLeftWalkable) {
neighbors.push([x - 1, y + dy]);
}
}
if (isRightWalkable) {
neighbors.push([x + 1, y]);
}
if (isLeftWalkable) {
neighbors.push([x - 1, y]);
}
}
}
}
// return all neighbors
else {
neighborNodes = grid.getNeighbors(node, DiagonalMovement.OnlyWhenNoObstacles);
for (i = 0, l = neighborNodes.length; i < l; ++i) {
neighborNode = neighborNodes[i];
neighbors.push([neighborNode.x, neighborNode.y]);
}
}
return neighbors;
};
module.exports = JPFMoveDiagonallyIfNoObstacles;

@ -1,120 +0,0 @@
/**
* @author imor / https://github.com/imor
*/
var JumpPointFinderBase = require('./JumpPointFinderBase');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Path finder using the Jump Point Search algorithm allowing only horizontal
* or vertical movements.
*/
function JPFNeverMoveDiagonally(opt) {
JumpPointFinderBase.call(this, opt);
}
JPFNeverMoveDiagonally.prototype = new JumpPointFinderBase();
JPFNeverMoveDiagonally.prototype.constructor = JPFNeverMoveDiagonally;
/**
* Search recursively in the direction (parent -> child), stopping only when a
* jump point is found.
* @protected
* @return {Array.<[number, number]>} The x, y coordinate of the jump point
* found, or null if not found
*/
JPFNeverMoveDiagonally.prototype._jump = function(x, y, px, py) {
var grid = this.grid,
dx = x - px, dy = y - py;
if (!grid.isWalkableAt(x, y)) {
return null;
}
if(this.trackJumpRecursion === true) {
grid.getNodeAt(x, y).tested = true;
}
if (grid.getNodeAt(x, y) === this.endNode) {
return [x, y];
}
if (dx !== 0) {
if ((grid.isWalkableAt(x, y - 1) && !grid.isWalkableAt(x - dx, y - 1)) ||
(grid.isWalkableAt(x, y + 1) && !grid.isWalkableAt(x - dx, y + 1))) {
return [x, y];
}
}
else if (dy !== 0) {
if ((grid.isWalkableAt(x - 1, y) && !grid.isWalkableAt(x - 1, y - dy)) ||
(grid.isWalkableAt(x + 1, y) && !grid.isWalkableAt(x + 1, y - dy))) {
return [x, y];
}
//When moving vertically, must check for horizontal jump points
if (this._jump(x + 1, y, x, y) || this._jump(x - 1, y, x, y)) {
return [x, y];
}
}
else {
throw new Error("Only horizontal and vertical movements are allowed");
}
return this._jump(x + dx, y + dy, x, y);
};
/**
* Find the neighbors for the given node. If the node has a parent,
* prune the neighbors based on the jump point search algorithm, otherwise
* return all available neighbors.
* @return {Array.<[number, number]>} The neighbors found.
*/
JPFNeverMoveDiagonally.prototype._findNeighbors = function(node) {
var parent = node.parent,
x = node.x, y = node.y,
grid = this.grid,
px, py, nx, ny, dx, dy,
neighbors = [], neighborNodes, neighborNode, i, l;
// directed pruning: can ignore most neighbors, unless forced.
if (parent) {
px = parent.x;
py = parent.y;
// get the normalized direction of travel
dx = (x - px) / Math.max(Math.abs(x - px), 1);
dy = (y - py) / Math.max(Math.abs(y - py), 1);
if (dx !== 0) {
if (grid.isWalkableAt(x, y - 1)) {
neighbors.push([x, y - 1]);
}
if (grid.isWalkableAt(x, y + 1)) {
neighbors.push([x, y + 1]);
}
if (grid.isWalkableAt(x + dx, y)) {
neighbors.push([x + dx, y]);
}
}
else if (dy !== 0) {
if (grid.isWalkableAt(x - 1, y)) {
neighbors.push([x - 1, y]);
}
if (grid.isWalkableAt(x + 1, y)) {
neighbors.push([x + 1, y]);
}
if (grid.isWalkableAt(x, y + dy)) {
neighbors.push([x, y + dy]);
}
}
}
// return all neighbors
else {
neighborNodes = grid.getNeighbors(node, DiagonalMovement.Never);
for (i = 0, l = neighborNodes.length; i < l; ++i) {
neighborNode = neighborNodes[i];
neighbors.push([neighborNode.x, neighborNode.y]);
}
}
return neighbors;
};
module.exports = JPFNeverMoveDiagonally;

@ -1,31 +0,0 @@
/**
* @author aniero / https://github.com/aniero
*/
var DiagonalMovement = require('../core/DiagonalMovement');
var JPFNeverMoveDiagonally = require('./JPFNeverMoveDiagonally');
var JPFAlwaysMoveDiagonally = require('./JPFAlwaysMoveDiagonally');
var JPFMoveDiagonallyIfNoObstacles = require('./JPFMoveDiagonallyIfNoObstacles');
var JPFMoveDiagonallyIfAtMostOneObstacle = require('./JPFMoveDiagonallyIfAtMostOneObstacle');
/**
* Path finder using the Jump Point Search algorithm
* @param {object} opt
* @param {function} opt.heuristic Heuristic function to estimate the distance
* (defaults to manhattan).
* @param {DiagonalMovement} opt.diagonalMovement Condition under which diagonal
* movement will be allowed.
*/
function JumpPointFinder(opt) {
opt = opt || {};
if (opt.diagonalMovement === DiagonalMovement.Never) {
return new JPFNeverMoveDiagonally(opt);
} else if (opt.diagonalMovement === DiagonalMovement.Always) {
return new JPFAlwaysMoveDiagonally(opt);
} else if (opt.diagonalMovement === DiagonalMovement.OnlyWhenNoObstacles) {
return new JPFMoveDiagonallyIfNoObstacles(opt);
} else {
return new JPFMoveDiagonallyIfAtMostOneObstacle(opt);
}
}
module.exports = JumpPointFinder;

@ -1,114 +0,0 @@
/**
* @author imor / https://github.com/imor
*/
var Heap = require('heap');
var Util = require('../core/Util');
var Heuristic = require('../core/Heuristic');
var DiagonalMovement = require('../core/DiagonalMovement');
/**
* Base class for the Jump Point Search algorithm
* @param {object} opt
* @param {function} opt.heuristic Heuristic function to estimate the distance
* (defaults to manhattan).
*/
function JumpPointFinderBase(opt) {
opt = opt || {};
this.heuristic = opt.heuristic || Heuristic.manhattan;
this.trackJumpRecursion = opt.trackJumpRecursion || false;
}
/**
* Find and return the path.
* @return {Array.<[number, number]>} The path, including both start and
* end positions.
*/
JumpPointFinderBase.prototype.findPath = function(startX, startY, endX, endY, grid) {
var openList = this.openList = new Heap(function(nodeA, nodeB) {
return nodeA.f - nodeB.f;
}),
startNode = this.startNode = grid.getNodeAt(startX, startY),
endNode = this.endNode = grid.getNodeAt(endX, endY), node;
this.grid = grid;
// set the `g` and `f` value of the start node to be 0
startNode.g = 0;
startNode.f = 0;
// push the start node into the open list
openList.push(startNode);
startNode.opened = true;
// while the open list is not empty
while (!openList.empty()) {
// pop the position of node which has the minimum `f` value.
node = openList.pop();
node.closed = true;
if (node === endNode) {
return Util.expandPath(Util.backtrace(endNode));
}
this._identifySuccessors(node);
}
// fail to find the path
return [];
};
/**
* Identify successors for the given node. Runs a jump point search in the
* direction of each available neighbor, adding any points found to the open
* list.
* @protected
*/
JumpPointFinderBase.prototype._identifySuccessors = function(node) {
var grid = this.grid,
heuristic = this.heuristic,
openList = this.openList,
endX = this.endNode.x,
endY = this.endNode.y,
neighbors, neighbor,
jumpPoint, i, l,
x = node.x, y = node.y,
jx, jy, dx, dy, d, ng, jumpNode,
abs = Math.abs, max = Math.max;
neighbors = this._findNeighbors(node);
for(i = 0, l = neighbors.length; i < l; ++i) {
neighbor = neighbors[i];
jumpPoint = this._jump(neighbor[0], neighbor[1], x, y);
if (jumpPoint) {
jx = jumpPoint[0];
jy = jumpPoint[1];
jumpNode = grid.getNodeAt(jx, jy);
if (jumpNode.closed) {
continue;
}
// include distance, as parent may not be immediately adjacent:
d = Heuristic.octile(abs(jx - x), abs(jy - y));
ng = node.g + d; // next `g` value
if (!jumpNode.opened || ng < jumpNode.g) {
jumpNode.g = ng;
jumpNode.h = jumpNode.h || heuristic(abs(jx - endX), abs(jy - endY));
jumpNode.f = jumpNode.g + jumpNode.h;
jumpNode.parent = node;
if (!jumpNode.opened) {
openList.push(jumpNode);
jumpNode.opened = true;
} else {
openList.updateItem(jumpNode);
}
}
}
}
};
module.exports = JumpPointFinderBase;

File diff suppressed because one or more lines are too long

@ -0,0 +1,68 @@
Веб приложение по планированию маршрутов.
_________________________________________________
Отображение
_________________________________________________
Для отображения элементов интерфейса используется фреймворк devextreme, в частности используются следующие элементы:
- карта
https://js.devexpress.com/jQuery/Demos/WidgetsGallery/Demo/Map/ProvidersAndTypes/MaterialBlueLight/
- кнопка
https://js.devexpress.com/jQuery/Demos/WidgetsGallery/Demo/Button/PredefinedTypes/MaterialBlueLight/
- списки
https://js.devexpress.com/jQuery/Demos/WidgetsGallery/Demo/List/ListSelection/MaterialBlueLight/
- выпадающий список
https://js.devexpress.com/jQuery/Demos/WidgetsGallery/Demo/DropDownButton/Overview/MaterialBlueLight/
Для указанных элементов и сайта используются стили из этого же фреймворка: dx.material.blue.dark.compact.css
Также из сторонних библиотек используется jquery, для обращения к элементам страницы, и создания новых элементов.
Собственные стили находятся в файле css/styles.css.
Данные по точкам доступным для посещения хранятся в файле js/data.js
_________________________________________________
Логика работы
_________________________________________________
Все функции находятся в файле js/index.js
При входе на страницу отобажается карта со всеми доступными для расчета порядка посещения точками.
Пользователь, в первую очередь, выбирает регион посещения, из списка под картой.
При выборе региона будут отображены точки для посещения в данном регионе в виде списка с возможности выбирать элемента,
а карта отобразит только точки выбранного региона.
На следующем шаге пользователь отбирает интересующие точки в списке, при выборе будет формироваться новый список в правой части страницы,
который содержит выбранные элементы.
По окончании отбора точек, пользователь нажимает кнопку Рассчитать маршрут. После чего под кнопкой отобразится найденный маршрут с относительно оптимальной длиной пути.
Маршрут отображается в виде списка точек посещения, между которыми отображается расстояние. В конце списка отображается общая длина маршрута.
_________________________________________________
Алгоритм построения маршрута
_________________________________________________
Для расчета расстояния между точек, используется метод calculateDistance из библиотеки
https://github.com/chrisveness/geodesy
Которая позволяет определить дистанцию между двумя точками заданными координатами в виде широты и долготы.
Для построения оптимального маршрута используется следующий подход:
1. Если точек три или меньше, выводятся в порядке заданном пользователем (старт, промежуточная точка, окончание маршрута)
2. Если точек больше трех, то используется метод stochasticPathFind:
2.1 Вычисляется количество комбинаций точек, как факториал общего количества точек за минусом двух (начало и окончание маршрута)
2.2 Если количество комбинаций превышает допустимое (300.000), то используется максимально допустимое количество комбинаций
2.3 Перебираются возможные комбинации, каждая новая комбинация формируется случайным перемешиванием точек маршрута, кроме начальной и конечной
2.4 Для каждой комбинации вычисляется общая длина пути, если найденная длина меньше предыдущей минимальной, запоминается длина маршрута и комбинация
2.5 Метод возвращает маршрут с минимальной найденной длиной пути.
Для точек количеством менее 10, будут перебраны все доступные комбинации (10 - 2)! < 300.000, для 11 будут перебраны почти все комбинации (11 - 2)! = 362 880
Для большего числа точек будет выбран лучший из перебранных маршрутов, но не лучший из всех возможных.
Данный подход был выбран после попыток использовать построение Гамильтонова пути (https://ru.wikipedia.org/wiki/%D0%93%D0%B0%D0%BC%D0%B8%D0%BB%D1%8C%D1%82%D0%BE%D0%BD%D0%BE%D0%B2_%D0%B3%D1%80%D0%B0%D1%84)
и нескольких других алгоритмов на графах. Попытки использовать данные алгоритмы показали что для большого числа точек (>9), временные затраты на поиск и нагрузка на браузер, процессор и память, превышают разумные границы. Вследствие чего было принято решение использовать простой перебор, и ограничить количество анализируемых маршрутов в 300.000 комбинаций.
Loading…
Cancel
Save

Powered by TurnKey Linux.