neuer Schalter --chain-graph, BTMT Eingang/Ausgang erkannt, Foerderer-Merkmal in tro_flow
--chain-graph zeichnet einen massstabsgetreuen Chain-/Lageplan der groben Objekte (Kreisel-Bahnhaelften, Strecke/Gefaellestrecke) als <name>_chain.svg, ohne Graphviz. BTMT-Stationen werden jetzt auch an "Eingang"/"Ausgang" statt nur "Beladung"/"Entladung" erkannt und bevorzugt ueber TrackIds statt Nachbarn verbunden. tro_flow.py erkennt bei Aus-/Einschleuselementen jetzt auch "Foerderer" als Merkmal-Schreibweise fuer die Strecke, nicht nur "Strecke". Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
516f30278b
commit
80e7acc8dd
+567
-50
@@ -16,8 +16,24 @@ Knotenmodell
|
||||
"Drehrichtung": UZS = rechts nach
|
||||
links, GUZ = links nach rechts
|
||||
* Separator / Scanner -> kein eigener Knoten; sie werden
|
||||
ueber "Zuordnung" am Label des
|
||||
uebergeordneten Knotens angezeigt
|
||||
ueber "Zuordnung" (bevorzugt) oder
|
||||
"TrackIds" des Containers (siehe
|
||||
unten) am Label des uebergeordneten
|
||||
Knotens angezeigt
|
||||
|
||||
Container und ihre Anbauteile (Spalte "TrackIds")
|
||||
--------------------------------------------------
|
||||
Seit Sept. 2026 listet ein Kreisel/Strecke/Gefaellestrecke in seiner eigenen
|
||||
Spalte "TrackIds" die TeileIds aller Anbauteile (Separator, Scanner, Weiche,
|
||||
Ein-/Ausschleuselement, BTMT Be-/Entladung), die zu ihm gehoeren. Fuer
|
||||
Separator/Scanner/Beladung/Entladung bleibt die praezisere "Zuordnung" des
|
||||
Anbauteils selbst massgeblich, solange sie sich aufloesen laesst; TrackIds
|
||||
schliesst nur die Luecken (fehlende/unaufloesbare Zuordnung, siehe
|
||||
_attach_devices/_attach_stations). Transferelemente (Weiche/Ein-/
|
||||
Ausschleuselement) werden dagegen weiterhin ausschliesslich ueber ihre
|
||||
eigenen Merkmale ("Kreisel"/"Strecke"/"Kreisel A"/"Kreisel B") verdrahtet -
|
||||
dass sie in TrackIds zweier Container auftauchen (sie verbinden ja genau
|
||||
diese zwei), ist normal, kein Datenfehler.
|
||||
|
||||
Kantenmodell
|
||||
------------
|
||||
@@ -90,16 +106,18 @@ DEVICE_KINDS = ("Separator", "Scanner")
|
||||
# Ein-/Ausgangspunkte des Materialflusses. Sie werden in sps_skel erkannt;
|
||||
# dxfmakros exportiert sie derzeit NICHT (Blockmuster fehlt), die Erkennung
|
||||
# greift also, sobald eine passende Zeile im CSV auftaucht - erkannt an
|
||||
# TeileArt/Bezeichnung: "Beladung"/"BTMT-Beladung" -> Eingang, "Entladung"/
|
||||
# "SC_Entladung"/"Abwurf" -> Ausgang.
|
||||
ENTRY_KINDS = ("Beladung",) # BTMT-Beladung -> Materialeingang (Quelle)
|
||||
EXIT_KINDS = ("Entladung",) # SC_Entladung -> Materialausgang (Senke)
|
||||
# TeileArt/Bezeichnung: "Beladung"/"BTMT-Beladung"/"Eingang" -> Eingang,
|
||||
# "Entladung"/"SC_Entladung"/"Abwurf"/"Ausgang" -> Ausgang (ein anderer Export
|
||||
# nennt dieselben Ein-/Ausgabepunkte schlicht "Eingang"/"Ausgang" statt der
|
||||
# BTMT-Namen - gleiche Rolle, andere Schreibweise).
|
||||
ENTRY_KINDS = ("Beladung",) # BTMT-Beladung/Eingang -> Materialeingang (Quelle)
|
||||
EXIT_KINDS = ("Entladung",) # SC_Entladung/Ausgang -> Materialausgang (Senke)
|
||||
STATION_KINDS = ENTRY_KINDS + EXIT_KINDS
|
||||
|
||||
# Schluesselwoerter zur Erkennung der Stationen (Kleinschreibung, Teilstring)
|
||||
STATION_PATTERNS = (
|
||||
("Beladung", ("beladung", "beladestation", "btmt-beladung", "btmt beladung")),
|
||||
("Entladung", ("entladung", "entladestation", "sc_entladung", "abwurf")),
|
||||
("Beladung", ("beladung", "beladestation", "btmt-beladung", "btmt beladung", "eingang")),
|
||||
("Entladung", ("entladung", "entladestation", "sc_entladung", "abwurf", "ausgang")),
|
||||
)
|
||||
|
||||
# Transfer-/Weichenelemente aus dem erweiterten Export. Sie bilden KEINE eigenen
|
||||
@@ -107,7 +125,10 @@ STATION_PATTERNS = (
|
||||
# Ausschleuselement = Kreisel -> Strecke (am Streckenanfang, Material verlaesst Kreisel)
|
||||
# Einschleuselement = Strecke -> Kreisel (am Streckenende, Material laeuft in Kreisel)
|
||||
# Weiche = Uebergang zwischen zwei Kreiseln
|
||||
# Merkmale: {"Kreisel","Strecke","Streckenende"} bzw. {"Kreisel A","Kreisel B"}.
|
||||
# Merkmale: {"Kreisel","Strecke"/"Foerderer","Streckenende"} bzw. {"Kreisel A","Kreisel B"}.
|
||||
# "Strecke" und "Foerderer" sind zwei Schreibweisen desselben Merkmals, die
|
||||
# nebeneinander in ein und demselben Export vorkommen (nicht projektabhaengig
|
||||
# aufteilbar) - siehe _apply_transfers.
|
||||
TRANSFER_KINDS = ("Einschleus", "Ausschleus", "Weiche")
|
||||
TRANSFER_PATTERNS = (
|
||||
("Ausschleus", ("ausschleus",)),
|
||||
@@ -346,6 +367,11 @@ class TrackNode:
|
||||
element: Element | None = None
|
||||
placeholder: bool = False
|
||||
note: str = ""
|
||||
# Zuordnung des naechstgelegenen Scanners (siehe build_track_graph, Schritt 5)
|
||||
# - Scanner bilden weiterhin keinen eigenen Knoten (TRACK_KINDS), aber
|
||||
# render_track_dot() markiert den betroffenen Separator damit per rotem
|
||||
# Punkt statt einen zusaetzlichen Scanner-Knoten einzufuehren.
|
||||
scanner_zuordnung: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -479,9 +505,36 @@ def parse_neighbours(raw: str) -> list[tuple[str, str | None]]:
|
||||
return result
|
||||
|
||||
|
||||
def parse_track_ids(raw: str) -> list[str]:
|
||||
"""'0118, 0059, 0119' -> ['0118', '0059', '0119'] (Reihenfolge erhalten)."""
|
||||
return [token.strip() for token in (raw or "").split(",") if token.strip()]
|
||||
|
||||
# def parse_track_ids(raw: str) -> list[str]:
|
||||
# """'0118, 0059, 0119' -> ['0118', '0059', '0119'] (Reihenfolge erhalten)."""
|
||||
# return [token.strip() for token in (raw or "").split(",") if token.strip()]
|
||||
|
||||
def parse_track_ids(raw: str, *, context: str = "", warnings: list[str] | None = None) -> list[str]:
|
||||
"""'0040, 0076, 0036' -> ['0040', '0076', '0036']
|
||||
|
||||
Spalte 'TrackIds' (siehe Element.track_ids): anders als 'Nachbarn' ohne
|
||||
Bahn-Suffix (-L/-R) - TrackIds nennt nur die TeileId, nie eine Kreiselbahn.
|
||||
|
||||
Der ILS-2.0-Export nennt eine TeileId in dieser Spalte gelegentlich doppelt
|
||||
(Datenfehler im CAD-Export, kein sps_skel-Artefakt) - ohne Entduplizierung
|
||||
wuerde das in _build_track_owners wie zwei verschiedene Container aussehen
|
||||
und faelschlich eine "mehrere Container"-Mehrdeutigkeit ausloesen.
|
||||
"""
|
||||
seen: set[str] = set()
|
||||
result: list[str] = []
|
||||
for token in (raw or "").split(","):
|
||||
token = token.strip()
|
||||
if not token:
|
||||
continue
|
||||
if token in seen:
|
||||
if warnings is not None:
|
||||
warnings.append(f"{context}: TrackIds nennt '{token}' mehrfach - Duplikat ignoriert")
|
||||
continue
|
||||
seen.add(token)
|
||||
result.append(token)
|
||||
return result
|
||||
|
||||
|
||||
|
||||
def _match_kind(patterns, *texts: str) -> str | None:
|
||||
@@ -552,7 +605,11 @@ def read_elements(path: Path, warnings: list[str]) -> list[Element]:
|
||||
name=(row.get("Bezeichnung") or "").strip(),
|
||||
plan_square=(row.get("Planquadrat") or "").strip(),
|
||||
neighbours=parse_neighbours(row.get("Nachbarn") or ""),
|
||||
track_ids=parse_track_ids(row.get("TrackIds") or ""),
|
||||
track_ids=parse_track_ids(
|
||||
row.get("TrackIds") or "",
|
||||
context=f"Zeile {offset} ({teile_art} {teile_id})",
|
||||
warnings=warnings
|
||||
),
|
||||
merkmale=merkmale,
|
||||
row=offset,
|
||||
position=parse_position(row.get("Position") or ""),
|
||||
@@ -603,25 +660,122 @@ def _build_nodes(elements: list[Element], warnings: list[str]) -> dict[str, Node
|
||||
return nodes
|
||||
|
||||
|
||||
def _build_track_owners(elements: list[Element], warnings: list[str]) -> dict[str, list[str]]:
|
||||
"""
|
||||
Rueckindex der Spalte 'TrackIds': welche(r) Container (Kreisel/Strecke/
|
||||
Gefaellestrecke) nennt ein Anbauteil (Separator/Scanner/Weiche/Ein-/
|
||||
Ausschleuselement/Beladung/Entladung) in seiner eigenen 'TrackIds'-Liste.
|
||||
|
||||
Transferelemente (Weiche/Einschleus/Ausschleus, siehe TRANSFER_KINDS)
|
||||
verbinden zwei Container und werden folgerichtig von beiden genannt -
|
||||
mehrere Nennungen sind dort normal (ihre Verbindung wird ohnehin ueber die
|
||||
eigenen Merkmale "Kreisel"/"Strecke"/"Kreisel A"/"Kreisel B" bestimmt, nicht
|
||||
hierueber). Fuer alle anderen Anbauteile (genau ein Besitzer erwartet)
|
||||
macht das keinen Sinn - siehe _track_owner.
|
||||
"""
|
||||
by_id = {element.teile_id: element for element in elements}
|
||||
owners: dict[str, list[str]] = {}
|
||||
for element in elements:
|
||||
if not (element.is_transport or element.is_circle):
|
||||
continue
|
||||
for fine_id in element.track_ids:
|
||||
if fine_id == element.teile_id:
|
||||
warnings.append(f"{element.describe()}: TrackIds verweist auf sich selbst - ignoriert")
|
||||
continue
|
||||
if fine_id not in by_id:
|
||||
warnings.append(
|
||||
f"{element.describe()}: TrackIds verweist auf unbekanntes Element '{fine_id}' - ignoriert"
|
||||
)
|
||||
continue
|
||||
owners.setdefault(fine_id, []).append(element.teile_id)
|
||||
return owners
|
||||
|
||||
|
||||
def _track_owner(
|
||||
element: Element, owners_of: dict[str, list[str]], by_id: dict[str, Element],
|
||||
warnings: list[str]
|
||||
) -> Element | None:
|
||||
"""
|
||||
Container-Element zu einem Anbauteil ueber 'TrackIds' (Rueckindex), oder
|
||||
None, wenn TrackIds dazu nichts sagt oder (fuer ein Anbauteil mit genau
|
||||
einem erwarteten Besitzer) mehrdeutig ist. Der Aufrufer faellt dann auf die
|
||||
bisherige Zuordnung/Nachbarn-Ableitung zurueck.
|
||||
"""
|
||||
owners = owners_of.get(element.teile_id) or []
|
||||
if not owners:
|
||||
return None
|
||||
if len(owners) > 1:
|
||||
warnings.append(
|
||||
f"{element.describe()}: TrackIds nennt mehrere Container ({', '.join(sorted(owners))}) - "
|
||||
f"mehrdeutig, bisherige Zuordnung/Nachbarn-Ableitung verwendet"
|
||||
)
|
||||
return None
|
||||
return by_id.get(owners[0])
|
||||
|
||||
|
||||
def _owner_node_ids(owner: Element, element: Element, nodes: dict[str, Node]) -> list[str]:
|
||||
"""
|
||||
Knoten-Id(s) eines per TrackIds gefundenen Container-Elements - nur wenn
|
||||
die eigene 'Zuordnung' des Anbauteils schon gescheitert ist (siehe
|
||||
_attach_devices: leer, oder ohne passenden Knoten).
|
||||
|
||||
TrackIds kennt nur den Kreisel als Ganzes, nicht die Bahn (-L/-R); da eine
|
||||
aufloesbare Zuordnung bereits Vorrang hatte, bleibt hier nur der Fall
|
||||
"Bahn unbekannt" - wie mit E-Planung abgestimmt, wird dann an beide Bahnen
|
||||
angehaengt.
|
||||
"""
|
||||
if owner.is_circle:
|
||||
return [node_id for lane in (LANE_LEFT, LANE_RIGHT)
|
||||
if (node_id := f"{owner.teile_id}-{lane}") in nodes]
|
||||
return [owner.teile_id] if owner.teile_id in nodes else []
|
||||
|
||||
|
||||
def _attach_devices(
|
||||
elements: list[Element], nodes: dict[str, Node], warnings: list[str]
|
||||
elements: list[Element], nodes: dict[str, Node], by_id: dict[str, Element],
|
||||
owners_of: dict[str, list[str]], warnings: list[str]
|
||||
) -> list[Element]:
|
||||
"""Separatoren/Scanner ueber 'Zuordnung' an ihren Knoten haengen."""
|
||||
"""
|
||||
Separatoren/Scanner an ihren Knoten haengen.
|
||||
|
||||
Die eigene 'Zuordnung' des Anbauteils bleibt massgeblich, solange sie auf
|
||||
einen existierenden Knoten zeigt - sie ist praeziser als 'TrackIds', das
|
||||
bei einem Anbauteil direkt an einer Kreisel-Strecke-Grenze (Ausschleus-/
|
||||
Einschleuselement) auch den benachbarten Kreisel nennt, obwohl das
|
||||
Anbauteil logisch zur Strecke gehoert (siehe _build_track_owners). Erst
|
||||
wenn die Zuordnung fehlt oder ins Leere zeigt (aeltere Exports ohne
|
||||
TrackIds-Spalte eingeschlossen), uebernimmt 'TrackIds' auf dem Container
|
||||
(siehe _track_owner) - das deckt u. a. Anbauteile ab, deren Zuordnung nur
|
||||
den Kreisel ohne Bahn (-L/-R) nennt.
|
||||
"""
|
||||
unassigned: list[Element] = []
|
||||
for element in elements:
|
||||
if not element.is_device:
|
||||
continue
|
||||
|
||||
target = element.assignment
|
||||
if target and target != UNASSIGNED and target in nodes:
|
||||
nodes[target].devices.setdefault(element.kind, []).append(element.teile_id)
|
||||
continue
|
||||
|
||||
owner = _track_owner(element, owners_of, by_id, warnings)
|
||||
if owner is not None:
|
||||
targets = _owner_node_ids(owner, element, nodes)
|
||||
if targets:
|
||||
for node_id in targets:
|
||||
nodes[node_id].devices.setdefault(element.kind, []).append(element.teile_id)
|
||||
continue
|
||||
warnings.append(
|
||||
f"{element.describe()}: TrackIds verweist auf Container {owner.teile_id}, "
|
||||
f"aber kein passender Knoten - nicht dargestellt"
|
||||
)
|
||||
|
||||
if not target or target == UNASSIGNED:
|
||||
unassigned.append(element)
|
||||
continue
|
||||
if target not in nodes:
|
||||
warnings.append(
|
||||
f"{element.describe()}: Zuordnung '{target}' zeigt auf keinen Knoten - nicht dargestellt"
|
||||
)
|
||||
unassigned.append(element)
|
||||
continue
|
||||
nodes[target].devices.setdefault(element.kind, []).append(element.teile_id)
|
||||
warnings.append(
|
||||
f"{element.describe()}: Zuordnung '{target}' zeigt auf keinen Knoten - nicht dargestellt"
|
||||
)
|
||||
unassigned.append(element)
|
||||
return unassigned
|
||||
|
||||
|
||||
@@ -842,19 +996,22 @@ def _nearest_kreisel(station: Element, elements: list[Element]) -> Element | Non
|
||||
|
||||
def _attach_stations(
|
||||
elements: list[Element], by_id: dict[str, Element], nodes: dict[str, Node],
|
||||
warnings: list[str]
|
||||
owners_of: dict[str, list[str]], warnings: list[str]
|
||||
) -> list[Edge]:
|
||||
"""
|
||||
Ein-/Ausgabestationen (BTMT Be-/Entladung) mit dem Fluss verbinden.
|
||||
|
||||
Eingang (Beladung) ist Quelle: Kante Station -> Ziel.
|
||||
Ausgang (Entladung) ist Senke: Kante Ziel -> Station.
|
||||
Ziel kommt aus den 'Nachbarn' des Exports; fehlen sie (dxfmakros exportiert
|
||||
fuer BTMT derzeit keine), haengen beide baulich direkt am naechstgelegenen
|
||||
Kreisel, auf dessen Eingangs- bzw. Ausgangsbahn (siehe _weiche_lane_roles):
|
||||
UZS -> Eingang Bahn R / Ausgang Bahn L, GUZ -> Eingang Bahn L / Ausgang
|
||||
Bahn R. Nur wenn kein Kreisel mit bekannter Drehrichtung gefunden wird,
|
||||
greift ersatzweise die naechstgelegene Bahn/Strecke per Abstand.
|
||||
Ziel kommt zuerst aus 'TrackIds' des Containers (siehe _track_owner) - ein
|
||||
Kreisel-Container legt ueber die Eingangs-/Ausgangsbahn fest (siehe
|
||||
_weiche_lane_roles): UZS -> Eingang Bahn R / Ausgang Bahn L, GUZ -> Eingang
|
||||
Bahn L / Ausgang Bahn R. Kennt TrackIds keinen Container (aeltere Exports
|
||||
ohne die Spalte), zaehlen ersatzweise die 'Nachbarn' des Exports (fuer BTMT
|
||||
inzwischen zwar meist gefuellt, nennen dort aber i. d. R. Separatoren oder
|
||||
die jeweils andere Station statt eines echten Flussknotens - siehe unten),
|
||||
dann baulich der naechstgelegene Kreisel, und zuletzt die naechstgelegene
|
||||
Bahn/Strecke per Abstand.
|
||||
"""
|
||||
edges: list[Edge] = []
|
||||
for element in elements:
|
||||
@@ -864,14 +1021,38 @@ def _attach_stations(
|
||||
if sid not in nodes:
|
||||
continue
|
||||
partners: list[str] = []
|
||||
for base, lane in element.neighbours:
|
||||
if lane and f"{base}-{lane}" in nodes:
|
||||
partners.append(f"{base}-{lane}")
|
||||
elif base in nodes:
|
||||
partners.append(base)
|
||||
owner = _track_owner(element, owners_of, by_id, warnings)
|
||||
if owner is not None and owner.is_circle:
|
||||
roles = _weiche_lane_roles(owner.teile_id, by_id, nodes)
|
||||
if roles:
|
||||
eingang, ausgang = roles
|
||||
lane, role = (eingang, "Eingangsbahn") if element.is_entry else (ausgang, "Ausgangsbahn")
|
||||
partners = [lane]
|
||||
warnings.append(
|
||||
f"{element.describe()}: aus TrackIds an Kreisel {owner.teile_id} "
|
||||
f"Bahn {lane.split('-')[-1]} angebunden ({role}, Drehrichtung {owner.rotation})"
|
||||
)
|
||||
else:
|
||||
partners += [f"{base}-{L}" for L in (LANE_LEFT, LANE_RIGHT)
|
||||
if f"{base}-{L}" in nodes]
|
||||
warnings.append(
|
||||
f"{element.describe()}: TrackIds nennt Kreisel {owner.teile_id}, aber dessen "
|
||||
f"Drehrichtung ist unbekannt - Bahn nicht bestimmbar"
|
||||
)
|
||||
elif owner is not None and owner.teile_id in nodes:
|
||||
partners = [owner.teile_id]
|
||||
warnings.append(f"{element.describe()}: aus TrackIds an {owner.describe()} angebunden")
|
||||
if not partners:
|
||||
# 'Nachbarn' verweist bei BTMT-Stationen haeufig auf Separatoren
|
||||
# oder die jeweils andere Station (naechste Objekte per Abstand,
|
||||
# keine echte Flussverbindung) - eine andere Station wird deshalb
|
||||
# nie als Partner akzeptiert, nur echte Flussobjekte.
|
||||
for base, lane in element.neighbours:
|
||||
if lane and f"{base}-{lane}" in nodes:
|
||||
partners.append(f"{base}-{lane}")
|
||||
elif base in nodes and not nodes[base].element.is_station:
|
||||
partners.append(base)
|
||||
else:
|
||||
partners += [f"{base}-{L}" for L in (LANE_LEFT, LANE_RIGHT)
|
||||
if f"{base}-{L}" in nodes]
|
||||
if not partners and (element.is_entry or element.is_exit):
|
||||
kreisel = _nearest_kreisel(element, elements)
|
||||
roles = _weiche_lane_roles(kreisel.teile_id, by_id, nodes) if kreisel else None
|
||||
@@ -963,7 +1144,7 @@ def _apply_transfers(
|
||||
for el in elements:
|
||||
if el.kind in ("Ausschleus", "Einschleus"):
|
||||
k = str(_merkmal(el.merkmale, "Kreisel") or "").strip()
|
||||
s = str(_merkmal(el.merkmale, "Strecke") or "").strip()
|
||||
s = str(_merkmal(el.merkmale, "Strecke", "Förderer") or "").strip()
|
||||
if k and s:
|
||||
role = "aus" if el.kind == "Ausschleus" else "ein"
|
||||
per_strecke.setdefault(s, {})[role] = k
|
||||
@@ -1070,7 +1251,8 @@ def _apply_transfers(
|
||||
def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Graph:
|
||||
by_id = {element.teile_id: element for element in elements}
|
||||
nodes = _build_nodes(elements, warnings)
|
||||
unassigned = _attach_devices(elements, nodes, warnings)
|
||||
owners_of = _build_track_owners(elements, warnings)
|
||||
unassigned = _attach_devices(elements, nodes, by_id, owners_of, warnings)
|
||||
|
||||
observed = _collect_links(elements, by_id, warnings)
|
||||
|
||||
@@ -1107,7 +1289,7 @@ def build_graph(elements: list[Element], source: str, warnings: list[str]) -> Gr
|
||||
edges.append(Edge(right, left, EDGE_CIRCLE, "unbestimmt"))
|
||||
|
||||
# Ein-/Ausgabestationen (BTMT Be-/Entladung) als Quelle/Senke anbinden
|
||||
edges.extend(_attach_stations(elements, by_id, nodes, warnings))
|
||||
edges.extend(_attach_stations(elements, by_id, nodes, owners_of, warnings))
|
||||
|
||||
# Kreisel<->Strecke gerichtet aus den Transferelementen (massgeblich)
|
||||
edges = _apply_transfers(elements, by_id, nodes, edges, warnings)
|
||||
@@ -1171,12 +1353,19 @@ def _attach_track_stations(
|
||||
Die Zielbahn wurde bereits von _attach_stations() im groben Graph
|
||||
bestimmt (naechster Kreisel + Eingangs-/Ausgangsbahn aus der Drehrichtung,
|
||||
siehe dort) - hier nur wiederverwendet statt neu berechnet. Eine Station,
|
||||
die bereits als Knoten in einer TrackIds-Kette liegt, wird nur noch an ihre
|
||||
Kreisel-*Bahn* (den groben Lane-/Transportknoten) gebrueckt; grobe
|
||||
Station<->Station-Kanten (ein Artefakt der Abstands-Anbindung im groben
|
||||
Graph) werden hier NICHT uebernommen, sonst entstuenden verwirrende
|
||||
Rueckkanten zwischen zwei BTMT-Punkten, die im Umlauf schon geordnet sind.
|
||||
die schon ueber eine TrackIds-Kette (Kreisel-Umlauf/Strecke, Schritte 1+2
|
||||
in build_track_graph) real mit einem anderen Bahnelement verbunden ist,
|
||||
braucht keine zusaetzliche Bruecke zur groben Kreisel-*Bahn* mehr - sonst
|
||||
haengt am sichtbar schon einsortierten Ein-/Ausgang ein ueberfluessiger
|
||||
"Bahn X"-Platzhalterknoten (Reihenfolge/Position offen) mit dran, obwohl
|
||||
die Reihenfolge laengst geklaert ist. Nur eine Station OHNE echte Kante
|
||||
(nicht Teil einer TrackIds-Kette) wird ueberhaupt an den Platzhalter
|
||||
gebrueckt; grobe Station<->Station-Kanten (ein Artefakt der Abstands-
|
||||
Anbindung im groben Graph) werden hier so oder so NICHT uebernommen,
|
||||
sonst entstuenden verwirrende Rueckkanten zwischen zwei BTMT-Punkten, die
|
||||
im Umlauf schon geordnet sind.
|
||||
"""
|
||||
already_linked = {edge.src for edge in track.edges} | {edge.dst for edge in track.edges}
|
||||
for element in elements:
|
||||
if not element.is_station:
|
||||
continue
|
||||
@@ -1184,6 +1373,8 @@ def _attach_track_stations(
|
||||
if sid not in graph.nodes:
|
||||
continue
|
||||
track.nodes.setdefault(sid, TrackNode(sid, element.kind, element))
|
||||
if sid in already_linked:
|
||||
continue
|
||||
for edge in graph.edges:
|
||||
partner = edge.dst if edge.src == sid else edge.src if edge.dst == sid else None
|
||||
if partner is None:
|
||||
@@ -1388,6 +1579,25 @@ def build_track_graph(
|
||||
f"{el.describe()}: im --track-Graph ohne jede Kante - TrackIds widerspruechlich"
|
||||
)
|
||||
|
||||
# 5. Scanner bilden weiterhin keinen eigenen Knoten (siehe TRACK_KINDS), aber
|
||||
# ihre "Naechster Separator"-Angabe wird aufgeloest und die eigene
|
||||
# Zuordnung des Scanners auf dem betroffenen Separator vermerkt -
|
||||
# render_track_dot() zeichnet dafuer einen roten Punkt unten mittig ans
|
||||
# Separator-Rechteck, statt einen zusaetzlichen Knoten einzufuehren.
|
||||
for el in elements:
|
||||
if el.kind != "Scanner":
|
||||
continue
|
||||
sep_id = str(_merkmal(el.merkmale, "Naechster Separator") or "").strip()
|
||||
sep_node = track.nodes.get(sep_id)
|
||||
if sep_node is None or sep_node.kind != "Separator":
|
||||
if sep_id:
|
||||
warnings.append(
|
||||
f"{el.describe()}: 'Naechster Separator' {sep_id} ist im --track-Graph "
|
||||
f"kein Separator-Knoten - Scanner-Zuordnung nicht dargestellt"
|
||||
)
|
||||
continue
|
||||
sep_node.scanner_zuordnung = el.assignment or sep_node.scanner_zuordnung
|
||||
|
||||
return track
|
||||
|
||||
|
||||
@@ -1688,6 +1898,11 @@ def render_dot(graph: Graph, use_coords: bool = False) -> str:
|
||||
|
||||
TRACK_NODE_STYLES: dict[str, dict[str, str]] = {
|
||||
"Separator": dict(fillcolor="#fff2cc", fontcolor="#7f6000", color="#bf8f00"),
|
||||
# Scanner ist keine eigene TrackNode-Art (siehe TRACK_KINDS) und taucht im
|
||||
# --track-DOT nie unter diesem Schluessel auf - der Eintrag existiert nur,
|
||||
# damit er sich farblich vom DEFAULT_NODE_STYLE-Grau abheben kann, sobald
|
||||
# ein Konsument ihn braucht.
|
||||
"Scanner": dict(fillcolor="#d9d2e9", fontcolor="#351c75", color="#674ea7"),
|
||||
"Ausschleus": dict(fillcolor="#f9cb9c", fontcolor="#b45f06", color="#e69138"),
|
||||
"Einschleus": dict(fillcolor="#cfe2f3", fontcolor="#1155cc", color="#3d85c6"),
|
||||
"Weiche": dict(fillcolor="#ead1dc", fontcolor="#741b47", color="#a64d79"),
|
||||
@@ -1723,6 +1938,22 @@ def _track_node_label(node: TrackNode) -> str:
|
||||
return _dot_label(lines)
|
||||
|
||||
|
||||
def _track_node_html_label(node: TrackNode) -> str:
|
||||
"""HTML-like Label mit rotem Punkt unten mittig fuer einen Separator mit
|
||||
Scanner-Zuordnung (node.scanner_zuordnung, siehe build_track_graph Schritt
|
||||
5) - der Scanner selbst bekommt weiterhin keinen eigenen Knoten."""
|
||||
el = node.element
|
||||
lines = [f"{el.name or node.kind} [{node.node_id}]" if el else node.kind, node.kind]
|
||||
if node.note:
|
||||
lines.append(node.note)
|
||||
rows = "".join(f"<TR><TD>{_xml_escape(line)}</TD></TR>" for line in lines if line)
|
||||
dot_row = '<TR><TD><FONT COLOR="#cc0000" POINT-SIZE="16">●</FONT></TD></TR>'
|
||||
return (
|
||||
f'<<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0" CELLPADDING="1">'
|
||||
f'{rows}{dot_row}</TABLE>>'
|
||||
)
|
||||
|
||||
|
||||
def render_track_dot(track: TrackGraph) -> str:
|
||||
"""DOT des Bahnzusammenhang-Graphen (--track); siehe build_track_graph()
|
||||
und doc/track.md. Gerendert ueber dieselben graph_to_svg/dot_to_svg wie
|
||||
@@ -1749,7 +1980,11 @@ def render_track_dot(track: TrackGraph) -> str:
|
||||
extra += ", shape=diamond"
|
||||
elif node.kind in (SYMBOL_ENTRY, SYMBOL_EXIT, "Beladung", "Entladung"):
|
||||
extra += ", shape=ellipse"
|
||||
add(f' "{node_id}" [label="{_track_node_label(node)}", {attrs}{extra}];')
|
||||
if node.kind == "Separator" and node.scanner_zuordnung:
|
||||
label_attr = f"label={_track_node_html_label(node)}"
|
||||
else:
|
||||
label_attr = f'label="{_track_node_label(node)}"'
|
||||
add(f' "{node_id}" [{label_attr}, {attrs}{extra}];')
|
||||
add("")
|
||||
|
||||
for edge in track.edges:
|
||||
@@ -1773,6 +2008,8 @@ def render_track_dot(track: TrackGraph) -> str:
|
||||
'<td align="left">Bahn-Platzhalter - Reihenfolge/Position offen (Teil B)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#bf8f00">gestrichelt gelb</font></td>'
|
||||
'<td align="left">Richtung unbestimmt (Element nicht in einer TrackIds-Kette)</td></tr>')
|
||||
add('<tr><td align="left"><font color="#cc0000">●</font></td>'
|
||||
'<td align="left">Separator hat einen zugeordneten Scanner (kein eigener Knoten)</td></tr>')
|
||||
add("</table>>];")
|
||||
add(" }")
|
||||
add("}")
|
||||
@@ -1780,6 +2017,7 @@ def render_track_dot(track: TrackGraph) -> str:
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# connect.ini (Item-/Flussebene) erzeugen
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1965,8 +2203,15 @@ def render_bbs_svg(
|
||||
show_kx zeichnet zusaetzlich je Objekt die Anschlusspunkte K1-K4 (rotes
|
||||
Kreuz + kleine Beschriftung) und verbindet sie in der Reihenfolge
|
||||
K1->K2->K3->K4 mit einer blauen Linie.
|
||||
|
||||
Ein Kreisel wird - anders als jedes andere Objekt - nicht als ein
|
||||
Rechteck, sondern als seine beiden Bahnen L/R gezeichnet: dieselbe
|
||||
massstabsgetreue Aufteilung (kurze Achse hälftig, Zuordnung-Mehrheits-
|
||||
entscheid fuer L/R), siehe _kreisel_lane_rects().
|
||||
Die K1-K4-Anschlusspunkte (real am Kreisel als Ganzem haengend) werden
|
||||
trotzdem nur einmal gezeichnet, nicht je Bahn doppelt.
|
||||
"""
|
||||
items: list[tuple[Element, float, float, float, float]] = []
|
||||
items: list[tuple[Element, float, float, float, float, str | None]] = []
|
||||
for element in elements:
|
||||
if element.position is None or element.bbox is None:
|
||||
warnings.append(
|
||||
@@ -1980,7 +2225,11 @@ def render_bbs_svg(
|
||||
f"{element.describe()}: Boundingbox {bw}, {bd} mm ungueltig - in --show-bbs nicht gezeichnet"
|
||||
)
|
||||
continue
|
||||
items.append((element, px, py, bw, bd))
|
||||
if element.is_circle:
|
||||
for lane, (x0, y0, x1, y1) in sorted(_kreisel_lane_rects(element, elements).items()):
|
||||
items.append((element, (x0 + x1) / 2, (y0 + y1) / 2, x1 - x0, y1 - y0, lane))
|
||||
continue
|
||||
items.append((element, px, py, bw, bd, None))
|
||||
|
||||
if not items:
|
||||
raise ValueError("keine Objekte mit Position und Boundingbox gefunden")
|
||||
@@ -2007,11 +2256,11 @@ def render_bbs_svg(
|
||||
add(f" <!-- Quelle: {_xml_escape(source)} -->")
|
||||
add(f' <rect x="0" y="0" width="{width:.2f}" height="{height:.2f}" fill="#ffffff"/>')
|
||||
|
||||
for element, px, py, bw, bd in items:
|
||||
for element, px, py, bw, bd, lane in items:
|
||||
style = BBS_STYLES.get(element.kind, BBS_DEFAULT_STYLE)
|
||||
x = (px - bw / 2) - min_x
|
||||
y = max_y - (py + bd / 2)
|
||||
name = element.name or element.teile_id
|
||||
name = f"{element.name or element.teile_id} Bahn {lane}" if lane else (element.name or element.teile_id)
|
||||
font_size = max(BBS_MIN_FONT_MM, min(bw, bd) * 0.28)
|
||||
add(f' <rect x="{x:.2f}" y="{y:.2f}" width="{bw:.2f}" height="{bd:.2f}" '
|
||||
f'fill="{style["fill"]}" fill-opacity="0.85" stroke="{style["stroke"]}" stroke-width="6"/>')
|
||||
@@ -2022,7 +2271,13 @@ def render_bbs_svg(
|
||||
# Anschlusspunkte K1-K4 zuletzt, damit sie ueber den Rechtecken liegen.
|
||||
if show_kx:
|
||||
half = KX_CROSS_MM / 2
|
||||
seen_kx: set[str] = set()
|
||||
for element, *_rest in items:
|
||||
# Ein per Bahn L/R aufgeteilter Kreisel steht zweimal in items,
|
||||
# seine K1-K4 haengen aber am Kreisel als Ganzem - nur einmal zeichnen.
|
||||
if element.teile_id in seen_kx:
|
||||
continue
|
||||
seen_kx.add(element.teile_id)
|
||||
# Anlage- in viewBox-Koordinaten (X verschoben, Y gespiegelt).
|
||||
pts = [(label, kx - min_x, max_y - ky) for label, (kx, ky) in element.kpoints]
|
||||
# Verbindungslinie K1->K2->K3->K4 (nur bei mindestens zwei Punkten).
|
||||
@@ -2042,6 +2297,239 @@ def render_bbs_svg(
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kreisel-Geometrie: Aufteilung in Bahn L/R
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _kreisel_lane_rects(
|
||||
kreisel: Element, fine: list[Element]
|
||||
) -> dict[str, tuple[float, float, float, float]]:
|
||||
"""
|
||||
(x0, y0, x1, y1) je Bahn (LANE_LEFT/LANE_RIGHT) in Anlage-mm: die Kreisel-
|
||||
Boundingbox wird entlang ihrer kurzen Achse haelftig geteilt (die beiden
|
||||
Bahnen laufen parallel zur langen Achse). Welche Haelfte L bzw. R ist, wird
|
||||
per Mehrheitsentscheid aus der 'Zuordnung' (…-L/…-R) der uebergebenen
|
||||
Feinelemente abgeleitet; ohne verwertbare Zuordnung faellt die Haelfte mit
|
||||
der kleineren Koordinate auf LANE_RIGHT zurueck (willkuerliche, aber
|
||||
stabile Konvention, wie zuvor in data/gen_chain_svg.py).
|
||||
"""
|
||||
x, y, bw, bh = kreisel.x, kreisel.y, kreisel.bbox_x, kreisel.bbox_y
|
||||
x0, x1 = x - bw / 2, x + bw / 2
|
||||
y0, y1 = y - bh / 2, y + bh / 2
|
||||
horizontal = bw >= bh # Bahnen laufen entlang X, Haelften entlang Y gestapelt
|
||||
|
||||
if horizontal:
|
||||
mid = (y0 + y1) / 2
|
||||
half_a, half_b = (x0, y0, x1, mid), (x0, mid, x1, y1)
|
||||
else:
|
||||
mid = (x0 + x1) / 2
|
||||
half_a, half_b = (x0, y0, mid, y1), (mid, y0, x1, y1)
|
||||
|
||||
votes: dict[str, list[float]] = {LANE_LEFT: [], LANE_RIGHT: []}
|
||||
for el in fine:
|
||||
base, _, lane = el.assignment.partition("-")
|
||||
if base != kreisel.teile_id or lane not in (LANE_LEFT, LANE_RIGHT):
|
||||
continue
|
||||
coord = el.y if horizontal else el.x
|
||||
if coord is not None:
|
||||
votes[lane].append(coord)
|
||||
|
||||
def avg(values: list[float]) -> float | None:
|
||||
return sum(values) / len(values) if values else None
|
||||
|
||||
avg_l, avg_r = avg(votes[LANE_LEFT]), avg(votes[LANE_RIGHT])
|
||||
a_mid = (half_a[1] + half_a[3]) / 2 if horizontal else (half_a[0] + half_a[2]) / 2
|
||||
if avg_l is None and avg_r is None:
|
||||
lane_a = LANE_RIGHT
|
||||
else:
|
||||
dist_l = abs(avg_l - a_mid) if avg_l is not None else float("inf")
|
||||
dist_r = abs(avg_r - a_mid) if avg_r is not None else float("inf")
|
||||
lane_a = LANE_LEFT if dist_l < dist_r else LANE_RIGHT
|
||||
lane_b = LANE_RIGHT if lane_a == LANE_LEFT else LANE_LEFT
|
||||
return {lane_a: half_a, lane_b: half_b}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Chain-/Lageplan-SVG der groben Objekte erzeugen (--chain-graph)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Schritt 1 des neu aufgebauten Materialfluss-Bildes: nur die groben Objekte -
|
||||
# Kreisel (je Bahn L/R) und Strecke/Gefaellestrecke (VF/GF) - als massstabs-
|
||||
# getreue Rechtecke an ihrer echten Position/Boundingbox, verbunden durch die
|
||||
# bereits von build_graph() aufgeloesten Materialfluss-Kanten (graph.edges).
|
||||
# Keine Anbauteile (Separator, Scanner, Weiche, ...) und keine Ein-/
|
||||
# Ausgabestationen - die kommen in einem spaeteren Schritt dazu.
|
||||
|
||||
CHAIN_MAX_W = 1400.0
|
||||
CHAIN_MAX_H = 850.0
|
||||
CHAIN_MARGIN = 70.0
|
||||
CHAIN_TITLE_H = 60.0
|
||||
|
||||
CHAIN_EDGE_COLOR = {
|
||||
EDGE_FLOW: "#2f5597",
|
||||
EDGE_CIRCLE: "#8ea9db",
|
||||
EDGE_UNRESOLVED: "#bf8f00",
|
||||
}
|
||||
|
||||
|
||||
def _chain_marker_id(color: str) -> str:
|
||||
return "chainArrow_" + color.lstrip("#")
|
||||
|
||||
|
||||
def render_chain_svg(elements: list[Element], graph: Graph, source: str) -> str:
|
||||
"""
|
||||
Massstabsgetreuer Chain-/Lageplan der groben Objekte (--chain-graph): jeder
|
||||
Kreisel als seine beiden Bahnen L/R (_kreisel_lane_rects(), Zuordnung aus
|
||||
allen Elementen fuer den Mehrheitsentscheid - wie --show-bbs) und jede
|
||||
Strecke/Gefaellestrecke als ein Rechteck an ihrer echten Position/
|
||||
Boundingbox. Kanten kommen direkt aus dem schon aufgeloesten groben
|
||||
Materialfluss-Graphen (graph.edges) - eine Kante, deren Knoten hier nicht
|
||||
gezeichnet wird (z. B. eine BTMT-Station), wird schlicht uebersprungen.
|
||||
"""
|
||||
rects: dict[str, tuple[float, float, float, float]] = {}
|
||||
lane_info: dict[str, tuple[Element, str]] = {}
|
||||
strecke_labels: dict[str, Element] = {}
|
||||
|
||||
for element in elements:
|
||||
if element.position is None or element.bbox is None:
|
||||
continue
|
||||
if element.is_circle:
|
||||
for lane, rect in _kreisel_lane_rects(element, elements).items():
|
||||
node_id = f"{element.teile_id}-{lane}"
|
||||
rects[node_id] = rect
|
||||
lane_info[node_id] = (element, lane)
|
||||
elif element.is_transport:
|
||||
rects[element.teile_id] = (
|
||||
element.x - element.bbox_x / 2, element.y - element.bbox_y / 2,
|
||||
element.x + element.bbox_x / 2, element.y + element.bbox_y / 2,
|
||||
)
|
||||
strecke_labels[element.teile_id] = element
|
||||
|
||||
if not rects:
|
||||
raise ValueError("keine Kreisel/Strecken mit Position/Boundingbox gefunden")
|
||||
|
||||
all_x = [v for r in rects.values() for v in (r[0], r[2])]
|
||||
all_y = [v for r in rects.values() for v in (r[1], r[3])]
|
||||
min_x, max_x = min(all_x), max(all_x)
|
||||
min_y, max_y = min(all_y), max(all_y)
|
||||
real_w, real_h = max(max_x - min_x, 1.0), max(max_y - min_y, 1.0)
|
||||
scale = min(CHAIN_MAX_W / real_w, CHAIN_MAX_H / real_h)
|
||||
|
||||
def sx(rx: float) -> float:
|
||||
return CHAIN_MARGIN + (rx - min_x) * scale
|
||||
|
||||
def sy(ry: float) -> float:
|
||||
return CHAIN_TITLE_H + (max_y - ry) * scale
|
||||
|
||||
draw_w, draw_h = real_w * scale, real_h * scale
|
||||
canvas_w = CHAIN_MARGIN * 2 + draw_w
|
||||
canvas_h = CHAIN_TITLE_H + draw_h + CHAIN_MARGIN
|
||||
|
||||
def center(node_id: str) -> tuple[float, float]:
|
||||
x0, y0, x1, y1 = rects[node_id]
|
||||
return ((x0 + x1) / 2, (y0 + y1) / 2)
|
||||
|
||||
def anchor(node_id: str, other: tuple[float, float]) -> tuple[float, float]:
|
||||
"""Punkt auf dem Rand von node_id, der am naechsten zu 'other' liegt -
|
||||
auch wenn 'other' (der Mittelpunkt des Partners) selbst innerhalb des
|
||||
Rechtecks liegt (schmale Kreisel-Bahn ueber die ganze Anlage)."""
|
||||
x0, y0, x1, y1 = rects[node_id]
|
||||
ox, oy = other
|
||||
cx, cy = min(max(ox, x0), x1), min(max(oy, y0), y1)
|
||||
if (cx, cy) != (ox, oy):
|
||||
return (cx, cy)
|
||||
d_left, d_right = ox - x0, x1 - ox
|
||||
d_bottom, d_top = oy - y0, y1 - oy
|
||||
nearest = min(d_left, d_right, d_bottom, d_top)
|
||||
if nearest == d_left:
|
||||
return (x0, oy)
|
||||
if nearest == d_right:
|
||||
return (x1, oy)
|
||||
if nearest == d_bottom:
|
||||
return (ox, y0)
|
||||
return (ox, y1)
|
||||
|
||||
edges = [e for e in graph.edges if e.src in rects and e.dst in rects]
|
||||
|
||||
# Mindestlaenge eines Pfeils in Bildschirm-Pixeln: zwei Kreisel-Bahnen
|
||||
# grenzen direkt aneinander (siehe _kreisel_lane_rects) - ihr Umlauf-Pfeil
|
||||
# laeuft dann exakt auf der gemeinsamen Grenze und wuerde ohne Nachschub
|
||||
# unsichtbar lang Null sein.
|
||||
MIN_ARROW_PX = 14.0
|
||||
|
||||
edge_svg: list[str] = []
|
||||
for edge in edges:
|
||||
x1, y1 = anchor(edge.src, center(edge.dst))
|
||||
x2, y2 = anchor(edge.dst, center(edge.src))
|
||||
px1, py1, px2, py2 = sx(x1), sy(y1), sx(x2), sy(y2)
|
||||
if (px2 - px1) ** 2 + (py2 - py1) ** 2 < (MIN_ARROW_PX / 2) ** 2:
|
||||
src_cx, src_cy = center(edge.src)
|
||||
dst_cx, dst_cy = center(edge.dst)
|
||||
dx, dy = sx(dst_cx) - sx(src_cx), sy(dst_cy) - sy(src_cy)
|
||||
length = max((dx ** 2 + dy ** 2) ** 0.5, 1e-6)
|
||||
ux, uy = dx / length * (MIN_ARROW_PX / 2), dy / length * (MIN_ARROW_PX / 2)
|
||||
mx, my = (px1 + px2) / 2, (py1 + py2) / 2
|
||||
px1, py1, px2, py2 = mx - ux, my - uy, mx + ux, my + uy
|
||||
color = CHAIN_EDGE_COLOR.get(edge.kind, CHAIN_EDGE_COLOR[EDGE_FLOW])
|
||||
dash = ' stroke-dasharray="4 3"' if edge.kind == EDGE_UNRESOLVED or edge.note == "unbestimmt" else ""
|
||||
title = f"{edge.src} -> {edge.dst}" + (f" ({_xml_escape(edge.note)})" if edge.note else "")
|
||||
edge_svg.append(
|
||||
f'<g><title>{title}</title><line x1="{px1:.1f}" y1="{py1:.1f}" '
|
||||
f'x2="{px2:.1f}" y2="{py2:.1f}" stroke="{color}" stroke-width="1.8"{dash} '
|
||||
f'marker-end="url(#{_chain_marker_id(color)})"/></g>'
|
||||
)
|
||||
|
||||
box_svg: list[str] = []
|
||||
for node_id, (x0, y0, x1, y1) in rects.items():
|
||||
if node_id in lane_info:
|
||||
kreisel, lane = lane_info[node_id]
|
||||
style = NODE_STYLES.get(kreisel.kind, DEFAULT_NODE_STYLE)
|
||||
label = f"{kreisel.name or kreisel.teile_id} Bahn {lane}"
|
||||
else:
|
||||
strecke = strecke_labels[node_id]
|
||||
style = NODE_STYLES.get(strecke.kind, DEFAULT_NODE_STYLE)
|
||||
label = f"{strecke.name or node_id} [{node_id}]"
|
||||
box_svg.append(
|
||||
f'<rect x="{sx(x0):.1f}" y="{sy(y1):.1f}" width="{(x1 - x0) * scale:.1f}" '
|
||||
f'height="{(y1 - y0) * scale:.1f}" fill="{style["fillcolor"]}" fill-opacity="0.3" '
|
||||
f'stroke="{style["color"]}" stroke-width="2.2"/>'
|
||||
)
|
||||
box_svg.append(
|
||||
f'<text x="{sx(x0) + 4:.1f}" y="{sy(y1) + 12:.1f}" font-size="10" font-weight="700" '
|
||||
f'fill="{style["color"]}">{_xml_escape(label)}</text>'
|
||||
)
|
||||
|
||||
heading = f"{Path(source).stem} - Materialfluss (grobe Objekte)"
|
||||
|
||||
out: list[str] = []
|
||||
add = out.append
|
||||
add('<?xml version="1.0" encoding="UTF-8"?>')
|
||||
add(f'<!-- Chain-/Lageplan (grobe Objekte): Kreisel-Bahnen und Strecke/Gefaellestrecke '
|
||||
f'massstabsgetreu aus Position/Boundingbox, Kanten aus dem groben Materialfluss-Graphen '
|
||||
f'(build_graph()). Automatisch erzeugt von lib/material_flow.py (Option "chain-graph"), '
|
||||
f'nicht manuell aendern. -->')
|
||||
add(f' <!-- Quelle: {_xml_escape(source)} -->')
|
||||
add(f'<svg viewBox="0 0 {canvas_w:.0f} {canvas_h:.0f}" xmlns="http://www.w3.org/2000/svg" '
|
||||
f'font-family="Segoe UI, Arial, sans-serif">')
|
||||
add(f' <title>{_xml_escape(heading)}</title>')
|
||||
add(f' <rect x="0" y="0" width="{canvas_w:.0f}" height="{canvas_h:.0f}" fill="#fbfaf6"/>')
|
||||
add(' <defs>')
|
||||
for color in sorted(set(CHAIN_EDGE_COLOR.values())):
|
||||
add(f' <marker id="{_chain_marker_id(color)}" viewBox="0 0 10 10" refX="8" refY="5" '
|
||||
f'markerWidth="6" markerHeight="6" orient="auto-start-reverse">'
|
||||
f'<path d="M0,0 L10,5 L0,10 z" fill="{color}"/></marker>')
|
||||
add(' </defs>')
|
||||
add(f' <text x="{canvas_w / 2:.0f}" y="26" text-anchor="middle" font-size="17" font-weight="700" '
|
||||
f'fill="#1f3864">{_xml_escape(heading)}</text>')
|
||||
add(f' <text x="{canvas_w / 2:.0f}" y="44" text-anchor="middle" font-size="11" fill="#5b6270">'
|
||||
f'Massstabsgetreu • Quelle: {_xml_escape(source)}</text>')
|
||||
out.extend(box_svg)
|
||||
out.extend(edge_svg)
|
||||
add("</svg>")
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SVG erzeugen
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2318,6 +2806,7 @@ def report(
|
||||
bbs_file: Path | None = None,
|
||||
track_dot_file: Path | None = None,
|
||||
track_svg_file: Path | None = None,
|
||||
chain_file: Path | None = None,
|
||||
) -> None:
|
||||
kinds: dict[str, int] = {}
|
||||
for node in graph.nodes.values():
|
||||
@@ -2350,6 +2839,7 @@ def report(
|
||||
print(f"Bahnzusammenhang = {track_dot_file if track_dot_file else '- (ohne --track)'}")
|
||||
if track_dot_file:
|
||||
print(f" SVG = {track_svg_file if track_svg_file else '- (ohne --tosvg)'}")
|
||||
print(f"Chain-Graph = {chain_file if chain_file else '- (ohne --chain-graph)'}")
|
||||
print("================================================================")
|
||||
|
||||
if stale_svg is not None:
|
||||
@@ -2400,6 +2890,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
" material_flow.bat --file mubea.csv --tosvg --doc\n"
|
||||
" material_flow.bat --file HundM05.csv --show-bbs\n"
|
||||
" material_flow.bat --file mubea.csv --track --tosvg\n"
|
||||
" material_flow.bat --file HundM05.csv --chain-graph\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
@@ -2469,6 +2960,18 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"zu liefern. Siehe doc/track.md. Aendert den normalen Materialfluss-"
|
||||
"Output nicht.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--chain-graph",
|
||||
dest="chain_graph",
|
||||
action="store_true",
|
||||
help="Massstabsgetreuen Chain-/Lageplan der groben Objekte als "
|
||||
"<name>_chain.svg nach %%SKEL_RESULTS%% schreiben: jeder Kreisel "
|
||||
"(je Bahn L/R) und jede Strecke/Gefaellestrecke (VF/GF) als "
|
||||
"Rechteck an ihrer echten Position/Boundingbox, verbunden durch "
|
||||
"die bereits aufgeloesten Materialfluss-Kanten. Schritt 1 - noch "
|
||||
"ohne Anbauteile (Separator, Scanner, Weiche, ...) oder Ein-/"
|
||||
"Ausgabestationen. Braucht kein Graphviz.",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
@@ -2578,9 +3081,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
connect_file = connect_target
|
||||
|
||||
by_id = {element.teile_id: element for element in elements}
|
||||
|
||||
track: TrackGraph | None = None
|
||||
track_dot_file = track_svg_file = None
|
||||
if args.track:
|
||||
by_id = {element.teile_id: element for element in elements}
|
||||
track = build_track_graph(elements, by_id, graph, warnings)
|
||||
track_dot_target = results / f"{stem}_track.dot"
|
||||
track_dot_target.write_text(render_track_dot(track), encoding="utf-8")
|
||||
@@ -2593,6 +3098,17 @@ def main(argv: list[str] | None = None) -> int:
|
||||
except RuntimeError as exc:
|
||||
print(f"FEHLER: Bahnzusammenhang-SVG nicht erzeugt: {exc}", file=sys.stderr)
|
||||
|
||||
chain_file = None
|
||||
if args.chain_graph:
|
||||
try:
|
||||
svg = render_chain_svg(elements, graph, csv_file.name)
|
||||
except ValueError as exc:
|
||||
print(f"WARNUNG: Chain-Graph: {exc} - nicht erzeugt.", file=sys.stderr)
|
||||
else:
|
||||
chain_target = results / f"{stem}_chain.svg"
|
||||
chain_target.write_text(svg, encoding="utf-8")
|
||||
chain_file = chain_target
|
||||
|
||||
report(
|
||||
graph,
|
||||
findings,
|
||||
@@ -2604,6 +3120,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
bbs_file=bbs_file,
|
||||
track_dot_file=track_dot_file,
|
||||
track_svg_file=track_svg_file,
|
||||
chain_file=chain_file,
|
||||
)
|
||||
|
||||
if svg_error:
|
||||
|
||||
Reference in New Issue
Block a user