diff --git a/pyqt/mainwindow.ui b/pyqt/mainwindow.ui index 11d9858cc6171d831d6f7b6693a34fcd928516f2..ea0e8e85cad6ee316fb9a20bd56906aeef3e6332 100644 --- a/pyqt/mainwindow.ui +++ b/pyqt/mainwindow.ui @@ -49,6 +49,9 @@ <height>201</height> </rect> </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectColumns</enum> + </property> <column> <property name="text"> <string>Instrument</string> @@ -155,12 +158,80 @@ </item> </layout> </widget> - <zorder>explo_graphicsview_top</zorder> - <zorder>explo_tablewidget_variables</zorder> - <zorder>horizontalLayoutWidget_2</zorder> - <zorder>explo_pushbutton_add_row</zorder> - <zorder>horizontalLayoutWidget_3</zorder> - <zorder>explo_checkbox_manualevent</zorder> + <widget class="QGroupBox" name="explo_groupbox_setups"> + <property name="geometry"> + <rect> + <x>950</x> + <y>10</y> + <width>391</width> + <height>241</height> + </rect> + </property> + <property name="title"> + <string>Save/load setups</string> + </property> + <widget class="QWidget" name="horizontalLayoutWidget_4"> + <property name="geometry"> + <rect> + <x>10</x> + <y>20</y> + <width>371</width> + <height>31</height> + </rect> + </property> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QLabel" name="explo_label_setup_name"> + <property name="text"> + <string>File name:</string> + </property> + </widget> + </item> + <item> + <widget class="QLineEdit" name="explo_lineedit_setup_name"/> + </item> + <item> + <widget class="QPushButton" name="explo_pushbutton_setup_save"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="text"> + <string>Save current setup</string> + </property> + </widget> + </item> + </layout> + </widget> + <widget class="QListWidget" name="explo_listwidget_setup_list"> + <property name="geometry"> + <rect> + <x>15</x> + <y>70</y> + <width>281</width> + <height>161</height> + </rect> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + </widget> + <widget class="QPushButton" name="explo_pushbutton_setup_load"> + <property name="enabled"> + <bool>false</bool> + </property> + <property name="geometry"> + <rect> + <x>301</x> + <y>70</y> + <width>81</width> + <height>23</height> + </rect> + </property> + <property name="text"> + <string>Load selected</string> + </property> + </widget> + </widget> </widget> <widget class="QWidget" name="tab_conductcalib"> <attribute name="title"> @@ -387,7 +458,7 @@ <x>0</x> <y>0</y> <width>1355</width> - <height>26</height> + <height>20</height> </rect> </property> <widget class="QMenu" name="menuInstrument"> diff --git a/src/dataprovider/exploprovider.py b/src/dataprovider/exploprovider.py index 17203a9087f1fed98d0aa66f3e8d542f3093f153..c8b4c096dfa484e1e912db0cf385f2b635758230 100644 --- a/src/dataprovider/exploprovider.py +++ b/src/dataprovider/exploprovider.py @@ -3,6 +3,8 @@ import datetime import os import re import xmltodict +import xml.etree.cElementTree as ET +from io import StringIO import utils from dataprovider.picarroprovider import PicarroProvider @@ -69,6 +71,10 @@ class Dataset: self.instlogs = {} self.manual_event_log = None + # Setup save/load + self.saved_setup_dir = self.full_directory_name + "/saved_setups/" + self.saved_setup_ext = ".xml" + self.explore_dataset() def explore_dataset(self) -> None: @@ -123,6 +129,80 @@ class Dataset: picarro_log = InstrumentPeriodicLog(self.full_directory_name, picarro_filename, "PICARRO") self.instlogs["PICARRO_periodic"] = picarro_log + def save_setup(self, setup_name: str, variable_df: pd.DataFrame, view_range: list) -> None: + # Build 'saved setup' full file name + if not os.path.exists(self.saved_setup_dir): + os.mkdir(self.saved_setup_dir) + filename = self.saved_setup_dir + setup_name + self.saved_setup_ext + + # Variables table + variables_str = variable_df.to_csv(sep=";", + index=False, + mode='w') + + # Create XML file + root_elmt = ET.Element("save") + ET.SubElement(root_elmt, "variables").text = variables_str + view_range_elmt = ET.SubElement(root_elmt, "view_range") + ET.SubElement(view_range_elmt, "xmin").text = "{:.2f}".format(view_range[0][0]) + ET.SubElement(view_range_elmt, "xmax").text = "{:.2f}".format(view_range[0][1]) + ET.SubElement(view_range_elmt, "ymin").text = "{:.4f}".format(view_range[1][0]) + ET.SubElement(view_range_elmt, "ymax").text = "{:.4f}".format(view_range[1][1]) + + tree = ET.ElementTree(root_elmt) + tree.write(filename) + + def load_setup(self, filename: str) -> tuple: + full_filename = self.saved_setup_dir + filename + self.saved_setup_ext + + # Open XML file + tree = ET.parse(full_filename) + root = tree.getroot() + + # Variable CSV table as pd.Dataframe + variables_str = root.findall("variables")[0].text + variable_io = StringIO(variables_str) + variable_df = pd.read_csv(variable_io, sep=";") + + # View range + view_range_elmt = root.findall("view_range")[0] + view_range_dict = {"xmin": float(view_range_elmt.findall("xmin")[0].text), + "xmax": float(view_range_elmt.findall("xmax")[0].text), + "ymin": float(view_range_elmt.findall("ymin")[0].text), + "ymax": float(view_range_elmt.findall("ymax")[0].text)} + + return variable_df, view_range_dict + + def setup_filename_is_valid(self, filename: str) -> tuple: + """Check if the file name is valid: no special characters, file does not already exists. + + Parameters + ---------- + filename: str + filename (without extension) to be tested. + + Returns + ------- + bool: + True if the file name is valid, False otherwise + str: + The error message explaining why the file name is not valid ; an empty string if file name is valid. + """ + if not re.match("^[A-Za-z0-9_-]*$", filename): + error_msg = "File name can only contain letters, digits and '-' or '_'. File extension is automatically set." + return False, error_msg + elif filename in self.get_setup_saved_files(): + error_msg = "File already exists." + return False, error_msg + else: + return True, "" + + def get_setup_saved_files(self) -> list: + """Get a list of the 'setup' file names (without extension) existing in the 'saved_setups' directory.""" + filenames = os.listdir(self.saved_setup_dir) + files_without_ext = [os.path.splitext(filename)[0] for filename in filenames] + return files_without_ext + class InstrumentLog: diff --git a/src/gui/uimainwindow.py b/src/gui/uimainwindow.py index f050c65c63f203653170308e5ce718f558f81701..2322e2334cdc65eaa500e40002ed28867c795206 100644 --- a/src/gui/uimainwindow.py +++ b/src/gui/uimainwindow.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file '../pyqt/mainwindow.ui' +# Form implementation generated from reading ui file '..\pyqt\mainwindow.ui' # # Created by: PyQt5 UI code generator 5.11.3 # @@ -24,6 +24,7 @@ class Ui_MainWindow(object): self.explo_graphicsview_top.setObjectName("explo_graphicsview_top") self.explo_tablewidget_variables = QtWidgets.QTableWidget(self.tab_explo) self.explo_tablewidget_variables.setGeometry(QtCore.QRect(20, 50, 921, 201)) + self.explo_tablewidget_variables.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectColumns) self.explo_tablewidget_variables.setObjectName("explo_tablewidget_variables") self.explo_tablewidget_variables.setColumnCount(9) self.explo_tablewidget_variables.setRowCount(0) @@ -70,12 +71,33 @@ class Ui_MainWindow(object): self.explo_checkbox_manualevent = QtWidgets.QCheckBox(self.horizontalLayoutWidget_3) self.explo_checkbox_manualevent.setObjectName("explo_checkbox_manualevent") self.horizontalLayout_3.addWidget(self.explo_checkbox_manualevent) - self.explo_graphicsview_top.raise_() - self.explo_tablewidget_variables.raise_() - self.horizontalLayoutWidget_2.raise_() - self.explo_pushbutton_add_row.raise_() - self.horizontalLayoutWidget_3.raise_() - self.explo_checkbox_manualevent.raise_() + self.explo_groupbox_setups = QtWidgets.QGroupBox(self.tab_explo) + self.explo_groupbox_setups.setGeometry(QtCore.QRect(950, 10, 391, 241)) + self.explo_groupbox_setups.setObjectName("explo_groupbox_setups") + self.horizontalLayoutWidget_4 = QtWidgets.QWidget(self.explo_groupbox_setups) + self.horizontalLayoutWidget_4.setGeometry(QtCore.QRect(10, 20, 371, 31)) + self.horizontalLayoutWidget_4.setObjectName("horizontalLayoutWidget_4") + self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.horizontalLayoutWidget_4) + self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.explo_label_setup_name = QtWidgets.QLabel(self.horizontalLayoutWidget_4) + self.explo_label_setup_name.setObjectName("explo_label_setup_name") + self.horizontalLayout_4.addWidget(self.explo_label_setup_name) + self.explo_lineedit_setup_name = QtWidgets.QLineEdit(self.horizontalLayoutWidget_4) + self.explo_lineedit_setup_name.setObjectName("explo_lineedit_setup_name") + self.horizontalLayout_4.addWidget(self.explo_lineedit_setup_name) + self.explo_pushbutton_setup_save = QtWidgets.QPushButton(self.horizontalLayoutWidget_4) + self.explo_pushbutton_setup_save.setEnabled(False) + self.explo_pushbutton_setup_save.setObjectName("explo_pushbutton_setup_save") + self.horizontalLayout_4.addWidget(self.explo_pushbutton_setup_save) + self.explo_listwidget_setup_list = QtWidgets.QListWidget(self.explo_groupbox_setups) + self.explo_listwidget_setup_list.setGeometry(QtCore.QRect(15, 70, 281, 161)) + self.explo_listwidget_setup_list.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + self.explo_listwidget_setup_list.setObjectName("explo_listwidget_setup_list") + self.explo_pushbutton_setup_load = QtWidgets.QPushButton(self.explo_groupbox_setups) + self.explo_pushbutton_setup_load.setEnabled(False) + self.explo_pushbutton_setup_load.setGeometry(QtCore.QRect(301, 70, 81, 23)) + self.explo_pushbutton_setup_load.setObjectName("explo_pushbutton_setup_load") self.tabWidget.addTab(self.tab_explo, "") self.tab_conductcalib = QtWidgets.QWidget() self.tab_conductcalib.setObjectName("tab_conductcalib") @@ -155,7 +177,7 @@ class Ui_MainWindow(object): self.tabWidget.addTab(self.tab_conductcalib, "") MainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(MainWindow) - self.menubar.setGeometry(QtCore.QRect(0, 0, 1355, 26)) + self.menubar.setGeometry(QtCore.QRect(0, 0, 1355, 20)) self.menubar.setObjectName("menubar") self.menuInstrument = QtWidgets.QMenu(self.menubar) self.menuInstrument.setObjectName("menuInstrument") @@ -212,6 +234,10 @@ class Ui_MainWindow(object): self.explo_label_dataset.setText(_translate("MainWindow", "Dataset:")) self.explo_pushbutton_add_row.setText(_translate("MainWindow", "+ Add Row")) self.explo_checkbox_manualevent.setText(_translate("MainWindow", "Display manual events")) + self.explo_groupbox_setups.setTitle(_translate("MainWindow", "Save/load setups")) + self.explo_label_setup_name.setText(_translate("MainWindow", "File name:")) + self.explo_pushbutton_setup_save.setText(_translate("MainWindow", "Save current setup")) + self.explo_pushbutton_setup_load.setText(_translate("MainWindow", "Load selected")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_explo), _translate("MainWindow", "Data exploration")) item = self.conduct_tablewidget_set.horizontalHeaderItem(0) item.setText(_translate("MainWindow", "Date")) diff --git a/src/uim/explouim.py b/src/uim/explouim.py index 1125dbd1330ecea49c4f78eacd6c042740bb350a..e3a9167912cfce731a7e8ec1980b6dab91e48c61 100644 --- a/src/uim/explouim.py +++ b/src/uim/explouim.py @@ -1,4 +1,5 @@ import datetime +import re import pyqtgraph as pg from PyQt5.QtWidgets import * from PyQt5.QtGui import QColor @@ -57,6 +58,14 @@ class ExploUim: self.main_ui.explo_checkbox_manualevent.stateChanged.connect(self.__update_manual_event__) + # Save/load current setup + self.main_ui.explo_lineedit_setup_name.textChanged.connect(self.__check_setup_save_name__) + self.main_ui.explo_pushbutton_setup_save.clicked.connect(self.__save_setup__) + self.__refresh_existing_setups__() + self.main_ui.explo_listwidget_setup_list.itemClicked.connect( + lambda: self.main_ui.explo_pushbutton_setup_load.setEnabled(True)) + self.main_ui.explo_pushbutton_setup_load.clicked.connect(self.__load_setup__) + def __initialize_dataset_combobox__(self): """Populate the "datasets" combobox with the existing dataset directory names.""" data_root_dir = self.config.read("DATA_SOURCE", "absolute_root_dir") @@ -68,6 +77,7 @@ class ExploUim: self.__update_current_dataset__(dataset_dirs[0]) def __update_current_dataset__(self, dataset_dir: str, ask_confirmation: bool = True): + # Clear table and plot self.main_ui.explo_tablewidget_variables.setRowCount(0) self.__initialize_plot__() @@ -79,6 +89,10 @@ class ExploUim: self.main_ui.explo_checkbox_manualevent.setEnabled(self.current_dataset.manual_event_log is not None) self.main_ui.explo_checkbox_manualevent.setChecked(False) + # Update 'existing setups' + self.__refresh_existing_setups__() + self.main_ui.explo_pushbutton_setup_load.setEnabled(False) + #################################################################################################################### # "Variables" table @@ -162,6 +176,9 @@ class ExploUim: self.__update_instruments_combobox__(row_id) + if hasattr(self, "plot_item"): + self.plot_item.getViewBox().enableAutoRange(enable=True) + def __update_instruments_combobox__(self, row_id: int): instrument_combobox = self.main_ui.explo_tablewidget_variables.cellWidget(row_id, self.INSTRUMENT_COL) instrument_combobox.clear() @@ -232,10 +249,6 @@ class ExploUim: # Get variable visibility visible = table.cellWidget(row_id, self.VISIBLE_COL).isChecked() - if variable_name == "event": - self.__update_manual_event__(timeseries, row_id, color, visible) - return - # Get position adjustment variables (offset, multiplicative factor and time shift) offset = table.cellWidget(row_id, self.OFFSET_COL).value() mult = table.cellWidget(row_id, self.MULT_COL).value() @@ -397,4 +410,102 @@ class ExploUim: """ # if self.step_curves[0].sceneBoundingRect().contains(pos): mouse_point = self.step_curves[0].getViewBox().mapSceneToView(pos) - self.cursor_vline.setPos(mouse_point.x()) \ No newline at end of file + self.cursor_vline.setPos(mouse_point.x()) + + #################################################################################################################### + # Save/load current setup + + def __check_setup_save_name__(self, filename: str): + valid, error_msg = self.current_dataset.setup_filename_is_valid(filename) + if not valid: + self.main_ui.statusbar.showMessage(error_msg, 5000) + self.main_ui.explo_lineedit_setup_name.setStyleSheet("color: 'red';") + self.main_ui.explo_pushbutton_setup_save.setEnabled(False) + else: + self.main_ui.explo_pushbutton_setup_save.setEnabled(True) + self.main_ui.explo_lineedit_setup_name.setStyleSheet("color: 'black';") + + def __save_setup__(self): + # Dataset + dataset = self.current_dataset + + # File name + filename = self.main_ui.explo_lineedit_setup_name.text() + + # Variables + variable_df = pd.DataFrame() + table = self.main_ui.explo_tablewidget_variables + for row_id in range(table.rowCount()): + row_dict = {"instrument": table.cellWidget(row_id, self.INSTRUMENT_COL).currentText(), + "variable": table.cellWidget(row_id, self.VARIABLE_COL).currentText(), + "color": table.item(row_id, self.COLOR_COL).background().color().name(), + "offset": table.cellWidget(row_id, self.OFFSET_COL).value(), + "mult": table.cellWidget(row_id, self.MULT_COL).value(), + "timeshift": table.cellWidget(row_id, self.TIMESHIFT_COL).value(), + "visible": table.cellWidget(row_id, self.VISIBLE_COL).checkState() == 2} + variable_df = variable_df.append(row_dict, ignore_index=True) + + # View range + view_range = self.plot_item.getViewBox().viewRange() + + dataset.save_setup(filename, variable_df, view_range) + self.__refresh_existing_setups__() + + # Reset filename input widgets + self.main_ui.explo_lineedit_setup_name.setText(None) + self.main_ui.explo_pushbutton_setup_save.setEnabled(False) + + def __load_setup__(self): + # Clear table and plot + self.main_ui.explo_tablewidget_variables.setRowCount(0) + self.__initialize_plot__() + + # Get a dataframe containing the variables data + filename = self.main_ui.explo_listwidget_setup_list.selectedItems()[0].text() + variable_df, view_range_dict = self.current_dataset.load_setup(filename) + + # Variables: table (and automatically: plot) + table = self.main_ui.explo_tablewidget_variables + for row_id, row in variable_df.iterrows(): + self.__add_new_row_in_variable_table__() + + # Instrument + instrument_index = table.cellWidget(row_id, self.INSTRUMENT_COL).findText(row["instrument"]) + table.cellWidget(row_id, self.INSTRUMENT_COL).setCurrentIndex(instrument_index) + + # Variable + variable_index = table.cellWidget(row_id, self.VARIABLE_COL).findText(row["variable"]) + table.cellWidget(row_id, self.VARIABLE_COL).setCurrentIndex(variable_index) + + # Color + color = QColor(row["color"]) + table.item(row_id, self.COLOR_COL).setBackground(color) + + # Offset + table.cellWidget(row_id, self.OFFSET_COL).setValue(row["offset"]) + + # Mult + table.cellWidget(row_id, self.MULT_COL).setValue(row["mult"]) + + # Timeshift + table.cellWidget(row_id, self.TIMESHIFT_COL).setValue(row["timeshift"]) + + # Visible + table.cellWidget(row_id, self.VISIBLE_COL).setChecked(row["visible"]) + + self.__apply_variable_change__(row_id) # Useful only for color change + + # Plot view range + self.plot_item.getViewBox().enableAutoRange(enable=False) + # self.plot_item.getViewBox().setYRange(view_range_dict["ymin"], view_range_dict["ymax"], padding=0) + # self.plot_item.getViewBox().setXRange(view_range_dict["xmin"], view_range_dict["xmax"], padding=0) + + self.plot_item.getViewBox().setRange(xRange=(view_range_dict["xmin"], view_range_dict["xmax"]), + yRange=(view_range_dict["ymin"], view_range_dict["ymax"]), + padding=0) + + def __refresh_existing_setups__(self): + files = self.current_dataset.get_setup_saved_files() + self.main_ui.explo_listwidget_setup_list.clear() + for file in files: + self.main_ui.explo_listwidget_setup_list.addItem(file)