diff --git a/lib/export_csv.py b/lib/export_csv.py index 1026ec0..1c3a019 100644 --- a/lib/export_csv.py +++ b/lib/export_csv.py @@ -592,8 +592,6 @@ def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette zaehler = {} for sp in schleus: elem_nr += 1 - naechste_id += 1 - teileid = f"{naechste_id:04d}" teileart = sp["teileart"] zaehler[teileart] = zaehler.get(teileart, 0) + 1 planquadrat = (compute_planquadrat(sp["x"], sp["y"], planquadrat_cfg) @@ -603,6 +601,7 @@ def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette 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. @@ -611,7 +610,18 @@ def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette # 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). - lfdnr = sepliste[0]["lfdnr"] if sp["ende_label"] == "Anfang" else sepliste[-1]["lfdnr"] + 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"] @@ -622,6 +632,22 @@ def build_strecke_schleus_items(items, planquadrat_cfg, start_elem_nr, sep_kette 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, @@ -997,6 +1023,19 @@ def process_blocks(blocks, lookup, dbg=None): if bname in SKIP_BLOCKS: continue + # 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. + if matches_any(bname, BLOCKPATTERNS.get("pattern_ases_marker", [])): + continue + # Omniflo Bogen oder Weiche if bname in lookup: typ, eintrag = lookup[bname] diff --git a/tests/test_export_separatorliste.py b/tests/test_export_separatorliste.py index a54b621..c6faf50 100644 --- a/tests/test_export_separatorliste.py +++ b/tests/test_export_separatorliste.py @@ -21,6 +21,7 @@ from export_neighbors import compute_sep_kette, compute_kreisel_umlauf # noqa: from export_csv import ( # noqa: E402 build_strecke_schleus_items, map_separator_kette_items, + max_numeric_teileid, write_separatorliste_merkmale, ) @@ -145,3 +146,59 @@ class TestSeparatorlisteMerkmal: write_separatorliste_merkmale([strecke], {}) assert "trackids" not in strecke assert "Separatorlist" not in strecke["merkmale"] + + +class TestMarkerIdWiederverwendung: + """build_strecke_schleus_items darf die ID eines dauerhaften + ZZ_ASES_MARKER (siehe csv:asmark-aktualisieren, Lisp/export.lsp; im + sepliste-Eintrag als Feld "id" gespiegelt) nicht ignorieren und + stattdessen eine neue erfinden - genau das erzeugte die vom Nutzer + gemeldete TeileId-Luecke (Marker-ID blieb ungenutzt, gleichzeitig + entstand fuer denselben Punkt eine zusaetzliche, hoehere ID).""" + + def test_marker_id_wiederverwendet_statt_neu_gemintet(self): + items = _strecke_mit_zwei_separatoren() + strecke = next(it for it in items if it.get("teileid") == "0010") + # AS = sepliste[0] (lfdnr=1), ES = sepliste[-1] (lfdnr=4) - siehe + # ssg-sepliste-sammeln (Lisp/ssg_ks_insert.lsp): AS immer zuerst, + # ES immer zuletzt. Bewusst NIEDRIGER als die Proxy-Separator-IDs + # 0052/0053 gewaehlt - genau wie im real beobachteten HundM05-Export + # (Marker 0024-0033, Separator-Proxies 0034-0045). + strecke["sepliste"][0]["id"] = "0024" + strecke["sepliste"][-1]["id"] = "0033" + + sep_kette = compute_sep_kette(items) + sep_kette_by_key = {} + build_strecke_schleus_items( + items, None, 100, sep_kette=sep_kette, + sep_kette_by_key=sep_kette_by_key) + + as_item = sep_kette_by_key["0010#1"] + es_item = sep_kette_by_key["0010#4"] + assert as_item["teileid"] == "0024", ( + f'AS-Zeile hat teileid={as_item["teileid"]!r}, erwartet die ' + f'wiederverwendete Marker-ID "0024"') + assert es_item["teileid"] == "0033", ( + f'ES-Zeile hat teileid={es_item["teileid"]!r}, erwartet die ' + f'wiederverwendete Marker-ID "0033"') + + # Kein spaeterer Mint in DIESEM Lauf darf eine wiederverwendete + # Marker-ID erneut vergeben - der laufende Zaehler muss mindestens + # auf die hoechste wiederverwendete ID hochgezogen worden sein. + assert max_numeric_teileid(items) >= 33 + + def test_strecke_ohne_marker_id_mintet_wie_bisher(self): + """Fehlt das "id"-Feld (Altbestand-Wrapper vor der Marker-Funktion, + oder Marker-Zuweisung in Lisp fehlgeschlagen), bleibt das Mint- + Verhalten unveraendert - dieselben TeileIds wie vor diesem Fix.""" + items = _strecke_mit_zwei_separatoren() + sep_kette = compute_sep_kette(items) + sep_kette_by_key = {} + build_strecke_schleus_items( + items, None, 100, sep_kette=sep_kette, + sep_kette_by_key=sep_kette_by_key) + + as_item = sep_kette_by_key["0010#1"] + es_item = sep_kette_by_key["0010#4"] + assert as_item["teileid"] == "0054" + assert es_item["teileid"] == "0055"