feat(extract): Haltelinien und Zebrastreifen an Kreuzungen

Zebrastreifen im Miami-Balkenmuster und Haltelinie je Zufahrt, dazu markings(graph)
als Sammelfunktion fuer Task 10. Beim Nachrechnen der Pruefkoerper zwei Fehler im
Auftrag gefunden und vor der Uebernahme berichtigt:

Die Haltelinie lag auf der falschen Fahrbahnhaelfte. "across" ist schon als links von
"along" definiert (dieselbe Konvention wie offset_curve() in _offset()), und die Umkehr
der Fahrtrichtung an der Zufahrt braucht deshalb kein zusaetzliches Vorzeichen - der
Auftrag zog eins ab und landete damit auf der Gegenspur. Eine Probe mit offset_curve()
auf der Einfahrtlinie bestaetigt +across als die rechte Seite; alle vier Arme der
Testkreuzung ergeben danach das erwartete Windradmuster.

Zwei Testfilter (|x| bzw. |y| < 20) waren zu locker: An der vierarmigen Testkreuzung
liegen Balken benachbarter Arme nahe genug am Ursprung, um faelschlich mitgezaehlt zu
werden - falsches Gruen durch Verwechslung. Auf die halbe Fahrbahnbreite verengt.

Zusaetzlich gefunden und mit eigenem Test belegt: Eine Schleifenkante haengt mit beiden
Enden am selben Knoten und stellt zwei getrennte Zufahrten dar (wie junction_arms() in
section.py); ohne die zweite Richtung fehlten Haltelinie und Zebra auf einer Seite der
Schleife. Betrifft 28 von 690 Kreuzungen am echten Netz.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-17 08:40:07 +02:00
1 parent 25f2912520
commit 0c8e9c7ba0
2 files changed
+165 -1

No files matched your search

+75 -1
View File
@@ -15,7 +15,7 @@ from shapely.geometry import LineString, Polygon
from shapely.ops import substring
from .roads import Graph
from .section import LANE, SECTION, trimmed_line
from .section import LANE, SECTION, setback, trimmed_line
WIDTH = 0.10 # 4 in
DASH = 3.0
@@ -76,3 +76,77 @@ def _dashed(line: LineString, distance: float) -> list:
WIDTH / 2, cap_style="flat", join_style="mitre", mitre_limit=1.5))
at = end + GAP
return out
CROSSWALK_DEPTH = 3.0 # Tiefe des Zebrastreifens in Fahrtrichtung, 10 ft
BAR = 0.6 # Balkenbreite und Lücke, 2 ft - das Miami-Balkenmuster
STOP_DEPTH = 0.6
STOP_SETBACK = 4.2 # Kreuzungsrand + Zebrastreifen (3,0) + Sicherheitsabstand (1,2)
def markings(graph: Graph) -> list[tuple[Polygon, str]]:
out = longitudinal(graph)
for node in range(len(graph.nodes)):
if graph.degree(node) >= 3:
out += junction_marks(graph, node)
return out
def junction_marks(graph: Graph, node: int) -> list[tuple[Polygon, str]]:
"""Zebrastreifen und Haltelinie je Zufahrt.
Ein Zebrastreifen entsteht nur, wenn die Kreuzung mindestens zwei Arme ab Wohnstraße hat -
zwischen zwei Gassen quert niemand. Arme, die selbst Gassen sind, bleiben in jedem Fall
blank.
"""
incident = graph.incident(node)
echte = [k for k in incident if SECTION[graph.edges[k].klass].kerb]
if len(echte) < 2:
return []
reach = setback(graph, node)
origin = graph.nodes[node].point
out = []
for k in echte:
edge = graph.edges[k]
# Eine Schleife hängt mit beiden Enden am selben Knoten und stellt deshalb zwei getrennte
# Zufahrten dar (wie bei junction_arms() in section.py - Fixrunde zu Task 9, am echten
# Netz betrifft das 28 von 690 Kreuzungen mit einer bordsteintragenden Schleife). incident()
# liefert sie nur einmal, und edge.a == node ist bei ihr immer wahr - ohne die zweite
# Richtung bekäme die Schleife auf einer ihrer beiden Seiten weder Haltelinie noch Zebra.
richtungen = ([edge.points, list(reversed(edge.points))] if edge.a == edge.b
else [edge.points if edge.a == node else list(reversed(edge.points))])
for points in richtungen:
along = _unit(origin, points[1] if len(points) > 1 else points[0])
across = (-along[1], along[0])
halb = SECTION[edge.klass].carriageway / 2
# Balken längs der Fahrtrichtung, quer über die Fahrbahn verteilt.
offset = -halb + BAR / 2
while offset <= halb - BAR / 2 + 1e-9:
mitte = (origin[0] + across[0] * offset, origin[1] + across[1] * offset)
out.append((_rect(mitte, along, across, reach, CROSSWALK_DEPTH, BAR), "zebra"))
offset += 2 * BAR
# Haltelinie quer, nur über die rechte Hälfte aus Sicht der Zufahrt. "across" ist schon
# als links von "along" definiert (dieselbe Konvention wie offset_curve() in _offset():
# positiv = links). Die Zufahrt fährt zum Knoten hin, also gegen "along" - und die Umkehr
# der Fahrtrichtung vertauscht links und rechts. Rechts der Einfahrt ist deshalb +across,
# ohne weiteres Vorzeichen: eine Probe mit offset_curve() auf der Einfahrtlinie bestätigt
# das (Fixrunde zu Task 9 - der Auftrag hatte hier -across und lag damit auf der Gegenspur).
mitte = (origin[0] + across[0] * halb / 2, origin[1] + across[1] * halb / 2)
out.append((_rect(mitte, along, across, reach + STOP_SETBACK, STOP_DEPTH, halb), "haltelinie"))
return out
def _unit(a: tuple[float, float], b: tuple[float, float]) -> tuple[float, float]:
dx, dy = b[0] - a[0], b[1] - a[1]
length = (dx * dx + dy * dy) ** 0.5 or 1.0
return (dx / length, dy / length)
def _rect(mitte, along, across, start: float, length: float, width: float) -> Polygon:
"""Rechteck im Fahrbahnsystem: start..start+length in Fahrtrichtung, width quer um mitte."""
punkte = []
for dl, dq in ((start, -width / 2), (start + length, -width / 2),
(start + length, width / 2), (start, width / 2)):
punkte.append((mitte[0] + along[0] * dl + across[0] * dq,
mitte[1] + along[1] * dl + across[1] * dq))
return Polygon(punkte)