UNPKG

@ntrip/caster

Version:
524 lines (523 loc) 23.7 kB
"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 = {}));