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:
Samer Ayadi
2026-09-22 14:48:45 +02:00
co-authored by Claude Sonnet 5
parent 719ffc2385
commit ab3958e0e4
5 changed files with 543 additions and 793 deletions
+87 -67
View File
@@ -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
View File
@@ -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
View File
@@ -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
+85 -55
View File
@@ -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")
+73 -97
View File
@@ -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