fix(extract): _sidewalk() legt nur dort Gehweg an, wo die Klasse ihn vorsieht

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>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-17 05:35:34 +02:00
1 parent 377a95f3dd
commit 167af5fc0b
2 files changed
+66 -26

No files matched your search

+37 -23
View File
@@ -35,44 +35,59 @@ class Street:
def build_street(graph: Graph, corridor, bridges) -> Street:
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
carriage = carriageway_area(graph)
sidewalk = _sidewalk(carriage, corridor, _kerbless_zone(graph, WALK_MAX))
return Street(
carriageway=carriage,
kerb=_kerb_lines(graph, carriage, bridges),
sidewalk=_sidewalk(carriage, corridor),
lots=_lots(carriage, corridor),
kerb=_kerb_lines(carriage, _kerbless_zone(graph, 0.2), bridges),
sidewalk=sidewalk,
lots=_lots(carriage, corridor, sidewalk),
)
def _sidewalk(carriage, corridor):
def _kerbless_zone(graph: Graph, extra: float):
"""Der Korridor aller Klassen ohne Bordstein, aus den vollen Mittellinien, um `extra` über die
halbe Fahrbahnbreite hinaus gepuffert.
Zweimal gebraucht, mit unterschiedlicher Reichweite: Der Bordstein hört direkt an der
Fahrbahnkante auf (extra=0,2, nur eine Fuge für einen sauberen Schnitt), aber der Gehwegpuffer
in _sidewalk() reicht bis WALK_MAX über dieselbe Kante hinaus - mit der schmalen Fuge bliebe
dort ein bis zu 2,8 m breiter Streifen übrig, den eine Klasse ohne Gehweg trotzdem als Gehweg
bekäme, sobald neben ihr mehr gemessene Fläche liegt als ihre eigene Breite (siehe Bericht).
Die volle Mittellinie statt ribbon(), damit auch der Anteil der Kreuzungsfläche abgedeckt ist,
an dem die bordsteinlose Klasse einmündet.
"""
zonen = [LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + extra,
cap_style="flat", join_style="mitre", mitre_limit=1.5)
for edge in graph.edges
if not SECTION[edge.klass].kerb and len(edge.points) >= 2]
return unary_union(zonen) if zonen else None
def _sidewalk(carriage, corridor, kerbless):
"""Der Streifen zwischen Fahrbahnkante und Korridorrand.
Ein einziger Puffer für alle Klassen reicht: Weil am Korridor beschnitten wird, bleibt bei
12 m Korridor und 7 m Fahrbahn genau der 2,5-m-Gehweg der Wohnstraße übrig, bei 18 m und
12 m genau die 3,0 m der Hauptstraße. Die Gasse füllt ihren Korridor ganz aus und bekommt
dadurch von selbst keinen Gehweg.
12 m genau die 3,0 m der Hauptstraße. `kerbless` schneidet zusätzlich heraus, wo eine Klasse
laut SECTION gar keinen Gehweg vorsieht (die Gasse) - der Korridor allein reicht dafür nicht:
er füllt zwar meist genau die Gassenbreite, aber sobald daneben mehr gemessene Fläche liegt,
würde der WALK_MAX-Puffer sie sonst fälschlich zu Gehweg machen.
Normalisiert wie carriageway_area() auf MultiPolygon (Task 6): Ein einseitiger Gehweg oder
ein leeres Ergebnis kämen sonst als einfaches Polygon bzw. als leere Geometrie zurück, und
Task 10/12 verlassen sich auf die MultiPolygon-Zusage.
"""
result = carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
if kerbless is not None:
result = result.difference(kerbless)
if result.is_empty:
return MultiPolygon()
return result if result.geom_type == "MultiPolygon" else MultiPolygon([result])
def _kerb_lines(graph: Graph, carriage, bridges) -> list[LineString]:
def _kerb_lines(carriage, kerbless, bridges) -> list[LineString]:
"""Die Fahrbahnkante, ohne die Stücke an Gassen und auf Brücken."""
ohne = []
for edge in graph.edges:
if SECTION[edge.klass].kerb or len(edge.points) < 2:
continue
# Die volle Mittellinie statt ribbon(): Das Band einer Gasse endet vor der Kreuzung, und
# der Bordstein bliebe genau im Einmündungstrichter stehen. Dass dabei auch die Kante der
# Hauptstraße über die Einmündungsbreite unterbrochen wird, ist richtig so — an einer
# Zufahrt ist der Bordstein abgesenkt.
ohne.append(LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + 0.2,
cap_style="flat", join_style="mitre", mitre_limit=1.5))
ohne = [] if kerbless is None else [kerbless]
ohne += [b.buffer(0.2) for b in bridges]
rand = carriage.boundary
if ohne:
@@ -81,15 +96,14 @@ def _kerb_lines(graph: Graph, carriage, bridges) -> list[LineString]:
return [g for g in teile if g.geom_type == "LineString" and g.length >= KERB_MIN]
def _lots(carriage, corridor) -> list[Polygon]:
def _lots(carriage, corridor, sidewalk) -> list[Polygon]:
"""Was weder Fahrbahn noch Gehweg ist: Parkplätze, Vorplätze, Rampen.
Der Gehwegstreifen muss mit abgezogen werden, nicht nur die Fahrbahn. Weil Fahrbahn plus
zweimal Gehweg je Klasse genau den Korridor ergibt, wäre "Korridor minus Fahrbahn" sonst
wörtlich der Gehweg — und läge auf jeder Straße ab 80 m ein zweites Mal als Restfläche
darüber.
Exakt gerechnet statt mit einem eigenen Puffer: Ein klassenunabhängiger 3-m-Abzug fräße an
einer Gasse (kein Gehweg) und an einer Wohnstraße (2,5 m) mehr weg, als dort Gehweg ist, und
ließe echte Pflasterflächen verschwinden.
"""
rest = corridor.difference(carriage.buffer(WALK_MAX, join_style="round"))
rest = corridor.difference(carriage).difference(sidewalk)
return [g for g in getattr(rest, "geoms", [rest])
if g.geom_type == "Polygon" and g.area >= LOT_MIN]
+29 -3
View File
@@ -32,9 +32,9 @@ 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.
# 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"
@@ -104,3 +104,29 @@ def test_bordstein_frei_an_kurzer_gasseneinmuendung():
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