Group parallel Gefaellestrecken in the material-flow diagram
A bank of gravity lanes between the same two Kreisel (same slope) was drawn as one box per lane, making large storage banks unreadable. render_dot now collapses such lanes into a single Gefaellestrecke-styled block with a double border, labelled with the id range and combined Separator/Scanner ranges. Purely visual - the graph, --doc report and --connect output still list every lane individually.
This commit is contained in:
+116
-1
@@ -138,6 +138,12 @@ NODE_STYLES = {
|
||||
}
|
||||
DEFAULT_NODE_STYLE = dict(fillcolor="#f2f2f2", fontcolor="#404040", color="#808080")
|
||||
|
||||
# Ab dieser Anzahl paralleler Gefaellestrecken zwischen denselben zwei
|
||||
# Kreisel-Bahnen (gleicher Zulauf, gleicher Ablauf, gleiche Steigung) werden
|
||||
# sie im Materialfluss-Diagramm zu einem Block zusammengefasst - sonst wird
|
||||
# eine grosse Linienbank (Speicher) unlesbar breit.
|
||||
MIN_GEFAELLE_GROUP = 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Datenmodell
|
||||
@@ -999,6 +1005,78 @@ def _pos_attr(node: "Node", use_coords: bool) -> str:
|
||||
return f', pos="{xy[0] * COORD_SCALE:.2f},{xy[1] * COORD_SCALE:.2f}"'
|
||||
|
||||
|
||||
def _flow_neighbours(node_id: str, edges: list[Edge]) -> tuple[str, str] | None:
|
||||
"""
|
||||
(Vorgaenger, Nachfolger) eines Knotens im Materialfluss.
|
||||
|
||||
None, wenn der Knoten nicht genau einen ankommenden und einen abgehenden
|
||||
EDGE_FLOW hat - dann ist er kein einfacher Durchlaufknoten zwischen zwei
|
||||
festen Nachbarn.
|
||||
"""
|
||||
incoming = {e.src for e in edges if e.dst == node_id and e.kind == EDGE_FLOW}
|
||||
outgoing = {e.dst for e in edges if e.src == node_id and e.kind == EDGE_FLOW}
|
||||
if len(incoming) != 1 or len(outgoing) != 1:
|
||||
return None
|
||||
return next(iter(incoming)), next(iter(outgoing))
|
||||
|
||||
|
||||
def _group_parallel_gefaelle(graph: Graph) -> list[list[Node]]:
|
||||
"""
|
||||
Parallele Gefaellestrecken zwischen denselben zwei Kreisel-Bahnen zu
|
||||
Gruppen zusammenfassen (z. B. eine Bank aus Schwerkraftlinien).
|
||||
|
||||
Kriterium: gleicher Zulauf- und Ablauf-Knoten (jeweils eine Kreisel-Bahn)
|
||||
und gleiche Steigung (Hoehe_Von/Hoehe_Bis) - "gleich lang und gleiche
|
||||
Steigung" laesst sich aus dem Export nur ueber die Hoehenangabe pruefen,
|
||||
die Laenge ergibt sich daraus, dass beide Enden an denselben zwei Knoten
|
||||
haengen. Rein fuer die Darstellung; Graph-Knoten/-Kanten bleiben unveraendert.
|
||||
"""
|
||||
groups: dict[tuple, list[Node]] = {}
|
||||
for node_id in sorted(graph.nodes):
|
||||
node = graph.nodes[node_id]
|
||||
if node.kind != "Gefaellestrecke":
|
||||
continue
|
||||
neighbours = _flow_neighbours(node_id, graph.edges)
|
||||
if neighbours is None:
|
||||
continue
|
||||
before, after = neighbours
|
||||
if graph.nodes[before].kind != "Kreisel" or graph.nodes[after].kind != "Kreisel":
|
||||
continue
|
||||
element = node.element
|
||||
key = (before, after, element.height_from, element.height_to)
|
||||
groups.setdefault(key, []).append(node)
|
||||
return [members for members in groups.values() if len(members) >= MIN_GEFAELLE_GROUP]
|
||||
|
||||
|
||||
def _id_range(ids: list[str]) -> str:
|
||||
"""Sortierte Ids als Bereich, z. B. ['0037', '0056'] -> '0037 - 0056'."""
|
||||
ids = sorted(ids)
|
||||
return ids[0] if len(ids) == 1 else f"{ids[0]} - {ids[-1]}"
|
||||
|
||||
|
||||
def _gefaelle_group_label(members: list[Node]) -> str:
|
||||
"""Sammel-Label einer Gefaellestrecken-Gruppe: Bereich der Linien-Ids,
|
||||
gemeinsame Steigung, zusammengefasste Separatoren/Scanner."""
|
||||
element = members[0].element
|
||||
ids = sorted(m.node_id for m in members)
|
||||
lines = [
|
||||
f"Gefaellestrecke {ids[0]} -> {ids[-1]} ({len(ids)}x)",
|
||||
"Gruppe paralleler Schwerkraftlinien",
|
||||
]
|
||||
if element.height is not None:
|
||||
lines.append(f"h = {element.height:.3f} m")
|
||||
if element.rise is not None:
|
||||
uphill = drive_uphill(element)[0]
|
||||
arrow = {True: "aufwaerts", False: "abwaerts", None: "Richtung offen"}[uphill]
|
||||
lines.append(f"{element.height_from:.0f} -> {element.height_to:.0f} mm "
|
||||
f"({element.rise:+.0f}) = {arrow}")
|
||||
for kind in DEVICE_KINDS:
|
||||
ids_dev = sorted({d for m in members for d in m.devices.get(kind, [])})
|
||||
if ids_dev:
|
||||
lines.append(f"{kind} ({len(ids_dev)}): {_id_range(ids_dev)}")
|
||||
return _dot_label(lines)
|
||||
|
||||
|
||||
def render_dot(graph: Graph, use_coords: bool = False) -> str:
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
@@ -1047,14 +1125,48 @@ def render_dot(graph: Graph, use_coords: bool = False) -> str:
|
||||
add(" }")
|
||||
add("")
|
||||
|
||||
# Parallele Gefaellestrecken (gleicher Zu-/Ablauf, gleiche Steigung) zu
|
||||
# je einem Block zusammenfassen, damit eine Linienbank nicht als N
|
||||
# einzelne Kaesten gezeichnet wird.
|
||||
gefaelle_groups = _group_parallel_gefaelle(graph)
|
||||
group_of: dict[str, str] = {}
|
||||
for members in gefaelle_groups:
|
||||
group_id = f"grp_{members[0].node_id}"
|
||||
for member in members:
|
||||
group_of[member.node_id] = group_id
|
||||
grouped_ids = set(group_of)
|
||||
|
||||
for node in plain:
|
||||
if node.node_id in grouped_ids:
|
||||
continue
|
||||
style = NODE_STYLES.get(node.kind, DEFAULT_NODE_STYLE)
|
||||
attrs = ", ".join(f'{key}="{value}"' for key, value in style.items())
|
||||
add(f' "{node.node_id}" [label="{_node_label(node)}", {attrs}'
|
||||
f'{_pos_attr(node, use_coords)}];')
|
||||
|
||||
for members in gefaelle_groups:
|
||||
group_id = f"grp_{members[0].node_id}"
|
||||
style = NODE_STYLES.get("Gefaellestrecke", DEFAULT_NODE_STYLE)
|
||||
attrs = ", ".join(f'{key}="{value}"' for key, value in style.items())
|
||||
pos = ""
|
||||
if use_coords:
|
||||
xs = [xy[0] for m in members if (xy := _node_xy(m))]
|
||||
ys = [xy[1] for m in members if (xy := _node_xy(m))]
|
||||
if xs and ys:
|
||||
pos = (f', pos="{sum(xs) / len(xs) * COORD_SCALE:.2f},'
|
||||
f'{sum(ys) / len(ys) * COORD_SCALE:.2f}"')
|
||||
add(f' "{group_id}" [label="{_gefaelle_group_label(members)}", {attrs}, '
|
||||
f'peripheries=2{pos}];')
|
||||
add("")
|
||||
|
||||
seen_group_edges: set[tuple[str, str]] = set()
|
||||
for edge in graph.edges:
|
||||
src = group_of.get(edge.src, edge.src)
|
||||
dst = group_of.get(edge.dst, edge.dst)
|
||||
if edge.src in grouped_ids or edge.dst in grouped_ids:
|
||||
if src == dst or (src, dst) in seen_group_edges:
|
||||
continue
|
||||
seen_group_edges.add((src, dst))
|
||||
if edge.kind == EDGE_CIRCLE:
|
||||
# Richtung kommt aus der Drehrichtung und steht als Label an der Kante
|
||||
label = f', xlabel="{_dot_escape(edge.note)}"' if edge.note else ""
|
||||
@@ -1070,7 +1182,7 @@ def render_dot(graph: Graph, use_coords: bool = False) -> str:
|
||||
'fontcolor="#cc8800", tooltip="Weiche (Kreisel-Uebergang)"]')
|
||||
else:
|
||||
attrs = "[]"
|
||||
add(f' "{edge.src}" -> "{edge.dst}" {attrs};')
|
||||
add(f' "{src}" -> "{dst}" {attrs};')
|
||||
add("")
|
||||
|
||||
legend_pos = ""
|
||||
@@ -1094,6 +1206,9 @@ def render_dot(graph: Graph, use_coords: bool = False) -> str:
|
||||
f'{ROTATION_CCW} = {ROTATION_FLOW[ROTATION_CCW]})</td></tr>'
|
||||
'<tr><td align="left"><font color="#bf8f00">gestrichelt gelb</font></td>'
|
||||
'<td align="left">Richtung nicht bestimmbar</td></tr>'
|
||||
'<tr><td align="left">doppelte Umrandung</td>'
|
||||
'<td align="left">Gruppe paralleler Gefaellestrecken (gleicher Zu-/Ablauf, '
|
||||
"gleiche Steigung)</td></tr>"
|
||||
"</table>>];")
|
||||
add(" }")
|
||||
add("}")
|
||||
|
||||
Reference in New Issue
Block a user