Befund 7 (seit Task 7 geparkt): carriageway_area() rundet die einspringenden Ecken einer Kreuzung mit FILLET=6,0 (Schließen: aufblasen, dann wieder schrumpfen), die Gehwegquelle in _walk_source() blieb seit Fixrunde 5 dagegen ungerundet. Eine reine Dilatation der Quelle um WALK_MAX=3,0 zeichnet eine Reflexecke nicht nach - zwischen der gerundeten Fahrbahn und dem ungerundeten Gehweg blieb an jeder gewöhnlichen Kreuzung (mehrere Arme mit Bordstein, keine Gasse beteiligt) eine kleine Lücke, zu klein für LOT_MIN und damit ein unsichtbares Loch im Boden statt einer Restfläche. Gemessen 0,26-2,2 m² je Ecke bei 690 Kreuzungen. junction_area() (section.py) bekommt einen kerb_only-Schalter und rundet damit wahlweise nur den Kreuzungsanteil der bordsteintragenden Arme; _walk_source() ruft sie jetzt statt des rohen junction_arms(kerb_only=True) auf. Der Unterschied zu Fixrunde 4 (die dieselbe Rundung auf ALLE Arme anwandte und damit Gassen mit rundete): Eine Gasse liefert gar keinen Arm in die zu schließende Fläche, kann also strukturell keine Nische mit einem Nachbararm bilden - unabhängig vom Winkel. Nachgemessen an einer T-Kreuzung aus drei Wohnstraßen (derselbe großzügige Korridor wie in den bestehenden Gassen-Tests, carriageway_area()+3 m): vor der Korrektur 0,256 m² Lücke je der beiden Ecken (0,5114 m² zusammen), exakt im berichteten Bereich; danach 0,0 m². Eine zweite, an sich schon bei Fixrunde 5 bestehende Kontrolle: Gasse in einem engen Winkel (10-30°) zwischen zwei Wohnstraßen-Armen - der Gehweg bleibt für jeden geprüften Winkel bei 0,0 m² im Gassenkorridor. Alle bestehenden Gassen-Sicherungen in test_street.py (fünf Fixrunden Vorarbeit) bleiben grün.
148 lines
7.3 KiB
Python
148 lines
7.3 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, _kerbless_zone(graph, 0.2), 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 Bordstein, 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(kerb_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].kerb 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, kerb_only=True)) is not None and not area.is_empty]
|
|
return unary_union(teile) if teile else None
|
|
|
|
|
|
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.
|
|
|
|
Nur noch einmal gebraucht: Der Bordstein hört hier auf (_kerb_lines()) - eine Gasse hat laut
|
|
SECTION weder Bordstein noch Gehweg, und die Fuge dafür ist 0,2 m über die eigene
|
|
Fahrbahnbreite hinaus.
|
|
|
|
Bis Fixrunde 4 schnitt dieselbe Zone auch den Kreuzungsanteil der Gehwegquelle zu; seit
|
|
Fixrunde 5 nimmt _walk_source() stattdessen von vornherein nur die Arme mit Bordstein
|
|
(junction_area(kerb_only=True), Befund 7 der Fixwelle) und braucht deshalb keinen
|
|
nachträglichen Abzug mehr.
|
|
"""
|
|
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(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, kerbless, bridges) -> list[LineString]:
|
|
"""Die Fahrbahnkante, ohne die Stücke an Gassen und auf Brücken."""
|
|
ohne = [] if kerbless is None else [kerbless]
|
|
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
|