diff --git a/documentation/06 _ Select SOH.help.md b/documentation/06 _ Select SOH.help.md index 41e6eef89e12b35e57a55a2ebc8040dda98612a6..74ff7ba9b4f1a43488619266b229ce25eacabcc3 100644 --- a/documentation/06 _ Select SOH.help.md +++ b/documentation/06 _ Select SOH.help.md @@ -72,4 +72,4 @@ channels in the data set. + "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 +reading the data set and the SOH channel will be plotted in this order. \ No newline at end of file diff --git a/sohstationviewer/model/data_type_model.py b/sohstationviewer/model/data_type_model.py index 4f7b6253f786b0f1c7b51d3347779e44e5a6cecb..2f8c6a5dd21e56315dc7a9c6a6527002d3f5fd75 100644 --- a/sohstationviewer/model/data_type_model.py +++ b/sohstationviewer/model/data_type_model.py @@ -309,7 +309,6 @@ class DataTypeModel(): self.sort_all_data() self.track_info("Combine data.", LogType.INFO) self.combine_traces_in_all_data() - self.check_not_found_soh_channels() for key in self.data_time: if self.data_time[key] == [constants.HIGHEST_INT, 0]: # this happens when there is text or ascii only in the data @@ -456,19 +455,6 @@ class DataTypeModel(): execute_db(f'UPDATE PersistentData SET FieldValue="{self.tmp_dir}" ' f'WHERE FieldName="tempDataDirectory"') - def check_not_found_soh_channels(self): - all_chans_meet_req = ( - list(self.soh_data[self.selected_key].keys()) + - list(self.mass_pos_data[self.selected_key].keys()) + - list(self.log_data[self.selected_key].keys())) - - not_found_chans = [c for c in self.req_soh_chans - if c not in all_chans_meet_req] - if not_found_chans != []: - msg = (f"No data found for the following channels: " - f"{', '.join( not_found_chans)}") - self.processing_log.append((msg, LogType.WARNING)) - def combine_times_data_of_traces_w_spr_less_or_equal_1( self, data: Dict[str, Dict], selected_key: Union[(str, str), str], data_name: str): diff --git a/sohstationviewer/view/main_window.py b/sohstationviewer/view/main_window.py index 2e47c78a88eb4a38836764b0267d98cf453b0a92..073165a5145f10c3da87b76b0b6144de734f622a 100755 --- a/sohstationviewer/view/main_window.py +++ b/sohstationviewer/view/main_window.py @@ -546,7 +546,8 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): self.is_plotting_waveform or self.is_plotting_tps) if is_working: msg = 'Already working' - display_tracking_info(self.tracking_info_text_browser, msg, 'info') + display_tracking_info(self.tracking_info_text_browser, + msg, LogType.INFO) return self.has_problem = False @@ -796,7 +797,8 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): try: self.plotting_widget.plot_channels( - d_obj, sel_key, self.start_tm, self.end_tm, time_tick_total) + d_obj, sel_key, self.start_tm, self.end_tm, time_tick_total, + self.req_soh_chans) except Exception: fmt = traceback.format_exc() msg = f"Can't plot SOH data due to error: {str(fmt)}" @@ -951,7 +953,8 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): 'WHERE current=1') if len(rows) > 0: 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 = [t.strip() for t in rows[0]['IDs'].split(',') + if t.strip() != ''] self.pref_soh_list_data_type = rows[0]['dataType'] def resizeEvent(self, event): @@ -982,7 +985,7 @@ class MainWindow(QtWidgets.QMainWindow, UIMainWindow): """ display_tracking_info(self.tracking_info_text_browser, 'Cleaning up...', - 'info') + LogType.INFO) if self.data_loader.running: self.data_loader.thread.requestInterruption() self.data_loader.thread.quit() diff --git a/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py b/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py index 35d207e62a5bb46624d6c4b362a66c89b5f863b2..0e36e7ab4e08bee2f3caba711566357eec7473f0 100644 --- a/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py +++ b/sohstationviewer/view/plotting/plotting_widget/multi_threaded_plotting_widget.py @@ -11,6 +11,8 @@ from sohstationviewer.view.plotting.plotting_widget.plotting_processor import ( from sohstationviewer.view.plotting.plotting_widget.plotting_widget import ( PlottingWidget) from sohstationviewer.view.util.enums import LogType +from sohstationviewer.view.util.functions import ( + replace_actual_question_chans, remove_not_found_chans) from sohstationviewer.controller.util import display_tracking_info from sohstationviewer.controller.plotting_data import get_title @@ -28,7 +30,8 @@ class MultiThreadedPlottingWidget(PlottingWidget): def __init__(self, *args, **kwargs): PlottingWidget.__init__(self, *args, **kwargs) self.data_processors: List[PlottingChannelProcessor] = [] - + # pref_order: order of channels to be plotted + self.pref_order: List[str] = [] # Only one data processor can run at a time, so it is not a big problem # self.thread_pool = QtCore.QThreadPool() @@ -105,19 +108,33 @@ class MultiThreadedPlottingWidget(PlottingWidget): return True def create_plotting_channel_processors( - self, plotting_data: Dict, need_db_info: bool = False) -> None: + self, plotting_data: Dict, + need_db_info: bool = False) -> None: """ - Create a data processor for each channel data. + Create a data processor for each channel data in the order of + pref_order. If pref_order isn't given, process in order of + plotting_data. :param plotting_data: dict of data by chan_id :param need_db_info: flag to get db info """ - for chan_id in plotting_data: + chan_order = self.pref_order if self.pref_order \ + else sorted(list(plotting_data.keys())) + chan_order = replace_actual_question_chans( + chan_order, list(plotting_data.keys())) + chan_order = remove_not_found_chans( + chan_order, list(plotting_data.keys()), self.processing_log) + + not_plot_chans = [] + for chan_id in chan_order: if need_db_info: - chan_db_info = get_chan_plot_info( - chan_id, self.parent.data_type, self.c_mode) - if chan_db_info['height'] == 0: + chan_db_info = get_chan_plot_info(chan_id, + self.parent.data_type, + self.c_mode) + if (chan_db_info['height'] == 0 or + chan_db_info['plotType'] == ''): # not draw + not_plot_chans.append(chan_id) continue if 'DEFAULT' in chan_db_info['channel']: msg = (f"Channel {chan_id}'s " @@ -127,14 +144,16 @@ class MultiThreadedPlottingWidget(PlottingWidget): # instruction here self.processing_log.append((msg, LogType.WARNING)) - if chan_db_info['plotType'] == '': - continue - plotting_data[chan_id]['chan_db_info'] = chan_db_info + if not_plot_chans != []: + msg = (f"The database settings 'plotType' or 'height' show not to " + f"be plotted for the following channels: " + f"{', '.join( not_plot_chans)}") + self.processing_log.append((msg, LogType.WARNING)) self.move_soh_channels_with_link_to_the_end() - for chan_id in plotting_data: + for chan_id in chan_order: if 'chan_db_info' not in plotting_data[chan_id]: continue channel_processor = PlottingChannelProcessor( @@ -165,7 +184,8 @@ class MultiThreadedPlottingWidget(PlottingWidget): for channel in channels_to_move: self.plotting_data1[channel] = self.plotting_data1.pop(channel) - def plot_channels(self, d_obj, key, start_tm, end_tm, time_ticks_total): + def plot_channels(self, d_obj, key, start_tm, end_tm, time_ticks_total, + pref_order=[]): """ Prepare to plot waveform/SOH/mass-position data by creating a data processor for each channel, then, run the processors. @@ -175,12 +195,14 @@ class MultiThreadedPlottingWidget(PlottingWidget): :param start_tm: requested start time to read :param end_tm: requested end time to read :param time_ticks_total: max number of tick to show on time bar + :param pref_order: order of channels to be plotted """ + self.pref_order = pref_order if not self.is_working: self.reset_widget() self.is_working = True start_msg = f'Plotting {self.name} data...' - display_tracking_info(self.tracking_box, start_msg, 'info') + display_tracking_info(self.tracking_box, start_msg, LogType.INFO) ret = self.init_plot(d_obj, key, start_tm, end_tm, time_ticks_total) if not ret: @@ -188,8 +210,10 @@ class MultiThreadedPlottingWidget(PlottingWidget): self.clean_up() self.finished.emit() return + self.create_plotting_channel_processors(self.plotting_data1, True) self.create_plotting_channel_processors(self.plotting_data2, True) + self.process_channel() @QtCore.Slot() @@ -307,6 +331,6 @@ class MultiThreadedPlottingWidget(PlottingWidget): all running background threads. """ display_tracking_info(self.tracking_box, - f'{self.name} plot stopped', 'info') + f'{self.name} plot stopped', LogType.INFO) self.is_working = False self.stopped.emit() diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting.py b/sohstationviewer/view/plotting/plotting_widget/plotting.py index 1a9268988a4960b05616b498961e8e3029a54e1a..faa1f90b097b42a274860aecfc252e4e0ee5df18 100644 --- a/sohstationviewer/view/plotting/plotting_widget/plotting.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting.py @@ -37,6 +37,7 @@ class Plotting: self.parent.plotting_bot, plot_h, has_min_max_lines=False) ax.x = None ax.plot([0], [0], linestyle="") + ax.chan_db_info = None return ax def plot_multi_color_dots(self, c_data, chan_db_info, chan_id, @@ -120,6 +121,7 @@ class Plotting: ax.x = x else: ax.linkedX = x + ax.chan_db_info = chan_db_info return ax def plot_up_down_dots(self, c_data, chan_db_info, chan_id, ax, linked_ax): @@ -184,6 +186,7 @@ class Plotting: ax.x = x else: ax.linkedX = x + ax.chan_db_info = chan_db_info return ax def plot_time_dots(self, c_data, chan_db_info, chan_id, ax, linked_ax): @@ -226,6 +229,7 @@ class Plotting: ax.x_list = x_list else: ax.linkedX = x_list + ax.chan_db_info = chan_db_info return ax def plot_lines_dots(self, c_data, chan_db_info, chan_id, @@ -315,6 +319,7 @@ class Plotting: else: ax.linkedX = x_list ax.linkedY = y_list + ax.chan_db_info = chan_db_info return ax def plot_lines_s_rate(self, c_data, chan_db_info, chan_id, ax, linked_ax): @@ -399,4 +404,5 @@ class Plotting: zorder=constants.Z_ORDER['DOT']) ax.x_list = x_list ax.y_list = y_list + ax.chan_db_info = chan_db_info return ax diff --git a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py index a26463af3e4afec0fba9b2b2c21f72a72e6ba8d4..da53fd8fea8b450de097386e6fbbb483377cfb63 100755 --- a/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py +++ b/sohstationviewer/view/plotting/plotting_widget/plotting_widget.py @@ -604,7 +604,8 @@ class PlottingWidget(QtWidgets.QScrollArea): pass if new_min_y is not None: - self.plotting_axes.set_axes_ylim(ax, new_min_y, new_max_y) + self.plotting_axes.set_axes_ylim( + ax, new_min_y, new_max_y, ax.chan_db_info) def draw(self): """ diff --git a/sohstationviewer/view/util/functions.py b/sohstationviewer/view/util/functions.py index 254f32030c796164cd0399d3e7d938174df27c8d..58e3faab6a4945964b1f8094712631ebc74479ef 100644 --- a/sohstationviewer/view/util/functions.py +++ b/sohstationviewer/view/util/functions.py @@ -328,5 +328,49 @@ def get_index_from_time(chan_data: List[np.ndarray], tm: float, val: float) \ return list_idx, section_idx +def remove_not_found_chans( + chan_order: List[str], actual_chans: List[str], + processing_log: List[Tuple[str, LogType]]) -> List[str]: + """ + Remove channels that are not found in actual_chans from chan_order. + + :param chan_order: list of channels in order that user wants to plot + :param actual_chans: The actual channel list + :param processing_log: The log list to keep track with not found channels + :return: chan_order from which not found channels have been removed. + """ + not_found_chans = [c for c in chan_order if c not in actual_chans] + if not_found_chans != []: + msg = (f"No data found for the following channels: " + f"{', '.join(not_found_chans)}") + processing_log.append((msg, LogType.WARNING)) + return [c for c in chan_order if c not in not_found_chans] + + +def replace_actual_question_chans( + chan_order: List[str], actual_chans: List[str]) -> List[str]: + """ + Remove channels end with '?' from chan_order and replace with corresponding + channels found in actual channels. + + :param chan_order: The list of channel that have channels end with '?' + :param actual_chans: The actual channel list + :return: chan_order that have channels end with '?' replaced by actual + channels. + """ + question_chans = [c for c in chan_order if c.endswith('?')] + for qc in question_chans: + actual_question_chans = [c for c in list(actual_chans) + if qc[:-1] == c[:-1]] + if actual_question_chans: + question_idx = chan_order.index(qc) + chan_order.remove(qc) + # replace a question channel with the actual channels that it + # represent for + chan_order[question_idx:question_idx] = \ + sorted(actual_question_chans) + return chan_order + + if __name__ == '__main__': create_table_of_content_file(Path('../../../documentation')) diff --git a/tests/test_view/test_util_functions.py b/tests/test_view/test_util_functions.py index bc59a41236a024290fa3540f5cf9a7106229de2e..8ad6187487f9eb673f656267f49908fc7ddfced9 100644 --- a/tests/test_view/test_util_functions.py +++ b/tests/test_view/test_util_functions.py @@ -9,7 +9,8 @@ from sohstationviewer.view.util.functions import ( get_soh_messages_for_view, log_str, is_doc_file, create_search_results_file, create_table_of_content_file, check_chan_wildcards_format, check_masspos, get_total_miny_maxy, - extract_netcodes, get_index_from_time + extract_netcodes, get_index_from_time, remove_not_found_chans, + replace_actual_question_chans ) from sohstationviewer.view.util.enums import LogType @@ -501,3 +502,36 @@ class TestGetIndexFromTime(TestCase): self.plotting_data['CH2'], 3, 4) self.assertEqual(list_idx, 1) self.assertEqual(section_idx, 0) + + +class RemoveNotFoundChansClass(TestCase): + def test_remove_not_found_chans(self): + chan_order = ['A', 'B', 'C', 'D'] + actual_chans = ['C', 'D', 'E', 'F'] + processing_log = [] + expected_new_chan_order = ['C', 'D'] + expected_processing_log = [ + ("No data found for the following channels: A, B", + LogType.WARNING)] + + ret = remove_not_found_chans(chan_order, actual_chans, processing_log) + self.assertListEqual(ret, expected_new_chan_order) + self.assertEqual(processing_log, expected_processing_log) + + +class ReplaceActualQuestChans(TestCase): + def test_question_chans_in_actual_chans(self): + chan_order = ['A', 'B', 'C?', 'D'] + actual_chans = ['C1', 'C3', 'C2', 'D', 'E', 'F'] + expected_new_chan_order = ['A', 'B', 'C1', 'C2', 'C3', 'D'] + + ret = replace_actual_question_chans(chan_order, actual_chans) + self.assertListEqual(ret, expected_new_chan_order) + + def test_question_chans_not_in_actual_chans(self): + chan_order = ['A?', 'B', 'C', 'D'] + actual_chans = ['C', 'D', 'E', 'F'] + expected_new_chan_order = ['A?', 'B', 'C', 'D'] + + ret = replace_actual_question_chans(chan_order, actual_chans) + self.assertListEqual(ret, expected_new_chan_order)