neuer Schalter Switch hinzugefügt
This commit is contained in:
@@ -115,6 +115,12 @@ TRANSFER_PATTERNS = (
|
||||
("Weiche", ("weiche",)),
|
||||
)
|
||||
|
||||
# Objektarten des feinen Bahnzusammenhang-Graphen (--track, siehe doc/track.md):
|
||||
# Separator plus die drei Transferarten bilden dort je einen eigenen Knoten -
|
||||
# anders als im normalen Materialfluss-Graphen, wo sie nur Kanten/Zuordnungen
|
||||
# liefern (siehe TRANSPORT_KINDS/CIRCLE_KINDS oben).
|
||||
TRACK_KINDS = ("Separator",) + TRANSFER_KINDS
|
||||
|
||||
LANE_LEFT = "L"
|
||||
LANE_RIGHT = "R"
|
||||
LANE_NAMES = {LANE_LEFT: "links", LANE_RIGHT: "rechts"}
|
||||
@@ -311,6 +317,38 @@ class Graph:
|
||||
source: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackNode:
|
||||
"""Ein Knoten des feinen Bahnzusammenhang-Graphen (--track, doc/track.md).
|
||||
|
||||
element ist None fuer die beiden Hilfsknotenarten: einen Bahn-Platzhalter
|
||||
(placeholder=True - eine Kreisel-Bahn oder Strecke, an der sich kein
|
||||
konkretes Bahnelement auflösen liess, siehe build_track_graph) oder ein
|
||||
generisches Eingang/Ausgang-Symbol (SYMBOL_ENTRY/SYMBOL_EXIT) an einem
|
||||
offenen Bahnende ohne BTMT-Station.
|
||||
"""
|
||||
|
||||
node_id: str
|
||||
kind: str
|
||||
element: Element | None = None
|
||||
placeholder: bool = False
|
||||
note: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackEdge:
|
||||
src: str
|
||||
dst: str
|
||||
note: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class TrackGraph:
|
||||
nodes: dict[str, TrackNode] = field(default_factory=dict)
|
||||
edges: list[TrackEdge] = field(default_factory=list)
|
||||
source: str = ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1081,6 +1119,459 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
|
||||
return Graph(nodes=nodes, edges=edges, warnings=warnings, unassigned=unassigned, source=source)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bahnzusammenhang-Graph (--track, siehe doc/track.md)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Feinerer Graph als build_graph(): Separator/Ausschleus/Einschleus/Weiche
|
||||
# werden hier zu eigenen Knoten (statt nur Kanten/Zuordnungen zu liefern wie
|
||||
# im normalen Materialfluss). Baut auf dem fertigen groben Graph auf (fuer
|
||||
# Kreisel-Drehrichtung ueber _weiche_lane_roles und die bereits geloeste
|
||||
# Stations-Anbindung aus _attach_stations) und auf der CSV-Spalte "Nachbarn"
|
||||
# der vier TRACK_KINDS selbst - die die bestehende Pipeline bisher komplett
|
||||
# ignoriert (_collect_links ueberspringt sie, siehe dort).
|
||||
#
|
||||
# Kernbefund (doc/track.md Abschnitt 1): ein Ausschleus-/Einschleuselement
|
||||
# nennt in seiner eigenen Nachbarn-Spalte konsequent nur die Foerderer-/
|
||||
# Streckenseite; die Kreisel-Seite ist nur ueber die Rueckmeldung des dortigen
|
||||
# Separators auffindbar (der das Transferelement seinerseits als Nachbarn
|
||||
# nennt). Die Fein-Adjazenz ist daher die Vereinigung aller Nennungen, nicht
|
||||
# nur die eigene Nachbarn-Liste eines Elements.
|
||||
|
||||
SYMBOL_ENTRY = "Eingang"
|
||||
SYMBOL_EXIT = "Ausgang"
|
||||
|
||||
|
||||
def _transfer_kreisel_lane(el: Element, by_id: dict[str, Element], nodes: dict[str, Node]) -> str | None:
|
||||
"""
|
||||
Kreisel-Bahn (…-L/…-R) eines Ausschleus-/Einschleuselements.
|
||||
|
||||
Die Merkmale von AS/ES nennen nur die blanke Kreisel-TeileId (Merkmal
|
||||
"Kreisel"), nie die Bahn. Welche der beiden Bahnen gemeint ist, steht
|
||||
stattdessen in den Nachbarn der zugehoerigen Strecke/Gefaellestrecke
|
||||
(Merkmal "Strecke" bzw. "Förderer") - dieselbe Aufloesung, die
|
||||
_apply_transfers fuer die groben Kreisel<->Strecke-Kanten schon nutzt.
|
||||
"""
|
||||
kreisel_id = str(_merkmal(el.merkmale, "Kreisel") or "").strip()
|
||||
if not kreisel_id:
|
||||
return None
|
||||
strecke_id = str(_merkmal(el.merkmale, "Strecke", "Förderer") or "").strip()
|
||||
strecke_el = by_id.get(strecke_id)
|
||||
if strecke_el is not None:
|
||||
lane = _kreisel_lane_node(kreisel_id, strecke_el, nodes)
|
||||
if lane:
|
||||
return lane
|
||||
for lane_suffix in (LANE_LEFT, LANE_RIGHT): # Fallback: irgendeine Bahn
|
||||
if f"{kreisel_id}-{lane_suffix}" in nodes:
|
||||
return f"{kreisel_id}-{lane_suffix}"
|
||||
return kreisel_id if kreisel_id in nodes else None
|
||||
|
||||
|
||||
def _wire_weiche(
|
||||
el: Element,
|
||||
by_id: dict[str, Element],
|
||||
graph: Graph,
|
||||
own: dict[str, list[str]],
|
||||
track: TrackGraph,
|
||||
warnings: list[str],
|
||||
placeholder,
|
||||
add_edge,
|
||||
) -> None:
|
||||
"""
|
||||
Weiche als 4-Wege-Kreuzung verdrahten (Kreisel A <-> Kreisel B, in beide
|
||||
Richtungen; siehe doc/track.md Abschnitt 2).
|
||||
|
||||
Pro Kreisel liefert das bereits vorhandene _weiche_lane_roles() die
|
||||
Eingangs-/Ausgangsbahn aus der Drehrichtung. Die eigenen Nachbarn der
|
||||
Weiche (bis zu 2) werden per Zuordnung auf diese vier Rollen verteilt;
|
||||
ein Nachbar, dessen Zuordnung zu keiner der vier Bahnen passt (z. B. weil
|
||||
er - wie in den Mubea-Testdaten - foelschlich einer Strecke statt der
|
||||
schaltenden Kreisel-Bahn zugeordnet ist, vgl. tro_overrides.py-Fall
|
||||
"split"), bekommt per Ausschlussverfahren die letzte freie Rolle,
|
||||
zusammen mit einem Hinweis. Rollen ohne passenden Nachbarn haengen am
|
||||
Bahn-Platzhalter der jeweiligen Bahn.
|
||||
"""
|
||||
kreisel_a = str(_merkmal(el.merkmale, "Kreisel A") or "").strip()
|
||||
kreisel_b = str(_merkmal(el.merkmale, "Kreisel B") or "").strip()
|
||||
roles_a = _weiche_lane_roles(kreisel_a, by_id, graph.nodes) if kreisel_a else None
|
||||
roles_b = _weiche_lane_roles(kreisel_b, by_id, graph.nodes) if kreisel_b else None
|
||||
if roles_a is None and kreisel_a:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Drehrichtung von Kreisel {kreisel_a} unbekannt - "
|
||||
f"Bahnrollen auf dieser Seite nicht bestimmbar"
|
||||
)
|
||||
if roles_b is None and kreisel_b:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Drehrichtung von Kreisel {kreisel_b} unbekannt - "
|
||||
f"Bahnrollen auf dieser Seite nicht bestimmbar"
|
||||
)
|
||||
lanes: dict[str, str | None] = {
|
||||
"eingang_a": roles_a[0] if roles_a else None,
|
||||
"ausgang_a": roles_a[1] if roles_a else None,
|
||||
"eingang_b": roles_b[0] if roles_b else None,
|
||||
"ausgang_b": roles_b[1] if roles_b else None,
|
||||
}
|
||||
|
||||
candidates = list(own.get(el.teile_id, []))
|
||||
matched: dict[str, str] = {}
|
||||
unmatched: list[str] = []
|
||||
for cand in candidates:
|
||||
cand_el = by_id.get(cand)
|
||||
lane = cand_el.assignment if cand_el else None
|
||||
role = next((r for r, lane_id in lanes.items() if lane_id and lane_id == lane), None)
|
||||
if role:
|
||||
matched[role] = cand
|
||||
else:
|
||||
unmatched.append(cand)
|
||||
|
||||
remaining_roles = [r for r in lanes if r not in matched and lanes[r] is not None]
|
||||
if len(unmatched) == 1 and len(remaining_roles) == 1:
|
||||
role = remaining_roles[0]
|
||||
matched[role] = unmatched[0]
|
||||
cand_el = by_id.get(unmatched[0])
|
||||
warnings.append(
|
||||
f"{el.describe()}: Nachbar {unmatched[0]} (Zuordnung "
|
||||
f"'{cand_el.assignment if cand_el else '-'}') passt zu keiner der "
|
||||
f"erwarteten Bahnen - durch Ausschlussverfahren als {role} angenommen"
|
||||
)
|
||||
elif unmatched:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Nachbar(n) {unmatched} nicht eindeutig einer Bahn "
|
||||
f"zuzuordnen (offene Rollen: {remaining_roles or '-'}) - nicht verbunden"
|
||||
)
|
||||
|
||||
def endpoint(role: str) -> str | None:
|
||||
if role in matched:
|
||||
return matched[role]
|
||||
lane_id = lanes[role]
|
||||
return placeholder(lane_id, "Weiche-Seite ohne passenden Fein-Nachbarn") if lane_id else None
|
||||
|
||||
add_edge(endpoint("ausgang_a"), el.teile_id, "Weiche")
|
||||
add_edge(el.teile_id, endpoint("eingang_b"), "Weiche")
|
||||
add_edge(endpoint("ausgang_b"), el.teile_id, "Weiche")
|
||||
add_edge(el.teile_id, endpoint("eingang_a"), "Weiche")
|
||||
|
||||
|
||||
def _orient_separator_links(
|
||||
fine: dict[str, Element],
|
||||
own: dict[str, list[str]],
|
||||
mentioned_by: dict[str, set[str]],
|
||||
track: TrackGraph,
|
||||
by_id: dict[str, Element],
|
||||
warnings: list[str],
|
||||
add_edge,
|
||||
suppressed_pairs: set[frozenset] = frozenset(),
|
||||
) -> None:
|
||||
"""
|
||||
Reine Separator-Separator-Kanten richten (doc/track.md Abschnitt 3).
|
||||
|
||||
Jede AS-/ES-/Weiche-Kante ist zu diesem Zeitpunkt bereits gerichtet
|
||||
(siehe build_track_graph) und "verankert" die anliegenden Separatoren.
|
||||
Von dort aus wird die Kette wie eine verkettete Liste abgearbeitet: hat
|
||||
ein Separator genau eine noch offene Separator-Separator-Verbindung und
|
||||
schon eine bekannte (Anker- oder bereits geloeste) Seite, bekommt die
|
||||
offene Seite zwangslaeufig die jeweils andere Rolle. Was am Ende keinen
|
||||
Anker erreicht, faellt auf Hoehe/Antriebsrichtung des zugeordneten
|
||||
Objekts zurueck (wie im groben Graph), sonst bleibt es unbestimmt.
|
||||
|
||||
suppressed_pairs schliesst Paare aus, die zwar gegenseitig als Nachbarn
|
||||
gelistet sind, aber beide zugleich eigener Nachbar DESSELBEN AS/ES/Weiche-
|
||||
Ankers sind (siehe build_track_graph) - das sind zwei unabhaengige Speichen
|
||||
desselben Uebergabepunkts (beide nur deshalb nah beieinander, weil sie am
|
||||
selben Knoten haengen), keine direkte Foerderverbindung zueinander. Ohne
|
||||
diesen Ausschluss wuerde z. B. in mubea_export.csv faelschlich eine Kante
|
||||
zwischen den beiden Weiche-Nachbarn 0040 (Kreisel 0001-R) und 0036 (an
|
||||
Strecke 0014, siehe _wire_weiche) gezeichnet.
|
||||
"""
|
||||
separators = {tid for tid, el in fine.items() if el.kind == "Separator"}
|
||||
adjacency: dict[str, set[str]] = {
|
||||
tid: {
|
||||
n
|
||||
for n in (set(own.get(tid, [])) | mentioned_by.get(tid, set()))
|
||||
if n in separators and frozenset((tid, n)) not in suppressed_pairs
|
||||
}
|
||||
for tid in separators
|
||||
}
|
||||
|
||||
# touches[sep]: bereits bekannte Nachbarn dieses Separators und ihre Rolle
|
||||
# ("upstream" = Material kommt von dort, "downstream" = Material geht dorthin).
|
||||
touches: dict[str, dict[str, str]] = {tid: {} for tid in separators}
|
||||
for e in track.edges:
|
||||
if e.dst in separators:
|
||||
touches[e.dst][e.src] = "upstream"
|
||||
if e.src in separators:
|
||||
touches[e.src][e.dst] = "downstream"
|
||||
|
||||
pending: dict[str, set[str]] = {
|
||||
tid: {n for n in adjacency[tid] if n not in touches[tid]} for tid in separators
|
||||
}
|
||||
|
||||
changed = True
|
||||
while changed:
|
||||
changed = False
|
||||
for tid in separators:
|
||||
if len(pending[tid]) != 1 or len(touches[tid]) == 0:
|
||||
continue
|
||||
other = next(iter(pending[tid]))
|
||||
resolved_dir = next(iter(touches[tid].values()))
|
||||
other_dir = "upstream" if resolved_dir == "downstream" else "downstream"
|
||||
if other_dir == "downstream":
|
||||
add_edge(tid, other, "Bahn")
|
||||
else:
|
||||
add_edge(other, tid, "Bahn")
|
||||
touches[tid][other] = other_dir
|
||||
touches[other][tid] = "upstream" if other_dir == "downstream" else "downstream"
|
||||
pending[tid].discard(other)
|
||||
pending[other].discard(tid)
|
||||
changed = True
|
||||
|
||||
seen: set[frozenset] = set()
|
||||
for tid in separators:
|
||||
for other in pending[tid]:
|
||||
pair = frozenset((tid, other))
|
||||
if pair in seen:
|
||||
continue
|
||||
seen.add(pair)
|
||||
# Hoehenvergleich nur, wenn beide Separatoren DERSELBEN Zuordnung
|
||||
# angehoeren (echtes Kettenmittelstueck, z. B. 0050<->0051 auf
|
||||
# Strecke 0013). Ein Nachbar-Paar mit UNTERSCHIEDLICHER Zuordnung
|
||||
# und ganz ohne erreichbaren Anker (z. B. 0043 auf Kreisel 0010-R
|
||||
# <-> 0045 auf Strecke 0013) ist derselbe Fall wie 0036/0040 bzw.
|
||||
# 0005/0078 - ein Uebergang, der eigentlich ueber ein AS/ES/Weiche
|
||||
# laufen sollte, hier aber keins hat; ohne dessen Rollenaufloesung
|
||||
# waere ein Hoehenvergleich (der hier zudem gleiche Z liefert)
|
||||
# geraten statt abgeleitet.
|
||||
same_host = bool(fine[tid].assignment) and fine[tid].assignment == fine[other].assignment
|
||||
host = by_id.get(fine[tid].assignment.split("-")[0]) if same_host and fine[tid].assignment else None
|
||||
uphill, reason = drive_uphill(host) if host is not None else (None, "kein zugeordnetes Objekt")
|
||||
za, zb = fine[tid].z, fine[other].z
|
||||
if uphill is not None and za is not None and zb is not None and za != zb:
|
||||
src, dst = (tid, other) if (za < zb) == uphill else (other, tid)
|
||||
add_edge(src, dst, "Hoehe")
|
||||
warnings.append(
|
||||
f"Separator {tid} <-> {other}: kein Anker (AS/ES/Weiche) in der "
|
||||
f"Kette erreichbar - Richtung ueber Hoehe des zugeordneten Objekts "
|
||||
f"({reason}) angenommen"
|
||||
)
|
||||
else:
|
||||
add_edge(tid, other, "unbestimmt")
|
||||
warnings.append(
|
||||
f"Separator {tid} <-> {other}: kein Anker und keine Hoehe - "
|
||||
f"Richtung unbestimmt, beidseitig markiert"
|
||||
)
|
||||
|
||||
|
||||
def _attach_track_stations(
|
||||
elements: list[Element], graph: Graph, track: TrackGraph, placeholder, add_edge
|
||||
) -> None:
|
||||
"""
|
||||
BTMT Be-/Entladung an den Bahn-Platzhalter anbinden.
|
||||
|
||||
Die Zielbahn wurde bereits von _attach_stations() im groben Graph
|
||||
bestimmt (naechster Kreisel + Eingangs-/Ausgangsbahn aus der Drehrichtung,
|
||||
siehe dort) - hier nur wiederverwendet statt neu berechnet.
|
||||
"""
|
||||
for element in elements:
|
||||
if not element.is_station:
|
||||
continue
|
||||
sid = element.teile_id
|
||||
if sid not in graph.nodes:
|
||||
continue
|
||||
track.nodes.setdefault(sid, TrackNode(sid, element.kind, element))
|
||||
for edge in graph.edges:
|
||||
if edge.src == sid:
|
||||
add_edge(sid, placeholder(edge.dst), "Station")
|
||||
elif edge.dst == sid:
|
||||
add_edge(placeholder(edge.src), sid, "Station")
|
||||
|
||||
|
||||
def _attach_track_symbols(
|
||||
graph: Graph, track: TrackGraph, placeholder, add_edge, warnings: list[str]
|
||||
) -> None:
|
||||
"""
|
||||
Offene Bahnenden ohne BTMT-Station als generisches Symbol Eingang/Ausgang
|
||||
markieren - dieselben Kandidaten, die render_connect_ini() heute nur als
|
||||
Kommentarzeile auflistet (kein Transportobjekt liefert eine ankommende
|
||||
bzw. abgehende Kante), hier aber als echter Graph-Knoten.
|
||||
"""
|
||||
incoming = {e.dst for e in graph.edges}
|
||||
outgoing = {e.src for e in graph.edges}
|
||||
symbol_no = 0
|
||||
for node_id in sorted(graph.nodes):
|
||||
node = graph.nodes[node_id]
|
||||
if node.element.is_station:
|
||||
continue
|
||||
if node_id not in incoming:
|
||||
symbol_no += 1
|
||||
sym_id = f"SYM_EIN_{symbol_no:02d}"
|
||||
track.nodes[sym_id] = TrackNode(sym_id, SYMBOL_ENTRY)
|
||||
add_edge(sym_id, placeholder(node_id), "Symbol")
|
||||
warnings.append(
|
||||
f"{node_id}: keine ankommende Verbindung und keine BTMT-Station - "
|
||||
f"im --track-Graph als Symbol {SYMBOL_ENTRY} markiert"
|
||||
)
|
||||
if node_id not in outgoing:
|
||||
symbol_no += 1
|
||||
sym_id = f"SYM_AUS_{symbol_no:02d}"
|
||||
track.nodes[sym_id] = TrackNode(sym_id, SYMBOL_EXIT)
|
||||
add_edge(placeholder(node_id), sym_id, "Symbol")
|
||||
warnings.append(
|
||||
f"{node_id}: keine abgehende Verbindung und keine BTMT-Station - "
|
||||
f"im --track-Graph als Symbol {SYMBOL_EXIT} markiert"
|
||||
)
|
||||
|
||||
|
||||
def build_track_graph(
|
||||
elements: list[Element], by_id: dict[str, Element], graph: Graph, warnings: list[str]
|
||||
) -> TrackGraph:
|
||||
"""
|
||||
Feiner Bahnzusammenhang-Graph fuer --track (siehe doc/track.md).
|
||||
|
||||
Nutzt den bereits fertig aufgeloesten groben Graph fuer Kreisel-
|
||||
Drehrichtung (_weiche_lane_roles) und die bereits geloeste Stations-
|
||||
Anbindung (graph.edges); die Fein-Adjazenz selbst kommt aus der CSV-Spalte
|
||||
"Nachbarn" der Separator-/Ausschleus-/Einschleus-/Weiche-Zeilen (siehe
|
||||
Modul-Docstring oben). warnings wird mit dem Aufruf von build_graph()
|
||||
geteilt, damit Fein-Hinweise im selben Bericht erscheinen.
|
||||
"""
|
||||
track = TrackGraph(source=graph.source)
|
||||
fine = {e.teile_id: e for e in elements if e.kind in TRACK_KINDS}
|
||||
|
||||
def add_edge(src: str | None, dst: str | None, note: str = "") -> None:
|
||||
if src is not None and dst is not None:
|
||||
track.edges.append(TrackEdge(src, dst, note))
|
||||
|
||||
def placeholder(host_id: str | None, reason: str = "") -> str | None:
|
||||
if not host_id:
|
||||
return None
|
||||
if host_id not in track.nodes:
|
||||
coarse = graph.nodes.get(host_id)
|
||||
track.nodes[host_id] = TrackNode(
|
||||
host_id, "Bahn", coarse.element if coarse else None, placeholder=True, note=reason
|
||||
)
|
||||
return host_id
|
||||
|
||||
for el in fine.values():
|
||||
track.nodes.setdefault(el.teile_id, TrackNode(el.teile_id, el.kind, el))
|
||||
|
||||
# Eigene Nachbarn je Feinelement (nur Feinelemente behalten, siehe
|
||||
# TRACK_KINDS) plus die Umkehr-Sicht (wer nennt mich als Nachbar) -
|
||||
# zusammen die vereinigte Fein-Adjazenz (doc/track.md Abschnitt 1).
|
||||
own: dict[str, list[str]] = {
|
||||
tid: [b for b, _lane in el.neighbours if b in fine] for tid, el in fine.items()
|
||||
}
|
||||
mentioned_by: dict[str, set[str]] = {tid: set() for tid in fine}
|
||||
for tid, targets in own.items():
|
||||
for target in targets:
|
||||
mentioned_by[target].add(tid)
|
||||
|
||||
# Ausschleus/Einschleus: eigener Nachbar = Foerderer-/Streckenseite,
|
||||
# Rueckmeldung = Kreisel-Seite (doc/track.md Abschnitt 1-2). Eine
|
||||
# Rueckmeldung wird nur akzeptiert, wenn ihre eigene Zuordnung auch
|
||||
# tatsaechlich zur erwarteten Bahn (_transfer_kreisel_lane) passt - sonst
|
||||
# ist es wie beim Weiche-Fall 0036/0040 nur eine raeumlich nahe, aber
|
||||
# fachlich andere Kette (z. B. 0005 auf Strecke 0014, das per Rueckmeldung
|
||||
# faelschlich als Kreisel-Seite mehrerer Storage-Bank-Einschleuselemente
|
||||
# auf 0002-R erscheint), keine echte Weiterfahrt auf derselben Bahn.
|
||||
for el in fine.values():
|
||||
if el.kind not in ("Ausschleus", "Einschleus"):
|
||||
continue
|
||||
strecke_side = list(own.get(el.teile_id, []))
|
||||
if not strecke_side:
|
||||
warnings.append(f"{el.describe()}: keine Foerderer-/Streckenseite in eigenen Nachbarn gefunden")
|
||||
elif len(strecke_side) > 1:
|
||||
warnings.append(f"{el.describe()}: mehrere Streckenseiten-Nachbarn {strecke_side} - alle verbunden")
|
||||
|
||||
expected_lane = _transfer_kreisel_lane(el, by_id, graph.nodes)
|
||||
raw_kreisel_side = sorted(mentioned_by.get(el.teile_id, set()) - set(strecke_side))
|
||||
kreisel_side: list[str] = []
|
||||
for cand in raw_kreisel_side:
|
||||
cand_el = by_id.get(cand)
|
||||
if expected_lane and cand_el is not None and cand_el.assignment == expected_lane:
|
||||
kreisel_side.append(cand)
|
||||
else:
|
||||
warnings.append(
|
||||
f"{el.describe()}: Rueckmeldung von {cand} (Zuordnung "
|
||||
f"'{cand_el.assignment if cand_el else '-'}') passt nicht zur erwarteten "
|
||||
f"Bahn {expected_lane or '?'} - als Kreisel-seitiger Nachbar verworfen"
|
||||
)
|
||||
if not kreisel_side:
|
||||
if expected_lane:
|
||||
kreisel_side = [placeholder(expected_lane, "keine passende Rueckmeldung eines Fein-Elements gefunden")]
|
||||
else:
|
||||
warnings.append(f"{el.describe()}: Kreisel-Bahn nicht aufloesbar - keine Kreisel-seitige Kante")
|
||||
elif len(kreisel_side) > 1:
|
||||
warnings.append(f"{el.describe()}: mehrere Kreisel-seitige Nachbarn {kreisel_side} - alle verbunden")
|
||||
|
||||
if el.kind == "Ausschleus":
|
||||
for k in kreisel_side:
|
||||
add_edge(k, el.teile_id, "Bahn")
|
||||
for s in strecke_side:
|
||||
add_edge(el.teile_id, s, "Zweig")
|
||||
else: # Einschleus
|
||||
for s in strecke_side:
|
||||
add_edge(s, el.teile_id, "Zweig")
|
||||
for k in kreisel_side:
|
||||
add_edge(el.teile_id, k, "Bahn")
|
||||
|
||||
# Weiche: 4-Wege-Kreuzung zwischen zwei Kreiseln (doc/track.md Abschnitt 2).
|
||||
for el in fine.values():
|
||||
if el.kind == "Weiche":
|
||||
_wire_weiche(el, by_id, graph, own, track, warnings, placeholder, add_edge)
|
||||
|
||||
# Zwei eigene Nachbarn DESSELBEN AS/ES/Weiche-Ankers sind zwei unabhaengige
|
||||
# Speichen desselben Uebergabepunkts, keine direkte Kette zueinander (siehe
|
||||
# _orient_separator_links-Docstring) - von der Separator-Separator-
|
||||
# Orientierung ausschliessen, bevor sie dort faelschlich verkettet werden.
|
||||
suppressed_pairs: set[frozenset] = set()
|
||||
for tid, el in fine.items():
|
||||
if el.kind not in ("Ausschleus", "Einschleus", "Weiche"):
|
||||
continue
|
||||
candidates = own.get(tid, [])
|
||||
for i, a in enumerate(candidates):
|
||||
for b in candidates[i + 1:]:
|
||||
suppressed_pairs.add(frozenset((a, b)))
|
||||
|
||||
# Reine Separator-Separator-Verbindungen orientieren (doc/track.md Abschnitt 3).
|
||||
_orient_separator_links(
|
||||
fine, own, mentioned_by, track, by_id, warnings, add_edge, suppressed_pairs
|
||||
)
|
||||
|
||||
# Verwaiste Separatoren (keine Nachbarn im Export, z. B. 0037/0038 in
|
||||
# mubea_export.csv) an ihren Zuordnung-Platzhalter haengen statt sie
|
||||
# unverbunden zu lassen - Reihenfolge dort bewusst offen.
|
||||
for el in fine.values():
|
||||
if el.kind != "Separator" or own.get(el.teile_id) or mentioned_by.get(el.teile_id):
|
||||
continue
|
||||
host = el.assignment if el.assignment and el.assignment != UNASSIGNED else None
|
||||
ph = placeholder(host, "Separator ohne Nachbarn im Export")
|
||||
if ph:
|
||||
add_edge(ph, el.teile_id, "unbestimmt")
|
||||
warnings.append(
|
||||
f"{el.describe()}: keine Nachbarn zu anderen Bahnelementen im Export - "
|
||||
f"Reihenfolge auf {host} ungeklaert"
|
||||
)
|
||||
else:
|
||||
warnings.append(f"{el.describe()}: keine Nachbarn und keine Zuordnung - im --track-Graph isoliert")
|
||||
|
||||
# Systemgrenzen: BTMT-Stationen (wiederverwendet) bzw. generisches
|
||||
# Eingang/Ausgang-Symbol an offenen Bahnenden ohne Station.
|
||||
_attach_track_stations(elements, graph, track, placeholder, add_edge)
|
||||
_attach_track_symbols(graph, track, placeholder, add_edge, warnings)
|
||||
|
||||
# Letzte Sicherung: ein Fein-Element, das nach alledem keine einzige Kante
|
||||
# hat (z. B. weil ein Weiche-Nachbar zu keiner Rolle passte, siehe
|
||||
# _wire_weiche, UND seine einzige sonstige Nachbarschaft als Speiche
|
||||
# desselben Ankers ausgeschlossen wurde), sichtbar melden statt es
|
||||
# stillschweigend unverbunden im Graph stehen zu lassen.
|
||||
touched = {e.src for e in track.edges} | {e.dst for e in track.edges}
|
||||
for tid, el in fine.items():
|
||||
if tid not in touched:
|
||||
warnings.append(f"{el.describe()}: im --track-Graph ohne jede Kante - Zuordnung/Nachbarn widerspruechlich")
|
||||
|
||||
return track
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DOT erzeugen
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1372,6 +1863,102 @@ def render_dot(graph: Graph, use_coords: bool = False) -> str:
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# DOT des Bahnzusammenhang-Graphen erzeugen (--track)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
TRACK_NODE_STYLES: dict[str, dict[str, str]] = {
|
||||
"Separator": dict(fillcolor="#fff2cc", fontcolor="#7f6000", color="#bf8f00"),
|
||||
"Ausschleus": dict(fillcolor="#f9cb9c", fontcolor="#b45f06", color="#e69138"),
|
||||
"Einschleus": dict(fillcolor="#cfe2f3", fontcolor="#1155cc", color="#3d85c6"),
|
||||
"Weiche": dict(fillcolor="#ead1dc", fontcolor="#741b47", color="#a64d79"),
|
||||
"Beladung": dict(fillcolor="#d4f0d4", fontcolor="#1e5b1e", color="#338833"),
|
||||
"Entladung": dict(fillcolor="#f4d4d4", fontcolor="#7a1f1f", color="#cc3333"),
|
||||
SYMBOL_ENTRY: dict(fillcolor="#d4f0d4", fontcolor="#1e5b1e", color="#338833"),
|
||||
SYMBOL_EXIT: dict(fillcolor="#f4d4d4", fontcolor="#7a1f1f", color="#cc3333"),
|
||||
"Bahn": dict(fillcolor="#f2f2f2", fontcolor="#666666", color="#999999"),
|
||||
}
|
||||
|
||||
TRACK_EDGE_ATTRS: dict[str, str] = {
|
||||
"unbestimmt": '[color="#bf8f00", style=dashed, dir=both, tooltip="Richtung unbestimmt"]',
|
||||
"Zweig": '[color="#e69138", tooltip="Verzweigung/Zusammenfuehrung an AS/ES"]',
|
||||
"Weiche": '[color="#a64d79", penwidth=1.6, tooltip="Weiche (Kreuzung zweier Kreisel)"]',
|
||||
"Symbol": '[color="#999999", style=dotted, tooltip="offenes Bahnende ohne BTMT-Station"]',
|
||||
"Station": '[color="#338833", tooltip="Anbindung BTMT Be-/Entladung"]',
|
||||
"Hoehe": '[color="#548235", style=dashed, tooltip="Richtung ueber Hoehe angenommen"]',
|
||||
}
|
||||
|
||||
|
||||
def _track_node_label(node: TrackNode) -> str:
|
||||
if node.placeholder:
|
||||
lines = [f"Bahn {node.node_id}", "Reihenfolge/Position offen (Teil B)"]
|
||||
if node.note:
|
||||
lines.append(node.note)
|
||||
return _dot_label(lines)
|
||||
if node.element is None:
|
||||
return _dot_label([node.kind])
|
||||
el = node.element
|
||||
lines = [f"{el.name or node.kind} [{node.node_id}]", node.kind]
|
||||
if node.note:
|
||||
lines.append(node.note)
|
||||
return _dot_label(lines)
|
||||
|
||||
|
||||
def render_track_dot(track: TrackGraph) -> str:
|
||||
"""DOT des Bahnzusammenhang-Graphen (--track); siehe build_track_graph()
|
||||
und doc/track.md. Gerendert ueber dieselben graph_to_svg/dot_to_svg wie
|
||||
render_dot()."""
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
|
||||
add("// Automatisch erzeugt von lib/material_flow.py (--track) - nicht manuell aendern.")
|
||||
add(f"// Quelle: {track.source}")
|
||||
add("digraph Bahnzusammenhang {")
|
||||
add(' graph [rankdir=LR, splines=spline, nodesep=0.2, ranksep=1.1,')
|
||||
add(' fontname="Segoe UI", fontsize=11, labelloc="t",')
|
||||
add(f' label="Bahnzusammenhang - {_dot_escape(track.source)}"];')
|
||||
add(' node [shape=box, style="rounded,filled", fontname="Segoe UI", fontsize=8];')
|
||||
add(' edge [fontname="Segoe UI", fontsize=7, color="#2f5597", penwidth=1.1, arrowsize=0.7];')
|
||||
add("")
|
||||
|
||||
for node_id in sorted(track.nodes):
|
||||
node = track.nodes[node_id]
|
||||
style = TRACK_NODE_STYLES.get(node.kind, DEFAULT_NODE_STYLE)
|
||||
attrs = ", ".join(f'{key}="{value}"' for key, value in style.items())
|
||||
extra = ', style="rounded,dashed,filled"' if node.placeholder else ""
|
||||
if node.kind == "Weiche":
|
||||
extra += ", shape=diamond"
|
||||
elif node.kind in (SYMBOL_ENTRY, SYMBOL_EXIT, "Beladung", "Entladung"):
|
||||
extra += ", shape=ellipse"
|
||||
add(f' "{node_id}" [label="{_track_node_label(node)}", {attrs}{extra}];')
|
||||
add("")
|
||||
|
||||
for edge in track.edges:
|
||||
attrs = TRACK_EDGE_ATTRS.get(edge.note, "[]")
|
||||
add(f' "{edge.src}" -> "{edge.dst}" {attrs};')
|
||||
add("")
|
||||
|
||||
add(" subgraph cluster_legende {")
|
||||
add(' label="Legende"; style="rounded"; color="#a6a6a6";')
|
||||
add(' fontname="Segoe UI"; fontsize=10; fontcolor="#404040";')
|
||||
add(' legende [shape=plaintext, style="", fillcolor="none", label=<')
|
||||
add('<table border="0" cellborder="0" cellspacing="2" cellpadding="2">')
|
||||
add('<tr><td align="left">Raute</td><td align="left">Weiche (Kreuzung zweier Kreisel)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#e69138">orange</font></td>'
|
||||
'<td align="left">Zweig an Ausschleus-/Einschleuselement</td></tr>')
|
||||
add('<tr><td align="left">gestrichelter Rand</td>'
|
||||
'<td align="left">Bahn-Platzhalter - Reihenfolge/Position offen (Teil B)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#548235">gestrichelt gruen</font></td>'
|
||||
'<td align="left">Richtung ueber Hoehe angenommen (kein Anker erreichbar)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#bf8f00">gestrichelt gelb</font></td>'
|
||||
'<td align="left">Richtung unbestimmt</td></tr>')
|
||||
add("</table>>];")
|
||||
add(" }")
|
||||
add("}")
|
||||
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# connect.ini (Item-/Flussebene) erzeugen
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1908,6 +2495,8 @@ def report(
|
||||
doc_file: Path | None,
|
||||
stale_svg: Path | None = None,
|
||||
bbs_file: Path | None = None,
|
||||
track_dot_file: Path | None = None,
|
||||
track_svg_file: Path | None = None,
|
||||
) -> None:
|
||||
kinds: dict[str, int] = {}
|
||||
for node in graph.nodes.values():
|
||||
@@ -1937,6 +2526,9 @@ def report(
|
||||
print(f"SVG = {svg_file if svg_file else '- (ohne --tosvg)'}")
|
||||
print(f"Doku = {doc_file if doc_file else '- (ohne --doc)'}")
|
||||
print(f"BB-Draufsicht = {bbs_file if bbs_file else '- (ohne --show-bbs)'}")
|
||||
print(f"Bahnzusammenhang = {track_dot_file if track_dot_file else '- (ohne --track)'}")
|
||||
if track_dot_file:
|
||||
print(f" SVG = {track_svg_file if track_svg_file else '- (ohne --tosvg)'}")
|
||||
print("================================================================")
|
||||
|
||||
if stale_svg is not None:
|
||||
@@ -1986,6 +2578,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
" material_flow.bat --file mubea.csv --tosvg\n"
|
||||
" material_flow.bat --file mubea.csv --tosvg --doc\n"
|
||||
" material_flow.bat --file HundM05.csv --show-bbs\n"
|
||||
" material_flow.bat --file mubea.csv --track --tosvg\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
@@ -2044,6 +2637,17 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"kleines Kreuz mit Beschriftung einzeichnen und in der Reihenfolge "
|
||||
"K1->K2->K3->K4 mit einer blauen Linie verbinden.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--track",
|
||||
action="store_true",
|
||||
help="Zusaetzlich einen feineren Bahnzusammenhang-Graph erzeugen "
|
||||
"(<name>_track.dot, mit --tosvg auch <name>_track.svg): Separator, "
|
||||
"Ausschleus-/Einschleuselement und Weiche werden zu eigenen Knoten "
|
||||
"(Verzweigung/Zusammenfuehrung/Kreuzung), statt nur wie im normalen "
|
||||
"Materialfluss Kanten/Zuordnungen an Kreisel/Strecke/Gefaellestrecke "
|
||||
"zu liefern. Siehe doc/track.md. Aendert den normalen Materialfluss-"
|
||||
"Output nicht.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
@@ -2153,6 +2757,21 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
connect_file = connect_target
|
||||
|
||||
track_dot_file = track_svg_file = None
|
||||
if args.track:
|
||||
by_id = {element.teile_id: element for element in elements}
|
||||
track = build_track_graph(elements, by_id, graph, warnings)
|
||||
track_dot_target = results / f"{stem}_track.dot"
|
||||
track_dot_target.write_text(render_track_dot(track), encoding="utf-8")
|
||||
track_dot_file = track_dot_target
|
||||
if args.tosvg:
|
||||
track_svg_target = results / f"{stem}_track.svg"
|
||||
try:
|
||||
dot_to_svg(track_dot_file, track_svg_target)
|
||||
track_svg_file = track_svg_target
|
||||
except RuntimeError as exc:
|
||||
print(f"FEHLER: Bahnzusammenhang-SVG nicht erzeugt: {exc}", file=sys.stderr)
|
||||
|
||||
report(
|
||||
graph,
|
||||
findings,
|
||||
@@ -2162,6 +2781,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
doc_file,
|
||||
stale_svg=None if args.tosvg and svg_error is None else existing_svg,
|
||||
bbs_file=bbs_file,
|
||||
track_dot_file=track_dot_file,
|
||||
track_svg_file=track_svg_file,
|
||||
)
|
||||
|
||||
if svg_error:
|
||||
|
||||
Reference in New Issue
Block a user