trimble_geodesy/modules/jxl_parser.py

957 lines
36 KiB
Python

"""
JXL Parser Module - Trimble JXL-Datei Parser
Liest und analysiert Trimble JXL-Dateien (XML-basiert)
Version 3.0 - Überarbeitet für korrekte Stationierungserkennung
"""
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
# Anzahl Messungen
num_backsight_measurements: int = 0
num_backsight_points: int = 0
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 = ""
timestamp: 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
@dataclass
class Measurement:
"""Detaillierte Messung mit allen Rohdaten"""
point_name: str
station_id: str
station_name: str
target_id: str
# Rohdaten
horizontal_circle: Optional[float] = None
vertical_circle: Optional[float] = None
edm_distance: Optional[float] = None
face: str = "Face1"
# Berechnete Koordinaten
north: Optional[float] = None
east: Optional[float] = None
elevation: Optional[float] = None
# Prismenkonstante
prism_constant: float = 0.0
prism_type: str = ""
target_height: float = 0.0
# Klassifikation
classification: str = "" # BackSight, Normal
deleted: bool = False
# Standardabweichungen
hz_std_error: Optional[float] = None
vz_std_error: Optional[float] = None
dist_std_error: Optional[float] = None
# Atmosphäre
pressure: Optional[float] = None
temperature: Optional[float] = None
timestamp: str = ""
record_id: str = ""
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] = []
# Detaillierte Messungen für Protokoll
self.measurements: List[Measurement] = []
# Station zu Messungen Mapping
self.station_measurements: Dict[str, List[Measurement]] = {}
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()
# Detaillierte Messungen erstellen
self._create_detailed_measurements()
return True
except Exception as e:
print(f"Fehler beim Parsen: {e}")
import traceback
traceback.print_exc()
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, timestamp)
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)
num_bs_meas = element.find('NumberOfBacksightMeasurements')
if num_bs_meas is not None and num_bs_meas.text:
station.num_backsight_measurements = int(num_bs_meas.text)
num_bs_pts = element.find('NumberOfBacksightPoints')
if num_bs_pts is not None and num_bs_pts.text:
station.num_backsight_points = int(num_bs_pts.text)
self.stations[record_id] = station
def _parse_target(self, element, record_id: str, timestamp: str = ""):
"""Parst ein Target/Prisma"""
target = Target()
target.record_id = record_id
target.timestamp = timestamp
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 _create_detailed_measurements(self):
"""Erstellt detaillierte Messungen für das Berechnungsprotokoll"""
self.measurements = []
self.station_measurements = {}
for point in self.all_point_records:
if not point.station_id:
continue
# Station finden
station = self.stations.get(point.station_id)
station_name = station.name if station else "?"
# Target/Prismenkonstante finden
target = self.targets.get(point.target_id)
prism_const = target.prism_constant if target else 0.0
prism_type = target.prism_type if target else ""
target_height = target.target_height if target else 0.0
meas = Measurement(
point_name=point.name,
station_id=point.station_id,
station_name=station_name,
target_id=point.target_id,
horizontal_circle=point.horizontal_circle,
vertical_circle=point.vertical_circle,
edm_distance=point.edm_distance,
face=point.face,
north=point.north,
east=point.east,
elevation=point.elevation,
prism_constant=prism_const,
prism_type=prism_type,
target_height=target_height,
classification=point.classification,
deleted=point.deleted,
hz_std_error=point.hz_std_error,
vz_std_error=point.vz_std_error,
dist_std_error=point.dist_std_error,
pressure=point.pressure,
temperature=point.temperature,
timestamp=point.timestamp,
record_id=point.record_id
)
self.measurements.append(meas)
# Station-Mapping
if point.station_id not in self.station_measurements:
self.station_measurements[point.station_id] = []
self.station_measurements[point.station_id].append(meas)
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_detailed_measurements_from_station(self, station_id: str) -> List[Measurement]:
"""Gibt detaillierte Messungen von einer Station zurück"""
return self.station_measurements.get(station_id, [])
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 get_unique_prism_types(self) -> List[Tuple[str, str, float]]:
"""Gibt eindeutige Prismentypen mit Konstanten zurück: (ID, Type, Constant)"""
seen = set()
result = []
for tid, target in self.targets.items():
key = (target.prism_type, target.prism_constant)
if key not in seen:
seen.add(key)
result.append((tid, target.prism_type, target.prism_constant))
return result
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 get_reference_points(self) -> List[str]:
"""
Gibt die absoluten Festpunkte (Passpunkte) zurück.
KORREKTE LOGIK:
- NUR 5xxx-Serie (5001, 5002, etc.) sind echte Festpunkte
- ALLE anderen Punkte (Standpunkte, Anschlusspunkte, Messpunkte) werden ausgeglichen
- Auch Punkte die als Anschlusspunkte verwendet werden (2xxx, 6xxx) sind KEINE Festpunkte!
Festpunkte werden erkannt durch:
1. Punktnummer beginnt mit 5 (5xxx Serie)
2. ODER Punkte aus Referenzlinien mit Method="Coordinates"
"""
ref_points = set()
# Methode 1: 5xxx-Serie = absolute Festpunkte
for name, point in self.points.items():
if point.deleted:
continue
# Prüfe ob Punktname mit 5 beginnt und eine Nummer ist
try:
if name.startswith('5') and name.isdigit():
ref_points.add(name)
except:
pass
# Methode 2: Punkte aus Referenzlinien mit expliziten Koordinaten
for line in self.lines.values():
if line.start_point:
# Prüfe ob es ein 5xxx Punkt ist oder explizit Coordinates hat
start_point = self.points.get(line.start_point)
if start_point and start_point.method == 'Coordinates':
ref_points.add(line.start_point)
if line.end_point:
end_point = self.points.get(line.end_point)
if end_point and end_point.method == 'Coordinates':
ref_points.add(line.end_point)
return list(ref_points)
def get_station_points(self) -> List[str]:
"""
Gibt Standpunkte zurück (1000er, 2000er Serie, etc.)
Das sind Punkte, an denen das Instrument aufgestellt wurde.
Gibt eindeutige Namen zurück.
"""
return list(set(station.name for station in self.stations.values() if station.name))
def get_measurement_points(self) -> List[str]:
"""
Gibt reine Messpunkte zurück (3000er Serie, etc.)
Das sind Punkte, die weder Passpunkte noch Standpunkte sind.
"""
ref_points = set(self.get_reference_points())
station_points = set(self.get_station_points())
measurement_points = []
for name, point in self.get_active_points().items():
if name not in ref_points and name not in station_points:
measurement_points.append(name)
return measurement_points
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("")
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)}")
return "\n".join(summary)
def get_calculation_protocol(self) -> str:
"""
Erstellt ein detailliertes Berechnungsprotokoll mit allen Rohdaten
"""
lines = []
lines.append("=" * 80)
lines.append("BERECHNUNGSPROTOKOLL")
lines.append("=" * 80)
lines.append(f"Job: {self.job_name}")
lines.append(f"Datei: {self.file_path}")
lines.append("")
# Koordinatensystem
lines.append("-" * 80)
lines.append("KOORDINATENSYSTEM")
lines.append("-" * 80)
lines.append(f"System: {self.coordinate_system}")
lines.append(f"Zone: {self.zone_name}")
lines.append(f"Datum: {self.datum_name}")
lines.append(f"Winkeleinheit: {self.angle_units}")
lines.append(f"Distanzeinheit: {self.distance_units}")
lines.append("")
# Instrumente
lines.append("-" * 80)
lines.append("INSTRUMENTE")
lines.append("-" * 80)
for inst_id, inst in self.instruments.items():
if inst.model:
lines.append(f" {inst.model} (SN: {inst.serial})")
lines.append(f" Typ: {inst.instrument_type}")
lines.append(f" EDM-Präzision: {inst.edm_precision*1000:.1f} mm + {inst.edm_ppm} ppm")
lines.append(f" Winkel-Präzision: {inst.hz_precision*1000:.3f} mgon")
lines.append("")
# Atmosphäre
lines.append("-" * 80)
lines.append("ATMOSPHÄRISCHE BEDINGUNGEN")
lines.append("-" * 80)
for atm_id, atm in self.atmospheres.items():
lines.append(f" Druck: {atm.pressure:.1f} hPa, Temperatur: {atm.temperature:.1f} °C")
lines.append(f" PPM: {atm.ppm:.2f}, Refraktionskoeff.: {atm.refraction_coefficient:.3f}")
lines.append("")
# Prismenkonstanten
lines.append("-" * 80)
lines.append("PRISMENKONSTANTEN")
lines.append("-" * 80)
seen = set()
for tid, target in self.targets.items():
key = (target.prism_type, target.prism_constant)
if key not in seen:
seen.add(key)
lines.append(f" {target.prism_type}: {target.prism_constant*1000:+.1f} mm")
lines.append("")
# Referenzlinien
if self.lines:
lines.append("-" * 80)
lines.append("REFERENZLINIEN")
lines.append("-" * 80)
for name, line in self.lines.items():
lines.append(f" {name}: {line.start_point}{line.end_point}")
lines.append("")
# Stationierungen
lines.append("=" * 80)
lines.append("STATIONIERUNGEN UND MESSUNGEN")
lines.append("=" * 80)
for station_id, station in sorted(self.stations.items(), key=lambda x: x[1].timestamp):
lines.append("")
lines.append("-" * 80)
lines.append(f"STATION: {station.name}")
lines.append("-" * 80)
lines.append(f" Typ: {station.station_type}")
lines.append(f" Instrumentenhöhe: {station.theodolite_height:.4f} m")
lines.append(f" Maßstab: {station.scale_factor:.8f}")
if station.east is not None:
lines.append(f" Koordinaten: E={station.east:.4f}, N={station.north:.4f}, H={station.elevation or 0:.4f}")
# Backbearing finden
for bb_id, bb in self.backbearings.items():
if bb.station_record_id == station_id:
lines.append(f" Orientierung:")
lines.append(f" Anschlusspunkt: {bb.backsight}")
if bb.face1_hz is not None:
lines.append(f" Hz-Kreis (L1): {bb.face1_hz:.6f} gon")
if bb.face2_hz is not None:
lines.append(f" Hz-Kreis (L2): {bb.face2_hz:.6f} gon")
if bb.orientation_correction is not None:
lines.append(f" Orientierungskorrektur: {bb.orientation_correction:.6f} gon")
# Messungen
measurements = self.get_detailed_measurements_from_station(station_id)
# Anschlussmessungen
backsight_meas = [m for m in measurements if m.classification == 'BackSight' and not m.deleted]
if backsight_meas:
lines.append("")
lines.append(" ANSCHLUSSMESSUNGEN:")
for m in backsight_meas:
lines.append(f" Punkt: {m.point_name}")
if m.horizontal_circle is not None:
lines.append(f" Hz: {m.horizontal_circle:.6f} gon")
if m.vertical_circle is not None:
lines.append(f" V: {m.vertical_circle:.6f} gon")
if m.edm_distance is not None:
lines.append(f" D: {m.edm_distance:.4f} m (Prismenkonstante: {m.prism_constant*1000:+.1f} mm)")
if m.east is not None:
lines.append(f" → E={m.east:.4f}, N={m.north:.4f}, H={m.elevation or 0:.4f}")
# Normale Messungen
normal_meas = [m for m in measurements if m.classification != 'BackSight' and not m.deleted]
if normal_meas:
lines.append("")
lines.append(" MESSUNGEN:")
lines.append(f" {'Punkt':<10} {'Hz [gon]':>14} {'V [gon]':>14} {'D [m]':>12} {'PK [mm]':>10} {'E':>12} {'N':>12} {'H':>10}")
lines.append(" " + "-" * 96)
for m in normal_meas:
hz = f"{m.horizontal_circle:.6f}" if m.horizontal_circle is not None else "-"
v = f"{m.vertical_circle:.6f}" if m.vertical_circle is not None else "-"
d = f"{m.edm_distance:.4f}" if m.edm_distance is not None else "-"
pk = f"{m.prism_constant*1000:+.1f}"
e = f"{m.east:.4f}" if m.east is not None else "-"
n = f"{m.north:.4f}" if m.north is not None else "-"
h = f"{m.elevation:.4f}" if m.elevation is not None else "-"
lines.append(f" {m.point_name:<10} {hz:>14} {v:>14} {d:>12} {pk:>10} {e:>12} {n:>12} {h:>10}")
# Alle berechneten Punkte
lines.append("")
lines.append("=" * 80)
lines.append("BERECHNETE KOORDINATEN")
lines.append("=" * 80)
lines.append(f"{'Punkt':<12} {'East [m]':>14} {'North [m]':>14} {'Elev [m]':>12} {'Methode':<20}")
lines.append("-" * 80)
for name, point in sorted(self.get_active_points().items()):
e = f"{point.east:.4f}" if point.east is not None else "-"
n = f"{point.north:.4f}" if point.north is not None else "-"
h = f"{point.elevation:.4f}" if point.elevation is not None else "-"
lines.append(f"{name:<12} {e:>14} {n:>14} {h:>12} {point.method:<20}")
lines.append("")
lines.append("=" * 80)
lines.append(f"Protokoll erstellt")
lines.append("=" * 80)
return "\n".join(lines)
def create_copy(self):
"""Erstellt eine tiefe Kopie des Parsers"""
return copy.deepcopy(self)