Deploy script for Windows
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,5 +9,5 @@ themes/.current_theme
|
|||||||
launch.bat
|
launch.bat
|
||||||
designer.bat
|
designer.bat
|
||||||
*.sh
|
*.sh
|
||||||
.vscode/
|
|
||||||
default_pics/
|
default_pics/
|
||||||
|
.vscode/
|
||||||
|
|||||||
292
artemis.py
292
artemis.py
@@ -16,10 +16,11 @@ from PyQt5.QtWidgets import (QMainWindow,
|
|||||||
QSplashScreen,
|
QSplashScreen,
|
||||||
QTreeWidgetItem,)
|
QTreeWidgetItem,)
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt5.QtGui import QPixmap
|
||||||
from PyQt5 import uic, QtGui
|
from PyQt5 import uic
|
||||||
from PyQt5.QtCore import (QFileInfo,
|
from PyQt5.QtCore import (QFileInfo,
|
||||||
Qt,
|
Qt,
|
||||||
pyqtSlot,)
|
pyqtSlot,
|
||||||
|
QRect,)
|
||||||
|
|
||||||
from audio_player import AudioPlayer
|
from audio_player import AudioPlayer
|
||||||
from weatherdata import SpaceWeatherData, ForecastData
|
from weatherdata import SpaceWeatherData, ForecastData
|
||||||
@@ -31,7 +32,8 @@ from constants import (Constants,
|
|||||||
Database,
|
Database,
|
||||||
ChecksumWhat,
|
ChecksumWhat,
|
||||||
Messages,
|
Messages,
|
||||||
Signal,)
|
Signal,
|
||||||
|
MainTabs,)
|
||||||
from themesmanager import ThemeManager
|
from themesmanager import ThemeManager
|
||||||
from utilities import (checksum_ok,
|
from utilities import (checksum_ok,
|
||||||
uncheck_and_emit,
|
uncheck_and_emit,
|
||||||
@@ -41,7 +43,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,12 +54,13 @@ 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()
|
||||||
self.setWindowIcon(QtGui.QIcon(":/icon/default_pics/Artemis3.500px.png"))
|
|
||||||
self.closing = False
|
self.closing = False
|
||||||
self.download_window = DownloadWindow()
|
self.download_window = DownloadWindow()
|
||||||
self.download_window.complete.connect(self.show_downloaded_signals)
|
self.download_window.complete.connect(self.show_downloaded_signals)
|
||||||
@@ -482,10 +486,9 @@ 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(self.set_visible_tab)
|
||||||
lambda: self.main_tab.setCurrentWidget(self.signal_properties_tab)
|
|
||||||
)
|
|
||||||
self.audio_widget = AudioPlayer(
|
self.audio_widget = AudioPlayer(
|
||||||
self.play,
|
self.play,
|
||||||
self.pause,
|
self.pause,
|
||||||
@@ -529,26 +532,56 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
self.update_forecast_bar.set_idle()
|
self.update_forecast_bar.set_idle()
|
||||||
self.forecast_data.update_complete.connect(self.update_forecast)
|
self.forecast_data.update_complete.connect(self.update_forecast)
|
||||||
|
|
||||||
|
self.main_tab.currentChanged.connect(self.hide_show_right_widget)
|
||||||
|
|
||||||
# Final operations.
|
# Final operations.
|
||||||
self.theme_manager.start()
|
self.theme_manager.start()
|
||||||
self.load_db()
|
self.load_db()
|
||||||
self.display_signals()
|
self.display_signals()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def hide_show_right_widget(self):
|
||||||
|
"""Hide or show the waterfall+audio widget based on the current tab."""
|
||||||
|
if self.main_tab.currentIndex() == MainTabs.FORECAST:
|
||||||
|
self.fixed_audio_and_image.setVisible(False)
|
||||||
|
elif not self.fixed_audio_and_image.isVisible():
|
||||||
|
self.fixed_audio_and_image.setVisible(True)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def set_visible_tab(self):
|
||||||
|
"""Set the current main tab when double-clicking a signal name."""
|
||||||
|
if self.main_tab.currentWidget() != self.signal_properties_tab:
|
||||||
|
self.main_tab.setCurrentWidget(self.signal_properties_tab)
|
||||||
|
else:
|
||||||
|
self.main_tab.setCurrentWidget(self.filter_tab)
|
||||||
|
|
||||||
@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()
|
||||||
@@ -559,10 +592,18 @@ 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}"
|
|
||||||
|
def format_text(letter, power):
|
||||||
|
return 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))
|
||||||
elif xray_long >= 1e-8 and xray_long < 1e-7:
|
elif xray_long >= 1e-8 and xray_long < 1e-7:
|
||||||
@@ -593,7 +634,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:
|
||||||
@@ -609,9 +650,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:
|
||||||
@@ -670,7 +715,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:
|
||||||
@@ -692,12 +739,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()
|
||||||
@@ -708,6 +761,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() \
|
||||||
@@ -723,23 +782,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, hide 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, hide 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 that 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():
|
||||||
@@ -748,6 +821,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)
|
||||||
@@ -757,6 +831,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:
|
||||||
@@ -766,14 +844,21 @@ 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()
|
||||||
|
center = d.center()
|
||||||
w = d.width()
|
w = d.width()
|
||||||
h = d.height()
|
h = d.height()
|
||||||
self.setGeometry(50, 50, (3 * w) // 4, (3 * h) // 4)
|
rect = QRect()
|
||||||
|
rect.setHeight((3 * h) // 4)
|
||||||
|
rect.setWidth((3 * w) // 4)
|
||||||
|
rect.moveCenter(center)
|
||||||
|
self.setGeometry(rect)
|
||||||
if w > 3000 or h > 2000:
|
if w > 3000 or h > 2000:
|
||||||
self.fixed_audio_and_image.setFixedSize(540, 1150)
|
self.fixed_audio_and_image.setFixedSize(540, 1150)
|
||||||
self.fixed_audio_and_image.setMaximumSize(540, 1150)
|
self.fixed_audio_and_image.setMaximumSize(540, 1150)
|
||||||
@@ -797,6 +882,10 @@ 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.locations_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 {
|
||||||
@@ -816,12 +905,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:
|
||||||
@@ -849,6 +949,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 a new database version is not available 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
|
||||||
@@ -882,19 +989,24 @@ 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):
|
||||||
names = Database.NAMES
|
"""Load the database from file.
|
||||||
|
|
||||||
|
Populate the signals list and set the total number of signals.
|
||||||
|
Handle possible missing file error.
|
||||||
|
"""
|
||||||
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),
|
||||||
sep=Database.DELIMITER,
|
sep=Database.DELIMITER,
|
||||||
header=None,
|
header=None,
|
||||||
index_col=0,
|
index_col=0,
|
||||||
dtype={name: str for name in Database.STRINGS},
|
dtype={name: str for name in Database.STRINGS},
|
||||||
names=names)
|
names=Database.NAMES)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
self.search_bar.setDisabled(True)
|
self.search_bar.setDisabled(True)
|
||||||
answer = pop_up(self, title=Messages.NO_DB,
|
answer = pop_up(self, title=Messages.NO_DB,
|
||||||
@@ -904,14 +1016,15 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
if answer == QMessageBox.Yes:
|
if answer == QMessageBox.Yes:
|
||||||
self.download_db()
|
self.download_db()
|
||||||
else:
|
else:
|
||||||
|
self.db = self.db.groupby(level=0).first()
|
||||||
self.signal_names = self.db.index
|
self.signal_names = self.db.index
|
||||||
self.total_signals = len(self.signal_names)
|
self.total_signals = len(self.signal_names)
|
||||||
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,7 +1036,13 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def collect_list(self, list_property, separator=';'):
|
def collect_list(self, list_property, separator=Constants.FIELD_SEPARATOR):
|
||||||
|
"""Collect all the entrys of a QListWidget.
|
||||||
|
|
||||||
|
Handle multiple entries in one item seprated by a separator.
|
||||||
|
Keyword argument:
|
||||||
|
separator -- the separator character for multiple-entries items.
|
||||||
|
"""
|
||||||
values = self.db[list_property]
|
values = self.db[list_property]
|
||||||
values = list(
|
values = list(
|
||||||
set([
|
set([
|
||||||
@@ -940,6 +1059,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'],
|
||||||
@@ -980,6 +1102,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
|
||||||
@@ -1021,6 +1146,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)
|
||||||
@@ -1033,12 +1159,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):
|
||||||
@@ -1050,15 +1181,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:
|
||||||
@@ -1069,6 +1201,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 on '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'")
|
||||||
|
|
||||||
@@ -1103,6 +1239,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():
|
||||||
@@ -1111,6 +1248,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 = []
|
||||||
@@ -1126,6 +1264,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(
|
||||||
@@ -1138,6 +1277,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(
|
||||||
@@ -1150,6 +1290,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)
|
||||||
@@ -1157,6 +1298,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 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])
|
||||||
@@ -1167,8 +1309,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
|
||||||
@@ -1196,6 +1338,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 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])
|
||||||
@@ -1206,8 +1349,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
|
||||||
@@ -1225,6 +1368,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 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]
|
||||||
@@ -1241,6 +1385,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 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,27 +1408,40 @@ class Artemis(QMainWindow, Ui_MainWindow):
|
|||||||
ok.append(item.text(0) == signal_mode)
|
ok.append(item.text(0) == signal_mode)
|
||||||
return any(ok)
|
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):
|
def modulation_filters_ok(self, signal_name):
|
||||||
|
"""Evalaute if the 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 = self.get_field_entries(
|
||||||
x.strip() for x in self.db.at[signal_name, Signal.MODULATION].split(',')
|
signal_name, Signal.MODULATION
|
||||||
]
|
)
|
||||||
for item in self.modulation_list.selectedItems():
|
for item in self.modulation_list.selectedItems():
|
||||||
if item.text() in signal_modulation:
|
if item.text() in signal_modulation:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def location_filters_ok(self, signal_name):
|
def location_filters_ok(self, signal_name):
|
||||||
|
"""Evalaute if the 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_locations = self.get_field_entries(
|
||||||
|
signal_name, Signal.LOCATION
|
||||||
|
)
|
||||||
for item in self.locations_list.selectedItems():
|
for item in self.locations_list.selectedItems():
|
||||||
if item.text() == signal_location:
|
if item.text() in signal_locations:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def acf_filters_ok(self, signal_name):
|
def acf_filters_ok(self, signal_name):
|
||||||
|
"""Evalaute if the 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]
|
||||||
@@ -1293,7 +1451,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
|
||||||
@@ -1304,6 +1462,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()
|
||||||
@@ -1369,11 +1532,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(
|
||||||
@@ -1382,23 +1543,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
|
||||||
@@ -1418,6 +1588,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()
|
||||||
@@ -1428,6 +1602,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 if 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}"
|
||||||
@@ -1436,6 +1614,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()
|
||||||
@@ -1448,11 +1629,12 @@ 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")
|
ARTEMIS_ICON = os.path.join(":", "icon", "default_pics", "Artemis3.500px.png")
|
||||||
|
img = QPixmap(ARTEMIS_ICON)
|
||||||
splash = QSplashScreen(img)
|
splash = QSplashScreen(img)
|
||||||
splash.show()
|
splash.show()
|
||||||
start = time()
|
start = time()
|
||||||
while time() - start < 2:
|
while time() - start < 1.5:
|
||||||
sleep(0.001)
|
sleep(0.001)
|
||||||
my_app.processEvents()
|
my_app.processEvents()
|
||||||
splash.close()
|
splash.close()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset resource="default_imgs.qrc">
|
<iconset resource="default_imgs.qrc">
|
||||||
<normaloff>:/icon/default_pics/Artemis3.ico</normaloff>:/icon/default_pics/Artemis3.ico</iconset>
|
<normaloff>:/icon/default_pics/Artemis3.500px.png</normaloff>:/icon/default_pics/Artemis3.500px.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="styleSheet">
|
<property name="styleSheet">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
@@ -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>
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
<number>0</number>
|
<number>0</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="movable">
|
<property name="movable">
|
||||||
<bool>true</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<widget class="QWidget" name="signal_properties_tab">
|
<widget class="QWidget" name="signal_properties_tab">
|
||||||
<property name="minimumSize">
|
<property name="minimumSize">
|
||||||
|
|||||||
155
audio_player.py
155
audio_player.py
@@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
from pydub import AudioSegment
|
|
||||||
from pygame import mixer
|
from pygame import mixer
|
||||||
from PyQt5.QtCore import QTimer, pyqtSlot, QObject
|
from PyQt5.QtCore import QTimer, pyqtSlot, QObject
|
||||||
|
|
||||||
@@ -7,123 +6,139 @@ 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
|
||||||
self.__play = play
|
self._play = play
|
||||||
self.__pause = pause
|
self._pause = pause
|
||||||
self.__stop = stop
|
self._stop = stop
|
||||||
self.__volume = volume
|
self._volume = volume
|
||||||
self.__audio_progress = audio_progress
|
self._audio_progress = audio_progress
|
||||||
self.__audio_file = None
|
self._audio_file = None
|
||||||
self.__timer = QTimer()
|
self._timer = QTimer()
|
||||||
self.__timer.timeout.connect(self.__update_bar)
|
self._timer.timeout.connect(self._update_bar)
|
||||||
self.__play.clicked.connect(self.__play_audio)
|
self._play.clicked.connect(self._play_audio)
|
||||||
self.__pause.clicked.connect(self.__pause_audio)
|
self._pause.clicked.connect(self._pause_audio)
|
||||||
self.__stop.clicked.connect(self.__stop_audio)
|
self._stop.clicked.connect(self._stop_audio)
|
||||||
self.__volume.valueChanged.connect(self.__set_volume)
|
self._volume.valueChanged.connect(self._set_volume)
|
||||||
self.__play.setIconSize(self.__play.size())
|
self._play.setIconSize(self._play.size())
|
||||||
self.__pause.setIconSize(self.__pause.size())
|
self._pause.setIconSize(self._pause.size())
|
||||||
self.__stop.setIconSize(self.__stop.size())
|
self._stop.setIconSize(self._stop.size())
|
||||||
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):
|
||||||
self.__play.setIcon(qta.icon('fa5.play-circle',
|
"""Repaint the buttons of the widgetd after the theme has changed."""
|
||||||
|
self._play.setIcon(qta.icon('fa5.play-circle',
|
||||||
color=active_color,
|
color=active_color,
|
||||||
color_disabled=inactive_color))
|
color_disabled=inactive_color))
|
||||||
self.__pause.setIcon(qta.icon('fa5.pause-circle',
|
self._pause.setIcon(qta.icon('fa5.pause-circle',
|
||||||
color=active_color,
|
color=active_color,
|
||||||
color_disabled=inactive_color))
|
color_disabled=inactive_color))
|
||||||
self.__stop.setIcon(qta.icon('fa5.stop-circle',
|
self._stop.setIcon(qta.icon('fa5.stop-circle',
|
||||||
color=active_color,
|
color=active_color,
|
||||||
color_disabled=inactive_color))
|
color_disabled=inactive_color))
|
||||||
|
|
||||||
@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()
|
||||||
self.__timer.stop()
|
self._timer.stop()
|
||||||
mixer.quit()
|
mixer.quit()
|
||||||
self.__audio_progress.reset()
|
self._audio_progress.reset()
|
||||||
self.__enable_buttons(False, False, False)
|
self._enable_buttons(False, False, False)
|
||||||
self.__paused = False
|
self._paused = False
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def __update_bar(self):
|
def _update_bar(self):
|
||||||
|
"""Update 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()
|
||||||
self.__audio_progress.reset()
|
self._audio_progress.reset()
|
||||||
self.__enable_buttons(True, False, False)
|
self._enable_buttons(True, False, False)
|
||||||
else:
|
else:
|
||||||
self.__audio_progress.setValue(pos)
|
self._audio_progress.setValue(pos)
|
||||||
|
|
||||||
def __set_max_progress_bar(self):
|
def _set_max_progress_bar(self):
|
||||||
self.__audio_progress.setMaximum(
|
"""Set the maximum value of the progress bar."""
|
||||||
mixer.Sound(self.__audio_file).get_length() * 1000
|
self._audio_progress.setMaximum(
|
||||||
|
mixer.Sound(self._audio_file).get_length() * 1000
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_audio_player(self, fname=""):
|
def set_audio_player(self, fname=""):
|
||||||
self.__first_call = True
|
"""Set the current audio sample."""
|
||||||
self.__reset_audio_widget()
|
self._first_call = True
|
||||||
|
self._reset_audio_widget()
|
||||||
full_name = os.path.join(
|
full_name = os.path.join(
|
||||||
Constants.DATA_FOLDER,
|
Constants.DATA_FOLDER,
|
||||||
Constants.AUDIO_FOLDER,
|
Constants.AUDIO_FOLDER,
|
||||||
fname + '.ogg'
|
fname + '.ogg'
|
||||||
)
|
)
|
||||||
if os.path.exists(full_name):
|
if os.path.exists(full_name):
|
||||||
self.__play.setEnabled(True)
|
self._play.setEnabled(True)
|
||||||
self.__audio_file = full_name
|
self._audio_file = full_name
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def __play_audio(self):
|
def _play_audio(self):
|
||||||
if not self.__paused:
|
"""Play the audio sample."""
|
||||||
if self.__first_call:
|
if not self._paused:
|
||||||
self.__first_call = False
|
if self._first_call:
|
||||||
mixer.init(frequency=AudioSegment.from_ogg(
|
self._first_call = False
|
||||||
self.__audio_file
|
mixer.init(48000, -16, 1, 1024)
|
||||||
).frame_rate,
|
mixer.music.load(self._audio_file)
|
||||||
buffer=2048)
|
self._set_volume()
|
||||||
mixer.music.load(self.__audio_file)
|
self._set_max_progress_bar()
|
||||||
self.__set_volume()
|
|
||||||
self.__set_max_progress_bar()
|
|
||||||
mixer.music.play()
|
mixer.music.play()
|
||||||
else:
|
else:
|
||||||
mixer.music.unpause()
|
mixer.music.unpause()
|
||||||
self.__paused = False
|
self._paused = False
|
||||||
self.__timer.start(self.__time_step)
|
self._timer.start(self._TIME_STEP)
|
||||||
self.__enable_buttons(False, True, True)
|
self._enable_buttons(False, True, True)
|
||||||
|
|
||||||
@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()
|
||||||
self.__enable_buttons(True, False, False)
|
self._enable_buttons(True, False, False)
|
||||||
|
|
||||||
@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):
|
||||||
self.__play.setEnabled(play_en)
|
"""Set the three buttons status."""
|
||||||
self.__pause.setEnabled(pause_en)
|
self._play.setEnabled(play_en)
|
||||||
self.__stop.setEnabled(stop_en)
|
self._pause.setEnabled(pause_en)
|
||||||
|
self._stop.setEnabled(stop_en)
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import os
|
|
||||||
from os import listdir
|
|
||||||
from os.path import isfile, join
|
|
||||||
|
|
||||||
mypath='../..'
|
|
||||||
excluded=['.gitignore','requirements_win.txt','artemis.py']
|
|
||||||
data_files = [f for f in listdir(mypath) if isfile(join(mypath, f))]
|
|
||||||
|
|
||||||
for i in excluded:
|
|
||||||
data_files.remove(i)
|
|
||||||
|
|
||||||
datas=["('../../" + i + "', '.')" for i in data_files]
|
|
||||||
|
|
||||||
pyinst_head='''
|
|
||||||
# -*- mode: python -*-
|
|
||||||
|
|
||||||
block_cipher = None
|
|
||||||
|
|
||||||
a = Analysis(['../../artemis.py'],
|
|
||||||
pathex=['../../'],
|
|
||||||
binaries=[],
|
|
||||||
datas=[
|
|
||||||
'''
|
|
||||||
|
|
||||||
pyinst_tail='''
|
|
||||||
hiddenimports=[],
|
|
||||||
hookspath=[],
|
|
||||||
runtime_hooks=[],
|
|
||||||
excludes=[],
|
|
||||||
win_no_prefer_redirects=False,
|
|
||||||
win_private_assemblies=False,
|
|
||||||
cipher=block_cipher,
|
|
||||||
noarchive=False)
|
|
||||||
|
|
||||||
pyz = PYZ(a.pure, a.zipped_data,
|
|
||||||
cipher=block_cipher)
|
|
||||||
|
|
||||||
exe = EXE(pyz,
|
|
||||||
a.scripts,
|
|
||||||
a.binaries,
|
|
||||||
a.zipfiles,
|
|
||||||
a.datas,
|
|
||||||
[],
|
|
||||||
name='Artemis',
|
|
||||||
debug=False,
|
|
||||||
bootloader_ignore_signals=False,
|
|
||||||
strip=False,
|
|
||||||
upx=True,
|
|
||||||
runtime_tmpdir=None,
|
|
||||||
console=True)
|
|
||||||
'''
|
|
||||||
|
|
||||||
setup_file = open('./setup.spec','w')
|
|
||||||
setup_file.write(pyinst_head + ','.join(datas) + "]," + pyinst_tail)
|
|
||||||
setup_file.close()
|
|
||||||
|
|
||||||
os.system("pyinstaller --onefile setup.spec")
|
|
||||||
os.system("cp -r ../../themes dist")
|
|
||||||
os.system("rm -rf build")
|
|
||||||
|
|
||||||
desktop = open('./artemis.desktop','w')
|
|
||||||
desktop.write("""#!/usr/bin/env xdg-open
|
|
||||||
[Desktop Entry]
|
|
||||||
Name=Artemis
|
|
||||||
StartupWMClass=artemis3
|
|
||||||
Exec=. /SETUP_PATH/Artemis
|
|
||||||
Terminal=False
|
|
||||||
Icon=artemis3
|
|
||||||
Type=Application""")
|
|
||||||
desktop.close()
|
|
||||||
|
|
||||||
print("""To finalize the installation (add Artemis in the main menu):\n
|
|
||||||
1)\tEdit artemis.desktop file properly and move it to '/.local/share/applications'
|
|
||||||
2)\tMove the icon file artemis3.svg to '/usr/share/icons/'
|
|
||||||
""")
|
|
||||||
@@ -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):
|
||||||
self.__text = ''
|
"""Initialize the instance."""
|
||||||
|
self._text = ''
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
# def __set_text(self, text):
|
|
||||||
# self.__text = text
|
|
||||||
|
|
||||||
def text(self):
|
def text(self):
|
||||||
return self.__text
|
"""Return the text displayed on the bar."""
|
||||||
|
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:
|
||||||
|
|||||||
70
constants.py
70
constants.py
@@ -1,24 +1,45 @@
|
|||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
import os.path
|
import os.path
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
|
|
||||||
|
class MainTabs(IntEnum):
|
||||||
|
"""The main tabs indeces."""
|
||||||
|
|
||||||
|
SIGNAL = 0
|
||||||
|
FILTERS = 1
|
||||||
|
GFD = 2
|
||||||
|
FORECAST = 3
|
||||||
|
|
||||||
|
|
||||||
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 +55,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 +73,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 +89,19 @@ 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"
|
||||||
@@ -83,7 +110,12 @@ class ForecastColors:
|
|||||||
KP5_COLOR = "#BEE3FE"
|
KP5_COLOR = "#BEE3FE"
|
||||||
|
|
||||||
|
|
||||||
|
_Band = namedtuple("Band", ["lower", "upper"])
|
||||||
|
|
||||||
|
|
||||||
class Constants:
|
class Constants:
|
||||||
|
"""Container class for several constants 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,21 +151,18 @@ 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"
|
_ELF = _Band(0, 30) # Formally it is (3, 30) Hz.
|
||||||
NOT_SELECTED = "nosignalselected.png"
|
_SLF = _Band(30, 300)
|
||||||
__Band = namedtuple("Band", ["lower", "upper"])
|
_ULF = _Band(300, 3000)
|
||||||
__ELF = __Band(0, 30) # Formally it is (3, 30) Hz.
|
_VLF = _Band(3000, 30000)
|
||||||
__SLF = __Band(30, 300)
|
_LF = _Band(30 * 10**3, 300 * 10**3)
|
||||||
__ULF = __Band(300, 3000)
|
_MF = _Band(300 * 10 ** 3, 3000 * 10**3)
|
||||||
__VLF = __Band(3000, 30000)
|
_HF = _Band(3 * 10**6, 30 * 10**6)
|
||||||
__LF = __Band(30 * 10**3, 300 * 10**3)
|
_VHF = _Band(30 * 10**6, 300 * 10**6)
|
||||||
__MF = __Band(300 * 10 ** 3, 3000 * 10**3)
|
_UHF = _Band(300 * 10**6, 3000 * 10**6)
|
||||||
__HF = __Band(3 * 10**6, 30 * 10**6)
|
_SHF = _Band(3 * 10**9, 30 * 10**9)
|
||||||
__VHF = __Band(30 * 10**6, 300 * 10**6)
|
_EHF = _Band(30 * 10**9, 300 * 10**9)
|
||||||
__UHF = __Band(300 * 10**6, 3000 * 10**6)
|
BANDS = (_ELF, _SLF, _ULF, _VLF, _LF, _MF, _HF, _VHF, _UHF, _SHF, _EHF)
|
||||||
__SHF = __Band(3 * 10**9, 30 * 10**9)
|
|
||||||
__EHF = __Band(30 * 10**9, 300 * 10**9)
|
|
||||||
BANDS = (__ELF, __SLF, __ULF, __VLF, __LF, __MF, __HF, __VHF, __UHF, __SHF, __EHF)
|
|
||||||
MAX_DIGITS = 3
|
MAX_DIGITS = 3
|
||||||
RANGE_SEPARATOR = ' ÷ '
|
RANGE_SEPARATOR = ' ÷ '
|
||||||
GFD_SITE = "http://qrg.globaltuners.com/"
|
GFD_SITE = "http://qrg.globaltuners.com/"
|
||||||
@@ -149,10 +178,15 @@ 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"
|
||||||
|
FIELD_SEPARATOR = ";"
|
||||||
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)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<RCC>
|
<RCC>
|
||||||
<qresource prefix="icon">
|
<qresource prefix="icon">
|
||||||
<file>default_pics/Artemis3.500px.png</file>
|
<file>default_pics/Artemis3.500px.png</file>
|
||||||
<file>default_pics/Artemis3.ico</file>
|
|
||||||
<file>default_pics/Artemis3.png</file>
|
|
||||||
</qresource>
|
</qresource>
|
||||||
<qresource prefix="pics">
|
<qresource prefix="pics">
|
||||||
<file>default_pics/nosignalselected.png</file>
|
<file>default_pics/nosignalselected.png</file>
|
||||||
|
|||||||
23012
default_imgs_rc.py
23012
default_imgs_rc.py
File diff suppressed because it is too large
Load Diff
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
BIN
deploy/Windows/artemis3.ico
Normal file
BIN
deploy/Windows/artemis3.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 KiB |
56
deploy/Windows/deploy_win.bat
Normal file
56
deploy/Windows/deploy_win.bat
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
@echo off
|
||||||
|
REM Check and gain admin permissions
|
||||||
|
IF "%PROCESSOR_ARCHITECTURE%" EQU "amd64" (
|
||||||
|
>nul 2>&1 "%SYSTEMROOT%\SysWOW64\cacls.exe" "%SYSTEMROOT%\SysWOW64\config\system"
|
||||||
|
) ELSE (
|
||||||
|
>nul 2>&1 "%SYSTEMROOT%\system32\cacls.exe" "%SYSTEMROOT%\system32\config\system"
|
||||||
|
)
|
||||||
|
|
||||||
|
if '%errorlevel%' NEQ '0' (
|
||||||
|
echo Requesting administrative privileges...
|
||||||
|
goto UACPrompt
|
||||||
|
) else ( goto gotAdmin )
|
||||||
|
|
||||||
|
:UACPrompt
|
||||||
|
echo Set UAC = CreateObject^("Shell.Application"^) > "%temp%\getadmin.vbs"
|
||||||
|
set params= %*
|
||||||
|
echo UAC.ShellExecute "cmd.exe", "/c ""%~s0"" %params:"=""%", "", "runas", 1 >> "%temp%\getadmin.vbs"
|
||||||
|
|
||||||
|
"%temp%\getadmin.vbs"
|
||||||
|
del "%temp%\getadmin.vbs"
|
||||||
|
exit /B
|
||||||
|
|
||||||
|
:gotAdmin
|
||||||
|
pushd "%CD%"
|
||||||
|
CD /D "%~dp0"
|
||||||
|
|
||||||
|
REM Set the correct permissions for Artemis folder
|
||||||
|
set artemis_path=%~dp0..\..
|
||||||
|
icacls "%artemis_path%" /grant %USERNAME%:(OI)(CI)F /T > nul
|
||||||
|
|
||||||
|
REM Download necessary libraries with pip3
|
||||||
|
set choice=Y
|
||||||
|
set /p choice=Download necessary Python libraries? [Y]/N
|
||||||
|
if /I '%choice%'=='Y' pip3 install -r %~dp0requirements_win.txt
|
||||||
|
echo:
|
||||||
|
|
||||||
|
REM Create a shortcut
|
||||||
|
set choice=Y
|
||||||
|
set /p choice=Create a desktop shortcut? [Y]/N
|
||||||
|
if /I '%choice%'=='N' goto end
|
||||||
|
|
||||||
|
ren "%artemis_path%\Artemis.py" "Artemis.pyw"
|
||||||
|
echo Set oWS = WScript.CreateObject("WScript.Shell") > CreateShortcut.vbs
|
||||||
|
echo sLinkFile = "%USERPROFILE%\Desktop\Artemis.lnk" >> CreateShortcut.vbs
|
||||||
|
echo Set oLink = oWS.CreateShortcut(sLinkFile) >> CreateShortcut.vbs
|
||||||
|
echo oLink.TargetPath = "%artemis_path%\Artemis.pyw" >> CreateShortcut.vbs
|
||||||
|
echo oLink.WorkingDirectory = "%artemis_path%" >> CreateShortcut.vbs
|
||||||
|
echo oLink.IconLocation = "%~dp0artemis3.ico" >> CreateShortcut.vbs
|
||||||
|
echo oLink.Save >> CreateShortcut.vbs
|
||||||
|
cscript /nologo CreateShortcut.vbs
|
||||||
|
del CreateShortcut.vbs
|
||||||
|
:end
|
||||||
|
echo:
|
||||||
|
echo SETTING COMPLETE!
|
||||||
|
echo:
|
||||||
|
pause
|
||||||
@@ -1,38 +1,55 @@
|
|||||||
from PyQt5.QtWidgets import QPushButton
|
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):
|
||||||
self.__text_a = text_a
|
"""Set the two texts to be displayed."""
|
||||||
self.__text_b = text_b
|
self._text_a = text_a
|
||||||
|
self._text_b = text_b
|
||||||
|
|
||||||
def set_slave_filters(self, simple_ones=None,
|
def set_slave_filters(self, simple_ones=None,
|
||||||
radio_1=None,
|
radio_1=None,
|
||||||
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):
|
||||||
self.__simple_ones = simple_ones
|
"""Set all the 'slave' widgets.
|
||||||
self.__ruled_by_radio_1 = ruled_by_radio_1
|
|
||||||
self.__radio_1 = radio_1
|
Keyword arguments:
|
||||||
self.__ruled_by_radio_2 = ruled_by_radio_2
|
simple_ones -- a list of widgets.
|
||||||
self.__radio_2 = radio_2
|
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._ruled_by_radio_1 = ruled_by_radio_1
|
||||||
|
self._radio_1 = radio_1
|
||||||
|
self._ruled_by_radio_2 = ruled_by_radio_2
|
||||||
|
self._radio_2 = radio_2
|
||||||
|
|
||||||
@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
|
||||||
else:
|
else:
|
||||||
self.setText(self.__text_a)
|
self.setText(self._text_a)
|
||||||
enable = True
|
enable = True
|
||||||
for f in self.__simple_ones:
|
for f in self._simple_ones:
|
||||||
f.setEnabled(enable)
|
f.setEnabled(enable)
|
||||||
radio_btns = self.__radio_1, self.__radio_2
|
radio_btns = self._radio_1, self._radio_2
|
||||||
ruled_widgets = self.__ruled_by_radio_1, self.__ruled_by_radio_2
|
ruled_widgets = self._ruled_by_radio_1, self._ruled_by_radio_2
|
||||||
for radio_btn, ruled_by in zip(radio_btns, ruled_widgets):
|
for radio_btn, ruled_by in zip(radio_btns, ruled_widgets):
|
||||||
if ruled_by:
|
if ruled_by:
|
||||||
for f in ruled_by:
|
for f in ruled_by:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset resource="default_imgs.qrc">
|
<iconset resource="default_imgs.qrc">
|
||||||
<normaloff>:/icon/default_pics/Artemis3.ico</normaloff>:/icon/default_pics/Artemis3.ico</iconset>
|
<normaloff>:/icon/default_pics/Artemis3.500px.png</normaloff>:/icon/default_pics/Artemis3.500px.png</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="styleSheet">
|
<property name="styleSheet">
|
||||||
<string notr="true"/>
|
<string notr="true"/>
|
||||||
|
|||||||
@@ -5,75 +5,91 @@ from threads import DownloadThread, ThreadStatus
|
|||||||
from utilities import pop_up, resource_path
|
from utilities import pop_up, resource_path
|
||||||
from constants import Constants, Messages
|
from constants import Constants, Messages
|
||||||
|
|
||||||
Ui_Download_window, _ = uic.loadUiType(resource_path("download_db_window.ui"))
|
|
||||||
|
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()
|
||||||
|
closed = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Initialize the window."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setWindowFlags(
|
self.setWindowFlags(
|
||||||
# Qt.Window |
|
# Qt.Window |
|
||||||
Qt.CustomizeWindowHint |
|
Qt.CustomizeWindowHint |
|
||||||
Qt.WindowTitleHint |
|
Qt.WindowTitleHint |
|
||||||
Qt.WindowCloseButtonHint #|
|
Qt.WindowCloseButtonHint |
|
||||||
# Qt.WindowStaysOnTopHint
|
Qt.WindowStaysOnTopHint
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__no_internet_msg = pop_up(self, title=Messages.NO_CONNECTION,
|
self._no_internet_msg = pop_up(self, title=Messages.NO_CONNECTION,
|
||||||
text=Messages.NO_CONNECTION_MSG,
|
text=Messages.NO_CONNECTION_MSG,
|
||||||
connection=self.close)
|
connection=self.close)
|
||||||
|
|
||||||
self.__bad_db_download_msg = pop_up(self, title=Messages.BAD_DOWNLOAD,
|
self._bad_db_download_msg = pop_up(self, title=Messages.BAD_DOWNLOAD,
|
||||||
text=Messages.BAD_DOWNLOAD_MSG,
|
text=Messages.BAD_DOWNLOAD_MSG,
|
||||||
connection=self.close)
|
connection=self.close)
|
||||||
|
|
||||||
self.__download_thread = DownloadThread()
|
self._download_thread = DownloadThread()
|
||||||
self.__download_thread.finished.connect(self.__wait_close)
|
self._download_thread.finished.connect(self._wait_close)
|
||||||
self.__download_thread.progress.connect(self.__display_progress)
|
self._download_thread.progress.connect(self._display_progress)
|
||||||
self.cancel_btn.clicked.connect(self.__terminate_process)
|
self.closed.connect(self._download_thread.set_exit)
|
||||||
|
self.cancel_btn.clicked.connect(self._terminate_process)
|
||||||
|
|
||||||
def start_download(self):
|
def start_download(self):
|
||||||
self.__download_thread.start()
|
"""Start the download thread."""
|
||||||
|
self._download_thread.start()
|
||||||
|
|
||||||
def __downlaod_format_str(self, n, speed):
|
def _downlaod_format_str(self, n, speed):
|
||||||
return f"Downloaded MB: {n}\nSpeed: {speed} MB/s"
|
"""Return a well-formatted string with downloaded MB and speed."""
|
||||||
|
return f"Downloaded: {n} MB\nSpeed: {speed} MB/s"
|
||||||
|
|
||||||
def show(self):
|
def show(self):
|
||||||
self.status_lbl.setText(self.__downlaod_format_str(0, 0))
|
"""Extends QWidget.show. Set downloaded MB and speed to zero."""
|
||||||
|
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:
|
||||||
self.status_lbl.setText(Constants.EXTRACTING_MSG + '\n')
|
self.status_lbl.setText(Constants.EXTRACTING_MSG + '\n')
|
||||||
|
|
||||||
|
def _stop_thread(self):
|
||||||
|
"""Ask the download thread to stop."""
|
||||||
|
if self._download_thread.isRunning():
|
||||||
|
self.closed.emit()
|
||||||
|
self._download_thread.wait()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def __terminate_process(self):
|
def _terminate_process(self):
|
||||||
if self.__download_thread.isRunning():
|
"""Terminate the download thread and close."""
|
||||||
self.__download_thread.terminate()
|
self._stop_thread()
|
||||||
self.__download_thread.wait()
|
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def __wait_close(self):
|
def _wait_close(self):
|
||||||
if self.__download_thread.status is ThreadStatus.OK:
|
"""Decide the action based on the download thread status and close."""
|
||||||
|
if self._download_thread.status is ThreadStatus.OK:
|
||||||
self.complete.emit()
|
self.complete.emit()
|
||||||
self.close()
|
self.close()
|
||||||
elif self.__download_thread.status is ThreadStatus.NO_CONNECTION_ERR:
|
elif self._download_thread.status is ThreadStatus.NO_CONNECTION_ERR:
|
||||||
self.__no_internet_msg.show()
|
self._no_internet_msg.show()
|
||||||
elif self.__download_thread.status is ThreadStatus.BAD_DOWNLOAD_ERR:
|
elif self._download_thread.status is ThreadStatus.BAD_DOWNLOAD_ERR:
|
||||||
self.__bad_db_download_msg.show()
|
self._bad_db_download_msg.show()
|
||||||
else:
|
else:
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def reject(self):
|
def reject(self):
|
||||||
if self.__download_thread.isRunning():
|
"""Extends QWidget.reject. Terminate the download thread."""
|
||||||
self.__download_thread.terminate()
|
self._stop_thread()
|
||||||
self.__download_thread.wait()
|
|
||||||
super().reject()
|
super().reject()
|
||||||
|
|||||||
@@ -3,21 +3,28 @@ 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 +33,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()
|
||||||
|
|||||||
@@ -3,14 +3,19 @@ from PyQt5.QtCore import QSize
|
|||||||
|
|
||||||
|
|
||||||
class FixedAspectRatioWidget(QWidget):
|
class FixedAspectRatioWidget(QWidget):
|
||||||
space = 10
|
"""Subclass QWidget. Keep all the internal labels to a fixed aspect ratio."""
|
||||||
|
|
||||||
|
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
|
||||||
w_pad = w - 10
|
w_pad = w - 10
|
||||||
if w_lbl > w_pad:
|
if w_lbl > w_pad:
|
||||||
|
|||||||
@@ -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 to False and level to 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 to True."""
|
||||||
self.is_on = True
|
self.is_on = True
|
||||||
|
|
||||||
def switch_off(self):
|
def switch_off(self):
|
||||||
|
"""Set is_on to 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):
|
||||||
super().switch_off()
|
"""Extend _BaseSwitchableLabel.switch_off.
|
||||||
self.__apply_colors(*self.switch_off_colors)
|
|
||||||
|
|
||||||
def __apply_colors(self, start, end):
|
Apply the inactive state colors."""
|
||||||
|
super().switch_off()
|
||||||
|
self._apply_colors(*self.switch_off_colors)
|
||||||
|
|
||||||
|
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 the applied 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()
|
||||||
|
|||||||
418
themesmanager.py
418
themesmanager.py
@@ -11,6 +11,8 @@ from utilities import pop_up
|
|||||||
|
|
||||||
|
|
||||||
class ThemeConstants:
|
class ThemeConstants:
|
||||||
|
"""Container class for all the theme-related constants."""
|
||||||
|
|
||||||
FOLDER = "themes"
|
FOLDER = "themes"
|
||||||
EXTENSION = ".qss"
|
EXTENSION = ".qss"
|
||||||
ICONS_FOLDER = "icons"
|
ICONS_FOLDER = "icons"
|
||||||
@@ -27,103 +29,188 @@ 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 _ColorsHandler:
|
||||||
|
"""Manage the theme's secondary colors.
|
||||||
|
|
||||||
|
Contains a _Color inner class."""
|
||||||
|
|
||||||
|
class _Color:
|
||||||
|
"""Characterize a color from a string.
|
||||||
|
|
||||||
|
Can handle strings representing multiple colors."""
|
||||||
|
|
||||||
|
MAX_COLORS = 2
|
||||||
|
|
||||||
|
def __init__(self, line):
|
||||||
|
"""Define the color from the string 'line'.
|
||||||
|
|
||||||
|
All relevant features are defined:
|
||||||
|
- if the format is valid;
|
||||||
|
- if 'line' represent a single color or a list of colors.
|
||||||
|
- the 'quality' of the color."""
|
||||||
|
quality, color_str = line.split(ThemeConstants.COLOR_SEPARATOR)
|
||||||
|
color_str = color_str.strip()
|
||||||
|
self.quality = quality.lower().strip()
|
||||||
|
self.color_str = ''
|
||||||
|
self.color_list = []
|
||||||
|
if ',' in color_str:
|
||||||
|
self.is_simple_string = False
|
||||||
|
self.color_list = [c.strip() for c in color_str.split(',')]
|
||||||
|
else:
|
||||||
|
self.is_simple_string = True
|
||||||
|
self.color_str = color_str
|
||||||
|
self.is_valid = self._color_is_valid()
|
||||||
|
|
||||||
|
def _color_is_valid(self):
|
||||||
|
"""Return if the color (or the list of colors) has a valid html format."""
|
||||||
|
pattern = "#([a-zA-Z0-9]){6}"
|
||||||
|
|
||||||
|
def match_ok(col):
|
||||||
|
return bool(re.match(pattern, col)) and len(col) == 7
|
||||||
|
|
||||||
|
if not self.is_simple_string:
|
||||||
|
if len(self.color_list) <= self.MAX_COLORS:
|
||||||
|
return all(match_ok(c) for c in self.color_list)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return match_ok(self.color_str)
|
||||||
|
|
||||||
|
def __init__(self, simple_color_list, double_color_list):
|
||||||
|
"""Initialize the lists of valid _Color objects."""
|
||||||
|
self.simple_color_list = simple_color_list
|
||||||
|
self.double_color_list = double_color_list
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_file(cls, colors_str):
|
||||||
|
"""Return a _ColorsHandler object with two lists of valid _Color objects.
|
||||||
|
|
||||||
|
If the file is empty or there are no valid colors return None."""
|
||||||
|
if colors_str:
|
||||||
|
simple_color_list = []
|
||||||
|
double_color_list = []
|
||||||
|
for line in colors_str.splitlines():
|
||||||
|
color = cls._Color(line)
|
||||||
|
if color.is_valid:
|
||||||
|
if color.is_simple_string:
|
||||||
|
simple_color_list.append(color)
|
||||||
|
else:
|
||||||
|
double_color_list.append(color)
|
||||||
|
if simple_color_list or double_color_list:
|
||||||
|
return cls(simple_color_list, double_color_list)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class ThemeManager:
|
class ThemeManager:
|
||||||
def __init__(self, parent):
|
"""Manage all the operations releted to the themes."""
|
||||||
self.__parent = parent
|
|
||||||
self.__parent.active_color = ThemeConstants.DEFAULT_ACTIVE_COLOR
|
|
||||||
self.__parent.inactive_color = ThemeConstants.DEFAULT_INACTIVE_COLOR
|
|
||||||
|
|
||||||
self.__theme_path = ""
|
def __init__(self, owner):
|
||||||
self.__current_theme = ""
|
"""Initialize the ThemeManager instance."""
|
||||||
|
self._owner = owner
|
||||||
|
self._owner.active_color = ThemeConstants.DEFAULT_ACTIVE_COLOR
|
||||||
|
self._owner.inactive_color = ThemeConstants.DEFAULT_INACTIVE_COLOR
|
||||||
|
|
||||||
self.__parent.default_images_folder = os.path.join(
|
self._theme_path = ""
|
||||||
ThemeConstants.FOLDER,
|
self._current_theme = ""
|
||||||
ThemeConstants.DEFAULT,
|
|
||||||
ThemeConstants.ICONS_FOLDER
|
|
||||||
)
|
|
||||||
|
|
||||||
self.__space_weather_labels = SwitchableLabelsIterable(
|
self._space_weather_labels = SwitchableLabelsIterable(
|
||||||
*list(
|
*list(
|
||||||
chain(
|
chain(
|
||||||
self.__parent.switchable_r_labels,
|
self._owner.switchable_r_labels,
|
||||||
self.__parent.switchable_s_labels,
|
self._owner.switchable_s_labels,
|
||||||
self.__parent.switchable_g_now_labels,
|
self._owner.switchable_g_now_labels,
|
||||||
self.__parent.switchable_g_today_labels,
|
self._owner.switchable_g_today_labels,
|
||||||
self.__parent.k_storm_labels,
|
self._owner.k_storm_labels,
|
||||||
self.__parent.a_storm_labels,
|
self._owner.a_storm_labels,
|
||||||
[self.__parent.expected_noise_lbl]
|
[self._owner.expected_noise_lbl]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"switch_on_colors",
|
"switch_on_colors",
|
||||||
ThemeConstants.DEFAULT_ON_COLORS
|
ThemeConstants.DEFAULT_ON_COLORS
|
||||||
)
|
)
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"switch_off_colors", ThemeConstants.DEFAULT_OFF_COLORS
|
"switch_off_colors", ThemeConstants.DEFAULT_OFF_COLORS
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__theme_names = {}
|
self._theme_names = {}
|
||||||
self.__detect_themes()
|
|
||||||
|
|
||||||
def __refresh_range_labels(self):
|
def _refresh_range_labels(self):
|
||||||
self.__parent.set_acf_interval_label()
|
"""Refresh the range-labels."""
|
||||||
self.__parent.set_band_filter_label(
|
self._owner.set_acf_interval_label()
|
||||||
self.__parent.activate_low_band_filter_btn,
|
self._owner.set_band_filter_label(
|
||||||
self.__parent.lower_band_spinbox,
|
self._owner.activate_low_band_filter_btn,
|
||||||
self.__parent.lower_band_filter_unit,
|
self._owner.lower_band_spinbox,
|
||||||
self.__parent.lower_band_confidence,
|
self._owner.lower_band_filter_unit,
|
||||||
self.__parent.activate_up_band_filter_btn,
|
self._owner.lower_band_confidence,
|
||||||
self.__parent.upper_band_spinbox,
|
self._owner.activate_up_band_filter_btn,
|
||||||
self.__parent.upper_band_filter_unit,
|
self._owner.upper_band_spinbox,
|
||||||
self.__parent.upper_band_confidence,
|
self._owner.upper_band_filter_unit,
|
||||||
self.__parent.band_range_lbl
|
self._owner.upper_band_confidence,
|
||||||
|
self._owner.band_range_lbl
|
||||||
)
|
)
|
||||||
|
|
||||||
self.__parent.set_band_filter_label(
|
self._owner.set_band_filter_label(
|
||||||
self.__parent.activate_low_freq_filter_btn,
|
self._owner.activate_low_freq_filter_btn,
|
||||||
self.__parent.lower_freq_spinbox,
|
self._owner.lower_freq_spinbox,
|
||||||
self.__parent.lower_freq_filter_unit,
|
self._owner.lower_freq_filter_unit,
|
||||||
self.__parent.lower_freq_confidence,
|
self._owner.lower_freq_confidence,
|
||||||
self.__parent.activate_up_freq_filter_btn,
|
self._owner.activate_up_freq_filter_btn,
|
||||||
self.__parent.upper_freq_spinbox,
|
self._owner.upper_freq_spinbox,
|
||||||
self.__parent.upper_freq_filter_unit,
|
self._owner.upper_freq_filter_unit,
|
||||||
self.__parent.upper_freq_confidence,
|
self._owner.upper_freq_confidence,
|
||||||
self.__parent.freq_range_lbl
|
self._owner.freq_range_lbl
|
||||||
)
|
)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def __apply(self, theme_path):
|
def _apply(self, theme_path):
|
||||||
self.__theme_path = theme_path
|
"""Apply the selected theme.
|
||||||
|
|
||||||
|
Refresh all relevant widgets.
|
||||||
|
Display a QMessageBox if the theme is not found."""
|
||||||
|
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._owner.display_specs(
|
||||||
item=self.__parent.result_list.currentItem(),
|
item=self._owner.signals_list.currentItem(),
|
||||||
previous_item=None
|
previous_item=None
|
||||||
)
|
)
|
||||||
self.__refresh_range_labels()
|
self._refresh_range_labels()
|
||||||
self.__parent.audio_widget.refresh_btns_colors(
|
self._owner.audio_widget.refresh_btns_colors(
|
||||||
self.__parent.active_color,
|
self._owner.active_color,
|
||||||
self.__parent.inactive_color
|
self._owner.inactive_color
|
||||||
)
|
)
|
||||||
self.__space_weather_labels.refresh()
|
self._space_weather_labels.refresh()
|
||||||
else:
|
else:
|
||||||
pop_up(self.__parent, title=ThemeConstants.THEME_NOT_FOUND,
|
pop_up(self._owner, title=ThemeConstants.THEME_NOT_FOUND,
|
||||||
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('_')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
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._owner, exclusive=True)
|
||||||
if os.path.exists(ThemeConstants.FOLDER):
|
if os.path.exists(ThemeConstants.FOLDER):
|
||||||
for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)):
|
for theme_folder in sorted(os.listdir(ThemeConstants.FOLDER)):
|
||||||
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
|
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
|
||||||
@@ -131,115 +218,64 @@ class ThemeManager:
|
|||||||
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
|
relative_folder = os.path.join(ThemeConstants.FOLDER, theme_folder)
|
||||||
themes.append(relative_folder)
|
themes.append(relative_folder)
|
||||||
for theme_path in themes:
|
for theme_path in themes:
|
||||||
theme_name = '&' + self.__pretty_name(os.path.basename(theme_path))
|
theme_name = '&' + self._pretty_name(os.path.basename(theme_path))
|
||||||
new_theme = ag.addAction(
|
new_theme = ag.addAction(
|
||||||
QAction(
|
QAction(
|
||||||
theme_name,
|
theme_name,
|
||||||
self.__parent, checkable=True
|
self._owner, checkable=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.__parent.menu_themes.addAction(new_theme)
|
self._owner.menu_themes.addAction(new_theme)
|
||||||
self.__theme_names[theme_name.lstrip('&')] = new_theme
|
self._theme_names[theme_name.lstrip('&')] = new_theme
|
||||||
new_theme.triggered.connect(partial(self.__apply, theme_path))
|
new_theme.triggered.connect(partial(self._apply, theme_path))
|
||||||
else:
|
else:
|
||||||
pop_up(self.__parent, title=ThemeConstants.THEME_FOLDER_NOT_FOUND,
|
pop_up(self._owner, title=ThemeConstants.THEME_FOLDER_NOT_FOUND,
|
||||||
text=ThemeConstants.MISSING_THEME_FOLDER).show()
|
text=ThemeConstants.MISSING_THEME_FOLDER).show()
|
||||||
|
|
||||||
def __is_valid_html_color(self, colors):
|
def _change(self):
|
||||||
pattern = "#([a-zA-Z0-9]){6}"
|
"""Change the current theme.
|
||||||
match_ok = lambda col: bool(re.match(pattern, col))
|
|
||||||
if isinstance(colors, list):
|
|
||||||
if len(colors) > 1:
|
|
||||||
return all(match_ok(c) for c in colors)
|
|
||||||
else:
|
|
||||||
return match_ok(colors[0])
|
|
||||||
else:
|
|
||||||
return match_ok(colors)
|
|
||||||
|
|
||||||
def __change(self):
|
Apply the stylesheet and set active and inactive colors.
|
||||||
theme_name = os.path.basename(self.__theme_path) + ThemeConstants.EXTENSION
|
Set all the new images needed.
|
||||||
|
Save the new current theme on file."""
|
||||||
|
theme_name = os.path.basename(self._theme_path) + ThemeConstants.EXTENSION
|
||||||
try:
|
try:
|
||||||
with open(
|
with open(os.path.join(self._theme_path, theme_name), "r") as stylesheet:
|
||||||
os.path.join(self.__theme_path, theme_name), "r"
|
|
||||||
) as stylesheet:
|
|
||||||
style = stylesheet.read()
|
style = stylesheet.read()
|
||||||
self.__parent.setStyleSheet(style)
|
self._owner.setStyleSheet(style)
|
||||||
self.__parent.download_window.setStyleSheet(style)
|
self._owner.download_window.setStyleSheet(style)
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
pop_up(self.__parent, title=ThemeConstants.THEME_NOT_FOUND,
|
pop_up(self._owner, title=ThemeConstants.THEME_NOT_FOUND,
|
||||||
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 \
|
path_to_search_label = os.path.join(icons_path, Constants.SEARCH_LABEL_IMG)
|
||||||
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(
|
|
||||||
icons_path,
|
|
||||||
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:
|
||||||
self.__parent.search_label.setPixmap(
|
path = ThemeConstants.DEFAULT_SEARCH_LABEL_PATH
|
||||||
QPixmap(default_search_label)
|
|
||||||
)
|
|
||||||
self.__parent.modulation_search_label.setPixmap(
|
|
||||||
QPixmap(default_search_label)
|
|
||||||
)
|
|
||||||
self.__parent.location_search_label.setPixmap(
|
|
||||||
QPixmap(default_search_label)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.__parent.search_label.setScaledContents(True)
|
self._owner.search_label.setPixmap(QPixmap(path))
|
||||||
self.__parent.modulation_search_label.setScaledContents(True)
|
self._owner.modulation_search_label.setPixmap(QPixmap(path))
|
||||||
self.__parent.location_search_label.setScaledContents(True)
|
self._owner.location_search_label.setPixmap(QPixmap(path))
|
||||||
|
|
||||||
path_to_volume_label = os.path.join(
|
self._owner.search_label.setScaledContents(True)
|
||||||
icons_path,
|
self._owner.modulation_search_label.setScaledContents(True)
|
||||||
Constants.VOLUME_LABEL_IMG
|
self._owner.location_search_label.setScaledContents(True)
|
||||||
)
|
|
||||||
default_volume_label = os.path.join(
|
path_to_volume_label = os.path.join(icons_path, Constants.VOLUME_LABEL_IMG)
|
||||||
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:
|
||||||
self.__parent.volume_label.setPixmap(
|
path = ThemeConstants.DEFAULT_VOLUME_LABEL_PATH
|
||||||
QPixmap(default_volume_label)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.__parent.volume_label.setScaledContents(True)
|
self._owner.volume_label.setPixmap(QPixmap(path))
|
||||||
|
self._owner.volume_label.setScaledContents(True)
|
||||||
|
|
||||||
path_to_colors = os.path.join(
|
path_to_colors = os.path.join(self._theme_path, ThemeConstants.COLORS)
|
||||||
self.__theme_path,
|
|
||||||
ThemeConstants.COLORS
|
|
||||||
)
|
|
||||||
|
|
||||||
active_color_ok = False
|
active_color_ok = False
|
||||||
inactive_color_ok = False
|
inactive_color_ok = False
|
||||||
@@ -249,100 +285,84 @@ class ThemeManager:
|
|||||||
|
|
||||||
if os.path.exists(path_to_colors):
|
if os.path.exists(path_to_colors):
|
||||||
with open(path_to_colors, "r") as colors_file:
|
with open(path_to_colors, "r") as colors_file:
|
||||||
for line in colors_file:
|
color_handler = _ColorsHandler.from_file(colors_file.read())
|
||||||
if ThemeConstants.COLOR_SEPARATOR in line:
|
|
||||||
quality, color = line.split(ThemeConstants.COLOR_SEPARATOR)
|
if color_handler is not None:
|
||||||
color = color.rstrip()
|
for color in color_handler.simple_color_list:
|
||||||
color_len = 1
|
if color.quality == Constants.ACTIVE:
|
||||||
if ',' in color:
|
self._owner.active_color = color.color_str
|
||||||
color = [c.strip() for c in color.split(',')]
|
|
||||||
color_len = len(color)
|
|
||||||
if self.__is_valid_html_color(color):
|
|
||||||
if color_len == 1:
|
|
||||||
if quality.lower() == Constants.ACTIVE:
|
|
||||||
self.__parent.active_color = color
|
|
||||||
active_color_ok = True
|
active_color_ok = True
|
||||||
if quality.lower() == Constants.INACTIVE:
|
if color.quality == Constants.INACTIVE:
|
||||||
self.__parent.inactive_color = color
|
self._owner.inactive_color = color.color_str
|
||||||
inactive_color_ok = True
|
inactive_color_ok = True
|
||||||
if quality.lower() == Constants.TEXT_COLOR:
|
if color.quality == Constants.TEXT_COLOR:
|
||||||
text_color_ok = True
|
text_color_ok = True
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"text_color",
|
"text_color",
|
||||||
color
|
color.color_str
|
||||||
)
|
)
|
||||||
if color_len == 2:
|
for color in color_handler.double_color_list:
|
||||||
if quality.lower() == Constants.LABEL_ON_COLOR:
|
if color.quality == Constants.LABEL_ON_COLOR:
|
||||||
switch_on_color_ok = True
|
switch_on_color_ok = True
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"switch_on_colors",
|
"switch_on_colors",
|
||||||
color
|
color.color_list
|
||||||
)
|
)
|
||||||
if quality.lower() == Constants.LABEL_OFF_COLOR:
|
if color.quality == Constants.LABEL_OFF_COLOR:
|
||||||
switch_off_color_ok = True
|
switch_off_color_ok = True
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"switch_off_colors",
|
"switch_off_colors",
|
||||||
color
|
color.color_list
|
||||||
)
|
)
|
||||||
|
|
||||||
if not (active_color_ok and inactive_color_ok):
|
if not (active_color_ok and inactive_color_ok):
|
||||||
self.__parent.active_color = ThemeConstants.DEFAULT_ACTIVE_COLOR
|
self._owner.active_color = ThemeConstants.DEFAULT_ACTIVE_COLOR
|
||||||
self.__parent.inactive_color = ThemeConstants.DEFAULT_INACTIVE_COLOR
|
self._owner.inactive_color = ThemeConstants.DEFAULT_INACTIVE_COLOR
|
||||||
|
|
||||||
if not (switch_on_color_ok and switch_off_color_ok):
|
if not (switch_on_color_ok and switch_off_color_ok):
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"switch_on_colors",
|
"switch_on_colors",
|
||||||
ThemeConstants.DEFAULT_ON_COLORS
|
ThemeConstants.DEFAULT_ON_COLORS
|
||||||
)
|
)
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"switch_off_colors",
|
"switch_off_colors",
|
||||||
ThemeConstants.DEFAULT_OFF_COLORS
|
ThemeConstants.DEFAULT_OFF_COLORS
|
||||||
)
|
)
|
||||||
|
|
||||||
if not text_color_ok:
|
if not text_color_ok:
|
||||||
self.__space_weather_labels.set(
|
self._space_weather_labels.set(
|
||||||
"text_color",
|
"text_color",
|
||||||
ThemeConstants.DEFAULT_TEXT_COLOR
|
ThemeConstants.DEFAULT_TEXT_COLOR
|
||||||
)
|
)
|
||||||
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,
|
current_theme.write(self._theme_path)
|
||||||
ThemeConstants.CURRENT
|
|
||||||
), "w") as current_theme:
|
|
||||||
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:
|
||||||
self.__theme_names[theme_name].setChecked(True)
|
self._theme_names[theme_name].setChecked(True)
|
||||||
except Exception:
|
except Exception:
|
||||||
pop_up(self.__parent, title=ThemeConstants.THEME_NOT_FOUND,
|
pop_up(self._owner, title=ThemeConstants.THEME_NOT_FOUND,
|
||||||
text=ThemeConstants.MISSING_THEME).show()
|
text=ThemeConstants.MISSING_THEME).show()
|
||||||
else:
|
else:
|
||||||
self.__apply(theme_path)
|
self._apply(theme_path)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self.__theme_names[
|
self._theme_names[
|
||||||
self.__pretty_name(ThemeConstants.DEFAULT)
|
self._pretty_name(ThemeConstants.DEFAULT)
|
||||||
].setChecked(True)
|
].setChecked(True)
|
||||||
except Exception:
|
except Exception:
|
||||||
pop_up(self.__parent, title=ThemeConstants.THEME_NOT_FOUND,
|
pop_up(self._owner, 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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|||||||
109
threads.py
109
threads.py
@@ -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,62 +24,88 @@ 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 to '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 = 512 * 1024
|
||||||
|
_MEGA = 1024**2
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Just call super().__init__."""
|
||||||
|
self._db = None
|
||||||
|
self._exit_call = False
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def __pretty_len(self, byte_obj):
|
def _pretty_len(self, byte_obj):
|
||||||
mega = len(byte_obj) / self.CHUNK
|
"""Return a well-formatted number of downloaded MB."""
|
||||||
|
mega = len(byte_obj) / self._MEGA
|
||||||
if mega.is_integer():
|
if mega.is_integer():
|
||||||
return int(mega)
|
return int(mega)
|
||||||
else:
|
else:
|
||||||
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._MEGA) / delta, 2
|
||||||
2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_exit(self):
|
||||||
|
self._exit_call = True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
|
"""Override 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
|
||||||
|
self._db = None
|
||||||
raw_data = bytes(0)
|
raw_data = bytes(0)
|
||||||
try:
|
try:
|
||||||
db = urllib3.PoolManager().request(
|
self._db = urllib3.PoolManager().request(
|
||||||
'GET',
|
'GET',
|
||||||
Database.LINK_LOC,
|
Database.LINK_LOC,
|
||||||
preload_content=False
|
preload_content=False,
|
||||||
|
timeout=4.0
|
||||||
)
|
)
|
||||||
while True:
|
while True:
|
||||||
start = time()
|
start = time()
|
||||||
data = db.read(self.CHUNK)
|
try:
|
||||||
|
data = self._db.read(self._CHUNK)
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
delta = time() - start
|
delta = time() - start
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
raw_data += data
|
raw_data += data
|
||||||
self.progress.emit(
|
self.progress.emit(
|
||||||
self.__pretty_len(raw_data),
|
self._pretty_len(raw_data),
|
||||||
self.__get_download_speed(data, delta)
|
self._get_download_speed(data, delta)
|
||||||
)
|
)
|
||||||
db.release_conn()
|
if self._exit_call:
|
||||||
|
self._exit_call = False
|
||||||
|
self._db.release_conn()
|
||||||
|
return
|
||||||
except Exception: # No internet connection.
|
except Exception: # No internet connection.
|
||||||
db.release_conn()
|
self._db.release_conn()
|
||||||
self.status = ThreadStatus.NO_CONNECTION_ERR
|
self.status = ThreadStatus.NO_CONNECTION_ERR
|
||||||
return
|
return
|
||||||
if db.status != 200:
|
if self._db.status != 200:
|
||||||
self.status = ThreadStatus.BAD_DOWNLOAD_ERR
|
self.status = ThreadStatus.BAD_DOWNLOAD_ERR
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
@@ -102,42 +130,54 @@ 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."""
|
||||||
self.__space_weather_data.images[n].loadFromData(im)
|
im = await self._download_resource(
|
||||||
|
session, Constants.SPACE_WEATHER_IMGS[n]
|
||||||
|
)
|
||||||
|
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 = []
|
||||||
for p in self.__properties:
|
for p in self._PROPERTIES:
|
||||||
t.append(
|
t.append(
|
||||||
asyncio.create_task(self.__download_property(session, p))
|
asyncio.create_task(self._download_property(session, p))
|
||||||
)
|
)
|
||||||
|
|
||||||
tot_images = range(len(Constants.SPACE_WEATHER_IMGS))
|
tot_images = range(len(Constants.SPACE_WEATHER_IMGS))
|
||||||
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,41 +188,47 @@ 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(
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self.__download_property(
|
self._download_property(
|
||||||
session,
|
session,
|
||||||
Constants.SPACE_WEATHER_GEO_STORM,
|
Constants.SPACE_WEATHER_GEO_STORM,
|
||||||
self._PropertyName.FORECAST
|
self._PropertyName.FORECAST
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
asyncio.create_task(
|
asyncio.create_task(
|
||||||
self.__download_property(
|
self._download_property(
|
||||||
session,
|
session,
|
||||||
Constants.FORECAST_PROBABILITIES,
|
Constants.FORECAST_PROBABILITIES,
|
||||||
self._PropertyName.PROBABILITIES
|
self._PropertyName.PROBABILITIES
|
||||||
@@ -197,5 +243,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())
|
||||||
|
|||||||
52
utilities.py
52
utilities.py
@@ -8,23 +8,35 @@ 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:
|
||||||
base_path = os.path.abspath(".")
|
base_path = os.path.abspath(".")
|
||||||
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()
|
||||||
|
|
||||||
|
|
||||||
def pop_up(cls, title, text,
|
def pop_up(cls, title, text,
|
||||||
informative_text=None,
|
informative_text=None,
|
||||||
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)
|
||||||
@@ -38,7 +50,9 @@ def pop_up(cls, title, text,
|
|||||||
msg.adjustSize()
|
msg.adjustSize()
|
||||||
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:
|
||||||
@@ -56,7 +70,11 @@ def checksum_ok(data, what):
|
|||||||
raise
|
raise
|
||||||
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))
|
||||||
@@ -64,22 +82,30 @@ def connect_events_to_func(events_to_connect, fun_to_connect, fun_args):
|
|||||||
for event in events_to_connect:
|
for event in events_to_connect:
|
||||||
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):
|
|
||||||
digits = len(num)
|
def _change_unit(str_num):
|
||||||
|
"""Return a scale factor given 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:
|
||||||
@@ -89,14 +115,16 @@ def _change_unit(num):
|
|||||||
else:
|
else:
|
||||||
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 +137,19 @@ 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, default=-1):
|
||||||
|
"""Call 'cast_type(value)' and return the result.
|
||||||
|
|
||||||
|
If the operation fails return 'default'.
|
||||||
|
Should be used to perform 'safe casts'.
|
||||||
|
Keyword argument:
|
||||||
|
default -- default value returned if the cast fails.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
r = cast_type(value)
|
||||||
|
except Exception:
|
||||||
|
r = default
|
||||||
|
finally:
|
||||||
|
return r
|
||||||
|
|||||||
340
weatherdata.py
340
weatherdata.py
@@ -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 = ''
|
||||||
@@ -90,7 +113,105 @@ class SpaceWeatherData(_BaseWeatherData):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _make_labels_table(forecast, probabilities, rows):
|
||||||
|
"""Organize all the arguments to feed _get_lbl_value."""
|
||||||
|
def get_first_split(x):
|
||||||
|
return x.split("/")[0]
|
||||||
|
|
||||||
|
def get_second_split(x):
|
||||||
|
return x.split("/")[1]
|
||||||
|
|
||||||
|
def get_third_split(x):
|
||||||
|
return x.split("/")[2]
|
||||||
|
|
||||||
|
solar_row = rows["solar_row"]
|
||||||
|
event_row = rows["event_row"]
|
||||||
|
rb_now_row = rows["rb_now_row"]
|
||||||
|
ga_now_row = rows["ga_now_row"]
|
||||||
|
kp_index_row = rows["kp_index_row"]
|
||||||
|
return [
|
||||||
|
[
|
||||||
|
[forecast, solar_row, 3, None],
|
||||||
|
[probabilities, event_row + 1, 2, get_first_split],
|
||||||
|
[probabilities, event_row + 2, 2, get_first_split],
|
||||||
|
[probabilities, event_row + 3, 1, get_first_split],
|
||||||
|
[forecast, rb_now_row, 1, None],
|
||||||
|
[forecast, rb_now_row + 1, 3, None],
|
||||||
|
[probabilities, ga_now_row + 2, 1, get_first_split],
|
||||||
|
[probabilities, ga_now_row + 3, 2, get_first_split],
|
||||||
|
[probabilities, ga_now_row + 4, 2, get_first_split],
|
||||||
|
[probabilities, ga_now_row + 6, 1, get_first_split],
|
||||||
|
[probabilities, ga_now_row + 7, 2, get_first_split],
|
||||||
|
[probabilities, ga_now_row + 8, 2, get_first_split],
|
||||||
|
[forecast, kp_index_row + 3, 1, None],
|
||||||
|
[forecast, kp_index_row + 4, 1, None],
|
||||||
|
[forecast, kp_index_row + 5, 1, None],
|
||||||
|
[forecast, kp_index_row + 6, 1, None],
|
||||||
|
[forecast, kp_index_row + 7, 1, None],
|
||||||
|
[forecast, kp_index_row + 8, 1, None],
|
||||||
|
[forecast, kp_index_row + 9, 1, None],
|
||||||
|
[forecast, kp_index_row + 10, 1, None]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[forecast, solar_row, 4, None],
|
||||||
|
[probabilities, event_row + 1, 2, get_second_split],
|
||||||
|
[probabilities, event_row + 2, 2, get_second_split],
|
||||||
|
[probabilities, event_row + 3, 1, get_second_split],
|
||||||
|
[forecast, rb_now_row, 2, None],
|
||||||
|
[forecast, rb_now_row + 1, 4, None],
|
||||||
|
[probabilities, ga_now_row + 2, 1, get_second_split],
|
||||||
|
[probabilities, ga_now_row + 3, 2, get_second_split],
|
||||||
|
[probabilities, ga_now_row + 4, 2, get_second_split],
|
||||||
|
[probabilities, ga_now_row + 6, 1, get_second_split],
|
||||||
|
[probabilities, ga_now_row + 7, 2, get_second_split],
|
||||||
|
[probabilities, ga_now_row + 8, 2, get_second_split],
|
||||||
|
[forecast, kp_index_row + 3, 2, None],
|
||||||
|
[forecast, kp_index_row + 4, 2, None],
|
||||||
|
[forecast, kp_index_row + 5, 2, None],
|
||||||
|
[forecast, kp_index_row + 6, 2, None],
|
||||||
|
[forecast, kp_index_row + 7, 2, None],
|
||||||
|
[forecast, kp_index_row + 8, 2, None],
|
||||||
|
[forecast, kp_index_row + 9, 2, None],
|
||||||
|
[forecast, kp_index_row + 10, 2, None]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
[forecast, solar_row, 5, None],
|
||||||
|
[probabilities, event_row + 1, 2, get_third_split],
|
||||||
|
[probabilities, event_row + 2, 2, get_third_split],
|
||||||
|
[probabilities, event_row + 3, 1, get_third_split],
|
||||||
|
[forecast, rb_now_row, 3, None],
|
||||||
|
[forecast, rb_now_row + 1, 5, None],
|
||||||
|
[probabilities, ga_now_row + 2, 1, get_third_split],
|
||||||
|
[probabilities, ga_now_row + 3, 2, get_third_split],
|
||||||
|
[probabilities, ga_now_row + 4, 2, get_third_split],
|
||||||
|
[probabilities, ga_now_row + 6, 1, get_third_split],
|
||||||
|
[probabilities, ga_now_row + 7, 2, get_third_split],
|
||||||
|
[probabilities, ga_now_row + 8, 2, get_third_split],
|
||||||
|
[forecast, kp_index_row + 3, 3, None],
|
||||||
|
[forecast, kp_index_row + 4, 3, None],
|
||||||
|
[forecast, kp_index_row + 5, 3, None],
|
||||||
|
[forecast, kp_index_row + 6, 3, None],
|
||||||
|
[forecast, kp_index_row + 7, 3, None],
|
||||||
|
[forecast, kp_index_row + 8, 3, None],
|
||||||
|
[forecast, kp_index_row + 9, 3, None],
|
||||||
|
[forecast, kp_index_row + 10, 3, None]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lbl_value(data, row, col, f=None):
|
||||||
|
"""Return the well-formatted string-value of the label."""
|
||||||
|
val = data[row][col]
|
||||||
|
if f is not None:
|
||||||
|
val = f(val)
|
||||||
|
val = val.rstrip('%')
|
||||||
|
if len(val) > 1:
|
||||||
|
val = val.lstrip('0')
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
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",
|
||||||
@@ -100,49 +221,44 @@ class ForecastData(_BaseWeatherData):
|
|||||||
"kp_index_row": "NOAA Kp index breakdown"
|
"kp_index_row": "NOAA Kp index breakdown"
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, parent):
|
LABELS_PER_COLUMN = 20
|
||||||
|
|
||||||
|
def __init__(self, owner):
|
||||||
|
"""Initialize all attributes and connect the thread to _parse_and_emit_signal."""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.forecast = ''
|
self.forecast = []
|
||||||
self.probabilities = ''
|
self.probabilities = []
|
||||||
self.__labels_table = []
|
|
||||||
self.__solar_row = None
|
|
||||||
self.__event_row = None
|
|
||||||
self.__rb_now_row = None
|
|
||||||
self.__ga_now_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
|
self._today_lbl = owner.today_lbl
|
||||||
self.today_p1_lbl = parent.today_p1_lbl
|
self._today_p1_lbl = owner.today_p1_lbl
|
||||||
self.today_p2_lbl = parent.today_p2_lbl
|
self._today_p2_lbl = owner.today_p2_lbl
|
||||||
self.__today_lbls = []
|
today_lbls = []
|
||||||
self.__today_p1_lbls = []
|
today_p1_lbls = []
|
||||||
self.__today_p2_lbls = []
|
today_p2_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(self.LABELS_PER_COLUMN):
|
||||||
label = getattr(
|
label = getattr(
|
||||||
parent,
|
owner,
|
||||||
"forecast_today_" + flag + str(index) + "_lbl"
|
"forecast_today_" + flag + str(index) + "_lbl"
|
||||||
)
|
)
|
||||||
label.setText(Constants.UNKNOWN)
|
label.setText(Constants.UNKNOWN)
|
||||||
if flag == flags[0]:
|
if flag == flags[0]:
|
||||||
self.__today_lbls.append(label)
|
today_lbls.append(label)
|
||||||
if flag == flags[1]:
|
if flag == flags[1]:
|
||||||
self.__today_p1_lbls.append(label)
|
today_p1_lbls.append(label)
|
||||||
if flag == flags[2]:
|
if flag == flags[2]:
|
||||||
self.__today_p2_lbls.append(label)
|
today_p2_lbls.append(label)
|
||||||
|
|
||||||
self.__all_lbls = [
|
self._all_lbls = [today_lbls, today_p1_lbls, today_p2_lbls]
|
||||||
self.__today_lbls,
|
|
||||||
self.__today_p1_lbls,
|
|
||||||
self.__today_p2_lbls
|
|
||||||
]
|
|
||||||
|
|
||||||
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
|
||||||
@@ -153,167 +269,85 @@ class ForecastData(_BaseWeatherData):
|
|||||||
)
|
)
|
||||||
self.probabilities = self.probabilities.splitlines()
|
self.probabilities = self.probabilities.splitlines()
|
||||||
|
|
||||||
def __split_lists(self):
|
def _split_lists(self):
|
||||||
self.forecast = [i.split() for i in self.forecast]
|
"""Split the elements of forecast and probabilities."""
|
||||||
self.probabilities = [i.split() for i in self.probabilities]
|
return [i.split() for i in self.forecast], [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):
|
||||||
self.__solar_row = self.__find_row_with(
|
"""Get all the rows needed for updating the screen.
|
||||||
|
|
||||||
|
Raise an exception if something goes wrong."""
|
||||||
|
|
||||||
|
rows = {}
|
||||||
|
rows["solar_row"] = self._find_row_with(
|
||||||
self.forecast,
|
self.forecast,
|
||||||
self.ROW_KEYWORDS["solar_row"]
|
self.ROW_KEYWORDS["solar_row"]
|
||||||
)
|
)
|
||||||
self.__event_row = self.__find_row_with(
|
rows["event_row"] = self._find_row_with(
|
||||||
self.probabilities,
|
self.probabilities,
|
||||||
self.ROW_KEYWORDS["event_row"]
|
self.ROW_KEYWORDS["event_row"]
|
||||||
)
|
)
|
||||||
self.__rb_now_row = self.__find_row_with(
|
rows["rb_now_row"] = self._find_row_with(
|
||||||
self.forecast,
|
self.forecast,
|
||||||
self.ROW_KEYWORDS["rb_now_row"]
|
self.ROW_KEYWORDS["rb_now_row"]
|
||||||
)
|
)
|
||||||
self.__ga_now_row = self.__find_row_with(
|
rows["ga_now_row"] = self._find_row_with(
|
||||||
self.probabilities,
|
self.probabilities,
|
||||||
self.ROW_KEYWORDS["ga_now_row"]
|
self.ROW_KEYWORDS["ga_now_row"]
|
||||||
)
|
)
|
||||||
self.__kp_index_row = self.__find_row_with(
|
rows["kp_index_row"] = self._find_row_with(
|
||||||
self.forecast,
|
self.forecast,
|
||||||
self.ROW_KEYWORDS["kp_index_row"]
|
self.ROW_KEYWORDS["kp_index_row"]
|
||||||
)
|
)
|
||||||
|
|
||||||
is_none = lambda x: x is None
|
if any(row is None for row in rows.values()):
|
||||||
if any([
|
|
||||||
is_none(self.__solar_row),
|
|
||||||
is_none(self.__event_row),
|
|
||||||
is_none(self.__rb_now_row),
|
|
||||||
is_none(self.__ga_now_row),
|
|
||||||
is_none(self.__kp_index_row)
|
|
||||||
]):
|
|
||||||
raise Exception('Missing Rows')
|
raise Exception('Missing Rows')
|
||||||
|
|
||||||
def __set_dates(self):
|
|
||||||
month = self.forecast[self.__solar_row - 1][0]
|
|
||||||
today = self.forecast[self.__solar_row - 1][1]
|
|
||||||
today_p1 = self.forecast[self.__solar_row - 1][3]
|
|
||||||
today_p2 = self.forecast[self.__solar_row - 1][5]
|
|
||||||
self.today_lbl.setText(month + ' ' + today)
|
|
||||||
self.today_p1_lbl.setText(month + ' ' + today_p1)
|
|
||||||
self.today_p2_lbl.setText(month + ' ' + today_p2)
|
|
||||||
|
|
||||||
def __make_labels_table(self):
|
|
||||||
get_first_split = lambda x: x.split("/")[0]
|
|
||||||
get_second_split = lambda x: x.split("/")[1]
|
|
||||||
get_third_split = lambda x: x.split("/")[2]
|
|
||||||
self.__labels_table = [
|
|
||||||
[
|
|
||||||
[self.forecast, self.__solar_row, 3, None],
|
|
||||||
[self.probabilities, self.__event_row + 1, 2, get_first_split],
|
|
||||||
[self.probabilities, self.__event_row + 2, 2, get_first_split],
|
|
||||||
[self.probabilities, self.__event_row + 3, 1, get_first_split],
|
|
||||||
[self.forecast, self.__rb_now_row, 1, None],
|
|
||||||
[self.forecast, self.__rb_now_row + 1, 3, None],
|
|
||||||
[self.probabilities, self.__ga_now_row + 2, 1, get_first_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 3, 2, get_first_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 4, 2, get_first_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 6, 1, get_first_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 7, 2, get_first_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 8, 2, get_first_split],
|
|
||||||
[self.forecast, self.__kp_index_row + 3, 1, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 4, 1, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 5, 1, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 6, 1, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 7, 1, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 8, 1, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 9, 1, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 10, 1, None]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[self.forecast, self.__solar_row, 4, None],
|
|
||||||
[self.probabilities, self.__event_row + 1, 2, get_second_split],
|
|
||||||
[self.probabilities, self.__event_row + 2, 2, get_second_split],
|
|
||||||
[self.probabilities, self.__event_row + 3, 1, get_second_split],
|
|
||||||
[self.forecast, self.__rb_now_row, 2, None],
|
|
||||||
[self.forecast, self.__rb_now_row + 1, 4, None],
|
|
||||||
[self.probabilities, self.__ga_now_row + 2, 1, get_second_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 3, 2, get_second_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 4, 2, get_second_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 6, 1, get_second_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 7, 2, get_second_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 8, 2, get_second_split],
|
|
||||||
[self.forecast, self.__kp_index_row + 3, 2, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 4, 2, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 5, 2, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 6, 2, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 7, 2, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 8, 2, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 9, 2, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 10, 2, None]
|
|
||||||
],
|
|
||||||
[
|
|
||||||
[self.forecast, self.__solar_row, 5, None],
|
|
||||||
[self.probabilities, self.__event_row + 1, 2, get_third_split],
|
|
||||||
[self.probabilities, self.__event_row + 2, 2, get_third_split],
|
|
||||||
[self.probabilities, self.__event_row + 3, 1, get_third_split],
|
|
||||||
[self.forecast, self.__rb_now_row, 3, None],
|
|
||||||
[self.forecast, self.__rb_now_row + 1, 5, None],
|
|
||||||
[self.probabilities, self.__ga_now_row + 2, 1, get_third_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 3, 2, get_third_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 4, 2, get_third_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 6, 1, get_third_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 7, 2, get_third_split],
|
|
||||||
[self.probabilities, self.__ga_now_row + 8, 2, get_third_split],
|
|
||||||
[self.forecast, self.__kp_index_row + 3, 3, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 4, 3, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 5, 3, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 6, 3, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 7, 3, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 8, 3, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 9, 3, None],
|
|
||||||
[self.forecast, self.__kp_index_row + 10, 3, None]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
def __get_lbl_value(self, data, row, col, f = None):
|
|
||||||
val = data[row][col]
|
|
||||||
if f is not None:
|
|
||||||
val = f(val)
|
|
||||||
val = val.lstrip('0').rstrip('%')
|
|
||||||
return val
|
|
||||||
|
|
||||||
def __is_integer(self, s):
|
|
||||||
try:
|
|
||||||
int(s)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
else:
|
else:
|
||||||
return True
|
return rows
|
||||||
|
|
||||||
def __set_labels_values(self):
|
def _set_dates(self, forecast, solar_row):
|
||||||
for lbl_list, table in zip(self.__all_lbls, self.__labels_table):
|
"""Set the date labels."""
|
||||||
|
month = forecast[solar_row - 1][0]
|
||||||
|
today = forecast[solar_row - 1][1]
|
||||||
|
today_p1 = forecast[solar_row - 1][3]
|
||||||
|
today_p2 = forecast[solar_row - 1][5]
|
||||||
|
self._today_lbl.setText(month + ' ' + today)
|
||||||
|
self._today_p1_lbl.setText(month + ' ' + today_p1)
|
||||||
|
self._today_p2_lbl.setText(month + ' ' + today_p2)
|
||||||
|
|
||||||
|
def _set_labels_values(self, labels_table):
|
||||||
|
"""Set all the labels values."""
|
||||||
|
for lbl_list, table in zip(self._all_lbls, 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 = _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()
|
rows = self._get_rows()
|
||||||
self.__split_lists()
|
forecast, probabilities = self._split_lists()
|
||||||
self.__make_labels_table()
|
labels_table = _make_labels_table(forecast, probabilities, rows)
|
||||||
self.__set_dates()
|
self._set_dates(forecast, rows["solar_row"])
|
||||||
self.__set_labels_values()
|
self._set_labels_values(labels_table)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def remove_data(self):
|
def remove_data(self):
|
||||||
self.forecast = ''
|
"""Remove the reference to the downloaded data."""
|
||||||
self.probabilities = ''
|
self.forecast = []
|
||||||
|
self.probabilities = []
|
||||||
|
|||||||
Reference in New Issue
Block a user