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:
co-authored by
Claude Opus 5.5
parent
fcfc4f1278
commit
d0a3ac7e5a
+120
-56
@@ -172,10 +172,11 @@ GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
|
||||
# Zeichnung, in der Symbole und Schrift verschwanden; Review 2026-10-01).
|
||||
COORD_SCALE = 0.1
|
||||
|
||||
# Mindestabstand (mm) einer Systemgrenze zu einem TRO in der lagerichtigen
|
||||
# Zeichnung - eine BTMT-Station sitzt oft fast auf ihrem Separator (Mubea SYS1
|
||||
# 186 mm neben TRO04); sie wird dann nur in der Darstellung beiseitegeschoben.
|
||||
COORD_MIN_ABSTAND = 600
|
||||
# Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
|
||||
# (mm) ab dem angehaengten TRO und Faecherwinkel (Grad), wenn mehrere
|
||||
# Stationen am selben TRO haengen (siehe _station_positionen).
|
||||
COORD_PFEIL_LAENGE = 1300
|
||||
COORD_PFEIL_FAECHER = 45
|
||||
|
||||
# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in
|
||||
# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne
|
||||
@@ -1274,13 +1275,9 @@ def link_tros(
|
||||
def _add_chain_pair(a: str, b: str) -> None:
|
||||
if a == b:
|
||||
return
|
||||
# Be- und Entladestationen auf der Kreisel-Bahn werden DURCHFAHREN: der
|
||||
# Kreisel laeuft immer weiter, was nicht entladen wird, faehrt an der
|
||||
# naechsten Station vorbei. Systemgrenzen sind in der Umlaufkette daher
|
||||
# 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.
|
||||
# Keine Sperre fuer Systemgrenzen noetig: sie sind kein Kettenglied
|
||||
# (siehe station_hosts unten) und kommen hier nur als Zu-/Ablaufpfeil
|
||||
# in der richtigen Richtung an.
|
||||
pair = (a, b)
|
||||
edges.add(pair)
|
||||
edge_kinds.setdefault(pair, "bahn")
|
||||
@@ -1371,25 +1368,61 @@ def link_tros(
|
||||
speicher_ausschleus[el.teile_id] = store_by_host[strecke_id]
|
||||
else:
|
||||
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:
|
||||
if not el.is_circle or len(el.track_ids) < 2:
|
||||
continue
|
||||
chain = list(el.track_ids)
|
||||
if el.rotation in (ROTATION_CW, ROTATION_CCW):
|
||||
chain.append(chain[0]) # Umlauf schliessen
|
||||
laenge = len(chain)
|
||||
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-/
|
||||
# Einschleuselemente bewusst uebersprungen (unowned) - das beschreibt
|
||||
# den BYPASS fuer Verkehr, der NICHT abzweigt (z. B. TRO01 -> TRO03,
|
||||
# wenn dazwischen niemand abbiegt).
|
||||
# Hauptkette: Separatoren in Reihenfolge, Aus-/Einschleuselemente und
|
||||
# Stationen bewusst uebersprungen (unowned) - das beschreibt den BYPASS
|
||||
# fuer Verkehr, der NICHT abzweigt (z. B. TRO01 -> TRO03, wenn
|
||||
# dazwischen niemand abbiegt).
|
||||
owned = [(i, ketten_owner[tid]) for i, tid in enumerate(chain) if tid in ketten_owner]
|
||||
for (_, a), (_, b) in zip(owned, owned[1:]):
|
||||
_add_chain_pair(a, b)
|
||||
if owned:
|
||||
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-
|
||||
# Segments: vom Einschleuselement zum naechsten Halt DANACH in
|
||||
# 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()}
|
||||
|
||||
|
||||
def _anzeige_position(
|
||||
boundary: Tro, tros: list[Tro]
|
||||
) -> tuple[float, float, float] | None:
|
||||
"""Position einer Systemgrenze fuer die Zeichnung: liegt sie naeher als
|
||||
COORD_MIN_ABSTAND an einem TRO, wird sie radial von ihm weggeschoben."""
|
||||
if not boundary.position:
|
||||
return None
|
||||
x, y, z = boundary.position
|
||||
for tro in tros:
|
||||
if not tro.position:
|
||||
def _station_positionen(
|
||||
analysis: Analysis, tro_zeichen: dict[str, tuple[float, float]]
|
||||
) -> dict[str, tuple[float, float]]:
|
||||
"""Zeichenposition der Stationsbeschriftung (Ende des Zu-/Ablaufpfeils).
|
||||
|
||||
Feste Pfeillaenge COORD_PFEIL_LAENGE ab dem angehaengten TRO (Beladung:
|
||||
der TRO danach, Entladung: der TRO davor), Richtung zur echten Station -
|
||||
so bleibt der Pfeil sichtbar, auch wenn die Station praktisch auf dem TRO
|
||||
sitzt (Mubea SYS1 186 mm neben TRO04). Haengen mehrere Stationen am selben
|
||||
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
|
||||
dx, dy = x - tro.position[0], y - tro.position[1]
|
||||
d = math.hypot(dx, dy)
|
||||
if d >= COORD_MIN_ABSTAND:
|
||||
continue
|
||||
if d == 0:
|
||||
dx, dy, d = 0.0, -1.0, 1.0
|
||||
x = tro.position[0] + dx / d * COORD_MIN_ABSTAND
|
||||
y = tro.position[1] + dy / d * COORD_MIN_ABSTAND
|
||||
return (x, y, z)
|
||||
je_tro.setdefault(anker, []).append(station)
|
||||
|
||||
for anker, stationen in je_tro.items():
|
||||
ax, ay = tro_zeichen[anker]
|
||||
echt = tro_by_id[anker].position
|
||||
for k, station in enumerate(sorted(stationen, key=lambda s: s.tro_id)):
|
||||
dx = dy = 0.0
|
||||
if echt and station.position:
|
||||
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:
|
||||
@@ -1765,19 +1816,18 @@ def _legend_pos_rechts(zeichen: dict[str, tuple[float, float]]) -> tuple[float,
|
||||
def _zeichen_positionen(
|
||||
analysis: Analysis, compact: bool
|
||||
) -> dict[str, tuple[float, float]]:
|
||||
"""Zeichenposition (mm) je TRO/Systemgrenze fuer --use-cords: Anlagen-
|
||||
koordinate, Systemgrenzen ggf. beiseitegeschoben (_anzeige_position),
|
||||
mit compact die Leerstrecken gekuerzt (_kompakte_positionen). Reine
|
||||
Darstellung - Tro.position und die Tooltip-Koordinaten bleiben echt."""
|
||||
"""Zeichenposition (mm) je TRO/Systemgrenze fuer --use-cords: TROs auf
|
||||
ihrer Anlagenkoordinate, mit compact die Leerstrecken gekuerzt
|
||||
(_kompakte_positionen); die Stationsbeschriftungen danach relativ zum
|
||||
angehaengten TRO (_station_positionen). Reine Darstellung - Tro.position
|
||||
und die Tooltip-Koordinaten bleiben echt."""
|
||||
roh: dict[str, tuple[float, float]] = {}
|
||||
for tro in analysis.tros:
|
||||
if tro.position:
|
||||
roh[tro.tro_id] = (tro.position[0], tro.position[1])
|
||||
for boundary in analysis.boundaries:
|
||||
position = _anzeige_position(boundary, analysis.tros)
|
||||
if position:
|
||||
roh[boundary.tro_id] = (position[0], position[1])
|
||||
return _kompakte_positionen(roh, COORD_MAX_LUECKE) if compact else roh
|
||||
zeichen = _kompakte_positionen(roh, COORD_MAX_LUECKE) if compact else roh
|
||||
zeichen.update(_station_positionen(analysis, zeichen))
|
||||
return zeichen
|
||||
|
||||
|
||||
# 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:
|
||||
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
|
||||
# Farbe/Form (ext-Stil, Ellipse statt Box) - deutlich als "kein TRO"
|
||||
# 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:
|
||||
rendered = ", ".join(f'{k}="{v}"' for k, v in STYLE_EXT.dot_attrs().items())
|
||||
pos = _pos(boundary)
|
||||
if use_coords:
|
||||
attrs = dict(STYLE_EXT.dot_attrs(), shape="ellipse")
|
||||
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}];')
|
||||
farbe = STATION_FARBE[boundary.type_name]
|
||||
add(f' "{boundary.tro_id}" [shape=plaintext, style="", fontsize=9, '
|
||||
f'fontcolor="{farbe}", label="{_dot_escape(boundary.label)}", '
|
||||
f'tooltip="{_tro_tooltip(boundary)}"{_pos(boundary)}];')
|
||||
add("")
|
||||
|
||||
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)))}];')
|
||||
|
||||
# "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>'
|
||||
for d in legend_types
|
||||
)
|
||||
rows += (f'<tr><td bgcolor="{STYLE_EXT.fill}" align="left"> Systemgrenze </td>'
|
||||
f'<td align="left">Ellipse (Materialein-/ausgang, kein TRO)</td></tr>')
|
||||
rows += (f'<tr><td align="left"><font color="{STATION_FARBE[BOUNDARY_ENTRY]}">'
|
||||
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">'
|
||||
+ rows
|
||||
+ '<tr><td align="left"><font color="#2f5597">durchgezogen</font></td>'
|
||||
|
||||
Reference in New Issue
Block a user