From 015d15d82a7037cd0d92afbdad3b8a8446a47946 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Fri, 18 Sep 2026 07:47:24 +0200 Subject: [PATCH] =?UTF-8?q?fix(extract):=20Kreuzungsmarkierung=20am=20tats?= =?UTF-8?q?=C3=A4chlichen=20Punkt=20verankern,=20nicht=20am=20Knoten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nacharbeit zu Befund 3, beim Nachmessen am echten Netz entdeckt: Die erste Korrektur (Richtung aus origin -> interpolate(reach) statt aus points[1]) reichte an zwei Stellen nicht. Erstens kann eine Schleife geradewegs von ihrem Knoten weg- und auf demselben Weg wieder zurücklaufen - am echten Netz eine 21,63 m lange Boulevard-Schleife, deren Punktliste den Hinweg exakt rückwärts wiederholt. Landet reach nahe dieser Kehre, zeigt die Sehne vom Ursprung weiter nach außen, obwohl die Kante dort längst umkehrt: Die Markierung lag an dieser einen Schleife weiterhin vollständig (100 %) neben der Fahrbahn. Die Richtung kommt jetzt aus dem letzten Teilstück von substring(line, 0, reach) - der tatsächlichen Anfahrtsrichtung an der Schnittstelle, gleich wohin die Kante von dort aus weiterläuft. Zweitens blieb "origin" (der Kreuzungsknoten) der Anker für Balken und Haltelinie. origin und der Punkt bei Streckenlänge reach fallen nur auf einer geraden Kante zusammen - mit der neuen Anfahrtsrichtung, aber dem Knoten als Anker, läge selbst eine gerade Fortsetzung ab reach systematisch daneben, sobald die Kante zwischen Knoten und reach von der reinen Ursprungsrichtung abweicht. Anker ist jetzt der tatsächliche Punkt auf der Kante bei Streckenlänge min(reach, länge). Nachgemessen am echten Netz: von 452,65 m² (vor Befund 3) über 342,66 m² (nur die erste Korrektur) auf 110,06 m² (0,25 % der Gesamtfläche) - beide Korrekturen zusammen eine Verbesserung um 75,7 %. Der Rest ist eine engere Spielart desselben Problems: Ein Bogen kann auch noch zwischen reach und reach + STOP_SETBACK + STOP_DEPTH abknicken, was ein einzelner Richtungsstrahl nicht mehr auffangen kann - das verlangte die vom Befund selbst genannte, größere Alternative ("die Markierung entlang der tatsächlichen Kante aufbauen"), die diese Fixwelle nicht umsetzt. Siehe Fixwelle-Bericht für die Einzelheiten und die verbleibenden Einzelfälle. Neuer Test mit den echten Schleifenmaßen (9 m hin, 6 m quer, reach 11 m): vor dieser Korrektur lag die gesamte Markierung an der Schleife daneben, danach vollständig darauf. --- tools/extract/leonida/marking.py | 53 ++++++++++++++------ tools/extract/tests/test_marking_junction.py | 28 +++++++++++ 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/tools/extract/leonida/marking.py b/tools/extract/leonida/marking.py index d032809..856de62 100644 --- a/tools/extract/leonida/marking.py +++ b/tools/extract/leonida/marking.py @@ -137,17 +137,42 @@ def junction_marks(graph: Graph, node: int) -> list[tuple[Polygon, str]]: for points in richtungen: line = LineString(points) länge = line.length - # Die Richtung kommt aus der tatsächlichen Kante bei Streckenlänge "reach", nicht aus - # dem zweiten Stützpunkt (Befund 3 der Fixwelle): Ein Knick konnte innerhalb der ersten - # reach Meter liegen - reach ist der Kreuzungsradius plus Luft, oft mehrere Meter -, - # während die alte Richtung (Anfang -> zweiter Stützpunkt) bis reach + STOP_SETBACK + - # STOP_DEPTH (bis 16 m) unverändert weiterlief. Am echten Netz lagen dadurch 452,65 m² - # Markierungsfläche (1,03 % der Gesamtfläche) neben statt auf der Fahrbahn, darunter - # ganze Haltelinien. min(reach, länge) schützt kurze Arme, die schon vorher an der - # Prüfung unten scheitern und dann gar nicht gezeichnet werden. - punkt = line.interpolate(min(reach, länge)) - along = _unit(origin, (punkt.x, punkt.y)) + # Befund 3 der Fixwelle: "along" kam aus der Richtung origin -> points[1], also allein + # aus dem ersten Segment der Zufahrt, während Zebrastreifen und Haltelinie bis + # reach + STOP_SETBACK + STOP_DEPTH (bis 16 m) auf dieser Geraden aufgespannt wurden. + # Knickte die Kante innerhalb dieser Strecke ab, landete die Markierung daneben statt + # darauf - am echten Netz gemessen 452,65 m² (1,03 % der Gesamtfläche), darunter ganze + # Haltelinien. + # + # Zwei Korrekturen daran, beide am echten Netz nachgewiesen nötig: + # + # 1) Die Richtung kommt aus dem letzten Teilstück von substring(line, 0, reach), nicht + # aus der Sehne vom Ursprung zu interpolate(reach): Eine Schleife kann geradewegs von + # ihrem Knoten weg- und auf demselben Weg wieder zurücklaufen (eine 21,63 m lange + # Schleife im echten Netz, deren Punktliste den Hinweg exakt rückwärts wiederholt). + # Landet reach nahe dieser Kehre, zeigt die Sehne vom Ursprung weiter nach außen, + # obwohl die Kante dort längst umkehrt. Das letzte Teilstück von substring() nimmt + # dagegen die tatsächliche Anfahrtsrichtung an der Schnittstelle an, gleich wohin die + # Kante von dort aus weiterläuft. + # 2) Anker für Balken und Haltelinie ist der tatsächliche Punkt auf der Kante bei + # Streckenlänge min(reach, länge), nicht der Kreuzungsknoten selbst: origin und + # dieser Punkt fallen nur auf einer geraden Kante zusammen. Mit der Anfahrtsrichtung + # aus (1), aber dem Knoten als Anker, läge selbst eine gerade Fortsetzung ab reach + # systematisch daneben, sobald die Kante zwischen Knoten und reach von der reinen + # Ursprungsrichtung abweicht. + # + # Mit beiden Korrekturen bleiben am echten Netz noch 110,06 m² (0,25 %) außerhalb der + # Fahrbahn - ein Bogen kann auch noch zwischen reach und reach + STOP_SETBACK + + # STOP_DEPTH abknicken, und ein einzelner Richtungsstrahl kann das nicht mehr auffangen; + # das verlangte die vom Befund selbst genannte, größere Alternative ("die Markierung + # entlang der tatsächlichen Kante aufbauen"), die diese Fixwelle nicht umsetzt. Siehe + # Bericht der Fixwelle für die Einzelheiten. min(reach, länge) schützt kurze Arme, die + # schon vorher an der Prüfung unten scheitern und dann gar nicht gezeichnet werden. + teil = substring(line, 0.0, min(reach, länge)) + rand = [] if teil.is_empty else list(teil.coords) + along = _unit(rand[-2], rand[-1]) if len(rand) > 1 else (0.0, 0.0) across = (-along[1], along[0]) + basis = rand[-1] if rand else origin halb = SECTION[edge.klass].carriageway / 2 # reach kommt vom breitesten Arm der ganzen Kreuzung, gilt hier aber für diesen einen # Arm - ein kurzer Arm an einer sonst breiten Kreuzung reicht oft nicht bis dorthin. Am @@ -168,8 +193,8 @@ def junction_marks(graph: Graph, node: int) -> list[tuple[Polygon, str]]: n = math.floor((2 * halb + BAR) / (2 * BAR)) offset = -(n - 1) * BAR for _ in range(n): - mitte = (origin[0] + across[0] * offset, origin[1] + across[1] * offset) - out.append((_rect(mitte, along, across, reach, CROSSWALK_DEPTH, BAR), "zebra")) + mitte = (basis[0] + across[0] * offset, basis[1] + across[1] * offset) + out.append((_rect(mitte, along, across, 0.0, CROSSWALK_DEPTH, BAR), "zebra")) offset += 2 * BAR if länge >= reach + STOP_SETBACK + STOP_DEPTH: # Haltelinie quer, nur über die rechte Hälfte aus Sicht der Zufahrt. "across" ist @@ -179,8 +204,8 @@ def junction_marks(graph: Graph, node: int) -> list[tuple[Polygon, str]]: # ist deshalb +across, ohne weiteres Vorzeichen: eine Probe mit offset_curve() auf # der Einfahrtlinie bestätigt das (Fixrunde zu Task 9 - der Auftrag hatte hier # -across und lag damit auf der Gegenspur). - mitte = (origin[0] + across[0] * halb / 2, origin[1] + across[1] * halb / 2) - out.append((_rect(mitte, along, across, reach + STOP_SETBACK, STOP_DEPTH, halb), + mitte = (basis[0] + across[0] * halb / 2, basis[1] + across[1] * halb / 2) + out.append((_rect(mitte, along, across, STOP_SETBACK, STOP_DEPTH, halb), "haltelinie")) return out diff --git a/tools/extract/tests/test_marking_junction.py b/tools/extract/tests/test_marking_junction.py index f881fec..973cfbd 100644 --- a/tools/extract/tests/test_marking_junction.py +++ b/tools/extract/tests/test_marking_junction.py @@ -155,6 +155,34 @@ def test_markierung_folgt_dem_geknickten_arm_nicht_der_ersten_richtung(): assert sum(1 for a in anteile if a > 0.9) >= len(anteile) - 1, "fast jeder Balken muss klar auf der Fahrbahn liegen" +def test_schleife_die_auf_sich_selbst_zurueckfuehrt_bekommt_keine_markierung_daneben(): + """Zweite Korrektur zu Befund 3, am echten Netz entdeckt: Eine Schleife kann geradewegs von + ihrem Knoten weg- und auf demselben Weg wieder zurücklaufen (am echten Netz eine 21,63 m + lange Boulevard-Schleife, deren Punktliste den Hinweg exakt rückwärts wiederholt). Landet + reach nahe dieser Kehre, zeigt die Sehne vom Ursprung zu interpolate(reach) weiter nach außen, + obwohl die Kante dort längst umkehrt - mit dieser Zwischenstufe der Korrektur lag die gesamte + Markierung (Zebra und Haltelinie) an genau dieser Schleife vollständig neben der Fahrbahn + (0 % statt 100 %, nachgerechnet gegen die echten Koordinaten). Anker am tatsächlichen Punkt + plus Richtung aus dem letzten Teilstück von substring() behebt das. + + Nachgebaut mit denselben Maßen wie am echten Netz (9 m hin, 6 m quer, macht 21,63 m Umweg; + reach=11 m, wie es der zweite Boulevard-Arm hier erzwingt). + """ + spitze = [(0.0, 0.0), (9.0, -6.0), (0.0, 0.0)] + graph = Graph( + nodes=[Node((0.0, 0.0)), Node((-80.0, 0.0)), Node((9.0, -6.0))], + edges=[Edge(0, 1, [(0.0, 0.0), (-80.0, 0.0)], SECTION["boulevard"].corridor, "boulevard"), + Edge(0, 0, spitze, SECTION["boulevard"].corridor, "boulevard")], + ) + halb = SECTION["boulevard"].carriageway / 2 + fahrbahn = LineString(spitze).buffer(halb, cap_style="flat") + ziel = [(p, a) for p, a in junction_marks(graph, 0) if p.centroid.x > 1] # nur die Schleife + assert ziel, "die Schleife muss Markierungen bekommen (zwei bordsteintragende Arme)" + for poly, art in ziel: + anteil = poly.intersection(fahrbahn).area / poly.area + assert anteil > 0.99, f"{art} liegt nur zu {anteil:.0%} auf der Schleifen-Fahrbahn" + + def test_kurzer_arm_an_breiter_kreuzung_bleibt_ohne_markierung(): # reach = setback() kommt vom breitesten anliegenden Arm (hier Boulevard, halbe Fahrbahn 9 m) # und gilt für alle Zufahrten gleich - ein kurzer Arm reicht dann nicht mehr bis dorthin. Am