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 (13)
Showing
with 497 additions and 217 deletions
<img alt="Whole Section" src="images/select_soh/whole_section.png" width="250" />
<br />
# Selecting SOH Channels
---------------------------
---------------------------
## Selecting all SOH channels
To read all SOH channels in the selected data set, check the checkbox "All SOH".
<br />
<img alt="Select all SOH channels" src="images/select_soh/all_soh_checkbox.png" height="30" />
<br />
If this checkbox is unchecked, the latest Preferred SOH list will be used when reading the data set.
<br />
<img alt="Current preferred SOH list" src="images/select_soh/current_soh_list.png" height="30" />
<br />
## Get a preferred SOH channel list to read
Click button "Pref" to open "SOH Channel Preferences" to create/edit/select a list of SOH channel to read.
<br />
<img alt="SOH Channel Preferences Dialog" src="images/select_soh/soh_channel_preferences.png" height="300" />
<br />
### SOH channel list
Each row in the table at the center of "SOH Channel Preferences" window represents an SOH list.
<br />
<img alt="A Row of SOH List" src="images/select_soh/full_row.png" height="75" />
<br />
### Select an SOH channel list
Check on the radio button (1) at the beginning of each row to select the list.
The radio button can also be checked automatically when user try to edit the row.
### Create a new list
+ Add name of the list to the Name box (2)
+ Select a Data Type (3)
+ Add preferred channels to Preferred SOH List box (4). Channels are separated by commas.
+ The Preferred SOH List can be edited directly in box 4, but user can also click on button EDIT (5) to open a separated dialog for editing easier.
+ Button CLR (6) is for quickly empty the Preferred SOH List box.
### Bottom buttons
<br />
<img alt="Buttons" src="images/select_soh/buttons.png" height="30" />
<br />
**Beside adding preferred channels manually, users have two other options to start the list:**
+ "Add DB channels". All SOH channels for the selected Data Type in the database will be added to Preferred SOH List box.
+ "Scan Channel from Data Source". SOHView will scan for all available SOH channels in the data set.
**For saving Preferred SOH List**
+ "Save": Save any changes to database
+ "Save - Add to Main": Save any changes to database and add the selected Preferred SOH's name to Main Window so that its SOH list will be included when reading the data set.
\ No newline at end of file
documentation/images/select_soh/all_soh_checkbox.png

7.21 KiB

documentation/images/select_soh/buttons.png

19.4 KiB

documentation/images/select_soh/current_soh_list.png

4.59 KiB

documentation/images/select_soh/full_row.png

38.1 KiB

documentation/images/select_soh/soh_channel_preferences.png

319 KiB

documentation/images/select_soh/whole_section.png

10.2 KiB

......@@ -92,14 +92,14 @@ def format_time(time: Union[UTCDateTime, float], date_mode: str,
format = ''
if date_mode == 'YYYY-MM-DD':
format = '%Y-%m-%d'
elif date_mode == 'YYYYMMDD':
format = '%Y%m%d'
elif date_mode == 'YYYYMMMDD':
format = '%Y%b%d'
elif date_mode == 'YYYY:DOY':
format = '%Y:%j'
if time_mode == 'HH:MM:SS':
format += " %H:%M:%S"
ret = t.strftime(format)
ret = t.strftime(format).upper()
return ret
......
......@@ -369,3 +369,12 @@ class DataTypeModel():
def save_temp_data_folder_to_database(self):
execute_db(f'UPDATE PersistentData SET FieldValue="{self.tmp_dir}" '
f'WHERE FieldName="tempDataDirectory"')
def check_not_found_soh_chans(self):
not_found_soh_chans = [
c for c in self.req_soh_chans
if c not in self.soh_data[self.selected_key].keys()]
if not_found_soh_chans != []:
msg = (f"No data found for soh channels: "
f"{', '.join( not_found_soh_chans)}")
self.processing_log.append((msg, LogType.WARNING))
from typing import NamedTuple
from typing import NamedTuple, Union
class GPSPoint(NamedTuple):
......@@ -7,7 +7,7 @@ class GPSPoint(NamedTuple):
"""
last_timemark: str
fix_type: str
num_satellite_used: int
num_satellite_used: Union[int, str]
latitude: float
longitude: float
height: float
......
......@@ -173,7 +173,7 @@ def read_waveform_trace(trace: Trace, sta_id: Union[Tuple[str, str], str],
return tr
def read_waveform_mseed(path2file: str, file_name: str,
def read_waveform_mseed(path2file: str,
sta_id: str, chan_id: str,
traces_info: List, data_time: List[float], tmp_dir: str
) -> None:
......@@ -181,7 +181,6 @@ def read_waveform_mseed(path2file: str, file_name: str,
Read traces from waveform mseed file to append to tracesInfo.
data_time is update for new min and max time.
:param path2file: absolute path to waveform mseed file
:param file_name: name of waveform mseed file
:param sta_id: station ID from indexing
:param chan_id: channel ID from indexing
:param traces_info: holder of traces_info, refer
......
......@@ -58,6 +58,7 @@ class MSeed(DataTypeModel):
if len(self.req_wf_chans) != 0:
self.read_wf_files(self.selected_key)
self.check_not_found_soh_chans()
self.get_gps_data_q330()
def read_soh_and_index_waveform(self, folder: str):
......
......@@ -15,7 +15,7 @@ class LogInfo():
def __init__(self, parent: RT130,
track_info: Callable[[str, LogType], None], log_text: str,
key: Tuple[str, LogType], req_data_streams: List[int],
is_log_file: bool = False):
req_soh_chans: List[str], is_log_file: bool = False):
"""
Help to extract channel data from LogText which include SOH Message and
Event message.
......@@ -23,7 +23,8 @@ class LogInfo():
:param track_info: to track data processing
:param log_text: SOH and Event messages in time order
:param key: ID of the data set including unit_id and exp_no
:param req_data_streams: requested data stream ID
:param req_data_streams: requested data stream ID,
:param req_soh_chans: requested data stream ID
:param is_log_file: flag indicate if this is a log file
"""
self.parent = parent
......@@ -32,6 +33,7 @@ class LogInfo():
self.key = key
self.unit_id, self.exp_no = key
self.req_data_streams = req_data_streams
self.req_soh_chans = req_soh_chans
self.is_log_file = is_log_file
"""
track_year to add year to time since year time not include year after
......@@ -311,6 +313,8 @@ class LogInfo():
:param idx: int - index of SOH message line
"""
if self.req_soh_chans and chan_id not in self.req_soh_chans:
return
if chan_id not in self.chans:
self.chans[chan_id] = {}
self.chans[chan_id]['orgTrace'] = {
......
"""
RT130 object to hold and process RefTek data
"""
from datetime import datetime
import os
from pathlib import Path
from typing import Tuple, List, Union
import numpy as np
from obspy.core import Stream
from sohstationviewer.model.gps_point import GPSPoint
from sohstationviewer.model.reftek.from_rt2ms import (
core, soh_packet, packet)
from sohstationviewer.model.reftek.log_info import LogInfo
from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped
from sohstationviewer.model.handling_data import (
read_waveform_reftek, squash_gaps, sort_data, read_mp_trace, read_text)
from sohstationviewer.conf import constants
from sohstationviewer.controller.util import validate_file
from sohstationviewer.view.util.enums import LogType
......@@ -58,6 +55,9 @@ class RT130(DataTypeModel):
msg = (f"No data found for data streams: "
f"{', '.join(map(str, not_found_data_streams))}")
self.processing_log.append((msg, LogType.WARNING))
self.get_gps_data()
self.check_not_found_soh_chans()
def read_soh_index_waveform(self, folder: str) -> None:
"""
......@@ -326,7 +326,8 @@ class RT130(DataTypeModel):
log_str = ''.join(logs)
self.log_data[k] = {'SOH': [log_str]}
log_obj = LogInfo(
self, self.track_info, log_str, k, self.req_data_streams)
self, self.track_info, log_str, k,
self.req_data_streams, self.req_soh_chans)
self.data_time[k][0] = min(log_obj.min_epoch, self.data_time[k][0])
self.data_time[k][1] = max(log_obj.max_epoch, self.data_time[k][1])
for c_name in self.soh_data[k]:
......@@ -352,3 +353,112 @@ class RT130(DataTypeModel):
# endtime, duration, number of missing samples]
all_gaps += [[g[4].timestamp, g[5].timestamp] for g in gaps]
self.gaps[k] = squash_gaps(all_gaps)
def get_gps_data(self):
"""
Retrieve the GPS of the current data set. Works by looking into the log
of the data set.
"""
log_lines = self.log_data[self.selected_key]['SOH']
log_lines = log_lines[0].split('\n')
log_lines = [line.strip('\r').strip('') for line in log_lines]
gps_year = None
for line in log_lines:
# The lines of the log that contains the GPS data does not include
# the year. Instead, we grab it by looking at the header of the
# state of health packet the GPS line is in.
# Also, it might be possible to only grab the year once. It would
# be easy to check if the year change by seeing if the day of year
# loops back to 1. That approach is a bit more complex, however,
# so we are not using it.
if 'State of Health' in line:
current_time = line.split()[3]
two_digit_year = current_time[:2]
gps_year = two_digit_year_to_four_digit_year(two_digit_year)
if 'GPS: POSITION' in line:
self.gps_points.append(self.parse_gps_point(line, gps_year))
@staticmethod
def parse_gps_point(gps_line: str, gps_year: str) -> GPSPoint:
"""
Parse a GPS log line to extract the GPS data point it contains. Needs
to be given a year because GPS log lines do not contain the year.
:param gps_line: the GPS log line to parse
:param gps_year: the year associated with gps_line
:return: a GPSPoint object that contains the GPS data points stored in
gps_line
"""
# RT130 data does not contain GPS fix type and number of satellites
# used, so we set them to not available.
fix_type = 'N/A'
num_sats_used = 'N/A'
time_str, *_, lat_str, long_str, height_str = gps_line.split()
time_str = gps_year + ':' + time_str
time_format = '%Y:%j:%H:%M:%S'
gps_time = datetime.strptime(time_str, time_format).isoformat(sep=' ')
# Latitude and longitude are stored in degrees, minutes, and seconds,
# with each quantity being separated by ':'. The degree part of
# latitude is stored as three characters, while the degree part of
# longitude is stored as four characters. The first character of the
# degree part is the cardinal direction (NS/EW), so we ignore it in
# calculating latitude and longitude.
lat_as_list = lat_str.split(':')
lat_second = float(lat_as_list[2])
lat_minute = float(lat_as_list[1]) + lat_second / 60
lat = float(lat_as_list[0][1:]) + lat_minute / 60
if lat_as_list[0][0] == 'S':
lat = -lat
long_as_list = long_str.split(':')
long_second = float(long_as_list[2])
long_minute = float(long_as_list[1]) + long_second / 60
long = float(long_as_list[0][1:]) + long_minute / 60
if long_as_list[0][0] == 'W':
long = -long
# Height is encoded as a signed float followed by the unit. We don't
# know how many characters the unit is composed of, so we have to loop
# through the height string backward until we can detect the end of the
# height value.
# Start pass the end of the string and look backward one index every
# iteration so we don't have to add 1 to the final index.
current_idx = len(height_str)
current_char = height_str[current_idx - 1]
while current_char != '.' and not current_char.isnumeric():
current_idx -= 1
current_char = height_str[current_idx - 1]
height = float(height_str[:current_idx])
height_unit = height_str[current_idx:]
return GPSPoint(gps_time, fix_type, num_sats_used, lat, long, height,
height_unit)
def two_digit_year_to_four_digit_year(year: str) -> str:
"""
Convert a year represented by two digits to its representation in 4 digits.
Follow the POSIX and ISO C standards mentioned by the Python documentation,
quoted below.
'When 2-digit years are parsed, they are converted according to the POSIX
and ISO C standards: values 69–99 are mapped to 1969–1999, and values 0–68
are mapped to 2000–2068.'
Raise ValueError if year is outside the 0-99 range (0n counts as n).
:param year: the 2-digit year as a string
:return: the 4-digit representation of year as a string
"""
year_as_int = int(year)
if not (0 <= year_as_int <= 99):
raise ValueError(f'Given year {year} is not in the valid range.')
if int(year) < 69:
prefix = '20'
else:
prefix = '19'
return f'{prefix}{year:0>2}'
......@@ -3,10 +3,9 @@ import pathlib
import shutil
import traceback
from datetime import datetime
from typing import Union
from typing import List, Tuple, Union
from copy import deepcopy
from pathlib import Path
from typing import List, Tuple
from PySide2 import QtCore, QtWidgets, QtGui
......@@ -26,6 +25,7 @@ from sohstationviewer.view.plotting.waveform_dialog import WaveformDialog
from sohstationviewer.view.search_message.search_message_dialog import (
SearchMessageDialog
)
from sohstationviewer.view.plotting.state_of_health_widget import SOHWidget
from sohstationviewer.view.help_view import HelpBrowser
from sohstationviewer.view.ui.main_ui import UIMainWindow
from sohstationviewer.view.util.enums import LogType
......@@ -47,15 +47,15 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
super().__init__(parent)
self.setup_ui(self)
"""
dir_names: [PosixPath,] - list of absolute path of data set directory
dir_names: list of absolute path of data set directory
"""
self.dir_names = []
self.dir_names: List[Path] = []
"""
data_type: str - type of data set
"""
self.data_type = 'Unknown'
self.data_type: str = 'Unknown'
self.data_loader = DataLoader()
self.data_loader: DataLoader = DataLoader()
self.data_loader.finished.connect(self.replot_loaded_data)
"""
......@@ -67,90 +67,84 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
"""
self.forms_in_forms_menu: List[QtWidgets.QWidget] = []
"""
req_soh_chans: [str,] - list of State-Of-Health channels to read data
req_soh_chans: list of State-Of-Health channels to read data
from. For Reftek, the list of channels is fixed => may not need
"""
self.req_soh_chans = []
self.req_soh_chans: List[str] = []
"""
req_wf_chans: [str,] - list of waveform channels to read data from.
req_wf_chans: list of waveform channels to read data from.
For Reftek, it is [int,] which is list of index of data stream
to read data from.
"""
self.req_wf_chans = []
self.req_wf_chans: List[Union[str, int]] = []
"""
start_tm: float - start time of data to read
end_tm: float - end time of data to read
start_tm: start time of data to read
end_tm: end time of data to read
"""
self.start_tm = 0
self.end_tm = 0
self.start_tm: float = 0
self.end_tm: float = 0
"""
data_object: DataTypeModel - Object that keep data read from data set
for plotting
data_object: Object that keep data read from data set for plotting
"""
self.data_object = None
self.data_object: Union[DataTypeModel, None] = None
"""
min_gap: float - minimum minutes of gap length to be display on gap bar
min_gap: minimum minutes of gap length to be display on gap bar
"""
self.min_gap = None
self.min_gap: Union[float, None] = None
"""
ids_name: str - name of selected preferred channels list
pref_soh_list_name: name of selected preferred channels list
"""
self.ids_name = ''
self.pref_soh_list_name: str = ''
"""
ids: [str,] - selected preferred channels
pref_soh_list: selected preferred channels
"""
self.ids = []
self.pref_soh_list: List[str] = []
"""
ids_data_type: str - data type of the preferred channels list
pref_soh_list_data_type: data type of the preferred channels list
"""
self.ids_data_type = 'Unknown'
self.pref_soh_list_data_type: str = 'Unknown'
# Options
"""
date_format: str - format for date
date_format: format for date
"""
self.date_format = 'YYYY-MM-DD'
self.date_format: str = 'YYYY-MM-DD'
"""
mass_pos_volt_range_opt: str - option for map value/color of
mass position
mass_pos_volt_range_opt: option for map value/color of mass position
"""
self.mass_pos_volt_range_opt = 'regular'
self.mass_pos_volt_range_opt: str = 'regular'
"""
bit_weight_opt: str - option for bitweight
bit_weight_opt: option for bitweight
"""
self.bit_weight_opt = '' # currently only need one option
self.bit_weight_opt: str = '' # currently only need one option
self.get_channel_prefer()
self.yyyy_mm_dd_action.triggered.emit()
"""
waveform_dlg: PlottingWidget - widget to display waveform channels'
plotting
waveform_dlg: widget to display waveform channels' plotting
"""
self.waveform_dlg = WaveformDialog(self)
self.waveform_dlg: SOHWidget = WaveformDialog(self)
"""
tps_dlg: PlottingWidget - widget to display time-power-squared of
waveform channels
tps_dlg: dialog to display time-power-squared of waveform channels
"""
self.tps_dlg = TimePowerSquaredDialog(self)
self.tps_dlg: TimePowerSquaredDialog = TimePowerSquaredDialog(self)
"""
help_browser: HelpBrowser - Display help documents with searching
feature.
help_browser: Display help documents with searching feature.
"""
self.help_browser = HelpBrowser()
self.help_browser: HelpBrowser = HelpBrowser()
"""
search_message_dialog: SearchMessageDialog - Display log, soh message
with searching feature.
search_message_dialog: Display log, soh message with searching feature.
"""
self.search_message_dialog = SearchMessageDialog()
self.search_message_dialog: SearchMessageDialog = SearchMessageDialog()
self.pull_current_directory_from_db()
self.delete_old_temp_data_folder()
self.has_problem = False
self.is_loading_data = False
self.is_plotting_soh = False
self.is_plotting_waveform = False
self.is_plotting_tps = False
self.is_stopping = False
self.has_problem: bool = False
self.is_loading_data: bool = False
self.is_plotting_soh: bool = False
self.is_plotting_waveform: bool = False
self.is_plotting_tps: bool = False
self.is_stopping: bool = False
@QtCore.Slot()
def open_data_type(self):
......@@ -246,12 +240,13 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
channels list selected. If no list selected, re-check "All SOH"
"""
if not self.all_soh_chans_check_box.isChecked():
if self.ids == []:
if self.pref_soh_list == []:
self.all_soh_chans_check_box.setChecked(True)
else:
self.curr_soh_ids_name_line_edit.setText(self.ids_name)
self.curr_pref_soh_list_name_txtbox.setText(
self.pref_soh_list_name)
else:
self.curr_soh_ids_name_line_edit.setText('')
self.curr_pref_soh_list_name_txtbox.setText('')
@QtCore.Slot()
def set_date_format(self, display_format: str):
......@@ -274,9 +269,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
which was clicked.
"""
print(f'Opening {item.text()}')
# TODO: Do something with the Path object,
# i.e., path.open(), or path.iterdir() ...
self.read_selected_files()
@QtCore.Slot()
def change_current_directory(self):
......@@ -334,11 +327,12 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
:rtype: List[str]
"""
if (not self.all_soh_chans_check_box.isChecked() and
self.data_type != self.ids_data_type):
msg = (f"DataType detected for the selected data set is "
f"{self.data_type} which is different to IDs' "
f"{self.ids_data_type}.\n"
f"SOHStationViewer will read all data available.\n"
self.data_type != self.pref_soh_list_data_type):
msg = (f"Data Type detected for the selected data set is "
f"{self.data_type} which is different from Channel "
f"Preferences {self.pref_soh_list_name}'s Data Type: "
f"{self.pref_soh_list_data_type}.\n\n"
f"SOHStationViewer will read all data available.\n\n"
f"Do you want to continue?")
result = QtWidgets.QMessageBox.question(
self, "Confirmation", msg,
......@@ -346,9 +340,9 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
if result == QtWidgets.QMessageBox.No:
return
self.all_soh_chans_check_box.setChecked(True)
self.curr_soh_ids_name_line_edit.setText('')
self.curr_pref_soh_list_name_txtbox.setText('')
return []
return (self.ids
return (self.pref_soh_list
if not self.all_soh_chans_check_box.isChecked() else [])
@QtCore.Slot()
......@@ -377,6 +371,10 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
except AttributeError:
pass
self.req_soh_chans = (self.pref_soh_list
if not self.all_soh_chans_check_box.isChecked()
else [])
self.dir_names = [
Path(self.curr_dir_line_edit.text()).joinpath(item.text())
for item in self.open_files_list.selectedItems()]
......@@ -391,6 +389,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
self.req_soh_chans = self.get_requested_soh_chan()
self.req_wf_chans = self.get_requested_wf_chans()
start_tm_str = self.time_from_date_edit.date().toString(
QtCore.Qt.ISODate)
end_tm_str = self.time_to_date_edit.date().toString(QtCore.Qt.ISODate)
......@@ -589,18 +588,18 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow):
def get_channel_prefer(self):
"""
Read the current preferred channel list from database to set ids_name,
ids, self.ids_data_type
Read the current preferred channel list from database to set
pref_soh_list_name, pref_soh_list, self.pref_soh_list_data_type
"""
self.ids_name = ''
self.ids = []
self.pref_soh_list_name = ''
self.pref_soh_list = []
self.data_type = 'Unknown'
rows = execute_db_dict('SELECT name, IDs, dataType FROM ChannelPrefer '
'WHERE current=1')
if len(rows) > 0:
self.ids_name = rows[0]['name']
self.ids = [t.strip() for t in rows[0]['IDs'].split(',')]
self.ids_data_type = rows[0]['dataType']
self.pref_soh_list_name = rows[0]['name']
self.pref_soh_list = [t.strip() for t in rows[0]['IDs'].split(',')]
self.pref_soh_list_data_type = rows[0]['dataType']
def resizeEvent(self, event):
"""
......
......@@ -249,6 +249,8 @@ class PlottingAxes:
ax.spines['top'].set_visible(False)
ax.spines['bottom'].set_visible(False)
else:
if y.size == 0:
return
min_y = y.min()
max_y = y.max()
ax.spines['top'].set_visible(True)
......
......@@ -482,6 +482,8 @@ class PlottingWidget(QtWidgets.QScrollArea):
if hasattr(ax, 'y'):
# don't need to reset y range if ax.y not exist
new_x = ax.x[new_x_indexes]
if new_x.size == 0:
return
new_min_x = min(new_x)
new_max_x = max(new_x)
try:
......
......@@ -5,8 +5,7 @@ from sohstationviewer.view.util.plot_func_names import plot_functions
from sohstationviewer.view.plotting.plotting_widget import plotting_widget
from sohstationviewer.controller.plotting_data import get_title
from sohstationviewer.controller.util import (
display_tracking_info, apply_convert_factor)
from sohstationviewer.controller.util import apply_convert_factor
from sohstationviewer.conf import constants
......@@ -73,10 +72,13 @@ class SOHWidget(plotting_widget.PlottingWidget):
if chan_db_info['height'] == 0:
# not draw
continue
if chan_db_info['channel'] == 'DEFAULT':
if 'DEFAULT' in chan_db_info['channel']:
msg = (f"Channel {chan_id}'s "
f"definition can't be found database.")
display_tracking_info(self.tracking_box, msg, LogType.WARNING)
f"definition can't be found in database. It will"
f"be displayed in DEFAULT style.")
# TODO: give user a way to add it to DB and leave instruction
# here
self.processing_log.append((msg, LogType.WARNING))
if chan_db_info['plotType'] == '':
continue
......
......@@ -280,6 +280,10 @@ class TimePowerSquaredWidget(plotting_widget.PlottingWidget):
:param event: pick event - event when object of canvas is selected.
The event happens before button_press_event.
"""
if event.mouseevent.name == 'scroll_event':
return
if event.mouseevent.button in ('up', 'down'):
return
info_str = ""
if event.artist in self.axes:
xdata = event.mouseevent.xdata
......@@ -400,7 +404,7 @@ class TimePowerSquaredDialog(QtWidgets.QWidget):
main_layout.setSpacing(0)
"""
trackingInfoTextBrowser: QTextBrowser - to display info text
tracking_info_text_browser: QTextBrowser - to display info text
"""
self.info_text_browser = QtWidgets.QTextBrowser(self)
"""
......