/*
* Philip Crotwell
* University of South Carolina, 2019
* https://www.seis.sc.edu
*/
import { Station, Channel } from "./stationxml";
import {
isDef,
isObject,
isStringArg,
isNonEmptyStringArg,
isoToDateTime,
stringify,
doFetchWithTimeout,
defaultFetchInitObj,
mightBeXml,
dataViewToString,
XML_MIME,
warn,
} from "./util";
import { latlonFormat, depthFormat, magFormat } from "./textformat";
import { DateTime } from "luxon";
export const QML_NS = "http://quakeml.org/xmlns/quakeml/1.2";
export const BED_NS = "http://quakeml.org/xmlns/bed/1.2";
export const IRIS_NS = "http://service.iris.edu/fdsnws/event/1/";
export const ANSS_NS = "http://anss.org/xmlns/event/0.1";
export const ANSS_CATALOG_NS = "http://anss.org/xmlns/catalog/0.1";
export const USGS_HOST = "earthquake.usgs.gov";
export const UNKNOWN_MAG_TYPE = "unknown";
export const UNKNOWN_PUBLIC_ID = "unknownId";
export const FAKE_ORIGIN_TIME = DateTime.fromISO("1900-01-01T00:00:00Z");
export const FAKE_EMPTY_XML =
'';
export const QUAKE_CLICK_EVENT = "quakeclick";
export interface QuakeEventDetail {
mouseevent: Event,
quake: Quake
}
/**
* Typescript guard for quakeclick CustomEvents.
* @param event generic event to ensure is a CustomEvent
* @return true if is correct type
*/
export function isQuakeClickCustomEvent(event: Event): event is CustomEvent {
if ('detail' in event) {
const customEvent = event as CustomEvent;
return "quake" in customEvent.detail;
}
return false;
}
/**
* Utility function to create CustomEvent for clicking on a Quake, for example
* in a map or table.
*
* @param q Quake clicked on
* @param mouseclick original mouse click Event
* @returns CustomEvent populated with quake field in detail.
*/
export function createQuakeClickEvent(
q: Quake,
mouseclick: Event,
): CustomEvent {
const detail: QuakeEventDetail = {
mouseevent: mouseclick,
quake: q,
};
return new CustomEvent(QUAKE_CLICK_EVENT,
{ detail: detail,
bubbles: true,
cancelable: false,
composed: true
}
);
}
export interface QuakeClickEventMap extends HTMLElementEventMap {
"quakeclick": CustomEvent,
}
// QuakeML classes
class BaseElement {
publicId: string = UNKNOWN_PUBLIC_ID;
comments: Comment[] = [];
creationInfo?: CreationInfo;
protected populate(qml: Element): void {
let pid = _grabAttribute(qml, "publicID");
if (!isNonEmptyStringArg(pid)) {
warn(`missing publicID on ${qml.localName}`);
pid = `${UNKNOWN_PUBLIC_ID}_${qml.localName}`;
}
this.publicId = pid;
this.comments = _grabAllElComment(qml, "comment");
this.creationInfo = _grabFirstElCreationInfo(qml, "creationInfo");
}
}
/**
* Represent a QuakeML EventParameters.
*/
export class EventParameters extends BaseElement {
eventList: Quake[] = [];
description?: string;
/**
* Parses a QuakeML event parameters xml element into an EventParameters object.
*
* @param eventParametersQML the event parameters xml Element
* @param host optional source of the xml, helpful for parsing the eventid
* @returns EventParameters instance
*/
static createFromXml(
eventParametersQML: Element,
host?: string,
): EventParameters {
if (eventParametersQML.localName !== "eventParameters") {
throw new Error(
`Cannot extract, not a QuakeML event parameters: ${eventParametersQML.localName}`,
);
}
const eventEls = Array.from(
eventParametersQML.getElementsByTagNameNS(BED_NS, "event"),
);
const events = eventEls.map((e) => Quake.createFromXml(e, host));
const description = _grabFirstElText(eventParametersQML, "description");
const out = new EventParameters();
out.populate(eventParametersQML);
out.eventList = events;
out.description = description;
return out;
}
}
/**
* Represent a QuakeML Event. Renamed to Quake as Event conflicts with
* other uses in javascript.
*/
export class Quake extends BaseElement {
eventId: string | undefined;
descriptionList: EventDescription[] = [];
amplitudeList: Array = [];
stationMagnitudeList: Array = [];
magnitudeList: Array = [];
originList: Array = [];
pickList: Array = [];
focalMechanismList: Array = [];
preferredOrigin?: Origin;
preferredMagnitude?: Magnitude;
preferredFocalMechanism?: FocalMechanism;
type?: string;
typeCertainty?: string;
/**
* Parses a QuakeML event xml element into a Quake object. Pass in
* host=seisplotjs.fdsnevent.USGS_HOST for xml from the USGS service
* in order to parse the eventid, otherwise this can be left out
*
* @param qml the event xml Element
* @param host optional source of the xml, helpful for parsing the eventid
* @returns QuakeML Quake(Event) object
*/
static createFromXml(qml: Element, host?: string): Quake {
if (qml.localName !== "event") {
throw new Error(`Cannot extract, not a QuakeML Event: ${qml.localName}`);
}
const out = new Quake();
out.populate(qml);
const descriptionEls = Array.from(qml.children).filter(
(e) => e.tagName === "description",
);
out.descriptionList = descriptionEls.map((d) =>
EventDescription.createFromXml(d),
);
//need picks before can do origins
const allPickEls = Array.from(qml.getElementsByTagNameNS(BED_NS, "pick"));
const allPicks = [];
for (const pickEl of allPickEls) {
allPicks.push(Pick.createFromXml(pickEl));
}
const allAmplitudeEls = Array.from(
qml.getElementsByTagNameNS(BED_NS, "amplitude"),
);
const allAmplitudes = [];
for (const amplitudeEl of allAmplitudeEls) {
allAmplitudes.push(Amplitude.createFromXml(amplitudeEl, allPicks));
}
const allOriginEls = Array.from(
qml.getElementsByTagNameNS(BED_NS, "origin"),
);
const allOrigins = [];
for (const originEl of allOriginEls) {
allOrigins.push(Origin.createFromXml(originEl, allPicks));
}
const allStationMagEls = Array.from(
qml.getElementsByTagNameNS(BED_NS, "stationMagnitude"),
);
const allStationMags = [];
for (const stationMagEl of allStationMagEls) {
allStationMags.push(
StationMagnitude.createFromXml(stationMagEl, allOrigins, allAmplitudes),
);
}
const allMagEls = Array.from(
qml.getElementsByTagNameNS(BED_NS, "magnitude"),
);
const allMags = [];
for (const magEl of allMagEls) {
allMags.push(Magnitude.createFromXml(magEl, allOrigins, allStationMags));
}
const allFocalMechEls = Array.from(
qml.getElementsByTagNameNS(BED_NS, "focalMechanism"),
);
const allFocalMechs = [];
for (const focalMechEl of allFocalMechEls) {
allFocalMechs.push(
FocalMechanism.createFromXml(focalMechEl, allOrigins, allMags),
);
}
out.originList = allOrigins;
out.magnitudeList = allMags;
out.pickList = allPicks;
out.amplitudeList = allAmplitudes;
out.stationMagnitudeList = allStationMags;
out.focalMechanismList = allFocalMechs;
out.eventId = Quake.extractEventId(qml, host);
const preferredOriginId = _grabFirstElText(qml, "preferredOriginID");
const preferredMagnitudeId = _grabFirstElText(qml, "preferredMagnitudeID");
const preferredFocalMechId = _grabFirstElText(
qml,
"preferredFocalMechanismID",
);
if (isNonEmptyStringArg(preferredOriginId)) {
out.preferredOrigin = allOrigins.find(
(o) => o.publicId === preferredOriginId,
);
if (!out.preferredOrigin) {
throw new Error(`no preferredOriginId match: ${preferredOriginId}`);
}
}
if (isNonEmptyStringArg(preferredMagnitudeId)) {
out.preferredMagnitude = allMags.find(
(m) => m.publicId === preferredMagnitudeId,
);
if (!out.preferredMagnitude) {
throw new Error(`no match: ${preferredMagnitudeId}`);
}
}
if (isNonEmptyStringArg(preferredFocalMechId)) {
out.preferredFocalMechanism = allFocalMechs.find(
(m) => m.publicId === preferredFocalMechId,
);
if (!out.preferredFocalMechanism) {
throw new Error(`no match: ${preferredFocalMechId}`);
}
}
out.type = _grabFirstElText(qml, "type");
out.typeCertainty = _grabFirstElText(qml, "typeCertainty");
return out;
}
/**
* Extracts the EventId from a QuakeML element, guessing from one of several
* incompatible (grumble grumble) formats.
*
* @param qml Quake(Event) to extract from
* @param host optional source of the xml to help determine the event id style
* @returns Extracted Id, or "unknownEventId" if we can't figure it out
*/
static extractEventId(qml: Element, _host?: string): string {
const dataId = _grabAttributeNS(qml, ANSS_CATALOG_NS, "dataid");
if (isNonEmptyStringArg(dataId)) {
// usgs sets ns0:dataid to be event id, so if that exists, use it
return dataId;
}
const eventId = _grabAttributeNS(qml, ANSS_CATALOG_NS, "eventid");
const catalogEventSource = _grabAttributeNS(
qml,
ANSS_CATALOG_NS,
"eventsource",
);
if (isNonEmptyStringArg(eventId)) {
if (isNonEmptyStringArg(catalogEventSource)) {
// USGS, NCEDC and SCEDC use concat of eventsource and eventId as eventid, sigh...
return catalogEventSource + eventId;
} else {
return eventId;
}
}
const publicid = _grabAttribute(qml, "publicID");
if (isNonEmptyStringArg(publicid)) {
let re = /eventid=([\w\d]+)/;
let parsed = re.exec(publicid);
if (parsed) {
return parsed[1];
}
re = /evid=([\w\d]+)/;
parsed = re.exec(publicid);
if (parsed) {
return parsed[1];
}
re = /quakeml:se.anss.org\/Event\/([\w\d]+)\/([\w\d]+)/;
parsed = re.exec(publicid);
if (parsed) {
return parsed[1]+parsed[2];
}
return publicid;
}
return UNKNOWN_PUBLIC_ID;
}
hasPreferredOrigin() {
return isDef(this.preferredOrigin);
}
hasOrigin() {
return isDef(this.preferredOrigin) || this.originList.length > 1;
}
get origin(): Origin {
if (isDef(this.preferredOrigin)) {
return this.preferredOrigin;
} else if (this.originList.length > 0) {
return this.originList[0];
} else {
throw new Error("No origins in quake");
}
}
hasPreferredMagnitude() {
return isDef(this.preferredMagnitude);
}
hasMagnitude() {
return isDef(this.preferredMagnitude) || this.magnitudeList.length > 1;
}
get magnitude(): Magnitude {
if (isDef(this.preferredMagnitude)) {
return this.preferredMagnitude;
} else if (this.magnitudeList.length > 0) {
return this.magnitudeList[0];
} else {
throw new Error("No magnitudes in quake");
}
}
get time(): DateTime {
return this.origin.time;
}
get latitude(): number {
return this.origin.latitude;
}
get longitude(): number {
return this.origin.longitude;
}
get depth(): number {
return this.origin.depth;
}
get depthKm(): number {
return this.depth / 1000;
}
get description(): string {
return this.descriptionList.length > 0 ? this.descriptionList[0].text : "";
}
get arrivals(): Array {
return this.origin.arrivalList;
}
get picks(): Array {
return this.pickList;
}
toString(): string {
if (this.hasOrigin()) {
const magStr = this.hasMagnitude() ? this.magnitude.toString() : "";
const latlon = `(${latlonFormat.format(this.latitude)}/${latlonFormat.format(this.longitude)})`;
const depth = depthFormat.format(this.depth / 1000);
return `${this.time.toISO()} ${latlon} ${depth} ${magStr}`;
} else if (this.eventId != null) {
return `Event: ${this.eventId}`;
} else {
return `Event: unknown`;
}
}
}
/**
Represents a QuakeML EventDescription.
*/
export class EventDescription {
text: string;
type?: string;
constructor(text: string) {
this.text = text;
}
/**
* Parses a QuakeML description xml element into a EventDescription object.
*
* @param descriptionQML the description xml Element
* @returns EventDescription instance
*/
static createFromXml(descriptionQML: Element): EventDescription {
if (descriptionQML.localName !== "description") {
throw new Error(
`Cannot extract, not a QuakeML description ID: ${descriptionQML.localName}`,
);
}
const text = _grabFirstElText(descriptionQML, "text");
if (!isNonEmptyStringArg(text)) {
throw new Error("description missing text");
}
const out = new EventDescription(text);
out.type = _grabFirstElText(descriptionQML, "type");
return out;
}
toString(): string {
return this.text;
}
}
/** Represents a QuakeML Amplitude. */
export class Amplitude extends BaseElement {
genericAmplitude: RealQuantity;
type?: string;
category?: string;
unit?: string;
methodID?: string;
period?: RealQuantity;
snr?: number;
timeWindow?: TimeWindow;
pick?: Pick;
waveformID?: WaveformID;
filterID?: string;
scalingTime?: TimeQuantity;
magnitudeHint?: string;
evaluationMode?: string;
evaluationStatus?: string;
constructor(genericAmplitude: RealQuantity) {
super();
this.genericAmplitude = genericAmplitude;
}
/**
* Parses a QuakeML amplitude xml element into an Amplitude object.
*
* @param amplitudeQML the amplitude xml Element
* @param allPicks picks already extracted from the xml for linking arrivals with picks
* @returns Amplitude instance
*/
static createFromXml(amplitudeQML: Element, allPicks: Pick[]): Amplitude {
if (amplitudeQML.localName !== "amplitude") {
throw new Error(
`Cannot extract, not a QuakeML amplitude: ${amplitudeQML.localName}`,
);
}
const genericAmplitude = _grabFirstElRealQuantity(
amplitudeQML,
"genericAmplitude",
);
if (!isDef(genericAmplitude)) {
throw new Error("amplitude missing genericAmplitude");
}
const out = new Amplitude(genericAmplitude);
out.populate(amplitudeQML);
out.type = _grabFirstElText(amplitudeQML, "type");
out.category = _grabFirstElText(amplitudeQML, "category");
out.unit = _grabFirstElText(amplitudeQML, "unit");
out.methodID = _grabFirstElText(amplitudeQML, "methodID");
out.period = _grabFirstElRealQuantity(amplitudeQML, "period");
out.snr = _grabFirstElFloat(amplitudeQML, "snr");
out.timeWindow = _grabFirstElType(
TimeWindow.createFromXml.bind(TimeWindow),
)(amplitudeQML, "timeWindow");
const pickID = _grabFirstElText(amplitudeQML, "pickID");
out.pick = allPicks.find((p) => p.publicId === pickID);
if (pickID && !out.pick) {
throw new Error("No pick with ID " + pickID);
}
out.waveformID = _grabFirstElType(
WaveformID.createFromXml.bind(WaveformID),
)(amplitudeQML, "waveformID");
out.filterID = _grabFirstElText(amplitudeQML, "filterID");
out.scalingTime = _grabFirstElTimeQuantity(amplitudeQML, "scalingTime");
out.magnitudeHint = _grabFirstElText(amplitudeQML, "magnitudeHint");
out.evaluationMode = _grabFirstElText(amplitudeQML, "evaluationMode");
out.evaluationStatus = _grabFirstElText(amplitudeQML, "evaluationStatus");
return out;
}
}
/** Represents a QuakeML StationMagnitude. */
export class StationMagnitude extends BaseElement {
origin: Origin;
mag: RealQuantity;
type?: string;
amplitude?: Amplitude;
methodID?: string;
waveformID?: WaveformID;
constructor(origin: Origin, mag: RealQuantity) {
super();
this.origin = origin;
this.mag = mag;
}
/**
* Parses a QuakeML station magnitude xml element into a StationMagnitude object.
*
* @param stationMagnitudeQML the station magnitude xml Element
* @param allOrigins origins already extracted from the xml for linking station magnitudes with origins
* @param allAmplitudes amplitudes already extracted from the xml for linking station magnitudes with amplitudes
* @returns StationMagnitude instance
*/
static createFromXml(
stationMagnitudeQML: Element,
allOrigins: Origin[],
allAmplitudes: Amplitude[],
): StationMagnitude {
if (stationMagnitudeQML.localName !== "stationMagnitude") {
throw new Error(
`Cannot extract, not a QuakeML station magnitude: ${stationMagnitudeQML.localName}`,
);
}
const originID = _grabFirstElText(stationMagnitudeQML, "originID");
if (!isNonEmptyStringArg(originID)) {
throw new Error("stationMagnitude missing origin ID");
}
const origin = allOrigins.find((o) => o.publicId === originID);
if (!isDef(origin)) {
throw new Error("No origin with ID " + originID);
}
const mag = _grabFirstElRealQuantity(stationMagnitudeQML, "mag");
if (!isDef(mag)) {
throw new Error("stationMagnitude missing mag");
}
const out = new StationMagnitude(origin, mag);
out.populate(stationMagnitudeQML);
out.type = _grabFirstElText(stationMagnitudeQML, "type");
const amplitudeID = _grabFirstElText(stationMagnitudeQML, "amplitudeID");
out.amplitude = allAmplitudes.find((a) => a.publicId === amplitudeID);
if (amplitudeID && !out.amplitude) {
throw new Error("No amplitude with ID " + amplitudeID);
}
out.methodID = _grabFirstElText(stationMagnitudeQML, "methodID");
out.waveformID = _grabFirstElType(
WaveformID.createFromXml.bind(WaveformID),
)(stationMagnitudeQML, "waveformID");
return out;
}
}
/** Represents a QuakeML TimeWindow. */
export class TimeWindow {
begin: number;
end: number;
reference: DateTime;
constructor(begin: number, end: number, reference: DateTime) {
this.begin = begin;
this.end = end;
this.reference = reference;
}
/**
* Parses a QuakeML time window xml element into a TimeWindow object.
*
* @param timeWindowQML the time window xml Element
* @returns TimeWindow instance
*/
static createFromXml(timeWindowQML: Element): TimeWindow {
if (timeWindowQML.localName !== "timeWindow") {
throw new Error(
`Cannot extract, not a QuakeML time window: ${timeWindowQML.localName}`,
);
}
const begin = _grabFirstElFloat(timeWindowQML, "begin");
if (!isDef(begin)) {
throw new Error("timeWindow missing begin");
}
const end = _grabFirstElFloat(timeWindowQML, "end");
if (!isDef(end)) {
throw new Error("timeWindow missing end");
}
const reference = _grabFirstElDateTime(timeWindowQML, "reference");
if (!isDef(reference)) {
throw new Error("timeWindow missing reference");
}
const out = new TimeWindow(begin, end, reference);
return out;
}
}
/** Represents a QuakeML Origin. */
export class Origin extends BaseElement {
compositeTimes: Array;
originUncertainty?: OriginUncertainty;
arrivalList: Array;
timeQuantity: TimeQuantity;
latitudeQuantity: RealQuantity;
longitudeQuantity: RealQuantity;
depthQuantity?: RealQuantity;
depthType?: string;
timeFixed?: boolean;
epicenterFixed?: boolean;
referenceSystemID?: string;
methodID?: string;
earthModelID?: string;
quality?: OriginQuality;
type?: string;
region?: string;
evaluationMode?: string;
evaluationStatus?: string;
constructor(
time: TimeQuantity | DateTime,
latitude: RealQuantity | number,
longitude: RealQuantity | number,
) {
super();
this.compositeTimes = [];
this.arrivalList = [];
if (time instanceof DateTime) {
this.timeQuantity = new Quantity(time);
} else {
this.timeQuantity = time;
}
if (typeof latitude == "number") {
this.latitudeQuantity = new Quantity(latitude);
} else {
this.latitudeQuantity = latitude;
}
if (typeof longitude == "number") {
this.longitudeQuantity = new Quantity(longitude);
} else {
this.longitudeQuantity = longitude;
}
}
/**
* Parses a QuakeML origin xml element into a Origin object.
*
* @param qml the origin xml Element
* @param allPicks picks already extracted from the xml for linking arrivals with picks
* @returns Origin instance
*/
static createFromXml(qml: Element, allPicks: Array): Origin {
if (qml.localName !== "origin") {
throw new Error(`Cannot extract, not a QuakeML Origin: ${qml.localName}`);
}
const time = _grabFirstElTimeQuantity(qml, "time");
if (!isObject(time)) {
throw new Error("origin missing time");
}
const lat = _grabFirstElRealQuantity(qml, "latitude");
if (!isObject(lat)) {
throw new Error("origin missing latitude");
}
const lon = _grabFirstElRealQuantity(qml, "longitude");
if (!isObject(lon)) {
throw new Error("origin missing longitude");
}
const out = new Origin(time, lat, lon);
out.populate(qml);
out.originUncertainty = _grabFirstElType(
OriginUncertainty.createFromXml.bind(OriginUncertainty),
)(qml, "originUncertainty");
const allArrivalEls = Array.from(
qml.getElementsByTagNameNS(BED_NS, "arrival"),
);
out.arrivalList = allArrivalEls.map((arrivalEl) =>
Arrival.createFromXml(arrivalEl, allPicks),
);
out.depthQuantity = _grabFirstElRealQuantity(qml, "depth");
out.depthType = _grabFirstElText(qml, "depthType");
out.timeFixed = _grabFirstElBool(qml, "timeFixed");
out.epicenterFixed = _grabFirstElBool(qml, "epicenterFixed");
out.referenceSystemID = _grabFirstElText(qml, "referenceSystemID");
out.methodID = _grabFirstElText(qml, "methodID");
out.earthModelID = _grabFirstElText(qml, "earthModelID");
out.quality = _grabFirstElType(
OriginQuality.createFromXml.bind(OriginQuality),
)(qml, "quality");
out.type = _grabFirstElText(qml, "type");
out.region = _grabFirstElText(qml, "region");
out.evaluationMode = _grabFirstElText(qml, "evaluationMode");
out.evaluationStatus = _grabFirstElText(qml, "evaluationStatus");
return out;
}
toString(): string {
const latlon = `(${latlonFormat.format(this.latitude)}/${latlonFormat.format(this.longitude)})`;
const depth = depthFormat.format(this.depth / 1000);
return `${this.time.toISO()} ${latlon} ${depth} km`;
}
get time(): DateTime {
return this.timeQuantity.value;
}
set time(t: TimeQuantity | DateTime) {
if (t instanceof DateTime) {
this.timeQuantity.value = t;
} else {
this.timeQuantity = t;
}
}
get latitude(): number {
return this.latitudeQuantity.value;
}
set latitude(lat: RealQuantity | number) {
if (typeof lat == "number") {
this.latitudeQuantity.value = lat;
} else {
this.latitudeQuantity = lat;
}
}
get longitude(): number {
return this.longitudeQuantity.value;
}
set longitude(lon: RealQuantity | number) {
if (typeof lon == "number") {
this.longitudeQuantity.value = lon;
} else {
this.longitudeQuantity = lon;
}
}
get depthKm(): number {
return this.depth / 1000;
}
get depth(): number {
return this.depthQuantity?.value ?? NaN;
}
set depth(depth: RealQuantity | number) {
if (typeof depth == "number") {
if (!this.depthQuantity) {
this.depthQuantity = new Quantity(depth);
} else {
this.depthQuantity.value = depth;
}
} else {
this.depthQuantity = depth;
}
}
get arrivals(): Array {
return this.arrivalList;
}
}
/** Represents a QuakeML CompositeTime. */
export class CompositeTime {
year?: IntegerQuantity;
month?: IntegerQuantity;
day?: IntegerQuantity;
hour?: IntegerQuantity;
minute?: IntegerQuantity;
second?: RealQuantity;
/**
* Parses a QuakeML composite time xml element into an CompositeTime object.
*
* @param qml the composite time xml Element
* @returns CompositeTime instance
*/
static createFromXml(qml: Element): CompositeTime {
if (qml.localName !== "compositeTime") {
throw new Error(
`Cannot extract, not a QuakeML Composite Time: ${qml.localName}`,
);
}
const out = new CompositeTime();
out.year = _grabFirstElIntegerQuantity(qml, "year");
out.month = _grabFirstElIntegerQuantity(qml, "month");
out.day = _grabFirstElIntegerQuantity(qml, "day");
out.hour = _grabFirstElIntegerQuantity(qml, "hour");
out.minute = _grabFirstElIntegerQuantity(qml, "minute");
out.second = _grabFirstElIntegerQuantity(qml, "second");
return out;
}
}
/** Represents a QuakeML OriginUncertainty. */
export class OriginUncertainty {
horizontalUncertainty?: number;
minHorizontalUncertainty?: number;
maxHorizontalUncertainty?: number;
azimuthMaxHorizontalUncertainty?: number;
confidenceEllipsoid?: ConfidenceEllipsoid;
preferredDescription?: string;
confidenceLevel?: number;
/**
* Parses a QuakeML origin uncertainty xml element into an OriginUncertainty object.
*
* @param qml the origin uncertainty xml Element
* @returns OriginUncertainty instance
*/
static createFromXml(qml: Element): OriginUncertainty {
if (qml.localName !== "originUncertainty") {
throw new Error(
`Cannot extract, not a QuakeML Origin Uncertainty: ${qml.localName}`,
);
}
const out = new OriginUncertainty();
out.horizontalUncertainty = _grabFirstElFloat(qml, "horizontalUncertainty");
out.minHorizontalUncertainty = _grabFirstElFloat(
qml,
"minHorizontalUncertainty",
);
out.maxHorizontalUncertainty = _grabFirstElFloat(
qml,
"maxHorizontalUncertainty",
);
out.azimuthMaxHorizontalUncertainty = _grabFirstElFloat(
qml,
"azimuthMaxHorizontalUncertainty",
);
out.confidenceEllipsoid = _grabFirstElType(
ConfidenceEllipsoid.createFromXml.bind(ConfidenceEllipsoid),
)(qml, "confidenceEllipsoid");
out.preferredDescription = _grabFirstElText(qml, "preferredDescription");
out.confidenceLevel = _grabFirstElFloat(qml, "confidenceLevel");
return out;
}
}
/** Represents a QuakeML ConfidenceEllipsoid. */
export class ConfidenceEllipsoid {
semiMajorAxisLength: number;
semiMinorAxisLength: number;
semiIntermediateAxisLength: number;
majorAxisPlunge: number;
majorAxisAzimuth: number;
majorAxisRotation: number;
constructor(
semiMajorAxisLength: number,
semiMinorAxisLength: number,
semiIntermediateAxisLength: number,
majorAxisPlunge: number,
majorAxisAzimuth: number,
majorAxisRotation: number,
) {
this.semiMajorAxisLength = semiMajorAxisLength;
this.semiMinorAxisLength = semiMinorAxisLength;
this.semiIntermediateAxisLength = semiIntermediateAxisLength;
this.majorAxisPlunge = majorAxisPlunge;
this.majorAxisAzimuth = majorAxisAzimuth;
this.majorAxisRotation = majorAxisRotation;
}
/**
* Parses a QuakeML confidence ellipsoid xml element into an ConfidenceEllipsoid object.
*
* @param qml the confidence ellipsoid xml Element
* @returns ConfidenceEllipsoid instance
*/
static createFromXml(qml: Element): ConfidenceEllipsoid {
if (qml.localName !== "confidenceEllipsoid") {
throw new Error(
`Cannot extract, not a QuakeML Confidence Ellipsoid: ${qml.localName}`,
);
}
const semiMajorAxisLength = _grabFirstElFloat(qml, "semiMajorAxisLength");
if (semiMajorAxisLength === undefined) {
throw new Error("confidenceEllipsoid missing semiMajorAxisLength");
}
const semiMinorAxisLength = _grabFirstElFloat(qml, "semiMinorAxisLength");
if (semiMinorAxisLength === undefined) {
throw new Error("confidenceEllipsoid missing semiMinorAxisLength");
}
const semiIntermediateAxisLength = _grabFirstElFloat(
qml,
"semiIntermediateAxisLength",
);
if (semiIntermediateAxisLength === undefined) {
throw new Error("confidenceEllipsoid missing semiIntermediateAxisLength");
}
const majorAxisPlunge = _grabFirstElFloat(qml, "majorAxisPlunge");
if (majorAxisPlunge === undefined) {
throw new Error("confidenceEllipsoid missing majorAxisPlunge");
}
const majorAxisAzimuth = _grabFirstElFloat(qml, "majorAxisAzimuth");
if (majorAxisAzimuth === undefined) {
throw new Error("confidenceEllipsoid missing majorAxisAzimuth");
}
const majorAxisRotation = _grabFirstElFloat(qml, "majorAxisRotation");
if (majorAxisRotation === undefined) {
throw new Error("confidenceEllipsoid missing majorAxisRotation");
}
const out = new ConfidenceEllipsoid(
semiMajorAxisLength,
semiMinorAxisLength,
semiIntermediateAxisLength,
majorAxisPlunge,
majorAxisAzimuth,
majorAxisRotation,
);
return out;
}
}
/** Represents a QuakeML OriginQuality. */
export class OriginQuality {
associatedPhaseCount?: number;
usedPhaseCount?: number;
associatedStationCount?: number;
usedStationCount?: number;
depthPhaseCount?: number;
standardError?: number;
azimuthalGap?: number;
secondaryAzimuthalGap?: number;
groundTruthLevel?: string;
maximumDistance?: number;
minimumDistance?: number;
medianDistance?: number;
/**
* Parses a QuakeML origin quality xml element into an OriginQuality object.
*
* @param qml the origin quality xml Element
* @returns OriginQuality instance
*/
static createFromXml(qml: Element): OriginQuality {
if (qml.localName !== "quality") {
throw new Error(
`Cannot extract, not a QuakeML Origin Quality: ${qml.localName}`,
);
}
const out = new OriginQuality();
out.associatedPhaseCount = _grabFirstElInt(qml, "associatedPhaseCount");
out.usedPhaseCount = _grabFirstElInt(qml, "usedPhaseCount");
out.associatedStationCount = _grabFirstElInt(qml, "associatedStationCount");
out.usedStationCount = _grabFirstElInt(qml, "usedStationCount");
out.standardError = _grabFirstElFloat(qml, "standardError");
out.azimuthalGap = _grabFirstElFloat(qml, "azimuthalGap");
out.secondaryAzimuthalGap = _grabFirstElFloat(qml, "secondaryAzimuthalGap");
out.groundTruthLevel = _grabFirstElText(qml, "groundTruthLevel");
out.maximumDistance = _grabFirstElFloat(qml, "maximumDistance");
out.minimumDistance = _grabFirstElFloat(qml, "minimumDistance");
out.medianDistance = _grabFirstElFloat(qml, "medianDistance");
return out;
}
}
/**
Represents a QuakeML Magnitude.
*/
export class Magnitude extends BaseElement {
stationMagnitudeContributions: StationMagnitudeContribution[] = [];
magQuantity: RealQuantity;
type?: string;
origin?: Origin;
methodID?: string;
stationCount?: number;
azimuthalGap?: number;
evaluationMode?: string;
evaluationStatus?: string;
constructor(mag: RealQuantity | number, type?: string) {
super();
if (typeof mag === "number") {
this.magQuantity = new Quantity(mag);
} else {
this.magQuantity = mag;
}
if (type) {
this.type = type;
}
}
/**
* Parses a QuakeML magnitude xml element into a Magnitude object.
*
* @param qml the magnitude xml Element
* @param allOrigins origins already extracted from the xml for linking magnitudes with origins
* @param allStationMagnitudes station magnitudes already extracted from the xml
* @returns Magnitude instance
*/
static createFromXml(
qml: Element,
allOrigins: Origin[],
allStationMagnitudes: StationMagnitude[],
): Magnitude {
if (qml.localName !== "magnitude") {
throw new Error(
`Cannot extract, not a QuakeML Magnitude: ${qml.localName}`,
);
}
const mag = _grabFirstElRealQuantity(qml, "mag");
if (!mag) {
throw new Error("magnitude missing mag");
}
const out = new Magnitude(mag);
out.populate(qml);
const stationMagnitudeContributionEls = Array.from(
qml.getElementsByTagNameNS(BED_NS, "stationMagnitudeContribution"),
);
out.stationMagnitudeContributions = stationMagnitudeContributionEls.map(
(smc) =>
StationMagnitudeContribution.createFromXml(smc, allStationMagnitudes),
);
out.type = _grabFirstElText(qml, "type");
const originID = _grabFirstElText(qml, "originID");
out.origin = allOrigins.find((o) => o.publicId === originID);
if (originID && !out.origin) {
throw new Error("No origin with ID " + originID);
}
out.methodID = _grabFirstElText(qml, "methodID");
out.stationCount = _grabFirstElInt(qml, "stationCount");
out.azimuthalGap = _grabFirstElFloat(qml, "azimuthalGap");
out.evaluationMode = _grabFirstElText(qml, "evaluationMode");
out.evaluationStatus = _grabFirstElText(qml, "evaluationStatus");
return out;
}
toString(): string {
return `${magFormat.format(this.mag)} ${this.type ? this.type : ""}`;
}
get mag(): number {
return this.magQuantity.value;
}
set mag(value: RealQuantity | number) {
if (typeof value === "number") {
this.magQuantity.value = value;
} else {
this.magQuantity = value;
}
}
}
/**
Represents a QuakeML StationMagnitudeContribution.
*/
export class StationMagnitudeContribution {
stationMagnitude: StationMagnitude;
residual?: number;
weight?: number;
constructor(stationMagnitude: StationMagnitude) {
this.stationMagnitude = stationMagnitude;
}
/**
* Parses a QuakeML station magnitude contribution xml element into a StationMagnitudeContribution object.
*
* @param qml the station magnitude contribution xml Element
* @param allStationMagnitudes station magnitudes already extracted from the xml for linking station magnitudes with station magnitude contributions
* @returns StationMagnitudeContribution instance
*/
static createFromXml(
qml: Element,
allStationMagnitudes: Array,
): StationMagnitudeContribution {
if (qml.localName !== "stationMagnitudeContribution") {
throw new Error(
`Cannot extract, not a QuakeML StationMagnitudeContribution: ${qml.localName}`,
);
}
const stationMagnitudeID = _grabFirstElText(qml, "stationMagnitudeID");
if (!isNonEmptyStringArg(stationMagnitudeID)) {
throw new Error("stationMagnitudeContribution missing stationMagnitude");
}
const stationMagnitude = allStationMagnitudes.find(
(sm) => sm.publicId === stationMagnitudeID,
);
if (!isDef(stationMagnitude)) {
throw new Error("No stationMagnitude with ID " + stationMagnitudeID);
}
const out = new StationMagnitudeContribution(stationMagnitude);
out.residual = _grabFirstElFloat(qml, "residual");
out.weight = _grabFirstElFloat(qml, "weight");
return out;
}
}
/**
Represents a QuakeML Arrival, a combination of a Pick with a phase name.
*/
export class Arrival extends BaseElement {
phase: string;
pick: Pick;
timeCorrection?: number;
azimuth?: number;
distance?: number;
takeoffAngle?: RealQuantity;
timeResidual?: number;
horizontalSlownessResidual?: number;
backazimuthResidual?: number;
timeWeight?: number;
horizontalSlownessWeight?: number;
backazimuthWeight?: number;
earthModelID?: string;
constructor(phase: string, pick: Pick) {
super();
this.phase = phase;
this.pick = pick;
}
/**
* Parses a QuakeML arrival xml element into a Arrival object.
*
* @param arrivalQML the arrival xml Element
* @param allPicks picks already extracted from the xml for linking arrivals with picks
* @returns Arrival instance
*/
static createFromXml(arrivalQML: Element, allPicks: Array): Arrival {
if (arrivalQML.localName !== "arrival") {
throw new Error(
`Cannot extract, not a QuakeML Arrival: ${arrivalQML.localName}`,
);
}
const pickId = _grabFirstElText(arrivalQML, "pickID");
const phase = _grabFirstElText(arrivalQML, "phase");
if (isNonEmptyStringArg(phase) && isNonEmptyStringArg(pickId)) {
const myPick = allPicks.find(function (p: Pick) {
return p.publicId === pickId;
});
if (!myPick) {
throw new Error("Can't find pick with Id=" + pickId + " for Arrival");
}
const out = new Arrival(phase, myPick);
out.populate(arrivalQML);
out.timeCorrection = _grabFirstElFloat(arrivalQML, "timeCorrection");
out.azimuth = _grabFirstElFloat(arrivalQML, "azimuth");
out.distance = _grabFirstElFloat(arrivalQML, "distance");
out.takeoffAngle = _grabFirstElRealQuantity(arrivalQML, "takeoffAngle");
out.timeResidual = _grabFirstElFloat(arrivalQML, "timeResidual");
out.horizontalSlownessResidual = _grabFirstElFloat(
arrivalQML,
"horizontalSlownessResidual",
);
out.backazimuthResidual = _grabFirstElFloat(
arrivalQML,
"backazimuthResidual",
);
out.timeWeight = _grabFirstElFloat(arrivalQML, "timeWeight");
out.horizontalSlownessWeight = _grabFirstElFloat(
arrivalQML,
"horizontalSlownessWeight",
);
out.backazimuthWeight = _grabFirstElFloat(
arrivalQML,
"backazimuthWeight",
);
out.earthModelID = _grabFirstElText(arrivalQML, "earthModelID");
return out;
} else {
throw new Error(
"Arrival does not have phase or pickId: " +
stringify(phase) +
" " +
stringify(pickId),
);
}
}
}
/**
Represents a QuakeML Pick.
*/
export class Pick extends BaseElement {
timeQuantity: TimeQuantity;
waveformID: WaveformID;
filterID?: string;
methodID?: string;
horizontalSlowness?: RealQuantity;
backazimuth?: RealQuantity;
slownessMethodID?: string;
onset?: string;
phaseHint?: string;
polarity?: string;
evaluationMode?: string;
evaluationStatus?: string;
constructor(time: TimeQuantity | DateTime, waveformID: WaveformID) {
super();
if (time instanceof DateTime) {
this.timeQuantity = new Quantity(time);
} else {
this.timeQuantity = time;
}
this.waveformID = waveformID;
}
get time(): DateTime {
return this.timeQuantity.value;
}
set time(t: Quantity | DateTime) {
if (t instanceof DateTime) {
this.timeQuantity.value = t;
} else {
this.timeQuantity = t;
}
}
/**
* Parses a QuakeML pick xml element into a Pick object.
*
* @param pickQML the pick xml Element
* @returns Pick instance
*/
static createFromXml(pickQML: Element): Pick {
if (pickQML.localName !== "pick") {
throw new Error(
`Cannot extract, not a QuakeML Pick: ${pickQML.localName}`,
);
}
const time = _grabFirstElTimeQuantity(pickQML, "time");
if (!isDef(time)) {
throw new Error("Missing time");
}
const waveformId = _grabFirstElType(
WaveformID.createFromXml.bind(WaveformID),
)(pickQML, "waveformID");
if (!isObject(waveformId)) {
throw new Error("pick missing waveformID");
}
const out = new Pick(time, waveformId);
out.populate(pickQML);
out.filterID = _grabFirstElText(pickQML, "filterID");
out.methodID = _grabFirstElText(pickQML, "methodID");
out.horizontalSlowness = _grabFirstElRealQuantity(
pickQML,
"horizontalSlowness",
);
out.backazimuth = _grabFirstElRealQuantity(pickQML, "backazimuth");
out.slownessMethodID = _grabFirstElText(pickQML, "slownessMethodID");
out.onset = _grabFirstElText(pickQML, "onset");
out.phaseHint = _grabFirstElText(pickQML, "phaseHint");
out.polarity = _grabFirstElText(pickQML, "polarity");
out.evaluationMode = _grabFirstElText(pickQML, "evaluationMode");
out.evaluationStatus = _grabFirstElText(pickQML, "evaluationStatus");
return out;
}
get networkCode(): string {
return this.waveformID.networkCode;
}
get stationCode(): string {
return this.waveformID.stationCode;
}
get locationCode(): string {
return this.waveformID.locationCode || "--";
}
get channelCode(): string {
return this.waveformID.channelCode || "---";
}
isAtStation(station: Station): boolean {
return (
this.networkCode === station.networkCode &&
this.stationCode === station.stationCode
);
}
isOnChannel(channel: Channel): boolean {
return (
this.networkCode === channel.station.networkCode &&
this.stationCode === channel.station.stationCode &&
this.locationCode === channel.locationCode &&
this.channelCode === channel.channelCode
);
}
toString(): string {
return (
stringify(this.time) +
` ${this.networkCode}.${this.stationCode}.${this.locationCode}.${this.channelCode}`
);
}
}
/**
Represents a QuakeML Focal Mechanism.
*/
export class FocalMechanism extends BaseElement {
waveformIDList: WaveformID[] = [];
momentTensorList: MomentTensor[] = [];
triggeringOrigin?: Origin;
nodalPlanes?: NodalPlanes;
principalAxes?: PrincipalAxes;
azimuthalGap?: number;
stationPolarityCount?: number;
misfit?: number;
stationDistributionRatio?: number;
methodID?: string;
evaluationMode?: string;
evaluationStatus?: string;
/**
* Parses a QuakeML focal mechanism xml element into a FocalMechanism object.
*
* @param focalMechQML the focal mechanism xml Element
* @param allOrigins origins already extracted from the xml for linking focal mechanisms with origins
* @param allMagnitudes magnitudes already extracted from the xml for linking moment tensors with magnitudes
* @returns FocalMechanism instance
*/
static createFromXml(
focalMechQML: Element,
allOrigins: Origin[],
allMagnitudes: Magnitude[],
): FocalMechanism {
if (focalMechQML.localName !== "focalMechanism") {
throw new Error(
`Cannot extract, not a QuakeML focalMechanism: ${focalMechQML.localName}`,
);
}
const out = new FocalMechanism();
out.populate(focalMechQML);
const waveformIDEls = Array.from(
focalMechQML.getElementsByTagNameNS(BED_NS, "waveformID"),
);
out.waveformIDList = waveformIDEls.map((wid) =>
WaveformID.createFromXml(wid),
);
const momentTensorEls = Array.from(
focalMechQML.getElementsByTagNameNS(BED_NS, "momentTensor"),
);
out.momentTensorList = momentTensorEls.map((mt) =>
MomentTensor.createFromXml(mt, allOrigins, allMagnitudes),
);
const triggeringOriginID = _grabFirstElText(
focalMechQML,
"triggeringOriginID",
);
out.triggeringOrigin = allOrigins.find(
(o) => o.publicId === triggeringOriginID,
);
if (triggeringOriginID && !out.triggeringOrigin) {
throw new Error("No origin with ID " + triggeringOriginID);
}
out.nodalPlanes = _grabFirstElType(
NodalPlanes.createFromXml.bind(NodalPlanes),
)(focalMechQML, "nodalPlanes");
out.principalAxes = _grabFirstElType(
PrincipalAxes.createFromXml.bind(PrincipalAxes),
)(focalMechQML, "principalAxes");
out.azimuthalGap = _grabFirstElFloat(focalMechQML, "azimuthalGap");
out.stationPolarityCount = _grabFirstElInt(
focalMechQML,
"stationPolarityCount",
);
out.misfit = _grabFirstElFloat(focalMechQML, "misfit");
out.stationDistributionRatio = _grabFirstElFloat(
focalMechQML,
"stationDistributionRatio",
);
out.methodID = _grabFirstElText(focalMechQML, "methodID");
out.evaluationMode = _grabFirstElText(focalMechQML, "evaluationMode");
out.evaluationStatus = _grabFirstElText(focalMechQML, "evaluationStatus");
return out;
}
}
/**
Represents a QuakeML NodalPlanes.
*/
export class NodalPlanes {
nodalPlane1?: NodalPlane;
nodalPlane2?: NodalPlane;
preferredPlane?: number;
/**
* Parses a QuakeML nodal planes xml element into a NodalPlanes object.
*
* @param nodalPlanesQML the nodal planes xml Element
* @returns NodalPlanes instance
*/
static createFromXml(nodalPlanesQML: Element): NodalPlanes {
const out = new NodalPlanes();
out.nodalPlane1 = _grabFirstElType(
NodalPlane.createFromXml.bind(NodalPlane),
)(nodalPlanesQML, "nodalPlane1");
out.nodalPlane2 = _grabFirstElType(
NodalPlane.createFromXml.bind(NodalPlane),
)(nodalPlanesQML, "nodalPlane2");
const preferredPlaneString = _grabAttribute(
nodalPlanesQML,
"preferredPlane",
);
out.preferredPlane = isNonEmptyStringArg(preferredPlaneString)
? parseInt(preferredPlaneString)
: undefined;
return out;
}
}
/**
Represents a QuakeML NodalPlane.
*/
export class NodalPlane {
strike: RealQuantity;
dip: RealQuantity;
rake: RealQuantity;
constructor(strike: RealQuantity, dip: RealQuantity, rake: RealQuantity) {
this.strike = strike;
this.dip = dip;
this.rake = rake;
}
/**
* Parses a QuakeML nodal plane xml element into a NodalPlane object.
*
* @param nodalPlaneQML the nodal plane xml Element
* @returns NodalPlane instance
*/
static createFromXml(nodalPlaneQML: Element): NodalPlane {
const strike = _grabFirstElRealQuantity(nodalPlaneQML, "strike");
if (!isObject(strike)) {
throw new Error("nodal plane missing strike");
}
const dip = _grabFirstElRealQuantity(nodalPlaneQML, "dip");
if (!isObject(dip)) {
throw new Error("nodal plane missing dip");
}
const rake = _grabFirstElRealQuantity(nodalPlaneQML, "rake");
if (!isObject(rake)) {
throw new Error("nodal plane missing rake");
}
const out = new NodalPlane(strike, dip, rake);
return out;
}
}
/**
Represents a QuakeML PrincipalAxes.
*/
export class PrincipalAxes {
tAxis: Axis;
pAxis: Axis;
nAxis?: Axis;
constructor(tAxis: Axis, pAxis: Axis) {
this.tAxis = tAxis;
this.pAxis = pAxis;
}
/**
* Parses a QuakeML princpalAxes element into a PrincipalAxes object.
*
* @param princpalAxesQML the princpalAxes xml Element
* @returns PrincipalAxes instance
*/
static createFromXml(princpalAxesQML: Element): PrincipalAxes {
if (princpalAxesQML.localName !== "principalAxes") {
throw new Error(
`Cannot extract, not a QuakeML princpalAxes: ${princpalAxesQML.localName}`,
);
}
const tAxis = _grabFirstElType(Axis.createFromXml.bind(Axis))(
princpalAxesQML,
"tAxis",
);
if (!isObject(tAxis)) {
throw new Error("nodal plane missing tAxis");
}
const pAxis = _grabFirstElType(Axis.createFromXml.bind(Axis))(
princpalAxesQML,
"pAxis",
);
if (!isObject(pAxis)) {
throw new Error("nodal plane missing pAxis");
}
const out = new PrincipalAxes(tAxis, pAxis);
out.nAxis = _grabFirstElType(Axis.createFromXml.bind(Axis))(
princpalAxesQML,
"nAxis",
);
return out;
}
}
/**
Represents a QuakeML Axis.
*/
export class Axis {
azimuth: RealQuantity;
plunge: RealQuantity;
length: RealQuantity;
constructor(
azimuth: RealQuantity,
plunge: RealQuantity,
length: RealQuantity,
) {
this.azimuth = azimuth;
this.plunge = plunge;
this.length = length;
}
/**
* Parses a QuakeML axis xml element into a Axis object.
*
* @param axisQML the axis xml Element
* @returns Axis instance
*/
static createFromXml(axisQML: Element): Axis {
const azimuth = _grabFirstElRealQuantity(axisQML, "azimuth");
if (!isObject(azimuth)) {
throw new Error("nodal plane missing azimuth");
}
const plunge = _grabFirstElRealQuantity(axisQML, "plunge");
if (!isObject(plunge)) {
throw new Error("nodal plane missing plunge");
}
const length = _grabFirstElRealQuantity(axisQML, "length");
if (!isObject(length)) {
throw new Error("nodal plane missing length");
}
const out = new Axis(azimuth, plunge, length);
return out;
}
}
/**
Represents a QuakeML MomentTensor.
*/
export class MomentTensor extends BaseElement {
dataUsedList: DataUsed[] = [];
derivedOrigin: Origin;
momentMagnitude?: Magnitude;
scalarMoment?: RealQuantity;
tensor?: Tensor;
variance?: number;
varianceReduction?: number;
doubleCouple?: number;
clvd?: number;
iso?: number;
greensFunctionID?: string;
filterID?: string;
sourceTimeFunction?: SourceTimeFunction;
methodID?: string;
category?: string;
inversionType?: string;
constructor(derivedOrigin: Origin) {
super();
this.derivedOrigin = derivedOrigin;
}
/**
* Parses a QuakeML momentTensor xml element into a MomentTensor object.
*
* @param momentTensorQML the momentTensor xml Element
* @param allOrigins origins already extracted from the xml for linking moment tensors with origins
* @param allMagnitudes magnitudes already extracted from the xml for linking moment tensors with magnitudes
* @returns MomentTensor instance
*/
static createFromXml(
momentTensorQML: Element,
allOrigins: Origin[],
allMagnitudes: Magnitude[],
): MomentTensor {
if (momentTensorQML.localName !== "momentTensor") {
throw new Error(
`Cannot extract, not a QuakeML momentTensor: ${momentTensorQML.localName}`,
);
}
const derivedOriginID = _grabFirstElText(
momentTensorQML,
"derivedOriginID",
);
if (!isNonEmptyStringArg(derivedOriginID)) {
throw new Error("momentTensor missing derivedOriginID");
}
const derivedOrigin = allOrigins.find(
(o) => o.publicId === derivedOriginID,
);
if (!isDef(derivedOrigin)) {
throw new Error("No origin with ID " + derivedOriginID);
}
const out = new MomentTensor(derivedOrigin);
out.populate(momentTensorQML);
const dataUsedEls = Array.from(
momentTensorQML.getElementsByTagNameNS(BED_NS, "dataUsed"),
);
out.dataUsedList = dataUsedEls.map(DataUsed.createFromXml.bind(DataUsed));
const momentMagnitudeID = _grabFirstElText(
momentTensorQML,
"momentMagnitudeID",
);
out.momentMagnitude = allMagnitudes.find(
(o) => o.publicId === momentMagnitudeID,
);
if (momentMagnitudeID && !out.momentMagnitude) {
throw new Error("No magnitude with ID " + momentMagnitudeID);
}
// usgs has bug in quakeml from fdsnws, scalarMoment doesn't have ,
// is just a number
try {
out.scalarMoment = _grabFirstElRealQuantity(
momentTensorQML,
"scalarMoment",
);
} catch (err) {
// try as just a number
const scalMom = _grabFirstElFloat(momentTensorQML, "scalarMoment");
if (scalMom != null ) {
out.scalarMoment = new Quantity(scalMom);
} else {
// neither Quantity or number, warn
warn(`scalarMoment in momentTensor is invalid: ${_grabFirstEl(momentTensorQML, "scalarMoment")}`)
}
warn(`scalarMoment in momentTensor is invalid: ${_grabFirstEl(momentTensorQML, "scalarMoment")}`)
}
out.tensor = _grabFirstElType(Tensor.createFromXml.bind(Tensor))(
momentTensorQML,
"tensor",
);
out.variance = _grabFirstElFloat(momentTensorQML, "variance");
out.varianceReduction = _grabFirstElFloat(
momentTensorQML,
"varianceReduction",
);
out.doubleCouple = _grabFirstElFloat(momentTensorQML, "doubleCouple");
out.clvd = _grabFirstElFloat(momentTensorQML, "clvd");
out.iso = _grabFirstElFloat(momentTensorQML, "iso");
out.greensFunctionID = _grabFirstElText(
momentTensorQML,
"greensFunctionID",
);
out.filterID = _grabFirstElText(momentTensorQML, "filterID");
out.sourceTimeFunction = _grabFirstElType(
SourceTimeFunction.createFromXml.bind(SourceTimeFunction),
)(momentTensorQML, "sourceTimeFunction");
out.methodID = _grabFirstElText(momentTensorQML, "methodID");
out.category = _grabFirstElText(momentTensorQML, "category");
out.inversionType = _grabFirstElText(momentTensorQML, "inversionType");
return out;
}
}
/**
Represents a QuakeML Tensor.
*/
export class Tensor {
Mrr: RealQuantity;
Mtt: RealQuantity;
Mpp: RealQuantity;
Mrt: RealQuantity;
Mrp: RealQuantity;
Mtp: RealQuantity;
constructor(
Mrr: RealQuantity,
Mtt: RealQuantity,
Mpp: RealQuantity,
Mrt: RealQuantity,
Mrp: RealQuantity,
Mtp: RealQuantity,
) {
this.Mrr = Mrr;
this.Mtt = Mtt;
this.Mpp = Mpp;
this.Mrt = Mrt;
this.Mrp = Mrp;
this.Mtp = Mtp;
}
/**
* Parses a QuakeML tensor xml element into a Tensor object.
*
* @param tensorQML the tensor xml Element
* @returns Tensor instance
*/
static createFromXml(tensorQML: Element): Tensor {
if (tensorQML.localName !== "tensor") {
throw new Error(
`Cannot extract, not a QuakeML tensor: ${tensorQML.localName}`,
);
}
const Mrr = _grabFirstElRealQuantity(tensorQML, "Mrr");
if (!isObject(Mrr)) {
throw new Error("tensor missing Mrr");
}
const Mtt = _grabFirstElRealQuantity(tensorQML, "Mtt");
if (!isObject(Mtt)) {
throw new Error("tensor missing Mtt");
}
const Mpp = _grabFirstElRealQuantity(tensorQML, "Mpp");
if (!isObject(Mpp)) {
throw new Error("tensor missing Mpp");
}
const Mrt = _grabFirstElRealQuantity(tensorQML, "Mrt");
if (!isObject(Mrt)) {
throw new Error("tensor missing Mrt");
}
const Mrp = _grabFirstElRealQuantity(tensorQML, "Mrp");
if (!isObject(Mrp)) {
throw new Error("tensor missing Mrp");
}
const Mtp = _grabFirstElRealQuantity(tensorQML, "Mtp");
if (!isObject(Mtp)) {
throw new Error("tensor missing Mtp");
}
const out = new Tensor(Mrr, Mtt, Mpp, Mrt, Mrp, Mtp);
return out;
}
}
/**
Represents a QuakeML SourceTimeFunction.
*/
export class SourceTimeFunction {
type: string;
duration: number;
riseTime?: number;
decayTime?: number;
constructor(type: string, duration: number) {
this.type = type;
this.duration = duration;
}
/**
* Parses a QuakeML sourceTimeFunction xml element into a SourceTimeFunction object.
*
* @param sourceTimeFunctionQML the sourceTimeFunction xml Element
* @returns SourceTimeFunction instance
*/
static createFromXml(sourceTimeFunctionQML: Element): SourceTimeFunction {
if (sourceTimeFunctionQML.localName !== "sourceTimeFunction") {
throw new Error(
`Cannot extract, not a QuakeML sourceTimeFunction: ${sourceTimeFunctionQML.localName}`,
);
}
const type = _grabFirstElText(sourceTimeFunctionQML, "type");
if (!isNonEmptyStringArg(type)) {
throw new Error("sourceTimeFunction missing type");
}
const duration = _grabFirstElFloat(sourceTimeFunctionQML, "duration");
if (!isDef(duration)) {
throw new Error("sourceTimeFunction missing duration");
}
const out = new SourceTimeFunction(type, duration);
out.riseTime = _grabFirstElFloat(sourceTimeFunctionQML, "riseTime");
out.decayTime = _grabFirstElFloat(sourceTimeFunctionQML, "decayTime");
return out;
}
}
/**
Represents a QuakeML DataUsed.
*/
export class DataUsed {
waveType: string;
stationCount?: number;
componentCount?: number;
shortestPeriod?: number;
longestPeriod?: number;
constructor(waveType: string) {
this.waveType = waveType;
}
/**
* Parses a QuakeML dataUsed xml element into a DataUsed object.
*
* @param dataUsedQML the dataUsed xml Element
* @returns SourceTimeFunction instance
*/
static createFromXml(dataUsedQML: Element): DataUsed {
if (dataUsedQML.localName !== "dataUsed") {
throw new Error(
`Cannot extract, not a QuakeML dataUsed: ${dataUsedQML.localName}`,
);
}
const waveType = _grabFirstElText(dataUsedQML, "waveType");
if (!isNonEmptyStringArg(waveType)) {
throw new Error("dataUsed missing waveType");
}
const out = new DataUsed(waveType);
out.stationCount = _grabFirstElInt(dataUsedQML, "stationCount");
out.componentCount = _grabFirstElInt(dataUsedQML, "componentCount");
out.shortestPeriod = _grabFirstElFloat(dataUsedQML, "shortestPeriod");
out.longestPeriod = _grabFirstElFloat(dataUsedQML, "longestPeriod");
return out;
}
}
/**
Represents a QuakeML WaveformID.
*/
export class WaveformID {
networkCode: string;
stationCode: string;
channelCode?: string;
locationCode?: string;
constructor(networkCode: string, stationCode: string) {
this.networkCode = networkCode;
this.stationCode = stationCode;
}
/**
* Parses a QuakeML waveform ID xml element into a WaveformID object.
*
* @param waveformQML the waveform ID xml Element
* @returns WaveformID instance
*/
static createFromXml(waveformQML: Element): WaveformID {
if (waveformQML.localName !== "waveformID") {
throw new Error(
`Cannot extract, not a QuakeML waveform ID: ${waveformQML.localName}`,
);
}
const networkCode = _grabAttribute(waveformQML, "networkCode");
if (!isNonEmptyStringArg(networkCode)) {
throw new Error("waveformID missing networkCode");
}
const stationCode = _grabAttribute(waveformQML, "stationCode");
if (!isNonEmptyStringArg(stationCode)) {
throw new Error("waveformID missing stationCode");
}
const out = new WaveformID(networkCode, stationCode);
out.channelCode = _grabAttribute(waveformQML, "channelCode");
out.locationCode = _grabAttribute(waveformQML, "locationCode");
return out;
}
toString(): string {
return `${this.networkCode}.${this.stationCode}.${this.locationCode || "--"}.${this.channelCode || "---"}`;
}
}
export class Quantity {
value: T;
uncertainty?: number;
lowerUncertainty?: number;
upperUncertainty?: number;
confidenceLevel?: number;
constructor(value: T) {
this.value = value;
}
/**
* Parses a QuakeML quantity xml element into a Quantity object.
*
* @param quantityQML the quantity xml Element
* @param grab a callback to obtain the value
* @param grabUncertainty a callback to obtain the uncertainties
* @returns Quantity instance
*/
static _createFromXml(
quantityQML: Element,
grab: (xml: Element | null | void, tagName: string) => T | undefined,
grabUncertainty: (
xml: Element | null | void,
tagName: string,
) => number | undefined,
): Quantity {
const value = grab(quantityQML, "value");
if (value === undefined) {
throw new Error("missing value");
}
const out = new Quantity(value);
out.uncertainty = grabUncertainty(quantityQML, "uncertainty");
out.lowerUncertainty = grabUncertainty(quantityQML, "lowerUncertainty");
out.upperUncertainty = grabUncertainty(quantityQML, "upperUncertainty");
out.confidenceLevel = _grabFirstElFloat(quantityQML, "confidenceLevel");
return out;
}
/**
* Parses a QuakeML real quantity xml element into a RealQuantity object.
*
* @param realQuantityQML the real quantity xml Element
* @returns RealQuantity instance
*/
static createRealQuantityFromXml(realQuantityQML: Element): RealQuantity {
return Quantity._createFromXml(
realQuantityQML,
_grabFirstElFloat,
_grabFirstElFloat,
);
}
/**
* Parses a QuakeML integer quantity xml element into a RealQuantity object.
*
* @param integerQuantityQML the integer quantity xml Element
* @returns IntegerQuantity instance
*/
static createIntegerQuantityFromXml(
integerQuantityQML: Element,
): IntegerQuantity {
return Quantity._createFromXml(
integerQuantityQML,
_grabFirstElFloat,
_grabFirstElInt,
);
}
/**
* Parses a QuakeML time quantity xml element into a TimeQuantity object.
*
* @param timeQuantityQML the time quantity xml Element
* @returns TimeQuantity instance
*/
static createTimeQuantityFromXml(timeQuantityQML: Element): TimeQuantity {
return Quantity._createFromXml(
timeQuantityQML,
_grabFirstElDateTime,
_grabFirstElFloat,
);
}
}
export type IntegerQuantity = Quantity;
export type RealQuantity = Quantity;
export type TimeQuantity = Quantity;
/**
Represents a QuakeML comment.
*/
export class Comment {
text: string;
creationInfo?: CreationInfo;
constructor(text: string) {
this.text = text;
}
/**
* Parses a QuakeML comment xml element into a Comment object.
*
* @param commentQML the comment xml Element
* @returns Comment instance
*/
static createFromXml(commentQML: Element): Comment {
const text = _grabFirstElText(commentQML, "text");
if (text === undefined) {
throw new Error("missing value");
}
const out = new Comment(text);
out.creationInfo = _grabFirstElCreationInfo(commentQML, "creationInfo");
return out;
}
}
export class CreationInfo {
agencyID?: string;
agencyURI?: string;
author?: string;
authorURI?: string;
creationTime?: DateTime;
version?: string;
/**
* Parses a QuakeML creation info xml element into a CreationInfo object.
*
* @param creationInfoQML the creation info xml Element
* @returns CreationInfo instance
*/
static createFromXml(creationInfoQML: Element): CreationInfo {
const out = new CreationInfo();
out.agencyID = _grabFirstElText(creationInfoQML, "agencyID");
out.agencyURI = _grabFirstElText(creationInfoQML, "agencyURI");
out.author = _grabFirstElText(creationInfoQML, "author");
out.authorURI = _grabFirstElText(creationInfoQML, "authorURI");
out.creationTime = _grabFirstElDateTime(creationInfoQML, "creationTime");
out.version = _grabFirstElText(creationInfoQML, "version");
return out;
}
}
/**
* Parses a QuakeML xml document into seisplotjs objects
*
* @param rawXml the xml Document to parse
* @param host optional source of the xml, helpful for parsing the eventid
* @returns EventParameters object
*/
export function parseQuakeML(rawXml: Document, host?: string): EventParameters {
const top = rawXml.documentElement;
if (!top) {
throw new Error("Can't get documentElement");
}
const eventParametersArray = Array.from(
top.getElementsByTagName("eventParameters"),
);
if (eventParametersArray.length !== 1) {
throw new Error(
`Document has ${eventParametersArray.length} eventParameters elements`,
);
}
return EventParameters.createFromXml(eventParametersArray[0], host);
}
export function createQuakeFromValues(
publicId: string,
time: DateTime,
latitude: number,
longitude: number,
depth_meter: number,
): Quake {
const origin = new Origin(
new Quantity(time),
new Quantity(latitude),
new Quantity(longitude),
);
origin.depth = new Quantity(depth_meter);
const quake = new Quake();
quake.publicId = publicId;
quake.originList.push(origin);
quake.preferredOrigin = origin;
return quake;
}
/**
* Fetches and parses QuakeML from a URL. This can be used in instances where
* a static quakeML 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 seconds in case of failed connection
* @param nodata nodata http error code
* @returns Promise to parsed quakeML as an EventParameters object
*/
export function fetchQuakeML(
url: string | URL,
timeoutSec = 10,
nodata = 204,
): Promise {
const fetchInit = defaultFetchInitObj(XML_MIME);
const host = new URL(url).hostname;
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 parseQuakeML(rawXml, host);
});
}
export function mightBeQuakeML(buf: ArrayBufferLike) {
if ( ! mightBeXml(buf)) {
return false;
}
const initialChars = dataViewToString(new DataView(buf.slice(0, 100))).trimStart();
if ( ! initialChars.includes("quakeml")) {
return false;
}
return true;
}
// these are similar methods as in seisplotjs.stationxml
// duplicate here to avoid dependency and diff NS, yes that is dumb...
const _grabAllElComment = function (
xml: Element | null | void,
tagName: string,
): Comment[] {
const out = [];
if (isObject(xml)) {
const elList = Array.from(xml.children).filter(
(e) => e.tagName === tagName,
);
for (const el of elList) {
if (isObject(el)) {
out.push(Comment.createFromXml(el));
}
}
}
return out;
};
const _grabFirstElNS = function (
xml: Element | null | void,
namespace: string,
tagName: string,
): Element | null {
let out = null;
if (isObject(xml)) {
const elList = xml.getElementsByTagNameNS(namespace, tagName);
for (let idx=0; idx e.tagName === tagName,
);
if (elList.length > 0) {
const e = elList[0];
if (e) {
return e;
}
}
}
return undefined;
};
const _grabFirstElText = function (
xml: Element | null | void,
tagName: string,
): string | undefined {
let out = undefined;
const el = _grabFirstEl(xml, tagName);
if (isObject(el)) {
out = el.textContent;
if (out === null) {
out = undefined;
}
}
return out;
};
const _grabFirstElBool = function (
xml: Element | null | void,
tagName: string,
): boolean | undefined {
const el = _grabFirstElText(xml, tagName);
if (!isStringArg(el)) {
return undefined;
}
switch (el) {
case "true":
case "1":
return true;
case "false":
case "0":
return false;
}
throw new Error("Invalid boolean: " + el);
};
const _grabFirstElInt = function (
xml: Element | null | void,
tagName: string,
): number | undefined {
let out = undefined;
const el = _grabFirstElText(xml, tagName);
if (isStringArg(el)) {
out = parseInt(el);
}
return out;
};
const _grabFirstElFloat = function (
xml: Element | null | void,
tagName: string,
): number | undefined {
let out = undefined;
const el = _grabFirstElText(xml, tagName);
if (isStringArg(el)) {
out = parseFloat(el);
}
return out;
};
const _grabFirstElDateTime = function (
xml: Element | null | void,
tagName: string,
): DateTime | undefined {
let out = undefined;
const el = _grabFirstElText(xml, tagName);
if (isStringArg(el)) {
out = isoToDateTime(el);
}
return out;
};
const _grabFirstElType = function (createFromXml: (el: Element) => T) {
return function (xml: Element | null | void, tagName: string): T | undefined {
let out = undefined;
const el = _grabFirstEl(xml, tagName);
if (isObject(el)) {
out = createFromXml(el);
}
return out;
};
};
const _grabFirstElRealQuantity = _grabFirstElType(
Quantity.createRealQuantityFromXml.bind(Quantity),
);
const _grabFirstElIntegerQuantity = _grabFirstElType(
Quantity.createIntegerQuantityFromXml.bind(Quantity),
);
const _grabFirstElTimeQuantity = _grabFirstElType(
Quantity.createTimeQuantityFromXml.bind(Quantity),
);
const _grabFirstElCreationInfo = _grabFirstElType(
CreationInfo.createFromXml.bind(CreationInfo),
);
const _grabAttribute = function (
xml: Element | null | void,
tagName: string,
): string | undefined {
let out = undefined;
if (isObject(xml)) {
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 | undefined {
let out = undefined;
if (isObject(xml)) {
const a = xml.getAttributeNS(namespace, tagName);
if (isStringArg(a)) {
out = a;
}
}
return out;
};
export const parseUtil = {
_grabFirstEl: _grabFirstEl,
_grabFirstElNS: _grabFirstElNS,
_grabFirstElText: _grabFirstElText,
_grabFirstElFloat: _grabFirstElFloat,
_grabFirstElInt: _grabFirstElInt,
_grabAttribute: _grabAttribute,
_requireAttribute: _requireAttribute,
_grabAttributeNS: _grabAttributeNS,
};