v2.0: Überarbeitung basierend auf Benutzer-Feedback

JXL-Analyse:
- TreeView für Stationierungen mit Unterknoten für Messungen
- Prismenkonstanten direkt bei jeder Messung editierbar
- Bei freier Stationierung: Passpunkt-Qualitätsbewertung mit Farbcodierung
  - Grün: Bester Passpunkt, Rot: Schlechtester, Gelb: Rest
- Checkbox zum Aktivieren/Deaktivieren von Passpunkten

Transformation:
- 2-Punkte-Definition: 7001 für XY, 7002 für Z als Standard-Vorschläge
- Y-Einzelfeld entfernt - nur XY-Nullpunkt und Z-Nullpunkt
- BUG FIX: 'Transformation anwenden' aktualisiert jetzt die Punktliste

Netzausgleichung:
- Manuelle Punktauswahl entfernt
- Automatische Erkennung von Festpunkten (aus Stationierungen)
- Automatische Erkennung von Messpunkten (3000er Serie)
- Detaillierter Bericht mit Punkttyp-Unterscheidung

Sonstiges:
- Testdaten hinzugefügt
- Interne Tests durchgeführt
This commit is contained in:
Developer 2026-01-18 12:54:23 +00:00
parent 486f166462
commit e8ce645a51
3 changed files with 10683 additions and 160 deletions

664
main.py
View File

@ -2,6 +2,7 @@
"""
Trimble Geodesy Tool - Hauptprogramm mit GUI
Geodätische Vermessungsarbeiten mit JXL-Dateien
Überarbeitet basierend auf Benutzer-Feedback
"""
import sys
@ -18,10 +19,10 @@ from PyQt5.QtWidgets import (
QSpinBox, QDoubleSpinBox, QCheckBox, QSplitter, QFrame, QScrollArea,
QHeaderView, QListWidget, QListWidgetItem, QDialog, QDialogButtonBox,
QFormLayout, QProgressBar, QStatusBar, QMenuBar, QMenu, QAction,
QToolBar, QStyle
QToolBar, QStyle, QTreeWidget, QTreeWidgetItem, QAbstractItemView
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QFont, QIcon, QPalette, QColor
from PyQt5.QtGui import QFont, QIcon, QPalette, QColor, QBrush
from modules.jxl_parser import JXLParser
from modules.cor_generator import CORGenerator, CORPoint
@ -32,11 +33,13 @@ from modules.reference_point_adjuster import ReferencePointAdjuster, Transformat
class JXLAnalysisTab(QWidget):
"""Tab für JXL-Datei Analyse und Bearbeitung"""
"""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):
@ -68,18 +71,43 @@ class JXLAnalysisTab(QWidget):
summary_layout = QVBoxLayout(summary_group)
self.summary_text = QTextEdit()
self.summary_text.setReadOnly(True)
self.summary_text.setMaximumHeight(200)
self.summary_text.setMaximumHeight(150)
summary_layout.addWidget(self.summary_text)
splitter.addWidget(summary_group)
# Punkte-Tabelle
points_group = QGroupBox("Punkte")
# 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(7)
self.points_table.setColumnCount(6)
self.points_table.setHorizontalHeaderLabels(
["Name", "Code", "East (X)", "North (Y)", "Elevation (Z)", "Methode", "Aktiv"])
["Name", "Code", "East (X)", "North (Y)", "Elevation (Z)", "Methode"])
self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
points_layout.addWidget(self.points_table)
@ -95,26 +123,6 @@ class JXLAnalysisTab(QWidget):
splitter.addWidget(points_group)
# Prismenkonstanten
prism_group = QGroupBox("Prismenkonstanten")
prism_layout = QVBoxLayout(prism_group)
self.prism_table = QTableWidget()
self.prism_table.setColumnCount(4)
self.prism_table.setHorizontalHeaderLabels(
["Target ID", "Prismentyp", "Konstante [mm]", "Neue Konstante [mm]"])
self.prism_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
prism_layout.addWidget(self.prism_table)
prism_actions = QHBoxLayout()
apply_prism_btn = QPushButton("Prismenkonstanten übernehmen")
apply_prism_btn.clicked.connect(self.apply_prism_changes)
prism_actions.addWidget(apply_prism_btn)
prism_actions.addStretch()
prism_layout.addLayout(prism_actions)
splitter.addWidget(prism_group)
layout.addWidget(splitter)
def browse_file(self):
@ -146,8 +154,171 @@ class JXLAnalysisTab(QWidget):
# Zusammenfassung
self.summary_text.setText(parser.get_summary())
# Punkte-Tabelle
points = parser.get_active_points()
# 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())):
@ -157,22 +328,6 @@ class JXLAnalysisTab(QWidget):
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))
self.points_table.setItem(row, 6, QTableWidgetItem("Ja" if not point.deleted else "Nein"))
# Prismen-Tabelle
targets = parser.targets
self.prism_table.setRowCount(len(targets))
for row, (tid, target) in enumerate(targets.items()):
self.prism_table.setItem(row, 0, QTableWidgetItem(tid))
self.prism_table.setItem(row, 1, QTableWidgetItem(target.prism_type))
self.prism_table.setItem(row, 2, QTableWidgetItem(f"{target.prism_constant * 1000:.1f}"))
spin = QDoubleSpinBox()
spin.setRange(-100, 100)
spin.setDecimals(1)
spin.setValue(target.prism_constant * 1000)
self.prism_table.setCellWidget(row, 3, spin)
def remove_selected_point(self):
row = self.points_table.currentRow()
@ -187,19 +342,6 @@ class JXLAnalysisTab(QWidget):
self.main_window.parser.remove_point(name)
self.update_display()
def apply_prism_changes(self):
parser = self.main_window.parser
if not parser:
return
for row in range(self.prism_table.rowCount()):
tid = self.prism_table.item(row, 0).text()
spin = self.prism_table.cellWidget(row, 3)
new_const = spin.value() / 1000.0 # mm to m
parser.modify_prism_constant(tid, new_const)
QMessageBox.information(self, "Info", "Prismenkonstanten wurden aktualisiert!")
class CORGeneratorTab(QWidget):
"""Tab für COR-Datei Generierung"""
@ -330,7 +472,7 @@ class CORGeneratorTab(QWidget):
class TransformationTab(QWidget):
"""Tab für Koordinatentransformation"""
"""Tab für Koordinatentransformation - Überarbeitet"""
def __init__(self, parent=None):
super().__init__(parent)
@ -402,21 +544,21 @@ class TransformationTab(QWidget):
layout.addWidget(self.manual_group)
# 2-Punkte-Definition
# 2-Punkte-Definition (überarbeitet)
self.twopoint_group = QGroupBox("2-Punkte-Definition")
twopoint_layout = QGridLayout(self.twopoint_group)
twopoint_layout.addWidget(QLabel("Ursprung (0,0):"), 0, 0)
self.origin_combo = QComboBox()
twopoint_layout.addWidget(self.origin_combo, 0, 1)
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-Referenz (0):"), 2, 0)
self.zref_combo = QComboBox()
twopoint_layout.addWidget(self.zref_combo, 2, 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)
@ -426,10 +568,17 @@ class TransformationTab(QWidget):
layout.addWidget(self.twopoint_group)
# Transformation durchführen
transform_btn = QPushButton("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)
@ -465,16 +614,40 @@ class TransformationTab(QWidget):
points = list(self.main_window.parser.get_active_points().keys())
self.origin_combo.clear()
self.xy_origin_combo.clear()
self.direction_combo.clear()
self.zref_combo.clear()
self.z_origin_combo.clear()
self.zref_combo.addItem("(wie Ursprung)")
# 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.origin_combo.addItem(name)
self.xy_origin_combo.addItem(name)
self.direction_combo.addItem(name)
self.zref_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:
@ -504,12 +677,9 @@ class TransformationTab(QWidget):
pivot_y=self.pivot_y_spin.value()
)
elif self.twopoint_radio.isChecked():
origin = self.origin_combo.currentText()
origin = self.xy_origin_combo.currentText()
direction = self.direction_combo.currentText()
zref = self.zref_combo.currentText()
if zref == "(wie Ursprung)":
zref = None
zref = self.z_origin_combo.currentText()
if not self.transformer.compute_from_two_points(origin, direction, zref):
QMessageBox.warning(self, "Fehler", "Punkte nicht gefunden!")
@ -523,7 +693,47 @@ class TransformationTab(QWidget):
report += self.transformer.get_comparison_table()
self.results_text.setText(report)
self.main_window.statusBar().showMessage("Transformation durchgeführt")
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(
@ -600,6 +810,7 @@ class GeoreferencingTab(QWidget):
# 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
@ -782,12 +993,14 @@ class GeoreferencingTab(QWidget):
class NetworkAdjustmentTab(QWidget):
"""Tab für Netzausgleichung"""
"""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):
@ -819,29 +1032,38 @@ class NetworkAdjustmentTab(QWidget):
layout.addWidget(config_group)
# Festpunkte
fixed_group = QGroupBox("Festpunkte")
fixed_layout = QVBoxLayout(fixed_group)
# Automatisch erkannte Punkte
points_group = QGroupBox("Automatisch erkannte Punkttypen")
points_layout = QVBoxLayout(points_group)
self.fixed_list = QListWidget()
self.fixed_list.setSelectionMode(QListWidget.MultiSelection)
fixed_layout.addWidget(self.fixed_list)
# 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)
fixed_buttons = QHBoxLayout()
refresh_btn = QPushButton("Liste aktualisieren")
refresh_btn.clicked.connect(self.refresh_point_list)
fixed_buttons.addWidget(refresh_btn)
# 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)
auto_btn = QPushButton("Auto (Referenzpunkte)")
auto_btn.clicked.connect(self.auto_select_fixed)
fixed_buttons.addWidget(auto_btn)
# 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)
fixed_layout.addLayout(fixed_buttons)
layout.addWidget(fixed_group)
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
@ -867,34 +1089,86 @@ class NetworkAdjustmentTab(QWidget):
results_layout.addLayout(export_layout)
layout.addWidget(results_group)
def refresh_point_list(self):
self.fixed_list.clear()
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
for name in sorted(self.main_window.parser.get_active_points().keys()):
item = QListWidgetItem(name)
self.fixed_list.addItem(item)
parser = self.main_window.parser
self.fixed_points.clear()
self.measurement_points.clear()
def auto_select_fixed(self):
if not self.main_window.parser:
# 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
self.fixed_list.clearSelection()
all_points = list(self.fixed_points) + list(self.measurement_points)
self.points_table.setRowCount(len(all_points))
ref_line = self.main_window.parser.get_reference_line()
if ref_line:
for i in range(self.fixed_list.count()):
item = self.fixed_list.item(i)
if item.text() in [ref_line.start_point, ref_line.end_point]:
item.setSelected(True)
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()
@ -905,16 +1179,15 @@ class NetworkAdjustmentTab(QWidget):
self.adjustment.extract_observations()
self.adjustment.initialize_points()
# Festpunkte setzen
for item in self.fixed_list.selectedItems():
self.adjustment.set_fixed_point(item.text())
if not self.adjustment.fixed_points:
self.adjustment.set_fixed_points_auto()
# Festpunkte setzen (automatisch erkannte)
for point_name in self.fixed_points:
self.adjustment.set_fixed_point(point_name)
try:
result = self.adjustment.adjust()
report = self.adjustment.get_adjustment_report()
# Bericht erstellen mit Festpunkt/Messpunkt-Unterscheidung
report = self.create_detailed_report()
self.results_text.setText(report)
status = "konvergiert" if result.converged else "nicht konvergiert"
@ -926,6 +1199,68 @@ class NetworkAdjustmentTab(QWidget):
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!")
@ -934,7 +1269,8 @@ class NetworkAdjustmentTab(QWidget):
file_path, _ = QFileDialog.getSaveFileName(
self, "Bericht speichern", "", "Text Files (*.txt)")
if file_path:
self.adjustment.export_report(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):
@ -945,7 +1281,22 @@ class NetworkAdjustmentTab(QWidget):
file_path, _ = QFileDialog.getSaveFileName(
self, "Koordinaten speichern", "", "CSV Files (*.csv)")
if file_path:
self.adjustment.export_adjusted_points(file_path, 'csv')
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}")
@ -1176,6 +1527,11 @@ class ReferencePointAdjusterTab(QWidget):
# 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"
@ -1228,47 +1584,32 @@ class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.parser: JXLParser = None
self.parser = None
self.setWindowTitle("Trimble Geodesy Tool v2.0")
self.setMinimumSize(1000, 700)
self.setup_ui()
def setup_ui(self):
self.setWindowTitle("Trimble Geodesy Tool - Geodätische Vermessungsarbeiten")
self.setMinimumSize(1200, 800)
# Menüleiste
self.setup_menu()
# Statusleiste
self.statusBar().showMessage("Bereit")
# Zentrale Widget mit Tabs
def setup_ui(self):
# Zentrales Widget
central_widget = QWidget()
self.setCentralWidget(central_widget)
layout = QVBoxLayout(central_widget)
main_layout = QVBoxLayout(central_widget)
# Tab-Widget
self.tabs = QTabWidget()
self.jxl_tab = JXLAnalysisTab(self)
self.tabs.addTab(self.jxl_tab, "📁 JXL-Analyse")
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")
self.cor_tab = CORGeneratorTab(self)
self.tabs.addTab(self.cor_tab, "📄 COR-Generator")
main_layout.addWidget(self.tabs)
self.transform_tab = TransformationTab(self)
self.tabs.addTab(self.transform_tab, "🔄 Transformation")
self.georef_tab = GeoreferencingTab(self)
self.tabs.addTab(self.georef_tab, "🌍 Georeferenzierung")
self.adjust_tab = NetworkAdjustmentTab(self)
self.tabs.addTab(self.adjust_tab, "📐 Netzausgleichung")
self.refpoint_tab = ReferencePointAdjusterTab(self)
self.tabs.addTab(self.refpoint_tab, "📍 Referenzpunkt anpassen")
layout.addWidget(self.tabs)
# Statusleiste
self.statusBar().showMessage("Bereit - Bitte JXL-Datei laden")
def setup_menu(self):
menubar = self.menuBar()
@ -1291,7 +1632,7 @@ class MainWindow(QMainWindow):
# Hilfe-Menü
help_menu = menubar.addMenu("&Hilfe")
about_action = QAction("&Über", self)
about_action = QAction("&Über...", self)
about_action.triggered.connect(self.show_about)
help_menu.addAction(about_action)
@ -1299,31 +1640,34 @@ class MainWindow(QMainWindow):
file_path, _ = QFileDialog.getOpenFileName(
self, "JXL-Datei öffnen", "", "JXL Files (*.jxl);;All Files (*)")
if file_path:
self.jxl_tab.file_path_edit.setText(file_path)
self.jxl_tab.load_file()
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",
"<h3>Trimble Geodesy Tool</h3>"
"<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 und Bearbeitung</li>"
"<li>COR-Datei Generierung</li>"
"<li>Koordinatentransformation (Rotation/Translation)</li>"
"<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 nach kleinsten Quadraten</li>"
"<li>Netzausgleichung mit automatischer Punkterkennung</li>"
"<li>Referenzpunkt-Anpassung</li>"
"</ul>"
"<p>Version 1.0</p>")
"<p>Version 2.0 - Überarbeitet Januar 2026</p>")
def main():
app = QApplication(sys.argv)
# Stil setzen
# Modernes Styling
app.setStyle('Fusion')
# Fenster erstellen und anzeigen
window = MainWindow()
window.show()

View File

@ -0,0 +1,84 @@
5001,0.000,0.000,0.000
5002,0.000,11.407,-0.035
1001,22.788,13.565,-1.952
2001,0.014,11.747,-1.361
2002,20.214,30.965,-1.663
2003,33.782,13.097,-2.616
2004,20.490,-15.429,-1.438
3001,33.838,13.220,-2.575
3002,26.308,30.140,-1.903
3003,26.357,-3.293,-2.115
3004,0.048,-0.530,-3.021
3005,-0.038,6.789,-3.414
6001,-18.196,13.913,-2.176
6002,-15.677,12.648,-2.286
6003,-26.318,12.550,-2.274
6004,-24.723,13.845,-1.559
3006,1.182,13.131,1.845
3007,0.867,16.745,1.841
1002,-20.883,13.008,-1.742
2005,-13.138,18.366,-1.730
2006,-19.762,10.592,-1.668
2007,-29.788,15.304,-1.497
2008,-20.421,22.326,-1.798
3008,-13.174,11.080,-0.280
3009,-13.168,10.756,3.783
3010,-14.042,4.481,1.836
3011,-13.166,13.790,5.689
3012,-13.164,11.191,7.558
3013,-13.164,10.602,10.922
2009,-13.144,12.601,-1.895
7002,-7.487,13.193,-3.675
7001,-13.134,19.746,-0.424
3014,-10.454,12.968,-3.593
3015,-5.752,14.319,-3.597
3016,-2.168,12.842,-3.598
3017,-16.960,9.871,-1.307
3018,-13.143,8.257,-1.713
3019,-21.129,4.481,-1.106
3020,-31.198,9.253,-2.128
3021,-29.609,16.923,-2.185
3022,-30.145,21.896,-3.368
3023,-20.860,18.943,-3.376
6005,-25.633,20.878,-2.243
1003,23.070,14.027,-1.969
3024,1.188,12.854,5.607
3025,1.213,3.983,5.615
3026,-0.029,17.401,9.376
3027,0.002,9.449,13.119
3028,-0.915,16.758,13.728
3029,1.096,3.374,9.955
1004,-22.696,19.765,-1.966
6006,-48.153,24.806,-2.092
6007,-48.551,23.493,-1.983
6008,-42.900,22.586,-2.096
1005,-46.031,23.575,-1.951
2010,-40.871,25.207,-1.886
2011,-42.736,16.007,-1.438
2012,-51.007,10.054,-1.947
2013,-46.596,26.451,-1.927
3030,-41.629,20.671,-2.199
3031,-42.358,17.069,-2.207
3032,-43.915,9.539,-2.218
3033,-39.002,22.932,-3.451
1006,22.923,13.878,-1.904
3034,-2.259,19.010,16.788
3035,-2.039,11.679,17.155
3036,-2.401,0.498,16.947
1007,-21.634,18.968,-1.815
3037,-29.302,18.141,1.265
3038,-30.284,13.473,-0.502
3039,-30.223,13.843,2.923
3040,-30.164,14.114,6.326
3041,-30.261,13.648,9.537
1008,-46.567,23.450,-1.900
3042,-43.596,11.396,4.659
3043,-41.019,23.777,4.663
1009,-17.132,16.548,-1.900
3044,-31.755,22.820,14.965
3045,-33.719,12.836,14.653
3046,-34.899,6.876,14.549
3047,-31.140,9.336,8.091
3048,-29.550,17.086,8.072
3049,-28.629,21.467,8.076
3050,-30.750,11.180,1.169

File diff suppressed because it is too large Load Diff