tro_annotate --anlage: Staubereiche in der Zeichnung, --read-jams zurueck
Mit --anlage <csv>_anlage.json zeichnet tro_annotate je Staubereich einen Block JAM_AREA (Layer TRO_JAM): Rechteck mit dem Namen Jam<NNNN>_<k> wie in der SCL, jam_versatz vor dem Separator Richtung Vorgaenger. Attribute CONVEYOR, NUMBER, TRO + ENTRY (stabiler Schluessel), AREA, AUTO. Doppelklick oeffnet den Dialog JAM_EDIT (dxfmakros). --read-jams uebernimmt im Dialog geaenderte Nummern aus der beschrifteten Zeichnung nach cfg/<csv>_jam_names.ini (nur Abweichungen von AUTO); scl_skeleton wendet die Datei mit der Anlagen-JSON an (scl_context: jam_numbers + Korrekturen, load/save_jam_overrides). Dematic: 25 Staubereiche gezeichnet; Rundreise TRO20 -> Jam0054_9 bis in die SCL geprueft. Gemeinsame Zeichnung mit --offset 0,0 beschriften (automatischer Lagebezug waehlte dy +360 mm). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+256
-6
@@ -85,6 +85,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import collections
|
||||
import configparser
|
||||
import csv as csv_module
|
||||
import fnmatch
|
||||
import json
|
||||
@@ -120,6 +121,17 @@ from tro_flow import (
|
||||
resolve_ids_file,
|
||||
)
|
||||
from tro_ids import TroIds
|
||||
from model import ANLAGE_SCHEMA, AnlageDocument, JamArea
|
||||
from scl_context import (
|
||||
JAM_NAMES_SUFFIX,
|
||||
HwSource,
|
||||
flow_order,
|
||||
jam_key,
|
||||
jam_label,
|
||||
jam_numbers,
|
||||
load_jam_overrides,
|
||||
save_jam_overrides,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
@@ -131,12 +143,18 @@ BLOCK_PREFIX = "TRO_SYM_"
|
||||
# Verbindungsblock. Der Name beginnt mit CONNECTION_, damit der Dispatcher
|
||||
# SSG_BLOCKEDIT in dxfmakros ihn ueber "CONNECTION_*" erkennt.
|
||||
CONNECTION_BLOCK = "CONNECTION_ARROW"
|
||||
# Staubereich (--anlage). Der Name beginnt mit JAM_, damit SSG_BLOCKEDIT ihn
|
||||
# ueber "JAM_*" an den Dialog JAM_EDIT (dxfmakros/Lisp/Jam_Edit.lsp) gibt.
|
||||
JAM_BLOCK = "JAM_AREA"
|
||||
# Anzeige eines Staubereichs ohne Foerderer (Nummer nicht ableitbar)
|
||||
JAM_OFFEN = "Jam????_?"
|
||||
|
||||
# Zeichnungsgroessen und Restfehler-Grenze des Lagebezugs aus cfg/parameter.ini,
|
||||
# Abschnitt [tro_annotate] (lib/parameter.py)
|
||||
DEFAULT_MARKER = get_float("tro_annotate", "default_marker")
|
||||
DEFAULT_TEXT = get_float("tro_annotate", "default_text")
|
||||
DEFAULT_MAX_RESIDUAL = get_float("tro_annotate", "default_max_residual")
|
||||
JAM_VERSATZ = get_float("tro_annotate", "jam_versatz")
|
||||
|
||||
|
||||
def hex_to_rgb(value: str) -> tuple[int, int, int]:
|
||||
@@ -335,6 +353,7 @@ def build_layers(doc, prefix: str) -> dict[str, str]:
|
||||
("weiche", "#cc8800"),
|
||||
("eingang", STATION_FARBE[BOUNDARY_ENTRY]),
|
||||
("ausgang", STATION_FARBE[BOUNDARY_EXIT]),
|
||||
("jam", "#7030a0"),
|
||||
("legend", "#808080")):
|
||||
name = f"{prefix}{key.upper()}"
|
||||
names[key] = name
|
||||
@@ -702,6 +721,150 @@ def draw_connections(
|
||||
return drawn
|
||||
|
||||
|
||||
def build_jam_symbol(doc, text: float) -> str:
|
||||
"""
|
||||
Blockdefinition fuer einen Staubereich: Rechteck, der Name mittig darin.
|
||||
|
||||
Attribute:
|
||||
NAME sichtbar - Jam0054_3 (Foerderer + laufende Nummer)
|
||||
CONVEYOR unsichtbar - Foerderer NNNN (Kreisel-/Vario-Motor), im Dialog
|
||||
NUMBER unsichtbar - laufende Nummer k auf dem Foerderer, im Dialog
|
||||
TRO unsichtbar - TRO, in den der Staubereich fuehrt (TRO17)
|
||||
ENTRY unsichtbar - Eingang des TRO (1, bei 2Sep-Typen auch 2)
|
||||
AREA unsichtbar - Kennung der TRO-JSON (J0039)
|
||||
AUTO unsichtbar - abgeleitete Nummer NNNN_k; --read-jams schreibt
|
||||
nur, was davon abweicht
|
||||
TRO + ENTRY ist der stabile Schluessel (scl_context.jam_key).
|
||||
"""
|
||||
if JAM_BLOCK in doc.blocks:
|
||||
return JAM_BLOCK
|
||||
blk = doc.blocks.new(name=JAM_BLOCK)
|
||||
w, h = text * 4.2, text * 1.2
|
||||
blk.add_lwpolyline([(-w, -h), (w, -h), (w, h), (-w, h)], close=True,
|
||||
dxfattribs={"color": 256})
|
||||
name = blk.add_attdef("NAME", insert=(0, 0), height=text,
|
||||
dxfattribs={"style": "Standard", "color": 256,
|
||||
"text": JAM_OFFEN})
|
||||
name.set_placement((0, 0), align=TextEntityAlignment.MIDDLE_CENTER)
|
||||
offset = -h - text
|
||||
for tag_name in ("CONVEYOR", "NUMBER", "TRO", "ENTRY", "AREA", "AUTO"):
|
||||
blk.add_attdef(tag_name, insert=(-w, offset), height=text * 0.7,
|
||||
dxfattribs={"style": "Standard", "color": 256, "text": "",
|
||||
"invisible": 1})
|
||||
offset -= text
|
||||
return JAM_BLOCK
|
||||
|
||||
|
||||
def _jam_position(area: JamArea, anlage: AnlageDocument) -> tuple[float, float] | None:
|
||||
"""Lage eines Staubereichs (CSV-Koordinaten): vor seinem Separator.
|
||||
|
||||
Ausgangspunkt ist der Separator (sonst die TRO-Position); von dort
|
||||
JAM_VERSATZ in Richtung des ersten Vorgaenger-TRO - gegen den Fluss, wo
|
||||
die Traeger warten. Liegt der Vorgaenger naeher, nur die halbe Strecke.
|
||||
"""
|
||||
layout = anlage.layout
|
||||
part = layout.part(area.separator) if area.separator else None
|
||||
if part is not None and part.position is not None:
|
||||
x, y = part.position[0], part.position[1]
|
||||
elif area.position is not None:
|
||||
x, y = area.position[0], area.position[1]
|
||||
else:
|
||||
return None
|
||||
for vor in area.predecessors:
|
||||
tro = layout.by_id(vor)
|
||||
if tro is None or not tro.position:
|
||||
continue
|
||||
vx, vy = tro.position[0] - x, tro.position[1] - y
|
||||
dist = math.hypot(vx, vy)
|
||||
if dist > 1e-6:
|
||||
schritt = min(JAM_VERSATZ, dist / 2)
|
||||
return x + vx / dist * schritt, y + vy / dist * schritt
|
||||
return x, y
|
||||
|
||||
|
||||
def place_jams(
|
||||
msp, doc, anlage: AnlageDocument, overrides: dict[str, tuple[str, int]],
|
||||
transform: Transform, layers: dict[str, str], text: float, warnings: list[str],
|
||||
) -> int:
|
||||
"""
|
||||
Je Staubereich der Anlagen-JSON einen JAM_AREA-Block einfuegen.
|
||||
|
||||
Die Nummern sind dieselben wie in der SCL (scl_context.jam_numbers, dann
|
||||
die Korrekturen aus <csv>_jam_names.ini). Ein Staubereich ohne Foerderer
|
||||
zeigt Jam????_? - im Dialog nachtragen.
|
||||
"""
|
||||
layout = anlage.layout
|
||||
geordnet, _grund = flow_order(layout.tros)
|
||||
automatisch = jam_numbers(layout, geordnet, HwSource(anlage=anlage))
|
||||
block = build_jam_symbol(doc, text)
|
||||
gezeichnet = 0
|
||||
for area in layout.jam_areas_or_derived():
|
||||
lage = _jam_position(area, anlage)
|
||||
if lage is None:
|
||||
warnings.append(f"Staubereich {area.jam_id} ({area.tro}): keine Lage - "
|
||||
f"nicht gezeichnet")
|
||||
continue
|
||||
auto = automatisch.get(area.jam_id)
|
||||
nr, k = overrides.get(jam_key(area), auto or ("", 0))
|
||||
ref = msp.add_blockref(block, transform.to_dxf(*lage),
|
||||
dxfattribs={"layer": layers["jam"]})
|
||||
ref.add_auto_attribs({
|
||||
"NAME": jam_label(nr, k) if nr else JAM_OFFEN,
|
||||
"CONVEYOR": nr,
|
||||
"NUMBER": str(k) if nr else "",
|
||||
"TRO": area.tro,
|
||||
"ENTRY": str(area.entry),
|
||||
"AREA": area.jam_id,
|
||||
"AUTO": f"{auto[0]}_{auto[1]}" if auto else "",
|
||||
})
|
||||
tag(ref, area.tro)
|
||||
gezeichnet += 1
|
||||
return gezeichnet
|
||||
|
||||
|
||||
def read_jams(dxf_file: Path, ini_file: Path) -> tuple[dict[str, tuple[str, int]], list[str]]:
|
||||
"""Im Dialog JAM_EDIT geaenderte Staubereichs-Nummern uebernehmen.
|
||||
|
||||
Liest die JAM_AREA-Bloecke der beschrifteten Zeichnung. Weicht
|
||||
CONVEYOR_NUMBER von AUTO ab, kommt der Staubereich in die Korrekturdatei
|
||||
``<csv>_jam_names.ini``; stimmt er (wieder) mit AUTO ueberein, faellt sein
|
||||
Eintrag heraus. Eintraege zu Staubereichen, die nicht in der Zeichnung
|
||||
stehen, bleiben. Rueckgabe: (Korrekturen danach, Meldungen).
|
||||
"""
|
||||
meldungen: list[str] = []
|
||||
overrides = load_jam_overrides(ini_file)
|
||||
doc = ezdxf.readfile(str(dxf_file))
|
||||
namen: dict[str, str] = {}
|
||||
for ref in doc.modelspace().query(f'INSERT[name=="{JAM_BLOCK}"]'):
|
||||
a = {att.dxf.tag.upper(): (att.dxf.text or "").strip() for att in ref.attribs}
|
||||
key = f"{a.get('TRO', '')}.{a.get('ENTRY', '')}"
|
||||
nr, k = a.get("CONVEYOR", ""), a.get("NUMBER", "")
|
||||
if not a.get("TRO") or not a.get("ENTRY"):
|
||||
meldungen.append(f"Staubereich ohne TRO/ENTRY ({a.get('NAME', '?')}) - uebersprungen")
|
||||
continue
|
||||
if not nr and not k:
|
||||
continue
|
||||
if not (nr.isdigit() and len(nr) == 4 and k.isdigit() and int(k) > 0):
|
||||
meldungen.append(f"{key} ({a.get('AREA', '')}): Foerderer '{nr}' / Nummer '{k}' "
|
||||
f"ungueltig (erwartet NNNN und k >= 1) - uebersprungen")
|
||||
continue
|
||||
wert = f"{nr}_{int(k)}"
|
||||
if wert in namen.values():
|
||||
andere = next(s for s, w in namen.items() if w == wert)
|
||||
meldungen.append(f"{key}: Jam{wert} ist schon {andere} - doppelt vergeben")
|
||||
namen[key] = wert
|
||||
alt = overrides.get(key)
|
||||
if wert == a.get("AUTO", ""):
|
||||
if alt is not None:
|
||||
del overrides[key]
|
||||
meldungen.append(f"{key}: wieder abgeleitet (Jam{wert}) - Korrektur entfernt")
|
||||
elif alt != (nr, int(k)):
|
||||
overrides[key] = (nr, int(k))
|
||||
meldungen.append(f"{key}: Jam{wert} (abgeleitet: Jam{a.get('AUTO') or '-'})")
|
||||
save_jam_overrides(ini_file, overrides)
|
||||
return overrides, meldungen
|
||||
|
||||
|
||||
def draw_legend(
|
||||
msp, analysis: Analysis, placed: dict[str, tuple[float, float]],
|
||||
layers: dict[str, str], marker: float, text: float, source: str,
|
||||
@@ -790,6 +953,9 @@ def report(
|
||||
print(f"TROs = {len(analysis.tros)}, beschriftet: {counts['marker']}")
|
||||
print(f"Verbindungen = {counts['flow']}"
|
||||
+ (" (Block CONNECTION_ARROW mit FROM_UID/TO_UID)" if counts["flow"] else ""))
|
||||
if counts.get("jams"):
|
||||
print(f"Staubereiche = {counts['jams']} (Block {JAM_BLOCK}, Doppelklick -> "
|
||||
f"JAM_EDIT; uebernehmen mit --read-jams)")
|
||||
if counts.get("first_id"):
|
||||
print(f"Interne IDs ab = {format_internal_id(counts['first_id'])} "
|
||||
f"(ID-Raum von SSG_LIB, siehe IDSCHECK)")
|
||||
@@ -820,6 +986,9 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
epilog="Beispiele:\n"
|
||||
" tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --check\n"
|
||||
" tro_annotate.bat --file mubea.csv --dxf 500573_60_1.dxf --flow --fb\n"
|
||||
" tro_annotate.bat --file dematic_merge.csv --dxf dematic_merge..dxf --flow "
|
||||
"--anlage dematic_merge_anlage.json\n"
|
||||
" tro_annotate.bat --file dematic_merge.csv --read-jams\n"
|
||||
"\n"
|
||||
"Exit-Codes:\n"
|
||||
" 0 ok\n"
|
||||
@@ -855,6 +1024,17 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser.add_argument("--re-register", action="store_true",
|
||||
help="Lagebezug neu ermitteln, auch wenn in %%SKEL_CFG%% "
|
||||
"einer abgelegt ist.")
|
||||
parser.add_argument("--anlage", metavar="NAME",
|
||||
help="Anlagen-JSON (<csv>_anlage.json aus merge_hw_layout, in "
|
||||
"%%SKEL_RESULTS%% oder ein Pfad): zusaetzlich die "
|
||||
"Staubereiche als JAM_AREA-Bloecke einzeichnen "
|
||||
"(Jam<NNNN>_<k>, Doppelklick -> Dialog JAM_EDIT).")
|
||||
parser.add_argument("--read-jams", nargs="?", const="", default=None, metavar="DXF",
|
||||
help="Im Dialog JAM_EDIT geaenderte Staubereichs-Nummern aus der "
|
||||
"beschrifteten Zeichnung uebernehmen (Standard: "
|
||||
"<csv>_annotated.dxf in %%SKEL_RESULTS%%) und nach "
|
||||
"%%SKEL_CFG%%\\<csv>_jam_names.ini schreiben, dann beenden. "
|
||||
"scl_skeleton liest die Datei.")
|
||||
parser.add_argument("--check", action="store_true",
|
||||
help="Nur pruefen und berichten, keine Datei schreiben.")
|
||||
parser.add_argument("--emit-lisp", action="store_true",
|
||||
@@ -863,6 +1043,38 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def _main_read_jams(args, csv_file: Path) -> int:
|
||||
"""--read-jams: Nummern aus der beschrifteten Zeichnung -> <csv>_jam_names.ini."""
|
||||
results = env_dir("SKEL_RESULTS", "results")
|
||||
name = args.read_jams or args.out or f"{csv_file.stem}_annotated.dxf"
|
||||
dxf_file = Path(name)
|
||||
if not dxf_file.is_absolute() and dxf_file.parent == Path("."):
|
||||
dxf_file = results / name
|
||||
if not dxf_file.is_file():
|
||||
print(f"FEHLER: beschriftete Zeichnung nicht gefunden: {dxf_file}", file=sys.stderr)
|
||||
print(" In BricsCAD als DXF speichern (Dateiname wie beim Beschriften).",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
ini_file = env_dir("SKEL_CFG", "cfg") / f"{csv_file.stem}{JAM_NAMES_SUFFIX}"
|
||||
try:
|
||||
overrides, meldungen = read_jams(dxf_file, ini_file)
|
||||
except (OSError, ValueError, configparser.Error, ezdxf.DXFError) as exc:
|
||||
print(f"FEHLER: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
print("")
|
||||
print("================================================================")
|
||||
print("STAUBEREICHE UEBERNEHMEN")
|
||||
print("================================================================")
|
||||
print(f"Zeichnung = {dxf_file}")
|
||||
print(f"Korrekturen = {len(overrides)} -> {ini_file if overrides else '- (keine, Datei entfernt)'}")
|
||||
for m in meldungen:
|
||||
print(f" {m}")
|
||||
print("Danach: scl_skeleton mit der Anlagen-JSON neu laufen lassen.")
|
||||
print("================================================================")
|
||||
print("")
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
warnings: list[str] = []
|
||||
@@ -875,16 +1087,19 @@ def main(argv: list[str] | None = None) -> int:
|
||||
"SSG_LIB-Module (dxfmakros/Lisp), damit TRO_EDIT sie findet.")
|
||||
return 0
|
||||
|
||||
if not args.dxf:
|
||||
print("FEHLER: --dxf fehlt (nur --emit-lisp geht ohne Zeichnung).",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
|
||||
csv_file = resolve_input(args.file)
|
||||
if csv_file.suffix.lower() != ".csv":
|
||||
print(f"FEHLER: --file erwartet eine CSV-Datei, keine '{csv_file.suffix}'-Datei: "
|
||||
f"{csv_file}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if args.read_jams is not None:
|
||||
return _main_read_jams(args, csv_file)
|
||||
|
||||
if not args.dxf:
|
||||
print("FEHLER: --dxf fehlt (nur --emit-lisp und --read-jams gehen ohne "
|
||||
"Zeichnung).", file=sys.stderr)
|
||||
return 1
|
||||
if not csv_file.is_file():
|
||||
print(f"FEHLER: CSV nicht gefunden: {csv_file}", file=sys.stderr)
|
||||
return 1
|
||||
@@ -904,6 +1119,29 @@ def main(argv: list[str] | None = None) -> int:
|
||||
cfg_dir = env_dir("SKEL_CFG", "cfg")
|
||||
results.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Anlagen-JSON fuer die Staubereiche (optional)
|
||||
anlage = None
|
||||
jam_overrides: dict[str, tuple[str, int]] = {}
|
||||
if args.anlage:
|
||||
anlage_file = Path(args.anlage)
|
||||
if not anlage_file.is_absolute() and anlage_file.parent == Path("."):
|
||||
anlage_file = results / args.anlage
|
||||
try:
|
||||
text = anlage_file.read_text(encoding="utf-8")
|
||||
if json.loads(text).get("schema") != ANLAGE_SCHEMA:
|
||||
raise ValueError(f"kein Schema '{ANLAGE_SCHEMA}' - erwartet wird die "
|
||||
f"Anlagen-JSON aus merge_hw_layout")
|
||||
anlage = AnlageDocument.from_json(text)
|
||||
jam_overrides = load_jam_overrides(
|
||||
cfg_dir / f"{csv_file.stem}{JAM_NAMES_SUFFIX}")
|
||||
except (OSError, ValueError, configparser.Error) as exc:
|
||||
print(f"FEHLER: Anlagen-JSON {anlage_file} nicht lesbar: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
if Path(anlage.layout.source).name != csv_file.name:
|
||||
print(f"FEHLER: {anlage_file.name} gehoert zu '{anlage.layout.source}', "
|
||||
f"nicht zu '{csv_file.name}'.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# TROs aus dem CSV
|
||||
read_warnings: list[str] = []
|
||||
try:
|
||||
@@ -955,7 +1193,16 @@ def main(argv: list[str] | None = None) -> int:
|
||||
return 4
|
||||
|
||||
counts = {"marker": 0, "flow": 0, "removed": 0, "layers": 0, "blocks": 0,
|
||||
"first_id": 0}
|
||||
"first_id": 0, "jams": 0}
|
||||
if anlage is not None:
|
||||
aus_csv = {t.tro_id for t in analysis.tros}
|
||||
aus_json = {t.tro_id for t in anlage.layout.tros}
|
||||
if aus_csv != aus_json:
|
||||
warnings.append(
|
||||
f"TROs der Zeichnung und der Anlagen-JSON weichen ab (nur CSV: "
|
||||
f"{', '.join(sorted(aus_csv - aus_json)) or '-'}; nur JSON: "
|
||||
f"{', '.join(sorted(aus_json - aus_csv)) or '-'}) - tro_flow --json und "
|
||||
f"merge_hw_layout neu laufen lassen")
|
||||
|
||||
if args.check:
|
||||
counts["marker"] = sum(1 for t in analysis.tros if t.position)
|
||||
@@ -986,6 +1233,9 @@ def main(argv: list[str] | None = None) -> int:
|
||||
msp, doc, analysis, {**placed, **stationen}, internal, layers,
|
||||
args.marker_size, args.text_height, next_id=next_id + len(placed),
|
||||
)
|
||||
if anlage is not None:
|
||||
counts["jams"] = place_jams(msp, doc, anlage, jam_overrides, transform, layers,
|
||||
args.text_height, warnings)
|
||||
if args.legend:
|
||||
draw_legend(msp, analysis, placed, layers, args.marker_size,
|
||||
args.text_height, csv_file.name)
|
||||
|
||||
Reference in New Issue
Block a user