Lisp (vf_linienzug.lsp/vf_spec.lsp): - vsp-eingang-hoehe unterstuetzt jetzt "auto" (NIL-Token statt REAL) - bewusst keine Zahl vorgeben, der Bauablauf berechnet seinen eigenen Vorschlag, wie bei leerer Konsoleneingabe - vfl-journal-reset-lauf: internes Reset innerhalb von vf-linienzug-modus/ -modus2 loescht *vfl-replay-queue*/*vflw-pending* nicht mehr mit (Bug: REPLAY-getriebener Bau (vsp-bau-aus-spec) brach mitten in der Kette ab, weil eine noch unverbrauchte Queue verworfen wurde) Python-Gegenstueck nachgezogen (lib/vf_linienzug_grammatik.py, lib/vf_spec_export.py, lib/vf_journal_export.py): - NIL-Token-Unterstuetzung fuer eingang_hoehe="auto" im gemeinsamen Parser-Kern, im Schreiber (SpecSchreiber) und in der Protokoll-Annotation - gf_verteilung ist im Schreiber jetzt an der Feldpraesenz in der Spec festgemacht statt an einem Grammatik-Flag (selbstbeschreibend, deckt Specs mit und ohne diese Frage ab, ohne dass der Aufrufer die Grammatik kennen muss) - Rundlauf-Pruefung (main(), test_verlustfrei) toleriert jetzt die bekannte, schon dokumentierte Erweiterung um drei neue Linie-GF-Pflichtfragen (ist_teilfolge statt exakter Gleichheit) - test_gleiche_journale_wie_protokoll: alle 5 Ketten in hundm05.json weichen inzwischen vom Rohprotokoll ab (Kette 1 bewusst wegen eingang_hoehe, Ketten 2-5 schon laenger wegen Handkorrektur gegen echte DXF-Positionen) - expliziter Skip mit Begruendung statt leerem Loop tests/testdata/hundm05.json: - mm-Werte (dl, Start-/Stuetzpunkte, Koordinaten der Nachtrag-Objekte) auf eine Nachkommastelle gerundet - nicht abgefragte Kopf-Metadaten entfernt (beschreibung, sivas_id, sivas_bezeichnung, planquadrat, block, dxf_handle) - nur quelle bleibt Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
618 lines
28 KiB
Python
618 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
vf_linienzug_grammatik.py - die EINE Grammatik des VF-Linienzug-Journals und
|
|
der EINE Parser-Kern, der sie laeuft.
|
|
|
|
Warum es diese Datei gibt
|
|
-------------------------
|
|
Die Frage-Reihenfolge des Linienzug-Bau-Ablaufs (Lisp/vf_linienzug.lsp, Modus
|
|
1) wurde frueher in ZWEI Python-Dateien unabhaengig nachgebaut:
|
|
|
|
- lib/vf_journal_export.py (Klasse Journal) - erzeugt daraus ein
|
|
annotiertes Frage-Antwort-PROTOKOLL,
|
|
- lib/vf_spec_export.py (Klasse SpecLeser) - erzeugt daraus eine SPEC in
|
|
Domaenenwerten.
|
|
|
|
Beide liefen denselben Ablauf ab und hielten dieselben Code-Tabellen doppelt.
|
|
Eine Grammatik-Aenderung wurde darum regelmaessig nur in einer der beiden
|
|
Dateien nachgezogen (realer Fall: das optionale Feld eingang_hoehe am
|
|
Horizontal-VF und die Position der ES-Untermarke im kettenende-Zweig landeten
|
|
nur in vf_spec_export.py - vf_journal_export.py konnte eine Aufzeichnung der
|
|
aktuellen Grammatik danach nicht mehr lesen).
|
|
|
|
Hier steht der Ablauf jetzt EINMAL. Was die beiden Ausgaben unterscheidet,
|
|
steckt allein in ihrer Sink-Implementierung: der Kern meldet an jeder
|
|
Entscheidungsstelle einen selbstbeschreibenden Wert, die Sink legt ihn ab
|
|
(Spec) oder schreibt eine Protokollzeile dazu (Aufzeichnung).
|
|
|
|
lib/vf_spec_export.py SpecSink -> Spec (OrderedDict)
|
|
lib/vf_journal_export.py ProtokollSink -> annotiertes Protokoll (Liste)
|
|
|
|
Die Gegenstuecke in AutoLISP (vsp-journal/vsp-sektion/vsp-vf-einheit in
|
|
Lisp/vf_spec.lsp, der lebende Ablauf in Lisp/vf_linienzug.lsp) lassen sich
|
|
nicht mitbenutzen (andere Sprache) - sie bleiben die zweite, unabhaengige
|
|
Niederschrift und werden gegen diese hier geprueft
|
|
(tests/test_vf_spec.py, tests/test_vf_spec.lsp).
|
|
|
|
Token-Form
|
|
----------
|
|
Token = (Art, Wert-String) genau wie im Journal auf der XDATA:
|
|
|
|
PT Startpunkt "x,y,z" DL Segmentlaenge
|
|
REAL Realzahl INT Ganzzahl
|
|
STR Menue-/Wertantwort STEP Glied-Marker (Label = Segmenttyp)
|
|
NIL bewusst leere Antwort (z.B. eingang_hoehe="auto", siehe
|
|
_opt_eingang_hoehe) - der Bauablauf berechnet dann seinen eigenen
|
|
Vorschlag, wie bei einer leeren Konsoleneingabe
|
|
"""
|
|
|
|
# ============================================================
|
|
# Code-Tabellen: Menue-Antwort <-> Domaenenwert
|
|
# ============================================================
|
|
# Links der Journal-Code, rechts der Wert in der Spec. Die Spec kennt KEINE
|
|
# Codes - sie sind eine Eigenschaft des Dialogs, nicht der Anlage. Nur die
|
|
# AS-/ES-Winkel und die Bogen-/Kurvenwinkel stehen im Journal als Wert und
|
|
# bleiben darum Werte.
|
|
#
|
|
# Die Werte sind bewusst die DOMAENEN-Kurzformen ("links", "haelfte", ...),
|
|
# nicht die Klartext-Antworten des Dialogs: sie sind maschinenlesbar und
|
|
# stehen so auch in der Spec-Datei. Den Annotationstext leitet
|
|
# lib/vf_journal_export.py aus dem Domaenenwert ab (Tabelle _CODE_FRAGEN
|
|
# dort) - eine zweite Code-Tabelle braucht es dafuer nicht.
|
|
|
|
SEITE = {"1": "links", "2": "rechts"}
|
|
VARIANTE = {"1": "aussen", "2": "innen"}
|
|
GEFAELLE = {"1": "hoehe", "2": "winkel"}
|
|
# Vorwahl "Wie soll die Laenge der Gefaellestrecke bestimmt werden?"
|
|
# (vfl-menu in vf-linienzug-modus, wahl="Linie-GF"). Die innere Wahl im
|
|
# Steigend-Sonderfall (Endpunkt/Hoehe) nutzt dieselbe Tabelle - dort sind
|
|
# nur "1"/"2" gueltig, "steigend" kommt dort nie vor.
|
|
GF_LAENGE_MODUS = {"1": "endpunkt", "2": "hoehe", "3": "steigend"}
|
|
VERTEILUNG = {"1": "haelfte", "2": "einlauf"}
|
|
IM_VF = {"1": "horizontal", "2": "vario-kurve", "3": "auf-ab"}
|
|
ENDPUNKT = {"1": "motorstation", "2": "weiter", "3": "kettenende"}
|
|
ENDE3 = {"1": "ja-mit-es", "2": "ja-ohne-es", "3": "nein"}
|
|
ENDE2 = {"1": "ja-mit-es", "2": "nein"}
|
|
# Ja/Nein-Antworten des Dialogs. Als Tabelle gefuehrt (und nicht als
|
|
# "code == 1"), damit ein unbekannter Code denselben Fehler liefert wie bei
|
|
# jeder anderen Frage, statt still als "Nein" durchzugehen.
|
|
JA_NEIN = {"1": True, "2": False}
|
|
|
|
# Glied-Typ -> Menue-Code, getrennt nach Position in der Kette. Der Code
|
|
# haengt am Frame: die ERSTE Sektion laeuft ohne Frame und bekommt 4 Optionen
|
|
# (kein GF-Bogen - es gibt noch keine Richtung, an die er anschliessen
|
|
# koennte), jede spaetere 5. Siehe vf-linienzug-modus.
|
|
#
|
|
# WICHTIG (2026-09-12): vf-linienzug-modus wurde SPAETER um "GF-Bogen"/
|
|
# "Vario-Kurve" auch am Kettenanfang erweitert (6 statt 4 Optionen dort,
|
|
# "Fortsetzung einer bestehenden Teilkette" - Referenzrichtung kommt dann aus
|
|
# einem KS_AUS am Startpunkt). Lisp/vf_spec.lsp's *vsp-menu-start* wurde
|
|
# entsprechend auf die NEUE 6er-Zuordnung umgestellt, WEIL vsp-journal seine
|
|
# Tokens durch den AKTUELLEN (lebenden) vf-linienzug-modus REPLAYT - der
|
|
# Code muss dort zur JETZT gueltigen Menue-Reihenfolge passen.
|
|
#
|
|
# Diese Python-Tabelle bleibt bewusst auf der ALTEN 4er-Zuordnung: sie
|
|
# decodiert tests/testdata/hm_recformat.json - eine EINGEFRORENE Aufzeichnung
|
|
# einer echten Kundenzeichnung, die zu einem Zeitpunkt VOR dieser Erweiterung
|
|
# entstand (ihr roher Code fuer "Linie-GF" am Kettenanfang ist "1", nicht
|
|
# "3"). Wuerde diese Tabelle auf die neue Zuordnung umgestellt, koennte der
|
|
# Parser-Kern die vorhandene Aufzeichnung nicht mehr korrekt lesen (siehe
|
|
# Testfall test_verlustfrei in tests/test_vf_spec.py). Die Tokenfolgen von
|
|
# LISP (vsp-journal, GEGEN den lebenden Bau-Ablauf) und Python (hier, GEGEN
|
|
# die eingefrorene Aufzeichnung) sind fuer "Linie-GF" am Kettenanfang darum
|
|
# ab dieser Erweiterung bewusst NICHT mehr identisch - eine unvermeidbare
|
|
# Folge davon, dass sich die Menue-Struktur nach der Aufzeichnung geaendert
|
|
# hat. Wird hm_recformat.json je aus einer NEUEN Aufzeichnung (mit dem
|
|
# aktuellen 6-Optionen-Ablauf) neu erzeugt, muesste diese Tabelle mitziehen.
|
|
#
|
|
# Fuer eine NEUE Uebersetzung, die durch den lebenden Ablauf replayt werden
|
|
# soll, gibt es die aktuelle Zuordnung daneben (MENU_START_AKTUELL): beide
|
|
# Auspraegungen sind ueber den Parameter "grammatik" waehlbar, diese hier
|
|
# bleibt die Vorgabe.
|
|
MENU_START = {"Linie-GF": "1", "Linie-VF": "2", "Horizontal-VF": "3",
|
|
"Linie": "4"}
|
|
# Die HEUTE gueltige Zuordnung am Kettenanfang (6 Optionen), identisch zu
|
|
# *vsp-menu-start* in Lisp/vf_spec.lsp. Sie gilt fuer JEDE neue Uebersetzung,
|
|
# die spaeter durch den lebenden vf-linienzug-modus replayt werden soll - die
|
|
# alte Tabelle daneben bleibt nur fuer die eingefrorene Aufzeichnung stehen.
|
|
MENU_START_AKTUELL = {"GF-Bogen": "1", "Vario-Kurve": "2", "Linie-GF": "3",
|
|
"Linie-VF": "4", "Horizontal-VF": "5", "Linie": "6"}
|
|
MENU_WEITER = {"GF-Bogen": "1", "Linie-GF": "2", "Linie-VF": "3",
|
|
"Horizontal-VF": "4", "Linie": "5"}
|
|
|
|
GLIEDER = sorted(set(MENU_WEITER) | {"ES"})
|
|
|
|
# Glieder, die eine VF-Einheit eroeffnen, und ob ihr erster Koerper
|
|
# horizontal ist (winkel1 = 0 in vfl-vf-einheit: nur dann laufen die
|
|
# Separator-/Endpunkt-Fragen VOR der Laengenberechnung).
|
|
VF_GLIEDER = {"Horizontal-VF": True, "Linie-VF": False, "Linie": False}
|
|
|
|
|
|
class Grammatik(object):
|
|
"""Eine Auspraegung der Dialog-Grammatik (welche Frage an welcher Stelle).
|
|
|
|
Die Grammatik ist KEINE Eigenschaft der Anlage, sondern des Dialogs - und
|
|
der hat sich seit der Aufzeichnung tests/testdata/hm_recformat.json
|
|
geaendert. Beide Auspraegungen muessen darum NEBENEINANDER benutzbar sein:
|
|
die eingefrorene, um die alte Aufzeichnung weiter lesen zu koennen, und die
|
|
aktuelle fuer jede neue Uebersetzung, die spaeter durch den lebenden
|
|
vf-linienzug-modus replayt wird (Lisp/vf_spec.lsp folgt immer der
|
|
aktuellen).
|
|
|
|
menu_start
|
|
Glied -> Menue-Code der ERSTEN Sektion (ohne Frame).
|
|
horizontal_vf_fragt_gf_verteilung
|
|
Ob eine mit "Horizontal-VF" eroeffnete VF-Einheit die GF-Verteilung
|
|
fragt. Der heutige Ablauf fragt sie dort NICHT mehr: die Aufrufstelle
|
|
in vf-linienzug-modus gibt "2" (alles am Einlauf) fest vor
|
|
(vfl-vf-einheit-abschluss, Argument gf-verteilung-fest).
|
|
"""
|
|
|
|
def __init__(self, name, menu_start, horizontal_vf_fragt_gf_verteilung):
|
|
self.name = name
|
|
self.menu_start = menu_start
|
|
self.horizontal_vf_fragt_gf_verteilung = (
|
|
horizontal_vf_fragt_gf_verteilung)
|
|
|
|
def __repr__(self):
|
|
return "Grammatik(%r)" % self.name
|
|
|
|
|
|
# Stand der EINGEFRORENEN Aufzeichnung (hm_recformat.json) - Vorgabe, damit
|
|
# alle bestehenden Aufrufer unveraendert weiterlaufen.
|
|
GRAMMATIK_AUFZEICHNUNG = Grammatik("aufzeichnung", MENU_START, True)
|
|
# Stand des LEBENDEN Bau-Ablaufs (vf_linienzug.lsp) - fuer neue Uebersetzungen.
|
|
GRAMMATIK_AKTUELL = Grammatik("aktuell", MENU_START_AKTUELL, False)
|
|
|
|
|
|
# ============================================================
|
|
# Code <-> Wert
|
|
# ============================================================
|
|
|
|
def wert_aus_code(tabelle, code, was):
|
|
"""Journal-Code in den Domaenenwert uebersetzen (fail fast)."""
|
|
if code not in tabelle:
|
|
raise ValueError("%s: unbekannter Code %r (erlaubt: %s)"
|
|
% (was, code, sorted(tabelle)))
|
|
return tabelle[code]
|
|
|
|
|
|
def code_aus_wert(tabelle, wert, was):
|
|
"""Domaenenwert in den Journal-Code uebersetzen (fail fast)."""
|
|
fuer = dict((v, k) for k, v in tabelle.items())
|
|
if wert not in fuer:
|
|
raise ValueError("%s: unbekannter Wert %r (erlaubt: %s)"
|
|
% (was, wert, sorted(fuer)))
|
|
return fuer[wert]
|
|
|
|
|
|
def ja_aus_code(code, was):
|
|
return wert_aus_code(JA_NEIN, code, was)
|
|
|
|
|
|
def code_aus_ja(wert):
|
|
return "1" if wert else "2"
|
|
|
|
|
|
# ============================================================
|
|
# Schutz fuer das flache, LISP-lesbare JSON
|
|
# ============================================================
|
|
|
|
def pruefe_flachen_freitext(objekte, wo="Spec-JSON"):
|
|
"""Jeden Stringwert der flachen Objekte auf rohe Anfuehrungszeichen
|
|
pruefen - und bei einem Treffer hart abbrechen.
|
|
|
|
Warum das kein Kosmetik-Check ist: der LISP-seitige Leser
|
|
(ssg-load-json -> ssg-cfg-parse-value, benutzt von vsp-json-laden/
|
|
vsp-gruppieren in Lisp/vf_spec.lsp) ist ein ZEILENWEISER Minimalparser
|
|
OHNE Escape-Behandlung. Er schneidet einen Stringwert am ersten rohen "
|
|
ab. Ein Anfuehrungszeichen mitten in einem Freitextfeld (quelle,
|
|
beschreibung, ...) verschiebt damit alles Folgende um eine Anfuehrung -
|
|
und zerlegt nicht nur dieses Feld, sondern das Parsen der GESAMTEN
|
|
restlichen Datei. Realer Fehlerfall: ein maskiertes \\"sub\\":\\"auf-ab\\"
|
|
in einem quelle-Feld liess auf LISP-Seite ALLE 5 Ketten ausfallen.
|
|
|
|
Python's json-Modul liest so eine Datei anstandslos wieder ein - genau
|
|
darum faellt der Fehler ohne diese Pruefung nicht auf. Gueltiges JSON
|
|
reicht hier NICHT; die strengere Bedingung des LISP-Lesers muss auch
|
|
gelten. Ein Kommentar allein hat das schon einmal nicht verhindert,
|
|
darum jetzt eine harte Pruefung im Schreibpfad.
|
|
"""
|
|
for nr, obj in enumerate(objekte, 1):
|
|
kennung = (obj.get("spec_id") or obj.get("glied") or obj.get("sub")
|
|
or obj.get("id") or "?")
|
|
for schluessel, wert in obj.items():
|
|
if isinstance(wert, str) and '"' in wert:
|
|
raise ValueError(
|
|
'%s, Objekt %d (%s): Feld %r enthaelt ein '
|
|
'Anfuehrungszeichen. Der LISP-Leser (ssg-cfg-parse-value) '
|
|
'kennt keine Escapes und schneidet den Wert dort ab - das '
|
|
'zerlegt das Parsen der GESAMTEN restlichen Datei. Text '
|
|
'ohne " formulieren. Wert: %s'
|
|
% (wo, nr, kennung, schluessel, wert))
|
|
|
|
|
|
# ============================================================
|
|
# Sink: was der Parser-Kern an jeder Entscheidungsstelle meldet
|
|
# ============================================================
|
|
|
|
class Sink(object):
|
|
"""Aufnehmer fuer den Parser-Kern.
|
|
|
|
Der Kern kennt nur die Grammatik, nicht das Ziel-Format. An jeder Stelle,
|
|
an der ein Wert verbraucht wird oder sich die Struktur aendert, ruft er
|
|
genau eine dieser Methoden. Die Schnittstelle ist bewusst FEINGRANULAR
|
|
(ein Aufruf je Wert, nicht je Glied): ein groeberer Schnitt ("bau mir das
|
|
Glied und gib mir das Ergebnis") waere wieder je Ausgabe eine eigene
|
|
Implementierung der Grammatik - genau das, was hier abgeschafft wird.
|
|
|
|
Jede Methode bekommt den Wert als DOMAENENWERT (bereits uebersetzt), dazu
|
|
|
|
name - Feldname in der Spec (selbstbeschreibend, kein Index)
|
|
frage - Schluessel der Frage im Dialog. Er unterscheidet gleichnamige
|
|
Felder an verschiedenen Stellen ("hoehe" ist am Kettenende
|
|
eine andere Frage als am Auf/Ab-Segment) und ist damit der
|
|
Anker fuer die Annotationstexte des Protokolls.
|
|
"""
|
|
|
|
# --- Struktur ---
|
|
def praeambel_start(self):
|
|
"""Kettenkopf beginnt (Startpunkt/Starthoehe/AS-Element)."""
|
|
|
|
def praeambel_ende(self):
|
|
"""Kettenkopf fertig."""
|
|
|
|
def sektion_start(self, label):
|
|
"""STEP-Marker eines Gliedes verbraucht; label = Segmenttyp."""
|
|
|
|
def glied_gewaehlt(self, label, code, frage):
|
|
"""Menue-Antwort zum Glied verbraucht (STR, bereits geprueft)."""
|
|
|
|
def sektion_ende(self):
|
|
"""Glied fertig."""
|
|
|
|
def marke(self, label):
|
|
"""STEP-Marker OHNE eigene Menue-Antwort (Vario-Kurve, ES-Untermarke
|
|
im kettenende-Zweig)."""
|
|
|
|
def vf_start(self):
|
|
"""VF-Einheit beginnt; die folgenden Felder gehoeren zu ihr."""
|
|
|
|
def vf_ende(self):
|
|
"""VF-Einheit fertig."""
|
|
|
|
def sub_start(self, sub):
|
|
"""Sub-Knoten der VF-Einheit beginnt (motorstation/kettenende/
|
|
horizontal/vario-kurve/auf-ab)."""
|
|
|
|
def sub_ende(self):
|
|
"""Sub-Knoten fertig."""
|
|
|
|
# --- Werte ---
|
|
def punkt_abs(self, punkt):
|
|
"""PT verbraucht - der absolute Startpunkt der Kette."""
|
|
|
|
def linie(self, dl, hz, frage):
|
|
"""DL (+ REAL beim ersten Segment der Kette) verbraucht."""
|
|
|
|
def real_feld(self, name, wert, frage):
|
|
"""REAL verbraucht (Hoehe, Winkel, ...)."""
|
|
|
|
def int_feld(self, name, wert, frage):
|
|
"""INT verbraucht (Bogen-/Kurvenwinkel, Winkelwahl-Index)."""
|
|
|
|
def code_feld(self, name, code, wert, frage):
|
|
"""STR ueber eine Code-Tabelle verbraucht; wert ist der
|
|
Domaenenwert, code die rohe Menue-Antwort aus dem Journal."""
|
|
|
|
def roh_feld(self, name, wert, frage):
|
|
"""STR, dessen Inhalt schon der Wert ist (AS-/ES-Winkel)."""
|
|
|
|
def ja_nein(self, name, code, wert, frage):
|
|
"""STR ueber die Ja/Nein-Tabelle verbraucht; wert ist True/False."""
|
|
|
|
def wahl(self, name, code, wert, frage):
|
|
"""Eine Antwort, die sich in der STRUKTUR niederschlaegt statt in
|
|
einem Feld (Endpunkt-Frage, Auswahl in der VF-Einheit). Die Spec
|
|
speichert sie nicht - sie steckt im Sub-Knoten -, das Protokoll
|
|
muss sie trotzdem zeigen."""
|
|
|
|
def implizit(self, name, wert):
|
|
"""Ein Feld, das der Ablauf ERGIBT, ohne dass ein Token dafuer
|
|
gelesen wurde (segment_typ, das automatische VF-Ende). Umgekehrter
|
|
Fall zu wahl(): die Spec braucht es, das Protokoll nicht."""
|
|
|
|
def ergebnis(self):
|
|
"""Das fertige Ergebnis nach dem letzten Token."""
|
|
raise NotImplementedError
|
|
|
|
|
|
# ============================================================
|
|
# Parser-Kern
|
|
# ============================================================
|
|
|
|
class Kern(object):
|
|
"""Laeuft den Frageablauf von vf-linienzug-modus (Modus 1) EINMAL ab:
|
|
|
|
Praeambel: Startpunkt, Starthoehe, AS-Element (Ja/Nein, Winkel, Seite)
|
|
Sektionsschleife je Glied: STEP-Marker, Menuewahl, dann die Fragen des
|
|
gewaehlten Segmenttyps
|
|
VF-Einheit (Horizontal-VF / Linie-VF / Linie): GF-Verteilung, ggf.
|
|
Separator-Fragen des horizontalen Erstkoerpers, dann die
|
|
Fortsetzungsschleife (Horizontal / Vario-Kurve / Auf-Ab) bis
|
|
Motorstation oder Kettenende, danach der Abschluss
|
|
Kettenende: ES-Glied (Winkel, Seite)
|
|
|
|
Was dabei herauskommt, bestimmt allein die Sink.
|
|
"""
|
|
|
|
def __init__(self, tok, sink, grammatik=GRAMMATIK_AUFZEICHNUNG):
|
|
self.tok = list(tok)
|
|
self.sink = sink
|
|
self.grammatik = grammatik
|
|
self.pos = 0
|
|
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
|
|
|
|
def real(self):
|
|
return float(self.nimm("REAL"))
|
|
|
|
def ganz(self):
|
|
return int(self.nimm("INT"))
|
|
|
|
# --- Werte lesen und melden ---
|
|
|
|
def _code(self, name, tabelle, was, frage):
|
|
code = self.nimm("STR")
|
|
wert = wert_aus_code(tabelle, code, was)
|
|
self.sink.code_feld(name, code, wert, frage)
|
|
return wert
|
|
|
|
def _wahl(self, name, tabelle, was, frage):
|
|
code = self.nimm("STR")
|
|
wert = wert_aus_code(tabelle, code, was)
|
|
self.sink.wahl(name, code, wert, frage)
|
|
return wert
|
|
|
|
def _ja(self, name, was, frage):
|
|
code = self.nimm("STR")
|
|
wert = ja_aus_code(code, was)
|
|
self.sink.ja_nein(name, code, wert, frage)
|
|
return wert
|
|
|
|
def _linie(self, frage):
|
|
"""Segmentlaenge, und NUR beim allerersten Segment der Kette die
|
|
gesnappte Fahrtrichtung (vfl-in-abstand journalisiert sie nur bei
|
|
freier Richtungswahl; jedes spaetere Segment erbt die Richtung des
|
|
Vorgaengers)."""
|
|
dl = float(self.nimm("DL"))
|
|
hz = self.real() if (self.erster_dl and self.art() == "REAL") else None
|
|
self.erster_dl = False
|
|
self.sink.linie(dl, hz, frage)
|
|
|
|
def _opt_winkel_idx(self):
|
|
"""Die Winkelwahl (vfl-waehle-winkel) steht nur im Journal, wenn
|
|
mehrere Kandidatenwinkel gueltig waren - sonst faellt die Frage weg."""
|
|
if self.art() == "INT":
|
|
self.sink.int_feld("winkel_idx", self.ganz(), "winkel_idx")
|
|
|
|
def _opt_eingang_hoehe(self):
|
|
"""Ziel-Hoehe fuer den GF1-Einlauf einer mit "Horizontal-VF"
|
|
eroeffneten VF-Einheit (vfl-prompt-hoehe-eingang-gf1). Sie wird nur
|
|
gefragt, wenn der Eingang KOMPLETT neu gebaut wird
|
|
(vfl-eingang-modus-bestimmen liefert nil) - also am echten
|
|
Kettenanfang und nach einer fertigen VF-Einheit, nicht dagegen im
|
|
Anschluss an GF/Separator/Vario-Kurve. Ob sie ansteht, haengt am
|
|
Zustand der Zeichnung und laesst sich hier nicht ausrechnen; am
|
|
Journal ist sie eindeutig (ein REAL/NIL vor der ersten Menue-Antwort
|
|
der VF-Einheit).
|
|
|
|
"auto" (statt einer Zahl, Gegenstueck zu vsp-eingang-hoehe in
|
|
Lisp/vf_spec.lsp): die Frage kommt, aber es wird bewusst KEINE Zahl
|
|
vorgegeben - wie eine leere Konsoleneingabe laesst das den
|
|
Bauablauf seinen eigenen, aus dem Fussabdruck der festen Elemente
|
|
berechneten Vorschlag nehmen. Dafuer steht ein NIL-Token (statt
|
|
REAL) im Journal."""
|
|
if self.art() == "REAL":
|
|
self.sink.real_feld("eingang_hoehe", self.real(), "eingang_hoehe")
|
|
elif self.art() == "NIL":
|
|
self.nimm("NIL")
|
|
self.sink.roh_feld("eingang_hoehe", "auto", "eingang_hoehe")
|
|
|
|
# --- Ablauf ---
|
|
|
|
def lies(self):
|
|
self.praeambel()
|
|
self.sektionen()
|
|
if self.pos != len(self.tok):
|
|
raise ValueError("nicht alle Tokens verbraucht (%d von %d)"
|
|
% (self.pos, len(self.tok)))
|
|
return self.sink.ergebnis()
|
|
|
|
def praeambel(self):
|
|
self.sink.praeambel_start()
|
|
self.sink.punkt_abs([float(v) for v in self.nimm("PT").split(",")])
|
|
self.sink.real_feld("hoehe", self.real(), "start_hoehe")
|
|
if self._ja("as", "AS-Element setzen", "as"):
|
|
self.sink.roh_feld("as_winkel", self.nimm("STR"), "as_winkel")
|
|
self._code("as_seite", SEITE, "AS-Seite", "as_seite")
|
|
self.sink.praeambel_ende()
|
|
|
|
def sektionen(self):
|
|
while self.pos < len(self.tok):
|
|
label = self.nimm("STEP")
|
|
if label == "ES":
|
|
self.sink.sektion_start("ES")
|
|
self.es_glied()
|
|
self.sink.sektion_ende()
|
|
continue
|
|
if label == "Vario-Kurve":
|
|
raise ValueError("Vario-Kurve-Marker ausserhalb einer "
|
|
"VF-Einheit (Token %d)" % (self.pos - 1))
|
|
self.sink.sektion_start(label)
|
|
tabelle = (self.grammatik.menu_start if self.erster_step
|
|
else MENU_WEITER)
|
|
frage = "menu_start" if self.erster_step else "menu_weiter"
|
|
code = self.nimm("STR")
|
|
soll = tabelle.get(label)
|
|
if soll is None:
|
|
raise ValueError("Glied %r ist an dieser Stelle nicht "
|
|
"waehlbar (Token %d)" % (label, self.pos - 1))
|
|
if code != soll:
|
|
raise ValueError("Glied %r erwartet Menue-Code %r, Journal "
|
|
"hat %r (Token %d)"
|
|
% (label, soll, code, self.pos - 1))
|
|
self.erster_step = False
|
|
self.sink.glied_gewaehlt(label, code, frage)
|
|
self.glied(label)
|
|
self.sink.sektion_ende()
|
|
|
|
def es_glied(self):
|
|
self.sink.roh_feld("winkel", self.nimm("STR"), "es_winkel")
|
|
self._code("seite", SEITE, "ES-Seite", "es_seite")
|
|
|
|
def glied(self, label):
|
|
if label == "GF-Bogen":
|
|
self.sink.int_feld("winkel", self.ganz(), "gf_bogen_winkel")
|
|
self._code("seite", SEITE, "GF-Bogen-Seite", "gf_bogen_seite")
|
|
elif label == "Linie-GF":
|
|
self._linie("gf_linie")
|
|
if self._code("gefaelle", GEFAELLE, "Gefaelle",
|
|
"gefaelle") == "winkel":
|
|
self.sink.real_feld("winkel", self.real(), "gf_winkel")
|
|
else:
|
|
self.sink.real_feld("hoehe", self.real(), "gf_hoehe")
|
|
self._code("ende", ENDE3, "Kettenende", "ende3")
|
|
elif label == "Linie-VF":
|
|
self._linie("vf_linie")
|
|
self.sink.real_feld("hoehe", self.real(), "vf_zielhoehe")
|
|
self._opt_winkel_idx()
|
|
self.vf_einheit(auto_ende=False, erst_hor=False)
|
|
elif label == "Horizontal-VF":
|
|
self._linie("horizontal_vf")
|
|
self._opt_eingang_hoehe()
|
|
self.vf_einheit(auto_ende=False, erst_hor=True)
|
|
elif label == "Linie":
|
|
self._linie("linie_kettenende")
|
|
self.sink.real_feld("hoehe", self.real(), "kettenende_hoehe")
|
|
self._opt_winkel_idx()
|
|
# vfl-segment-entscheidung waehlt selbst zwischen reiner
|
|
# Gefaellestrecke und VF-Einheit. Am Journal erkennbar: eine
|
|
# VF-Einheit beginnt mit der GF-Verteilungsfrage (STR), eine
|
|
# Gefaellestrecke geht direkt zum ES-Glied (STEP) oder endet.
|
|
if self.art() == "STR":
|
|
self.sink.implizit("segment_typ", "VF")
|
|
self.vf_einheit(auto_ende=True, erst_hor=False)
|
|
else:
|
|
self.sink.implizit("segment_typ", "GF")
|
|
else:
|
|
raise ValueError("unbekanntes Glied: %r" % label)
|
|
|
|
def vf_einheit(self, auto_ende, erst_hor):
|
|
self.sink.vf_start()
|
|
# GF-Verteilung: bei einem horizontalen Erstkoerper stellt der heutige
|
|
# Ablauf die Frage nicht mehr (fest "2" = alles am Einlauf) - siehe
|
|
# Grammatik.horizontal_vf_fragt_gf_verteilung.
|
|
if not erst_hor or self.grammatik.horizontal_vf_fragt_gf_verteilung:
|
|
self._code("gf_verteilung", VERTEILUNG, "GF-Verteilung",
|
|
"gf_verteilung")
|
|
if erst_hor:
|
|
# Erster Koerper horizontal: vfl-baue-horizontal-koerper stellt
|
|
# die Separator-Fragen VOR der Laengenberechnung. Die
|
|
# Endpunkt-Antwort danach gehoert schon zum naechsten Knoten und
|
|
# wird darum unten in der Schleife gelesen.
|
|
self._ja("sep_vor", "Separator vor", "sep_vor")
|
|
self._ja("sep_nach", "Separator nach", "sep_nach")
|
|
ziel_ende = False
|
|
es_gewuenscht = True
|
|
while True:
|
|
was = self._wahl("endpunkt", ENDPUNKT, "Endpunkt des Foerderers",
|
|
"endpunkt")
|
|
if was == "motorstation":
|
|
self.sink.sub_start("motorstation")
|
|
self.sink.sub_ende()
|
|
break
|
|
if was == "kettenende":
|
|
self.sink.sub_start("kettenende")
|
|
es_gewuenscht = self._ja("es", "ES-Element setzen", "es")
|
|
if es_gewuenscht:
|
|
# Winkel+Seite des ES-Elements werden HIER erfragt (nicht
|
|
# erst in vfl-vf-einheit-abschluss): sie bestimmen den
|
|
# ES-Fussabdruck, der schon in die Streckenzerlegung
|
|
# eingeht. Der Marker kommt aus vfl-frage-es-seite.
|
|
marke = self.nimm("STEP")
|
|
if marke != "ES":
|
|
raise ValueError("ES-Marker erwartet, %r gefunden"
|
|
% marke)
|
|
self.sink.marke("ES")
|
|
self.sink.roh_feld("es_winkel", self.nimm("STR"),
|
|
"es_winkel")
|
|
self._code("es_seite", SEITE, "ES-Seite", "es_seite")
|
|
self._linie("kettenende_ziel")
|
|
self.sink.real_feld("hoehe", self.real(), "kettenende_hoehe")
|
|
self._opt_winkel_idx()
|
|
self.sink.sub_ende()
|
|
ziel_ende = True
|
|
break
|
|
wahl = self._wahl("im_vf", IM_VF, "Naechstes in der VF-Einheit",
|
|
"im_vf")
|
|
self.sink.sub_start(wahl)
|
|
if wahl == "horizontal":
|
|
self._linie("horizontal_sub")
|
|
self._ja("sep_vor", "Separator vor", "sep_vor")
|
|
self._ja("sep_nach", "Separator nach", "sep_nach")
|
|
elif wahl == "vario-kurve":
|
|
marke = self.nimm("STEP")
|
|
if marke != "Vario-Kurve":
|
|
raise ValueError("Vario-Kurve-Marker erwartet, %r "
|
|
"gefunden" % marke)
|
|
self.sink.marke("Vario-Kurve")
|
|
self.sink.int_feld("winkel", self.ganz(), "vario_winkel")
|
|
self._code("seite", SEITE, "Vario-Kurve-Seite", "vario_seite")
|
|
self._code("variante", VARIANTE, "Vario-Kurve-Variante",
|
|
"vario_variante")
|
|
else: # auf-ab
|
|
self._linie("aufab_sub")
|
|
self.sink.real_feld("hoehe", self.real(), "aufab_hoehe")
|
|
self._opt_winkel_idx()
|
|
self.sink.sub_ende()
|
|
# --- Abschluss (vfl-vf-einheit-abschluss) ---
|
|
# Ohne ES-Wunsch am Zielpunkt endet die Kette dort direkt; bei
|
|
# auto_ende (Glied "Linie") oder erreichtem Zielpunkt folgt das
|
|
# ES-Glied ohne Frage. Sonst wird gefragt.
|
|
if ziel_ende and not es_gewuenscht:
|
|
self.sink.implizit("ende", "zielpunkt-ohne-es")
|
|
elif auto_ende or ziel_ende:
|
|
self.sink.implizit("ende", "automatisch")
|
|
else:
|
|
if self._code("ende", ENDE2, "Ist das Kettenende",
|
|
"ende2") != "ja-mit-es":
|
|
self._ja("separator", "Separator an dieser Stelle",
|
|
"separator")
|
|
self.sink.vf_ende()
|
|
|
|
|
|
def lies_journal(tok, sink, grammatik=GRAMMATIK_AUFZEICHNUNG):
|
|
"""Tokenfolge durch den gemeinsamen Kern laufen lassen und das Ergebnis
|
|
der Sink zurueckgeben."""
|
|
return Kern(tok, sink, grammatik).lies()
|