export_csv: AS/ES direkt aus ZZ_ASES_MARKER klassifizieren, Kreisel<->Strecke-Kollisionserzeugung entfernen
Stufe 2 (Python-Seite von "sepliste: Bauteil-IDs eintragen"): AS/ES-Kettenenden sind jetzt ganz normale Items, direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert (TeileId/Position/Boundingbox vom Marker-Block selbst) - keine Kollisionsberechnung mehr, die sie erfindet. - lib/export_neighbors.py: compute_strecke_kreisel_schleus entfernt (suchte bisher per Kollisionstest den anschliessenden Kreisel und erzeugte dabei bei JEDEM Export eine neue TeileId fuer denselben physischen Punkt). AUSSCHLEUS_/ EINSCHLEUS_TEILEART sind jetzt Teil von KREISEL_UMLAUF_TEILEARTEN - ein AS/ES bringt seine eigene Bounding-Box vom Marker mit und wird dadurch, wie jeder freie Separator, ueber die normale BBox-Ueberschneidung einem Kreisel zugeordnet; die gesonderte "naechster Kreisel per sep_kette"-Suche in compute_kreisel_umlauf (dessen sep_kette-Parameter damit entfaellt) ist ueberfluessig geworden. compute_sep_kette gibt je Eintrag zusaetzlich das Feld "id" weiter (Bauteil-TeileId, siehe vorheriger Commit). - lib/export_csv.py: process_blocks klassifiziert ZZ_ASES_MARKER direkt (TYP- Attribut waehlt Ausschleus-/Einschleuselement); build_strecke_schleus_items entfaellt komplett. map_separator_kette_items -> map_sepliste_kette_items: ordnet jetzt AS/SEP/ES exakt per ID-Gleichheit zu (sepliste-Feld "id" gegen die TeileId der klassifizierten Items) statt per Koordinaten-Naehe - SEP faellt bei fehlendem "id"-Feld weiterhin auf den alten Naehe-Abgleich zurueck (Altbestand), AS/ES ohne "id" bleiben unaufgeloest. - Neu: pruefe_ases_in_beiden_containern prueft am Ende von process_blocks, ob jede AS-/ES-Zeile sowohl in den TrackIds ihrer eigenen Strecke als auch in denen mindestens eines Kreisels auftaucht, und traegt bei Abweichung eine Warnung ein (Spalte "Warnungen") - Konsistenzpruefung fuer genau die beiden "Container", zu denen ein AS/ES gehoert. - Aufgeraeumt (tote Routinen): _strecke_ends, _closest_pt_on_segment, _distinct_kreisel_ids (nur von den entfernten Funktionen genutzt), sowie der temporaere Diagnose-Filter DEBUG_HIGHLIGHT_TEILEIDS/dbg_highlight (an die jetzt behobene Koordinaten-Matching-Ambiguitaet gebunden, in seinem eigenen Kommentar bereits als Wegwerf-Code markiert). LISP-seitiger Dead-Code-Scan (export.lsp, ssg_ks_insert.lsp - die diese Woche bearbeiteten Module): keine Funde. Verifiziert an den echten HundM05-Daten (results/export_raw.json, 45 Bloecke): TeileId-Bereich luecken- und duplikatfrei 1-45 (vorher 1-55 mit Luecke 24-33 + Zusatz-IDs 46-55), alle 10 AS/ES-IDs korrekt in ihrer Strecke UND im Kreisel 06 TrackIds, keine Warnungen der neuen Pruefung. Zwei Testdateien auf die neue Architektur umgestellt (test_export_kreisel_umlauf.py: AS/ES als Item statt Kollisions-Erzeugung, neue Klasse TestAsEsImKreiselUmlauf fuer die BBox- Teilnahme; test_export_separatorliste.py: id-basierte Zuordnung, Klasse TestMarkerIdWiederverwendung durch TestMapSeplisteKetteItemsFallback ersetzt - die alte testete inzwischen entfernte Mint-Logik). Volle Suite: 224 bestanden (4 vorbestehende, unabhaengige Fixture-Failures unveraendert). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
719ffc2385
commit
ab3958e0e4
+248
-298
@@ -41,14 +41,15 @@ from export_neighbors import (
|
||||
compute_sensor_zuordnung,
|
||||
compute_scanner_nearest_separator,
|
||||
compute_kreisel_touch_switches,
|
||||
compute_strecke_kreisel_schleus,
|
||||
compute_sep_kette,
|
||||
compute_kreisel_umlauf,
|
||||
dbg_highlight,
|
||||
SENSOR_TEILEARTEN,
|
||||
WEICHE_TEILEART,
|
||||
GF_VF_TEILEARTEN,
|
||||
KREISEL_SPLIT_TEILEART,
|
||||
KREISEL_UMLAUF_TEILEARTEN,
|
||||
AUSSCHLEUS_TEILEART,
|
||||
EINSCHLEUS_TEILEART,
|
||||
)
|
||||
|
||||
BLOCKPATTERNS = load_patterns()
|
||||
@@ -534,174 +535,44 @@ def _strecke_typ_feld(teileart):
|
||||
return "Strecke" if teileart == "ILS 2.0 Gefaellestrecke" else "Förderer"
|
||||
|
||||
|
||||
def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette=None,
|
||||
sep_kette_by_key=None, dbg=None):
|
||||
"""Haengt fuer jedes Strecken-Ende mit anschliessendem Kreisel eine
|
||||
"ILS Ausschleuselement"- (Anfang) bzw. "ILS Einschleuselement"-Zeile
|
||||
(Ende) an items an (siehe export_neighbors.compute_strecke_kreisel_schleus
|
||||
fuer Position/Boundingbox - reine Geometrie, unveraendert).
|
||||
def map_sepliste_kette_items(items, sep_kette, dbg=None):
|
||||
"""Ordnet jeden sepliste-Eintrag (AS/SEP/ES, siehe compute_sep_kette)
|
||||
einem bereits klassifizierten Item zu und liefert sie unter ihrem
|
||||
sep_kette-Schluessel zurueck - fuer die Nachbar-Aufloesung in
|
||||
resolve_umlauf_und_sepliste_nachbarn sowie die TrackIds-Spalte der
|
||||
besitzenden Strecke (siehe write_separatorliste_merkmale).
|
||||
|
||||
Muss NACH build_kreisel_weiche_items laufen, damit die TeileId-Zaehlung
|
||||
(max_numeric_teileid) auch die bereits erzeugten Weichen einschliesst.
|
||||
Anzahl 1; K1-K4/Insertpoint leer; Boundingbox = die statische, an der
|
||||
Kreiselachse ausgerichtete Default-Box.
|
||||
ERZEUGT KEINE neuen Zeilen: AS/ES liegen bereits als eigene Items vor
|
||||
(direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert, siehe
|
||||
process_blocks), SEP als die beim Export ausgepackte temporaere
|
||||
Separator_SP-Kopie (csv:sep-proxies-erzeugen, export.lsp). Beide Wege
|
||||
wuerden sonst dieselben physischen Bauteile DOPPELT zaehlen.
|
||||
|
||||
sep_kette = Rueckgabe von export_neighbors.compute_sep_kette (keys
|
||||
"<strecke_id>#<lfdnr>" -> Eintrag mit typ/vorgaenger/nachfolger). Traegt
|
||||
die zugehoerige Strecke eine "sepliste" (neue LISP-XDATA, siehe
|
||||
ssg-sepliste-xdata-schreiben), bleibt "Nachbarn" hier zunaechst LEER und
|
||||
der sep_kette-Schluessel dieser Zeile wird stattdessen in
|
||||
sep_kette_by_key (dict, vom Aufrufer bereitgestellt und HIER befuellt)
|
||||
eingetragen - process_blocks loest daraus NACH der Registrierung ALLER
|
||||
SEP/AS/ES-Schluessel (auch der von map_separator_kette_items) die
|
||||
tatsaechlichen TeileIds auf (eigener Block am Ende von process_blocks,
|
||||
direkt nach beiden Aufrufen). Ein sep_kette-Schluessel ist naemlich KEINE TeileId,
|
||||
sondern nur waehrend des Baus eindeutig -
|
||||
ihn direkt in die CSV zu schreiben waere ein Bug (zeigte frueher auf
|
||||
"<strecke_id>#<lfdnr>" statt auf den eigentlichen Nachbarn). Die
|
||||
Kreisel-/Foerderer-Zugehoerigkeit steht bei sepliste-Zeilen stattdessen
|
||||
in den Merkmalen ("Kreisel" + "Förderer"/"Strecke" je Typ, analog zum
|
||||
"Zuordnung"-Feld der Separatoren). Fehlt die sepliste (Altbestand-Wrapper
|
||||
ohne die neue XDATA), bleibt der bisherige Nachbarn-Inhalt
|
||||
[kreisel_id, strecke_id] unveraendert erhalten (sofort aufgeloest, kein
|
||||
sep_kette-Schluessel involviert).
|
||||
Zuordnung: EXAKTER ID-Abgleich. Seit csv:sepliste-ids-setzen/
|
||||
csv:asmark-ids-setzen (Lisp/export.lsp) traegt praktisch jeder sepliste-
|
||||
Eintrag bereits die TeileId des ihn vertretenden Items als Feld "id" -
|
||||
das Item mit GENAU dieser teileid IST der gesuchte Partner. Kein Rate-/
|
||||
Naeheverfahren mehr noetig, keine der frueheren Ambiguitaeten (ein
|
||||
einzelner fehlerhafter sepliste-Koordinatenwert konnte vorher per
|
||||
Naeherungssuche einen FALSCHEN Nachbarn waehlen und einen anderen
|
||||
Separator ganz ohne Zuordnung lassen - siehe Memory
|
||||
sepliste-lfdnr2-koordinaten-bug.md).
|
||||
|
||||
Rueckgabe: fortgeschriebene Elementnummer.
|
||||
Fallback (kein "id"-Feld - Altbestand-Export ohne Marker/Kopie-ID, oder
|
||||
deren Zuweisung ist in Lisp fehlgeschlagen): NUR fuer SEP-Eintraege
|
||||
weiterhin per naechstem INSERT-Punkt gegen die per Zuordnung
|
||||
vorgefilterten Separator-Items. AS/ES ohne "id" bleiben unaufgeloest -
|
||||
fuer sie existiert seit dem Wegfall der Kreisel<->Strecke-Kollisions-
|
||||
erzeugung (frueher compute_strecke_kreisel_schleus/
|
||||
build_strecke_schleus_items) ohnehin kein Item mehr, das sie vertreten
|
||||
koennte.
|
||||
|
||||
Rueckgabe: sep_kette_by_key (neues dict).
|
||||
"""
|
||||
if sep_kette is None:
|
||||
sep_kette = {}
|
||||
if sep_kette_by_key is None:
|
||||
sep_kette_by_key = {}
|
||||
schleus, warnings = compute_strecke_kreisel_schleus(
|
||||
items, durchmesser_mm=KREISEL_DURCHMESSER_MM,
|
||||
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
|
||||
sep_kette_by_key = {}
|
||||
by_teileid = {it.get("teileid", ""): it for it in items if it.get("teileid", "")}
|
||||
|
||||
strecke_by_id = {it.get("teileid", ""): it for it in items
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN}
|
||||
|
||||
# Warnungen (>2 Kreisel-BBox-Beruehrungen je Strecke) auf die Strecken-Zeile
|
||||
# schreiben (Spalte "Warnungen") und in die Konsole ausgeben.
|
||||
if warnings:
|
||||
for sid, text in warnings.items():
|
||||
if sid in strecke_by_id:
|
||||
strecke_by_id[sid]["warnung"] = text
|
||||
print(f"[export_csv] WARNUNG: Strecke {sid} {text}")
|
||||
|
||||
naechste_id = max_numeric_teileid(items)
|
||||
elem_nr = start_elem_nr
|
||||
zaehler = {}
|
||||
for sp in schleus:
|
||||
elem_nr += 1
|
||||
teileart = sp["teileart"]
|
||||
zaehler[teileart] = zaehler.get(teileart, 0) + 1
|
||||
planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg)
|
||||
if planquadrat_cfg is not None else "")
|
||||
|
||||
strecke = strecke_by_id.get(sp["strecke_id"])
|
||||
sepliste = strecke.get("sepliste") if strecke else None
|
||||
merkmale = {"Erzeugt aus": "Kreisel-Strecke-Kollision"}
|
||||
sep_kette_key = None
|
||||
marker_id = ""
|
||||
if sepliste:
|
||||
# Neue XDATA vorhanden: Nachbarn nur noch aus der Ketten-internen
|
||||
# Vorgaenger/Nachfolger-Beziehung (AS/ES ist immer das erste bzw.
|
||||
# letzte Sepliste-Element seiner Strecke). Die Keys (vorgaenger/
|
||||
# nachfolger) sind sep_kette-interne Schluessel, KEINE fertigen
|
||||
# TeileIds - erst nach dem Bau ALLER SEP/AS/ES-Zeilen dieser Kette
|
||||
# aufloesbar (siehe process_blocks, Abschnitt nach beiden Aufrufen,
|
||||
# analog zum Kreisel-Umlauf/Weichen-Muster).
|
||||
sepliste_ende = sepliste[0] if sp["ende_label"] == "Anfang" else sepliste[-1]
|
||||
lfdnr = sepliste_ende["lfdnr"]
|
||||
# ZZ_ASES_MARKER (csv:asmark-aktualisieren, Lisp/export.lsp) traegt
|
||||
# bereits eine STABILE, ueber Exportlaeufe hinweg gleichbleibende
|
||||
# ID fuer genau dieses Kettenende (schon 4-stellig formatiert,
|
||||
# ssg-id-format) - im sepliste-Eintrag als Feld "id" gespiegelt.
|
||||
# Diese ID unten WIEDERVERWENDEN statt eine neue zu minten: der
|
||||
# Marker bekommt in process_blocks bewusst KEINE eigene CSV-Zeile
|
||||
# (siehe pattern_ases_marker), seine ID bliebe sonst fuer immer
|
||||
# ungenutzt, waehrend hier bei jedem Exportlauf zusaetzlich eine
|
||||
# neue, hoehere ID fuer denselben physischen Punkt erfunden wuerde.
|
||||
marker_id = str(sepliste_ende.get("id", "")).strip()
|
||||
sep_kette_key = f"{sp['strecke_id']}#{lfdnr}"
|
||||
nachbarn = [] # in process_blocks nachgetragen
|
||||
merkmale["Kreisel"] = sp["kreisel_id"]
|
||||
merkmale[_strecke_typ_feld(strecke.get("teileart", ""))] = sp["strecke_id"]
|
||||
else:
|
||||
nachbarn = [sp["kreisel_id"], sp["strecke_id"]]
|
||||
merkmale["Kreisel"] = sp["kreisel_id"]
|
||||
merkmale["Strecke"] = sp["strecke_id"]
|
||||
merkmale["Streckenende"] = sp["ende_label"]
|
||||
|
||||
if marker_id:
|
||||
# Marker-ID uebernehmen (schon 4-stellig formatiert - NICHT
|
||||
# erneut formatieren) und den laufenden Zaehler ggf. hochziehen,
|
||||
# damit ein SPAETERER Mint in dieser Schleife (Altbestand-Wrapper
|
||||
# ohne Marker/sepliste) nie mit einer bereits wiederverwendeten
|
||||
# Marker-ID kollidiert - unabhaengig davon, in welcher Reihenfolge
|
||||
# Lisp Marker- vs. Separator-Proxy-IDs vergeben hat.
|
||||
teileid = marker_id
|
||||
if marker_id.isdigit():
|
||||
naechste_id = max(naechste_id, int(marker_id))
|
||||
else:
|
||||
# Altbestand-Wrapper ohne Marker-ID (oder Marker-ID-Zuweisung in
|
||||
# Lisp fehlgeschlagen) - unveraendertes bisheriges Verhalten.
|
||||
naechste_id += 1
|
||||
teileid = f"{naechste_id:04d}"
|
||||
|
||||
item = {
|
||||
"nr": elem_nr,
|
||||
"teileart": teileart,
|
||||
"teileid": teileid,
|
||||
"bezeichnung": f"{teileart} :{zaehler[teileart]}",
|
||||
"planquadrat": planquadrat,
|
||||
"position": f'{sp["x"]:.2f}, {sp["y"]:.2f}, {sp["z"]:.2f}',
|
||||
"boundingbox": f'{sp["dx"]:.2f}, {sp["dy"]:.2f}, {sp["dz"]:.2f}',
|
||||
"insertpoint": "",
|
||||
"k1": "", "k2": "", "k3": "", "k4": "",
|
||||
"nachbarn": ", ".join(nachbarn),
|
||||
"fehler": "",
|
||||
"merkmale": merkmale,
|
||||
}
|
||||
items.append(item)
|
||||
if sep_kette_key is not None:
|
||||
sep_kette_by_key[sep_kette_key] = item
|
||||
return elem_nr
|
||||
|
||||
|
||||
def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None):
|
||||
"""Ordnet jeden in einer VF_n/GF_n-Kette eingebetteten Separator
|
||||
(sepliste-Eintrag typ "SEP", siehe compute_sep_kette) dem BEREITS
|
||||
vorhandenen Separator-Item zu und traegt es unter seinem sep_kette-
|
||||
Schluessel in sep_kette_by_key ein - fuer die Nachbar-Aufloesung in
|
||||
resolve_umlauf_und_sepliste_nachbarn.
|
||||
|
||||
WICHTIG - erzeugt KEINE neuen Zeilen (frueher tat das
|
||||
build_separator_kette_items): die in einer Kette verpackten Separatoren
|
||||
liegen bereits als ECHTE Separator_SP-INSERTs in der Zeichnung - der
|
||||
LISP-Export fuegt fuer jedes verpackte Symbol eine temporaere Kopie ein
|
||||
(csv:sep-proxies-erzeugen, export.lsp), die den normalen freien-Separator-
|
||||
Pfad in process_blocks durchlaeuft (mit echter vla-getboundingbox-Box, ID
|
||||
und fest gesetzter ZUORDNUNG = Wrapper-ID ueber "zuordnung_fix"). Beide
|
||||
Wege wuerden sonst dieselben physischen Separatoren DOPPELT zaehlen (26
|
||||
Proxy-INSERTs + 26 sepliste-Zeilen = 64 statt 38). Die sepliste liefert
|
||||
hier nur noch die REIHENFOLGE (Vorgaenger/Nachfolger), nicht die Zeilen.
|
||||
|
||||
Zuordnung sepliste-SEP -> vorhandenes Separator-Item: ueber die
|
||||
Wrapper-ID (Item-Merkmal "Zuordnung" == sep_kette-Eintrag "strecke_id",
|
||||
fixiert durch den LISP-Proxy) UND den naechstgelegenen INSERT-Punkt
|
||||
(Item-Felder "_x"/"_y" vs. sepliste-Koordinate x/y - beide sind der rohe
|
||||
Sub-INSERT-Einfuegepunkt aus derselben Bauphase, daher deckungsgleich;
|
||||
verifiziert 26/26 <300mm). Reicht der Wrapper mehrere Separatoren, trennt
|
||||
der INSERT-Punkt sie; bei nur einem ist die Wrapper-ID schon eindeutig.
|
||||
|
||||
Ein sepliste-SEP ohne passendes Item (sollte nicht vorkommen, da der Proxy
|
||||
jeden verpackten Separator einfuegt) wird uebersprungen (dbg-Hinweis).
|
||||
"""
|
||||
if sep_kette_by_key is None:
|
||||
sep_kette_by_key = {}
|
||||
# Vorhandene Separator-Items nach Wrapper-Zuordnung gruppieren (nur die mit
|
||||
# fixierter Zuordnung = die Proxy-Kopien verpackter Ketten-Separatoren;
|
||||
# frei platzierte Separatoren tragen keine sepliste und stoeren nicht).
|
||||
# Kandidatenpool nur noch fuer den Koordinaten-Fallback (SEP ohne "id").
|
||||
sep_items_by_zuordnung = {}
|
||||
for it in items:
|
||||
if it.get("teileart", "") != "ILS 2.0 Separator":
|
||||
@@ -711,96 +582,68 @@ def map_separator_kette_items(items, sep_kette, sep_kette_by_key=None, dbg=None)
|
||||
sep_items_by_zuordnung.setdefault(zuordnung, []).append(it)
|
||||
|
||||
if dbg:
|
||||
dbg("=== map_separator_kette_items: Kandidatenpools je Zuordnung ===")
|
||||
for zuordnung, kandidaten in sep_items_by_zuordnung.items():
|
||||
dbg(f" Zuordnung {zuordnung!r}: "
|
||||
f"{[k.get('teileid', '') for k in kandidaten]}")
|
||||
dbg("")
|
||||
dbg("=== map_separator_kette_items: sepliste-SEP -> Separator-Item ===")
|
||||
|
||||
# id(item) -> Liste der sep_kette-Keys, die genau dieses Item als
|
||||
# naechsten Kandidaten gewaehlt haben - Diagnose der NICHT-exklusiven
|
||||
# Zuordnung (jeder Key sucht unabhaengig im vollen Pool, siehe Docstring):
|
||||
# waehlen zwei Keys denselben Kandidaten, bleibt ein anderer Kandidat im
|
||||
# selben Pool ohne jeden sep_kette-Eintrag und damit ohne Nachbarn/TrackIds.
|
||||
gewaehlt_von = {}
|
||||
# Zuordnungen, fuer die es ueberhaupt sepliste-SEP-Schluessel gibt. Nur in
|
||||
# DIESEN Pools ist ein nicht gewaehlter Kandidat auffaellig; frei auf einem
|
||||
# Kreisel platzierte Separatoren tragen keine sepliste und bekommen ihre
|
||||
# Nachbarn aus dem Kreisel-Umlauf (siehe resolve_umlauf_und_sepliste_nachbarn).
|
||||
zuordnungen_mit_sepliste = set()
|
||||
dbg("=== map_sepliste_kette_items: sepliste-Eintrag -> Item ===")
|
||||
|
||||
per_id = 0
|
||||
per_fallback = 0
|
||||
ohne_treffer = 0
|
||||
for key, eintrag in sep_kette.items():
|
||||
marker_id = eintrag.get("id", "")
|
||||
if marker_id:
|
||||
item = by_teileid.get(marker_id)
|
||||
if item is not None:
|
||||
sep_kette_by_key[key] = item
|
||||
per_id += 1
|
||||
if dbg:
|
||||
dbg(f" {key} (typ={eintrag.get('typ')!r}) -> Item per ID "
|
||||
f"{marker_id!r} (TeileId {item.get('teileid', '')!r})")
|
||||
continue
|
||||
if dbg:
|
||||
dbg(f" {key}: id={marker_id!r} gesetzt, aber KEIN Item mit "
|
||||
f"dieser TeileId gefunden")
|
||||
|
||||
# Fallback nur fuer SEP ohne (aufloesbares) "id"-Feld (Altbestand).
|
||||
if eintrag.get("typ") != "SEP":
|
||||
ohne_treffer += 1
|
||||
continue
|
||||
zuordnungen_mit_sepliste.add(eintrag["strecke_id"])
|
||||
kandidaten = sep_items_by_zuordnung.get(eintrag["strecke_id"], [])
|
||||
if not kandidaten:
|
||||
ohne_treffer += 1
|
||||
if dbg:
|
||||
dbg(f" sepliste-SEP {key}: kein Separator-Item mit Zuordnung "
|
||||
dbg(f" {key}: kein Separator-Item mit Zuordnung "
|
||||
f"{eintrag['strecke_id']!r} gefunden - uebersprungen")
|
||||
continue
|
||||
# Naechster INSERT-Punkt zur sepliste-Koordinate.
|
||||
sx, sy = eintrag.get("x", 0.0), eintrag.get("y", 0.0)
|
||||
entfernungen = sorted(
|
||||
((math.hypot(it.get("_x", 0.0) - sx, it.get("_y", 0.0) - sy), it)
|
||||
for it in kandidaten),
|
||||
key=lambda p: p[0])
|
||||
best_d, best = entfernungen[0]
|
||||
best = min(kandidaten,
|
||||
key=lambda it: math.hypot(it.get("_x", 0.0) - sx, it.get("_y", 0.0) - sy))
|
||||
sep_kette_by_key[key] = best
|
||||
gewaehlt_von.setdefault(id(best), []).append(key)
|
||||
per_fallback += 1
|
||||
if dbg:
|
||||
rangliste = ", ".join(f"{it.get('teileid', '')!r}={d:.0f}mm"
|
||||
for d, it in entfernungen)
|
||||
dbg(f" sepliste-SEP {key} (x={sx:.0f},y={sy:.0f}) -> Separator-Item "
|
||||
f"TeileId {best.get('teileid', '')!r} (d={best_d:.0f}mm); "
|
||||
f"Kandidaten sortiert: [{rangliste}]"
|
||||
f"{dbg_highlight(best.get('teileid', ''))}")
|
||||
dbg(f" {key} (x={sx:.0f},y={sy:.0f}) -> Naehe-Fallback auf "
|
||||
f"Separator-Item TeileId {best.get('teileid', '')!r}")
|
||||
|
||||
if dbg:
|
||||
dbg("")
|
||||
mehrfach = {iid: keys for iid, keys in gewaehlt_von.items() if len(keys) > 1}
|
||||
if mehrfach:
|
||||
dbg(" WARNUNG: folgende Separator-Items wurden von MEHREREN "
|
||||
"sepliste-Keys unabhaengig als naechster Kandidat gewaehlt "
|
||||
"(nicht-exklusive Zuordnung):")
|
||||
for keys in mehrfach.values():
|
||||
teileid = sep_kette_by_key[keys[0]].get("teileid", "")
|
||||
dbg(f" TeileId {teileid!r} gewaehlt von Keys: {keys}"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
gewaehlte_ids = set(gewaehlt_von.keys())
|
||||
for zuordnung, kandidaten in sep_items_by_zuordnung.items():
|
||||
if zuordnung not in zuordnungen_mit_sepliste:
|
||||
continue
|
||||
for it in kandidaten:
|
||||
if id(it) not in gewaehlte_ids:
|
||||
teileid = it.get("teileid", "")
|
||||
dbg(f" Separator-Item TeileId {teileid!r} (Zuordnung "
|
||||
f"{zuordnung!r}) wurde von KEINEM sepliste-Key gewaehlt "
|
||||
f"- bleibt ohne sep_kette-Eintrag, damit ohne Nachbarn/"
|
||||
f"TrackIds{dbg_highlight(teileid)}")
|
||||
dbg(f" Zuordnung: {per_id} per ID, {per_fallback} per Naehe-Fallback, "
|
||||
f"{ohne_treffer} ohne Treffer")
|
||||
return sep_kette_by_key
|
||||
|
||||
|
||||
def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
sep_kette_by_key, weiche_by_key, dbg=None):
|
||||
"""Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items/
|
||||
build_strecke_schleus_items/map_separator_kette_items zunaechst leer
|
||||
gelassen (bzw. deren Item nur registriert) haben, weil ihr(e) Nachbar(n)
|
||||
beim eigenen Bau/der Registrierung noch nicht als TeileId feststand(en)
|
||||
(Weiche vs. Separator im Kreisel-Umlauf, oder eine sepliste-Zeile, deren
|
||||
Nachfolger erst danach registriert wird).
|
||||
"""Loest die "Nachbarn"-Spalte aller Zeilen auf, die build_kreisel_weiche_items
|
||||
zunaechst leer gelassen bzw. map_sepliste_kette_items nur registriert hat,
|
||||
weil ihr(e) Nachbar(n) beim eigenen Bau/der Registrierung noch nicht als
|
||||
TeileId feststand(en) (Weiche vs. Separator im Kreisel-Umlauf, oder eine
|
||||
sepliste-Zeile, deren Nachfolger erst danach registriert wird).
|
||||
|
||||
Muss NACH allen drei Aufrufen laufen (ihre TeileIds stehen erst dann
|
||||
fest). items wird IN-PLACE veraendert (item["nachbarn"]).
|
||||
Muss NACH build_kreisel_weiche_items UND map_sepliste_kette_items laufen
|
||||
(deren TeileIds stehen erst dann fest). items wird IN-PLACE veraendert
|
||||
(item["nachbarn"]).
|
||||
|
||||
kreisel_umlauf = Rueckgabe von export_neighbors.compute_kreisel_umlauf.
|
||||
sep_kette = Rueckgabe von export_neighbors.compute_sep_kette.
|
||||
sep_kette_by_key = dict sep_kette-Schluessel -> Item (befuellt von
|
||||
build_strecke_schleus_items UND
|
||||
map_separator_kette_items - DIESELBE dict fuer beide,
|
||||
siehe process_blocks: ein sep_kette-Nachbar kann in der
|
||||
jeweils anderen Funktion registriert worden sein).
|
||||
sep_kette_by_key = Rueckgabe von map_sepliste_kette_items (dict
|
||||
sep_kette-Schluessel -> Item).
|
||||
weiche_by_key = dict switches-"key" -> erzeugtes Item (von
|
||||
build_kreisel_weiche_items).
|
||||
"""
|
||||
@@ -837,8 +680,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
if item is None:
|
||||
if dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: "
|
||||
f"KEIN Item in alle_schluessel gefunden - "
|
||||
f"uebersprungen{dbg_highlight(punkt_key)}")
|
||||
f"KEIN Item in alle_schluessel gefunden - uebersprungen")
|
||||
continue
|
||||
nachbarn = []
|
||||
for nachbar_key in (vorgaenger, nachfolger):
|
||||
@@ -849,20 +691,17 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
nachbarn.append(nachbar_item.get("teileid", ""))
|
||||
elif dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r}: "
|
||||
f"Nachbar-Key {nachbar_key!r} nicht in alle_schluessel "
|
||||
f"aufloesbar{dbg_highlight(item.get('teileid', ''))}")
|
||||
f"Nachbar-Key {nachbar_key!r} nicht in alle_schluessel aufloesbar")
|
||||
teileid = item.get("teileid", "")
|
||||
if nachbarn:
|
||||
item["nachbarn"] = ", ".join(nachbarn)
|
||||
if dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} "
|
||||
f"(TeileId {teileid!r}): nachbarn={nachbarn}"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"(TeileId {teileid!r}): nachbarn={nachbarn}")
|
||||
elif dbg:
|
||||
dbg(f" Kreisel {kreisel_tid} Punkt {punkt_key!r} "
|
||||
f"(TeileId {teileid!r}): KEINE Nachbarn aufgeloest "
|
||||
f"(vorgaenger={vorgaenger!r}, nachfolger={nachfolger!r})"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"(vorgaenger={vorgaenger!r}, nachfolger={nachfolger!r})")
|
||||
|
||||
if dbg:
|
||||
dbg("")
|
||||
@@ -878,7 +717,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
if dbg:
|
||||
dbg(f" sepliste-Key {key!r} (typ={eintrag.get('typ')!r}): "
|
||||
f"KEIN Item in sep_kette_by_key - uebersprungen "
|
||||
f"(siehe map_separator_kette_items){dbg_highlight(key)}")
|
||||
f"(siehe map_sepliste_kette_items)")
|
||||
continue
|
||||
nachbarn = []
|
||||
for nachbar_key in (eintrag.get("vorgaenger"), eintrag.get("nachfolger")):
|
||||
@@ -894,8 +733,7 @@ def resolve_umlauf_und_sepliste_nachbarn(items, kreisel_umlauf, sep_kette,
|
||||
dbg(f" sepliste-Key {key!r} (TeileId {teileid!r}): "
|
||||
f"vorgaenger={eintrag.get('vorgaenger')!r} "
|
||||
f"nachfolger={eintrag.get('nachfolger')!r} -> "
|
||||
f"nachbarn={nachbarn} item.nachbarn={item.get('nachbarn', '')!r}"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"nachbarn={nachbarn} item.nachbarn={item.get('nachbarn', '')!r}")
|
||||
|
||||
|
||||
def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None):
|
||||
@@ -909,18 +747,18 @@ def write_separatorliste_merkmale(items, sep_kette_by_key, dbg=None):
|
||||
Baureihenfolge - siehe compute_sep_kette) statt sep_kette selbst: dessen
|
||||
dict-Reihenfolge ist nicht garantiert, die sepliste-Liste dagegen schon.
|
||||
AS/ES zaehlen HIER mit (anders als beim "Separatorlist"-Merkmal frueher) -
|
||||
sie sind die Schleus-Enden der Kette (siehe build_strecke_schleus_items)
|
||||
und bekommen dort eine eigene TeileId ("ILS Ausschleuselement"/"ILS
|
||||
Einschleuselement"), analog zu den AS/ES-Enden, die beim Kreisel bereits
|
||||
ueber compute_kreisel_umlauf in dessen TrackIds auftauchen - ohne sie
|
||||
fehlten hier die Kettenenden, obwohl sie beim Kreisel schon drin sind.
|
||||
sie sind eigene Items ("ILS Ausschleuselement"/"ILS Einschleuselement",
|
||||
direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert, siehe
|
||||
process_blocks), analog zu den AS/ES-Enden, die beim Kreisel bereits ueber
|
||||
compute_kreisel_umlauf in dessen TrackIds auftauchen - ohne sie fehlten
|
||||
hier die Kettenenden, obwohl sie beim Kreisel schon drin sind.
|
||||
|
||||
Der eigentliche Separator wurde bereits von map_separator_kette_items ueber
|
||||
die sepliste-Koordinate (x/y) auf sein reales Separator-Item (TeileId)
|
||||
aufgeloest, AS/ES von build_strecke_schleus_items - beide unter demselben
|
||||
"<strecke_id>#<lfdnr>"-Schluessel in sep_kette_by_key eingetragen; hier
|
||||
wird dieser Schluessel nur noch je Strecke eingesammelt. Muss darum NACH
|
||||
map_separator_kette_items/build_strecke_schleus_items laufen.
|
||||
Sowohl der Separator als auch AS/ES wurden bereits von
|
||||
map_sepliste_kette_items ueber die sepliste-ID (Feld "id") auf ihr reales
|
||||
Item (TeileId) aufgeloest, unter demselben "<strecke_id>#<lfdnr>"-
|
||||
Schluessel in sep_kette_by_key eingetragen; hier wird dieser Schluessel
|
||||
nur noch je Strecke eingesammelt. Muss darum NACH map_sepliste_kette_items
|
||||
laufen.
|
||||
"""
|
||||
for item in items:
|
||||
if item.get("teileart", "") not in GF_VF_TEILEARTEN:
|
||||
@@ -960,8 +798,8 @@ def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_ke
|
||||
Schluessel oder Weichen-"key") auf die TeileId des jeweils zugehoerigen
|
||||
Items aufgeloest. Derselbe Lookup wie in
|
||||
resolve_umlauf_und_sepliste_nachbarn, daher muss diese Funktion NACH
|
||||
map_separator_kette_items/build_kreisel_weiche_items/
|
||||
build_strecke_schleus_items laufen (deren TeileIds stehen erst dann fest).
|
||||
map_sepliste_kette_items/build_kreisel_weiche_items laufen (deren
|
||||
TeileIds stehen erst dann fest).
|
||||
"""
|
||||
freipunkt_by_id = {it.get("teileid", ""): it for it in items
|
||||
if it.get("teileart", "") in KREISEL_UMLAUF_TEILEARTEN}
|
||||
@@ -987,6 +825,67 @@ def write_kreisel_separatorliste_merkmale(items, kreisel_umlauf, sep_kette_by_ke
|
||||
kreisel_item["trackids"] = ", ".join(teileids)
|
||||
|
||||
|
||||
def pruefe_ases_in_beiden_containern(items, dbg=None):
|
||||
"""Prueft, ob jede AS-/ES-Zeile (AUSSCHLEUS_TEILEART/EINSCHLEUS_TEILEART)
|
||||
in BEIDEN Containern auftaucht, zu denen sie gehoert:
|
||||
- in den TrackIds ihrer EIGENEN Strecke (VF_n/GF_n, ueber das
|
||||
ZUORDNUNG-Attribut des Markers - siehe Item-Feld "_zuordnung")
|
||||
- in den TrackIds MINDESTENS eines Kreisels (der beruehrte Carrier,
|
||||
geometrisch ueber compute_kreisel_umlauf/KREISEL_UMLAUF_TEILEARTEN
|
||||
ermittelt - nur "ILS 2.0 Kreisel", Eckrad nimmt am Umlauf nicht teil,
|
||||
siehe compute_kreisel_umlauf/KREISEL_SPLIT_TEILEART)
|
||||
|
||||
Muss NACH write_separatorliste_merkmale UND
|
||||
write_kreisel_separatorliste_merkmale laufen (beide TrackIds-Spalten
|
||||
muessen bereits stehen).
|
||||
|
||||
Ein AS/ES, das in einem der beiden Container fehlt, zeigt einen echten
|
||||
Fehler in der Zuordnungskette an (z.B. ZUORDNUNG-Attribut leer/falsch,
|
||||
Kollisionstoleranz zu knapp, Kreisel zu weit weg, fehlende Bounding-Box am
|
||||
Marker) - kein Normalfall. Traegt in diesem Fall eine Warnung in
|
||||
item["warnung"] ein (erscheint in der CSV-Spalte "Warnungen") und gibt sie
|
||||
zusaetzlich auf der Konsole/im Debuglog aus.
|
||||
|
||||
Rueckgabe: Liste der betroffenen TeileIds (fuer Tests/Diagnose), leer bei
|
||||
vollstaendiger Zuordnung.
|
||||
"""
|
||||
strecke_by_id = {it.get("teileid", ""): it for it in items
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN}
|
||||
kreisel_trackid_sets = [
|
||||
{t.strip() for t in (it.get("trackids", "") or "").split(",") if t.strip()}
|
||||
for it in items if it.get("teileart", "") == KREISEL_SPLIT_TEILEART
|
||||
]
|
||||
|
||||
auffaellig = []
|
||||
for it in items:
|
||||
if it.get("teileart", "") not in (AUSSCHLEUS_TEILEART, EINSCHLEUS_TEILEART):
|
||||
continue
|
||||
teileid = it.get("teileid", "")
|
||||
fehlend = []
|
||||
|
||||
strecke = strecke_by_id.get(it.get("_zuordnung", ""))
|
||||
if strecke is None:
|
||||
fehlend.append("eigene Strecke (ZUORDNUNG unaufloesbar)")
|
||||
else:
|
||||
strecke_ids = {t.strip() for t in (strecke.get("trackids", "") or "").split(",") if t.strip()}
|
||||
if teileid not in strecke_ids:
|
||||
fehlend.append("eigene Strecke")
|
||||
|
||||
if not any(teileid in ids for ids in kreisel_trackid_sets):
|
||||
fehlend.append("Kreisel")
|
||||
|
||||
if fehlend:
|
||||
auffaellig.append(teileid)
|
||||
warnung = f"nicht in TrackIds von: {', '.join(fehlend)}"
|
||||
bisherige = it.get("warnung", "")
|
||||
it["warnung"] = f"{bisherige}; {warnung}" if bisherige else warnung
|
||||
meldung = f"[export_csv] WARNUNG: {it.get('teileart', '')} {teileid}: {warnung}"
|
||||
print(meldung)
|
||||
if dbg:
|
||||
dbg(f" {meldung}")
|
||||
return auffaellig
|
||||
|
||||
|
||||
def process_blocks(blocks, lookup, dbg=None):
|
||||
# x_origin_mm/y_origin_mm einmalig fuer diesen Exportlauf aufloesen (siehe
|
||||
# export_planquadrat.resolve_origins): automatisch aus den Bloecken, falls
|
||||
@@ -1010,6 +909,8 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
eingang_count = 0
|
||||
ausgang_count = 0
|
||||
geruest_count = 0
|
||||
ausschleus_count = 0
|
||||
einschleus_count = 0
|
||||
|
||||
# Zaehlung fuer die Omniflo-Sum-Zeile (nur Bogen/Weiche/Gerade)
|
||||
cnt_wk = cnt_einzel = cnt_delta = cnt_doppel = cnt_stern = 0
|
||||
@@ -1025,15 +926,54 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
|
||||
# ZZ_ASES_MARKER (siehe cfg/export.cfg pattern_ases_marker sowie
|
||||
# csv:asmark-aktualisieren in Lisp/export.lsp): der persistente
|
||||
# Marker, den Lisp an jedes AS-/ES-Kettenende setzt, damit dessen ID
|
||||
# ueber Exportlaeufe hinweg stabil bleibt. Bekommt hier bewusst KEINE
|
||||
# eigene CSV-Zeile - die sichtbare AS/ES-Zeile fuer denselben
|
||||
# physischen Punkt liefert weiterhin build_strecke_schleus_items
|
||||
# (reine Geometrie); die Marker-ID wird dort ueber das sepliste-Feld
|
||||
# "id" wiederverwendet (siehe dort). Eine eigene Zeile hier waere
|
||||
# doppelt gezaehlt und haette ohnehin keine sinnvolle Geometrie/
|
||||
# Boundingbox aus dem kleinen Marker-Rechteck selbst.
|
||||
# Marker, den Lisp an jedes AS-/ES-Kettenende setzt. Er IST die
|
||||
# AS-/ES-Zeile - TeileId, Position und Boundingbox kommen direkt vom
|
||||
# Marker-Block selbst (echtes INSERT, echte vla-getboundingbox-Box),
|
||||
# kein synthetischer Bau-Schritt mehr noetig (fruehere Version:
|
||||
# compute_strecke_kreisel_schleus/build_strecke_schleus_items suchten
|
||||
# den anschliessenden Kreisel per Kollisionstest und erfanden dabei
|
||||
# eine eigene, bei jedem Export neue TeileId). "Kreisel"-Merkmal und
|
||||
# ID-basierte sep_kette-Zuordnung tragen unten zwei kurze Nachlaeufe
|
||||
# in process_blocks nach, sobald alle Items klassifiziert sind (die
|
||||
# eigene Kette bzw. der beruehrte Kreisel koennen an DIESER Stelle
|
||||
# noch unklassifiziert sein).
|
||||
if matches_any(bname, BLOCKPATTERNS.get("pattern_ases_marker", [])):
|
||||
attribs = block.get("attribs", {})
|
||||
typ = attribs.get("TYP", "")
|
||||
teileart = {"AS": AUSSCHLEUS_TEILEART, "ES": EINSCHLEUS_TEILEART}.get(typ)
|
||||
if teileart is None:
|
||||
# Sollte nicht vorkommen (TYP wird von csv:asmark-aktualisieren
|
||||
# immer gesetzt) - defensiv ueberspringen statt abzustuerzen.
|
||||
print(f"[export_csv] WARNUNG: {bname} ohne gueltiges TYP-Attribut "
|
||||
f"(TYP={typ!r}) - uebersprungen")
|
||||
continue
|
||||
elem_nr += 1
|
||||
if typ == "AS":
|
||||
ausschleus_count += 1
|
||||
zaehler_n = ausschleus_count
|
||||
else:
|
||||
einschleus_count += 1
|
||||
zaehler_n = einschleus_count
|
||||
items.append({
|
||||
"nr": elem_nr,
|
||||
"teileart": teileart,
|
||||
"teileid": attribs.get("ID") or format_sensor_id("ASES", elem_nr),
|
||||
"bezeichnung": f"{teileart} :{zaehler_n}",
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": {
|
||||
"Streckenende": "Anfang" if typ == "AS" else "Ende",
|
||||
"Element": attribs.get("ELEMENT", ""),
|
||||
"ARTINR": attribs.get("ARTINR", ""),
|
||||
},
|
||||
# Private Felder (nicht in der CSV) fuer die beiden Nachlaeufe
|
||||
# weiter unten in process_blocks: "_zuordnung" = ID des
|
||||
# eigenen VF_n/GF_n-Wrappers (fuer das "Förderer"/"Strecke"-
|
||||
# Merkmal), "_lfdnr" wird hier nicht gebraucht (die sep_kette-
|
||||
# Zuordnung laeuft exakt ueber die TeileId, siehe
|
||||
# map_sepliste_kette_items).
|
||||
"_zuordnung": attribs.get("ZUORDNUNG", ""),
|
||||
**bbox_columns(block),
|
||||
})
|
||||
continue
|
||||
|
||||
# Omniflo Bogen oder Weiche
|
||||
@@ -1108,12 +1048,8 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
"bezeichnung": bezeichnung_oder_fallback(block, f"VarioFoerderer :{vf_count}"),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_variofoerderer_merkmale(block),
|
||||
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
|
||||
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
|
||||
"x": block.get("x", 0.0),
|
||||
"y": block.get("y", 0.0),
|
||||
# Sepliste-XDATA (AS/SEP/ES-Reihenfolge) durchreichen - Quelle
|
||||
# fuer compute_sep_kette/map_separator_kette_items (Ketten-
|
||||
# fuer compute_sep_kette/map_sepliste_kette_items (Ketten-
|
||||
# interne Separatoren + AS/ES-Nachbarn). Ohne diese Weitergabe
|
||||
# liefe die gesamte sepliste-basierte Nachbarverkettung ins
|
||||
# Leere (das Feld stand im JSON, wurde aber nicht ans Item
|
||||
@@ -1134,10 +1070,6 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
"bezeichnung": bezeichnung_oder_fallback(block, f"Gefaellestrecke :{gf_count}"),
|
||||
"planquadrat": planquadrat_column(block, planquadrat_cfg),
|
||||
"merkmale": build_variofoerderer_merkmale(block),
|
||||
# x/y (Block-Einfuegepunkt = Kettenanfang) fuer die Anfang/Ende-
|
||||
# Bestimmung der Ein-/Ausschleuselemente (compute_strecke_kreisel_schleus)
|
||||
"x": block.get("x", 0.0),
|
||||
"y": block.get("y", 0.0),
|
||||
# Sepliste-XDATA durchreichen (siehe VF_N oben).
|
||||
"sepliste": block.get("sepliste"),
|
||||
**bbox_columns(block),
|
||||
@@ -1232,9 +1164,9 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
# ueberschreiben darf - siehe build_sensor_merkmale.
|
||||
"_zuordnung_fix": bool(block.get("zuordnung_fix")),
|
||||
# Roher INSERT-Punkt (nicht der bbox-Mittelpunkt) - dient
|
||||
# map_separator_kette_items zum Zuordnen dieses (Proxy-)
|
||||
# Separators zu seinem sepliste-Eintrag (Match ueber
|
||||
# Wrapper-ID + naechster INSERT-Punkt, siehe dort).
|
||||
# map_sepliste_kette_items als Koordinaten-FALLBACK, falls der
|
||||
# sepliste-Eintrag (noch) kein "id"-Feld traegt (Altbestand;
|
||||
# der Normalfall loest exakt per ID auf, siehe dort).
|
||||
"_x": block.get("x", 0.0), "_y": block.get("y", 0.0),
|
||||
**cols,
|
||||
})
|
||||
@@ -1433,34 +1365,46 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
items, NEIGHBOR_TOLERANCE_MM, durchmesser_mm=KREISEL_DURCHMESSER_MM,
|
||||
element_box_mm=ELEMENT_BOX_MM, dbg=dbg)
|
||||
|
||||
# Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren (per Zuordnung
|
||||
# oben bereits einem Kreisel zugeordnet), die AS/ES-Enden andockender
|
||||
# Ketten (aus sep_kette) UND die Kreisel-Kreisel-Weichen (touch_switches),
|
||||
# sortiert nach Winkel um die Kreiselachse (siehe compute_kreisel_umlauf -
|
||||
# DREHRICHTUNG-Attribut bestimmt den Umlaufsinn).
|
||||
# Umlauf-Reihenfolge an jedem Kreisel: freie Separatoren, BTMT-Stationen
|
||||
# UND die AS/ES-Enden andockender Ketten (alle zusammen KREISEL_UMLAUF_
|
||||
# TEILEARTEN, einheitlich per eigener Bounding-Box zugeordnet - ein AS/ES
|
||||
# bringt seine Box vom ZZ_ASES_MARKER direkt mit) sowie die Kreisel-
|
||||
# Kreisel-Weichen (touch_switches), sortiert nach Winkel um die
|
||||
# Kreiselachse (siehe compute_kreisel_umlauf - DREHRICHTUNG-Attribut
|
||||
# bestimmt den Umlaufsinn).
|
||||
kreisel_umlauf = compute_kreisel_umlauf(
|
||||
items, sep_kette, NEIGHBOR_TOLERANCE_MM,
|
||||
items, NEIGHBOR_TOLERANCE_MM,
|
||||
durchmesser_mm=KREISEL_DURCHMESSER_MM, touch_switches=touch_switches, dbg=dbg)
|
||||
|
||||
# Neue Zeilen, deren "Nachbarn"-Spalte aus einem sep_kette-Schluessel
|
||||
# aufgeloest werden muss, erzeugen: Kreisel-Kreisel-Weichen, dann Aus-/
|
||||
# Einschleuselemente (AS/ES). Ein sep_kette-Schluessel kann auf eine Zeile
|
||||
# zeigen, die ERST DANACH registriert wird, darum erst NACH allen
|
||||
# Registrierungen aufloesen (siehe unten) statt inline - frueherer Bug
|
||||
# schrieb sonst den rohen Schluessel ("0026#3") statt der TeileId.
|
||||
# aufgeloest werden muss, erzeugen: nur noch die Kreisel-Kreisel-Weichen
|
||||
# (AS/ES sind bereits als eigene Items klassifiziert, siehe process_blocks
|
||||
# oben). Ein sep_kette-Schluessel kann auf eine Zeile zeigen, die ERST
|
||||
# DANACH registriert wird, darum erst NACH allen Registrierungen aufloesen
|
||||
# (siehe unten) statt inline - frueherer Bug schrieb sonst den rohen
|
||||
# Schluessel ("0026#3") statt der TeileId.
|
||||
elem_nr, weiche_by_key = build_kreisel_weiche_items(
|
||||
items, planquadrat_cfg, elem_nr, touch_switches, dbg=dbg)
|
||||
sep_kette_by_key = {}
|
||||
elem_nr = build_strecke_schleus_items(
|
||||
items, planquadrat_cfg, elem_nr, sep_kette=sep_kette,
|
||||
sep_kette_by_key=sep_kette_by_key, dbg=dbg)
|
||||
# Die in einer Kette verpackten Separatoren erzeugen KEINE eigenen Zeilen -
|
||||
# sie liegen bereits als (Proxy-)Separator_SP-Items vor (freier-Separator-
|
||||
# Pfad oben). map_separator_kette_items ordnet ihnen nur ihren sepliste-
|
||||
# Schluessel zu, damit die Nachbar-Reihenfolge (AS -> SEP -> ES) auf die
|
||||
# vorhandenen Items angewandt werden kann (kein Doppel-Export).
|
||||
map_separator_kette_items(
|
||||
items, sep_kette, sep_kette_by_key=sep_kette_by_key, dbg=dbg)
|
||||
|
||||
# "Förderer"/"Strecke"-Merkmal an jedem AS/ES nachtragen: die eigene Kette
|
||||
# (ZUORDNUNG-Attribut des Markers, siehe "_zuordnung") kann bei der
|
||||
# Klassifikation des Markers selbst noch unklassifiziert gewesen sein -
|
||||
# der FeldNAME haengt von deren TeileArt ab (VF -> "Förderer",
|
||||
# GF -> "Strecke", siehe _strecke_typ_feld), der WERT ist einfach die
|
||||
# ZUORDNUNG selbst (keine Geometrie mehr noetig).
|
||||
strecke_by_id = {it.get("teileid", ""): it for it in items
|
||||
if it.get("teileart", "") in GF_VF_TEILEARTEN}
|
||||
for it in items:
|
||||
if it.get("teileart", "") not in (AUSSCHLEUS_TEILEART, EINSCHLEUS_TEILEART):
|
||||
continue
|
||||
strecke = strecke_by_id.get(it.get("_zuordnung", ""))
|
||||
if strecke is not None:
|
||||
it["merkmale"][_strecke_typ_feld(strecke.get("teileart", ""))] = \
|
||||
strecke.get("teileid", "")
|
||||
|
||||
# sep_kette-Schluessel (AS/SEP/ES) auf ihre bereits vorhandenen Items
|
||||
# aufloesen - exakt per ID, siehe map_sepliste_kette_items.
|
||||
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette, dbg=dbg)
|
||||
|
||||
# Spalte "TrackIds" je Strecke (Reihenfolge der SEP-TeileIds aus der
|
||||
# sepliste, x/y bereits oben auf das reale Separator-Item aufgeloest).
|
||||
@@ -1477,6 +1421,12 @@ def process_blocks(blocks, lookup, dbg=None):
|
||||
write_kreisel_separatorliste_merkmale(
|
||||
items, kreisel_umlauf, sep_kette_by_key, weiche_by_key, dbg=dbg)
|
||||
|
||||
# Konsistenzpruefung: jedes AS/ES muss jetzt sowohl in den TrackIds seiner
|
||||
# eigenen Strecke als auch in denen eines Kreisels auftauchen (siehe
|
||||
# Docstring) - Abweichungen deuten auf einen echten Fehler in der
|
||||
# Zuordnungskette hin und werden ueber "Warnungen" sichtbar gemacht.
|
||||
pruefe_ases_in_beiden_containern(items, dbg=dbg)
|
||||
|
||||
# Nach dem Sammelheimen aller Zeilen: sortiert nach TeileId ausgeben, damit
|
||||
# Elementnummer (erste Spalte) und TeileId nach Moeglichkeit uebereinstimmen
|
||||
# (siehe format_csv_line/Elementnummer-Neuvergabe unten). Numerische IDs
|
||||
|
||||
+50
-276
@@ -88,26 +88,21 @@ SENSOR_TEILEARTEN = {"ILS 2.0 Separator", "ILS 2.0 Scanner"}
|
||||
# pattern_ausgang in cfg/export.cfg) werden hier wie BTMT als frei platzierte
|
||||
# Umlauf-Punkte gefuehrt - so tauchen sie, wenn sie auf einer Kreiselbahn
|
||||
# liegen, in dessen "TrackIds"-Spalte auf (Nutzeranforderung: wie
|
||||
# Separator/AS/ES behandeln). Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"-
|
||||
# Praefix (siehe export_csv.py) - dieselben Strings muessen hier stehen.
|
||||
# Separator/AS/ES behandeln). AUSSCHLEUS_TEILEART/EINSCHLEUS_TEILEART (AS/ES-
|
||||
# Kettenenden, siehe unten): seit sie als dauerhafter ZZ_ASES_MARKER real in
|
||||
# der Zeichnung stehen (statt wie frueher nur ueber eine Kreisel<->Strecke-
|
||||
# Kollisionsberechnung synthetisiert zu werden, siehe git-Historie
|
||||
# compute_strecke_kreisel_schleus), reicht ihre EIGENE Bounding-Box fuer die
|
||||
# ganz normale BBox-Ueberschneidung mit einem Kreisel - keine gesonderte
|
||||
# Naeherungs-/Kollisionslogik mehr noetig (siehe compute_kreisel_umlauf, das
|
||||
# vorher zusaetzlich sep_kette nach der naechstgelegenen Kreisel-Achse absuchte).
|
||||
# Die TeileArt-Strings sind bewusst OHNE "ILS 2.0"-Praefix (siehe
|
||||
# export_csv.py) - dieselben Strings muessen hier stehen.
|
||||
KREISEL_UMLAUF_TEILEARTEN = {
|
||||
"ILS 2.0 Separator", "ILS 2.0 BTMT Beladung", "ILS 2.0 BTMT Entladung", "ILS Weiche",
|
||||
"Eingang", "Ausgang",
|
||||
"Eingang", "Ausgang", "ILS Ausschleuselement", "ILS Einschleuselement",
|
||||
}
|
||||
|
||||
# Temporaerer Diagnose-Filter fuers Debuglog (siehe [Nachbarschaft] debug_log
|
||||
# in cfg/export.cfg): markiert Zeilen zu diesen TeileIds im .dbg-Log
|
||||
# besonders, um die fehlende Nachbarn-/TrackIds-Zuordnung einzelner
|
||||
# Separatoren (0025/0029/0031/0035) nachzuvollziehen. Nach Abschluss der
|
||||
# Diagnose wieder auf einen leeren Set zuruecksetzen oder entfernen.
|
||||
DEBUG_HIGHLIGHT_TEILEIDS = {"0025", "0029", "0031", "0035"}
|
||||
|
||||
|
||||
def dbg_highlight(teileid):
|
||||
"""Suffix fuer eine Debuglog-Zeile, wenn teileid in
|
||||
DEBUG_HIGHLIGHT_TEILEIDS steht - sonst leerer String."""
|
||||
return " <<<< ZIEL-ID" if str(teileid) in DEBUG_HIGHLIGHT_TEILEIDS else ""
|
||||
|
||||
# Nur "ILS 2.0 Kreisel" (echte Kreisel-Bloecke mit AN8/Antrieb + SP8/Spann-
|
||||
# station + ABSTAND-Attribut) werden links/rechts gesplittet - "ILS 2.0
|
||||
# Eckrad" hat keine dieser zwei Stationen und bleibt eine einzelne BBox.
|
||||
@@ -117,9 +112,10 @@ KREISEL_SPLIT_TEILEART = "ILS 2.0 Kreisel"
|
||||
# (compute_kreisel_touch_switches / export_csv.py).
|
||||
WEICHE_TEILEART = "ILS Weiche"
|
||||
|
||||
# TeileArten der automatisch an den Strecken-Enden (Kollision Kreisel <->
|
||||
# Gefaellestrecke/Foerderer) erzeugten Ein-/Ausschleuselemente
|
||||
# (compute_strecke_kreisel_schleus / export_csv.py).
|
||||
# TeileArten der Ein-/Ausschleuselemente an den Strecken-Enden (AS/ES) einer
|
||||
# Gefaellestrecke/eines Foerderers - siehe process_blocks (export_csv.py):
|
||||
# direkt aus dem dauerhaften ZZ_ASES_MARKER klassifiziert (dessen Attribut
|
||||
# TYP="AS"/"ES" waehlt die TeileArt), keine gesonderte Erzeugung mehr hier.
|
||||
AUSSCHLEUS_TEILEART = "ILS Ausschleuselement"
|
||||
EINSCHLEUS_TEILEART = "ILS Einschleuselement"
|
||||
|
||||
@@ -1096,39 +1092,12 @@ def compute_kreisel_touch_switches(items, tolerance_mm, durchmesser_mm=800.0,
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Kreisel <-> Strecken-Enden -> Ein-/Ausschleuselemente
|
||||
# Kreisel <-> Strecken-Enden: verfeinerte Kollisionsbox fuer compute_neighbor_
|
||||
# ids (die "Nachbarn"-Spalte einer GF/VF-Strecke soll nur an ihrem echten AS-/
|
||||
# ES-Ende auf einen Kreisel reagieren, nicht auf die gesamte, oft lange
|
||||
# Wrapper-Bounding-Box).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _closest_pt_on_segment(a, b, p):
|
||||
"""Naechstgelegener Punkt auf der 2D-Strecke [a,b] zum Punkt p."""
|
||||
abx, aby = b[0] - a[0], b[1] - a[1]
|
||||
denom = abx * abx + aby * aby
|
||||
if denom <= 1e-9:
|
||||
return (a[0], a[1])
|
||||
t = _clamp(((p[0] - a[0]) * abx + (p[1] - a[1]) * aby) / denom, 0.0, 1.0)
|
||||
return (a[0] + abx * t, a[1] + aby * t)
|
||||
|
||||
|
||||
def _strecke_ends(bbox, insert_xy):
|
||||
"""Die beiden Enden einer GF/VF-Strecke als (Anfang, Ende) in Welt-x/y.
|
||||
|
||||
Enden = Mitten der beiden kurzen Stirnseiten der (achsparallelen) Bounding-
|
||||
Box entlang ihrer laengeren Ausdehnung. "Anfang" ist das Ende, das dem
|
||||
Block-Einfuegepunkt (insert_xy = Kettenanfang/startpunkt, siehe
|
||||
ssg-block-wrap-welt) naeher liegt; "Ende" das andere.
|
||||
"""
|
||||
cx, cy = bbox.get("cx", 0.0), bbox.get("cy", 0.0)
|
||||
dx, dy = bbox.get("dx", 0.0), bbox.get("dy", 0.0)
|
||||
if dx >= dy:
|
||||
e0, e1 = (cx - dx / 2.0, cy), (cx + dx / 2.0, cy)
|
||||
else:
|
||||
e0, e1 = (cx, cy - dy / 2.0), (cx, cy + dy / 2.0)
|
||||
ix, iy = insert_xy
|
||||
if math.hypot(e0[0] - ix, e0[1] - iy) <= math.hypot(e1[0] - ix, e1[1] - iy):
|
||||
return e0, e1
|
||||
return e1, e0
|
||||
|
||||
|
||||
def _asel_kos_xy(item):
|
||||
"""(k1_xy, k2_xy) einer GF/VF-Strecke = Welt-(x,y) von AS (K1/KS_EIN) und
|
||||
ES (K2/KS_AUS), dekodiert aus item["k1"]/["k2"] (csv:trans-encode, siehe
|
||||
@@ -1177,188 +1146,6 @@ def _asel_collision_boxes(item, half_tol, box_dims):
|
||||
return boxes
|
||||
|
||||
|
||||
def _distinct_kreisel_ids(nachbarn):
|
||||
"""Eindeutige Kreisel-TeileIds aus einem Nachbarn-String ("0009-L, 0002-R,
|
||||
0001-L" -> ["0009", "0002", "0001"]). Das Haelften-Suffix -L/-R (siehe
|
||||
kreisel_half_bboxes) wird entfernt, Reihenfolge bleibt erhalten."""
|
||||
ids = []
|
||||
for part in (nachbarn or "").split(","):
|
||||
p = part.strip()
|
||||
if not p:
|
||||
continue
|
||||
if p.endswith("-L") or p.endswith("-R"):
|
||||
p = p[:-2]
|
||||
if p not in ids:
|
||||
ids.append(p)
|
||||
return ids
|
||||
|
||||
|
||||
def compute_strecke_kreisel_schleus(items, durchmesser_mm=800.0,
|
||||
element_box_mm=None, dbg=None):
|
||||
"""Erzeugt je Gefaellestrecke/Foerderer (GF_VF_TEILEARTEN) an Anfang und
|
||||
Ende ein Aus- bzw. Einschleuselement an der Kollisionsposition zum
|
||||
anschliessenden Kreisel.
|
||||
|
||||
Grundlage sind NICHT eigene Abstandsschwellen, sondern die bereits per
|
||||
Bounding-Box ermittelten Kreisel-Nachbarn der Strecke (Spalte "Nachbarn",
|
||||
siehe compute_neighbor_ids). Jeder der beiden Streckenenden (Anfang = dem
|
||||
Block-Einfuegepunkt naeheres Ende, siehe _strecke_ends) wird der unter
|
||||
diesen Nachbarn naechstgelegene Kreisel zugeordnet:
|
||||
- Am ANFANG entsteht ein Ausschleuselement (AUSSCHLEUS_TEILEART),
|
||||
- am ENDE ein Einschleuselement (EINSCHLEUS_TEILEART).
|
||||
Beruehrt eine Strecke nur EINEN Kreisel (z.B. ein Foerderer, der zum selben
|
||||
Kreisel zurueckfuehrt), wird dieser beiden Enden zugeordnet. Beruehrt sie
|
||||
KEINEN Kreisel, entstehen keine Elemente.
|
||||
|
||||
Eine Strecke sollte genau zwei Kreisel-Bounding-Boxen beruehren (ein Anfang
|
||||
+ ein Ende). Beruehrt ihre BBox MEHR als zwei (mit exakten 3D-BBoxen sollte
|
||||
das nicht vorkommen - hier wird in x/y ohne Hoehe geprueft), entsteht eine
|
||||
Warnung: die beiden Enden bekommen weiterhin ihren jeweils naechsten
|
||||
Kreisel, die ueberzaehligen werden ignoriert und in der Warnung genannt.
|
||||
|
||||
Position eines Elements = das AS-/ES-Koordinatensystem (K1/K2, siehe
|
||||
csv:vfgf-k-kos-strings), um eine HALBE Box (Box-Breite/2 entlang der
|
||||
Foerderachse, siehe element_box_mm) nach aussen versetzt: das
|
||||
Ausschleuselement (AS/Anfang) VOR das AS-KOS (entgegen der Achse), das
|
||||
Einschleuselement (ES/Ende) HINTER das ES-KOS (in Achsrichtung) - beide
|
||||
also zum anschliessenden Kreisel hin, sodass das Element realistisch
|
||||
zwischen Streckenende und Kreisel liegt. Fehlen K1/K2 (Altbestand ohne
|
||||
AS/ES-Export), werden die Streckenenden aus der Wrapper-BBox genommen
|
||||
(_strecke_ends). Die feste Box (element_box_mm) wird mit ihrer Laengsseite
|
||||
senkrecht zur Foerderachse ausgerichtet (Welt-AABB, _oriented_box_aabb).
|
||||
|
||||
element_box_mm = (laenge, breite, hoehe) in mm; None -> load_element_box_mm().
|
||||
|
||||
Rueckgabe: (result, warnings)
|
||||
result = Liste von dicts:
|
||||
{"teileart": AUSSCHLEUS_/EINSCHLEUS_TEILEART,
|
||||
"x","y","z": Kollisionsposition (mm),
|
||||
"dx","dy","dz": Welt-AABB der ausgerichteten Box (mm),
|
||||
"kreisel_id": TeileId des anschliessenden Kreisels,
|
||||
"strecke_id": TeileId der GF/VF,
|
||||
"ende_label": "Anfang" | "Ende"}
|
||||
warnings = dict {strecke_teileid: warntext} fuer Strecken mit > 2
|
||||
Kreisel-BBox-Beruehrungen.
|
||||
"""
|
||||
if element_box_mm is None:
|
||||
element_box_mm = load_element_box_mm()
|
||||
box_l, box_b, box_h = element_box_mm
|
||||
kreisel = {}
|
||||
for it in items:
|
||||
if it.get("teileart", "") == KREISEL_SPLIT_TEILEART:
|
||||
kreisel[it.get("teileid", "")] = _kreisel_capsule(it, durchmesser_mm)
|
||||
|
||||
if dbg:
|
||||
dbg("")
|
||||
dbg("=== Strecken-Enden <-> Kreisel -> Ein-/Ausschleuselemente ===")
|
||||
dbg(f"Echte Kreisel: {len(kreisel)}; Zuordnung ueber die BBox-Nachbarn "
|
||||
f"der Strecke (naechster Kreisel je Ende), erwartet 2 Kreisel/Strecke")
|
||||
|
||||
result = []
|
||||
warnings = {}
|
||||
for it in items:
|
||||
if it.get("teileart", "") not in GF_VF_TEILEARTEN:
|
||||
continue
|
||||
bbox = it.get("_bbox")
|
||||
if not bbox:
|
||||
continue
|
||||
sid = it.get("teileid", "")
|
||||
nb_ids = [k for k in _distinct_kreisel_ids(it.get("nachbarn", "")) if k in kreisel]
|
||||
if not nb_ids:
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: kein Kreisel-Nachbar -> keine Schleuselemente")
|
||||
continue
|
||||
|
||||
insert_xy = (it.get("x", bbox.get("cx", 0.0)) or 0.0,
|
||||
it.get("y", bbox.get("cy", 0.0)) or 0.0)
|
||||
# Streckenenden bevorzugt aus den AS/ES-Koordinatensystemen (K1=AS,
|
||||
# K2=ES, siehe csv:vfgf-k-kos-strings) - das ist die tatsaechliche
|
||||
# Position der Ein-/Ausschleuspunkte. "Anfang" bleibt das dem Block-
|
||||
# Einfuegepunkt naehere Ende. Fehlen K1/K2 (Altbestand ohne AS/ES-
|
||||
# Export), Fallback auf die Stirnseiten-Mitten der Wrapper-BBox.
|
||||
k1_xy, k2_xy = _asel_kos_xy(it)
|
||||
if k1_xy is not None and k2_xy is not None:
|
||||
ix, iy = insert_xy
|
||||
if math.hypot(k1_xy[0] - ix, k1_xy[1] - iy) <= math.hypot(k2_xy[0] - ix, k2_xy[1] - iy):
|
||||
anfang_end, ende_end = k1_xy, k2_xy
|
||||
else:
|
||||
anfang_end, ende_end = k2_xy, k1_xy
|
||||
else:
|
||||
anfang_end, ende_end = _strecke_ends(bbox, insert_xy)
|
||||
# Foerderachse (Einheitsvektor Anfang->Ende) fuer den Halbe-Box-Versatz
|
||||
# der Schleuselemente. Ohne verwertbare Richtung (Enden fallen zusammen)
|
||||
# bleibt der Versatz 0.
|
||||
adx, ady = ende_end[0] - anfang_end[0], ende_end[1] - anfang_end[1]
|
||||
alen = math.hypot(adx, ady)
|
||||
axis_u = (adx / alen, ady / alen) if alen > 1e-9 else (0.0, 0.0)
|
||||
# Versatz = halbe Box-Ausdehnung ENTLANG der Foerderachse. Die feste Box
|
||||
# (element_box_mm) liegt mit ihrer Breite laengs der Achse (siehe
|
||||
# _oriented_box_aabb), also ist die halbe Achs-Ausdehnung Breite/2.
|
||||
halbe_box = box_b / 2.0
|
||||
# Ausrichtung der Schleuselement-Box: entlang der Foerderachse (nicht
|
||||
# mehr der Kreiselachse) - das Element sitzt am AS/ES-KOS in
|
||||
# Foerderrichtung, konsistent mit den AS/ES-Kollisionsboxen.
|
||||
achs_rot = math.atan2(ady, adx) if alen > 1e-9 else 0.0
|
||||
scz = bbox.get("cz", 0.0)
|
||||
|
||||
def _nearest(end_pt):
|
||||
best = None
|
||||
for kid in nb_ids:
|
||||
cap = kreisel[kid]
|
||||
p = _closest_pt_on_segment(cap["a"], cap["b"], end_pt)
|
||||
d = math.hypot(p[0] - end_pt[0], p[1] - end_pt[1])
|
||||
if best is None or d < best[3]:
|
||||
best = (kid, cap, p, d)
|
||||
return best
|
||||
|
||||
a_best = _nearest(anfang_end)
|
||||
e_best = _nearest(ende_end)
|
||||
|
||||
if len(nb_ids) > 2:
|
||||
genutzt = {a_best[0], e_best[0]}
|
||||
extra = [k for k in nb_ids if k not in genutzt]
|
||||
warnings[sid] = (
|
||||
f"beruehrt {len(nb_ids)} Kreisel-BoundingBoxen "
|
||||
f"(erwartet 2 = Anfang + Ende): {', '.join(nb_ids)}"
|
||||
+ (f"; ueberzaehlig/ignoriert: {', '.join(extra)}" if extra else "")
|
||||
+ " - mit exakten 3D-BoundingBoxen sollte das nicht auftreten")
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid}: WARNUNG - {warnings[sid]}")
|
||||
|
||||
# Versatz der Schleuselement-Position gegenueber dem AS/ES-KOS: eine
|
||||
# HALBE Box entlang der Foerderachse, jeweils NACH AUSSEN (weg von der
|
||||
# Streckenmitte, zum anschliessenden Kreisel hin) - damit das Element
|
||||
# realistisch zwischen Streckenende und Kreisel liegt statt exakt auf
|
||||
# dem KOS. Vorzeichen: Ausschleuselement (AS/Anfang) "vor das AS-KOS"
|
||||
# = entgegen der Achse (-u), Einschleuselement (ES/Ende) "hinter das
|
||||
# ES-KOS" = in Achsrichtung (+u).
|
||||
for best, teileart, label, end_pt, vorzeichen in (
|
||||
(a_best, AUSSCHLEUS_TEILEART, "Anfang", anfang_end, -1.0),
|
||||
(e_best, EINSCHLEUS_TEILEART, "Ende", ende_end, 1.0)):
|
||||
kid, cap, p, d = best
|
||||
pos = (end_pt[0] + vorzeichen * axis_u[0] * halbe_box,
|
||||
end_pt[1] + vorzeichen * axis_u[1] * halbe_box)
|
||||
dx, dy, dz = _oriented_box_aabb(achs_rot, box_l, box_b, box_h)
|
||||
result.append({
|
||||
"teileart": teileart,
|
||||
"x": pos[0], "y": pos[1], "z": (cap["z"] + scz) / 2.0,
|
||||
"dx": dx, "dy": dy, "dz": dz,
|
||||
"kreisel_id": kid, "strecke_id": sid, "ende_label": label,
|
||||
})
|
||||
if dbg:
|
||||
dbg(f" Strecke {sid} {label}: Kreisel {kid} (Abstand Ende<->Achse "
|
||||
f"{d:.0f}mm) -> {teileart} bei ({pos[0]:.0f}, {pos[1]:.0f}, "
|
||||
f"{(cap['z'] + scz) / 2.0:.0f}) = KOS ({end_pt[0]:.0f}, {end_pt[1]:.0f}) "
|
||||
f"{'+' if vorzeichen > 0 else '-'} halbe Box ({halbe_box:.0f}mm) entlang Achse, "
|
||||
f"Box-AABB ({dx:.0f}, {dy:.0f}, {dz:.0f})")
|
||||
|
||||
if dbg:
|
||||
dbg(f"Erzeugte Ein-/Ausschleuselemente: {len(result)}; "
|
||||
f"Strecken mit Warnung (>2 Kreisel): {len(warnings)}")
|
||||
|
||||
return result, warnings
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sepliste (LISP-XDATA, siehe ssg-sepliste-xdata-schreiben) -> Nachbarschaft
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1376,15 +1163,19 @@ def compute_sep_kette(items, dbg=None):
|
||||
|
||||
Jeder Sepliste-Eintrag bekommt eine (innerhalb dieser Kette eindeutige)
|
||||
Schluessel-ID "<strecke_teileid>#<lfdnr>" - das ist die ID, mit der
|
||||
build_strecke_schleus_items/map_separator_kette_items die spaeteren
|
||||
globalen TeileIds verknuepft (siehe export_csv.py). Vorgaenger/Nachfolger
|
||||
sind schlicht der vorherige/naechste Eintrag in der Sepliste (die bereits
|
||||
in Baureihenfolge vorliegt) - kein Rateverfahren noetig.
|
||||
map_sepliste_kette_items die spaeteren globalen TeileIds verknuepft
|
||||
(siehe export_csv.py). Vorgaenger/Nachfolger sind schlicht der vorherige/
|
||||
naechste Eintrag in der Sepliste (die bereits in Baureihenfolge vorliegt)
|
||||
- kein Rateverfahren noetig.
|
||||
|
||||
Rueckgabe: dict {key: {"typ", "x", "y", "strecke_id", "vorgaenger",
|
||||
Rueckgabe: dict {key: {"typ", "x", "y", "id", "strecke_id", "vorgaenger",
|
||||
"nachfolger"}} - vorgaenger/nachfolger sind wieder solche keys, oder None
|
||||
am jeweiligen Kettenende (Verkettung mit dem Kreisel-Umlauf uebernimmt
|
||||
compute_kreisel_umlauf).
|
||||
compute_kreisel_umlauf). "id" ist die TeileId des Bauteils, das diesen
|
||||
Eintrag in der Zeichnung vertritt (AS/ES ueber den dauerhaften
|
||||
ZZ_ASES_MARKER, SEP ueber die beim Export ausgepackte temporaere
|
||||
Separator_SP-Kopie - siehe csv:sepliste-ids-setzen, Lisp/export.lsp) -
|
||||
leerer String, wenn (noch) keine ID zugewiesen wurde (Altbestand).
|
||||
"""
|
||||
result = {}
|
||||
for item in items:
|
||||
@@ -1401,6 +1192,7 @@ def compute_sep_kette(items, dbg=None):
|
||||
"typ": entry.get("typ", ""),
|
||||
"x": entry.get("x", 0.0),
|
||||
"y": entry.get("y", 0.0),
|
||||
"id": str(entry.get("id", "")).strip(),
|
||||
"strecke_id": sid,
|
||||
"vorgaenger": keys[i - 1] if i > 0 else None,
|
||||
"nachfolger": keys[i + 1] if i < len(keys) - 1 else None,
|
||||
@@ -1411,16 +1203,18 @@ def compute_sep_kette(items, dbg=None):
|
||||
return result
|
||||
|
||||
|
||||
def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
def compute_kreisel_umlauf(items, tolerance_mm, durchmesser_mm=800.0,
|
||||
touch_switches=None, dbg=None):
|
||||
"""Bringt fuer jeden echten Kreisel (KREISEL_SPLIT_TEILEART) alle dort
|
||||
liegenden Punkte - frei platzierte Separatoren UND BTMT-Beladung/
|
||||
-Entladung (KREISEL_UMLAUF_TEILEARTEN, per BBox-Ueberschneidung
|
||||
wie compute_sensor_zuordnung einer Kreiselhaelfte zugeordnet), die AS/ES-
|
||||
Enden aller andockenden VF_n/GF_n-Ketten (aus sep_kette, per BBox-Naehe
|
||||
wie compute_strecke_kreisel_schleus zugeordnet) UND die Kreisel-Kreisel-
|
||||
Weichen (touch_switches, siehe compute_kreisel_touch_switches) - in eine
|
||||
geschlossene Umlauf-Reihenfolge (zirkulaer, siehe DREHRICHTUNG-Attribut).
|
||||
liegenden Punkte - frei platzierte Separatoren, BTMT-Beladung/-Entladung
|
||||
UND die AS/ES-Enden aller andockenden VF_n/GF_n-Ketten (alle zusammen
|
||||
KREISEL_UMLAUF_TEILEARTEN, einheitlich per BBox-Ueberschneidung einer
|
||||
Kreiselhaelfte/eines Kreisels zugeordnet - wie compute_sensor_zuordnung;
|
||||
ein AS/ES bringt seine eigene Bounding-Box vom dauerhaften ZZ_ASES_MARKER
|
||||
mit, seit es als reales Item existiert - keine gesonderte Naeherungslogik
|
||||
mehr noetig) UND die Kreisel-Kreisel-Weichen (touch_switches, siehe
|
||||
compute_kreisel_touch_switches) - in eine geschlossene Umlauf-Reihenfolge
|
||||
(zirkulaer, siehe DREHRICHTUNG-Attribut).
|
||||
|
||||
Eine Kreisel-Kreisel-Weiche liegt (anders als ein Separator oder ein
|
||||
AS/ES-Ende) auf GENAU EINEM Punkt, gehoert aber zu ZWEI Kreiseln - man
|
||||
@@ -1441,17 +1235,15 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
"TrackIds"-Spalte (siehe export_csv.py write_kreisel_separatorliste_merkmale).
|
||||
|
||||
items = dieselbe Liste wie bei compute_neighbor_ids/compute_sensor_
|
||||
zuordnung. sep_kette = Rueckgabe von compute_sep_kette (fuer die AS/ES-
|
||||
Punkte: x/y + strecke_id). touch_switches = Rueckgabe von
|
||||
compute_kreisel_touch_switches (None/leer = wie bisher ohne Weichen im
|
||||
Umlauf).
|
||||
zuordnung. touch_switches = Rueckgabe von compute_kreisel_touch_switches
|
||||
(None/leer = wie bisher ohne Weichen im Umlauf).
|
||||
|
||||
Rueckgabe: dict {kreisel_teileid: [(punkt_key, vorgaenger_key,
|
||||
nachfolger_key), ...]} in Umlaufreihenfolge, beginnend bei Winkel 0 Grad -
|
||||
punkt_key ist die TeileId eines freien Punkts (Separator oder BTMT-
|
||||
Beladung/-Entladung, siehe KREISEL_UMLAUF_TEILEARTEN), ein
|
||||
sep_kette-Schluessel (AS/ES-Enden) oder ein touch_switches-"key" (Kreisel-
|
||||
Kreisel-Weiche). Kreisel ohne zugeordnete Punkte liefern eine leere Liste.
|
||||
punkt_key ist die TeileId eines freien Punkts (Separator, BTMT-Beladung/
|
||||
-Entladung ODER AS/ES-Ende, siehe KREISEL_UMLAUF_TEILEARTEN) oder ein
|
||||
touch_switches-"key" (Kreisel-Kreisel-Weiche). Kreisel ohne zugeordnete
|
||||
Punkte liefern eine leere Liste.
|
||||
"""
|
||||
half_tol = tolerance_mm / 2.0
|
||||
kreisel = {}
|
||||
@@ -1498,8 +1290,7 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
if not bbox:
|
||||
if dbg:
|
||||
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}): "
|
||||
f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
f"KEINE Bounding-Box - kann keinem Kreisel zugeordnet werden")
|
||||
continue
|
||||
bounds = _bounds(bbox, half_tol)
|
||||
matched_tid = None
|
||||
@@ -1513,28 +1304,11 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
if matched_tid is not None:
|
||||
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) "
|
||||
f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) "
|
||||
f"-> Kreisel {matched_tid}{dbg_highlight(teileid)}")
|
||||
f"-> Kreisel {matched_tid}")
|
||||
else:
|
||||
dbg(f" Umlauf-Kandidat {teileid!r} ({item.get('teileart', '')!r}) "
|
||||
f"bbox(cx={bbox.get('cx', 0):.0f},cy={bbox.get('cy', 0):.0f}) "
|
||||
f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})"
|
||||
f"{dbg_highlight(teileid)}")
|
||||
|
||||
# AS/ES-Enden jeder Kette mit sepliste: naechstgelegener Kreisel (wie
|
||||
# compute_strecke_kreisel_schleus._nearest), aber direkt aus sep_kette
|
||||
# statt ueber die grobe BBox-Nachbarschaft der Strecke.
|
||||
for key, eintrag in sep_kette.items():
|
||||
if eintrag["typ"] not in ("AS", "ES"):
|
||||
continue
|
||||
p = (eintrag["x"], eintrag["y"])
|
||||
best_tid, best_d = None, None
|
||||
for tid, (cap, _dr) in kreisel.items():
|
||||
q = _closest_pt_on_segment(cap["a"], cap["b"], p)
|
||||
d = math.hypot(q[0] - p[0], q[1] - p[1])
|
||||
if best_d is None or d < best_d:
|
||||
best_tid, best_d = tid, d
|
||||
if best_tid is not None:
|
||||
punkte_je_kreisel[best_tid].append((key, eintrag["x"], eintrag["y"]))
|
||||
f"-> KEIN Kreisel in Reichweite (bounds={_fmt_bounds(bounds)})")
|
||||
|
||||
# Kreisel-Kreisel-Weichen: die beiden beruehrenden Kreisel-IDs stehen
|
||||
# bereits in sw["nachbarn"] (siehe compute_kreisel_touch_switches) - kein
|
||||
@@ -1623,6 +1397,6 @@ def compute_kreisel_umlauf(items, sep_kette, tolerance_mm, durchmesser_mm=800.0,
|
||||
f"{' -> '.join(p[0] for p in sortiert)}")
|
||||
for punkt_key, vorgaenger, nachfolger in umlauf:
|
||||
dbg(f" Punkt {punkt_key!r}: vorgaenger={vorgaenger!r} "
|
||||
f"nachfolger={nachfolger!r}{dbg_highlight(punkt_key)}")
|
||||
f"nachfolger={nachfolger!r}")
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user