trimble_geodesy/main.py

2071 lines
84 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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:
result = self.adjustment.adjust()
report = self.create_detailed_report()
self.results_text.setText(report)
self.adopt_btn.setEnabled(True)
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):
"""Erstellt einen detaillierten Bericht"""
if not self.adjustment or not self.adjustment.result:
return "Keine Ergebnisse."
lines = []
lines.append("=" * 90)
lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT (KORREKTES KONZEPT)")
lines.append("=" * 90)
lines.append("")
lines.append("KONZEPT:")
lines.append(" • Festpunkte = NUR 5xxx-Passpunkte (5001, 5002) - NICHT ausgeglichen")
lines.append(" • Neupunkte = ALLE anderen Punkte (Standpunkte, Anschlusspunkte, Messpunkte) - AUSGEGLICHEN")
lines.append(" • Das gesamte Netz wird spannungsfrei ausgeglichen")
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"Unbekannte: {self.adjustment.result.num_unknowns}")
lines.append(f"Redundanz: {self.adjustment.result.redundancy}")
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-posteriori: {self.adjustment.result.sigma_0_posteriori:.4f}")
lines.append("")
# Festpunkte (unverändert)
lines.append("-" * 90)
lines.append("FESTPUNKTE (Passpunkte - NICHT ausgeglichen)")
lines.append("-" * 90)
lines.append(f"{'Punkt':<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} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}")
lines.append("")
# Neupunkte (ausgeglichen)
lines.append("-" * 90)
lines.append("NEUPUNKTE (inkl. Standpunkte & Anschlusspunkte - AUSGEGLICHEN)")
lines.append("-" * 90)
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10} {'σPos [mm]':>10}")
lines.append("-" * 90)
for name in sorted(self.new_points):
if name in self.adjustment.points:
p = self.adjustment.points[name]
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} "
f"{p.std_x*1000:>10.2f} {p.std_y*1000:>10.2f} {p.std_position*1000:>10.2f}")
lines.append("")
# Messpunkte (ausgeglichen)
lines.append("-" * 90)
lines.append("MESSPUNKTE (AUSGEGLICHEN)")
lines.append("-" * 90)
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10} {'σPos [mm]':>10}")
lines.append("-" * 90)
for name in sorted(self.measurement_points):
if name in self.adjustment.points:
p = self.adjustment.points[name]
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} "
f"{p.std_x*1000:>10.2f} {p.std_y*1000:>10.2f} {p.std_position*1000:>10.2f}")
lines.append("")
lines.append("=" * 90)
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()