From 161a940a3b156f85b6b47302ab3eeb537001da32 Mon Sep 17 00:00:00 2001 From: mistangl Date: Wed, 15 Apr 2026 17:03:26 +0200 Subject: [PATCH] Toter Code und Funktionen entfernt --- doc/Symbole_und_Benennungen.md | 283 +++++++++++++++++++++++++++++++++ lib/getpositions.py | 100 ++---------- lib/getpositions_test.py | 17 +- lib/translate.py | 102 ++++++++---- 4 files changed, 387 insertions(+), 115 deletions(-) create mode 100644 doc/Symbole_und_Benennungen.md diff --git a/doc/Symbole_und_Benennungen.md b/doc/Symbole_und_Benennungen.md new file mode 100644 index 0000000..8f9e3ea --- /dev/null +++ b/doc/Symbole_und_Benennungen.md @@ -0,0 +1,283 @@ +# Symbole und Benennungen im DXF-Layout + +Diese Dokumentation beschreibt, wie Symbole und Texte in der DXF-Zeichnung benannt und strukturiert sein muessen, damit sie vom System erkannt werden. + +--- + +## 1. Sensoren / Aktoren / Motoren (Equipment) + +Equipment wird ueber **INSERT-Bloecke mit Attributen** erkannt. + +### Block-Attribute + +| Attribut-Tag | Bedeutung | Beispiel | Pflicht | +|---|---|---|---| +| `NAME` | Geraete-ID (moderner Block) | `MA0062`, `BG3240` | Ja (oder IO+B) | +| `IO` | Geraete-ID (I/O-Block, Legacy) | `MA0062` | Alternativ zu NAME | +| `B` | Geraete-ID (Technik-Block, Legacy) | `MA0062` | Alternativ zu NAME | +| `ARTINR` | SIVAS-Artikelnummer | `790902001` | Ja | +| `KENNZEICHNUNG` | Routing-Adresse | `=A01+UH01-KF1DQ04` | Ja | +| `SPS` | SPS-Praefix / Anlage | `1` | Ja (fuer Zuordnung) | +| `VERW` | Verwendung/Beschreibung | `CV-M0062_0,75` | Nein | +| `REALE_POSITION` | Marker fuer tatsaechliche Position | `x` | Nein | + +### Geraete-Praefix (erste 2 Zeichen der ID) + +Welche Geraete geroutet werden, bestimmt die `cfg/BMK.cfg`: + +**Routing-Include** (werden geroutet): + +| Praefix | Typ | +|---|---| +| `MA` | Motoren | +| `MB` | Ventile | +| `QM` | Ventile/Pumpen | +| `BG` | Sensoren/Naeherungsschalter | +| `BP` | Naeherungsschalter | +| `BX` | Mehrzweckgeraete | +| `PO` | Pneumatik-Elemente | +| `SF` | Sicherheitselemente | +| `PF` | Programmierbare Logik | + +**Routing-Ignore** (Schaltschrankelemente, nicht geroutet): + +| Praefix | Typ | +|---|---| +| `FC` | Steuerungselemente | +| `UH` | Unterverteiler | +| `UC` | Unterverteiler | +| `UZ` | Spezialverteiler | +| `DI` | Digitale Eingaenge | +| `DQ` | Digitale Ausgaenge | +| `QA` | Ausgabegeraete | + +### KENNZEICHNUNG-Format + +``` +=ANLAGE+VERTEILER-KARTE +``` + +**Beispiel:** `=A01+UH01-KF1DQ04` + +| Teil | Bedeutung | Beispiel | +|---|---|---| +| ANLAGE | Anlagengruppe | `A01` | +| VERTEILER | Unterverteiler-ID | `UH01` | +| KARTE | Karte/Kanal | `KF1DQ04` | + +Trennzeichen: `=` (Start), `+` (zwischen Anlage und Verteiler), `-` (zwischen Verteiler und Karte) + +### Block-Zusammenfuehrung (Legacy Dual-Block) + +Bei der alten Dual-Block-Struktur werden zwei Bloecke mit gleicher ID (z.B. `MA0062`) zusammengefuehrt, wenn: +- Beide innerhalb von **1000mm** Abstand liegen +- Einer das `SPS`-Attribut hat, der andere nicht +- Ergebnis-ID: `{id}@{sps}` (z.B. `MA0062@1`) + +--- + +## 2. Unterverteiler (Distributoren) + +Unterverteiler koennen auf **zwei Arten** definiert werden: als Text oder als Symbol. + +### Variante A: Text (MTEXT) + +Ein MTEXT-Element auf einem erlaubten Layer mit folgendem Muster: + +``` +-UH01 (Minus vor dem Namen) ++UH01 (Plus vor dem Namen) +``` + +Der Text muss den Verteilernamen als Teilstring enthalten, mit `-` oder `+` davor. + +**Erlaubte Layer** (konfiguriert in `cfg/allgemein.cfg`, Sektion `GetPos-Layer_Distributors`): +- `Busverteiler-Kennzeichnung` +- `0-0_ILS_Busverteiler-Kennzeichnung` +- `0-0_ILS_UNTERVERTEILER` +- `0-0_ILS_Unterverteiler` +- `0-0_Omniflo_UNTERVERTEILER` +- `0-0_Omniflo_Unterverteiler` +- `UNTERVERTEILER` +- `0-0_Omniflo_Busverteiler-Kennzeichnung` +- `Schaltschrank-ILS` + +### Variante B: Symbol (INSERT-Block) + +Ein INSERT-Block mit dem Attribut `NAME`, dessen Wert einem **Cabinet-Pattern** entspricht. + +**Cabinet-Patterns** (Regex, konfiguriert in `cfg/BMK.cfg`, Sektion `Cabinet-Pattern`): + +| Pattern | Beispiel-Match | Extrahierte ID | +|---|---|---| +| `A\d\d\+(UH0\d)` | `A01+UH01` | `UH01` | +| `A\d\d\+(UC\d\d\d)` | `A01+UC001` | `UC001` | +| `\+(UC\d\d\d\d)` | `+UC0101` | `UC0101` | +| `\+(UH\d\d)` | `+UH01` | `UH01` | + +Die Klammer-Gruppe im Regex bestimmt die extrahierte Verteiler-ID. + +**Optionale Attribute:** +- `REALE_POSITION` = `x` : Position wird aus Attribut-Position berechnet (Mittelpunkt des Markers) +- `SPS` : SPS-Zuordnung + +--- + +## 3. Tunnel + +Tunnel koennen ebenfalls als **Text** oder **Symbol** definiert werden. + +### Variante A: Text (MTEXT) + +Ein MTEXT-Element auf einem erlaubten Layer mit folgendem Muster: + +``` +TUNNEL1-5 (Tunnelname-Laenge) +TUNNEL2-10 +``` + +**Regex:** `(TUNNEL\d+)-(\d+)` + +| Teil | Bedeutung | Beispiel | +|---|---|---| +| Gruppe 1 | Tunnelname | `TUNNEL1` | +| Gruppe 2 | Laenge in Metern | `5` | + +Pro Tunnel muessen genau **2 MTEXT-Elemente** vorhanden sein (Ein- und Ausgang). + +**Erlaubte Layer** (konfiguriert in `cfg/allgemein.cfg`, Sektion `GetPos-Layer_Tunnel`): +- `Busverteiler-Kennzeichnung` +- `0-0_Tunnel` + +### Variante B: Symbol (INSERT-Block) + +Ein INSERT-Block mit dem Attribut `NAME`, dessen Wert einem **Tunnel-Pattern** entspricht. + +**Tunnel-Patterns** (Regex, konfiguriert in `cfg/BMK.cfg`, Sektion `Tunnel-Pattern`): + +| Pattern | Beispiel | +|---|---| +| `Tunnel\d+` | `Tunnel1`, `Tunnel2` | +| `Tunnel_\d+` | `Tunnel_1`, `Tunnel_2` | +| `Tunnel-\d+` | `Tunnel-1`, `Tunnel-2` | +| `TUNNEL\d+` | `TUNNEL1`, `TUNNEL2` | + +**Attribute:** + +| Attribut-Tag | Bedeutung | Beispiel | Pflicht | +|---|---|---|---| +| `NAME` | Tunnelname | `Tunnel1` | Ja | +| `LAENGE` | Tunnellaenge in Metern | `5` | Nein (Default: 5) | +| `REALE_POSITION` | Positionsmarker | `x` | Nein | + +Pro Tunnel muessen genau **2 Symbole** platziert werden (Ein- und Ausgang). + +**Fehler bei Tunnel-Definition:** +- Weniger als 2 Positionen: Fehler `missing_tunnel` +- Mehr als 2 Positionen: Fehler `overdefined_tunnel` +- Fehlende Laenge: Warnung, Default 5m wird verwendet + +--- + +## 4. Kabelpritschen (Racks) + +Kabelpritschen werden als **LWPOLYLINE** oder **POLYLINE** auf bestimmten Layern erkannt. + +Es sind keine Attribute oder Texte noetig - nur der **Layer** entscheidet. + +**Erlaubte Layer** (konfiguriert in `cfg/allgemein.cfg`, Sektion `GetPos-Layer_Racks`): + +| Layer | Beschreibung | +|---|---| +| `PRITSCHE_100-60` | Standard 100x60 | +| `PRITSCHE_100-60-SCHRAFF` | Standard 100x60 schraffiert | +| `PRITSCHE_200-60` | Standard 200x60 | +| `PRITSCHE_200-60_ILS` | ILS 200x60 | +| `PRITSCHE_200-60_OMNIFLO` | Omniflo 200x60 | +| `0-0_ILS_PRITSCHE_200-60_storage_Level1` | ILS Storage Level 1 | +| `0-0_ILS_PRITSCHE_200-60_storage_Level2` | ILS Storage Level 2 | +| `0-0_ILS_PRITSCHE_200-60_Workstation` | ILS Workstation | +| `0-0_ILS_Pritsche_200-60_AMR` | ILS AMR | +| `0-0_ILS_PRITSCHE_200-60_Highway` | ILS Highway | +| `0-0_ILS_Pritsche_200-60_Inbound` | ILS Inbound | +| `0-0_Omniflo_Pritsche_200-60_Workstation-Outbound` | Omniflo Workstation Outbound | +| `0-0_Omniflo_Pritsche_200-60_outbound` | Omniflo Outbound | +| `0-0_Omniflo_Pritsche_200-60_AMR` | Omniflo AMR | +| `0-0_Omniflo_Pritsche_200-60_Highway` | Omniflo Highway | + +**Validierung:** Wenn die Z-Koordinaten aller Racks mehr als 2000mm auseinanderliegen, wird eine Warnung ausgegeben (Schwellwert konfigurierbar in `cfg/allgemein.cfg`, Sektion `Racks`, Schluessel `MaximalTotalHeightDifferences`). + +--- + +## 5. Zusammenfassung: Was muss im Layout vorhanden sein? + +| Element | Erkennungsart | Mindestangaben | +|---|---|---| +| **Sensor/Motor** | INSERT-Block | `NAME` (oder `IO`+`B`), `ARTINR`, `KENNZEICHNUNG` | +| **Unterverteiler** | MTEXT **oder** INSERT-Block | Text: `-UH01` auf richtigem Layer / Block: `NAME` = `A01+UH01` | +| **Tunnel** | MTEXT **oder** INSERT-Block | Text: `TUNNEL1-5` (2x) / Block: `NAME` = `Tunnel1`, `LAENGE` = `5` (2x) | +| **Kabelpritsche** | LWPOLYLINE/POLYLINE | Auf richtigem Layer gezeichnet | + +--- + +## 6. Funktionsreferenz (getpositions.py) + +Uebersicht der zentralen Funktionen, die die Erkennung durchfuehren. + +### Sensoren / Equipment + +| Funktion | Zeile | Aufgabe | +|---|---|---| +| `get_attributes_of_insert(d_insert, d_pos)` | ~76 | Extrahiert ID, Typ und Position aus einem einzelnen INSERT-Block. Prueft zuerst `IO`, dann `NAME`, dann `B`. Bei `REALE_POSITION='x'` wird der Mittelpunkt aus Marker-Geometrie berechnet (Breite/Hoehe aus Config). Gibt `(dict, id, typ)` zurueck. | +| `extract_input_positions(all_inserts, all_positions, error_collector)` | ~221 | Hauptfunktion: Iteriert ueber alle Bloecke, ruft `get_attributes_of_insert()` auf, trennt Sensoren von Schaltschrankelementen anhand des Praefixes. Fuehrt Block-Zusammenfuehrung via `CompareBuffer` durch. | +| `allocate_blocks_together(...)` | ~329 | Fuehrt die Zusammenfuehrung von Dual-Bloecken durch (Legacy). Nutzt `CompareBuffer.positions_are_close()` mit 1000mm Toleranz. | + +### Unterverteiler + +| Funktion | Zeile | Aufgabe | +|---|---|---| +| `get_subdistributor_positions_from_entities(entities, dist2sensors)` | ~547 | **Text-Erkennung:** Iteriert ueber MTEXT-Entities auf erlaubten Layern. Sucht nach `-{name}` oder `+{name}` im Text. Gibt `{distname: (x, y)}` zurueck. | +| `get_subdistributor_positions_from_symbols(all_inserts, all_positions, dist2sensors)` | ~472 | **Symbol-Erkennung:** Iteriert ueber alle INSERT-Bloecke mit `NAME`-Attribut. Prueft NAME gegen Cabinet-Patterns aus BMK.cfg. Ruft `get_subdistributor_position_of_symbol()` fuer die Positionsextraktion auf. | +| `get_subdistributor_position_of_symbol(d_insert, d_pos)` | ~413 | Hilfsfunktion: Extrahiert Position aus einem einzelnen Unterverteiler-Block. Beruecksichtigt `REALE_POSITION` fuer Mittelpunktberechnung. Gibt `(dict, id)` zurueck. | + +### Tunnel + +| Funktion | Zeile | Aufgabe | +|---|---|---| +| `get_tunnel_positions_from_entities(entities)` | ~574 | **Text-Erkennung:** Iteriert ueber MTEXT-Entities auf erlaubten Layern. Regex `(TUNNEL\d+)-(\d+)` extrahiert Name und Laenge. Sammelt alle Positionen pro Tunnelname. | +| `get_tunnel_positions_from_symbols(all_inserts, all_positions, error_collector)` | ~493 | **Symbol-Erkennung:** Iteriert ueber alle INSERT-Bloecke mit `NAME`-Attribut. Prueft NAME gegen Tunnel-Patterns aus BMK.cfg. Liest `LAENGE`-Attribut (Default: 5m). Warnung bei fehlender Laenge. | +| `get_tunnel_position_of_symbol(d_insert, d_pos)` | ~441 | Hilfsfunktion: Extrahiert Position und Laenge aus einem einzelnen Tunnel-Block. Beruecksichtigt `REALE_POSITION`. Gibt `(dict, id)` zurueck. | + +### Kabelpritschen + +| Funktion | Zeile | Aufgabe | +|---|---|---| +| `get_rack_positions(msp)` | ~614 | Sucht LWPOLYLINE und POLYLINE auf erlaubten Layern. Nummeriert Racks automatisch (`Rack_1`, `Rack_2`, ...). Delegiert an `handle_lwpolyline()` bzw. `handle_polyline()`. | +| `handle_lwpolyline(entity, rack_key, ret)` | ~636 | Verarbeitet 2D-Polylinien. Z-Wert kommt global aus `elevation`. | +| `handle_polyline(entity, rack_key, ret)` | ~644 | Verarbeitet 3D-Polylinien. Jeder Vertex hat individuelle x/y/z-Koordinaten. | +| `check_rack_z_coordinates(res_racks, error_collector, config)` | ~654 | Validierung: Prueft ob Z-Koordinaten-Differenz den Schwellwert uebersteigt. | + +### Erkennungsablauf im Hauptprogramm + +``` +1. DXF laden: get_dxf_file() -> msp +2. Alle Bloecke extrahieren: attribs_to_dicts(msp) -> all_inserts, all_positions +3. Sensoren: extract_input_positions(all_inserts, all_positions) +4. Mappings: create_mappings(res_sens) -> sensor2unterverteiler +5. Unterverteiler (Text): get_subdistributor_positions_from_entities(msp.query('MTEXT')) +6. Unterverteiler (Symbol): get_subdistributor_positions_from_symbols(all_inserts, all_positions) +7. Tunnel (Text): get_tunnel_positions_from_entities(msp.query('MTEXT')) +8. Tunnel (Symbol): get_tunnel_positions_from_symbols(all_inserts, all_positions) +9. Racks: get_rack_positions(msp) +``` + +--- + +## 7. Konfigurationsdateien + +| Datei | Inhalt | +|---|---| +| `cfg/allgemein.cfg` | Layer-Zuordnungen, Geometrie-Parameter, Toleranzen | +| `cfg/BMK.cfg` | Geraete-Praefixe, Cabinet-Patterns, Tunnel-Patterns, Kabelzuordnungen | +| `cfg/kabel.cfg` | SIVAS-Artikelnummern nach Kabeltyp und Laenge | +| `cfg/bezeichner.cfg` | Artikelnummer-Beschreibungen | diff --git a/lib/getpositions.py b/lib/getpositions.py index e81f895..4aff004 100644 --- a/lib/getpositions.py +++ b/lib/getpositions.py @@ -6,16 +6,12 @@ import re import sys from pathlib import Path -import ezdxf -from ezdxf.addons import iterdxf from shapely.geometry import Point -from ezdxf.lldxf.const import DXFStructureError -from error_collector import ErrorCollector, write_json_file +from error_collector import ErrorCollector from utils import ( check_environment_var, check_file_in_work, - dxf_is_binary, get_dxf_file, merge_two_dicts, to_json, @@ -551,10 +547,10 @@ def get_tunnel_positions_from_symbols(all_inserts: list, all_positions: list, er def get_subdistributor_positions_from_entities(entities, dist2sensors: dict) -> dict: """Ermittelt Unterverteiler-Positionen aus einer beliebigen Entity-Iterable. - Erwartet eine Iterable von DXF-Entities (z. B. aus `msp.query("MTEXT")` oder - `iterdxf.modelspace(dxf_path)`) und filtert nach den in der Config erlaubten - Layern. Es werden beide bisher verwendeten Suchmuster in der MTEXT-Zeile - unterstützt ("-" und "+"). + Erwartet eine Iterable von DXF-Entities (z. B. aus `msp.query("MTEXT")`) + und filtert nach den in der Config erlaubten Layern. Es werden beide bisher + verwendeten Suchmuster in der MTEXT-Zeile unterstützt ("-" und + "+"). """ ret = {} all_distributors = dist2sensors.keys() @@ -578,10 +574,10 @@ def get_subdistributor_positions_from_entities(entities, dist2sensors: dict) -> def get_tunnel_positions_from_entities(entities) -> dict: """Ermittelt Tunnel-Ein/Ausgangs-Positionen aus einer beliebigen Entity-Iterable. - Erwartet eine Iterable von DXF-Entities (z. B. aus `msp.query("MTEXT")` oder - `iterdxf.modelspace(dxf_path)`) und filtert nach den in der Config erlaubten - Layern. Erkennt Tunnel anhand des Musters "TUNNEL-" und sammelt - pro Tunnelname die gefundenen Positionen sowie die Länge. + Erwartet eine Iterable von DXF-Entities (z. B. aus `msp.query("MTEXT")`) + und filtert nach den in der Config erlaubten Layern. Erkennt Tunnel anhand + des Musters "TUNNEL-" und sammelt pro Tunnelname die gefundenen + Positionen sowie die Länge. """ all_tunnels = dict() tunnel_length = dict() @@ -614,17 +610,6 @@ def get_tunnel_positions_from_entities(entities) -> dict: all_tunnels['length'] = tunnel_length return all_tunnels -# helper function -def print_line(e) -> None: - print(f"LINE on layer: {e.dxf.layer}\n") - print(f"points: {repr(e.dxf)}\n") - -def print_polyline(e) -> None: - print(f"POLYLINE on layer: {e.dxf.layer}\n") - for x, y, start_width, end_width, bulge in e.get_points(): - print(f" Punkt: ({x}, {y}), Startbreite: ({start_width}, Endbreite: {end_width})") - if e.is_closed: - print("Diese Polyline ist geschlossen.") def get_rack_positions(msp) -> dict: """Hole alle Positionen aller Kabelpritschen und nummeriere Racks.""" @@ -647,30 +632,6 @@ def get_rack_positions(msp) -> dict: return ret -def get_rack_positions_iter(dxf_path) -> dict: - """Hole alle Positionen aller Kabelpritschen (Racks) mithilfe von iterdxf.""" - ret = dict() - rack_counter = 1 - all_layers = config.items('GetPos-Layer_Racks') - - for entity in iterdxf.modelspace(dxf_path): - layer = entity.dxf.layer - - if not any(layer == cfg_layer for cfg_layer, _ in all_layers): - continue - - rack_key = f"Rack_{rack_counter}" - - if entity.dxftype() == "LWPOLYLINE": - handle_lwpolyline(entity, rack_key, ret) - elif entity.dxftype() == "POLYLINE": - handle_polyline(entity, rack_key, ret) - else: - continue - - rack_counter += 1 - - return ret def handle_lwpolyline(entity, rack_key: str, ret: dict) -> None: """Verarbeitet eine 2D LWPOLYLINE mit globalem Z-Wert (elevation).""" @@ -689,15 +650,6 @@ def handle_polyline(entity, rack_key: str, ret: dict) -> None: z = vertex.dxf.location.z ret[rack_key].append([round(x, 1), round(y, 1), round(z, 1)]) -def scan(dxf_source) -> dict: - layer_names_inside = list(dxf_source.layers.names()) - alle_block_defs = set(dxf_source.blocks.block_names()) - used_block_names = set(insert.dxf.name for insert in dxf_source.modelspace().query("INSERT")) - ret = dict() - ret['all_layers'] = layer_names_inside - ret['used_blocks'] = used_block_names - ret['all_blocks'] = alle_block_defs - return ret def check_rack_z_coordinates(res_racks: dict, error_collector, config) -> None: """ @@ -828,17 +780,10 @@ if __name__ == '__main__': parser.print_help() exit() - if dxf_is_binary(dxf_path): # Wenn dxf eine binary ist, dann komplett parsen und modelspace anlegen - print("Given .dxf-file is binary dxf. Proceeding to read file. Watch RAM-usage.") - doc = get_dxf_file(dxf_path) - msp = doc.modelspace() - use_iter = False - else: - print("Given .dxf-file is ASCII-dxf. Proceeding to use iterative functions. Process may take longer.") - use_iter = True + doc = get_dxf_file(dxf_path) + msp = doc.modelspace() res_sens = dict() - res_cables = dict() res_dist = dict() res_rac = dict() res_mappings = dict() @@ -870,16 +815,12 @@ if __name__ == '__main__': if args.sensors: # Sensoren auslesen aus den Symbolen - if use_iter: - all_inserts, all_positions = attribs_to_dicts(iterdxf.modelspace(dxf_path)) - else: - all_inserts, all_positions = attribs_to_dicts(msp) + all_inserts, all_positions = attribs_to_dicts(msp) res_sens, res_schaltschrank_elemente = extract_input_positions(all_inserts, all_positions, error_collector) output_results['sensors'] = res_sens output_results['schaltschrank_elemente'] = res_schaltschrank_elemente - #output_results['cables'] = res_cables - + if args.console: print(to_json(res_sens)) @@ -893,10 +834,7 @@ if __name__ == '__main__': print(to_json(res_mappings)) # Distributoren auslesen (generisch über Entities) - if use_iter: - entities = iterdxf.modelspace(dxf_path) - else: - entities = msp.query('MTEXT') + entities = msp.query('MTEXT') # die Infos aus den Texten (alter Stil) res_dist = get_subdistributor_positions_from_entities(entities, res_mappings) # die Infos aus den Blöcken @@ -907,10 +845,7 @@ if __name__ == '__main__': print(to_json(res_dist)) # Tunnel auslesen (generisch über Entities) - if use_iter: - t_entities = iterdxf.modelspace(dxf_path) - else: - t_entities = msp.query('MTEXT') + t_entities = msp.query('MTEXT') # die Infos aus den Texten (alter Stil) res_tunnel = get_tunnel_positions_from_entities(t_entities) # die Infos aus den Blöcken (neuer Stil) @@ -921,10 +856,7 @@ if __name__ == '__main__': print(to_json(res_tunnel)) if args.rack: - if use_iter: - res_rac = get_rack_positions_iter(dxf_path) - else: - res_rac = get_rack_positions(msp) + res_rac = get_rack_positions(msp) output_results['racks'] = res_rac diff --git a/lib/getpositions_test.py b/lib/getpositions_test.py index 804562e..39a2bca 100644 --- a/lib/getpositions_test.py +++ b/lib/getpositions_test.py @@ -4,6 +4,7 @@ Test für die check_rack_z_coordinates Funktion """ +import configparser import sys from pathlib import Path @@ -13,6 +14,14 @@ sys.path.insert(0, str(Path(__file__).parent)) from getpositions import check_rack_z_coordinates from error_collector import ErrorCollector + +def _make_config(max_height_diff=2000.0): + """Erzeugt ein minimales Config-Objekt für die Tests.""" + config = configparser.ConfigParser() + config.add_section("Racks") + config.set("Racks", "MaximalTotalHeightDifferences", str(max_height_diff)) + return config + def test_no_deviation(): """Test mit Racks ohne starke Abweichung (< 2000mm)""" print("Test 1: Keine starke Abweichung (< 2000mm)") @@ -24,7 +33,7 @@ def test_no_deviation(): } error_collector = ErrorCollector() - check_rack_z_coordinates(racks, error_collector) + check_rack_z_coordinates(racks, error_collector, _make_config()) warnings = error_collector.warnings if warnings: @@ -44,7 +53,7 @@ def test_with_deviation(): } error_collector = ErrorCollector() - check_rack_z_coordinates(racks, error_collector) + check_rack_z_coordinates(racks, error_collector, _make_config()) warnings = error_collector.warnings if warnings: @@ -63,7 +72,7 @@ def test_exactly_2000mm(): } error_collector = ErrorCollector() - check_rack_z_coordinates(racks, error_collector) + check_rack_z_coordinates(racks, error_collector, _make_config()) warnings = error_collector.warnings if warnings: @@ -82,7 +91,7 @@ def test_dict_format(): } error_collector = ErrorCollector() - check_rack_z_coordinates(racks, error_collector) + check_rack_z_coordinates(racks, error_collector, _make_config()) warnings = error_collector.warnings if warnings: diff --git a/lib/translate.py b/lib/translate.py index ccc3ea1..deba629 100644 --- a/lib/translate.py +++ b/lib/translate.py @@ -517,10 +517,10 @@ def load_ignore_patterns(cfg_path: Path) -> tuple[list[str], list[str]]: return wildcard_patterns, regex_patterns -def translate_text(text: str, multi: dict[str, str], trigramme: dict[str, str], bigramme: dict[str, str], single: dict[str, str]) -> str: +def translate_text(text: str, multi: dict[str, str], trigramme: dict[str, str], bigramme: dict[str, str], single: dict[str, str], transfer_terms: set[str] | None = None) -> str: """ Übersetzt einen Text durch Ersetzen von Multi-Phrasen, Trigrammen, Bigrammen und Einzelwörtern. - Ersetzungen erfolgen in dieser Reihenfolge: Multi → Trigramme → Bigramme → Einzelwörter. + Ersetzungen erfolgen in dieser Reihenfolge: Transfer → Multi → Trigramme → Bigramme → Einzelwörter. Bei Einzelwörtern werden auch Satzzeichen und eingebettete Wörter berücksichtigt: - "Weight:" wird zu "Hmotnost:" (Doppelpunkt bleibt erhalten) @@ -529,16 +529,23 @@ def translate_text(text: str, multi: dict[str, str], trigramme: dict[str, str], (alle Wörter in einem Token werden durchsucht, Trennzeichen bleiben erhalten) Bindestriche im Wort (wie "Sub-distributor") bleiben Teil des Kern-Worts. + Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung) wenn der Text exakt übereinstimmt. + Args: text: Zu übersetzender Text multi: Dictionary mit Multi-Wort-Phrasen (4+ Wörter) trigramme: Dictionary mit Drei-Wort-Phrasen bigramme: Dictionary mit Zwei-Wort-Phrasen single: Dictionary mit Einzelwörtern + transfer_terms: Set mit Begriffen die 1:1 übernommen werden (aus [transfer] Sektion) Returns: Übersetzter Text (leer wenn keine Übersetzung gefunden wurde) """ + # 0. Prüfe Transfer-Begriffe (exakte Übereinstimmung, 1:1 Übernahme) + if transfer_terms is not None and text in transfer_terms: + return text + translated_text = text has_translation = False @@ -1668,7 +1675,8 @@ def _retranslate_language_block( trigramme: dict[str, str], bigramme: dict[str, str], single: dict[str, str], - label: str) -> tuple[int, int, int]: + label: str, + transfer_terms: set[str] | None = None) -> tuple[int, int, int, int]: """ Verarbeitet einen einzelnen Sprach-Block einer JSON-Übersetzungsdatei in-place. @@ -1687,6 +1695,8 @@ def _retranslate_language_block( Einträge mit bereits gesetztem target (manuell befüllt) werden ebenfalls direkt nach translations_list übernommen. + Schritt 3 – Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung). + Args: translations_list: Liste der bereits übersetzten Einträge (in-place verändert) untranslated_list: Liste der noch nicht übersetzten Einträge (in-place verändert) @@ -1695,13 +1705,15 @@ def _retranslate_language_block( bigramme: Bigramme-Dict single: Einzelwörter-Dict label: Bezeichnung für Warnmeldungen, z.B. "[CS]" + transfer_terms: Set mit Begriffen die 1:1 übernommen werden (aus [transfer] Sektion) Returns: - Tuple (aktualisiert, unveraendert, neu_aus_untranslated) + Tuple (aktualisiert, unveraendert, neu_aus_untranslated, transferred) """ updated = 0 unchanged = 0 newly = 0 + transferred = 0 # Schritt 0: Deduplizierung – erstes Vorkommen je source wird behalten deduped_t, seen_t = _dedup_source_list( @@ -1718,7 +1730,7 @@ def _retranslate_language_block( continue source = entry['source'] old_target = entry.get('target', '') - new_target = translate_text(source, multi, trigramme, bigramme, single) + new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms) if new_target: if new_target != old_target: entry['target'] = new_target @@ -1743,18 +1755,23 @@ def _retranslate_language_block( newly += 1 continue source = entry['source'] - new_target = translate_text(source, multi, trigramme, bigramme, single) + new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms) if new_target: translations_list.append({"source": source, "target": new_target}) untranslated_list.remove(entry) - newly += 1 + # Prüfe ob es ein Transfer-Begriff war + if transfer_terms and source in transfer_terms: + transferred += 1 + else: + newly += 1 - return updated, unchanged, newly + return updated, unchanged, newly, transferred def _retranslate_legacy(data: dict, translation_languages: list[str], - translation_dicts_all: dict[str, tuple]) -> dict: + translation_dicts_all: dict[str, tuple], + transfer_terms_all: dict[str, set[str]] | None = None) -> dict: """ Verarbeitet eine Legacy-Flat-JSON-Struktur und konvertiert sie ins Multilang-Format. @@ -1762,6 +1779,7 @@ def _retranslate_legacy(data: dict, data: Geladene JSON-Daten (flache Struktur mit 'translations'/'untranslated') translation_languages: Sprachen in der Reihenfolge der Verarbeitung translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...} + transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion) Returns: Konvertierte Daten im Multilang-Format @@ -1778,11 +1796,13 @@ def _retranslate_legacy(data: dict, lang = translation_languages[0] multi, trigramme, bigramme, single = translation_dicts_all[lang] + transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set() - updated, unchanged, newly = _retranslate_language_block( + updated, unchanged, newly, transferred = _retranslate_language_block( data['translations'], data['untranslated'], multi, trigramme, bigramme, single, - f"[{lang}]" + f"[{lang}]", + transfer_terms ) print(f"\nRe-Übersetzung abgeschlossen (alte Struktur → wird zu multilang konvertiert, Sprache: {lang}):") @@ -1790,6 +1810,8 @@ def _retranslate_legacy(data: dict, print(f" Unverändert: {unchanged}") if newly: print(f" Neu aus 'untranslated' übersetzt/verschoben: {newly}") + if transferred: + print(f" Transfer-Begriffe (1:1): {transferred}") # Legacy → Multilang konvertieren: flache Struktur in [LANG]-Block einpacken return { @@ -1804,7 +1826,8 @@ def _retranslate_legacy(data: dict, def _retranslate_multilang(data: dict, existing_languages: list[str], translation_languages: list[str], - translation_dicts_all: dict[str, tuple]) -> None: + translation_dicts_all: dict[str, tuple], + transfer_terms_all: dict[str, set[str]] | None = None) -> None: """ Verarbeitet eine Multilang-JSON-Struktur in-place. @@ -1813,12 +1836,14 @@ def _retranslate_multilang(data: dict, existing_languages: Sprachen die bereits in der Datei vorhanden sind translation_languages: Sprachen in der Reihenfolge der Verarbeitung translation_dicts_all: Dict mit {LANG: (multi, trigramme, bigramme, single), ...} + transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion) """ print(f"Erkannte mehrsprachige Struktur mit Sprachen: {', '.join(existing_languages)}") total_updated = 0 total_unchanged = 0 total_newly = 0 + total_transferred = 0 new_block_count = 0 for lang in translation_languages: @@ -1844,30 +1869,38 @@ def _retranslate_multilang(data: dict, continue multi, trigramme, bigramme, single = translation_dicts_all[lang] - updated, unchanged, newly = _retranslate_language_block( + transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set() + updated, unchanged, newly, transferred = _retranslate_language_block( lang_data['translations'], lang_data['untranslated'], multi, trigramme, bigramme, single, - f"[{lang}]" + f"[{lang}]", + transfer_terms ) total_updated += updated + newly total_unchanged += unchanged total_newly += newly + total_transferred += transferred print(f" [{lang}] Aktualisiert: {updated}, Unverändert: {unchanged}", end="") if newly: print(f", Neu aus 'untranslated': {newly}", end="") + if transferred: + print(f", Transfer-Begriffe (1:1): {transferred}", end="") print() print(f"\nRe-Übersetzung abgeschlossen (mehrsprachige Struktur):") print(f" Gesamt aktualisiert: {total_updated}") print(f" Gesamt unverändert: {total_unchanged}") + if total_transferred: + print(f" Gesamt Transfer-Begriffe (1:1): {total_transferred}") if new_block_count: print(f" Neue Sprach-Blöcke erstellt: {new_block_count}") def retranslate_json_file(json_path: Path, translation_dicts_all: dict[str, tuple], - translation_languages: list[str]): + translation_languages: list[str], + transfer_terms_all: dict[str, set[str]] | None = None): """ Re-übersetzt eine JSON-Datei mit neuen Übersetzungs-Configs. Überschreibt die ursprüngliche JSON-Datei mit den aktualisierten Übersetzungen. @@ -1894,9 +1927,9 @@ def retranslate_json_file(json_path: Path, structure_type, existing_languages = detect_json_structure(data) if structure_type == "legacy": - data = _retranslate_legacy(data, translation_languages, translation_dicts_all) + data = _retranslate_legacy(data, translation_languages, translation_dicts_all, transfer_terms_all) elif structure_type == "multilang": - _retranslate_multilang(data, existing_languages, translation_languages, translation_dicts_all) + _retranslate_multilang(data, existing_languages, translation_languages, translation_dicts_all, transfer_terms_all) else: print(f"FEHLER: JSON-Datei hat unbekannte Struktur") sys.exit(1) @@ -2004,7 +2037,8 @@ def retranslate_txt_file(txt_path: Path, translation_dicts_all: dict[str, tuple], translation_languages: list[str], wildcard_patterns: list[str] | None = None, - regex_patterns: list[str] | None = None) -> None: + regex_patterns: list[str] | None = None, + transfer_terms_all: dict[str, set[str]] | None = None) -> None: """ Re-übersetzt eine Textdatei im Format von write_texts_to_text. Überschreibt die ursprüngliche Datei mit aktualisierten Inhalten. @@ -2015,6 +2049,7 @@ def retranslate_txt_file(txt_path: Path, 2. Einträge aus untranslated werden gegen die Ignore-Pattern geprüft: - Treffer → ignored (neu erkannte Pattern seit dem letzten Extrakt) - Kein Treffer → auto-übersetzen; Erfolg → translations, sonst bleibt in untranslated. + 3. Transfer-Begriffe werden 1:1 übernommen (ohne Übersetzung). Arbeitet immer mit der ersten Sprache aus translation_languages. @@ -2024,6 +2059,7 @@ def retranslate_txt_file(txt_path: Path, translation_languages: Liste der Sprachen (nur die erste wird verwendet) wildcard_patterns: Wildcard-Muster zum Filtern (aus translator.cfg) regex_patterns: Regex-Muster zum Filtern (aus translator.cfg) + transfer_terms_all: Dict mit {LANG: set[transfer_terms], ...} (aus [transfer] Sektion) """ if not translation_languages or translation_languages[0] not in translation_dicts_all: print(f"FEHLER: Keine Übersetzungs-Config für Re-Übersetzung verfügbar") @@ -2034,6 +2070,7 @@ def retranslate_txt_file(txt_path: Path, lang = translation_languages[0] multi, trigramme, bigramme, single = translation_dicts_all[lang] + transfer_terms = transfer_terms_all.get(lang, set()) if transfer_terms_all else set() existing_translations, untranslated, ignored = _parse_txt_sections(txt_path) @@ -2048,7 +2085,7 @@ def retranslate_txt_file(txt_path: Path, # Schritt 1: Bestehende Übersetzungen neu übersetzen for source, old_target in existing_translations: - new_target = translate_text(source, multi, trigramme, bigramme, single) + new_target = translate_text(source, multi, trigramme, bigramme, single, transfer_terms) if new_target: new_translations.append((source, new_target)) if new_target != old_target: @@ -2064,16 +2101,21 @@ def retranslate_txt_file(txt_path: Path, still_untranslated: list[str] = list(moved_to_untranslated) newly = 0 newly_ignored = 0 + transferred = 0 for text in untranslated: # Neu hinzugefügte Ignore-Pattern prüfen if should_ignore_text(text, wildcard_patterns, regex_patterns): ignored.append(text) newly_ignored += 1 continue - new_target = translate_text(text, multi, trigramme, bigramme, single) + new_target = translate_text(text, multi, trigramme, bigramme, single, transfer_terms) if new_target: new_translations.append((text, new_target)) - newly += 1 + # Prüfe ob es ein Transfer-Begriff war + if transfer_terms and text in transfer_terms: + transferred += 1 + else: + newly += 1 else: still_untranslated.append(text) @@ -2098,6 +2140,8 @@ def retranslate_txt_file(txt_path: Path, print(f" Unverändert: {unchanged}") if newly: print(f" Neu übersetzt: {newly}") + if transferred: + print(f" Transfer-Begriffe (1:1): {transferred}") if newly_ignored: print(f" Neu ignoriert (Pattern): {newly_ignored}") if moved_to_untranslated: @@ -2112,7 +2156,8 @@ def _run_retranslate_txt( translation_dicts_all: dict, translation_languages: list, wildcard_patterns: list, - regex_patterns: list) -> None: + regex_patterns: list, + transfer_terms_all: dict) -> None: """Workflow: Re-Übersetzung einer bestehenden TXT-Datei (--retranslate-txt).""" print("=== Re-Übersetzung TXT-Datei ===\n") @@ -2127,7 +2172,7 @@ def _run_retranslate_txt( print(f"Sprache: {translation_languages[0]}") retranslate_txt_file(txt_file, translation_dicts_all, translation_languages, - wildcard_patterns, regex_patterns) + wildcard_patterns, regex_patterns, transfer_terms_all) print("\nVerarbeitung erfolgreich abgeschlossen.") sys.exit(0) @@ -2227,7 +2272,8 @@ def _run_retranslate( args, work_dir: Path, translation_dicts_all: dict, - translation_languages: list) -> None: + translation_languages: list, + transfer_terms_all: dict) -> None: """Workflow: Re-Übersetzung einer bestehenden JSON-Datei (--retranslate-json).""" print("=== Re-Übersetzung JSON-Datei ===\n") @@ -2241,7 +2287,7 @@ def _run_retranslate( print(f"JSON-Datei: {json_file}") print(f"Sprachen: {', '.join(translation_languages)}") - retranslate_json_file(json_file, translation_dicts_all, translation_languages) + retranslate_json_file(json_file, translation_dicts_all, translation_languages, transfer_terms_all) print("\nVerarbeitung erfolgreich abgeschlossen.") sys.exit(0) @@ -2451,10 +2497,12 @@ def main() -> None: # Übersetzungs-Configs laden: {LANG: (multi, trigramme, bigramme, single), ...} translation_dicts_all = {} + transfer_terms_all = {} if translation_languages: translation_dir = check_environment_var('PROJECT_TRANSLATION') for lang in translation_languages: translation_dicts_all[lang] = load_translation_config(lang, translation_dir) + transfer_terms_all[lang] = load_transfer_terms(lang, translation_dir) # Für backwards compatibility: bei genau einer Sprache flaches Dict bereitstellen translation_dicts = translation_dicts_all.get(translation_languages[0]) if len(translation_languages) == 1 else None @@ -2464,11 +2512,11 @@ def main() -> None: # Workflow delegieren if args.retranslate_json: - _run_retranslate(args, work_dir, translation_dicts_all, translation_languages) + _run_retranslate(args, work_dir, translation_dicts_all, translation_languages, transfer_terms_all) if args.retranslate_txt: _run_retranslate_txt(args, work_dir, translation_dicts_all, translation_languages, - wildcard_patterns, regex_patterns) + wildcard_patterns, regex_patterns, transfer_terms_all) if args.todxf: _run_todxf(args, work_dir, translation_languages, wildcard_patterns, regex_patterns,