TRO-Beschriftung wie das TRO-Fluss-SVG; Stationspfeile zeigen von der Kette weg

tro_flow:
- _station_positionen: Richtung des Zu-/Ablaufpfeils mit groesstem
  Winkelabstand zu allen Kanten des Anker-TROs und zu bereits gesetzten
  Stationspfeilen (15-Grad-Raster), echte Stationsrichtung nur bei
  Gleichstand - vorher lag z. B. Mubea TRO02 -> SYS2 fast auf TRO02 -> TRO13

tro_annotate:
- Symbole mit typ-eigener Fuellfarbe (Solid-Hatch) unter der Gruppenkontur
- Kantenfarben/-layer wie im SVG: Uebergabe blau, Weiche orange,
  Materialeingang gruen, Materialausgang rot (TRO_WEICHE/_EINGANG/_AUSGANG)
- Stationen als farbige Beschriftung am Pfeilende, Lage ueber
  tro_flow._station_positionen (ohne CONNECTION_ARROW-Block)
- Legende wie im SVG: je vorkommendem Typ Fuellung/Rand/Form, dann Kantenfarben
- CONNECTION_ARROW: FROM_TRO/TO_TRO unsichtbar (Daten bleiben), KIND auch weiche

cfg/dxf_registration.json: mubea.csv|mubea.dxf und HundM05.csv|HundM05.dxf
mit Versatz 0 (CSV aus derselben dxfmakros-Zeichnung; Auto-Ermittlung sieht
die verschachtelten Separatoren nicht und war mehrdeutig);
mubea.csv|500573_60_1.dxf neu ermittelt (36/46 Punkte).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Samer Ayadi
2026-10-02 13:30:47 +02:00
co-authored by Claude Opus 5.5
parent d0a3ac7e5a
commit 7b98c8b8d4
3 changed files with 213 additions and 52 deletions
+35 -7
View File
@@ -1,17 +1,17 @@
{ {
"mubea.csv|500573_60_1.dxf": { "mubea.csv|500573_60_1.dxf": {
"dx": 50905.678327433096, "dx": 50833.7283274331,
"dy": 8414.490373798908, "dy": 8414.490373798908,
"rotation": 0.0, "rotation": 0.0,
"source": "auto", "source": "auto",
"anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:4, Separator:31; 20 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft", "anchor": "20 exakte Treffer (<= 50 mm) ueber die Rollen Scanner:8, Separator:38; 22 Stimmen im 100-mm-Raster, 6 Kandidaten geprueft",
"matched": 34, "matched": 36,
"candidates": 35, "candidates": 46,
"residual_max": 452.04557170439375, "residual_max": 487.77757110283585,
"residual_mean": 102.6006793267277, "residual_mean": 93.12692369578822,
"csv_file": "mubea.csv", "csv_file": "mubea.csv",
"dxf_file": "500573_60_1.dxf", "dxf_file": "500573_60_1.dxf",
"created": "2026-07-30 15:32" "created": "2026-10-02 12:45"
}, },
"export.csv|Mubea.dxf": { "export.csv|Mubea.dxf": {
"dx": 59.950000000000045, "dx": 59.950000000000045,
@@ -26,5 +26,33 @@
"csv_file": "export.csv", "csv_file": "export.csv",
"dxf_file": "Mubea.dxf", "dxf_file": "Mubea.dxf",
"created": "2026-08-12 10:09" "created": "2026-08-12 10:09"
},
"HundM05.csv|HundM05.dxf": {
"dx": 0.0,
"dy": 0.0,
"rotation": 0.0,
"source": "cli",
"anchor": "von Hand: CSV aus derselben dxfmakros-Zeichnung exportiert, Koordinaten identisch (Auto-Ermittlung sieht die in VF_/KREISEL_-Bloecken verschachtelten Separatoren nicht und war mehrdeutig)",
"matched": 14,
"candidates": 26,
"residual_max": 360.0,
"residual_mean": 164.97465794329284,
"csv_file": "HundM05.csv",
"dxf_file": "HundM05.dxf",
"created": "2026-10-02 13:40"
},
"mubea.csv|mubea.dxf": {
"dx": 0.0,
"dy": 0.0,
"rotation": 0.0,
"source": "cli",
"anchor": "von Hand: CSV aus derselben dxfmakros-Zeichnung exportiert, Koordinaten identisch (Auto-Ermittlung sieht die in VF_/KREISEL_-Bloecken verschachtelten Separatoren nicht und war mehrdeutig)",
"matched": 20,
"candidates": 46,
"residual_max": 360.0,
"residual_mean": 256.77327549812964,
"csv_file": "mubea.csv",
"dxf_file": "mubea.dxf",
"created": "2026-10-02 13:40"
} }
} }
+144 -31
View File
@@ -101,7 +101,14 @@ from dxf_registration import (
) )
from material_flow import build_graph, env_dir, read_elements, resolve_input from material_flow import build_graph, env_dir, read_elements, resolve_input
from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro from tro_catalog import STYLES, TRO_CATALOG, TroSymbol, get_tro
from tro_flow import Analysis, analyse_tros from tro_flow import (
BOUNDARY_ENTRY,
BOUNDARY_EXIT,
STATION_FARBE,
Analysis,
_station_positionen,
analyse_tros,
)
from tro_overrides import Overrides, load_overrides from tro_overrides import Overrides, load_overrides
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -314,7 +321,12 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
}) })
# openpoint: im Review gemeldete Stellen, die noch KEIN TRO sind. Kraeftiges # openpoint: im Review gemeldete Stellen, die noch KEIN TRO sind. Kraeftiges
# Rot, damit sie sich von den abgeleiteten Markern klar abheben. # Rot, damit sie sich von den abgeleiteten Markern klar abheben.
# Kantenfarben wie im TRO-Fluss-SVG (tro_flow.render_dot): Uebergabe blau,
# Weiche orange, Materialeingang gruen, Materialausgang rot.
for key, colour in (("label", "#404040"), ("flow", "#2f5597"), for key, colour in (("label", "#404040"), ("flow", "#2f5597"),
("weiche", "#cc8800"),
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
("legend", "#808080"), ("openpoint", "#e00000")): ("legend", "#808080"), ("openpoint", "#e00000")):
name = f"{prefix}{key.upper()}" name = f"{prefix}{key.upper()}"
names[key] = name names[key] = name
@@ -334,6 +346,40 @@ def _polygon(sides: int, radius: float, start_deg: float) -> list[tuple[float, f
] ]
def shape_outline(symbol: str, r: float) -> list[tuple[float, float]]:
"""Aeussere Kontur eines Symbols als Polygon - Rand der Fuellflaeche
(Kreise als 48-Eck angenaehert)."""
if symbol in (TroSymbol.CIRCLE, TroSymbol.DOUBLE_CIRCLE):
return _polygon(48, r, 0)
if symbol == TroSymbol.TRIANGLE:
return _polygon(3, r, 90)
if symbol in (TroSymbol.TRIANGLE_DOWN, TroSymbol.DOUBLE_TRIANGLE_DOWN):
return _polygon(3, r, 270)
if symbol == TroSymbol.SQUARE:
return _polygon(4, r, 45)
if symbol == TroSymbol.DIAMOND:
return _polygon(4, r, 90)
if symbol == TroSymbol.PENTAGON:
return _polygon(5, r, 90)
if symbol == TroSymbol.HEXAGON:
return _polygon(6, r, 0)
if symbol == TroSymbol.RECT:
w, h = r * 1.45, r * 0.72
return [(-w, -h), (w, -h), (w, h), (-w, h)]
if symbol == TroSymbol.ARROW:
return [(-r * 0.55, -r), (r, 0), (-r * 0.55, r), (-r * 0.55, r * 0.35),
(-r, r * 0.35), (-r, -r * 0.35), (-r * 0.55, -r * 0.35)]
return _polygon(48, r, 0)
def draw_fill(blk, symbol: str, r: float, fill_hex: str) -> None:
"""Typ-eigene Fuellfarbe (wie im TRO-Fluss-SVG) als Solid-Hatch unter der
Kontur. Feste Farbe statt BYLAYER: der Layer traegt die Randfarbe der
Gruppe, die Fuellung unterscheidet den einzelnen Typ."""
hatch = blk.add_hatch(dxfattribs={"true_color": ezdxf.rgb2int(hex_to_rgb(fill_hex))})
hatch.paths.add_polyline_path(shape_outline(symbol, r), is_closed=True)
def draw_shape(blk, symbol: str, r: float) -> None: def draw_shape(blk, symbol: str, r: float) -> None:
""" """
Symbolgeometrie in eine Blockdefinition zeichnen. Symbolgeometrie in eine Blockdefinition zeichnen.
@@ -399,7 +445,10 @@ def build_symbol(doc, type_name: str, marker: float, text: float, show_fb: bool)
blk = doc.blocks.new(name=name) blk = doc.blocks.new(name=name)
definition = get_tro(type_name) definition = get_tro(type_name)
draw_shape(blk, definition.get_symbol() if definition else "", marker) symbol = definition.get_symbol() if definition else ""
if definition:
draw_fill(blk, symbol, marker, definition.get_fill())
draw_shape(blk, symbol, marker)
gap = marker * 1.5 gap = marker * 1.5
rows = [("ID", "TRO??", text * 1.35), ("TYPE", type_name, text)] rows = [("ID", "TRO??", text * 1.35), ("TYPE", type_name, text)]
@@ -522,6 +571,27 @@ def draw_open_points(
return drawn return drawn
def place_stations(
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
layers: dict[str, str], text: float,
) -> dict[str, tuple[float, float]]:
"""Be-/Entladestationen wie im TRO-Fluss-SVG: kein Symbol, nur der Name
in Gruen (Eingang) bzw. Rot (Ausgang) am Ende des Zu-/Ablaufpfeils.
Lage wie dort (tro_flow._station_positionen): feste Pfeillaenge ab dem
angehaengten TRO, Richtung von dessen Kanten weg."""
positionen = _station_positionen(analysis, placed)
for station in analysis.boundaries:
if station.tro_id not in positionen:
continue
x, y = positionen[station.tro_id]
layer = layers["eingang" if station.type_name == BOUNDARY_ENTRY else "ausgang"]
label = msp.add_text(station.label, height=text,
dxfattribs={"layer": layer, "style": "Standard"})
label.set_placement((x, y), align=TextEntityAlignment.MIDDLE_CENTER)
tag(label, station.tro_id)
return positionen
def build_connection_symbol(doc, marker: float, text: float) -> str: def build_connection_symbol(doc, marker: float, text: float) -> str:
""" """
Blockdefinition fuer eine Verbindung anlegen. Blockdefinition fuer eine Verbindung anlegen.
@@ -537,9 +607,9 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
UID unsichtbar - eindeutige interne Nummer, dauerhaft UID unsichtbar - eindeutige interne Nummer, dauerhaft
FROM_UID unsichtbar - interne UID des Quell-TRO FROM_UID unsichtbar - interne UID des Quell-TRO
TO_UID unsichtbar - interne UID des Ziel-TRO TO_UID unsichtbar - interne UID des Ziel-TRO
FROM_TRO sichtbar - sprechender Name der Quelle (TRO01) FROM_TRO unsichtbar - sprechender Name der Quelle (TRO01)
TO_TRO sichtbar - sprechender Name des Ziels (TRO09) TO_TRO unsichtbar - sprechender Name des Ziels (TRO09)
KIND unsichtbar - flow | colocated KIND unsichtbar - flow | weiche | colocated
""" """
name = CONNECTION_BLOCK name = CONNECTION_BLOCK
if name in doc.blocks: if name in doc.blocks:
@@ -552,13 +622,17 @@ def build_connection_symbol(doc, marker: float, text: float) -> str:
dxfattribs={"color": 256}) dxfattribs={"color": 256})
blk.add_circle((0, 0), size * 0.12, dxfattribs={"color": 256}) blk.add_circle((0, 0), size * 0.12, dxfattribs={"color": 256})
# FROM_TRO/TO_TRO unsichtbar: an jeder Kante sichtbar machten sie die
# Zeichnung unruhig, im TRO-Fluss-SVG stehen sie auch nicht (Review
# 2026-10-02). Die Daten bleiben am Block, CONNECTION_EDIT zeigt sie.
offset = size * 1.4 offset = size * 1.4
for tag_name in ("FROM_TRO", "TO_TRO"): for tag_name in ("FROM_TRO", "TO_TRO"):
blk.add_attdef( blk.add_attdef(
tag_name, tag_name,
insert=(size * 1.3, offset), insert=(size * 1.3, offset),
height=text * 0.8, height=text * 0.8,
dxfattribs={"style": "Standard", "color": 256, "text": ""}, dxfattribs={"style": "Standard", "color": 256, "text": "",
"invisible": 1},
) )
offset -= text * 1.2 offset -= text * 1.2
@@ -589,30 +663,47 @@ def draw_connections(
""" """
head = marker * 0.9 head = marker * 0.9
block = build_connection_symbol(doc, marker, text) block = build_connection_symbol(doc, marker, text)
stationen = {b.tro_id for b in analysis.boundaries}
drawn = 0 drawn = 0
for src, dst in analysis.edges: for src, dst in analysis.edges:
if src not in placed or dst not in placed: if src not in placed or dst not in placed:
continue continue
# Farbe/Layer wie im TRO-Fluss-SVG
if src in stationen:
layer, kind = layers["eingang"], "eingang"
elif dst in stationen:
layer, kind = layers["ausgang"], "ausgang"
elif analysis.edge_kinds.get((src, dst)) == "weiche":
layer, kind = layers["weiche"], "weiche"
else:
layer, kind = layers["flow"], "flow"
x1, y1 = placed[src] x1, y1 = placed[src]
x2, y2 = placed[dst] x2, y2 = placed[dst]
length = math.dist((x1, y1), (x2, y2)) length = math.dist((x1, y1), (x2, y2))
if length <= 2 * marker * 1.1: if length <= 2 * marker * 1.1:
continue continue
ux, uy = (x2 - x1) / length, (y2 - y1) / length ux, uy = (x2 - x1) / length, (y2 - y1) / length
# An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht deckt # An beiden Enden um den Marker kuerzen, damit der Pfeil ihn nicht
sx, sy = x1 + ux * marker * 1.1, y1 + uy * marker * 1.1 # deckt; das Stationsende ist nur eine Beschriftung, dort weniger.
ex, ey = x2 - ux * marker * 1.1, y2 - uy * marker * 1.1 kurz_a = marker * (0.3 if src in stationen else 1.1)
kurz_b = marker * (0.3 if dst in stationen else 1.1)
sx, sy = x1 + ux * kurz_a, y1 + uy * kurz_a
ex, ey = x2 - ux * kurz_b, y2 - uy * kurz_b
shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)], shaft = msp.add_lwpolyline([(sx, sy), (ex, ey)],
dxfattribs={"layer": layers["flow"]}) dxfattribs={"layer": layer})
tag(shaft, f"{src}->{dst}") tag(shaft, f"{src}->{dst}")
# Pfeilspitze als gefuellte Flaeche # Pfeilspitze als gefuellte Flaeche
bx, by = ex - ux * head, ey - uy * head bx, by = ex - ux * head, ey - uy * head
px, py = -uy * head * 0.38, ux * head * 0.38 px, py = -uy * head * 0.38, ux * head * 0.38
point = msp.add_solid( point = msp.add_solid(
[(ex, ey), (bx + px, by + py), (bx - px, by - py)], [(ex, ey), (bx + px, by + py), (bx - px, by - py)],
dxfattribs={"layer": layers["flow"]}, dxfattribs={"layer": layer},
) )
tag(point, f"{src}->{dst}") tag(point, f"{src}->{dst}")
if kind in ("eingang", "ausgang"):
# Stationspfeile ohne Verbindungsblock: keine TRO-TRO-Beziehung
drawn += 1
continue
# Traeger der Beziehung: Block in der Pfeilmitte, entlang des Pfeils # Traeger der Beziehung: Block in der Pfeilmitte, entlang des Pfeils
ref = msp.add_blockref( ref = msp.add_blockref(
@@ -626,7 +717,7 @@ def draw_connections(
"TO_UID": internal.get(dst, ""), "TO_UID": internal.get(dst, ""),
"FROM_TRO": src, "FROM_TRO": src,
"TO_TRO": dst, "TO_TRO": dst,
"KIND": "flow", "KIND": kind,
}) })
tag(ref, f"{src}->{dst}") tag(ref, f"{src}->{dst}")
next_id += 1 next_id += 1
@@ -653,28 +744,48 @@ def draw_legend(
title.set_placement((x0, y0 + row)) title.set_placement((x0, y0 + row))
tag(title) tag(title)
# Wie die Legende im TRO-Fluss-SVG: je vorkommendem Typ Fuellfarbe, Rand
# der Gruppe und Form, danach die Kantenfarben.
used = {t.type_name for t in analysis.tros} used = {t.type_name for t in analysis.tros}
groups = [] zeilen: list[tuple[str, str | None, str | None]] = []
for style in STYLES: for definition in TRO_CATALOG:
types = sorted(n for n in used if definition.get_name() in used:
if (get_tro(n).get_group() if get_tro(n) else "ext") == style.group) zeilen.append((f"{definition.get_name()} - {definition.describe_symbol()}",
if types: definition.get_fill(),
groups.append((style, types)) layers.get(definition.get_group(), layers["ext"])))
pfeile = [("eingang", "Materialeingang (Beladung / Eingang)"),
("ausgang", "Materialausgang (Entladung / Ausgang)"),
("flow", "Uebergabe / Ein-Ausschleusung"),
("weiche", "Weiche (Kreisel-Uebergang)")]
for index, (style, types) in enumerate(groups): y = y0
y = y0 - index * row for beschriftung, fill, rand_layer in zeilen:
box = msp.add_solid( quadrat = [(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)]
[(x0, y), (x0 + text, y), (x0 + text, y - text), (x0, y - text)], box = msp.add_hatch(dxfattribs={"layer": layers["legend"],
dxfattribs={"layer": layers[style.group]}, "true_color": ezdxf.rgb2int(hex_to_rgb(fill))})
) box.paths.add_polyline_path(quadrat, is_closed=True)
tag(box) tag(box)
label = msp.add_text( rand = msp.add_lwpolyline(quadrat, close=True, dxfattribs={"layer": rand_layer})
f"{style.label}: {', '.join(types)}", tag(rand)
height=text, label = msp.add_text(beschriftung, height=text,
dxfattribs={"layer": layers["legend"], "style": "Standard"}, dxfattribs={"layer": layers["legend"], "style": "Standard"})
)
label.set_placement((x0 + text * 1.8, y - text)) label.set_placement((x0 + text * 1.8, y - text))
tag(label) tag(label)
y -= row
for key, beschriftung in pfeile:
mitte = y - text / 2
linie = msp.add_lwpolyline([(x0 - text * 0.5, mitte), (x0 + text * 0.9, mitte)],
dxfattribs={"layer": layers[key]})
tag(linie)
spitze = msp.add_solid([(x0 + text * 1.4, mitte), (x0 + text * 0.9, mitte + text * 0.3),
(x0 + text * 0.9, mitte - text * 0.3)],
dxfattribs={"layer": layers[key]})
tag(spitze)
label = msp.add_text(beschriftung, height=text,
dxfattribs={"layer": layers[key], "style": "Standard"})
label.set_placement((x0 + text * 1.8, y - text))
tag(label)
y -= row
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -896,9 +1007,11 @@ def main(argv: list[str] | None = None) -> int:
args.marker_size, args.text_height) args.marker_size, args.text_height)
if args.flow: if args.flow:
# Stationen nur mit --flow: sie bestehen aus ihrem Zu-/Ablaufpfeil
stationen = place_stations(msp, analysis, placed, layers, args.text_height)
counts["flow"] = draw_connections( counts["flow"] = draw_connections(
msp, doc, analysis, placed, internal, layers, args.marker_size, msp, doc, analysis, {**placed, **stationen}, internal, layers,
args.text_height, next_id=next_id + len(placed), args.marker_size, args.text_height, next_id=next_id + len(placed),
) )
if args.legend: if args.legend:
draw_legend(msp, analysis, placed, layers, args.marker_size, draw_legend(msp, analysis, placed, layers, args.marker_size,
+34 -14
View File
@@ -173,10 +173,8 @@ GENERATED_MARKER = "<!-- erzeugt von lib/tro_flow.py -->"
COORD_SCALE = 0.1 COORD_SCALE = 0.1
# Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge # Zu-/Ablaufpfeil einer Station in der lagerichtigen Zeichnung: feste Laenge
# (mm) ab dem angehaengten TRO und Faecherwinkel (Grad), wenn mehrere # (mm) ab dem angehaengten TRO (siehe _station_positionen).
# Stationen am selben TRO haengen (siehe _station_positionen).
COORD_PFEIL_LAENGE = 1300 COORD_PFEIL_LAENGE = 1300
COORD_PFEIL_FAECHER = 45
# Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in # Groesste Luecke (mm) zwischen zwei benachbarten Knotenkoordinaten je Achse in
# der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne # der lagerichtigen Zeichnung. Groessere Leerstrecken (lange Foerderer ohne
@@ -1733,11 +1731,12 @@ def _station_positionen(
"""Zeichenposition der Stationsbeschriftung (Ende des Zu-/Ablaufpfeils). """Zeichenposition der Stationsbeschriftung (Ende des Zu-/Ablaufpfeils).
Feste Pfeillaenge COORD_PFEIL_LAENGE ab dem angehaengten TRO (Beladung: Feste Pfeillaenge COORD_PFEIL_LAENGE ab dem angehaengten TRO (Beladung:
der TRO danach, Entladung: der TRO davor), Richtung zur echten Station - der TRO danach, Entladung: der TRO davor) - so bleibt der Pfeil sichtbar,
so bleibt der Pfeil sichtbar, auch wenn die Station praktisch auf dem TRO auch wenn die Station praktisch auf dem TRO sitzt (Mubea SYS1 186 mm neben
sitzt (Mubea SYS1 186 mm neben TRO04). Haengen mehrere Stationen am selben TRO04). Die Richtung haelt den groessten Winkelabstand zu allen Kanten des
TRO, werden sie um COORD_PFEIL_FAECHER Grad aufgefaechert. Station ohne Anker-TROs und zu bereits gesetzten Stationspfeilen; die Richtung zur
angehaengten TRO: echte Koordinate.""" echten Station entscheidet nur bei Gleichstand. Station ohne angehaengten
TRO: echte Koordinate."""
tro_by_id = {t.tro_id: t for t in analysis.tros} tro_by_id = {t.tro_id: t for t in analysis.tros}
je_tro: dict[str, list[Tro]] = {} je_tro: dict[str, list[Tro]] = {}
ergebnis: dict[str, tuple[float, float]] = {} ergebnis: dict[str, tuple[float, float]] = {}
@@ -1751,17 +1750,38 @@ def _station_positionen(
continue continue
je_tro.setdefault(anker, []).append(station) je_tro.setdefault(anker, []).append(station)
def _abstand(a: float, b: float) -> float:
d = abs(a - b) % (2 * math.pi)
return min(d, 2 * math.pi - d)
for anker, stationen in je_tro.items(): for anker, stationen in je_tro.items():
ax, ay = tro_zeichen[anker] ax, ay = tro_zeichen[anker]
echt = tro_by_id[anker].position echt = tro_by_id[anker].position
for k, station in enumerate(sorted(stationen, key=lambda s: s.tro_id)): # Richtungen der Kettenkanten am Anker-TRO - der Pfeil soll von der
dx = dy = 0.0 # Kette WEG zeigen, nicht auf ihr liegen (Review 2026-10-02: Mubea
# TRO02 -> SYS2 lief fast deckungsgleich mit TRO02 -> TRO13).
belegt: list[float] = []
for src, dst in analysis.edges:
anderer = dst if src == anker else src if dst == anker else None
if anderer is None or anderer not in tro_zeichen:
continue
ox, oy = tro_zeichen[anderer]
if (ox, oy) != (ax, ay):
belegt.append(math.atan2(oy - ay, ox - ax))
for station in sorted(stationen, key=lambda s: s.tro_id):
wunsch = -math.pi / 2
if echt and station.position: if echt and station.position:
dx, dy = station.position[0] - echt[0], station.position[1] - echt[1] dx, dy = station.position[0] - echt[0], station.position[1] - echt[1]
if math.hypot(dx, dy) < 1.0: if math.hypot(dx, dy) >= 1.0:
dx, dy = 0.0, -1.0 wunsch = math.atan2(dy, dx)
winkel = math.atan2(dy, dx) + math.radians( # Kandidaten im 15-Grad-Raster: groesster Mindestabstand zu allen
(k - (len(stationen) - 1) / 2) * COORD_PFEIL_FAECHER) # belegten Richtungen gewinnt, bei Gleichstand die zur echten
# Station naechste Richtung.
kandidaten = [math.radians(g) for g in range(0, 360, 15)]
winkel = max(kandidaten, key=lambda w: (
round(min((_abstand(w, b) for b in belegt), default=math.pi), 3),
-_abstand(w, wunsch)))
belegt.append(winkel)
ergebnis[station.tro_id] = (ax + math.cos(winkel) * COORD_PFEIL_LAENGE, ergebnis[station.tro_id] = (ax + math.cos(winkel) * COORD_PFEIL_LAENGE,
ay + math.sin(winkel) * COORD_PFEIL_LAENGE) ay + math.sin(winkel) * COORD_PFEIL_LAENGE)
return ergebnis return ergebnis