""" 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 echten Passpunkte zurück. Das sind Punkte mit bekannten Koordinaten, die zur Orientierung verwendet werden. WICHTIG: Standpunkte (1001, 1002 etc.) sind KEINE Festpunkte! Festpunkte sind: - Punkte aus Referenzlinien (5001, 5002) - Punkte mit Method="Coordinates" oder "AzimuthOnly" (und NICHT als Station verwendet) """ ref_points = set() # Alle Stationsnamen sammeln (diese sind KEINE Festpunkte) station_names = set(s.name for s in self.stations.values() if s.name) # Punkte aus Referenzlinien - das sind die echten Passpunkte for line in self.lines.values(): if line.start_point and line.start_point not in station_names: ref_points.add(line.start_point) if line.end_point and line.end_point not in station_names: ref_points.add(line.end_point) # Punkte mit Method="Coordinates" (aber nicht Stationen) for name, point in self.points.items(): if name in station_names: continue # Stationen überspringen if point.method == 'Coordinates': ref_points.add(name) elif point.method == 'AzimuthOnly': ref_points.add(name) 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)