export_csv: AS/ES direkt aus ZZ_ASES_MARKER klassifizieren, Kreisel<->Strecke-Kollisionserzeugung entfernen
Stufe 2 (Python-Seite von "sepliste: Bauteil-IDs eintragen"): AS/ES-Kettenenden sind jetzt ganz normale Items, direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert (TeileId/Position/Boundingbox vom Marker-Block selbst) - keine Kollisionsberechnung mehr, die sie erfindet. - lib/export_neighbors.py: compute_strecke_kreisel_schleus entfernt (suchte bisher per Kollisionstest den anschliessenden Kreisel und erzeugte dabei bei JEDEM Export eine neue TeileId fuer denselben physischen Punkt). AUSSCHLEUS_/ EINSCHLEUS_TEILEART sind jetzt Teil von KREISEL_UMLAUF_TEILEARTEN - ein AS/ES bringt seine eigene Bounding-Box vom Marker mit und wird dadurch, wie jeder freie Separator, ueber die normale BBox-Ueberschneidung einem Kreisel zugeordnet; die gesonderte "naechster Kreisel per sep_kette"-Suche in compute_kreisel_umlauf (dessen sep_kette-Parameter damit entfaellt) ist ueberfluessig geworden. compute_sep_kette gibt je Eintrag zusaetzlich das Feld "id" weiter (Bauteil-TeileId, siehe vorheriger Commit). - lib/export_csv.py: process_blocks klassifiziert ZZ_ASES_MARKER direkt (TYP- Attribut waehlt Ausschleus-/Einschleuselement); build_strecke_schleus_items entfaellt komplett. map_separator_kette_items -> map_sepliste_kette_items: ordnet jetzt AS/SEP/ES exakt per ID-Gleichheit zu (sepliste-Feld "id" gegen die TeileId der klassifizierten Items) statt per Koordinaten-Naehe - SEP faellt bei fehlendem "id"-Feld weiterhin auf den alten Naehe-Abgleich zurueck (Altbestand), AS/ES ohne "id" bleiben unaufgeloest. - Neu: pruefe_ases_in_beiden_containern prueft am Ende von process_blocks, ob jede AS-/ES-Zeile sowohl in den TrackIds ihrer eigenen Strecke als auch in denen mindestens eines Kreisels auftaucht, und traegt bei Abweichung eine Warnung ein (Spalte "Warnungen") - Konsistenzpruefung fuer genau die beiden "Container", zu denen ein AS/ES gehoert. - Aufgeraeumt (tote Routinen): _strecke_ends, _closest_pt_on_segment, _distinct_kreisel_ids (nur von den entfernten Funktionen genutzt), sowie der temporaere Diagnose-Filter DEBUG_HIGHLIGHT_TEILEIDS/dbg_highlight (an die jetzt behobene Koordinaten-Matching-Ambiguitaet gebunden, in seinem eigenen Kommentar bereits als Wegwerf-Code markiert). LISP-seitiger Dead-Code-Scan (export.lsp, ssg_ks_insert.lsp - die diese Woche bearbeiteten Module): keine Funde. Verifiziert an den echten HundM05-Daten (results/export_raw.json, 45 Bloecke): TeileId-Bereich luecken- und duplikatfrei 1-45 (vorher 1-55 mit Luecke 24-33 + Zusatz-IDs 46-55), alle 10 AS/ES-IDs korrekt in ihrer Strecke UND im Kreisel 06 TrackIds, keine Warnungen der neuen Pruefung. Zwei Testdateien auf die neue Architektur umgestellt (test_export_kreisel_umlauf.py: AS/ES als Item statt Kollisions-Erzeugung, neue Klasse TestAsEsImKreiselUmlauf fuer die BBox- Teilnahme; test_export_separatorliste.py: id-basierte Zuordnung, Klasse TestMarkerIdWiederverwendung durch TestMapSeplisteKetteItemsFallback ersetzt - die alte testete inzwischen entfernte Mint-Logik). Volle Suite: 224 bestanden (4 vorbestehende, unabhaengige Fixture-Failures unveraendert). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
719ffc2385
commit
ab3958e0e4
+87
-67
@@ -149,7 +149,7 @@ Konzept zur sepliste — es sind dieselben physischen Symbole, nur aus zwei
|
||||
Blickwinkeln: `csv:sep-proxies-erzeugen` liefert sie als echte, exportierbare
|
||||
INSERT-Kopie (mit Bounding-Box und ID); die sepliste liefert dieselben
|
||||
Separatoren als Eintraege mit Vorgaenger-/Nachfolger-Wissen. Python fuehrt
|
||||
beides zusammen (siehe `map_separator_kette_items` unten) statt sie doppelt
|
||||
beides zusammen (siehe `map_sepliste_kette_items` unten) statt sie doppelt
|
||||
zu zaehlen.
|
||||
|
||||
### Das Feld `id` je sepliste-Eintrag
|
||||
@@ -161,13 +161,15 @@ realen Bauteils, das ihn in der Zeichnung vertritt:
|
||||
{"typ": "AS", "lfdnr": 1, "x": 5070.1, "y": 1199.2, "id": "0031"}
|
||||
```
|
||||
|
||||
Das ist dieselbe ID, die die zugehoerige temporaere Kopie in ihrer eigenen
|
||||
JSON-Zeile traegt — die Verbindung "Kettenglied <-> reales Teil" steht damit
|
||||
schon im Export und muss in Python nicht mehr ueber Koordinaten-Naehe geraten
|
||||
werden (`map_separator_kette_items`, siehe unten; solange Python das Feld noch
|
||||
nicht liest, aendert sich dort nichts). Das Feld fehlt, wenn keine Kopie
|
||||
erzeugt werden konnte oder deren ID-Attribut leer blieb — Altbestand und
|
||||
Fehlerfaelle verhalten sich damit wie bisher.
|
||||
Das ist dieselbe ID, die die zugehoerige temporaere Kopie (SEP) bzw. der
|
||||
dauerhafte `ZZ_ASES_MARKER` (AS/ES) in ihrer eigenen JSON-Zeile traegt — die
|
||||
Verbindung "Kettenglied <-> reales Teil" steht damit schon im Export und muss
|
||||
in Python nicht mehr ueber Koordinaten-Naehe geraten werden. `map_sepliste_
|
||||
kette_items` (siehe Abschnitt 5 unten) loest genau darueber jeden sepliste-
|
||||
Eintrag exakt per ID-Gleichheit auf. Das Feld fehlt, wenn keine Kopie erzeugt
|
||||
werden konnte oder deren ID-Attribut leer blieb — fuer SEP faellt das auf den
|
||||
alten Koordinaten-Abgleich zurueck (Altbestand), fuer AS/ES bleibt der
|
||||
Eintrag dann unaufgeloest (siehe Abschnitt 3 unten).
|
||||
|
||||
### Der AS/ES-Marker (`ZZ_ASES_MARKER`)
|
||||
|
||||
@@ -358,10 +360,11 @@ Zu beachten:
|
||||
- `VF_1` (`0021`) bekommt seine eigene `TrackIds`-Spalte
|
||||
(`write_separatorliste_merkmale`) aus der `sepliste`: AS -> SEP -> SEP -> ES
|
||||
in Baureihenfolge, unabhaengig vom Kreisel-Umlauf.
|
||||
- Die AS/ES-Enden (`0021`=AS, `0024`=ES) sind hier synthetische Zeilen aus
|
||||
`build_strecke_schleus_items` (Zeilentyp "ILS Ausschleuselement"/
|
||||
"ILS Einschleuselement") — in diesem gekuerzten Beispiel nicht einzeln
|
||||
aufgefuehrt, aber ihre TeileIds tauchen in `TrackIds` beider Seiten auf.
|
||||
- Die AS/ES-Enden (`0021`=AS, `0024`=ES) sind eigene Zeilen (Zeilentyp
|
||||
"ILS Ausschleuselement"/"ILS Einschleuselement"), direkt aus dem
|
||||
dauerhaften `ZZ_ASES_MARKER` klassifiziert — in diesem gekuerzten Beispiel
|
||||
nicht einzeln aufgefuehrt, aber ihre TeileIds tauchen in `TrackIds` beider
|
||||
Seiten auf.
|
||||
- Die Separatoren 3 und 5 (`0012`/`0022`) unterscheiden sich nur durch ihre
|
||||
`Zuordnung`: `0012` haengt am Kreisel, `0022`/`0023` an der VF-Kette — beide
|
||||
Faelle laufen ueber denselben `pattern_separator`-Blocktyp.
|
||||
@@ -435,39 +438,46 @@ Partner", je nach Mindestanzahl Partner pro TeileArt).
|
||||
|
||||
### 3. Synthetische Zeilen an den Uebergangsstellen
|
||||
|
||||
Drei Arten von "Zeilen ohne eigene Zeichnungsgeometrie" werden aus der
|
||||
Geometrie der bereits klassifizierten Items abgeleitet:
|
||||
Nur noch EINE Art von "Zeile ohne eigene Zeichnungsgeometrie" wird aus der
|
||||
Geometrie bereits klassifizierter Items abgeleitet:
|
||||
|
||||
- **`compute_kreisel_touch_switches`** — praezise Kapsel-Geometrie (nicht nur
|
||||
Bounding-Box) findet jeden Punkt, an dem sich zwei echte Kreisel wirklich
|
||||
beruehren, und erzeugt dort eine `"ILS Weiche"`-Zeile
|
||||
(`build_kreisel_weiche_items`). Das ist der Verzweigungspunkt, an dem
|
||||
Material von einem Kreisel zum anderen wechseln kann.
|
||||
- **`compute_strecke_kreisel_schleus`** — an jedem Streckenende (AS/ES,
|
||||
ueber K1/K2 bzw. als Fallback die Wrapper-Enden), das einen Kreisel
|
||||
beruehrt (siehe Schritt 2), entsteht ein `"ILS Ausschleuselement"`
|
||||
(Anfang) bzw. `"ILS Einschleuselement"` (Ende), leicht zum Kreisel hin
|
||||
versetzt (`build_strecke_schleus_items`).
|
||||
- **`map_separator_kette_items`** — ordnet jedem `"SEP"`-Eintrag einer
|
||||
sepliste das dazu passende, bereits vorhandene Separator-Item zu (siehe
|
||||
naechster Abschnitt). Erzeugt bewusst KEINE eigenen Zeilen.
|
||||
|
||||
AS/ES-Kettenenden werden NICHT mehr synthetisiert (fruehere Version:
|
||||
`compute_strecke_kreisel_schleus`/`build_strecke_schleus_items` suchten den
|
||||
anschliessenden Kreisel per Kollisionstest und erfanden dabei eine eigene,
|
||||
bei jedem Export neue TeileId). Der dauerhafte `ZZ_ASES_MARKER`
|
||||
(Lisp/export.lsp, siehe oben) ist ein echter Block mit echter Geometrie -
|
||||
`"ILS Ausschleuselement"`/`"ILS Einschleuselement"` sind darum eine ganz
|
||||
normale Zeile aus Schritt 1 (Klassifikation), TeileId/Position/Boundingbox
|
||||
direkt vom Marker-Block selbst.
|
||||
|
||||
`map_sepliste_kette_items` (siehe Abschnitt 5 unten) ordnet danach jedem
|
||||
sepliste-Eintrag (AS/SEP/ES) das dazu passende, bereits vorhandene Item zu.
|
||||
Erzeugt bewusst KEINE eigenen Zeilen.
|
||||
|
||||
### 4. Der Kreisel-Umlauf — wie Nachbarn ueber einen Kreisel hinweg entstehen
|
||||
|
||||
`compute_kreisel_umlauf` ist der zentrale Baustein, der Ketten-Enden,
|
||||
freie Punkte (Separatoren UND BTMT Beladung/Entladung) und Kreisel-Kreisel-
|
||||
`compute_kreisel_umlauf` ist der zentrale Baustein, der freie Punkte
|
||||
(Separatoren, BTMT Beladung/Entladung, AS/ES-Enden) und Kreisel-Kreisel-
|
||||
Weichen zu einer widerspruchsfreien Nachbarschaft zusammenfuehrt. Fuer jeden
|
||||
echten Kreisel werden alle Punkte gesammelt, die dort geometrisch liegen:
|
||||
|
||||
- freie Punkte (`KREISEL_UMLAUF_TEILEARTEN` in `lib/export_neighbors.py`:
|
||||
Separator, BTMT Beladung, BTMT Entladung, Eingang, Ausgang — alles
|
||||
Stationen/Markierungen direkt auf der Bahn/dem Track), deren Bounding-Box
|
||||
die Kreisel-Box ueberschneidet,
|
||||
Separator, BTMT Beladung, BTMT Entladung, Eingang, Ausgang UND die AS-/ES-
|
||||
Ein-/Ausschleuselemente — alles Stationen/Markierungen direkt auf der
|
||||
Bahn/dem Track mit eigener Bounding-Box), deren Bounding-Box die Kreisel-
|
||||
Box ueberschneidet. Ein AS/ES bringt seine Box vom eigenen `ZZ_ASES_MARKER`
|
||||
mit — keine gesonderte Naeherungslogik mehr noetig (fruehere Version
|
||||
suchte hierfuer je AS/ES-sepliste-Eintrag den geometrisch naechstgelegenen
|
||||
Kreisel, unabhaengig davon ob er wirklich in Reichweite lag),
|
||||
- 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),
|
||||
- die AS/ES-Enden andockender VF_n/GF_n-Ketten (aus der `sepliste`,
|
||||
zugeordnet zum geometrisch naechstgelegenen Kreisel).
|
||||
man kann von Kreisel A nach B und zurueck).
|
||||
|
||||
Alle Punkte werden nach Winkel um die Kreiselachse sortiert (Richtung ueber
|
||||
das `DREHRICHTUNG`-Attribut, UZS/GUZ) — das ergibt eine geschlossene,
|
||||
@@ -484,32 +494,31 @@ echte Nachbarn.
|
||||
|
||||
`compute_sep_kette` liest jede `sepliste` und baut daraus einen Schluessel
|
||||
je Eintrag (`"<strecke_id>#<lfdnr>"`) mit `vorgaenger`/`nachfolger`
|
||||
(wieder solche Schluessel, `None` am Kettenende). Das ist eine reine
|
||||
Uebersetzung der LISP-Baureihenfolge — kein Rateverfahren.
|
||||
(wieder solche Schluessel, `None` am Kettenende) sowie `id` (die TeileId des
|
||||
vertretenden Items, siehe oben). Das ist eine reine Uebersetzung der
|
||||
LISP-Baureihenfolge — kein Rateverfahren.
|
||||
|
||||
**Warum die Zeilen erst NACH allen Schluesseln aufgeloest werden:** ein
|
||||
Schluessel kann auf eine Zeile zeigen, die zum Zeitpunkt seiner Vergabe noch
|
||||
gar nicht existiert (eine SEP in der Mitte einer Kette hat ihre AS als
|
||||
Vorgaenger, aber die AS-Zeile entsteht erst in `build_strecke_schleus_items`,
|
||||
das VOR der SEP-Zuordnung laeuft). Darum sammeln
|
||||
`build_strecke_schleus_items` und `map_separator_kette_items` ihre Treffer
|
||||
zunaechst nur in einem gemeinsamen `sep_kette_by_key`-dict
|
||||
(Schluessel -> Item), OHNE die `Nachbarn`-Spalte zu setzen.
|
||||
`resolve_umlauf_und_sepliste_nachbarn` loest danach in einem einzigen Schritt
|
||||
sowohl die Kreisel-Umlauf-Punkte als auch die sepliste-Schluessel gegen einen
|
||||
gemeinsamen Lookup (freie Separatoren nach TeileId, Weichen nach Schluessel,
|
||||
sepliste-Zeilen nach Schluessel) zu echten TeileIds auf.
|
||||
**`map_sepliste_kette_items`** loest jeden sepliste-Schluessel auf sein
|
||||
bereits vorhandenes Item auf — EXAKT per ID-Gleichheit (Feld `id` gegen die
|
||||
`teileid` der klassifizierten Items), nicht mehr per Koordinaten-Naehe:
|
||||
AS/ES sind eigene Items (Schritt 3 entfaellt fuer sie), SEP ein bereits
|
||||
klassifiziertes Separator-Item (der verpackte Separator liegt durch
|
||||
`csv:sep-proxies-erzeugen` bereits als echtes Item vor — eine zweite, aus
|
||||
der sepliste erzeugte Zeile waere derselbe physische Separator ein zweites
|
||||
Mal). Nur wenn das `id`-Feld fehlt (Altbestand-Export ohne Marker/Kopie-ID),
|
||||
faellt SEP auf den alten Abgleich zurueck: Wrapper-ID (Item-Merkmal
|
||||
`"Zuordnung"`) plus naechstgelegener rohen INSERT-Punkt (Item-Felder
|
||||
`_x`/`_y` vs. sepliste-Koordinate). AS/ES ohne `id` bleiben unaufgeloest -
|
||||
fuer sie existiert kein Item mehr, das sie ersatzweise vertreten koennte.
|
||||
|
||||
**`map_separator_kette_items` erzeugt keine neuen Zeilen** (Architektur-
|
||||
entscheidung, siehe Docstring in `lib/export_csv.py`): der verpackte
|
||||
Separator liegt durch `csv:sep-proxies-erzeugen` (Phase 1, Schritt 1) bereits
|
||||
als echtes Separator-Item vor. Eine zweite, aus der sepliste erzeugte Zeile
|
||||
waere derselbe physische Separator ein zweites Mal. Die Zuordnung
|
||||
sepliste-Eintrag -> vorhandenes Item laeuft ueber die Wrapper-ID (Item-
|
||||
Merkmal `"Zuordnung"`, von `csv:sep-proxies-zuordnung-setzen` fest gesetzt)
|
||||
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.
|
||||
Die Treffer landen zunaechst nur in `sep_kette_by_key` (Schluessel -> Item),
|
||||
OHNE die `Nachbarn`-Spalte zu setzen: ein Schluessel kann auf eine Zeile
|
||||
zeigen, deren eigener Nachbar (Vorgaenger/Nachfolger) zu diesem Zeitpunkt
|
||||
selbst noch nicht aufgeloest ist. `resolve_umlauf_und_sepliste_nachbarn`
|
||||
loest danach in einem einzigen Schritt sowohl die Kreisel-Umlauf-Punkte als
|
||||
auch die sepliste-Schluessel gegen einen gemeinsamen Lookup (freie Punkte
|
||||
nach TeileId, Weichen nach Schluessel, sepliste-Zeilen nach Schluessel) zu
|
||||
echten TeileIds auf.
|
||||
|
||||
### Die Spalte `"TrackIds"` — Reihenfolge als eigene CSV-Spalte
|
||||
|
||||
@@ -517,17 +526,28 @@ Zusaetzlich zur `"Nachbarn"`-Spalte (Vorgaenger/Nachfolger je Zeile) traegt
|
||||
`write_separatorliste_merkmale` (`lib/export_csv.py`) jeder VF_n/GF_n-Zeile
|
||||
die eigene Spalte `"TrackIds"` (`item["trackids"]`, Top-Level-Feld wie
|
||||
`"nachbarn"` — KEIN Merkmal) ein: die TeileIds von AS-Ende, Separatoren UND
|
||||
ES-Ende in Baureihenfolge, kommagetrennt (z.B. `"0051, 0052, 0053, 0054,
|
||||
0055"` fuer AS, zwei SEP, ES). AS/ES zaehlen mit, weil sie beim Kreisel
|
||||
(siehe unten) ebenfalls Teil von dessen `TrackIds` sind — ohne sie fehlten
|
||||
hier die Kettenenden. Quelle ist die rohe `sepliste` des Items (nicht
|
||||
`sep_kette`, dessen dict-Reihenfolge nicht garantiert ist). Muss NACH
|
||||
`map_separator_kette_items`/`build_strecke_schleus_items` laufen, weil erst
|
||||
dann jeder sepliste-Schluessel (AS/SEP/ES) auf sein reales Item (TeileId)
|
||||
aufgeloest ist. Beruehrt ein Streckenende geometrisch KEINEN Kreisel,
|
||||
entsteht dort keine AS/ES-Zeile (`build_strecke_schleus_items`) und damit
|
||||
auch keine TeileId — dieses Ende fehlt dann in `TrackIds` (kein Fehler,
|
||||
sondern Folge der fehlenden Kollision).
|
||||
ES-Ende in Baureihenfolge, kommagetrennt (z.B. `"0056, 0052, 0053, 0057"`
|
||||
fuer AS, zwei SEP, ES). AS/ES zaehlen mit, weil sie beim Kreisel (siehe
|
||||
unten) ebenfalls Teil von dessen `TrackIds` sind — ohne sie fehlten hier die
|
||||
Kettenenden. Quelle ist die rohe `sepliste` des Items (nicht `sep_kette`,
|
||||
dessen dict-Reihenfolge nicht garantiert ist). Muss NACH
|
||||
`map_sepliste_kette_items` laufen, weil erst dann jeder sepliste-Schluessel
|
||||
(AS/SEP/ES) auf sein reales Item (TeileId) aufgeloest ist. Fehlt einem
|
||||
Eintrag die TeileId (kein `id`-Feld und - nur bei SEP - kein Koordinaten-
|
||||
Fallback-Treffer), fehlt dieses Kettenglied dann in `TrackIds` (kein Fehler,
|
||||
sondern Folge der fehlenden Zuordnung).
|
||||
|
||||
### Konsistenzpruefung: AS/ES in beiden Containern
|
||||
|
||||
`pruefe_ases_in_beiden_containern` (`lib/export_csv.py`, laeuft am Ende von
|
||||
`process_blocks`, NACH beiden `TrackIds`-Spalten) prueft fuer jede AS-/ES-
|
||||
Zeile, ob ihre TeileId sowohl in den `TrackIds` ihrer EIGENEN Strecke
|
||||
(ZUORDNUNG-Attribut des Markers) als auch in den `TrackIds` MINDESTENS eines
|
||||
Kreisels auftaucht — beide "Container", zu denen ein AS/ES logisch gehoert.
|
||||
Fehlt einer von beiden, ist das kein Normalfall (z.B. ZUORDNUNG leer/falsch,
|
||||
Kollisionstoleranz zu knapp, Kreisel zu weit weg, fehlende Bounding-Box am
|
||||
Marker): die Zeile bekommt eine Eintragung in der Spalte `"Warnungen"`
|
||||
(`item["warnung"]`) UND eine Konsolen-/Debuglog-Meldung.
|
||||
|
||||
`write_kreisel_separatorliste_merkmale` ueberträgt dasselbe Prinzip auf
|
||||
echte Kreisel (`"ILS 2.0 Kreisel"`): Quelle ist hier NICHT die sepliste
|
||||
@@ -544,8 +564,8 @@ 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 in `item["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.
|
||||
Zeilen-Registrierungen: `build_kreisel_weiche_items`,
|
||||
`map_sepliste_kette_items`) laufen.
|
||||
|
||||
BTMT Beladung/Entladung sind bewusst NICHT in `SENSOR_TEILEARTEN`
|
||||
aufgenommen (die bleibt Separator/Scanner vorbehalten, siehe
|
||||
|
||||
+248
-298
@@ -41,14 +41,15 @@ from export_neighbors import (
|
||||
compute_sensor_zuordnung,
|
||||
compute_scanner_nearest_separator,
|
||||
compute_kreisel_touch_switches,
|
||||
compute_strecke_kreisel_schleus,
|
||||
compute_sep_kette,
|
||||
compute_kreisel_umlauf,
|
||||
dbg_highlight,
|
||||
SENSOR_TEILEARTEN,
|
||||
WEICHE_TEILEART,
|
||||
GF_VF_TEILEARTEN,
|
||||
KREISEL_SPLIT_TEILEART,
|
||||
KREISEL_UMLAUF_TEILEARTEN,
|
||||
AUSSCHLEUS_TEILEART,
|
||||
EINSCHLEUS_TEILEART,
|
||||
)
|
||||
|
||||
BLOCKPATTERNS = load_patterns()
|
||||
@@ -534,174 +535,44 @@ def _strecke_typ_feld(teileart):
|
||||
return "Strecke" if teileart == "ILS 2.0 Gefaellestrecke" else "Förderer"
|
||||
|
||||
|
||||
def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette=None,
|
||||
sep_kette_by_key=None, dbg=None):
|
||||
"""Haengt fuer jedes Strecken-Ende mit anschliessendem Kreisel eine
|
||||
"ILS Ausschleuselement"- (Anfang) bzw. "ILS Einschleuselement"-Zeile
|
||||
(Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus
|
||||
fuer Position/Boundingbox - reine Geometrie, unveraendert).
|
||||
def map_sepliste_kette_items(items, sep_kette, dbg=None):
|
||||
"""Ordnet jeden sepliste-Eintrag (AS/SEP/ES, siehe compute_sep_kette)
|
||||
einem bereits klassifizierten Item zu und liefert sie unter ihrem
|
||||
sep_kette-Schluessel zurueck - fuer die Nachbar-Aufloesung in
|
||||
resolve_umlauf_und_sepliste_nachbarn sowie die TrackIds-Spalte der
|
||||
besitzenden Strecke (siehe write_separatorliste_merkmale).
|
||||
|
||||
Muss NACH build_kreisel_weiche_items laufen, damit die TeileId-Zaehlung
|
||||
(max_numeric_teileid) auch die bereits erzeugten Weichen einschliesst.
|
||||
Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische, an der
|
||||
Kreiselachse ausgerichtete Default-Box.
|
||||
ERZEUGT KEINE neuen Zeilen: AS/ES liegen bereits als eigene Items vor
|
||||
(direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert, siehe
|
||||
process_blocks), SEP als die beim Export ausgepackte temporaere
|
||||
Separator_SP-Kopie (csv:sep-proxies-erzeugen, export.lsp). Beide Wege
|
||||
wuerden sonst dieselben physischen Bauteile DOPPELT zaehlen.
|
||||
|
||||
sep_kette = Rueckgabe von export_neighbors.compute_sep_kette (keys
|
||||
"<strecke_id>#<lfdnr>" -> Eintrag mit typ/vorgaenger/nachfolger). Traegt
|
||||
die zugehoerige Strecke eine "sepliste" (neue LISP-XDATA, siehe
|
||||
ssg-sepliste-xdata-schreiben), bleibt "Nachbarn" hier zunaechst LEER und
|
||||
der sep_kette-Schluessel dieser Zeile wird stattdessen in
|
||||
sep_kette_by_key (dict, vom Aufrufer bereitgestellt und HIER befuellt)
|
||||
eingetragen - process_blocks loest daraus NACH der Registrierung ALLER
|
||||
SEP/AS/ES-Schluessel (auch der von map_separator_kette_items) die
|
||||
tatsaechlichen TeileIds auf (eigener Block am Ende von process_blocks,
|
||||
direkt nach beiden Aufrufen). Ein sep_kette-Schluessel ist naemlich KEINE TeileId,
|
||||
sondern nur waehrend des Baus eindeutig -
|
||||
ihn direkt in die CSV zu schreiben waere ein Bug (zeigte frueher auf
|
||||
"<strecke_id>#<lfdnr>" statt auf den eigentlichen Nachbarn). Die
|
||||
Kreisel-/Foerderer-Zugehoerigkeit steht bei sepliste-Zeilen stattdessen
|
||||
in den Merkmalen ("Kreisel" + "Förderer"/"Strecke" je Typ, analog zum
|
||||
"Zuordnung"-Feld der Separatoren). Fehlt die sepliste (Altbestand-Wrapper
|
||||
ohne die neue XDATA), bleibt der bisherige Nachbarn-Inhalt
|
||||
[kreisel_id, strecke_id] unveraendert erhalten (sofort aufgeloest, kein
|
||||
sep_kette-Schluessel involviert).
|
||||
Zuordnung: EXAKTER ID-Abgleich. Seit csv:sepliste-ids-setzen/
|
||||
csv:asmark-ids-setzen (Lisp/export.lsp) traegt praktisch jeder sepliste-
|
||||
Eintrag bereits die TeileId des ihn vertretenden Items als Feld "id" -
|
||||
das Item mit GENAU dieser teileid IST der gesuchte Partner. Kein Rate-/
|
||||
Naeheverfahren mehr noetig, keine der frueheren Ambiguitaeten (ein
|
||||
einzelner fehlerhafter sepliste-Koordinatenwert konnte vorher per
|
||||
Naeherungssuche einen FALSCHEN Nachbarn waehlen und einen anderen
|
||||
Separator ganz ohne Zuordnung lassen - siehe Memory
|
||||
sepliste-lfdnr2-koordinaten-bug.md).
|
||||
|
||||
Rueckgabe: fortgeschriebene Elementnummer.
|
||||
Fallback (kein "id"-Feld - Altbestand-Export ohne Marker/Kopie-ID, oder
|
||||
deren Zuweisung ist in Lisp fehlgeschlagen): NUR fuer SEP-Eintraege
|
||||
weiterhin per naechstem INSERT-Punkt gegen die per Zuordnung
|
||||
vorgefilterten Separator-Items. AS/ES ohne "id" bleiben unaufgeloest -
|
||||
fuer sie existiert seit dem Wegfall der Kreisel<->Strecke-Kollisions-
|
||||
erzeugung (frueher compute_strecke_kreisel_schleus/
|
||||
build_strecke_schleus_items) ohnehin kein Item mehr, das sie vertreten
|
||||
koennte.
|
||||
|
||||
Rueckgabe: sep_kette_by_key (neues dict).
|
||||
"""
|
||||
if sep_kette is None:
|
||||
sep_kette = {}
|
||||
if sep_kette_by_key is None:
|
||||
sep_kette_by_key = {}
|
||||
schleus, warnings = compute_strecke_kreisel_schleus(
|
||||
items, durchmesser_mm=KREISEL_DURCHMESSER_MM,
|
||||
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
|
||||
sep_kette_by_key = {}
|
||||
by_teileid = {it.get("teileid", ""): it for it in items if it.get("teileid", "")}
|
||||
|
||||
strecke_by_id = {it.get("teileid", ""): it for it in items
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN}
|
||||
|
||||
# Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile
|
||||
# schreiben (Spalte "Warnungen") und in die Konsole ausgeben.
|
||||
if warnings:
|
||||
for sid, text in warnings.items():
|
||||
if sid in strecke_by_id:
|
||||
strecke_by_id[sid]["warnung"] = text
|
||||
print(f"[export_csv] WARNUNG: Strecke {sid} {text}")
|
||||
|
||||
naechste_id = max_numeric_teileid(items)
|
||||
elem_nr = start_elem_nr
|
||||
zaehler = {}
|
||||
for sp in schleus:
|
||||
elem_nr += 1
|
||||
teileart = sp["teileart"]
|
||||
zaehler[teileart] = zaehler.get(teileart, 0) + 1
|
||||
planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg)
|
||||
if planquadrat_cfg is not None else "")
|
||||
|
||||
strecke = strecke_by_id.get(sp["strecke_id"])
|
||||
sepliste = strecke.get("sepliste") if strecke else None
|
||||
merkmale = {"Erzeugt aus": "Kreisel-Strecke-Kollision"}
|
||||
sep_kette_key = None
|
||||
marker_id = ""
|
||||
if sepliste:
|
||||
# Neue XDATA vorhanden: Nachbarn nur noch aus der Ketten-internen
|
||||
# Vorgaenger/Nachfolger-Beziehung (AS/ES ist immer das erste bzw.
|
||||
# letzte Sepliste-Element seiner Strecke). Die Keys (vorgaenger/
|
||||
# nachfolger) sind sep_kette-interne Schluessel, KEINE fertigen
|
||||
# TeileIds - erst nach dem Bau ALLER SEP/AS/ES-Zeilen dieser Kette
|
||||
# aufloesbar (siehe process_blocks, Abschnitt nach beiden Aufrufen,
|
||||
# analog zum Kreisel-Umlauf/Weichen-Muster).
|
||||
sepliste_ende = sepliste[0] if sp["ende_label"] == "Anfang" else sepliste[-1]
|
||||
lfdnr = sepliste_ende["lfdnr"]
|
||||
# ZZ_ASES_MARKER (csv:asmark-aktualisieren, Lisp/export.lsp) traegt
|
||||
# bereits eine STABILE, ueber Exportlaeufe hinweg gleichbleibende
|
||||
# ID fuer genau dieses Kettenende (schon 4-stellig formatiert,
|
||||
# ssg-id-format) - im sepliste-Eintrag als Feld "id" gespiegelt.
|
||||
# Diese ID unten WIEDERVERWENDEN statt eine neue zu minten: der
|
||||
# Marker bekommt in process_blocks bewusst KEINE eigene CSV-Zeile
|
||||
# (siehe pattern_ases_marker), seine ID bliebe sonst fuer immer
|
||||
# ungenutzt, waehrend hier bei jedem Exportlauf zusaetzlich eine
|
||||
# neue, hoehere ID fuer denselben physischen Punkt erfunden wuerde.
|
||||
marker_id = str(sepliste_ende.get("id", "")).strip()
|
||||
sep_kette_key = f"{sp['strecke_id']}#{lfdnr}"
|
||||
nachbarn = [] # in process_blocks nachgetragen
|
||||
merkmale["Kreisel"] = sp["kreisel_id"]
|
||||
merkmale[_strecke_typ_feld(strecke.get("teileart", ""))] = sp["strecke_id"]
|
||||
else:
|
||||
nachbarn = [sp["kreisel_id"], sp["strecke_id"]]
|
||||
merkmale["Kreisel"] = sp["kreisel_id"]
|
||||
merkmale["Strecke"] = sp["strecke_id"]
|
||||
merkmale["Streckenende"] = sp["ende_label"]
|
||||
|
||||
if marker_id:
|
||||
# Marker-ID uebernehmen (schon 4-stellig formatiert - NICHT
|
||||
# erneut formatieren) und den laufenden Zaehler ggf. hochziehen,
|
||||
# damit ein SPAETERER Mint in dieser Schleife (Altbestand-Wrapper
|
||||
# ohne Marker/sepliste) nie mit einer bereits wiederverwendeten
|
||||
# Marker-ID kollidiert - unabhaengig davon, in welcher Reihenfolge
|
||||
# Lisp Marker- vs. Separator-Proxy-IDs vergeben hat.
|
||||
teileid = marker_id
|
||||
if marker_id.isdigit():
|
||||
naechste_id = max(naechste_id, int(marker_id))
|
||||
else:
|
||||
# Altbestand-Wrapper ohne Marker-ID (oder Marker-ID-Zuweisung in
|
||||
# Lisp fehlgeschlagen) - unveraendertes bisheriges Verhalten.
|
||||
naechste_id += 1
|
||||
teileid = f"{naechste_id:04d}"
|
||||
|
||||
item = {
|
||||
"nr": elem_nr,
|
||||
"teileart": teileart,
|
||||
"teileid": teileid,
|
||||
"bezeichnung": f"{teileart} :{zaehler[teileart]}",
|
||||
"planquadrat": planquadrat,
|
||||
"position": f'{sp["x"]:.2f}, {sp["y"]:.2f}, {sp["z"]:.2f}',
|
||||
"boundingbox": f'{sp["dx"]:.2f}, {sp["dy"]:.2f}, {sp["dz"]:.2f}',
|
||||
"insertpoint": "",
|
||||
"k1": "", "k2": "", "k3": "", "k4": "",
|
||||
"nachbarn": ", ".join(nachbarn),
|
||||
"fehler": "",
|
||||
"merkmale": merkmale,
|
||||
}
|
||||
items.append(item)
|
||||
if sep_kette_key is not None:
|
||||
sep_kette_by_key[sep_kette_key] = item
|
||||
return elem_nr
|
||||
|
||||
|
||||
def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None):
|
||||
"""Ordnet jeden in einer VF_n/GF_n-Kette eingebetteten Separator
|
||||
(sepliste-Eintrag typ "SEP", siehe compute_sep_kette) dem BEREITS
|
||||
vorhandenen Separator-Item zu und traegt es unter seinem sep_kette-
|
||||
Schluessel in sep_kette_by_key ein - fuer die Nachbar-Aufloesung in
|
||||
resolve_umlauf_und_sepliste_nachbarn.
|
||||
|
||||
WICHTIG - erzeugt KEINE neuen Zeilen (frueher tat das
|
||||
build_separator_kette_items): die in einer Kette verpackten Separatoren
|
||||
liegen bereits als ECHTE Separator_SP-INSERTs in der Zeichnung - der
|
||||
LISP-Export fuegt fuer jedes verpackte Symbol eine temporaere Kopie ein
|
||||
(csv:sep-proxies-erzeugen, export.lsp), die den normalen freien-Separator-
|
||||
Pfad in process_blocks durchlaeuft (mit echter vla-getboundingbox-Box, ID
|
||||
und fest gesetzter ZUORDNUNG = Wrapper-ID ueber "zuordnung_fix"). Beide
|
||||
Wege wuerden sonst dieselben physischen Separatoren DOPPELT zaehlen (26
|
||||
Proxy-INSERTs + 26 sepliste-Zeilen = 64 statt 38). Die sepliste liefert
|
||||
hier nur noch die REIHENFOLGE (Vorgaenger/Nachfolger), nicht die Zeilen.
|
||||
|
||||
Zuordnung sepliste-SEP -> vorhandenes Separator-Item: ueber die
|
||||
Wrapper-ID (Item-Merkmal "Zuordnung" == sep_kette-Eintrag "strecke_id",
|
||||
fixiert durch den LISP-Proxy) UND den naechstgelegenen INSERT-Punkt
|
||||
(Item-Felder "_x"/"_y" vs. sepliste-Koordinate x/y - beide sind der rohe
|
||||
Sub-INSERT-Einfuegepunkt aus derselben Bauphase, daher deckungsgleich;
|
||||
verifiziert 26/26 <300mm). Reicht der Wrapper mehrere Separatoren, trennt
|
||||
der INSERT-Punkt sie; bei nur einem ist die Wrapper-ID schon eindeutig.
|
||||
|
||||
Ein sepliste-SEP ohne passendes Item (sollte nicht vorkommen, da der Proxy
|
||||
jeden verpackten Separator einfuegt) wird uebersprungen (dbg-Hinweis).
|
||||
"""
|
||||
if sep_kette_by_key is None:
|
||||
sep_kette_by_key = {}
|
||||
# Vorhandene Separator-Items nach Wrapper-Zuordnung gruppieren (nur die mit
|
||||
# fixierter Zuordnung = die Proxy-Kopien verpackter Ketten-Separatoren;
|
||||
# frei platzierte Separatoren tragen keine sepliste und stoeren nicht).
|
||||
# Kandidatenpool nur noch fuer den Koordinaten-Fallback (SEP ohne "id").
|
||||
sep_items_by_zuordnung = {}
|
||||
for it in items:
|
||||
if it.get("teileart", "") != "ILS 2.0 Separator":
|
||||
@@ -711,96 +582,68 @@ def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None)
|
||||
sep_items_by_zuordnung.setdefault(zuordnung, []).append(it)
|
||||
|
||||
if dbg:
|
||||
dbg("=== map_separator_kette_items: Kandidatenpools je Zuordnung ===")
|
||||
for zuordnung, kandidaten in sep_items_by_zuordnung.items():
|
||||
dbg(f" Zuordnung {zuordnung!r}: "
|
||||
f"{[k.get('teileid', '') for k in kandidaten]}")
|
||||
dbg("")
|
||||
dbg("=== map_separator_kette_items: sepliste-SEP -> Separator-Item ===")
|
||||
|
||||
# id(item) -> Liste der sep_kette-Keys, die genau dieses Item als
|
||||
# naechsten Kandidaten gewaehlt haben - Diagnose der NICHT-exklusiven
|
||||
# Zuordnung (jeder Key sucht unabhaengig im vollen Pool, siehe Docstring):
|
||||
# waehlen zwei Keys denselben Kandidaten, bleibt ein anderer Kandidat im
|
||||
# selben Pool ohne jeden sep_kette-Eintrag und damit ohne Nachbarn/TrackIds.
|
||||
gewaehlt_von = {}
|
||||
# Zuordnungen, fuer die es ueberhaupt sepliste-SEP-Schluessel gibt. Nur in
|
||||
# DIESEN Pools ist ein nicht gewaehlter Kandidat auffaellig; frei auf einem
|
||||
# Kreisel platzierte Separatoren tragen keine sepliste und bekommen ihre
|
||||
# Nachbarn aus dem Kreisel-Umlauf (siehe resolve_umlauf_und_sepliste_nachbarn).
|
||||
zuordnungen_mit_sepliste = set()
|
||||
dbg("=== map_sepliste_kette_items: sepliste-Eintrag -> Item ===")
|
||||
|
||||
per_id = 0
|
||||
per_fallback = 0
|
||||
ohne_treffer = 0
|
||||
for key, eintrag in sep_kette.items():
|
||||
marker_id = eintrag.get("id", "")
|
||||
if marker_id:
|
||||
item = by_teileid.get(marker_id)
|
||||
if item is not None:
|
||||
sep_kette_by_key[key] = item
|
||||
per_id += 1
|
||||
if dbg:
|
||||
dbg(f" {key} (typ={eintrag.get('typ')!r}) -> Item per ID "
|
||||
f"{marker_id!r} (TeileId {item.get('teileid', '')!r})")
|
||||
continue
|
||||
if dbg:
|
||||
dbg(f" {key}: id={marker_id!r} gesetzt, aber KEIN Item mit "
|
||||
f"dieser TeileId gefunden")
|
||||
|
||||
# Fallback nur fuer SEP ohne (aufloesbares) "id"-Feld (Altbestand).
|
||||
if eintrag.get("typ") != "SEP":
|
||||
ohne_treffer += 1
|
||||
continue
|
||||
zuordnungen_mit_sepliste.add(eintrag["strecke_id"])
|
||||
kandidaten = sep_items_by_zuordnung.get(eintrag["strecke_id"], [])
|
||||
if not kandidaten:
|
||||
ohne_treffer += 1
|
||||
if dbg:
|
||||
dbg(f" sepliste-SEP {key}: kein Separator-Item mit Zuordnung "
|
||||
dbg(f" {key}: kein Separator-Item mit Zuordnung "
|
||||
f"{eintrag['strecke_id']!r} gefunden - uebersprungen")
|
||||
continue
|
||||
# Naechster INSERT-Punkt zur sepliste-Koordinate.
|
||||
sx, sy = eintrag.get("x", 0.0), eintrag.get("y", 0.0)
|
||||
entfernungen = sorted(
|
||||
((math.hypot(it.get("_x", 0.0) - sx, it.get("_y", 0.0) - sy), it)
|
||||
for it in kandidaten),
|
||||
key=lambda p: p[0])
|
||||
best_d, best = entfernungen[0]
|
||||
best = min(kandidaten,
|
||||
key=lambda it: math.hypot(it.get("_x", 0.0) - sx, it.get("_y", 0.0) - sy))
|
||||
sep_kette_by_key[key] = best
|
||||
gewaehlt_von.setdefault(id(best), []).append(key)
|
||||
per_fallback += 1
|
||||
if dbg:
|
||||
rangliste = ", ".join(f"{it.get('teileid', '')!r}={d:.0f}mm"
|
||||
for d, it in entfernungen)
|
||||
dbg(f" sepliste-SEP {key} (x={sx:.0f},y={sy:.0f}) -> Separator-Item "
|
||||
f"TeileId {best.get('teileid', '')!r} (d={best_d:.0f}mm); "
|
||||
f"Kandidaten sortiert: [{rangliste}]"
|
||||
f"{dbg_highlight(best.get('teileid', ''))}")
|
||||
dbg(f" {key} (x={sx:.0f},y={sy:.0f}) -> Naehe-Fallback auf "
|
||||
f"Separator-Item TeileId {best.get('teileid', '')!r}")
|
||||
|
||||
if dbg:
|
||||
dbg("")
|
||||
mehrfach = {iid: keys for iid, keys in gewaehlt_von.items() if len(keys) > 1}
|
||||
if mehrfach:
|
||||
dbg(" WARNUNG: folgende Separator-Items wurden von MEHREREN "
|
||||
"sepliste-Keys unabhaengig als naechster Kandidat gewaehlt "
|
||||
"(nicht-exklusive Zuordnung):")
|
||||
for keys in mehrfach.values():
|
||||
teileid = sep_kette_by_key[keys[0]].get("teileid", "")
|
||||
dbg(f" TeileId {teileid!r} gewaehlt von Keys: {keys}"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
gewaehlte_ids = set(gewaehlt_von.keys())
|
||||
for zuordnung, kandidaten in sep_items_by_zuordnung.items():
|
||||
if zuordnung not in zuordnungen_mit_sepliste:
|
||||
continue
|
||||
for it in kandidaten:
|
||||
if id(it) not in gewaehlte_ids:
|
||||
teileid = it.get("teileid", "")
|
||||
dbg(f" Separator-Item TeileId {teileid!r} (Zuordnung "
|
||||
f"{zuordnung!r}) wurde von KEINEM sepliste-Key gewaehlt "
|
||||
f"- bleibt ohne sep_kette-Eintrag, damit ohne Nachbarn/"
|
||||
f"TrackIds{dbg_highlight(teileid)}")
|
||||
dbg(f" Zuordnung: {per_id} per ID, {per_fallback} per Naehe-Fallback, "
|
||||
f"{ohne_treffer} ohne Treffer")
|
||||
return sep_kette_by_key
|
||||
|
||||
|
||||
def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
sep_kette_by_key, weiche_by_key, dbg=None):
|
||||
"""Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items/
|
||||
build_strecke_schleus_items/map_separator_kette_items zunaechst leer
|
||||
gelassen (bzw. deren Item nur registriert) haben, weil ihr(e) Nachbar(n)
|
||||
beim eigenen Bau/der Registrierung noch nicht als TeileId feststand(en)
|
||||
(Weiche vs. Separator im Kreisel-Umlauf, oder eine sepliste-Zeile, deren
|
||||
Nachfolger erst danach registriert wird).
|
||||
"""Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items
|
||||
zunaechst leer gelassen bzw. map_sepliste_kette_items nur registriert hat,
|
||||
weil ihr(e) Nachbar(n) beim eigenen Bau/der Registrierung noch nicht als
|
||||
TeileId feststand(en) (Weiche vs. Separator im Kreisel-Umlauf, oder eine
|
||||
sepliste-Zeile, deren Nachfolger erst danach registriert wird).
|
||||
|
||||
Muss NACH allen drei Aufrufen laufen (ihre TeileIds stehen erst dann
|
||||
fest). items wird IN-PLACE veraendert (item["nachbarn"]).
|
||||
Muss NACH build_kreisel_weiche_items UND map_sepliste_kette_items laufen
|
||||
(deren TeileIds stehen erst dann fest). items wird IN-PLACE veraendert
|
||||
(item["nachbarn"]).
|
||||
|
||||
kreisel_umlauf = Rueckgabe von export_neighbors.compute_kreisel_umlauf.
|
||||
sep_kette = Rueckgabe von export_neighbors.compute_sep_kette.
|
||||
sep_kette_by_key = dict sep_kette-Schluessel -> Item (befuellt von
|
||||
build_strecke_schleus_items UND
|
||||
map_separator_kette_items - DIESELBE dict fuer beide,
|
||||
siehe process_blocks: ein sep_kette-Nachbar kann in der
|
||||
jeweils anderen Funktion registriert worden sein).
|
||||
sep_kette_by_key = Rueckgabe von map_sepliste_kette_items (dict
|
||||
sep_kette-Schluessel -> Item).
|
||||
weiche_by_key = dict switches-"key" -> erzeugtes Item (von
|
||||
build_kreisel_weiche_items).
|
||||
"""
|
||||
@@ -837,8 +680,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
if item is None:
|
||||
if dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: "
|
||||
f"KEIN Item in alle_schluessel gefunden - "
|
||||
f"uebersprungen{dbg_highlight(punkt_key)}")
|
||||
f"KEIN Item in alle_schluessel gefunden - uebersprungen")
|
||||
continue
|
||||
nachbarn = []
|
||||
for nachbar_key in (vorgaenger, nachfolger):
|
||||
@@ -849,20 +691,17 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
nachbarn.append(nachbar_item.get("teileid", ""))
|
||||
elif dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: "
|
||||
f"Nachbar-Key {nachbar_key!r} nicht in alle_schluessel "
|
||||
f"aufloesbar{dbg_highlight(item.get('teileid', ''))}")
|
||||
f"Nachbar-Key {nachbar_key!r} nicht in alle_schluessel aufloesbar")
|
||||
teileid = item.get("teileid", "")
|
||||
if nachbarn:
|
||||
item["nachbarn"] = ", ".join(nachbarn)
|
||||
if dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} "
|
||||
f"(TeileId {teileid!r}): nachbarn={nachbarn}"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"(TeileId {teileid!r}): nachbarn={nachbarn}")
|
||||
elif dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} "
|
||||
f"(TeileId {teileid!r}): KEINE Nachbarn aufgeloest "
|
||||
f"(vorgaenger={vorgaenger!r}, nachfolger={nachfolger!r})"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"(vorgaenger={vorgaenger!r}, nachfolger={nachfolger!r})")
|
||||
|
||||
if dbg:
|
||||
dbg("")
|
||||
@@ -878,7 +717,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
if dbg:
|
||||
dbg(f" sepliste-Key {key!r} (typ={eintrag.get('typ')!r}): "
|
||||
f"KEIN Item in sep_kette_by_key - uebersprungen "
|
||||
f"(siehe map_separator_kette_items){dbg_highlight(key)}")
|
||||
f"(siehe map_sepliste_kette_items)")
|
||||
continue
|
||||
nachbarn = []
|
||||
for nachbar_key in (eintrag.get("vorgaenger"), eintrag.get("nachfolger")):
|
||||
@@ -894,8 +733,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
dbg(f" sepliste-Key {key!r} (TeileId {teileid!r}): "
|
||||
f"vorgaenger={eintrag.get('vorgaenger')!r} "
|
||||
f"nachfolger={eintrag.get('nachfolger')!r} -> "
|
||||
f"nachbarn={nachbarn} item.nachbarn={item.get('nachbarn', '')!r}"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"nachbarn={nachbarn} item.nachbarn={item.get('nachbarn', '')!r}")
|
||||
|
||||
|
||||
def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None):
|
||||
@@ -909,18 +747,18 @@ def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None):
|
||||
Baureihenfolge - siehe compute_sep_kette) statt sep_kette selbst: dessen
|
||||
dict-Reihenfolge ist nicht garantiert, die sepliste-Liste dagegen schon.
|
||||
AS/ES zaehlen HIER mit (anders als beim "Separatorlist"-Merkmal frueher) -
|
||||
sie sind die Schleus-Enden der Kette (siehe build_strecke_schleus_items)
|
||||
und bekommen dort eine eigene TeileId ("ILS Ausschleuselement"/"ILS
|
||||
Einschleuselement"), analog zu den AS/ES-Enden, die beim Kreisel bereits
|
||||
ueber compute_kreisel_umlauf in dessen TrackIds auftauchen - ohne sie
|
||||
fehlten hier die Kettenenden, obwohl sie beim Kreisel schon drin sind.
|
||||
sie sind eigene Items ("ILS Ausschleuselement"/"ILS Einschleuselement",
|
||||
direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert, siehe
|
||||
process_blocks), analog zu den AS/ES-Enden, die beim Kreisel bereits ueber
|
||||
compute_kreisel_umlauf in dessen TrackIds auftauchen - ohne sie fehlten
|
||||
hier die Kettenenden, obwohl sie beim Kreisel schon drin sind.
|
||||
|
||||
Der eigentliche Separator wurde bereits von map_separator_kette_items ueber
|
||||
die sepliste-Koordinate (x/y) auf sein reales Separator-Item (TeileId)
|
||||
aufgeloest, AS/ES von build_strecke_schleus_items - beide unter demselben
|
||||
"<strecke_id>#<lfdnr>"-Schluessel in sep_kette_by_key eingetragen; hier
|
||||
wird dieser Schluessel nur noch je Strecke eingesammelt. Muss darum NACH
|
||||
map_separator_kette_items/build_strecke_schleus_items laufen.
|
||||
Sowohl der Separator als auch AS/ES wurden bereits von
|
||||
map_sepliste_kette_items ueber die sepliste-ID (Feld "id") auf ihr reales
|
||||
Item (TeileId) aufgeloest, unter demselben "<strecke_id>#<lfdnr>"-
|
||||
Schluessel in sep_kette_by_key eingetragen; hier wird dieser Schluessel
|
||||
nur noch je Strecke eingesammelt. Muss darum NACH map_sepliste_kette_items
|
||||
laufen.
|
||||
"""
|
||||
for item in items:
|
||||
if item.get("teileart", "") not in GF_VF_TEILEARTEN:
|
||||
@@ -960,8 +798,8 @@ def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_ke
|
||||
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).
|
||||
map_sepliste_kette_items/build_kreisel_weiche_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}
|
||||
@@ -987,6 +825,67 @@ def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_ke
|
||||
kreisel_item["trackids"] = ", ".join(teileids)
|
||||
|
||||
|
||||
def pruefe_ases_in_beiden_containern(items, dbg=None):
|
||||
"""Prueft, ob jede AS-/ES-Zeile (AUSSCHLEUS_TEILEART/EINSCHLEUS_TEILEART)
|
||||
in BEIDEN Containern auftaucht, zu denen sie gehoert:
|
||||
- in den TrackIds ihrer EIGENEN Strecke (VF_n/GF_n, ueber das
|
||||
ZUORDNUNG-Attribut des Markers - siehe Item-Feld "_zuordnung")
|
||||
- in den TrackIds MINDESTENS eines Kreisels (der beruehrte Carrier,
|
||||
geometrisch ueber compute_kreisel_umlauf/KREISEL_UMLAUF_TEILEARTEN
|
||||
ermittelt - nur "ILS 2.0 Kreisel", Eckrad nimmt am Umlauf nicht teil,
|
||||
siehe compute_kreisel_umlauf/KREISEL_SPLIT_TEILEART)
|
||||
|
||||
Muss NACH write_separatorliste_merkmale UND
|
||||
write_kreisel_separatorliste_merkmale laufen (beide TrackIds-Spalten
|
||||
muessen bereits stehen).
|
||||
|
||||
Ein AS/ES, das in einem der beiden Container fehlt, zeigt einen echten
|
||||
Fehler in der Zuordnungskette an (z.B. ZUORDNUNG-Attribut leer/falsch,
|
||||
Kollisionstoleranz zu knapp, Kreisel zu weit weg, fehlende Bounding-Box am
|
||||
Marker) - kein Normalfall. Traegt in diesem Fall eine Warnung in
|
||||
item["warnung"] ein (erscheint in der CSV-Spalte "Warnungen") und gibt sie
|
||||
zusaetzlich auf der Konsole/im Debuglog aus.
|
||||
|
||||
Rueckgabe: Liste der betroffenen TeileIds (fuer Tests/Diagnose), leer bei
|
||||
vollstaendiger Zuordnung.
|
||||
"""
|
||||
strecke_by_id = {it.get("teileid", ""): it for it in items
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN}
|
||||
kreisel_trackid_sets = [
|
||||
{t.strip() for t in (it.get("trackids", "") or "").split(",") if t.strip()}
|
||||
for it in items if it.get("teileart", "") == KREISEL_SPLIT_TEILEART
|
||||
]
|
||||
|
||||
auffaellig = []
|
||||
for it in items:
|
||||
if it.get("teileart", "") not in (AUSSCHLEUS_TEILEART, EINSCHLEUS_TEILEART):
|
||||
continue
|
||||
teileid = it.get("teileid", "")
|
||||
fehlend = []
|
||||
|
||||
strecke = strecke_by_id.get(it.get("_zuordnung", ""))
|
||||
if strecke is None:
|
||||
fehlend.append("eigene Strecke (ZUORDNUNG unaufloesbar)")
|
||||
else:
|
||||
strecke_ids = {t.strip() for t in (strecke.get("trackids", "") or "").split(",") if t.strip()}
|
||||
if teileid not in strecke_ids:
|
||||
fehlend.append("eigene Strecke")
|
||||
|
||||
if not any(teileid in ids for ids in kreisel_trackid_sets):
|
||||
fehlend.append("Kreisel")
|
||||
|
||||
if fehlend:
|
||||
auffaellig.append(teileid)
|
||||
warnung = f"nicht in TrackIds von: {', '.join(fehlend)}"
|
||||
bisherige = it.get("warnung", "")
|
||||
it["warnung"] = f"{bisherige}; {warnung}" if bisherige else warnung
|
||||
meldung = f"[export_csv] WARNUNG: {it.get('teileart', '')} {teileid}: {warnung}"
|
||||
print(meldung)
|
||||
if dbg:
|
||||
dbg(f" {meldung}")
|
||||
return auffaellig
|
||||
|
||||
|
||||
def process_blocks(blocks, lookup, dbg=None):
|
||||
# x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe
|
||||
# export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls
|
||||
@@ -1010,6 +909,8 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
eingang_count = 0
|
||||
ausgang_count = 0
|
||||
geruest_count = 0
|
||||
ausschleus_count = 0
|
||||
einschleus_count = 0
|
||||
|
||||
# Zaehlung fuer die Omniflo-Sum-Zeile (nur Bogen/Weiche/Gerade)
|
||||
cnt_wk = cnt_einzel = cnt_delta = cnt_doppel = cnt_stern = 0
|
||||
@@ -1025,15 +926,54 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
|
||||
# ZZ_ASES_MARKER (siehe cfg/export.cfg pattern_ases_marker sowie
|
||||
# csv:asmark-aktualisieren in Lisp/export.lsp): der persistente
|
||||
# Marker, den Lisp an jedes AS-/ES-Kettenende setzt, damit dessen ID
|
||||
# ueber Exportlaeufe hinweg stabil bleibt. Bekommt hier bewusst KEINE
|
||||
# eigene CSV-Zeile - die sichtbare AS/ES-Zeile fuer denselben
|
||||
# physischen Punkt liefert weiterhin build_strecke_schleus_items
|
||||
# (reine Geometrie); die Marker-ID wird dort ueber das sepliste-Feld
|
||||
# "id" wiederverwendet (siehe dort). Eine eigene Zeile hier waere
|
||||
# doppelt gezaehlt und haette ohnehin keine sinnvolle Geometrie/
|
||||
# Boundingbox aus dem kleinen Marker-Rechteck selbst.
|
||||
# Marker, den Lisp an jedes AS-/ES-Kettenende setzt. Er IST die
|
||||
# AS-/ES-Zeile - TeileId, Position und Boundingbox kommen direkt vom
|
||||
# Marker-Block selbst (echtes INSERT, echte vla-getboundingbox-Box),
|
||||
# kein synthetischer Bau-Schritt mehr noetig (fruehere Version:
|
||||
# compute_strecke_kreisel_schleus/build_strecke_schleus_items suchten
|
||||
# den anschliessenden Kreisel per Kollisionstest und erfanden dabei
|
||||
# eine eigene, bei jedem Export neue TeileId). "Kreisel"-Merkmal und
|
||||
# ID-basierte sep_kette-Zuordnung tragen unten zwei kurze Nachlaeufe
|
||||
# in process_blocks nach, sobald alle Items klassifiziert sind (die
|
||||
# eigene Kette bzw. der beruehrte Kreisel koennen an DIESER Stelle
|
||||
# noch unklassifiziert sein).
|
||||
if matches_any(bname, BLOCKPATTERNS.get("pattern_ases_marker", [])):
|
||||
attribs = block.get("attribs", {})
|
||||
typ = attribs.get("TYP", "")
|
||||
teileart = {"AS": AUSSCHLEUS_TEILEART, "ES": EINSCHLEUS_TEILEART}.get(typ)
|
||||
if teileart is None:
|
||||
# Sollte nicht vorkommen (TYP wird von csv:asmark-aktualisieren
|
||||
# immer gesetzt) - defensiv ueberspringen statt abzustuerzen.
|
||||
print(f"[export_csv] WARNUNG: {bname} ohne gueltiges TYP-Attribut "
|
||||
f"(TYP={typ!r}) - uebersprungen")
|
||||
continue
|
||||
elem_nr += 1
|
||||
if typ == "AS":
|
||||
ausschleus_count += 1
|
||||
zaehler_n = ausschleus_count
|
||||
else:
|
||||
einschleus_count += 1
|
||||
zaehler_n = einschleus_count
|
||||
items.append({
|
||||
"nr": elem_nr,
|
||||
"teileart": teileart,
|
||||
"teileid": attribs.get("ID") or format_sensor_id("ASES", elem_nr),
|
||||
"bezeichnung": f"{teileart} :{zaehler_n}",
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": {
|
||||
"Streckenende": "Anfang" if typ == "AS" else "Ende",
|
||||
"Element": attribs.get("ELEMENT", ""),
|
||||
"ARTINR": attribs.get("ARTINR", ""),
|
||||
},
|
||||
# Private Felder (nicht in der CSV) fuer die beiden Nachlaeufe
|
||||
# weiter unten in process_blocks: "_zuordnung" = ID des
|
||||
# eigenen VF_n/GF_n-Wrappers (fuer das "Förderer"/"Strecke"-
|
||||
# Merkmal), "_lfdnr" wird hier nicht gebraucht (die sep_kette-
|
||||
# Zuordnung laeuft exakt ueber die TeileId, siehe
|
||||
# map_sepliste_kette_items).
|
||||
"_zuordnung": attribs.get("ZUORDNUNG", ""),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
continue
|
||||
|
||||
# Omniflo Bogen oder Weiche
|
||||
@@ -1108,12 +1048,8 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
"bezeichnung": bezeichnung_oder_fallback(block, f"VarioFoerderer :{vf_count}"),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_variofoerderer_merkmale(block),
|
||||
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
|
||||
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
|
||||
"x": block.get("x", 0.0),
|
||||
"y": block.get("y", 0.0),
|
||||
# Sepliste-XDATA (AS/SEP/ES-Reihenfolge) durchreichen - Quelle
|
||||
# fuer compute_sep_kette/map_separator_kette_items (Ketten-
|
||||
# fuer compute_sep_kette/map_sepliste_kette_items (Ketten-
|
||||
# interne Separatoren + AS/ES-Nachbarn). Ohne diese Weitergabe
|
||||
# liefe die gesamte sepliste-basierte Nachbarverkettung ins
|
||||
# Leere (das Feld stand im JSON, wurde aber nicht ans Item
|
||||
@@ -1134,10 +1070,6 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
"bezeichnung": bezeichnung_oder_fallback(block, f"Gefaellestrecke :{gf_count}"),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_variofoerderer_merkmale(block),
|
||||
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
|
||||
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
|
||||
"x": block.get("x", 0.0),
|
||||
"y": block.get("y", 0.0),
|
||||
# Sepliste-XDATA durchreichen (siehe VF_N oben).
|
||||
"sepliste": block.get("sepliste"),
|
||||
**bbox_columns(block),
|
||||
@@ -1232,9 +1164,9 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
# ueberschreiben darf - siehe build_sensor_merkmale.
|
||||
"_zuordnung_fix": bool(block.get("zuordnung_fix")),
|
||||
# Roher INSERT-Punkt (nicht der bbox-Mittelpunkt) - dient
|
||||
# map_separator_kette_items zum Zuordnen dieses (Proxy-)
|
||||
# Separators zu seinem sepliste-Eintrag (Match ueber
|
||||
# Wrapper-ID + naechster INSERT-Punkt, siehe dort).
|
||||
# map_sepliste_kette_items als Koordinaten-FALLBACK, falls der
|
||||
# sepliste-Eintrag (noch) kein "id"-Feld traegt (Altbestand;
|
||||
# der Normalfall loest exakt per ID auf, siehe dort).
|
||||
"_x": block.get("x", 0.0), "_y": block.get("y", 0.0),
|
||||
**cols,
|
||||
})
|
||||
@@ -1433,34 +1365,46 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM,
|
||||
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
|
||||
|
||||
# Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren (per Zuordnung
|
||||
# oben bereits einem Kreisel zugeordnet), die AS/ES-Enden andockender
|
||||
# Ketten (aus sep_kette) UND die Kreisel-Kreisel-Weichen (touch_switches),
|
||||
# sortiert nach Winkel um die Kreiselachse (siehe compute_kreisel_umlauf -
|
||||
# DREHRICHTUNG-Attribut bestimmt den Umlaufsinn).
|
||||
# Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren, BTMT-Stationen
|
||||
# UND die AS/ES-Enden andockender Ketten (alle zusammen KREISEL_UMLAUF_
|
||||
# TEILEARTEN, einheitlich per eigener Bounding-Box zugeordnet - ein AS/ES
|
||||
# bringt seine Box vom ZZ_ASES_MARKER direkt mit) sowie die Kreisel-
|
||||
# Kreisel-Weichen (touch_switches), sortiert nach Winkel um die
|
||||
# Kreiselachse (siehe compute_kreisel_umlauf - DREHRICHTUNG-Attribut
|
||||
# bestimmt den Umlaufsinn).
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette, NEIGHBOR_TOLERANCE_MM,
|
||||
items, NEIGHBOR_TOLERANCE_MM,
|
||||
durchmesser_mm=KREISEL_DURCHMESSER_MM, touch_switches=touch_switches, dbg=dbg)
|
||||
|
||||
# Neue Zeilen, deren "Nachbarn"-Spalte aus einem sep_kette-Schluessel
|
||||
# aufgeloest werden muss, erzeugen: Kreisel-Kreisel-Weichen, dann Aus-/
|
||||
# Einschleuselemente (AS/ES). Ein sep_kette-Schluessel kann auf eine Zeile
|
||||
# zeigen, die ERST DANACH registriert wird, darum erst NACH allen
|
||||
# Registrierungen aufloesen (siehe unten) statt inline - frueherer Bug
|
||||
# schrieb sonst den rohen Schluessel ("0026#3") statt der TeileId.
|
||||
# aufgeloest werden muss, erzeugen: nur noch die Kreisel-Kreisel-Weichen
|
||||
# (AS/ES sind bereits als eigene Items klassifiziert, siehe process_blocks
|
||||
# oben). Ein sep_kette-Schluessel kann auf eine Zeile zeigen, die ERST
|
||||
# DANACH registriert wird, darum erst NACH allen Registrierungen aufloesen
|
||||
# (siehe unten) statt inline - frueherer Bug schrieb sonst den rohen
|
||||
# Schluessel ("0026#3") statt der TeileId.
|
||||
elem_nr, weiche_by_key = build_kreisel_weiche_items(
|
||||
items, planquadrat_cfg, elem_nr, touch_switches, dbg=dbg)
|
||||
sep_kette_by_key = {}
|
||||
elem_nr = build_strecke_schleus_items(
|
||||
items, planquadrat_cfg, elem_nr, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key, dbg=dbg)
|
||||
# Die in einer Kette verpackten Separatoren erzeugen KEINE eigenen Zeilen -
|
||||
# sie liegen bereits als (Proxy-)Separator_SP-Items vor (freier-Separator-
|
||||
# Pfad oben). map_separator_kette_items ordnet ihnen nur ihren sepliste-
|
||||
# Schluessel zu, damit die Nachbar-Reihenfolge (AS -> SEP -> ES) auf die
|
||||
# vorhandenen Items angewandt werden kann (kein Doppel-Export).
|
||||
map_separator_kette_items(
|
||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key, dbg=dbg)
|
||||
|
||||
# "Förderer"/"Strecke"-Merkmal an jedem AS/ES nachtragen: die eigene Kette
|
||||
# (ZUORDNUNG-Attribut des Markers, siehe "_zuordnung") kann bei der
|
||||
# Klassifikation des Markers selbst noch unklassifiziert gewesen sein -
|
||||
# der FeldNAME haengt von deren TeileArt ab (VF -> "Förderer",
|
||||
# GF -> "Strecke", siehe _strecke_typ_feld), der WERT ist einfach die
|
||||
# ZUORDNUNG selbst (keine Geometrie mehr noetig).
|
||||
strecke_by_id = {it.get("teileid", ""): it for it in items
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN}
|
||||
for it in items:
|
||||
if it.get("teileart", "") not in (AUSSCHLEUS_TEILEART, EINSCHLEUS_TEILEART):
|
||||
continue
|
||||
strecke = strecke_by_id.get(it.get("_zuordnung", ""))
|
||||
if strecke is not None:
|
||||
it["merkmale"][_strecke_typ_feld(strecke.get("teileart", ""))] = \
|
||||
strecke.get("teileid", "")
|
||||
|
||||
# sep_kette-Schluessel (AS/SEP/ES) auf ihre bereits vorhandenen Items
|
||||
# aufloesen - exakt per ID, siehe map_sepliste_kette_items.
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette, dbg=dbg)
|
||||
|
||||
# Spalte "TrackIds" je Strecke (Reihenfolge der SEP-TeileIds aus der
|
||||
# sepliste, x/y bereits oben auf das reale Separator-Item aufgeloest).
|
||||
@@ -1477,6 +1421,12 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
write_kreisel_separatorliste_merkmale(
|
||||
items, kreisel_umlauf, sep_kette_by_key, weiche_by_key, dbg=dbg)
|
||||
|
||||
# Konsistenzpruefung: jedes AS/ES muss jetzt sowohl in den TrackIds seiner
|
||||
# eigenen Strecke als auch in denen eines Kreisels auftauchen (siehe
|
||||
# Docstring) - Abweichungen deuten auf einen echten Fehler in der
|
||||
# Zuordnungskette hin und werden ueber "Warnungen" sichtbar gemacht.
|
||||
pruefe_ases_in_beiden_containern(items, 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
|
||||
|
||||
+50
-276
@@ -88,26 +88,21 @@ SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"}
|
||||
# pattern_ausgang in cfg/export.cfg) werden hier wie BTMT als frei platzierte
|
||||
# Umlauf-Punkte gefuehrt - so tauchen sie, wenn sie auf einer Kreiselbahn
|
||||
# liegen, in dessen "TrackIds"-Spalte auf (Nutzeranforderung: wie
|
||||
# Separator/AS/ES behandeln). Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"-
|
||||
# Praefix (siehe export_csv.py) - dieselben Strings muessen hier stehen.
|
||||
# Separator/AS/ES behandeln). AUSSCHLEUS_TEILEART/EINSCHLEUS_TEILEART (AS/ES-
|
||||
# Kettenenden, siehe unten): seit sie als dauerhafter ZZ_ASES_MARKER real in
|
||||
# der Zeichnung stehen (statt wie frueher nur ueber eine Kreisel<->Strecke-
|
||||
# Kollisionsberechnung synthetisiert zu werden, siehe git-Historie
|
||||
# compute_strecke_kreisel_schleus), reicht ihre EIGENE Bounding-Box fuer die
|
||||
# ganz normale BBox-Ueberschneidung mit einem Kreisel - keine gesonderte
|
||||
# Naeherungs-/Kollisionslogik mehr noetig (siehe compute_kreisel_umlauf, das
|
||||
# vorher zusaetzlich sep_kette nach der naechstgelegenen Kreisel-Achse absuchte).
|
||||
# Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"-Praefix (siehe
|
||||
# export_csv.py) - dieselben Strings muessen hier stehen.
|
||||
KREISEL_UMLAUF_TEILEARTEN = {
|
||||
"ILS 2.0 Separator", "ILS 2.0 BTMT Beladung", "ILS 2.0 BTMT Entladung", "ILS Weiche",
|
||||
"Eingang", "Ausgang",
|
||||
"Eingang", "Ausgang", "ILS Ausschleuselement", "ILS Einschleuselement",
|
||||
}
|
||||
|
||||
# Temporaerer Diagnose-Filter fuers Debuglog (siehe [Nachbarschaft] debug_log
|
||||
# in cfg/export.cfg): markiert Zeilen zu diesen TeileIds im .dbg-Log
|
||||
# besonders, um die fehlende Nachbarn-/TrackIds-Zuordnung einzelner
|
||||
# Separatoren (0025/0029/0031/0035) nachzuvollziehen. Nach Abschluss der
|
||||
# Diagnose wieder auf einen leeren Set zuruecksetzen oder entfernen.
|
||||
DEBUG_HIGHLIGHT_TEILEIDS = {"0025", "0029", "0031", "0035"}
|
||||
|
||||
|
||||
def dbg_highlight(teileid):
|
||||
"""Suffix fuer eine Debuglog-Zeile, wenn teileid in
|
||||
DEBUG_HIGHLIGHT_TEILEIDS steht - sonst leerer String."""
|
||||
return " <<<< ZIEL-ID" if str(teileid) in DEBUG_HIGHLIGHT_TEILEIDS else ""
|
||||
|
||||
# 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.
|
||||
@@ -117,9 +112,10 @@ KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel"
|
||||
# (compute_kreisel_touch_switches / export_csv.py).
|
||||
WEICHE_TEILEART = "ILS Weiche"
|
||||
|
||||
# TeileArten der automatisch an den Strecken-Enden (Kollision Kreisel <->
|
||||
# Gefaellestrecke/Foerderer) erzeugten Ein-/Ausschleuselemente
|
||||
# (compute_strecke_kreisel_schleus / export_csv.py).
|
||||
# TeileArten der Ein-/Ausschleuselemente an den Strecken-Enden (AS/ES) einer
|
||||
# Gefaellestrecke/eines Foerderers - siehe process_blocks (export_csv.py):
|
||||
# direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert (dessen Attribut
|
||||
# TYP="AS"/"ES" waehlt die TeileArt), keine gesonderte Erzeugung mehr hier.
|
||||
AUSSCHLEUS_TEILEART = "ILS Ausschleuselement"
|
||||
EINSCHLEUS_TEILEART = "ILS Einschleuselement"
|
||||
|
||||
@@ -1096,39 +1092,12 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0,
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kreisel <-> Strecken-Enden -> Ein-/Ausschleuselemente
|
||||
# Kreisel <-> Strecken-Enden: verfeinerte Kollisionsbox fuer compute_neighbor_
|
||||
# ids (die "Nachbarn"-Spalte einer GF/VF-Strecke soll nur an ihrem echten AS-/
|
||||
# ES-Ende auf einen Kreisel reagieren, nicht auf die gesamte, oft lange
|
||||
# Wrapper-Bounding-Box).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _closest_pt_on_segment(a, b, p):
|
||||
"""Naechstgelegener Punkt auf der 2D-Strecke [a,b] zum Punkt p."""
|
||||
abx, aby = b[0] - a[0], b[1] - a[1]
|
||||
denom = abx * abx + aby * aby
|
||||
if denom <= 1e-9:
|
||||
return (a[0], a[1])
|
||||
t = _clamp(((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / denom, 0.0, 1.0)
|
||||
return (a[0] + abx * t, a[1] + aby * t)
|
||||
|
||||
|
||||
def _strecke_ends(bbox, insert_xy):
|
||||
"""Die beiden Enden einer GF/VF-Strecke als (Anfang, Ende) in Welt-x/y.
|
||||
|
||||
Enden = Mitten der beiden kurzen Stirnseiten der (achsparallelen) Bounding-
|
||||
Box entlang ihrer laengeren Ausdehnung. "Anfang" ist das Ende, das dem
|
||||
Block-Einfuegepunkt (insert_xy = Kettenanfang/startpunkt, siehe
|
||||
ssg-block-wrap-welt) naeher liegt; "Ende" das andere.
|
||||
"""
|
||||
cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0)
|
||||
dx, dy = bbox.get("dx", 0.0), bbox.get("dy", 0.0)
|
||||
if dx >= dy:
|
||||
e0, e1 = (cx - dx / 2.0, cy), (cx + dx / 2.0, cy)
|
||||
else:
|
||||
e0, e1 = (cx, cy - dy / 2.0), (cx, cy + dy / 2.0)
|
||||
ix, iy = insert_xy
|
||||
if math.hypot(e0[0] - ix, e0[1] - iy) <= math.hypot(e1[0] - ix, e1[1] - iy):
|
||||
return e0, e1
|
||||
return e1, e0
|
||||
|
||||
|
||||
def _asel_kos_xy(item):
|
||||
"""(k1_xy, k2_xy) einer GF/VF-Strecke = Welt-(x,y) von AS (K1/KS_EIN) und
|
||||
ES (K2/KS_AUS), dekodiert aus item["k1"]/["k2"] (csv:trans-encode, siehe
|
||||
@@ -1177,188 +1146,6 @@ def _asel_collision_boxes(item, half_tol, box_dims):
|
||||
return boxes
|
||||
|
||||
|
||||
def _distinct_kreisel_ids(nachbarn):
|
||||
"""Eindeutige Kreisel-TeileIds aus einem Nachbarn-String ("0009-L, 0002-R,
|
||||
0001-L" -> ["0009", "0002", "0001"]). Das Haelften-Suffix -L/-R (siehe
|
||||
kreisel_half_bboxes) wird entfernt, Reihenfolge bleibt erhalten."""
|
||||
ids = []
|
||||
for part in (nachbarn or "").split(","):
|
||||
p = part.strip()
|
||||
if not p:
|
||||
continue
|
||||
if p.endswith("-L") or p.endswith("-R"):
|
||||
p = p[:-2]
|
||||
if p not in ids:
|
||||
ids.append(p)
|
||||
return ids
|
||||
|
||||
|
||||
def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0,
|
||||
element_box_mm=None, dbg=None):
|
||||
"""Erzeugt je Gefaellestrecke/Foerderer (GF_VF_TEILEARTEN) an Anfang und
|
||||
Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum
|
||||
anschliessenden Kreisel.
|
||||
|
||||
Grundlage sind NICHT eigene Abstandsschwellen, sondern die bereits per
|
||||
Bounding-Box ermittelten Kreisel-Nachbarn der Strecke (Spalte "Nachbarn",
|
||||
siehe compute_neighbor_ids). Jeder der beiden Streckenenden (Anfang = dem
|
||||
Block-Einfuegepunkt naeheres Ende, siehe _strecke_ends) wird der unter
|
||||
diesen Nachbarn naechstgelegene Kreisel zugeordnet:
|
||||
- Am ANFANG entsteht ein Ausschleuselement (AUSSCHLEUS_TEILEART),
|
||||
- am ENDE ein Einschleuselement (EINSCHLEUS_TEILEART).
|
||||
Beruehrt eine Strecke nur EINEN Kreisel (z.B. ein Foerderer, der zum selben
|
||||
Kreisel zurueckfuehrt), wird dieser beiden Enden zugeordnet. Beruehrt sie
|
||||
KEINEN Kreisel, entstehen keine Elemente.
|
||||
|
||||
Eine Strecke sollte genau zwei Kreisel-Bounding-Boxen beruehren (ein Anfang
|
||||
+ ein Ende). Beruehrt ihre BBox MEHR als zwei (mit exakten 3D-BBoxen sollte
|
||||
das nicht vorkommen - hier wird in x/y ohne Hoehe geprueft), entsteht eine
|
||||
Warnung: die beiden Enden bekommen weiterhin ihren jeweils naechsten
|
||||
Kreisel, die ueberzaehligen werden ignoriert und in der Warnung genannt.
|
||||
|
||||
Position eines Elements = das AS-/ES-Koordinatensystem (K1/K2, siehe
|
||||
csv:vfgf-k-kos-strings), um eine HALBE Box (Box-Breite/2 entlang der
|
||||
Foerderachse, siehe element_box_mm) nach aussen versetzt: das
|
||||
Ausschleuselement (AS/Anfang) VOR das AS-KOS (entgegen der Achse), das
|
||||
Einschleuselement (ES/Ende) HINTER das ES-KOS (in Achsrichtung) - beide
|
||||
also zum anschliessenden Kreisel hin, sodass das Element realistisch
|
||||
zwischen Streckenende und Kreisel liegt. Fehlen K1/K2 (Altbestand ohne
|
||||
AS/ES-Export), werden die Streckenenden aus der Wrapper-BBox genommen
|
||||
(_strecke_ends). Die feste Box (element_box_mm) wird mit ihrer Laengsseite
|
||||
senkrecht zur Foerderachse ausgerichtet (Welt-AABB, _oriented_box_aabb).
|
||||
|
||||
element_box_mm = (laenge, breite, hoehe) in mm; None -> load_element_box_mm().
|
||||
|
||||
Rueckgabe: (result, warnings)
|
||||
result = Liste von dicts:
|
||||
{"teileart": AUSSCHLEUS_/EINSCHLEUS_TEILEART,
|
||||
"x","y","z": Kollisionsposition (mm),
|
||||
"dx","dy","dz": Welt-AABB der ausgerichteten Box (mm),
|
||||
"kreisel_id": TeileId des anschliessenden Kreisels,
|
||||
"strecke_id": TeileId der GF/VF,
|
||||
"ende_label": "Anfang" | "Ende"}
|
||||
warnings = dict {strecke_teileid: warntext} fuer Strecken mit > 2
|
||||
Kreisel-BBox-Beruehrungen.
|
||||
"""
|
||||
if element_box_mm is None:
|
||||
element_box_mm = load_element_box_mm()
|
||||
box_l, box_b, box_h = element_box_mm
|
||||
kreisel = {}
|
||||
for it in items:
|
||||
if it.get("teileart", "") == KREISEL_SPLIT_TEILEART:
|
||||
kreisel[it.get("teileid", "")] = _kreisel_capsule(it, durchmesser_mm)
|
||||
|
||||
if dbg:
|
||||
dbg("")
|
||||
dbg("=== Strecken-Enden <-> Kreisel -> Ein-/Ausschleuselemente ===")
|
||||
dbg(f"Echte Kreisel: {len(kreisel)}; Zuordnung ueber die BBox-Nachbarn "
|
||||
f"der Strecke (naechster Kreisel je Ende), erwartet 2 Kreisel/Strecke")
|
||||
|
||||
result = []
|
||||
warnings = {}
|
||||
for it in items:
|
||||
if it.get("teileart", "") not in GF_VF_TEILEARTEN:
|
||||
continue
|
||||
bbox = it.get("_bbox")
|
||||
if not bbox:
|
||||
continue
|
||||
sid = it.get("teileid", "")
|
||||
nb_ids = [k for k in _distinct_kreisel_ids(it.get("nachbarn", "")) if k in kreisel]
|
||||
if not nb_ids:
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: kein Kreisel-Nachbar -> keine Schleuselemente")
|
||||
continue
|
||||
|
||||
insert_xy = (it.get("x", bbox.get("cx", 0.0)) or 0.0,
|
||||
it.get("y", bbox.get("cy", 0.0)) or 0.0)
|
||||
# Streckenenden bevorzugt aus den AS/ES-Koordinatensystemen (K1=AS,
|
||||
# K2=ES, siehe csv:vfgf-k-kos-strings) - das ist die tatsaechliche
|
||||
# Position der Ein-/Ausschleuspunkte. "Anfang" bleibt das dem Block-
|
||||
# Einfuegepunkt naehere Ende. Fehlen K1/K2 (Altbestand ohne AS/ES-
|
||||
# Export), Fallback auf die Stirnseiten-Mitten der Wrapper-BBox.
|
||||
k1_xy, k2_xy = _asel_kos_xy(it)
|
||||
if k1_xy is not None and k2_xy is not None:
|
||||
ix, iy = insert_xy
|
||||
if math.hypot(k1_xy[0] - ix, k1_xy[1] - iy) <= math.hypot(k2_xy[0] - ix, k2_xy[1] - iy):
|
||||
anfang_end, ende_end = k1_xy, k2_xy
|
||||
else:
|
||||
anfang_end, ende_end = k2_xy, k1_xy
|
||||
else:
|
||||
anfang_end, ende_end = _strecke_ends(bbox, insert_xy)
|
||||
# Foerderachse (Einheitsvektor Anfang->Ende) fuer den Halbe-Box-Versatz
|
||||
# der Schleuselemente. Ohne verwertbare Richtung (Enden fallen zusammen)
|
||||
# bleibt der Versatz 0.
|
||||
adx, ady = ende_end[0] - anfang_end[0], ende_end[1] - anfang_end[1]
|
||||
alen = math.hypot(adx, ady)
|
||||
axis_u = (adx / alen, ady / alen) if alen > 1e-9 else (0.0, 0.0)
|
||||
# Versatz = halbe Box-Ausdehnung ENTLANG der Foerderachse. Die feste Box
|
||||
# (element_box_mm) liegt mit ihrer Breite laengs der Achse (siehe
|
||||
# _oriented_box_aabb), also ist die halbe Achs-Ausdehnung Breite/2.
|
||||
halbe_box = box_b / 2.0
|
||||
# Ausrichtung der Schleuselement-Box: entlang der Foerderachse (nicht
|
||||
# mehr der Kreiselachse) - das Element sitzt am AS/ES-KOS in
|
||||
# Foerderrichtung, konsistent mit den AS/ES-Kollisionsboxen.
|
||||
achs_rot = math.atan2(ady, adx) if alen > 1e-9 else 0.0
|
||||
scz = bbox.get("cz", 0.0)
|
||||
|
||||
def _nearest(end_pt):
|
||||
best = None
|
||||
for kid in nb_ids:
|
||||
cap = kreisel[kid]
|
||||
p = _closest_pt_on_segment(cap["a"], cap["b"], end_pt)
|
||||
d = math.hypot(p[0] - end_pt[0], p[1] - end_pt[1])
|
||||
if best is None or d < best[3]:
|
||||
best = (kid, cap, p, d)
|
||||
return best
|
||||
|
||||
a_best = _nearest(anfang_end)
|
||||
e_best = _nearest(ende_end)
|
||||
|
||||
if len(nb_ids) > 2:
|
||||
genutzt = {a_best[0], e_best[0]}
|
||||
extra = [k for k in nb_ids if k not in genutzt]
|
||||
warnings[sid] = (
|
||||
f"beruehrt {len(nb_ids)} Kreisel-BoundingBoxen "
|
||||
f"(erwartet 2 = Anfang + Ende): {', '.join(nb_ids)}"
|
||||
+ (f"; ueberzaehlig/ignoriert: {', '.join(extra)}" if extra else "")
|
||||
+ " - mit exakten 3D-BoundingBoxen sollte das nicht auftreten")
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: WARNUNG - {warnings[sid]}")
|
||||
|
||||
# Versatz der Schleuselement-Position gegenueber dem AS/ES-KOS: eine
|
||||
# HALBE Box entlang der Foerderachse, jeweils NACH AUSSEN (weg von der
|
||||
# Streckenmitte, zum anschliessenden Kreisel hin) - damit das Element
|
||||
# realistisch zwischen Streckenende und Kreisel liegt statt exakt auf
|
||||
# dem KOS. Vorzeichen: Ausschleuselement (AS/Anfang) "vor das AS-KOS"
|
||||
# = entgegen der Achse (-u), Einschleuselement (ES/Ende) "hinter das
|
||||
# ES-KOS" = in Achsrichtung (+u).
|
||||
for best, teileart, label, end_pt, vorzeichen in (
|
||||
(a_best, AUSSCHLEUS_TEILEART, "Anfang", anfang_end, -1.0),
|
||||
(e_best, EINSCHLEUS_TEILEART, "Ende", ende_end, 1.0)):
|
||||
kid, cap, p, d = best
|
||||
pos = (end_pt[0] + vorzeichen * axis_u[0] * halbe_box,
|
||||
end_pt[1] + vorzeichen * axis_u[1] * halbe_box)
|
||||
dx, dy, dz = _oriented_box_aabb(achs_rot, box_l, box_b, box_h)
|
||||
result.append({
|
||||
"teileart": teileart,
|
||||
"x": pos[0], "y": pos[1], "z": (cap["z"] + scz) / 2.0,
|
||||
"dx": dx, "dy": dy, "dz": dz,
|
||||
"kreisel_id": kid, "strecke_id": sid, "ende_label": label,
|
||||
})
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid} {label}: Kreisel {kid} (Abstand Ende<->Achse "
|
||||
f"{d:.0f}mm) -> {teileart} bei ({pos[0]:.0f}, {pos[1]:.0f}, "
|
||||
f"{(cap['z'] + scz) / 2.0:.0f}) = KOS ({end_pt[0]:.0f}, {end_pt[1]:.0f}) "
|
||||
f"{'+' if vorzeichen > 0 else '-'} halbe Box ({halbe_box:.0f}mm) entlang Achse, "
|
||||
f"Box-AABB ({dx:.0f}, {dy:.0f}, {dz:.0f})")
|
||||
|
||||
if dbg:
|
||||
dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; "
|
||||
f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}")
|
||||
|
||||
return result, warnings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sepliste (LISP-XDATA, siehe ssg-sepliste-xdata-schreiben) -> Nachbarschaft
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1376,15 +1163,19 @@ def compute_sep_kette(items, dbg=None):
|
||||
|
||||
Jeder Sepliste-Eintrag bekommt eine (innerhalb dieser Kette eindeutige)
|
||||
Schluessel-ID "<strecke_teileid>#<lfdnr>" - das ist die ID, mit der
|
||||
build_strecke_schleus_items/map_separator_kette_items die spaeteren
|
||||
globalen TeileIds verknuepft (siehe export_csv.py). Vorgaenger/Nachfolger
|
||||
sind schlicht der vorherige/naechste Eintrag in der Sepliste (die bereits
|
||||
in Baureihenfolge vorliegt) - kein Rateverfahren noetig.
|
||||
map_sepliste_kette_items die spaeteren globalen TeileIds verknuepft
|
||||
(siehe export_csv.py). Vorgaenger/Nachfolger sind schlicht der vorherige/
|
||||
naechste Eintrag in der Sepliste (die bereits in Baureihenfolge vorliegt)
|
||||
- kein Rateverfahren noetig.
|
||||
|
||||
Rueckgabe: dict {key: {"typ", "x", "y", "strecke_id", "vorgaenger",
|
||||
Rueckgabe: dict {key: {"typ", "x", "y", "id", "strecke_id", "vorgaenger",
|
||||
"nachfolger"}} - vorgaenger/nachfolger sind wieder solche keys, oder None
|
||||
am jeweiligen Kettenende (Verkettung mit dem Kreisel-Umlauf uebernimmt
|
||||
compute_kreisel_umlauf).
|
||||
compute_kreisel_umlauf). "id" ist die TeileId des Bauteils, das diesen
|
||||
Eintrag in der Zeichnung vertritt (AS/ES ueber den dauerhaften
|
||||
ZZ_ASES_MARKER, SEP ueber die beim Export ausgepackte temporaere
|
||||
Separator_SP-Kopie - siehe csv:sepliste-ids-setzen, Lisp/export.lsp) -
|
||||
leerer String, wenn (noch) keine ID zugewiesen wurde (Altbestand).
|
||||
"""
|
||||
result = {}
|
||||
for item in items:
|
||||
@@ -1401,6 +1192,7 @@ def compute_sep_kette(items, dbg=None):
|
||||
"typ": entry.get("typ", ""),
|
||||
"x": entry.get("x", 0.0),
|
||||
"y": entry.get("y", 0.0),
|
||||
"id": str(entry.get("id", "")).strip(),
|
||||
"strecke_id": sid,
|
||||
"vorgaenger": keys[i - 1] if i > 0 else None,
|
||||
"nachfolger": keys[i + 1] if i < len(keys) - 1 else None,
|
||||
@@ -1411,16 +1203,18 @@ def compute_sep_kette(items, dbg=None):
|
||||
return result
|
||||
|
||||
|
||||
def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
def compute_kreisel_umlauf(items, 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 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).
|
||||
liegenden Punkte - frei platzierte Separatoren, BTMT-Beladung/-Entladung
|
||||
UND die AS/ES-Enden aller andockenden VF_n/GF_n-Ketten (alle zusammen
|
||||
KREISEL_UMLAUF_TEILEARTEN, einheitlich per BBox-Ueberschneidung einer
|
||||
Kreiselhaelfte/eines Kreisels zugeordnet - wie compute_sensor_zuordnung;
|
||||
ein AS/ES bringt seine eigene Bounding-Box vom dauerhaften ZZ_ASES_MARKER
|
||||
mit, seit es als reales Item existiert - keine gesonderte Naeherungslogik
|
||||
mehr noetig) UND die Kreisel-Kreisel-Weichen (touch_switches, siehe
|
||||
compute_kreisel_touch_switches) - in eine geschlossene Umlauf-Reihenfolge
|
||||
(zirkulaer, siehe DREHRICHTUNG-Attribut).
|
||||
|
||||
Eine Kreisel-Kreisel-Weiche liegt (anders als ein Separator oder ein
|
||||
AS/ES-Ende) auf GENAU EINEM Punkt, gehoert aber zu ZWEI Kreiseln - man
|
||||
@@ -1441,17 +1235,15 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
"TrackIds"-Spalte (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-
|
||||
Punkte: x/y + strecke_id). touch_switches = Rueckgabe von
|
||||
compute_kreisel_touch_switches (None/leer = wie bisher ohne Weichen im
|
||||
Umlauf).
|
||||
zuordnung. touch_switches = Rueckgabe von compute_kreisel_touch_switches
|
||||
(None/leer = wie bisher ohne Weichen im Umlauf).
|
||||
|
||||
Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key,
|
||||
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.
|
||||
punkt_key ist die TeileId eines freien Punkts (Separator, BTMT-Beladung/
|
||||
-Entladung ODER AS/ES-Ende, siehe KREISEL_UMLAUF_TEILEARTEN) oder ein
|
||||
touch_switches-"key" (Kreisel-Kreisel-Weiche). Kreisel ohne zugeordnete
|
||||
Punkte liefern eine leere Liste.
|
||||
"""
|
||||
half_tol = tolerance_mm / 2.0
|
||||
kreisel = {}
|
||||
@@ -1498,8 +1290,7 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
if not bbox:
|
||||
if dbg:
|
||||
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}): "
|
||||
f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden")
|
||||
continue
|
||||
bounds = _bounds(bbox, half_tol)
|
||||
matched_tid = None
|
||||
@@ -1513,28 +1304,11 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
if matched_tid is not None:
|
||||
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) "
|
||||
f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) "
|
||||
f"-> Kreisel {matched_tid}{dbg_highlight(teileid)}")
|
||||
f"-> Kreisel {matched_tid}")
|
||||
else:
|
||||
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) "
|
||||
f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) "
|
||||
f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
|
||||
# AS/ES-Enden jeder Kette mit sepliste: naechstgelegener Kreisel (wie
|
||||
# compute_strecke_kreisel_schleus._nearest), aber direkt aus sep_kette
|
||||
# statt ueber die grobe BBox-Nachbarschaft der Strecke.
|
||||
for key, eintrag in sep_kette.items():
|
||||
if eintrag["typ"] not in ("AS", "ES"):
|
||||
continue
|
||||
p = (eintrag["x"], eintrag["y"])
|
||||
best_tid, best_d = None, None
|
||||
for tid, (cap, _dr) in kreisel.items():
|
||||
q = _closest_pt_on_segment(cap["a"], cap["b"], p)
|
||||
d = math.hypot(q[0] - p[0], q[1] - p[1])
|
||||
if best_d is None or d < best_d:
|
||||
best_tid, best_d = tid, d
|
||||
if best_tid is not None:
|
||||
punkte_je_kreisel[best_tid].append((key, eintrag["x"], eintrag["y"]))
|
||||
f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})")
|
||||
|
||||
# Kreisel-Kreisel-Weichen: die beiden beruehrenden Kreisel-IDs stehen
|
||||
# bereits in sw["nachbarn"] (siehe compute_kreisel_touch_switches) - kein
|
||||
@@ -1623,6 +1397,6 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
f"{' -> '.join(p[0] for p in sortiert)}")
|
||||
for punkt_key, vorgaenger, nachfolger in umlauf:
|
||||
dbg(f" Punkt {punkt_key!r}: vorgaenger={vorgaenger!r} "
|
||||
f"nachfolger={nachfolger!r}{dbg_highlight(punkt_key)}")
|
||||
f"nachfolger={nachfolger!r}")
|
||||
|
||||
return result
|
||||
|
||||
@@ -22,8 +22,7 @@ from export_neighbors import ( # noqa: E402
|
||||
compute_kreisel_umlauf,
|
||||
)
|
||||
from export_csv import ( # noqa: E402
|
||||
build_strecke_schleus_items,
|
||||
map_separator_kette_items,
|
||||
map_sepliste_kette_items,
|
||||
resolve_umlauf_und_sepliste_nachbarn,
|
||||
write_kreisel_separatorliste_merkmale,
|
||||
write_separatorliste_merkmale,
|
||||
@@ -53,6 +52,20 @@ def _separator(teileid, x, y):
|
||||
}
|
||||
|
||||
|
||||
def _ases(teileart, teileid, x, y, zuordnung):
|
||||
"""AS-/ES-Item, wie es process_blocks direkt aus einem ZZ_ASES_MARKER
|
||||
klassifiziert: eigene Bounding-Box vom Marker-Rechteck, "_zuordnung" =
|
||||
ZUORDNUNG-Attribut (die eigene Strecke), teilnahmefaehig am Kreisel-
|
||||
Umlauf ueber KREISEL_UMLAUF_TEILEARTEN wie ein freier Separator."""
|
||||
return {
|
||||
"teileart": teileart,
|
||||
"teileid": teileid,
|
||||
"_zuordnung": zuordnung,
|
||||
"merkmale": {},
|
||||
"_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 200.0},
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -105,7 +118,7 @@ class TestKreiselKreiselWeicheImUmlauf:
|
||||
items = _zwei_beruehrende_kreisel()
|
||||
switches = compute_kreisel_touch_switches(items, tolerance_mm=0.0,
|
||||
durchmesser_mm=DURCHMESSER_MM)
|
||||
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=0.0,
|
||||
umlauf = compute_kreisel_umlauf(items, tolerance_mm=0.0,
|
||||
durchmesser_mm=DURCHMESSER_MM,
|
||||
touch_switches=switches)
|
||||
weiche_key = switches[0]["key"]
|
||||
@@ -130,7 +143,7 @@ class TestKreiselKreiselWeicheImUmlauf:
|
||||
assert len(switches) == 1
|
||||
weiche_key = switches[0]["key"]
|
||||
|
||||
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=50.0,
|
||||
umlauf = compute_kreisel_umlauf(items, tolerance_mm=50.0,
|
||||
durchmesser_mm=DURCHMESSER_MM,
|
||||
touch_switches=switches)
|
||||
|
||||
@@ -166,7 +179,7 @@ class TestKreiselBoundsRotationsUnabhaengig:
|
||||
k = self._langer_kreisel_90grad()
|
||||
sep = _separator("9001", 0.0, 10000.0) # liegt mitten auf der Achse
|
||||
items = [k, sep]
|
||||
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=200.0,
|
||||
umlauf = compute_kreisel_umlauf(items, tolerance_mm=200.0,
|
||||
durchmesser_mm=DURCHMESSER_MM)
|
||||
keys = [p[0] for p in umlauf["0002"]]
|
||||
assert "9001" in keys, "Separator auf der gedrehten Kreiselachse wurde nicht erkannt"
|
||||
@@ -179,7 +192,7 @@ class TestKreiselBoundsRotationsUnabhaengig:
|
||||
k = self._langer_kreisel_90grad()
|
||||
sep = _separator("0065", 958.0, 5100.0)
|
||||
items = [k, sep]
|
||||
umlauf = compute_kreisel_umlauf(items, sep_kette={}, tolerance_mm=200.0,
|
||||
umlauf = compute_kreisel_umlauf(items, tolerance_mm=200.0,
|
||||
durchmesser_mm=DURCHMESSER_MM)
|
||||
keys = [p[0] for p in umlauf["0002"]]
|
||||
assert "0065" not in keys, (
|
||||
@@ -191,26 +204,24 @@ class TestSeplisteNachbarnAufgeloest:
|
||||
"""Regressionstest fuer einen dritten realen Mubea-Befund: die meisten
|
||||
Separatoren blieben trotz sepliste-XDATA komplett ohne "Nachbarn".
|
||||
|
||||
Architektur (nach dem Umbau "Proxy liefert Zeilen, sepliste nur
|
||||
Reihenfolge"): die in einer Kette verpackten Separatoren liegen bereits als
|
||||
echte (Proxy-)Separator_SP-Items vor (der LISP-Export fuegt je verpacktem
|
||||
Symbol eine temporaere Kopie ein). map_separator_kette_items erzeugt daher
|
||||
KEINE neuen Zeilen mehr, sondern ordnet jedem sepliste-SEP das vorhandene
|
||||
Separator-Item zu (Match ueber Wrapper-ID + INSERT-Punkt) und traegt es in
|
||||
sep_kette_by_key ein; die AS/ES-Zeilen kommen weiter aus
|
||||
build_strecke_schleus_items. resolve_umlauf_und_sepliste_nachbarn setzt
|
||||
dann die Nachbarn als ECHTE TeileIds (nicht die rohen sep_kette-Schluessel,
|
||||
Architektur: SEP-Eintraege liegen bereits als echte (Proxy-)Separator_SP-
|
||||
Items vor (der LISP-Export fuegt je verpacktem Symbol eine temporaere
|
||||
Kopie ein), AS-/ES-Eintraege als eigene Items, direkt aus dem dauerhaften
|
||||
ZZ_ASES_MARKER klassifiziert (siehe export_csv.py process_blocks) - beide
|
||||
tragen bereits ihre TeileId als sepliste-Feld "id". map_sepliste_kette_
|
||||
items erzeugt daher KEINE neuen Zeilen, sondern ordnet jedem sepliste-
|
||||
Eintrag sein vorhandenes Item exakt per ID zu und traegt es in
|
||||
sep_kette_by_key ein. resolve_umlauf_und_sepliste_nachbarn setzt dann die
|
||||
Nachbarn als ECHTE TeileIds (nicht die rohen sep_kette-Schluessel,
|
||||
frueherer Bug)."""
|
||||
|
||||
def _sepliste_kette(self):
|
||||
"""Ein Kreisel + eine Gefaellestrecke mit sepliste AS(1) -> SEP(2) ->
|
||||
ES(3) PLUS das bereits vorhandene Proxy-Separator-Item fuer den SEP
|
||||
(wie es der freie-Separator-Pfad in process_blocks erzeugt: TeileId,
|
||||
Merkmal "Zuordnung" = Wrapper-ID, INSERT-Punkt _x/_y an der sepliste-
|
||||
SEP-Koordinate). "nachbarn" auf der Strecke simuliert das Ergebnis von
|
||||
compute_neighbor_ids (BBox-Ueberschneidung Strecke <-> Kreisel) - die
|
||||
Vorbedingung, unter der compute_strecke_kreisel_schleus die AS/ES-
|
||||
Schleuselemente erzeugt."""
|
||||
ES(3), PLUS die drei bereits vorhandenen Items, die diese Eintraege
|
||||
vertreten (AS/ES ueber den ZZ_ASES_MARKER, SEP ueber den freien-
|
||||
Separator-Pfad in process_blocks) - jeder sepliste-Eintrag traegt
|
||||
bereits die TeileId seines Items als Feld "id" (siehe
|
||||
csv:asmark-ids-setzen/csv:sepliste-ids-setzen, Lisp/export.lsp)."""
|
||||
k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0)
|
||||
strecke = {
|
||||
"teileart": "ILS 2.0 Gefaellestrecke",
|
||||
@@ -220,13 +231,14 @@ class TestSeplisteNachbarnAufgeloest:
|
||||
"_bbox": {"cx": 5000.0, "cy": 0.0, "cz": 0.0, "dx": 8000.0, "dy": 300.0},
|
||||
"k1": "", "k2": "",
|
||||
"sepliste": [
|
||||
{"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0},
|
||||
{"typ": "SEP", "lfdnr": 2, "x": 5000.0, "y": 0.0},
|
||||
{"typ": "ES", "lfdnr": 3, "x": 8500.0, "y": 0.0},
|
||||
{"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0, "id": "0056"},
|
||||
{"typ": "SEP", "lfdnr": 2, "x": 5000.0, "y": 0.0, "id": "0055"},
|
||||
{"typ": "ES", "lfdnr": 3, "x": 8500.0, "y": 0.0, "id": "0057"},
|
||||
],
|
||||
}
|
||||
# Proxy-Separator-Item (bereits vorhanden), Zuordnung = Wrapper-ID 0010,
|
||||
# INSERT-Punkt an der sepliste-SEP-Koordinate (5000, 0).
|
||||
# SEP: Proxy-Separator-Item (INSERT-Punkt an der sepliste-SEP-
|
||||
# Koordinate - nur als Fallback-Feld gefuehrt, die Zuordnung selbst
|
||||
# laeuft exakt per ID).
|
||||
proxy_sep = {
|
||||
"teileart": "ILS 2.0 Separator",
|
||||
"teileid": "0055",
|
||||
@@ -235,7 +247,12 @@ class TestSeplisteNachbarnAufgeloest:
|
||||
"_bbox": {"cx": 5000.0, "cy": 0.0, "cz": 0.0, "dx": 150.0, "dy": 210.0},
|
||||
"nachbarn": "",
|
||||
}
|
||||
return [k, strecke, proxy_sep]
|
||||
# AS/ES: eigene Items wie aus dem ZZ_ASES_MARKER klassifiziert.
|
||||
as_item = _ases("ILS Ausschleuselement", "0056", 1500.0, 0.0, "0010")
|
||||
as_item["nachbarn"] = ""
|
||||
es_item = _ases("ILS Einschleuselement", "0057", 8500.0, 0.0, "0010")
|
||||
es_item["nachbarn"] = ""
|
||||
return [k, strecke, proxy_sep, as_item, es_item]
|
||||
|
||||
def test_keine_rohen_sep_kette_schluessel_in_nachbarn(self):
|
||||
from export_neighbors import compute_sep_kette
|
||||
@@ -246,20 +263,15 @@ class TestSeplisteNachbarnAufgeloest:
|
||||
touch_switches = []
|
||||
weiche_by_key = {}
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
|
||||
items, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
|
||||
touch_switches=touch_switches)
|
||||
|
||||
sep_kette_by_key = {}
|
||||
build_strecke_schleus_items(
|
||||
items, None, 100, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key)
|
||||
map_separator_kette_items(
|
||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
|
||||
|
||||
resolve_umlauf_und_sepliste_nachbarn(
|
||||
items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key)
|
||||
|
||||
# AS/ES (neu erzeugt) + der bereits vorhandene Proxy-Separator.
|
||||
# AS/ES + der bereits vorhandene Proxy-Separator.
|
||||
sepliste_items = [it for it in items
|
||||
if it.get("teileart") in
|
||||
("ILS 2.0 Separator", "ILS Ausschleuselement", "ILS Einschleuselement")]
|
||||
@@ -282,14 +294,9 @@ class TestSeplisteNachbarnAufgeloest:
|
||||
items = self._sepliste_kette()
|
||||
sep_kette = compute_sep_kette(items)
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
|
||||
items, tolerance_mm=50.0, durchmesser_mm=DURCHMESSER_MM,
|
||||
touch_switches=[])
|
||||
sep_kette_by_key = {}
|
||||
build_strecke_schleus_items(
|
||||
items, None, 100, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key)
|
||||
map_separator_kette_items(
|
||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
|
||||
resolve_umlauf_und_sepliste_nachbarn(
|
||||
items, kreisel_umlauf, sep_kette, sep_kette_by_key, {})
|
||||
|
||||
@@ -299,6 +306,8 @@ class TestSeplisteNachbarnAufgeloest:
|
||||
|
||||
# Der SEP-Schluessel muss auf das vorhandene Proxy-Separator-Item zeigen.
|
||||
assert sep_item["teileid"] == "0055", "sepliste-SEP nicht auf Proxy-Separator gemappt"
|
||||
assert as_item["teileid"] == "0056", "sepliste-AS nicht auf sein Item gemappt"
|
||||
assert es_item["teileid"] == "0057", "sepliste-ES nicht auf sein Item gemappt"
|
||||
|
||||
sep_nachbarn = {n.strip() for n in sep_item["nachbarn"].split(",") if n.strip()}
|
||||
assert sep_nachbarn == {as_item["teileid"], es_item["teileid"]}, (
|
||||
@@ -313,12 +322,7 @@ class TestSeplisteNachbarnAufgeloest:
|
||||
from export_neighbors import compute_sep_kette
|
||||
items = self._sepliste_kette()
|
||||
sep_kette = compute_sep_kette(items)
|
||||
sep_kette_by_key = {}
|
||||
build_strecke_schleus_items(
|
||||
items, None, 100, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key)
|
||||
map_separator_kette_items(
|
||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
|
||||
write_separatorliste_merkmale(items, sep_kette_by_key)
|
||||
|
||||
strecke = next(it for it in items if it.get("teileid") == "0010")
|
||||
@@ -363,7 +367,7 @@ class TestKreiselUmlaufStartetBeiNullGrad:
|
||||
# 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,
|
||||
umlauf = compute_kreisel_umlauf(items, 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}"
|
||||
@@ -374,7 +378,7 @@ class TestKreiselUmlaufStartetBeiNullGrad:
|
||||
# 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,
|
||||
umlauf = compute_kreisel_umlauf(items, 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}"
|
||||
@@ -396,7 +400,7 @@ class TestKreiselTrackidsSpalte:
|
||||
items = [k, sep_a, sep_b]
|
||||
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
write_kreisel_separatorliste_merkmale(
|
||||
items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={})
|
||||
|
||||
@@ -408,7 +412,7 @@ class TestKreiselTrackidsSpalte:
|
||||
k["merkmale"] = {}
|
||||
items = [k]
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette={}, tolerance_mm=0.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
items, tolerance_mm=0.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
write_kreisel_separatorliste_merkmale(
|
||||
items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={})
|
||||
assert "trackids" not in items[0]
|
||||
@@ -425,7 +429,7 @@ class TestKreiselTrackidsSpalte:
|
||||
items = [k, sep, bel, ent]
|
||||
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
items, 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}")
|
||||
@@ -447,7 +451,7 @@ class TestKreiselTrackidsSpalte:
|
||||
items = [k, sep, ein, aus]
|
||||
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette={}, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
keys = [p[0] for p in kreisel_umlauf["0001"]]
|
||||
assert set(keys) == {"0020", "0040", "0041"}, (
|
||||
f"Eingang/Ausgang fehlen im Kreisel-Umlauf: {keys}")
|
||||
@@ -455,3 +459,29 @@ class TestKreiselTrackidsSpalte:
|
||||
write_kreisel_separatorliste_merkmale(
|
||||
items, kreisel_umlauf, sep_kette_by_key={}, weiche_by_key={})
|
||||
assert k["trackids"] == "0041, 0020, 0040"
|
||||
|
||||
|
||||
class TestAsEsImKreiselUmlauf:
|
||||
"""Neu (Marker-Architektur): AS/ES sind jetzt ein ganz normaler
|
||||
KREISEL_UMLAUF_TEILEARTEN-Eintrag - ihre eigene Bounding-Box (vom
|
||||
ZZ_ASES_MARKER) entscheidet, ob sie im Umlauf eines Kreisels auftauchen,
|
||||
genau wie ein freier Separator. Keine gesonderte Naeherungslogik mehr."""
|
||||
|
||||
def test_as_in_reichweite_taucht_im_umlauf_auf(self):
|
||||
k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0)
|
||||
as_item = _ases("ILS Ausschleuselement", "0056", 300.0, 300.0, "0010")
|
||||
items = [k, as_item]
|
||||
umlauf = compute_kreisel_umlauf(
|
||||
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
keys = [p[0] for p in umlauf["0001"]]
|
||||
assert "0056" in keys, "AS-Item mit eigener BBox wurde nicht im Umlauf erkannt"
|
||||
|
||||
def test_es_ausserhalb_der_reichweite_fehlt_im_umlauf(self):
|
||||
k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0)
|
||||
es_item = _ases("ILS Einschleuselement", "0057", 20000.0, 0.0, "0010")
|
||||
items = [k, es_item]
|
||||
umlauf = compute_kreisel_umlauf(
|
||||
items, tolerance_mm=200.0, durchmesser_mm=DURCHMESSER_MM)
|
||||
keys = [p[0] for p in umlauf["0001"]]
|
||||
assert "0057" not in keys, (
|
||||
"ES-Item weit ausserhalb der Kreiselbox wurde faelschlich erkannt")
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
test_export_separatorliste.py - Merkmal "Separatorlist" einer Strecke.
|
||||
test_export_separatorliste.py - "TrackIds"-Spalte einer Strecke (VF_n/GF_n).
|
||||
Laeuft komplett ohne BricsCAD (reine Python-Logik, siehe lib/export_csv.py).
|
||||
|
||||
Eine Strecke (VF_n/GF_n) traegt in ihrer "sepliste"-XDATA nur x/y je AS-/SEP-/
|
||||
ES-Eintrag (Rohkoordinaten aus der Zeichnung, kein TeileId). write_separator-
|
||||
liste_merkmale() soll daraus, ueber die bereits von map_separator_kette_items
|
||||
per x/y-Abgleich aufgeloesten sep_kette_by_key-Eintraege, das Merkmal
|
||||
"Separatorlist" der Strecke fuellen: die TeileIds ihrer SEP-Eintraege (NICHT
|
||||
AS/ES) in Baureihenfolge, kommagetrennt (z.B. "0052, 0053, 0054").
|
||||
Eine Strecke (VF_n/GF_n) traegt in ihrer "sepliste"-XDATA je AS-/SEP-/ES-
|
||||
Eintrag x/y sowie (seit csv:asmark-ids-setzen/csv:sepliste-ids-setzen,
|
||||
Lisp/export.lsp) ein Feld "id" - die TeileId des Bauteils, das diesen
|
||||
Eintrag in der Zeichnung vertritt (AS/ES ueber den dauerhaften
|
||||
ZZ_ASES_MARKER, SEP ueber die beim Export ausgepackte temporaere
|
||||
Separator_SP-Kopie). write_separatorliste_merkmale() soll daraus, ueber die
|
||||
bereits von map_sepliste_kette_items per exaktem ID-Abgleich aufgeloesten
|
||||
sep_kette_by_key-Eintraege, die Spalte "TrackIds" der Strecke fuellen: die
|
||||
TeileIds von AS-Ende, SEP-Eintraegen und ES-Ende in Baureihenfolge,
|
||||
kommagetrennt (z.B. "0056, 0052, 0053, 0057").
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -17,11 +21,9 @@ import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
|
||||
|
||||
from export_neighbors import compute_sep_kette, compute_kreisel_umlauf # noqa: E402
|
||||
from export_neighbors import compute_sep_kette # noqa: E402
|
||||
from export_csv import ( # noqa: E402
|
||||
build_strecke_schleus_items,
|
||||
map_separator_kette_items,
|
||||
max_numeric_teileid,
|
||||
map_sepliste_kette_items,
|
||||
write_separatorliste_merkmale,
|
||||
)
|
||||
|
||||
@@ -40,13 +42,27 @@ def _kreisel(teileid, x0, y0, rotation, abstand=1000.0):
|
||||
}
|
||||
|
||||
|
||||
def _ases(teileart, teileid, x, y, zuordnung):
|
||||
"""AS-/ES-Item, wie es process_blocks direkt aus einem ZZ_ASES_MARKER
|
||||
klassifiziert - siehe test_export_kreisel_umlauf.py fuer dasselbe
|
||||
Fixture-Muster."""
|
||||
return {
|
||||
"teileart": teileart,
|
||||
"teileid": teileid,
|
||||
"_zuordnung": zuordnung,
|
||||
"merkmale": {},
|
||||
"nachbarn": "",
|
||||
"_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 200.0},
|
||||
}
|
||||
|
||||
|
||||
def _strecke_mit_zwei_separatoren():
|
||||
"""Ein Kreisel + eine Gefaellestrecke mit sepliste AS(1) -> SEP(2) ->
|
||||
SEP(3) -> ES(4), PLUS die zwei bereits vorhandenen Proxy-Separator-Items
|
||||
(wie sie der freie-Separator-Pfad in process_blocks erzeugt: TeileId,
|
||||
Merkmal "Zuordnung" = Wrapper-ID, INSERT-Punkt _x/_y an der jeweiligen
|
||||
sepliste-SEP-Koordinate) - siehe TestSeplisteNachbarnAufgeloest in
|
||||
test_export_kreisel_umlauf.py fuer dasselbe Fixture-Muster."""
|
||||
SEP(3) -> ES(4), PLUS die vier bereits vorhandenen Items, die diese
|
||||
Eintraege vertreten (zwei Proxy-Separator-Items wie sie der freie-
|
||||
Separator-Pfad in process_blocks erzeugt, sowie das AS-/ES-Item wie aus
|
||||
dem ZZ_ASES_MARKER klassifiziert) - jeder sepliste-Eintrag traegt bereits
|
||||
die TeileId seines Items als Feld "id"."""
|
||||
k = _kreisel("0001", 0.0, 0.0, 0.0, abstand=1000.0)
|
||||
strecke = {
|
||||
"teileart": "ILS 2.0 Gefaellestrecke",
|
||||
@@ -57,10 +73,10 @@ def _strecke_mit_zwei_separatoren():
|
||||
"k1": "", "k2": "",
|
||||
"merkmale": {"Anzahl_Separator": "2", "Anzahl_Scanner": "0"},
|
||||
"sepliste": [
|
||||
{"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0},
|
||||
{"typ": "SEP", "lfdnr": 2, "x": 4000.0, "y": 0.0},
|
||||
{"typ": "SEP", "lfdnr": 3, "x": 6000.0, "y": 0.0},
|
||||
{"typ": "ES", "lfdnr": 4, "x": 8500.0, "y": 0.0},
|
||||
{"typ": "AS", "lfdnr": 1, "x": 1500.0, "y": 0.0, "id": "0056"},
|
||||
{"typ": "SEP", "lfdnr": 2, "x": 4000.0, "y": 0.0, "id": "0052"},
|
||||
{"typ": "SEP", "lfdnr": 3, "x": 6000.0, "y": 0.0, "id": "0053"},
|
||||
{"typ": "ES", "lfdnr": 4, "x": 8500.0, "y": 0.0, "id": "0057"},
|
||||
],
|
||||
}
|
||||
proxy_sep_1 = {
|
||||
@@ -79,7 +95,9 @@ def _strecke_mit_zwei_separatoren():
|
||||
"_bbox": {"cx": 6000.0, "cy": 0.0, "cz": 0.0, "dx": 150.0, "dy": 210.0},
|
||||
"nachbarn": "",
|
||||
}
|
||||
return [k, strecke, proxy_sep_1, proxy_sep_2]
|
||||
as_item = _ases("ILS Ausschleuselement", "0056", 1500.0, 0.0, "0010")
|
||||
es_item = _ases("ILS Einschleuselement", "0057", 8500.0, 0.0, "0010")
|
||||
return [k, strecke, proxy_sep_1, proxy_sep_2, as_item, es_item]
|
||||
|
||||
|
||||
class TestSeparatorlisteMerkmal:
|
||||
@@ -87,53 +105,36 @@ class TestSeparatorlisteMerkmal:
|
||||
def test_separatorlist_in_baureihenfolge(self):
|
||||
items = _strecke_mit_zwei_separatoren()
|
||||
sep_kette = compute_sep_kette(items)
|
||||
compute_kreisel_umlauf( # AS/ES muessen als Punkte existieren, sonst
|
||||
items, sep_kette, tolerance_mm=50.0, # unveraendert fuer diesen Test
|
||||
durchmesser_mm=DURCHMESSER_MM, touch_switches=[])
|
||||
|
||||
sep_kette_by_key = {}
|
||||
build_strecke_schleus_items(
|
||||
items, None, 100, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key)
|
||||
map_separator_kette_items(
|
||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
|
||||
|
||||
write_separatorliste_merkmale(items, sep_kette_by_key)
|
||||
|
||||
# write_separatorliste_merkmale fuellt seit dem Umbau das Top-Level-Feld
|
||||
# write_separatorliste_merkmale fuellt das Top-Level-Feld
|
||||
# item["trackids"] (CSV-Spalte "TrackIds") mit AS-Ende + Separatoren +
|
||||
# ES-Ende in Baureihenfolge - NICHT mehr das Merkmal "Separatorlist" mit
|
||||
# nur den SEP-Eintraegen. AS/ES bekommen ueber build_strecke_schleus_items
|
||||
# eigene TeileIds (hier 0054/0055), die SEP-Proxies sind 0052/0053.
|
||||
# ES-Ende in Baureihenfolge. AS/ES tragen ihre eigene TeileId (0056/
|
||||
# 0057), die SEP-Proxies 0052/0053 - alle exakt per sepliste-Feld
|
||||
# "id" aufgeloest, kein Minten mehr noetig.
|
||||
strecke = next(it for it in items if it.get("teileid") == "0010")
|
||||
assert strecke.get("trackids") == "0054, 0052, 0053, 0055", (
|
||||
assert strecke.get("trackids") == "0056, 0052, 0053, 0057", (
|
||||
f'trackids = {strecke.get("trackids")!r}, '
|
||||
f'erwartet "0054, 0052, 0053, 0055" (AS, SEP, SEP, ES in Baureihenfolge)')
|
||||
f'erwartet "0056, 0052, 0053, 0057" (AS, SEP, SEP, ES in Baureihenfolge)')
|
||||
|
||||
def test_vertauschte_proxy_reihenfolge_in_items_aendert_nichts(self):
|
||||
"""Die Reihenfolge in "items" (bzw. in welcher Reihenfolge die Proxy-
|
||||
Separatoren im Export auftauchen) darf die Separatorlist-Reihenfolge
|
||||
nicht beeinflussen - massgeblich ist allein die lfdnr aus der
|
||||
sepliste, nicht die Item-Liste."""
|
||||
Separatoren im Export auftauchen) darf die TrackIds-Reihenfolge nicht
|
||||
beeinflussen - massgeblich ist allein die lfdnr aus der sepliste,
|
||||
nicht die Item-Liste."""
|
||||
items = _strecke_mit_zwei_separatoren()
|
||||
k, strecke, proxy_1, proxy_2 = items
|
||||
items = [k, strecke, proxy_2, proxy_1] # vertauscht
|
||||
k, strecke, proxy_1, proxy_2, as_item, es_item = items
|
||||
items = [k, strecke, proxy_2, proxy_1, es_item, as_item] # vertauscht
|
||||
|
||||
sep_kette = compute_sep_kette(items)
|
||||
compute_kreisel_umlauf(
|
||||
items, sep_kette, tolerance_mm=50.0,
|
||||
durchmesser_mm=DURCHMESSER_MM, touch_switches=[])
|
||||
sep_kette_by_key = {}
|
||||
build_strecke_schleus_items(
|
||||
items, None, 100, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key)
|
||||
map_separator_kette_items(
|
||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key)
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
|
||||
|
||||
write_separatorliste_merkmale(items, sep_kette_by_key)
|
||||
|
||||
strecke = next(it for it in items if it.get("teileid") == "0010")
|
||||
assert strecke.get("trackids") == "0054, 0052, 0053, 0055"
|
||||
assert strecke.get("trackids") == "0056, 0052, 0053, 0057"
|
||||
|
||||
def test_strecke_ohne_sepliste_bleibt_unveraendert(self):
|
||||
"""Altbestand ohne "sepliste"-XDATA darf kein leeres/kaputtes
|
||||
@@ -148,57 +149,32 @@ class TestSeparatorlisteMerkmal:
|
||||
assert "Separatorlist" not in strecke["merkmale"]
|
||||
|
||||
|
||||
class TestMarkerIdWiederverwendung:
|
||||
"""build_strecke_schleus_items darf die ID eines dauerhaften
|
||||
ZZ_ASES_MARKER (siehe csv:asmark-aktualisieren, Lisp/export.lsp; im
|
||||
sepliste-Eintrag als Feld "id" gespiegelt) nicht ignorieren und
|
||||
stattdessen eine neue erfinden - genau das erzeugte die vom Nutzer
|
||||
gemeldete TeileId-Luecke (Marker-ID blieb ungenutzt, gleichzeitig
|
||||
entstand fuer denselben Punkt eine zusaetzliche, hoehere ID)."""
|
||||
class TestMapSeplisteKetteItemsFallback:
|
||||
"""map_sepliste_kette_items faellt fuer SEP-Eintraege OHNE "id"-Feld
|
||||
(Altbestand-Export vor der Marker-Funktion) weiterhin auf den
|
||||
Naehe-Abgleich zurueck - AS/ES ohne "id" bleiben dagegen unaufgeloest
|
||||
(fuer sie existiert seit dem Wegfall der Kreisel<->Strecke-Kollisions-
|
||||
erzeugung kein Item mehr, das sie vertreten koennte)."""
|
||||
|
||||
def test_marker_id_wiederverwendet_statt_neu_gemintet(self):
|
||||
def test_sep_ohne_id_faellt_auf_naehe_zurueck(self):
|
||||
items = _strecke_mit_zwei_separatoren()
|
||||
strecke = next(it for it in items if it.get("teileid") == "0010")
|
||||
# AS = sepliste[0] (lfdnr=1), ES = sepliste[-1] (lfdnr=4) - siehe
|
||||
# ssg-sepliste-sammeln (Lisp/ssg_ks_insert.lsp): AS immer zuerst,
|
||||
# ES immer zuletzt. Bewusst NIEDRIGER als die Proxy-Separator-IDs
|
||||
# 0052/0053 gewaehlt - genau wie im real beobachteten HundM05-Export
|
||||
# (Marker 0024-0033, Separator-Proxies 0034-0045).
|
||||
strecke["sepliste"][0]["id"] = "0024"
|
||||
strecke["sepliste"][-1]["id"] = "0033"
|
||||
for entry in strecke["sepliste"]:
|
||||
if entry["typ"] == "SEP":
|
||||
entry["id"] = ""
|
||||
|
||||
sep_kette = compute_sep_kette(items)
|
||||
sep_kette_by_key = {}
|
||||
build_strecke_schleus_items(
|
||||
items, None, 100, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key)
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
|
||||
|
||||
as_item = sep_kette_by_key["0010#1"]
|
||||
es_item = sep_kette_by_key["0010#4"]
|
||||
assert as_item["teileid"] == "0024", (
|
||||
f'AS-Zeile hat teileid={as_item["teileid"]!r}, erwartet die '
|
||||
f'wiederverwendete Marker-ID "0024"')
|
||||
assert es_item["teileid"] == "0033", (
|
||||
f'ES-Zeile hat teileid={es_item["teileid"]!r}, erwartet die '
|
||||
f'wiederverwendete Marker-ID "0033"')
|
||||
assert sep_kette_by_key["0010#2"]["teileid"] == "0052"
|
||||
assert sep_kette_by_key["0010#3"]["teileid"] == "0053"
|
||||
|
||||
# Kein spaeterer Mint in DIESEM Lauf darf eine wiederverwendete
|
||||
# Marker-ID erneut vergeben - der laufende Zaehler muss mindestens
|
||||
# auf die hoechste wiederverwendete ID hochgezogen worden sein.
|
||||
assert max_numeric_teileid(items) >= 33
|
||||
|
||||
def test_strecke_ohne_marker_id_mintet_wie_bisher(self):
|
||||
"""Fehlt das "id"-Feld (Altbestand-Wrapper vor der Marker-Funktion,
|
||||
oder Marker-Zuweisung in Lisp fehlgeschlagen), bleibt das Mint-
|
||||
Verhalten unveraendert - dieselben TeileIds wie vor diesem Fix."""
|
||||
def test_as_ohne_id_bleibt_unaufgeloest(self):
|
||||
items = _strecke_mit_zwei_separatoren()
|
||||
sep_kette = compute_sep_kette(items)
|
||||
sep_kette_by_key = {}
|
||||
build_strecke_schleus_items(
|
||||
items, None, 100, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key)
|
||||
strecke = next(it for it in items if it.get("teileid") == "0010")
|
||||
strecke["sepliste"][0]["id"] = ""
|
||||
|
||||
as_item = sep_kette_by_key["0010#1"]
|
||||
es_item = sep_kette_by_key["0010#4"]
|
||||
assert as_item["teileid"] == "0054"
|
||||
assert es_item["teileid"] == "0055"
|
||||
sep_kette = compute_sep_kette(items)
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
|
||||
|
||||
assert "0010#1" not in sep_kette_by_key
|
||||
|
||||
Reference in New Issue
Block a user