trimble_geodesy/modules/jxl_parser.py

617 lines
22 KiB
Python

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