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