diff --git a/sohstationviewer/controller/processing.py b/sohstationviewer/controller/processing.py index a369f987e1de705cd9b64338b0597062dbd9c381..e11f408d19490c7fd6c86cdcffeb6e1005777c7a 100644 --- a/sohstationviewer/controller/processing.py +++ b/sohstationviewer/controller/processing.py @@ -25,7 +25,13 @@ from sohstationviewer.view.util.enums import LogType def loadData(dataType, tracking_box, listOfDir, reqWFChans=[], reqSOHChans=[], readStart=None, readEnd=None): """ - Go through root dir and read all files in that dir and its subdirs + Load the data stored in listOfDir and store it in a DataTypeModel object. + The concrete class of the data object is based on dataType. Run on the same + thread as its caller, and so will block the GUI if called on the main + thread. It is advisable to use model.data_loader.DataLoader to load data + unless it is necessary to load data in the main thread (e.g. if there is + a need to access the call stack). + :param dataType: str - type of data read :param tracking_box: QTextBrowser - widget to display tracking info :param listOfDir: [str,] - list of directories selected by users diff --git a/sohstationviewer/controller/util.py b/sohstationviewer/controller/util.py index 8ab6be99a7cf43441d01bb78de23fe9a297e7511..fd809def89e35c9141373b152c96d9d58230a4e7 100644 --- a/sohstationviewer/controller/util.py +++ b/sohstationviewer/controller/util.py @@ -5,6 +5,8 @@ basic functions: format, validate, display tracking import os import re from datetime import datetime + +from PySide2 import QtCore from obspy import UTCDateTime import numpy as np from sohstationviewer.view.util.enums import LogType @@ -27,6 +29,7 @@ def validateFile(path2file, fileName): return True +@QtCore.Slot() def displayTrackingInfo(trackingBox, text, type=LogType.INFO): """ Display text in the given widget with different background and text colors diff --git a/sohstationviewer/model/data_loader.py b/sohstationviewer/model/data_loader.py new file mode 100644 index 0000000000000000000000000000000000000000..732d8ede8d05369a10b45a1f8a57acb58bba0987 --- /dev/null +++ b/sohstationviewer/model/data_loader.py @@ -0,0 +1,212 @@ +""" +This module provides access to a class that loads data in a separate thread. +""" +import traceback +from pathlib import Path +from typing import Union, List, Optional + +from PySide2 import QtCore, QtWidgets + +from sohstationviewer.conf import constants +from sohstationviewer.controller.util import displayTrackingInfo +from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped +from sohstationviewer.view.util.enums import LogType + + +class DataLoaderWorker(QtCore.QObject): + """ + The worker class that executes the code to load the data. + """ + finished = QtCore.Signal(DataTypeModel) + failed = QtCore.Signal() + stopped = QtCore.Signal() + notification = QtCore.Signal(QtWidgets.QTextBrowser, str, str) + button_dialog = QtCore.Signal(str, list) + button_chosen = QtCore.Signal(int) + + def __init__(self, data_type: str, tracking_box: QtWidgets.QTextBrowser, + folder: str, req_wf_chans: Union[List[str], List[int]] = [], + req_soh_chans: List[str] = [], read_start: float = 0, + read_end: float = constants.HIGHEST_INT, parent_thread=None): + super().__init__() + self.data_type = data_type + self.tracking_box = tracking_box + self.folder = folder + self.req_wf_chans = req_wf_chans + self.req_soh_chans = req_soh_chans + self.read_start = read_start + self.read_end = read_end + self.parent_thread = parent_thread + # displayTrackingInfo updates a QtWidget, which can only be done in the + # main thread. Since self.run runs in a background thread, we need to + # use signal-slot mechanism to ensure that displayTrackingInfo runs in + # the main thread. + self.notification.connect(displayTrackingInfo) + self.end_msg = None + + def run(self): + try: + if self.data_type == 'RT130': + from sohstationviewer.model.reftek.reftek import RT130 + ObjectType = RT130 + else: + from sohstationviewer.model.mseed.mseed import MSeed + ObjectType = MSeed + # Create data object without loading any data in order to connect + # its unpause slot to the loader's unpause signal + dataObject = ObjectType.get_empty_instance() + self.button_chosen.connect(dataObject.receive_pause_response, + type=QtCore.Qt.DirectConnection) + dataObject.__init__( + self.tracking_box, self.folder, + reqWFChans=self.req_wf_chans, + reqSOHhans=self.req_soh_chans, readStart=self.read_start, + readEnd=self.read_end, creator_thread=self.parent_thread, + notification_signal=self.notification, + pause_signal=self.button_dialog + ) + + except ThreadStopped: + self.end_msg = 'Data loading has been stopped' + self.stopped.emit() + except Exception: + fmt = traceback.format_exc() + self.end_msg = (f"Dir {self.folder} can't be read " + f"due to error: {str(fmt)}") + self.failed.emit() + else: + self.end_msg = f'Finished loading data stored in {self.folder}' + self.finished.emit(dataObject) + + +class DataLoader: + """ + The class that coordinate the loading of data using multiple threads. The + code inside has to be encapsulated in a class because a connection between + a signal and a receiver is automatically disconnected when either of them + is deleted (e.g. goes out of scope). + """ + + def __init__(self): + self.running = False + self.thread: Optional[QtCore.QThread] = None + self.worker: Optional[DataLoaderWorker] = None + + def init_loader(self, data_type: str, tracking_box: QtWidgets.QTextBrowser, + list_of_dir: List[Union[str, Path]], + req_wf_chans: Union[List[str], List[int]] = [], + req_soh_chans: List[str] = [], read_start: float = 0, + read_end: float = constants.HIGHEST_INT): + """ + Initialize the data loader. Construct the thread and worker and connect + them together. Separated from the actual loading of the data to allow + the main window a chance to connect its slots to the data loader. + + :param data_type: the type of data being loaded. 'RT130' for RT130 data + and 'Centaur', 'Pegasus', and 'Q330' for MSeed data. + :param tracking_box: the widget used to display tracking info + :param list_of_dir: list of directories selected by users + :param req_wf_chans: list of requested waveform channels + :param req_soh_chans: list of requested SOH channel + :param read_start: the time before which no data is read + :param read_end: the time after which no data is read + :return: + """ + if self.running: + # TODO: implement showing an error window + print('Already running') + return False + + self.running = True + self.thread = QtCore.QThread() + self.worker = DataLoaderWorker( + data_type, + tracking_box, + list_of_dir[0], # Only work on one directory for now. + req_wf_chans=req_wf_chans, + req_soh_chans=req_soh_chans, + read_start=read_start, + read_end=read_end, + parent_thread=self.thread + ) + + self.connect_worker_signals() + + self.worker.moveToThread(self.thread) + + def connect_worker_signals(self): + """ + Connect the signals of the data loader to the appropriate slots. + """ + # Connection order from https://realpython.com/python-pyqt-qthread + self.thread.started.connect(self.worker.run) + + self.worker.finished.connect(self.thread.quit) + self.worker.failed.connect(self.thread.quit) + self.worker.stopped.connect(self.thread.quit) + + self.thread.finished.connect(self.thread.deleteLater) + self.thread.finished.connect(self.load_end) + self.thread.finished.connect(self.worker.deleteLater) + + self.worker.button_dialog.connect(self.create_button_dialog) + + def load_data(self): + """ + Start the data loading thread. + """ + self.thread.start() + + @QtCore.Slot() + def load_end(self): + """ + Cleans up after data loading ended. Called even if the loading fails or + is stopped. + + Currently does the following: + - Set running state of self to False + """ + displayTrackingInfo(self.worker.tracking_box, + self.worker.end_msg, LogType.INFO) + print(self.worker.end_msg) + self.running = False + + @QtCore.Slot() + def create_button_dialog(self, msg: str, button_labels: List[str]): + """ + Create a modal dialog with buttons. Show the dialog and send the user's + choice to the data object being created. + + :param msg: the instruction shown to the user + :type msg: str + :param button_labels: the list of labels that are shown on the buttons + :type button_labels: List[str] + """ + msg_box = QtWidgets.QMessageBox() + msg_box.setText(msg) + buttons = [] + for label in button_labels: + # RT130's labels have type Tuple[str, int], so we need to convert + # them to strings. + if not isinstance(label, str): + # When we convert a tuple to a string, any strings in the tuple + # will be surrounded by quotes in the result string. We remove + # those quotes before displaying them to the user for aesthetic + # reasons. + label = str(label).replace("'", '').replace('"', '') + buttons.append( + msg_box.addButton(label, QtWidgets.QMessageBox.ActionRole) + ) + abortButton = msg_box.addButton(QtWidgets.QMessageBox.Abort) + + msg_box.exec_() + + if msg_box.clickedButton() == abortButton: + # The default choice is the first item, so we default to it if the + # user presses the abort button. An alternative choice is to stop + # when the user presses the abort button. + chosen_idx = 0 + else: + chosen_idx = buttons.index(msg_box.clickedButton()) + + self.worker.button_chosen.emit(chosen_idx) diff --git a/sohstationviewer/model/data_type_model.py b/sohstationviewer/model/data_type_model.py index 4c1cf6e67f037c5d18163fac54d7780768541935..6bf892951e3408c660fcc9962f7284a8edd8ed85 100644 --- a/sohstationviewer/model/data_type_model.py +++ b/sohstationviewer/model/data_type_model.py @@ -1,7 +1,11 @@ +from __future__ import annotations import os from tempfile import mkdtemp import shutil +from typing import Optional + +from PySide2 import QtCore from sohstationviewer.controller.util import displayTrackingInfo from sohstationviewer.conf import constants @@ -14,10 +18,22 @@ class WrongDataTypeError(Exception): self.args = (args, kwargs) +class ThreadStopped(Exception): + """ + An exception that is raised when the user requests for the data loader + thread to be stopped. + """ + def __init__(self, *args, **kwargs): + self.args = (args, kwargs) + + class DataTypeModel(): def __init__(self, trackingBox, folder, readChanOnly=False, reqWFChans=[], reqSOHChans=[], readStart=0, readEnd=constants.HIGHEST_INT, + creator_thread: Optional[QtCore.QThread] = None, + notification_signal: Optional[QtCore.Signal] = None, + pause_signal: Optional[QtCore.Signal] = None, *args, **kwargs): """ Super class for different data type to process data from data files @@ -29,6 +45,13 @@ class DataTypeModel(): :param reqSOHChans: list of str - requested SOH channel list :param readStart: float - requested start time to read :param readEnd: float - requested end time to read + :param creator_thread: the thread the current DataTypeModel instance is + being created in. If None, the DataTypeModel instance is being + created in the main thread + :param notification_signal: signal used to send notifications to the + main thread. Only not None when creator_thread is not None + :param pause_signal: signal used to notify the main thread that the + data loader is paused. """ self.trackingBox = trackingBox self.dir = folder @@ -37,7 +60,17 @@ class DataTypeModel(): self.readChanOnly = readChanOnly self.readStart = readStart self.readEnd = readEnd - + if creator_thread is None: + err_msg = ( + 'A signal is not None while running in main thread' + ) + assert notification_signal is None, err_msg + assert pause_signal is None, err_msg + self.creator_thread = QtCore.QThread() + else: + self.creator_thread = creator_thread + self.notification_signal = notification_signal + self.pause_signal = pause_signal """ processingLog: [(message, type)] - record the progress of processing """ @@ -192,6 +225,9 @@ class DataTypeModel(): shutil.rmtree(self.tmpDir) os.mkdir(self.tmpDir) + self._pauser = QtCore.QSemaphore() + self.pause_response = None + def __del__(self): print("delete dataType Object") try: @@ -220,7 +256,15 @@ class DataTypeModel(): :param text: str - message to display :param type: str - type of message (error/warning/info) """ - displayTrackingInfo(self.trackingBox, text, type) + # displayTrackingInfo updates a QtWidget, which can only be done in the + # main thread. So, if we are running in a background thread + # (i.e. self.creator_thread is not None), we need to use signal slot + # mechanism to ensure that displayTrackingInfo is run in the main + # thread. + if self.notification_signal is None: + displayTrackingInfo(self.trackingBox, text, type) + else: + self.notification_signal.emit(self.trackingBox, text, type) if type != LogType.INFO: self.processingLog.append((text, type)) @@ -228,6 +272,23 @@ class DataTypeModel(): def create_data_object(cls, data_type, tracking_box, folder, readChanOnly=False, reqWFChans=[], reqSOHChans=[], readStart=0, readEnd=constants.HIGHEST_INT): + """ + Create a DataTypeModel object, with the concrete class being based on + data_type. Run on the same thread as its caller, and so will block the + GUI if called on the main thread. Do not call this method directly. + Instead, call the wrapper controller.processing.loadData. + + :param data_type: str - type of data read + :param tracking_box: QTextBrowser - widget to display tracking info + :param folder: [str,] - the data directory + :param readChanOnly: if True, only read channel name + :param reqWFChans: [str,] - requested waveform channel list + :param reqSOHChans: [str,] - requested soh channel list + :param readStart: [float,] - start time of read data + :param readEnd: [float,] - finish time of read data + :return: DataTypeModel - object that keep the data read from + folder + """ if data_type == 'RT130': from sohstationviewer.model.reftek.reftek import RT130 dataObject = RT130( @@ -242,6 +303,59 @@ class DataTypeModel(): readStart=readStart, readEnd=readEnd) return dataObject + def pause(self) -> None: + """ + Pause the thread this DataTypeModel instance is in. Works by trying + to acquire a semaphore that is not available, which causes the thread + to block. + + Note: due to how this is implemented, each call to pause will require + a corresponding call to unpause. Thus, it is inadvisable to call this + method more than once. + + Caution: not safe to call in the main thread. Unless a background + thread releases the semaphore, the whole program will freeze. + """ + self._pauser.acquire() + + @QtCore.Slot() + def unpause(self): + """ + Unpause the thread this DataTypeModel instance is in. Works by trying + to acquire a semaphore that is not available, which causes the thread + to block. + + Caution: due to how this is implemented, if unpause is called before + pause, the thread will not be paused until another call to pause is + made. Also, like pause, each call to unpause must be matched by another + call to pause for everything to work. + """ + self._pauser.release() + + @QtCore.Slot() + def receive_pause_response(self, response: object): + """ + Receive a response to a request made to another thread and unpause the + calling thread. + + :param response: the response to the request made + :type response: object + """ + self.pause_response = response + self.unpause() + + @classmethod + def get_empty_instance(cls) -> DataTypeModel: + """ + Create an empty data object. Useful if a DataTypeModel instance is + needed, but it is undesirable to load a data set. Basically wraps + __new__(). + + :return: an empty data object + :rtype: DataTypeModel + """ + return cls.__new__(cls) + def save_temp_data_folder_to_database(self): executeDB(f'UPDATE PersistentData SET FieldValue="{self.tmpDir}" WHERE' f' FieldName="tempDataDirectory"') diff --git a/sohstationviewer/model/mseed/mseed.py b/sohstationviewer/model/mseed/mseed.py index 9ac67a4f0cb5c01ceb9ec11f6a68cb8f4598f607..4c09b26075c92594fada14c4e0352961f0e86cac 100644 --- a/sohstationviewer/model/mseed/mseed.py +++ b/sohstationviewer/model/mseed/mseed.py @@ -2,25 +2,17 @@ MSeed object to hold and process MSeed data """ - import os from pathlib import Path -from PySide2 import QtWidgets - -from sohstationviewer.view.select_buttons_dialog import ( - SelectButtonDialog) -from sohstationviewer.view.util.enums import LogType - -from sohstationviewer.model.data_type_model import DataTypeModel -from sohstationviewer.model.mseed.from_mseedpeek.mseed_header import readHdrs -from sohstationviewer.model.handling_data import ( - readWaveformMSeed, squash_gaps, checkWFChan, - sortData, readSOHTrace) - from sohstationviewer.conf import constants - from sohstationviewer.controller.util import validateFile +from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped +from sohstationviewer.model.handling_data import ( + readWaveformMSeed, squash_gaps, checkWFChan, sortData, readSOHTrace, +) +from sohstationviewer.model.mseed.from_mseedpeek.mseed_header import readHdrs +from sohstationviewer.view.util.enums import LogType class MSeed(DataTypeModel): @@ -48,10 +40,16 @@ class MSeed(DataTypeModel): """ self.netsProbInFile = {} + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() self.read_soh_and_index_waveform(self.dir) + + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() self.selectedKey = self.selectStaID() + if self.selectedKey is None: - return + raise ThreadStopped() if len(self.reqWFChans) != 0: self.readWFFiles(self.selectedKey) @@ -91,6 +89,9 @@ class MSeed(DataTypeModel): for path, sub_dirs, files in os.walk(folder): for file_name in files: + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() + path2file = Path(path).joinpath(file_name) if not validateFile(path2file, file_name): continue @@ -191,16 +192,16 @@ class MSeed(DataTypeModel): stream = soh_streams[sta_id][chan_id] stream.merge() + + tr = stream[0] if len(stream) > 1: nets = [tr.stats['network'].strip() for tr in stream] nets += [f"Combine to {n}" for n in nets] msg = (f"There are more than one net for sta {sta_id}.\n" "Please select one or combine all to one.") - msg_box = SelectButtonDialog(message=msg, - button_labels=nets) - msg_box.exec_() - sel_net = nets[msg_box.ret] - + self.pause_signal.emit(msg, nets) + self.pause() + sel_net = nets[self.pause_response] if "Combine" not in sel_net: tr = [tr for tr in stream if tr.stats['network'] == sel_net][0] @@ -212,8 +213,6 @@ class MSeed(DataTypeModel): self.nets.add(sel_net) stream.merge() tr = stream[0] - else: - tr = stream[0] gaps_in_stream = stream.get_gaps() all_gaps += [[g[4].timestamp, g[5].timestamp] @@ -245,21 +244,11 @@ class MSeed(DataTypeModel): selectedStaID = stats[0] if len(stats) > 1: msg = ("There are more than one stations in the given data.\n" - "Please select one to one to display") - msgBox = QtWidgets.QMessageBox() - msgBox.setText(msg) - staButtons = [] - for staID in stats: - staButtons.append(msgBox.addButton( - staID, QtWidgets.QMessageBox.ActionRole)) - abortButton = msgBox.addButton(QtWidgets.QMessageBox.Abort) - - msgBox.exec_() - - if msgBox.clickedButton() == abortButton: - return selectedStaID - selectedIdx = staButtons.index(msgBox.clickedButton()) - selectedStaID = stats[selectedIdx] + "Please select one to display") + self.pause_signal.emit(msg, stats) + self.pause() + selectedStaID = stats[self.pause_response] + self.trackInfo(f'Select Station {selectedStaID}', LogType.INFO) return selectedStaID @@ -291,6 +280,8 @@ class MSeed(DataTypeModel): 'readData'][chanID]['tracesInfo'] for fileInfo in self.waveformData[staID]['filesInfo'][chanID]: + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped # file have been read if fileInfo['read']: continue diff --git a/sohstationviewer/model/reftek/reftek.py b/sohstationviewer/model/reftek/reftek.py index 2c1cefeaf5bc8c6811fb7393405a6abbf15033ac..6333c44caae4a2924790542572473b8e47e611ce 100755 --- a/sohstationviewer/model/reftek/reftek.py +++ b/sohstationviewer/model/reftek/reftek.py @@ -6,13 +6,12 @@ import os from pathlib import Path import numpy as np -from PySide2 import QtWidgets from obspy.core import Stream from sohstationviewer.model.reftek.from_rt2ms import ( core, soh_packet, packet) from sohstationviewer.model.reftek.logInfo import LogInfo -from sohstationviewer.model.data_type_model import DataTypeModel +from sohstationviewer.model.data_type_model import DataTypeModel, ThreadStopped from sohstationviewer.model.handling_data import ( readWaveformReftek, squash_gaps, sortData, readMPTrace, readText) @@ -34,11 +33,19 @@ class RT130(DataTypeModel): self.keys = set() self.reqDSs = self.reqWFChans self.massPosStream = {} + + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() self.readSOH_indexWaveform(self.dir) + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() self.selectedKey = self.selectKey() if self.selectedKey is None: - return + raise ThreadStopped() + + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() if len(self.reqWFChans) != 0: self.readWFFiles(self.selectedKey) @@ -51,6 +58,8 @@ class RT130(DataTypeModel): count = 0 for path, subdirs, files in os.walk(folder): for fileName in files: + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() path2file = Path(path).joinpath(fileName) if not validateFile(path2file, fileName): continue @@ -76,21 +85,11 @@ class RT130(DataTypeModel): selectedKey = self.keys[0] if len(self.keys) > 1: msg = ("There are more than one keys in the given data.\n" - "Please select one to one to display") - msgBox = QtWidgets.QMessageBox() - msgBox.setText(msg) - staButtons = [] - for key in self.keys: - staButtons.append(msgBox.addButton( - key, QtWidgets.QMessageBox.ActionRole)) - abortButton = msgBox.addButton(QtWidgets.QMessageBox.Abort) - - msgBox.exec_() - - if msgBox.clickedButton() == abortButton: - return selectedKey - selectedIdx = staButtons.index(msgBox.clickedButton()) - selectedKey = self.keys[selectedIdx] + "Please select one to display") + self.pause_signal.emit(msg, self.keys) + self.pause() + selectedKey = self.keys[self.pause_response] + self.trackInfo(f'Select Key {selectedKey}', LogType.INFO) return selectedKey @@ -113,6 +112,8 @@ class RT130(DataTypeModel): for DS in self.waveformData[key]['filesInfo']: readData = self.waveformData[key]['readData'] for fileInfo in self.waveformData[key]['filesInfo'][DS]: + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() # file have been read if fileInfo['read']: continue @@ -285,6 +286,8 @@ class RT130(DataTypeModel): is too big to consider calculating gaps. """ for k in self.logData: + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() if k == 'TEXT': continue if k not in self.dataTime: @@ -320,6 +323,8 @@ class RT130(DataTypeModel): self.gaps = {k: [] for k in self.keys} self.massPosData = {k: {} for k in self.keys} for k in self.massPosStream: + if self.creator_thread.isInterruptionRequested(): + raise ThreadStopped() stream = self.massPosStream[k] stream.merge() for tr in stream: diff --git a/sohstationviewer/view/main_window.py b/sohstationviewer/view/main_window.py index fe88497b2051ed131f25a99ea27fc26b5c6072c9..4ae23402950735299223e659f0a7acb0604f39f7 100755 --- a/sohstationviewer/view/main_window.py +++ b/sohstationviewer/view/main_window.py @@ -1,29 +1,36 @@ -import pathlib import os +import pathlib import shutil -from pathlib import Path -from datetime import datetime from copy import deepcopy -from PySide2 import QtCore, QtWidgets +from datetime import datetime +from pathlib import Path -from sohstationviewer.view.search_message.search_message_dialog import ( - SearchMessageDialog) -from sohstationviewer.view.ui.main_ui import UIMainWindow +from PySide2 import QtCore, QtWidgets, QtGui + +from sohstationviewer.conf.constants import TM_FORMAT +from sohstationviewer.controller.processing import detectDataType +from sohstationviewer.controller.util import displayTrackingInfo +from sohstationviewer.database.proccessDB import executeDB_dict, executeDB +from sohstationviewer.model.data_loader import DataLoader +from sohstationviewer.model.data_type_model import DataTypeModel from sohstationviewer.view.calendar.calendar_dialog import ( - CalendarDialog) -from sohstationviewer.view.file_list_widget import FileListItem -from sohstationviewer.view.plotting.waveform_dialog import WaveformDialog -from sohstationviewer.view.plotting.time_power_squared_dialog import ( - TimePowerSquaredDialog) + CalendarDialog +) +from sohstationviewer.view.channel_prefer_dialog import ChannelPreferDialog +from sohstationviewer.view.db_config.channel_dialog import ChannelDialog from sohstationviewer.view.db_config.data_type_dialog import DataTypeDialog from sohstationviewer.view.db_config.param_dialog import ParamDialog -from sohstationviewer.view.db_config.channel_dialog import ChannelDialog from sohstationviewer.view.db_config.plot_type_dialog import PlotTypeDialog -from sohstationviewer.view.channel_prefer_dialog import ChannelPreferDialog - -from sohstationviewer.controller.processing import loadData, detectDataType -from sohstationviewer.database.proccessDB import executeDB_dict, executeDB -from sohstationviewer.conf.constants import TM_FORMAT +from sohstationviewer.view.file_list_widget import FileListItem +from sohstationviewer.view.plotting.time_power_squared_dialog import ( + TimePowerSquaredDialog +) +from sohstationviewer.view.plotting.waveform_dialog import WaveformDialog +from sohstationviewer.view.search_message.search_message_dialog import ( + SearchMessageDialog +) +from sohstationviewer.view.ui.main_ui import UIMainWindow +from sohstationviewer.view.util.enums import LogType class MainWindow(QtWidgets.QMainWindow, UIMainWindow): @@ -41,6 +48,9 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): data_type: str - type of data set """ self.data_type = 'Unknown' + + self.data_loader = DataLoader() + """ req_soh_chans: [str,] - list of State-Of-Health channels to read data from. For Reftek, the list of channels is fixed => may not need @@ -285,13 +295,39 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): end_tm_str = self.time_to_date_edit.date().toString(QtCore.Qt.ISODate) self.start_tm = datetime.strptime(start_tm_str, TM_FORMAT).timestamp() self.end_tm = datetime.strptime(end_tm_str, TM_FORMAT).timestamp() - self.data_object = loadData(self.data_type, - self.tracking_info_text_browser, - self.dir_names, - reqWFChans=self.req_wf_chans, - reqSOHChans=self.req_soh_chans, - readStart=self.start_tm, - readEnd=self.end_tm) + + self.data_loader.init_loader(self.data_type, + self.tracking_info_text_browser, + self.dir_names, + req_wf_chans=self.req_wf_chans, + req_soh_chans=self.req_soh_chans, + read_start=self.start_tm, + read_end=self.end_tm) + self.data_loader.worker.finished.connect(self.plot_data) + self.data_loader.load_data() + + @QtCore.Slot() + def stop_load_data(self): + # TODO: find a way to stop the data loader without a long wait. + """ + Request the data loader thread to stop. The thread will stop at the + earliest possible point, meaning that the wait is variable and can be + very long. + """ + if self.data_loader.running: + self.data_loader.thread.requestInterruption() + displayTrackingInfo(self.tracking_info_text_browser, + 'Stopping data loading...', + LogType.INFO) + + @QtCore.Slot() + def plot_data(self, data_obj: DataTypeModel): + """ + Process the loaded data and pass control to the plotter. + + :param data_obj: the data object that contains the loaded data. + """ + self.data_object = data_obj self.replot_loaded_data() @QtCore.Slot() @@ -417,6 +453,20 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): if len(rows) > 0 and rows[0]['FieldValue']: self.set_current_directory(rows[0]['FieldValue']) + def closeEvent(self, event: QtGui.QCloseEvent) -> None: + """ + Cleans up when the user exits the program. Currently only clean up + running data loaders. + + :param event: parameter of method being overridden + """ + displayTrackingInfo(self.tracking_info_text_browser, 'Cleaning up...', + 'info') + if self.data_loader.running: + self.data_loader.thread.requestInterruption() + self.data_loader.thread.quit() + self.data_loader.thread.wait() + def delete_old_temp_data_folder(self): rows = executeDB( 'SELECT FieldValue FROM PersistentData ' diff --git a/sohstationviewer/view/ui/main_ui.py b/sohstationviewer/view/ui/main_ui.py index 85f1d90f0f517e5e80e630cbaf41fa96aaf02ef7..b27229d04de72eb2f50c5dd817e6c89cb11964db 100755 --- a/sohstationviewer/view/ui/main_ui.py +++ b/sohstationviewer/view/ui/main_ui.py @@ -704,3 +704,4 @@ class UIMainWindow(object): self.prefer_soh_chan_button.clicked.connect( main_window.open_channel_preferences) self.read_button.clicked.connect(main_window.read_selected_files) + self.stop_button.clicked.connect(main_window.stop_load_data)