#!/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 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 "_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. SEP_SCANNER_PATTERNS = ( BLOCKPATTERNS.get("pattern_separator", []) + BLOCKPATTERNS.get("pattern_scanner", []) ) # 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__) - 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 ") 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()