fix(export): freie Separatoren nach Lage in die Kette ihrer Strecke einsortieren

Eigenstaendige Separatoren, deren Zuordnung eine VF/GF nennt, die aber nicht
beim Bau der Strecke entstanden, fehlten in deren sepliste und damit in
TrackIds und Nachbarn (Mubea VF_3: 0037/0038). Sie werden jetzt beim Export
nach Lage zwischen AS und ES aufgenommen - nur im Speicher des Laufs, die
sepliste-XDATA bleibt Bauprotokoll (Lisp paart sie per Anzahl je Typ mit den
Bausteinen und fuehrt reservierte AS/ES-IDs unter der lfdnr).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-28 17:22:36 +01:00
parent c3b4bf9db3
commit 2bb00bb48f
3 changed files with 364 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
test_export_freie_separatoren.py - freie Separatoren in der Kette einer Strecke.
Laeuft komplett ohne BricsCAD (reine Python-Logik, siehe lib/export_neighbors.py).
Szenario nach einem realen Mubea-Befund (VF_3 = 0015): die sepliste aus dem
Bau kennt nur AS 0061, SEP 0062, SEP 0063, ES 0064. Die eigenstaendigen
Separatoren 0037/0038 liegen auf der Strecke, ihre Zuordnung nennt 0015 - sie
fehlten aber in TrackIds und Nachbarn. einsortieren_freie_separatoren nimmt
sie nach Lage auf.
Alle Daten sind hier fest eingetragen (Stand des Mubea-Exports 2026-10-28) und
werden aus keiner Datei gelesen - aendert sich die Mubea-Zeichnung, bleibt
dieser Test unberuehrt. Er prueft den Algorithmus, nicht die Szene.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "lib"))
from export_neighbors import ( # noqa: E402
compute_sep_kette,
einsortieren_freie_separatoren,
sepliste_einsortier_positionen,
)
from export_csv import ( # noqa: E402
map_sepliste_kette_items,
resolve_umlauf_und_sepliste_nachbarn,
write_separatorliste_merkmale,
)
TOL_MM = 100.0
def _sep(teileid, x, y, zuordnung):
return {
"teileart": "ILS 2.0 Separator",
"teileid": teileid,
"merkmale": {"Zuordnung": zuordnung},
"nachbarn": "",
"_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 150.0, "dy": 210.0},
}
def _ases(teileart, teileid, x, y, zuordnung):
return {
"teileart": teileart,
"teileid": teileid,
"_zuordnung": zuordnung,
"merkmale": {},
"nachbarn": "",
"_bbox": {"cx": x, "cy": y, "cz": 0.0, "dx": 200.0, "dy": 200.0},
}
def _mubea_vf3():
strecke = {
"teileart": "ILS 2.0 Strecke",
"teileid": "0015",
"nachbarn": "0002-L",
"merkmale": {},
"k1": "", "k2": "",
"_bbox": {"cx": -631.28, "cy": 6543.11, "cz": 1624.70,
"dx": 1018.09, "dy": 12791.0},
"sepliste": [
{"typ": "AS", "lfdnr": 1, "id": "0061"},
{"typ": "SEP", "lfdnr": 2, "id": "0062"},
{"typ": "SEP", "lfdnr": 3, "id": "0063"},
{"typ": "ES", "lfdnr": 4, "id": "0064"},
],
}
return [
strecke,
_ases("ILS Ausschleuselement", "0061", -400.00, 270.54, "0015"),
_sep("0062", -898.87, 1134.60, "0015"),
_sep("0063", -702.98, 12307.81, "0015"),
_ases("ILS Einschleuselement", "0064", -400.00, 12832.58, "0015"),
_sep("0037", -1026.00, 10723.05, "0015"),
_sep("0038", -1026.00, 5250.05, "0015"),
]
def _by_id(items, teileid):
return next(it for it in items if it.get("teileid") == teileid)
def _pipeline(items):
einsortieren_freie_separatoren(items, TOL_MM)
sep_kette = compute_sep_kette(items)
sep_kette_by_key = map_sepliste_kette_items(items, sep_kette)
write_separatorliste_merkmale(items, sep_kette_by_key)
resolve_umlauf_und_sepliste_nachbarn(items, {}, sep_kette, sep_kette_by_key, {})
return items
class TestEinsortierPositionen:
"""Reiner Rechenkern, ohne Items."""
KETTE = [(0.0, 0.0), (0.0, 1000.0), (0.0, 5000.0), (0.0, 6000.0)]
def test_zwei_kandidaten_im_selben_abschnitt_sortiert(self):
einfuegen, verworfen = sepliste_einsortier_positionen(
self.KETTE, [("B", 50.0, 4000.0), ("A", -50.0, 2000.0)])
assert einfuegen == {1: ["A", "B"]}
assert verworfen == []
def test_vor_as_und_hinter_es_verworfen(self):
einfuegen, verworfen = sepliste_einsortier_positionen(
self.KETTE, [("vor", 0.0, -500.0), ("hinter", 0.0, 6500.0)])
assert einfuegen == {}
assert sorted(verworfen) == ["hinter", "vor"]
def test_kette_um_eine_kurve(self):
"""Polygonzug statt Gerade AS->ES: ein Separator auf dem zweiten
Schenkel einer L-Kette gehoert in DEN Abschnitt, nicht nach
seiner Projektion auf die Diagonale."""
kette = [(0.0, 0.0), (0.0, 3000.0), (3000.0, 3000.0)]
einfuegen, _ = sepliste_einsortier_positionen(kette, [("S", 1500.0, 3050.0)])
assert einfuegen == {1: ["S"]}
def test_entartete_kette(self):
einfuegen, verworfen = sepliste_einsortier_positionen(
[(0.0, 0.0), (0.0, 0.0)], [("S", 10.0, 10.0)])
assert einfuegen == {}
assert verworfen == ["S"]
class TestMubeaVf3:
def test_trackids_mit_freien_separatoren(self):
items = _pipeline(_mubea_vf3())
assert _by_id(items, "0015").get("trackids") == \
"0061, 0062, 0038, 0037, 0063, 0064"
def test_nachbarn_der_kette(self):
items = _pipeline(_mubea_vf3())
assert _by_id(items, "0062")["nachbarn"] == "0061, 0038"
assert _by_id(items, "0038")["nachbarn"] == "0062, 0037"
assert _by_id(items, "0037")["nachbarn"] == "0038, 0063"
assert _by_id(items, "0063")["nachbarn"] == "0037, 0064"
def test_hinweis_am_freien_separator(self):
items = _pipeline(_mubea_vf3())
assert "0015" in _by_id(items, "0037").get("warnung", "")
assert _by_id(items, "0062").get("warnung", "") == ""
def test_fremde_zuordnung_bleibt_draussen(self):
items = _mubea_vf3()
_by_id(items, "0037")["merkmale"]["Zuordnung"] = "0002-L"
_pipeline(items)
assert _by_id(items, "0015").get("trackids") == "0061, 0062, 0038, 0063, 0064"
def test_ausserhalb_der_strecken_bbox_bleibt_draussen(self):
items = _mubea_vf3()
_by_id(items, "0038")["_bbox"]["cx"] = -3000.0
_pipeline(items)
assert _by_id(items, "0015").get("trackids") == "0061, 0062, 0037, 0063, 0064"
def test_ohne_ids_kein_einsortieren(self):
"""Altbestand ohne aufgeloeste IDs: Glieder nicht lokalisierbar ->
sepliste bleibt wie gebaut."""
items = _mubea_vf3()
for e in _by_id(items, "0015")["sepliste"]:
e["id"] = ""
einsortieren_freie_separatoren(items, TOL_MM)
assert len(_by_id(items, "0015")["sepliste"]) == 4