Files
gta6/tools/extract/tests/test_street.py
T
knsoandClaude Sonnet 5 377a95f3dd fix(extract): Gehwegabzug in _lots(), MultiPolygon-Zusage, Bordstein an Zufahrten
_lots() zog bisher nur die Fahrbahn vom Korridor ab, nicht den Gehwegstreifen - weil
carriageway + 2*walk je Klasse genau den Korridor ergibt, war "Korridor minus Fahrbahn"
wörtlich der Gehweg und legte ihn ab 80 m Straßenlänge ein zweites Mal als Restfläche
darüber. _sidewalk() hielt die MultiPolygon-Zusage nicht (derselbe Fehler wie bei
carriageway_area() in Task 6): ein leeres Ergebnis kam als leeres Polygon zurück statt
als leeres MultiPolygon. _kerb_lines() nimmt jetzt die volle Kantenmittellinie statt
ribbon() für den Gassen-/Brückenausschluss, weil ribbon() an der Kreuzung endet und der
Bordstein sonst im Einmündungstrichter stehen bliebe.

Die im Fix-Auftrag genannte Test-Konfiguration für den dritten Punkt (30 m lange Gasse)
zeigte den gemeldeten Fehler nicht: ribbon() liefert dort kein None (Setback 5,5 m,
30-5,5=24,5 >= 1,0), der alte Ausschluss deckte die Gasse also schon vollständig ab. Der
Fehler braucht eine Gasse kürzer als setback+1=6,5 m - erst dann liefert trimmed_line()
None und der alte Code baut gar keinen Ausschluss. Testkonfiguration entsprechend auf
eine 5 m kurze Gasse korrigiert, siehe task-7-report.md für die vollständige Herleitung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 05:03:46 +02:00

107 lines
5.8 KiB
Python

"""Die Mulde: Löcher in der Bodenplatte, Bordsteinlinien, Gehwegflächen, Restflächen."""
from __future__ import annotations
from shapely.geometry import LineString, Point, box
from shapely.ops import unary_union
from leonida.roads import Edge, Graph, Node
from leonida.section import SECTION
from leonida.street import LOT_MIN, build_street, punch
def strasse(klass: str = "wohnstrasse") -> Graph:
return Graph(nodes=[Node((0.0, 0.0)), Node((100.0, 0.0))],
edges=[Edge(0, 1, [(0.0, 0.0), (100.0, 0.0)], SECTION[klass].corridor, klass)])
def korridor(klass: str = "wohnstrasse"):
halb = SECTION[klass].corridor / 2
return box(0, -halb, 100, halb)
def test_gehweg_fuellt_genau_den_rest_des_korridors():
street = build_street(strasse(), korridor(), bridges=[])
section = SECTION["wohnstrasse"]
# 100 m lang, zwei Streifen von 2,5 m. Die Enden sind offen, deshalb genau die Fläche.
assert round(street.sidewalk.area) == round(100 * 2 * section.walk)
assert street.sidewalk.intersection(street.carriageway).area < 0.01
assert street.sidewalk.geom_type == "MultiPolygon"
def test_gasse_bekommt_weder_gehweg_noch_bordstein():
street = build_street(strasse("gasse"), korridor("gasse"), bridges=[])
assert street.sidewalk.area < 0.01
assert street.kerb == []
# Der Grenzfall fuer die MultiPolygon-Zusage: ein leeres Ergebnis kaeme ohne Normalisierung
# als leeres Polygon zurueck, nicht als leeres MultiPolygon (wie carriageway_area() es seit
# Task 6 zusagt) - Task 10/12 iterieren ueber .geoms und wuerden daran brechen.
assert street.sidewalk.geom_type == "MultiPolygon"
def test_bordstein_laeuft_entlang_der_fahrbahnkante():
street = build_street(strasse(), korridor(), bridges=[])
gesamt = sum(line.length for line in street.kerb)
# Zwei Längsseiten von 100 m; die kurzen Stirnseiten sind mit 7 m kaum der Rede wert.
assert 190 < gesamt < 220
def test_bruecken_bekommen_keinen_bordstein():
bridge = box(40, -10, 60, 10)
street = build_street(strasse(), korridor(), bridges=[bridge])
for line in street.kerb:
assert not bridge.buffer(-0.5).intersects(line), "über der Brücke bleibt die Kante frei"
def test_loch_hat_genau_die_form_der_fahrbahn():
platte = box(-50, -50, 150, 50)
street = build_street(strasse(), korridor(), bridges=[])
rest = punch([platte], street.carriageway)
assert len(rest) == 1
assert not rest[0].contains(Point(50, 0)), "die Fahrbahnmitte ist ausgeschnitten"
assert rest[0].contains(Point(50, 20)), "daneben bleibt die Platte"
assert abs(unary_union(rest).area + street.carriageway.area - platte.area) < 1.0
def test_kleine_reststuecke_fallen_weg_grosse_bleiben():
# Ein Korridor mit einer 30 x 30 m großen Ausbuchtung (ein Parkplatz) und einem Splitter.
# Der Splitter braucht eine Lücke zum Korridorrand: Läge er wie die Ausbuchtung direkt an
# y = 6 an, verschmölze schon unary_union() ihn mit ihr, weil beide dieselbe durchgehende
# Kante berühren - dann bliebe für LOT_MIN unten gar nichts mehr zu filtern übrig.
weit = unary_union([korridor(), box(40, 6, 70, 36), box(10, 8, 12, 9)])
street = build_street(strasse(), weit, bridges=[])
assert all(lot.area >= LOT_MIN for lot in street.lots)
assert any(lot.area > 500 for lot in street.lots), "der Parkplatz bleibt"
# Nur die Ausbuchtung (30x30 minus den 0,5 m Gehwegsaum an ihrer Unterkante = 885 m²) bleibt.
# Die beiden Seitenstreifen des Basiskorridors (je 250 m²) sind jetzt korrekt als Gehweg
# erkannt, nicht mehr als zusätzliche Restfläche gezählt - genau der Fehler aus Fixrunde 1,
# Punkt 1 (_lots() zog vorher nur die Fahrbahn ab, nicht auch den Gehwegstreifen).
assert len(street.lots) == 1, "der 2 m² große Splitter ist herausgefallen, die Seitenstreifen sind Gehweg"
def test_gerade_strasse_ohne_ausbuchtung_hat_keine_restflaeche():
# Fixrunde 1, Punkt 1: "Korridor minus Fahrbahn" allein ist bei jeder Klasse wörtlich der
# Gehweg (SECTION garantiert carriageway + 2*walk == corridor). Ohne den Gehwegabzug in
# _lots() wären die beiden 2,5-m-Seitenstreifen (je 250 m², ab 80 m Länge über LOT_MIN)
# eine zweite, asphaltfarbene Restfläche genau dort, wo schon der Gehweg liegt.
street = build_street(strasse(), korridor(), bridges=[])
assert street.lots == []
def test_bordstein_frei_an_kurzer_gasseneinmuendung():
# Fixrunde 1, Punkt 2, mit einer nachgerechneten Korrektur der Gassenlänge (siehe Bericht):
# Bei der vom Auftrag genannten 30 m langen Gasse liefert ribbon() *kein* None (Setback 5,5 m,
# 30-5,5=24,5 >= 1,0) - ribbon() schneidet dort ganz regulär zurück, und der alte Ausschluss
# deckt die Gasse bereits vollständig ab. Der Fehler braucht eine Gasse kürzer als
# setback+1=6,5 m: Erst dann liefert trimmed_line() None, der alte Code fügt für diese Kante
# überhaupt keinen Ausschluss ein (der Walrus-Filter "is not None" schlägt fehl), und der
# Bordstein läuft quer über die Einmündung, statt an ihr abgesenkt zu sein.
graph = Graph(nodes=[Node((0.0, 0.0)), Node((50.0, 0.0)), Node((100.0, 0.0)), Node((50.0, -5.0))],
edges=[Edge(0, 1, [(0.0, 0.0), (50.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
Edge(1, 2, [(50.0, 0.0), (100.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
Edge(1, 3, [(50.0, 0.0), (50.0, -5.0)], SECTION["gasse"].corridor, "gasse")])
assert graph.degree(1) == 3
street = build_street(graph, box(-20, -40, 120, 20), bridges=[])
gassenkorridor = LineString([(50.0, 0.0), (50.0, -5.0)]).buffer(SECTION["gasse"].carriageway / 2)
for line in street.kerb:
assert not gassenkorridor.intersects(line), "die Zufahrt bleibt frei, kein Bordstein quer davor"