@ntrip/caster
Version:
NTRIP caster
524 lines (523 loc) • 23.7 kB
JavaScript
"use strict";
/*
* This file is part of the @ntrip/caster distribution (https://github.com/node-ntrip/caster).
* Copyright (c) 2020 Nebojsa Cvetkovic.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sourcetable = void 0;
const verror_1 = __importDefault(require("verror"));
var Sourcetable;
(function (Sourcetable) {
/**
* An entry in the caster sourcetable
*
* Provides helper methods for filtering sourcetable entries and generating sourcetable text lines.
*/
class Entry {
/** The sourcetable entry type as 3 characters e.g. STR, CAS, NET */
get entryType() {
return this.constructor.ENTRY_TYPE;
}
;
/**
* Returns the entry as a sourcetable text line
*/
toSourcetableLine() {
return this.toSourcetableLineElements()
.map(item => item !== null && item !== void 0 ? item : '')
.join(';');
}
;
/**
* Returns a list of elements included in the entry's sourcetable text line
*/
toSourcetableLineElements() {
let result = Entry.structureElementsToLine(this.toRawSourcetableLineElements(), this.constructor.STRUCTURING_CONVERSIONS);
// Insert entry type
result.unshift(this.entryType);
return result;
}
/**
* Parses the provided sourcetable text line and updates fields in this entry
*
* @param line Sourcetable text line
*/
fromSourcetableLine(line) {
const elements = line.split(';');
if (elements[0] !== this.entryType)
throw new Error(`Unexpected entry type: ${elements[0]}`);
this.fromSourcetableLineElements(elements);
}
;
/**
* Updates the entry's elements based on the provided list of elements
*/
fromSourcetableLineElements(elements) {
// Remove entry type
elements = elements.slice(1);
this.fromRawSourcetableLineElements(Entry.destructureElementsFromLine(elements, this.constructor.DESTRUCTURING_CONVERSIONS));
}
static destructureElementsFromLine(elements, destructuringConversions) {
return elements.map((element, index) => {
element = element.trim();
if (element.length === 0)
return undefined;
const conversion = destructuringConversions[index];
if (conversion === null || conversion === undefined)
return element;
let value = conversion(element);
// Treat NaN as error/undefined
if (typeof value === 'number' && isNaN(value))
return undefined;
return value;
});
}
static structureElementsToLine(elements, structuringConversions) {
return elements.map((element, index) => {
if (element == undefined)
return element;
const conversion = structuringConversions[index];
if (conversion === null || conversion === undefined)
return element;
return conversion(element);
});
}
/**
* Filter this source entry by simple and advanced element filtering
*
* @param filters Filter lists
* @param simple Whether a simple search is being performed
* @param strict Whether to throw errors when an invalid condition is found
*/
filter(filters, simple, strict = false) {
return filters.every(f => this.filterElements(f, simple, strict));
}
/**
* Filters this source entry based on its elements
*
* @param filters List of filters for each element
* @param simple Whether a simple search is being performed
* @param strict Whether to throw errors when an invalid condition is found
*/
filterElements(filters, simple, strict) {
// No filtering to be done if elements list is empty
if (filters.length == 0)
return true;
const elements = this.toSourcetableLineElements();
// Was a specific entry type requested
const typeRequested = typeof filters[0] === 'string' &&
[CasterEntry.ENTRY_TYPE, NetworkEntry.ENTRY_TYPE, SourceEntry.ENTRY_TYPE].includes(filters[0]);
// Prevent ambiguity in strict mode
if (!simple && strict && !typeRequested)
throw new Error("Sourcetable entry type was not selected in filter, will result in search term ambiguity");
// Check if all filter conditions are met
return filters.every((filter, i) => {
const element = elements[i];
// Don't check match for undefined elements
if (filter === undefined)
return true;
// If element is provided but not set for entry, match fails
if (element === undefined)
return false;
// Simple matching, loosely compare values
if (typeof filter === 'string')
return element == filter;
// Number approximation filter (for closest stream) is applied later
if (typeof filter === 'number')
return true;
const { string, number } = filter;
const newParseError = (type, cause) => new verror_1.default(cause, "Could not parse sourcetable entry %s element using provided filters", type);
// Advanced comparison with operators
if (typeof element === 'string') {
if (string instanceof Error) {
if (strict)
throw newParseError('string', string);
// Be as forgiving as possible by default
return true;
}
// Attempt to match
return string.test(element);
}
else { // if (typeof selfElement === "number") {
if (number instanceof Error) {
if (strict)
throw newParseError('number', number);
// Be as forgiving as possible by default
return true;
}
// Attempt to match
return number.some(terms => // One of ORs must be true
terms.every(term => {
if (term instanceof Error) {
if (strict)
throw newParseError('number', term);
// Be as forgiving as possible by default
return true;
}
return term.test(element);
}));
}
});
}
/**
* Parses the provided sourcetable text line and returns a corresponding sourcetable entry object
*
* @param line Sourcetable text line
*/
static parseSourcetableLine(line) {
let entry;
if (line.startsWith(CasterEntry.ENTRY_TYPE)) {
entry = new CasterEntry('', 0);
}
else if (line.startsWith(NetworkEntry.ENTRY_TYPE)) {
entry = new NetworkEntry('');
}
else if (line.startsWith(SourceEntry.ENTRY_TYPE)) {
entry = new SourceEntry('');
}
else {
return new Error(`Unexpected sourcetable entry type: ${line.slice(0, 3)}`);
}
entry.fromSourcetableLine(line);
return entry;
}
}
Sourcetable.Entry = Entry;
function fromOneZeroValue(val) {
return val === '1';
}
function toOneZeroValue(val) {
return val ? '1' : '0';
}
function fromYesNoValue(val) {
return val === 'Y';
}
function toYesNoValue(val) {
return val ? 'Y' : 'N';
}
function fromFormatDetails(val) {
let rawTypes = val.split(',');
let parsedTypes = rawTypes.map(type => { var _a; return (_a = /^(?<type>.+)(?:\((?<rate>[0-9]+)\))?$/.exec(type)) === null || _a === void 0 ? void 0 : _a.groups; });
return parsedTypes.some(t => t === undefined) ? rawTypes : parsedTypes;
}
function toFormatDetails(val) {
if (!(val instanceof Array))
return val;
if (val.length === 0)
return '';
if (typeof val[0] === 'object') {
val = val
.map(type => `${type.type}${type.rate === undefined ? '' : `(${type.rate})`}`);
}
return val.join(',');
}
class SourceEntry extends Entry {
constructor(mountpoint) {
super();
this.mountpoint = mountpoint;
}
fromRawSourcetableLineElements(elements) {
if (typeof elements[0] === 'string')
this.mountpoint = elements[0];
[, this.identifier, this.format, this.formatDetails, this.carrier, this.navSystem,
this.network, this.country, this.latitude, this.longitude, this.nmea, this.solution, this.generator,
this.compressionEncryption, this.authentication, this.fee, this.bitrate, ...this.misc] = elements;
}
toRawSourcetableLineElements() {
var _a;
return [this.mountpoint, this.identifier, this.format, this.formatDetails, this.carrier, this.navSystem,
this.network, this.country, this.latitude, this.longitude, this.nmea, this.solution, this.generator,
this.compressionEncryption, this.authentication, this.fee, this.bitrate, ...((_a = this.misc) !== null && _a !== void 0 ? _a : ['none'])];
}
}
SourceEntry.ENTRY_TYPE = 'STR';
SourceEntry.DESTRUCTURING_CONVERSIONS = [
null, null, null, fromFormatDetails, parseInt, (s) => s.split('+'), null, null, parseFloat, parseFloat,
fromOneZeroValue, parseInt, null, null, null, fromYesNoValue, parseFloat
];
SourceEntry.STRUCTURING_CONVERSIONS = [
null, null, null, toFormatDetails, null, (e) => e.join('+'), null, null, null, null,
toOneZeroValue, null, null, null, null, toYesNoValue, null
];
Sourcetable.SourceEntry = SourceEntry;
class CasterEntry extends Entry {
constructor(host, port) {
super();
this.fallback_host = '0.0.0.0'; // Fallback Caster IP address, No Fallback: 0.0.0.0
this.fallback_port = 0; // Fallback Caster port number, No Fallback: 0
this.host = host;
this.port = port;
}
fromRawSourcetableLineElements(elements) {
if (typeof elements[0] === 'string')
this.host = elements[0];
if (typeof elements[1] === 'number')
this.port = elements[1];
[, , this.identifier, this.operator, this.nmea, this.country,
this.latitude, this.longitude, this.fallback_host, this.fallback_port, ...this.misc] = elements;
}
toRawSourcetableLineElements() {
var _a;
return [this.host, this.port, this.identifier, this.operator, this.nmea, this.country,
this.latitude, this.longitude, this.fallback_host, this.fallback_port, ...((_a = this.misc) !== null && _a !== void 0 ? _a : ['none'])];
}
}
CasterEntry.ENTRY_TYPE = 'CAS';
CasterEntry.DESTRUCTURING_CONVERSIONS = [
null, parseInt, null, null, parseInt, null, parseFloat, parseFloat, null, parseInt
];
CasterEntry.STRUCTURING_CONVERSIONS = [
null, null, null, null, null, null, null, null, null, null
];
Sourcetable.CasterEntry = CasterEntry;
class NetworkEntry extends Entry {
constructor(identifier) {
super();
this.identifier = identifier;
}
fromRawSourcetableLineElements(elements) {
if (typeof elements[0] === 'string')
this.identifier = elements[0];
[, this.operator, this.authentication, this.fee, this.webNetwork,
this.webStream, this.webRegistration, ...this.misc] = elements;
}
toRawSourcetableLineElements() {
var _a;
return [this.identifier, this.operator, this.authentication, this.fee, this.webNetwork,
this.webStream, this.webRegistration, ...((_a = this.misc) !== null && _a !== void 0 ? _a : ['none'])];
}
}
NetworkEntry.ENTRY_TYPE = 'NET';
NetworkEntry.DESTRUCTURING_CONVERSIONS = [
null, null, fromYesNoValue, null, null, null
];
NetworkEntry.STRUCTURING_CONVERSIONS = [
null, null, toYesNoValue, null, null, null
];
Sourcetable.NetworkEntry = NetworkEntry;
const FILTER_SIMPLE_CHECK = /^[^!|+=<>*~]*$/;
const FILTER_NUMBER_CHECK = /^[0-9!|+=<>.]+$/;
const FILTER_NUMBER_COMPARISON = /^(?<negate>!)?(?<comparator><|>|<=|>=|=)?(?<number>[-+]?(?:\d*\.\d+|\d+)?)$/;
const FILTER_NUMBER_APPROXIMATION = /^~(?<number>[-+]?(?:\d*\.\d+|\d+))$/;
class NumberFilter {
constructor(negate, comparator, value) {
this.negate = negate;
this.comparator = comparator;
this.value = value;
}
test(input) {
return this.compare(input) != this.negate;
}
compare(input) {
let a = input;
let b = this.value;
switch (this.comparator) {
case '=': return Math.abs(a - b) < Number.EPSILON;
case '>': return a > b;
case '<': return a < b;
case '>=': return a >= b;
case '<=': return a <= b;
}
}
}
/**
* Parses advanced NTRIP filters
*
* Converts filters from string form to RegExp/number matcher or simplified string form.
*
* Used to process filters that were passed in by the user during sourcetable GET request.
*
* @param filters Parsed advanced filter list
*/
function parseAdvancedFilters(filters) {
return filters.map(filter => {
var _a, _b;
// Undefined filters are ignored
if (filter === undefined || filter.length === 0)
return undefined;
// Strings not containing any special characters are treated as simple filters
if (FILTER_SIMPLE_CHECK.test(filter))
return filter;
// Number approximation filter (for closest stream)
const approximation = (_b = (_a = FILTER_NUMBER_APPROXIMATION.exec(filter)) === null || _a === void 0 ? void 0 : _a.groups) === null || _b === void 0 ? void 0 : _b['number'];
if (approximation !== undefined)
return parseFloat(approximation);
// Parse for both number and string values (warnings shown when filtering is performed in strict mode)
let string;
let number;
// Number filter
if (FILTER_NUMBER_CHECK.test(filter)) {
// Convert to SourcetableElementNumberFilter for later application
number = filter.split('|') // Split ORs
.map(terms => terms.split('+')
.map(term => {
var _a;
const match = FILTER_NUMBER_COMPARISON.exec(term);
if (match === null)
return new Error(`Invalid term for number filter: ${term}`);
return new NumberFilter(match.groups['negate'] !== undefined, (_a = match.groups['comparator']) !== null && _a !== void 0 ? _a : '=', parseFloat(match.groups['number']));
}));
}
else {
number = new Error(`Invalid number filter: ${filter}`);
}
// String filter
try {
// Double up each group of parentheses (for internal ORs)
filter = filter.replace(/[()]/g, '$&$&');
// Escape RegExp characters (except parentheses, * and |)
filter = filter.replace(/[.+?^${}[\]\\]/g, '\\$&');
// Treat each OR as an independent group
filter = filter.replace(/\|/g, ')|(');
// Entire string must be matched
filter = '^((' + filter + '))$';
// Replace all wildcards
filter = filter.replace(/\*/g, '.*');
// Replace all negations
filter = filter.replace(/\(!([^)]+)\)/g, '((?!$1).*)');
string = new RegExp(filter, 'i');
}
catch (error) {
string = new Error(`Invalid string filter: ${filter}`);
}
// Return options for both string and number depending on element type
return {
string: string,
number: number
};
});
}
Sourcetable.parseAdvancedFilters = parseAdvancedFilters;
/**
* Performs filtering of sourcetable for approximation filters (closest values)
*
* Allows user to provide one or more values for which the closest matching entries are to be returned.
*
* For example, user could request nearest servers by lat/lng with STR;;;;;;;;~53.1;~-7.6.
*
* A score is calculated for each entry as the sum of the distances of its elements to the target values.
*
* Multiple values can be provided for a given field to allow for multiple simultaneous filters, and each is
* added to the cumulative score for each entry.
*
* @param filters Array of numbers at entry element indices to approximate
* @param entries List of entries to filter
* @param strict Whether to throw an error if a non numeric element is encountered in an entry when expected
* @return Filtered list of entries containing the entries closest to the request values
*/
function filterApproximations(filters, entries, strict = false) {
if (entries.length <= 1)
return entries;
// Aggregate from filter list to numbers to approximate for each element
const maxElements = Math.max(...filters.map((filters) => filters.length));
const approximations = [];
for (let i = 0; i < maxElements; i++) {
let numbers = filters.map((filters) => filters[i])
.filter((filter) => typeof filter === "number");
approximations[i] = numbers.length === 0 ? undefined :
numbers.reduce((a, b) => a + b, 0) / numbers.length;
}
if (approximations.length == 0)
return entries;
return entries.map(entry => {
// Select elements from each entry
const elements = entry.toSourcetableLineElements();
let score = 0;
for (let i = 0; i < approximations.length; i++) {
const number = approximations[i];
// Skip undefined approximation filter entries
if (number === undefined)
continue;
const element = elements[i];
// Element must also be a number to be compared (can't approximate string)
if (typeof element !== 'number') {
if (strict)
throw new Error("Could not approximate entry element value as it is not a number");
return null;
}
score += Math.abs(element - number);
}
// Store entry and its score for later reduction
return {
entry: entry,
score: score
};
}).reduce((accumulator, entry) => {
// Ignore entries that were invalid
if (entry === null)
return accumulator;
if (accumulator.entries.length == 0 || entry.score < accumulator.score) {
// First entry or entry with lower score
accumulator.entries = [entry.entry];
accumulator.score = entry.score;
}
else if (Math.abs(entry.score - accumulator.score) < Number.EPSILON) {
// Entry with same score as current best
accumulator.entries.push(entry.entry);
}
return accumulator;
}, { entries: [], score: Infinity }).entries;
}
Sourcetable.filterApproximations = filterApproximations;
let Format;
(function (Format) {
Format["BINEX"] = "BINEX";
Format["CMR"] = "CMR";
Format["NMEA"] = "NMEA";
Format["RAW"] = "RAW";
Format["RTCA"] = "RTCA";
Format["RTCM_2"] = "RTCM 2";
Format["RTCM_2_1"] = "RTCM 2.1";
Format["RTCM_2_2"] = "RTCM 2.2";
Format["RTCM_2_3"] = "RTCM 2.3";
Format["RTCM_3"] = "RTCM 3";
Format["RTCM_3_0"] = "RTCM 3.0";
Format["RTCM_3_1"] = "RTCM 3.1";
Format["RTCM_3_2"] = "RTCM 3.2";
Format["RTCM_3_3"] = "RTCM 3.3";
Format["RTCM_SAPOS"] = "RTCM SAPOS";
})(Format = Sourcetable.Format || (Sourcetable.Format = {}));
let NavSystem;
(function (NavSystem) {
NavSystem["GPS"] = "GPS";
NavSystem["GLONASS"] = "GLO";
NavSystem["GALILEO"] = "GAL";
NavSystem["BEIDOU"] = "BDS";
NavSystem["QZSS"] = "QZS";
NavSystem["SBAS"] = "SBAS";
NavSystem["IRNSS"] = "IRS";
})(NavSystem = Sourcetable.NavSystem || (Sourcetable.NavSystem = {}));
let SolutionType;
(function (SolutionType) {
SolutionType[SolutionType["SingleBase"] = 0] = "SingleBase";
SolutionType[SolutionType["Network"] = 1] = "Network";
})(SolutionType = Sourcetable.SolutionType || (Sourcetable.SolutionType = {}));
let AuthenticationMode;
(function (AuthenticationMode) {
AuthenticationMode["None"] = "N";
AuthenticationMode["Basic"] = "B";
AuthenticationMode["Digest"] = "D";
})(AuthenticationMode = Sourcetable.AuthenticationMode || (Sourcetable.AuthenticationMode = {}));
let CarrierPhaseInformation;
(function (CarrierPhaseInformation) {
CarrierPhaseInformation[CarrierPhaseInformation["None"] = 0] = "None";
CarrierPhaseInformation[CarrierPhaseInformation["L1"] = 1] = "L1";
CarrierPhaseInformation[CarrierPhaseInformation["L1_L2"] = 2] = "L1_L2";
})(CarrierPhaseInformation = Sourcetable.CarrierPhaseInformation || (Sourcetable.CarrierPhaseInformation = {}));
})(Sourcetable = exports.Sourcetable || (exports.Sourcetable = {}));