diff --git a/QtPyLoT.py b/QtPyLoT.py index 998a6c72..1136a29d 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -42,6 +42,12 @@ from PySide.QtGui import QMainWindow, QInputDialog, QIcon, QFileDialog, \ import numpy as np from obspy import UTCDateTime +try: + import pyqtgraph as pg +except: + print('QtPyLoT: Could not import pyqtgraph.') + pg = None + try: from matplotlib.backends.backend_qt4agg import FigureCanvas except ImportError: @@ -66,7 +72,7 @@ from pylot.core.util.utils import fnConstructor, getLogin, \ full_range from pylot.core.io.location import create_creation_info, create_event from pylot.core.util.widgets import FilterOptionsDialog, NewEventDlg, \ - WaveformWidget, PropertiesDlg, HelpForm, createAction, PickDlg, \ + WaveformWidget, WaveformWidgetPG, PropertiesDlg, HelpForm, createAction, PickDlg, \ getDataType, ComparisonDialog, TuneAutopicker, AutoPickParaBox from pylot.core.util.map_projection import map_projection from pylot.core.util.structure import DATASTRUCTURE @@ -139,9 +145,6 @@ class MainWindow(QMainWindow): self.dirty = False - # setup UI - self.setupUi() - if settings.value("user/FullName", None) is None: fulluser = QInputDialog.getText(self, "Enter Name:", "Full name") settings.setValue("user/FullName", fulluser) @@ -165,6 +168,9 @@ class MainWindow(QMainWindow): settings.setValue('compclass', SetChannelComponents()) settings.sync() + # setup UI + self.setupUi() + self.filteroptions = {} self.pickDlgs = {} self.picks = {} @@ -184,8 +190,6 @@ class MainWindow(QMainWindow): self.setWindowTitle("PyLoT - do seismic processing the python way") self.setWindowIcon(pylot_icon) - xlab = self.startTime.strftime('seconds since %Y/%m/%d %H:%M:%S (%Z)') - _widget = QWidget() self._main_layout = QVBoxLayout() @@ -208,15 +212,13 @@ class MainWindow(QMainWindow): self._main_layout.addWidget(self.tabs) self.tabs.currentChanged.connect(self.refreshTabs) - # create central matplotlib figure canvas widget - plottitle = "Overview: {0} components ".format(self.getComponent()) - self.dataPlot = WaveformWidget(parent=self, xlabel=xlab, ylabel=None, - title=plottitle) - self.dataPlot.setCursor(Qt.CrossCursor) - # add scroll area used in case number of traces gets too high self.wf_scroll_area = QtGui.QScrollArea() + # create central matplotlib figure canvas widget + self.pg = pg + self.init_wfWidget() + # init main widgets for main tabs wf_tab = QtGui.QWidget() array_tab = QtGui.QWidget() @@ -236,7 +238,6 @@ class MainWindow(QMainWindow): self.tabs.addTab(events_tab, 'Eventlist') self.wf_layout.addWidget(self.wf_scroll_area) - self.wf_scroll_area.setWidget(self.dataPlot) self.wf_scroll_area.setWidgetResizable(True) self.init_array_tab() self.init_event_table() @@ -510,6 +511,24 @@ class MainWindow(QMainWindow): self.setCentralWidget(_widget) + def init_wfWidget(self): + settings = QSettings() + xlab = self.startTime.strftime('seconds since %Y/%m/%d %H:%M:%S (%Z)') + plottitle = None#"Overview: {0} components ".format(self.getComponent()) + self.disconnectWFplotEvents() + if str(settings.value('pyqtgraphic')) == 'false' or not pg: + self.pg = False + self.dataPlot = WaveformWidget(parent=self, xlabel=xlab, ylabel=None, + title=plottitle) + else: + self.pg = True + self.dataPlot = WaveformWidgetPG(parent=self, xlabel=xlab, ylabel=None, + title=plottitle) + self.dataPlot.setCursor(Qt.CrossCursor) + self.wf_scroll_area.setWidget(self.dataPlot) + if self.get_current_event(): + self.plotWaveformDataThread() + def init_ref_test_buttons(self): ''' Initiate/create buttons for assigning events containing manual picks to reference or test set. @@ -998,10 +1017,7 @@ class MainWindow(QMainWindow): return self.dataPlot @staticmethod - def getWFID(gui_event): - - ycoord = gui_event.ydata - + def getWFID(ycoord): try: statID = int(round(ycoord)) except TypeError as e: @@ -1187,6 +1203,15 @@ class MainWindow(QMainWindow): ''' Connect signals refering to WF-Dataplot (select station, tutor_user, scrolling) ''' + if self.pg: + self.connect_pg() + else: + self.connect_mpl() + + def connect_pg(self): + self.poS_id = self.dataPlot.plotWidget.scene().sigMouseClicked.connect(self.pickOnStation) + + def connect_mpl(self): if not self.poS_id: self.poS_id = self.dataPlot.mpl_connect('button_press_event', self.pickOnStation) @@ -1198,11 +1223,20 @@ class MainWindow(QMainWindow): self.scroll_id = self.dataPlot.mpl_connect('scroll_event', self.scrollPlot) - def disconnectWFplotEvents(self): ''' Disconnect all signals refering to WF-Dataplot (select station, tutor_user, scrolling) ''' + if self.pg: + self.disconnect_pg() + else: + self.disconnect_mpl() + + def disconnect_pg(self): + if self.poS_id: + self.dataPlot.plotWidget.scene().sigMouseClicked.disconnect() + + def disconnect_mpl(self): if self.poS_id: self.dataPlot.mpl_disconnect(self.poS_id) if self.ae_id: @@ -1213,7 +1247,29 @@ class MainWindow(QMainWindow): self.ae_id = None self.scroll_id = None + def finish_pg_plot(self): + self.getPlotWidget().updateWidget() + plots = self.wfp_thread.data + for times, data in plots: + self.dataPlot.plotWidget.getPlotItem().plot(times, data, pen='k') + self.dataPlot.reinitMoveProxy() + self.dataPlot.plotWidget.showAxis('left') + self.dataPlot.plotWidget.showAxis('bottom') + def finishWaveformDataPlot(self): + if self.pg: + self.finish_pg_plot() + else: + self._max_xlims = self.dataPlot.getXLims() + plotWidget = self.getPlotWidget() + plotDict = plotWidget.getPlotDict() + pos = plotDict.keys() + labels = [plotDict[n][2]+'.'+plotDict[n][0] for n in pos] + plotWidget.setYTickLabels(pos, labels) + try: + plotWidget.figure.tight_layout() + except: + pass self.connectWFplotEvents() self.loadlocationaction.setEnabled(True) self.auto_tune.setEnabled(True) @@ -1237,7 +1293,10 @@ class MainWindow(QMainWindow): def clearWaveformDataPlot(self): self.disconnectWFplotEvents() - self.dataPlot.getAxes().cla() + if self.pg: + self.dataPlot.plotWidget.getPlotItem().clear() + else: + self.dataPlot.getAxes().cla() self.loadlocationaction.setEnabled(False) self.auto_tune.setEnabled(False) self.auto_pick.setEnabled(False) @@ -1254,10 +1313,11 @@ class MainWindow(QMainWindow): ''' Open a modal thread to plot current waveform data. ''' - wfp_thread = Thread(self, self.plotWaveformData, - progressText='Plotting waveform data...') - wfp_thread.finished.connect(self.finishWaveformDataPlot) - wfp_thread.start() + self.clearWaveformDataPlot() + self.wfp_thread = Thread(self, self.plotWaveformData, + progressText='Plotting waveform data...') + self.wfp_thread.finished.connect(self.finishWaveformDataPlot) + self.wfp_thread.start() def plotWaveformData(self): ''' @@ -1275,18 +1335,12 @@ class MainWindow(QMainWindow): # wfst += self.get_data().getWFData().select(component=alter_comp) plotWidget = self.getPlotWidget() self.adjustPlotHeight() - plotWidget.plotWFData(wfdata=wfst, title=title, mapping=False, component=comp, nth_sample=int(nth_sample)) - plotDict = plotWidget.getPlotDict() - pos = plotDict.keys() - labels = [plotDict[n][2]+'.'+plotDict[n][0] for n in pos] - plotWidget.setYTickLabels(pos, labels) - try: - plotWidget.figure.tight_layout() - except: - pass - self._max_xlims = self.dataPlot.getXLims() + plots = plotWidget.plotWFData(wfdata=wfst, title=title, mapping=False, component=comp, nth_sample=int(nth_sample)) + return plots def adjustPlotHeight(self): + if self.pg: + return height_need = len(self.data.getWFData())*self.height_factor plotWidget = self.getPlotWidget() if self.tabs.widget(0).frameSize().height() < height_need: @@ -1297,20 +1351,20 @@ class MainWindow(QMainWindow): def plotZ(self): self.setComponent('Z') self.plotWaveformDataThread() - self.drawPicks() - self.draw() + # self.drawPicks() + # self.draw() def plotN(self): self.setComponent('N') self.plotWaveformDataThread() - self.drawPicks() - self.draw() + # self.drawPicks() + # self.draw() def plotE(self): self.setComponent('E') self.plotWaveformDataThread() - self.drawPicks() - self.draw() + # self.drawPicks() + # self.draw() def pushFilterWF(self, param_args): self.get_data().filterWFData(param_args) @@ -1382,7 +1436,9 @@ class MainWindow(QMainWindow): return self.seismicPhase def getStationName(self, wfID): - return self.getPlotWidget().getPlotDict()[wfID][0] + plot_dict = self.getPlotWidget().getPlotDict() + if wfID in plot_dict.keys(): + return plot_dict[wfID][0] def alterPhase(self): pass @@ -1429,14 +1485,25 @@ class MainWindow(QMainWindow): self.dataPlot.draw() def pickOnStation(self, gui_event): - if not gui_event.button == 1: - return - - wfID = self.getWFID(gui_event) + if self.pg: + if not gui_event.button() == 1: + return + else: + if not gui_event.button == 1: + return + + if self.pg: + ycoord = self.dataPlot.plotWidget.getPlotItem().vb.mapSceneToView(gui_event.scenePos()).y() + else: + ycoord = gui_event.ydata + + wfID = self.getWFID(ycoord) if wfID is None: return station = self.getStationName(wfID) + if not station: + return self.update_status('picking on station {0}'.format(station)) data = self.get_data().getWFData() pickDlg = PickDlg(self, parameter=self._inputs, @@ -1603,12 +1670,23 @@ class MainWindow(QMainWindow): plotID = self.getStationID(station) if plotID is None: return - ax = self.getPlotWidget().axes + if self.pg: + pw = self.getPlotWidget().plotWidget + else: + ax = self.getPlotWidget().axes ylims = np.array([-.5, +.5]) + plotID - phase_col = { - 'P': ('c', 'c--', 'b-', 'bv', 'b^', 'b'), - 'S': ('m', 'm--', 'r-', 'rv', 'r^', 'r') - } + if self.pg: + dashed = QtCore.Qt.DashLine + dotted = QtCore.Qt.DotLine + phase_col = { + 'P': (pg.mkPen('c'), pg.mkPen((0, 255, 255, 100), style=dashed), pg.mkPen('b', style=dashed), pg.mkPen('b', style=dotted)), + 'S': (pg.mkPen('m'), pg.mkPen((255, 0, 255, 100), style=dashed), pg.mkPen('r', style=dashed), pg.mkPen('r', style=dotted)) + } + else: + phase_col = { + 'P': ('c', 'c--', 'b-', 'bv', 'b^', 'b'), + 'S': ('m', 'm--', 'r-', 'rv', 'r^', 'r') + } stat_picks = self.getPicks(type=picktype)[station] @@ -1629,22 +1707,47 @@ class MainWindow(QMainWindow): if not spe and epp and lpp: spe = symmetrize_error(mpp - epp, lpp - mpp) - if picktype == 'manual': - if picks['epp'] and picks['lpp']: - ax.fill_between([epp, lpp], ylims[0], ylims[1], - alpha=.25, color=colors[0], label='EPP, LPP') - if spe: - ax.plot([mpp - spe, mpp - spe], ylims, colors[1], label='{}-SPE'.format(phase)) - ax.plot([mpp + spe, mpp + spe], ylims, colors[1]) - ax.plot([mpp, mpp], ylims, colors[2], label='{}-Pick'.format(phase)) + if self.pg: + if picktype == 'manual': + if picks['epp'] and picks['lpp']: + pw.plot([epp, epp], ylims, + alpha=.25, pen=colors[0], name='EPP') + pw.plot([lpp, lpp], ylims, + alpha=.25, pen=colors[0], name='LPP') + if spe: + spe_l = pg.PlotDataItem([mpp - spe, mpp - spe], ylims, pen=colors[1], name='{}-SPE'.format(phase)) + spe_r = pg.PlotDataItem([mpp + spe, mpp + spe], ylims, pen=colors[1]) + pw.addItem(spe_l) + pw.addItem(spe_r) + try: + fill = pg.FillBetweenItem(spe_l, spe_r, brush=colors[1].brush()) + fb = pw.addItem(fill) + except: + print('Warning: drawPicks: Could not create fill for symmetric pick error.') + pw.plot([mpp, mpp], ylims, pen=colors[2], name='{}-Pick'.format(phase)) + else: + pw.plot([mpp, mpp], ylims, pen=colors[0], name='{}-Pick (NO PICKERROR)'.format(phase)) + elif picktype == 'auto': + pw.plot([mpp, mpp], ylims, pen=colors[3]) else: - ax.plot([mpp, mpp], ylims, colors[6], label='{}-Pick (NO PICKERROR)'.format(phase)) - elif picktype == 'auto': - ax.plot(mpp, ylims[1], colors[3], - mpp, ylims[0], colors[4]) - ax.vlines(mpp, ylims[0], ylims[1], colors[5], linestyles='dotted') + raise TypeError('Unknown picktype {0}'.format(picktype)) else: - raise TypeError('Unknown picktype {0}'.format(picktype)) + if picktype == 'manual': + if picks['epp'] and picks['lpp']: + ax.fill_between([epp, lpp], ylims[0], ylims[1], + alpha=.25, color=colors[0], label='EPP, LPP') + if spe: + ax.plot([mpp - spe, mpp - spe], ylims, colors[1], label='{}-SPE'.format(phase)) + ax.plot([mpp + spe, mpp + spe], ylims, colors[1]) + ax.plot([mpp, mpp], ylims, colors[2], label='{}-Pick'.format(phase)) + else: + ax.plot([mpp, mpp], ylims, colors[6], label='{}-Pick (NO PICKERROR)'.format(phase)) + elif picktype == 'auto': + ax.plot(mpp, ylims[1], colors[3], + mpp, ylims[0], colors[4]) + ax.vlines(mpp, ylims[0], ylims[1], colors[5], linestyles='dotted') + else: + raise TypeError('Unknown picktype {0}'.format(picktype)) def locate_event(self): """ @@ -2157,6 +2260,7 @@ class MainWindow(QMainWindow): self._props = PropertiesDlg(self, infile=self.infile) if self._props.exec_(): + self.init_wfWidget() return def helpHelp(self): diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index 8fc5044c..9515eb1e 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -0af79-dirty +8e83-dirty diff --git a/pylot/core/util/widgets.py b/pylot/core/util/widgets.py index f28d2679..60257c90 100644 --- a/pylot/core/util/widgets.py +++ b/pylot/core/util/widgets.py @@ -12,6 +12,11 @@ import copy import datetime import numpy as np +try: + import pyqtgraph as pg +except: + pg = None + from matplotlib.figure import Figure from pylot.core.util.utils import find_horizontals @@ -42,6 +47,12 @@ from autoPyLoT import autoPyLoT from pylot.core.util.thread import Thread import icons_rc +if pg: + pg.setConfigOption('background', 'w') + pg.setConfigOption('foreground', 'k') + pg.setConfigOptions(antialias=True) + #pg.setConfigOption('leftButtonPan', False) + def getDataType(parent): type = QInputDialog().getItem(parent, "Select phases type", "Type:", ["manual", "automatic"]) @@ -393,6 +404,172 @@ class PlotWidget(FigureCanvas): return self._parent +class WaveformWidgetPG(QtGui.QWidget): + def __init__(self, parent=None, xlabel='x', ylabel='y', title='Title'): + QtGui.QWidget.__init__(self, parent)#, 1) + self.setParent(parent) + self._parent = parent + # attribute plotdict is a dictionary connecting position and a name + self.plotdict = dict() + # create plot + self.main_layout = QtGui.QVBoxLayout() + self.label = QtGui.QLabel() + self.setLayout(self.main_layout) + self.plotWidget = pg.PlotWidget(title=title, autoDownsample=True) + self.main_layout.addWidget(self.plotWidget) + self.main_layout.addWidget(self.label) + self.plotWidget.showGrid(x=False, y=True, alpha=0.2) + self.plotWidget.hideAxis('bottom') + self.plotWidget.hideAxis('left') + self.reinitMoveProxy() + self._proxy = pg.SignalProxy(self.plotWidget.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved) + + def reinitMoveProxy(self): + self.vLine = pg.InfiniteLine(angle=90, movable=False) + self.hLine = pg.InfiniteLine(angle=0, movable=False) + self.plotWidget.addItem(self.vLine, ignoreBounds=True) + self.plotWidget.addItem(self.hLine, ignoreBounds=True) + + def mouseMoved(self, evt): + pos = evt[0] ## using signal proxy turns original arguments into a tuple + if self.plotWidget.sceneBoundingRect().contains(pos): + mousePoint = self.plotWidget.getPlotItem().vb.mapSceneToView(pos) + x, y, = (mousePoint.x(), mousePoint.y()) + #if x > 0:# and index < len(data1): + wfID = self._parent.getWFID(y) + station = self._parent.getStationName(wfID) + if self._parent.get_current_event(): + self.label.setText("station = {}, t = {} [s]".format(station, x)) + self.vLine.setPos(mousePoint.x()) + self.hLine.setPos(mousePoint.y()) + + def getPlotDict(self): + return self.plotdict + + def setPlotDict(self, key, value): + self.plotdict[key] = value + + def clearPlotDict(self): + self.plotdict = dict() + + def getParent(self): + return self._parent + + def setParent(self, parent): + self._parent = parent + + def plotWFData(self, wfdata, title=None, zoomx=None, zoomy=None, + noiselevel=None, scaleddata=False, mapping=True, + component='*', nth_sample=1, iniPick=None): + self.title = title + self.clearPlotDict() + wfstart, wfend = full_range(wfdata) + nmax = 0 + + settings = QSettings() + compclass = settings.value('compclass') + if not compclass: + print('Warning: No settings for channel components found. Using default') + compclass = SetChannelComponents() + + if not component == '*': + alter_comp = compclass.getCompPosition(component) + #alter_comp = str(alter_comp[0]) + + wfdata = wfdata.select(component=component) + wfdata += wfdata.select(component=alter_comp) + + # list containing tuples of network, station, channel (for sorting) + nsc = [] + for trace in wfdata: + nsc.append((trace.stats.network, trace.stats.station, trace.stats.channel)) + nsc.sort() + nsc.reverse() + plots = [] + + try: + self.plotWidget.getPlotItem().vb.setLimits(xMin=float(0), + xMax=float(wfend-wfstart), + yMin=-0.5, + yMax=len(nsc)+0.5) + except: + print('Warning: Could not set zoom limits') + + for n, (network, station, channel) in enumerate(nsc): + st = wfdata.select(network=network, station=station, channel=channel) + trace = st[0] + if mapping: + comp = channel[-1] + n = compclass.getPlotPosition(str(comp)) + #n = n[0] + if n > nmax: + nmax = n + msg = 'plotting %s channel of station %s' % (channel, station) + print(msg) + stime = trace.stats.starttime - wfstart + time_ax = prepTimeAxis(stime, trace) + if time_ax is not None: + if not scaleddata: + trace.detrend('constant') + trace.normalize(np.max(np.abs(trace.data)) * 2) + times = [time for index, time in enumerate(time_ax) if not index%nth_sample] + data = [datum + n for index, datum in enumerate(trace.data) if not index%nth_sample] + plots.append((times, data)) + self.setPlotDict(n, (station, channel, network)) + self.xlabel = 'seconds since {0}'.format(wfstart) + self.ylabel = '' + self.setXLims([0, wfend - wfstart]) + self.setYLims([-0.5, nmax + 0.5]) + return plots + + # def getAxes(self): + # return self.axes + + # def getXLims(self): + # return self.getAxes().get_xlim() + + # def getYLims(self): + # return self.getAxes().get_ylim() + + def setXLims(self, lims): + vb = self.plotWidget.getPlotItem().getViewBox() + vb.setXRange(float(lims[0]), float(lims[1]), padding=0) + + def setYLims(self, lims): + vb = self.plotWidget.getPlotItem().getViewBox() + vb.setYRange(float(lims[0]), float(lims[1]), padding=0) + + def setYTickLabels(self, pos, labels): + ticks = zip(pos, labels) + minorTicks = [(0, 0) for item in labels] + # leftAx.tickLength = 5 + # leftAx.orientation = 'right' + self.getAxItem('left').setTicks([ticks, minorTicks]) + + def updateXLabel(self, text): + self.getAxItem('bottom').setLabel(text) + self.draw() + + def updateYLabel(self, text): + self.getAxItem('left').setLabel(text) + self.draw() + + def getAxItem(self, position): + return self.plotWidget.getPlotItem().axes[position]['item'] + + def updateTitle(self, text): + self.plotWidget.getPlotItem().setTitle(text) + self.draw() + + def updateWidget(self):#, xlabel, ylabel, title): + self.updateXLabel(self.xlabel) + self.updateYLabel(self.ylabel) + self.updateTitle(self.title) + + def draw(self): + pass + + class WaveformWidget(FigureCanvas): def __init__(self, parent=None, xlabel='x', ylabel='y', title='Title'): @@ -2196,6 +2373,7 @@ class PropTab(QWidget): def resetValues(self, infile=None): return None + class InputsTab(PropTab): def __init__(self, parent, infile=None): @@ -2307,6 +2485,7 @@ class GraphicsTab(PropTab): def __init__(self, parent=None): super(GraphicsTab, self).__init__(parent) self.init_layout() + self.add_pg_cb() self.add_nth_sample() self.setLayout(self.main_layout) @@ -2321,15 +2500,28 @@ class GraphicsTab(PropTab): self.spinbox_nth_sample = QtGui.QSpinBox() label = QLabel('nth sample') + label.setToolTip('Plot every nth sample (to speed up plotting)') self.spinbox_nth_sample.setMinimum(1) self.spinbox_nth_sample.setMaximum(10e3) self.spinbox_nth_sample.setValue(int(nth_sample)) - label.setToolTip('Plot every nth sample (to speed up plotting)') - self.main_layout.addWidget(label, 0, 0) - self.main_layout.addWidget(self.spinbox_nth_sample, 0, 1) + self.main_layout.addWidget(label, 1, 0) + self.main_layout.addWidget(self.spinbox_nth_sample, 1, 1) + def add_pg_cb(self): + text = {True: 'Use pyqtgraphic library for plotting', + False: 'Cannot use library: pyqtgraphic not found on system'} + label = QLabel('PyQt graphic') + label.setToolTip(text[bool(pg)]) + label.setEnabled(bool(pg)) + self.checkbox_pg = QtGui.QCheckBox() + self.checkbox_pg.setEnabled(bool(pg)) + self.checkbox_pg.setChecked(bool(pg)) + self.main_layout.addWidget(label, 0, 0) + self.main_layout.addWidget(self.checkbox_pg, 0, 1) + def getValues(self): - values = {'nth_sample': self.spinbox_nth_sample.value()} + values = {'nth_sample': self.spinbox_nth_sample.value(), + 'pyqtgraphic': self.checkbox_pg.isChecked()} return values