/* * Philip Crotwell * University of South Carolina, 2019 * https://www.seis.sc.edu */ import { DateTime, Duration } from "luxon"; import {extractDLProto} from "./datalink"; import {defaultPortStringForProtocol, appendToPath} from "./fdsncommon"; import { FDSN_PREFIX, StationSourceId, NetworkSourceId, NslcId, parseSourceId, FDSNSourceId } from "./fdsnsourceid"; import { sidForId, //typeForId } from "./ringserverweb4"; import * as util from "./util"; // for util.log import { doIntGetterSetter, doStringGetterSetter, doFloatGetterSetter, checkProtocol, isNonEmptyStringArg, isNumArg, isDef, isoToDateTime, pullText } from "./util"; export const SEEDLINK_PATH = "seedlink"; export const DATALINK_PATH = "datalink"; export type RingserverVersion = { ringserverVersion: string; serverId: string; }; export type StreamsResult = { accessTime: DateTime; streams: Array; }; export const IRIS_HOST = "rtserve.iris.washington.edu"; const ORG = "Organization: "; /** * Web connection to a Ringserver. * * * @param host optional host to connect to, defaults to IRIS. This maybe a full URL. * @param port optional host to connect to, defaults to 80 */ export class RingserverConnection { /** @private */ _protocol: string; /** @private */ _host: string; /** @private */ _port: number; /** @private */ _prefix: string; /** @private */ _timeoutSec: number; isFDSNSourceId = false; dlproto = "1.0"; constructor(host?: string, port?: number) { const hostStr = isNonEmptyStringArg(host) ? host : IRIS_HOST; if (hostStr.startsWith("http")) { const rs_url = new URL(hostStr); this._host = rs_url.hostname; this._port = parseInt(rs_url.port); this._protocol = rs_url.protocol; if (!Number.isInteger(this._port)) { this._port = 80; } this._prefix = rs_url.pathname; } else { this._protocol = "http:"; this._host = hostStr; this._port = 80; this._prefix = "/"; } // override port in URL if given if (isNumArg(port)) { this._port = port; } this._timeoutSec = 30; } /** * Gets/Sets the remote host to connect to. * * @param value optional new value if setting * @returns new value if getting, this if setting */ host(value?: string): RingserverConnection { doStringGetterSetter(this, "host", value); return this; } getHost(): string { return this._host; } /** * Gets/Sets the remote port to connect to. * * @param value new value to set * @returns this */ port(value?: number): RingserverConnection { doIntGetterSetter(this, "port", value); return this; } getPort(): number | undefined { return this._port; } /** * Sets the prefix for the URL path. * * @param value optional new value if setting * @returns new value if getting, this if setting */ prefix(value?: string): RingserverConnection { if (value && ! value.startsWith("/")) { value = "/"+value; } doStringGetterSetter(this, "prefix", value); return this; } getPrefix(): string { return this._prefix; } /** * Get/Set the timeout in seconds for the request. Default is 30. * * @param value optional new value if setting * @returns new value if getting, this if setting */ timeout(value?: number): RingserverConnection { doFloatGetterSetter(this, "timeoutSec", value); return this; } getTimeout(): number | undefined { return this._timeoutSec; } /** * Pulls id result from ringserver /id parsed into an object with * 'ringserverVersion' and 'serverId' fields. Also sets the * isFDSNSourceId value as ringserver v4 uses new FDSN style ids * while * * @returns Result as a Promise. */ pullId(): Promise { return pullText(this.formIdURL(), this._timeoutSec).then((raw) => { const lines = raw.split("\n"); const ringserver_v4 = "ringserver/4"; const version = lines[0]; let organization = lines[1]; if (organization.startsWith(ORG)) { organization = organization.substring(ORG.length); } if (version.startsWith(ringserver_v4)) { this.isFDSNSourceId = true; } this.dlproto = extractDLProto(lines); if (this.dlproto === "1.0") { if (version.startsWith(ringserver_v4)) { // version 4.0 was FDSN Sid, but did not have DLPROTO:1.1 // version 4.1 and greater should have it this.isFDSNSourceId = true; } else { this.isFDSNSourceId = false; } } else { this.isFDSNSourceId = true; } return { ringserverVersion: lines[0], serverId: organization, datalink: "DLPROTO:1.0", seedlink: "SLPROTO:3.1" }; }); } /** * Use numeric level (1-6) to pull just IDs from ringserver. * In a default ringserver, * level=1 would return all networks like * CO * and level=2 would return all stations like * CO_JSC * If level is falsy/missing, level=6 is used. * The optional matchPattern is a regular expression, so for example * '.+_JSC_00_HH.' would get all HH? channels from any station name JSC. * * @param level 1-6 * @param matchPattern regular expression to match * @returns Result as a Promise. */ pullStreamIds(level: number, matchPattern?: string): Promise> { let queryParams = "level=6"; if (isNumArg(level) && level > 0) { queryParams = "level=" + level; } if (matchPattern && matchPattern.length > 0) { queryParams = queryParams + "&match=" + matchPattern; } const url = this.formStreamIdsURL(queryParams); return pullText(url, this._timeoutSec).then((raw) => { return raw.split("\n").filter((line) => line.length > 0); }); } /** * Pull streams, including start and end times, from the ringserver. * The optional matchPattern is a regular expression, so for example * '.+_JSC_00_HH.' would get all HH? channels from any station name JSC. * Result returned is an Promise. * * @param matchPattern regular expression to match * @returns promise to object with 'accessTime' as a DateTime * and 'streams' as an array of StreamStat objects. */ pullStreams(matchPattern?: string): Promise { let queryParams = ""; if (matchPattern && matchPattern.length >0) { queryParams = "match=" + matchPattern; } const url = this.formStreamsURL(queryParams); return pullText(url, this._timeoutSec).then((raw) => { const lines = raw.split("\n"); const out: StreamsResult = { accessTime: DateTime.utc(), streams: [], }; for (const line of lines) { if (line.length === 0) { continue; } const vals = line.split(/\s+/); if (vals.length === 0) { // blank line, skip continue; } else if (vals.length >= 2) { out.streams.push(new StreamStat(vals[0], vals[1], vals[2])); } else { util.log("Bad /streams line, skipping: '" + line + "'"); } } return out; }); } getDataLinkURL(): string { let proto = "ws:"; if (checkProtocol(this._protocol) === "https:") { proto = "wss:"; } return ( proto + "//" + this._host + (this._port === 80 ? "" : ":" + this._port) + this._prefix + DATALINK_PATH ); } getSeedLinkURL(): string { let proto = "ws:"; if (checkProtocol(this._protocol) === "https:") { proto = "wss:"; } return ( proto + "//" + this._host + (this._port === 80 ? "" : ":" + this._port) + this._prefix + SEEDLINK_PATH ); } /** * Forms base url from protocol, host and port. * * @returns the string url */ formBaseURL(): string { if (this._port === 0) { this._port = 80; } const port = defaultPortStringForProtocol(this._protocol, this._port); return `${checkProtocol(this._protocol)}//${this._host}${port}${this._prefix}`; } /** * Forms the ringserver id url. * * @returns the id url */ formIdURL(): string { return appendToPath(this.formBaseURL(), "id"); } /** * Forms the ringserver streams url using the query parameters. * * @param queryParams optional string of query parameters * @returns the streams url */ formStreamsURL(queryParams?: string): string { return ( appendToPath(this.formBaseURL(), "streams") + (isNonEmptyStringArg(queryParams) && queryParams.length > 0 ? "?" + queryParams : "") ); } /** * Forms the ringserver stream ids url using the query parameters. * * @param queryParams optional string of query parameters * @returns the stream ids url */ formStreamIdsURL(queryParams: string): string { return ( appendToPath(this.formBaseURL(), "streamids") + (queryParams && queryParams.length > 0 ? "?" + queryParams : "") ); } } /** * Extract one StreamStat per station from an array of channel level * StreamStats. The start and end times are the min and max times for all * the channels within the station. Can be used to get most time of most * recent packet from the stations to give an idea of current latency. * * @param streams array of channel level StreamStats * @returns array of station level StreamStats */ export function stationsFromStreams( streams: Array, ): Array { const out: Map = new Map(); for (const s of streams) { const sid = sidForId(s.key); if (sid == null || sid instanceof NetworkSourceId) { // oh well, doesn't look like a seismic channel? continue; } const staSid = sid instanceof StationSourceId ? sid : sid.stationSourceId(); const staKey = staSid.networkCode + "_" + staSid.stationCode; let stat = out.get(staKey); if (!isDef(stat)) { stat = new StreamStat(staKey, s.startRaw, s.endRaw); out.set(staKey, stat); } else { if (stat.start > s.start) { stat.start = s.start; stat.startRaw = s.startRaw; } if (stat.end < s.end) { stat.end = s.end; stat.endRaw = s.endRaw; } } } return Array.from(out.values()); } /** * Holds ringserver/datalink id split into nslc and type. * * @deprecated * @param type [description] * @param nslc [description] */ export class NslcWithType { type: string; nslc: NslcId; constructor(type: string, nslc: NslcId) { this.type = type; this.nslc = nslc; } } /** * extracts the type from a ringserver id, ie the type from * xxx/type. * @param id ringserver/datalink style id * @return type, usually MSEED, MSEED3, JSON or TEXT */ export function typeForId(id: string): string | null { const split = id.split("/"); if (split.length >= 2) { return split[split.length-1]; } return null; } /** * Split type, networkCode, stationCode, locationCode and channelCode * from a ringserver id formatted like net_sta_loc_chan/type * or FDSN:net_sta_loc_b_s_s/type for new FDSN SourceIds * * @deprecated * @param id id string to split * @returns object with the split fields */ export function nslcSplit(id: string): NslcWithType { const split = id.split("/"); if (split[0].startsWith(FDSN_PREFIX)) { const sid = parseSourceId(split[0]); if (sid instanceof FDSNSourceId) { return new NslcWithType( split[1], sid.asNslc()); } else { throw new Error("tried to split, not an FDSN SourceId: " + id); } } const nslc = split[0].split("_"); if (nslc.length === 4) { // assume net, station, loc, chan return new NslcWithType( split[1], new NslcId(nslc[0], nslc[1], nslc[2], nslc[3]), ); } else { throw new Error("tried to split, did not find 4 elements in array: " + id); } } /** * Object to hold start and end times for a key, usually channel or station. * * @param key id, usually station or channel * @param start start time * @param end end time */ export class StreamStat { key: string; startRaw: string; endRaw: string; start: DateTime; end: DateTime; constructor(key: string, start: string, end: string) { this.key = key; this.startRaw = start; this.endRaw = end; if ( this.startRaw.indexOf(".") !== -1 && this.startRaw.indexOf(".") < this.startRaw.length - 4 ) { this.startRaw = this.startRaw.substring( 0, this.startRaw.indexOf(".") + 4, ); } if (this.startRaw.charAt(this.startRaw.length - 1) !== "Z") { this.startRaw = this.startRaw + "Z"; } if ( this.endRaw.indexOf(".") !== -1 && this.endRaw.indexOf(".") < this.endRaw.length - 4 ) { this.endRaw = this.endRaw.substring(0, this.endRaw.indexOf(".") + 4); } if (this.endRaw.charAt(this.endRaw.length - 1) !== "Z") { this.endRaw = this.endRaw + "Z"; } this.start = isoToDateTime(this.startRaw); this.end = isoToDateTime(this.endRaw); this.startRaw = start; // reset to unchanged strings this.endRaw = end; } /** * Calculates latency time difference between last packet and current time. * * @param accessTime time latency is calculated relative to * @returns latency */ calcLatency(accessTime?: DateTime): Duration { if (!accessTime) accessTime = DateTime.utc(); return this.end.diff(accessTime); } }