UNPKG

gnss_solutions

Version:

Javascript GNSS solution analysis library

1,187 lines (1,062 loc) 53 kB
"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