2071 lines
84 KiB
Python
2071 lines
84 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 = Passpunkte (5001, 5002, etc.) - werden NICHT ausgeglichen
|
||
- Neupunkte = Standpunkte (1001, 1002, etc.) - werden ausgeglichen
|
||
- Messpunkte = 3000er Serie - werden 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:\n"
|
||
" • Festpunkte (grün): Passpunkte mit bekannten Koordinaten - werden NICHT ausgeglichen\n"
|
||
" • Neupunkte (blau): Standpunkte des Tachymeters - werden AUSGEGLICHEN\n"
|
||
" • Messpunkte (gelb): Detailpunkte - werden AUSGEGLICHEN"
|
||
)
|
||
info_label.setStyleSheet("background-color: #f0f0f0; padding: 10px;")
|
||
points_layout.addWidget(info_label)
|
||
|
||
self.points_table = QTableWidget()
|
||
self.points_table.setColumnCount(5)
|
||
self.points_table.setHorizontalHeaderLabels(["Punkt", "Typ", "X", "Y", "Z"])
|
||
self.points_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||
self.points_table.setMaximumHeight(200)
|
||
points_layout.addWidget(self.points_table)
|
||
|
||
refresh_btn = QPushButton("🔍 Punkte automatisch klassifizieren")
|
||
refresh_btn.clicked.connect(self.auto_classify_points)
|
||
refresh_btn.setStyleSheet("background-color: #FF9800; color: white; font-weight: bold;")
|
||
points_layout.addWidget(refresh_btn)
|
||
|
||
layout.addWidget(points_group)
|
||
|
||
# Ausgleichung
|
||
btn_layout = QHBoxLayout()
|
||
|
||
adjust_btn = QPushButton("📐 Netzausgleichung durchführen")
|
||
adjust_btn.clicked.connect(self.run_adjustment)
|
||
adjust_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold; font-size: 14px; padding: 10px;")
|
||
btn_layout.addWidget(adjust_btn)
|
||
|
||
self.adopt_btn = QPushButton("✓ Ausgeglichene Punkte übernehmen")
|
||
self.adopt_btn.clicked.connect(self.adopt_adjusted_points)
|
||
self.adopt_btn.setStyleSheet("background-color: #2196F3; color: white; font-weight: bold;")
|
||
self.adopt_btn.setEnabled(False)
|
||
btn_layout.addWidget(self.adopt_btn)
|
||
|
||
layout.addLayout(btn_layout)
|
||
|
||
# Ergebnisse
|
||
results_group = QGroupBox("Ergebnisse")
|
||
results_layout = QVBoxLayout(results_group)
|
||
|
||
self.results_text = QTextEdit()
|
||
self.results_text.setReadOnly(True)
|
||
self.results_text.setFont(QFont("Courier", 9))
|
||
results_layout.addWidget(self.results_text)
|
||
|
||
export_layout = QHBoxLayout()
|
||
|
||
export_report_btn = QPushButton("Bericht exportieren")
|
||
export_report_btn.clicked.connect(self.export_report)
|
||
export_layout.addWidget(export_report_btn)
|
||
|
||
export_points_btn = QPushButton("📥 Koordinaten exportieren...")
|
||
export_points_btn.clicked.connect(self.export_points)
|
||
export_layout.addWidget(export_points_btn)
|
||
|
||
results_layout.addLayout(export_layout)
|
||
layout.addWidget(results_group)
|
||
|
||
def auto_classify_points(self):
|
||
"""Klassifiziert Punkte automatisch nach dem korrekten Konzept"""
|
||
if not self.main_window.parser:
|
||
QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!")
|
||
return
|
||
|
||
parser = self.main_window.parser
|
||
self.fixed_points.clear()
|
||
self.new_points.clear()
|
||
self.measurement_points.clear()
|
||
|
||
# 1. FESTPUNKTE = Passpunkte (Referenzpunkte mit bekannten Koordinaten)
|
||
ref_points = parser.get_reference_points()
|
||
for name in ref_points:
|
||
if name in parser.points:
|
||
self.fixed_points.add(name)
|
||
|
||
# 2. NEUPUNKTE = Standpunkte (Stationen des Tachymeters)
|
||
station_points = parser.get_station_points()
|
||
for name in station_points:
|
||
if name not in self.fixed_points: # Nicht wenn es ein Passpunkt ist
|
||
self.new_points.add(name)
|
||
|
||
# 3. MESSPUNKTE = Alle anderen Punkte
|
||
for name in parser.get_active_points().keys():
|
||
if name not in self.fixed_points and name not in self.new_points:
|
||
self.measurement_points.add(name)
|
||
|
||
self.update_points_table()
|
||
|
||
self.main_window.statusBar().showMessage(
|
||
f"Klassifiziert: {len(self.fixed_points)} Festpunkte, "
|
||
f"{len(self.new_points)} Neupunkte, "
|
||
f"{len(self.measurement_points)} Messpunkte")
|
||
|
||
def update_points_table(self):
|
||
"""Aktualisiert die Punkttabelle"""
|
||
parser = self.main_window.parser
|
||
if not parser:
|
||
return
|
||
|
||
all_classified = []
|
||
for name in self.fixed_points:
|
||
all_classified.append((name, "Festpunkt", QColor(200, 255, 200)))
|
||
for name in self.new_points:
|
||
all_classified.append((name, "Neupunkt", QColor(200, 200, 255)))
|
||
for name in self.measurement_points:
|
||
all_classified.append((name, "Messpunkt", QColor(255, 255, 200)))
|
||
|
||
self.points_table.setRowCount(len(all_classified))
|
||
|
||
for row, (name, point_type, color) in enumerate(sorted(all_classified)):
|
||
name_item = QTableWidgetItem(name)
|
||
name_item.setBackground(QBrush(color))
|
||
self.points_table.setItem(row, 0, name_item)
|
||
|
||
type_item = QTableWidgetItem(point_type)
|
||
type_item.setBackground(QBrush(color))
|
||
self.points_table.setItem(row, 1, type_item)
|
||
|
||
if name in parser.points:
|
||
p = parser.points[name]
|
||
x_item = QTableWidgetItem(f"{p.east:.4f}" if p.east else "")
|
||
y_item = QTableWidgetItem(f"{p.north:.4f}" if p.north else "")
|
||
z_item = QTableWidgetItem(f"{p.elevation:.4f}" if p.elevation else "")
|
||
x_item.setBackground(QBrush(color))
|
||
y_item.setBackground(QBrush(color))
|
||
z_item.setBackground(QBrush(color))
|
||
self.points_table.setItem(row, 2, x_item)
|
||
self.points_table.setItem(row, 3, y_item)
|
||
self.points_table.setItem(row, 4, z_item)
|
||
|
||
def run_adjustment(self):
|
||
"""Führt die Netzausgleichung durch"""
|
||
if not self.main_window.parser:
|
||
QMessageBox.warning(self, "Fehler", "Bitte zuerst eine JXL-Datei laden!")
|
||
return
|
||
|
||
if not self.fixed_points and not self.new_points and not self.measurement_points:
|
||
self.auto_classify_points()
|
||
|
||
if not self.fixed_points:
|
||
QMessageBox.warning(self, "Fehler",
|
||
"Keine Festpunkte erkannt! Mindestens ein Passpunkt mit bekannten Koordinaten wird benötigt.")
|
||
return
|
||
|
||
self.adjustment = NetworkAdjustment(self.main_window.parser)
|
||
self.adjustment.max_iterations = self.max_iter_spin.value()
|
||
self.adjustment.convergence_limit = self.convergence_spin.value() / 1000.0
|
||
|
||
self.adjustment.extract_observations()
|
||
self.adjustment.initialize_points()
|
||
|
||
# NUR Festpunkte setzen - Neupunkte und Messpunkte werden ausgeglichen!
|
||
for point_name in self.fixed_points:
|
||
self.adjustment.set_fixed_point(point_name)
|
||
|
||
try:
|
||
result = self.adjustment.adjust()
|
||
report = self.create_detailed_report()
|
||
self.results_text.setText(report)
|
||
|
||
self.adopt_btn.setEnabled(True)
|
||
|
||
status = "konvergiert" if result.converged else "nicht konvergiert"
|
||
self.main_window.statusBar().showMessage(
|
||
f"Ausgleichung {status}, {result.iterations} Iterationen")
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "Fehler", f"Ausgleichung fehlgeschlagen: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
def create_detailed_report(self):
|
||
"""Erstellt einen detaillierten Bericht"""
|
||
if not self.adjustment or not self.adjustment.result:
|
||
return "Keine Ergebnisse."
|
||
|
||
lines = []
|
||
lines.append("=" * 90)
|
||
lines.append("NETZAUSGLEICHUNG - ERGEBNISBERICHT (KORREKTES KONZEPT)")
|
||
lines.append("=" * 90)
|
||
lines.append("")
|
||
lines.append("KONZEPT:")
|
||
lines.append(" • Festpunkte = Passpunkte mit bekannten Koordinaten (NICHT ausgeglichen)")
|
||
lines.append(" • Neupunkte = Standpunkte des Tachymeters (AUSGEGLICHEN)")
|
||
lines.append(" • Messpunkte = Detailpunkte (AUSGEGLICHEN)")
|
||
lines.append("")
|
||
lines.append("-" * 90)
|
||
lines.append("STATISTIK")
|
||
lines.append("-" * 90)
|
||
lines.append(f"Festpunkte (Passpunkte): {len(self.fixed_points)}")
|
||
lines.append(f"Neupunkte (Standpunkte): {len(self.new_points)}")
|
||
lines.append(f"Messpunkte: {len(self.measurement_points)}")
|
||
lines.append(f"Beobachtungen: {self.adjustment.result.num_observations}")
|
||
lines.append(f"Unbekannte: {self.adjustment.result.num_unknowns}")
|
||
lines.append(f"Redundanz: {self.adjustment.result.redundancy}")
|
||
lines.append(f"Iterationen: {self.adjustment.result.iterations}")
|
||
lines.append(f"Konvergiert: {'Ja' if self.adjustment.result.converged else 'Nein'}")
|
||
lines.append(f"Sigma-0 a-posteriori: {self.adjustment.result.sigma_0_posteriori:.4f}")
|
||
lines.append("")
|
||
|
||
# Festpunkte (unverändert)
|
||
lines.append("-" * 90)
|
||
lines.append("FESTPUNKTE (Passpunkte - NICHT ausgeglichen)")
|
||
lines.append("-" * 90)
|
||
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12}")
|
||
lines.append("-" * 90)
|
||
for name in sorted(self.fixed_points):
|
||
if name in self.adjustment.points:
|
||
p = self.adjustment.points[name]
|
||
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}")
|
||
lines.append("")
|
||
|
||
# Neupunkte (ausgeglichen)
|
||
lines.append("-" * 90)
|
||
lines.append("NEUPUNKTE (Standpunkte - AUSGEGLICHEN)")
|
||
lines.append("-" * 90)
|
||
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10} {'σPos [mm]':>10}")
|
||
lines.append("-" * 90)
|
||
for name in sorted(self.new_points):
|
||
if name in self.adjustment.points:
|
||
p = self.adjustment.points[name]
|
||
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} "
|
||
f"{p.std_x*1000:>10.2f} {p.std_y*1000:>10.2f} {p.std_position*1000:>10.2f}")
|
||
lines.append("")
|
||
|
||
# Messpunkte (ausgeglichen)
|
||
lines.append("-" * 90)
|
||
lines.append("MESSPUNKTE (AUSGEGLICHEN)")
|
||
lines.append("-" * 90)
|
||
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12} {'σX [mm]':>10} {'σY [mm]':>10} {'σPos [mm]':>10}")
|
||
lines.append("-" * 90)
|
||
for name in sorted(self.measurement_points):
|
||
if name in self.adjustment.points:
|
||
p = self.adjustment.points[name]
|
||
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f} "
|
||
f"{p.std_x*1000:>10.2f} {p.std_y*1000:>10.2f} {p.std_position*1000:>10.2f}")
|
||
|
||
lines.append("")
|
||
lines.append("=" * 90)
|
||
|
||
return "\n".join(lines)
|
||
|
||
def adopt_adjusted_points(self):
|
||
"""Übernimmt ausgeglichene Punkte in den globalen Speicher"""
|
||
if not self.adjustment or not self.adjustment.points:
|
||
QMessageBox.warning(self, "Fehler", "Keine ausgeglichenen Punkte!")
|
||
return
|
||
|
||
points_dict = {}
|
||
for name, p in self.adjustment.points.items():
|
||
points_dict[name] = (p.x, p.y, p.z)
|
||
|
||
adjusted_points_store.set_points(points_dict)
|
||
|
||
QMessageBox.information(self, "Erfolg",
|
||
f"{len(points_dict)} ausgeglichene Punkte übernommen!\n\n"
|
||
"Die Punkte sind jetzt in anderen Modulen verfügbar:\n"
|
||
"• COR Generator\n"
|
||
"• Transformation\n"
|
||
"• Georeferenzierung")
|
||
|
||
self.main_window.statusBar().showMessage(
|
||
f"Ausgeglichene Punkte verfügbar ({len(points_dict)} Punkte)")
|
||
|
||
def export_report(self):
|
||
if not self.adjustment:
|
||
QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!")
|
||
return
|
||
|
||
report = self.create_detailed_report()
|
||
export_text_with_dialog(self, report, "netzausgleichung_bericht")
|
||
|
||
def export_points(self):
|
||
if not self.adjustment or not self.adjustment.result:
|
||
QMessageBox.warning(self, "Fehler", "Keine Ergebnisse!")
|
||
return
|
||
|
||
points = []
|
||
for name, p in self.adjustment.points.items():
|
||
points.append(CORPoint(name=name, x=p.x, y=p.y, z=p.z))
|
||
|
||
export_points_with_dialog(self, points, "ausgeglichen")
|
||
|
||
|
||
# =============================================================================
|
||
# Referenzpunkt-Anpassung Tab
|
||
# =============================================================================
|
||
class ReferencePointAdjusterTab(QWidget):
|
||
"""Tab für Referenzpunkt-Anpassung"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.main_window = parent
|
||
self.adjuster = ReferencePointAdjuster()
|
||
self.setup_ui()
|
||
|
||
def setup_ui(self):
|
||
layout = QVBoxLayout(self)
|
||
|
||
# Info-Gruppe
|
||
info_group = QGroupBox("Aktueller Referenzpunkt")
|
||
info_layout = QFormLayout(info_group)
|
||
|
||
self.ref_point_name = QLabel("Nicht geladen")
|
||
self.ref_point_name.setStyleSheet("font-weight: bold; font-size: 14px;")
|
||
info_layout.addRow("Punktname:", self.ref_point_name)
|
||
|
||
self.current_east = QLabel("0.000")
|
||
self.current_north = QLabel("0.000")
|
||
self.current_elev = QLabel("0.000")
|
||
|
||
info_layout.addRow("East (X):", self.current_east)
|
||
info_layout.addRow("North (Y):", self.current_north)
|
||
info_layout.addRow("Elevation (Z):", self.current_elev)
|
||
|
||
refresh_btn = QPushButton("Referenzpunkt aktualisieren")
|
||
refresh_btn.clicked.connect(self.load_reference_point)
|
||
info_layout.addRow("", refresh_btn)
|
||
|
||
layout.addWidget(info_group)
|
||
|
||
# Neue Koordinaten
|
||
new_coords_group = QGroupBox("Neue Koordinaten")
|
||
new_coords_layout = QGridLayout(new_coords_group)
|
||
|
||
new_coords_layout.addWidget(QLabel("East (X):"), 0, 0)
|
||
self.new_east_spin = QDoubleSpinBox()
|
||
self.new_east_spin.setRange(-10000000, 10000000)
|
||
self.new_east_spin.setDecimals(4)
|
||
self.new_east_spin.setSuffix(" m")
|
||
new_coords_layout.addWidget(self.new_east_spin, 0, 1)
|
||
|
||
new_coords_layout.addWidget(QLabel("North (Y):"), 1, 0)
|
||
self.new_north_spin = QDoubleSpinBox()
|
||
self.new_north_spin.setRange(-10000000, 10000000)
|
||
self.new_north_spin.setDecimals(4)
|
||
self.new_north_spin.setSuffix(" m")
|
||
new_coords_layout.addWidget(self.new_north_spin, 1, 1)
|
||
|
||
new_coords_layout.addWidget(QLabel("Elevation (Z):"), 2, 0)
|
||
self.new_elev_spin = QDoubleSpinBox()
|
||
self.new_elev_spin.setRange(-10000, 10000)
|
||
self.new_elev_spin.setDecimals(4)
|
||
self.new_elev_spin.setSuffix(" m")
|
||
new_coords_layout.addWidget(self.new_elev_spin, 2, 1)
|
||
|
||
self.delta_label = QLabel("ΔX: 0.000 m | ΔY: 0.000 m | ΔZ: 0.000 m")
|
||
self.delta_label.setStyleSheet("color: blue;")
|
||
new_coords_layout.addWidget(self.delta_label, 3, 0, 1, 2)
|
||
|
||
self.new_east_spin.valueChanged.connect(self.update_delta)
|
||
self.new_north_spin.valueChanged.connect(self.update_delta)
|
||
self.new_elev_spin.valueChanged.connect(self.update_delta)
|
||
|
||
layout.addWidget(new_coords_group)
|
||
|
||
# Aktionen
|
||
actions_layout = QHBoxLayout()
|
||
|
||
preview_btn = QPushButton("Vorschau")
|
||
preview_btn.clicked.connect(self.preview_transformation)
|
||
preview_btn.setStyleSheet("background-color: #4CAF50; color: white;")
|
||
actions_layout.addWidget(preview_btn)
|
||
|
||
apply_btn = QPushButton("Anwenden")
|
||
apply_btn.clicked.connect(self.apply_transformation)
|
||
apply_btn.setStyleSheet("background-color: #2196F3; color: white;")
|
||
actions_layout.addWidget(apply_btn)
|
||
|
||
export_btn = QPushButton("📥 Exportieren...")
|
||
export_btn.clicked.connect(self.export_points)
|
||
export_btn.setStyleSheet("background-color: #FF9800; color: white;")
|
||
actions_layout.addWidget(export_btn)
|
||
|
||
layout.addLayout(actions_layout)
|
||
|
||
# Vorschau
|
||
preview_group = QGroupBox("Vorschau")
|
||
preview_layout = QVBoxLayout(preview_group)
|
||
|
||
self.preview_table = QTableWidget()
|
||
self.preview_table.setColumnCount(7)
|
||
self.preview_table.setHorizontalHeaderLabels(
|
||
["Punkt", "Alt X", "Alt Y", "Alt Z", "Neu X", "Neu Y", "Neu Z"])
|
||
self.preview_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||
preview_layout.addWidget(self.preview_table)
|
||
|
||
layout.addWidget(preview_group)
|
||
|
||
# Bericht
|
||
report_group = QGroupBox("Bericht")
|
||
report_layout = QVBoxLayout(report_group)
|
||
|
||
self.report_text = QTextEdit()
|
||
self.report_text.setReadOnly(True)
|
||
self.report_text.setFont(QFont("Courier", 9))
|
||
self.report_text.setMaximumHeight(150)
|
||
report_layout.addWidget(self.report_text)
|
||
|
||
layout.addWidget(report_group)
|
||
|
||
def load_reference_point(self):
|
||
if not self.main_window.parser:
|
||
QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!")
|
||
return
|
||
|
||
self.adjuster.set_parser(self.main_window.parser)
|
||
info = self.adjuster.get_reference_point_info()
|
||
|
||
if not info["found"]:
|
||
QMessageBox.warning(self, "Fehler", info['message'])
|
||
return
|
||
|
||
self.ref_point_name.setText(info["name"])
|
||
self.current_east.setText(f"{info['east']:.4f} m")
|
||
self.current_north.setText(f"{info['north']:.4f} m")
|
||
self.current_elev.setText(f"{info['elevation']:.4f} m")
|
||
|
||
self.new_east_spin.setValue(info['east'])
|
||
self.new_north_spin.setValue(info['north'])
|
||
self.new_elev_spin.setValue(info['elevation'])
|
||
|
||
def update_delta(self):
|
||
if not self.adjuster.parser:
|
||
return
|
||
|
||
dx = self.new_east_spin.value() - self.adjuster.original_coords[0]
|
||
dy = self.new_north_spin.value() - self.adjuster.original_coords[1]
|
||
dz = self.new_elev_spin.value() - self.adjuster.original_coords[2]
|
||
|
||
self.delta_label.setText(f"ΔX: {dx:+.4f} m | ΔY: {dy:+.4f} m | ΔZ: {dz:+.4f} m")
|
||
|
||
def preview_transformation(self):
|
||
if not self.main_window.parser:
|
||
QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!")
|
||
return
|
||
|
||
valid, message = self.adjuster.validate_input(
|
||
self.new_east_spin.value(),
|
||
self.new_north_spin.value(),
|
||
self.new_elev_spin.value()
|
||
)
|
||
|
||
if not valid:
|
||
reply = QMessageBox.warning(self, "Warnung",
|
||
f"{message}\n\nTrotzdem fortfahren?",
|
||
QMessageBox.Yes | QMessageBox.No)
|
||
if reply == QMessageBox.No:
|
||
return
|
||
|
||
self.adjuster.set_new_coordinates(
|
||
self.new_east_spin.value(),
|
||
self.new_north_spin.value(),
|
||
self.new_elev_spin.value()
|
||
)
|
||
|
||
results = self.adjuster.preview_transformation()
|
||
|
||
self.preview_table.setRowCount(len(results))
|
||
for i, result in enumerate(results):
|
||
self.preview_table.setItem(i, 0, QTableWidgetItem(result.original_point))
|
||
self.preview_table.setItem(i, 1, QTableWidgetItem(f"{result.original_coords[0]:.4f}"))
|
||
self.preview_table.setItem(i, 2, QTableWidgetItem(f"{result.original_coords[1]:.4f}"))
|
||
self.preview_table.setItem(i, 3, QTableWidgetItem(f"{result.original_coords[2]:.4f}"))
|
||
self.preview_table.setItem(i, 4, QTableWidgetItem(f"{result.new_coords[0]:.4f}"))
|
||
self.preview_table.setItem(i, 5, QTableWidgetItem(f"{result.new_coords[1]:.4f}"))
|
||
self.preview_table.setItem(i, 6, QTableWidgetItem(f"{result.new_coords[2]:.4f}"))
|
||
|
||
self.report_text.setText(self.adjuster.get_summary_report())
|
||
|
||
def apply_transformation(self):
|
||
if not self.main_window.parser:
|
||
QMessageBox.warning(self, "Fehler", "Bitte zuerst JXL laden!")
|
||
return
|
||
|
||
if not self.adjuster.affected_points:
|
||
QMessageBox.warning(self, "Fehler", "Bitte zuerst Vorschau berechnen!")
|
||
return
|
||
|
||
reply = QMessageBox.question(self, "Bestätigung",
|
||
f"Transformation auf {len(self.adjuster.affected_points)} Punkte anwenden?",
|
||
QMessageBox.Yes | QMessageBox.No)
|
||
|
||
if reply == QMessageBox.No:
|
||
return
|
||
|
||
if self.adjuster.apply_transformation():
|
||
self.report_text.setText(self.adjuster.get_summary_report())
|
||
|
||
jxl_tab = self.main_window.tabs.widget(0)
|
||
if hasattr(jxl_tab, 'update_display'):
|
||
jxl_tab.update_display()
|
||
|
||
QMessageBox.information(self, "Erfolg", "Transformation angewendet!")
|
||
|
||
def export_points(self):
|
||
if not self.main_window.parser:
|
||
QMessageBox.warning(self, "Fehler", "Keine Punkte!")
|
||
return
|
||
|
||
points = []
|
||
for name, p in self.main_window.parser.get_active_points().items():
|
||
if p.east is not None and p.north is not None:
|
||
points.append(CORPoint(name=name, x=p.east, y=p.north, z=p.elevation or 0))
|
||
|
||
export_points_with_dialog(self, points, "angepasst")
|
||
|
||
|
||
# =============================================================================
|
||
# Hauptfenster
|
||
# =============================================================================
|
||
class MainWindow(QMainWindow):
|
||
"""Hauptfenster der Anwendung"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.parser = None
|
||
self.setWindowTitle("Trimble Geodesy Tool v3.0")
|
||
self.setMinimumSize(1200, 850)
|
||
self.setup_ui()
|
||
self.setup_menu()
|
||
|
||
def setup_ui(self):
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
main_layout = QVBoxLayout(central_widget)
|
||
|
||
# Status für ausgeglichene Punkte
|
||
self.adjusted_status = QLabel("🔴 Keine ausgeglichenen Punkte verfügbar")
|
||
self.adjusted_status.setStyleSheet("background-color: #fff3e0; padding: 5px;")
|
||
main_layout.addWidget(self.adjusted_status)
|
||
|
||
# Tab-Widget
|
||
self.tabs = QTabWidget()
|
||
self.tabs.addTab(JXLAnalysisTab(self), "📊 JXL-Analyse")
|
||
self.tabs.addTab(CORGeneratorTab(self), "📄 COR-Generator")
|
||
self.tabs.addTab(TransformationTab(self), "🔄 Transformation")
|
||
self.tabs.addTab(GeoreferencingTab(self), "🌍 Georeferenzierung")
|
||
self.tabs.addTab(NetworkAdjustmentTab(self), "📐 Netzausgleichung")
|
||
self.tabs.addTab(ReferencePointAdjusterTab(self), "📍 Referenzpunkt")
|
||
|
||
self.tabs.currentChanged.connect(self.on_tab_changed)
|
||
main_layout.addWidget(self.tabs)
|
||
|
||
# Status Bar
|
||
self.setStatusBar(QStatusBar())
|
||
self.statusBar().showMessage("Bereit - Bitte JXL-Datei laden")
|
||
|
||
def on_tab_changed(self, index):
|
||
"""Aktualisiert Status bei Tab-Wechsel"""
|
||
self.update_adjusted_status()
|
||
|
||
def update_adjusted_status(self):
|
||
"""Aktualisiert die Statusanzeige für ausgeglichene Punkte"""
|
||
if adjusted_points_store.is_available():
|
||
n = len(adjusted_points_store.points)
|
||
self.adjusted_status.setText(f"🟢 {n} ausgeglichene Punkte verfügbar")
|
||
self.adjusted_status.setStyleSheet("background-color: #e8f5e9; padding: 5px; color: green;")
|
||
else:
|
||
self.adjusted_status.setText("🔴 Keine ausgeglichenen Punkte verfügbar")
|
||
self.adjusted_status.setStyleSheet("background-color: #fff3e0; padding: 5px;")
|
||
|
||
def setup_menu(self):
|
||
menubar = self.menuBar()
|
||
|
||
# Datei-Menü
|
||
file_menu = menubar.addMenu("Datei")
|
||
|
||
open_action = QAction("JXL öffnen...", self)
|
||
open_action.setShortcut("Ctrl+O")
|
||
open_action.triggered.connect(self.open_jxl_file)
|
||
file_menu.addAction(open_action)
|
||
|
||
file_menu.addSeparator()
|
||
|
||
exit_action = QAction("Beenden", self)
|
||
exit_action.setShortcut("Ctrl+Q")
|
||
exit_action.triggered.connect(self.close)
|
||
file_menu.addAction(exit_action)
|
||
|
||
# Hilfe-Menü
|
||
help_menu = menubar.addMenu("Hilfe")
|
||
|
||
about_action = QAction("Über...", self)
|
||
about_action.triggered.connect(self.show_about)
|
||
help_menu.addAction(about_action)
|
||
|
||
def open_jxl_file(self):
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "JXL-Datei öffnen", "", "JXL Files (*.jxl);;All Files (*)")
|
||
|
||
if file_path:
|
||
jxl_tab = self.tabs.widget(0)
|
||
jxl_tab.file_path_edit.setText(file_path)
|
||
jxl_tab.load_file()
|
||
|
||
def show_about(self):
|
||
QMessageBox.about(self, "Über Trimble Geodesy Tool",
|
||
"Trimble Geodesy Tool v3.0\n\n"
|
||
"Geodätische Vermessungsarbeiten mit JXL-Dateien\n\n"
|
||
"Features:\n"
|
||
"• JXL-Datei Analyse mit Berechnungsprotokoll\n"
|
||
"• COR/CSV/TXT/DXF Export\n"
|
||
"• Koordinatentransformation\n"
|
||
"• Georeferenzierung mit automatischer Punktzuordnung\n"
|
||
"• Netzausgleichung (korrektes Konzept)\n"
|
||
"• Datenfluss zwischen Modulen\n"
|
||
"• Referenzpunkt-Anpassung")
|
||
|
||
|
||
def main():
|
||
app = QApplication(sys.argv)
|
||
app.setStyle("Fusion")
|
||
|
||
# Theme
|
||
palette = QPalette()
|
||
palette.setColor(QPalette.Window, QColor(240, 240, 240))
|
||
palette.setColor(QPalette.WindowText, QColor(0, 0, 0))
|
||
palette.setColor(QPalette.Base, QColor(255, 255, 255))
|
||
palette.setColor(QPalette.AlternateBase, QColor(245, 245, 245))
|
||
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 220))
|
||
palette.setColor(QPalette.ToolTipText, QColor(0, 0, 0))
|
||
palette.setColor(QPalette.Text, QColor(0, 0, 0))
|
||
palette.setColor(QPalette.Button, QColor(240, 240, 240))
|
||
palette.setColor(QPalette.ButtonText, QColor(0, 0, 0))
|
||
palette.setColor(QPalette.BrightText, QColor(255, 0, 0))
|
||
palette.setColor(QPalette.Highlight, QColor(42, 130, 218))
|
||
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
||
app.setPalette(palette)
|
||
|
||
window = MainWindow()
|
||
window.show()
|
||
|
||
sys.exit(app.exec_())
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|