Rework radar system
This commit is contained in:
@@ -2,22 +2,24 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, is_dataclass
|
||||
import math
|
||||
from collections.abc import Callable
|
||||
from typing import cast
|
||||
|
||||
from PyQt6.QtCore import QLocale, QSignalBlocker, pyqtSignal
|
||||
from PyQt6.QtCore import QItemSelectionModel, QLocale, QSignalBlocker, pyqtSignal
|
||||
from PyQt6.QtGui import QDoubleValidator, QIntValidator
|
||||
from PyQt6.QtWidgets import (
|
||||
QAbstractItemView, QHeaderView, QHBoxLayout, QLineEdit, QMessageBox, QPushButton,
|
||||
QStyledItemDelegate, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from python_app.models.run_config_validation import normalize_antenna_geometry
|
||||
|
||||
|
||||
class _CoordinateDelegate(QStyledItemDelegate):
|
||||
def createEditor(self, parent, option, index):
|
||||
editor = QLineEdit(parent)
|
||||
validator = (
|
||||
QIntValidator(0, self.parent().property("position_count") - 1, editor)
|
||||
QIntValidator(0, cast(QTableWidget, self.parent()).property("position_count") - 1, editor)
|
||||
if index.column() == 0 else QDoubleValidator(editor)
|
||||
)
|
||||
validator.setLocale(QLocale.c())
|
||||
@@ -30,16 +32,19 @@ class AntennaGeometryEditor(QWidget):
|
||||
|
||||
changed = pyqtSignal()
|
||||
|
||||
def __init__(self, position_field: str, entries, *, position_count: int, parent=None) -> None:
|
||||
def __init__(self, position_field: str, entries, *, position_count: int,
|
||||
validate_change: Callable[[], object] | None = None, parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self.position_field = position_field
|
||||
self.validate_change = validate_change
|
||||
self._pending_row: int | None = None
|
||||
self._table = QTableWidget(0, 4, self)
|
||||
self.set_position_count(position_count)
|
||||
self._table.setHorizontalHeaderLabels([
|
||||
"Output" if position_field == "output_pos" else "Input", "X (m)", "Y (m)", "Z (m)",
|
||||
])
|
||||
self._table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
self._table.verticalHeader().hide()
|
||||
cast(QHeaderView, self._table.horizontalHeader()).setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
|
||||
cast(QHeaderView, self._table.verticalHeader()).hide()
|
||||
self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
||||
self._table.setItemDelegate(_CoordinateDelegate(self._table))
|
||||
self._table.setMinimumHeight(130)
|
||||
@@ -61,73 +66,75 @@ class AntennaGeometryEditor(QWidget):
|
||||
def records(self) -> list[dict]:
|
||||
records = []
|
||||
for row in range(self._table.rowCount()):
|
||||
records.append(dict(zip(
|
||||
(self.position_field, "x_m", "y_m", "z_m"),
|
||||
(self._table.item(row, column).text() for column in range(4)),
|
||||
strict=True,
|
||||
)))
|
||||
return self._normalize(records)
|
||||
|
||||
def _normalize(self, entries) -> list[dict]:
|
||||
records = []
|
||||
positions = set()
|
||||
for entry in entries:
|
||||
entry = asdict(entry) if is_dataclass(entry) else entry
|
||||
if any(isinstance(entry.get(key), bool) for key in (self.position_field, "x_m", "y_m", "z_m")):
|
||||
raise ValueError("Antenna positions and coordinates must be numbers, not booleans")
|
||||
position = int(entry[self.position_field])
|
||||
if float(entry[self.position_field]) != position or not 0 <= position < self._table.property("position_count"):
|
||||
raise ValueError("Antenna position is outside the configured switch positions")
|
||||
if position in positions:
|
||||
raise ValueError(f"Duplicate antenna position: {position}")
|
||||
positions.add(position)
|
||||
coordinates = {key: float(entry.get(key, 0.0)) for key in ("x_m", "y_m", "z_m")}
|
||||
if any(not math.isfinite(value) or abs(value) > 3.4e38 for value in coordinates.values()):
|
||||
raise ValueError("Antenna coordinates must be finite numbers")
|
||||
records.append({self.position_field: position, **coordinates})
|
||||
return records
|
||||
if row == self._pending_row and not cast(QTableWidgetItem, self._table.item(row, 1)).text().strip():
|
||||
continue
|
||||
record = {}
|
||||
for column, key in enumerate((self.position_field, "x_m", "y_m", "z_m")):
|
||||
text = cast(QTableWidgetItem, self._table.item(row, column)).text()
|
||||
try:
|
||||
record[key] = int(text) if column == 0 else float(text)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Antenna row {row + 1}: {key} must be {'an integer' if column == 0 else 'a number'}") from exc
|
||||
records.append(record)
|
||||
return normalize_antenna_geometry(records, self.position_field, self.position_count)
|
||||
|
||||
def set_position_count(self, count: int) -> None:
|
||||
self.position_count = int(count)
|
||||
self._table.setProperty("position_count", int(count))
|
||||
|
||||
def set_records(self, entries) -> None:
|
||||
records = self._normalize(entries)
|
||||
records = normalize_antenna_geometry(entries, self.position_field, self.position_count)
|
||||
with QSignalBlocker(self._table):
|
||||
self._table.setRowCount(len(records))
|
||||
for row, record in enumerate(records):
|
||||
for column, value in enumerate(record.values()):
|
||||
self._table.setItem(row, column, QTableWidgetItem(str(value)))
|
||||
self._pending_row = None
|
||||
self._valid_records = records
|
||||
self._add_button.setEnabled(len(records) < self._table.property("position_count"))
|
||||
self.changed.emit()
|
||||
|
||||
def _on_item_changed(self, _item) -> None:
|
||||
try:
|
||||
self._valid_records = self.records()
|
||||
except ValueError as exc:
|
||||
records = self.records()
|
||||
if records == self._valid_records:
|
||||
return
|
||||
if self.validate_change is not None:
|
||||
self.validate_change()
|
||||
except (ValueError, RuntimeError) as exc:
|
||||
with QSignalBlocker(self):
|
||||
self.set_records(self._valid_records)
|
||||
QMessageBox.warning(self, "Invalid antenna geometry", str(exc))
|
||||
return
|
||||
self._pending_row = None
|
||||
self._valid_records = records
|
||||
self._add_button.setEnabled(len(records) < self.position_count)
|
||||
self.changed.emit()
|
||||
|
||||
def _add_row(self) -> None:
|
||||
records = self.records()
|
||||
if len(records) >= self._table.property("position_count"):
|
||||
row = self._table.rowCount()
|
||||
if row >= self.position_count or self._pending_row is not None:
|
||||
return
|
||||
positions = {record[self.position_field] for record in records}
|
||||
position = next(index for index in range(len(positions) + 1) if index not in positions)
|
||||
self.set_records([*records, {self.position_field: position}])
|
||||
self._table.setCurrentCell(len(records), 1)
|
||||
self._table.editItem(self._table.item(len(records), 1))
|
||||
positions = {record[self.position_field] for record in self.records()}
|
||||
position = next(index for index in range(self.position_count) if index not in positions)
|
||||
with QSignalBlocker(self._table):
|
||||
self._table.insertRow(row)
|
||||
for column, value in enumerate((str(position), "", "0", "0")):
|
||||
self._table.setItem(row, column, QTableWidgetItem(value))
|
||||
self._pending_row = row
|
||||
self._add_button.setEnabled(False)
|
||||
self._table.setCurrentCell(row, 1)
|
||||
self._table.editItem(self._table.item(row, 1))
|
||||
|
||||
def _remove_rows(self) -> None:
|
||||
rows = {index.row() for index in self._table.selectionModel().selectedRows()}
|
||||
if rows:
|
||||
with QSignalBlocker(self._table):
|
||||
for row in sorted(rows, reverse=True):
|
||||
self._table.removeRow(row)
|
||||
self._valid_records = self.records()
|
||||
self._add_button.setEnabled(True)
|
||||
self.changed.emit()
|
||||
rows = {index.row() for index in cast(QItemSelectionModel, self._table.selectionModel()).selectedRows()}
|
||||
if not rows:
|
||||
return
|
||||
with QSignalBlocker(self._table):
|
||||
for row in sorted(rows, reverse=True):
|
||||
self._table.removeRow(row)
|
||||
if self._pending_row is not None:
|
||||
self._pending_row = (None if self._pending_row in rows else
|
||||
self._pending_row - sum(row < self._pending_row for row in rows))
|
||||
self._on_item_changed(None)
|
||||
self._add_button.setEnabled(self._pending_row is None and self._table.rowCount() < self.position_count)
|
||||
|
||||
Reference in New Issue
Block a user