Example #1
0
class Artemis(QMainWindow, Ui_MainWindow):
    """Main application class."""

    def __init__(self):
        """Set all connections of the application."""
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle("ARTĪ£MIS " + __VERSION__)
        self.set_initial_size()
        self.closing = False
        self.download_window = DownloadWindow()
        self.download_window.complete.connect(self.show_downloaded_signals)
        self.actionExit.triggered.connect(qApp.quit)
        self.action_update_database.triggered.connect(self.ask_if_download)
        self.action_check_db_ver.triggered.connect(self.check_db_ver)
        self.action_sigidwiki_com.triggered.connect(
            lambda: webbrowser.open(Constants.SIGIDWIKI)
        )
        self.action_add_a_signal.triggered.connect(
            lambda: webbrowser.open(Constants.ADD_SIGNAL_LINK)
        )
        self.action_aresvalley_com.triggered.connect(
            lambda: webbrowser.open(Constants.ARESVALLEY_LINK)
        )
        self.action_forum.triggered.connect(
            lambda: webbrowser.open(Constants.FORUM_LINK)
        )
        self.action_rtl_sdr_com.triggered.connect(
            lambda: webbrowser.open(Constants.RTL_SDL_LINK)
        )
        self.db = None
        self.current_signal_name = ''
        self.signal_names = []
        self.total_signals = 0

        # Forecast
        self.forecast_info_btn.clicked.connect(
            lambda: webbrowser.open(Constants.SPACE_WEATHER_INFO)
        )
        self.forecast_data = ForecastData(self)
        self.update_forecast_bar.clicked.connect(self.start_update_forecast)
        self.update_forecast_bar.set_idle()
        self.forecast_data.update_complete.connect(self.update_forecast)

        # Spaceweather manager
        self.spaceweather_screen = SpaceWeatherManager(self)

        self.theme_manager = ThemeManager(self)

        self.filters = Filters(self)

# #######################################################################################

        UrlColors = namedtuple("UrlColors", ["inactive", "active", "clicked"])
        self.url_button.colors = UrlColors("#9f9f9f", "#4c75ff", "#942ccc")
        self.category_labels = [
            self.cat_mil,
            self.cat_rad,
            self.cat_active,
            self.cat_inactive,
            self.cat_ham,
            self.cat_comm,
            self.cat_avi,
            self.cat_mar,
            self.cat_ana,
            self.cat_dig,
            self.cat_trunked,
            self.cat_utility,
            self.cat_sat,
            self.cat_navi,
            self.cat_interf,
            self.cat_num_stat,
            self.cat_time_sig
        ]

        self.property_labels = [
            self.freq_lab,
            self.band_lab,
            self.mode_lab,
            self.modul_lab,
            self.loc_lab,
            self.acf_lab,
            self.description_text
        ]

        self.url_button.clicked.connect(self.go_to_web_page_signal)

        # GFD
        self.freq_search_gfd_btn.clicked.connect(partial(self.go_to_gfd, GfdType.FREQ))
        self.location_search_gfd_btn.clicked.connect(partial(self.go_to_gfd, GfdType.LOC))
        self.gfd_line_edit.returnPressed.connect(partial(self.go_to_gfd, GfdType.LOC))

# ##########################################################################################

        # Left list widget and search bar.
        self.search_bar.textChanged.connect(self.display_signals)
        self.signals_list.currentItemChanged.connect(self.display_specs)
        self.signals_list.itemDoubleClicked.connect(self.set_visible_tab)

        self.audio_widget = AudioPlayer(
            self.play,
            self.pause,
            self.stop,
            self.volume,
            self.loop,
            self.audio_progress,
            self.active_color,
            self.inactive_color
        )

        BandLabel = namedtuple("BandLabel", ["left", "center", "right"])
        self.band_labels = [
            BandLabel(self.elf_left, self.elf, self.elf_right),
            BandLabel(self.slf_left, self.slf, self.slf_right),
            BandLabel(self.ulf_left, self.ulf, self.ulf_right),
            BandLabel(self.vlf_left, self.vlf, self.vlf_right),
            BandLabel(self.lf_left, self.lf, self.lf_right),
            BandLabel(self.mf_left, self.mf, self.mf_right),
            BandLabel(self.hf_left, self.hf, self.hf_right),
            BandLabel(self.vhf_left, self.vhf, self.vhf_right),
            BandLabel(self.uhf_left, self.uhf, self.uhf_right),
            BandLabel(self.shf_left, self.shf, self.shf_right),
            BandLabel(self.ehf_left, self.ehf, self.ehf_right),
        ]

        self.main_tab.currentChanged.connect(self.hide_show_right_widget)

# Final operations.
        self.theme_manager.start()
        self.load_db()
        self.display_signals()

    @pyqtSlot()
    def hide_show_right_widget(self):
        if self.main_tab.currentWidget() == self.forecast_tab:
            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()
    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:
            self.update_forecast_bar.set_updating()
            self.forecast_data.update()

    @pyqtSlot(bool)
    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()
        if status_ok:
            self.forecast_data.update_all_labels()
        elif not self.closing:
            pop_up(self, title=Messages.BAD_DOWNLOAD,
                   text=Messages.BAD_DOWNLOAD_MSG).show()
        self.forecast_data.remove_data()

    @pyqtSlot()
    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="
        if by is GfdType.FREQ:
            value_in_mhz = self.freq_gfd.value() \
                * Constants.CONVERSION_FACTORS[self.unit_freq_gfd.currentText()] \
                / Constants.CONVERSION_FACTORS["MHz"]
            query += str(value_in_mhz)
        elif by is GfdType.LOC:
            query += self.gfd_line_edit.text()
        try:
            webbrowser.open(Constants.GFD_SITE + query.lower())
        except Exception:
            pass

    def set_initial_size(self):
        """Handle high resolution screens.

        Set bigger sizes for all the relevant fixed-size widgets.
        """
        d = QDesktopWidget().availableGeometry()
        w = d.width()
        h = d.height()
        self.showMaximized()

        if w > 3000 or h > 2000:
            self.fixed_audio_and_image.setFixedSize(540, 1150)
            self.fixed_audio_and_image.setMaximumSize(540, 1150)
            audio_btn_h, audio_btn_w = 90, 90
            self.play.setFixedSize(audio_btn_h, audio_btn_w)
            self.pause.setFixedSize(audio_btn_h, audio_btn_w)
            self.stop.setFixedSize(audio_btn_h, audio_btn_w)
            self.loop.setFixedSize(audio_btn_h, audio_btn_w)
            self.lower_freq_spinbox.setFixedWidth(200)
            self.upper_freq_spinbox.setFixedWidth(200)
            self.lower_freq_filter_unit.setFixedWidth(120)
            self.upper_freq_filter_unit.setFixedWidth(120)
            self.lower_freq_confidence.setFixedWidth(120)
            self.upper_freq_confidence.setFixedWidth(120)

            self.lower_band_spinbox.setFixedWidth(200)
            self.upper_band_spinbox.setFixedWidth(200)
            self.lower_band_filter_unit.setFixedWidth(120)
            self.upper_band_filter_unit.setFixedWidth(120)
            self.lower_band_confidence.setFixedWidth(120)
            self.upper_band_confidence.setFixedWidth(120)

            self.freq_gfd.setFixedWidth(200)
            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.volume.setStyleSheet("""
                QSlider::groove:horizontal {
                    height: 12px;
                    background: #7a7a7a;
                    margin: 0 10px;
                    border-radius: 6px
                }
                QSlider::handle:horizontal {
                    background: qlineargradient(x1:0, y1:0, x2:1, y2:1, stop:0 gray, stop:0.5 white, stop:1.0 gray);
                    border: 1px solid #5c5c5c;
                    width: 28px;
                    margin: -8px -8px;
                    border-radius: 14px;
                }
                """)

    @pyqtSlot()
    def download_db(self):
        """Start the database download.

        Do nothing if already downloading.
        """
        if not self.download_window.isVisible():
            self.download_window.start_download()
            self.download_window.show()

    @pyqtSlot()
    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():
            db_path = os.path.join(Constants.DATA_FOLDER, Database.NAME)
            try:
                with open(db_path, "rb") as file_db:
                    db = file_db.read()
            except Exception:
                self.download_db()
            else:
                try:
                    is_checksum_ok = checksum_ok(db, ChecksumWhat.DB)
                except Exception:
                    pop_up(self, title=Messages.NO_CONNECTION,
                           text=Messages.NO_CONNECTION_MSG).show()
                else:
                    if not is_checksum_ok:
                        self.download_db()
                    else:
                        answer = pop_up(self, title=Messages.DB_UP_TO_DATE,
                                        text=Messages.DB_UP_TO_DATE_MSG,
                                        informative_text=Messages.DOWNLOAD_ANYWAY_QUESTION,
                                        is_question=True,
                                        default_btn=QMessageBox.No).exec()
                        if answer == QMessageBox.Yes:
                            self.download_db()

    @pyqtSlot()
    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():
            db_path = os.path.join(Constants.DATA_FOLDER, Database.NAME)
            answer = None
            try:
                with open(db_path, "rb") as file_db:
                    db = file_db.read()
            except Exception:
                answer = pop_up(self, title=Messages.NO_DB,
                                text=Messages.NO_DB_AVAIL,
                                informative_text=Messages.DOWNLOAD_NOW_QUESTION,
                                is_question=True).exec()
                if answer == QMessageBox.Yes:
                    self.download_db()
            else:
                try:
                    is_checksum_ok = checksum_ok(db, ChecksumWhat.DB)
                except Exception:
                    pop_up(self, title=Messages.NO_CONNECTION,
                           text=Messages.NO_CONNECTION_MSG).show()
                else:
                    if is_checksum_ok:
                        pop_up(self, title=Messages.DB_UP_TO_DATE,
                               text=Messages.DB_UP_TO_DATE_MSG).show()
                    else:
                        answer = pop_up(self, title=Messages.DB_NEW_VER,
                                        text=Messages.DB_NEW_VER_MSG,
                                        informative_text=Messages.DOWNLOAD_NOW_QUESTION,
                                        is_question=True).exec()
                        if answer == QMessageBox.Yes:
                            self.download_db()

    @pyqtSlot()
    def show_downloaded_signals(self):
        """Load and display the database signal list."""
        self.search_bar.setEnabled(True)
        self.load_db()
        self.display_signals()

    def load_db(self):
        """Load the database from file.

        Populate the signals list and set the total number of signals.
        Handle possible missing file error.
        """
        try:
            self.db = read_csv(os.path.join(Constants.DATA_FOLDER, Database.NAME),
                               sep=Database.DELIMITER,
                               header=None,
                               index_col=0,
                               dtype={name: str for name in Database.STRINGS},
                               names=Database.NAMES)
        except FileNotFoundError:
            self.search_bar.setDisabled(True)
            answer = pop_up(self, title=Messages.NO_DB,
                            text=Messages.NO_DB_AVAIL,
                            informative_text=Messages.DOWNLOAD_NOW_QUESTION,
                            is_question=True).exec()
            if answer == QMessageBox.Yes:
                self.download_db()
        else:
            # Avoid a crash if there are duplicated signals
            self.db = self.db.groupby(level=0).first()
            self.signal_names = self.db.index
            self.total_signals = len(self.signal_names)
            self.db.fillna(Constants.UNKNOWN, inplace=True)
            self.db[Signal.ACF] = ACFValue.list_from_series(self.db[Signal.ACF])
            self.db[Signal.WIKI_CLICKED] = False
            self.update_status_tip(self.total_signals)
            self.signals_list.clear()
            self.signals_list.addItems(self.signal_names)
            self.signals_list.setCurrentItem(None)
            self.modulation_list.addItems(
                self.collect_list(
                    Signal.MODULATION
                )
            )
            self.locations_list.addItems(
                self.collect_list(
                    Signal.LOCATION
                )
            )

    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 = list(
            set([
                x.strip() for value in values[values != Constants.UNKNOWN]
                for x in value.split(separator)
            ])
        )
        values.sort()
        values.insert(0, Constants.UNKNOWN)
        return values

    @pyqtSlot()
    def activate_if_toggled(self, radio_btn, *widgets):
        """If radio_btn is toggled, activate all *widgets.

        Do nothing otherwise.
        """
        toggled = radio_btn.isChecked()
        for w in widgets[:-1]:  # Neglect the bool coming from the emitted signal.
            w.setEnabled(toggled)

    @pyqtSlot()
    def display_signals(self):
        """Display all the signal names which matches the applied filters."""
        text = self.search_bar.text()
        available_signals = 0
        for index, signal_name in enumerate(self.signal_names):
            if text.lower() in signal_name.lower() and self.filters.ok(signal_name):
                self.signals_list.item(index).setHidden(False)
                available_signals += 1
            else:
                self.signals_list.item(index).setHidden(True)
        # Remove selected item.
        self.signals_list.setCurrentItem(None)
        self.update_status_tip(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:
            self.statusbar.setStyleSheet(f'color: {self.active_color}')
        else:
            self.statusbar.setStyleSheet(f'color: {self.inactive_color}')
        self.statusbar.showMessage(
            f"{available_signals} out of {self.total_signals} signals displayed."
        )

    @pyqtSlot(QListWidgetItem, QListWidgetItem)
    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()
        if item is not None:
            self.current_signal_name = item.text()
            self.name_lab.setText(self.current_signal_name)
            self.name_lab.setAlignment(Qt.AlignHCenter)
            current_signal = self.db.loc[self.current_signal_name]
            self.url_button.setEnabled(True)
            if not current_signal.at[Signal.WIKI_CLICKED]:
                self.url_button.setStyleSheet(
                    f"color: {self.url_button.colors.active};"
                )
            else:
                self.url_button.setStyleSheet(
                    f"color: {self.url_button.colors.clicked};"
                )
            category_code = current_signal.at[Signal.CATEGORY_CODE]
            undef_freq = is_undef_freq(current_signal)
            undef_band = is_undef_band(current_signal)
            if not undef_freq:
                self.freq_lab.setText(
                    format_numbers(
                        current_signal.at[Signal.INF_FREQ],
                        current_signal.at[Signal.SUP_FREQ]
                    )
                )
            else:
                self.freq_lab.setText("Undefined")
            if not undef_band:
                self.band_lab.setText(
                    format_numbers(
                        current_signal.at[Signal.INF_BAND],
                        current_signal.at[Signal.SUP_BAND]
                    )
                )
            else:
                self.band_lab.setText("Undefined")

            self.mode_lab.setText(current_signal.at[Signal.MODE])
            self.modul_lab.setText(current_signal.at[Signal.MODULATION])
            self.loc_lab.setText(current_signal.at[Signal.LOCATION])
            self.acf_lab.setText(
                ACFValue.concat_strings(current_signal.at[Signal.ACF])
            )
            self.description_text.setText(current_signal.at[Signal.DESCRIPTION])
            for cat, cat_lab in zip(category_code, self.category_labels):
                if cat == '0':
                    cat_lab.setStyleSheet(f"color: {self.inactive_color};")
                elif cat == '1':
                    cat_lab.setStyleSheet(f"color: {self.active_color};")
            self.set_band_range(current_signal)
            self.audio_widget.set_audio_player(self.current_signal_name)
        else:
            self.url_button.setEnabled(False)
            self.url_button.setStyleSheet(
                f"color: {self.url_button.colors.inactive};"
            )
            self.current_signal_name = ''
            self.name_lab.setText("No Signal")
            self.name_lab.setAlignment(Qt.AlignHCenter)
            for lab in self.property_labels:
                lab.setText(Constants.UNKNOWN)
            for lab in self.category_labels:
                lab.setStyleSheet(f"color: {self.inactive_color};")
            self.set_band_range()
            self.audio_widget.set_audio_player()

    def display_spectrogram(self):
        """Display the selected signal's waterfall."""
        default_pic = Constants.DEFAULT_NOT_SELECTED
        item = self.signals_list.currentItem()
        if item:
            spectrogram_name = item.text()
            path_spectr = os.path.join(
                Constants.DATA_FOLDER,
                Constants.SPECTRA_FOLDER,
                spectrogram_name + Constants.SPECTRA_EXT
            )
            if not QFileInfo(path_spectr).exists():
                path_spectr = Constants.DEFAULT_NOT_AVAILABLE
        else:
            path_spectr = default_pic
        self.spectrogram.setPixmap(QPixmap(path_spectr))

    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
        for label in band_label:
            label.setStyleSheet(f"color: {color};")

    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):
            lower_freq = safe_cast(
                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))
            for i, w in enumerate(zipped):
                band, band_label = w
                if lower_freq >= band.lower and lower_freq < band.upper:
                    self.activate_band_category(band_label)
                    for uband, uband_label in zipped[i + 1:]:
                        if upper_freq > uband.lower:
                            self.activate_band_category(uband_label)
                        else:
                            self.activate_band_category(uband_label, False)
                    break
                else:
                    self.activate_band_category(band_label, False)
        else:
            for band_label in self.band_labels:
                self.activate_band_category(band_label, False)

    @pyqtSlot()
    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:
            self.url_button.setStyleSheet(
                f"color: {self.url_button.colors.clicked}"
            )
            webbrowser.open(self.db.at[self.current_signal_name, Signal.URL])
            self.db.at[self.current_signal_name, Signal.WIKI_CLICKED] = True

    def closeEvent(self, event):
        """Extends closeEvent of QMainWindow.

        Shutdown all active threads and close all open windows."""
        self.closing = True
        if self.download_window.isVisible():
            self.download_window.close()
        if self.space_weather_data.is_updating:
            self.space_weather_data.shutdown_thread()
        if self.forecast_data.is_updating:
            self.forecast_data.shutdown_thread()
        super().closeEvent(event)