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>
This commit is contained in:
1 parent
45a1a62103
commit
377a95f3dd
2 files changed
+69
-10
No files matched your search
@@ -10,11 +10,11 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from shapely.geometry import LineString, Polygon
|
from shapely.geometry import LineString, MultiPolygon, Polygon
|
||||||
from shapely.ops import unary_union
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
from .roads import Graph
|
from .roads import Graph
|
||||||
from .section import SECTION, WALK_MAX, carriageway_area, ribbon
|
from .section import SECTION, WALK_MAX, carriageway_area
|
||||||
|
|
||||||
# Die Tiefe steht hier als dokumentierter Wert; gebaut wird sie im Browser (geometry.js).
|
# Die Tiefe steht hier als dokumentierter Wert; gebaut wird sie im Browser (geometry.js).
|
||||||
DEPTH = 0.15
|
DEPTH = 0.15
|
||||||
@@ -39,7 +39,7 @@ def build_street(graph: Graph, corridor, bridges) -> Street:
|
|||||||
carriageway=carriage,
|
carriageway=carriage,
|
||||||
kerb=_kerb_lines(graph, carriage, bridges),
|
kerb=_kerb_lines(graph, carriage, bridges),
|
||||||
sidewalk=_sidewalk(carriage, corridor),
|
sidewalk=_sidewalk(carriage, corridor),
|
||||||
lots=_lots(corridor, carriage),
|
lots=_lots(carriage, corridor),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -50,14 +50,29 @@ def _sidewalk(carriage, corridor):
|
|||||||
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. Die Gasse füllt ihren Korridor ganz aus und bekommt
|
||||||
dadurch von selbst keinen Gehweg.
|
dadurch von selbst keinen Gehweg.
|
||||||
|
|
||||||
|
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.
|
||||||
"""
|
"""
|
||||||
return carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
|
result = carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
|
||||||
|
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(graph: Graph, carriage, 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 = [band.buffer(0.2) for k, edge in enumerate(graph.edges)
|
ohne = []
|
||||||
if not SECTION[edge.klass].kerb and (band := ribbon(graph, k)) is not None]
|
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:
|
||||||
@@ -66,8 +81,15 @@ 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(corridor, carriage) -> list[Polygon]:
|
def _lots(carriage, corridor) -> list[Polygon]:
|
||||||
rest = corridor.difference(carriage)
|
"""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.
|
||||||
|
"""
|
||||||
|
rest = corridor.difference(carriage.buffer(WALK_MAX, join_style="round"))
|
||||||
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]
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Die Mulde: Löcher in der Bodenplatte, Bordsteinlinien, Gehwegflächen, Restflächen."""
|
"""Die Mulde: Löcher in der Bodenplatte, Bordsteinlinien, Gehwegflächen, Restflächen."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from shapely.geometry import Point, box
|
from shapely.geometry import LineString, Point, box
|
||||||
from shapely.ops import unary_union
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
from leonida.roads import Edge, Graph, Node
|
from leonida.roads import Edge, Graph, Node
|
||||||
@@ -25,12 +25,17 @@ def test_gehweg_fuellt_genau_den_rest_des_korridors():
|
|||||||
# 100 m lang, zwei Streifen von 2,5 m. Die Enden sind offen, deshalb genau die Fläche.
|
# 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 round(street.sidewalk.area) == round(100 * 2 * section.walk)
|
||||||
assert street.sidewalk.intersection(street.carriageway).area < 0.01
|
assert street.sidewalk.intersection(street.carriageway).area < 0.01
|
||||||
|
assert street.sidewalk.geom_type == "MultiPolygon"
|
||||||
|
|
||||||
|
|
||||||
def test_gasse_bekommt_weder_gehweg_noch_bordstein():
|
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
|
||||||
|
# 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():
|
def test_bordstein_laeuft_entlang_der_fahrbahnkante():
|
||||||
@@ -66,4 +71,36 @@ def test_kleine_reststuecke_fallen_weg_grosse_bleiben():
|
|||||||
street = build_street(strasse(), weit, bridges=[])
|
street = build_street(strasse(), weit, bridges=[])
|
||||||
assert all(lot.area >= LOT_MIN for lot in street.lots)
|
assert all(lot.area >= LOT_MIN for lot in street.lots)
|
||||||
assert any(lot.area > 500 for lot in street.lots), "der Parkplatz bleibt"
|
assert any(lot.area > 500 for lot in street.lots), "der Parkplatz bleibt"
|
||||||
assert len(street.lots) == 2, "der 2 m² große Splitter ist tatsächlich herausgefallen"
|
# 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"
|
||||||
Reference in new issue
Block a user