"""Table editor for antenna switch positions and coordinates in metres.""" from __future__ import annotations from dataclasses import asdict, is_dataclass import math from PyQt6.QtCore import QLocale, QSignalBlocker, pyqtSignal from PyQt6.QtGui import QDoubleValidator, QIntValidator from PyQt6.QtWidgets import ( QAbstractItemView, QHeaderView, QHBoxLayout, QLineEdit, QMessageBox, QPushButton, QStyledItemDelegate, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, ) class _CoordinateDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): editor = QLineEdit(parent) validator = ( QIntValidator(0, self.parent().property("position_count") - 1, editor) if index.column() == 0 else QDoubleValidator(editor) ) validator.setLocale(QLocale.c()) editor.setValidator(validator) return editor class AntennaGeometryEditor(QWidget): """Keep geometry explicit and preserve all three coordinates on profile round trips.""" changed = pyqtSignal() def __init__(self, position_field: str, entries, *, position_count: int, parent=None) -> None: super().__init__(parent) self.position_field = position_field 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() self._table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self._table.setItemDelegate(_CoordinateDelegate(self._table)) self._table.setMinimumHeight(130) self._table.setMaximumHeight(220) layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._table) buttons = QHBoxLayout() self._add_button = QPushButton("Add antenna", self) self._remove_button = QPushButton("Remove selected", self) buttons.addWidget(self._add_button) buttons.addWidget(self._remove_button) layout.addLayout(buttons) self.set_records(entries) self._table.itemChanged.connect(self._on_item_changed) self._add_button.clicked.connect(self._add_row) self._remove_button.clicked.connect(self._remove_rows) 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 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) 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._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: with QSignalBlocker(self): self.set_records(self._valid_records) QMessageBox.warning(self, "Invalid antenna geometry", str(exc)) return self.changed.emit() def _add_row(self) -> None: records = self.records() if len(records) >= self._table.property("position_count"): 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)) 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()