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.
|
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
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,
|
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':
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user