trimble_geodesy/main.py

2124 lines
86 KiB
Python

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