diff --git a/lib/set_koords.py b/lib/set_koords.py index 1c88e62..43c6eef 100644 --- a/lib/set_koords.py +++ b/lib/set_koords.py @@ -13,9 +13,32 @@ Schalter: Weichen: Beginn der untersten vertikalen Linie, x oben, y links, z aus Zeichenebene. --k2set K2 an alle Boegen und Weichen setzen. - Boegen: anderes Kettenende, x tangential, - y senkrecht, z wie K1. - Weichen: noch nicht implementiert (Dummy). + Nutzt dieselben Schalter-Filter wie set_einfuegepkt.py + (SWITCH_FILTERS aus utils.py). + + Boegen: + Position am anderen Ende der Kette (Gerade-Bogen-Gerade). + x-Achse tangential zur Linie, zur Bogenmitte zeigend. + y senkrecht dazu, z wie K1 (aus Zeichenebene). + 180-Grad-Sonderfall: x nach rechts (rz=0). + + Weichen (weichen45, weichen90, weichen_parallel): + Folgt dem Bogenzweig bis zum Ende. + x tangential zur Linie, zur Kurve zeigend. + Bei Doppelweichen: linker Zweig. + y senkrecht dazu, z wie K1. + + --k3set K3 an Weichen mit 2+ Boegen setzen (rechter Zweig). + Gleiche Logik wie K2, aber rechter Ast (groesstes x). + Nur bei: Doppelweichen, Dreiwegeweichen, + Dreifachweiche, Delta, Stern, Weichenkoerper Doppel/Dreiwege. + --k4set K4 an Weichen setzen (oberes Ende der K1-Linie). + K4 liegt K1 gegenueber, am oberen Ende derselben V-Linie. + Rotation wie K1: x oben, y links, z aus Zeichenebene. + Kein K4 bei: Boegen, Doppelweiche, Dreifachweiche, Delta. + --set-all Alle Koordinatensysteme (K1-K4) in einem Durchgang setzen. + --force Bestehende Kx ueberschreiben (sonst skip wenn vorhanden). + Duplikate werden immer bereinigt (letztes bleibt). --show-omniflo Uebersichts-DXF mit K-Positionen als farbige Kreuze. --test Erzeugt Testdatei mit 4 KOS und verifiziert diese. --number SIVA Nur diese eine 9-stellige Sivasnr verarbeiten. @@ -256,6 +279,25 @@ def k1_point_weiche(doc): return (x, bottom_y, 0), 0, 0, 90 +# --------------------------------------------------------------------------- +# K2-Positionsbestimmung (nur Boegen) +# --------------------------------------------------------------------------- + +def k4_point_weiche(doc): + """K4-Position fuer Weichen: oberes Ende der K1-Vertikallinie. + + K4 liegt K1 gegenueber, am oberen Ende derselben vertikalen Linie. + Rotation wie K1: x oben, y links, z aus Zeichenebene (rz=90). + """ + vline = find_bottommost_vertical_line(doc) + if vline is None: + return None + s, e = vline.dxf.start, vline.dxf.end + x = s[0] + top_y = max(s[1], e[1]) + return (x, top_y, 0), 0, 0, 90 + + # --------------------------------------------------------------------------- # K2-Positionsbestimmung (nur Boegen) # --------------------------------------------------------------------------- @@ -346,65 +388,329 @@ def k2_point_bogen(doc): # --------------------------------------------------------------------------- -# K2-Positionsbestimmung Weichen (Dummy-Funktionen) +# K2-Positionsbestimmung Weichen # --------------------------------------------------------------------------- -def k2_point_weichen45(doc): - """K2 fuer Weichen 45 Grad – noch nicht implementiert.""" - return None +def _find_weiche_arc_branches(doc): + """Findet alle Bogenzweig-Endpunkte einer Weiche. + + Returns: + Liste von (k2_pos, junc_pt) Tupeln, sortiert nach x (links zuerst), + oder None falls keine Boegen gefunden. + """ + msp = doc.modelspace() + + # K1-Position bestimmen (unterste vertikale Linie) + vline = find_bottommost_vertical_line(doc) + if vline is None: + return None + s, e = vline.dxf.start, vline.dxf.end + k1_x = s[0] + k1_y = min(s[1], e[1]) + + # Alle Boegen sammeln + arcs = [e for e in msp if e.dxftype() == "ARC"] + if not arcs: + return None + + tol = 1.0 + + # Fuer jeden Bogen: K2-seitiges Ende bestimmen und Endpunkt der Linie finden + candidates = [] + for arc in arcs: + cx, cy = arc.dxf.center[0], arc.dxf.center[1] + r = arc.dxf.radius + sa, ea = arc.dxf.start_angle, arc.dxf.end_angle + + arc_start = (cx + r * math.cos(math.radians(sa)), + cy + r * math.sin(math.radians(sa))) + arc_end = (cx + r * math.cos(math.radians(ea)), + cy + r * math.sin(math.radians(ea))) + + # K1-Seite = naeher an K1 + d_start = ((arc_start[0] - k1_x)**2 + (arc_start[1] - k1_y)**2) ** 0.5 + d_end = ((arc_end[0] - k1_x)**2 + (arc_end[1] - k1_y)**2) ** 0.5 + + if d_start < d_end: + k2_junction = arc_end + else: + k2_junction = arc_start + + # Laengste Linie am K2-Bogenende suchen + best_len = 0 + k2_pos = None + junc_pt = None + for ent in msp: + if ent.dxftype() != "LINE": + continue + ls = (ent.dxf.start[0], ent.dxf.start[1]) + le = (ent.dxf.end[0], ent.dxf.end[1]) + ds = ((ls[0] - k2_junction[0])**2 + (ls[1] - k2_junction[1])**2) ** 0.5 + de = ((le[0] - k2_junction[0])**2 + (le[1] - k2_junction[1])**2) ** 0.5 + if ds > tol and de > tol: + continue + ln = ((le[0] - ls[0])**2 + (le[1] - ls[1])**2) ** 0.5 + if ln > best_len: + best_len = ln + if ds < tol: + k2_pos = le + junc_pt = ls + else: + k2_pos = ls + junc_pt = le + + if k2_pos is not None: + candidates.append((k2_pos, junc_pt)) + + if not candidates: + return None + + # Sortieren nach x: links (kleinstes x) zuerst, rechts (groesstes x) zuletzt + candidates.sort(key=lambda c: c[0][0]) + return candidates -def k2_point_weichen90(doc): - """K2 fuer Weichen 90 Grad – noch nicht implementiert.""" - return None +def _pick_branch(candidates, branch): + """Waehlt linken oder rechten Zweig aus und berechnet rz.""" + if branch == "left": + k2_pos, junc_pt = candidates[0] + else: + k2_pos, junc_pt = candidates[-1] + + # x-Achse tangential zur Linie, zur Kurve zeigend + dx = junc_pt[0] - k2_pos[0] + dy = junc_pt[1] - k2_pos[1] + rz = math.degrees(math.atan2(dy, dx)) + + return (k2_pos[0], k2_pos[1], 0), 0, 0, rz + + +def k2_point_weiche_arc(doc): + """K2 fuer Weichen: Ende des linken Bogenzweigs.""" + candidates = _find_weiche_arc_branches(doc) + if candidates is None: + return None + return _pick_branch(candidates, "left") + + +def k3_point_weiche_arc(doc): + """K3 fuer Weichen: Ende des rechten Bogenzweigs. + + Nur bei Weichen mit 2+ Boegen (Doppel-, Dreiwegeweichen etc.). + Bei Einzelweichen (nur 1 Bogen) gibt es keinen rechten Zweig. + """ + candidates = _find_weiche_arc_branches(doc) + if candidates is None or len(candidates) < 2: + return None + return _pick_branch(candidates, "right") + + +# --------------------------------------------------------------------------- +# K2/K3 fuer Weichenkoerper (keine ARCs, nur Diagonalen) +# --------------------------------------------------------------------------- + +def _find_weichenkoerper_diag_branches(doc): + """Findet die Enden der langen Diagonalen eines Weichenkoerpers. + + Weichenkoerper haben keine Boegen, sondern Diagonalen die sich an einem + Punkt nahe der K1-Vertikallinie treffen. K2/K3 liegen an den aeusseren + Enden der langen Diagonalen (Laenge > 100). + + Returns: + Liste von (outer_pt, junction_pt) Tupeln, sortiert nach x (links zuerst), + oder None falls keine langen Diagonalen gefunden. + """ + msp = doc.modelspace() + + # K1-Position bestimmen (unterste vertikale Linie) + vline = find_bottommost_vertical_line(doc) + if vline is None: + return None + s, e = vline.dxf.start, vline.dxf.end + k1_x = s[0] + k1_y = min(s[1], e[1]) + + # Lange Diagonalen finden (nicht horizontal, nicht vertikal, Laenge > 100) + candidates = [] + for ent in msp: + if ent.dxftype() != "LINE": + continue + ls = (ent.dxf.start[0], ent.dxf.start[1]) + le = (ent.dxf.end[0], ent.dxf.end[1]) + dx = abs(le[0] - ls[0]) + dy = abs(le[1] - ls[1]) + if dx < 0.01 or dy < 0.01: + continue + ln = (dx**2 + dy**2) ** 0.5 + if ln < 100: + continue + + # Das Ende naeher an K1 ist die Junction, das andere ist der K2/K3-Punkt + d_start = ((ls[0] - k1_x)**2 + (ls[1] - k1_y)**2) ** 0.5 + d_end = ((le[0] - k1_x)**2 + (le[1] - k1_y)**2) ** 0.5 + if d_start < d_end: + candidates.append((le, ls)) + else: + candidates.append((ls, le)) + + if not candidates: + return None + + # Sortieren nach x: links (kleinstes x) zuerst + candidates.sort(key=lambda c: c[0][0]) + return candidates def k2_point_weichenkoerper(doc): - """K2 fuer Weichenkoerper – noch nicht implementiert.""" - return None + """K2 fuer Weichenkoerper: Ende der linken langen Diagonale.""" + candidates = _find_weichenkoerper_diag_branches(doc) + if candidates is None: + return None + outer_pt, junc_pt = candidates[0] + dx = junc_pt[0] - outer_pt[0] + dy = junc_pt[1] - outer_pt[1] + rz = math.degrees(math.atan2(dy, dx)) + return (outer_pt[0], outer_pt[1], 0), 0, 0, rz -def k2_point_weichen_parallel(doc): - """K2 fuer Weichen Parallel – noch nicht implementiert.""" - return None +def k3_point_weichenkoerper(doc): + """K3 fuer Weichenkoerper: Ende der rechten langen Diagonale. - -def k2_point_sternweiche(doc): - """K2 fuer Sternweiche – noch nicht implementiert.""" - return None - - -def k2_point_delta(doc): - """K2 fuer Deltaweichen – noch nicht implementiert.""" - return None - - -def k2_point_dreifachweiche(doc): - """K2 fuer Dreifachweichen – noch nicht implementiert.""" - return None + Nur bei Doppel- und Dreiwegeweichenkoerpern (2+ Diagonalen). + """ + candidates = _find_weichenkoerper_diag_branches(doc) + if candidates is None or len(candidates) < 2: + return None + outer_pt, junc_pt = candidates[-1] + dx = junc_pt[0] - outer_pt[0] + dy = junc_pt[1] - outer_pt[1] + rz = math.degrees(math.atan2(dy, dx)) + return (outer_pt[0], outer_pt[1], 0), 0, 0, rz # --------------------------------------------------------------------------- -# K2 Schalter-Zuordnung: SWITCH_FILTERS-Key -> K2-Funktion +# K2/K3 fuer Dreifach-, Delta-, Sternweiche (Enden der laengsten H-Linie) +# --------------------------------------------------------------------------- + +def _find_longest_hline(doc): + """Findet die laengste horizontale Linie im Modelspace.""" + best = None + best_len = 0 + for e in doc.modelspace(): + if e.dxftype() != "LINE": + continue + s, end = e.dxf.start, e.dxf.end + if abs(end[1] - s[1]) > 0.01: + continue + ln = abs(end[0] - s[0]) + if ln > best_len: + best_len = ln + best = e + return best + + +def k2_point_hline(doc): + """K2 am linken Ende der laengsten horizontalen Linie. x zeigt nach rechts.""" + hline = _find_longest_hline(doc) + if hline is None: + return None + s, e = hline.dxf.start, hline.dxf.end + lx = min(s[0], e[0]) + y = s[1] + return (lx, y, 0), 0, 0, 0 + + +def k3_point_hline(doc): + """K3 am rechten Ende der laengsten horizontalen Linie. x zeigt nach links.""" + hline = _find_longest_hline(doc) + if hline is None: + return None + s, e = hline.dxf.start, hline.dxf.end + rx = max(s[0], e[0]) + y = s[1] + return (rx, y, 0), 0, 0, 180 + + +# --------------------------------------------------------------------------- +# K2/K3 Schalter-Zuordnung: SWITCH_FILTERS-Key -> Funktion +# K2 = linker Zweig, K3 = rechter Zweig (nur bei 2+ Boegen/Diagonalen) # --------------------------------------------------------------------------- K2_FUNCS = { "boegen": k2_point_bogen, - "weichen45": k2_point_weichen45, - "weichen90": k2_point_weichen90, + "weichen45": k2_point_weiche_arc, + "weichen90": k2_point_weiche_arc, "weichenkoerper": k2_point_weichenkoerper, - "weichen_parallel": k2_point_weichen_parallel, - "sternweiche": k2_point_sternweiche, - "delta": k2_point_delta, - "dreifachweiche": k2_point_dreifachweiche, + "weichen_parallel": k2_point_weiche_arc, + "sternweiche": k2_point_hline, + "delta": k2_point_hline, + "dreifachweiche": k2_point_hline, } +K3_FUNCS = { + "weichen45": k3_point_weiche_arc, + "weichen90": k3_point_weiche_arc, + "weichenkoerper": k3_point_weichenkoerper, + "weichen_parallel": k3_point_weiche_arc, + "sternweiche": k3_point_hline, + "delta": k3_point_hline, + "dreifachweiche": k3_point_hline, +} + +# K4: oberes Ende der K1-Linie. Kein K4 bei: Boegen, Doppelweiche, +# Dreifachweiche, Deltaweiche (inkl. Weichenkoerper Doppel). +K4_FUNCS = { + "weichen45": k4_point_weiche, + "weichen90": k4_point_weiche, + "weichenkoerper": k4_point_weiche, + "weichen_parallel": k4_point_weiche, + "sternweiche": k4_point_weiche, +} + +_K4_EXCLUDE_TYPES = ("Doppelweiche", "Dreifachweiche", "Deltaweiche") + + +# --------------------------------------------------------------------------- +# Existenz-Pruefung und Duplikat-Bereinigung +# --------------------------------------------------------------------------- + +def _check_and_clean_ks(msp, k_name, sivasnr, force=False): + """Prueft ob ein Kx bereits existiert und bereinigt Duplikate. + + - Mehrfach vorhandene Kx: alle bis auf das letzte entfernen (immer). + - Genau ein Kx vorhanden und kein --force: skip (return False). + - Genau ein Kx vorhanden und --force: entfernen (return True). + - Kein Kx vorhanden: return True (neu erzeugen). + + Returns: + True = Kx soll (neu) erzeugt werden. + False = Kx existiert bereits, skip. + """ + existing = [e for e in msp if e.dxftype() == "INSERT" and e.dxf.name == k_name] + + if len(existing) > 1: + # Duplikate: alle bis auf das letzte entfernen + for e in existing[:-1]: + msp.delete_entity(e) + print(f" {sivasnr}: {len(existing)-1} Duplikat(e) von {k_name} entfernt.") + existing = existing[-1:] + + if len(existing) == 1: + if force: + msp.delete_entity(existing[0]) + return True + else: + print(f" {sivasnr}: skipping {k_name} (bereits vorhanden)") + return False + + return True + # --------------------------------------------------------------------------- # --k1set # --------------------------------------------------------------------------- -def process_k1set(data_dir, results_dir, number=None): +def process_k1set(data_dir, results_dir, number=None, force=False): """Setzt K1-Block an alle Boegen und Weichen.""" sources = load_omniflo_data(data_dir) omniflo_dir = os.path.join(data_dir, "omniflo") @@ -431,12 +737,10 @@ def process_k1set(data_dir, results_dir, number=None): continue doc = ezdxf.readfile(dxf_path) - - # Bestehende K1-Referenzen entfernen msp = doc.modelspace() - for entity in list(msp): - if entity.dxftype() == "INSERT" and entity.dxf.name == "K1": - msp.delete_entity(entity) + + if not _check_and_clean_ks(msp, "K1", sivasnr, force): + continue if is_bog: result = k1_point_bogen(doc) @@ -448,26 +752,25 @@ def process_k1set(data_dir, results_dir, number=None): continue point, rx, ry, rz = result - msp = doc.modelspace() insert_ks(msp, "K1", point, rx, ry, rz) out_path = os.path.join(results_dir, f"{sivasnr}.dxf") doc.saveas(out_path) - print(f"{sivasnr}: K1 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) " + print(f" {sivasnr}: K1 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) " f"rx={rx} ry={ry} rz={rz} [{profil}]") print(f"\nErgebnisse in: {results_dir}") # --------------------------------------------------------------------------- -# --k2set +# --k2set / --k3set # --------------------------------------------------------------------------- -def process_k2set(data_dir, results_dir, number=None): - """Setzt K2-Block an alle Boegen und Weichen (ueber SWITCH_FILTERS).""" +def _process_kset(k_name, funcs, data_dir, results_dir, number=None, force=False): + """Generische Verarbeitung fuer K2/K3: iteriert ueber SWITCH_FILTERS.""" omniflo_dir = os.path.join(data_dir, "omniflo") - for key, k2_func in K2_FUNCS.items(): + for key, k_func in funcs.items(): label, json_file, filter_func = SWITCH_FILTERS[key] json_path = os.path.join(data_dir, "json", json_file) @@ -482,7 +785,7 @@ def process_k2set(data_dir, results_dir, number=None): if not filtered: continue - print(f"=== K2 setzen: {label} ===") + print(f"=== {k_name} setzen: {label} ===") for item in filtered: sivasnr = str(item["Sivasnr"]) dxf_path = os.path.join(results_dir, f"{sivasnr}.dxf") @@ -494,23 +797,82 @@ def process_k2set(data_dir, results_dir, number=None): doc = ezdxf.readfile(dxf_path) msp = doc.modelspace() - # Bestehende K2-Referenzen entfernen - for entity in list(msp): - if entity.dxftype() == "INSERT" and entity.dxf.name == "K2": - msp.delete_entity(entity) + if not _check_and_clean_ks(msp, k_name, sivasnr, force): + continue - result = k2_func(doc) + result = k_func(doc) if result is None: - print(f" {sivasnr}: K2 nicht bestimmbar (Dummy), ueberspringe.") continue point, rx, ry, rz = result - insert_ks(msp, "K2", point, rx, ry, rz) + insert_ks(msp, k_name, point, rx, ry, rz) out_path = os.path.join(results_dir, f"{sivasnr}.dxf") doc.saveas(out_path) - print(f" {sivasnr}: K2 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) " + print(f" {sivasnr}: {k_name} at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) " + f"rx={rx} ry={ry} rz={rz:.1f} [{item['ProfilTyp']}]") + + print(f"\nErgebnisse in: {results_dir}") + + +def process_k2set(data_dir, results_dir, number=None, force=False): + """Setzt K2-Block (linker Bogenzweig) an alle Boegen und Weichen.""" + _process_kset("K2", K2_FUNCS, data_dir, results_dir, number, force) + + +def process_k3set(data_dir, results_dir, number=None, force=False): + """Setzt K3-Block (rechter Bogenzweig) an Weichen mit 2+ Boegen.""" + _process_kset("K3", K3_FUNCS, data_dir, results_dir, number, force) + + +def process_k4set(data_dir, results_dir, number=None, force=False): + """Setzt K4-Block (oberes K1-Linienende) an Weichen ohne Doppel/Dreifach/Delta.""" + omniflo_dir = os.path.join(data_dir, "omniflo") + + for key, k4_func in K4_FUNCS.items(): + label, json_file, filter_func = SWITCH_FILTERS[key] + + json_path = os.path.join(data_dir, "json", json_file) + if not os.path.exists(json_path): + continue + with open(json_path, "r", encoding="utf-8") as f: + all_items = json.load(f) + + # SWITCH_FILTERS-Filter + K4-Ausschluss (Doppelweiche etc.) + filtered = [item for item in all_items + if filter_func(item) + and item.get("WeichenTyp", "") not in _K4_EXCLUDE_TYPES] + if number: + filtered = [item for item in filtered if str(item["Sivasnr"]) == str(number)] + if not filtered: + continue + + print(f"=== K4 setzen: {label} ===") + for item in filtered: + sivasnr = str(item["Sivasnr"]) + dxf_path = os.path.join(results_dir, f"{sivasnr}.dxf") + if not os.path.exists(dxf_path): + dxf_path = os.path.join(omniflo_dir, f"{sivasnr}.dxf") + if not os.path.exists(dxf_path): + continue + + doc = ezdxf.readfile(dxf_path) + msp = doc.modelspace() + + if not _check_and_clean_ks(msp, "K4", sivasnr, force): + continue + + result = k4_func(doc) + if result is None: + continue + + point, rx, ry, rz = result + insert_ks(msp, "K4", point, rx, ry, rz) + + out_path = os.path.join(results_dir, f"{sivasnr}.dxf") + doc.saveas(out_path) + print(f" {sivasnr}: K4 at ({point[0]:.2f},{point[1]:.2f},{point[2]:.2f}) " f"rx={rx} ry={ry} rz={rz:.1f} [{item['ProfilTyp']}]") print(f"\nErgebnisse in: {results_dir}") @@ -619,7 +981,15 @@ def main(): parser.add_argument("--k1set", action="store_true", help="K1-Block an alle Boegen und Weichen setzen") parser.add_argument("--k2set", action="store_true", - help="K2-Block an alle Boegen setzen (anderes Kettenende)") + help="K2-Block setzen (linker Bogenzweig)") + parser.add_argument("--k3set", action="store_true", + help="K3-Block setzen (rechter Bogenzweig, nur Doppel-/Dreiwegeweichen)") + parser.add_argument("--k4set", action="store_true", + help="K4-Block setzen (oberes K1-Linienende, nicht Doppel/Dreifach/Delta)") + parser.add_argument("--set-all", action="store_true", + help="Alle Koordinatensysteme (K1-K4) in einem Durchgang setzen") + parser.add_argument("--force", action="store_true", + help="Bestehende Kx ueberschreiben (sonst skip)") parser.add_argument("--show-omniflo", action="store_true", help="Uebersichts-DXF mit K-Positionen erzeugen") parser.add_argument("--test", action="store_true", @@ -632,7 +1002,7 @@ def main(): print("FEHLER: --number muss eine 9-stellige Ganzzahl sein.") sys.exit(1) - if not args.k1set and not args.k2set and not args.show_omniflo and not args.test: + if not args.k1set and not args.k2set and not args.k3set and not args.k4set and not args.set_all and not args.show_omniflo and not args.test: parser.print_help() sys.exit(1) @@ -653,11 +1023,23 @@ def main(): if args.test: run_test(results_dir) + if args.set_all: + process_k1set(data_dir, results_dir, args.number, args.force) + process_k2set(data_dir, results_dir, args.number, args.force) + process_k3set(data_dir, results_dir, args.number, args.force) + process_k4set(data_dir, results_dir, args.number, args.force) + if args.k1set: - process_k1set(data_dir, results_dir, args.number) + process_k1set(data_dir, results_dir, args.number, args.force) if args.k2set: - process_k2set(data_dir, results_dir, args.number) + process_k2set(data_dir, results_dir, args.number, args.force) + + if args.k3set: + process_k3set(data_dir, results_dir, args.number, args.force) + + if args.k4set: + process_k4set(data_dir, results_dir, args.number, args.force) if args.show_omniflo: print("=== Koordinaten Uebersicht ===")