Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • software_public/passoft/sohstationviewer
1 result
Show changes
Commits on Source (18)
Showing
with 372 additions and 105 deletions
...@@ -7,6 +7,7 @@ ROOT_PATH = Path(os.path.abspath(__file__)).parent.parent ...@@ -7,6 +7,7 @@ ROOT_PATH = Path(os.path.abspath(__file__)).parent.parent
# The current version of SOHStationViewer # The current version of SOHStationViewer
SOFTWARE_VERSION = '2024.2.1.0' SOFTWARE_VERSION = '2024.2.1.0'
BUILD_TIME = "March 13, 2024"
# waveform pattern # waveform pattern
WF_1ST = 'A-HLM-V' WF_1ST = 'A-HLM-V'
......
...@@ -245,23 +245,6 @@ def add_thousand_separator(value: float) -> str: ...@@ -245,23 +245,6 @@ def add_thousand_separator(value: float) -> str:
return new_value return new_value
def apply_convert_factor(c_data: dict, convert_factor: float):
"""
convertFactor = 150mV/count = 150V/1000count
=> unit data * convertFactor= data *150/1000 V
:param c_data: data of the channel which includes down-sampled
data in keys 'times' and 'data'.
Refer to data_dict in data_structures.MD
:param convert_factor: convertFactor field retrieved from
db table Channels for this channel
"""
if c_data['needConvert']:
c_data['needConvert'] = False
if convert_factor is not None and convert_factor != 1:
c_data['data'] = [d * convert_factor for d in c_data['data']]
def is_hex(text: str) -> bool: def is_hex(text: str) -> bool:
""" """
Check if text is hexadecimal Check if text is hexadecimal
......
No preview for this file type
# SOH Station Viewer Documentation # SOHViewer Documentation
Welcome to the SOH Station Viewer documentation. Here you will find usage guides and other useful information in navigating and using this software. Welcome to the SOHViewer documentation. Here you will find usage guides and other useful information in navigating and using this software.
On the left-hand side you will find a list of currently available help topics. On the left-hand side you will find a list of currently available help topics.
......
...@@ -12,6 +12,7 @@ from sohstationviewer.conf.config_processor import ( ...@@ -12,6 +12,7 @@ from sohstationviewer.conf.config_processor import (
ConfigProcessor, ConfigProcessor,
BadConfigError, BadConfigError,
) )
from sohstationviewer.conf import constants
def fix_relative_paths() -> None: def fix_relative_paths() -> None:
...@@ -73,7 +74,7 @@ def check_if_user_want_to_reset_config() -> bool: ...@@ -73,7 +74,7 @@ def check_if_user_want_to_reset_config() -> bool:
def main(): def main():
# Change the working directory so that relative paths work correctly. # Change the working directory so that relative paths work correctly.
fix_relative_paths() fix_relative_paths()
print(f"SOHViewer - Version {constants.SOFTWARE_VERSION}")
app = QtWidgets.QApplication(sys.argv) app = QtWidgets.QApplication(sys.argv)
wnd = MainWindow() wnd = MainWindow()
......
...@@ -26,7 +26,7 @@ Note: log_data for RT130's dataset has only one channel: SOH ...@@ -26,7 +26,7 @@ Note: log_data for RT130's dataset has only one channel: SOH
'chan_db_info' (dict): the plotting parameters got from database 'chan_db_info' (dict): the plotting parameters got from database
for this channel - dict, for this channel - dict,
'ax': axes to draw the channel in PlottingWidget 'ax': axes to draw the channel in PlottingWidget
'ax_wf' (matplotlib.axes.Axes): axes to draw the channel in WaveformWidget 'ax_wf': axes to draw the channel in WaveformWidget
'visible': flag to show or hide channel 'visible': flag to show or hide channel
} }
} }
......
from __future__ import annotations from __future__ import annotations
from typing import Optional, List from typing import List
""" """
Routines building upon obspy.io.reftek.packet. Routines building upon obspy.io.reftek.packet.
...@@ -302,9 +302,7 @@ class SOHPacket(obspy_rt130_packet.Packet): ...@@ -302,9 +302,7 @@ class SOHPacket(obspy_rt130_packet.Packet):
raise NotImplementedError(msg.format(packet_type)) raise NotImplementedError(msg.format(packet_type))
@staticmethod @staticmethod
def time_tag(time: UTCDateTime, implement_time: Optional[int] = None): def time_tag(time: UTCDateTime):
if implement_time is not None and time > UTCDateTime(ns=implement_time): # noqa: E501
time = UTCDateTime(ns=implement_time)
return "{:04d}:{:03d}:{:02d}:{:02d}:{:02d}:{:03d}".format(time.year, return "{:04d}:{:03d}:{:02d}:{:02d}:{:02d}:{:03d}".format(time.year,
time.julday, time.julday,
time.hour, time.hour,
...@@ -391,6 +389,10 @@ class SCPacket(SOHPacket): ...@@ -391,6 +389,10 @@ class SCPacket(SOHPacket):
.format(self.time_tag(self.time), .format(self.time_tag(self.time),
self.unit_id.decode())) self.unit_id.decode()))
info.append(packet_soh_string) info.append(packet_soh_string)
implement_time_tag = self.time_tag(
UTCDateTime(self.implement_time / 10 ** 9)
)
info.append("\n Implemented = " + implement_time_tag)
info.append("\n Experiment Number = " + self.experiment_number_sc) info.append("\n Experiment Number = " + self.experiment_number_sc)
info.append("\n Experiment Name = " + self.experiment_name) info.append("\n Experiment Name = " + self.experiment_name)
info.append("\n Comments - " + self.experiment_comment) info.append("\n Comments - " + self.experiment_comment)
...@@ -477,9 +479,13 @@ class OMPacket(SOHPacket): ...@@ -477,9 +479,13 @@ class OMPacket(SOHPacket):
info = [] info = []
# info.append(self.packet_tagline) # info.append(self.packet_tagline)
packet_soh_string = ("\nOperating Mode Definition {:s} ST: {:s}" packet_soh_string = ("\nOperating Mode Definition {:s} ST: {:s}"
.format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 .format(self.time_tag(self.time), # noqa: E501
self.unit_id.decode())) self.unit_id.decode()))
info.append(packet_soh_string) info.append(packet_soh_string)
implement_time_tag = self.time_tag(
UTCDateTime(self.implement_time / 10 ** 9)
)
info.append("\n Implemented = " + implement_time_tag)
info.append("\n Operating Mode 72A Power State " + self._72A_power_state) # noqa: E501 info.append("\n Operating Mode 72A Power State " + self._72A_power_state) # noqa: E501
info.append("\n Operating Mode Recording Mode " + self.recording_mode) info.append("\n Operating Mode Recording Mode " + self.recording_mode)
info.append("\n Operating Mode Auto Dump on ET " + self.auto_dump_on_ET) # noqa: E501 info.append("\n Operating Mode Auto Dump on ET " + self.auto_dump_on_ET) # noqa: E501
...@@ -557,9 +563,13 @@ class DSPacket(SOHPacket): ...@@ -557,9 +563,13 @@ class DSPacket(SOHPacket):
info = [] info = []
info.append(self.packet_tagline) info.append(self.packet_tagline)
packet_soh_string = ("\nData Stream Definition {:s} ST: {:s}" packet_soh_string = ("\nData Stream Definition {:s} ST: {:s}"
.format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 .format(self.time_tag(self.time), # noqa: E501
self.unit_id.decode())) self.unit_id.decode()))
info.append(packet_soh_string) info.append(packet_soh_string)
implement_time_tag = self.time_tag(
UTCDateTime(self.implement_time / 10 ** 9)
)
info.append("\n Implemented = " + implement_time_tag)
for ind_ds in range(1, DS_MAX_NBR_ST + 1): for ind_ds in range(1, DS_MAX_NBR_ST + 1):
stream_number = getattr(self, "ds" + str(ind_ds) + "_number") stream_number = getattr(self, "ds" + str(ind_ds) + "_number")
if stream_number.strip(): if stream_number.strip():
...@@ -640,9 +650,13 @@ class ADPacket(SOHPacket): ...@@ -640,9 +650,13 @@ class ADPacket(SOHPacket):
info = [] info = []
# info.append(self.packet_tagline) # info.append(self.packet_tagline)
packet_soh_string = ("\nAuxiliary Data Parameter {:s} ST: {:s}" packet_soh_string = ("\nAuxiliary Data Parameter {:s} ST: {:s}"
.format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 .format(self.time_tag(self.time), # noqa: E501
self.unit_id.decode())) self.unit_id.decode()))
info.append(packet_soh_string) info.append(packet_soh_string)
implement_time_tag = self.time_tag(
UTCDateTime(self.implement_time / 10 ** 9)
)
info.append("\n Implemented = " + implement_time_tag)
channel_nbr = [str(ind_chan) for ind_chan, val in channel_nbr = [str(ind_chan) for ind_chan, val in
enumerate(self.channels, 1) if val.strip()] enumerate(self.channels, 1) if val.strip()]
info.append("\n Channels " + ", ".join(channel_nbr)) info.append("\n Channels " + ", ".join(channel_nbr))
...@@ -732,9 +746,13 @@ class CDPacket(SOHPacket): ...@@ -732,9 +746,13 @@ class CDPacket(SOHPacket):
info = [] info = []
# info.append(self.packet_tagline) # info.append(self.packet_tagline)
packet_soh_string = ("\nCalibration Definition {:s} ST: {:s}" packet_soh_string = ("\nCalibration Definition {:s} ST: {:s}"
.format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 .format(self.time_tag(self.time), # noqa: E501
self.unit_id.decode())) self.unit_id.decode()))
info.append(packet_soh_string) info.append(packet_soh_string)
implement_time_tag = self.time_tag(
UTCDateTime(self.implement_time / 10 ** 9)
)
info.append("\n Implemented = " + implement_time_tag)
if self._72A_start_time.split(): if self._72A_start_time.split():
info.append("\n 72A Calibration Start Time " + self._72A_start_time) # noqa: E501 info.append("\n 72A Calibration Start Time " + self._72A_start_time) # noqa: E501
...@@ -855,9 +873,13 @@ class FDPacket(SOHPacket): ...@@ -855,9 +873,13 @@ class FDPacket(SOHPacket):
info = [] info = []
# info.append(self.packet_tagline) # info.append(self.packet_tagline)
packet_soh_string = ("\nFilter Description {:s} ST: {:s}" packet_soh_string = ("\nFilter Description {:s} ST: {:s}"
.format(self.time_tag(self.time, implement_time=self.implement_time), # noqa: E501 .format(self.time_tag(self.time), # noqa: E501
self.unit_id.decode())) self.unit_id.decode()))
info.append(packet_soh_string) info.append(packet_soh_string)
implement_time_tag = self.time_tag(
UTCDateTime(self.implement_time / 10 ** 9)
)
info.append("\n Implemented = " + implement_time_tag)
for ind_fb in range(1, self.nbr_fbs + 1): for ind_fb in range(1, self.nbr_fbs + 1):
info.append("\n Filter Block Count " + str(getattr(self, "fb" + str(ind_fb) + "_filter_block_count"))) # noqa: E501 info.append("\n Filter Block Count " + str(getattr(self, "fb" + str(ind_fb) + "_filter_block_count"))) # noqa: E501
info.append("\n Filter ID " + getattr(self, "fb" + str(ind_fb) + "_filter_ID")) # noqa: E501 info.append("\n Filter ID " + getattr(self, "fb" + str(ind_fb) + "_filter_ID")) # noqa: E501
......
import sys
from typing import Union
from PySide6 import QtWidgets, QtCore
from PySide6.QtWidgets import QApplication, QWidget, QDialog, QLabel, QFrame
from sohstationviewer.conf import constants
def add_separation_line(layout):
"""
Add a line for separation to the given layout.
:param layout: QLayout - the layout that contains the line
"""
label = QLabel()
label.setFrameStyle(QFrame.Shape.HLine | QFrame.Shadow.Sunken)
label.setLineWidth(1)
layout.addWidget(label)
class AboutDialog(QDialog):
"""
Dialog to show information of the software.
About Dialog is always opened and will be raised up when the about menu
action is triggered.
"""
def __init__(self, parent: Union[QWidget, QApplication]):
"""
:param parent: the parent widget
"""
super(AboutDialog, self).__init__(parent)
# set block interaction with other windows
self.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
self.software_name_label = QLabel("SOHViewer")
self.software_name_label.setStyleSheet(
"QLabel {font-size: 18pt; font-style: bold; color: darkblue}"
)
description = ("Visualize State-of-Health packets from data in "
"mseed or reftek formats recorded\n"
"by different types of data loggers.")
self.description_label = QLabel(description)
version = f"Version {constants.SOFTWARE_VERSION}"
self.version_label = QLabel(version)
built_time = f"Built on {constants.BUILD_TIME}"
self.built_time_label = QLabel(built_time)
copyright = u"Copyright \u00A9 2024 EarthScope Consortium"
self.copyright_label = QLabel(copyright)
self.ok_button = QtWidgets.QPushButton('OK', self)
self.setup_ui()
self.connect_signals()
def setup_ui(self):
self.setWindowTitle("About SOHViewer")
main_layout = QtWidgets.QVBoxLayout()
self.setLayout(main_layout)
main_layout.addWidget(self.software_name_label)
main_layout.addWidget(self.description_label)
add_separation_line(main_layout)
main_layout.addWidget(self.version_label)
main_layout.addWidget(self.built_time_label)
main_layout.addWidget(self.copyright_label)
button_layout = QtWidgets.QHBoxLayout()
main_layout.addLayout(button_layout)
button_layout.addStretch()
button_layout.addWidget(self.ok_button)
def connect_signals(self) -> None:
self.ok_button.clicked.connect(self.close)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
test = AboutDialog(None)
test.exec()
sys.exit(app.exec())
from typing import Dict, List, Union, Optional, Tuple from typing import Dict, List, Union, Optional, Tuple
from pathlib import Path from pathlib import Path
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore, QtGui
from PySide6.QtWidgets import QDialogButtonBox, QDialog, QPlainTextEdit, \ from PySide6.QtWidgets import QDialogButtonBox, QDialog, QPlainTextEdit, \
QMainWindow QMainWindow
...@@ -328,12 +328,51 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -328,12 +328,51 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
count, COL['dataType']).setCurrentText(r['dataType']) count, COL['dataType']).setCurrentText(r['dataType'])
self.soh_list_table_widget.item( self.soh_list_table_widget.item(
count, COL['preferredSOHs']).setText(r['preferredSOHs']) count, COL['preferredSOHs']).setText(r['preferredSOHs'])
if r['default'] == 1:
self.set_default_row(count)
if r['current'] == 1: if r['current'] == 1:
self.curr_sel_changed(count) self.curr_sel_changed(count)
self.soh_list_table_widget.selectRow(count) self.soh_list_table_widget.selectRow(count)
count += 1 count += 1
self.update() self.update()
def set_default_row(self, row_idx):
"""
Set row at row_idx to default in which,
+ Edit and clear button are disabled and greyed out.
+ All other widgets except for the select radio button are disabled
but not greyed out.
"""
self.soh_list_table_widget.item(
row_idx, COL['name']).setFlags(
QtCore.Qt.ItemFlag.ItemIsSelectable |
QtCore.Qt.ItemFlag.ItemIsEnabled)
data_type_widget = self.soh_list_table_widget.cellWidget(
row_idx, COL['dataType'])
data_type_widget.setEnabled(False)
# Only want it to be unchangeable but still look active so
# the text color is changed to black instead of being grey as default
# for disable column box.
palette = data_type_widget.palette()
palette.setColor(QtGui.QPalette.ColorRole.Text, 'black')
palette.setColor(QtGui.QPalette.ColorRole.ButtonText, 'black')
data_type_widget.setPalette(palette)
self.soh_list_table_widget.item(
row_idx, COL['preferredSOHs']).setFlags(
QtCore.Qt.ItemFlag.ItemIsSelectable |
QtCore.Qt.ItemFlag.ItemIsEnabled)
# Buttons Edit and Clear are disable showing that this row is a default
# row and can't be editable.
self.soh_list_table_widget.cellWidget(
row_idx, COL['edit']).setEnabled(False)
self.soh_list_table_widget.cellWidget(
row_idx, COL['clr']).setEnabled(False)
def get_row(self, row_idx): def get_row(self, row_idx):
""" """
Get content of a self.soh_list_table_widget's row Get content of a self.soh_list_table_widget's row
...@@ -744,6 +783,6 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -744,6 +783,6 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
:return id_rows: [dict,] - list of data for each row :return id_rows: [dict,] - list of data for each row
""" """
id_rows = execute_db_dict( id_rows = execute_db_dict(
"SELECT name, preferredSOHs, dataType, current FROM ChannelPrefer " "SELECT * FROM ChannelPrefer "
" ORDER BY name ASC") " ORDER BY name ASC")
return id_rows return id_rows
...@@ -242,8 +242,7 @@ class AddEditSingleChannelDialog(QDialog): ...@@ -242,8 +242,7 @@ class AddEditSingleChannelDialog(QDialog):
) )
PlottingAxes.clean_axes(self.ax) PlottingAxes.clean_axes(self.ax)
self.plotting.plot_channel(self.ax.c_data, self.plotting.plot_channel(self.ax.c_data,
self.channel_name_lnedit.text(), self.channel_name_lnedit.text())
self.ax)
self.close() self.close()
def set_buttons_enabled(self): def set_buttons_enabled(self):
......
import getpass
import os
from PySide6 import QtCore, QtWidgets
UrlRole = QtCore.Qt.UserRole + 1
EnabledRole = QtCore.Qt.UserRole + 2
EXT_DRIVE_BASE = ['/media', '/run/media']
def get_external_drive_url():
"""
External drives of Linux machine can be located under:
/media/<username>
OR /run/media/<username>
return url_dict: dict with key is a url of an external drive's path and
value is the drive's name.
"""
url_dict = {}
for base in EXT_DRIVE_BASE:
if not os.path.isdir(base):
continue
username = getpass.getuser()
external_drive_root = os.path.join(base, username)
if not os.path.isdir(external_drive_root):
continue
for d in os.listdir(external_drive_root):
full_path = os.path.join(os.path.join(external_drive_root, d))
if not os.path.isdir(full_path):
continue
url_dict[QtCore.QUrl.fromLocalFile(full_path)] = d
return url_dict
class URLShortcutTextDelegate(QtWidgets.QStyledItemDelegate):
"""
Help with displaying shortcut text for each drive.
https://stackoverflow.com/questions/69284292/is-there-an-option-to-rename-a-qurl-shortcut-in-a-sidebar-of-a-qfiledialog # noqa: E501
"""
mapping = dict()
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
url = index.data(UrlRole)
text = self.mapping.get(url)
if isinstance(text, str):
option.text = text
is_enabled = index.data(EnabledRole)
if is_enabled is not None and not is_enabled:
option.state &= ~QtWidgets.QStyle.State_Enabled
class FileDialogForLinuxExternalDrive(QtWidgets.QFileDialog):
"""
File Dialog that show external drives of linux (Ubuntu, Fedora) on sidebar
"""
def __init__(self, parent, curr_dir):
url_dict = get_external_drive_url()
# only use customized sidebar if len(url_dict)>0
options = (QtWidgets.QFileDialog.Option.DontUseNativeDialog if url_dict
else QtWidgets.QFileDialog.Option.ShowDirsOnly)
super().__init__(parent, caption="Select Main Data Directory",
options=options,
fileMode=QtWidgets.QFileDialog.FileMode.Directory)
if url_dict:
self.setSidebarUrls(self.sidebarUrls() + list(url_dict.keys()))
sidebar = self.findChild(QtWidgets.QListView, "sidebar")
# fit sidebar with content
sidebar.setMinimumWidth(sidebar.sizeHintForColumn(0))
delegate = URLShortcutTextDelegate(sidebar)
delegate.mapping = url_dict
sidebar.setItemDelegate(delegate)
self.setDirectory(curr_dir)
...@@ -15,6 +15,7 @@ from sohstationviewer.model.data_loader import DataLoader ...@@ -15,6 +15,7 @@ from sohstationviewer.model.data_loader import DataLoader
from sohstationviewer.model.general_data.general_data import \ from sohstationviewer.model.general_data.general_data import \
GeneralData GeneralData
from sohstationviewer.view.about_dialog import AboutDialog
from sohstationviewer.view.calendar.calendar_dialog import CalendarDialog from sohstationviewer.view.calendar.calendar_dialog import CalendarDialog
from sohstationviewer.view.db_config.channel_dialog import ChannelDialog from sohstationviewer.view.db_config.channel_dialog import ChannelDialog
from sohstationviewer.view.db_config.data_type_dialog import DataTypeDialog from sohstationviewer.view.db_config.data_type_dialog import DataTypeDialog
...@@ -22,7 +23,9 @@ from sohstationviewer.view.db_config.param_dialog import ParamDialog ...@@ -22,7 +23,9 @@ from sohstationviewer.view.db_config.param_dialog import ParamDialog
from sohstationviewer.view.db_config.plot_type_dialog import PlotTypeDialog from sohstationviewer.view.db_config.plot_type_dialog import PlotTypeDialog
from sohstationviewer.view.file_information.get_file_information import \ from sohstationviewer.view.file_information.get_file_information import \
extract_data_set_info extract_data_set_info
from sohstationviewer.view.file_list_widget import FileListItem from sohstationviewer.view.file_list.file_dialog_for_linux_external_drive \
import FileDialogForLinuxExternalDrive
from sohstationviewer.view.file_list.file_list_widget import FileListItem
from sohstationviewer.view.plotting.gps_plot.extract_gps_data import \ from sohstationviewer.view.plotting.gps_plot.extract_gps_data import \
extract_gps_data extract_gps_data
from sohstationviewer.view.plotting.gps_plot.gps_dialog import GPSDialog from sohstationviewer.view.plotting.gps_plot.gps_dialog import GPSDialog
...@@ -192,6 +195,10 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -192,6 +195,10 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
""" """
self.help_browser: HelpBrowser = HelpBrowser() self.help_browser: HelpBrowser = HelpBrowser()
""" """
about_dialog: Dialog showing info of the app.
"""
self.about_dialog: AboutDialog = AboutDialog(self)
"""
search_message_dialog: Display log, soh message with searching feature. search_message_dialog: Display log, soh message with searching feature.
""" """
self.search_message_dialog: SearchMessageDialog = SearchMessageDialog() self.search_message_dialog: SearchMessageDialog = SearchMessageDialog()
...@@ -214,6 +221,14 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -214,6 +221,14 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.yyyy_mm_dd_action.trigger() self.yyyy_mm_dd_action.trigger()
@QtCore.Slot()
def open_about(self):
"""
About dialog is always open. This function will raise it up when called
"""
self.about_dialog.show()
self.about_dialog.raise_()
@QtCore.Slot() @QtCore.Slot()
def save_plot(self): def save_plot(self):
self.plotting_widget.save_plot('SOH-Plot') self.plotting_widget.save_plot('SOH-Plot')
...@@ -420,9 +435,8 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -420,9 +435,8 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
the user can load data. The starting directory is taken from the user can load data. The starting directory is taken from
curr_dir_line_edit. curr_dir_line_edit.
""" """
fd = QtWidgets.QFileDialog(self) fd = FileDialogForLinuxExternalDrive(
fd.setFileMode(QtWidgets.QFileDialog.FileMode.Directory) self, self.curr_dir_line_edit.text())
fd.setDirectory(self.curr_dir_line_edit.text())
fd.exec() fd.exec()
if fd.result() == QtWidgets.QDialog.DialogCode.Accepted: if fd.result() == QtWidgets.QDialog.DialogCode.Accepted:
new_path = fd.selectedFiles()[0] new_path = fd.selectedFiles()[0]
...@@ -623,6 +637,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -623,6 +637,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
Read data from selected files/directories, process and plot channels Read data from selected files/directories, process and plot channels
read from those according to current options set on the GUI read from those according to current options set on the GUI
""" """
self.replot_button.setEnabled(False)
self.plot_diff_data_set_id_button.setEnabled(False) self.plot_diff_data_set_id_button.setEnabled(False)
display_tracking_info(self.tracking_info_text_browser, display_tracking_info(self.tracking_info_text_browser,
"Loading started", "Loading started",
...@@ -891,6 +906,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): ...@@ -891,6 +906,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
Plot using data from self.data_object with the current options set Plot using data from self.data_object with the current options set
from GUI from GUI
""" """
self.replot_button.setEnabled(False)
self.clear_plots() self.clear_plots()
if self.has_problem: if self.has_problem:
return return
......
...@@ -179,6 +179,34 @@ class MultiThreadedPlottingWidget(PlottingWidget): ...@@ -179,6 +179,34 @@ class MultiThreadedPlottingWidget(PlottingWidget):
self.plotting_axes.set_title(self.title) self.plotting_axes.set_title(self.title)
self.plotting_axes.add_gap_bar(self.gaps) self.plotting_axes.add_gap_bar(self.gaps)
def create_ax(self, plotting_data, chan_id):
"""
Create axes for chan_id in plotting_data to add to key ax or ax_wf of
channel, and keep track of axes themselves in self.axes.
:param plotting_data: dict of channels data by chan_id
:param chan_id: name of channel
"""
if ('LINES' in
plotting_data[chan_id]['chan_db_info']['plotType'].upper()):
has_min_max_lines = True
else:
has_min_max_lines = False
plot_h = self.plotting_axes.get_height(
plotting_data[chan_id]['chan_db_info']['height'])
ax = self.plotting_axes.create_axes(
self.plotting_bot, plot_h, has_min_max_lines)
if self.name == 'SOH':
plotting_data[chan_id]['ax'] = ax
else:
plotting_data[chan_id]['ax_wf'] = ax
ax.c_data = plotting_data[chan_id]
ax.chan = chan_id
self.axes.append(ax)
def create_plotting_channel_processors( def create_plotting_channel_processors(
self, plotting_data: Dict, chan_order) -> None: self, plotting_data: Dict, chan_order) -> None:
""" """
...@@ -193,6 +221,10 @@ class MultiThreadedPlottingWidget(PlottingWidget): ...@@ -193,6 +221,10 @@ class MultiThreadedPlottingWidget(PlottingWidget):
continue continue
if not plotting_data[chan_id].get('visible'): if not plotting_data[chan_id].get('visible'):
continue continue
# create ax before plotting, or it will be disordered
# because of threading
self.create_ax(plotting_data, chan_id)
channel_processor = PlottingChannelProcessor( channel_processor = PlottingChannelProcessor(
plotting_data[chan_id], chan_id, plotting_data[chan_id], chan_id,
self.min_x, self.max_x self.min_x, self.max_x
...@@ -276,7 +308,7 @@ class MultiThreadedPlottingWidget(PlottingWidget): ...@@ -276,7 +308,7 @@ class MultiThreadedPlottingWidget(PlottingWidget):
if channel_id is not None: if channel_id is not None:
self.data_processors.pop(0) self.data_processors.pop(0)
self.notification.emit(f'Plotting channel {channel_id}...') self.notification.emit(f'Plotting channel {channel_id}...')
self.plot_single_channel(channel_data, channel_id) self.plotting.plot_channel(channel_data, channel_id)
self.draw() self.draw()
try: try:
...@@ -314,8 +346,8 @@ class MultiThreadedPlottingWidget(PlottingWidget): ...@@ -314,8 +346,8 @@ class MultiThreadedPlottingWidget(PlottingWidget):
finished_msg = f'{self.name} plot finished.' finished_msg = f'{self.name} plot finished.'
display_tracking_info(self.tracking_box, finished_msg, LogType.INFO) display_tracking_info(self.tracking_box, finished_msg, LogType.INFO)
self.is_working = False self.is_working = False
self.handle_replot_button()
def done(self): def done(self):
""" """
......
...@@ -69,11 +69,6 @@ class Plotting: ...@@ -69,11 +69,6 @@ class Plotting:
otherwise, plot_from_value_color_equal_on_lower_bound will be use otherwise, plot_from_value_color_equal_on_lower_bound will be use
:return: ax in which the channel is plotted :return: ax in which the channel is plotted
""" """
if ax is None:
plot_h = self.plotting_axes.get_height(chan_db_info['height'])
ax = self.plotting_axes.create_axes(
self.parent.plotting_bot, plot_h,
has_min_max_lines=False)
if equal_upper: if equal_upper:
points_list, colors = \ points_list, colors = \
get_categorized_data_from_value_color_equal_on_upper_bound( get_categorized_data_from_value_color_equal_on_upper_bound(
...@@ -146,12 +141,6 @@ class Plotting: ...@@ -146,12 +141,6 @@ class Plotting:
:param ax: axes to plot channel :param ax: axes to plot channel
:return ax: axes of the channel :return ax: axes of the channel
""" """
if ax is None:
plot_h = self.plotting_axes.get_height(chan_db_info['height'])
ax = self.plotting_axes.create_axes(
self.parent.plotting_bot, plot_h,
has_min_max_lines=False)
value_colors = chan_db_info['valueColors'].split('|') value_colors = chan_db_info['valueColors'].split('|')
sample_no_colors = [] sample_no_colors = []
...@@ -214,12 +203,6 @@ class Plotting: ...@@ -214,12 +203,6 @@ class Plotting:
:param ax: axes to plot channel :param ax: axes to plot channel
:return ax: axes of the channel :return ax: axes of the channel
""" """
if ax is None:
plot_h = self.plotting_axes.get_height(chan_db_info['height'])
ax = self.plotting_axes.create_axes(
self.parent.plotting_bot, plot_h,
has_min_max_lines=False)
val_cols = chan_db_info['valueColors'].split('|') val_cols = chan_db_info['valueColors'].split('|')
# up/down has 2 values: 0, 1 which match with index of points_list # up/down has 2 values: 0, 1 which match with index of points_list
points_list = [[], []] points_list = [[], []]
...@@ -283,11 +266,6 @@ class Plotting: ...@@ -283,11 +266,6 @@ class Plotting:
:param ax: axes to plot channel :param ax: axes to plot channel
:return ax: axes of the channel :return ax: axes of the channel
""" """
if ax is None:
plot_h = self.plotting_axes.get_height(chan_db_info['height'])
ax = self.plotting_axes.create_axes(
self.parent.plotting_bot, plot_h)
# Set the color to white by default # Set the color to white by default
color = '#000000' color = '#000000'
if chan_db_info['valueColors'] not in [None, 'None', '']: if chan_db_info['valueColors'] not in [None, 'None', '']:
...@@ -337,11 +315,6 @@ class Plotting: ...@@ -337,11 +315,6 @@ class Plotting:
main-title main-title
:return ax: axes of the channel :return ax: axes of the channel
""" """
if ax is None:
plot_h = self.plotting_axes.get_height(chan_db_info['height'])
ax = self.plotting_axes.create_axes(
self.parent.plotting_bot, plot_h)
x_list, y_list = c_data['times'], c_data['data'] x_list, y_list = c_data['times'], c_data['data']
y_list = apply_convert_factor( y_list = apply_convert_factor(
y_list, chan_id, self.main_window.data_type) y_list, chan_id, self.main_window.data_type)
...@@ -472,11 +445,12 @@ class Plotting: ...@@ -472,11 +445,12 @@ class Plotting:
if value_colors is None: if value_colors is None:
return return
if ax is None:
plot_h = self.plotting_axes.get_height(chan_db_info['height'])
ax = self.plotting_axes.create_axes(
self.parent.plotting_bot, plot_h)
x_list, y_list = c_data['times'], c_data['data'] x_list, y_list = c_data['times'], c_data['data']
y_list = apply_convert_factor(
y_list, chan_id, self.main_window.data_type
)
total_x = sum([len(x) for x in x_list]) total_x = sum([len(x) for x in x_list])
self.plotting_axes.set_axes_info( self.plotting_axes.set_axes_info(
...@@ -500,8 +474,7 @@ class Plotting: ...@@ -500,8 +474,7 @@ class Plotting:
# ax.chan_plots. Also scatter is different with ax.plot and # ax.chan_plots. Also scatter is different with ax.plot and
# should be treated in a different way if want to replot. # should be treated in a different way if want to replot.
ax.x_center = x_list[0] ax.x_center = x_list[0]
ax.y_center = apply_convert_factor( ax.y_center = y_list[0]
y_list, chan_id, self.main_window.data_type)[0]
ax.chan_db_info = chan_db_info ax.chan_db_info = chan_db_info
return ax return ax
...@@ -533,26 +506,26 @@ class Plotting: ...@@ -533,26 +506,26 @@ class Plotting:
ax.chan_db_info = chan_db_info ax.chan_db_info = chan_db_info
return ax return ax
def plot_channel(self, c_data: Dict, chan_id: str, def plot_channel(self, c_data: Dict, chan_id: str) -> None:
ax: Optional[Axes]) -> Axes:
""" """
Plot/replot channel for given data Plot/replot channel for given data
:param c_data: data of the channel which includes keys 'times' and :param c_data: data of the channel which includes keys 'times' and
'data'. Refer to general_data/data_structures.MD 'data'. Refer to general_data/data_structures.MD
:param chan_id: name of channel :param chan_id: name of channel
:param ax: axes to plot the channel. If there's no axes provides,
a new axes will be created
:return ax: axes that has been used to plot the channel
""" """
if len(c_data['times']) == 0: if len(c_data['times']) == 0:
return return
if self.parent.name == 'SOH':
ax = c_data['ax']
else:
ax = c_data['ax_wf']
chan_db_info = c_data['chan_db_info'] chan_db_info = c_data['chan_db_info']
plot_type = chan_db_info['plotType'] plot_type = chan_db_info['plotType']
if plot_type in [None, ""]: if plot_type in [None, ""]:
# when edit select a param that has no plot type # when edit select a param that has no plot type
return self.plot_no_plot_type(c_data, chan_db_info, chan_id, ax) return self.plot_no_plot_type(c_data, chan_db_info, chan_id, ax)
ax = getattr( getattr(
self, plot_types[plot_type]['plot_function'])( self, plot_types[plot_type]['plot_function'])(
c_data, chan_db_info, chan_id, ax) c_data, chan_db_info, chan_id, ax)
return ax
...@@ -192,7 +192,7 @@ def get_colors_sizes_for_abs_y_from_value_colors( ...@@ -192,7 +192,7 @@ def get_colors_sizes_for_abs_y_from_value_colors(
def apply_convert_factor(data: List[np.ndarray], chan_id: str, data_type: str def apply_convert_factor(data: List[np.ndarray], chan_id: str, data_type: str
) -> np.ndarray: ) -> List[np.ndarray]:
""" """
Convert data according to convert_factor got from DB Convert data according to convert_factor got from DB
......
...@@ -407,6 +407,10 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -407,6 +407,10 @@ class PlottingWidget(QtWidgets.QScrollArea):
+ If the chan_data has key 'logIdx', raise the Search Messages dialog, + If the chan_data has key 'logIdx', raise the Search Messages dialog,
focus SOH tab, roll to the corresponding line. focus SOH tab, roll to the corresponding line.
""" """
if event.mouseevent.name == 'scroll_event':
return
if event.mouseevent.button in ('up', 'down'):
return
self.is_button_press_event_triggered_pick_event = True self.is_button_press_event_triggered_pick_event = True
artist = event.artist artist = event.artist
self.log_idxes = None self.log_idxes = None
...@@ -507,7 +511,11 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -507,7 +511,11 @@ class PlottingWidget(QtWidgets.QScrollArea):
self.ruler_text = None self.ruler_text = None
except AttributeError: except AttributeError:
pass pass
if (self.main_window.tps_check_box.isChecked() and
modifiers in [QtCore.Qt.KeyboardModifier.ControlModifier,
QtCore.Qt.KeyboardModifier.MetaModifier,
QtCore.Qt.KeyboardModifier.NoModifier]):
self.main_window.tps_dlg.set_indexes_and_display_info(xdata)
for w in self.peer_plotting_widgets: for w in self.peer_plotting_widgets:
if not w.has_data: if not w.has_data:
continue continue
...@@ -840,3 +848,16 @@ class PlottingWidget(QtWidgets.QScrollArea): ...@@ -840,3 +848,16 @@ class PlottingWidget(QtWidgets.QScrollArea):
self.draw() self.draw()
except AttributeError: except AttributeError:
pass pass
def handle_replot_button(self):
"""
Check if all plotting_widgets are done with plotting before enable
replot buttons.
"""
any_is_working = False
for w in self.peer_plotting_widgets:
if w.is_working:
any_is_working = True
break
if not any_is_working:
self.main_window.replot_button.setEnabled(True)
# Drawing State-Of-Health channels and mass position # Drawing State-Of-Health channels and mass position
from typing import Tuple, Union, Dict, Optional from typing import Tuple, Union, Optional
from matplotlib.axes import Axes from matplotlib.axes import Axes
from matplotlib.backend_bases import MouseButton from matplotlib.backend_bases import MouseButton
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore
...@@ -144,21 +144,6 @@ class SOHWidget(MultiThreadedPlottingWidget): ...@@ -144,21 +144,6 @@ class SOHWidget(MultiThreadedPlottingWidget):
self.processing_log.append((msg, LogType.WARNING)) self.processing_log.append((msg, LogType.WARNING))
return True return True
def plot_single_channel(self, c_data: Dict, chan_id: str):
"""
Plot the channel chan_id.
:param c_data: data of the channel which includes down-sampled
data in keys 'times' and 'data'. Refer to data_dict in
data_structure.MD
:param chan_id: name of channel
"""
ax = self.plotting.plot_channel(c_data, chan_id, None)
c_data['ax'] = ax
ax.c_data = c_data
ax.chan = chan_id
self.axes.append(ax)
def add_edit_channel(self): def add_edit_channel(self):
win = AddEditSingleChannelDialog( win = AddEditSingleChannelDialog(
self.parent, self.parent,
......
# Display time-power-squared values for waveform data # Display time-power-squared values for waveform data
from math import sqrt
from typing import Union, Tuple, Dict, List from typing import Union, Tuple, Dict, List
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore
...@@ -6,17 +7,23 @@ from PySide6.QtCore import QEventLoop, Qt, QSize ...@@ -6,17 +7,23 @@ from PySide6.QtCore import QEventLoop, Qt, QSize
from PySide6.QtGui import QCursor from PySide6.QtGui import QCursor
from PySide6.QtWidgets import QApplication, QTabWidget from PySide6.QtWidgets import QApplication, QTabWidget
from sohstationviewer.database.extract_data import ( from sohstationviewer.conf.constants import DAY_LIMIT_FOR_TPS_IN_ONE_TAB
from sohstationviewer.controller.util import \
display_tracking_info, add_thousand_separator
from sohstationviewer.controller.plotting_data import format_time
from sohstationviewer.database.extract_data import \
get_color_def, get_color_ranges get_color_def, get_color_ranges
)
from sohstationviewer.model.general_data.general_data import GeneralData
from sohstationviewer.view.plotting.time_power_square.\ from sohstationviewer.view.plotting.time_power_square.\
time_power_squared_widget import TimePowerSquaredWidget time_power_squared_widget import TimePowerSquaredWidget
from sohstationviewer.controller.util import display_tracking_info
from sohstationviewer.model.general_data.general_data import GeneralData
from sohstationviewer.view.plotting.time_power_square.\ from sohstationviewer.view.plotting.time_power_square.\
time_power_squared_helper import get_start_5mins_of_diff_days time_power_squared_helper import \
from sohstationviewer.conf.constants import DAY_LIMIT_FOR_TPS_IN_ONE_TAB get_start_5mins_of_diff_days, find_tps_tm_idx
class TimePowerSquaredDialog(QtWidgets.QWidget): class TimePowerSquaredDialog(QtWidgets.QWidget):
...@@ -317,7 +324,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): ...@@ -317,7 +324,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
else: else:
self.main_window.tps_tab_total = len( self.main_window.tps_tab_total = len(
d_obj.waveform_data[data_set_id]) d_obj.waveform_data[data_set_id])
for chan_id in d_obj.waveform_data[data_set_id]: for chan_id in sorted(d_obj.waveform_data[data_set_id].keys()):
self.create_tps_widget( self.create_tps_widget(
data_set_id, chan_id, data_set_id, chan_id,
{chan_id: d_obj.waveform_data[data_set_id][chan_id]}) {chan_id: d_obj.waveform_data[data_set_id][chan_id]})
...@@ -346,3 +353,30 @@ class TimePowerSquaredDialog(QtWidgets.QWidget): ...@@ -346,3 +353,30 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
tps_widget.plot_channels( tps_widget.plot_channels(
data_dict, data_set_id, self.start_5mins_of_diff_days, data_dict, data_set_id, self.start_5mins_of_diff_days,
self.min_x, self.max_x) self.min_x, self.max_x)
def set_indexes_and_display_info(self, timestamp):
"""
computing five_minute_idx/day_idx and displaying info of all tps
channels' points.
:param timestamp: real timestamp value
"""
# calculate the indexes corresponding to the timestamp
self.five_minute_idx, self.day_idx = find_tps_tm_idx(
timestamp, self.start_5mins_of_diff_days)
# timestamp in display format
format_t = format_time(timestamp, self.date_format, 'HH:MM:SS')
# display the highlighted point's info on all tabs
info_str = f"<pre>{format_t}:"
for tps_widget in self.tps_widget_dict.values():
for chan_id in tps_widget.plotting_data1:
c_data = tps_widget.plotting_data1[chan_id]
data = c_data['tps_data'][self.day_idx,
self.five_minute_idx]
info_str += (f" {chan_id}:"
f"{add_thousand_separator(sqrt(data))}")
info_str += " (counts)</pre>"
display_tracking_info(self.info_text_browser, info_str)