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 (24)
Showing
with 531 additions and 214 deletions
import os
from pathlib import Path
from typing import Literal from typing import Literal
# The path to the package's root
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'
......
...@@ -16,33 +16,6 @@ dbConf = { ...@@ -16,33 +16,6 @@ dbConf = {
'seisRE': re.compile(f'[{WF_1ST}][{WF_2ND}][{WF_3RD}]'), 'seisRE': re.compile(f'[{WF_1ST}][{WF_2ND}][{WF_3RD}]'),
# key is last char of chan # key is last char of chan
'seisLabel': {'1': 'NS', '2': 'EW', 'N': 'NS', 'E': 'EW', 'Z': 'V'}, 'seisLabel': {'1': 'NS', '2': 'EW', 'N': 'NS', 'E': 'EW', 'Z': 'V'},
# +0.2:Y
'multiColorDots': {
'pattern': re.compile('^\+?\-?[0-9]+\.?[0-9]?:[RYGMWC_]'), # noqa: W605,E501
'instruction': (
"Colors include: RYGMCW\n"
"Ex: *:W means everything with white color\n",
"Ex: -1:_|0:R|2.3:Y|+2.3:G means:\n"
" value <= -1 => not plot\n"
" value <= 0 => plot with R color\n"
" value <= 2.3 => plot with Y color\n"
" value > 2.3 => plot with G color")},
'upDownDots': {
'pattern': re.compile("^[01]:[WRYGMC]$"),
'instruction': (
"Colors include: RYGMCW\n"
"Ex: 1:Y|0:R means:\n"
" value == 1 => plot above center line with Y color\n"
" value == 0 => plot under center line with R color")},
'linesDots': {
'pattern': re.compile('^[LD]:[WRYGMC]$'),
'instruction': (
"Colors include: RYGMCW\n"
"Ex: L:G|D:W means\n"
" Lines are plotted with color G\n"
" Dots are plotted with color W\n"
"If D is not defined, dots won't be displayed.\n"
"If L is not defined, lines will be plotted with color G")}
} }
......
...@@ -171,12 +171,16 @@ def get_time_4(time_str: str, nearest_prior_epoch: float) -> float: ...@@ -171,12 +171,16 @@ def get_time_4(time_str: str, nearest_prior_epoch: float) -> float:
def get_val(text: str) -> float: def get_val(text: str) -> float:
""" """
Get the value part of a string with non-number substring following. Get the value part of a string. Examples:
:param text: value string including unit - 6.5V -> 6.5
- <=2.2 -> 2.2
- <2 -> 2.0
- 6m -> 6.0
:param text: value string.
:return: value part including +/-, remove str that follows :return: value part including +/-, remove str that follows
and remove '=' prefix and remove '<' and '=' characters
""" """
text = text.replace('=', '') text = text.replace('=', '').replace('<', '')
re_val = '^\+?\-?[0-9]+\.?[0-9]?' # noqa: W605 re_val = '^\+?\-?[0-9]+\.?[0-9]?' # noqa: W605
return float(re.search(re_val, text).group()) return float(re.search(re_val, text).group())
...@@ -418,3 +422,43 @@ def get_valid_file_count(list_of_dir: Path) -> int: ...@@ -418,3 +422,43 @@ def get_valid_file_count(list_of_dir: Path) -> int:
total += len([f for f in files total += len([f for f in files
if validate_file(Path(path).joinpath(f), f)]) if validate_file(Path(path).joinpath(f), f)])
return total return total
def _format_time_item(
unit: str, value: Union[int, float], time_list: List[str]):
"""
Append the format string of time item to time_list if not zero
:param unit: a time unit (day/hour/minute/second)
:param value: value of the time item
:param time_list: list of different formatted time item
"""
if value == 0:
return
else:
formatted = f"{value} {unit}"
if value > 1:
formatted += 's'
time_list.append(formatted)
def get_formatted_time_delta(time_delta: float) -> str:
"""
Convert time_delta in seconds into formatted string of
(days, hours, minutes, seconds)
:param time_delta: length of time delta in seconds
:return formatted string of time delta
"""
time_list = []
days = int(time_delta // 86400)
_format_time_item('day', days, time_list)
hours = int((time_delta - days * 86400) // 3600)
_format_time_item('hour', hours, time_list)
minutes = int((time_delta - days * 86400 - hours * 3600) // 60)
_format_time_item('minute', minutes, time_list)
seconds = time_delta - days * 86400 - hours * 3600 - minutes * 60
_format_time_item('second', seconds, time_list)
if not time_list:
return "0.0 seconds"
else:
return " ".join(time_list)
No preview for this file type
...@@ -25,8 +25,9 @@ Note: log_data for RT130's dataset has only one channel: SOH ...@@ -25,8 +25,9 @@ Note: log_data for RT130's dataset has only one channel: SOH
'data' (np.array): data that has been trimmed and down-sampled for plotting 'data' (np.array): data that has been trimmed and down-sampled for plotting
'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' (matplotlib.axes.Axes): axes to draw the channel in WaveformWidget
'visible': flag to show or hide channel
} }
} }
......
...@@ -370,6 +370,8 @@ class GeneralData(): ...@@ -370,6 +370,8 @@ class GeneralData():
""" """
for data_set_id in self.data_set_ids: for data_set_id in self.data_set_ids:
self.gaps[data_set_id] = [] self.gaps[data_set_id] = []
if self.gap_minimum is None:
continue
retrieve_gaps_from_data_dict(data_set_id, retrieve_gaps_from_data_dict(data_set_id,
self.soh_data, self.gaps) self.soh_data, self.gaps)
retrieve_gaps_from_data_dict(data_set_id, retrieve_gaps_from_data_dict(data_set_id,
......
...@@ -187,14 +187,29 @@ class MSeedReader: ...@@ -187,14 +187,29 @@ class MSeedReader:
def _append_trace(self, channel: Dict, def _append_trace(self, channel: Dict,
data_points: Tuple[Real, Real], data_points: Tuple[Real, Real],
times: Tuple[Real, Real]): times: Tuple[Real, Real],
one_sample_apart: bool):
""" """
Appending data_point to the latest trace of channel['tracesInfo'] Appending data_point to the latest trace of channel['tracesInfo']
If the added trace is one sample apart from the previous one, the last
point before adding new trace will be removed.
:param channel: dict of channel's info :param channel: dict of channel's info
:param data_points: data points extracted from the record frame :param data_points: data points extracted from the record frame
:param times: time data of the data point :param times: time data of the data point
:param one_sample_apart: flag to tell if the added trace is one sample
apart from the previous one.
""" """
if one_sample_apart:
# Remove last point of the previous trace if it is only one sample
# apart from beginning of the next one to not showing 2 points that
# is very close together.
channel['tracesInfo'][-1]['data'] = channel['tracesInfo'][-1][
'data'][:-1]
channel['tracesInfo'][-1]['times'] = channel['tracesInfo'][-1][
'times'][:-1]
channel['tracesInfo'][-1]['data'].extend(data_points) channel['tracesInfo'][-1]['data'].extend(data_points)
channel['tracesInfo'][-1]['times'].extend(times) channel['tracesInfo'][-1]['times'].extend(times)
...@@ -203,9 +218,12 @@ class MSeedReader: ...@@ -203,9 +218,12 @@ class MSeedReader:
times: Tuple[Real, Real]) -> None: times: Tuple[Real, Real]) -> None:
""" """
Add new channel to the passed station and append data_point to the Add new channel to the passed station and append data_point to the
channel if there's no gap/overlap or start a new trace of data channel if there's no gap/overlap or start a new trace of data
when there's a gap. when there's a gap.
If gap/overlap > gap_minimum, add to gaps list.
If gap/overlap > gap_minimum, add to gaps list but gap won't be added
if the added trace is one sample apart from the previous one to keep
the view sample rate for the channel consistent.
:param station: dict of chan by id of a station :param station: dict of chan by id of a station
:param metadata: metadata of the record the data points are extracted :param metadata: metadata of the record the data points are extracted
...@@ -230,24 +248,33 @@ class MSeedReader: ...@@ -230,24 +248,33 @@ class MSeedReader:
'endTmEpoch': meta.end_time, 'endTmEpoch': meta.end_time,
'data': list(data_points), 'data': list(data_points),
'times': list(times) 'times': list(times)
}] }],
'visible': True
} }
else: else:
channel = station[chan_id] channel = station[chan_id]
record_start_time = meta.start_time record_start_time = meta.start_time
previous_end_time = channel['endTmEpoch'] previous_end_time = channel['endTmEpoch']
delta = abs(record_start_time - previous_end_time) delta = record_start_time - previous_end_time
sample_interval = 1 / meta.sample_rate
if channel['file_path'] != self.file_path: if channel['file_path'] != self.file_path:
# Start new trace for each file to reorder trace and # Start new trace for each file to reorder trace and
# combine traces again later # combine traces again later
channel['file_path'] = self.file_path channel['file_path'] = self.file_path
self._add_new_trace(channel, meta, data_points, times) self._add_new_trace(channel, meta, data_points, times)
else: else:
if self.gap_minimum is not None and delta >= self.gap_minimum: if (self.gap_minimum is not None and
abs(delta) >= self.gap_minimum and
delta != sample_interval):
gap = [previous_end_time, record_start_time] gap = [previous_end_time, record_start_time]
channel['gaps'].append(gap) channel['gaps'].append(gap)
# appending data # appending data
self._append_trace(channel, data_points, times) if delta == sample_interval:
one_sample_apart = True
else:
one_sample_apart = False
self._append_trace(channel, data_points, times,
one_sample_apart)
channel['tracesInfo'][-1]['endTmEpoch'] = meta.end_time channel['tracesInfo'][-1]['endTmEpoch'] = meta.end_time
# update channel's metadata # update channel's metadata
......
...@@ -496,6 +496,7 @@ class LogInfo(): ...@@ -496,6 +496,7 @@ class LogInfo():
'reftek': True, 'reftek': True,
'samplerate': 1, 'samplerate': 1,
'chanID': chan_id, 'chanID': chan_id,
'visible': True,
'tracesInfo': [{ 'tracesInfo': [{
'unitID': self.unit_id, 'unitID': self.unit_id,
'expNo': self.exp_no, 'expNo': self.exp_no,
......
...@@ -50,7 +50,8 @@ def check_reftek_header( ...@@ -50,7 +50,8 @@ def check_reftek_header(
samplerate = trace.stats['sampling_rate'] samplerate = trace.stats['sampling_rate']
if chan_id not in cur_data_dict: if chan_id not in cur_data_dict:
cur_data_dict[chan_id] = {'tracesInfo': [], cur_data_dict[chan_id] = {'tracesInfo': [],
'samplerate': samplerate} 'samplerate': samplerate,
'visible': True}
if trace.stats.npts == 0: if trace.stats.npts == 0:
# this trace isn't available to prevent bug when creating memmap # this trace isn't available to prevent bug when creating memmap
# with no data # with no data
......
from typing import Dict, List, Union 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
...@@ -24,7 +24,8 @@ main window. ...@@ -24,7 +24,8 @@ main window.
""" """
TOTAL_ROW = 20 TOTAL_ROW = 20
COL = {'sel': 0, 'name': 1, 'dataType': 2, 'IDs': 3, 'edit': 4, 'clr': 5} COL = {'sel': 0, 'name': 1, 'dataType': 2,
'preferredSOHs': 3, 'edit': 4, 'clr': 5}
class InputDialog(QDialog): class InputDialog(QDialog):
...@@ -81,7 +82,8 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -81,7 +82,8 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
""" """
avail_data_types: list of available data types in DB avail_data_types: list of available data types in DB
""" """
self.avail_data_types: List[str] = [] self.avail_data_types: List[str] = self.get_data_types(
include_default=False)
""" """
curr_row: current row curr_row: current row
""" """
...@@ -241,7 +243,6 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -241,7 +243,6 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
) )
self.soh_list_table_widget.setRowCount(TOTAL_ROW) self.soh_list_table_widget.setRowCount(TOTAL_ROW)
self.avail_data_types = self.get_data_types()
for row_idx in range(TOTAL_ROW): for row_idx in range(TOTAL_ROW):
self.add_row(row_idx) self.add_row(row_idx)
self.update_data_table_widget_items() self.update_data_table_widget_items()
...@@ -285,7 +286,8 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -285,7 +286,8 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
row_idx, COL['dataType'], data_type_combo_box) row_idx, COL['dataType'], data_type_combo_box)
soh_list_item = QtWidgets.QTableWidgetItem() soh_list_item = QtWidgets.QTableWidgetItem()
self.soh_list_table_widget.setItem(row_idx, COL['IDs'], soh_list_item) self.soh_list_table_widget.setItem(
row_idx, COL['preferredSOHs'], soh_list_item)
edit_button = QtWidgets.QPushButton(self, text='EDIT') edit_button = QtWidgets.QPushButton(self, text='EDIT')
edit_button.clicked.connect(lambda arg: self.edit_soh_list(row_idx)) edit_button.clicked.connect(lambda arg: self.edit_soh_list(row_idx))
...@@ -325,7 +327,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -325,7 +327,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
self.soh_list_table_widget.cellWidget( self.soh_list_table_widget.cellWidget(
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['IDs']).setText(r['IDs']) count, COL['preferredSOHs']).setText(r['preferredSOHs'])
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)
...@@ -343,7 +345,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -343,7 +345,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
data_type_combobox = self.soh_list_table_widget.cellWidget( data_type_combobox = self.soh_list_table_widget.cellWidget(
row_idx, COL['dataType']) row_idx, COL['dataType'])
soh_list_item = self.soh_list_table_widget.item( soh_list_item = self.soh_list_table_widget.item(
row_idx, COL['IDs']) row_idx, COL['preferredSOHs'])
clear_widget = self.soh_list_table_widget.cellWidget( clear_widget = self.soh_list_table_widget.cellWidget(
row_idx, COL['clr']) row_idx, COL['clr'])
return (soh_list_name_item, return (soh_list_name_item,
...@@ -366,7 +368,8 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -366,7 +368,8 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
@QtCore.Slot() @QtCore.Slot()
def edit_soh_list(self, row_idx): def edit_soh_list(self, row_idx):
soh_list_item = self.soh_list_table_widget.item(row_idx, COL['IDs']) soh_list_item = self.soh_list_table_widget.item(row_idx,
COL['preferredSOHs'])
edit_dialog = InputDialog(text=soh_list_item.text()) edit_dialog = InputDialog(text=soh_list_item.text())
if edit_dialog.exec(): if edit_dialog.exec():
soh_list_item.setText(edit_dialog.get_input()) soh_list_item.setText(edit_dialog.get_input())
...@@ -430,6 +433,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -430,6 +433,7 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
def add_db_channels(self): def add_db_channels(self):
""" """
Add channels from DB to preferred channel list. Add channels from DB to preferred channel list.
A DB data_type is required in to retrieve its channels from DB
""" """
if not self.validate_row(check_data_type=True): if not self.validate_row(check_data_type=True):
return return
...@@ -550,9 +554,22 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -550,9 +554,22 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
""" """
if not self.validate_row(): if not self.validate_row():
return return
# call setFocus() to finish any QEditLine's editing or the editing not
# take affect
self.save_btn.setFocus()
sql_list = self.get_sql_list()
if sql_list is None:
return False
if len(sql_list) == 0:
self.parent.pref_soh_list = []
self.parent.pref_soh_list_name = ''
self.parent.pref_soh_list_data_type = 'Unknown'
if self.changed: if self.changed:
msg = ("All IDs in the database will be overwritten with " msg = ("All Preferred SOH IDs in the database will be "
"current IDs in the dialog.\nClick Cancel to stop updating " "overwritten with current Preferred SOH IDs in the dialog."
"\nClick Cancel to stop updating "
"database.") "database.")
result = QtWidgets.QMessageBox.question( result = QtWidgets.QMessageBox.question(
self, "Confirmation", msg, self, "Confirmation", msg,
...@@ -560,19 +577,10 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -560,19 +577,10 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
QtWidgets.QMessageBox.StandardButton.Cancel) QtWidgets.QMessageBox.StandardButton.Cancel)
if result == QtWidgets.QMessageBox.StandardButton.Cancel: if result == QtWidgets.QMessageBox.StandardButton.Cancel:
return False return False
sql_list = []
for row_idx in range(TOTAL_ROW):
sql = self.create_save_row_sql(row_idx)
if sql is not None:
sql_list.append(sql)
if len(sql_list) == 0:
self.parent.pref_soh_list = []
self.parent.pref_soh_list_name = ''
self.parent.pref_soh_list_data_type = 'Unknown'
ret = trunc_add_db('ChannelPrefer', sql_list) ret = trunc_add_db('ChannelPrefer', sql_list)
if ret is not True: if ret is not True:
display_tracking_info(self.parent, ret, LogType.ERROR) display_tracking_info(self.tracking_info_text_browser,
ret, LogType.ERROR)
self.parent.pref_soh_list = [ self.parent.pref_soh_list = [
t.strip() for t in self.soh_list_item.text().split(',')] t.strip() for t in self.soh_list_item.text().split(',')]
self.parent.pref_soh_list_name = self.soh_list_name_item.text().strip() self.parent.pref_soh_list_name = self.soh_list_name_item.text().strip()
...@@ -581,12 +589,73 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -581,12 +589,73 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
self.changed = False self.changed = False
return True return True
def create_save_row_sql(self, row_idx): def get_sql_list(self) -> Optional[List[str]]:
"""
Create list of sql to insert data to ChannelPrefer table in which:
+ If there is error it will mark it to return but continue to report
all errors of all rows.
+ Beside checking each row, primary key will be checked to make sure
the list_of_sql returned can be executed.
:return:
None if there are any error
Otherwise, list of sql
"""
sql_list = []
display_id_by_primary_key_dict = {}
has_error = False
for row_idx in range(TOTAL_ROW):
result = self.create_save_row_sql(row_idx)
if result is None:
has_error = True
continue
else:
primary_key, display_id, sql = result
if sql == '':
continue
sql_list.append(sql)
if primary_key not in display_id_by_primary_key_dict:
display_id_by_primary_key_dict[primary_key] = []
display_id_by_primary_key_dict[primary_key].append(display_id)
if self.check_primary_key_duplicated(display_id_by_primary_key_dict):
return
if has_error:
return
return sql_list
def check_primary_key_duplicated(self, display_id_by_primary_key_dict) \
-> bool:
"""
Check if there are any primary keys duplicated.
:param display_id_by_primary_key_dict: dictionary with primary key as
key and list of display row id for that primary key as value.
:return: True if duplicated, False otherwise
"""
duplicated = False
duplicated_primary_keys = [
pk for pk in display_id_by_primary_key_dict
if len(display_id_by_primary_key_dict[pk]) > 1]
if duplicated_primary_keys:
duplicated = True
for pk in duplicated_primary_keys:
msg = (f"Name '{pk}' is duplicated in rows "
f"{', '.join(display_id_by_primary_key_dict[pk])}.\n"
f"Please change Name '{pk}' to make Name unique.")
QtWidgets.QMessageBox.information(self, "Missing info", msg)
return duplicated
def create_save_row_sql(self, row_idx: int) \
-> Optional[Tuple[str, str, str]]:
""" """
Read info from the row with index row_idx to create insert sql. Read info from the row with index row_idx to create insert sql.
:param row_idx: int - index of a row :param row_idx: int - index of a row
:return: str - insert sql with the row's info :return:
None: has error
Tuple of:
primary key of the row
display_id of the row
sql to insert row data to ChannelPrefer table
""" """
current = 1 if self.soh_list_table_widget.cellWidget( current = 1 if self.soh_list_table_widget.cellWidget(
row_idx, COL['sel']).isChecked() else 0 row_idx, COL['sel']).isChecked() else 0
...@@ -594,16 +663,29 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -594,16 +663,29 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
row_idx, COL['name']).text() row_idx, COL['name']).text()
data_type = self.soh_list_table_widget.cellWidget( data_type = self.soh_list_table_widget.cellWidget(
row_idx, COL['dataType']).currentText() row_idx, COL['dataType']).currentText()
idx = self.soh_list_table_widget.item( preferred_sohs = self.soh_list_table_widget.item(
row_idx, COL['IDs']).text() row_idx, COL['preferredSOHs']).text()
if idx.strip() == '': if preferred_sohs.strip() == '' and name.strip() == '':
return '', '', ''
display_id = row_idx + 1
if preferred_sohs.strip() == '' and name.strip() != '':
msg = f"Please add Preferred SOH IDs for row {display_id}."
QtWidgets.QMessageBox.information(self, "Missing info", msg)
return
if name.strip() == '' and preferred_sohs.strip() != '':
msg = f"Please add Name for row {display_id}."
QtWidgets.QMessageBox.information(self, "Missing info", msg)
return return
if name.strip() == '' and idx.strip() != '': if preferred_sohs.strip() != '' and data_type == '':
msg = f"Please add Name for row {row_idx}." msg = f"Please add a data type for row {display_id}."
QtWidgets.QMessageBox.information(self, "Missing info", msg) QtWidgets.QMessageBox.information(self, "Missing info", msg)
return return
return (f"INSERT INTO ChannelPrefer (name, IDs, dataType, current)" sql = (f"INSERT INTO ChannelPrefer (name, preferredSOHs, dataType, "
f"VALUES ('{name}', '{idx}', '{data_type}', {current})") f"current) VALUES "
f"('{name}', '{preferred_sohs}', '{data_type}', {current})")
primary_key = name
return primary_key, str(display_id), sql
@QtCore.Slot() @QtCore.Slot()
def save_add_to_main_and_close(self): def save_add_to_main_and_close(self):
...@@ -613,21 +695,31 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -613,21 +695,31 @@ class ChannelPreferDialog(OneWindowAtATimeDialog):
""" """
if not self.save(): if not self.save():
return return
if self.parent.pref_soh_list_name == '':
msg = "Please select a row to add to Main Window."
QtWidgets.QMessageBox.information(self, "Missing info", msg)
return
self.parent.curr_pref_soh_list_name_txtbox.setText( self.parent.curr_pref_soh_list_name_txtbox.setText(
self.parent.pref_soh_list_name) self.parent.pref_soh_list_name)
self.parent.all_soh_chans_check_box.setChecked(False) self.parent.all_soh_chans_check_box.setChecked(False)
self.close() self.close()
@staticmethod @staticmethod
def get_data_types(): def get_data_types(include_default: bool = True):
""" """
Get list of data types from DB. Get list of data types from DB.
:param include_default: flag indicate if Default data type should be
included or not
:return: [str, ] - list of data types :return: [str, ] - list of data types
""" """
data_type_rows = execute_db( data_type_rows = execute_db(
'SELECT * FROM DataTypes ORDER BY dataType ASC') 'SELECT * FROM DataTypes ORDER BY dataType ASC')
return [d[0] for d in data_type_rows] if include_default:
return [d[0] for d in data_type_rows]
else:
return [d[0] for d in data_type_rows
if d[0] != 'Default']
@staticmethod @staticmethod
def get_db_channels(data_type) -> List[str]: def get_db_channels(data_type) -> List[str]:
...@@ -652,6 +744,6 @@ class ChannelPreferDialog(OneWindowAtATimeDialog): ...@@ -652,6 +744,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, IDs, dataType, current FROM ChannelPrefer " "SELECT name, preferredSOHs, dataType, current FROM ChannelPrefer "
" ORDER BY name ASC") " ORDER BY name ASC")
return id_rows return id_rows
...@@ -64,17 +64,13 @@ class ChannelDialog(UiDBInfoDialog): ...@@ -64,17 +64,13 @@ class ChannelDialog(UiDBInfoDialog):
:param fk: bool: True if there is a foreign constrain that prevents the :param fk: bool: True if there is a foreign constrain that prevents the
row to be deleted row to be deleted
""" """
self.add_widget(None, row_idx, 0) # No. self.add_row_number_button(row_idx) # No.
self.add_widget(self.database_rows, row_idx, 1, self.add_widget(row_idx, 1, foreign_key=fk) # chanID
foreign_key=fk) # chanID self.add_widget(row_idx, 2) # label
self.add_widget(self.database_rows, row_idx, 2) # label self.add_widget(row_idx, 3, choices=self.param_choices)
self.add_widget(self.database_rows, row_idx, 3, self.add_widget(row_idx, 4, field_name='convertFactor')
choices=self.param_choices) self.add_widget(row_idx, 5) # unit
self.add_widget(self.database_rows, row_idx, 4, self.add_widget(row_idx, 6, range_values=[0, 5]) # fixPoint
field_name='convertFactor')
self.add_widget(self.database_rows, row_idx, 5) # unit
self.add_widget(self.database_rows, row_idx, 6,
range_values=[0, 5]) # fixPoint
self.add_delete_button_to_row(row_idx, fk) self.add_delete_button_to_row(row_idx, fk)
def get_data_type_from_selector(self): def get_data_type_from_selector(self):
...@@ -138,7 +134,7 @@ class ChannelDialog(UiDBInfoDialog): ...@@ -138,7 +134,7 @@ class ChannelDialog(UiDBInfoDialog):
# case where the exit button is pressed. # case where the exit button is pressed.
self.data_type_combo_box.setCurrentText(self.data_type) self.data_type_combo_box.setCurrentText(self.data_type)
def get_data_list(self): def get_database_rows(self):
""" """
Get list of data to fill self.data_table_widgets' content Get list of data to fill self.data_table_widgets' content
""" """
......
...@@ -25,11 +25,11 @@ class DataTypeDialog(UiDBInfoDialog): ...@@ -25,11 +25,11 @@ class DataTypeDialog(UiDBInfoDialog):
:param fk: bool: True if there is a foreign constrain that prevents the :param fk: bool: True if there is a foreign constrain that prevents the
row to be deleted row to be deleted
""" """
self.add_widget(None, row_idx, 0) # No. self.add_row_number_button(row_idx) # No.
self.add_widget(self.database_rows, row_idx, 1, foreign_key=fk) self.add_widget(row_idx, 1, foreign_key=fk)
self.add_delete_button_to_row(row_idx, fk) self.add_delete_button_to_row(row_idx, fk)
def get_data_list(self): def get_database_rows(self):
""" """
Get list of data to fill self.data_table_widgets' content Get list of data to fill self.data_table_widgets' content
""" """
......
...@@ -3,10 +3,13 @@ from __future__ import annotations ...@@ -3,10 +3,13 @@ from __future__ import annotations
from typing import Dict, Optional, List, Tuple from typing import Dict, Optional, List, Tuple
from PySide6 import QtWidgets, QtGui, QtCore from PySide6 import QtWidgets, QtGui, QtCore
from PySide6.QtCore import Signal
from PySide6.QtGui import QCloseEvent from PySide6.QtGui import QCloseEvent
from PySide6.QtWidgets import QMessageBox, QWidget from PySide6.QtWidgets import QMessageBox, QWidget
from sohstationviewer.database.process_db import execute_db from sohstationviewer.database.process_db import execute_db
from sohstationviewer.view.db_config.value_color_helper.value_color_edit \
import ValueColorEdit
from sohstationviewer.view.util.one_instance_at_a_time import \ from sohstationviewer.view.util.one_instance_at_a_time import \
OneWindowAtATimeDialog OneWindowAtATimeDialog
...@@ -35,7 +38,12 @@ def set_widget_color(widget, changed=False, read_only=False): ...@@ -35,7 +38,12 @@ def set_widget_color(widget, changed=False, read_only=False):
:param read_only: True: grey Text, blue background (priority) :param read_only: True: grey Text, blue background (priority)
if both flags are False: black Text, white blackground if both flags are False: black Text, white blackground
""" """
palette = QtGui.QPalette() if isinstance(widget, ValueColorEdit):
# ValueColorEdit handles its own styling, so we don't need to style
# it here.
return
palette = widget.palette()
if read_only: if read_only:
# grey text # grey text
...@@ -55,6 +63,11 @@ def set_widget_color(widget, changed=False, read_only=False): ...@@ -55,6 +63,11 @@ def set_widget_color(widget, changed=False, read_only=False):
# red text # red text
palette.setColor(QtGui.QPalette.ColorRole.Text, palette.setColor(QtGui.QPalette.ColorRole.Text,
QtGui.QColor(255, 0, 0)) QtGui.QColor(255, 0, 0))
# 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))
else: else:
try: try:
if widget.isReadOnly(): if widget.isReadOnly():
...@@ -124,7 +137,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -124,7 +137,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
""" """
is_row_changed_array: a bit array that store whether a row is changed. is_row_changed_array: a bit array that store whether a row is changed.
""" """
self.is_row_changed_array: List[bool] = [] self.changes_array: List[List[bool]] = []
""" """
queued_row_delete_sqls: a dictionary of delete SQL statements that are queued_row_delete_sqls: a dictionary of delete SQL statements that are
executed when changes are saved to the database. Map a row id in the executed when changes are saved to the database. Map a row id in the
...@@ -182,50 +195,56 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -182,50 +195,56 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
0, 0, 1, 1) 0, 0, 1, 1)
main_layout.addLayout(self.bottom_layout) main_layout.addLayout(self.bottom_layout)
def add_widget(self, data_list, row_idx, col_idx, foreign_key=False, def add_row_number_button(self, row_idx: int) -> None:
choices=None, range_values=None, field_name='', """
place_holder_text='') -> str: Add a button that shows the row number to a row of the data table. The
""" button will be on the left of the row.
Add a cell widget at a given row and column in the data_table_widget. :param row_idx: the index of the row to insert the button
"""
:param data_list: list of data to be display in the data_table_widget. row_button = QtWidgets.QPushButton(str(row_idx))
data_list is None when that column shows the row number. set_widget_color(row_button)
:param row_idx: index of row row_button.setFixedWidth(40)
:param col_idx: index of column row_button.clicked.connect(
:param foreign_key: True if there is a foreign constrain that prevents lambda: self.row_number_clicked(row_button))
the row to be deleted => set readonly of cell=True self.data_table_widget.setCellWidget(row_idx, 0, row_button)
:param choices: List of choices to add to a drop down list. If
it is available, QComboBox will be assigned for the widget. def create_widget(self, widget_content: str,
:param range_values: list consists of min and max values. If choices: Optional[List] = None,
it is available, QSpinbox will be assigned for the widget range_values: Optional[List] = None,
If no choices or range available, QLineEdit will be assigned for the field_name: str = '', **kwargs
widget. ) -> Tuple[QWidget, Signal]:
:param field_name: name of the column in database. If field_name= """
'convertFactor', add validator to limit precision to 6 decimal Create a widget with the given content. The actual type of widget
points created depends on the arguments passed in. By default, the created
:param place_holder_text: the text displayed in QTextEdit as a hint widget will be a QLineEdit.
for user
:return text: value of the current field in string :param widget_content: the content of the created widget
:param choices: list of choices to add to a drop-down list. If this is
available, the created widget will be a QComboBox.
:param range_values: list consists of min and max values. If this is
available, the created widget will be a QSpinbox.
:param field_name: name of the column in database.
If field_name is 'convertFactor', a validator will be added to the
created widget, limiting the input to numbers in the range 0.0-5.0
with at most 6 decimal digits.
If field_name is 'description', the created widget will be a
QTextEdit.
:return:
- a widget whose type depends on the arguments passed in.
- the QT signal that is emitted when the content of the returned
widget is modified
""" """
if data_list is None: widget, change_signal = None, None
text = str(row_idx) # row number if choices is None and range_values is None:
else:
text = (str(data_list[row_idx][col_idx - 1])
if row_idx < len(data_list) else '')
if data_list is None:
widget = QtWidgets.QPushButton(text)
elif choices is None and range_values is None:
if field_name == 'description': if field_name == 'description':
widget = QtWidgets.QTextEdit() widget = QtWidgets.QTextEdit()
height = 12 height = 12
for line in text.split('\n'): for line in widget_content.split('\n'):
widget.append(line) widget.append(line)
height += 16 height += 16
widget.setFixedHeight(height) widget.setFixedHeight(height)
else: else:
widget = QtWidgets.QLineEdit(text) widget = QtWidgets.QLineEdit(widget_content)
widget.setPlaceholderText(place_holder_text)
if field_name == 'convertFactor': if field_name == 'convertFactor':
# precision=6 # precision=6
validator = QtGui.QDoubleValidator(0.0, 5.0, 6) validator = QtGui.QDoubleValidator(0.0, 5.0, 6)
...@@ -233,46 +252,72 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -233,46 +252,72 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
QtGui.QDoubleValidator.Notation.StandardNotation QtGui.QDoubleValidator.Notation.StandardNotation
) )
widget.setValidator(validator) widget.setValidator(validator)
if text == '': if widget_content == '':
widget.setText('1') widget.setText('1')
change_signal = widget.textChanged
elif choices: elif choices:
widget = QtWidgets.QComboBox() widget = QtWidgets.QComboBox()
widget.addItems(choices) widget.addItems(choices)
widget.setCurrentText(text) widget.setCurrentText(widget_content)
change_signal = widget.currentTextChanged
elif range_values: elif range_values:
widget = QtWidgets.QSpinBox() widget = QtWidgets.QSpinBox()
widget.setMinimum(range_values[0]) widget.setMinimum(range_values[0])
widget.setMaximum(range_values[1]) widget.setMaximum(range_values[1])
if text in ["", None, 'None']: if widget_content in ["", None, 'None']:
widget.setValue(range_values[0]) widget.setValue(range_values[0])
else: else:
widget.setValue(int(text)) widget.setValue(int(widget_content))
change_signal = widget.valueChanged
if data_list is None: if widget is None:
set_widget_color(widget) raise
widget.setFixedWidth(40) return widget, change_signal
widget.clicked.connect(
lambda: self.row_number_clicked(widget)) def add_widget(self, row_idx, col_idx, foreign_key=False, choices=None,
elif foreign_key: range_values=None, plot_type=None, field_name=''
) -> QWidget:
"""
Add a cell widget at a given row and column in the data_table_widget.
:param row_idx: index of row
:param col_idx: index of column
:param foreign_key: True if there is a foreign constrain that prevents
the row to be deleted => set readonly of cell=True
:param choices: List of choices to add to a drop down list. If
it is available, QComboBox will be assigned for the widget.
:param range_values: list consists of min and max values. If
it is available, QSpinbox will be assigned for the widget
:param plot_type: the plot type associated with the value colors
contain in the created widget. If it is available, ValueColorEdit
will be assigned for the widget
If no choices, range, or plot type available, QLineEdit will be
assigned for the widget.
:param field_name: name of the column in database. If field_name=
'convertFactor', add validator to limit precision to 6 decimal
points
:return: value stored in created widget as a string
"""
widget_content = (str(self.database_rows[row_idx][col_idx - 1])
if row_idx < len(self.database_rows) else '')
widget, change_signal = self.create_widget(
widget_content, choices=choices, range_values=range_values,
field_name=field_name, plot_type=plot_type, row_idx=row_idx
)
if foreign_key:
set_widget_color(widget, read_only=True) set_widget_color(widget, read_only=True)
else: else:
new = False if row_idx < len(data_list) else True new = False if row_idx < len(self.database_rows) else True
set_widget_color(widget, read_only=False, changed=new) set_widget_color(widget, read_only=False, changed=new)
change_signal = None
if choices is None and range_values is None:
change_signal = widget.textChanged
elif choices:
change_signal = widget.currentTextChanged
elif range_values:
change_signal = widget.valueChanged
change_signal.connect( change_signal.connect(
lambda changed_text: lambda changed_text:
self.on_cell_input_change(changed_text, widget) self.on_cell_input_change(changed_text, widget)
) )
self.data_table_widget.setCellWidget(row_idx, col_idx, widget) self.data_table_widget.setCellWidget(row_idx, col_idx, widget)
if field_name == 'description': if field_name == 'description':
self.data_table_widget.resizeRowToContents(row_idx) self.data_table_widget.resizeRowToContents(row_idx)
return text return widget
@QtCore.Slot() @QtCore.Slot()
def row_number_clicked(self, widget): def row_number_clicked(self, widget):
...@@ -353,7 +398,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -353,7 +398,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
self.data_type = self.data_type_combo_box.currentText() self.data_type = self.data_type_combo_box.currentText()
self.update_data_table_widget_items() self.update_data_table_widget_items()
def get_data_list(self) -> list: def get_database_rows(self) -> list:
""" """
Get list of data to fill self.data_table_widgets' content. Should be Get list of data to fill self.data_table_widgets' content. Should be
implemented by all children. implemented by all children.
...@@ -364,12 +409,12 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -364,12 +409,12 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
def update_data_table_widget_items(self): def update_data_table_widget_items(self):
""" """
Create widget cell for self.data_table_widget based on self.data_list Create widget cell for self.data_table_widget based on
for data to be displayed. self.database_rows for data to be displayed.
Each row will be check for foreign_key constrain to decide readonly Each row will be check for foreign_key constrain to decide readonly
status. status.
If self.data_list is empty, call clear_first_row to create a row with If self.database_rows is empty, call clear_first_row to create a row
all necessary widget cells but empty. with all necessary widget cells but empty.
""" """
# When it comes to deleted rows, we can either reset them manually or # When it comes to deleted rows, we can either reset them manually or
# remove them altogether. We chose to remove them using this line # remove them altogether. We chose to remove them using this line
...@@ -377,8 +422,11 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -377,8 +422,11 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
# matter because we don't really expect to show more than 100 channels # matter because we don't really expect to show more than 100 channels
# at a time. # at a time.
self.data_table_widget.setRowCount(0) self.data_table_widget.setRowCount(0)
self.database_rows = self.get_data_list() self.database_rows = self.get_database_rows()
self.is_row_changed_array = [False] * len(self.database_rows) self.changes_array = [
[False] * len(self.database_rows[0])
for _ in range(len(self.database_rows))
]
row_count = len(self.database_rows) row_count = len(self.database_rows)
row_count = 1 if row_count == 0 else row_count row_count = 1 if row_count == 0 else row_count
self.data_table_widget.setRowCount(row_count) self.data_table_widget.setRowCount(row_count)
...@@ -405,7 +453,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -405,7 +453,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
self.data_table_widget.scrollToBottom() self.data_table_widget.scrollToBottom()
self.data_table_widget.repaint() # to show row's header self.data_table_widget.repaint() # to show row's header
self.data_table_widget.cellWidget(row_position, 1).setFocus() self.data_table_widget.cellWidget(row_position, 1).setFocus()
self.is_row_changed_array.append(True) self.changes_array.append([True] * len(self.database_rows[0]))
def remove_row(self, remove_row_idx): def remove_row(self, remove_row_idx):
""" """
...@@ -439,7 +487,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -439,7 +487,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
set_widget_color(cell_widget, changed=changed) set_widget_color(cell_widget, changed=changed)
else: else:
changed = True changed = True
self.is_row_changed_array[row_idx] = changed self.changes_array[row_idx][col_idx - 1] = changed
def check_data_foreign_key(self, val): def check_data_foreign_key(self, val):
""" """
...@@ -547,12 +595,12 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -547,12 +595,12 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
# Because self.changed_rows only track changes to row content and not # Because self.changed_rows only track changes to row content and not
# deletions, we want to remove the deleted row's ID from it. # deletions, we want to remove the deleted row's ID from it.
if row_idx < len(self.database_rows): if row_idx < len(self.database_rows):
self.is_row_changed_array[row_idx] = False self.changes_array[row_idx] = [False] * len(self.database_rows[0])
else: else:
# Because rows not in the database are removed from the table when # Because rows not in the database are removed from the table when
# deleted, we delete the value that tracks whether they changed # deleted, we delete the value that tracks whether they changed
# when they are deleted. # when they are deleted.
self.is_row_changed_array.pop(row_idx) self.changes_array.pop(row_idx)
if row_idx >= len(self.database_rows): if row_idx >= len(self.database_rows):
# Deleting a row that is not in the database. Because no rows that # Deleting a row that is not in the database. Because no rows that
...@@ -602,7 +650,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -602,7 +650,7 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
def data_type_changed(self): def data_type_changed(self):
""" """
Load new self.data_list when self.data_type_combo_box's value is Load new self.database_rows when self.data_type_combo_box's value is
changed changed
""" """
pass pass
...@@ -630,13 +678,13 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -630,13 +678,13 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
False otherwise False otherwise
""" """
return (len(self.queued_row_delete_sqls) != 0 or return (len(self.queued_row_delete_sqls) != 0 or
any(self.is_row_changed_array)) any([any(row_changes) for row_changes in self.changes_array]))
def untrack_changes(self): def untrack_changes(self):
""" """
Untrack all changes that have been made. Untrack all changes that have been made.
""" """
self.is_row_changed_array = [] self.changes_array = []
self.queued_row_delete_sqls = {} self.queued_row_delete_sqls = {}
def validate_row(self, row_id: int, row_content: List) -> Tuple[bool, str]: def validate_row(self, row_id: int, row_content: List) -> Tuple[bool, str]:
...@@ -693,9 +741,9 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -693,9 +741,9 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
no issue no issue
""" """
changed_row_ids = [idx changed_row_ids = [idx
for (idx, is_changed) for (idx, is_cell_changed_in_row_array)
in enumerate(self.is_row_changed_array) in enumerate(self.changes_array)
if is_changed] if any(is_cell_changed_in_row_array)]
for row_id in changed_row_ids: for row_id in changed_row_ids:
row_content = self.get_row_inputs(row_id) row_content = self.get_row_inputs(row_id)
is_row_valid, msg = self.validate_row(row_id, row_content) is_row_valid, msg = self.validate_row(row_id, row_content)
...@@ -737,9 +785,9 @@ class UiDBInfoDialog(OneWindowAtATimeDialog): ...@@ -737,9 +785,9 @@ class UiDBInfoDialog(OneWindowAtATimeDialog):
return False return False
changed_row_ids = [idx changed_row_ids = [idx
for (idx, is_changed) for (idx, is_cell_changed_in_row_array)
in enumerate(self.is_row_changed_array) in enumerate(self.changes_array)
if is_changed] if any(is_cell_changed_in_row_array)]
for row_id in changed_row_ids: for row_id in changed_row_ids:
if row_id < len(self.database_rows): if row_id < len(self.database_rows):
current_primary_key = self.database_rows[row_id][0] current_primary_key = self.database_rows[row_id][0]
......
...@@ -6,7 +6,7 @@ from typing import Optional, Dict ...@@ -6,7 +6,7 @@ from typing import Optional, Dict
from PySide6 import QtWidgets from PySide6 import QtWidgets
from PySide6.QtWidgets import QWidget, QDialog, QLineEdit from PySide6.QtWidgets import QWidget, QDialog, QLineEdit
from sohstationviewer.view.util.plot_func_names import plot_functions from sohstationviewer.view.util.plot_type_info import plot_types
from sohstationviewer.database.process_db import execute_db from sohstationviewer.database.process_db import execute_db
from sohstationviewer.database.extract_data import ( from sohstationviewer.database.extract_data import (
...@@ -39,7 +39,7 @@ class EditSingleParamDialog(QDialog): ...@@ -39,7 +39,7 @@ class EditSingleParamDialog(QDialog):
# parameter's plot type which decides the shape of the plot # parameter's plot type which decides the shape of the plot
self.plot_type_cbo_box = QtWidgets.QComboBox(self) self.plot_type_cbo_box = QtWidgets.QComboBox(self)
self.plot_type_cbo_box.addItems([""] + list(plot_functions.keys())) self.plot_type_cbo_box.addItems([""] + list(plot_types.keys()))
# value color in black mode # value color in black mode
self.value_colorb_widget = QLineEdit(self) self.value_colorb_widget = QLineEdit(self)
......
...@@ -3,20 +3,22 @@ param_dialog.py ...@@ -3,20 +3,22 @@ param_dialog.py
GUI to add/dit/remove params GUI to add/dit/remove params
NOTE: Cannot remove or change params that are already used for channels. NOTE: Cannot remove or change params that are already used for channels.
""" """
from typing import Optional, List, Tuple
from PySide6 import QtWidgets, QtCore from PySide6 import QtWidgets, QtCore
from PySide6.QtCore import Qt from PySide6.QtCore import Qt, Signal
from PySide6.QtWidgets import QComboBox, QWidget from PySide6.QtWidgets import QComboBox, QWidget
from sohstationviewer.conf.constants import ColorMode, ALL_COLOR_MODES from sohstationviewer.conf.constants import ColorMode, ALL_COLOR_MODES
from sohstationviewer.view.util.plot_func_names import plot_functions from sohstationviewer.view.db_config.value_color_helper.value_color_edit \
import ValueColorEdit
from sohstationviewer.view.util.plot_type_info import plot_types
from sohstationviewer.view.db_config.db_config_dialog import ( from sohstationviewer.view.db_config.db_config_dialog import (
UiDBInfoDialog, UiDBInfoDialog,
) )
from sohstationviewer.database.process_db import execute_db from sohstationviewer.database.process_db import execute_db
from sohstationviewer.conf.dbSettings import dbConf
class ParamDialog(UiDBInfoDialog): class ParamDialog(UiDBInfoDialog):
def __init__(self, parent: QWidget, color_mode: ColorMode) -> None: def __init__(self, parent: QWidget, color_mode: ColorMode) -> None:
...@@ -26,9 +28,9 @@ class ParamDialog(UiDBInfoDialog): ...@@ -26,9 +28,9 @@ class ParamDialog(UiDBInfoDialog):
""" """
self.color_mode = color_mode self.color_mode = color_mode
self.require_valuecolors_plottypes = [ self.plot_types_with_value_colors = [
p for p in plot_functions.keys() p for p in plot_types.keys()
if 'ValueColors' in plot_functions[p]['description']] if 'pattern' in plot_types[p]]
super().__init__( super().__init__(
parent, parent,
['No.', 'Param', 'Plot Type', 'ValueColors', 'Height '], ['No.', 'Param', 'Plot Type', 'ValueColors', 'Height '],
...@@ -47,6 +49,69 @@ class ParamDialog(UiDBInfoDialog): ...@@ -47,6 +49,69 @@ class ParamDialog(UiDBInfoDialog):
self.setWindowTitle("Edit/Add/Delete Parameters") self.setWindowTitle("Edit/Add/Delete Parameters")
self.add_color_selector(color_mode) self.add_color_selector(color_mode)
def create_widget(self, widget_content: str,
choices: Optional[List] = None,
range_values: Optional[List] = None,
plot_type: Optional[str] = None, field_name: str = '',
row_idx: int = -1, **kwargs) -> Tuple[QWidget, Signal]:
"""
Create a widget with the given content. The actual type of widget
created depends on the arguments passed in. This method delegates to
the parent's implementation unless plot_type is not None.
:param widget_content: the content of the created widget
:param choices: list of choices to add to a drop-down list. If this is
available, the created widget will be a QComboBox.
:param range_values: list consists of min and max values. If this is
available, the created widget will be a QSpinbox.
:param plot_type: the plot type associated with the value colors
contain in the created widget. If this is available, the created
widget will be a ValueColorEdit.
:param field_name: name of the column in database.
If field_name is 'convertFactor', a validator will be added to the
created widget, limiting the input to numbers in the range 0.0-5.0
with at most 6 decimal digits.
If field_name is 'description', the created widget will be a
QTextEdit.
:param row_idx: the index of the row the created widget will be placed
in. Used to determine the original plot type contained in the row.
:return:
- a widget whose type depends on the arguments passed in.
- the QT signal that is emitted when the content of the returned
widget is modified
"""
if plot_type is not None:
# Rows that are not in the database do not have a plot type, so we
# consider the passed in plot type the original plot type. This
# choice was made to allow for the easiest implementation of the
# intended behavior.
original_plot_type = (
self.database_rows[row_idx][1]
if row_idx < len(self.database_rows)
else plot_type
)
# A string that is known to not be a value colors string. Only
# used to make sure that the ValueColorEdit created later is forced
# to use the default value colors for the passed in plot type, so
# the actual string does not matter.
BAD_VALUE_COLORS = 'VC'
value_colors = (
widget_content
if original_plot_type == plot_type
else BAD_VALUE_COLORS
)
# We create a new ValueColorEdit and let the created widget decides
# what its content should be. This is far easier than managing the
# value we want the created widget to have.
widget = ValueColorEdit(self.data_table_widget, self.color_mode,
plot_type, value_colors)
change_signal = widget.value_color_edited
else:
widget, change_signal = super().create_widget(
widget_content, choices, range_values, field_name
)
return widget, change_signal
def add_color_selector(self, initial_color_mode: ColorMode) -> None: def add_color_selector(self, initial_color_mode: ColorMode) -> None:
""" """
Add a color mode selector widget to the dialog. Add a color mode selector widget to the dialog.
...@@ -73,28 +138,24 @@ class ParamDialog(UiDBInfoDialog): ...@@ -73,28 +138,24 @@ class ParamDialog(UiDBInfoDialog):
:param fk: bool: True if there is a foreign constrain that prevents the :param fk: bool: True if there is a foreign constrain that prevents the
row to be deleted row to be deleted
""" """
self.add_widget(None, row_idx, 0) # No. self.add_row_number_button(row_idx) # No.
self.add_widget(self.database_rows, row_idx, 1, foreign_key=fk) self.add_widget(row_idx, 1, foreign_key=fk)
plot_type = self.add_widget( plot_type_selector = self.add_widget(
self.database_rows, row_idx, 2, row_idx, 2, choices=[''] + sorted(plot_types.keys())
choices=[''] + sorted(plot_functions.keys())) )
place_holder_text = "" # ValueColorEdit was designed so that each instance only handles one
if plot_type in self.require_valuecolors_plottypes: # plot type, so we need to create a new one whenever the user changes
place_holder_text = "Enter ValueColors..." # the plot type.
if plot_type == 'linesDots': plot_type_selector.currentTextChanged.connect(
place_holder_text = "Ex: L:G|D:W" lambda new_plot_type: self.add_widget(row_idx, 3,
elif plot_type == 'multiColorDots': plot_type=new_plot_type)
place_holder_text = "Ex: -1:_|0:R|2.3:Y|+2.3:G" )
elif plot_type == "upDownDots": self.add_widget(row_idx, 3, plot_type=plot_type_selector.currentText())
place_holder_text = "Ex: 1:R|0:Y"
elif plot_type == "dotForTime": self.add_widget(row_idx, 4, range_values=[0, 10])
place_holder_text = "Ex: G"
self.add_widget(self.database_rows, row_idx, 3,
place_holder_text=place_holder_text)
self.add_widget(self.database_rows, row_idx, 4, range_values=[0, 10])
self.add_delete_button_to_row(row_idx, fk) self.add_delete_button_to_row(row_idx, fk)
def get_data_list(self): def get_database_rows(self):
""" """
Get list of data to fill self.data_table_widgets' content Get list of data to fill self.data_table_widgets' content
""" """
...@@ -120,7 +181,7 @@ class ParamDialog(UiDBInfoDialog): ...@@ -120,7 +181,7 @@ class ParamDialog(UiDBInfoDialog):
return [ return [
self.data_table_widget.cellWidget(row_idx, 1).text().strip(), self.data_table_widget.cellWidget(row_idx, 1).text().strip(),
self.data_table_widget.cellWidget(row_idx, 2).currentText(), self.data_table_widget.cellWidget(row_idx, 2).currentText(),
self.data_table_widget.cellWidget(row_idx, 3).text().strip(), self.data_table_widget.cellWidget(row_idx, 3).value_color_str,
int(self.data_table_widget.cellWidget(row_idx, 4).value()) int(self.data_table_widget.cellWidget(row_idx, 4).value())
] ]
...@@ -147,11 +208,11 @@ class ParamDialog(UiDBInfoDialog): ...@@ -147,11 +208,11 @@ class ParamDialog(UiDBInfoDialog):
return False, err_msg return False, err_msg
if value_colors_string != "": if value_colors_string != "":
if plot_type in self.require_valuecolors_plottypes: if plot_type in self.plot_types_with_value_colors:
value_colors = value_colors_string.split("|") value_colors = value_colors_string.split("|")
for vc in value_colors: for vc in value_colors:
vc = vc.strip() vc = vc.strip()
plot_type_info = dbConf.get(plot_type) plot_type_info = plot_types.get(plot_type)
if plot_type_info is None: if plot_type_info is None:
continue continue
if not plot_type_info['pattern'].match(vc): if not plot_type_info['pattern'].match(vc):
......
...@@ -3,7 +3,7 @@ GUI to view the types of plotting and their descriptions ...@@ -3,7 +3,7 @@ GUI to view the types of plotting and their descriptions
NOTE: plot's types are defined in __init__.plot_functions NOTE: plot's types are defined in __init__.plot_functions
""" """
from sohstationviewer.view.util.plot_func_names import plot_functions from sohstationviewer.view.util.plot_type_info import plot_types
from sohstationviewer.view.db_config.db_config_dialog import UiDBInfoDialog from sohstationviewer.view.db_config.db_config_dialog import UiDBInfoDialog
...@@ -23,14 +23,13 @@ class PlotTypeDialog(UiDBInfoDialog): ...@@ -23,14 +23,13 @@ class PlotTypeDialog(UiDBInfoDialog):
plot's types are based on plotting functions which are hard code plot's types are based on plotting functions which are hard code
so it's needed to set to True to prevent the row to be deleted so it's needed to set to True to prevent the row to be deleted
""" """
self.add_widget(None, row_idx, 0) # No. self.add_row_number_button(row_idx) # No.
self.add_widget(self.database_rows, row_idx, 1, foreign_key=True) self.add_widget(row_idx, 1, foreign_key=True)
self.add_widget(self.database_rows, row_idx, 2, foreign_key=True, self.add_widget(row_idx, 2, foreign_key=True, field_name='description')
field_name='description')
def get_data_list(self): def get_database_rows(self):
""" """
Get list of data to fill self.data_table_widgets' content Get list of data to fill self.data_table_widgets' content
""" """
return [[key, val['description']] return [[key, val['description']]
for key, val in plot_functions.items()] for key, val in plot_types.items()]
from .edit_value_color_dialog import * # noqa: F403, F401
from .line_dot_dialog import * # noqa: F403, F401
from .multi_color_dot_dialog import * # noqa: F403, F401
from .tri_color_lines_dialog import * # noqa: F403, F401
from .up_down_dialog import * # noqa: F403, F401
from .dot_for_time_dialog import * # noqa: F403, F401
from typing import Optional
from PySide6.QtWidgets import QWidget, QPushButton, QLabel
from sohstationviewer.view.db_config.value_color_helper. \
edit_value_color_dialog.edit_value_color_dialog import (
EditValueColorDialog, display_color,
)
class DotForTimeDialog(EditValueColorDialog):
def __init__(self, parent: Optional[QWidget], value_color_str: str):
"""
:param parent: the parent widget
:param value_color_str: string for value color to be saved in DB
"""
# Widget that allow user to add/edit down's color
self.select_color_button = QPushButton("Select Color")
# Widget to display down's color
self.color_label = QLabel()
self.color_label.setFixedWidth(30)
self.color_label.setAutoFillBackground(True)
super().__init__(parent, value_color_str)
self.setWindowTitle("Edit Dot For Time Plot's Colors")
def setup_ui(self):
self.main_layout.addWidget(QLabel('Dot Color'), 0, 0, 1, 1)
self.main_layout.addWidget(self.color_label, 0, 1, 1, 1)
self.main_layout.addWidget(self.select_color_button, 0, 2, 1, 1)
self.setup_complete_buttons(1)
def connect_signals(self) -> None:
self.select_color_button.clicked.connect(
lambda: self.on_select_color(self.color_label))
super().connect_signals()
def set_value(self):
"""
Change the corresponding color_labels's color according to the color
from value_color_str.
"""
if self.value_color_str == "":
return
vc_parts = self.value_color_str.split('|')
for vc_str in vc_parts:
obj_type, color = vc_str.split(':')
display_color(self.color_label, color)
def save_color(self):
"""
Create value_color_str from GUI's info and close the GUI with color
is the hex color got from color_labels' color
"""
color = self.color_label.palette().window().color().name()
self.value_color_str = f"Color:{color.upper()}"
self.accept()
...@@ -39,6 +39,9 @@ class EditValueColorDialog(QDialog): ...@@ -39,6 +39,9 @@ class EditValueColorDialog(QDialog):
def set_value(self): def set_value(self):
pass pass
def save_color(self):
pass
def setup_complete_buttons(self, row_total) -> None: def setup_complete_buttons(self, row_total) -> None:
""" """
:param row_total: total of rows to edit :param row_total: total of rows to edit
...@@ -48,7 +51,7 @@ class EditValueColorDialog(QDialog): ...@@ -48,7 +51,7 @@ class EditValueColorDialog(QDialog):
def connect_signals(self) -> None: def connect_signals(self) -> None:
self.cancel_btn.clicked.connect(self.close) self.cancel_btn.clicked.connect(self.close)
self.save_colors_btn.clicked.connect(self.on_save_color) self.save_colors_btn.clicked.connect(self.save_color)
def on_select_color(self, color_label: QtWidgets.QLabel): def on_select_color(self, color_label: QtWidgets.QLabel):
""" """
......
...@@ -6,7 +6,7 @@ from PySide6 import QtWidgets ...@@ -6,7 +6,7 @@ from PySide6 import QtWidgets
from PySide6.QtWidgets import QWidget from PySide6.QtWidgets import QWidget
from sohstationviewer.view.db_config.value_color_helper.\ from sohstationviewer.view.db_config.value_color_helper.\
edit_value_color_dialog.edit_value_color_dialog_super_class import \ edit_value_color_dialog.edit_value_color_dialog import \
EditValueColorDialog, display_color EditValueColorDialog, display_color
...@@ -84,17 +84,17 @@ class LineDotDialog(EditValueColorDialog): ...@@ -84,17 +84,17 @@ class LineDotDialog(EditValueColorDialog):
display_color(self.dot_color_label, color) display_color(self.dot_color_label, color)
self.dot_include_chkbox.setChecked(True) self.dot_include_chkbox.setChecked(True)
def on_save_color(self): def save_color(self):
""" """
Create value_color_str from GUI's info and close the GUI with color Create value_color_str from GUI's info and close the GUI with color
is the hex color got from color_labels' color is the hex color got from color_labels' color
""" """
line_color = self.line_color_label.palette().window().color().name() line_color = self.line_color_label.palette().window().color().name()
self.value_color_str = f"Line:{line_color}" self.value_color_str = f"Line:{line_color.upper()}"
if self.dot_include_chkbox.isChecked(): if self.dot_include_chkbox.isChecked():
dot_color = self.dot_color_label.palette().window().color().name() dot_color = self.dot_color_label.palette().window().color().name()
self.value_color_str += f"|Dot:{dot_color}" self.value_color_str += f"|Dot:{dot_color.upper()}"
self.close() self.accept()
if __name__ == '__main__': if __name__ == '__main__':
......