"""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, 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