gnss_solutions
Version:
Javascript GNSS solution analysis library
1,187 lines (1,062 loc) • 53 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.OnlineSolutionTable = exports.BatchSolutionTable = exports.SolutionTable = exports.SolutionStatusEvent = exports.MissingDataException = exports.IndexOutOfBoundsExceptions = exports.UnitializedVariableException = undefined;
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
exports.getRandomStaticSoln = getRandomStaticSoln;
var _accumulators = require("./accumulators");
var _coords = require("./coords");
var _time = require("./time");
var _constants = require("./constants");
var sym = _interopRequireWildcard(_constants);
var _errors = require("./errors");
var err = _interopRequireWildcard(_errors);
var _lodash = require("lodash");
var _ = _interopRequireWildcard(_lodash);
var _immutable = require("immutable");
var Immutable = _interopRequireWildcard(_immutable);
var _pondjs = require("pondjs");
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /*
* Copyright (c) 2016 Swift Navigation Inc.
* Contact: engineering@swiftnav.com
*
* This source is subject to the license found in the file 'LICENSE' which must
* be be distributed together with this source. All other rights reserved.
*
* THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND,
* EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
*/
// An approximate threshold (milliseconds), used by Piksi's firmware to
// time-match observations.
var TIME_MATCH_THRESHOLD_MSEC = 50;
var MSEC_IN_SEC = 1000; // Dimensionless count
var UnitializedVariableException = exports.UnitializedVariableException = function (_err$CustomException) {
_inherits(UnitializedVariableException, _err$CustomException);
function UnitializedVariableException() {
_classCallCheck(this, UnitializedVariableException);
return _possibleConstructorReturn(this, (UnitializedVariableException.__proto__ || Object.getPrototypeOf(UnitializedVariableException)).call(this, "Variable needs to be initialized!"));
}
return UnitializedVariableException;
}(err.CustomException);
var IndexOutOfBoundsExceptions = exports.IndexOutOfBoundsExceptions = function (_err$CustomException2) {
_inherits(IndexOutOfBoundsExceptions, _err$CustomException2);
function IndexOutOfBoundsExceptions() {
_classCallCheck(this, IndexOutOfBoundsExceptions);
return _possibleConstructorReturn(this, (IndexOutOfBoundsExceptions.__proto__ || Object.getPrototypeOf(IndexOutOfBoundsExceptions)).call(this, "Index out of bounds"));
}
return IndexOutOfBoundsExceptions;
}(err.CustomException);
var MissingDataException = exports.MissingDataException = function (_err$CustomException3) {
_inherits(MissingDataException, _err$CustomException3);
function MissingDataException() {
_classCallCheck(this, MissingDataException);
return _possibleConstructorReturn(this, (MissingDataException.__proto__ || Object.getPrototypeOf(MissingDataException)).call(this, "Looks like you're missing data, dude!"));
}
return MissingDataException;
}(err.CustomException);
/**
* A solution status event wraps a SolutionStatus representation of a position
* solution from a sample GNSS receiver, and additionally keeps track of a
* measured error against a truth reference if available at the same time. It
* represents a sample solution event at a particular instance of time, and
* stores a flat map for a particular instance in time, using the result schema
* defined by Swift's gnss-testing repository:
* https://github.com/swift-nav/gnss-testing/blob/master/DESIGN.md#swift-solution-csv
*
* Example:
* ```
* import { ECEFSolution, SolutionStatus } from "./coords";
* const time = new Date('2016-06-05');
* const ecef = new coords.ECEFSolution(ctest.earthA, 0, 0);
* const soln = new coords.SolutionStatus(time, ecef.toLLA(), 0, 3, 0.5);
* const event = new tab.SolutionStatusEvent(soln);
* console.log(event.toJSON()); =>
* {"epoch(gpst)": new Date("2016-06-05T00:00:00.000Z"),
* "fix_mode": "integer_rtk",
* "latency(sec)": 0.5,
* "num_sats": 3,
* "abs_error_2d(m)": NaN,
* "abs_error_3d(m)": NaN,
* "abs_error_v(m)": NaN,
* "est_error_2d(m)": NaN,
* "est_error_3d(m)": NaN,
* "est_error_v(m)": NaN,
* "baseline_x(m)": NaN,
* "baseline_y(m)": NaN,
* "baseline_z(m)": NaN,
* "rover_pos_x(m)":6378137,
* "rover_pos_y(m)":0,
* "rover_pos_z(m)":0,
* "rover_pos_lat(deg)":0,
* "rover_pos_lon(deg)":0,
* "rover_pos_height(m)":0}
* ```
*/
var SolutionStatusEvent = exports.SolutionStatusEvent = function (_Event) {
_inherits(SolutionStatusEvent, _Event);
/**
* Construct a SolutionStatusEvent.
*
* @param {SolutionStatus} soln - A sample solution
* @param {number} absError3d - Optional, measured spherical error (m)
* @param {number} absErrorH - Optional, measured horizontal error (m)
* @param {Number} absErrorV - Optional, measured vertical error (m)
* @return {SolutionStatusEvent} The constructed solution status event.
*/
function SolutionStatusEvent(soln) {
var absError3d = arguments.length <= 1 || arguments[1] === undefined ? NaN : arguments[1];
var _Object$assign;
var absErrorH = arguments.length <= 2 || arguments[2] === undefined ? NaN : arguments[2];
var absErrorV = arguments.length <= 3 || arguments[3] === undefined ? NaN : arguments[3];
_classCallCheck(this, SolutionStatusEvent);
var _this4 = _possibleConstructorReturn(this, (SolutionStatusEvent.__proto__ || Object.getPrototypeOf(SolutionStatusEvent)).call(this, soln.time, Object.assign((_Object$assign = {}, _defineProperty(_Object$assign, sym.MEAS_H_ERR, absErrorH), _defineProperty(_Object$assign, sym.MEAS_V_ERR, absErrorV), _defineProperty(_Object$assign, sym.MEAS_SPH_ERR, absError3d), _Object$assign), soln.toJSON(), {})));
_this4.soln = soln;
_this4.absError3d = absError3d;
_this4.absErrorH = absErrorH;
_this4.absErrorV = absErrorV;
return _this4;
}
/**
* Checks to see if a solution is (approximately) at the same time.
*
* @param {SolutionStatus} ref - Another position solution
* @return {boolean}
*/
_createClass(SolutionStatusEvent, [{
key: "sameTime",
value: function sameTime(ref) {
var diff = Math.abs(this.timestamp() - ref.time);
return diff <= TIME_MATCH_THRESHOLD_MSEC;
}
/**
* Compare this solution status event a reference position.
*
* @param {SolutionStatus} ref - The reference position.
* @return {SolutionStatusEvent}
*/
}, {
key: "compareTo",
value: function compareTo(ref) {
if (!this.sameTime(ref)) {
throw new err.InvalidArgumentException("Invalid time!");
}
var _soln$compareTo = this.soln.compareTo(ref);
var _soln$compareTo2 = _slicedToArray(_soln$compareTo, 3);
this.absError3d = _soln$compareTo2[0];
this.absErrorH = _soln$compareTo2[1];
this.absErrorV = _soln$compareTo2[2];
return this;
}
/**
* Get a JSON-string presentation of this solution event.
*
* @return {string}
*/
}, {
key: "toString",
value: function toString() {
return JSON.stringify(this.toJSON());
}
/**
* Get a JSON presentation of this solution event, using the gnss-testing
* solution schema.
*
* @return {Object}
*/
}, {
key: "toJSON",
value: function toJSON() {
var _Object$assign2;
return Object.assign((_Object$assign2 = {}, _defineProperty(_Object$assign2, sym.MEAS_H_ERR, this.absErrorH), _defineProperty(_Object$assign2, sym.MEAS_V_ERR, this.absErrorV), _defineProperty(_Object$assign2, sym.MEAS_SPH_ERR, this.absError3d), _Object$assign2), this.soln.toJSON(), {});
}
/**
* Helper method for constructing a SolutionStatusEvent from a pond.js
* Event. A timeseries queried at a particular time returns an Event, from
* which we have to reconstruct the subclassed SolutionStatusEvent.
*
* @param {Event} event - Plain Old Event
* @return {SolutionStatusEvent}
*/
}], [{
key: "fromEvent",
value: function fromEvent(event) {
// TODO: Replace with something better
var data = event.toJSON()['data'];
var status = _coords.SolutionStatus.fromJSON(data);
return new SolutionStatusEvent(status, data[sym.MEAS_SPH_ERR], data[sym.MEAS_H_ERR], data[sym.MEAS_V_ERR]);
}
}]);
return SolutionStatusEvent;
}(_pondjs.Event);
/**
* Generate a random static solution hovering around a point.
*
* @param {ECEFSolution} sol - Center point in ECEF
* @param {Date} t - Time for solution
* @return {SolutionStatusEvent} - Random static solution
*/
function getRandomStaticSoln(sol, t) {
var amplitude = 500;
var offset = 500;
var freq = 10000000;
var base = Math.sin(t.getTime() / freq) * amplitude + offset;
var factor = base + Math.random() * 1000;
var ecef = new _coords.ECEFSolution(sol.x + factor, sol.y + factor, sol.z + factor);
var randFixMode = sym.FIX_MODES[Math.floor(Math.random() * sym.FIX_MODES.length)];
var numSats = 12;
var randNumSats = _.range(numSats)[Math.floor(Math.random() * numSats)];
var randLatency = Math.random();
var ecefBaseline = new _coords.ECEFBaseline(factor, factor, factor);
var estHError = Math.abs(Math.random());
var estVError = Math.abs(Math.random());
var estSphError = Math.abs(Math.random());
return new _coords.SolutionStatus(t, ecef, ecef.toLLA(), randFixMode, randNumSats, randLatency, ecefBaseline, estHError, estVError, estSphError);
}
/**
* A solution table is an abstract class used to store solution events. It is
* intended to be subclassed to have different kinds of implementations.
*
* @param {string} name - Name of the table
* @param {boolean} warnOnOOO - Warn on out-of-order updates.
*/
var SolutionTable = exports.SolutionTable = function () {
function SolutionTable(name) {
var warnOnOOO = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1];
_classCallCheck(this, SolutionTable);
this.name = name;
this.index = NaN;
this.warnOnOOO = warnOnOOO;
}
/**
* Get most recent element.
*
* @return {SolutionStatusEvent}
*/
_createClass(SolutionTable, [{
key: "tail",
value: function tail() {
if (!this.index) {
throw new UnitializedVariableException();
}
if (this.index.size() > 0) {
var result = [];
var done = false;
// Find the last occurrence of a time, and check to see if there are any
// preceding elements. Accumulate those, and then return the merged
// value.
var pos = this.index.size() - 1;
var lastTime = this.index.at(pos).timestamp();
while (!done) {
if (pos >= 0 && pos < this.index.size() && this.index.at(pos).timestamp().getTime() == lastTime.getTime()) {
result.unshift(this.index.at(pos));
} else {
done = true;
break;
}
--pos;
}
return SolutionStatusEvent.fromEvent((0, _time.mergeEvents)(result));
} else {
return null;
}
}
// TODO: Add approximate match.
/**
* Checks to see if the current time is in the table.
*
* Optimizes for online use cases and will actually warn if calling any
* expensive lookups for events that are likely in the backing store's index
* but earlier than the latest event.
*
* @param {Date} time - Time
* @return {boolean} Return true if time is in the index, false otherwise.
*/
}, {
key: "hasTime",
value: function hasTime(time) {
if (!this.index) {
throw new UnitializedVariableException();
}
if (this.index.size() === 0) {
return false;
}
// Some optimizations! For online usage pattern, check last element and exit
// early.
var latestTime = this.index.atLast().timestamp().getTime();
if (latestTime === time.getTime()) {
return true;
}
// Guaranteed ordering - exit early when possible to avoid bisect
if (latestTime < time.getTime()) {
return false;
}
if (this.warnOnOOO) {
console.warn("hasTime: Sample time ${time.getTime()} precedes ${latestTime}");
}
var pos = this.index.bisect(time);
// Bisect method only bounds timestamp intervals, but doesn't check to see
// that the resulting event timestamp is an exact match.
return pos >= 0 && pos < this.index.size() && this.index.at(pos).timestamp().getTime() === time.getTime();
}
/**
* Get SolutionStatusEvent at a particular index, between 0 and the size
* of the index.
*
* @throws IndexOutOfBoundsExceptions
* @param {Number} pos - position within the index
* @return {SolutionStatusEvent}
*/
}, {
key: "atPos",
value: function atPos(pos) {
if (!this.index) {
throw new UnitializedVariableException();
}
if (pos < 0 || pos >= this.index.size()) {
throw new IndexOutOfBoundsExceptions();
}
return SolutionStatusEvent.fromEvent((0, _time.mergeEvents)([this.index.at(pos)]));
}
/**
* Get SolutionStatusEvent at a particular point in time.
*
* This retrieval method merges all the solution events at a particular
* time. The backing table actually stores all updates at a particular time,
* but the backing timeseries API doesn't give an option to conditionally
* retrieve items at an instance of time.
*
* @param {date} Time - (degrees)
* @return {SolutionStatusEvent}
*/
}, {
key: "atTime",
value: function atTime(time) {
if (!this.index) {
throw new UnitializedVariableException();
}
// For online usage pattern, check last element and exit early.
if (this.index.size() > 0 && this.index.atLast().timestamp().getTime() === time.getTime()) {
return this.tail();
}
var result = [];
var done = false;
// Find the first occurrence of a time, and check to see if there are any
// sequential elements. Accumulate those, and then return the merged
// value.
var pos = this.index.bisect(time);
while (!done) {
if (pos >= 0 && pos < this.index.size() && this.index.at(pos).timestamp().getTime() == time.getTime()) {
result.push(this.index.at(pos));
} else {
done = true;
break;
}
++pos;
}
if (result.length === 0) {
return null;
}
return SolutionStatusEvent.fromEvent((0, _time.mergeEvents)(result));
}
/**
* Return an array of SolutionStatusEvents.
*
* @return {Array<SolutionStatusEvent>}
*/
}, {
key: "toArray",
value: function toArray() {
if (!this.index) {
throw new UnitializedVariableException();
}
var result = [];
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = this.index.events()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var event = _step.value;
result.push(SolutionStatusEvent.fromEvent(event));
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
return result;
}
/**
* Return an array of Objects.
*
* @return {Array<Object>}
*/
}, {
key: "toObjectArray",
value: function toObjectArray() {
if (!this.index) {
throw new UnitializedVariableException();
}
var result = [];
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = this.index.events()[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var event = _step2.value;
result.push(event.data().toJS());
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return result;
}
/**
* Return the number of events store in the table.
*
* @return {number} Size, in elements, of the current timeseries.
*/
}, {
key: "size",
value: function size() {
if (!this.index) {
throw new UnitializedVariableException();
}
return this.index.size();
}
/**
* What's the duration of data in this table?
*
* @return {number} - Duration in seconds.
*/
}, {
key: "getDuration",
value: function getDuration() {
if (!this.index) {
throw new UnitializedVariableException();
}
if (this.size() > 0) {
return this.getTimeRange().duration() / MSEC_IN_SEC;
} else {
return 0;
}
}
/**
* Add a ECEF offset to the sample coordinates.
*
* @param {Array<number>} Offset - ECEF offset to apply to sample coordinates.
*/
}, {
key: "addSampleEcefOffset",
value: function addSampleEcefOffset(offset) {
if (!this.index) {
throw new UnitializedVariableException();
}
throw new err.NotImplementedException();
}
/**
* LatLon-based horizontal error.
*
* @return {any}
*/
}, {
key: "getAbsError2dTs",
value: function getAbsError2dTs() {
if (!this.index) {
throw new UnitializedVariableException();
}
return this.index.select(sym.MEAS_H_ERR);
}
/**
* ECEF-frame 3d error.
*
* @return {any}
*/
}, {
key: "getAbsError3dTs",
value: function getAbsError3dTs() {
if (!this.index) {
throw new UnitializedVariableException();
}
return this.index.select(sym.MEAS_SPH_ERR);
}
/**
* Convenience method for filling in all the errors.
*
* @return {any}
*/
}, {
key: "getErrorsTs",
value: function getErrorsTs() {
if (!this.index) {
throw new UnitializedVariableException();
}
return this.index.select([sym.MEAS_H_ERR, sym.MEAS_SPH_ERR]);
}
/**
* Produce percentiles of errors.
*
* @return {Object}
*/
}, {
key: "getPercentileErrors",
value: function getPercentileErrors() {
if (!this.index) {
throw new UnitializedVariableException();
}
throw new err.NotImplementedException();
}
/**
* Calculate fixing percentage for different RTK fixing modes.
*
* @return {any}
*/
}, {
key: "getFixingPercentage",
value: function getFixingPercentage(freq) {
if (!this.index) {
throw new UnitializedVariableException();
}
throw new err.NotImplementedException();
}
/**
* Calculates solution frequency, in Hertz.
*
* @return {any}
*/
}, {
key: "getSolutionFrequency",
value: function getSolutionFrequency() {
if (!this.index) {
throw new UnitializedVariableException();
}
throw new err.NotImplementedException();
}
/**
* Summarize a metrics map of performance, per the evaluation schema defined
* at
* https://github.com/swift-nav/gnss-testing/blob/master/schemas/evaluation.json
*/
}, {
key: "getSummarizePerformance",
value: function getSummarizePerformance() {
if (!this.index) {
throw new UnitializedVariableException();
}
throw new err.NotImplementedException();
}
/**
* Get the TimeRange of the data in this table.
*
* @return {TimeRange}
*/
}, {
key: "getTimeRange",
value: function getTimeRange() {
if (!this.index) {
throw new UnitializedVariableException();
}
if (this.size() == 0) {
throw new _time.EmptyTimeRangeError(this.name);
}
return new _pondjs.TimeRange(this.index.at(0).timestamp(), this.index.atLast().timestamp());
}
/**
* Return this table as a JSON object
*
* @return {Object}
*/
}, {
key: "toJSON",
value: function toJSON() {
if (!this.index) {
throw new UnitializedVariableException();
}
if (this.size() == 0) {
throw new _time.EmptyTimeRangeError(this.name);
}
return this.index.toJSON();
}
}]);
return SolutionTable;
}();
/**
* Solution state table for pre-processed solution events.
*
*/
var BatchSolutionTable = exports.BatchSolutionTable = function (_SolutionTable) {
_inherits(BatchSolutionTable, _SolutionTable);
function BatchSolutionTable(name, events) {
_classCallCheck(this, BatchSolutionTable);
var _this5 = _possibleConstructorReturn(this, (BatchSolutionTable.__proto__ || Object.getPrototypeOf(BatchSolutionTable)).call(this, name));
_this5.index = new _pondjs.TimeSeries({ name: _this5.name,
columns: sym.COLUMNS,
events: events });
return _this5;
}
/**
* Merging
*
* @return {any}
*/
_createClass(BatchSolutionTable, [{
key: "join",
value: function join(reference) {
var _iteratorNormalCompletion3 = true;
var _didIteratorError3 = false;
var _iteratorError3 = undefined;
try {
for (var _iterator3 = this.index.events()[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
var event = _step3.value;
var time = event.timestamp();
if (reference.atTime(event.timestamp())) {
event.compareTo(reference.atTime(event.timestamp()).soln);
}
}
} catch (err) {
_didIteratorError3 = true;
_iteratorError3 = err;
} finally {
try {
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
} finally {
if (_didIteratorError3) {
throw _iteratorError3;
}
}
}
}
/**
* Adds geodetic reference coordinates to a sample.
*
* @param {Number} lat - latitude (degrees)
*/
}, {
key: "addRefLlh",
value: function addRefLlh(refPos) {
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = this.index.events()[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var event = _step4.value;
event.compareTo(new _coords.SolutionStatus(event.timestamp(), refPos.toECEF(), refPos));
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
}
/**
* Add ECEF reference coordinates to a sample.
*
* @param {Number} lat - latitude (degrees)
*/
}, {
key: "addRefEcef",
value: function addRefEcef(refPos) {
var _iteratorNormalCompletion5 = true;
var _didIteratorError5 = false;
var _iteratorError5 = undefined;
try {
for (var _iterator5 = this.index.events()[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) {
var event = _step5.value;
event.compareTo(new _coords.SolutionStatus(event.timestamp(), refPos, refPos.toLLA()));
}
} catch (err) {
_didIteratorError5 = true;
_iteratorError5 = err;
} finally {
try {
if (!_iteratorNormalCompletion5 && _iterator5.return) {
_iterator5.return();
}
} finally {
if (_didIteratorError5) {
throw _iteratorError5;
}
}
}
}
}]);
return BatchSolutionTable;
}(SolutionTable);
/**
* Painfully construct a Timeseries with manual metadata. See comment in the
* body.
*
* @param {string} name - Name of timeseries
* @param {boolean} utc - Are the times here "UTC"?
* @param {Collection} coll - Pond Collection
* @return {TimeSeries} New Pond Timeseries
*/
function getNewTimeSeries(name, utc, coll) {
// HACK(mookerji): this construction here is a hack, as the building of
// metadata in pond 0.7.0 seems to require a linear scan of the events in the
// timeseries to build metadata that ultimately isn't consumed. Once this is
// resolved in Pond's Timeseries copy constructors, we should replace this
// with the proper constructor.
var index = new _pondjs.TimeSeries();
index._collection = coll;
index._data = Immutable.Map({ name: name,
utc: utc });
return index;
}
/**
* Online solution state table.
*
* A columnar timeseries table that supports streaming, immutable updates. It
* intended to store a flat map of results, for a particular instance in time,
* using the result schema defined by Swift's gnss-testing repository:
* https://github.com/swift-nav/gnss-testing/blob/master/DESIGN.md#swift-solution-csv
*
* Additionally, stores some accumulated statistical state about solutions that
* have been seen so far: a histogram of solution fix modes and *approximate*
* CDFs of the spherical, horizontal, and vertical error (implemented using a
* T-Digest).
*
* Example usage:
* ```
* let table1 = new tab.OnlineSolutionTable("test_table1");
*
* // Construct a sample solution point
* const time1 = new Date('2016-06-05');
* const ecef1 = new coords.ECEFSolution(ctest.earthA, 0, 0);
* table1 = table1.addSampleSoln(new coords.SolutionStatus(time1, ecef1));
*
* // Construct a reference point at the same time and update reference errors
* const ecef2 = new coords.ECEFSolution(ctest.earthA, 0, ctest.earthA);
* table1 = table1.updateReferenceSoln(
* new coords.SolutionStatus(time1, ecef2, ecef2.toLLA()));
*
* // Retrieve and print error at a particular time, and aggregated errors:
* let result = table1.atTime(time1).toJSON();
* console.log(result['abs_error_2d']); => // A number
* console.log(table1.getPercentileErrors()); // A map
* ```
*
*/
var OnlineSolutionTable = exports.OnlineSolutionTable = function (_SolutionTable2) {
_inherits(OnlineSolutionTable, _SolutionTable2);
/**
* Construct online solution state table.
*
* @param {string} name - Human-readable name
* @return {OnlineSolutionTable} The constructed solution table.
*/
function OnlineSolutionTable(name) {
_classCallCheck(this, OnlineSolutionTable);
var _this6 = _possibleConstructorReturn(this, (OnlineSolutionTable.__proto__ || Object.getPrototypeOf(OnlineSolutionTable)).call(this, name));
_this6.utc = true;
_this6._init();
return _this6;
}
/**
* Initialize state: backing timeseries and accumulators.
*
* @return {OnlineSolutionTable}
*/
_createClass(OnlineSolutionTable, [{
key: "_init",
value: function _init() {
this._setCollection(new _pondjs.Collection());
// Initialize online accumulators
this.modeCounter = new _accumulators.OnlineHistogram('fix_mode_counter');
this.absError3dCDF = new _accumulators.OnlineStatistics('absError3dCDF');
this.absErrorHCDF = new _accumulators.OnlineStatistics('absErrorHCDF');
this.absErrorVCDF = new _accumulators.OnlineStatistics('absErrorVCDF');
return this;
}
/**
* Sets the backing collection and timeseries, which are immutable objects.
*
* @param {Collection} coll - Collection to set
* @return {OnlineSolutionTable}
*/
}, {
key: "_setCollection",
value: function _setCollection(coll) {
this.index = getNewTimeSeries(this.name, this.utc, coll);
return this;
}
/**
* Update timeseries with a solution status event, which includes positions in
* different coordinate systems and measured errors at a particular instance
* in-time. This is an internal method.
*
* @param {SolutionStatusEvent} event
* @return {OnlineSolutionTable}
*/
}, {
key: "_update",
value: function _update(event) {
var table = new OnlineSolutionTable(this.name)._setCollection(this.index.collection().addEvent(event));
table.warnOnOOO = this.warnOnOOO;
table.modeCounter.setStore(this.modeCounter);
// Sometimes we may update a timeseries more than once if we receive a ECEF
// or geodetic solution as separate messages. Assuming that the fix mode
// doesn't appear more than once, if a fix mode event has been updated for a
// particular instant in-time, don't update it again (i.e., double count an
// event).
if (!isNaN(event.soln.fixMode) && !this.hasTime(event.soln.time)) {
table.modeCounter = table.modeCounter.update(sym.fixModeToString[event.soln.fixMode]);
}
table.absError3dCDF.setStore(this.absError3dCDF);
if (!isNaN(event.absError3d)) {
table.absError3dCDF = table.absError3dCDF.update(event.absError3d);
}
table.absErrorHCDF.setStore(this.absErrorHCDF);
if (!isNaN(event.absErrorH)) {
table.absErrorHCDF = table.absErrorHCDF.update(event.absErrorH);
}
table.absErrorVCDF.setStore(this.absErrorVCDF);
if (!isNaN(event.absErrorV)) {
table.absErrorVCDF = table.absErrorVCDF.update(event.absErrorV);
}
return table;
}
/**
* Update the timeseries with a new sample solution. Returns a new table.
*
* @param {SolutionStatus} soln - Sample solution
* @return {OnlineSolutionTable}
*/
}, {
key: "addSampleSoln",
value: function addSampleSoln(soln) {
var updatedTable = this._update(new SolutionStatusEvent(soln));
// If there's a buffered reference solution, update the table with it
// We assume that this corresponds to the event that was just added (if not,
// this is a noop)
// We only attempt to add buffered solutions if they're equal to the solution that we
// just added, otherwise a bunch of CPU cycles can be wasted in `hasTime`, `bisect`, etc.
if (this.bufferedReferenceSolution && this.bufferedReferenceSolution.time.getTime() === soln.time.getTime()) {
return updatedTable.updateReferenceSoln(this.bufferedReferenceSolution);
}
return updatedTable;
}
/**
* Add reference coordinates to a sample.
*
* @param {SolutionStatus} ref - Reference solution
* @return {OnlineSolutionTable}
*/
}, {
key: "updateReferenceSoln",
value: function updateReferenceSoln(ref) {
if (!this.hasTime(ref.time)) {
this.bufferedReferenceSolution = ref;
return this;
}
var solnEvent = this.atTime(ref.time).compareTo(ref);
this.bufferedReferenceSolution = null;
return this._update(new SolutionStatusEvent(solnEvent.soln, solnEvent.absError3d, solnEvent.absErrorH, solnEvent.absErrorV));
}
/**
* Crop solution table to range. Returns a new OnlineSolutionTable, discarding
* accumulated aggregated statistics state of the original range.
*
* @param {TimeRange} range - TimeRange
* @return {OnlineSolutionTable}
*/
}, {
key: "crop",
value: function crop(range) {
if (!this.index) {
throw new UnitializedVariableException();
}
var timerange = this.index.timerange();
if (!timerange) {
throw new err.InvalidArgumentException("Invalid index!");
}
if (timerange.disjoint(range)) {
return new OnlineSolutionTable(this.name);
} else if (range.within(timerange)) {
var coll = this.index.crop(range).collection();
return new OnlineSolutionTable(this.name)._setCollection(coll);
} else if (timerange.within(range)) {
var _coll = this.index.collection();
return new OnlineSolutionTable(this.name)._setCollection(_coll);
} else if (timerange.overlaps(range)) {
var _coll2 = this.index.crop(timerange.intersection(range)).collection();
return new OnlineSolutionTable(this.name)._setCollection(_coll2);
} else {
throw new err.InvalidArgumentException("Invalid timerange!");
}
}
/**
* Select OnlineSolutionTable between begin and end.
*
* @param {number} begin - The position to begin slicing
* @param {number} end - The position to end slicing
* @return {OnlineSolutionTable}
*/
}, {
key: "slice",
value: function slice(begin, end) {
if (!this.index) {
throw new UnitializedVariableException();
}
if (begin < 0 || begin >= end) {
throw new err.InvalidArgumentException("Invalid begin for slice!" + begin);
}
if (end < 0 || end >= this.size()) {
throw new err.InvalidArgumentException("Invalid end for slice! " + end);
}
return new OnlineSolutionTable(this.name)._setCollection(this.index.slice(begin, end).collection());
}
/**
* Select OnlineSolutionTable max.
*
* @param {string} column - The column to find the max of.
* @return {number}
*/
}, {
key: "max",
value: function max(column) {
if (!this.index) {
throw new UnitializedVariableException();
}
return this.index.max(column);
}
/**
* Internal, generic function for collecting summary statistics from internal
* statistics accumulators.
*
* @param {Function} fn - One of the class methods on OnlineStatistics.
* @return {Object} - Object of values keyed error type (one of
* abs_error_2d, abs_error_3d, abs_error_h).
*/
}, {
key: "_collectAggValue",
value: function _collectAggValue(fn) {
if (!this.index || !this.absError3dCDF || !this.absErrorHCDF || !this.absErrorVCDF) {
throw new UnitializedVariableException();
}
var result = {};
result[sym.MEAS_SPH_ERR] = fn(this.absError3dCDF);
result[sym.MEAS_H_ERR] = fn(this.absErrorHCDF);
result[sym.MEAS_V_ERR] = fn(this.absErrorVCDF);
return result;
}
/**
* Returns the max errors. Queries are O(1), using backing online accumulator.
*
* @return {Object} - Object of max values keyed error type (one of
* abs_error_2d, abs_error_3d, abs_error_h).
*/
}, {
key: "maxErrors",
value: function maxErrors() {
return this._collectAggValue(function (obj) {
return obj.getMax();
});
}
/**
* Select OnlineSolutionTable min.
*
* @param {string} column - The column to find the min of.
* @return {number}
*/
}, {
key: "min",
value: function min(column) {
if (!this.index) {
throw new UnitializedVariableException();
}
return this.index.min(column);
}
/**
* Returns the min errors. Queries are O(1), using backing online accumulator.
*
* @return {Object} - Object of min values keyed error type (one of
* abs_error_2d, abs_error_3d, abs_error_h).
*/
}, {
key: "minErrors",
value: function minErrors() {
return this._collectAggValue(function (obj) {
return obj.getMin();
});
}
/**
* Produce approximate cumulative distribution function (CDF) errors.
*
* Returns a map result that looks like (bogus values):
*
* { abs_error_2d: { '10': 637, ... , '90': 630, '95': 635, '99': 637 },
* ... }
*
* @return {Object} Nested Object of error types, with percentile values keyed
* by a string percentile value.
*/
}, {
key: "getErrorsCDF",
value: function getErrorsCDF() {
return this._collectAggValue(function (obj) {
return obj.getCDF();
});
}
/**
* Produce approximate percentiles of errors.
*
* Returns a map result that looks like (bogus values):
*
* { abs_error_2d: { '50': 637, '90': 637, '99': 637 },
* abs_error_v: { '50': 500, '90': 500, '99': 500 },
* abs_error_3d: { '50': -265, '90': -265, '99': -265 }}
*
* @param {Array<number>} percentiles - List of percentiles, with each
* element between 0 and 1. Defaults to [0.50, 0.90, 0.99].
* @return {Object} Nested Object of error types, with percentile values keyed
* by a string percentile value.
*/
}, {
key: "getPercentileErrors",
value: function getPercentileErrors() {
var percentiles = arguments.length <= 0 || arguments[0] === undefined ? [0.50, 0.90, 0.99] : arguments[0];
return this._collectAggValue(function (obj) {
return obj.getPercentiles(percentiles);
});
}
/**
* Calculate exact fixing percentage for different RTK