diff --git a/HISTORY.rst b/HISTORY.rst index a11eba1dbaaf89fe3edd23aef4f0ce3f323948b5..cc55785c82c0e320b44944cc5b4fac2e0d37e20e 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -6,3 +6,8 @@ History -------- * First release + +2023.1.0.1 +-------- + +* Fixed a problem with SOHViewer not fitting on smaller resolutions diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index 62621b47e87de2ea12d6334f6c0437ebba27707b..85c88f6da3497683abd645dd3caf947c640ec730 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -1,6 +1,6 @@ package: name: sohviewer - version: 2023.1.0.0 + version: 2023.1.0.1 source: path: ../ diff --git a/setup.py b/setup.py index c1e87d5a3c4b8d50f5b49600df7619c752abdb22..f39f957cfba0bbc849b3a904505f8e172cb4304b 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,6 @@ setup( name='sohstationviewer', packages=find_packages(include=['sohstationviewer*']), url='https://git.passcal.nmt.edu/software_public/passoft/sohstationviewer', - version='2023.1.0.0', + version='2023.1.0.1', zip_safe=False, ) diff --git a/sohstationviewer/conf/constants.py b/sohstationviewer/conf/constants.py index 887062311ad6caced57d3e2729d62e45f55a108b..eaa03f70e9de9a08e258a826332c182bcb828994 100644 --- a/sohstationviewer/conf/constants.py +++ b/sohstationviewer/conf/constants.py @@ -1,7 +1,7 @@ from typing import Literal # The current version of SOHStationViewer -SOFTWARE_VERSION = '2023.1.0.0' +SOFTWARE_VERSION = '2023.1.0.1' # waveform pattern WF_1ST = 'A-HLM-V' diff --git a/sohstationviewer/database/extract_data.py b/sohstationviewer/database/extract_data.py index 55997e90a683cc8d119fa26aebc419ebb0abe3ff..2a49d557064a2e5c78f127f6bc1e9f13c86bc7d4 100755 --- a/sohstationviewer/database/extract_data.py +++ b/sohstationviewer/database/extract_data.py @@ -160,3 +160,35 @@ def get_color_ranges(): clr_labels[idx].append("+/- {:,} counts".format(cnt)) clr_labels[idx].append("> {:,} counts".format(cnt)) return range_names, all_square_counts, clr_labels + + +def create_assign_string_for_db_query(col: str, val: str) -> str: + """ + Create assign db string that assign value in single quote signs if val is + a string or to NULL if val is empty str + :param col: column name in the db table + :param val: value to be assigned to the column + :return: the assign db string + """ + return f"{col}='{val}'" if val != '' else f"{col}=NULL" + + +def get_params(): + # get parameter list from database + param_rows = execute_db("SELECT param from parameters") + return sorted([d[0] for d in param_rows]) + + +def get_channel_info(chan_id: str, data_type: str): + # get channel info from DB + sql = f"SELECT * FROM Channels " \ + f"WHERE channel='{chan_id}' AND dataType='{data_type}'" + chan_info = execute_db_dict(sql)[0] + return chan_info + + +def get_param_info(param: str): + # get all info of a param from DB + sql = f"SELECT * FROM Parameters WHERE param='{param}'" + param_info = execute_db_dict(sql)[0] + return param_info diff --git a/sohstationviewer/images/edit_icon_black_background.png b/sohstationviewer/images/edit_icon_black_background.png new file mode 100644 index 0000000000000000000000000000000000000000..12b52393c45fc4d156d15a83e6f4156542dec75a Binary files /dev/null and b/sohstationviewer/images/edit_icon_black_background.png differ diff --git a/sohstationviewer/images/edit_icon_white_background.png b/sohstationviewer/images/edit_icon_white_background.png new file mode 100644 index 0000000000000000000000000000000000000000..2f374595e8ed857d346ba0af0fc3e37fb1863447 Binary files /dev/null and b/sohstationviewer/images/edit_icon_white_background.png differ diff --git a/sohstationviewer/main.py b/sohstationviewer/main.py index a25d1b15a3f6e3d3e04707a131472287d279c00d..e09e707700dec1f93cad45a377edd9c7b663dbf0 100755 --- a/sohstationviewer/main.py +++ b/sohstationviewer/main.py @@ -6,6 +6,7 @@ import traceback from pathlib import Path from PySide2 import QtWidgets +from PySide2.QtGui import QGuiApplication from PySide2.QtWidgets import QMessageBox from sohstationviewer.view.main_window import MainWindow @@ -14,7 +15,6 @@ from sohstationviewer.conf.config_processor import ( BadConfigError, ) - # Enable Layer-backing for MacOs version >= 11 # Only needed if using the pyside2 library with version>=5.15. # Layer-backing is always enabled in pyside6. @@ -26,47 +26,91 @@ if os_name == 'macOS': os.environ['QT_MAC_WANTS_LAYER'] = '1' -def main(): - # Change the working directory so that relative paths work correctly. +def fix_relative_paths() -> None: + """ + Change the working directory so that the relative paths in the code work + correctly. + """ current_file_path = os.path.abspath(__file__) current_file_dir = Path(current_file_path).parent os.chdir(current_file_dir) + +def resize_windows(main_window: MainWindow): + """ + Resize the plotting windows in the program so that they fit well on all + screen resolutions. + """ + screen_size = QtWidgets.QApplication.primaryScreen().size().toTuple() + screen_width, screen_height = screen_size + main_window.resize(screen_width * 1, screen_height * 1) + main_window.waveform_dlg.resize(screen_width * (2 / 3), + screen_height * (2 / 3)) + main_window.tps_dlg.resize(screen_width * (2 / 3), screen_height * (2 / 3)) + + main_right_x = (main_window.x() + main_window.frameSize().width()) + # This offset is based on the hard-coded geometry previously assigned to + # the waveform and TPS dialogs + wf_tps_x_offset = 50 + # We are moving the TPS dialog so that it is slightly offset from the right + # edge of the main window + tps_dlg_x = main_right_x - main_window.tps_dlg.width() - wf_tps_x_offset + wf_tps_y = 50 + main_window.waveform_dlg.move(wf_tps_x_offset, wf_tps_y) + main_window.tps_dlg.move(tps_dlg_x, wf_tps_y) + + gps_dlg_y = main_window.height() / 5 + main_window.gps_dialog.move(main_window.gps_dialog.y(), gps_dlg_y) + + +def check_if_user_want_to_reset_config() -> bool: + """ + Show a dialog asking the user if they want to reset the config. + :return: whether the user wants to reset the config. + """ + bad_config_dialog = QMessageBox() + bad_config_dialog.setText('Something went wrong when reading the ' + 'config.') + bad_config_dialog.setDetailedText(traceback.format_exc()) + bad_config_dialog.setInformativeText('Do you want to reset the config ' + 'file?') + bad_config_dialog.setStandardButtons(QMessageBox.Ok | + QMessageBox.Close) + bad_config_dialog.setDefaultButton(QMessageBox.Ok) + bad_config_dialog.setIcon(QMessageBox.Critical) + reset_choice = bad_config_dialog.exec_() + return reset_choice == QMessageBox.Ok + + +def main(): + # Change the working directory so that relative paths work correctly. + fix_relative_paths() + app = QtWidgets.QApplication(sys.argv) wnd = MainWindow() + config = ConfigProcessor() config.load_config() - need_reset = False + do_reset = None try: config.validate_config() config.apply_config(wnd) except (BadConfigError, ValueError) as e: - bad_config_dialog = QMessageBox() - bad_config_dialog.setText('Something went wrong when reading the ' - 'config.') - bad_config_dialog.setDetailedText(traceback.format_exc()) - bad_config_dialog.setInformativeText('Do you want to reset the config ' - 'file?') - bad_config_dialog.setStandardButtons(QMessageBox.Ok | - QMessageBox.Close) - bad_config_dialog.setDefaultButton(QMessageBox.Ok) - bad_config_dialog.setIcon(QMessageBox.Critical) - reset_choice = bad_config_dialog.exec_() - if reset_choice == QMessageBox.Ok: - need_reset = True - else: - sys.exit(1) - if need_reset: + do_reset = check_if_user_want_to_reset_config() + if do_reset: try: config.reset() except OSError: QMessageBox.critical(None, 'Cannot reset config', - 'Config file cannot be reset. Please ' - 'ensure that it is not opened in another ' - 'program.', + 'Config file cannot be reset. Please ensure ' + 'that it is not opened in another program.', QMessageBox.Close) sys.exit(1) + elif do_reset is not None: + sys.exit(1) config.apply_config(wnd) + + resize_windows(wnd) wnd.show() sys.exit(app.exec_()) diff --git a/sohstationviewer/view/db_config/add_edit_single_channel_dialog.py b/sohstationviewer/view/db_config/add_edit_single_channel_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..26c3b30db1ced1aa2de65890f15d3cf4483ec083 --- /dev/null +++ b/sohstationviewer/view/db_config/add_edit_single_channel_dialog.py @@ -0,0 +1,351 @@ +import sys +import platform +import os +from typing import Optional, Dict + +from PySide2 import QtWidgets, QtGui +from PySide2.QtWidgets import QWidget, QDialog + +from sohstationviewer.database.process_db import execute_db +from sohstationviewer.database.extract_data import ( + get_params, get_channel_info, create_assign_string_for_db_query +) + +from sohstationviewer.view.db_config.edit_single_param_dialog import \ + EditSingleParamDialog + +from sohstationviewer.conf.dbSettings import modify_db_path + + +def add_separation_line(layout): + """ + Add a line for separation to the given layout. + :param layout: QLayout - the layout that contains the line + """ + label = QtWidgets.QLabel() + label.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) + label.setLineWidth(1) + layout.addWidget(label) + + +class AddEditSingleChannelDialog(QDialog): + """ + Dialog to add info for channel not in database or edit the existing channel + """ + def __init__(self, parent: Optional[QWidget], + chan_id: str, data_type: str): + """ + :param parent: the parent widget + :param chan_id: name of channel to be added/edited + :param data_type: type of the data being processed + """ + self.parent = parent + # name of the channel + self.chan_id = chan_id + # data_type of the channel + self.data_type = data_type + + # param of the channel + self.param: str = 'Default' + # True if this channel isn't in DB yet + self.is_new_db_channel: bool = False + # To skip on_param_chkbox_changed() when param is changed by the + # program at the beginning + self.param_changed_by_signal: bool = False + # database info of the channel + self.channel_info: Dict = {} + # database info of the channel's parameter + self.param_info: Dict = {} + super(AddEditSingleChannelDialog, self).__init__(parent) + + # short name of the channel + self.channel_name_lnedit = QtWidgets.QLineEdit(self) + self.channel_name_lnedit.setReadOnly(True) + + # description added to channel name + self.label_lnedit = QtWidgets.QLineEdit(self) + self.label_lnedit.setPlaceholderText( + "added to channel name to be displayed") + + # convert factor to change from count to actual value + self.conversion_lnedit = QtWidgets.QLineEdit(self) + self.conversion_lnedit.setPlaceholderText( + "to convert from count to actual value" + ) + validator = QtGui.QDoubleValidator(0.0, 5.0, 6) + validator.setNotation(QtGui.QDoubleValidator.StandardNotation) + self.conversion_lnedit.setValidator(validator) + self.conversion_lnedit.setText('1') + + # channel's unit + self.unit_lnedit = QtWidgets.QLineEdit(self) + + # dedimal point for channel's value + self.fix_point_spnbox = QtWidgets.QSpinBox() + self.fix_point_spnbox.setToolTip("Decimal point that allow in display") + self.fix_point_spnbox.setMinimum(0) + self.fix_point_spnbox.setMaximum(5) + + # data_type + self.data_type_lnedit = QtWidgets.QLineEdit(self) + self.data_type_lnedit.setReadOnly(True) + + # channel's parameter which decides how channel is plotted + self.param_cbobox = QtWidgets.QComboBox(self) + self.param_cbobox.addItems(get_params()) + + # button to edit param + self.edit_param_btn = QtWidgets.QPushButton("EDIT PARAMETER", self) + # button to save changes to DB + self.save_btn = QtWidgets.QPushButton("SAVE CHANNEL", self) + # button to save changes and replot channel + self.save_replot_btn = QtWidgets.QPushButton("SAVE & REPLOT", self) + # button to close dialog without doing anything + self.cancel_btn = QtWidgets.QPushButton('CANCEL', self) + + self.setup_ui() + self.set_channel_info() + self.connect_signals() + + def setup_ui(self) -> None: + dlg_type = 'Add' if 'DEFAULT' in self.chan_id else 'Edit' + self.setWindowTitle(f"{dlg_type} channel {self.chan_id}" + f" - {self.data_type}") + + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + instruction = ( + f"This dialog is to {dlg_type} channel {self.chan_id}.\n" + "Parameter need to be any value different than 'Default' to be " + "saved.") + main_layout.addWidget(QtWidgets.QLabel(instruction)) + + channel_layout = QtWidgets.QGridLayout() + main_layout.addLayout(channel_layout) + + channel_layout.addWidget(QtWidgets.QLabel('Name'), 0, 0, 1, 1) + channel_layout.addWidget(self.channel_name_lnedit, 0, 1, 1, 1) + + channel_layout.addWidget(QtWidgets.QLabel('Label'), 1, 0, 1, 1) + channel_layout.addWidget(self.label_lnedit, 1, 1, 1, 1) + + channel_layout.addWidget(QtWidgets.QLabel('Conversion'), 2, 0, 1, 1) + channel_layout.addWidget(self.conversion_lnedit, 2, 1, 1, 1) + + channel_layout.addWidget(QtWidgets.QLabel('Unit'), 3, 0, 1, 1) + channel_layout.addWidget(self.unit_lnedit, 3, 1, 1, 1) + + channel_layout.addWidget(QtWidgets.QLabel('Fix Point'), 4, 0, 1, 1) + channel_layout.addWidget(self.fix_point_spnbox, 4, 1, 1, 1) + + channel_layout.addWidget(QtWidgets.QLabel('Data Type'), 5, 0, 1, 1) + channel_layout.addWidget(self.data_type_lnedit, 5, 1, 1, 1) + + channel_layout.addWidget(QtWidgets.QLabel('Parameter'), 6, 0, 1, 1) + channel_layout.addWidget(self.param_cbobox, 6, 1, 1, 1) + + channel_layout.addWidget(self.save_btn, 7, 0, 1, 1) + channel_layout.addWidget(self.save_replot_btn, 7, 1, 1, 1) + + channel_layout.addWidget(self.edit_param_btn, 8, 1, 1, 1) + channel_layout.addWidget(self.cancel_btn, 8, 0, 1, 1) + self.save_replot_btn.setFocus() + + def connect_signals(self) -> None: + self.param_cbobox.currentTextChanged.connect( + self.on_param_cbobox_changed) + self.cancel_btn.clicked.connect(self.close) + self.save_btn.clicked.connect(self.on_save) + self.save_replot_btn.clicked.connect(self.on_save_replot) + self.edit_param_btn.clicked.connect(self.on_edit_param) + + def set_channel_info(self): + """ + Add all Channel related info according to information got from DB. + In case Channel isn't in the DB, use the info of DEFAULT channel. + Call set_param_info to set Parameter related info. + """ + try: + self.channel_info = get_channel_info(self.chan_id, self.data_type) + except IndexError: + self.is_new_db_channel = True + self.channel_info = get_channel_info('DEFAULT', 'Default') + + self.channel_name_lnedit.setText(self.chan_id) + + self.label_lnedit.setText(self.channel_info['label']) + + self.conversion_lnedit.setText( + str(float(self.channel_info['convertFactor']))) + + self.unit_lnedit.setText(self.channel_info['unit']) + + if self.channel_info['fixPoint'] is not None: + self.fix_point_spnbox.setValue(self.channel_info['fixPoint']) + + self.data_type_lnedit.setText(self.data_type) + + self.param_cbobox.setCurrentText(self.channel_info['param']) + self.param = self.channel_info['param'] + self.set_buttons_enabled() + + def on_param_cbobox_changed(self): + """ + + Check self.param_changed_by_signal to make sure the signal come from + user selecting a parameter from param_cbobox, not the signal from + changing back to original value when condition not pass fo the set + value + + Not allow parameter 'Default' to be select because it is only the + parameter for the channel that has no record in DB + + If the channel already has a record in DB, give a warning when user's + trying to change its parameter. + """ + if self.param_changed_by_signal: + self.param_changed_by_signal = False + return + new_param = self.param_cbobox.currentText() + if new_param == 'Default': + # Parameter Default is only for channel that has no record in DB. + # So it isn't allowed to be selected + self.param_changed_by_signal = True + self.param_cbobox.setCurrentText(self.param) + return + if not self.is_new_db_channel: + msg = ("ARE YOU SURE YOU WANT TO CHANGE PARAMETER FOR CHANNEL " + f"'{self.chan_id}'?") + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + if result == QtWidgets.QMessageBox.No: + self.param_changed_by_signal = True + self.param_cbobox.setCurrentText(self.param) + return + self.param = new_param + self.set_buttons_enabled() + + def save(self): + """ + Save info from GUI to DB + """ + if self.is_new_db_channel: + self.insert_channel_info() + else: + self.update_channel_info() + + def on_save(self): + """ + Save new channel info to DB + """ + self.save() + self.close() + + def on_save_replot(self): + """ + Save new channel info to DB + TODO: Replot the channel in the plotting area + """ + self.save() + print("Do REPLOT") + self.close() + + def set_buttons_enabled(self): + """ + Disable the 3 buttons to save and to edit parameters so that user are + forced to change param before they want to continue. + """ + if self.param == 'Default': + self.edit_param_btn.setEnabled(False) + self.save_btn.setEnabled(False) + self.save_replot_btn.setEnabled(False) + else: + self.edit_param_btn.setEnabled(True) + self.save_btn.setEnabled(True) + self.save_replot_btn.setEnabled(True) + + def on_edit_param(self): + """ + Give user a warning when they want to change parameter's info then + open EditSingleParamDialog for user to edit parameter's info after + they give their confirmation. + """ + msg = ("Changing parameter will affect all of other channels that " + "have the same parameter.\n\n" + "Are you sure you want to continue?") + result = QtWidgets.QMessageBox.question( + self, "Confirmation", msg, + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + if result == QtWidgets.QMessageBox.No: + return + win = EditSingleParamDialog(self, self.param_cbobox.currentText()) + win.exec_() + + def update_para_info(self, param): + """ + Save parameter related info to Parameters table + :param param: param condition string + """ + plot_type = create_assign_string_for_db_query( + 'plotType', self.plot_type_cbo_box.currentText()) + value_colorb = create_assign_string_for_db_query( + 'valueColorsB', self.value_colorb_widget.text()) + value_colorw = create_assign_string_for_db_query( + 'valueColorsW', self.value_colorw_widget.text()) + height = f"height={self.height_spnbox.value()}" + sql = (f"UPDATE Parameters SET {plot_type}, {value_colorb}, " + f"{value_colorw}, {height} WHERE {param}") + execute_db(sql) + + def insert_channel_info(self): + sql = ("INSERT INTO Channels VALUES (" + f"'{self.channel_name_lnedit.text()}', " + f"'{self.label_lnedit.text()}', " + f"'{self.param_cbobox.currentText()}', " + f"NULL, " # linkedChan for RT130 only and won't be changed + f"{self.conversion_lnedit.text()}, " + f"'{self.unit_lnedit.text()}', " + f"{self.fix_point_spnbox.value()}, " + f"'{self.data_type_lnedit.text()}')") + execute_db(sql) + + def update_channel_info(self): + channel = f"channel='{self.channel_name_lnedit.text()}'" + label = f"label='{self.label_lnedit.text()}'" + param = f"param='{self.param_cbobox.currentText()}'" + linked_chan = "linkedChan=NULL" + convert_factor = f"convertFactor={self.conversion_lnedit.text()}" + unit = f"unit='{self.unit_lnedit.text()}'" + fix_point = f"fixPoint={self.fix_point_spnbox.value()}" + data_type = f"dataType='{self.data_type_lnedit.text()}'" + sql = (f"UPDATE Channels SET {label}, {param}, {linked_chan}, " + f"{convert_factor}, {unit}, {fix_point}, {data_type} " + f"WHERE {channel}") + execute_db(sql) + + +if __name__ == '__main__': + modify_db_path() + os_name, version, *_ = platform.platform().split('-') + if os_name == 'macOS': + os.environ['QT_MAC_WANTS_LAYER'] = '1' + app = QtWidgets.QApplication(sys.argv) + + # test new channel + # test = AddEditSingleChannelDialog(None, 'VEE', 'Q330') + + # test linesDots. Ex: param: Input power supply current + # test = AddEditSingleChannelDialog(None, 'VEC', 'Q330') + + # test MultiColorDotsLowerBound. Ex: param:Backup volt + # test = AddEditSingleChannelDialog(None, 'Backup Volt', 'RT130') + + # test MultiColorDotsUpperBound. Ex: param:GNSS status + test = AddEditSingleChannelDialog(None, 'VST', 'Pegasus') + + # test UpDownDots. Ex: param. Ex: param:Net Up/down + # test = AddEditSingleChannelDialog(None, 'Net Up/Down', 'RT130') + + # test TriColorLInes. Ex: param. Ex: param:Error/warning + # test = AddEditSingleChannelDialog(None, 'Error/Warning', 'RT130') + test.exec_() + sys.exit(app.exec_()) diff --git a/sohstationviewer/view/db_config/edit_single_param_dialog.py b/sohstationviewer/view/db_config/edit_single_param_dialog.py new file mode 100755 index 0000000000000000000000000000000000000000..57ccae061d33867e21124237b22e72798b78b407 --- /dev/null +++ b/sohstationviewer/view/db_config/edit_single_param_dialog.py @@ -0,0 +1,169 @@ +import sys +import platform +import os +from typing import Optional, Dict + +from PySide2 import QtWidgets +from PySide2.QtWidgets import QWidget, QDialog, QLineEdit + +from sohstationviewer.view.util.plot_func_names import plot_functions + +from sohstationviewer.database.process_db import execute_db +from sohstationviewer.database.extract_data import ( + get_param_info, create_assign_string_for_db_query +) + +from sohstationviewer.conf.dbSettings import modify_db_path + + +class EditSingleParamDialog(QDialog): + """ + Dialog to add info for channel not in database or edit the existing channel + """ + def __init__(self, parent: Optional[QWidget], + param: str): + """ + :param parent: the parent widget + :param chan_id: name of channel to be added/edited + :param data_type: type of the data being processed + """ + self.param = param + # # To skip on_param_chkbox_changed() when param is changed by the + # # program at the beginning + # self.param_changed_by_signal = False + # database info of the channel + self.channel_info: Dict = {} + # database info of the channel's parameter + self.param_info: Dict = {} + super(EditSingleParamDialog, self).__init__(parent) + + # parameter's plot type which decides the shape of the plot + self.plot_type_cbo_box = QtWidgets.QComboBox(self) + self.plot_type_cbo_box.addItems([""] + list(plot_functions.keys())) + + # value color in black mode + self.value_colorb_widget = QLineEdit(self) + self.value_colorb_widget.setPlaceholderText( + "Click edit button to add value color string") + self.value_colorb_widget.setToolTip("Priority from left to right") + # value, color in white mode + self.value_colorw_widget = QLineEdit(self) + self.value_colorw_widget.setPlaceholderText( + "Click edit button to add value color string") + self.value_colorw_widget.setToolTip("Priority from left to right") + + # height of the plot + self.height_spnbox = QtWidgets.QSpinBox() + self.height_spnbox.setMinimum(0) + self.height_spnbox.setMaximum(8) + self.height_spnbox.setToolTip("Relative height of the plot") + + # button to save change to DB + self.save_param_btn = QtWidgets.QPushButton( + "SAVE PARAMETER", self) + # button to close dialog without doing anything + self.cancel_btn = QtWidgets.QPushButton('CANCEL', self) + + self.setup_ui() + self.set_param_info() + self.connect_signals() + + def setup_ui(self) -> None: + self.setWindowTitle(f"Edit Parameter {self.param}") + + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + param_layout = QtWidgets.QGridLayout() + main_layout.addLayout(param_layout) + + param_layout.addWidget(QtWidgets.QLabel('Plot Type'), 0, 0, 1, 1) + param_layout.addWidget(self.plot_type_cbo_box, 0, 1, 1, 1) + + param_layout.addWidget(QtWidgets.QLabel( + 'Value Color (black)'), 1, 0, 1, 1) + param_layout.addWidget(self.value_colorb_widget, 1, 1, 1, 1) + + param_layout.addWidget(QtWidgets.QLabel( + 'Value Color (white)'), 2, 0, 1, 1) + param_layout.addWidget(self.value_colorw_widget, 2, 1, 1, 1) + + param_layout.addWidget(QtWidgets.QLabel('Height'), 3, 0, 1, 1) + param_layout.addWidget(self.height_spnbox, 3, 1, 1, 1) + + param_layout.addWidget(self.cancel_btn, 4, 0, 1, 1) + param_layout.addWidget(self.save_param_btn, 4, 1, 1, 1) + + def connect_signals(self) -> None: + self.plot_type_cbo_box.currentTextChanged.connect(self.set_plot_type) + self.cancel_btn.clicked.connect(self.close) + self.save_param_btn.clicked.connect(self.on_save_param) + + def set_param_info(self) -> None: + """ + Fill up all info boxes + """ + self.param_info = get_param_info(self.param) + self.set_plot_type(self.param_info['plotType']) + self.height_spnbox.setValue(self.param_info['height']) + + def set_plot_type(self, plot_type: str) -> None: + """ + Add Plot Type, Value Color strings. + If there is no Plot Type, no Value Color or Height because no plot. + :param plot_type: name of Plot Type + """ + if plot_type in ["", None]: + self.plot_type_cbo_box.setCurrentText('') + self.value_colorb_widget.setEnabled(False) + self.value_colorb_widget.clear() + self.value_colorw_widget.setEnabled(False) + self.value_colorw_widget.clear() + self.height_spnbox.setValue(0) + else: + self.plot_type_cbo_box.setCurrentText(plot_type) + value_color_b = self.param_info['valueColorsB'] + value_color_w = self.param_info['valueColorsW'] + self.value_colorb_widget.setText(value_color_b) + self.value_colorw_widget.setText(value_color_w) + + def on_save_param(self): + """ + Save parameter info to Parameters table + """ + plot_type = create_assign_string_for_db_query( + 'plotType', self.plot_type_cbo_box.currentText()) + value_colorb = create_assign_string_for_db_query( + 'valueColorsB', self.value_colorb_widget.text()) + value_colorw = create_assign_string_for_db_query( + 'valueColorsW', self.value_colorw_widget.text()) + height = f"height={self.height_spnbox.value()}" + sql = (f"UPDATE Parameters SET {plot_type}, {value_colorb}, " + f"{value_colorw}, {height} WHERE param='{self.param}'") + execute_db(sql) + self.close() + + +if __name__ == '__main__': + modify_db_path() + os_name, version, *_ = platform.platform().split('-') + if os_name == 'macOS': + os.environ['QT_MAC_WANTS_LAYER'] = '1' + app = QtWidgets.QApplication(sys.argv) + + # test linesDots. Ex: param: Input power supply current + test = EditSingleParamDialog(None, 'Input power supply current') + + # test MultiColorDotsLowerBound. Ex: param:Backup volt + # test = EditSingleParamDialog(None, 'Backup Volt', 'RT130') + + # test MultiColorDotsUpperBound. Ex: param:GNSS status + # test = EditSingleParamDialog(None, 'GNSS status') + + # test UpDownDots. Ex: param. Ex: param:Net Up/down + # test = EditSingleParamDialog(None, 'Net Up/Down', 'RT130') + + # test TriColorLInes. Ex: param. Ex: param:Error/warning + # test = EditSingleParamDialog(None, 'Error/Warning', 'RT130') + test.exec_() + sys.exit(app.exec_()) diff --git a/sohstationviewer/view/db_config/value_color_helper/value_color_widget.py b/sohstationviewer/view/db_config/value_color_helper/value_color_widget.py new file mode 100644 index 0000000000000000000000000000000000000000..914655d5f2e22b25606e5dad769056aedd2b2287 --- /dev/null +++ b/sohstationviewer/view/db_config/value_color_helper/value_color_widget.py @@ -0,0 +1,136 @@ +import sys +import platform +import os +from pathlib import Path +from typing import Optional + +from PySide2 import QtWidgets, QtGui, QtCore +from PySide2.QtCore import Qt +from PySide2.QtWidgets import QWidget, QDialog, QTextEdit + +from sohstationviewer.view.db_config.value_color_helper.functions import \ + convert_value_color_str, prepare_value_color_html + + +class ValueColorWidget(QTextEdit): + def __init__(self, parent: Optional[QWidget], background: str): + """ + Widget to display valueColors and call a dialog to edit tha value + :param parent: the parent widget + :param background: 'B'/'W': flag indicating background color + """ + QtWidgets.QTextEdit.__init__(self, parent) + self.set_background(background) + # string for value color to be saved in DB + self.value_color_str: str = '' + # dialog that pop up when clicking on edit_button to help edit color + # and value + self.edit_value_color_dialog: Optional[QWidget] = None + # type of channel's plot + self.plot_type: str = '' + + self.setReadOnly(True) + # change cursor to Arrow so user know they can't edit directly + self.viewport().setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor)) + # to see all info + self.setFixedHeight(28) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.verticalScrollBar().setDisabled(True) + + self.edit_button = QtWidgets.QToolButton(self) + self.edit_button.setCursor(Qt.PointingHandCursor) + current_file_path = os.path.abspath(__file__) + root_path = Path(current_file_path).parent.parent.parent.parent + if background == 'B': + img_file = f"{root_path}/images/edit_icon_black_background.png" + else: + img_file = f"{root_path}/images/edit_icon_white_background.png" + self.edit_button.setIcon(QtGui.QIcon(img_file)) + self.edit_button.setStyleSheet( + "background: transparent; border: none;") + self.edit_button.clicked.connect(self.on_edit) + + layout = QtWidgets.QHBoxLayout(self) + layout.addWidget(self.edit_button, 0, Qt.AlignRight) + + layout.setSpacing(0) + layout.setMargin(5) + + def set_background(self, background: str): + """ + Set black background for user to have better feeling how the colors + displayed on black background. Text and PlaceholderText's colors + have to be changed to be readable on the black background too. + :param background: 'B'/'W': sign for background color + """ + palette = self.palette() + if background == 'B': + palette.setColor(QtGui.QPalette.Text, Qt.white) + palette.setColor(QtGui.QPalette.Base, Qt.black) + palette.setColor(QtGui.QPalette.PlaceholderText, Qt.lightGray) + self.setPalette(palette) + + def set_value_color(self, plot_type: str, value_color_str: str) \ + -> None: + """ + Set value_color_str, value_color_edit_dialog and display value color + string in html to show color for user to have the feeling. + :param plot_type: type of channel's plot + :param value_color_str: string for value color to be saved in DB + """ + + self.plot_type = plot_type + # Won't need to convert after database's valueColors are changed + self.value_color_str = convert_value_color_str( + plot_type, value_color_str) + value_color_html = prepare_value_color_html(self.value_color_str) + self.setHtml(value_color_html) + + def on_edit(self): + print('edit value color') + + +class TestDialog(QDialog): + def __init__(self): + super(TestDialog, self).__init__(None) + main_layout = QtWidgets.QVBoxLayout() + self.setLayout(main_layout) + + self.value_colorb_widget = ValueColorWidget(self, 'B') + main_layout.addWidget(self.value_colorb_widget) + self.value_colorw_widget = ValueColorWidget(self, 'W') + main_layout.addWidget(self.value_colorw_widget) + + # linesDots + self.value_colorb_widget.set_value_color('linesDots', 'L:R|D:G') + self.value_colorw_widget.set_value_color('linesDots', 'L:R|D:G') + + # triColorLines + # self.value_colorb_widget.set_value_color( + # 'triColorLines', '-1:M|0:R|1:G') + # self.value_colorw_widget.set_value_color( + # 'triColorLines', '-1:M|0:R|1:G') + + # multiColorDotsEqualOnUpperBound + # self.value_colorb_widget.set_value_color( + # 'multiColorDotsEqualOnUpperBound', '0:R|1:Y|2:G|+2:M') + # self.value_colorw_widget.set_value_color( + # 'multiColorDotsEqualOnUpperBound', '0:R|1:Y|2:G|+2:M') + + # multiColorDotsEqualOnLowerBound + # self.value_colorb_widget.set_value_color('multiColorDotsEqualOnLowerBound', + # '3:R|3.3:Y|=3.3:G') + # self.value_colorw_widget.set_value_color('multiColorDotsEqualOnLowerBound', + # '3:R|3.3:Y|=3.3:G') + + +if __name__ == '__main__': + os_name, version, *_ = platform.platform().split('-') + if os_name == 'macOS': + os.environ['QT_MAC_WANTS_LAYER'] = '1' + app = QtWidgets.QApplication(sys.argv) + + test = TestDialog() + test.exec_() + sys.exit(app.exec_()) diff --git a/sohstationviewer/view/plotting/gps_plot/gps_dialog.py b/sohstationviewer/view/plotting/gps_plot/gps_dialog.py index ebdc9d4f1500493ea850ff21ae6a40b69196a0ac..c1be4ff05b1d799e85ddd9748ff1f36e2bff72b9 100644 --- a/sohstationviewer/view/plotting/gps_plot/gps_dialog.py +++ b/sohstationviewer/view/plotting/gps_plot/gps_dialog.py @@ -3,7 +3,7 @@ import sys import os import traceback from pathlib import Path -from typing import List, Optional, Literal, Iterable +from typing import List, Optional, Literal, Iterable, Any from PySide2 import QtWidgets, QtCore from matplotlib.axes import Axes @@ -31,7 +31,7 @@ class GPSWidget(QtWidgets.QWidget): # one point for each coordinate. self.unique_gps_points: Optional[List[GPSPoint]] = None - self.fig = Figure(figsize=(6, 6), dpi=100) + self.fig = Figure(figsize=(6, 6), dpi=100, facecolor='#ECECEC') self.canvas = Canvas(self.fig) self.canvas.mpl_connect('pick_event', self.on_pick_event) @@ -156,7 +156,7 @@ class GPSWidget(QtWidgets.QWidget): self.repaint() self.tracking_box.clear() - def on_pick_event(self, event) -> None: + def on_pick_event(self, event) -> Any: """ On a GPS point being picked, display the data of that point on the tracking box. @@ -172,13 +172,13 @@ class GPSWidget(QtWidgets.QWidget): # and longitude follows correspondingly. lat_dir = 'N' if picked_point.latitude > 0 else 'S' long_dir = 'E' if picked_point.longitude > 0 else 'W' - meta_separator = ' ' * 22 - loc_separator = ' ' * 10 + meta_separator = ' ' * 7 + loc_separator = ' ' * 5 msg = ( - f'Mark: {picked_point.last_timemark}{meta_separator}' + f' Mark: {picked_point.last_timemark}{meta_separator}' f'Fix: {picked_point.fix_type}{meta_separator}' f'Sats: {picked_point.num_satellite_used}<br>' - f'Lat: {lat_dir}{abs(picked_point.latitude):.6f}{loc_separator}' + f' Lat: {lat_dir}{abs(picked_point.latitude):.6f}{loc_separator}' f'Long: {long_dir}{abs(picked_point.longitude):.6f}{loc_separator}' f'Elev: {picked_point.height}{picked_point.height_unit}' ) @@ -254,7 +254,7 @@ class GPSDialog(QtWidgets.QWidget): bottom_layout = QtWidgets.QVBoxLayout() bottom_layout.addLayout(button_layout) - self.info_text_browser.setFixedHeight(42) + self.info_text_browser.setFixedHeight(45) bottom_layout.addWidget(self.info_text_browser) main_layout.addLayout(bottom_layout) diff --git a/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py b/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py index 36d2bcde405a21b652770413cf67ab81b20efb03..97244a870fcdd70bee47f311e16419487d2dcc54 100755 --- a/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py +++ b/sohstationviewer/view/plotting/time_power_square/time_power_squared_dialog.py @@ -40,7 +40,6 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): """ self.date_format: str = 'YYYY-MM-DD' - self.setGeometry(50, 50, 1200, 800) self.setWindowTitle("TPS Plot") main_layout = QtWidgets.QVBoxLayout() diff --git a/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py b/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py index b82e7cb8fe591ff9ad18b4ad829308cb0c7fe491..9b475c7af53f20032d35c8be55351149f367796e 100644 --- a/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py +++ b/sohstationviewer/view/plotting/time_power_square/time_power_squared_widget.py @@ -231,7 +231,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget): ax = self.create_axes(self.plotting_bot, plot_h) ax.spines[['right', 'left', 'top', 'bottom']].set_visible(False) ax.text( - -0.12, 1, + -0.15, 1, f"{get_seismic_chan_label(chan_id)} {c_data['samplerate']}sps", horizontalalignment='left', verticalalignment='top', diff --git a/sohstationviewer/view/plotting/waveform_dialog.py b/sohstationviewer/view/plotting/waveform_dialog.py index 28ad89de67668eccf4bb78efc7c885c0f5381425..f6d8334504aa11c4e6a50ee26f3965984fe8bcba 100755 --- a/sohstationviewer/view/plotting/waveform_dialog.py +++ b/sohstationviewer/view/plotting/waveform_dialog.py @@ -77,7 +77,6 @@ class WaveformDialog(QtWidgets.QWidget): date_format: format for date """ self.date_format: str = 'YYYY-MM-DD' - self.setGeometry(50, 10, 1600, 700) self.setWindowTitle("Raw Data Plot") main_layout = QtWidgets.QVBoxLayout() diff --git a/sohstationviewer/view/ui/main_ui.py b/sohstationviewer/view/ui/main_ui.py index 02e92845edcc28646a4869b7181cb4fd96c5579c..c1e13a3003f4bca9c6f4977aa87da20a96d576ba 100755 --- a/sohstationviewer/view/ui/main_ui.py +++ b/sohstationviewer/view/ui/main_ui.py @@ -7,6 +7,7 @@ from PySide2.QtWidgets import ( QMainWindow, QWidget, QTextBrowser, QPushButton, QLineEdit, QDateEdit, QListWidget, QCheckBox, QRadioButton, QMenu, QLabel, QFrame, QVBoxLayout, QHBoxLayout, QGridLayout, QAbstractItemView, QButtonGroup, + QSplitter, ) from PySide2.QtWidgets import ( QAction, QActionGroup, QShortcut @@ -279,7 +280,6 @@ class UIMainWindow(object): :param main_window: QMainWindow - main GUI for user to interact with """ self.main_window = main_window - main_window.resize(1798, 1110) main_window.setWindowTitle("SOH Station Viewer") self.central_widget = QWidget(main_window) main_window.setCentralWidget(self.central_widget) @@ -294,8 +294,6 @@ class UIMainWindow(object): self.set_first_row(main_layout) self.set_second_row(main_layout) - self.tracking_info_text_browser.setFixedHeight(80) - main_layout.addWidget(self.tracking_info_text_browser) self.create_menu_bar(main_window) self.connect_signals(main_window) self.create_shortcuts(main_window) @@ -352,7 +350,7 @@ class UIMainWindow(object): left_widget = QWidget(self.central_widget) h_layout.addWidget(left_widget) left_widget.setFixedWidth(240) - left_widget.setMinimumHeight(650) + # left_widget.setMinimumHeight(650) left_layout = QVBoxLayout() left_layout.setContentsMargins(0, 0, 0, 0) left_layout.setSpacing(0) @@ -360,12 +358,22 @@ class UIMainWindow(object): self.set_control_column(left_layout) + plot_splitter = QSplitter(QtCore.Qt.Orientation.Vertical) + h_layout.addWidget(plot_splitter, 2) + self.plotting_widget = SOHWidget(self.main_window, self.tracking_info_text_browser, 'SOH', self.main_window) + plot_splitter.addWidget(self.plotting_widget) - h_layout.addWidget(self.plotting_widget, 2) + self.tracking_info_text_browser.setMinimumHeight(60) + self.tracking_info_text_browser.setMaximumHeight(80) + plot_splitter.addWidget(self.tracking_info_text_browser) + tracking_browser_idx = plot_splitter.indexOf( + self.tracking_info_text_browser + ) + plot_splitter.setCollapsible(tracking_browser_idx, False) def set_control_column(self, left_layout): """