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 <noreply@anthropic.com>
This commit is contained in:
+42
-4
@@ -199,11 +199,13 @@ Geometrie der bereits klassifizierten Items abgeleitet:
|
|||||||
### 4. Der Kreisel-Umlauf — wie Nachbarn ueber einen Kreisel hinweg entstehen
|
### 4. Der Kreisel-Umlauf — wie Nachbarn ueber einen Kreisel hinweg entstehen
|
||||||
|
|
||||||
`compute_kreisel_umlauf` ist der zentrale Baustein, der Ketten-Enden,
|
`compute_kreisel_umlauf` ist der zentrale Baustein, der Ketten-Enden,
|
||||||
freie Separatoren und Kreisel-Kreisel-Weichen zu einer widerspruchsfreien
|
freie Punkte (Separatoren UND BTMT Beladung/Entladung) und Kreisel-Kreisel-
|
||||||
Nachbarschaft zusammenfuehrt. Fuer jeden echten Kreisel werden alle Punkte
|
Weichen zu einer widerspruchsfreien Nachbarschaft zusammenfuehrt. Fuer jeden
|
||||||
gesammelt, die dort geometrisch liegen:
|
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
|
- die Kreisel-Kreisel-Weichen aus Schritt 3 (in BEIDEN beteiligten
|
||||||
Kreisel-Umlaeufen, da eine Weiche der gemeinsame Verzweigungspunkt ist —
|
Kreisel-Umlaeufen, da eine Weiche der gemeinsame Verzweigungspunkt ist —
|
||||||
man kann von Kreisel A nach B und zurueck),
|
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
|
sepliste-Koordinate) — beide stammen aus derselben Bauphase und liegen daher
|
||||||
nah beieinander, auch wenn sie nicht exakt uebereinstimmen.
|
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
|
### 6. Sensor-Zuordnung, Ausgabe
|
||||||
|
|
||||||
`compute_sensor_zuordnung` bestimmt fuer jeden Separator/Scanner ohne
|
`compute_sensor_zuordnung` bestimmt fuer jeden Separator/Scanner ohne
|
||||||
|
|||||||
+70
-14
@@ -130,6 +130,7 @@ from export_neighbors import (
|
|||||||
SENSOR_TEILEARTEN,
|
SENSOR_TEILEARTEN,
|
||||||
WEICHE_TEILEART,
|
WEICHE_TEILEART,
|
||||||
GF_VF_TEILEARTEN,
|
GF_VF_TEILEARTEN,
|
||||||
|
KREISEL_UMLAUF_TEILEARTEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
BLOCKPATTERNS = load_patterns()
|
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
|
export_raw.json-Dateien (z.B. tests/reference/export_raw.json) und noch
|
||||||
nicht neu aufgebaute Bloecke in der Zeichnung weiterhin Werte liefern.
|
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-
|
der Strecke) steht hier NICHT drin - es kommt erst nach der sepliste-
|
||||||
Aufloesung dazu (siehe write_separatorliste_merkmale in process_blocks),
|
Aufloesung dazu (siehe write_separatorliste_merkmale in process_blocks),
|
||||||
weil an dieser Stelle die x/y-Koordinaten aus der sepliste-XDATA noch
|
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
|
# Gemeinsamer Lookup fuer BEIDE Schluesselarten, die in kreisel_umlauf und
|
||||||
# sep_kette als punkt_key/vorgaenger/nachfolger auftreten koennen: freie
|
# sep_kette als punkt_key/vorgaenger/nachfolger auftreten koennen: freie
|
||||||
# Separatoren (TeileId), Kreisel-Kreisel-Weichen (weiche_by_key) UND
|
# Punkte (Separator/BTMT Beladung/BTMT Entladung, TeileId - siehe
|
||||||
# sepliste-Zeilen (sep_kette_by_key, Schluessel "<strecke_id>#<lfdnr>").
|
# KREISEL_UMLAUF_TEILEARTEN), Kreisel-Kreisel-Weichen
|
||||||
# Ein Umlauf-Punkt kann naemlich ein AS/ES-Ende sein (sep_kette-Schluessel)
|
# (weiche_by_key) UND sepliste-Zeilen (sep_kette_by_key, Schluessel
|
||||||
# und ein sep_kette-Vorgaenger/Nachfolger kann das freie Kreiselende gar
|
# "<strecke_id>#<lfdnr>"). Ein Umlauf-Punkt kann naemlich ein AS/ES-Ende
|
||||||
# nicht sein (das loest ausschliesslich compute_kreisel_umlauf auf) - erst
|
# sein (sep_kette-Schluessel) und ein sep_kette-Vorgaenger/Nachfolger kann
|
||||||
# der gemeinsame Lookup deckt alle Kombinationen ab.
|
# das freie Kreiselende gar nicht sein (das loest ausschliesslich
|
||||||
separator_by_id = {it.get("teileid", ""): it for it in items
|
# compute_kreisel_umlauf auf) - erst der gemeinsame Lookup deckt alle
|
||||||
if it.get("teileart", "") == "ILS 2.0 Separator"}
|
# Kombinationen ab.
|
||||||
alle_schluessel = {**sep_kette_by_key, **weiche_by_key, **separator_by_id}
|
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
|
# Freie Separatoren, Weichen UND die AS/ES-Enden andockender Ketten
|
||||||
# bekommen ihre "Nachbarn"-Spalte aus dem Kreisel-Umlauf.
|
# 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):
|
def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None):
|
||||||
"""Traegt in jede Strecke (VF_n/GF_n mit "sepliste") das Merkmal
|
"""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".
|
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
|
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)
|
sep_item = sep_kette_by_key.get(key)
|
||||||
if sep_item is None:
|
if sep_item is None:
|
||||||
if dbg:
|
if dbg:
|
||||||
dbg(f" Separatorlist {key}: kein aufgeloestes Separator-Item "
|
dbg(f" list_trackids {key}: kein aufgeloestes Separator-Item "
|
||||||
f"- uebersprungen")
|
f"- uebersprungen")
|
||||||
continue
|
continue
|
||||||
separator_ids.append(sep_item.get("teileid", ""))
|
separator_ids.append(sep_item.get("teileid", ""))
|
||||||
if separator_ids:
|
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):
|
def process_blocks(blocks, lookup, dbg=None):
|
||||||
@@ -1262,13 +1310,21 @@ def process_blocks(blocks, lookup, dbg=None):
|
|||||||
map_separator_kette_items(
|
map_separator_kette_items(
|
||||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key, dbg=dbg)
|
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).
|
# sepliste, x/y bereits oben auf das reale Separator-Item aufgeloest).
|
||||||
write_separatorliste_merkmale(items, sep_kette_by_key, dbg=dbg)
|
write_separatorliste_merkmale(items, sep_kette_by_key, dbg=dbg)
|
||||||
|
|
||||||
resolve_umlauf_und_sepliste_nachbarn(
|
resolve_umlauf_und_sepliste_nachbarn(
|
||||||
items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key)
|
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
|
# Nach dem Sammelheimen aller Zeilen: sortiert nach TeileId ausgeben, damit
|
||||||
# Elementnummer (erste Spalte) und TeileId nach Moeglichkeit uebereinstimmen
|
# Elementnummer (erste Spalte) und TeileId nach Moeglichkeit uebereinstimmen
|
||||||
# (siehe format_csv_line/Elementnummer-Neuvergabe unten). Numerische IDs
|
# (siehe format_csv_line/Elementnummer-Neuvergabe unten). Numerische IDs
|
||||||
|
|||||||
+49
-14
@@ -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"}
|
OMNIFLO_TEILEARTEN = {"Omniflo Kurve", "Omniflo Weiche", "Omniflo Gerade"}
|
||||||
SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"}
|
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-
|
# Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann-
|
||||||
# station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0
|
# station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0
|
||||||
# Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox.
|
# 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,
|
def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||||
touch_switches=None, dbg=None):
|
touch_switches=None, dbg=None):
|
||||||
"""Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort
|
"""Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort
|
||||||
liegenden Punkte - frei platzierte Separatoren (per compute_sensor_
|
liegenden Punkte - frei platzierte Separatoren UND BTMT-Beladung/
|
||||||
zuordnung bereits einer Kreiselhaelfte zugeordnet), die AS/ES-Enden
|
-Entladung (KREISEL_UMLAUF_TEILEARTEN, per BBox-Ueberschneidung
|
||||||
aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe wie
|
wie compute_sensor_zuordnung einer Kreiselhaelfte zugeordnet), die AS/ES-
|
||||||
compute_strecke_kreisel_schleus zugeordnet) UND die Kreisel-Kreisel-
|
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
|
Weichen (touch_switches, siehe compute_kreisel_touch_switches) - in eine
|
||||||
geschlossene Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut).
|
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
|
_kreisel_capsule) gemessen, 0 Grad = Kapsel-Achsrichtung. UZS
|
||||||
(Uhrzeigersinn, Default) sortiert nach fallendem Winkel, GUZ nach
|
(Uhrzeigersinn, Default) sortiert nach fallendem Winkel, GUZ nach
|
||||||
steigendem - beides ergibt einen konsistenten Umlauf, die Achsrichtung
|
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_
|
items = dieselbe Liste wie bei compute_neighbor_ids/compute_sensor_
|
||||||
zuordnung. sep_kette = Rueckgabe von compute_sep_kette (fuer die AS/ES-
|
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).
|
Umlauf).
|
||||||
|
|
||||||
Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key,
|
Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key,
|
||||||
nachfolger_key), ...]} in Umlaufreihenfolge - punkt_key ist eine
|
nachfolger_key), ...]} in Umlaufreihenfolge, beginnend bei Winkel 0 Grad -
|
||||||
Separator-TeileId (freie Separatoren), ein sep_kette-Schluessel (AS/ES-
|
punkt_key ist die TeileId eines freien Punkts (Separator oder BTMT-
|
||||||
Enden) oder ein touch_switches-"key" (Kreisel-Kreisel-Weiche). Kreisel
|
Beladung/-Entladung, siehe KREISEL_UMLAUF_TEILEARTEN), ein
|
||||||
ohne zugeordnete Punkte liefern eine leere Liste.
|
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
|
half_tol = tolerance_mm / 2.0
|
||||||
kreisel = {}
|
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}
|
punkte_je_kreisel = {tid: [] for tid in kreisel}
|
||||||
|
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.get("teileart", "") != "ILS 2.0 Separator":
|
if item.get("teileart", "") not in KREISEL_UMLAUF_TEILEARTEN:
|
||||||
continue
|
continue
|
||||||
bbox = item.get("_bbox")
|
bbox = item.get("_bbox")
|
||||||
if not 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
|
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])
|
achs_winkel = math.atan2(cap["b"][1] - cap["a"][1], cap["b"][0] - cap["a"][0])
|
||||||
|
|
||||||
def _winkel(pxy):
|
uzs = (drehrichtung or "UZS").strip().upper() != "GUZ"
|
||||||
return math.atan2(pxy[2] - acy, pxy[1] - acx) - achs_winkel
|
|
||||||
|
|
||||||
reverse = (drehrichtung or "UZS").strip().upper() != "GUZ"
|
def _winkel(pxy):
|
||||||
sortiert = sorted(punkte, key=lambda p: _winkel(p), reverse=reverse)
|
# 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)
|
n = len(sortiert)
|
||||||
umlauf = []
|
umlauf = []
|
||||||
for i, p in enumerate(sortiert):
|
for i, p in enumerate(sortiert):
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from export_csv import ( # noqa: E402
|
|||||||
build_strecke_schleus_items,
|
build_strecke_schleus_items,
|
||||||
map_separator_kette_items,
|
map_separator_kette_items,
|
||||||
resolve_umlauf_und_sepliste_nachbarn,
|
resolve_umlauf_und_sepliste_nachbarn,
|
||||||
|
write_kreisel_separatorliste_merkmale,
|
||||||
)
|
)
|
||||||
|
|
||||||
DURCHMESSER_MM = 800.0
|
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():
|
def _zwei_beruehrende_kreisel():
|
||||||
"""Zwei Kreisel, deren AN8-Enden sich exakt beruehren (End-an-End, wie im
|
"""Zwei Kreisel, deren AN8-Enden sich exakt beruehren (End-an-End, wie im
|
||||||
Mubea-Fall: 'Kreis/Kreis'-Beruehrung an der jeweiligen AN8-Kappe).
|
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"]}, (
|
assert sep_nachbarn == {as_item["teileid"], es_item["teileid"]}, (
|
||||||
f'SEP-Zeile hat Nachbarn {sep_nachbarn!r}, erwartet '
|
f'SEP-Zeile hat Nachbarn {sep_nachbarn!r}, erwartet '
|
||||||
f'{{{as_item["teileid"]!r}, {es_item["teileid"]!r}}}')
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user