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.
203 lines
12 KiB
Python
203 lines
12 KiB
Python
"""Haltelinien und Zebrastreifen."""
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
|
|
import pytest
|
|
from shapely.geometry import LineString
|
|
|
|
from leonida.roads import Edge, Graph, Node
|
|
from leonida.marking import BAR, CROSSWALK_DEPTH, junction_marks, markings
|
|
from leonida.section import SECTION, setback
|
|
|
|
|
|
def kreuzung(klassen=("hauptstrasse",) * 4, arm: float = 80.0) -> Graph:
|
|
punkte = [(arm, 0.0), (-arm, 0.0), (0.0, arm), (0.0, -arm)]
|
|
return Graph(nodes=[Node((0.0, 0.0))] + [Node(p) for p in punkte],
|
|
edges=[Edge(0, i + 1, [(0.0, 0.0), p], SECTION[k].corridor, k)
|
|
for i, (p, k) in enumerate(zip(punkte, klassen))])
|
|
|
|
|
|
def test_jeder_arm_bekommt_zebra_und_haltelinie():
|
|
zahl = Counter(art for _, art in junction_marks(kreuzung(), 0))
|
|
assert zahl["haltelinie"] == 4, "eine je Zufahrt"
|
|
assert zahl["zebra"] > 4 * 5, "je Arm mehrere Balken"
|
|
|
|
|
|
def test_zebra_liegt_am_kreuzungsrand():
|
|
rand = setback(kreuzung(), 0)
|
|
zebra = [p for p, art in junction_marks(kreuzung(), 0) if art == "zebra"]
|
|
# Der Arm nach Osten: Balken zwischen Kreuzungsrand und Kreuzungsrand plus Tiefe. Die
|
|
# Querbedingung muss enger sein als die halbe Fahrbahnbreite (6 m): Bei |y| < 20 träfe sie
|
|
# auch Nord- und Südarm-Balken nahe der Ecke (deren eigene Querachse dort ohnehin liegt), die
|
|
# aber bei x um 8-11 m liegen wie der Ostarm selbst - falsches Grün durch Verwechslung, keine
|
|
# echte Prüfung (Fixrunde zu Task 9).
|
|
halb = SECTION["hauptstrasse"].carriageway / 2
|
|
ost = [p for p in zebra if p.centroid.x > 0 and abs(p.centroid.y) < halb]
|
|
assert ost, "der Ostarm hat Balken"
|
|
for poly in ost:
|
|
assert rand - 0.1 <= poly.bounds[0] <= rand + CROSSWALK_DEPTH
|
|
|
|
|
|
def test_haltelinie_liegt_hinter_dem_zebrastreifen():
|
|
rand = setback(kreuzung(), 0)
|
|
halt = [p for p, art in junction_marks(kreuzung(), 0) if art == "haltelinie"]
|
|
# Wie bei test_zebra_liegt_am_kreuzungsrand: |y| < 20 wäre zu locker. Die Haltelinie des
|
|
# Südarms erfüllt sie ebenfalls (centroid bei x=3, |y|=12,5 < 20) - next() traf bisher nur
|
|
# zufällig zuerst auf den Ostarm, weil er in "echte" zuerst kommt, nicht weil der Filter ihn
|
|
# tatsächlich isoliert (Fixrunde 1 des Koordinators zu Task 9, vom Prüfer nachgerechnet).
|
|
halb = SECTION["hauptstrasse"].carriageway / 2
|
|
ost = next(p for p in halt if p.centroid.x > 0 and abs(p.centroid.y) < halb)
|
|
assert ost.bounds[0] > rand + CROSSWALK_DEPTH, "erst Zebrastreifen, dann Haltelinie"
|
|
|
|
|
|
def test_haltelinie_deckt_nur_die_rechte_haelfte():
|
|
halt = [p for p, art in junction_marks(kreuzung(), 0) if art == "haltelinie"]
|
|
# Derselbe zu lockere Filter wie oben - siehe dortigen Kommentar.
|
|
halb = SECTION["hauptstrasse"].carriageway / 2
|
|
ost = next(p for p in halt if p.centroid.x > 0 and abs(p.centroid.y) < halb)
|
|
# Zufahrt von Osten nach Westen, also Fahrtrichtung (-1, 0): Rechts davon liegt y > 0 - per
|
|
# offset_curve()-Konvention nachgerechnet (negativ = rechts der Linienrichtung), nicht nur
|
|
# behauptet. Der Auftrag ging von y < 0 aus; das war die Gegenspur (Fixrunde zu Task 9).
|
|
assert ost.centroid.y > 0
|
|
assert round(ost.bounds[3] - ost.bounds[1], 1) == round(SECTION["hauptstrasse"].carriageway / 2, 1)
|
|
|
|
|
|
def test_zebrabalken_liegen_symmetrisch_zur_fahrbahnmitte():
|
|
"""Befund 8 der Fixwelle: Der alte Startversatz (-halb + BAR/2, Takt 2*BAR) liess die
|
|
Balkenreihe an einem Fahrbahnrand bündig sitzen und am gegenüberliegenden bis zu eine
|
|
Taktbreite frei - 0,6 m bei Hauptstraße/Boulevard, 0,4 m bei Wohnstraße, statt der laut
|
|
Entwurf verlangten Aufteilung "Balken 60 cm, Lücke 60 cm" über die ganze Breite. n Balken,
|
|
symmetrisch um die Fahrbahnmitte gestartet, lassen an beiden Rändern denselben Rest frei.
|
|
"""
|
|
halb = SECTION["hauptstrasse"].carriageway / 2
|
|
zebra = [p for p, art in junction_marks(kreuzung(), 0) if art == "zebra"]
|
|
# Wie in test_zebra_liegt_am_kreuzungsrand: nur der Ostarm, klar von den Nachbararmen isoliert.
|
|
ost = sorted((p.centroid.y for p in zebra if p.centroid.x > 0 and abs(p.centroid.y) < halb))
|
|
rand_unten = (ost[0] - BAR / 2) - (-halb)
|
|
rand_oben = halb - (ost[-1] + BAR / 2)
|
|
assert rand_unten == pytest.approx(rand_oben, abs=1e-6), (
|
|
f"Ränder ungleich: unten {rand_unten:.3f} m, oben {rand_oben:.3f} m"
|
|
)
|
|
assert rand_unten == pytest.approx(0.3, abs=1e-6), "bei 12 m Fahrbahn bleiben 0,3 m an jedem Rand"
|
|
|
|
|
|
def test_arm_der_eine_gasse_ist_bekommt_keinen_zebrastreifen():
|
|
graph = kreuzung(("hauptstrasse", "hauptstrasse", "hauptstrasse", "gasse"))
|
|
marks = junction_marks(graph, 0)
|
|
# Wie bei test_zebra_liegt_am_kreuzungsrand: |x| < 20 wäre zu locker und träfe auch Ost- und
|
|
# Westarm-Balken nahe der Ecke, obwohl deren Querachse (|x| bis 9,5 m für Zebra, 12,5 m für
|
|
# die Haltelinie) klar außerhalb der Südarm-Fahrbahnhälfte liegt (Fixrunde zu Task 9).
|
|
halb = SECTION["hauptstrasse"].carriageway / 2
|
|
süd = [art for p, art in marks if p.centroid.y < -1 and abs(p.centroid.x) < halb]
|
|
assert süd == [], "der Gassenarm bleibt blank"
|
|
|
|
|
|
def test_kreuzung_aus_lauter_gassen_bekommt_nichts():
|
|
assert junction_marks(kreuzung(("gasse",) * 4), 0) == []
|
|
|
|
|
|
def test_markings_sammelt_laengs_und_kreuzung():
|
|
arten = {art for _, art in markings(kreuzung())}
|
|
assert "gelb_doppelt" in arten and "zebra" in arten and "haltelinie" in arten
|
|
|
|
|
|
def test_schleife_bekommt_haltelinie_fuer_beide_richtungen():
|
|
# Wie test_schleife_bekommt_zwei_stummel_kein_loch_im_belag in test_junction.py: Eine
|
|
# Schleife hängt mit beiden Enden am selben Knoten. graph.incident() liefert sie nur einmal,
|
|
# aber sie stellt zwei getrennte Abgangsrichtungen dar (Verkehr kann von beiden Seiten in die
|
|
# Kreuzung einfahren) - am echten Netz betrifft das 28 von 690 Kreuzungen mit einer
|
|
# bordsteintragenden Schleife. Ohne die zweite Richtung fehlten Haltelinie und Zebrastreifen
|
|
# auf der einen Seite der Schleife komplett.
|
|
graph = Graph(
|
|
nodes=[Node((0.0, 0.0)), Node((-100.0, 0.0)), Node((0.0, -100.0))],
|
|
edges=[Edge(0, 0, [(0.0, 0.0), (0.0, 20.0), (20.0, 20.0), (20.0, 0.0), (0.0, 0.0)],
|
|
SECTION["hauptstrasse"].corridor, "hauptstrasse"),
|
|
Edge(0, 1, [(0.0, 0.0), (-100.0, 0.0)], SECTION["hauptstrasse"].corridor, "hauptstrasse"),
|
|
Edge(0, 2, [(0.0, 0.0), (0.0, -100.0)], SECTION["hauptstrasse"].corridor, "hauptstrasse")])
|
|
zahl = Counter(art for _, art in junction_marks(graph, 0))
|
|
assert zahl["haltelinie"] == 4, "zwei gerade Arme plus zwei Richtungen der Schleife"
|
|
|
|
|
|
def test_markierung_folgt_dem_geknickten_arm_nicht_der_ersten_richtung():
|
|
"""Befund 3 der Fixwelle: "along" kam aus der Richtung origin -> points[1], also allein aus
|
|
dem ersten Segment der Zufahrt. Knickt die Kante innerhalb von reach + STOP_SETBACK +
|
|
STOP_DEPTH ab (bis 16 m), blieben Zebrastreifen und Haltelinie auf der Verlängerung des
|
|
ersten Segments stehen, statt der tatsächlichen Kante zu folgen. Am echten Netz lagen dadurch
|
|
452,65 m² Markierungsfläche (1,03 %) neben der Fahrbahn, darunter ganze Haltelinien.
|
|
|
|
Nachgebaut mit einer Zufahrt, die 1 m nach dem Knoten um 90° abknickt: reach (5,5 m bei
|
|
Wohnstraße) und erst recht die Haltelinie bei 9,7-10,3 m liegen weit hinter dem Knick. Der
|
|
zweite Arm zeigt nach Westen, weit weg von jeder möglichen Richtung des ersten - er dient nur
|
|
dazu, die Mindestzahl von zwei Zufahrten mit Bordstein zu erfüllen (`echte`).
|
|
"""
|
|
arm = [(0.0, 0.0), (0.0, -1.0), (20.0, -1.0)]
|
|
graph = Graph(
|
|
nodes=[Node((0.0, 0.0)), Node((-80.0, 0.0)), Node((20.0, -1.0))],
|
|
edges=[Edge(0, 1, [(0.0, 0.0), (-80.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
|
|
Edge(0, 2, arm, SECTION["wohnstrasse"].corridor, "wohnstrasse")],
|
|
)
|
|
halb = SECTION["wohnstrasse"].carriageway / 2
|
|
fahrbahn = LineString(arm).buffer(halb, cap_style="flat")
|
|
# Der Westarm liegt bei x <= -80 * 0,15 (Kreuzungsradius) .. deutlich unter -4; alles mit
|
|
# centroid.x > -4 gehört zur geknickten Zufahrt, gleich in welche Richtung sie zeigt.
|
|
ziel = [(p, a) for p, a in junction_marks(graph, 0) if p.centroid.x > -4]
|
|
haltelinie = next(p for p, a in ziel if a == "haltelinie")
|
|
anteil_halt = haltelinie.intersection(fahrbahn).area / haltelinie.area
|
|
assert anteil_halt > 0.9, (
|
|
f"Haltelinie liegt nur zu {anteil_halt:.0%} auf der (geknickten) Fahrbahn - sie folgt "
|
|
"noch der Richtung des ersten Segments statt der tatsächlichen Kante"
|
|
)
|
|
zebra = [p for p, a in ziel if a == "zebra"]
|
|
anteile = [p.intersection(fahrbahn).area / p.area for p in zebra]
|
|
mittel = sum(anteile) / len(anteile)
|
|
assert mittel > 0.7, f"Zebrastreifen im Mittel nur zu {mittel:.0%} auf der Fahrbahn"
|
|
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
|
|
# echten Netz sind 38 von 1.508 geprüften Armstummeln zu kurz für die Haltelinie (44 von 1.536
|
|
# tatsächlichen Zufahrten, die Schleifen aus der vorigen Fixrunde zählen doppelt), im
|
|
# schlimmsten Fall um 10,6 m (Fixrunde 1 des Koordinators zu Task 9). Der Nordarm hier ist nur
|
|
# 10 m lang; reach allein liegt schon bei 11 m, also klar dahinter - jede Markierung, die
|
|
# trotzdem entstünde, läge neben der Straße.
|
|
graph = Graph(
|
|
nodes=[Node((0.0, 0.0)), Node((100.0, 0.0)), Node((0.0, 10.0))],
|
|
edges=[Edge(0, 1, [(0.0, 0.0), (100.0, 0.0)], SECTION["boulevard"].corridor, "boulevard"),
|
|
Edge(0, 2, [(0.0, 0.0), (0.0, 10.0)], SECTION["hauptstrasse"].corridor, "hauptstrasse")])
|
|
marks = junction_marks(graph, 0)
|
|
jenseits_des_kurzen_arms = [p for p, _ in marks if p.centroid.y > 10.0]
|
|
assert jenseits_des_kurzen_arms == [], "der 10 m lange Nordarm bekommt nichts, reach ist 11 m"
|
|
arten = {art for _, art in marks}
|
|
assert {"zebra", "haltelinie"} <= arten, "der breite, lange Ostarm behält beides"
|