refactor: Sivas-CSV-Export auf Merkmalnummern umgestellt, lesbare Zweitdatei

- ILS 2.0 Kreisel/Strecke(VF_N)/Gefaellestrecke/Automation: details-JSON
  nutzt jetzt Sivas-Merkmalnummern (z.B. "2952") statt beschreibender
  Klartext-Keys, abgeglichen mit dem Merkmalstamm in
  cfg/Merkmalsstamm_in_Sivas.xlsx.
- ILS 2.0 Zusatzmodul: BT-Beladung und Abwurfstationen laufen jetzt in
  einer gemeinsamen Zeile (Merkmale 3501-3504) statt zwei separaten
  Zeilen ohne details.
- Gefaellestrecke-Gruppe: Anzahl-Spalte bleibt fest 1, die reale
  Stueckzahl der Gruppe steht im Merkmal 4311 (anders als bei Kreisel,
  wo die Anzahl-Spalte die Stueckzahl traegt und 3511 fix "1" ist).
- Automation-Summenzeile: 4407 zaehlt jetzt echte Streckengruppen
  (VF_N-Bloecke) statt eines 0/1-Flags, 4408 (Motoren Vario-Foerderer)
  wird erstmals tatsaechlich berechnet (vorher nie inkrementiert), 4426
  steht fest auf "0" (Konzept "einzelner Variofoerderer" entfaellt).
- Neues Modul lib/merkmalstamm.py: liest Merkmalnummer->Bezeichnung aus
  der xlsx rein per Standard-Library (zipfile/xml.etree, keine neue
  pip-Abhaengigkeit fuer den ueber export.lsp aufgeloesten Interpreter).
- export_sivas.py schreibt bei jedem Lauf zusaetzlich eine zweite Datei
  "<name>_mit_bezeichnung.csv" (gleicher Inhalt, aber jeder
  Merkmalnummer-Key um "<Nummer> | <Bezeichnung>" erweitert) - reine
  Lesehilfe zur Pruefung, wird von Sivas nicht eingelesen.
- cfg/sivas_merkmal_zuordnung.md: Nachschlagetabelle Block-Attribut <->
  Merkmalnummer je Teileart, inkl. offener Punkte (Separator-/
  Scanner-Aufteilung Gefaelle-/Vario-Anteil noch Platzhalter).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Yuelin Wang
2026-09-15 10:33:20 +02:00
co-authored by Claude Sonnet 5
parent 18771d53ff
commit 6f12cab86f
3 changed files with 492 additions and 119 deletions
+246 -119
View File
@@ -18,15 +18,31 @@ Neues CSV-Format:
- 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
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 um die zugehoerige Bezeichnung aus
cfg/Merkmalsstamm_in_Sivas.xlsx erweitert ("<Nummer> | <Bezeichnung>") -
reine Lesehilfe zur Pruefung, wird von Sivas selbst nicht eingelesen.
"""
import json
@@ -34,6 +50,7 @@ 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()
@@ -94,19 +111,56 @@ def format_sivasnr(sivasnr):
return ", ".join(p.strip() for p in str(sivasnr).split("+") if p.strip())
def build_gefaellestrecke_details(block):
"""Merkmale-Dict fuer eine einzelne ILS Gefaellestrecke (GF_N-Block)."""
# --- 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 {
"Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"),
"Typ": attribs.get("TYP", "Gefaellestrecke"),
"Laenge in Meter": attribs.get("L_GF_m", ""),
"Winkel": attribs.get("GF_WINKEL", ""),
"Anzahl der Separatoren": attribs.get("ANZAHL_SEPARATOR", "0"),
"Anzahl der Scanner": attribs.get("ANZAHL_SCANNER", "0"),
"Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1",
"Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
"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")),
}
@@ -220,62 +274,104 @@ def parse_str_list(csv_str):
def build_variofoerderer_details(block):
"""
Merkmale-Dict fuer eine VF_N-Streckengruppe (VarioFoerderer, ggf.
mehrsegmentig aus Gefaelle- und angetriebenen Foerderer-Teilen).
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")
return {
"Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"),
"Anzahl Foerdergruppen": to_int("ANZAHL_VF"),
"Anzahl Gefaellegruppen": to_int("ANZAHL_GF"),
"GruppenteileGefaelle": {
"AnzahlGeraden": to_int("ANZAHL_GF"),
"Laenge in Meter": parse_float_list(attribs.get("L_GF_m", "")),
"Winkel": parse_float_list(attribs.get("GF_WINKEL", "")),
"AnzahlKurvenLinks90": to_int("GF_Bogen_L_90"),
"AnzahlKurvenLinks60": to_int("GF_Bogen_L_60"),
"AnzahlKurvenLinks30": to_int("GF_Bogen_L_30"),
"AnzahlKurvenRechts90": to_int("GF_Bogen_R_90"),
"AnzahlKurvenRechts60": to_int("GF_Bogen_R_60"),
"AnzahlKurvenRechts30": to_int("GF_Bogen_R_30"),
},
"GruppenteileAngetrieben": {
"MotorSeite": parse_str_list(attribs.get("MOTORSEITE", "")),
"AnzahlGeraden": to_int("ANZAHL_VF"),
"Laenge in Meter": parse_float_list(attribs.get("L_VF_m", "")),
"Winkel": parse_float_list(attribs.get("VF_WINKEL", "")),
"Foerderrichtung": parse_str_list(attribs.get("ANTRIEBFAHRTRICHTUNG", "")),
"AnzahlKurvenAussen90": to_int("VF_Bogen_A_90"),
"AnzahlKurvenAussen60": to_int("VF_Bogen_A_60"),
"AnzahlKurvenAussen30": to_int("VF_Bogen_A_30"),
"AnzahlKurvenInnen90": to_int("VF_Bogen_I_90"),
"AnzahlKurvenInnen60": to_int("VF_Bogen_I_60"),
"AnzahlKurvenInnen30": to_int("VF_Bogen_I_30"),
},
"Anzahl der Separatoren": to_int("ANZAHL_SEPARATOR"),
"Anzahl der Scanner": to_int("ANZAHL_SCANNER"),
"Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1",
"Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
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 fuer einen ILS Kreisel."""
"""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 {
"Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"),
"Kreiselart": attribs.get("KREISELART", "STANDARD"),
"Abstand (Kreiselachse A - Kreiselachse) in Meter": abstand_m,
"Anzahl der Separatoren": attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")),
"Anzahl der Scanner": attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")),
"Anzahl der Rampen": attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")),
"Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1",
"Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
"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")),
}
@@ -325,8 +421,6 @@ ILS_SIVASNR = {
"ils_weiche": ("6216", "ILS 2.0 Weiche"),
"strecke": ("6220", "ILS 2.0 Strecke"),
"gefaellestrecke": ("6220", "ILS 2.0 Gefaellestrecke"),
"bt_beladung": ("6215", "BT - Beladung (mit Vereinzeler, ohne Separator)"),
"bg_entladung": ("6215", "Abwurf fuer Klammerbuegel Sitzbezuege / Kleiderbuegel"),
"automation": ("6269", "ILS 2.0 Automation und Pneumatik - Gesamtanlage"),
}
@@ -351,9 +445,7 @@ def process_blocks(blocks, lookup):
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_beladung_bezeichnungen = []
btmt_entladung_ids = [] # IDs aller SC_Entladung*-Bloecke (eine gruppierte Zeile)
btmt_entladung_bezeichnungen = []
geruest_list = [] # Liste von GR_-Geruest-Baugruppenbloecken, je einer Zeile
counters = {
@@ -375,7 +467,6 @@ def process_blocks(blocks, lookup):
"anzahl_eckraeder": 0,
"anzahl_gefaellestrecken": 0,
"anzahl_variofoerderer": 0,
"anzahl_streckengruppen": 0,
"laengen_gefaellestrecken": 0.0,
"anzahl_motoren_vario": 0,
"laengen_variofoerderer": 0.0,
@@ -404,14 +495,12 @@ def process_blocks(blocks, lookup):
# der ILS-Automation-Summierungszeile (siehe unten).
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_beladung", [])):
btmt_beladung_ids.append(get_id(block))
btmt_beladung_bezeichnungen.append(block.get("attribs", {}).get("Bezeichnung", ""))
counters["anzahl_btbeladung"] += 1
continue
# --- BTMT-Entladestation (Abwurfstation) ---
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_entladung", [])):
btmt_entladung_ids.append(get_id(block))
btmt_entladung_bezeichnungen.append(block.get("attribs", {}).get("Bezeichnung", ""))
counters["anzahl_abwurfstationen"] += 1
continue
@@ -542,8 +631,6 @@ def process_blocks(blocks, lookup):
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")
if counters["anzahl_streckengruppen"] == 0:
counters["anzahl_streckengruppen"] = 1
continue
# --- VarioFoerderer Compound-Block (VF_N) ---
@@ -556,8 +643,7 @@ def process_blocks(blocks, lookup):
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")
if counters["anzahl_streckengruppen"] == 0:
counters["anzahl_streckengruppen"] = 1
counters["anzahl_motoren_vario"] += safe_int(attribs.get("ANZAHL_VF", "0") or "0")
continue
# --- Gefaellestrecke Compound-Block (GF_N) ---
@@ -569,8 +655,6 @@ def process_blocks(blocks, lookup):
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")
if counters["anzahl_streckengruppen"] == 0:
counters["anzahl_streckengruppen"] = 1
continue
# --- ILS Strecke-Module (Vario/Staustrecke, einzelne Komponenten in Modelspace) ---
@@ -695,39 +779,29 @@ def process_blocks(blocks, lookup):
})
nr += 1
# BTMT-Beladestationen: eine gruppierte Zeile fuer alle Instanzen (wie die
# losen Strecke-Elemente oben) - Stueckzahl zusaetzlich im Automation-
# Zaehler "Anzahl aller BTBeladung" (siehe counters oben).
if btmt_beladung_ids:
sivasnr, teileart = ILS_SIVASNR["bt_beladung"]
# 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": ", ".join(b for b in btmt_beladung_bezeichnungen if b),
"bezeichnung": "",
"dispgruppe": 20,
"anzahl": len(btmt_beladung_ids),
"anzahl": 1,
"laenge_mm": "",
"ids": btmt_beladung_ids,
"details": {},
})
nr += 1
# BTMT-Entladestationen (Abwurfstationen): analog gruppiert.
if btmt_entladung_ids:
sivasnr, teileart = ILS_SIVASNR["bg_entladung"]
items.append({
"nr": nr,
"teileart": teileart,
"sivasnr": sivasnr,
"sivasnr_quoted": True,
"bezeichnung": ", ".join(b for b in btmt_entladung_bezeichnungen if b),
"dispgruppe": 20,
"anzahl": len(btmt_entladung_ids),
"laenge_mm": "",
"ids": btmt_entladung_ids,
"details": {},
"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
@@ -796,7 +870,10 @@ def process_blocks(blocks, lookup):
for group in gefaelle_groups.values():
sivasnr, teileart = ILS_SIVASNR["gefaellestrecke"]
details = build_gefaellestrecke_details(group["block"])
# 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,
@@ -804,7 +881,7 @@ def process_blocks(blocks, lookup):
"sivasnr_quoted": True,
"bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b),
"dispgruppe": 20,
"anzahl": len(group["ids"]),
"anzahl": 1,
"laenge_mm": "",
"ids": group["ids"],
"details": details,
@@ -819,27 +896,36 @@ def process_blocks(blocks, lookup):
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 = {
"Anzahl Kreisel": counters["anzahl_kreisel"],
"Anzahl Kreisel mit Pinband": counters["anzahl_kreisel_pinband"],
"Anzahl aller Separatoren in Kreisel mit PIN": counters["sep_kreisel_pin"],
"Anzahl aller Separatoren in Kreisel ohne PIN": counters["sep_kreisel_ohne_pin"],
"Anzahl Verbindungskreisel": counters["anzahl_verbindungskreisel"],
"Laengen aller Kreisel": counters["laengen_kreisel"],
"Anzahl der Weichen": counters["anzahl_weichen_ils"],
"Anzahl der Eckraeder": counters["anzahl_eckraeder"],
"Anzahl der einzelnen Gefaellestrecken": counters["anzahl_gefaellestrecken"],
"Anzahl der einzelnen Variofoerderer": counters["anzahl_variofoerderer"],
"Anzahl der Streckengruppen": counters["anzahl_streckengruppen"],
"Laengen aller Gefaellestrecken": counters["laengen_gefaellestrecken"],
"Anzahl der Motoren Variofoerderer": counters["anzahl_motoren_vario"],
"Laengen aller Variofoerderer": counters["laengen_variofoerderer"],
"Anzahl ETFs": counters["anzahl_etf"],
"Laengen aller ETFs": counters["laengen_etf"],
"Anzahl aller Separatoren": counters["anzahl_separatoren"],
"Anzahl aller Scanner": counters["anzahl_scanner"],
"Anzahl aller BTBeladung": counters["anzahl_btbeladung"],
"Anzahl aller Abwurfstationen": counters["anzahl_abwurfstationen"],
"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({
@@ -857,9 +943,17 @@ def process_blocks(blocks, lookup):
return items, counters
def format_csv_line(item):
"""Formatiert eine CSV-Zeile im neuen Sivas-Format."""
details_json = json.dumps(item["details"], ensure_ascii=False)
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 (
@@ -875,6 +969,28 @@ def format_csv_line(item):
)
def details_mit_bezeichnung(details, merkmalstamm):
"""Kopie von details, bei der jeder Merkmalnummer-Key um die Bezeichnung
aus dem Merkmalstamm erweitert wird ("<Nummer> | <Bezeichnung>"). 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[f"{key} | {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>")
@@ -903,9 +1019,20 @@ def main():
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()