trimble_geodesy/main.py

1679 lines
68 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
Überarbeitet basierend auf Benutzer-Feedback
"""
import sys
import os
from pathlib import Path
# Module-Pfad hinzufügen
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QTabWidget, QVBoxLayout, QHBoxLayout,
QGridLayout, QLabel, QPushButton, QLineEdit, QTextEdit, QFileDialog,
QTableWidget, QTableWidgetItem, QMessageBox, QGroupBox, QComboBox,
QSpinBox, QDoubleSpinBox, QCheckBox, QSplitter, QFrame, QScrollArea,
QHeaderView, QListWidget, QListWidgetItem, QDialog, QDialogButtonBox,
QFormLayout, QProgressBar, QStatusBar, QMenuBar, QMenu, QAction,
QToolBar, QStyle, QTreeWidget, QTreeWidgetItem, QAbstractItemView
)
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
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 = {} # {measurement_record_id: QDoubleSpinBox}
self.control_point_checkboxes = {} # {(station_id, point_name): QCheckBox}
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(150)
summary_layout.addWidget(self.summary_text)
splitter.addWidget(summary_group)
# Stationierungen TreeView
stations_group = QGroupBox("Stationierungen und Messungen")
stations_layout = QVBoxLayout(stations_group)
self.stations_tree = QTreeWidget()
self.stations_tree.setHeaderLabels([
"Station/Messung", "Typ", "Prismenkonstante [mm]", "Qualität", "Aktiv"
])
self.stations_tree.setColumnCount(5)
self.stations_tree.setSelectionMode(QAbstractItemView.SingleSelection)
self.stations_tree.header().setSectionResizeMode(0, QHeaderView.Stretch)
self.stations_tree.header().setSectionResizeMode(1, QHeaderView.ResizeToContents)
self.stations_tree.header().setSectionResizeMode(2, QHeaderView.ResizeToContents)
self.stations_tree.header().setSectionResizeMode(3, QHeaderView.ResizeToContents)
self.stations_tree.header().setSectionResizeMode(4, QHeaderView.ResizeToContents)
stations_layout.addWidget(self.stations_tree)
# Info-Label
info_label = QLabel("💡 Tipp: Prismenkonstanten können direkt in der Spalte 'Prismenkonstante' geändert werden.\n"
"Bei freien Stationierungen: Passpunkte mit Checkbox aktivieren/deaktivieren.")
info_label.setStyleSheet("color: #666; font-style: italic;")
stations_layout.addWidget(info_label)
splitter.addWidget(stations_group)
# Punkte-Tabelle (kompaktere Ansicht)
points_group = QGroupBox("Alle Punkte")
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)
layout.addWidget(splitter)
def browse_file(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "JXL-Datei öffnen", "", "JXL Files (*.jxl);;All Files (*)")
if file_path:
self.file_path_edit.setText(file_path)
def load_file(self):
file_path = self.file_path_edit.text()
if not file_path or not os.path.exists(file_path):
QMessageBox.warning(self, "Fehler", "Bitte eine gültige JXL-Datei auswählen!")
return
parser = JXLParser()
if parser.parse(file_path):
self.main_window.parser = parser
self.update_display()
self.main_window.statusBar().showMessage(f"JXL-Datei geladen: {file_path}")
else:
QMessageBox.critical(self, "Fehler", "Datei konnte nicht geladen werden!")
def update_display(self):
if not self.main_window.parser:
return
parser = self.main_window.parser
# Zusammenfassung
self.summary_text.setText(parser.get_summary())
# Stationierungen TreeView
self.update_stations_tree()
# Punkte-Tabelle aktualisieren
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
# Stationen durchgehen
for station_id, station in parser.stations.items():
# Station als Hauptknoten
station_item = QTreeWidgetItem()
station_item.setText(0, f"📍 {station.name}")
station_item.setText(1, station.station_type)
station_item.setData(0, Qt.UserRole, station_id)
# Hintergrundfarbe für verschiedene Stationstypen
if station.station_type == "ReflineStationSetup":
station_item.setBackground(0, QBrush(QColor(200, 230, 200))) # Hellgrün
elif station.station_type == "StandardResection":
station_item.setBackground(0, QBrush(QColor(200, 200, 230))) # Hellblau
self.stations_tree.addTopLevelItem(station_item)
# Messungen von dieser Station
measurements = parser.get_measurements_from_station(station_id)
# Bei freier Stationierung: Qualität der Passpunkte berechnen
control_point_residuals = {}
if station.station_type == "StandardResection":
control_point_residuals = self.calculate_control_point_quality(
station_id, station, measurements)
for meas in measurements:
meas_item = QTreeWidgetItem()
meas_item.setText(0, f"{meas.name}")
# Typ ermitteln
if meas.classification == "BackSight":
meas_type = "Passpunkt"
else:
meas_type = "Messung"
meas_item.setText(1, meas_type)
# Prismenkonstante als editierbares SpinBox
if meas.target_id and meas.target_id in parser.targets:
target = parser.targets[meas.target_id]
prism_spin = QDoubleSpinBox()
prism_spin.setRange(-100, 100)
prism_spin.setDecimals(1)
prism_spin.setValue(target.prism_constant * 1000) # m -> mm
prism_spin.setSuffix(" mm")
prism_spin.setProperty("target_id", meas.target_id)
prism_spin.valueChanged.connect(
lambda val, tid=meas.target_id: self.on_prism_changed(tid, val))
self.prism_spin_widgets[meas.record_id] = prism_spin
self.stations_tree.setItemWidget(meas_item, 2, prism_spin)
# Bei freier Stationierung: Passpunkt-Qualität und Checkbox
if station.station_type == "StandardResection" and meas.classification == "BackSight":
# Qualitätswert anzeigen
if meas.name in control_point_residuals:
quality_info = control_point_residuals[meas.name]
quality_value = quality_info['residual']
rank = quality_info['rank']
total = quality_info['total']
# Farbcodierung basierend auf Rang
quality_label = QLabel(f"{quality_value:.1f} mm")
if rank == 1: # Bester
quality_label.setStyleSheet("background-color: #90EE90; padding: 2px;") # Grün
elif rank == total: # Schlechtester
quality_label.setStyleSheet("background-color: #FFB6C1; padding: 2px;") # Rot
else: # Mittlere
quality_label.setStyleSheet("background-color: #FFFF99; padding: 2px;") # Gelb
self.stations_tree.setItemWidget(meas_item, 3, quality_label)
# Checkbox für Aktivierung/Deaktivierung
checkbox = QCheckBox()
checkbox.setChecked(not meas.deleted)
checkbox.setProperty("station_id", station_id)
checkbox.setProperty("point_name", meas.name)
checkbox.stateChanged.connect(
lambda state, sid=station_id, pn=meas.name:
self.on_control_point_toggled(sid, pn, state))
self.control_point_checkboxes[(station_id, meas.name)] = checkbox
self.stations_tree.setItemWidget(meas_item, 4, checkbox)
station_item.addChild(meas_item)
station_item.setExpanded(True)
def calculate_control_point_quality(self, station_id, station, measurements):
"""
Berechnet die Qualität der Passpunkte einer freien Stationierung.
Nutzt die Residuen aus der Stationierungsberechnung.
"""
parser = self.main_window.parser
control_points = []
for meas in measurements:
if meas.classification == "BackSight" and not meas.deleted:
# Residuum berechnen: Differenz zwischen gemessenen und berechneten Koordinaten
# Vereinfachte Berechnung basierend auf Streckendifferenz
if meas.edm_distance is not None and meas.name in parser.points:
target_point = parser.points[meas.name]
if target_point.east is not None and target_point.north is not None:
if station.east is not None and station.north is not None:
# Berechnete Strecke
dx = target_point.east - station.east
dy = target_point.north - station.north
calc_dist = (dx**2 + dy**2)**0.5
# Residuum (Differenz zur gemessenen Strecke)
residual = abs(meas.edm_distance - calc_dist) * 1000 # in mm
control_points.append((meas.name, residual))
# Sortieren und Ränge vergeben
if not control_points:
return {}
control_points.sort(key=lambda x: x[1])
result = {}
for rank, (name, residual) in enumerate(control_points, 1):
result[name] = {
'residual': residual,
'rank': rank,
'total': len(control_points)
}
return result
def on_prism_changed(self, target_id, new_value_mm):
"""Wird aufgerufen, wenn eine Prismenkonstante geändert wird"""
if self.main_window.parser and target_id in self.main_window.parser.targets:
new_value_m = new_value_mm / 1000.0
self.main_window.parser.modify_prism_constant(target_id, new_value_m)
self.main_window.statusBar().showMessage(
f"Prismenkonstante für {target_id} auf {new_value_mm:.1f} mm gesetzt")
def on_control_point_toggled(self, station_id, point_name, state):
"""Wird aufgerufen, wenn ein Passpunkt aktiviert/deaktiviert wird"""
is_active = state == Qt.Checked
# Hier könnte man die Stationierung neu berechnen
self.main_window.statusBar().showMessage(
f"Passpunkt {point_name} {'aktiviert' if is_active else 'deaktiviert'}")
def update_points_table(self):
"""Aktualisiert die Punkte-Tabelle"""
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()
class CORGeneratorTab(QWidget):
"""Tab für COR-Datei Generierung"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.cor_generator = None
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Optionen
options_group = QGroupBox("Generierungsoptionen")
options_layout = QGridLayout(options_group)
options_layout.addWidget(QLabel("Methode:"), 0, 0)
self.method_combo = QComboBox()
self.method_combo.addItems([
"Aus berechneten Koordinaten (ComputedGrid)",
"Aus Rohbeobachtungen berechnen"
])
options_layout.addWidget(self.method_combo, 0, 1)
self.include_header_check = QCheckBox("Stations-Header einfügen")
self.include_header_check.setChecked(True)
options_layout.addWidget(self.include_header_check, 1, 0, 1, 2)
layout.addWidget(options_group)
# Generieren Button
generate_btn = QPushButton("COR-Datei generieren")
generate_btn.clicked.connect(self.generate_cor)
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
export_group = QGroupBox("Export")
export_layout = QHBoxLayout(export_group)
export_cor_btn = QPushButton("Als COR speichern")
export_cor_btn.clicked.connect(lambda: self.export_file('cor'))
export_layout.addWidget(export_cor_btn)
export_csv_btn = QPushButton("Als CSV speichern")
export_csv_btn.clicked.connect(lambda: self.export_file('csv'))
export_layout.addWidget(export_csv_btn)
export_txt_btn = QPushButton("Als TXT speichern")
export_txt_btn.clicked.connect(lambda: self.export_file('txt'))
export_layout.addWidget(export_txt_btn)
export_dxf_btn = QPushButton("Als DXF speichern")
export_dxf_btn.clicked.connect(lambda: self.export_file('dxf'))
export_layout.addWidget(export_dxf_btn)
layout.addWidget(export_group)
def generate_cor(self):
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!")
return
self.cor_generator = CORGenerator(self.main_window.parser)
if self.method_combo.currentIndex() == 0:
points = self.cor_generator.generate_from_computed_grid()
else:
points = self.cor_generator.compute_from_observations()
# Tabelle aktualisieren
self.preview_table.setRowCount(len(points))
for row, p in enumerate(points):
self.preview_table.setItem(row, 0, QTableWidgetItem(p.name))
self.preview_table.setItem(row, 1, QTableWidgetItem(f"{p.x:.4f}"))
self.preview_table.setItem(row, 2, QTableWidgetItem(f"{p.y:.4f}"))
self.preview_table.setItem(row, 3, QTableWidgetItem(f"{p.z:.4f}"))
# Statistiken
self.stats_text.setText(self.cor_generator.get_statistics())
self.main_window.statusBar().showMessage(f"{len(points)} Punkte generiert")
def export_file(self, format_type):
if not self.cor_generator or not self.cor_generator.cor_points:
QMessageBox.warning(self, "Fehler", "Bitte zuerst Punkte generieren!")
return
filters = {
'cor': "COR Files (*.cor)",
'csv': "CSV Files (*.csv)",
'txt': "Text Files (*.txt)",
'dxf': "DXF Files (*.dxf)"
}
file_path, _ = QFileDialog.getSaveFileName(
self, "Speichern unter", "", filters.get(format_type, "All Files (*)"))
if file_path:
if format_type == 'cor':
self.cor_generator.write_cor_file(file_path, self.include_header_check.isChecked())
elif format_type == 'csv':
self.cor_generator.export_csv(file_path)
elif format_type == 'txt':
self.cor_generator.export_txt(file_path)
elif format_type == 'dxf':
self.cor_generator.export_dxf(file_path)
QMessageBox.information(self, "Erfolg", f"Datei gespeichert: {file_path}")
class TransformationTab(QWidget):
"""Tab für Koordinatentransformation - Überarbeitet"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.transformer = CoordinateTransformer()
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Methode auswählen
method_group = QGroupBox("Transformationsmethode")
method_layout = QVBoxLayout(method_group)
self.manual_radio = QCheckBox("Manuelle Parameter")
self.manual_radio.setChecked(True)
self.manual_radio.stateChanged.connect(self.toggle_method)
method_layout.addWidget(self.manual_radio)
self.twopoint_radio = QCheckBox("Über 2 Punkte definieren")
self.twopoint_radio.stateChanged.connect(self.toggle_method)
method_layout.addWidget(self.twopoint_radio)
layout.addWidget(method_group)
# Manuelle Parameter
self.manual_group = QGroupBox("Manuelle Parameter")
manual_layout = QGridLayout(self.manual_group)
manual_layout.addWidget(QLabel("Verschiebung X (East):"), 0, 0)
self.dx_spin = QDoubleSpinBox()
self.dx_spin.setRange(-1000000, 1000000)
self.dx_spin.setDecimals(4)
manual_layout.addWidget(self.dx_spin, 0, 1)
manual_layout.addWidget(QLabel("m"), 0, 2)
manual_layout.addWidget(QLabel("Verschiebung Y (North):"), 1, 0)
self.dy_spin = QDoubleSpinBox()
self.dy_spin.setRange(-1000000, 1000000)
self.dy_spin.setDecimals(4)
manual_layout.addWidget(self.dy_spin, 1, 1)
manual_layout.addWidget(QLabel("m"), 1, 2)
manual_layout.addWidget(QLabel("Verschiebung Z (Höhe):"), 2, 0)
self.dz_spin = QDoubleSpinBox()
self.dz_spin.setRange(-1000000, 1000000)
self.dz_spin.setDecimals(4)
manual_layout.addWidget(self.dz_spin, 2, 1)
manual_layout.addWidget(QLabel("m"), 2, 2)
manual_layout.addWidget(QLabel("Rotation:"), 3, 0)
self.rotation_spin = QDoubleSpinBox()
self.rotation_spin.setRange(-400, 400)
self.rotation_spin.setDecimals(6)
manual_layout.addWidget(self.rotation_spin, 3, 1)
manual_layout.addWidget(QLabel("gon"), 3, 2)
manual_layout.addWidget(QLabel("Drehpunkt X:"), 4, 0)
self.pivot_x_spin = QDoubleSpinBox()
self.pivot_x_spin.setRange(-1000000, 1000000)
self.pivot_x_spin.setDecimals(4)
manual_layout.addWidget(self.pivot_x_spin, 4, 1)
manual_layout.addWidget(QLabel("Drehpunkt Y:"), 5, 0)
self.pivot_y_spin = QDoubleSpinBox()
self.pivot_y_spin.setRange(-1000000, 1000000)
self.pivot_y_spin.setDecimals(4)
manual_layout.addWidget(self.pivot_y_spin, 5, 1)
layout.addWidget(self.manual_group)
# 2-Punkte-Definition (überarbeitet)
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("Y-Richtung:"), 1, 0)
self.direction_combo = QComboBox()
twopoint_layout.addWidget(self.direction_combo, 1, 1)
twopoint_layout.addWidget(QLabel("Z-Nullpunkt (0):"), 2, 0)
self.z_origin_combo = QComboBox()
twopoint_layout.addWidget(self.z_origin_combo, 2, 1)
refresh_btn = QPushButton("Punktliste aktualisieren")
refresh_btn.clicked.connect(self.refresh_point_lists)
twopoint_layout.addWidget(refresh_btn, 3, 0, 1, 2)
self.twopoint_group.setVisible(False)
layout.addWidget(self.twopoint_group)
# Transformation durchführen
transform_btn = QPushButton("Transformation berechnen")
transform_btn.clicked.connect(self.execute_transformation)
transform_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
layout.addWidget(transform_btn)
# Anwenden Button (NEU - Bug Fix)
apply_btn = QPushButton("Transformation anwenden (Punktliste aktualisieren)")
apply_btn.clicked.connect(self.apply_transformation)
apply_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;")
layout.addWidget(apply_btn)
# Ergebnisse
results_group = QGroupBox("Ergebnisse")
results_layout = QVBoxLayout(results_group)
self.results_text = QTextEdit()
self.results_text.setReadOnly(True)
results_layout.addWidget(self.results_text)
# Export
export_layout = QHBoxLayout()
export_report_btn = QPushButton("Bericht exportieren")
export_report_btn.clicked.connect(self.export_report)
export_layout.addWidget(export_report_btn)
export_points_btn = QPushButton("Punkte exportieren")
export_points_btn.clicked.connect(self.export_points)
export_layout.addWidget(export_points_btn)
results_layout.addLayout(export_layout)
layout.addWidget(results_group)
def toggle_method(self):
self.manual_group.setVisible(self.manual_radio.isChecked())
self.twopoint_group.setVisible(self.twopoint_radio.isChecked())
if self.twopoint_radio.isChecked():
self.refresh_point_lists()
def refresh_point_lists(self):
if not self.main_window.parser:
return
points = list(self.main_window.parser.get_active_points().keys())
self.xy_origin_combo.clear()
self.direction_combo.clear()
self.z_origin_combo.clear()
# Standard-Vorschläge: 7001 für XY, 7002 für Z
default_xy = None
default_direction = None
default_z = None
for name in sorted(points):
self.xy_origin_combo.addItem(name)
self.direction_combo.addItem(name)
self.z_origin_combo.addItem(name)
if name == "7001":
default_xy = name
if name == "7002":
default_z = name
# Standard-Werte setzen falls vorhanden
if default_xy:
idx = self.xy_origin_combo.findText(default_xy)
if idx >= 0:
self.xy_origin_combo.setCurrentIndex(idx)
if default_z:
idx = self.z_origin_combo.findText(default_z)
if idx >= 0:
self.z_origin_combo.setCurrentIndex(idx)
# Info anzeigen
if default_xy or default_z:
self.main_window.statusBar().showMessage(
f"Standard-Vorschlag: XY={default_xy or 'nicht gefunden'}, Z={default_z or 'nicht gefunden'}")
def execute_transformation(self):
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!")
return
# Punkte aus Parser holen
points = []
for name, p in self.main_window.parser.get_active_points().items():
if p.east is not None and p.north is not None:
points.append(CORPoint(
name=name,
x=p.east,
y=p.north,
z=p.elevation or 0.0
))
self.transformer.set_points(points)
if self.manual_radio.isChecked():
self.transformer.set_manual_parameters(
dx=self.dx_spin.value(),
dy=self.dy_spin.value(),
dz=self.dz_spin.value(),
rotation_gon=self.rotation_spin.value(),
pivot_x=self.pivot_x_spin.value(),
pivot_y=self.pivot_y_spin.value()
)
elif self.twopoint_radio.isChecked():
origin = self.xy_origin_combo.currentText()
direction = self.direction_combo.currentText()
zref = self.z_origin_combo.currentText()
if not self.transformer.compute_from_two_points(origin, direction, zref):
QMessageBox.warning(self, "Fehler", "Punkte nicht gefunden!")
return
self.transformer.transform()
# Ergebnisse anzeigen
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 (noch nicht angewendet)")
def apply_transformation(self):
"""Wendet die Transformation auf die Punktliste an (Bug Fix)"""
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",
"Sollen die transformierten Koordinaten auf alle Punkte angewendet werden?\n\n"
"Dies ändert die Koordinaten im Speicher.",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.No:
return
# Transformierte Koordinaten in Parser übernehmen
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
# GUI aktualisieren
# JXL-Tab aktualisieren
jxl_tab = self.main_window.tabs.widget(0)
if hasattr(jxl_tab, 'update_display'):
jxl_tab.update_display()
# Erfolgsmeldung
QMessageBox.information(self, "Erfolg",
f"{len(self.transformer.transformed_points)} Punkte wurden transformiert!\n\n"
"Die Punktliste wurde aktualisiert.")
self.main_window.statusBar().showMessage("Transformation angewendet - Punktliste aktualisiert")
def export_report(self):
file_path, _ = QFileDialog.getSaveFileName(
self, "Bericht speichern", "", "Text Files (*.txt)")
if file_path:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.results_text.toPlainText())
QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}")
def export_points(self):
if not self.transformer.transformed_points:
QMessageBox.warning(self, "Fehler", "Keine transformierten Punkte vorhanden!")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "Punkte speichern", "", "CSV Files (*.csv)")
if file_path:
lines = ["Punkt;X;Y;Z"]
for p in self.transformer.transformed_points:
lines.append(f"{p.name};{p.x:.4f};{p.y:.4f};{p.z:.4f}")
with open(file_path, 'w', encoding='utf-8') as f:
f.write("\n".join(lines))
QMessageBox.information(self, "Erfolg", f"Punkte gespeichert: {file_path}")
class GeoreferencingTab(QWidget):
"""Tab für Georeferenzierung"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.georeferencer = Georeferencer()
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Passpunkte
cp_group = QGroupBox("Passpunkte (mind. 3)")
cp_layout = QVBoxLayout(cp_group)
self.cp_table = QTableWidget()
self.cp_table.setColumnCount(7)
self.cp_table.setHorizontalHeaderLabels([
"Punkt", "X_lokal", "Y_lokal", "Z_lokal", "X_Ziel", "Y_Ziel", "Z_Ziel"])
self.cp_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
cp_layout.addWidget(self.cp_table)
# Passpunkt-Buttons
cp_buttons = QHBoxLayout()
add_cp_btn = QPushButton("Passpunkt hinzufügen")
add_cp_btn.clicked.connect(self.add_control_point)
cp_buttons.addWidget(add_cp_btn)
remove_cp_btn = QPushButton("Entfernen")
remove_cp_btn.clicked.connect(self.remove_control_point)
cp_buttons.addWidget(remove_cp_btn)
load_local_btn = QPushButton("Lokale Koordinaten aus JXL")
load_local_btn.clicked.connect(self.load_local_from_jxl)
cp_buttons.addWidget(load_local_btn)
load_target_btn = QPushButton("Zielkoordinaten importieren")
load_target_btn.clicked.connect(self.load_target_coords)
cp_buttons.addWidget(load_target_btn)
cp_buttons.addStretch()
cp_layout.addLayout(cp_buttons)
layout.addWidget(cp_group)
# Transformation berechnen
calc_btn = QPushButton("Transformation berechnen")
calc_btn.clicked.connect(self.calculate_transformation)
calc_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
layout.addWidget(calc_btn)
# Ergebnisse
results_group = QGroupBox("Ergebnisse")
results_layout = QVBoxLayout(results_group)
self.results_text = QTextEdit()
self.results_text.setReadOnly(True)
self.results_text.setFont(QFont("Courier"))
results_layout.addWidget(self.results_text)
# Export
export_layout = QHBoxLayout()
export_report_btn = QPushButton("Bericht exportieren")
export_report_btn.clicked.connect(self.export_report)
export_layout.addWidget(export_report_btn)
transform_all_btn = QPushButton("Alle Punkte transformieren")
transform_all_btn.clicked.connect(self.transform_all_points)
export_layout.addWidget(transform_all_btn)
results_layout.addLayout(export_layout)
layout.addWidget(results_group)
def add_control_point(self):
row = self.cp_table.rowCount()
self.cp_table.insertRow(row)
# Punkt-Auswahl
combo = QComboBox()
if self.main_window.parser:
for name in sorted(self.main_window.parser.get_active_points().keys()):
combo.addItem(name)
self.cp_table.setCellWidget(row, 0, combo)
# Editierbare Felder für Koordinaten
for col in range(1, 7):
spin = QDoubleSpinBox()
spin.setRange(-10000000, 10000000)
spin.setDecimals(4)
self.cp_table.setCellWidget(row, col, spin)
def remove_control_point(self):
row = self.cp_table.currentRow()
if row >= 0:
self.cp_table.removeRow(row)
def load_local_from_jxl(self):
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!")
return
for row in range(self.cp_table.rowCount()):
combo = self.cp_table.cellWidget(row, 0)
if combo:
name = combo.currentText()
if name in self.main_window.parser.points:
p = self.main_window.parser.points[name]
self.cp_table.cellWidget(row, 1).setValue(p.east or 0)
self.cp_table.cellWidget(row, 2).setValue(p.north or 0)
self.cp_table.cellWidget(row, 3).setValue(p.elevation or 0)
def load_target_coords(self):
file_path, _ = QFileDialog.getOpenFileName(
self, "Zielkoordinaten laden", "",
"CSV Files (*.csv);;Text Files (*.txt);;All Files (*)")
if not file_path:
return
try:
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
# Format: Punkt;X;Y;Z oder Punkt,X,Y,Z
coord_dict = {}
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()
x = float(parts[1])
y = float(parts[2])
z = float(parts[3])
coord_dict[name] = (x, y, z)
# In Tabelle eintragen
for row in range(self.cp_table.rowCount()):
combo = self.cp_table.cellWidget(row, 0)
if combo:
name = combo.currentText()
if name in coord_dict:
x, y, z = coord_dict[name]
self.cp_table.cellWidget(row, 4).setValue(x)
self.cp_table.cellWidget(row, 5).setValue(y)
self.cp_table.cellWidget(row, 6).setValue(z)
QMessageBox.information(self, "Erfolg", f"{len(coord_dict)} Koordinaten geladen!")
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Fehler beim Laden: {e}")
def calculate_transformation(self):
self.georeferencer.clear_control_points()
for row in range(self.cp_table.rowCount()):
combo = self.cp_table.cellWidget(row, 0)
if not combo:
continue
name = combo.currentText()
local_x = self.cp_table.cellWidget(row, 1).value()
local_y = self.cp_table.cellWidget(row, 2).value()
local_z = self.cp_table.cellWidget(row, 3).value()
target_x = self.cp_table.cellWidget(row, 4).value()
target_y = self.cp_table.cellWidget(row, 5).value()
target_z = self.cp_table.cellWidget(row, 6).value()
self.georeferencer.add_control_point(
name, local_x, local_y, local_z, target_x, target_y, target_z)
if len(self.georeferencer.control_points) < 3:
QMessageBox.warning(self, "Fehler", "Mindestens 3 Passpunkte erforderlich!")
return
try:
self.georeferencer.compute_transformation()
report = self.georeferencer.get_transformation_report()
self.results_text.setText(report)
self.main_window.statusBar().showMessage("Georeferenzierung berechnet")
except Exception as e:
QMessageBox.critical(self, "Fehler", f"Berechnung fehlgeschlagen: {e}")
def export_report(self):
file_path, _ = QFileDialog.getSaveFileName(
self, "Bericht speichern", "", "Text Files (*.txt)")
if file_path:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.results_text.toPlainText())
QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}")
def transform_all_points(self):
if self.georeferencer.result is None:
QMessageBox.warning(self, "Fehler", "Bitte zuerst Transformation berechnen!")
return
if not self.main_window.parser:
return
# Punkte transformieren
points = []
for name, p in self.main_window.parser.get_active_points().items():
if p.east is not None and p.north is not None:
points.append(CORPoint(
name=name, x=p.east, y=p.north, z=p.elevation or 0))
self.georeferencer.set_points_to_transform(points)
transformed = self.georeferencer.transform_points()
# Export-Dialog
file_path, _ = QFileDialog.getSaveFileName(
self, "Transformierte Punkte speichern", "", "CSV Files (*.csv)")
if file_path:
lines = ["Punkt;X;Y;Z"]
for p in transformed:
lines.append(f"{p.name};{p.x:.4f};{p.y:.4f};{p.z:.4f}")
with open(file_path, 'w', encoding='utf-8') as f:
f.write("\n".join(lines))
QMessageBox.information(self, "Erfolg",
f"{len(transformed)} Punkte transformiert und gespeichert!")
class NetworkAdjustmentTab(QWidget):
"""Tab für Netzausgleichung - Überarbeitet mit automatischer Punkterkennung"""
def __init__(self, parent=None):
super().__init__(parent)
self.main_window = parent
self.adjustment = None
self.fixed_points = set()
self.measurement_points = set()
self.setup_ui()
def setup_ui(self):
layout = QVBoxLayout(self)
# Konfiguration
config_group = QGroupBox("Konfiguration")
config_layout = QGridLayout(config_group)
config_layout.addWidget(QLabel("Max. Iterationen:"), 0, 0)
self.max_iter_spin = QSpinBox()
self.max_iter_spin.setRange(1, 100)
self.max_iter_spin.setValue(20)
config_layout.addWidget(self.max_iter_spin, 0, 1)
config_layout.addWidget(QLabel("Konvergenzlimit [mm]:"), 1, 0)
self.convergence_spin = QDoubleSpinBox()
self.convergence_spin.setRange(0.001, 10)
self.convergence_spin.setDecimals(3)
self.convergence_spin.setValue(0.01)
config_layout.addWidget(self.convergence_spin, 1, 1)
config_layout.addWidget(QLabel("Sigma-0 a-priori:"), 2, 0)
self.sigma0_spin = QDoubleSpinBox()
self.sigma0_spin.setRange(0.1, 10)
self.sigma0_spin.setDecimals(2)
self.sigma0_spin.setValue(1.0)
config_layout.addWidget(self.sigma0_spin, 2, 1)
layout.addWidget(config_group)
# Automatisch erkannte Punkte
points_group = QGroupBox("Automatisch erkannte Punkttypen")
points_layout = QVBoxLayout(points_group)
# Info-Label
info_label = QLabel(
"💡 Das Programm erkennt automatisch:\n"
" • Festpunkte: Alle Punkte, die in Stationierungen verwendet werden (1000er, 2000er Serie)\n"
" • Messpunkte: 3000er Punkte (Detailmessungen)")
info_label.setStyleSheet("color: #666; background-color: #f0f0f0; padding: 10px;")
points_layout.addWidget(info_label)
# Tabelle für erkannte Punkte
self.points_table = QTableWidget()
self.points_table.setColumnCount(4)
self.points_table.setHorizontalHeaderLabels(["Punkt", "Typ", "X", "Y"])
self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
self.points_table.setMaximumHeight(200)
points_layout.addWidget(self.points_table)
# Button zum Aktualisieren
refresh_btn = QPushButton("Punkte automatisch erkennen")
refresh_btn.clicked.connect(self.auto_detect_points)
refresh_btn.setStyleSheet("background-color: #FF9800; color: white;")
points_layout.addWidget(refresh_btn)
layout.addWidget(points_group)
# Ausgleichung durchführen
adjust_btn = QPushButton("Netzausgleichung durchführen")
adjust_btn.clicked.connect(self.run_adjustment)
adjust_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
layout.addWidget(adjust_btn)
# Ergebnisse
results_group = QGroupBox("Ergebnisse")
results_layout = QVBoxLayout(results_group)
self.results_text = QTextEdit()
self.results_text.setReadOnly(True)
self.results_text.setFont(QFont("Courier", 9))
results_layout.addWidget(self.results_text)
# Export
export_layout = QHBoxLayout()
export_report_btn = QPushButton("Bericht exportieren")
export_report_btn.clicked.connect(self.export_report)
export_layout.addWidget(export_report_btn)
export_points_btn = QPushButton("Koordinaten exportieren")
export_points_btn.clicked.connect(self.export_points)
export_layout.addWidget(export_points_btn)
results_layout.addLayout(export_layout)
layout.addWidget(results_group)
def auto_detect_points(self):
"""Erkennt automatisch Festpunkte und Messpunkte"""
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!")
return
parser = self.main_window.parser
self.fixed_points.clear()
self.measurement_points.clear()
# Festpunkte: Punkte die in Stationierungen verwendet werden
# Das sind typischerweise 1000er (Stationen) und 2000er (Anschlusspunkte)
for station_id, station in parser.stations.items():
# Station selbst ist Festpunkt
if station.name:
self.fixed_points.add(station.name)
# Anschlusspunkte aus Backbearings
for bb_id, bb in parser.backbearings.items():
if bb.station_record_id == station_id and bb.backsight:
self.fixed_points.add(bb.backsight)
# Anschlusspunkte aus Messungen (BackSight Classification)
measurements = parser.get_measurements_from_station(station_id)
for meas in measurements:
if meas.classification == "BackSight" and meas.name:
self.fixed_points.add(meas.name)
# Messpunkte: 3000er Serie
for name in parser.get_active_points().keys():
if name.startswith("3"):
self.measurement_points.add(name)
# Tabelle aktualisieren
self.update_points_table()
self.main_window.statusBar().showMessage(
f"Erkannt: {len(self.fixed_points)} Festpunkte, {len(self.measurement_points)} Messpunkte")
def update_points_table(self):
"""Aktualisiert die Tabelle mit erkannten Punkten"""
parser = self.main_window.parser
if not parser:
return
all_points = list(self.fixed_points) + list(self.measurement_points)
self.points_table.setRowCount(len(all_points))
for row, name in enumerate(sorted(all_points)):
# Punkt-Name
self.points_table.setItem(row, 0, QTableWidgetItem(name))
# Typ
if name in self.fixed_points:
type_item = QTableWidgetItem("Festpunkt")
type_item.setBackground(QBrush(QColor(200, 230, 200))) # Hellgrün
else:
type_item = QTableWidgetItem("Messpunkt")
type_item.setBackground(QBrush(QColor(200, 200, 230))) # Hellblau
self.points_table.setItem(row, 1, type_item)
# Koordinaten
if name in parser.points:
p = parser.points[name]
self.points_table.setItem(row, 2, QTableWidgetItem(f"{p.east:.4f}" if p.east else ""))
self.points_table.setItem(row, 3, QTableWidgetItem(f"{p.north:.4f}" if p.north else ""))
def run_adjustment(self):
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!")
return
# Automatische Erkennung falls noch nicht geschehen
if not self.fixed_points and not self.measurement_points:
self.auto_detect_points()
if not self.fixed_points:
QMessageBox.warning(self, "Fehler", "Keine Festpunkte erkannt!")
return
# Adjustment erstellen
self.adjustment = NetworkAdjustment(self.main_window.parser)
self.adjustment.max_iterations = self.max_iter_spin.value()
self.adjustment.convergence_limit = self.convergence_spin.value() / 1000.0
self.adjustment.sigma_0_priori = self.sigma0_spin.value()
# Beobachtungen extrahieren
self.adjustment.extract_observations()
self.adjustment.initialize_points()
# Festpunkte setzen (automatisch erkannte)
for point_name in self.fixed_points:
self.adjustment.set_fixed_point(point_name)
try:
result = self.adjustment.adjust()
# Bericht erstellen mit Festpunkt/Messpunkt-Unterscheidung
report = self.create_detailed_report()
self.results_text.setText(report)
status = "konvergiert" if result.converged else "nicht konvergiert"
self.main_window.statusBar().showMessage(
f"Ausgleichung abgeschlossen ({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 mit Festpunkt/Messpunkt-Unterscheidung"""
if not self.adjustment or not self.adjustment.result:
return "Keine Ergebnisse vorhanden."
lines = []
lines.append("=" * 80)
lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT")
lines.append("=" * 80)
lines.append("")
# Allgemeine Informationen
lines.append("ALLGEMEINE INFORMATIONEN")
lines.append("-" * 80)
lines.append(f"Job: {self.main_window.parser.job_name}")
lines.append(f"Anzahl Festpunkte: {len(self.fixed_points)}")
lines.append(f"Anzahl Messpunkte: {len(self.measurement_points)}")
lines.append(f"Anzahl Beobachtungen: {self.adjustment.result.num_observations}")
lines.append(f"Iterationen: {self.adjustment.result.iterations}")
lines.append(f"Konvergiert: {'Ja' if self.adjustment.result.converged else 'Nein'}")
lines.append("")
# Qualitätsparameter
lines.append("GLOBALE QUALITÄTSPARAMETER")
lines.append("-" * 80)
lines.append(f"Sigma-0 a-posteriori: {self.adjustment.result.sigma_0_posteriori:.4f}")
lines.append(f"RMSE Richtungen: {self.adjustment.result.rmse_directions:.2f} mgon")
lines.append(f"RMSE Strecken: {self.adjustment.result.rmse_distances:.2f} mm")
lines.append("")
# Festpunkte
lines.append("FESTPUNKTE (Stationspunkte und Anschlusspunkte)")
lines.append("-" * 80)
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10}")
lines.append("-" * 80)
for name in sorted(self.fixed_points):
if name in self.adjustment.points:
p = self.adjustment.points[name]
std_x = p.std_x * 1000 if p.std_x else 0
std_y = p.std_y * 1000 if p.std_y else 0
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} {std_x:>10.2f} {std_y:>10.2f}")
lines.append("")
# Messpunkte
lines.append("MESSPUNKTE (3000er Serie)")
lines.append("-" * 80)
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10}")
lines.append("-" * 80)
for name in sorted(self.measurement_points):
if name in self.adjustment.points:
p = self.adjustment.points[name]
std_x = p.std_x * 1000 if p.std_x else 0
std_y = p.std_y * 1000 if p.std_y else 0
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} {std_x:>10.2f} {std_y:>10.2f}")
lines.append("")
lines.append("=" * 80)
return "\n".join(lines)
def export_report(self):
if not self.adjustment or not self.adjustment.result:
QMessageBox.warning(self, "Fehler", "Keine Ergebnisse vorhanden!")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "Bericht speichern", "", "Text Files (*.txt)")
if file_path:
with open(file_path, 'w', encoding='utf-8') as f:
f.write(self.create_detailed_report())
QMessageBox.information(self, "Erfolg", f"Bericht gespeichert: {file_path}")
def export_points(self):
if not self.adjustment or not self.adjustment.result:
QMessageBox.warning(self, "Fehler", "Keine Ergebnisse vorhanden!")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "Koordinaten speichern", "", "CSV Files (*.csv)")
if file_path:
lines = ["Punkt;Typ;X;Y;Z;Sigma_X;Sigma_Y"]
for name, p in sorted(self.adjustment.points.items()):
if name in self.fixed_points:
typ = "Festpunkt"
elif name in self.measurement_points:
typ = "Messpunkt"
else:
typ = "Sonstig"
std_x = p.std_x * 1000 if p.std_x else 0
std_y = p.std_y * 1000 if p.std_y else 0
lines.append(f"{name};{typ};{p.x:.4f};{p.y:.4f};{p.z:.4f};{std_x:.2f};{std_y:.2f}")
with open(file_path, 'w', encoding='utf-8') as f:
f.write("\n".join(lines))
QMessageBox.information(self, "Erfolg", f"Koordinaten gespeichert: {file_path}")
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 Gruppe
new_coords_group = QGroupBox("Neue Koordinaten für Referenzpunkt")
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)
# Translation anzeigen
new_coords_layout.addWidget(QLabel(""), 3, 0)
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, 4, 0, 1, 2)
# Koordinaten-Änderungen live berechnen
self.new_east_spin.valueChanged.connect(self.update_delta)
self.new_north_spin.valueChanged.connect(self.update_delta)
self.new_elev_spin.valueChanged.connect(self.update_delta)
layout.addWidget(new_coords_group)
# Aktionen
actions_group = QGroupBox("Aktionen")
actions_layout = QHBoxLayout(actions_group)
preview_btn = QPushButton("Vorschau berechnen")
preview_btn.clicked.connect(self.preview_transformation)
preview_btn.setStyleSheet("background-color: #4CAF50; color: white;")
actions_layout.addWidget(preview_btn)
apply_btn = QPushButton("Transformation anwenden")
apply_btn.clicked.connect(self.apply_transformation)
apply_btn.setStyleSheet("background-color: #2196F3; color: white;")
actions_layout.addWidget(apply_btn)
export_btn = QPushButton("Neue JXL exportieren")
export_btn.clicked.connect(self.export_jxl)
export_btn.setStyleSheet("background-color: #FF9800; color: white;")
actions_layout.addWidget(export_btn)
layout.addWidget(actions_group)
# Vorschau-Tabelle
preview_group = QGroupBox("Vorschau der betroffenen Punkte")
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("Transformationsbericht")
report_layout = QVBoxLayout(report_group)
self.report_text = QTextEdit()
self.report_text.setReadOnly(True)
self.report_text.setFont(QFont("Courier", 9))
report_layout.addWidget(self.report_text)
layout.addWidget(report_group)
def load_reference_point(self):
"""Lädt die Informationen zum Referenzpunkt"""
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler",
"Bitte zuerst eine JXL-Datei im 'JXL-Analyse' Tab 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",
f"Referenzpunkt nicht gefunden: {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")
# Setze aktuelle Werte als Standard für neue Koordinaten
self.new_east_spin.setValue(info['east'])
self.new_north_spin.setValue(info['north'])
self.new_elev_spin.setValue(info['elevation'])
self.main_window.statusBar().showMessage(
f"Referenzpunkt '{info['name']}' geladen")
def update_delta(self):
"""Aktualisiert die Anzeige der Verschiebung"""
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):
"""Zeigt eine Vorschau der Transformation"""
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler",
"Bitte zuerst eine JXL-Datei laden!")
return
# Validierung
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"Mögliche Probleme erkannt:\n\n{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()
# Tabelle aktualisieren
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}"))
# Bericht aktualisieren
self.report_text.setText(self.adjuster.get_summary_report())
self.main_window.statusBar().showMessage(
f"Vorschau berechnet: {len(results)} Punkte betroffen")
def apply_transformation(self):
"""Wendet die Transformation auf die JXL-Datei an"""
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler",
"Bitte zuerst eine JXL-Datei laden!")
return
if not self.adjuster.affected_points:
QMessageBox.warning(self, "Fehler",
"Bitte zuerst eine Vorschau berechnen!")
return
reply = QMessageBox.question(self, "Bestätigung",
f"Soll die Transformation auf {len(self.adjuster.affected_points)} Punkte angewendet werden?\n\n"
"Die Änderungen werden in der geladenen JXL-Datei gespeichert.",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.No:
return
if self.adjuster.apply_transformation():
# Bericht aktualisieren
self.report_text.setText(self.adjuster.get_summary_report())
# JXL-Tab aktualisieren
jxl_tab = self.main_window.tabs.widget(0)
if hasattr(jxl_tab, 'update_display'):
jxl_tab.update_display()
QMessageBox.information(self, "Erfolg",
"Transformation erfolgreich angewendet!\n\n"
"Die Koordinaten wurden aktualisiert.\n"
"Verwenden Sie 'Neue JXL exportieren' zum Speichern.")
self.main_window.statusBar().showMessage("Transformation angewendet")
else:
QMessageBox.critical(self, "Fehler",
"Transformation konnte nicht angewendet werden!")
def export_jxl(self):
"""Exportiert die modifizierte JXL-Datei"""
if not self.main_window.parser:
QMessageBox.warning(self, "Fehler",
"Bitte zuerst eine JXL-Datei laden!")
return
if not self.adjuster.transformation_applied:
reply = QMessageBox.warning(self, "Warnung",
"Die Transformation wurde noch nicht angewendet.\n"
"Möchten Sie die Originaldatei exportieren?",
QMessageBox.Yes | QMessageBox.No)
if reply == QMessageBox.No:
return
# Standard-Dateiname vorschlagen
original_path = self.main_window.parser.file_path
if original_path:
import os
base, ext = os.path.splitext(original_path)
default_name = f"{base}_transformed{ext}"
else:
default_name = "transformed.jxl"
file_path, _ = QFileDialog.getSaveFileName(
self, "JXL-Datei exportieren", default_name, "JXL Files (*.jxl)")
if file_path:
if self.adjuster.export_jxl(file_path):
QMessageBox.information(self, "Erfolg",
f"JXL-Datei exportiert:\n{file_path}")
self.main_window.statusBar().showMessage(f"Exportiert: {file_path}")
else:
QMessageBox.critical(self, "Fehler",
"Fehler beim Exportieren der JXL-Datei!")
class MainWindow(QMainWindow):
"""Hauptfenster der Anwendung"""
def __init__(self):
super().__init__()
self.parser = None
self.setWindowTitle("Trimble Geodesy Tool v2.0")
self.setMinimumSize(1000, 700)
self.setup_ui()
self.setup_menu()
def setup_ui(self):
# Zentrales Widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# Tab-Widget
self.tabs = QTabWidget()
self.tabs.addTab(JXLAnalysisTab(self), "📁 JXL-Analyse")
self.tabs.addTab(CORGeneratorTab(self), "📄 COR-Generator")
self.tabs.addTab(TransformationTab(self), "🔄 Transformation")
self.tabs.addTab(GeoreferencingTab(self), "🌍 Georeferenzierung")
self.tabs.addTab(NetworkAdjustmentTab(self), "📐 Netzausgleichung")
self.tabs.addTab(ReferencePointAdjusterTab(self), "📍 Referenzpunkt-Anpassung")
main_layout.addWidget(self.tabs)
# Statusleiste
self.statusBar().showMessage("Bereit - Bitte JXL-Datei laden")
def setup_menu(self):
menubar = self.menuBar()
# Datei-Menü
file_menu = menubar.addMenu("&Datei")
open_action = QAction("&Öffnen...", self)
open_action.setShortcut("Ctrl+O")
open_action.triggered.connect(self.open_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_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",
"<h2>Trimble Geodesy Tool v2.0</h2>"
"<p>Geodätische Vermessungsarbeiten mit JXL-Dateien</p>"
"<p><b>Funktionen:</b></p>"
"<ul>"
"<li>JXL-Datei Analyse mit TreeView für Stationierungen</li>"
"<li>Prismenkonstanten-Verwaltung bei einzelnen Messungen</li>"
"<li>Passpunkt-Qualitätsbewertung bei freien Stationierungen</li>"
"<li>COR-Datei Generierung und Export</li>"
"<li>Koordinatentransformation mit 2-Punkte-Definition</li>"
"<li>Georeferenzierung mit Passpunkten</li>"
"<li>Netzausgleichung mit automatischer Punkterkennung</li>"
"<li>Referenzpunkt-Anpassung</li>"
"</ul>"
"<p>Version 2.0 - Überarbeitet Januar 2026</p>")
def main():
app = QApplication(sys.argv)
# Modernes Styling
app.setStyle('Fusion')
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()