diff --git a/Lisp/export.lsp b/Lisp/export.lsp index 3d02f20..3f2e2a2 100644 --- a/Lisp/export.lsp +++ b/Lisp/export.lsp @@ -116,6 +116,18 @@ (defun csv:gefrorene-ausschliessen-setzen (an) (setq *csv-gefrorene-ausschliessen* (if an T nil))) +;; --- Sivas-Export zusaetzlich mit Klartext-Bezeichnung schreiben --- +;; Standard aus (nil): EXPORTSIVAS schreibt nur die eigentliche _sivas.csv +;; (fuer den Sivas-Import). Nur bei eingeschaltetem Schalter schreibt +;; lib/export_sivas.py zusaetzlich _sivas_mit_bezeichnung.csv (Merkmal- +;; Keys durch Klartext aus cfg/Merkmalsstamm_in_Sivas.xlsx ersetzt, reine +;; Lesehilfe zur manuellen Pruefung, wird von Sivas selbst nicht eingelesen). +;; Bedienbar im Dialog CSV_EXPORT_KONFIG ("Sivas Export mit Bezeichner"). +(if (not (boundp '*csv-sivas-mit-bezeichnung*)) (setq *csv-sivas-mit-bezeichnung* nil)) +(defun csv:sivas-mit-bezeichnung-p () (if *csv-sivas-mit-bezeichnung* T nil)) +(defun csv:sivas-mit-bezeichnung-setzen (an) + (setq *csv-sivas-mit-bezeichnung* (if an T nil))) + ;; Hilfsobjekte (car eines Records), deren Wrapper (nth 1) gefroren-fn ;; erfuellt. records = Record-Listen der Form (hilfsobjekt wrapper ...), wie ;; sie csv:sep-proxies-erzeugen, csv:element-proxies-erzeugen, @@ -2622,8 +2634,14 @@ ;; Synchron ausfuehren und Exitcode auswerten - ein stiller ;; Fehlschlag (z.B. WindowsApps-Platzhalter statt Interpreter) ;; darf nicht mehr als "Export gestartet" durchgehen. + ;; --mit-bezeichnung nur an export_sivas.py anhaengen (Schalter + ;; *csv-sivas-mit-bezeichnung*, Dialog CSV_EXPORT_KONFIG) - export_csv.py + ;; kennt dieses Argument nicht. (setq rc (export:run-python py-exe py-skript - (list out-pfad (getenv "DXFM_DATA") ergebnis-pfad) + (append (list out-pfad (getenv "DXFM_DATA") ergebnis-pfad) + (if (and (= py-name (export:filename "sivas_script")) + (csv:sivas-mit-bezeichnung-p)) + (list "--mit-bezeichnung"))) log-pfad)) (cond ((null rc) @@ -2786,6 +2804,7 @@ (set_tile "modus" (itoa idx)) (set_tile "hilfskopien_behalten" (if (csv:hilfskopien-behalten-p) "1" "0")) (set_tile "gefrorene_ausschliessen" (if (csv:gefrorene-ausschliessen-p) "1" "0")) + (set_tile "sivas_mit_bezeichnung" (if (csv:sivas-mit-bezeichnung-p) "1" "0")) ) (defun csv:export-konfig-accept ( / idx modus) @@ -2794,6 +2813,7 @@ (if modus (csv:export-modus-setzen modus)) (csv:hilfskopien-behalten-setzen (= (get_tile "hilfskopien_behalten") "1")) (csv:gefrorene-ausschliessen-setzen (= (get_tile "gefrorene_ausschliessen") "1")) + (csv:sivas-mit-bezeichnung-setzen (= (get_tile "sivas_mit_bezeichnung") "1")) ) (defun c:CSV_EXPORT_KONFIG () diff --git a/cfg/sivas.cfg b/cfg/sivas.cfg new file mode 100644 index 0000000..d3b1aa8 --- /dev/null +++ b/cfg/sivas.cfg @@ -0,0 +1,106 @@ +# ============================================================ +# sivas.cfg - Attribut->Sivas-Merkmal-Zuordnung fuer lib/export_sivas.py +# +# Deckt NUR die "einfachen" Faelle ab: eine Sivas-Merkmalnummer entspricht +# genau einem (ggf. per Fallback alternativen) Block-Attribut, optional mit +# festem Default und/oder einer benannten Transform-Funktion. Berechnete +# Werte, die von der Anzahl vorhandener Segmente abhaengen oder mehrere +# Attribute kombinieren (VarioFoerderer-Gefaelle-/Vario-Segmentpaare 4312- +# 4392/4353-4386, die ILS-Automation-Summenzeile) bleiben bewusst Python-Code +# in export_sivas.py - ein flaches Attribut-Mapping kann bedingte/berechnete +# Werte nicht abbilden. +# +# Format je Zeile (Abschnitt = Elementtyp, Key = Sivas-Merkmalnummer): +# merkmal = attribut +# merkmal = attribut : default +# merkmal = attribut | alt_attribut : default +# merkmal = attribut : default | transform +# +# attribut - Name des Block-Attributs (ssg-attrib-*, siehe jeweiliges +# .lsp-Modul). Fehlt es in der Zeichnung, greift default. +# | alt_attribut - optionaler Fallback-Attributname (aeltere Zeichnungen mit +# abweichendem Attributnamen, z.B. N_SEPARATOREN vor der +# Umbenennung zu ANZAHL_SEPARATOR). Wird nur genutzt, wenn +# attribut selbst fehlt. +# : default - Vorgabewert, falls weder attribut noch alt_attribut in +# der Zeichnung gesetzt sind. Fehlt ":", ist default "". +# | transform - Name einer Python-Funktion in export_sivas.py +# (TRANSFORMS-Dict), die den ermittelten Rohwert +# (Attribut- oder Default-Wert) in die Sivas-Anzeige +# umwandelt (z.B. "0"/"1" -> "Ja"/"Nein"). Bekannte Namen: +# ja_nein, geruest_typ_anzeige. Ein unbekannter Name +# bricht NICHT ab, der Rohwert wird unveraendert +# uebernommen (siehe sivas_mapping.anwenden). +# +# Abschnittsnamen entsprechen den bisherigen build_*_details-Funktionen in +# export_sivas.py: [gefaellestrecke], [kreisel], [eckrad], [geruest]. Die +# geruest-typ-Anzeige-Tabelle (ASCII-Attributwert -> Sivas-Klartext mit +# Umlauten) UND SEPARATOR_BEARBEITUNG_ANZEIGE bleiben eigene, kleine +# Abschnitte weiter unten. +# ============================================================ + +[gefaellestrecke] +2950= MONTAGEHOEHE_m : 0 +4384= : Gefällestrecke +4318= L_GF_m +4391= GF_WINKEL +4337= ANZAHL_SEPARATOR : 0 +4338= : ohne HARTCOAT HRC 50 ± 5 +4339= ANZAHL_SCANNER : 0 +3992= GERUEST_EINZELMODUL : 0 | ja_nein +3993= GERUEST_TYP : Schoenenberger Geruest | geruest_typ_anzeige +# 4300 (Streckengruppe "ein Objekt"/"mehrere Objekte") und 4311 (Anzahl der +# gruppierten Bloecke) bleiben Code (streckengruppe_objekt_text, anzahl-Param.). + +[kreisel] +2952= MONTAGEHOEHE_m : 0 +3511= : 1 +3515= ANZAHL_SEPARATOR | N_SEPARATOREN : 2 +3517= ANZAHL_SCANNER | N_SCANNER : 0 +3519= ANZAHL_RAMPEN | N_RAMPEN : 0 +3992= GERUEST_EINZELMODUL : 0 | ja_nein +3993= GERUEST_TYP : Schoenenberger Geruest | geruest_typ_anzeige +# 3510 (Kreiselart-Anzeigetext) und 3512 (Abstand in m, gerundet) bleiben +# Code (bedingte Umbenennung bzw. Umrechnung mm->m). + +[eckrad] +# Bewusst (noch) beschreibende Klartext-Keys statt Merkmalnummern (kein +# bekanntes Sivas-Merkmal fuer Eckrad-Geruest hinterlegt) - details_mit_ +# bezeichnung() in export_sivas.py laesst nicht-numerische Keys unveraendert. +# "ist_eins" liefert ein ECHTES bool (True/False), NICHT "Ja"/"Nein" wie +# ja_nein bei Kreisel/Gefaellestrecke - json.dumps schreibt dann true/false +# ohne Anfuehrungszeichen (bisheriges Verhalten von build_eckrad_details). +Geruest fuer Einzelmodul= GERUEST_EINZELMODUL : 0 | ist_eins +Geruestoption fuer Einzelmodul= GERUEST_TYP : Schoenenberger Geruest + +[geruest] +Farbe= FARBE + +[variofoerderer] +# Nur die von der Segmentanzahl UNABHAENGIGEN Top-Level-Felder einer VF_N- +# Streckengruppe (siehe build_variofoerderer_details in export_sivas.py). +# Die Gefaelle-/Vario-Segmentpaare (4312-4392, 4353/4354/4386) haengen an der +# Listenlaenge von L_GF_m/L_VF_m und bleiben Code. +2950= MONTAGEHOEHE_m : 0 +4329= GF_Bogen_L_90 : 0 +4330= GF_Bogen_L_60 : 0 +4331= GF_Bogen_L_30 : 0 +4332= GF_Bogen_R_90 : 0 +4333= GF_Bogen_R_60 : 0 +4334= GF_Bogen_R_30 : 0 +4373= VF_Bogen_A_90 : 0 +4374= VF_Bogen_A_60 : 0 +4375= VF_Bogen_A_30 : 0 +4376= VF_Bogen_I_90 : 0 +4377= VF_Bogen_I_60 : 0 +4378= VF_Bogen_I_30 : 0 +4381= ANZAHL_SEPARATOR : 0 +4382= : ohne HARTCOAT HRC 50 ± 5 +4383= ANZAHL_SCANNER : 0 +3992= GERUEST_EINZELMODUL : 0 | ja_nein +3993= GERUEST_TYP : Schoenenberger Geruest | geruest_typ_anzeige + +[geruest_typ_anzeige] +# ASCII-Attributwert (GERUEST_TYP) -> Sivas-Klartext mit Umlauten/Sonder- +# zeichen (Attribute selbst bleiben ASCII, siehe CLAUDE.md-Konvention). +Schoenenberger Geruest= Schönenberger-Gerüst diff --git a/dcl/csv_export_konfig.dcl b/dcl/csv_export_konfig.dcl index 9fc4fab..6fc938f 100644 --- a/dcl/csv_export_konfig.dcl +++ b/dcl/csv_export_konfig.dcl @@ -1,9 +1,10 @@ // ============================================================ // csv_export_konfig.dcl - CSV-Export Konfiguration (c:CSV_EXPORT_KONFIG, // export.lsp). Steuert *csv-export-modus* (Combobox mit drei sich -// gegenseitig ausschliessenden Eintraegen) sowie den Debug-Schalter -// csv_leave_proxies (Hilfskopien nach dem Export stehen lassen) und -// *csv-gefrorene-ausschliessen* (gefrorene Layer nicht exportieren). +// gegenseitig ausschliessenden Eintraegen), *csv-gefrorene-ausschliessen* +// (gefrorene Layer nicht exportieren), *csv-sivas-mit-bezeichnung* (Sivas- +// Export zusaetzlich mit Klartext-Bezeichnung schreiben) sowie den +// Debug-Schalter csv_leave_proxies (Hilfskopien nach dem Export stehen lassen). // // popup_list statt radio_column: set_tile/get_tile auf einer // boxed_radio_column liefern/erwarten den KEY des gewaehlten radio_button @@ -29,6 +30,11 @@ csv_export_konfig : dialog { label = "Bloecke auf gefrorenen Layern nicht exportieren"; } + : toggle { + key = "sivas_mit_bezeichnung"; + label = "Sivas Export mit Bezeichner"; + } + spacer; : boxed_column { diff --git a/lib/export_sivas.py b/lib/export_sivas.py index 6e425bb..7b42551 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -31,7 +31,7 @@ Neues CSV-Format: cfg/Merkmalsstamm_in_Sivas.xlsx fuer die Bezeichnung je Nummer. Aufruf: - python export_sivas.py + python export_sivas.py [--mit-bezeichnung] Argumente: export_raw.json - JSON-Datei mit allen INSERT-Entities aus der Zeichnung @@ -39,13 +39,15 @@ Argumente: fuer den Aufrufer export.lsp erhalten) 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. + --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. """ import json @@ -54,6 +56,7 @@ import os from export_blockpatterns import load_patterns, matches_any, csv_quote, safe_float, safe_int from merkmalstamm import lade_merkmalstamm, bezeichnung_fuer +import sivas_mapping BLOCKPATTERNS = load_patterns() @@ -81,11 +84,10 @@ def get_id(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", -} +# deutsche Schreibweise mit Umlauten/Sonderzeichen). Tabelle kommt aus +# cfg/sivas.cfg [geruest_typ_anzeige], nicht ueber die uebliche ssg-text- +# Sprachdatei, da rein Sivas-seitig. +GERUEST_TYP_ANZEIGE = sivas_mapping.load_flat_mapping("geruest_typ_anzeige") SEPARATOR_BEARBEITUNG_ANZEIGE = "ohne HARTCOAT HRC 50 ± 5" @@ -100,6 +102,20 @@ def ja_nein(flag_1_0): return "Ja" if flag_1_0 == "1" else "Nein" +def ist_eins(flag_1_0): + """Wandelt ein "0"/"1"-Attribut in ein echtes bool um (True/False im + JSON, NICHT "Ja"/"Nein" wie ja_nein - siehe build_eckrad_details).""" + return flag_1_0 == "1" + + +# --- Transform-Namen, wie sie in cfg/sivas.cfg hinter "|" stehen koennen --- +TRANSFORMS = { + "ja_nein": ja_nein, + "geruest_typ_anzeige": geruest_typ_anzeige, + "ist_eins": ist_eins, +} + + def streckengruppe_objekt_text(attribs): """Merkmal 4300 "Streckengruppe": "mehrere Objekte", wenn der Block ein kombinierter Streckengruppe-Block (Gefaelle+Vario, TYP-Attribut @@ -108,27 +124,24 @@ def streckengruppe_objekt_text(attribs): 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.""" - attribs = block.get("attribs", {}) + process_blocks), die reale Stueckzahl steckt nur in 4311. - 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")), - } + 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): @@ -203,6 +216,9 @@ def parse_str_list(csv_str): 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 @@ -216,12 +232,14 @@ def build_variofoerderer_details(block): 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", {}) - 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", "")) @@ -229,11 +247,9 @@ def build_variofoerderer_details(block): 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", - } + details = sivas_mapping.anwenden(SIVAS_MAPPING_VARIOFOERDERER, attribs, TRANSFORMS) + details["4300"] = "mehrere Objekte" + details["4311"] = "1" if len(gf_laengen) >= 1: details["4312"] = "Ja" @@ -249,85 +265,62 @@ def build_variofoerderer_details(block): 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"), - }) + 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 "" - 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()} +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") + + 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).""" + 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" - 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")), - } + 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 ein ILS Eckrad.""" + """Merkmale-Dict fuer ein ILS Eckrad. Vollstaendig aus cfg/sivas.cfg + [eckrad] (keine berechneten Werte).""" attribs = block.get("attribs", {}) - return { - "Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1", - "Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"), - } + return sivas_mapping.anwenden(SIVAS_MAPPING_ECKRAD, attribs, TRANSFORMS) def build_geruest_details(block): """Merkmale-Dict fuer eine GR_-Geruest-Baugruppe (siehe pattern_geruest in cfg/export.cfg, GeruestInsert.lsp *geruest-attrib-defs*). ARTINR/ BEZEICHNUNG/TEILEART/LAENGE wandern in feste Sivas-Spalten (sivasnr/ - bezeichnung/teileart/laenge_mm), FARBE bleibt als Rest-Attribut hier. - 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).""" + 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 { - "Farbe": attribs.get("FARBE", ""), - } + return sivas_mapping.anwenden(SIVAS_MAPPING_GERUEST, attribs, TRANSFORMS) # --- Bekannte Bloecke ignorieren --- @@ -890,7 +883,7 @@ def lesbarer_dateiname(output_csv): def main(): if len(sys.argv) < 4: - print("Aufruf: python export_sivas.py ") + print("Aufruf: python export_sivas.py [--mit-bezeichnung]") sys.exit(1) raw_json_path = sys.argv[1] @@ -898,6 +891,7 @@ def main(): # 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.") @@ -914,14 +908,16 @@ def main(): # 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}") + # 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__": diff --git a/lib/sivas_mapping.py b/lib/sivas_mapping.py new file mode 100644 index 0000000..2a6aa81 --- /dev/null +++ b/lib/sivas_mapping.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +sivas_mapping.py - Liest die Attribut->Sivas-Merkmal-Zuordnung aus +cfg/sivas.cfg und wendet sie auf ein Block-Attribut-Dict an. + +Einzige Quelle fuer die "einfachen" build_*_details-Zuordnungen in +export_sivas.py (siehe cfg/sivas.cfg fuer das Zeilenformat und die +Begruendung, warum nur die einfachen Faelle hier landen). +""" + +import configparser +import functools +import os + + +def cfg_path_from_env(default_dir=None): + """Pfad zu sivas.cfg: DXFM_CFG env var, sonst /cfg.""" + dxfm_cfg = os.environ.get("DXFM_CFG") + if dxfm_cfg: + return os.path.join(dxfm_cfg, "sivas.cfg") + base = default_dir or os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + return os.path.join(base, "cfg", "sivas.cfg") + + +def _parse_wert(roh): + """Zerlegt eine Config-Wertzeile "attribut|alt_attribut:default|transform" + in (attribut, alt_attribut_oder_None, default, transform_oder_None). + + Reihenfolge beim Parsen: erst am ":" in (attributteil, defaultteil) + trennen (nur der erste ":" zaehlt, ein Default-Text soll selbst keinen + ":" enthalten muessen), danach im defaultteil am LETZTEN "|" den + Transform-Namen abtrennen (ein Default-Text wie "ohne HARTCOAT..." + enthaelt kein "|"). attributteil wird am ERSTEN "|" in Attribut/ + Fallback-Attribut gesplittet. + """ + roh = roh.strip() + if ":" in roh: + attribut_teil, rest = roh.split(":", 1) + else: + attribut_teil, rest = roh, "" + + transform = None + default = rest.strip() + if "|" in rest: + default_teil, transform_teil = rest.rsplit("|", 1) + default = default_teil.strip() + transform = transform_teil.strip() or None + + attribut_teil = attribut_teil.strip() + alt_attribut = None + if "|" in attribut_teil: + attribut, alt_attribut = (p.strip() for p in attribut_teil.split("|", 1)) + else: + attribut = attribut_teil + alt_attribut = alt_attribut or None + + return (attribut or None, alt_attribut, default, transform) + + +@functools.lru_cache(maxsize=None) +def load_sivas_cfg(cfg_path): + """Liest sivas.cfg einmal je Pfad (Prozess-Cache).""" + parser = configparser.ConfigParser(inline_comment_prefixes=None) + parser.optionxform = str # Keys (Merkmalnummern/Klartext-Keys) case-sensitive erhalten + parser.read(cfg_path, encoding="utf-8") + return parser + + +def load_section_mapping(section, cfg_path=None): + """Liest einen Abschnitt aus sivas.cfg. + + Rueckgabe: dict merkmal -> (attribut, alt_attribut, default, transform), + wie von _parse_wert geliefert. Bei fehlender Datei/Sektion: leeres dict - + Aufrufer bekommt dann einfach keine Merkmale aus der Config (bisheriges + Verhalten vor der Config-Einfuehrung, kein harter Fehler). + """ + if cfg_path is None: + cfg_path = cfg_path_from_env() + parser = load_sivas_cfg(cfg_path) + mapping = {} + if parser.has_section(section): + for merkmal, roh in parser.items(section): + mapping[merkmal] = _parse_wert(roh) + return mapping + + +def load_flat_mapping(section, cfg_path=None): + """Liest einen Abschnitt als einfaches dict key -> value (z.B. + [geruest_typ_anzeige]: ASCII-Wert -> Sivas-Klartext), ohne das + attribut/default/transform-Format von load_section_mapping.""" + if cfg_path is None: + cfg_path = cfg_path_from_env() + parser = load_sivas_cfg(cfg_path) + if parser.has_section(section): + return {k: v.strip() for k, v in parser.items(section)} + return {} + + +def anwenden(section_mapping, attribs, transforms): + """Wendet eine per load_section_mapping() gelesene Zuordnung auf ein + Block-Attribut-Dict an. + + transforms: dict transform_name -> Funktion(wert) -> wert. Ein in der + Config genannter, aber hier unbekannter Transform-Name wird ignoriert + (Rohwert bleibt unveraendert) statt abzubrechen - ein Tippfehler in + sivas.cfg darf den Export nicht zum Stillstand bringen. + + Rueckgabe: dict merkmal -> wert. Ohne Transform wird der Rohwert zu str() + gewandelt (bisheriges Verhalten der build_*_details-Funktionen: alles + Merkmalnummer-Werte sind Strings). Ein Transform liefert seinen + Rueckgabewert UNVERAENDERT (z.B. build_eckrad_details erwartet fuer + "Geruest fuer Einzelmodul" ein echtes bool, kein "True"/"False"-String - + json.dumps schreibt dann true/false statt eines Strings). + """ + ergebnis = {} + for merkmal, (attribut, alt_attribut, default, transform) in section_mapping.items(): + if attribut is not None: + wert = attribs.get(attribut) + if wert is None and alt_attribut is not None: + wert = attribs.get(alt_attribut) + if wert is None: + wert = default + else: + wert = default + if transform: + fn = transforms.get(transform) + if fn is not None: + ergebnis[merkmal] = fn(wert) + continue + ergebnis[merkmal] = str(wert) + return ergebnis diff --git a/tests/test_run_all.lsp b/tests/test_run_all.lsp index c97976e..4399820 100644 --- a/tests/test_run_all.lsp +++ b/tests/test_run_all.lsp @@ -185,6 +185,16 @@ (setq auto-export nil) ) + ;; *export-test-override* (Lisp/export.lsp, csv:run-export) fuer die Dauer + ;; des Automatik-Laufs setzen, damit c:EXPORTCSV/c:EXPORTSIVAS nach + ;; tests/output statt in den normalen Ergebnisordner (DXFM_RESULTS) + ;; schreiben - ein Testrun soll vollstaendig in tests/output landen. + ;; Analog zu TEST_EXPORT_ALL (tests/test_export_all.lsp). WICHTIG: + ;; Absichtlich KEIN (setenv "DXFM_RESULTS" ...), siehe dortiger Kommentar. + (if auto-export + (setq *export-test-override* tests-out-dir) + ) + (princ "\n") (princ "\n================================================================") (princ "\n SSG_RUN_ALL_TESTS") @@ -412,6 +422,8 @@ ;; GUI wieder einschalten (Batch fertig). (if (car (atoms-family 1 '("SSG-GUI-AN"))) (ssg-gui-an)) + (setq *export-test-override* nil) + (princ "\n\n================================================================") (princ "\n SSG_RUN_ALL_TESTS abgeschlossen.") (princ (strcat "\n Ergebnisse in: " (getenv "DXFMAKRO") "/tests/output/"))