Closes #9 Add support for complex/multiple ACF values

This commit is contained in:
Alessandro
2019-09-01 18:54:24 +02:00
parent 4a54ef54cb
commit 1509e04c93
6 changed files with 149 additions and 42 deletions

View File

@@ -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
View 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)

View File

@@ -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':

View File

@@ -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>

View File

@@ -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)

View File

@@ -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()