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:
1 parent
377a95f3dd
commit
167af5fc0b
2 files changed
+66
-26
No files matched your search
@@ -35,44 +35,59 @@ class Street:
|
|||||||
def build_street(graph: Graph, corridor, bridges) -> Street:
|
def build_street(graph: Graph, corridor, bridges) -> Street:
|
||||||
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
|
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
|
||||||
carriage = carriageway_area(graph)
|
carriage = carriageway_area(graph)
|
||||||
|
sidewalk = _sidewalk(carriage, corridor, _kerbless_zone(graph, WALK_MAX))
|
||||||
return Street(
|
return Street(
|
||||||
carriageway=carriage,
|
carriageway=carriage,
|
||||||
kerb=_kerb_lines(graph, carriage, bridges),
|
kerb=_kerb_lines(carriage, _kerbless_zone(graph, 0.2), bridges),
|
||||||
sidewalk=_sidewalk(carriage, corridor),
|
sidewalk=sidewalk,
|
||||||
lots=_lots(carriage, corridor),
|
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.
|
"""Der Streifen zwischen Fahrbahnkante und Korridorrand.
|
||||||
|
|
||||||
Ein einziger Puffer für alle Klassen reicht: Weil am Korridor beschnitten wird, bleibt bei
|
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 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
|
12 m genau die 3,0 m der Hauptstraße. `kerbless` schneidet zusätzlich heraus, wo eine Klasse
|
||||||
dadurch von selbst keinen Gehweg.
|
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
|
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
|
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.
|
Task 10/12 verlassen sich auf die MultiPolygon-Zusage.
|
||||||
"""
|
"""
|
||||||
result = carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
|
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:
|
if result.is_empty:
|
||||||
return MultiPolygon()
|
return MultiPolygon()
|
||||||
return result if result.geom_type == "MultiPolygon" else MultiPolygon([result])
|
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."""
|
"""Die Fahrbahnkante, ohne die Stücke an Gassen und auf Brücken."""
|
||||||
ohne = []
|
ohne = [] if kerbless is None else [kerbless]
|
||||||
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 += [b.buffer(0.2) for b in bridges]
|
ohne += [b.buffer(0.2) for b in bridges]
|
||||||
rand = carriage.boundary
|
rand = carriage.boundary
|
||||||
if ohne:
|
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]
|
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.
|
"""Was weder Fahrbahn noch Gehweg ist: Parkplätze, Vorplätze, Rampen.
|
||||||
|
|
||||||
Der Gehwegstreifen muss mit abgezogen werden, nicht nur die Fahrbahn. Weil Fahrbahn plus
|
Exakt gerechnet statt mit einem eigenen Puffer: Ein klassenunabhängiger 3-m-Abzug fräße an
|
||||||
zweimal Gehweg je Klasse genau den Korridor ergibt, wäre "Korridor minus Fahrbahn" sonst
|
einer Gasse (kein Gehweg) und an einer Wohnstraße (2,5 m) mehr weg, als dort Gehweg ist, und
|
||||||
wörtlich der Gehweg — und läge auf jeder Straße ab 80 m ein zweites Mal als Restfläche
|
ließe echte Pflasterflächen verschwinden.
|
||||||
darüber.
|
|
||||||
"""
|
"""
|
||||||
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])
|
return [g for g in getattr(rest, "geoms", [rest])
|
||||||
if g.geom_type == "Polygon" and g.area >= LOT_MIN]
|
if g.geom_type == "Polygon" and g.area >= LOT_MIN]
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ def test_gasse_bekommt_weder_gehweg_noch_bordstein():
|
|||||||
street = build_street(strasse("gasse"), korridor("gasse"), bridges=[])
|
street = build_street(strasse("gasse"), korridor("gasse"), bridges=[])
|
||||||
assert street.sidewalk.area < 0.01
|
assert street.sidewalk.area < 0.01
|
||||||
assert street.kerb == []
|
assert street.kerb == []
|
||||||
# Der Grenzfall fuer die MultiPolygon-Zusage: ein leeres Ergebnis kaeme ohne Normalisierung
|
# Der Grenzfall für die MultiPolygon-Zusage: ein leeres Ergebnis käme ohne Normalisierung
|
||||||
# als leeres Polygon zurueck, nicht als leeres MultiPolygon (wie carriageway_area() es seit
|
# als leeres Polygon zurück, nicht als leeres MultiPolygon (wie carriageway_area() es seit
|
||||||
# Task 6 zusagt) - Task 10/12 iterieren ueber .geoms und wuerden daran brechen.
|
# Task 6 zusagt) - Task 10/12 iterieren über .geoms und würden daran brechen.
|
||||||
assert street.sidewalk.geom_type == "MultiPolygon"
|
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)
|
gassenkorridor = LineString([(50.0, 0.0), (50.0, -5.0)]).buffer(SECTION["gasse"].carriageway / 2)
|
||||||
for line in street.kerb:
|
for line in street.kerb:
|
||||||
assert not gassenkorridor.intersects(line), "die Zufahrt bleibt frei, kein Bordstein quer davor"
|
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
|
||||||
Reference in new issue
Block a user