CSV-Export: Nachbarschaftserkennung neu aufgebaut (ohne shapely), Fehlerspalte ergaenzt

Nachbarschaft wird jetzt in drei getrennten Gruppen geprueft statt global
ueber alle Elemente: Kreisel/Eckrad gegeneinander, Kreisel/Eckrad gegen
Gefaellestrecke/Foerderer/Strecke-Modul (diese drei nicht gegeneinander),
und Omniflo-Elemente gegeneinander (rasterbasiert vorgefiltert nach x/y-
Koordinate). Ersetzt die bisherige shapely-STRtree-Loesung durch reines
Python, da die shapely-Abhaengigkeit den regulaeren CSV-Export in der von
BricsCAD genutzten Python-Umgebung brechen konnte.

Neue Spalte "Fehler" markiert Elemente mit zu wenigen Partnern
("unverbunden"/"nur ein Partner") je nach TeileArt-Mindestanzahl.

Ausserdem: SSG_RUN_ALL_TESTS-Absturz nach TEST_KREISEL behoben (ssg-start/
ssg-end nutzten flache globale Variablen statt eines Stacks, wodurch
verschachtelte Aufrufe den *error*-Handler dauerhaft korrumpierten) und
FILEDIA=0 um den DXFOUT-Kommandozeilenaufruf ergaenzt.
This commit is contained in:
2026-07-23 10:13:18 +02:00
parent 6d81b30628
commit ad287b683d
8 changed files with 491 additions and 198 deletions
+49 -21
View File
@@ -11,7 +11,7 @@ Aufruf:
python export_csv.py <export_raw.json> <data_dir> <output.csv>
CSV-Format:
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Merkmale
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale
Position und Boundingbox stammen aus der von export.lsp (csv:get-bbox, per
vla-getboundingbox) ermittelten Bounding-Box je Block:
@@ -20,10 +20,21 @@ vla-getboundingbox) ermittelten Bounding-Box je Block:
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 per shapely-STRtree, Toleranz konfigurierbar
ueber [Nachbarschaft] -> toleranz_mm in cfg/export.cfg), siehe export_neighbors.py.
Position, Boundingbox, Planquadrat und Nachbarn sind nur fuer EXPORTCSV
vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export.
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 (rasterbasiert
vorgefiltert nach x/y-Koordinate). Toleranz und Omniflo-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).
Position, Boundingbox, Planquadrat, Nachbarn und Fehler sind nur fuer
EXPORTCSV vorhanden (nicht EXPORTSIVAS) - siehe csv:run-export.
"""
import json
@@ -31,12 +42,18 @@ import sys
import os
from export_blockpatterns import load_patterns, matches_any
from export_planquadrat import load_planquadrat_config, compute_planquadrat
from export_neighbors import load_neighbor_tolerance_mm, compute_neighbor_ids
from export_planquadrat import load_planquadrat_config, resolve_origins, compute_planquadrat
from export_neighbors import (
load_neighbor_tolerance_mm,
load_omniflo_cell_size_mm,
compute_neighbor_ids,
compute_neighbor_errors,
)
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()
# ---------------------------------------------------------------------------
@@ -258,19 +275,21 @@ def bbox_columns(block):
}
def planquadrat_column(block):
def planquadrat_column(block, planquadrat_cfg):
"""Berechnet das Planquadrat aus x/y des Blocks (siehe export_planquadrat.py).
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).
"""
if PLANQUADRAT_CFG is None:
if planquadrat_cfg is None:
return ""
x = block.get("x")
y = block.get("y")
if x is None or y is None:
return ""
return compute_planquadrat(float(x), float(y), PLANQUADRAT_CFG)
return compute_planquadrat(float(x), float(y), planquadrat_cfg)
# ---------------------------------------------------------------------------
@@ -285,6 +304,11 @@ SKIP_BLOCKS = set(BLOCKPATTERNS.get("pattern_ks_subblocks", ["K1", "K2", "K3", "
# ---------------------------------------------------------------------------
def process_blocks(blocks, lookup):
# 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
@@ -322,7 +346,7 @@ def process_blocks(blocks, lookup):
"teileart": "Omniflo Kurve",
"teileid": shape_id,
"bezeichnung": f"OFBogen :{bogen_count}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_bogen_merkmale(block, eintrag),
**bbox_columns(block),
})
@@ -335,7 +359,7 @@ def process_blocks(blocks, lookup):
"teileart": "Omniflo Weiche",
"teileid": shape_id,
"bezeichnung": f"OFWeiche :{weiche_count[wt]}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_weiche_merkmale(block, eintrag),
**bbox_columns(block),
})
@@ -362,7 +386,7 @@ def process_blocks(blocks, lookup):
"teileart": "Omniflo Gerade",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"OFGerade :{gerade_count}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_gerade_merkmale(block),
**bbox_columns(block),
})
@@ -378,7 +402,7 @@ def process_blocks(blocks, lookup):
"teileart": "ILS 2.0 Strecke",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"VarioFoerderer :{vf_count}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_variofoerderer_merkmale(block),
**bbox_columns(block),
})
@@ -393,7 +417,7 @@ def process_blocks(blocks, lookup):
"teileart": "ILS 2.0 Gefaellestrecke",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"Gefaellestrecke :{gf_count}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_variofoerderer_merkmale(block),
**bbox_columns(block),
})
@@ -409,7 +433,7 @@ def process_blocks(blocks, lookup):
"teileart": "ILS 2.0 Eckrad",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"Eckrad :{eckrad_count}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_kreisel_merkmale(block),
**bbox_columns(block),
})
@@ -424,7 +448,7 @@ def process_blocks(blocks, lookup):
"teileart": "ILS 2.0 Kreisel",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"Kreisel :{kreisel_count}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": build_kreisel_merkmale(block),
**bbox_columns(block),
})
@@ -439,7 +463,7 @@ def process_blocks(blocks, lookup):
"teileart": "ILS 2.0 Strecke - Modul",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"Streckenmodul :{strecke_modul_count}",
"planquadrat": planquadrat_column(block),
"planquadrat": planquadrat_column(block, planquadrat_cfg),
"merkmale": {"Blockname": bname},
**bbox_columns(block),
})
@@ -460,9 +484,12 @@ def process_blocks(blocks, lookup):
"boundingbox": "",
})
neighbor_ids = compute_neighbor_ids(items, NEIGHBOR_TOLERANCE_MM)
for item, nachbarn in zip(items, neighbor_ids):
neighbor_ids = compute_neighbor_ids(
items, NEIGHBOR_TOLERANCE_MM, NEIGHBOR_OMNIFLO_CELL_SIZE_MM)
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
return items
@@ -483,6 +510,7 @@ def format_csv_line(item):
f';"{item["position"]}"'
f';"{item["boundingbox"]}"'
f';"{item["nachbarn"]}"'
f';"{item["fehler"]}"'
f';{merkmale_json}'
)
@@ -514,7 +542,7 @@ def main():
items = process_blocks(blocks, lookup)
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Merkmale"
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Nachbarn;Fehler;Merkmale"
with open(output_csv, "w", encoding="utf-8") as f:
f.write(header + "\n")
for item in items: