"""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