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:
1 parent
25f2912520
commit
0c8e9c7ba0
2 files changed
+165
-1
No files matched your search
@@ -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)
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Haltelinien und Zebrastreifen."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from leonida.roads import Edge, Graph, Node
|
||||
from leonida.marking import CROSSWALK_DEPTH, junction_marks, markings
|
||||
from leonida.section import SECTION, setback
|
||||
|
||||
|
||||
def kreuzung(klassen=("hauptstrasse",) * 4, arm: float = 80.0) -> Graph:
|
||||
punkte = [(arm, 0.0), (-arm, 0.0), (0.0, arm), (0.0, -arm)]
|
||||
return Graph(nodes=[Node((0.0, 0.0))] + [Node(p) for p in punkte],
|
||||
edges=[Edge(0, i + 1, [(0.0, 0.0), p], SECTION[k].corridor, k)
|
||||
for i, (p, k) in enumerate(zip(punkte, klassen))])
|
||||
|
||||
|
||||
def test_jeder_arm_bekommt_zebra_und_haltelinie():
|
||||
zahl = Counter(art for _, art in junction_marks(kreuzung(), 0))
|
||||
assert zahl["haltelinie"] == 4, "eine je Zufahrt"
|
||||
assert zahl["zebra"] > 4 * 5, "je Arm mehrere Balken"
|
||||
|
||||
|
||||
def test_zebra_liegt_am_kreuzungsrand():
|
||||
rand = setback(kreuzung(), 0)
|
||||
zebra = [p for p, art in junction_marks(kreuzung(), 0) if art == "zebra"]
|
||||
# Der Arm nach Osten: Balken zwischen Kreuzungsrand und Kreuzungsrand plus Tiefe. Die
|
||||
# Querbedingung muss enger sein als die halbe Fahrbahnbreite (6 m): Bei |y| < 20 träfe sie
|
||||
# auch Nord- und Südarm-Balken nahe der Ecke (deren eigene Querachse dort ohnehin liegt), die
|
||||
# aber bei x um 8-11 m liegen wie der Ostarm selbst - falsches Grün durch Verwechslung, keine
|
||||
# echte Prüfung (Fixrunde zu Task 9).
|
||||
halb = SECTION["hauptstrasse"].carriageway / 2
|
||||
ost = [p for p in zebra if p.centroid.x > 0 and abs(p.centroid.y) < halb]
|
||||
assert ost, "der Ostarm hat Balken"
|
||||
for poly in ost:
|
||||
assert rand - 0.1 <= poly.bounds[0] <= rand + CROSSWALK_DEPTH
|
||||
|
||||
|
||||
def test_haltelinie_liegt_hinter_dem_zebrastreifen():
|
||||
rand = setback(kreuzung(), 0)
|
||||
halt = [p for p, art in junction_marks(kreuzung(), 0) if art == "haltelinie"]
|
||||
ost = next(p for p in halt if p.centroid.x > 0 and abs(p.centroid.y) < 20)
|
||||
assert ost.bounds[0] > rand + CROSSWALK_DEPTH, "erst Zebrastreifen, dann Haltelinie"
|
||||
|
||||
|
||||
def test_haltelinie_deckt_nur_die_rechte_haelfte():
|
||||
halt = [p for p, art in junction_marks(kreuzung(), 0) if art == "haltelinie"]
|
||||
ost = next(p for p in halt if p.centroid.x > 0 and abs(p.centroid.y) < 20)
|
||||
# Zufahrt von Osten nach Westen, also Fahrtrichtung (-1, 0): Rechts davon liegt y > 0 - per
|
||||
# offset_curve()-Konvention nachgerechnet (negativ = rechts der Linienrichtung), nicht nur
|
||||
# behauptet. Der Auftrag ging von y < 0 aus; das war die Gegenspur (Fixrunde zu Task 9).
|
||||
assert ost.centroid.y > 0
|
||||
assert round(ost.bounds[3] - ost.bounds[1], 1) == round(SECTION["hauptstrasse"].carriageway / 2, 1)
|
||||
|
||||
|
||||
def test_arm_der_eine_gasse_ist_bekommt_keinen_zebrastreifen():
|
||||
graph = kreuzung(("hauptstrasse", "hauptstrasse", "hauptstrasse", "gasse"))
|
||||
marks = junction_marks(graph, 0)
|
||||
# Wie bei test_zebra_liegt_am_kreuzungsrand: |x| < 20 wäre zu locker und träfe auch Ost- und
|
||||
# Westarm-Balken nahe der Ecke, obwohl deren Querachse (|x| bis 9,5 m für Zebra, 12,5 m für
|
||||
# die Haltelinie) klar außerhalb der Südarm-Fahrbahnhälfte liegt (Fixrunde zu Task 9).
|
||||
halb = SECTION["hauptstrasse"].carriageway / 2
|
||||
süd = [art for p, art in marks if p.centroid.y < -1 and abs(p.centroid.x) < halb]
|
||||
assert süd == [], "der Gassenarm bleibt blank"
|
||||
|
||||
|
||||
def test_kreuzung_aus_lauter_gassen_bekommt_nichts():
|
||||
assert junction_marks(kreuzung(("gasse",) * 4), 0) == []
|
||||
|
||||
|
||||
def test_markings_sammelt_laengs_und_kreuzung():
|
||||
arten = {art for _, art in markings(kreuzung())}
|
||||
assert "gelb_doppelt" in arten and "zebra" in arten and "haltelinie" in arten
|
||||
|
||||
|
||||
def test_schleife_bekommt_haltelinie_fuer_beide_richtungen():
|
||||
# Wie test_schleife_bekommt_zwei_stummel_kein_loch_im_belag in test_junction.py: Eine
|
||||
# Schleife hängt mit beiden Enden am selben Knoten. graph.incident() liefert sie nur einmal,
|
||||
# aber sie stellt zwei getrennte Abgangsrichtungen dar (Verkehr kann von beiden Seiten in die
|
||||
# Kreuzung einfahren) - am echten Netz betrifft das 28 von 690 Kreuzungen mit einer
|
||||
# bordsteintragenden Schleife. Ohne die zweite Richtung fehlten Haltelinie und Zebrastreifen
|
||||
# auf der einen Seite der Schleife komplett.
|
||||
graph = Graph(
|
||||
nodes=[Node((0.0, 0.0)), Node((-100.0, 0.0)), Node((0.0, -100.0))],
|
||||
edges=[Edge(0, 0, [(0.0, 0.0), (0.0, 20.0), (20.0, 20.0), (20.0, 0.0), (0.0, 0.0)],
|
||||
SECTION["hauptstrasse"].corridor, "hauptstrasse"),
|
||||
Edge(0, 1, [(0.0, 0.0), (-100.0, 0.0)], SECTION["hauptstrasse"].corridor, "hauptstrasse"),
|
||||
Edge(0, 2, [(0.0, 0.0), (0.0, -100.0)], SECTION["hauptstrasse"].corridor, "hauptstrasse")])
|
||||
zahl = Counter(art for _, art in junction_marks(graph, 0))
|
||||
assert zahl["haltelinie"] == 4, "zwei gerade Arme plus zwei Richtungen der Schleife"
|
||||
Reference in new issue
Block a user