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:
co-authored by
Claude Opus 5.5
parent
9ac6f88123
commit
fcfc4f1278
+82
-24
@@ -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",
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user