TRO-Fluss: Stationen nicht mehr in der Kette, nur als Zu-/Ablaufpfeil

- link_tros: Systemgrenzen (Be-/Entladung, Eingang/Ausgang) sind kein
  Kettenglied mehr - die Umlaufkette verbindet die TROs direkt an ihnen
  vorbei. Entladung/Ausgang haengt am TRO direkt davor, Beladung/Eingang
  am TRO direkt danach. HundM05: TRO19 -> TRO20, SYS1/SYS2 -> TRO20,
  TRO19 -> SYS3. Mubea: TRO02 -> TRO13, TRO04 -> TRO03, TRO02 -> SYS2/SYS3,
  SYS1 -> TRO03.
- Umlauf schliessen ueber Stationen am Listenanfang hinweg (HundM05 Kreisel1
  beginnt mit den Beladungen, sonst fehlte TRO19 -> TRO20)
- render_dot: Station als farbige Beschriftung am Ende eines Pfeils (gruen
  Eingang, rot Ausgang) statt Ellipse, Legende entsprechend; lagerichtig
  feste Pfeillaenge ab dem angehaengten TRO, mehrere Stationen am selben TRO
  aufgefaechert (_station_positionen ersetzt _anzeige_position)
- HundM05 28 Verbindungen, Mubea 26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-02 10:15:23 +02:00
co-authored by Claude Opus 5.5
parent fcfc4f1278
commit d0a3ac7e5a
+120 -56
View File
@@ -172,10 +172,11 @@ GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
# Zeichnung, in der Symbole und Schrift verschwanden; Review 2026-10-01). # Zeichnung, in der Symbole und Schrift verschwanden; Review 2026-10-01).
COORD_SCALE = 0.1 COORD_SCALE = 0.1
# Mindestabstand (mm) einer Systemgrenze zu einem TRO in der lagerichtigen # Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
# Zeichnung - eine BTMT-Station sitzt oft fast auf ihrem Separator (Mubea SYS1 # (mm) ab dem angehaengten TRO und Faecherwinkel (Grad), wenn mehrere
# 186 mm neben TRO04); sie wird dann nur in der Darstellung beiseitegeschoben. # Stationen am selben TRO haengen (siehe _station_positionen).
COORD_MIN_ABSTAND = 600 COORD_PFEIL_LAENGE = 1300
COORD_PFEIL_FAECHER = 45
# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in # Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in
# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne # der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne
@@ -1274,13 +1275,9 @@ def link_tros(
def _add_chain_pair(a: str, b: str) -> None: def _add_chain_pair(a: str, b: str) -> None:
if a == b: if a == b:
return return
# Be- und Entladestationen auf der Kreisel-Bahn werden DURCHFAHREN: der # Keine Sperre fuer Systemgrenzen noetig: sie sind kein Kettenglied
# Kreisel laeuft immer weiter, was nicht entladen wird, faehrt an der # (siehe station_hosts unten) und kommen hier nur als Zu-/Ablaufpfeil
# naechsten Station vorbei. Systemgrenzen sind in der Umlaufkette daher # in der richtigen Richtung an.
# gewoehnliche Glieder (Review 2026-10-01): Mubea Kreisel2
# TRO02 -> SYS3 -> SYS2 -> TRO13, HundM05 Kreisel1
# TRO19 -> SYS3 -> Beladungen -> TRO20. Vorher endete der Fluss an jeder
# Entladung, und eine Beladung durfte nie Ziel sein.
pair = (a, b) pair = (a, b)
edges.add(pair) edges.add(pair)
edge_kinds.setdefault(pair, "bahn") edge_kinds.setdefault(pair, "bahn")
@@ -1371,25 +1368,61 @@ def link_tros(
speicher_ausschleus[el.teile_id] = store_by_host[strecke_id] speicher_ausschleus[el.teile_id] = store_by_host[strecke_id]
else: else:
einschleus_vario[el.teile_id] = store_by_host[strecke_id] einschleus_vario[el.teile_id] = store_by_host[strecke_id]
ketten_owner = {**elem_owner, **speicher_ausschleus} # Systemgrenzen (Be-/Entladung, Eingang/Ausgang) sind KEIN Kettenglied:
# die Umlaufkette verbindet die TROs direkt an ihnen vorbei, die Station
# haengt nur als Zu- bzw. Ablaufpfeil am benachbarten TRO (Review
# 2026-10-02). Vorher liefen sie als Glied mit (TRO19 -> SYS3 -> SYS1 ->
# SYS2 -> TRO20), jetzt TRO19 -> TRO20 plus TRO19 -> SYS3, SYS1/SYS2 -> TRO20.
station_hosts = {t.hosts[0]: t for t in tros if t.type_name in BOUNDARY_TYPES}
ketten_owner = {
**{k: v for k, v in elem_owner.items() if k not in station_hosts},
**speicher_ausschleus,
}
for el in elements: for el in elements:
if not el.is_circle or len(el.track_ids) < 2: if not el.is_circle or len(el.track_ids) < 2:
continue continue
chain = list(el.track_ids) chain = list(el.track_ids)
if el.rotation in (ROTATION_CW, ROTATION_CCW): laenge = len(chain)
chain.append(chain[0]) # Umlauf schliessen geschlossen = el.rotation in (ROTATION_CW, ROTATION_CCW)
if geschlossen:
# Umlauf schliessen: erstes Element hinten anhaengen. Stationen am
# Listenanfang sind kein Kettenglied - ueber sie hinweg bis zum
# ersten anderen Element verlaengern, sonst fehlt der Schluss
# (HundM05 Kreisel1 beginnt mit den Beladungen 0021/0022: ohne das
# fehlte TRO19 -> TRO20).
k = 0
while k < laenge - 1 and chain[k] in station_hosts:
k += 1
chain.extend(chain[:k + 1])
# Hauptkette: Separatoren/Systemgrenzen in Reihenfolge, Aus-/ # Hauptkette: Separatoren in Reihenfolge, Aus-/Einschleuselemente und
# Einschleuselemente bewusst uebersprungen (unowned) - das beschreibt # Stationen bewusst uebersprungen (unowned) - das beschreibt den BYPASS
# den BYPASS fuer Verkehr, der NICHT abzweigt (z. B. TRO01 -> TRO03, # fuer Verkehr, der NICHT abzweigt (z. B. TRO01 -> TRO03, wenn
# wenn dazwischen niemand abbiegt). # dazwischen niemand abbiegt).
owned = [(i, ketten_owner[tid]) for i, tid in enumerate(chain) if tid in ketten_owner] owned = [(i, ketten_owner[tid]) for i, tid in enumerate(chain) if tid in ketten_owner]
for (_, a), (_, b) in zip(owned, owned[1:]): for (_, a), (_, b) in zip(owned, owned[1:]):
_add_chain_pair(a, b) _add_chain_pair(a, b)
if owned: if owned:
covered_kreisel_bases.add(el.teile_id) covered_kreisel_bases.add(el.teile_id)
# Stationen als Pfeile: Entladung/Ausgang verlaesst den TRO direkt
# DAVOR, Beladung/Eingang muendet in den TRO direkt DANACH.
for i, tid in enumerate(chain[:laenge]):
station = station_hosts.get(tid)
if station is None or not owned:
continue
if station.type_name == BOUNDARY_EXIT:
tro_id = next((o for j, o in reversed(owned) if j < i),
owned[-1][1] if geschlossen else None)
if tro_id is not None:
_add_chain_pair(tro_id, station.tro_id)
else:
tro_id = next((o for j, o in owned if j > i),
owned[0][1] if geschlossen else None)
if tro_id is not None:
_add_chain_pair(station.tro_id, tro_id)
# Zusaetzlich der reale Wiedereintritt eines abgezweigten Vario- # Zusaetzlich der reale Wiedereintritt eines abgezweigten Vario-
# Segments: vom Einschleuselement zum naechsten Halt DANACH in # Segments: vom Einschleuselement zum naechsten Halt DANACH in
# derselben Kette - ersetzt fuer dieses Vario-TRO den groben "Strecke # derselben Kette - ersetzt fuer dieses Vario-TRO den groben "Strecke
@@ -1694,26 +1727,44 @@ def _kompakte_positionen(
return {k: (ax[x], ay[y]) for k, (x, y) in positionen.items()} return {k: (ax[x], ay[y]) for k, (x, y) in positionen.items()}
def _anzeige_position( def _station_positionen(
boundary: Tro, tros: list[Tro] analysis: Analysis, tro_zeichen: dict[str, tuple[float, float]]
) -> tuple[float, float, float] | None: ) -> dict[str, tuple[float, float]]:
"""Position einer Systemgrenze fuer die Zeichnung: liegt sie naeher als """Zeichenposition der Stationsbeschriftung (Ende des Zu-/Ablaufpfeils).
COORD_MIN_ABSTAND an einem TRO, wird sie radial von ihm weggeschoben."""
if not boundary.position: Feste Pfeillaenge COORD_PFEIL_LAENGE ab dem angehaengten TRO (Beladung:
return None der TRO danach, Entladung: der TRO davor), Richtung zur echten Station -
x, y, z = boundary.position so bleibt der Pfeil sichtbar, auch wenn die Station praktisch auf dem TRO
for tro in tros: sitzt (Mubea SYS1 186 mm neben TRO04). Haengen mehrere Stationen am selben
if not tro.position: TRO, werden sie um COORD_PFEIL_FAECHER Grad aufgefaechert. Station ohne
angehaengten TRO: echte Koordinate."""
tro_by_id = {t.tro_id: t for t in analysis.tros}
je_tro: dict[str, list[Tro]] = {}
ergebnis: dict[str, tuple[float, float]] = {}
for station in analysis.boundaries:
nachbarn = (station.successors if station.type_name == BOUNDARY_ENTRY
else station.predecessors)
anker = next((n for n in nachbarn if n in tro_zeichen), None)
if anker is None:
if station.position:
ergebnis[station.tro_id] = (station.position[0], station.position[1])
continue continue
dx, dy = x - tro.position[0], y - tro.position[1] je_tro.setdefault(anker, []).append(station)
d = math.hypot(dx, dy)
if d >= COORD_MIN_ABSTAND: for anker, stationen in je_tro.items():
continue ax, ay = tro_zeichen[anker]
if d == 0: echt = tro_by_id[anker].position
dx, dy, d = 0.0, -1.0, 1.0 for k, station in enumerate(sorted(stationen, key=lambda s: s.tro_id)):
x = tro.position[0] + dx / d * COORD_MIN_ABSTAND dx = dy = 0.0
y = tro.position[1] + dy / d * COORD_MIN_ABSTAND if echt and station.position:
return (x, y, z) dx, dy = station.position[0] - echt[0], station.position[1] - echt[1]
if math.hypot(dx, dy) < 1.0:
dx, dy = 0.0, -1.0
winkel = math.atan2(dy, dx) + math.radians(
(k - (len(stationen) - 1) / 2) * COORD_PFEIL_FAECHER)
ergebnis[station.tro_id] = (ax + math.cos(winkel) * COORD_PFEIL_LAENGE,
ay + math.sin(winkel) * COORD_PFEIL_LAENGE)
return ergebnis
def _tro_kurzinfo(tro: Tro) -> str: def _tro_kurzinfo(tro: Tro) -> str:
@@ -1765,19 +1816,18 @@ def _legend_pos_rechts(zeichen: dict[str, tuple[float, float]]) -> tuple[float,
def _zeichen_positionen( def _zeichen_positionen(
analysis: Analysis, compact: bool analysis: Analysis, compact: bool
) -> dict[str, tuple[float, float]]: ) -> dict[str, tuple[float, float]]:
"""Zeichenposition (mm) je TRO/Systemgrenze fuer --use-cords: Anlagen- """Zeichenposition (mm) je TRO/Systemgrenze fuer --use-cords: TROs auf
koordinate, Systemgrenzen ggf. beiseitegeschoben (_anzeige_position), ihrer Anlagenkoordinate, mit compact die Leerstrecken gekuerzt
mit compact die Leerstrecken gekuerzt (_kompakte_positionen). Reine (_kompakte_positionen); die Stationsbeschriftungen danach relativ zum
Darstellung - Tro.position und die Tooltip-Koordinaten bleiben echt.""" angehaengten TRO (_station_positionen). Reine Darstellung - Tro.position
und die Tooltip-Koordinaten bleiben echt."""
roh: dict[str, tuple[float, float]] = {} roh: dict[str, tuple[float, float]] = {}
for tro in analysis.tros: for tro in analysis.tros:
if tro.position: if tro.position:
roh[tro.tro_id] = (tro.position[0], tro.position[1]) roh[tro.tro_id] = (tro.position[0], tro.position[1])
for boundary in analysis.boundaries: zeichen = _kompakte_positionen(roh, COORD_MAX_LUECKE) if compact else roh
position = _anzeige_position(boundary, analysis.tros) zeichen.update(_station_positionen(analysis, zeichen))
if position: return zeichen
roh[boundary.tro_id] = (position[0], position[1])
return _kompakte_positionen(roh, COORD_MAX_LUECKE) if compact else roh
# Kantenstil je Art (an material_flow.py angelehnt: Umlauf hellblau gestrichelt, # Kantenstil je Art (an material_flow.py angelehnt: Umlauf hellblau gestrichelt,
@@ -1791,6 +1841,13 @@ TRO_EDGE_STYLE = {
} }
# Farbe der Zu-/Ablaufpfeile und ihrer Stationsbeschriftung
STATION_FARBE = {
BOUNDARY_ENTRY: "#2e8b57", # gruen: Material kommt herein
BOUNDARY_EXIT: "#c0392b", # rot: Material geht hinaus
}
def _tro_edge_attrs(kind: str | None) -> str: def _tro_edge_attrs(kind: str | None) -> str:
return TRO_EDGE_STYLE.get(kind or "normal", "") return TRO_EDGE_STYLE.get(kind or "normal", "")
@@ -1864,18 +1921,23 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
# Systemgrenzen (BTMT Be-/Entladestationen): kein Katalogtyp, eigene # Systemgrenzen (BTMT Be-/Entladestationen): kein Katalogtyp, eigene
# Farbe/Form (ext-Stil, Ellipse statt Box) - deutlich als "kein TRO" # Farbe/Form (ext-Stil, Ellipse statt Box) - deutlich als "kein TRO"
# erkennbar, aber sichtbar als tatsaechlicher Anfang/Ende des Flusses. # erkennbar, aber sichtbar als tatsaechlicher Anfang/Ende des Flusses.
# Systemgrenzen (Be-/Entladung, Eingang/Ausgang) sind kein TRO und kein
# Kettenglied: sie erscheinen nur als Beschriftung am Ende eines farbigen
# Zu- bzw. Ablaufpfeils (Review 2026-10-02).
boundary_ids = {b.tro_id for b in analysis.boundaries}
for boundary in analysis.boundaries: for boundary in analysis.boundaries:
rendered = ", ".join(f'{k}="{v}"' for k, v in STYLE_EXT.dot_attrs().items()) farbe = STATION_FARBE[boundary.type_name]
pos = _pos(boundary) add(f' "{boundary.tro_id}" [shape=plaintext, style="", fontsize=9, '
if use_coords: f'fontcolor="{farbe}", label="{_dot_escape(boundary.label)}", '
attrs = dict(STYLE_EXT.dot_attrs(), shape="ellipse") f'tooltip="{_tro_tooltip(boundary)}"{_pos(boundary)}];')
add(f' "{boundary.tro_id}" [{_coord_node_attrs(boundary, attrs)}{pos}];')
else:
add(f' "{boundary.tro_id}" [label="{_tro_label(boundary)}", '
f'shape=ellipse, style=filled, {rendered}{pos}];')
add("") add("")
for src, dst in analysis.edges: for src, dst in analysis.edges:
if src in boundary_ids or dst in boundary_ids:
farbe = STATION_FARBE[BOUNDARY_ENTRY if src in boundary_ids else BOUNDARY_EXIT]
add(f' "{src}" -> "{dst}" [color="{farbe}", penwidth=2.0, arrowsize=0.9, '
f'tooltip="{"Materialeingang" if src in boundary_ids else "Materialausgang"}"];')
continue
add(f' "{src}" -> "{dst}" [{_tro_edge_attrs(analysis.edge_kinds.get((src, dst)))}];') add(f' "{src}" -> "{dst}" [{_tro_edge_attrs(analysis.edge_kinds.get((src, dst)))}];')
# "gleiche Bahn"-Punktlinien nur in der berechneten Anordnung: lagerichtig # "gleiche Bahn"-Punktlinien nur in der berechneten Anordnung: lagerichtig
@@ -1906,8 +1968,10 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
f'<td align="left">{d.describe_symbol()}</td></tr>' f'<td align="left">{d.describe_symbol()}</td></tr>'
for d in legend_types for d in legend_types
) )
rows += (f'<tr><td bgcolor="{STYLE_EXT.fill}" align="left"> Systemgrenze </td>' rows += (f'<tr><td align="left"><font color="{STATION_FARBE[BOUNDARY_ENTRY]}">'
f'<td align="left">Ellipse (Materialein-/ausgang, kein TRO)</td></tr>') f'gruener Pfeil</font></td><td align="left">Materialeingang (Beladung / Eingang)</td></tr>'
f'<tr><td align="left"><font color="{STATION_FARBE[BOUNDARY_EXIT]}">'
f'roter Pfeil</font></td><td align="left">Materialausgang (Entladung / Ausgang)</td></tr>')
legend_label = ('<<table border="0" cellborder="1" cellspacing="0" cellpadding="3">' legend_label = ('<<table border="0" cellborder="1" cellspacing="0" cellpadding="3">'
+ rows + rows
+ '<tr><td align="left"><font color="#2f5597">durchgezogen</font></td>' + '<tr><td align="left"><font color="#2f5597">durchgezogen</font></td>'