Files
kabellaengen/lib/utils.py
T

650 lines
20 KiB
Python

"""
Common utility functions for the cable routing system.
This module contains frequently used helper functions for:
- File and path operations
- Environment variable handling
- JSON operations
- DXF file handling
- Dictionary operations
"""
import json
import os
import sys
from pathlib import Path
from typing import Any, Tuple
import ezdxf
from ezdxf.lldxf.const import DXFStructureError
# ============================================================================
# File Operations
# ============================================================================
def check_file_in_work(work_dir, filename) -> Tuple[Path, bool]:
"""
Check if a file exists, either at the given path or in the work directory.
Args:
work_dir: Working directory to check if file not found at filename path (str or Path)
filename: Path to the file to check (str or Path)
Returns:
Tuple of (resolved_path, exists_flag)
- resolved_path: Path where file was found (or would be)
- exists_flag: True if file exists, False otherwise
"""
work_dir = Path(work_dir)
filename = Path(filename)
fexists = True
if not filename.exists():
mypath = work_dir.joinpath(filename)
if not mypath.exists():
fexists = False
else:
mypath = filename
return mypath, fexists
# ============================================================================
# Environment Variable Operations
# ============================================================================
def check_environment_var(env_str: str) -> Path:
"""
Get and validate an environment variable as a Path.
Args:
env_str: Name of the environment variable
Returns:
Path object from the environment variable
Exits:
Exits the program if the environment variable is not set or empty
"""
out_path = os.environ.get(env_str)
if out_path:
return Path(out_path)
else:
print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.")
exit()
# ============================================================================
# JSON Operations
# ============================================================================
def to_json(d: Any, pretty: bool = True) -> str:
"""
Convert a Python object to a JSON string.
Args:
d: Object to convert to JSON
pretty: If True, format with indentation for readability
Returns:
JSON string representation of the object
Note:
Uses ensure_ascii=False to properly display German umlauts (ä, ö, ü)
"""
return json.dumps(d, indent=2 if pretty else None, ensure_ascii=False, default=str)
def load_json(jsonfilename: str) -> dict:
"""
Load JSON data from a file.
Args:
jsonfilename: Path to the JSON file
Returns:
Parsed JSON data as a dictionary
"""
with open(jsonfilename, encoding='utf-8') as fh:
return json.load(fh)
def write_results(jsn_results: str, out_dir: Path, filename: str) -> None:
"""
Write JSON results to a file.
Args:
jsn_results: JSON string to write
out_dir: Output directory path
filename: Name of the output file
"""
print("writing results to a json file ...")
outfile = Path(out_dir) / filename
with open(outfile, 'w', encoding='utf-8') as fh:
fh.write(jsn_results)
print("done")
# ============================================================================
# DXF File Operations
# ============================================================================
def dxf_is_binary(dxf_path: Path) -> bool:
"""
Check if a DXF file is in binary format.
Args:
dxf_path: Path to the DXF file
Returns:
True if the file is a binary DXF, False otherwise
"""
with open(dxf_path, 'rb') as f:
header = f.read(22)
return b'AutoCAD Binary DXF' in header
def get_dxf_file(filepath: Path):
"""
Load a DXF file using ezdxf.
Args:
filepath: Path to the DXF file
Returns:
ezdxf Drawing object
Exits:
Exits with code 1 for I/O errors
Exits with code 2 for invalid/corrupted DXF files
"""
try:
print("reading file ..", end='')
doc = ezdxf.filemanagement.readfile(filepath)
print("done")
except IOError:
print("Not a DXF file or a generic I/O error.")
sys.exit(1)
except DXFStructureError:
print("Invalid or corrupted DXF file.")
sys.exit(2)
return doc
# ============================================================================
# ezdxf Operations (High-Level DXF Helpers)
# ============================================================================
def extract_insert_attributes(insert, error_collector=None) -> dict:
"""
Extrahiert alle Attribute aus einem INSERT-Block.
Unterstützt zweistufige Blockstruktur:
- Äußerer Block enthält LWPOLYLINE und INSERT zu Attribut-Block
- Innerer Attribut-Block enthält die eigentlichen Attribute
Args:
insert: INSERT-Entity des Blocks
error_collector: Optional ErrorCollector für Warnungen
Returns:
Dictionary mit allen gefundenen Attributen {tag: text}
"""
attributes = {}
if insert.dxftype() != 'INSERT':
return attributes
# Prüfe zuerst ob der Block direkt Attribute hat (alte Struktur)
direct_attribs = list(insert.attribs)
if direct_attribs:
for attrib in direct_attribs:
tag = attrib.dxf.tag
value = attrib.dxf.text
attributes[tag] = value
return attributes
# Neue zweistufige Struktur: Suche im Block nach einem INSERT mit Attributen
# Dies erfordert das doc-Objekt, daher nur wenn verfügbar
if hasattr(insert, 'doc') and insert.doc:
try:
block_layout = insert.doc.blocks.get(insert.dxf.name)
for entity in block_layout:
if entity.dxftype() == 'INSERT':
inner_attribs = list(entity.attribs)
if inner_attribs:
# Gefunden: innerer Block mit Attributen
for attrib in inner_attribs:
tag = attrib.dxf.tag
value = attrib.dxf.text
attributes[tag] = value
break # Nur das erste INSERT mit Attributen verwenden
except Exception as e:
if error_collector:
error_msg = f"Fehler beim Extrahieren der Attribute aus Block '{insert.dxf.name}': {e}"
if hasattr(error_collector, 'add_warnings'):
error_collector.add_warnings({"attribute_extraction_error": error_msg})
return attributes
def extract_insert_attributes_with_doc(doc, insert, error_collector=None) -> dict:
"""
Extrahiert alle Attribute aus einem INSERT-Block mit explizitem doc-Parameter.
Empfohlen für neue Code, da es zweistufige Blockstrukturen besser unterstützt.
Args:
doc: DXF-Dokument
insert: INSERT-Entity des Blocks
error_collector: Optional ErrorCollector für Warnungen
Returns:
Dictionary mit allen gefundenen Attributen {tag: text}
"""
attributes = {}
if insert.dxftype() != 'INSERT':
return attributes
# Prüfe zuerst ob der Block direkt Attribute hat (alte Struktur)
direct_attribs = list(insert.attribs)
if direct_attribs:
for attrib in direct_attribs:
tag = attrib.dxf.tag
value = attrib.dxf.text
attributes[tag] = value
return attributes
# Neue zweistufige Struktur: Suche im Block nach einem INSERT mit Attributen
try:
block_layout = doc.blocks.get(insert.dxf.name)
for entity in block_layout:
if entity.dxftype() == 'INSERT':
inner_attribs = list(entity.attribs)
if inner_attribs:
# Gefunden: innerer Block mit Attributen
for attrib in inner_attribs:
tag = attrib.dxf.tag
value = attrib.dxf.text
attributes[tag] = value
break # Nur das erste INSERT mit Attributen verwenden
except Exception as e:
if error_collector:
error_msg = f"Fehler beim Extrahieren der Attribute aus Block '{insert.dxf.name}': {e}"
if hasattr(error_collector, 'add_warnings'):
error_collector.add_warnings({"attribute_extraction_error": error_msg})
return attributes
def ensure_layer_exists(doc, layer_name: str, color: int = None, linetype: str = None,
lineweight: int = None, description: str = None):
"""
Stellt sicher, dass ein Layer existiert und erstellt ihn falls nötig.
Args:
doc: DXF-Dokument
layer_name: Name des Layers
color: ACI Color Index (1-255), optional
linetype: Name des Linientyps (z.B. 'CONTINUOUS', 'DASHED'), optional
lineweight: Linienbreite in mm*100 (z.B. 25 für 0.25mm), optional
description: Layer-Beschreibung, optional
Returns:
Layer-Objekt (existierend oder neu erstellt)
"""
if layer_name in doc.layers:
layer = doc.layers.get(layer_name)
else:
layer = doc.layers.add(layer_name)
# Setze optionale Attribute
if color is not None:
layer.color = color
if linetype is not None:
layer.dxf.linetype = linetype
if lineweight is not None:
layer.dxf.lineweight = lineweight
if description is not None:
layer.dxf.description = description
return layer
def add_rectangle_lwpolyline(msp, x: float, y: float, width: float, height: float,
layer: str = '0', color: int = None, rgb: tuple = None,
closed: bool = True):
"""
Fügt eine rechteckige LWPOLYLINE hinzu.
Args:
msp: Modelspace des DXF-Dokuments
x: X-Koordinate der unteren linken Ecke
y: Y-Koordinate der unteren linken Ecke
width: Breite des Rechtecks
height: Höhe des Rechtecks
layer: Layer-Name (default: '0')
color: ACI Color Index (1-255), optional
rgb: RGB-Tuple (r, g, b) mit Werten 0-255, optional
closed: Ob die Polylinie geschlossen sein soll (default: True)
Returns:
LWPOLYLINE-Entity
"""
points = [
(x, y),
(x + width, y),
(x + width, y + height),
(x, y + height)
]
polyline = msp.add_lwpolyline(points, close=closed)
polyline.dxf.layer = layer
if color is not None:
polyline.dxf.color = color
if rgb is not None:
polyline.rgb = rgb
return polyline
def add_text_with_alignment(msp, text: str, x: float, y: float,
height: float = 50, layer: str = '0',
color: int = None, halign: int = 0, valign: int = 0,
rotation: float = 0.0, style: str = 'Standard'):
"""
Fügt einen Text mit Ausrichtung hinzu.
Args:
msp: Modelspace des DXF-Dokuments
text: Textinhalt
x: X-Koordinate
y: Y-Koordinate
height: Texthöhe (default: 50)
layer: Layer-Name (default: '0')
color: ACI Color Index (1-255), optional
halign: Horizontale Ausrichtung (0=links, 1=mitte, 2=rechts)
valign: Vertikale Ausrichtung (0=basis, 1=unten, 2=mitte, 3=oben)
rotation: Rotation in Grad (default: 0.0)
style: Textstil-Name (default: 'Standard')
Returns:
TEXT-Entity
"""
text_entity = msp.add_text(text, height=height)
text_entity.dxf.layer = layer
text_entity.dxf.style = style
text_entity.dxf.rotation = rotation
if color is not None:
text_entity.dxf.color = color
# Setze Ausrichtung
text_entity.set_placement((x, y), align=f"{'LEFT' if halign == 0 else 'CENTER' if halign == 1 else 'RIGHT'}_{'BASELINE' if valign == 0 else 'BOTTOM' if valign == 1 else 'MIDDLE' if valign == 2 else 'TOP'}")
return text_entity
def extract_attributes_with_positions(insert_iterable, round_decimals: int = 1):
"""
Wandelt eine Iterable von INSERT-Objekten in zwei Listen um.
Kompatibilität mit getpositions.py attribs_to_dicts().
Args:
insert_iterable: Iterable von INSERT-Entities (z.B. msp oder iterdxf.modelspace())
round_decimals: Anzahl Nachkommastellen für Positions-Rundung (default: 1)
Returns:
Tuple von (all_inserts, all_positions):
- all_inserts: Liste von Dicts mit Attribut-Tags und deren Textwerten
- all_positions: Liste von Dicts mit Attribut-Tags und deren (x, y, z)-Positionen
Note:
Blöcke ohne Attribute werden übersprungen.
Diese Funktion ist kompatibel mit der alten attribs_to_dicts() aus getpositions.py
"""
all_inserts = []
all_positions = []
for insert in insert_iterable:
if insert.dxftype() != 'INSERT':
continue
itemdata = {}
positions = {}
# Direkte Attribute (alte Struktur)
for attrib in insert.attribs:
if len(insert.attribs) == 0:
continue # Überspringe Blöcke ohne Attribute
attr_tag = attrib.dxf.tag
attr_text = attrib.dxf.text
pos = attrib.dxf.insert
itemdata[attr_tag] = attr_text
positions[attr_tag] = (
round(pos.x, round_decimals),
round(pos.y, round_decimals),
round(pos.z, round_decimals)
)
if len(itemdata) > 0:
all_inserts.append(itemdata)
all_positions.append(positions)
return all_inserts, all_positions
def add_blockref_with_attributes(msp, block_name: str, insert_point: tuple,
attributes: dict = None, layer: str = '0',
scale: float = 1.0, rotation: float = 0.0):
"""
Fügt eine Block-Referenz (INSERT) mit Attributen hinzu.
Args:
msp: Modelspace des DXF-Dokuments
block_name: Name des Blocks
insert_point: Einfügepunkt als (x, y) oder (x, y, z) Tuple
attributes: Dictionary mit Attribut-Namen und -Werten (optional)
layer: Layer-Name (default: '0')
scale: Skalierungsfaktor (default: 1.0)
rotation: Rotation in Grad (default: 0.0)
Returns:
INSERT-Entity mit gesetzten Attributen
"""
# Füge Block-Referenz hinzu
blockref = msp.add_blockref(block_name, insert_point)
blockref.dxf.layer = layer
if scale != 1.0:
blockref.dxf.xscale = scale
blockref.dxf.yscale = scale
blockref.dxf.zscale = scale
if rotation != 0.0:
blockref.dxf.rotation = rotation
# Füge Attribute hinzu falls vorhanden
if attributes:
blockref.add_auto_attribs(attributes)
return blockref
def draw_symbol_frames(doc, msp, symbols, symbol_width=1210, symbol_height=381, frame_layer="SYMBOL_FRAMES"):
"""
Zeichnet Rahmen um alle Symbole zur Visualisierung der Symbol-Grenzen.
Args:
doc: DXF-Dokument
msp: Modelspace
symbols: Liste von Symbol-Dictionaries mit 'entity', 'position', 'attributes'
symbol_width: Feste Breite des Symbols (Standard: 1210 für MA-Frame)
symbol_height: Feste Höhe des Symbols (Standard: 381)
frame_layer: Layer-Name für die Rahmen (Standard: "SYMBOL_FRAMES")
Returns:
Anzahl der gezeichneten Rahmen
"""
print("\n" + "="*60)
print("Zeichne Symbol-Rahmen zur Visualisierung")
print("="*60)
# Erstelle Layer falls nicht vorhanden
if frame_layer not in doc.layers:
doc.layers.add(frame_layer, color=3) # Farbe 3 = Grün
print(f" Layer '{frame_layer}' erstellt (Farbe: Grün)")
frame_count = 0
for symbol in symbols:
# Hole Position und Attribute
pos = symbol['position']
attribs = symbol['attributes']
# Hole IO-Attribut um die Zeichen-Anzahl zu bestimmen
io_value = attribs.get('IO', '')
if not io_value:
# Versuche andere Attribute als Fallback
for key in ['KENNZEICHNUNG', 'NAME']:
if key in attribs and attribs[key]:
io_value = attribs[key]
break
# Bestimme Dimensionen basierend auf Zeichen-Anzahl
# 6 Zeichen (MA-1@@) = 1210, 7 Zeichen (BG-1@@@) = 1410
char_count = len(str(io_value))
if char_count >= 7:
width = 1410 # Multi-Frame (BG/MB/POT)
else:
width = symbol_width # MA-Frame (Standard)
height = symbol_height
# Berechne Eckpunkte (Position = linke untere Ecke des Symbols)
# Symbol erstreckt sich von (x, y) nach rechts und nach oben
x_min = pos[0]
x_max = pos[0] + width
y_min = pos[1]
y_max = pos[1] + height
# Erstelle Rechteck als LWPOLYLINE
points = [
(x_min, y_min),
(x_max, y_min),
(x_max, y_max),
(x_min, y_max),
(x_min, y_min) # Schließe das Rechteck
]
msp.add_lwpolyline(points, dxfattribs={
'layer': frame_layer,
'color': 3, # Grün
'closed': True
})
frame_count += 1
print(f" {frame_count} Symbol-Rahmen gezeichnet")
return frame_count
def query_entities_by_layer(msp, entity_type: str = None, layers: list = None,
exclude_layers: list = None):
"""
Filtert Entities nach Typ und Layer.
Args:
msp: Modelspace des DXF-Dokuments
entity_type: Entity-Typ (z.B. 'INSERT', 'LINE', 'TEXT'), None für alle
layers: Liste der zu inkludierenden Layer (None = alle außer exclude_layers)
exclude_layers: Liste der zu exkludierenden Layer (None = keine Exklusion)
Returns:
Liste der gefilterten Entities
Examples:
# Alle INSERTs auf Layer 'ILS_MOTOR'
query_entities_by_layer(msp, 'INSERT', layers=['ILS_MOTOR'])
# Alle Entities außer auf Layer '0'
query_entities_by_layer(msp, exclude_layers=['0'])
# Alle LINEs auf ILS_* Layern (manuelles Filtern nötig)
all_lines = query_entities_by_layer(msp, 'LINE')
ils_lines = [e for e in all_lines if e.dxf.layer.startswith('ILS_')]
"""
# Baue Query-String
if entity_type:
entities = msp.query(entity_type)
else:
entities = msp
# Filtere nach Layern
result = []
for entity in entities:
layer = entity.dxf.layer
# Prüfe Exklusion
if exclude_layers and layer in exclude_layers:
continue
# Prüfe Inklusion
if layers is None or layer in layers:
result.append(entity)
return result
def create_layers_from_config(doc, layer_definitions: dict):
"""
Erstellt mehrere Layer aus einer Konfiguration.
Args:
doc: DXF-Dokument
layer_definitions: Dictionary mit Layer-Definitionen
Format: {
'layer_name': {
'color': int (optional),
'linetype': str (optional),
'lineweight': int (optional),
'description': str (optional)
},
...
}
Example:
layer_config = {
'ILS_MOTOR': {'color': 1, 'description': 'Motor layer'},
'ILS_SENSOR': {'color': 3, 'lineweight': 25},
'ILS_CABLE': {'color': 5, 'linetype': 'DASHED'}
}
create_layers_from_config(doc, layer_config)
"""
for layer_name, props in layer_definitions.items():
ensure_layer_exists(
doc,
layer_name,
color=props.get('color'),
linetype=props.get('linetype'),
lineweight=props.get('lineweight'),
description=props.get('description')
)
# ============================================================================
# Dictionary Operations
# ============================================================================
def merge_two_dicts(x: dict, y: dict) -> dict:
"""
Merge two dictionaries, with values from y overwriting those in x.
Args:
x: First dictionary (base)
y: Second dictionary (override values)
Returns:
New dictionary with merged values
"""
z = x.copy()
z.update(y)
return z