feat(extract): Querschnitt je Klasse und Fahrbahnband
Der Korridor, den Task 4 misst, ist auf der Karte eine einzige graue Fläche - Fahrbahn und Gehweg tragen dort dieselbe Farbe. Der Querschnitt teilt sie deshalb nur auf, statt sie zu vergrößern: Fahrbahn plus zweimal Gehweg ergibt für jede Klasse genau die gemessene Breite, und der Stadtgrundriss bleibt unberührt. Die Maße (Fahrspur 3,5 m, Parkspur 2,0-2,5 m, Gehweg 2,5-3,0 m) stehen im Entwurf; eine Assertion auf Modulebene hält die Aufteilung für alle vier Klassen fest. Das Fahrbahnband entsteht aus der Mittellinie: An jedem Kreuzungsende (Grad >= 3) weicht sie um die halbe Fahrbahn der breitesten anliegenden Kante plus 2 m zurück, freie Enden bleiben stehen. Eine Kante, die ganz in den Kreuzungsflächen verschwindet, liefert kein Band - das füllt Task 6. Testkorrektur: test_kante_ganz_innerhalb_der_kreuzung_liefert_kein_band hatte an beiden "Kreuzungsknoten" nur je zwei Kanten, also Grad 2 statt Grad 3 - keine Kreuzung nach der Definition, die trimmed_line() selbst verwendet. Gegen die fertige Implementierung geprüft, lieferte sie eine echte Linie statt None. Je ein zweiter Arm an beiden Knoten (wie schon in test_kontraktion_zieht_alle_anliegenden_kanten_mit für denselben 3-m-Kreuzungsabstand) behebt das. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2d8f4e730c
commit
f91b906827
2 files changed
+141
No files matched your search
@@ -0,0 +1,73 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user