Add docstrings. Also add safe_cast function. Finally

fix some minor issues.
This commit is contained in:
alessandro90
2019-05-25 15:18:06 +02:00
parent a7e36505e9
commit 43a9ce954e
14 changed files with 531 additions and 181 deletions

View File

@@ -41,7 +41,8 @@ from utilities import (checksum_ok,
is_undef_freq, is_undef_freq,
is_undef_band, is_undef_band,
format_numbers, format_numbers,
resource_path,) resource_path,
safe_cast)
# import default_imgs_rc # import default_imgs_rc
@@ -51,8 +52,10 @@ Ui_MainWindow, _ = uic.loadUiType(qt_creator_file)
class Artemis(QMainWindow, Ui_MainWindow): class Artemis(QMainWindow, Ui_MainWindow):
"""Main application class."""
def __init__(self): def __init__(self):
"""Set all connections of the application."""
super().__init__() super().__init__()
self.setupUi(self) self.setupUi(self)
self.set_initial_size() self.set_initial_size()
@@ -481,8 +484,8 @@ class Artemis(QMainWindow, Ui_MainWindow):
# Left list widget and search bar. # Left list widget and search bar.
self.search_bar.textChanged.connect(self.display_signals) self.search_bar.textChanged.connect(self.display_signals)
self.result_list.currentItemChanged.connect(self.display_specs) self.signals_list.currentItemChanged.connect(self.display_specs)
self.result_list.itemDoubleClicked.connect( self.signals_list.itemDoubleClicked.connect(
lambda: self.main_tab.setCurrentWidget(self.signal_properties_tab) lambda: self.main_tab.setCurrentWidget(self.signal_properties_tab)
) )
self.audio_widget = AudioPlayer( self.audio_widget = AudioPlayer(
@@ -536,18 +539,31 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def start_update_forecast(self): def start_update_forecast(self):
"""Start the update of the 3-day forecast screen.
Start the corresponding thread.
"""
if not self.forecast_data.is_updating: if not self.forecast_data.is_updating:
self.update_forecast_bar.set_updating() self.update_forecast_bar.set_updating()
self.forecast_data.update() self.forecast_data.update()
@pyqtSlot() @pyqtSlot()
def start_update_space_weather(self): 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: if not self.space_weather_data.is_updating:
self.update_now_bar.set_updating() self.update_now_bar.set_updating()
self.space_weather_data.update() self.space_weather_data.update()
@pyqtSlot(bool) @pyqtSlot(bool)
def update_forecast(self, status_ok): def update_forecast(self, status_ok):
"""Update the 3-day forecast screen after a successful download.
If the download was not successful throw a warning. In any case remove
the downloaded data.
"""
self.update_forecast_bar.set_idle() self.update_forecast_bar.set_idle()
if status_ok: if status_ok:
self.forecast_data.update_all_labels() self.forecast_data.update_all_labels()
@@ -558,9 +574,14 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot(bool) @pyqtSlot(bool)
def update_space_weather(self, status_ok): 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() self.update_now_bar.set_idle()
if status_ok: if status_ok:
xray_long = float(self.space_weather_data.xray[-1][7]) xray_long = safe_cast(self.space_weather_data.xray[-1][7], float)
format_text = lambda letter, power: letter + f"{xray_long * 10**power:.1f}" format_text = lambda letter, power: letter + f"{xray_long * 10**power:.1f}"
if xray_long < 1e-8 and xray_long != -1.00e+05: if xray_long < 1e-8 and xray_long != -1.00e+05:
self.peak_flux_lbl.setText(format_text("<A", 8)) self.peak_flux_lbl.setText(format_text("<A", 8))
@@ -592,7 +613,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
elif xray_long == -1.00e+05: elif xray_long == -1.00e+05:
self.switchable_r_labels.switch_off_all() self.switchable_r_labels.switch_off_all()
pro10 = float(self.space_weather_data.prot_el[-1][8]) pro10 = safe_cast(self.space_weather_data.prot_el[-1][8], float)
if pro10 < 10 and pro10 != -1.00e+05: if pro10 < 10 and pro10 != -1.00e+05:
self.switchable_s_labels.switch_on(self.s0_now_lbl) self.switchable_s_labels.switch_on(self.s0_now_lbl)
elif pro10 >= 10 and pro10 < 100: elif pro10 >= 10 and pro10 < 100:
@@ -608,9 +629,13 @@ class Artemis(QMainWindow, Ui_MainWindow):
elif pro10 == -1.00e+05: elif pro10 == -1.00e+05:
self.switchable_s_labels.switch_off_all() self.switchable_s_labels.switch_off_all()
k_index = int(self.space_weather_data.ak_index[8][11].replace('.', '')) k_index = safe_cast(
self.space_weather_data.ak_index[8][11].replace('.', ''), int
)
self.k_index_lbl.setText(str(k_index)) self.k_index_lbl.setText(str(k_index))
a_index = int(self.space_weather_data.ak_index[7][7].replace('.', '')) a_index = safe_cast(
self.space_weather_data.ak_index[7][7].replace('.', ''), int
)
self.a_index_lbl.setText(str(a_index)) self.a_index_lbl.setText(str(a_index))
if k_index == 0: if k_index == 0:
@@ -669,7 +694,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.a_storm_labels.switch_on(self.a_sev_storm_lbl) self.a_storm_labels.switch_on(self.a_sev_storm_lbl)
index = self.space_weather_data.geo_storm[6].index("was") + 1 index = self.space_weather_data.geo_storm[6].index("was") + 1
k_index_24_hmax = int(self.space_weather_data.geo_storm[6][index]) k_index_24_hmax = safe_cast(
self.space_weather_data.geo_storm[6][index], int
)
if k_index_24_hmax == 0: if k_index_24_hmax == 0:
self.switchable_g_today_labels.switch_on(self.g0_today_lbl) self.switchable_g_today_labels.switch_on(self.g0_today_lbl)
elif k_index_24_hmax == 1: elif k_index_24_hmax == 1:
@@ -691,12 +718,18 @@ class Artemis(QMainWindow, Ui_MainWindow):
elif k_index_24_hmax == 9: elif k_index_24_hmax == 9:
self.switchable_g_today_labels.switch_on(self.g5_today_lbl) self.switchable_g_today_labels.switch_on(self.g5_today_lbl)
val = int(self.space_weather_data.ak_index[7][2].replace('.', '')) val = safe_cast(
self.space_weather_data.ak_index[7][2].replace('.', ''), int
)
self.sfi_lbl.setText(f"{val}") self.sfi_lbl.setText(f"{val}")
val = int([x[4] for x in self.space_weather_data.sgas if "SSN" in x][0]) 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}") self.sn_lbl.setText(f"{val:d}")
for label, pixmap in zip(self.space_weather_labels, self.space_weather_data.images): for label, pixmap in zip(self.space_weather_labels,
self.space_weather_data.images):
label.pixmap = pixmap label.pixmap = pixmap
label.make_transparent() label.make_transparent()
label.apply_pixmap() label.apply_pixmap()
@@ -707,6 +740,12 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def go_to_gfd(self, by): def go_to_gfd(self, by):
"""Open a browser tab with the GFD site.
Make the search by frequency or location.
Argument:
by -- either GfdType.FREQ or GfdType.LOC.
"""
query = "/?q=" query = "/?q="
if by is GfdType.FREQ: if by is GfdType.FREQ:
value_in_mhz = self.freq_gfd.value() \ value_in_mhz = self.freq_gfd.value() \
@@ -722,23 +761,37 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot(QListWidgetItem) @pyqtSlot(QListWidgetItem)
def remove_if_unselected_modulation(self, item): def remove_if_unselected_modulation(self, item):
"""If an item is unselected from the modulations list, remove the item."""
if not item.isSelected(): if not item.isSelected():
self.show_matching_modulations(self.search_bar_modulation.text()) self.show_matching_modulations(self.search_bar_modulation.text())
@pyqtSlot(QListWidgetItem) @pyqtSlot(QListWidgetItem)
def remove_if_unselected_location(self, item): def remove_if_unselected_location(self, item):
"""If an item is unselected from the locations list, remove the item."""
if not item.isSelected(): if not item.isSelected():
self.show_matching_locations(self.search_bar_location.text()) self.show_matching_locations(self.search_bar_location.text())
@pyqtSlot(str) @pyqtSlot(str)
def show_matching_modulations(self, text): 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) self.show_matching_strings(self.modulation_list, text)
@pyqtSlot(str) @pyqtSlot(str)
def show_matching_locations(self, text): 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) self.show_matching_strings(self.locations_list, text)
def show_matching_strings(self, list_elements, text): def show_matching_strings(self, list_elements, text):
"""Show all elements of QListWidget the matches (even partially) a target text.
Arguments:
list_elements -- the QListWidget
text -- the target text.
"""
for index in range(list_elements.count()): for index in range(list_elements.count()):
item = list_elements.item(index) item = list_elements.item(index)
if text.lower() in item.text().lower() or item.isSelected(): if text.lower() in item.text().lower() or item.isSelected():
@@ -747,6 +800,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
item.setHidden(True) item.setHidden(True)
def set_mode_tree_widget(self): def set_mode_tree_widget(self):
"""Construct the QTreeWidget for the 'Mode' screen."""
for parent, children in Constants.MODES.items(): for parent, children in Constants.MODES.items():
iparent = QTreeWidgetItem([parent]) iparent = QTreeWidgetItem([parent])
self.mode_tree_widget.addTopLevelItem(iparent) self.mode_tree_widget.addTopLevelItem(iparent)
@@ -756,6 +810,10 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.mode_tree_widget.expandAll() self.mode_tree_widget.expandAll()
def manage_mode_selections(self): 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() selected_items = self.mode_tree_widget.selectedItems()
parents = Constants.MODES.keys() parents = Constants.MODES.keys()
for parent in parents: for parent in parents:
@@ -765,10 +823,12 @@ class Artemis(QMainWindow, Ui_MainWindow):
item.child(i).setSelected(True) item.child(i).setSelected(True)
def set_initial_size(self): def set_initial_size(self):
"""Function to handle high resolution screens. The function sets bigger """Handle high resolution screens.
sizes for all the relevant fixed-size widgets.
Also by default it sets the size to 3/4 of the available space Set bigger sizes for all the relevant fixed-size widgets.
both vertically and horizontally.""" Also by default set the size to 3/4 of the available space both
vertically and horizontally.
"""
d = QDesktopWidget().availableGeometry() d = QDesktopWidget().availableGeometry()
w = d.width() w = d.width()
h = d.height() h = d.height()
@@ -796,6 +856,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.freq_gfd.setFixedWidth(200) self.freq_gfd.setFixedWidth(200)
self.unit_freq_gfd.setFixedWidth(120) self.unit_freq_gfd.setFixedWidth(120)
self.mode_tree_widget.setMinimumWidth(500)
self.modulation_list.setMinimumWidth(500)
self.audio_progress.setFixedHeight(20) self.audio_progress.setFixedHeight(20)
self.volume.setStyleSheet(""" self.volume.setStyleSheet("""
QSlider::groove:horizontal { QSlider::groove:horizontal {
@@ -815,12 +878,23 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def download_db(self): def download_db(self):
"""Start the database download.
Do nothing if already downloading.
"""
if not self.download_window.isVisible(): if not self.download_window.isVisible():
self.download_window.start_download() self.download_window.start_download()
self.download_window.show() self.download_window.show()
@pyqtSlot() @pyqtSlot()
def ask_if_download(self): def ask_if_download(self):
"""Check if the database is at its latest version.
If a new database is available automatically start the download.
If not ask if should download it anyway.
If already downloading do nothing.
Handle possible connection errors.
"""
if not self.download_window.isVisible(): if not self.download_window.isVisible():
db_path = os.path.join(Constants.DATA_FOLDER, Database.NAME) db_path = os.path.join(Constants.DATA_FOLDER, Database.NAME)
try: try:
@@ -848,6 +922,13 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def check_db_ver(self): def check_db_ver(self):
"""Check if the database is at its latest version.
If a new database version is available, ask if it should be downloaded.
If not display a message.
If already downloading do nothing.
Handle possible connection errors.
"""
if not self.download_window.isVisible(): if not self.download_window.isVisible():
db_path = os.path.join(Constants.DATA_FOLDER, Database.NAME) db_path = os.path.join(Constants.DATA_FOLDER, Database.NAME)
answer = None answer = None
@@ -881,11 +962,17 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def show_downloaded_signals(self): def show_downloaded_signals(self):
"""Load and display the database signal list."""
self.search_bar.setEnabled(True) self.search_bar.setEnabled(True)
self.load_db() self.load_db()
self.display_signals() self.display_signals()
def load_db(self): def load_db(self):
"""Load the database from file.
Populate the signals list and set the total number of signals.
Handle possible missing file.
"""
names = Database.NAMES names = Database.NAMES
try: try:
self.db = read_csv(os.path.join(Constants.DATA_FOLDER, Database.NAME), self.db = read_csv(os.path.join(Constants.DATA_FOLDER, Database.NAME),
@@ -908,9 +995,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.db.fillna(Constants.UNKNOWN, inplace=True) self.db.fillna(Constants.UNKNOWN, inplace=True)
self.db[Signal.WIKI_CLICKED] = False self.db[Signal.WIKI_CLICKED] = False
self.update_status_tip(self.total_signals) self.update_status_tip(self.total_signals)
self.result_list.clear() self.signals_list.clear()
self.result_list.addItems(self.signal_names) self.signals_list.addItems(self.signal_names)
self.result_list.setCurrentItem(None) self.signals_list.setCurrentItem(None)
self.modulation_list.addItems( self.modulation_list.addItems(
self.collect_list( self.collect_list(
Signal.MODULATION Signal.MODULATION
@@ -923,6 +1010,13 @@ class Artemis(QMainWindow, Ui_MainWindow):
) )
def collect_list(self, list_property, separator=';'): def collect_list(self, list_property, separator=';'):
"""Collect all the entrys of a QListWidget.
Handle multiple entries in one item seprated by a separator.
Keyword argument:
seprator -- the separator character for multiple-entries items.
"""
values = self.db[list_property] values = self.db[list_property]
values = list( values = list(
set([ set([
@@ -939,6 +1033,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
lower_spin_box, lower_spin_box,
upper_combo_box, upper_combo_box,
upper_spin_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(): if lower_spin_box.isEnabled():
unit_conversion = {'Hz' : ['kHz', 'MHz', 'GHz'], unit_conversion = {'Hz' : ['kHz', 'MHz', 'GHz'],
'kHz': ['MHz', 'GHz'], 'kHz': ['MHz', 'GHz'],
@@ -979,6 +1076,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
upper_unit, upper_unit,
upper_confidence, upper_confidence,
range_lbl): range_lbl):
"""Display the actual range applied for the signal's property search.
Used for frequency and bandwidth screens."""
activate_low = False activate_low = False
activate_high = False activate_high = False
color = self.inactive_color color = self.inactive_color
@@ -1020,6 +1120,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def set_acf_interval_label(self): def set_acf_interval_label(self):
"""Display the actual acf interval for the search."""
tolerance = self.acf_spinbox.value() * self.acf_confidence.value() / 100 tolerance = self.acf_spinbox.value() * self.acf_confidence.value() / 100
if tolerance > 0: if tolerance > 0:
val = round(self.acf_spinbox.value() - tolerance, Constants.MAX_DIGITS) val = round(self.acf_spinbox.value() - tolerance, Constants.MAX_DIGITS)
@@ -1032,12 +1133,17 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def activate_if_toggled(self, radio_btn, *widgets): def activate_if_toggled(self, radio_btn, *widgets):
"""If radio_btn is toggled, activate all *widgets.
Do nothing otherwise.
"""
toggled = radio_btn.isChecked() toggled = radio_btn.isChecked()
for w in widgets[:-1]: # Neglect the bool coming from the emitted signal. for w in widgets[:-1]: # Neglect the bool coming from the emitted signal.
w.setEnabled(toggled) w.setEnabled(toggled)
@pyqtSlot() @pyqtSlot()
def display_signals(self): def display_signals(self):
"""Display all the signal names which matches the applied filters."""
text = self.search_bar.text() text = self.search_bar.text()
available_signals = 0 available_signals = 0
for index, signal_name in enumerate(self.signal_names): for index, signal_name in enumerate(self.signal_names):
@@ -1049,15 +1155,16 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.modulation_filters_ok(signal_name) , self.modulation_filters_ok(signal_name) ,
self.location_filters_ok(signal_name) , self.location_filters_ok(signal_name) ,
self.acf_filters_ok(signal_name)]): self.acf_filters_ok(signal_name)]):
self.result_list.item(index).setHidden(False) self.signals_list.item(index).setHidden(False)
available_signals += 1 available_signals += 1
else: else:
self.result_list.item(index).setHidden(True) self.signals_list.item(index).setHidden(True)
# Remove selected item. # Remove selected item.
self.result_list.setCurrentItem(None) self.signals_list.setCurrentItem(None)
self.update_status_tip(available_signals) self.update_status_tip(available_signals)
def update_status_tip(self, available_signals): def update_status_tip(self, available_signals):
"""Display the number of displayed signals in the status tip."""
if available_signals < self.total_signals: if available_signals < self.total_signals:
self.statusbar.setStyleSheet(f'color: {self.active_color}') self.statusbar.setStyleSheet(f'color: {self.active_color}')
else: else:
@@ -1068,6 +1175,10 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def reset_fb_filters(self, ftype): def reset_fb_filters(self, ftype):
"""Reset the Frequency or Bandwidth depending og 'ftype'.
ftype can be either Ftype.FREQ or Ftype.BAND.
"""
if ftype != Ftype.FREQ and ftype != Ftype.BAND: if ftype != Ftype.FREQ and ftype != Ftype.BAND:
raise ValueError("Wrong ftype in function 'reset_fb_filters'") raise ValueError("Wrong ftype in function 'reset_fb_filters'")
@@ -1102,6 +1213,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def reset_cat_filters(self): def reset_cat_filters(self):
"""Reset the category filter screen."""
uncheck_and_emit(self.apply_remove_cat_filter_btn) uncheck_and_emit(self.apply_remove_cat_filter_btn)
for f in self.cat_filter_btns: for f in self.cat_filter_btns:
if f.isChecked(): if f.isChecked():
@@ -1110,6 +1222,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def reset_mode_filters(self): def reset_mode_filters(self):
"""Reset the mode filter screen."""
uncheck_and_emit(self.apply_remove_mode_filter_btn) uncheck_and_emit(self.apply_remove_mode_filter_btn)
parents = Constants.MODES.keys() parents = Constants.MODES.keys()
selected_children = [] selected_children = []
@@ -1125,6 +1238,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def reset_modulation_filters(self): def reset_modulation_filters(self):
"""Reset the modulation filter screen."""
uncheck_and_emit(self.apply_remove_modulation_filter_btn) uncheck_and_emit(self.apply_remove_modulation_filter_btn)
self.search_bar_modulation.setText('') self.search_bar_modulation.setText('')
self.show_matching_strings( self.show_matching_strings(
@@ -1137,6 +1251,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def reset_location_filters(self): def reset_location_filters(self):
"""Reset the location filter screen."""
uncheck_and_emit(self.apply_remove_location_filter_btn) uncheck_and_emit(self.apply_remove_location_filter_btn)
self.search_bar_location.setText('') self.search_bar_location.setText('')
self.show_matching_strings( self.show_matching_strings(
@@ -1149,6 +1264,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def reset_acf_filters(self): def reset_acf_filters(self):
"""Reset the acf filter screen."""
uncheck_and_emit(self.apply_remove_acf_filter_btn) uncheck_and_emit(self.apply_remove_acf_filter_btn)
if self.include_undef_acf.isChecked(): if self.include_undef_acf.isChecked():
self.include_undef_acf.setChecked(False) self.include_undef_acf.setChecked(False)
@@ -1156,6 +1272,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.acf_confidence.setValue(0) self.acf_confidence.setValue(0)
def frequency_filters_ok(self, signal_name): def frequency_filters_ok(self, signal_name):
"""Evalaute if the a signal matches the frequency filters."""
if not self.apply_remove_freq_filter_btn.isChecked(): if not self.apply_remove_freq_filter_btn.isChecked():
return True return True
undef_freq = is_undef_freq(self.db.loc[signal_name]) undef_freq = is_undef_freq(self.db.loc[signal_name])
@@ -1166,8 +1283,8 @@ class Artemis(QMainWindow, Ui_MainWindow):
return False return False
signal_freqs = ( signal_freqs = (
int(self.db.at[signal_name, Signal.INF_FREQ]), safe_cast(self.db.at[signal_name, Signal.INF_FREQ], int),
int(self.db.at[signal_name, Signal.SUP_FREQ]) safe_cast(self.db.at[signal_name, Signal.SUP_FREQ], int)
) )
band_filter_ok = False band_filter_ok = False
@@ -1195,6 +1312,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
return lower_limit_ok and upper_limit_ok return lower_limit_ok and upper_limit_ok
def band_filters_ok(self, signal_name): def band_filters_ok(self, signal_name):
"""Evalaute if the a signal matches the band filters."""
if not self.apply_remove_band_filter_btn.isChecked(): if not self.apply_remove_band_filter_btn.isChecked():
return True return True
undef_band = is_undef_band(self.db.loc[signal_name]) undef_band = is_undef_band(self.db.loc[signal_name])
@@ -1205,8 +1323,8 @@ class Artemis(QMainWindow, Ui_MainWindow):
return False return False
signal_bands = ( signal_bands = (
int(self.db.at[signal_name, Signal.INF_BAND]), safe_cast(self.db.at[signal_name, Signal.INF_BAND], int),
int(self.db.at[signal_name, Signal.SUP_BAND]) safe_cast(self.db.at[signal_name, Signal.SUP_BAND], int)
) )
lower_limit_ok = True lower_limit_ok = True
@@ -1224,6 +1342,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
return lower_limit_ok and upper_limit_ok return lower_limit_ok and upper_limit_ok
def category_filters_ok(self, signal_name): def category_filters_ok(self, signal_name):
"""Evalaute if the a signal matches the category filters."""
if not self.apply_remove_cat_filter_btn.isChecked(): if not self.apply_remove_cat_filter_btn.isChecked():
return True return True
cat_code = self.db.at[signal_name, Signal.CATEGORY_CODE] cat_code = self.db.at[signal_name, Signal.CATEGORY_CODE]
@@ -1240,6 +1359,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
return cat_checked == positive_cases and cat_checked > 0 return cat_checked == positive_cases and cat_checked > 0
def mode_filters_ok(self, signal_name): def mode_filters_ok(self, signal_name):
"""Evalaute if the a signal matches the mode filters."""
if not self.apply_remove_mode_filter_btn.isChecked(): if not self.apply_remove_mode_filter_btn.isChecked():
return True return True
signal_mode = self.db.at[signal_name, Signal.MODE] signal_mode = self.db.at[signal_name, Signal.MODE]
@@ -1263,6 +1383,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
return any(ok) return any(ok)
def modulation_filters_ok(self, signal_name): def modulation_filters_ok(self, signal_name):
"""Evalaute if the a signal matches the modulation filters."""
if not self.apply_remove_modulation_filter_btn.isChecked(): if not self.apply_remove_modulation_filter_btn.isChecked():
return True return True
signal_modulation = [ signal_modulation = [
@@ -1274,6 +1395,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
return False return False
def location_filters_ok(self, signal_name): def location_filters_ok(self, signal_name):
"""Evalaute if the a signal matches the location filters."""
if not self.apply_remove_location_filter_btn.isChecked(): if not self.apply_remove_location_filter_btn.isChecked():
return True return True
signal_location = self.db.at[signal_name, Signal.LOCATION] signal_location = self.db.at[signal_name, Signal.LOCATION]
@@ -1283,6 +1405,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
return False return False
def acf_filters_ok(self, signal_name): def acf_filters_ok(self, signal_name):
"""Evalaute if the a signal matches the acf filters."""
if not self.apply_remove_acf_filter_btn.isChecked(): if not self.apply_remove_acf_filter_btn.isChecked():
return True return True
signal_acf = self.db.at[signal_name, Signal.ACF] signal_acf = self.db.at[signal_name, Signal.ACF]
@@ -1292,7 +1415,7 @@ class Artemis(QMainWindow, Ui_MainWindow):
else: else:
return False return False
else: else:
signal_acf = float(signal_acf.rstrip("ms")) signal_acf = safe_cast(signal_acf.rstrip("ms"), float)
tolerance = self.acf_spinbox.value() * self.acf_confidence.value() / 100 tolerance = self.acf_spinbox.value() * self.acf_confidence.value() / 100
upper_limit = self.acf_spinbox.value() + tolerance upper_limit = self.acf_spinbox.value() + tolerance
lower_limit = self.acf_spinbox.value() - tolerance lower_limit = self.acf_spinbox.value() - tolerance
@@ -1303,6 +1426,11 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot(QListWidgetItem, QListWidgetItem) @pyqtSlot(QListWidgetItem, QListWidgetItem)
def display_specs(self, item, previous_item): def display_specs(self, item, previous_item):
"""Display the signal properties.
item is the item corresponding to the selected signal
previous_item is unused.
"""
self.display_spectrogram() self.display_spectrogram()
if item is not None: if item is not None:
self.current_signal_name = item.text() self.current_signal_name = item.text()
@@ -1368,11 +1496,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.audio_widget.set_audio_player() self.audio_widget.set_audio_player()
def display_spectrogram(self): def display_spectrogram(self):
default_pic = os.path.join( """Display the selected signal's waterfall."""
Constants.DEFAULT_IMGS_FOLDER, default_pic = Constants.DEFAULT_NOT_SELECTED
Constants.NOT_SELECTED item = self.signals_list.currentItem()
)
item = self.result_list.currentItem()
if item: if item:
spectrogram_name = item.text() spectrogram_name = item.text()
path_spectr = os.path.join( path_spectr = os.path.join(
@@ -1381,23 +1507,32 @@ class Artemis(QMainWindow, Ui_MainWindow):
spectrogram_name + Constants.SPECTRA_EXT spectrogram_name + Constants.SPECTRA_EXT
) )
if not QFileInfo(path_spectr).exists(): if not QFileInfo(path_spectr).exists():
path_spectr = os.path.join( path_spectr = Constants.DEFAULT_NOT_AVAILABLE
Constants.DEFAULT_IMGS_FOLDER,
Constants.NOT_AVAILABLE
)
else: else:
path_spectr = default_pic path_spectr = default_pic
self.spectrogram.setPixmap(QPixmap(path_spectr)) self.spectrogram.setPixmap(QPixmap(path_spectr))
def activate_band_category(self, band_label, activate=True): def activate_band_category(self, band_label, activate=True):
"""Highlight the given band_label.
If activate is False remove the highlight (default to True).
"""
color = self.active_color if activate else self.inactive_color color = self.active_color if activate else self.inactive_color
for label in band_label: for label in band_label:
label.setStyleSheet(f"color: {color};") label.setStyleSheet(f"color: {color};")
def set_band_range(self, current_signal=None): def set_band_range(self, current_signal=None):
"""Highlight the signal's band labels.
If no signal is selected remove all highlights.
"""
if current_signal is not None and not is_undef_freq(current_signal): if current_signal is not None and not is_undef_freq(current_signal):
lower_freq = int(current_signal.at[Signal.INF_FREQ]) lower_freq = safe_cast(
upper_freq = int(current_signal.at[Signal.SUP_FREQ]) current_signal.at[Signal.INF_FREQ], int
)
upper_freq = safe_cast(
current_signal.at[Signal.SUP_FREQ], int
)
zipped = list(zip(Constants.BANDS, self.band_labels)) zipped = list(zip(Constants.BANDS, self.band_labels))
for i, w in enumerate(zipped): for i, w in enumerate(zipped):
band, band_label = w band, band_label = w
@@ -1417,6 +1552,10 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def reset_all_filters(self): def reset_all_filters(self):
"""Reset all filter screens.
Show all available signals.
"""
self.reset_frequency_filters_btn.clicked.emit() self.reset_frequency_filters_btn.clicked.emit()
self.reset_band_filters_btn.clicked.emit() self.reset_band_filters_btn.clicked.emit()
self.reset_cat_filters_btn.clicked.emit() self.reset_cat_filters_btn.clicked.emit()
@@ -1427,6 +1566,10 @@ class Artemis(QMainWindow, Ui_MainWindow):
@pyqtSlot() @pyqtSlot()
def go_to_web_page_signal(self): def go_to_web_page_signal(self):
"""Go the web page of the signal's wiki.
Do nothing is no signal is selected.
"""
if self.current_signal_name: if self.current_signal_name:
self.url_button.setStyleSheet( self.url_button.setStyleSheet(
f"color: {self.url_button.colors.clicked}" f"color: {self.url_button.colors.clicked}"
@@ -1435,6 +1578,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.db.at[self.current_signal_name, Signal.WIKI_CLICKED] = True self.db.at[self.current_signal_name, Signal.WIKI_CLICKED] = True
def closeEvent(self, event): def closeEvent(self, event):
"""Extends closeEvent of QMainWindow.
Shutdown all active threads and close all open windows."""
self.closing = True self.closing = True
if self.download_window.isVisible(): if self.download_window.isVisible():
self.download_window.close() self.download_window.close()
@@ -1447,7 +1593,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
if __name__ == '__main__': if __name__ == '__main__':
my_app = QApplication(sys.argv) my_app = QApplication(sys.argv)
img = QPixmap(":/icon/default_pics/Artemis3.500px.png") img = QPixmap(os.path.join(
":", "icon", "default_pics", "Artemis3.500px.png")
)
splash = QSplashScreen(img) splash = QSplashScreen(img)
splash.show() splash.show()
start= time() start= time()

View File

@@ -119,7 +119,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="0" colspan="2"> <item row="1" column="0" colspan="2">
<widget class="QListWidget" name="result_list"> <widget class="QListWidget" name="signals_list">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding"> <sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch> <horstretch>0</horstretch>

View File

@@ -7,14 +7,23 @@ from constants import Constants
import qtawesome as qta import qtawesome as qta
class AudioPlayer(QObject): # Maybe useless inheriting from QObject class AudioPlayer(QObject):
"""This is the audio player widget. The only public methods are the __init__ """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_btns_colors.
Everything else is managed internally.""" Everything else is managed internally."""
__time_step = 500 # Milliseconds. __time_step = 500 # Milliseconds.
def __init__(self, play, pause, stop, volume, audio_progress, active_color, inactive_color): def __init__(self, play,
pause,
stop,
volume,
audio_progress,
active_color,
inactive_color):
"""Initialize the player."""
super().__init__() super().__init__()
self.__paused = False self.__paused = False
self.__first_call = True self.__first_call = True
@@ -36,6 +45,7 @@ class AudioPlayer(QObject): # Maybe useless inheriting from QObject
self.refresh_btns_colors(active_color, inactive_color) self.refresh_btns_colors(active_color, inactive_color)
def refresh_btns_colors(self, active_color, inactive_color): def refresh_btns_colors(self, active_color, inactive_color):
"""Repaint the buttons of the widgetd after the theme has changed."""
self.__play.setIcon(qta.icon('fa5.play-circle', self.__play.setIcon(qta.icon('fa5.play-circle',
color=active_color, color=active_color,
color_disabled=inactive_color)) color_disabled=inactive_color))
@@ -48,12 +58,14 @@ class AudioPlayer(QObject): # Maybe useless inheriting from QObject
@pyqtSlot() @pyqtSlot()
def __set_volume(self): def __set_volume(self):
"""Set the volume of the audio samples."""
if mixer.get_init(): if mixer.get_init():
mixer.music.set_volume( mixer.music.set_volume(
self.__volume.value() / self.__volume.maximum() self.__volume.value() / self.__volume.maximum()
) )
def __reset_audio_widget(self): def __reset_audio_widget(self):
"""Reset the widget. Stop all playing samples."""
if mixer.get_init(): if mixer.get_init():
if mixer.music.get_busy(): if mixer.music.get_busy():
mixer.music.stop() mixer.music.stop()
@@ -65,6 +77,7 @@ class AudioPlayer(QObject): # Maybe useless inheriting from QObject
@pyqtSlot() @pyqtSlot()
def __update_bar(self): def __update_bar(self):
"""Upadte the progress bar."""
pos = mixer.music.get_pos() pos = mixer.music.get_pos()
if pos == -1: if pos == -1:
self.__timer.stop() self.__timer.stop()
@@ -74,11 +87,13 @@ class AudioPlayer(QObject): # Maybe useless inheriting from QObject
self.__audio_progress.setValue(pos) self.__audio_progress.setValue(pos)
def __set_max_progress_bar(self): def __set_max_progress_bar(self):
"""Set the maximum value of the progress bar."""
self.__audio_progress.setMaximum( self.__audio_progress.setMaximum(
mixer.Sound(self.__audio_file).get_length() * 1000 mixer.Sound(self.__audio_file).get_length() * 1000
) )
def set_audio_player(self, fname=""): def set_audio_player(self, fname=""):
"""Set the current audio sample."""
self.__first_call = True self.__first_call = True
self.__reset_audio_widget() self.__reset_audio_widget()
full_name = os.path.join( full_name = os.path.join(
@@ -92,6 +107,7 @@ class AudioPlayer(QObject): # Maybe useless inheriting from QObject
@pyqtSlot() @pyqtSlot()
def __play_audio(self): def __play_audio(self):
"""Play the audio sample."""
if not self.__paused: if not self.__paused:
if self.__first_call: if self.__first_call:
self.__first_call = False self.__first_call = False
@@ -111,6 +127,7 @@ class AudioPlayer(QObject): # Maybe useless inheriting from QObject
@pyqtSlot() @pyqtSlot()
def __stop_audio(self): def __stop_audio(self):
"""Stop the audio sample."""
mixer.music.stop() mixer.music.stop()
self.__audio_progress.reset() self.__audio_progress.reset()
self.__timer.stop() self.__timer.stop()
@@ -118,12 +135,14 @@ class AudioPlayer(QObject): # Maybe useless inheriting from QObject
@pyqtSlot() @pyqtSlot()
def __pause_audio(self): def __pause_audio(self):
"""Pause the audio sample."""
mixer.music.pause() mixer.music.pause()
self.__timer.stop() self.__timer.stop()
self.__paused = True self.__paused = True
self.__enable_buttons(True, False, False) self.__enable_buttons(True, False, False)
def __enable_buttons(self, play_en, pause_en, stop_en): def __enable_buttons(self, play_en, pause_en, stop_en):
"""Set the three buttons status."""
self.__play.setEnabled(play_en) self.__play.setEnabled(play_en)
self.__pause.setEnabled(pause_en) self.__pause.setEnabled(pause_en)
self.__stop.setEnabled(stop_en) self.__stop.setEnabled(stop_en)

View File

@@ -4,30 +4,31 @@ from constants import Constants
class ClickableProgressBar(QProgressBar): class ClickableProgressBar(QProgressBar):
"""Subclass QProgressBar. Clickable progress bar class."""
clicked = pyqtSignal() clicked = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
"""Initialize the instance."""
self.__text = '' self.__text = ''
super().__init__(parent) super().__init__(parent)
# def __set_text(self, text):
# self.__text = text
def text(self): def text(self):
"""Return the text displayed on the bar."""
return self.__text return self.__text
def set_idle(self): def set_idle(self):
# self.__set_text(Constants.CLICK_TO_UPDATE_STR) """Set the bar to a non-downloading status."""
self.__text = Constants.CLICK_TO_UPDATE_STR self.__text = Constants.CLICK_TO_UPDATE_STR
self.setMaximum(self.minimum() + 1) self.setMaximum(self.minimum() + 1)
def set_updating(self): def set_updating(self):
# self.__set_text(Constants.UPDATING_STR) """Set the bar to a downloading status."""
self.__text = Constants.UPDATING_STR self.__text = Constants.UPDATING_STR
self.setMaximum(self.minimum()) self.setMaximum(self.minimum())
def mousePressEvent(self, event): def mousePressEvent(self, event):
"""Override QWidget.mousePressEvent. Detect a click on the bar."""
if event.button() == Qt.LeftButton: if event.button() == Qt.LeftButton:
self.clicked.emit() self.clicked.emit()
else: else:

View File

@@ -4,21 +4,32 @@ import os.path
class Ftype: class Ftype:
"""Container class to differentiate between frequency and band.
used in reset_fb_filters.
"""
FREQ = "freq" FREQ = "freq"
BAND = "band" BAND = "band"
class GfdType(Enum): class GfdType(Enum):
"""Enum class to differentiate the possible GFD search criterias."""
FREQ = auto() FREQ = auto()
LOC = auto() LOC = auto()
class ChecksumWhat(Enum): class ChecksumWhat(Enum):
"""Enum class to distinguish the object you want to verify the checksum."""
FOLDER = auto() FOLDER = auto()
DB = auto() DB = auto()
class Messages: class Messages:
"""Container class for messages to be displayed."""
DB_UP_TO_DATE = "Already up to date" DB_UP_TO_DATE = "Already up to date"
DB_UP_TO_DATE_MSG = "No newer version to download." DB_UP_TO_DATE_MSG = "No newer version to download."
DB_NEW_VER = "New version available" DB_NEW_VER = "New version available"
@@ -34,6 +45,8 @@ class Messages:
class Signal: class Signal:
"""Container class for the signal property names."""
NAME = "name" NAME = "name"
INF_FREQ = "inf_freq" INF_FREQ = "inf_freq"
SUP_FREQ = "sup_freq" SUP_FREQ = "sup_freq"
@@ -50,6 +63,8 @@ class Signal:
class Database: class Database:
"""Container class for the database-related constants."""
LINK_LOC = "https://aresvalley.com/Storage/Artemis/Database/data.zip" LINK_LOC = "https://aresvalley.com/Storage/Artemis/Database/data.zip"
LINK_REF = "https://aresvalley.com/Storage/Artemis/Database/data.zip.log" LINK_REF = "https://aresvalley.com/Storage/Artemis/Database/data.zip.log"
NAME = "db.csv" NAME = "db.csv"
@@ -64,17 +79,18 @@ class Database:
Signal.DESCRIPTION, Signal.DESCRIPTION,
Signal.MODULATION, Signal.MODULATION,
Signal.CATEGORY_CODE, Signal.CATEGORY_CODE,
Signal.ACF,) Signal.ACF)
DELIMITER = "*" DELIMITER = "*"
STRINGS = (Signal.INF_FREQ, STRINGS = (Signal.INF_FREQ,
Signal.SUP_FREQ, Signal.SUP_FREQ,
Signal.MODE, Signal.MODE,
Signal.INF_BAND, Signal.INF_BAND,
Signal.SUP_BAND, Signal.SUP_BAND,
Signal.CATEGORY_CODE,) Signal.CATEGORY_CODE)
class ForecastColors: class ForecastColors:
"""Container class for the forecast labels colors."""
WARNING_COLOR = "#F95423" WARNING_COLOR = "#F95423"
KP9_COLOR = "#FFCCCB" KP9_COLOR = "#FFCCCB"
KP8_COLOR = "#FFCC9A" KP8_COLOR = "#FFCC9A"
@@ -84,6 +100,8 @@ class ForecastColors:
class Constants: class Constants:
"""Container class for several contants of the software."""
CLICK_TO_UPDATE_STR = "Click to update" CLICK_TO_UPDATE_STR = "Click to update"
SIGIDWIKI = "https://www.sigidwiki.com/wiki/Signal_Identification_Guide" SIGIDWIKI = "https://www.sigidwiki.com/wiki/Signal_Identification_Guide"
ADD_SIGNAL_LINK = "https://www.sigidwiki.com/index.php/Special:FormEdit/Signal/?preload=Signal_Identification_Wiki:Signal_form_preload_text" ADD_SIGNAL_LINK = "https://www.sigidwiki.com/index.php/Special:FormEdit/Signal/?preload=Signal_Identification_Wiki:Signal_form_preload_text"
@@ -119,8 +137,6 @@ class Constants:
LABEL_ON_COLOR = "on" LABEL_ON_COLOR = "on"
LABEL_OFF_COLOR = "off" LABEL_OFF_COLOR = "off"
TEXT_COLOR = "text" TEXT_COLOR = "text"
NOT_AVAILABLE = "spectrumnotavailable.png"
NOT_SELECTED = "nosignalselected.png"
__Band = namedtuple("Band", ["lower", "upper"]) __Band = namedtuple("Band", ["lower", "upper"])
__ELF = __Band(0, 30) # Formally it is (3, 30) Hz. __ELF = __Band(0, 30) # Formally it is (3, 30) Hz.
__SLF = __Band(30, 300) __SLF = __Band(30, 300)
@@ -149,10 +165,14 @@ class Constants:
"Chirp Spread Spectrum": (), "Chirp Spread Spectrum": (),
"FHSS-TDM": (), "FHSS-TDM": (),
"RAW": (), "RAW": (),
"SC-FDMA": (),} "SC-FDMA": ()}
APPLY = "Apply" APPLY = "Apply"
REMOVE = "Remove" REMOVE = "Remove"
UNKNOWN = "N/A" UNKNOWN = "N/A"
EXTRACTING_MSG = "Extracting..." EXTRACTING_MSG = "Extracting..."
EXTRACTING_CODE = -1 EXTRACTING_CODE = -1
NOT_AVAILABLE = "spectrumnotavailable.png"
NOT_SELECTED = "nosignalselected.png"
DEFAULT_IMGS_FOLDER = os.path.join(":", "pics", "default_pics") DEFAULT_IMGS_FOLDER = os.path.join(":", "pics", "default_pics")
DEFAULT_NOT_SELECTED = os.path.join(DEFAULT_IMGS_FOLDER, NOT_SELECTED)
DEFAULT_NOT_AVAILABLE = os.path.join(DEFAULT_IMGS_FOLDER, NOT_AVAILABLE)

View File

@@ -2,11 +2,18 @@ from PyQt5.QtWidgets import QPushButton
from PyQt5.QtCore import pyqtSlot from PyQt5.QtCore import pyqtSlot
class DoubleTextButton(QPushButton): class DoubleTextButton(QPushButton):
"""Subclass QPushButton.
A click will deactivate/activate a series of 'slave' widgets depending
on the 'checked' status of the button."""
def __init__(self, parent=None): def __init__(self, parent=None):
"""Extends QPushButton.__init__."""
super().__init__(parent) super().__init__(parent)
self.clicked.connect(self.__manage_click) self.clicked.connect(self.__manage_click)
def set_texts(self, text_a, text_b): def set_texts(self, text_a, text_b):
"""Set the two texts to be displayed."""
self.__text_a = text_a self.__text_a = text_a
self.__text_b = text_b self.__text_b = text_b
@@ -15,6 +22,14 @@ class DoubleTextButton(QPushButton):
ruled_by_radio_1=None, ruled_by_radio_1=None,
radio_2=None, radio_2=None,
ruled_by_radio_2=None): ruled_by_radio_2=None):
"""Set all the 'slave' widgets.
Keyword arguments:
simple_ones -- a list of widgets.
radio_1 -- a radio button.
ruled_by_radio_1 -- a list of widgets whose status depend upon radio_1.
radio_2 -- a radio button.
ruled_by_radio_2 -- a list of widgets whose status depend upon radio_2."""
self.__simple_ones = simple_ones self.__simple_ones = simple_ones
self.__ruled_by_radio_1 = ruled_by_radio_1 self.__ruled_by_radio_1 = ruled_by_radio_1
self.__radio_1 = radio_1 self.__radio_1 = radio_1
@@ -23,6 +38,7 @@ class DoubleTextButton(QPushButton):
@pyqtSlot() @pyqtSlot()
def __manage_click(self): def __manage_click(self):
"""Set the status of all the 'slave widgets' based on the status of the instance."""
if self.isChecked(): if self.isChecked():
self.setText(self.__text_b) self.setText(self.__text_b)
enable = False enable = False

View File

@@ -9,10 +9,12 @@ Ui_Download_window, _ = uic.loadUiType(resource_path("download_db_window.ui"))
class DownloadWindow(QWidget, Ui_Download_window): class DownloadWindow(QWidget, Ui_Download_window):
"""Subclass QWidget and Ui_Download_window. It is the window displayed during the database download."""
complete = pyqtSignal() complete = pyqtSignal()
def __init__(self): def __init__(self):
"""Initialize the window."""
super().__init__() super().__init__()
self.setupUi(self) self.setupUi(self)
self.setWindowFlags( self.setWindowFlags(
@@ -37,17 +39,21 @@ class DownloadWindow(QWidget, Ui_Download_window):
self.cancel_btn.clicked.connect(self.__terminate_process) self.cancel_btn.clicked.connect(self.__terminate_process)
def start_download(self): def start_download(self):
"""Start the download thread."""
self.__download_thread.start() self.__download_thread.start()
def __downlaod_format_str(self, n, speed): def __downlaod_format_str(self, n, speed):
"""Return a well-formatted string with downloaded MB and speed."""
return f"Downloaded MB: {n}\nSpeed: {speed} MB/s" return f"Downloaded MB: {n}\nSpeed: {speed} MB/s"
def show(self): def show(self):
"""Extends QWidget.show. Set downloaded MB and speed to zero."""
self.status_lbl.setText(self.__downlaod_format_str(0, 0)) self.status_lbl.setText(self.__downlaod_format_str(0, 0))
super().show() super().show()
@pyqtSlot(int, float) @pyqtSlot(int, float)
def __display_progress(self, progress, speed): def __display_progress(self, progress, speed):
"""Display the downloaded MB and speed."""
if progress != Constants.EXTRACTING_CODE: if progress != Constants.EXTRACTING_CODE:
self.status_lbl.setText(self.__downlaod_format_str(progress, speed)) self.status_lbl.setText(self.__downlaod_format_str(progress, speed))
elif progress == Constants.EXTRACTING_CODE: elif progress == Constants.EXTRACTING_CODE:
@@ -55,6 +61,7 @@ class DownloadWindow(QWidget, Ui_Download_window):
@pyqtSlot() @pyqtSlot()
def __terminate_process(self): def __terminate_process(self):
"""Terminate the download thread and close."""
if self.__download_thread.isRunning(): if self.__download_thread.isRunning():
self.__download_thread.terminate() self.__download_thread.terminate()
self.__download_thread.wait() self.__download_thread.wait()
@@ -62,6 +69,7 @@ class DownloadWindow(QWidget, Ui_Download_window):
@pyqtSlot() @pyqtSlot()
def __wait_close(self): def __wait_close(self):
"""Decide the action based on the download thread status and close."""
if self.__download_thread.status is ThreadStatus.OK: if self.__download_thread.status is ThreadStatus.OK:
self.complete.emit() self.complete.emit()
self.close() self.close()
@@ -73,6 +81,7 @@ class DownloadWindow(QWidget, Ui_Download_window):
self.close() self.close()
def reject(self): def reject(self):
"""Extends QWidget.reject. Terminate the download thread."""
if self.__download_thread.isRunning(): if self.__download_thread.isRunning():
self.__download_thread.terminate() self.__download_thread.terminate()
self.__download_thread.wait() self.__download_thread.wait()

View File

@@ -3,21 +3,29 @@ from PyQt5.QtCore import Qt
class FixedAspectRatioLabel(QLabel): class FixedAspectRatioLabel(QLabel):
"""Subclass QLabel. A resizable label class."""
def __init__(self, parent = None): def __init__(self, parent = None):
"""Initialize the instance. Set the pixmap to None."""
super().__init__(parent) super().__init__(parent)
self.pixmap = None self.pixmap = None
def set_default_stylesheet(self): def set_default_stylesheet(self):
"""Set the initial stylesheet of the label."""
self.setStyleSheet("""border-width: 1px; self.setStyleSheet("""border-width: 1px;
border-style: solid; border-style: solid;
border-color: black;""" border-color: black;"""
) )
def make_transparent(self): def make_transparent(self):
"""Make the label transparent.
Remove text and border."""
self.setText('') self.setText('')
self.setStyleSheet("border-width: 0px;") self.setStyleSheet("border-width: 0px;")
def apply_pixmap(self): def apply_pixmap(self):
"""Apply a scaled pixmap without modifying the dimension of the original one."""
if self.pixmap: if self.pixmap:
self.setPixmap( self.setPixmap(
self.pixmap.scaled( self.pixmap.scaled(
@@ -26,5 +34,6 @@ class FixedAspectRatioLabel(QLabel):
) )
def rescale(self, size): def rescale(self, size):
"""Rescale the widget and the displayed pixmap to the given size."""
self.resize(size) self.resize(size)
self.apply_pixmap() self.apply_pixmap()

View File

@@ -3,12 +3,16 @@ from PyQt5.QtCore import QSize
class FixedAspectRatioWidget(QWidget): class FixedAspectRatioWidget(QWidget):
"""Subclass QWidget. Keep all the internal labels to a fixed aaspect ratio."""
space = 10 space = 10
def __init__(self, parent=None): def __init__(self, parent=None):
"""Initialize the instance."""
super().__init__(parent) super().__init__(parent)
self.labels = [] self.labels = []
def resizeEvent(self, event): def resizeEvent(self, event):
"""Override QWidget.resizeEvent. Rescale all the internal widgets."""
h, w = self.height(), self.width() h, w = self.height(), self.width()
h_lbl = h / 9 - self.space h_lbl = h / 9 - self.space
w_lbl = 5 * h_lbl w_lbl = 5 * h_lbl

View File

@@ -3,34 +3,49 @@ from constants import ForecastColors
class _BaseSwitchableLabel(QLabel): class _BaseSwitchableLabel(QLabel):
"""Subclass QLabel. Base class for the switchable labels."""
def __init__(self, parent=None): def __init__(self, parent=None):
"""Set is_on = False and level = 0."""
super().__init__(parent) super().__init__(parent)
self.is_on = False self.is_on = False
self.level = 0 self.level = 0
def switch_on(self): def switch_on(self):
"""Set is_on = True."""
self.is_on = True self.is_on = True
def switch_off(self): def switch_off(self):
"""Set is_on = False."""
self.is_on = False self.is_on = False
class SwitchableLabel(_BaseSwitchableLabel): class SwitchableLabel(_BaseSwitchableLabel):
"""Subclass _BaseSwitchableLabel."""
def __init__(self, parent=None): def __init__(self, parent=None):
"""Define text and colors attributes."""
super().__init__(parent) super().__init__(parent)
self.switch_on_colors = () self.switch_on_colors = ()
self.switch_off_colors = () self.switch_off_colors = ()
self.text_color = '' self.text_color = ''
def switch_on(self): def switch_on(self):
"""Extend _BaseSwitchableLabel.switch_on.
Apply the active state colors."""
super().switch_on() super().switch_on()
self.__apply_colors(*self.switch_on_colors) self.__apply_colors(*self.switch_on_colors)
def switch_off(self): def switch_off(self):
"""Extend _BaseSwitchableLabel.switch_off.
Apply the inactive state colors."""
super().switch_off() super().switch_off()
self.__apply_colors(*self.switch_off_colors) self.__apply_colors(*self.switch_off_colors)
def __apply_colors(self, start, end): def __apply_colors(self, start, end):
"""Set text and background color of the label."""
self.setStyleSheet( self.setStyleSheet(
f""" f"""
color:{self.text_color}; color:{self.text_color};
@@ -40,25 +55,33 @@ class SwitchableLabel(_BaseSwitchableLabel):
class SingleColorSwitchableLabel(_BaseSwitchableLabel): class SingleColorSwitchableLabel(_BaseSwitchableLabel):
"""Subclass _BaseSwitchableLabel."""
THRESHOLD = 30
def __init__(self, parent=None): def __init__(self, parent=None):
"""Set default active color."""
super().__init__(parent) super().__init__(parent)
self.active_color = ForecastColors.WARNING_COLOR self.active_color = ForecastColors.WARNING_COLOR
def switch_on(self): def switch_on(self):
if self.level >= 30: """Extend _BaseSwitchableLabel.switch_on.
Apply the active state color if level >= THRESHOLD."""
if self.level >= self.THRESHOLD:
super().switch_on() super().switch_on()
self.setStyleSheet(f"color: {self.active_color}" self.setStyleSheet(f"color: {self.active_color}")
# f"""background-color: {self.active_color};
# color: #000000;"""
)
def switch_off(self): def switch_off(self):
"""Extend _BaseSwitchableLabel.switch_off.
Apply an empty stylesheet."""
super().switch_off() super().switch_off()
# self.setStyleSheet("""background-color: transparent;""")
self.setStyleSheet("") self.setStyleSheet("")
class MultiColorSwitchableLabel(_BaseSwitchableLabel): class MultiColorSwitchableLabel(_BaseSwitchableLabel):
"""Subclass _BaseSwitchableLabel."""
LEVEL_COLORS = { LEVEL_COLORS = {
9: ForecastColors.KP9_COLOR, 9: ForecastColors.KP9_COLOR,
@@ -68,11 +91,18 @@ class MultiColorSwitchableLabel(_BaseSwitchableLabel):
5: ForecastColors.KP5_COLOR 5: ForecastColors.KP5_COLOR
} }
MIN_LEVEL = list(LEVEL_COLORS.keys())[-1]
MAX_LEVEL = list(LEVEL_COLORS.keys())[0]
def __init__(self, parent=None): def __init__(self, parent=None):
"""Initialize the instance."""
super().__init__(parent) super().__init__(parent)
def switch_on(self): def switch_on(self):
if 5 <= self.level <= 9: """Extend _BaseSwitchableLabel.switch_on.
Apply the active state color based on LEVEL_COLORS."""
if self.MIN_LEVEL <= self.level <= self.MAX_LEVEL:
super().switch_on() super().switch_on()
self.setStyleSheet( self.setStyleSheet(
f"""color: {self.LEVEL_COLORS[self.level]}; f"""color: {self.LEVEL_COLORS[self.level]};
@@ -80,20 +110,27 @@ class MultiColorSwitchableLabel(_BaseSwitchableLabel):
) )
def switch_off(self): def switch_off(self):
"""Extend _BaseSwitchableLabel.switch_off.
Apply an empty stylesheet."""
super().switch_off() super().switch_off()
# self.setStyleSheet("background-color: transparent;")
self.setStyleSheet("") self.setStyleSheet("")
class SwitchableLabelsIterable: class SwitchableLabelsIterable:
"""Iterable class of _BaseSwitchableLabel."""
def __init__(self, *labels): def __init__(self, *labels):
"""Set the labels to iterate through."""
self.labels = labels self.labels = labels
def __iter__(self): def __iter__(self):
"""Define the iterator."""
for lab in self.labels: for lab in self.labels:
yield lab yield lab
def switch_on(self, label): def switch_on(self, label):
"""Switch on the label 'label. Switch off all the other labels."""
for lab in self.labels: for lab in self.labels:
if lab is label: if lab is label:
lab.switch_on() lab.switch_on()
@@ -101,14 +138,19 @@ class SwitchableLabelsIterable:
lab.switch_off() lab.switch_off()
def switch_off_all(self): def switch_off_all(self):
"""Switch off all the labels."""
for lab in self.labels: for lab in self.labels:
lab.switch_off() lab.switch_off()
def set(self, attr, value): def set(self, attr, value):
"""Set the attribute 'attr' equal to 'value' for all the labels."""
for lab in self.labels: for lab in self.labels:
setattr(lab, attr, value) setattr(lab, attr, value)
def refresh(self): def refresh(self):
"""Refresh the state of all the labels.
Used after theme has changed."""
for lab in self.labels: for lab in self.labels:
if lab.is_on: if lab.is_on:
lab.switch_on() lab.switch_on()

View File

@@ -11,6 +11,8 @@ from utilities import pop_up
class ThemeConstants: class ThemeConstants:
"""Container class for all the relevant theme-related constants."""
FOLDER = "themes" FOLDER = "themes"
EXTENSION = ".qss" EXTENSION = ".qss"
ICONS_FOLDER = "icons" ICONS_FOLDER = "icons"
@@ -27,9 +29,18 @@ class ThemeConstants:
MISSING_THEME = "Missing theme in '" + FOLDER + "' folder." MISSING_THEME = "Missing theme in '" + FOLDER + "' folder."
MISSING_THEME_FOLDER = "'" + FOLDER + "'" + " folder not found.\nOnly the basic theme is available." MISSING_THEME_FOLDER = "'" + FOLDER + "'" + " folder not found.\nOnly the basic theme is available."
THEME_FOLDER_NOT_FOUND = "'" + FOLDER + "'" + " folder not found" THEME_FOLDER_NOT_FOUND = "'" + FOLDER + "'" + " folder not found"
DEFAULT_ICONS_PATH = os.path.join(FOLDER, DEFAULT, ICONS_FOLDER)
DEFAULT_SEARCH_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.SEARCH_LABEL_IMG)
DEFAULT_VOLUME_LABEL_PATH = os.path.join(DEFAULT_ICONS_PATH, Constants.VOLUME_LABEL_IMG)
CURRENT_THEME_FILE = os.path.join(FOLDER, CURRENT)
DEFAULT_THEME_PATH = os.path.join(FOLDER, DEFAULT)
class ThemeManager: class ThemeManager:
"""Manage all the operations releted the the themes."""
def __init__(self, parent): def __init__(self, parent):
"""Initialize the ThemeManager instance."""
self.__parent = parent self.__parent = parent
self.__parent.active_color = ThemeConstants.DEFAULT_ACTIVE_COLOR self.__parent.active_color = ThemeConstants.DEFAULT_ACTIVE_COLOR
self.__parent.inactive_color = ThemeConstants.DEFAULT_INACTIVE_COLOR self.__parent.inactive_color = ThemeConstants.DEFAULT_INACTIVE_COLOR
@@ -37,12 +48,6 @@ class ThemeManager:
self.__theme_path = "" self.__theme_path = ""
self.__current_theme = "" self.__current_theme = ""
self.__parent.default_images_folder = os.path.join(
ThemeConstants.FOLDER,
ThemeConstants.DEFAULT,
ThemeConstants.ICONS_FOLDER
)
self.__space_weather_labels = SwitchableLabelsIterable( self.__space_weather_labels = SwitchableLabelsIterable(
*list( *list(
chain( chain(
@@ -66,9 +71,9 @@ class ThemeManager:
) )
self.__theme_names = {} self.__theme_names = {}
self.__detect_themes()
def __refresh_range_labels(self): def __refresh_range_labels(self):
"""Refresh the range-labels."""
self.__parent.set_acf_interval_label() self.__parent.set_acf_interval_label()
self.__parent.set_band_filter_label( self.__parent.set_band_filter_label(
self.__parent.activate_low_band_filter_btn, self.__parent.activate_low_band_filter_btn,
@@ -96,12 +101,16 @@ class ThemeManager:
@pyqtSlot() @pyqtSlot()
def __apply(self, theme_path): def __apply(self, theme_path):
"""Apply the selected theme.
Refresh all relevant widgets.
Display a QMessageBox if the theme is not found."""
self.__theme_path = theme_path self.__theme_path = theme_path
if os.path.exists(theme_path): if os.path.exists(theme_path):
if self.__theme_path != self.__current_theme: if self.__theme_path != self.__current_theme:
self.__change() self.__change()
self.__parent.display_specs( self.__parent.display_specs(
item=self.__parent.result_list.currentItem(), item=self.__parent.signals_list.currentItem(),
previous_item=None previous_item=None
) )
self.__refresh_range_labels() self.__refresh_range_labels()
@@ -115,6 +124,7 @@ class ThemeManager:
text=ThemeConstants.MISSING_THEME).show() text=ThemeConstants.MISSING_THEME).show()
def __pretty_name(self, bad_name): def __pretty_name(self, bad_name):
"""Return a well-formatted theme name."""
return ' '.join( return ' '.join(
map(lambda s: s.capitalize(), map(lambda s: s.capitalize(),
bad_name.split('_') bad_name.split('_')
@@ -122,6 +132,10 @@ class ThemeManager:
) )
def __detect_themes(self): def __detect_themes(self):
"""Detect all available themes.
Connect all the actions to change the theme.
Display a QMessageBox if the theme folder is not found."""
themes = [] themes = []
ag = QActionGroup(self.__parent, exclusive=True) ag = QActionGroup(self.__parent, exclusive=True)
if os.path.exists(ThemeConstants.FOLDER): if os.path.exists(ThemeConstants.FOLDER):
@@ -146,6 +160,7 @@ class ThemeManager:
text=ThemeConstants.MISSING_THEME_FOLDER).show() text=ThemeConstants.MISSING_THEME_FOLDER).show()
def __is_valid_html_color(self, colors): def __is_valid_html_color(self, colors):
"""Return if a string or a list of strings has a valid html format."""
pattern = "#([a-zA-Z0-9]){6}" pattern = "#([a-zA-Z0-9]){6}"
match_ok = lambda col: bool(re.match(pattern, col)) match_ok = lambda col: bool(re.match(pattern, col))
if isinstance(colors, list): if isinstance(colors, list):
@@ -157,6 +172,11 @@ class ThemeManager:
return match_ok(colors) return match_ok(colors)
def __change(self): def __change(self):
"""Change the current theme.
Apply the stylesheet and set active and inactive colors.
Set all the new images needed.
Save the new current theme on file."""
theme_name = os.path.basename(self.__theme_path) + ThemeConstants.EXTENSION theme_name = os.path.basename(self.__theme_path) + ThemeConstants.EXTENSION
try: try:
with open( with open(
@@ -170,46 +190,25 @@ class ThemeManager:
text=ThemeConstants.MISSING_THEME).show() text=ThemeConstants.MISSING_THEME).show()
else: else:
icons_path = os.path.join(self.__theme_path, ThemeConstants.ICONS_FOLDER) icons_path = os.path.join(self.__theme_path, ThemeConstants.ICONS_FOLDER)
default_icons_path = os.path.join(
ThemeConstants.FOLDER,
ThemeConstants.DEFAULT,
ThemeConstants.ICONS_FOLDER
)
if os.path.exists(os.path.join(icons_path, Constants.NOT_SELECTED)) and \
os.path.exists(os.path.join(icons_path, Constants.NOT_AVAILABLE)):
self.__parent.default_images_folder = icons_path
else:
self.__parent.default_images_folder = default_icons_path
path_to_search_label = os.path.join( path_to_search_label = os.path.join(
icons_path, icons_path,
Constants.SEARCH_LABEL_IMG Constants.SEARCH_LABEL_IMG
) )
default_search_label = os.path.join(
default_icons_path,
Constants.SEARCH_LABEL_IMG
)
if os.path.exists(path_to_search_label): if os.path.exists(path_to_search_label):
self.__parent.search_label.setPixmap( path = path_to_search_label
QPixmap(path_to_search_label)
)
self.__parent.modulation_search_label.setPixmap(
QPixmap(path_to_search_label)
)
self.__parent.location_search_label.setPixmap(
QPixmap(path_to_search_label)
)
else: else:
path = ThemeConstants.DEFAULT_SEARCH_LABEL_PATH
self.__parent.search_label.setPixmap( self.__parent.search_label.setPixmap(
QPixmap(default_search_label) QPixmap(path)
) )
self.__parent.modulation_search_label.setPixmap( self.__parent.modulation_search_label.setPixmap(
QPixmap(default_search_label) QPixmap(path)
) )
self.__parent.location_search_label.setPixmap( self.__parent.location_search_label.setPixmap(
QPixmap(default_search_label) QPixmap(path)
) )
self.__parent.search_label.setScaledContents(True) self.__parent.search_label.setScaledContents(True)
@@ -220,18 +219,14 @@ class ThemeManager:
icons_path, icons_path,
Constants.VOLUME_LABEL_IMG Constants.VOLUME_LABEL_IMG
) )
default_volume_label = os.path.join(
default_icons_path,
Constants.VOLUME_LABEL_IMG
)
if os.path.exists(path_to_volume_label): if os.path.exists(path_to_volume_label):
self.__parent.volume_label.setPixmap( path = path_to_volume_label
QPixmap(path_to_volume_label)
)
else: else:
path = ThemeConstants.DEFAULT_VOLUME_LABEL_PATH
self.__parent.volume_label.setPixmap( self.__parent.volume_label.setPixmap(
QPixmap(default_volume_label) QPixmap(path)
) )
self.__parent.volume_label.setScaledContents(True) self.__parent.volume_label.setScaledContents(True)
@@ -307,21 +302,16 @@ class ThemeManager:
self.__current_theme = self.__theme_path self.__current_theme = self.__theme_path
try: try:
with open(os.path.join( with open(ThemeConstants.CURRENT_THEME_FILE, "w") as current_theme:
ThemeConstants.FOLDER,
ThemeConstants.CURRENT
), "w") as current_theme:
current_theme.write(self.__theme_path) current_theme.write(self.__theme_path)
except Exception: except Exception:
pass pass
def start(self): def start(self):
current_theme_file = os.path.join( """Start the theme manager."""
ThemeConstants.FOLDER, self.__detect_themes()
ThemeConstants.CURRENT if os.path.exists(ThemeConstants.CURRENT_THEME_FILE):
) with open(ThemeConstants.CURRENT_THEME_FILE, "r") as current_theme_path:
if os.path.exists(current_theme_file):
with open(current_theme_file, "r") as current_theme_path:
theme_path = current_theme_path.read() theme_path = current_theme_path.read()
theme_name = self.__pretty_name(os.path.basename(theme_path)) theme_name = self.__pretty_name(os.path.basename(theme_path))
try: try:
@@ -340,9 +330,4 @@ class ThemeManager:
pop_up(self.__parent, title=ThemeConstants.THEME_NOT_FOUND, pop_up(self.__parent, title=ThemeConstants.THEME_NOT_FOUND,
text=ThemeConstants.MISSING_THEME).show() text=ThemeConstants.MISSING_THEME).show()
else: else:
self.__apply( self.__apply(ThemeConstants.DEFAULT_THEME_PATH)
os.path.join(
ThemeConstants.FOLDER,
ThemeConstants.DEFAULT
)
)

View File

@@ -14,6 +14,8 @@ from utilities import checksum_ok
class ThreadStatus(Enum): class ThreadStatus(Enum):
"""Possible thread status."""
OK = auto() OK = auto()
NO_CONNECTION_ERR = auto() NO_CONNECTION_ERR = auto()
UNKNOWN_ERR = auto() UNKNOWN_ERR = auto()
@@ -22,24 +24,31 @@ class ThreadStatus(Enum):
class BaseDownloadThread(QThread): class BaseDownloadThread(QThread):
"""Subclass QThread. Base class for the download threads."""
def __init__(self, parent=None): def __init__(self, parent=None):
"""Set the status as 'UNDEFINED'."""
super().__init__(parent) super().__init__(parent)
self.status = ThreadStatus.UNDEFINED self.status = ThreadStatus.UNDEFINED
def __del__(self): def __del__(self):
"""Force the termination of the thread."""
self.terminate() self.terminate()
self.wait() self.wait()
class DownloadThread(BaseDownloadThread): class DownloadThread(BaseDownloadThread):
"""Subclass BaseDownloadThread. Download the database, images and audio samples."""
progress = pyqtSignal(int, float) progress = pyqtSignal(int, float)
CHUNK = 1024**2 CHUNK = 1024**2
def __init__(self): def __init__(self):
"""Just call super().__init__."""
super().__init__() super().__init__()
def __pretty_len(self, byte_obj): def __pretty_len(self, byte_obj):
"""Return a well-formatted number of downloaded MB."""
mega = len(byte_obj) / self.CHUNK mega = len(byte_obj) / self.CHUNK
if mega.is_integer(): if mega.is_integer():
return int(mega) return int(mega)
@@ -47,12 +56,17 @@ class DownloadThread(BaseDownloadThread):
return ceil(mega) return ceil(mega)
def __get_download_speed(self, data, delta): def __get_download_speed(self, data, delta):
"""Return the download speed in MB/s."""
return round( return round(
(len(data) / self.CHUNK) / delta, (len(data) / self.CHUNK) / delta,
2 2
) )
def run(self): def run(self):
"""QThread.run. Download the database, images and audio samples.
Handle all possible exceptions. Also extract the files
in the local folder."""
self.status = ThreadStatus.UNDEFINED self.status = ThreadStatus.UNDEFINED
raw_data = bytes(0) raw_data = bytes(0)
try: try:
@@ -102,29 +116,39 @@ class DownloadThread(BaseDownloadThread):
class _AsyncDownloader: class _AsyncDownloader:
"""Mixin class for asynchronous threads."""
async def _download_resource(self, session, link): async def _download_resource(self, session, link):
"""Return the content of 'link' as bytes."""
resp = await session.get(link) resp = await session.get(link)
return await resp.read() return await resp.read()
class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader): class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
"""Subclass BaseDownloadThread. Downlaod the space weather data."""
__properties = ("xray", "prot_el", "ak_index", "sgas", "geo_storm") __properties = ("xray", "prot_el", "ak_index", "sgas", "geo_storm")
def __init__(self, space_weather_data): def __init__(self, space_weather_data):
"""Initialize the a local space_weather_data."""
super().__init__() super().__init__()
self.__space_weather_data = space_weather_data self.__space_weather_data = space_weather_data
async def __download_property(self, session, property_name): async def __download_property(self, session, property_name):
"""Download the data conteining the information of a specific property."""
link = getattr(Constants, "SPACE_WEATHER_" + property_name.upper()) link = getattr(Constants, "SPACE_WEATHER_" + property_name.upper())
data = await self._download_resource(session, link) data = await self._download_resource(session, link)
setattr(self.__space_weather_data, property_name, str(data, 'utf-8')) setattr(self.__space_weather_data, property_name, str(data, 'utf-8'))
async def __download_image(self, session, n): async def __download_image(self, session, n):
im = await self._download_resource(session, Constants.SPACE_WEATHER_IMGS[n]) """Download the data corresponding the n-th image displayed in the screen."""
im = await self._download_resource(
session, Constants.SPACE_WEATHER_IMGS[n]
)
self.__space_weather_data.images[n].loadFromData(im) self.__space_weather_data.images[n].loadFromData(im)
async def _download_resources(self): async def _download_resources(self):
"""Download all the data."""
session = aiohttp.ClientSession() session = aiohttp.ClientSession()
try: try:
t = [] t = []
@@ -137,7 +161,9 @@ class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
t1 = [] t1 = []
for im_number in tot_images: for im_number in tot_images:
t1.append( t1.append(
asyncio.create_task(self.__download_image(session, im_number)) asyncio.create_task(
self.__download_image(session, im_number)
)
) )
await asyncio.gather(*t, *t1) await asyncio.gather(*t, *t1)
except Exception: except Exception:
@@ -148,29 +174,35 @@ class UpdateSpaceWeatherThread(BaseDownloadThread, _AsyncDownloader):
await session.close() await session.close()
def run(self): def run(self):
"""Override QThread.run. Start the download of the data."""
self.status = ThreadStatus.UNDEFINED self.status = ThreadStatus.UNDEFINED
asyncio.run(self._download_resources()) asyncio.run(self._download_resources())
class UpdateForecastThread(BaseDownloadThread, _AsyncDownloader): class UpdateForecastThread(BaseDownloadThread, _AsyncDownloader):
"""Subclass BaseDownloadThread. Download the forecast data."""
class _PropertyName(Enum): class _PropertyName(Enum):
"""Enum used to differentiate between the two data needed."""
FORECAST = auto() FORECAST = auto()
PROBABILITIES = auto() PROBABILITIES = auto()
def __init__(self, parent): def __init__(self, owner):
"""Set the owner object (a ForecastData instance)."""
super().__init__() super().__init__()
self.parent = parent self.owner = owner
async def __download_property(self, session, link, prop_name): async def __download_property(self, session, link, prop_name):
"""Download the data from 'link' and set the corresponding property of the owner."""
resp = await self._download_resource(session, link) resp = await self._download_resource(session, link)
resp = str(resp, 'utf-8') resp = str(resp, 'utf-8')
if prop_name is self._PropertyName.FORECAST: if prop_name is self._PropertyName.FORECAST:
self.parent.forecast = resp self.owner.forecast = resp
if prop_name is self._PropertyName.PROBABILITIES: if prop_name is self._PropertyName.PROBABILITIES:
self.parent.probabilities = resp self.owner.probabilities = resp
async def _download_resources(self): async def _download_resources(self):
"""Download all the data needed."""
session = aiohttp.ClientSession() session = aiohttp.ClientSession()
try: try:
await asyncio.gather( await asyncio.gather(
@@ -197,5 +229,6 @@ class UpdateForecastThread(BaseDownloadThread, _AsyncDownloader):
await session.close() await session.close()
def run(self): def run(self):
"""Override QThread.run. Start the data download."""
self.status = ThreadStatus.UNDEFINED self.status = ThreadStatus.UNDEFINED
asyncio.run(self._download_resources()) asyncio.run(self._download_resources())

View File

@@ -9,6 +9,7 @@ from PyQt5.QtWidgets import QMessageBox
from constants import Constants, Signal, Database, ChecksumWhat from constants import Constants, Signal, Database, ChecksumWhat
def resource_path(relative_path): def resource_path(relative_path):
"""Get absolute path to resource, works for dev and for PyInstaller."""
try: try:
base_path = sys._MEIPASS base_path = sys._MEIPASS
except Exception: except Exception:
@@ -16,6 +17,7 @@ def resource_path(relative_path):
return os.path.join(base_path, relative_path) return os.path.join(base_path, relative_path)
def uncheck_and_emit(button): def uncheck_and_emit(button):
"""Set the button to the unchecked state and emit the clicked signal."""
if button.isChecked(): if button.isChecked():
button.setChecked(False) button.setChecked(False)
button.clicked.emit() button.clicked.emit()
@@ -25,6 +27,13 @@ def pop_up(cls, title, text,
connection=None, connection=None,
is_question=False, is_question=False,
default_btn=QMessageBox.Yes): default_btn=QMessageBox.Yes):
"""Return a QMessageBox object.
Keyword arguments:
informative_text -- possible informative text to be displayed.
connection -- a callable to connect the message when emitting the finished signal.
is_question -- whether the message contains a question.
default_btn -- the default button for the possible answer to the question."""
msg = QMessageBox(cls) msg = QMessageBox(cls)
msg.setWindowTitle(title) msg.setWindowTitle(title)
msg.setText(text) msg.setText(text)
@@ -39,6 +48,7 @@ def pop_up(cls, title, text,
return msg return msg
def checksum_ok(data, what): def checksum_ok(data, what):
"""Check whether the checksum of the 'data' argument is correct."""
code = hashlib.sha256() code = hashlib.sha256()
code.update(data) code.update(data)
if what is ChecksumWhat.FOLDER: if what is ChecksumWhat.FOLDER:
@@ -57,6 +67,9 @@ def checksum_ok(data, what):
return code.hexdigest() == reference return code.hexdigest() == reference
def connect_events_to_func(events_to_connect, fun_to_connect, fun_args): def connect_events_to_func(events_to_connect, fun_to_connect, fun_args):
"""Connect all elements of events_to_connect to the callable fun_to_connect.
fun_args is a list of fun_to_connect arguments."""
if fun_args is not None: if fun_args is not None:
for event in events_to_connect: for event in events_to_connect:
event.connect(partial(fun_to_connect, *fun_args)) event.connect(partial(fun_to_connect, *fun_args))
@@ -65,21 +78,25 @@ def connect_events_to_func(events_to_connect, fun_to_connect, fun_args):
event.connect(fun_to_connect) event.connect(fun_to_connect)
def filters_limit(spinbox, filter_unit, confidence, sign=1): def filters_limit(spinbox, filter_unit, confidence, sign=1):
"""Return the actual limit of a numerical filter."""
band_filter = spinbox.value() * Constants.CONVERSION_FACTORS[filter_unit.currentText()] band_filter = spinbox.value() * Constants.CONVERSION_FACTORS[filter_unit.currentText()]
return band_filter + sign * (confidence.value() * band_filter) // 100 return band_filter + sign * (confidence.value() * band_filter) // 100
def is_undef_freq(current_signal): def is_undef_freq(current_signal):
"""Return whether the lower or upper frequency of a signal is undefined."""
lower_freq = current_signal.at[Signal.INF_FREQ] lower_freq = current_signal.at[Signal.INF_FREQ]
upper_freq = current_signal.at[Signal.SUP_FREQ] upper_freq = current_signal.at[Signal.SUP_FREQ]
return lower_freq == Constants.UNKNOWN or upper_freq == Constants.UNKNOWN return lower_freq == Constants.UNKNOWN or upper_freq == Constants.UNKNOWN
def is_undef_band(current_signal): def is_undef_band(current_signal):
"""Return whether the lower or upper band of a signal is undefined."""
lower_band = current_signal.at[Signal.INF_BAND] lower_band = current_signal.at[Signal.INF_BAND]
upper_band = current_signal.at[Signal.SUP_BAND] upper_band = current_signal.at[Signal.SUP_BAND]
return lower_band == Constants.UNKNOWN or upper_band == Constants.UNKNOWN return lower_band == Constants.UNKNOWN or upper_band == Constants.UNKNOWN
def _change_unit(num): def _change_unit(str_num):
digits = len(num) """Return a scale factor givent the number of digits of a numeric string."""
digits = len(str_num)
if digits < 4: if digits < 4:
return 1 return 1
elif digits < 7: elif digits < 7:
@@ -90,13 +107,14 @@ def _change_unit(num):
return 10**9 return 10**9
def format_numbers(lower, upper): def format_numbers(lower, upper):
"""Return the string which displays the numeric limits of a filter."""
units = {1: 'Hz', 1000: 'kHz', 10**6: 'MHz', 10**9: 'GHz'} units = {1: 'Hz', 1000: 'kHz', 10**6: 'MHz', 10**9: 'GHz'}
lower_factor = _change_unit(lower) lower_factor = _change_unit(lower)
upper_factor = _change_unit(upper) upper_factor = _change_unit(upper)
pre_lower = lower pre_lower = lower
pre_upper = upper pre_upper = upper
lower = int(lower) / lower_factor lower = safe_cast(lower, int) / lower_factor
upper = int(upper) / upper_factor upper = safe_cast(upper, int) / upper_factor
if lower.is_integer(): if lower.is_integer():
lower = int(lower) lower = int(lower)
else: else:
@@ -109,3 +127,15 @@ def format_numbers(lower, upper):
return f"{lower:,} {units[lower_factor]} - {upper:,} {units[upper_factor]}" return f"{lower:,} {units[lower_factor]} - {upper:,} {units[upper_factor]}"
else: else:
return f"{lower:,} {units[lower_factor]}" return f"{lower:,} {units[lower_factor]}"
def safe_cast(value, cast_type):
"""Calls 'cast_type(value)' and returns the result.
If the operation fails returns -1. Should be used to perform 'safe casts'.
"""
try:
r = cast_type(value)
except Exception:
r = -1
finally:
return r

View File

@@ -7,27 +7,39 @@ from threads import (BaseDownloadThread,
UpdateForecastThread) UpdateForecastThread)
from constants import Constants from constants import Constants
from switchable_label import MultiColorSwitchableLabel from switchable_label import MultiColorSwitchableLabel
from utilities import safe_cast
class _BaseWeatherData(QObject): class _BaseWeatherData(QObject):
"""Base class for the weather data. Extends QObject."""
update_complete = pyqtSignal(bool) update_complete = pyqtSignal(bool)
def __init__(self): def __init__(self):
"""Create a BaseDownloadThread object."""
super().__init__() super().__init__()
self._update_thread = BaseDownloadThread() self._update_thread = BaseDownloadThread()
@property @property
def is_updating(self): def is_updating(self):
"""Return whether the thread is running."""
return self._update_thread.isRunning() return self._update_thread.isRunning()
def update(self): def update(self):
"""Start the thread."""
self._update_thread.start() self._update_thread.start()
def _parse_data(self): def _parse_data(self):
"""Dummy function. Must be overrided by subclasses."""
pass pass
@pyqtSlot() @pyqtSlot()
def _parse_and_emit_signal(self): def _parse_and_emit_signal(self):
"""Parse the data and emit an 'update_complete' signal.
If the download was not successful, do not parse the data.
The 'update_complete' signal propagates the thread status up to the
calling slot."""
status_ok = False status_ok = False
if self._update_thread.status is ThreadStatus.OK: if self._update_thread.status is ThreadStatus.OK:
status_ok = True status_ok = True
@@ -35,15 +47,22 @@ class _BaseWeatherData(QObject):
self.update_complete.emit(status_ok) self.update_complete.emit(status_ok)
def _double_split(self, string): def _double_split(self, string):
"""Given a string, return a list of lists.
First split on each line. Then split each line on whitespaces."""
return [i.split() for i in string.splitlines()] return [i.split() for i in string.splitlines()]
def shutdown_thread(self): def shutdown_thread(self):
"""Terminate the download thread."""
self._update_thread.terminate() self._update_thread.terminate()
self._update_thread.wait() self._update_thread.wait()
class SpaceWeatherData(_BaseWeatherData): class SpaceWeatherData(_BaseWeatherData):
"""Space weather class. Extends _BaseWeatherData."""
def __init__(self): def __init__(self):
"""Set all attributes and connect the thread to _parse_and_emit_signal."""
super().__init__() super().__init__()
self.xray = '' self.xray = ''
self.prot_el = '' self.prot_el = ''
@@ -65,6 +84,9 @@ class SpaceWeatherData(_BaseWeatherData):
self._update_thread.finished.connect(self._parse_and_emit_signal) self._update_thread.finished.connect(self._parse_and_emit_signal)
def _parse_data(self): def _parse_data(self):
"""Override _BaseWeatherData._parse_data.
Set all the data."""
self.xray = self._double_split(self.xray) self.xray = self._double_split(self.xray)
self.prot_el = self._double_split(self.prot_el) self.prot_el = self._double_split(self.prot_el)
self.ak_index = self._double_split(self.ak_index) self.ak_index = self._double_split(self.ak_index)
@@ -72,6 +94,7 @@ class SpaceWeatherData(_BaseWeatherData):
self.geo_storm = self._double_split(self.geo_storm) self.geo_storm = self._double_split(self.geo_storm)
def remove_data(self): def remove_data(self):
"""Remove the reference to all the data."""
self.xray = '' self.xray = ''
self.prot_el = '' self.prot_el = ''
self.ak_index = '' self.ak_index = ''
@@ -91,6 +114,7 @@ class SpaceWeatherData(_BaseWeatherData):
class ForecastData(_BaseWeatherData): class ForecastData(_BaseWeatherData):
"""3-day forecast class. Extends _BaseWeatherData."""
ROW_KEYWORDS = { ROW_KEYWORDS = {
"solar_row": "S1 or greater", "solar_row": "S1 or greater",
@@ -101,6 +125,7 @@ class ForecastData(_BaseWeatherData):
} }
def __init__(self, parent): def __init__(self, parent):
"""Initialize all attributes and connect the thread to _parse_and_emit_signal."""
super().__init__() super().__init__()
self.forecast = '' self.forecast = ''
self.probabilities = '' self.probabilities = ''
@@ -112,16 +137,17 @@ class ForecastData(_BaseWeatherData):
self.__kp_index_row = None self.__kp_index_row = None
self._update_thread = UpdateForecastThread(self) self._update_thread = UpdateForecastThread(self)
self._update_thread.finished.connect(self._parse_and_emit_signal) self._update_thread.finished.connect(self._parse_and_emit_signal)
self.today_lbl = parent.today_lbl # Cannot use '__' here because of the for loop below.
self.today_p1_lbl = parent.today_p1_lbl self._today_lbl = parent.today_lbl
self.today_p2_lbl = parent.today_p2_lbl self._today_p1_lbl = parent.today_p1_lbl
self._today_p2_lbl = parent.today_p2_lbl
self.__today_lbls = [] self.__today_lbls = []
self.__today_p1_lbls = [] self.__today_p1_lbls = []
self.__today_p2_lbls = [] self.__today_p2_lbls = []
self.__all_lbls = [] self.__all_lbls = []
flags = ['', 'p1_', 'p2_'] flags = ['', 'p1_', 'p2_']
for flag in flags: for flag in flags:
title_lbl = getattr(self, "today_" + flag + "lbl") title_lbl = getattr(self, "_today_" + flag + "lbl")
title_lbl.setText("-") title_lbl.setText("-")
for index in range(20): for index in range(20):
label = getattr( label = getattr(
@@ -143,6 +169,9 @@ class ForecastData(_BaseWeatherData):
] ]
def _parse_data(self): def _parse_data(self):
"""Override _BaseWeatherData._parse_data.
Set all the relevant data."""
# Remove possible '(G\d)' from the kp_index table # Remove possible '(G\d)' from the kp_index table
self.forecast = re.sub( self.forecast = re.sub(
'\(G\d\)', lambda obj: '', self.forecast '\(G\d\)', lambda obj: '', self.forecast
@@ -154,16 +183,21 @@ class ForecastData(_BaseWeatherData):
self.probabilities = self.probabilities.splitlines() self.probabilities = self.probabilities.splitlines()
def __split_lists(self): def __split_lists(self):
"""Split the elements of forecast and probabilities."""
self.forecast = [i.split() for i in self.forecast] self.forecast = [i.split() for i in self.forecast]
self.probabilities = [i.split() for i in self.probabilities] self.probabilities = [i.split() for i in self.probabilities]
def __find_row_with(self, data, text): def __find_row_with(self, data, text):
"""Given a list of strings, return the index of the first string containing the target text."""
for i, row in enumerate(data): for i, row in enumerate(data):
if text in row: if text in row:
return i return i
return None return None
def __get_rows(self): def __get_rows(self):
"""Set all the rows needed for updating the screen.
Raise an exception if something goes wrong."""
self.__solar_row = self.__find_row_with( self.__solar_row = self.__find_row_with(
self.forecast, self.forecast,
self.ROW_KEYWORDS["solar_row"] self.ROW_KEYWORDS["solar_row"]
@@ -185,26 +219,27 @@ class ForecastData(_BaseWeatherData):
self.ROW_KEYWORDS["kp_index_row"] self.ROW_KEYWORDS["kp_index_row"]
) )
is_none = lambda x: x is None
if any([ if any([
is_none(self.__solar_row), self.__solar_row is None,
is_none(self.__event_row), self.__event_row is None,
is_none(self.__rb_now_row), self.__rb_now_row is None,
is_none(self.__ga_now_row), self.__ga_now_row is None,
is_none(self.__kp_index_row) self.__kp_index_row is None
]): ]):
raise Exception('Missing Rows') raise Exception('Missing Rows')
def __set_dates(self): def __set_dates(self):
"""Set the date labels."""
month = self.forecast[self.__solar_row - 1][0] month = self.forecast[self.__solar_row - 1][0]
today = self.forecast[self.__solar_row - 1][1] today = self.forecast[self.__solar_row - 1][1]
today_p1 = self.forecast[self.__solar_row - 1][3] today_p1 = self.forecast[self.__solar_row - 1][3]
today_p2 = self.forecast[self.__solar_row - 1][5] today_p2 = self.forecast[self.__solar_row - 1][5]
self.today_lbl.setText(month + ' ' + today) self._today_lbl.setText(month + ' ' + today)
self.today_p1_lbl.setText(month + ' ' + today_p1) self._today_p1_lbl.setText(month + ' ' + today_p1)
self.today_p2_lbl.setText(month + ' ' + today_p2) self._today_p2_lbl.setText(month + ' ' + today_p2)
def __make_labels_table(self): def __make_labels_table(self):
"""Organize all the arguments to feed __get_lbl_value."""
get_first_split = lambda x: x.split("/")[0] get_first_split = lambda x: x.split("/")[0]
get_second_split = lambda x: x.split("/")[1] get_second_split = lambda x: x.split("/")[1]
get_third_split = lambda x: x.split("/")[2] get_third_split = lambda x: x.split("/")[2]
@@ -278,33 +313,31 @@ class ForecastData(_BaseWeatherData):
] ]
def __get_lbl_value(self, data, row, col, f=None): def __get_lbl_value(self, data, row, col, f=None):
"""Return the well-formatted string-value of the label."""
val = data[row][col] val = data[row][col]
if f is not None: if f is not None:
val = f(val) val = f(val)
val = val.lstrip('0').rstrip('%') val = val.rstrip('%')
if len(val) > 1:
val = val.lstrip('0')
return val return val
def __is_integer(self, s):
try:
int(s)
except Exception:
return False
else:
return True
def __set_labels_values(self): def __set_labels_values(self):
"""Set all the labels values."""
for lbl_list, table in zip(self.__all_lbls, self.__labels_table): for lbl_list, table in zip(self.__all_lbls, self.__labels_table):
for lbl, row in zip(lbl_list, table): for lbl, row in zip(lbl_list, table):
lbl.switch_off() lbl.switch_off()
value = self.__get_lbl_value(*row) value = self.__get_lbl_value(*row)
if self.__is_integer(value): lbl.level = safe_cast(value, int)
lbl.level = int(value)
if not isinstance(lbl, MultiColorSwitchableLabel): if not isinstance(lbl, MultiColorSwitchableLabel):
value += '%' value += '%'
lbl.setText(value) lbl.setText(value)
lbl.switch_on() lbl.switch_on()
def update_all_labels(self): def update_all_labels(self):
"""Update all the labels values.
If an exception is raised in the process, do nothing."""
try: try:
self.__get_rows() self.__get_rows()
self.__split_lists() self.__split_lists()
@@ -315,5 +348,6 @@ class ForecastData(_BaseWeatherData):
pass pass
def remove_data(self): def remove_data(self):
"""Remove the reference to the downloaded data."""
self.forecast = '' self.forecast = ''
self.probabilities = '' self.probabilities = ''