From 72d0a21f65f4d817bff3c9c8272fdcaf5cef1520 Mon Sep 17 00:00:00 2001 From: Samer Ayadi Date: Fri, 18 Sep 2026 16:12:13 +0200 Subject: [PATCH] TRO-Fluss: je Typ eigenes Symbol im Diagramm, Weichenerkennung ueber Scanner lib/tro_catalog.py: dot_shape_attrs()/SYMBOL_LABELS geben render_dot() eine Graphviz-Form je TroSymbol (bisher nur fuer den CAD-Marker in tro_annotate.py genutzt) - Farbe gruppiert weiterhin verwandte Typen, Form unterscheidet den einzelnen Typ. Legende und Doku-Tabellen zeigen jetzt jeden Katalogtyp mit Farbe+Symbol statt nur die 6 Farbgruppen. lib/tro_flow.py: _find_switch_separators() bekommt einen zweiten Durchlauf, der einen Separator mit eigenem Scanner (CAD-Merkmal "Naechster Separator") als Weiche erkennt, wenn die reine Abstandsregel (SWITCH_PROXIMITY_MM) ihn verfehlt - Rolle (Aus-/Einschleusung) wird aus einem bereits bestaetigten Treffer auf derselben Kreisel-Bahn gelesen, keine neue feste mm-Schwelle. Grund: bei HundM05 (5 Abzweige an einem Kreisel) lagen 3 der 5 echten Weichen-Separatoren 727-1139mm von ihrem Ausschleuselement entfernt - ausserhalb der auf einer aelteren Revision kalibrierten 500mm-Grenze - und wurden bisher faelschlich als einfacher 1Sep statt 1Sep1Swi eingestuft. Co-Authored-By: Claude Sonnet 5 --- lib/tro_catalog.py | 62 ++++++++++++++++++++++++++- lib/tro_flow.py | 102 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 140 insertions(+), 24 deletions(-) diff --git a/lib/tro_catalog.py b/lib/tro_catalog.py index 03ebf0b..f7013a0 100644 --- a/lib/tro_catalog.py +++ b/lib/tro_catalog.py @@ -77,6 +77,9 @@ __all__ = [ "set_tro", "catalog", "mermaid_classdefs", + "dot_shape_attrs", + "SYMBOL_LABELS", + "symbol_label", ] # Stand der Dokumente, aus denen die Typdefinition uebernommen wurde @@ -165,6 +168,53 @@ class TroSymbol: ) +# Graphviz-Knotenform je Symbol, fuer render_dot() in tro_flow.py. Peripheries +# zeichnet die Doppel-Varianten als zwei konzentrische Formen - dasselbe Bild, +# das draw_shape() in tro_annotate.py fuer den CAD-Marker als zwei ineinander +# liegende Polygone zeichnet. Namen sind Graphviz-Bordformen (dot -Tsvg). +_DOT_SHAPE: dict[str, dict[str, str]] = { + TroSymbol.CIRCLE: {"shape": "circle"}, + TroSymbol.DOUBLE_CIRCLE: {"shape": "circle", "peripheries": "2"}, + TroSymbol.TRIANGLE: {"shape": "triangle"}, + TroSymbol.TRIANGLE_DOWN: {"shape": "invtriangle"}, + TroSymbol.DOUBLE_TRIANGLE_DOWN: {"shape": "invtriangle", "peripheries": "2"}, + TroSymbol.SQUARE: {"shape": "square"}, + TroSymbol.DIAMOND: {"shape": "diamond"}, + TroSymbol.PENTAGON: {"shape": "pentagon"}, + TroSymbol.HEXAGON: {"shape": "hexagon"}, + TroSymbol.RECT: {"shape": "box"}, + TroSymbol.ARROW: {"shape": "rarrow"}, +} + + +def dot_shape_attrs(symbol: str) -> dict[str, str]: + """Graphviz-Knotenform (shape[, peripheries]) fuer ein TroSymbol; unbekannte + Symbole fallen auf die normale Box zurueck.""" + return dict(_DOT_SHAPE.get(symbol, {"shape": "box"})) + + +# Klartext je Symbol - fuer die Legende in render_dot()/render_doc() (tro_flow.py), +# damit dort nicht nur die Farbgruppe, sondern auch die Form je Typ lesbar ist. +SYMBOL_LABELS: dict[str, str] = { + TroSymbol.CIRCLE: "Kreis", + TroSymbol.DOUBLE_CIRCLE: "Doppelkreis", + TroSymbol.TRIANGLE: "Dreieck (Spitze oben)", + TroSymbol.TRIANGLE_DOWN: "Dreieck (Spitze unten)", + TroSymbol.DOUBLE_TRIANGLE_DOWN: "Doppel-Dreieck (Spitze unten)", + TroSymbol.SQUARE: "Quadrat", + TroSymbol.DIAMOND: "Raute", + TroSymbol.PENTAGON: "Fuenfeck", + TroSymbol.HEXAGON: "Sechseck", + TroSymbol.RECT: "liegendes Rechteck", + TroSymbol.ARROW: "Pfeil", +} + + +def symbol_label(symbol: str) -> str: + """Klartext eines Symbols; der Symbolname selbst, wenn kein Klartext hinterlegt ist.""" + return SYMBOL_LABELS.get(symbol, symbol) + + class TroStyle(BaseModel): """ Darstellung einer TRO-Typgruppe. @@ -371,8 +421,12 @@ class TroDefinition(BaseModel): return self.style.group def get_dot_attrs(self) -> dict[str, str]: - """Graphviz-Knotenattribute dieses Typs.""" - return self.style.dot_attrs() + """Graphviz-Knotenattribute dieses Typs: Fuell-/Randfarbe der Farbgruppe + plus die typ-eigene Form (siehe TroSymbol/dot_shape_attrs) - Farbe + gruppiert verwandte Typen, Form unterscheidet den einzelnen Typ.""" + attrs = self.style.dot_attrs() + attrs.update(dot_shape_attrs(self.symbol)) + return attrs # -- set --------------------------------------------------------------- # @@ -432,6 +486,10 @@ class TroDefinition(BaseModel): return "-" return ", ".join(f"{self.items[i]}x {i}" for i in self.get_item_names()) + def describe_symbol(self) -> str: + """Symbol als Klartext, z. B. 'Dreieck (Spitze oben)'.""" + return symbol_label(self.symbol) + # --------------------------------------------------------------------------- # Statische Typdefinition diff --git a/lib/tro_flow.py b/lib/tro_flow.py index c4daa53..f2b2458 100644 --- a/lib/tro_flow.py +++ b/lib/tro_flow.py @@ -273,20 +273,38 @@ def _default_resolve_host(sep: Element, findings: list[str], graph: Graph) -> st def _find_switch_separators( separators: list[Element], elements: list[Element], tol_mm: float ) -> dict[str, tuple[Element, float]]: - """Separator-Id -> (naechstgelegenes Aus-/Einschleuselement, Abstand mm), - nur wenn Abstand <= tol_mm. + """Separator-Id -> (Aus-/Einschleuselement der Weiche, Abstand mm). - HundM05 hat an seinen Kreisel<->Strecke-Uebergaengen kein eigenes "ILS - Weiche"-Bauteil (anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe - Mubea) - das von dxfmakros synthetisierte Ausschleuselement (Kreisel -> - Strecke, am Streckenanfang) bzw. Einschleuselement (Strecke -> Kreisel, - am Streckenende) uebernimmt dort faktisch die Rolle der Weiche. Sitzt ein - Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig davon, - ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf die Strecke - zeigt (in den Daten kommt beides vor, siehe derive_tros). + Zwei Regeln, angewandt in dieser Reihenfolge: + + 1. Geometrische Naehe (Distanz <= tol_mm). HundM05 hat an seinen + Kreisel<->Strecke-Uebergaengen kein eigenes "ILS Weiche"-Bauteil + (anders als ein Kreisel<->Kreisel-Beruehrpunkt, siehe Mubea) - das von + dxfmakros synthetisierte Ausschleuselement (Kreisel -> Strecke, am + Streckenanfang) bzw. Einschleuselement (Strecke -> Kreisel, am + Streckenende) uebernimmt dort faktisch die Rolle der Weiche. Sitzt ein + Separator direkt daneben, gehoert er zu dieser Weiche - unabhaengig + davon, ob seine CSV-Spalte "Zuordnung" auf die Kreisel-Bahn oder auf + die Strecke zeigt (in den Daten kommt beides vor, siehe derive_tros). + 2. Separator mit eigenem Scanner (Scanner-Merkmal "Naechster Separator"), + auf noch unbelegten Uebergabestellen der SELBEN Rolle (Aus- oder + Einschleusung) und dem SELBEN Kreisel-Anschluss wie ein bereits durch + Regel 1 bestaetigter Treffer auf dieser Bahn - keine feste mm-Grenze, + stattdessen die naechstgelegene freie Uebergabestelle dieser Rolle. + Grund (HundM05, Review 2026-09-18): an einem Kreisel mit 5 Abzweigen + liegen die tatsaechlichen Schalt-Separatoren pro Abzweig unterschiedlich + weit vom Uebergabeelement entfernt (418-1139mm in der aktuellen + Registrierung) - eine einzelne mm-Schwelle (Regel 1) trifft nicht alle. + Ein Scanner direkt am Separator ist hier aber der CAD-belegte + Lesepunkt vor der Weichenentscheidung (Barcode -> Richtung), also ein + verlaesslicherer Marker als der reine Abstand. Nach Rolle (Aus- vs. + Einschleusung) gefiltert, damit z. B. ein Separator auf der + Ausschleus-Bahn nicht versehentlich mit dem naeher liegenden + Einschleuselement einer anderen Strecke verkoppelt wird. """ transfers = [e for e in elements if e.kind in ("Ausschleus", "Einschleus")] result: dict[str, tuple[Element, float]] = {} + for sep in separators: if sep.x is None or sep.y is None: continue @@ -299,6 +317,41 @@ def _find_switch_separators( best = (d, transfer) if best is not None: result[sep.teile_id] = (best[1], best[0]) + + # Rolle je Kreisel-Anschluss aus einem bereits bestaetigten Treffer lesen - + # keine Annahme, welche Bahn (-L/-R) Aus- bzw. Einschleusung uebernimmt. + bahn_role: dict[str, str] = {} + for sep in separators: + hit = result.get(sep.teile_id) + if hit and sep.assignment: + bahn_role.setdefault(sep.assignment, hit[0].kind) + + scanner_targets = { + str(_merkmal(e.merkmale, "Naechster Separator") or "").strip() + for e in elements if e.kind == "Scanner" + } + used_transfers = {hit[0].teile_id for hit in result.values()} + + for sep in separators: + if sep.teile_id in result or sep.teile_id not in scanner_targets: + continue + if sep.x is None or sep.y is None: + continue + role = bahn_role.get(sep.assignment or "") + if role is None: + continue + best: tuple[float, Element] | None = None + for transfer in transfers: + if (transfer.kind != role or transfer.teile_id in used_transfers + or transfer.x is None or transfer.y is None): + continue + d = math.hypot(transfer.x - sep.x, transfer.y - sep.y) + if best is None or d < best[0]: + best = (d, transfer) + if best is not None: + result[sep.teile_id] = (best[1], best[0]) + used_transfers.add(best[1].teile_id) + return result @@ -976,14 +1029,17 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str f'color="#7f7f7f", constraint=false, tooltip="gleiche Bahn"];') add("") - # Legende: eine Zeile je Farbgruppe, plus die Kantenarten. - # Bei -n braucht auch sie ein pos, sonst landet sie auf 0,0 mitten in der - # Anlage - daher unterhalb links vom Anlagenrand. + # Legende: eine Zeile je TRO-Typ (Farbe der Gruppe + eigene Form, siehe + # tro_catalog.TRO_CATALOG), plus eine Zeile fuer die Systemgrenzen und die + # Kantenarten. Bei -n braucht auch sie ein pos, sonst landet sie auf 0,0 + # mitten in der Anlage - daher unterhalb links vom Anlagenrand. rows = "".join( - f' {s.group} ' - f'{s.label}' - for s in STYLES + f' {d.get_name()} ' + f'{d.describe_symbol()}' + for d in TRO_CATALOG ) + rows += (f' Systemgrenze ' + f'Ellipse (Materialein-/ausgang, kein TRO)') legend_label = ('<' + rows + '' @@ -1165,14 +1221,15 @@ def render_doc( add(f"| Offene Punkte (noch kein TRO) | " f"{len(analysis.overrides.open_points)} |") add("") - add("| Typ | FB-Baustein | Anzahl hier | Bauteile je TRO | Farbgruppe |") - add("|---|---|---:|---|---|") + add("| Typ | FB-Baustein | Anzahl hier | Bauteile je TRO | Farbgruppe | Symbol |") + add("|---|---|---:|---|---|---|") for name, count in sorted(types_used.items()): definition = get_tro(name) add(f"| `{name}` | " f"{f'`{definition.get_fb_block()}`' if definition else '_kein Katalogeintrag_'} " f"| {count} | {definition.describe_items() if definition else '-'} " - f"| {definition.get_group() if definition else '-'} |") + f"| {definition.get_group() if definition else '-'} " + f"| {definition.describe_symbol() if definition else '-'} |") add("") add("## 2. TRO-Flussdiagramm") @@ -1346,11 +1403,12 @@ def render_doc( add(f"Typen, FB-Bausteine, Bauteile und Farben stammen vollstaendig aus " f"`TRO_CATALOG` in `lib/tro_catalog.py` (Stand {CATALOG_AS_OF}):") add("") - add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe |") - add("|---:|---|---|---|---|") + add("| # | Typ | FB-Baustein | Bauteile je TRO | Farbgruppe | Symbol |") + add("|---:|---|---|---|---|---|") for index, definition in enumerate(TRO_CATALOG, start=1): add(f"| {index} | `{definition.get_name()}` | `{definition.get_fb_block()}` " - f"| {definition.describe_items()} | {definition.get_group()} |") + f"| {definition.describe_items()} | {definition.get_group()} " + f"| {definition.describe_symbol()} |") add("") add("| Bauteil | Sub-FB |") add("|---|---|")
durchgezogen