@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
JavaScript
"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());
}