Befund 3: In junction_marks() kam die Richtung einer Zufahrt aus origin -> points[1], also nur aus dem ersten Segment der Kante. Haltelinie und Zebrastreifen wurden aber bis reach + STOP_SETBACK + STOP_DEPTH (bis 16 m) auf dieser Geraden aufgespannt - knickte die Kante innerhalb dieser Strecke ab, landete die Markierung daneben statt darauf. Am echten Netz (gemessen über alle 144 committeten Chunks): 452,65 m² von 43.906 m² Markierungsfläche (1,03 %) liegen außerhalb der Fahrbahn, davon 291,44 m² Zebra und 147,68 m² Haltelinie; die schlimmsten Einzelfälle sind vollständige Haltelinien mit 5,41 m². Die Richtung kommt jetzt aus line.interpolate(min(reach, länge)) - dem tatsächlichen Punkt auf der Kante bei Streckenlänge reach - statt aus dem zweiten Stützpunkt. Befund 4: offset_curve() liefert bei einem engen Zickzack eine MultiLineString statt einer LineString; substring() in _dashed() akzeptiert nur LineString und wirft dann GeometryTypeError. Am heutigen Mosaik tritt das nicht auf, aber eine geänderte Korrektur oder eine eingehängte Kante mit engem Knick hätte den Vier-Minuten-Lauf abgebrochen. Zweiter, stiller Ast: Eine sehr spitze Kehre lässt offset_curve() leer zurückkommen; _solid() puffert das ungeprüft zu einem Nullflächen-Polygon. _offset() normalisiert jetzt auf eine Liste von LineStrings (leer -> [], MultiLineString -> ihre geoms), _dashed() und _solid() iterieren darüber; longitudinal() flacht die jetzt listenwertigen _solid()-Aufrufe ab. Beide Befunde mit Tests, die ohne die jeweilige Korrektur rot sind: der Zickzack-Fall bricht am unkorrigierten Code mit GeometryTypeError ab, der Kehre-Fall lieferte zwei Nullflächen-Polygone unter den Rand-/ Doppellinien.
155 lines
8.9 KiB
Python
155 lines
8.9 KiB
Python
"""Haltelinien und Zebrastreifen."""
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
|
|
from shapely.geometry import LineString
|
|
|
|
from leonida.roads import Edge, Graph, Node
|
|
from leonida.marking import 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_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_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"
|