From 62876dd01d58eae94b892e3b2fa1cf1a3abc547c Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Tue, 18 Apr 2017 16:24:26 +0200 Subject: [PATCH 01/11] added first project structure and event lists (testing needed) --- QtPyLoT.py | 269 +++++++++++++++++++++++++++++++++----- pylot/RELEASE-VERSION | 2 +- pylot/core/util/thread.py | 39 +++++- 3 files changed, 275 insertions(+), 35 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index f75b322b..4dc63b57 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -33,9 +33,10 @@ matplotlib.rcParams['backend.qt4'] = 'PySide' from PySide.QtCore import QCoreApplication, QSettings, Signal, QFile, \ QFileInfo, Qt, QSize from PySide.QtGui import QMainWindow, QInputDialog, QIcon, QFileDialog, \ - QWidget, QHBoxLayout, QStyle, QKeySequence, QLabel, QFrame, QAction, \ + QWidget, QHBoxLayout, QVBoxLayout, QStyle, QKeySequence, QLabel, QFrame, QAction, \ QDialog, QErrorMessage, QApplication, QPixmap, QMessageBox, QSplashScreen, \ - QActionGroup, QListWidget, QDockWidget, QLineEdit + QActionGroup, QListWidget, QDockWidget, QLineEdit, QListView, QAbstractItemView, \ + QTreeView, QComboBox import numpy as np from obspy import UTCDateTime @@ -60,7 +61,7 @@ from pylot.core.util.widgets import FilterOptionsDialog, NewEventDlg, \ getDataType, ComparisonDialog from pylot.core.util.map_projection import map_projection from pylot.core.util.structure import DATASTRUCTURE -from pylot.core.util.thread import AutoPickThread +from pylot.core.util.thread import AutoPickThread, Thread from pylot.core.util.version import get_git_version as _getVersionString import icons_rc @@ -121,6 +122,7 @@ class MainWindow(QMainWindow): self.autopicks = {} self.loc = False self._metadata = None + self.project = None self.array_map = None @@ -152,8 +154,19 @@ class MainWindow(QMainWindow): _widget = QWidget() _widget.setCursor(Qt.CrossCursor) - _layout = QHBoxLayout() + _layout = QVBoxLayout() + # add event combo box + self.eventBox = QComboBox() + self.eventBox.setMaxVisibleItems(30) + self.eventBox.setEnabled(False) + _event_layout = QHBoxLayout() + _event_layout.addWidget(QLabel('Event: ')) + _event_layout.addWidget(self.eventBox) + _event_layout.setStretch(1,1) #set stretch of item 1 to 1 + _layout.addLayout(_event_layout) + self.eventBox.activated.connect(self.loadWaveformDataThread) + plottitle = "Overview: {0} components ".format(self.getComponent()) # create central matplotlib figure canvas widget @@ -167,9 +180,11 @@ class MainWindow(QMainWindow): quitIcon = self.style().standardIcon(QStyle.SP_MediaStop) saveIcon = self.style().standardIcon(QStyle.SP_DriveHDIcon) + openIcon = self.style().standardIcon(QStyle.SP_DirOpenIcon) helpIcon = self.style().standardIcon(QStyle.SP_DialogHelpButton) newIcon = self.style().standardIcon(QStyle.SP_FileIcon) - + newFolderIcon = self.style().standardIcon(QStyle.SP_FileDialogNewFolder) + # create resource icons locactionicon = QIcon() locactionicon.addPixmap(QPixmap(':/icons/locactionicon.png')) @@ -199,10 +214,24 @@ class MainWindow(QMainWindow): locate_icon.addPixmap(QPixmap(':/icons/locate_button.png')) compare_icon = QIcon() compare_icon.addPixmap(QPixmap(':/icons/compare_button.png')) - newEventAction = self.createAction(self, "&New event ...", - self.createNewEvent, + self.newProjectAction = self.createAction(self, "&New project ...", + self.createNewProject, QKeySequence.New, newIcon, - "Create a new event.") + "Create a new Project.") + self.openProjectAction = self.createAction(self, "Load project ...", + self.loadProject, + QKeySequence.Open, + openIcon, + "Load project file") + self.saveProjectAction = self.createAction(self, "Save project ...", + self.saveProject, + QKeySequence.Save, + saveIcon, + "Save project file") + # newEventAction = self.createAction(self, "&New event ...", + # self.createNewEvent, + # QKeySequence.New, newIcon, + # "Create a new event.") self.openmanualpicksaction = self.createAction(self, "Load &picks ...", self.load_data, QKeySequence.Open, @@ -240,11 +269,10 @@ class MainWindow(QMainWindow): saveIcon, "Save actual event data.") self.saveEventAction.setEnabled(False) - openWFDataAction = self.createAction(self, "Open &waveforms ...", - self.loadWaveformData, - "Ctrl+W", QIcon(":/wfIcon.png"), - "Open waveform data (event will " - "be closed)") + self.addEventDataAction = self.createAction(self, "Add &events ...", + self.add_events, + "Ctrl+W", newFolderIcon, + "Add event data") prefsEventAction = self.createAction(self, "Preferences", self.PyLoTprefs, QKeySequence.Preferences, @@ -290,8 +318,9 @@ class MainWindow(QMainWindow): homepage (internet connection available), or shipped documentation files.""") self.fileMenu = self.menuBar().addMenu('&File') - self.fileMenuActions = (newEventAction, self.openmanualpicksaction, - self.saveEventAction, openWFDataAction, None, + self.fileMenuActions = (self.newProjectAction, self.addEventDataAction, + self.openProjectAction, self.saveProjectAction, + self.openmanualpicksaction, self.saveEventAction, None, prefsEventAction, quitAction) self.fileMenu.aboutToShow.connect(self.updateFileMenu) self.updateFileMenu() @@ -307,7 +336,9 @@ class MainWindow(QMainWindow): self.addActions(self.helpMenu, helpActions) fileToolBar = self.addToolBar("FileTools") - fileToolActions = (newEventAction, self.openmanualpicksaction, + fileToolActions = (self.newProjectAction, self.addEventDataAction, + self.openProjectAction, self.saveProjectAction, + self.openmanualpicksaction, self.openautopicksaction, loadlocationaction, self.loadpilotevent, self.saveEventAction) fileToolBar.setObjectName("FileTools") @@ -523,6 +554,50 @@ class MainWindow(QMainWindow): else: return + def getWFFnames_from_eventlist(self): + if self.dataStructure: + searchPath = self.dataStructure.expandDataPath() + directory = self.eventBox.currentText() + self.fnames = [os.path.join(directory, f) for f in os.listdir(directory)] + else: + raise DatastructureError('not specified') + if not self.fnames: + return None + return self.fnames + + def add_events(self): + if not self.project: + self.project = Project() + ed = getExistingDirectories(self, 'Select event directories...') + if ed.exec_(): + eventlist = ed.selectedFiles() + # select only folders that start with 'e', containin two dots and have length 12 + eventlist = [item for item in eventlist if item.split('/')[-1].startswith('e') + and len(item.split('/')[-1].split('.')) == 3 + and len(item.split('/')[-1]) == 12] + else: + return + if not self.project: + print('No project found.') + return + self.project.add_eventlist(eventlist) + self.init_events() + + def init_events(self, new=False): + nitems = self.eventBox.count() + self.eventBox.clear() + if len(self.project.eventlist) == 0: + print('No events to init.') + return + self.eventBox.setEnabled(True) + for event in self.project.eventlist: + self.eventBox.addItem(event) + if new: + self.eventBox.setCurrentIndex(0) + else: + self.eventBox.setCurrentIndex(nitems) + self.loadWaveformDataThread() + def filename_from_action(self, action): if action.data() is None: filt = "Supported file formats" \ @@ -687,15 +762,23 @@ class MainWindow(QMainWindow): return self.saveData() return True + def loadWaveformDataThread(self): + wfd_thread = Thread(self, self.loadWaveformData, 'Reading data input...') + wfd_thread.finished.connect(self.plotWaveformDataThread) + wfd_thread.start() + def loadWaveformData(self): - if self.fnames and self.okToContinue(): - self.setDirty(True) - ans = self.data.setWFData(self.fnames) - elif self.fnames is None and self.okToContinue(): - ans = self.data.setWFData(self.getWFFnames()) - else: - ans = False + # if self.fnames and self.okToContinue(): + # self.setDirty(True) + # ans = self.data.setWFData(self.fnames) + # elif self.fnames is None and self.okToContinue(): + # ans = self.data.setWFData(self.getWFFnames()) + # else: + # ans = False + self.data.setWFData(self.getWFFnames_from_eventlist()) self._stime = full_range(self.get_data().getWFData())[0] + + def finishWaveformDataPlot(self): self.auto_pick.setEnabled(True) self.z_action.setEnabled(True) self.e_action.setEnabled(True) @@ -704,12 +787,13 @@ class MainWindow(QMainWindow): self.openautopicksaction.setEnabled(True) self.loadpilotevent.setEnabled(True) self.saveEventAction.setEnabled(True) - if ans: - self.plotWaveformData() - return ans - else: - return ans + self.draw() + def plotWaveformDataThread(self): + wfp_thread = Thread(self, self.plotWaveformData, 'Plotting waveform data...') + wfp_thread.finished.connect(self.finishWaveformDataPlot) + wfp_thread.start() + def plotWaveformData(self): zne_text = {'Z': 'vertical', 'N': 'north-south', 'E': 'east-west'} comp = self.getComponent() @@ -718,7 +802,6 @@ class MainWindow(QMainWindow): wfst = self.get_data().getWFData().select(component=comp) wfst += self.get_data().getWFData().select(component=alter_comp) self.getPlotWidget().plotWFData(wfdata=wfst, title=title, mapping=False) - self.draw() plotDict = self.getPlotWidget().getPlotDict() pos = plotDict.keys() labels = [plotDict[n][0] for n in pos] @@ -726,19 +809,19 @@ class MainWindow(QMainWindow): def plotZ(self): self.setComponent('Z') - self.plotWaveformData() + self.plotWaveformDataThread() self.drawPicks() self.draw() def plotN(self): self.setComponent('N') - self.plotWaveformData() + self.plotWaveformDataThread() self.drawPicks() self.draw() def plotE(self): self.setComponent('E') - self.plotWaveformData() + self.plotWaveformDataThread() self.drawPicks() self.draw() @@ -1142,7 +1225,7 @@ class MainWindow(QMainWindow): self.setWindowTitle( "PyLoT - processing event %s[*]" % self.get_data().getID()) elif self.get_data().isNew(): - self.setWindowTitle("PyLoT - New event [*]") + self.setWindowTitle("PyLoT - New project [*]") else: self.setWindowTitle( "PyLoT - seismic processing the python way[*]") @@ -1164,6 +1247,50 @@ class MainWindow(QMainWindow): self.data = Data(self, evtdata=event) self.setDirty(True) + def createNewProject(self, exists=False): + if self.okToContinue(): + dlg = QFileDialog() + fnm = dlg.getSaveFileName(self, 'Create a new project file...', filter='Pylot project (*.plp)') + filename = fnm[0] + if not filename.split('.')[-1] == 'plp': + filename = fnm[0] + '.plp' + if not exists: + self.project = Project() + self.project.save(filename) + + def loadProject(self): + if self.project: + if self.project.dirty: + qmb = QMessageBox(icon=QMessageBox.Question, text='Save changes in current project?') + qmb.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel) + qmb.setDefaultButton(QMessageBox.Yes) + if qmb.exec_() == 16384: + self.saveProject() + elif qmb.exec_() == 65536: + pass + elif qmb.exec_() == 4194304: + return + dlg = QFileDialog() + fnm = dlg.getOpenFileName(self, 'Open project file...', filter='Pylot project (*.plp)') + if fnm[0]: + self.project = Project.load(fnm[0]) + self.init_events(new=True) + + def saveProject(self): + if self.project: + self.project.save() + if not self.project.dirty: + qmb = QMessageBox(icon=QMessageBox.Information, text='Saved back project to file:\n{}'.format(self.project.location)) + qmb.exec_() + return + else: + # if still dirty because saving failed + qmb = QMessageBox(icon=QMessageBox.Warning, text='Could not save back to original file.\n' + 'Choose new file') + qmb.setStandardButtons(QMessageBox.Ok) + qmb.exec_() + self.createNewProject(exists=True) + def draw(self): self.getPlotWidget().draw() @@ -1189,6 +1316,82 @@ class MainWindow(QMainWindow): form.show() +class Project(object): + ''' + Pickable class containing information of a QtPyLoT project, like event lists and file locations. + ''' + def __init__(self): + self.eventlist = [] + self.location = None + self.dirty = False + + def add_eventlist(self, eventlist): + if len(eventlist) == 0: + return + for item in eventlist: + if not item in self.eventlist: + self.eventlist.append(item) + self.setDirty() + + def setDirty(self): + self.dirty = True + + def setClean(self): + self.dirty = False + + def save(self, filename=None): + ''' + Save PyLoT Project to a file. + Can be loaded by using project.load(filename). + ''' + try: + import cPickle + except ImportError: + import _pickle as cPickle + + if filename: + self.location = filename + else: + filename = self.location + + try: + outfile = open(filename, 'wb') + cPickle.dump(self, outfile, -1) + self.setClean() + except Exception as e: + print('Could not pickle PyLoT project. Reason: {}'.format(e)) + self.setDirty() + + @staticmethod + def load(filename): + try: + import cPickle + except ImportError: + import _pickle as cPickle + infile = open(filename, 'rb') + project = cPickle.load(infile) + print('Loaded %s' % filename) + return project + + +class event(object): + ''' + Pickable class containing information on a single event. + ''' + def __init__(self): + self.eventID = None + + +class getExistingDirectories(QFileDialog): + def __init__(self, *args): + super(getExistingDirectories, self).__init__(*args) + self.setOption(self.DontUseNativeDialog, True) + self.setFileMode(self.Directory) + self.setOption(self.ShowDirsOnly, True) + self.findChildren(QListView)[0].setSelectionMode(QAbstractItemView.ExtendedSelection) + self.findChildren(QTreeView)[0].setSelectionMode(QAbstractItemView.ExtendedSelection) + + def create_window(): app_created = False app = QCoreApplication.instance() diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index 0fec275e..70ad8557 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -de38-dirty +3092-dirty diff --git a/pylot/core/util/thread.py b/pylot/core/util/thread.py index 038aef8d..8bd035ba 100644 --- a/pylot/core/util/thread.py +++ b/pylot/core/util/thread.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import sys -from PySide.QtCore import QThread, Signal +from PySide.QtCore import QThread, Signal, Qt +from PySide.QtGui import QDialog, QProgressBar, QLabel, QVBoxLayout class AutoPickThread(QThread): @@ -35,3 +36,39 @@ class AutoPickThread(QThread): def flush(self): pass + + +class Thread(QThread): + def __init__(self, parent, func, progressText = None): + QThread.__init__(self, parent) + self.func = func + self.progressText = progressText + self.pbdlg = None + self.finished.connect(self.hideProgressbar) + self.showProgressbar() + + def run(self): + self.func() + + def __del__(self): + self.wait() + + def showProgressbar(self): + if self.progressText: + self.pbdlg = QDialog(self.parent()) + self.pbdlg.setModal(True) + vl = QVBoxLayout() + pb = QProgressBar() + pb.setRange(0, 0) + vl.addWidget(pb) + vl.addWidget(QLabel(self.progressText)) + self.pbdlg.setLayout(vl) + self.pbdlg.show() + self.pbdlg.setWindowFlags(Qt.FramelessWindowHint) + self.pbdlg.show() + + def hideProgressbar(self): + if self.pbdlg: + self.pbdlg.hide() + + From fd70ef2251bc9f0e4ddde99710146affc091372c Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Tue, 18 Apr 2017 17:17:46 +0200 Subject: [PATCH 02/11] adding some tabs for map_projection, WIP --- QtPyLoT.py | 49 ++++++++++++++++++++----------- pylot/RELEASE-VERSION | 2 +- pylot/core/util/map_projection.py | 33 ++++++++++++++------- 3 files changed, 55 insertions(+), 29 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index 4dc63b57..53a12ea2 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -36,7 +36,7 @@ from PySide.QtGui import QMainWindow, QInputDialog, QIcon, QFileDialog, \ QWidget, QHBoxLayout, QVBoxLayout, QStyle, QKeySequence, QLabel, QFrame, QAction, \ QDialog, QErrorMessage, QApplication, QPixmap, QMessageBox, QSplashScreen, \ QActionGroup, QListWidget, QDockWidget, QLineEdit, QListView, QAbstractItemView, \ - QTreeView, QComboBox + QTreeView, QComboBox, QTabWidget import numpy as np from obspy import UTCDateTime @@ -87,6 +87,10 @@ class MainWindow(QMainWindow): else: self.infile = infile + self.project = None + self.array_map = None + self._metadata = None + # UI has to be set up before(!) children widgets are about to show up self.createAction = createAction # read settings @@ -121,10 +125,6 @@ class MainWindow(QMainWindow): self.picks = {} self.autopicks = {} self.loc = False - self._metadata = None - self.project = None - - self.array_map = None # initialize event data if self.recentfiles: @@ -154,30 +154,39 @@ class MainWindow(QMainWindow): _widget = QWidget() _widget.setCursor(Qt.CrossCursor) - _layout = QVBoxLayout() + self._main_layout = QVBoxLayout() + # add event combo box self.eventBox = QComboBox() self.eventBox.setMaxVisibleItems(30) self.eventBox.setEnabled(False) - _event_layout = QHBoxLayout() - _event_layout.addWidget(QLabel('Event: ')) - _event_layout.addWidget(self.eventBox) - _event_layout.setStretch(1,1) #set stretch of item 1 to 1 - _layout.addLayout(_event_layout) + self._event_layout = QHBoxLayout() + self._event_layout.addWidget(QLabel('Event: ')) + self._event_layout.addWidget(self.eventBox) + self._event_layout.setStretch(1,1) #set stretch of item 1 to 1 + self._main_layout.addLayout(self._event_layout) self.eventBox.activated.connect(self.loadWaveformDataThread) - plottitle = "Overview: {0} components ".format(self.getComponent()) + # add tabs + self.tabs = QTabWidget() + self._main_layout.addWidget(self.tabs) + + # init Map + #self.init_array_map() # 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.mpl_connect('button_press_event', self.pickOnStation) self.DataPlot.mpl_connect('axes_enter_event', lambda event: self.tutor_user()) - _layout.addWidget(self.DataPlot) + self.tabs.addTab(self.DataPlot, 'Waveform Plot') + #self.tabs.addTab(self.array_map, 'Array Map') + quitIcon = self.style().standardIcon(QStyle.SP_MediaStop) saveIcon = self.style().standardIcon(QStyle.SP_DriveHDIcon) openIcon = self.style().standardIcon(QStyle.SP_DirOpenIcon) @@ -415,7 +424,7 @@ class MainWindow(QMainWindow): status.addPermanentWidget(self.eventLabel) status.showMessage("Ready", 500) - _widget.setLayout(_layout) + _widget.setLayout(self._main_layout) _widget.showFullScreen() self.setCentralWidget(_widget) @@ -1121,14 +1130,20 @@ class MainWindow(QMainWindow): self.get_data().applyEVTData(lt.read_location(locpath), type='event') self.get_data().applyEVTData(self.calc_magnitude(), type='event') - def show_array_map(self): + def init_array_map(self): if not self.array_map: self.get_metadata() if not self.metadata: return self.array_map = map_projection(self) - else: - self.array_map.show() + + def show_array_map(self, container=None): + if not self.array_map: + print('No array map found. Init array map first!') + return + if self.container: + container.addWidget(self.array_map) + self.array_map.show() def get_metadata(self): def set_inv(settings): diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index 70ad8557..c95ddf95 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -3092-dirty +6287-dirty diff --git a/pylot/core/util/map_projection.py b/pylot/core/util/map_projection.py index c235d2d1..ebdbc967 100644 --- a/pylot/core/util/map_projection.py +++ b/pylot/core/util/map_projection.py @@ -12,10 +12,16 @@ from pylot.core.util.widgets import PickDlg plt.interactive(False) class map_projection(QtGui.QWidget): - def __init__(self, mainwindow): + def __init__(self, mainwindow, figure=None, picked=False): + ''' + :param: picked, can be False, auto, manual + :value: str + ''' QtGui.QWidget.__init__(self) self.pyl_mainwindow = mainwindow self.parser = mainwindow.metadata[1] + self.picked = picked + self.figure = figure self.init_graphics() self.init_stations() self.init_lat_lon_dimensions() @@ -24,7 +30,7 @@ class map_projection(QtGui.QWidget): self.init_x_y_dimensions() self.connectSignals() self.draw_everything() - self.show() + #self.show() def onpick(self, event): ind = event.ind @@ -71,8 +77,11 @@ class map_projection(QtGui.QWidget): self.combobox.insertItem(1, 'S') self.top_row.addWidget(QtGui.QLabel('Select a phase: ')) self.top_row.addWidget(self.combobox) - - fig = plt.figure() + + if not self.figure: + fig = plt.figure() + else: + fig = self.figure self.main_ax = fig.add_subplot(111) self.canvas = fig.canvas self.main_box.addWidget(self.canvas) @@ -221,15 +230,17 @@ class map_projection(QtGui.QWidget): self.draw_everything() def draw_everything(self): - self.init_picks() - self.init_picks_active() - self.init_stations_active() - self.init_picksgrid() - self.draw_contour_filled() + if self.picked: + self.init_picks() + self.init_picks_active() + self.init_stations_active() + self.init_picksgrid() + self.draw_contour_filled() self.scatter_all_stations() - self.scatter_picked_stations() + if self.picked: + self.scatter_picked_stations() + self.cbar = self.add_cbar(label='Time relative to first onset [s]') self.annotate_ax() - self.cbar = self.add_cbar(label='Time relative to first onset [s]') self.canvas.draw() def remove_drawings(self): From 80577dcfc748f8be3d1c590316e219b73b054a6a Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Wed, 19 Apr 2017 15:31:08 +0200 Subject: [PATCH 03/11] added map_projection plot --- QtPyLoT.py | 166 +++++++++++++++++++++++------- pylot/RELEASE-VERSION | 2 +- pylot/core/util/map_projection.py | 15 +-- pylot/core/util/thread.py | 11 +- pylot/core/util/widgets.py | 2 +- 5 files changed, 144 insertions(+), 52 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index 53a12ea2..cfb2a50f 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -30,13 +30,14 @@ import matplotlib matplotlib.use('Qt4Agg') matplotlib.rcParams['backend.qt4'] = 'PySide' +from PySide import QtGui, QtCore from PySide.QtCore import QCoreApplication, QSettings, Signal, QFile, \ QFileInfo, Qt, QSize from PySide.QtGui import QMainWindow, QInputDialog, QIcon, QFileDialog, \ QWidget, QHBoxLayout, QVBoxLayout, QStyle, QKeySequence, QLabel, QFrame, QAction, \ QDialog, QErrorMessage, QApplication, QPixmap, QMessageBox, QSplashScreen, \ QActionGroup, QListWidget, QDockWidget, QLineEdit, QListView, QAbstractItemView, \ - QTreeView, QComboBox, QTabWidget + QTreeView, QComboBox, QTabWidget, QPushButton, QGridLayout import numpy as np from obspy import UTCDateTime @@ -91,12 +92,27 @@ class MainWindow(QMainWindow): self.array_map = None self._metadata = None + self.poS_id = None + self.ae_id = None + # UI has to be set up before(!) children widgets are about to show up self.createAction = createAction # read settings settings = QSettings() self.recentfiles = settings.value("data/recentEvents", []) self.dispComponent = str(settings.value("plotting/dispComponent", "Z")) + + # initialize event data + if self.recentfiles: + lastEvent = self.getLastEvent() + self.data = Data(self, lastEvent) + else: + self.data = Data(self) + self.autodata = Data(self) + + self.dirty = False + + # setup UI self.setupUi() if settings.value("user/FullName", None) is None: @@ -126,16 +142,6 @@ class MainWindow(QMainWindow): self.autopicks = {} self.loc = False - # initialize event data - if self.recentfiles: - lastEvent = self.getLastEvent() - self.data = Data(self, lastEvent) - else: - self.data = Data(self) - self.autodata = Data(self) - - self.dirty = False - def setupUi(self): try: @@ -153,10 +159,8 @@ class MainWindow(QMainWindow): xlab = self.startTime.strftime('seconds since %Y/%m/%d %H:%M:%S (%Z)') _widget = QWidget() - _widget.setCursor(Qt.CrossCursor) self._main_layout = QVBoxLayout() - # add event combo box self.eventBox = QComboBox() self.eventBox.setMaxVisibleItems(30) @@ -166,25 +170,20 @@ class MainWindow(QMainWindow): self._event_layout.addWidget(self.eventBox) self._event_layout.setStretch(1,1) #set stretch of item 1 to 1 self._main_layout.addLayout(self._event_layout) - self.eventBox.activated.connect(self.loadWaveformDataThread) + self.eventBox.activated.connect(self.refreshTabs) # add tabs self.tabs = QTabWidget() self._main_layout.addWidget(self.tabs) - - # init Map - #self.init_array_map() + 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, + self.dataPlot = WaveformWidget(parent=self, xlabel=xlab, ylabel=None, title=plottitle) - self.DataPlot.mpl_connect('button_press_event', - self.pickOnStation) - self.DataPlot.mpl_connect('axes_enter_event', - lambda event: self.tutor_user()) - self.tabs.addTab(self.DataPlot, 'Waveform Plot') - #self.tabs.addTab(self.array_map, 'Array Map') + self.dataPlot.setCursor(Qt.CrossCursor) + self.tabs.addTab(self.dataPlot, 'Waveform Plot') + self.init_array_tab() quitIcon = self.style().standardIcon(QStyle.SP_MediaStop) @@ -597,16 +596,28 @@ class MainWindow(QMainWindow): self.eventBox.clear() if len(self.project.eventlist) == 0: print('No events to init.') + self.clearWaveformDataPlot() return self.eventBox.setEnabled(True) - for event in self.project.eventlist: - self.eventBox.addItem(event) + self.fill_eventbox(self.project.eventlist) if new: self.eventBox.setCurrentIndex(0) else: self.eventBox.setCurrentIndex(nitems) - self.loadWaveformDataThread() + self.refreshTabs() + def fill_eventbox(self, eventlist): + model = self.eventBox.model() + for event in self.project.eventlist: + item = QtGui.QStandardItem(str(event)) + # if ref: set different color e.g. + #item.setBackground(QtGui.QColor('teal')) + item.setForeground(QtGui.QColor('black')) + font = item.font() + font.setPointSize(10) + item.setFont(font) + model.appendRow(item) + def filename_from_action(self, action): if action.data() is None: filt = "Supported file formats" \ @@ -732,7 +743,7 @@ class MainWindow(QMainWindow): compare_dlg.exec_() def getPlotWidget(self): - return self.DataPlot + return self.dataPlot @staticmethod def getWFID(gui_event): @@ -771,8 +782,16 @@ class MainWindow(QMainWindow): return self.saveData() return True + def refreshTabs(self): + if self.tabs.currentIndex() == 0: + if hasattr(self.project, 'eventlist'): + if len(self.project.eventlist) > 0: + self.loadWaveformDataThread() + if self.tabs.currentIndex() == 1: + self.refresh_array_map() + def loadWaveformDataThread(self): - wfd_thread = Thread(self, self.loadWaveformData, 'Reading data input...') + wfd_thread = Thread(self, self.loadWaveformData, progressText='Reading data input...') wfd_thread.finished.connect(self.plotWaveformDataThread) wfd_thread.start() @@ -787,7 +806,24 @@ class MainWindow(QMainWindow): self.data.setWFData(self.getWFFnames_from_eventlist()) self._stime = full_range(self.get_data().getWFData())[0] + def connectWFplotEvents(self): + if not self.poS_id: + self.poS_id = self.dataPlot.mpl_connect('button_press_event', + self.pickOnStation) + if not self.ae_id: + self.ae_id = self.dataPlot.mpl_connect('axes_enter_event', + lambda event: self.tutor_user()) + + def disconnectWFplotEvents(self): + if self.poS_id: + self.dataPlot.mpl_disconnect(self.poS_id) + if self.ae_id: + self.dataPlot.mpl_disconnect(self.ae_id) + self.poS_id = None + self.ae_id = None + def finishWaveformDataPlot(self): + self.connectWFplotEvents() self.auto_pick.setEnabled(True) self.z_action.setEnabled(True) self.e_action.setEnabled(True) @@ -798,8 +834,21 @@ class MainWindow(QMainWindow): self.saveEventAction.setEnabled(True) self.draw() + def clearWaveformDataPlot(self): + self.disconnectWFplotEvents() + self.dataPlot.getAxes().cla() + self.auto_pick.setEnabled(False) + self.z_action.setEnabled(False) + self.e_action.setEnabled(False) + self.n_action.setEnabled(False) + self.openmanualpicksaction.setEnabled(False) + self.openautopicksaction.setEnabled(False) + self.loadpilotevent.setEnabled(False) + self.saveEventAction.setEnabled(False) + self.draw() + def plotWaveformDataThread(self): - wfp_thread = Thread(self, self.plotWaveformData, 'Plotting waveform data...') + wfp_thread = Thread(self, self.plotWaveformData, progressText='Plotting waveform data...') wfp_thread.finished.connect(self.finishWaveformDataPlot) wfp_thread.start() @@ -1130,20 +1179,48 @@ class MainWindow(QMainWindow): self.get_data().applyEVTData(lt.read_location(locpath), type='event') self.get_data().applyEVTData(self.calc_magnitude(), type='event') + def init_array_tab(self): + widget = QWidget(self) + grid_layout = QGridLayout() + grid_layout.setColumnStretch(0, 1) + grid_layout.setColumnStretch(2, 1) + grid_layout.setRowStretch(0, 1) + grid_layout.setRowStretch(3, 1) + + label = QLabel('No inventory set...') + new_inv_button = QPushButton('Set &inventory file') + new_inv_button.clicked.connect(self.get_metadata) + + grid_layout.addWidget(label, 1, 1) + grid_layout.addWidget(new_inv_button, 2, 1) + + widget.setLayout(grid_layout) + self.tabs.addTab(widget, 'Array Maps') + def init_array_map(self): if not self.array_map: self.get_metadata() if not self.metadata: return - self.array_map = map_projection(self) + self.array_map = map_projection(self) + self.tabs.removeTab(1) + self.tabs.addTab(self.array_map, 'Array Map') + self.tabs.setCurrentIndex(1) - def show_array_map(self, container=None): + def refresh_array_map(self): if not self.array_map: - print('No array map found. Init array map first!') return - if self.container: - container.addWidget(self.array_map) + # refresh with new picks here!!! self.array_map.show() + + def read_metadata_thread(self, fninv): + self.rm_thread = Thread(self, read_metadata, arg=fninv, progressText='Reading metadata...') + self.rm_thread.finished.connect(self.set_metadata) + self.rm_thread.start() + + def set_metadata(self): + self.metadata = self.rm_thread.data + self.init_array_map() def get_metadata(self): def set_inv(settings): @@ -1160,7 +1237,7 @@ class MainWindow(QMainWindow): if ans == QMessageBox.Yes: settings.setValue("inventoryFile", fninv) settings.sync() - self.metadata = read_metadata(fninv) + self.read_metadata_thread(fninv) return True settings = QSettings() @@ -1179,7 +1256,7 @@ class MainWindow(QMainWindow): if not set_inv(settings): return None else: - self.metadata = read_metadata(fninv) + self.read_metadata_thread(fninv) def calc_magnitude(self, type='ML'): self.get_metadata() @@ -1271,8 +1348,9 @@ class MainWindow(QMainWindow): filename = fnm[0] + '.plp' if not exists: self.project = Project() + self.init_events(new=True) self.project.save(filename) - + def loadProject(self): if self.project: if self.project.dirty: @@ -1393,8 +1471,16 @@ class event(object): ''' Pickable class containing information on a single event. ''' - def __init__(self): - self.eventID = None + def __init__(self, eventPath): + self.eventPath = eventPath + self.autopicks = None + self.picks = None + + def addPicks(self, picks): + self.picks = picks + + def addAutopicks(self, autopicks): + self.autopicks = autopicks class getExistingDirectories(QFileDialog): diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index c95ddf95..5fb487d8 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -6287-dirty +fd70-dirty diff --git a/pylot/core/util/map_projection.py b/pylot/core/util/map_projection.py index ebdbc967..e64ef084 100644 --- a/pylot/core/util/map_projection.py +++ b/pylot/core/util/map_projection.py @@ -63,7 +63,7 @@ class map_projection(QtGui.QWidget): print('Could not generate Plot for station {st}.\n{er}'.format(st=station, er=e)) def connectSignals(self): - self.combobox.currentIndexChanged.connect(self.refresh_drawings) + self.comboBox_phase.currentIndexChanged.connect(self.refresh_drawings) def init_graphics(self): self.main_box = QtGui.QVBoxLayout() @@ -72,11 +72,11 @@ class map_projection(QtGui.QWidget): self.top_row = QtGui.QHBoxLayout() self.main_box.addLayout(self.top_row) - self.combobox = QtGui.QComboBox() - self.combobox.insertItem(0, 'P') - self.combobox.insertItem(1, 'S') + self.comboBox_phase = QtGui.QComboBox() + self.comboBox_phase.insertItem(0, 'P') + self.comboBox_phase.insertItem(1, 'S') self.top_row.addWidget(QtGui.QLabel('Select a phase: ')) - self.top_row.addWidget(self.combobox) + self.top_row.addWidget(self.comboBox_phase) if not self.figure: fig = plt.figure() @@ -108,7 +108,7 @@ class map_projection(QtGui.QWidget): self.lon = lon def init_picks(self): - phase = self.combobox.currentText() + phase = self.comboBox_phase.currentText() def get_picks(station_names): picks=[] for station in station_names: @@ -240,6 +240,9 @@ class map_projection(QtGui.QWidget): if self.picked: self.scatter_picked_stations() self.cbar = self.add_cbar(label='Time relative to first onset [s]') + self.comboBox_phase.setEnabled(True) + else: + self.comboBox_phase.setEnabled(False) self.annotate_ax() self.canvas.draw() diff --git a/pylot/core/util/thread.py b/pylot/core/util/thread.py index 8bd035ba..fc76081b 100644 --- a/pylot/core/util/thread.py +++ b/pylot/core/util/thread.py @@ -39,16 +39,20 @@ class AutoPickThread(QThread): class Thread(QThread): - def __init__(self, parent, func, progressText = None): + def __init__(self, parent, func, arg=None, progressText=None): QThread.__init__(self, parent) self.func = func + self.arg = arg self.progressText = progressText self.pbdlg = None self.finished.connect(self.hideProgressbar) self.showProgressbar() def run(self): - self.func() + if self.arg: + self.data = self.func(self.arg) + else: + self.data = self.func() def __del__(self): self.wait() @@ -63,8 +67,7 @@ class Thread(QThread): vl.addWidget(pb) vl.addWidget(QLabel(self.progressText)) self.pbdlg.setLayout(vl) - self.pbdlg.show() - self.pbdlg.setWindowFlags(Qt.FramelessWindowHint) + self.pbdlg.setWindowFlags(Qt.SplashScreen) self.pbdlg.show() def hideProgressbar(self): diff --git a/pylot/core/util/widgets.py b/pylot/core/util/widgets.py index 969ad945..8ad102df 100644 --- a/pylot/core/util/widgets.py +++ b/pylot/core/util/widgets.py @@ -397,7 +397,7 @@ class WaveformWidget(FigureCanvas): self.setParent(parent) self.figure = Figure() self.figure.set_facecolor((.92, .92, .92)) - # attribute plotdict is an dictionary connecting position and a name + # attribute plotdict is a dictionary connecting position and a name self.plotdict = dict() # create axes self.axes = self.figure.add_subplot(111) From f935da82960806df199662cd503a161607ddf3ac Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Wed, 19 Apr 2017 16:05:45 +0200 Subject: [PATCH 04/11] few bugfixes --- QtPyLoT.py | 42 +++++++++++++++++++++++++++++------------- pylot/RELEASE-VERSION | 2 +- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index cfb2a50f..d3e51eb2 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -91,6 +91,7 @@ class MainWindow(QMainWindow): self.project = None self.array_map = None self._metadata = None + self._eventChanged = False self.poS_id = None self.ae_id = None @@ -170,7 +171,7 @@ class MainWindow(QMainWindow): self._event_layout.addWidget(self.eventBox) self._event_layout.setStretch(1,1) #set stretch of item 1 to 1 self._main_layout.addLayout(self._event_layout) - self.eventBox.activated.connect(self.refreshTabs) + self.eventBox.activated.connect(self.refreshEvents) # add tabs self.tabs = QTabWidget() @@ -184,7 +185,6 @@ class MainWindow(QMainWindow): self.dataPlot.setCursor(Qt.CrossCursor) self.tabs.addTab(self.dataPlot, 'Waveform Plot') self.init_array_tab() - quitIcon = self.style().standardIcon(QStyle.SP_MediaStop) saveIcon = self.style().standardIcon(QStyle.SP_DriveHDIcon) @@ -265,11 +265,11 @@ class MainWindow(QMainWindow): "Load location information on " "the displayed event.") self.loadpilotevent = self.createAction(self, "Load PILOT &event ...", - self.load_pilotevent, "Ctrl+E", - loadpiloticon, - "Load PILOT event from information " - "MatLab binary collections (created" - " in former MatLab based version).") + self.load_pilotevent, "Ctrl+E", + loadpiloticon, + "Load PILOT event from information " + "MatLab binary collections (created" + " in former MatLab based version).") self.loadpilotevent.setEnabled(False) self.saveEventAction = self.createAction(self, "&Save event ...", @@ -604,7 +604,7 @@ class MainWindow(QMainWindow): self.eventBox.setCurrentIndex(0) else: self.eventBox.setCurrentIndex(nitems) - self.refreshTabs() + self.refreshEvents() def fill_eventbox(self, eventlist): model = self.eventBox.model() @@ -782,11 +782,17 @@ class MainWindow(QMainWindow): return self.saveData() return True + def refreshEvents(self): + self._eventChanged = True + self.refreshTabs() + def refreshTabs(self): if self.tabs.currentIndex() == 0: if hasattr(self.project, 'eventlist'): if len(self.project.eventlist) > 0: - self.loadWaveformDataThread() + if self._eventChanged: + self.loadWaveformDataThread() + self._eventChanged = False if self.tabs.currentIndex() == 1: self.refresh_array_map() @@ -1197,15 +1203,15 @@ class MainWindow(QMainWindow): widget.setLayout(grid_layout) self.tabs.addTab(widget, 'Array Maps') - def init_array_map(self): + def init_array_map(self, index=1): if not self.array_map: self.get_metadata() if not self.metadata: return - self.array_map = map_projection(self) self.tabs.removeTab(1) + self.array_map = map_projection(self) self.tabs.addTab(self.array_map, 'Array Map') - self.tabs.setCurrentIndex(1) + self.tabs.setCurrentIndex(index) def refresh_array_map(self): if not self.array_map: @@ -1220,6 +1226,7 @@ class MainWindow(QMainWindow): def set_metadata(self): self.metadata = self.rm_thread.data + self.project.metadata = self.rm_thread.data self.init_array_map() def get_metadata(self): @@ -1239,6 +1246,10 @@ class MainWindow(QMainWindow): settings.sync() self.read_metadata_thread(fninv) return True + + if hasattr(self.project, 'metadata'): + self.metadata = self.project.metadata + return True settings = QSettings() fninv = settings.value("inventoryFile", None) @@ -1368,10 +1379,15 @@ class MainWindow(QMainWindow): if fnm[0]: self.project = Project.load(fnm[0]) self.init_events(new=True) + if hasattr(self.project, 'metadata'): + self.init_array_map(index=0) def saveProject(self): if self.project: - self.project.save() + if not self.project.location: + self.createNewProject(exists=True) + else: + self.project.save() if not self.project.dirty: qmb = QMessageBox(icon=QMessageBox.Information, text='Saved back project to file:\n{}'.format(self.project.location)) qmb.exec_() diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index 5fb487d8..08a48f05 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -fd70-dirty +8057-dirty From 227faf14c274e518307ed5c9873bae64db100935 Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Wed, 19 Apr 2017 16:51:19 +0200 Subject: [PATCH 05/11] saving picks in events as part of project --- QtPyLoT.py | 33 ++++++++++++++++++++++++++------- pylot/RELEASE-VERSION | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index d3e51eb2..0cfc8693 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -522,6 +522,11 @@ class MainWindow(QMainWindow): self.drawPicks(picktype=type) self.draw() + def getCurrentEvent(self): + for event in self.project.eventlist: + if event.path == self.eventBox.currentText(): + return event + def getLastEvent(self): return self.recentfiles[0] @@ -609,6 +614,7 @@ class MainWindow(QMainWindow): def fill_eventbox(self, eventlist): model = self.eventBox.model() for event in self.project.eventlist: + event = event.path item = QtGui.QStandardItem(str(event)) # if ref: set different color e.g. #item.setBackground(QtGui.QColor('teal')) @@ -791,10 +797,13 @@ class MainWindow(QMainWindow): if hasattr(self.project, 'eventlist'): if len(self.project.eventlist) > 0: if self._eventChanged: - self.loadWaveformDataThread() - self._eventChanged = False + self.newWFplot() if self.tabs.currentIndex() == 1: self.refresh_array_map() + + def newWFplot(self): + self.loadWaveformDataThread() + self._eventChanged = False def loadWaveformDataThread(self): wfd_thread = Thread(self, self.loadWaveformData, progressText='Reading data input...') @@ -838,6 +847,13 @@ class MainWindow(QMainWindow): self.openautopicksaction.setEnabled(True) self.loadpilotevent.setEnabled(True) self.saveEventAction.setEnabled(True) + event = self.getCurrentEvent() + if event.picks: + self.picks = event.picks + self.drawPicks(picktype='manual') + if event.autopicks: + self.autopicks = event.autopicks + self.drawPicks(picktype='auto') self.draw() def clearWaveformDataPlot(self): @@ -1071,8 +1087,10 @@ class MainWindow(QMainWindow): def updatePicks(self, type='manual'): picks = picksdict_from_picks(evt=self.get_data(type).get_evt_data()) if type == 'manual': + self.getCurrentEvent().addPicks(picks) self.picks.update(picks) elif type == 'auto': + self.getCurrentEvent().addAutopicks(picks) self.autopicks.update(picks) self.check4Comparison() @@ -1438,8 +1456,9 @@ class Project(object): if len(eventlist) == 0: return for item in eventlist: - if not item in self.eventlist: - self.eventlist.append(item) + event = Event(item) + if not event in self.eventlist: + self.eventlist.append(event) self.setDirty() def setDirty(self): @@ -1483,12 +1502,12 @@ class Project(object): return project -class event(object): +class Event(object): ''' Pickable class containing information on a single event. ''' - def __init__(self, eventPath): - self.eventPath = eventPath + def __init__(self, path): + self.path = path self.autopicks = None self.picks = None diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index 08a48f05..dc89e442 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -8057-dirty +f935-dirty From f5dcfc654dccc366b10fac0a8204df6b4174402f Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Thu, 20 Apr 2017 12:05:34 +0200 Subject: [PATCH 06/11] added eventlist, some changes in tab structure (layouts for each tab to keep tabs when refreshing) --- QtPyLoT.py | 146 ++++++++++++++++++++++++++++-- pylot/RELEASE-VERSION | 2 +- pylot/core/util/map_projection.py | 3 +- 3 files changed, 142 insertions(+), 9 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index 0cfc8693..99592182 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -88,7 +88,7 @@ class MainWindow(QMainWindow): else: self.infile = infile - self.project = None + self.project = Project() self.array_map = None self._metadata = None self._eventChanged = False @@ -183,8 +183,23 @@ class MainWindow(QMainWindow): self.dataPlot = WaveformWidget(parent=self, xlabel=xlab, ylabel=None, title=plottitle) self.dataPlot.setCursor(Qt.CrossCursor) - self.tabs.addTab(self.dataPlot, 'Waveform Plot') + wf_tab = QtGui.QWidget() + array_tab = QtGui.QWidget() + events_tab = QtGui.QWidget() + self.wf_layout = QtGui.QVBoxLayout() + self.array_layout = QtGui.QVBoxLayout() + self.events_layout = QtGui.QVBoxLayout() + wf_tab.setLayout(self.wf_layout) + array_tab.setLayout(self.array_layout) + events_tab.setLayout(self.events_layout) + self.tabs.addTab(wf_tab, 'Waveform Plot') + self.tabs.addTab(array_tab, 'Array Map') + self.tabs.addTab(events_tab, 'Eventlist') + + self.wf_layout.addWidget(self.dataPlot) self.init_array_tab() + self.init_event_table() + self.tabs.setCurrentIndex(0) quitIcon = self.style().standardIcon(QStyle.SP_MediaStop) saveIcon = self.style().standardIcon(QStyle.SP_DriveHDIcon) @@ -610,6 +625,7 @@ class MainWindow(QMainWindow): else: self.eventBox.setCurrentIndex(nitems) self.refreshEvents() + tabindex = self.tabs.currentIndex() def fill_eventbox(self, eventlist): model = self.eventBox.model() @@ -800,6 +816,8 @@ class MainWindow(QMainWindow): self.newWFplot() if self.tabs.currentIndex() == 1: self.refresh_array_map() + if self.tabs.currentIndex() == 2: + self.init_event_table() def newWFplot(self): self.loadWaveformDataThread() @@ -1204,7 +1222,7 @@ class MainWindow(QMainWindow): self.get_data().applyEVTData(self.calc_magnitude(), type='event') def init_array_tab(self): - widget = QWidget(self) + self.metadata_widget = QWidget(self) grid_layout = QGridLayout() grid_layout.setColumnStretch(0, 1) grid_layout.setColumnStretch(2, 1) @@ -1218,17 +1236,17 @@ class MainWindow(QMainWindow): grid_layout.addWidget(label, 1, 1) grid_layout.addWidget(new_inv_button, 2, 1) - widget.setLayout(grid_layout) - self.tabs.addTab(widget, 'Array Maps') + self.metadata_widget.setLayout(grid_layout) + self.array_layout.addWidget(self.metadata_widget) def init_array_map(self, index=1): if not self.array_map: self.get_metadata() if not self.metadata: return - self.tabs.removeTab(1) self.array_map = map_projection(self) - self.tabs.addTab(self.array_map, 'Array Map') + self.array_layout.removeWidget(self.metadata_widget) + self.array_layout.addWidget(self.array_map) self.tabs.setCurrentIndex(index) def refresh_array_map(self): @@ -1237,6 +1255,91 @@ class MainWindow(QMainWindow): # refresh with new picks here!!! self.array_map.show() + def init_event_table(self, index=2): + def set_enabled(item, enabled=True, checkable=False): + if enabled and not checkable: + item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) + elif enabled and checkable: + item_ref.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsSelectable) + else: + item.setFlags(QtCore.Qt.ItemIsSelectable) + + if self.project: + eventlist = self.project.eventlist + else: + eventlist = [] + + def cell_changed(row=None, column=None): + table = self.project._table + event = self.project.getEventFromPath(table[row][0].text()) + if column == 1 or column == 2: + #toggle checked states (exclusive) + item_ref = table[row][1] + item_test = table[row][2] + if column == 1 and item_ref.checkState(): + item_test.setCheckState(QtCore.Qt.Unchecked) + event.setRefEvent(True) + elif column == 1 and not item_ref.checkState(): + event.setRefEvent(False) + elif column == 2 and item_test.checkState(): + item_ref.setCheckState(QtCore.Qt.Unchecked) + event.setTestEvent(True) + elif column == 2 and not item_test.checkState(): + event.setTestEvent(False) + elif column == 3: + #update event notes + notes = table[row][3].text() + event.addNotes(notes) + + if hasattr(self, 'qtl'): + self.events_layout.removeWidget(self.qtl) + self.qtl = QtGui.QTableWidget() + self.qtl.setColumnCount(4) + self.qtl.setRowCount(len(eventlist)) + self.qtl.setHorizontalHeaderLabels(['Event', 'Reference', 'Test Set', 'Notes']) + + self.project._table = [] + for index, event in enumerate(eventlist): + item_path = QtGui.QTableWidgetItem() + item_ref = QtGui.QTableWidgetItem() + item_test = QtGui.QTableWidgetItem() + item_notes = QtGui.QTableWidgetItem() + + item_path.setText(event.path) + item_notes.setText(event.notes) + if event.picks: + set_enabled(item_path, True, False) + set_enabled(item_ref, True, True) + set_enabled(item_test, True, True) + else: + set_enabled(item_path, False, False) + set_enabled(item_ref, False, True) + set_enabled(item_test, False, True) + + if event.isRefEvent(): + item_ref.setCheckState(QtCore.Qt.Checked) + else: + item_ref.setCheckState(QtCore.Qt.Unchecked) + if event.isTestEvent(): + item_test.setCheckState(QtCore.Qt.Checked) + else: + item_test.setCheckState(QtCore.Qt.Unchecked) + + column=[item_path, item_ref, item_test, item_notes] + self.project._table.append(column) + + for r_index, row in enumerate(self.project._table): + for c_index, item in enumerate(row): + self.qtl.setItem(r_index, c_index, item) + + header = self.qtl.horizontalHeader() + header.setResizeMode(QtGui.QHeaderView.ResizeToContents) + header.setStretchLastSection(True) + self.qtl.cellChanged[int, int].connect(cell_changed) + + self.events_layout.addWidget(self.qtl) + self.tabs.setCurrentIndex(index) + def read_metadata_thread(self, fninv): self.rm_thread = Thread(self, read_metadata, arg=fninv, progressText='Reading metadata...') self.rm_thread.finished.connect(self.set_metadata) @@ -1451,6 +1554,7 @@ class Project(object): self.eventlist = [] self.location = None self.dirty = False + self._table = None def add_eventlist(self, eventlist): if len(eventlist) == 0: @@ -1467,6 +1571,11 @@ class Project(object): def setClean(self): self.dirty = False + def getEventFromPath(self, path): + for event in self.eventlist: + if event.path == path: + return event + def save(self, filename=None): ''' Save PyLoT Project to a file. @@ -1510,12 +1619,35 @@ class Event(object): self.path = path self.autopicks = None self.picks = None + self.notes = None + self._testEvent = False + self._refEvent = False def addPicks(self, picks): self.picks = picks def addAutopicks(self, autopicks): self.autopicks = autopicks + + def addNotes(self, notes): + self.notes = notes + + def clearNotes(self): + self.notes = None + + def isRefEvent(self): + return self._refEvent + + def isTestEvent(self): + return self._testEvent + + def setRefEvent(self, bool): + self._refEvent = bool + if bool: self._testEvent = False + + def setTestEvent(self, bool): + self._testEvent = bool + if bool: self._refEvent = False class getExistingDirectories(QFileDialog): diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index dc89e442..9031f24d 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -f935-dirty +227f-dirty diff --git a/pylot/core/util/map_projection.py b/pylot/core/util/map_projection.py index e64ef084..5fbe693b 100644 --- a/pylot/core/util/map_projection.py +++ b/pylot/core/util/map_projection.py @@ -75,8 +75,9 @@ class map_projection(QtGui.QWidget): self.comboBox_phase = QtGui.QComboBox() self.comboBox_phase.insertItem(0, 'P') self.comboBox_phase.insertItem(1, 'S') - self.top_row.addWidget(QtGui.QLabel('Select a phase: ')) + self.top_row.addWidget(QtGui.QLabel('Select a phase: ')) self.top_row.addWidget(self.comboBox_phase) + self.top_row.setStretch(1,1) #set stretch of item 1 to 1 if not self.figure: fig = plt.figure() From bcc58e4937662509cf76517776a4738a5502b5d0 Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Thu, 20 Apr 2017 14:03:26 +0200 Subject: [PATCH 07/11] changed eventlist string and coloring, [bug]: picks not displayed --- QtPyLoT.py | 67 ++++++++++++++++++++++++++++++++++--------- pylot/RELEASE-VERSION | 2 +- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index 99592182..edc0e90e 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -163,7 +163,7 @@ class MainWindow(QMainWindow): self._main_layout = QVBoxLayout() # add event combo box - self.eventBox = QComboBox() + self.eventBox = self.createEventBox() self.eventBox.setMaxVisibleItems(30) self.eventBox.setEnabled(False) self._event_layout = QHBoxLayout() @@ -539,8 +539,11 @@ class MainWindow(QMainWindow): def getCurrentEvent(self): for event in self.project.eventlist: - if event.path == self.eventBox.currentText(): + if event.path == self.getCurrentEventPath(): return event + + def getCurrentEventPath(self): + return str(self.eventBox.currentText().split('|')[0]).strip() def getLastEvent(self): return self.recentfiles[0] @@ -585,7 +588,7 @@ class MainWindow(QMainWindow): def getWFFnames_from_eventlist(self): if self.dataStructure: searchPath = self.dataStructure.expandDataPath() - directory = self.eventBox.currentText() + directory = self.getCurrentEventPath() self.fnames = [os.path.join(directory, f) for f in os.listdir(directory)] else: raise DatastructureError('not specified') @@ -611,15 +614,25 @@ class MainWindow(QMainWindow): self.project.add_eventlist(eventlist) self.init_events() + def createEventBox(self): + qcb = QComboBox() + palette = qcb.palette() + palette.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Highlight, + QtGui.QBrush(QtGui.QColor(0,0,127,127))) + palette.setBrush(QtGui.QPalette.Active, QtGui.QPalette.Highlight, + QtGui.QBrush(QtGui.QColor(0,0,127,127))) + qcb.setPalette(palette) + return qcb + + def init_events(self, new=False): nitems = self.eventBox.count() - self.eventBox.clear() if len(self.project.eventlist) == 0: print('No events to init.') self.clearWaveformDataPlot() return self.eventBox.setEnabled(True) - self.fill_eventbox(self.project.eventlist) + self.fill_eventbox() if new: self.eventBox.setCurrentIndex(0) else: @@ -627,19 +640,41 @@ class MainWindow(QMainWindow): self.refreshEvents() tabindex = self.tabs.currentIndex() - def fill_eventbox(self, eventlist): + def fill_eventbox(self): + index=self.eventBox.currentIndex() + self.eventBox.clear() model = self.eventBox.model() for event in self.project.eventlist: - event = event.path - item = QtGui.QStandardItem(str(event)) + event_path = event.path + event_npicks = 0 + event_nautopicks = 0 + if event.picks: + event_npicks = len(event.picks) + if event.autopicks: + event_nautopicks = len(event.autopicks) + event_ref = event.isRefEvent() + event_test = event.isTestEvent() + + text = '{path} | manual: [{p}] | auto: [{a}] | ref: {ref} | test: {test}' + text = text.format(path=event_path, + p=event_npicks, + a=event_nautopicks, + ref=event_ref, + test=event_test) + + item = QtGui.QStandardItem(str(text)) # if ref: set different color e.g. - #item.setBackground(QtGui.QColor('teal')) + if event_ref: + item.setBackground(QtGui.QColor('cyan')) + if event_test: + item.setBackground(QtGui.QColor('yellow')) item.setForeground(QtGui.QColor('black')) font = item.font() font.setPointSize(10) item.setFont(font) model.appendRow(item) - + self.eventBox.setCurrentIndex(index) + def filename_from_action(self, action): if action.data() is None: filt = "Supported file formats" \ @@ -1285,7 +1320,8 @@ class MainWindow(QMainWindow): item_ref.setCheckState(QtCore.Qt.Unchecked) event.setTestEvent(True) elif column == 2 and not item_test.checkState(): - event.setTestEvent(False) + event.setTestEvent(False) + self.fill_eventbox() elif column == 3: #update event notes notes = table[row][3].text() @@ -1303,8 +1339,10 @@ class MainWindow(QMainWindow): item_path = QtGui.QTableWidgetItem() item_ref = QtGui.QTableWidgetItem() item_test = QtGui.QTableWidgetItem() - item_notes = QtGui.QTableWidgetItem() - + item_notes = QtGui.QTableWidgetItem() + + item_ref.setBackground(QtGui.QColor('cyan')) + item_test.setBackground(QtGui.QColor('yellow')) item_path.setText(event.path) item_notes.setText(event.notes) if event.picks: @@ -1336,7 +1374,7 @@ class MainWindow(QMainWindow): header.setResizeMode(QtGui.QHeaderView.ResizeToContents) header.setStretchLastSection(True) self.qtl.cellChanged[int, int].connect(cell_changed) - + self.events_layout.addWidget(self.qtl) self.tabs.setCurrentIndex(index) @@ -1522,6 +1560,7 @@ class MainWindow(QMainWindow): self.createNewProject(exists=True) def draw(self): + #self.fill_eventbox() self.getPlotWidget().draw() def setDirty(self, value): diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index 9031f24d..10b8f267 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -227f-dirty +f5dcf-dirty From 29701ea68bc9f4900048d3875db071b9f9a0acca Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Thu, 20 Apr 2017 14:54:57 +0200 Subject: [PATCH 08/11] plotting picks problem was no bug, but resulted from a high sensitivity of the code to random files or folders in the event directory causing HUGE problems... [fix this] --- QtPyLoT.py | 3 +-- pylot/RELEASE-VERSION | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index edc0e90e..0347bbb5 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -144,7 +144,6 @@ class MainWindow(QMainWindow): self.loc = False def setupUi(self): - try: self.startTime = min( [tr.stats.starttime for tr in self.data.wfdata]) @@ -1560,7 +1559,7 @@ class MainWindow(QMainWindow): self.createNewProject(exists=True) def draw(self): - #self.fill_eventbox() + self.fill_eventbox() self.getPlotWidget().draw() def setDirty(self, value): diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index 10b8f267..bb4d840c 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -f5dcf-dirty +bcc5-dirty From b23228253a57d192d7386c1b91b8ccdf60131a2e Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Thu, 20 Apr 2017 15:28:43 +0200 Subject: [PATCH 09/11] [bugfix] plotting pick of lower most channel --- QtPyLoT.py | 2 +- pylot/RELEASE-VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index 0347bbb5..ed91d0d1 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -1154,7 +1154,7 @@ class MainWindow(QMainWindow): return # plotting picks plotID = self.getStationID(station) - if not plotID: + if plotID is None: return ax = self.getPlotWidget().axes ylims = np.array([-.5, +.5]) + plotID diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index bb4d840c..bcfe41c9 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -bcc5-dirty +29701-dirty From 6b7dbe3f9acabacec9120d5b4563a60e322adf44 Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Thu, 20 Apr 2017 17:06:36 +0200 Subject: [PATCH 10/11] some bugfixes --- QtPyLoT.py | 41 ++++++++++++++++++-------- pylot/RELEASE-VERSION | 2 +- pylot/core/util/map_projection.py | 48 +++++++++++++++++++++---------- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/QtPyLoT.py b/QtPyLoT.py index ed91d0d1..6ad74af1 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -91,7 +91,7 @@ class MainWindow(QMainWindow): self.project = Project() self.array_map = None self._metadata = None - self._eventChanged = False + self._eventChanged = [False, False] self.poS_id = None self.ae_id = None @@ -839,23 +839,34 @@ class MainWindow(QMainWindow): return True def refreshEvents(self): - self._eventChanged = True + self._eventChanged = [True, True] self.refreshTabs() def refreshTabs(self): - if self.tabs.currentIndex() == 0: - if hasattr(self.project, 'eventlist'): - if len(self.project.eventlist) > 0: - if self._eventChanged: - self.newWFplot() - if self.tabs.currentIndex() == 1: - self.refresh_array_map() + if self._eventChanged[0] or self._eventChanged[1]: + event = self.getCurrentEvent() + if not event.picks: + self.picks = {} + else: + self.picks = event.picks + if not event.autopicks: + self.autopicks = {} + else: + self.autopicks = event.autopicks + if self.tabs.currentIndex() == 0: + if hasattr(self.project, 'eventlist'): + if len(self.project.eventlist) > 0: + if self._eventChanged[0]: + self.newWFplot() + if self.tabs.currentIndex() == 1: + if self._eventChanged[1]: + self.refresh_array_map() if self.tabs.currentIndex() == 2: self.init_event_table() def newWFplot(self): self.loadWaveformDataThread() - self._eventChanged = False + self._eventChanged[0] = False def loadWaveformDataThread(self): wfd_thread = Thread(self, self.loadWaveformData, progressText='Reading data input...') @@ -1282,12 +1293,14 @@ class MainWindow(QMainWindow): self.array_layout.removeWidget(self.metadata_widget) self.array_layout.addWidget(self.array_map) self.tabs.setCurrentIndex(index) + self.refresh_array_map() def refresh_array_map(self): if not self.array_map: return # refresh with new picks here!!! - self.array_map.show() + self.array_map.refresh_drawings(self.picks) + self._eventChanged[1] = False def init_event_table(self, index=2): def set_enabled(item, enabled=True, checkable=False): @@ -1513,12 +1526,15 @@ class MainWindow(QMainWindow): dlg = QFileDialog() fnm = dlg.getSaveFileName(self, 'Create a new project file...', filter='Pylot project (*.plp)') filename = fnm[0] + if not len(fnm[0]): + return if not filename.split('.')[-1] == 'plp': filename = fnm[0] + '.plp' if not exists: self.project = Project() self.init_events(new=True) self.project.save(filename) + return True def loadProject(self): if self.project: @@ -1543,7 +1559,8 @@ class MainWindow(QMainWindow): def saveProject(self): if self.project: if not self.project.location: - self.createNewProject(exists=True) + if not self.createNewProject(exists=True): + return else: self.project.save() if not self.project.dirty: diff --git a/pylot/RELEASE-VERSION b/pylot/RELEASE-VERSION index bcfe41c9..e02486db 100644 --- a/pylot/RELEASE-VERSION +++ b/pylot/RELEASE-VERSION @@ -1 +1 @@ -29701-dirty +b232-dirty diff --git a/pylot/core/util/map_projection.py b/pylot/core/util/map_projection.py index 5fbe693b..53fc8ac7 100644 --- a/pylot/core/util/map_projection.py +++ b/pylot/core/util/map_projection.py @@ -12,7 +12,7 @@ from pylot.core.util.widgets import PickDlg plt.interactive(False) class map_projection(QtGui.QWidget): - def __init__(self, mainwindow, figure=None, picked=False): + def __init__(self, mainwindow, figure=None): ''' :param: picked, can be False, auto, manual :value: str @@ -20,7 +20,8 @@ class map_projection(QtGui.QWidget): QtGui.QWidget.__init__(self) self.pyl_mainwindow = mainwindow self.parser = mainwindow.metadata[1] - self.picked = picked + self.picks = None + self.picks_dict = None self.figure = figure self.init_graphics() self.init_stations() @@ -63,7 +64,7 @@ class map_projection(QtGui.QWidget): print('Could not generate Plot for station {st}.\n{er}'.format(st=station, er=e)) def connectSignals(self): - self.comboBox_phase.currentIndexChanged.connect(self.refresh_drawings) + self.comboBox_phase.currentIndexChanged.connect(self._refresh_drawings) def init_graphics(self): self.main_box = QtGui.QVBoxLayout() @@ -75,6 +76,11 @@ class map_projection(QtGui.QWidget): self.comboBox_phase = QtGui.QComboBox() self.comboBox_phase.insertItem(0, 'P') self.comboBox_phase.insertItem(1, 'S') + + # self.comboBox_am = QtGui.QComboBox() + # self.comboBox_am.insertItem(0, 'auto') + # self.comboBox_am.insertItem(1, 'manual') + self.top_row.addWidget(QtGui.QLabel('Select a phase: ')) self.top_row.addWidget(self.comboBox_phase) self.top_row.setStretch(1,1) #set stretch of item 1 to 1 @@ -89,7 +95,7 @@ class map_projection(QtGui.QWidget): self.toolbar = NavigationToolbar(self.canvas, self) self.main_box.addWidget(self.toolbar) - + def init_stations(self): def get_station_names_lat_lon(parser): station_names=[] @@ -114,7 +120,7 @@ class map_projection(QtGui.QWidget): picks=[] for station in station_names: try: - picks.append(self.pyl_mainwindow.autopicks[station][phase]['mpp']) + picks.append(self.picks_dict[station][phase]['mpp']) except: picks.append(np.nan) return picks @@ -226,19 +232,24 @@ class map_projection(QtGui.QWidget): cbar.set_label(label) return cbar - def refresh_drawings(self): + def refresh_drawings(self, picks=None): + self.picks_dict = picks self.remove_drawings() self.draw_everything() - + + def _refresh_drawings(self): + self.remove_drawings() + self.draw_everything() + def draw_everything(self): - if self.picked: + if self.picks_dict: self.init_picks() self.init_picks_active() self.init_stations_active() self.init_picksgrid() self.draw_contour_filled() self.scatter_all_stations() - if self.picked: + if self.picks_dict: self.scatter_picked_stations() self.cbar = self.add_cbar(label='Time relative to first onset [s]') self.comboBox_phase.setEnabled(True) @@ -248,13 +259,20 @@ class map_projection(QtGui.QWidget): self.canvas.draw() def remove_drawings(self): - self.sc_picked.remove() - self.sc.remove() - self.cbar.remove() - self.remove_annotations() + if hasattr(self, 'sc_picked'): + self.sc_picked.remove() + del(self.sc_picked) + if hasattr(self, 'cbar'): + self.cbar.remove() + del(self.cbar) + if hasattr(self, 'contourf'): + self.remove_contourf() + del(self.contourf) + if hasattr(self, 'cid'): + self.canvas.mpl_disconnect(self.cid) + del(self.cid) + self.sc.remove() self.legend.remove() - self.remove_contourf() - self.canvas.mpl_disconnect(self.cid) self.canvas.draw() def remove_contourf(self): From 08dfe35620b814d80042582d860c5db0b816df41 Mon Sep 17 00:00:00 2001 From: Marcel Paffrath Date: Fri, 21 Apr 2017 15:46:57 +0200 Subject: [PATCH 11/11] final commit of first project/event branch, ready for merging to develop [open issues: no differentiation between auto and manual picks mainly for map (internal structure change incoming), new icon integration failed...] --- QtPyLoT.py | 117 ++++++++++++++++++++++++---------- icons.qrc | 2 + icons/autopicksicon_small.png | Bin 0 -> 27864 bytes icons/manupicksicon_small.png | Bin 0 -> 22384 bytes pylot/RELEASE-VERSION | 2 +- 5 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 icons/autopicksicon_small.png create mode 100644 icons/manupicksicon_small.png diff --git a/QtPyLoT.py b/QtPyLoT.py index 6ad74af1..1e4b2147 100755 --- a/QtPyLoT.py +++ b/QtPyLoT.py @@ -214,6 +214,14 @@ class MainWindow(QMainWindow): manupicksicon.addPixmap(QPixmap(':/icons/manupicsicon.png')) autopicksicon = QIcon() autopicksicon.addPixmap(QPixmap(':/icons/autopicsicon.png')) + self.autopicksicon_small = QIcon() + self.autopicksicon_small.addPixmap(QPixmap(':/icons/autopicsicon.png')) + self.manupicksicon_small = QIcon() + self.manupicksicon_small.addPixmap(QPixmap(':/icons/manupicsicon.png')) + # self.autopicksicon_small = QIcon() + # self.autopicksicon_small.addPixmap(QPixmap(':/icons/autopicksicon_small.png')) + # self.manupicksicon_small = QIcon() + # self.manupicksicon_small.addPixmap(QPixmap(':/icons/manupicksicon_small.png')) loadpiloticon = QIcon() loadpiloticon.addPixmap(QPixmap(':/icons/Matlab_PILOT_icon.png')) p_icon = QIcon() @@ -641,8 +649,23 @@ class MainWindow(QMainWindow): def fill_eventbox(self): index=self.eventBox.currentIndex() + tv=QtGui.QTableView() + header = tv.horizontalHeader() + header.setResizeMode(QtGui.QHeaderView.ResizeToContents) + header.setStretchLastSection(True) + header.hide() + tv.verticalHeader().hide() + tv.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + + self.eventBox.setView(tv) self.eventBox.clear() model = self.eventBox.model() + plmax=0 + for event in self.project.eventlist: + pl = len(event.path) + if pl > plmax: + plmax=pl + for event in self.project.eventlist: event_path = event.path event_npicks = 0 @@ -654,24 +677,40 @@ class MainWindow(QMainWindow): event_ref = event.isRefEvent() event_test = event.isTestEvent() - text = '{path} | manual: [{p}] | auto: [{a}] | ref: {ref} | test: {test}' + text = '{path:{plen}} | manual: [{p:3d}] | auto: [{a:3d}]' text = text.format(path=event_path, + plen=plmax, p=event_npicks, - a=event_nautopicks, - ref=event_ref, - test=event_test) - - item = QtGui.QStandardItem(str(text)) - # if ref: set different color e.g. + a=event_nautopicks) + + item_path = QtGui.QStandardItem('{path:{plen}}'.format(path=event_path, plen=plmax)) + item_nmp = QtGui.QStandardItem(str(event_npicks)) + item_nmp.setIcon(self.manupicksicon_small) + item_nap = QtGui.QStandardItem(str(event_nautopicks)) + item_nap.setIcon(self.autopicksicon_small) + item_ref = QtGui.QStandardItem()#str(event_ref)) + item_test = QtGui.QStandardItem()#str(event_test)) if event_ref: - item.setBackground(QtGui.QColor('cyan')) + item_ref.setBackground(QtGui.QColor(200, 210, 230, 255)) if event_test: - item.setBackground(QtGui.QColor('yellow')) - item.setForeground(QtGui.QColor('black')) - font = item.font() - font.setPointSize(10) - item.setFont(font) - model.appendRow(item) + item_test.setBackground(QtGui.QColor(200, 230, 200, 255)) + item_notes = QtGui.QStandardItem(event.notes) + + openIcon = self.style().standardIcon(QStyle.SP_DirOpenIcon) + item_path.setIcon(openIcon) + # if ref: set different color e.g. + # if event_ref: + # item.setBackground(QtGui.QColor(200, 210, 230, 255)) + # if event_test: + # item.setBackground(QtGui.QColor(200, 230, 200, 255)) + # item.setForeground(QtGui.QColor('black')) + # font = item.font() + # font.setPointSize(10) + # item.setFont(font) + # item2.setForeground(QtGui.QColor('black')) + # item2.setFont(font) + itemlist = [item_path, item_nmp, item_nap, item_ref, item_test, item_notes] + model.appendRow(itemlist) self.eventBox.setCurrentIndex(index) def filename_from_action(self, action): @@ -1307,7 +1346,7 @@ class MainWindow(QMainWindow): if enabled and not checkable: item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) elif enabled and checkable: - item_ref.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsSelectable) + item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsSelectable) else: item.setFlags(QtCore.Qt.ItemIsSelectable) @@ -1319,50 +1358,64 @@ class MainWindow(QMainWindow): def cell_changed(row=None, column=None): table = self.project._table event = self.project.getEventFromPath(table[row][0].text()) - if column == 1 or column == 2: + if column == 3 or column == 4: #toggle checked states (exclusive) - item_ref = table[row][1] - item_test = table[row][2] - if column == 1 and item_ref.checkState(): + item_ref = table[row][3] + item_test = table[row][4] + if column == 3 and item_ref.checkState(): item_test.setCheckState(QtCore.Qt.Unchecked) event.setRefEvent(True) - elif column == 1 and not item_ref.checkState(): + elif column == 3 and not item_ref.checkState(): event.setRefEvent(False) - elif column == 2 and item_test.checkState(): + elif column == 4 and item_test.checkState(): item_ref.setCheckState(QtCore.Qt.Unchecked) event.setTestEvent(True) - elif column == 2 and not item_test.checkState(): + elif column == 4 and not item_test.checkState(): event.setTestEvent(False) self.fill_eventbox() - elif column == 3: + elif column == 5: #update event notes - notes = table[row][3].text() + notes = table[row][5].text() event.addNotes(notes) + self.fill_eventbox() if hasattr(self, 'qtl'): self.events_layout.removeWidget(self.qtl) self.qtl = QtGui.QTableWidget() - self.qtl.setColumnCount(4) + self.qtl.setColumnCount(6) self.qtl.setRowCount(len(eventlist)) - self.qtl.setHorizontalHeaderLabels(['Event', 'Reference', 'Test Set', 'Notes']) + self.qtl.setHorizontalHeaderLabels(['Event', '[N] MP', + '[N] AP', 'Reference', + 'Test Set', 'Notes']) self.project._table = [] for index, event in enumerate(eventlist): + event_npicks = 0 + event_nautopicks = 0 + if event.picks: + event_npicks = len(event.picks) + if event.autopicks: + event_nautopicks = len(event.autopicks) item_path = QtGui.QTableWidgetItem() + item_nmp = QtGui.QTableWidgetItem(str(event_npicks)) + item_nmp.setIcon(self.manupicksicon_small) + item_nap = QtGui.QTableWidgetItem(str(event_nautopicks)) + item_nap.setIcon(self.autopicksicon_small) item_ref = QtGui.QTableWidgetItem() item_test = QtGui.QTableWidgetItem() item_notes = QtGui.QTableWidgetItem() - - item_ref.setBackground(QtGui.QColor('cyan')) - item_test.setBackground(QtGui.QColor('yellow')) + + item_ref.setBackground(QtGui.QColor(200, 210, 230, 255)) + item_test.setBackground(QtGui.QColor(200, 230, 200, 255)) item_path.setText(event.path) item_notes.setText(event.notes) + set_enabled(item_path, True, False) + set_enabled(item_nmp, True, False) + set_enabled(item_nap, True, False) if event.picks: - set_enabled(item_path, True, False) set_enabled(item_ref, True, True) set_enabled(item_test, True, True) else: - set_enabled(item_path, False, False) set_enabled(item_ref, False, True) set_enabled(item_test, False, True) @@ -1375,7 +1428,7 @@ class MainWindow(QMainWindow): else: item_test.setCheckState(QtCore.Qt.Unchecked) - column=[item_path, item_ref, item_test, item_notes] + column=[item_path, item_nmp, item_nap, item_ref, item_test, item_notes] self.project._table.append(column) for r_index, row in enumerate(self.project._table): diff --git a/icons.qrc b/icons.qrc index 4c39b449..40fca409 100644 --- a/icons.qrc +++ b/icons.qrc @@ -4,6 +4,8 @@ icons/pylot.png icons/manupicsicon.png icons/autopicsicon.png + icons/manupicksicon_small.png + icons/autopicksicon_small.png icons/autopick_button.png icons/locactionicon.png icons/compare_button.png diff --git a/icons/autopicksicon_small.png b/icons/autopicksicon_small.png new file mode 100644 index 0000000000000000000000000000000000000000..2c626102f90b1241985affa835745835ffcea78d GIT binary patch literal 27864 zcmb@u2{hGj`#$&Z{XF+`-`9Oz*S#O9U6E&?+ee3C7=xn1 zMGXw2qQo%DN4Rb98(dieJN%%&E30@F2mjo0CV}w(w>v24-Nmp2V(5Pqa?vMlz%TbY z%IP|4+M78#8{aX-oSmJIS=w3Oy=ClRdd&WgdFl`X4E3lv%dEwEum4+M3G=-n?W=u(u6KEUd5Ps7TIC&c z+r^TqnM1|O!emJuNzIZ{@07JlNlZ4YX(;$b?KB@BYs_ z8SMHqC8N`&xh@Z<7_o~~|9#^0A*Wjum~`-;twmcVU3B6W6;>_(|MYpP5_9(KlV(x- zfAI+;$@4UDOgi=b8!oSsZs>__ko_ml`SL9Ea$_>Pln+OIBUw^}m=Z4FE6aevobxH03jjaJ$f9joLAWz*y8WfU?O4q#P9 zVx&%wjinbIyI53MD5YCrWe!nNhNlMY%ha~*7CF218_eDRAijHPgF>ce;U%WA5}Cm1 zKCsh}*Dvcs6QQgEhw>`zMEF3EZ?e9R>b-Tt7Y6=*}Q-hoM#aC3LJ+7s~h^ z>@TII@Vo>kGp*3hN1g0Fbz(pVUda8)*VN-fG$-~gNm6;r!JjRY73x`p1KX+=_fBVQ z1rC#338!gzead>R*ILK9bEB$LPd^5;6Nf^i4lAkdNtB@6fGxzPy(cxlQ_=41@!DwF zd?+ZD`OaXKxUuTBS$T1c6W(`;u`&w7U!qqxcMOo0lPeGuZ_>_s%aV3C4I7ZpH91*t zfB2w4#A05~+Rv1g@LylD4#q#bS@|ukiEa$?Ce|)Kt{*yoFO6r4OxbZ40XX zWFOH>i69ux%1eID=$c$L!4Rj7CNj6n!m-YE5PpYY_A5t-IJ3b(ac24AO zUVtKzEyX>Iq4L+SYu47*yHh*3sFxUEr@C4N7)_ZS$UUcfD?HK#rRa_s*%3Z{BGoW= z*KeQRjZUPl!}PURl}sHXQNONT|2ebS%W$qsfi|&Gd@A5GhIC z#*pxzKh5;>%;FuW&1kSn6c@!87b%Oav`q142EM)vc>F#K2HBLheHX3O4qGsk2ozMt zz7yh$QR(SWm9kG)^%AZRNrvkW|7Fc1|CH&8v>Vd^?HU^;^QU8y4k4481M91dYSdzo zF0Wm_3~%GaNV(<>*LZ+(m`Kq{nBeOuq`eL2LYu$sKzs7HeLtM6>9b@u(pEQkn=&P` zCLUV;+5S5(a_R;@&`uO|8M3CDuP@Mg>pW-12Hf(G)5P1IxiR$VGYf^R;J$4&PvFJz zX*Tp46|a-i(!TZdTs48P_+NYNW zaq#pqlpvYfSt;+`WBA(BbF|U9y&-D&j9p)_q0yylM+KsSb1fMu|2#@@pRJ5@R1~Ye zp5CLbXJOF{`_B-CE7uNk_}s*%>EMW^FE08z&;H;ScO1{tBokTLJk?jySJa8pwn z`)6_g*?T&QDghKP%q046Z?&R)y2Cr0GRw-oe4i<1;5uJ-ST*6t{BVk#n>JMdHoyv@ z;UF_tDBVfMMW`IkDk;$p&r;LSn9JT2e@&L*_AKtI*xY#RKpjL;MUP&Uf)q@u@K`0! zjZK*@UvXU?er7Y#ePVNElb$Y`<__&klarRGPv3*MgIi6BmM#5WL6?~JJyWU$n;Y+H z`T03^sIAlFC&^jBe=Lq4bMZmvm*fA&@YjpjSljtur^9X1KMvg%ICgBUag*!ILelOd zS9%6NB;w?gBybM8<;&lvD1skQh(eiT{uq4r6l8_Vm&*$Z z3mazR^Ic|S8yg$PXA>*uzw+)sd&fUm=LbiRpZ7Gn3}U>N`81^Y9GdD}-OP@Fe`IoQ z51GxJV}Pv{LRBI6oB_%qcBS%JFZhjkWYw0b%EnMOt*W8gXEPVhg4nSLs zk)8_Io3y{pus%ejBb_~he{D`TK`L$e&+#(A8bY_3@olIqeoylnb8LRCu1buxuLzs1 zG}6|-si&t`BhcQ;Za%?9U5pMbG|c6^U5{K%!xgL8YFVDB989o1%86KW&w<#)P%h)bFkS2jsLsh`!;``Mno*7CbYeU@G zy?0wAddZvbl5WJz|LRnBkaL9~p+?7!xAj<=%RQvbRzLf}(MeioWq#cJ?ntxKl2zYJ zi62iE@Y3j3h8Dsjh&&4vTOiuRA-&{>9q%|ma)moFd*>*e$ zT~CErg3#+irvms_KN~;SEG{zf=m?kUYGtCR{Zj@GvDRvVJB*Mj9pJpZm1_!zN0Ve! zdm!pemenojOO6~lQX75DV4}6N*t#Q4==(hm7AkLaUGYMxsUHav)2eY|mJb=d4%Rze zOO*7mX-lTrI}#biD(-N4(%R0hurs3z0-FiDib}hsdLfPF;%n!x?Ww_8_Wc!mM^qBl zqMvY_t??1KlemycOGhD%&a&0^)~CmXw}%WPO^!q;X1sY5VIm+_&??FntatnNOCCMD z-Xi^;E z;GkLfsJ(GFohV|SpSbmcf%DwOa1sQo_18>T8Q;Pt7wj#X=|p=;{TvLmrKTVlDlYeQ8>kU=qfoZ|d}OcKC4b`@7G0do6Qy zReEZ6Lo%`MdPgj@75C@KfzrYY$*&?w-z4zi*p-)scTC?;oU^hX&&tZW96gn?efOtc zP1W_w(MD=HO|J954mIk$&HSN4f#*X<#lIKDdZMIOG;;GPJLPI_s`F{K2)>~wd$=2G%^P;F%$*UYD1sO`NT($ z{;7t%`qf#gzwX|-V>8zN@!jL$e5jS(V;%jl;ea5hVI5T$_|2&#mu_UopS`OMpU`uC zh3HH3`}%&>ayFSm@G4qbPqs?;5lApxx2!KO8Jy=T`u<&3&oi{gH(E&l`!74mh$EGz zP@5fW&mVyR;O}F2EJA~!CtzHo2zYdyBGpSK<7v zZTf`jwWseGZ6+n~|BH{dfWt}HM{G14Zhu-@Gn4|80txSXy^a6w z{5(l05HTowJ6fTrv+ZHo+KosW^`j4V?6t8kKi$@0toa5WFva8YMR$8foy_TND z#>yOr_ZjAU#1t#tn1UbpIKm!JKe@V-`=(h9r*QeHD}|bu4~8)%o*NgejGOGuHj;`J zF^@*j0fAZP{&=>hRhd}mPIT}_d~V;R(7{V>`oT7zJ$tqgHYrzAFvWX6y3ro+sg&B)y?{SVY`a%;KIb%yy;qik<$q!c>|Iaz*uGkfF0Rk zB?Cs$!bVmes|!&kMiqS~Po9)QmGfNxA;A``*yX?n_wjR~ZSMh&mlvKh_%p&2-dtT7 zyGM^}GiWGGLTK9F{u$KVrD@|cmf>Eivj*=8edEdsu}<273-}~)h%J20r0|z*j9ey~ z=~tqku%;9y5eU12Yu8r;IUsC^XBn+xMePeRbh6C*Jd^PeN)6&weJil@P zOYxP39;;h5UCzP8)KpJVF-%ZOA=p1%HPH(4HY`I!`%fkuo;OWNO-@!SzO~1k4ts+x z$Ft=uocl=p-i03#stSauzW2ArzuJbvlE}LkplA3uGN&rUZD|=jdC`&vj8V;)>80me zlYx-1BW?Se1m3@YUuaT)FkARludjAlpK7JWo+>{GerbP+**cy=fLMB-rm3@XEqe6q z%|-C)=#c;`Z$T^}T>Wc&e`ac`Tx7zjAc}!%IJqUIqY@vQY{bloghO9To$VDDh8r(` z?Jm{_q-$t+RHfyl$mbT@mIMh~ctE&K0jd#SvwF9179&c?%+LY6&CD!ZSa1SNxNXOt z8+m5sJRAq?8szPAdi^4U>-nj@X#tUW*D3Wx^4r(9`{nDaK`9>o@guPPR^@t|#GQ1N z`;C{LavW&X=^>cL8oa+Fhw4Ks!ry_J{-&m;is2!g;&w*vw=cr7w6z6QDi>du>oAE4 zvjx-qTJu|lGS~B~)7l@QKZC9JYA?r^ui6Dbwt$43Gc(lB@|0=n`=--ZXfg;kr`IQC z06IB7fXj4H(Rn}vC5wIInNb3b0rah#=joGTd`^h$M=5VGAeZ2t>PIU@0IW@i3iRj> zxFQ$BCqIs^FG}V737D+>t3y>12Qfsf1x+Ji8Lmc4U0K z&^OBwnGoF`zjLpQPVE@*geYe^n@WN;?fChjTyaU(4^dGdg8I(Nzzo~Z2Zs5`mO7m*YmXbgoRsvHpfHlSX~}Tv~7&G zh15>%y$UHTRMAvD-|ja4T)G?cf1_e$ZtPYtyD-bs(1tTF^J(v063+m5*%yL#|3dFR zX2=%+O!fLJJmkjSB;YgMs9O@Hx*(SD5D_K!O8<=r2b7#HKA5B#9nTIShVjTlm-V|) zXf)v}{e#KI`uf~dWp-sk=QVx(tcnU5F#u}+eFpSYCd4alYcsVIP>Sj0N1Ic31$sUzg&$&HcLBaIgKB%B-;LY<4C z(b%gH$wK=|i38}mRy~%*ycy+;DRNf>9#4K8DPLUZW|X=PiR3f4t<2(RkKawG^K8eS znUubSCK4r-=dfAth9YcS{N!rP%v7Z$8VtSGPb)ed49l70$|#Ya*^1Kwt&F1q+;pR6 zb>4Us)~y}$7yC=@HU6n2y1B_J(d!$l+`6ublj(#`3)fGN_u2}V9oLS|cKq3-*IT*i zy@gre9^oWN^erX#_D?`EAh@STMY7ZrGTvI9T*}rv)wg#&*%;q*v!3tN5T!v~it-Us zf1JuSzmv=N?j;|3Q#xdRC+T@2BhTHp<&EXz_ECpgUYu@mn(R9SJ-h$*A=}LjiSP+% zu_2}g7a%jI#DpFf`D|=6(z1DjM_%%`{Bt>!kKg`$4EE4z0q`%q_# zQO2Pb%+ncv34 zR0OS2uA7?q99ia+JpKhLuaSb%2q)@%2+d>@Y`K2(wMGyP#MYtmHFgp7~q~Oo*45o;GV)F8`V)5_v{!c2MTwn6cRrM>} z-|+S_Gz->ELT{G*2)KbT&D9nv#&oybwn9rW>wlqLg_)T@+!!$#&h`h|c~6$@pdE~0ErkOCwx?le)1*ITqmIDbDJ2%D=AlM54PQTK&nl1T!kqjM$f zFQBCKevTD|lh^d5d}poIRzQm=4u1Y{xGl-a*Wk8zj}CHBTzt_xf-~va_s?0%7=w`A zzPF!x@7C9IvisTe9@Q(r>?4VrrJUkU&|kH+8DDs~@!4~2CS7W6s@X$T+;erP?3H|F zJ~W0ACK<#3Ga3P~wbiJKGf@~D#4>%Y)G>2Rv@$GnssH9?A0pUq2`HtWuz$bvW-87> zJG!9AdMCTv%t!W#0QOHxgiZ)jJN-hN-3U)6L+9b`|16KYEp3^FIAJHCBLDDMzar|d zC|{APe;yldQVwNj7;j<0k^#nfo4XL5UpS3KG$@&R73O_&wGM}NRyG`fnyn6-ia_v_ z_V$~>wSLQVIkvq!t*xwfJ(WJ;J&OBj0A22O5`7aSK=Npt@V|E^Tfe9}il$?{%Rcgn z$lp5y6}zzTOh$$^(^F~KMg&KEu9}I*84Xnd?PK;A?CMqQx0Nw-F0vx!7)J;i=?T6W z>-Bq{b@3EJ7JQ>UzPldzj}8bvS^hoJQ{_W-hzLy2yU7#{@Fk~El_L{F>zBD@h9#gqBWVbq&-WWcyw&ory?pz4b z*Augsj^ZLB-F6k0__LwaEXE`PW*6+wg3H{~+nZHk*u}^_NAlaXy|bm?%lFZ@L@@}-{O*T;3$sQ-rZWFK?tkta7X z)U?3n%*}?#BAf18F%d1=_|2kY=aP~ zA?EAF74>TYPr7NH_2HpZu6@O_Hp)P_^cx$Ws1)?NRZs0)?a#I@VUGar-D#{n1omgF z*LUp}fEL1C&Ib(pts)Lzjx;HiDNTcmH25Q1oC8yJxvijabj{UZLs~zSNTSLGK1q%L>RRLBg$JHmW&6!rX0fa_bV;JOoriDZeP&-kPKR)is&v zYhe;R6av|_*YxMeNN?Z!JUENjuYqwLMGYj5qsO7K@{~`)l7$Ml-|O2jh8eV$2VO6BXj_Pfn%;*utDz+r(&2>nKO$ItBgS zI1~&37@wZ(FSP7n8^p0t;@IJRrqveR8pL_KjGdfwO;KX8>7_{vjTb*9op{%*(Y0vrrbbUg3gv8HL_@EpGPYN@>I+Q~n61IWzG z6ujN2l*1MLOjwi%1+jisSvg8v#8OLJTmRroSL3c@eYAbMGR3)=q)TX?1nwlIhltvL z%YU+86yWc|LT?AT{zzh|Nq_O}557yfR|{#QqS^G%bCqR3iXpzgJ3{`zxVRm42-WOO z{dRa0-{PpleJO5(78bhJMRug{Ve!i#QJv4yv4oI!`u+2=;==Uc2b=bkN8vW24r$o* z4ajH%ZWC-KW~dd@2dWOb%n`o6DX6gRdYt5H+iSpzkfAY1o=bu2Jql@~s>@M$AN#TE z`HYg?0*%f+;fI1VfgTN?pl3f8)T@vmDW@V+eR7d{%dA{;OoQVn`E;NTSoH2TzHeO5ZerDPaNZTEZq-W@c`f9O>;r#r43wE`fAaSKh%8?uNxKI_WbsLb?Od^`lHm4ouR}e2ee>tf z1zX6Ulw{9%(~Pwm0mF(EYkDht`y$Zv6boxQR6__|MqZc0;}v)_p{EZg(+u|?LLmm! zlgqZpdKA>VA+aun+c8fy$Cv&i(b0TVSU5@u*Pl0XVRi5!eYlPARc-A?Li&|>8|e7M zhBLHuy68IishL1q#s-u@ywKFp;BBm^6si-Z9}(eqMTB0{DLKvaT+cj(zaUw3GCb9w zoAEUiRoTIBUw54=IXD29y!}IZ+!@cc*ag@Z1wt(7JmjAFa2xwEF0zarV|yZNbtdKu zNoo4Ikz{~>;;13t*G#=5=Pf($H?X&W($kHUu*SuwlWmd^|7V;g_UKjY1Ma~K?SkwW zXW%8MTY64Tg^=$eh98<1B}oN;{GR4s_AHvMkw~{uzKzCHt*JUGFys);MjRneYHQN zQ`oSZ>Y@v`r2F&BFHWgPXJ2w-Li#oK^rnyF;e7EBiy~FWuOL9&1bX7xu#OhZh;^9c zaHh0Fuisqnh{g`Q7=(23Q<`>zwsPrF#;bR>=ziy^Z{ZVp<}F0%cn=XblHLAc|4;6o zqy43h+j?4+s4lO22ym9k$w%itj*?>DBlum{(o%Hxu9+eM1qaQMd0Tt~|Uw4sWGb;ZT!iBewg=BN6NqO;RTlF_nTPpevN?+um`yL0ExN$#wF+wj$JRd;C9c^L3MUo3bf4&hQhjYuI_yWH|1gkOJ5pi zP4jAvfd15g0BmmnJ-g}hMylHv)%bJ72Rk_rHtGziP+a7Mz<1%3lIll2eIuiWzM3Fg zUU>LHJnl&-?l^j#34z|Buk3yYNW6j^ppk9S*Id_fWxMD!_4I5XU34(K7@3X$ zbsMx&UB@=>FZKR#3|W0gAW6AO8~Z$9z~_hy=4C3Mr#LQ8J-z}9naG901j`IjwviF^^=>@Vl8AuqYK-#*+eMoFk z3L(C{@KBEUtysq)t*Atb?he+-^+I+;#ktB0u5F0_PyprF7AaBw7y3Zx z4iBf+kjxy9{Mr^f*;WZ1?jLybM1+RvG|D;26ja-~sB@l|l zWzcG%Ae=gtSpe*D&=xOMIe2V4cI@cUqoG2t&qMo5CY7qf^8Ob70G{bUk@2@$VJOsq zCizdO)fnuOwxBk)eE{MSd}?K0-`~kE6B(vATP~!v+n@L%vGL!$j-qT!KsMIZoSEx* zHSt-rKXh0}dt15?A6JMzFqe0zjh*%YsWaEFpY<2ZB5h-UyN}<8Y4%sd?Q}*GT`a3W zgGqnvzfc$!dGD7AD0;6QsJZMJ<>B_8J9iG1A_SY({KOA2sjGV)SdTnZ?Nqhy z>6K6O(x6GaM92HTR{;95;>{XvRQG9iAAVe<{wg8dq^PJ!AILjieVNKUl|?RFb-o~R z;lP_|?hJfFLeZuf9y1@AS7#fu{SjH{g?d1CKKAYQy@ek&ySDEyRa zw(aELQ>;M2^Q}w^J~)UZu@Y`+@WHJ~>2a($JGa)4!OtduJ!L{|b6G9t?rCWj*YMj? zB&y1ait5R}_dL)784une`dBpxo%nEh@o3Vz=QzloHBaGc{cvPl3kn*Dyh|SbD4`?`^>^aeQ_(~j}!$-cy z?|iXs7z*U^uR^`h_#&afCAjo3rtpSs|2JEGoJMTStaQQaA{x_;yGd@Gt&_V_<=vQS zfb7Fe^2PnGYIW8TSfMow7}Kkn;A>^o_{+{K7Vd|Hg3>KCcRQ-YAS9u;+q!O$uF1;+|&Sa7d^1#JbUNM zQ?7N5t;BN4aWVlLI7&J=XxrmmJJ7-&zEyQTd zGgJ(i9oARw^#Z96Z&L2ISO^|7I{ASUKQo{6SEe00BuB6*0AiTvHbrJeq0m>$$>N~r zU}zR5NunzMPblbhW~cImzG=q#g3b#zDjJ{quK9I0-h+1Ir8z9zhc1BU%*kbOSpl!87K~DxhRa z)Gx9t63}@E&WMGdvG(0i)p#IabkZ~uvtCNP77*NbX^(>_jrT*BZe>k7yLao)HfIw} zhG?^d0_AFqlUH3;W6ym213N&ID|ouI)2X`W39YwV@heK{2W*1Rk2F?*)NM;DF@V;1 zV!}p%Pxml<=&v8dO8Jbspf=ZL0oH)Rg+c~2i7h(EwNIVmVUG0`$1Gj-!;SAU^lgEW z2^$u-{hH~F+$!u(t8O)VfaoA6uAeR*#{|246f?OoP~~&R`l~nr5{Z>wA*HR1F$7EH z+N|=Qmc z!iiBf6bNA)XOZF2g!?}&-2ecYEO&MmKtYr$prIjuY~Sq4a5LC*;%4#XEvQlGwz#_R zfsq|R$_{Dh4y2Qo#&*wotroEoCPE}vx+~Tf{Nc_aWGiB(vis@4+E9b4l=7R==6S0P zy9PaeEoNeRByCUFE5eqSHUh%cNH2_?aO!LOXl~X(03fW3HK2dMDn-wimlK;zezIo z!RQ*&y&N9QPI#}ZY};~&iraqwXFH)_m-Ec?S?(UclH~Z$w-)hYA>x7q;Ysd6fwU-- zqy+EtE7T8l>XqkG-$+@lP(iT|85Z(Tk)7ZA%6RrxXyp|*Qc8Yw4AKB&o zdBAOI?odSI&4S9bZve=Pd{(Zz$ee zvL4^xECf{F2W%Q0m3pmd{EEI>tA6L&g~fg92xBiy@c*wsa@ZDtd8^%-)f@!R1)`KW z8%z74C$oXGd}fl-%Elr&wpjt;Ud*uPxU{QFVh@Puh1OkeTT;)@;vvetbjtf>k0yq$ z3{U-9NPiDU2E4@IXli=iw(vbz80t>$k5lVY!IKqLF_erolQMN2&eyAG@()n=HpSRC z5+I&hK^3fVB&9d1wZ|{#I1IXjp)z7P1DMWg=$oKI=uwz}rj6TP>3)q{lJn~;c}B%q zu)7pOO3)3@VhXAKxLW#Kf$lJ=eb7brZ z3VJK;p_&HbJdm?TIS#xCNx2&2vbh;&(ZYf|>BQ)pDEIpvJ;vWQ+4NGvXISXp(!5Fs~K^6id`%;Gh5 zL&JTew&9?rW&-UMc?Dh;RerCRV1WiPvA2*MIxGOTM*5EXJC+rH3ip&ujRZ1(u&wc* zfppsVEH*Rub`eTAtsZ?oK0BzFJNVBBPWpg~Z7(Dw^uIuBZK!^qc(C+)3thd)UzX-n zLcAC%Lra!J}+T9R96 z!nyf)51@|qDtw)M%%X=L4T`0Tj($%&I{lZ*hHihbIbQtVQZ~v34e1gU`QRYF-2YdR z%@1Z4!;+YUq(uIzNtx;0lQrNuhomp4OKcHnsif7P3XO-q0bPx^yiYeU%k)(x&sa)b`ZcM=NyC26(E#!D2=xA4?8oe4v%WIGud0@!beH=6*R>ztQP@+HYfRT-4d{4B7l4YMFKm`V|B9wmS%$ zZaJ$^*;RB>yFcUOySjLYlyT|)eaj@NNWDtWR|b`y2FPi{1uc#AKVw{x=~q0=w;a|2 zfLZeO4?obEmQrgiw|b1)CY3$HjXKDe3yLjl6jPNSyl0>o$?s0*+Xm_Kf1UsMfpwnYuyl!$*RRC?slO^x)&9=6&@hCmq(O8_om+VQmcsFUo`m=a{`;PL3uz zB7&z;2XP8BQ`Wa>>p)q!#H}Gf5+?OH zPKOpq>AkyE&mXJbKpvX$ujFu>%2!%%L;$I#mXsjVCGy|Dyo=VSphJBfne^=Yp)RRa zNOyLLQvWg;zS?#(@)*gw_OHz!l1Wu81m_1lc`%z^?f zNV$?8E8R5#zfG65v;bd@o|VdY`9BgbJy{0L2HpLh`(O&_N1DK4C0+XKR zh;ljegygqHURx>;Yl?cY9)O-47^;BE&GoZx^PM_dWGr;;hlmb|lO6YllLrA)@Bije zB>TKMh7ysZ-hm3we+~W}O*lk6+CcOsmG&2bGSe_S$YD=^k0hX=uI>YE1q3lj8J0-! za-i<yw*=Q;_5|2>(FOwZ8Hn>Jls^89L;06NNL=)OhbwlSe@g~orctMLLA90)Y-G(^ zGtKR1&+&$NzX>Mw!E8b~;2qSfxCZpV(zlvqW6&%DsSOUO85o#D3PlibwCw{OUG#SO z>8V>Ejp(~HcmHEOnb2+T756MQ)*4bEl#hS~{Oje#rwr7(OWfb8Xa3n80ycw6r1(p$ zjJ|?gn&}_rwBU&bHXg(|`o_gYvEDbTTF%P=+GRrwl7^d6OJTjU36|u%!Pn{^5gIXQpeL z3-(|kgQ*{#H3z4Jg@C}??I&NZF5ibxumKE5a6i5NaOVUUi)r6`3hHY*>z_fmN2qwn zDt$a&)MXkU3+}N&-1#e^kDl{;Jpkg!6g2(919#G-BX5svV^V@fEf0v@KO)fQ7x%(# zqEK)*XbAmG72>0t8{rx?RD^jK`nmTED^IptFb2JaSw6g$%m&Sa#2J1CT}XT5MRRw7 z`fv#~IT_P_S9@8|^GBHcL@jrV-qg2p&K53cAQ5xU3I5j^o1&{%uM)v8c(`8j?$3OQ z`7d`RyJhGM)IIEr-KcQ!C{ci%XtFFLE`DQfbi_0`ZR)$aZxiZXkTk7Vv2f8=Vt6nB zSuA1RN`WwF-&gK~-Ac>6s~jyz0A2+$@1L13*kXiT=Zf3dsMEKYv*2KQ7|M~4&UTvr zr8QP={*jIpBY>EC{EuX@&J1vszIjsw<`sYh$UVo(ij!BEIw8v(+-%cd(E-C{2kW_x z-|+q%w+sdnp$1|di0Ok1HPZs1gBYv3&n!e#`_U4&hEZg!>0kVH?m75ZN?uyxlrDh; zYXbvSgf&1gIq+eKD=El~i|i=-H$bbzUqyonr9*LIx7+^=CeQ`@GoRAZDv|ijE^KIV z|NebQxZrxTe3=q=IY8<*J1x`E2HF@WVf&F`my-;q z8?~E4N+B_NjOu6BK5=u8fYG6nys`^OnBro!XzqNu8lbTR*Qm}h!>@9VcPF|6F~+|Z zddQNaa4#n$b(6k02lrL-*cr!BCPgPyUJXhO!?QI0bXZ_l$0}In7Lxu}4LDs~-rx~< zo~+D!1(y#GE)#@Suljlh9Zj11m66^3{KTvBipP&F%=!7LlfjKGJph9*@+G?!T6=sS zFmjuLJ9m2JBxa;pxT^$Amdya4<)DDd(Lg4x(l2q$ohSF|gnKOCi6HmASB7O68d}YW z6*&*cz>WG%x|Tj!>_mQH+h3t6Cj0RCe$VO%$}pxMu@YQYh-_CiHO+xBBe(~EES4=y zF&qB-cYulKhTZB=-6)qBzYqszK9-y#tI?^kF9!nks`>S-)biLGu!lGp-E%!tL zjE(`?^4*Z7axn(j3(U$0_}YbHRDz`PZ)oG2Kmo>;t!=W395CZ>I|*a&f4l(sH0#Gi zMRi=GDVekpr()4YnJvu2lcB9eH?m=;A_)29kdY+C;gog9E4bZ$po>EOv}Aw!J&-P24;vaAyox29J@NSsz`L zbw8K>JHcyX3h@Hs9xHCy!Zf%is8aap%eI zBpDt`=NHE@$Kio$#sKjk)4h3=gH?|)j#@#AQ;vBQ(9)*hFs7q05`&|nqp0f>!12)y z?C9~u@LI@y+unWvN|Q?fp#Mz1m@Z=j0sk3vbA*(`Mn_Tn)E6VA14c$I zyg}XqhLxqnli@UX=nC#oeZ$6b zn>_*jDs$@HH0jV2JXqI>{oY^3%&5M3ZBhkcSeP1=4;9#eIuyX7LD=E27+!Ph zGnV^fGuq5jlo-?p31K1`#L*psr-`fv{NdvZf2m(#X-h;PknN z3Y!)o+wcWQGv(^;IXTv}?fhu1daiWT#~Kp1ex;RhX=LmQ;zbw?$&{vJdfItU($GGnCq|EGO zDJ#JS!%@P3h`R+lZ~nGwb-M|sFP(eF3ThlzrQX|@&29G5K60`Sh2i`#h3>kr7Kazd zz3ZSw{U0ZCoCmATxy*MmJ(j^`s$7DF0=r=bTtJOukDsrWvY1(|ES3>(DqHN>;BlGlng;3Gcha1m04fAY0jv`k9rF>{pV6a9E!P zm{x&T+H#OlgX6aSrzx{{PMgH>f4OhiN8izrM6W_2{le|g=i?~pC{jKOo71T2`Ugz< zsDoL9Ony;73*U9pL)4)2!QaR|ULvH@>;K?hu0x5DwLRO>@#dY!MPOR6i(D7>3eFk# z7AzX24ufuPb8Yf+JSO`S&#vz8=X6wl9)v9gOlrQ$22=)%Hn8~bv7iP@1AfUs^83Cu zyn!)oTOs+m&`yh7^9v;$KejXNblq}psc6NYlL%y1?L`LnA5_x1?!P}jLydn>`&rGZ zmgmgUN;SIFRg920N}VfJ;f?sW41yh8Kg?MuUHF2qQ3oe5!Ps`bRT{i2ZkbQJ%$_CV zl9ZE8pM>5+lUvHz?1im_J*mW0Rq|{fu!a2fwB9+y(nX2bQIV zx)^`XfRqG-H&XD2_#h?bs>j~HZ>39mX7Jp)XZAJoK60l(wuk1#Z8+*#dBqOQ@*0f0 zd8~|?!=zTv?C;;?_SC$u;LvZw#qke7o4geK8s-aFD1&k}_ZxMwInI1Pv|rLa@}8rk z!YtB$MsLB0UBvkckP>Gqnh5WBqDY&P!3X@n_~96 zcMMmVZJ_rt zhh5{?k^Ltt0`sFti1OXoe$cxgb){fQ|21!F@?$83kyzB9fGh^yiO~6R$YFh{ZrDW6 ze1bu-19;P>>v%%CVQgY+T}S!(=T>%|)CQ>*X1Cy5L40tpaNetqjNbTTy9|LRpPwZ4 z!uReBB-Kas69E3X0rW#@dnvQaZjaC8OQt{wqhi&r{jOjq&zSlD8EC)G&>C{YJj39gY^X+0uFqxv>Wd; zq%<+ZS4@F8Hrfy3J)8%@A>fg@4@49nFBveezPcn~y17(4jur#+q{ce$t~0~L!oX?R z%Osq4k)!AeG>ScKuZ$XIwS2#w#PFwO>?v%lt@C<%IuR9AcIg1q9Ttjdu3-Nu(Z60s zfUMs1&?-|^@_ScE1THcFk2oOAM>)U&#s|;{w(Ol6dkR^pLtoT#do`u0CjOeyGWps1 z^1}kGo(}A|Rmv=s2R`kTnNFD8*56&+3$8g}J_0}9)V_Lk0Far3`ktUoxGsR8y`S+> z%LW~UL0lMhLq0ZGFxr4(3-BU+&Cn4>QHV;YGd20aE|=qUK1?@7cP;t7Tl|ewU*ivl z6$>>Kw+H+CydLYyP6QiYEdr;1{U=Bbg0Cx)uFDbIv+=StSM-mwsbQ8CNJE&fOf@vx zhB5jBahxpsUP~mvTj!?YH|&Dn!KBrbG3V7ih$rLQ>VK?`{bSQUHB@94*BJ%l#kauL z0J7(E=oGg8L16U&GN%ihT99T0GF@9}`Wl3@-_$YG_7@sx@epx%|{nuMldpvPh@?(i27*t9e? zJ(N5rDAK4jwmQ-|Ri@eXv$pcq+pZtA*0qTRF>h06q}J0ntu|-4H-Q97t*Gb$qUcfk zkW(*EzRO^crfYA&q0>?B|hh5MZI|M;em@26;ndi!xM!RaI5qv-dhpt1q?fIpAJ zenxu9l$l^by^~}a##g;g`{IYT?@|-o`G6Y{-75EXPmPWunF>xF^7xjOzk`ihOm)#} z4jdbHEt0E`1^XG`D=u#KyZ_EY8c*NF15DBo;~;$lV}-_JAZo*hGb|XHhuC&njCvLY z1^apBBspcV1$f801X1sh!`9x)hj3|M&A%Sa{ZC<%@rMUJDT=!TcpT_yPdQfV!~G)w zlk=~I-A>F$>O21~JiJF9_+E)Hr+Bo!0(>mCz_EpAUDePqnJNBtX1^$4WmeL7n@G!f z+w6*!AXq1wk^z$rvalnw$N_m~tO$7*AhB#Er;Hr^&^)nF`pNGqKD2LO0D^Can4TEE z`|wr2h=BOL;xHC^%;20gnEx6q*4{NeKhcQfWM2K@DKD?)r`%FUIR>-oL)X`mC;ll| zo}U`|veT|FZ~ejUcJ36TO3#I%kP(x`_0sem@>5idMKi2BF#rb!z>iKCI{-DY3RG^m zO)4;A#^;Lcq*WK)&LB<=T#2xBNEbQKT;b5t8O~pJf0{?hL@l5_%2o;9e%e{hJRpbA z)2cZ0X(a+-JmPiRz8|*)cYb|!n(ue6{Np`#{a#+O+WCMZGknA^FG1?aftlY(lTF~d z7V3DY#MjL|ZR1bC!&&=E*ZC~XY{P#b?7EF%pE01RaCcD?b92BS?)IwZXNs+;hx{0Y zDoP~fn7xy9-xIEDQ@ca20P)_GpEw6{txPoew3+mvRLO-^DD<{a<3Jxk@d!3*gh^kk z7@@WnS+RFU(7wLJb_4;}b zypkA|`7^syo`d|SoOBe4H^Ml4%_jSZ3!|?>ln7qy^E$|u$E$A%-?V_t9sdqIsz2L| zbHgR2xEZ*@SbdFwbKmXBzWb}wA3RWoReQ-aO~FRX69~Lcgel6gFmu?rRsBqfkaYHL zHs5W*+Ec9=8{nI7h8v$L5lH{UP*(qQ(>O$(+C@MmI3Rv0M&TZsPAhdB%bcGeiVU3) z7x7q8aob#H>~#@9U#|_TJC4_j=Y^&sz6=ZvzjHr4t<0r2E4=Bx;~|+=cgxN<95pAg*gH z!O-KUA>d^cz(9lmy(>(PhNXCuuUG&*36no%m)i6LCsr#XCFGcM4TXAEldNC=$I?$r zrw-L+f)ov|W~s)L3+qh#Ha$J>DP3!tjbam}$8kogZSFg7ds0C#tgg-%P_&(#Oy>)z z9{O;2ZF_!ATzDp9KaPPCfNtNnFS5lo6z@^to{e8=BEroZmD$f5U!%ofr^%MS>g~m~ zUseMxq?4ufw>*X`Hl^=uZjJt4qk5zGGU^WP2wK1DeDw)>vte;@)>G{Xuy-WLOL+Y} zRFuDN`H0Ai$f4pfnV5CJ%adzTyo`bhmtr7t3gzPgr4lz)a#Q~1TK~%SFAr2aNAAVq zV~`Ta$j}u*(F|!Sn35+VI}DCZareDkV7FH$l5X%~KQ~9L;1V~agEZ~}u#kCKsJG~| zsK?p0MOWh>o_XXs4Uz~e}&w+@Tq z8<=@BcI%2WJmhzSazbeLj=58J@_LzMu`tjUX(dvSy*X}iOxvfiWsUlE5P4$@-!G}D zIk(}Q!u6yKfKi1;nI}xmP0f)~TBCnwVjumy>7k6t#8CL~A{Uga_$q8isN0xDl}PS0opt3+j*B>h>yk-fmDwgh_@QLx*|b~mO#eP z4?cdToRUl>V4+>qThr&a{Olb@mzQ=^=IWUR4v% z`{+e4t%O5J*=d6X3a0Im{13- zCQiR%z@9Lmdu$8RaHctxXD+iCmbb=U($Uke?OT?< z+s|Z<1#$V@5;B|Z5x#jjjp5IQk}$-aaFG+DRebkS-BmB^Pz^$_R!Bso@W&74uwJnX$dIe~@ zYcsRa+=(SO^5b4t+z4A5uH2emo)FGk7ZTPuUc9_)ui`lL86Vk6O0vpi%T6Fk{%D+y)iix=bgoI<{4Srt`P zT~Jwh(#d(+rRChk8a1KydyQd2eL^6m8vH@D!3@NoZR`YI8BKSo@y zW3d`Y9}L91Ii((VS3awTGUus0d66o#<^V!+%DfSXZ_9S#}~mf=4}~y>#(BDawGI?XvrCv+L+iHAacxgr8J}5 zg5|oo=SKsPk9Fue6BzN>{LJdKJT3`F1u(Q&)Qi|rsEUtLTk|9iWOB2;xz(hjI{-~@ z%y>|r0Co1-1lfl2g$Li}h}1kF9Mw^{5q#)WKg)3!HAd+x;6^!XC%q40X$|NuRhZ7r zq@`WuO;Z)LvRw@Mr~zCHMy~0LhpD#ZT|00P>BOAAee9_{pvBxkeX0Y&k~7G@ea_Xo z3gyU1F~iV0%_v!qcvR@_EcsowrJw~#ef`Q>&PUTc+S`ezC5vfZsbR*w52bU#Ll)2{ ze^4yq-;E#}Y};eRz9;Nbs*mcGl%u0{GIMVJ9u>S@Kt1tPdzuWkdttS1i%_wL6a_8L zgQ4>C^K0l=0;FA#ZRy~=+#b|mJhs1o`ZF@-d}UAYiEd>d`-X>&Wn^UFgdmnmR2q&2 zFLqM(#HjF)tt%L!D|AOqe|pzOk$2V{ccd@cizf&UcT z=~OR&IyU>`2P3|ntqy1NGGGDnoAoaK7HF0XK*97you;6eSTQ6NdoQy++I(=|>bsu( zA10Jr~jBs1!3dl5pfeA?4#8*-@!rh z?9W3cB6=k?@q|28aQFq(zR%=qT%_3Urwth=y$L8v+zNd}whIXy(DS>$YABgd-`(BW z18|2(6z*VpaI4Rs7wO<{0uy;2ckY1>i=o+68i$b3cJmmoc6!g zVtQ~EJ?F&tIpS7#cQ*)VMq$eq1bK+*okMpj0YjJfeAN53E&aE6Ywlc83My}2#J#hS zGDTGoGPPHHMThO)*w)(`oQd?&+=~oGsf4R8YYUZ5 z8_Uevgovglfw|v3Q?!@;t@|lW0*AcwI;)f}@tg?JYP-Tj2H860P)6?k2!6)5UtXT* z_ac^b4p~CmXUKr?lM*#3i9#*Igdgw2oi6U{smo?Cicub=`%5~=y&ZYHs_RGh{@UqW zrBbBjiMpq!e#LsF+e)Y07$%cuML;7TyviQu&;0ePt8}w7GpO;3-!5y7C(({!aurLF zor`M&hLvUS1A`uvAEfj>1^BNR?Eqe+Br;E=p*4Tc;Uk)RUs-jDWW?Isei}eKH2-ye zEqb!?8?_nm&cAkd^9RO@2YONm;d!7UJE*i4wuG|ep5LTv{wCbr(?j#hFVd#b?9sO5 zC8xjU0YMWL1UP@?IF09f?l&K7`y+kyw@EEgzusPtS#=o0yw>F&c9cp4y(7S6Lcm~x zit-exJ7FJl3iSXTo^oI$SWTq(t64yKqKRU6xC~FiK_lJ!#*gV?@GpIT; zxTYS7dB#Z^yb*U!Bq&E!Pw(D?2NJahDsB`a_LpF(cg+-J8IEML%drhyA8xx*fummW z%ipt5Xg5X<(gomeY}K-u9I|$M0$JffOLcn)J(0d zIl_7^`vvYU*v_(=ja!$bG~Ak(0}+nZh4Sjqk91uqCUgDgL3Hhw#5nsRctkIaB>KaN zw!hLf2i2qm(_LR0D}sOyLI)|@$VGWqH;JSRQY=h%@53KJOfTwoR06B=p6pg% zyF%xKo4ZJfqVw$UHfdY53k$2fYjDug&Fw9 ztq7%s3B2)o@M_{ch(BxwEvitm8<$53DN|qGywSc(FS8PY-Rwx%3{67}XKbn`+uT~? zA6cRi0ebQ#`uh6G(}!NMm9%ciL+0GzyTCt&dY7>e;G`m;v%>I{S8c*SIhohWBK_56LX1%DRW1N?J z)Yf*Zf_>k(V);dR2ZxN~MXC_4nOQSd*UjMp^iA8mztcvjk9W0s0pq%`K60n;1bcBRoKRDkajOAnH`Yfv2O_bSN42mc^=QYFekHq~xv3 z-My=Ai<*dT|9*+&wtz#;ew>orEU0X1R^m|BU_}rBtY(wtw>x{`qOecn) z<&&ROXr)OYVuW?PDT&HmTz*Or3d&K?WguE~amZ__s2D3nioW>Aq*`N&X)gPlyM}0< zV#OnNexgaJNsZfPWoEW$WD_*c%Ack`;P7*V#$RAaqz^o7Uay?VT4HUGQNQ?m1lcE! zej~sXE#K8BTR7t^?rMt#W!+LQY~XaA6Gt=4^T_?Aa{kYg)uZh2$n;A(1lf7q>u}>V zWH7>X$ACGIyv_%3LPVWrvIquOFvE!rfAK=+!EYLP(kY+sf>8LurwrARkB>^Y=3d7t zW18~!pfP(zCNESm1&OU&kn5}1-8pBxvK}YM5Y-n1zDT}ru|FToUOqwUa>ErTMJzyjuj>_e}- zLMoI^-^QZvG%4u@qqIfEX}z;a?M6&wI37qL!UHrfd*r2_2@abjmL$nwV^ULDlu}fa zr{SVBg$#)pgy!l$s7?^l0{?r<(C{{-G|AHil>1p8vQIx-_&Qg|YWNQ9G!$%pqFHmU z#`782ytz8}3bQ9c{2@Z>%v)7=4u=i5!ClwsZUWn>7|BJC*z?z2KzstoWdz~D(9ADhS_jygrk>dkv;U}8 z`jRa&G8@o8bvgWX>$T6D_LD8n;G(WgReONn2KknUuIpod_@m)k?3}AU_P#Lfq?vew zSQ*#03#64(+Juz)di7dfZsP(I~dBB4LfqEG!iLV`9L99rZ=bDUm+2 zlTn!-J29fhAe?i~oc2BxCrg4NALkpdB_`1pNccy3ACOS7-rsbyzQv|787NfEKyb2! ztPV|oIiy#8?R-ue&+T5a>L4Y%=oe5Wy>pzGixkx_`uRN}uj)dv>)62npE#K<$(R$y z6&4h`1K#B2=K>KNe(lS*hR~aF$H`l?_9LU`^tZ-J?%%cszsaBL9eyJ%tFZ9;S+4<#xt<&IO0FGSi#Z1`t?gd(#OZiLil#E*ZWUrP zQ&L>qBP1*^P`&e8>?$Dzge?Hu!bNi#lP%KFQq2-AO(+-D@m6*z``V7m0hQc8n3a}x znNjwNq1*3LG!Ic(`IfM=esxDHh!;`r%Ax}4lYvIwnwERrLK=G*tXmu!f)_e!K?q7m zy_@&yT;k8JrO~VAr(tHO)X25Wb7-2ZrUtbX@S`D)Qs%#ZP-)L`OGp;$2&HIqtX0Zf zS)DhDQN~@@q}Yc2IrQZ@;oUh8KWqjqIgvmEQvhUnr8G+^y}0#9I68+9=^FMHuFSk8*qb*!_gABLp=v!0kOPiUuycbeJN2pqW;4HfLxuQ&gcqh4C?d={( zS#H!&dmfX)hY%92=oR@GHFPrujd;$^afVlW#T^A#WfvMVg_&NBICCoIg4>JdX?vd? zmS}=%ovoQ!w9HO|n^RHT`Hf&cr!!UAMVn4fiP{T2Ox^9zu(PMsb2b42G!QG50~t6d z2ln>md*lO*IOq~K7jyoiSh|3DbDNtHUC@{jpRKa93Bn>e`I?%{48pfD!T2%-A< zG18!@-t;~R=Z%tOMJt97u_1Q5A7AdePDV|E>4Q|%<#}J$J3+~;G4>CRS$M}w2$B;| zOl#}`*EnGcEo0DJQ&feiaGZB(fD46xd@4P;hug4>+4In;R9f43?>eOqrANL4 zv1~c@#Bb~6`NsXDPf_Fa7IZE%f@G-k^*#FK+iQuCW%U8;&G%?YwWZ06Pm+*AW1fqS zZIc$w=Ri~5>}N!{t0Cx(55TU2?+mgpIGaH^zLD{Dd_3ns#ls9c3RxP@Fuff{@O8$Y z`C~yQSND3WBA9bo*j0&%#CcamCsU0e4pu-_y8BW?++nufB3DnFH(v>D* zDe|h0w=}a?MznOnK20m~)3&6YHS2a*w5 zxSWBeeOegU2|L>`k@IaOl9oQu!;{^pE=r)4&e?GKK=8Be!SfJDmqM@v{2h(MtA1=@ z?#*m~1rXZSa(BF{tLtY+Z;4=ik`s2g&o3khDtG+e7lXm7Q(7{qu5T z4&^e_RjOUZulJP%^Kx6Xwq}i=MxH`niC|MqotsLbz=FrILE1aYVlz=YweXm8&CPY$ zjj{guDUf>Sw)pfc;T-@+4y}Gdi073tKJ7yro1P=_dbSIK1L}{OTkl z=4P!8*NMp&WJ(P?Va!+eDLedX^lAo1d6z9fNIL5SaqEMz#rf_I$wj`Lqq4=$+;-|# zR<6BV#<+0F5&0CdxJ^U?1J@amGz3! zr7Q^y29)&292T#UHC^VmA9PI~Epywt!1fM9q?265CD|#lURcvu+4zcthMnmN^QVXU z1d*#Gi#9=*{ug{WBom*5K63s8JrBSE_U0bU1kt~mYF6J{GqgU59M>MFhvgCLSnoejp;e%Ek?Zxt~dVj?pJm=Mvhq>wg2`iMI~lC**}tQdez zUk>x{yczNBp!sIPOJqIHvF0R1N9RQB!#_`MTi!q#U$PYRw?)qDqy|O zzv>=>Y%5ryKV`TGK`(?dgBQNm1MC9gQS222@kLct5ac8JK_~88nT`k!l`4X-OtGPt zZ(s7^KtgEYo^bT>S>R;fGdp<*V1XD7afI6?6CDyF{;x~?y$gci_Q5UCgXmY8Mj4!X z$PgwPVRHD-e~zG|8ua~o1i|T{JPZk&Z8GFO{=Y;0_fP*j$ba1-*UjPyGqW#g?_j-0 zwP&udz#r>H@A_w||L^&KNBDmZ|9?Li;X@*0h`)Ah?k3!f`0gD%(!bB}U-u@-zTd8Y U>_Gj=4Ly1le?uW#-pK2J0G@ZA82|tP literal 0 HcmV?d00001 diff --git a/icons/manupicksicon_small.png b/icons/manupicksicon_small.png new file mode 100644 index 0000000000000000000000000000000000000000..05266976e6d898fde1990ae869572dfeb3340ed3 GIT binary patch literal 22384 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