Files
dxfmakros/lib/export_sivas.py
T
Michael StanglandClaude Sonnet 5 f751a90076 export_csv: VF_n/GF_n-Einzelelemente (Boegen, Streckenelemente, Umlenk-/Motorstation) als Proxy-Zeilen exportieren
Bisher tauchten nur der Wrapper selbst, verpackte Separatoren und AS/ES-
Kettenenden als eigene CSV-Zeilen auf - die uebrigen Kettenbausteine
(Boegen, gerade Foerderer-/Gefaellestrecken-Segmente, Umlenk-/Motorstation)
blieben nur in der Wrapper-Blockdefinition verpackt. csv:element-proxies-
erzeugen (analog zu csv:sep-proxies-erzeugen) fuegt fuer jeden Fund eine
temporaere Kopie ein, K1/K2 kommen aus dem EIGENEN KS_EIN/KS_AUS jedes
Elements (nicht dem Gesamt-AS/ES der Kette) und bleiben leer, wo keines
existiert (z.B. das nie tatsaechlich eingefuegte Vario_Kurve_*).

Zusaetzlicher Fix: csv:get-k-kos-strings ignorierte bisher die Skalierung
des Top-Level-Inserts - fuer Staustrecke_SP_1000_mm (das per Laenge nicht-
uniform skaliert wird) waere K2 damit falsch gewesen, unabhaengig von der
tatsaechlich gebauten Segmentlaenge.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-24 13:08:35 +02:00

1064 lines
45 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 Bogen/Weiche: gruppiert nach SivasNummer, Anzahl summiert
- 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>
Argumente:
export_raw.json - JSON-Datei mit allen INSERT-Entities aus der Zeichnung
data_dir - Pfad zum data/-Verzeichnis (mit json/ Unterordner)
output.csv - Ziel-CSV-Datei (reine Merkmalnummern, fuer den
Sivas-Import)
Zusaetzlich zu output.csv wird immer 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.
"""
import json
import sys
import os
from export_blockpatterns import load_patterns, matches_any, csv_quote, safe_float, safe_int
from merkmalstamm import lade_merkmalstamm, bezeichnung_fuer
BLOCKPATTERNS = load_patterns()
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 build_lookup(boegen, weichen):
"""Erzeugt ein Dict: SivasNr (als String) -> (typ, eintrag)"""
lookup = {}
for b in boegen:
lookup[str(b["Sivasnr"])] = ("bogen", b)
for w in weichen:
lookup[str(w["Sivasnr"])] = ("weiche", w)
return lookup
def classify_weiche(eintrag):
"""Bestimmt den Weichen-Subtyp fuer die ILS-Automation-Zaehlung."""
wt = eintrag.get("WeichenTyp", "")
profil = eintrag.get("ProfilTyp", "")
if "WEICHENKOERPER" in profil.upper():
return "weichenkoerper"
if wt == "Einzelweiche":
return "einzelweiche"
if wt in ("Doppelweiche", "Dreiwegeweiche"):
return "doppelweiche"
if wt in ("Deltaweiche", "Dreifachweiche") or "DELTA" in profil.upper():
return "deltaweiche"
if wt == "Sternweiche" or "STAR" in profil.upper():
return "sternweiche"
return "einzelweiche"
def get_id(block):
"""Liest das eindeutige ID-Attribut eines Blocks (von ssg-id-check-all vergeben)."""
return block.get("attribs", {}).get("ID", "")
def format_sivasnr(sivasnr):
"""
Wandelt einen "+"-separierten Sivasnr-Wert (Rohformat aus den JSON-Katalogen,
dort weiterhin benoetigt fuer Dateinamen-Referenzen) in eine reine
kommaseparierte Liste fuer die Ausgabe (CSV/Block-Attribut) um.
"""
return ", ".join(p.strip() for p in str(sivasnr).split("+") if p.strip())
# --- 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). Nur hier zentral, nicht
# ueber die uebliche ssg-text-Sprachdatei, da rein Sivas-seitig.
GERUEST_TYP_ANZEIGE = {
"Schoenenberger Geruest": "Schönenberger-Gerüst",
}
SEPARATOR_BEARBEITUNG_ANZEIGE = "ohne HARTCOAT HRC 50 ± 5"
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 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"
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."""
attribs = block.get("attribs", {})
return {
"2950": attribs.get("MONTAGEHOEHE_m", "0"),
"4300": streckengruppe_objekt_text(attribs),
"4384": "Gefällestrecke",
"4311": str(anzahl),
"4318": attribs.get("L_GF_m", ""),
"4391": attribs.get("GF_WINKEL", ""),
"4337": attribs.get("ANZAHL_SEPARATOR", "0"),
"4338": SEPARATOR_BEARBEITUNG_ANZEIGE,
"4339": attribs.get("ANZAHL_SCANNER", "0"),
"3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")),
"3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")),
}
def kreisel_signature(block):
"""
Schluessel zur Gruppierung gleichartiger Kreisel: 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.
"""
attribs = block.get("attribs", {})
return (
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: Geruest fuer
Einzelmodul/Geruestoption entscheiden, ob Eckraeder in derselben
Sivas-Zeile summiert werden.
"""
attribs = block.get("attribs", {})
return (
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) landen in derselben Sivas-Zeile -
nur IDs/Bezeichnung unterscheiden sich.
"""
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"),
)
def expand_tef_components(sivasnr_parts, sivasnr_tef):
"""Ersetzt "0_BG*"-Platzhalter-Teile in sivasnr_parts durch die dahinter
liegenden echten TEF-Sivasnummern (Katalogfeld SivasnrTEF).
"0_BG*"-Werte im Sivasnr-Rohwert (z.B. "834372404+0_BG071090") sind KEINE
echten Sivasnummern, sondern reine Blocknamen-Platzhalter fuer eine TEF-
Baugruppe aus mehreren realen Teilen - "0_BG071090" steht z.B. fuer die
drei echten Teile "0_B10071"+"0_B10090"+"0_B10090" (siehe SivasnrTEF im
Katalog, omniflo_weichen.json/omniflo_boegen.json). Ohne diese Aufloesung
wuerde "0_BG071090" selbst als (nicht existente) Sivasnummer gezaehlt und
die echten Teile (u.a. 0_B10071) fehlten in der Summierung.
Reale TEF-Einzelteile, die bereits direkt im Sivasnr-Rohwert stehen
(Praefix "0_B", aber nicht "0_BG", z.B. "821104026+0_B10071"), sind schon
echte Sivasnummern und werden unveraendert uebernommen.
Bei mehreren "0_BG*"-Platzhaltern im selben Rohwert (TEF beidseitig) wird
SivasnrTEF zu gleichen Teilen aufgeteilt - der Katalog listet die Teile
je Seite hintereinander in SivasnrTEF.
"""
bg_count = sum(1 for p in sivasnr_parts if p.startswith("0_BG"))
if bg_count == 0:
return list(sivasnr_parts)
tef_parts = [p.strip() for p in str(sivasnr_tef or "").split("+") if p.strip()]
chunk_size = len(tef_parts) // bg_count if bg_count else 0
expanded = []
tef_index = 0
for part in sivasnr_parts:
if part.startswith("0_BG"):
expanded.extend(tef_parts[tef_index:tef_index + chunk_size])
tef_index += chunk_size
else:
expanded.append(part)
return expanded
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()]
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.
"""
attribs = block.get("attribs", {})
def to_int(key):
return safe_int(attribs.get(key, "0") or "0")
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 = {
"2950": attribs.get("MONTAGEHOEHE_m", "0"),
"4300": "mehrere Objekte",
"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.update({
"4329": to_int("GF_Bogen_L_90"),
"4330": to_int("GF_Bogen_L_60"),
"4331": to_int("GF_Bogen_L_30"),
"4332": to_int("GF_Bogen_R_90"),
"4333": to_int("GF_Bogen_R_60"),
"4334": to_int("GF_Bogen_R_30"),
"4337": "0",
"4338": SEPARATOR_BEARBEITUNG_ANZEIGE,
"4339": "0",
"4340": "Ja" if vf_laengen else "Nein",
"4341": sum(1 for seite in motorseiten if seite.lower() == "links"),
"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 ""
details.update({
"4373": to_int("VF_Bogen_A_90"),
"4374": to_int("VF_Bogen_A_60"),
"4375": to_int("VF_Bogen_A_30"),
"4376": to_int("VF_Bogen_I_90"),
"4377": to_int("VF_Bogen_I_60"),
"4378": to_int("VF_Bogen_I_30"),
"4381": attribs.get("ANZAHL_SEPARATOR", "0"),
"4382": SEPARATOR_BEARBEITUNG_ANZEIGE,
"4383": attribs.get("ANZAHL_SCANNER", "0"),
"3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")),
"3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")),
})
return {k: str(v) for k, v in details.items()}
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)."""
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"
return {
"2952": attribs.get("MONTAGEHOEHE_m", "0"),
"3510": kreiselart,
"3511": "1",
"3512": abstand_m,
"3515": attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")),
"3517": attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")),
"3519": attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")),
"3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")),
"3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")),
}
def build_eckrad_details(block):
"""Merkmale-Dict fuer ein ILS Eckrad."""
attribs = block.get("attribs", {})
return {
"Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1",
"Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
}
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.
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 {
"Farbe": attribs.get("FARBE", ""),
}
# --- 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): 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", [])
)
# 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, lookup):
"""
Verarbeitet alle Bloecke.
Rueckgabe:
items - Liste der CSV-Zeilen als Dicts
counters - Zaehler fuer ILS-Automation-Summierungszeile
"""
# Gruppen: sivasnr_str -> {anzahl, ids}
bogen_groups = {} # sivasnr -> dict
weiche_groups = {} # sivasnr -> dict
# 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_boegen": 0,
"anzahl_weichenkoerper": 0,
"anzahl_einzelweichen": 0,
"anzahl_deltaweichen": 0,
"anzahl_doppel_dreiwegeweichen": 0,
"anzahl_sternweichen": 0,
"gesamtlaenge_ap110": 0.0,
"gesamtlaenge_ap60": 0.0,
"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": 0,
"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 Bogen oder Weiche (erkannt via Katalog) ---
if bname in lookup:
typ, eintrag = lookup[bname]
sivasnr_raw = eintrag.get("Sivasnr", bname)
sivasnr_parts = [p.strip() for p in str(sivasnr_raw).split("+")]
has_tef = any(p.startswith("0_B") for p in sivasnr_parts)
block_id = get_id(block)
if typ == "bogen":
if has_tef and len(sivasnr_parts) > 1:
# Multi-Komponenten: jede Komponente separat gruppieren
# ("0_BG*"-Platzhalter werden ueber SivasnrTEF in ihre
# echten Einzelteile aufgeloest, siehe expand_tef_components)
expanded_parts = expand_tef_components(sivasnr_parts, eintrag.get("SivasnrTEF"))
for i, comp_sivasnr in enumerate(expanded_parts):
comp_id = f"{block_id}.t{i+1}"
if comp_sivasnr not in bogen_groups:
bogen_groups[comp_sivasnr] = {"anzahl": 0, "ids": []}
bogen_groups[comp_sivasnr]["anzahl"] += 1
bogen_groups[comp_sivasnr]["ids"].append(comp_id)
else:
# Normal: alle als eine Komponente
sivasnr = format_sivasnr(sivasnr_raw)
if sivasnr not in bogen_groups:
bogen_groups[sivasnr] = {"anzahl": 0, "ids": []}
bogen_groups[sivasnr]["anzahl"] += 1
bogen_groups[sivasnr]["ids"].append(block_id)
counters["anzahl_boegen"] += 1
elif typ == "weiche":
subtyp = classify_weiche(eintrag)
if has_tef and len(sivasnr_parts) > 1:
# Multi-Komponenten: jede Komponente separat gruppieren
# ("0_BG*"-Platzhalter werden ueber SivasnrTEF in ihre
# echten Einzelteile aufgeloest, siehe expand_tef_components)
expanded_parts = expand_tef_components(sivasnr_parts, eintrag.get("SivasnrTEF"))
for i, comp_sivasnr in enumerate(expanded_parts):
comp_id = f"{block_id}.t{i+1}"
if comp_sivasnr.startswith("0_B"):
# TEF-Komponente (Bogen) in bogen_groups
if comp_sivasnr not in bogen_groups:
bogen_groups[comp_sivasnr] = {"anzahl": 0, "ids": []}
bogen_groups[comp_sivasnr]["anzahl"] += 1
bogen_groups[comp_sivasnr]["ids"].append(comp_id)
else:
# Weichen-Komponente in weiche_groups
if comp_sivasnr not in weiche_groups:
weiche_groups[comp_sivasnr] = {"anzahl": 0, "ids": []}
weiche_groups[comp_sivasnr]["anzahl"] += 1
weiche_groups[comp_sivasnr]["ids"].append(comp_id)
else:
# Normal: alle als eine Komponente
sivasnr = format_sivasnr(sivasnr_raw)
if sivasnr not in weiche_groups:
weiche_groups[sivasnr] = {"anzahl": 0, "ids": []}
weiche_groups[sivasnr]["anzahl"] += 1
weiche_groups[sivasnr]["ids"].append(block_id)
if subtyp == "einzelweiche":
counters["anzahl_einzelweichen"] += 1
elif subtyp in ("doppelweiche", "dreiwegeweiche"):
counters["anzahl_doppel_dreiwegeweichen"] += 1
elif subtyp == "deltaweiche":
counters["anzahl_deltaweichen"] += 1
elif subtyp == "sternweiche":
counters["anzahl_sternweichen"] += 1
elif subtyp == "weichenkoerper":
counters["anzahl_weichenkoerper"] += 1
continue
# --- Omniflo Gerade (AP110) ---
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))
counters["gesamtlaenge_ap110"] += laenge
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 bogen_groups.items():
teileart = "TEF Bogen" if str(sivasnr).startswith("0_B") else "Omniflo Bogen"
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": False,
"bezeichnung": "",
"dispgruppe": 10,
"anzahl": g["anzahl"],
"laenge_mm": "",
"ids": g["ids"],
"details": {},
})
nr += 1
for sivasnr, g in weiche_groups.items():
teileart = "TEF Weiche" if str(sivasnr).startswith("0_B") else "Omniflo Weiche"
items.append({
"nr": nr,
"teileart": 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 bogen_groups/weiche_groups oben). Die volle
# LAENGE der Baugruppe steht auf JEDER so entstandenen Zeile - Sivas hat
# kein Konzept einer "gemeinsamen" Laenge ueber mehrere Zeilen hinweg.
for block in geruest_list:
attribs = block.get("attribs", {})
artinr_list = [a.strip() for a in attribs.get("ARTINR", "").split(",") if a.strip()]
artinr_groups = {}
for artinr in artinr_list:
artinr_groups.setdefault(artinr, 0)
artinr_groups[artinr] += 1
for artinr, 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(block),
})
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 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 (aktuell nur bei
Eckrad/Geruest, siehe build_eckrad_details/build_geruest_details - dort
noch beschreibende Klartext-Keys statt Merkmalnummern), bleiben
unveraendert."""
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 main():
if len(sys.argv) < 4:
print("Aufruf: python export_sivas.py <export_raw.json> <data_dir> <output.csv>")
sys.exit(1)
raw_json_path = sys.argv[1]
data_dir = sys.argv[2]
output_csv = sys.argv[3]
blocks = load_json(raw_json_path)
boegen_path = os.path.join(data_dir, "json", "omniflo_boegen.json")
weichen_path = os.path.join(data_dir, "json", "omniflo_weichen.json")
boegen = load_json(boegen_path) if os.path.exists(boegen_path) else []
weichen = load_json(weichen_path) if os.path.exists(weichen_path) else []
lookup = build_lookup(boegen, weichen)
print(f"[export_sivas] {len(blocks)} Bloecke geladen, "
f"{len(boegen)} Boegen, {len(weichen)} Weichen im Katalog.")
items, _ = process_blocks(blocks, lookup)
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)")
# 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.
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()