TRO-Fluss --use-cords: Leerstrecken kuerzen fuer kompaktere Darstellung

Je Achse bleiben Reihenfolge und echte Abstaende der Knoten bis
COORD_MAX_LUECKE (1500 mm) erhalten, groessere Leerstrecken (lange Foerderer
ohne TRO) werden darauf gekuerzt (_achse_kompakt/_kompakte_positionen,
_zeichen_positionen). Legende naeher an der Anlage. Reine Darstellung:
Knoten, Kanten, Labels und Tooltips (mit echten Koordinaten) unveraendert -
DOT ohne pos-Attribute fuer HundM05 und Mubea identisch. Mubea 2929x3333 ->
2212x2411 px. --true-scale zeichnet weiterhin masstaeblich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-01 15:49:31 +02:00
co-authored by Claude Opus 5.5
parent 9ac6f88123
commit fcfc4f1278
+82 -24
View File
@@ -177,6 +177,13 @@ COORD_SCALE = 0.1
# 186 mm neben TRO04); sie wird dann nur in der Darstellung beiseitegeschoben.
COORD_MIN_ABSTAND = 600
# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in
# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne
# TRO) werden auf diesen Wert gekuerzt - Reihenfolge und Nachbarschaft der
# Knoten bleiben erhalten, nur der Leerraum verschwindet (Review 2026-10-01:
# "zu verstreut, zu viel Weissraum"). --true-scale schaltet das ab.
COORD_MAX_LUECKE = 1500
# Abstand der Legende zum Anlagenrand in mm
LEGEND_MARGIN = 3000
@@ -1665,6 +1672,28 @@ COORD_SYMBOL_SIZE: dict[str, tuple[float, float]] = {
}
def _achse_kompakt(werte: list[float], max_luecke: float) -> dict[float, float]:
"""Koordinaten einer Achse -> gekuerzte Koordinaten: Abstaende bis
max_luecke bleiben, groessere werden auf max_luecke gekuerzt. Monoton,
die Reihenfolge der Knoten bleibt also unveraendert."""
sortiert = sorted(set(werte))
if not sortiert:
return {}
abbildung = {sortiert[0]: sortiert[0]}
for vorher, wert in zip(sortiert, sortiert[1:]):
abbildung[wert] = abbildung[vorher] + min(wert - vorher, max_luecke)
return abbildung
def _kompakte_positionen(
positionen: dict[str, tuple[float, float]], max_luecke: float
) -> dict[str, tuple[float, float]]:
"""Knoten-Id -> Zeichenposition (mm) mit gekuerzten Leerstrecken."""
ax = _achse_kompakt([p[0] for p in positionen.values()], max_luecke)
ay = _achse_kompakt([p[1] for p in positionen.values()], max_luecke)
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:
@@ -1723,15 +1752,32 @@ def _coord_node_attrs(tro: Tro, base: dict[str, str]) -> str:
return ", ".join(f'{k}="{v}"' for k, v in attrs.items())
def _legend_pos_rechts(analysis: Analysis) -> tuple[float, float]:
def _legend_pos_rechts(zeichen: dict[str, tuple[float, float]]) -> tuple[float, float]:
"""Legende rechts neben der Anlage, obere Kante etwa auf Anlagenoberkante
(mm; pos ist bei neato -n der Knotenmittelpunkt)."""
all_nodes = analysis.tros + analysis.boundaries
xs = [t.position[0] for t in all_nodes if t.position]
ys = [t.position[1] for t in all_nodes if t.position]
if not xs:
(mm, Zeichenpositionen; pos ist bei neato -n der Knotenmittelpunkt)."""
if not zeichen:
return (0.0, 0.0)
return (max(xs) + LEGEND_MARGIN + 1500, max(ys) - 1500)
xs = [p[0] for p in zeichen.values()]
ys = [p[1] for p in zeichen.values()]
return (max(xs) + LEGEND_MARGIN, max(ys) - 1000)
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."""
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
# Kantenstil je Art (an material_flow.py angelehnt: Umlauf hellblau gestrichelt,
@@ -1749,16 +1795,31 @@ def _tro_edge_attrs(kind: str | None) -> str:
return TRO_EDGE_STYLE.get(kind or "normal", "")
def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str:
def render_dot(analysis: Analysis, source: str, use_coords: bool = False,
compact: bool = True) -> str:
"""
Gerichtetes Flussdiagramm der TROs als Graphviz-Quelle.
use_coords=False Anordnung berechnet 'dot' (Fluss von links nach rechts)
use_coords=True jeder Knoten wird auf seine Anlagenkoordinate gesetzt und
mit "neato -n" lagerichtig gezeichnet
compact nur mit use_coords: Leerstrecken ueber COORD_MAX_LUECKE
kuerzen (Reihenfolge der Knoten bleibt); False = masstaeblich
"""
out: list[str] = []
add = out.append
zeichen = _zeichen_positionen(analysis, compact) if use_coords else {}
def _pos(node: Tro) -> str:
if use_coords:
p = zeichen.get(node.tro_id)
return (f', pos="{p[0] * COORD_SCALE:.2f},{p[1] * COORD_SCALE:.2f}"'
if p else "")
# Ohne --use-cords ignoriert dot das pos, die Angabe bleibt aber als
# Information im DOT stehen.
if node.position:
return f', pos="{node.position[0] * 0.01:.2f},{node.position[1] * 0.01:.2f}"'
return ""
add("// Automatisch erzeugt von lib/tro_flow.py - nicht manuell aendern.")
add(f"// Quelle: {source}")
@@ -1793,13 +1854,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
"fillcolor": "#ffffff", "color": "#c00000", "fontcolor": "#c00000"
}
rendered = ", ".join(f'{k}="{v}"' for k, v in attrs.items())
# Anlagenkoordinate mitgeben. Ohne --use-cords ignoriert dot das pos,
# die Angabe bleibt aber als Information im DOT stehen.
pos = ""
if tro.position:
scale = COORD_SCALE if use_coords else 0.01
pos = (f', pos="{tro.position[0] * scale:.2f},'
f'{tro.position[1] * scale:.2f}"')
pos = _pos(tro)
if use_coords:
add(f' "{tro.tro_id}" [{_coord_node_attrs(tro, attrs)}{pos}];')
else:
@@ -1811,13 +1866,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
# erkennbar, aber sichtbar als tatsaechlicher Anfang/Ende des Flusses.
for boundary in analysis.boundaries:
rendered = ", ".join(f'{k}="{v}"' for k, v in STYLE_EXT.dot_attrs().items())
pos = ""
position = (_anzeige_position(boundary, analysis.tros) if use_coords
else boundary.position)
if position:
scale = COORD_SCALE if use_coords else 0.01
pos = (f', pos="{position[0] * scale:.2f},'
f'{position[1] * scale:.2f}"')
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}];')
@@ -1870,7 +1919,7 @@ def render_dot(analysis: Analysis, source: str, use_coords: bool = False) -> str
'<td align="left">gleiche Bahn / Strecke</td></tr>')
+ "</table>>")
if use_coords:
lx, ly = _legend_pos_rechts(analysis)
lx, ly = _legend_pos_rechts(zeichen)
add(f' legende [shape=plaintext, style="", fillcolor="none", fontsize=11, '
f'label={legend_label}, pos="{lx * COORD_SCALE:.2f},'
f'{ly * COORD_SCALE:.2f}"];')
@@ -2507,6 +2556,14 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"Anschlusspunkte als derselbe Uebergabepunkt gelten. "
"Standard: %(default)s",
)
parser.add_argument(
"--true-scale",
dest="true_scale",
action="store_true",
help="Nur mit --use-cords: masstaeblich zeichnen. Standard kuerzt "
f"Leerstrecken ueber {COORD_MAX_LUECKE} mm zwischen den Knoten "
"(Reihenfolge und Nachbarschaft bleiben erhalten).",
)
parser.add_argument(
"--use-overrides",
dest="use_overrides",
@@ -2630,7 +2687,8 @@ def main(argv: list[str] | None = None) -> int:
return 1
dot_file.write_text(
render_dot(analysis, csv_file.name, use_coords=args.use_cords),
render_dot(analysis, csv_file.name, use_coords=args.use_cords,
compact=not args.true_scale),
encoding="utf-8",
)