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. The first release is [3.0.0] because this is actually the third major version (completely rewritten) of the software.
## [Unreleased] ## [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 ### Fixed
- The audio buttons are of the same dimension also for high resolution screens ([#13](https://github.com/AresValley/Artemis/pull/13)) - 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)) - 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, from PyQt5.QtCore import (QFileInfo,
Qt, Qt,
pyqtSlot,) pyqtSlot,)
from acfvalue import ACFValue
from audio_player import AudioPlayer from audio_player import AudioPlayer
from weatherdata import ForecastData from weatherdata import ForecastData
from download_window import DownloadWindow from download_window import DownloadWindow
@@ -410,10 +410,12 @@ class Artemis(QMainWindow, Ui_MainWindow):
if answer == QMessageBox.Yes: if answer == QMessageBox.Yes:
self.download_db() self.download_db()
else: else:
# Avoid a crash if there are duplicated signals
self.db = self.db.groupby(level=0).first() 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.ACF] = ACFValue.list_from_series(self.db[Signal.ACF])
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.signals_list.clear() self.signals_list.clear()
@@ -530,7 +532,9 @@ class Artemis(QMainWindow, Ui_MainWindow):
self.mode_lab.setText(current_signal.at[Signal.MODE]) self.mode_lab.setText(current_signal.at[Signal.MODE])
self.modul_lab.setText(current_signal.at[Signal.MODULATION]) self.modul_lab.setText(current_signal.at[Signal.MODULATION])
self.loc_lab.setText(current_signal.at[Signal.LOCATION]) 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]) self.description_text.setText(current_signal.at[Signal.DESCRIPTION])
for cat, cat_lab in zip(category_code, self.category_labels): for cat, cat_lab in zip(category_code, self.category_labels):
if cat == '0': if cat == '0':

View File

@@ -4537,9 +4537,9 @@ Inactive</string>
<attribute name="title"> <attribute name="title">
<string>ACF</string> <string>ACF</string>
</attribute> </attribute>
<layout class="QVBoxLayout" name="verticalLayout_22"> <layout class="QVBoxLayout" name="verticalLayout_21">
<item> <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> <item>
<layout class="QHBoxLayout" name="horizontalLayout_65"> <layout class="QHBoxLayout" name="horizontalLayout_65">
<item> <item>
@@ -4631,6 +4631,53 @@ Inactive</string>
</item> </item>
</layout> </layout>
</item> </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> <item>
<layout class="QHBoxLayout" name="horizontalLayout_67"> <layout class="QHBoxLayout" name="horizontalLayout_67">
<item> <item>

View File

@@ -88,7 +88,8 @@ class Database:
Signal.MODE, Signal.MODE,
Signal.INF_BAND, Signal.INF_BAND,
Signal.SUP_BAND, Signal.SUP_BAND,
Signal.CATEGORY_CODE) Signal.CATEGORY_CODE,
Signal.ACF,)
class ForecastColors: class ForecastColors:
@@ -181,6 +182,7 @@ class Constants:
NOT_AVAILABLE = "spectrumnotavailable.png" NOT_AVAILABLE = "spectrumnotavailable.png"
NOT_SELECTED = "nosignalselected.png" NOT_SELECTED = "nosignalselected.png"
FIELD_SEPARATOR = ";" FIELD_SEPARATOR = ";"
ACF_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_SELECTED = os.path.join(DEFAULT_IMGS_FOLDER, NOT_SELECTED)
DEFAULT_NOT_AVAILABLE = os.path.join(DEFAULT_IMGS_FOLDER, NOT_AVAILABLE) 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; - reset(): to reset all the applied filters;
- refresh(): used when the theme is changed.""" - refresh(): used when the theme is changed."""
from collections import namedtuple
from functools import partial from functools import partial
import webbrowser import webbrowser
@@ -729,6 +728,7 @@ class ACFFilter(_BaseFilter):
self.apply_remove_btn.set_slave_filters( self.apply_remove_btn.set_slave_filters(
simple_ones=[ simple_ones=[
self._owner.include_undef_acf, self._owner.include_undef_acf,
self._owner.include_variable_acf,
self._owner.acf_spinbox, self._owner.acf_spinbox,
self._owner.acf_confidence self._owner.acf_confidence
] ]
@@ -761,6 +761,8 @@ class ACFFilter(_BaseFilter):
uncheck_and_emit(self.apply_remove_btn) uncheck_and_emit(self.apply_remove_btn)
if self._owner.include_undef_acf.isChecked(): if self._owner.include_undef_acf.isChecked():
self._owner.include_undef_acf.setChecked(False) 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_spinbox.setValue(50)
self._owner.acf_confidence.setValue(0) self._owner.acf_confidence.setValue(0)
@@ -768,20 +770,22 @@ class ACFFilter(_BaseFilter):
"""Evalaute if the signal matches the acf filters.""" """Evalaute if the signal matches the acf filters."""
if not self.apply_remove_btn.isChecked(): if not self.apply_remove_btn.isChecked():
return True return True
signal_acf = self._owner.db.at[signal_name, Signal.ACF] signal_acf_list = self._owner.db.at[signal_name, Signal.ACF]
if signal_acf == Constants.UNKNOWN: if signal_acf_list[0].unknown: # Unknown acf are the only acf of the signal.
if self._owner.include_undef_acf.isChecked(): if self._owner.include_undef_acf.isChecked():
return True return True
else: else:
return False return False
else: else:
signal_acf = safe_cast(signal_acf.rstrip("ms"), float)
tolerance = self._owner.acf_spinbox.value() * self._owner.acf_confidence.value() / 100 tolerance = self._owner.acf_spinbox.value() * self._owner.acf_confidence.value() / 100
upper_limit = self._owner.acf_spinbox.value() + tolerance upper_limit = self._owner.acf_spinbox.value() + tolerance
lower_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 return True
else:
return False return False
def refresh(self): def refresh(self):
@@ -794,57 +798,48 @@ class Filters(QObject):
"""Global filter class. """Global filter class.
Provides the information about all the filters. Its only public attribute 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 only exposed methods are reset(), ok(signal_name) and refresh().
The class also connects the apply and reset buttons to the relevant functions.""" 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): def __init__(self, owner):
super().__init__() super().__init__()
self.filters = self._FiltersTuple( self.filters = {
FreqFilter(owner), "freq_filter": FreqFilter(owner),
BandFilter(owner), "band_filter": BandFilter(owner),
CatFilter(owner), "cat_filter": CatFilter(owner),
ModeFilter(owner), "mode_filter": ModeFilter(owner),
ModulationFilter(owner), "modulation_filter": ModulationFilter(owner),
LocFilter(owner), "location_filter": LocFilter(owner),
ACFFilter(owner), "acf_filter": ACFFilter(owner),
) }
self._owner = 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. # 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.apply_remove_btn.clicked.connect(self._display_signals)
f.reset_btn.clicked.connect(f.reset) f.reset_btn.clicked.connect(f.reset)
@property
def _values(self):
return self.filters.values()
@pyqtSlot() @pyqtSlot()
def _display_signals(self): def _display_signals(self):
self._owner.display_signals() self._owner.display_signals()
@pyqtSlot() @pyqtSlot()
def reset(self): def _reset(self):
"""Reset all the filters.""" """Reset all the filters."""
for f in self.filters: for f in self._values:
f.reset() f.reset()
def ok(self, signal_name): def ok(self, signal_name):
"""Check whether all the filters are passed.""" """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): def refresh(self):
"""Refresh the relevant widgets when changing theme.""" """Refresh the relevant widgets when changing theme."""
for f in self.filters: for f in self._values:
f.refresh() f.refresh()