From 2d8f4e730c394a6acc773160f1faf609402a8a17 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Thu, 17 Sep 2026 01:52:19 +0200 Subject: [PATCH] fix(extract): Kontraktion zieht jetzt alle anliegenden Kanten mit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _contract_short() ließ Kanten, die schon vorher am überlebenden Knoten hingen, auf ihrer alten Koordinate stehen - nur die vom eingezogenen Knoten umgehängten folgten dem neuen Mittelpunkt. Da Kontraktion beidseitig Grad >= 3 voraussetzt, hat der überlebende Knoten danach immer mindestens Grad 4, und _merge_chains() fasst nur Grad-2-Knoten an: Der Fehler konnte sich nie von selbst heilen. Am heutigen Mosaik wirkungslos (die Funktion greift dort kein einziges Mal), aber real für jeden Ausschnitt mit zwei nahen Kreuzungen - genau der Fall, den die Funktion behandeln soll. Dazu zwei Kleinigkeiten: _median_width() deckte den Kurzkanten-Fallback (unter 2 * WIDTH_MARGIN) bisher nicht ab, und ein Kommentar in _merge_chains() war ein Zeichen zu weit eingerückt. Co-Authored-By: Claude Sonnet 5 --- tools/extract/leonida/roads.py | 8 +++-- tools/extract/tests/test_roads_clean.py | 43 ++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/tools/extract/leonida/roads.py b/tools/extract/leonida/roads.py index c6e3c9f..d84ae86 100644 --- a/tools/extract/leonida/roads.py +++ b/tools/extract/leonida/roads.py @@ -213,7 +213,7 @@ def _merge_chains(graph: Graph) -> None: first, second = (graph.edges[i] for i in incident) if first.a == first.b or second.a == second.b: continue # eine Schleife (z.B. ein kleiner Ring am Skelett) hat kein - # zweites Ende - mit der Nachbarkante lässt sie sich nicht verketten + # zweites Ende - mit der Nachbarkante lässt sie sich nicht verketten points = _join(first, second, graph.nodes[node].point) if points is None: continue @@ -251,9 +251,13 @@ def _contract_short(graph: Graph) -> None: for other in graph.edges: if other.a == edge.b: other.a = edge.a - other.points = [graph.nodes[edge.a].point, *other.points[1:]] if other.b == edge.b: other.b = edge.a + # Auch die Kanten, die schon vorher am überlebenden Knoten hingen, müssen ihm + # folgen: Er ist gerade auf den Mittelpunkt zwischen beiden Kreuzungen gewandert. + if other.a == edge.a: + other.points = [graph.nodes[edge.a].point, *other.points[1:]] + if other.b == edge.a: other.points = [*other.points[:-1], graph.nodes[edge.a].point] changed = True break diff --git a/tools/extract/tests/test_roads_clean.py b/tools/extract/tests/test_roads_clean.py index fff24da..65ef447 100644 --- a/tools/extract/tests/test_roads_clean.py +++ b/tools/extract/tests/test_roads_clean.py @@ -4,7 +4,8 @@ from __future__ import annotations import numpy as np from leonida.coords import SLICE -from leonida.roads import (CLASS_WIDTH, Edge, Graph, Node, SPUR_MIN, clean, measure_widths) +from leonida.roads import (CLASS_WIDTH, CONTRACT_MAX, Edge, Graph, Node, SPUR_MIN, clean, + measure_widths) def punkt(x: float, y: float) -> tuple[float, float]: @@ -103,6 +104,20 @@ def test_klasse_ist_die_naechstliegende_regelbreite(): assert CLASS_WIDTH["hauptstrasse"] == 18.0 +def test_kurze_kante_ohne_randregel_misst_sich_ganz(): + # Kante unter 2 * WIDTH_MARGIN (16 m): "end <= start" bei der Randregel greift ins Leere - + # _median_width() muss dann auf die ganze Kante zurückfallen statt auf ein leeres Stück. + # Alle anderen Breitentests laufen mit Kanten ab 160 m und erreichen diesen Zweig nie. + distance = np.full((50, 50), 3.0) # halbe Breite 3 m -> 6 m + graph = Graph(nodes=[Node(punkt(SLICE["x0"] + 5, SLICE["y1"] - 20)), + Node(punkt(SLICE["x0"] + 15, SLICE["y1"] - 20))], + edges=[Edge(0, 1, gerade(punkt(SLICE["x0"] + 5, SLICE["y1"] - 20), + punkt(SLICE["x0"] + 15, SLICE["y1"] - 20)))]) + out = measure_widths(graph, distance) + assert out.edges[0].width == 6.0 + assert out.edges[0].klass == "gasse" + + # Die folgenden zwei Fälle hat erst der Gegencheck am echten Mosaik gezeigt (Schritt 5): Kein # synthetischer Test bildet sie ab, aber am echten Netz treten sie zuverlässig auf. @@ -134,3 +149,29 @@ def test_schleife_an_einem_durchgangsknoten_bringt_clean_nicht_zum_absturz(): Edge(1, 1, ring)]) out = clean(graph) assert any(e.a == e.b for e in out.edges), "die Schleife bleibt erhalten, statt clean() abstürzen zu lassen" + + +# Diesen Fall hat nicht das Mosaik gezeigt, sondern die Codeprüfung von Fixrunde 1: Die +# Kontraktion griff bisher an keiner Stelle im heutigen Netz, ihr Fehler blieb dort wirkungslos. + + +def test_kontraktion_zieht_alle_anliegenden_kanten_zum_neuen_knoten(): + # Zwei Kreuzungen mit je Grad 3, 3 m auseinander (unter CONTRACT_MAX) - _contract_short() + # zieht sie zu einem Knoten in der Mitte zusammen. Nicht nur die zwei Kanten, die vorher am + # eingezogenen Knoten hingen, müssen dem neuen Punkt folgen, sondern auch die zwei, die schon + # vorher am überlebenden Knoten hingen: Der ist gerade selbst auf den Mittelpunkt gewandert. + assert 3 < CONTRACT_MAX + graph = Graph(nodes=[Node(punkt(0, 0)), Node(punkt(3, 0)), + Node(punkt(-50, 0)), Node(punkt(0, 50)), + Node(punkt(53, 0)), Node(punkt(3, 50))], + edges=[Edge(0, 1, gerade(punkt(0, 0), punkt(3, 0))), + Edge(0, 2, gerade(punkt(0, 0), punkt(-50, 0))), + Edge(0, 3, gerade(punkt(0, 0), punkt(0, 50))), + Edge(1, 4, gerade(punkt(3, 0), punkt(53, 0))), + Edge(1, 5, gerade(punkt(3, 0), punkt(3, 50)))]) + out = clean(graph) + ziel = out.nodes[0].point + assert ziel == punkt(1.5, 0.0), "der überlebende Knoten steht auf dem Mittelpunkt beider Kreuzungen" + fehler = [(edge.a, edge.b, edge.points) for edge in out.edges + if (edge.a == 0 and edge.points[0] != ziel) or (edge.b == 0 and edge.points[-1] != ziel)] + assert not fehler, f"Kanten mit veralteter Koordinate am zusammengezogenen Knoten: {fehler}"