Die Einzelteil-Proxy-Zeilen (Boegen, Streckenelemente, Umlenk-/Motorstation) aus der letzten Session sind fuer den Normalfall zu granular. Standardmaessig (*csv-export-alle-elemente* = nil) fasst csv:unitmark-aktualisieren sie jetzt zu je einer "Foerdereinheit"/"Gefaelleeinheit"-Zeile pro zusammenhaengendem VF-/GF-Abschnitt einer Kette zusammen, mit einem persistenten Rechteck-Marker (ZZ_UNIT_MARKER, analog ZZ_ASES_MARKER) auf der Bounding-Box-Vereinigung aller enthaltenen Elemente. Der bisherige Rohdaten-Export bleibt ueber (csv:export-alle-elemente-an) weiterhin verfuegbar. Einheiten-Grenzen kommen rein export-seitig aus der sepliste (SEP/AS/ES in echter Bau-Reihenfolge) plus dem bereits vorhandenen SSG_SEP_TYP je Separator - eine bauzeitige Markierung in vf_linienzug.lsp/vf_standard.lsp/ vf_etage.lsp waere deutlich invasiver gewesen (3 unabhaengige Baupfade ohne gemeinsamen Einstiegspunkt). csv:sepliste-partition (Lauflaengen-Kodierung ueber die typisierte sepliste) und csv:element-unit-zuordnung (Zuordnung der gesammelten Elemente per Kettenrichtungs-Projektion statt der nachweislich unzuverlaessigen csv:nested-welt-records-Listenreihenfolge) sind reine, dateifrei getestete Funktionen. Korrigiert nebenbei eine falsche Annahme: L_VF_m/L_GF_m zaehlen pro Segment, nicht pro Einheit - die tatsaechliche Einheiten-Anzahl kommt nur aus der sepliste-Typfolge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1068 lines
45 KiB
Python
1068 lines
45 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
export_sivas.py - Erzeugt Sivas-Export-CSV mit gruppierten Zeilen und Summierungszeilen.
|
|
|
|
Neues CSV-Format:
|
|
Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details
|
|
|
|
- Bezeichnung: Wert des Attributs "Bezeichnung" (Kreisel/Eckrad/VF/GF/Strecke).
|
|
Bei gruppierten Zeilen (mehrere Bloecke je Zeile) kommasepariert.
|
|
Bei Omniflo-Zeilen (Bogen/Weiche/Gerade) und der Automation-Summenzeile
|
|
bleibt das Feld leer, da es sich um Sammlungen gleichartiger Objekte handelt.
|
|
|
|
- IDs: kommaseparierte Liste der ID-Attribute aller Bloecke der Zeile
|
|
(eigene Spalte, nicht mehr Teil des details-JSON).
|
|
|
|
- Omniflo Bogen/Weiche: gruppiert nach SivasNummer, Anzahl summiert
|
|
- Omniflo Gerade: gruppiert nach ARTINR-Attribut, Laenge[mm] summiert
|
|
- ILS 2.0 Kreisel: gruppiert nach Kreiselart+Abstand+Anzahl Separatoren/
|
|
Scanner/Rampen, Anzahl summiert
|
|
- ILS 2.0 Gefaellestrecke: gruppiert nach identischen Massen, Anzahl-Spalte
|
|
bleibt 1 - die reale Stueckzahl steht im Merkmal 4311 (siehe
|
|
build_gefaellestrecke_details)
|
|
- ILS 2.0 Zusatzmodul: BTMT-Beladung UND Abwurfstationen in EINER Zeile,
|
|
Anzahl bleibt 1, Stueckzahlen stehen in 3503/3504 (siehe process_blocks)
|
|
- ILS 2.0 Automation: Summierungszeile (nur wenn ILS-Elemente vorhanden)
|
|
|
|
details-JSON: Schluessel sind durchgaengig Sivas-Merkmalnummern (z.B.
|
|
"2952" fuer Kreiselhoehe) statt beschreibender Texte - siehe
|
|
cfg/Merkmalsstamm_in_Sivas.xlsx fuer die Bezeichnung je Nummer.
|
|
|
|
Aufruf:
|
|
python export_sivas.py <export_raw.json> <data_dir> <output.csv>
|
|
|
|
Argumente:
|
|
export_raw.json - JSON-Datei mit allen INSERT-Entities aus der Zeichnung
|
|
data_dir - Pfad zum data/-Verzeichnis (mit json/ Unterordner)
|
|
output.csv - Ziel-CSV-Datei (reine Merkmalnummern, fuer den
|
|
Sivas-Import)
|
|
|
|
Zusaetzlich zu output.csv wird immer eine zweite Datei
|
|
"<output>_mit_bezeichnung.csv" geschrieben: gleicher Inhalt, aber jeder
|
|
Merkmalnummer-Key im details-JSON wird durch die zugehoerige Klartext-
|
|
Bezeichnung aus cfg/Merkmalsstamm_in_Sivas.xlsx ersetzt (Wert bleibt
|
|
unveraendert) - reine Lesehilfe zur Pruefung, wird von Sivas selbst nicht
|
|
eingelesen.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import os
|
|
|
|
from export_blockpatterns import load_patterns, matches_any, csv_quote, safe_float, safe_int
|
|
from merkmalstamm import lade_merkmalstamm, bezeichnung_fuer
|
|
|
|
BLOCKPATTERNS = load_patterns()
|
|
|
|
|
|
def load_json(path):
|
|
"""Laedt JSON, tolerant gegenueber BricsCAD-Ausgaben.
|
|
|
|
AutoLISP schreibt export_raw.json (write-line) im ANSI-Codepage des
|
|
Systems, nicht in UTF-8 - Sonderzeichen (z.B. Gradzeichen) koennen
|
|
daher nicht als UTF-8 dekodierbar sein. Fallback auf cp1252.
|
|
"""
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except UnicodeDecodeError:
|
|
with open(path, "r", encoding="cp1252") as f:
|
|
return json.load(f)
|
|
|
|
|
|
def build_lookup(boegen, weichen):
|
|
"""Erzeugt ein Dict: SivasNr (als String) -> (typ, eintrag)"""
|
|
lookup = {}
|
|
for b in boegen:
|
|
lookup[str(b["Sivasnr"])] = ("bogen", b)
|
|
for w in weichen:
|
|
lookup[str(w["Sivasnr"])] = ("weiche", w)
|
|
return lookup
|
|
|
|
|
|
def classify_weiche(eintrag):
|
|
"""Bestimmt den Weichen-Subtyp fuer die ILS-Automation-Zaehlung."""
|
|
wt = eintrag.get("WeichenTyp", "")
|
|
profil = eintrag.get("ProfilTyp", "")
|
|
if "WEICHENKOERPER" in profil.upper():
|
|
return "weichenkoerper"
|
|
if wt == "Einzelweiche":
|
|
return "einzelweiche"
|
|
if wt in ("Doppelweiche", "Dreiwegeweiche"):
|
|
return "doppelweiche"
|
|
if wt in ("Deltaweiche", "Dreifachweiche") or "DELTA" in profil.upper():
|
|
return "deltaweiche"
|
|
if wt == "Sternweiche" or "STAR" in profil.upper():
|
|
return "sternweiche"
|
|
return "einzelweiche"
|
|
|
|
|
|
def get_id(block):
|
|
"""Liest das eindeutige ID-Attribut eines Blocks (von ssg-id-check-all vergeben)."""
|
|
return block.get("attribs", {}).get("ID", "")
|
|
|
|
|
|
def format_sivasnr(sivasnr):
|
|
"""
|
|
Wandelt einen "+"-separierten Sivasnr-Wert (Rohformat aus den JSON-Katalogen,
|
|
dort weiterhin benoetigt fuer Dateinamen-Referenzen) in eine reine
|
|
kommaseparierte Liste fuer die Ausgabe (CSV/Block-Attribut) um.
|
|
"""
|
|
return ", ".join(p.strip() for p in str(sivasnr).split("+") if p.strip())
|
|
|
|
|
|
# --- Sivas-Merkmalnummern: Anzeigetexte, die nicht 1:1 aus dem Attribut
|
|
# uebernommen werden koennen (Attribute bleiben ASCII, siehe CLAUDE.md-
|
|
# Konvention "Alle Texte in Dateien ohne Umlaute" - Sivas erwartet aber die
|
|
# deutsche Schreibweise mit Umlauten/Sonderzeichen). Nur hier zentral, nicht
|
|
# ueber die uebliche ssg-text-Sprachdatei, da rein Sivas-seitig.
|
|
GERUEST_TYP_ANZEIGE = {
|
|
"Schoenenberger Geruest": "Schönenberger-Gerüst",
|
|
}
|
|
|
|
SEPARATOR_BEARBEITUNG_ANZEIGE = "ohne HARTCOAT HRC 50 ± 5"
|
|
|
|
|
|
def geruest_typ_anzeige(wert):
|
|
"""Wandelt den ASCII-Attributwert GERUEST_TYP in die Sivas-Anzeige um."""
|
|
return GERUEST_TYP_ANZEIGE.get(wert, wert)
|
|
|
|
|
|
def ja_nein(flag_1_0):
|
|
"""Wandelt ein "0"/"1"-Attribut in die Sivas-Anzeige "Ja"/"Nein" um."""
|
|
return "Ja" if flag_1_0 == "1" else "Nein"
|
|
|
|
|
|
def streckengruppe_objekt_text(attribs):
|
|
"""Merkmal 4300 "Streckengruppe": "mehrere Objekte", wenn der Block ein
|
|
kombinierter Streckengruppe-Block (Gefaelle+Vario, TYP-Attribut
|
|
"Streckengruppe") ist, sonst "ein Objekt" (reine Gefaellestrecke)."""
|
|
typ = attribs.get("TYP", "Gefaellestrecke")
|
|
return "mehrere Objekte" if typ == "Streckengruppe" else "ein Objekt"
|
|
|
|
|
|
def build_gefaellestrecke_details(block, anzahl=1):
|
|
"""Merkmale-Dict (Sivas-Merkmalnummern) fuer eine Gruppe gleichartiger
|
|
ILS Gefaellestrecken (GF_N-Bloecke). anzahl ist die Anzahl der in dieser
|
|
Zeile zusammengefassten, baugleichen Bloecke (4311) - anders als bei
|
|
Kreisel steht hier auf der Sivas-Hauptzeile selbst immer Anzahl=1 (siehe
|
|
process_blocks), die reale Stueckzahl steckt nur in 4311."""
|
|
attribs = block.get("attribs", {})
|
|
|
|
return {
|
|
"2950": attribs.get("MONTAGEHOEHE_m", "0"),
|
|
"4300": streckengruppe_objekt_text(attribs),
|
|
"4384": "Gefällestrecke",
|
|
"4311": str(anzahl),
|
|
"4318": attribs.get("L_GF_m", ""),
|
|
"4391": attribs.get("GF_WINKEL", ""),
|
|
"4337": attribs.get("ANZAHL_SEPARATOR", "0"),
|
|
"4338": SEPARATOR_BEARBEITUNG_ANZEIGE,
|
|
"4339": attribs.get("ANZAHL_SCANNER", "0"),
|
|
"3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")),
|
|
"3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")),
|
|
}
|
|
|
|
|
|
def kreisel_signature(block):
|
|
"""
|
|
Schluessel zur Gruppierung gleichartiger Kreisel: Kreiselart, Abstand,
|
|
Anzahl der Separatoren, Anzahl der Scanner, Anzahl der Rampen sowie
|
|
Geruest fuer Einzelmodul/Geruestoption entscheiden, ob Kreisel in
|
|
derselben Sivas-Zeile summiert werden.
|
|
"""
|
|
attribs = block.get("attribs", {})
|
|
return (
|
|
attribs.get("KREISELART", "STANDARD"),
|
|
attribs.get("ABSTAND", "2300"),
|
|
attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")),
|
|
attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")),
|
|
attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")),
|
|
attribs.get("GERUEST_EINZELMODUL", "0"),
|
|
attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
|
|
)
|
|
|
|
|
|
def eckrad_signature(block):
|
|
"""
|
|
Schluessel zur Gruppierung gleichartiger Eckraeder: Geruest fuer
|
|
Einzelmodul/Geruestoption entscheiden, ob Eckraeder in derselben
|
|
Sivas-Zeile summiert werden.
|
|
"""
|
|
attribs = block.get("attribs", {})
|
|
return (
|
|
attribs.get("GERUEST_EINZELMODUL", "0"),
|
|
attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
|
|
)
|
|
|
|
|
|
def gefaellestrecke_signature(block):
|
|
"""
|
|
Schluessel zur Gruppierung gleichartiger Gefaellestrecken: Bloecke mit
|
|
identischen Massen (Hoehe, Laenge, Winkel, Separatoren, Scanner, Typ,
|
|
Geruest fuer Einzelmodul/Geruestoption) landen in derselben Sivas-Zeile -
|
|
nur IDs/Bezeichnung unterscheiden sich.
|
|
"""
|
|
attribs = block.get("attribs", {})
|
|
return (
|
|
attribs.get("HOEHE_VON_mm", "0"),
|
|
attribs.get("HOEHE_BIS_mm", "0"),
|
|
attribs.get("L_GF_m", ""),
|
|
attribs.get("GF_WINKEL", ""),
|
|
attribs.get("ANZAHL_SEPARATOR", "0"),
|
|
attribs.get("ANZAHL_SCANNER", "0"),
|
|
attribs.get("TYP", "Gefaellestrecke"),
|
|
attribs.get("GERUEST_EINZELMODUL", "0"),
|
|
attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
|
|
)
|
|
|
|
|
|
def expand_tef_components(sivasnr_parts, sivasnr_tef):
|
|
"""Ersetzt "0_BG*"-Platzhalter-Teile in sivasnr_parts durch die dahinter
|
|
liegenden echten TEF-Sivasnummern (Katalogfeld SivasnrTEF).
|
|
|
|
"0_BG*"-Werte im Sivasnr-Rohwert (z.B. "834372404+0_BG071090") sind KEINE
|
|
echten Sivasnummern, sondern reine Blocknamen-Platzhalter fuer eine TEF-
|
|
Baugruppe aus mehreren realen Teilen - "0_BG071090" steht z.B. fuer die
|
|
drei echten Teile "0_B10071"+"0_B10090"+"0_B10090" (siehe SivasnrTEF im
|
|
Katalog, omniflo_weichen.json/omniflo_boegen.json). Ohne diese Aufloesung
|
|
wuerde "0_BG071090" selbst als (nicht existente) Sivasnummer gezaehlt und
|
|
die echten Teile (u.a. 0_B10071) fehlten in der Summierung.
|
|
Reale TEF-Einzelteile, die bereits direkt im Sivasnr-Rohwert stehen
|
|
(Praefix "0_B", aber nicht "0_BG", z.B. "821104026+0_B10071"), sind schon
|
|
echte Sivasnummern und werden unveraendert uebernommen.
|
|
Bei mehreren "0_BG*"-Platzhaltern im selben Rohwert (TEF beidseitig) wird
|
|
SivasnrTEF zu gleichen Teilen aufgeteilt - der Katalog listet die Teile
|
|
je Seite hintereinander in SivasnrTEF.
|
|
"""
|
|
bg_count = sum(1 for p in sivasnr_parts if p.startswith("0_BG"))
|
|
if bg_count == 0:
|
|
return list(sivasnr_parts)
|
|
|
|
tef_parts = [p.strip() for p in str(sivasnr_tef or "").split("+") if p.strip()]
|
|
chunk_size = len(tef_parts) // bg_count if bg_count else 0
|
|
|
|
expanded = []
|
|
tef_index = 0
|
|
for part in sivasnr_parts:
|
|
if part.startswith("0_BG"):
|
|
expanded.extend(tef_parts[tef_index:tef_index + chunk_size])
|
|
tef_index += chunk_size
|
|
else:
|
|
expanded.append(part)
|
|
return expanded
|
|
|
|
|
|
def parse_float_list(csv_str):
|
|
"""Kommagetrennten Attribut-String (z.B. L_GF_m) in Liste von floats."""
|
|
out = []
|
|
for part in str(csv_str or "").split(","):
|
|
part = part.strip()
|
|
if not part:
|
|
continue
|
|
try:
|
|
out.append(float(part))
|
|
except ValueError:
|
|
pass
|
|
return out
|
|
|
|
|
|
def parse_str_list(csv_str):
|
|
"""Kommagetrennten Attribut-String (z.B. MOTORSEITE) in Liste von Strings."""
|
|
return [p.strip() for p in str(csv_str or "").split(",") if p.strip()]
|
|
|
|
|
|
def build_variofoerderer_details(block):
|
|
"""
|
|
Merkmale-Dict (Sivas-Merkmalnummern) fuer eine VF_N-Streckengruppe
|
|
(VarioFoerderer, ggf. mehrsegmentig aus Gefaelle- und angetriebenen
|
|
Foerderer-Teilen). Sivas kennt je Streckengruppe nur EIN Gefaelle-
|
|
Segment-Paar (001/002) und EIN Vario-Segment (001) - ANZAHL_GF/ANZAHL_VF
|
|
ueber diese Grenze hinaus werden hier (noch) nicht abgebildet.
|
|
|
|
Die Aufteilung der Separatoren/Scanner auf Gefaelle-Anteil (4337/4339)
|
|
vs. Vario-Anteil (4381/4383) ist NOCH NICHT umgesetzt (haengt an der noch
|
|
ausstehenden Neuzuordnung von Separator/Scanner zum jeweiligen
|
|
Streckenabschnitt) - bis dahin steht 4337/4339 fest auf "0" und der volle
|
|
Attributwert steht auf 4381/4383.
|
|
"""
|
|
attribs = block.get("attribs", {})
|
|
|
|
def to_int(key):
|
|
return safe_int(attribs.get(key, "0") or "0")
|
|
|
|
gf_laengen = parse_float_list(attribs.get("L_GF_m", ""))
|
|
gf_winkel = parse_float_list(attribs.get("GF_WINKEL", ""))
|
|
vf_laengen = parse_float_list(attribs.get("L_VF_m", ""))
|
|
vf_winkel = parse_float_list(attribs.get("VF_WINKEL", ""))
|
|
vf_richtung = parse_str_list(attribs.get("ANTRIEBFAHRTRICHTUNG", ""))
|
|
motorseiten = parse_str_list(attribs.get("MOTORSEITE", ""))
|
|
|
|
details = {
|
|
"2950": attribs.get("MONTAGEHOEHE_m", "0"),
|
|
"4300": "mehrere Objekte",
|
|
"4311": "1",
|
|
}
|
|
|
|
if len(gf_laengen) >= 1:
|
|
details["4312"] = "Ja"
|
|
details["4318"] = gf_laengen[0]
|
|
details["4391"] = str(int(gf_winkel[0])) if gf_winkel else ""
|
|
else:
|
|
details["4312"] = "Nein"
|
|
|
|
if len(gf_laengen) >= 2:
|
|
details["4314"] = "Ja"
|
|
details["4319"] = gf_laengen[1]
|
|
details["4392"] = str(int(gf_winkel[1])) if len(gf_winkel) >= 2 else ""
|
|
else:
|
|
details["4314"] = "Nein"
|
|
|
|
details.update({
|
|
"4329": to_int("GF_Bogen_L_90"),
|
|
"4330": to_int("GF_Bogen_L_60"),
|
|
"4331": to_int("GF_Bogen_L_30"),
|
|
"4332": to_int("GF_Bogen_R_90"),
|
|
"4333": to_int("GF_Bogen_R_60"),
|
|
"4334": to_int("GF_Bogen_R_30"),
|
|
"4337": "0",
|
|
"4338": SEPARATOR_BEARBEITUNG_ANZEIGE,
|
|
"4339": "0",
|
|
"4340": "Ja" if vf_laengen else "Nein",
|
|
"4341": sum(1 for seite in motorseiten if seite.lower() == "links"),
|
|
"4342": sum(1 for seite in motorseiten if seite.lower() == "rechts"),
|
|
})
|
|
|
|
if vf_laengen:
|
|
details["4353"] = vf_laengen[0]
|
|
details["4354"] = vf_richtung[0].lower() if vf_richtung else ""
|
|
details["4386"] = str(int(vf_winkel[0])) if vf_winkel else ""
|
|
|
|
details.update({
|
|
"4373": to_int("VF_Bogen_A_90"),
|
|
"4374": to_int("VF_Bogen_A_60"),
|
|
"4375": to_int("VF_Bogen_A_30"),
|
|
"4376": to_int("VF_Bogen_I_90"),
|
|
"4377": to_int("VF_Bogen_I_60"),
|
|
"4378": to_int("VF_Bogen_I_30"),
|
|
"4381": attribs.get("ANZAHL_SEPARATOR", "0"),
|
|
"4382": SEPARATOR_BEARBEITUNG_ANZEIGE,
|
|
"4383": attribs.get("ANZAHL_SCANNER", "0"),
|
|
"3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")),
|
|
"3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")),
|
|
})
|
|
return {k: str(v) for k, v in details.items()}
|
|
|
|
|
|
def build_kreisel_details(block):
|
|
"""Merkmale-Dict (Sivas-Merkmalnummern) fuer eine Gruppe gleichartiger
|
|
ILS Kreisel. 3511 ("Kreisel - zu fertigende Menge") steht bewusst fest
|
|
auf "1" - die reale Stueckzahl der Gruppe steht auf der Sivas-Hauptzeile
|
|
selbst (Anzahl-Spalte, siehe kreisel_groups in process_blocks)."""
|
|
attribs = block.get("attribs", {})
|
|
abstand_m = str(round(safe_float(attribs.get("ABSTAND", "2300"), 2300.0) / 1000.0, 2))
|
|
kreiselart = "Kreisel mit PINBAND" if attribs.get("KREISELART", "STANDARD") == "PIN" else "Kreisel"
|
|
return {
|
|
"2952": attribs.get("MONTAGEHOEHE_m", "0"),
|
|
"3510": kreiselart,
|
|
"3511": "1",
|
|
"3512": abstand_m,
|
|
"3515": attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")),
|
|
"3517": attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")),
|
|
"3519": attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")),
|
|
"3992": ja_nein(attribs.get("GERUEST_EINZELMODUL", "0")),
|
|
"3993": geruest_typ_anzeige(attribs.get("GERUEST_TYP", "Schoenenberger Geruest")),
|
|
}
|
|
|
|
|
|
def build_eckrad_details(block):
|
|
"""Merkmale-Dict fuer ein ILS Eckrad."""
|
|
attribs = block.get("attribs", {})
|
|
return {
|
|
"Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1",
|
|
"Geruestoption fuer Einzelmodul": attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
|
|
}
|
|
|
|
|
|
def build_geruest_details(block):
|
|
"""Merkmale-Dict fuer eine GR_-Geruest-Baugruppe (siehe pattern_geruest in
|
|
cfg/export.cfg, GeruestInsert.lsp *geruest-attrib-defs*). ARTINR/
|
|
BEZEICHNUNG/TEILEART/LAENGE wandern in feste Sivas-Spalten (sivasnr/
|
|
bezeichnung/teileart/laenge_mm), FARBE bleibt als Rest-Attribut hier.
|
|
LAYER ist reine BricsCAD-Zeichnungsinformation ohne Sivas-Bedeutung und
|
|
wird bewusst NICHT mit exportiert (anders als im CSV-Export
|
|
export_csv.py, wo sie fuer die Zuordnung in der Zeichnung nuetzlich
|
|
bleibt)."""
|
|
attribs = block.get("attribs", {})
|
|
return {
|
|
"Farbe": attribs.get("FARBE", ""),
|
|
}
|
|
|
|
|
|
# --- Bekannte Bloecke ignorieren ---
|
|
SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"]))
|
|
|
|
# Eigenstaendige Separator_SP/S-LP- und Scanner-Bloecke werden von der Lisp-
|
|
# Sammlung (csv:collect-export-blocks) zwar mit in export_raw.json geschrieben
|
|
# (export_csv.py gibt ihnen dort eigene TeileArt-Zeilen), aber HIER bewusst
|
|
# NICHT in eine eigene Sivas-Zeile uebernommen: ihre Stueckzahl wird bereits
|
|
# ueber die ANZAHL_SEPARATOR/ANZAHL_SCANNER-Attribute des zugehoerigen
|
|
# Kreisel/GF/VF-Blocks gezaehlt (siehe kreisel_signature/build_*_details
|
|
# oben) - ein zusaetzlicher eigener Eintrag wuerde doppelt zaehlen.
|
|
#
|
|
# 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:unitmark-aktualisieren, 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, lookup):
|
|
"""
|
|
Verarbeitet alle Bloecke.
|
|
|
|
Rueckgabe:
|
|
items - Liste der CSV-Zeilen als Dicts
|
|
counters - Zaehler fuer ILS-Automation-Summierungszeile
|
|
"""
|
|
# Gruppen: sivasnr_str -> {anzahl, ids}
|
|
bogen_groups = {} # sivasnr -> dict
|
|
weiche_groups = {} # sivasnr -> dict
|
|
# Gerade: artinr_str -> {anzahl, laenge_mm_total, ids}
|
|
gerade_groups = {}
|
|
kreisel_list = [] # Liste von blocks
|
|
eckrad_groups = {} # eckrad_signature(block) -> {"ids", "bezeichnungen", "block"}
|
|
strecke_ids = [] # IDs loser Strecke-Elemente (Vario/Staustrecke ausserhalb VF_N)
|
|
strecke_bezeichnungen = [] # Bezeichnung-Attribut loser Strecke-Elemente
|
|
vario_list = [] # Liste von VarioFoerderer-Compound-Bloecken (VF_N), je einer Zeile
|
|
gefaelle_list = [] # Liste von Gefaellestrecke-Bloecken (GF_*), je einer Zeile
|
|
btmt_beladung_ids = [] # IDs aller BTMT-Beladung*-Bloecke (eine gruppierte Zeile)
|
|
btmt_entladung_ids = [] # IDs aller SC_Entladung*-Bloecke (eine gruppierte Zeile)
|
|
geruest_list = [] # Liste von GR_-Geruest-Baugruppenbloecken, je einer Zeile
|
|
|
|
counters = {
|
|
"anzahl_boegen": 0,
|
|
"anzahl_weichenkoerper": 0,
|
|
"anzahl_einzelweichen": 0,
|
|
"anzahl_deltaweichen": 0,
|
|
"anzahl_doppel_dreiwegeweichen": 0,
|
|
"anzahl_sternweichen": 0,
|
|
"gesamtlaenge_ap110": 0.0,
|
|
"gesamtlaenge_ap60": 0.0,
|
|
"anzahl_kreisel": 0,
|
|
"anzahl_kreisel_pinband": 0,
|
|
"sep_kreisel_pin": 0,
|
|
"sep_kreisel_ohne_pin": 0,
|
|
"anzahl_verbindungskreisel": 0,
|
|
"laengen_kreisel": 0.0,
|
|
"anzahl_weichen_ils": 0,
|
|
"anzahl_eckraeder": 0,
|
|
"anzahl_gefaellestrecken": 0,
|
|
"anzahl_variofoerderer": 0,
|
|
"laengen_gefaellestrecken": 0.0,
|
|
"anzahl_motoren_vario": 0,
|
|
"laengen_variofoerderer": 0.0,
|
|
"anzahl_etf": 0,
|
|
"laengen_etf": 0.0,
|
|
"anzahl_separatoren": 0,
|
|
"anzahl_scanner": 0,
|
|
"anzahl_btbeladung": 0,
|
|
"anzahl_abwurfstationen": 0,
|
|
"anzahl_geruest": 0,
|
|
}
|
|
|
|
for block in blocks:
|
|
bname = block.get("block_name", "")
|
|
|
|
if bname in SKIP_BLOCKS:
|
|
continue
|
|
|
|
# Separator/Scanner: siehe Kommentar bei SEP_SCANNER_PATTERNS oben.
|
|
if matches_any(bname, SEP_SCANNER_PATTERNS):
|
|
continue
|
|
|
|
# --- BTMT-Beladestation ---
|
|
# Anders als Separator/Scanner wird sie von keinem Carrier-Attribut
|
|
# mitgezaehlt, darum eigene (gruppierte) Zeile UND eigener Zaehler in
|
|
# der ILS-Automation-Summierungszeile (siehe unten).
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_beladung", [])):
|
|
btmt_beladung_ids.append(get_id(block))
|
|
counters["anzahl_btbeladung"] += 1
|
|
continue
|
|
|
|
# --- BTMT-Entladestation (Abwurfstation) ---
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_entladung", [])):
|
|
btmt_entladung_ids.append(get_id(block))
|
|
counters["anzahl_abwurfstationen"] += 1
|
|
continue
|
|
|
|
# --- Eingangs-/Ausgangssymbol (Materialfluss-Ein-/Ausgang) ---
|
|
# Bewusst NUR im CSV-Export als eigene Zeile (siehe export_csv.py) -
|
|
# hier ohne Sivas-Zeile und ohne eigenen Zaehler ueberspringen. Ein
|
|
# Eingangs-/Ausgangssymbol ist eine reine Fluss-Markierung ohne Sivas-
|
|
# Artikel; ein eigener Sivas-Eintrag wuerde die Stueckliste verfaelschen
|
|
# (Nutzeranforderung: Eingang/Ausgang nur im CSV-Export).
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_eingang", [])) or \
|
|
matches_any(bname, BLOCKPATTERNS.get("pattern_ausgang", [])):
|
|
continue
|
|
|
|
# --- Geruest-Baugruppe ---
|
|
# Anders als BTMT/Separator je Block eine EIGENE Zeile statt gruppiert:
|
|
# ARTINR (Sivas-IDs) und Laenge/Farbe koennen von Block zu Block
|
|
# unterschiedlich sein (freie Basis-Variante+Anbauteile-Auswahl je
|
|
# GERUEST_*-Aufruf), eine Gruppierung wuerde das verschleiern - wie
|
|
# bei VF_N/Kreisel oben, die aus demselben Grund ebenfalls einzeln
|
|
# bleiben.
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_geruest", [])):
|
|
geruest_list.append(block)
|
|
counters["anzahl_geruest"] += 1
|
|
continue
|
|
|
|
# --- Omniflo Bogen oder Weiche (erkannt via Katalog) ---
|
|
if bname in lookup:
|
|
typ, eintrag = lookup[bname]
|
|
sivasnr_raw = eintrag.get("Sivasnr", bname)
|
|
sivasnr_parts = [p.strip() for p in str(sivasnr_raw).split("+")]
|
|
has_tef = any(p.startswith("0_B") for p in sivasnr_parts)
|
|
block_id = get_id(block)
|
|
|
|
if typ == "bogen":
|
|
if has_tef and len(sivasnr_parts) > 1:
|
|
# Multi-Komponenten: jede Komponente separat gruppieren
|
|
# ("0_BG*"-Platzhalter werden ueber SivasnrTEF in ihre
|
|
# echten Einzelteile aufgeloest, siehe expand_tef_components)
|
|
expanded_parts = expand_tef_components(sivasnr_parts, eintrag.get("SivasnrTEF"))
|
|
for i, comp_sivasnr in enumerate(expanded_parts):
|
|
comp_id = f"{block_id}.t{i+1}"
|
|
if comp_sivasnr not in bogen_groups:
|
|
bogen_groups[comp_sivasnr] = {"anzahl": 0, "ids": []}
|
|
bogen_groups[comp_sivasnr]["anzahl"] += 1
|
|
bogen_groups[comp_sivasnr]["ids"].append(comp_id)
|
|
else:
|
|
# Normal: alle als eine Komponente
|
|
sivasnr = format_sivasnr(sivasnr_raw)
|
|
if sivasnr not in bogen_groups:
|
|
bogen_groups[sivasnr] = {"anzahl": 0, "ids": []}
|
|
bogen_groups[sivasnr]["anzahl"] += 1
|
|
bogen_groups[sivasnr]["ids"].append(block_id)
|
|
counters["anzahl_boegen"] += 1
|
|
|
|
elif typ == "weiche":
|
|
subtyp = classify_weiche(eintrag)
|
|
if has_tef and len(sivasnr_parts) > 1:
|
|
# Multi-Komponenten: jede Komponente separat gruppieren
|
|
# ("0_BG*"-Platzhalter werden ueber SivasnrTEF in ihre
|
|
# echten Einzelteile aufgeloest, siehe expand_tef_components)
|
|
expanded_parts = expand_tef_components(sivasnr_parts, eintrag.get("SivasnrTEF"))
|
|
for i, comp_sivasnr in enumerate(expanded_parts):
|
|
comp_id = f"{block_id}.t{i+1}"
|
|
if comp_sivasnr.startswith("0_B"):
|
|
# TEF-Komponente (Bogen) in bogen_groups
|
|
if comp_sivasnr not in bogen_groups:
|
|
bogen_groups[comp_sivasnr] = {"anzahl": 0, "ids": []}
|
|
bogen_groups[comp_sivasnr]["anzahl"] += 1
|
|
bogen_groups[comp_sivasnr]["ids"].append(comp_id)
|
|
else:
|
|
# Weichen-Komponente in weiche_groups
|
|
if comp_sivasnr not in weiche_groups:
|
|
weiche_groups[comp_sivasnr] = {"anzahl": 0, "ids": []}
|
|
weiche_groups[comp_sivasnr]["anzahl"] += 1
|
|
weiche_groups[comp_sivasnr]["ids"].append(comp_id)
|
|
else:
|
|
# Normal: alle als eine Komponente
|
|
sivasnr = format_sivasnr(sivasnr_raw)
|
|
if sivasnr not in weiche_groups:
|
|
weiche_groups[sivasnr] = {"anzahl": 0, "ids": []}
|
|
weiche_groups[sivasnr]["anzahl"] += 1
|
|
weiche_groups[sivasnr]["ids"].append(block_id)
|
|
if subtyp == "einzelweiche":
|
|
counters["anzahl_einzelweichen"] += 1
|
|
elif subtyp in ("doppelweiche", "dreiwegeweiche"):
|
|
counters["anzahl_doppel_dreiwegeweichen"] += 1
|
|
elif subtyp == "deltaweiche":
|
|
counters["anzahl_deltaweichen"] += 1
|
|
elif subtyp == "sternweiche":
|
|
counters["anzahl_sternweichen"] += 1
|
|
elif subtyp == "weichenkoerper":
|
|
counters["anzahl_weichenkoerper"] += 1
|
|
continue
|
|
|
|
# --- Omniflo Gerade (AP110) ---
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_gerade", [])):
|
|
attribs = block.get("attribs", {})
|
|
artinr = attribs.get("ARTINR", "")
|
|
key = artinr if artinr else "AP110"
|
|
laenge_attr = attribs.get("LAENGE") or attribs.get("A")
|
|
laenge = safe_float(laenge_attr, 2000.0)
|
|
if key not in gerade_groups:
|
|
gerade_groups[key] = {
|
|
"anzahl": 0, "laenge_mm": 0.0,
|
|
"artinr": artinr, "ids": [],
|
|
}
|
|
gerade_groups[key]["anzahl"] += 1
|
|
gerade_groups[key]["laenge_mm"] += laenge
|
|
gerade_groups[key]["ids"].append(get_id(block))
|
|
counters["gesamtlaenge_ap110"] += laenge
|
|
continue
|
|
|
|
# --- ILS Eckrad (vor Kreisel pruefen: pattern_kreisel schliesst
|
|
# ECKRAD_* mit ein, pattern_eckrad ist die praezisere Teilmenge) ---
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_eckrad", [])):
|
|
counters["anzahl_eckraeder"] += 1
|
|
sig = eckrad_signature(block)
|
|
if sig not in eckrad_groups:
|
|
eckrad_groups[sig] = {"ids": [], "bezeichnungen": [], "block": block}
|
|
eckrad_groups[sig]["ids"].append(get_id(block))
|
|
eckrad_groups[sig]["bezeichnungen"].append(
|
|
block.get("attribs", {}).get("Bezeichnung", ""))
|
|
continue
|
|
|
|
# --- ILS Kreisel ---
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_kreisel", [])):
|
|
kreisel_list.append(block)
|
|
attribs = block.get("attribs", {})
|
|
counters["anzahl_kreisel"] += 1
|
|
n_sep = safe_int(attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")) or "2", 2)
|
|
kreiselart = attribs.get("KREISELART", "STANDARD")
|
|
if kreiselart == "PIN":
|
|
counters["anzahl_kreisel_pinband"] += 1
|
|
counters["sep_kreisel_pin"] += n_sep
|
|
else:
|
|
counters["sep_kreisel_ohne_pin"] += n_sep
|
|
abstand_mm = safe_float(attribs.get("ABSTAND", "2300") or "2300", 2300.0)
|
|
counters["laengen_kreisel"] += abstand_mm / 1000.0
|
|
counters["anzahl_separatoren"] += n_sep
|
|
counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")) or "0")
|
|
continue
|
|
|
|
# --- VarioFoerderer Compound-Block (VF_N) ---
|
|
# Bekommt immer eine eigene Detail-Zeile (vario_list) statt der
|
|
# generischen IDs-Gruppierung - siehe build_variofoerderer_details.
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_variofoerderer", [])):
|
|
attribs = block.get("attribs", {})
|
|
counters["anzahl_variofoerderer"] += 1
|
|
vario_list.append(block)
|
|
counters["laengen_variofoerderer"] += safe_float(attribs.get("L_VF_m", "0") or "0")
|
|
counters["anzahl_separatoren"] += safe_int(attribs.get("ANZAHL_SEPARATOR", "0") or "0")
|
|
counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", "0") or "0")
|
|
counters["anzahl_motoren_vario"] += safe_int(attribs.get("ANZAHL_VF", "0") or "0")
|
|
continue
|
|
|
|
# --- Gefaellestrecke Compound-Block (GF_N) ---
|
|
# Eigener Typ "ILS 2.0 Gefaellestrecke" - NICHT als Variofoerderer zaehlen.
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_gefaellestrecke", [])):
|
|
attribs = block.get("attribs", {})
|
|
counters["anzahl_gefaellestrecken"] += 1
|
|
gefaelle_list.append(block)
|
|
counters["laengen_gefaellestrecken"] += safe_float(attribs.get("L_GF_m", "0") or "0")
|
|
counters["anzahl_separatoren"] += safe_int(attribs.get("ANZAHL_SEPARATOR", "0") or "0")
|
|
counters["anzahl_scanner"] += safe_int(attribs.get("ANZAHL_SCANNER", "0") or "0")
|
|
continue
|
|
|
|
# --- ILS Strecke-Module (Vario/Staustrecke, einzelne Komponenten in Modelspace) ---
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_strecke_module", [])):
|
|
counters["anzahl_variofoerderer"] += 1
|
|
strecke_ids.append(get_id(block))
|
|
strecke_bezeichnungen.append(block.get("attribs", {}).get("Bezeichnung", ""))
|
|
|
|
# --- Zeilen aufbauen ---
|
|
items = []
|
|
nr = 1
|
|
|
|
for sivasnr, g in bogen_groups.items():
|
|
teileart = "TEF Bogen" if str(sivasnr).startswith("0_B") else "Omniflo Bogen"
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": False,
|
|
"bezeichnung": "",
|
|
"dispgruppe": 10,
|
|
"anzahl": g["anzahl"],
|
|
"laenge_mm": "",
|
|
"ids": g["ids"],
|
|
"details": {},
|
|
})
|
|
nr += 1
|
|
|
|
for sivasnr, g in weiche_groups.items():
|
|
teileart = "TEF Weiche" if str(sivasnr).startswith("0_B") else "Omniflo Weiche"
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": False,
|
|
"bezeichnung": "",
|
|
"dispgruppe": 10,
|
|
"anzahl": g["anzahl"],
|
|
"laenge_mm": "",
|
|
"ids": g["ids"],
|
|
"details": {},
|
|
})
|
|
nr += 1
|
|
|
|
for key, g in gerade_groups.items():
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": "Omniflo Gerade",
|
|
"sivasnr": g["artinr"],
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": "",
|
|
"dispgruppe": 10,
|
|
"anzahl": g["anzahl"],
|
|
"laenge_mm": int(g["laenge_mm"]),
|
|
"ids": g["ids"],
|
|
"details": {},
|
|
})
|
|
nr += 1
|
|
|
|
# Gleichartige Kreisel (gleiche Kreiselart und gleicher Abstand) zu einer
|
|
# Zeile zusammenfassen - nur IDs/Bezeichnung bleiben je Block unterschiedlich.
|
|
kreisel_groups = {}
|
|
for block in kreisel_list:
|
|
sig = kreisel_signature(block)
|
|
if sig not in kreisel_groups:
|
|
kreisel_groups[sig] = {"ids": [], "bezeichnungen": [], "block": block}
|
|
kreisel_groups[sig]["ids"].append(get_id(block))
|
|
kreisel_groups[sig]["bezeichnungen"].append(
|
|
block.get("attribs", {}).get("Bezeichnung", ""))
|
|
|
|
for group in kreisel_groups.values():
|
|
sivasnr, teileart = ILS_SIVASNR["kreisel"]
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b),
|
|
"dispgruppe": 20,
|
|
"anzahl": len(group["ids"]),
|
|
"laenge_mm": "",
|
|
"ids": group["ids"],
|
|
"details": build_kreisel_details(group["block"]),
|
|
})
|
|
nr += 1
|
|
|
|
# Gleichartige Eckraeder (gleiches Geruest fuer Einzelmodul/Geruestoption)
|
|
# zu einer Zeile zusammenfassen - nur IDs/Bezeichnung bleiben je Block
|
|
# unterschiedlich.
|
|
for group in eckrad_groups.values():
|
|
sivasnr, teileart = ILS_SIVASNR["eckrad"]
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b),
|
|
"dispgruppe": 20,
|
|
"anzahl": len(group["ids"]),
|
|
"laenge_mm": "",
|
|
"ids": group["ids"],
|
|
"details": build_eckrad_details(group["block"]),
|
|
})
|
|
nr += 1
|
|
|
|
# Lose Strecke-Elemente (Vario/Staustrecke ausserhalb eines VF_N-Blocks)
|
|
# als eine gruppierte Zeile; VF_N-Compound-Bloecke bekommen unten je eine
|
|
# eigene Detail-Zeile.
|
|
if strecke_ids:
|
|
sivasnr, teileart = ILS_SIVASNR["strecke"]
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": ", ".join(b for b in strecke_bezeichnungen if b),
|
|
"dispgruppe": 20,
|
|
"anzahl": len(strecke_ids),
|
|
"laenge_mm": "",
|
|
"ids": strecke_ids,
|
|
"details": {},
|
|
})
|
|
nr += 1
|
|
|
|
# ILS 2.0 Zusatzmodul: BTMT-Beladestationen UND BTMT-Entladestationen
|
|
# (Abwurfstationen) zusammen in EINER Zeile (Merkmale 3501-3504) statt wie
|
|
# bisher zwei separaten TeileArt-Zeilen ohne details - Anzahl-Spalte
|
|
# bleibt 1 (wie bei der Gefaellestrecke-Gruppe), die realen Stueckzahlen
|
|
# stehen in 3503/3504.
|
|
if btmt_beladung_ids or btmt_entladung_ids:
|
|
sivasnr, teileart = ILS_SIVASNR["zusatzmodul"]
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": "",
|
|
"dispgruppe": 20,
|
|
"anzahl": 1,
|
|
"laenge_mm": "",
|
|
"ids": btmt_beladung_ids + btmt_entladung_ids,
|
|
"details": {
|
|
"3501": ja_nein("1" if btmt_beladung_ids else "0"),
|
|
"3503": str(len(btmt_beladung_ids)),
|
|
"3502": ja_nein("1" if btmt_entladung_ids else "0"),
|
|
"3504": str(len(btmt_entladung_ids)),
|
|
},
|
|
})
|
|
nr += 1
|
|
|
|
# Geruest-Baugruppen (GR_<id>_<timestamp>) - je Block eine eigene Zeile
|
|
# (siehe Kommentar bei geruest_list oben). Sivasnr kommt direkt aus dem
|
|
# ARTINR-Attribut - kann mehrere, kommagetrennte Sivas-IDs enthalten
|
|
# (Baugruppe aus mehreren Einzelteilen, siehe ssg-join in
|
|
# GeruestInsert.lsp) und dieselbe ID mehrfach (menge>1 je Einzelteil).
|
|
# Anders als bei den uebrigen Teilearten also KEIN fester Eintrag aus
|
|
# ILS_SIVASNR - JEDE Artikelnummer bekommt ihre EIGENE Sivas-Zeile (Sivas
|
|
# kennt nur eine Sivasnr je Zeile), gleiche Artikelnummern INNERHALB
|
|
# derselben Baugruppe werden dabei zu einer Zeile mit anzahl>1
|
|
# zusammengefasst (wie bogen_groups/weiche_groups oben). Die volle
|
|
# LAENGE der Baugruppe steht auf JEDER so entstandenen Zeile - Sivas hat
|
|
# kein Konzept einer "gemeinsamen" Laenge ueber mehrere Zeilen hinweg.
|
|
for block in geruest_list:
|
|
attribs = block.get("attribs", {})
|
|
artinr_list = [a.strip() for a in attribs.get("ARTINR", "").split(",") if a.strip()]
|
|
artinr_groups = {}
|
|
for artinr in artinr_list:
|
|
artinr_groups.setdefault(artinr, 0)
|
|
artinr_groups[artinr] += 1
|
|
for artinr, anzahl in artinr_groups.items():
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": attribs.get("TEILEART", "Geruest"),
|
|
"sivasnr": artinr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": attribs.get("BEZEICHNUNG", ""),
|
|
"dispgruppe": 20,
|
|
"anzahl": anzahl,
|
|
"laenge_mm": attribs.get("LAENGE", ""),
|
|
"ids": [get_id(block)],
|
|
"details": build_geruest_details(block),
|
|
})
|
|
nr += 1
|
|
|
|
# VarioFoerderer-Compound-Bloecke (VF_N) - je Block eine eigene Zeile mit
|
|
# vollstaendigen Gefaelle-/Angetrieben-Teilgruppen (immer TYP "Streckengruppe").
|
|
for block in vario_list:
|
|
sivasnr, teileart = ILS_SIVASNR["strecke"]
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": block.get("attribs", {}).get("Bezeichnung", ""),
|
|
"dispgruppe": 20,
|
|
"anzahl": 1,
|
|
"laenge_mm": "",
|
|
"ids": [get_id(block)],
|
|
"details": build_variofoerderer_details(block),
|
|
})
|
|
nr += 1
|
|
|
|
# Gleichartige Gefaellestrecken (identische Masse) zu einer Zeile
|
|
# zusammenfassen - nur IDs/Bezeichnung bleiben je Block unterschiedlich.
|
|
gefaelle_groups = {}
|
|
for block in gefaelle_list:
|
|
sig = gefaellestrecke_signature(block)
|
|
if sig not in gefaelle_groups:
|
|
gefaelle_groups[sig] = {"ids": [], "bezeichnungen": [], "block": block}
|
|
gefaelle_groups[sig]["ids"].append(get_id(block))
|
|
gefaelle_groups[sig]["bezeichnungen"].append(
|
|
block.get("attribs", {}).get("Bezeichnung", block.get("block_name", "")))
|
|
|
|
for group in gefaelle_groups.values():
|
|
sivasnr, teileart = ILS_SIVASNR["gefaellestrecke"]
|
|
# Anders als bei Kreisel steht hier die reale Stueckzahl der Gruppe
|
|
# NICHT in der Anzahl-Spalte (die bleibt 1), sondern im Merkmal 4311 -
|
|
# siehe build_gefaellestrecke_details.
|
|
details = build_gefaellestrecke_details(group["block"], anzahl=len(group["ids"]))
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": teileart,
|
|
"sivasnr": sivasnr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": ", ".join(b for b in group["bezeichnungen"] if b),
|
|
"dispgruppe": 20,
|
|
"anzahl": 1,
|
|
"laenge_mm": "",
|
|
"ids": group["ids"],
|
|
"details": details,
|
|
})
|
|
nr += 1
|
|
|
|
# ILS Automation Summierungszeile (nur wenn ILS-Elemente vorhanden)
|
|
has_ils = (counters["anzahl_kreisel"] > 0
|
|
or counters["anzahl_eckraeder"] > 0
|
|
or counters["anzahl_variofoerderer"] > 0
|
|
or counters["anzahl_gefaellestrecken"] > 0
|
|
or counters["anzahl_btbeladung"] > 0
|
|
or counters["anzahl_abwurfstationen"] > 0)
|
|
if has_ils:
|
|
# 4407 "Anzahl der Streckengruppen" zaehlt NUR die kombinierten
|
|
# Gefaelle+Vario-Compound-Bloecke (VF_N, TYP "Streckengruppe") - das
|
|
# ist len(vario_list), NICHT counters["anzahl_variofoerderer"] (der
|
|
# zaehlt auch lose Strecke-Module ausserhalb eines VF_N mit).
|
|
# 4426 "Anzahl der einzelnen Vario-Foerderer" gibt es als eigenen
|
|
# Typ nicht mehr (nur noch Gefaellestrecke ODER Streckengruppe) und
|
|
# steht daher fest auf "0".
|
|
automation_details = {
|
|
"4000": "Ja",
|
|
"4400": str(counters["anzahl_kreisel"]),
|
|
"4413": str(counters["sep_kreisel_ohne_pin"]),
|
|
"4401": str(counters["anzahl_kreisel_pinband"]),
|
|
"4414": str(counters["sep_kreisel_pin"]),
|
|
"4402": str(counters["anzahl_verbindungskreisel"]),
|
|
"4419": f"{counters['laengen_kreisel']:.2f}",
|
|
"4403": str(counters["anzahl_weichen_ils"]),
|
|
"4404": str(counters["anzahl_eckraeder"]),
|
|
"4425": str(counters["anzahl_gefaellestrecken"]),
|
|
"4426": "0",
|
|
"4407": str(len(vario_list)),
|
|
"4418": f"{counters['laengen_gefaellestrecken']:.2f}",
|
|
"4408": str(counters["anzahl_motoren_vario"]),
|
|
"4420": str(counters["laengen_variofoerderer"]),
|
|
"4409": str(counters["anzahl_etf"]),
|
|
"4417": str(counters["laengen_etf"]),
|
|
"4410": str(counters["anzahl_separatoren"]),
|
|
"4411": str(counters["anzahl_scanner"]),
|
|
"4421": str(counters["anzahl_btbeladung"]),
|
|
"4422": str(counters["anzahl_abwurfstationen"]),
|
|
"4100": "Ja",
|
|
}
|
|
auto_sivasnr, auto_teileart = ILS_SIVASNR["automation"]
|
|
items.append({
|
|
"nr": nr,
|
|
"teileart": auto_teileart,
|
|
"sivasnr": auto_sivasnr,
|
|
"sivasnr_quoted": True,
|
|
"bezeichnung": "",
|
|
"dispgruppe": 20,
|
|
"anzahl": 1,
|
|
"laenge_mm": "",
|
|
"details": automation_details,
|
|
})
|
|
|
|
return items, counters
|
|
|
|
|
|
def format_csv_line(item, details=None):
|
|
"""Formatiert eine CSV-Zeile im neuen Sivas-Format.
|
|
|
|
details ueberschreibt item["details"] fuer die Ausgabe - genutzt von
|
|
details_mit_bezeichnung(), um dieselbe Zeile mit lesbaren Merkmal-Keys
|
|
in die zweite ("..._mit_bezeichnung.csv") Datei zu schreiben, ohne
|
|
item["details"] selbst zu veraendern.
|
|
"""
|
|
if details is None:
|
|
details = item["details"]
|
|
details_json = json.dumps(details, ensure_ascii=False)
|
|
sivasnr = f'"{csv_quote(item["sivasnr"])}"' if item["sivasnr_quoted"] else item["sivasnr"]
|
|
ids_str = ", ".join(str(i) for i in item.get("ids", []) if i)
|
|
return (
|
|
f'{item["nr"]}'
|
|
f';"{csv_quote(item["teileart"])}"'
|
|
f';{sivasnr}'
|
|
f';"{csv_quote(item["bezeichnung"])}"'
|
|
f';{item["dispgruppe"]}'
|
|
f';{item["anzahl"]}'
|
|
f';{item["laenge_mm"]}'
|
|
f';"{csv_quote(ids_str)}"'
|
|
f';{details_json}'
|
|
)
|
|
|
|
|
|
def details_mit_bezeichnung(details, merkmalstamm):
|
|
"""Kopie von details, bei der jeder Merkmalnummer-Key durch seine
|
|
Klartext-Bezeichnung aus dem Merkmalstamm ersetzt wird (Wert bleibt
|
|
unveraendert). Keys, die keine reine Zahl sind (aktuell nur bei
|
|
Eckrad/Geruest, siehe build_eckrad_details/build_geruest_details - dort
|
|
noch beschreibende Klartext-Keys statt Merkmalnummern), bleiben
|
|
unveraendert."""
|
|
neu = {}
|
|
for key, value in details.items():
|
|
if key.isdigit():
|
|
neu[bezeichnung_fuer(key, merkmalstamm)] = value
|
|
else:
|
|
neu[key] = value
|
|
return neu
|
|
|
|
|
|
def lesbarer_dateiname(output_csv):
|
|
"""Leitet den Dateinamen der Lesbarkeits-Kopie aus output_csv ab, z.B.
|
|
"500651_sivas.csv" -> "500651_sivas_mit_bezeichnung.csv"."""
|
|
stamm, endung = os.path.splitext(output_csv)
|
|
return f"{stamm}_mit_bezeichnung{endung}"
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 4:
|
|
print("Aufruf: python export_sivas.py <export_raw.json> <data_dir> <output.csv>")
|
|
sys.exit(1)
|
|
|
|
raw_json_path = sys.argv[1]
|
|
data_dir = sys.argv[2]
|
|
output_csv = sys.argv[3]
|
|
|
|
blocks = load_json(raw_json_path)
|
|
boegen_path = os.path.join(data_dir, "json", "omniflo_boegen.json")
|
|
weichen_path = os.path.join(data_dir, "json", "omniflo_weichen.json")
|
|
|
|
boegen = load_json(boegen_path) if os.path.exists(boegen_path) else []
|
|
weichen = load_json(weichen_path) if os.path.exists(weichen_path) else []
|
|
|
|
lookup = build_lookup(boegen, weichen)
|
|
|
|
print(f"[export_sivas] {len(blocks)} Bloecke geladen, "
|
|
f"{len(boegen)} Boegen, {len(weichen)} Weichen im Katalog.")
|
|
|
|
items, _ = process_blocks(blocks, lookup)
|
|
|
|
header = "Nr;TeileArt;SivasNummer;Bezeichnung;Dispogruppe;Anzahl;Laenge[mm];IDs;details"
|
|
with open(output_csv, "w", encoding="utf-8") as f:
|
|
f.write(header + "\n")
|
|
for item in items:
|
|
f.write(format_csv_line(item) + "\n")
|
|
print(f"[export_sivas] CSV geschrieben: {output_csv} ({len(items)} Zeilen)")
|
|
|
|
# Zweite, lesbare Kopie fuer die manuelle Pruefung (Merkmalnummer +
|
|
# Bezeichnung aus cfg/Merkmalsstamm_in_Sivas.xlsx) - wird von Sivas
|
|
# selbst NICHT eingelesen, nur output_csv ist der eigentliche Import.
|
|
merkmalstamm = lade_merkmalstamm()
|
|
output_csv_lesbar = lesbarer_dateiname(output_csv)
|
|
with open(output_csv_lesbar, "w", encoding="utf-8") as f:
|
|
f.write(header + "\n")
|
|
for item in items:
|
|
details = details_mit_bezeichnung(item["details"], merkmalstamm)
|
|
f.write(format_csv_line(item, details=details) + "\n")
|
|
print(f"[export_sivas] Lesbare Kopie geschrieben: {output_csv_lesbar}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|