reach kommt vom breitesten Arm der ganzen Kreuzung, wurde aber fuer jeden Arm gleich verwendet. An einer Kreuzung, wo ein kurzer Arm auf einen breiten trifft, reicht der kurze Arm dann nicht mehr bis reach (Zebrastreifen) oder bis reach + STOP_SETBACK + STOP_DEPTH (Haltelinie) - die Markierung entstand trotzdem und ragte ueber das Kantenende hinaus. junction_arms() in section.py schuetzt sich dagegen laengst; junction_marks() jetzt auch, mit getrennter Pruefung je Markierung, weil beide unterschiedlich viel Platz brauchen. Am echten Netz: 38 von 1.508 Armstummeln sind zu kurz fuer die Haltelinie, groesster Ueberstand 10,6 m (Fixrunde 1 des Koordinators). Auf tatsaechliche Zufahrten umgerechnet (Schleifen zaehlen doppelt) sind es 44 von 1.536 - die Differenz zur Kantenzahl war anfangs unklar, ist jetzt aber nachgerechnet und im Kommentar festgehalten. markings(graph) sinkt dadurch von 22.586 auf 22.336 Flaechen (-206 Zebrabalken, -44 Haltelinien) - genau das vorher falsch platzierte Material. Dazu zwei bisher uebersehene Stellen mit demselben zu lockeren Testfilter (abs(centroid.y) < 20) auf die halbe Fahrbahnbreite verengt, wie schon an den zwei Schwesterstellen der vorigen Fixrunde - die Haltelinie des Suedarms erfuellte die lockere Bedingung ebenfalls, next() traf bisher nur zufaellig zuerst auf den Ostarm. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
167 lines
8.4 KiB
Python
167 lines
8.4 KiB
Python
"""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, Polygon
|
|
from shapely.ops import substring
|
|
|
|
from .roads import Graph
|
|
from .section import LANE, SECTION, setback, 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[Polygon, 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: Die Achsen liegen bei ±WIDTH, also 20 cm auseinander. Jede
|
|
# Linie ist WIDTH breit (Pufferung um WIDTH/2 je Seite), sodass von den 20 cm genau
|
|
# WIDTH = 10 cm als Lücke zwischen den gemalten Linien übrig bleiben - das ist der im
|
|
# Entwurf verlangte Abstand, nicht der Achsabstand selbst.
|
|
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
|
|
|
|
|
|
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
|
|
# reach kommt vom breitesten Arm der ganzen Kreuzung, gilt hier aber für diesen einen
|
|
# Arm - ein kurzer Arm an einer sonst breiten Kreuzung reicht oft nicht bis dorthin. Am
|
|
# echten Netz sind 38 von 1.508 geprüften Armstummeln zu kurz für die Haltelinie,
|
|
# im schlimmsten Fall um 10,6 m (Fixrunde 1 des Koordinators zu Task 9; auf die
|
|
# tatsächlichen Zufahrten umgerechnet, inklusive der doppelt zählenden Schleifen aus der
|
|
# vorigen Fixrunde, sind es 44 von 1.536). Ohne diese Prüfung ragte die Markierung über
|
|
# das Ende der Kante hinaus und läge neben der Straße statt darauf. Zebrastreifen und
|
|
# Haltelinie brauchen unterschiedlich viel Platz, deshalb getrennt geprüft statt
|
|
# gemeinsam verworfen.
|
|
länge = LineString(points).length
|
|
if länge >= reach + CROSSWALK_DEPTH:
|
|
# 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
|
|
if länge >= reach + STOP_SETBACK + STOP_DEPTH:
|
|
# 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)
|