/* * Philip Crotwell * University of South Carolina, 2019 * https://www.seis.sc.edu */ import { isDef, isStringArg, isNonEmptyStringArg, isNumArg, checkStringOrDate, reErrorWithMessage, WAY_FUTURE, doFetchWithTimeout, defaultFetchInitObj, mightBeXml, dataViewToString, XML_MIME, } from "./util"; import { Complex } from "./oregondsputil"; import { FDSNSourceId, NetworkSourceId, StationSourceId, NslcId, SourceIdSorter, } from "./fdsnsourceid"; import { DateTime, Interval } from "luxon"; export const STAXML_MIME="application/vnd.fdsn.stationxml+xml"; /** xml namespace for stationxml */ export const STAML_NS = "http://www.fdsn.org/xml/station/1"; export const COUNT_UNIT_NAME = "count"; export const FIX_INVALID_STAXML = true; export const INVALID_NUMBER = -99999; export const FAKE_START_DATE = DateTime.fromISO("1900-01-01T00:00:00Z"); /** a fake, completely empty stationxml document in case of no data. */ export const FAKE_EMPTY_XML = ' '; export const CHANNEL_CLICK_EVENT = "channelclick"; export const STATION_CLICK_EVENT = "stationclick"; export interface ChannelEventDetail { mouseevent: Event, channel: Channel } export interface StationEventDetail { mouseevent: Event, station: Station } /** * Typescript guard for channelclick CustomEvents. * @param event generic event to ensure is a CustomEvent * @return true if is correct type */ export function isChannelClickCustomEvent(event: Event): event is CustomEvent { if ('detail' in event) { const customEvent = event as CustomEvent; return "channel" in customEvent.detail; } return false; } /** * Typescript guard for stationclick CustomEvents. * @param event generic event to ensure is a CustomEvent * @return true if is correct type */ export function isStationClickCustomEvent(event: Event): event is CustomEvent { if ('detail' in event) { const customEvent = event as CustomEvent; return "station" in customEvent.detail; } return false; } /** * Utility function to create CustomEvent for clicking on a Channel, for example * in a map or table. * * @param sta Channel clicked on * @param mouseclick original mouse click Event * @returns CustomEvent populated with channel field in detail. */ export function createChannelClickEvent( sta: Channel, mouseclick: Event, ): CustomEvent { const detail: ChannelEventDetail = { mouseevent: mouseclick, channel: sta, }; return new CustomEvent(CHANNEL_CLICK_EVENT, { detail: detail, bubbles: true, cancelable: false, composed: true } ); } /** * Utility function to create CustomEvent for clicking on a Station, for example * in a map or table. * * @param sta Station clicked on * @param mouseclick original mouse click Event * @returns CustomEvent populated with station field in detail. */ export function createStationClickEvent( sta: Station, mouseclick: Event, ): CustomEvent { const detail: StationEventDetail = { mouseevent: mouseclick, station: sta, }; return new CustomEvent(STATION_CLICK_EVENT, { detail: detail, bubbles: true, cancelable: false, composed: true } ); } export interface StationClickEventMap extends HTMLElementEventMap { "stationclick": CustomEvent, } export interface ChannelClickEventMap extends HTMLElementEventMap { "channelclick": CustomEvent, } // StationXML classes export class Network { networkCode: string; _startDate: DateTime; _endDate: DateTime | null; restrictedStatus: string; description: string; totalNumberStations: number | null; stations: Array; constructor(networkCode: string) { this.networkCode = networkCode; this._startDate = FAKE_START_DATE; this._endDate = null; this.description = ""; this.restrictedStatus = ""; this.stations = []; this.totalNumberStations = null; } get sourceId(): NetworkSourceId { return new NetworkSourceId(this.networkCode ? this.networkCode : ""); } get startDate(): DateTime { return this._startDate; } set startDate(value: DateTime | string) { this._startDate = checkStringOrDate(value); } get endDate(): null | DateTime { return this._endDate; } set endDate(value: DateTime | string | null) { if (!isDef(value)) { this._endDate = null; } else { this._endDate = checkStringOrDate(value); } } get timeRange(): Interval { return createInterval(this.startDate, this.endDate); } codes(): string { return this.networkCode; } isActiveAt(d?: DateTime): boolean { if (!isDef(d)) { d = DateTime.utc(); } return this.timeRange.contains(d); } isTempNet(): boolean { const first = this.networkCode.charAt(0); return ( first === "X" || first === "Y" || first === "Z" || (first >= "0" && first <= "9") ); } } export class Station { network: Network; stationCode: string; sourceID: string | null; /** @private */ _startDate: DateTime; /** @private */ _endDate: DateTime | null; restrictedStatus: string; name: string; latitude: number; longitude: number; elevation: number; waterLevel: number | null; comments: Array; equipmentList: Array; dataAvailability: DataAvailability | null; identifierList: Array; description: string; geology: string; vault: string; channels: Array; constructor(network: Network, stationCode: string) { this.network = network; this.name = ""; this.description = ""; this.sourceID = null; this.restrictedStatus = ""; this._startDate = FAKE_START_DATE; this._endDate = null; this.stationCode = stationCode; this.channels = []; this.latitude = INVALID_NUMBER; this.longitude = INVALID_NUMBER; this.elevation = 0; this.waterLevel = null; this.comments = []; this.equipmentList = []; this.dataAvailability = null; this.geology = ""; this.vault = ""; this.identifierList = []; } get sourceId(): StationSourceId { return new StationSourceId( this.networkCode ? this.networkCode : "", this.stationCode ? this.stationCode : "", ); } get startDate(): DateTime { return this._startDate; } set startDate(value: DateTime | string) { this._startDate = checkStringOrDate(value); } get endDate(): DateTime | null { return this._endDate; } set endDate(value: DateTime | string | null) { if (!isDef(value)) { this._endDate = null; } else { this._endDate = checkStringOrDate(value); } } get timeRange(): Interval { return createInterval(this.startDate, this.endDate); } get networkCode(): string { return this.network.networkCode; } isActiveAt(d?: DateTime): boolean { if (!isDef(d)) { d = DateTime.utc(); } return this.timeRange.contains(d); } codes(sep = "."): string { return this.network.codes() + sep + this.stationCode; } } export class Channel { station: Station; /** @private */ _locationCode: string; channelCode: string; /** @private */ _sourceId: FDSNSourceId | undefined; /** @private */ _startDate: DateTime; /** @private */ _endDate: DateTime | null; restrictedStatus: string; latitude: number; longitude: number; elevation: number; depth: number; azimuth: number; dip: number; sampleRate: number; waterLevel: number | null = null; comments: Array = []; equipmentList: Array = []; dataAvailability: DataAvailability | null = null; identifierList: Array = []; description: string = ""; response: Response | null; sensor: Equipment | null; preamplifier: Equipment | null; datalogger: Equipment | null; constructor(station: Station, channelCode: string, locationCode: string) { this.station = station; this._startDate = FAKE_START_DATE; this._endDate = null; this.response = null; this.sensor = null; this.preamplifier = null; this.datalogger = null; this.restrictedStatus = ""; this.azimuth = INVALID_NUMBER; this.dip = INVALID_NUMBER; this.latitude = INVALID_NUMBER; this.longitude = INVALID_NUMBER; this.depth = 0; this.elevation = 0; this.sampleRate = 0; if (channelCode.length !== 3 && (channelCode.split('_').length !== 3)) { throw new Error(`Channel code must be 3 chars or of form b_s_s: "${channelCode} ${channelCode.split('_').length}"`); } this.channelCode = channelCode; this._locationCode = locationCode; if (!locationCode) { // make sure "null" is encoded as empty string this._locationCode = ""; } if (!(this._locationCode.length === 2 || this._locationCode.length === 0)) { throw new Error( `locationCode must be 2 chars, or empty: "${locationCode}"`, ); } } get sourceId(): FDSNSourceId { if (this._sourceId) { return this._sourceId; } return FDSNSourceId.fromNslc( this.networkCode, this.stationCode, this.locationCode, this.channelCode, ); } set sourceId(sid: FDSNSourceId) { this._sourceId = sid; } get nslcId(): NslcId { return new NslcId( this.networkCode ? this.networkCode : "", this.stationCode ? this.stationCode : "", this.locationCode && this.locationCode !== "--" ? this.locationCode : "", this.channelCode ? this.channelCode : "", ); } get startDate(): DateTime { return this._startDate; } set startDate(value: DateTime | string) { this._startDate = checkStringOrDate(value); } get endDate(): null | DateTime { return this._endDate; } set endDate(value: DateTime | string | null) { if (!isDef(value)) { this._endDate = null; } else { this._endDate = checkStringOrDate(value); } } get timeRange(): Interval { return createInterval(this.startDate, this.endDate); } get locationCode(): string { return this._locationCode; } set locationCode(value: string) { this._locationCode = value; if (!value) { // make sure "null" is encoded as empty string this._locationCode = ""; } } get stationCode(): string { return this.station.stationCode; } get networkCode(): string { return this.station.networkCode; } /** * Checks if this channel has sensitivity defined, within the response. * * @returns true if instrumentSensitivity exits */ hasInstrumentSensitivity(): boolean { return isDef(this.response) && isDef(this.response.instrumentSensitivity); } set instrumentSensitivity(value: InstrumentSensitivity) { if (!isDef(this.response)) { this.response = new Response(value); } else { this.response.instrumentSensitivity = value; } } get instrumentSensitivity(): InstrumentSensitivity { if (isDef(this.response) && isDef(this.response.instrumentSensitivity)) { return this.response.instrumentSensitivity; } else { throw new Error("no Response or InstrumentSensitivity defined"); } } /** * return network, station, location and channels codes as one string. * * @returns net.sta.loc.chan */ get nslc(): string { return this.codes(); } /** * return network, station, location and channels codes as one string. * * @param sep separator, defaults to dot '.' * @returns net.sta.loc.chan */ codes(sep = "."): string { return ( this.station.codes(sep) + sep + this.locationCode + sep + this.channelCode ); } isActiveAt(d?: DateTime): boolean { if (!isDef(d)) { d = DateTime.utc(); } return this.timeRange.contains(d); } } export class InstrumentSensitivity { sensitivity: number; frequency: number; inputUnits: string; outputUnits: string; constructor( sensitivity: number, frequency: number, inputUnits: string, outputUnits: string, ) { this.sensitivity = sensitivity; this.frequency = frequency; this.inputUnits = inputUnits; this.outputUnits = outputUnits; } } export class Equipment { resourceId: string; type: string; description: string; manufacturer: string; vendor: string; model: string; serialNumber: string; installationDate: DateTime | null; removalDate: DateTime | null; calibrationDateList: Array; constructor() { this.resourceId = ""; this.type = ""; this.description = ""; this.manufacturer = ""; this.vendor = ""; this.model = ""; this.serialNumber = ""; this.installationDate = null; this.removalDate = null; this.calibrationDateList = []; } } export class Response { instrumentSensitivity: InstrumentSensitivity | null; stages: Array; constructor( instrumentSensitivity?: InstrumentSensitivity, stages?: Array, ) { if (instrumentSensitivity) { this.instrumentSensitivity = instrumentSensitivity; } else { this.instrumentSensitivity = null; } if (stages) { this.stages = stages; } else { this.stages = []; } } } export class Stage { filter: AbstractFilterType | null; decimation: Decimation | null; gain: Gain; constructor( filter: AbstractFilterType | null, decimation: Decimation | null, gain: Gain, ) { this.filter = filter; this.decimation = decimation; this.gain = gain; } } export class AbstractFilterType { inputUnits: string; outputUnits: string; name: string; description: string; constructor(inputUnits: string, outputUnits: string) { this.inputUnits = inputUnits; this.outputUnits = outputUnits; this.description = ""; this.name = ""; } } export class PolesZeros extends AbstractFilterType { pzTransferFunctionType: string; normalizationFactor: number; normalizationFrequency: number; zeros: Array>; poles: Array>; constructor(inputUnits: string, outputUnits: string) { super(inputUnits, outputUnits); this.pzTransferFunctionType = ""; this.normalizationFactor = 1; this.normalizationFrequency = 0; this.zeros = new Array>(0); this.poles = new Array>(0); } } export class FIR extends AbstractFilterType { symmetry: string; numerator: Array; constructor(inputUnits: string, outputUnits: string) { super(inputUnits, outputUnits); this.symmetry = "none"; this.numerator = [1]; } } export class CoefficientsFilter extends AbstractFilterType { cfTransferFunction: string; numerator: Array; denominator: Array; constructor(inputUnits: string, outputUnits: string) { super(inputUnits, outputUnits); this.cfTransferFunction = ""; this.numerator = [1]; this.denominator = new Array(0); } } export class Decimation { inputSampleRate: number; factor: number; offset: number | null | undefined; delay: number | null | undefined; correction: number | null | undefined; constructor(inputSampleRate: number, factor: number) { this.inputSampleRate = inputSampleRate; this.factor = factor; } } export class Gain { value: number; frequency: number; constructor(value: number, frequency: number) { this.value = value; this.frequency = frequency; } } export class Span { interval: Interval; numberSegments = 0; maximumTimeTear: number | null; constructor(interval: Interval) { this.maximumTimeTear = null; this.interval = interval; } } export class DataAvailability { extent: Interval | null; spanList: Array; constructor() { this.extent = null; this.spanList = []; } } export class Comment { id: string | null = null; subject: string | null = null; value: string; beginEffectiveTime: DateTime | null = null; endEffectiveTime: DateTime | null = null; authorList: Array = []; constructor(value: string) { this.value = value; } } export class Author { name: string | null = null; agency: string | null = null; email: string | null = null; phone: string | null = null; } /** * Parses the FDSN StationXML returned from a query. * * @param rawXml parsed xml to extract objects from * @returns an Array of Network objects. */ export function parseStationXml(rawXml: Document): Array { const top = rawXml.documentElement; if (!top) { throw new Error("No documentElement in XML"); } const netArray = Array.from(top.getElementsByTagNameNS(STAML_NS, "Network")); const out = []; for (const n of netArray) { out.push(convertToNetwork(n)); } return out; } /** * Parses a FDSNStationXML Network xml element into a Network object. * * @param xml the network xml Element * @returns Network instance */ export function convertToNetwork(xml: Element): Network { let netCode = ""; try { netCode = _requireAttribute(xml, "code"); const out = new Network(netCode); out.startDate = _requireAttribute(xml, "startDate"); const rs = _grabAttribute(xml, "restrictedStatus"); if (isNonEmptyStringArg(rs)) { out.restrictedStatus = rs; } const desc = _grabFirstElText(xml, "Description"); if (isNonEmptyStringArg(desc)) { out.description = desc; } if (_grabAttribute(xml, "endDate")) { out.endDate = _grabAttribute(xml, "endDate"); } const totSta = xml.getElementsByTagNameNS(STAML_NS, "TotalNumberStations"); if (totSta && totSta.length > 0) { out.totalNumberStations = _grabFirstElInt(xml, "TotalNumberStations"); } const staArray = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Station"), ); const stations = []; for (const s of staArray) { stations.push(convertToStation(out, s)); } out.stations = stations; return out; } catch (err) { throw reErrorWithMessage(err, netCode); } } /** * Parses a FDSNStationXML Station xml element into a Station object. * * @param network the containing network * @param xml the station xml Element * @returns Station instance */ export function convertToStation(network: Network, xml: Element): Station { let staCode = ""; // so can use in rethrow exception try { staCode = _requireAttribute(xml, "code"); if (!isNonEmptyStringArg(staCode)) { throw new Error("station code missing in station!"); } const out = new Station(network, staCode); out.startDate = _requireAttribute(xml, "startDate"); const rs = _grabAttribute(xml, "restrictedStatus"); if (isNonEmptyStringArg(rs)) { out.restrictedStatus = rs; } const lat = _grabFirstElFloat(xml, "Latitude"); if (isNumArg(lat)) { out.latitude = lat; } const lon = _grabFirstElFloat(xml, "Longitude"); if (isNumArg(lon)) { out.longitude = lon; } const elev = _grabFirstElFloat(xml, "Elevation"); if (isNumArg(elev)) { out.elevation = elev; } const waterLevel = _grabFirstElFloat(xml, "WaterLevel"); if (isNumArg(waterLevel)) { out.waterLevel = waterLevel; } const vault = _grabFirstElText(xml, "Vault"); if (isStringArg(vault)) { out.vault = vault; } const geology = _grabFirstElText(xml, "Geology"); if (isStringArg(geology)) { out.geology = geology; } const name = _grabFirstElText(_grabFirstEl(xml, "Site"), "Name"); if (isStringArg(name)) { out.name = name; } const endDate = _grabAttribute(xml, "endDate"); if (isDef(endDate)) { out.endDate = _grabAttribute(xml, "endDate"); } const description = _grabFirstElText(xml, "Description"); if (isDef(description)) { out.description = description; } const identifierList = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Identifier"), ); out.identifierList = identifierList.map((el) => { return el.textContent ? el.textContent : ""; }); const dataAvailEl = _grabFirstEl(xml, "DataAvailability"); if (isDef(dataAvailEl)) { out.dataAvailability = convertToDataAvailability(dataAvailEl); } const commentArray = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Comment"), ); const comments = []; for (const c of commentArray) { comments.push(convertToComment(c)); } out.comments = comments; const equipmentArray = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Equipment"), ); const equipmentList = []; for (const c of equipmentArray) { equipmentList.push(convertToEquipment(c)); } out.equipmentList = equipmentList; const chanArray = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Channel"), ); const channels = []; for (const c of chanArray) { channels.push(convertToChannel(out, c)); } out.channels = channels; return out; } catch (err) { throw reErrorWithMessage(err, staCode); } } /** * Parses a FDSNStationXML Channel xml element into a Channel object. * * @param station the containing staton * @param xml the channel xml Element * @returns Channel instance */ export function convertToChannel(station: Station, xml: Element): Channel { let locCode: string | null = ""; // so can use in rethrow exception const chanCode = ""; try { locCode = _grabAttribute(xml, "locationCode"); if (!isNonEmptyStringArg(locCode)) { locCode = ""; } const chanCode = _requireAttribute(xml, "code"); const out = new Channel(station, chanCode, locCode); out.startDate = checkStringOrDate(_requireAttribute(xml, "startDate")); const rs = _grabAttribute(xml, "restrictedStatus"); if (isNonEmptyStringArg(rs)) { out.restrictedStatus = rs; } const lat = _grabFirstElFloat(xml, "Latitude"); if (isNumArg(lat)) { out.latitude = lat; } const lon = _grabFirstElFloat(xml, "Longitude"); if (isNumArg(lon)) { out.longitude = lon; } const elev = _grabFirstElFloat(xml, "Elevation"); if (isNumArg(elev)) { out.elevation = elev; } const depth = _grabFirstElFloat(xml, "Depth"); if (isNumArg(depth)) { out.depth = depth; } const waterLevel = _grabFirstElFloat(xml, "WaterLevel"); if (isNumArg(waterLevel)) { out.waterLevel = waterLevel; } const azimuth = _grabFirstElFloat(xml, "Azimuth"); if (isNumArg(azimuth)) { out.azimuth = azimuth; } const dip = _grabFirstElFloat(xml, "Dip"); if (isNumArg(dip)) { out.dip = dip; } const desc = _grabFirstElText(xml, "Description"); if (desc) { out.description = desc; } const sampleRate = _grabFirstElFloat(xml, "SampleRate"); if (isNumArg(sampleRate)) { out.sampleRate = sampleRate; } if (_grabAttribute(xml, "endDate")) { out.endDate = _grabAttribute(xml, "endDate"); } const sensor = xml.getElementsByTagNameNS(STAML_NS, "Sensor"); if (sensor && sensor.length > 0) { const sensorTmp = sensor.item(0); if (isDef(sensorTmp)) { out.sensor = convertToEquipment(sensorTmp); } } const preamp = xml.getElementsByTagNameNS(STAML_NS, "PreAmplifier"); if (preamp && preamp.length > 0) { const preampTmp = sensor.item(0); if (isDef(preampTmp)) { out.preamplifier = convertToEquipment(preampTmp); } } const datalogger = xml.getElementsByTagNameNS(STAML_NS, "DataLogger"); if (datalogger && datalogger.length > 0) { const dataloggerTmp = sensor.item(0); if (isDef(dataloggerTmp)) { out.datalogger = convertToEquipment(dataloggerTmp); } } const description = _grabFirstElText(xml, "Description"); if (isDef(description)) { out.description = description; } const identifierList = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Identifier"), ); out.identifierList = identifierList.map((el) => { return el.textContent ? el.textContent : ""; }); const dataAvailEl = _grabFirstEl(xml, "DataAvailability"); if (isDef(dataAvailEl)) { out.dataAvailability = convertToDataAvailability(dataAvailEl); } const commentArray = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Comment"), ); const comments = []; for (const c of commentArray) { comments.push(convertToComment(c)); } out.comments = comments; const equipmentArray = Array.from( xml.getElementsByTagNameNS(STAML_NS, "Equipment"), ); const equipmentList = []; for (const c of equipmentArray) { equipmentList.push(convertToEquipment(c)); } out.equipmentList = equipmentList; const responseXml = xml.getElementsByTagNameNS(STAML_NS, "Response"); if (responseXml && responseXml.length > 0) { const r = responseXml.item(0); if (r) { out.response = convertToResponse(r); } } return out; } catch (err) { throw reErrorWithMessage(err, `${locCode}.${chanCode}`); } } export function convertToDataAvailability(xml: Element): DataAvailability { const out = new DataAvailability(); const extent = _grabFirstEl(xml, "Extent"); if (extent && "start" in extent && "end" in extent) { const s = _grabAttribute(extent, "start"); const e = _grabAttribute(extent, "end"); if (s && e) { out.extent = Interval.fromDateTimes( DateTime.fromISO(s), DateTime.fromISO(e), ); } } const spanArray = Array.from(xml.getElementsByTagNameNS(STAML_NS, "Span")); const spanList = []; for (const c of spanArray) { const s = _grabAttribute(c, "start"); const e = _grabAttribute(c, "end"); if (s && e) { const span = new Span( Interval.fromDateTimes(DateTime.fromISO(s), DateTime.fromISO(e)), ); const numSeg = _grabAttribute(c, "numberSegments"); if (numSeg) { span.numberSegments = parseInt(numSeg); } const maxTear = _grabAttribute(c, "maximumTimeTear"); if (maxTear) { span.maximumTimeTear = parseFloat(maxTear); } spanList.push(span); } } out.spanList = spanList; return out; } export function convertToComment(xml: Element): Comment { let val = _grabFirstElText(xml, "Value"); if (!val) { val = ""; } const out = new Comment(val); const id = _grabAttribute(xml, "id"); if (id) { out.id = id; } const subject = _grabAttribute(xml, "subject"); if (subject) { out.subject = subject; } const b = _grabFirstElText(xml, "BeginEffectiveTime"); if (b) { out.beginEffectiveTime = DateTime.fromISO(b); } const e = _grabFirstElText(xml, "EndEffectiveTime"); if (e) { out.endEffectiveTime = DateTime.fromISO(e); } const authList = Array.from(xml.getElementsByTagNameNS(STAML_NS, "Author")); out.authorList = authList.map((aEl) => convertToAuthor(aEl)); return out; } export function convertToAuthor(xml: Element): Author { const out = new Author(); const name = _grabFirstElText(xml, "Name"); if (name) { out.name = name; } const agency = _grabFirstElText(xml, "Agency"); if (agency) { out.agency = agency; } const phEl = _grabFirstEl(xml, "Phone"); if (phEl) { out.phone = `${_grabFirstElText(phEl, "CountryCode")}-${_grabFirstElText(phEl, "AreaCode")}-${_grabFirstElText(phEl, "PhoneNumber")}`; } return out; } export function convertToEquipment(xml: Element): Equipment { const out = new Equipment(); let val; val = _grabFirstElText(xml, "Type"); if (isNonEmptyStringArg(val)) { out.type = val; } val = _grabFirstElText(xml, "Description"); if (isNonEmptyStringArg(val)) { out.description = val; } val = _grabFirstElText(xml, "Manufacturer"); if (isNonEmptyStringArg(val)) { out.manufacturer = val; } val = _grabFirstElText(xml, "Vendor"); if (isNonEmptyStringArg(val)) { out.vendor = val; } val = _grabFirstElText(xml, "Model"); if (isNonEmptyStringArg(val)) { out.model = val; } val = _grabFirstElText(xml, "SerialNumber"); if (isNonEmptyStringArg(val)) { out.serialNumber = val; } val = _grabFirstElText(xml, "InstallationDate"); if (isNonEmptyStringArg(val)) { out.installationDate = checkStringOrDate(val); } val = _grabFirstElText(xml, "RemovalDate"); if (isNonEmptyStringArg(val)) { out.removalDate = checkStringOrDate(val); } const calibXml = Array.from( xml.getElementsByTagNameNS(STAML_NS, "CalibrationDate"), ); out.calibrationDateList = []; for (const cal of calibXml) { if (isDef(cal.textContent)) { const d = checkStringOrDate(cal.textContent); if (isDef(d)) { out.calibrationDateList.push(d); } } } return out; } /** * Parses a FDSNStationXML Response xml element into a Response object. * * @param responseXml the response xml Element * @returns Response instance */ export function convertToResponse(responseXml: Element): Response { let out = new Response(); const inst = responseXml.getElementsByTagNameNS( STAML_NS, "InstrumentSensitivity", ); if (inst && inst.item(0)) { const i = inst.item(0); if (i) { out = new Response(convertToInstrumentSensitivity(i)); } } if (!isDef(out)) { // DMC returns empty response element when they know nothing (instead // of just leaving it out). Return empty object in this case out = new Response(); } const xmlStages = responseXml.getElementsByTagNameNS(STAML_NS, "Stage"); if (xmlStages && xmlStages.length > 0) { const jsStages = Array.from(xmlStages).map(function (stageXml) { return convertToStage(stageXml); }); out.stages = jsStages; } return out; } /** * Parses a FDSNStationXML InstrumentSensitivity xml element into a InstrumentSensitivity object. * * @param xml the InstrumentSensitivity xml Element * @returns InstrumentSensitivity instance */ export function convertToInstrumentSensitivity( xml: Element, ): InstrumentSensitivity { const sensitivity = _grabFirstElFloat(xml, "Value"); const frequency = _grabFirstElFloat(xml, "Frequency"); const inputUnits = _grabFirstElText(_grabFirstEl(xml, "InputUnits"), "Name"); let outputUnits = _grabFirstElText(_grabFirstEl(xml, "OutputUnits"), "Name"); if (FIX_INVALID_STAXML && !isDef(outputUnits)) { // assume last output unit is count? outputUnits = COUNT_UNIT_NAME; } if ( !( isDef(sensitivity) && isDef(frequency) && isDef(inputUnits) && isDef(outputUnits) ) ) { throw new Error( `Not all elements of Sensitivity exist: ${sensitivity} ${frequency} ${inputUnits} ${outputUnits}`, ); } return new InstrumentSensitivity( sensitivity, frequency, inputUnits, outputUnits, ); } /** * Parses a FDSNStationXML Stage xml element into a Stage object. * * @param stageXml the Stage xml Element * @returns Stage instance */ export function convertToStage(stageXml: Element): Stage { const subEl = stageXml.firstElementChild; let filter: AbstractFilterType | null = null; if (!subEl) { throw new Error("Stage element has no child elements"); } else if ( stageXml.childElementCount === 1 && subEl.localName === "StageGain" ) { // degenerate case of a gain only stage // fix the lack of units after all stages are converted. } else { // shoudl be a filter of some kind, check for units const inputUnits = _grabFirstElText( _grabFirstEl(subEl, "InputUnits"), "Name", ); const outputUnits = _grabFirstElText( _grabFirstEl(subEl, "OutputUnits"), "Name", ); if (!isNonEmptyStringArg(inputUnits)) { throw new Error("Stage inputUnits required"); } if (!isNonEmptyStringArg(outputUnits)) { throw new Error("Stage outputUnits required"); } // here we assume there must be a filter, and so must have units if (subEl.localName === "PolesZeros") { const pzFilter = new PolesZeros(inputUnits, outputUnits); const pzt = _grabFirstElText(subEl, "PzTransferFunctionType"); if (isNonEmptyStringArg(pzt)) { pzFilter.pzTransferFunctionType = pzt; } const nfa = _grabFirstElFloat(subEl, "NormalizationFactor"); if (isNumArg(nfa)) { pzFilter.normalizationFactor = nfa; } const nfr = _grabFirstElFloat(subEl, "NormalizationFrequency"); if (isNumArg(nfr)) { pzFilter.normalizationFrequency = nfr; } const zeros = Array.from( subEl.getElementsByTagNameNS(STAML_NS, "Zero"), ).map(function (zeroEl) { return extractComplex(zeroEl); }); const poles = Array.from( subEl.getElementsByTagNameNS(STAML_NS, "Pole"), ).map(function (poleEl) { return extractComplex(poleEl); }); pzFilter.zeros = zeros; pzFilter.poles = poles; filter = pzFilter; } else if (subEl.localName === "Coefficients") { const coeffXml = subEl; const cFilter = new CoefficientsFilter(inputUnits, outputUnits); const cft = _grabFirstElText(coeffXml, "CfTransferFunctionType"); if (isNonEmptyStringArg(cft)) { cFilter.cfTransferFunction = cft; } cFilter.numerator = Array.from( coeffXml.getElementsByTagNameNS(STAML_NS, "Numerator"), ) .map(function (numerEl) { return isNonEmptyStringArg(numerEl.textContent) ? parseFloat(numerEl.textContent) : null; }) .filter(isDef); cFilter.denominator = Array.from( coeffXml.getElementsByTagNameNS(STAML_NS, "Denominator"), ) .map(function (denomEl) { return isNonEmptyStringArg(denomEl.textContent) ? parseFloat(denomEl.textContent) : null; }) .filter(isDef); filter = cFilter; } else if (subEl.localName === "ResponseList") { throw new Error("ResponseList not supported: "); } else if (subEl.localName === "FIR") { const firXml = subEl; const firFilter = new FIR(inputUnits, outputUnits); const s = _grabFirstElText(firXml, "Symmetry"); if (isNonEmptyStringArg(s)) { firFilter.symmetry = s; } firFilter.numerator = Array.from( firXml.getElementsByTagNameNS(STAML_NS, "NumeratorCoefficient"), ) .map(function (numerEl) { return isNonEmptyStringArg(numerEl.textContent) ? parseFloat(numerEl.textContent) : null; }) .filter(isDef); filter = firFilter; } else if (subEl.localName === "Polynomial") { throw new Error("Polynomial not supported: "); } else if (subEl.localName === "StageGain") { // gain only stage, pick it up below } else { throw new Error("Unknown Stage type: " + subEl.localName); } if (filter) { // add description and name if it was there const description = _grabFirstElText(subEl, "Description"); if (isNonEmptyStringArg(description)) { filter.description = description; } if (subEl.hasAttribute("name")) { const n = _grabAttribute(subEl, "name"); if (isNonEmptyStringArg(n)) { filter.name = n; } } } } const decimationXml = _grabFirstEl(stageXml, "Decimation"); let decimation: Decimation | null = null; if (decimationXml) { decimation = convertToDecimation(decimationXml); } const gainXml = _grabFirstEl(stageXml, "StageGain"); let gain = null; if (gainXml) { gain = convertToGain(gainXml); } else { // stage has no element, but gain is required in schema? // just use unity gain at 1 Hz, weird but less bad than an exception? gain = new Gain(1, 1); } const out = new Stage(filter, decimation, gain); return out; } /** * Parses a FDSNStationXML Decimation xml element into a Decimation object. * * @param decXml the Decimation xml Element * @returns Decimation instance */ export function convertToDecimation(decXml: Element): Decimation { let out: Decimation; const insr = _grabFirstElFloat(decXml, "InputSampleRate"); const fac = _grabFirstElInt(decXml, "Factor"); if (isNumArg(insr) && isNumArg(fac)) { out = new Decimation(insr, fac); } else { throw new Error( `Decimation without InputSampleRate and Factor: ${insr} ${fac}`, ); } out.offset = _grabFirstElInt(decXml, "Offset"); out.delay = _grabFirstElFloat(decXml, "Delay"); out.correction = _grabFirstElFloat(decXml, "Correction"); return out; } /** * Parses a FDSNStationXML Gain xml element into a Gain object. * * @param gainXml the Gain xml Element * @returns Gain instance */ export function convertToGain(gainXml: Element): Gain { let out: Gain; const v = _grabFirstElFloat(gainXml, "Value"); const f = _grabFirstElFloat(gainXml, "Frequency"); if (isNumArg(v) && isNumArg(f)) { out = new Gain(v, f); } else { throw new Error(`Gain does not have value and frequency: ${v} ${f}`); } return out; } export function createInterval( start: DateTime, end: null | DateTime, ): Interval { if (end) { return Interval.fromDateTimes(start, end); } else { return Interval.fromDateTimes(start, WAY_FUTURE); } } /** * Extracts a complex number from an stationxml element. * * @param el xml element * @returns Complex instance */ export function extractComplex(el: Element): InstanceType { const re = _grabFirstElFloat(el, "Real"); const im = _grabFirstElFloat(el, "Imaginary"); if (isNumArg(re) && isNumArg(im)) { return new Complex(re, im); } else { throw new Error(`Both Real and Imaginary required: ${re} ${im}`); } } /** * Generator function to access all active stations within all networks in the array. * * @param networks array of Networks * @param atTime time for station to be active, defaults to now * @yields generator yeiding stations */ export function* activeNetworks( networks: Array, atTime?: DateTime ): Generator { for (const n of networks) { if (n.isActiveAt(atTime)) { yield n; } } } /** * Generator function to access all stations within all networks in the array. * * @param networks array of Networks * @yields generator yeiding stations */ export function* allStations( networks: Array, ): Generator { for (const n of networks) { for (const s of n.stations) { yield s; } } } /** * Generator function to access all active stations within all networks in the array. * * @param networks array of Networks * @param atTime time for station to be active, defaults to now * @yields generator yeiding stations */ export function* activeStations( networks: Array, atTime?: DateTime ): Generator { for (const s of allStations(networks)) { if (s.isActiveAt(atTime)) { yield s; } } } /** * Generator function to access all channels within all stations * within all networks in the array. * * @param networks array of Networks * @yields generator yeiding channels */ export function* allChannels( networks: Array, ): Generator { for (const s of allStations(networks)) { for (const c of s.channels) { yield c; } } } /** * Generator function to access all active channels within all networks in the array. * * @param networks array of Networks * @param atTime time for channel to be active, defaults to now * @yields generator yeiding channels */ export function* activeChannels( networks: Array, atTime?: DateTime ): Generator { for (const c of allChannels(networks)) { if (c.isActiveAt(atTime)) { yield c; } } } /** * Extract all channels from all stations from all networks in the input array. * Regular expressions may be used instead of exact code matchs. * * @param networks Array of networks. * @param netCode network code to match, defaults to .* * @param staCode station code to match, defaults to .* * @param locCode location code to match, defaults to .* * @param chanCode channel code to match, defaults to .* * @yields Array of channels. */ export function* findChannels( networks: Array, netCode: string = ".*", staCode: string = ".*", locCode: string = ".*", chanCode: string = ".*" ): Generator { if (netCode.length == 0) {netCode = ".*";} const netRE = new RegExp(`^${netCode}$`); if (staCode.length == 0) {staCode = ".*";} const staRE = new RegExp(`^${staCode}$`); if (locCode.length == 0) {locCode = ".*";} const locRE = new RegExp(`^${locCode}$`); if (chanCode.length == 0) {chanCode = ".*";} const chanRE = new RegExp(`^${chanCode}$`); for (const n of networks.filter((n) => netRE.test(n.networkCode))) { for (const s of n.stations.filter((s) => staRE.test(s.stationCode))) { for (const c of s.channels.filter( (c) => locRE.test(c.locationCode) && chanRE.test(c.channelCode), )) { yield c; } } } } export function* findChannelsForSourceId( networks: Array, sid: FDSNSourceId, ): Generator { const netRE = new RegExp(`^${sid.networkCode}$`); const staRE = new RegExp(`^${sid.stationCode}$`); const locRE = new RegExp(`^${sid.locationCode}$`); const bandRE = new RegExp(`^${sid.bandCode}$`); const sourceRE = new RegExp(`^${sid.sourceCode}$`); const subsourceRE = new RegExp(`^${sid.subsourceCode}$`); for (const n of networks.filter((n) => netRE.test(n.networkCode))) { for (const s of n.stations.filter((s) => staRE.test(s.stationCode))) { for (const c of s.channels.filter( (c) => locRE.test(c.locationCode), )) { const chanSid = c.sourceId; if (bandRE.test(chanSid.bandCode) && sourceRE.test(chanSid.sourceCode) && subsourceRE.test(chanSid.subsourceCode)) { yield c; } } } } } export function uniqueSourceIds( channelList: Iterable, ): Array { const out = new Map(); for (const c of channelList) { if (c) { out.set(c.sourceId.toString(), c.sourceId); } } return Array.from(out.values()).sort(SourceIdSorter); } export function uniqueStations(channelList: Iterable): Array { const out = new Set(); for (const c of channelList) { if (c) { out.add(c.station); } } return Array.from(out.values()); } export function uniqueNetworks(channelList: Iterable): Array { const out = new Set(); for (const c of channelList) { if (c) { if (c instanceof Station) { out.add(c.network); } else if (c instanceof Channel) { out.add(c.station.network); } else { throw new Error(`unknown type for uniqueNetworks: ${c}`); } } } return Array.from(out.values()); } /** * Fetches and parses StationXML from a URL. This can be used in instances where * a static stationXML file is available on a web site instead of via a web * service with query paramters. * @param url the url to download from * @param timeoutSec timeout in case of failed connection * @param nodata http code for no data * @returns Promise to parsed StationXML as an Network array */ export function fetchStationXml( url: string | URL, timeoutSec = 10, nodata = 204, ): Promise> { const fetchInit = defaultFetchInitObj(XML_MIME); return doFetchWithTimeout(url, fetchInit, timeoutSec * 1000) .then((response) => { if (response.status === 200) { return response.text(); } else if ( response.status === 204 || (isDef(nodata) && response.status === nodata) ) { // 204 is nodata, so successful but empty return FAKE_EMPTY_XML; } else { throw new Error(`Status not successful: ${response.status}`); } }) .then(function (rawXmlText) { return new DOMParser().parseFromString(rawXmlText, XML_MIME); }) .then((rawXml) => { return parseStationXml(rawXml); }); } export function mightBeStatonXml(buf: ArrayBufferLike) { if ( ! mightBeXml(buf)) { return false; } const initialChars = dataViewToString(new DataView(buf.slice(0, 100))).trimStart(); if ( ! initialChars.includes("FDSNStationXML")) { return false; } return true; } // these are similar methods as in seisplotjs.quakeml // duplicate here to avoid dependency and diff NS, yes that is dumb... const _grabFirstEl = function ( xml: Element | null | void, tagName: string, ): Element | null { if (xml instanceof Element) { const elList = Array.from(xml.children).filter( (e) => e.tagName === tagName, ); if (elList.length > 0) { const e = elList[0]; if (e) { return e; } } } return null; }; const _grabFirstElText = function _grabFirstElText( xml: Element | null | void, tagName: string, ): string | null { let out = null; const el = _grabFirstEl(xml, tagName); if (el instanceof Element) { out = el.textContent; } return out; }; const _grabFirstElFloat = function _grabFirstElFloat( xml: Element | null | void, tagName: string, ): number | null { let out = null; const elText = _grabFirstElText(xml, tagName); if (isStringArg(elText)) { out = parseFloat(elText); } return out; }; const _grabFirstElInt = function _grabFirstElInt( xml: Element | null | void, tagName: string, ): number | null { let out = null; const elText = _grabFirstElText(xml, tagName); if (isStringArg(elText)) { out = parseInt(elText); } return out; }; const _grabAttribute = function _grabAttribute( xml: Element | null | void, tagName: string, ): string | null { let out = null; if (xml instanceof Element) { const a = xml.getAttribute(tagName); if (isStringArg(a)) { out = a; } } return out; }; const _requireAttribute = function _requireAttribute( xml: Element | null | void, tagName: string, ): string { const out = _grabAttribute(xml, tagName); if (typeof out !== "string") { throw new Error(`Attribute ${tagName} not found.`); } return out; }; const _grabAttributeNS = function ( xml: Element | null | void, namespace: string, tagName: string, ): string | null { let out = null; if (xml instanceof Element) { const a = xml.getAttributeNS(namespace, tagName); if (isStringArg(a)) { out = a; } } return out; }; export const parseUtil = { _grabFirstEl: _grabFirstEl, _grabFirstElText: _grabFirstElText, _grabFirstElFloat: _grabFirstElFloat, _grabFirstElInt: _grabFirstElInt, _grabAttribute: _grabAttribute, _requireAttribute: _requireAttribute, _grabAttributeNS: _grabAttributeNS, };