Files
radar_system/python_app/gui/controllers/plot_controller.py
T
2026-10-08 16:24:01 +03:00

169 lines
6.8 KiB
Python

"""Select radar views and supply independent renderers with settings and history."""
from collections.abc import Callable
from PyQt6.QtCore import QSignalBlocker
from PyQt6.QtWidgets import QComboBox, QPushButton, QStackedWidget, QWidget
from python_app.gui.controllers.renderers.sar3d import Sar3dRenderer
from python_app.gui.controllers.history_controller import HistoryController
from python_app.gui.controllers.renderers.bscan import BscanRenderer
from python_app.gui.controllers.renderers.gpr import GprRenderer
from python_app.gui.controllers.renderers.trace import TraceRenderer
from python_app.models.dataset_model import ResultCollection, TraceData
from python_app.models.gui_profile_schema import GuiProcessingStateModel
from python_app.models.run_config_schema import GprModel
class PlotController:
def __init__(
self, history: HistoryController, settings: Callable[[], GuiProcessingStateModel],
geometry: Callable[[], GprModel], running: Callable[[], bool],
warning: Callable[..., None],
) -> None:
self.history = history
self._settings = settings
self._geometry = geometry
self._running = running
self._warning = warning
self.required_revision: int | None = None
self.live_window = 50
self.bscan = BscanRenderer()
self.gpr = GprRenderer()
self.sar3d = Sar3dRenderer()
self.trace = TraceRenderer(warning)
self.stack = QStackedWidget()
for renderer in (self.bscan, self.gpr, self.sar3d, self.trace):
self.stack.addWidget(renderer.widget)
self.stack.setCurrentWidget(self.trace.widget)
self._combo: QComboBox | None = None
self._next: QPushButton | None = None
def bind_display_controls(self, combo: QComboBox, next_button: QPushButton) -> None:
self._combo, self._next = combo, next_button
combo.currentIndexChanged.connect(self.on_display_combo_changed)
next_button.clicked.connect(self.select_next_combo)
self.sync_display_combos()
@property
def display_key(self) -> tuple[int, int] | None:
return self._combo.currentData() if self._combo is not None else None
@property
def capture_widget(self) -> QWidget | None:
current = self.stack.currentWidget()
return self.sar3d.grid if current is self.sar3d.widget else current
def set_mode(self, mode: str) -> None:
renderer = {"bscan": self.bscan, "gpr": self.gpr, "sar3d": self.sar3d}.get(mode, self.trace)
self.stack.setCurrentWidget(renderer.widget)
self.sync_display_combos()
def select_combo(self, input_pos: int, output_pos: int) -> None:
key = input_pos, output_pos
keys = sorted(self.bscan.history_by_combo)
if key not in keys or self._combo is None:
raise ValueError(f"Unknown channel combination: {input_pos}:{output_pos}")
self._combo.setCurrentIndex(keys.index(key))
def select_next_combo(self) -> None:
combo = self._combo
if combo is not None and combo.count():
combo.setCurrentIndex((combo.currentIndex() + 1) % combo.count())
def sync_display_combos(self) -> None:
combo, next_button = self._combo, self._next
if combo is None or next_button is None:
return
keys = sorted(self.bscan.history_by_combo)
current = combo.currentData()
if keys != [combo.itemData(i) for i in range(combo.count())]:
with QSignalBlocker(combo):
combo.clear()
for key in keys:
combo.addItem(f"{key[0]} : {key[1]}", key)
combo.setCurrentIndex(keys.index(current) if current in keys else 0)
enabled = bool(keys) and self._settings().selected_mode == "bscan"
combo.setEnabled(enabled)
next_button.setEnabled(enabled and len(keys) > 1)
def window_scans(self) -> int:
return self.live_window if self._running() else max(1, self._settings().bscan.history_window_scans)
def sync_bscan(self) -> None:
results = self.history.bscan_source()
self.bscan.sync(results, self.window_scans())
self.sync_display_combos()
def draw_bscan(self) -> bool:
self.sync_bscan()
window, retained = self.window_scans(), len(self.history.preprocessed)
if retained < window and self.bscan.history_by_combo:
self._warning(
f"B-scan is set to show {window} sweeps but only {retained} are retained; "
"the image shows what history there is.",
details="Capture more sweeps, or reduce 'Scans to show (stopped)'.",
once_key=f"bscan_window_underfilled_{window}",
)
return self.bscan.draw(self.display_key, self._settings().bscan)
def on_display_combo_changed(self, *_args: object) -> None:
if self._settings().selected_mode == "bscan":
self.draw_bscan()
def draw(self, collection: ResultCollection) -> bool:
settings = self._settings()
mode = settings.selected_mode
if mode in {"gpr", "sar3d"} and self.required_revision is not None:
if collection.settings_revision != self.required_revision:
return False
if mode == "bscan":
return self.draw_bscan()
if mode == "gpr":
return self.gpr.draw(collection, settings.gpr, self._geometry())
if mode == "sar3d":
return self.sar3d.draw(collection, settings.sar3d, self._geometry())
return self.trace.draw(collection, settings.pass_through)
def redraw(self, *_args: object) -> None:
mode = self._settings().selected_mode
if mode == "bscan":
self.draw_bscan()
elif mode == "sar3d":
self.sar3d.clear()
if self.history.results:
self.draw(self.history.results[-1])
elif not self.history.results or not self.draw(self.history.results[-1]):
self.clear_current()
def draw_trace(self, trace: TraceData, title: str) -> None:
self.trace.draw_trace(trace, title, self._settings().pass_through)
def clear_bscan(self) -> None:
self.bscan.clear()
self.sync_display_combos()
def clear_gpr(self) -> None:
self.gpr.clear()
def clear_current(self) -> None:
mode = self._settings().selected_mode
if mode == "bscan":
self.clear_bscan()
elif mode == "gpr":
self.clear_gpr()
elif mode == "sar3d":
self.sar3d.clear()
else:
self.trace.clear()
def clear(self) -> None:
self.clear_bscan()
self.clear_gpr()
self.sar3d.clear()
self.trace.clear()
@staticmethod
def has_output(collection: ResultCollection) -> bool:
return bool(collection.collection_payloads) or any(p.kind == 1 for b in collection.blocks for p in b.payloads)