CSV-Export: Bounding-Box (Position/Boundingbox) und Planquadrat-Spalte ergaenzt

Nur EXPORTCSV betroffen, EXPORTSIVAS bleibt unveraendert:
- Position/Boundingbox: Bounding-Box je Block per vla-getboundingbox
  (Lisp/export.lsp, csv:get-bbox), Mittelpunkt und Ausdehnung x/y/z.
- Planquadrat: aus x/y-Koordinate berechnet nach cfg/export.cfg
  [Planquadrate] (Schrittweite, numerisch/alphabetisch je Achse,
  alphabetische Zaehlung mit Excel-Spalten-Umlauf Z->AA), siehe
  lib/export_planquadrat.py.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 12:49:25 +02:00
parent a0fd5d83bd
commit 193b0dea04
7 changed files with 367 additions and 127 deletions
+70 -2
View File
@@ -11,7 +11,16 @@ Aufruf:
python export_csv.py <export_raw.json> <data_dir> <output.csv>
CSV-Format:
Elementnummer;TeileArt;TeileId;NachbarIds;Bezeichnung;Planquadrat;rotation;Merkmale
Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;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
Planquadrat wird aus der x/y-Koordinate des Blocks berechnet, siehe
export_planquadrat.py ([Planquadrate] in cfg/export.cfg).
Position, Boundingbox und Planquadrat sind nur fuer EXPORTCSV vorhanden
(nicht EXPORTSIVAS) - siehe csv:run-export.
"""
import json
@@ -19,8 +28,10 @@ import sys
import os
from export_blockpatterns import load_patterns, matches_any
from export_planquadrat import load_planquadrat_config, compute_planquadrat
BLOCKPATTERNS = load_patterns()
PLANQUADRAT_CFG = load_planquadrat_config()
# ---------------------------------------------------------------------------
@@ -219,6 +230,41 @@ def build_kreisel_merkmale(block):
}
# ---------------------------------------------------------------------------
# Bounding-Box-Spalten (Position/Boundingbox)
# ---------------------------------------------------------------------------
def bbox_columns(block):
"""Liest die von export.lsp (csv:get-bbox) ermittelte Bounding-Box.
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).
"""
bbox = block.get("bbox")
if not bbox:
return {"position": "", "boundingbox": ""}
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}',
}
def planquadrat_column(block):
"""Berechnet das Planquadrat aus x/y des Blocks (siehe export_planquadrat.py).
Leerer String, wenn keine Planquadrat-Konfiguration geladen werden konnte
(fehlende/unvollstaendige [Planquadrate]-Sektion in cfg/export.cfg).
"""
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)
# ---------------------------------------------------------------------------
# Bekannte Blocknamen
# ---------------------------------------------------------------------------
@@ -268,7 +314,9 @@ def process_blocks(blocks, lookup):
"teileart": "Omniflo Kurve",
"teileid": shape_id,
"bezeichnung": f"OFBogen :{bogen_count}",
"planquadrat": planquadrat_column(block),
"merkmale": build_bogen_merkmale(block, eintrag),
**bbox_columns(block),
})
elif typ == "weiche":
@@ -279,7 +327,9 @@ def process_blocks(blocks, lookup):
"teileart": "Omniflo Weiche",
"teileid": shape_id,
"bezeichnung": f"OFWeiche :{weiche_count[wt]}",
"planquadrat": planquadrat_column(block),
"merkmale": build_weiche_merkmale(block, eintrag),
**bbox_columns(block),
})
subtyp = weichensubtyp(block, eintrag)
if subtyp == "weichenkoerper":
@@ -304,7 +354,9 @@ def process_blocks(blocks, lookup):
"teileart": "Omniflo Gerade",
"teileid": block.get("attribs", {}).get("ID", "0000"),
"bezeichnung": f"OFGerade :{gerade_count}",
"planquadrat": planquadrat_column(block),
"merkmale": build_gerade_merkmale(block),
**bbox_columns(block),
})
len_ap110_mm += get_laenge_mm(block)
continue
@@ -318,7 +370,9 @@ 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),
"merkmale": build_variofoerderer_merkmale(block),
**bbox_columns(block),
})
continue
@@ -331,7 +385,9 @@ 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),
"merkmale": build_variofoerderer_merkmale(block),
**bbox_columns(block),
})
continue
@@ -345,7 +401,9 @@ 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),
"merkmale": build_kreisel_merkmale(block),
**bbox_columns(block),
})
continue
@@ -358,7 +416,9 @@ 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),
"merkmale": build_kreisel_merkmale(block),
**bbox_columns(block),
})
continue
@@ -371,7 +431,9 @@ 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),
"merkmale": {"Blockname": bname},
**bbox_columns(block),
})
continue
@@ -382,9 +444,12 @@ def process_blocks(blocks, lookup):
"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": "",
})
return items
@@ -401,7 +466,10 @@ def format_csv_line(item):
f';"{item["teileart"]}"'
f';"{item["teileid"]}"'
f';"{item["bezeichnung"]}"'
f';"{item["planquadrat"]}"'
f';1'
f';"{item["position"]}"'
f';"{item["boundingbox"]}"'
f';{merkmale_json}'
)
@@ -433,7 +501,7 @@ def main():
items = process_blocks(blocks, lookup)
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Anzahl;Merkmale"
header = "Elementnummer;TeileArt;TeileId;Bezeichnung;Planquadrat;Anzahl;Position;Boundingbox;Merkmale"
with open(output_csv, "w", encoding="utf-8") as f:
f.write(header + "\n")
for item in items:
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
export_planquadrat.py - Berechnet das Planquadrat eines Blocks aus x/y-Koordinate.
Liest die aktive Planquadrat-Konfiguration aus cfg/export.cfg
([Planquadrate] -> section=<Name der Sub-Sektion>, z.B. Planquadrate_AN) und
rundet die x/y-Koordinate eines Blocks (mm, wie im Roh-JSON von export.lsp)
je Achse auf die konfigurierte Schrittweite (in Metern) herunter.
Nur von export_csv.py genutzt (EXPORTCSV), nicht von export_sivas.py.
"""
import configparser
from export_blockpatterns import cfg_path_from_env
def load_planquadrat_config(cfg_path=None):
"""Liest die aktive Planquadrat-Konfiguration aus export.cfg.
Rueckgabe: dict mit x_step_mm, x_alpha, y_step_mm, y_alpha, format,
oder None, wenn [Planquadrate] bzw. die referenzierte Sub-Sektion fehlt.
"""
if cfg_path is None:
cfg_path = cfg_path_from_env()
# inline_comment_prefixes noetig, da export.cfg Werte wie
# "numerisch # zahlen, von links nach rechts, 2m pro Spalte" nutzt.
parser = configparser.ConfigParser(inline_comment_prefixes=("#",))
parser.read(cfg_path, encoding="utf-8")
if not parser.has_section("Planquadrate"):
return None
section_name = parser.get("Planquadrate", "section", fallback=None)
if not section_name or not parser.has_section(section_name):
return None
sec = parser[section_name]
return {
"x_step_mm": float(sec.get("x_step", "1")) * 1000.0,
"x_alpha": sec.get("x_description", "numerisch").strip().lower().startswith("alpha"),
"y_step_mm": float(sec.get("y_step", "1")) * 1000.0,
"y_alpha": sec.get("y_description", "numerisch").strip().lower().startswith("alpha"),
"format": sec.get("format", "{x}/{y}").strip(),
}
def _index_to_alpha(n):
"""Wandelt einen 1-basierten Index in eine Buchstaben-Bezeichnung um.
1=A, 26=Z, 27=AA, 28=AB, ... (wie Excel-Spaltennamen).
"""
result = ""
while n > 0:
n, remainder = divmod(n - 1, 26)
result = chr(65 + remainder) + result
return result
def _index_for(coord_mm, step_mm):
"""1-basierter Planquadrat-Index einer Koordinate fuer eine Schrittweite."""
return int(coord_mm // step_mm) + 1
def compute_planquadrat(x_mm, y_mm, cfg):
"""Berechnet die Planquadrat-Bezeichnung fuer eine x/y-Koordinate (mm)."""
x_idx = _index_for(x_mm, cfg["x_step_mm"])
y_idx = _index_for(y_mm, cfg["y_step_mm"])
x_label = _index_to_alpha(x_idx) if cfg["x_alpha"] else str(x_idx)
y_label = _index_to_alpha(y_idx) if cfg["y_alpha"] else str(y_idx)
return cfg["format"].format(x=x_label, y=y_label)