"""Querschnitt je Straßenklasse und das Fahrbahnband, das daraus entsteht. Die Maße stehen im Entwurf (docs/superpowers/specs/2026-09-16-strassen-design.md) und stammen aus dem amerikanischen Regelwerk: Fahrspur 3,5 m, Längsparkspur 2,0-2,5 m, Gehweg 2,5-3,0 m. Gemessen wird auf der Karte der ganze graue Korridor - Fahrbahn und Gehweg sind dort dasselbe Grau. Die Aufteilung nimmt dem Korridor deshalb nichts weg und gibt ihm nichts dazu: Fahrbahn plus zweimal Gehweg ist genau die gemessene Breite. Der Stadtgrundriss bleibt damit unberührt. """ from __future__ import annotations from dataclasses import dataclass from shapely.geometry import LineString, Point from shapely.ops import substring, unary_union from .roads import CLASS_WIDTH, Graph LANE = 3.5 # Wie weit das Band vor dem Kreuzungsmittelpunkt endet: halbe Fahrbahn der breitesten # anliegenden Kante plus etwas Luft, damit die Kreuzungsfläche die Lücke sicher füllt. SETBACK_EXTRA = 2.0 # Breitester Gehweg. Weil die Gehwegfläche am Korridor beschnitten wird, ergibt sich die # schmalere Variante von selbst: bei 12 m Korridor und 7 m Fahrbahn bleiben genau 2,5 m. WALK_MAX = 3.0 @dataclass(frozen=True) class Section: corridor: float carriageway: float walk: float lanes: int parking: float kerb: bool SECTION = { # Hinterhofgasse: eine Fahrgasse, kein Bordstein, kein Gehweg, keine Markierung. "gasse": Section(corridor=6.0, carriageway=6.0, walk=0.0, lanes=1, parking=0.0, kerb=False), "wohnstrasse": Section(corridor=12.0, carriageway=7.0, walk=2.5, lanes=2, parking=0.0, kerb=True), "hauptstrasse": Section(corridor=18.0, carriageway=12.0, walk=3.0, lanes=2, parking=2.5, kerb=True), "boulevard": Section(corridor=24.0, carriageway=18.0, walk=3.0, lanes=4, parking=2.0, kerb=True), } assert all(s.carriageway + 2 * s.walk == s.corridor == CLASS_WIDTH[n] for n, s in SECTION.items()) def setback(graph: Graph, node: int) -> float: return max(SECTION[graph.edges[k].klass].carriageway / 2 for k in graph.incident(node)) + SETBACK_EXTRA def trimmed_line(graph: Graph, k: int): """Die Mittellinie zwischen den Kreuzungsrändern. Freie Enden bleiben, wo sie sind.""" edge = graph.edges[k] line = LineString(edge.points) start = setback(graph, edge.a) if graph.degree(edge.a) >= 3 else 0.0 end = line.length - (setback(graph, edge.b) if graph.degree(edge.b) >= 3 else 0.0) if end - start < 1.0: return None # die Kante liegt ganz in den Kreuzungen; die füllen sie mit return substring(line, start, end) def ribbon(graph: Graph, k: int): """Das Fahrbahnband einer Kante: Mittellinie auf Fahrbahnbreite gepuffert. join_style mitre mit knappem Limit: Eine Straße mit einem Knick soll eine gerade Außenkante behalten statt einer abgerundeten, aber bei einem sehr spitzen Knick würde eine unbegrenzte Gehrung einen meterlangen Dorn erzeugen. """ line = trimmed_line(graph, k) if line is None: return None return line.buffer(SECTION[graph.edges[k].klass].carriageway / 2, cap_style="flat", join_style="mitre", mitre_limit=1.5)