""" 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)