Files
gta6/tools/extract/tests/test_marking_junction.py
T
knsoandClaude Sonnet 5 35d69c3e6c test(extract): toothless Gassen-Gehweg-Probe entfernt, Flaechenvaliditaet abgesichert
Nachpruefung der Fixwelle, drei Punkte:

test_gehweg_waechst_auch_bei_engem_winkel_nicht_in_die_gasse
(test_street.py) besteht identisch (0,0000 m²) unter dem aktuellen Code,
unter der verworfenen Fassung aus Fixrunde 4 und sogar unter einem
komplett ungefilterten walk_source = carriageway_area(graph) - nachgerechnet
mit einer Wegwerf-Kopie aller drei Varianten. Ursache: SECTION["gasse"].corridor
== SECTION["gasse"].carriageway, weshalb .difference(carriage) am Ende von
_sidewalk() den geprueften Streifen (die eigene Gassen-Fahrbahn) immer
loescht, unabhaengig davon, was _walk_source() liefert. Die Eigenschaft
selbst bleibt durch die drei benachbarten Tests
(test_gasse_bekommt_weder_gehweg_noch_bordstein,
test_gehweg_waechst_nicht_ueber_den_gassenstummel_an_geteilter_kreuzung,
test_gehwegrest_an_kreuzung_mit_mehreren_gassen_bleibt_aus) abgesichert,
die gegen dieselbe verworfene Fassung mit 53,11 bzw. 53,30 m² hart
durchfallen. Ein Test, der nicht fehlschlagen kann, taeuscht Deckung vor,
die es nicht gibt - entfernt statt umdimensioniert, weil eine Geometrie
mit nur einem Gassenarm die eigentliche Fixrunde-4-Nische (zwischen ZWEI
benachbarten Gassenstummeln) strukturell nie nachstellen kann.

test_schleife_die_auf_sich_selbst_zurueckfuehrt_bekommt_keine_markierung_daneben
(test_marking_junction.py) galt in der Nachpruefung ebenfalls als
unterscheidungslos (100% Fahrbahnanteil auch mit altem Code). Mit den
byte-genauen historischen Fassungen von junction_marks() (git show
4bf670e / ca01141) direkt in die bestehende Testfunktion eingesetzt
(Monkeypatch) zeigt sich das Gegenteil: Beide Vorstufen schlagen rot an
(schlechtester Anteil 64,9% bzw. 85,3%, an Haltelinie und aeusserstem
Zebrabalken), nur der aktuelle Code besteht. Der Test bleibt unveraendert,
mit einem Kommentar zur Gegenpruefung.

Neu: test_alle_flaechenschichten_sind_gueltige_polygone
(test_committed_roads.py) - Waechter aus Befund 5 (die alte
Koordinatenrundung erzeugte acht Selbstschnitte, ohne dass ein Test es
merkte). Prueft ueber alle 144 Chunks und alle sieben Flaechenschichten
(carriageway, land, sidewalk, surfaces, lots, bridges, paths), dass jeder
Ring ein gueltiges Polygon ergibt. Heute gruen (4.190 Flaechen, 0
ungueltige); rot nachgewiesen gegen den Datenstand vor Commit a397892
(9 ungueltige Polygone in 6 Chunks, per git show ohne Codeaenderung
nachgestellt).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-18 09:07:27 +02:00

213 lines
13 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).
Gegengeprüft in der Gehwegtextur-Korrekturrunde: Die Nachprüfung der Fixwelle vermutete, der
Test unterscheide nicht (halb=9 m deckt jede kleine Markierung nahe am Knoten ab, gleich wohin
sie zeigt) und schlage auch mit dem alten Code an dieser Schleife grün an. Nachgestellt mit den
byte-genauen historischen Fassungen von junction_marks() (Commits 4bf670e "vor Befund 3" und
ca01141 "nach Befund 3, vor dieser Korrektur hier") und exakt dieser Testfunktion (nur
junction_marks per Monkeypatch getauscht): Beide Fassungen schlagen rot an (schlechtester
Anteil 64,9 % bzw. 85,3 %, an der Haltelinie und am äußersten Zebrabalken - die runde Fuge am
Schleifenscheitel deckt zwar die meisten Balken vollständig ab, aber nicht alle). Nur der
aktuelle Code besteht (100 %). Der Test bleibt deshalb unverändert.
"""
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"