Initial commit: Trimble Geodesy Tool mit PyQt5 GUI
Features: - JXL-Datei Analyse und Bearbeitung - COR-Datei Generierung - Koordinatentransformation (Rotation/Translation) - Georeferenzierung mit Passpunkten - Netzausgleichung nach kleinsten Quadraten
This commit is contained in:
commit
6af2c0333f
|
|
@ -0,0 +1 @@
|
|||
gAAAAABpbMkVj3c_kR7OnAA7zo0Sfzw05-3MyOtYVW_kwm6pXuL5RNaqVAIlgDozOuzqrE7RPfvpzW2IRwTvNCMn1AQOXx-EsHqqzRj1WS153NEFBXvkT8g=
|
||||
|
|
@ -0,0 +1,128 @@
|
|||
# Trimble Geodesy Tool
|
||||
|
||||
Ein vollständiges Python-Programm mit grafischer Benutzeroberfläche (GUI) für geodätische Vermessungsarbeiten mit Trimble JXL-Dateien.
|
||||
|
||||
## Funktionen
|
||||
|
||||
### 1. JXL-Datei Analyse und Bearbeitung
|
||||
- Trimble JXL-Dateien einlesen und analysieren
|
||||
- Übersicht über Punkte, Stationen und Messungen
|
||||
- Passpunkte entfernen
|
||||
- Prismenkonstanten ändern
|
||||
- Parameter bearbeiten
|
||||
|
||||
### 2. COR-Datei Generierung
|
||||
- Aus JXL-Dateien Punktdateien im COR-Format berechnen
|
||||
- Unterstützt zwei Stationierungsarten:
|
||||
- Erste Stationierung über eine Referenzlinie
|
||||
- Alle anderen als freie Stationierungen
|
||||
- Export in verschiedene Formate (COR, CSV, TXT, DXF)
|
||||
|
||||
### 3. Koordinatensystem-Transformation
|
||||
- Rotation um einen wählbaren Punkt
|
||||
- Verschiebung in XY-Richtung
|
||||
- Verschiebung in Z-Richtung
|
||||
- Zwei Eingabemodi:
|
||||
- Manuelle Eingabe von Transformationsparametern
|
||||
- Definition über 2 Punkte (Ursprung und Y-Richtung)
|
||||
- **Keine Maßstabsänderung** (wie gefordert)
|
||||
|
||||
### 4. Georeferenzierung
|
||||
- Mindestens 3 Passpunkte für Transformation
|
||||
- Rotation und Translation des Koordinatensystems
|
||||
- **Keine Maßstabsänderung** (keine Helmert-Transformation)
|
||||
- Restfehler ausgleichen und anzeigen
|
||||
- Detaillierte Qualitätsparameter (RMSE, Residuen)
|
||||
|
||||
### 5. Netzausgleichung
|
||||
- Methode der kleinsten Quadrate
|
||||
- Basierend auf Beobachtungen der JXL-Datei
|
||||
- Automatische Festpunkterkennung
|
||||
- Ausgabe von:
|
||||
- Ausgeglichenen Koordinaten
|
||||
- Standardabweichungen
|
||||
- Residuen
|
||||
- Qualitätsparametern (Sigma-0, Chi-Quadrat, RMSE)
|
||||
|
||||
## Installation
|
||||
|
||||
### Voraussetzungen
|
||||
- Python 3.8 oder höher
|
||||
- PyQt5
|
||||
- NumPy
|
||||
- SciPy
|
||||
- lxml
|
||||
|
||||
### Installation der Abhängigkeiten
|
||||
```bash
|
||||
pip install PyQt5 numpy scipy lxml
|
||||
```
|
||||
|
||||
## Verwendung
|
||||
|
||||
### Programm starten
|
||||
```bash
|
||||
cd /home/ubuntu/trimble_geodesy
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
### Workflow
|
||||
1. **JXL-Analyse Tab**: JXL-Datei laden und analysieren
|
||||
2. **COR-Generator Tab**: Koordinaten generieren und exportieren
|
||||
3. **Transformation Tab**: Koordinatensystem rotieren/verschieben
|
||||
4. **Georeferenzierung Tab**: Mit Passpunkten transformieren
|
||||
5. **Netzausgleichung Tab**: Netzausgleichung durchführen
|
||||
|
||||
## Dateistruktur
|
||||
|
||||
```
|
||||
trimble_geodesy/
|
||||
├── main.py # Hauptprogramm mit GUI
|
||||
├── modules/
|
||||
│ ├── __init__.py
|
||||
│ ├── jxl_parser.py # JXL-Datei Parser
|
||||
│ ├── cor_generator.py # COR-Datei Generator
|
||||
│ ├── transformation.py # Koordinatentransformation
|
||||
│ ├── georeferencing.py # Georeferenzierung
|
||||
│ └── network_adjustment.py # Netzausgleichung
|
||||
├── output/ # Ausgabeverzeichnis
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Technische Details
|
||||
|
||||
### JXL-Format
|
||||
Das JXL-Format (Trimble JobXML) ist ein XML-basiertes Format für Vermessungsdaten:
|
||||
- `<PointRecord>`: Punktdaten mit Koordinaten und Messungen
|
||||
- `<StationRecord>`: Stationsinformationen
|
||||
- `<TargetRecord>`: Prismeneinstellungen
|
||||
- `<BackBearingRecord>`: Orientierungsdaten
|
||||
|
||||
### COR-Format
|
||||
Das COR-Format ist ein einfaches Textformat für Koordinaten:
|
||||
```
|
||||
|Punkt |X |Y |Z |
|
||||
|5001 |0.000 |0.000 |0.000 |
|
||||
|5002 |0 | 11.407 | -0.035 |
|
||||
```
|
||||
|
||||
### Transformation
|
||||
Die Transformation verwendet eine 4-Parameter-Transformation:
|
||||
- Translation X, Y, Z
|
||||
- Rotation um die Z-Achse
|
||||
- **Kein Maßstabsfaktor** (Maßstab = 1.0)
|
||||
|
||||
### Netzausgleichung
|
||||
Die Netzausgleichung verwendet:
|
||||
- Gauß-Markov-Modell
|
||||
- Beobachtungsgleichungen für Richtungen und Strecken
|
||||
- Iterative Lösung nach Gauß-Newton
|
||||
- Varianzfortpflanzung für Genauigkeitsmaße
|
||||
|
||||
## Lizenz
|
||||
|
||||
Dieses Programm wurde für geodätische Vermessungsarbeiten entwickelt.
|
||||
|
||||
## Autor
|
||||
|
||||
Entwickelt für geodätische Vermessungsarbeiten mit Trimble-Instrumenten.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Trimble Geodesy Modules
|
||||
from .jxl_parser import JXLParser
|
||||
from .cor_generator import CORGenerator
|
||||
from .transformation import CoordinateTransformer
|
||||
from .georeferencing import Georeferencer
|
||||
from .network_adjustment import NetworkAdjustment
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,406 @@
|
|||
"""
|
||||
COR Generator Module - Generiert COR-Punktdateien aus JXL-Daten
|
||||
Unterstützt Referenzlinien-Stationierung und freie Stationierung
|
||||
"""
|
||||
|
||||
import math
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from .jxl_parser import JXLParser, Point, Station
|
||||
|
||||
|
||||
@dataclass
|
||||
class CORPoint:
|
||||
"""Repräsentiert einen Punkt im COR-Format"""
|
||||
name: str
|
||||
x: float # East
|
||||
y: float # North
|
||||
z: float # Elevation
|
||||
|
||||
def to_cor_line(self) -> str:
|
||||
"""Formatiert als COR-Zeile"""
|
||||
# Format: |Name |X |Y |Z |
|
||||
x_str = f"{self.x:.3f}" if abs(self.x) >= 0.001 else "0"
|
||||
y_str = f"{self.y:.3f}" if abs(self.y) >= 0.001 else "0"
|
||||
z_str = f"{self.z:.3f}" if abs(self.z) >= 0.001 else "0"
|
||||
|
||||
return f"|{self.name} |{x_str} | {y_str} | {z_str} |"
|
||||
|
||||
|
||||
class CORGenerator:
|
||||
"""Generator für COR-Dateien aus JXL-Daten"""
|
||||
|
||||
def __init__(self, parser: JXLParser):
|
||||
self.parser = parser
|
||||
self.cor_points: List[CORPoint] = []
|
||||
|
||||
# Transformationsparameter (für lokales System)
|
||||
self.origin_east: float = 0.0
|
||||
self.origin_north: float = 0.0
|
||||
self.origin_elev: float = 0.0
|
||||
self.rotation: float = 0.0 # in Radiant
|
||||
|
||||
def compute_from_observations(self) -> List[CORPoint]:
|
||||
"""
|
||||
Berechnet Koordinaten aus den Rohbeobachtungen
|
||||
Erste Station: Referenzlinie
|
||||
Weitere Stationen: Freie Stationierung
|
||||
"""
|
||||
self.cor_points = []
|
||||
computed_coords: Dict[str, Tuple[float, float, float]] = {}
|
||||
|
||||
# Referenzlinie finden
|
||||
ref_line = self.parser.get_reference_line()
|
||||
|
||||
# Stationen sortieren nach Aufnahmezeitpunkt
|
||||
stations_sorted = sorted(self.parser.stations.items(),
|
||||
key=lambda x: x[1].timestamp)
|
||||
|
||||
first_station = True
|
||||
|
||||
for station_id, station in stations_sorted:
|
||||
if station.station_type == 'ReflineStationSetup' and first_station:
|
||||
# Erste Station mit Referenzlinie
|
||||
self._compute_refline_station(station_id, station, ref_line, computed_coords)
|
||||
first_station = False
|
||||
else:
|
||||
# Freie Stationierung
|
||||
self._compute_free_station(station_id, station, computed_coords)
|
||||
|
||||
# COR-Punkte erstellen
|
||||
for name, (e, n, elev) in computed_coords.items():
|
||||
self.cor_points.append(CORPoint(
|
||||
name=name,
|
||||
x=e,
|
||||
y=n,
|
||||
z=elev
|
||||
))
|
||||
|
||||
return self.cor_points
|
||||
|
||||
def _compute_refline_station(self, station_id: str, station: Station,
|
||||
ref_line, computed_coords: Dict):
|
||||
"""Berechnet Punkte für eine Referenzlinien-Station"""
|
||||
|
||||
# Referenzpunkte definieren das lokale System
|
||||
if ref_line:
|
||||
start_name = ref_line.start_point
|
||||
end_name = ref_line.end_point
|
||||
|
||||
# Startpunkt ist Ursprung (0,0)
|
||||
if start_name in self.parser.points:
|
||||
computed_coords[start_name] = (0.0, 0.0, 0.0)
|
||||
|
||||
# Endpunkt liegt auf der Y-Achse (Nord-Richtung)
|
||||
# Die Distanz muss aus den Messungen berechnet werden
|
||||
|
||||
# Alle Messungen von dieser Station
|
||||
measurements = self.parser.get_measurements_from_station(station_id)
|
||||
|
||||
# Finde Backbearing für Orientierung
|
||||
backbearing = None
|
||||
for bb_id, bb in self.parser.backbearings.items():
|
||||
if bb.station_record_id == station_id:
|
||||
backbearing = bb
|
||||
break
|
||||
|
||||
# Stationskoordinaten
|
||||
if station.name in self.parser.points:
|
||||
st_point = self.parser.points[station.name]
|
||||
st_e = st_point.east if st_point.east is not None else 0.0
|
||||
st_n = st_point.north if st_point.north is not None else 0.0
|
||||
st_elev = st_point.elevation if st_point.elevation is not None else 0.0
|
||||
|
||||
computed_coords[station.name] = (st_e, st_n, st_elev)
|
||||
|
||||
# Punkte aus Messungen berechnen
|
||||
for meas in measurements:
|
||||
if meas.name and meas.horizontal_circle is not None and meas.edm_distance is not None:
|
||||
# Prismenkonstante holen
|
||||
prism_const = 0.0
|
||||
if meas.target_id in self.parser.targets:
|
||||
prism_const = self.parser.targets[meas.target_id].prism_constant
|
||||
|
||||
# Korrigierte Distanz
|
||||
dist = meas.edm_distance + prism_const
|
||||
|
||||
# Vertikalwinkel
|
||||
vz = meas.vertical_circle if meas.vertical_circle is not None else 100.0
|
||||
vz_rad = vz * math.pi / 200.0 # Gon zu Radiant
|
||||
|
||||
# Horizontaldistanz
|
||||
h_dist = dist * math.sin(vz_rad)
|
||||
|
||||
# Höhendifferenz (von Zenitwinkel)
|
||||
dh = dist * math.cos(vz_rad)
|
||||
|
||||
# Richtung berechnen
|
||||
hz = meas.horizontal_circle
|
||||
|
||||
# Orientierung anwenden
|
||||
if backbearing and backbearing.orientation_correction is not None:
|
||||
ori = backbearing.orientation_correction
|
||||
else:
|
||||
ori = 0.0
|
||||
|
||||
# Azimut berechnen
|
||||
# Bei Referenzlinie: Der Hz-Kreis zum Backsight definiert die Nordrichtung
|
||||
azimut_gon = hz + ori
|
||||
azimut_rad = azimut_gon * math.pi / 200.0
|
||||
|
||||
# Koordinatenberechnung
|
||||
de = h_dist * math.sin(azimut_rad)
|
||||
dn = h_dist * math.cos(azimut_rad)
|
||||
|
||||
# Endkoordinaten
|
||||
if meas.north is not None and meas.east is not None:
|
||||
# Verwende berechnete Koordinaten aus JXL
|
||||
e = meas.east
|
||||
n = meas.north
|
||||
elev = meas.elevation if meas.elevation is not None else 0.0
|
||||
else:
|
||||
# Berechne aus Messungen
|
||||
st_point = self.parser.points.get(station.name)
|
||||
if st_point:
|
||||
e = (st_point.east or 0.0) + de
|
||||
n = (st_point.north or 0.0) + dn
|
||||
elev = (st_point.elevation or 0.0) + dh
|
||||
else:
|
||||
e = de
|
||||
n = dn
|
||||
elev = dh
|
||||
|
||||
# Nur hinzufügen wenn noch nicht vorhanden oder neuere Messung
|
||||
if meas.name not in computed_coords:
|
||||
computed_coords[meas.name] = (e, n, elev)
|
||||
|
||||
def _compute_free_station(self, station_id: str, station: Station,
|
||||
computed_coords: Dict):
|
||||
"""Berechnet Punkte für eine freie Stationierung"""
|
||||
|
||||
# Bei freier Stationierung: Station wurde bereits berechnet
|
||||
# Wir verwenden die Koordinaten aus dem Parser
|
||||
|
||||
measurements = self.parser.get_measurements_from_station(station_id)
|
||||
|
||||
# Stationskoordinaten hinzufügen falls vorhanden
|
||||
if station.name in self.parser.points:
|
||||
st_point = self.parser.points[station.name]
|
||||
if st_point.east is not None and st_point.north is not None:
|
||||
computed_coords[station.name] = (
|
||||
st_point.east,
|
||||
st_point.north,
|
||||
st_point.elevation or 0.0
|
||||
)
|
||||
|
||||
# Punkte aus Messungen
|
||||
for meas in measurements:
|
||||
if meas.name and meas.north is not None and meas.east is not None:
|
||||
if meas.name not in computed_coords:
|
||||
computed_coords[meas.name] = (
|
||||
meas.east,
|
||||
meas.north,
|
||||
meas.elevation or 0.0
|
||||
)
|
||||
|
||||
def generate_from_computed_grid(self) -> List[CORPoint]:
|
||||
"""
|
||||
Generiert COR-Punkte direkt aus den ComputedGrid-Koordinaten der JXL-Datei
|
||||
"""
|
||||
self.cor_points = []
|
||||
seen_names = set()
|
||||
|
||||
# Referenzlinie finden für Header
|
||||
ref_line = self.parser.get_reference_line()
|
||||
|
||||
# Sortiere Stationen nach Zeitstempel
|
||||
stations_sorted = sorted(self.parser.stations.items(),
|
||||
key=lambda x: x[1].timestamp)
|
||||
|
||||
current_station_header = None
|
||||
|
||||
for station_id, station in stations_sorted:
|
||||
# Neuer Stationsheader wenn sich die Station ändert
|
||||
if station.station_type == 'ReflineStationSetup':
|
||||
if ref_line:
|
||||
# Header für Referenzlinie-Station
|
||||
header_point = CORPoint(
|
||||
name=ref_line.start_point,
|
||||
x=0.0, y=0.0, z=0.0
|
||||
)
|
||||
if ref_line.start_point not in seen_names:
|
||||
self.cor_points.append(header_point)
|
||||
seen_names.add(ref_line.start_point)
|
||||
|
||||
# Punkte von dieser Station
|
||||
measurements = self.parser.get_measurements_from_station(station_id)
|
||||
|
||||
for meas in measurements:
|
||||
if meas.name and meas.name not in seen_names:
|
||||
if meas.north is not None and meas.east is not None:
|
||||
self.cor_points.append(CORPoint(
|
||||
name=meas.name,
|
||||
x=meas.east,
|
||||
y=meas.north,
|
||||
z=meas.elevation or 0.0
|
||||
))
|
||||
seen_names.add(meas.name)
|
||||
|
||||
# Alle verbleibenden Punkte hinzufügen
|
||||
for name, point in self.parser.get_active_points().items():
|
||||
if name not in seen_names and point.east is not None and point.north is not None:
|
||||
self.cor_points.append(CORPoint(
|
||||
name=name,
|
||||
x=point.east,
|
||||
y=point.north,
|
||||
z=point.elevation or 0.0
|
||||
))
|
||||
seen_names.add(name)
|
||||
|
||||
return self.cor_points
|
||||
|
||||
def write_cor_file(self, output_path: str, include_header: bool = True) -> str:
|
||||
"""Schreibt die COR-Datei"""
|
||||
lines = []
|
||||
|
||||
# Sammle eindeutige Stationsstarts für Header
|
||||
ref_line = self.parser.get_reference_line()
|
||||
stations_sorted = sorted(self.parser.stations.items(),
|
||||
key=lambda x: x[1].timestamp)
|
||||
|
||||
current_station_idx = 0
|
||||
written_points = set()
|
||||
|
||||
for station_id, station in stations_sorted:
|
||||
# Header für neue Station (Referenzlinie)
|
||||
if station.station_type == 'ReflineStationSetup' and ref_line:
|
||||
if include_header:
|
||||
# Markdown-Style Header
|
||||
lines.append(f"|{ref_line.start_point} |0.000 |0.000.1 |0.000.2 |")
|
||||
lines.append("|----:|----:|----:|----:|")
|
||||
|
||||
# Punkte von dieser Station
|
||||
measurements = self.parser.get_measurements_from_station(station_id)
|
||||
for meas in measurements:
|
||||
if meas.name and meas.name not in written_points:
|
||||
# Finde den COR-Punkt
|
||||
for cp in self.cor_points:
|
||||
if cp.name == meas.name:
|
||||
lines.append(cp.to_cor_line())
|
||||
written_points.add(meas.name)
|
||||
break
|
||||
|
||||
# Verbleibende Punkte
|
||||
for cp in self.cor_points:
|
||||
if cp.name not in written_points:
|
||||
lines.append(cp.to_cor_line())
|
||||
written_points.add(cp.name)
|
||||
|
||||
content = "\n".join(lines)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return content
|
||||
|
||||
def export_csv(self, output_path: str) -> str:
|
||||
"""Exportiert als CSV-Datei"""
|
||||
lines = ["Punktname;East;North;Elevation"]
|
||||
|
||||
for cp in self.cor_points:
|
||||
lines.append(f"{cp.name};{cp.x:.4f};{cp.y:.4f};{cp.z:.4f}")
|
||||
|
||||
content = "\n".join(lines)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return content
|
||||
|
||||
def export_txt(self, output_path: str, delimiter: str = "\t") -> str:
|
||||
"""Exportiert als TXT-Datei"""
|
||||
lines = []
|
||||
|
||||
for cp in self.cor_points:
|
||||
lines.append(f"{cp.name}{delimiter}{cp.x:.4f}{delimiter}{cp.y:.4f}{delimiter}{cp.z:.4f}")
|
||||
|
||||
content = "\n".join(lines)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return content
|
||||
|
||||
def export_dxf(self, output_path: str) -> str:
|
||||
"""Exportiert als DXF-Datei (einfaches Format)"""
|
||||
lines = []
|
||||
|
||||
# DXF Header
|
||||
lines.extend([
|
||||
"0", "SECTION",
|
||||
"2", "ENTITIES"
|
||||
])
|
||||
|
||||
# Punkte als POINT und TEXT
|
||||
for cp in self.cor_points:
|
||||
# POINT entity
|
||||
lines.extend([
|
||||
"0", "POINT",
|
||||
"8", "POINTS", # Layer
|
||||
"10", f"{cp.x:.4f}", # X
|
||||
"20", f"{cp.y:.4f}", # Y
|
||||
"30", f"{cp.z:.4f}" # Z
|
||||
])
|
||||
|
||||
# TEXT entity für Punktname
|
||||
lines.extend([
|
||||
"0", "TEXT",
|
||||
"8", "NAMES", # Layer
|
||||
"10", f"{cp.x + 0.5:.4f}", # X offset
|
||||
"20", f"{cp.y + 0.5:.4f}", # Y offset
|
||||
"30", f"{cp.z:.4f}", # Z
|
||||
"40", "0.5", # Texthöhe
|
||||
"1", cp.name # Text
|
||||
])
|
||||
|
||||
# DXF Footer
|
||||
lines.extend([
|
||||
"0", "ENDSEC",
|
||||
"0", "EOF"
|
||||
])
|
||||
|
||||
content = "\n".join(lines)
|
||||
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
return content
|
||||
|
||||
def get_statistics(self) -> str:
|
||||
"""Gibt Statistiken über die generierten Punkte zurück"""
|
||||
if not self.cor_points:
|
||||
return "Keine Punkte generiert."
|
||||
|
||||
x_vals = [p.x for p in self.cor_points]
|
||||
y_vals = [p.y for p in self.cor_points]
|
||||
z_vals = [p.z for p in self.cor_points]
|
||||
|
||||
stats = []
|
||||
stats.append(f"Anzahl Punkte: {len(self.cor_points)}")
|
||||
stats.append(f"")
|
||||
stats.append(f"X (East):")
|
||||
stats.append(f" Min: {min(x_vals):.3f} m")
|
||||
stats.append(f" Max: {max(x_vals):.3f} m")
|
||||
stats.append(f" Spanne: {max(x_vals) - min(x_vals):.3f} m")
|
||||
stats.append(f"")
|
||||
stats.append(f"Y (North):")
|
||||
stats.append(f" Min: {min(y_vals):.3f} m")
|
||||
stats.append(f" Max: {max(y_vals):.3f} m")
|
||||
stats.append(f" Spanne: {max(y_vals) - min(y_vals):.3f} m")
|
||||
stats.append(f"")
|
||||
stats.append(f"Z (Elevation):")
|
||||
stats.append(f" Min: {min(z_vals):.3f} m")
|
||||
stats.append(f" Max: {max(z_vals):.3f} m")
|
||||
stats.append(f" Spanne: {max(z_vals) - min(z_vals):.3f} m")
|
||||
|
||||
return "\n".join(stats)
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
"""
|
||||
Georeferenzierungs-Modul
|
||||
Transformation mit Passpunkten (mind. 3 Punkte)
|
||||
NUR Rotation und Translation - KEINE Maßstabsänderung
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
from .cor_generator import CORPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class ControlPoint:
|
||||
"""Passpunkt mit Soll- und Ist-Koordinaten"""
|
||||
name: str
|
||||
# Ist-Koordinaten (lokales System)
|
||||
local_x: float
|
||||
local_y: float
|
||||
local_z: float
|
||||
# Soll-Koordinaten (Zielsystem, z.B. UTM)
|
||||
target_x: float
|
||||
target_y: float
|
||||
target_z: float
|
||||
# Residuen nach Transformation
|
||||
residual_x: float = 0.0
|
||||
residual_y: float = 0.0
|
||||
residual_z: float = 0.0
|
||||
|
||||
@property
|
||||
def residual_2d(self) -> float:
|
||||
"""2D-Residuum"""
|
||||
return math.sqrt(self.residual_x**2 + self.residual_y**2)
|
||||
|
||||
@property
|
||||
def residual_3d(self) -> float:
|
||||
"""3D-Residuum"""
|
||||
return math.sqrt(self.residual_x**2 + self.residual_y**2 + self.residual_z**2)
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeoreferenceResult:
|
||||
"""Ergebnis der Georeferenzierung"""
|
||||
# Transformationsparameter
|
||||
translation_x: float = 0.0
|
||||
translation_y: float = 0.0
|
||||
translation_z: float = 0.0
|
||||
rotation_rad: float = 0.0
|
||||
|
||||
# Qualitätsparameter
|
||||
rmse_x: float = 0.0
|
||||
rmse_y: float = 0.0
|
||||
rmse_z: float = 0.0
|
||||
rmse_2d: float = 0.0
|
||||
rmse_3d: float = 0.0
|
||||
max_residual_2d: float = 0.0
|
||||
max_residual_3d: float = 0.0
|
||||
|
||||
# Passpunkte mit Residuen
|
||||
control_points: List[ControlPoint] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.control_points is None:
|
||||
self.control_points = []
|
||||
|
||||
@property
|
||||
def rotation_gon(self) -> float:
|
||||
"""Rotation in Gon"""
|
||||
return self.rotation_rad * 200.0 / math.pi
|
||||
|
||||
@property
|
||||
def rotation_deg(self) -> float:
|
||||
"""Rotation in Grad"""
|
||||
return math.degrees(self.rotation_rad)
|
||||
|
||||
|
||||
class Georeferencer:
|
||||
"""
|
||||
Georeferenzierung mit mindestens 3 Passpunkten
|
||||
Verwendet Methode der kleinsten Quadrate für Überbestimmung
|
||||
NUR Rotation und Translation (4 Parameter)
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.control_points: List[ControlPoint] = []
|
||||
self.result: Optional[GeoreferenceResult] = None
|
||||
self.points_to_transform: List[CORPoint] = []
|
||||
self.transformed_points: List[CORPoint] = []
|
||||
|
||||
def add_control_point(self, name: str,
|
||||
local_x: float, local_y: float, local_z: float,
|
||||
target_x: float, target_y: float, target_z: float):
|
||||
"""Fügt einen Passpunkt hinzu"""
|
||||
self.control_points.append(ControlPoint(
|
||||
name=name,
|
||||
local_x=local_x, local_y=local_y, local_z=local_z,
|
||||
target_x=target_x, target_y=target_y, target_z=target_z
|
||||
))
|
||||
|
||||
def clear_control_points(self):
|
||||
"""Löscht alle Passpunkte"""
|
||||
self.control_points = []
|
||||
self.result = None
|
||||
|
||||
def set_points_to_transform(self, points: List[CORPoint]):
|
||||
"""Setzt die zu transformierenden Punkte"""
|
||||
self.points_to_transform = points.copy()
|
||||
|
||||
def compute_transformation(self) -> GeoreferenceResult:
|
||||
"""
|
||||
Berechnet die Transformationsparameter
|
||||
4-Parameter-Transformation: Rotation + Translation (kein Maßstab)
|
||||
Methode der kleinsten Quadrate
|
||||
"""
|
||||
if len(self.control_points) < 3:
|
||||
raise ValueError("Mindestens 3 Passpunkte erforderlich!")
|
||||
|
||||
n = len(self.control_points)
|
||||
|
||||
# Koordinaten extrahieren
|
||||
local_coords = np.array([[cp.local_x, cp.local_y] for cp in self.control_points])
|
||||
target_coords = np.array([[cp.target_x, cp.target_y] for cp in self.control_points])
|
||||
|
||||
# Schwerpunkte berechnen
|
||||
local_centroid = np.mean(local_coords, axis=0)
|
||||
target_centroid = np.mean(target_coords, axis=0)
|
||||
|
||||
# Zum Schwerpunkt verschieben
|
||||
local_centered = local_coords - local_centroid
|
||||
target_centered = target_coords - target_centroid
|
||||
|
||||
# Rotation berechnen mit SVD (Procrustes ohne Skalierung)
|
||||
H = local_centered.T @ target_centered
|
||||
U, S, Vt = np.linalg.svd(H)
|
||||
|
||||
# Rotationsmatrix
|
||||
R = Vt.T @ U.T
|
||||
|
||||
# Reflexion korrigieren falls nötig
|
||||
if np.linalg.det(R) < 0:
|
||||
Vt[-1, :] *= -1
|
||||
R = Vt.T @ U.T
|
||||
|
||||
# Rotationswinkel aus Matrix
|
||||
rotation_rad = math.atan2(R[1, 0], R[0, 0])
|
||||
|
||||
# Translation berechnen
|
||||
translation = target_centroid - R @ local_centroid
|
||||
|
||||
# Ergebnis speichern
|
||||
self.result = GeoreferenceResult(
|
||||
translation_x=translation[0],
|
||||
translation_y=translation[1],
|
||||
rotation_rad=rotation_rad
|
||||
)
|
||||
|
||||
# Z-Translation (Mittelwert der Höhendifferenzen)
|
||||
z_diffs = [cp.target_z - cp.local_z for cp in self.control_points]
|
||||
self.result.translation_z = np.mean(z_diffs)
|
||||
|
||||
# Residuen berechnen
|
||||
self._compute_residuals()
|
||||
|
||||
return self.result
|
||||
|
||||
def _compute_residuals(self):
|
||||
"""Berechnet Residuen für alle Passpunkte"""
|
||||
if self.result is None:
|
||||
return
|
||||
|
||||
cos_r = math.cos(self.result.rotation_rad)
|
||||
sin_r = math.sin(self.result.rotation_rad)
|
||||
|
||||
sum_res_x2 = 0.0
|
||||
sum_res_y2 = 0.0
|
||||
sum_res_z2 = 0.0
|
||||
max_res_2d = 0.0
|
||||
max_res_3d = 0.0
|
||||
|
||||
for cp in self.control_points:
|
||||
# Transformation anwenden
|
||||
x_trans = cos_r * cp.local_x - sin_r * cp.local_y + self.result.translation_x
|
||||
y_trans = sin_r * cp.local_x + cos_r * cp.local_y + self.result.translation_y
|
||||
z_trans = cp.local_z + self.result.translation_z
|
||||
|
||||
# Residuen
|
||||
cp.residual_x = cp.target_x - x_trans
|
||||
cp.residual_y = cp.target_y - y_trans
|
||||
cp.residual_z = cp.target_z - z_trans
|
||||
|
||||
sum_res_x2 += cp.residual_x**2
|
||||
sum_res_y2 += cp.residual_y**2
|
||||
sum_res_z2 += cp.residual_z**2
|
||||
|
||||
max_res_2d = max(max_res_2d, cp.residual_2d)
|
||||
max_res_3d = max(max_res_3d, cp.residual_3d)
|
||||
|
||||
n = len(self.control_points)
|
||||
self.result.rmse_x = math.sqrt(sum_res_x2 / n)
|
||||
self.result.rmse_y = math.sqrt(sum_res_y2 / n)
|
||||
self.result.rmse_z = math.sqrt(sum_res_z2 / n)
|
||||
self.result.rmse_2d = math.sqrt((sum_res_x2 + sum_res_y2) / n)
|
||||
self.result.rmse_3d = math.sqrt((sum_res_x2 + sum_res_y2 + sum_res_z2) / n)
|
||||
self.result.max_residual_2d = max_res_2d
|
||||
self.result.max_residual_3d = max_res_3d
|
||||
self.result.control_points = self.control_points
|
||||
|
||||
def transform_points(self) -> List[CORPoint]:
|
||||
"""Transformiert alle Punkte"""
|
||||
if self.result is None:
|
||||
raise ValueError("Transformation noch nicht berechnet!")
|
||||
|
||||
self.transformed_points = []
|
||||
|
||||
cos_r = math.cos(self.result.rotation_rad)
|
||||
sin_r = math.sin(self.result.rotation_rad)
|
||||
|
||||
for p in self.points_to_transform:
|
||||
x_trans = cos_r * p.x - sin_r * p.y + self.result.translation_x
|
||||
y_trans = sin_r * p.x + cos_r * p.y + self.result.translation_y
|
||||
z_trans = p.z + self.result.translation_z
|
||||
|
||||
self.transformed_points.append(CORPoint(
|
||||
name=p.name,
|
||||
x=x_trans,
|
||||
y=y_trans,
|
||||
z=z_trans
|
||||
))
|
||||
|
||||
return self.transformed_points
|
||||
|
||||
def transform_single_point(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
||||
"""Transformiert einen einzelnen Punkt"""
|
||||
if self.result is None:
|
||||
raise ValueError("Transformation noch nicht berechnet!")
|
||||
|
||||
cos_r = math.cos(self.result.rotation_rad)
|
||||
sin_r = math.sin(self.result.rotation_rad)
|
||||
|
||||
x_trans = cos_r * x - sin_r * y + self.result.translation_x
|
||||
y_trans = sin_r * x + cos_r * y + self.result.translation_y
|
||||
z_trans = z + self.result.translation_z
|
||||
|
||||
return x_trans, y_trans, z_trans
|
||||
|
||||
def get_transformation_report(self) -> str:
|
||||
"""Erstellt einen detaillierten Transformationsbericht"""
|
||||
if self.result is None:
|
||||
return "Keine Transformation berechnet."
|
||||
|
||||
lines = []
|
||||
lines.append("=" * 70)
|
||||
lines.append("GEOREFERENZIERUNG - TRANSFORMATIONSBERICHT")
|
||||
lines.append("=" * 70)
|
||||
lines.append("")
|
||||
lines.append("TRANSFORMATIONSPARAMETER (4-Parameter, ohne Maßstab)")
|
||||
lines.append("-" * 70)
|
||||
lines.append(f" Translation X: {self.result.translation_x:>15.4f} m")
|
||||
lines.append(f" Translation Y: {self.result.translation_y:>15.4f} m")
|
||||
lines.append(f" Translation Z: {self.result.translation_z:>15.4f} m")
|
||||
lines.append(f" Rotation: {self.result.rotation_gon:>15.6f} gon")
|
||||
lines.append(f" {self.result.rotation_deg:>15.6f}°")
|
||||
lines.append(f" {self.result.rotation_rad:>15.8f} rad")
|
||||
lines.append(f" Maßstab: {1.0:>15.6f} (fest)")
|
||||
lines.append("")
|
||||
lines.append("QUALITÄTSPARAMETER")
|
||||
lines.append("-" * 70)
|
||||
lines.append(f" RMSE X: {self.result.rmse_x*1000:>15.2f} mm")
|
||||
lines.append(f" RMSE Y: {self.result.rmse_y*1000:>15.2f} mm")
|
||||
lines.append(f" RMSE Z: {self.result.rmse_z*1000:>15.2f} mm")
|
||||
lines.append(f" RMSE 2D: {self.result.rmse_2d*1000:>15.2f} mm")
|
||||
lines.append(f" RMSE 3D: {self.result.rmse_3d*1000:>15.2f} mm")
|
||||
lines.append(f" Max. Residuum 2D: {self.result.max_residual_2d*1000:>15.2f} mm")
|
||||
lines.append(f" Max. Residuum 3D: {self.result.max_residual_3d*1000:>15.2f} mm")
|
||||
lines.append("")
|
||||
lines.append("PASSPUNKTE UND RESIDUEN")
|
||||
lines.append("-" * 70)
|
||||
lines.append(f"{'Punkt':<10} {'vX [mm]':>10} {'vY [mm]':>10} {'vZ [mm]':>10} {'v2D [mm]':>10} {'v3D [mm]':>10}")
|
||||
lines.append("-" * 70)
|
||||
|
||||
for cp in self.result.control_points:
|
||||
lines.append(f"{cp.name:<10} {cp.residual_x*1000:>10.2f} {cp.residual_y*1000:>10.2f} "
|
||||
f"{cp.residual_z*1000:>10.2f} {cp.residual_2d*1000:>10.2f} {cp.residual_3d*1000:>10.2f}")
|
||||
|
||||
lines.append("-" * 70)
|
||||
lines.append(f"Anzahl Passpunkte: {len(self.control_points)}")
|
||||
lines.append(f"Redundanz (2D): {2 * len(self.control_points) - 4}")
|
||||
lines.append("")
|
||||
lines.append("=" * 70)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def get_control_points_comparison(self) -> str:
|
||||
"""Erstellt eine Vergleichstabelle für Passpunkte"""
|
||||
if self.result is None:
|
||||
return "Keine Transformation berechnet."
|
||||
|
||||
lines = []
|
||||
lines.append("PASSPUNKT-KOORDINATEN: LOKAL → ZIEL")
|
||||
lines.append("=" * 100)
|
||||
lines.append(f"{'Punkt':<8} {'X_lokal':>12} {'Y_lokal':>12} {'Z_lokal':>10} | "
|
||||
f"{'X_Ziel':>12} {'Y_Ziel':>12} {'Z_Ziel':>10}")
|
||||
lines.append("-" * 100)
|
||||
|
||||
for cp in self.control_points:
|
||||
lines.append(f"{cp.name:<8} {cp.local_x:>12.3f} {cp.local_y:>12.3f} {cp.local_z:>10.3f} | "
|
||||
f"{cp.target_x:>12.3f} {cp.target_y:>12.3f} {cp.target_z:>10.3f}")
|
||||
|
||||
lines.append("=" * 100)
|
||||
return "\n".join(lines)
|
||||
|
||||
def export_report(self, filepath: str):
|
||||
"""Exportiert den vollständigen Bericht"""
|
||||
report = self.get_transformation_report()
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
|
||||
|
||||
class RigidBodyTransformation:
|
||||
"""
|
||||
Alternative Implementierung: Rigide Körpertransformation
|
||||
6 Parameter: 3 Translationen + 3 Rotationen
|
||||
Für 3D-Daten mit mehreren Rotationsachsen
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.control_points: List[ControlPoint] = []
|
||||
self.translation = np.zeros(3)
|
||||
self.rotation_matrix = np.eye(3)
|
||||
|
||||
def compute(self, local_points: np.ndarray, target_points: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
Berechnet rigide Transformation mit Kabsch-Algorithmus
|
||||
Keine Maßstabsänderung
|
||||
"""
|
||||
# Schwerpunkte
|
||||
centroid_local = np.mean(local_points, axis=0)
|
||||
centroid_target = np.mean(target_points, axis=0)
|
||||
|
||||
# Zentrieren
|
||||
local_centered = local_points - centroid_local
|
||||
target_centered = target_points - centroid_target
|
||||
|
||||
# Kovarianzmatrix
|
||||
H = local_centered.T @ target_centered
|
||||
|
||||
# SVD
|
||||
U, S, Vt = np.linalg.svd(H)
|
||||
|
||||
# Rotation
|
||||
R = Vt.T @ U.T
|
||||
|
||||
# Reflexionskorrektur
|
||||
if np.linalg.det(R) < 0:
|
||||
Vt[-1, :] *= -1
|
||||
R = Vt.T @ U.T
|
||||
|
||||
# Translation
|
||||
t = centroid_target - R @ centroid_local
|
||||
|
||||
self.rotation_matrix = R
|
||||
self.translation = t
|
||||
|
||||
return R, t
|
||||
|
||||
def transform(self, points: np.ndarray) -> np.ndarray:
|
||||
"""Transformiert Punkte"""
|
||||
return (self.rotation_matrix @ points.T).T + self.translation
|
||||
|
|
@ -0,0 +1,616 @@
|
|||
"""
|
||||
JXL Parser Module - Trimble JXL-Datei Parser
|
||||
Liest und analysiert Trimble JXL-Dateien (XML-basiert)
|
||||
"""
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Dict, Optional, Tuple
|
||||
import math
|
||||
import copy
|
||||
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""Repräsentiert einen vermessenen Punkt"""
|
||||
name: str
|
||||
code: str = ""
|
||||
method: str = ""
|
||||
survey_method: str = ""
|
||||
classification: str = ""
|
||||
deleted: bool = False
|
||||
|
||||
# Grid-Koordinaten
|
||||
north: Optional[float] = None
|
||||
east: Optional[float] = None
|
||||
elevation: Optional[float] = None
|
||||
|
||||
# Kreis-Messungen (Rohwerte)
|
||||
horizontal_circle: Optional[float] = None # in Gon
|
||||
vertical_circle: Optional[float] = None # in Gon
|
||||
edm_distance: Optional[float] = None # in Meter
|
||||
face: str = "Face1"
|
||||
|
||||
# Standardabweichungen
|
||||
hz_std_error: Optional[float] = None
|
||||
vz_std_error: Optional[float] = None
|
||||
dist_std_error: Optional[float] = None
|
||||
|
||||
# Referenzen
|
||||
station_id: str = ""
|
||||
target_id: str = ""
|
||||
backsight_id: str = ""
|
||||
|
||||
# Zusätzliche Infos
|
||||
timestamp: str = ""
|
||||
record_id: str = ""
|
||||
pressure: Optional[float] = None
|
||||
temperature: Optional[float] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class Station:
|
||||
"""Repräsentiert eine Instrumentenstation"""
|
||||
name: str
|
||||
theodolite_height: float = 0.0
|
||||
station_type: str = "" # ReflineStationSetup, StandardResection, etc.
|
||||
instrument_id: str = ""
|
||||
atmosphere_id: str = ""
|
||||
scale_factor: float = 1.0
|
||||
|
||||
# Berechnete Koordinaten
|
||||
north: Optional[float] = None
|
||||
east: Optional[float] = None
|
||||
elevation: Optional[float] = None
|
||||
|
||||
# Orientierung
|
||||
orientation_correction: Optional[float] = None
|
||||
|
||||
record_id: str = ""
|
||||
timestamp: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Target:
|
||||
"""Repräsentiert ein Ziel/Prisma"""
|
||||
prism_type: str = ""
|
||||
prism_constant: float = 0.0
|
||||
target_height: float = 0.0
|
||||
record_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class BackBearing:
|
||||
"""Repräsentiert einen Rückwärtseinschnitt"""
|
||||
station: str = ""
|
||||
backsight: str = ""
|
||||
face1_hz: Optional[float] = None
|
||||
face2_hz: Optional[float] = None
|
||||
orientation_correction: Optional[float] = None
|
||||
record_id: str = ""
|
||||
station_record_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Instrument:
|
||||
"""Repräsentiert ein Vermessungsinstrument"""
|
||||
instrument_type: str = ""
|
||||
model: str = ""
|
||||
serial: str = ""
|
||||
edm_precision: float = 0.001
|
||||
edm_ppm: float = 1.5
|
||||
hz_precision: float = 0.0
|
||||
vz_precision: float = 0.0
|
||||
edm_constant: float = 0.0
|
||||
record_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Atmosphere:
|
||||
"""Atmosphärische Bedingungen"""
|
||||
pressure: float = 1013.25
|
||||
temperature: float = 20.0
|
||||
ppm: float = 0.0
|
||||
refraction_coefficient: float = 0.13
|
||||
record_id: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Line:
|
||||
"""Repräsentiert eine Referenzlinie"""
|
||||
name: str = ""
|
||||
start_point: str = ""
|
||||
end_point: str = ""
|
||||
start_station: float = 0.0
|
||||
|
||||
|
||||
class JXLParser:
|
||||
"""Parser für Trimble JXL-Dateien"""
|
||||
|
||||
def __init__(self):
|
||||
self.job_name: str = ""
|
||||
self.coordinate_system: str = ""
|
||||
self.zone_name: str = ""
|
||||
self.datum_name: str = ""
|
||||
self.angle_units: str = "Gons" # Gons oder Degrees
|
||||
self.distance_units: str = "Metres"
|
||||
|
||||
self.points: Dict[str, Point] = {}
|
||||
self.stations: Dict[str, Station] = {}
|
||||
self.targets: Dict[str, Target] = {}
|
||||
self.backbearings: Dict[str, BackBearing] = {}
|
||||
self.instruments: Dict[str, Instrument] = {}
|
||||
self.atmospheres: Dict[str, Atmosphere] = {}
|
||||
self.lines: Dict[str, Line] = {}
|
||||
|
||||
# Alle Messungen (auch gelöschte)
|
||||
self.all_point_records: List[Point] = []
|
||||
|
||||
self.raw_xml = None
|
||||
self.file_path: str = ""
|
||||
|
||||
def parse(self, file_path: str) -> bool:
|
||||
"""Parst eine JXL-Datei"""
|
||||
self.file_path = file_path
|
||||
try:
|
||||
tree = ET.parse(file_path)
|
||||
self.raw_xml = tree
|
||||
root = tree.getroot()
|
||||
|
||||
# Job-Informationen
|
||||
self.job_name = root.get('jobName', '')
|
||||
|
||||
# FieldBook durchsuchen
|
||||
fieldbook = root.find('FieldBook')
|
||||
if fieldbook is None:
|
||||
return False
|
||||
|
||||
for element in fieldbook:
|
||||
self._parse_element(element)
|
||||
|
||||
# Stationskoordinaten aus Punkten zuweisen
|
||||
self._assign_station_coordinates()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"Fehler beim Parsen: {e}")
|
||||
return False
|
||||
|
||||
def _parse_element(self, element):
|
||||
"""Parst ein einzelnes Element"""
|
||||
tag = element.tag
|
||||
record_id = element.get('ID', '')
|
||||
timestamp = element.get('TimeStamp', '')
|
||||
|
||||
if tag == 'CoordinateSystemRecord':
|
||||
self._parse_coordinate_system(element)
|
||||
elif tag == 'UnitsRecord':
|
||||
self._parse_units(element)
|
||||
elif tag == 'PointRecord':
|
||||
self._parse_point(element, record_id, timestamp)
|
||||
elif tag == 'StationRecord':
|
||||
self._parse_station(element, record_id, timestamp)
|
||||
elif tag == 'TargetRecord':
|
||||
self._parse_target(element, record_id)
|
||||
elif tag == 'BackBearingRecord':
|
||||
self._parse_backbearing(element, record_id)
|
||||
elif tag == 'InstrumentRecord':
|
||||
self._parse_instrument(element, record_id)
|
||||
elif tag == 'AtmosphereRecord':
|
||||
self._parse_atmosphere(element, record_id)
|
||||
elif tag == 'LineRecord':
|
||||
self._parse_line(element)
|
||||
|
||||
def _parse_coordinate_system(self, element):
|
||||
"""Parst Koordinatensystem-Informationen"""
|
||||
system = element.find('SystemName')
|
||||
zone = element.find('ZoneName')
|
||||
datum = element.find('DatumName')
|
||||
|
||||
if system is not None and system.text:
|
||||
self.coordinate_system = system.text
|
||||
if zone is not None and zone.text:
|
||||
self.zone_name = zone.text
|
||||
if datum is not None and datum.text:
|
||||
self.datum_name = datum.text
|
||||
|
||||
def _parse_units(self, element):
|
||||
"""Parst Einheiten"""
|
||||
angle = element.find('AngleUnits')
|
||||
distance = element.find('DistanceUnits')
|
||||
|
||||
if angle is not None and angle.text:
|
||||
self.angle_units = angle.text
|
||||
if distance is not None and distance.text:
|
||||
self.distance_units = distance.text
|
||||
|
||||
def _parse_point(self, element, record_id: str, timestamp: str):
|
||||
"""Parst einen Punkt"""
|
||||
point = Point(name="")
|
||||
point.record_id = record_id
|
||||
point.timestamp = timestamp
|
||||
|
||||
# Basisdaten
|
||||
name_elem = element.find('Name')
|
||||
point.name = name_elem.text if name_elem is not None and name_elem.text else ""
|
||||
|
||||
code_elem = element.find('Code')
|
||||
point.code = code_elem.text if code_elem is not None and code_elem.text else ""
|
||||
|
||||
method_elem = element.find('Method')
|
||||
point.method = method_elem.text if method_elem is not None and method_elem.text else ""
|
||||
|
||||
survey_elem = element.find('SurveyMethod')
|
||||
point.survey_method = survey_elem.text if survey_elem is not None and survey_elem.text else ""
|
||||
|
||||
class_elem = element.find('Classification')
|
||||
point.classification = class_elem.text if class_elem is not None and class_elem.text else ""
|
||||
|
||||
deleted_elem = element.find('Deleted')
|
||||
point.deleted = deleted_elem is not None and deleted_elem.text == 'true'
|
||||
|
||||
# Station und Target IDs
|
||||
station_id_elem = element.find('StationID')
|
||||
point.station_id = station_id_elem.text if station_id_elem is not None and station_id_elem.text else ""
|
||||
|
||||
target_id_elem = element.find('TargetID')
|
||||
point.target_id = target_id_elem.text if target_id_elem is not None and target_id_elem.text else ""
|
||||
|
||||
backsight_id_elem = element.find('BackBearingID')
|
||||
point.backsight_id = backsight_id_elem.text if backsight_id_elem is not None and backsight_id_elem.text else ""
|
||||
|
||||
# Grid-Koordinaten
|
||||
grid = element.find('Grid')
|
||||
if grid is not None:
|
||||
north = grid.find('North')
|
||||
east = grid.find('East')
|
||||
elev = grid.find('Elevation')
|
||||
|
||||
if north is not None and north.text:
|
||||
try:
|
||||
point.north = float(north.text)
|
||||
except ValueError:
|
||||
pass
|
||||
if east is not None and east.text:
|
||||
try:
|
||||
point.east = float(east.text)
|
||||
except ValueError:
|
||||
pass
|
||||
if elev is not None and elev.text:
|
||||
try:
|
||||
point.elevation = float(elev.text)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# ComputedGrid-Koordinaten (falls vorhanden)
|
||||
computed_grid = element.find('ComputedGrid')
|
||||
if computed_grid is not None:
|
||||
north = computed_grid.find('North')
|
||||
east = computed_grid.find('East')
|
||||
elev = computed_grid.find('Elevation')
|
||||
|
||||
if north is not None and north.text:
|
||||
try:
|
||||
point.north = float(north.text)
|
||||
except ValueError:
|
||||
pass
|
||||
if east is not None and east.text:
|
||||
try:
|
||||
point.east = float(east.text)
|
||||
except ValueError:
|
||||
pass
|
||||
if elev is not None and elev.text:
|
||||
try:
|
||||
point.elevation = float(elev.text)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Kreis-Messungen
|
||||
circle = element.find('Circle')
|
||||
if circle is not None:
|
||||
hz = circle.find('HorizontalCircle')
|
||||
vz = circle.find('VerticalCircle')
|
||||
dist = circle.find('EDMDistance')
|
||||
face = circle.find('Face')
|
||||
|
||||
if hz is not None and hz.text:
|
||||
point.horizontal_circle = float(hz.text)
|
||||
if vz is not None and vz.text:
|
||||
point.vertical_circle = float(vz.text)
|
||||
if dist is not None and dist.text:
|
||||
point.edm_distance = float(dist.text)
|
||||
if face is not None and face.text:
|
||||
point.face = face.text
|
||||
|
||||
# Standardabweichungen
|
||||
hz_std = circle.find('HorizontalCircleStandardError')
|
||||
vz_std = circle.find('VerticalCircleStandardError')
|
||||
dist_std = circle.find('EDMDistanceStandardError')
|
||||
|
||||
if hz_std is not None and hz_std.text:
|
||||
point.hz_std_error = float(hz_std.text)
|
||||
if vz_std is not None and vz_std.text:
|
||||
point.vz_std_error = float(vz_std.text)
|
||||
if dist_std is not None and dist_std.text:
|
||||
point.dist_std_error = float(dist_std.text)
|
||||
|
||||
# Atmosphäre
|
||||
pressure_elem = element.find('Pressure')
|
||||
temp_elem = element.find('Temperature')
|
||||
|
||||
if pressure_elem is not None and pressure_elem.text:
|
||||
point.pressure = float(pressure_elem.text)
|
||||
if temp_elem is not None and temp_elem.text:
|
||||
point.temperature = float(temp_elem.text)
|
||||
|
||||
# Zur Liste aller Punkt-Records hinzufügen
|
||||
self.all_point_records.append(point)
|
||||
|
||||
# Nur nicht-gelöschte Punkte zum Dictionary hinzufügen
|
||||
# Überschreibe vorhandene Punkte (neuester Wert)
|
||||
if not point.deleted and point.name:
|
||||
self.points[point.name] = point
|
||||
|
||||
def _parse_station(self, element, record_id: str, timestamp: str):
|
||||
"""Parst eine Station"""
|
||||
name_elem = element.find('StationName')
|
||||
name = name_elem.text if name_elem is not None and name_elem.text else ""
|
||||
|
||||
station = Station(name=name)
|
||||
station.record_id = record_id
|
||||
station.timestamp = timestamp
|
||||
|
||||
th_elem = element.find('TheodoliteHeight')
|
||||
if th_elem is not None and th_elem.text:
|
||||
station.theodolite_height = float(th_elem.text)
|
||||
|
||||
type_elem = element.find('StationType')
|
||||
if type_elem is not None and type_elem.text:
|
||||
station.station_type = type_elem.text
|
||||
|
||||
inst_elem = element.find('InstrumentID')
|
||||
if inst_elem is not None and inst_elem.text:
|
||||
station.instrument_id = inst_elem.text
|
||||
|
||||
atm_elem = element.find('AtmosphereID')
|
||||
if atm_elem is not None and atm_elem.text:
|
||||
station.atmosphere_id = atm_elem.text
|
||||
|
||||
scale_elem = element.find('ScaleFactor')
|
||||
if scale_elem is not None and scale_elem.text:
|
||||
station.scale_factor = float(scale_elem.text)
|
||||
|
||||
ori_elem = element.find('OrientationCorrection')
|
||||
if ori_elem is not None and ori_elem.text:
|
||||
station.orientation_correction = float(ori_elem.text)
|
||||
|
||||
self.stations[record_id] = station
|
||||
|
||||
def _parse_target(self, element, record_id: str):
|
||||
"""Parst ein Target/Prisma"""
|
||||
target = Target()
|
||||
target.record_id = record_id
|
||||
|
||||
type_elem = element.find('PrismType')
|
||||
if type_elem is not None and type_elem.text:
|
||||
target.prism_type = type_elem.text
|
||||
|
||||
const_elem = element.find('PrismConstant')
|
||||
if const_elem is not None and const_elem.text:
|
||||
target.prism_constant = float(const_elem.text)
|
||||
|
||||
height_elem = element.find('TargetHeight')
|
||||
if height_elem is not None and height_elem.text:
|
||||
target.target_height = float(height_elem.text)
|
||||
|
||||
self.targets[record_id] = target
|
||||
|
||||
def _parse_backbearing(self, element, record_id: str):
|
||||
"""Parst einen Rückwärtseinschnitt"""
|
||||
bb = BackBearing()
|
||||
bb.record_id = record_id
|
||||
|
||||
station_elem = element.find('Station')
|
||||
if station_elem is not None and station_elem.text:
|
||||
bb.station = station_elem.text
|
||||
|
||||
bs_elem = element.find('BackSight')
|
||||
if bs_elem is not None and bs_elem.text:
|
||||
bb.backsight = bs_elem.text
|
||||
|
||||
face1_elem = element.find('Face1HorizontalCircle')
|
||||
if face1_elem is not None and face1_elem.text:
|
||||
bb.face1_hz = float(face1_elem.text)
|
||||
|
||||
face2_elem = element.find('Face2HorizontalCircle')
|
||||
if face2_elem is not None and face2_elem.text:
|
||||
bb.face2_hz = float(face2_elem.text)
|
||||
|
||||
ori_elem = element.find('OrientationCorrection')
|
||||
if ori_elem is not None and ori_elem.text:
|
||||
bb.orientation_correction = float(ori_elem.text)
|
||||
|
||||
station_record_elem = element.find('StationRecordID')
|
||||
if station_record_elem is not None and station_record_elem.text:
|
||||
bb.station_record_id = station_record_elem.text
|
||||
|
||||
self.backbearings[record_id] = bb
|
||||
|
||||
def _parse_instrument(self, element, record_id: str):
|
||||
"""Parst ein Instrument"""
|
||||
inst = Instrument()
|
||||
inst.record_id = record_id
|
||||
|
||||
type_elem = element.find('Type')
|
||||
if type_elem is not None and type_elem.text:
|
||||
inst.instrument_type = type_elem.text
|
||||
|
||||
model_elem = element.find('Model')
|
||||
if model_elem is not None and model_elem.text:
|
||||
inst.model = model_elem.text
|
||||
|
||||
serial_elem = element.find('Serial')
|
||||
if serial_elem is not None and serial_elem.text:
|
||||
inst.serial = serial_elem.text
|
||||
|
||||
edm_prec_elem = element.find('EDMPrecision')
|
||||
if edm_prec_elem is not None and edm_prec_elem.text:
|
||||
inst.edm_precision = float(edm_prec_elem.text)
|
||||
|
||||
edm_ppm_elem = element.find('EDMppm')
|
||||
if edm_ppm_elem is not None and edm_ppm_elem.text:
|
||||
inst.edm_ppm = float(edm_ppm_elem.text)
|
||||
|
||||
hz_prec_elem = element.find('HorizontalAnglePrecision')
|
||||
if hz_prec_elem is not None and hz_prec_elem.text:
|
||||
inst.hz_precision = float(hz_prec_elem.text)
|
||||
|
||||
vz_prec_elem = element.find('VerticalAnglePrecision')
|
||||
if vz_prec_elem is not None and vz_prec_elem.text:
|
||||
inst.vz_precision = float(vz_prec_elem.text)
|
||||
|
||||
edm_const_elem = element.find('InstrumentAppliedEDMConstant')
|
||||
if edm_const_elem is not None and edm_const_elem.text:
|
||||
inst.edm_constant = float(edm_const_elem.text)
|
||||
|
||||
self.instruments[record_id] = inst
|
||||
|
||||
def _parse_atmosphere(self, element, record_id: str):
|
||||
"""Parst Atmosphäre-Daten"""
|
||||
atm = Atmosphere()
|
||||
atm.record_id = record_id
|
||||
|
||||
press_elem = element.find('Pressure')
|
||||
if press_elem is not None and press_elem.text:
|
||||
atm.pressure = float(press_elem.text)
|
||||
|
||||
temp_elem = element.find('Temperature')
|
||||
if temp_elem is not None and temp_elem.text:
|
||||
atm.temperature = float(temp_elem.text)
|
||||
|
||||
ppm_elem = element.find('PPM')
|
||||
if ppm_elem is not None and ppm_elem.text:
|
||||
atm.ppm = float(ppm_elem.text)
|
||||
|
||||
refr_elem = element.find('RefractionCoefficient')
|
||||
if refr_elem is not None and refr_elem.text:
|
||||
atm.refraction_coefficient = float(refr_elem.text)
|
||||
|
||||
self.atmospheres[record_id] = atm
|
||||
|
||||
def _parse_line(self, element):
|
||||
"""Parst eine Referenzlinie"""
|
||||
line = Line()
|
||||
|
||||
name_elem = element.find('Name')
|
||||
if name_elem is not None and name_elem.text:
|
||||
line.name = name_elem.text
|
||||
|
||||
start_elem = element.find('StartPoint')
|
||||
if start_elem is not None and start_elem.text:
|
||||
line.start_point = start_elem.text
|
||||
|
||||
end_elem = element.find('EndPoint')
|
||||
if end_elem is not None and end_elem.text:
|
||||
line.end_point = end_elem.text
|
||||
|
||||
station_elem = element.find('StartStation')
|
||||
if station_elem is not None and station_elem.text:
|
||||
line.start_station = float(station_elem.text)
|
||||
|
||||
if line.name:
|
||||
self.lines[line.name] = line
|
||||
|
||||
def _assign_station_coordinates(self):
|
||||
"""Weist den Stationen Koordinaten aus berechneten Punkten zu"""
|
||||
for station_id, station in self.stations.items():
|
||||
if station.name in self.points:
|
||||
point = self.points[station.name]
|
||||
station.north = point.north
|
||||
station.east = point.east
|
||||
station.elevation = point.elevation
|
||||
|
||||
def get_active_points(self) -> Dict[str, Point]:
|
||||
"""Gibt nur aktive (nicht gelöschte) Punkte zurück"""
|
||||
return {name: p for name, p in self.points.items() if not p.deleted}
|
||||
|
||||
def get_control_points(self) -> Dict[str, Point]:
|
||||
"""Gibt Passpunkte zurück (BackSight klassifiziert)"""
|
||||
return {name: p for name, p in self.points.items()
|
||||
if p.classification == 'BackSight' and not p.deleted}
|
||||
|
||||
def get_measurements_for_point(self, point_name: str) -> List[Point]:
|
||||
"""Gibt alle Messungen für einen Punkt zurück"""
|
||||
return [p for p in self.all_point_records if p.name == point_name]
|
||||
|
||||
def get_measurements_from_station(self, station_id: str) -> List[Point]:
|
||||
"""Gibt alle Messungen von einer Station zurück"""
|
||||
return [p for p in self.all_point_records
|
||||
if p.station_id == station_id and not p.deleted]
|
||||
|
||||
def get_prism_constants(self) -> Dict[str, float]:
|
||||
"""Gibt alle verwendeten Prismenkonstanten zurück"""
|
||||
return {tid: t.prism_constant for tid, t in self.targets.items()}
|
||||
|
||||
def modify_prism_constant(self, target_id: str, new_constant: float):
|
||||
"""Ändert die Prismenkonstante für ein Target"""
|
||||
if target_id in self.targets:
|
||||
self.targets[target_id].prism_constant = new_constant
|
||||
|
||||
def remove_point(self, point_name: str):
|
||||
"""Entfernt einen Punkt (markiert als gelöscht)"""
|
||||
if point_name in self.points:
|
||||
del self.points[point_name]
|
||||
|
||||
def get_station_list(self) -> List[Tuple[str, str]]:
|
||||
"""Gibt Liste der Stationen mit Typ zurück"""
|
||||
result = []
|
||||
for sid, station in self.stations.items():
|
||||
result.append((station.name, station.station_type))
|
||||
return result
|
||||
|
||||
def get_reference_line(self) -> Optional[Line]:
|
||||
"""Gibt die erste gefundene Referenzlinie zurück"""
|
||||
if self.lines:
|
||||
return list(self.lines.values())[0]
|
||||
return None
|
||||
|
||||
def gon_to_rad(self, gon: float) -> float:
|
||||
"""Konvertiert Gon zu Radiant"""
|
||||
return gon * math.pi / 200.0
|
||||
|
||||
def rad_to_gon(self, rad: float) -> float:
|
||||
"""Konvertiert Radiant zu Gon"""
|
||||
return rad * 200.0 / math.pi
|
||||
|
||||
def get_summary(self) -> str:
|
||||
"""Gibt eine Zusammenfassung der geladenen Daten zurück"""
|
||||
summary = []
|
||||
summary.append(f"Job: {self.job_name}")
|
||||
summary.append(f"Koordinatensystem: {self.coordinate_system}")
|
||||
summary.append(f"Zone: {self.zone_name}")
|
||||
summary.append(f"Datum: {self.datum_name}")
|
||||
summary.append(f"Winkeleinheit: {self.angle_units}")
|
||||
summary.append(f"")
|
||||
summary.append(f"Anzahl Punkte (aktiv): {len(self.get_active_points())}")
|
||||
summary.append(f"Anzahl Stationen: {len(self.stations)}")
|
||||
summary.append(f"Anzahl Messungen gesamt: {len(self.all_point_records)}")
|
||||
summary.append(f"Anzahl Targets/Prismen: {len(self.targets)}")
|
||||
summary.append(f"Anzahl Referenzlinien: {len(self.lines)}")
|
||||
|
||||
# Stationsübersicht
|
||||
summary.append(f"\nStationen:")
|
||||
for sid, station in self.stations.items():
|
||||
summary.append(f" - {station.name}: {station.station_type}")
|
||||
|
||||
# Prismenkonstanten
|
||||
summary.append(f"\nPrismenkonstanten:")
|
||||
for tid, target in self.targets.items():
|
||||
summary.append(f" - {target.prism_type}: {target.prism_constant*1000:.1f} mm")
|
||||
|
||||
return "\n".join(summary)
|
||||
|
||||
def create_copy(self):
|
||||
"""Erstellt eine tiefe Kopie des Parsers"""
|
||||
return copy.deepcopy(self)
|
||||
|
|
@ -0,0 +1,633 @@
|
|||
"""
|
||||
Netzausgleichungs-Modul
|
||||
Methode der kleinsten Quadrate für geodätische Netze
|
||||
Basiert auf Beobachtungen aus JXL-Dateien
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
from scipy import sparse
|
||||
from scipy.sparse.linalg import spsolve
|
||||
from typing import List, Dict, Tuple, Optional, Set
|
||||
from dataclasses import dataclass, field
|
||||
from .jxl_parser import JXLParser, Point, Station
|
||||
|
||||
|
||||
@dataclass
|
||||
class Observation:
|
||||
"""Repräsentiert eine geodätische Beobachtung"""
|
||||
obs_type: str # 'direction', 'distance', 'zenith', 'height_diff'
|
||||
from_station: str
|
||||
to_point: str
|
||||
value: float # Messwert (Gon, Meter, etc.)
|
||||
std_dev: float # Standardabweichung
|
||||
residual: float = 0.0 # Verbesserung
|
||||
weight: float = 1.0
|
||||
|
||||
def __post_init__(self):
|
||||
if self.std_dev > 0:
|
||||
self.weight = 1.0 / (self.std_dev ** 2)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdjustedPoint:
|
||||
"""Ausgeglichener Punkt mit Genauigkeitsangaben"""
|
||||
name: str
|
||||
x: float # East
|
||||
y: float # North
|
||||
z: float # Elevation
|
||||
|
||||
# A-priori Koordinaten
|
||||
x_approx: float = 0.0
|
||||
y_approx: float = 0.0
|
||||
z_approx: float = 0.0
|
||||
|
||||
# Korrekturen
|
||||
dx: float = 0.0
|
||||
dy: float = 0.0
|
||||
dz: float = 0.0
|
||||
|
||||
# Standardabweichungen
|
||||
std_x: float = 0.0
|
||||
std_y: float = 0.0
|
||||
std_z: float = 0.0
|
||||
std_position: float = 0.0 # 2D Punktlagegenauigkeit
|
||||
|
||||
# Konfidenzellipse
|
||||
semi_major: float = 0.0
|
||||
semi_minor: float = 0.0
|
||||
orientation: float = 0.0 # in Gon
|
||||
|
||||
# Status
|
||||
is_fixed: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdjustmentResult:
|
||||
"""Ergebnis der Netzausgleichung"""
|
||||
# Ausgeglichene Punkte
|
||||
adjusted_points: Dict[str, AdjustedPoint] = field(default_factory=dict)
|
||||
|
||||
# Beobachtungen mit Residuen
|
||||
observations: List[Observation] = field(default_factory=list)
|
||||
|
||||
# Globale Qualitätsparameter
|
||||
sigma_0_priori: float = 1.0
|
||||
sigma_0_posteriori: float = 0.0
|
||||
chi_square: float = 0.0
|
||||
redundancy: int = 0
|
||||
|
||||
# RMSE der Residuen
|
||||
rmse_directions: float = 0.0 # in mgon
|
||||
rmse_distances: float = 0.0 # in mm
|
||||
rmse_zenith: float = 0.0 # in mgon
|
||||
|
||||
# Iterationsinfo
|
||||
iterations: int = 0
|
||||
converged: bool = False
|
||||
|
||||
# Statistiken
|
||||
num_points: int = 0
|
||||
num_fixed_points: int = 0
|
||||
num_observations: int = 0
|
||||
num_unknowns: int = 0
|
||||
|
||||
|
||||
class NetworkAdjustment:
|
||||
"""
|
||||
Netzausgleichung nach der Methode der kleinsten Quadrate
|
||||
Gauß-Markov-Modell mit Beobachtungsgleichungen
|
||||
"""
|
||||
|
||||
def __init__(self, parser: JXLParser):
|
||||
self.parser = parser
|
||||
self.observations: List[Observation] = []
|
||||
self.points: Dict[str, AdjustedPoint] = {}
|
||||
self.fixed_points: Set[str] = set()
|
||||
self.result: Optional[AdjustmentResult] = None
|
||||
|
||||
# Konfiguration
|
||||
self.max_iterations: int = 20
|
||||
self.convergence_limit: float = 1e-8 # Meter
|
||||
self.sigma_0_priori: float = 1.0
|
||||
|
||||
# Standard-Genauigkeiten (falls nicht aus JXL)
|
||||
self.default_std_direction: float = 0.0003 # Gon (0.3 mgon)
|
||||
self.default_std_distance: float = 0.002 # Meter
|
||||
self.default_std_zenith: float = 0.0003 # Gon
|
||||
|
||||
def extract_observations(self):
|
||||
"""Extrahiert Beobachtungen aus JXL-Daten"""
|
||||
self.observations = []
|
||||
|
||||
for station_id, station in self.parser.stations.items():
|
||||
# Messungen von dieser Station
|
||||
measurements = self.parser.get_measurements_from_station(station_id)
|
||||
|
||||
# Backbearing für Orientierung finden
|
||||
orientation = 0.0
|
||||
for bb_id, bb in self.parser.backbearings.items():
|
||||
if bb.station_record_id == station_id:
|
||||
if bb.orientation_correction is not None:
|
||||
orientation = bb.orientation_correction
|
||||
break
|
||||
|
||||
for meas in measurements:
|
||||
if meas.name and not meas.deleted:
|
||||
# Richtung
|
||||
if meas.horizontal_circle is not None:
|
||||
std = meas.hz_std_error if meas.hz_std_error else self.default_std_direction
|
||||
azimuth = meas.horizontal_circle + orientation
|
||||
|
||||
self.observations.append(Observation(
|
||||
obs_type='direction',
|
||||
from_station=station.name,
|
||||
to_point=meas.name,
|
||||
value=azimuth,
|
||||
std_dev=std
|
||||
))
|
||||
|
||||
# Strecke
|
||||
if meas.edm_distance is not None:
|
||||
std = meas.dist_std_error if meas.dist_std_error else self.default_std_distance
|
||||
|
||||
# Prismenkonstante berücksichtigen
|
||||
prism_const = 0.0
|
||||
if meas.target_id in self.parser.targets:
|
||||
prism_const = self.parser.targets[meas.target_id].prism_constant
|
||||
|
||||
distance = meas.edm_distance + prism_const
|
||||
|
||||
self.observations.append(Observation(
|
||||
obs_type='distance',
|
||||
from_station=station.name,
|
||||
to_point=meas.name,
|
||||
value=distance,
|
||||
std_dev=std
|
||||
))
|
||||
|
||||
# Zenitwinkel
|
||||
if meas.vertical_circle is not None:
|
||||
std = meas.vz_std_error if meas.vz_std_error else self.default_std_zenith
|
||||
|
||||
self.observations.append(Observation(
|
||||
obs_type='zenith',
|
||||
from_station=station.name,
|
||||
to_point=meas.name,
|
||||
value=meas.vertical_circle,
|
||||
std_dev=std
|
||||
))
|
||||
|
||||
return self.observations
|
||||
|
||||
def initialize_points(self):
|
||||
"""Initialisiert Näherungskoordinaten aus JXL-Daten"""
|
||||
self.points = {}
|
||||
|
||||
# Alle aktiven Punkte
|
||||
for name, point in self.parser.get_active_points().items():
|
||||
self.points[name] = AdjustedPoint(
|
||||
name=name,
|
||||
x=point.east if point.east is not None else 0.0,
|
||||
y=point.north if point.north is not None else 0.0,
|
||||
z=point.elevation if point.elevation is not None else 0.0,
|
||||
x_approx=point.east if point.east is not None else 0.0,
|
||||
y_approx=point.north if point.north is not None else 0.0,
|
||||
z_approx=point.elevation if point.elevation is not None else 0.0
|
||||
)
|
||||
|
||||
# Stationen hinzufügen
|
||||
for station_id, station in self.parser.stations.items():
|
||||
if station.name not in self.points:
|
||||
self.points[station.name] = AdjustedPoint(
|
||||
name=station.name,
|
||||
x=station.east if station.east is not None else 0.0,
|
||||
y=station.north if station.north is not None else 0.0,
|
||||
z=station.elevation if station.elevation is not None else 0.0,
|
||||
x_approx=station.east if station.east is not None else 0.0,
|
||||
y_approx=station.north if station.north is not None else 0.0,
|
||||
z_approx=station.elevation if station.elevation is not None else 0.0
|
||||
)
|
||||
|
||||
def set_fixed_point(self, point_name: str):
|
||||
"""Setzt einen Punkt als Festpunkt"""
|
||||
if point_name in self.points:
|
||||
self.fixed_points.add(point_name)
|
||||
self.points[point_name].is_fixed = True
|
||||
|
||||
def set_fixed_points_auto(self):
|
||||
"""Setzt automatisch Festpunkte (Referenzpunkte)"""
|
||||
# Referenzlinie als Festpunkte
|
||||
ref_line = self.parser.get_reference_line()
|
||||
if ref_line:
|
||||
self.set_fixed_point(ref_line.start_point)
|
||||
self.set_fixed_point(ref_line.end_point)
|
||||
|
||||
# Zusätzlich: Erste Station als Festpunkt falls keine Referenzlinie
|
||||
if not self.fixed_points:
|
||||
stations = list(self.parser.stations.values())
|
||||
if stations:
|
||||
first_station = min(stations, key=lambda s: s.timestamp)
|
||||
self.set_fixed_point(first_station.name)
|
||||
|
||||
def adjust(self) -> AdjustmentResult:
|
||||
"""
|
||||
Führt die Netzausgleichung durch
|
||||
Iterative Lösung nach Gauß-Newton
|
||||
"""
|
||||
if not self.observations:
|
||||
self.extract_observations()
|
||||
|
||||
if not self.points:
|
||||
self.initialize_points()
|
||||
|
||||
if not self.fixed_points:
|
||||
self.set_fixed_points_auto()
|
||||
|
||||
# Unbekannte bestimmen (nur nicht-fixierte Punkte)
|
||||
unknown_points = [name for name in self.points.keys()
|
||||
if name not in self.fixed_points]
|
||||
|
||||
num_unknowns = len(unknown_points) * 2 # Nur X, Y (2D-Ausgleichung)
|
||||
num_observations = len([o for o in self.observations
|
||||
if o.obs_type in ['direction', 'distance']])
|
||||
|
||||
if num_unknowns == 0:
|
||||
raise ValueError("Keine unbekannten Punkte!")
|
||||
|
||||
if num_observations < num_unknowns:
|
||||
raise ValueError(f"Unterbestimmtes System: {num_observations} Beobachtungen, "
|
||||
f"{num_unknowns} Unbekannte")
|
||||
|
||||
# Index-Mapping für Unbekannte
|
||||
point_index = {name: i for i, name in enumerate(unknown_points)}
|
||||
|
||||
# Iterative Lösung
|
||||
converged = False
|
||||
iteration = 0
|
||||
|
||||
while not converged and iteration < self.max_iterations:
|
||||
iteration += 1
|
||||
|
||||
# Designmatrix A und Beobachtungsvektor l erstellen
|
||||
A, l, P = self._build_normal_equations(point_index, num_unknowns)
|
||||
|
||||
# Normalgleichungssystem: N = A^T * P * A, n = A^T * P * l
|
||||
N = A.T @ np.diag(P) @ A
|
||||
n = A.T @ np.diag(P) @ l
|
||||
|
||||
# Lösung: x = N^-1 * n
|
||||
try:
|
||||
dx = np.linalg.solve(N, n)
|
||||
except np.linalg.LinAlgError:
|
||||
# Regularisierung bei singulärer Matrix
|
||||
N += np.eye(num_unknowns) * 1e-10
|
||||
dx = np.linalg.solve(N, n)
|
||||
|
||||
# Koordinaten aktualisieren
|
||||
max_correction = 0.0
|
||||
for name, idx in point_index.items():
|
||||
i = idx * 2
|
||||
self.points[name].dx = dx[i]
|
||||
self.points[name].dy = dx[i + 1]
|
||||
self.points[name].x += dx[i]
|
||||
self.points[name].y += dx[i + 1]
|
||||
|
||||
max_correction = max(max_correction,
|
||||
abs(dx[i]), abs(dx[i + 1]))
|
||||
|
||||
# Konvergenzprüfung
|
||||
if max_correction < self.convergence_limit:
|
||||
converged = True
|
||||
|
||||
# Nachbearbeitung
|
||||
self._compute_residuals()
|
||||
self._compute_accuracy(point_index, num_unknowns)
|
||||
|
||||
# Ergebnis zusammenstellen
|
||||
self.result = AdjustmentResult(
|
||||
adjusted_points=self.points,
|
||||
observations=self.observations,
|
||||
sigma_0_priori=self.sigma_0_priori,
|
||||
iterations=iteration,
|
||||
converged=converged,
|
||||
num_points=len(self.points),
|
||||
num_fixed_points=len(self.fixed_points),
|
||||
num_observations=num_observations,
|
||||
num_unknowns=num_unknowns,
|
||||
redundancy=num_observations - num_unknowns
|
||||
)
|
||||
|
||||
self._compute_global_statistics()
|
||||
|
||||
return self.result
|
||||
|
||||
def _build_normal_equations(self, point_index: Dict[str, int],
|
||||
num_unknowns: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
||||
"""Erstellt Designmatrix A und Beobachtungsvektor l"""
|
||||
|
||||
# Nur Richtungen und Strecken für 2D-Ausgleichung
|
||||
relevant_obs = [o for o in self.observations
|
||||
if o.obs_type in ['direction', 'distance']]
|
||||
|
||||
n_obs = len(relevant_obs)
|
||||
A = np.zeros((n_obs, num_unknowns))
|
||||
l = np.zeros(n_obs)
|
||||
P = np.zeros(n_obs)
|
||||
|
||||
for i, obs in enumerate(relevant_obs):
|
||||
from_pt = self.points.get(obs.from_station)
|
||||
to_pt = self.points.get(obs.to_point)
|
||||
|
||||
if from_pt is None or to_pt is None:
|
||||
continue
|
||||
|
||||
dx = to_pt.x - from_pt.x
|
||||
dy = to_pt.y - from_pt.y
|
||||
dist = math.sqrt(dx**2 + dy**2)
|
||||
|
||||
if dist < 1e-10:
|
||||
continue
|
||||
|
||||
from_idx = point_index.get(obs.from_station)
|
||||
to_idx = point_index.get(obs.to_point)
|
||||
|
||||
if obs.obs_type == 'direction':
|
||||
# Azimut berechnen
|
||||
azimuth_calc = math.atan2(dx, dy) * 200.0 / math.pi
|
||||
if azimuth_calc < 0:
|
||||
azimuth_calc += 400.0
|
||||
|
||||
# Partielle Ableitungen (in Gon)
|
||||
# dAz/dx = dy / (rho * s^2)
|
||||
# dAz/dy = -dx / (rho * s^2)
|
||||
rho = 200.0 / math.pi
|
||||
factor = rho / (dist ** 2)
|
||||
|
||||
if from_idx is not None:
|
||||
A[i, from_idx * 2] = -dy * factor # dAz/dx_from
|
||||
A[i, from_idx * 2 + 1] = dx * factor # dAz/dy_from
|
||||
|
||||
if to_idx is not None:
|
||||
A[i, to_idx * 2] = dy * factor # dAz/dx_to
|
||||
A[i, to_idx * 2 + 1] = -dx * factor # dAz/dy_to
|
||||
|
||||
# Verkürzung l = beobachtet - berechnet
|
||||
diff = obs.value - azimuth_calc
|
||||
# Normalisierung auf [-200, 200] Gon
|
||||
while diff > 200:
|
||||
diff -= 400
|
||||
while diff < -200:
|
||||
diff += 400
|
||||
l[i] = diff
|
||||
|
||||
elif obs.obs_type == 'distance':
|
||||
# Partielle Ableitungen
|
||||
# ds/dx = dx/s
|
||||
# ds/dy = dy/s
|
||||
|
||||
if from_idx is not None:
|
||||
A[i, from_idx * 2] = -dx / dist
|
||||
A[i, from_idx * 2 + 1] = -dy / dist
|
||||
|
||||
if to_idx is not None:
|
||||
A[i, to_idx * 2] = dx / dist
|
||||
A[i, to_idx * 2 + 1] = dy / dist
|
||||
|
||||
# Verkürzung
|
||||
l[i] = obs.value - dist
|
||||
|
||||
# Gewicht
|
||||
P[i] = obs.weight
|
||||
|
||||
return A, l, P
|
||||
|
||||
def _compute_residuals(self):
|
||||
"""Berechnet Verbesserungen (Residuen) für alle Beobachtungen"""
|
||||
for obs in self.observations:
|
||||
from_pt = self.points.get(obs.from_station)
|
||||
to_pt = self.points.get(obs.to_point)
|
||||
|
||||
if from_pt is None or to_pt is None:
|
||||
continue
|
||||
|
||||
dx = to_pt.x - from_pt.x
|
||||
dy = to_pt.y - from_pt.y
|
||||
dz = to_pt.z - from_pt.z
|
||||
dist_2d = math.sqrt(dx**2 + dy**2)
|
||||
dist_3d = math.sqrt(dx**2 + dy**2 + dz**2)
|
||||
|
||||
if obs.obs_type == 'direction':
|
||||
azimuth_calc = math.atan2(dx, dy) * 200.0 / math.pi
|
||||
if azimuth_calc < 0:
|
||||
azimuth_calc += 400.0
|
||||
|
||||
residual = obs.value - azimuth_calc
|
||||
while residual > 200:
|
||||
residual -= 400
|
||||
while residual < -200:
|
||||
residual += 400
|
||||
obs.residual = residual
|
||||
|
||||
elif obs.obs_type == 'distance':
|
||||
obs.residual = obs.value - dist_3d
|
||||
|
||||
elif obs.obs_type == 'zenith':
|
||||
if dist_2d > 0:
|
||||
zenith_calc = math.atan2(dist_2d, dz) * 200.0 / math.pi
|
||||
obs.residual = obs.value - zenith_calc
|
||||
|
||||
def _compute_accuracy(self, point_index: Dict[str, int], num_unknowns: int):
|
||||
"""Berechnet Genauigkeitsmaße für ausgeglichene Punkte"""
|
||||
# Vereinfachte Berechnung der Standardabweichungen
|
||||
# Vollständige Varianzfortpflanzung würde Inverse von N erfordern
|
||||
|
||||
# Erstelle Normalgleichungsmatrix erneut
|
||||
A, l, P = self._build_normal_equations(point_index, num_unknowns)
|
||||
N = A.T @ np.diag(P) @ A
|
||||
|
||||
try:
|
||||
Q = np.linalg.inv(N) # Kofaktormatrix
|
||||
except np.linalg.LinAlgError:
|
||||
Q = np.eye(num_unknowns) * 0.001
|
||||
|
||||
# A-posteriori Varianzfaktor
|
||||
v = A @ np.zeros(num_unknowns) - l # Residuen (vereinfacht)
|
||||
redundancy = len(l) - num_unknowns
|
||||
|
||||
if redundancy > 0:
|
||||
sum_pvv = np.sum(P * v**2)
|
||||
sigma_0_post = math.sqrt(sum_pvv / redundancy)
|
||||
else:
|
||||
sigma_0_post = self.sigma_0_priori
|
||||
|
||||
self.sigma_0_posteriori = sigma_0_post
|
||||
|
||||
# Punktgenauigkeiten
|
||||
for name, idx in point_index.items():
|
||||
i = idx * 2
|
||||
|
||||
# Standardabweichungen
|
||||
self.points[name].std_x = sigma_0_post * math.sqrt(abs(Q[i, i]))
|
||||
self.points[name].std_y = sigma_0_post * math.sqrt(abs(Q[i + 1, i + 1]))
|
||||
|
||||
# Punktlagegenauigkeit
|
||||
self.points[name].std_position = math.sqrt(
|
||||
self.points[name].std_x**2 + self.points[name].std_y**2
|
||||
)
|
||||
|
||||
# Konfidenzellipse (vereinfacht)
|
||||
# Vollständige Berechnung würde Eigenwertanalyse erfordern
|
||||
cov_xy = Q[i, i + 1] if i + 1 < num_unknowns else 0
|
||||
var_x = Q[i, i]
|
||||
var_y = Q[i + 1, i + 1]
|
||||
|
||||
# Eigenwerte für Ellipse
|
||||
trace = var_x + var_y
|
||||
det = var_x * var_y - cov_xy**2
|
||||
discriminant = trace**2 / 4 - det
|
||||
|
||||
if discriminant >= 0:
|
||||
sqrt_disc = math.sqrt(discriminant)
|
||||
lambda1 = trace / 2 + sqrt_disc
|
||||
lambda2 = trace / 2 - sqrt_disc
|
||||
|
||||
self.points[name].semi_major = sigma_0_post * math.sqrt(max(lambda1, 0))
|
||||
self.points[name].semi_minor = sigma_0_post * math.sqrt(max(lambda2, 0))
|
||||
|
||||
if abs(var_x - var_y) > 1e-10:
|
||||
self.points[name].orientation = 0.5 * math.atan2(2 * cov_xy, var_x - var_y) * 200 / math.pi
|
||||
|
||||
def _compute_global_statistics(self):
|
||||
"""Berechnet globale Statistiken"""
|
||||
if self.result is None:
|
||||
return
|
||||
|
||||
# RMSE für verschiedene Beobachtungstypen
|
||||
dir_residuals = [o.residual for o in self.observations if o.obs_type == 'direction']
|
||||
dist_residuals = [o.residual for o in self.observations if o.obs_type == 'distance']
|
||||
zen_residuals = [o.residual for o in self.observations if o.obs_type == 'zenith']
|
||||
|
||||
if dir_residuals:
|
||||
self.result.rmse_directions = math.sqrt(sum(r**2 for r in dir_residuals) / len(dir_residuals)) * 1000 # mgon
|
||||
|
||||
if dist_residuals:
|
||||
self.result.rmse_distances = math.sqrt(sum(r**2 for r in dist_residuals) / len(dist_residuals)) * 1000 # mm
|
||||
|
||||
if zen_residuals:
|
||||
self.result.rmse_zenith = math.sqrt(sum(r**2 for r in zen_residuals) / len(zen_residuals)) * 1000 # mgon
|
||||
|
||||
self.result.sigma_0_posteriori = self.sigma_0_posteriori
|
||||
|
||||
# Chi-Quadrat-Test
|
||||
if self.result.redundancy > 0:
|
||||
self.result.chi_square = (self.sigma_0_posteriori / self.sigma_0_priori) ** 2 * self.result.redundancy
|
||||
|
||||
def get_adjustment_report(self) -> str:
|
||||
"""Erstellt einen detaillierten Ausgleichungsbericht"""
|
||||
if self.result is None:
|
||||
return "Keine Ausgleichung durchgeführt."
|
||||
|
||||
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.parser.job_name}")
|
||||
lines.append(f"Anzahl Punkte: {self.result.num_points}")
|
||||
lines.append(f" davon Festpunkte: {self.result.num_fixed_points}")
|
||||
lines.append(f" davon Neupunkte: {self.result.num_points - self.result.num_fixed_points}")
|
||||
lines.append(f"Anzahl Beobachtungen: {self.result.num_observations}")
|
||||
lines.append(f"Anzahl Unbekannte: {self.result.num_unknowns}")
|
||||
lines.append(f"Redundanz: {self.result.redundancy}")
|
||||
lines.append(f"Iterationen: {self.result.iterations}")
|
||||
lines.append(f"Konvergiert: {'Ja' if self.result.converged else 'Nein'}")
|
||||
lines.append("")
|
||||
|
||||
# Qualitätsparameter
|
||||
lines.append("GLOBALE QUALITÄTSPARAMETER")
|
||||
lines.append("-" * 80)
|
||||
lines.append(f"Sigma-0 a-priori: {self.sigma_0_priori:.4f}")
|
||||
lines.append(f"Sigma-0 a-posteriori: {self.result.sigma_0_posteriori:.4f}")
|
||||
lines.append(f"Chi-Quadrat: {self.result.chi_square:.2f}")
|
||||
lines.append(f"RMSE Richtungen: {self.result.rmse_directions:.2f} mgon")
|
||||
lines.append(f"RMSE Strecken: {self.result.rmse_distances:.2f} mm")
|
||||
lines.append(f"RMSE Zenitwinkel: {self.result.rmse_zenith:.2f} mgon")
|
||||
lines.append("")
|
||||
|
||||
# Festpunkte
|
||||
lines.append("FESTPUNKTE")
|
||||
lines.append("-" * 80)
|
||||
lines.append(f"{'Punkt':<12} {'X [m]':>14} {'Y [m]':>14} {'Z [m]':>12}")
|
||||
lines.append("-" * 80)
|
||||
for name in self.fixed_points:
|
||||
if name in self.points:
|
||||
p = self.points[name]
|
||||
lines.append(f"{name:<12} {p.x:>14.4f} {p.y:>14.4f} {p.z:>12.4f}")
|
||||
lines.append("")
|
||||
|
||||
# Ausgeglichene Koordinaten
|
||||
lines.append("AUSGEGLICHENE KOORDINATEN (NEUPUNKTE)")
|
||||
lines.append("-" * 80)
|
||||
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("-" * 80)
|
||||
|
||||
for name, p in sorted(self.points.items()):
|
||||
if name not in self.fixed_points:
|
||||
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("")
|
||||
|
||||
# Beobachtungen und Residuen
|
||||
lines.append("BEOBACHTUNGEN UND VERBESSERUNGEN")
|
||||
lines.append("-" * 80)
|
||||
lines.append(f"{'Von':<10} {'Nach':<10} {'Typ':<10} {'Messwert':>14} {'Residuum':>12}")
|
||||
lines.append("-" * 80)
|
||||
|
||||
for obs in sorted(self.observations, key=lambda x: (x.from_station, x.to_point)):
|
||||
if obs.obs_type == 'direction':
|
||||
unit = "gon"
|
||||
res_str = f"{obs.residual*1000:.2f} mgon"
|
||||
elif obs.obs_type == 'distance':
|
||||
unit = "m"
|
||||
res_str = f"{obs.residual*1000:.2f} mm"
|
||||
elif obs.obs_type == 'zenith':
|
||||
unit = "gon"
|
||||
res_str = f"{obs.residual*1000:.2f} mgon"
|
||||
else:
|
||||
unit = ""
|
||||
res_str = f"{obs.residual:.4f}"
|
||||
|
||||
lines.append(f"{obs.from_station:<10} {obs.to_point:<10} {obs.obs_type:<10} "
|
||||
f"{obs.value:>12.4f} {unit:<2} {res_str:>12}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("=" * 80)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def export_adjusted_points(self, filepath: str, format: str = 'csv'):
|
||||
"""Exportiert ausgeglichene Punkte"""
|
||||
if format == 'csv':
|
||||
lines = ["Punkt;X;Y;Z;Sigma_X;Sigma_Y;Sigma_Pos;Festpunkt"]
|
||||
for name, p in sorted(self.points.items()):
|
||||
fixed = "Ja" if p.is_fixed else "Nein"
|
||||
lines.append(f"{name};{p.x:.4f};{p.y:.4f};{p.z:.4f};"
|
||||
f"{p.std_x*1000:.2f};{p.std_y*1000:.2f};{p.std_position*1000:.2f};{fixed}")
|
||||
else:
|
||||
lines = []
|
||||
for name, p in sorted(self.points.items()):
|
||||
lines.append(f"{name}\t{p.x:.4f}\t{p.y:.4f}\t{p.z:.4f}")
|
||||
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write("\n".join(lines))
|
||||
|
||||
def export_report(self, filepath: str):
|
||||
"""Exportiert den vollständigen Bericht"""
|
||||
report = self.get_adjustment_report()
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(report)
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
"""
|
||||
Koordinatentransformations-Modul
|
||||
Unterstützt Rotation, Translation in XY und Z
|
||||
KEINE Maßstabsänderung (wie vom Benutzer gefordert)
|
||||
"""
|
||||
|
||||
import math
|
||||
import numpy as np
|
||||
from typing import List, Tuple, Optional, Dict
|
||||
from dataclasses import dataclass
|
||||
from .cor_generator import CORPoint
|
||||
|
||||
|
||||
@dataclass
|
||||
class TransformationParameters:
|
||||
"""Parameter für die Koordinatentransformation"""
|
||||
# Translation
|
||||
dx: float = 0.0 # Verschiebung in X (East)
|
||||
dy: float = 0.0 # Verschiebung in Y (North)
|
||||
dz: float = 0.0 # Verschiebung in Z (Höhe)
|
||||
|
||||
# Rotation um Z-Achse (in Gon)
|
||||
rotation_gon: float = 0.0
|
||||
|
||||
# Drehpunkt (für Rotation)
|
||||
pivot_x: float = 0.0
|
||||
pivot_y: float = 0.0
|
||||
|
||||
def rotation_rad(self) -> float:
|
||||
"""Gibt Rotation in Radiant zurück"""
|
||||
return self.rotation_gon * math.pi / 200.0
|
||||
|
||||
|
||||
class CoordinateTransformer:
|
||||
"""Transformiert Koordinaten: Rotation und Translation"""
|
||||
|
||||
def __init__(self):
|
||||
self.params = TransformationParameters()
|
||||
self.original_points: List[CORPoint] = []
|
||||
self.transformed_points: List[CORPoint] = []
|
||||
|
||||
def set_points(self, points: List[CORPoint]):
|
||||
"""Setzt die zu transformierenden Punkte"""
|
||||
self.original_points = points.copy()
|
||||
self.transformed_points = []
|
||||
|
||||
def set_manual_parameters(self, dx: float, dy: float, dz: float,
|
||||
rotation_gon: float, pivot_x: float = 0.0,
|
||||
pivot_y: float = 0.0):
|
||||
"""Setzt Transformationsparameter manuell"""
|
||||
self.params.dx = dx
|
||||
self.params.dy = dy
|
||||
self.params.dz = dz
|
||||
self.params.rotation_gon = rotation_gon
|
||||
self.params.pivot_x = pivot_x
|
||||
self.params.pivot_y = pivot_y
|
||||
|
||||
def compute_from_two_points(self,
|
||||
point1_name: str,
|
||||
point2_name: str,
|
||||
z_reference_name: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Berechnet Transformation aus zwei Punkten:
|
||||
- point1 wird zum Ursprung (0,0)
|
||||
- point2 definiert die Y-Richtung (Nordrichtung)
|
||||
- z_reference (optional) definiert Z=0
|
||||
"""
|
||||
# Finde Punkte
|
||||
point1 = None
|
||||
point2 = None
|
||||
z_ref = None
|
||||
|
||||
for p in self.original_points:
|
||||
if p.name == point1_name:
|
||||
point1 = p
|
||||
elif p.name == point2_name:
|
||||
point2 = p
|
||||
if z_reference_name and p.name == z_reference_name:
|
||||
z_ref = p
|
||||
|
||||
if point1 is None or point2 is None:
|
||||
return False
|
||||
|
||||
# Drehpunkt ist point1
|
||||
self.params.pivot_x = point1.x
|
||||
self.params.pivot_y = point1.y
|
||||
|
||||
# Translation: point1 zum Ursprung
|
||||
self.params.dx = -point1.x
|
||||
self.params.dy = -point1.y
|
||||
|
||||
# Rotation: point2 soll auf der positiven Y-Achse liegen
|
||||
# Berechne Richtung von point1 zu point2
|
||||
dx_12 = point2.x - point1.x
|
||||
dy_12 = point2.y - point1.y
|
||||
|
||||
# Aktueller Winkel zur Y-Achse (Nordrichtung)
|
||||
current_angle_rad = math.atan2(dx_12, dy_12) # atan2(x,y) für Azimut
|
||||
|
||||
# Rotation um diesen Winkel (negativ, um auf Y-Achse zu drehen)
|
||||
self.params.rotation_gon = -current_angle_rad * 200.0 / math.pi
|
||||
|
||||
# Z-Verschiebung
|
||||
if z_ref:
|
||||
self.params.dz = -z_ref.z
|
||||
else:
|
||||
self.params.dz = -point1.z
|
||||
|
||||
return True
|
||||
|
||||
def transform(self) -> List[CORPoint]:
|
||||
"""Führt die Transformation durch"""
|
||||
self.transformed_points = []
|
||||
|
||||
rot_rad = self.params.rotation_rad()
|
||||
cos_r = math.cos(rot_rad)
|
||||
sin_r = math.sin(rot_rad)
|
||||
|
||||
for p in self.original_points:
|
||||
# 1. Zum Drehpunkt verschieben
|
||||
x_shifted = p.x - self.params.pivot_x
|
||||
y_shifted = p.y - self.params.pivot_y
|
||||
|
||||
# 2. Rotation anwenden
|
||||
x_rotated = x_shifted * cos_r - y_shifted * sin_r
|
||||
y_rotated = x_shifted * sin_r + y_shifted * cos_r
|
||||
|
||||
# 3. Translation anwenden
|
||||
x_final = x_rotated + self.params.pivot_x + self.params.dx
|
||||
y_final = y_rotated + self.params.pivot_y + self.params.dy
|
||||
z_final = p.z + self.params.dz
|
||||
|
||||
self.transformed_points.append(CORPoint(
|
||||
name=p.name,
|
||||
x=x_final,
|
||||
y=y_final,
|
||||
z=z_final
|
||||
))
|
||||
|
||||
return self.transformed_points
|
||||
|
||||
def transform_single_point(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
||||
"""Transformiert einen einzelnen Punkt"""
|
||||
rot_rad = self.params.rotation_rad()
|
||||
cos_r = math.cos(rot_rad)
|
||||
sin_r = math.sin(rot_rad)
|
||||
|
||||
# Zum Drehpunkt verschieben
|
||||
x_shifted = x - self.params.pivot_x
|
||||
y_shifted = y - self.params.pivot_y
|
||||
|
||||
# Rotation
|
||||
x_rotated = x_shifted * cos_r - y_shifted * sin_r
|
||||
y_rotated = x_shifted * sin_r + y_shifted * cos_r
|
||||
|
||||
# Translation
|
||||
x_final = x_rotated + self.params.pivot_x + self.params.dx
|
||||
y_final = y_rotated + self.params.pivot_y + self.params.dy
|
||||
z_final = z + self.params.dz
|
||||
|
||||
return x_final, y_final, z_final
|
||||
|
||||
def inverse_transform(self, x: float, y: float, z: float) -> Tuple[float, float, float]:
|
||||
"""Inverse Transformation"""
|
||||
# Inverse Translation
|
||||
x_inv = x - self.params.dx - self.params.pivot_x
|
||||
y_inv = y - self.params.dy - self.params.pivot_y
|
||||
z_inv = z - self.params.dz
|
||||
|
||||
# Inverse Rotation
|
||||
rot_rad = -self.params.rotation_rad()
|
||||
cos_r = math.cos(rot_rad)
|
||||
sin_r = math.sin(rot_rad)
|
||||
|
||||
x_rotated = x_inv * cos_r - y_inv * sin_r
|
||||
y_rotated = x_inv * sin_r + y_inv * cos_r
|
||||
|
||||
# Zurück vom Drehpunkt
|
||||
x_final = x_rotated + self.params.pivot_x
|
||||
y_final = y_rotated + self.params.pivot_y
|
||||
|
||||
return x_final, y_final, z_inv
|
||||
|
||||
def get_transformation_matrix(self) -> np.ndarray:
|
||||
"""Gibt die 4x4 Transformationsmatrix zurück"""
|
||||
rot_rad = self.params.rotation_rad()
|
||||
cos_r = math.cos(rot_rad)
|
||||
sin_r = math.sin(rot_rad)
|
||||
|
||||
# Translation zum Drehpunkt
|
||||
T1 = np.array([
|
||||
[1, 0, 0, -self.params.pivot_x],
|
||||
[0, 1, 0, -self.params.pivot_y],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1]
|
||||
])
|
||||
|
||||
# Rotation
|
||||
R = np.array([
|
||||
[cos_r, -sin_r, 0, 0],
|
||||
[sin_r, cos_r, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1]
|
||||
])
|
||||
|
||||
# Translation zurück und zusätzliche Verschiebung
|
||||
T2 = np.array([
|
||||
[1, 0, 0, self.params.pivot_x + self.params.dx],
|
||||
[0, 1, 0, self.params.pivot_y + self.params.dy],
|
||||
[0, 0, 1, self.params.dz],
|
||||
[0, 0, 0, 1]
|
||||
])
|
||||
|
||||
return T2 @ R @ T1
|
||||
|
||||
def get_parameters_report(self) -> str:
|
||||
"""Gibt einen Bericht über die Transformationsparameter zurück"""
|
||||
report = []
|
||||
report.append("=" * 50)
|
||||
report.append("KOORDINATENTRANSFORMATION - PARAMETER")
|
||||
report.append("=" * 50)
|
||||
report.append("")
|
||||
report.append("Translation:")
|
||||
report.append(f" dX (East): {self.params.dx:+.4f} m")
|
||||
report.append(f" dY (North): {self.params.dy:+.4f} m")
|
||||
report.append(f" dZ (Höhe): {self.params.dz:+.4f} m")
|
||||
report.append("")
|
||||
report.append("Rotation:")
|
||||
report.append(f" Winkel: {self.params.rotation_gon:+.6f} gon")
|
||||
report.append(f" Winkel: {self.params.rotation_gon * 0.9:+.6f}°")
|
||||
report.append(f" Winkel: {self.params.rotation_rad():+.8f} rad")
|
||||
report.append("")
|
||||
report.append("Drehpunkt:")
|
||||
report.append(f" X: {self.params.pivot_x:.4f} m")
|
||||
report.append(f" Y: {self.params.pivot_y:.4f} m")
|
||||
report.append("")
|
||||
report.append("Hinweis: Keine Maßstabsänderung (Maßstab = 1.0)")
|
||||
report.append("=" * 50)
|
||||
|
||||
return "\n".join(report)
|
||||
|
||||
def get_comparison_table(self) -> str:
|
||||
"""Erstellt eine Vergleichstabelle Original vs. Transformiert"""
|
||||
if not self.original_points or not self.transformed_points:
|
||||
return "Keine Daten verfügbar."
|
||||
|
||||
lines = []
|
||||
lines.append("=" * 90)
|
||||
lines.append("KOORDINATENVERGLEICH: ORIGINAL → TRANSFORMIERT")
|
||||
lines.append("=" * 90)
|
||||
lines.append(f"{'Punkt':<10} {'X_orig':>12} {'Y_orig':>12} {'Z_orig':>10} | "
|
||||
f"{'X_trans':>12} {'Y_trans':>12} {'Z_trans':>10}")
|
||||
lines.append("-" * 90)
|
||||
|
||||
for orig, trans in zip(self.original_points, self.transformed_points):
|
||||
lines.append(f"{orig.name:<10} {orig.x:>12.4f} {orig.y:>12.4f} {orig.z:>10.4f} | "
|
||||
f"{trans.x:>12.4f} {trans.y:>12.4f} {trans.z:>10.4f}")
|
||||
|
||||
lines.append("=" * 90)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
class LocalSystemTransformer:
|
||||
"""
|
||||
Spezielle Transformation für lokale Systeme
|
||||
Transformiert in ein System mit definiertem Ursprung und Ausrichtung
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.origin_point: Optional[str] = None
|
||||
self.direction_point: Optional[str] = None
|
||||
self.z_reference_point: Optional[str] = None
|
||||
self.transformer = CoordinateTransformer()
|
||||
|
||||
def setup_local_system(self, points: List[CORPoint],
|
||||
origin_name: str,
|
||||
direction_name: str,
|
||||
z_ref_name: Optional[str] = None) -> bool:
|
||||
"""
|
||||
Richtet ein lokales Koordinatensystem ein:
|
||||
- origin_name: Punkt bei (0,0)
|
||||
- direction_name: Punkt definiert Y-Richtung (liegt auf positiver Y-Achse)
|
||||
- z_ref_name: Punkt definiert Z=0
|
||||
"""
|
||||
self.origin_point = origin_name
|
||||
self.direction_point = direction_name
|
||||
self.z_reference_point = z_ref_name
|
||||
|
||||
self.transformer.set_points(points)
|
||||
|
||||
success = self.transformer.compute_from_two_points(
|
||||
origin_name,
|
||||
direction_name,
|
||||
z_ref_name
|
||||
)
|
||||
|
||||
if success:
|
||||
self.transformer.transform()
|
||||
|
||||
return success
|
||||
|
||||
def get_transformed_points(self) -> List[CORPoint]:
|
||||
"""Gibt die transformierten Punkte zurück"""
|
||||
return self.transformer.transformed_points
|
||||
|
||||
def get_report(self) -> str:
|
||||
"""Gibt einen vollständigen Bericht zurück"""
|
||||
report = []
|
||||
report.append("=" * 60)
|
||||
report.append("LOKALES KOORDINATENSYSTEM")
|
||||
report.append("=" * 60)
|
||||
report.append(f"Ursprung (0,0): {self.origin_point}")
|
||||
report.append(f"Y-Richtung: {self.direction_point}")
|
||||
if self.z_reference_point:
|
||||
report.append(f"Z-Referenz (0): {self.z_reference_point}")
|
||||
report.append("")
|
||||
report.append(self.transformer.get_parameters_report())
|
||||
report.append("")
|
||||
report.append(self.transformer.get_comparison_table())
|
||||
|
||||
return "\n".join(report)
|
||||
Loading…
Reference in New Issue