@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
762 lines (761 loc) • 25.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.XHTML_NS = exports.SVG_NS = exports.default_fetch = exports.WAY_FUTURE = exports.UTC_OPTIONS = exports.BINARY_MIME = exports.TEXT_MIME = exports.SVG_MIME = exports.JSONAPI_MIME = exports.JSON_MIME = exports.XML_MIME = void 0;
exports.hasArgs = hasArgs;
exports.hasNoArgs = hasNoArgs;
exports.isStringArg = isStringArg;
exports.isNumArg = isNumArg;
exports.isNonEmptyStringArg = isNonEmptyStringArg;
exports.isObject = isObject;
exports.isDef = isDef;
exports.reErrorWithMessage = reErrorWithMessage;
exports.asStringDictionary = asStringDictionary;
exports.doStringGetterSetter = doStringGetterSetter;
exports.doBoolGetterSetter = doBoolGetterSetter;
exports.doIntGetterSetter = doIntGetterSetter;
exports.doFloatGetterSetter = doFloatGetterSetter;
exports.doMomentGetterSetter = doMomentGetterSetter;
exports.dataViewToString = dataViewToString;
exports.log = log;
exports.isError = isError;
exports.toError = toError;
exports.warn = warn;
exports.stringify = stringify;
exports.isoToDateTime = isoToDateTime;
exports.startEnd = startEnd;
exports.startDuration = startDuration;
exports.durationEnd = durationEnd;
exports.calcClockOffset = calcClockOffset;
exports.checkStringOrDate = checkStringOrDate;
exports.makeParam = makeParam;
exports.makePostParam = makePostParam;
exports.toIsoWoZ = toIsoWoZ;
exports.validStartTime = validStartTime;
exports.validEndTime = validEndTime;
exports.toJSDate = toJSDate;
exports.checkLuxonValid = checkLuxonValid;
exports.checkProtocol = checkProtocol;
exports.defaultFetchInitObj = defaultFetchInitObj;
exports.cloneFetchInitObj = cloneFetchInitObj;
exports.errorFetch = errorFetch;
exports.setDefaultFetch = setDefaultFetch;
exports.getFetch = getFetch;
exports.doFetchWithTimeout = doFetchWithTimeout;
exports.downloadBlobAsFile = downloadBlobAsFile;
exports.meanOfSlice = meanOfSlice;
exports.createSVGElement = createSVGElement;
exports.updateVersionText = updateVersionText;
/*
* Philip Crotwell
* University of South Carolina, 2019
* https://www.seis.sc.edu
*/
const version_1 = require("../version");
const luxon_1 = require("luxon");
exports.XML_MIME = "application/xml";
exports.JSON_MIME = "application/json";
exports.JSONAPI_MIME = "application/vnd.api+json";
exports.SVG_MIME = "image/svg+xml";
exports.TEXT_MIME = "text/plain";
exports.BINARY_MIME = "application/octet-stream";
exports.UTC_OPTIONS = { zone: luxon_1.FixedOffsetZone.utcInstance };
function hasArgs(value) {
return arguments.length !== 0 && typeof value !== "undefined";
}
function hasNoArgs(value) {
return arguments.length === 0 || typeof value === "undefined";
}
function isStringArg(value) {
return (arguments.length !== 0 &&
(typeof value === "string" || (isObject(value) && value instanceof String)));
}
function isNumArg(value) {
return (arguments.length !== 0 &&
(typeof value === "number" || (isObject(value) && value instanceof Number)));
}
function isNonEmptyStringArg(value) {
return arguments.length !== 0 && isStringArg(value) && value.length !== 0;
}
function isObject(obj) {
return obj !== null && typeof obj === "object";
}
//export function isDef(v: unknown): boolean {
// return typeof v !== "undefined" && v !== null;
//}
function isDef(value) {
return value !== null && value !== undefined;
}
function reErrorWithMessage(err, message) {
let out;
if (!isDef(err)) {
out = new Error(`${message}`);
}
else if (typeof err === "string") {
out = new Error(`${message} ${err}`);
}
else if (err instanceof Error) {
err.message = `${message} ${err.message}`;
out = err;
}
else {
out = new Error(`${message} ${stringify(err)}`);
}
return out;
}
function asStringDictionary(inobj) {
if (typeof inobj !== "object") {
throw new Error(`Expect obj to be object, but was ${stringify(inobj)}`);
}
const obj = inobj;
return obj;
}
function doStringGetterSetter(inobj, field, value) {
const hiddenField = `_${field}`;
const obj = asStringDictionary(inobj);
if (hasNoArgs(value) || value === null) {
// passing no args or null effectively unsets field
obj[hiddenField] = undefined;
}
else if (isStringArg(value)) {
obj[hiddenField] = value;
}
else {
throw new Error(`${field} value argument is optional or string, but was type ${typeof value}, '${value}' `);
}
return inobj;
}
function doBoolGetterSetter(inobj, field, value) {
const hiddenField = `_${field}`;
const obj = asStringDictionary(inobj);
if (hasNoArgs(value) || value === null) {
// passing no args or null effectively unsets field
obj[hiddenField] = undefined;
}
else if (value === true || value === false) {
obj[hiddenField] = value;
}
else {
throw new Error(`${field} value argument is optional or boolean, but was type ${typeof value}, '${value}' `);
}
return inobj;
}
function doIntGetterSetter(inobj, field, value) {
const hiddenField = `_${field}`;
const obj = asStringDictionary(inobj);
if (hasNoArgs(value) || value === null) {
// passing no args or null effectively unsets field
obj[hiddenField] = undefined;
}
else if (isNumArg(value)) {
obj[hiddenField] = value;
}
else if (isStringArg(value) && Number.isFinite(Number(value))) {
obj[hiddenField] = parseInt(value);
}
else {
throw new Error(`${field} value argument is optional or number, but was type ${typeof value}, '${value}' `);
}
return inobj;
}
function doFloatGetterSetter(inobj, field, value) {
const hiddenField = `_${field}`;
const obj = asStringDictionary(inobj);
if (hasNoArgs(value) || value === null) {
// passing no args or null effectively unsets field
obj[hiddenField] = undefined;
}
else if (isNumArg(value)) {
obj[hiddenField] = value;
}
else if (isStringArg(value) && Number.isFinite(Number(value))) {
obj[hiddenField] = parseFloat(value);
}
else {
throw new Error(`value argument is optional or number, but was type ${typeof value}, '${value}' `);
}
return obj;
}
function doMomentGetterSetter(inobj, field, value) {
const hiddenField = `_${field}`;
const obj = asStringDictionary(inobj);
if (hasNoArgs(value) || value === null) {
// passing no args or null effectively unsets field
obj[hiddenField] = undefined;
}
else if (isDef(value) && isObject(value) && luxon_1.DateTime.isDateTime(value)) {
obj[hiddenField] = value;
}
else if (isDef(value) && luxon_1.DateTime.isDateTime(checkStringOrDate(value))) {
obj[hiddenField] = checkStringOrDate(value);
}
else {
throw new Error(`${field} value argument is optional, DateTime, date or date string, but was type ${typeof value}, '${stringify(value)}' `);
}
return obj;
}
/**
* Converts entire DataView to a string as utf-8.
*
* @param dataView bytes to convert
* @returns the string
*/
function dataViewToString(dataView) {
let out = "";
for (let i = 0; i < dataView.byteLength; i++) {
out += String.fromCharCode(dataView.getUint8(i));
}
return out;
}
/**
* Log a message to the console. Put here to limit lint console errors
* for the times we really do want to use console.log. Will also append a
* p tag to a div#debug if it exists.
*
* @param msg the message to log
*/
function log(msg) {
// eslint-disable-next-line no-console
if (console) {
// eslint-disable-next-line no-console
console.log(`${stringify(msg)}`);
}
if (typeof document !== "undefined" && document !== null) {
const p = document.createElement("p");
p.textContent = `${stringify(msg)}`;
const divDebug = document.querySelector("div#debug");
if (isDef(divDebug)) {
divDebug.appendChild(p);
}
}
}
/**
* typescript-y check if Error.
*
* @param error object that might be an Error
* @returns true if Error object
*/
function isError(error) {
return typeof error === "object" && error !== null && error instanceof Error;
}
/**
* typescript-y convert errors.
*
* @param maybeError obejct that might be an Error object
* @returns an Error object
*/
function toError(maybeError) {
if (isError(maybeError))
return maybeError;
try {
return new Error(JSON.stringify(maybeError));
}
catch {
// fallback in case there's an error stringifying the maybeError
// like with circular references for example.
return new Error(String(maybeError));
}
}
/**
* Log a warning message to the console. Put here to limit lint console errors
* for the times we really do want to use console.log. Will also append a
* p tag to a div#debug if it exists.
*
* @param msg the message to log
*/
function warn(msg) {
// eslint-disable-next-line no-console
if (console) {
// eslint-disable-next-line no-console
console.assert(false, `${stringify(msg)}`);
}
if (typeof document !== "undefined" && document !== null) {
const p = document.createElement("p");
p.textContent = `${stringify(msg)}`;
document.querySelector("div#debug").appendChild(p);
}
}
/**
* String representation of input. This is kind of dumb but makes
* flow happier.
*
* @param value any kind of thing that can be turned into a string
* @returns a string
*/
function stringify(value) {
if (typeof value === "string") {
return value;
}
else if (typeof value === "number") {
return value.toString();
}
else if (typeof value === "boolean") {
return value ? "true" : "false";
}
else if (typeof value === "undefined") {
return "undefined";
}
else if (typeof value === "function") {
return "function " + value.name;
}
else if (typeof value === "object") {
if (value) {
if (luxon_1.DateTime.isDateTime(value)) {
const dateTimeValue = value;
// typescript null check
const s = dateTimeValue.toISO();
return dateTimeValue.isValid && s
? s
: `Invalid DateTime: ${dateTimeValue.invalidReason}: ${dateTimeValue.invalidExplanation}`;
}
else {
return `${value?.constructor?.name} ${String(value)}`;
}
//} else if (typeof value === 'symbol') {
// return value.toString();
}
else {
return `${value}`;
}
}
else {
return "<unknown" + typeof value + "???>";
}
}
function isoToDateTime(val) {
if (val.toLowerCase() === "now") {
return luxon_1.DateTime.utc();
}
return luxon_1.DateTime.fromISO(val, exports.UTC_OPTIONS);
}
/**
* Create a luxon Interval from a start and end.
*
* @param start start of the interval as iso string or DateTime
* @param end end of the interval as string or DateTime
* @returns the interval
*/
function startEnd(start, end) {
if (isStringArg(start)) {
start = isoToDateTime(start);
}
if (isStringArg(end)) {
end = isoToDateTime(end);
}
return luxon_1.Interval.fromDateTimes(start, end);
}
/**
* Create a luxon Interval from a start and a duration. If the duration is negative, the
* start time will become the end time. This differs from luxon Interval.after which
* will return an invalid Interval instead.
*
* @param start start of the interval as iso string or DateTime
* @param duration duration of the interval as iso string, number of seconds, or Duration
* @returns the interval
*/
function startDuration(start, duration) {
if (isStringArg(start)) {
start = isoToDateTime(start);
}
if (isStringArg(duration)) {
duration = luxon_1.Duration.fromISO(duration);
}
else if (isNumArg(duration)) {
duration = luxon_1.Duration.fromMillis(1000 * duration);
}
if (duration.valueOf() < 0) {
return luxon_1.Interval.before(start, duration.negate());
}
else {
return luxon_1.Interval.after(start, duration);
}
}
/**
* Create a luxon Interval from a duration and a end. If the duration is negative, the
* end time will become the start time. This differs from luxon Interval.before which
* will return an invalid Interval instead.
*
* @param duration duration of the interval as iso string, number of seconds, or Duration
* @param end end of the interval as string or DateTime
* @returns the interval
*/
function durationEnd(duration, end) {
if (isStringArg(end)) {
end = isoToDateTime(end);
}
if (isStringArg(duration)) {
duration = luxon_1.Duration.fromISO(duration);
}
else if (isNumArg(duration)) {
duration = luxon_1.Duration.fromMillis(1000 * duration);
}
if (duration.valueOf() < 0) {
return luxon_1.Interval.after(end, duration.negate());
}
else {
return luxon_1.Interval.before(end, duration);
}
}
/**
* Calculates offset of remote server versus local time. It is assumed that the
* argument was acquired as close in time to calling this as possible.
*
* @param serverTimeUTC now as reported by remote server
* @returns offset in seconds to now on local machine
*/
function calcClockOffset(serverTimeUTC) {
return luxon_1.DateTime.utc().diff(serverTimeUTC).toMillis() * 1000.0;
}
exports.WAY_FUTURE = luxon_1.DateTime.fromISO("2500-01-01T00:00:00Z");
/**
* converts the input value is a DateTime, throws Error if not
* a string, Date or DateTime. Zero length string or "now" return
* current time.
*
* @param d 'now', string time, Date, number of milliseconds since epoch, or DateTime
* @returns DateTime created from argument
*/
function checkStringOrDate(d) {
if (luxon_1.DateTime.isDateTime(d)) {
return d;
}
else if (d instanceof Date) {
return luxon_1.DateTime.fromJSDate(d, exports.UTC_OPTIONS);
}
else if (isNumArg(d)) {
return luxon_1.DateTime.fromMillis(d, exports.UTC_OPTIONS);
}
else if (isNonEmptyStringArg(d)) {
const lc = d.toLowerCase();
if (d.length === 0 || lc === "now") {
return luxon_1.DateTime.utc();
}
else {
return isoToDateTime(d);
}
}
throw new Error(`unknown date type: ${stringify(d)} ${typeof d}`);
}
/**
* Converts name and value into a html query parameter, with appending ampersand.
*
* @param name parameter name
* @param val parameter value
* @returns formated query parameter
*/
function makeParam(name, val) {
return `${name}=${encodeURIComponent(stringify(val))}&`;
}
/**
* Converts name and value into a parameter line, with appending newline,
* for including in POST body.
*
* @param name parameter name
* @param val parameter value
* @returns formated query parameter
*/
function makePostParam(name, val) {
return name + "=" + stringify(val) + "\n";
}
/**
* converts to ISO8601 but removes the trailing Z as FDSN web services
* do not allow that.
*
* @param date DateTime to convert to string
* @returns ISO8601 without timezone Z
*/
function toIsoWoZ(date) {
if (date.isValid) {
let out = date.toISO();
if (out == null) {
throw new Error(`Bad date: ${stringify(date)}`);
}
if (out.endsWith("Z")) {
out = out.substring(0, out.length - 1);
}
return out;
}
else {
throw new Error(`${date.invalidReason}: ${date.invalidExplanation}`);
}
}
/**
* Extracts a valid starting DateTime from interval.
* Throws Error if interval is not valid.
* @param interval luxon Interval
* @returns start DateTime
*/
function validStartTime(interval) {
const d = interval.start;
if (d == null) {
throw new Error(`Bad interval: ${stringify(interval)}`);
}
return d;
}
/**
* Extracts a valid ending DateTime from interval.
* Throws Error if interval is not valid.
* @param interval luxon Interval
* @returns end DateTime
*/
function validEndTime(interval) {
const d = interval.end;
if (d == null) {
throw new Error(`Bad interval: ${stringify(interval)}`);
}
return d;
}
/**
* Converts a luxon DateTime to a Javascript Date, checking for null,
* undefined and isValid first. Throws Error in that case.
*
* @param d luxon DateTime
* @returns Javascript Date
*/
function toJSDate(d) {
if (!d) {
throw new Error(`Null/undef DateTime: ${d}`);
}
if (!d.isValid) {
throw new Error(`${d.invalidReason}: ${d.invalidExplanation}`);
}
return d.toJSDate();
}
/**
* Check a Luxon DateTime, Interval or Duration for valid.
* Throws Error if not. THis is to avoid globally setting
* luxon's Settings.throwOnInvalid = true;
* but still throw/catch on invalid dates.
* @param d luxon object
* @param msg optional message to add to error
* @returns passed in object if valid
*/
function checkLuxonValid(d, msg) {
if (d == null) {
const m = msg ? msg : "";
throw new Error(`Null luxon value: ${d} ${m}`);
}
if (!d.isValid) {
const m = msg ? msg : "";
throw new Error(`Invalid Luxon: ${typeof d} ${d?.constructor?.name} ${d.invalidReason}: ${d.invalidExplanation} ${m}`);
}
return d;
}
/**
* @returns the protocol, http: or https: for the document if possible.
* Note this includes the colon.
*/
function checkProtocol() {
let _protocol = "http:";
if (typeof document !== "undefined" &&
document !== null &&
"location" in document &&
"protocol" in document.location &&
"https:" === document.location.protocol) {
_protocol = "https:";
}
return _protocol;
}
/**
* Create default fetch init object with the given mimeType. Sets
* no-cache, follow redirects, cors mode, referrer as seisplotjs and
* mimetype as a header. Note that redirect with POST may fail due to
* POST being changed to GET on a 301. Fetching with POST may wish
* to use redirect: "manual" to handle the 301 correctly by POSTing to
* the new URL.
*
*
* @param mimeType requested mime type
* @returns object with fetch configuration parameters
*/
function defaultFetchInitObj(mimeType) {
const headers = {};
if (isStringArg(mimeType)) {
headers.Accept = mimeType;
}
return {
cache: "no-cache",
redirect: "follow",
mode: "cors",
referrer: "seisplotjs",
headers: headers,
};
}
function cloneFetchInitObj(fetchInit) {
const out = {};
if (fetchInit) {
for (const [key, value] of Object.entries(fetchInit)) {
if (Array.isArray(value)) {
// @ts-expect-error typescript can't do reflection, but ok for clone
out[key] = value.slice();
}
else {
// @ts-expect-error typescript can't do reflection, but ok for clone
out[key] = value;
}
}
}
return out;
}
function errorFetch(_url, _init) {
throw new Error("There is no fetch!?!?!");
}
exports.default_fetch = null;
function setDefaultFetch(fetcher) {
if (fetcher != null) {
exports.default_fetch = fetcher;
}
}
function getFetch() {
if (exports.default_fetch != null) {
return exports.default_fetch;
}
else if (typeof window !== 'undefined' && window != null) {
return window.fetch;
}
else if (global != null) {
return global.fetch;
}
else {
return errorFetch;
}
}
/**
* Does a fetch, but times out if it takes too long.
*
* @param url url to retrieve
* @param fetchInit fetch configuration, initialization
* @param timeoutSec maximum time to wait in seconds
* @param fetcher optional fetch to use instead of global fetch
* @returns promise to the result
* @throws Error if time out or other failure
*/
function doFetchWithTimeout(url, fetchInit, timeoutSec, fetcher) {
const controller = new AbortController();
const signal = controller.signal;
if (!fetcher) {
fetcher = getFetch();
}
if (!fetcher) {
fetcher = window.fetch;
}
let internalFetchInit = isDef(fetchInit) ? fetchInit : defaultFetchInitObj();
internalFetchInit = cloneFetchInitObj(internalFetchInit);
if (internalFetchInit.redirect === "follow" &&
internalFetchInit.method === "POST") {
// follow on POST is dangerous if the server returns 301, handle it ourselves
// note this is assuming that the redirect is a simple http -> https.
internalFetchInit.redirect = "manual";
}
if (!isDef(timeoutSec)) {
timeoutSec = 30;
}
setTimeout(() => controller.abort(), timeoutSec * 1000);
internalFetchInit.signal = signal;
let absoluteUrl;
if (url instanceof URL) {
absoluteUrl = url;
}
else if (isStringArg(url)) {
if (url.startsWith("http://") || url.startsWith("https://")) {
absoluteUrl = new URL(url);
}
else {
absoluteUrl = new URL(url, document.URL);
}
}
else {
throw new Error(`url must be string or URL, ${stringify(url)}`);
}
log(`attempt to fetch ${internalFetchInit.method ? internalFetchInit.method : ""} ${stringify(absoluteUrl)}`);
// save fetcher as const so typescript won't think it has become undef
const fetchForRedirect = fetcher;
return fetcher(absoluteUrl.href, internalFetchInit)
.catch((err) => {
log("fetch failed, possible CORS or PrivacyBadger or NoScript?");
throw err;
})
.then(function (response) {
if (response.ok || response.status === 404) {
return response;
}
else if (response.status >= 300 && response.status <= 399) {
if (checkProtocol() === "http:" &&
absoluteUrl.href.startsWith("http://")) {
// maybe try https just in case
const httpsUrl = new URL(`https://${absoluteUrl.href.slice(7)}`);
const method = internalFetchInit.method
? internalFetchInit.method
: "";
log(`attempt fetch redirect ${response.status} ${method} to ${stringify(httpsUrl)}`);
return fetchForRedirect(httpsUrl.href, internalFetchInit).then((httpsResponse) => {
if (httpsResponse.ok || httpsResponse.status === 404) {
return httpsResponse;
}
else {
return response.text().then((text) => {
throw new Error(`fetch response was redirect for http and failed for https. ${response.ok} ${response.status}, ${httpsResponse.ok} ${httpsResponse.status} \n${text}`);
});
}
});
}
}
return response.text().then((text) => {
throw new Error(`fetch response was not ok. ${response.ok} ${response.status}\n${text}`);
});
});
}
/**
* Allows downloading of in memory data, as ArrayBuffer, to file as if
* the user clicked a download link.
*
* @param data ArrayBuffer to download
* @param filename default filename
* @param mimeType mimeType, default application/octet-stream
*/
function downloadBlobAsFile(data, filename, mimeType = "application/octet-stream") {
if (!data) {
throw new Error("data is empty");
}
if (!filename)
filename = "filetodownload.txt";
const blob = new Blob([data], { type: mimeType });
const e = document.createEvent("MouseEvents");
const a = document.createElement("a");
a.download = filename;
a.href = window.URL.createObjectURL(blob);
a.dataset.downloadurl = [mimeType, a.download, a.href].join(":");
e.initMouseEvent("click", true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
a.dispatchEvent(e);
}
/**
* Recursively calculates the mean of a slice of an array. This helps with
* very long seismograms to equally weight each sample point without overflowing.
*
* @param dataSlice slice of a seismogram
* @param totalPts number of points in the original seismogram
* @returns sum of slice data points divided by totalPts
*/
function meanOfSlice(dataSlice, totalPts) {
if (dataSlice.length < 8) {
return (
// @ts-expect-error different array types confuses typescript
dataSlice.reduce(function (acc, val) {
return acc + val;
}, 0) / totalPts);
}
else {
const byTwo = Math.floor(dataSlice.length / 2);
return (meanOfSlice(dataSlice.slice(0, byTwo), totalPts) +
meanOfSlice(dataSlice.slice(byTwo, dataSlice.length), totalPts));
}
}
exports.SVG_NS = "http://www.w3.org/2000/svg";
exports.XHTML_NS = "http://www.w3.org/1999/xhtml";
function createSVGElement(name) {
return document.createElementNS(exports.SVG_NS, name);
}
function updateVersionText(selector = "#sp-version") {
document.querySelectorAll(selector).forEach((el) => {
el.textContent = version_1.version;
});
}