#!/usr/bin/env python3 """ Trimble Geodesy Tool - Hauptprogramm mit GUI Geodätische Vermessungsarbeiten mit JXL-Dateien Version 3.0 - Überarbeitet: Korrekte Netzausgleichung, Berechnungsprotokoll, Datenfluss """ import sys import os from pathlib import Path from itertools import combinations import math # Module-Pfad hinzufügen sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from PyQt5.QtWidgets import ( QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QLabel, QPushButton, QLineEdit, QTextEdit, QFileDialog, QTableWidget, QTableWidgetItem, QMessageBox, QGroupBox, QComboBox, QSpinBox, QDoubleSpinBox, QCheckBox, QSplitter, QFrame, QScrollArea, QHeaderView, QListWidget, QListWidgetItem, QDialog, QDialogButtonBox, QFormLayout, QProgressBar, QStatusBar, QMenuBar, QMenu, QAction, QToolBar, QStyle, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QRadioButton, QButtonGroup ) from PyQt5.QtCore import Qt, QThread, pyqtSignal from PyQt5.QtGui import QFont, QIcon, QPalette, QColor, QBrush from modules.jxl_parser import JXLParser from modules.cor_generator import CORGenerator, CORPoint from modules.transformation import CoordinateTransformer, LocalSystemTransformer from modules.georeferencing import Georeferencer, ControlPoint from modules.network_adjustment import NetworkAdjustment from modules.reference_point_adjuster import ReferencePointAdjuster, TransformationResult # ============================================================================= # Globaler Speicher für ausgeglichene Punkte # ============================================================================= class AdjustedPointsStore: """Globaler Speicher für ausgeglichene Koordinaten""" _instance = None def __new__(cls): if cls._instance is None: cls._instance = super().__new__(cls) cls._instance.points = {} # {name: (x, y, z)} cls._instance.available = False return cls._instance def set_points(self, points_dict): """Speichert ausgeglichene Punkte""" self.points = points_dict.copy() self.available = True def get_points(self): """Gibt ausgeglichene Punkte zurück""" return self.points.copy() def clear(self): """Löscht gespeicherte Punkte""" self.points = {} self.available = False def is_available(self): """Prüft ob ausgeglichene Punkte verfügbar sind""" return self.available and len(self.points) > 0 # Globale Instanz adjusted_points_store = AdjustedPointsStore() # ============================================================================= # Export-Dialog (wiederverwendbar für alle Module) # ============================================================================= class ExportFormatDialog(QDialog): """Dialog zur Auswahl des Export-Formats (COR, CSV, TXT, DXF)""" def __init__(self, parent=None, title="Export-Format wählen"): super().__init__(parent) self.setWindowTitle(title) self.setMinimumWidth(300) self.selected_format = "cor" # Standard self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Format-Auswahl format_group = QGroupBox("Export-Format auswählen") format_layout = QVBoxLayout(format_group) self.format_group = QButtonGroup(self) self.cor_radio = QRadioButton("COR - Koordinatendatei (Standard)") self.cor_radio.setChecked(True) self.format_group.addButton(self.cor_radio, 0) format_layout.addWidget(self.cor_radio) self.csv_radio = QRadioButton("CSV - Komma-getrennte Werte") self.format_group.addButton(self.csv_radio, 1) format_layout.addWidget(self.csv_radio) self.txt_radio = QRadioButton("TXT - Tabulatorgetrennt") self.format_group.addButton(self.txt_radio, 2) format_layout.addWidget(self.txt_radio) self.dxf_radio = QRadioButton("DXF - AutoCAD Format") self.format_group.addButton(self.dxf_radio, 3) format_layout.addWidget(self.dxf_radio) layout.addWidget(format_group) # Info-Text info_label = QLabel( "💡 COR und CSV: PunktID,X,Y,Z (keine Header-Zeile)\n" " TXT: Tabulatorgetrennt\n" " DXF: AutoCAD-kompatibel" ) info_label.setStyleSheet("color: #666; font-style: italic; padding: 5px;") layout.addWidget(info_label) # Buttons button_box = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel ) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) layout.addWidget(button_box) def get_selected_format(self): """Gibt das ausgewählte Format zurück""" if self.cor_radio.isChecked(): return "cor" elif self.csv_radio.isChecked(): return "csv" elif self.txt_radio.isChecked(): return "txt" elif self.dxf_radio.isChecked(): return "dxf" return "cor" def get_file_filter(self): """Gibt den Dateifilter für den Speicherdialog zurück""" format_type = self.get_selected_format() filters = { 'cor': "COR Files (*.cor)", 'csv': "CSV Files (*.csv)", 'txt': "Text Files (*.txt)", 'dxf': "DXF Files (*.dxf)" } return filters.get(format_type, "All Files (*)") def show_export_dialog_and_save(parent, cor_generator, default_name="export"): """ Zeigt den Export-Dialog und speichert die Datei. Wiederverwendbare Funktion für alle Module. """ # Format-Dialog anzeigen dialog = ExportFormatDialog(parent) if dialog.exec_() != QDialog.Accepted: return None format_type = dialog.get_selected_format() file_filter = dialog.get_file_filter() # Dateiendung hinzufügen extensions = {'cor': '.cor', 'csv': '.csv', 'txt': '.txt', 'dxf': '.dxf'} suggested_name = f"{default_name}{extensions.get(format_type, '.cor')}" # Datei-Dialog file_path, _ = QFileDialog.getSaveFileName( parent, "Speichern unter", suggested_name, file_filter ) if not file_path: return None # Exportieren try: if format_type == 'cor': cor_generator.write_cor_file(file_path) elif format_type == 'csv': cor_generator.export_csv(file_path) elif format_type == 'txt': cor_generator.export_txt(file_path) elif format_type == 'dxf': cor_generator.export_dxf(file_path) QMessageBox.information(parent, "Erfolg", f"Datei gespeichert: {file_path}") return file_path except Exception as e: QMessageBox.critical(parent, "Fehler", f"Fehler beim Speichern: {e}") return None def export_points_with_dialog(parent, points, default_name="punkte"): """ Exportiert eine Liste von CORPoint-Objekten mit Export-Dialog. Universelle Funktion für alle Module. """ if not points: QMessageBox.warning(parent, "Fehler", "Keine Punkte zum Exportieren!") return None # Temporären COR-Generator erstellen class TempGenerator: def __init__(self, pts): self.cor_points = pts def write_cor_file(self, path): lines = [f"{p.name},{p.x:.4f},{p.y:.4f},{p.z:.4f}" for p in self.cor_points] with open(path, 'w', encoding='utf-8') as f: f.write("\n".join(lines)) def export_csv(self, path): self.write_cor_file(path) # Gleiches Format def export_txt(self, path): lines = [f"{p.name}\t{p.x:.4f}\t{p.y:.4f}\t{p.z:.4f}" for p in self.cor_points] with open(path, 'w', encoding='utf-8') as f: f.write("\n".join(lines)) def export_dxf(self, path): lines = ["0", "SECTION", "2", "ENTITIES"] for p in self.cor_points: lines.extend([ "0", "POINT", "8", "POINTS", "10", f"{p.x:.4f}", "20", f"{p.y:.4f}", "30", f"{p.z:.4f}", "0", "TEXT", "8", "NAMES", "10", f"{p.x + 0.5:.4f}", "20", f"{p.y + 0.5:.4f}", "30", f"{p.z:.4f}", "40", "0.5", "1", p.name ]) lines.extend(["0", "ENDSEC", "0", "EOF"]) with open(path, 'w', encoding='utf-8') as f: f.write("\n".join(lines)) temp_gen = TempGenerator(points) return show_export_dialog_and_save(parent, temp_gen, default_name) def export_text_with_dialog(parent, text, default_name="protokoll"): """Exportiert Text als TXT oder PDF""" file_path, selected_filter = QFileDialog.getSaveFileName( parent, "Protokoll speichern", f"{default_name}.txt", "Text Files (*.txt);;PDF Files (*.pdf)" ) if not file_path: return None try: if file_path.endswith('.pdf'): # PDF-Export (einfach) try: from reportlab.lib.pagesizes import A4 from reportlab.pdfgen import canvas from reportlab.lib.units import mm c = canvas.Canvas(file_path, pagesize=A4) width, height = A4 y = height - 30*mm for line in text.split('\n'): if y < 30*mm: c.showPage() y = height - 30*mm c.setFont("Courier", 8) c.drawString(20*mm, y, line[:100]) # Max 100 chars pro Zeile y -= 10 c.save() except ImportError: # Fallback: Als TXT speichern file_path = file_path.replace('.pdf', '.txt') with open(file_path, 'w', encoding='utf-8') as f: f.write(text) QMessageBox.warning(parent, "Hinweis", "PDF-Export nicht verfügbar (reportlab fehlt). Als TXT gespeichert.") else: with open(file_path, 'w', encoding='utf-8') as f: f.write(text) QMessageBox.information(parent, "Erfolg", f"Datei gespeichert: {file_path}") return file_path except Exception as e: QMessageBox.critical(parent, "Fehler", f"Fehler beim Speichern: {e}") return None # ============================================================================= # JXL-Analyse Tab # ============================================================================= class JXLAnalysisTab(QWidget): """Tab für JXL-Datei Analyse und Bearbeitung - Mit TreeView für Stationierungen""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.prism_spin_widgets = {} self.control_point_checkboxes = {} self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Datei laden file_group = QGroupBox("JXL-Datei") file_layout = QHBoxLayout(file_group) self.file_path_edit = QLineEdit() self.file_path_edit.setPlaceholderText("JXL-Datei auswählen...") file_layout.addWidget(self.file_path_edit) browse_btn = QPushButton("Durchsuchen") browse_btn.clicked.connect(self.browse_file) file_layout.addWidget(browse_btn) load_btn = QPushButton("Laden") load_btn.clicked.connect(self.load_file) file_layout.addWidget(load_btn) layout.addWidget(file_group) # Splitter für Info und Tabellen splitter = QSplitter(Qt.Vertical) # Zusammenfassung summary_group = QGroupBox("Zusammenfassung") summary_layout = QVBoxLayout(summary_group) self.summary_text = QTextEdit() self.summary_text.setReadOnly(True) self.summary_text.setMaximumHeight(120) summary_layout.addWidget(self.summary_text) splitter.addWidget(summary_group) # Stationierungen TreeView stations_group = QGroupBox("Stationierungen und Messungen (TreeView)") stations_layout = QVBoxLayout(stations_group) self.stations_tree = QTreeWidget() self.stations_tree.setHeaderLabels([ "Station/Messung", "Hz [gon]", "V [gon]", "Distanz [m]", "PK [mm]", "Typ" ]) self.stations_tree.setColumnCount(6) self.stations_tree.setSelectionMode(QAbstractItemView.SingleSelection) self.stations_tree.setMinimumHeight(250) # Spaltenbreiten self.stations_tree.setColumnWidth(0, 180) self.stations_tree.setColumnWidth(1, 110) self.stations_tree.setColumnWidth(2, 110) self.stations_tree.setColumnWidth(3, 100) self.stations_tree.setColumnWidth(4, 80) self.stations_tree.setColumnWidth(5, 120) stations_layout.addWidget(self.stations_tree) # Buttons für Protokoll protocol_layout = QHBoxLayout() show_protocol_btn = QPushButton("📋 Berechnungsprotokoll anzeigen") show_protocol_btn.clicked.connect(self.show_calculation_protocol) show_protocol_btn.setStyleSheet("background-color: #FF9800; color: white;") protocol_layout.addWidget(show_protocol_btn) export_protocol_btn = QPushButton("💾 Protokoll exportieren") export_protocol_btn.clicked.connect(self.export_calculation_protocol) protocol_layout.addWidget(export_protocol_btn) protocol_layout.addStretch() stations_layout.addLayout(protocol_layout) splitter.addWidget(stations_group) # Punkte-Tabelle points_group = QGroupBox("Alle Punkte (Übersicht)") points_layout = QVBoxLayout(points_group) self.points_table = QTableWidget() self.points_table.setColumnCount(6) self.points_table.setHorizontalHeaderLabels( ["Name", "Code", "East (X)", "North (Y)", "Elevation (Z)", "Methode"]) self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) points_layout.addWidget(self.points_table) # Punkt-Aktionen point_actions = QHBoxLayout() remove_point_btn = QPushButton("Ausgewählten Punkt entfernen") remove_point_btn.clicked.connect(self.remove_selected_point) point_actions.addWidget(remove_point_btn) point_actions.addStretch() points_layout.addLayout(point_actions) splitter.addWidget(points_group) # Splitter-Größen splitter.setSizes([120, 350, 200]) layout.addWidget(splitter) def browse_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "JXL-Datei öffnen", "", "JXL Files (*.jxl);;All Files (*)") if file_path: self.file_path_edit.setText(file_path) def load_file(self): file_path = self.file_path_edit.text() if not file_path or not os.path.exists(file_path): QMessageBox.warning(self, "Fehler", "Bitte eine gültige JXL-Datei auswählen!") return parser = JXLParser() if parser.parse(file_path): self.main_window.parser = parser adjusted_points_store.clear() # Ausgeglichene Punkte zurücksetzen self.update_display() self.main_window.statusBar().showMessage(f"JXL-Datei geladen: {file_path}") else: QMessageBox.critical(self, "Fehler", "Datei konnte nicht geladen werden!") def update_display(self): if not self.main_window.parser: return parser = self.main_window.parser # Zusammenfassung self.summary_text.setText(parser.get_summary()) # TreeView aktualisieren self.update_stations_tree() # Punkte-Tabelle self.update_points_table() def update_stations_tree(self): """Aktualisiert den TreeView mit Stationierungen und Messungen""" self.stations_tree.clear() self.prism_spin_widgets.clear() self.control_point_checkboxes.clear() if not self.main_window.parser: return parser = self.main_window.parser # Sortiere Stationen nach Zeitstempel sorted_stations = sorted(parser.stations.items(), key=lambda x: x[1].timestamp) for station_id, station in sorted_stations: # Station als Hauptknoten station_item = QTreeWidgetItem() # Stationskoordinaten coord_str = "" if station.east is not None: coord_str = f" (E={station.east:.2f}, N={station.north:.2f})" station_item.setText(0, f"📍 {station.name}{coord_str}") station_item.setData(0, Qt.UserRole, station_id) # Stationstyp station_item.setText(5, station.station_type) # Hintergrundfarbe if station.station_type == "ReflineStationSetup": for i in range(6): station_item.setBackground(i, QBrush(QColor(200, 230, 200))) elif station.station_type == "StandardResection": for i in range(6): station_item.setBackground(i, QBrush(QColor(200, 200, 230))) else: for i in range(6): station_item.setBackground(i, QBrush(QColor(230, 230, 200))) self.stations_tree.addTopLevelItem(station_item) # Orientierung/Backbearing for bb_id, bb in parser.backbearings.items(): if bb.station_record_id == station_id: ori_item = QTreeWidgetItem() ori_item.setText(0, f" 🧭 Orientierung → {bb.backsight}") if bb.face1_hz is not None: ori_item.setText(1, f"{bb.face1_hz:.6f}") if bb.orientation_correction is not None: ori_item.setText(5, f"Korr: {bb.orientation_correction:.6f}") ori_item.setForeground(0, QBrush(QColor(100, 100, 100))) station_item.addChild(ori_item) # Detaillierte Messungen measurements = parser.get_detailed_measurements_from_station(station_id) # Zuerst Anschlussmessungen backsight_meas = [m for m in measurements if m.classification == 'BackSight' and not m.deleted] if backsight_meas: bs_header = QTreeWidgetItem() bs_header.setText(0, " Anschlussmessungen:") bs_header.setForeground(0, QBrush(QColor(0, 100, 0))) station_item.addChild(bs_header) for m in backsight_meas: meas_item = QTreeWidgetItem() meas_item.setText(0, f" ↳ {m.point_name}") if m.horizontal_circle is not None: meas_item.setText(1, f"{m.horizontal_circle:.6f}") if m.vertical_circle is not None: meas_item.setText(2, f"{m.vertical_circle:.6f}") if m.edm_distance is not None: meas_item.setText(3, f"{m.edm_distance:.4f}") meas_item.setText(4, f"{m.prism_constant*1000:.1f}") meas_item.setText(5, "Passpunkt") meas_item.setForeground(5, QBrush(QColor(0, 100, 0))) station_item.addChild(meas_item) # Dann normale Messungen normal_meas = [m for m in measurements if m.classification != 'BackSight' and not m.deleted] if normal_meas: norm_header = QTreeWidgetItem() norm_header.setText(0, f" Messungen ({len(normal_meas)}):") station_item.addChild(norm_header) for m in normal_meas: meas_item = QTreeWidgetItem() meas_item.setText(0, f" ↳ {m.point_name}") if m.horizontal_circle is not None: meas_item.setText(1, f"{m.horizontal_circle:.6f}") if m.vertical_circle is not None: meas_item.setText(2, f"{m.vertical_circle:.6f}") if m.edm_distance is not None: meas_item.setText(3, f"{m.edm_distance:.4f}") meas_item.setText(4, f"{m.prism_constant*1000:.1f}") meas_item.setText(5, m.prism_type[:15] if m.prism_type else "") station_item.addChild(meas_item) station_item.setExpanded(True) def update_points_table(self): if not self.main_window.parser: return points = self.main_window.parser.get_active_points() self.points_table.setRowCount(len(points)) for row, (name, point) in enumerate(sorted(points.items())): self.points_table.setItem(row, 0, QTableWidgetItem(name)) self.points_table.setItem(row, 1, QTableWidgetItem(point.code or "")) self.points_table.setItem(row, 2, QTableWidgetItem(f"{point.east:.4f}" if point.east else "")) self.points_table.setItem(row, 3, QTableWidgetItem(f"{point.north:.4f}" if point.north else "")) self.points_table.setItem(row, 4, QTableWidgetItem(f"{point.elevation:.4f}" if point.elevation else "")) self.points_table.setItem(row, 5, QTableWidgetItem(point.method)) def remove_selected_point(self): row = self.points_table.currentRow() if row >= 0: name = self.points_table.item(row, 0).text() reply = QMessageBox.question( self, "Bestätigung", f"Punkt '{name}' wirklich entfernen?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.Yes: self.main_window.parser.remove_point(name) self.update_display() def show_calculation_protocol(self): """Zeigt das Berechnungsprotokoll in einem Dialog""" if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return protocol = self.main_window.parser.get_calculation_protocol() dialog = QDialog(self) dialog.setWindowTitle("Berechnungsprotokoll") dialog.setMinimumSize(900, 700) layout = QVBoxLayout(dialog) text_edit = QTextEdit() text_edit.setReadOnly(True) text_edit.setFont(QFont("Courier", 9)) text_edit.setText(protocol) layout.addWidget(text_edit) # Buttons btn_layout = QHBoxLayout() export_btn = QPushButton("💾 Exportieren") export_btn.clicked.connect(lambda: export_text_with_dialog(dialog, protocol, "berechnungsprotokoll")) btn_layout.addWidget(export_btn) close_btn = QPushButton("Schließen") close_btn.clicked.connect(dialog.close) btn_layout.addWidget(close_btn) layout.addLayout(btn_layout) dialog.exec_() def export_calculation_protocol(self): """Exportiert das Berechnungsprotokoll""" if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return protocol = self.main_window.parser.get_calculation_protocol() export_text_with_dialog(self, protocol, "berechnungsprotokoll") # ============================================================================= # COR-Generator Tab (NUR aus ComputedGrid) # ============================================================================= class CORGeneratorTab(QWidget): """Tab für COR-Datei Generierung - Nur aus berechneten Koordinaten""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.cor_generator = None self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Info info_group = QGroupBox("COR-Generator") info_layout = QVBoxLayout(info_group) info_label = QLabel( "💡 Generiert Punktdateien aus den berechneten Koordinaten (ComputedGrid) der JXL-Datei.\n" "Die Koordinaten werden direkt aus Trimble Access übernommen." ) info_label.setStyleSheet("color: #666; background-color: #f0f0f0; padding: 10px;") info_label.setWordWrap(True) info_layout.addWidget(info_label) # Option: Ausgeglichene Punkte verwenden self.use_adjusted_check = QCheckBox("✓ Ausgeglichene Punkte verwenden (falls verfügbar)") self.use_adjusted_check.setEnabled(False) info_layout.addWidget(self.use_adjusted_check) layout.addWidget(info_group) # Generieren Button generate_btn = QPushButton("Punkte generieren") generate_btn.clicked.connect(self.generate_cor) generate_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") layout.addWidget(generate_btn) # Vorschau preview_group = QGroupBox("Vorschau") preview_layout = QVBoxLayout(preview_group) self.preview_table = QTableWidget() self.preview_table.setColumnCount(4) self.preview_table.setHorizontalHeaderLabels(["Punkt", "X (East)", "Y (North)", "Z (Elev)"]) self.preview_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) preview_layout.addWidget(self.preview_table) # Statistiken self.stats_text = QTextEdit() self.stats_text.setReadOnly(True) self.stats_text.setMaximumHeight(100) preview_layout.addWidget(self.stats_text) layout.addWidget(preview_group) # Export Button (mit Dialog) export_btn = QPushButton("📥 Punkte exportieren...") export_btn.clicked.connect(self.export_with_dialog) export_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold; font-size: 14px; padding: 10px;") layout.addWidget(export_btn) def showEvent(self, event): """Wird aufgerufen wenn Tab angezeigt wird""" super().showEvent(event) self.update_adjusted_points_status() def update_adjusted_points_status(self): """Aktualisiert den Status der ausgeglichenen Punkte""" if adjusted_points_store.is_available(): self.use_adjusted_check.setEnabled(True) self.use_adjusted_check.setText( f"✓ Ausgeglichene Punkte verwenden ({len(adjusted_points_store.points)} Punkte)") else: self.use_adjusted_check.setEnabled(False) self.use_adjusted_check.setChecked(False) self.use_adjusted_check.setText("✓ Ausgeglichene Punkte verwenden (nicht verfügbar)") def generate_cor(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return self.cor_generator = CORGenerator(self.main_window.parser) # Verwende ausgeglichene Punkte wenn aktiviert if self.use_adjusted_check.isChecked() and adjusted_points_store.is_available(): points = [] for name, (x, y, z) in adjusted_points_store.get_points().items(): points.append(CORPoint(name=name, x=x, y=y, z=z)) self.cor_generator.cor_points = points else: # Aus ComputedGrid generieren (einzige korrekte Methode) points = self.cor_generator.generate_from_computed_grid() # Tabelle aktualisieren self.preview_table.setRowCount(len(self.cor_generator.cor_points)) for row, p in enumerate(self.cor_generator.cor_points): self.preview_table.setItem(row, 0, QTableWidgetItem(p.name)) self.preview_table.setItem(row, 1, QTableWidgetItem(f"{p.x:.4f}")) self.preview_table.setItem(row, 2, QTableWidgetItem(f"{p.y:.4f}")) self.preview_table.setItem(row, 3, QTableWidgetItem(f"{p.z:.4f}")) self.stats_text.setText(self.cor_generator.get_statistics()) self.main_window.statusBar().showMessage(f"{len(self.cor_generator.cor_points)} Punkte generiert") def export_with_dialog(self): if not self.cor_generator or not self.cor_generator.cor_points: QMessageBox.warning(self, "Fehler", "Bitte zuerst Punkte generieren!") return show_export_dialog_and_save(self, self.cor_generator, "koordinaten") # ============================================================================= # Transformation Tab # ============================================================================= class TransformationTab(QWidget): """Tab für Koordinatentransformation""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.transformer = CoordinateTransformer() self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Datenquelle source_group = QGroupBox("Datenquelle") source_layout = QVBoxLayout(source_group) self.source_jxl_radio = QRadioButton("Aus JXL-Datei") self.source_jxl_radio.setChecked(True) source_layout.addWidget(self.source_jxl_radio) self.source_adjusted_radio = QRadioButton("Ausgeglichene Punkte verwenden") self.source_adjusted_radio.setEnabled(False) source_layout.addWidget(self.source_adjusted_radio) layout.addWidget(source_group) # Methode auswählen method_group = QGroupBox("Transformationsmethode") method_layout = QVBoxLayout(method_group) self.manual_radio = QCheckBox("Manuelle Parameter") self.manual_radio.setChecked(True) self.manual_radio.stateChanged.connect(self.toggle_method) method_layout.addWidget(self.manual_radio) self.twopoint_radio = QCheckBox("Über 2 Punkte definieren") self.twopoint_radio.stateChanged.connect(self.toggle_method) method_layout.addWidget(self.twopoint_radio) layout.addWidget(method_group) # Manuelle Parameter self.manual_group = QGroupBox("Manuelle Parameter") manual_layout = QGridLayout(self.manual_group) manual_layout.addWidget(QLabel("Verschiebung X (East):"), 0, 0) self.dx_spin = QDoubleSpinBox() self.dx_spin.setRange(-1000000, 1000000) self.dx_spin.setDecimals(4) manual_layout.addWidget(self.dx_spin, 0, 1) manual_layout.addWidget(QLabel("m"), 0, 2) manual_layout.addWidget(QLabel("Verschiebung Y (North):"), 1, 0) self.dy_spin = QDoubleSpinBox() self.dy_spin.setRange(-1000000, 1000000) self.dy_spin.setDecimals(4) manual_layout.addWidget(self.dy_spin, 1, 1) manual_layout.addWidget(QLabel("m"), 1, 2) manual_layout.addWidget(QLabel("Verschiebung Z (Höhe):"), 2, 0) self.dz_spin = QDoubleSpinBox() self.dz_spin.setRange(-1000000, 1000000) self.dz_spin.setDecimals(4) manual_layout.addWidget(self.dz_spin, 2, 1) manual_layout.addWidget(QLabel("m"), 2, 2) manual_layout.addWidget(QLabel("Rotation:"), 3, 0) self.rotation_spin = QDoubleSpinBox() self.rotation_spin.setRange(-400, 400) self.rotation_spin.setDecimals(6) manual_layout.addWidget(self.rotation_spin, 3, 1) manual_layout.addWidget(QLabel("gon"), 3, 2) layout.addWidget(self.manual_group) # 2-Punkte-Definition self.twopoint_group = QGroupBox("2-Punkte-Definition") twopoint_layout = QGridLayout(self.twopoint_group) twopoint_layout.addWidget(QLabel("XY-Nullpunkt (0,0):"), 0, 0) self.xy_origin_combo = QComboBox() twopoint_layout.addWidget(self.xy_origin_combo, 0, 1) twopoint_layout.addWidget(QLabel("Z-Nullpunkt (0):"), 1, 0) self.z_origin_combo = QComboBox() twopoint_layout.addWidget(self.z_origin_combo, 1, 1) refresh_btn = QPushButton("Punktliste aktualisieren") refresh_btn.clicked.connect(self.refresh_point_lists) twopoint_layout.addWidget(refresh_btn, 2, 0, 1, 2) self.twopoint_group.setVisible(False) layout.addWidget(self.twopoint_group) # Buttons btn_layout = QHBoxLayout() transform_btn = QPushButton("Transformation berechnen") transform_btn.clicked.connect(self.execute_transformation) transform_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") btn_layout.addWidget(transform_btn) apply_btn = QPushButton("Auf Punkte anwenden") apply_btn.clicked.connect(self.apply_transformation) apply_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") btn_layout.addWidget(apply_btn) layout.addLayout(btn_layout) # Ergebnisse results_group = QGroupBox("Ergebnisse") results_layout = QVBoxLayout(results_group) self.results_text = QTextEdit() self.results_text.setReadOnly(True) results_layout.addWidget(self.results_text) export_points_btn = QPushButton("📥 Punkte exportieren...") export_points_btn.clicked.connect(self.export_points) results_layout.addWidget(export_points_btn) layout.addWidget(results_group) def showEvent(self, event): super().showEvent(event) self.update_adjusted_points_status() def update_adjusted_points_status(self): if adjusted_points_store.is_available(): self.source_adjusted_radio.setEnabled(True) self.source_adjusted_radio.setText( f"Ausgeglichene Punkte verwenden ({len(adjusted_points_store.points)} Punkte)") else: self.source_adjusted_radio.setEnabled(False) self.source_jxl_radio.setChecked(True) def toggle_method(self): self.manual_group.setVisible(self.manual_radio.isChecked()) self.twopoint_group.setVisible(self.twopoint_radio.isChecked()) if self.twopoint_radio.isChecked(): self.refresh_point_lists() def refresh_point_lists(self): if not self.main_window.parser: return points = list(self.main_window.parser.get_active_points().keys()) self.xy_origin_combo.clear() self.z_origin_combo.clear() for name in sorted(points): self.xy_origin_combo.addItem(name) self.z_origin_combo.addItem(name) def execute_transformation(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return # Punkte sammeln points = [] if self.source_adjusted_radio.isChecked() and adjusted_points_store.is_available(): for name, (x, y, z) in adjusted_points_store.get_points().items(): points.append(CORPoint(name=name, x=x, y=y, z=z)) else: for name, p in self.main_window.parser.get_active_points().items(): if p.east is not None and p.north is not None: points.append(CORPoint( name=name, x=p.east, y=p.north, z=p.elevation or 0.0 )) self.transformer.set_points(points) if self.manual_radio.isChecked(): self.transformer.set_manual_parameters( dx=self.dx_spin.value(), dy=self.dy_spin.value(), dz=self.dz_spin.value(), rotation_gon=self.rotation_spin.value(), pivot_x=0, pivot_y=0 ) elif self.twopoint_radio.isChecked(): origin = self.xy_origin_combo.currentText() zref = self.z_origin_combo.currentText() if not self.transformer.compute_translation_only(origin, zref): QMessageBox.warning(self, "Fehler", "Punkt nicht gefunden!") return self.transformer.transform() report = self.transformer.get_parameters_report() report += "\n\n" report += self.transformer.get_comparison_table() self.results_text.setText(report) self.main_window.statusBar().showMessage("Transformation berechnet") def apply_transformation(self): if not self.transformer.transformed_points: QMessageBox.warning(self, "Fehler", "Bitte zuerst 'Transformation berechnen' ausführen!") return if not self.main_window.parser: return reply = QMessageBox.question( self, "Bestätigung", "Transformierte Koordinaten auf alle Punkte anwenden?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: return for trans_point in self.transformer.transformed_points: if trans_point.name in self.main_window.parser.points: p = self.main_window.parser.points[trans_point.name] p.east = trans_point.x p.north = trans_point.y p.elevation = trans_point.z jxl_tab = self.main_window.tabs.widget(0) if hasattr(jxl_tab, 'update_display'): jxl_tab.update_display() QMessageBox.information(self, "Erfolg", f"{len(self.transformer.transformed_points)} Punkte transformiert!") def export_points(self): if not self.transformer.transformed_points: QMessageBox.warning(self, "Fehler", "Keine transformierten Punkte!") return export_points_with_dialog(self, self.transformer.transformed_points, "transformiert") # ============================================================================= # Georeferenzierung Tab (mit automatischer Punktzuordnung) # ============================================================================= class GeoreferencingTab(QWidget): """Tab für Georeferenzierung - Mit automatischer Punktzuordnung über Tripel""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.georeferencer = Georeferencer() self.loaded_target_points = {} # {name: (x, y, z)} self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Datenquelle source_group = QGroupBox("Datenquelle für Ist-Koordinaten") source_layout = QVBoxLayout(source_group) self.source_jxl_radio = QRadioButton("Aus JXL-Datei") self.source_jxl_radio.setChecked(True) source_layout.addWidget(self.source_jxl_radio) self.source_adjusted_radio = QRadioButton("Ausgeglichene Punkte verwenden") self.source_adjusted_radio.setEnabled(False) source_layout.addWidget(self.source_adjusted_radio) layout.addWidget(source_group) # Schritt 1: Punktdatei laden load_group = QGroupBox("Schritt 1: Soll-Koordinaten laden") load_layout = QHBoxLayout(load_group) self.load_file_btn = QPushButton("📂 Punktdatei laden (COR/CSV)") self.load_file_btn.clicked.connect(self.load_target_file) self.load_file_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") load_layout.addWidget(self.load_file_btn) self.loaded_file_label = QLabel("Keine Datei geladen") self.loaded_file_label.setStyleSheet("color: #666;") load_layout.addWidget(self.loaded_file_label) load_layout.addStretch() layout.addWidget(load_group) # Schritt 2: Punkt-Zuordnung assign_group = QGroupBox("Schritt 2: Punkt-Zuordnung (Soll → Ist)") assign_layout = QVBoxLayout(assign_group) # Automatische Zuordnung Button auto_layout = QHBoxLayout() auto_btn = QPushButton("🔍 Automatische Zuordnung (Tripel-Analyse)") auto_btn.clicked.connect(self.auto_assign_points) auto_btn.setStyleSheet("background-color: #9C27B0; color: white; font-weight: bold;") auto_layout.addWidget(auto_btn) self.auto_result_label = QLabel("") auto_layout.addWidget(self.auto_result_label) auto_layout.addStretch() assign_layout.addLayout(auto_layout) # Zuordnungstabelle self.assign_table = QTableWidget() self.assign_table.setColumnCount(8) self.assign_table.setHorizontalHeaderLabels([ "Soll-Punkt", "X_Soll", "Y_Soll", "Z_Soll", "JXL-Punkt ⬇", "X_Ist", "Y_Ist", "Z_Ist" ]) self.assign_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) assign_layout.addWidget(self.assign_table) layout.addWidget(assign_group) # Schritt 3: Berechnung calc_group = QGroupBox("Schritt 3: Georeferenzierung") calc_layout = QHBoxLayout(calc_group) calc_btn = QPushButton("🔄 Transformation berechnen") calc_btn.clicked.connect(self.calculate_transformation) calc_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") calc_layout.addWidget(calc_btn) apply_btn = QPushButton("✓ Auf alle Punkte anwenden") apply_btn.clicked.connect(self.apply_to_all_points) apply_btn.setStyleSheet("background-color: #2196F3; color: white;") calc_layout.addWidget(apply_btn) layout.addWidget(calc_group) # Ergebnisse results_group = QGroupBox("Ergebnisse") results_layout = QVBoxLayout(results_group) self.results_text = QTextEdit() self.results_text.setReadOnly(True) self.results_text.setFont(QFont("Courier", 9)) results_layout.addWidget(self.results_text) export_btn = QPushButton("📥 Punkte exportieren...") export_btn.clicked.connect(self.export_transformed_points) results_layout.addWidget(export_btn) layout.addWidget(results_group) def showEvent(self, event): super().showEvent(event) self.update_adjusted_points_status() def update_adjusted_points_status(self): if adjusted_points_store.is_available(): self.source_adjusted_radio.setEnabled(True) self.source_adjusted_radio.setText( f"Ausgeglichene Punkte verwenden ({len(adjusted_points_store.points)} Punkte)") else: self.source_adjusted_radio.setEnabled(False) self.source_jxl_radio.setChecked(True) def get_ist_points(self): """Gibt die Ist-Punkte zurück (aus JXL oder ausgeglichene)""" if self.source_adjusted_radio.isChecked() and adjusted_points_store.is_available(): return adjusted_points_store.get_points() elif self.main_window.parser: result = {} for name, p in self.main_window.parser.get_active_points().items(): if p.east is not None and p.north is not None: result[name] = (p.east, p.north, p.elevation or 0.0) return result return {} def load_target_file(self): """Lädt eine COR/CSV-Datei mit Soll-Koordinaten""" file_path, _ = QFileDialog.getOpenFileName( self, "Soll-Koordinaten laden", "", "Koordinatendateien (*.cor *.csv *.txt);;All Files (*)") if not file_path: return try: self.loaded_target_points.clear() with open(file_path, 'r', encoding='utf-8') as f: lines = f.readlines() for line in lines: line = line.strip() if not line or line.startswith('#'): continue parts = line.replace(';', ',').split(',') if len(parts) >= 4: name = parts[0].strip() try: x = float(parts[1].strip()) y = float(parts[2].strip()) z = float(parts[3].strip()) self.loaded_target_points[name] = (x, y, z) except ValueError: continue self.loaded_file_label.setText( f"✓ {os.path.basename(file_path)} ({len(self.loaded_target_points)} Punkte)") self.loaded_file_label.setStyleSheet("color: green; font-weight: bold;") self.update_assignment_table() except Exception as e: QMessageBox.critical(self, "Fehler", f"Fehler beim Laden: {e}") def update_assignment_table(self): """Aktualisiert die Zuordnungstabelle""" self.assign_table.setRowCount(len(self.loaded_target_points)) ist_points = self.get_ist_points() jxl_names = ["-- Nicht zugeordnet --"] + sorted(ist_points.keys()) for row, (name, (x, y, z)) in enumerate(sorted(self.loaded_target_points.items())): self.assign_table.setItem(row, 0, QTableWidgetItem(name)) self.assign_table.setItem(row, 1, QTableWidgetItem(f"{x:.4f}")) self.assign_table.setItem(row, 2, QTableWidgetItem(f"{y:.4f}")) self.assign_table.setItem(row, 3, QTableWidgetItem(f"{z:.4f}")) combo = QComboBox() combo.addItems(jxl_names) # Automatische Zuordnung bei gleichem Namen if name in jxl_names: idx = combo.findText(name) if idx >= 0: combo.setCurrentIndex(idx) combo.currentTextChanged.connect( lambda text, r=row: self.on_jxl_point_selected(r, text)) self.assign_table.setCellWidget(row, 4, combo) self.assign_table.setItem(row, 5, QTableWidgetItem("")) self.assign_table.setItem(row, 6, QTableWidgetItem("")) self.assign_table.setItem(row, 7, QTableWidgetItem("")) self.on_jxl_point_selected(row, combo.currentText()) def on_jxl_point_selected(self, row, jxl_name): """Wird aufgerufen, wenn ein JXL-Punkt ausgewählt wird""" ist_points = self.get_ist_points() if jxl_name == "-- Nicht zugeordnet --" or jxl_name not in ist_points: self.assign_table.setItem(row, 5, QTableWidgetItem("")) self.assign_table.setItem(row, 6, QTableWidgetItem("")) self.assign_table.setItem(row, 7, QTableWidgetItem("")) return x, y, z = ist_points[jxl_name] self.assign_table.setItem(row, 5, QTableWidgetItem(f"{x:.4f}")) self.assign_table.setItem(row, 6, QTableWidgetItem(f"{y:.4f}")) self.assign_table.setItem(row, 7, QTableWidgetItem(f"{z:.4f}")) def auto_assign_points(self): """Automatische Punktzuordnung basierend auf Tripel-Analyse""" if len(self.loaded_target_points) < 3: QMessageBox.warning(self, "Fehler", "Mindestens 3 Soll-Punkte erforderlich!") return ist_points = self.get_ist_points() if len(ist_points) < 3: QMessageBox.warning(self, "Fehler", "Mindestens 3 Ist-Punkte erforderlich!") return # Berechne Distanzen für Soll-Punkte soll_names = list(self.loaded_target_points.keys()) soll_distances = {} for i, name1 in enumerate(soll_names): for name2 in soll_names[i+1:]: x1, y1, _ = self.loaded_target_points[name1] x2, y2, _ = self.loaded_target_points[name2] dist = math.sqrt((x2-x1)**2 + (y2-y1)**2) soll_distances[(name1, name2)] = dist soll_distances[(name2, name1)] = dist # Berechne Distanzen für Ist-Punkte ist_names = list(ist_points.keys()) ist_distances = {} for i, name1 in enumerate(ist_names): for name2 in ist_names[i+1:]: x1, y1, _ = ist_points[name1] x2, y2, _ = ist_points[name2] dist = math.sqrt((x2-x1)**2 + (y2-y1)**2) ist_distances[(name1, name2)] = dist ist_distances[(name2, name1)] = dist # Finde beste Zuordnung über Tripel best_assignment = None best_rmse = float('inf') # Alle möglichen Tripel aus Soll-Punkten for soll_tripel in combinations(soll_names, 3): # Alle möglichen Tripel aus Ist-Punkten for ist_tripel in combinations(ist_names, 3): # Berechne RMSE für diese Zuordnung rmse = self._compute_tripel_rmse( soll_tripel, ist_tripel, soll_distances, ist_distances) if rmse < best_rmse: best_rmse = rmse best_assignment = dict(zip(soll_tripel, ist_tripel)) if best_assignment and best_rmse < 0.5: # Max 0.5m Abweichung # Zuordnung in Tabelle übernehmen for row in range(self.assign_table.rowCount()): soll_name = self.assign_table.item(row, 0).text() combo = self.assign_table.cellWidget(row, 4) if soll_name in best_assignment: ist_name = best_assignment[soll_name] idx = combo.findText(ist_name) if idx >= 0: combo.setCurrentIndex(idx) self.auto_result_label.setText( f"✓ Zuordnung gefunden (RMSE: {best_rmse*1000:.1f} mm)") self.auto_result_label.setStyleSheet("color: green; font-weight: bold;") else: self.auto_result_label.setText("❌ Keine passende Zuordnung gefunden") self.auto_result_label.setStyleSheet("color: red;") def _compute_tripel_rmse(self, soll_tripel, ist_tripel, soll_dist, ist_dist): """Berechnet RMSE für eine Tripel-Zuordnung""" errors = [] for i in range(3): for j in range(i+1, 3): s1, s2 = soll_tripel[i], soll_tripel[j] i1, i2 = ist_tripel[i], ist_tripel[j] d_soll = soll_dist.get((s1, s2), 0) d_ist = ist_dist.get((i1, i2), 0) if d_soll > 0 and d_ist > 0: errors.append((d_soll - d_ist) ** 2) if errors: return math.sqrt(sum(errors) / len(errors)) return float('inf') def calculate_transformation(self): """Berechnet die Transformation""" if not self.loaded_target_points: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine Punktdatei laden!") return ist_points = self.get_ist_points() if not ist_points: QMessageBox.warning(self, "Fehler", "Keine Ist-Punkte verfügbar!") return self.georeferencer.clear_control_points() valid_pairs = 0 for row in range(self.assign_table.rowCount()): combo = self.assign_table.cellWidget(row, 4) if not combo: continue jxl_name = combo.currentText() if jxl_name == "-- Nicht zugeordnet --": continue target_name = self.assign_table.item(row, 0).text() if target_name not in self.loaded_target_points: continue target_x, target_y, target_z = self.loaded_target_points[target_name] if jxl_name not in ist_points: continue local_x, local_y, local_z = ist_points[jxl_name] self.georeferencer.add_control_point( jxl_name, local_x, local_y, local_z, target_x, target_y, target_z ) valid_pairs += 1 if valid_pairs < 2: QMessageBox.warning(self, "Fehler", f"Mindestens 2 gültige Punkt-Paare erforderlich! Aktuell: {valid_pairs}") return try: self.georeferencer.compute_transformation() report = self.georeferencer.get_transformation_report() self.results_text.setText(report) self.main_window.statusBar().showMessage( f"Georeferenzierung berechnet ({valid_pairs} Passpunkte)") except Exception as e: QMessageBox.critical(self, "Fehler", f"Berechnung fehlgeschlagen: {e}") def apply_to_all_points(self): """Wendet die Transformation auf alle Punkte an""" if self.georeferencer.result is None: QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!") return if not self.main_window.parser: return reply = QMessageBox.question( self, "Bestätigung", "Transformation auf alle Punkte anwenden?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: return ist_points = self.get_ist_points() points = [CORPoint(name=n, x=x, y=y, z=z) for n, (x, y, z) in ist_points.items()] self.georeferencer.set_points_to_transform(points) transformed = self.georeferencer.transform_points() for tp in transformed: if tp.name in self.main_window.parser.points: p = self.main_window.parser.points[tp.name] p.east = tp.x p.north = tp.y p.elevation = tp.z jxl_tab = self.main_window.tabs.widget(0) if hasattr(jxl_tab, 'update_display'): jxl_tab.update_display() QMessageBox.information(self, "Erfolg", f"{len(transformed)} Punkte georeferenziert!") def export_transformed_points(self): if self.georeferencer.result is None: QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!") return ist_points = self.get_ist_points() points = [CORPoint(name=n, x=x, y=y, z=z) for n, (x, y, z) in ist_points.items()] self.georeferencer.set_points_to_transform(points) transformed = self.georeferencer.transform_points() export_points_with_dialog(self, transformed, "georeferenziert") # ============================================================================= # Netzausgleichung Tab (KOMPLETT ÜBERARBEITET) # ============================================================================= class NetworkAdjustmentTab(QWidget): """ Tab für Netzausgleichung - KORREKTES KONZEPT: - Festpunkte = NUR 5xxx-Passpunkte (5001, 5002, etc.) - werden NICHT ausgeglichen - Neupunkte = ALLE anderen Punkte (Standpunkte, Anschlusspunkte, Messpunkte) - werden AUSGEGLICHEN - Das gesamte Netz wird spannungsfrei ausgeglichen """ def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.adjustment = None self.fixed_points = set() # Passpunkte (nicht ausgeglichen) self.new_points = set() # Standpunkte (ausgeglichen) self.measurement_points = set() # Messpunkte (ausgeglichen) self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Konfiguration config_group = QGroupBox("Konfiguration") config_layout = QGridLayout(config_group) config_layout.addWidget(QLabel("Max. Iterationen:"), 0, 0) self.max_iter_spin = QSpinBox() self.max_iter_spin.setRange(1, 100) self.max_iter_spin.setValue(20) config_layout.addWidget(self.max_iter_spin, 0, 1) config_layout.addWidget(QLabel("Konvergenzlimit [mm]:"), 1, 0) self.convergence_spin = QDoubleSpinBox() self.convergence_spin.setRange(0.001, 10) self.convergence_spin.setDecimals(3) self.convergence_spin.setValue(0.01) config_layout.addWidget(self.convergence_spin, 1, 1) layout.addWidget(config_group) # Punktklassifikation points_group = QGroupBox("Punktklassifikation (KORREKTES KONZEPT)") points_layout = QVBoxLayout(points_group) info_label = QLabel( "💡 KORREKTES KONZEPT (Netzausgleichung):\n" " • Festpunkte (grün): NUR 5xxx-Passpunkte (5001, 5002) - werden NICHT ausgeglichen\n" " • Neupunkte (blau/gelb): ALLE anderen Punkte - werden AUSGEGLICHEN\n" " (inkl. Standpunkte 1xxx, Anschlusspunkte 2xxx/6xxx, Messpunkte 3xxx)" ) info_label.setStyleSheet("background-color: #f0f0f0; padding: 10px;") points_layout.addWidget(info_label) self.points_table = QTableWidget() self.points_table.setColumnCount(5) self.points_table.setHorizontalHeaderLabels(["Punkt", "Typ", "X", "Y", "Z"]) self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.points_table.setMaximumHeight(200) points_layout.addWidget(self.points_table) refresh_btn = QPushButton("🔍 Punkte automatisch klassifizieren") refresh_btn.clicked.connect(self.auto_classify_points) refresh_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;") points_layout.addWidget(refresh_btn) layout.addWidget(points_group) # Ausgleichung btn_layout = QHBoxLayout() adjust_btn = QPushButton("📐 Netzausgleichung durchführen") adjust_btn.clicked.connect(self.run_adjustment) adjust_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; font-size: 14px; padding: 10px;") btn_layout.addWidget(adjust_btn) self.adopt_btn = QPushButton("✓ Ausgeglichene Punkte übernehmen") self.adopt_btn.clicked.connect(self.adopt_adjusted_points) self.adopt_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") self.adopt_btn.setEnabled(False) btn_layout.addWidget(self.adopt_btn) layout.addLayout(btn_layout) # Ergebnisse results_group = QGroupBox("Ergebnisse") results_layout = QVBoxLayout(results_group) self.results_text = QTextEdit() self.results_text.setReadOnly(True) self.results_text.setFont(QFont("Courier", 9)) results_layout.addWidget(self.results_text) export_layout = QHBoxLayout() export_report_btn = QPushButton("Bericht exportieren") export_report_btn.clicked.connect(self.export_report) export_layout.addWidget(export_report_btn) export_points_btn = QPushButton("📥 Koordinaten exportieren...") export_points_btn.clicked.connect(self.export_points) export_layout.addWidget(export_points_btn) results_layout.addLayout(export_layout) layout.addWidget(results_group) def auto_classify_points(self): """Klassifiziert Punkte automatisch nach dem korrekten Konzept""" if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return parser = self.main_window.parser self.fixed_points.clear() self.new_points.clear() self.measurement_points.clear() # 1. FESTPUNKTE = Passpunkte (Referenzpunkte mit bekannten Koordinaten) ref_points = parser.get_reference_points() for name in ref_points: if name in parser.points: self.fixed_points.add(name) # 2. NEUPUNKTE = Standpunkte (Stationen des Tachymeters) station_points = parser.get_station_points() for name in station_points: if name not in self.fixed_points: # Nicht wenn es ein Passpunkt ist self.new_points.add(name) # 3. MESSPUNKTE = Alle anderen Punkte for name in parser.get_active_points().keys(): if name not in self.fixed_points and name not in self.new_points: self.measurement_points.add(name) self.update_points_table() self.main_window.statusBar().showMessage( f"Klassifiziert: {len(self.fixed_points)} Festpunkte, " f"{len(self.new_points)} Neupunkte, " f"{len(self.measurement_points)} Messpunkte") def update_points_table(self): """Aktualisiert die Punkttabelle""" parser = self.main_window.parser if not parser: return all_classified = [] for name in self.fixed_points: all_classified.append((name, "Festpunkt", QColor(200, 255, 200))) for name in self.new_points: all_classified.append((name, "Neupunkt", QColor(200, 200, 255))) for name in self.measurement_points: all_classified.append((name, "Messpunkt", QColor(255, 255, 200))) self.points_table.setRowCount(len(all_classified)) for row, (name, point_type, color) in enumerate(sorted(all_classified)): name_item = QTableWidgetItem(name) name_item.setBackground(QBrush(color)) self.points_table.setItem(row, 0, name_item) type_item = QTableWidgetItem(point_type) type_item.setBackground(QBrush(color)) self.points_table.setItem(row, 1, type_item) if name in parser.points: p = parser.points[name] x_item = QTableWidgetItem(f"{p.east:.4f}" if p.east else "") y_item = QTableWidgetItem(f"{p.north:.4f}" if p.north else "") z_item = QTableWidgetItem(f"{p.elevation:.4f}" if p.elevation else "") x_item.setBackground(QBrush(color)) y_item.setBackground(QBrush(color)) z_item.setBackground(QBrush(color)) self.points_table.setItem(row, 2, x_item) self.points_table.setItem(row, 3, y_item) self.points_table.setItem(row, 4, z_item) def run_adjustment(self): """Führt die Netzausgleichung durch""" if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return if not self.fixed_points and not self.new_points and not self.measurement_points: self.auto_classify_points() if not self.fixed_points: QMessageBox.warning(self, "Fehler", "Keine Festpunkte erkannt! Mindestens ein Passpunkt mit bekannten Koordinaten wird benötigt.") return self.adjustment = NetworkAdjustment(self.main_window.parser) self.adjustment.max_iterations = self.max_iter_spin.value() self.adjustment.convergence_limit = self.convergence_spin.value() / 1000.0 self.adjustment.extract_observations() self.adjustment.initialize_points() # NUR Festpunkte setzen - Neupunkte und Messpunkte werden ausgeglichen! for point_name in self.fixed_points: self.adjustment.set_fixed_point(point_name) try: # Konsistenzprüfung durchführen consistency = self.adjustment.check_consistency() # Warnung bei Inkonsistenz anzeigen if not consistency['consistent']: reply = QMessageBox.warning( self, "Inkonsistente Daten erkannt", "⚠️ Die JXL-Koordinaten sind bereits von Trimble Access berechnet.\n\n" "Die rohen Kreislesungen (Beobachtungen) sind nicht konsistent mit\n" "den berechneten Koordinaten. Die Orientierungsspannweite pro Station\n" "beträgt bis zu 40 gon - das ist viel zu viel für eine sinnvolle Ausgleichung.\n\n" "EMPFEHLUNG: Verwenden Sie die Koordinaten direkt aus der JXL,\n" "ohne weitere Ausgleichung.\n\n" "Trotzdem fortfahren (nur für Diagnose)?", QMessageBox.Yes | QMessageBox.No, QMessageBox.No ) if reply == QMessageBox.No: report = self.create_consistency_report(consistency) self.results_text.setText(report) self.adopt_btn.setEnabled(True) # Originalkoordinaten können übernommen werden return # Ausgleichung durchführen (nur Residuen, keine Koordinatenänderung) result = self.adjustment.adjust(mode="residuals_only") report = self.create_detailed_report() self.results_text.setText(report) self.adopt_btn.setEnabled(True) status = "abgeschlossen (keine Änderung)" if result.iterations == 0 else f"konvergiert nach {result.iterations} Iterationen" self.main_window.statusBar().showMessage(f"Analyse {status}") except Exception as e: QMessageBox.critical(self, "Fehler", f"Ausgleichung fehlgeschlagen: {e}") import traceback traceback.print_exc() def create_consistency_report(self, consistency): """Erstellt einen Konsistenzbericht""" lines = [] lines.append("=" * 90) lines.append("KONSISTENZPRÜFUNG - ERGEBNIS") lines.append("=" * 90) lines.append("") lines.append("❌ INKONSISTENZ ERKANNT") lines.append("") lines.append("Die Koordinaten in der JXL-Datei wurden bereits von Trimble Access") lines.append("berechnet. Die Beobachtungen (Kreislesungen) sind rohe Messwerte,") lines.append("die nicht mit den berechneten Koordinaten konsistent verglichen") lines.append("werden können.") lines.append("") lines.append("-" * 90) lines.append("PROBLEME:") lines.append("-" * 90) for issue in consistency['issues'][:10]: lines.append(f" • {issue}") if len(consistency['issues']) > 10: lines.append(f" ... und {len(consistency['issues']) - 10} weitere") lines.append("") lines.append("-" * 90) lines.append("EMPFEHLUNG:") lines.append("-" * 90) lines.append("") lines.append(" 1. Verwenden Sie die Koordinaten direkt aus der JXL") lines.append(" 2. Nutzen Sie den COR-Generator zum Exportieren") lines.append(" 3. Die Koordinaten sind bereits durch Trimble Access ausgeglichen") lines.append("") lines.append("=" * 90) lines.append("") lines.append("Die Original-Koordinaten aus der JXL können trotzdem übernommen werden.") lines.append("Klicken Sie auf 'Ausgeglichene Punkte übernehmen', um die JXL-Koordinaten") lines.append("in anderen Modulen zu verwenden.") return "\n".join(lines) def create_detailed_report(self): """Erstellt einen detaillierten Bericht""" if not self.adjustment or not self.adjustment.result: return "Keine Ergebnisse." lines = [] lines.append("=" * 90) lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT") lines.append("=" * 90) lines.append("") lines.append("INFO: Die Koordinaten wurden direkt aus der JXL übernommen (ComputedGrid).") lines.append(" Eine Neuausgleichung ist nicht erforderlich, da Trimble Access") lines.append(" die Koordinaten bereits berechnet hat.") lines.append("") lines.append("-" * 90) lines.append("STATISTIK") lines.append("-" * 90) lines.append(f"Festpunkte (Passpunkte): {len(self.fixed_points)}") lines.append(f"Neupunkte (Standpunkte): {len(self.new_points)}") lines.append(f"Messpunkte: {len(self.measurement_points)}") lines.append(f"Beobachtungen: {self.adjustment.result.num_observations}") lines.append(f"Redundanz: {self.adjustment.result.redundancy}") lines.append("") # Alle Punkte (unverändert aus JXL) lines.append("-" * 90) lines.append("KOORDINATEN AUS JXL (ComputedGrid)") lines.append("-" * 90) lines.append(f"{'Punkt':<12} {'Typ':>12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12}") lines.append("-" * 90) for name in sorted(self.fixed_points): if name in self.adjustment.points: p = self.adjustment.points[name] lines.append(f"{name:<12} {'Festpunkt':>12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}") for name in sorted(self.new_points): if name in self.adjustment.points: p = self.adjustment.points[name] lines.append(f"{name:<12} {'Neupunkt':>12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}") for name in sorted(self.measurement_points): if name in self.adjustment.points: p = self.adjustment.points[name] lines.append(f"{name:<12} {'Messpunkt':>12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}") lines.append("") lines.append("-" * 90) lines.append("ORIENTIERUNGEN PRO STATION") lines.append("-" * 90) for station, ori in sorted(self.adjustment.orientations.items()): lines.append(f" {station}: {ori:.4f} gon") lines.append("") lines.append("=" * 90) lines.append("") lines.append("✅ Koordinaten sind verfügbar und können übernommen werden.") lines.append(" Klicken Sie auf 'Ausgeglichene Punkte übernehmen'.") return "\n".join(lines) def adopt_adjusted_points(self): """Übernimmt ausgeglichene Punkte in den globalen Speicher""" if not self.adjustment or not self.adjustment.points: QMessageBox.warning(self, "Fehler", "Keine ausgeglichenen Punkte!") return points_dict = {} for name, p in self.adjustment.points.items(): points_dict[name] = (p.x, p.y, p.z) adjusted_points_store.set_points(points_dict) QMessageBox.information(self, "Erfolg", f"{len(points_dict)} ausgeglichene Punkte übernommen!\n\n" "Die Punkte sind jetzt in anderen Modulen verfügbar:\n" "• COR Generator\n" "• Transformation\n" "• Georeferenzierung") self.main_window.statusBar().showMessage( f"Ausgeglichene Punkte verfügbar ({len(points_dict)} Punkte)") def export_report(self): if not self.adjustment: QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!") return report = self.create_detailed_report() export_text_with_dialog(self, report, "netzausgleichung_bericht") def export_points(self): if not self.adjustment or not self.adjustment.result: QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!") return points = [] for name, p in self.adjustment.points.items(): points.append(CORPoint(name=name, x=p.x, y=p.y, z=p.z)) export_points_with_dialog(self, points, "ausgeglichen") # ============================================================================= # Referenzpunkt-Anpassung Tab # ============================================================================= class ReferencePointAdjusterTab(QWidget): """Tab für Referenzpunkt-Anpassung""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.adjuster = ReferencePointAdjuster() self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Info-Gruppe info_group = QGroupBox("Aktueller Referenzpunkt") info_layout = QFormLayout(info_group) self.ref_point_name = QLabel("Nicht geladen") self.ref_point_name.setStyleSheet("font-weight: bold; font-size: 14px;") info_layout.addRow("Punktname:", self.ref_point_name) self.current_east = QLabel("0.000") self.current_north = QLabel("0.000") self.current_elev = QLabel("0.000") info_layout.addRow("East (X):", self.current_east) info_layout.addRow("North (Y):", self.current_north) info_layout.addRow("Elevation (Z):", self.current_elev) refresh_btn = QPushButton("Referenzpunkt aktualisieren") refresh_btn.clicked.connect(self.load_reference_point) info_layout.addRow("", refresh_btn) layout.addWidget(info_group) # Neue Koordinaten new_coords_group = QGroupBox("Neue Koordinaten") new_coords_layout = QGridLayout(new_coords_group) new_coords_layout.addWidget(QLabel("East (X):"), 0, 0) self.new_east_spin = QDoubleSpinBox() self.new_east_spin.setRange(-10000000, 10000000) self.new_east_spin.setDecimals(4) self.new_east_spin.setSuffix(" m") new_coords_layout.addWidget(self.new_east_spin, 0, 1) new_coords_layout.addWidget(QLabel("North (Y):"), 1, 0) self.new_north_spin = QDoubleSpinBox() self.new_north_spin.setRange(-10000000, 10000000) self.new_north_spin.setDecimals(4) self.new_north_spin.setSuffix(" m") new_coords_layout.addWidget(self.new_north_spin, 1, 1) new_coords_layout.addWidget(QLabel("Elevation (Z):"), 2, 0) self.new_elev_spin = QDoubleSpinBox() self.new_elev_spin.setRange(-10000, 10000) self.new_elev_spin.setDecimals(4) self.new_elev_spin.setSuffix(" m") new_coords_layout.addWidget(self.new_elev_spin, 2, 1) self.delta_label = QLabel("ΔX: 0.000 m | ΔY: 0.000 m | ΔZ: 0.000 m") self.delta_label.setStyleSheet("color: blue;") new_coords_layout.addWidget(self.delta_label, 3, 0, 1, 2) self.new_east_spin.valueChanged.connect(self.update_delta) self.new_north_spin.valueChanged.connect(self.update_delta) self.new_elev_spin.valueChanged.connect(self.update_delta) layout.addWidget(new_coords_group) # Aktionen actions_layout = QHBoxLayout() preview_btn = QPushButton("Vorschau") preview_btn.clicked.connect(self.preview_transformation) preview_btn.setStyleSheet("background-color: #4CAF50; color: white;") actions_layout.addWidget(preview_btn) apply_btn = QPushButton("Anwenden") apply_btn.clicked.connect(self.apply_transformation) apply_btn.setStyleSheet("background-color: #2196F3; color: white;") actions_layout.addWidget(apply_btn) export_btn = QPushButton("📥 Exportieren...") export_btn.clicked.connect(self.export_points) export_btn.setStyleSheet("background-color: #FF9800; color: white;") actions_layout.addWidget(export_btn) layout.addLayout(actions_layout) # Vorschau preview_group = QGroupBox("Vorschau") preview_layout = QVBoxLayout(preview_group) self.preview_table = QTableWidget() self.preview_table.setColumnCount(7) self.preview_table.setHorizontalHeaderLabels( ["Punkt", "Alt X", "Alt Y", "Alt Z", "Neu X", "Neu Y", "Neu Z"]) self.preview_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) preview_layout.addWidget(self.preview_table) layout.addWidget(preview_group) # Bericht report_group = QGroupBox("Bericht") report_layout = QVBoxLayout(report_group) self.report_text = QTextEdit() self.report_text.setReadOnly(True) self.report_text.setFont(QFont("Courier", 9)) self.report_text.setMaximumHeight(150) report_layout.addWidget(self.report_text) layout.addWidget(report_group) def load_reference_point(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!") return self.adjuster.set_parser(self.main_window.parser) info = self.adjuster.get_reference_point_info() if not info["found"]: QMessageBox.warning(self, "Fehler", info['message']) return self.ref_point_name.setText(info["name"]) self.current_east.setText(f"{info['east']:.4f} m") self.current_north.setText(f"{info['north']:.4f} m") self.current_elev.setText(f"{info['elevation']:.4f} m") self.new_east_spin.setValue(info['east']) self.new_north_spin.setValue(info['north']) self.new_elev_spin.setValue(info['elevation']) def update_delta(self): if not self.adjuster.parser: return dx = self.new_east_spin.value() - self.adjuster.original_coords[0] dy = self.new_north_spin.value() - self.adjuster.original_coords[1] dz = self.new_elev_spin.value() - self.adjuster.original_coords[2] self.delta_label.setText(f"ΔX: {dx:+.4f} m | ΔY: {dy:+.4f} m | ΔZ: {dz:+.4f} m") def preview_transformation(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!") return valid, message = self.adjuster.validate_input( self.new_east_spin.value(), self.new_north_spin.value(), self.new_elev_spin.value() ) if not valid: reply = QMessageBox.warning(self, "Warnung", f"{message}\n\nTrotzdem fortfahren?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: return self.adjuster.set_new_coordinates( self.new_east_spin.value(), self.new_north_spin.value(), self.new_elev_spin.value() ) results = self.adjuster.preview_transformation() self.preview_table.setRowCount(len(results)) for i, result in enumerate(results): self.preview_table.setItem(i, 0, QTableWidgetItem(result.original_point)) self.preview_table.setItem(i, 1, QTableWidgetItem(f"{result.original_coords[0]:.4f}")) self.preview_table.setItem(i, 2, QTableWidgetItem(f"{result.original_coords[1]:.4f}")) self.preview_table.setItem(i, 3, QTableWidgetItem(f"{result.original_coords[2]:.4f}")) self.preview_table.setItem(i, 4, QTableWidgetItem(f"{result.new_coords[0]:.4f}")) self.preview_table.setItem(i, 5, QTableWidgetItem(f"{result.new_coords[1]:.4f}")) self.preview_table.setItem(i, 6, QTableWidgetItem(f"{result.new_coords[2]:.4f}")) self.report_text.setText(self.adjuster.get_summary_report()) def apply_transformation(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!") return if not self.adjuster.affected_points: QMessageBox.warning(self, "Fehler", "Bitte zuerst Vorschau berechnen!") return reply = QMessageBox.question(self, "Bestätigung", f"Transformation auf {len(self.adjuster.affected_points)} Punkte anwenden?", QMessageBox.Yes | QMessageBox.No) if reply == QMessageBox.No: return if self.adjuster.apply_transformation(): self.report_text.setText(self.adjuster.get_summary_report()) jxl_tab = self.main_window.tabs.widget(0) if hasattr(jxl_tab, 'update_display'): jxl_tab.update_display() QMessageBox.information(self, "Erfolg", "Transformation angewendet!") def export_points(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Keine Punkte!") return points = [] for name, p in self.main_window.parser.get_active_points().items(): if p.east is not None and p.north is not None: points.append(CORPoint(name=name, x=p.east, y=p.north, z=p.elevation or 0)) export_points_with_dialog(self, points, "angepasst") # ============================================================================= # Hauptfenster # ============================================================================= class MainWindow(QMainWindow): """Hauptfenster der Anwendung""" def __init__(self): super().__init__() self.parser = None self.setWindowTitle("Trimble Geodesy Tool v3.0") self.setMinimumSize(1200, 850) self.setup_ui() self.setup_menu() def setup_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # Status für ausgeglichene Punkte self.adjusted_status = QLabel("🔴 Keine ausgeglichenen Punkte verfügbar") self.adjusted_status.setStyleSheet("background-color: #fff3e0; padding: 5px;") main_layout.addWidget(self.adjusted_status) # Tab-Widget self.tabs = QTabWidget() self.tabs.addTab(JXLAnalysisTab(self), "📊 JXL-Analyse") self.tabs.addTab(CORGeneratorTab(self), "📄 COR-Generator") self.tabs.addTab(TransformationTab(self), "🔄 Transformation") self.tabs.addTab(GeoreferencingTab(self), "🌍 Georeferenzierung") self.tabs.addTab(NetworkAdjustmentTab(self), "📐 Netzausgleichung") self.tabs.addTab(ReferencePointAdjusterTab(self), "📍 Referenzpunkt") self.tabs.currentChanged.connect(self.on_tab_changed) main_layout.addWidget(self.tabs) # Status Bar self.setStatusBar(QStatusBar()) self.statusBar().showMessage("Bereit - Bitte JXL-Datei laden") def on_tab_changed(self, index): """Aktualisiert Status bei Tab-Wechsel""" self.update_adjusted_status() def update_adjusted_status(self): """Aktualisiert die Statusanzeige für ausgeglichene Punkte""" if adjusted_points_store.is_available(): n = len(adjusted_points_store.points) self.adjusted_status.setText(f"🟢 {n} ausgeglichene Punkte verfügbar") self.adjusted_status.setStyleSheet("background-color: #e8f5e9; padding: 5px; color: green;") else: self.adjusted_status.setText("🔴 Keine ausgeglichenen Punkte verfügbar") self.adjusted_status.setStyleSheet("background-color: #fff3e0; padding: 5px;") def setup_menu(self): menubar = self.menuBar() # Datei-Menü file_menu = menubar.addMenu("Datei") open_action = QAction("JXL öffnen...", self) open_action.setShortcut("Ctrl+O") open_action.triggered.connect(self.open_jxl_file) file_menu.addAction(open_action) file_menu.addSeparator() exit_action = QAction("Beenden", self) exit_action.setShortcut("Ctrl+Q") exit_action.triggered.connect(self.close) file_menu.addAction(exit_action) # Hilfe-Menü help_menu = menubar.addMenu("Hilfe") about_action = QAction("Über...", self) about_action.triggered.connect(self.show_about) help_menu.addAction(about_action) def open_jxl_file(self): file_path, _ = QFileDialog.getOpenFileName( self, "JXL-Datei öffnen", "", "JXL Files (*.jxl);;All Files (*)") if file_path: jxl_tab = self.tabs.widget(0) jxl_tab.file_path_edit.setText(file_path) jxl_tab.load_file() def show_about(self): QMessageBox.about(self, "Über Trimble Geodesy Tool", "Trimble Geodesy Tool v3.0\n\n" "Geodätische Vermessungsarbeiten mit JXL-Dateien\n\n" "Features:\n" "• JXL-Datei Analyse mit Berechnungsprotokoll\n" "• COR/CSV/TXT/DXF Export\n" "• Koordinatentransformation\n" "• Georeferenzierung mit automatischer Punktzuordnung\n" "• Netzausgleichung (korrektes Konzept)\n" "• Datenfluss zwischen Modulen\n" "• Referenzpunkt-Anpassung") def main(): app = QApplication(sys.argv) app.setStyle("Fusion") # Theme palette = QPalette() palette.setColor(QPalette.Window, QColor(240, 240, 240)) palette.setColor(QPalette.WindowText, QColor(0, 0, 0)) palette.setColor(QPalette.Base, QColor(255, 255, 255)) palette.setColor(QPalette.AlternateBase, QColor(245, 245, 245)) palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 220)) palette.setColor(QPalette.ToolTipText, QColor(0, 0, 0)) palette.setColor(QPalette.Text, QColor(0, 0, 0)) palette.setColor(QPalette.Button, QColor(240, 240, 240)) palette.setColor(QPalette.ButtonText, QColor(0, 0, 0)) palette.setColor(QPalette.BrightText, QColor(255, 0, 0)) palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255)) app.setPalette(palette) window = MainWindow() window.show() sys.exit(app.exec_()) if __name__ == "__main__": main()