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 (12)
Showing
with 654 additions and 131 deletions
......@@ -16,3 +16,17 @@ The home button can be used to return to this page at any time.
+ [Search SOH n LOG](04%20_%20Search%20SOH%20n%20LOG.help.md)
+ [Select Waveforms](05%20_%20Select%20Waveforms.help.md)
+ [Select Mass Position](06%20_%20Select%20Mass%20Position.help.md)
+ [Select SOH](07%20_%20Select%20SOH.help.md)
+ [View-Edit Database Tables](07%20_%20View-Edit%20Database%20Tables.help.md)
+ [GPS Dialog](08%20_%20GPS%20Dialog.help.md)
+ [Read from Data Card](08%20_%20Read%20from%20Data%20Card.help.md)
+ [Search List of Directories](10%20_%20Search%20List%20of%20Directories.help.md)
......@@ -29,16 +29,23 @@ List all parameters available.
<img alt="Parameters Table" src="images/database_tables/parameters.png" width="590" />
<br />
+ Depend on the PlotType entered, ValueColors will be applied for the parameter or not. If ValueColors is available, there will be some hint for values to entered in grey color.
+ To add a new row to the table, click on ADD ROW button.
+ To save any changes to database, click on SAVE CHANGES. If ValueColors isn't applicable, the field will be cleared; otherwise it will be validated at this point.
+ To close the dialog, click on CLOSE. User will be asked to confirm closing if there have been any changes made.
+ Depend on the PlotType entered, ValueColors will be applied for the parameter or not. If ValueColors is available, there will be some hint for values to entered in grey color.
+ To add a new row to the table, click on ADD ROW button.
+ To save any changes to database, click on SAVE CHANGES. If ValueColors isn't applicable, the field will be cleared; otherwise it will be validated at this point.
+ To close the dialog, click on CLOSE. User will be asked to confirm closing if there have been any changes made.
+ Fields' meaning:
+ No.: Line number, start with 0.
+ No.: Line number, start with 0.
+ Param: Parameter's name.
+ Plot Type: Define how to plot the parameter. Description of Plot Type can be found in Plot Types Table.
+ ValueColors: mapping between values and colors or dot/line and color, or simply color of dots in the plots
+ Plot Type: Define how to plot the parameter. Description of Plot Type can be found in Plot Types Table.
+ ValueColors: mapping between values and colors or dot/line and color, or simply color of dots in the plots
+ Height: Height of plot in compare with other plots.
+ For each color mode available, a row in the database has a separate value for ValueColors. In order to change between the color mode, use the selector at the bottom right of the dialog. The color mode chosen when the Parameters table is opened is the one chosen in the main window.
<br />
<br />
<img alt="Parameters Table" src="images/database_tables/parameters_change_color_mode.jpeg" width="590" />
<br />
## Data Types Table
......
# Search List of Directories
---------------------------
---------------------------
## User Interface
The search bar is located above the list of directories in the main data
directory. When there is text in the search bar, a button that clears the
search will show up.
---------------------------
## How to use
Whenever the content of the search bar is updated, the list of current
directory will be filtered. The result is displayed above the directory list
as a list of directories that match the search text, prefaced with the text
"Found files:".
<br />
<br />
<img alt="Search results" src="images/search_directories/search_result.jpeg"
height="280" />
<br />
There are two methods to clear the search. The first one is to click on the
clear button that appears on the right edge of the search bar. The second one
is to delete the content of the search bar. Either way, the directory list is
reset and the search bar is cleared.
<br />
<br />
<img alt="Clear search" src="images/search_directories/clear_button_pressed.jpg"
height="280" />
<br />
documentation/images/database_tables/parameters_change_color_mode.jpeg

76.7 KiB

documentation/images/search_directories/clear_button_pressed.jpg

19.4 KiB

documentation/images/search_directories/search_result.jpeg

29.8 KiB

import sys
if sys.version_info.minor >= 8:
from typing import Literal
# waveform pattern
WF_1ST = 'A-HLM-V'
WF_2ND = 'HLN'
......@@ -45,6 +49,9 @@ TABLE_CONTENTS = "01 _ Table of Contents.help.md"
# name of search through all documents file
SEARCH_RESULTS = "Search Results.md"
# the list of all color modes
ALL_COLOR_MODES = {'B', 'W'}
# ================================================================= #
# PLOTTING CONSTANT
# ================================================================= #
......@@ -60,3 +67,12 @@ Z_ORDER = {'AXIS_SPINES': 0, 'CENTER_LINE': 1, 'LINE': 2, 'GAP': 3, 'DOT': 3}
# Distance from 'Hour' label to timestamp bar
HOUR_TO_TMBAR_D = 100
# ================================================================= #
# TYPING CONSTANT
# ================================================================= #
if sys.version_info.minor >= 8:
ColorMode = Literal['B', 'W']
else:
ColorMode = str
from typing import Dict
from sohstationviewer.conf.constants import ColorMode
from sohstationviewer.database.process_db import execute_db_dict, execute_db
from sohstationviewer.conf.dbSettings import dbConf
def get_chan_plot_info(org_chan_id, chan_info, data_type):
def get_chan_plot_info(org_chan_id: str, chan_info: Dict, data_type: str,
color_mode: ColorMode = 'B') -> Dict:
"""
Given chanID read from raw data file and detected dataType
Return plotting info from DB for that channel
......@@ -23,16 +26,19 @@ def get_chan_plot_info(org_chan_id, chan_info, data_type):
chan = 'Disk Usage?'
if dbConf['seisRE'].match(chan):
chan = 'SEISMIC'
o_sql = ("SELECT channel, plotType, height, unit, linkedChan,"
" convertFactor, label, fixPoint, valueColors "
"FROM Channels as C, Parameters as P")
# The valueColors for each color mode is stored in a separate column.
# 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' + color_mode
o_sql = (f"SELECT channel, plotType, height, unit, linkedChan,"
f" convertFactor, label, fixPoint, "
f"{value_colors_column} AS valueColors "
f"FROM Channels as C, Parameters as P")
if data_type == 'Unknown':
sql = f"{o_sql} WHERE channel='{chan}' and C.param=P.param"
else:
sql = (f"{o_sql} WHERE channel='{chan}' and C.param=P.param"
f" and dataType='{data_type}'")
# print("SQL:", sql)
chan_db_info = execute_db_dict(sql)
if len(chan_db_info) == 0:
......@@ -64,9 +70,21 @@ def get_chan_plot_info(org_chan_id, chan_info, data_type):
return chan_db_info[0]
def get_wf_plot_info(org_chan):
def get_wf_plot_info(org_chan: str) -> Dict:
"""
Get plotting information for waveform plots from the database.
:param org_chan: the original name of the channel.
:return: the plotting information for org_chan.
"""
# Waveform plot's color is fixed to NULL in the database, so we do not need
# to get the valueColors columns from the database.
chan_info = execute_db_dict(
"SELECT * FROM Parameters WHERE param='Seismic data'")
"SELECT param, plotType, height "
"FROM Parameters WHERE param='Seismic data'")
# The plotting API still requires that the key 'valueColors' is mapped to
# something, so we are setting it to None.
chan_info[0]['valueColors'] = None
chan_info[0]['label'] = get_chan_label(org_chan)
chan_info[0]['unit'] = ''
chan_info[0]['channel'] = 'SEISMIC'
......
No preview for this file type
......@@ -379,6 +379,8 @@ class DataTypeModel():
f'WHERE FieldName="tempDataDirectory"')
def check_not_found_soh_chans(self):
if self.selected_key not in self.soh_data:
return
not_found_soh_chans = [
c for c in self.req_soh_chans
if c not in self.soh_data[self.selected_key].keys()]
......
from PySide2 import QtCore
from obspy import Trace
# Global flag that determines whether the user requested to stop processing and
# its corresponding global lock.
stop = False
stop_lock = QtCore.QMutex()
class DecimatorWorkerSignals(QtCore.QObject):
"""
Object that contains the signals for DownsamplerWorker.
Signals:
finished: emitted when a DownsamplerWorker instance finishes, contains
the result of the computation done by the instance
stopped: emitted when a DownsamplerWorker instance is stopped as a
result of a request from the user
"""
finished = QtCore.Signal(Trace)
stopped = QtCore.Signal()
class DecimatorWorker(QtCore.QRunnable):
"""
The worker class that executes the code to downsample data for plotting.
"""
def __init__(self, trace, decimate_factor, do_decimate=True):
super().__init__()
self.trace = trace
self.decimate_factor = decimate_factor
self.signals = DecimatorWorkerSignals()
# Whether to downsample the data. Intended for times when a user of
# this class has some data sets that might not need downsampling.
self.do_decimate = do_decimate
def run(self):
global stop, stop_lock
try:
# stop_locker = QMutexLocker(stop_lock)
# stop_lock.lock()
if stop:
# stop_lock.unlock()
raise StopRequested
# stop_lock.unlock()
elif self.do_decimate:
self.trace.decimate(self.decimate_factor)
except StopRequested:
# The way we set things up, only one background thread can do
# downsampling at a time. When we stop processing waveform data,
# all other queued background downsampling tasks are killed,
# leaving the sole running downsampler. As a result, we can reset
# the stop flag with no issue because no other downsampler will
# read it afterward.
stop = False
self.signals.stopped.emit()
else:
if not stop:
self.signals.finished.emit(
self.trace
)
class Decimator:
"""
The class that coordinate the downsampling of data in background threads.
Intended to be used in cases where there are multiple data sets that need
to be downsampled. For instances where there is only one data set,
DownsamplerWorker offers an easier-to-use API at the cost of having to
do your own thread management.
"""
def __init__(self):
self.thread_pool = QtCore.QThreadPool()
# Setting the maximum number of thread executed in the background in
# this thread pool to 1. Doing so helps to prevent the situation where
# multiple threads finish their work at once, which may lead to the
# main thread hanging if the post-processing procedure is too complex.
self.thread_pool.setMaxThreadCount(1)
self.worker_list = []
def add_worker(self, trace, decimate_factor, do_decimate=True):
"""
Create a downsampler worker for a data set and add it to the worker
list. Also returns the created worker so that users can connect its
signals to their slots.
:param times: the times array to downsample
:param data: the data array to downsample
:param log_indexes: the soh message line indices array to downsample
:param rq_points: the requested size of the downsampled arrays
:param do_decimate: whether to downsample the given data set. True by
default
:return: the worker that will downsample the given data
"""
worker = DecimatorWorker(trace, decimate_factor, do_decimate)
self.worker_list.append(worker)
return worker
def start(self):
"""
Signal the internal thread pool to put all the stored downsampler
workers into its execution queue. Each worker will begin running when
its turn comes.
"""
for worker in self.worker_list:
self.thread_pool.start(worker)
def request_stop(self):
"""
Request the downsampler to stop all its workers by setting the global
stop flag.
"""
global stop, stop_lock
stop_lock.lock()
stop = True
stop_lock.unlock()
self.thread_pool.clear()
class StopRequested(Exception):
"""
Exception that is raised to indicate that a downsampler worker should stop
running and starts cleaning up.
"""
pass
......@@ -162,7 +162,6 @@ def check_mseed_header(
endtime=endtime)
except TypeError:
return "WRONG FORMAT"
has_req_data = False
for trace in stream:
chan_id = trace.stats['channel'].strip()
......@@ -276,8 +275,8 @@ def read_mseed(path2file: Path, tmp_dir, stream: Stream,
"tracesInfo": []}
traces_info = waveform_data[sta_id]["readData"][chan_id][
"tracesInfo"]
tr = read_waveform_trace(
trace, sta_id, chan_id, traces_info, tmp_dir)
tr = read_mseed_trace(
trace, sta_id, chan_id, len(traces_info), tmp_dir)
traces_info.append(tr)
if sta_id not in data_time:
......@@ -292,18 +291,41 @@ def read_mseed(path2file: Path, tmp_dir, stream: Stream,
return stat_ids, chan_ids
def read_soh_trace(trace: Trace) -> Dict:
def read_mseed_trace(trace: Trace, sta_id: Union[Tuple[str, str], str],
chan_id: str, tr_idx: int, tmp_dir: str) -> Dict:
"""
Read SOH trace's info
Read mseed trace using read_mseed_trace_spr_less_than_or_equal_1()
for sample-rate <= 1
and read_mseed_trace_spr_greater_than_1 for sample-rate > 1
:param trace: mseed trace
:return tr: with trace's info
(structure in DataTypeModel.__init__.soh_ata[key][chan_id][orgTrace])
:param sta_id: station name
:param chan_id: channel name
:param tr_idx: index of trace in tracesInfo list
:param tmp_dir: path to the directory that store memmap files
:return dict of trace's info
"""
if trace.stats.sampling_rate <= 1:
return read_mseed_trace_spr_less_than_or_equal_1(trace)
else:
return read_mseed_trace_spr_greater_than_1(
trace, sta_id, chan_id, tr_idx, tmp_dir)
def read_mseed_trace_spr_less_than_or_equal_1(trace: Trace) -> Dict:
"""
For mseed trace of which sample rate <=1, read and keep all info of the
trace including times and data in memory.
Traces that have sample_rate <=1 can be soh, mass position, waveform
:param trace: mseed trace
:return tr: dict of trace's info in which data and times are kept
"""
tr = {}
tr['chanID'] = trace.stats['channel']
tr['samplerate'] = trace.stats['sampling_rate']
tr['startTmEpoch'] = trace.stats['starttime'].timestamp
tr['endTmEpoch'] = trace.stats['endtime'].timestamp
tr['chanID'] = trace.stats.channel
tr['startTmEpoch'] = trace.stats.starttime.timestamp
tr['endTmEpoch'] = trace.stats.endtime.timestamp
tr['samplerate'] = trace.stats.sampling_rate
tr['size'] = trace.stats.npts
"""
trace time start with 0 => need to add with epoch starttime
times and data have type ndarray
......@@ -313,63 +335,61 @@ def read_soh_trace(trace: Trace) -> Dict:
return tr
def read_mp_trace(trace: Trace) -> Dict:
"""
Read mass possition trace's info using read_soh_trace(), then calculate
real value for mass possition
:param trace: mseed trace
:return tr: with trace's info from read_soh_trace in which tr['data']
has been converted from 16-bit signed integer in which
32767= 2 ** 16/2 - 1 is the highest value of 16-bit two's complement
number. The value is also multiplied by 10 for readable display.
(structure in DataTypeModel.__init__.soh_data[key][chan_id][orgTrace])
(According to 130_theory.pdf: Each channel connects to a 12-bit A/D
converter with an input range of +/- 10V. These channel are read
once per second as left-justified, 2's-compliment, 16 bit values.)
def read_mseed_trace_spr_greater_than_1(
trace: Trace, sta_id: Union[Tuple[str, str], str],
chan_id: str, tr_idx: int, tmp_dir: str) -> Dict:
"""
tr = read_soh_trace(trace)
tr['data'] = np.round_(tr['data'] / 32767.0 * 10.0, 1)
return tr
For mseed trace with sample rate (spr) > 1, info will be read but data will
will be saved in a file to be retrieved later when needed because
data can be very big.
Traces that have spr>1 can be soh, mass position, waveform.
(For soh, traces with spr>1 only be considered when the channels are
explicitly stated in channel preferences)
def read_waveform_trace(trace: Trace, sta_id: Union[Tuple[str, str], str],
chan_id: str, traces_info: List, tmp_dir: str) -> Dict:
"""
read mseed waveform trace and save data to files to save mem for processing
since waveform data are big.
:param trace: mseed trace
:param sta_id: station name
:param chan_id: channel name
:param traces_info: holder of traces_info, refer
DataTypeModel.__init__.
waveform_data[key]['read_data'][chan_id]['tracesInfo']
:param tr_idx: index of trace in tracesInfo list
:param tmp_dir: path to the directory that store memmap files
:return tr: with trace's info
(structure in DataTypeModel.__init__.
waveform_data[key][chan_id][tracesInfo])
:return tr: dict of trace's info in which,
+ times: aren't kept and will be reconstruct using np.linspace
fed with startTmEpoch, endTmEpoch and samplerate
+ data: are saved in a file of which name is kept in tr['data_f']
"""
# gaps for SOH only for now
tr = {}
tr['samplerate'] = trace.stats['sampling_rate']
tr['samplerate'] = trace.stats.sampling_rate
tr['startTmEpoch'] = trace.stats['starttime'].timestamp
tr['endTmEpoch'] = trace.stats['endtime'].timestamp
"""
trace time start with 0 => need to add with epoch starttime
times and data have type ndarray
"""
times = trace.times() + trace.stats['starttime'].timestamp
tr['size'] = trace.stats.npts
data = trace.data
tr['size'] = times.size
tr_idx = len(traces_info)
tr['times_f'] = save_data_2_file(
tmp_dir, 'times', sta_id, chan_id, times, tr_idx, tr['size'])
tr['data_f'] = save_data_2_file(
tmp_dir, 'data', sta_id, chan_id, data, tr_idx, tr['size'])
return tr
def read_mp_trace(trace: Trace) -> Dict:
"""
Read mass possition trace's info using
read_mseed_trace_spr_less_than_or_equal_1(),
then calculate real value for mass possition
:param trace: mseed trace
:return tr: with trace's info from
read_mseed_trace_spr_less_than_or_equal_1 in which tr['data']
has been converted from 16-bit signed integer in which
32767= 2 ** 16/2 - 1 is the highest value of 16-bit two's complement
number. The value is also multiplied by 10 for readable display.
(structure in DataTypeModel.__init__.soh_data[key][chan_id][orgTrace])
(According to 130_theory.pdf: Each channel connects to a 12-bit A/D
converter with an input range of +/- 10V. These channel are read
once per second as left-justified, 2's-compliment, 16 bit values.)
"""
tr = read_mseed_trace_spr_less_than_or_equal_1(trace)
tr['data'] = np.round_(tr['data'] / 32767.0 * 10.0, 1)
return tr
def read_waveform_reftek(rt130: Reftek130, key: Tuple[str, str],
read_data: Dict, data_time: List[float], tmp_dir: str
) -> None:
......@@ -397,7 +417,7 @@ def read_waveform_reftek(rt130: Reftek130, key: Tuple[str, str],
"tracesInfo": [],
"samplerate": samplerate}
traces_info = read_data[chan_id]['tracesInfo']
tr = read_waveform_trace(trace, key, chan_id, traces_info, tmp_dir)
tr = read_mseed_trace(trace, key, chan_id, len(traces_info), tmp_dir)
data_time[0] = min(tr['startTmEpoch'], data_time[0])
data_time[1] = max(tr['endTmEpoch'], data_time[1])
traces_info.append(tr)
......
......@@ -10,7 +10,8 @@ from obspy.core import Stream
from sohstationviewer.controller.util import validate_file, validate_dir
from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped
from sohstationviewer.model.handling_data import (
squash_gaps, sort_data, read_soh_trace, read_mseed_or_text)
squash_gaps, sort_data, read_mseed_trace_spr_less_than_or_equal_1,
read_mseed_or_text)
from sohstationviewer.view.util.enums import LogType
......@@ -47,7 +48,7 @@ class MSeed(DataTypeModel):
if self.selected_key is None:
raise ThreadStopped()
if (len(self.req_wf_chans) != 0 and
self.selected_key not in self.waveform_data.keys()):
self.selected_key in self.waveform_data.keys()):
sort_data(self.waveform_data[self.selected_key]['readData'])
self.check_not_found_soh_chans()
......@@ -195,7 +196,7 @@ class MSeed(DataTypeModel):
gaps_in_stream = stream.get_gaps()
all_gaps += [[g[4].timestamp, g[5].timestamp]
for g in gaps_in_stream]
trace_info = read_soh_trace(tr)
trace_info = read_mseed_trace_spr_less_than_or_equal_1(tr)
if is_mass_pos:
data_dict[sta_id][chan_id] = {
......
......@@ -131,6 +131,8 @@ class RT130(DataTypeModel):
waveform_data[key]['read_data'][chan_id]['tracesInfo'].
"""
if key not in self.waveform_data:
return
count = 0
for data_stream in self.waveform_data[key]['filesInfo']:
read_data = self.waveform_data[key]['readData']
......@@ -214,6 +216,8 @@ class RT130(DataTypeModel):
and save to self.waveform_data[self.cur_key]["filesInfo"] to
be processed later
"""
if len(rt130._data) == 0:
return
data_stream = rt130._data['data_stream_number'][0] + 1
if (self.req_data_streams != ['*'] and
data_stream not in self.req_data_streams + [9]):
......@@ -227,10 +231,10 @@ class RT130(DataTypeModel):
for ind in range(0, len(rt130._data))
if ind not in ind_ehet])
self.cur_key = (rt130._data[0]['unit_id'].decode(),
rt130._data[0]['experiment_number'])
for ind in ind_ehet:
d = rt130._data[ind]
self.cur_key = (d['unit_id'].decode(),
d['experiment_number'])
if self.cur_key not in self.log_data:
self.log_data[self.cur_key] = {}
logs = packet.EHPacket(d).eh_et_info(nbr_dt_samples)
......
......@@ -11,7 +11,8 @@ from sohstationviewer.controller.processing import (
from sohstationviewer.controller.util import display_tracking_info
from sohstationviewer.view.util.enums import LogType
from sohstationviewer.view.util.one_instance_at_a_time import \
OneWindowAtATimeDialog
INSTRUCTION = """
Select the list of SOH channels to be used in plotting.\n
......@@ -44,7 +45,7 @@ class InputDialog(QDialog):
return self.text_box.toPlainText()
class ChannelPreferDialog(QtWidgets.QWidget):
class ChannelPreferDialog(OneWindowAtATimeDialog):
def __init__(self, parent, dir_names):
"""
Dialog to create lists of preferred SOH channels that users want to
......
from PySide2 import QtWidgets, QtGui, QtCore
from __future__ import annotations
from typing import Set, Dict
from PySide2 import QtWidgets, QtGui, QtCore
from sohstationviewer.database.process_db import execute_db
from sohstationviewer.view.util.one_instance_at_a_time import \
OneWindowAtATimeDialog
def set_widget_color(widget, changed=False, read_only=False):
......@@ -42,10 +46,15 @@ def set_widget_color(widget, changed=False, read_only=False):
widget.setPalette(palette)
class UiDBInfoDialog(QtWidgets.QWidget):
class UiDBInfoDialog(OneWindowAtATimeDialog):
"""
Superclass for info database dialogs under database menu.
"""
# This line is only to type hint the class attribute inherited from
# OneWindowAtATimeDialog.
current_instance: UiDBInfoDialog
def __init__(self, parent, column_headers, col_name, table_name,
resize_content_columns=[], required_columns={},
need_data_type_choice=False, check_fk=True):
......@@ -98,14 +107,20 @@ class UiDBInfoDialog(QtWidgets.QWidget):
Color code is given at the end of the dialog.
Other instructions should be given when hover over the widgets.
"""
if self.table_name != '':
# Not really used in this (base) class, but made available so that
# any children can use the empty space on the bottom right of the
# widget.
self.bottom_layout = QtWidgets.QGridLayout()
instruction = ("Background: LIGHT BLUE - Non Editable due to "
"FK constrain; "
"WHITE - Editable. "
"Text: BLACK - Saved; RED - Not saved")
"WHITE - Editable.\n"
"Text: BLACK - Saved; RED - Not saved.")
# TODO: add question mark button to give instruction
main_layout.addWidget(QtWidgets.QLabel(instruction))
self.bottom_layout.addWidget(QtWidgets.QLabel(instruction),
0, 0, 1, 1)
main_layout.addLayout(self.bottom_layout)
def add_widget(self, data_list, row_idx, col_idx, foreign_key=False,
choices=None, range_values=None, field_name='',
......
......@@ -3,8 +3,13 @@ param_dialog.py
GUI to add/dit/remove params
NOTE: Cannot remove or change params that are already used for channels.
"""
from PySide2 import QtWidgets
from typing import List
from PySide2 import QtWidgets, QtCore
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QComboBox, QWidget
from sohstationviewer.conf.constants import ColorMode, ALL_COLOR_MODES
from sohstationviewer.view.util.plot_func_names import plot_functions
from sohstationviewer.view.db_config.db_config_dialog import UiDBInfoDialog
......@@ -14,10 +19,13 @@ from sohstationviewer.conf.dbSettings import dbConf
class ParamDialog(UiDBInfoDialog):
def __init__(self, parent):
def __init__(self, parent: QWidget, color_mode: ColorMode) -> None:
"""
:param parent: QMainWindow/QWidget - the parent widget
:param color_mode: the initial color mode of the dialog
"""
self.color_mode = color_mode
self.require_valuecolors_plottypes = [
p for p in plot_functions.keys()
if 'ValueColors' in plot_functions[p][0]]
......@@ -27,8 +35,27 @@ class ParamDialog(UiDBInfoDialog):
'param', 'parameters',
resize_content_columns=[0, 3])
self.setWindowTitle("Edit/Add/Delete Parameters")
self.add_color_selector(color_mode)
def add_color_selector(self, initial_color_mode: ColorMode) -> None:
"""
Add a color mode selector widget to the dialog.
:param initial_color_mode: the color mode to use as the initial
selection when the dialog is shown.
"""
color_mode_label = QtWidgets.QLabel('Color mode:')
color_selector = QComboBox()
color_selector.insertItem(0, initial_color_mode)
other_color_modes = ALL_COLOR_MODES - {initial_color_mode}
color_selector.insertItems(1, other_color_modes)
color_selector.setFixedWidth(100)
color_selector.currentTextChanged.connect(self.on_color_mode_changed)
self.bottom_layout.addWidget(color_mode_label, 0, 1, 1, 1,
Qt.AlignRight)
self.bottom_layout.addWidget(color_selector, 0, 2, 1, 1)
def add_row(self, row_idx, fk=False):
def add_row(self, row_idx: int, fk: bool = False) -> None:
"""
Add a row of widgets to self.data_table_widgets.
......@@ -60,11 +87,15 @@ class ParamDialog(UiDBInfoDialog):
"""
Get list of data to fill self.data_table_widgets' content
"""
# The valueColors for each color mode is stored in a separate column.
# 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(
"SELECT param,"
" IFNULL(plotType, '') AS plotType,"
" IFNULL(valueColors, '') AS valueColors,"
" IFNULL(height, 0) AS height FROM Parameters")
f"SELECT param, "
f"IFNULL(plotType, '') AS plotType, "
f"IFNULL({value_colors_column}, '') AS valueColors, "
f"IFNULL(height, 0) AS height FROM Parameters")
return [[d[0], d[1], d[2], int(d[3])]
for d in param_rows]
......@@ -115,7 +146,7 @@ class ParamDialog(UiDBInfoDialog):
int(self.data_table_widget.cellWidget(row_idx, 4).value())
]
def update_data(self, row, widget_idx, list_idx):
def update_data(self, row: List, widget_idx: int, list_idx: int) -> int:
"""
Prepare insert, update queries then update data of a row from
self.data_table_widgets' content.
......@@ -124,11 +155,36 @@ class ParamDialog(UiDBInfoDialog):
:param widget_idx: index of row in self.data_table_widgets
:param list_idx: index of row in self.data_list
"""
insert_sql = (f"INSERT INTO Parameters VALUES"
# The valueColors for each color mode is stored in a separate column.
# 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
insert_sql = (f"INSERT INTO Parameters "
f"(param, plotType, {value_colors_column}, height) "
f"VALUES"
f"('{row[0]}', '{row[1]}', '{row[2]}', {row[3]})")
update_sql = (f"UPDATE Parameters SET param='{row[0]}', "
f"plotType='{row[1]}', valueColors='{row[2]}',"
f"plotType='{row[1]}', {value_colors_column}='{row[2]}',"
f"height={row[3]} "
f"WHERE param='%s'")
return super().update_data(
row, widget_idx, list_idx, insert_sql, update_sql)
@QtCore.Slot()
def on_color_mode_changed(self, new_color_mode: ColorMode):
"""
Slot called when the color mode is changed in the color mode selector.
:param new_color_mode: the new color mode
"""
self.color_mode = new_color_mode
# Remove all rows in the table while keeping the widths of the columns
# intact
self.data_table_widget.setRowCount(0)
# Repopulates the table with data from the database. A custom routine
# could have been written to replace only the ValueColors column in the
# table, but that would take too much time for only a marginal increase
# in performance, especially considering the Parameters table is
# expected to be pretty small.
self.update_data_table_widget_items()
from typing import Union, Dict, List, Set, Tuple
from PySide2.QtWidgets import QTextBrowser
from sohstationviewer.controller.plotting_data import format_time
from sohstationviewer.model.data_type_model import DataTypeModel
from sohstationviewer.model.mseed.mseed import MSeed
......@@ -9,8 +7,7 @@ from sohstationviewer.model.reftek.reftek import RT130
from sohstationviewer.view.util.functions import extract_netcodes
def extract_data_set_info(tracking_box: QTextBrowser,
data_obj: Union[DataTypeModel, RT130, MSeed],
def extract_data_set_info(data_obj: Union[DataTypeModel, RT130, MSeed],
date_format: str
) -> Dict[str, Union[str, List[str]]]:
"""
......@@ -19,12 +16,12 @@ def extract_data_set_info(tracking_box: QTextBrowser,
- Start / end times
- Station ID
- DAS Serial Number
- Experiment number
- Network Code
- Tag number
- GPS information
- Software version
:param tracking_box: widget to display tracking info on
:param data_obj: the data object to extract file information from
:param date_format: the format by which to format file's start/end times
:return: a dictionary containing the information of a file
......@@ -50,12 +47,18 @@ def extract_data_set_info(tracking_box: QTextBrowser,
if data_type == 'RT130':
das_serials = list({key[0] for key in data_obj.keys})
data_set_info['DAS Serial Numbers'] = ','.join(das_serials)
if len(das_serials) > 1:
experiment_numbers = list({str(key[1]) for key in data_obj.keys})
# The insertion order into data_set_info in this piece of code is
# important, so we had to write it in a way that is a bit repetitive.
data_set_info['DAS Serial Numbers'] = ', '.join(das_serials)
if len(data_obj.keys) > 1:
data_set_info['Selected DAS'] = data_obj.selected_key[0]
data_set_info['Experiment Numbers'] = ', '.join(experiment_numbers)
if len(data_obj.keys) > 1:
data_set_info['Selected Experiment'] = data_obj.selected_key[1]
else:
stations = list(data_obj.sta_ids)
data_set_info['Station IDs'] = ','.join(stations)
data_set_info['Station IDs'] = ', '.join(stations)
if len(stations) > 1:
data_set_info['Selected Station'] = data_obj.selected_key
data_set_info['Network Codes'] = extract_netcodes(data_obj)
......
......@@ -7,7 +7,9 @@ from copy import deepcopy
from pathlib import Path
from PySide2 import QtCore, QtWidgets, QtGui
from PySide2.QtWidgets import QListWidgetItem, QMessageBox
from PySide2.QtCore import QSize
from PySide2.QtGui import QFont, QPalette, QColor
from PySide2.QtWidgets import QFrame, QListWidgetItem, QMessageBox
from sohstationviewer.conf import constants
from sohstationviewer.model.data_loader import DataLoader
......@@ -46,7 +48,9 @@ from sohstationviewer.controller.util import (
from sohstationviewer.database.process_db import execute_db_dict, execute_db
from sohstationviewer.conf.constants import TM_FORMAT
from sohstationviewer.conf.constants import TM_FORMAT, ColorMode
from sohstationviewer.view.util.one_instance_at_a_time import \
DialogAlreadyOpenedError
class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
......@@ -60,6 +64,10 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
"""
self.dir_names: List[Path] = []
"""
current_dir: str - the current main data directory
"""
self.current_dir = ''
"""
rt130_das_dict: dict by rt130 for data paths, so user can choose
dasses to assign list of data paths to selected_rt130_paths
"""
......@@ -73,6 +81,11 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
data_type: str - type of data set
"""
self.data_type: str = 'Unknown'
"""
color_mode: str - the current color mode of the plot; can be either 'B'
or 'W'
"""
self.color_mode: ColorMode = 'B'
self.data_loader: DataLoader = DataLoader()
self.data_loader.finished.connect(self.replot_loaded_data)
......@@ -172,36 +185,56 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.background_black_radio_button.toggle()
@QtCore.Slot()
def open_data_type(self):
def open_data_type(self) -> None:
"""
Open a dialog to add/edit data types in DB
Open a dialog to add/edit data types in DB If a dialog of this type is
currently open, raise it to the front.
"""
win = DataTypeDialog(self)
win.show()
try:
win = DataTypeDialog(self)
win.show()
except DialogAlreadyOpenedError:
DataTypeDialog.current_instance.activateWindow()
DataTypeDialog.current_instance.raise_()
@QtCore.Slot()
def open_param(self):
def open_param(self) -> None:
"""
Open a dialog to add/edit parameters in DB
Open a dialog to add/edit parameters in DB If a dialog of this type is
currently open, raise it to the front.
"""
win = ParamDialog(self)
win.show()
try:
win = ParamDialog(self, self.color_mode)
win.show()
except DialogAlreadyOpenedError:
ParamDialog.current_instance.activateWindow()
ParamDialog.current_instance.raise_()
@QtCore.Slot()
def open_channel(self):
def open_channel(self) -> None:
"""
Open a dialog to add/edit channels in DB
Open a dialog to add/edit channels in DB If a dialog of this type is
currently open, raise it to the front.
"""
win = ChannelDialog(self)
win.show()
try:
win = ChannelDialog(self)
win.show()
except DialogAlreadyOpenedError:
ChannelDialog.current_instance.activateWindow()
ChannelDialog.current_instance.raise_()
@QtCore.Slot()
def open_plot_type(self):
def open_plot_type(self) -> None:
"""
Open a dialog to view plot types and their description
Open a dialog to view plot types and their description If a dialog of
this type is currently open, raise it to the front.
"""
win = PlotTypeDialog(self)
win.show()
try:
win = PlotTypeDialog(self)
win.show()
except DialogAlreadyOpenedError:
PlotTypeDialog.current_instance.activateWindow()
PlotTypeDialog.current_instance.raise_()
@QtCore.Slot()
def open_calendar(self):
......@@ -220,23 +253,44 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.help_browser.raise_()
@QtCore.Slot()
def open_channel_preferences(self):
def open_channel_preferences(self) -> None:
"""
Open a dialog to view, select, add, edit, scan for preferred channels
list.
list. If a dialog of this type is currently open, raise it to the
front.
"""
try:
self.read_from_file_list()
except Exception as e:
QtWidgets.QMessageBox.warning(self, "Select directory", str(e))
return
win = ChannelPreferDialog(self, self.dir_names)
win.show()
try:
win = ChannelPreferDialog(self, self.dir_names)
win.show()
except DialogAlreadyOpenedError:
ChannelPreferDialog.current_instance.activateWindow()
ChannelPreferDialog.current_instance.raise_()
@QtCore.Slot()
def from_data_card_check_box_clicked(self):
def from_data_card_check_box_clicked(self, is_from_data_card_checked):
self.open_files_list.clear()
self.set_open_files_list_texts()
# self.search_button.setEnabled(not is_from_data_card_checked)
self.search_line_edit.setEnabled(not is_from_data_card_checked)
# QLineEdit does not change its color when it is disabled unless
# there is text inside, so we have to do it manually.
palette = self.search_line_edit.palette()
if is_from_data_card_checked:
# We are copying the color of a disabled button
search_line_edit_color = QColor(246, 246, 246)
else:
search_line_edit_color = QColor(255, 255, 255)
palette.setColor(QPalette.Base, search_line_edit_color)
self.search_line_edit.setPalette(palette)
if not is_from_data_card_checked and self.search_line_edit.text():
self.filter_folder_list(self.search_line_edit.text())
@QtCore.Slot()
def all_wf_chans_clicked(self):
......@@ -611,8 +665,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
log_message = ('Cannot get GPS data.', LogType.WARNING)
self.processing_log.append(log_message)
data_set_info = extract_data_set_info(self.tracking_info_text_browser,
data_obj, self.date_format)
data_set_info = extract_data_set_info(data_obj, self.date_format)
for info_name, info in data_set_info.items():
if isinstance(info, str):
info_string = f'{info_name}:\n\t{info}'
......@@ -672,7 +725,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.plotting_widget.plot_channels(
self.start_tm, self.end_tm, sel_key,
do.data_time[sel_key], soh_chans, time_tick_total,
soh_data, mp_data, gaps)
soh_data, mp_data, gaps, self.color_mode)
except Exception:
fmt = traceback.format_exc()
msg = f"Can't plot SOH data due to error: {str(fmt)}"
......@@ -718,7 +771,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.waveform_dlg.plotting_widget.plot_channels(
self.start_tm, self.end_tm, sel_key,
do.data_time[sel_key], time_tick_total,
wf_data, mp_data)
wf_data, mp_data, self.color_mode)
self.add_action_to_forms_menu('Raw Data Plot', self.waveform_dlg)
else:
self.waveform_dlg.hide()
......@@ -779,6 +832,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
# Signal current_directory_changed, and gather list of files in new
# current directory
self.current_directory_changed.emit(path)
self.current_dir = path
execute_db(f'UPDATE PersistentData SET FieldValue="{path}" WHERE '
'FieldName="currentDirectory"')
self.set_open_files_list_texts()
......@@ -930,7 +984,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
Close all forms related to forms_menu.
Add no_forms_action to let user know that there are no forms available.
"""
self.forms_menu.clear() # remove all actions
self.forms_menu.clear() # remove all actions
for i in range(len(self.forms_in_forms_menu) - 1, -1, -1):
form = self.forms_in_forms_menu.pop(i)
form.close()
......@@ -947,7 +1001,74 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.gps_dialog.raise_()
@QtCore.Slot()
def set_plots_color(self, checked, color_mode):
def set_plots_color(self, checked: bool, color_mode: ColorMode) -> None:
"""
Slot called when a new color mode radio button is pressed.
:param checked: whether the button that calls this slot is checked
:param color_mode: the color mode associated with the button that calls
this slot
"""
if not checked:
return
self.color_mode = color_mode
self.plotting_widget.set_colors(color_mode)
self.waveform_dlg.plotting_widget.set_colors(color_mode)
self.tps_dlg.plotting_widget.set_colors(color_mode)
self.gps_dialog.set_colors(color_mode)
@QtCore.Slot()
def clear_file_search(self):
"""
Clear the content of the file search widget.
"""
self.search_line_edit.clear()
self.set_current_directory(self.current_dir)
def filter_folder_list(self, search_text: str) -> None:
"""
Filter the current list of folders based on the search input.
"""
if search_text == '':
self.clear_file_search()
return
self.set_current_directory(self.current_dir)
open_file_paths = [
self.open_files_list.item(i).file_path
for i in range(self.open_files_list.count())
]
filtered_file_paths = [
path
for path in open_file_paths
if search_text.casefold() in path.casefold()
]
# We are inserting the widgets in reverse order because doing so means
# that we only have to insert into the 0th row. Otherwise, we would
# have to keep track of the current index to insert into.
# Create a line that separate filtered list of directories and list of
# directories.
separator_list_item = QListWidgetItem()
separator_list_item.setFlags(QtCore.Qt.NoItemFlags)
separator_list_item.setSizeHint(
QSize(separator_list_item.sizeHint().width(), 10) # noqa
)
self.open_files_list.insertItem(0, separator_list_item)
line = QFrame()
line.setFrameShape(QFrame.HLine)
self.open_files_list.setItemWidget(separator_list_item, line)
for path in reversed(filtered_file_paths):
current_file_list_item = FileListItem(path)
self.open_files_list.insertItem(0, current_file_list_item)
found_files_list_item = QListWidgetItem('Found files:')
bold_font = QFont()
bold_font.setBold(True)
found_files_list_item.setFont(bold_font)
found_files_list_item.setFlags(QtCore.Qt.NoItemFlags)
found_files_list_item.setForeground(QtCore.Qt.black)
self.open_files_list.insertItem(0, found_files_list_item)
......@@ -272,7 +272,8 @@ class Plotting:
linestyle='-', linewidth=0.7,
zorder=constants.Z_ORDER['LINE'],
color=clr[l_color],
markerfacecolor=clr[d_color],
mfc=clr[d_color],
mec=clr[d_color],
picker=True, pickradius=3)
if linked_ax is None:
ax.x = x
......@@ -302,8 +303,10 @@ class Plotting:
info = "%dsps" % c_data['samplerate']
else:
info = "%gsps" % c_data['samplerate']
return self.plot_lines_dots(c_data, chan_db_info, chan_id,
ax, linked_ax, info=info)
# return self.plot_lines_dots(c_data, chan_db_info, chan_id,
# ax, linked_ax, info=info)
return self.plot_waveform(c_data, chan_db_info, chan_id,
ax, linked_ax, info=info)
def plot_lines_mass_pos(self, c_data, chan_db_info, chan_id,
ax, linked_ax):
......@@ -361,3 +364,76 @@ class Plotting:
ax.scatter(ax.x, ax.y, marker='s', c=colors, s=sizes,
zorder=constants.Z_ORDER['DOT'])
return ax
def plot_waveform(self, c_data, chan_db_info, chan_id,
ax, linked_ax, info=''):
"""
Plot lines with dots at the data points. Colors of dot and lines are
defined in valueColors in database.
Ex: L:G|D:W means
Lines are plotted with color G
Dots are plotted with color W
If D is not defined, dots won't be displayed.
If L is not defined, lines will be plotted with color G
Color codes are defined in colorSettings
:param c_data: dict - data of the channel which includes down-sampled
data in keys 'times' and 'data'. Refer to DataTypeModel.__init__.
soh_data[key][chan_id] or DataTypeModel.__init__.
waveform_data[key]['read_data'][chan_id] for waveform data
:param chan_db_info: dict - info of channel from DB
:param chan_id: str - name of channel
:param ax: matplotlib.axes.Axes - axes to draw plot of channel
:param linked_ax: matplotlib.axes.Axes/None - axes of another channel
linked to this channel => both channels' will be plotted on the
same axes
:param info: str - additional info to be displayed on sub-title under
main-title
:return ax: matplotlib.axes.Axes - axes of the channel
"""
if linked_ax is not None:
ax = linked_ax
if ax is None:
plot_h = self.plotting_axes.get_height(chan_db_info['height'])
ax = self.plotting_axes.create_axes(
self.parent.plotting_bot, plot_h)
x_list, y_list = c_data['times'], c_data['data']
total_x = sum([len(x) for x in x_list])
self.plotting_axes.set_axes_info(
ax, [total_x], chan_db_info=chan_db_info,
info=info, y=y_list, linked_ax=linked_ax)
colors = {}
if chan_db_info['valueColors'] not in [None, 'None', '']:
color_parts = chan_db_info['valueColors'].split('|')
for cStr in color_parts:
obj, c = cStr.split(':')
colors[obj] = c
l_color = 'G'
has_dot = False
if 'L' in colors:
l_color = colors['L']
if 'D' in colors:
d_color = colors['D']
has_dot = True
for x, y in zip(x_list, y_list):
if not has_dot:
ax.myPlot = ax.plot(x, y,
linestyle='-', linewidth=0.7,
color=clr[l_color])
else:
ax.myPlot = ax.plot(x, y, marker='s', markersize=1.5,
linestyle='-', linewidth=0.7,
zorder=constants.Z_ORDER['LINE'],
color=clr[l_color],
markerfacecolor=clr[d_color],
picker=True, pickradius=3)
if linked_ax is None:
ax.x_list = x_list
ax.y_list = y_list
else:
ax.linkedX = x_list
ax.linkedY = y_list
return ax