Der Bordstein ist keine Ausstattung wie ein Gehweg, sondern die Kante, an der der Asphalt endet — und die hat auch eine Hinterhofgasse. _kerb_lines() schneidet darum nur noch die Brücken aus dem Fahrbahnrand: Dort steckt die Fahrbahn im Deck, eine Kante stünde in der Luft. Gehweg und Markierung bleiben der Gasse weiterhin fern. Das Feld kerb im Querschnitt entfällt. Es steuerte zuletzt nur noch Gehweg und Markierung und war aus walk > 0 ableitbar; kerb_only heißt deshalb jetzt walk_only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
135 lines
6.7 KiB
Python
135 lines
6.7 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, junction_area, ribbon, to_multipolygon
|
|
|
|
# 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)
|
|
sidewalk = _sidewalk(_walk_source(graph), carriage, corridor)
|
|
return Street(
|
|
carriageway=carriage,
|
|
kerb=_kerb_lines(carriage, bridges),
|
|
sidewalk=sidewalk,
|
|
lots=_lots(carriage, corridor, sidewalk),
|
|
)
|
|
|
|
|
|
def _walk_source(graph: Graph):
|
|
"""Die Fahrbahnteile, an denen ein Gehweg entstehen darf.
|
|
|
|
Alle Kanten mit Bordstein, dazu an jeder Kreuzung der Kreuzungsanteil nur der Armstummel mit
|
|
Bordstein, mit derselben Eckrundung wie junction_area() (Befund 7 der Fixwelle). Fixrunde 5
|
|
hatte die Rundung noch ganz weggelassen: Die volle, auf ALLE Arme angewandte Rundung brachte
|
|
in Fixrunde 4 zu viel Gasse mit, weil sie die einspringende Ecke zwischen zwei benachbarten
|
|
Gassenstummeln zu einer Nische auffüllte. Der eigentliche Fehler war aber, Gassenarme
|
|
überhaupt in die zu rundende Fläche aufzunehmen - nicht die Rundung selbst. Ohne sie blieb
|
|
stattdessen an jeder gewöhnlichen Kreuzung (mehrere Arme mit Gehweg, keine Gasse
|
|
beteiligt) eine Lücke zwischen der ungerundeten Gehwegquelle und der gerundeten Fahrbahn:
|
|
0,26-2,2 m² je Ecke bei 690 Kreuzungen, weil `carriage` (carriageway_area(), gerundet mit
|
|
FILLET=6) und `walk_source` (ungerundet) an der Kreuzungsecke verschieden geformt waren und
|
|
eine reine Dilatation um WALK_MAX=3 eine Reflexecke nicht nachzeichnet.
|
|
|
|
junction_area(walk_only=True) rundet jetzt nur den Kreuzungsanteil der bordsteintragenden
|
|
Arme: Eine Gasse liefert dabei keinen Arm in die zu schließende Fläche und kann deshalb auch
|
|
keine Nische mit einem Nachbararm bilden, gewöhnliche Kreuzungsecken bekommen aber dieselbe
|
|
Rundung wie die Fahrbahn und schließen die Lücke.
|
|
"""
|
|
teile = [band for k, edge in enumerate(graph.edges)
|
|
if SECTION[edge.klass].walk and (band := ribbon(graph, k)) is not None]
|
|
teile += [area for node in range(len(graph.nodes)) if graph.degree(node) >= 3
|
|
and (area := junction_area(graph, node, walk_only=True)) is not None and not area.is_empty]
|
|
return unary_union(teile) if teile else None
|
|
|
|
|
|
def _sidewalk(walk_source, carriage, corridor):
|
|
"""Der Streifen zwischen Fahrbahnkante und Korridorrand.
|
|
|
|
Wächst aus walk_source (nur die Fahrbahnteile mit Bordstein) statt aus der ganzen Fahrbahn:
|
|
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 - und
|
|
weil der Puffer erst gar nicht aus einer Gasse heraus wächst, kann er auch keinen fremden
|
|
Gehweg verschlucken, der zufällig nah an einer liegt. `.difference(carriage)` schneidet die
|
|
tatsächliche Fahrbahn heraus (auch die einer benachbarten Gasse), nicht nur die Quelle.
|
|
|
|
Normalisiert wie carriageway_area() auf MultiPolygon (Task 6, Fixrunde 4): Ein einseitiger
|
|
Gehweg oder ein leeres Ergebnis kämen sonst als einfaches Polygon bzw. als leere Geometrie
|
|
zurück; an einer Berührstelle kann intersection()/difference() sogar eine GeometryCollection
|
|
mit zusätzlichen entarteten Linien liefern, an der MultiPolygon([result]) mit TypeError
|
|
abstürzt (der Mechanismus ist in test_section.py mit einer erzwungenen GeometryCollection
|
|
reproduziert). to_multipolygon() fängt alle drei Fälle einheitlich ab.
|
|
"""
|
|
if walk_source is None or walk_source.is_empty:
|
|
return MultiPolygon()
|
|
result = walk_source.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
|
|
return to_multipolygon(result)
|
|
|
|
|
|
def _kerb_lines(carriage, bridges) -> list[LineString]:
|
|
"""Die ganze Fahrbahnkante, nur ohne die Stücke auf Brücken.
|
|
|
|
Bis zur Nutzerentscheidung vom 18.09.2026 blieben auch die Gassen frei: Der Entwurf sah für sie
|
|
weder Bordstein noch Gehweg noch Markierung vor (Hinterhofgassen in Miami Beach). Der Bordstein
|
|
gehört jetzt an jede Straße - wo Asphalt an Land grenzt, endet er mit einer Kante. Gehweg und
|
|
Markierung bleiben der Gasse weiterhin fern, das ändert die Entscheidung nicht.
|
|
|
|
Brücken bleiben ausgenommen: Dort steckt die Fahrbahn im Deck, eine Kante stünde in der Luft.
|
|
"""
|
|
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, sidewalk) -> list[Polygon]:
|
|
"""Was weder Fahrbahn noch Gehweg ist: Parkplätze, Vorplätze, Rampen.
|
|
|
|
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).difference(sidewalk)
|
|
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
|