bfd95ea725
Kreisel bekommen wie VF_n/GF_n ein Merkmal mit der Reihenfolge aller Separator-/BTMT-Beladung-/BTMT-Entladung-TeileIds entlang der Bahn, sortiert nach Winkel um die Kreiselachse (Start bei 0 Grad in Drehrichtung DREHRICHTUNG) - nutzt dieselbe Umlauf-Logik, die bisher nur die "Nachbarn"-Spalte gespeist hat. Merkmal-Schluessel einheitlich auf "list_trackids" umbenannt (vorher "Separatorlist" bei VF/GF). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1437 lines
66 KiB
Python
1437 lines
66 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
export_csv.py - Erzeugt eine einfache Item-Liste als CSV.
|
|
|
|
Fuer Omniflo-Elemente (Bogen/Weiche/Gerade) wird am Ende zusaetzlich eine
|
|
"Omniflo Sum"-Zeile mit Anzahl Boegen/Weichentypen und Gesamtlaenge AP110/AP60
|
|
angehaengt (siehe build_omni_sum_merkmale).
|
|
|
|
Nach der Kollisions-/Nachbarschaftserkennung wird ausserdem an jedem
|
|
Beruehrpunkt zweier Kreisel eine "ILS Weiche"-Zeile angehaengt (siehe
|
|
build_kreisel_weiche_items / export_neighbors.compute_kreisel_touch_switches):
|
|
Anzahl 1, Position = Beruehrkoordinaten, Nachbarn = die beiden Kreisel-IDs,
|
|
K1-K4/Insertpoint leer, Boundingbox = feste Element-Box (element_box_mm,
|
|
cfg/export.cfg [Boundingbox]),
|
|
mit der Laengsseite senkrecht zur Achse des beruehrenden Kreisels; die TeileId
|
|
zaehlt aus der hoechsten bereits vergebenen numerischen ID hoch.
|
|
|
|
Anschliessend (TeileId-Zaehlung laeuft ueber die Weichen hinweg weiter) wird
|
|
an jedem Ende einer Gefaellestrecke/eines Foerderers, an dem ein Kreisel
|
|
anschliesst, ein Aus- (Anfang) bzw. Einschleuselement (Ende) an der Kollisions-
|
|
position Kreisel<->Strecke angehaengt (siehe build_strecke_schleus_items /
|
|
export_neighbors.compute_strecke_kreisel_schleus) - Format wie die Weiche,
|
|
Nachbarn = Kreisel-ID + Strecken-ID. Zugeordnet wird je Streckenende der
|
|
naechstgelegene ihrer BBox-Kreisel-Nachbarn.
|
|
|
|
Warnungen = Spalte fuer Auffaelligkeiten. Aktuell: eine Strecke sollte genau
|
|
zwei Kreisel (Anfang + Ende) beruehren - beruehrt ihre (in x/y gepruefte)
|
|
Bounding-Box mehr, steht hier ein Hinweis und es wird zusaetzlich eine
|
|
Konsolen-WARNUNG ausgegeben (mit exakten 3D-BBoxen sollte das nicht auftreten).
|
|
Scanner-Zeilen bekommen dieselbe Spalte fuer strittige Distanz-Zuordnungen:
|
|
liegt ein Scanner ausserhalb jeder Kreisel-/Streckengruppen-/Gefaellestrecke-
|
|
Boundingbox und ist der Abstand zum naechsten und zweitnaechsten Carrier fast
|
|
gleich gross, schreibt Lisp/count_sep_scan.lsp (cs-zuordnung-lauf, vor dem
|
|
Export automatisch aufgerufen) einen Warnungstext mit den IDs beider
|
|
Kandidaten in das "warnung"-Feld des Scanner-Blocks im Roh-JSON (siehe
|
|
csv:block-to-json in export.lsp) - hier unveraendert uebernommen.
|
|
|
|
Aufruf:
|
|
python export_csv.py <export_raw.json> <data_dir> <output.csv>
|
|
|
|
CSV-Format:
|
|
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;
|
|
Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Warnungen;Merkmale
|
|
|
|
Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per
|
|
vla-getboundingbox) ermittelten Bounding-Box je Block:
|
|
Position = Mittenkoordinate x, y, z
|
|
Boundingbox = Ausdehnung (Laenge, Breite, Hoehe) in x, y, z
|
|
Insertpoint = KOS-String (24-Zeichen Base64, Position+Rotation als Quaternion,
|
|
siehe csv:kos-encode in export.lsp) des Block-Einfuegepunkts selbst.
|
|
K1-K4 = Positions-Strings der im Block enthaltenen Koordinatensysteme K1..K4
|
|
(12-Zeichen Base64, nur Position ohne Rotation, siehe csv:trans-encode;
|
|
nur bei Omniflo Bogen/Weiche/Gerade vorhanden, sonst leer).
|
|
Planquadrat wird aus der x/y-Koordinate des Blocks berechnet, siehe
|
|
export_planquadrat.py ([Planquadrate] in cfg/export.cfg).
|
|
Nachbarn = kommaseparierte TeileId-Liste ueberschneidender Elemente (Bounding-
|
|
Box-Ueberschneidung in der x/y-Ebene, reines Python ohne externe Abhaengigkeit,
|
|
siehe export_neighbors.py). Geprueft wird NICHT global ueber alle Elemente,
|
|
sondern in drei getrennten Gruppen: 1) Kreisel/Eckrad gegen Kreisel/Eckrad,
|
|
2) Kreisel/Eckrad gegen Gefaellestrecke/Foerderer/Strecke-Modul (diese drei
|
|
nicht gegeneinander), 3) Omniflo-Elemente gegen Omniflo-Elemente. Fuer die
|
|
Omniflo-Gruppe liefert eine shapely-STRtree-Broad-Phase zunaechst alle Paare
|
|
mit ueberschneidenden Bounding-Boxen; anschliessend gelten sie nur dann als
|
|
benachbart, wenn sich ihre Anschluss-Koordinatensysteme K1-K4 tatsaechlich
|
|
beruehren (ein K-Punkt naeher als omniflo_ks_toleranz_mm, Default 10mm) -
|
|
das trennt bloss ueberlappende BBoxen von echten Anschluessen (Geraden fuehren
|
|
dafuer beim Export synthetisierte K1/K2, siehe csv:gerade-k-kos-strings).
|
|
Toleranzen und (Fallback-)Rastergroesse konfigurierbar ueber [Nachbarschaft]
|
|
in cfg/export.cfg.
|
|
Fehler = "unverbunden" (keine Partner, aber mindestens einer noetig),
|
|
"nur ein Partner" (ein Partner, aber mindestens zwei noetig) oder leer
|
|
(Mindestanzahl erreicht bzw. TeileArt wird nicht geprueft). Kreisel/Eckrad/
|
|
Omniflo-Elemente brauchen mindestens einen Partner, Gefaellestrecke/
|
|
Foerderer/Strecke-Modul mindestens zwei (siehe export_neighbors.py
|
|
MIN_PARTNER). Ein Kreisel wird dabei NICHT als eine, sondern als zwei
|
|
BBoxen (links/rechts, geteilt entlang der Achse durch Antriebs-/
|
|
Spannstation, siehe export_neighbors.kreisel_half_bboxes) gegen
|
|
Gefaellestrecke/Foerderer getestet - deren "Nachbarn"-Spalte zeigt darum
|
|
"<KreiselTeileId>-L" bzw. "-R" statt nur der Kreisel-TeileId.
|
|
Separator/Scanner-Zeilen bekommen zusaetzlich ein berechnetes "Zuordnung"-
|
|
Merkmal (Gefaellestrecke > Foerderer > Kreiselhaelfte, per BBox-Ueber-
|
|
schneidung - siehe export_neighbors.compute_sensor_zuordnung), das die
|
|
ATTRIB-Zuordnung (falls das Attribut ZUORDNUNG in der Zeichnung von Hand
|
|
gesetzt wurde) ersetzt, sofern ein Treffer gefunden wird. Scanner-Zeilen
|
|
bekommen zusaetzlich ein "Naechster Separator"-Merkmal (raeumlich naechst-
|
|
gelegener Separator, euklidischer Abstand der BBox-Mittelpunkte - eigene,
|
|
von der Zuordnung unabhaengige Suche, siehe export_neighbors.compute_
|
|
scanner_nearest_separator).
|
|
Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer
|
|
EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export. Insertpoint/
|
|
K1-K4 stehen zwar in export_raw.json fuer jeden Export zur Verfuegung (siehe
|
|
csv:block-to-json), werden aber ebenfalls nur von diesem Skript (EXPORTCSV)
|
|
als CSV-Spalten ausgegeben.
|
|
|
|
Reihenfolge/Elementnummer: die Zeilen werden am Ende aufsteigend nach
|
|
TeileId sortiert (numerische IDs) und die Elementnummer (erste Spalte)
|
|
danach neu von 1 an vergeben (siehe teileid_sort_key in process_blocks) -
|
|
wurde kein Element geloescht, stimmen Elementnummer und TeileId dadurch
|
|
ueberein, geloeschte Elemente hinterlassen Luecken in der TeileId-Folge.
|
|
Zeilen ohne numerische TeileId (Omniflo-Sum-Zeile, SEP-/SCN-Fallback-IDs
|
|
ohne ID-Attribut) behalten stabil ihre bisherige Reihenfolge und stehen
|
|
danach.
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import sys
|
|
import os
|
|
|
|
from export_blockpatterns import load_patterns, matches_any, csv_quote, safe_float
|
|
from export_planquadrat import load_planquadrat_config, resolve_origins, compute_planquadrat
|
|
from export_neighbors import (
|
|
load_neighbor_tolerance_mm,
|
|
load_omniflo_cell_size_mm,
|
|
load_omniflo_ks_tolerance_mm,
|
|
load_kreisel_durchmesser_mm,
|
|
load_element_box_mm,
|
|
load_separator_box_mm,
|
|
separator_bbox,
|
|
load_collision_debug_target,
|
|
compute_neighbor_ids,
|
|
compute_neighbor_errors,
|
|
compute_sensor_zuordnung,
|
|
compute_scanner_nearest_separator,
|
|
compute_kreisel_touch_switches,
|
|
compute_strecke_kreisel_schleus,
|
|
compute_sep_kette,
|
|
compute_kreisel_umlauf,
|
|
SENSOR_TEILEARTEN,
|
|
WEICHE_TEILEART,
|
|
GF_VF_TEILEARTEN,
|
|
KREISEL_UMLAUF_TEILEARTEN,
|
|
)
|
|
|
|
BLOCKPATTERNS = load_patterns()
|
|
PLANQUADRAT_CFG = load_planquadrat_config()
|
|
NEIGHBOR_TOLERANCE_MM = load_neighbor_tolerance_mm()
|
|
NEIGHBOR_OMNIFLO_CELL_SIZE_MM = load_omniflo_cell_size_mm()
|
|
NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM = load_omniflo_ks_tolerance_mm()
|
|
KREISEL_DURCHMESSER_MM = load_kreisel_durchmesser_mm()
|
|
ELEMENT_BOX_MM = load_element_box_mm()
|
|
SEPARATOR_BOX_MM = load_separator_box_mm()
|
|
NEIGHBOR_DEBUG_TARGET = load_collision_debug_target()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Gemeinsame Hilfsfunktionen (auch in export_sivas.py verwendet)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
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 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())
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Merkmale-Builder
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def get_hoehe(block):
|
|
"""Liest HOEHE aus Block-Attributen. Fallback: Z-Koordinate."""
|
|
attribs = block.get("attribs", {})
|
|
return attribs.get("HOEHE") or str(int(block.get("z", 0) or 0)) or "2000"
|
|
|
|
|
|
def get_drehung(block):
|
|
"""Liest DREHUNG aus Block-Attributen. Fallback: CAD-Rotation."""
|
|
attribs = block.get("attribs", {})
|
|
drehung_raw = attribs.get("DREHUNG")
|
|
if drehung_raw is None:
|
|
return block.get("rotation", 0.0)
|
|
return safe_float(drehung_raw, block.get("rotation", 0.0))
|
|
|
|
|
|
def build_bogen_merkmale(block, eintrag):
|
|
return {
|
|
"Kurvenwinkel": float(eintrag.get("KurvenWinkel", 0)),
|
|
"Radius": float(eintrag.get("Radius", 0)),
|
|
"Höhe": get_hoehe(block),
|
|
"Drehung": get_drehung(block),
|
|
"SivasNummer": format_sivasnr(eintrag.get("Sivasnr", ""))
|
|
}
|
|
|
|
|
|
def build_weiche_merkmale(block, eintrag):
|
|
wt = eintrag.get("WeichenTyp", "Einzelweiche")
|
|
return {
|
|
"Weichentyp": wt,
|
|
"Richtung": str(eintrag.get("KurvenRichtung", "")),
|
|
"Weichenwinkel": float(eintrag.get("KurvenWinkel", 0)),
|
|
"Höhe": get_hoehe(block),
|
|
"Drehung": get_drehung(block),
|
|
"Antrieb Kurve": eintrag.get("SivasnrTEF") is not None,
|
|
"SivasNummer": format_sivasnr(eintrag.get("Sivasnr", ""))
|
|
}
|
|
|
|
|
|
def build_variofoerderer_merkmale(block):
|
|
"""Merkmale-Dict fuer einen VF_N- oder GF_N-Streckenblock.
|
|
|
|
Die Attributnamen folgen dem gemeinsamen Schema *strecke-attr-front* /
|
|
*strecke-attr-gruppe* in ssg_core.lsp. Hoehen und Delta-Werte tragen dort
|
|
das Einheiten-Suffix _mm; SEITE ist in SEITE_AS/SEITE_ES (Antriebs-/
|
|
Endstation) und WINKEL in GF_WINKEL/VF_WINKEL aufgeteilt. Die alten Namen
|
|
ohne Suffix werden per Fallback weiter gelesen, damit aeltere
|
|
export_raw.json-Dateien (z.B. tests/reference/export_raw.json) und noch
|
|
nicht neu aufgebaute Bloecke in der Zeichnung weiterhin Werte liefern.
|
|
|
|
Das Merkmal "list_trackids" (Reihenfolge der Separator-TeileIds entlang
|
|
der Strecke) steht hier NICHT drin - es kommt erst nach der sepliste-
|
|
Aufloesung dazu (siehe write_separatorliste_merkmale in process_blocks),
|
|
weil an dieser Stelle die x/y-Koordinaten aus der sepliste-XDATA noch
|
|
nicht auf reale Separator-TeileIds aufgeloest sind.
|
|
"""
|
|
attribs = block.get("attribs", {})
|
|
|
|
def att(*keys):
|
|
"""Erster nicht-leerer Attributwert; aktuelles Schema zuerst, Alt-Name
|
|
als Fallback."""
|
|
for key in keys:
|
|
value = attribs.get(key)
|
|
if value not in (None, ""):
|
|
return value
|
|
return ""
|
|
|
|
return {
|
|
"Typ": att("TYP"),
|
|
"Seite_AS": att("SEITE_AS", "SEITE"),
|
|
"Seite_ES": att("SEITE_ES", "SEITE"),
|
|
"Winkel": att("GF_WINKEL", "WINKEL"),
|
|
"VF_Winkel": att("VF_WINKEL"),
|
|
"DeltaH_mm": att("DELTA_H_mm", "DELTA_H"),
|
|
"DeltaL_mm": att("DELTA_L_mm", "DELTA_L"),
|
|
"L_VF_m": att("L_VF_m"),
|
|
"L_GF_m": att("L_GF_m"),
|
|
"Montagehoehe_m": att("MONTAGEHOEHE_m"),
|
|
"Hoehe_Von_mm": att("HOEHE_VON_mm", "HOEHE_VON"),
|
|
"Hoehe_Bis_mm": att("HOEHE_BIS_mm", "HOEHE_BIS"),
|
|
"Antriebfahrtrichtung": att("ANTRIEBFAHRTRICHTUNG"),
|
|
"Anzahl_Separator": att("ANZAHL_SEPARATOR"),
|
|
"Anzahl_Scanner": att("ANZAHL_SCANNER"),
|
|
}
|
|
|
|
|
|
def get_laenge_mm(block):
|
|
"""Liest LAENGE (oder A) aus Block-Attributen. Fallback: 2000mm."""
|
|
attribs = block.get("attribs", {})
|
|
laenge = attribs.get("LAENGE") or attribs.get("A")
|
|
return safe_float(laenge, 2000.0)
|
|
|
|
|
|
def build_gerade_merkmale(block):
|
|
laenge_mm = get_laenge_mm(block)
|
|
hoehe = get_hoehe(block)
|
|
return {
|
|
"Anzahl der Separatoren": "0",
|
|
"Länge in Meter": f"{laenge_mm / 1000.0:.2f}",
|
|
"Winkel": "0",
|
|
"Anzahl der Scanner": 0,
|
|
"Höhe oben": hoehe,
|
|
"Höhe unten": hoehe,
|
|
"Drehung": get_drehung(block),
|
|
"SivasNummer": ""
|
|
}
|
|
|
|
|
|
def build_sensor_merkmale(block):
|
|
"""Merkmale-Dict fuer einen eigenstaendigen Separator_SP/S-LP- oder
|
|
Scanner-Block (siehe pattern_separator/pattern_scanner in cfg/export.cfg).
|
|
ZUORDNUNG/ARTINR/LAYER (+ neuerdings ID) sind die ATTDEFs (siehe
|
|
ils-insert-sensor/mubea:build-separator-one, ssg-id-collect-blocks in
|
|
ssg_id.lsp).
|
|
|
|
"zuordnung_fix" (csv:block-to-json) hat Vorrang vor dem ZUORDNUNG-Attribut:
|
|
es steht an den temporaeren Kopien der in VF_n/GF_n/KREISEL_n VERPACKTEN
|
|
Separator_SP-Symbole und traegt die ID des Wrapper-Blocks, in dem das
|
|
Symbol steckt - eine bekannte, keine geratene Zuordnung."""
|
|
attribs = block.get("attribs", {})
|
|
return {
|
|
"Zuordnung": block.get("zuordnung_fix") or attribs.get("ZUORDNUNG", ""),
|
|
"ARTINR": attribs.get("ARTINR", ""),
|
|
"Höhe": get_hoehe(block),
|
|
"Drehung": get_drehung(block),
|
|
}
|
|
|
|
|
|
def build_btmt_merkmale(block):
|
|
"""Merkmale-Dict fuer eine eigenstaendige BTMT-Beladung/SC_Entladung-Station
|
|
(siehe pattern_btmt_beladung/pattern_btmt_entladung in cfg/export.cfg).
|
|
|
|
Anders als build_sensor_merkmale ohne "Zuordnung": eine BTMT-Station ist
|
|
keinem Kreisel/GF/VF-Carrier zugeordnet, das ZUORDNUNG-Attribut existiert
|
|
hier nicht.
|
|
|
|
Drehung wird auf ganze Grad gerundet (0, 90, 180, ...) - anders als bei
|
|
Separator/Scanner/Bogen/Weiche, wo Kommastellen Bedeutung haben."""
|
|
attribs = block.get("attribs", {})
|
|
return {
|
|
"ARTINR": attribs.get("ARTINR", ""),
|
|
"Höhe": get_hoehe(block),
|
|
"Drehung": round(get_drehung(block)),
|
|
}
|
|
|
|
|
|
def format_sensor_id(prefix, n):
|
|
"""Fallback-TeileId fuer Separator/Scanner-Zeilen im CSV-Export, falls das
|
|
Block-Attribut "ID" (noch) leer ist.
|
|
|
|
Separator_SP/Scanner haben seit kurzem ein eigenes ID-ATTDEF (siehe
|
|
ssg-id-collect-blocks in ssg_id.lsp) - normalerweise wird also das echte,
|
|
ueber ssg-id-generate/ssg-id-check-all vergebene "ID"-Attribut als
|
|
TeileId verwendet, genau wie bei Kreisel/GF/VF. Nur wenn das (noch)
|
|
fehlt (z.B. bei aelteren, noch nicht neu exportierten Bloecken), erzeugt
|
|
diese Funktion eine rein fuer DIESEN Exportlauf gueltige, NICHT ueber
|
|
mehrere Exportlaeufe hinweg stabile Ersatznummer - der Praefix macht
|
|
sichtbar, dass es keine echte, persistierte Block-ID ist.
|
|
"""
|
|
return f"{prefix}-{n:04d}"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Omniflo Sum-Zeile (Anzahl Boegen/Weichentypen, Gesamtlaenge AP110/AP60)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
WEICHE_SUBTYP_MAP = {
|
|
"Einzelweiche": "einzelweiche",
|
|
"Doppelweiche": "doppelweiche",
|
|
"Dreiwegeweiche": "doppelweiche",
|
|
"Deltaweiche": "deltaweiche",
|
|
"Dreifachweiche": "deltaweiche",
|
|
"Sternweiche": "sternweiche",
|
|
}
|
|
|
|
|
|
def weichensubtyp(block, eintrag):
|
|
"""Bestimmt den Weichen-Subtyp fuer die Omniflo-Sum-Zaehlung."""
|
|
beschr = (block.get("attribs", {}).get("BESCHR") or "").upper()
|
|
if "WEICHENK" in beschr:
|
|
return "weichenkoerper"
|
|
subtyp = WEICHE_SUBTYP_MAP.get(eintrag.get("WeichenTyp", ""))
|
|
if subtyp:
|
|
return subtyp
|
|
if "DELTA" in beschr:
|
|
return "deltaweiche"
|
|
if "STAR" in beschr:
|
|
return "sternweiche"
|
|
return "einzelweiche"
|
|
|
|
|
|
def build_omni_sum_merkmale(cnt_boegen, cnt_wk, cnt_einzel, cnt_delta, cnt_doppel, cnt_stern,
|
|
len_ap110_mm, len_ap60_mm):
|
|
return {
|
|
"Anzahl Bögen": cnt_boegen,
|
|
"Anzahl Weichengrundkörper": cnt_wk,
|
|
"Anzahl Einwegweichen": cnt_einzel,
|
|
"Anzahl Deltaweichen": cnt_delta,
|
|
"Anzahl Doppelweichen und Dreiwegeweichen": cnt_doppel,
|
|
"Anzahl Sternweichen": cnt_stern,
|
|
"Gesamtlänge AP110": round(len_ap110_mm, 1),
|
|
"Gesamtlänge AP60": round(len_ap60_mm, 1),
|
|
}
|
|
|
|
|
|
def build_kreisel_merkmale(block):
|
|
attribs = block.get("attribs", {})
|
|
abstand_m = str(round(safe_float(attribs.get("ABSTAND", "2300"), 2300.0) / 1000.0, 2))
|
|
hoehe_m = str(round(safe_float(attribs.get("HOEHE", "0"), 0.0) / 1000.0, 2))
|
|
return {
|
|
"Abstand (Kreiselachse A - Kreiselachse) in Meter": abstand_m,
|
|
"Anzahl der Separatoren": attribs.get("ANZAHL_SEPARATOR", attribs.get("N_SEPARATOREN", "2")),
|
|
"Kreiselart": attribs.get("KREISELART", "STANDARD"),
|
|
"Anzahl der Scanner": attribs.get("ANZAHL_SCANNER", attribs.get("N_SCANNER", "0")),
|
|
"Anzahl der Rampen": attribs.get("ANZAHL_RAMPEN", attribs.get("N_RAMPEN", "0")),
|
|
"Höhe in m": hoehe_m,
|
|
"Drehrichtung": attribs.get("DREHRICHTUNG", "UZS"),
|
|
"Drehung": block.get("rotation", 0.0),
|
|
"Name": attribs.get("Bezeichnung", ""),
|
|
"Geruest fuer Einzelmodul": attribs.get("GERUEST_EINZELMODUL", "0") == "1",
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bounding-Box-Spalten (Position/Boundingbox)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def bbox_columns(block):
|
|
"""Liest die von export.lsp (csv:get-bbox) ermittelte Bounding-Box, sowie
|
|
die KOS-Strings von Insertpoint und K1-K4 (csv:kos-encode/csv:block-to-json).
|
|
|
|
Rueckgabe: dict mit "position" (Mittenkoordinate x,y,z) und "boundingbox"
|
|
(Ausdehnung x,y,z) als Strings, oder leere Strings falls kein bbox-Feld
|
|
vorhanden ist (z.B. EXPORTSIVAS oder die synthetische Omniflo-Sum-Zeile).
|
|
"insertpoint" ist ein 24-Zeichen Base64-KOS-String (Position + Rotation
|
|
als Quaternion, csv:kos-encode); "k1".."k4" sind 12-Zeichen Positions-
|
|
Strings (nur Position, csv:trans-encode) - leer falls das jeweilige
|
|
Koordinatensystem am Block nicht existiert.
|
|
"_bbox" enthaelt zusaetzlich die rohen Zahlenwerte fuer die Nachbarschafts-
|
|
erkennung (siehe export_neighbors.py), wird nicht in die CSV geschrieben.
|
|
"""
|
|
kos_cols = {
|
|
"insertpoint": block.get("insertpoint", ""),
|
|
"k1": block.get("k1", ""),
|
|
"k2": block.get("k2", ""),
|
|
"k3": block.get("k3", ""),
|
|
"k4": block.get("k4", ""),
|
|
}
|
|
bbox = block.get("bbox")
|
|
if not bbox:
|
|
return {"position": "", "boundingbox": "", "_bbox": None, **kos_cols}
|
|
return {
|
|
"position": f'{bbox.get("cx", 0):.2f}, {bbox.get("cy", 0):.2f}, {bbox.get("cz", 0):.2f}',
|
|
"boundingbox": f'{bbox.get("dx", 0):.2f}, {bbox.get("dy", 0):.2f}, {bbox.get("dz", 0):.2f}',
|
|
"_bbox": bbox,
|
|
**kos_cols,
|
|
}
|
|
|
|
|
|
def _fallback_separator_bbox(cols, block, separator_box_mm=None):
|
|
"""Ergaenzt eine fehlende Bounding-Box (cols["_bbox"] is None) durch die
|
|
feste Separator-Symbol-Box aus cfg [Boundingbox] separator_box_* an der
|
|
realen INSERT-Position/-Drehung des Blocks. Wird fuer frei platzierte
|
|
Separator_SP-INSERTs gebraucht, deren vla-getboundingbox beim Export
|
|
scheiterte (z.B. eingefrorener Layer) - ohne "_bbox" fielen sie sonst aus
|
|
der Nachbarschafts-/Zuordnungserkennung. Hat der Block schon eine bbox,
|
|
bleibt cols unveraendert.
|
|
|
|
Rueckgabe: (ggf. ergaenztes) cols-dict."""
|
|
if cols.get("_bbox"):
|
|
return cols
|
|
if separator_box_mm is None:
|
|
separator_box_mm = SEPARATOR_BOX_MM
|
|
bbox = separator_bbox(block.get("x", 0.0) or 0.0, block.get("y", 0.0) or 0.0,
|
|
block.get("rotation", 0.0) or 0.0,
|
|
separator_box_mm=separator_box_mm,
|
|
z=block.get("z", 0.0) or 0.0)
|
|
cols = dict(cols)
|
|
cols["position"] = f'{bbox["cx"]:.2f}, {bbox["cy"]:.2f}, {bbox["cz"]:.2f}'
|
|
cols["boundingbox"] = f'{bbox["dx"]:.2f}, {bbox["dy"]:.2f}, {bbox["dz"]:.2f}'
|
|
cols["_bbox"] = bbox
|
|
return cols
|
|
|
|
|
|
def planquadrat_column(block, planquadrat_cfg):
|
|
"""Berechnet das Planquadrat aus der Bounding-Box des Blocks, falls
|
|
vorhanden (siehe export_planquadrat.py), sonst aus dem Einfuegepunkt.
|
|
|
|
planquadrat_cfg = Rueckgabe von resolve_origins() fuer den aktuellen
|
|
Exportlauf (x_origin_mm/y_origin_mm bereits aufgeloest).
|
|
Leerer String, wenn keine Planquadrat-Konfiguration geladen werden konnte
|
|
(fehlende/unvollstaendige [Planquadrate]-Sektion in cfg/export.cfg).
|
|
|
|
Ueberdeckt die Bounding-Box des Elements mehr als eine Zelle (z.B. ein
|
|
Kreisel oder eine lange VarioFoerderer-Kette), liefert compute_planquadrat
|
|
eine SPANNE "start-ende" (z.B. "A/1-J/1") statt nur der Mittelpunkt-Zelle.
|
|
"""
|
|
if planquadrat_cfg is None:
|
|
return ""
|
|
bbox = block.get("bbox") or {}
|
|
x = bbox.get("cx", block.get("x"))
|
|
y = bbox.get("cy", block.get("y"))
|
|
if x is None or y is None:
|
|
return ""
|
|
dx = bbox.get("dx", 0.0)
|
|
dy = bbox.get("dy", 0.0)
|
|
return compute_planquadrat(float(x), float(y), planquadrat_cfg, dx_mm=float(dx), dy_mm=float(dy))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bekannte Blocknamen
|
|
# ---------------------------------------------------------------------------
|
|
|
|
SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "K4", "KS_EIN", "KS_AUS"]))
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Bloecke verarbeiten (einfache Liste, keine Summierung)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def max_numeric_teileid(items):
|
|
"""Hoechste rein-numerische TeileId in items (z.B. "0008" -> 8). Nicht-
|
|
numerische IDs (SEP-0001, autogenerated_of_json, ...) werden ignoriert.
|
|
0, wenn keine numerische ID existiert. Basis fuer die fortlaufende
|
|
Nummerierung der am Kreisel-Beruehrpunkt erzeugten ILS-Weichen (siehe
|
|
'Zaehle die Id aus der letzten enthaltenen ID hoch')."""
|
|
max_id = 0
|
|
for item in items:
|
|
tid = str(item.get("teileid", "")).strip()
|
|
if tid.isdigit():
|
|
max_id = max(max_id, int(tid))
|
|
return max_id
|
|
|
|
|
|
def build_kreisel_weiche_items(items, planquadrat_cfg, start_elem_nr, switches, dbg=None):
|
|
"""Haengt fuer jeden Kreisel-Beruehrpunkt eine "ILS Weiche"-Zeile an items
|
|
an (siehe export_neighbors.compute_kreisel_touch_switches).
|
|
|
|
switches = Rueckgabe von compute_kreisel_touch_switches, bereits VOR dem
|
|
Aufruf berechnet (der Aufrufer speist dieselbe Liste auch in
|
|
compute_kreisel_umlauf ein, damit die Weiche im Kreisel-Umlauf auftaucht -
|
|
siehe process_blocks). Jeder switches-Eintrag traegt seinen stabilen
|
|
"key" (sw["key"]) - hier wird KEIN neuer Schluessel erzeugt.
|
|
|
|
TeileId zaehlt aus der hoechsten bereits enthaltenen numerischen ID hoch
|
|
(max_numeric_teileid), 4-stellig aufgefuellt wie die Kreisel-IDs. Anzahl
|
|
ist wie bei allen Zeilen 1; Nachbarn = die beiden Kreisel-IDs (durch den
|
|
Kreisel-Umlauf ggf. bereits um die eigentlichen Umlauf-Nachbarn ergaenzt/
|
|
ersetzt, siehe process_blocks); K1-K4 und Insertpoint bleiben leer;
|
|
Position = Beruehrkoordinaten; Boundingbox = die feste, an der
|
|
Kreiselachse ausgerichtete Element-Box (element_box_mm, cfg/export.cfg
|
|
[Boundingbox]).
|
|
|
|
Rueckgabe: (fortgeschriebene Elementnummer, dict switches-"key" -> erzeugtes
|
|
Item) - die Key-Zuordnung braucht process_blocks, um nach dem Kreisel-
|
|
Umlauf die tatsaechliche "Nachbarn"-Spalte der Weiche zu setzen.
|
|
"""
|
|
naechste_id = max_numeric_teileid(items)
|
|
elem_nr = start_elem_nr
|
|
weiche_by_key = {}
|
|
for n, sw in enumerate(switches, start=1):
|
|
elem_nr += 1
|
|
naechste_id += 1
|
|
teileid = f"{naechste_id:04d}"
|
|
planquadrat = (compute_planquadrat(sw["x"], sw["y"], planquadrat_cfg)
|
|
if planquadrat_cfg is not None else "")
|
|
item = {
|
|
"nr": elem_nr,
|
|
"teileart": WEICHE_TEILEART,
|
|
"teileid": teileid,
|
|
"bezeichnung": f"ILS Weiche :{n}",
|
|
"planquadrat": planquadrat,
|
|
"position": f'{sw["x"]:.2f}, {sw["y"]:.2f}, {sw["z"]:.2f}',
|
|
"boundingbox": f'{sw["dx"]:.2f}, {sw["dy"]:.2f}, {sw["dz"]:.2f}',
|
|
"insertpoint": "",
|
|
"k1": "", "k2": "", "k3": "", "k4": "",
|
|
"nachbarn": ", ".join(sw["nachbarn"]),
|
|
"fehler": "",
|
|
"merkmale": {
|
|
"Erzeugt aus": "Kreisel-Beruehrpunkt",
|
|
"Kreisel A": sw["nachbarn"][0],
|
|
"Kreisel B": sw["nachbarn"][1],
|
|
"Achse von Kreisel": sw["achse_von"],
|
|
"Beruehrung": sw["beruehrung"],
|
|
},
|
|
}
|
|
items.append(item)
|
|
weiche_by_key[sw["key"]] = item
|
|
return elem_nr, weiche_by_key
|
|
|
|
|
|
def _strecke_typ_feld(teileart):
|
|
"""Merkmals-Feldname fuer die Ketten-Zugehoerigkeit einer AS/ES-/Separator-
|
|
Zeile, je Kettentyp unterschiedlich (User-Vorgabe): VF_n ("ILS 2.0
|
|
Strecke") -> "Förderer", GF_n ("ILS 2.0 Gefaellestrecke") -> "Strecke"
|
|
(wie bisher)."""
|
|
return "Strecke" if teileart == "ILS 2.0 Gefaellestrecke" else "Förderer"
|
|
|
|
|
|
def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette=None,
|
|
sep_kette_by_key=None, dbg=None):
|
|
"""Haengt fuer jedes Strecken-Ende mit anschliessendem Kreisel eine
|
|
"ILS Ausschleuselement"- (Anfang) bzw. "ILS Einschleuselement"-Zeile
|
|
(Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus
|
|
fuer Position/Boundingbox - reine Geometrie, unveraendert).
|
|
|
|
Muss NACH build_kreisel_weiche_items laufen, damit die TeileId-Zaehlung
|
|
(max_numeric_teileid) auch die bereits erzeugten Weichen einschliesst.
|
|
Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische, an der
|
|
Kreiselachse ausgerichtete Default-Box.
|
|
|
|
sep_kette = Rueckgabe von export_neighbors.compute_sep_kette (keys
|
|
"<strecke_id>#<lfdnr>" -> Eintrag mit typ/vorgaenger/nachfolger). Traegt
|
|
die zugehoerige Strecke eine "sepliste" (neue LISP-XDATA, siehe
|
|
ssg-sepliste-xdata-schreiben), bleibt "Nachbarn" hier zunaechst LEER und
|
|
der sep_kette-Schluessel dieser Zeile wird stattdessen in
|
|
sep_kette_by_key (dict, vom Aufrufer bereitgestellt und HIER befuellt)
|
|
eingetragen - process_blocks loest daraus NACH der Registrierung ALLER
|
|
SEP/AS/ES-Schluessel (auch der von map_separator_kette_items) die
|
|
tatsaechlichen TeileIds auf (eigener Block am Ende von process_blocks,
|
|
direkt nach beiden Aufrufen). Ein sep_kette-Schluessel ist naemlich KEINE TeileId,
|
|
sondern nur waehrend des Baus eindeutig -
|
|
ihn direkt in die CSV zu schreiben waere ein Bug (zeigte frueher auf
|
|
"<strecke_id>#<lfdnr>" statt auf den eigentlichen Nachbarn). Die
|
|
Kreisel-/Foerderer-Zugehoerigkeit steht bei sepliste-Zeilen stattdessen
|
|
in den Merkmalen ("Kreisel" + "Förderer"/"Strecke" je Typ, analog zum
|
|
"Zuordnung"-Feld der Separatoren). Fehlt die sepliste (Altbestand-Wrapper
|
|
ohne die neue XDATA), bleibt der bisherige Nachbarn-Inhalt
|
|
[kreisel_id, strecke_id] unveraendert erhalten (sofort aufgeloest, kein
|
|
sep_kette-Schluessel involviert).
|
|
|
|
Rueckgabe: fortgeschriebene Elementnummer.
|
|
"""
|
|
if sep_kette is None:
|
|
sep_kette = {}
|
|
if sep_kette_by_key is None:
|
|
sep_kette_by_key = {}
|
|
schleus, warnings = compute_strecke_kreisel_schleus(
|
|
items, durchmesser_mm=KREISEL_DURCHMESSER_MM,
|
|
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
|
|
|
|
strecke_by_id = {it.get("teileid", ""): it for it in items
|
|
if it.get("teileart", "") in GF_VF_TEILEARTEN}
|
|
|
|
# Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile
|
|
# schreiben (Spalte "Warnungen") und in die Konsole ausgeben.
|
|
if warnings:
|
|
for sid, text in warnings.items():
|
|
if sid in strecke_by_id:
|
|
strecke_by_id[sid]["warnung"] = text
|
|
print(f"[export_csv] WARNUNG: Strecke {sid} {text}")
|
|
|
|
naechste_id = max_numeric_teileid(items)
|
|
elem_nr = start_elem_nr
|
|
zaehler = {}
|
|
for sp in schleus:
|
|
elem_nr += 1
|
|
naechste_id += 1
|
|
teileid = f"{naechste_id:04d}"
|
|
teileart = sp["teileart"]
|
|
zaehler[teileart] = zaehler.get(teileart, 0) + 1
|
|
planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg)
|
|
if planquadrat_cfg is not None else "")
|
|
|
|
strecke = strecke_by_id.get(sp["strecke_id"])
|
|
sepliste = strecke.get("sepliste") if strecke else None
|
|
merkmale = {"Erzeugt aus": "Kreisel-Strecke-Kollision"}
|
|
sep_kette_key = None
|
|
if sepliste:
|
|
# Neue XDATA vorhanden: Nachbarn nur noch aus der Ketten-internen
|
|
# Vorgaenger/Nachfolger-Beziehung (AS/ES ist immer das erste bzw.
|
|
# letzte Sepliste-Element seiner Strecke). Die Keys (vorgaenger/
|
|
# nachfolger) sind sep_kette-interne Schluessel, KEINE fertigen
|
|
# TeileIds - erst nach dem Bau ALLER SEP/AS/ES-Zeilen dieser Kette
|
|
# aufloesbar (siehe process_blocks, Abschnitt nach beiden Aufrufen,
|
|
# analog zum Kreisel-Umlauf/Weichen-Muster).
|
|
lfdnr = sepliste[0]["lfdnr"] if sp["ende_label"] == "Anfang" else sepliste[-1]["lfdnr"]
|
|
sep_kette_key = f"{sp['strecke_id']}#{lfdnr}"
|
|
nachbarn = [] # in process_blocks nachgetragen
|
|
merkmale["Kreisel"] = sp["kreisel_id"]
|
|
merkmale[_strecke_typ_feld(strecke.get("teileart", ""))] = sp["strecke_id"]
|
|
else:
|
|
nachbarn = [sp["kreisel_id"], sp["strecke_id"]]
|
|
merkmale["Kreisel"] = sp["kreisel_id"]
|
|
merkmale["Strecke"] = sp["strecke_id"]
|
|
merkmale["Streckenende"] = sp["ende_label"]
|
|
|
|
item = {
|
|
"nr": elem_nr,
|
|
"teileart": teileart,
|
|
"teileid": teileid,
|
|
"bezeichnung": f"{teileart} :{zaehler[teileart]}",
|
|
"planquadrat": planquadrat,
|
|
"position": f'{sp["x"]:.2f}, {sp["y"]:.2f}, {sp["z"]:.2f}',
|
|
"boundingbox": f'{sp["dx"]:.2f}, {sp["dy"]:.2f}, {sp["dz"]:.2f}',
|
|
"insertpoint": "",
|
|
"k1": "", "k2": "", "k3": "", "k4": "",
|
|
"nachbarn": ", ".join(nachbarn),
|
|
"fehler": "",
|
|
"merkmale": merkmale,
|
|
}
|
|
items.append(item)
|
|
if sep_kette_key is not None:
|
|
sep_kette_by_key[sep_kette_key] = item
|
|
return elem_nr
|
|
|
|
|
|
def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None):
|
|
"""Ordnet jeden in einer VF_n/GF_n-Kette eingebetteten Separator
|
|
(sepliste-Eintrag typ "SEP", siehe compute_sep_kette) dem BEREITS
|
|
vorhandenen Separator-Item zu und traegt es unter seinem sep_kette-
|
|
Schluessel in sep_kette_by_key ein - fuer die Nachbar-Aufloesung in
|
|
resolve_umlauf_und_sepliste_nachbarn.
|
|
|
|
WICHTIG - erzeugt KEINE neuen Zeilen (frueher tat das
|
|
build_separator_kette_items): die in einer Kette verpackten Separatoren
|
|
liegen bereits als ECHTE Separator_SP-INSERTs in der Zeichnung - der
|
|
LISP-Export fuegt fuer jedes verpackte Symbol eine temporaere Kopie ein
|
|
(csv:sep-proxies-erzeugen, export.lsp), die den normalen freien-Separator-
|
|
Pfad in process_blocks durchlaeuft (mit echter vla-getboundingbox-Box, ID
|
|
und fest gesetzter ZUORDNUNG = Wrapper-ID ueber "zuordnung_fix"). Beide
|
|
Wege wuerden sonst dieselben physischen Separatoren DOPPELT zaehlen (26
|
|
Proxy-INSERTs + 26 sepliste-Zeilen = 64 statt 38). Die sepliste liefert
|
|
hier nur noch die REIHENFOLGE (Vorgaenger/Nachfolger), nicht die Zeilen.
|
|
|
|
Zuordnung sepliste-SEP -> vorhandenes Separator-Item: ueber die
|
|
Wrapper-ID (Item-Merkmal "Zuordnung" == sep_kette-Eintrag "strecke_id",
|
|
fixiert durch den LISP-Proxy) UND den naechstgelegenen INSERT-Punkt
|
|
(Item-Felder "_x"/"_y" vs. sepliste-Koordinate x/y - beide sind der rohe
|
|
Sub-INSERT-Einfuegepunkt aus derselben Bauphase, daher deckungsgleich;
|
|
verifiziert 26/26 <300mm). Reicht der Wrapper mehrere Separatoren, trennt
|
|
der INSERT-Punkt sie; bei nur einem ist die Wrapper-ID schon eindeutig.
|
|
|
|
Ein sepliste-SEP ohne passendes Item (sollte nicht vorkommen, da der Proxy
|
|
jeden verpackten Separator einfuegt) wird uebersprungen (dbg-Hinweis).
|
|
"""
|
|
if sep_kette_by_key is None:
|
|
sep_kette_by_key = {}
|
|
# Vorhandene Separator-Items nach Wrapper-Zuordnung gruppieren (nur die mit
|
|
# fixierter Zuordnung = die Proxy-Kopien verpackter Ketten-Separatoren;
|
|
# frei platzierte Separatoren tragen keine sepliste und stoeren nicht).
|
|
sep_items_by_zuordnung = {}
|
|
for it in items:
|
|
if it.get("teileart", "") != "ILS 2.0 Separator":
|
|
continue
|
|
zuordnung = it.get("merkmale", {}).get("Zuordnung", "")
|
|
if zuordnung:
|
|
sep_items_by_zuordnung.setdefault(zuordnung, []).append(it)
|
|
|
|
for key, eintrag in sep_kette.items():
|
|
if eintrag.get("typ") != "SEP":
|
|
continue
|
|
kandidaten = sep_items_by_zuordnung.get(eintrag["strecke_id"], [])
|
|
if not kandidaten:
|
|
if dbg:
|
|
dbg(f" sepliste-SEP {key}: kein Separator-Item mit Zuordnung "
|
|
f"{eintrag['strecke_id']!r} gefunden - uebersprungen")
|
|
continue
|
|
# Naechster INSERT-Punkt zur sepliste-Koordinate.
|
|
sx, sy = eintrag.get("x", 0.0), eintrag.get("y", 0.0)
|
|
best = min(kandidaten,
|
|
key=lambda it: math.hypot(it.get("_x", 0.0) - sx,
|
|
it.get("_y", 0.0) - sy))
|
|
sep_kette_by_key[key] = best
|
|
if dbg:
|
|
dbg(f" sepliste-SEP {key} -> Separator-Item TeileId "
|
|
f"{best.get('teileid', '')!r}")
|
|
return sep_kette_by_key
|
|
|
|
|
|
def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
|
sep_kette_by_key, weiche_by_key):
|
|
"""Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items/
|
|
build_strecke_schleus_items/map_separator_kette_items zunaechst leer
|
|
gelassen (bzw. deren Item nur registriert) haben, weil ihr(e) Nachbar(n)
|
|
beim eigenen Bau/der Registrierung noch nicht als TeileId feststand(en)
|
|
(Weiche vs. Separator im Kreisel-Umlauf, oder eine sepliste-Zeile, deren
|
|
Nachfolger erst danach registriert wird).
|
|
|
|
Muss NACH allen drei Aufrufen laufen (ihre TeileIds stehen erst dann
|
|
fest). items wird IN-PLACE veraendert (item["nachbarn"]).
|
|
|
|
kreisel_umlauf = Rueckgabe von export_neighbors.compute_kreisel_umlauf.
|
|
sep_kette = Rueckgabe von export_neighbors.compute_sep_kette.
|
|
sep_kette_by_key = dict sep_kette-Schluessel -> Item (befuellt von
|
|
build_strecke_schleus_items UND
|
|
map_separator_kette_items - DIESELBE dict fuer beide,
|
|
siehe process_blocks: ein sep_kette-Nachbar kann in der
|
|
jeweils anderen Funktion registriert worden sein).
|
|
weiche_by_key = dict switches-"key" -> erzeugtes Item (von
|
|
build_kreisel_weiche_items).
|
|
"""
|
|
# Gemeinsamer Lookup fuer BEIDE Schluesselarten, die in kreisel_umlauf und
|
|
# sep_kette als punkt_key/vorgaenger/nachfolger auftreten koennen: freie
|
|
# Punkte (Separator/BTMT Beladung/BTMT Entladung, TeileId - siehe
|
|
# KREISEL_UMLAUF_TEILEARTEN), Kreisel-Kreisel-Weichen
|
|
# (weiche_by_key) UND sepliste-Zeilen (sep_kette_by_key, Schluessel
|
|
# "<strecke_id>#<lfdnr>"). Ein Umlauf-Punkt kann naemlich ein AS/ES-Ende
|
|
# sein (sep_kette-Schluessel) und ein sep_kette-Vorgaenger/Nachfolger kann
|
|
# das freie Kreiselende gar nicht sein (das loest ausschliesslich
|
|
# compute_kreisel_umlauf auf) - erst der gemeinsame Lookup deckt alle
|
|
# Kombinationen ab.
|
|
freipunkt_by_id = {it.get("teileid", ""): it for it in items
|
|
if it.get("teileart", "") in KREISEL_UMLAUF_TEILEARTEN}
|
|
alle_schluessel = {**sep_kette_by_key, **weiche_by_key, **freipunkt_by_id}
|
|
|
|
# Freie Separatoren, Weichen UND die AS/ES-Enden andockender Ketten
|
|
# bekommen ihre "Nachbarn"-Spalte aus dem Kreisel-Umlauf.
|
|
if kreisel_umlauf:
|
|
for punkte in kreisel_umlauf.values():
|
|
for punkt_key, vorgaenger, nachfolger in punkte:
|
|
item = alle_schluessel.get(punkt_key)
|
|
if item is None:
|
|
continue
|
|
nachbarn = []
|
|
for nachbar_key in (vorgaenger, nachfolger):
|
|
if not nachbar_key:
|
|
continue
|
|
nachbar_item = alle_schluessel.get(nachbar_key)
|
|
if nachbar_item is not None:
|
|
nachbarn.append(nachbar_item.get("teileid", ""))
|
|
if nachbarn:
|
|
item["nachbarn"] = ", ".join(nachbarn)
|
|
|
|
# Ketteninterne sepliste-Nachbarn (SEP <-> SEP, SEP <-> AS/ES) - das
|
|
# jeweilige Kettenende (vorgaenger/nachfolger ist None, z.B. das AS-Ende
|
|
# ohne SEP davor) bleibt hier unveraendert, es wird stattdessen oben vom
|
|
# Kreisel-Umlauf gesetzt (compute_sep_kette dokumentiert diese Aufteilung).
|
|
for key, eintrag in sep_kette.items():
|
|
item = sep_kette_by_key.get(key)
|
|
if item is None:
|
|
continue
|
|
nachbarn = []
|
|
for nachbar_key in (eintrag.get("vorgaenger"), eintrag.get("nachfolger")):
|
|
if not nachbar_key:
|
|
continue
|
|
nachbar_item = alle_schluessel.get(nachbar_key)
|
|
if nachbar_item is not None:
|
|
nachbarn.append(nachbar_item.get("teileid", ""))
|
|
if nachbarn:
|
|
item["nachbarn"] = ", ".join(nachbarn)
|
|
|
|
|
|
def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None):
|
|
"""Traegt in jede Strecke (VF_n/GF_n mit "sepliste") das Merkmal
|
|
"list_trackids" ein: die TeileIds ihrer Separatoren in Baureihenfolge,
|
|
kommagetrennt (analog zu "Zuordnung"/"Nachbarn"), z.B. "0052, 0053, 0054".
|
|
|
|
Quelle ist die rohe "sepliste" des Items (AS/SEP/ES mit x/y, bereits in
|
|
Baureihenfolge - siehe compute_sep_kette) statt sep_kette selbst: dessen
|
|
dict-Reihenfolge ist nicht garantiert, die sepliste-Liste dagegen schon.
|
|
Nur "SEP"-Eintraege zaehlen als Separator, AS/ES sind die Schleus-Enden
|
|
der Kette (siehe build_strecke_schleus_items), keine Separatoren.
|
|
|
|
Der eigentliche Separator wurde bereits von map_separator_kette_items ueber
|
|
die sepliste-Koordinate (x/y) auf sein reales Separator-Item (TeileId)
|
|
aufgeloest und unter demselben "<strecke_id>#<lfdnr>"-Schluessel in
|
|
sep_kette_by_key eingetragen - hier wird dieser Schluessel nur noch je
|
|
Strecke eingesammelt. Muss darum NACH map_separator_kette_items laufen.
|
|
"""
|
|
for item in items:
|
|
if item.get("teileart", "") not in GF_VF_TEILEARTEN:
|
|
continue
|
|
sepliste = item.get("sepliste")
|
|
if not sepliste:
|
|
continue
|
|
sid = item.get("teileid", "")
|
|
separator_ids = []
|
|
for entry in sepliste:
|
|
if entry.get("typ") != "SEP":
|
|
continue
|
|
key = f"{sid}#{entry.get('lfdnr')}"
|
|
sep_item = sep_kette_by_key.get(key)
|
|
if sep_item is None:
|
|
if dbg:
|
|
dbg(f" list_trackids {key}: kein aufgeloestes Separator-Item "
|
|
f"- uebersprungen")
|
|
continue
|
|
separator_ids.append(sep_item.get("teileid", ""))
|
|
if separator_ids:
|
|
item["merkmale"]["list_trackids"] = ", ".join(separator_ids)
|
|
|
|
|
|
def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_key,
|
|
weiche_by_key, dbg=None):
|
|
"""Traegt in jeden Kreisel ("ILS 2.0 Kreisel") das Merkmal "list_trackids"
|
|
ein (gleicher Schluessel wie bei VF_n/GF_n, siehe write_separatorliste_
|
|
merkmale): die TeileIds aller auf ihm liegenden freien Punkte (Separator,
|
|
BTMT Beladung/Entladung - siehe KREISEL_UMLAUF_TEILEARTEN),
|
|
andockenden AS/ES-Streckenenden und Kreisel-Kreisel-Weichen, in
|
|
Umlaufreihenfolge ab Winkel 0 Grad (siehe compute_kreisel_umlauf - die
|
|
Liste beginnt dort bereits bei 0 Grad in Drehrichtung DREHRICHTUNG),
|
|
kommagetrennt.
|
|
|
|
Quelle ist kreisel_umlauf (export_neighbors.compute_kreisel_umlauf), das
|
|
fuer jeden Kreisel bereits alle Punkte (freie Punkte, AS/ES-Enden,
|
|
Kreisel-Kreisel-Weichen) nach Winkel um die Kreiselachse sortiert hat -
|
|
hier werden nur noch deren rohe Schluessel (TeileId direkt, sep_kette-
|
|
Schluessel oder Weichen-"key") auf die TeileId des jeweils zugehoerigen
|
|
Items aufgeloest. Derselbe Lookup wie in
|
|
resolve_umlauf_und_sepliste_nachbarn, daher muss diese Funktion NACH
|
|
map_separator_kette_items/build_kreisel_weiche_items/
|
|
build_strecke_schleus_items laufen (deren TeileIds stehen erst dann fest).
|
|
"""
|
|
freipunkt_by_id = {it.get("teileid", ""): it for it in items
|
|
if it.get("teileart", "") in KREISEL_UMLAUF_TEILEARTEN}
|
|
alle_schluessel = {**sep_kette_by_key, **weiche_by_key, **freipunkt_by_id}
|
|
|
|
kreisel_by_id = {it.get("teileid", ""): it for it in items
|
|
if it.get("teileart", "") == "ILS 2.0 Kreisel"}
|
|
|
|
for tid, punkte in (kreisel_umlauf or {}).items():
|
|
kreisel_item = kreisel_by_id.get(tid)
|
|
if kreisel_item is None or not punkte:
|
|
continue
|
|
teileids = []
|
|
for punkt_key, _vorgaenger, _nachfolger in punkte:
|
|
item = alle_schluessel.get(punkt_key)
|
|
if item is None:
|
|
if dbg:
|
|
dbg(f" list_trackids Kreisel {tid}: kein Item fuer "
|
|
f"Umlauf-Punkt {punkt_key!r} - uebersprungen")
|
|
continue
|
|
teileids.append(item.get("teileid", ""))
|
|
if teileids:
|
|
kreisel_item["merkmale"]["list_trackids"] = ", ".join(teileids)
|
|
|
|
|
|
def process_blocks(blocks, lookup, dbg=None):
|
|
# x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe
|
|
# export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls
|
|
# nicht explizit in [Planquadrate] konfiguriert.
|
|
planquadrat_cfg = resolve_origins(PLANQUADRAT_CFG, blocks)
|
|
|
|
items = []
|
|
elem_nr = 0
|
|
bogen_count = 0
|
|
weiche_count = {}
|
|
gerade_count = 0
|
|
kreisel_count = 0
|
|
eckrad_count = 0
|
|
vf_count = 0
|
|
gf_count = 0
|
|
strecke_modul_count = 0
|
|
separator_count = 0
|
|
scanner_count = 0
|
|
btmt_beladung_count = 0
|
|
btmt_entladung_count = 0
|
|
|
|
# Zaehlung fuer die Omniflo-Sum-Zeile (nur Bogen/Weiche/Gerade)
|
|
cnt_wk = cnt_einzel = cnt_delta = cnt_doppel = cnt_stern = 0
|
|
len_ap110_mm = 0.0
|
|
len_ap60_mm = 0.0
|
|
has_omniflo = False
|
|
|
|
for block in blocks:
|
|
bname = block.get("block_name", "")
|
|
|
|
if bname in SKIP_BLOCKS:
|
|
continue
|
|
|
|
# Omniflo Bogen oder Weiche
|
|
if bname in lookup:
|
|
typ, eintrag = lookup[bname]
|
|
elem_nr += 1
|
|
shape_id = block.get("attribs", {}).get("ID", "0000")
|
|
has_omniflo = True
|
|
|
|
if typ == "bogen":
|
|
bogen_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "Omniflo Kurve",
|
|
"teileid": shape_id,
|
|
"bezeichnung": f"OFBogen :{bogen_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_bogen_merkmale(block, eintrag),
|
|
**bbox_columns(block),
|
|
})
|
|
|
|
elif typ == "weiche":
|
|
wt = eintrag.get("WeichenTyp", "Einzelweiche")
|
|
weiche_count[wt] = weiche_count.get(wt, 0) + 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "Omniflo Weiche",
|
|
"teileid": shape_id,
|
|
"bezeichnung": f"OFWeiche :{weiche_count[wt]}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_weiche_merkmale(block, eintrag),
|
|
**bbox_columns(block),
|
|
})
|
|
subtyp = weichensubtyp(block, eintrag)
|
|
if subtyp == "weichenkoerper":
|
|
cnt_wk += 1
|
|
elif subtyp == "deltaweiche":
|
|
cnt_delta += 1
|
|
elif subtyp == "sternweiche":
|
|
cnt_stern += 1
|
|
elif subtyp == "doppelweiche":
|
|
cnt_doppel += 1
|
|
else:
|
|
cnt_einzel += 1
|
|
continue
|
|
|
|
# Omniflo Gerade (AP110)
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_gerade", [])):
|
|
elem_nr += 1
|
|
gerade_count += 1
|
|
has_omniflo = True
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "Omniflo Gerade",
|
|
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
|
"bezeichnung": f"OFGerade :{gerade_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_gerade_merkmale(block),
|
|
**bbox_columns(block),
|
|
})
|
|
len_ap110_mm += get_laenge_mm(block)
|
|
continue
|
|
|
|
# VarioFoerderer Compound-Block (VF_N)
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_variofoerderer", [])):
|
|
elem_nr += 1
|
|
vf_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 Strecke",
|
|
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
|
"bezeichnung": f"VarioFoerderer :{vf_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_variofoerderer_merkmale(block),
|
|
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
|
|
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
|
|
"x": block.get("x", 0.0),
|
|
"y": block.get("y", 0.0),
|
|
# Sepliste-XDATA (AS/SEP/ES-Reihenfolge) durchreichen - Quelle
|
|
# fuer compute_sep_kette/map_separator_kette_items (Ketten-
|
|
# interne Separatoren + AS/ES-Nachbarn). Ohne diese Weitergabe
|
|
# liefe die gesamte sepliste-basierte Nachbarverkettung ins
|
|
# Leere (das Feld stand im JSON, wurde aber nicht ans Item
|
|
# uebernommen).
|
|
"sepliste": block.get("sepliste"),
|
|
**bbox_columns(block),
|
|
})
|
|
continue
|
|
|
|
# Gefaellestrecke Compound-Block (GF_N)
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_gefaellestrecke", [])):
|
|
elem_nr += 1
|
|
gf_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 Gefaellestrecke",
|
|
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
|
"bezeichnung": f"Gefaellestrecke :{gf_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_variofoerderer_merkmale(block),
|
|
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
|
|
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
|
|
"x": block.get("x", 0.0),
|
|
"y": block.get("y", 0.0),
|
|
# Sepliste-XDATA durchreichen (siehe VF_N oben).
|
|
"sepliste": block.get("sepliste"),
|
|
**bbox_columns(block),
|
|
})
|
|
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", [])):
|
|
elem_nr += 1
|
|
eckrad_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 Eckrad",
|
|
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
|
"bezeichnung": f"Eckrad :{eckrad_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_kreisel_merkmale(block),
|
|
**bbox_columns(block),
|
|
})
|
|
continue
|
|
|
|
# ILS Kreisel (Compound-Bloecke mit Praefix KR_ oder KREISEL_)
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_kreisel", [])):
|
|
elem_nr += 1
|
|
kreisel_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 Kreisel",
|
|
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
|
"bezeichnung": f"Kreisel :{kreisel_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_kreisel_merkmale(block),
|
|
# x/y/rotation/attribs (roh, nicht nur die Merkmale) werden
|
|
# von export_neighbors.kreisel_half_bboxes gebraucht, um die
|
|
# Links/Rechts-Trennung ueber AN8/SP8/ABSTAND nachzurechnen -
|
|
# bbox_columns() liefert nur die fertige Gesamt-BBox, keine
|
|
# Rohdaten dafuer.
|
|
"x": block.get("x", 0.0),
|
|
"y": block.get("y", 0.0),
|
|
"rotation": block.get("rotation", 0.0),
|
|
"attribs": block.get("attribs", {}),
|
|
**bbox_columns(block),
|
|
})
|
|
continue
|
|
|
|
# ILS Strecke-Module (Vario/Staustrecke-Einzelkomponenten)
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_strecke_module", [])):
|
|
elem_nr += 1
|
|
strecke_modul_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 Strecke - Modul",
|
|
"teileid": block.get("attribs", {}).get("ID", "0000"),
|
|
"bezeichnung": f"Streckenmodul :{strecke_modul_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": {"Blockname": bname},
|
|
**bbox_columns(block),
|
|
})
|
|
continue
|
|
|
|
# Separator (Separator_SP/S-LP) - eigene TeileArt-Zeile. TeileId: das
|
|
# echte ID-Attribut, falls vorhanden (Separator_SP/Scanner haben seit
|
|
# kurzem ein eigenes ID-ATTDEF, siehe ssg-id-collect-blocks in
|
|
# ssg_id.lsp - ssg-id-check-all fuellt es vor jedem Export). Fallback
|
|
# format_sensor_id (laufende SEP-/SCN-Nummer NUR fuer diesen Export-
|
|
# lauf) fuer aeltere Bloecke, deren ID-Attribut noch leer ist (z.B.
|
|
# vor dem naechsten Insert/ssg-id-check-all-Durchlauf).
|
|
# EXPORTSIVAS schliesst denselben Blockpattern bewusst aus (siehe
|
|
# export_sivas.py) - dort zaehlt bereits ANZAHL_SEPARATOR am
|
|
# zugehoerigen Kreisel/GF/VF.
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_separator", [])):
|
|
elem_nr += 1
|
|
separator_count += 1
|
|
cols = bbox_columns(block)
|
|
# Fehlende Bounding-Box (vla-getboundingbox scheiterte, z.B.
|
|
# eingefrorener Layer) durch die feste Separator-Symbol-Box aus
|
|
# cfg [Boundingbox] separator_box_* ersetzen - sonst nimmt der
|
|
# Separator an der Nachbarschafts-/Umlauf-/Zuordnungserkennung
|
|
# nicht teil (die brauchen "_bbox"). Position/Drehung des realen
|
|
# INSERTs sind ja bekannt.
|
|
cols = _fallback_separator_bbox(cols, block)
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 Separator",
|
|
"teileid": block.get("attribs", {}).get("ID") or format_sensor_id("SEP", separator_count),
|
|
"bezeichnung": f"Separator :{separator_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_sensor_merkmale(block),
|
|
# Privates Feld (nicht in der CSV): markiert eine BEKANNTE
|
|
# Zuordnung, die compute_sensor_zuordnung unten nicht
|
|
# ueberschreiben darf - siehe build_sensor_merkmale.
|
|
"_zuordnung_fix": bool(block.get("zuordnung_fix")),
|
|
# Roher INSERT-Punkt (nicht der bbox-Mittelpunkt) - dient
|
|
# map_separator_kette_items zum Zuordnen dieses (Proxy-)
|
|
# Separators zu seinem sepliste-Eintrag (Match ueber
|
|
# Wrapper-ID + naechster INSERT-Punkt, siehe dort).
|
|
"_x": block.get("x", 0.0), "_y": block.get("y", 0.0),
|
|
**cols,
|
|
})
|
|
continue
|
|
|
|
# Scanner - eigene TeileArt-Zeile, sonst analog zu Separator oben.
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_scanner", [])):
|
|
elem_nr += 1
|
|
scanner_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 Scanner",
|
|
"teileid": block.get("attribs", {}).get("ID") or format_sensor_id("SCN", scanner_count),
|
|
"bezeichnung": f"Scanner :{scanner_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_sensor_merkmale(block),
|
|
# "warnung" kommt von cs-zuordnung-lauf (Lisp/count_sep_scan.lsp),
|
|
# das vor dem Export strittige Distanz-Zuordnungen (Scanner
|
|
# ausserhalb jeder Kreisel-/Strecken-Boundingbox, Abstand zu zwei
|
|
# Kandidaten fast gleich) in *cs-scanner-warnung-by-handle*
|
|
# sammelt - csv:block-to-json (export.lsp) haengt den Text als
|
|
# "warnung"-Feld an den betroffenen Scanner-Block im JSON an.
|
|
"warnung": block.get("warnung", ""),
|
|
**bbox_columns(block),
|
|
})
|
|
continue
|
|
|
|
# BTMT-Beladestation - eigene TeileArt-Zeile, sonst analog zu Separator/
|
|
# Scanner oben. Anders als dort NICHT von export_sivas.py ausgeschlossen
|
|
# (siehe SEP_SCANNER_PATTERNS-Kommentar) - eine BTMT-Station wird von
|
|
# keinem anderen Element mitgezaehlt.
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_beladung", [])):
|
|
elem_nr += 1
|
|
btmt_beladung_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 BTMT Beladung",
|
|
"teileid": block.get("attribs", {}).get("ID") or format_sensor_id("BEL", btmt_beladung_count),
|
|
"bezeichnung": f"BTMT Beladung :{btmt_beladung_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_btmt_merkmale(block),
|
|
**bbox_columns(block),
|
|
})
|
|
continue
|
|
|
|
# BTMT-Entladestation (Abwurfstation) - analog zur Beladestation oben.
|
|
if matches_any(bname, BLOCKPATTERNS.get("pattern_btmt_entladung", [])):
|
|
elem_nr += 1
|
|
btmt_entladung_count += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "ILS 2.0 BTMT Entladung",
|
|
"teileid": block.get("attribs", {}).get("ID") or format_sensor_id("ENT", btmt_entladung_count),
|
|
"bezeichnung": f"BTMT Entladung :{btmt_entladung_count}",
|
|
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
|
"merkmale": build_btmt_merkmale(block),
|
|
**bbox_columns(block),
|
|
})
|
|
continue
|
|
|
|
if has_omniflo:
|
|
elem_nr += 1
|
|
items.append({
|
|
"nr": elem_nr,
|
|
"teileart": "Omniflo Sum",
|
|
"teileid": "autogenerated_of_json",
|
|
"bezeichnung": "Omniflo sum",
|
|
"planquadrat": "",
|
|
"merkmale": build_omni_sum_merkmale(
|
|
bogen_count, cnt_wk, cnt_einzel, cnt_delta, cnt_doppel, cnt_stern,
|
|
len_ap110_mm, len_ap60_mm),
|
|
"position": "",
|
|
"boundingbox": "",
|
|
"insertpoint": "",
|
|
"k1": "",
|
|
"k2": "",
|
|
"k3": "",
|
|
"k4": "",
|
|
})
|
|
|
|
neighbor_ids = compute_neighbor_ids(
|
|
items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM,
|
|
kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM,
|
|
omniflo_ks_tolerance_mm=NEIGHBOR_OMNIFLO_KS_TOLERANCE_MM,
|
|
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
|
|
neighbor_errors = compute_neighbor_errors(items, neighbor_ids)
|
|
for item, nachbarn, fehler in zip(items, neighbor_ids, neighbor_errors):
|
|
item["nachbarn"] = nachbarn
|
|
item["fehler"] = fehler
|
|
|
|
# Separator/Scanner: Zuordnung zu Gefaellestrecke/Foerderer/Kreiselhaelfte
|
|
# per BBox-Ueberschneidung berechnen (Prioritaet siehe compute_sensor_
|
|
# zuordnung) - ersetzt die von Hand gesetzte ZUORDNUNG-Attribut-Merkmale
|
|
# nur, wenn tatsaechlich ein Treffer gefunden wurde.
|
|
sensor_zuordnung = compute_sensor_zuordnung(
|
|
items, NEIGHBOR_TOLERANCE_MM, kreisel_durchmesser_mm=KREISEL_DURCHMESSER_MM, dbg=dbg)
|
|
for item, zuordnung in zip(items, sensor_zuordnung):
|
|
if item.get("teileart", "") not in SENSOR_TEILEARTEN or not zuordnung:
|
|
continue
|
|
# Bekannte Zuordnung (verpackter Separator, Carrier = Wrapper-Block,
|
|
# siehe "_zuordnung_fix"/build_sensor_merkmale) nicht ueberschreiben:
|
|
# die BBox-Suche hier priorisiert GF > Foerderer > Kreiselhaelfte und
|
|
# wuerde einen in einem VF-Block verpackten Separator an eine
|
|
# Gefaellestrecke haengen, in deren Box er zufaellig hineinragt.
|
|
if item.get("_zuordnung_fix"):
|
|
continue
|
|
# Kein Treffer ("nicht zugeordnet") ersetzt einen vorhandenen
|
|
# ZUORDNUNG-Attributwert nicht - sonst geht die in der Zeichnung
|
|
# gesetzte Zuordnung verloren (siehe Docstring von
|
|
# compute_sensor_zuordnung: ersetzen nur bei echtem Treffer).
|
|
if zuordnung == "nicht zugeordnet" and item["merkmale"].get("Zuordnung"):
|
|
continue
|
|
item["merkmale"]["Zuordnung"] = zuordnung
|
|
|
|
# NACH der Separator-Zuordnung: zu jedem Scanner den raeumlich naechst-
|
|
# gelegenen Separator suchen (eigene, unabhaengige Suche - siehe
|
|
# compute_scanner_nearest_separator).
|
|
nearest_separator = compute_scanner_nearest_separator(items, dbg=dbg)
|
|
for item, separator_id in zip(items, nearest_separator):
|
|
if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id:
|
|
item["merkmale"]["Naechster Separator"] = separator_id
|
|
|
|
# Ketteninterne Separator-/AS-/ES-Reihenfolge aus der neuen "sepliste"-
|
|
# XDATA (siehe ssg-sepliste-xdata-schreiben) - leeres dict, wenn keine
|
|
# Strecke eine sepliste traegt (reiner Altbestand ohne neue XDATA).
|
|
sep_kette = compute_sep_kette(items, dbg=dbg)
|
|
|
|
# Kreisel-Kreisel-Weichen VOR dem Umlauf berechnen (reine Geometrie,
|
|
# braucht keine TeileIds) - dieselbe Liste geht sowohl in
|
|
# compute_kreisel_umlauf (damit die Weiche als Verzweigungspunkt in
|
|
# BEIDEN beruehrenden Kreisel-Umlaeufen auftaucht) als auch in
|
|
# build_kreisel_weiche_items (das die tatsaechlichen Zeilen erzeugt).
|
|
touch_switches = compute_kreisel_touch_switches(
|
|
items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM,
|
|
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
|
|
|
|
# Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren (per Zuordnung
|
|
# oben bereits einem Kreisel zugeordnet), die AS/ES-Enden andockender
|
|
# Ketten (aus sep_kette) UND die Kreisel-Kreisel-Weichen (touch_switches),
|
|
# sortiert nach Winkel um die Kreiselachse (siehe compute_kreisel_umlauf -
|
|
# DREHRICHTUNG-Attribut bestimmt den Umlaufsinn).
|
|
kreisel_umlauf = compute_kreisel_umlauf(
|
|
items, sep_kette, NEIGHBOR_TOLERANCE_MM,
|
|
durchmesser_mm=KREISEL_DURCHMESSER_MM, touch_switches=touch_switches, dbg=dbg)
|
|
|
|
# Neue Zeilen, deren "Nachbarn"-Spalte aus einem sep_kette-Schluessel
|
|
# aufgeloest werden muss, erzeugen: Kreisel-Kreisel-Weichen, dann Aus-/
|
|
# Einschleuselemente (AS/ES). Ein sep_kette-Schluessel kann auf eine Zeile
|
|
# zeigen, die ERST DANACH registriert wird, darum erst NACH allen
|
|
# Registrierungen aufloesen (siehe unten) statt inline - frueherer Bug
|
|
# schrieb sonst den rohen Schluessel ("0026#3") statt der TeileId.
|
|
elem_nr, weiche_by_key = build_kreisel_weiche_items(
|
|
items, planquadrat_cfg, elem_nr, touch_switches, dbg=dbg)
|
|
sep_kette_by_key = {}
|
|
elem_nr = build_strecke_schleus_items(
|
|
items, planquadrat_cfg, elem_nr, sep_kette=sep_kette,
|
|
sep_kette_by_key=sep_kette_by_key, dbg=dbg)
|
|
# Die in einer Kette verpackten Separatoren erzeugen KEINE eigenen Zeilen -
|
|
# sie liegen bereits als (Proxy-)Separator_SP-Items vor (freier-Separator-
|
|
# Pfad oben). map_separator_kette_items ordnet ihnen nur ihren sepliste-
|
|
# Schluessel zu, damit die Nachbar-Reihenfolge (AS -> SEP -> ES) auf die
|
|
# vorhandenen Items angewandt werden kann (kein Doppel-Export).
|
|
map_separator_kette_items(
|
|
items, sep_kette, sep_kette_by_key=sep_kette_by_key, dbg=dbg)
|
|
|
|
# Merkmal "list_trackids" je Strecke (Reihenfolge der SEP-TeileIds aus der
|
|
# sepliste, x/y bereits oben auf das reale Separator-Item aufgeloest).
|
|
write_separatorliste_merkmale(items, sep_kette_by_key, dbg=dbg)
|
|
|
|
resolve_umlauf_und_sepliste_nachbarn(
|
|
items, kreisel_umlauf, sep_kette, sep_kette_by_key, weiche_by_key)
|
|
|
|
# Merkmal "list_trackids" je Kreisel: dieselben Umlauf-Punkte (freie
|
|
# Separatoren, AS/ES-Enden, Kreisel-Kreisel-Weichen), die oben schon die
|
|
# "Nachbarn"-Spalte gespeist haben, jetzt als eine einzige, bei 0 Grad
|
|
# beginnende Reihenfolge auf den Kreisel selbst geschrieben (analog zu
|
|
# write_separatorliste_merkmale bei VF_n/GF_n).
|
|
write_kreisel_separatorliste_merkmale(
|
|
items, kreisel_umlauf, sep_kette_by_key, weiche_by_key, dbg=dbg)
|
|
|
|
# Nach dem Sammelheimen aller Zeilen: sortiert nach TeileId ausgeben, damit
|
|
# Elementnummer (erste Spalte) und TeileId nach Moeglichkeit uebereinstimmen
|
|
# (siehe format_csv_line/Elementnummer-Neuvergabe unten). Numerische IDs
|
|
# zuerst aufsteigend; nicht-numerische IDs (z.B. "autogenerated_of_json"
|
|
# der Omniflo-Sum-Zeile oder SEP-/SCN-Fallback-IDs ohne ID-Attribut)
|
|
# behalten stabil ihre bisherige Reihenfolge und stehen danach. Wurde kein
|
|
# Element geloescht, ergibt sich dadurch Elementnummer == TeileId.
|
|
def teileid_sort_key(indexed_item):
|
|
index, item = indexed_item
|
|
tid = str(item.get("teileid", "")).strip()
|
|
return (0, int(tid)) if tid.isdigit() else (1, index)
|
|
|
|
items = [item for _, item in sorted(enumerate(items), key=teileid_sort_key)]
|
|
for i, item in enumerate(items, start=1):
|
|
item["nr"] = i
|
|
|
|
return items
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CSV-Formatierung
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def format_csv_line(item):
|
|
merkmale_json = json.dumps(item["merkmale"], ensure_ascii=False)
|
|
return (
|
|
f'{item["nr"]}'
|
|
f';"{csv_quote(item["teileart"])}"'
|
|
f';"{csv_quote(item["teileid"])}"'
|
|
f';"{csv_quote(item["bezeichnung"])}"'
|
|
f';"{csv_quote(item["planquadrat"])}"'
|
|
f';1'
|
|
f';"{csv_quote(item["position"])}"'
|
|
f';"{csv_quote(item["boundingbox"])}"'
|
|
f';"{csv_quote(item["insertpoint"])}"'
|
|
f';"{csv_quote(item["k1"])}"'
|
|
f';"{csv_quote(item["k2"])}"'
|
|
f';"{csv_quote(item["k3"])}"'
|
|
f';"{csv_quote(item["k4"])}"'
|
|
f';"{csv_quote(item["nachbarn"])}"'
|
|
f';"{csv_quote(item["fehler"])}"'
|
|
f';"{csv_quote(item.get("warnung", ""))}"'
|
|
f';{merkmale_json}'
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
if len(sys.argv) < 4:
|
|
print("Aufruf: python export_csv.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_csv] {len(blocks)} Bloecke geladen, "
|
|
f"{len(boegen)} Boegen, {len(weichen)} Weichen im Katalog.")
|
|
|
|
# Optionales Kollisions-/Nachbarschafts-Debuglog (siehe [Nachbarschaft] ->
|
|
# debug_log in cfg/export.cfg). dbg ist ein Callable(str), das eine Zeile
|
|
# in die .dbg-Datei schreibt - oder None, wenn das Log deaktiviert ist.
|
|
dbg_file = None
|
|
dbg = None
|
|
if NEIGHBOR_DEBUG_TARGET:
|
|
try:
|
|
os.makedirs(os.path.dirname(NEIGHBOR_DEBUG_TARGET), exist_ok=True)
|
|
dbg_file = open(NEIGHBOR_DEBUG_TARGET, "w", encoding="utf-8")
|
|
dbg = lambda msg: dbg_file.write(msg + "\n")
|
|
dbg(f"# Kollisions-/Nachbarschafts-Debuglog (export_csv.py)")
|
|
dbg(f"# Quelle: {raw_json_path}")
|
|
dbg(f"# Bloecke im Roh-JSON: {len(blocks)}")
|
|
dbg("")
|
|
except OSError as exc:
|
|
print(f"[export_csv] WARNUNG: Debuglog nicht schreibbar "
|
|
f"({NEIGHBOR_DEBUG_TARGET}): {exc}")
|
|
dbg_file = None
|
|
dbg = None
|
|
|
|
items = process_blocks(blocks, lookup, dbg=dbg)
|
|
|
|
if dbg_file:
|
|
dbg_file.close()
|
|
print(f"[export_csv] Kollisions-Debuglog geschrieben: {NEIGHBOR_DEBUG_TARGET}")
|
|
|
|
header = ("Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;"
|
|
"Insertpoint;K1;K2;K3;K4;Nachbarn;Fehler;Warnungen;Merkmale")
|
|
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_csv] CSV geschrieben: {output_csv} ({len(items)} Zeilen)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|