bbc9e9b9f9
Grundlage fuer reproduzierbare Nachbau-Testfaelle aus echten Projekt- zeichnungen (HundM05). - lib/vf_journal_export.py (neu): liest die Eingabe-Journale der VF_n-Bloecke aus der XDATA (App SSG_VF_EDIT, Marker "linienzug") und schreibt sie als Frage-Antwort-Protokoll im Schema von tests/testdata/linienzug_tests.json - Gegenstueck zu vfl-entry->string, ohne ezdxf (reine Gruppencode-Lesung). Optional --csv fuer Strecken-ID und Erwartungswerte je Kette. - lib/extract_polylines.py (neu): LWPOLYLINE-Objekte als JSON, inkl. OCS->Welt-Umrechnung fuer gekippte Ebenen (Handles, Segment-/Gesamtlaengen). - lib/dxf_scan_components.py: erkennt jetzt zusaetzlich, ob ein Block ein 2D-Schema oder ein 3D-Modell ist (TYPEN_3D, ist_3d_block/darstellung_von) - eine Anlagenzeichnung enthaelt beides. Dazu Sensor-Erfassung, Zwillings-Zusammenfuehrung und Hoehenschaetzung aus der Umgebung. - lib/dxf_abbild.py: gibt die erkannte Darstellung je Element mit aus. - tests/test_vf_journal_grammatik.py (neu): prueft den Journal-Dekoder gegen handgebaute Journale - ohne CAD und ohne die grosse DXF. Deckt die zwei Zweige ab, die in den echten Ketten nicht vorkommen: Glied "Linie-VF" und eine VF-Einheit mit gewinkeltem Erstkoerper (dort fragt vfl-vf-einheit KEINE Separator-/Endpunkt-Fragen vorab, anders als beim horizontalen). - data/polylines.md, tests/testdata/hundm05_linienzug_polylines.json: ausgewertete Streckenzuege der Quellzeichnung. - .gitignore: die Quellzeichnungen selbst (data/polylines.dxf 124 MB, data/polylines.dwg, tests/HM5.dwg) bleiben draussen - eingecheckt ist die Auswertung, nicht die Zeichnung. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
655 lines
28 KiB
Python
655 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
vf_journal_export.py - Liest die SSG_VF_EDIT-Eingabe-Journale der VF_n-Bloecke
|
|
aus einer Zeichnung und schreibt sie als Frage-Antwort-Protokoll (JSON) im
|
|
Schema von tests/testdata/linienzug_tests.json.
|
|
|
|
Hintergrund: Der Linienzug-Modus (Lisp/vf_linienzug.lsp) ist nicht scriptbar -
|
|
er fragt jede Eingabe einzeln ab (getpoint/getstring/getint/getreal) und
|
|
zeichnet sie in einem Journal auf, das nach dem Bau als XDATA (App
|
|
SSG_VF_EDIT, Marker "linienzug") am fertigen VF_n-Block landet. Dieses Journal
|
|
ist die vollstaendige, wiederabspielbare Bau-Anleitung der Kette
|
|
(vfl-journal-replay-start). Dieses Skript dreht die Serialisierung aus
|
|
vfl-entry->string wieder zurueck und annotiert jeden Wert mit der Frage, die
|
|
er beantwortet - abgeleitet aus dem Frageablauf von vf-linienzug-modus.
|
|
|
|
Bewusst OHNE ezdxf: gebraucht werden nur INSERT-Kopf (Blockname, Einfuegepunkt,
|
|
Drehung) und die 1000er-Gruppen der XDATA - das ist reine Gruppencode-Paar-
|
|
Lesung und funktioniert auch bei ASCII-DXF im 100-MB-Bereich.
|
|
|
|
Zwei Eigenheiten der Quelldaten, die hier aufgeloest werden:
|
|
|
|
1. Das Newline zwischen zwei Journal-Eintraegen steht im ASCII-DXF als
|
|
Steuerzeichen-Notation "^J" (zwei Zeichen) - ein echtes 0x0A wuerde die
|
|
zeilenweise DXF-Struktur zerreissen. Getrennt wird darum an "^J".
|
|
2. Kopierte Ketten teilen ihr Journal: die vier VF_46-Bloecke in
|
|
data/polylines.dxf sind Kopien EINER gebauten Kette und tragen deshalb
|
|
Wort fuer Wort dasselbe Journal - inklusive des Startpunkts der zuerst
|
|
gebauten Kette. Alle uebrigen Eintraege sind relativ (Distanz, Winkel,
|
|
Zielhoehen), nur dieser eine Punkt ist absolut. Er wird darum je Instanz
|
|
auf deren echten Blockeinfuegepunkt gezogen (--kein-startpunkt-fix
|
|
schaltet das ab), damit jeder Eintrag fuer sich abspielbar ist.
|
|
|
|
Aufruf:
|
|
python vf_journal_export.py <dxf> <output.json> [--csv <sivas_export.csv>]
|
|
[--kein-startpunkt-fix]
|
|
|
|
Beispiel (erzeugt tests/testdata/hundm05.json):
|
|
python lib/vf_journal_export.py data/polylines.dxf \
|
|
tests/testdata/hundm05.json --csv results/HundM_export.csv
|
|
"""
|
|
|
|
import argparse
|
|
import csv
|
|
import json
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
PREFIX = "[vf_journal_export]"
|
|
|
|
XDATA_APP = "SSG_VF_EDIT"
|
|
MARKER = "linienzug"
|
|
|
|
# Steuerzeichen-Notation des Eintrags-Trenners im ASCII-DXF (siehe Kopf).
|
|
TRENNER = "^J"
|
|
|
|
RUND = 6
|
|
|
|
# Antwort-Klartexte der Menues aus vf_linienzug.lsp (nur zur Annotation).
|
|
JA_NEIN = {"1": "Ja", "2": "Nein"}
|
|
SEITE = {"1": "links", "2": "rechts"}
|
|
VARIANTE = {"1": "aussen", "2": "innen"}
|
|
GF_MODUS = {"1": "ueber Zielhoehe", "2": "ueber Neigungswinkel"}
|
|
ENDPUNKT3 = {"1": "Ja, nur Motorstation", "2": "Nein, weiterbauen",
|
|
"3": "Ja, Motorstation + Kettenende am Zielpunkt"}
|
|
KETTENENDE3 = {"1": "Ja, mit ES-Element", "2": "Ja, ohne ES-Element",
|
|
"3": "Nein, weiterbauen"}
|
|
KETTENENDE2 = {"1": "Ja, Separator + ES-Element", "2": "Nein, weiterbauen"}
|
|
VERTEILUNG = {"1": "halbe Staustrecke am Ausgang", "2": "alles am Einlauf"}
|
|
IM_VF = {"1": "Horizontaler Foerderer", "2": "Vario-Kurve",
|
|
"3": "Auf/Ab-Foerderer"}
|
|
MENU_START = {"1": "Neue Linie: GF", "2": "Neue Linie: Ab/Auf VF",
|
|
"3": "Horizontal-VF", "4": "Neue Linie bis Kettenende"}
|
|
MENU_WEITER = {"1": "GF-Bogen", "2": "Neue Linie: GF",
|
|
"3": "Neue Linie: Ab/Auf VF", "4": "Horizontal-VF",
|
|
"5": "Neue Linie bis Kettenende"}
|
|
|
|
STEP_KOMMENTAR = ("Glied-Marker (vfl-journal-mark) - beim Replay "
|
|
"uebersprungen, Label = gewaehlter Segmenttyp")
|
|
|
|
|
|
# ============================================================
|
|
# DXF lesen
|
|
# ============================================================
|
|
|
|
def gruppen(dxf_pfad):
|
|
"""Alle (Gruppencode, Wert)-Paare der Datei."""
|
|
with open(dxf_pfad, "r", encoding="utf-8", errors="replace") as fh:
|
|
zeilen = fh.read().splitlines()
|
|
return [(zeilen[i].strip(), zeilen[i + 1])
|
|
for i in range(0, len(zeilen) - 1, 2)]
|
|
|
|
|
|
def lese_vf_inserts(dxf_pfad):
|
|
"""INSERTs mit SSG_VF_EDIT-XDATA einsammeln.
|
|
|
|
Rueckgabe: Liste von Dicts (handle, block, punkt, rotation, marker,
|
|
journal_roh). Das Journal steckt in den 1000er-Gruppen NACH dem Marker
|
|
und ist auf 250 Zeichen je Gruppe gestueckelt (DXF-Limit 255).
|
|
"""
|
|
paare = gruppen(dxf_pfad)
|
|
treffer = []
|
|
i = 0
|
|
while i < len(paare):
|
|
code, wert = paare[i]
|
|
if code != "0" or wert != "INSERT":
|
|
i += 1
|
|
continue
|
|
e = {"handle": None, "block": None, "x": 0.0, "y": 0.0, "z": 0.0,
|
|
"rotation": 0.0, "marker": None, "chunks": []}
|
|
in_app = False
|
|
j = i + 1
|
|
while j < len(paare) and paare[j][0] != "0":
|
|
c, v = paare[j]
|
|
if c == "5" and e["handle"] is None:
|
|
e["handle"] = v
|
|
elif c == "2" and e["block"] is None:
|
|
e["block"] = v
|
|
elif c == "10":
|
|
e["x"] = float(v)
|
|
elif c == "20":
|
|
e["y"] = float(v)
|
|
elif c == "30":
|
|
e["z"] = float(v)
|
|
elif c == "50":
|
|
e["rotation"] = float(v)
|
|
elif c == "1001":
|
|
in_app = (v == XDATA_APP)
|
|
elif c == "1000" and in_app:
|
|
if e["marker"] is None:
|
|
e["marker"] = v
|
|
else:
|
|
e["chunks"].append(v)
|
|
j += 1
|
|
if e["marker"] is not None:
|
|
e["punkt"] = [e["x"], e["y"], e["z"]]
|
|
e["journal_roh"] = "".join(e["chunks"])
|
|
del e["chunks"]
|
|
treffer.append(e)
|
|
i = j
|
|
return treffer
|
|
|
|
|
|
# ============================================================
|
|
# Journal dekodieren
|
|
# ============================================================
|
|
|
|
class Journal(object):
|
|
"""Ein Journal (kind:payload je Zeile) in das Frage-Antwort-Protokoll
|
|
uebersetzen. Der Ablauf folgt vf-linienzug-modus (Modus 1):
|
|
|
|
Praeambel: Startpunkt, Starthoehe, AS-Element (Ja/Nein, Winkel, Seite)
|
|
Hauptschleife je Glied: STEP-Marker, Menuewahl, dann die Fragen des
|
|
gewaehlten Segmenttyps
|
|
Innerhalb einer VF-Einheit (Horizontal-VF / Linie): Separator vor/nach,
|
|
"Ist der Endpunkt der Foerderer?", danach Horizontal / Vario-Kurve /
|
|
Auf-Ab bis die Einheit schliesst
|
|
Kettenende: ES-Glied (Winkel, Seite)
|
|
"""
|
|
|
|
def __init__(self, roh):
|
|
self.tok = []
|
|
for zeile in roh.split(TRENNER):
|
|
if zeile:
|
|
art, _, wert = zeile.partition(":")
|
|
self.tok.append((art, wert))
|
|
self.pos = 0
|
|
self.out = []
|
|
self.erster_step = True
|
|
self.erster_dl = True
|
|
|
|
# --- Tokenzugriff ---
|
|
|
|
def art(self):
|
|
return self.tok[self.pos][0] if self.pos < len(self.tok) else None
|
|
|
|
def label(self):
|
|
return self.tok[self.pos][1] if self.pos < len(self.tok) else None
|
|
|
|
def nimm(self, erwartet):
|
|
if self.pos >= len(self.tok):
|
|
raise ValueError("Journal endet vor %s" % erwartet)
|
|
art, wert = self.tok[self.pos]
|
|
if art != erwartet:
|
|
raise ValueError("Token %d: %s erwartet, %s:%s gefunden"
|
|
% (self.pos, erwartet, art, wert))
|
|
self.pos += 1
|
|
return wert
|
|
|
|
# --- Ausgabe ---
|
|
|
|
def add(self, typ, kommentar, **felder):
|
|
eintrag = {"typ": typ}
|
|
eintrag.update(felder)
|
|
eintrag["kommentar"] = kommentar
|
|
self.out.append(eintrag)
|
|
|
|
def frage_str(self, kommentar_fmt, tabelle=None):
|
|
w = self.nimm("STR")
|
|
self.add("string", kommentar_fmt % (tabelle.get(w, "?") if tabelle
|
|
else w), wert=w)
|
|
return w
|
|
|
|
def frage_real(self, kommentar_fmt):
|
|
w = float(self.nimm("REAL"))
|
|
self.add("real", kommentar_fmt % w, wert=w)
|
|
return w
|
|
|
|
def frage_int(self, kommentar_fmt):
|
|
w = int(self.nimm("INT"))
|
|
self.add("int", kommentar_fmt % w, wert=w)
|
|
return w
|
|
|
|
def marker(self, label):
|
|
self.add("step", STEP_KOMMENTAR, wert=label)
|
|
|
|
def frage_linie(self, was):
|
|
"""Segmentlaenge; die gesnappte Fahrtrichtung steht nur beim ersten
|
|
Segment der Kette im Journal (vfl-in-abstand), danach erbt jedes
|
|
Segment die Richtung des Vorgaengers."""
|
|
dl = float(self.nimm("DL"))
|
|
if self.erster_dl and self.art() == "REAL":
|
|
hz = float(self.nimm("REAL"))
|
|
self.erster_dl = False
|
|
self.add("point_rel",
|
|
"getpoint %s: dL=%.1f mm, gesnappte Fahrtrichtung "
|
|
"hz=%.0f Grad (nur beim ersten Segment der Kette "
|
|
"journalisiert)" % (was, dl, hz), dL=dl, hz=hz)
|
|
else:
|
|
self.erster_dl = False
|
|
self.add("point_rel",
|
|
"getpoint %s: dL=%.1f mm (Fahrtrichtung vom "
|
|
"Vorgaenger geerbt)" % (was, dl), dL=dl)
|
|
return dl
|
|
|
|
# --- Ablauf ---
|
|
|
|
def dekodiere(self):
|
|
self.praeambel()
|
|
while self.pos < len(self.tok):
|
|
label = self.nimm("STEP")
|
|
self.marker(label)
|
|
if label == "ES":
|
|
self.glied_es(marker_gelesen=True)
|
|
continue
|
|
if self.erster_step:
|
|
tabelle, kopf = MENU_START, (
|
|
"Naechstes Element am Kettenanfang (1=Neue Linie GF, "
|
|
"2=Neue Linie Ab/Auf VF, 3=Horizontal-VF, 4=Neue Linie "
|
|
"bis Kettenende) -> %s")
|
|
else:
|
|
tabelle, kopf = MENU_WEITER, (
|
|
"Naechstes Element (1=GF-Bogen, 2=Neue Linie GF, "
|
|
"3=Neue Linie Ab/Auf VF, 4=Horizontal-VF, 5=Neue Linie "
|
|
"bis Kettenende) -> %s")
|
|
self.frage_str("getstring " + kopf, tabelle)
|
|
self.erster_step = False
|
|
if label == "Linie-GF":
|
|
self.glied_linie_gf()
|
|
elif label == "Linie-VF":
|
|
self.glied_linie_vf()
|
|
elif label == "Horizontal-VF":
|
|
self.glied_horizontal_vf()
|
|
elif label == "Linie":
|
|
self.glied_linie_kettenende()
|
|
elif label == "GF-Bogen":
|
|
self.frage_int("getint GF-Bogen Winkel (30/60/90 Grad) "
|
|
"-> %d")
|
|
self.frage_str("getstring GF-Bogen Seite (1=links, "
|
|
"2=rechts) -> %s", SEITE)
|
|
else:
|
|
raise ValueError("unbekanntes Glied: %s" % label)
|
|
if self.pos != len(self.tok):
|
|
raise ValueError("nicht alle Tokens verbraucht (%d von %d)"
|
|
% (self.pos, len(self.tok)))
|
|
return self.out
|
|
|
|
def praeambel(self):
|
|
punkt = [float(v) for v in self.nimm("PT").split(",")]
|
|
self.add("point_abs", "getpoint Startpunkt der Kette (absoluter "
|
|
"Weltpunkt, einziger Anker der ganzen Kette)", wert=punkt)
|
|
self.frage_real("getreal Hoehe (Z) Startpunkt -> %.1f mm")
|
|
if self.frage_str("getstring AS-Element setzen? (1=Ja, 2=Nein) "
|
|
"-> %s", JA_NEIN) == "1":
|
|
w = self.nimm("STR")
|
|
self.add("string", "getstring AS-Element Winkel (Wert direkt, "
|
|
"90 oder 30 Grad) -> %s Grad" % w, wert=w)
|
|
self.frage_str("getstring AS-Seite (1=links, 2=rechts) -> %s",
|
|
SEITE)
|
|
|
|
def glied_linie_gf(self):
|
|
self.frage_linie("Endpunkt der GF-Linie")
|
|
if self.frage_str("getstring Gefaelle festlegen (1=ueber Zielhoehe, "
|
|
"2=ueber Neigungswinkel) -> %s", GF_MODUS) == "2":
|
|
self.frage_real("getreal Neigungswinkel -> %.1f Grad")
|
|
else:
|
|
self.frage_real("getreal Zielhoehe (Z) Linienendpunkt "
|
|
"-> %.1f mm")
|
|
if self.frage_str("getstring Ist das Kettenende? (1=Ja mit ES, "
|
|
"2=Ja ohne ES, 3=Nein weiterbauen) -> %s",
|
|
KETTENENDE3) == "1":
|
|
self.glied_es()
|
|
|
|
def glied_horizontal_vf(self):
|
|
"""VF-Einheit mit HORIZONTALEM Anfangskoerper (winkel1=0 in
|
|
vfl-vf-einheit): dort stellt vfl-baue-horizontal-koerper im
|
|
Ziel-Modus die Separator-/Endpunkt-Fragen VOR der
|
|
Laengenberechnung."""
|
|
self.frage_linie("Endpunkt des horizontalen VF-Stuecks")
|
|
self.vf_einheit(auto_ende=False, erster_horizontal=True)
|
|
|
|
def glied_linie_vf(self):
|
|
""""Neue Linie: Ab/Auf VF" - Laenge, Zielhoehe, ggf. Winkelwahl
|
|
(vfl-vf-entscheidung), dann eine VF-Einheit mit GEWINKELTEM
|
|
Anfangskoerper. Zu dem Koerper wird nichts gefragt (vfl-vf-einheit
|
|
baut ihn direkt), die erste Frage der Einheit ist also die
|
|
Endpunkt-Frage der Fortsetzungsschleife."""
|
|
self.frage_linie("Endpunkt des Ab/Auf-VF-Segments")
|
|
self.frage_real("getreal Zielhoehe (Z) Linienendpunkt -> %.1f mm")
|
|
self.frage_winkelwahl()
|
|
self.vf_einheit(auto_ende=False, erster_horizontal=False)
|
|
|
|
def glied_linie_kettenende(self):
|
|
""""Neue Linie bis Kettenende": Laenge + Zielhoehe, danach entscheidet
|
|
vfl-segment-entscheidung selbst. Reine Gefaellestrecke -> ES-Element
|
|
ohne weitere Frage; VF-Einheit -> vf_einheit mit auto-ende."""
|
|
self.frage_linie("Endpunkt der Linie bis Kettenende (Ziel-XY der "
|
|
"Kette)")
|
|
self.frage_real("getreal Zielhoehe (Z) am Kettenende -> %.1f mm")
|
|
self.frage_winkelwahl()
|
|
if self.art() == "STEP" and self.label() == "ES":
|
|
self.glied_es()
|
|
elif self.art() == "STR":
|
|
# vfl-segment-entscheidung liefert fuer typ="VF" immer einen
|
|
# Winkel > 0 (die VF-Winkelstufen beginnen bei 3 Grad), der erste
|
|
# Koerper ist hier also nie horizontal.
|
|
self.vf_einheit(auto_ende=True, erster_horizontal=False)
|
|
|
|
def glied_es(self, marker_gelesen=False):
|
|
if not marker_gelesen:
|
|
label = self.nimm("STEP")
|
|
if label != "ES":
|
|
raise ValueError("ES-Marker erwartet, %s gefunden" % label)
|
|
self.marker(label)
|
|
w = self.nimm("STR")
|
|
self.add("string", "getstring ES-Element Winkel (Wert direkt, 90 "
|
|
"oder 30 Grad) -> %s Grad" % w, wert=w)
|
|
self.frage_str("getstring ES-Seite (1=links, 2=rechts) -> %s", SEITE)
|
|
|
|
def frage_winkelwahl(self):
|
|
"""Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn mehrere
|
|
Kandidatenwinkel gueltig waren - bei genau einem faellt die Frage
|
|
weg."""
|
|
if self.art() == "INT":
|
|
self.frage_int("getint Winkelwahl aus den gueltigen "
|
|
"Kandidaten (vfl-waehle-winkel) -> Option %d")
|
|
|
|
def horizontal_koerper(self):
|
|
"""vfl-baue-horizontal-koerper im Ziel-Modus: Separator vor/nach und
|
|
die Endpunkt-Frage stehen VOR der Laengenberechnung. Rueckgabe: die
|
|
Endpunkt-Antwort, die die Fortsetzungsschleife weiterverwendet."""
|
|
self.frage_str("getstring Separator VOR dem horizontalen Stueck? "
|
|
"(1=Ja, 2=Nein) -> %s", JA_NEIN)
|
|
self.frage_str("getstring Separator NACH dem horizontalen Stueck? "
|
|
"(1=Ja, 2=Nein) -> %s", JA_NEIN)
|
|
return self.frage_str("getstring Ist der Endpunkt der Foerderer? "
|
|
"(1=Ja nur Motorstation, 2=Nein weiterbauen, "
|
|
"3=Ja Motorstation + Kettenende) -> %s",
|
|
ENDPUNKT3)
|
|
|
|
def vario_kurve(self):
|
|
label = self.nimm("STEP")
|
|
if label != "Vario-Kurve":
|
|
raise ValueError("Vario-Kurve-Marker erwartet, %s" % label)
|
|
self.marker(label)
|
|
self.frage_int("getint Vario-Kurve Winkel (30/60/90 Grad, Wert "
|
|
"direkt) -> %d Grad")
|
|
self.frage_str("getstring Vario-Kurve Seite (1=links, 2=rechts) "
|
|
"-> %s", SEITE)
|
|
self.frage_str("getstring Vario-Kurve Variante (1=aussen, 2=innen) "
|
|
"-> %s", VARIANTE)
|
|
|
|
def vf_einheit(self, auto_ende, erster_horizontal):
|
|
"""Eine VF-Einheit: GF-Verteilung, erster Koerper, dann die
|
|
Fortsetzungsschleife (vfl-vf-einheit) bis Motorstation/Kettenende,
|
|
danach der Abschluss (vfl-vf-einheit-abschluss).
|
|
|
|
erster_horizontal spiegelt die Verzweigung ueber winkel1 in
|
|
vfl-vf-einheit: nur der horizontale Erstkoerper laeuft durch
|
|
vfl-baue-horizontal-koerper und fragt darum Separator vor/nach und
|
|
die Endpunkt-Frage schon vorab (deren Antwort die Schleife dann
|
|
uebernimmt, statt erneut zu fragen). Ein gewinkelter Erstkoerper
|
|
wird ohne jede Frage gebaut."""
|
|
self.frage_str("getstring GF-Verteilung (1=halbe Staustrecke am "
|
|
"Ausgang, 2=alles am Einlauf) -> %s", VERTEILUNG)
|
|
vorantwort = self.horizontal_koerper() if erster_horizontal else None
|
|
ziel_ende = False
|
|
es_gewuenscht = True
|
|
while True:
|
|
if vorantwort is None:
|
|
antwort = self.frage_str(
|
|
"getstring Ist der Endpunkt der Foerderer? (1=Ja nur "
|
|
"Motorstation, 2=Nein weiterbauen, 3=Ja Motorstation + "
|
|
"Kettenende) -> %s", ENDPUNKT3)
|
|
else:
|
|
antwort, vorantwort = vorantwort, None
|
|
if antwort == "1":
|
|
break
|
|
if antwort == "3":
|
|
es_gewuenscht = self.frage_str(
|
|
"getstring ES-Element setzen? (1=Ja, 2=Nein - Kette "
|
|
"endet direkt am Zielpunkt) -> %s", JA_NEIN) != "2"
|
|
self.frage_linie("Zielpunkt des Kettenendes")
|
|
self.frage_real("getreal Zielhoehe (Z) am Kettenende "
|
|
"-> %.1f mm")
|
|
self.frage_winkelwahl()
|
|
ziel_ende = True
|
|
break
|
|
wahl = self.frage_str("getstring Naechstes in der VF-Einheit "
|
|
"(1=Horizontaler Foerderer, "
|
|
"2=Vario-Kurve, 3=Auf/Ab-Foerderer) "
|
|
"-> %s", IM_VF)
|
|
if wahl == "1":
|
|
self.frage_linie("Endpunkt des horizontalen VF-Stuecks")
|
|
vorantwort = self.horizontal_koerper()
|
|
elif wahl == "2":
|
|
self.vario_kurve()
|
|
else:
|
|
self.frage_linie("Endpunkt des Auf/Ab-VF-Segments")
|
|
self.frage_real("getreal Zielhoehe (Z) am Segmentende "
|
|
"-> %.1f mm")
|
|
self.frage_winkelwahl()
|
|
if ziel_ende and not es_gewuenscht:
|
|
return
|
|
if auto_ende or ziel_ende:
|
|
self.glied_es()
|
|
return
|
|
if self.frage_str("getstring Ist das Kettenende? (1=Ja, Separator + "
|
|
"ES-Element, 2=Nein, weiterbauen) -> %s",
|
|
KETTENENDE2) == "1":
|
|
self.glied_es()
|
|
else:
|
|
self.frage_str("getstring Separator an dieser Stelle einfuegen? "
|
|
"(1=Ja, 2=Nein) -> %s", JA_NEIN)
|
|
|
|
|
|
# ============================================================
|
|
# Sivas-Export als Quervergleich
|
|
# ============================================================
|
|
|
|
def lese_strecken(csv_pfad):
|
|
"""Die "ILS 2.0 Strecke"-Zeilen eines Sivas-Exports (EXPORTSIVAS).
|
|
|
|
Dient nur der Zuordnung/Dokumentation: die Merkmale (DeltaL, DeltaH,
|
|
L_GF_m, L_VF_m, ...) sind die Attribute des fertigen VF_n-Blocks und
|
|
damit der Erwartungswert zu einem nachgebauten Foerderer.
|
|
"""
|
|
strecken = []
|
|
with open(csv_pfad, encoding="utf-8") as fh:
|
|
for zeile in csv.DictReader(fh, delimiter=";"):
|
|
if zeile.get("TeileArt") != "ILS 2.0 Strecke":
|
|
continue
|
|
if not zeile.get("Nachbarn"):
|
|
continue # unverbunden - gehoert zu keinem Kreisel
|
|
strecken.append({
|
|
"id": zeile["TeileId"],
|
|
"bezeichnung": zeile["Bezeichnung"],
|
|
"planquadrat": zeile["Planquadrat"],
|
|
"x": float(zeile["Position"].split(",")[0]),
|
|
"merkmale": json.loads(zeile["Merkmale"]),
|
|
})
|
|
return strecken
|
|
|
|
|
|
def erwartung_hinweis(strecke):
|
|
m = strecke["merkmale"]
|
|
felder = ["DeltaL_mm", "DeltaH_mm", "Hoehe_Von_mm", "Hoehe_Bis_mm",
|
|
"L_GF_m", "L_VF_m", "VF_Winkel", "Anzahl_Separator"]
|
|
werte = ", ".join("%s=%s" % (f, m[f]) for f in felder if f in m)
|
|
return ("Sivas-Export (EXPORTSIVAS), Strecke %s: %s"
|
|
% (strecke["id"], werte))
|
|
|
|
|
|
# ============================================================
|
|
# Kurzbeschreibung der Kette
|
|
# ============================================================
|
|
|
|
def kurzbeschreibung(eintraege):
|
|
"""Kette in einer Zeile: die Glied-Marker mit ihren Kernwerten."""
|
|
teile = []
|
|
for i, e in enumerate(eintraege):
|
|
if e["typ"] != "step":
|
|
continue
|
|
folge = eintraege[i + 1:i + 8]
|
|
strings = [x["wert"] for x in folge if x["typ"] == "string"]
|
|
ints = [x["wert"] for x in folge if x["typ"] == "int"]
|
|
reals = [x["wert"] for x in folge if x["typ"] == "real"]
|
|
laengen = [x["dL"] for x in folge if x["typ"] == "point_rel"]
|
|
label = e["wert"]
|
|
if label == "Vario-Kurve":
|
|
teile.append("Vario-Kurve %d %s %s"
|
|
% (ints[0], SEITE.get(strings[0], "?"),
|
|
VARIANTE.get(strings[1], "?")))
|
|
elif label == "ES":
|
|
teile.append("ES %s %s" % (strings[0], SEITE.get(strings[1], "?")))
|
|
elif label == "Linie-GF":
|
|
teile.append("GF-Linie %.0f mm / %.0f Grad"
|
|
% (laengen[0], reals[0]))
|
|
elif label == "Horizontal-VF":
|
|
teile.append("Horizontal-VF %.0f mm" % laengen[0])
|
|
elif label == "Linie":
|
|
teile.append("Linie bis Kettenende %.0f mm auf Z%.0f"
|
|
% (laengen[0], reals[0]))
|
|
elif label == "GF-Bogen":
|
|
teile.append("GF-Bogen %d %s" % (ints[0],
|
|
SEITE.get(strings[0], "?")))
|
|
return " -> ".join(teile)
|
|
|
|
|
|
# ============================================================
|
|
# Testfaelle bauen
|
|
# ============================================================
|
|
|
|
def baue_testfaelle(inserts, strecken, startpunkt_fix, quelle_datei,
|
|
ziel_datei):
|
|
"""FLACHE Testfall-Liste: je Kette ein Kopf-Objekt (traegt "test_id"),
|
|
danach ihre Eingaben (tragen "typ") - Struktur wie linienzug_tests.json.
|
|
|
|
Bewusst NICHT verschachtelt ("eingaben": [...]): ssg-load-json /
|
|
ssg-parse-json-array (Lisp/ssg_core.lsp) liest die Datei ZEILENWEISE und
|
|
behandelt jedes "{" als Objektanfang. Ein verschachteltes Array wuerde die
|
|
Kopf-Objekte verschlucken. Der Treiber gruppiert wieder ueber "test_id"
|
|
(siehe hundm05:gruppiere in tests/test_hundm05.lsp).
|
|
"""
|
|
# Anlagenkuerzel der test_id kommt aus dem Namen der Ziel-JSON (die
|
|
# Testfall-Sammlung), nicht aus dem Zeichnungsnamen: tests/testdata/
|
|
# hundm05.json -> VF_hundm05_LZ_01 .. _nn.
|
|
praefix = os.path.splitext(os.path.basename(ziel_datei))[0]
|
|
faelle = []
|
|
for nr, e in enumerate(sorted(inserts, key=lambda i: i["x"]), start=1):
|
|
eintraege = Journal(e["journal_roh"]).dekodiere()
|
|
journal_start = list(eintraege[0]["wert"])
|
|
punkt = [round(v, RUND) for v in e["punkt"]]
|
|
if startpunkt_fix:
|
|
eintraege[0]["wert"] = punkt
|
|
eintraege[1]["wert"] = punkt[2]
|
|
eintraege[1]["kommentar"] = ("getreal Hoehe (Z) Startpunkt "
|
|
"-> %.1f mm" % punkt[2])
|
|
else:
|
|
punkt = [round(v, RUND) for v in journal_start]
|
|
strecke = None
|
|
if strecken:
|
|
strecke = min(strecken, key=lambda s: abs(s["x"] - e["x"]))
|
|
kopf = {
|
|
"test_id": "VF_%s_LZ_%02d" % (praefix, nr),
|
|
# "anlagetyp", nicht "typ": "typ" markiert im flachen Array einen
|
|
# EINGABE-Eintrag (point_abs/real/string/int/step) - ein Kopf mit
|
|
# "typ" waere davon nicht zu unterscheiden.
|
|
"anlagetyp": "linienzug",
|
|
"modus": 1,
|
|
"anzahl_eingaben": len(eintraege),
|
|
"block": e["block"],
|
|
"dxf_handle": e["handle"],
|
|
"startpunkt_mm": punkt,
|
|
"rotation_grad": round(e["rotation"], 4),
|
|
"beschreibung": "Linienzug (Modus 1), Start Z%.0f: %s"
|
|
% (punkt[2], kurzbeschreibung(eintraege)),
|
|
}
|
|
if strecke:
|
|
kopf["sivas_id"] = strecke["id"]
|
|
kopf["sivas_bezeichnung"] = strecke["bezeichnung"]
|
|
kopf["planquadrat"] = strecke["planquadrat"]
|
|
kopf["erwartung_hinweis"] = erwartung_hinweis(strecke)
|
|
# Ohne Anfuehrungszeichen im Text: ssg-cfg-parse-value schneidet einen
|
|
# String am ersten/letzten Zeichen ab und kennt keine Escapes.
|
|
kopf["quelle"] = ("INSERT %s (Block %s) in %s, XDATA-App %s, Marker "
|
|
"%s; im Journal aufgezeichneter Kettenstart: [%s]"
|
|
% (e["handle"], e["block"],
|
|
os.path.basename(quelle_datei), XDATA_APP,
|
|
e["marker"],
|
|
", ".join("%.6f" % v for v in journal_start)))
|
|
faelle.append(kopf)
|
|
faelle.extend(eintraege)
|
|
return faelle
|
|
|
|
|
|
def arrays_einzeilig(text):
|
|
"""Rein numerische JSON-Arrays auf eine Zeile ziehen (wie
|
|
dxf_scan_components.py) - ssg-parse-json-array (Lisp/ssg_core.lsp) liest
|
|
zeilenweise und kann ein umgebrochenes Zahlen-Array nicht lesen."""
|
|
def zusammen(m):
|
|
return "[" + " ".join(m.group(1).split()) + "]"
|
|
return re.sub(r"\[([\s\d.,+\-eE]*?)\]", zusammen, text)
|
|
|
|
|
|
def main():
|
|
p = argparse.ArgumentParser(
|
|
description="Liest die SSG_VF_EDIT-Eingabe-Journale der VF_n-Bloecke "
|
|
"einer Zeichnung und schreibt sie als Frage-Antwort-"
|
|
"Protokoll (Schema linienzug_tests.json).")
|
|
p.add_argument("dxf", help="Quellzeichnung (ASCII-DXF)")
|
|
p.add_argument("output", help="Ziel-JSON")
|
|
p.add_argument("--csv", help="Sivas-Export (EXPORTSIVAS) der gleichen "
|
|
"Zeichnung - liefert Strecken-ID und "
|
|
"Erwartungswerte je Kette")
|
|
p.add_argument("--kein-startpunkt-fix", action="store_true",
|
|
help="Kettenstart NICHT auf den Blockeinfuegepunkt "
|
|
"ziehen (Journal unveraendert uebernehmen)")
|
|
args = p.parse_args()
|
|
|
|
if not os.path.isfile(args.dxf):
|
|
print("%s FEHLER: DXF nicht gefunden: %s" % (PREFIX, args.dxf))
|
|
sys.exit(1)
|
|
|
|
mb = os.path.getsize(args.dxf) / (1024.0 * 1024.0)
|
|
print("%s lese %s (%.1f MB) ..." % (PREFIX, args.dxf, mb))
|
|
inserts = lese_vf_inserts(args.dxf)
|
|
print("%s %d INSERT(s) mit %s-XDATA gefunden"
|
|
% (PREFIX, len(inserts), XDATA_APP))
|
|
inserts = [e for e in inserts if e["marker"].startswith(MARKER)]
|
|
if not inserts:
|
|
print("%s FEHLER: kein Linienzug-Journal (Marker \"%s\") in der "
|
|
"Zeichnung" % (PREFIX, MARKER))
|
|
sys.exit(1)
|
|
|
|
strecken = lese_strecken(args.csv) if args.csv else []
|
|
if args.csv:
|
|
print("%s %d verbundene Strecken im Sivas-Export"
|
|
% (PREFIX, len(strecken)))
|
|
|
|
faelle = baue_testfaelle(inserts, strecken,
|
|
not args.kein_startpunkt_fix, args.dxf,
|
|
args.output)
|
|
koepfe = [f for f in faelle if "test_id" in f]
|
|
for f in koepfe:
|
|
print("%s %s %s (%s) %d Eingaben"
|
|
% (PREFIX, f["test_id"], f["block"],
|
|
f.get("sivas_bezeichnung", "ohne Sivas-Zuordnung"),
|
|
f["anzahl_eingaben"]))
|
|
|
|
ziel_dir = os.path.dirname(os.path.abspath(args.output))
|
|
if ziel_dir and not os.path.isdir(ziel_dir):
|
|
os.makedirs(ziel_dir)
|
|
with open(args.output, "w", encoding="utf-8") as fh:
|
|
fh.write(arrays_einzeilig(json.dumps(faelle, ensure_ascii=False,
|
|
indent=2)))
|
|
fh.write("\n")
|
|
print("%s %d Ketten / %d JSON-Objekte geschrieben -> %s"
|
|
% (PREFIX, len(koepfe), len(faelle), args.output))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|