#!/usr/bin/env python3 """ Trimble Geodesy Tool - Hauptprogramm mit GUI Geodätische Vermessungsarbeiten mit JXL-Dateien Überarbeitet: Export-Dialog, Georeferenzierung, TreeView """ import sys import os from pathlib import Path # 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 # ============================================================================= # 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) # ============================================================================= # 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", "Typ", "Prismenkonstante [mm]", "Qualität", "Aktiv" ]) self.stations_tree.setColumnCount(5) self.stations_tree.setSelectionMode(QAbstractItemView.SingleSelection) self.stations_tree.setMinimumHeight(200) # Spaltenbreiten self.stations_tree.setColumnWidth(0, 250) self.stations_tree.setColumnWidth(1, 120) self.stations_tree.setColumnWidth(2, 140) self.stations_tree.setColumnWidth(3, 100) self.stations_tree.setColumnWidth(4, 60) stations_layout.addWidget(self.stations_tree) # Info-Label info_label = QLabel( "💡 Prismenkonstanten können direkt geändert werden. " "Bei freien Stationierungen: Passpunkte mit Checkbox aktivieren/deaktivieren." ) info_label.setStyleSheet("color: #666; font-style: italic;") info_label.setWordWrap(True) stations_layout.addWidget(info_label) 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, 300, 250]) 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 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 for station_id, station in parser.stations.items(): # Station als Hauptknoten station_item = QTreeWidgetItem() station_item.setText(0, f"📍 {station.name}") station_item.setText(1, station.station_type) station_item.setData(0, Qt.UserRole, station_id) # Hintergrundfarbe if station.station_type == "ReflineStationSetup": station_item.setBackground(0, QBrush(QColor(200, 230, 200))) elif station.station_type == "StandardResection": station_item.setBackground(0, QBrush(QColor(200, 200, 230))) self.stations_tree.addTopLevelItem(station_item) # Messungen measurements = parser.get_measurements_from_station(station_id) # Qualitätsberechnung für freie Stationierung control_point_residuals = {} if station.station_type == "StandardResection": control_point_residuals = self.calculate_control_point_quality( station_id, station, measurements) for meas in measurements: meas_item = QTreeWidgetItem() meas_item.setText(0, f" ↳ {meas.name}") # Typ if meas.classification == "BackSight": meas_type = "Passpunkt" else: meas_type = "Messung" meas_item.setText(1, meas_type) # Prismenkonstante SpinBox if meas.target_id and meas.target_id in parser.targets: target = parser.targets[meas.target_id] prism_spin = QDoubleSpinBox() prism_spin.setRange(-100, 100) prism_spin.setDecimals(1) prism_spin.setValue(target.prism_constant * 1000) prism_spin.setSuffix(" mm") prism_spin.setProperty("target_id", meas.target_id) prism_spin.valueChanged.connect( lambda val, tid=meas.target_id: self.on_prism_changed(tid, val)) self.prism_spin_widgets[meas.record_id] = prism_spin self.stations_tree.setItemWidget(meas_item, 2, prism_spin) # Bei freier Stationierung: Qualität und Checkbox if station.station_type == "StandardResection" and meas.classification == "BackSight": # Qualitätswert if meas.name in control_point_residuals: quality_info = control_point_residuals[meas.name] quality_value = quality_info['residual'] rank = quality_info['rank'] total = quality_info['total'] quality_label = QLabel(f"{quality_value:.1f} mm") if rank == 1: quality_label.setStyleSheet("background-color: #90EE90; padding: 2px;") elif rank == total: quality_label.setStyleSheet("background-color: #FFB6C1; padding: 2px;") else: quality_label.setStyleSheet("background-color: #FFFF99; padding: 2px;") self.stations_tree.setItemWidget(meas_item, 3, quality_label) # Checkbox checkbox = QCheckBox() checkbox.setChecked(not meas.deleted) checkbox.setProperty("station_id", station_id) checkbox.setProperty("point_name", meas.name) checkbox.stateChanged.connect( lambda state, sid=station_id, pn=meas.name: self.on_control_point_toggled(sid, pn, state)) self.control_point_checkboxes[(station_id, meas.name)] = checkbox self.stations_tree.setItemWidget(meas_item, 4, checkbox) station_item.addChild(meas_item) station_item.setExpanded(True) def calculate_control_point_quality(self, station_id, station, measurements): """Berechnet Qualität der Passpunkte""" parser = self.main_window.parser control_points = [] for meas in measurements: if meas.classification == "BackSight" and not meas.deleted: if meas.edm_distance is not None and meas.name in parser.points: target_point = parser.points[meas.name] if target_point.east is not None and target_point.north is not None: if station.east is not None and station.north is not None: dx = target_point.east - station.east dy = target_point.north - station.north calc_dist = (dx**2 + dy**2)**0.5 residual = abs(meas.edm_distance - calc_dist) * 1000 control_points.append((meas.name, residual)) if not control_points: return {} control_points.sort(key=lambda x: x[1]) result = {} for rank, (name, residual) in enumerate(control_points, 1): result[name] = {'residual': residual, 'rank': rank, 'total': len(control_points)} return result def on_prism_changed(self, target_id, new_value_mm): if self.main_window.parser and target_id in self.main_window.parser.targets: new_value_m = new_value_mm / 1000.0 self.main_window.parser.modify_prism_constant(target_id, new_value_m) self.main_window.statusBar().showMessage( f"Prismenkonstante für {target_id} auf {new_value_mm:.1f} mm gesetzt") def on_control_point_toggled(self, station_id, point_name, state): is_active = state == Qt.Checked self.main_window.statusBar().showMessage( f"Passpunkt {point_name} {'aktiviert' if is_active else 'deaktiviert'}") 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() # ============================================================================= # COR-Generator Tab # ============================================================================= class CORGeneratorTab(QWidget): """Tab für COR-Datei Generierung""" 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) # Optionen options_group = QGroupBox("Generierungsoptionen") options_layout = QGridLayout(options_group) options_layout.addWidget(QLabel("Methode:"), 0, 0) self.method_combo = QComboBox() self.method_combo.addItems([ "Aus berechneten Koordinaten (ComputedGrid)", "Aus Rohbeobachtungen berechnen" ]) options_layout.addWidget(self.method_combo, 0, 1) layout.addWidget(options_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 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) if self.method_combo.currentIndex() == 0: points = self.cor_generator.generate_from_computed_grid() else: points = self.cor_generator.compute_from_observations() # Tabelle aktualisieren self.preview_table.setRowCount(len(points)) for row, p in enumerate(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(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 (Y-Richtung entfernt) # ============================================================================= class TransformationTab(QWidget): """Tab für Koordinatentransformation - Y-Richtung entfernt""" 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) # 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) manual_layout.addWidget(QLabel("Drehpunkt X:"), 4, 0) self.pivot_x_spin = QDoubleSpinBox() self.pivot_x_spin.setRange(-1000000, 1000000) self.pivot_x_spin.setDecimals(4) manual_layout.addWidget(self.pivot_x_spin, 4, 1) manual_layout.addWidget(QLabel("Drehpunkt Y:"), 5, 0) self.pivot_y_spin = QDoubleSpinBox() self.pivot_y_spin.setRange(-1000000, 1000000) self.pivot_y_spin.setDecimals(4) manual_layout.addWidget(self.pivot_y_spin, 5, 1) layout.addWidget(self.manual_group) # 2-Punkte-Definition (OHNE Y-Richtung!) 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) # Transformation berechnen transform_btn = QPushButton("Transformation berechnen") transform_btn.clicked.connect(self.execute_transformation) transform_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") layout.addWidget(transform_btn) # Anwenden Button apply_btn = QPushButton("Transformation anwenden (Punktliste aktualisieren)") apply_btn.clicked.connect(self.apply_transformation) apply_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;") layout.addWidget(apply_btn) # 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 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("📥 Punkte 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 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() default_xy = None default_z = None for name in sorted(points): self.xy_origin_combo.addItem(name) self.z_origin_combo.addItem(name) if name == "7001": default_xy = name if name == "7002": default_z = name if default_xy: idx = self.xy_origin_combo.findText(default_xy) if idx >= 0: self.xy_origin_combo.setCurrentIndex(idx) if default_z: idx = self.z_origin_combo.findText(default_z) if idx >= 0: self.z_origin_combo.setCurrentIndex(idx) def execute_transformation(self): if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") 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.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=self.pivot_x_spin.value(), pivot_y=self.pivot_y_spin.value() ) elif self.twopoint_radio.isChecked(): origin = self.xy_origin_combo.currentText() zref = self.z_origin_combo.currentText() # Transformation zum Nullpunkt (nur Translation, keine Rotation) 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!") self.main_window.statusBar().showMessage("Transformation angewendet") def export_report(self): file_path, _ = QFileDialog.getSaveFileName( self, "Bericht speichern", "", "Text Files (*.txt)") if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write(self.results_text.toPlainText()) QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}") 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 (KOMPLETT NEU) # ============================================================================= class GeoreferencingTab(QWidget): """Tab für Georeferenzierung - NEUER WORKFLOW mit Punktdatei-Laden""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.georeferencer = Georeferencer() self.loaded_target_points = {} # {name: (x, y, z)} self.point_assignments = {} # {row: (target_name, jxl_name)} self.setup_ui() def setup_ui(self): layout = QVBoxLayout(self) # Schritt 1: Punktdatei laden load_group = QGroupBox("Schritt 1: Soll-Koordinaten laden") load_layout = QVBoxLayout(load_group) load_btn_layout = QHBoxLayout() 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_btn_layout.addWidget(self.load_file_btn) self.loaded_file_label = QLabel("Keine Datei geladen") self.loaded_file_label.setStyleSheet("color: #666;") load_btn_layout.addWidget(self.loaded_file_label) load_btn_layout.addStretch() load_layout.addLayout(load_btn_layout) layout.addWidget(load_group) # Schritt 2: Punkt-Zuordnungstabelle assign_group = QGroupBox("Schritt 2: Punkt-Zuordnung (Soll → Ist aus JXL)") assign_layout = QVBoxLayout(assign_group) info_label = QLabel( "💡 Wählen Sie für jeden geladenen Punkt den entsprechenden Punkt aus der JXL-Datei." ) info_label.setStyleSheet("color: #666; font-style: italic;") assign_layout.addWidget(info_label) 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 durchführen") 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; font-size: 14px; padding: 10px;") 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; font-weight: bold;") 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 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("📥 Punkte exportieren...") export_points_btn.clicked.connect(self.export_transformed_points) export_layout.addWidget(export_points_btn) results_layout.addLayout(export_layout) layout.addWidget(results_group) 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 # Komma oder Semikolon als Trenner 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 # Header oder ungültige Zeile überspringen 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;") # Tabelle aktualisieren self.update_assignment_table() QMessageBox.information(self, "Erfolg", f"{len(self.loaded_target_points)} Punkte geladen!") 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)) # JXL-Punkte für Dropdown jxl_points = ["-- Nicht zugeordnet --"] if self.main_window.parser: jxl_points.extend(sorted(self.main_window.parser.get_active_points().keys())) for row, (name, (x, y, z)) in enumerate(sorted(self.loaded_target_points.items())): # Soll-Punkt Name self.assign_table.setItem(row, 0, QTableWidgetItem(name)) # Soll-Koordinaten 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}")) # JXL-Punkt Dropdown combo = QComboBox() combo.addItems(jxl_points) # Automatische Zuordnung: gleicher Name? if name in jxl_points: 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) # Ist-Koordinaten (werden bei Auswahl gefüllt) self.assign_table.setItem(row, 5, QTableWidgetItem("")) self.assign_table.setItem(row, 6, QTableWidgetItem("")) self.assign_table.setItem(row, 7, QTableWidgetItem("")) # Initiale Zuordnung auslösen 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""" if jxl_name == "-- Nicht zugeordnet --" or not self.main_window.parser: self.assign_table.setItem(row, 5, QTableWidgetItem("")) self.assign_table.setItem(row, 6, QTableWidgetItem("")) self.assign_table.setItem(row, 7, QTableWidgetItem("")) return if jxl_name in self.main_window.parser.points: p = self.main_window.parser.points[jxl_name] self.assign_table.setItem(row, 5, QTableWidgetItem(f"{p.east:.4f}" if p.east else "")) self.assign_table.setItem(row, 6, QTableWidgetItem(f"{p.north:.4f}" if p.north else "")) self.assign_table.setItem(row, 7, QTableWidgetItem(f"{p.elevation:.4f}" if p.elevation else "")) def calculate_transformation(self): """Berechnet die Transformation basierend auf den Punkt-Paaren""" if not self.loaded_target_points: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine Punktdatei laden!") return if not self.main_window.parser: QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!") return self.georeferencer.clear_control_points() # Punkt-Paare sammeln 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 # Soll-Koordinaten 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] # Ist-Koordinaten aus JXL if jxl_name not in self.main_window.parser.points: continue p = self.main_window.parser.points[jxl_name] if p.east is None or p.north is None: continue local_x = p.east local_y = p.north local_z = p.elevation or 0 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!\n" f"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 JXL-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 # Punkte transformieren 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 )) self.georeferencer.set_points_to_transform(points) transformed = self.georeferencer.transform_points() # In Parser übernehmen 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 # GUI aktualisieren 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!") self.main_window.statusBar().showMessage("Georeferenzierung angewendet") def export_report(self): file_path, _ = QFileDialog.getSaveFileName( self, "Bericht speichern", "", "Text Files (*.txt)") if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write(self.results_text.toPlainText()) QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}") def export_transformed_points(self): if self.georeferencer.result is None: QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!") return if not self.main_window.parser: return # Punkte transformieren für Export 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 )) self.georeferencer.set_points_to_transform(points) transformed = self.georeferencer.transform_points() export_points_with_dialog(self, transformed, "georeferenziert") # ============================================================================= # Netzausgleichung Tab # ============================================================================= class NetworkAdjustmentTab(QWidget): """Tab für Netzausgleichung""" def __init__(self, parent=None): super().__init__(parent) self.main_window = parent self.adjustment = None self.fixed_points = set() self.measurement_points = set() 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) config_layout.addWidget(QLabel("Sigma-0 a-priori:"), 2, 0) self.sigma0_spin = QDoubleSpinBox() self.sigma0_spin.setRange(0.1, 10) self.sigma0_spin.setDecimals(2) self.sigma0_spin.setValue(1.0) config_layout.addWidget(self.sigma0_spin, 2, 1) layout.addWidget(config_group) # Automatisch erkannte Punkte points_group = QGroupBox("Automatisch erkannte Punkttypen") points_layout = QVBoxLayout(points_group) info_label = QLabel( "💡 Automatische Erkennung:\n" " • Festpunkte: Stationspunkte und Anschlusspunkte\n" " • Messpunkte: 3000er Punkte" ) info_label.setStyleSheet("color: #666; background-color: #f0f0f0; padding: 10px;") points_layout.addWidget(info_label) self.points_table = QTableWidget() self.points_table.setColumnCount(4) self.points_table.setHorizontalHeaderLabels(["Punkt", "Typ", "X", "Y"]) self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.points_table.setMaximumHeight(150) points_layout.addWidget(self.points_table) refresh_btn = QPushButton("Punkte automatisch erkennen") refresh_btn.clicked.connect(self.auto_detect_points) refresh_btn.setStyleSheet("background-color: #FF9800; color: white;") points_layout.addWidget(refresh_btn) layout.addWidget(points_group) # Ausgleichung durchführen adjust_btn = QPushButton("Netzausgleichung durchführen") adjust_btn.clicked.connect(self.run_adjustment) adjust_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;") layout.addWidget(adjust_btn) # 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 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_detect_points(self): 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.measurement_points.clear() for station_id, station in parser.stations.items(): if station.name: self.fixed_points.add(station.name) for bb_id, bb in parser.backbearings.items(): if bb.station_record_id == station_id and bb.backsight: self.fixed_points.add(bb.backsight) measurements = parser.get_measurements_from_station(station_id) for meas in measurements: if meas.classification == "BackSight" and meas.name: self.fixed_points.add(meas.name) for name in parser.get_active_points().keys(): if name.startswith("3"): self.measurement_points.add(name) self.update_points_table() self.main_window.statusBar().showMessage( f"Erkannt: {len(self.fixed_points)} Festpunkte, {len(self.measurement_points)} Messpunkte") def update_points_table(self): parser = self.main_window.parser if not parser: return all_points = list(self.fixed_points) + list(self.measurement_points) self.points_table.setRowCount(len(all_points)) for row, name in enumerate(sorted(all_points)): self.points_table.setItem(row, 0, QTableWidgetItem(name)) if name in self.fixed_points: type_item = QTableWidgetItem("Festpunkt") type_item.setBackground(QBrush(QColor(200, 230, 200))) else: type_item = QTableWidgetItem("Messpunkt") type_item.setBackground(QBrush(QColor(200, 200, 230))) self.points_table.setItem(row, 1, type_item) if name in parser.points: p = parser.points[name] self.points_table.setItem(row, 2, QTableWidgetItem(f"{p.east:.4f}" if p.east else "")) self.points_table.setItem(row, 3, QTableWidgetItem(f"{p.north:.4f}" if p.north else "")) def run_adjustment(self): 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.measurement_points: self.auto_detect_points() if not self.fixed_points: QMessageBox.warning(self, "Fehler", "Keine Festpunkte erkannt!") 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.sigma_0_priori = self.sigma0_spin.value() self.adjustment.extract_observations() self.adjustment.initialize_points() for point_name in self.fixed_points: self.adjustment.set_fixed_point(point_name) try: result = self.adjustment.adjust() report = self.create_detailed_report() self.results_text.setText(report) status = "konvergiert" if result.converged else "nicht konvergiert" self.main_window.statusBar().showMessage( f"Ausgleichung {status}, {result.iterations} Iterationen") except Exception as e: QMessageBox.critical(self, "Fehler", f"Ausgleichung fehlgeschlagen: {e}") import traceback traceback.print_exc() def create_detailed_report(self): if not self.adjustment or not self.adjustment.result: return "Keine Ergebnisse." lines = [] lines.append("=" * 80) lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT") lines.append("=" * 80) lines.append("") lines.append(f"Festpunkte: {len(self.fixed_points)}") lines.append(f"Messpunkte: {len(self.measurement_points)}") lines.append(f"Beobachtungen: {self.adjustment.result.num_observations}") lines.append(f"Iterationen: {self.adjustment.result.iterations}") lines.append(f"Konvergiert: {'Ja' if self.adjustment.result.converged else 'Nein'}") lines.append(f"Sigma-0 a-post.: {self.adjustment.result.sigma_0_posteriori:.4f}") lines.append("") lines.append("-" * 80) lines.append(f"{'Punkt':<12} {'Typ':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12}") lines.append("-" * 80) for name in sorted(list(self.fixed_points) + list(self.measurement_points)): if name in self.adjustment.points: p = self.adjustment.points[name] typ = "Festpunkt" if name in self.fixed_points else "Messpunkt" lines.append(f"{name:<12} {typ:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}") return "\n".join(lines) def export_report(self): if not self.adjustment: QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!") return file_path, _ = QFileDialog.getSaveFileName( self, "Bericht speichern", "", "Text Files (*.txt)") if file_path: with open(file_path, 'w', encoding='utf-8') as f: f.write(self.create_detailed_report()) QMessageBox.information(self, "Erfolg", f"Gespeichert: {file_path}") 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_group = QGroupBox("Aktionen") actions_layout = QHBoxLayout(actions_group) 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.addWidget(actions_group) # Vorschau-Tabelle 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 v2.1") self.setMinimumSize(1100, 800) self.setup_ui() self.setup_menu() def setup_ui(self): central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 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") main_layout.addWidget(self.tabs) # Status Bar self.setStatusBar(QStatusBar()) self.statusBar().showMessage("Bereit - Bitte JXL-Datei laden") 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 v2.1\n\n" "Geodätische Vermessungsarbeiten mit JXL-Dateien\n\n" "Features:\n" "• JXL-Datei Analyse mit TreeView\n" "• COR/CSV/TXT/DXF Export\n" "• Koordinatentransformation\n" "• Georeferenzierung\n" "• Netzausgleichung\n" "• Referenzpunkt-Anpassung") def main(): app = QApplication(sys.argv) app.setStyle("Fusion") # Dunkleres 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()