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