Rework radar system

This commit is contained in:
Ayzen
2026-10-06 12:32:26 +03:00
parent 552cdc688a
commit 5bb86b7165
299 changed files with 13537 additions and 27763 deletions
+57 -50
View File
@@ -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)