Files
dxfmakros/lib/export_sivas.py
T
y.wang cccd362bba feat: VF-Hoehe je Segment (Sivas 4433-4437) + AS-Zufuehrungsachse bei 30 Grad
Zwei Themen, die dieselben Dateien beruehren und daher zusammen gehen.

1) VF_HOEHE_m - Hoehe je VF-Segment, Sivas-Merkmale 4433-4437

   Sivas erwartet je Streckengruppe bis zu fuenf Variofoerderer-Hoehen.
   Neues Block-Attribut VF_HOEHE_m: kommagetrennte Liste in Metern mit 3
   Nachkommastellen, Eintrag fuer Eintrag parallel zu L_VF_m/VF_WINKEL.

   Je Eintrag der Mittelwert aus KS_EIN und KS_AUS DES GERADEN VF-Abschnitts.
   Zwischen Umlenk- und Motorstation koennen mehrere Abschnitte liegen
   (auf/ab/horizontal), jeder bekommt einen eigenen Wert.

   SONDERFALL Standard/Etage: dort gibt es nur einen Motor und nur einen
   geraden Abschnitt - gemessen wird die GANZE Kette (AS-KS_EIN bis
   ES-KS_AUS, vf-vfhoehe-kette). Das ersetzt den Segmentwert; der Linienzug
   laeuft nicht durch vf-dialog-berechnen-einfuegen und behaelt seine Liste.

   WICHTIG - vollstaendige Erfassung: die VF-Segmente entstehen NICHT alle in
   vfs-vf-koerper. Horizontale Zwischenstuecke, das "schon flach"-Segment und
   die Modus-2/3-Fuellstuecke bauen direkt mit insert-inclined-scaled-block,
   die Kette-Merge-Rekonstruktion liest sie aus bestehenden Bloecken. Ein
   erster Fix nur in vfs-vf-koerper lieferte darum zu wenige Werte (realer
   Befund: L_VF_m hatte drei Eintraege, VF_HOEHE_m einen). Jetzt sind alle 14
   Stellen erfasst, die vfl-acc-vf-seg aufrufen - dafuer das Paar
   vf-vfhoehe-merke/-schliesse, das ohne lokale Variable auskommt.

   REGEL (im Code dokumentiert): Jede Stelle, die vfl-acc-vf-seg aufruft,
   MUSS auch eine Hoehe erfassen - sonst verschiebt sich die Liste gegen
   L_VF_m und im Export landet eine Hoehe beim falschen Foerderer.

   Formatierung ueber vf-meter-3: (rtos x 2 3) allein kuerzt je nach DIMZIN
   nachfolgende Nullen ("2.5" statt "2.500", "-1" statt "-1.000") - im
   Testlauf real aufgetreten.

   Attribut-Schema *strecke-attr-gruppe* (ssg_core.lsp) ergaenzt, sonst
   verschluckt ssg-attrib-set-on den neuen Tag stillschweigend.
   export_sivas.py verteilt die Liste auf VF_HOEHE_MERKMALE (4433-4437);
   ueberzaehlige Abschnitte werden weggelassen statt zusammengefasst, fehlende
   lassen das Merkmal ganz weg.

2) AS-Element 30 Grad: Zufuehrungsachse statt Seitenwahl

   Der Startpunkt einer Kette mit AS-Element ist geometrisch keine Position,
   sondern eine ACHSE: das AS sitzt an einer durchlaufenden Strecke und
   schleust daraus aus, seine KS_EIN-Achse muss darauf liegen. Bisher richtete
   vfl-insert-as-element das KS_AUS entlang der Kettenrichtung aus, wodurch
   das KS_EIN um den Blockschwenk verdreht stand - beim 90-Grad-Element kaum
   sichtbar, beim 30-Grad-Element deutlich.

   Beim 30-Grad-Element wird die Seitenauswahl im AS-Gruppendialog jetzt
   ausgegraut; stattdessen gibt ein zweiter Punkt die Zufuehrungsrichtung
   (der Startpunkt bleibt der Sitz des Elements). Die Seite folgt aus der Lage
   des naechsten Kettenpunkts zur Achse (vfl-seite-aus-lage). 90 Grad bleibt
   unveraendert bei der Seitenfrage.

   Der Dialog legt bei 30 Grad bewusst KEINE Seite mehr in die Pending-Queue -
   sonst zog die nachfolgende Punktfrage sie als vermeintlichen Punkt heraus
   und erschien gar nicht (realer Fehler beim ersten Test).

   Abwaertskompatibilitaet: aeltere 30-Grad-Ketten wurden noch MIT Seitenfrage
   gebaut. vfl-replay-peek-art sieht beim Replay nach, was tatsaechlich
   ansteht ("PT" = Achse, "STR" = Seite) - die Modus-Wahl selbst wird nicht
   journalisiert, weil sie daraus eindeutig rekonstruierbar ist.

Unit-Suite 687/687 gruen. VF-Hoehe in BricsCAD bestaetigt. Die AS-Achsenfrage
ist dort noch NICHT gelaufen, seit die Seitenauswahl ausgegraut wird.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-21 12:55:02 +02:00

1166 lines
51 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
export_sivas.py - Erzeugt Sivas-Export-CSV mit gruppierten Zeilen und Summierungszeilen.
Neues CSV-Format:
Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details
- Bezeichnung: Wert des Attributs "Bezeichnung" (Kreisel/Eckrad/VF/GF/Strecke).
Bei gruppierten Zeilen (mehrere Bloecke je Zeile) kommasepariert.
Bei Omniflo-Zeilen (Bogen/Weiche/Gerade) und der Automation-Summenzeile
bleibt das Feld leer, da es sich um Sammlungen gleichartiger Objekte handelt.
- IDs: kommaseparierte Liste der ID-Attribute aller Bloecke der Zeile
(eigene Spalte, nicht mehr Teil des details-JSON).
- Omniflo (TEILEART "Omniflo ..."): je Nummer aus dem ARTINR-Attribut
(kommagetrennt) eine Zeile, Anzahl summiert; TEF-Teile als "Omniflo TEF"
- Omniflo Gerade: gruppiert nach ARTINR-Attribut, Laenge[mm] summiert
- ILS 2.0 Kreisel: gruppiert nach Kreiselart+Abstand+Anzahl Separatoren/
Scanner/Rampen, Anzahl summiert
- ILS 2.0 Gefaellestrecke: gruppiert nach identischen Massen, Anzahl-Spalte
bleibt 1 - die reale Stueckzahl steht im Merkmal 4311 (siehe
build_gefaellestrecke_details)
- ILS 2.0 Zusatzmodul: BTMT-Beladung UND Abwurfstationen in EINER Zeile,
Anzahl bleibt 1, Stueckzahlen stehen in 3503/3504 (siehe process_blocks)
- ILS 2.0 Automation: Summierungszeile (nur wenn ILS-Elemente vorhanden)
details-JSON: Schluessel sind durchgaengig Sivas-Merkmalnummern (z.B.
"2952" fuer Kreiselhoehe) statt beschreibender Texte - siehe
cfg/Merkmalsstamm_in_Sivas.xlsx fuer die Bezeichnung je Nummer.
Aufruf:
python export_sivas.py <export_raw.json> <data_dir> <output.csv> [--mit-bezeichnung]
Argumente:
export_raw.json - JSON-Datei mit allen INSERT-Entities aus der Zeichnung
data_dir - Pfad zum data/-Verzeichnis (derzeit ungenutzt, bleibt
fuer den Aufrufer export.lsp erhalten)
output.csv - Ziel-CSV-Datei (reine Merkmalnummern, fuer den
Sivas-Import)
--mit-bezeichnung - optional. Nur dann wird zusaetzlich zu output.csv
eine zweite Datei "<output>_mit_bezeichnung.csv"
geschrieben: gleicher Inhalt, aber jeder Merkmalnummer-
Key im details-JSON wird durch die zugehoerige Klartext-
Bezeichnung aus cfg/Merkmalsstamm_in_Sivas.xlsx ersetzt
(Wert bleibt unveraendert) - reine Lesehilfe zur
Pruefung, wird von Sivas selbst nicht eingelesen.
Schalter im Dialog CSV_EXPORT_KONFIG
("Sivas Export mit Bezeichner"), Standard aus.
Zusaetzlich wird IMMER "<output>_zusammengefasst.csv" geschrieben: gleiches
Format, aber Zeilen, die sich nur in Nr/Anzahl/IDs unterscheiden, sind zu
einer Zeile zusammengefasst (Anzahl addiert, IDs vereinigt, Nr neu) - siehe
items_zusammenfassen. output.csv selbst bleibt unveraendert.
"""
import json
import sys
import os
from export_blockpatterns import (load_patterns, load_export_cfg, cfg_path_from_env,
matches_any, csv_quote, safe_float, safe_int)
from merkmalstamm import lade_merkmalstamm, bezeichnung_fuer
import sivas_mapping
from export_neighbors import load_lisp_kreisel_weichen
BLOCKPATTERNS = load_patterns()
# Merkmalnummer fuer die Teilefarbe der Geruest-Baugruppen ([Sivas] in
# cfg/export.cfg; 108 = "Farbe" im Merkmalstamm).
MERKMAL_TEILEFARBE = load_export_cfg(cfg_path_from_env()).get(
"Sivas", "merkmal_teilefarbe", fallback="108").strip()
def load_json(path):
"""Laedt JSON, tolerant gegenueber BricsCAD-Ausgaben.
AutoLISP schreibt export_raw.json (write-line) im ANSI-Codepage des
Systems, nicht in UTF-8 - Sonderzeichen (z.B. Gradzeichen) koennen
daher nicht als UTF-8 dekodierbar sein. Fallback auf cp1252.
"""
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except UnicodeDecodeError:
with open(path, "r", encoding="cp1252") as f:
return json.load(f)
def get_id(block):
"""Liest das eindeutige ID-Attribut eines Blocks (von ssg-id-check-all vergeben)."""
return block.get("attribs", {}).get("ID", "")
# --- Sivas-Merkmalnummern: Anzeigetexte, die nicht 1:1 aus dem Attribut
# uebernommen werden koennen (Attribute bleiben ASCII, siehe CLAUDE.md-
# Konvention "Alle Texte in Dateien ohne Umlaute" - Sivas erwartet aber die
# deutsche Schreibweise mit Umlauten/Sonderzeichen). Tabelle kommt aus
# cfg/sivas.cfg [geruest_typ_anzeige], nicht ueber die uebliche ssg-text-
# Sprachdatei, da rein Sivas-seitig.
GERUEST_TYP_ANZEIGE = sivas_mapping.load_flat_mapping("geruest_typ_anzeige")
SEPARATOR_BEARBEITUNG_ANZEIGE = "ohne HARTCOAT HRC 50 ± 5"
# Sivas-Merkmale "Streckengruppe - Variofoerderer 001..005 - Hoehe in Meter".
# Reihenfolge = Bau-Reihenfolge der Foerdereinheiten in VF_HOEHE_m.
VF_HOEHE_MERKMALE = ("4433", "4434", "4435", "4436", "4437")
def geruest_typ_anzeige(wert):
"""Wandelt den ASCII-Attributwert GERUEST_TYP in die Sivas-Anzeige um."""
return GERUEST_TYP_ANZEIGE.get(wert, wert)
def ja_nein(flag_1_0):
"""Wandelt ein "0"/"1"-Attribut in die Sivas-Anzeige "Ja"/"Nein" um."""
return "Ja" if flag_1_0 == "1" else "Nein"
def mm_zu_m(wert_mm):
"""Wandelt ein mm-Attribut (z.B. Eckrad-HOEHE) in Meter um, gerundet wie
3512 beim Kreisel ("2000" -> "2.0")."""
return str(round(safe_float(wert_mm, 0.0) / 1000.0, 3))
# --- AS-/ES-Element (Merkmale 4427-4432) ---------------------------------
# Zuordnung komplett in cfg/sivas.cfg ([gefaellestrecke]/[variofoerderer]),
# hier stehen nur die Transforms. Die Sivas-Hierarchie ist: 4431/4432
# ("AS-Element"/"ES-Element", Ja/Nein) schalten die Unterknoten Winkel
# (4427/4429) und Seite (4428/4430) frei.
#
# Ob ein Element vorhanden ist, entscheidet AUSSCHLIESSLICH WINKEL_AS/
# WINKEL_ES: das gemeinsame Strecken-Schema (*strecke-attr-front* in
# ssg_core.lsp) laesst den Winkel leer, wenn kein Element gewaehlt wurde.
# SEITE_AS/SEITE_ES taugen dafuer NICHT - sie tragen mit "rechts" immer einen
# Schema-Vorgabewert. Ohne Element bleiben Winkel UND Seite leer, damit in
# Sivas keine Seite unter einem "Nein"-Knoten haengt.
# Attributwert -> Sivas-Optionswert der Auswahlliste ("30"/"90" -> "30 °"/
# "90 °", siehe Merkmalstamm). Wie [geruest_typ_anzeige] in der Config, damit
# die exakte Schreibweise ohne Code-Aenderung nachgezogen werden kann.
AS_ES_WINKEL_ANZEIGE = sivas_mapping.load_flat_mapping("as_es_winkel_anzeige")
def winkel_text(wert):
"""Elementwinkel ganzzahlig normiert ("90.0" -> "90"), ohne Sivas-
Anzeigeform. Basis fuer as_es_winkel UND fuer die Gruppierung (dort darf
die Anzeigeform keine Rolle spielen). Leerer Wert bleibt leer."""
wert = str(wert or "").strip()
if not wert:
return ""
zahl = safe_float(wert, None)
return wert if zahl is None else str(int(zahl))
def as_es_vorhanden(winkel):
"""Merkmal 4431/4432: "Ja", sobald ein Elementwinkel gesetzt ist."""
return "Ja" if winkel_text(winkel) else "Nein"
def as_es_winkel(winkel):
"""Merkmal 4427/4429: Elementwinkel in der Sivas-Schreibweise der
Auswahlliste (cfg/sivas.cfg [as_es_winkel_anzeige]). Ein Winkel ohne
Eintrag dort wird unveraendert (nur ganzzahlig normiert) uebernommen."""
winkel = winkel_text(winkel)
return AS_ES_WINKEL_ANZEIGE.get(winkel, winkel)
def _seite_text(seite, winkel):
"""Gemeinsamer Kern von as_seite/es_seite: Seite nur, wenn es das Element
ueberhaupt gibt."""
if not winkel_text(winkel):
return ""
return str(seite or "").strip().lower()
@sivas_mapping.braucht_attribs
def as_seite(wert, attribs):
"""Merkmal 4428: AS-Seite, aber nur bei vorhandenem AS-Element."""
return _seite_text(wert, attribs.get("WINKEL_AS", ""))
@sivas_mapping.braucht_attribs
def es_seite(wert, attribs):
"""Merkmal 4430: ES-Seite, aber nur bei vorhandenem ES-Element."""
return _seite_text(wert, attribs.get("WINKEL_ES", ""))
# --- Transform-Namen, wie sie in cfg/sivas.cfg hinter "|" stehen koennen ---
TRANSFORMS = {
"ja_nein": ja_nein,
"geruest_typ_anzeige": geruest_typ_anzeige,
"mm_zu_m": mm_zu_m,
"as_es_vorhanden": as_es_vorhanden,
"as_es_winkel": as_es_winkel,
"as_seite": as_seite,
"es_seite": es_seite,
}
def as_es_signatur(attribs):
"""AS-/ES-Anteil des Gruppierungsschluessels (siehe
gefaellestrecke_signature). Nutzt dieselben Transforms wie der Export,
damit Schluessel und Zeileninhalt nicht auseinanderlaufen: ohne Element
ist die Seite leer und darf dann auch nicht trennen."""
return (as_es_vorhanden(attribs.get("WINKEL_AS", "")),
winkel_text(attribs.get("WINKEL_AS", "")),
as_seite(attribs.get("SEITE_AS", ""), attribs),
as_es_vorhanden(attribs.get("WINKEL_ES", "")),
winkel_text(attribs.get("WINKEL_ES", "")),
es_seite(attribs.get("SEITE_ES", ""), attribs))
def streckengruppe_objekt_text(attribs):
"""Merkmal 4300 "Streckengruppe": "mehrere Objekte", wenn der Block ein
kombinierter Streckengruppe-Block (Gefaelle+Vario, TYP-Attribut
"Streckengruppe") ist, sonst "ein Objekt" (reine Gefaellestrecke)."""
typ = attribs.get("TYP", "Gefaellestrecke")
return "mehrere Objekte" if typ == "Streckengruppe" else "ein Objekt"
SIVAS_MAPPING_GEFAELLESTRECKE = sivas_mapping.load_section_mapping("gefaellestrecke")
def build_gefaellestrecke_details(block, anzahl=1):
"""Merkmale-Dict (Sivas-Merkmalnummern) fuer eine Gruppe gleichartiger
ILS Gefaellestrecken (GF_N-Bloecke). anzahl ist die Anzahl der in dieser
Zeile zusammengefassten, baugleichen Bloecke (4311) - anders als bei
Kreisel steht hier auf der Sivas-Hauptzeile selbst immer Anzahl=1 (siehe
process_blocks), die reale Stueckzahl steckt nur in 4311.
Die reine Attribut->Merkmal-Zuordnung kommt aus cfg/sivas.cfg
[gefaellestrecke]; 4300 (Streckengruppe-Text) und 4311 (Anzahl-Parameter)
sind berechnete/parametrisierte Werte und bleiben hier Code."""
attribs = block.get("attribs", {})
details = sivas_mapping.anwenden(SIVAS_MAPPING_GEFAELLESTRECKE, attribs, TRANSFORMS)
details["4300"] = streckengruppe_objekt_text(attribs)
details["4311"] = str(anzahl)
return details
def kreisel_signature(block):
"""
Schluessel zur Gruppierung gleichartiger Kreisel: Montagehoehe (Merkmal
2952), Kreiselart, Abstand, Anzahl der Separatoren, Anzahl der Scanner,
Anzahl der Rampen sowie Geruest fuer Einzelmodul/Geruestoption
entscheiden, ob Kreisel in derselben Sivas-Zeile summiert werden.
Die Hoehe wird numerisch verglichen ("4.820" == "4.82").
"""
attribs = block.get("attribs", {})
return (
round(safe_float(attribs.get("MONTAGEHOEHE_m", "0"), 0.0), 3),
attribs.get("KREISELART", "STANDARD"),
attribs.get("ABSTAND", "2300"),
attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")),
attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")),
attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")),
attribs.get("GERUEST_EINZELMODUL", "0"),
attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
)
def eckrad_signature(block):
"""
Schluessel zur Gruppierung gleichartiger Eckraeder: Hoehe (Merkmal 2950)
und Geruest fuer Einzelmodul/Geruestoption entscheiden, ob Eckraeder in
derselben Sivas-Zeile summiert werden.
"""
attribs = block.get("attribs", {})
return (
mm_zu_m(attribs.get("HOEHE", "0")),
attribs.get("GERUEST_EINZELMODUL", "0"),
attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
)
def gefaellestrecke_signature(block):
"""
Schluessel zur Gruppierung gleichartiger Gefaellestrecken: Bloecke mit
identischen Massen (Hoehe, Laenge, Winkel, Separatoren, Scanner, Typ,
Geruest fuer Einzelmodul/Geruestoption, AS-/ES-Element) landen in
derselben Sivas-Zeile - nur IDs/Bezeichnung unterscheiden sich.
AS-/ES-Element (Merkmale 4427-4432) gehoeren mit in den Schluessel: die
Merkmale der Zeile entstehen aus dem ERSTEN Block der Gruppe, zwei sonst
baugleiche Gefaellestrecken mit AS links bzw. AS rechts wuerden sonst zu
einer Zeile mit nur einer der beiden Seiten verschmelzen (siehe
as_es_signatur).
"""
attribs = block.get("attribs", {})
return (
attribs.get("HOEHE_VON_mm", "0"),
attribs.get("HOEHE_BIS_mm", "0"),
attribs.get("L_GF_m", ""),
attribs.get("GF_WINKEL", ""),
attribs.get("ANZAHL_SEPARATOR", "0"),
attribs.get("ANZAHL_SCANNER", "0"),
attribs.get("TYP", "Gefaellestrecke"),
attribs.get("GERUEST_EINZELMODUL", "0"),
attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
as_es_signatur(attribs),
)
def parse_float_list(csv_str):
"""Kommagetrennten Attribut-String (z.B. L_GF_m) in Liste von floats."""
out = []
for part in str(csv_str or "").split(","):
part = part.strip()
if not part:
continue
try:
out.append(float(part))
except ValueError:
pass
return out
def parse_str_list(csv_str):
"""Kommagetrennten Attribut-String (z.B. MOTORSEITE) in Liste von Strings."""
return [p.strip() for p in str(csv_str or "").split(",") if p.strip()]
SIVAS_MAPPING_VARIOFOERDERER = sivas_mapping.load_section_mapping("variofoerderer")
def build_variofoerderer_details(block):
"""
Merkmale-Dict (Sivas-Merkmalnummern) fuer eine VF_N-Streckengruppe
(VarioFoerderer, ggf. mehrsegmentig aus Gefaelle- und angetriebenen
Foerderer-Teilen). Sivas kennt je Streckengruppe nur EIN Gefaelle-
Segment-Paar (001/002) und EIN Vario-Segment (001) - ANZAHL_GF/ANZAHL_VF
ueber diese Grenze hinaus werden hier (noch) nicht abgebildet.
Die Aufteilung der Separatoren/Scanner auf Gefaelle-Anteil (4337/4339)
vs. Vario-Anteil (4381/4383) ist NOCH NICHT umgesetzt (haengt an der noch
ausstehenden Neuzuordnung von Separator/Scanner zum jeweiligen
Streckenabschnitt) - bis dahin steht 4337/4339 fest auf "0" und der volle
Attributwert steht auf 4381/4383.
Die von der Segmentanzahl UNABHAENGIGEN Felder (2950, GF_Bogen_*/
VF_Bogen_*-Zaehler, 4381-4383, 3992/3993) kommen aus cfg/sivas.cfg
[variofoerderer]. Die Gefaelle-/Vario-Segmentpaare (4312-4392, 4353/
4354/4386, abhaengig von len(gf_laengen)/len(vf_laengen)) bleiben Code.
"""
attribs = block.get("attribs", {})
gf_laengen = parse_float_list(attribs.get("L_GF_m", ""))
gf_winkel = parse_float_list(attribs.get("GF_WINKEL", ""))
vf_laengen = parse_float_list(attribs.get("L_VF_m", ""))
vf_winkel = parse_float_list(attribs.get("VF_WINKEL", ""))
vf_richtung = parse_str_list(attribs.get("ANTRIEBFAHRTRICHTUNG", ""))
motorseiten = parse_str_list(attribs.get("MOTORSEITE", ""))
details = sivas_mapping.anwenden(SIVAS_MAPPING_VARIOFOERDERER, attribs, TRANSFORMS)
details["4300"] = "mehrere Objekte"
details["4311"] = "1"
if len(gf_laengen) >= 1:
details["4312"] = "Ja"
details["4318"] = gf_laengen[0]
details["4391"] = str(int(gf_winkel[0])) if gf_winkel else ""
else:
details["4312"] = "Nein"
if len(gf_laengen) >= 2:
details["4314"] = "Ja"
details["4319"] = gf_laengen[1]
details["4392"] = str(int(gf_winkel[1])) if len(gf_winkel) >= 2 else ""
else:
details["4314"] = "Nein"
details["4337"] = "0"
details["4338"] = SEPARATOR_BEARBEITUNG_ANZEIGE
details["4339"] = "0"
details["4340"] = "Ja" if vf_laengen else "Nein"
details["4341"] = sum(1 for seite in motorseiten if seite.lower() == "links")
details["4342"] = sum(1 for seite in motorseiten if seite.lower() == "rechts")
if vf_laengen:
details["4353"] = vf_laengen[0]
details["4354"] = vf_richtung[0].lower() if vf_richtung else ""
details["4386"] = str(int(vf_winkel[0])) if vf_winkel else ""
# Hoehe je Foerdereinheit (4433-4437, "Variofoerderer 001-005 - Hoehe in
# Meter"). Quelle ist das Attribut VF_HOEHE_m: eine kommagetrennte Liste
# in Bau-Reihenfolge, je Eintrag der Mittelwert aus KS_EIN der
# Umlenkstation und KS_AUS der Motorstation (siehe vf-vfhoehe-anfang/-ende
# in Lisp/vf_core.lsp). Einheit Meter, 3 Nachkommastellen - wie L_VF_m.
#
# Mehr als fuenf Foerdereinheiten kann Sivas hier nicht abbilden; die
# ueberzaehligen werden bewusst weggelassen statt zusammengefasst, damit
# kein Merkmal einen erfundenen Wert traegt. Fehlende Eintraege bleiben
# ganz weg (kein leeres Merkmal).
vf_hoehen = parse_float_list(attribs.get("VF_HOEHE_m", ""))
for nummer, hoehe in zip(VF_HOEHE_MERKMALE, vf_hoehen):
details[nummer] = "%.3f" % hoehe
return {k: str(v) for k, v in details.items()}
SIVAS_MAPPING_KREISEL = sivas_mapping.load_section_mapping("kreisel")
SIVAS_MAPPING_ECKRAD = sivas_mapping.load_section_mapping("eckrad")
SIVAS_MAPPING_GERUEST = sivas_mapping.load_section_mapping("geruest")
SIVAS_MAPPING_WEICHEN = sivas_mapping.load_section_mapping("weichen")
def build_kreisel_details(block):
"""Merkmale-Dict (Sivas-Merkmalnummern) fuer eine Gruppe gleichartiger
ILS Kreisel. 3511 ("Kreisel - zu fertigende Menge") steht bewusst fest
auf "1" - die reale Stueckzahl der Gruppe steht auf der Sivas-Hauptzeile
selbst (Anzahl-Spalte, siehe kreisel_groups in process_blocks).
Die reine Attribut->Merkmal-Zuordnung kommt aus cfg/sivas.cfg [kreisel];
3510 (Kreiselart-Anzeigetext) und 3512 (Abstand mm->m, gerundet) sind
berechnete Werte und bleiben hier Code."""
attribs = block.get("attribs", {})
abstand_m = str(round(safe_float(attribs.get("ABSTAND", "2300"), 2300.0) / 1000.0, 2))
kreiselart = "Kreisel mit PINBAND" if attribs.get("KREISELART", "STANDARD") == "PIN" else "Kreisel"
details = sivas_mapping.anwenden(SIVAS_MAPPING_KREISEL, attribs, TRANSFORMS)
details["3510"] = kreiselart
details["3512"] = abstand_m
return details
def build_eckrad_details(block):
"""Merkmale-Dict fuer eine Gruppe gleichartiger ILS Eckraeder.
Vollstaendig aus cfg/sivas.cfg [eckrad] (2950 per Transform mm_zu_m)."""
attribs = block.get("attribs", {})
return sivas_mapping.anwenden(SIVAS_MAPPING_ECKRAD, attribs, TRANSFORMS)
# def build_geruest_details(block):
#"""Merkmale-Dict fuer eine GR_-Geruest-Baugruppe (siehe pattern_geruest in
#cfg/export.cfg, GeruestInsert.lsp *geruest-attrib-defs*). ARTINR/
#BEZEICHNUNG/TEILEART/LAENGE wandern in feste Sivas-Spalten (sivasnr/
#bezeichnung/teileart/laenge_mm), FARBE bleibt als Rest-Attribut hier
#(cfg/sivas.cfg [geruest]). LAYER ist reine BricsCAD-Zeichnungsinformation
#ohne Sivas-Bedeutung und wird bewusst NICHT mit exportiert (anders als im
#CSV-Export export_csv.py, wo sie fuer die Zuordnung in der Zeichnung
#nuetzlich bleibt)."""
#attribs = block.get("attribs", {})
#return sivas_mapping.anwenden(SIVAS_MAPPING_GERUEST, attribs, TRANSFORMS)
def build_geruest_details(farbe):
"""Merkmale-Dict fuer EINE Artikelnummer einer GR_-Geruest-Baugruppe
(siehe pattern_geruest in cfg/export.cfg, GeruestInsert.lsp
*geruest-attrib-defs*). ARTINR/BEZEICHNUNG/TEILEART/LAENGE wandern in
feste Sivas-Spalten (sivasnr/bezeichnung/teileart/laenge_mm); hier bleibt
nur die Teilefarbe dieser Artikelnummer als Merkmal MERKMAL_TEILEFARBE -
ohne Farbe (Teil darf keine tragen) ein leeres Dict. LAYER ist reine
BricsCAD-Zeichnungsinformation ohne Sivas-Bedeutung und wird bewusst
NICHT mit exportiert (anders als im CSV-Export export_csv.py, wo sie fuer
die Zuordnung in der Zeichnung nuetzlich bleibt)."""
return {MERKMAL_TEILEFARBE: farbe} if farbe else {}
def geruest_artinr_farben(attribs):
"""(artinr, farbe)-Paare eines GR_-Blocks. TEILEFARBE ist eine
kommagetrennte Liste parallel zu ARTINR (GeruestInsert.lsp,
geruest-farbliste): je Artikelnummer die Baugruppenfarbe oder "".
Passt die Laenge nicht (Altbestand mit einem Einzelwert oder von Hand
geaendertes Attribut), wird gewarnt und ohne Farbe exportiert - eine
geratene Zuordnung waere schlimmer als keine. Leere Artikelnummern
werden verworfen."""
artinr_roh = attribs.get("ARTINR", "").split(",")
farben_roh = attribs.get("TEILEFARBE", "")
farben = [f.strip() for f in farben_roh.split(",")]
if farben_roh.strip() == "":
farben = [""] * len(artinr_roh)
elif len(farben) != len(artinr_roh):
print(f"WARNUNG: Geruest-Block ID {attribs.get('ID', '?')}: TEILEFARBE "
f"({len(farben)} Eintraege) passt nicht zu ARTINR "
f"({len(artinr_roh)} Eintraege) - ohne Farbe exportiert.",
file=sys.stderr)
farben = [""] * len(artinr_roh)
return [(a.strip(), f) for a, f in zip(artinr_roh, farben) if a.strip()]
# --- Bekannte Bloecke ignorieren ---
SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"]))
# Eigenstaendige Separator_SP/S-LP- und Scanner-Bloecke werden von der Lisp-
# Sammlung (csv:collect-export-blocks) zwar mit in export_raw.json geschrieben
# (export_csv.py gibt ihnen dort eigene TeileArt-Zeilen), aber HIER bewusst
# NICHT in eine eigene Sivas-Zeile uebernommen: ihre Stueckzahl wird bereits
# ueber die ANZAHL_SEPARATOR/ANZAHL_SCANNER-Attribute des zugehoerigen
# Kreisel/GF/VF-Blocks gezaehlt (siehe kreisel_signature/build_*_details
# oben) - ein zusaetzlicher eigener Eintrag wuerde doppelt zaehlen.
#
# Dieselbe Ueberlegung gilt fuer die ausgepackten VF_n/GF_n-Sub-Komponenten
# (Boegen, gerade Foerderer-/Gefaellestrecken-Segmente, Umlenk-/Motorstation -
# siehe csv:element-proxies-erzeugen, export.lsp; nur im Rohdaten-Modus
# *csv-export-alle-elemente*=T ueberhaupt in export_raw.json vorhanden) sowie
# fuer die gruppierten Foerdereinheit-/Gefaelleeinheit-Marker (ZZ_UNIT_MARKER,
# csv:ketten-verarbeiten, Default-Modus): ihre Stueckzahl steckt bereits
# in den GF_Bogen_*/VF_Bogen_*/ANZAHL_VF/ANZAHL_GF-Attributen des Wrappers
# (*strecke-attr-gruppe*, ssg_core.lsp) - ein eigener Sivas-Eintrag wuerde
# ebenso doppelt zaehlen. Muss VOR der pattern_strecke_module-Pruefung unten
# greifen, sonst faengt deren generisches "ILS 2.0 Strecke"-Sammeln diese
# Kopien zuerst ab.
SEP_SCANNER_PATTERNS = (
BLOCKPATTERNS.get("pattern_separator", []) + BLOCKPATTERNS.get("pattern_scanner", [])
+ BLOCKPATTERNS.get("pattern_nested_bogen", [])
+ BLOCKPATTERNS.get("pattern_nested_streckenelement", [])
+ BLOCKPATTERNS.get("pattern_nested_umlenkstation", [])
+ BLOCKPATTERNS.get("pattern_nested_motorstation", [])
+ BLOCKPATTERNS.get("pattern_unit_marker", [])
)
# Sivas-Nummern und Bezeichnungen je ILS-Elementtyp
ILS_SIVASNR = {
"kreisel": ("6200", "ILS 2.0 Kreisel"),
"eckrad": ("6210", "ILS 2.0 Eckrad"),
"zusatzmodul": ("6215", "ILS 2.0 Zusatzmodul"),
"ils_weiche": ("6216", "ILS 2.0 Weiche"),
"strecke": ("6220", "ILS 2.0 Strecke"),
"gefaellestrecke": ("6220", "ILS 2.0 Gefaellestrecke"),
"automation": ("6269", "ILS 2.0 Automation und Pneumatik - Gesamtanlage"),
}
def process_blocks(blocks, anzahl_weichen_ils=0):
# anzahl_weichen_ils = Anzahl Kreisel-Beruehrpunkte aus dem LISP-Export
# (kreisel_weichen.json, siehe main) -> Merkmal 4403 der Automation-Zeile.
"""
Verarbeitet alle Bloecke.
Omniflo-Elemente werden ueber das TEILEART-Attribut ("Omniflo ...")
erkannt, ihre Sivas-Nummern kommen - wie beim Geruest - ausschliesslich
aus dem ARTINR-Attribut (kommagetrennt, TEF-Teile dort bereits
aufgeloest). Ein Katalog-Lookup ueber den Blocknamen findet nicht statt.
Rueckgabe:
items - Liste der CSV-Zeilen als Dicts
counters - Zaehler fuer ILS-Automation-Summierungszeile
"""
# Omniflo: artinr -> {teileart, anzahl, ids}
omniflo_groups = {}
# Gerade: artinr_str -> {anzahl, laenge_mm_total, ids}
gerade_groups = {}
kreisel_list = [] # Liste von blocks
eckrad_groups = {} # eckrad_signature(block) -> {"ids", "bezeichnungen", "block"}
strecke_ids = [] # IDs loser Strecke-Elemente (Vario/Staustrecke ausserhalb VF_N)
strecke_bezeichnungen = [] # Bezeichnung-Attribut loser Strecke-Elemente
vario_list = [] # Liste von VarioFoerderer-Compound-Bloecken (VF_N), je einer Zeile
gefaelle_list = [] # Liste von Gefaellestrecke-Bloecken (GF_*), je einer Zeile
btmt_beladung_ids = [] # IDs aller BTMT-Beladung*-Bloecke (eine gruppierte Zeile)
btmt_entladung_ids = [] # IDs aller SC_Entladung*-Bloecke (eine gruppierte Zeile)
geruest_list = [] # Liste von GR_-Geruest-Baugruppenbloecken, je einer Zeile
counters = {
"anzahl_kreisel": 0,
"anzahl_kreisel_pinband": 0,
"sep_kreisel_pin": 0,
"sep_kreisel_ohne_pin": 0,
"anzahl_verbindungskreisel": 0,
"laengen_kreisel": 0.0,
"anzahl_weichen_ils": anzahl_weichen_ils,
"anzahl_eckraeder": 0,
"anzahl_gefaellestrecken": 0,
"anzahl_variofoerderer": 0,
"laengen_gefaellestrecken": 0.0,
"anzahl_motoren_vario": 0,
"laengen_variofoerderer": 0.0,
"anzahl_etf": 0,
"laengen_etf": 0.0,
"anzahl_separatoren": 0,
"anzahl_scanner": 0,
"anzahl_btbeladung": 0,
"anzahl_abwurfstationen": 0,
"anzahl_geruest": 0,
}
for block in blocks:
bname = block.get("block_name", "")
if bname in SKIP_BLOCKS:
continue
# Separator/Scanner: siehe Kommentar bei SEP_SCANNER_PATTERNS oben.
if matches_any(bname, SEP_SCANNER_PATTERNS):
continue
# --- BTMT-Beladestation ---
# Anders als Separator/Scanner wird sie von keinem Carrier-Attribut
# mitgezaehlt, darum eigene (gruppierte) Zeile UND eigener Zaehler in
# der ILS-Automation-Summierungszeile (siehe unten).
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_beladung", [])):
btmt_beladung_ids.append(get_id(block))
counters["anzahl_btbeladung"] += 1
continue
# --- BTMT-Entladestation (Abwurfstation) ---
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_entladung", [])):
btmt_entladung_ids.append(get_id(block))
counters["anzahl_abwurfstationen"] += 1
continue
# --- Eingangs-/Ausgangssymbol (Materialfluss-Ein-/Ausgang) ---
# Bewusst NUR im CSV-Export als eigene Zeile (siehe export_csv.py) -
# hier ohne Sivas-Zeile und ohne eigenen Zaehler ueberspringen. Ein
# Eingangs-/Ausgangssymbol ist eine reine Fluss-Markierung ohne Sivas-
# Artikel; ein eigener Sivas-Eintrag wuerde die Stueckliste verfaelschen
# (Nutzeranforderung: Eingang/Ausgang nur im CSV-Export).
if matches_any(bname, BLOCKPATTERNS.get("pattern_eingang", [])) or \
matches_any(bname, BLOCKPATTERNS.get("pattern_ausgang", [])):
continue
# --- Geruest-Baugruppe ---
# Anders als BTMT/Separator je Block eine EIGENE Zeile statt gruppiert:
# ARTINR (Sivas-IDs) und Laenge/Farbe koennen von Block zu Block
# unterschiedlich sein (freie Basis-Variante+Anbauteile-Auswahl je
# GERUEST_*-Aufruf), eine Gruppierung wuerde das verschleiern - wie
# bei VF_N/Kreisel oben, die aus demselben Grund ebenfalls einzeln
# bleiben.
if matches_any(bname, BLOCKPATTERNS.get("pattern_geruest", [])):
geruest_list.append(block)
counters["anzahl_geruest"] += 1
continue
# --- Omniflo Gerade (AP110) ---
# VOR dem allgemeinen Omniflo-Zweig: Geraden tragen ggf. ebenfalls
# TEILEART "Omniflo ...", brauchen aber die Laengensumme.
if matches_any(bname, BLOCKPATTERNS.get("pattern_gerade", [])):
attribs = block.get("attribs", {})
artinr = attribs.get("ARTINR", "")
key = artinr if artinr else "AP110"
laenge_attr = attribs.get("LAENGE") or attribs.get("A")
laenge = safe_float(laenge_attr, 2000.0)
if key not in gerade_groups:
gerade_groups[key] = {
"anzahl": 0, "laenge_mm": 0.0,
"artinr": artinr, "ids": [],
}
gerade_groups[key]["anzahl"] += 1
gerade_groups[key]["laenge_mm"] += laenge
gerade_groups[key]["ids"].append(get_id(block))
continue
# --- Omniflo-Element (Bogen, Weiche, Ladeschleife, TEF, ...) ---
# Erkennung ueber TEILEART, Sivas-Nummern aus ARTINR - wie beim
# Geruest eine Zeile je Nummer, hier aber ueber ALLE Bloecke
# gruppiert. Die DWGs fuehren TEF-Teile bereits aufgeloest im ARTINR
# (z.B. "834372002, 0_B10090, 0_B10090"). TEF-Teile ("0_B*") laufen
# unter "Omniflo TEF", damit dasselbe Teil aus Bogen und Weiche EINE
# Zeile bleibt.
attribs = block.get("attribs", {})
teileart = attribs.get("TEILEART", "")
if teileart.startswith("Omniflo"):
artinr_list = [a.strip() for a in attribs.get("ARTINR", "").split(",") if a.strip()]
block_id = get_id(block)
if not artinr_list:
print(f"[export_sivas] WARNUNG: Omniflo-Block {bname} (ID {block_id}) "
f"ohne ARTINR - nicht exportiert.", file=sys.stderr)
continue
for i, artinr in enumerate(artinr_list):
comp_id = f"{block_id}.t{i+1}" if len(artinr_list) > 1 else block_id
if artinr not in omniflo_groups:
omniflo_groups[artinr] = {
"teileart": "Omniflo TEF" if artinr.startswith("0_B") else teileart,
"anzahl": 0, "ids": [],
}
omniflo_groups[artinr]["anzahl"] += 1
omniflo_groups[artinr]["ids"].append(comp_id)
continue
# --- ILS Eckrad (vor Kreisel pruefen: pattern_kreisel schliesst
# ECKRAD_* mit ein, pattern_eckrad ist die praezisere Teilmenge) ---
if matches_any(bname, BLOCKPATTERNS.get("pattern_eckrad", [])):
counters["anzahl_eckraeder"] += 1
sig = eckrad_signature(block)
if sig not in eckrad_groups:
eckrad_groups[sig] = {"ids": [], "bezeichnungen": [], "block": block}
eckrad_groups[sig]["ids"].append(get_id(block))
eckrad_groups[sig]["bezeichnungen"].append(
block.get("attribs", {}).get("Bezeichnung", ""))
continue
# --- ILS Kreisel ---
if matches_any(bname, BLOCKPATTERNS.get("pattern_kreisel", [])):
kreisel_list.append(block)
attribs = block.get("attribs", {})
counters["anzahl_kreisel"] += 1
n_sep = safe_int(attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")) or "2", 2)
kreiselart = attribs.get("KREISELART", "STANDARD")
if kreiselart == "PIN":
counters["anzahl_kreisel_pinband"] += 1
counters["sep_kreisel_pin"] += n_sep
else:
counters["sep_kreisel_ohne_pin"] += n_sep
abstand_mm = safe_float(attribs.get("ABSTAND", "2300") or "2300", 2300.0)
counters["laengen_kreisel"] += abstand_mm / 1000.0
counters["anzahl_separatoren"] += n_sep
counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")) or "0")
continue
# --- VarioFoerderer Compound-Block (VF_N) ---
# Bekommt immer eine eigene Detail-Zeile (vario_list) statt der
# generischen IDs-Gruppierung - siehe build_variofoerderer_details.
if matches_any(bname, BLOCKPATTERNS.get("pattern_variofoerderer", [])):
attribs = block.get("attribs", {})
counters["anzahl_variofoerderer"] += 1
vario_list.append(block)
counters["laengen_variofoerderer"] += safe_float(attribs.get("L_VF_m", "0") or "0")
counters["anzahl_separatoren"] += safe_int(attribs.get("ANZAHL_SEPARATOR", "0") or "0")
counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", "0") or "0")
counters["anzahl_motoren_vario"] += safe_int(attribs.get("ANZAHL_VF", "0") or "0")
continue
# --- Gefaellestrecke Compound-Block (GF_N) ---
# Eigener Typ "ILS 2.0 Gefaellestrecke" - NICHT als Variofoerderer zaehlen.
if matches_any(bname, BLOCKPATTERNS.get("pattern_gefaellestrecke", [])):
attribs = block.get("attribs", {})
counters["anzahl_gefaellestrecken"] += 1
gefaelle_list.append(block)
counters["laengen_gefaellestrecken"] += safe_float(attribs.get("L_GF_m", "0") or "0")
counters["anzahl_separatoren"] += safe_int(attribs.get("ANZAHL_SEPARATOR", "0") or "0")
counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", "0") or "0")
continue
# --- ILS Strecke-Module (Vario/Staustrecke, einzelne Komponenten in Modelspace) ---
if matches_any(bname, BLOCKPATTERNS.get("pattern_strecke_module", [])):
counters["anzahl_variofoerderer"] += 1
strecke_ids.append(get_id(block))
strecke_bezeichnungen.append(block.get("attribs", {}).get("Bezeichnung", ""))
# --- Zeilen aufbauen ---
items = []
nr = 1
for sivasnr, g in omniflo_groups.items():
items.append({
"nr": nr,
"teileart": g["teileart"],
"sivasnr": sivasnr,
"sivasnr_quoted": False,
"bezeichnung": "",
"dispgruppe": 10,
"anzahl": g["anzahl"],
"laenge_mm": "",
"ids": g["ids"],
"details": {},
})
nr += 1
for key, g in gerade_groups.items():
items.append({
"nr": nr,
"teileart": "Omniflo Gerade",
"sivasnr": g["artinr"],
"sivasnr_quoted": True,
"bezeichnung": "",
"dispgruppe": 10,
"anzahl": g["anzahl"],
"laenge_mm": int(g["laenge_mm"]),
"ids": g["ids"],
"details": {},
})
nr += 1
# Gleichartige Kreisel (gleiche Kreiselart und gleicher Abstand) zu einer
# Zeile zusammenfassen - nur IDs/Bezeichnung bleiben je Block unterschiedlich.
kreisel_groups = {}
for block in kreisel_list:
sig = kreisel_signature(block)
if sig not in kreisel_groups:
kreisel_groups[sig] = {"ids": [], "bezeichnungen": [], "block": block}
kreisel_groups[sig]["ids"].append(get_id(block))
kreisel_groups[sig]["bezeichnungen"].append(
block.get("attribs", {}).get("Bezeichnung", ""))
for group in kreisel_groups.values():
sivasnr, teileart = ILS_SIVASNR["kreisel"]
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": True,
"bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b),
"dispgruppe": 20,
"anzahl": len(group["ids"]),
"laenge_mm": "",
"ids": group["ids"],
"details": build_kreisel_details(group["block"]),
})
nr += 1
# Gleichartige Eckraeder (gleiches Geruest fuer Einzelmodul/Geruestoption)
# zu einer Zeile zusammenfassen - nur IDs/Bezeichnung bleiben je Block
# unterschiedlich.
for group in eckrad_groups.values():
sivasnr, teileart = ILS_SIVASNR["eckrad"]
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": True,
"bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b),
"dispgruppe": 20,
"anzahl": len(group["ids"]),
"laenge_mm": "",
"ids": group["ids"],
"details": build_eckrad_details(group["block"]),
})
nr += 1
# Lose Strecke-Elemente (Vario/Staustrecke ausserhalb eines VF_N-Blocks)
# als eine gruppierte Zeile; VF_N-Compound-Bloecke bekommen unten je eine
# eigene Detail-Zeile.
if strecke_ids:
sivasnr, teileart = ILS_SIVASNR["strecke"]
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": True,
"bezeichnung": ", ".join(b for b in strecke_bezeichnungen if b),
"dispgruppe": 20,
"anzahl": len(strecke_ids),
"laenge_mm": "",
"ids": strecke_ids,
"details": {},
})
nr += 1
# ILS 2.0 Zusatzmodul: BTMT-Beladestationen UND BTMT-Entladestationen
# (Abwurfstationen) zusammen in EINER Zeile (Merkmale 3501-3504) statt wie
# bisher zwei separaten TeileArt-Zeilen ohne details - Anzahl-Spalte
# bleibt 1 (wie bei der Gefaellestrecke-Gruppe), die realen Stueckzahlen
# stehen in 3503/3504.
if btmt_beladung_ids or btmt_entladung_ids:
sivasnr, teileart = ILS_SIVASNR["zusatzmodul"]
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": True,
"bezeichnung": "",
"dispgruppe": 20,
"anzahl": 1,
"laenge_mm": "",
"ids": btmt_beladung_ids + btmt_entladung_ids,
"details": {
"3501": ja_nein("1" if btmt_beladung_ids else "0"),
"3503": str(len(btmt_beladung_ids)),
"3502": ja_nein("1" if btmt_entladung_ids else "0"),
"3504": str(len(btmt_entladung_ids)),
},
})
nr += 1
# Geruest-Baugruppen (GR_<id>_<timestamp>) - je Block eine eigene Zeile
# (siehe Kommentar bei geruest_list oben). Sivasnr kommt direkt aus dem
# ARTINR-Attribut - kann mehrere, kommagetrennte Sivas-IDs enthalten
# (Baugruppe aus mehreren Einzelteilen, siehe ssg-join in
# GeruestInsert.lsp) und dieselbe ID mehrfach (menge>1 je Einzelteil).
# Anders als bei den uebrigen Teilearten also KEIN fester Eintrag aus
# ILS_SIVASNR - JEDE Artikelnummer bekommt ihre EIGENE Sivas-Zeile (Sivas
# kennt nur eine Sivasnr je Zeile), gleiche Artikelnummern INNERHALB
# derselben Baugruppe werden dabei zu einer Zeile mit anzahl>1
# zusammengefasst (wie omniflo_groups oben). Die volle
# LAENGE der Baugruppe steht auf JEDER so entstandenen Zeile - Sivas hat
# kein Konzept einer "gemeinsamen" Laenge ueber mehrere Zeilen hinweg.
# Die Teilefarbe steht je Artikelnummer in TEILEFARBE (parallele Liste),
# gruppiert wird darum ueber (Artikelnummer, Farbe).
for block in geruest_list:
attribs = block.get("attribs", {})
artinr_groups = {}
for paar in geruest_artinr_farben(attribs):
artinr_groups.setdefault(paar, 0)
artinr_groups[paar] += 1
for (artinr, farbe), anzahl in artinr_groups.items():
items.append({
"nr": nr,
"teileart": attribs.get("TEILEART", "Geruest"),
"sivasnr": artinr,
"sivasnr_quoted": True,
"bezeichnung": attribs.get("BEZEICHNUNG", ""),
"dispgruppe": 20,
"anzahl": anzahl,
"laenge_mm": attribs.get("LAENGE", ""),
"ids": [get_id(block)],
"details": build_geruest_details(farbe),
})
nr += 1
# VarioFoerderer-Compound-Bloecke (VF_N) - je Block eine eigene Zeile mit
# vollstaendigen Gefaelle-/Angetrieben-Teilgruppen (immer TYP "Streckengruppe").
for block in vario_list:
sivasnr, teileart = ILS_SIVASNR["strecke"]
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": True,
"bezeichnung": block.get("attribs", {}).get("Bezeichnung", ""),
"dispgruppe": 20,
"anzahl": 1,
"laenge_mm": "",
"ids": [get_id(block)],
"details": build_variofoerderer_details(block),
})
nr += 1
# Gleichartige Gefaellestrecken (identische Masse) zu einer Zeile
# zusammenfassen - nur IDs/Bezeichnung bleiben je Block unterschiedlich.
gefaelle_groups = {}
for block in gefaelle_list:
sig = gefaellestrecke_signature(block)
if sig not in gefaelle_groups:
gefaelle_groups[sig] = {"ids": [], "bezeichnungen": [], "block": block}
gefaelle_groups[sig]["ids"].append(get_id(block))
gefaelle_groups[sig]["bezeichnungen"].append(
block.get("attribs", {}).get("Bezeichnung", block.get("block_name", "")))
for group in gefaelle_groups.values():
sivasnr, teileart = ILS_SIVASNR["gefaellestrecke"]
# Anders als bei Kreisel steht hier die reale Stueckzahl der Gruppe
# NICHT in der Anzahl-Spalte (die bleibt 1), sondern im Merkmal 4311 -
# siehe build_gefaellestrecke_details.
details = build_gefaellestrecke_details(group["block"], anzahl=len(group["ids"]))
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": True,
"bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b),
"dispgruppe": 20,
"anzahl": 1,
"laenge_mm": "",
"ids": group["ids"],
"details": details,
})
nr += 1
# ILS 2.0 Weiche (6216): EINE Summenzeile fuer alle Kreisel-Beruehrpunkte
# (kreisel_weichen.json des LISP-Exports, Anzahl = anzahl_weichen_ils).
# Ein Beruehrpunkt = eine Weichenkombination aus 1x aktiv (3371) + 1x
# passiv (3372); die Zuordnung Merkmal -> Wert steht in sivas.cfg
# [weichen], beide lesen ANZAHL_WEICHEN. Anzahl-Spalte 1 wie bei der
# Automation-Zeile - die Gesamtanzahlen stecken in den Merkmalen.
if counters["anzahl_weichen_ils"] > 0:
weiche_sivasnr, weiche_teileart = ILS_SIVASNR["ils_weiche"]
items.append({
"nr": nr,
"teileart": weiche_teileart,
"sivasnr": weiche_sivasnr,
"sivasnr_quoted": True,
"bezeichnung": "",
"dispgruppe": 20,
"anzahl": 1,
"laenge_mm": "",
"details": sivas_mapping.anwenden(
SIVAS_MAPPING_WEICHEN,
{"ANZAHL_WEICHEN": str(counters["anzahl_weichen_ils"])},
TRANSFORMS),
})
nr += 1
# ILS Automation Summierungszeile (nur wenn ILS-Elemente vorhanden)
has_ils = (counters["anzahl_kreisel"] > 0
or counters["anzahl_eckraeder"] > 0
or counters["anzahl_variofoerderer"] > 0
or counters["anzahl_gefaellestrecken"] > 0
or counters["anzahl_btbeladung"] > 0
or counters["anzahl_abwurfstationen"] > 0)
if has_ils:
# 4407 "Anzahl der Streckengruppen" zaehlt NUR die kombinierten
# Gefaelle+Vario-Compound-Bloecke (VF_N, TYP "Streckengruppe") - das
# ist len(vario_list), NICHT counters["anzahl_variofoerderer"] (der
# zaehlt auch lose Strecke-Module ausserhalb eines VF_N mit).
# 4426 "Anzahl der einzelnen Vario-Foerderer" gibt es als eigenen
# Typ nicht mehr (nur noch Gefaellestrecke ODER Streckengruppe) und
# steht daher fest auf "0".
automation_details = {
"4000": "Ja",
"4400": str(counters["anzahl_kreisel"]),
"4413": str(counters["sep_kreisel_ohne_pin"]),
"4401": str(counters["anzahl_kreisel_pinband"]),
"4414": str(counters["sep_kreisel_pin"]),
"4402": str(counters["anzahl_verbindungskreisel"]),
"4419": f"{counters['laengen_kreisel']:.2f}",
"4403": str(counters["anzahl_weichen_ils"]),
"4404": str(counters["anzahl_eckraeder"]),
"4425": str(counters["anzahl_gefaellestrecken"]),
"4426": "0",
"4407": str(len(vario_list)),
"4418": f"{counters['laengen_gefaellestrecken']:.2f}",
"4408": str(counters["anzahl_motoren_vario"]),
"4420": str(counters["laengen_variofoerderer"]),
"4409": str(counters["anzahl_etf"]),
"4417": str(counters["laengen_etf"]),
"4410": str(counters["anzahl_separatoren"]),
"4411": str(counters["anzahl_scanner"]),
"4421": str(counters["anzahl_btbeladung"]),
"4422": str(counters["anzahl_abwurfstationen"]),
"4100": "Ja",
}
auto_sivasnr, auto_teileart = ILS_SIVASNR["automation"]
items.append({
"nr": nr,
"teileart": auto_teileart,
"sivasnr": auto_sivasnr,
"sivasnr_quoted": True,
"bezeichnung": "",
"dispgruppe": 20,
"anzahl": 1,
"laenge_mm": "",
"details": automation_details,
})
return items, counters
def format_csv_line(item, details=None):
"""Formatiert eine CSV-Zeile im neuen Sivas-Format.
details ueberschreibt item["details"] fuer die Ausgabe - genutzt von
details_mit_bezeichnung(), um dieselbe Zeile mit lesbaren Merkmal-Keys
in die zweite ("..._mit_bezeichnung.csv") Datei zu schreiben, ohne
item["details"] selbst zu veraendern.
"""
if details is None:
details = item["details"]
details_json = json.dumps(details, ensure_ascii=False)
sivasnr = f'"{csv_quote(item["sivasnr"])}"' if item["sivasnr_quoted"] else item["sivasnr"]
ids_str = ", ".join(str(i) for i in item.get("ids", []) if i)
return (
f'{item["nr"]}'
f';"{csv_quote(item["teileart"])}"'
f';{sivasnr}'
f';"{csv_quote(item["bezeichnung"])}"'
f';{item["dispgruppe"]}'
f';{item["anzahl"]}'
f';{item["laenge_mm"]}'
f';"{csv_quote(ids_str)}"'
f';{details_json}'
)
def details_mit_bezeichnung(details, merkmalstamm):
"""Kopie von details, bei der jeder Merkmalnummer-Key durch seine
Klartext-Bezeichnung aus dem Merkmalstamm ersetzt wird (Wert bleibt
unveraendert). Keys, die keine reine Zahl sind, bleiben unveraendert
(aktuell kein Normalfall mehr - alle Sektionen in cfg/sivas.cfg nutzen
echte Merkmalnummern)."""
neu = {}
for key, value in details.items():
if key.isdigit():
neu[bezeichnung_fuer(key, merkmalstamm)] = value
else:
neu[key] = value
return neu
def lesbarer_dateiname(output_csv):
"""Leitet den Dateinamen der Lesbarkeits-Kopie aus output_csv ab, z.B.
"500651_sivas.csv" -> "500651_sivas_mit_bezeichnung.csv"."""
stamm, endung = os.path.splitext(output_csv)
return f"{stamm}_mit_bezeichnung{endung}"
def zusammengefasster_dateiname(output_csv):
"""Leitet den Dateinamen der zusammengefassten Kopie aus output_csv ab,
z.B. "500651_sivas.csv" -> "500651_sivas_zusammengefasst.csv"."""
stamm, endung = os.path.splitext(output_csv)
return f"{stamm}_zusammengefasst{endung}"
def items_zusammenfassen(items):
"""Fasst Zeilen zusammen, die sich nur in Nr, Anzahl und IDs
unterscheiden: Anzahl wird addiert, die IDs werden (ohne Dubletten, in
Reihenfolge des Auftretens) vereinigt, Nr wird neu durchgezaehlt.
Reihenfolge der Gruppen = erstes Auftreten.
Betrifft in der Praxis vor allem die Geruest-Zeilen (je Block und
Artikelnummer eine Zeile, siehe process_blocks) - die uebrigen Teilearten
sind dort bereits gruppiert, und ihre Zeilen unterscheiden sich meist in
Bezeichnung/details. Die Laenge gehoert zum Schluessel: Saeulen mit
unterschiedlicher Laenge bleiben getrennte Zeilen.
Die Eingabe bleibt unveraendert (die Hauptdatei wird aus denselben
items geschrieben)."""
gruppen = {}
for item in items:
key = (
item["teileart"], str(item["sivasnr"]), item["sivasnr_quoted"],
item["bezeichnung"], item["dispgruppe"], str(item["laenge_mm"]),
json.dumps(item["details"], ensure_ascii=False, sort_keys=True),
)
g = gruppen.get(key)
if g is None:
g = dict(item, anzahl=0, ids=[])
gruppen[key] = g
g["anzahl"] += safe_int(item["anzahl"], 0)
for i in item.get("ids", []):
if i and i not in g["ids"]:
g["ids"].append(i)
ergebnis = list(gruppen.values())
for nr, g in enumerate(ergebnis, 1):
g["nr"] = nr
return ergebnis
def main():
if len(sys.argv) < 4:
print("Aufruf: python export_sivas.py <export_raw.json> <data_dir> <output.csv> [--mit-bezeichnung]")
sys.exit(1)
raw_json_path = sys.argv[1]
# sys.argv[2] (data_dir) wird derzeit nicht gebraucht - Omniflo-Nummern
# kommen aus ARTINR, nicht mehr aus dem Katalog. Argument bleibt fuer
# den Aufrufer (export.lsp) bestehen.
output_csv = sys.argv[3]
mit_bezeichnung = "--mit-bezeichnung" in sys.argv[4:]
blocks = load_json(raw_json_path)
print(f"[export_sivas] {len(blocks)} Bloecke geladen.")
# Kreisel-Beruehrpunkte (= ILS-Weichen) zaehlt der LISP-Export
# (kreisel_weichen.json neben der Raw-Datei); fehlt sie, bleibt 4403 = 0.
lisp_weichen = load_lisp_kreisel_weichen(raw_json_path)
anzahl_weichen = len(lisp_weichen["beruehrungen"]) if lisp_weichen else 0
if lisp_weichen is not None:
print(f"[export_sivas] Kreisel-Weichen aus LISP: {anzahl_weichen}")
items, _ = process_blocks(blocks, anzahl_weichen_ils=anzahl_weichen)
header = "Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details"
with open(output_csv, "w", encoding="utf-8") as f:
f.write(header + "\n")
for item in items:
f.write(format_csv_line(item) + "\n")
print(f"[export_sivas] CSV geschrieben: {output_csv} ({len(items)} Zeilen)")
# Zusaetzliche Datei mit zusammengefassten identischen Zeilen (siehe
# items_zusammenfassen) - output_csv selbst bleibt unveraendert.
items_zus = items_zusammenfassen(items)
output_csv_zus = zusammengefasster_dateiname(output_csv)
with open(output_csv_zus, "w", encoding="utf-8") as f:
f.write(header + "\n")
for item in items_zus:
f.write(format_csv_line(item) + "\n")
print(f"[export_sivas] Zusammengefasste CSV geschrieben: {output_csv_zus} "
f"({len(items_zus)} Zeilen)")
# Zweite, lesbare Kopie fuer die manuelle Pruefung (Merkmalnummer +
# Bezeichnung aus cfg/Merkmalsstamm_in_Sivas.xlsx) - wird von Sivas
# selbst NICHT eingelesen, nur output_csv ist der eigentliche Import.
# Nur bei explizitem --mit-bezeichnung-Schalter (Dialog CSV_EXPORT_KONFIG).
if mit_bezeichnung:
merkmalstamm = lade_merkmalstamm()
output_csv_lesbar = lesbarer_dateiname(output_csv)
with open(output_csv_lesbar, "w", encoding="utf-8") as f:
f.write(header + "\n")
for item in items:
details = details_mit_bezeichnung(item["details"], merkmalstamm)
f.write(format_csv_line(item, details=details) + "\n")
print(f"[export_sivas] Lesbare Kopie geschrieben: {output_csv_lesbar}")
if __name__ == "__main__":
main()