Closes #9 Add support for complex/multiple ACF values
This commit is contained in:
@@ -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))
|
||||
|
||||
56
src/acfvalue.py
Normal file
56
src/acfvalue.py
Normal file
@@ -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)
|
||||
@@ -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':
|
||||
|
||||
@@ -4537,9 +4537,9 @@ Inactive</string>
|
||||
<attribute name="title">
|
||||
<string>ACF</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_22">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_21">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_21" stretch="0,0,1,0,0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,1,0,0">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_65">
|
||||
<item>
|
||||
@@ -4631,6 +4631,53 @@ Inactive</string>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_70">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_67">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="include_variable_acf">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>12</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Include variable ACF</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_68">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_67">
|
||||
<item>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,20 +770,22 @@ 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:
|
||||
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
|
||||
else:
|
||||
return False
|
||||
|
||||
def refresh(self):
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user