From bfd95ea7253887656b85cf695c761e7b43590b2b Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Thu, 10 Sep 2026 14:41:25 +0200 Subject: [PATCH] Kreisel-Separatorliste (list_trackids) im CSV-Export ergaenzt Kreisel bekommen wie VF_n/GF_n ein Merkmal mit der Reihenfolge aller Separator-/BTMT-Beladung-/BTMT-Entladung-TeileIds entlang der Bahn, sortiert nach Winkel um die Kreiselachse (Start bei 0 Grad in Drehrichtung DREHRICHTUNG) - nutzt dieselbe Umlauf-Logik, die bisher nur die "Nachbarn"-Spalte gespeist hat. Merkmal-Schluessel einheitlich auf "list_trackids" umbenannt (vorher "Separatorlist" bei VF/GF). Co-Authored-By: Claude Sonnet 5 --- doc/csv-export.md | 46 ++++++++++- lib/export_csv.py | 84 ++++++++++++++++---- lib/export_neighbors.py | 63 +++++++++++---- tests/test_export_kreisel_umlauf.py | 117 ++++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 32 deletions(-) diff --git a/doc/csv-export.md b/doc/csv-export.md index 119f38b..1f4c3ea 100644 --- a/doc/csv-export.md +++ b/doc/csv-export.md @@ -199,11 +199,13 @@ Geometrie der bereits klassifizierten Items abgeleitet: ### 4. Der Kreisel-Umlauf — wie Nachbarn ueber einen Kreisel hinweg entstehen `compute_kreisel_umlauf` ist der zentrale Baustein, der Ketten-Enden, -freie Separatoren und Kreisel-Kreisel-Weichen zu einer widerspruchsfreien -Nachbarschaft zusammenfuehrt. Fuer jeden echten Kreisel werden alle Punkte -gesammelt, die dort geometrisch liegen: +freie Punkte (Separatoren UND BTMT Beladung/Entladung) und Kreisel-Kreisel- +Weichen zu einer widerspruchsfreien Nachbarschaft zusammenfuehrt. Fuer jeden +echten Kreisel werden alle Punkte gesammelt, die dort geometrisch liegen: -- freie Separatoren, deren Bounding-Box die Kreisel-Box ueberschneidet, +- freie Punkte (`KREISEL_UMLAUF_TEILEARTEN` in `lib/export_neighbors.py`: + Separator, BTMT Beladung, BTMT Entladung — alles Stationen direkt auf der + Bahn/dem Track), deren Bounding-Box die Kreisel-Box ueberschneidet, - die Kreisel-Kreisel-Weichen aus Schritt 3 (in BEIDEN beteiligten Kreisel-Umlaeufen, da eine Weiche der gemeinsame Verzweigungspunkt ist — man kann von Kreisel A nach B und zurueck), @@ -252,6 +254,42 @@ plus den naechstgelegenen rohen INSERT-Punkt (Item-Felder `_x`/`_y` vs. die sepliste-Koordinate) — beide stammen aus derselben Bauphase und liegen daher nah beieinander, auch wenn sie nicht exakt uebereinstimmen. +### Das Merkmal `"list_trackids"` — Reihenfolge als Item-Merkmal + +Zusaetzlich zur `"Nachbarn"`-Spalte (Vorgaenger/Nachfolger je Zeile) traegt +`write_separatorliste_merkmale` (`lib/export_csv.py`) jeder VF_n/GF_n-Zeile +das Merkmal `"list_trackids"` ein: die TeileIds ihrer SEP-Eintraege in +Baureihenfolge, kommagetrennt (z.B. `"0052, 0053, 0054"`). Quelle ist die +rohe `sepliste` des Items (nicht `sep_kette`, dessen dict-Reihenfolge nicht +garantiert ist). Muss NACH `map_separator_kette_items` laufen, weil erst +dann jeder sepliste-SEP-Schluessel auf sein reales Separator-Item (TeileId) +aufgeloest ist. + +`write_kreisel_separatorliste_merkmale` ueberträgt dasselbe Prinzip auf +echte Kreisel (`"ILS 2.0 Kreisel"`): Quelle ist hier NICHT die sepliste +(die kennt nur eine einzelne VF_n/GF_n-Kette), sondern +`compute_kreisel_umlauf` (`lib/export_neighbors.py`) — der bereits fuer die +Kreisel-`"Nachbarn"`-Spalte berechnete, zirkulaere Umlauf aller auf dem +Kreisel liegenden Punkte (freie Punkte — Separator UND BTMT Beladung/ +Entladung, siehe `KREISEL_UMLAUF_TEILEARTEN` —, AS/ES-Enden andockender +VF_n/GF_n-Ketten, Kreisel-Kreisel-Weichen), sortiert nach Winkel um die +Kreiselachse in Richtung des `DREHRICHTUNG`-Attributs (UZS/GUZ). Die +zurueckgegebene Liste beginnt IMMER bei Winkel 0 Grad (Kapsel-Achsrichtung, +kleinster Winkel >= 0 Grad in Umlaufrichtung) — `write_kreisel_ +separatorliste_merkmale` loest die rohen Umlauf-Schluessel (TeileId eines +freien Punkts, sep_kette-Schluessel oder Weichen-Key) auf echte TeileIds auf +und schreibt sie kommagetrennt als `"list_trackids"` auf den Kreisel selbst. +Muss darum NACH `resolve_umlauf_und_sepliste_nachbarn` (bzw. nach allen +Zeilen-Registrierungen: `build_kreisel_weiche_items`, `build_strecke_ +schleus_items`, `map_separator_kette_items`) laufen. + +BTMT Beladung/Entladung sind bewusst NICHT in `SENSOR_TEILEARTEN` +aufgenommen (die bleibt Separator/Scanner vorbehalten, siehe +`compute_sensor_zuordnung`/`compute_scanner_nearest_separator` in Abschnitt +6) — eine eigene Konstante `KREISEL_UMLAUF_TEILEARTEN` sammelt stattdessen +genau die TeileArten, die als frei platzierter Punkt auf einer Kreisel-Bahn +liegen koennen. + ### 6. Sensor-Zuordnung, Ausgabe `compute_sensor_zuordnung` bestimmt fuer jeden Separator/Scanner ohne diff --git a/lib/export_csv.py b/lib/export_csv.py index db520a5..46cde87 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -130,6 +130,7 @@ from export_neighbors import ( SENSOR_TEILEARTEN, WEICHE_TEILEART, GF_VF_TEILEARTEN, + KREISEL_UMLAUF_TEILEARTEN, ) BLOCKPATTERNS = load_patterns() @@ -234,7 +235,7 @@ def build_variofoerderer_merkmale(block): export_raw.json-Dateien (z.B. tests/reference/export_raw.json) und noch nicht neu aufgebaute Bloecke in der Zeichnung weiterhin Werte liefern. - Das Merkmal "Separatorlist" (Reihenfolge der Separator-TeileIds entlang + Das Merkmal "list_trackids" (Reihenfolge der Separator-TeileIds entlang der Strecke) steht hier NICHT drin - es kommt erst nach der sepliste- Aufloesung dazu (siehe write_separatorliste_merkmale in process_blocks), weil an dieser Stelle die x/y-Koordinaten aus der sepliste-XDATA noch @@ -778,15 +779,17 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette, """ # Gemeinsamer Lookup fuer BEIDE Schluesselarten, die in kreisel_umlauf und # sep_kette als punkt_key/vorgaenger/nachfolger auftreten koennen: freie - # Separatoren (TeileId), Kreisel-Kreisel-Weichen (weiche_by_key) UND - # sepliste-Zeilen (sep_kette_by_key, Schluessel "#"). - # Ein Umlauf-Punkt kann naemlich ein AS/ES-Ende sein (sep_kette-Schluessel) - # und ein sep_kette-Vorgaenger/Nachfolger kann das freie Kreiselende gar - # nicht sein (das loest ausschliesslich compute_kreisel_umlauf auf) - erst - # der gemeinsame Lookup deckt alle Kombinationen ab. - separator_by_id = {it.get("teileid", ""): it for it in items - if it.get("teileart", "") == "ILS 2.0 Separator"} - alle_schluessel = {**sep_kette_by_key, **weiche_by_key, **separator_by_id} + # Punkte (Separator/BTMT Beladung/BTMT Entladung, TeileId - siehe + # KREISEL_UMLAUF_TEILEARTEN), Kreisel-Kreisel-Weichen + # (weiche_by_key) UND sepliste-Zeilen (sep_kette_by_key, Schluessel + # "#"). Ein Umlauf-Punkt kann naemlich ein AS/ES-Ende + # sein (sep_kette-Schluessel) und ein sep_kette-Vorgaenger/Nachfolger kann + # das freie Kreiselende gar nicht sein (das loest ausschliesslich + # compute_kreisel_umlauf auf) - erst der gemeinsame Lookup deckt alle + # Kombinationen ab. + freipunkt_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") in KREISEL_UMLAUF_TEILEARTEN} + alle_schluessel = {**sep_kette_by_key, **weiche_by_key, **freipunkt_by_id} # Freie Separatoren, Weichen UND die AS/ES-Enden andockender Ketten # bekommen ihre "Nachbarn"-Spalte aus dem Kreisel-Umlauf. @@ -827,7 +830,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette, def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None): """Traegt in jede Strecke (VF_n/GF_n mit "sepliste") das Merkmal - "Separatorlist" ein: die TeileIds ihrer Separatoren in Baureihenfolge, + "list_trackids" ein: die TeileIds ihrer Separatoren in Baureihenfolge, kommagetrennt (analog zu "Zuordnung"/"Nachbarn"), z.B. "0052, 0053, 0054". Quelle ist die rohe "sepliste" des Items (AS/SEP/ES mit x/y, bereits in @@ -857,12 +860,57 @@ def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None): sep_item = sep_kette_by_key.get(key) if sep_item is None: if dbg: - dbg(f" Separatorlist {key}: kein aufgeloestes Separator-Item " + dbg(f" list_trackids {key}: kein aufgeloestes Separator-Item " f"- uebersprungen") continue separator_ids.append(sep_item.get("teileid", "")) if separator_ids: - item["merkmale"]["Separatorlist"] = ", ".join(separator_ids) + item["merkmale"]["list_trackids"] = ", ".join(separator_ids) + + +def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_key, + weiche_by_key, dbg=None): + """Traegt in jeden Kreisel ("ILS 2.0 Kreisel") das Merkmal "list_trackids" + ein (gleicher Schluessel wie bei VF_n/GF_n, siehe write_separatorliste_ + merkmale): die TeileIds aller auf ihm liegenden freien Punkte (Separator, + BTMT Beladung/Entladung - siehe KREISEL_UMLAUF_TEILEARTEN), + andockenden AS/ES-Streckenenden und Kreisel-Kreisel-Weichen, in + Umlaufreihenfolge ab Winkel 0 Grad (siehe compute_kreisel_umlauf - die + Liste beginnt dort bereits bei 0 Grad in Drehrichtung DREHRICHTUNG), + kommagetrennt. + + Quelle ist kreisel_umlauf (export_neighbors.compute_kreisel_umlauf), das + fuer jeden Kreisel bereits alle Punkte (freie Punkte, AS/ES-Enden, + Kreisel-Kreisel-Weichen) nach Winkel um die Kreiselachse sortiert hat - + hier werden nur noch deren rohe Schluessel (TeileId direkt, sep_kette- + Schluessel oder Weichen-"key") auf die TeileId des jeweils zugehoerigen + Items aufgeloest. Derselbe Lookup wie in + resolve_umlauf_und_sepliste_nachbarn, daher muss diese Funktion NACH + map_separator_kette_items/build_kreisel_weiche_items/ + build_strecke_schleus_items laufen (deren TeileIds stehen erst dann fest). + """ + freipunkt_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") in KREISEL_UMLAUF_TEILEARTEN} + alle_schluessel = {**sep_kette_by_key, **weiche_by_key, **freipunkt_by_id} + + kreisel_by_id = {it.get("teileid", ""): it for it in items + if it.get("teileart", "") == "ILS 2.0 Kreisel"} + + for tid, punkte in (kreisel_umlauf or {}).items(): + kreisel_item = kreisel_by_id.get(tid) + if kreisel_item is None or not punkte: + continue + teileids = [] + for punkt_key, _vorgaenger, _nachfolger in punkte: + item = alle_schluessel.get(punkt_key) + if item is None: + if dbg: + dbg(f" list_trackids Kreisel {tid}: kein Item fuer " + f"Umlauf-Punkt {punkt_key!r} - uebersprungen") + continue + teileids.append(item.get("teileid", "")) + if teileids: + kreisel_item["merkmale"]["list_trackids"] = ", ".join(teileids) def process_blocks(blocks, lookup, dbg=None): @@ -1262,13 +1310,21 @@ def process_blocks(blocks, lookup, dbg=None): map_separator_kette_items( items, sep_kette, sep_kette_by_key=sep_kette_by_key, dbg=dbg) - # Merkmal "Separatorlist" je Strecke (Reihenfolge der SEP-TeileIds aus der + # Merkmal "list_trackids" je Strecke (Reihenfolge der SEP-TeileIds aus der # sepliste, x/y bereits oben auf das reale Separator-Item aufgeloest). write_separatorliste_merkmale(items, sep_kette_by_key, dbg=dbg) resolve_umlauf_und_sepliste_nachbarn( items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key) + # Merkmal "list_trackids" je Kreisel: dieselben Umlauf-Punkte (freie + # Separatoren, AS/ES-Enden, Kreisel-Kreisel-Weichen), die oben schon die + # "Nachbarn"-Spalte gespeist haben, jetzt als eine einzige, bei 0 Grad + # beginnende Reihenfolge auf den Kreisel selbst geschrieben (analog zu + # write_separatorliste_merkmale bei VF_n/GF_n). + write_kreisel_separatorliste_merkmale( + items, kreisel_umlauf, sep_kette_by_key, weiche_by_key, dbg=dbg) + # Nach dem Sammelheimen aller Zeilen: sortiert nach TeileId ausgeben, damit # Elementnummer (erste Spalte) und TeileId nach Moeglichkeit uebereinstimmen # (siehe format_csv_line/Elementnummer-Neuvergabe unten). Numerische IDs diff --git a/lib/export_neighbors.py b/lib/export_neighbors.py index 0761ee1..3ef6f2b 100644 --- a/lib/export_neighbors.py +++ b/lib/export_neighbors.py @@ -74,6 +74,19 @@ STRECKEN_TEILEARTEN = {"ILS 2.0 Gefaellestrecke", "ILS 2.0 Strecke", "ILS 2.0 St OMNIFLO_TEILEARTEN = {"Omniflo Kurve", "Omniflo Weiche", "Omniflo Gerade"} SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"} +# Frei auf der Bahn platzierte Elemente, die im Kreisel-Umlauf +# (compute_kreisel_umlauf) und im Kreisel-Merkmal "list_trackids" (export_csv.py +# write_kreisel_separatorliste_merkmale) als eigene Punkte auftreten koennen - +# wie ein Separator per Bounding-Box-Ueberschneidung einer Kreiselhaelfte/ +# einem Kreisel zugeordnet (siehe compute_sensor_zuordnung/compute_kreisel_ +# umlauf). BTMT-Beladung/-Entladung sind Stationen AUF der Strecke, keine +# Sensoren - eigene Konstante statt Erweiterung von SENSOR_TEILEARTEN, damit +# deren Sensor-spezifische Weiterverarbeitung (z.B. compute_scanner_nearest_ +# separator) unberuehrt bleibt. +KREISEL_UMLAUF_TEILEARTEN = { + "ILS 2.0 Separator", "ILS 2.0 BTMT Beladung", "ILS 2.0 BTMT Entladung", "ILS Weiche", +} + # Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann- # station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0 # Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox. @@ -1380,10 +1393,11 @@ def compute_sep_kette(items, dbg=None): def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, touch_switches=None, dbg=None): """Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort - liegenden Punkte - frei platzierte Separatoren (per compute_sensor_ - zuordnung bereits einer Kreiselhaelfte zugeordnet), die AS/ES-Enden - aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe wie - compute_strecke_kreisel_schleus zugeordnet) UND die Kreisel-Kreisel- + liegenden Punkte - frei platzierte Separatoren UND BTMT-Beladung/ + -Entladung (KREISEL_UMLAUF_TEILEARTEN, per BBox-Ueberschneidung + wie compute_sensor_zuordnung einer Kreiselhaelfte zugeordnet), die AS/ES- + Enden aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe + wie compute_strecke_kreisel_schleus zugeordnet) UND die Kreisel-Kreisel- Weichen (touch_switches, siehe compute_kreisel_touch_switches) - in eine geschlossene Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut). @@ -1399,7 +1413,11 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, _kreisel_capsule) gemessen, 0 Grad = Kapsel-Achsrichtung. UZS (Uhrzeigersinn, Default) sortiert nach fallendem Winkel, GUZ nach steigendem - beides ergibt einen konsistenten Umlauf, die Achsrichtung - selbst ist nur der Nullpunkt. + selbst ist nur der Nullpunkt. Die zurueckgegebene Liste beginnt IMMER bei + diesem Nullpunkt (kleinster Winkel >= 0 Grad in Umlaufrichtung) - das ist + fuer die reine Vorgaenger-/Nachfolger-Ermittlung (zirkulaer) irrelevant, + aber Voraussetzung fuer eine reproduzierbare, bei 0 Grad beginnende + "list_trackids" (siehe export_csv.py write_kreisel_separatorliste_merkmale). items = dieselbe Liste wie bei compute_neighbor_ids/compute_sensor_ zuordnung. sep_kette = Rueckgabe von compute_sep_kette (fuer die AS/ES- @@ -1408,10 +1426,11 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, Umlauf). Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key, - nachfolger_key), ...]} in Umlaufreihenfolge - punkt_key ist eine - Separator-TeileId (freie Separatoren), ein sep_kette-Schluessel (AS/ES- - Enden) oder ein touch_switches-"key" (Kreisel-Kreisel-Weiche). Kreisel - ohne zugeordnete Punkte liefern eine leere Liste. + nachfolger_key), ...]} in Umlaufreihenfolge, beginnend bei Winkel 0 Grad - + punkt_key ist die TeileId eines freien Punkts (Separator oder BTMT- + Beladung/-Entladung, siehe KREISEL_UMLAUF_TEILEARTEN), ein + sep_kette-Schluessel (AS/ES-Enden) oder ein touch_switches-"key" (Kreisel- + Kreisel-Weiche). Kreisel ohne zugeordnete Punkte liefern eine leere Liste. """ half_tol = tolerance_mm / 2.0 kreisel = {} @@ -1451,7 +1470,7 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, punkte_je_kreisel = {tid: [] for tid in kreisel} for item in items: - if item.get("teileart", "") != "ILS 2.0 Separator": + if item.get("teileart", "") not in KREISEL_UMLAUF_TEILEARTEN: continue bbox = item.get("_bbox") if not bbox: @@ -1495,11 +1514,27 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0, acy = (cap["a"][1] + cap["b"][1]) / 2.0 achs_winkel = math.atan2(cap["b"][1] - cap["a"][1], cap["b"][0] - cap["a"][0]) - def _winkel(pxy): - return math.atan2(pxy[2] - acy, pxy[1] - acx) - achs_winkel + uzs = (drehrichtung or "UZS").strip().upper() != "GUZ" - reverse = (drehrichtung or "UZS").strip().upper() != "GUZ" - sortiert = sorted(punkte, key=lambda p: _winkel(p), reverse=reverse) + def _winkel(pxy): + # Rohwinkel relativ zur Achsrichtung, auf [0, 2*pi) normiert IN + # UMLAUFRICHTUNG. Vorher (reine Vorgaenger-/Nachfolger-Ermittlung + # ohne Startpunkt-Anspruch) wurde UZS durch reverse=True auf dem + # rohen mathematischen Winkel erreicht (= "absteigend sortieren"); + # absteigend sortieren auf theta ist aequivalent zu aufsteigend + # sortieren auf -theta - darum wird HIER fuer UZS negiert (NICHT + # fuer GUZ, das dem unveraenderten mathematischen Winkel bereits + # aufsteigend entspricht). Ohne dieses Vorzeichen liefe der UZS- + # Umlauf in die falsche (zu GUZ spiegelverkehrte) Richtung - siehe + # tests/test_export_kreisel_umlauf.py::TestKreiselUmlaufStartetBeiNullGrad. + # Die Normierung auf [0, 2*pi) sorgt zusaetzlich dafuer, dass + # Index 0 nach dem Sortieren der kleinste Winkel >= 0 Grad ist + # (der gewuenschte Start bei 0 Grad). + roh = math.atan2(pxy[2] - acy, pxy[1] - acx) - achs_winkel + roh = -roh if uzs else roh + return roh % (2.0 * math.pi) + + sortiert = sorted(punkte, key=lambda p: _winkel(p)) n = len(sortiert) umlauf = [] for i, p in enumerate(sortiert): diff --git a/tests/test_export_kreisel_umlauf.py b/tests/test_export_kreisel_umlauf.py index 1e2f202..43da2a2 100644 --- a/tests/test_export_kreisel_umlauf.py +++ b/tests/test_export_kreisel_umlauf.py @@ -25,6 +25,7 @@ from export_csv import ( # noqa: E402 build_strecke_schleus_items, map_separator_kette_items, resolve_umlauf_und_sepliste_nachbarn, + write_kreisel_separatorliste_merkmale, ) DURCHMESSER_MM = 800.0 @@ -51,6 +52,17 @@ def _separator(teileid, x, y): } +def _btmt(teileart, teileid, x, y): + """BTMT-Beladung/-Entladung-Stationsblock ("ILS 2.0 BTMT Beladung"/ + "ILS 2.0 BTMT Entladung") - frei auf der Bahn platziert wie ein + Separator, siehe KREISEL_UMLAUF_TEILEARTEN.""" + return { + "teileart": teileart, + "teileid": teileid, + "_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 150.0}, + } + + def _zwei_beruehrende_kreisel(): """Zwei Kreisel, deren AN8-Enden sich exakt beruehren (End-an-End, wie im Mubea-Fall: 'Kreis/Kreis'-Beruehrung an der jeweiligen AN8-Kappe). @@ -280,3 +292,108 @@ class TestSeplisteNachbarnAufgeloest: assert sep_nachbarn == {as_item["teileid"], es_item["teileid"]}, ( f'SEP-Zeile hat Nachbarn {sep_nachbarn!r}, erwartet ' f'{{{as_item["teileid"]!r}, {es_item["teileid"]!r}}}') + + +class TestKreiselUmlaufStartetBeiNullGrad: + """compute_kreisel_umlauf muss die Umlauf-Liste je Kreisel bei Winkel + 0 Grad (Kapsel-Achsrichtung, siehe _kreisel_capsule) beginnen lassen - + Voraussetzung fuer eine reproduzierbare Separatorliste (Kundenwunsch: + Start immer bei 0 Grad, nicht bei einem beliebigen Listenanfang).""" + + def _kreisel_mit_vier_separatoren(self, drehrichtung): + # Kreisel entlang +X (Achse von x=400 bis x=1400, siehe _kreisel). + # Vier Separatoren rundherum bei ca. 10/100/190/280 Grad um die + # Kreiselmitte (900, 0) - keiner exakt auf 0 Grad, damit der Test + # echte Rotation der sortierten Liste beweist statt eines Zufalls- + # treffers bei Index 0. + k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0) + k["attribs"]["DREHRICHTUNG"] = drehrichtung + # r=350mm < Kreiselradius 400mm, damit alle vier Punkte innerhalb der + # Kreisel-AABB liegen (axis x in [400,1400] +- radius 400 -> y in + # [-400,400]); r=700 (ausserhalb) faellt bei ~90/270 Grad aus der Box. + cx, cy, r = 900.0, 0.0, 350.0 + import math + seps = [] + for name, grad in (("S010", 10.0), ("S100", 100.0), + ("S190", 190.0), ("S280", 280.0)): + rad = math.radians(grad) + seps.append(_separator(name, cx + r * math.cos(rad), cy + r * math.sin(rad))) + return [k] + seps + + def test_start_bei_0_grad_uzs(self): + # UZS negiert den Rohwinkel vor der Normierung (siehe compute_kreisel_ + # umlauf - UZS entspricht dem alten "absteigend nach rohem Winkel + # sortieren", das ueber die Negation+aufsteigend-Sortierung erreicht + # wird): 10/100/190/280 Grad werden dadurch zu (normiert) 350/260/ + # 170/80 Grad - der kleinste (und damit Listenanfang) ist S280 (80 + # Grad), NICHT S010 (350 Grad, liegt kurz VOR dem vollen Umlauf). + items = self._kreisel_mit_vier_separatoren("UZS") + umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=0.0, + durchmesser_mm=DURCHMESSER_MM) + keys = [p[0] for p in umlauf["0001"]] + assert keys[0] == "S280", f"Umlauf startet nicht bei 0 Grad (UZS): {keys}" + assert keys == ["S280", "S190", "S100", "S010"] + + def test_start_bei_0_grad_guz(self): + # GUZ (kein Vorzeichenwechsel): der Rohwinkel steigt hier bereits + # monoton mit 10/100/190/280 Grad - der Umlauf startet beim kleinsten + # (S010) und laeuft aufsteigend weiter. + items = self._kreisel_mit_vier_separatoren("GUZ") + umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=0.0, + durchmesser_mm=DURCHMESSER_MM) + keys = [p[0] for p in umlauf["0001"]] + assert keys[0] == "S010", f"Umlauf startet nicht bei 0 Grad (GUZ): {keys}" + assert keys == ["S010", "S100", "S190", "S280"] + + +class TestKreiselListTrackidsMerkmal: + """write_kreisel_separatorliste_merkmale schreibt die bei 0 Grad + beginnende Umlaufreihenfolge (Separatoren, AS/ES-Enden, Kreisel-Kreisel- + Weichen) als Merkmal "list_trackids" auf den Kreisel selbst (analog zum + gleichnamigen Merkmal bei VF_n/GF_n, siehe write_separatorliste_merkmale).""" + + def test_freie_separatoren_in_umlaufreihenfolge(self): + k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0) + k["merkmale"] = {} + sep_a = _separator("0020", 300.0, 300.0) # ~63 Grad + sep_b = _separator("0021", 1400.0, -50.0) # nahe 0 Grad (Ende Achse) + items = [k, sep_a, sep_b] + + kreisel_umlauf = compute_kreisel_umlauf( + items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM) + write_kreisel_separatorliste_merkmale( + items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={}) + + kreisel_item = items[0] + assert kreisel_item["merkmale"]["list_trackids"] == "0021, 0020" + + def test_kreisel_ohne_umlauf_punkte_bekommt_kein_merkmal(self): + k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0) + k["merkmale"] = {} + items = [k] + kreisel_umlauf = compute_kreisel_umlauf( + items, sep_kette={}, tolerance_mm=0.0, durchmesser_mm=DURCHMESSER_MM) + write_kreisel_separatorliste_merkmale( + items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={}) + assert "list_trackids" not in items[0]["merkmale"] + + def test_btmt_beladung_und_entladung_zaehlen_als_freie_punkte(self): + """BTMT Beladung/Entladung sind Stationen AUF der Bahn/Track wie ein + Separator - sie muessen im Kreisel-Umlauf UND in list_trackids + des Kreisels auftauchen (Kundenwunsch), nicht nur echte Separatoren.""" + k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0) + k["merkmale"] = {} + sep = _separator("0020", 300.0, 300.0) + bel = _btmt("ILS 2.0 BTMT Beladung", "0030", 900.0, 400.0) + ent = _btmt("ILS 2.0 BTMT Entladung", "0031", 1400.0, -50.0) + items = [k, sep, bel, ent] + + kreisel_umlauf = compute_kreisel_umlauf( + items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM) + keys = [p[0] for p in kreisel_umlauf["0001"]] + assert set(keys) == {"0020", "0030", "0031"}, ( + f"BTMT Beladung/Entladung fehlen im Kreisel-Umlauf: {keys}") + + write_kreisel_separatorliste_merkmale( + items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={}) + assert k["merkmale"]["list_trackids"] == "0031, 0020, 0030"