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.
119 lines
5.9 KiB
Python
119 lines
5.9 KiB
Python
"""Längsmarkierungen: Mittellinie, Spurtrenner, Randlinien."""
|
|
from __future__ import annotations
|
|
|
|
from collections import Counter
|
|
|
|
from leonida.roads import Edge, Graph, Node
|
|
from leonida.marking import DASH, GAP, WIDTH, longitudinal
|
|
from leonida.section import LANE, SECTION
|
|
|
|
|
|
def strecke(klass: str, length: float = 120.0) -> Graph:
|
|
return Graph(nodes=[Node((0.0, 0.0)), Node((length, 0.0))],
|
|
edges=[Edge(0, 1, [(0.0, 0.0), (length, 0.0)], SECTION[klass].corridor, klass)])
|
|
|
|
|
|
def arten(graph: Graph) -> Counter:
|
|
return Counter(art for _, art in longitudinal(graph))
|
|
|
|
|
|
def test_gasse_bekommt_keine_markierung():
|
|
assert longitudinal(strecke("gasse")) == []
|
|
|
|
|
|
def test_wohnstrasse_bekommt_eine_gestrichelte_gelbe_mitte():
|
|
zahl = arten(strecke("wohnstrasse"))
|
|
assert set(zahl) == {"gelb_gestrichelt"}
|
|
# 120 m in 12-m-Takten: zehn Striche.
|
|
assert zahl["gelb_gestrichelt"] == 10
|
|
assert DASH == 3.0 and GAP == 9.0
|
|
|
|
|
|
def test_hauptstrasse_bekommt_doppelte_mitte_und_zwei_randlinien():
|
|
zahl = arten(strecke("hauptstrasse"))
|
|
assert zahl["gelb_doppelt"] == 2, "zwei durchgezogene Linien nebeneinander"
|
|
assert zahl["weiss_rand"] == 2, "je eine an der Grenze zur Parkspur"
|
|
assert "weiss_gestrichelt" not in zahl, "zwei Spuren brauchen keinen Spurtrenner"
|
|
|
|
|
|
def test_boulevard_bekommt_zusaetzlich_spurtrenner():
|
|
zahl = arten(strecke("boulevard"))
|
|
assert zahl["gelb_doppelt"] == 2
|
|
assert zahl["weiss_rand"] == 2
|
|
assert zahl["weiss_gestrichelt"] == 20, "je Richtung ein gestrichelter Trenner, 10 Striche"
|
|
|
|
|
|
def test_randlinie_liegt_an_der_grenze_zur_parkspur():
|
|
section = SECTION["hauptstrasse"]
|
|
ränder = [poly for poly, art in longitudinal(strecke("hauptstrasse")) if art == "weiss_rand"]
|
|
abstände = sorted(round(abs(poly.centroid.y), 2) for poly in ränder)
|
|
erwartet = round(section.carriageway / 2 - section.parking, 2)
|
|
assert abstände == [erwartet, erwartet]
|
|
|
|
|
|
def test_gelb_doppelt_liegt_symmetrisch_mit_zehn_zentimeter_luecke():
|
|
# Fixrunde 1: Der Kommentar im Code verwechselte den Achsabstand (2*WIDTH = 20 cm) mit der
|
|
# Lücke zwischen den gemalten Linien (WIDTH = 10 cm, die nach der Pufferung um je WIDTH/2
|
|
# übrig bleibt). Beide Zahlen sind richtig, aber verschieden - hier einzeln festgehalten,
|
|
# damit sie nicht wieder verwechselt werden.
|
|
unten, oben = sorted(
|
|
(poly for poly, art in longitudinal(strecke("hauptstrasse")) if art == "gelb_doppelt"),
|
|
key=lambda poly: poly.centroid.y)
|
|
assert round(unten.centroid.y, 3) == -round(oben.centroid.y, 3), "symmetrisch um die Achse"
|
|
assert round(oben.centroid.y - unten.centroid.y, 3) == 2 * WIDTH, "Achsabstand: 20 cm"
|
|
assert round(oben.bounds[1] - unten.bounds[3], 3) == WIDTH, "Lücke zwischen den Linien: 10 cm"
|
|
|
|
|
|
def test_spurtrenner_liegt_zwischen_den_gleichgerichteten_spuren():
|
|
trenner = [poly for poly, art in longitudinal(strecke("boulevard")) if art == "weiss_gestrichelt"]
|
|
achsen = sorted({round(poly.centroid.y, 2) for poly in trenner})
|
|
assert achsen == [-LANE, LANE], "je Richtung ein Trenner, an der Grenze der beiden Spuren dieser Richtung"
|
|
|
|
|
|
def test_linien_sind_zehn_zentimeter_breit():
|
|
poly = longitudinal(strecke("wohnstrasse"))[0][0]
|
|
x0, y0, x1, y1 = poly.bounds
|
|
assert round(y1 - y0, 3) == WIDTH
|
|
assert round(x1 - x0, 3) == DASH
|
|
|
|
|
|
def test_dashed_stuerzt_bei_engem_zickzack_nicht_ab():
|
|
"""Befund 4 der Fixwelle: offset_curve() liefert bei einem engen Zickzack eine
|
|
MultiLineString statt einer LineString - substring() in _dashed() akzeptiert aber nur
|
|
LineString und wirft sonst GeometryTypeError. Nachgestellt mit einer S-Kurve (1 m Amplitude):
|
|
Bei LANE=3,5 m Versatz (Spurtrenner, nur ab Boulevard) spaltet offset_curve(-3,5) sie in zwei
|
|
Teilstücke. Vor der Korrektur bricht das hier mit GeometryTypeError ab; das ist die
|
|
Bestätigung, dass der Test ohne sie rot ist.
|
|
"""
|
|
zickzack = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (2.0, 1.0), (2.0, 0.0), (3.0, 0.0)]
|
|
graph = Graph(nodes=[Node((0.0, 0.0)), Node((3.0, 0.0))],
|
|
edges=[Edge(0, 1, zickzack, SECTION["boulevard"].corridor, "boulevard")])
|
|
ergebnis = longitudinal(graph) # darf nicht mit GeometryTypeError abbrechen
|
|
arten = {art for _, art in ergebnis}
|
|
assert "weiss_gestrichelt" in arten, "der Spurtrenner entsteht trotz Zickzack (in Teilstücken)"
|
|
|
|
|
|
def test_solid_erzeugt_kein_nullflaechen_polygon_bei_leerer_offset_curve():
|
|
"""Befund 4 der Fixwelle, zweiter (stiller) Ast: An einer sehr spitzen Kehre liefert
|
|
offset_curve() eine leere LineString. _solid() puffert sie ungeprüft zu einem
|
|
Nullflächen-Polygon statt gar nichts zurückzugeben - kein Absturz, aber eine entartete
|
|
Geometrie in der Ausgabe. Nachgestellt mit einer Kehre, die bei WIDTH (0,1 m, gelb_doppelt)
|
|
und bei rand (7 m beim Boulevard, weiss_rand) je auf einer Seite ins Leere läuft.
|
|
"""
|
|
kehre = [(0.0, 0.0), (1.0, 0.0), (0.0, 0.01)]
|
|
graph = Graph(nodes=[Node((0.0, 0.0)), Node((0.0, 0.01))],
|
|
edges=[Edge(0, 1, kehre, SECTION["boulevard"].corridor, "boulevard")])
|
|
flaechen = [p.area for p, art in longitudinal(graph) if art in ("gelb_doppelt", "weiss_rand")]
|
|
assert all(a > 0.01 for a in flaechen), (
|
|
f"eine leere offset_curve() darf kein Nullflächen-Polygon in der Ausgabe hinterlassen: {flaechen}"
|
|
)
|
|
|
|
|
|
def test_markierung_endet_am_kreuzungsrand():
|
|
graph = Graph(nodes=[Node((0.0, 0.0)), Node((120.0, 0.0)), Node((120.0, 60.0)), Node((180.0, 0.0))],
|
|
edges=[Edge(0, 1, [(0.0, 0.0), (120.0, 0.0)], 12.0, "wohnstrasse"),
|
|
Edge(1, 2, [(120.0, 0.0), (120.0, 60.0)], 12.0, "wohnstrasse"),
|
|
Edge(1, 3, [(120.0, 0.0), (180.0, 0.0)], 12.0, "wohnstrasse")])
|
|
ende = max(poly.bounds[2] for poly, _ in longitudinal(graph) if poly.bounds[2] < 120)
|
|
assert ende < 120 - SECTION["wohnstrasse"].carriageway / 2, "vor der Kreuzung ist Schluss"
|