From 4a54ef54cb926540aca475db0f8e26c5942033da Mon Sep 17 00:00:00 2001 From: Alessandro Date: Sat, 31 Aug 2019 19:28:22 +0200 Subject: [PATCH] Major refactor of filters and spaceweather --- .gitignore | 1 + artemis-workspace.code-workspace | 7 + src/artemis.py | 1017 +----------------------------- src/audio_player.py | 6 +- src/constants.py | 2 +- src/filters.py | 850 +++++++++++++++++++++++++ src/spaceweathermanager.py | 297 +++++++++ src/themesmanager.py | 69 +- src/utilities.py | 23 + 9 files changed, 1210 insertions(+), 1062 deletions(-) create mode 100644 artemis-workspace.code-workspace create mode 100644 src/filters.py create mode 100644 src/spaceweathermanager.py diff --git a/.gitignore b/.gitignore index 9a6b398..b969a23 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ src/themes/.current_theme designer.bat launch.bat .vscode/ +.code-workspace diff --git a/artemis-workspace.code-workspace b/artemis-workspace.code-workspace new file mode 100644 index 0000000..362d7c2 --- /dev/null +++ b/artemis-workspace.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} \ No newline at end of file diff --git a/src/artemis.py b/src/artemis.py index 397c185..ab67473 100644 --- a/src/artemis.py +++ b/src/artemis.py @@ -13,8 +13,7 @@ from PyQt5.QtWidgets import (QMainWindow, QDesktopWidget, QListWidgetItem, QMessageBox, - QSplashScreen, - QTreeWidgetItem,) + QSplashScreen,) from PyQt5.QtGui import QPixmap from PyQt5 import uic from PyQt5.QtCore import (QFileInfo, @@ -22,22 +21,19 @@ from PyQt5.QtCore import (QFileInfo, pyqtSlot,) from audio_player import AudioPlayer -from weatherdata import SpaceWeatherData, ForecastData +from weatherdata import ForecastData from download_window import DownloadWindow -from switchable_label import SwitchableLabelsIterable +from spaceweathermanager import SpaceWeatherManager from constants import (Constants, - Ftype, GfdType, Database, ChecksumWhat, Messages, Signal,) from themesmanager import ThemeManager +from filters import Filters from utilities import (checksum_ok, - uncheck_and_emit, pop_up, - connect_events_to_func, - filters_limit, is_undef_freq, is_undef_band, format_numbers, @@ -87,300 +83,24 @@ class Artemis(QMainWindow, Ui_MainWindow): self.signal_names = [] self.total_signals = 0 - self.switchable_r_labels = SwitchableLabelsIterable( - self.r0_now_lbl, - self.r1_now_lbl, - self.r2_now_lbl, - self.r3_now_lbl, - self.r4_now_lbl, - self.r5_now_lbl + # Forecast + self.forecast_info_btn.clicked.connect( + lambda: webbrowser.open(Constants.SPACE_WEATHER_INFO) ) + self.forecast_data = ForecastData(self) + self.update_forecast_bar.clicked.connect(self.start_update_forecast) + self.update_forecast_bar.set_idle() + self.forecast_data.update_complete.connect(self.update_forecast) - self.switchable_s_labels = SwitchableLabelsIterable( - self.s0_now_lbl, - self.s1_now_lbl, - self.s2_now_lbl, - self.s3_now_lbl, - self.s4_now_lbl, - self.s5_now_lbl - ) + # Spaceweather manager + self.spaceweather_screen = SpaceWeatherManager(self) - self.switchable_g_now_labels = SwitchableLabelsIterable( - self.g0_now_lbl, - self.g1_now_lbl, - self.g2_now_lbl, - self.g3_now_lbl, - self.g4_now_lbl, - self.g5_now_lbl - ) - - self.switchable_g_today_labels = SwitchableLabelsIterable( - self.g0_today_lbl, - self.g1_today_lbl, - self.g2_today_lbl, - self.g3_today_lbl, - self.g4_today_lbl, - self.g5_today_lbl - ) - - self.k_storm_labels = SwitchableLabelsIterable( - self.k_ex_sev_storm_lbl, - self.k_very_sev_storm_lbl, - self.k_sev_storm_lbl, - self.k_maj_storm_lbl, - self.k_min_storm_lbl, - self.k_active_lbl, - self.k_unsettled_lbl, - self.k_quiet_lbl, - self.k_very_quiet_lbl, - self.k_inactive_lbl - ) - - self.a_storm_labels = SwitchableLabelsIterable( - self.a_sev_storm_lbl, - self.a_maj_storm_lbl, - self.a_min_storm_lbl, - self.a_active_lbl, - self.a_unsettled_lbl, - self.a_quiet_lbl - ) - - self.space_weather_labels = ( - self.space_weather_lbl_0, - self.space_weather_lbl_1, - self.space_weather_lbl_2, - self.space_weather_lbl_3, - self.space_weather_lbl_4, - self.space_weather_lbl_5, - self.space_weather_lbl_6, - self.space_weather_lbl_7, - self.space_weather_lbl_8 - ) - - for lab in self.space_weather_labels: - lab.set_default_stylesheet() - - self.space_weather_label_container.labels = self.space_weather_labels - self.space_weather_label_name_container.labels = [ - self.eme_lbl, - self.ms_lbl, - self.muf_lbl, - self.hi_lbl, - self.eu50_lbl, - self.eu70_lbl, - self.eu144_lbl, - self.na_lbl, - self.aurora_lbl - ] self.theme_manager = ThemeManager(self) - # Manage frequency filters. - self.frequency_filters_btns = ( - self.elf_filter_btn, - self.slf_filter_btn, - self.ulf_filter_btn, - self.vlf_filter_btn, - self.lf_filter_btn, - self.mf_filter_btn, - self.hf_filter_btn, - self.vhf_filter_btn, - self.uhf_filter_btn, - self.shf_filter_btn, - self.ehf_filter_btn, - ) - - connect_events_to_func( - events_to_connect=[self.lower_freq_spinbox.valueChanged, - self.upper_freq_spinbox.valueChanged, - self.lower_freq_filter_unit.currentTextChanged, - self.upper_freq_filter_unit.currentTextChanged, - self.activate_low_freq_filter_btn.toggled], - fun_to_connect=self.set_min_value_upper_limit, - fun_args=[self.lower_freq_filter_unit, - self.lower_freq_spinbox, - self.upper_freq_filter_unit, - self.upper_freq_spinbox] - ) - - connect_events_to_func( - events_to_connect=[self.lower_freq_spinbox.valueChanged, - self.upper_freq_spinbox.valueChanged, - self.lower_freq_filter_unit.currentTextChanged, - self.upper_freq_filter_unit.currentTextChanged, - self.activate_low_freq_filter_btn.clicked, - self.activate_up_freq_filter_btn.clicked, - self.lower_freq_confidence.valueChanged, - self.upper_freq_confidence.valueChanged], - fun_to_connect=self.set_band_filter_label, - fun_args=[self.activate_low_freq_filter_btn, - self.lower_freq_spinbox, - self.lower_freq_filter_unit, - self.lower_freq_confidence, - self.activate_up_freq_filter_btn, - self.upper_freq_spinbox, - self.upper_freq_filter_unit, - self.upper_freq_confidence, - self.freq_range_lbl] - ) - - self.activate_low_freq_filter_btn.toggled.connect( - partial(self.activate_if_toggled, - self.activate_low_freq_filter_btn, - self.lower_freq_spinbox, - self.lower_freq_filter_unit, - self.lower_freq_confidence) - ) - - self.activate_up_freq_filter_btn.toggled.connect( - partial(self.activate_if_toggled, - self.activate_up_freq_filter_btn, - self.upper_freq_spinbox, - self.upper_freq_filter_unit, - self.upper_freq_confidence) - ) - - self.apply_remove_freq_filter_btn.set_texts(Constants.APPLY, Constants.REMOVE) - self.apply_remove_freq_filter_btn.set_slave_filters( - simple_ones=[ - *self.frequency_filters_btns, - self.include_undef_freqs, - self.activate_low_freq_filter_btn, - self.activate_up_freq_filter_btn - ], - radio_1=self.activate_low_freq_filter_btn, - ruled_by_radio_1=[ - self.lower_freq_spinbox, - self.lower_freq_filter_unit, - self.lower_freq_confidence - ], - radio_2=self.activate_up_freq_filter_btn, - ruled_by_radio_2=[ - self.upper_freq_spinbox, - self.upper_freq_filter_unit, - self.upper_freq_confidence - ] - ) - self.apply_remove_freq_filter_btn.clicked.connect(self.display_signals) - self.reset_frequency_filters_btn.clicked.connect( - partial(self.reset_fb_filters, Ftype.FREQ) - ) - - # Manage bandwidth filters. - - connect_events_to_func( - events_to_connect=[self.lower_band_spinbox.valueChanged, - self.upper_band_spinbox.valueChanged, - self.lower_band_filter_unit.currentTextChanged, - self.upper_band_filter_unit.currentTextChanged, - self.activate_low_band_filter_btn.toggled], - fun_to_connect=self.set_min_value_upper_limit, - fun_args=[self.lower_band_filter_unit, - self.lower_band_spinbox, - self.upper_band_filter_unit, - self.upper_band_spinbox] - ) - - connect_events_to_func( - events_to_connect=[self.lower_band_spinbox.valueChanged, - self.upper_band_spinbox.valueChanged, - self.lower_band_filter_unit.currentTextChanged, - self.upper_band_filter_unit.currentTextChanged, - self.activate_low_band_filter_btn.clicked, - self.activate_up_band_filter_btn.clicked, - self.lower_band_confidence.valueChanged, - self.upper_band_confidence.valueChanged], - fun_to_connect=self.set_band_filter_label, - fun_args=[self.activate_low_band_filter_btn, - self.lower_band_spinbox, - self.lower_band_filter_unit, - self.lower_band_confidence, - self.activate_up_band_filter_btn, - self.upper_band_spinbox, - self.upper_band_filter_unit, - self.upper_band_confidence, - self.band_range_lbl] - ) - - self.activate_low_band_filter_btn.toggled.connect( - partial(self.activate_if_toggled, - self.activate_low_band_filter_btn, - self.lower_band_spinbox, - self.lower_band_filter_unit, - self.lower_band_confidence) - ) - - self.activate_up_band_filter_btn.toggled.connect( - partial(self.activate_if_toggled, - self.activate_up_band_filter_btn, - self.upper_band_spinbox, - self.upper_band_filter_unit, - self.upper_band_confidence) - ) - - self.apply_remove_band_filter_btn.set_texts(Constants.APPLY, - Constants.REMOVE) - self.apply_remove_band_filter_btn.set_slave_filters( - simple_ones=[ - self.include_undef_bands, - self.activate_low_band_filter_btn, - self.activate_up_band_filter_btn - ], - radio_1=self.activate_low_band_filter_btn, - ruled_by_radio_1=[ - self.lower_band_spinbox, - self.lower_band_filter_unit, - self.lower_band_confidence - ], - radio_2=self.activate_up_band_filter_btn, - ruled_by_radio_2=[ - self.upper_band_spinbox, - self.upper_band_filter_unit, - self.upper_band_confidence - ] - ) - self.apply_remove_band_filter_btn.clicked.connect(self.display_signals) - self.reset_band_filters_btn.clicked.connect( - partial(self.reset_fb_filters, Ftype.BAND) - ) - -# Manage category filters - - # Order matters! - self.cat_filter_btns = [ - self.military_btn, - self.radar_btn, - self.active_btn, - self.inactive_btn, - self.ham_btn, - self.commercial_btn, - self.aviation_btn, - self.marine_btn, - self.analogue_btn, - self.digital_btn, - self.trunked_btn, - self.utility_btn, - self.sat_btn, - self.navigation_btn, - self.interfering_btn, - self.number_stations_btn, - self.time_signal_btn - ] - - self.apply_remove_cat_filter_btn.set_texts(Constants.APPLY, Constants.REMOVE) - self.apply_remove_cat_filter_btn.set_slave_filters( - simple_ones=[ - *self.cat_filter_btns, - self.cat_at_least_one, - self.cat_all - ] - ) - self.apply_remove_cat_filter_btn.clicked.connect(self.display_signals) - self.reset_cat_filters_btn.clicked.connect(self.reset_cat_filters) + self.filters = Filters(self) # ####################################################################################### - self.reset_filters_btn.clicked.connect(self.reset_all_filters) - UrlColors = namedtuple("UrlColors", ["inactive", "active", "clicked"]) self.url_button.colors = UrlColors("#9f9f9f", "#4c75ff", "#942ccc") self.category_labels = [ @@ -415,68 +135,6 @@ class Artemis(QMainWindow, Ui_MainWindow): self.url_button.clicked.connect(self.go_to_web_page_signal) - # Set mode TreeView - - self.set_mode_tree_widget() - self.mode_tree_widget.itemSelectionChanged.connect(self.manage_mode_selections) - self.reset_mode_filters_btn.clicked.connect(self.reset_mode_filters) - self.apply_remove_mode_filter_btn.set_texts(Constants.APPLY, Constants.REMOVE) - self.apply_remove_mode_filter_btn.set_slave_filters( - simple_ones=[ - self.mode_tree_widget, - self.include_unknown_modes_btn - ] - ) - self.apply_remove_mode_filter_btn.clicked.connect(self.display_signals) - - # Set modulation filter screen. - - self.search_bar_modulation.textEdited.connect(self.show_matching_modulations) - self.apply_remove_modulation_filter_btn.set_texts(Constants.APPLY, Constants.REMOVE) - self.apply_remove_modulation_filter_btn.set_slave_filters( - simple_ones=[ - self.search_bar_modulation, - self.modulation_list - ] - ) - self.apply_remove_modulation_filter_btn.clicked.connect(self.display_signals) - self.reset_modulation_filters_btn.clicked.connect(self.reset_modulation_filters) - self.modulation_list.itemClicked.connect(self.remove_if_unselected_modulation) - - # Set location filter screen. - - self.search_bar_location.textEdited.connect(self.show_matching_locations) - self.apply_remove_location_filter_btn.set_texts(Constants.APPLY, Constants.REMOVE) - self.apply_remove_location_filter_btn.set_slave_filters( - simple_ones=[ - self.search_bar_location, - self.locations_list - ] - ) - self.apply_remove_location_filter_btn.clicked.connect(self.display_signals) - self.reset_location_filters_btn.clicked.connect(self.reset_location_filters) - self.locations_list.itemClicked.connect(self.remove_if_unselected_location) - - # Set ACF filter screen. - self.apply_remove_acf_filter_btn.set_texts(Constants.APPLY, Constants.REMOVE) - self.apply_remove_acf_filter_btn.set_slave_filters( - simple_ones=[ - self.include_undef_acf, - self.acf_spinbox, - self.acf_confidence - ] - ) - self.apply_remove_acf_filter_btn.clicked.connect(self.display_signals) - self.reset_acf_filters_btn.clicked.connect(self.reset_acf_filters) - self.acf_info_btn.clicked.connect(lambda: webbrowser.open(Constants.ACF_DOCS)) - - connect_events_to_func( - events_to_connect=[self.acf_spinbox.valueChanged, - self.acf_confidence.valueChanged], - fun_to_connect=self.set_acf_interval_label, - fun_args=None - ) - # GFD self.freq_search_gfd_btn.clicked.connect(partial(self.go_to_gfd, GfdType.FREQ)) self.location_search_gfd_btn.clicked.connect(partial(self.go_to_gfd, GfdType.LOC)) @@ -515,24 +173,6 @@ class Artemis(QMainWindow, Ui_MainWindow): BandLabel(self.ehf_left, self.ehf, self.ehf_right), ] - # Space weather - self.info_now_btn.clicked.connect( - lambda: webbrowser.open(Constants.SPACE_WEATHER_INFO) - ) - self.update_now_bar.clicked.connect(self.start_update_space_weather) - self.update_now_bar.set_idle() - self.space_weather_data = SpaceWeatherData() - self.space_weather_data.update_complete.connect(self.update_space_weather) - - # Forecast - self.forecast_info_btn.clicked.connect( - lambda: webbrowser.open(Constants.SPACE_WEATHER_INFO) - ) - self.forecast_data = ForecastData(self) - self.update_forecast_bar.clicked.connect(self.start_update_forecast) - self.update_forecast_bar.set_idle() - self.forecast_data.update_complete.connect(self.update_forecast) - self.main_tab.currentChanged.connect(self.hide_show_right_widget) # Final operations. @@ -565,16 +205,6 @@ class Artemis(QMainWindow, Ui_MainWindow): self.update_forecast_bar.set_updating() self.forecast_data.update() - @pyqtSlot() - def start_update_space_weather(self): - """Start the update of the space weather screen. - - Start the corresponding thread. - """ - if not self.space_weather_data.is_updating: - self.update_now_bar.set_updating() - self.space_weather_data.update() - @pyqtSlot(bool) def update_forecast(self, status_ok): """Update the 3-day forecast screen after a successful download. @@ -590,175 +220,6 @@ class Artemis(QMainWindow, Ui_MainWindow): text=Messages.BAD_DOWNLOAD_MSG).show() self.forecast_data.remove_data() - @pyqtSlot(bool) - def update_space_weather(self, status_ok): - """Update the space weather screen after a successful download. - - If the download was not successful throw a warning. In any case remove - the downloaded data. - """ - self.update_now_bar.set_idle() - if status_ok: - xray_long = safe_cast(self.space_weather_data.xray[-1][7], float) - - def format_text(letter, power): - return letter + f"{xray_long * 10**power:.1f}" - - if xray_long < 1e-8 and xray_long != -1.00e+05: - self.peak_flux_lbl.setText(format_text("= 1e-8 and xray_long < 1e-7: - self.peak_flux_lbl.setText(format_text("A", 8)) - elif xray_long >= 1e-7 and xray_long < 1e-6: - self.peak_flux_lbl.setText(format_text("B", 7)) - elif xray_long >= 1e-6 and xray_long < 1e-5: - self.peak_flux_lbl.setText(format_text("C", 6)) - elif xray_long >= 1e-5 and xray_long < 1e-4: - self.peak_flux_lbl.setText(format_text("M", 5)) - elif xray_long >= 1e-4: - self.peak_flux_lbl.setText(format_text("X", 4)) - elif xray_long == -1.00e+05: - self.peak_flux_lbl.setText("No Data") - - if xray_long < 1e-5 and xray_long != -1.00e+05: - self.switchable_r_labels.switch_on(self.r0_now_lbl) - elif xray_long >= 1e-5 and xray_long < 5e-5: - self.switchable_r_labels.switch_on(self.r1_now_lbl) - elif xray_long >= 5e-5 and xray_long < 1e-4: - self.switchable_r_labels.switch_on(self.r2_now_lbl) - elif xray_long >= 1e-4 and xray_long < 1e-3: - self.switchable_r_labels.switch_on(self.r3_now_lbl) - elif xray_long >= 1e-3 and xray_long < 2e-3: - self.switchable_r_labels.switch_on(self.r4_now_lbl) - elif xray_long >= 2e-3: - self.switchable_r_labels.switch_on(self.r5_now_lbl) - elif xray_long == -1.00e+05: - self.switchable_r_labels.switch_off_all() - - pro10 = safe_cast(self.space_weather_data.prot_el[-1][8], float) - if pro10 < 10 and pro10 != -1.00e+05: - self.switchable_s_labels.switch_on(self.s0_now_lbl) - elif pro10 >= 10 and pro10 < 100: - self.switchable_s_labels.switch_on(self.s1_now_lbl) - elif pro10 >= 100 and pro10 < 1000: - self.switchable_s_labels.switch_on(self.s2_now_lbl) - elif pro10 >= 1000 and pro10 < 10000: - self.switchable_s_labels.switch_on(self.s3_now_lbl) - elif pro10 >= 10000 and pro10 < 100000: - self.switchable_s_labels.switch_on(self.s4_now_lbl) - elif pro10 >= 100000: - self.switchable_s_labels.switch_on(self.s5_now_lbl) - elif pro10 == -1.00e+05: - self.switchable_s_labels.switch_off_all() - - k_index = safe_cast( - self.space_weather_data.ak_index[8][11].replace('.', ''), int - ) - self.k_index_lbl.setText(str(k_index)) - a_index = safe_cast( - self.space_weather_data.ak_index[7][7].replace('.', ''), int - ) - self.a_index_lbl.setText(str(a_index)) - - if k_index == 0: - self.switchable_g_now_labels.switch_on(self.g0_now_lbl) - self.k_storm_labels.switch_on(self.k_inactive_lbl) - self.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ") - elif k_index == 1: - self.switchable_g_now_labels.switch_on(self.g0_now_lbl) - self.k_storm_labels.switch_on(self.k_very_quiet_lbl) - self.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ") - elif k_index == 2: - self.switchable_g_now_labels.switch_on(self.g0_now_lbl) - self.k_storm_labels.switch_on(self.k_quiet_lbl) - self.expected_noise_lbl.setText(" S1 - S2 (-115 dBm) ") - elif k_index == 3: - self.switchable_g_now_labels.switch_on(self.g0_now_lbl) - self.k_storm_labels.switch_on(self.k_unsettled_lbl) - self.expected_noise_lbl.setText(" S2 - S3 (-110 dBm) ") - elif k_index == 4: - self.switchable_g_now_labels.switch_on(self.g0_now_lbl) - self.k_storm_labels.switch_on(self.k_active_lbl) - self.expected_noise_lbl.setText(" S3 - S4 (-100 dBm) ") - elif k_index == 5: - self.switchable_g_now_labels.switch_on(self.g1_now_lbl) - self.k_storm_labels.switch_on(self.k_min_storm_lbl) - self.expected_noise_lbl.setText(" S4 - S6 (-90 dBm) ") - elif k_index == 6: - self.switchable_g_now_labels.switch_on(self.g2_now_lbl) - self.k_storm_labels.switch_on(self.k_maj_storm_lbl) - self.expected_noise_lbl.setText(" S6 - S9 (-80 dBm) ") - elif k_index == 7: - self.switchable_g_now_labels.switch_on(self.g3_now_lbl) - self.k_storm_labels.switch_on(self.k_sev_storm_lbl) - self.expected_noise_lbl.setText(" S9 - S20 (>-60 dBm) ") - elif k_index == 8: - self.switchable_g_now_labels.switch_on(self.g4_now_lbl) - self.k_storm_labels.switch_on(self.k_very_sev_storm_lbl) - self.expected_noise_lbl.setText(" S20 - S30 (>-60 dBm) ") - elif k_index == 9: - self.switchable_g_now_labels.switch_on(self.g5_now_lbl) - self.k_storm_labels.switch_on(self.k_ex_sev_storm_lbl) - self.expected_noise_lbl.setText(" S30+ (>>-60 dBm) ") - self.expected_noise_lbl.switch_on() - - if a_index >= 0 and a_index < 8: - self.a_storm_labels.switch_on(self.a_quiet_lbl) - elif a_index >= 8 and a_index < 16: - self.a_storm_labels.switch_on(self.a_unsettled_lbl) - elif a_index >= 16 and a_index < 30: - self.a_storm_labels.switch_on(self.a_active_lbl) - elif a_index >= 30 and a_index < 50: - self.a_storm_labels.switch_on(self.a_min_storm_lbl) - elif a_index >= 50 and a_index < 100: - self.a_storm_labels.switch_on(self.a_maj_storm_lbl) - elif a_index >= 100 and a_index < 400: - self.a_storm_labels.switch_on(self.a_sev_storm_lbl) - - index = self.space_weather_data.geo_storm[6].index("was") + 1 - k_index_24_hmax = safe_cast( - self.space_weather_data.geo_storm[6][index], int - ) - if k_index_24_hmax == 0: - self.switchable_g_today_labels.switch_on(self.g0_today_lbl) - elif k_index_24_hmax == 1: - self.switchable_g_today_labels.switch_on(self.g0_today_lbl) - elif k_index_24_hmax == 2: - self.switchable_g_today_labels.switch_on(self.g0_today_lbl) - elif k_index_24_hmax == 3: - self.switchable_g_today_labels.switch_on(self.g0_today_lbl) - elif k_index_24_hmax == 4: - self.switchable_g_today_labels.switch_on(self.g0_today_lbl) - elif k_index_24_hmax == 5: - self.switchable_g_today_labels.switch_on(self.g1_today_lbl) - elif k_index_24_hmax == 6: - self.switchable_g_today_labels.switch_on(self.g2_today_lbl) - elif k_index_24_hmax == 7: - self.switchable_g_today_labels.switch_on(self.g3_today_lbl) - elif k_index_24_hmax == 8: - self.switchable_g_today_labels.switch_on(self.g4_today_lbl) - elif k_index_24_hmax == 9: - self.switchable_g_today_labels.switch_on(self.g5_today_lbl) - - val = safe_cast( - self.space_weather_data.ak_index[7][2].replace('.', ''), int - ) - self.sfi_lbl.setText(f"{val}") - val = safe_cast( - [x[4] for x in self.space_weather_data.sgas - if "SSN" in x][0], int - ) - self.sn_lbl.setText(f"{val:d}") - - for label, pixmap in zip(self.space_weather_labels, - self.space_weather_data.images): - label.pixmap = pixmap - label.make_transparent() - label.apply_pixmap() - elif not self.closing: - pop_up(self, title=Messages.BAD_DOWNLOAD, - text=Messages.BAD_DOWNLOAD_MSG).show() - self.space_weather_data.remove_data() - @pyqtSlot() def go_to_gfd(self, by): """Open a browser tab with the GFD site. @@ -780,69 +241,6 @@ class Artemis(QMainWindow, Ui_MainWindow): except Exception: pass - @pyqtSlot(QListWidgetItem) - def remove_if_unselected_modulation(self, item): - """If an item is unselected from the modulations list, hide the item.""" - if not item.isSelected(): - self.show_matching_modulations(self.search_bar_modulation.text()) - - @pyqtSlot(QListWidgetItem) - def remove_if_unselected_location(self, item): - """If an item is unselected from the locations list, hide the item.""" - if not item.isSelected(): - self.show_matching_locations(self.search_bar_location.text()) - - @pyqtSlot(str) - def show_matching_modulations(self, text): - """Show the modulations which matches 'text'. - - The match criterion is defined in 'show_matching_strings'.""" - self.show_matching_strings(self.modulation_list, text) - - @pyqtSlot(str) - def show_matching_locations(self, text): - """Show the locations which matches 'text'. - - The match criterion is defined in 'show_matching_strings'.""" - self.show_matching_strings(self.locations_list, text) - - def show_matching_strings(self, list_elements, text): - """Show all elements of QListWidget that matches (even partially) a target text. - - Arguments: - list_elements -- the QListWidget - text -- the target text. - """ - for index in range(list_elements.count()): - item = list_elements.item(index) - if text.lower() in item.text().lower() or item.isSelected(): - item.setHidden(False) - else: - item.setHidden(True) - - def set_mode_tree_widget(self): - """Construct the QTreeWidget for the 'Mode' screen.""" - for parent, children in Constants.MODES.items(): - iparent = QTreeWidgetItem([parent]) - self.mode_tree_widget.addTopLevelItem(iparent) - for child in children: - ichild = QTreeWidgetItem([child]) - iparent.addChild(ichild) - self.mode_tree_widget.expandAll() - - def manage_mode_selections(self): - """Rules the selection of childs items of the 'Mode' QTreeWidget. - - If a parent is selected all its children will be selected as well. - """ - selected_items = self.mode_tree_widget.selectedItems() - parents = Constants.MODES.keys() - for parent in parents: - for item in selected_items: - if parent == item.text(0): - for i in range(len(Constants.MODES[parent])): - item.child(i).setSelected(True) - def set_initial_size(self): """Handle high resolution screens. @@ -1050,109 +448,6 @@ class Artemis(QMainWindow, Ui_MainWindow): values.insert(0, Constants.UNKNOWN) return values - @pyqtSlot() - def set_min_value_upper_limit(self, lower_combo_box, - lower_spin_box, - upper_combo_box, - upper_spin_box): - """Forbid to a lower limit to be greater than the corresponding upper one. - - Used for frequency and bandwidth screens.""" - if lower_spin_box.isEnabled(): - unit_conversion = {'Hz': ['kHz', 'MHz', 'GHz'], - 'kHz': ['MHz', 'GHz'], - 'MHz': ['GHz']} - lower_units = lower_combo_box.currentText() - upper_units = upper_combo_box.currentText() - lower_value = lower_spin_box.value() - inf_limit = (lower_value * Constants.CONVERSION_FACTORS[lower_units]) \ - // Constants.CONVERSION_FACTORS[upper_units] - counter = 0 - while inf_limit > upper_spin_box.maximum(): - counter += 1 - inf_limit //= 1000 - if upper_spin_box.minimum() != inf_limit: - upper_spin_box.setMinimum(inf_limit) - if counter > 0: - new_unit = unit_conversion[upper_units][counter - 1] - upper_combo_box.disconnect() - upper_combo_box.setCurrentText(new_unit) - upper_combo_box.currentTextChanged.connect( - partial( - self.set_min_value_upper_limit, - lower_combo_box, - lower_spin_box, - upper_combo_box, - upper_spin_box - ) - ) - - @pyqtSlot() - def set_band_filter_label(self, - activate_low_btn, - lower_spinbox, - lower_unit, - lower_confidence, - activate_up_btn, - upper_spinbox, - upper_unit, - upper_confidence, - range_lbl): - """Display the actual range applied for the signal's property search. - - Used for frequency and bandwidth screens.""" - activate_low = False - activate_high = False - color = self.inactive_color - title = '' - to_display = '' - if activate_low_btn.isChecked(): - activate_low = True - color = self.active_color - min_value = lower_spinbox.value() - if lower_confidence.value() != 0: - min_value -= lower_spinbox.value() * lower_confidence.value() / 100 - to_display += str(round(min_value, Constants.MAX_DIGITS)) \ - + ' ' + lower_unit.currentText() - else: - to_display += 'DC' - to_display += Constants.RANGE_SEPARATOR - if activate_up_btn.isChecked(): - max_value = upper_spinbox.value() - activate_high = True - color = self.active_color - if upper_confidence.value() != 0: - max_value += upper_spinbox.value() * upper_confidence.value() / 100 - to_display += str(round(max_value, Constants.MAX_DIGITS)) + ' ' \ - + upper_unit.currentText() - else: - to_display += 'INF' - if activate_low and activate_high: - title = 'Band-pass\n\n' - elif activate_low and not activate_high: - title = 'Low-pass\n\n' - elif not activate_low and activate_high: - title = 'High-pass\n\n' - else: - title = "Selected range:\n\n" - to_display = "Inactive" - to_display = title + to_display - range_lbl.setText(to_display) - range_lbl.setStyleSheet(f'color: {color};') - - @pyqtSlot() - def set_acf_interval_label(self): - """Display the actual acf interval for the search.""" - tolerance = self.acf_spinbox.value() * self.acf_confidence.value() / 100 - if tolerance > 0: - val = round(self.acf_spinbox.value() - tolerance, Constants.MAX_DIGITS) - to_display = f"Selected range:\n\n{val}" + Constants.RANGE_SEPARATOR \ - + f"{round(self.acf_spinbox.value() + tolerance, Constants.MAX_DIGITS)} ms" - else: - to_display = f"Selected value:\n\n{self.acf_spinbox.value()} ms" - self.acf_range_lbl.setText(to_display) - self.acf_range_lbl.setStyleSheet(f"color: {self.active_color}") - @pyqtSlot() def activate_if_toggled(self, radio_btn, *widgets): """If radio_btn is toggled, activate all *widgets. @@ -1169,14 +464,7 @@ class Artemis(QMainWindow, Ui_MainWindow): text = self.search_bar.text() available_signals = 0 for index, signal_name in enumerate(self.signal_names): - if all([text.lower() in signal_name.lower(), - self.frequency_filters_ok(signal_name), - self.band_filters_ok(signal_name), - self.category_filters_ok(signal_name), - self.mode_filters_ok(signal_name), - self.modulation_filters_ok(signal_name), - self.location_filters_ok(signal_name), - self.acf_filters_ok(signal_name)]): + if text.lower() in signal_name.lower() and self.filters.ok(signal_name): self.signals_list.item(index).setHidden(False) available_signals += 1 else: @@ -1195,267 +483,6 @@ class Artemis(QMainWindow, Ui_MainWindow): f"{available_signals} out of {self.total_signals} signals displayed." ) - @pyqtSlot() - def reset_fb_filters(self, ftype): - """Reset the Frequency or Bandwidth depending on 'ftype'. - - ftype can be either Ftype.FREQ or Ftype.BAND. - """ - if ftype != Ftype.FREQ and ftype != Ftype.BAND: - raise ValueError("Wrong ftype in function 'reset_fb_filters'") - - apply_remove_btn = getattr(self, 'apply_remove_' + ftype + '_filter_btn') - include_undef_btn = getattr(self, 'include_undef_' + ftype + 's') - activate_low = getattr(self, 'activate_low_' + ftype + '_filter_btn') - activate_up = getattr(self, 'activate_up_' + ftype + '_filter_btn') - lower_unit = getattr(self, 'lower_' + ftype + '_filter_unit') - upper_unit = getattr(self, 'upper_' + ftype + '_filter_unit') - lower_spinbox = getattr(self, 'lower_' + ftype + '_spinbox') - upper_spinbox = getattr(self, 'upper_' + ftype + '_spinbox') - lower_confidence = getattr(self, 'lower_' + ftype + '_confidence') - upper_confidence = getattr(self, 'lower_' + ftype + '_confidence') - - default_val = 1 if ftype == Ftype.FREQ else 5000 - if ftype == Ftype.FREQ: - for f in self.frequency_filters_btns: - if f.isChecked(): - f.setChecked(False) - uncheck_and_emit(apply_remove_btn) - if include_undef_btn.isChecked(): - include_undef_btn.setChecked(False) - uncheck_and_emit(activate_low) - uncheck_and_emit(activate_up) - lower_unit.setCurrentText("MHz") - upper_unit.setCurrentText("MHz") - lower_spinbox.setValue(default_val) - upper_spinbox.setMinimum(1) - upper_spinbox.setValue(default_val) - lower_confidence.setValue(0) - upper_confidence.setValue(0) - - @pyqtSlot() - def reset_cat_filters(self): - """Reset the category filter screen.""" - uncheck_and_emit(self.apply_remove_cat_filter_btn) - for f in self.cat_filter_btns: - if f.isChecked(): - f.setChecked(False) - self.cat_at_least_one.setChecked(True) - - @pyqtSlot() - def reset_mode_filters(self): - """Reset the mode filter screen.""" - uncheck_and_emit(self.apply_remove_mode_filter_btn) - parents = Constants.MODES.keys() - selected_children = [] - for item in self.mode_tree_widget.selectedItems(): - if item.text(0) in parents: - item.setSelected(False) - else: - selected_children.append(item) - for children in selected_children: - children.setSelected(False) - if self.include_unknown_modes_btn.isChecked(): - self.include_unknown_modes_btn.setChecked(False) - - @pyqtSlot() - def reset_modulation_filters(self): - """Reset the modulation filter screen.""" - uncheck_and_emit(self.apply_remove_modulation_filter_btn) - self.search_bar_modulation.setText('') - self.show_matching_strings( - self.modulation_list, - self.search_bar_modulation.text() - ) - for i in range(self.modulation_list.count()): - if self.modulation_list.item(i).isSelected(): - self.modulation_list.item(i).setSelected(False) - - @pyqtSlot() - def reset_location_filters(self): - """Reset the location filter screen.""" - uncheck_and_emit(self.apply_remove_location_filter_btn) - self.search_bar_location.setText('') - self.show_matching_strings( - self.locations_list, - self.search_bar_location.text() - ) - for i in range(self.locations_list.count()): - if self.locations_list.item(i).isSelected(): - self.locations_list.item(i).setSelected(False) - - @pyqtSlot() - def reset_acf_filters(self): - """Reset the acf filter screen.""" - uncheck_and_emit(self.apply_remove_acf_filter_btn) - if self.include_undef_acf.isChecked(): - self.include_undef_acf.setChecked(False) - self.acf_spinbox.setValue(50) - self.acf_confidence.setValue(0) - - def frequency_filters_ok(self, signal_name): - """Evalaute if the signal matches the frequency filters.""" - if not self.apply_remove_freq_filter_btn.isChecked(): - return True - undef_freq = is_undef_freq(self.db.loc[signal_name]) - if undef_freq: - if self.include_undef_freqs.isChecked(): - return True - else: - return False - - signal_freqs = ( - safe_cast(self.db.at[signal_name, Signal.INF_FREQ], int), - safe_cast(self.db.at[signal_name, Signal.SUP_FREQ], int) - ) - - band_filter_ok = False - any_checked = False - for btn, band_limits in zip(self.frequency_filters_btns, Constants.BANDS): - if btn.isChecked(): - any_checked = True - if signal_freqs[0] < band_limits.upper and signal_freqs[1] >= band_limits.lower: - band_filter_ok = True - lower_limit_ok = True - upper_limit_ok = True - if self.activate_low_freq_filter_btn.isChecked(): - if not signal_freqs[1] >= filters_limit(self.lower_freq_spinbox, - self.lower_freq_filter_unit, - self.lower_freq_confidence, -1): - lower_limit_ok = False - if self.activate_up_freq_filter_btn.isChecked(): - if not signal_freqs[0] < filters_limit(self.upper_freq_spinbox, - self.upper_freq_filter_unit, - self.upper_freq_confidence): - upper_limit_ok = False - if any_checked: - return band_filter_ok and lower_limit_ok and upper_limit_ok - else: - return lower_limit_ok and upper_limit_ok - - def band_filters_ok(self, signal_name): - """Evalaute if the signal matches the band filters.""" - if not self.apply_remove_band_filter_btn.isChecked(): - return True - undef_band = is_undef_band(self.db.loc[signal_name]) - if undef_band: - if self.include_undef_bands.isChecked(): - return True - else: - return False - - signal_bands = ( - safe_cast(self.db.at[signal_name, Signal.INF_BAND], int), - safe_cast(self.db.at[signal_name, Signal.SUP_BAND], int) - ) - - lower_limit_ok = True - upper_limit_ok = True - if self.activate_low_band_filter_btn.isChecked(): - if not signal_bands[1] >= filters_limit(self.lower_band_spinbox, - self.lower_band_filter_unit, - self.lower_band_confidence, -1): - lower_limit_ok = False - if self.activate_up_band_filter_btn.isChecked(): - if not signal_bands[0] < filters_limit(self.upper_band_spinbox, - self.upper_band_filter_unit, - self.upper_band_confidence): - upper_limit_ok = False - return lower_limit_ok and upper_limit_ok - - def category_filters_ok(self, signal_name): - """Evalaute if the signal matches the category filters.""" - if not self.apply_remove_cat_filter_btn.isChecked(): - return True - cat_code = self.db.at[signal_name, Signal.CATEGORY_CODE] - cat_checked = 0 - positive_cases = 0 - for index, cat in enumerate(self.cat_filter_btns): - if cat.isChecked(): - cat_checked += 1 - if cat_code[index] == '1': - positive_cases += 1 - if self.cat_at_least_one.isChecked(): - return positive_cases > 0 - else: - return cat_checked == positive_cases and cat_checked > 0 - - def mode_filters_ok(self, signal_name): - """Evalaute if the signal matches the mode filters.""" - if not self.apply_remove_mode_filter_btn.isChecked(): - return True - signal_mode = self.db.at[signal_name, Signal.MODE] - if signal_mode == Constants.UNKNOWN: - if self.include_unknown_modes_btn.isChecked(): - return True - else: - return False - selected_items = [item for item in self.mode_tree_widget.selectedItems()] - selected_items_text = [i.text(0) for i in selected_items] - parents = [ - item for item in selected_items_text - if item in Constants.MODES.keys() - ] - ok = [] - for item in selected_items: - if item.text(0) in parents: - ok.append(item.text(0) in signal_mode) - elif not item.parent().isSelected(): - ok.append(item.text(0) == signal_mode) - return any(ok) - - def get_field_entries(self, signal_name, field, separator=Constants.FIELD_SEPARATOR): - """Take a signal name, a column label and optionally a separator string. - - Return a list obtained by splitting the signal field with separator.""" - return [ - x.strip() for x in self.db.at[signal_name, field].split(separator) - ] - - def modulation_filters_ok(self, signal_name): - """Evalaute if the signal matches the modulation filters.""" - if not self.apply_remove_modulation_filter_btn.isChecked(): - return True - signal_modulation = self.get_field_entries( - signal_name, Signal.MODULATION - ) - for item in self.modulation_list.selectedItems(): - if item.text() in signal_modulation: - return True - return False - - def location_filters_ok(self, signal_name): - """Evalaute if the signal matches the location filters.""" - if not self.apply_remove_location_filter_btn.isChecked(): - return True - signal_locations = self.get_field_entries( - signal_name, Signal.LOCATION - ) - for item in self.locations_list.selectedItems(): - if item.text() in signal_locations: - return True - return False - - def acf_filters_ok(self, signal_name): - """Evalaute if the signal matches the acf filters.""" - if not self.apply_remove_acf_filter_btn.isChecked(): - return True - signal_acf = self.db.at[signal_name, Signal.ACF] - if signal_acf == Constants.UNKNOWN: - if self.include_undef_acf.isChecked(): - return True - else: - return False - else: - signal_acf = safe_cast(signal_acf.rstrip("ms"), float) - tolerance = self.acf_spinbox.value() * self.acf_confidence.value() / 100 - upper_limit = self.acf_spinbox.value() + tolerance - lower_limit = self.acf_spinbox.value() - tolerance - if signal_acf <= upper_limit and signal_acf >= lower_limit: - return True - else: - return False - @pyqtSlot(QListWidgetItem, QListWidgetItem) def display_specs(self, item, previous_item): """Display the signal properties. @@ -1582,20 +609,6 @@ class Artemis(QMainWindow, Ui_MainWindow): for band_label in self.band_labels: self.activate_band_category(band_label, False) - @pyqtSlot() - def reset_all_filters(self): - """Reset all filter screens. - - Show all available signals. - """ - self.reset_frequency_filters_btn.clicked.emit() - self.reset_band_filters_btn.clicked.emit() - self.reset_cat_filters_btn.clicked.emit() - self.reset_mode_filters_btn.clicked.emit() - self.reset_modulation_filters_btn.clicked.emit() - self.reset_location_filters_btn.clicked.emit() - self.reset_acf_filters_btn.clicked.emit() - @pyqtSlot() def go_to_web_page_signal(self): """Go the web page of the signal's wiki. diff --git a/src/audio_player.py b/src/audio_player.py index e1b8a90..1ee1452 100644 --- a/src/audio_player.py +++ b/src/audio_player.py @@ -10,7 +10,7 @@ class AudioPlayer(QObject): """Subclass QObject. Audio player widget for the audio samples. The only public methods are the __init__ - method, set_audio_player, which loads the current file and refresh_btns_colors. + method, set_audio_player, which loads the current file and refresh. Everything else is managed internally.""" _TIME_STEP = 500 # Milliseconds. @@ -47,7 +47,7 @@ class AudioPlayer(QObject): self._pause.setIconSize(self._pause.size()) self._stop.setIconSize(self._stop.size()) self._loop.setIconSize(self._loop.size()) - self.refresh_btns_colors(active_color, inactive_color) + self.refresh(active_color, inactive_color) @pyqtSlot() def _set_loop_icon(self): @@ -67,7 +67,7 @@ class AudioPlayer(QObject): ) self._loop.setIcon(loop_icon) - def refresh_btns_colors(self, active_color, inactive_color): + def refresh(self, active_color, inactive_color): """Repaint the buttons of the widgetd after the theme has changed.""" self._active_color = active_color self._inactive_color = inactive_color diff --git a/src/constants.py b/src/constants.py index dbe2c34..e875826 100644 --- a/src/constants.py +++ b/src/constants.py @@ -41,7 +41,7 @@ class Messages: NO_CONNECTION = "No connection" NO_CONNECTION_MSG = "Unable to establish an internet connection." BAD_DOWNLOAD = "Something went wrong" - BAD_DOWNLOAD_MSG = "Something went wrong with the downaload.\nCheck your internet connection and try again." + BAD_DOWNLOAD_MSG = "Something went wrong with the download.\nCheck your internet connection and try again." SLOW_CONN = "Slow internet connection" SLOW_CONN_MSG = "Your internet connection is unstable or too slow." diff --git a/src/filters.py b/src/filters.py new file mode 100644 index 0000000..fff8f0f --- /dev/null +++ b/src/filters.py @@ -0,0 +1,850 @@ +"""This module contains all the filter-related classes and functions. + +The only class exposed is Filters which provides the following methods: +- ok(signal_name): to check if all the filters are passed; +- reset(): to reset all the applied filters; +- refresh(): used when the theme is changed.""" + +from collections import namedtuple +from functools import partial +import webbrowser + +from PyQt5.QtWidgets import QListWidgetItem, QTreeWidgetItem +from PyQt5.QtCore import pyqtSlot, QObject + +from constants import (Constants, + Ftype, + Signal,) +from utilities import (uncheck_and_emit, + connect_events_to_func, + filters_limit, + is_undef_freq, + is_undef_band, + safe_cast, + show_matching_strings, + get_field_entries,) + + +class _BaseFilter(QObject): + """Base class for all filters.""" + + def __init__(self, owner): + """Positional argument: + owner - the object containing the filter screen.""" + super().__init__() + self._owner = owner + + def refresh(self): + """Refresh the screen.""" + pass + + +class _FreqBandMixIn: + """Mixin class for the frequency and band filters. + + Provides some functions used in both classes.""" + + @pyqtSlot() + def _set_min_value_upper_limit(self, lower_combo_box, + lower_spin_box, + upper_combo_box, + upper_spin_box): + """Forbid to a lower limit to be greater than the corresponding upper one. + + Used for frequency and bandwidth screens.""" + if lower_spin_box.isEnabled(): + unit_conversion = { + 'Hz': ['kHz', 'MHz', 'GHz'], + 'kHz': ['MHz', 'GHz'], + 'MHz': ['GHz'] + } + lower_units = lower_combo_box.currentText() + upper_units = upper_combo_box.currentText() + lower_value = lower_spin_box.value() + inf_limit = (lower_value * Constants.CONVERSION_FACTORS[lower_units]) \ + // Constants.CONVERSION_FACTORS[upper_units] + counter = 0 + while inf_limit > upper_spin_box.maximum(): + counter += 1 + inf_limit //= 1000 + if upper_spin_box.minimum() != inf_limit: + upper_spin_box.setMinimum(inf_limit) + if counter > 0: + new_unit = unit_conversion[upper_units][counter - 1] + upper_combo_box.disconnect() + upper_combo_box.setCurrentText(new_unit) + upper_combo_box.currentTextChanged.connect( + partial( + self._set_min_value_upper_limit, + lower_combo_box, + lower_spin_box, + upper_combo_box, + upper_spin_box + ) + ) + + @pyqtSlot() + def _reset_fb_filters(self, ftype): + """Reset the Frequency or Bandwidth depending on 'ftype'. + + ftype can be either Ftype.FREQ or Ftype.BAND. + """ + if ftype != Ftype.FREQ and ftype != Ftype.BAND: + raise ValueError("Wrong ftype in function '_reset_fb_filters'") + + apply_remove_btn = getattr(self._owner, 'apply_remove_' + ftype + '_filter_btn') + include_undef_btn = getattr(self._owner, 'include_undef_' + ftype + 's') + activate_low = getattr(self._owner, 'activate_low_' + ftype + '_filter_btn') + activate_up = getattr(self._owner, 'activate_up_' + ftype + '_filter_btn') + lower_unit = getattr(self._owner, 'lower_' + ftype + '_filter_unit') + upper_unit = getattr(self._owner, 'upper_' + ftype + '_filter_unit') + lower_spinbox = getattr(self._owner, 'lower_' + ftype + '_spinbox') + upper_spinbox = getattr(self._owner, 'upper_' + ftype + '_spinbox') + lower_confidence = getattr(self._owner, 'lower_' + ftype + '_confidence') + upper_confidence = getattr(self._owner, 'lower_' + ftype + '_confidence') + + default_val = 1 if ftype == Ftype.FREQ else 5000 + if ftype == Ftype.FREQ: + for f in self._frequency_filters_btns: + if f.isChecked(): + f.setChecked(False) + uncheck_and_emit(apply_remove_btn) + if include_undef_btn.isChecked(): + include_undef_btn.setChecked(False) + uncheck_and_emit(activate_low) + uncheck_and_emit(activate_up) + lower_unit.setCurrentText("MHz") + upper_unit.setCurrentText("MHz") + lower_spinbox.setValue(default_val) + upper_spinbox.setMinimum(1) + upper_spinbox.setValue(default_val) + lower_confidence.setValue(0) + upper_confidence.setValue(0) + + @pyqtSlot() + def _set_band_filter_label(self, + activate_low_btn, + lower_spinbox, + lower_unit, + lower_confidence, + activate_up_btn, + upper_spinbox, + upper_unit, + upper_confidence, + range_lbl): + """Display the actual range applied for the signal's property search. + + Used for frequency and bandwidth screens.""" + activate_low = False + activate_high = False + color = self._owner.inactive_color + title = '' + to_display = '' + if activate_low_btn.isChecked(): + activate_low = True + color = self._owner.active_color + min_value = lower_spinbox.value() + if lower_confidence.value() != 0: + min_value -= lower_spinbox.value() * lower_confidence.value() / 100 + to_display += str(round(min_value, Constants.MAX_DIGITS)) \ + + ' ' + lower_unit.currentText() + else: + to_display += 'DC' + to_display += Constants.RANGE_SEPARATOR + if activate_up_btn.isChecked(): + max_value = upper_spinbox.value() + activate_high = True + color = self._owner.active_color + if upper_confidence.value() != 0: + max_value += upper_spinbox.value() * upper_confidence.value() / 100 + to_display += str(round(max_value, Constants.MAX_DIGITS)) + ' ' \ + + upper_unit.currentText() + else: + to_display += 'INF' + if activate_low and activate_high: + title = 'Band-pass\n\n' + elif activate_low and not activate_high: + title = 'Low-pass\n\n' + elif not activate_low and activate_high: + title = 'High-pass\n\n' + else: + title = "Selected range:\n\n" + to_display = "Inactive" + to_display = title + to_display + range_lbl.setText(to_display) + range_lbl.setStyleSheet(f'color: {color};') + + +class FreqFilter(_BaseFilter, _FreqBandMixIn): + """Frequency filter class.""" + + def __init__(self, owner): + super().__init__(owner) + self.apply_remove_btn = self._owner.apply_remove_freq_filter_btn + self.reset_btn = self._owner.reset_frequency_filters_btn + self._frequency_filters_btns = ( + self._owner.elf_filter_btn, + self._owner.slf_filter_btn, + self._owner.ulf_filter_btn, + self._owner.vlf_filter_btn, + self._owner.lf_filter_btn, + self._owner.mf_filter_btn, + self._owner.hf_filter_btn, + self._owner.vhf_filter_btn, + self._owner.uhf_filter_btn, + self._owner.shf_filter_btn, + self._owner.ehf_filter_btn, + ) + + self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE) + self.apply_remove_btn.set_slave_filters( + simple_ones=[ + *self._frequency_filters_btns, + self._owner.include_undef_freqs, + self._owner.activate_low_freq_filter_btn, + self._owner.activate_up_freq_filter_btn + ], + radio_1=self._owner.activate_low_freq_filter_btn, + ruled_by_radio_1=[ + self._owner.lower_freq_spinbox, + self._owner.lower_freq_filter_unit, + self._owner.lower_freq_confidence + ], + radio_2=self._owner.activate_up_freq_filter_btn, + ruled_by_radio_2=[ + self._owner.upper_freq_spinbox, + self._owner.upper_freq_filter_unit, + self._owner.upper_freq_confidence + ] + ) + + connect_events_to_func( + events_to_connect=[self._owner.lower_freq_spinbox.valueChanged, + self._owner.upper_freq_spinbox.valueChanged, + self._owner.lower_freq_filter_unit.currentTextChanged, + self._owner.upper_freq_filter_unit.currentTextChanged, + self._owner.activate_low_freq_filter_btn.toggled], + fun_to_connect=self._set_min_value_upper_limit, + fun_args=[self._owner.lower_freq_filter_unit, + self._owner.lower_freq_spinbox, + self._owner.upper_freq_filter_unit, + self._owner.upper_freq_spinbox] + ) + + connect_events_to_func( + events_to_connect=[self._owner.lower_freq_spinbox.valueChanged, + self._owner.upper_freq_spinbox.valueChanged, + self._owner.lower_freq_filter_unit.currentTextChanged, + self._owner.upper_freq_filter_unit.currentTextChanged, + self._owner.activate_low_freq_filter_btn.clicked, + self._owner.activate_up_freq_filter_btn.clicked, + self._owner.lower_freq_confidence.valueChanged, + self._owner.upper_freq_confidence.valueChanged], + fun_to_connect=self._set_band_filter_label, + fun_args=[self._owner.activate_low_freq_filter_btn, + self._owner.lower_freq_spinbox, + self._owner.lower_freq_filter_unit, + self._owner.lower_freq_confidence, + self._owner.activate_up_freq_filter_btn, + self._owner.upper_freq_spinbox, + self._owner.upper_freq_filter_unit, + self._owner.upper_freq_confidence, + self._owner.freq_range_lbl] + ) + + self._owner.activate_low_freq_filter_btn.toggled.connect( + partial(self._owner.activate_if_toggled, + self._owner.activate_low_freq_filter_btn, + self._owner.lower_freq_spinbox, + self._owner.lower_freq_filter_unit, + self._owner.lower_freq_confidence) + ) + + self._owner.activate_up_freq_filter_btn.toggled.connect( + partial(self._owner.activate_if_toggled, + self._owner.activate_up_freq_filter_btn, + self._owner.upper_freq_spinbox, + self._owner.upper_freq_filter_unit, + self._owner.upper_freq_confidence) + ) + + @pyqtSlot() + def reset(self): + """Reset the filter screen.""" + self._reset_fb_filters(Ftype.FREQ) + + def _ok(self, signal_name): + """Evalaute if the signal matches the frequency filters.""" + if not self.apply_remove_btn.isChecked(): + return True + undef_freq = is_undef_freq(self._owner.db.loc[signal_name]) + if undef_freq: + if self._owner.include_undef_freqs.isChecked(): + return True + else: + return False + + signal_freqs = ( + safe_cast(self._owner.db.at[signal_name, Signal.INF_FREQ], int), + safe_cast(self._owner.db.at[signal_name, Signal.SUP_FREQ], int) + ) + + band_filter_ok = False + any_checked = False + for btn, band_limits in zip(self._frequency_filters_btns, Constants.BANDS): + if btn.isChecked(): + any_checked = True + if signal_freqs[0] < band_limits.upper and signal_freqs[1] >= band_limits.lower: + band_filter_ok = True + lower_limit_ok = True + upper_limit_ok = True + if self._owner.activate_low_freq_filter_btn.isChecked(): + if not signal_freqs[1] >= filters_limit(self._owner.lower_freq_spinbox, + self._owner.lower_freq_filter_unit, + self._owner.lower_freq_confidence, -1): + lower_limit_ok = False + if self._owner.activate_up_freq_filter_btn.isChecked(): + if not signal_freqs[0] < filters_limit(self._owner.upper_freq_spinbox, + self._owner.upper_freq_filter_unit, + self._owner.upper_freq_confidence): + upper_limit_ok = False + if any_checked: + return band_filter_ok and lower_limit_ok and upper_limit_ok + else: + return lower_limit_ok and upper_limit_ok + + def refresh(self): + """Extend _BaseFilter.refresh.""" + super().refresh() + self._set_band_filter_label( + self._owner.activate_low_band_filter_btn, + self._owner.lower_band_spinbox, + self._owner.lower_band_filter_unit, + self._owner.lower_band_confidence, + self._owner.activate_up_band_filter_btn, + self._owner.upper_band_spinbox, + self._owner.upper_band_filter_unit, + self._owner.upper_band_confidence, + self._owner.band_range_lbl + ) + + +class BandFilter(_BaseFilter, _FreqBandMixIn): + """Band filter class.""" + def __init__(self, owner): + super().__init__(owner) + self.apply_remove_btn = self._owner.apply_remove_band_filter_btn + self.reset_btn = self._owner.reset_band_filters_btn + connect_events_to_func( + events_to_connect=[self._owner.lower_band_spinbox.valueChanged, + self._owner.upper_band_spinbox.valueChanged, + self._owner.lower_band_filter_unit.currentTextChanged, + self._owner.upper_band_filter_unit.currentTextChanged, + self._owner.activate_low_band_filter_btn.toggled], + fun_to_connect=self._set_min_value_upper_limit, + fun_args=[self._owner.lower_band_filter_unit, + self._owner.lower_band_spinbox, + self._owner.upper_band_filter_unit, + self._owner.upper_band_spinbox] + ) + + connect_events_to_func( + events_to_connect=[self._owner.lower_band_spinbox.valueChanged, + self._owner.upper_band_spinbox.valueChanged, + self._owner.lower_band_filter_unit.currentTextChanged, + self._owner.upper_band_filter_unit.currentTextChanged, + self._owner.activate_low_band_filter_btn.clicked, + self._owner.activate_up_band_filter_btn.clicked, + self._owner.lower_band_confidence.valueChanged, + self._owner.upper_band_confidence.valueChanged], + fun_to_connect=self._set_band_filter_label, + fun_args=[self._owner.activate_low_band_filter_btn, + self._owner.lower_band_spinbox, + self._owner.lower_band_filter_unit, + self._owner.lower_band_confidence, + self._owner.activate_up_band_filter_btn, + self._owner.upper_band_spinbox, + self._owner.upper_band_filter_unit, + self._owner.upper_band_confidence, + self._owner.band_range_lbl] + ) + + self._owner.activate_low_band_filter_btn.toggled.connect( + partial(self._owner.activate_if_toggled, + self._owner.activate_low_band_filter_btn, + self._owner.lower_band_spinbox, + self._owner.lower_band_filter_unit, + self._owner.lower_band_confidence) + ) + + self._owner.activate_up_band_filter_btn.toggled.connect( + partial(self._owner.activate_if_toggled, + self._owner.activate_up_band_filter_btn, + self._owner.upper_band_spinbox, + self._owner.upper_band_filter_unit, + self._owner.upper_band_confidence) + ) + + self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE) + self.apply_remove_btn.set_slave_filters( + simple_ones=[ + self._owner.include_undef_bands, + self._owner.activate_low_band_filter_btn, + self._owner.activate_up_band_filter_btn + ], + radio_1=self._owner.activate_low_band_filter_btn, + ruled_by_radio_1=[ + self._owner.lower_band_spinbox, + self._owner.lower_band_filter_unit, + self._owner.lower_band_confidence + ], + radio_2=self._owner.activate_up_band_filter_btn, + ruled_by_radio_2=[ + self._owner.upper_band_spinbox, + self._owner.upper_band_filter_unit, + self._owner.upper_band_confidence + ] + ) + + @pyqtSlot() + def reset(self): + """Reset the filter screen.""" + self._reset_fb_filters(Ftype.BAND) + + def _ok(self, signal_name): + """Evalaute if the signal matches the band filters.""" + if not self.apply_remove_btn.isChecked(): + return True + undef_band = is_undef_band(self._owner.db.loc[signal_name]) + if undef_band: + if self._owner.include_undef_bands.isChecked(): + return True + else: + return False + + signal_bands = ( + safe_cast(self._owner.db.at[signal_name, Signal.INF_BAND], int), + safe_cast(self._owner.db.at[signal_name, Signal.SUP_BAND], int) + ) + + lower_limit_ok = True + upper_limit_ok = True + if self._owner.activate_low_band_filter_btn.isChecked(): + if not signal_bands[1] >= filters_limit(self._owner.lower_band_spinbox, + self._owner.lower_band_filter_unit, + self._owner.lower_band_confidence, -1): + lower_limit_ok = False + if self._owner.activate_up_band_filter_btn.isChecked(): + if not signal_bands[0] < filters_limit(self._owner.upper_band_spinbox, + self._owner.upper_band_filter_unit, + self._owner.upper_band_confidence): + upper_limit_ok = False + return lower_limit_ok and upper_limit_ok + + def refresh(self): + """Extend _BaseFilter.refresh.""" + super().refresh() + self._set_band_filter_label( + self._owner.activate_low_freq_filter_btn, + self._owner.lower_freq_spinbox, + self._owner.lower_freq_filter_unit, + self._owner.lower_freq_confidence, + self._owner.activate_up_freq_filter_btn, + self._owner.upper_freq_spinbox, + self._owner.upper_freq_filter_unit, + self._owner.upper_freq_confidence, + self._owner.freq_range_lbl + ) + + +class CatFilter(_BaseFilter): + """Category filter class.""" + + def __init__(self, owner): + super().__init__(owner) + self.apply_remove_btn = self._owner.apply_remove_cat_filter_btn + self.reset_btn = self._owner.reset_cat_filters_btn + # Order matters! + self._cat_filter_btns = [ + self._owner.military_btn, + self._owner.radar_btn, + self._owner.active_btn, + self._owner.inactive_btn, + self._owner.ham_btn, + self._owner.commercial_btn, + self._owner.aviation_btn, + self._owner.marine_btn, + self._owner.analogue_btn, + self._owner.digital_btn, + self._owner.trunked_btn, + self._owner.utility_btn, + self._owner.sat_btn, + self._owner.navigation_btn, + self._owner.interfering_btn, + self._owner.number_stations_btn, + self._owner.time_signal_btn + ] + + self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE) + self.apply_remove_btn.set_slave_filters( + simple_ones=[ + *self._cat_filter_btns, + self._owner.cat_at_least_one, + self._owner.cat_all + ] + ) + + @pyqtSlot() + def reset(self): + """Reset the category filter screen.""" + uncheck_and_emit(self.apply_remove_btn) + for f in self._cat_filter_btns: + if f.isChecked(): + f.setChecked(False) + self._owner.cat_at_least_one.setChecked(True) + + def _ok(self, signal_name): + """Evalaute if the signal matches the category filters.""" + if not self.apply_remove_btn.isChecked(): + return True + cat_code = self._owner.db.at[signal_name, Signal.CATEGORY_CODE] + cat_checked = 0 + positive_cases = 0 + for index, cat in enumerate(self._cat_filter_btns): + if cat.isChecked(): + cat_checked += 1 + if cat_code[index] == '1': + positive_cases += 1 + if self._owner.cat_at_least_one.isChecked(): + return positive_cases > 0 + else: + return cat_checked == positive_cases and cat_checked > 0 + + +class ModeFilter(_BaseFilter): + """Mode filter class.""" + + def __init__(self, owner): + super().__init__(owner) + self.apply_remove_btn = self._owner.apply_remove_mode_filter_btn + self.reset_btn = self._owner.reset_mode_filters_btn + self._set_mode_tree_widget() + self._owner.mode_tree_widget.itemSelectionChanged.connect( + self._manage_mode_selections + ) + self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE) + self.apply_remove_btn.set_slave_filters( + simple_ones=[ + self._owner.mode_tree_widget, + self._owner.include_unknown_modes_btn + ] + ) + + def _manage_mode_selections(self): + """Rules the selection of childs items of the 'Mode' QTreeWidget. + + If a parent is selected all its children will be selected as well. + """ + selected_items = self._owner.mode_tree_widget.selectedItems() + parents = Constants.MODES.keys() + for parent in parents: + for item in selected_items: + if parent == item.text(0): + for i in range(len(Constants.MODES[parent])): + item.child(i).setSelected(True) + + def _set_mode_tree_widget(self): + """Construct the QTreeWidget for the 'Mode' screen.""" + for parent, children in Constants.MODES.items(): + iparent = QTreeWidgetItem([parent]) + self._owner.mode_tree_widget.addTopLevelItem(iparent) + for child in children: + ichild = QTreeWidgetItem([child]) + iparent.addChild(ichild) + self._owner.mode_tree_widget.expandAll() + + @pyqtSlot() + def reset(self): + """Reset the mode filter screen.""" + uncheck_and_emit(self.apply_remove_btn) + parents = Constants.MODES.keys() + selected_children = [] + for item in self._owner.mode_tree_widget.selectedItems(): + if item.text(0) in parents: + item.setSelected(False) + else: + selected_children.append(item) + for children in selected_children: + children.setSelected(False) + if self._owner.include_unknown_modes_btn.isChecked(): + self._owner.include_unknown_modes_btn.setChecked(False) + + def _ok(self, signal_name): + """Evalaute if the signal matches the mode filters.""" + if not self.apply_remove_btn.isChecked(): + return True + signal_mode = self._owner.db.at[signal_name, Signal.MODE] + if signal_mode == Constants.UNKNOWN: + if self._owner.include_unknown_modes_btn.isChecked(): + return True + else: + return False + selected_items = [item for item in self._owner.mode_tree_widget.selectedItems()] + selected_items_text = [i.text(0) for i in selected_items] + parents = [ + item for item in selected_items_text + if item in Constants.MODES.keys() + ] + ok = [] + for item in selected_items: + if item.text(0) in parents: + ok.append(item.text(0) in signal_mode) + elif not item.parent().isSelected(): + ok.append(item.text(0) == signal_mode) + return any(ok) + + +class ModulationFilter(_BaseFilter): + """Modulation filter class.""" + + def __init__(self, owner): + super().__init__(owner) + self.apply_remove_btn = self._owner.apply_remove_modulation_filter_btn + self.reset_btn = self._owner.reset_modulation_filters_btn + self._owner.search_bar_modulation.textEdited.connect(self._show_matching_modulations) + self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE) + self.apply_remove_btn.set_slave_filters( + simple_ones=[ + self._owner.search_bar_modulation, + self._owner.modulation_list + ] + ) + self._owner.modulation_list.itemClicked.connect(self._remove_if_unselected_modulation) + + @pyqtSlot(QListWidgetItem) + def _remove_if_unselected_modulation(self, item): + """If an item is unselected from the modulations list, hide the item.""" + if not item.isSelected(): + self._show_matching_modulations(self.search_bar_modulation.text()) + + @pyqtSlot(str) + def _show_matching_modulations(self, text): + """Show the modulations which matches 'text'. + + The match criterion is defined in 'show_matching_strings'.""" + show_matching_strings(self._owner.modulation_list, text) + + @pyqtSlot() + def reset(self): + """Reset the modulation filter screen.""" + uncheck_and_emit(self.apply_remove_btn) + self._owner.search_bar_modulation.setText('') + show_matching_strings( + self._owner.modulation_list, + self._owner.search_bar_modulation.text() + ) + for i in range(self._owner.modulation_list.count()): + if self._owner.modulation_list.item(i).isSelected(): + self._owner.modulation_list.item(i).setSelected(False) + + def _ok(self, signal_name): + """Evalaute if the signal matches the modulation filters.""" + if not self.apply_remove_btn.isChecked(): + return True + signal_modulation = get_field_entries( + self._owner.db.at[signal_name, Signal.MODULATION] + ) + for item in self._owner.modulation_list.selectedItems(): + if item.text() in signal_modulation: + return True + return False + + +class LocFilter(_BaseFilter): + """Location filter class.""" + + def __init__(self, owner): + super().__init__(owner) + self.apply_remove_btn = self._owner.apply_remove_location_filter_btn + self.reset_btn = self._owner.reset_location_filters_btn + self._owner.search_bar_location.textEdited.connect( + self._show_matching_locations + ) + self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE) + self.apply_remove_btn.set_slave_filters( + simple_ones=[ + self._owner.search_bar_location, + self._owner.locations_list + ] + ) + self._owner.locations_list.itemClicked.connect(self._remove_if_unselected_location) + + @pyqtSlot(str) + def _show_matching_locations(self, text): + """Show the locations which matches 'text'. + + The match criterion is defined in 'show_matching_strings'.""" + show_matching_strings(self._owner.locations_list, text) + + @pyqtSlot(QListWidgetItem) + def _remove_if_unselected_location(self, item): + """If an item is unselected from the locations list, hide the item.""" + if not item.isSelected(): + self._show_matching_locations(self._owner.search_bar_location.text()) + + @pyqtSlot() + def reset(self): + """Reset the location filter screen.""" + uncheck_and_emit(self.apply_remove_btn) + self._owner.search_bar_location.setText('') + show_matching_strings( + self._owner.locations_list, + self._owner.search_bar_location.text() + ) + for i in range(self._owner.locations_list.count()): + if self._owner.locations_list.item(i).isSelected(): + self._owner.locations_list.item(i).setSelected(False) + + def _ok(self, signal_name): + """Evalaute if the signal matches the location filters.""" + if not self.apply_remove_btn.isChecked(): + return True + signal_locations = get_field_entries( + self._owner.db.at[signal_name, Signal.LOCATION] + ) + for item in self._owner.locations_list.selectedItems(): + if item.text() in signal_locations: + return True + return False + + +class ACFFilter(_BaseFilter): + """Autocorrelation function filter class.""" + + def __init__(self, owner): + super().__init__(owner) + self.apply_remove_btn = self._owner.apply_remove_acf_filter_btn + self.reset_btn = self._owner.reset_acf_filters_btn + self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE) + self.apply_remove_btn.set_slave_filters( + simple_ones=[ + self._owner.include_undef_acf, + self._owner.acf_spinbox, + self._owner.acf_confidence + ] + ) + self._owner.acf_info_btn.clicked.connect(lambda: webbrowser.open(Constants.ACF_DOCS)) + + connect_events_to_func( + events_to_connect=[self._owner.acf_spinbox.valueChanged, + self._owner.acf_confidence.valueChanged], + fun_to_connect=self._set_acf_interval_label, + fun_args=None + ) + + @pyqtSlot() + def _set_acf_interval_label(self): + """Display the actual acf interval for the search.""" + tolerance = self._owner.acf_spinbox.value() * self._owner.acf_confidence.value() / 100 + if tolerance > 0: + val = round(self._owner.acf_spinbox.value() - tolerance, Constants.MAX_DIGITS) + to_display = f"Selected range:\n\n{val}" + Constants.RANGE_SEPARATOR \ + + f"{round(self._owner.acf_spinbox.value() + tolerance, Constants.MAX_DIGITS)} ms" + else: + to_display = f"Selected value:\n\n{self._owner.acf_spinbox.value()} ms" + self._owner.acf_range_lbl.setText(to_display) + self._owner.acf_range_lbl.setStyleSheet(f"color: {self._owner.active_color}") + + @pyqtSlot() + def reset(self): + """Reset the acf filter screen.""" + uncheck_and_emit(self.apply_remove_btn) + if self._owner.include_undef_acf.isChecked(): + self._owner.include_undef_acf.setChecked(False) + self._owner.acf_spinbox.setValue(50) + self._owner.acf_confidence.setValue(0) + + def _ok(self, signal_name): + """Evalaute if the signal matches the acf filters.""" + if not self.apply_remove_btn.isChecked(): + return True + signal_acf = self._owner.db.at[signal_name, Signal.ACF] + if signal_acf == Constants.UNKNOWN: + if self._owner.include_undef_acf.isChecked(): + return True + else: + return False + else: + signal_acf = safe_cast(signal_acf.rstrip("ms"), float) + tolerance = self._owner.acf_spinbox.value() * self._owner.acf_confidence.value() / 100 + upper_limit = self._owner.acf_spinbox.value() + tolerance + lower_limit = self._owner.acf_spinbox.value() - tolerance + if signal_acf <= upper_limit and signal_acf >= lower_limit: + return True + else: + return False + + def refresh(self): + """Extend _BaseFilter.refresh.""" + super().refresh() + self._set_acf_interval_label() + + +class Filters(QObject): + """Global filter class. + + Provides the information about all the filters. Its only public attribute + is filters, which is a namedtuple containing instances of all the filters. + The only exposed methods are reset(), ok(signal_name) and refresh(). + The class also connects the apply and reset buttons to the relevant functions.""" + + _FiltersTuple = namedtuple( + "_FiltersTuple", + [ + "freq_filter", + "band_filter", + "cat_filter", + "mode_filter", + "modulation_filter", + "location_filter", + "acf_filter", + ] + ) + + def __init__(self, owner): + super().__init__() + self.filters = self._FiltersTuple( + FreqFilter(owner), + BandFilter(owner), + CatFilter(owner), + ModeFilter(owner), + ModulationFilter(owner), + LocFilter(owner), + ACFFilter(owner), + ) + self._owner = owner + self._owner.reset_filters_btn.clicked.connect(self.reset) + + # Connect Apply and Reset buttons clicks to functions. + for f in self.filters: + f.apply_remove_btn.clicked.connect(self._display_signals) + f.reset_btn.clicked.connect(f.reset) + + @pyqtSlot() + def _display_signals(self): + self._owner.display_signals() + + @pyqtSlot() + def reset(self): + """Reset all the filters.""" + for f in self.filters: + f.reset() + + def ok(self, signal_name): + """Check whether all the filters are passed.""" + return all(f._ok(signal_name) for f in self.filters) + + def refresh(self): + """Refresh the relevant widgets when changing theme.""" + for f in self.filters: + f.refresh() diff --git a/src/spaceweathermanager.py b/src/spaceweathermanager.py new file mode 100644 index 0000000..22c3756 --- /dev/null +++ b/src/spaceweathermanager.py @@ -0,0 +1,297 @@ +import webbrowser +from PyQt5.QtCore import QObject, pyqtSlot +from constants import Constants, Messages +from switchable_label import SwitchableLabelsIterable +from weatherdata import SpaceWeatherData +from utilities import safe_cast, pop_up + + +class SpaceWeatherManager(QObject): + """Class to manage the spaceweather screen.""" + + def __init__(self, owner): + super().__init__() + self._owner = owner + self._owner.info_now_btn.clicked.connect( + lambda: webbrowser.open(Constants.SPACE_WEATHER_INFO) + ) + self._owner.update_now_bar.clicked.connect(self._start_update_space_weather) + self._owner.update_now_bar.set_idle() + self._owner.space_weather_data = SpaceWeatherData() + self._owner.space_weather_data.update_complete.connect(self._update_space_weather) + + self.space_weather_labels = ( + self._owner.space_weather_lbl_0, + self._owner.space_weather_lbl_1, + self._owner.space_weather_lbl_2, + self._owner.space_weather_lbl_3, + self._owner.space_weather_lbl_4, + self._owner.space_weather_lbl_5, + self._owner.space_weather_lbl_6, + self._owner.space_weather_lbl_7, + self._owner.space_weather_lbl_8 + ) + + for lab in self.space_weather_labels: + lab.set_default_stylesheet() + + self._owner.space_weather_label_container.labels = self.space_weather_labels + self._owner.space_weather_label_name_container.labels = [ + self._owner.eme_lbl, + self._owner.ms_lbl, + self._owner.muf_lbl, + self._owner.hi_lbl, + self._owner.eu50_lbl, + self._owner.eu70_lbl, + self._owner.eu144_lbl, + self._owner.na_lbl, + self._owner.aurora_lbl + ] + + self._switchable_r_labels = SwitchableLabelsIterable( + self._owner.r0_now_lbl, + self._owner.r1_now_lbl, + self._owner.r2_now_lbl, + self._owner.r3_now_lbl, + self._owner.r4_now_lbl, + self._owner.r5_now_lbl + ) + + self._switchable_s_labels = SwitchableLabelsIterable( + self._owner.s0_now_lbl, + self._owner.s1_now_lbl, + self._owner.s2_now_lbl, + self._owner.s3_now_lbl, + self._owner.s4_now_lbl, + self._owner.s5_now_lbl + ) + + self._switchable_g_now_labels = SwitchableLabelsIterable( + self._owner.g0_now_lbl, + self._owner.g1_now_lbl, + self._owner.g2_now_lbl, + self._owner.g3_now_lbl, + self._owner.g4_now_lbl, + self._owner.g5_now_lbl + ) + + self._switchable_g_today_labels = SwitchableLabelsIterable( + self._owner.g0_today_lbl, + self._owner.g1_today_lbl, + self._owner.g2_today_lbl, + self._owner.g3_today_lbl, + self._owner.g4_today_lbl, + self._owner.g5_today_lbl + ) + + self._k_storm_labels = SwitchableLabelsIterable( + self._owner.k_ex_sev_storm_lbl, + self._owner.k_very_sev_storm_lbl, + self._owner.k_sev_storm_lbl, + self._owner.k_maj_storm_lbl, + self._owner.k_min_storm_lbl, + self._owner.k_active_lbl, + self._owner.k_unsettled_lbl, + self._owner.k_quiet_lbl, + self._owner.k_very_quiet_lbl, + self._owner.k_inactive_lbl + ) + + self._a_storm_labels = SwitchableLabelsIterable( + self._owner.a_sev_storm_lbl, + self._owner.a_maj_storm_lbl, + self._owner.a_min_storm_lbl, + self._owner.a_active_lbl, + self._owner.a_unsettled_lbl, + self._owner.a_quiet_lbl + ) + + # Used by ThemeManager. + self.refreshable_labels = SwitchableLabelsIterable( + *self._switchable_r_labels, + *self._switchable_s_labels, + *self._switchable_g_now_labels, + *self._switchable_g_today_labels, + *self._k_storm_labels, + *self._a_storm_labels, + self._owner.expected_noise_lbl + ) + + @pyqtSlot() + def _start_update_space_weather(self): + """Start the update of the space weather screen. + + Start the corresponding thread. + """ + if not self._owner.space_weather_data.is_updating: + self._owner.update_now_bar.set_updating() + self._owner.space_weather_data.update() + + @pyqtSlot(bool) + def _update_space_weather(self, status_ok): + """Update the space weather screen after a successful download. + + If the download was not successful throw a warning. In any case remove + the downloaded data. + """ + self._owner.update_now_bar.set_idle() + if status_ok: + xray_long = safe_cast(self._owner.space_weather_data.xray[-1][7], float) + + def format_text(letter, power): + return letter + f"{xray_long * 10**power:.1f}" + + if xray_long < 1e-8 and xray_long != -1.00e+05: + self._owner.peak_flux_lbl.setText(format_text("= 1e-8 and xray_long < 1e-7: + self._owner.peak_flux_lbl.setText(format_text("A", 8)) + elif xray_long >= 1e-7 and xray_long < 1e-6: + self._owner.peak_flux_lbl.setText(format_text("B", 7)) + elif xray_long >= 1e-6 and xray_long < 1e-5: + self._owner.peak_flux_lbl.setText(format_text("C", 6)) + elif xray_long >= 1e-5 and xray_long < 1e-4: + self._owner.peak_flux_lbl.setText(format_text("M", 5)) + elif xray_long >= 1e-4: + self._owner.peak_flux_lbl.setText(format_text("X", 4)) + elif xray_long == -1.00e+05: + self._owner.peak_flux_lbl.setText("No Data") + + if xray_long < 1e-5 and xray_long != -1.00e+05: + self._switchable_r_labels.switch_on(self._owner.r0_now_lbl) + elif xray_long >= 1e-5 and xray_long < 5e-5: + self._switchable_r_labels.switch_on(self._owner.r1_now_lbl) + elif xray_long >= 5e-5 and xray_long < 1e-4: + self._switchable_r_labels.switch_on(self._owner.r2_now_lbl) + elif xray_long >= 1e-4 and xray_long < 1e-3: + self._switchable_r_labels.switch_on(self._owner.r3_now_lbl) + elif xray_long >= 1e-3 and xray_long < 2e-3: + self._switchable_r_labels.switch_on(self._owner.r4_now_lbl) + elif xray_long >= 2e-3: + self._switchable_r_labels.switch_on(self._owner.r5_now_lbl) + elif xray_long == -1.00e+05: + self._switchable_r_labels.switch_off_all() + + pro10 = safe_cast(self._owner.space_weather_data.prot_el[-1][8], float) + if pro10 < 10 and pro10 != -1.00e+05: + self._switchable_s_labels.switch_on(self._owner.s0_now_lbl) + elif pro10 >= 10 and pro10 < 100: + self._switchable_s_labels.switch_on(self._owner.s1_now_lbl) + elif pro10 >= 100 and pro10 < 1000: + self._switchable_s_labels.switch_on(self._owner.s2_now_lbl) + elif pro10 >= 1000 and pro10 < 10000: + self._switchable_s_labels.switch_on(self._owner.s3_now_lbl) + elif pro10 >= 10000 and pro10 < 100000: + self._switchable_s_labels.switch_on(self._owner.s4_now_lbl) + elif pro10 >= 100000: + self._switchable_s_labels.switch_on(self._owner.s5_now_lbl) + elif pro10 == -1.00e+05: + self._switchable_s_labels.switch_off_all() + + k_index = safe_cast( + self._owner.space_weather_data.ak_index[8][11].replace('.', ''), int + ) + self._owner.k_index_lbl.setText(str(k_index)) + a_index = safe_cast( + self._owner.space_weather_data.ak_index[7][7].replace('.', ''), int + ) + self._owner.a_index_lbl.setText(str(a_index)) + + if k_index == 0: + self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl) + self._k_storm_labels.switch_on(self.k_inactive_lbl) + self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ") + elif k_index == 1: + self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_very_quiet_lbl) + self._owner.expected_noise_lbl.setText(" S0 - S1 (<-120 dBm) ") + elif k_index == 2: + self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_quiet_lbl) + self._owner.expected_noise_lbl.setText(" S1 - S2 (-115 dBm) ") + elif k_index == 3: + self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_unsettled_lbl) + self._owner.expected_noise_lbl.setText(" S2 - S3 (-110 dBm) ") + elif k_index == 4: + self._switchable_g_now_labels.switch_on(self._owner.g0_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_active_lbl) + self._owner.expected_noise_lbl.setText(" S3 - S4 (-100 dBm) ") + elif k_index == 5: + self._switchable_g_now_labels.switch_on(self._owner.g1_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_min_storm_lbl) + self._owner.expected_noise_lbl.setText(" S4 - S6 (-90 dBm) ") + elif k_index == 6: + self._switchable_g_now_labels.switch_on(self._owner.g2_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_maj_storm_lbl) + self._owner.expected_noise_lbl.setText(" S6 - S9 (-80 dBm) ") + elif k_index == 7: + self._switchable_g_now_labels.switch_on(self._owner.g3_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_sev_storm_lbl) + self._owner.expected_noise_lbl.setText(" S9 - S20 (>-60 dBm) ") + elif k_index == 8: + self._switchable_g_now_labels.switch_on(self._owner.g4_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_very_sev_storm_lbl) + self._owner.expected_noise_lbl.setText(" S20 - S30 (>-60 dBm) ") + elif k_index == 9: + self._switchable_g_now_labels.switch_on(self._owner.g5_now_lbl) + self._k_storm_labels.switch_on(self._owner.k_ex_sev_storm_lbl) + self._owner.expected_noise_lbl.setText(" S30+ (>>-60 dBm) ") + self._owner.expected_noise_lbl.switch_on() + + if a_index >= 0 and a_index < 8: + self._a_storm_labels.switch_on(self._owner.a_quiet_lbl) + elif a_index >= 8 and a_index < 16: + self._a_storm_labels.switch_on(self._owner.a_unsettled_lbl) + elif a_index >= 16 and a_index < 30: + self._a_storm_labels.switch_on(self._owner.a_active_lbl) + elif a_index >= 30 and a_index < 50: + self._a_storm_labels.switch_on(self._owner.a_min_storm_lbl) + elif a_index >= 50 and a_index < 100: + self._a_storm_labels.switch_on(self._owner.a_maj_storm_lbl) + elif a_index >= 100 and a_index < 400: + self._a_storm_labels.switch_on(self._owner.a_sev_storm_lbl) + + index = self._owner.space_weather_data.geo_storm[6].index("was") + 1 + k_index_24_hmax = safe_cast( + self._owner.space_weather_data.geo_storm[6][index], int + ) + if k_index_24_hmax == 0: + self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl) + elif k_index_24_hmax == 1: + self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl) + elif k_index_24_hmax == 2: + self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl) + elif k_index_24_hmax == 3: + self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl) + elif k_index_24_hmax == 4: + self._switchable_g_today_labels.switch_on(self._owner.g0_today_lbl) + elif k_index_24_hmax == 5: + self._switchable_g_today_labels.switch_on(self._owner.g1_today_lbl) + elif k_index_24_hmax == 6: + self._switchable_g_today_labels.switch_on(self._owner.g2_today_lbl) + elif k_index_24_hmax == 7: + self._switchable_g_today_labels.switch_on(self._owner.g3_today_lbl) + elif k_index_24_hmax == 8: + self._switchable_g_today_labels.switch_on(self._owner.g4_today_lbl) + elif k_index_24_hmax == 9: + self._switchable_g_today_labels.switch_on(self._owner.g5_today_lbl) + + val = safe_cast( + self._owner.space_weather_data.ak_index[7][2].replace('.', ''), int + ) + self._owner.sfi_lbl.setText(f"{val}") + val = safe_cast( + [x[4] for x in self._owner.space_weather_data.sgas + if "SSN" in x][0], int + ) + self._owner.sn_lbl.setText(f"{val:d}") + + for label, pixmap in zip(self.space_weather_labels, + self._owner.space_weather_data.images): + label.pixmap = pixmap + label.make_transparent() + label.apply_pixmap() + elif not self._owner.closing: + pop_up(self._owner, title=Messages.BAD_DOWNLOAD, + text=Messages.BAD_DOWNLOAD_MSG).show() + self._owner.space_weather_data.remove_data() diff --git a/src/themesmanager.py b/src/themesmanager.py index 2b42454..b76cea9 100644 --- a/src/themesmanager.py +++ b/src/themesmanager.py @@ -1,12 +1,10 @@ from functools import partial -from itertools import chain import os import re from PyQt5.QtWidgets import QAction, QActionGroup from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QPixmap from constants import Constants -from switchable_label import SwitchableLabelsIterable from utilities import pop_up @@ -46,7 +44,7 @@ class _ColorsHandler: Can handle strings representing multiple colors.""" - MAX_COLORS = 2 + _MAX_COLORS = 2 def __init__(self, line): """Define the color from the string 'line'. @@ -76,7 +74,7 @@ class _ColorsHandler: return bool(re.match(pattern, col)) and len(col) == 7 if not self.is_simple_string: - if len(self.color_list) <= self.MAX_COLORS: + if len(self.color_list) <= self._MAX_COLORS: return all(match_ok(c) for c in self.color_list) else: return False @@ -120,57 +118,16 @@ class ThemeManager: self._theme_path = "" self._current_theme = "" - self._space_weather_labels = SwitchableLabelsIterable( - *list( - chain( - self._owner.switchable_r_labels, - self._owner.switchable_s_labels, - self._owner.switchable_g_now_labels, - self._owner.switchable_g_today_labels, - self._owner.k_storm_labels, - self._owner.a_storm_labels, - [self._owner.expected_noise_lbl] - ) - ) - ) - - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "switch_on_colors", ThemeConstants.DEFAULT_ON_COLORS ) - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "switch_off_colors", ThemeConstants.DEFAULT_OFF_COLORS ) self._theme_names = {} - def _refresh_range_labels(self): - """Refresh the range-labels.""" - self._owner.set_acf_interval_label() - self._owner.set_band_filter_label( - self._owner.activate_low_band_filter_btn, - self._owner.lower_band_spinbox, - self._owner.lower_band_filter_unit, - self._owner.lower_band_confidence, - self._owner.activate_up_band_filter_btn, - self._owner.upper_band_spinbox, - self._owner.upper_band_filter_unit, - self._owner.upper_band_confidence, - self._owner.band_range_lbl - ) - - self._owner.set_band_filter_label( - self._owner.activate_low_freq_filter_btn, - self._owner.lower_freq_spinbox, - self._owner.lower_freq_filter_unit, - self._owner.lower_freq_confidence, - self._owner.activate_up_freq_filter_btn, - self._owner.upper_freq_spinbox, - self._owner.upper_freq_filter_unit, - self._owner.upper_freq_confidence, - self._owner.freq_range_lbl - ) - @pyqtSlot() def _apply(self, theme_path): """Apply the selected theme. @@ -185,12 +142,12 @@ class ThemeManager: item=self._owner.signals_list.currentItem(), previous_item=None ) - self._refresh_range_labels() - self._owner.audio_widget.refresh_btns_colors( + self._owner.filters.refresh() + self._owner.audio_widget.refresh( self._owner.active_color, self._owner.inactive_color ) - self._space_weather_labels.refresh() + self._owner.spaceweather_screen.refreshable_labels.refresh() else: pop_up(self._owner, title=ThemeConstants.THEME_NOT_FOUND, text=ThemeConstants.MISSING_THEME).show() @@ -297,20 +254,20 @@ class ThemeManager: inactive_color_ok = True if color.quality == Constants.TEXT_COLOR: text_color_ok = True - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "text_color", color.color_str ) for color in color_handler.double_color_list: if color.quality == Constants.LABEL_ON_COLOR: switch_on_color_ok = True - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "switch_on_colors", color.color_list ) if color.quality == Constants.LABEL_OFF_COLOR: switch_off_color_ok = True - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "switch_off_colors", color.color_list ) @@ -320,17 +277,17 @@ class ThemeManager: self._owner.inactive_color = ThemeConstants.DEFAULT_INACTIVE_COLOR if not (switch_on_color_ok and switch_off_color_ok): - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "switch_on_colors", ThemeConstants.DEFAULT_ON_COLORS ) - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "switch_off_colors", ThemeConstants.DEFAULT_OFF_COLORS ) if not text_color_ok: - self._space_weather_labels.set( + self._owner.spaceweather_screen.refreshable_labels.set( "text_color", ThemeConstants.DEFAULT_TEXT_COLOR ) diff --git a/src/utilities.py b/src/utilities.py index 375ec88..00e7de2 100644 --- a/src/utilities.py +++ b/src/utilities.py @@ -23,6 +23,29 @@ def uncheck_and_emit(button): button.clicked.emit() +def show_matching_strings(list_elements, text): + """Show all elements of QListWidget that matches (even partially) a target text. + + Arguments: + list_elements -- the QListWidget + text -- the target text.""" + for index in range(list_elements.count()): + item = list_elements.item(index) + if text.lower() in item.text().lower() or item.isSelected(): + item.setHidden(False) + else: + item.setHidden(True) + + +def get_field_entries(db_entry, separator=Constants.FIELD_SEPARATOR): + """Take a database entry and optionally a separator string. + + Return a list obtained by splitting the signal field with separator.""" + return [ + x.strip() for x in db_entry.split(separator) + ] + + def pop_up(cls, title, text, informative_text=None, connection=None,