diff --git a/cfg/sivas_merkmal_zuordnung.md b/cfg/sivas_merkmal_zuordnung.md new file mode 100644 index 0000000..1ecd8e1 --- /dev/null +++ b/cfg/sivas_merkmal_zuordnung.md @@ -0,0 +1,154 @@ +# Sivas-Merkmal-Zuordnung (export_sivas.py) + +Diese Datei dokumentiert, welches Block-Attribut in `export_raw.json` welche +Sivas-Merkmalnummer im `details`-JSON von `export_sivas.py` befuellt (Spalte +`Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details`). +Bezeichnungen stammen aus `cfg/Merkmalsstamm_in_Sivas.xlsx`. Einzige Quelle +der Wahrheit ist der Code in `lib/export_sivas.py` - diese Datei ist ein +Backup/Nachschlagewerk, bei Aenderungen am Code bitte hier nachziehen. + +Stand: abgeglichen mit `lib/export_sivas.py` nach der Umstellung von +beschreibenden JSON-Keys auf reine Merkmalnummern (Sept. 2026). + +## ILS 2.0 Kreisel (SivasNr 6200, Dispogruppe 20) + +Gruppierung: gleiche Kreiselart+Abstand+Separatoren/Scanner/Rampen+Geruest +werden zu EINER Zeile zusammengefasst (`kreisel_signature`). Die Anzahl-Spalte +traegt die reale Stueckzahl der Gruppe. + +| Merkmalnr | Bezeichnung (Merkmalstamm) | Block-Attribut | Berechnung | +|---|---|---|---| +| 2952 | Kreiselhoehe in Meter | `MONTAGEHOEHE_m` | direkt | +| 3510 | Kreisel oder Kreisel mit PINBAND oder Verbindungskreisel | `KREISELART` | `"PIN"` -> `"Kreisel mit PINBAND"`, sonst `"Kreisel"` | +| 3511 | Kreisel - zu fertigende Menge | - | fest `"1"` (reale Stueckzahl steht in der Anzahl-Spalte der Zeile) | +| 3512 | Kreisel - Abstand (Kreiselachse A - Kreiselachse B) in Meter | `ABSTAND` | `round(ABSTAND/1000, 2)` | +| 3515 | Kreisel - Anzahl der Separatoren | `ANZAHL_SEPARATOR` (Fallback `N_SEPARATOREN`) | direkt | +| 3517 | Kreisel - Anzahl der Scanner | `ANZAHL_SCANNER` (Fallback `N_SCANNER`) | direkt | +| 3519 | Kreisel - Anzahl der Rampen | `ANZAHL_RAMPEN` (Fallback `N_RAMPEN`) | direkt | +| 3992 | Geruest fuer Einzelmodul | `GERUEST_EINZELMODUL` | `"1"` -> `"Ja"`, sonst `"Nein"` | +| 3993 | Geruestoption fuer Einzelmodul | `GERUEST_TYP` | Anzeige-Uebersetzung, siehe `GERUEST_TYP_ANZEIGE` (`"Schoenenberger Geruest"` -> `"Schoenenberger-Geruest"` mit Umlauten) | + +## ILS 2.0 Strecke - Streckengruppe VF_N (SivasNr 6220, Dispogruppe 20) + +Je VF_N-Block IMMER eine eigene Zeile (keine Gruppierung), Anzahl-Spalte +immer 1. Sivas kennt je Streckengruppe nur EIN Gefaelle-Segment-Paar +(001/002) und EIN Vario-Segment (001). + +| Merkmalnr | Bezeichnung (Merkmalstamm) | Block-Attribut | Berechnung | +|---|---|---|---| +| 2950 | Anlagenhoehe in Meter | `MONTAGEHOEHE_m` | direkt | +| 4300 | Streckengruppe | - | fest `"mehrere Objekte"` | +| 4311 | Strecken- zu fertigende Menge | - | fest `"1"` | +| 4312 | Streckengruppe - Gefaellestrecke (Segment 001 vorhanden) | `L_GF_m` (1. Wert vorhanden?) | `"Ja"`/`"Nein"` | +| 4318 | Streckengruppe - Gefaellestrecke 001-Laenge in Meter | `L_GF_m` (1. Wert) | nur wenn Segment 1 existiert | +| 4391 | Streckengruppe - Gefaellestrecke 001 -Winkel | `GF_WINKEL` (1. Wert) | `int(...)`, nur wenn Segment 1 existiert | +| 4314 | Streckengruppe - Gefaellestrecke 002 (Segment vorhanden) | `L_GF_m` (2. Wert vorhanden?) | `"Ja"`/`"Nein"` | +| 4319 | Streckengruppe - Gefaellestrecke 002-Laenge in Meter | `L_GF_m` (2. Wert) | nur wenn Segment 2 existiert | +| 4392 | Streckengruppe - Gefaellestrecke 002- Winkel | `GF_WINKEL` (2. Wert) | `int(...)`, nur wenn Segment 2 existiert | +| 4329-4334 | ...Kurven Gefaellestrecke links/rechts 90/60/30 Grad | `GF_Bogen_L_90/60/30`, `GF_Bogen_R_90/60/30` | direkt | +| 4337 | Streckengruppe - Anzahl der Separatoren - Gefaellestrecke | - | **Platzhalter, fest `"0"`** (Aufteilung Gefaelle-/Vario-Anteil noch nicht umgesetzt, siehe unten) | +| 4338 | Streckengruppe - Bearbeitung Gefaellestreckenteilstueck fuer Separator (829516050) | - | fest `SEPARATOR_BEARBEITUNG_ANZEIGE` = `"ohne HARTCOAT HRC 50 +/- 5"` | +| 4339 | Streckengruppe - Anzahl der Scanner (Gefaelle-Anteil) | - | **Platzhalter, fest `"0"`** | +| 4340 | Streckengruppe - Variofoerderer (vorhanden) | `L_VF_m` (vorhanden?) | `"Ja"`/`"Nein"` | +| 4341 | ...Anzahl der Motoren fuer Vario-Foerderer (links) | `MOTORSEITE` | Anzahl Eintraege == "links" | +| 4342 | ...Anzahl der Motoren fuer Vario-Foerderer (rechts) | `MOTORSEITE` | Anzahl Eintraege == "rechts" | +| 4353 | Streckengruppe - Variofoerderer 001-Laenge in Meter | `L_VF_m` (1. Wert) | nur wenn Vario-Segment existiert | +| 4354 | Streckengruppe - Variofoerderer 001-Richtung | `ANTRIEBFAHRTRICHTUNG` (1. Wert) | kleingeschrieben (`"Auf"` -> `"auf"`) | +| 4386 | Streckengruppe - Variofoerderer 001- Winkel | `VF_WINKEL` (1. Wert) | `int(...)` | +| 4373-4378 | ...Kurven Variofoerderer Aussen-/Innenbogen 90/60/30 Grad | `VF_Bogen_A_90/60/30`, `VF_Bogen_I_90/60/30` | direkt | +| 4381 | Streckengruppe - Anzahl der Separatoren - Variofoerderer | `ANZAHL_SEPARATOR` | **voller Blockwert** (Aufteilung noch offen, siehe 4337) | +| 4382 | (dieselbe Bezeichnung wie 4338 im Merkmalstamm) | - | fest `SEPARATOR_BEARBEITUNG_ANZEIGE` | +| 4383 | Streckengruppe - Anzahl der Scanner (Vario-Anteil) | `ANZAHL_SCANNER` | **voller Blockwert** | +| 3992 | Geruest fuer Einzelmodul | `GERUEST_EINZELMODUL` | wie Kreisel | +| 3993 | Geruestoption fuer Einzelmodul | `GERUEST_TYP` | wie Kreisel | + +**Offener Punkt:** 4337/4339 (Gefaelle-Anteil) stehen fest auf `"0"`, 4381/4383 +(Vario-Anteil) tragen den KOMPLETTEN `ANZAHL_SEPARATOR`/`ANZAHL_SCANNER`-Wert +des Blocks. Die korrekte Aufteilung auf beide Streckenabschnitte folgt erst, +wenn die Separator-/Scanner-Zuordnung im Zeichnungsmodell selbst ueberarbeitet +ist (siehe `build_variofoerderer_details` in `export_sivas.py`). + +## ILS 2.0 Gefaellestrecke - eigenstaendige GF_N-Gruppe (SivasNr 6220, Dispogruppe 20) + +Gruppierung: gleiche Masse (Hoehe/Laenge/Winkel/Separatoren/Scanner/Typ/ +Geruest) werden zu EINER Zeile zusammengefasst (`gefaellestrecke_signature`). +ANDERS ALS BEI KREISEL bleibt die Anzahl-Spalte fest auf `1` - die reale +Stueckzahl der Gruppe steht im Merkmal 4311. + +| Merkmalnr | Bezeichnung (Merkmalstamm) | Block-Attribut | Berechnung | +|---|---|---|---| +| 2950 | Anlagenhoehe in Meter | `MONTAGEHOEHE_m` | direkt | +| 4300 | Streckengruppe | `TYP` | `"Streckengruppe"` -> `"mehrere Objekte"`, sonst (`"Gefaellestrecke"`) -> `"ein Objekt"` | +| 4384 | Gefaellestrecke oder Vario-Foerderer | - | fest `"Gefaellestrecke"` | +| 4311 | Strecken- zu fertigende Menge | - | Anzahl der in dieser Zeile gruppierten, baugleichen GF_N-Bloecke (Gegenstueck zur Anzahl-Spalte, die hier fest 1 ist) | +| 4318 | Streckengruppe - Gefaellestrecke 001-Laenge in Meter | `L_GF_m` | direkt | +| 4391 | Streckengruppe - Gefaellestrecke 001 -Winkel | `GF_WINKEL` | direkt | +| 4337 | Streckengruppe - Anzahl der Separatoren - Gefaellestrecke | `ANZAHL_SEPARATOR` | direkt (kein Split noetig, kein Vario-Anteil vorhanden) | +| 4338 | Streckengruppe - Bearbeitung Gefaellestreckenteilstueck fuer Separator (829516050) | - | fest `SEPARATOR_BEARBEITUNG_ANZEIGE` | +| 4339 | Streckengruppe - Anzahl der Scanner | `ANZAHL_SCANNER` | direkt | +| 3992 | Geruest fuer Einzelmodul | `GERUEST_EINZELMODUL` | wie Kreisel | +| 3993 | Geruestoption fuer Einzelmodul | `GERUEST_TYP` | wie Kreisel | + +## ILS 2.0 Zusatzmodul (SivasNr 6215, Dispogruppe 20) + +BTMT-Beladestationen UND Abwurfstationen (Klammerbuegel-Entladung) in EINER +Zeile zusammengefasst. Anzahl-Spalte fest `1`, Bezeichnung leer, IDs beider +Blockarten kombiniert. + +| Merkmalnr | Bezeichnung (Merkmalstamm) | Quelle | Berechnung | +|---|---|---|---| +| 3501 | BT - Beladung (mit Vereinzeler, ohne Separator) | Vorhandensein von BTMT-Beladung-Bloecken (`pattern_btmt_beladung`) | `"Ja"`, wenn mind. 1 Block, sonst `"Nein"` | +| 3503 | Anzahl der BT - Beladung (mit Vereinzeler, ohne Separator) | Anzahl BTMT-Beladung-Bloecke | direkt gezaehlt | +| 3502 | Abwurf fuer Klammerbuegel Sitzbezuege / Kleiderbuegel | Vorhandensein von Abwurf-Bloecken (`pattern_btmt_entladung`) | `"Ja"`, wenn mind. 1 Block, sonst `"Nein"` | +| 3504 | Anzahl der Abwuerfe | Anzahl Abwurf-Bloecke | direkt gezaehlt | + +## ILS 2.0 Automation und Pneumatik - Gesamtanlage (SivasNr 6269, Dispogruppe 20) + +Eine Summenzeile pro Zeichnung, nur wenn mindestens ein ILS-Element (Kreisel/ +Eckrad/Variofoerderer/Gefaellestrecke/BTMT) vorhanden ist. + +| Merkmalnr | Bezeichnung (Merkmalstamm) | Quelle (counters/process_blocks) | Berechnung | +|---|---|---|---| +| 4000 | Automation | - | fest `"Ja"` (Zeile existiert nur bei `has_ils`) | +| 4400 | Anzahl der Kreisel | `anzahl_kreisel` | Anzahl aller Kreisel-Bloecke (vor Gruppierung) | +| 4401 | Anzahl der Kreisel mit PINBAND | `anzahl_kreisel_pinband` | | +| 4402 | Anzahl der Verbindungskreisel | `anzahl_verbindungskreisel` | aktuell nie befuellt (kein Verbindungskreisel-Typ erkannt) | +| 4403 | Anzahl der Weichen | `anzahl_weichen_ils` | aktuell nie befuellt (kein ILS-Weichen-Typ erkannt) | +| 4404 | Anzahl der Eckraeder | `anzahl_eckraeder` | | +| 4407 | Anzahl der Streckengruppen | `len(vario_list)` | Anzahl VF_N-Compound-Bloecke (TYP "Streckengruppe") - NICHT `anzahl_variofoerderer` (der zaehlt auch lose Strecke-Module mit) | +| 4408 | Anzahl der Motoren in Vario-Foerderer | `anzahl_motoren_vario` | Summe `ANZAHL_VF` ueber alle VF_N-Bloecke | +| 4409 | Anzahl der ETFs | `anzahl_etf` | aktuell nie befuellt | +| 4410 | Anzahl der Separatoren in der Anlage | `anzahl_separatoren` | Summe ueber Kreisel+VF_N+GF_N | +| 4411 | Anzahl der Scanner in der Anlage | `anzahl_scanner` | Summe ueber Kreisel+VF_N+GF_N | +| 4413 | Anzahl der Separatoren - Kreisel ohne PINBAND | `sep_kreisel_ohne_pin` | | +| 4414 | Anzahl der Separatoren - Kreisel mit PINBAND | `sep_kreisel_pin` | | +| 4417 | Laengen aller ETFS und Zusatzstuecke (Gefaellestrecken) | `laengen_etf` | aktuell immer `0.0`, NICHT auf 2 Nachkommastellen gerundet | +| 4418 | Laengen aller Gefaellestrecken | `laengen_gefaellestrecken` | auf 2 Nachkommastellen gerundet (`.2f`) | +| 4419 | Laengen aller Kreisel | `laengen_kreisel` | auf 2 Nachkommastellen gerundet (`.2f`) | +| 4420 | Laengen aller Vario-Foerderer | `laengen_variofoerderer` | NICHT gerundet (roher `str()`-Wert) | +| 4421 | Zusatzmodul - Anzahl der BT-Beladung | `anzahl_btbeladung` | | +| 4422 | Zusatzmodul - Anzahl der Abwurfstation | `anzahl_abwurfstationen` | | +| 4425 | Anzahl der einzelnen Gefaellestrecken | `anzahl_gefaellestrecken` | Anzahl aller GF_N-Bloecke (vor Gruppierung) | +| 4426 | Anzahl der einzelnen Vario-Foerderer | - | fest `"0"` (Konzept "einzelner Variofoerderer" getrennt von "Streckengruppe" gibt es nicht mehr) | +| 4100 | Pneumatik | - | fest `"Ja"` | + +## Nicht angefasst (ausserhalb dieser Umstellung) + +- **ILS 2.0 Eckrad** (`build_eckrad_details`): bleibt beim alten, + beschreibenden Key-Format (`"Geruest fuer Einzelmodul"`/`"Geruestoption + fuer Einzelmodul"`) - war nicht Teil der angefragten 5 Teilearten. +- **Geruest-Baugruppen** (`build_geruest_details`, `GR_*`-Bloecke): eigenes + Format ueber `ARTINR`/`BEZEICHNUNG`/`TEILEART`/`LAENGE` in den festen + CSV-Spalten, nur `FARBE` bleibt als details-Key - unveraendert. +- **Omniflo Bogen/Weiche/Gerade**: `details` bleibt leer (`{}`), keine + Merkmalnummern-Zuordnung noetig. + +## Bekannte offene Punkte + +1. **4337/4339 vs. 4381/4383** (Streckengruppe VF_N): Aufteilung der + Separatoren/Scanner auf Gefaelle- und Vario-Anteil ist ein Platzhalter + (siehe oben) - folgt nach der Neuzuordnung von Separator/Scanner zum + jeweiligen Streckenabschnitt im Zeichnungsmodell. +2. Zeichnungsseitige Datenpflege (kein Code-Thema): `GERUEST_EINZELMODUL` + muss am jeweiligen Block korrekt gesetzt sein (Dialog-Haekchen beim + Einfuegen) - der Export uebernimmt das Attribut unveraendert 1:1. diff --git a/lib/export_sivas.py b/lib/export_sivas.py index 0e3cd26..a8f2204 100644 --- a/lib/export_sivas.py +++ b/lib/export_sivas.py @@ -18,15 +18,31 @@ Neues CSV-Format: - Omniflo Gerade: gruppiert nach ARTINR-Attribut, Laenge[mm] summiert - ILS 2.0 Kreisel: gruppiert nach Kreiselart+Abstand+Anzahl Separatoren/ Scanner/Rampen, Anzahl summiert + - ILS 2.0 Gefaellestrecke: gruppiert nach identischen Massen, Anzahl-Spalte + bleibt 1 - die reale Stueckzahl steht im Merkmal 4311 (siehe + build_gefaellestrecke_details) + - ILS 2.0 Zusatzmodul: BTMT-Beladung UND Abwurfstationen in EINER Zeile, + Anzahl bleibt 1, Stueckzahlen stehen in 3503/3504 (siehe process_blocks) - ILS 2.0 Automation: Summierungszeile (nur wenn ILS-Elemente vorhanden) + details-JSON: Schluessel sind durchgaengig Sivas-Merkmalnummern (z.B. + "2952" fuer Kreiselhoehe) statt beschreibender Texte - siehe + cfg/Merkmalsstamm_in_Sivas.xlsx fuer die Bezeichnung je Nummer. + Aufruf: python export_sivas.py Argumente: export_raw.json - JSON-Datei mit allen INSERT-Entities aus der Zeichnung data_dir - Pfad zum data/-Verzeichnis (mit json/ Unterordner) - output.csv - Ziel-CSV-Datei + output.csv - Ziel-CSV-Datei (reine Merkmalnummern, fuer den + Sivas-Import) + + Zusaetzlich zu output.csv wird immer eine zweite Datei + "_mit_bezeichnung.csv" geschrieben: gleicher Inhalt, aber jeder + Merkmalnummer-Key im details-JSON wird um die zugehoerige Bezeichnung aus + cfg/Merkmalsstamm_in_Sivas.xlsx erweitert (" | ") - + reine Lesehilfe zur Pruefung, wird von Sivas selbst nicht eingelesen. """ import json @@ -34,6 +50,7 @@ import sys import os from export_blockpatterns import load_patterns, matches_any, csv_quote, safe_float, safe_int +from merkmalstamm import lade_merkmalstamm, bezeichnung_fuer BLOCKPATTERNS = load_patterns() @@ -94,19 +111,56 @@ def format_sivasnr(sivasnr): return ", ".join(p.strip() for p in str(sivasnr).split("+") if p.strip()) -def build_gefaellestrecke_details(block): - """Merkmale-Dict fuer eine einzelne ILS Gefaellestrecke (GF_N-Block).""" +# --- Sivas-Merkmalnummern: Anzeigetexte, die nicht 1:1 aus dem Attribut +# uebernommen werden koennen (Attribute bleiben ASCII, siehe CLAUDE.md- +# Konvention "Alle Texte in Dateien ohne Umlaute" - Sivas erwartet aber die +# deutsche Schreibweise mit Umlauten/Sonderzeichen). Nur hier zentral, nicht +# ueber die uebliche ssg-text-Sprachdatei, da rein Sivas-seitig. +GERUEST_TYP_ANZEIGE = { + "Schoenenberger Geruest": "Schönenberger-Gerüst", +} + +SEPARATOR_BEARBEITUNG_ANZEIGE = "ohne HARTCOAT HRC 50 ± 5" + + +def geruest_typ_anzeige(wert): + """Wandelt den ASCII-Attributwert GERUEST_TYP in die Sivas-Anzeige um.""" + return GERUEST_TYP_ANZEIGE.get(wert, wert) + + +def ja_nein(flag_1_0): + """Wandelt ein "0"/"1"-Attribut in die Sivas-Anzeige "Ja"/"Nein" um.""" + return "Ja" if flag_1_0 == "1" else "Nein" + + +def streckengruppe_objekt_text(attribs): + """Merkmal 4300 "Streckengruppe": "mehrere Objekte", wenn der Block ein + kombinierter Streckengruppe-Block (Gefaelle+Vario, TYP-Attribut + "Streckengruppe") ist, sonst "ein Objekt" (reine Gefaellestrecke).""" + typ = attribs.get("TYP", "Gefaellestrecke") + return "mehrere Objekte" if typ == "Streckengruppe" else "ein Objekt" + + +def build_gefaellestrecke_details(block, anzahl=1): + """Merkmale-Dict (Sivas-Merkmalnummern) fuer eine Gruppe gleichartiger + ILS Gefaellestrecken (GF_N-Bloecke). anzahl ist die Anzahl der in dieser + Zeile zusammengefassten, baugleichen Bloecke (4311) - anders als bei + Kreisel steht hier auf der Sivas-Hauptzeile selbst immer Anzahl=1 (siehe + process_blocks), die reale Stueckzahl steckt nur in 4311.""" attribs = block.get("attribs", {}) return { - "Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"), - "Typ": attribs.get("TYP", "Gefaellestrecke"), - "Laenge in Meter": attribs.get("L_GF_m", ""), - "Winkel": attribs.get("GF_WINKEL", ""), - "Anzahl der Separatoren": attribs.get("ANZAHL_SEPARATOR", "0"), - "Anzahl der Scanner": attribs.get("ANZAHL_SCANNER", "0"), - "Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1", - "Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"), + "2950": attribs.get("MONTAGEHOEHE_m", "0"), + "4300": streckengruppe_objekt_text(attribs), + "4384": "Gefällestrecke", + "4311": str(anzahl), + "4318": attribs.get("L_GF_m", ""), + "4391": attribs.get("GF_WINKEL", ""), + "4337": attribs.get("ANZAHL_SEPARATOR", "0"), + "4338": SEPARATOR_BEARBEITUNG_ANZEIGE, + "4339": attribs.get("ANZAHL_SCANNER", "0"), + "3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")), + "3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")), } @@ -220,62 +274,104 @@ def parse_str_list(csv_str): def build_variofoerderer_details(block): """ - Merkmale-Dict fuer eine VF_N-Streckengruppe (VarioFoerderer, ggf. - mehrsegmentig aus Gefaelle- und angetriebenen Foerderer-Teilen). + Merkmale-Dict (Sivas-Merkmalnummern) fuer eine VF_N-Streckengruppe + (VarioFoerderer, ggf. mehrsegmentig aus Gefaelle- und angetriebenen + Foerderer-Teilen). Sivas kennt je Streckengruppe nur EIN Gefaelle- + Segment-Paar (001/002) und EIN Vario-Segment (001) - ANZAHL_GF/ANZAHL_VF + ueber diese Grenze hinaus werden hier (noch) nicht abgebildet. + + Die Aufteilung der Separatoren/Scanner auf Gefaelle-Anteil (4337/4339) + vs. Vario-Anteil (4381/4383) ist NOCH NICHT umgesetzt (haengt an der noch + ausstehenden Neuzuordnung von Separator/Scanner zum jeweiligen + Streckenabschnitt) - bis dahin steht 4337/4339 fest auf "0" und der volle + Attributwert steht auf 4381/4383. """ attribs = block.get("attribs", {}) def to_int(key): return safe_int(attribs.get(key, "0") or "0") - return { - "Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"), - "Anzahl Foerdergruppen": to_int("ANZAHL_VF"), - "Anzahl Gefaellegruppen": to_int("ANZAHL_GF"), - "GruppenteileGefaelle": { - "AnzahlGeraden": to_int("ANZAHL_GF"), - "Laenge in Meter": parse_float_list(attribs.get("L_GF_m", "")), - "Winkel": parse_float_list(attribs.get("GF_WINKEL", "")), - "AnzahlKurvenLinks90": to_int("GF_Bogen_L_90"), - "AnzahlKurvenLinks60": to_int("GF_Bogen_L_60"), - "AnzahlKurvenLinks30": to_int("GF_Bogen_L_30"), - "AnzahlKurvenRechts90": to_int("GF_Bogen_R_90"), - "AnzahlKurvenRechts60": to_int("GF_Bogen_R_60"), - "AnzahlKurvenRechts30": to_int("GF_Bogen_R_30"), - }, - "GruppenteileAngetrieben": { - "MotorSeite": parse_str_list(attribs.get("MOTORSEITE", "")), - "AnzahlGeraden": to_int("ANZAHL_VF"), - "Laenge in Meter": parse_float_list(attribs.get("L_VF_m", "")), - "Winkel": parse_float_list(attribs.get("VF_WINKEL", "")), - "Foerderrichtung": parse_str_list(attribs.get("ANTRIEBFAHRTRICHTUNG", "")), - "AnzahlKurvenAussen90": to_int("VF_Bogen_A_90"), - "AnzahlKurvenAussen60": to_int("VF_Bogen_A_60"), - "AnzahlKurvenAussen30": to_int("VF_Bogen_A_30"), - "AnzahlKurvenInnen90": to_int("VF_Bogen_I_90"), - "AnzahlKurvenInnen60": to_int("VF_Bogen_I_60"), - "AnzahlKurvenInnen30": to_int("VF_Bogen_I_30"), - }, - "Anzahl der Separatoren": to_int("ANZAHL_SEPARATOR"), - "Anzahl der Scanner": to_int("ANZAHL_SCANNER"), - "Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1", - "Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"), + gf_laengen = parse_float_list(attribs.get("L_GF_m", "")) + gf_winkel = parse_float_list(attribs.get("GF_WINKEL", "")) + vf_laengen = parse_float_list(attribs.get("L_VF_m", "")) + vf_winkel = parse_float_list(attribs.get("VF_WINKEL", "")) + vf_richtung = parse_str_list(attribs.get("ANTRIEBFAHRTRICHTUNG", "")) + motorseiten = parse_str_list(attribs.get("MOTORSEITE", "")) + + details = { + "2950": attribs.get("MONTAGEHOEHE_m", "0"), + "4300": "mehrere Objekte", + "4311": "1", } + if len(gf_laengen) >= 1: + details["4312"] = "Ja" + details["4318"] = gf_laengen[0] + details["4391"] = str(int(gf_winkel[0])) if gf_winkel else "" + else: + details["4312"] = "Nein" + + if len(gf_laengen) >= 2: + details["4314"] = "Ja" + details["4319"] = gf_laengen[1] + details["4392"] = str(int(gf_winkel[1])) if len(gf_winkel) >= 2 else "" + else: + details["4314"] = "Nein" + + details.update({ + "4329": to_int("GF_Bogen_L_90"), + "4330": to_int("GF_Bogen_L_60"), + "4331": to_int("GF_Bogen_L_30"), + "4332": to_int("GF_Bogen_R_90"), + "4333": to_int("GF_Bogen_R_60"), + "4334": to_int("GF_Bogen_R_30"), + "4337": "0", + "4338": SEPARATOR_BEARBEITUNG_ANZEIGE, + "4339": "0", + "4340": "Ja" if vf_laengen else "Nein", + "4341": sum(1 for seite in motorseiten if seite.lower() == "links"), + "4342": sum(1 for seite in motorseiten if seite.lower() == "rechts"), + }) + + if vf_laengen: + details["4353"] = vf_laengen[0] + details["4354"] = vf_richtung[0].lower() if vf_richtung else "" + details["4386"] = str(int(vf_winkel[0])) if vf_winkel else "" + + details.update({ + "4373": to_int("VF_Bogen_A_90"), + "4374": to_int("VF_Bogen_A_60"), + "4375": to_int("VF_Bogen_A_30"), + "4376": to_int("VF_Bogen_I_90"), + "4377": to_int("VF_Bogen_I_60"), + "4378": to_int("VF_Bogen_I_30"), + "4381": attribs.get("ANZAHL_SEPARATOR", "0"), + "4382": SEPARATOR_BEARBEITUNG_ANZEIGE, + "4383": attribs.get("ANZAHL_SCANNER", "0"), + "3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")), + "3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")), + }) + return {k: str(v) for k, v in details.items()} + def build_kreisel_details(block): - """Merkmale-Dict fuer einen ILS Kreisel.""" + """Merkmale-Dict (Sivas-Merkmalnummern) fuer eine Gruppe gleichartiger + ILS Kreisel. 3511 ("Kreisel - zu fertigende Menge") steht bewusst fest + auf "1" - die reale Stueckzahl der Gruppe steht auf der Sivas-Hauptzeile + selbst (Anzahl-Spalte, siehe kreisel_groups in process_blocks).""" attribs = block.get("attribs", {}) abstand_m = str(round(safe_float(attribs.get("ABSTAND", "2300"), 2300.0) / 1000.0, 2)) + kreiselart = "Kreisel mit PINBAND" if attribs.get("KREISELART", "STANDARD") == "PIN" else "Kreisel" return { - "Hoehe in m": attribs.get("MONTAGEHOEHE_m", "0"), - "Kreiselart": attribs.get("KREISELART", "STANDARD"), - "Abstand (Kreiselachse A - Kreiselachse) in Meter": abstand_m, - "Anzahl der Separatoren": attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")), - "Anzahl der Scanner": attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")), - "Anzahl der Rampen": attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")), - "Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1", - "Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"), + "2952": attribs.get("MONTAGEHOEHE_m", "0"), + "3510": kreiselart, + "3511": "1", + "3512": abstand_m, + "3515": attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")), + "3517": attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")), + "3519": attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")), + "3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")), + "3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")), } @@ -325,8 +421,6 @@ ILS_SIVASNR = { "ils_weiche": ("6216", "ILS 2.0 Weiche"), "strecke": ("6220", "ILS 2.0 Strecke"), "gefaellestrecke": ("6220", "ILS 2.0 Gefaellestrecke"), - "bt_beladung": ("6215", "BT - Beladung (mit Vereinzeler, ohne Separator)"), - "bg_entladung": ("6215", "Abwurf fuer Klammerbuegel Sitzbezuege / Kleiderbuegel"), "automation": ("6269", "ILS 2.0 Automation und Pneumatik - Gesamtanlage"), } @@ -351,9 +445,7 @@ def process_blocks(blocks, lookup): vario_list = [] # Liste von VarioFoerderer-Compound-Bloecken (VF_N), je einer Zeile gefaelle_list = [] # Liste von Gefaellestrecke-Bloecken (GF_*), je einer Zeile btmt_beladung_ids = [] # IDs aller BTMT-Beladung*-Bloecke (eine gruppierte Zeile) - btmt_beladung_bezeichnungen = [] btmt_entladung_ids = [] # IDs aller SC_Entladung*-Bloecke (eine gruppierte Zeile) - btmt_entladung_bezeichnungen = [] geruest_list = [] # Liste von GR_-Geruest-Baugruppenbloecken, je einer Zeile counters = { @@ -375,7 +467,6 @@ def process_blocks(blocks, lookup): "anzahl_eckraeder": 0, "anzahl_gefaellestrecken": 0, "anzahl_variofoerderer": 0, - "anzahl_streckengruppen": 0, "laengen_gefaellestrecken": 0.0, "anzahl_motoren_vario": 0, "laengen_variofoerderer": 0.0, @@ -404,14 +495,12 @@ def process_blocks(blocks, lookup): # der ILS-Automation-Summierungszeile (siehe unten). if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_beladung", [])): btmt_beladung_ids.append(get_id(block)) - btmt_beladung_bezeichnungen.append(block.get("attribs", {}).get("Bezeichnung", "")) counters["anzahl_btbeladung"] += 1 continue # --- BTMT-Entladestation (Abwurfstation) --- if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_entladung", [])): btmt_entladung_ids.append(get_id(block)) - btmt_entladung_bezeichnungen.append(block.get("attribs", {}).get("Bezeichnung", "")) counters["anzahl_abwurfstationen"] += 1 continue @@ -542,8 +631,6 @@ def process_blocks(blocks, lookup): counters["laengen_kreisel"] += abstand_mm / 1000.0 counters["anzahl_separatoren"] += n_sep counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")) or "0") - if counters["anzahl_streckengruppen"] == 0: - counters["anzahl_streckengruppen"] = 1 continue # --- VarioFoerderer Compound-Block (VF_N) --- @@ -556,8 +643,7 @@ def process_blocks(blocks, lookup): counters["laengen_variofoerderer"] += safe_float(attribs.get("L_VF_m", "0") or "0") counters["anzahl_separatoren"] += safe_int(attribs.get("ANZAHL_SEPARATOR", "0") or "0") counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", "0") or "0") - if counters["anzahl_streckengruppen"] == 0: - counters["anzahl_streckengruppen"] = 1 + counters["anzahl_motoren_vario"] += safe_int(attribs.get("ANZAHL_VF", "0") or "0") continue # --- Gefaellestrecke Compound-Block (GF_N) --- @@ -569,8 +655,6 @@ def process_blocks(blocks, lookup): counters["laengen_gefaellestrecken"] += safe_float(attribs.get("L_GF_m", "0") or "0") counters["anzahl_separatoren"] += safe_int(attribs.get("ANZAHL_SEPARATOR", "0") or "0") counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", "0") or "0") - if counters["anzahl_streckengruppen"] == 0: - counters["anzahl_streckengruppen"] = 1 continue # --- ILS Strecke-Module (Vario/Staustrecke, einzelne Komponenten in Modelspace) --- @@ -695,39 +779,29 @@ def process_blocks(blocks, lookup): }) nr += 1 - # BTMT-Beladestationen: eine gruppierte Zeile fuer alle Instanzen (wie die - # losen Strecke-Elemente oben) - Stueckzahl zusaetzlich im Automation- - # Zaehler "Anzahl aller BTBeladung" (siehe counters oben). - if btmt_beladung_ids: - sivasnr, teileart = ILS_SIVASNR["bt_beladung"] + # ILS 2.0 Zusatzmodul: BTMT-Beladestationen UND BTMT-Entladestationen + # (Abwurfstationen) zusammen in EINER Zeile (Merkmale 3501-3504) statt wie + # bisher zwei separaten TeileArt-Zeilen ohne details - Anzahl-Spalte + # bleibt 1 (wie bei der Gefaellestrecke-Gruppe), die realen Stueckzahlen + # stehen in 3503/3504. + if btmt_beladung_ids or btmt_entladung_ids: + sivasnr, teileart = ILS_SIVASNR["zusatzmodul"] items.append({ "nr": nr, "teileart": teileart, "sivasnr": sivasnr, "sivasnr_quoted": True, - "bezeichnung": ", ".join(b for b in btmt_beladung_bezeichnungen if b), + "bezeichnung": "", "dispgruppe": 20, - "anzahl": len(btmt_beladung_ids), + "anzahl": 1, "laenge_mm": "", - "ids": btmt_beladung_ids, - "details": {}, - }) - nr += 1 - - # BTMT-Entladestationen (Abwurfstationen): analog gruppiert. - if btmt_entladung_ids: - sivasnr, teileart = ILS_SIVASNR["bg_entladung"] - items.append({ - "nr": nr, - "teileart": teileart, - "sivasnr": sivasnr, - "sivasnr_quoted": True, - "bezeichnung": ", ".join(b for b in btmt_entladung_bezeichnungen if b), - "dispgruppe": 20, - "anzahl": len(btmt_entladung_ids), - "laenge_mm": "", - "ids": btmt_entladung_ids, - "details": {}, + "ids": btmt_beladung_ids + btmt_entladung_ids, + "details": { + "3501": ja_nein("1" if btmt_beladung_ids else "0"), + "3503": str(len(btmt_beladung_ids)), + "3502": ja_nein("1" if btmt_entladung_ids else "0"), + "3504": str(len(btmt_entladung_ids)), + }, }) nr += 1 @@ -796,7 +870,10 @@ def process_blocks(blocks, lookup): for group in gefaelle_groups.values(): sivasnr, teileart = ILS_SIVASNR["gefaellestrecke"] - details = build_gefaellestrecke_details(group["block"]) + # Anders als bei Kreisel steht hier die reale Stueckzahl der Gruppe + # NICHT in der Anzahl-Spalte (die bleibt 1), sondern im Merkmal 4311 - + # siehe build_gefaellestrecke_details. + details = build_gefaellestrecke_details(group["block"], anzahl=len(group["ids"])) items.append({ "nr": nr, "teileart": teileart, @@ -804,7 +881,7 @@ def process_blocks(blocks, lookup): "sivasnr_quoted": True, "bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b), "dispgruppe": 20, - "anzahl": len(group["ids"]), + "anzahl": 1, "laenge_mm": "", "ids": group["ids"], "details": details, @@ -819,27 +896,36 @@ def process_blocks(blocks, lookup): or counters["anzahl_btbeladung"] > 0 or counters["anzahl_abwurfstationen"] > 0) if has_ils: + # 4407 "Anzahl der Streckengruppen" zaehlt NUR die kombinierten + # Gefaelle+Vario-Compound-Bloecke (VF_N, TYP "Streckengruppe") - das + # ist len(vario_list), NICHT counters["anzahl_variofoerderer"] (der + # zaehlt auch lose Strecke-Module ausserhalb eines VF_N mit). + # 4426 "Anzahl der einzelnen Vario-Foerderer" gibt es als eigenen + # Typ nicht mehr (nur noch Gefaellestrecke ODER Streckengruppe) und + # steht daher fest auf "0". automation_details = { - "Anzahl Kreisel": counters["anzahl_kreisel"], - "Anzahl Kreisel mit Pinband": counters["anzahl_kreisel_pinband"], - "Anzahl aller Separatoren in Kreisel mit PIN": counters["sep_kreisel_pin"], - "Anzahl aller Separatoren in Kreisel ohne PIN": counters["sep_kreisel_ohne_pin"], - "Anzahl Verbindungskreisel": counters["anzahl_verbindungskreisel"], - "Laengen aller Kreisel": counters["laengen_kreisel"], - "Anzahl der Weichen": counters["anzahl_weichen_ils"], - "Anzahl der Eckraeder": counters["anzahl_eckraeder"], - "Anzahl der einzelnen Gefaellestrecken": counters["anzahl_gefaellestrecken"], - "Anzahl der einzelnen Variofoerderer": counters["anzahl_variofoerderer"], - "Anzahl der Streckengruppen": counters["anzahl_streckengruppen"], - "Laengen aller Gefaellestrecken": counters["laengen_gefaellestrecken"], - "Anzahl der Motoren Variofoerderer": counters["anzahl_motoren_vario"], - "Laengen aller Variofoerderer": counters["laengen_variofoerderer"], - "Anzahl ETFs": counters["anzahl_etf"], - "Laengen aller ETFs": counters["laengen_etf"], - "Anzahl aller Separatoren": counters["anzahl_separatoren"], - "Anzahl aller Scanner": counters["anzahl_scanner"], - "Anzahl aller BTBeladung": counters["anzahl_btbeladung"], - "Anzahl aller Abwurfstationen": counters["anzahl_abwurfstationen"], + "4000": "Ja", + "4400": str(counters["anzahl_kreisel"]), + "4413": str(counters["sep_kreisel_ohne_pin"]), + "4401": str(counters["anzahl_kreisel_pinband"]), + "4414": str(counters["sep_kreisel_pin"]), + "4402": str(counters["anzahl_verbindungskreisel"]), + "4419": f"{counters['laengen_kreisel']:.2f}", + "4403": str(counters["anzahl_weichen_ils"]), + "4404": str(counters["anzahl_eckraeder"]), + "4425": str(counters["anzahl_gefaellestrecken"]), + "4426": "0", + "4407": str(len(vario_list)), + "4418": f"{counters['laengen_gefaellestrecken']:.2f}", + "4408": str(counters["anzahl_motoren_vario"]), + "4420": str(counters["laengen_variofoerderer"]), + "4409": str(counters["anzahl_etf"]), + "4417": str(counters["laengen_etf"]), + "4410": str(counters["anzahl_separatoren"]), + "4411": str(counters["anzahl_scanner"]), + "4421": str(counters["anzahl_btbeladung"]), + "4422": str(counters["anzahl_abwurfstationen"]), + "4100": "Ja", } auto_sivasnr, auto_teileart = ILS_SIVASNR["automation"] items.append({ @@ -857,9 +943,17 @@ def process_blocks(blocks, lookup): return items, counters -def format_csv_line(item): - """Formatiert eine CSV-Zeile im neuen Sivas-Format.""" - details_json = json.dumps(item["details"], ensure_ascii=False) +def format_csv_line(item, details=None): + """Formatiert eine CSV-Zeile im neuen Sivas-Format. + + details ueberschreibt item["details"] fuer die Ausgabe - genutzt von + details_mit_bezeichnung(), um dieselbe Zeile mit lesbaren Merkmal-Keys + in die zweite ("..._mit_bezeichnung.csv") Datei zu schreiben, ohne + item["details"] selbst zu veraendern. + """ + if details is None: + details = item["details"] + details_json = json.dumps(details, ensure_ascii=False) sivasnr = f'"{csv_quote(item["sivasnr"])}"' if item["sivasnr_quoted"] else item["sivasnr"] ids_str = ", ".join(str(i) for i in item.get("ids", []) if i) return ( @@ -875,6 +969,28 @@ def format_csv_line(item): ) +def details_mit_bezeichnung(details, merkmalstamm): + """Kopie von details, bei der jeder Merkmalnummer-Key um die Bezeichnung + aus dem Merkmalstamm erweitert wird (" | "). Keys, + die keine reine Zahl sind (aktuell nur bei Eckrad/Geruest, siehe + build_eckrad_details/build_geruest_details - dort noch beschreibende + Klartext-Keys statt Merkmalnummern), bleiben unveraendert.""" + neu = {} + for key, value in details.items(): + if key.isdigit(): + neu[f"{key} | {bezeichnung_fuer(key, merkmalstamm)}"] = value + else: + neu[key] = value + return neu + + +def lesbarer_dateiname(output_csv): + """Leitet den Dateinamen der Lesbarkeits-Kopie aus output_csv ab, z.B. + "500651_sivas.csv" -> "500651_sivas_mit_bezeichnung.csv".""" + stamm, endung = os.path.splitext(output_csv) + return f"{stamm}_mit_bezeichnung{endung}" + + def main(): if len(sys.argv) < 4: print("Aufruf: python export_sivas.py ") @@ -903,9 +1019,20 @@ def main(): f.write(header + "\n") for item in items: f.write(format_csv_line(item) + "\n") - print(f"[export_sivas] CSV geschrieben: {output_csv} ({len(items)} Zeilen)") + # Zweite, lesbare Kopie fuer die manuelle Pruefung (Merkmalnummer + + # Bezeichnung aus cfg/Merkmalsstamm_in_Sivas.xlsx) - wird von Sivas + # selbst NICHT eingelesen, nur output_csv ist der eigentliche Import. + merkmalstamm = lade_merkmalstamm() + output_csv_lesbar = lesbarer_dateiname(output_csv) + with open(output_csv_lesbar, "w", encoding="utf-8") as f: + f.write(header + "\n") + for item in items: + details = details_mit_bezeichnung(item["details"], merkmalstamm) + f.write(format_csv_line(item, details=details) + "\n") + print(f"[export_sivas] Lesbare Kopie geschrieben: {output_csv_lesbar}") + if __name__ == "__main__": main() diff --git a/lib/merkmalstamm.py b/lib/merkmalstamm.py new file mode 100644 index 0000000..93ee36a --- /dev/null +++ b/lib/merkmalstamm.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +merkmalstamm.py - Liest Merkmalnummer -> Bezeichnung aus +cfg/Merkmalsstamm_in_Sivas.xlsx. + +Bewusst ohne openpyxl (reine Standard-Library: zipfile + xml.etree) - +export.lsp/export:python-exe loest den Interpreter fuer EXPORTSIVAS ueber +Config/DXFM_PYTHON/Projekt-venv/PATH auf und garantiert dabei keine +zusaetzliche pip-Abhaengigkeit; ein .xlsx ist im Kern ein ZIP aus XML- +Dateien, das reicht hier ohne Zusatzpaket. +""" + +import os +import re +import zipfile +from functools import lru_cache +from xml.etree import ElementTree as ET + +_NS = {"main": "http://schemas.openxmlformats.org/spreadsheetml/2006/main"} + + +def merkmalstamm_pfad(): + """Pfad zu Merkmalsstamm_in_Sivas.xlsx: DXFM_CFG env var, sonst /cfg.""" + dxfm_cfg = os.environ.get("DXFM_CFG") + if dxfm_cfg: + return os.path.join(dxfm_cfg, "Merkmalsstamm_in_Sivas.xlsx") + basis = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + return os.path.join(basis, "cfg", "Merkmalsstamm_in_Sivas.xlsx") + + +def _spalte_zu_zahl(spalte): + zahl = 0 + for zeichen in spalte: + zahl = zahl * 26 + (ord(zeichen) - ord("A") + 1) + return zahl + + +def _spalte_aus_zellref(ref): + treffer = re.match(r"([A-Z]+)\d+", ref) + return treffer.group(1) if treffer else "A" + + +@lru_cache(maxsize=None) +def lade_merkmalstamm(pfad=None): + """Liest Sheet 1 (Spalte A=Merkmal-Nr., Spalte C=Bezeichnung) und liefert + ein Dict Merkmalnummer (str) -> Bezeichnung (str, von Excel- + Zeilenumbruch-Artefakten wie "_x000D_" bereinigt). Bei fehlender/kaputter + Datei: leeres Dict - Aufrufer faellt dann auf die reine Merkmalnummer + zurueck (siehe bezeichnung_fuer).""" + if pfad is None: + pfad = merkmalstamm_pfad() + if not os.path.exists(pfad): + return {} + + try: + with zipfile.ZipFile(pfad) as z: + shared = [] + if "xl/sharedStrings.xml" in z.namelist(): + baum = ET.fromstring(z.read("xl/sharedStrings.xml")) + for si in baum.findall("main:si", _NS): + texte = si.findall(".//main:t", _NS) + shared.append("".join(t.text or "" for t in texte)) + + sheet = ET.fromstring(z.read("xl/worksheets/sheet1.xml")) + mapping = {} + for row in sheet.findall(".//main:sheetData/main:row", _NS): + zellen = {} + for c in row.findall("main:c", _NS): + spalte = _spalte_zu_zahl(_spalte_aus_zellref(c.get("r", "A1"))) + v = c.find("main:v", _NS) + wert = v.text if v is not None else None + if c.get("t") == "s" and wert is not None: + wert = shared[int(wert)] + zellen[spalte] = wert + nummer = zellen.get(1) + bezeichnung = zellen.get(3) + if nummer and str(nummer).strip().isdigit() and bezeichnung: + bezeichnung = bezeichnung.replace("_x000D_", " ") + bezeichnung = re.sub(r"\s+", " ", bezeichnung).strip() + mapping[str(nummer).strip()] = bezeichnung + return mapping + except (KeyError, zipfile.BadZipFile, ET.ParseError): + return {} + + +def bezeichnung_fuer(nummer, mapping=None): + """Bezeichnung zu einer Merkmalnummer, Fallback: die Nummer selbst + (z.B. bei fehlendem Eintrag im Merkmalstamm).""" + if mapping is None: + mapping = lade_merkmalstamm() + return mapping.get(str(nummer), str(nummer))