[MERGE] Lager branch in main
This commit is contained in:
@@ -13,7 +13,7 @@ Edit-Befehl (Doppelklick) zum Neuaufbau liest. Typischer Ablauf: Scanner-
|
||||
Vorlage nachbauen, in BricsCAD von Hand korrigieren/ergaenzen, Zeichnung als
|
||||
DXF speichern und mit diesem Skript die Testdaten aktualisieren.
|
||||
|
||||
Je Wrapper (Felder wie in tests/testdata/object_data.md bzw. mubea.json):
|
||||
Je Wrapper (Felder wie in doc/json-input-lisp.md bzw. mubea.json):
|
||||
|
||||
KREISEL_n -> Kreisel ("function": "insert", x/y/z = Einfuegepunkt,
|
||||
abstand = Mittenabstand AN8-SP8 aus der Blockdefinition,
|
||||
|
||||
@@ -6,7 +6,7 @@ dxf_scan_components.py - Erkennt ILS-/Omniflo-Komponenten in einem FREMDEN DXF.
|
||||
Gedacht fuer Projekt-Zeichnungen, die NICHT von unseren Makros erzeugt wurden
|
||||
(also ohne die Wrapper-Bloecke KREISEL_*, VF_*, GF_*). Das Skript sucht die
|
||||
Bauteile ueber Blocknamen-Muster, rechnet ihre Position in Weltkoordinaten um
|
||||
und schreibt das Ergebnis im JSON-Schema aus tests/testdata/object_data.md -
|
||||
und schreibt das Ergebnis im JSON-Schema aus doc/json-input-lisp.md -
|
||||
also so, dass die Anlage per JSON-Treiber mit den Produktionsmakros
|
||||
nachgebaut werden kann (Vorbild: tests/testdata/mubea.json, das bisher von
|
||||
Hand aus einem DXF abgeleitet wurde).
|
||||
@@ -1697,7 +1697,7 @@ def gf_eintraege_aus_linien(linien, as_liste, kreisel=None, sep_liste=None,
|
||||
def baue_eintraege(treffer, defaults, boegen, weichen,
|
||||
bekannte_bloecke=None, linien=None, kreisel_hoehen=None,
|
||||
kreisel_achsen=None, flach=False):
|
||||
"""Erkannte Vorkommen auf die Schemata aus object_data.md abbilden.
|
||||
"""Erkannte Vorkommen auf die Schemata aus json-input-lisp.md abbilden.
|
||||
|
||||
kreisel_hoehen: Liste (x, y, z) vom Anwender - z gilt fuer den Kreisel,
|
||||
dessen Einfuegepunkt (Eintrag x/y) hoechstens KREISEL_HOEHE_TOL entfernt
|
||||
@@ -2082,7 +2082,7 @@ def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Erkennt ILS-/Omniflo-Komponenten in einem fremden DXF "
|
||||
"und schreibt sie im JSON-Schema aus "
|
||||
"tests/testdata/object_data.md.")
|
||||
"doc/json-input-lisp.md.")
|
||||
parser.add_argument("dxf", help="Quellzeichnung (binaeres oder ASCII-DXF)")
|
||||
parser.add_argument("data_dir", help="Datenverzeichnis mit json/")
|
||||
parser.add_argument("output", help="Ziel-JSON")
|
||||
|
||||
@@ -42,6 +42,7 @@ from export_neighbors import (
|
||||
compute_kreisel_touch_switches,
|
||||
lisp_touch_switches,
|
||||
load_lisp_kreisel_weichen,
|
||||
einsortieren_freie_separatoren,
|
||||
compute_sep_kette,
|
||||
compute_kreisel_umlauf,
|
||||
SENSOR_TEILEARTEN,
|
||||
@@ -1543,6 +1544,12 @@ def process_blocks(blocks, lookup, dbg=None, lisp_weichen=None):
|
||||
if item.get("teileart", "") == "ILS 2.0 Scanner" and separator_id:
|
||||
item["merkmale"]["Naechster Separator"] = separator_id
|
||||
|
||||
# Freie Separatoren, deren Zuordnung eine VF_n/GF_n nennt, die aber nicht
|
||||
# beim Bau der Strecke entstanden sind (fehlen darum in ihrer sepliste),
|
||||
# nach Lage in die Kette aufnehmen - nur fuer diesen Lauf, die XDATA am
|
||||
# Block bleibt unveraendert (siehe einsortieren_freie_separatoren).
|
||||
einsortieren_freie_separatoren(items, NEIGHBOR_TOLERANCE_MM, dbg=dbg)
|
||||
|
||||
# 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).
|
||||
|
||||
@@ -1255,6 +1255,194 @@ def _asel_collision_boxes(item, half_tol, box_dims):
|
||||
# Ersetzt das bisherige Raten der Ketten-internen Reihenfolge (die Linie
|
||||
# selbst wird nicht exportiert) durch die tatsaechliche Bau-Reihenfolge.
|
||||
|
||||
def sepliste_einsortier_positionen(kette_xy, kandidaten):
|
||||
"""Reiner Rechenkern fuer einsortieren_freie_separatoren: wohin gehoert
|
||||
jeder Kandidat in eine vorhandene Kette?
|
||||
|
||||
kette_xy = [(x, y), ...] Lage der Kettenglieder in Kettenfolge (AS ...
|
||||
ES), mindestens zwei Punkte. Sie bilden einen Polygonzug - so
|
||||
folgt die Zuordnung auch einer Kette, die um eine Kurve laeuft,
|
||||
statt nur einer Geraden AS->ES.
|
||||
kandidaten = [(key, x, y), ...]
|
||||
|
||||
Jeder Kandidat wird dem naechstgelegenen Abschnitt (Glied i -> i+1)
|
||||
zugeordnet. Liegt er dabei VOR dem ersten oder HINTER dem letzten Glied,
|
||||
wird er verworfen - eingefuegt wird nur zwischen AS und ES.
|
||||
|
||||
Rueckgabe (einfuegen, verworfen): einfuegen = {i: [key, ...]} - nach
|
||||
Glied i einzufuegen, untereinander in Kettenrichtung sortiert;
|
||||
verworfen = [key, ...].
|
||||
"""
|
||||
abschnitte = []
|
||||
for i in range(len(kette_xy) - 1):
|
||||
(ax, ay), (bx, by) = kette_xy[i], kette_xy[i + 1]
|
||||
dx, dy = bx - ax, by - ay
|
||||
l2 = dx * dx + dy * dy
|
||||
if l2 > 1e-12:
|
||||
abschnitte.append((i, ax, ay, dx, dy, l2))
|
||||
letzter = len(kette_xy) - 2
|
||||
|
||||
einfuegen = {}
|
||||
verworfen = []
|
||||
for key, x, y in kandidaten:
|
||||
best = None
|
||||
for i, ax, ay, dx, dy, l2 in abschnitte:
|
||||
t = ((x - ax) * dx + (y - ay) * dy) / l2
|
||||
tc = min(max(t, 0.0), 1.0)
|
||||
d = math.hypot(x - (ax + tc * dx), y - (ay + tc * dy))
|
||||
if best is None or d < best[0] - 1e-9:
|
||||
best = (d, i, t)
|
||||
if best is None:
|
||||
verworfen.append(key)
|
||||
continue
|
||||
_d, i, t = best
|
||||
if (i == 0 and t <= 0.0) or (i == letzter and t >= 1.0):
|
||||
verworfen.append(key)
|
||||
continue
|
||||
einfuegen.setdefault(i, []).append((min(max(t, 0.0), 1.0), key))
|
||||
|
||||
return ({i: [key for _t, key in sorted(liste)] for i, liste in einfuegen.items()},
|
||||
verworfen)
|
||||
|
||||
|
||||
def _kettenglied_xy(entry, item, strecke):
|
||||
"""Lage eines sepliste-Glieds fuer den Polygonzug: AS/ES ueber K1/K2 der
|
||||
Strecke (Ein-/Auslauf - der ZZ_ASES_MARKER sitzt auf dem Einfuegepunkt
|
||||
des Elements und kann bis ~600 mm seitlich davon liegen), sonst bzw.
|
||||
ohne K1/K2 der Bounding-Box-Mittelpunkt des vertretenden Items."""
|
||||
typ = entry.get("typ", "")
|
||||
if typ in ("AS", "ES"):
|
||||
k1_xy, k2_xy = _asel_kos_xy(strecke)
|
||||
pt = k1_xy if typ == "AS" else k2_xy
|
||||
if pt is not None:
|
||||
return pt
|
||||
bbox = (item or {}).get("_bbox")
|
||||
if not bbox:
|
||||
return None
|
||||
return (bbox.get("cx", 0.0), bbox.get("cy", 0.0))
|
||||
|
||||
|
||||
def einsortieren_freie_separatoren(items, tolerance_mm, dbg=None):
|
||||
"""Nimmt freie Separatoren in die sepliste ihrer VF_n/GF_n-Strecke auf.
|
||||
|
||||
Die sepliste entsteht beim BAU der Strecke (ssg-sepliste-sammeln) und
|
||||
kennt nur die Separatoren, die dabei erzeugt wurden. Ein eigenstaendiger
|
||||
Separator_SP-Block, der spaeter (oder aus einer frueheren Fassung der
|
||||
Strecke) auf ihr liegt, fehlt darin - obwohl seine ZUORDNUNG
|
||||
(cs-zuordnung-lauf) die Strecke nennt. Ohne diesen Schritt fehlt er in
|
||||
TrackIds und Nachbarn (realer Fall Mubea VF_3: 0037/0038 zwischen 0062
|
||||
und 0063).
|
||||
|
||||
Aufgenommen wird ein Separator nur, wenn
|
||||
- seine Zuordnung die Strecke nennt,
|
||||
- er in keiner sepliste steht,
|
||||
- sein Mittelpunkt in der Bounding-Box der Strecke liegt (+ tolerance_mm/2),
|
||||
- er laengs der Kette zwischen AS und ES liegt
|
||||
(sepliste_einsortier_positionen).
|
||||
|
||||
Bewusst NUR im Speicher dieses Exportlaufs: die sepliste-XDATA am Block
|
||||
bleibt das Bauprotokoll. Lisp ordnet ihre Eintraege den eingebauten
|
||||
Bausteinen ueber die Anzahl je Typ zu (csv:sepliste-kette-paare) und
|
||||
fuehrt reservierte AS/ES-IDs unter der lfdnr - ein in die XDATA
|
||||
eingefuegter fremder Separator verschoebe beides. Ausserdem schreibt jeder
|
||||
Neuaufbau der Strecke die XDATA neu; hier wird bei jedem Export frisch
|
||||
aus dem aktuellen Zeichnungsstand gerechnet.
|
||||
|
||||
Aendert items in-place: item["sepliste"] der Strecke bekommt neue
|
||||
SEP-Eintraege (lfdnr "frei<TeileId>", "id" = TeileId des Separators,
|
||||
"frei": True); der Separator bekommt "_in_strecke_kette" (Strecken-
|
||||
TeileId, damit compute_kreisel_umlauf ihn nicht zusaetzlich einem
|
||||
Kreisel zuschlaegt), das Merkmal "Einsortiert in" (Strecken-TeileId,
|
||||
in der CSV-Spalte Merkmale) und einen Hinweis in "warnung".
|
||||
Muss NACH der Separator-Zuordnung und VOR compute_sep_kette laufen.
|
||||
"""
|
||||
half_tol = tolerance_mm / 2.0
|
||||
by_teileid = {it.get("teileid", ""): it for it in items if it.get("teileid", "")}
|
||||
in_ketten = set()
|
||||
for it in items:
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN:
|
||||
for e in it.get("sepliste") or []:
|
||||
eid = str(e.get("id", "")).strip()
|
||||
if eid:
|
||||
in_ketten.add(eid)
|
||||
|
||||
if dbg:
|
||||
dbg("=== einsortieren_freie_separatoren ===")
|
||||
|
||||
for strecke in items:
|
||||
if strecke.get("teileart", "") not in GF_VF_TEILEARTEN:
|
||||
continue
|
||||
sepliste = strecke.get("sepliste")
|
||||
sbbox = strecke.get("_bbox")
|
||||
if not sepliste or not sbbox:
|
||||
continue
|
||||
sid = strecke.get("teileid", "")
|
||||
sbounds = _bounds(sbbox, half_tol)
|
||||
|
||||
kandidaten = []
|
||||
for it in items:
|
||||
if it.get("teileart", "") != "ILS 2.0 Separator":
|
||||
continue
|
||||
tid = it.get("teileid", "")
|
||||
if not tid or tid in in_ketten:
|
||||
continue
|
||||
zuordnung = ((it.get("merkmale") or {}).get("Zuordnung")
|
||||
or it.get("_zuordnung_attr") or "").strip()
|
||||
if zuordnung != sid:
|
||||
continue
|
||||
bbox = it.get("_bbox")
|
||||
if not bbox:
|
||||
continue
|
||||
cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0)
|
||||
if not (sbounds[0] <= cx <= sbounds[1] and sbounds[2] <= cy <= sbounds[3]):
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: Separator {tid} ausserhalb der Strecken-BBox "
|
||||
f"- nicht einsortiert")
|
||||
continue
|
||||
kandidaten.append((tid, cx, cy))
|
||||
if not kandidaten:
|
||||
continue
|
||||
|
||||
kette_xy = []
|
||||
for e in sepliste:
|
||||
pt = _kettenglied_xy(e, by_teileid.get(str(e.get("id", "")).strip()), strecke)
|
||||
if pt is None:
|
||||
break
|
||||
kette_xy.append(pt)
|
||||
if len(kette_xy) != len(sepliste) or len(kette_xy) < 2:
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: Kettenglieder nicht vollstaendig lokalisierbar "
|
||||
f"- freie Separatoren {[k[0] for k in kandidaten]} nicht einsortiert")
|
||||
continue
|
||||
|
||||
einfuegen, verworfen = sepliste_einsortier_positionen(kette_xy, kandidaten)
|
||||
if dbg and verworfen:
|
||||
dbg(f" Strecke {sid}: {verworfen} liegen nicht zwischen AS und ES "
|
||||
f"- nicht einsortiert")
|
||||
if not einfuegen:
|
||||
continue
|
||||
|
||||
neu = []
|
||||
for i, entry in enumerate(sepliste):
|
||||
neu.append(entry)
|
||||
for tid in einfuegen.get(i, []):
|
||||
neu.append({"typ": "SEP", "lfdnr": f"frei{tid}", "id": tid, "frei": True})
|
||||
sep = by_teileid[tid]
|
||||
sep["_in_strecke_kette"] = sid
|
||||
# Fuer Folgewerkzeuge (sps_skel: sepliste-Gegenprobe): dieser
|
||||
# Separator steht in TrackIds, aber nicht im Bauprotokoll.
|
||||
sep.setdefault("merkmale", {})["Einsortiert in"] = sid
|
||||
hinweis = (f"nicht im Bau von {sid} erzeugt - per Lage in dessen "
|
||||
f"Kette einsortiert")
|
||||
bisherige = sep.get("warnung", "")
|
||||
sep["warnung"] = f"{bisherige}; {hinweis}" if bisherige else hinweis
|
||||
in_ketten.add(tid)
|
||||
strecke["sepliste"] = neu
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: Kette jetzt "
|
||||
f"{' -> '.join(str(e.get('id', '')) for e in neu)}")
|
||||
|
||||
|
||||
def compute_sep_kette(items, dbg=None):
|
||||
"""Baut je VF_n/GF_n-Item MIT "sepliste" die ketteninterne Vorgaenger-/
|
||||
Nachfolger-Beziehung AS -> Sep1 -> ... -> SepN -> ES.
|
||||
@@ -1392,6 +1580,11 @@ def compute_kreisel_umlauf(items, tolerance_mm, durchmesser_mm=800.0,
|
||||
for item in items:
|
||||
if item.get("teileart", "") not in KREISEL_UMLAUF_TEILEARTEN:
|
||||
continue
|
||||
# Freier Separator, den einsortieren_freie_separatoren in die Kette
|
||||
# einer Strecke aufgenommen hat - gehoert nicht zusaetzlich auf einen
|
||||
# Kreisel, auch wenn seine Box einen beruehrt.
|
||||
if item.get("_in_strecke_kette"):
|
||||
continue
|
||||
teileid = item.get("teileid", "")
|
||||
bbox = item.get("_bbox")
|
||||
if not bbox:
|
||||
|
||||
+243
-3
@@ -58,6 +58,7 @@ items_zusammenfassen. output.csv selbst bleibt unveraendert.
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
|
||||
from export_blockpatterns import (load_patterns, load_export_cfg, cfg_path_from_env,
|
||||
matches_any, csv_quote, safe_float, safe_int)
|
||||
@@ -101,14 +102,36 @@ def get_id(block):
|
||||
# Sprachdatei, da rein Sivas-seitig.
|
||||
GERUEST_TYP_ANZEIGE = sivas_mapping.load_flat_mapping("geruest_typ_anzeige")
|
||||
|
||||
# Kettenfoerderer: Attribut WINKEL fuehrt den Neigungswinkel als nackte Zahl
|
||||
# ("26"/"30"), Sivas-Merkmal 359 erwartet den Optionstext mit Gradzeichen.
|
||||
# Gleiche Bauart und derselbe Grund wie GERUEST_TYP_ANZEIGE.
|
||||
KF_TYP_ANZEIGE = sivas_mapping.load_flat_mapping("kf_typ_anzeige")
|
||||
|
||||
# TeileArt-Spalte der Kettenfoerderer-Zeilen. Fester Wert statt Block-Attribut:
|
||||
# er ist fuer jeden Kettenfoerderer gleich, stuende also an jedem Block nur
|
||||
# redundant herum. Die Erkennung laeuft ueber den Blocknamen
|
||||
# (pattern_kettenfoerderer), nicht ueber TEILEART. Ein Block, der das Attribut
|
||||
# doch mitbringt (Altbestand), hat weiterhin Vorrang.
|
||||
KF_TEILEART = "Schraegfoerderer Stahl"
|
||||
|
||||
SEPARATOR_BEARBEITUNG_ANZEIGE = "ohne HARTCOAT HRC 50 ± 5"
|
||||
|
||||
# Sivas-Merkmale "Streckengruppe - Variofoerderer 001..005 - Hoehe in Meter".
|
||||
# Reihenfolge = Bau-Reihenfolge der Foerdereinheiten in VF_HOEHE_m.
|
||||
VF_HOEHE_MERKMALE = ("4433", "4434", "4435", "4436", "4437")
|
||||
|
||||
|
||||
def geruest_typ_anzeige(wert):
|
||||
"""Wandelt den ASCII-Attributwert GERUEST_TYP in die Sivas-Anzeige um."""
|
||||
return GERUEST_TYP_ANZEIGE.get(wert, wert)
|
||||
|
||||
|
||||
def kf_typ_anzeige(wert):
|
||||
"""Wandelt den Kettenfoerderer-Winkel ("26"/"30") in die Sivas-Anzeige
|
||||
zu Merkmal 359 um ("Schrägförderer Stahl 26°")."""
|
||||
return KF_TYP_ANZEIGE.get(str(wert).strip(), wert)
|
||||
|
||||
|
||||
def ja_nein(flag_1_0):
|
||||
"""Wandelt ein "0"/"1"-Attribut in die Sivas-Anzeige "Ja"/"Nein" um."""
|
||||
return "Ja" if flag_1_0 == "1" else "Nein"
|
||||
@@ -120,14 +143,95 @@ def mm_zu_m(wert_mm):
|
||||
return str(round(safe_float(wert_mm, 0.0) / 1000.0, 3))
|
||||
|
||||
|
||||
# --- AS-/ES-Element (Merkmale 4427-4432) ---------------------------------
|
||||
# Zuordnung komplett in cfg/sivas.cfg ([gefaellestrecke]/[variofoerderer]),
|
||||
# hier stehen nur die Transforms. Die Sivas-Hierarchie ist: 4431/4432
|
||||
# ("AS-Element"/"ES-Element", Ja/Nein) schalten die Unterknoten Winkel
|
||||
# (4427/4429) und Seite (4428/4430) frei.
|
||||
#
|
||||
# Ob ein Element vorhanden ist, entscheidet AUSSCHLIESSLICH WINKEL_AS/
|
||||
# WINKEL_ES: das gemeinsame Strecken-Schema (*strecke-attr-front* in
|
||||
# ssg_core.lsp) laesst den Winkel leer, wenn kein Element gewaehlt wurde.
|
||||
# SEITE_AS/SEITE_ES taugen dafuer NICHT - sie tragen mit "rechts" immer einen
|
||||
# Schema-Vorgabewert. Ohne Element bleiben Winkel UND Seite leer, damit in
|
||||
# Sivas keine Seite unter einem "Nein"-Knoten haengt.
|
||||
|
||||
# Attributwert -> Sivas-Optionswert der Auswahlliste ("30"/"90" -> "30 °"/
|
||||
# "90 °", siehe Merkmalstamm). Wie [geruest_typ_anzeige] in der Config, damit
|
||||
# die exakte Schreibweise ohne Code-Aenderung nachgezogen werden kann.
|
||||
AS_ES_WINKEL_ANZEIGE = sivas_mapping.load_flat_mapping("as_es_winkel_anzeige")
|
||||
|
||||
|
||||
def winkel_text(wert):
|
||||
"""Elementwinkel ganzzahlig normiert ("90.0" -> "90"), ohne Sivas-
|
||||
Anzeigeform. Basis fuer as_es_winkel UND fuer die Gruppierung (dort darf
|
||||
die Anzeigeform keine Rolle spielen). Leerer Wert bleibt leer."""
|
||||
wert = str(wert or "").strip()
|
||||
if not wert:
|
||||
return ""
|
||||
zahl = safe_float(wert, None)
|
||||
return wert if zahl is None else str(int(zahl))
|
||||
|
||||
|
||||
def as_es_vorhanden(winkel):
|
||||
"""Merkmal 4431/4432: "Ja", sobald ein Elementwinkel gesetzt ist."""
|
||||
return "Ja" if winkel_text(winkel) else "Nein"
|
||||
|
||||
|
||||
def as_es_winkel(winkel):
|
||||
"""Merkmal 4427/4429: Elementwinkel in der Sivas-Schreibweise der
|
||||
Auswahlliste (cfg/sivas.cfg [as_es_winkel_anzeige]). Ein Winkel ohne
|
||||
Eintrag dort wird unveraendert (nur ganzzahlig normiert) uebernommen."""
|
||||
winkel = winkel_text(winkel)
|
||||
return AS_ES_WINKEL_ANZEIGE.get(winkel, winkel)
|
||||
|
||||
|
||||
def _seite_text(seite, winkel):
|
||||
"""Gemeinsamer Kern von as_seite/es_seite: Seite nur, wenn es das Element
|
||||
ueberhaupt gibt."""
|
||||
if not winkel_text(winkel):
|
||||
return ""
|
||||
return str(seite or "").strip().lower()
|
||||
|
||||
|
||||
@sivas_mapping.braucht_attribs
|
||||
def as_seite(wert, attribs):
|
||||
"""Merkmal 4428: AS-Seite, aber nur bei vorhandenem AS-Element."""
|
||||
return _seite_text(wert, attribs.get("WINKEL_AS", ""))
|
||||
|
||||
|
||||
@sivas_mapping.braucht_attribs
|
||||
def es_seite(wert, attribs):
|
||||
"""Merkmal 4430: ES-Seite, aber nur bei vorhandenem ES-Element."""
|
||||
return _seite_text(wert, attribs.get("WINKEL_ES", ""))
|
||||
|
||||
|
||||
# --- Transform-Namen, wie sie in cfg/sivas.cfg hinter "|" stehen koennen ---
|
||||
TRANSFORMS = {
|
||||
"ja_nein": ja_nein,
|
||||
"geruest_typ_anzeige": geruest_typ_anzeige,
|
||||
"kf_typ_anzeige": kf_typ_anzeige,
|
||||
"mm_zu_m": mm_zu_m,
|
||||
"as_es_vorhanden": as_es_vorhanden,
|
||||
"as_es_winkel": as_es_winkel,
|
||||
"as_seite": as_seite,
|
||||
"es_seite": es_seite,
|
||||
}
|
||||
|
||||
|
||||
def as_es_signatur(attribs):
|
||||
"""AS-/ES-Anteil des Gruppierungsschluessels (siehe
|
||||
gefaellestrecke_signature). Nutzt dieselben Transforms wie der Export,
|
||||
damit Schluessel und Zeileninhalt nicht auseinanderlaufen: ohne Element
|
||||
ist die Seite leer und darf dann auch nicht trennen."""
|
||||
return (as_es_vorhanden(attribs.get("WINKEL_AS", "")),
|
||||
winkel_text(attribs.get("WINKEL_AS", "")),
|
||||
as_seite(attribs.get("SEITE_AS", ""), attribs),
|
||||
as_es_vorhanden(attribs.get("WINKEL_ES", "")),
|
||||
winkel_text(attribs.get("WINKEL_ES", "")),
|
||||
es_seite(attribs.get("SEITE_ES", ""), attribs))
|
||||
|
||||
|
||||
def streckengruppe_objekt_text(attribs):
|
||||
"""Merkmal 4300 "Streckengruppe": "mehrere Objekte", wenn der Block ein
|
||||
kombinierter Streckengruppe-Block (Gefaelle+Vario, TYP-Attribut
|
||||
@@ -191,12 +295,54 @@ def eckrad_signature(block):
|
||||
)
|
||||
|
||||
|
||||
def kettenfoerderer_signature(block):
|
||||
"""
|
||||
Schluessel zur Gruppierung gleichartiger Kettenfoerderer (KF_n): baugleiche
|
||||
Bloecke landen in EINER Sivas-Zeile, deren Anzahl das Merkmal 353 ("Anzahl
|
||||
der gleichen Schraegfoerderer") traegt.
|
||||
|
||||
In den Schluessel gehoert JEDES Attribut, aus dem ein Merkmal der Zeile
|
||||
entsteht - sonst verschmelzen zwei unterschiedlich ausgefuehrte Foerderer
|
||||
zu einer Zeile, und die Merkmale des ERSTEN Blocks gelten fuer beide
|
||||
(gleiche Ueberlegung wie bei gefaellestrecke_signature/as_es_signatur).
|
||||
Die Kommentare nennen darum je Attribut die Merkmalnummer; einzige
|
||||
Ausnahme ist 353, das erst aus der Gruppengroesse entsteht.
|
||||
|
||||
Das ist zugleich die Voraussetzung dafuer, dass items_zusammenfassen (fuer
|
||||
die _zusammengefasst.csv) zwei KF-Zeilen NIE verschmelzen kann: gleiche
|
||||
Merkmale wuerden schon hier dieselbe Gruppe ergeben. Sonst wuerde dort
|
||||
"anzahl" addiert, waehrend 353 unveraendert stehen bliebe.
|
||||
|
||||
Die Laenge des Zwischenstuecks (L_KF_mm) ist eine FOLGE der Einbauhoehe und
|
||||
steht darum nicht eigens im Schluessel.
|
||||
"""
|
||||
attribs = block.get("attribs", {})
|
||||
return (
|
||||
attribs.get("WINKEL", "26"), # 359
|
||||
attribs.get("STANDARD_STEUERUNG", "0"), # 361
|
||||
attribs.get("FOERDERERTYP", ""), # 350
|
||||
attribs.get("H_EINBAUHOEHE_mm", "0"), # 360
|
||||
attribs.get("FARBE", ""), # 354
|
||||
attribs.get("ANTRIEBSMOTOR", ""), # 355
|
||||
attribs.get("ANTRIEBSART", ""), # 357
|
||||
attribs.get("MOVIMOT", "0"), # 356
|
||||
attribs.get("VERBINDUNGSELEMENTE", "Nein"), # 391
|
||||
attribs.get("ARTINR", ""),
|
||||
)
|
||||
|
||||
|
||||
def gefaellestrecke_signature(block):
|
||||
"""
|
||||
Schluessel zur Gruppierung gleichartiger Gefaellestrecken: Bloecke mit
|
||||
identischen Massen (Hoehe, Laenge, Winkel, Separatoren, Scanner, Typ,
|
||||
Geruest fuer Einzelmodul/Geruestoption) landen in derselben Sivas-Zeile -
|
||||
nur IDs/Bezeichnung unterscheiden sich.
|
||||
Geruest fuer Einzelmodul/Geruestoption, AS-/ES-Element) landen in
|
||||
derselben Sivas-Zeile - nur IDs/Bezeichnung unterscheiden sich.
|
||||
|
||||
AS-/ES-Element (Merkmale 4427-4432) gehoeren mit in den Schluessel: die
|
||||
Merkmale der Zeile entstehen aus dem ERSTEN Block der Gruppe, zwei sonst
|
||||
baugleiche Gefaellestrecken mit AS links bzw. AS rechts wuerden sonst zu
|
||||
einer Zeile mit nur einer der beiden Seiten verschmelzen (siehe
|
||||
as_es_signatur).
|
||||
"""
|
||||
attribs = block.get("attribs", {})
|
||||
return (
|
||||
@@ -209,6 +355,7 @@ def gefaellestrecke_signature(block):
|
||||
attribs.get("TYP", "Gefaellestrecke"),
|
||||
attribs.get("GERUEST_EINZELMODUL", "0"),
|
||||
attribs.get("GERUEST_TYP", "Schoenenberger Geruest"),
|
||||
as_es_signatur(attribs),
|
||||
)
|
||||
|
||||
|
||||
@@ -292,6 +439,20 @@ def build_variofoerderer_details(block):
|
||||
details["4354"] = vf_richtung[0].lower() if vf_richtung else ""
|
||||
details["4386"] = str(int(vf_winkel[0])) if vf_winkel else ""
|
||||
|
||||
# Hoehe je Foerdereinheit (4433-4437, "Variofoerderer 001-005 - Hoehe in
|
||||
# Meter"). Quelle ist das Attribut VF_HOEHE_m: eine kommagetrennte Liste
|
||||
# in Bau-Reihenfolge, je Eintrag der Mittelwert aus KS_EIN der
|
||||
# Umlenkstation und KS_AUS der Motorstation (siehe vf-vfhoehe-anfang/-ende
|
||||
# in Lisp/vf_core.lsp). Einheit Meter, 3 Nachkommastellen - wie L_VF_m.
|
||||
#
|
||||
# Mehr als fuenf Foerdereinheiten kann Sivas hier nicht abbilden; die
|
||||
# ueberzaehligen werden bewusst weggelassen statt zusammengefasst, damit
|
||||
# kein Merkmal einen erfundenen Wert traegt. Fehlende Eintraege bleiben
|
||||
# ganz weg (kein leeres Merkmal).
|
||||
vf_hoehen = parse_float_list(attribs.get("VF_HOEHE_m", ""))
|
||||
for nummer, hoehe in zip(VF_HOEHE_MERKMALE, vf_hoehen):
|
||||
details[nummer] = "%.3f" % hoehe
|
||||
|
||||
return {k: str(v) for k, v in details.items()}
|
||||
|
||||
|
||||
@@ -299,6 +460,7 @@ SIVAS_MAPPING_KREISEL = sivas_mapping.load_section_mapping("kreisel")
|
||||
SIVAS_MAPPING_ECKRAD = sivas_mapping.load_section_mapping("eckrad")
|
||||
SIVAS_MAPPING_GERUEST = sivas_mapping.load_section_mapping("geruest")
|
||||
SIVAS_MAPPING_WEICHEN = sivas_mapping.load_section_mapping("weichen")
|
||||
SIVAS_MAPPING_KETTENFOERDERER = sivas_mapping.load_section_mapping("kettenfoerderer")
|
||||
|
||||
|
||||
def build_kreisel_details(block):
|
||||
@@ -326,6 +488,21 @@ def build_eckrad_details(block):
|
||||
return sivas_mapping.anwenden(SIVAS_MAPPING_ECKRAD, attribs, TRANSFORMS)
|
||||
|
||||
|
||||
def build_kettenfoerderer_details(block, anzahl=1):
|
||||
"""Merkmale-Dict fuer eine Gruppe baugleicher Kettenfoerderer (KF_n,
|
||||
Lisp/KettenFoerderer.lsp).
|
||||
|
||||
350/359/354/356/360/391 kommen flach aus cfg/sivas.cfg [kettenfoerderer].
|
||||
353 ("Anzahl der gleichen Schraegfoerderer") entsteht erst hier aus der
|
||||
Gruppengroesse und kann darum nicht im Mapping stehen - gleiche Bauart wie
|
||||
3510/3512 beim Kreisel.
|
||||
"""
|
||||
attribs = block.get("attribs", {})
|
||||
details = sivas_mapping.anwenden(SIVAS_MAPPING_KETTENFOERDERER, attribs, TRANSFORMS)
|
||||
details["353"] = str(anzahl)
|
||||
return details
|
||||
|
||||
|
||||
# def build_geruest_details(block):
|
||||
#"""Merkmale-Dict fuer eine GR_-Geruest-Baugruppe (siehe pattern_geruest in
|
||||
#cfg/export.cfg, GeruestInsert.lsp *geruest-attrib-defs*). ARTINR/
|
||||
@@ -444,6 +621,8 @@ def process_blocks(blocks, anzahl_weichen_ils=0):
|
||||
btmt_beladung_ids = [] # IDs aller BTMT-Beladung*-Bloecke (eine gruppierte Zeile)
|
||||
btmt_entladung_ids = [] # IDs aller SC_Entladung*-Bloecke (eine gruppierte Zeile)
|
||||
geruest_list = [] # Liste von GR_-Geruest-Baugruppenbloecken, je einer Zeile
|
||||
# Kettenfoerderer: kettenfoerderer_signature(block) -> {"block", "ids", "anzahl"}
|
||||
kettenfoerderer_groups = {}
|
||||
|
||||
counters = {
|
||||
"anzahl_kreisel": 0,
|
||||
@@ -466,6 +645,7 @@ def process_blocks(blocks, anzahl_weichen_ils=0):
|
||||
"anzahl_btbeladung": 0,
|
||||
"anzahl_abwurfstationen": 0,
|
||||
"anzahl_geruest": 0,
|
||||
"anzahl_kettenfoerderer": 0,
|
||||
}
|
||||
|
||||
for block in blocks:
|
||||
@@ -515,6 +695,22 @@ def process_blocks(blocks, anzahl_weichen_ils=0):
|
||||
counters["anzahl_geruest"] += 1
|
||||
continue
|
||||
|
||||
# --- Kettenfoerderer / Schraegfoerderer Stahl (KF_n) ---
|
||||
# MUSS vor dem allgemeinen Omniflo-Zweig stehen: die KF-Baugruppe
|
||||
# traegt TEILEART "Omniflo Kettenfoerderer ..." und wuerde dort sonst
|
||||
# ueber ARTINR aufgeloest - ihre eigenen Merkmale (350/353/354/356/
|
||||
# 359/360/391) gingen dabei verloren. Anders als das Geruest werden
|
||||
# BAUGLEICHE Foerderer zu einer Zeile zusammengefasst, weil Merkmal
|
||||
# 353 genau diese Stueckzahl verlangt.
|
||||
if matches_any(bname, BLOCKPATTERNS.get("pattern_kettenfoerderer", [])):
|
||||
sig = kettenfoerderer_signature(block)
|
||||
if sig not in kettenfoerderer_groups:
|
||||
kettenfoerderer_groups[sig] = {"block": block, "ids": [], "anzahl": 0}
|
||||
kettenfoerderer_groups[sig]["anzahl"] += 1
|
||||
kettenfoerderer_groups[sig]["ids"].append(get_id(block))
|
||||
counters["anzahl_kettenfoerderer"] += 1
|
||||
continue
|
||||
|
||||
# --- Omniflo Gerade (AP110) ---
|
||||
# VOR dem allgemeinen Omniflo-Zweig: Geraden tragen ggf. ebenfalls
|
||||
# TEILEART "Omniflo ...", brauchen aber die Laengensumme.
|
||||
@@ -773,7 +969,7 @@ def process_blocks(blocks, anzahl_weichen_ils=0):
|
||||
"sivasnr": artinr,
|
||||
"sivasnr_quoted": True,
|
||||
"bezeichnung": attribs.get("BEZEICHNUNG", ""),
|
||||
"dispgruppe": 20,
|
||||
"dispgruppe": 30,
|
||||
"anzahl": anzahl,
|
||||
"laenge_mm": attribs.get("LAENGE", ""),
|
||||
"ids": [get_id(block)],
|
||||
@@ -781,6 +977,31 @@ def process_blocks(blocks, anzahl_weichen_ils=0):
|
||||
})
|
||||
nr += 1
|
||||
|
||||
# Kettenfoerderer / Schraegfoerderer Stahl (KF_n) - eine Zeile je Gruppe
|
||||
# BAUGLEICHER Foerderer, deren Groesse als Merkmal 353 mitgefuehrt wird.
|
||||
# Die Sivasnr kommt - wie beim Geruest - aus dem ARTINR-Attribut (gepflegt
|
||||
# als "sivas_id" je Variante in data/json/kettenfoerderer.json); ist dort
|
||||
# nichts hinterlegt, bleibt die Spalte leer und die Zeile traegt allein
|
||||
# ihre Merkmale. Die drei Bausteine des Foerderers (Umlenkstation,
|
||||
# skaliertes Zwischenstueck, Antriebsstation) bekommen KEINE eigenen
|
||||
# Zeilen - der Schraegfoerderer ist in Sivas EIN Artikel.
|
||||
for gruppe in kettenfoerderer_groups.values():
|
||||
block = gruppe["block"]
|
||||
attribs = block.get("attribs", {})
|
||||
items.append({
|
||||
"nr": nr,
|
||||
"teileart": attribs.get("TEILEART") or attribs.get("FOERDERERTYP") or KF_TEILEART,
|
||||
"sivasnr": attribs.get("ARTINR", ""),
|
||||
"sivasnr_quoted": True,
|
||||
"bezeichnung": attribs.get("Bezeichnung", ""),
|
||||
"dispgruppe": 20,
|
||||
"anzahl": gruppe["anzahl"],
|
||||
"laenge_mm": attribs.get("L_KF_mm", ""),
|
||||
"ids": gruppe["ids"],
|
||||
"details": build_kettenfoerderer_details(block, gruppe["anzahl"]),
|
||||
})
|
||||
nr += 1
|
||||
|
||||
# VarioFoerderer-Compound-Bloecke (VF_N) - je Block eine eigene Zeile mit
|
||||
# vollstaendigen Gefaelle-/Angetrieben-Teilgruppen (immer TYP "Streckengruppe").
|
||||
for block in vario_list:
|
||||
@@ -964,6 +1185,13 @@ def zusammengefasster_dateiname(output_csv):
|
||||
return f"{stamm}_zusammengefasst{endung}"
|
||||
|
||||
|
||||
def dispogruppen_dateiname(output_csv, dispo):
|
||||
"""z.B. "500651_sivas.csv", "10" -> "500651_sivas_zusammengefasst_dispo10.csv"."""
|
||||
stamm, endung = os.path.splitext(output_csv)
|
||||
sauber = re.sub(r"[^0-9A-Za-z_-]", "_", str(dispo)) or "leer"
|
||||
return f"{stamm}_zusammengefasst_dispo{sauber}{endung}"
|
||||
|
||||
|
||||
def items_zusammenfassen(items):
|
||||
"""Fasst Zeilen zusammen, die sich nur in Nr, Anzahl und IDs
|
||||
unterscheiden: Anzahl wird addiert, die IDs werden (ohne Dubletten, in
|
||||
@@ -1041,6 +1269,18 @@ def main():
|
||||
print(f"[export_sivas] Zusammengefasste CSV geschrieben: {output_csv_zus} "
|
||||
f"({len(items_zus)} Zeilen)")
|
||||
|
||||
# Je Dispogruppe eine eigene zusammengefasste Datei, z.B.
|
||||
# "<stamm>_zusammengefasst_dispo10.csv" (Nr je Datei neu gezaehlt).
|
||||
for dispo in sorted({str(i["dispgruppe"]) for i in items_zus}):
|
||||
teil = [i for i in items_zus if str(i["dispgruppe"]) == dispo]
|
||||
teil = [dict(i, nr=n) for n, i in enumerate(teil, 1)]
|
||||
pfad = dispogruppen_dateiname(output_csv, dispo)
|
||||
with open(pfad, "w", encoding="utf-8") as f:
|
||||
f.write(header + "\n")
|
||||
for item in teil:
|
||||
f.write(format_csv_line(item) + "\n")
|
||||
print(f"[export_sivas] Dispogruppe {dispo}: {pfad} ({len(teil)} Zeilen)")
|
||||
|
||||
# Zweite, lesbare Kopie fuer die manuelle Pruefung (Merkmalnummer +
|
||||
# Bezeichnung aus cfg/Merkmalsstamm_in_Sivas.xlsx) - wird von Sivas
|
||||
# selbst NICHT eingelesen, nur output_csv ist der eigentliche Import.
|
||||
|
||||
@@ -9,7 +9,7 @@ Hintergrund: data/polylines.dxf enthaelt 5 LWPOLYLINE-Objekte auf Layer
|
||||
"Z-6" - Streckenzuege, die reale Foerderer-Pfade der HundM05-Zeichnung
|
||||
nachbilden (siehe data/polylines.md, von Hand ausgewertet). Dieses Skript
|
||||
macht dieselbe Auswertung reproduzierbar und maschinenlesbar, als Grundlage
|
||||
fuer einen neuen Testfall (siehe tests/testdata/object_data.md fuer das
|
||||
fuer einen neuen Testfall (siehe doc/json-input-lisp.md fuer das
|
||||
allgemeine JSON-Schema).
|
||||
|
||||
Eine Polylinie kann auf einer gekippten Ebene liegen (Gruppencode
|
||||
|
||||
+25
-5
@@ -97,14 +97,31 @@ def load_flat_mapping(section, cfg_path=None):
|
||||
return {}
|
||||
|
||||
|
||||
def braucht_attribs(fn):
|
||||
"""Markiert einen Transform, der NEBEN seinem Rohwert auch das komplette
|
||||
Attribut-Dict des Blocks braucht - er wird dann als fn(wert, attribs)
|
||||
aufgerufen statt als fn(wert).
|
||||
|
||||
Noetig fuer Merkmale, deren Wert von einem ZWEITEN Attribut abhaengt: die
|
||||
AS-/ES-Seite (4428/4430) darf nur gefuellt werden, wenn der zugehoerige
|
||||
WINKEL_AS/WINKEL_ES ueberhaupt ein Element ausweist. Ohne diese Klammer
|
||||
muessten solche Merkmale wieder aus der Config heraus in Python-Code
|
||||
wandern - genau das soll sivas.cfg vermeiden.
|
||||
"""
|
||||
fn.braucht_attribs = True
|
||||
return fn
|
||||
|
||||
|
||||
def anwenden(section_mapping, attribs, transforms):
|
||||
"""Wendet eine per load_section_mapping() gelesene Zuordnung auf ein
|
||||
Block-Attribut-Dict an.
|
||||
|
||||
transforms: dict transform_name -> Funktion(wert) -> wert. Ein in der
|
||||
Config genannter, aber hier unbekannter Transform-Name wird ignoriert
|
||||
(Rohwert bleibt unveraendert) statt abzubrechen - ein Tippfehler in
|
||||
sivas.cfg darf den Export nicht zum Stillstand bringen.
|
||||
transforms: dict transform_name -> Funktion(wert) -> wert. Ein mit
|
||||
@braucht_attribs markierter Transform wird stattdessen als
|
||||
fn(wert, attribs) aufgerufen und sieht damit die uebrigen Attribute des
|
||||
Blocks. Ein in der Config genannter, aber hier unbekannter Transform-Name
|
||||
wird ignoriert (Rohwert bleibt unveraendert) statt abzubrechen - ein
|
||||
Tippfehler in sivas.cfg darf den Export nicht zum Stillstand bringen.
|
||||
|
||||
Rueckgabe: dict merkmal -> wert. Ohne Transform wird der Rohwert zu str()
|
||||
gewandelt (bisheriges Verhalten der build_*_details-Funktionen: alles
|
||||
@@ -124,7 +141,10 @@ def anwenden(section_mapping, attribs, transforms):
|
||||
if transform:
|
||||
fn = transforms.get(transform)
|
||||
if fn is not None:
|
||||
ergebnis[merkmal] = fn(wert)
|
||||
if getattr(fn, "braucht_attribs", False):
|
||||
ergebnis[merkmal] = fn(wert, attribs)
|
||||
else:
|
||||
ergebnis[merkmal] = fn(wert)
|
||||
continue
|
||||
ergebnis[merkmal] = str(wert)
|
||||
return ergebnis
|
||||
|
||||
@@ -65,6 +65,11 @@ PREFIX = "[vf_journal_export]"
|
||||
|
||||
XDATA_APP = "SSG_VF_EDIT"
|
||||
MARKER = "linienzug"
|
||||
# Marker -> Modus-Nummer. "linienzug" (Modus 1) hat ein Frage-Antwort-
|
||||
# Protokoll (Grammatik in vf_linienzug_grammatik.py); "linienzug2"/"3" werden
|
||||
# als Rohtokens ausgegeben - fuer sie gibt es bewusst keine Grammatik, ihre
|
||||
# SEGS-Eintraege machen sie ohnehin selbst-enthalten.
|
||||
MARKER_MODUS = {"linienzug": 1, "linienzug2": 2, "linienzug3": 3}
|
||||
|
||||
# Steuerzeichen-Notation des Eintrags-Trenners im ASCII-DXF (siehe Kopf).
|
||||
TRENNER = "^J"
|
||||
@@ -322,6 +327,19 @@ class ProtokollSink(Sink):
|
||||
self.add("point_abs", "getpoint Startpunkt der Kette (absoluter "
|
||||
"Weltpunkt, einziger Anker der ganzen Kette)", wert=punkt)
|
||||
|
||||
def as_achse(self, punkt):
|
||||
# Zweiter absoluter Punkt - gibt NUR die Richtung der Zufuehrung,
|
||||
# der Startpunkt bleibt der Sitz des AS-Elements. "nil" heisst: die
|
||||
# Frage wurde gestellt und bewusst uebergangen (die Kettenrichtung
|
||||
# kommt dann aus dem naechsten Klick).
|
||||
if punkt is None:
|
||||
self.add("nil", "getpoint Zufuehrungsachse - 2. Punkt: keiner "
|
||||
"gewaehlt, Richtung kommt aus dem naechsten Klick")
|
||||
else:
|
||||
self.add("point_abs", "getpoint Zufuehrungsachse - 2. Punkt "
|
||||
"(gibt nur die Richtung, der Startpunkt bleibt)",
|
||||
wert=punkt)
|
||||
|
||||
def linie(self, dl, hz, frage):
|
||||
was = _vorlage(_LINIE_TEXT, frage)
|
||||
if hz is None:
|
||||
@@ -541,6 +559,58 @@ def baue_testfaelle(inserts, strecken, startpunkt_fix, quelle_datei,
|
||||
return faelle
|
||||
|
||||
|
||||
def baue_tokenfaelle(inserts, quelle_datei):
|
||||
"""Modus-2/3-Ketten als TOKEN-Records (flache Liste, Kopf + Eintraege).
|
||||
|
||||
Warum nicht wie Modus 1 als Frage-Antwort-Protokoll: dafuer braeuchte es
|
||||
eine Grammatik des Modus-2/3-Fragenflusses, die es (bewusst, siehe
|
||||
doc/todo/spec-modus2-3.md) nicht gibt. Die Tokens selbst sind aber bereits
|
||||
aussagekraeftig - vor allem traegt SEGS die komplette Segment-Essenz, womit
|
||||
die Kette OHNE Zeichnung abspielbar ist.
|
||||
|
||||
Ausgabeformat ist genau das, was tests/test_linienzug.lsp
|
||||
(c:TEST_LINIENZUG_MODUS2) einliest:
|
||||
{"art": "PT", "x": .., "y": .., "z": ..}
|
||||
{"art": "REAL", "wert": ..} {"art": "INT", "wert": ..}
|
||||
{"art": "STR", "wert": ".."} {"art": "SEGS", "wert": ".."}
|
||||
Die Metadaten-Zeile (beschreibung/erwartung_*) ist bewusst NICHT dabei -
|
||||
die wird je Testfall von Hand gepflegt und soll beim Neuerzeugen nicht
|
||||
stillschweigend ueberschrieben werden.
|
||||
"""
|
||||
faelle = []
|
||||
for nr, e in enumerate(sorted(inserts, key=lambda i: i["x"]), start=1):
|
||||
toks = tokens_aus_journal(e["journal_roh"])
|
||||
modus = MARKER_MODUS.get(e["marker"], 0)
|
||||
kopf = {
|
||||
"test_id": "VF_%s_M%d_%02d" % (TEST_ID_ANLAGE, modus, nr),
|
||||
"anlagetyp": e["marker"],
|
||||
"modus": modus,
|
||||
"anzahl_eingaben": len(toks),
|
||||
"block": e["block"],
|
||||
"dxf_handle": e["handle"],
|
||||
"beschreibung": ("Linienzug Modus %d, %d Journal-Eintraege - "
|
||||
"Rohtokens, siehe doc/todo/spec-modus2-3.md"
|
||||
% (modus, len(toks))),
|
||||
"quelle": ("INSERT %s (Block %s) in %s, XDATA-App %s, Marker %s"
|
||||
% (e["handle"], e["block"],
|
||||
os.path.basename(quelle_datei), XDATA_APP,
|
||||
e["marker"])),
|
||||
}
|
||||
faelle.append(kopf)
|
||||
for art, wert in toks:
|
||||
if art == "PT":
|
||||
x, y, z = [float(v) for v in wert.split(",")]
|
||||
faelle.append({"art": "PT", "x": round(x, RUND),
|
||||
"y": round(y, RUND), "z": round(z, RUND)})
|
||||
elif art == "REAL":
|
||||
faelle.append({"art": "REAL", "wert": float(wert)})
|
||||
elif art == "INT":
|
||||
faelle.append({"art": "INT", "wert": int(wert)})
|
||||
else:
|
||||
faelle.append({"art": art, "wert": wert})
|
||||
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
|
||||
@@ -585,9 +655,20 @@ def main():
|
||||
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)
|
||||
# Nach Modus trennen: nur Modus 1 hat eine Grammatik. Frueher lief die
|
||||
# Modus-1-Grammatik auch ueber "linienzug2"/"3" (der startswith-Filter
|
||||
# laesst sie durch) und brach mit "Token 6: STEP erwartet" ab.
|
||||
m1 = [e for e in inserts if MARKER_MODUS.get(e["marker"]) == 1]
|
||||
m23 = [e for e in inserts if MARKER_MODUS.get(e["marker"]) in (2, 3)]
|
||||
faelle = []
|
||||
if m1:
|
||||
faelle.extend(baue_testfaelle(m1, strecken,
|
||||
not args.kein_startpunkt_fix, args.dxf,
|
||||
args.output))
|
||||
if m23:
|
||||
print("%s %d Modus-2/3-Kette(n) als Rohtokens (keine Grammatik, "
|
||||
"siehe doc/todo/spec-modus2-3.md)" % (PREFIX, len(m23)))
|
||||
faelle.extend(baue_tokenfaelle(m23, args.dxf))
|
||||
koepfe = [f for f in faelle if "test_id" in f]
|
||||
for f in koepfe:
|
||||
print("%s %s %s (%s) %d Eingaben"
|
||||
|
||||
@@ -287,6 +287,12 @@ class Sink(object):
|
||||
def praeambel_start(self):
|
||||
"""Kettenkopf beginnt (Startpunkt/Starthoehe/AS-Element)."""
|
||||
|
||||
def as_achse(self, punkt):
|
||||
"""Zufuehrungsachse des AS-Elements verbraucht (2. Punkt, gibt nur
|
||||
die Richtung). punkt = [x, y, z] oder None, wenn die Kette bewusst
|
||||
OHNE Achse gebaut wurde. Wird gar nicht gerufen, wenn das Journal
|
||||
diese Frage noch nicht kennt (vor 2026-10-21)."""
|
||||
|
||||
def praeambel_ende(self):
|
||||
"""Kettenkopf fertig."""
|
||||
|
||||
@@ -518,9 +524,31 @@ class Kern(object):
|
||||
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")
|
||||
# Drei Journal-Formate, unterscheidbar an der Eintragsart (siehe
|
||||
# vfl-frage-as-winkel-seite-oder-achse):
|
||||
# PT direkt nach dem Winkel -> Alt-Achse ohne Seitenfrage
|
||||
# (30-Grad-Element, 2026-10-14
|
||||
# bis 10-20); die Seite wurde
|
||||
# damals aus der Lage des
|
||||
# naechsten Punkts ermittelt.
|
||||
# STR, dann PT bzw. NIL -> Format ab 2026-10-21: Seite UND
|
||||
# (optionale) Zufuehrungsachse.
|
||||
# STR, dann etwas anderes -> Altbestand ohne Achsenfrage.
|
||||
if self.art() == "PT":
|
||||
self._as_achse()
|
||||
else:
|
||||
self._code("as_seite", SEITE, "AS-Seite", "as_seite")
|
||||
if self.art() == "PT":
|
||||
self._as_achse()
|
||||
elif self.art() == "NIL":
|
||||
self.nimm("NIL")
|
||||
self.sink.as_achse(None)
|
||||
self.sink.praeambel_ende()
|
||||
|
||||
def _as_achse(self):
|
||||
"""Zufuehrungsachse: zweiter Punkt, der NUR die Richtung gibt."""
|
||||
self.sink.as_achse([float(v) for v in self.nimm("PT").split(",")])
|
||||
|
||||
def sektionen(self):
|
||||
while self.pos < len(self.tok):
|
||||
label = self.nimm("STEP")
|
||||
|
||||
@@ -87,6 +87,11 @@ def tokens_aus_protokoll(eintraege):
|
||||
tok.append(("STR", str(e["wert"])))
|
||||
elif typ == "step":
|
||||
tok.append(("STEP", str(e["wert"])))
|
||||
elif typ == "nil":
|
||||
# Eine Frage, die gestellt und bewusst uebergangen wurde (bisher
|
||||
# nur die Zufuehrungsachse des AS-Elements) - im Journal ein
|
||||
# "NIL"-Eintrag ohne Wert.
|
||||
tok.append(("NIL", None))
|
||||
else:
|
||||
raise ValueError("unbekannter Eintragstyp: %r" % typ)
|
||||
return tok
|
||||
@@ -255,6 +260,14 @@ class SpecSink(Sink):
|
||||
def punkt_abs(self, punkt):
|
||||
self.ziel["punkt"] = punkt
|
||||
|
||||
def as_achse(self, punkt):
|
||||
# Zufuehrungsachse des AS-Elements. "ohne" ist NICHT dasselbe wie ein
|
||||
# fehlendes Feld: "ohne" heisst "die Frage wurde gestellt und bewusst
|
||||
# uebergangen" (NIL im Journal), ein fehlendes Feld heisst "das
|
||||
# Journal kennt die Frage gar nicht" (vor 2026-10-21). Nur so bleibt
|
||||
# der Rundlauf Journal -> Spec -> Journal tokengleich.
|
||||
self.ziel["as_achse"] = punkt if punkt is not None else "ohne"
|
||||
|
||||
def linie(self, dl, hz, frage):
|
||||
self.ziel["dl"] = dl
|
||||
if hz is not None:
|
||||
@@ -398,6 +411,14 @@ class SpecSchreiber(object):
|
||||
self.raus("STR", str(self.feld(start, "as_winkel")))
|
||||
self.raus("STR", code_aus_wert(SEITE, self.feld(start, "as_seite"),
|
||||
"as_seite"))
|
||||
# Optional, Gegenstueck zu SpecSink.as_achse und zu
|
||||
# vsp-praeambel in Lisp/vf_spec.lsp: Liste -> PT, "ohne" -> NIL,
|
||||
# fehlt -> gar kein Token (Journalformat vor 2026-10-21).
|
||||
achse = start.get("as_achse")
|
||||
if isinstance(achse, (list, tuple)):
|
||||
self.raus("PT", ",".join(_zahl(v) for v in achse))
|
||||
elif achse == "ohne":
|
||||
self.raus("NIL", None)
|
||||
|
||||
def sektion(self, sek, erster, naechste=None):
|
||||
label = self.feld(sek, "glied")
|
||||
@@ -641,6 +662,11 @@ def flache_objekte(spec_id, spec, kopf_extra=None):
|
||||
if start["as"]:
|
||||
kopf["as_winkel"] = start["as_winkel"]
|
||||
kopf["as_seite"] = start["as_seite"]
|
||||
# Nur schreiben, wenn vorhanden - ein fehlendes Feld ist die
|
||||
# bewusste Aussage "Journalformat ohne Achsenfrage" (siehe
|
||||
# SpecSink.as_achse), kein Datenverlust.
|
||||
if "as_achse" in start:
|
||||
kopf["as_achse"] = start["as_achse"]
|
||||
for k, v in (kopf_extra or collections.OrderedDict()).items():
|
||||
kopf[k] = v
|
||||
objekte.append(kopf)
|
||||
|
||||
Reference in New Issue
Block a user