"""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) sidewalk = _sidewalk(carriage, corridor, _kerbless_zone(graph, WALK_MAX)) return Street( carriageway=carriage, kerb=_kerb_lines(carriage, _kerbless_zone(graph, 0.2), bridges), sidewalk=sidewalk, lots=_lots(carriage, corridor, sidewalk), ) 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 unterschiedlicher Reichweite: Der Bordstein hört direkt an der Fahrbahnkante auf (extra=0,2, nur eine Fuge für einen sauberen Schnitt), aber der Gehwegpuffer in _sidewalk() reicht bis WALK_MAX über dieselbe Kante hinaus - mit der schmalen Fuge bliebe dort ein bis zu 2,8 m breiter Streifen übrig, den eine Klasse ohne Gehweg trotzdem als Gehweg bekäme, sobald neben ihr mehr gemessene Fläche liegt als ihre eigene Breite (siehe Bericht). Die volle Mittellinie statt ribbon(), damit auch der Anteil der Kreuzungsfläche abgedeckt ist, an dem die bordsteinlose Klasse einmündet. """ 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(carriage, corridor, kerbless): """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. `kerbless` schneidet zusätzlich heraus, wo eine Klasse laut SECTION gar keinen Gehweg vorsieht (die Gasse) - der Korridor allein reicht dafür nicht: er füllt zwar meist genau die Gassenbreite, aber sobald daneben mehr gemessene Fläche liegt, würde der WALK_MAX-Puffer sie sonst fälschlich zu Gehweg machen. 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 kerbless is not None: result = result.difference(kerbless) if result.is_empty: return MultiPolygon() return result if result.geom_type == "MultiPolygon" else 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