import { Quake, createQuakeClickEvent, QuakeClickEventMap } from "./quakeml";
import {
Channel,
Station,
createStationClickEvent,
createChannelClickEvent,
StationClickEventMap, ChannelClickEventMap
} from "./stationxml";
import { SeisPlotElement, addStyleToElement } from "./spelement";
import { SeismogramDisplayData } from "./seismogram";
import { SeismographConfig } from "./seismographconfig";
import { stringify, nameForTimeZone } from "./util";
import * as textformat from "./textformat";
import { Handlebars } from "./handlebarshelpers";
import {DateTime, Zone} from "luxon";
import {csvFormatRows} from "d3-dsv";
export const INFO_ELEMENT = "sp-station-quake-table";
export const QUAKE_INFO_ELEMENT = "sp-quake-table";
export enum QUAKE_COLUMN {
LAT = "Lat",
LON = "Lon",
TIME = "Time",
LOCALTIME = "Local Time",
MAG = "Mag",
MAGTYPE = "MagType",
DEPTH = "Depth",
DESC = "Description",
EVENTID = "EventId",
}
export enum CHANNEL_COLUMN {
LAT = "Lat",
LON = "Lon",
AZIMUTH = "Az",
DIP = "Dip",
START = "Start",
END = "End",
ELEVATION = "Elev",
DEPTH = "Depth",
SOURCEID = "SourceId",
CODE = "Code",
NETWORK_CODE = "NetworkCode",
STATION_CODE = "StationCode",
LOCATION_CODE = "LocationCode",
CHANNEL_CODE = "ChannelCode",
}
export enum STATION_COLUMN {
LAT = "Lat",
LON = "Lon",
START = "Start",
END = "End",
ELEVATION = "Elev",
SOURCEID = "SourceId",
CODE = "Code",
NETWORK_CODE = "NetworkCode",
STATION_CODE = "StationCode",
DESCRIPTION = "Description",
}
export enum SEISMOGRAM_COLUMN {
START = "Start",
DURATION = "Duration",
END = "End",
NUM_POINTS = "Num Pts",
SAMPLE_RATE = "Sample Rate",
SAMPLE_PERIOD = "Sample Period",
SEGMENTS = "Segments",
SOURCEID = "SourceId",
CODE = "Codes",
NETWORK_CODE = "NetworkCode",
STATION_CODE = "StationCode",
}
export const DEFAULT_TEMPLATE = `
| Waveform |
Channel |
Event |
DistAz |
| Codes |
Start |
Duration |
End |
Num Pts |
Sample Rate |
YUnit |
Seg |
Lat |
Lon |
Elev |
Depth |
Time |
Lat |
Lon |
Mag |
Depth |
Dist deg |
Dist km |
Azimuth |
Back Azimuth |
{{#each seisDataList as |sdd|}}
| {{sdd.nslc}} |
{{formatIsoDate sdd.seismogram.startTime}} |
{{formatDuration sdd.seismogram.timeRange}} |
{{formatIsoDate sdd.seismogram.endTime}} |
{{sdd.seismogram.numPoints}} |
{{sdd.seismogram.sampleRate}} |
{{sdd.seismogram.yUnit}} |
{{sdd.seismogram.segments.length}} |
{{#if sdd.channel}}
{{sdd.channel.latitude}} |
{{sdd.channel.longitude}} |
{{sdd.channel.elevation}} |
{{sdd.channel.depth}} |
{{else}}
no channel |
|
|
|
{{/if}}
{{#if sdd.quake }}
{{formatIsoDate sdd.quake.time}} |
{{sdd.quake.latitude}} |
{{sdd.quake.longitude}} |
{{sdd.quake.magnitude.mag}} |
{{sdd.quake.magnitude.type}} |
{{sdd.quake.depthKm}} |
{{else}}
no quake |
|
|
|
|
|
{{/if}}
{{#if sdd.quake }}
{{#if sdd.channel }}
{{formatNumber sdd.distaz.distanceDeg 2}} |
{{formatNumber sdd.distaz.distanceKm 0}} |
{{formatNumber sdd.distaz.az 2}} |
{{formatNumber sdd.distaz.baz 2}} |
{{/if}}
{{/if}}
{{/each}}
`;
export const TABLE_CSS = `
tbody tr:nth-child(even)
{
background: var(--even-row-background, Cornsilk);
}
tbody tr:nth-child(odd)
{
background: var(--odd-row-background);
}
table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
caption {
caption-side: bottom;
}
`;
/**
* Table displaying information about waveforms, quakes, channels and stations.
*
* The CSS vars --even-row-background and --odd-row-background will change
* the color of even and odd rows. Default for odd is nothing, even is Cornsilk.
*
* @param seisData Array of SeismogramDisplayData for display
* @param seisConfig configuration
*/
export class QuakeStationTable extends SeisPlotElement {
_template: string;
constructor(
seisData?: Array,
seisConfig?: SeismographConfig,
) {
super(seisData, seisConfig);
this._template = DEFAULT_TEMPLATE;
this.addStyle(TABLE_CSS);
const wrapper = document.createElement("div");
wrapper.setAttribute("class", "wrapper");
this.getShadowRoot().appendChild(wrapper);
}
get template(): string {
return this._template;
}
set template(t: string) {
this._template = t;
this.redraw();
}
draw() {
if (!this.isConnected) {
return;
}
const wrapper = this.getShadowRoot().querySelector("div") as HTMLDivElement;
while (wrapper.firstChild) {
// typescript
if (wrapper.lastChild) {
wrapper.removeChild(wrapper.lastChild);
}
}
const handlebarsCompiled = Handlebars.compile(this.template);
wrapper.innerHTML = handlebarsCompiled(
{
seisDataList: this.seisData,
seisConfig: this.seismographConfig,
},
{
allowProtoPropertiesByDefault: true, // this might be a security issue???
},
);
}
}
customElements.define(INFO_ELEMENT, QuakeStationTable);
export interface QuakeTable extends HTMLElement {
// overload for custom events
addEventListener(type: E, listener: (ev: QuakeClickEventMap[E]) => any): void;
}
export class QuakeTable extends HTMLElement {
_columnLabels: Map;
_quakeList: Array;
_rowToQuake: Map;
_timezone?: Zone;
_timeFormat?: any;
lastSortAsc = true;
lastSortCol: string | undefined;
_columnValues: Map string|HTMLElement>;
_caption?: string|HTMLElement;
constructor(
quakeList?: Array,
columnLabels?: Map,
columnValues?: Map string>,
) {
super();
if (!quakeList) {
quakeList = [];
}
if (!columnLabels) {
columnLabels = QuakeTable.createDefaultColumnLabels();
}
// Column Values are optional at the individual key level.
// For the columns that the user does not provide a function,
// use the default display style in getQuakeValue
if (!columnValues) {
columnValues = new Map string>();
const defColumnValues = QuakeTable.createDefaultColumnValues();
for (const key of columnLabels.keys()) {
if (defColumnValues.has(key)) {
const fn = defColumnValues.get(key);
if (fn != null) {
columnValues.set(key, fn);
} else {
throw new Error(`QuakeTable function for key is missing: ${key}`);
}
} else {
throw new Error(`Unknown QuakeTable key: ${key}`);
}
}
}
this._quakeList = quakeList;
this._columnLabels = columnLabels;
this._columnValues = columnValues;
this._rowToQuake = new Map();
const shadow = this.attachShadow({ mode: "open" });
const table = document.createElement("table");
table.setAttribute("class", "wrapper");
addStyleToElement(this, TABLE_CSS);
shadow.appendChild(table);
}
get quakeList(): Array {
return this._quakeList;
}
set quakeList(ql: Array) {
this._quakeList = ql;
this.draw();
}
get columnLabels(): Map {
return this._columnLabels;
}
set columnLabels(cols: Map) {
this._columnLabels = cols;
this.draw();
}
get columnValues(): Map string|HTMLElement> {
return this._columnValues;
}
set columnValues(cols: Map string|HTMLElement>) {
this._columnValues = cols;
this.draw();
}
get timeZone(): Zone|undefined {
return this._timezone;
}
set timeZone(timezone: Zone|undefined) {
this._timezone = timezone;
this.draw();
}
get caption(): string|HTMLElement|undefined {
return this._caption;
}
set caption(cap: string|HTMLElement|undefined) {
this._caption = cap;
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (table && this._caption) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
}
addColumn(key: string, label: string, valueFn: (q: Quake) => string|HTMLElement) {
this.columnLabels.set(key, label);
this.columnValues.set(key, valueFn);
}
/**
* Time format, passed to luxon toLocaleString()
* https://moment.github.io/luxon/api-docs/index.html#datetimetolocalestring
* @return time as a string
*/
get timeFormat(): any|undefined {
return this._timeFormat;
}
set timeFormat(timeFormat: any|undefined) {
this._timeFormat = timeFormat;
this.draw();
}
static createDefaultColumnLabels() {
const columnLabels = new Map();
columnLabels.set(QUAKE_COLUMN.TIME, "Time");
columnLabels.set(QUAKE_COLUMN.LAT, "Lat");
columnLabels.set(QUAKE_COLUMN.LON, "Lon");
columnLabels.set(QUAKE_COLUMN.MAG, "Mag");
columnLabels.set(QUAKE_COLUMN.MAGTYPE, "Type");
columnLabels.set(QUAKE_COLUMN.DEPTH, "Depth");
columnLabels.set(QUAKE_COLUMN.DESC, "Description");
return columnLabels;
}
static createDefaultColumnValues() {
const columnValues = new Map string>();
columnValues.set(QUAKE_COLUMN.TIME, (q: Quake) => stringify(q.time.toISO()));
columnValues.set(QUAKE_COLUMN.LOCALTIME, (q: Quake) => stringify(q.time.setZone('local').toISO()));
columnValues.set(QUAKE_COLUMN.LAT, (q: Quake) => latlonFormat.format(q.latitude));
columnValues.set(QUAKE_COLUMN.LON, (q: Quake) => latlonFormat.format(q.longitude));
columnValues.set(QUAKE_COLUMN.MAG, (q: Quake) => magFormat.format(q.magnitude.mag));
columnValues.set(QUAKE_COLUMN.MAGTYPE, (q: Quake) => q.magnitude.type ? q.magnitude.type : "");
columnValues.set(QUAKE_COLUMN.DEPTH, (q: Quake) => depthFormat.format(q.depthKm));
columnValues.set(QUAKE_COLUMN.EVENTID, (q: Quake) => stringify(q.eventId));
columnValues.set(QUAKE_COLUMN.DESC,
(q: Quake) => { const desc = q.description;
if (desc && desc.length > 0) {
return desc;
} else {
return stringify(q.time.toISO());
}
});
return columnValues;
}
addStyle(css: string, id?: string): HTMLStyleElement {
return addStyleToElement(this, css, id);
}
findRowForQuake(q: Quake): HTMLTableRowElement | null {
let quakeRow = null;
this._rowToQuake.forEach((v, k) => {
if (v === q) {
quakeRow = k;
}
});
return quakeRow;
}
draw() {
if (!this.isConnected) {
return;
}
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (this._caption ) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
table.deleteTHead();
const theader = table.createTHead().insertRow();
this.headers().forEach((h) => {
const cell = theader.appendChild(document.createElement("th"));
const label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
cell.textContent = `${label}`;
if (h === QUAKE_COLUMN.LOCALTIME) {
if (this.timeZone) {
cell.textContent = `${this._columnLabels.get(h)} ${nameForTimeZone(this.timeZone, DateTime.now())}`;
} else {
cell.textContent = `${this._columnLabels.get(h)} ${nameForTimeZone('local', DateTime.now())}`;
}
}
cell.addEventListener("click", () => {
this.sort(h, cell);
});
});
table.querySelectorAll("tbody")?.forEach((tb: Node) => {
table.removeChild(tb);
});
const tbody = table.createTBody();
this.quakeList.forEach((q) => {
const row = tbody.insertRow();
this.populateRow(q, row, -1);
row.addEventListener("click", (evt) => {
this.dispatchEvent(createQuakeClickEvent(q, evt));
});
});
}
headers(): Array {
return Array.from(this._columnValues.keys());
}
populateRow(q: Quake, row: HTMLTableRowElement, index: number) {
this._rowToQuake.set(row, q);
this.headers().forEach((h) => {
const cell = row.insertCell(index);
if (h === QUAKE_COLUMN.LOCALTIME && this.timeZone) {
// special case if set timezone
const localQuakeTime = q.time.setZone(this.timeZone);
if (this.timeFormat) {
cell.textContent = localQuakeTime.toLocaleString(this.timeFormat);
} else {
cell.textContent = localQuakeTime.toISO();
}
} else {
const cellValue = this.getQuakeValue(q, h);
if (cellValue instanceof HTMLElement) {
cell.appendChild(cellValue);
} else {
cell.textContent = cellValue;
}
}
if (index !== -1) {
index++;
}
});
}
tableToCSV() {
const out: Array> = [];
const headRow:Array = [];
out.push(headRow);
this.headers().forEach((h) => {
let label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
label = label ? label : "";
headRow.push(label);
});
this._quakeList.forEach((q) => {
const row: Array = [];
out.push(row);
this.headers().forEach((h) => {
const cellValue = this.getQuakeValue(q, h);
if (cellValue instanceof HTMLElement) {
row.push(cellValue.textContent);
} else {
row.push(cellValue);
}
});
});
return csvFormatRows(out);
}
getQuakeValue(q: Quake, h: string): string|HTMLElement {
const fn = this._columnValues.has(h) ? this._columnValues.get(h) : null;
if (fn != null) {
return fn(q);
} else {
return `unknown: ${String(h)}`;
}
}
sort(h: string, _headerCell: HTMLTableCellElement) {
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
const tbody = table.querySelector("tbody");
if (tbody) {
const rows = Array.from(tbody.querySelectorAll("tr"));
rows.sort((rowa, rowb) => {
let out = 0;
const qa = this._rowToQuake.get(rowa);
const qb = this._rowToQuake.get(rowb);
if (qa && qb) {
if (h === QUAKE_COLUMN.TIME || h === QUAKE_COLUMN.LOCALTIME) {
out = qa.time.toMillis() - qb.time.toMillis();
} else if (h === QUAKE_COLUMN.LAT) {
out = qa.latitude - qb.latitude;
} else if (h === QUAKE_COLUMN.LON) {
out = qa.longitude - qb.longitude;
} else if (h === QUAKE_COLUMN.MAG) {
out = qa.magnitude.mag - qb.magnitude.mag;
} else if (h === QUAKE_COLUMN.DEPTH) {
out = qa.depthKm - qb.depthKm;
} else {
// just use string
const ta = this.getQuakeValue(qa, h);
const tb = this.getQuakeValue(qb, h);
if (ta < tb) {
out = -1;
} else if (ta > tb) {
out = 1;
} else {
out = 0;
}
}
} else {
// cant find one of the quakes, oh well
}
return out;
});
if (this.lastSortCol === h) {
if (this.lastSortAsc) {
rows.reverse();
}
this.lastSortAsc = !this.lastSortAsc;
} else {
this.lastSortAsc = true;
}
// this effectively remove and then appends the rows in new order
rows.forEach((v) => {
tbody.appendChild(v);
});
this.lastSortCol = h;
} else {
// no tbody for table sort???
}
}
}
customElements.define(QUAKE_INFO_ELEMENT, QuakeTable);
export interface ChannelTable extends HTMLElement {
// overload for custom events
addEventListener(type: E, listener: (ev: ChannelClickEventMap[E]) => any): void;
}
export class ChannelTable extends HTMLElement {
_columnLabels: Map;
_columnValues: Map string|HTMLElement>;
_channelList: Array;
_rowToChannel: Map;
lastSortAsc = true;
lastSortCol: string | undefined;
_caption?: string|HTMLElement;
constructor(
channelList?: Array,
columnLabels?: Map,
columnValues?: Map string|HTMLElement>
) {
super();
if (!channelList) {
channelList = [];
}
if (!columnLabels) {
columnLabels = new Map();
columnLabels.set(CHANNEL_COLUMN.CODE, "Code");
columnLabels.set(CHANNEL_COLUMN.START, "Start");
columnLabels.set(CHANNEL_COLUMN.END, "End");
columnLabels.set(CHANNEL_COLUMN.LAT, "Lat");
columnLabels.set(CHANNEL_COLUMN.LON, "Lon");
columnLabels.set(CHANNEL_COLUMN.AZIMUTH, "Az");
columnLabels.set(CHANNEL_COLUMN.DIP, "Dip");
columnLabels.set(CHANNEL_COLUMN.DEPTH, "Depth");
columnLabels.set(CHANNEL_COLUMN.ELEVATION, "Evel");
columnLabels.set(CHANNEL_COLUMN.SOURCEID, "SourceId");
}
this._channelList = channelList;
this._columnLabels = columnLabels;
this._rowToChannel = new Map();
if (!columnValues) {
columnValues = new Map string|HTMLElement>();
const defColumnValues = ChannelTable.createDefaultColumnValues();
for (const key of columnLabels.keys()) {
if (defColumnValues.has(key)) {
const fn = defColumnValues.get(key);
if (fn != null) {
columnValues.set(key, fn);
} else {
throw new Error(`ChannelTable function for key is missing: ${key}`);
}
} else {
throw new Error(`Unknown ChannelTable key: ${key}`);
}
}
}
this._columnValues = columnValues;
const shadow = this.attachShadow({ mode: "open" });
const table = document.createElement("table");
table.setAttribute("class", "wrapper");
addStyleToElement(this, TABLE_CSS);
shadow.appendChild(table);
}
get channelList(): Array {
return this._channelList;
}
set channelList(ql: Array) {
this._channelList = ql;
this.draw();
}
get columnLabels(): Map {
return this._columnLabels;
}
set columnLabels(cols: Map) {
this._columnLabels = cols;
this.draw();
}
get columnValues(): Map string|HTMLElement> {
return this._columnValues;
}
set columnValues(cols: Map string|HTMLElement>) {
this._columnValues = cols;
this.draw();
}
addColumn(key: string, label: string, valueFn: (c: Channel) => string|HTMLElement) {
this.columnLabels.set(key, label);
this.columnValues.set(key, valueFn);
}
get caption(): string|HTMLElement|undefined {
return this._caption;
}
set caption(cap: string|HTMLElement|undefined) {
this._caption = cap;
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (table && this._caption ) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
}
addStyle(css: string, id?: string): HTMLStyleElement {
return addStyleToElement(this, css, id);
}
draw() {
if (!this.isConnected) {
return;
}
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (this._caption ) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
table.deleteTHead();
const theader = table.createTHead().insertRow();
this.headers().forEach((h) => {
const cell = theader.appendChild(document.createElement("th"));
const label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
cell.textContent = `${label}`;
cell.addEventListener("click", () => {
this.sort(h, cell);
});
});
table.querySelectorAll("tbody")?.forEach((tb: Node) => {
table.removeChild(tb);
});
const tbody = table.createTBody();
this.channelList.forEach((c) => {
const row = tbody.insertRow();
this.populateRow(c, row, -1);
row.addEventListener("click", (evt) => {
this.dispatchEvent(createChannelClickEvent(c, evt));
});
});
}
headers(): Array {
return Array.from(this._columnLabels.keys());
}
populateRow(q: Channel, row: HTMLTableRowElement, index: number) {
this._rowToChannel.set(row, q);
this.headers().forEach((h) => {
const cell = row.insertCell(index);
const cellValue = this.getChannelValue(q, h);
if (cellValue instanceof HTMLElement) {
cell.appendChild(cellValue);
} else {
cell.textContent = cellValue;
}
if (index !== -1) {
index++;
}
});
}
tableToCSV() {
const out: Array> = [];
const headRow:Array = [];
out.push(headRow);
this.headers().forEach((h) => {
let label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
label = label ? label : "";
headRow.push(label);
});
this._channelList.forEach((q) => {
const row: Array = [];
out.push(row);
this.headers().forEach((h) => {
const cellValue = this.getChannelValue(q, h);
if (cellValue instanceof HTMLElement) {
row.push(cellValue.textContent);
} else {
row.push(cellValue);
}
});
});
return csvFormatRows(out);
}
getChannelValue(c: Channel, h: string): string|HTMLElement {
const fn = this._columnValues.has(h) ? this._columnValues.get(h) : null;
if (fn != null) {
return fn(c);
} else {
return `unknown: ${String(h)}`;
}
}
static createDefaultColumnValues() {
const columnValues = new Map string|HTMLElement>();
columnValues.set(CHANNEL_COLUMN.START, (c: Channel) => stringify(c.startDate.toISO()));
columnValues.set(CHANNEL_COLUMN.END, (c: Channel) => c.endDate ? stringify(c.endDate.toISO()) : "");
columnValues.set(CHANNEL_COLUMN.LAT, (c: Channel) => latlonFormat.format(c.latitude));
columnValues.set(CHANNEL_COLUMN.LON, (c: Channel) => latlonFormat.format(c.longitude));
columnValues.set(CHANNEL_COLUMN.ELEVATION, (c: Channel) => depthMeterFormat.format(c.elevation));
columnValues.set(CHANNEL_COLUMN.DEPTH, (c: Channel) => depthFormat.format(c.depth));
columnValues.set(CHANNEL_COLUMN.AZIMUTH, (c: Channel) => latlonFormat.format(c.azimuth));
columnValues.set(CHANNEL_COLUMN.DIP, (c: Channel) => latlonFormat.format(c.dip));
columnValues.set(CHANNEL_COLUMN.SOURCEID, (c: Channel) => stringify(c.sourceId.toString()));
columnValues.set(CHANNEL_COLUMN.CODE, (c: Channel) => stringify(c.codes()));
columnValues.set(CHANNEL_COLUMN.NETWORK_CODE, (c: Channel) => c.networkCode);
columnValues.set(CHANNEL_COLUMN.STATION_CODE, (c: Channel) => c.stationCode);
columnValues.set(CHANNEL_COLUMN.LOCATION_CODE, (c: Channel) => c.locationCode);
columnValues.set(CHANNEL_COLUMN.CHANNEL_CODE, (c: Channel) => c.channelCode);
columnValues.set(CHANNEL_COLUMN.CODE, (c: Channel) => stringify(c.codes()));
return columnValues;
}
static getChannelValue(q: Channel, h: CHANNEL_COLUMN): string {
if (h === CHANNEL_COLUMN.START) {
return stringify(q.startDate.toISO());
} else if (h === CHANNEL_COLUMN.END) {
return q.endDate ? stringify(q.endDate.toISO()) : "";
} else if (h === CHANNEL_COLUMN.LAT) {
return latlonFormat.format(q.latitude);
} else if (h === CHANNEL_COLUMN.LON) {
return latlonFormat.format(q.longitude);
} else if (h === CHANNEL_COLUMN.ELEVATION) {
return depthMeterFormat.format(q.elevation);
} else if (h === CHANNEL_COLUMN.DEPTH) {
return depthMeterFormat.format(q.depth);
} else if (h === CHANNEL_COLUMN.AZIMUTH) {
return latlonFormat.format(q.azimuth);
} else if (h === CHANNEL_COLUMN.DIP) {
return latlonFormat.format(q.dip);
} else if (h === CHANNEL_COLUMN.SOURCEID) {
return `${q.sourceId.toString()}`;
} else if (h === CHANNEL_COLUMN.CODE) {
return `${q.codes()}`;
} else if (h === CHANNEL_COLUMN.NETWORK_CODE) {
return `${q.networkCode}`;
} else if (h === CHANNEL_COLUMN.STATION_CODE) {
return `${q.stationCode}`;
} else if (h === CHANNEL_COLUMN.LOCATION_CODE) {
return `${q.locationCode}`;
} else if (h === CHANNEL_COLUMN.CHANNEL_CODE) {
return `${q.channelCode}`;
} else {
return `unknown: ${String(h)}`;
}
}
sort(h: string, _headerCell: HTMLTableCellElement) {
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
const tbody = table.querySelector("tbody");
if (tbody) {
const rows = Array.from(tbody.querySelectorAll("tr"));
rows.sort((rowa, rowb) => {
let out = 0;
const qa = this._rowToChannel.get(rowa);
const qb = this._rowToChannel.get(rowb);
if (qa && qb) {
if (h === CHANNEL_COLUMN.START) {
out = qa.startDate.toMillis() - qb.startDate.toMillis();
} else if (h === CHANNEL_COLUMN.END) {
if (qa.endDate && qb.endDate) {
out = qa.endDate.toMillis() - qb.endDate.toMillis();
} else if (qb.endDate) {
return 1;
} else {
return -1;
}
} else if (h === CHANNEL_COLUMN.LAT) {
out = qa.latitude - qb.latitude;
} else if (h === CHANNEL_COLUMN.LON) {
out = qa.longitude - qb.longitude;
} else if (h === CHANNEL_COLUMN.AZIMUTH) {
out = qa.azimuth - qb.azimuth;
} else if (h === CHANNEL_COLUMN.DIP) {
out = qa.dip - qb.dip;
} else if (h === CHANNEL_COLUMN.DEPTH) {
out = qa.depth - qb.depth;
} else if (h === CHANNEL_COLUMN.ELEVATION) {
out = qa.elevation - qb.elevation;
} else {
// just use string
const ta = this.getChannelValue(qa, h);
const tb = this.getChannelValue(qb, h);
if (ta < tb) {
out = -1;
} else if (ta > tb) {
out = 1;
} else {
out = 0;
}
}
} else {
// cant find one of the Channels, oh well
}
return out;
});
if (this.lastSortCol === h) {
if (this.lastSortAsc) {
rows.reverse();
}
this.lastSortAsc = !this.lastSortAsc;
} else {
this.lastSortAsc = true;
}
// this effectively remove and then appends the rows in new order
rows.forEach((v) => {
tbody.appendChild(v);
});
this.lastSortCol = h;
} else {
// no tbody for table sort??
}
}
}
export const CHANNEL_INFO_ELEMENT = "sp-channel-table";
customElements.define(CHANNEL_INFO_ELEMENT, ChannelTable);
export interface StationTable extends HTMLElement {
// overload for custom events
addEventListener(type: E, listener: (ev: StationClickEventMap[E]) => any): void;
}
export class StationTable extends HTMLElement {
_columnLabels: Map = new Map();
_stationList: Array;
_rowToStation: Map;
lastSortAsc = true;
lastSortCol: string | undefined;
_columnValues: Map string|HTMLElement>;
_caption?: string|HTMLElement;
constructor(
stationList?: Array,
columnLabels?: Map,
columnValues?: Map string|HTMLElement>
) {
super();
if (!stationList) {
stationList = [];
}
if (!columnLabels) {
columnLabels = StationTable.createDefaultColumnLabels();
}
this._columnLabels = columnLabels;
if (!columnValues) {
columnValues = new Map string|HTMLElement>();
const defColumnValues = StationTable.createDefaultColumnValues();
for (const key of columnLabels.keys()) {
if (defColumnValues.has(key)) {
const fn = defColumnValues.get(key);
if (fn != null) {
columnValues.set(key, fn);
} else {
throw new Error(`StationTable function for key is missing: ${key}`);
}
} else {
throw new Error(`Unknown StationTable key: ${key}`);
}
}
}
this._columnValues = columnValues;
this._stationList = stationList;
this._rowToStation = new Map();
const shadow = this.attachShadow({ mode: "open" });
const table = document.createElement("table");
table.setAttribute("class", "wrapper");
addStyleToElement(this, TABLE_CSS);
shadow.appendChild(table);
}
get stationList(): Array {
return this._stationList;
}
set stationList(ql: Array) {
this._stationList = ql;
this.draw();
}
get columnLabels(): Map {
return this._columnLabels;
}
set columnLabels(cols: Map) {
this._columnLabels = cols;
this.draw();
}
get columnValues(): Map string|HTMLElement> {
return this._columnValues;
}
set columnValues(cols: Map string|HTMLElement>) {
this._columnValues = cols;
this.draw();
}
addColumn(key: string, label: string, valueFn: (q: Station) => string|HTMLElement) {
this.columnLabels.set(key, label);
this.columnValues.set(key, valueFn);
}
get caption(): string|HTMLElement|undefined {
return this._caption;
}
set caption(cap: string|HTMLElement|undefined) {
this._caption = cap;
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (table && this._caption) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
}
addStyle(css: string, id?: string): HTMLStyleElement {
return addStyleToElement(this, css, id);
}
draw() {
if (!this.isConnected) {
return;
}
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (this._caption) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
table.deleteTHead();
const theader = table.createTHead().insertRow();
this.headers().forEach((h) => {
const cell = theader.appendChild(document.createElement("th"));
const label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
cell.textContent = `${label}`;
cell.addEventListener("click", () => {
this.sort(h, cell);
});
});
table.querySelectorAll("tbody")?.forEach((tb: Node) => {
table.removeChild(tb);
});
const tbody = table.createTBody();
this.stationList.forEach((s) => {
const row = tbody.insertRow();
this.populateRow(s, row, -1);
row.addEventListener("click", (evt) => {
this.dispatchEvent(createStationClickEvent(s, evt));
});
});
}
headers(): Array {
return Array.from(this._columnValues.keys());
}
populateRow(q: Station, row: HTMLTableRowElement, index: number) {
this._rowToStation.set(row, q);
this.headers().forEach((h) => {
const cell = row.insertCell(index);
const cellValue = this.getStationValue(q, h);
if (cellValue instanceof HTMLElement) {
cell.appendChild(cellValue);
} else {
cell.textContent = cellValue;
}
if (index !== -1) {
index++;
}
});
}
tableToCSV() {
const out: Array> = [];
const headRow:Array = [];
out.push(headRow);
this.headers().forEach((h) => {
let label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
label = label ? label : "";
headRow.push(label);
});
this._stationList.forEach((q) => {
const row: Array = [];
out.push(row);
this.headers().forEach((h) => {
const cellValue = this.getStationValue(q, h);
if (cellValue instanceof HTMLElement) {
row.push(cellValue.textContent);
} else {
row.push(cellValue);
}
});
});
return csvFormatRows(out);
}
static createDefaultColumnLabels() {
const columnLabels = new Map();
columnLabels.set(STATION_COLUMN.CODE, "Code");
columnLabels.set(STATION_COLUMN.START, "Start");
columnLabels.set(STATION_COLUMN.END, "End");
columnLabels.set(STATION_COLUMN.LAT, "Lat");
columnLabels.set(STATION_COLUMN.LON, "Lon");
columnLabels.set(STATION_COLUMN.ELEVATION, "Evel");
columnLabels.set(STATION_COLUMN.SOURCEID, "SourceId");
return columnLabels;
}
static createDefaultColumnValues() {
const columnValues = new Map string>();
columnValues.set(STATION_COLUMN.START, (sta: Station) => stringify(sta.startDate.toISO()));
columnValues.set(STATION_COLUMN.END, (sta: Station) => sta.endDate ? stringify(sta.endDate.toISO()) : "");
columnValues.set(STATION_COLUMN.LAT, (sta: Station) => latlonFormat.format(sta.latitude));
columnValues.set(STATION_COLUMN.LON, (sta: Station) => latlonFormat.format(sta.longitude));
columnValues.set(STATION_COLUMN.ELEVATION, (sta: Station) => depthMeterFormat.format(sta.elevation));
columnValues.set(STATION_COLUMN.SOURCEID, (sta: Station) => `${sta.sourceId.toString()}`);
columnValues.set(STATION_COLUMN.CODE, (sta: Station) => `${sta.codes()}`);
columnValues.set(STATION_COLUMN.NETWORK_CODE, (sta: Station) => `${sta.networkCode}`);
columnValues.set(STATION_COLUMN.STATION_CODE, (sta: Station) => `${sta.stationCode}`);
columnValues.set(STATION_COLUMN.DESCRIPTION, (sta: Station) => `${sta.description}`);
return columnValues;
}
getStationValue(q: Station, h: string): string|HTMLElement {
if (this._columnValues.has(h)) {
const fn = this._columnValues.get(h);
if (fn != null) {
return fn(q);
}
}
if (h === STATION_COLUMN.START) {
return stringify(q.startDate.toISO());
} else if (h === STATION_COLUMN.END) {
return q.endDate ? stringify(q.endDate.toISO()) : "";
} else if (h === STATION_COLUMN.LAT) {
return latlonFormat.format(q.latitude);
} else if (h === STATION_COLUMN.LON) {
return latlonFormat.format(q.longitude);
} else if (h === STATION_COLUMN.ELEVATION) {
return depthMeterFormat.format(q.elevation);
} else if (h === STATION_COLUMN.SOURCEID) {
return `${q.sourceId.toString()}`;
} else if (h === STATION_COLUMN.CODE) {
return `${q.codes()}`;
} else if (h === STATION_COLUMN.NETWORK_CODE) {
return `${q.networkCode}`;
} else if (h === STATION_COLUMN.STATION_CODE) {
return `${q.stationCode}`;
} else if (h === STATION_COLUMN.DESCRIPTION) {
return `${q.description}`;
} else {
return `unknown: ${String(h)}`;
}
}
sort(h: string, _headerCell: HTMLTableCellElement) {
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
const tbody = table.querySelector("tbody");
if (tbody) {
const rows = Array.from(tbody.querySelectorAll("tr"));
rows.sort((rowa, rowb) => {
let out = 0;
const qa = this._rowToStation.get(rowa);
const qb = this._rowToStation.get(rowb);
if (qa && qb) {
if (h === STATION_COLUMN.START) {
out = qa.startDate.toMillis() - qb.startDate.toMillis();
} else if (h === STATION_COLUMN.END) {
if (qa.endDate && qb.endDate) {
out = qa.endDate.toMillis() - qb.endDate.toMillis();
} else if (qb.endDate) {
return 1;
} else {
return -1;
}
} else if (h === STATION_COLUMN.LAT) {
out = qa.latitude - qb.latitude;
} else if (h === STATION_COLUMN.LON) {
out = qa.longitude - qb.longitude;
} else if (h === STATION_COLUMN.ELEVATION) {
out = qa.elevation - qb.elevation;
} else {
// just use string
const ta = this.getStationValue(qa, h);
const tb = this.getStationValue(qb, h);
if (ta < tb) {
out = -1;
} else if (ta > tb) {
out = 1;
} else {
out = 0;
}
}
} else {
// cant find one of the Stations, oh well
}
return out;
});
if (this.lastSortCol === h) {
if (this.lastSortAsc) {
rows.reverse();
}
this.lastSortAsc = !this.lastSortAsc;
} else {
this.lastSortAsc = true;
}
// this effectively remove and then appends the rows in new order
rows.forEach((v) => {
tbody.appendChild(v);
});
this.lastSortCol = h;
} else {
// no tbody for table sort???
}
}
}
export const STATION_INFO_ELEMENT = "sp-station-table";
customElements.define(STATION_INFO_ELEMENT, StationTable);
export class SeismogramTable extends HTMLElement {
_columnLabels: Map;
_columnValues: Mapstring|HTMLElement>;
_sddList: Array;
_rowToSDD: Map;
lastSortAsc = true;
lastSortCol: string | undefined;
_caption?: string|HTMLElement;
constructor(
sddList?: Array,
columnLabels?: Map,
columnValues?: Mapstring|HTMLElement>
) {
super();
if (!sddList) {
sddList = [];
}
if (!columnLabels) {
columnLabels = new Map();
columnLabels.set(SEISMOGRAM_COLUMN.CODE, "Code");
columnLabels.set(SEISMOGRAM_COLUMN.START, "Start");
columnLabels.set(SEISMOGRAM_COLUMN.END, "End");
columnLabels.set(SEISMOGRAM_COLUMN.DURATION, "Dur");
columnLabels.set(SEISMOGRAM_COLUMN.SAMPLE_RATE, "Sample Rate");
columnLabels.set(SEISMOGRAM_COLUMN.SAMPLE_PERIOD, "Sample Period");
columnLabels.set(SEISMOGRAM_COLUMN.NUM_POINTS, "Npts");
columnLabels.set(SEISMOGRAM_COLUMN.SEGMENTS, "Segments");
columnLabels.set(SEISMOGRAM_COLUMN.SOURCEID, "SourceId");
}
this._sddList = sddList;
this._columnLabels = columnLabels;
this._rowToSDD = new Map();
// Column Values are optional at the individual key level.
// For the columns that the user does not provide a function,
// use the default display style in getSeismogramValue
if (!columnValues) {
columnValues = new Map string>();
const defColumnValues = SeismogramTable.createDefaultColumnValues();
for (const key of columnLabels.keys()) {
if (defColumnValues.has(key)) {
const fn = defColumnValues.get(key);
if (fn != null) {
columnValues.set(key, fn);
} else {
throw new Error(`SeismogramTable function for key is missing: ${key}`);
}
} else {
throw new Error(`Unknown SeismogramTable key: ${key}`);
}
}
}
this._columnValues = columnValues;
const shadow = this.attachShadow({ mode: "open" });
const table = document.createElement("table");
table.setAttribute("class", "wrapper");
addStyleToElement(this, TABLE_CSS);
shadow.appendChild(table);
}
get seisData(): Array {
return this._sddList;
}
set seisData(ql: Array) {
this._sddList = ql;
this.draw();
}
get columnLabels(): Map {
return this._columnLabels;
}
set columnLabels(cols: Map) {
this._columnLabels = cols;
this.draw();
}
get columnValues(): Map string|HTMLElement> {
return this._columnValues;
}
set columnValues(cols: Map string|HTMLElement>) {
this._columnValues = cols;
this.draw();
}
addColumn(key: string, label: string, valueFn: (q: SeismogramDisplayData) => string|HTMLElement) {
this.columnLabels.set(key, label);
this.columnValues.set(key, valueFn);
}
get caption(): string|HTMLElement|undefined {
return this._caption;
}
set caption(cap: string|HTMLElement|undefined) {
this._caption = cap;
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (table && this._caption) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
}
addStyle(css: string, id?: string): HTMLStyleElement {
return addStyleToElement(this, css, id);
}
draw() {
if (!this.isConnected) {
return;
}
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
if (this._caption) {
let captionEl = table.createCaption();
if (this._caption instanceof HTMLElement) {
captionEl.innerHTML="";
captionEl.appendChild(this._caption);
} else {
captionEl.textContent = this._caption;
}
} else {
table.deleteCaption();
}
table.deleteTHead();
const theader = table.createTHead().insertRow();
this.headers().forEach((h) => {
const cell = theader.appendChild(document.createElement("th"));
const label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
cell.textContent = `${label}`;
cell.addEventListener("click", () => {
this.sort(h, cell);
});
});
table.querySelectorAll("tbody")?.forEach((tb: Node) => {
table.removeChild(tb);
});
const tbody = table.createTBody();
this._sddList.forEach((q) => {
const row = tbody.insertRow();
this.populateRow(q, row, -1);
});
}
headers(): Array {
return Array.from(this._columnLabels.keys());
}
populateRow(q: SeismogramDisplayData, row: HTMLTableRowElement, index: number) {
this._rowToSDD.set(row, q);
this.headers().forEach((h) => {
const cell = row.insertCell(index);
const cellValue = this.getSeismogramValue(q, h);
if (cellValue instanceof HTMLElement) {
cell.appendChild(cellValue);
} else {
cell.textContent = cellValue;
}
if (index !== -1) {
index++;
}
});
}
tableToCSV() {
const out: Array> = [];
const headRow:Array = [];
out.push(headRow);
this.headers().forEach((h) => {
let label = this._columnLabels.has(h) ? this._columnLabels.get(h) : h;
label = label ? label : "";
headRow.push(label);
});
this._sddList.forEach((q) => {
const row: Array = [];
out.push(row);
this.headers().forEach((h) => {
const cellValue = this.getSeismogramValue(q, h);
if (cellValue instanceof HTMLElement) {
row.push(cellValue.textContent);
} else {
row.push(cellValue);
}
});
});
return csvFormatRows(out);
}
getSeismogramValue(q: SeismogramDisplayData, h: string): string|HTMLElement {
const fn = this._columnValues.has(h) ? this._columnValues.get(h) : null;
if (fn != null) {
return fn(q);
} else {
return `unknown: ${String(h)}`;
}
}
static createDefaultColumnValues() {
const columnValues = new Map string>();
columnValues.set(SEISMOGRAM_COLUMN.START, (q: SeismogramDisplayData) => stringify(q.start.toISO()));
columnValues.set(SEISMOGRAM_COLUMN.END, (q: SeismogramDisplayData) => stringify(q.start.toISO()));
columnValues.set(SEISMOGRAM_COLUMN.DURATION, (q: SeismogramDisplayData) => stringify(q.timeRange.toDuration().toISO()));
columnValues.set(SEISMOGRAM_COLUMN.NUM_POINTS, (q: SeismogramDisplayData) => `${q.numPoints}`);
columnValues.set(SEISMOGRAM_COLUMN.SAMPLE_RATE, (q: SeismogramDisplayData) => `${q._seismogram?.sampleRate}`);
columnValues.set(SEISMOGRAM_COLUMN.SAMPLE_PERIOD, (q: SeismogramDisplayData) => `${q._seismogram?.samplePeriod}`);
columnValues.set(SEISMOGRAM_COLUMN.SEGMENTS, (q: SeismogramDisplayData) => `${q._seismogram?.segments.length}`);
columnValues.set(SEISMOGRAM_COLUMN.SOURCEID, (q: SeismogramDisplayData) => `${q.sourceId.toString()}`);
columnValues.set(SEISMOGRAM_COLUMN.CODE, (q: SeismogramDisplayData) => `${q.codes()}`);
columnValues.set(SEISMOGRAM_COLUMN.NETWORK_CODE, (q: SeismogramDisplayData) => `${q.networkCode}`);
columnValues.set(SEISMOGRAM_COLUMN.STATION_CODE, (q: SeismogramDisplayData) => `${q.stationCode}`);
return columnValues;
}
static getSeismogramValue(
q: SeismogramDisplayData,
h: string,
): string {
if (h === SEISMOGRAM_COLUMN.START) {
return stringify(q.start.toISO());
} else if (h === SEISMOGRAM_COLUMN.END) {
return stringify(q.end.toISO());
} else if (h === SEISMOGRAM_COLUMN.DURATION) {
return stringify(q.timeRange.toDuration().toISO());
} else if (h === SEISMOGRAM_COLUMN.NUM_POINTS) {
return `${q.numPoints}`;
} else if (h === SEISMOGRAM_COLUMN.SAMPLE_RATE) {
return q._seismogram ? `${q._seismogram.sampleRate}` : "";
} else if (h === SEISMOGRAM_COLUMN.SAMPLE_PERIOD) {
return q._seismogram ? `${q._seismogram.samplePeriod}` : "";
} else if (h === SEISMOGRAM_COLUMN.SEGMENTS) {
return q._seismogram ? `${q._seismogram.segments.length}` : "";
} else if (h === SEISMOGRAM_COLUMN.SOURCEID) {
return `${q.sourceId.toString()}`;
} else if (h === SEISMOGRAM_COLUMN.CODE) {
return `${q.codes()}`;
} else if (h === SEISMOGRAM_COLUMN.NETWORK_CODE) {
return `${q.networkCode}`;
} else if (h === SEISMOGRAM_COLUMN.STATION_CODE) {
return `${q.stationCode}`;
} else {
return `unknown: ${String(h)}`;
}
}
sort(h: string, _headerCell: HTMLTableCellElement) {
const table = this.shadowRoot?.querySelector("table") as HTMLTableElement;
const tbody = table.querySelector("tbody");
if (tbody) {
const rows = Array.from(tbody.querySelectorAll("tr"));
rows.sort((rowa, rowb) => {
let out = 0;
const qa = this._rowToSDD.get(rowa);
const qb = this._rowToSDD.get(rowb);
if (qa && qb) {
if (h === SEISMOGRAM_COLUMN.START) {
out = qa.start.toMillis() - qb.start.toMillis();
} else if (h === SEISMOGRAM_COLUMN.END) {
out = qa.end.toMillis() - qb.end.toMillis();
} else if (h === SEISMOGRAM_COLUMN.DURATION) {
out =
qa.timeRange.toDuration().toMillis() -
qb.timeRange.toDuration().toMillis();
} else if (h === SEISMOGRAM_COLUMN.NUM_POINTS) {
out = qa.numPoints - qb.numPoints;
} else if (h === SEISMOGRAM_COLUMN.SAMPLE_RATE) {
out =
(qa._seismogram ? qa._seismogram.sampleRate : 0) -
(qb._seismogram ? qb._seismogram.sampleRate : 0);
} else if (h === SEISMOGRAM_COLUMN.SAMPLE_PERIOD) {
out =
(qa._seismogram ? qa._seismogram.samplePeriod : 0) -
(qb._seismogram ? qb._seismogram.samplePeriod : 0);
} else if (h === SEISMOGRAM_COLUMN.SEGMENTS) {
out =
(qa._seismogram ? qa._seismogram.segments.length : 0) -
(qb._seismogram ? qb._seismogram.segments.length : 0);
} else {
// just use string
const ta = SeismogramTable.getSeismogramValue(qa, h);
const tb = SeismogramTable.getSeismogramValue(qb, h);
if (ta < tb) {
out = -1;
} else if (ta > tb) {
out = 1;
} else {
out = 0;
}
}
} else {
// cant find one of the items, oh well
}
return out;
});
if (this.lastSortCol === h) {
if (this.lastSortAsc) {
rows.reverse();
}
this.lastSortAsc = !this.lastSortAsc;
} else {
this.lastSortAsc = true;
}
// this effectively remove and then appends the rows in new order
rows.forEach((v) => {
tbody.appendChild(v);
});
this.lastSortCol = h;
} else {
// no tbody for table sort???
}
}
}
export const SDD_INFO_ELEMENT = "sp-seismogram-table";
customElements.define(SDD_INFO_ELEMENT, SeismogramTable);
export const latlonFormat = textformat.latlonFormat;
export const magFormat = textformat.magFormat;
export const depthFormat = textformat.depthFormat;
export const depthNoUnitFormat = textformat.depthNoUnitFormat;
export const depthMeterFormat = textformat.depthMeterFormat;