diff --git a/sohstationviewer/conf/dbSettings.py b/sohstationviewer/conf/dbSettings.py index 2a913a89d55879d55d25e256f70905e66809b80d..88989c8994f03767628be3ea36c132e2f47a9e6a 100755 --- a/sohstationviewer/conf/dbSettings.py +++ b/sohstationviewer/conf/dbSettings.py @@ -19,30 +19,30 @@ dbConf = { 'plotFunc': { 'linesDots': ( ("Lines, one color dots. "), - "plotLinesDots"), + "plot_lines_dots"), 'linesSRate': ( ("Lines, one color dots, bitweight info. "), - "plotLinesSRate"), + "plot_lines_s_rate"), 'linesMasspos': ( ("multi-line mass position, multi-color dots. "), - "plotLinesMasspos"), + "plot_lines_mass_pos"), # 'dotsMasspos': ( # ("mass position, multi-color, single line. "), # "plotDotsMasspos"), 'dotForTime': ( "Dots according to timestamp. Color defined by valueColors. Ex: G", - "plotTimeDots"), + "plot_time_dots"), 'multiColorDots': ( ("Multicolor dots with colors defined by valueColors. " "Value from low to high. " "Ex:*:W or -1:_|0:R|2.3:Y|+2.3:G. " "With colors: RYGMC: _: not plot"), - "plotMultiColorDots" + "plot_multi_color_dots" ), 'upDownDots': ( ("Show data with 2 different values: first down/ second up. " "With colors defined by valueColors. Ex: 1:R|0:Y"), - 'plotUpDownDots' + 'plot_up_down_dots' ) } } diff --git a/sohstationviewer/view/calendar_dialog.py b/sohstationviewer/view/calendar_dialog.py new file mode 100644 index 0000000000000000000000000000000000000000..ceb2e9ea16242dcc118b2255bb1c9203b442075a --- /dev/null +++ b/sohstationviewer/view/calendar_dialog.py @@ -0,0 +1,10 @@ +from PySide2 import QtWidgets + +from sohstationviewer.view.ui.calendar_ui_qtdesigner import Ui_CalendarDialog + + +class CalendarDialog(QtWidgets.QDialog, Ui_CalendarDialog): + + def __init__(self, parent=None): + super().__init__(parent) + self.setupUi(self) diff --git a/sohstationviewer/view/calendardialog.py b/sohstationviewer/view/calendardialog.py deleted file mode 100644 index afb18249a3ae8f3fe3ffad6941f1913f4c2d5246..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/calendardialog.py +++ /dev/null @@ -1,18 +0,0 @@ -from PySide2 import QtWidgets - -from sohstationviewer.view.ui.calendar_ui_qtdesigner import Ui_CalendarDialog - - -class CalendarDialog(QtWidgets.QDialog, Ui_CalendarDialog): - - def __init__(self, parent=None): - super().__init__(parent) - self.setupUi(self) - - # Connect the "selectionChanged" signal from the QCalendar - # This lets the user select a date using either the mouse, or - # arrow keys on the keyboard. - # self.calendarWidget.selectionChanged.connect(self.onDatePicked) - # date = self.calendarWidget.selectedDate() - # doy = date.dayOfYear() - # self.julianDateLineEdit.setText(str(doy)) diff --git a/sohstationviewer/view/channel_dialog.py b/sohstationviewer/view/channel_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..0673821e58f93828988a41e06113288d93cb19d0 --- /dev/null +++ b/sohstationviewer/view/channel_dialog.py @@ -0,0 +1,88 @@ +""" +channel_dialog.py +GUI to add/edit/remove channels +""" + +from sohstationviewer.view.core.db_gui_superclass import Ui_DBInfoDialog +from sohstationviewer.database.proccessDB import executeDB + + +class ChannelDialog(Ui_DBInfoDialog): + def __init__(self, parent): + super().__init__( + parent, ['No.', 'Channel', 'Label', 'Param', + 'ConvertFactor', 'Unit', 'FixPoint'], + 'channel', 'channels', resize_content_columns=[0, 4, 5, 6], + need_data_type_choice=True, requested_columns={3: 'Param'}, + check_fk=False) + self.setWindowTitle("Edit/Add/Delete Channels") + + def update_data_table_widget_items(self): + param_rows = executeDB("SELECT param from parameters") + self.param_choices = [''] + sorted([d[0] for d in param_rows]) + super(ChannelDialog, self).update_data_table_widget_items() + + def clear_first_row(self): + """ + device with no channels yet, there will be empty channel left + """ + self.data_table_widget.cellWidget(0, 1).setText('') + self.data_table_widget.cellWidget(0, 2).setText('') + self.data_table_widget.cellWidget(0, 3).setCurrentIndex(-1) + self.data_table_widget.cellWidget(0, 4).setText('1') + self.data_table_widget.cellWidget(0, 5).setText('') + self.data_table_widget.cellWidget(0, 6).setValue(0) + + def add_row(self, row_idx, fk=False): + self.addWidget(None, row_idx, 0) # No. + self.addWidget(self.data_list, row_idx, 1, foreign_key=fk) # chanID + self.addWidget(self.data_list, row_idx, 2) # label + self.addWidget(self.data_list, row_idx, 3, choices=self.param_choices) + self.addWidget(self.data_list, row_idx, 4, + field_name='convertFactor') + self.addWidget(self.data_list, row_idx, 5) # unit + self.addWidget(self.data_list, row_idx, 6, + range=[0, 5]) # fixPoint + + def data_type_changed(self): + self.data_type = self.data_type_combo_box.currentText() + self.update_data_table_widget_items() + + def get_data_list(self): + channel_rows = executeDB( + f"SELECT channel, label, param, convertFactor, unit, fixPoint " + f"FROM Channels " + f"WHERE dataType='{self.data_type}'") + return [[d[0], d[1], d[2], d[3], + '' if d[4] is None else d[4], + d[5]] + for d in channel_rows] + + def get_row_inputs(self, row_idx): + return [ + self.data_table_widget.cellWidget(row_idx, 1).text().strip(), + self.data_table_widget.cellWidget(row_idx, 2).text().strip(), + self.data_table_widget.cellWidget(row_idx, 3).currentText(), + float(self.data_table_widget.cellWidget(row_idx, 4).text()), + self.data_table_widget.cellWidget(row_idx, 5).text(), + self.data_table_widget.cellWidget(row_idx, 6).value() + ] + + def remove_row(self, remove_row_idx): + self.data_table_widget.remove_row(remove_row_idx) + for i in range(remove_row_idx, self.data_table_widget.rowCount()): + cell_widget = self.data_table_widget.cellWidget(i, 0) + cell_widget.setText(str(i)) + + def update_data(self, row, widget_idx, list_idx): + insertsql = (f"INSERT INTO Channels VALUES" + f"('{row[0]}', '{row[1]}', '{row[2]}'," + f" {row[3]}, '{row[4]}', {row[5]}, '{self.data_type}')") + updatesql = (f"UPDATE Channels SET channel='{row[0]}', " + f"label='{row[1]}', param='{row[2]}', " + f"convertFactor={row[3]}, unit='{row[4]}' " + f"fixPoint={row[5]} " + f"WHERE channel='%s'" + f" AND dataType='{self.data_type}'") + return super().update_data( + row, widget_idx, list_idx, insertsql, updatesql) diff --git a/sohstationviewer/view/channel_prefer_dialog.py b/sohstationviewer/view/channel_prefer_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..fea4be9e0af59d3b19327e8e8379661429eb2800 --- /dev/null +++ b/sohstationviewer/view/channel_prefer_dialog.py @@ -0,0 +1,292 @@ +from PySide2 import QtWidgets, QtCore + +from sohstationviewer.database.proccessDB import ( + executeDB, trunc_addDB, executeDB_dict) +from sohstationviewer.controller.processing import readChannels, detectDataType +from sohstationviewer.controller.util import displayTrackingInfo + +INSTRUCTION = """ +Place lists of channels to be read in the IDs field.\n +Select the radiobutton for the list to be used in plotting. +""" +TOTAL_ROW = 20 + +COL = {'sel': 0, 'name': 1, 'dataType': 2, 'IDs': 3, 'clr': 4} + + +class ChannelPreferDialog(QtWidgets.QWidget): + def __init__(self, parent, dir_names): + super(ChannelPreferDialog, self).__init__() + self.parent = parent + self.dir_names = dir_names + self.setWindowTitle("Channel Preferences") + self.setGeometry(100, 100, 1100, 800) + main_layout = QtWidgets.QVBoxLayout() + main_layout.setContentsMargins(7, 7, 7, 7) + self.setLayout(main_layout) + + main_layout.addWidget(QtWidgets.QLabel(INSTRUCTION)) + + self.create_id_table_widget() + main_layout.addWidget(self.id_table_widget, 1) + + button_layout = self.create_buttons_section() + main_layout.addLayout(button_layout) + + self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self) + self.trackingInfoTextBrowser.setFixedHeight(60) + main_layout.addWidget(self.trackingInfoTextBrowser) + self.changed = False + + def create_buttons_section(self): + h_layout = QtWidgets.QHBoxLayout() + self.add_db_chan_btn = QtWidgets.QPushButton( + self, text='Add DB Channels') + self.add_db_chan_btn.clicked.connect(self.add_db_channels) + h_layout.addWidget(self.add_db_chan_btn) + + self.scan_chan_btn = QtWidgets.QPushButton( + self, text='Scan Channels from Data Source') + self.scan_chan_btn.clicked.connect(self.scan_channels) + h_layout.addWidget(self.scan_chan_btn) + + self.save_btn = QtWidgets.QPushButton(self, text='Save') + self.save_btn.clicked.connect(self.save) + h_layout.addWidget(self.save_btn) + + self.save_add_main_btn = QtWidgets.QPushButton( + self, text='Save - Add to Main') + self.save_add_main_btn.clicked.connect(self.save_add_to_main_and_close) + h_layout.addWidget(self.save_add_main_btn) + + self.close_btn = QtWidgets.QPushButton(self, text='Cancel') + self.close_btn.clicked.connect(self.close) + h_layout.addWidget(self.close_btn) + return h_layout + + def create_id_table_widget(self): + self.id_table_widget = QtWidgets.QTableWidget(self) + # self.id_table_widget.verticalHeader().hide() + self.id_table_widget.setColumnCount(5) + col_headers = ['', 'Name', 'DataType', 'IDs', 'Clear'] + self.id_table_widget.setHorizontalHeaderLabels(col_headers) + header = self.id_table_widget.horizontalHeader() + header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) + header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch) + + self.id_table_widget.setRowCount(TOTAL_ROW) + self.avail_data_types = self.get_data_types() + for row_idx in range(TOTAL_ROW): + self.add_row(row_idx) + + self.curr_row = -1 + self.update_data_table_widget_items() + + @QtCore.Slot() + def add_row(self, row_idx): + curr_sel_radio_btn = QtWidgets.QRadioButton(self) + curr_sel_radio_btn.clicked.connect( + lambda checked: self.curr_sel_changed(row_idx)) + self.id_table_widget.setCellWidget( + row_idx, COL['sel'], curr_sel_radio_btn) + + name_line_edit = QtWidgets.QLineEdit(self) + name_line_edit.textChanged.connect(self.input_changed) + self.id_table_widget.setCellWidget( + row_idx, COL['name'], name_line_edit) + + data_type_combo_box = QtWidgets.QComboBox(self) + data_type_combo_box.currentIndexChanged.connect(self.input_changed) + data_type_combo_box.addItems(['Unknown'] + self.avail_data_types) + data_type_combo_box.setCurrentIndex(-1) + self.id_table_widget.setCellWidget( + row_idx, COL['dataType'], data_type_combo_box) + + id_line_edit = QtWidgets.QLineEdit(self) + id_line_edit.textChanged.connect(self.input_changed) + self.id_table_widget.setCellWidget( + row_idx, COL['IDs'], id_line_edit) + + del_button = QtWidgets.QPushButton(self, text='CLR') + del_button.clicked.connect(lambda arg: self.clear_id(row_idx)) + self.id_table_widget.setCellWidget( + row_idx, COL['clr'], del_button) + + @QtCore.Slot() + def input_changed(self): + self.changed = True + + def update_data_table_widget_items(self): + id_rows = self.get_id_rows() + # first row + self.id_table_widget.cellWidget(0, COL['sel']).setChecked(True) + self.curr_sel_changed(0) + count = 0 + for r in id_rows: + self.id_table_widget.cellWidget( + count, COL['sel']).setChecked( + True if ['current'] == 1 else False) + self.id_table_widget.cellWidget( + count, COL['name']).setText(r['name']) + self.id_table_widget.cellWidget( + count, COL['dataType']).setCurrentText(r['dataType']) + self.id_table_widget.cellWidget( + count, COL['IDs']).setText(r['IDs']) + self.id_table_widget.cellWidget( + count, COL['sel']).setChecked( + True if r['current'] == 1 else False) + if ['current'] == 1: + self.curr_sel_changed(count) + count += 1 + self.update() + + def get_row(self, row_idx): + name_widget = self.id_table_widget.cellWidget(row_idx, COL['name']) + data_type_widget = self.id_table_widget.cellWidget(row_idx, + COL['dataType']) + id_widget = self.id_table_widget.cellWidget(row_idx, COL['IDs']) + clear_widget = self.id_table_widget.cellWidget(row_idx, COL['clr']) + return name_widget, data_type_widget, id_widget, clear_widget + + @QtCore.Slot() + def curr_sel_changed(self, row_idx): + self.curr_row = row_idx + (self.name_widget, self.data_type_widget, + self.id_widget, self.clear_widget) = self.get_row(row_idx) + self.input_changed() + + @QtCore.Slot() + def clear_id(self, row_idx): + (name_widget, data_type_widget, id_widget, clear_widget) =\ + self.get_row(row_idx) + if id_widget.text().strip() != "": + msg = ("Are you sure you want to delete the channel IDs of " + "row #%s?" % (row_idx + 1)) + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + if result == QtWidgets.QMessageBox.No: + return + self.changed = True + if name_widget.text() != '': + self.changed = True + name_widget.setText('') + if data_type_widget.currentText != '': + self.changed = True + data_type_widget.setCurrentIndex(-1) + if id_widget.text() != '': + self.changed = True + id_widget.setText('') + + def validate_row(self, check_data_type=False): + if self.curr_row == -1: + msg = "Please select a row." + QtWidgets.QMessageBox.information(self, "Select row", msg) + return False + + if check_data_type: + self.data_type = self.data_type_widget.currentText() + if self.data_type not in self.avail_data_types: + msg = ("Please select a data type that isn't 'Unknown' for " + "the selected row.") + QtWidgets.QMessageBox.information( + self, "Select data type", msg) + return False + return True + + @QtCore.Slot() + def add_db_channels(self): + if not self.validate_row(check_data_type=True): + return + + db_channels = self.get_db_channels(self.data_type) + self.id_widget.setText(','.join(db_channels)) + + @QtCore.Slot() + def scan_channels(self): + if not self.validate_row(): + return + + data_type = detectDataType(self, self.dir_names) + if data_type in self.avail_data_types: + self.data_type_widget.setCurrentText(data_type) + else: + self.data_type_widget.setCurrenText('Unknown') + scanned_channels = readChannels(self, self.dir_names) + self.id_widget.setText(','.join(scanned_channels)) + + @QtCore.Slot() + def save(self): + if not self.validate_row(): + return + if self.changed: + msg = ("All IDs in the database will be overwritten with " + "current IDs in the dialog.\nClick Cancel to stop updating " + "database.") + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) + if result == QtWidgets.QMessageBox.Cancel: + return False + sql_list = [] + for row_idx in range(TOTAL_ROW): + sql = self.save_row_sql(row_idx) + if sql is not None: + sql_list.append(sql) + if len(sql_list) == 0: + self.parent.IDs = [] + self.parent.IDsName = '' + self.parent.data_type = 'Unknown' + return True + + ret = trunc_addDB('ChannelPrefer', sql_list) + if ret is not True: + displayTrackingInfo(self.parent, ret, "error") + self.parent.IDs = [ + t.strip() for t in self.id_widget.text().split(',')] + self.parent.IDsName = self.name_widget.text().strip() + self.parent.IDsDataType = self.data_type_widget.currentText() + return True + + def save_row_sql(self, row_idx): + current = 1 if self.id_table_widget.cellWidget( + row_idx, COL['sel']).isChecked() else 0 + name = self.id_table_widget.cellWidget( + row_idx, COL['name']).text() + data_type = self.id_table_widget.cellWidget( + row_idx, COL['dataType']).currentText() + id = self.id_table_widget.cellWidget( + row_idx, COL['IDs']).text() + if id.strip() == '': + return + if name.strip() == '' and id.strip() != '': + msg = f"Please add Name for row {row_idx}." + QtWidgets.QMessageBox.information(self, "Missing info", msg) + return + return (f"INSERT INTO ChannelPrefer (name, IDs, dataType, current)" + f"VALUES ('{name}', '{id}', '{data_type}', {current})") + + @QtCore.Slot() + def save_add_to_main_and_close(self): + if not self.save(): + return + self.parent.currIDsNameLineEdit.setText(self.parent.IDsName) + self.parent.allChanCheckBox.setChecked(False) + self.close() + + def get_data_types(self): + data_type_rows = executeDB( + 'SELECT * FROM DataTypes ORDER BY dataType ASC') + return [d[0] for d in data_type_rows] + + def get_db_channels(self, data_type): + channel_rows = executeDB( + f"SELECT channel FROM CHANNELS WHERE dataType='{data_type}' " + f" ORDER BY dataType ASC") + return [c[0] for c in channel_rows] + + def get_id_rows(self): + i_ds_rows = executeDB_dict( + "SELECT name, IDs, dataType, current FROM ChannelPrefer " + " ORDER BY name ASC") + return i_ds_rows diff --git a/sohstationviewer/view/channeldialog.py b/sohstationviewer/view/channeldialog.py deleted file mode 100755 index 205f701d65d7738a4c7f4467a024b9c96db360d3..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/channeldialog.py +++ /dev/null @@ -1,88 +0,0 @@ -""" -channeldialog.py -GUI to add/edit/remove channels -""" - -from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog -from sohstationviewer.database.proccessDB import executeDB - - -class ChannelDialog(Ui_DBInfoDialog): - def __init__(parent, self): - super().__init__( - parent, ['No.', 'Channel', 'Label', 'Param', - 'ConvertFactor', 'Unit', 'FixPoint'], - 'channel', 'channels', resizeContentColumns=[0, 4, 5, 6], - needDataTypeChoice=True, requestedColumns={3: 'Param'}, - checkFK=False) - self.setWindowTitle("Edit/Add/Delete Channels") - - def updateDataTableWidgetItems(self): - paraRows = executeDB("SELECT param from parameters") - self.paramChoices = [''] + sorted([d[0] for d in paraRows]) - super(ChannelDialog, self).updateDataTableWidgetItems() - - def clearFirstRow(self): - """ - device with no channels yet, there will be empty channel left - """ - self.dataTableWidget.cellWidget(0, 1).setText('') - self.dataTableWidget.cellWidget(0, 2).setText('') - self.dataTableWidget.cellWidget(0, 3).setCurrentIndex(-1) - self.dataTableWidget.cellWidget(0, 4).setText('1') - self.dataTableWidget.cellWidget(0, 5).setText('') - self.dataTableWidget.cellWidget(0, 6).setValue(0) - - def addRow(self, rowidx, fk=False): - self.addWidget(None, rowidx, 0) # No. - self.addWidget(self.dataList, rowidx, 1, foreignkey=fk) # chanID - self.addWidget(self.dataList, rowidx, 2) # label - self.addWidget(self.dataList, rowidx, 3, choices=self.paramChoices) - self.addWidget(self.dataList, rowidx, 4, - fieldName='convertFactor') - self.addWidget(self.dataList, rowidx, 5) # unit - self.addWidget(self.dataList, rowidx, 6, - range=[0, 5]) # fixPoint - - def dataTypeChanged(self): - self.dataType = self.dataTypeCombobox.currentText() - self.updateDataTableWidgetItems() - - def getDataList(self): - channelRows = executeDB( - f"SELECT channel, label, param, convertFactor, unit, fixPoint " - f"FROM Channels " - f"WHERE dataType='{self.dataType}'") - return [[d[0], d[1], d[2], d[3], - '' if d[4] is None else d[4], - d[5]] - for d in channelRows] - - def getRowInputs(self, rowidx): - return [ - self.dataTableWidget.cellWidget(rowidx, 1).text().strip(), - self.dataTableWidget.cellWidget(rowidx, 2).text().strip(), - self.dataTableWidget.cellWidget(rowidx, 3).currentText().strip(), - float(self.dataTableWidget.cellWidget(rowidx, 4).text()), - self.dataTableWidget.cellWidget(rowidx, 5).text(), - self.dataTableWidget.cellWidget(rowidx, 6).value() - ] - - def removeRow(self, removeRowidx): - self.dataTableWidget.removeRow(removeRowidx) - for i in range(removeRowidx, self.dataTableWidget.rowCount()): - cellWget = self.dataTableWidget.cellWidget(i, 0) - cellWget.setText(str(i)) - - def updateData(self, row, widgetidx, listidx): - insertsql = (f"INSERT INTO Channels VALUES" - f"('{row[0]}', '{row[1]}', '{row[2]}'," - f" {row[3]}, '{row[4]}', {row[5]}, '{self.dataType}')") - updatesql = (f"UPDATE Channels SET channel='{row[0]}', " - f"label='{row[1]}', param='{row[2]}', " - f"convertFactor={row[3]}, unit='{row[4]}' " - f"fixPoint={row[5]} " - f"WHERE channel='%s'" - f" AND dataType='{self.dataType}'") - return super().updateData( - row, widgetidx, listidx, insertsql, updatesql) diff --git a/sohstationviewer/view/channelpreferdialog.py b/sohstationviewer/view/channelpreferdialog.py deleted file mode 100755 index 25e62dd9a1118feafaaadb6f466de6de3ddff0c9..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/channelpreferdialog.py +++ /dev/null @@ -1,299 +0,0 @@ -from PySide2 import QtWidgets, QtCore - -from sohstationviewer.database.proccessDB import ( - executeDB, trunc_addDB, executeDB_dict) -from sohstationviewer.controller.processing import readChannels, detectDataType -from sohstationviewer.controller.util import displayTrackingInfo - -INSTRUCTION = """ -Place lists of channels to be read in the IDs field.\n -Select the radiobutton for the list to be used in plotting. -""" -TOTAL_ROW = 20 - -COL = {'sel': 0, 'name': 1, 'dataType': 2, 'IDs': 3, 'clr': 4} - - -class ChannelPreferDialog(QtWidgets.QWidget): - def __init__(self, parent, dirnames): - super(ChannelPreferDialog, self).__init__() - self.parent = parent - self.dirnames = dirnames - self.setWindowTitle("Channel Preferences") - self.setGeometry(100, 100, 1100, 800) - mainLayout = QtWidgets.QVBoxLayout() - mainLayout.setContentsMargins(7, 7, 7, 7) - self.setLayout(mainLayout) - - mainLayout.addWidget(QtWidgets.QLabel(INSTRUCTION)) - - self.createIDsTableWidget() - mainLayout.addWidget(self.IDsTableWidget, 1) - - buttonLayout = self.createButtonsSection() - mainLayout.addLayout(buttonLayout) - - self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self) - self.trackingInfoTextBrowser.setFixedHeight(60) - mainLayout.addWidget(self.trackingInfoTextBrowser) - self.changed = False - - def createButtonsSection(self): - hLayout = QtWidgets.QHBoxLayout() - self.addDBChanBtn = QtWidgets.QPushButton( - self, text='Add DB Channels') - self.addDBChanBtn.clicked.connect(self.addDBChannels) - hLayout.addWidget(self.addDBChanBtn) - - self.scanChanBtn = QtWidgets.QPushButton( - self, text='Scan Channels from Data Source') - self.scanChanBtn.clicked.connect(self.scanChannels) - hLayout.addWidget(self.scanChanBtn) - - self.saveBtn = QtWidgets.QPushButton(self, text='Save') - self.saveBtn.clicked.connect(self.save) - hLayout.addWidget(self.saveBtn) - - self.save_addMainBtn = QtWidgets.QPushButton( - self, text='Save - Add to Main') - self.save_addMainBtn.clicked.connect(self.save_addToMainNClose) - hLayout.addWidget(self.save_addMainBtn) - - self.closeBtn = QtWidgets.QPushButton(self, text='Cancel') - self.closeBtn.clicked.connect(self.close) - hLayout.addWidget(self.closeBtn) - return hLayout - - def createIDsTableWidget(self): - self.IDsTableWidget = QtWidgets.QTableWidget(self) - # self.IDsTableWidget.verticalHeader().hide() - self.IDsTableWidget.setColumnCount(5) - colHeaders = ['', 'Name', 'DataType', 'IDs', 'Clear'] - self.IDsTableWidget.setHorizontalHeaderLabels(colHeaders) - header = self.IDsTableWidget.horizontalHeader() - header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) - header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch) - - self.IDsTableWidget.setRowCount(TOTAL_ROW) - self.availDataTypes = self.getDataTypes() - for rowidx in range(TOTAL_ROW): - self.addRow(rowidx) - - self.currRow = -1 - self.updateDataTableWidgetItems() - - @QtCore.Slot() - def addRow(self, rowidx): - currSelRadioBtn = QtWidgets.QRadioButton(self) - currSelRadioBtn.clicked.connect( - lambda checked: self.currSelChanged(rowidx)) - self.IDsTableWidget.setCellWidget( - rowidx, COL['sel'], currSelRadioBtn) - - nameLineEdit = QtWidgets.QLineEdit(self) - nameLineEdit.textChanged.connect(self.inputChanged) - self.IDsTableWidget.setCellWidget( - rowidx, COL['name'], nameLineEdit) - - dataTypeCombobox = QtWidgets.QComboBox(self) - dataTypeCombobox.currentIndexChanged.connect(self.inputChanged) - dataTypeCombobox.addItems(['Unknown'] + self.availDataTypes) - dataTypeCombobox.setCurrentIndex(-1) - self.IDsTableWidget.setCellWidget( - rowidx, COL['dataType'], dataTypeCombobox) - - IDsLineEdit = QtWidgets.QLineEdit(self) - IDsLineEdit.textChanged.connect(self.inputChanged) - self.IDsTableWidget.setCellWidget( - rowidx, COL['IDs'], IDsLineEdit) - - delButton = QtWidgets.QPushButton(self, text='CLR') - delButton.clicked.connect(lambda arg: self.clearIDs(rowidx)) - self.IDsTableWidget.setCellWidget( - rowidx, COL['clr'], delButton) - - @QtCore.Slot() - def inputChanged(self): - self.changed = True - - def updateDataTableWidgetItems(self): - IDsRows = self.getIDsRows() - # first row - self.IDsTableWidget.cellWidget(0, COL['sel']).setChecked(True) - self.currSelChanged(0) - count = 0 - for r in IDsRows: - self.IDsTableWidget.cellWidget( - count, COL['sel']).setChecked( - True if ['current'] == 1 else False) - self.IDsTableWidget.cellWidget( - count, COL['name']).setText(r['name']) - self.IDsTableWidget.cellWidget( - count, COL['dataType']).setCurrentText(r['dataType']) - self.IDsTableWidget.cellWidget( - count, COL['IDs']).setText(r['IDs']) - self.IDsTableWidget.cellWidget( - count, COL['sel']).setChecked( - True if r['current'] == 1 else False) - if ['current'] == 1: - self.currSelChanged(count) - count += 1 - self.update() - - def getRow(self, rowidx): - nameWget = self.IDsTableWidget.cellWidget(rowidx, COL['name']) - dataTypeWget = self.IDsTableWidget.cellWidget(rowidx, COL['dataType']) - IDsWget = self.IDsTableWidget.cellWidget(rowidx, COL['IDs']) - clearWget = self.IDsTableWidget.cellWidget(rowidx, COL['clr']) - return (nameWget, dataTypeWget, IDsWget, clearWget) - - @QtCore.Slot() - def currSelChanged(self, rowidx): - self.currRow = rowidx - (self.nameWget, self.dataTypeWget, - self.IDsWget, self.clearWget) = self.getRow(rowidx) - self.inputChanged() - - @QtCore.Slot() - def clearIDs(self, rowidx): - (nameWget, dataTypeWget, IDsWget, clearWget) = self.getRow(rowidx) - if IDsWget.text().strip() != "": - msg = ("Are you sure you want to delete the channel IDs of " - "row #%s?" % (rowidx+1)) - result = QtWidgets.QMessageBox.question( - self, "Confirmation", msg, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) - if result == QtWidgets.QMessageBox.No: - return - self.changed = True - if nameWget.text() != '': - self.changed = True - nameWget.setText('') - if dataTypeWget.currentText != '': - self.changed = True - dataTypeWget.setCurrentIndex(-1) - if IDsWget.text() != '': - self.changed = True - IDsWget.setText('') - - def validateRow(self, checkDataType=False): - if self.currRow == -1: - msg = ("Please select a row.") - QtWidgets.QMessageBox.information(self, "Select row", msg) - return False - - if checkDataType: - self.dataType = self.dataTypeWget.currentText() - if self.dataType not in self.availDataTypes: - msg = ("Please select a data type that isn't 'Unknown' for " - "the selected row.") - QtWidgets.QMessageBox.information( - self, "Select data type", msg) - return False - # # check IDs - # if self.IDsWget.text().strip() != '': - # msg = ("The selected row's IDs will be overwritten.\n" - # "Do you want to continue?") - # result = QtWidgets.QMessageBox.question( - # self, "Confirmation", msg, - # QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) - # if result == QtWidgets.QMessageBox.No: - # return False - return True - - @QtCore.Slot() - def addDBChannels(self): - if not self.validateRow(checkDataType=True): - return - - dbChannels = self.getDBChannels(self.dataType) - self.IDsWget.setText(','.join(dbChannels)) - - @QtCore.Slot() - def scanChannels(self): - if not self.validateRow(): - return - - dataType = detectDataType(self, self.dirnames) - if dataType in self.availDataTypes: - self.dataTypeWget.setCurrentText(dataType) - else: - self.dataTypeWget.setCurrenText('Unknown') - scannedChannels = readChannels(self, self.dirnames) - self.IDsWget.setText(','.join(scannedChannels)) - - @QtCore.Slot() - def save(self): - if not self.validateRow(): - return - if self.changed: - msg = ("All IDs in the database will be overwritten with " - "current IDs in the dialog.\nClick Cancel to stop updating " - "database.") - result = QtWidgets.QMessageBox.question( - self, "Confirmation", msg, - QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel) - if result == QtWidgets.QMessageBox.Cancel: - return False - sqlList = [] - for rowidx in range(TOTAL_ROW): - sql = self.saveRowSql(rowidx) - if sql is not None: - sqlList.append(sql) - if len(sqlList) == 0: - self.parent.IDs = [] - self.parent.IDsName = '' - self.parent.dataType = 'Unknown' - return True - - ret = trunc_addDB('ChannelPrefer', sqlList) - if ret is not True: - displayTrackingInfo(self.parent, ret, "error") - self.parent.IDs = [ - t.strip() for t in self.IDsWget.text().split(',')] - self.parent.IDsName = self.nameWget.text().strip() - self.parent.IDsDataType = self.dataTypeWget.currentText() - return True - - def saveRowSql(self, rowidx): - current = 1 if self.IDsTableWidget.cellWidget( - rowidx, COL['sel']).isChecked() else 0 - name = self.IDsTableWidget.cellWidget( - rowidx, COL['name']).text() - dataType = self.IDsTableWidget.cellWidget( - rowidx, COL['dataType']).currentText() - IDs = self.IDsTableWidget.cellWidget( - rowidx, COL['IDs']).text() - if IDs.strip() == '': - return - if name.strip() == '' and IDs.strip() != '': - msg = f"Please add Name for row {rowidx}." - QtWidgets.QMessageBox.information(self, "Missing info", msg) - return - return (f"INSERT INTO ChannelPrefer (name, IDs, dataType, current)" - f"VALUES ('{name}', '{IDs}', '{dataType}', {current})") - - @QtCore.Slot() - def save_addToMainNClose(self): - if not self.save(): - return - self.parent.currIDsNameLineEdit.setText(self.parent.IDsName) - self.parent.allChanCheckBox.setChecked(False) - self.close() - - def getDataTypes(self): - dataTypeRows = executeDB( - 'SELECT * FROM DataTypes ORDER BY dataType ASC') - return [d[0] for d in dataTypeRows] - - def getDBChannels(self, dataType): - channelRows = executeDB( - f"SELECT channel FROM CHANNELS WHERE dataType='{dataType}' " - f" ORDER BY dataType ASC") - return [c[0] for c in channelRows] - - def getIDsRows(self): - IDsRows = executeDB_dict( - "SELECT name, IDs, dataType, current FROM ChannelPrefer " - " ORDER BY name ASC") - return IDsRows diff --git a/sohstationviewer/view/core/calendarwidget.py b/sohstationviewer/view/core/calendar_widget.py similarity index 52% rename from sohstationviewer/view/core/calendarwidget.py rename to sohstationviewer/view/core/calendar_widget.py index fea881591b0756bfade96981bab6216bdc19ba3b..f476d95207944151872bfa0ea4c5d27d9760d578 100644 --- a/sohstationviewer/view/core/calendarwidget.py +++ b/sohstationviewer/view/core/calendar_widget.py @@ -6,36 +6,39 @@ class CalendarWidget(QtWidgets.QCalendarWidget): def __init__(self, parent): super().__init__(parent) self.setupUi() - self._showDayOfYear = getattr(self, 'toggleDayOfYear', None) is None + self._show_day_of_year = \ + getattr(self, 'toggle_day_of_year', None) is None def setupUi(self): self.setMinimumWidth(300) self.setMinimumHeight(275) - navbar = self.findChild(QtWidgets.QWidget, 'qt_calendar_navigationbar') + nav_bar = self.findChild( + QtWidgets.QWidget, 'qt_calendar_navigationbar' + ) - if navbar: - self.toggleDayOfYear = QtWidgets.QCheckBox('Show DOY') - navbar.layout().insertWidget(2, self.toggleDayOfYear) + if nav_bar: + self.toggle_day_of_year = QtWidgets.QCheckBox('Show DOY') + nav_bar.layout().insertWidget(2, self.toggle_day_of_year) - self.toggleDayOfYear.toggled.connect(self.setShowDayOfYear) - self.toggleDayOfYear.setCheckable(True) + self.toggle_day_of_year.toggled.connect(self.set_show_day_of_year) + self.toggle_day_of_year.setCheckable(True) - palette = self.toggleDayOfYear.palette() + palette = self.toggle_day_of_year.palette() palette.setColor(QtGui.QPalette.WindowText, QtGui.QColor('white')) - self.toggleDayOfYear.setPalette(palette) + self.toggle_day_of_year.setPalette(palette) - self.toggleDayOfYear.show() + self.toggle_day_of_year.show() - def showDayOfYear(self): - return self._showDayOfYear + def show_day_of_year(self): + return self._show_day_of_year def paintCell(self, painter, rect, date): super().paintCell(painter, rect, date) painter.save() - if self.showDayOfYear(): + if self.show_day_of_year(): color = (QtGui.QColor('red') if date != self.selectedDate() else QtGui.QColor('white')) font = painter.font() @@ -46,6 +49,6 @@ class CalendarWidget(QtWidgets.QCalendarWidget): painter.restore() @QtCore.Slot(bool) - def setShowDayOfYear(self, value): - self._showDayOfYear = value + def set_show_day_of_year(self, value): + self._show_day_of_year = value self.updateCells() diff --git a/sohstationviewer/view/core/db_gui_superclass.py b/sohstationviewer/view/core/db_gui_superclass.py new file mode 100755 index 0000000000000000000000000000000000000000..a961f17891c6fc8d27047e2e76e385108463e023 --- /dev/null +++ b/sohstationviewer/view/core/db_gui_superclass.py @@ -0,0 +1,366 @@ +from PySide2 import QtWidgets, QtGui + +from sohstationviewer.database.proccessDB import executeDB + + +def set_widget_color(widget, changed=False, read_only=False): + palette = QtGui.QPalette() + + if read_only: + # grey text + palette.setColor(QtGui.QPalette.Text, QtGui.QColor(100, 100, 100)) + # light blue background + palette.setColor(QtGui.QPalette.Base, QtGui.QColor(210, 240, 255)) + widget.setReadOnly(True) + widget.setPalette(palette) + return + else: + # white background + palette.setColor(QtGui.QPalette.Base, QtGui.QColor(255, 255, 255)) + if changed: + # red text + palette.setColor(QtGui.QPalette.Text, QtGui.QColor(255, 0, 0)) + else: + try: + if widget.isReadOnly(): + # grey text + palette.setColor( + QtGui.QPalette.Text, QtGui.QColor(100, 100, 100)) + else: + # black text + palette.setColor(QtGui.QPalette.Text, QtGui.QColor(0, 0, 0)) + except AttributeError: + palette.setColor(QtGui.QPalette.Text, QtGui.QColor(0, 0, 0)) + widget.setPalette(palette) + + +class Ui_DBInfoDialog(QtWidgets.QWidget): + def __init__(self, parent, column_headers, col_name, table_name, + resize_content_columns=[], requested_columns={}, + need_data_type_choice=False, check_fk=True): + self.total_col = len(column_headers) + self.column_headers = column_headers + self.resize_content_columns = resize_content_columns + self.requested_columns = requested_columns + self.need_data_type_choice = need_data_type_choice + self.col_name = col_name + self.table_name = table_name + self.check_fk = check_fk + super(Ui_DBInfoDialog, self).__init__() + + self.setGeometry(100, 100, 900, 900) + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + button_layout = self.create_buttons_section() + main_layout.addLayout(button_layout) + + self.create_data_table_widget() + main_layout.addWidget(self.data_table_widget, 1) + if self.table_name != '': + instruction = ("Background: LIGHT BLUE - Non Editable due to " + "FK constrain; " + "WHITE - Editable. " + "Text: BLACK - Saved; RED - Not saved") + # TODO: add question mark button to give instruction + + main_layout.addWidget(QtWidgets.QLabel(instruction)) + + def addWidget(self, data_list, row_idx, col_idx, foreign_key=False, + choices=None, range=None, field_name=''): + if data_list is None: + text = str(row_idx) # row number + else: + text = (str(data_list[row_idx][col_idx - 1]) + if row_idx < len(data_list) else '') + + if data_list is None: + widget = QtWidgets.QPushButton(text) + elif choices is None and range is None: + widget = QtWidgets.QLineEdit(text) + if field_name == 'convertFactor': + # precision=6 + validator = QtGui.QDoubleValidator(0.0, 5.0, 6) + validator.setNotation(QtGui.QDoubleValidator.StandardNotation) + widget.setValidator(validator) + if text == '': + widget.setText('1') + elif choices: + widget = QtWidgets.QComboBox() + widget.addItems(choices) + widget.setCurrentText(text) + elif range: + widget = QtWidgets.QSpinBox() + widget.setMinimum(range[0]) + widget.setMaximum(range[1]) + if text in ["", None, 'None']: + widget.setValue(range[0]) + else: + widget.setValue(int(text)) + + if data_list is None: + set_widget_color(widget) + widget.setFixedWidth(40) + widget.clicked.connect( + lambda: self.row_number_clicked(widget)) + elif foreign_key: + set_widget_color(widget, read_only=True) + else: + new = False if row_idx < len(data_list) else True + set_widget_color(widget, read_only=False, changed=new) + if choices is None and range is None: + widget.textChanged.connect( + lambda changed_text: + self.cell_input_change(changed_text, row_idx, col_idx)) + elif choices: + widget.currentTextChanged.connect( + lambda changed_text: + self.cell_input_change(changed_text, row_idx, col_idx)) + elif range: + widget.valueChanged.connect( + lambda changed_text: + self.cell_input_change(changed_text, row_idx, col_idx)) + self.data_table_widget.setCellWidget(row_idx, col_idx, widget) + + def row_number_clicked(self, widget): + self.data_table_widget.selectRow(int(widget.text())) + self.data_table_widget.repaint() + + def create_buttons_section(self): + h_layout = QtWidgets.QHBoxLayout() + + if self.need_data_type_choice: + self.data_type_combo_box = QtWidgets.QComboBox(self) + data_type_rows = executeDB('SELECT * FROM DataTypes') + self.data_type_combo_box.addItems([d[0] for d in data_type_rows]) + self.data_type_combo_box.currentTextChanged.connect( + self.data_type_changed) + h_layout.addWidget(self.data_type_combo_box) + if self.table_name != '': + self.add_row_btn = QtWidgets.QPushButton(self, text='ADD ROW') + self.add_row_btn.setFixedWidth(300) + self.add_row_btn.clicked.connect(self.add_new_row) + h_layout.addWidget(self.add_row_btn) + + self.save_changes_btn = QtWidgets.QPushButton( + self, text='SAVE CHANGES') + self.save_changes_btn.setFixedWidth(300) + self.save_changes_btn.clicked.connect(self.save_changes) + h_layout.addWidget(self.save_changes_btn) + + self.close_btn = QtWidgets.QPushButton(self, text='CLOSE') + self.close_btn.setFixedWidth(300) + self.close_btn.clicked.connect(self.close) + h_layout.addWidget(self.close_btn) + return h_layout + + def create_data_table_widget(self): + self.data_table_widget = QtWidgets.QTableWidget(self) + self.data_table_widget.verticalHeader().hide() + self.data_table_widget.setColumnCount(self.total_col) + self.data_table_widget.setHorizontalHeaderLabels(self.column_headers) + header = self.data_table_widget.horizontalHeader() + header.setSectionResizeMode(QtWidgets.QHeaderView.Stretch) + for i in self.resize_content_columns: + header.setSectionResizeMode( + i, QtWidgets.QHeaderView.ResizeToContents) + + if self.need_data_type_choice: + self.data_type = self.data_type_combo_box.currentText() + self.update_data_table_widget_items() + + def update_data_table_widget_items(self): + self.data_list = self.get_data_list() + row_count = len(self.data_list) + row_count = 1 if row_count == 0 else row_count + self.data_table_widget.setRowCount(row_count) + for i in range(len(self.data_list)): + fk = self.check_data_foreign_key(self.data_list[i][0]) + self.add_row(i, fk) + if len(self.data_list) == 0: + """ + No Row, should leave 1 empty row + """ + self.clearFirstRow() + self.update() + + def add_new_row(self): + row_position = self.data_table_widget.rowCount() + self.data_table_widget.insertRow(row_position) + self.add_row(row_position) + self.data_table_widget.scrollToBottom() + self.data_table_widget.repaint() # to show row's header + self.data_table_widget.cellWidget(row_position, 1).setFocus() + + def remove_row(self, remove_row_idx): + self.data_table_widget.removeRow(remove_row_idx) + for i in range(remove_row_idx, self.data_table_widget.rowCount()): + cell_widget = self.data_table_widget.cellWidget(i, 0) + cell_widget.setText(str(i)) + + def cell_input_change(self, changed_text, rowidx, colidx): + """ + If cell's value is changed, text's color will be red + otherwise, text's color will be black + """ + changed = False + if rowidx < len(self.data_list): + if changed_text != self.data_list[rowidx][colidx - 1]: + changed = True + cell_widget = self.data_table_widget.cellWidget(rowidx, colidx) + set_widget_color(cell_widget, changed=changed) + + def check_data_foreign_key(self, val): + if not self.check_fk: + return False + sql = (f"SELECT {self.col_name} FROM channels " + f"WHERE {self.col_name}='{val}'") + param_rows = executeDB(sql) + if len(param_rows) > 0: + return True + else: + return False + + def reset_row_inputs(self, reset, widget_idx, list_idx): + for col_idx in range(1, self.total_col): + cell_widget = self.data_table_widget.cellWidget( + widget_idx, + col_idx + ) + read_only = False + if hasattr(cell_widget, 'isReadOnly'): + read_only = cell_widget.isReadOnly() + if not read_only: + if reset == 1: + org_val = self.data_list[list_idx][col_idx - 1] + if isinstance(cell_widget, QtWidgets.QLineEdit): + cell_widget.setText(str(org_val)) + elif isinstance(cell_widget, QtWidgets.QComboBox): + cell_widget.setCurrentText(str(org_val)) + elif isinstance(cell_widget, QtWidgets.QSpinBox): + cell_widget.setValue(int(org_val)) + set_widget_color(cell_widget) + + def add_row(self, row_idx, fk=False): + pass + + def data_type_changed(self): + pass + + def save_changes(self): + try: + self.data_table_widget.focusWidget().clearFocus() + except AttributeError: + pass + self.remove_count = 0 + self.insert_count = 0 + self.skip_count = 0 + row_count = self.data_table_widget.rowCount() + for i in range(row_count): + widget_idx = i - (row_count - self.data_table_widget.rowCount()) + list_idx = (i - self.remove_count + + self.insert_count - self.skip_count) + row_inputs = self.get_row_inputs(widget_idx) + reset = self.update_data(row_inputs, widget_idx, list_idx) + if reset > -1: + self.reset_row_inputs(reset, widget_idx, list_idx) + + def update_data(self, row, widget_idx, list_idx, insert_sql, update_sql): + """ + update data_table_widget and data_list according to the action to + add, remove, update + :param row: values of processed row in data_table_widget + :param widget_idx: index of the process rows in data_table_widget + :param list_idx: index of the process rows in self.data_list + :param col_name: key db column in the table + :param table_name: data table name + :param insert_sql: query to insert a row to the table + :param update_sql: query to update a row in the table + :return -1 for doing nothing + 0 no need to reset input values to org row + 1 need to reset input values to org row + """ + if list_idx < len(self.data_list): + org_row = self.data_list[list_idx] + if row[0] == "": + msg = (f"The {self.col_name} of '{org_row}' at " + f"row {widget_idx} has been changed to blank. " + f"Are you sure you want to delete this row in " + f"database?") + result = QtWidgets.QMessageBox.question( + self, "Delete %s?" % org_row, msg, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No + ) + if result == QtWidgets.QMessageBox.No: + # reset the first widget value and call the function again + # to check other fields + cell_widget = self.data_table_widget.cellWidget( + widget_idx, 1 + ) + cell_widget.setText(org_row[0]) + row[0] = org_row[0] + self.update_data(row, widget_idx, list_idx) + else: + sql = (f"DELETE FROM {self.table_name} " + f"WHERE {self.col_name}='{org_row[0]}'") + executeDB(sql) + self.data_list.remove(org_row) + self.remove_row(widget_idx) + self.remove_count += 1 + return -1 + + if row == org_row: + return -1 + if (row[0] in [p[0] for p in self.data_list] and + self.data_list[list_idx][0] != row[0]): + msg = (f"The {self.col_name} of {org_row} at row" + f" {widget_idx} has been changed to '{row[0]}' " + f"which already is in the database. " + f"It will be changed back to {org_row[0]}.") + QtWidgets.QMessageBox.information(self, "Error", msg) + # reset the first widget value and call the function again + # to check other fields + cell_widget = self.data_table_widget.cellWidget(widget_idx, 1) + cell_widget.setText(org_row[0]) + row[0] = org_row[0] + self.update_data(row, widget_idx, list_idx) + else: + msg = (f"{org_row} at row {widget_idx} has " + f"been changed to {row}. Please confirm it.") + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel + ) + if result == QtWidgets.QMessageBox.Cancel: + return 1 + else: + executeDB(update_sql % org_row[0]) + self.data_list[list_idx] = row + return 0 + + if row[0] == "": + msg = (f"Row {widget_idx} has blank {self.col_name}. " + f"It will be removed.") + QtWidgets.QMessageBox.information(self, "Error", msg) + self.remove_row(widget_idx) + return -1 + blank_requested_columns = [self.requested_columns[i] + for i in self.requested_columns.keys() + if row[i - 1] == ""] + if blank_requested_columns != []: + msg = (f"Row {widget_idx}: blank " + f"{', '.join(blank_requested_columns)} which require some " + f"value(s).") + QtWidgets.QMessageBox.information(self, "Error", msg) + return -1 + + if row[0] in [p[0] for p in self.data_list]: + msg = (f"The {self.col_name} '{row[0]}' is already in the " + f"database. Row {widget_idx} will be removed.") + QtWidgets.QMessageBox.information(self, "Error", msg) + self.remove_row(widget_idx) + return -1 + executeDB(insert_sql) + self.data_list.append(row) + self.insert_count += 1 + return 0 diff --git a/sohstationviewer/view/core/dbgui_superclass.py b/sohstationviewer/view/core/dbgui_superclass.py deleted file mode 100755 index 45dc5189241ad68adceaf157f2bfbb9d1bf452aa..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/core/dbgui_superclass.py +++ /dev/null @@ -1,368 +0,0 @@ -from PySide2 import QtWidgets, QtGui - -from sohstationviewer.database.proccessDB import executeDB - - -def setWidgetColor(widget, changed=False, readOnly=False): - pallete = QtGui.QPalette() - - if readOnly: - # grey text - pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(100, 100, 100)) - # light blue background - pallete.setColor(QtGui.QPalette.Base, QtGui.QColor(210, 240, 255)) - widget.setReadOnly(True) - widget.setPalette(pallete) - return - else: - # white background - pallete.setColor(QtGui.QPalette.Base, QtGui.QColor(255, 255, 255)) - if changed: - # red text - pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(255, 0, 0)) - else: - try: - if widget.isReadOnly(): - # grey text - pallete.setColor( - QtGui.QPalette.Text, QtGui.QColor(100, 100, 100)) - else: - # black text - pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(0, 0, 0)) - except AttributeError: - pallete.setColor(QtGui.QPalette.Text, QtGui.QColor(0, 0, 0)) - widget.setPalette(pallete) - - -class Ui_DBInfoDialog(QtWidgets.QWidget): - def __init__(self, parent, columnHeaders, colName, tableName, - resizeContentColumns=[], requestedColumns={}, - needDataTypeChoice=False, checkFK=True): - self.totalCol = len(columnHeaders) - self.columnHeaders = columnHeaders - self.resizeContentColumns = resizeContentColumns - self.requestedColumns = requestedColumns - self.needDataTypeChoice = needDataTypeChoice - self.colName = colName - self.tableName = tableName - self.checkFK = checkFK - super(Ui_DBInfoDialog, self).__init__() - - self.setGeometry(100, 100, 900, 900) - mainLayout = QtWidgets.QVBoxLayout() - self.setLayout(mainLayout) - - buttonLayout = self.createButtonsSection() - mainLayout.addLayout(buttonLayout) - - self.createDataTableWidget() - mainLayout.addWidget(self.dataTableWidget, 1) - if self.tableName != '': - instruction = ("Background: LIGHT BLUE - Non Editable due to " - "FK constrain; " - "WHITE - Editable. " - "Text: BLACK - Saved; RED - Not saved") - # TODO: add question mark button to give instruction - # if self.tableName == 'parameters': - # instruction += ( - # "\nValueColors is requested for multiColorDots plotType." - # "Value from low to high" - # "\nThe format for less than or equal value pair is: " - # "value:color" - # "\nThe format for greater than value pair is: " - # "+value:color with " - # "color=R,Y,G,M,C.\n Ex: 2.3:C|+4:M") - - mainLayout.addWidget(QtWidgets.QLabel(instruction)) - - def addWidget(self, dataList, rowidx, colidx, foreignkey=False, - choices=None, range=None, fieldName=''): - if dataList is None: - text = str(rowidx) # row number - else: - text = (str(dataList[rowidx][colidx - 1]) - if rowidx < len(dataList) else '') - - if dataList is None: - widget = QtWidgets.QPushButton(text) - elif choices is None and range is None: - widget = QtWidgets.QLineEdit(text) - if fieldName == 'convertFactor': - # precision=6 - validator = QtGui.QDoubleValidator(0.0, 5.0, 6) - validator.setNotation(QtGui.QDoubleValidator.StandardNotation) - widget.setValidator(validator) - if text == '': - widget.setText('1') - elif choices: - widget = QtWidgets.QComboBox() - widget.addItems(choices) - widget.setCurrentText(text) - elif range: - widget = QtWidgets.QSpinBox() - widget.setMinimum(range[0]) - widget.setMaximum(range[1]) - if text in ["", None, 'None']: - widget.setValue(range[0]) - else: - widget.setValue(int(text)) - - if dataList is None: - setWidgetColor(widget) - widget.setFixedWidth(40) - widget.clicked.connect( - lambda: self.rowNumberClicked(widget)) - elif foreignkey: - setWidgetColor(widget, readOnly=True) - else: - new = False if rowidx < len(dataList) else True - setWidgetColor(widget, readOnly=False, changed=new) - if choices is None and range is None: - widget.textChanged.connect( - lambda changedtext: - self.cellInputChange(changedtext, rowidx, colidx)) - elif choices: - widget.currentTextChanged.connect( - lambda changedtext: - self.cellInputChange(changedtext, rowidx, colidx)) - elif range: - widget.valueChanged.connect( - lambda changedtext: - self.cellInputChange(changedtext, rowidx, colidx)) - self.dataTableWidget.setCellWidget(rowidx, colidx, widget) - - def rowNumberClicked(self, widget): - self.dataTableWidget.selectRow(int(widget.text())) - self.dataTableWidget.repaint() - - def createButtonsSection(self): - hLayout = QtWidgets.QHBoxLayout() - - if self.needDataTypeChoice: - self.dataTypeCombobox = QtWidgets.QComboBox(self) - dataTypeRows = executeDB('SELECT * FROM DataTypes') - self.dataTypeCombobox.addItems([d[0] for d in dataTypeRows]) - self.dataTypeCombobox.currentTextChanged.connect( - self.dataTypeChanged) - hLayout.addWidget(self.dataTypeCombobox) - if self.tableName != '': - self.addRowBtn = QtWidgets.QPushButton(self, text='ADD ROW') - self.addRowBtn.setFixedWidth(300) - self.addRowBtn.clicked.connect(self.addNewRow) - hLayout.addWidget(self.addRowBtn) - - self.saveChangesBtn = QtWidgets.QPushButton( - self, text='SAVE CHANGES') - self.saveChangesBtn.setFixedWidth(300) - self.saveChangesBtn.clicked.connect(self.saveChanges) - hLayout.addWidget(self.saveChangesBtn) - - self.closeBtn = QtWidgets.QPushButton(self, text='CLOSE') - self.closeBtn.setFixedWidth(300) - self.closeBtn.clicked.connect(self.close) - hLayout.addWidget(self.closeBtn) - return hLayout - - def createDataTableWidget(self): - self.dataTableWidget = QtWidgets.QTableWidget(self) - self.dataTableWidget.verticalHeader().hide() - self.dataTableWidget.setColumnCount(self.totalCol) - self.dataTableWidget.setHorizontalHeaderLabels(self.columnHeaders) - header = self.dataTableWidget.horizontalHeader() - header.setSectionResizeMode(QtWidgets.QHeaderView.Stretch) - for i in self.resizeContentColumns: - header.setSectionResizeMode( - i, QtWidgets.QHeaderView.ResizeToContents) - - if self.needDataTypeChoice: - self.dataType = self.dataTypeCombobox.currentText() - self.updateDataTableWidgetItems() - - def updateDataTableWidgetItems(self): - self.dataList = self.getDataList() - rowCount = len(self.dataList) - rowCount = 1 if rowCount == 0 else rowCount - self.dataTableWidget.setRowCount(rowCount) - for i in range(len(self.dataList)): - fk = self.checkDataForeignKey(self.dataList[i][0]) - self.addRow(i, fk) - if len(self.dataList) == 0: - """ - No Row, should leave 1 empty row - """ - self.clearFirstRow() - self.update() - - def addNewRow(self): - rowPosition = self.dataTableWidget.rowCount() - self.dataTableWidget.insertRow(rowPosition) - self.addRow(rowPosition) - self.dataTableWidget.scrollToBottom() - self.dataTableWidget.repaint() # to show row's header - self.dataTableWidget.cellWidget(rowPosition, 1).setFocus() - - def removeRow(self, removeRowidx): - self.dataTableWidget.removeRow(removeRowidx) - for i in range(removeRowidx, self.dataTableWidget.rowCount()): - cellWget = self.dataTableWidget.cellWidget(i, 0) - cellWget.setText(str(i)) - - def cellInputChange(self, changedText, rowidx, colidx): - """ - If cell's value is changed, text's color will be red - otherwise, text's color will be black - """ - changed = False - if rowidx < len(self.dataList): - if changedText != self.dataList[rowidx][colidx - 1]: - changed = True - cellWget = self.dataTableWidget.cellWidget(rowidx, colidx) - setWidgetColor(cellWget, changed=changed) - - def checkDataForeignKey(self, val): - if not self.checkFK: - return False - sql = (f"SELECT {self.colName} FROM channels " - f"WHERE {self.colName}='{val}'") - paramRows = executeDB(sql) - if len(paramRows) > 0: - return True - else: - return False - - def resetRowInputs(self, reset, widgetidx, listidx): - for colidx in range(1, self.totalCol): - cellWget = self.dataTableWidget.cellWidget(widgetidx, colidx) - readOnly = False - if hasattr(cellWget, 'isReadOnly'): - readOnly = cellWget.isReadOnly() - if not readOnly: - if reset == 1: - orgVal = self.dataList[listidx][colidx - 1] - if isinstance(cellWget, QtWidgets.QLineEdit): - cellWget.setText(str(orgVal)) - elif isinstance(cellWget, QtWidgets.QComboBox): - cellWget.setCurrentText(str(orgVal)) - elif isinstance(cellWget, QtWidgets.QSpinBox): - cellWget.setValue(int(orgVal)) - setWidgetColor(cellWget) - - def addRow(self, rowidx, fk=False): - pass - - def dataTypeChanged(self): - pass - - def saveChanges(self): - try: - self.dataTableWidget.focusWidget().clearFocus() - except AttributeError: - pass - self.removeCount = 0 - self.insertCount = 0 - self.skipCount = 0 - rowCount = self.dataTableWidget.rowCount() - for i in range(rowCount): - widgetidx = i - (rowCount - self.dataTableWidget.rowCount()) - listidx = i - self.removeCount + self.insertCount - self.skipCount - rowInputs = self.getRowInputs(widgetidx) - reset = self.updateData(rowInputs, widgetidx, listidx) - if reset > -1: - self.resetRowInputs(reset, widgetidx, listidx) - - def updateData(self, row, widgetidx, listidx, insertsql, updatesql): - """ - update dataTableWidget and dataList according to the action to - add, remove, update - :param row: values of processed row in dataTableWidget - :param widgetidx: index of the process rows in dataTableWidget - :param listidx: index of the process rows in self.dataList - :param colName: key db column in the table - :param tableName: data table name - :param insertsql: query to insert a row to the table - :param updatesql: query to update a row in the table - :return -1 for doing nothing - 0 no need to reset input values to org row - 1 need to reset input values to org row - """ - if listidx < len(self.dataList): - orgRow = self.dataList[listidx] - if row[0] == "": - msg = (f"The {self.colName} of '{orgRow}' at row {widgetidx} " - f"has been changed to blank. Are you sure you want to " - f"delete this row in database?") - result = QtWidgets.QMessageBox.question( - self, "Delete %s?" % orgRow, msg, - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No - ) - if result == QtWidgets.QMessageBox.No: - # reset the first widget value and call the function again - # to check other fields - cellWget = self.dataTableWidget.cellWidget(widgetidx, 1) - cellWget.setText(orgRow[0]) - row[0] = orgRow[0] - self.updateData(row, widgetidx, listidx) - else: - sql = (f"DELETE FROM {self.tableName} " - f"WHERE {self.colName}='{orgRow[0]}'") - executeDB(sql) - self.dataList.remove(orgRow) - self.removeRow(widgetidx) - self.removeCount += 1 - return -1 - - if row == orgRow: - return -1 - if (row[0] in [p[0] for p in self.dataList] and - self.dataList[listidx][0] != row[0]): - msg = (f"The {self.colName} of {orgRow} at row" - f" {widgetidx} has been changed to '{row[0]}' " - f"which already is in the database. " - f"It will be changed back to {orgRow[0]}.") - QtWidgets.QMessageBox.information(self, "Error", msg) - # reset the first widget value and call the function again - # to check other fields - cellWget = self.dataTableWidget.cellWidget(widgetidx, 1) - cellWget.setText(orgRow[0]) - row[0] = orgRow[0] - self.updateData(row, widgetidx, listidx) - else: - msg = (f"{orgRow} at row {widgetidx} has " - f"been changed to {row}. Please confirm it.") - result = QtWidgets.QMessageBox.question( - self, "Confirmation", msg, - QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel - ) - if result == QtWidgets.QMessageBox.Cancel: - return 1 - else: - executeDB(updatesql % orgRow[0]) - self.dataList[listidx] = row - return 0 - - if row[0] == "": - msg = (f"Row {widgetidx} has blank {self.colName}. " - f"It will be removed.") - QtWidgets.QMessageBox.information(self, "Error", msg) - self.removeRow(widgetidx) - return -1 - blankRequestedColumns = [self.requestedColumns[i] - for i in self.requestedColumns.keys() - if row[i-1] == ""] - if blankRequestedColumns != []: - msg = (f"Row {widgetidx}: blank " - f"{', '.join(blankRequestedColumns)} which require some " - f"value(s).") - QtWidgets.QMessageBox.information(self, "Error", msg) - return -1 - - if row[0] in [p[0] for p in self.dataList]: - msg = (f"The {self.colName} '{row[0]}' is already in the database." - f" Row {widgetidx} will be removed.") - QtWidgets.QMessageBox.information(self, "Error", msg) - self.removeRow(widgetidx) - return -1 - executeDB(insertsql) - self.dataList.append(row) - self.insertCount += 1 - return 0 diff --git a/sohstationviewer/view/core/plottingWidget.py b/sohstationviewer/view/core/plottingWidget.py deleted file mode 100755 index 53f735180b138bea851f6d7830d3890366af59b9..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/core/plottingWidget.py +++ /dev/null @@ -1,1055 +0,0 @@ - -from PySide2 import QtCore, QtWidgets -from matplotlib.backends.backend_qt5agg import ( - FigureCanvasQTAgg as Canvas) -from matplotlib import pyplot as pl -from matplotlib.patches import ConnectionPatch, Rectangle -from matplotlib.ticker import AutoMinorLocator -import numpy as np - -from sohstationviewer.controller.plottingData import ( - getTitle, getGaps, getTimeTicks, getUnitBitweight, getMassposValueColors) -from sohstationviewer.controller.util import displayTrackingInfo, getVal - -from sohstationviewer.conf import constants -from sohstationviewer.conf.colorSettings import Clr, set_colors -from sohstationviewer.conf.dbSettings import dbConf - -from sohstationviewer.database import extractData - -from sohstationviewer.model.handling_data import trim_downsample_SOHChan - - -BOTTOM = 0.996 -BOTTOM_PX = 200 - - -class PlottingWidget(QtWidgets.QScrollArea): - """ - zorder: - axis spines: 0 - center line: 1 - lines: 2 - gap, dots, : 3 - """ - - def __init__(self, parent, trackingBox, name=''): - super().__init__() - self.name = name - self.parent = parent - self.trackingBox = trackingBox - self.peerPlottingWidgets = [self] - self.plotNo = 0 - self.infoWidget = None - self.widgt = QtWidgets.QWidget(parent) - self.axes = [] - self.currplot_title = None - self.hidden_plots = {} - self.zoomMarker1Shown = False - self.axes = [] - - self.widthBase = 0.25 - self.widthBasePx = 1546 - self.ratioW = 1 - - # width of plotting area - self.plottingW = self.widthBase - # X1: 0.19: Width = 20% of 50in (Figure's width) - # X2: 0.19*2: Width = 40% of 50in - # X4: 0.19*4: Width = 80% of 50 in - # height of plotting area - # + changed when plots are added or removed - # + changed when changing the V-magnify param - - self.plottingBot = BOTTOM - # this is the height of a standard plot - # plotH = 0.01 # 0.01: Height = 1% of 100in (Figure's height) - # left of plotting area: no change - self.plottingLBase = 0.04 - self.plottingLBase = self.plottingLBase - self.plottingH = 0.996 - # this is the height of a standard plot - # plotH = 0.01 # 0.01: Height = 1% of 100in (Figure's height) - # left of plotting area: no change - self.plottingL = 0.03 - # bottom of plot gap, where we start to draw data - # self.plotGapB = 0.990 - # distance from left axis to start of label - self.labelPad = 100 - self.fontSize = 7 - - """ - Set Figure size 50in width, 100in height. - This is the maximum size of plotting container. - add_axes will draw proportion to this size. - The actual view for user based on size of self.widgt. - """ - self.fig = pl.Figure(facecolor='white', figsize=(50, 100)) - self.fig.canvas.mpl_connect('pick_event', self.on_pick_on_artist) - self.fig.canvas.mpl_connect('button_press_event', self.on_pick) - - self.canvas = Canvas(self.fig) - self.canvas.setParent(self.widgt) - self.setWidget(self.widgt) - self.set_colors('B') - - # ============================= EVENT ============================== - def resizeEvent(self, event): - # print("resizeEvent") - geo = self.maximumViewportSize() - # print("resize geo:", geo) - - # set view size fit with the scroll's view port size - self.widgt.setFixedWidth(geo.width()) - self.ratioW = geo.width()/self.widthBasePx - self.plottingW = self.ratioW * self.widthBase - self.plottingL = self.ratioW * self.plottingLBase - if self.plotNo == 0: - self.widgt.setFixedHeight(geo.height()) - - return super(PlottingWidget, self).resizeEvent(event) - - def contextMenuEvent(self, event): - if self.axes == []: - return - contextMenu = QtWidgets.QMenu(self) - removePlotAct = contextMenu.addAction( - "Remove %s" % self.currplot_title) - removePlotAct.setStatusTip("Remove the current Plot") - removePlotAct.triggered.connect(self.hide_currplot) - - if self.hidden_plots != {}: - showAllPlotAct = contextMenu.addAction("Show All Plots") - showAllPlotAct.triggered.connect(self.show_all_hidden_plots) - showPlotActs = [] - for i in sorted(self.hidden_plots.keys()): - showPlotActs.append( - contextMenu.addAction('Show hidden Plot %s' % i)) - showPlotActs[-1].triggered.connect( - lambda *arg, index=i: self.show_hidden_plot(index)) - contextMenu.exec_(self.mapToGlobal(event.pos())) - - def getTimestamp(self, event): - x, y = event.x, event.y # coordinate data - inv = self.axes[0].transData.inverted() - # convert to timestamp, bc event.xdata is None in the space bw axes - xdata = inv.transform((x, y))[0] - return xdata - - def zoomBwMarkers(self, xdata): - if self.currMinX == xdata: - return - self.zoomMarker1Shown = False - [self.currMinX, self.currMaxX] = sorted([self.currMinX, xdata]) - self.set_lim() - self.zoomMarker1.set_visible(False) - self.zoomMarker2.set_visible(False) - - def on_pick(self, event): - """ - xmouse, ymouse = event.mouseevent.xdata, event.mouseevent.ydata - # x, y = artist.get_xdata(), artist.get_ydata() - # ind = event.ind - print('Artist picked:', artist) - # print(self.plots) - # print('Index:', self.plots.index(event.artist)) - # print('{} vertices picked'.format(len(ind))) - # print('Pick between vertices {} and {}'.format( - min(ind), max(ind) + 1)) - print('x, y of mouse: {:.2f},{:.2f}'.format(xmouse, ymouse)) - # print('Data point:', x[ind[0]], y[ind[0]]) - print("mouseevent:", dir(event.mouseevent)) - print("guievent:", dir(event.guiEvent)) - print("modifiers:", event.guiEvent.modifiers()) - """ - # print(dir(event)) - modifiers = event.guiEvent.modifiers() - try: - # on_pick() take place after on_pick_on_artist where - # tps_t was assigned in TimePowerSquareWidget - xdata = self.tps_t - except AttributeError: - xdata = self.getTimestamp(event) - for w in self.peerPlottingWidgets: - if modifiers == QtCore.Qt.ShiftModifier: - w.on_shift_click(xdata) - elif modifiers in [QtCore.Qt.ControlModifier, - QtCore.Qt.MetaModifier]: - w.on_ctrl_cmd_click(xdata) - - else: - if w.zoomMarker1Shown: - w.zoomBwMarkers(xdata) - w.draw() - - def on_ctrl_cmd_click(self, xdata): - """ - On ctrl/cmd + left click - + hide zoomMarker1 - + connect ruler to follow mouse - + show ruler - """ - self.zoomMarker1.set_visible(False) - self.zoomMarker1Shown = False - try: - self.fig.canvas.mpl_disconnect(self.follower) - except AttributeError: - pass - self.set_ruler_visibled(self.ruler, xdata) - - def on_shift_click(self, xdata): - """ - On shift + left click - if there're no zoom marker - + hide ruler - + show zoomMarker1, zoomMarker2 - + connect zoomMarker2 to follow mouse - else: - + zoom data in between 2 zoomMarkers - - """ - if not self.zoomMarker1Shown: - self.ruler.set_visible(False) - self.set_ruler_visibled(self.zoomMarker1, xdata) - self.currMinX = xdata - self.zoomMarker1Shown = True - self.set_ruler_visibled(self.zoomMarker2, xdata) - else: - self.zoomBwMarkers(xdata) - - def on_pick_on_artist(self, event): - artist = event.artist - if isinstance(artist, pl.Axes): - self.currplot = artist - self.currplot_index = self.axes.index(artist) - self.currplot_title = "Plot %s" % self.currplot_index - - def set_ruler_visibled(self, ruler, x): - ruler.set_visible(True) - ruler.xy1 = (x, 0) - ruler.xy2 = (x, self.bottom) - try: - if ruler == self.zoomMarker2: - # make zoomMarker2 follow mouse. - # need to disconnect when state of rulers change - self.follower = self.fig.canvas.mpl_connect( - "motion_notify_event", self.zoomMarker2_follow_mouse) - except AttributeError: - pass - - def zoomMarker2_follow_mouse(self, mouseevent): - xdata = self.getTimestamp(mouseevent) - self.zoomMarker2.xy1 = (xdata, 0) - self.zoomMarker2.xy2 = (xdata, self.bottom) - self.draw() - - def keyPressEvent(self, event): - if event.key() == QtCore.Qt.Key_Escape: - for w in self.peerPlottingWidgets: - w.set_rulers_invisible() - w.zoomMarker1Shown = False - w.draw() - return super(PlottingWidget, self).keyPressEvent(event) - - def set_rulers_invisible(self): - self.ruler.set_visible(False) - self.zoomMarker1.set_visible(False) - self.zoomMarker2.set_visible(False) - - # ============================= END EVENT ============================== - - def add_timestamp_bar(self, usedHeight, top=True, hasLabel=True): - """ - set the axes to display timestampBar on top of the plotting area - setting axis off to not display at the begining - Color in qpeek is DClr[T0] - :return: - """ - self.plottingBot -= usedHeight - timestampBar = self.canvas.figure.add_axes( - [self.plottingL, self.plottingBot, self.plottingW, 0.00005], - ) - timestampBar.axis('off') - timestampBar.xaxis.set_minor_locator(AutoMinorLocator()) - timestampBar.spines['bottom'].set_color(self.displayColor['basic']) - timestampBar.spines['top'].set_color(self.displayColor['basic']) - - if top: - labelbottom = False - else: - labelbottom = True - timestampBar.tick_params(which='major', length=7, width=2, - direction='inout', - colors=self.displayColor['basic'], - labelbottom=labelbottom, - labeltop=not labelbottom) - timestampBar.tick_params(which='minor', length=4, width=1, - direction='inout', - colors=self.displayColor['basic']) - if hasLabel: - timestampBar.set_ylabel('Hours', - fontweight='bold', - fontsize=self.fontSize, - rotation=0, - labelpad=self.labelPad, - ha='left', - color=self.displayColor['basic']) - - # self.update_timestamp_bar(timestampBar) - return timestampBar - - def update_timestamp_bar(self, timestampBar): - times, majorTimes, majorTimeLabels = getTimeTicks( - self.currMinX, self.currMaxX, self.dateMode, self.timeTicksTotal) - timestampBar.axis('on') - timestampBar.set_yticks([]) - timestampBar.set_xticks(times, minor=True) - timestampBar.set_xticks(majorTimes) - timestampBar.set_xticklabels(majorTimeLabels, - fontsize=self.fontSize+2) - timestampBar.set_xlim(self.currMinX, self.currMaxX) - - def create_axes(self, plotB, plotH, hasMinMaxLines=True): - ax = self.canvas.figure.add_axes( - [self.plottingL, plotB, self.plottingW, plotH], - picker=True - ) - - ax.spines['right'].set_visible(False) - ax.spines['left'].set_visible(False) - if hasMinMaxLines: - ax.spines['top'].set_zorder(0) - ax.spines['bottom'].set_zorder(0) - ax.spines['top'].set_color(self.displayColor['sub_basic']) - ax.spines['bottom'].set_color(self.displayColor['sub_basic']) - - ax.set_yticks([]) - ax.set_xticks([]) - ax.tick_params(colors=self.displayColor['basic'], - width=0, - pad=-2, - labelsize=self.fontSize) - ax.patch.set_alpha(0) - return ax - - def setAxesInfo(self, ax, sampleNoList, sampleNoClrList=None, - label=None, info='', y=None, chanDB=None, linkedAx=None): - if label is None: - label = chanDB['label'] - titleVerAlignment = 'center' - # set info undertitle - if linkedAx is not None: - info = label - if info != '': - ax.text( - -0.15, 0.2, - info, - horizontalalignment='left', - verticalalignment='top', - rotation='horizontal', - transform=ax.transAxes, - color=self.displayColor['sub_basic'], - size=self.fontSize - ) - titleVerAlignment = 'top' - - if linkedAx is None: - # set title on left side - color = self.displayColor['plot_label'] - if label.startswith("DEFAULT"): - color = self.displayColor["warning"] - ax.text( - -0.15, 0.6, - label, - horizontalalignment='left', - verticalalignment=titleVerAlignment, - rotation='horizontal', - transform=ax.transAxes, - color=color, - size=self.fontSize + 2 - ) - - # set samples' total on right side - # if sampleNoClrList is None: - # sampleNoClrList = len(sampleNoList) * ['W'] - if len(sampleNoList) == 1: - ax.sampleLbl = ax.text( - 1.005, 0.5, - sampleNoList[0], - horizontalalignment='left', - verticalalignment='center', - rotation='horizontal', - transform=ax.transAxes, - # color=Clr[sampleNoClrList[0]], - color=self.displayColor['basic'], - size=self.fontSize - ) - else: - # bottom - ax.sampleLbl = ax.text( - 1.005, 0.25, - sampleNoList[0], - horizontalalignment='left', - verticalalignment='center', - rotation='horizontal', - transform=ax.transAxes, - # color=Clr[sampleNoClrList[0]], - color=self.displayColor['basic'], - size=self.fontSize - ) - # top - ax.sampleLbl = ax.text( - 1.005, 0.75, - sampleNoList[1], - horizontalalignment='left', - verticalalignment='center', - rotation='horizontal', - transform=ax.transAxes, - # color=Clr[sampleNoClrList[0]], - color=self.displayColor['basic'], - size=self.fontSize - ) - - if y is None: - # draw center line - ax.plot([self.currMinX, self.currMaxX], - [0, 0], - color=self.displayColor['sub_basic'], - linewidth=0.5, - zorder=1 - ) - ax.spines['top'].set_visible(False) - ax.spines['bottom'].set_visible(False) - else: - try: - minY = y.min() - maxY = y.max() - ax.spines['top'].set_visible(True) - ax.spines['bottom'].set_visible(True) - ax.unit_bw = getUnitBitweight(chanDB, self.parent.bitweightOpt) - self.setAxesYlim(ax, minY, maxY) - except Exception: - pass - - def setAxesYlim(self, ax, minY, maxY): - minY = round(minY, 7) - maxY = round(maxY, 7) - if maxY > minY: - ax.set_yticks([minY, maxY]) - ax.set_yticklabels( - [ax.unit_bw.format(minY), ax.unit_bw.format(maxY)]) - if minY == maxY: - maxY += 1 - ax.set_yticks([minY]) - ax.set_yticklabels([ax.unit_bw.format(minY)]) - ax.set_ylim(minY, maxY) - - def addGapBar(self, gaps): - """ - set the axes to display gapBar on top of the plotting area - setting axis off to not display at the begining - :return: - """ - if self.parent.minGap is None: - return - self.gaps = getGaps(gaps, float(self.parent.minGap)) - self.plottingBot -= 0.003 - self.gapBar = self.create_axes(self.plottingBot, - 0.001, - hasMinMaxLines=False) - self.updateGapBar() - - def updateGapBar(self): - gapLabel = "%sm" % self.parent.minGap - # TODO: calculate gap limit - # self.gaps = [] - h = 0.001 # height of rectangle represent gap - self.setAxesInfo(self.gapBar, [len(self.gaps)], - label=gapLabel) - # draw gaps - for i in range(len(self.gaps)): - x = self.gaps[i][0] - w = self.gaps[i][1] - self.gaps[i][ - 0] # width of rectangle represent gap - self.gapBar.add_patch(Rectangle((x, - h / 2), w, h, - color='r', - picker=True, - lw=0., - zorder=3)) # on top of center line - - def get_height(self, ratio, bwPlotsDistance=0.0015): - plotH = 0.0012 * ratio # ratio with figure height - self.plottingBot -= plotH + bwPlotsDistance - self.plottingBotPixel += 19 * ratio - return plotH - - # -------------------- Different color dots ----------------------- # - def plotNone(self): - """ - plot with nothing needed to show rulers - """ - plotH = 0.00001 - bwPlotsDistance = 0.0001 - self.plottingBot -= plotH + bwPlotsDistance - ax = self.create_axes(self.plottingBot, plotH, hasMinMaxLines=False) - ax.x = None - ax.plot([0], [0], linestyle="") - return ax - - def plotMultiColorDots(self, cData, chanDB, chan, ax, linkedAx): - """ - plot scattered dots with colors defined by valueColors: - *:W or -1:_|0:R|2.3:Y|+2.3:G - with colors: RYGMC in dbSettings.py - _: not plot - :param data: data of the channel which is list of (time, value) - :param chanDB: info of channel from DB - :return: - """ - if linkedAx is not None: - ax = linkedAx - if ax is None: - plotH = self.get_height(chanDB['height']) - ax = self.create_axes( - self.plottingBot, plotH, hasMinMaxLines=False) - - x = [] - prevVal = -constants.HIGHEST_INT - - if chanDB['valueColors'] in [None, 'None', '']: - chanDB['valueColors'] = '*:W' - valueColors = chanDB['valueColors'].split('|') - for vc in valueColors: - v, c = vc.split(':') - val = getVal(v) - if c == '_': - prevVal = val - continue - - if v.startswith('+'): - points = [cData['times'][i] - for i in range(len(cData['data'])) - if cData['data'][i] > val] - elif v == '*': - points = cData['times'] - else: - points = [cData['times'][i] - for i in range(len(cData['data'])) - if prevVal < cData['data'][i] <= val] - x += points - - ax.plot(points, len(points) * [0], linestyle="", - marker='s', markersize=0.5, zorder=3, - color=Clr[c], picker=True, pickradius=3) - prevVal = val - - totalSamples = len(x) - - x = sorted(x) - self.setAxesInfo(ax, [totalSamples], chanDB=chanDB, linkedAx=linkedAx) - if linkedAx is None: - ax.x = x - else: - ax.linkedX = x - return ax - - # def plotDotsMasspos(self, cData, chanDB, chan, linkedAx): - # valueColors = getMassposValueColors( - # self.parent.massPosVoltRangeOpt, chan, self.cMode, self.errors) - # if valueColors is None: - # return - # chanDB['valueColors'] = valueColors - # return self.plotMultiColorDots(cData, chanDB, chan, linkedAx) - - # ---------------------------- up/down dots ---------------------------- # - def plotUpDownDots(self, cData, chanDB, chan, ax, linkedAx): - """ - data with 2 different values defined in valueColors - """ - if linkedAx is not None: - ax = linkedAx - if ax is None: - plotH = self.get_height(chanDB['height']) - ax = self.create_axes( - self.plottingBot, plotH, hasMinMaxLines=False) - - valCols = chanDB['valueColors'].split('|') - pointsList = [] - colors = [] - for vc in valCols: - v, c = vc.split(':') - val = getVal(v) - - points = [cData['times'][i] - for i in range(len(cData['data'])) - if cData['data'][i] == val] - pointsList.append(points) - colors.append(c) - - # down dots - ax.plot(pointsList[0], len(pointsList[0]) * [-0.5], linestyle="", - marker='s', markersize=2, zorder=3, - color=Clr[colors[0]], picker=True, pickradius=3) - # up dots - ax.plot(pointsList[1], len(pointsList[1]) * [0.5], linestyle="", - marker='s', markersize=2, zorder=3, - color=Clr[colors[1]], picker=True, pickradius=3) - x = pointsList[0] + pointsList[1] - x = sorted(x) - ax.set_ylim(-2, 2) - self.setAxesInfo(ax, [len(pointsList[0]), len(pointsList[1])], - sampleNoClrList=colors, - chanDB=chanDB, - linkedAx=linkedAx) - if linkedAx is None: - ax.x = x - else: - ax.linkedX = x - return ax - - # ----------------------- dots for times, ignore data------------------- # - def plotTimeDots(self, cData, chanDB, chan, ax, linkedAx): - if linkedAx is not None: - ax = linkedAx - if ax is None: - plotH = self.get_height(chanDB['height']) - ax = self.create_axes(self.plottingBot, plotH) - - color = 'W' - if chanDB['valueColors'] not in [None, 'None', '']: - color = chanDB['valueColors'].strip() - x = cData['times'] - self.setAxesInfo(ax, [len(x)], chanDB=chanDB, linkedAx=linkedAx) - - ax.myPlot = ax.plot(x, [0]*len(x), marker='s', markersize=1.5, - linestyle='', zorder=2, - color=Clr[color], picker=True, - pickradius=3) - if linkedAx is None: - ax.x = x - else: - ax.linkedX = x - return ax - - # ----------------------- lines - one color dots ----------------------- # - def plotLinesDots(self, cData, chanDB, chan, ax, linkedAx, info=''): - """ L:G|D:W """ - if linkedAx is not None: - ax = linkedAx - if ax is None: - plotH = self.get_height(chanDB['height']) - ax = self.create_axes(self.plottingBot, plotH) - - x, y = cData['times'], cData['data'] - self.setAxesInfo(ax, [len(x)], chanDB=chanDB, - info=info, y=y, linkedAx=linkedAx) - colors = {} - if chanDB['valueColors'] not in [None, 'None', '']: - colorParts = chanDB['valueColors'].split('|') - for cStr in colorParts: - obj, c = cStr.split(':') - colors[obj] = c - - lColor = 'G' - hasDot = False - if 'L' in colors: - lColor = colors['L'] - if 'D' in colors: - dColor = colors['D'] - hasDot = True - - if not hasDot: - ax.myPlot = ax.plot(x, y, - linestyle='-', linewidth=0.7, - color=Clr[lColor]) - else: - ax.myPlot = ax.plot(x, y, marker='s', markersize=1.5, - linestyle='-', linewidth=0.7, zorder=2, - color=Clr[lColor], - markerfacecolor=Clr[dColor], - picker=True, pickradius=3) - if linkedAx is None: - ax.x = x - ax.y = y - else: - ax.linkedX = x - ax.linkedY = y - return ax - - def plotLinesSRate(self, cData, chanDB, chan, ax, linkedAx): - """ - multi-line line seismic, one color, line only, - can apply bit weights in (get_unit_bitweight()) - """ - if cData['samplerate'] >= 1.0: - info = "%dsps" % cData['samplerate'] - else: - info = "%gsps" % cData['samplerate'] - return self.plotLinesDots(cData, chanDB, chan, ax, linkedAx, info=info) - - # ----------------------- lines - multi-color dots --------------------- # - def plotLinesMasspos(self, cData, chanDB, chan, ax, linkedAx): - valueColors = getMassposValueColors( - self.parent.massPosVoltRangeOpt, chan, - self.cMode, self.errors, retType='tupleList') - - if valueColors is None: - return - - if ax is None: - plotH = self.get_height(chanDB['height']) - ax = self.create_axes(self.plottingBot, plotH) - - ax.x, ax.y = cData['times'], cData['data'] - self.setAxesInfo(ax, [len(ax.x)], chanDB=chanDB, y=ax.y) - ax.myPlot = ax.plot(ax.x, ax.y, - linestyle='-', linewidth=0.7, - color=self.displayColor['sub_basic'], - zorder=2)[0] - colors = [None] * len(ax.y) - sizes = [0.5] * len(ax.y) - for i in range(len(ax.y)): - count = 0 - prevV = 0 - for v, c in valueColors: - if count < (len(valueColors) - 1): - if prevV < abs(ax.y[i]) <= v: - colors[i] = Clr[c] - break - else: - # if abs(ax.y[i]) > v: - colors[i] = Clr[c] - break - prevV = v - count += 1 - ax.scatter(ax.x, ax.y, marker='s', c=colors, s=sizes, zorder=3) - return ax - # ---------------------------------------------------------# - - def add_ruler(self, color): - ruler = ConnectionPatch( - xyA=(0, 0), - xyB=(0, self.bottom), - coordsA="data", - coordsB="data", - axesA=self.timestampBarTop, - axesB=self.timestampBarBottom, - color=color, - ) - ruler.set_visible(False) - self.timestampBarBottom.add_artist(ruler) - return ruler - - def set_lim(self, orgSize=False): - if not orgSize: - for chanID in self.plottingData1: - cData = self.plottingData1[chanID] - self.getZoomData(cData, chanID) - for chanID in self.plottingData2: - cData = self.plottingData2[chanID] - self.getZoomData(cData, chanID) - self.update_timestamp_bar(self.timestampBarTop) - self.update_timestamp_bar(self.timestampBarBottom) - if hasattr(self, 'gapBar'): - self.gapBar.set_xlim(self.currMinX, self.currMaxX) - if not orgSize: - newGaps = [g for g in self.gaps - if (self.currMinX <= g[0] <= self.currMaxX - or self.currMinX <= g[1] <= self.currMaxX)] - - # reset total of samples on the right - self.gapBar.sampleLbl.set_text(len(newGaps)) - for ax in self.axes: - ax.set_xlim(self.currMinX, self.currMaxX) - if ax.x is None: - # the plotNone bar is at the end, no need to process - break - if not orgSize: - # x, y - newXIndexes = np.where((ax.x >= self.currMinX) & - (ax.x <= self.currMaxX)) - - # reset total of samples on the right - ax.sampleLbl.set_text(len(newXIndexes)) - - if len(newXIndexes) == 0: - continue - if hasattr(ax, 'y'): - # don't need to reset y range if ax.y not exist - newX = ax.x[newXIndexes] - newMinX = min(newX) - newMaxX = max(newX) - try: - newMinXIndex = ax.x.index(newMinX) - newMaxXIndex = ax.x.index(newMaxX) - except AttributeError: - newMinXIndex = np.where(ax.x == newMinX)[0][0] - newMaxXIndex = np.where(ax.x == newMaxX)[0][0] - newY = ax.y[newMinXIndex:newMaxXIndex + 1] - newMinY = min(newY) - newMaxY = max(newY) - self.setAxesYlim(ax, newMinY, newMaxY) - - def set_title(self, title, y=100, valight='top'): - self.fig.text(-0.15, y, title, - verticalalignment=valight, - horizontalalignment='left', - transform=self.timestampBarTop.transAxes, - color=self.displayColor['basic'], - size=self.fontSize) - - def draw(self): - try: - self.canvas.draw() - # a bug on mac: - # not showing updated info until clicking on another window - # fix by calling repaint() - self.widgt.repaint() - except TypeError: - pass - - # ######## Functions for outside world ##### - def init_size(self): - geo = self.maximumViewportSize() - if self.plotNo == 0: - # set view size fit with the scroll's view port size - self.widgt.setFixedWidth(geo.width()) - self.widgt.setFixedHeight(geo.height()) - - def set_msg_widget(self, msgWidget): - self.msgWidget = msgWidget - - def set_background_color(self, color='black'): - self.fig.patch.set_facecolor(color) - self.draw() - - def resetView(self): - """ - reset all zooms back to the first plotting - """ - if self.axes == []: - return - self.currMinX = self.minX - self.currMaxX = self.maxX - self.set_lim() - self.draw() - - def clear(self): - if self.zoomMarker1.get_visible(): - self.zoomMarker1.set_visible(False) - else: - self.zoomMarker1.set_visible(True) - # self.fig.clear() - # self.axes = [] - self.draw() - - def applyConvertFactor(self, cData, convertFactor): - """ - convertFactor = 150mV/count = 150V/1000count - => unit data * convertFactor= data *150/1000 V - """ - cData['data'] = np.multiply(cData['data'], [convertFactor]) - - # =============================== axes ================================ - def plot_channels(self, startTm, endTm, key, dataTime, - gaps, channelList, timeTicksTotal, - plottingData1, plottingData2): - """ - :param key: staID for mseed, (device, expNo) for reftek - :param plottingData: a ditionary including: - { gaps: [(t1,t2),(t1,t2),...] (in epoch time) - channels:{cha: {netID, statID, locID, chanID, times, data, - samplerate, startTmEpoch, endTmEpoch} # - earliestUTC: the earliest time of all channels - latestUTC: the latest time of all channels - :timeTicksTotal: max number of tick to show on time bar - - Data set: {channelname: [(x,y), (x,y)...] - """ - self.plottingData1 = plottingData1 - self.plottingData2 = plottingData2 - self.processingLog = [] # [(message, type)] - self.errors = [] - if self.axes != []: - self.fig.clear() - self.dateMode = self.parent.dateFormat.upper() - self.timeTicksTotal = timeTicksTotal - - self.minX = self.currMinX = max(dataTime[0], startTm) - self.maxX = self.currMaxX = min(dataTime[1], endTm) - self.plotNo = len(self.plottingData1) + len(self.plottingData2) - title = getTitle(key, self.minX, self.maxX, self.dateMode) - self.plottingBot = BOTTOM - self.plottingBotPixel = BOTTOM_PX - self.axes = [] - - self.timestampBarTop = self.add_timestamp_bar(0.003) - self.set_title(title) - self.addGapBar(gaps) - notFoundChan = [c for c in channelList - if c not in self.plottingData1.keys()] - if len(notFoundChan) > 0: - msg = (f"The following channels is in Channel Preferences but " - f"not in the given data: {notFoundChan}") - self.processingLog.append((msg, 'warning')) - - for chanID in self.plottingData1: - chanDB = extractData.getChanPlotInfo(chanID, self.parent.dataType) - if chanDB['height'] == 0: - # not draw - continue - if chanDB['channel'] == 'DEFAULT': - msg = (f"Channel {chanID}'s " - f"definition can't be found database.") - displayTrackingInfo(self.trackingBox, msg, 'warning') - - if chanDB['plotType'] == '': - continue - self.plottingData1[chanID]['chanDB'] = chanDB - self.getZoomData(self.plottingData1[chanID], chanID, - self.plottingData1, True) - - for chanID in self.plottingData2: - chanDB = extractData.getChanPlotInfo(chanID, self.parent.dataType) - self.plottingData2[chanID]['chanDB'] = chanDB - self.getZoomData(self.plottingData2[chanID], chanID, True) - - self.axes.append(self.plotNone()) - self.timestampBarBottom = self.add_timestamp_bar(0.003, top=False) - self.set_lim(orgSize=True) - self.bottom = self.axes[-1].get_ybound()[0] - self.ruler = self.add_ruler(self.displayColor['time_ruler']) - self.zoomMarker1 = self.add_ruler(self.displayColor['zoom_marker']) - self.zoomMarker2 = self.add_ruler(self.displayColor['zoom_marker']) - # Set view size fit with the given data - if self.widgt.geometry().height() < self.plottingBotPixel: - self.widgt.setFixedHeight(self.plottingBotPixel) - - self.draw() - - def getZoomData(self, cData, chanID, plottingData=None, firsttime=False): - """ - :param setID: (netID, statID, locID) - :param plottingData: a ditionary including: - { gaps: [(t1,t2),(t1,t2),...] (in epoch time) - channels:{cha: {netID, statID, locID, chanID, times, data, - samplerate, startTmEpoch, endTmEpoch} # - earliestUTC: the earliest time of all channels - latestUTC: the latest time of all channels - :timeTicksTotal: max number of tick to show on time bar - - Data set: {channelname: [(x,y), (x,y)...] - """ - chanDB = cData['chanDB'] - plotType = chanDB['plotType'] - trim_downsample_SOHChan(cData, self.currMinX, self.currMaxX, firsttime) - self.applyConvertFactor(cData, 1) - if 'ax' not in cData: - linkedAx = None - if chanDB['linkedChan'] not in [None, 'None', '']: - try: - linkedAx = plottingData['channels'][ - chanDB['linkedChan']]['ax'] - except KeyError: - pass - ax = getattr(self, dbConf['plotFunc'][plotType][1])( - cData, chanDB, chanID, None, linkedAx) - if ax is None: - return - cData['ax'] = ax - ax.chan = chanID - self.axes.append(ax) - else: - getattr(self, dbConf['plotFunc'][plotType][1])( - cData, chanDB, chanID, cData['ax'], None) - - def hide_plots(self, plot_indexes): - if self.axes == []: - return - plot_indexes = sorted(plot_indexes) - idx = 0 - total_h = 0 - for i in range(plot_indexes[0], len(self.axes)): - pos = self.axes[i].get_position() - pos.y0 += total_h - pos.y1 += total_h - if idx < len(plot_indexes) and i == plot_indexes[idx]: - h = pos.y1 - pos.y0 - total_h += h - pos.y0 = pos.y1 - idx += 1 - self.hidden_plots[i] = h - self.axes[i].set_position(pos) - - # currently consider every plot height are all 100px height - height = self.widgt.geometry().height() - 100 * len(plot_indexes) - self.widgt.setFixedHeight(height) - self.draw() - - def hide_currplot(self): - pos = self.currplot.get_position() - h = pos.y1 - pos.y0 - pos.y0 = pos.y1 - self.currplot.set_position(pos) - for i in range(self.currplot_index + 1, len(self.axes)): - pos = self.axes[i].get_position() - pos.y0 += h - pos.y1 += h - self.axes[i].set_position(pos) - # currently consider every plot height are all 100px height - height = self.widgt.geometry().height() - 100 - self.widgt.setFixedHeight(height) - self.hidden_plots[self.currplot_index] = h - self.draw() - - def show_hidden_plot(self, index): - h = self.hidden_plots[index] - workplot = self.axes[index] - pos = workplot.get_position() - pos.y1 = pos.y0 + h - workplot.set_position(pos) - for i in range(index, len(self.axes)): - pos = self.axes[i].get_position() - pos.y0 -= h - pos.y1 -= h - self.axes[i].set_position(pos) - # currently consider every plot height are all 100px height - height = self.widgt.geometry().height() + 100 - self.widgt.setFixedHeight(height) - del self.hidden_plots[index] - pos = workplot.get_position() - self.draw() - - def show_all_hidden_plots(self): - plot_indexes = sorted(self.hidden_plots.keys()) - idx = 0 - total_h = 0 - for i in range(plot_indexes[0], len(self.axes)): - pos = self.axes[i].get_position() - if idx < len(plot_indexes) and i == plot_indexes[idx]: - h = self.hidden_plots[i] - total_h += h - pos.y1 = pos.y0 + h - idx += 1 - del self.hidden_plots[i] - pos.y0 -= total_h - pos.y1 -= total_h - self.axes[i].set_position(pos) - - # currently consider every plot height are all 100px height - height = self.widgt.geometry().height() + 100 * len(plot_indexes) - self.widgt.setFixedHeight(height) - self.draw() - - def rePlot(self): - for ax in self.axes: - print("pos", self.ax.get_position()) - - # ============================= END AXES ============================== - def set_colors(self, mode): - self.cMode = mode - self.displayColor = set_colors(mode) - self.fig.patch.set_facecolor(self.displayColor['background']) - - def setPeerPlottingWidgets(self, widgets): - self.peerPlottingWidgets = widgets diff --git a/sohstationviewer/view/core/plotting_widget.py b/sohstationviewer/view/core/plotting_widget.py new file mode 100755 index 0000000000000000000000000000000000000000..e4452faf812061a47153083b923353827d6a7748 --- /dev/null +++ b/sohstationviewer/view/core/plotting_widget.py @@ -0,0 +1,1066 @@ +from PySide2 import QtCore, QtWidgets +from matplotlib.backends.backend_qt5agg import ( + FigureCanvasQTAgg as Canvas) +from matplotlib import pyplot as pl +from matplotlib.patches import ConnectionPatch, Rectangle +from matplotlib.ticker import AutoMinorLocator +import numpy as np + +from sohstationviewer.controller.plottingData import ( + getTitle, getGaps, getTimeTicks, getUnitBitweight, getMassposValueColors) +from sohstationviewer.controller.util import displayTrackingInfo, getVal + +from sohstationviewer.conf import constants +from sohstationviewer.conf.colorSettings import Clr, set_colors +from sohstationviewer.conf.dbSettings import dbConf + +from sohstationviewer.database import extractData + +from sohstationviewer.model.handling_data import trim_downsample_SOHChan + +BOTTOM = 0.996 +BOTTOM_PX = 200 + + +class PlottingWidget(QtWidgets.QScrollArea): + """ + zorder: + axis spines: 0 + center line: 1 + lines: 2 + gap, dots, : 3 + """ + + def __init__(self, parent, tracking_box, name=''): + super().__init__() + self.name = name + self.parent = parent + self.tracking_box = tracking_box + self.peer_plotting_widgets = [self] + self.plot_no = 0 + self.info_widget = None + self.widgt = QtWidgets.QWidget(parent) + self.axes = [] + self.curr_plot_title = None + self.hidden_plots = {} + self.zoom_marker1_shown = False + self.axes = [] + + self.width_base = 0.25 + self.width_base_px = 1546 + self.ratio_w = 1 + + # width of plotting area + self.plotting_w = self.width_base + # X1: 0.19: Width = 20% of 50in (Figure's width) + # X2: 0.19*2: Width = 40% of 50in + # X4: 0.19*4: Width = 80% of 50 in + # height of plotting area + # + changed when plots are added or removed + # + changed when changing the V-magnify param + + self.plotting_bot = BOTTOM + # this is the height of a standard plot + # plot_h = 0.01 # 0.01: Height = 1% of 100in (Figure's height) + # left of plotting area: no change + self.plotting_l_base = 0.04 + self.plotting_l_base = self.plotting_l_base + self.plotting_h = 0.996 + # this is the height of a standard plot + # plot_h = 0.01 # 0.01: Height = 1% of 100in (Figure's height) + # left of plotting area: no change + self.plotting_l = 0.03 + # bottom of plot gap, where we start to draw data + # self.plotGapB = 0.990 + # distance from left axis to start of label + self.label_pad = 100 + self.font_size = 7 + + """ + Set Figure size 50in width, 100in height. + This is the maximum size of plotting container. + add_axes will draw proportion to this size. + The actual view for user based on size of self.widgt. + """ + self.fig = pl.Figure(facecolor='white', figsize=(50, 100)) + self.fig.canvas.mpl_connect('pick_event', self.on_pick_on_artist) + self.fig.canvas.mpl_connect('button_press_event', self.on_pick) + + self.canvas = Canvas(self.fig) + self.canvas.setParent(self.widgt) + self.setWidget(self.widgt) + self.set_colors('B') + + # ============================= EVENT ============================== + def resizeEvent(self, event): + # print("resizeEvent") + geo = self.maximumViewportSize() + # print("resize geo:", geo) + + # set view size fit with the scroll's view port size + self.widgt.setFixedWidth(geo.width()) + self.ratio_w = geo.width() / self.width_base_px + self.plotting_w = self.ratio_w * self.width_base + self.plotting_l = self.ratio_w * self.plotting_l_base + if self.plot_no == 0: + self.widgt.setFixedHeight(geo.height()) + + return super(PlottingWidget, self).resizeEvent(event) + + def contextMenuEvent(self, event): + if self.axes == []: + return + context_menu = QtWidgets.QMenu(self) + remove_plot_act = context_menu.addAction( + "Remove %s" % self.curr_plot_title) + remove_plot_act.setStatusTip("Remove the current Plot") + remove_plot_act.triggered.connect(self.hide_curr_plot) + + if self.hidden_plots != {}: + show_all_plot_act = context_menu.addAction("Show All Plots") + show_all_plot_act.triggered.connect(self.show_all_hidden_plots) + show_plot_acts = [] + for i in sorted(self.hidden_plots.keys()): + show_plot_acts.append( + context_menu.addAction('Show hidden Plot %s' % i)) + show_plot_acts[-1].triggered.connect( + lambda *arg, index=i: self.show_hidden_plot(index)) + context_menu.exec_(self.mapToGlobal(event.pos())) + + def get_timestamp(self, event): + x, y = event.x, event.y # coordinate data + inv = self.axes[0].transData.inverted() + # convert to timestamp, bc event.xdata is None in the space bw axes + xdata = inv.transform((x, y))[0] + return xdata + + def zoom_bw_markers(self, xdata): + if self.curr_min_x == xdata: + return + self.zoom_marker1_shown = False + [self.curr_min_x, self.curr_max_x] = sorted([self.curr_min_x, xdata]) + self.set_lim() + self.zoom_marker1.set_visible(False) + self.zoom_marker2.set_visible(False) + + def on_pick(self, event): + """ + xmouse, ymouse = event.mouseevent.xdata, event.mouseevent.ydata + # x, y = artist.get_xdata(), artist.get_ydata() + # ind = event.ind + print('Artist picked:', artist) + # print(self.plots) + # print('Index:', self.plots.index(event.artist)) + # print('{} vertices picked'.format(len(ind))) + # print('Pick between vertices {} and {}'.format( + min(ind), max(ind) + 1)) + print('x, y of mouse: {:.2f},{:.2f}'.format(xmouse, ymouse)) + # print('Data point:', x[ind[0]], y[ind[0]]) + print("mouseevent:", dir(event.mouseevent)) + print("guievent:", dir(event.guiEvent)) + print("modifiers:", event.guiEvent.modifiers()) + """ + # print(dir(event)) + modifiers = event.guiEvent.modifiers() + try: + # on_pick() take place after on_pick_on_artist where + # tps_t was assigned in TimePowerSquareWidget + xdata = self.tps_t + except AttributeError: + xdata = self.get_timestamp(event) + for w in self.peer_plotting_widgets: + if modifiers == QtCore.Qt.ShiftModifier: + w.on_shift_click(xdata) + elif modifiers in [QtCore.Qt.ControlModifier, + QtCore.Qt.MetaModifier]: + w.on_ctrl_cmd_click(xdata) + + else: + if w.zoom_marker1_shown: + w.zoom_bw_markers(xdata) + w.draw() + + def on_ctrl_cmd_click(self, xdata): + """ + On ctrl/cmd + left click + + hide zoom_marker1 + + connect ruler to follow mouse + + show ruler + """ + self.zoom_marker1.set_visible(False) + self.zoom_marker1_shown = False + try: + self.fig.canvas.mpl_disconnect(self.follower) + except AttributeError: + pass + self.set_ruler_visibled(self.ruler, xdata) + + def on_shift_click(self, xdata): + """ + On shift + left click + if there're no zoom marker + + hide ruler + + show zoom_marker1, zoom_marker2 + + connect zoom_marker2 to follow mouse + else: + + zoom data in between 2 zoomMarkers + + """ + if not self.zoom_marker1_shown: + self.ruler.set_visible(False) + self.set_ruler_visibled(self.zoom_marker1, xdata) + self.curr_min_x = xdata + self.zoom_marker1_shown = True + self.set_ruler_visibled(self.zoom_marker2, xdata) + else: + self.zoom_bw_markers(xdata) + + def on_pick_on_artist(self, event): + artist = event.artist + if isinstance(artist, pl.Axes): + self.curr_plot = artist + self.curr_plot_index = self.axes.index(artist) + self.curr_plot_title = "Plot %s" % self.curr_plot_index + + def set_ruler_visibled(self, ruler, x): + ruler.set_visible(True) + ruler.xy1 = (x, 0) + ruler.xy2 = (x, self.bottom) + try: + if ruler == self.zoom_marker2: + # make zoom_marker2 follow mouse. + # need to disconnect when state of rulers change + self.follower = self.fig.canvas.mpl_connect( + "motion_notify_event", self.zoom_marker2_follow_mouse) + except AttributeError: + pass + + def zoom_marker2_follow_mouse(self, mouseevent): + xdata = self.get_timestamp(mouseevent) + self.zoom_marker2.xy1 = (xdata, 0) + self.zoom_marker2.xy2 = (xdata, self.bottom) + self.draw() + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + for w in self.peer_plotting_widgets: + w.set_rulers_invisible() + w.zoom_marker1_shown = False + w.draw() + return super(PlottingWidget, self).keyPressEvent(event) + + def set_rulers_invisible(self): + self.ruler.set_visible(False) + self.zoom_marker1.set_visible(False) + self.zoom_marker2.set_visible(False) + + # ============================= END EVENT ============================== + + def add_timestamp_bar(self, used_height, top=True, has_label=True): + """ + set the axes to display timestamp_bar on top of the plotting area + setting axis off to not display at the begining + Color in qpeek is DClr[T0] + :return: + """ + self.plotting_bot -= used_height + timestamp_bar = self.canvas.figure.add_axes( + [self.plotting_l, self.plotting_bot, self.plotting_w, 0.00005], + ) + timestamp_bar.axis('off') + timestamp_bar.xaxis.set_minor_locator(AutoMinorLocator()) + timestamp_bar.spines['bottom'].set_color(self.display_color['basic']) + timestamp_bar.spines['top'].set_color(self.display_color['basic']) + + if top: + labelbottom = False + else: + labelbottom = True + timestamp_bar.tick_params(which='major', length=7, width=2, + direction='inout', + colors=self.display_color['basic'], + labelbottom=labelbottom, + labeltop=not labelbottom) + timestamp_bar.tick_params(which='minor', length=4, width=1, + direction='inout', + colors=self.display_color['basic']) + if has_label: + timestamp_bar.set_ylabel('Hours', + fontweight='bold', + fontsize=self.font_size, + rotation=0, + labelpad=self.label_pad, + ha='left', + color=self.display_color['basic']) + + # self.update_timestamp_bar(timestamp_bar) + return timestamp_bar + + def update_timestamp_bar(self, timestamp_bar): + times, major_times, major_time_labels = getTimeTicks( + self.curr_min_x, self.curr_max_x, self.date_mode, + self.time_ticks_total + ) + timestamp_bar.axis('on') + timestamp_bar.set_yticks([]) + timestamp_bar.set_xticks(times, minor=True) + timestamp_bar.set_xticks(major_times) + timestamp_bar.set_xticklabels(major_time_labels, + fontsize=self.font_size + 2) + timestamp_bar.set_xlim(self.curr_min_x, self.curr_max_x) + + def create_axes(self, plot_b, plot_h, has_min_max_lines=True): + ax = self.canvas.figure.add_axes( + [self.plotting_l, plot_b, self.plotting_w, plot_h], + picker=True + ) + + ax.spines['right'].set_visible(False) + ax.spines['left'].set_visible(False) + if has_min_max_lines: + ax.spines['top'].set_zorder(0) + ax.spines['bottom'].set_zorder(0) + ax.spines['top'].set_color(self.display_color['sub_basic']) + ax.spines['bottom'].set_color(self.display_color['sub_basic']) + + ax.set_yticks([]) + ax.set_xticks([]) + ax.tick_params(colors=self.display_color['basic'], + width=0, + pad=-2, + labelsize=self.font_size) + ax.patch.set_alpha(0) + return ax + + def set_axes_info(self, ax, sample_no_list, sample_no_clr_list=None, + label=None, info='', y=None, chan_db=None, + linked_ax=None): + if label is None: + label = chan_db['label'] + title_ver_alignment = 'center' + # set info undertitle + if linked_ax is not None: + info = label + if info != '': + ax.text( + -0.15, 0.2, + info, + horizontalalignment='left', + verticalalignment='top', + rotation='horizontal', + transform=ax.transAxes, + color=self.display_color['sub_basic'], + size=self.font_size + ) + title_ver_alignment = 'top' + + if linked_ax is None: + # set title on left side + color = self.display_color['plot_label'] + if label.startswith("DEFAULT"): + color = self.display_color["warning"] + ax.text( + -0.15, 0.6, + label, + horizontalalignment='left', + verticalalignment=title_ver_alignment, + rotation='horizontal', + transform=ax.transAxes, + color=color, + size=self.font_size + 2 + ) + + # set samples' total on right side + # if sample_no_clr_list is None: + # sample_no_clr_list = len(sample_no_list) * ['W'] + if len(sample_no_list) == 1: + ax.sampleLbl = ax.text( + 1.005, 0.5, + sample_no_list[0], + horizontalalignment='left', + verticalalignment='center', + rotation='horizontal', + transform=ax.transAxes, + # color=Clr[sample_no_clr_list[0]], + color=self.display_color['basic'], + size=self.font_size + ) + else: + # bottom + ax.sampleLbl = ax.text( + 1.005, 0.25, + sample_no_list[0], + horizontalalignment='left', + verticalalignment='center', + rotation='horizontal', + transform=ax.transAxes, + # color=Clr[sample_no_clr_list[0]], + color=self.display_color['basic'], + size=self.font_size + ) + # top + ax.sampleLbl = ax.text( + 1.005, 0.75, + sample_no_list[1], + horizontalalignment='left', + verticalalignment='center', + rotation='horizontal', + transform=ax.transAxes, + # color=Clr[sample_no_clr_list[0]], + color=self.display_color['basic'], + size=self.font_size + ) + + if y is None: + # draw center line + ax.plot([self.curr_min_x, self.curr_max_x], + [0, 0], + color=self.display_color['sub_basic'], + linewidth=0.5, + zorder=1 + ) + ax.spines['top'].set_visible(False) + ax.spines['bottom'].set_visible(False) + else: + try: + min_y = y.min() + max_y = y.max() + ax.spines['top'].set_visible(True) + ax.spines['bottom'].set_visible(True) + ax.unit_bw = getUnitBitweight( + chan_db, self.parent.bitweightOpt + ) + self.set_axes_ylim(ax, min_y, max_y) + except Exception: + pass + + def set_axes_ylim(self, ax, min_y, max_y): + min_y = round(min_y, 7) + max_y = round(max_y, 7) + if max_y > min_y: + ax.set_yticks([min_y, max_y]) + ax.set_yticklabels( + [ax.unit_bw.format(min_y), ax.unit_bw.format(max_y)]) + if min_y == max_y: + max_y += 1 + ax.set_yticks([min_y]) + ax.set_yticklabels([ax.unit_bw.format(min_y)]) + ax.set_ylim(min_y, max_y) + + def add_gap_bar(self, gaps): + """ + set the axes to display gap_bar on top of the plotting area + setting axis off to not display at the beginning + :return: + """ + if self.parent.minGap is None: + return + self.gaps = getGaps(gaps, float(self.parent.minGap)) + self.plotting_bot -= 0.003 + self.gap_bar = self.create_axes(self.plotting_bot, + 0.001, + has_min_max_lines=False) + self.update_gap_bar() + + def update_gap_bar(self): + gap_label = "%sm" % self.parent.minGap + # TODO: calculate gap limit + # self.gaps = [] + h = 0.001 # height of rectangle represent gap + self.set_axes_info(self.gap_bar, [len(self.gaps)], + label=gap_label) + # draw gaps + for i in range(len(self.gaps)): + x = self.gaps[i][0] + w = self.gaps[i][1] - self.gaps[i][ + 0] # width of rectangle represent gap + self.gap_bar.add_patch( + Rectangle( + (x, - h / 2), w, h, color='r', picker=True, lw=0., zorder=3 + ) + ) # on top of center line + + def get_height(self, ratio, bw_plots_distance=0.0015): + plot_h = 0.0012 * ratio # ratio with figure height + self.plotting_bot -= plot_h + bw_plots_distance + self.plotting_bot_pixel += 19 * ratio + return plot_h + + # -------------------- Different color dots ----------------------- # + def plot_none(self): + """ + plot with nothing needed to show rulers + """ + plot_h = 0.00001 + bw_plots_distance = 0.0001 + self.plotting_bot -= plot_h + bw_plots_distance + ax = self.create_axes(self.plotting_bot, plot_h, + has_min_max_lines=False) + ax.x = None + ax.plot([0], [0], linestyle="") + return ax + + def plot_multi_color_dots(self, c_data, chan_db, chan, ax, linked_ax): + """ + plot scattered dots with colors defined by valueColors: + *:W or -1:_|0:R|2.3:Y|+2.3:G + with colors: RYGMC in dbSettings.py + _: not plot + :param data: data of the channel which is list of (time, value) + :param chan_db: info of channel from DB + :return: + """ + if linked_ax is not None: + ax = linked_ax + if ax is None: + plot_h = self.get_height(chan_db['height']) + ax = self.create_axes( + self.plotting_bot, plot_h, has_min_max_lines=False) + + x = [] + prev_val = -constants.HIGHEST_INT + + if chan_db['valueColors'] in [None, 'None', '']: + chan_db['valueColors'] = '*:W' + value_colors = chan_db['valueColors'].split('|') + for vc in value_colors: + v, c = vc.split(':') + val = getVal(v) + if c == '_': + prev_val = val + continue + + if v.startswith('+'): + points = [c_data['times'][i] + for i in range(len(c_data['data'])) + if c_data['data'][i] > val] + elif v == '*': + points = c_data['times'] + else: + points = [c_data['times'][i] + for i in range(len(c_data['data'])) + if prev_val < c_data['data'][i] <= val] + x += points + + ax.plot(points, len(points) * [0], linestyle="", + marker='s', markersize=0.5, zorder=3, + color=Clr[c], picker=True, pickradius=3) + prev_val = val + + total_samples = len(x) + + x = sorted(x) + self.set_axes_info(ax, [total_samples], chan_db=chan_db, + linked_ax=linked_ax) + if linked_ax is None: + ax.x = x + else: + ax.linkedX = x + return ax + + # def plotDotsMasspos(self, c_data, chan_db, chan, linked_ax): + # valueColors = getMassposValueColors( + # self.parent.massPosVoltRangeOpt, chan, self.cMode, self.errors) + # if valueColors is None: + # return + # chan_db['valueColors'] = valueColors + # return self.plot_multi_color_dots(c_data, chan_db, chan, linked_ax) + + # ---------------------------- up/down dots ---------------------------- # + def plot_up_down_dots(self, c_data, chan_db, chan, ax, linked_ax): + """ + data with 2 different values defined in valueColors + """ + if linked_ax is not None: + ax = linked_ax + if ax is None: + plot_h = self.get_height(chan_db['height']) + ax = self.create_axes( + self.plotting_bot, plot_h, has_min_max_lines=False) + + val_cols = chan_db['valueColors'].split('|') + points_list = [] + colors = [] + for vc in val_cols: + v, c = vc.split(':') + val = getVal(v) + + points = [c_data['times'][i] + for i in range(len(c_data['data'])) + if c_data['data'][i] == val] + points_list.append(points) + colors.append(c) + + # down dots + ax.plot(points_list[0], len(points_list[0]) * [-0.5], linestyle="", + marker='s', markersize=2, zorder=3, + color=Clr[colors[0]], picker=True, pickradius=3) + # up dots + ax.plot(points_list[1], len(points_list[1]) * [0.5], linestyle="", + marker='s', markersize=2, zorder=3, + color=Clr[colors[1]], picker=True, pickradius=3) + x = points_list[0] + points_list[1] + x = sorted(x) + ax.set_ylim(-2, 2) + self.set_axes_info(ax, [len(points_list[0]), len(points_list[1])], + sample_no_clr_list=colors, + chan_db=chan_db, + linked_ax=linked_ax) + if linked_ax is None: + ax.x = x + else: + ax.linkedX = x + return ax + + # ----------------------- dots for times, ignore data------------------- # + def plot_time_dots(self, c_data, chan_db, chan, ax, linked_ax): + if linked_ax is not None: + ax = linked_ax + if ax is None: + plot_h = self.get_height(chan_db['height']) + ax = self.create_axes(self.plotting_bot, plot_h) + + color = 'W' + if chan_db['valueColors'] not in [None, 'None', '']: + color = chan_db['valueColors'].strip() + x = c_data['times'] + self.set_axes_info(ax, [len(x)], chan_db=chan_db, linked_ax=linked_ax) + + ax.myPlot = ax.plot(x, [0] * len(x), marker='s', markersize=1.5, + linestyle='', zorder=2, + color=Clr[color], picker=True, + pickradius=3) + if linked_ax is None: + ax.x = x + else: + ax.linkedX = x + return ax + + # ----------------------- lines - one color dots ----------------------- # + def plot_lines_dots(self, c_data, chan_db, chan, ax, linked_ax, info=''): + """ L:G|D:W """ + if linked_ax is not None: + ax = linked_ax + if ax is None: + plot_h = self.get_height(chan_db['height']) + ax = self.create_axes(self.plotting_bot, plot_h) + + x, y = c_data['times'], c_data['data'] + self.set_axes_info(ax, [len(x)], chan_db=chan_db, + info=info, y=y, linked_ax=linked_ax) + colors = {} + if chan_db['valueColors'] not in [None, 'None', '']: + color_parts = chan_db['valueColors'].split('|') + for cStr in color_parts: + obj, c = cStr.split(':') + colors[obj] = c + + l_color = 'G' + has_dot = False + if 'L' in colors: + l_color = colors['L'] + if 'D' in colors: + d_color = colors['D'] + has_dot = True + + if not has_dot: + ax.myPlot = ax.plot(x, y, + linestyle='-', linewidth=0.7, + color=Clr[l_color]) + else: + ax.myPlot = ax.plot(x, y, marker='s', markersize=1.5, + linestyle='-', linewidth=0.7, zorder=2, + color=Clr[l_color], + markerfacecolor=Clr[d_color], + picker=True, pickradius=3) + if linked_ax is None: + ax.x = x + ax.y = y + else: + ax.linkedX = x + ax.linkedY = y + return ax + + def plot_lines_s_rate(self, c_data, chan_db, chan, ax, linked_ax): + """ + multi-line line seismic, one color, line only, + can apply bit weights in (get_unit_bitweight()) + """ + if c_data['samplerate'] >= 1.0: + info = "%dsps" % c_data['samplerate'] + else: + info = "%gsps" % c_data['samplerate'] + return self.plot_lines_dots(c_data, chan_db, chan, ax, linked_ax, + info=info) + + # ----------------------- lines - multi-color dots --------------------- # + def plot_lines_mass_pos(self, c_data, chan_db, chan, ax, linked_ax): + value_colors = getMassposValueColors( + self.parent.massPosVoltRangeOpt, chan, + self.c_mode, self.errors, retType='tupleList') + + if value_colors is None: + return + + if ax is None: + plot_h = self.get_height(chan_db['height']) + ax = self.create_axes(self.plotting_bot, plot_h) + + ax.x, ax.y = c_data['times'], c_data['data'] + self.set_axes_info(ax, [len(ax.x)], chan_db=chan_db, y=ax.y) + ax.myPlot = ax.plot(ax.x, ax.y, + linestyle='-', linewidth=0.7, + color=self.display_color['sub_basic'], + zorder=2)[0] + colors = [None] * len(ax.y) + sizes = [0.5] * len(ax.y) + for i in range(len(ax.y)): + count = 0 + prev_v = 0 + for v, c in value_colors: + if count < (len(value_colors) - 1): + if prev_v < abs(ax.y[i]) <= v: + colors[i] = Clr[c] + break + else: + # if abs(ax.y[i]) > v: + colors[i] = Clr[c] + break + prev_v = v + count += 1 + ax.scatter(ax.x, ax.y, marker='s', c=colors, s=sizes, zorder=3) + return ax + + # ---------------------------------------------------------# + + def add_ruler(self, color): + ruler = ConnectionPatch( + xyA=(0, 0), + xyB=(0, self.bottom), + coordsA="data", + coordsB="data", + axesA=self.timestamp_bar_top, + axesB=self.timestamp_bar_bottom, + color=color, + ) + ruler.set_visible(False) + self.timestamp_bar_bottom.add_artist(ruler) + return ruler + + def set_lim(self, org_size=False): + if not org_size: + for chanID in self.plotting_data1: + c_data = self.plotting_data1[chanID] + self.get_zoom_data(c_data, chanID) + for chanID in self.plotting_data2: + c_data = self.plotting_data2[chanID] + self.get_zoom_data(c_data, chanID) + self.update_timestamp_bar(self.timestamp_bar_top) + self.update_timestamp_bar(self.timestamp_bar_bottom) + if hasattr(self, 'gap_bar'): + self.gap_bar.set_xlim(self.curr_min_x, self.curr_max_x) + if not org_size: + new_gaps = [g for g in self.gaps + if (self.curr_min_x <= g[0] <= self.curr_max_x + or self.curr_min_x <= g[1] <= self.curr_max_x)] + + # reset total of samples on the right + self.gap_bar.sampleLbl.set_text(len(new_gaps)) + for ax in self.axes: + ax.set_xlim(self.curr_min_x, self.curr_max_x) + if ax.x is None: + # the plot_none bar is at the end, no need to process + break + if not org_size: + # x, y + new_x_indexes = np.where((ax.x >= self.curr_min_x) & + (ax.x <= self.curr_max_x)) + + # reset total of samples on the right + ax.sampleLbl.set_text(len(new_x_indexes)) + + if len(new_x_indexes) == 0: + continue + if hasattr(ax, 'y'): + # don't need to reset y range if ax.y not exist + new_x = ax.x[new_x_indexes] + new_min_x = min(new_x) + new_max_x = max(new_x) + try: + new_min_x_index = ax.x.index(new_min_x) + new_max_x_index = ax.x.index(new_max_x) + except AttributeError: + new_min_x_index = np.where(ax.x == new_min_x)[0][0] + new_max_x_index = np.where(ax.x == new_max_x)[0][0] + new_y = ax.y[new_min_x_index:new_max_x_index + 1] + new_min_y = min(new_y) + new_max_y = max(new_y) + self.set_axes_ylim(ax, new_min_y, new_max_y) + + def set_title(self, title, y=100, v_align='top'): + self.fig.text(-0.15, y, title, + verticalalignment=v_align, + horizontalalignment='left', + transform=self.timestamp_bar_top.transAxes, + color=self.display_color['basic'], + size=self.font_size) + + def draw(self): + try: + self.canvas.draw() + # a bug on mac: + # not showing updated info until clicking on another window + # fix by calling repaint() + self.widgt.repaint() + except TypeError: + pass + + # ######## Functions for outside world ##### + def init_size(self): + geo = self.maximumViewportSize() + if self.plot_no == 0: + # set view size fit with the scroll's view port size + self.widgt.setFixedWidth(geo.width()) + self.widgt.setFixedHeight(geo.height()) + + def set_msg_widget(self, msg_widget): + self.msg_widget = msg_widget + + def set_background_color(self, color='black'): + self.fig.patch.set_facecolor(color) + self.draw() + + def reset_view(self): + """ + reset all zooms back to the first plotting + """ + if self.axes == []: + return + self.curr_min_x = self.min_x + self.curr_max_x = self.max_x + self.set_lim() + self.draw() + + def clear(self): + if self.zoom_marker1.get_visible(): + self.zoom_marker1.set_visible(False) + else: + self.zoom_marker1.set_visible(True) + # self.fig.clear() + # self.axes = [] + self.draw() + + def apply_convert_factor(self, c_data, convert_factor): + """ + convertFactor = 150mV/count = 150V/1000count + => unit data * convertFactor= data *150/1000 V + """ + c_data['data'] = np.multiply(c_data['data'], [convert_factor]) + + # =============================== axes ================================ + def plot_channels(self, start_tm, end_tm, key, data_time, + gaps, channel_list, time_ticks_total, + plotting_data1, plotting_data2): + """ + :param key: staID for mseed, (device, expNo) for reftek + :param plottingData: a ditionary including: + { gaps: [(t1,t2),(t1,t2),...] (in epoch time) + channels:{cha: {netID, statID, locID, chanID, times, data, + samplerate, startTmEpoch, endTmEpoch} # + earliestUTC: the earliest time of all channels + latestUTC: the latest time of all channels + :timeTicksTotal: max number of tick to show on time bar + + Data set: {channelname: [(x,y), (x,y)...] + """ + self.plotting_data1 = plotting_data1 + self.plotting_data2 = plotting_data2 + self.processing_log = [] # [(message, type)] + self.errors = [] + if self.axes != []: + self.fig.clear() + self.date_mode = self.parent.dateFormat.upper() + self.time_ticks_total = time_ticks_total + + self.min_x = self.curr_min_x = max(data_time[0], start_tm) + self.max_x = self.curr_max_x = min(data_time[1], end_tm) + self.plot_no = len(self.plotting_data1) + len(self.plotting_data2) + title = getTitle(key, self.min_x, self.max_x, self.date_mode) + self.plotting_bot = BOTTOM + self.plotting_bot_pixel = BOTTOM_PX + self.axes = [] + + self.timestamp_bar_top = self.add_timestamp_bar(0.003) + self.set_title(title) + self.add_gap_bar(gaps) + not_found_chan = [c for c in channel_list + if c not in self.plotting_data1.keys()] + if len(not_found_chan) > 0: + msg = (f"The following channels is in Channel Preferences but " + f"not in the given data: {not_found_chan}") + self.processing_log.append((msg, 'warning')) + + for chan_id in self.plotting_data1: + chan_db = extractData.getChanPlotInfo(chan_id, + self.parent.dataType) + if chan_db['height'] == 0: + # not draw + continue + if chan_db['channel'] == 'DEFAULT': + msg = (f"Channel {chan_id}'s " + f"definition can't be found database.") + displayTrackingInfo(self.tracking_box, msg, 'warning') + + if chan_db['plotType'] == '': + continue + self.plotting_data1[chan_id]['chan_db'] = chan_db + self.get_zoom_data(self.plotting_data1[chan_id], chan_id, + self.plotting_data1, True) + + for chan_id in self.plotting_data2: + chan_db = extractData.getChanPlotInfo(chan_id, + self.parent.dataType) + self.plotting_data2[chan_id]['chan_db'] = chan_db + self.get_zoom_data(self.plotting_data2[chan_id], chan_id, True) + + self.axes.append(self.plot_none()) + self.timestamp_bar_bottom = self.add_timestamp_bar(0.003, top=False) + self.set_lim(org_size=True) + self.bottom = self.axes[-1].get_ybound()[0] + self.ruler = self.add_ruler(self.display_color['time_ruler']) + self.zoom_marker1 = self.add_ruler(self.display_color['zoom_marker']) + self.zoom_marker2 = self.add_ruler(self.display_color['zoom_marker']) + # Set view size fit with the given data + if self.widgt.geometry().height() < self.plotting_bot_pixel: + self.widgt.setFixedHeight(self.plotting_bot_pixel) + + self.draw() + + def get_zoom_data(self, c_data, chan_id, plotting_data=None, + first_time=False): + """ + :param setID: (netID, statID, locID) + :param plotting_data: a ditionary including: + { gaps: [(t1,t2),(t1,t2),...] (in epoch time) + channels:{cha: {netID, statID, locID, chanID, times, data, + samplerate, startTmEpoch, endTmEpoch} # + earliestUTC: the earliest time of all channels + latestUTC: the latest time of all channels + :timeTicksTotal: max number of tick to show on time bar + + Data set: {channelname: [(x,y), (x,y)...] + """ + chan_db = c_data['chan_db'] + plot_type = chan_db['plotType'] + trim_downsample_SOHChan(c_data, self.curr_min_x, self.curr_max_x, + first_time) + self.apply_convert_factor(c_data, 1) + if 'ax' not in c_data: + linked_ax = None + if chan_db['linkedChan'] not in [None, 'None', '']: + try: + linked_ax = plotting_data['channels'][ + chan_db['linkedChan']]['ax'] + except KeyError: + pass + ax = getattr(self, dbConf['plotFunc'][plot_type][1])( + c_data, chan_db, chan_id, None, linked_ax) + if ax is None: + return + c_data['ax'] = ax + ax.chan = chan_id + self.axes.append(ax) + else: + getattr(self, dbConf['plotFunc'][plot_type][1])( + c_data, chan_db, chan_id, c_data['ax'], None) + + def hide_plots(self, plot_indexes): + if self.axes == []: + return + plot_indexes = sorted(plot_indexes) + idx = 0 + total_h = 0 + for i in range(plot_indexes[0], len(self.axes)): + pos = self.axes[i].get_position() + pos.y0 += total_h + pos.y1 += total_h + if idx < len(plot_indexes) and i == plot_indexes[idx]: + h = pos.y1 - pos.y0 + total_h += h + pos.y0 = pos.y1 + idx += 1 + self.hidden_plots[i] = h + self.axes[i].set_position(pos) + + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() - 100 * len(plot_indexes) + self.widgt.setFixedHeight(height) + self.draw() + + def hide_curr_plot(self): + pos = self.curr_plot.get_position() + h = pos.y1 - pos.y0 + pos.y0 = pos.y1 + self.curr_plot.set_position(pos) + for i in range(self.curr_plot_index + 1, len(self.axes)): + pos = self.axes[i].get_position() + pos.y0 += h + pos.y1 += h + self.axes[i].set_position(pos) + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() - 100 + self.widgt.setFixedHeight(height) + self.hidden_plots[self.curr_plot_index] = h + self.draw() + + def show_hidden_plot(self, index): + h = self.hidden_plots[index] + work_plot = self.axes[index] + pos = work_plot.get_position() + pos.y1 = pos.y0 + h + work_plot.set_position(pos) + for i in range(index, len(self.axes)): + pos = self.axes[i].get_position() + pos.y0 -= h + pos.y1 -= h + self.axes[i].set_position(pos) + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() + 100 + self.widgt.setFixedHeight(height) + del self.hidden_plots[index] + pos = work_plot.get_position() + self.draw() + + def show_all_hidden_plots(self): + plot_indexes = sorted(self.hidden_plots.keys()) + idx = 0 + total_h = 0 + for i in range(plot_indexes[0], len(self.axes)): + pos = self.axes[i].get_position() + if idx < len(plot_indexes) and i == plot_indexes[idx]: + h = self.hidden_plots[i] + total_h += h + pos.y1 = pos.y0 + h + idx += 1 + del self.hidden_plots[i] + pos.y0 -= total_h + pos.y1 -= total_h + self.axes[i].set_position(pos) + + # currently consider every plot height are all 100px height + height = self.widgt.geometry().height() + 100 * len(plot_indexes) + self.widgt.setFixedHeight(height) + self.draw() + + def replot(self): + for ax in self.axes: + print("pos", self.ax.get_position()) + + # ============================= END AXES ============================== + def set_colors(self, mode): + self.c_mode = mode + self.display_color = set_colors(mode) + self.fig.patch.set_facecolor(self.display_color['background']) + + def set_peer_plotting_widgets(self, widgets): + self.peer_plotting_widgets = widgets diff --git a/sohstationviewer/view/dataTypedialog.py b/sohstationviewer/view/dataTypedialog.py deleted file mode 100755 index a546a2ddfb15fa2c90a001cbe229bb4e8d118d4a..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/dataTypedialog.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -datatypedialog.py -GUI to add/edit/remove dataTypes -NOTE: Cannot remove or change dataTypes that already have channels. -""" - -from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog -from sohstationviewer.database.proccessDB import executeDB - - -class DataTypeDialog(Ui_DBInfoDialog): - def __init__(self, parent): - super().__init__(parent, ['No.', 'DataType'], 'dataType', 'dataTypes', - resizeContentColumns=[0]) - self.setWindowTitle("Edit/Add/Delete DataTypes") - - def addRow(self, rowidx, fk=False): - self.addWidget(None, rowidx, 0) # No. - self.addWidget(self.dataList, rowidx, 1, foreignkey=fk) - - def getDataList(self): - dataTypeRows = executeDB('SELECT * FROM DataTypes') - return [[d[0]] for d in dataTypeRows] - - def getRowInputs(self, rowidx): - return [self.dataTableWidget.cellWidget(rowidx, 1).text().strip()] - - def updateData(self, row, widgetidx, listidx): - insertsql = (f"INSERT INTO DataTypes VALUES('{row[0]}')") - updatesql = (f"UPDATE DataTypes SET dataType='{row[0]}' " - f"WHERE dataType='%s'") - - return super().updateData( - row, widgetidx, listidx, insertsql, updatesql) diff --git a/sohstationviewer/view/data_type_dialog.py b/sohstationviewer/view/data_type_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..726dbb4c5f33f672a0b83119938333326d9eff73 --- /dev/null +++ b/sohstationviewer/view/data_type_dialog.py @@ -0,0 +1,34 @@ +""" +datatypedialog.py +GUI to add/edit/remove dataTypes +NOTE: Cannot remove or change dataTypes that already have channels. +""" + +from sohstationviewer.view.core.db_gui_superclass import Ui_DBInfoDialog +from sohstationviewer.database.proccessDB import executeDB + + +class DataTypeDialog(Ui_DBInfoDialog): + def __init__(self, parent): + super().__init__(parent, ['No.', 'DataType'], 'dataType', 'dataTypes', + resize_content_columns=[0]) + self.setWindowTitle("Edit/Add/Delete DataTypes") + + def add_row(self, row_idx, fk=False): + self.addWidget(None, row_idx, 0) # No. + self.addWidget(self.data_list, row_idx, 1, foreign_key=fk) + + def get_data_list(self): + data_type_rows = executeDB('SELECT * FROM DataTypes') + return [[d[0]] for d in data_type_rows] + + def get_row_inputs(self, row_idx): + return [self.data_table_widget.cellWidget(row_idx, 1).text().strip()] + + def update_data(self, row, widget_idx, list_idx): + insert_sql = f"INSERT INTO DataTypes VALUES('{row[0]}')" + update_sql = (f"UPDATE DataTypes SET dataType='{row[0]}' " + f"WHERE dataType='%s'") + + return super().update_data( + row, widget_idx, list_idx, insert_sql, update_sql) diff --git a/sohstationviewer/view/mainwindow.py b/sohstationviewer/view/mainwindow.py index b6936d4b0945387170d9c137d7bc9178cef5e5b0..323182d75e2c4b895c7f5a4c0fbd2bf3cd2d8af8 100755 --- a/sohstationviewer/view/mainwindow.py +++ b/sohstationviewer/view/mainwindow.py @@ -6,16 +6,16 @@ from copy import deepcopy from PySide2 import QtCore, QtWidgets from sohstationviewer.view.ui.main_ui import Ui_MainWindow -from sohstationviewer.view.calendardialog import CalendarDialog +from sohstationviewer.view.calendar_dialog import CalendarDialog from sohstationviewer.view.core.filelistwidget import FileListItem from sohstationviewer.controller.processing import loadData, detectDataType -from sohstationviewer.view.dataTypedialog import DataTypeDialog -from sohstationviewer.view.paramdialog import ParamDialog -from sohstationviewer.view.channeldialog import ChannelDialog -from sohstationviewer.view.plottypedialog import PlotTypeDialog -from sohstationviewer.view.channelpreferdialog import ChannelPreferDialog +from sohstationviewer.view.data_type_dialog import DataTypeDialog +from sohstationviewer.view.param_dialog import ParamDialog +from sohstationviewer.view.channel_dialog import ChannelDialog +from sohstationviewer.view.plot_type_dialog import PlotTypeDialog +from sohstationviewer.view.channel_prefer_dialog import ChannelPreferDialog from sohstationviewer.database.proccessDB import executeDB_dict -from sohstationviewer.view.waveformdialog import WaveformDialog +from sohstationviewer.view.waveform_dialog import WaveformDialog from sohstationviewer.view.time_power_squareddialog import ( TimePowerSquaredDialog) from sohstationviewer.controller.util import displayTrackingInfo @@ -39,9 +39,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): self.waveformDlg = WaveformDialog(self) self.TPSDlg = TimePowerSquaredDialog(self) - # def resizeEvent(self, event): - # self.plottingWidget.init_size() - @QtCore.Slot() def openDataType(self): win = DataTypeDialog(self) @@ -90,7 +87,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): @QtCore.Slot() def resetView(self): - self.plottingWidget.resetView() + self.plottingWidget.reset_view() @QtCore.Slot() def setDateFormat(self, displayFormat): @@ -116,9 +113,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): """ print(f'Opening {item.text()}') - # Do something with the Path object, + # TODO: Do something with the Path object, # i.e., path.open(), or path.iterdir() ... - # path = item.filePath @QtCore.Slot() def changeCurrentDirectory(self): @@ -218,7 +214,6 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): selKey = do.selectedKey - # do.createPlottingData(startTm, endTm) # mainPlot soh_data = deepcopy(do.SOHData[selKey]) @@ -236,11 +231,11 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): peerPlottingWidgets = [self.plottingWidget] if self.tpsCheckBox.isChecked(): - peerPlottingWidgets.append(self.TPSDlg.plottingWidget) - self.TPSDlg.setData(self.dataType, ','.join(self.dirnames)) + peerPlottingWidgets.append(self.TPSDlg.plotting_widget) + self.TPSDlg.set_data(self.dataType, ','.join(self.dirnames)) self.TPSDlg.show() wfChans = list(do.waveformData[do.selectedKey].keys()) - self.TPSDlg.plottingWidget.plot_channels( + self.TPSDlg.plotting_widget.plot_channels( self.startTm, self.endTm, selKey, do.dataTime[selKey], wf_data) @@ -249,21 +244,21 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): if self.reqWFChans != []: # waveformPlot - peerPlottingWidgets.append(self.waveformDlg.plottingWidget) - self.waveformDlg.setData(self.dataType, ','.join(self.dirnames)) + peerPlottingWidgets.append(self.waveformDlg.plotting_widget) + self.waveformDlg.set_data(self.dataType, ','.join(self.dirnames)) self.waveformDlg.show() wfChans = list(do.waveformData[do.selectedKey].keys()) - self.waveformDlg.plottingWidget.plot_channels( + self.waveformDlg.plotting_widget.plot_channels( self.startTm, self.endTm, selKey, do.dataTime[selKey], wfChans, timeTickTotal, wf_data, mp_data) else: self.waveformDlg.hide() - self.plottingWidget.setPeerPlottingWidgets(peerPlottingWidgets) - self.waveformDlg.plottingWidget.setPeerPlottingWidgets( + self.plottingWidget.set_peer_plotting_widgets(peerPlottingWidgets) + self.waveformDlg.plotting_widget.set_peer_plotting_widgets( peerPlottingWidgets) - self.TPSDlg.plottingWidget.setPeerPlottingWidgets( + self.TPSDlg.plotting_widget.set_peer_plotting_widgets( peerPlottingWidgets) def setCurrentDirectory(self, path=''): diff --git a/sohstationviewer/view/param_dialog.py b/sohstationviewer/view/param_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..1b181bbc8e135520acb728fac4b73801b8e7e980 --- /dev/null +++ b/sohstationviewer/view/param_dialog.py @@ -0,0 +1,68 @@ +""" +param_dialog.py +GUI to add/dit/remove params +NOTE: Cannot remove or change params that are already used for channels. +""" + +from PySide2 import QtWidgets + + +from sohstationviewer.view.core.db_gui_superclass import Ui_DBInfoDialog +from sohstationviewer.view.core import plotting_widget +from sohstationviewer.database.proccessDB import executeDB + +from sohstationviewer.conf.dbSettings import dbConf + + +class ParamDialog(Ui_DBInfoDialog): + def __init__(self, parent): + super().__init__( + parent, + ['No.', 'Param', 'Plot Type', 'ValueColors', 'Height '], + 'param', 'parameters', + resize_content_columns=[0, 3]) + self.setWindowTitle("Edit/Add/Delete Parameters") + + def add_row(self, row_idx, fk=False): + self.addWidget(None, row_idx, 0) # No. + self.addWidget(self.data_list, row_idx, 1, foreign_key=fk) + self.addWidget(self.data_list, row_idx, 2, + choices=[''] + sorted(plotting_widget.plotFunc.keys())) + + self.addWidget(self.data_list, row_idx, 3) + self.addWidget(self.data_list, row_idx, 4, range=[0, 10]) + + def get_data_list(self): + param_rows = executeDB('SELECT * FROM Parameters') + return [[d[0], + '' if d[1] is None else d[1], + d[2]] + for d in param_rows] + + def get_row_inputs(self, row_idx): + # check value_colors string + value_colors_string = self.data_table_widget.cellWidget( + row_idx, 3).text().strip() + value_colors = value_colors_string.split("|") + for vc in value_colors: + if not dbConf['valColRE'].match(vc): + msg = (f"The valueColor is requested for '{vc}' at line " + f"{row_idx}does not match the required format:" + f"[+]value:color with color=R,Y,G,M,C." + f"\n Ex: 2.3:C|+4:M") + QtWidgets.QMessageBox.information(self, "Error", msg) + return [ + self.data_table_widget.cellWidget(row_idx, 1).text().strip(), + self.data_table_widget.cellWidget(row_idx, 2).currentText(), + self.data_table_widget.cellWidget(row_idx, 3).text().strip(), + int(self.data_table_widget.cellWidget(row_idx, 4).text()) + ] + + def update_data(self, row, widget_idx, list_idx): + insertsql = (f"INSERT INTO Parameters VALUES" + f"('{row[0]}', '{row[1]}', {row[2]})") + updatesql = (f"UPDATE Parameters SET param='{row[0]}', " + f"plotType='{row[1]}', height={row[2]} " + f"WHERE param='%s'") + return super().update_data( + row, widget_idx, list_idx, insertsql, updatesql) diff --git a/sohstationviewer/view/paramdialog.py b/sohstationviewer/view/paramdialog.py deleted file mode 100755 index 0315073a7ff1589cd15faaeaf81e1256d071c433..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/paramdialog.py +++ /dev/null @@ -1,68 +0,0 @@ -""" -paramdialog.py -GUI to add/dit/remove params -NOTE: Cannot remove or change params that are already used for channels. -""" - -from PySide2 import QtWidgets - - -from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog -from sohstationviewer.view.core import plottingWidget -from sohstationviewer.database.proccessDB import executeDB - -from sohstationviewer.conf.dbSettings import dbConf - - -class ParamDialog(Ui_DBInfoDialog): - def __init__(self, parent): - super().__init__( - parent, - ['No.', 'Param', 'Plot Type', 'ValueColors', 'Height '], - 'param', 'parameters', - resizeContentColumns=[0, 3]) - self.setWindowTitle("Edit/Add/Delete Parameters") - - def addRow(self, rowidx, fk=False): - self.addWidget(None, rowidx, 0) # No. - self.addWidget(self.dataList, rowidx, 1, foreignkey=fk) - self.addWidget(self.dataList, rowidx, 2, - choices=[''] + sorted(plottingWidget.plotFunc.keys())) - - self.addWidget(self.dataList, rowidx, 3) - self.addWidget(self.dataList, rowidx, 4, range=[0, 10]) - - def getDataList(self): - paramRows = executeDB('SELECT * FROM Parameters') - return [[d[0], - '' if d[1] is None else d[1], - d[2]] - for d in paramRows] - - def getRowInputs(self, rowidx): - # check vallueColors string - valueColorsString = self.dataTableWidget.cellWidget( - rowidx, 3).currentText().strip() - valueColors = valueColorsString.split("|") - for vc in valueColors: - if not dbConf['valColRE'].match(vc): - msg = (f"The valueColor is requested for '{vc}' at line " - f"{rowidx}does not match the required format:" - f"[+]value:color with color=R,Y,G,M,C." - f"\n Ex: 2.3:C|+4:M") - QtWidgets.QMessageBox.information(self, "Error", msg) - return [ - self.dataTableWidget.cellWidget(rowidx, 1).text().strip(), - self.dataTableWidget.cellWidget(rowidx, 2).currentText().strip(), - self.dataTableWidget.cellWidget(rowidx, 3).currentText().strip(), - int(self.dataTableWidget.cellWidget(rowidx, 4).text()) - ] - - def updateData(self, row, widgetidx, listidx): - insertsql = (f"INSERT INTO Parameters VALUES" - f"('{row[0]}', '{row[1]}', {row[2]})") - updatesql = (f"UPDATE Parameters SET param='{row[0]}', " - f"plotType='{row[1]}', height={row[2]} " - f"WHERE param='%s'") - return super().updateData( - row, widgetidx, listidx, insertsql, updatesql) diff --git a/sohstationviewer/view/plot_type_dialog.py b/sohstationviewer/view/plot_type_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..7e965fb99536a0359267146c8258f79f797811f9 --- /dev/null +++ b/sohstationviewer/view/plot_type_dialog.py @@ -0,0 +1,24 @@ +""" +plottypedialog +GUI to view the types of plotting and their descriptions +NOTE: plottypes are defined in plottingWidget +""" + +from sohstationviewer.view.core.db_gui_superclass import Ui_DBInfoDialog +from sohstationviewer.view.core import plotting_widget + + +class PlotTypeDialog(Ui_DBInfoDialog): + def __init__(self, parent): + super().__init__( + parent, ['No.', ' Plot Type ', 'Description'], + '', '', resize_content_columns=[0, 1], check_fk=False) + self.setWindowTitle("Plotting Types") + + def add_row(self, row_idx, fk=False): + self.addWidget(None, row_idx, 0) # No. + self.addWidget(self.data_list, row_idx, 1, foreign_key=True) + self.addWidget(self.data_list, row_idx, 2, foreign_key=True) + + def get_data_list(self): + return [[key, val[0]] for key, val in plotting_widget.plotFunc.items()] diff --git a/sohstationviewer/view/plottypedialog.py b/sohstationviewer/view/plottypedialog.py deleted file mode 100755 index 4611d3eef0d7e87c0050a61f33f7dfecafd84709..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/plottypedialog.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -plottypedialog -GUI to view the types of plotting and their descriptions -NOTE: plottypes are defined in plottingWidget -""" - -from sohstationviewer.view.core.dbgui_superclass import Ui_DBInfoDialog -from sohstationviewer.view.core import plottingWidget - - -class PlotTypeDialog(Ui_DBInfoDialog): - def __init__(self, parent): - super().__init__( - parent, ['No.', ' Plot Type ', 'Description'], - '', '', resizeContentColumns=[0, 1], checkFK=False) - self.setWindowTitle("Plotting Types") - - def addRow(self, rowidx, fk=False): - self.addWidget(None, rowidx, 0) # No. - self.addWidget(self.dataList, rowidx, 1, foreignkey=True) - self.addWidget(self.dataList, rowidx, 2, foreignkey=True) - - def getDataList(self): - return [[key, val[0]] for key, val in plottingWidget.plotFunc.items()] diff --git a/sohstationviewer/view/time_power_squareddialog.py b/sohstationviewer/view/time_power_squareddialog.py index 225f7121321a5898c36e5a8db03dd5135f19368c..c0aa7dca1076b19a2c88f6b269a1fbb76ec858c3 100755 --- a/sohstationviewer/view/time_power_squareddialog.py +++ b/sohstationviewer/view/time_power_squareddialog.py @@ -4,7 +4,7 @@ import numpy as np from PySide2 import QtWidgets, QtCore -from sohstationviewer.view.core import plottingWidget as plottingWidget +from sohstationviewer.view.core import plotting_widget as plottingWidget from sohstationviewer.controller.plottingData import ( getTitle, getDayTicks, formatTime) from sohstationviewer.model.handling_data import ( @@ -18,113 +18,113 @@ from sohstationviewer.controller.util import displayTrackingInfo, fmti class TimePowerSquaredWidget(plottingWidget.PlottingWidget): - def plot_channels(self, startTm=None, endTm=None, key=None, - dataTime=None, plottingData=None): - self.processingLog = [] # [(message, type)] + def plot_channels(self, start_tm=None, end_tm=None, key=None, + data_time=None, plotting_data=None): + self.processing_log = [] # [(message, type)] - self.minX = self.currMinX = max(dataTime[0], startTm) - self.maxX = self.currMaxX = min(dataTime[1], endTm) + self.min_x = self.curr_min_x = max(data_time[0], start_tm) + self.max_x = self.curr_max_x = min(data_time[1], end_tm) self.errors = [] if self.axes != []: self.fig.clear() - self.dateMode = self.parent.dateFormat.upper() - if plottingData is not None: - self.plottingData = plottingData - self.minX = max(dataTime[0], startTm) - self.maxX = min(dataTime[1], endTm) - self.plotNo = len(plottingData) - title = getTitle(key, self.minX, self.maxX, self.dateMode) - self.plottingBot = plottingWidget.BOTTOM - self.plottingBotPixel = plottingWidget.BOTTOM_PX + self.date_mode = self.parent.date_format.upper() + if plotting_data is not None: + self.plotting_data = plotting_data + self.min_x = max(data_time[0], start_tm) + self.max_x = min(data_time[1], end_tm) + self.plot_no = len(plotting_data) + title = getTitle(key, self.min_x, self.max_x, self.date_mode) + self.plotting_bot = plottingWidget.BOTTOM + self.plotting_bot_pixel = plottingWidget.BOTTOM_PX self.axes = [] self.rulers = [] self.zoom_marker1s = [] self.zoom_marker2s = [] - self.timestampBarTop = self.add_timestamp_bar(0.) - self.set_title(title, y=0, valight='bottom') - self.eachDay5MinList = get_eachDay5MinList(self.minX, self.maxX) - for chanID in self.plottingData: - ax = self.getZoomData(self.plottingData[chanID], chanID) - # ax.chan = chanID + self.timestamp_bar_top = self.add_timestamp_bar(0.) + self.set_title(title, y=0, v_align='bottom') + self.each_day5_min_list = get_eachDay5MinList(self.min_x, self.max_x) + for chanID in self.plotting_data: + ax = self.get_zoom_data(self.plotting_data[chanID], chanID) self.axes.append(ax) - self.setLegend() + self.set_legend() # Set view size fit with the given data - if self.widgt.geometry().height() < self.plottingBotPixel: - self.widgt.setFixedHeight(self.plottingBotPixel) + if self.widgt.geometry().height() < self.plotting_bot_pixel: + self.widgt.setFixedHeight(self.plotting_bot_pixel) self.set_lim_markers() self.draw() - def getZoomData(self, cData, chanID): - if 'tps_data' not in cData: + def get_zoom_data(self, c_data, chan_id): + if 'tps_data' not in c_data: # get new minX, maxX according to exact start time of days - get_trimTPSData(cData, self.minX, self.maxX, self.eachDay5MinList) - totalDays = cData['tps_data'].shape[0] - plotH = self.get_height(1.5 * totalDays, bwPlotsDistance=0.003) - ax = self.create_axes(self.plottingBot, plotH) + get_trimTPSData(c_data, self.min_x, self.max_x, + self.each_day5_min_list) + total_days = c_data['tps_data'].shape[0] + plot_h = self.get_height(1.5 * total_days, bw_plots_distance=0.003) + ax = self.create_axes(self.plotting_bot, plot_h) ax.text( -0.1, 1.2, - f"{getChanLabel(chanID)} {cData['samplerate']}", + f"{getChanLabel(chan_id)} {c_data['samplerate']}", horizontalalignment='left', verticalalignment='top', rotation='horizontal', transform=ax.transAxes, - color=self.displayColor['plot_label'], - size=self.fontSize + 2 + color=self.display_color['plot_label'], + size=self.font_size + 2 ) zm1 = ax.plot( [], [], marker='|', markersize=10, - markeredgecolor=self.displayColor['zoom_marker'])[0] + markeredgecolor=self.display_color['zoom_marker'])[0] self.zoom_marker1s.append(zm1) zm2 = ax.plot( [], [], marker='|', markersize=10, - markeredgecolor=self.displayColor['zoom_marker'])[0] + markeredgecolor=self.display_color['zoom_marker'])[0] self.zoom_marker2s.append(zm2) rl = ax.plot( [], [], marker='s', markersize=5, - markeredgecolor=self.displayColor['time_ruler'], + markeredgecolor=self.display_color['time_ruler'], markerfacecolor='None')[0] self.rulers.append(rl) x = np.array([i for i in range(const.NO_5M_DAY)]) - sCnts = self.parent.selSquareCounts # square counts range - clrs = self.parent.colorDef # colordef + s_cnts = self.parent.sel_square_counts # square counts range + clrs = self.parent.color_def # colordef - for dayIdx, y in enumerate(cData['tps_data']): + for dayIdx, y in enumerate(c_data['tps_data']): # not draw data out of day range - colorSet = self.getColorSet(y, sCnts, clrs) + color_set = self.get_color_set(y, s_cnts, clrs) # (- dayIdx): each day is a line, increase from top to bottom - ax.scatter(x, [- dayIdx]*len(x), marker='|', - c=colorSet, s=7, alpha=0.8) + ax.scatter(x, [- dayIdx] * len(x), marker='|', + c=color_set, s=7, alpha=0.8) # extra to show highlight square - ax.set_ylim(-(cData['tps_data'].shape[0] + 1), 1) + ax.set_ylim(-(c_data['tps_data'].shape[0] + 1), 1) return ax - def setLegend(self): + def set_legend(self): """ plot one dot for each color and assign label to it legend will be one color for each label """ # set height of legend and distance bw legend and upper ax - plotH = self.get_height(7, bwPlotsDistance=0.003) + plot_h = self.get_height(7, bw_plots_distance=0.003) ax = self.canvas.figure.add_axes( - [self.plottingL, self.plottingBot, self.plottingW, plotH], + [self.plotting_l, self.plotting_bot, self.plotting_w, plot_h], picker=True ) ax.patch.set_alpha(0) - cLabels = self.parent.selColLabels - clrs = self.parent.colorDef # colordef - for idx in range(len(cLabels)): + c_labels = self.parent.sel_col_labels + clrs = self.parent.color_def # colordef + for idx in range(len(c_labels)): # draw a dot out of xlim so it isn't displayed in plotting area ax.scatter([300], [1], c=Clr[clrs[idx]], s=0.1, - label=cLabels[idx], - edgecolor=self.displayColor['basic'], + label=c_labels[idx], + edgecolor=self.display_color['basic'], alpha=0.8, zorder=1, picker=True) @@ -132,9 +132,9 @@ class TimePowerSquaredWidget(plottingWidget.PlottingWidget): ax.set_xlim(-2, const.NO_5M_DAY + 1) ax.legend(loc="upper left", framealpha=0.2, markerscale=25, - labelcolor=self.displayColor['basic']) + labelcolor=self.display_color['basic']) - def getColorSet(self, y, sCnts, col): + def get_color_set(self, y, s_cnts, col): """ create array of color (col) according to value of y compare with sCnts (square count range) @@ -145,38 +145,37 @@ class TimePowerSquaredWidget(plottingWidget.PlottingWidget): """ return ( np.where( - y == sCnts[0], Clr[col[0]], np.where( - y < sCnts[1], Clr[col[1]], np.where( - y < sCnts[2], Clr[col[2]], np.where( - y < sCnts[3], Clr[col[3]], np.where( - y < sCnts[4], Clr[col[4]], np.where( - y < sCnts[5], Clr[col[5]], np.where( - y < sCnts[6], Clr[col[6]], Clr[col[7]]) - ))))))) - - def create_axes(self, plotB, plotH, hasMinMaxLines=False): + y == s_cnts[0], Clr[col[0]], np.where( + y < s_cnts[1], Clr[col[1]], np.where( + y < s_cnts[2], Clr[col[2]], np.where( + y < s_cnts[3], Clr[col[3]], np.where( + y < s_cnts[4], Clr[col[4]], np.where( + y < s_cnts[5], Clr[col[5]], np.where( + y < s_cnts[6], Clr[col[6]], Clr[col[7]] + )))))))) + + def create_axes(self, plot_b, plot_h, has_min_max_lines=False): """ create axes for 288 of 5m in a day in which minor tick for every hour, major tick for every 4 hour """ ax = self.canvas.figure.add_axes( - [self.plottingL, plotB, self.plottingW, plotH], + [self.plotting_l, plot_b, self.plotting_w, plot_h], picker=True ) - # ax.axis('off') ax.spines['right'].set_visible(False) ax.spines['left'].set_visible(False) ax.xaxis.grid(True, which='major', - color=self.displayColor['basic'], linestyle='-') + color=self.display_color['basic'], linestyle='-') ax.xaxis.grid(True, which='minor', - color=self.displayColor['sub_basic'], linestyle='-') + color=self.display_color['sub_basic'], linestyle='-') ax.set_yticks([]) - times, majorTimes, majorTimeLabels = getDayTicks() + times, major_times, major_time_labels = getDayTicks() ax.set_xticks(times, minor=True) - ax.set_xticks(majorTimes) - ax.set_xticklabels(majorTimeLabels, fontsize=self.fontSize, - color=self.displayColor['basic']) + ax.set_xticks(major_times) + ax.set_xticklabels(major_time_labels, fontsize=self.font_size, + color=self.display_color['basic']) # extra to show highlight square ax.set_xlim(-2, const.NO_5M_DAY + 1) ax.patch.set_alpha(0) @@ -187,7 +186,7 @@ class TimePowerSquaredWidget(plottingWidget.PlottingWidget): Click on each point will highlight time position for each channel and display time and counts for each channel """ - infoStr = "" + info_str = "" if event.artist in self.axes: xdata = round(event.mouseevent.xdata) # when click on outside xrange that close to edge, adjust to edge @@ -197,18 +196,18 @@ class TimePowerSquaredWidget(plottingWidget.PlottingWidget): xdata = 287 ydata = round(event.mouseevent.ydata) if xdata is not None: - yIdx = - ydata - xIdx = xdata + y_idx = - ydata + x_idx = xdata # identify time for other plots' rulers displaying - self.tps_t = self.eachDay5MinList[yIdx, xIdx] - format_t = formatTime(self.tps_t, self.dateMode, 'HH:MM:SS') - infoStr += f"{format_t}:" - for chanID in self.plottingData: - cData = self.plottingData[chanID] - data = cData['tps_data'][yIdx, xIdx] - infoStr += f" {chanID}:{fmti(sqrt(data))}" - infoStr += " (counts)" - displayTrackingInfo(self.trackingBox, infoStr) + self.tps_t = self.each_day5_min_list[y_idx, x_idx] + format_t = formatTime(self.tps_t, self.date_mode, 'HH:MM:SS') + info_str += f"{format_t}:" + for chanID in self.plotting_data: + c_data = self.plotting_data[chanID] + data = c_data['tps_data'][y_idx, x_idx] + info_str += f" {chanID}:{fmti(sqrt(data))}" + info_str += " (counts)" + displayTrackingInfo(self.tracking_box, info_str) self.draw() def on_ctrl_cmd_click(self, xdata): @@ -216,111 +215,111 @@ class TimePowerSquaredWidget(plottingWidget.PlottingWidget): Ctrl + cmd: base on xdata to find indexes of x an y in self.each_day_5_min_list to display ruler for each channel """ - self.zoomMarker1Shown = False - xIdx, yIdx = findTPSTm(xdata, self.eachDay5MinList) + self.zoom_marker1_shown = False + x_idx, y_idx = findTPSTm(xdata, self.each_day5_min_list) for rl in self.rulers: - rl.set_data(xIdx, yIdx) + rl.set_data(x_idx, y_idx) def on_shift_click(self, xdata): """ Shift + right click in TPS on the second of zoommaker, call set_lim_markers to mark the new limit """ - if not self.zoomMarker1Shown: + if not self.zoom_marker1_shown: self.set_rulers_invisible() - self.currMinX = xdata - self.zoomMarker1Shown = True + self.curr_min_x = xdata + self.zoom_marker1_shown = True else: - [self.currMinX, self.currMaxX] = sorted( - [self.currMinX, xdata]) + [self.curr_min_x, self.curr_max_x] = sorted( + [self.curr_min_x, xdata]) self.set_lim_markers() - self.zoomMarker1Shown = False + self.zoom_marker1_shown = False def set_rulers_invisible(self): for rl in self.rulers: rl.set_data([], []) def set_lim_markers(self): - xIdx, yIdx = findTPSTm(self.currMinX, self.eachDay5MinList) + x_idx, y_idx = findTPSTm(self.curr_min_x, self.each_day5_min_list) for zm1 in self.zoom_marker1s: - zm1.set_data(xIdx, yIdx) - xIdx, yIdx = findTPSTm(self.currMaxX, self.eachDay5MinList) + zm1.set_data(x_idx, y_idx) + x_idx, y_idx = findTPSTm(self.curr_max_x, self.each_day5_min_list) for zm2 in self.zoom_marker2s: - zm2.set_data(xIdx, yIdx) + zm2.set_data(x_idx, y_idx) class TimePowerSquaredDialog(QtWidgets.QWidget): def __init__(self, parent): super().__init__() self.parent = parent - self.dateFormat = self.parent.dateFormat - self.bitweightOpt = self.parent.bitweightOpt + self.date_format = self.parent.dateFormat + self.bitweight_opt = self.parent.bitweightOpt self.setGeometry(50, 50, 1200, 700) self.setWindowTitle("TPS Plot") - mainLayout = QtWidgets.QVBoxLayout() - self.setLayout(mainLayout) - mainLayout.setContentsMargins(5, 5, 5, 5) - mainLayout.setSpacing(0) + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + main_layout.setContentsMargins(5, 5, 5, 5) + main_layout.setSpacing(0) - self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self) + self.tracking_info_text_browser = QtWidgets.QTextBrowser(self) - self.plottingWidget = TimePowerSquaredWidget( - self, self.trackingInfoTextBrowser, "timepowersquaredwidget") - mainLayout.addWidget(self.plottingWidget, 2) + self.plotting_widget = TimePowerSquaredWidget( + self, self.tracking_info_text_browser, "timepowersquaredwidget") + main_layout.addWidget(self.plotting_widget, 2) - bottomLayout = QtWidgets.QHBoxLayout() - bottomLayout.addSpacing(20) - mainLayout.addLayout(bottomLayout) + bottom_layout = QtWidgets.QHBoxLayout() + bottom_layout.addSpacing(20) + main_layout.addLayout(bottom_layout) # ################ Color range ################# - bottomLayout.addWidget(QtWidgets.QLabel("Color Range")) - - self.colorDef = getColorDef() - (self.colorRanges, - self.allSquareCounts, - self.colorLabel) = getColorRanges() - self.colorRangeChoice = QtWidgets.QComboBox(self) - self.colorRangeChoice.addItems(self.colorRanges) - self.colorRangeChoice.setCurrentText('high') - bottomLayout.addWidget(self.colorRangeChoice) + bottom_layout.addWidget(QtWidgets.QLabel("Color Range")) + + self.color_def = getColorDef() + (self.color_ranges, + self.all_square_counts, + self.color_label) = getColorRanges() + self.color_range_choice = QtWidgets.QComboBox(self) + self.color_range_choice.addItems(self.color_ranges) + self.color_range_choice.setCurrentText('high') + bottom_layout.addWidget(self.color_range_choice) ################################################ - self.replotButton = QtWidgets.QPushButton("RePlot", self) - bottomLayout.addWidget(self.replotButton) + self.replot_button = QtWidgets.QPushButton("RePlot", self) + bottom_layout.addWidget(self.replot_button) - self.writeFileButton = QtWidgets.QPushButton('Write file', self) - bottomLayout.addWidget(self.writeFileButton) + self.write_file_button = QtWidgets.QPushButton('Write file', self) + bottom_layout.addWidget(self.write_file_button) - self.trackingInfoTextBrowser.setFixedHeight(60) - bottomLayout.addWidget(self.trackingInfoTextBrowser) + self.tracking_info_text_browser.setFixedHeight(60) + bottom_layout.addWidget(self.tracking_info_text_browser) - self.connectSignals() - self.colorRangeChanged() + self.connect_signals() + self.color_range_changed() - def setData(self, dataType, fileName): - self.dataType = dataType - self.setWindowTitle("TPS Plot %s - %s" % (dataType, fileName)) + def set_data(self, data_type, file_name): + self.data_type = data_type + self.setWindowTitle("TPS Plot %s - %s" % (data_type, file_name)) def resizeEvent(self, event): - self.plottingWidget.init_size() + self.plotting_widget.init_size() - def connectSignals(self): - self.writeFileButton.clicked.connect(self.writeFile) - self.replotButton.clicked.connect(self.replot) - self.colorRangeChoice.currentTextChanged.connect( - self.colorRangeChanged) + def connect_signals(self): + self.write_file_button.clicked.connect(self.write_file) + self.replot_button.clicked.connect(self.replot) + self.color_range_choice.currentTextChanged.connect( + self.color_range_changed) @QtCore.Slot() - def colorRangeChanged(self): - colorRange = self.colorRangeChoice.currentText() - crIndex = self.colorRanges.index(colorRange) - self.selSquareCounts = self.allSquareCounts[crIndex] - self.selColLabels = self.colorLabel[crIndex] + def color_range_changed(self): + color_range = self.color_range_choice.currentText() + cr_index = self.color_ranges.index(color_range) + self.sel_square_counts = self.all_square_counts[cr_index] + self.sel_col_labels = self.color_label[cr_index] @QtCore.Slot() - def writeFile(self): + def write_file(self): print("writeFile") @QtCore.Slot() def replot(self): - self.plottingWidget.plot_channels() + self.plotting_widget.plot_channels() diff --git a/sohstationviewer/view/ui/calendar_ui_qtdesigner.py b/sohstationviewer/view/ui/calendar_ui_qtdesigner.py index 34e8bfe7329fc58cfc6afa4bd9c9ef2b93d007ff..6db266827f8561da36409fb314163c68c43fb1ea 100644 --- a/sohstationviewer/view/ui/calendar_ui_qtdesigner.py +++ b/sohstationviewer/view/ui/calendar_ui_qtdesigner.py @@ -10,7 +10,7 @@ from PySide2 import QtCore, QtWidgets -from sohstationviewer.view.core.calendarwidget import CalendarWidget +from sohstationviewer.view.core.calendar_widget import CalendarWidget class Ui_CalendarDialog(object): diff --git a/sohstationviewer/view/ui/main_ui.py b/sohstationviewer/view/ui/main_ui.py index 8a6e102c08421d4056a46d27956cf9982158741c..ce350d4a11bf975e59714ec40f4079e203d5b6c8 100755 --- a/sohstationviewer/view/ui/main_ui.py +++ b/sohstationviewer/view/ui/main_ui.py @@ -2,9 +2,9 @@ from PySide2 import QtCore, QtGui, QtWidgets -from sohstationviewer.view.core.calendarwidget import CalendarWidget +from sohstationviewer.view.core.calendar_widget import CalendarWidget -from sohstationviewer.view.core.plottingWidget import PlottingWidget +from sohstationviewer.view.core.plotting_widget import PlottingWidget from sohstationviewer.conf import constants diff --git a/sohstationviewer/view/waveform_dialog.py b/sohstationviewer/view/waveform_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..6a90104615c78f9275b9e1ffa77312b88a162faa --- /dev/null +++ b/sohstationviewer/view/waveform_dialog.py @@ -0,0 +1,149 @@ +# UI and connectSignals for MainWindow + +from PySide2 import QtWidgets + +from sohstationviewer.view.core import plotting_widget as plottingWidget +from sohstationviewer.model.handling_data import trim_downsample_WFChan + +from sohstationviewer.controller.plottingData import getTitle + +from sohstationviewer.database import extractData + +from sohstationviewer.conf.dbSettings import dbConf + + +class WaveformWidget(plottingWidget.PlottingWidget): + + def plot_channels(self, start_tm, end_tm, key, + data_time, channel_list, time_ticks_total, + plotting_data1, plotting_data2): + """ + :param setID: (netID, statID, locID) + :param plottingData: a ditionary including: + { gaps: [(t1,t2),(t1,t2),...] (in epoch time) + channels:[cha: {netID, statID, locID, chanID, times, data, + samplerate, startTmEpoch, endTmEpoch} # + earliestUTC: the earliest time of all channels + latestUTC: the latest time of all channels + :timeTicksTotal: max number of tick to show on time bar + + """ + self.plotting_data1 = plotting_data1 + self.plotting_data2 = plotting_data2 + self.processing_log = [] # [(message, type)] + self.errors = [] + if self.axes != []: + self.fig.clear() + self.date_mode = self.parent.date_format.upper() + self.time_ticks_total = time_ticks_total + self.min_x = self.curr_min_x = max(data_time[0], start_tm) + self.max_x = self.curr_max_x = min(data_time[1], end_tm) + self.plot_no = len(self.plotting_data1) + len(self.plotting_data2) + title = getTitle(key, self.min_x, self.max_x, self.date_mode) + self.plotting_bot = plottingWidget.BOTTOM + self.plotting_bot_pixel = plottingWidget.BOTTOM_PX + self.axes = [] + + self.timestamp_bar_top = self.add_timestamp_bar(0.003) + self.set_title(title) + + for chanID in self.plotting_data1: + chan_db = extractData.getWFPlotInfo(chanID) + if chan_db['plotType'] == '': + continue + self.plotting_data1[chanID]['chan_db'] = chan_db + self.get_zoom_data(self.plotting_data1[chanID], chanID, True) + for chanID in self.plotting_data2: + chan_db = extractData.getChanPlotInfo(chanID, + self.parent.data_type) + self.plotting_data2[chanID]['chan_db'] = chan_db + self.get_zoom_data(self.plotting_data2[chanID], chanID, True) + + self.axes.append(self.plot_none()) + self.timestamp_bar_bottom = self.add_timestamp_bar(0.003, top=False) + self.set_lim(org_size=True) + self.bottom = self.axes[-1].get_ybound()[0] + self.ruler = self.add_ruler(self.display_color['time_ruler']) + self.zoom_marker1 = self.add_ruler(self.display_color['zoom_marker']) + self.zoom_marker2 = self.add_ruler(self.display_color['zoom_marker']) + # Set view size fit with the given data + if self.widgt.geometry().height() < self.plotting_bot_pixel: + self.widgt.setFixedHeight(self.plotting_bot_pixel) + + self.draw() + + def get_zoom_data(self, c_data, chan_id, first_time=False): + """ + :param setID: (netID, statID, locID) + :param plottingData: a ditionary including: + { gaps: [(t1,t2),(t1,t2),...] (in epoch time) + channels:{cha: {netID, statID, locID, chanID, times, data, + samplerate, startTmEpoch, endTmEpoch} # + earliestUTC: the earliest time of all channels + latestUTC: the latest time of all channels + :timeTicksTotal: max number of tick to show on time bar + + Data set: {channelname: [(x,y), (x,y)...] + """ + chan_db = c_data['chan_db'] + plot_type = chan_db['plotType'] + # data already processed for massposition in plottingWidget + if not (chan_id.startswith('VM') or chan_id.startswith('MP')): + trim_downsample_WFChan( + c_data, self.curr_min_x, self.curr_max_x, first_time) + self.apply_convert_factor(c_data, 1) + # use ax_wf because with massposition, ax has been used + # in plottingWidget + if 'ax_wf' not in c_data: + ax = getattr(self, dbConf['plotFunc'][plot_type][1])( + c_data, chan_db, chan_id, None, None) + if ax is None: + return + c_data['ax_wf'] = ax + ax.chan = chan_id + self.axes.append(ax) + else: + getattr(self, dbConf['plotFunc'][plot_type][1])( + c_data, chan_db, chan_id, c_data['ax_wf'], None) + + +class WaveformDialog(QtWidgets.QWidget): + def __init__(self, parent): + super().__init__() + self.parent = parent + self.date_format = self.parent.dateFormat + self.bitweight_opt = self.parent.bitweightOpt + self.massPosVoltRangeOpt = self.parent.massPosVoltRangeOpt + self.setGeometry(300, 300, 1200, 700) + self.setWindowTitle("Raw Data Plot") + + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + main_layout.setContentsMargins(5, 5, 5, 5) + main_layout.setSpacing(0) + + self.tracking_info_text_browser = QtWidgets.QTextBrowser(self) + + self.plotting_widget = WaveformWidget( + self, self.tracking_info_text_browser, "waveformWidget") + main_layout.addWidget(self.plotting_widget, 2) + + bottom_layout = QtWidgets.QHBoxLayout() + main_layout.addLayout(bottom_layout) + + self.write_ps_button = QtWidgets.QPushButton('Write .ps', self) + bottom_layout.addWidget(self.write_ps_button) + self.tracking_info_text_browser.setFixedHeight(60) + bottom_layout.addWidget(self.tracking_info_text_browser) + + self.connect_signals() + + def set_data(self, data_type, file_name): + self.data_type = data_type + self.setWindowTitle("Raw Data Plot %s - %s" % (data_type, file_name)) + + def resizeEvent(self, event): + self.plotting_widget.init_size() + + def connect_signals(self): + print("connectSignals") diff --git a/sohstationviewer/view/waveformdialog.py b/sohstationviewer/view/waveformdialog.py deleted file mode 100755 index be9b2d1761066539a782dd3c9f2478b56d6b3a8c..0000000000000000000000000000000000000000 --- a/sohstationviewer/view/waveformdialog.py +++ /dev/null @@ -1,148 +0,0 @@ -# UI and connectSignals for MainWindow - -from PySide2 import QtWidgets - -from sohstationviewer.view.core import plottingWidget as plottingWidget -from sohstationviewer.model.handling_data import trim_downsample_WFChan - -from sohstationviewer.controller.plottingData import getTitle - -from sohstationviewer.database import extractData - -from sohstationviewer.conf.dbSettings import dbConf - - -class WaveformWidget(plottingWidget.PlottingWidget): - - def plot_channels(self, startTm, endTm, key, - dataTime, channelList, timeTicksTotal, - plottingData1, plottingData2): - """ - :param setID: (netID, statID, locID) - :param plottingData: a ditionary including: - { gaps: [(t1,t2),(t1,t2),...] (in epoch time) - channels:[cha: {netID, statID, locID, chanID, times, data, - samplerate, startTmEpoch, endTmEpoch} # - earliestUTC: the earliest time of all channels - latestUTC: the latest time of all channels - :timeTicksTotal: max number of tick to show on time bar - - """ - self.plottingData1 = plottingData1 - self.plottingData2 = plottingData2 - self.processingLog = [] # [(message, type)] - self.errors = [] - if self.axes != []: - self.fig.clear() - self.dateMode = self.parent.dateFormat.upper() - self.timeTicksTotal = timeTicksTotal - self.minX = self.currMinX = max(dataTime[0], startTm) - self.maxX = self.currMaxX = min(dataTime[1], endTm) - self.plotNo = len(self.plottingData1) + len(self.plottingData2) - title = getTitle(key, self.minX, self.maxX, self.dateMode) - self.plottingBot = plottingWidget.BOTTOM - self.plottingBotPixel = plottingWidget.BOTTOM_PX - self.axes = [] - - self.timestampBarTop = self.add_timestamp_bar(0.003) - self.set_title(title) - - for chanID in self.plottingData1: - chanDB = extractData.getWFPlotInfo(chanID) - if chanDB['plotType'] == '': - continue - self.plottingData1[chanID]['chanDB'] = chanDB - self.getZoomData(self.plottingData1[chanID], chanID, True) - for chanID in self.plottingData2: - chanDB = extractData.getChanPlotInfo(chanID, self.parent.dataType) - self.plottingData2[chanID]['chanDB'] = chanDB - self.getZoomData(self.plottingData2[chanID], chanID, True) - - self.axes.append(self.plotNone()) - self.timestampBarBottom = self.add_timestamp_bar(0.003, top=False) - self.set_lim(orgSize=True) - self.bottom = self.axes[-1].get_ybound()[0] - self.ruler = self.add_ruler(self.displayColor['time_ruler']) - self.zoomMarker1 = self.add_ruler(self.displayColor['zoom_marker']) - self.zoomMarker2 = self.add_ruler(self.displayColor['zoom_marker']) - # Set view size fit with the given data - if self.widgt.geometry().height() < self.plottingBotPixel: - self.widgt.setFixedHeight(self.plottingBotPixel) - - self.draw() - - def getZoomData(self, cData, chanID, firsttime=False): - """ - :param setID: (netID, statID, locID) - :param plottingData: a ditionary including: - { gaps: [(t1,t2),(t1,t2),...] (in epoch time) - channels:{cha: {netID, statID, locID, chanID, times, data, - samplerate, startTmEpoch, endTmEpoch} # - earliestUTC: the earliest time of all channels - latestUTC: the latest time of all channels - :timeTicksTotal: max number of tick to show on time bar - - Data set: {channelname: [(x,y), (x,y)...] - """ - chanDB = cData['chanDB'] - plotType = chanDB['plotType'] - # data already processed for massposition in plottingWidget - if not (chanID.startswith('VM') or chanID.startswith('MP')): - trim_downsample_WFChan( - cData, self.currMinX, self.currMaxX, firsttime) - self.applyConvertFactor(cData, 1) - # use ax_wf because with massposition, ax has been used - # in plottingWidget - if 'ax_wf' not in cData: - ax = getattr(self, dbConf['plotFunc'][plotType][1])( - cData, chanDB, chanID, None, None) - if ax is None: - return - cData['ax_wf'] = ax - ax.chan = chanID - self.axes.append(ax) - else: - getattr(self, dbConf['plotFunc'][plotType][1])( - cData, chanDB, chanID, cData['ax_wf'], None) - - -class WaveformDialog(QtWidgets.QWidget): - def __init__(self, parent): - super().__init__() - self.parent = parent - self.dateFormat = self.parent.dateFormat - self.bitweightOpt = self.parent.bitweightOpt - self.massPosVoltRangeOpt = self.parent.massPosVoltRangeOpt - self.setGeometry(300, 300, 1200, 700) - self.setWindowTitle("Raw Data Plot") - - mainLayout = QtWidgets.QVBoxLayout() - self.setLayout(mainLayout) - mainLayout.setContentsMargins(5, 5, 5, 5) - mainLayout.setSpacing(0) - - self.trackingInfoTextBrowser = QtWidgets.QTextBrowser(self) - - self.plottingWidget = WaveformWidget( - self, self.trackingInfoTextBrowser, "waveformWidget") - mainLayout.addWidget(self.plottingWidget, 2) - - bottomLayout = QtWidgets.QHBoxLayout() - mainLayout.addLayout(bottomLayout) - - self.writePSButton = QtWidgets.QPushButton('Write .ps', self) - bottomLayout.addWidget(self.writePSButton) - self.trackingInfoTextBrowser.setFixedHeight(60) - bottomLayout.addWidget(self.trackingInfoTextBrowser) - - self.connectSignals() - - def setData(self, dataType, fileName): - self.dataType = dataType - self.setWindowTitle("Raw Data Plot %s - %s" % (dataType, fileName)) - - def resizeEvent(self, event): - self.plottingWidget.init_size() - - def connectSignals(self): - print("connectSignals")