UNPKG

@seismo/core

Version:

This is the package for the core library of Seismo, a JavaScript library for seismic data processing and visualization. It provides utilities for handling seismic data, including FDSN web services, waveform processing, and event handling. The library is d

1,223 lines (1,222 loc) 40.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.SeismogramDisplayStats = exports.SeismogramDisplayData = exports.NonContiguousData = exports.Seismogram = exports.COUNT_UNIT = void 0; exports.ensureIsSeismogram = ensureIsSeismogram; exports.findStartEnd = findStartEnd; exports.findMaxDuration = findMaxDuration; exports.findMaxDurationOfType = findMaxDurationOfType; exports.findMinMax = findMinMax; exports.findMinMaxOverTimeRange = findMinMaxOverTimeRange; exports.findMinMaxOverRelativeTimeRange = findMinMaxOverRelativeTimeRange; exports.calcMinMax = calcMinMax; exports.findStartEndOfSeismograms = findStartEndOfSeismograms; exports.findMinMaxOfSeismograms = findMinMaxOfSeismograms; exports.findMinMaxOfSDD = findMinMaxOfSDD; exports.uniqueStations = uniqueStations; exports.uniqueChannels = uniqueChannels; exports.uniqueQuakes = uniqueQuakes; /* * Philip Crotwell * University of South Carolina, 2019 * https://www.seis.sc.edu */ const luxon_1 = require("luxon"); const source_id_1 = require("../fdsn/source-id"); const utils_1 = require("../utils"); const distaz_1 = require("../utils/distaz"); const stationxml_1 = require("../stations/stationxml"); const scale_1 = require("../waveform/scale"); const seismogram_segment_1 = require("./seismogram-segment"); exports.COUNT_UNIT = "count"; /** * Represents time window for a single channel that may * contain gaps or overlaps, but is otherwise more or less * continuous, or at least adjacent data from the channel. * Each segment within * the Seismogram will have the same units, channel identifiers * and sample rate, but cover different times. */ class Seismogram { _segmentArray; _interval; _y; constructor(segmentArray) { this._y = null; if (Array.isArray(segmentArray) && segmentArray[0] instanceof seismogram_segment_1.SeismogramSegment) { this._segmentArray = segmentArray; } else if (segmentArray instanceof seismogram_segment_1.SeismogramSegment) { this._segmentArray = [segmentArray]; } else { throw new Error(`segmentArray is not Array<SeismogramSegment> or SeismogramSegment: ${(0, utils_1.stringify)(segmentArray)}`); } this.checkAllSimilar(); this._interval = this.findStartEnd(); (0, utils_1.checkLuxonValid)(this._interval, "seis const"); } checkAllSimilar() { if (this._segmentArray.length === 0) { throw new Error("Seismogram is empty"); } const f = this._segmentArray[0]; this._segmentArray.forEach((s, i) => { if (!s) { throw new Error(`index ${i} is null in trace`); } this.checkSimilar(f, s); }); } checkSimilar(f, s) { if (!s.sourceId.equals(f.sourceId)) { throw new Error(`SourceId not same: ${s.sourceId.toString()} !== ${f.sourceId.toString()}`); } if (s.yUnit !== f.yUnit) { throw new Error("yUnit not same: " + s.yUnit + " !== " + f.yUnit); } } findStartEnd() { if (this._segmentArray.length === 0) { throw new Error("Seismogram is empty"); } return this._segmentArray.reduce((acc, cur) => acc.union(cur.timeRange), this._segmentArray[0].timeRange); } findMinMax(minMaxAccumulator) { if (this._segmentArray.length === 0) { throw new Error("No data"); } for (const s of this._segmentArray) { minMaxAccumulator = s.findMinMax(minMaxAccumulator); } if (minMaxAccumulator) { return minMaxAccumulator; } else { throw new Error("No data to calc minmax"); } } /** * calculates the mean of a seismogrma. * * @returns mean value */ mean() { let meanVal = 0; const npts = this.numPoints; for (const s of this.segments) { meanVal += (0, utils_1.meanOfSlice)(s.y, s.y.length) * s.numPoints; } meanVal = meanVal / npts; return meanVal; } get start() { return this.startTime; } get startTime() { return (0, utils_1.validStartTime)(this._interval); } get end() { return this.endTime; } get endTime() { return (0, utils_1.validEndTime)(this._interval); } get timeRange() { return this._interval; } get networkCode() { return this.sourceId.networkCode; } get stationCode() { return this.sourceId.stationCode; } get locationCode() { return this.sourceId.locationCode; } get channelCode() { return this.sourceId.formChannelCode(); } /** * return FDSN source id as a string. * * @returns FDSN source id */ get sourceId() { return this._segmentArray[0].sourceId; } set sourceId(sid) { this._segmentArray.forEach((s) => (s.sourceId = sid)); } get sampleRate() { return this._segmentArray[0].sampleRate; } get samplePeriod() { return 1.0 / this.sampleRate; } get yUnit() { return this._segmentArray[0].yUnit; } isYUnitCount() { return this.yUnit?.toLowerCase() === exports.COUNT_UNIT; } get numPoints() { return this._segmentArray.reduce((accumulator, seis) => accumulator + seis.numPoints, 0); } hasCodes() { return this._segmentArray[0].hasCodes(); } /** * return network, station, location and channels codes as one string. * Uses this.channel if it exists, this.seismogram if not. * * @returns net.sta.loc.chan */ get nslc() { return this.codes(); } get nslcId() { return this._segmentArray[0].nslcId; } codes() { return this._segmentArray[0].codes(); } get segments() { return this._segmentArray; } append(seismogram) { if (seismogram instanceof Seismogram) { seismogram._segmentArray.forEach((s) => this.append(s)); } else { this.checkSimilar(this._segmentArray[0], seismogram); this._interval = this._interval.union(seismogram.timeRange); this._segmentArray.push(seismogram); } } /** * Cut the seismogram. Creates a new seismogram with all datapoints * contained in the time window. * * @param timeRange start and end of cut * @returns new seismogram */ cut(timeRange) { // coarse trim first let out = this.trim(timeRange); if (out && out._segmentArray) { const cutSeisArray = this._segmentArray .map((seg) => seg.cut(timeRange)) .filter(utils_1.isDef); if (cutSeisArray.length > 0) { out = new Seismogram(cutSeisArray); } else { out = null; } } else { out = null; } return out; } /** * Creates a new Seismogram composed of all seismogram segments that overlap the * given time window. If none do, this returns null. This is a faster but coarser * version of cut as it only removes whole segments that do not overlap the * time window. For most seismograms that consist of a single contiguous * data segment, this will do nothing. * * @param timeRange time range to trim to * @returns new seismogram if data in the window, null otherwise * @see cut */ trim(timeRange) { let out = null; (0, utils_1.checkLuxonValid)(timeRange); const timeRange_start = (0, utils_1.validStartTime)(timeRange); const timeRange_end = (0, utils_1.validEndTime)(timeRange); if (this._segmentArray) { const trimSeisArray = this._segmentArray .filter(function (d) { return d.endTime >= timeRange_start; }) .filter(function (d) { return d.startTime <= timeRange_end; }); if (trimSeisArray.length > 0) { out = new Seismogram(trimSeisArray); } } return out; } break(duration) { if (duration.valueOf() < 0) { throw new Error(`Negative duration not allowed: ${duration.toString()}`); } if (this._segmentArray) { let breakStart = this.startTime; let out = []; while (breakStart < this.endTime) { const breakWindow = luxon_1.Interval.after(breakStart, duration); const cutSeisArray = this._segmentArray .map((seg) => seg.cut(breakWindow)) .filter(utils_1.isDef); out = out.concat(cutSeisArray); breakStart = breakStart.plus(duration); } // check for null, filter true if seg not null out = out.filter(utils_1.isDef); this._segmentArray = out; } } isContiguous() { if (this._segmentArray.length === 1) { return true; } let prev = null; for (const s of this._segmentArray) { if (prev && !(prev.endTime < s.startTime && prev.endTime.plus(luxon_1.Duration.fromMillis((1000 * 1.5) / prev.sampleRate)) > s.startTime)) { return false; } prev = s; } return true; } /** * Merges all segments into a single array of the same type as the first * segment. No checking is done for gaps or overlaps, this is a simple * congatination. Be careful! * * @returns contatenated data */ merge() { let outArray; let idx = 0; if (this._segmentArray.every((seg) => seg.y instanceof Int32Array)) { outArray = new Int32Array(this.numPoints); this._segmentArray.forEach((seg) => { outArray.set(seg.y, idx); idx += seg.y.length; }); } else if (this._segmentArray.every((seg) => seg.y instanceof Float32Array)) { outArray = new Float32Array(this.numPoints); this._segmentArray.forEach((seg) => { outArray.set(seg.y, idx); idx += seg.y.length; }); } else if (this._segmentArray.every((seg) => seg.y instanceof Float64Array)) { outArray = new Float64Array(this.numPoints); this._segmentArray.forEach((seg) => { outArray.set(seg.y, idx); idx += seg.y.length; }); } else { throw new Error(`data not all same one of Int32Array, Float32Array or Float64Array`); } return outArray; } /** * Gets the timeseries as an typed array if it is contiguous. * * @throws {NonContiguousData} if data is not contiguous. * @returns timeseries as array of number */ get y() { if (!this._y) { if (this.isContiguous()) { this._y = this.merge(); } } if (this._y) { return this._y; } else { throw new Error("Seismogram is not contiguous, access each SeismogramSegment idividually."); } } set y(val) { // ToDo throw new Error("seismogram y setter not impl, see cloneWithNewData()"); } clone() { const cloned = this._segmentArray.map((s) => s.clone()); return new Seismogram(cloned); } cloneWithNewData(newY) { if (newY && newY.length > 0) { const seg = this._segmentArray[0].cloneWithNewData(newY); return new Seismogram([seg]); } else { throw new Error("Y value is empty"); } } /** * factory method to create a single segment Seismogram from either encoded data * or a TypedArray, along with sample rate and start time. * * @param yArray array of encoded data or typed array * @param sampleRate sample rate, samples per second of the data * @param startTime time of first sample * @param sourceId optional source id * @returns seismogram initialized with the data */ static fromContiguousData(yArray, sampleRate, startTime, sourceId) { const seg = new seismogram_segment_1.SeismogramSegment(yArray, sampleRate, startTime, sourceId); return new Seismogram([seg]); } } exports.Seismogram = Seismogram; class NonContiguousData extends Error { constructor(message) { super(message); this.name = this.constructor.name; } } exports.NonContiguousData = NonContiguousData; function ensureIsSeismogram(seisSeismogram) { if (typeof seisSeismogram === "object") { if (seisSeismogram instanceof Seismogram) { return seisSeismogram; } else if (seisSeismogram instanceof seismogram_segment_1.SeismogramSegment) { return new Seismogram([seisSeismogram]); } else { throw new Error("must be Seismogram or SeismogramSegment but " + (0, utils_1.stringify)(seisSeismogram)); } } else { throw new Error("must be Seismogram or SeismogramSegment but not an object: " + (0, utils_1.stringify)(seisSeismogram)); } } class SeismogramDisplayData { /** @private */ _seismogram; _id; _sourceId; label; markerList; traveltimeList; channel; _instrumentSensitivity; quakeList; quakeReferenceList = []; timeRange; alignmentTime; doShow; _statsCache; constructor(timeRange) { if (!timeRange) { throw new Error("timeRange must not be missing."); } (0, utils_1.checkLuxonValid)(timeRange); this._id = null; this._sourceId = null; this._seismogram = null; this.label = null; this.markerList = []; this.traveltimeList = []; this.channel = null; this._instrumentSensitivity = null; this.quakeList = []; this.timeRange = timeRange; this.alignmentTime = (0, utils_1.validStartTime)(timeRange); this.doShow = true; this._statsCache = null; } static fromSeismogram(seismogram) { const out = new SeismogramDisplayData(luxon_1.Interval.fromDateTimes(seismogram.startTime, seismogram.endTime)); out.seismogram = seismogram; return out; } /** * Create a Seismogram from the segment, then call fromSeismogram to create * the SeismogramDisplayData; * @param seisSegment segment of contiguous data * @returns new SeismogramDisplayData */ static fromSeismogramSegment(seisSegment) { return SeismogramDisplayData.fromSeismogram(new Seismogram([seisSegment])); } /** * Useful for creating fake data from an array, sample rate and start time * * @param yArray fake data * @param sampleRate samples per second * @param startTime start of data, time of first point * @param sourceId optional source id * @returns seismogramdisplaydata */ static fromContiguousData(yArray, sampleRate, startTime, sourceId) { return SeismogramDisplayData.fromSeismogram(Seismogram.fromContiguousData(yArray, sampleRate, startTime, sourceId)); } static fromChannelAndTimeWindow(channel, timeRange) { if (!channel) { throw new Error("fromChannelAndTimeWindow, channel is undef"); } const out = new SeismogramDisplayData(timeRange); out.channel = channel; return out; } static fromChannelAndTimes(channel, startTime, endTime) { const out = new SeismogramDisplayData(luxon_1.Interval.fromDateTimes(startTime, endTime)); out.channel = channel; return out; } static fromSourceIdAndTimes(sourceId, startTime, endTime) { const out = new SeismogramDisplayData(luxon_1.Interval.fromDateTimes(startTime, endTime)); out._sourceId = sourceId; return out; } static fromCodesAndTimes(networkCode, stationCode, locationCode, channelCode, startTime, endTime) { const out = new SeismogramDisplayData(luxon_1.Interval.fromDateTimes(startTime, endTime)); out._sourceId = source_id_1.FDSNSourceId.fromNslc(networkCode, stationCode, locationCode, channelCode); return out; } addQuake(quake) { if (Array.isArray(quake)) { quake.forEach((q) => this.quakeList.push(q)); } else { this.quakeList.push(quake); } } /** * Adds a public id for a Quake to the seismogram. For use in case where * the quake is not yet available, but wish to retain the connection. * @param publicId id of the earthquake assocated with this seismogram */ addQuakeId(publicId) { this.quakeReferenceList.push(publicId); } addMarker(marker) { this.addMarkers([marker]); } addMarkers(markers) { if (Array.isArray(markers)) { markers.forEach((m) => this.markerList.push(m)); } else { this.markerList.push(markers); } } getMarkers() { return this.markerList; } addTravelTimes(ttimes) { if (Array.isArray(ttimes)) { ttimes.forEach((m) => this.traveltimeList.push(m)); } else if ("arrivals" in ttimes) { // TraveltimeJsonType ttimes.arrivals.forEach((m) => this.traveltimeList.push(m)); } else { this.traveltimeList.push(ttimes); } } hasQuake() { return this.quakeList.length > 0; } get quake() { if (this.hasQuake()) { return this.quakeList[0]; } return null; } hasSeismogram() { return (0, utils_1.isDef)(this._seismogram); } append(seismogram) { if ((0, utils_1.isDef)(this._seismogram)) { this._seismogram.append(seismogram); if (this.startTime > seismogram.startTime || this.endTime < seismogram.endTime) { const startTime = this.startTime < seismogram.startTime ? this.startTime : seismogram.startTime; const endTime = this.endTime > seismogram.endTime ? this.endTime : seismogram.endTime; this.timeRange = luxon_1.Interval.fromDateTimes(startTime, endTime); } } else { if (seismogram instanceof seismogram_segment_1.SeismogramSegment) { this.seismogram = new Seismogram(seismogram); } else { this.seismogram = seismogram; } } this._statsCache = null; } hasChannel() { return (0, utils_1.isDef)(this.channel); } hasSensitivity() { return (this._instrumentSensitivity !== null || ((0, utils_1.isDef)(this.channel) && this.channel.hasInstrumentSensitivity())); } /** * Allows id-ing a seismogram. Optional. * * @returns string id */ get id() { return this._id; } /** * Allows iding a seismogram. Optional. * * @param value string id */ set id(value) { this._id = value; } /** * return network code as a string. * Uses this.channel if it exists, this.seismogram if not. * * @returns network code */ get networkCode() { let out = this.sourceId.networkCode; if (!(0, utils_1.isDef)(out)) { out = "unknown"; } return out; } /** * return station code as a string. * Uses this.channel if it exists, this.seismogram if not. * * @returns station code */ get stationCode() { let out = this.sourceId.stationCode; if (!(0, utils_1.isDef)(out)) { out = "unknown"; } return out; } /** * return location code a a string. * Uses this.channel if it exists, this.seismogram if not. * * @returns location code */ get locationCode() { let out = this.sourceId.locationCode; if (!(0, utils_1.isDef)(out)) { out = "unknown"; } return out; } /** * return channels code as a string. * Uses this.channel if it exists, this.seismogram if not. * * @returns channel code */ get channelCode() { let out = this.sourceId.formChannelCode(); if (!(0, utils_1.isDef)(out)) { out = "unknown"; } return out; } /** * return FDSN source id as a string. * Uses this.channel if it exists, this.seismogram if not. * * @returns FDSN source id */ get sourceId() { if ((0, utils_1.isDef)(this.channel)) { return this.channel.sourceId; } else if ((0, utils_1.isDef)(this._seismogram)) { return this._seismogram.sourceId; } else if ((0, utils_1.isDef)(this._sourceId)) { return this._sourceId; } else { // should not happen return source_id_1.FDSNSourceId.createUnknown(); //throw new Error("unable to create Id, neither channel, _sourceId nor seismogram"); } } /** * return network, station, location and channels codes as one string. * Uses this.channel if it exists, this.seismogram if not * * @returns net.sta.loc.chan */ get nslc() { return this.codes(); } get nslcId() { if (this.channel !== null) { return this.channel.nslcId; } else { return new source_id_1.NslcId(this.networkCode ? this.networkCode : "", this.stationCode ? this.stationCode : "", this.locationCode && this.locationCode !== "--" ? this.locationCode : "", this.channelCode ? this.channelCode : ""); } } /** * return network, station, location and channels codes as one string. * Uses this.channel if it exists, this.seismogram if not. * * @param sep separator, defaults to '.' * @returns nslc codes separated by sep */ codes(sep = ".") { if (this.channel !== null) { return this.channel.codes(); } else { return ((this.networkCode ? this.networkCode : "") + sep + (this.stationCode ? this.stationCode : "") + sep + (this.locationCode ? this.locationCode : "") + sep + (this.channelCode ? this.channelCode : "")); } } get startTime() { return (0, utils_1.validStartTime)(this.timeRange); } get start() { return this.startTime; } get endTime() { return (0, utils_1.validEndTime)(this.timeRange); } get end() { return this.endTime; } get numPoints() { if (this._seismogram) { return this._seismogram.numPoints; } return 0; } associateChannel(nets) { const matchChans = (0, stationxml_1.findChannels)(nets, this.networkCode, this.stationCode, this.locationCode, this.channelCode); for (const c of matchChans) { if (c.timeRange.overlaps(this.timeRange)) { this.channel = c; return; } } } alignStartTime() { this.alignmentTime = this.start; } alignOriginTime() { if (this.quake) { this.alignmentTime = this.quake.time; } else { this.alignmentTime = this.start; } } alignPhaseTime(phaseRegExp) { let intPhaseRegExp; if (typeof phaseRegExp === "string") { intPhaseRegExp = new RegExp(phaseRegExp); } else { intPhaseRegExp = phaseRegExp; } if (this.quake && this.traveltimeList) { const q = this.quake; const matchArrival = this.traveltimeList.find((ttArrival) => { const match = intPhaseRegExp.exec(ttArrival.phase); // make sure regexp matches whole string, not just partial return match !== null && match[0] === ttArrival.phase; }); if (matchArrival) { this.alignmentTime = q.time.plus(luxon_1.Duration.fromMillis(matchArrival.time * 1000)); //seconds } else { this.alignmentTime = this.start; } } } /** * Create a time window relative to the alignmentTime if set, or the start time if not. * Negative durations are allowed. * @param alignmentOffset offset duration from the alignment time * @param duration duration from the offset for the window * @returns time window as an Interval */ relativeTimeWindow(alignmentOffset, duration) { const atime = this.alignmentTime ? this.alignmentTime.plus(alignmentOffset) : this.startTime.plus(alignmentOffset); return (0, utils_1.startDuration)(atime, duration); } get sensitivity() { const channel = this.channel; if (this._instrumentSensitivity) { return this._instrumentSensitivity; } else if ((0, utils_1.isDef)(channel) && channel.hasInstrumentSensitivity()) { return channel.instrumentSensitivity; } else { return null; } } set sensitivity(value) { this._instrumentSensitivity = value; } get min() { if (!this._statsCache) { this._statsCache = this.calcStats(); } return this._statsCache.min; } get max() { if (!this._statsCache) { this._statsCache = this.calcStats(); } return this._statsCache.max; } get mean() { if (!this._statsCache) { this._statsCache = this.calcStats(); } return this._statsCache.mean; } get middle() { if (!this._statsCache) { this._statsCache = this.calcStats(); } return this._statsCache.middle; } get seismogram() { return this._seismogram; } set seismogram(value) { this._seismogram = value; this._statsCache = null; } get segments() { if (this._seismogram) { return this._seismogram.segments; } else { return []; } } calcStats() { const stats = new SeismogramDisplayStats(); if (this.seismogram) { const minMax = this.seismogram.findMinMax(); stats.min = minMax.min; stats.max = minMax.max; stats.mean = this.seismogram.mean(); } this._statsCache = stats; return stats; } /** * Calculates distance and azimuth for each event in quakeList. * * @returns Array of DistAzOutput, empty array if no quakes. */ get distazList() { if (this.quakeList.length > 0 && (0, utils_1.isDef)(this.channel)) { const c = this.channel; return this.quakeList.map((q) => (0, distaz_1.distaz)(c.latitude, c.longitude, q.latitude, q.longitude)); } return []; } /** * Calculates distance and azimuth for the first event in quakeList. This is * a convienence method as usually there will only be one quake. * * @returns DistAzOutput, null if no quakes. */ get distaz() { let out = null; if (this.quakeList.length > 0 && this.channel !== null) { out = (0, distaz_1.distaz)(this.channel.latitude, this.channel.longitude, this.quakeList[0].latitude, this.quakeList[0].longitude); } return out; } clone() { return this.cloneWithNewSeismogram(this.seismogram ? this.seismogram.clone() : null); } cloneWithNewSeismogram(seis) { const out = new SeismogramDisplayData(this.timeRange); const handled = ["_seismogram", "_statsCache", "_sourceId"]; Object.assign(out, this); Object.getOwnPropertyNames(this).forEach((name) => { // @ts-expect-error typscript can't handle reflection, but is ok here as just cloning const v = this[name]; if (handled.find((n) => name === n)) { // handled below } else if (Array.isArray(v)) { // @ts-expect-error typscript can't handle reflection, but is ok here as just cloning out[name] = v.slice(); } }); out.seismogram = seis; out._statsCache = null; if (!(0, utils_1.isDef)(out._seismogram) && !(0, utils_1.isDef)(out.channel)) { // so we con't forget our channel if (this.sourceId) { out._sourceId = this.sourceId.clone(); } } return out; } /** * Cut the seismogram. Creates a new seismogramDisplayData with the cut * seismogram and the timeRange set to the new time window. * * @param timeRange start and end of cut * @returns new seismogramDisplayData */ cut(timeRange) { let cutSeis = this.seismogram; let out; if (cutSeis) { cutSeis = cutSeis.cut(timeRange); out = this.cloneWithNewSeismogram(cutSeis); if (!(0, utils_1.isDef)(out._seismogram) && !(0, utils_1.isDef)(out.channel)) { // so we con't forget our channel out._sourceId = this.sourceId; } } else { // no seismogram, so just clone? out = this.clone(); } out.timeRange = timeRange; return out; } /** * Coarse trim the seismogram. Creates a new seismogramDisplayData with the * trimmed seismogram and the timeRange set to the new time window. * If timeRange is not given, the current time range of the * SeismogramDisplayData is used, effectively trimming data to the current * window. * * @param timeRange start and end of cut * @returns new seismogramDisplayData */ trim(timeRange) { if (!timeRange) { timeRange = this.timeRange; } let cutSeis = this.seismogram; let out; if (cutSeis) { cutSeis = cutSeis.trim(timeRange); out = this.cloneWithNewSeismogram(cutSeis); if (!(0, utils_1.isDef)(out._seismogram) && !(0, utils_1.isDef)(out.channel)) { // so we con't forget our channel out._sourceId = this.sourceId; } } else { // no seismogram, so just clone? out = this.clone(); } out.timeRange = timeRange; return out; } /** * Coarse trim the seismogram in place. The seismogram is * trimmed to the given time window. * If timeRange is not given, the current time range of the * SeismogramDisplayData is used, effectively trimming data to the current * window. * * @param timeRange start and end of cut */ trimInPlace(timeRange) { if (!timeRange) { timeRange = this.timeRange; } const cutSeis = this.seismogram; if (cutSeis) { this.seismogram = cutSeis.trim(timeRange); } } toString() { return `${this.sourceId.toString()} ${this.timeRange.toString()}`; } } exports.SeismogramDisplayData = SeismogramDisplayData; class SeismogramDisplayStats { min; max; mean; trendSlope; constructor() { this.min = 0; this.max = 0; this.mean = 0; this.trendSlope = 0; } get middle() { return (this.min + this.max) / 2; } } exports.SeismogramDisplayStats = SeismogramDisplayStats; function findStartEnd(sddList) { if (sddList.length === 0) { // just use zero length at now return luxon_1.Interval.before(luxon_1.DateTime.utc(), 0); } return sddList.reduce((acc, sdd) => acc.union(sdd.timeRange), sddList[0].timeRange); } function findMaxDuration(sddList) { return findMaxDurationOfType("start", sddList); } /** * Finds max duration of from one of starttime of sdd, origin time * of earthquake, or alignmentTime. * * @param type one of start, origin or align * @param sddList list of seis data * @returns max duration */ function findMaxDurationOfType(type, sddList) { return sddList.reduce((acc, sdd) => { let timeRange; if (type === "start") { timeRange = sdd.timeRange; } else if (type === "origin" && sdd.hasQuake()) { timeRange = luxon_1.Interval.fromDateTimes(sdd.quakeList[0].time, (0, utils_1.validEndTime)(sdd.timeRange)); } else if (type === "align" && sdd.alignmentTime) { timeRange = luxon_1.Interval.fromDateTimes(sdd.alignmentTime, (0, utils_1.validEndTime)(sdd.timeRange)); } else { timeRange = sdd.timeRange; } if (timeRange.toDuration().toMillis() > acc.toMillis()) { return timeRange.toDuration(); } else { return acc; } }, luxon_1.Duration.fromMillis(0)); } /** * Finds the min and max amplitude over the seismogram list, considering gain * and how to center the seismograms, either Raw, MinMax or Mean. * * @param sddList list of seismogramdisplaydata * @param doGain should gain be used * @param amplitudeMode centering style * @returns min max */ function findMinMax(sddList, doGain = false, amplitudeMode = scale_1.AMPLITUDE_MODE.MinMax) { return findMinMaxOverTimeRange(sddList, null, doGain, amplitudeMode); } function findMinMaxOverTimeRange(sddList, timeRange = null, doGain = false, amplitudeMode = scale_1.AMPLITUDE_MODE.MinMax) { if (sddList.length === 0) { return new scale_1.MinMaxable(-1, 1); } const minMaxArr = sddList .map((sdd) => { return calcMinMax(sdd, timeRange, doGain, amplitudeMode); }) .filter((x) => x) // remove nulls .reduce(function (p, v) { if (amplitudeMode === scale_1.AMPLITUDE_MODE.Raw || amplitudeMode === scale_1.AMPLITUDE_MODE.Zero) { return p ? (v ? p.union(v) : p) : v; } else { // non-Raw mode assumes only halfwidth matters, middle will be zeroed let hw = 0; if (p && v) { hw = Math.max(p.halfWidth, v.halfWidth); } else if (p) { hw = p.halfWidth; } else if (v) { hw = v.halfWidth; } else { hw = 0; } return scale_1.MinMaxable.fromMiddleHalfWidth(0, hw); } }, null); if (minMaxArr) { return minMaxArr; } return new scale_1.MinMaxable(-1, 1); // no data, just return something. } function findMinMaxOverRelativeTimeRange(sddList, alignmentOffset, duration, doGain = false, amplitudeMode = scale_1.AMPLITUDE_MODE.MinMax) { if (sddList.length === 0) { return new scale_1.MinMaxable(0, 0); } const minMaxArr = sddList .map((sdd) => { const timeRange = sdd.relativeTimeWindow(alignmentOffset, duration); return calcMinMax(sdd, timeRange, doGain, amplitudeMode); }) .filter((x) => x) // remove nulls .reduce(function (p, v) { // Raw and Zero are actual values, no centering if (amplitudeMode === scale_1.AMPLITUDE_MODE.Raw || amplitudeMode === scale_1.AMPLITUDE_MODE.Zero) { return p ? (v ? p.union(v) : p) : v; } else { // non-Raw mode assumes only halfwidth matters, middle will be zeroed let hw = 0; if (p && v) { hw = Math.max(p.halfWidth, v.halfWidth); } else if (p) { hw = p.halfWidth; } else if (v) { hw = v.halfWidth; } else { hw = 0; } return scale_1.MinMaxable.fromMiddleHalfWidth(0, hw); } }, null); if (minMaxArr) { return minMaxArr; } return new scale_1.MinMaxable(-1, 1); // no data, just return something. } function calcMinMax(sdd, timeRange = null, doGain = false, amplitudeMode = scale_1.AMPLITUDE_MODE.MinMax) { if (sdd.seismogram) { let cutSDD; if (timeRange) { cutSDD = sdd.cut(timeRange); } else { cutSDD = sdd; } if (cutSDD) { let sens = 1.0; if (doGain && sdd.sensitivity) { sens = sdd.sensitivity.sensitivity; } let middle = 0; let halfWidth = 0; if (amplitudeMode === scale_1.AMPLITUDE_MODE.MinMax || amplitudeMode === scale_1.AMPLITUDE_MODE.Raw) { middle = cutSDD.middle; halfWidth = Math.max((middle - cutSDD.min) / sens, (cutSDD.max - middle) / sens); } else if (amplitudeMode === scale_1.AMPLITUDE_MODE.Mean) { middle = sdd.mean; halfWidth = Math.max((middle - cutSDD.min) / sens, (cutSDD.max - middle) / sens); } else if (amplitudeMode === scale_1.AMPLITUDE_MODE.Zero) { const minwz = Math.min(0, cutSDD.min); const maxwz = Math.max(0, cutSDD.max); middle = (minwz + maxwz) / 2.0; halfWidth = (maxwz - minwz) / 2.0 / sens; } else { throw new Error(`Unknown amplitudeMode: ${(0, utils_1.stringify)(amplitudeMode)}. Must be one of raw, zero, minmax, mean`); } return scale_1.MinMaxable.fromMiddleHalfWidth(middle, halfWidth); } } // otherwise return null; } function findStartEndOfSeismograms(data, accumulator) { let out; if (!accumulator && !data) { throw new Error("data and accumulator are not defined"); } else if (!accumulator) { if (data.length !== 0) { out = data[0].timeRange; } else { throw new Error("data.length == 0 and accumulator is not defined"); } } else { out = accumulator; } if (Array.isArray(data)) { return data.reduce((acc, cur) => acc.union(cur.timeRange), data[0].timeRange); } else { throw new Error(`Expected Array as first arg but was: ${typeof data}`); } return out; } function findMinMaxOfSeismograms(data, minMaxAccumulator) { for (const s of data) { minMaxAccumulator = s.findMinMax(minMaxAccumulator); } if (minMaxAccumulator) { return minMaxAccumulator; } else { return new scale_1.MinMaxable(-1, 1); } } function findMinMaxOfSDD(data, minMaxAccumulator) { const seisData = []; data.forEach((sdd) => { if (!!sdd && !!sdd.seismogram) { seisData.push(sdd.seismogram); } }); return findMinMaxOfSeismograms(seisData, minMaxAccumulator); } function uniqueStations(seisData) { const out = new Set(); seisData.forEach((sdd) => { if (sdd.channel) { out.add(sdd.channel.station); } }); return Array.from(out.values()); } function uniqueChannels(seisData) { const out = new Set(); seisData.forEach((sdd) => { if (sdd.channel) { out.add(sdd.channel); } }); return Array.from(out.values()); } function uniqueQuakes(seisData) { const out = new Set(); seisData.forEach((sdd) => { sdd.quakeList.forEach((q) => out.add(q)); }); return Array.from(out.values()); }