617 lines
22 KiB
Python
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)
|