diff --git a/CHANGELOG.md b/CHANGELOG.md
index 078c524..de78ba1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
The first release is [3.0.0] because this is actually the third major version (completely rewritten) of the software.
## [Unreleased]
+### Added
+- Add support for signals with multiple-value acf ([#9](https://github.com/AresValley/Artemis/pull/9)). This breaks the backward compatibility because the database changed structure.
+
### Fixed
- The audio buttons are of the same dimension also for high resolution screens ([#13](https://github.com/AresValley/Artemis/pull/13))
- An audio sample can be paused and a different one can be played without a program crash ([#12](https://github.com/AresValley/Artemis/pull/12))
diff --git a/src/acfvalue.py b/src/acfvalue.py
new file mode 100644
index 0000000..aff004a
--- /dev/null
+++ b/src/acfvalue.py
@@ -0,0 +1,56 @@
+from constants import Constants
+
+
+class ACFValue:
+ """Handle complex/multiple ACF values."""
+
+ def __init__(self, value):
+ """Given a string describing an acf value, build an object with the
+ following attrributes:
+ - is_numeric: whether the value is a number or a string;
+ - numeric_value: the numeric value (if any, zero otherwise);
+ - unknown: whether the value is unknown."""
+ if value == Constants.UNKNOWN:
+ self._value = value
+ self._description = ""
+ self._string = self._value
+ self.is_numeric = False
+ self.unknown = True
+ self.numeric_value = 0.0
+ else:
+ self.unknown = False
+ if Constants.ACF_SEPARATOR in value:
+ description, acf_value = value.split(Constants.ACF_SEPARATOR)
+ self._description = description
+ self._value = acf_value
+ self._string = f"{self._description}: {self._value}"
+ else:
+ self._description = ""
+ self._value = value
+ self._string = self._value
+ try:
+ self.numeric_value = float(self._value)
+ except Exception:
+ self.is_numeric = False
+ self.numeric_value = 0.0
+ else:
+ self.is_numeric = True
+ self._string += " ms"
+
+ @classmethod
+ def list_from_series(cls, series):
+ """Parse all acf values from the database.
+
+ Accept an iterable of ACFValues.
+ Return a list of lists of ACFValues."""
+ entries = []
+ for entry in series:
+ entries.append([
+ cls(value.strip()) for value in entry.split(Constants.FIELD_SEPARATOR)
+ ])
+ return entries
+
+ @staticmethod
+ def concat_strings(acf_list_values):
+ """Concatenate a list of ACFValues to be displayed."""
+ return '\n'.join(s._string for s in acf_list_values)
diff --git a/src/artemis.py b/src/artemis.py
index ab67473..65eec88 100644
--- a/src/artemis.py
+++ b/src/artemis.py
@@ -19,7 +19,7 @@ from PyQt5 import uic
from PyQt5.QtCore import (QFileInfo,
Qt,
pyqtSlot,)
-
+from acfvalue import ACFValue
from audio_player import AudioPlayer
from weatherdata import ForecastData
from download_window import DownloadWindow
@@ -410,10 +410,12 @@ class Artemis(QMainWindow, Ui_MainWindow):
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()
@@ -530,7 +532,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
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(current_signal.at[Signal.ACF])
+ 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':
diff --git a/src/artemis.ui b/src/artemis.ui
index bdcf1c7..7297fdc 100644
--- a/src/artemis.ui
+++ b/src/artemis.ui
@@ -4537,9 +4537,9 @@ Inactive
ACF
-
+
-
-
+
-
-
@@ -4631,6 +4631,53 @@ Inactive
+ -
+
+
-
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+
+ 12
+ 75
+ true
+
+
+
+ Include variable ACF
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
-
-
diff --git a/src/constants.py b/src/constants.py
index e875826..30bc4b7 100644
--- a/src/constants.py
+++ b/src/constants.py
@@ -88,7 +88,8 @@ class Database:
Signal.MODE,
Signal.INF_BAND,
Signal.SUP_BAND,
- Signal.CATEGORY_CODE)
+ Signal.CATEGORY_CODE,
+ Signal.ACF,)
class ForecastColors:
@@ -181,6 +182,7 @@ class Constants:
NOT_AVAILABLE = "spectrumnotavailable.png"
NOT_SELECTED = "nosignalselected.png"
FIELD_SEPARATOR = ";"
+ ACF_SEPARATOR = " - "
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)
diff --git a/src/filters.py b/src/filters.py
index fff8f0f..3a16c1c 100644
--- a/src/filters.py
+++ b/src/filters.py
@@ -5,7 +5,6 @@ The only class exposed is Filters which provides the following methods:
- reset(): to reset all the applied filters;
- refresh(): used when the theme is changed."""
-from collections import namedtuple
from functools import partial
import webbrowser
@@ -729,6 +728,7 @@ class ACFFilter(_BaseFilter):
self.apply_remove_btn.set_slave_filters(
simple_ones=[
self._owner.include_undef_acf,
+ self._owner.include_variable_acf,
self._owner.acf_spinbox,
self._owner.acf_confidence
]
@@ -761,6 +761,8 @@ class ACFFilter(_BaseFilter):
uncheck_and_emit(self.apply_remove_btn)
if self._owner.include_undef_acf.isChecked():
self._owner.include_undef_acf.setChecked(False)
+ if self._owner.include_variable_acf.isChecked():
+ self._owner.include_variable_acf.setChecked(False)
self._owner.acf_spinbox.setValue(50)
self._owner.acf_confidence.setValue(0)
@@ -768,21 +770,23 @@ class ACFFilter(_BaseFilter):
"""Evalaute if the signal matches the acf filters."""
if not self.apply_remove_btn.isChecked():
return True
- signal_acf = self._owner.db.at[signal_name, Signal.ACF]
- if signal_acf == Constants.UNKNOWN:
+ signal_acf_list = self._owner.db.at[signal_name, Signal.ACF]
+ if signal_acf_list[0].unknown: # Unknown acf are the only acf of the signal.
if self._owner.include_undef_acf.isChecked():
return True
else:
return False
else:
- signal_acf = safe_cast(signal_acf.rstrip("ms"), float)
tolerance = self._owner.acf_spinbox.value() * self._owner.acf_confidence.value() / 100
upper_limit = self._owner.acf_spinbox.value() + tolerance
lower_limit = self._owner.acf_spinbox.value() - tolerance
- if signal_acf <= upper_limit and signal_acf >= lower_limit:
- return True
- else:
- return False
+ for v in signal_acf_list:
+ if v.is_numeric:
+ if lower_limit <= v.numeric_value <= upper_limit:
+ return True
+ elif self._owner.include_variable_acf.isChecked():
+ return True
+ return False
def refresh(self):
"""Extend _BaseFilter.refresh."""
@@ -794,57 +798,48 @@ class Filters(QObject):
"""Global filter class.
Provides the information about all the filters. Its only public attribute
- is filters, which is a namedtuple containing instances of all the filters.
+ is filters, which is a dictionary containing instances of all the filters.
The only exposed methods are reset(), ok(signal_name) and refresh().
The class also connects the apply and reset buttons to the relevant functions."""
- _FiltersTuple = namedtuple(
- "_FiltersTuple",
- [
- "freq_filter",
- "band_filter",
- "cat_filter",
- "mode_filter",
- "modulation_filter",
- "location_filter",
- "acf_filter",
- ]
- )
-
def __init__(self, owner):
super().__init__()
- self.filters = self._FiltersTuple(
- FreqFilter(owner),
- BandFilter(owner),
- CatFilter(owner),
- ModeFilter(owner),
- ModulationFilter(owner),
- LocFilter(owner),
- ACFFilter(owner),
- )
+ self.filters = {
+ "freq_filter": FreqFilter(owner),
+ "band_filter": BandFilter(owner),
+ "cat_filter": CatFilter(owner),
+ "mode_filter": ModeFilter(owner),
+ "modulation_filter": ModulationFilter(owner),
+ "location_filter": LocFilter(owner),
+ "acf_filter": ACFFilter(owner),
+ }
self._owner = owner
- self._owner.reset_filters_btn.clicked.connect(self.reset)
+ self._owner.reset_filters_btn.clicked.connect(self._reset)
# Connect Apply and Reset buttons clicks to functions.
- for f in self.filters:
+ for f in self._values:
f.apply_remove_btn.clicked.connect(self._display_signals)
f.reset_btn.clicked.connect(f.reset)
+ @property
+ def _values(self):
+ return self.filters.values()
+
@pyqtSlot()
def _display_signals(self):
self._owner.display_signals()
@pyqtSlot()
- def reset(self):
+ def _reset(self):
"""Reset all the filters."""
- for f in self.filters:
+ for f in self._values:
f.reset()
def ok(self, signal_name):
"""Check whether all the filters are passed."""
- return all(f._ok(signal_name) for f in self.filters)
+ return all(f._ok(signal_name) for f in self._values)
def refresh(self):
"""Refresh the relevant widgets when changing theme."""
- for f in self.filters:
+ for f in self._values:
f.refresh()