Der eigene Fix aus der letzten Runde (_lots() zieht carriage.buffer(WALK_MAX) ab) hatte einen neuen Fehler eingeführt: Der klassenunabhängige 3-m-Abzug frisst bei der Gasse (walk=0,0, gar kein Gehweg) und bei der Wohnstraße (walk=2,5, nicht 3,0) mehr weg, als dort tatsächlich Gehweg ist - eine echte Pflasterfläche neben einer Gasse wurde bis zu WALK_MAX komplett als Gehweg gezählt statt als Restfläche. An der Wurzel behoben: _kerbless_zone(graph, extra) berechnet den Korridor aller Klassen ohne Bordstein aus der vollen Kantenmittellinie, mit extra als Parameter statt festem Wert - _kerb_lines() ruft sie mit extra=0.2 auf (schmale Fuge an der physischen Kante, unveraendert), _sidewalk() mit extra=WALK_MAX (muss die volle Gehweg-Reichweite abdecken, sonst bliebe ein bis zu 2,8 m breiter Streifen faelschlich Gehweg). Ein einzelner fester Radius fuer beide Zwecke funktioniert nicht: zu schmal laesst echten Gehweg als Gehweg einer Klasse ohne Gehweg stehen, zu breit schneidet er benachbarten, legitimen Bordstein weg - nachgerechnet mit der vom Fix-Auftrag selbst vorgeschlagenen Testfixture, bevor der feste Wert uebernommen wurde. _lots() rechnet jetzt exakt (corridor.difference(carriage).difference(sidewalk)) statt mit einem eigenen Puffer, wie vorgegeben. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
133 lines
7.4 KiB
Python
133 lines
7.4 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 für die MultiPolygon-Zusage: ein leeres Ergebnis käme ohne Normalisierung
|
|
# als leeres Polygon zurück, nicht als leeres MultiPolygon (wie carriageway_area() es seit
|
|
# Task 6 zusagt) - Task 10/12 iterieren über .geoms und würden 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"
|
|
|
|
|
|
def test_pflasterflaeche_neben_der_gasse_ist_restflaeche_kein_gehweg():
|
|
# Fixrunde 2: die Gasse hat laut SECTION überhaupt keinen Gehweg (walk=0.0). Eine echte,
|
|
# 3 m tiefe, 100 m lange Pflasterfläche direkt neben ihr - z.B. ein durchgehender Hofstreifen,
|
|
# den die Kartierung mit zur Straßenfläche zählt - darf nicht zu Gehweg werden, nur weil
|
|
# WALK_MAX=3.0 dort zufällig genau hinreicht. Sie muss als Restfläche erscheinen.
|
|
pflaster = box(0, 3, 100, 6)
|
|
weit = unary_union([korridor("gasse"), pflaster])
|
|
street = build_street(strasse("gasse"), weit, bridges=[])
|
|
assert street.sidewalk.area < 0.01, "die Gasse bekommt so wenig Gehweg wie sonst: keinen"
|
|
assert any(lot.area > 250 for lot in street.lots), "die Pflasterfläche bleibt als Restfläche"
|
|
|
|
|
|
def test_fahrbahn_gehweg_und_rest_ergeben_wieder_den_korridor():
|
|
# Die drei Teile dürfen sich weder überlappen noch eine Lücke lassen - unabhängig vom
|
|
# LOT_MIN-Filter in street.lots prüfen wir hier den *rohen* Rest (vor dem Filter), denn nur
|
|
# der ergänzt Fahrbahn und Gehweg exakt wieder zum Korridor.
|
|
weit = unary_union([korridor(), box(40, 6, 70, 36), box(10, 8, 12, 9)])
|
|
street = build_street(strasse(), weit, bridges=[])
|
|
roh = weit.difference(street.carriageway).difference(street.sidewalk)
|
|
assert street.carriageway.intersection(street.sidewalk).area < 1e-6
|
|
assert street.carriageway.intersection(roh).area < 1e-6
|
|
assert street.sidewalk.intersection(roh).area < 1e-6
|
|
bilanz = street.carriageway.area + street.sidewalk.area + roh.area
|
|
assert abs(bilanz - weit.area) < 1e-6
|