_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>
105 lines
4.5 KiB
Python
105 lines
4.5 KiB
Python
"""Die Straße als Mulde: Loch in der Bodenplatte, Fahrbahn darunter, Bordstein dazwischen.
|
|
|
|
Warum die Fahrbahn nach unten und nicht der Gehweg nach oben (Entwurf, Abschnitt "Die Mulde"):
|
|
Läge der Gehweg 15 cm hoch, hätte er überall dort eine Kante ins Nichts, wo kein Haus
|
|
anschließt - an jedem Park, jeder Baulücke, jedem Strandzugang. So bleibt die ganze Welt auf
|
|
y = 0, und der Bordstein ist genau das, was er in Wirklichkeit ist: die Wand zwischen Gehweg
|
|
und Fahrbahn.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from shapely.geometry import LineString, MultiPolygon, Polygon
|
|
from shapely.ops import unary_union
|
|
|
|
from .roads import Graph
|
|
from .section import SECTION, WALK_MAX, carriageway_area
|
|
|
|
# Die Tiefe steht hier als dokumentierter Wert; gebaut wird sie im Browser (geometry.js).
|
|
DEPTH = 0.15
|
|
# Kürzere Bordsteinstücke sind Rechenreste an Kreuzungsecken, keine Kante, die jemand sieht.
|
|
KERB_MIN = 2.0
|
|
# Ab hier ist ein Reststück eine Fläche (Parkplatz, Vorplatz, Rampe) und keine Zacke.
|
|
LOT_MIN = 200.0
|
|
|
|
|
|
@dataclass
|
|
class Street:
|
|
carriageway: object
|
|
kerb: list = field(default_factory=list)
|
|
sidewalk: object = None
|
|
lots: list = field(default_factory=list)
|
|
|
|
|
|
def build_street(graph: Graph, corridor, bridges) -> Street:
|
|
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
|
|
carriage = carriageway_area(graph)
|
|
return Street(
|
|
carriageway=carriage,
|
|
kerb=_kerb_lines(graph, carriage, bridges),
|
|
sidewalk=_sidewalk(carriage, corridor),
|
|
lots=_lots(carriage, corridor),
|
|
)
|
|
|
|
|
|
def _sidewalk(carriage, corridor):
|
|
"""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.
|
|
|
|
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 result.is_empty:
|
|
return MultiPolygon()
|
|
return result if result.geom_type == "MultiPolygon" else MultiPolygon([result])
|
|
|
|
|
|
def _kerb_lines(graph: Graph, carriage, 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 += [b.buffer(0.2) for b in bridges]
|
|
rand = carriage.boundary
|
|
if ohne:
|
|
rand = rand.difference(unary_union(ohne))
|
|
teile = getattr(rand, "geoms", [rand])
|
|
return [g for g in teile if g.geom_type == "LineString" and g.length >= KERB_MIN]
|
|
|
|
|
|
def _lots(carriage, corridor) -> 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.
|
|
"""
|
|
rest = corridor.difference(carriage.buffer(WALK_MAX, join_style="round"))
|
|
return [g for g in getattr(rest, "geoms", [rest])
|
|
if g.geom_type == "Polygon" and g.area >= LOT_MIN]
|
|
|
|
|
|
def punch(polygons, carriage) -> list[Polygon]:
|
|
"""Die Fahrbahn aus Bodenplatte oder Belag herausschneiden."""
|
|
out = []
|
|
for poly in polygons:
|
|
rest = poly.difference(carriage)
|
|
out += [g for g in getattr(rest, "geoms", [rest])
|
|
if g.geom_type == "Polygon" and g.area > 0.5]
|
|
return out
|