"""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 der Kreuzungsanteil jedes Knotens, an dem mindestens ein solcher Arm hängt - aber ohne den Gassenstummel darin (Fixrunde 4): junction_area() liefert die ganze Kreuzungsfläche als ein Stück, auch den Anteil der Gasse, die dort einmündet. Ohne den Abzug wüchse der Gehweg die volle Länge dieses Stummels (bis zu setback+1, beim Boulevard 11 m) die Gasse hinauf, statt nur um die Ecke herum. Dieselbe schmale Fuge wie beim Bordstein (_kerbless_zone, extra=0,2) schneidet nur den Gassenanteil heraus; die Kreuzung selbst bleibt Quelle, ein kurzer Rest an der Einmündungsecke bleibt und soll bleiben - das ist die Ecke, nicht die Gasse. Vorher wuchs der Gehweg aus der gesamten Fahrbahn, und die Klassen ohne Bordstein wurden hinterher über eine Reichweitenzone wieder abgezogen — die kennt aber ihre Nachbarschaft nicht und nahm dabei auch den Gehweg einer Wohnstraße mit, die zufällig nah an einer Gasse lag. Wer von der richtigen Seite her wächst, braucht diesen zweiten Abzug nicht. """ teile = [band for k, edge in enumerate(graph.edges) if SECTION[edge.klass].kerb and (band := ribbon(graph, k)) is not None] ohne = _kerbless_zone(graph, 0.2) teile += [flaeche if ohne is None else flaeche.difference(ohne) for node in range(len(graph.nodes)) if graph.degree(node) >= 3 and any(SECTION[graph.edges[k].klass].kerb for k in graph.incident(node)) and (flaeche := junction_area(graph, node)) is not None] 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. Zweimal gebraucht, mit derselben Reichweite (Fixrunde 4): Der Bordstein hört hier auf (_kerb_lines()), und der Kreuzungsanteil der Gehwegquelle hört hier ebenfalls auf (_walk_source()) - eine Gasse hat laut SECTION weder Bordstein noch Gehweg, in beiden Fällen ist es dieselbe Fuge von 0,2 m über die eigene Fahrbahnbreite hinaus. """ 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