From 1f7673e32822197df3ef0ae4406636ead200baa7 Mon Sep 17 00:00:00 2001 From: Jakob Ketterl Date: Thu, 11 May 2023 23:19:35 +0200 Subject: [PATCH] implement advanced filtering --- owrx/active/list/__init__.py | 67 ++++++++++++++----- owrx/sdr.py | 59 +++++++++------- test/owrx/active/list/test_advanced_filter.py | 34 ++++++++++ 3 files changed, 121 insertions(+), 39 deletions(-) create mode 100644 test/owrx/active/list/test_advanced_filter.py diff --git a/owrx/active/list/__init__.py b/owrx/active/list/__init__.py index ca7ca39b..7f18a239 100644 --- a/owrx/active/list/__init__.py +++ b/owrx/active/list/__init__.py @@ -65,6 +65,26 @@ class BasicTransformation(ActiveListTransformation): return self.transformation(value) +class ActiveListFilter(ABC): + @abstractmethod + def predicate(self, value) -> bool: + pass + + def monitor(self, member, callback: callable): + pass + + def unmonitor(self, member): + pass + + +class BasicFilter(ActiveListFilter): + def __init__(self, predicate: callable): + self.predicate = predicate + + def predicate(self, value) -> bool: + return self.predicate(value) + + class ActiveListTransformationListener(ActiveListListener): def __init__(self, transformation: ActiveListTransformation, source: "ActiveList", target: "ActiveList"): self.transformation = transformation @@ -92,33 +112,51 @@ class ActiveListTransformationListener(ActiveListListener): class ActiveListFilterListener(ActiveListListener): - def __init__(self, filter: callable, keyMap: list, target: "ActiveList"): + def __init__(self, filter: ActiveListFilter, source: "ActiveList", target: "ActiveList"): self.filter = filter - self.keyMap = keyMap + self.source = source + self.keyMap = [idx for idx, val in enumerate(self.source) if self.filter.predicate(val)] + for v in self.source: + self.filter.monitor(v, partial(self._onMonitor, v)) self.target = target def onListChange(self, source: "ActiveList", changes: list[ActiveListChange]): for change in changes: if isinstance(change, ActiveListIndexAdded): - if self.filter(change.newValue): + if self.filter.predicate(change.newValue): idx = len([x for x in self.keyMap if x < change.index]) - self.target.insert(idx, change.newValue) self.keyMap.insert(idx, change.index) + self.target.insert(idx, change.newValue) + self.filter.monitor(change.newValue, partial(self._onMonitor, change.newValue)) elif isinstance(change, ActiveListIndexUpdated): - if change.index in self.keyMap and not self.filter(change.newValue): + self.filter.unmonitor(change.oldValue) + if change.index in self.keyMap and not self.filter.predicate(change.newValue): idx = self.keyMap.index(change.index) del self.target[idx] del self.keyMap[idx] - elif change.index not in self.keyMap and self.filter(change.newValue): + elif change.index not in self.keyMap and self.filter.predicate(change.newValue): idx = len([x for x in self.keyMap if x < change.index]) - self.target.insert(idx, change.newValue) self.keyMap.insert(idx, change.index) + self.target.insert(idx, change.newValue) + self.filter.monitor(change.newValue, partial(self._onMonitor, change.newValue)) elif isinstance(change, ActiveListIndexDeleted): + self.filter.unmonitor(change.oldValue) if change.index in self.keyMap: idx = self.keyMap.index(change.index) del self.target[idx] del self.keyMap[idx] + def _onMonitor(self, value): + idx = self.source.index(value) + if idx in self.keyMap and not self.filter.predicate(value): + idx = self.keyMap.index(idx) + del self.target[idx] + del self.keyMap[idx] + elif idx not in self.keyMap and self.filter.predicate(value): + newIndex = len([x for x in self.keyMap if x < idx]) + self.keyMap.insert(newIndex, idx) + self.target.insert(newIndex, value) + class ActiveListFlattenListener(ActiveListListener): def __init__(self, source: "ActiveList", target: "ActiveList"): @@ -183,7 +221,7 @@ class ActiveList: self.__fireChanges([ActiveListIndexAppended(len(self) - 1, value)]) def __fireChanges(self, changes: list[ActiveListChange]): - for listener in self.listeners: + for listener in self.listeners.copy(): try: listener.onListChange(self, changes) except Exception: @@ -206,14 +244,11 @@ class ActiveList: self.addListener(ActiveListTransformationListener(transformation, self, res)) return res - def filter(self, filter: callable): - res = ActiveList() - keyMap = [] - for idx, val in enumerate(self): - if filter(val): - res.append(val) - keyMap.append(idx) - self.addListener(ActiveListFilterListener(filter, keyMap, res)) + def filter(self, filter: Union[callable, ActiveListFilter]): + if not isinstance(filter, ActiveListFilter): + filter = BasicFilter(filter) + res = ActiveList([val for val in self if filter.predicate(val)]) + self.addListener(ActiveListFilterListener(filter, self, res)) return res def flatten(self): diff --git a/owrx/sdr.py b/owrx/sdr.py index 669e1ec3..13952926 100644 --- a/owrx/sdr.py +++ b/owrx/sdr.py @@ -1,7 +1,7 @@ from owrx.config import Config from owrx.source import SdrSource from owrx.feature import FeatureDetector, UnknownFeatureException -from owrx.active.list import ActiveListTransformation +from owrx.active.list import ActiveListTransformation, ActiveListFilter, ActiveListListener, ActiveList, ActiveListChange import logging @@ -12,36 +12,53 @@ class ProfileNameMapper(ActiveListTransformation): def __init__(self, source_id, source_name): self.source_id = source_id self.source_name = source_name - self.subscriptions = [] + self.subscriptions = {} def transform(self, profile): return {"id": "{}|{}".format(self.source_id, profile["id"]), "name": "{} {}".format(self.source_name, profile["name"])} def monitor(self, profile, callback: callable): - self.subscriptions.append(profile.filter("name").wire(lambda _: callback())) + self.subscriptions[id(profile)] = profile.filter("name").wire(lambda _: callback()) - def unmonitor(self, member): - affected = [sub for sub in self.subscriptions if sub.subscriptee is member] - for sub in affected: - sub.cancel() - self.subscriptions.remove(sub) + def unmonitor(self, profile): + self.subscriptions[id(profile)].cancel() class ProfileMapper(ActiveListTransformation): def __init__(self): - self.subscriptions = [] + self.subscriptions = {} def transform(self, source): return source.getProfiles().map(ProfileNameMapper(source.getId(), source.getName())) def monitor(self, source, callback: callable): - self.subscriptions.append(source.getProps().filter("name").wire(lambda _: callback())) + self.subscriptions[id(source)] = source.getProps().filter("name").wire(lambda _: callback()) - def unmonitor(self, member): - affected = [sub for sub in self.subscriptions if sub.subscriptee is member] - for sub in affected: - sub.cancel() - self.subscriptions.remove(sub) + def unmonitor(self, source): + self.subscriptions[id(source)].cancel() + + +class ProfileChangeListener(ActiveListListener): + def __init__(self, callback: callable): + self.callback = callback + + def onListChange(self, source: ActiveList, changes: list[ActiveListChange]): + self.callback() + + +class HasProfilesFilter(ActiveListFilter): + def __init__(self): + self.monitors = {} + + def predicate(self, device) -> bool: + return "profiles" in device and device["profiles"] and len(device["profiles"]) > 0 + + def monitor(self, device, callback: callable): + self.monitors[id(device)] = monitor = ProfileChangeListener(callback) + device["profiles"].addListener(monitor) + + def unmonitor(self, device): + device["profiles"].removeListener(self.monitors[id(device)]) class SdrService(object): @@ -76,9 +93,6 @@ class SdrService(object): @staticmethod def getAllSources(): - def hasProfiles(device): - return "profiles" in device and device["profiles"] and len(device["profiles"]) > 0 - def sdrTypeAvailable(value): featureDetector = FeatureDetector() try: @@ -106,17 +120,16 @@ class SdrService(object): if SdrService.sources is None: SdrService.sources = Config.get()["sdrs"] \ .filter(sdrTypeAvailable) \ - .filter(hasProfiles) \ + .filter(HasProfilesFilter()) \ .map(buildNewSource) return SdrService.sources @staticmethod def getActiveSources(): - def isAvailable(source: SdrSource): - return source.isEnabled() and not source.isFailed() - if SdrService.activeSources is None: - SdrService.activeSources = SdrService.getAllSources().filter(isAvailable) + SdrService.activeSources = SdrService.getAllSources() \ + .filter(lambda source: source.isEnabled()) \ + .filter(lambda source: not source.isFailed()) return SdrService.activeSources @staticmethod diff --git a/test/owrx/active/list/test_advanced_filter.py b/test/owrx/active/list/test_advanced_filter.py new file mode 100644 index 00000000..811ec1ed --- /dev/null +++ b/test/owrx/active/list/test_advanced_filter.py @@ -0,0 +1,34 @@ +from unittest import TestCase +from owrx.active.list import ActiveList, ActiveListFilter + + +class AdvancedFilter(ActiveListFilter): + def __init__(self, result: bool): + self.result = result + self.callback = None + + def predicate(self, value) -> bool: + return self.result + + def monitor(self, member, callback: callable): + self.callback = callback + + def trigger(self, newResult: bool): + self.result = newResult + self.callback() + + +class AdvancedFilterTest(TestCase): + def testAdvancedFilter(self): + list = ActiveList([1, 2, 3]) + filteredList = list.filter(AdvancedFilter(True)) + self.assertEqual(len(filteredList), 3) + filteredList = list.filter(AdvancedFilter(False)) + self.assertEqual(len(filteredList), 0) + + def testListMonitor(self): + list = ActiveList([1, 2, 3]) + filter = AdvancedFilter(True) + filteredList = list.filter(filter) + filter.trigger(False) + self.assertEqual(len(filteredList), 2)