Ogoun 5 months ago
parent 70dd714cd7
commit df671f53a8

@ -0,0 +1 @@
https://www.movable-type.co.uk/scripts/latlong.html

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Web Server Health Monitor</title>
<link href="styles/styles.css" rel="stylesheet" />
<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.viz.js"></script>
</head>
<body class="dark">
<div class="layout">
<div class="header-text">Web Server Health Monitoring</div>
<div class="img-theme" data-bind="click:toggleTheme"></div>
<div class="content helth">
<div class="slide-button right-button" data-bind="click:goto"></div>
<div class="row">
<div class="legend-row">
<div class="legend">Requests per second</div>
<div class="hr"></div>
</div>
<div class="clear"></div>
<div class="col1">
<div class="text">View the number of requests completed at the moment and for the selected range within the last day.</div>
<label id="requestNumber" class="label-value" data-bind="text: requestsNumber"></label>
</div>
<div class="col2" data-bind="dxCircularGauge: gaugeRequestsNumberOptions"></div>
<div class="col3" id="RequestChartContainer" data-bind="dxChart: chartRequestsNumberOptions" ></div>
</div>
<div class="row">
<div class="legend-row">
<div class="legend">CPU, %</div>
<div class="hr"></div>
</div>
<div class="clear"></div>
<div class="col1">
<div class="text">View how much CPU is being used at the moment and for the selected range within the last day.</div>
<label id="CPU" class="label-value" data-bind="text:CPU"></label>
</div>
<div class="col2" data-bind="dxCircularGauge: gaugeCPUOptions"></div>
<div class="col3" id="CPUChartContainer" data-bind="dxChart: chartCPUOptions"></div>
</div>
<div class="row">
<div class="legend-row">
<div class="legend">Memory Consumption, Mb</div>
<div class="hr"></div>
</div>
<div class="clear"></div>
<div class="col1">
<div class="text">View how much memory is used at the moment and for the selected range within the last day.</div>
<label id="memoryConsumption" class="label-value" data-bind="text:memoryConsumption"></label>
</div>
<div class="col2" data-bind="dxCircularGauge: gaugeMemoryConsumptionOptions"></div>
<div class="col3" data-bind="dxChart: chartMemoryConsumptionOptions" id="memoryChartContainer"></div>
</div>
<div class="row">
<div class="col1"></div>
<div class="col2"></div>
<div class="col3" id="rangeSelectorContainer"></div>
</div>
</div>
</div>
<script src="js/WebServerMonitor.js"></script>
<script src="js/index.js"></script>
</body>
</html>

@ -0,0 +1,284 @@
"use strict";
(function () {
window.WebServerMonitor = window.WebServerMonitor || {};
window.WebServerMonitor.ViewModel = function (action) {
var themes = ['light', 'dark'],
themeIndex = (function () {
var themeName = window.location.href.match(/[?&]theme=([^&$]*)/);
themeName = themeName && themeName.length > 1 ? themeName[1] : themes[1];
return !Boolean($.inArray(themeName, themes));
})();
action = (action || "traffic") + ".html?theme=";
this.toggleTheme = function (index) {
themeIndex = !themeIndex;
var curTheme = themes[~~(themeIndex)];
$('body').removeClass().addClass(curTheme);
this.applyTheme(curTheme, false);
};
this.goto = function () {
window.location = action + themes[~~themeIndex];
};
this.applyTheme = $.noop;
this.inherit = function (otherModel) {
var darkPalette = ['#46508c', '#556fa6', '#5d8dbc', '#62b7db', '#70cdd6', '#8ccebb'],
lightPalette = ['#737db5', '#7e93bf', '#8bafd2', '#90cce6', '#70cdd6', '#bae3d7'];
DevExpress.viz.registerPalette('Dark Palette', darkPalette);
DevExpress.viz.registerPalette('Light Palette', lightPalette);
$.extend(true, this, otherModel);
this.applyTheme(themes[~~(themeIndex)], true);
};
this.toggleTheme();
};
window.WebServerMonitor.app = {};
(function (app) {
var j,
random = Math.random,
round = Math.round,
tmpArray = [],
countriesList = ['China', 'USA', 'Russia', 'Canada', 'Japan', 'Others'],
months = [],
days = [],
hours = [24],
date = new Date(),
baseDate = new Date(date.getFullYear(), date.getMonth(), date.getDate() - 7, 4, 0);
app.arrayForBarChart = [];
app.arrayForLineChart = [];
app.arrayForStackedBar = [];
var findHoursValue = function () {
var hoursValue = [],
k;
for (k = 0; k < 6; k++) {
hoursValue.push(~~(180 * random()));
}
return hoursValue;
};
var findDayValue = function () {
var dayValue = [],
k;
for (k = 0; k < 6; k++) {
dayValue.push(~~(600 * random() + 200));
}
return dayValue;
};
for (j = 1; j < 7; j++) {
hours[j] = (baseDate.getHours());
baseDate.setHours(baseDate.getHours() + 4);
}
for (j = 0; j < 6; j++) {
months[j] = baseDate.getMonth() + 1;
days[j] = baseDate.getDate();
baseDate.setDate(baseDate.getDate() + 1);
}
tmpArray = $.map(countriesList, function (country) {
return {
country: country,
value: findDayValue(),
days: days,
hours: hours,
months: months,
hoursValue: findHoursValue()
};
});
app.arrayForBarChart = $.map(tmpArray, function (item) {
return {
country: item.country,
value: item.value[tmpArray[0].value.length - 1]
};
});
app.arrayForLineChart = (function () {
var date = new Date(),
lineChartData = [];
date.setDate(date.getDate() + 1);
date = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 0, 0);
lineChartData.push({
hour: date,
y1: 0,
y2: 0,
y3: 0,
y4: 0,
y5: 0,
y6: 0
});
date = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours() + 4, 0);
for (var i = 1; i < 6; i++) {
lineChartData.push({
hour: date,
y1: lineChartData[i - 1].y1 + ~~((app.arrayForBarChart[0].value / 10) + random() * 50),
y2: lineChartData[i - 1].y2 + ~~((app.arrayForBarChart[1].value / 10) + random() * 50),
y3: lineChartData[i - 1].y3 + ~~((app.arrayForBarChart[2].value / 10) + random() * 50),
y4: lineChartData[i - 1].y4 + ~~((app.arrayForBarChart[3].value / 10) + random() * 50),
y5: lineChartData[i - 1].y5 + ~~((app.arrayForBarChart[4].value / 10) + random() * 50),
y6: lineChartData[i - 1].y6 + ~~((app.arrayForBarChart[5].value / 10) + random() * 50)
});
date = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours() + 4, 0);
}
return lineChartData;
})();
app.arrayForStackedBar = (function (arr) {
var data = [],
dataItem,
sum = [0, 0, 0, 0, 0, 0],
i,
j;
for (i = 0; i < 6; i++) {
for (j = 0; j < 6; j++) {
sum[i] += arr[j].value[i];
}
}
for (i = 0; i < 6; i++) {
data[i] = {
day: arr[0].days[i] + '/' + arr[0].months[i],
y1: round((arr[0].value[i] / sum[i]) * 100),
y2: round((arr[1].value[i] / sum[i]) * 100),
y3: round((arr[2].value[i] / sum[i]) * 100),
y4: round((arr[3].value[i] / sum[i]) * 100),
y5: round((arr[4].value[i] / sum[i]) * 100),
y6: 0
};
dataItem = data[i];
dataItem.y6 = 100 - (dataItem.y1 + dataItem.y2 + dataItem.y3 + dataItem.y4 + dataItem.y5);
}
return data;
})(tmpArray);
var findRandomValue = function () {
var randomArray = [],
timeNow = new Date();
timeNow.setDate(timeNow.getDate() - 3);
timeNow.setHours(12);
timeNow.setMinutes(0);
for (var i = 0; i < 37; i++) {
randomArray.push({
x: new Date(timeNow.getFullYear(), timeNow.getMonth(), timeNow.getDate(), timeNow.getHours()),
y1: ~~(Math.random() * 200),
y2: ~~(Math.random() * 100),
y3: ~~(Math.random() * 1000)
});
timeNow.setHours(timeNow.getHours() + 2);
}
return randomArray;
};
app.allSeries = findRandomValue();
app._createGaugeOptions = function (gaugeValue, tickInterval, gaugeRanges, colors) {
var gaugeOptions = {
size: {
width: 210,
height: 175
},
margin: {
left: 10,
right: 10,
top: 10,
bottom: 10
},
containerBackgroundColor: colors.bkgColor,
scale: {
startValue: gaugeRanges[0].startValue,
endValue: gaugeRanges[3].endValue,
label: {
font: {
color: colors.fontColor
},
indentFromTick: 8
},
tick: {
color: 'none'
},
tickInterval: tickInterval
},
rangeContainer: {
width: 3,
ranges: gaugeRanges,
backgroundColor: 'none'
},
value: gaugeValue,
valueIndicator: {
offset: 5,
indentFromCenter: 7,
color: colors.needle
}
};
return gaugeOptions;
};
app._createChartOptions = function (allSeries, maxValue, chartField, chartColor, colors, animation) {
var chartOptions = {
commonAxisSettings: {
visible: false,
tick: {
visible: false
},
grid: {
color: colors.gridColor,
opacity:1
},
label: {
font: {
color: colors.fontColor
}
}
},
margin: {
top: 5,
bottom: 5,
right: 36
},
argumentAxis: {
valueMarginsEnabled: false,
grid: { visible: true }
},
animation: animation,
commonPaneSettings: {
border: {
visible: true,
color: colors.gridColor,
opacity:1
}
},
legend: { visible: false },
dataSource: allSeries,
valueAxis: {
placeholderSize: 60,
valueMarginsEnabled: false,
visualRange: {
startValue: 0
}
},
series: [
{
argumentField: 'x',
valueField: chartField,
type: 'area',
point: { visible: false },
color: chartColor,
style: { opacity: 0.38 }
}]
};
return chartOptions;
};
}(window.WebServerMonitor.app));
}());

@ -0,0 +1,5 @@
/*!
* 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}));

@ -0,0 +1,11 @@
/*!
* CLDR JavaScript Library v0.5.4 2020-10-22T15:56Z MIT license © Rafael Xavier
* http://git.io/h4lmVg
*/
!function(e){"function"==typeof define&&define.amd?define(["../cldr"],e):"object"==typeof module&&"object"==typeof module.exports?module.exports=e(require("../cldr")):e(Cldr)}((function(e){var t,n=e._pathNormalize,r=e._validatePresence,i=e._validateType;
/*!
* EventEmitter v4.2.7 - git.io/ee
* Oliver Caldwell
* MIT license
* @preserve
*/t=function(){function e(){}var t=e.prototype,n={};function r(e,t){for(var n=e.length;n--;)if(e[n].listener===t)return n;return-1}function i(e){return function(){return this[e].apply(this,arguments)}}return t.getListeners=function(e){var t,n,r=this._getEvents();if(e instanceof RegExp)for(n in t={},r)r.hasOwnProperty(n)&&e.test(n)&&(t[n]=r[n]);else t=r[e]||(r[e]=[]);return t},t.flattenListeners=function(e){var t,n=[];for(t=0;t<e.length;t+=1)n.push(e[t].listener);return n},t.getListenersAsObject=function(e){var t,n=this.getListeners(e);return n instanceof Array&&((t={})[e]=n),t||n},t.addListener=function(e,t){var n,i=this.getListenersAsObject(e),s="object"==typeof t;for(n in i)i.hasOwnProperty(n)&&-1===r(i[n],t)&&i[n].push(s?t:{listener:t,once:!1});return this},t.on=i("addListener"),t.addOnceListener=function(e,t){return this.addListener(e,{listener:t,once:!0})},t.once=i("addOnceListener"),t.defineEvent=function(e){return this.getListeners(e),this},t.defineEvents=function(e){for(var t=0;t<e.length;t+=1)this.defineEvent(e[t]);return this},t.removeListener=function(e,t){var n,i,s=this.getListenersAsObject(e);for(i in s)s.hasOwnProperty(i)&&-1!==(n=r(s[i],t))&&s[i].splice(n,1);return this},t.off=i("removeListener"),t.addListeners=function(e,t){return this.manipulateListeners(!1,e,t)},t.removeListeners=function(e,t){return this.manipulateListeners(!0,e,t)},t.manipulateListeners=function(e,t,n){var r,i,s=e?this.removeListener:this.addListener,o=e?this.removeListeners:this.addListeners;if("object"!=typeof t||t instanceof RegExp)for(r=n.length;r--;)s.call(this,t,n[r]);else for(r in t)t.hasOwnProperty(r)&&(i=t[r])&&("function"==typeof i?s.call(this,r,i):o.call(this,r,i));return this},t.removeEvent=function(e){var t,n=typeof e,r=this._getEvents();if("string"===n)delete r[e];else if(e instanceof RegExp)for(t in r)r.hasOwnProperty(t)&&e.test(t)&&delete r[t];else delete this._events;return this},t.removeAllListeners=i("removeEvent"),t.emitEvent=function(e,t){var n,r,i,s=this.getListenersAsObject(e);for(i in s)if(s.hasOwnProperty(i))for(r=s[i].length;r--;)!0===(n=s[i][r]).once&&this.removeListener(e,n.listener),n.listener.apply(this,t||[])===this._getOnceReturnValue()&&this.removeListener(e,n.listener);return this},t.trigger=i("emitEvent"),t.emit=function(e){var t=Array.prototype.slice.call(arguments,1);return this.emitEvent(e,t)},t.setOnceReturnValue=function(e){return this._onceReturnValue=e,this},t._getOnceReturnValue=function(){return!this.hasOwnProperty("_onceReturnValue")||this._onceReturnValue},t._getEvents=function(){return this._events||(this._events={})},e.noConflict=function(){return n.EventEmitter=originalGlobalValue,e},e}();var s,o,u=function(e,t){i(e,t,void 0===e||"function"==typeof e,"Function")},f=new t;function c(e,t){i(e,t,"string"==typeof e||e instanceof RegExp,"String or RegExp")}function a(e,t){return function(n,i){return r(n,"event"),c(n,"event"),r(i,"listener"),u(i,"listener"),t[e].apply(t,arguments)}}function h(e){return a("off",e)}function l(e){return a("on",e)}function p(e){return a("once",e)}function v(){s=e.prototype.get,e.prototype.get=function(e){var t=s.apply(this,arguments);return e=n(e,this.attributes).join("/"),f.trigger("get",[e,t]),this.ee.trigger("get",[e,t]),t}}return e.off=h(f),e.on=l(f),e.once=p(f),o=e.prototype.init,e.prototype.init=function(){var e;this.ee=e=new t,this.off=h(e),this.on=l(e),this.once=p(e),o.apply(this,arguments)},e._eventInit=v,v(),e}));

@ -0,0 +1,5 @@
/*!
* CLDR JavaScript Library v0.5.4 2020-10-22T15:56Z MIT license © Rafael Xavier
* http://git.io/h4lmVg
*/
!function(e){"function"==typeof define&&define.amd?define(["../cldr"],e):"object"==typeof module&&"object"==typeof module.exports?module.exports=e(require("../cldr")):e(Cldr)}((function(e){var t=e._alwaysArray,a=function(e){var a,r;return(r=(a=function(a){return function(r){return r=t(r),e.get([a].concat(r))}})("supplemental")).weekData=a("supplemental/weekData"),r.weekData.firstDay=function(){return e.get("supplemental/weekData/firstDay/{territory}")||e.get("supplemental/weekData/firstDay/001")},r.weekData.minDays=function(){var t=e.get("supplemental/weekData/minDays/{territory}")||e.get("supplemental/weekData/minDays/001");return parseInt(t,10)},r.timeData=a("supplemental/timeData"),r.timeData.allowed=function(){return e.get("supplemental/timeData/{territory}/_allowed")||e.get("supplemental/timeData/001/_allowed")},r.timeData.preferred=function(){return e.get("supplemental/timeData/{territory}/_preferred")||e.get("supplemental/timeData/001/_preferred")},r},r=e.prototype.init;return e.prototype.init=function(){r.apply(this,arguments),this.supplemental=a(this)},e}));

@ -0,0 +1,5 @@
/*!
* CLDR JavaScript Library v0.5.4 2020-10-22T15:56Z MIT license © Rafael Xavier
* http://git.io/h4lmVg
*/
!function(e){"function"==typeof define&&define.amd?define(["../cldr"],e):"object"==typeof module&&"object"==typeof module.exports?module.exports=e(require("../cldr")):e(Cldr)}((function(e){var t,o=e._coreLoad,r=e._jsonMerge,n=e._pathNormalize,a=e._resourceGet,i=e._validatePresence,d=e._validateTypePath,u=t=function(e,o,i,d,u){var l,c,f;if(void 0!==o&&o!==u)return l=n(i,d),void 0!==(f=a(e._resolved,l))&&"object"!=typeof f||(void 0===(f=a(e._raw,l))&&(c=function(e,t){var o,r;if("root"!==t)return o=n(["supplemental/parentLocales/parentLocale",t]),(r=a(e._resolved,o)||a(e._raw,o))?r:(r=t.substr(0,t.lastIndexOf(e.localeSep)))||"root"}(e,o),f=t(e,c,i,r(d,{bundle:c}),o)),void 0!==f&&function(e,t,o){var r,n=e,a=t.length;for(r=0;r<a-1;r++)n[t[r]]||(n[t[r]]={}),n=n[t[r]];n[t[r]]=o}(e._resolved,l,f)),f};return e._raw={},e.load=function(){e._raw=o(e,e._raw,arguments)},e.prototype.get=function(t){return i(t,"path"),d(t,"path"),u(e,this.attributes&&this.attributes.bundle||"",t,this.attributes)},e._eventInit&&e._eventInit(),e}));

File diff suppressed because one or more lines are too long

@ -0,0 +1,353 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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;

@ -0,0 +1,402 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,148 @@
/* 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
*/

@ -0,0 +1,533 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,331 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,429 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,445 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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

@ -0,0 +1,865 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,305 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,348 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,379 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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 };

@ -0,0 +1,256 @@
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* 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;

@ -0,0 +1,5 @@
/*!
* 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,_}));

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,219 @@
"use strict";
(function(){
var view = new window.WebServerMonitor.ViewModel(),
requestChart,
cpuChart,
memoryChart;
view.inherit(function(){
var gaugesPalette,
chartsPalette,
app = window.WebServerMonitor.app,
colors,
fonts,
allSeries = app.allSeries;
return {
applyTheme: function (theme, animation) {
if (theme === 'dark') {
gaugesPalette = ['#7cd2c7', '#f9d191', '#f9d191', '#fd7888', '#8b98c2'];
chartsPalette = ['#58ffe8', '#5eceff', '#93a9ff'];
colors = {
fontColor:'#a7acbc',
gridColor:'#515873',
bkgColor: '#363E5B',
needle: '#ffffff',
shutter: {
color: '#363e5b',
opacity: 0.65
}
};
fonts = {
sliderMarker: {
color: '#43474b',
size: 11,
weight: 400
}
};
} else {
gaugesPalette = ['#76c8bd', '#f7c676', '#f7c676', '#c5819a', '#96a3d4'];
chartsPalette = ['#76c8bd', '#75c0e0', '#c5cce7'];
colors = {
fontColor: '#7f7f7f',
gridColor: '#e9e9e9',
bkgColor: '#ffffff',
needle: '#43474b',
shutter: {
color: 'white',
opacity: 0.65
}
};
fonts = {
sliderMarker: {
color: 'white',
size: 11,
weight: 400
}
};
}
this.gaugeRequestsNumberOptions(app._createGaugeOptions(allSeries[36].y1,
50,
[{
startValue: 0,
endValue: 48,
color: gaugesPalette[0]
}, {
startValue: 52,
endValue: 98,
color: gaugesPalette[1]
}, {
startValue: 102,
endValue: 148,
color: gaugesPalette[2]
}, {
startValue: 152,
endValue: 200,
color: gaugesPalette[3]
}],
colors));
this.gaugeCPUOptions(app._createGaugeOptions(allSeries[36].y2,
25,
[{
startValue: 0,
endValue: 24,
color: gaugesPalette[0]
}, {
startValue: 26,
endValue: 49,
color: gaugesPalette[1]
}, {
startValue: 51,
endValue: 74,
color: gaugesPalette[2]
}, {
startValue: 76,
endValue: 100,
color: gaugesPalette[3]
}],
colors));
this.gaugeMemoryConsumptionOptions(app._createGaugeOptions(allSeries[36].y3,
250,
[{
startValue: 0,
endValue: 240,
color: gaugesPalette[4]
}, {
startValue: 260,
endValue: 490,
color: gaugesPalette[4]
}, {
startValue: 510,
endValue: 740,
color: gaugesPalette[4]
}, {
startValue: 760,
endValue: 1000,
color: gaugesPalette[4]
}],
colors));
this.chartRequestsNumberOptions(app._createChartOptions(allSeries, 200, 'y1', chartsPalette[0], colors, animation));
this.chartCPUOptions(app._createChartOptions(allSeries, 100, 'y2', chartsPalette[1], colors, animation));
this.chartMemoryConsumptionOptions(app._createChartOptions(allSeries, 1000, 'y3', chartsPalette[2], colors, animation));
$('#rangeSelectorContainer').empty();
$('#rangeSelectorContainer').removeData();
$('#rangeSelectorContainer').dxRangeSelector({
containerBackgroundColor: colors.bkgColor,
margin: {
bottom: 0,
left: 0,
top: 0,
right: 0
},
minorTick: {
visible: true
},
tickInterval: {
hours: 12
},
scale: {
minorTickInterval: { hours: 4 },
minRange: 'hour',
tick: {
color: colors.gridColor,
opacity: 1
},
label: {
font: {
color: colors.fontColor
}
}
},
dataSource: allSeries,
behavior: {
callValueChanged: 'onMoving',
animationEnabled: false
},
indent: {
left: 60,
right: 60
},
sliderMarker: {
format: 'hour',
font: fonts.sliderMarker,
color: colors.needle
},
shutter: colors.shutter,
onValueChanged: function(e) {
zoomChart(requestChart, e);
zoomChart(cpuChart, e);
zoomChart(memoryChart, e);
},
chart: {
palette: chartsPalette,
commonSeriesSettings: {
type: 'area',
argumentField: 'x'
},
topIndent: 0,
bottomIndent: 0,
valueAxis: {
visualRange: {
startValue: 0
}
},
series: [{
valueField: 'y1'
}, {
valueField: 'y2'
}, {
valueField: 'y3'
}]
}
});
},
requestsNumber: allSeries[36].y1,
CPU: allSeries[36].y2,
memoryConsumption: allSeries[36].y3,
gaugeRequestsNumberOptions: ko.observable({}),
gaugeCPUOptions: ko.observable({}),
gaugeMemoryConsumptionOptions: ko.observable({}),
chartRequestsNumberOptions: ko.observable({}),
chartCPUOptions: ko.observable({}),
chartMemoryConsumptionOptions: ko.observable({})
};
}());
ko.applyBindings(view);
requestChart = $('#RequestChartContainer').dxChart('instance');
cpuChart = $('#CPUChartContainer').dxChart('instance');
memoryChart = $('#memoryChartContainer').dxChart('instance');
function zoomChart(chart, args) {
clearTimeout(chart.zoomTimeout);
chart.zoomTimeout = setTimeout(function () {
chart.getArgumentAxis().visualRange(args.value);
}, 30);
}
}());

10993
src/js/jquery.js vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

@ -0,0 +1,139 @@
/*!
* 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)})()})})();})();

@ -0,0 +1,249 @@
"use strict";
(function () {
var view = new window.WebServerMonitor.ViewModel("index");
view.inherit(function () {
var palette,
fontColor,
gridColor,
bkgColor,
app = window.WebServerMonitor.app,
labelTextColor,
labelColor;
return {
applyTheme: function (theme, animation) {
if (theme == 'dark') {
palette = 'Dark Palette';
fontColor = '#a7acbc';
gridColor = '#515873';
bkgColor = '#363E5B';
labelTextColor = '#363e5b';
labelColor = 'white';
} else {
palette = 'Light Palette';
fontColor = '#7f7f7f';
gridColor = '#e9e9e9';
bkgColor = '#ffffff';
labelTextColor = 'white';
labelColor = '#43474b';
}
this.pieChartOptions({
palette: palette,
size: {
height: 270
},
margin: {
top: 30
},
legend: {
visible: false
},
tooltip: {
enabled: true,
customizeText: function () {
return this.argumentText + '<br/>' + this.percentText;
}
},
animation: animation,
dataSource: app.arrayForBarChart,
series: {
border: {
color: bkgColor,
width: 2,
visible: true
},
hoverStyle: {
border: {
color: bkgColor,
width: 2,
visible: true
}
},
argumentField: 'country',
valueField: 'value'
}
});
this.barChartOptions({
commonAxisSettings: {
visible: false,
tick: {
visible: false
},
grid: {
color: gridColor,
opacity:1
},
label: {
font: {
color: fontColor
}
}
},
animation: animation,
margin: {
right: 177
},
commonSeriesSettings: {
type: 'bar'
},
valueAxis: {
visualRange: {
startValue: 0,
endValue: 1000
}
},
legend: { visible: false },
dataSource: app.arrayForBarChart,
series: [{
label: {
visible: true,
backgroundColor: labelColor,
font: {
color: labelTextColor,
size:11
}
},
argumentField: 'country',
valueField: 'value',
color: DevExpress.viz.getPalette(palette).simpleSet[2]
}]
});
this.stackedBarChartOptions({
palette: palette,
commonAxisSettings: {
visible: false,
tick: {
visible: false
},
grid: {
color: gridColor,
opacity: 1,
},
label: {
font: {
color: fontColor
}
},
opacity: 1
},
animation: animation,
commonSeriesSettings: {
argumentField: 'day',
type: 'stackedBar'
},
valueAxis: {
inverted: true,
label: {
customizeText: function () {
return 100 - this.value;
}
}
},
legend: {
margin: 30,
rowItemSpacing: 10,
markerSize: 20,
font: {
color:fontColor
}
},
margin: {
top: 16,
right: 41
},
dataSource: app.arrayForStackedBar,
series: [{
valueField: 'y1',
name: 'China'
}, {
valueField: 'y2',
name: 'USA'
}, {
valueField: 'y3',
name: 'Russia'
}, {
valueField: 'y4',
name: 'Canada'
}, {
valueField: 'y5',
name: 'Japan'
}, {
valueField: 'y6',
name: 'Others'
}]
});
this.lineChartOptions({
palette: palette,
commonAxisSettings: {
visible: false,
tick: {
visible: false
},
valueMarginsEnabled: false,
grid: {
color: gridColor,
visible: true,
opacity: 1
},
label: {
font: {
color: fontColor
}
},
opacity: 1
},
animation: animation,
size: {
width: 400
},
margin: {
top: 16
},
commonSeriesSettings: {
argumentField: 'hour',
type: 'spline',
point: { visible: false }
},
commonPaneSettings: {
border: {
visible: true,
color: gridColor,
opacity:1
}
},
argumentAxis: {
tickInterval: {
hours: 4
}
},
valueAxis: {
visualRange: {
startValue: 0,
endValue: 500
}
},
legend: { visible: false },
dataSource: app.arrayForLineChart,
series: [{
valueField: 'y1'
}, {
valueField: 'y2'
}, {
valueField: 'y3'
}, {
valueField: 'y4'
}, {
valueField: 'y5'
}, {
valueField: 'y6'
}]
});
},
pieChartOptions: ko.observable({}),
barChartOptions: ko.observable({}),
stackedBarChartOptions: ko.observable({}),
lineChartOptions: ko.observable({})
};
}());
ko.applyBindings(view);
}());

@ -0,0 +1,349 @@
{
"regions": [
{
"name": "Алтай",
"points": [
{
"name": "Гора Белуха",
"description": "Высочайшая вершина Горного Алтая, с потрясающим видом на окружающие горы и ледники.",
"gps": "50.7144, 86.5861"
},
{
"name": "Озеро Кучерлинское",
"description": "Кристально чистое озеро, окруженное высокими горами, особенно красиво на закате.",
"gps": "50.5854, 86.1704"
},
{
"name": "Гора Актру",
"description": "Панорамный вид на Актруский хребет, луга и леса.",
"gps": "50.1999, 86.0329"
},
{
"name": "Каньон Чулышман",
"description": "Глубокий каньон, вырезанный рекой Чулышман, с дикой и живописной красотой.",
"gps": "50.7004, 87.3289"
},
{
"name": "Гора Горелый Камень",
"description": "Вид на озеро Телецкое и горы.",
"gps": "51.6088, 87.5417"
},
{
"name": "Озеро Телецкое",
"description": "Крупнейшее озеро Алтая с кристально чистой водой и окружающими горами.",
"gps": "51.7683, 87.7138"
},
{
"name": "Гора Чертов Щит",
"description": "Панорамный вид на озеро Кучерлинское и горы.",
"gps": "50.4394, 86.1293"
},
{
"name": "Озеро Аккем",
"description": "Прекрасное озеро в Аккемской долине с отражением гор.",
"gps": "50.2185, 86.6175"
},
{
"name": "Гора Курманджи",
"description": "Вид на сосновые леса и окружающие горы.",
"gps": "50.1061, 85.8888"
},
{
"name": "Озеро Синецкое",
"description": "Озеро с ярко-синей водой, окруженное горами.",
"gps": "51.0296, 85.6982"
},
{
"name": "Гора Казырчик",
"description": "Панорамный вид на долину реки Чулышман.",
"gps": "50.5366, 87.1453"
},
{
"name": "Озеро Шавлинское",
"description": "Красивое озеро с темной водой и окружающей природой.",
"gps": "50.2278, 87.4682"
},
{
"name": "Гора Монхон",
"description": "Вид на озеро Киделю и горы Северо-Чуйский хребет.",
"gps": "50.3131, 87.4837"
},
{
"name": "Озеро Киделя",
"description": "Озеро с прозрачной водой и окружающими горами.",
"gps": "49.9272, 87.2483"
},
{
"name": "Гора Алтын-Ту",
"description": "Панорамный вид на долину Аккема и сосновые леса.",
"gps": "50.1532, 86.8409"
},
{
"name": "Озеро Большое Мультинское",
"description": "Красивое озеро в окружении гор.",
"gps": "50.3530, 86.3885"
},
{
"name": "Гора Кузнецкий Алатау",
"description": "Вид на луга и окружающие горы.",
"gps": "53.3458, 87.0526"
},
{
"name": "Озеро Белое",
"description": "Озеро с кристально чистой водой и отражением гор.",
"gps": "51.2346, 85.7923"
},
{
"name": "Гора Томичка",
"description": "Панорамный вид на долину реки Катунь и окружающие хребты.",
"gps": "51.8150, 85.9910"
},
{
"name": "Озеро Мультинское",
"description": "Прекрасное озеро в Аккемской долине с видом на горы.",
"gps": "50.3276, 86.4315"
},
{
"name": "Гора Большой Уштас",
"description": "Вид на реку Катунь и окружающие хребты.",
"gps": "52.6889, 85.8830"
},
{
"name": "Озеро Сарыколь",
"description": "Озеро с ярко-голубой водой, окруженное горами и лугами.",
"gps": "50.4194, 87.1858"
},
{
"name": "Гора Семенов-Тянь-Шань",
"description": "Панорамный вид на Алтайские горы и бескрайние просторы.",
"gps": "50.3056, 87.5547"
},
{
"name": "Озеро Кара-Кёль",
"description": "Озеро с темной водой, окруженное скалами и лесами.",
"gps": "50.4153, 86.0706"
},
{
"name": "Гора Крылатая",
"description": "Вид на долину реки Чулышман и окружающие хребты.",
"gps": "50.2278, 87.4682"
},
{
"name": "Гора Большой Турчан",
"description": "Панорамный вид на озеро Аккем и сосновые леса.",
"gps": "50.1925, 86.6286"
},
{
"name": "Озеро Долгое",
"description": "Красивое озеро в окружении гор и лугов.",
"gps": "50.3078, 86.0633"
},
{
"name": "Гора Катуньский Хребет",
"description": "Вид на долину реки Катунь и сосновые леса.",
"gps": "51.0254, 86.2282"
},
{
"name": "Озеро Аккуль",
"description": "Озеро с яркой зеленоватой водой и песчаными пляжами.",
"gps": "50.4500, 86.4458"
},
{
"name": "Гора Большой Сумоно",
"description": "Панорамный вид на Алтайские горы и озеро Телецкое.",
"gps": "51.2366, 87.7101"
}
]
},
{
"name": "Кавказ",
"points": [
{
"name": "Эльбрус",
"description": "Самая высокая гора в Европе, предлагает потрясающий вид на окружающие горные вершины и долины.",
"gps": "43.3500, 42.4500"
},
{
"name": "Красная Поляна",
"description": "Популярный горнолыжный курорт с великолепными пейзажами и множеством развлечений.",
"gps": "43.6814, 40.2326"
},
{
"name": "Домбай",
"description": "Природный парк с высокими горами, каскадами водопадов и живописными лугами.",
"gps": "43.2557, 40.3121"
},
{
"name": "Гора Чегет",
"description": "Популярная туристическая база с видом на Эльбрус и окружающие горные вершины.",
"gps": "43.2353, 42.6099"
},
{
"name": "Тебердинский заповедник",
"description": "Заповедник с уникальной флорой и фауной, где можно увидеть различные виды растений и животных.",
"gps": "43.4487, 41.7627"
},
{
"name": "Озеро Рица",
"description": "Кристально чистое озеро с прекрасными горными пейзажами и возможностями для рыбной ловли и отдыха.",
"gps": "43.1939, 42.7036"
},
{
"name": "Водопады Абхазии",
"description": "Серия живописных водопадов, включая Водопады Агуа, Водопады Мамзырпсы и другие.",
"gps": "43.3339, 40.0906"
},
{
"name": "Гора Фишт",
"description": "Вторая по высоте вершина Кавказа, предлагает потрясающий вид на горные хребты и долины.",
"gps": "43.3519, 40.2311"
},
{
"name": "Гора Казбек",
"description": "Одна из высочайших вершин Кавказа с уникальной формой и потрясающими видами.",
"gps": "42.7034, 44.5071"
},
{
"name": "Скала Столбы",
"description": "Несколько высоких скал, известных своими уникальными формами и панорамными видами.",
"gps": "43.5200, 40.1433"
},
{
"name": "Гора Шхара",
"description": "Одна из самых высоких вершин Главного Кавказского хребта, с потрясающими видами и возможностями для альпинизма.",
"gps": "42.6804, 43.3585"
},
{
"name": "Ачигварские водопады",
"description": "Серия каскадных водопадов в красивой горной местности.",
"gps": "42.6941, 42.8152"
},
{
"name": "Гора Мусса-Ачитара",
"description": "Величественная гора с прекрасными пейзажами и интересными маршрутами для пеших прогулок.",
"gps": "43.4086, 42.5462"
},
{
"name": "Гора Оштен",
"description": "Уникальная скальная формация с живописными видами на окружающие горы и долины.",
"gps": "43.1366, 40.7408"
},
{
"name": "Гора Пшиш",
"description": "Известная вершина, предлагающая потрясающий вид на Северный Кавказ.",
"gps": "42.7320, 43.4290"
},
{
"name": "Хребет Каменка",
"description": "Панорамные виды на горные луга и долины, отличные места для пеших прогулок и фотографии.",
"gps": "42.5844, 41.0029"
},
{
"name": "Озеро Гиагин",
"description": "Красивое горное озеро с яркой синей водой, окруженное высокими скалами.",
"gps": "42.9697, 43.5361"
},
{
"name": "Архыз",
"description": "Живописное горное поселение с изумительными видами и возможностями для туризма и активного отдыха.",
"gps": "43.5562, 41.4452"
}
]
},
{
"name": "Сахалин",
"points": [
{
"name": "Гора Аникеевка",
"description": "Высокая вершина с прекрасным видом на остров и Татарский пролив.",
"gps": "46.6273, 142.5289"
},
{
"name": "Озеро Тунайча",
"description": "Красивое озеро с чистой водой и окружающими лесами.",
"gps": "46.4658, 142.7892"
},
{
"name": "Берег Японского моря",
"description": "Прекрасные пляжи и виды на Японское море, идеальное место для отдыха и наблюдения за закатами.",
"gps": "42.812166575, 133.722529264"
},
{
"name": "Плато Ливадийский",
"description": "Живописное плато с уникальной флорой и фауной, идеальное место для прогулок и пикников.",
"gps": "46.7391, 142.8303"
},
{
"name": "Остров Русский",
"description": "Исторический и культурный центр с прекрасными пейзажами и множеством достопримечательностей.",
"gps": "46.9056, 142.7207"
},
{
"name": "Гора Вулканчик",
"description": "Небольшая активная гора с возможностью подъема на вершину и видом на окружающие пейзажи.",
"gps": "46.9346, 142.7462"
},
{
"name": "Остров Моржовец",
"description": "Место, где можно увидеть моржей и других морских животных, а также насладиться красивыми пейзажами.",
"gps": "47.2903, 142.6831"
},
{
"name": "Бухта Александра",
"description": "Живописная бухта с прекрасными видами на море и окружающие горы.",
"gps": "46.9408, 142.7474"
},
{
"name": "Озеро Большой Иткуль",
"description": "Крупное озеро с чистой водой, окруженное густыми лесами и горами.",
"gps": "46.7767, 142.3050"
},
{
"name": "Мыс Столбчатый",
"description": "Уникальный каменный обрыв на берегу Татарского пролива, предлагающий потрясающие виды на море и окружающие горы.",
"gps": "46.8624, 142.5412"
},
{
"name": "Гора Семушки",
"description": "Высокая вершина с великолепным видом на окружающие горы и долины.",
"gps": "46.8024, 142.7473"
},
{
"name": "Озеро Горное",
"description": "Красивое горное озеро с яркой водой и живописными пейзажами.",
"gps": "47.0456, 142.0719"
},
{
"name": "Парк Зеленый остров",
"description": "Очаровательный парк с живописными аллеями, озерами и растительностью.",
"gps": "46.9573, 142.7362"
},
{
"name": "Мыс Патрокль",
"description": "Прекрасный природный заповедник с крутыми скалами и бурной морской стихией.",
"gps": "46.6403, 141.8701"
},
{
"name": "Остров Троицкий",
"description": "Маленький остров с белыми песчаными пляжами и кристально чистой водой.",
"gps": "46.8117, 142.5963"
},
{
"name": "Вулкан Райкузи",
"description": "Активный вулкан с живописным кратером и великолепными видами на природные ландшафты.",
"gps": "47.1120, 142.8137"
},
{
"name": "Полярный круг",
"description": "Место, где можно пересечь Полярный круг и увидеть уникальные явления природы в зимний период.",
"gps": "66.5608, 164.0014"
},
{
"name": "Остров Матуа",
"description": "Заповедный остров с уникальной природой и историческими достопримечательностями.",
"gps": "48.9817, 153.5418"
}
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -0,0 +1,258 @@
body
{
position: relative;
background-color: gray;
margin: 0;
}
.layout
{
width: 1270px;
font-family: 'Segoe UI', Helvetica, 'Droid Sans', Tahoma, Geneva, sans-serif;
font-weight: 400;
margin: 0 auto;
}
.dark
{
background-color: #363e5b;
}
.light
{
background-color: white;
}
.header-text
{
display: inline-block;
font-size: 30px;
font-family: 'Segoe UI Light', 'Helvetica Neue Light', 'Segoe UI', 'Helvetica Neue', Helvetica, 'Trebuchet MS', 'Droid Sans', Tahoma, Geneva, sans-serif;
font-weight: 200;
margin-left: 100px;
padding: 60px 0 0 0;
}
.dark .header-text
{
color: white;
}
.light .header-text
{
color: #43474b;
}
.img-theme
{
float: right;
width: 60px;
height: 50px;
margin-right: 100px;
margin-top: 60px;
z-index: 101;
background-image: url(sprite.png);
background-position: -90px 0;
cursor: pointer;
}
.light .img-theme
{
background-position: 0 0;
}
.content
{
width: 1070px;
height: 750px;
padding: 40px 100px;
}
.slide-button
{
position: absolute;
top: 430px;
background-image: url(sprite.png);
width: 40px;
height: 80px;
cursor: pointer;
}
.slide-button.right-button
{
right: 0;
background-position: -90px -55px;
}
.light .slide-button.right-button
{
background-position: 0px -55px;
}
.slide-button.left-button
{
left: 0;
background-position: -135px -55px;
}
.light .slide-button.left-button
{
background-position: -45px -55px;
}
.row
{
width: 1070px;
height: 180px;
margin-top: 10px;
overflow: hidden;
}
.traffic .row
{
height: 340px;
}
.legend-row
{
position: relative;
height: 17px;
width: 1500px;
}
.row::after .legend-row::after
{
content: ' ';
display: block;
height: 0;
clear: both;
visibility: hidden;
}
.legend
{
font-family: 'Segoe UI', Helvetica, 'Droid Sans', Tahoma, Geneva, sans-serif;
font-weight: 400;
font-size: 15px;
margin-right: 3px;
z-index: 10;
float: left;
color: white;
}
.light .legend
{
color: #43474b;
}
.hr
{
width: 1100px;
height: 10px;
border-bottom: 1px solid #6b7289;
float: left;
}
.light .hr
{
border-bottom: 1px solid #c0c0c0;
}
.clear
{
clear: both;
}
.helth .row .col1
{
width: 210px;
height: 170px;
float: left;
}
.traffic .row .col1
{
float: left;
width: 420px;
height: 320px;
overflow: hidden;
}
.helth .row .col2
{
width: 210px;
height: 175px;
float: left;
margin: -10px 0 0 0;
}
.traffic .row .col2
{
width: 600px;
height: 320px;
float: right;
overflow: hidden;
}
.helth .row .col3
{
height: 165px;
float: left;
width: 650px;
}
.col1 .text
{
-ms-word-wrap: normal;
word-wrap: normal;
font-size: 11px;
color: #8e93a7;
width: 200px;
margin-top: 20px;
}
.col1 .label-value
{
font-family: 'Segoe UI Semibold', 'Helvetica Neue Medium', 'Segoe UI', 'Helvetica Neue', Helvetica, 'Droid Sans', Tahoma, Geneva, sans-serif;
font-size: 60px;
line-height: 60px;
font-weight: 600;
}
label#requestNumber
{
color: #7cd2c7;
}
.light label#requestsNumber
{
color: #76c8bd;
}
label#CPU
{
color: #75c0e0;
}
label#memoryConsumption
{
color: #7e8ab6;
}
.light label#memoryConsumption
{
color: #96a3d4;
}
.traffic .chart-container
{
width: inherit;
height: 300px;
}
.traffic .chartContainer
{
width: inherit;
height: 300px;
}
Loading…
Cancel
Save

Powered by TurnKey Linux.