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 (16)
Showing
with 410 additions and 114 deletions
......@@ -12,7 +12,7 @@ Third letter (Orientation Code): ZNE123
"""
dbConf = {
'dbpath': 'database/soh.db',
'db_path': 'database/soh.db',
'backup_db_path': 'database/backup.db',
'seisRE': re.compile(f'[{WF_1ST}][{WF_2ND}][{WF_3RD}]'),
# key is last char of chan
......@@ -22,7 +22,7 @@ dbConf = {
def modify_db_path():
"""
Modify dbpath to absolute path.
Modify db_path to absolute path.
This function is called in case database needs to be used when the working
directory is a sub folder of root, sohstationviewer/. The following lines
......@@ -34,4 +34,4 @@ def modify_db_path():
current_file_path = os.path.abspath(__file__)
root = Path(current_file_path).parent.parent
db_path = [x for x in root.glob('**/soh.db')][0]
dbConf['dbpath'] = db_path.as_posix()
dbConf['db_path'] = db_path.as_posix()
......@@ -7,19 +7,22 @@ from typing import Sequence, Union
from sohstationviewer.conf.dbSettings import dbConf
def execute_db(sql: str, parameters: Union[dict, Sequence] = ()):
def execute_db(sql: str, parameters: Union[dict, Sequence] = (),
db_path: str = 'db_path'):
"""
Execute or fetch data from DB
:param sql: str - request string to execute or fetch data from database
:param parameters: the parameters used for executing parameterized queries
:param db_path: key of path to db or backup db
:return rows: [(str/int,] - result of querying DB
"""
conn = sqlite3.connect(dbConf['dbpath'])
conn = sqlite3.connect(dbConf[db_path])
cur = conn.cursor()
try:
cur.execute(sql, parameters)
except sqlite3.OperationalError as e:
print("sqlite3.OperationalError:%s\n\tSQL: %s" % (str(e), sql))
print("sqlite3.OperationalError:%s\n\tSQL: %s\n\tpath: %s"
% (str(e), sql, dbConf[db_path]))
rows = cur.fetchall()
conn.commit() # used for execute: update/insert/delete
cur.close()
......@@ -33,7 +36,7 @@ def execute_db_dict(sql):
:param sql: str - request string to fetch data from database
:return: [dict,] - result of fetching DB in which each row is a dict
"""
conn = sqlite3.connect(dbConf['dbpath'])
conn = sqlite3.connect(dbConf['db_path'])
conn.row_factory = sqlite3.Row
cur = conn.cursor()
try:
......@@ -55,7 +58,7 @@ def trunc_add_db(table, sql_list):
or bool(True): if successful
"""
try:
conn = sqlite3.connect(dbConf['dbpath'])
conn = sqlite3.connect(dbConf['db_path'])
cur = conn.cursor()
cur.execute('BEGIN')
cur.execute(f'DELETE FROM {table}')
......
......@@ -404,13 +404,18 @@ class RT130(GeneralData):
for ind in range(0, len(rt130._data))
if ind not in ind_ehet])
log_data = []
for index in ind_ehet:
d = rt130._data[index]
logs = core.EHPacket(d).eh_et_info(nbr_dt_samples)
if 'EHET' not in self.log_data[cur_data_set_id]:
self.log_data[cur_data_set_id]['EHET'] = []
self.log_data[cur_data_set_id]['EHET'].append(
(d['time'], logs))
item = (d['time'], logs)
if item not in log_data:
# Prevent duplicated item in a file caused by EH and ET having
# the same info
log_data.append(item)
self.log_data[cur_data_set_id]['EHET'] += log_data
def get_mass_pos_data_and_waveform_data(
self, rt130: DecimatedReftek130, data_stream: int,
......
......@@ -2,6 +2,7 @@
channel_dialog.py
GUI to add/edit/remove channels
"""
from typing import List, Union
from PySide6.QtWidgets import QMessageBox
from sohstationviewer.view.db_config.db_config_dialog import UiDBInfoDialog
......@@ -49,6 +50,26 @@ class ChannelDialog(UiDBInfoDialog):
self.data_table_widget.cellWidget(0, 4).setText('1')
self.data_table_widget.cellWidget(0, 6).setValue(0)
def set_row_content(self, row_idx: int,
row_content: List[Union[str, int]]):
"""
Set content to each cell in row.
:param row_idx: position of row to set the content
:param row_content: list of contents of all cells in a row
"""
self.data_table_widget.cellWidget(row_idx, 1).setText(row_content[0])
self.data_table_widget.cellWidget(row_idx, 2).setText(row_content[1])
if row_content[2] == -1:
self.data_table_widget.cellWidget(row_idx, 3).setCurrentIndex(-1)
else:
self.data_table_widget.cellWidget(row_idx, 3).setCurrentText(
row_content[2])
self.data_table_widget.cellWidget(row_idx, 4).setText(
str(row_content[3]))
self.data_table_widget.cellWidget(row_idx, 5).setText(row_content[4])
self.data_table_widget.cellWidget(row_idx, 6).setValue(row_content[5])
def set_row_widgets(self, row_idx, fk=False):
"""
Set the widgets in a row in self.data_table_widgets.
......@@ -65,6 +86,7 @@ class ChannelDialog(UiDBInfoDialog):
self.add_widget(row_idx, 5) # unit
self.add_widget(row_idx, 6, range_values=[0, 5]) # fixPoint
self.add_delete_button_to_row(row_idx, fk)
self.add_reset_button_to_row(row_idx)
def get_data_type_from_selector(self):
"""
......@@ -131,7 +153,7 @@ class ChannelDialog(UiDBInfoDialog):
"""
Get list of data to fill self.data_table_widgets' content
"""
channel_rows = execute_db(
sql = (
f"SELECT channel, "
f"IFNULL(label, '') AS label,"
f" IFNULL(param, '') AS param,"
......@@ -139,7 +161,13 @@ class ChannelDialog(UiDBInfoDialog):
f" IFNULL(unit, '') AS unit,"
f" IFNULL(fixPoint, 0) AS fixPoint "
f"FROM Channels "
f"WHERE dataType='{self.data_type}'")
f"WHERE dataType='{self.data_type}'"
)
backup_rows = execute_db(sql, db_path='backup_db_path')
self.backup_database_rows = [
[d[0], d[1], d[2], float(d[3]), d[4], int(d[5])]
for d in backup_rows]
channel_rows = execute_db(sql)
return [[d[0], d[1], d[2], float(d[3]), d[4], int(d[5])]
for d in channel_rows]
......
from __future__ import annotations
from typing import Dict, Optional, List, Tuple, TypeVar
from typing import Dict, Optional, List, Tuple, Union, TypeVar
from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Signal
......@@ -12,6 +12,7 @@ from sohstationviewer.view.db_config.value_color_helper.value_color_edit \
import ValueColorEdit
from sohstationviewer.view.util.one_instance_at_a_time import \
OneWindowAtATimeDialog
from sohstationviewer.view.util.color import COLOR
class DeleteRowButton(QtWidgets.QPushButton):
......@@ -30,6 +31,23 @@ class DeleteRowButton(QtWidgets.QPushButton):
self.setStyleSheet(style_sheet)
class ResetRowButton(QtWidgets.QPushButton):
"""
A button used to reset a row in the data table of UiDBInfoDialog to
that row in backup_db.
"""
def __init__(self):
super().__init__()
self.setText('Reset to org. DB')
style_sheet = self.styleSheet() + (
'.ResetRowButton:enabled {'
' color: #FF474C'
'}\n'
)
self.setStyleSheet(style_sheet)
def set_widget_color(widget, changed=False, read_only=False):
"""
Set color of the given widget depend on flag read_only or changed
......@@ -60,14 +78,16 @@ def set_widget_color(widget, changed=False, read_only=False):
palette.setColor(QtGui.QPalette.ColorRole.Base,
QtGui.QColor(255, 255, 255))
if changed:
# red text
# blue text
palette.setColor(QtGui.QPalette.ColorRole.Text,
QtGui.QColor(255, 0, 0))
QtGui.QColor(COLOR['changedStatus']))
# The selected text in a QComboBox on Linux has ButtonText as its color
# role (probably because the QComboBox on Linux looks like one button,
# while the one on Mac looks like a TextEdit combined with a button).
palette.setColor(QtGui.QPalette.ColorRole.ButtonText,
QtGui.QColor(255, 0, 0))
QtGui.QColor(COLOR['changedStatus']))
palette.setColor(QtGui.QPalette.ColorRole.Base,
QtGui.QColor(COLOR['changedStatusBackground']))
else:
try:
if widget.isReadOnly():
......@@ -79,6 +99,8 @@ def set_widget_color(widget, changed=False, read_only=False):
# black text
palette.setColor(QtGui.QPalette.ColorRole.Text,
QtGui.QColor(0, 0, 0))
palette.setColor(QtGui.QPalette.ColorRole.Base,
QtGui.QColor(255, 255, 255))
except AttributeError:
palette.setColor(QtGui.QPalette.ColorRole.Text,
QtGui.QColor(0, 0, 0))
......@@ -138,13 +160,17 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
super(UiDBInfoDialog, self).__init__()
self.parent = parent
# We need an additional column to accommodate the delete button
self.total_col = len(column_headers) + 1
# We need an additional column to accommodate the delete and reset
# buttons
self.total_col = len(column_headers) + 2
self.column_headers = column_headers
self.resize_content_columns = resize_content_columns
self.delete_button_col_idx = self.total_col - 1
self.reset_button_col_idx = self.total_col - 2
# We want to resize the columns that contain the delete buttons (the
# last on) to be snug with those button
self.resize_content_columns.append(self.total_col - 1)
self.resize_content_columns.append(self.delete_button_col_idx)
self.resize_content_columns.append(self.reset_button_col_idx)
self.required_columns = required_columns
self.need_data_type_choice = need_data_type_choice
self.primary_column = primary_column
......@@ -153,11 +179,15 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
# ========================== internal data ===========================
"""
database_data: list of entries in the database.
database_rows: list of entries in the database.
"""
self.database_rows = []
"""
is_row_changed_array: a bit array that store whether a row is changed.
backup_database_rows: list of entries in the backup_database.
"""
self.backup_database_rows = []
"""
changes_array: a bit array that store whether a row is changed.
"""
self.changes_array: List[List[bool]] = []
"""
......@@ -518,7 +548,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
return
changed = False
if row_idx < len(self.database_rows):
if changed_text != self.database_rows[row_idx][col_idx - 1]:
if changed_text != str(self.database_rows[row_idx][col_idx - 1]):
changed = True
cell_widget = self.data_table_widget.cellWidget(row_idx, col_idx)
set_widget_color(cell_widget, changed=changed)
......@@ -552,6 +582,14 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
"""
pass
def set_row_content(self, row_idx: int, row: List[Union[str, int]]):
"""
Set content to each cell in row.
:param row_idx: position of row to set the content
:param row: list of contents of all cells in a row
"""
pass
def get_row_inputs(self, row_idx: int) -> list:
"""
Get content of a row in the data table. Need to be implemented in all
......@@ -572,12 +610,33 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
delete_button = DeleteRowButton()
if fk:
delete_button.setEnabled(False)
self.data_table_widget.setCellWidget(row_idx, self.total_col - 1,
delete_button)
self.data_table_widget.setCellWidget(
row_idx, self.delete_button_col_idx, delete_button)
delete_button.clicked.connect(
lambda: self.delete_table_row(delete_button)
)
def add_reset_button_to_row(self, row_idx):
"""
Add a reset button to a row of the data table. The button will be on
the right of the row.
This button will be disabled if the current row is the same with
the backup DB's row at the same row_idx and will be enabled otherwise.
:param row_idx: the index of the row to insert a reset button
"""
reset_button = ResetRowButton()
# reset button is enable when current saved row different from
# backup row
if self.database_rows[row_idx] == self.backup_database_rows[row_idx]:
reset_button.setEnabled(False)
self.data_table_widget.setCellWidget(
row_idx, self.reset_button_col_idx, reset_button)
reset_button.clicked.connect(
lambda: self.reset_table_row(reset_button)
)
def display_row_deleted_notification(self, row_idx: int, primary_key: str):
"""
Display a notification that a row has been deleted in place of the
......@@ -656,7 +715,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
lambda: self.undo_delete_table_row(row_idx)
)
self.data_table_widget.setCellWidget(
row_idx, self.total_col - 1, undo_button
row_idx, self.delete_button_col_idx, undo_button
)
delete_sql = (f"DELETE FROM {self.table_name} "
......@@ -666,6 +725,57 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
self.data_table_widget.setUpdatesEnabled(True)
@QtCore.Slot()
def reset_table_row(self, row_reset_button: QtWidgets.QPushButton):
"""
Reset a row to the content in backup db.
Validate backup row before reset. If backup row is invalid, inform
user that row cannot be reset.
Otherwise, set row content from row_idx of backup db to reset the row
and notify user that the row has been reset. Also convert the reset
button into an undo button.
The reset row will also be added to changes_array to so that database
will be updated when SAVE CHANGES button is clicked.
Note that set_row_content will trigger on_cell_input_change which
will update changes_array and the cell's color.
:param row_reset_button: the reset button assigned to the row to be
reset
"""
# If we don't have this line, there is an issue with the undo button
# flickering in behind the 0 row label before moving to the correct
# location.
self.data_table_widget.setUpdatesEnabled(False)
reset_button_y = row_reset_button.y()
row_idx = self.data_table_widget.rowAt(reset_button_y)
backup_row_content = self.backup_database_rows[row_idx]
is_reset_row_valid, msg = self.validate_row(
row_idx, backup_row_content)
if not is_reset_row_valid:
header = "Backup row's validation failed.\n\n"
footer = "\n\nReset can't be performed."
QMessageBox.warning(self, "Resetting Failed",
header + msg + footer)
return
# update row with content from backup database
self.set_row_content(row_idx, backup_row_content)
undo_button = QtWidgets.QPushButton('Undo Reset')
undo_button.clicked.connect(
lambda: self.undo_reset_table_row(row_idx)
)
self.data_table_widget.setCellWidget(
row_idx, self.reset_button_col_idx, undo_button
)
self.data_table_widget.setUpdatesEnabled(True)
QMessageBox.information(
self, "Successful Reset",
"The row has been reset back to original database.")
def undo_delete_table_row(self, row_idx: int):
"""
Undo the deletion of a table row and dequeue the corresponding SQL
......@@ -685,6 +795,34 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
self.data_table_widget.setUpdatesEnabled(True)
def undo_reset_table_row(self, row_idx: int):
"""
Set the content of row_idx's row to the corresponding row in the
current database. Also convert the undo button into a reset button.
Note that set_row_content will trigger on_cell_input_change which
will update changes_array and the cell's color.
:param row_idx: the row number of the row being reset
"""
# If we don't have this line, there is an issue with the undo button
# flickering in behind the 0 row label before moving to the correct
# location.
self.data_table_widget.setUpdatesEnabled(False)
self.set_row_content(row_idx, self.database_rows[row_idx])
reset_button = ResetRowButton()
reset_button.clicked.connect(
lambda: self.reset_table_row(reset_button)
)
self.data_table_widget.setCellWidget(
row_idx, self.reset_button_col_idx, reset_button)
self.data_table_widget.setUpdatesEnabled(True)
QMessageBox.information(
self, "Successfully Undo Reset",
"The row has been set up to current values in database.")
def data_type_changed(self):
"""
Load new self.database_rows when self.data_type_combo_box's value is
......
......@@ -3,7 +3,7 @@ param_dialog.py
GUI to add/dit/remove params
NOTE: Cannot remove or change params that are already used for channels.
"""
from typing import Optional, List, Tuple
from typing import Optional, List, Tuple, Union
from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import Qt, Signal
......@@ -162,6 +162,7 @@ class ParamDialog(UiDBInfoDialog):
self.add_widget(row_idx, 4, range_values=[0, 10])
self.add_delete_button_to_row(row_idx, fk)
self.add_reset_button_to_row(row_idx)
def get_database_rows(self):
"""
......@@ -171,11 +172,16 @@ class ParamDialog(UiDBInfoDialog):
# Seeing as we only need one of these columns for a color mode, we only
# pull the needed valueColors column from the database.
value_colors_column = 'valueColors' + self.color_mode
param_rows = execute_db(
sql = (
f"SELECT param, "
f"IFNULL(plotType, '') AS plotType, "
f"IFNULL({value_colors_column}, '') AS valueColors, "
f"IFNULL(height, 0) AS height FROM Parameters")
backup_rows = execute_db(sql, db_path='backup_db_path')
self.backup_database_rows = [[d[0], d[1], d[2], int(d[3])]
for d in backup_rows]
param_rows = execute_db(sql)
return [[d[0], d[1], d[2], int(d[3])]
for d in param_rows]
......@@ -183,6 +189,20 @@ class ParamDialog(UiDBInfoDialog):
"""Clear the content of the first row of the editor."""
self.data_table_widget.cellWidget(0, 4).setValue(0)
def set_row_content(self, row_idx: int,
row_content: List[Union[str, int]]):
"""
Set content to each cell in row.
:param row_idx: position of row to set the content
:param row_content: list of contents of all cells in a row
"""
self.data_table_widget.cellWidget(row_idx, 1).setText(row_content[0])
self.data_table_widget.cellWidget(row_idx, 2).setCurrentText(
row_content[1])
self.data_table_widget.cellWidget(row_idx, 3).set_value_color(
row_content[2])
self.data_table_widget.cellWidget(row_idx, 4).setValue(row_content[3])
def get_row_inputs(self, row_idx):
"""
Get content from a row of widgets.
......
......@@ -33,6 +33,15 @@ class LineDotDialog(EditValueColorDialog):
self.dot_color_label.setFixedWidth(30)
self.dot_color_label.setAutoFillBackground(True)
# check box to include zero in value_color_str or not
self.zero_include_chkbox = QtWidgets.QCheckBox('Included')
# Widget that allow user to add/edit zero's color
self.select_zero_color_btn = QtWidgets.QPushButton("Select Color")
# Widget to display dot's color
self.zero_color_label = QtWidgets.QLabel()
self.zero_color_label.setFixedWidth(30)
self.zero_color_label.setAutoFillBackground(True)
super(LineDotDialog, self).__init__(parent, value_color_str)
self.setWindowTitle("Edit Line/Dot Plotting's Colors")
......@@ -48,7 +57,12 @@ class LineDotDialog(EditValueColorDialog):
self.main_layout.addWidget(self.dot_color_label, 1, 2, 1, 1)
self.main_layout.addWidget(self.select_dot_color_btn, 1, 3, 1, 1)
self.setup_complete_buttons(2)
self.main_layout.addWidget(self.zero_include_chkbox, 2, 0, 1, 1)
self.main_layout.addWidget(QtWidgets.QLabel('Zero Color'), 2, 1, 1, 1)
self.main_layout.addWidget(self.zero_color_label, 2, 2, 1, 1)
self.main_layout.addWidget(self.select_zero_color_btn, 2, 3, 1, 1)
self.setup_complete_buttons(3)
def connect_signals(self) -> None:
self.select_line_color_btn.clicked.connect(
......@@ -56,6 +70,9 @@ class LineDotDialog(EditValueColorDialog):
self.select_dot_color_btn.clicked.connect(
lambda: self.on_select_color(self.dot_color_label))
self.dot_include_chkbox.clicked.connect(self.on_click_include_dot)
self.select_zero_color_btn.clicked.connect(
lambda: self.on_select_color(self.zero_color_label))
self.zero_include_chkbox.clicked.connect(self.on_click_include_zero)
super().connect_signals()
def on_click_include_dot(self):
......@@ -67,12 +84,22 @@ class LineDotDialog(EditValueColorDialog):
self.select_dot_color_btn.setEnabled(enabled)
self.dot_color_label.setHidden(not enabled)
def on_click_include_zero(self):
"""
Enable/disable select color and show/hide color label according to
dot_include_chkbox is checked or unchecked.
"""
enabled = self.zero_include_chkbox.isChecked()
self.select_zero_color_btn.setEnabled(enabled)
self.zero_color_label.setHidden(not enabled)
def set_value(self):
"""
Change the corresponding color_labels's color according to the color
from value_color_str.
"""
self.dot_include_chkbox.setChecked(False)
self.zero_include_chkbox.setChecked(False)
if self.value_color_str == "":
return
vc_parts = self.value_color_str.split('|')
......@@ -83,6 +110,11 @@ class LineDotDialog(EditValueColorDialog):
if obj_type == 'Dot':
display_color(self.dot_color_label, color)
self.dot_include_chkbox.setChecked(True)
if obj_type == 'Zero':
display_color(self.zero_color_label, color)
self.zero_include_chkbox.setChecked(True)
self.on_click_include_dot()
self.on_click_include_zero()
def save_color(self):
"""
......@@ -94,6 +126,10 @@ class LineDotDialog(EditValueColorDialog):
if self.dot_include_chkbox.isChecked():
dot_color = self.dot_color_label.palette().window().color().name()
self.value_color_str += f"|Dot:{dot_color.upper()}"
if self.zero_include_chkbox.isChecked():
zero_color = \
self.zero_color_label.palette().window().color().name()
self.value_color_str += f"|Zero:{zero_color.upper()}"
self.accept()
......
......@@ -240,7 +240,7 @@ class MultiColorDotDialog(EditValueColorDialog):
if prev_higher_bound != -20 else '')
cond += (f"d < {next_higher_bound}"
if next_higher_bound != 20 else 'd')
msg = f"Value entered must be: {cond}"
msg = f"Value d entered must be: {cond}"
QtWidgets.QMessageBox.information(self, "Error", msg)
try:
self.higher_bound_lnedits[row_id].setText(
......@@ -293,7 +293,7 @@ class MultiColorDotDialog(EditValueColorDialog):
# to always satisfy validating this row's higher bound value for value
# range is [-10,10]
next_higher_bound = (
20 if row_id == len(self.row_id_value_dict) - 1
20 if row_id >= len(self.row_id_value_dict) - 1
else float(self.higher_bound_lnedits[row_id + 1].text()))
curr_higher_bound = float(self.higher_bound_lnedits[row_id].text())
......@@ -384,8 +384,8 @@ class MultiColorDotDialog(EditValueColorDialog):
if color == 'not plot':
self.set_color_enabled(vc_idx, False)
else:
self.set_color_enabled(vc_idx, True)
display_color(self.color_labels[vc_idx], color)
self.set_color_enabled(vc_idx, True)
def save_color(self):
"""
......
......@@ -23,6 +23,7 @@ from sohstationviewer.view.db_config.value_color_helper.functions import \
from sohstationviewer.view.db_config.param_helper import \
validate_value_color_str
from sohstationviewer.view.util.plot_type_info import plot_types
from sohstationviewer.view.util.color import COLOR
plot_types_with_value_colors = [
......@@ -129,7 +130,7 @@ class ValueColorEdit(QTextEdit):
Set border color for the widget.
:param color: color to set to border
"""
self.setStyleSheet("QTextEdit {border: 2px solid %s;}" % color)
self.setStyleSheet("QTextEdit {border: 4px solid %s;}" % color)
def set_background_color(self, background: str):
"""
......@@ -142,9 +143,10 @@ class ValueColorEdit(QTextEdit):
if background == 'B':
palette.setColor(QtGui.QPalette.ColorRole.Text, Qt.white)
palette.setColor(QtGui.QPalette.ColorRole.Base, Qt.black)
palette.setColor(QtGui.QPalette.ColorRole.PlaceholderText,
Qt.lightGray)
self.setPalette(palette)
else:
palette.setColor(QtGui.QPalette.ColorRole.Text, Qt.black)
palette.setColor(QtGui.QPalette.ColorRole.Base, Qt.white)
self.setPalette(palette)
def set_value_color(self, value_color_str: str) -> None:
"""
......@@ -161,12 +163,12 @@ class ValueColorEdit(QTextEdit):
def set_border_color_according_to_value_color_status(self):
"""
Set border color according to current value color string.
+ Red for changed
+ Blue for changed
+ Same color as background (no border) for other case.
"""
if self.value_color_str != self.org_value_color_str:
# Show that value_color_str has been changed
self.set_border_color('red')
self.set_border_color(COLOR['changedStatus'])
else:
# Reset border the same color as background
self.set_border_color(self.background)
......@@ -186,7 +188,7 @@ class ValueColorEdit(QTextEdit):
'upDownDots': UpDownDialog,
}
# set border color blue showing that the widget is being edited
self.set_border_color('blue')
self.set_border_color(COLOR['changedStatus'])
edit_dialog = plot_type_dialog_map[self.plot_type](
self, self.value_color_str
)
......
......@@ -1300,7 +1300,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
# We want a backup of the working database to restore to if
# something goes wrong when restoring from the backup database.
current_main_db_backup = sqlite3.connect(':memory:')
main_db = sqlite3.connect(dbConf['dbpath'])
main_db = sqlite3.connect(dbConf['db_path'])
backup_db = sqlite3.connect(dbConf['backup_db_path'])
main_db.backup(current_main_db_backup)
......
......@@ -324,30 +324,41 @@ class Plotting:
for cStr in color_parts:
obj, c = cStr.split(':')
colors[obj] = c
l_color = '#00FF00'
has_dot = False
l_color = '#00FF00' # default Line color
if 'Line' in colors:
l_color = colors['Line']
has_dot = False # Optional dot
if 'Dot' in colors:
d_color = colors['Dot']
has_dot = True
else:
d_color = l_color
if chan_id == 'GPS Lk/Unlk':
has_zero = False # Optional zero
if 'Zero' in colors:
z_color = colors['Zero']
has_zero = True
if chan_id == 'GPS Lk/Unlk':
info = "GPS Clock Power"
if has_zero:
sample_no_list = []
ax.x_bottom = x_list[0][np.where(y_list[0] == -1)[0]]
# compute x_bottom, x_center, x_top for labels of total numbers on
# the left of the channel
ax.x_bottom = x_list[0][np.where(y_list[0] < 0)[0]]
sample_no_list.append(ax.x_bottom.size)
ax.x_center = x_list[0][np.where(y_list[0] == 0)[0]]
sample_no_list.append(ax.x_center.size)
ax.x_top = x_list[0][np.where(y_list[0] == 1)[0]]
ax.x_top = x_list[0][np.where(y_list[0] > 0)[0]]
sample_no_list.append(ax.x_top.size)
sample_no_colors = [d_color, z_color, d_color]
sample_no_pos = [0.05, 0.5, 0.95]
top_bottom_index = np.where(y_list[0] != 0)[0]
# for plotting top & bottom
top_bottom_index = np.where(y_list[0] != 0)[0]
x_list = [x_list[0][top_bottom_index]]
y_list = [y_list[0][top_bottom_index]]
......@@ -362,7 +373,6 @@ class Plotting:
picker=True, pickradius=3)
ax.chan_plots.append(chan_plot)
info = "GPS Clock Power"
else:
sample_no_list = [None, sum([len(x) for x in x_list]), None]
sample_no_colors = [None, d_color, None]
......
......@@ -501,6 +501,8 @@ class PlottingWidget(QtWidgets.QScrollArea):
return
# on_button_press_event() take place after on_pick_event where
# tps_t was assigned in TPS Widget
if self.tps_t is None:
return
xdata = self.tps_t
else:
xdata = self.get_timestamp(event)
......
......@@ -145,11 +145,15 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
color_layout.addWidget(self.color_range_choice)
"""
every_day_5_min_list: [[288 of floats], ] - the list of all starts
of five minutes for every day in which each day has 288 of
5 minutes.
every_day_5_min_blocks: the list of starts of five minutes blocks for
full days.
"""
self.start_5min_blocks: np.ndarray[float] = np.array([])
"""
start_first_day: timestamp of the beginning of the min time's day
"""
self.start_first_day: float = 0
# ##################### Replot button ########################
self.replot_current_tab_button = QtWidgets.QPushButton(
"RePlot Current Tab", self)
......@@ -307,7 +311,8 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
self.processing_log_msg = ""
self.min_x = max(d_obj.data_time[data_set_id][0], start_tm)
self.max_x = min(d_obj.data_time[data_set_id][1], end_tm)
self.start_5min_blocks = get_start_5min_blocks(self.min_x, self.max_x)
self.start_5min_blocks, self.start_first_day = get_start_5min_blocks(
self.min_x, self.max_x)
for i in range(self.plotting_tab.count() - 1, -1, -1):
# delete all tps tabs
widget = self.plotting_tab.widget(i)
......@@ -349,7 +354,8 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
self.plotting_tab.addTab(tps_widget, tab_name)
self.tps_widget_dict[tab_name] = tps_widget
tps_widget.plot_channels(
data_dict, data_set_id, self.start_5min_blocks,
data_dict, data_set_id,
self.start_5min_blocks, self.start_first_day,
self.min_x, self.max_x)
def set_indexes_and_display_info(self, timestamp):
......@@ -362,7 +368,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
"""
# calculate the indexes corresponding to the timestamp
self.five_minute_idx, self.day_idx = find_tps_tm_idx(
timestamp, self.start_5min_blocks)
timestamp, self.start_5min_blocks, self.start_first_day)
# timestamp in display format
format_t = format_time(timestamp, self.date_format, 'HH:MM:SS')
......
......@@ -6,7 +6,8 @@ from obspy import UTCDateTime
from sohstationviewer.conf import constants as const
def get_start_5min_blocks(start_tm: float, end_tm: float) -> np.ndarray:
def get_start_5min_blocks(start_tm: float, end_tm: float) \
-> Tuple[np.ndarray, float]:
"""
Get the array of the start time of all five minutes for each day start from
the start of startTm's day and end at the end of endTm's day.
......@@ -16,6 +17,7 @@ def get_start_5min_blocks(start_tm: float, end_tm: float) -> np.ndarray:
:return start_5min_blocks: array of starts of five minutes blocks for
full days that cover start_tm and end_tm.
:return start_first_day: timestamp of the beginning of the min time's day.
"""
# get start of the first day
utc_start = UTCDateTime(start_tm)
......@@ -39,11 +41,12 @@ def get_start_5min_blocks(start_tm: float, end_tm: float) -> np.ndarray:
start_5min_blocks = np.arange(start_first_day,
end_last_day,
const.SEC_5M)
return start_5min_blocks
return start_5min_blocks, start_first_day
def find_tps_tm_idx(
given_tm: float, start_5min_blocks: List[List[float]]) \
given_tm: float, start_5min_blocks: List[List[float]],
start_first_day: float) \
-> Tuple[int, int]:
"""
Convert from given real timestamp to tps plot's day index and
......@@ -52,22 +55,20 @@ def find_tps_tm_idx(
:param given_tm: real timestamp
:param start_5min_blocks: the array of starts of five minutes blocks for
full days.
:param start_first_day: timestamp of the beginning of the min time's day.
:return x_idx: index of 5m blocks
:return y_idx: index of the day the given time belong to in plotting
:return five_minute_idx: index of 5m blocks
:return day_idx: index of the day the given time belong to in plotting
"""
try:
# get the first index of 5m blocks closest to the given_tm
tm_idx = np.where(start_5min_blocks >= given_tm)[0][0]
# 5m index in a day
five_minute_idx = tm_idx % const.NUMBER_OF_5M_IN_DAY
# day index
day_idx = tm_idx // const.NUMBER_OF_5M_IN_DAY
except IndexError:
# No tm_idx found happens when the given time fall into the last 5m of
# the last day. Although the time 24:00 of the last day belongs
time_delta = given_tm - start_first_day
day_idx = day_delta = int(time_delta // const.SEC_DAY)
second_of_day_of_given_tm = time_delta - day_delta * const.SEC_DAY
five_minute_idx = int(second_of_day_of_given_tm // const.SEC_5M)
five_minute_idx_not_exclude_days = time_delta // const.SEC_5M
if five_minute_idx_not_exclude_days > len(start_5min_blocks) - 1:
# When the given time fall into the last 5m of the last day.
# Although the time 24:00 of the last day belongs
# to the next days of other cases, but since there is no more days to
# plot it, it is no harm to set it at the last 5m of the last day.
five_minute_idx = const.NUMBER_OF_5M_IN_DAY - 1
......
......@@ -62,6 +62,10 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
"""
self.start_5min_blocks: np.ndarray[float] = np.array([])
"""
start_first_day: timestamp of the beginning of the min time's day
"""
self.start_first_day: float = 0
"""
tps_t: float - prompt's time on tps's chart to help rulers on other
plotting widgets to identify their location
"""
......@@ -126,6 +130,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
def plot_channels(self, data_dict: Dict,
data_set_id: Union[str, Tuple[str, str]],
start_5min_blocks: np.ndarray,
start_first_day,
min_x: float, max_x: float):
"""
Recursively plot each TPS channels for waveform_data.
......@@ -145,6 +150,7 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
self.max_x = max_x
self.plot_total = len(self.plotting_data1)
self.start_5min_blocks = start_5min_blocks
self.start_first_day = start_first_day
self.start_message_and_reset_processing_states(
f'Plotting {self.get_plot_name()} ...')
self.gap_bar = None
......@@ -330,13 +336,13 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
)
zoom_marker1 = ax.plot(
[], [], marker='|', markersize=5,
[], [], marker='|', markersize=5, markeredgewidth=1.5,
markeredgecolor=self.display_color['zoom_marker'],
zorder=const.Z_ORDER['TPS_MARKER'])[0]
self.zoom_marker1s.append(zoom_marker1)
zoom_marker2 = ax.plot(
[], [], marker='|', markersize=5,
[], [], marker='|', markersize=5, markeredgewidth=1.5,
markeredgecolor=self.display_color['zoom_marker'],
zorder=const.Z_ORDER['TPS_MARKER'])[0]
self.zoom_marker2s.append(zoom_marker2)
......@@ -476,6 +482,9 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
:param event: event when object of canvas is selected.
The event happens before button_press_event.
"""
# Set tps_t None in case the function is return before tps_t is
# calculated, then other function that look for tps_t will know.
self.tps_t = None
if event.mouseevent.name == 'scroll_event':
return
if event.mouseevent.button in ('up', 'down'):
......@@ -492,28 +501,35 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
xdata = event.mouseevent.xdata
if xdata is None:
return
# Because of the thickness of the marker, round help cover the part
# before and after the point
xdata = round(xdata)
# when click on outside xrange that close to edge, adjust to edge
if xdata in [-2, -1]:
xdata = 0
if xdata in [288, 289]:
xdata = 287
if xdata < 0 or xdata > 287:
# Ignore when clicking outside xrange
return
# clicked point's x value is the 5m index in a day
five_minute_index = xdata
ydata = round(event.mouseevent.ydata)
# day start at a new integer number, so any float between one day
# to the next will be considered belong to that day.
ydata = int(event.mouseevent.ydata)
total_days = (len(self.start_5min_blocks)
// const.NUMBER_OF_5M_IN_DAY)
if ydata > 0 or ydata < - (total_days - 1):
# Ignore when clicking outside yrange
return
# Clicked point's y value is corresponding to the day index but
# negative because days are plotted from top to bottom.
# If click in the area above first day, set day_index to 0 or
# it will highlight day 1 which isn't close to the clicked point.
day_index = abs(ydata) if ydata <= 0 else 0
day_index = abs(ydata)
try:
# Assign tps_t to be use as xdata or real timestamp
# Assign tps_t to be used as xdata or real timestamp
# from plotting_widget.button_press_event() (super class)
self.tps_t = self.start_5min_blocks[
day_index * const.NUMBER_OF_5M_IN_DAY + five_minute_index]
self.tps_t = (day_index * const.SEC_DAY +
five_minute_index*const.SEC_5M +
self.start_first_day)
except IndexError:
# exclude the extra points added to the 2 sides of x axis to
# show the entire highlight box
......@@ -577,14 +593,16 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
y index (which day) of self.min_x and self.min_y, and set data for
all markers in self.zoom_marker1s and self.zoom_marker2s.
"""
x_idx, y_idx = find_tps_tm_idx(self.min_x,
self.start_5min_blocks)
five_minute_idx, day_idx = find_tps_tm_idx(self.min_x,
self.start_5min_blocks,
self.start_first_day)
for zm1 in self.zoom_marker1s:
zm1.set_data(x_idx, y_idx)
x_idx, y_idx = find_tps_tm_idx(self.max_x,
self.start_5min_blocks)
zm1.set_data(five_minute_idx, - day_idx)
five_minute_idx, day_idx = find_tps_tm_idx(self.max_x,
self.start_5min_blocks,
self.start_first_day)
for zm2 in self.zoom_marker2s:
zm2.set_data(x_idx, y_idx)
zm2.set_data(five_minute_idx, - day_idx)
def request_stop(self):
"""Request all running channel processors to stop."""
......
# colors that are good for both light and dark mode
COLOR = {
'changedStatus': '#268BD2', # blue
'changedStatusBackground': "#e0ecff", # light blue
}
# Just using RGB for everything since some things don't handle color names
# correctly, like PIL on macOS doesn't handle "green" very well.
# b = dark blue, was the U value for years, but it can be hard to see, so U
......@@ -39,7 +45,7 @@ def set_color_mode(mode):
display_color['sub_basic'] = clr["A"]
display_color["plot_label"] = clr["C"]
display_color["time_ruler"] = clr["Y"]
display_color["zoom_marker"] = clr["O"]
display_color["zoom_marker"] = "#FFA500" # to show up on tps's colors
display_color["warning"] = clr["O"]
display_color["error"] = clr["R"]
display_color["state_of_health"] = clr["u"]
......@@ -51,7 +57,7 @@ def set_color_mode(mode):
display_color['sub_basic'] = clr["A"]
display_color["plot_label"] = clr["B"]
display_color["time_ruler"] = clr["U"]
display_color["zoom_marker"] = clr["O"]
display_color["zoom_marker"] = "#FFA500" # to show up on tps's colors
display_color["warning"] = clr["O"]
display_color["error"] = clr["s"]
display_color["state_of_health"] = clr["u"]
......
......@@ -12,11 +12,11 @@ plot_types = {
" Dots are plotted with color #FF0000\n"
"If Dot is not defined, dots won't be displayed.\n"
"If L is not defined, lines will be plotted with color "
"#00FF00.\n"
"Optionally, a color for points with value 0 can be defined "
"This is currently only used for channel GPS Lk/Unlk.\n"
"Ex: Zero:#0000FF means points with value are plotted with "
"color #0000FF."
"#00FF00.\n\n"
"If Zero is defined, this plot type will plot a line through "
"bottom points (<0) and top points (>0). Zero points will be "
"plotted in the middle."
"Ex: Line:#00FF00|Dot:#FF0000|Zero:#0000FF"
),
"plot_function": "plot_lines_dots",
"value_pattern": re.compile('^(L|D|Z|Line|Dot|Zero)$'),
......
......@@ -9,4 +9,4 @@ class BaseTestCase(TestCase):
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
dbSettings.dbConf['dbpath'] = 'sohstationviewer/database/soh.db'
dbSettings.dbConf['db_path'] = 'sohstationviewer/database/soh.db'
......@@ -37,11 +37,11 @@ class TestValidateValueColorStr(BaseTestCase):
" Dots are plotted with color #FF0000\n"
"If Dot is not defined, dots won't be displayed.\n"
"If L is not defined, lines will be plotted with color "
"#00FF00.\n"
"Optionally, a color for points with value 0 can be defined "
"This is currently only used for channel GPS Lk/Unlk.\n"
"Ex: Zero:#0000FF means points with value are plotted with "
"color #0000FF.")
"#00FF00.\n\n"
"If Zero is defined, this plot type will plot a line through "
"bottom points (<0) and top points (>0). Zero points will be "
"plotted in the middle."
"Ex: Line:#00FF00|Dot:#FF0000|Zero:#0000FF")
)
def test_up_down_dots(self):
......
......@@ -23,19 +23,21 @@ class TestGetEachDay5MinList(BaseTestCase):
with self.subTest("start, end in different day"):
start = UTCDateTime("2012-09-07T12:15:00").timestamp
end = UTCDateTime("2012-09-09T00:00:00").timestamp
start_5min_blocks = get_start_5min_blocks(
start_5min_blocks, start_first_day = get_start_5min_blocks(
start, end
)
self.assertEqual(len(start_5min_blocks),
const.NUMBER_OF_5M_IN_DAY * 2)
self.assertEqual(start_first_day, 1346976000.0)
with self.subTest("start, end in same day"):
start = UTCDateTime("2012-09-07T12:15:00").timestamp
end = UTCDateTime("2012-09-08T00:00:00").timestamp
start_5min_blocks = get_start_5min_blocks(
start_5min_blocks, start_first_day = get_start_5min_blocks(
start, end
)
self.assertEqual(len(start_5min_blocks), const.NUMBER_OF_5M_IN_DAY)
self.assertEqual(start_first_day, 1346976000.0)
def test_start_exact_end_in_middle(self):
"""
......@@ -44,17 +46,20 @@ class TestGetEachDay5MinList(BaseTestCase):
with self.subTest("start, end in different day"):
start = UTCDateTime("2012-09-07T00:0:00").timestamp
end = UTCDateTime("2012-09-08T12:15:00").timestamp
start_5min_blocks = get_start_5min_blocks(start, end)
start_5min_blocks, start_first_day = get_start_5min_blocks(
start, end)
self.assertEqual(len(start_5min_blocks),
2 * const.NUMBER_OF_5M_IN_DAY)
self.assertEqual(start_first_day, 1346976000)
with self.subTest("start, end in same day"):
start = UTCDateTime("2012-09-0700:13:00").timestamp
end = UTCDateTime("2012-09-07T12:13:00").timestamp
start_5min_blocks = get_start_5min_blocks(
start_5min_blocks, start_first_day = get_start_5min_blocks(
start, end
)
self.assertEqual(len(start_5min_blocks), const.NUMBER_OF_5M_IN_DAY)
self.assertEqual(start_first_day, 1346976000)
class TestFindTPSTmIdx(BaseTestCase):
......@@ -63,28 +68,44 @@ class TestFindTPSTmIdx(BaseTestCase):
start = UTCDateTime("2012-09-07T12:15:00").timestamp
end = UTCDateTime("2012-09-09T00:00:00").timestamp
# cover 2 days: 2012/09/07 and 2012/09/08
cls.start_5min_blocks = get_start_5min_blocks(
cls.start_5min_blocks, cls.start_first_day = get_start_5min_blocks(
start, end
)
def test_given_time_beginning_of_first_day(self):
tm = UTCDateTime("2012-09-07T00:00:00").timestamp
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks)
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks,
self.start_first_day)
self.assertEqual(tps_tm_idx, (0, 0))
def test_given_time_middle_of_day(self):
def test_within_first_five_minute(self):
tm = UTCDateTime("2012-09-07T00:00:30").timestamp
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks,
self.start_first_day)
self.assertEqual(tps_tm_idx, (0, 0))
def test_given_time_middle_of_first_day(self):
tm = UTCDateTime("2012-09-07T00:35:00").timestamp
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks)
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks,
self.start_first_day)
self.assertEqual(tps_tm_idx, (7, 0))
def test_given_time_middle_of_second_day(self):
tm = UTCDateTime("2012-09-08T00:35:00").timestamp
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks,
self.start_first_day)
self.assertEqual(tps_tm_idx, (7, 1))
def test_given_time_beginning_of_day_in_middle(self):
tm = UTCDateTime("2012-09-08T00:00:00").timestamp
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks)
tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks,
self.start_first_day)
self.assertEqual(tps_tm_idx, (0, 1))
def test_given_time_very_end_of_last_day(self):
tm = UTCDateTime("2012-09-09T00:00:00").timestamp
start_tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks)
start_tps_tm_idx = find_tps_tm_idx(tm, self.start_5min_blocks,
self.start_first_day)
self.assertEqual(start_tps_tm_idx, (287, 1))
......@@ -94,7 +115,7 @@ class TestGetTPSForDiscontinuousData(BaseTestCase):
cls.day_begin = UTCDateTime("2021-07-05T00:00:00").timestamp
cls.start = UTCDateTime("2021-07-05T22:59:28.340").timestamp
cls.end = UTCDateTime("2021-07-06T3:59:51.870").timestamp
cls.start_5mins_blocks = get_start_5min_blocks(cls.start, cls.end)
cls.start_5mins_blocks, _ = get_start_5min_blocks(cls.start, cls.end)
def test_more_than_10_minute_apart(self):
# check for empty block in between tps data
......