TRO flow svg file can be created.

This commit is contained in:
2026-09-10 11:29:22 +02:00
parent 6fda4951e1
commit 4bab45705d
4 changed files with 738 additions and 30 deletions
+106 -5
View File
@@ -769,8 +769,23 @@ def _nearest_node(station: Element, nodes: dict[str, Node]) -> str | None:
return best
def _nearest_kreisel(station: Element, elements: list[Element]) -> Element | None:
"""Naechstgelegener Kreisel zur Station (2D-Abstand zur Kreisel-Achsposition)."""
if station.x is None or station.y is None:
return None
best, best_d = None, None
for el in elements:
if not el.is_circle or el.x is None or el.y is None:
continue
d = (el.x - station.x) ** 2 + (el.y - station.y) ** 2
if best_d is None or d < best_d:
best, best_d = el, d
return best
def _attach_stations(
elements: list[Element], nodes: dict[str, Node], warnings: list[str]
elements: list[Element], by_id: dict[str, Element], nodes: dict[str, Node],
warnings: list[str]
) -> list[Edge]:
"""
Ein-/Ausgabestationen (BTMT Be-/Entladung) mit dem Fluss verbinden.
@@ -778,7 +793,11 @@ def _attach_stations(
Eingang (Beladung) ist Quelle: Kante Station -> Ziel.
Ausgang (Entladung) ist Senke: Kante Ziel -> Station.
Ziel kommt aus den 'Nachbarn' des Exports; fehlen sie (dxfmakros exportiert
fuer BTMT derzeit keine), wird das naechstgelegene Objekt per Abstand genommen.
fuer BTMT derzeit keine), haengen beide baulich direkt am naechstgelegenen
Kreisel, auf dessen Eingangs- bzw. Ausgangsbahn (siehe _weiche_lane_roles):
UZS -> Eingang Bahn R / Ausgang Bahn L, GUZ -> Eingang Bahn L / Ausgang
Bahn R. Nur wenn kein Kreisel mit bekannter Drehrichtung gefunden wird,
greift ersatzweise die naechstgelegene Bahn/Strecke per Abstand.
"""
edges: list[Edge] = []
for element in elements:
@@ -796,6 +815,18 @@ def _attach_stations(
else:
partners += [f"{base}-{L}" for L in (LANE_LEFT, LANE_RIGHT)
if f"{base}-{L}" in nodes]
if not partners and (element.is_entry or element.is_exit):
kreisel = _nearest_kreisel(element, elements)
roles = _weiche_lane_roles(kreisel.teile_id, by_id, nodes) if kreisel else None
if roles:
eingang, ausgang = roles
lane, role = (eingang, "Eingangsbahn") if element.is_entry else (ausgang, "Ausgangsbahn")
partners = [lane]
warnings.append(
f"{element.describe()}: keine Nachbarn im Export - "
f"an Kreisel {kreisel.teile_id} Bahn {lane.split('-')[-1]} "
f"angebunden ({role}, Drehrichtung {kreisel.rotation})"
)
if not partners:
nearest = _nearest_node(element, nodes)
if nearest:
@@ -827,6 +858,30 @@ def _kreisel_lane_node(kreisel_base: str, strecke: Element, nodes: dict[str, Nod
return kreisel_base if kreisel_base in nodes else None
def _weiche_lane_roles(kreisel_base: str, by_id: dict[str, Element], nodes: dict[str, Node]) -> tuple[str, str] | None:
"""Eingangs-/Ausgangsbahn eines Kreisels an einer Kreisel<->Kreisel-Weiche.
Ein Kreisel ist intern ein Umlauf zwischen seinen zwei Bahnen (Merkmal
"Drehrichtung"); am Beruehrpunkt mit dem anderen Kreisel liegt daher
zwingend die eine Bahn als Eingang (freies Ende vor dem internen Umlauf)
und die andere als Ausgang (freies Ende danach):
UZS (Umlauf R -> L) -> Eingang R, Ausgang L
GUZ (Umlauf L -> R) -> Eingang L, Ausgang R
None, wenn die Drehrichtung fehlt/unbekannt ist oder eine Bahn im Graph fehlt.
"""
element = by_id.get(kreisel_base)
if element is None or not element.is_circle:
return None
left, right = f"{kreisel_base}-{LANE_LEFT}", f"{kreisel_base}-{LANE_RIGHT}"
if left not in nodes or right not in nodes:
return None
if element.rotation == ROTATION_CW:
return right, left
if element.rotation == ROTATION_CCW:
return left, right
return None
def _apply_transfers(
elements: list[Element], by_id: dict[str, Element],
nodes: dict[str, Node], edges: list[Edge], warnings: list[str]
@@ -836,7 +891,10 @@ def _apply_transfers(
Ausschleuselement: Kreisel -> Strecke (Streckenanfang).
Einschleuselement: Strecke -> Kreisel (Streckenende).
Weiche: Kreisel<->Kreisel als Weiche markieren.
Weiche: Kreisel<->Kreisel, Bahn und Richtung aus der Drehrichtung
beider Kreisel (siehe _weiche_lane_roles); ohne bekannte
Drehrichtung bleibt die Verbindung unbestimmt, wird aber
weiterhin als Weiche markiert.
Die so definierten Kanten sind massgeblich und ersetzen die aus Hoehe/
Nachbarn abgeleitete Richtung fuer genau diese Knotenpaare.
"""
@@ -844,6 +902,7 @@ def _apply_transfers(
# ein = Auslauf (Strecke->Kreisel, Ende)
per_strecke: dict[str, dict[str, str]] = {}
weiche_pairs: list[set[str]] = []
weiche_ab: list[tuple[str, str]] = []
for el in elements:
if el.kind in ("Ausschleus", "Einschleus"):
k = str(_merkmal(el.merkmale, "Kreisel") or "").strip()
@@ -856,6 +915,28 @@ def _apply_transfers(
b = str(_merkmal(el.merkmale, "Kreisel B") or "").strip()
if a and b:
weiche_pairs.append({a, b})
weiche_ab.append((a, b))
# Weiche zwischen zwei Kreiseln: Bahn und Richtung deterministisch aus der
# Drehrichtung beider Kreisel ableiten (Ausgang der einen Bahn -> Eingang
# der anderen, in beiden Richtungen) statt aus dem rohen Nachbarn-Kreuzprodukt,
# das links/rechts nicht unterscheiden kann.
weiche_edges: list[Edge] = []
weiche_resolved: set[frozenset] = set()
for a, b in weiche_ab:
roles_a = _weiche_lane_roles(a, by_id, nodes)
roles_b = _weiche_lane_roles(b, by_id, nodes)
if roles_a is None or roles_b is None:
warnings.append(
f"Weiche {a}<->{b}: Drehrichtung mindestens eines Kreisels unbekannt - "
f"Bahnzuordnung nicht bestimmbar, Verbindung bleibt unbestimmt"
)
continue
in_a, out_a = roles_a
in_b, out_b = roles_b
weiche_edges.append(Edge(out_a, in_b, EDGE_FLOW, "Weiche"))
weiche_edges.append(Edge(out_b, in_a, EDGE_FLOW, "Weiche"))
weiche_resolved.add(frozenset({a, b}))
directed: dict[frozenset, tuple[str, str]] = {}
for s, roles in per_strecke.items():
@@ -885,9 +966,13 @@ def _apply_transfers(
# einem dritten Kreisel liegt) ist ein Falschtreffer und wird verworfen.
covered = set(per_strecke)
result: list[Edge] = []
superseded: set[tuple[str, str]] = set()
for e in edges:
if frozenset({e.src, e.dst}) in directed:
continue # wird gerichtet neu gesetzt
if frozenset({e.src.split("-")[0], e.dst.split("-")[0]}) in weiche_resolved:
superseded.add((e.src, e.dst))
continue # wird durch die Weiche-Kanten ersetzt
ends = {e.src, e.dst}
strecke_end = ends & covered
others = ends - strecke_end
@@ -902,7 +987,23 @@ def _apply_transfers(
result.append(e)
for (src, dst, note) in directed.values():
result.append(Edge(src, dst, EDGE_FLOW, note))
# Kreisel<->Kreisel-Kanten, die eine Weiche verbinden, markieren
result.extend(weiche_edges)
# Die anfaengliche Hoehen-basierte Ausrichtung (_orient) kann eine
# Kreisel<->Kreisel-Verbindung nie bestimmen und meldet sie immer als
# "beidseitig gezeichnet" - das stimmt nicht mehr, sobald die Weiche sie
# gerade durch obenstehende Kanten ersetzt hat. Die dadurch ueberholte
# Meldung wird entfernt.
if superseded:
stale = {
f"Verbindung {a} <-> {b}: Richtung nicht bestimmbar "
f"(kein Transportobjekt beteiligt) - beidseitig gezeichnet"
for src, dst in superseded for a, b in ((src, dst), (dst, src))
}
warnings[:] = [w for w in warnings if w not in stale]
# Kreisel<->Kreisel-Kanten, die eine Weiche verbinden, markieren (deckt vor
# allem die Faelle ab, die mangels Drehrichtung nicht gerichtet aufgeloest
# werden konnten - die aufgeloesten Weiche-Kanten tragen die Markierung
# bereits).
for e in result:
if {e.src.split("-")[0], e.dst.split("-")[0]} in weiche_pairs:
e.note = "Weiche"
@@ -949,7 +1050,7 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
edges.append(Edge(right, left, EDGE_CIRCLE, "unbestimmt"))
# Ein-/Ausgabestationen (BTMT Be-/Entladung) als Quelle/Senke anbinden
edges.extend(_attach_stations(elements, nodes, warnings))
edges.extend(_attach_stations(elements, by_id, nodes, warnings))
# Kreisel<->Strecke gerichtet aus den Transferelementen (massgeblich)
edges = _apply_transfers(elements, by_id, nodes, edges, warnings)