docs: CSV-Export-Doku in doc/csv-export.md ausgelagert und mit Beispiel ergaenzt

Der lange Docstring in lib/export_csv.py wandert nach doc/csv-export.md
(inkl. durchgerechnetem Beispiel Kreisel+VarioFoerderer), der Docstring
verweist nur noch darauf.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-09-14 12:59:44 +02:00
co-authored by Claude Sonnet 5
parent 23d79ec354
commit c171bc44d6
2 changed files with 135 additions and 98 deletions
+127
View File
@@ -145,6 +145,133 @@ Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingb
Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
```
- **Position** = Mittenkoordinate x, y, z; **Boundingbox** = Ausdehnung
(Laenge, Breite, Hoehe) in x, y, z — beide aus `csv:get-bbox`
(`vla-getboundingbox`), siehe Phase 1.
- **Insertpoint** = KOS-String (24-Zeichen Base64, Position+Rotation als
Quaternion, `csv:kos-encode`) des Block-Einfuegepunkts selbst.
- **K1-K4** = Positions-Strings der im Block enthaltenen Koordinatensysteme
K1..K4 (12-Zeichen Base64, nur Position, `csv:trans-encode`) — nur bei
Omniflo Bogen/Weiche/Gerade und VF_n/GF_n (K1/K2 aus AS/ES) vorhanden,
sonst leer.
- **Fehler** = `"unverbunden"` (keine Partner, aber mindestens einer noetig),
`"nur ein Partner"` (ein Partner, aber mindestens zwei noetig) oder leer.
Kreisel/Eckrad/Omniflo brauchen mindestens einen Partner,
Gefaellestrecke/Foerderer/Strecke-Modul mindestens zwei
(`export_neighbors.MIN_PARTNER`).
- **Elementnummer** wird NICHT aus der TeileId uebernommen, sondern am Ende
neu von 1 an vergeben, nachdem alle Zeilen aufsteigend nach TeileId
sortiert wurden (`teileid_sort_key` in `process_blocks`). Ohne geloeschte
Elemente stimmen Elementnummer und TeileId dadurch ueberein; geloeschte
Elemente hinterlassen Luecken nur in der TeileId-Folge. Zeilen ohne
numerische TeileId (Omniflo-Sum-Zeile, SEP-/SCN-Fallback-IDs ohne
ID-Attribut) behalten stabil ihre bisherige Reihenfolge und stehen danach.
### Beispiel: Kreisel mit 2 Separatoren + VarioFoerderer mit 2 Separatoren
Ausgangslage in der Zeichnung: ein kleiner Kreisel (`KREISEL_1`, ID `0010`)
mit zwei frei aufgesetzten Separatoren auf seiner Bahn, daran andockend ein
`VF_1` mit einer Kette AS -> Separator -> Separator -> ES (IDs `0021`-`0024`).
**Ausschnitt `export_raw.json`** (von `csv:collect-export-blocks` /
`csv:block-to-json` geschrieben, hier gekuerzt auf die fuer dieses Beispiel
relevanten Felder):
```json
[
{
"block_name": "KREISEL_1",
"layer": "ILS_Kreisel",
"handle": "2A1",
"x": 10000.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0010", "DREHRICHTUNG": "UZS", "ABSTAND": "800"},
"insertpoint": "AAB3QAAATkAAAAAA...",
"bbox": {"min": [9200.0, 4200.0, 0.0], "max": [10800.0, 5800.0, 0.0]}
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2A5",
"x": 10800.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0011"},
"insertpoint": "AAD4QAAATkAAAAAA..."
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2A9",
"x": 10000.0, "y": 5800.0, "z": 0.0, "rotation": 90.0,
"attribs": {"ID": "0012"},
"insertpoint": "AAB3QAAAxEAAAAAA..."
},
{
"block_name": "VF_1",
"layer": "ILS_Foerderer",
"handle": "2B0",
"x": 10800.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0021", "Bezeichnung": ""},
"insertpoint": "AAD4QAAATkAAAAAA...",
"k1": "AAD4QAAATkAAAAAA...",
"k2": "AAAAQwAATkAAAAAA...",
"bbox": {"min": [10800.0, 4850.0, 0.0], "max": [13000.0, 5150.0, 300.0]},
"sepliste": [
["AS", 1, 10800.0, 5000.0],
["SEP", 2, 11600.0, 5000.0],
["SEP", 3, 12300.0, 5000.0],
["ES", 4, 13000.0, 5000.0]
]
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2B4",
"x": 11600.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0022"}
},
{
"block_name": "Separator_SP_300mm",
"layer": "ILS_Sensor",
"handle": "2B8",
"x": 12300.0, "y": 5000.0, "z": 0.0, "rotation": 0.0,
"attribs": {"ID": "0023"}
}
]
```
Die beiden Separatoren `0022`/`0023` sind hier die ECHTEN, top-level
exportierten Separator-Bloecke (identisch mit den `SEP`-Eintraegen der
`sepliste` von `VF_1` — siehe "Die `sepliste`" oben: kein doppeltes Konzept,
zwei Blickwinkel auf dieselben Symbole).
**Resultierende CSV-Zeilen** (Header + betroffene Zeilen, Spalten
`Insertpoint`/`K3`/`K4`/`Merkmale` fuer die Lesbarkeit gekuerzt):
```text
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
1;"ILS 2.0 Kreisel";"0010";"Kreisel :1";"B/3";1;"10000.0, 5000.0, 0.0";"1600.0, 1600.0, 0.0";"...";"";"";"";"";"0011, 0012, 0021";"0011, 0012, 0021, 0022, 0023, 0024";"";"";{"Abstand":"800"}
2;"ILS 2.0 Separator";"0011";"Separator :1";"B/3";1;"10800.0, 5000.0, 0.0";"210.0, 150.0, 14.0";"...";"";"";"";"";"0010, 0012";"";"";"";{"Zuordnung":"0010"}
3;"ILS 2.0 Separator";"0012";"Separator :2";"B/3";1;"10000.0, 5800.0, 0.0";"150.0, 210.0, 14.0";"...";"";"";"";"";"0011, 0021";"";"";"";{"Zuordnung":"0010"}
4;"ILS 2.0 VarioFoerderer";"0021";"VarioFoerderer :1";"B/3";1;"11900.0, 5000.0, 0.0";"2200.0, 300.0, 300.0";"...";"...";"...";"";"";"0010, 0024";"0021, 0022, 0023, 0024";"";"";{}
5;"ILS 2.0 Separator";"0022";"Separator :3";"C/4";1;"11600.0, 5000.0, 0.0";"210.0, 150.0, 14.0";"...";"";"";"";"";"";"";"";"";{"Zuordnung":"0021"}
6;"ILS 2.0 Separator";"0023";"Separator :4";"C/4";1;"12300.0, 5000.0, 0.0";"210.0, 150.0, 14.0";"...";"";"";"";"";"";"";"";"";{"Zuordnung":"0021"}
```
Zu beachten:
- Der Kreisel (`0010`) traegt in `TrackIds` den KOMPLETTEN Umlauf inkl. der
VF-Kettenenden (`0021`=AS, `0024`=ES, aus `compute_kreisel_umlauf`), waehrend
seine `Nachbarn`-Spalte nur die unmittelbaren Umlauf-Nachbarn zeigt.
- `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 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.
### 1. Blöcke klassifizieren
Eine grosse Schleife über alle rohen JSON-Blöcke ordnet jeden Block anhand
+8 -98
View File
@@ -3,111 +3,21 @@
"""
export_csv.py - Erzeugt eine einfache Item-Liste als CSV.
Fuer Omniflo-Elemente (Bogen/Weiche/Gerade) wird am Ende zusaetzlich eine
"Omniflo Sum"-Zeile mit Anzahl Boegen/Weichentypen und Gesamtlaenge AP110/AP60
angehaengt (siehe build_omni_sum_merkmale).
Nach der Kollisions-/Nachbarschaftserkennung wird ausserdem an jedem
Beruehrpunkt zweier Kreisel eine "ILS Weiche"-Zeile angehaengt (siehe
build_kreisel_weiche_items / export_neighbors.compute_kreisel_touch_switches):
Anzahl 1, Position = Beruehrkoordinaten, Nachbarn = die beiden Kreisel-IDs,
K1-K4/Insertpoint leer, Boundingbox = feste Element-Box (element_box_mm,
cfg/export.cfg [Boundingbox]),
mit der Laengsseite senkrecht zur Achse des beruehrenden Kreisels; die TeileId
zaehlt aus der hoechsten bereits vergebenen numerischen ID hoch.
Anschliessend (TeileId-Zaehlung laeuft ueber die Weichen hinweg weiter) wird
an jedem Ende einer Gefaellestrecke/eines Foerderers, an dem ein Kreisel
anschliesst, ein Aus- (Anfang) bzw. Einschleuselement (Ende) an der Kollisions-
position Kreisel<->Strecke angehaengt (siehe build_strecke_schleus_items /
export_neighbors.compute_strecke_kreisel_schleus) - Format wie die Weiche,
Nachbarn = Kreisel-ID + Strecken-ID. Zugeordnet wird je Streckenende der
naechstgelegene ihrer BBox-Kreisel-Nachbarn.
Warnungen = Spalte fuer Auffaelligkeiten. Aktuell: eine Strecke sollte genau
zwei Kreisel (Anfang + Ende) beruehren - beruehrt ihre (in x/y gepruefte)
Bounding-Box mehr, steht hier ein Hinweis und es wird zusaetzlich eine
Konsolen-WARNUNG ausgegeben (mit exakten 3D-BBoxen sollte das nicht auftreten).
Scanner-Zeilen bekommen dieselbe Spalte fuer strittige Distanz-Zuordnungen:
liegt ein Scanner ausserhalb jeder Kreisel-/Streckengruppen-/Gefaellestrecke-
Boundingbox und ist der Abstand zum naechsten und zweitnaechsten Carrier fast
gleich gross, schreibt Lisp/count_sep_scan.lsp (cs-zuordnung-lauf, vor dem
Export automatisch aufgerufen) einen Warnungstext mit den IDs beider
Kandidaten in das "warnung"-Feld des Scanner-Blocks im Roh-JSON (siehe
csv:block-to-json in export.lsp) - hier unveraendert uebernommen.
Aufruf:
python export_csv.py <export_raw.json> <data_dir> <output.csv>
CSV-Format:
CSV-Format (Header):
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;
Insertpoint;K1;K2;K3;K4;Nachbarn;TrackIds;Fehler;Warnungen;Merkmale
Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per
vla-getboundingbox) ermittelten Bounding-Box je Block:
Position = Mittenkoordinate x, y, z
Boundingbox = Ausdehnung (Laenge, Breite, Hoehe) in x, y, z
Insertpoint = KOS-String (24-Zeichen Base64, Position+Rotation als Quaternion,
siehe csv:kos-encode in export.lsp) des Block-Einfuegepunkts selbst.
K1-K4 = Positions-Strings der im Block enthaltenen Koordinatensysteme K1..K4
(12-Zeichen Base64, nur Position ohne Rotation, siehe csv:trans-encode;
nur bei Omniflo Bogen/Weiche/Gerade vorhanden, sonst leer).
Planquadrat wird aus der x/y-Koordinate des Blocks berechnet, siehe
export_planquadrat.py ([Planquadrate] in cfg/export.cfg).
Nachbarn = kommaseparierte TeileId-Liste ueberschneidender Elemente (Bounding-
Box-Ueberschneidung in der x/y-Ebene, reines Python ohne externe Abhaengigkeit,
siehe export_neighbors.py). Geprueft wird NICHT global ueber alle Elemente,
sondern in drei getrennten Gruppen: 1) Kreisel/Eckrad gegen Kreisel/Eckrad,
2) Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul (diese drei
nicht gegeneinander), 3) Omniflo-Elemente gegen Omniflo-Elemente. Fuer die
Omniflo-Gruppe liefert eine shapely-STRtree-Broad-Phase zunaechst alle Paare
mit ueberschneidenden Bounding-Boxen; anschliessend gelten sie nur dann als
benachbart, wenn sich ihre Anschluss-Koordinatensysteme K1-K4 tatsaechlich
beruehren (ein K-Punkt naeher als omniflo_ks_toleranz_mm, Default 10mm) -
das trennt bloss ueberlappende BBoxen von echten Anschluessen (Geraden fuehren
dafuer beim Export synthetisierte K1/K2, siehe csv:gerade-k-kos-strings).
Toleranzen und (Fallback-)Rastergroesse konfigurierbar ueber [Nachbarschaft]
in cfg/export.cfg.
TrackIds = kommaseparierte TeileId-Liste der Separatoren/BTMT-Beladung/
BTMT-Entladung (und bei einem Kreisel zusaetzlich der AS/ES-Streckenenden
und Kreisel-Kreisel-Weichen) entlang einer VF_n/GF_n-Strecke bzw. rund um
einen Kreisel, in Baureihenfolge (Strecke) bzw. Umlaufreihenfolge ab Winkel
0 Grad (Kreisel, siehe export_neighbors.compute_kreisel_umlauf) - nur bei
VF_n/GF_n/Kreisel vorhanden, sonst leer. Siehe write_separatorliste_
merkmale/write_kreisel_separatorliste_merkmale.
Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig),
"nur ein Partner" (ein Partner, aber mindestens zwei noetig) oder leer
(Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/
Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/
Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py
MIN_PARTNER). Ein Kreisel wird dabei NICHT als eine, sondern als zwei
BBoxen (links/rechts, geteilt entlang der Achse durch Antriebs-/
Spannstation, siehe export_neighbors.kreisel_half_bboxes) gegen
Gefaellestrecke/Foerderer getestet - deren "Nachbarn"-Spalte zeigt darum
"<KreiselTeileId>-L" bzw. "-R" statt nur der Kreisel-TeileId.
Separator/Scanner-Zeilen bekommen zusaetzlich ein berechnetes "Zuordnung"-
Merkmal (Gefaellestrecke > Foerderer > Kreiselhaelfte, per BBox-Ueber-
schneidung - siehe export_neighbors.compute_sensor_zuordnung), das die
ATTRIB-Zuordnung (falls das Attribut ZUORDNUNG in der Zeichnung von Hand
gesetzt wurde) ersetzt, sofern ein Treffer gefunden wird. Scanner-Zeilen
bekommen zusaetzlich ein "Naechster Separator"-Merkmal (raeumlich naechst-
gelegener Separator, euklidischer Abstand der BBox-Mittelpunkte - eigene,
von der Zuordnung unabhaengige Suche, siehe export_neighbors.compute_
scanner_nearest_separator).
Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer
EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/
K1-K4 stehen zwar in export_raw.json fuer jeden Export zur Verfuegung (siehe
csv:block-to-json), werden aber ebenfalls nur von diesem Skript (EXPORTCSV)
als CSV-Spalten ausgegeben.
Beispielzeile (Separator, TeileId 0011 an Kreisel 0010):
2;"ILS 2.0 Separator";"0011";"Separator :1";"B/3";1;
"10800.0, 5000.0, 0.0";"210.0, 150.0, 14.0";"...";"";"";"";"";
"0010, 0012";"";"";"";{"Zuordnung":"0010"}
Reihenfolge/Elementnummer: die Zeilen werden am Ende aufsteigend nach
TeileId sortiert (numerische IDs) und die Elementnummer (erste Spalte)
danach neu von 1 an vergeben (siehe teileid_sort_key in process_blocks) -
wurde kein Element geloescht, stimmen Elementnummer und TeileId dadurch
ueberein, geloeschte Elemente hinterlassen Luecken in der TeileId-Folge.
Zeilen ohne numerische TeileId (Omniflo-Sum-Zeile, SEP-/SCN-Fallback-IDs
ohne ID-Attribut) behalten stabil ihre bisherige Reihenfolge und stehen
danach.
Fuer die vollstaendige Herleitung jeder Spalte (Nachbarn/TrackIds/sepliste/
Fehler/Warnungen, inkl. eines durchgerechneten Beispiels Kreisel+VarioFoerderer
von export_raw.json bis zur fertigen CSV-Zeile) siehe doc/csv-export.md.
"""
import json