134 lines
5.9 KiB
Python
134 lines
5.9 KiB
Python
"""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()
|