feat(extract): amerikanische Laengsmarkierungen als Geometrie
Gelbe Mitte, Spurtrenner und Randlinien je Kante, zwischen den Kreuzungsraendern - Geometrie statt Textur, weil Haltelinie und Zebrastreifen (Task 9) quer zur Fahrtrichtung liegen und in einer laengs laufenden Kachel nicht unterzubringen waeren. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2f5db94fe
commit
3216e9407f
2 files changed
+142
No files matched your search
@@ -0,0 +1,75 @@
|
||||
"""Amerikanische Fahrbahnmarkierung als Geometrie.
|
||||
|
||||
Das Schema ist das Gegenteil des europäischen: Gelb trennt Gegenrichtungen und steht deshalb in
|
||||
der Mitte, Weiß trennt gleichgerichtete Spuren und begrenzt die Fahrbahn. Die mitgelieferten
|
||||
Texturen von modular_city_road_pack__game_ready zeigen das europäische Schema und sind hier
|
||||
deshalb unbrauchbar (Entwurf, Abschnitt "Die beiden Kits").
|
||||
|
||||
Markierungen sind flache Vierecke, keine Textur: An Kreuzungen liegen Haltelinie und
|
||||
Zebrastreifen quer zur Fahrtrichtung und ließen sich in einer längs laufenden Kachel gar nicht
|
||||
unterbringen, und aus der Nähe bleibt Geometrie scharf, wo eine Textur verwischt.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from shapely.geometry import LineString
|
||||
from shapely.ops import substring
|
||||
|
||||
from .roads import Graph
|
||||
from .section import LANE, SECTION, trimmed_line
|
||||
|
||||
WIDTH = 0.10 # 4 in
|
||||
DASH = 3.0
|
||||
GAP = 9.0
|
||||
KINDS = ("gelb_gestrichelt", "gelb_doppelt", "weiss_gestrichelt", "weiss_rand",
|
||||
"haltelinie", "zebra")
|
||||
|
||||
|
||||
def longitudinal(graph: Graph) -> list[tuple[object, str]]:
|
||||
"""Mittellinie, Spurtrenner und Randlinien je Kante, zwischen den Kreuzungsrändern."""
|
||||
out = []
|
||||
for k, edge in enumerate(graph.edges):
|
||||
section = SECTION[edge.klass]
|
||||
line = trimmed_line(graph, k)
|
||||
if line is None or section.lanes < 2:
|
||||
continue # die Gasse hat nur eine Fahrgasse und bleibt blank
|
||||
if section.parking == 0.0:
|
||||
# Wohnstraße: eine gestrichelte gelbe Mitte, Überholen erlaubt.
|
||||
out += [(p, "gelb_gestrichelt") for p in _dashed(line, 0.0)]
|
||||
else:
|
||||
# Doppelt durchgezogen: zwei Linien mit 10 cm Abstand der Mitten.
|
||||
out += [(_solid(line, off), "gelb_doppelt") for off in (-WIDTH, WIDTH)]
|
||||
rand = section.carriageway / 2 - section.parking
|
||||
out += [(_solid(line, off), "weiss_rand") for off in (-rand, rand)]
|
||||
if section.lanes >= 4:
|
||||
# Zwischen den gleichgerichteten Spuren, je Richtung einer.
|
||||
out += [(p, "weiss_gestrichelt") for off in (-LANE, LANE) for p in _dashed(line, off)]
|
||||
return out
|
||||
|
||||
|
||||
def _offset(line: LineString, distance: float) -> LineString:
|
||||
if distance == 0.0:
|
||||
return line
|
||||
# offset_curve versetzt nach links; negative Werte nach rechts. Bei geraden Straßen ist das
|
||||
# exakt, bei Knicken folgt die Kurve dem Knick - genau das soll eine Markierung auch.
|
||||
return line.offset_curve(distance)
|
||||
|
||||
|
||||
def _solid(line: LineString, distance: float):
|
||||
return _offset(line, distance).buffer(WIDTH / 2, cap_style="flat", join_style="mitre", mitre_limit=1.5)
|
||||
|
||||
|
||||
def _dashed(line: LineString, distance: float) -> list:
|
||||
"""Striche im Takt DASH/GAP, beginnend am Anfang der Kante.
|
||||
|
||||
Die Phase läuft bewusst nicht über Kreuzungen hinweg weiter: Jede Kante fängt neu an. Alles
|
||||
andere hieße, eine Ordnung zu behaupten, die die Karte gar nicht hergibt.
|
||||
"""
|
||||
versetzt = _offset(line, distance)
|
||||
out, at = [], 0.0
|
||||
while at < versetzt.length:
|
||||
end = min(at + DASH, versetzt.length)
|
||||
if end - at > 0.5:
|
||||
out.append(substring(versetzt, at, end).buffer(
|
||||
WIDTH / 2, cap_style="flat", join_style="mitre", mitre_limit=1.5))
|
||||
at = end + GAP
|
||||
return out
|
||||
@@ -0,0 +1,67 @@
|
||||
"""Längsmarkierungen: Mittellinie, Spurtrenner, Randlinien."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from leonida.roads import Edge, Graph, Node
|
||||
from leonida.marking import DASH, GAP, WIDTH, longitudinal
|
||||
from leonida.section import SECTION
|
||||
|
||||
|
||||
def strecke(klass: str, length: float = 120.0) -> Graph:
|
||||
return Graph(nodes=[Node((0.0, 0.0)), Node((length, 0.0))],
|
||||
edges=[Edge(0, 1, [(0.0, 0.0), (length, 0.0)], SECTION[klass].corridor, klass)])
|
||||
|
||||
|
||||
def arten(graph: Graph) -> Counter:
|
||||
return Counter(art for _, art in longitudinal(graph))
|
||||
|
||||
|
||||
def test_gasse_bekommt_keine_markierung():
|
||||
assert longitudinal(strecke("gasse")) == []
|
||||
|
||||
|
||||
def test_wohnstrasse_bekommt_eine_gestrichelte_gelbe_mitte():
|
||||
zahl = arten(strecke("wohnstrasse"))
|
||||
assert set(zahl) == {"gelb_gestrichelt"}
|
||||
# 120 m in 12-m-Takten: zehn Striche.
|
||||
assert zahl["gelb_gestrichelt"] == 10
|
||||
assert DASH == 3.0 and GAP == 9.0
|
||||
|
||||
|
||||
def test_hauptstrasse_bekommt_doppelte_mitte_und_zwei_randlinien():
|
||||
zahl = arten(strecke("hauptstrasse"))
|
||||
assert zahl["gelb_doppelt"] == 2, "zwei durchgezogene Linien nebeneinander"
|
||||
assert zahl["weiss_rand"] == 2, "je eine an der Grenze zur Parkspur"
|
||||
assert "weiss_gestrichelt" not in zahl, "zwei Spuren brauchen keinen Spurtrenner"
|
||||
|
||||
|
||||
def test_boulevard_bekommt_zusaetzlich_spurtrenner():
|
||||
zahl = arten(strecke("boulevard"))
|
||||
assert zahl["gelb_doppelt"] == 2
|
||||
assert zahl["weiss_rand"] == 2
|
||||
assert zahl["weiss_gestrichelt"] == 20, "je Richtung ein gestrichelter Trenner, 10 Striche"
|
||||
|
||||
|
||||
def test_randlinie_liegt_an_der_grenze_zur_parkspur():
|
||||
section = SECTION["hauptstrasse"]
|
||||
ränder = [poly for poly, art in longitudinal(strecke("hauptstrasse")) if art == "weiss_rand"]
|
||||
abstände = sorted(round(abs(poly.centroid.y), 2) for poly in ränder)
|
||||
erwartet = round(section.carriageway / 2 - section.parking, 2)
|
||||
assert abstände == [erwartet, erwartet]
|
||||
|
||||
|
||||
def test_linien_sind_zehn_zentimeter_breit():
|
||||
poly = longitudinal(strecke("wohnstrasse"))[0][0]
|
||||
x0, y0, x1, y1 = poly.bounds
|
||||
assert round(y1 - y0, 3) == WIDTH
|
||||
assert round(x1 - x0, 3) == DASH
|
||||
|
||||
|
||||
def test_markierung_endet_am_kreuzungsrand():
|
||||
graph = Graph(nodes=[Node((0.0, 0.0)), Node((120.0, 0.0)), Node((120.0, 60.0)), Node((180.0, 0.0))],
|
||||
edges=[Edge(0, 1, [(0.0, 0.0), (120.0, 0.0)], 12.0, "wohnstrasse"),
|
||||
Edge(1, 2, [(120.0, 0.0), (120.0, 60.0)], 12.0, "wohnstrasse"),
|
||||
Edge(1, 3, [(120.0, 0.0), (180.0, 0.0)], 12.0, "wohnstrasse")])
|
||||
ende = max(poly.bounds[2] for poly, _ in longitudinal(graph) if poly.bounds[2] < 120)
|
||||
assert ende < 120 - SECTION["wohnstrasse"].carriageway / 2, "vor der Kreuzung ist Schluss"
|
||||
Reference in new issue
Block a user