diff --git a/InfraView/widgets/IPApplicationWindow.py b/InfraView/widgets/IPApplicationWindow.py
index 827e13f..6e467c8 100644
--- a/InfraView/widgets/IPApplicationWindow.py
+++ b/InfraView/widgets/IPApplicationWindow.py
@@ -17,9 +17,9 @@
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtGui import QKeySequence
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, QSettings, QSize, QPoint, QDir
-from PyQt5.QtWidgets import (QAction, QFileDialog, QTabWidget, QGridLayout, QVBoxLayout,
- QHBoxLayout, QInputDialog, QLabel, QTableWidgetItem, QMessageBox, QWidget,
- QApplication, QDialog, QDoubleSpinBox)
+from PyQt5.QtWidgets import (QAction, QDialog, QFileDialog, QTabWidget, QGridLayout,
+ QFormLayout, QHBoxLayout, QVBoxLayout, QLabel, QTableWidgetItem, QMessageBox, QWidget,
+ QApplication, QDialog, QDialogButtonBox, QDoubleSpinBox, QLineEdit)
# Infrapy includes
@@ -111,6 +111,9 @@ def buildUI(self):
self.setCentralWidget(self.main_widget)
+ self.fill_sta_info_dialog = IPFillStationInfoDialog()
+ self.redundant_trace_dialog = IPRedundantTraceDialog()
+
def debug_trace(self): # for debugging, you have to call pyqtRemoveInputHook before set_trace()
from PyQt5.QtCore import pyqtRemoveInputHook
from pdb import set_trace
@@ -231,26 +234,60 @@ def filemenu_Open(self):
ifiles = QFileDialog.getOpenFileNames(self, 'Open File...', previous_directory)
+ new_inventory = None
+ new_stream = None
+
if len(ifiles[0]) > 0:
for ifile in ifiles[0]:
+ current_trace_names = []
+ if self.waveformWidget._sts is not None:
+ for trace in self.waveformWidget._sts:
+ current_trace_names.append(self.getTraceName(trace))
+
ipath = os.path.dirname(ifile)
if self._project is None:
self.settings.setValue("last_open_directory", ipath)
else:
self._project.set_dataPath(ipath)
try:
- if self.waveformWidget._sts is not None:
- self.waveformWidget._sts += obsRead(ifile)
+ new_stream = obsRead(ifile)
+ # do our best to generate new inventory from the new stream
+ for trace in new_stream:
+ trace_name = self.getTraceName(trace)
+ if trace_name in current_trace_names:
+ # redundant trace!
+ netid, staid, locid, chaid = self.parseTraceName(trace_name)
+ self.redundant_trace_dialog.exec_(trace_name)
+
+ if self.redundant_trace_dialog.get_result():
+ # if accepted, they want to use the new trace so first remove the old one
+ print(self.waveformWidget._inv)
+ self.waveformWidget.remove_from_inventory(netid, staid, locid, chaid)
+ print(self.waveformWidget._inv)
+ else:
+ # if rejected, they want to keep the old trace, and ignore this one
+ #so remove the trace from new_stream, and continue to the next trace
+ new_stream.remove(trace)
+ continue
+
+ if new_inventory is None:
+ new_inventory = self.trace_to_inventory(trace)
+ else:
+ new_inventory += self.trace_to_inventory(trace)
+
+ # for now we will remove dc offset when loading the file. Maybe should be an option?
+ trace.data = trace.data - np.mean(trace.data)
+
+ if self.waveformWidget._sts is not None:
+ self.waveformWidget._sts += new_stream
else:
- self.waveformWidget._sts = obsRead(ifile)
+ self.waveformWidget._sts = new_stream
+
except Exception:
self.setStatus("File Read Error", 5000)
continue
- for trace in self.waveformWidget._sts:
- trace.data = trace.data - np.mean(trace.data)
-
self.waveformWidget._sts.merge(fill_value=0)
else:
# No files were chosen to open
@@ -260,25 +297,9 @@ def filemenu_Open(self):
# if not populate the trace stats viewer and plot the traces
if self.waveformWidget._sts is not None:
self.beamformingWidget.setStreams(self.waveformWidget._sts)
-
- new_inventory = None
- for trace in self.waveformWidget._sts:
- if trace.stats['_format'] == 'SAC':
- if new_inventory is None:
- new_inventory = self.sac_trace_to_inventory(trace)
- else:
- new_inventory += self.sac_trace_to_inventory(trace)
- elif trace.stats['_format'] == 'MSEED':
- # miniseed files have no metadata, so we need to deal with
- # the inventory seperately
-
- # TODO
- # First, there's a chance that the inventory data has been
- # loaded already, so lets check the current inventory, and if it
- # has been, leave it alone. We do need to remove inventory that does
- pass
-
+
self.sig_stream_changed.emit(self.waveformWidget._sts)
+
if new_inventory is not None:
self.sig_inventory_changed.emit(new_inventory)
@@ -292,6 +313,15 @@ def filemenu_import(self):
if self.fdsnDialog.exec_():
self.mainTabs.setCurrentIndex(0)
+ def getTraceName(self, trace):
+ traceName = trace.stats['network'] + '.' + trace.stats['station'] + \
+ '.' + trace.stats['location'] + '.' + trace.stats['channel']
+ return traceName
+
+ def parseTraceName(self, trace_name):
+ bits = trace_name.split('.')
+ return bits[0], bits[1], bits[2], bits[3]
+
def filemenu_saveAllWaveforms(self):
if self.waveformWidget._sts is None:
self.errorPopup('Oops... No waveforms to save')
@@ -347,72 +377,96 @@ def filemenu_ClearWaveforms(self):
self.beamformingWidget.clearWaveformPlot()
self.waveformWidget.clearWaveforms()
- def sac_trace_to_inventory(self, trace):
- # if sac files are opened, it's useful to extract inventory from their streams so that we can populate the stations tabs and the location widget
+ def trace_to_inventory(self, trace):
+ # if sac files are opened, it's useful to extract inventory from their streams so that we can populate the
+ # stations tabs and the location widget
new_inventory = None
- # The next bit is cribbed from the obspy webpage on building a stationxml site from scratch
+ # The next bit is modified from the obspy webpage on building a stationxml site from scratch
# https://docs.obspy.org/tutorial/code_snippets/stationxml_file_from_scratch.html
#
# We'll first create all the various objects. These strongly follow the
# hierarchy of StationXML files.
+ # initialize the lat/lon/ele
+ _lat = 0.0
+ _lon = 0.0
+ _ele = -1.0
+
+ _network = trace.stats['network']
+ _station = trace.stats['station']
+ _channel = trace.stats['channel']
+ _location = trace.stats['location']
+
+ # if the trace is from a sac file, the sac header might have some inventory information
+ if trace.stats['_format'] == 'SAC':
+ print('sac file!!')
+ if 'stla' in trace.stats['sac']:
+ _lat = trace.stats['sac']['stla']
+
+ if 'stlo' in trace.stats['sac']:
+ _lon = trace.stats['sac']['stlo']
+
+ if 'stel' in trace.stats['sac']:
+ _ele = trace.stats['sac']['stel']
+ else:
+ _ele = 0.333
+
+ if _lat == 0.0 or _lon == 0.0 or _ele < 0:
+ if self.fill_sta_info_dialog.exec_(_network, _station, _location, _channel, _lat, _lon, _ele):
+
+ edited_values = self.fill_sta_info_dialog.get_values()
+
+ _lat = edited_values['lat']
+ _lon = edited_values['lon']
+ _ele = edited_values['ele']
+
+ _network = edited_values['net']
+ _station = edited_values['sta']
+ _location = edited_values['loc']
+ _channel = edited_values['cha']
+
+ # (re)populate sac headers where possible
+ if trace.stats['_format'] == 'SAC':
+ trace.stats['sac']['stla'] = _lat
+ trace.stats['sac']['stlo'] = _lon
+ trace.stats['sac']['stel'] = _ele
+ trace.stats['sac']['knetwk'] = _network
+ trace.stats['sac']['kstnm'] = _station
+ # (re)populate trace stats where possible
+ trace.stats['network'] = _network
+ trace.stats['station'] = _station
+ trace.stats['location'] = _location
+ trace.stats['channel'] = _channel
+
new_inventory = Inventory(
# We'll add networks later.
networks=[],
# The source should be the id whoever create the file.
source="InfraView")
-
- # Attempt to retrieve it from the sac header, if not found, set it to '---'
- _network = trace.stats['network']
-
- if _network == '':
- _network = '###'
-
+
net = Network(
# This is the network code according to the SEED standard.
code=_network,
# A list of stations. We'll add one later.
stations=[],
- # Description isn't something that's in the SAC header, so lets set it to the network cod
+ # Description isn't something that's in the trace stats or SAC header, so lets set it to the network cod
description=_network,
# Start-and end dates are optional.
# Start and end dates for the network are not stored in the sac header so lets set it to 1/1/1900
start_date=UTCDateTime(1900, 1, 1))
- _station = trace.stats['station']
- if _station == '':
- _station = '###'
-
- if 'stla' in trace.stats['sac']:
- _lat = trace.stats['sac']['stla']
- else:
- self.errorPopup("SAC header doesn't contain latitude information")
- _lat = 10.0
- if 'stlo' in trace.stats['sac']:
- _lon = trace.stats['sac']['stlo']
- else:
- self.errorPopup("SAC header doesn't contain longitude information")
- _lon = 10.0
- if 'stel' in trace.stats['sac']:
- _ele = trace.stats['sac']['stel']
- else:
- _ele = -999.9
-
sta = Station(
# This is the station code according to the SEED standard.
-
code=_station,
latitude=_lat,
longitude=_lon,
elevation=_ele,
- # Creation_date is not saved in the sac header
+ # Creation_date is not saved in the trace stats or sac header
creation_date=UTCDateTime(1900, 1, 1),
- # Site name is not in the sac header, so set it to the site code
+ # Site name is not in the trace stats or sac header, so set it to the site code
site=Site(name=_station))
- _channel = trace.stats['channel']
- _location = trace.stats['location']
# This is the channel code according to the SEED standard.
cha = Channel(code=_channel,
# This is the location code according to the SEED standard.
@@ -465,3 +519,187 @@ def closeEvent(self, ce):
# Obligatory about
def about(self):
QtWidgets.QMessageBox.about(self, "About", self.progname + " " + self.progversion + "\n" + "Copyright 2018\nLos Alamos National Laboratory")
+
+
+class CapsValidator(QtGui.QValidator):
+ # since most of the fields will require capitalized values only, here is a validator for the
+ # lineEdits
+ def validate(self, string, pos):
+ return QtGui.QValidator.Acceptable, string.upper(), pos
+
+class IPRedundantTraceDialog(QDialog):
+
+ def __init__(self):
+ super().__init__()
+ self.buildUI()
+
+ def exec_(self, trace_name):
+ intro_text = "I appears there is already a trace loaded with the name " + trace_name +". Would you like to keep the one that is currently in memory, or replace it with this one?"
+ self.intro_label.setText(intro_text)
+
+ return super().exec_()
+
+ def buildUI(self):
+ self.setWindowTitle("Redundant Trace")
+ self.intro_label = QLabel("")
+ self.intro_label.setWordWrap(True)
+
+ # OK and Cancel buttons
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
+ Qt.Horizontal,
+ self)
+ buttons.button(QDialogButtonBox.Ok).setText("Use New One")
+ buttons.button(QDialogButtonBox.Cancel).setText("Keep Old One")
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+
+ vbox_layout = QVBoxLayout()
+ vbox_layout.addWidget(self.intro_label)
+ vbox_layout.addWidget(buttons)
+
+ self.setLayout(vbox_layout)
+
+ def accept(self):
+ self.result = True
+ super().accept()
+
+ def reject(self):
+ self.result = False
+ super().reject()
+
+ def get_result(self):
+ return self.result
+
+
+class IPFillStationInfoDialog(QDialog):
+
+ def __init__(self):
+ super().__init__()
+ self.buildUI()
+
+
+ def exec_(self, net, sta, loc, cha, lat, lon, ele):
+
+ self.lat_spin.setValue(lat)
+ self.lon_spin.setValue(lon)
+ self.ele_spin.setValue(ele)
+
+ self.net_edit.setText(net)
+ self.sta_edit.setText(sta)
+ self.loc_edit.setText(loc)
+ self.cha_edit.setText(cha)
+
+ self.update_fullname_label()
+
+ return super().exec_()
+
+ def buildUI(self):
+ self.setWindowTitle('Trace metadata')
+ self.setMinimumWidth(300)
+ self.setMaximumWidth(400)
+
+ intro_text = """Some of the inventory information in this trace's stats appears to be absent or incorrect. This could cause problems later.
+ You can edit the information here. If you have a stationxml file, you can skip this form and load your stationxml file from the Station tab later."""
+ self.intro_label = QLabel(intro_text)
+ self.intro_label.setWordWrap(True)
+
+ self.lat_spin = QDoubleSpinBox()
+ self.lat_spin.setRange(-90.1, 90.0) # the -90.1 is used as the "unset" value
+ self.lat_spin.setDecimals(8)
+ self.lat_spin.setMaximumWidth(120)
+ self.lat_spin.setSingleStep(0.1)
+
+ self.lon_spin = QDoubleSpinBox()
+ self.lon_spin.setRange(-180.1, 180.0)
+ self.lon_spin.setDecimals(8)
+ self.lon_spin.setMaximumWidth(120)
+ self.lon_spin.setSingleStep(0.1)
+
+ self.ele_spin = QDoubleSpinBox()
+ self.ele_spin.setRange(-20000,20000)
+ self.ele_spin.setMaximumWidth(100)
+ self.ele_spin.setDecimals(2)
+
+ self.full_name = QLabel('')
+
+ caps_validator = CapsValidator(self)
+
+ # Network selector
+ self.net_edit = QLineEdit()
+ self.net_edit.setToolTip('Wildcards OK \nCan be SEED network codes or data center defined codes. \nMultiple codes are comma-separated (e.g. "IU,TA").')
+ self.net_edit.setMaximumWidth(60)
+ self.net_edit.setValidator(caps_validator)
+
+ self.sta_edit = QLineEdit()
+ self.sta_edit.setMaximumWidth(60)
+ self.sta_edit.setToolTip('Wildcards OK \nCan be SEED network codes or data center defined codes. \nMultiple codes are comma-separated (e.g. "IU,TA").')
+ self.sta_edit.setValidator(caps_validator)
+
+ self.loc_edit = QLineEdit()
+ self.loc_edit.setMaximumWidth(60)
+ self.loc_edit.setToolTip('Wildcards OK \nCan be SEED network codes or data center defined codes. \nMultiple codes are comma-separated (e.g. "IU,TA").')
+ self.loc_edit.setValidator(caps_validator)
+
+ self.cha_edit = QLineEdit()
+ self.cha_edit.setMaximumWidth(60)
+ self.cha_edit.setToolTip('Wildcards OK \nCan be SEED network codes or data center defined codes. \nMultiple codes are comma-separated (e.g. "IU,TA").')
+ self.cha_edit.setValidator(caps_validator)
+
+ # OK and Cancel buttons
+ buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
+ Qt.Horizontal,
+ self)
+ buttons.button(QDialogButtonBox.Ok).setText("Set Values")
+ buttons.button(QDialogButtonBox.Cancel).setText("Skip")
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+
+ form_layout_col1 = QFormLayout()
+ form_layout_col1.addRow("", self.full_name)
+ form_layout_col1.addRow("Net: ", self.net_edit)
+ form_layout_col1.addRow("Sta: ", self.sta_edit)
+ form_layout_col1.addRow("Loc: ", self.loc_edit)
+ form_layout_col1.addRow("Cha: ", self.cha_edit)
+
+ form_layout_col2 = QFormLayout()
+ form_layout_col2.addRow("Lat: ", self.lat_spin)
+ form_layout_col2.addRow("Lon: ", self.lon_spin)
+ form_layout_col2.addRow("Ele: ", self.ele_spin)
+
+ hbox = QHBoxLayout()
+ hbox.addLayout(form_layout_col1)
+ hbox.addLayout(form_layout_col2)
+
+ vbox = QVBoxLayout()
+ vbox.addWidget(self.intro_label)
+ vbox.addLayout(hbox)
+ vbox.addWidget(buttons)
+
+ self.setLayout(vbox)
+
+ self.connectSignalsAndSlots()
+
+ def connectSignalsAndSlots(self):
+ self.net_edit.textChanged.connect(self.update_fullname_label)
+ self.sta_edit.textChanged.connect(self.update_fullname_label)
+ self.loc_edit.textChanged.connect(self.update_fullname_label)
+ self.cha_edit.textChanged.connect(self.update_fullname_label)
+
+ def get_values(self):
+ values = {"net": self.net_edit.text(),
+ "sta": self.sta_edit.text(),
+ "loc": self.loc_edit.text(),
+ "cha": self.cha_edit.text(),
+ "lat": self.lat_spin.value(),
+ "lon": self.lon_spin.value(),
+ "ele": self.ele_spin.value()}
+ return values
+
+ @pyqtSlot()
+ def update_fullname_label(self):
+ self.full_name.setText(self.net_edit.text() + '.' +
+ self.sta_edit.text() + '.' +
+ self.loc_edit.text() + '.' +
+ self.cha_edit.text())
+
+
diff --git a/InfraView/widgets/IPBeamformingWidget.py b/InfraView/widgets/IPBeamformingWidget.py
index a667441..40df1a1 100644
--- a/InfraView/widgets/IPBeamformingWidget.py
+++ b/InfraView/widgets/IPBeamformingWidget.py
@@ -687,7 +687,11 @@ def show_calculating_threshold_label(self, show):
def runBeamforming(self):
if self._streams is None:
- self.errorPopup('No data Loaded')
+ self.errorPopup('You should have at least 3 streams loaded to run beamfinder')
+ return
+
+ if len(self._streams) < 3:
+ self.errorPopup('You should have at least 3 waveforms loaded to run beamfinder')
return
if self._parent.waveformWidget.get_inventory() is None:
@@ -1074,11 +1078,11 @@ def runFinished(self):
for w in w_array:
- self.errorPopup(str(w.message), "Warning...")
+ self.errorPopup(str(w.message), "Warning")
if len(dets) == 0:
- self.errorPopup("No Detections Found")
+ self.errorPopup("No Detections Found", "Results")
return
self.detectionWidget.new_detections(dets,
diff --git a/InfraView/widgets/IPStationView.py b/InfraView/widgets/IPStationView.py
index c912555..4004c0a 100644
--- a/InfraView/widgets/IPStationView.py
+++ b/InfraView/widgets/IPStationView.py
@@ -120,13 +120,13 @@ def setInventory(self, _inventory):
ret = ("Network: {network_code}
"
"Station: {station_name}
"
"Station Code: {station_code}
"
- "Location Code: {location_code}"
+ "Location Code: {location_code}
"
"Channel Count: {selected}/{total} (Selected/Total)
"
- " {start_date} - {end_date}
"
+ "Available Dates: {start_date} - {end_date}
"
"Access: {restricted} {alternate_code}{historical_code}
"
- "Latitude: {lat:.6f}
"
- "Longitude: {lng:.6f}
"
- "Elevation: {elevation:.1f} m
")
+ "Latitude: {lat:.8f}
"
+ "Longitude: {lng:.8f}
"
+ "Elevation: {elevation:.2f} m
")
ret = ret.format(
network_code=network.code,
station_name=contents["stations"][0],
@@ -156,9 +156,9 @@ def setInventory(self, _inventory):
"Channel Count: {selected}/{total} (Selected/Total)
"
" {start_date} - {end_date}
"
"Access: {restricted} {alternate_code}{historical_code}
"
- "Latitude: {lat:.2f}
"
- "Longitude: {lng:.2f}
"
- "Elevation: {elevation:.1f} m
")
+ "Latitude: {lat:.8f}
"
+ "Longitude: {lng:.8f}
"
+ "Elevation: {elevation:.2f} m
")
ret = ret.format(
network_code=network.code,
station_name=contents["stations"][0],
diff --git a/InfraView/widgets/IPWaveformSelectorWidget.py b/InfraView/widgets/IPWaveformSelectorWidget.py
index 546d868..2fd43cf 100644
--- a/InfraView/widgets/IPWaveformSelectorWidget.py
+++ b/InfraView/widgets/IPWaveformSelectorWidget.py
@@ -50,6 +50,10 @@ def update_selections(self, new_stream):
# before we clear everything, lets first record the current elements in the name dictionary
# so we can keep track of what is currently visible/not visible. That way we can preserve
# their settings
+
+ if new_stream is None:
+ return # nothing to do
+
previous_name_list = self.name_list.copy()
previous_value_list = self.value_list.copy()
@@ -57,11 +61,8 @@ def update_selections(self, new_stream):
self.name_list.clear()
self.value_list.clear()
self.clear_form()
-
- # pull in the current list of loaded waveforms
- sts = new_stream
- for trace in sts:
+ for trace in new_stream:
# All new traces are automatically set to display
val = True # default value for a checkbox is True
if trace.id in previous_name_list:
diff --git a/InfraView/widgets/IPWaveformWidget.py b/InfraView/widgets/IPWaveformWidget.py
index abc7eb1..7feb3ae 100644
--- a/InfraView/widgets/IPWaveformWidget.py
+++ b/InfraView/widgets/IPWaveformWidget.py
@@ -185,9 +185,26 @@ def update_streams(self, new_stream):
@pyqtSlot(Inventory)
def update_inventory(self, new_inventory):
- self._inv = new_inventory
+ if self._inv is None:
+ self._inv = new_inventory
+ else:
+ self._inv += new_inventory
self.stationViewer.setInventory(self._inv)
+ def remove_from_inventory(self, net, sta, loc, cha):
+ #print("removing! {}.{}.{}.{}".format(net,sta,loc,cha))
+ #new_inventory = self._inv.remove(network=net, station=sta, location=loc, channel=cha)
+ self.inv_remove(self._inv, network=net, station=sta, location=loc, channel=cha, keep_empty=False)
+ #print("updating!")
+ self.update_inventory(new_inventory)
+ #print("removed!")
+
+ def debug_trace(self): # for debugging, you have to call pyqtRemoveInputHook before set_trace()
+ from PyQt5.QtCore import pyqtRemoveInputHook
+ from pdb import set_trace
+ pyqtRemoveInputHook()
+ set_trace()
+
@pyqtSlot(dict)
def update_filtered_data(self, filter_settings):