Files
gta6/tools/extract/leonida/street.py
T
knsoandClaude Sonnet 5 377a95f3dd fix(extract): Gehwegabzug in _lots(), MultiPolygon-Zusage, Bordstein an Zufahrten
_lots() zog bisher nur die Fahrbahn vom Korridor ab, nicht den Gehwegstreifen - weil
carriageway + 2*walk je Klasse genau den Korridor ergibt, war "Korridor minus Fahrbahn"
wörtlich der Gehweg und legte ihn ab 80 m Straßenlänge ein zweites Mal als Restfläche
darüber. _sidewalk() hielt die MultiPolygon-Zusage nicht (derselbe Fehler wie bei
carriageway_area() in Task 6): ein leeres Ergebnis kam als leeres Polygon zurück statt
als leeres MultiPolygon. _kerb_lines() nimmt jetzt die volle Kantenmittellinie statt
ribbon() für den Gassen-/Brückenausschluss, weil ribbon() an der Kreuzung endet und der
Bordstein sonst im Einmündungstrichter stehen bliebe.

Die im Fix-Auftrag genannte Test-Konfiguration für den dritten Punkt (30 m lange Gasse)
zeigte den gemeldeten Fehler nicht: ribbon() liefert dort kein None (Setback 5,5 m,
30-5,5=24,5 >= 1,0), der alte Ausschluss deckte die Gasse also schon vollständig ab. Der
Fehler braucht eine Gasse kürzer als setback+1=6,5 m - erst dann liefert trimmed_line()
None und der alte Code baut gar keinen Ausschluss. Testkonfiguration entsprechend auf
eine 5 m kurze Gasse korrigiert, siehe task-7-report.md für die vollständige Herleitung.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-17 05:03:46 +02:00

105 lines
4.5 KiB
Python

"""Die Straße als Mulde: Loch in der Bodenplatte, Fahrbahn darunter, Bordstein dazwischen.
Warum die Fahrbahn nach unten und nicht der Gehweg nach oben (Entwurf, Abschnitt "Die Mulde"):
Läge der Gehweg 15 cm hoch, hätte er überall dort eine Kante ins Nichts, wo kein Haus
anschließt - an jedem Park, jeder Baulücke, jedem Strandzugang. So bleibt die ganze Welt auf
y = 0, und der Bordstein ist genau das, was er in Wirklichkeit ist: die Wand zwischen Gehweg
und Fahrbahn.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from shapely.geometry import LineString, MultiPolygon, Polygon
from shapely.ops import unary_union
from .roads import Graph
from .section import SECTION, WALK_MAX, carriageway_area
# Die Tiefe steht hier als dokumentierter Wert; gebaut wird sie im Browser (geometry.js).
DEPTH = 0.15
# Kürzere Bordsteinstücke sind Rechenreste an Kreuzungsecken, keine Kante, die jemand sieht.
KERB_MIN = 2.0
# Ab hier ist ein Reststück eine Fläche (Parkplatz, Vorplatz, Rampe) und keine Zacke.
LOT_MIN = 200.0
@dataclass
class Street:
carriageway: object
kerb: list = field(default_factory=list)
sidewalk: object = None
lots: list = field(default_factory=list)
def build_street(graph: Graph, corridor, bridges) -> Street:
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
carriage = carriageway_area(graph)
return Street(
carriageway=carriage,
kerb=_kerb_lines(graph, carriage, bridges),
sidewalk=_sidewalk(carriage, corridor),
lots=_lots(carriage, corridor),
)
def _sidewalk(carriage, corridor):
"""Der Streifen zwischen Fahrbahnkante und Korridorrand.
Ein einziger Puffer für alle Klassen reicht: Weil am Korridor beschnitten wird, bleibt bei
12 m Korridor und 7 m Fahrbahn genau der 2,5-m-Gehweg der Wohnstraße übrig, bei 18 m und
12 m genau die 3,0 m der Hauptstraße. Die Gasse füllt ihren Korridor ganz aus und bekommt
dadurch von selbst keinen Gehweg.
Normalisiert wie carriageway_area() auf MultiPolygon (Task 6): Ein einseitiger Gehweg oder
ein leeres Ergebnis kämen sonst als einfaches Polygon bzw. als leere Geometrie zurück, und
Task 10/12 verlassen sich auf die MultiPolygon-Zusage.
"""
result = carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
if result.is_empty:
return MultiPolygon()
return result if result.geom_type == "MultiPolygon" else MultiPolygon([result])
def _kerb_lines(graph: Graph, carriage, bridges) -> list[LineString]:
"""Die Fahrbahnkante, ohne die Stücke an Gassen und auf Brücken."""
ohne = []
for edge in graph.edges:
if SECTION[edge.klass].kerb or len(edge.points) < 2:
continue
# Die volle Mittellinie statt ribbon(): Das Band einer Gasse endet vor der Kreuzung, und
# der Bordstein bliebe genau im Einmündungstrichter stehen. Dass dabei auch die Kante der
# Hauptstraße über die Einmündungsbreite unterbrochen wird, ist richtig so — an einer
# Zufahrt ist der Bordstein abgesenkt.
ohne.append(LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + 0.2,
cap_style="flat", join_style="mitre", mitre_limit=1.5))
ohne += [b.buffer(0.2) for b in bridges]
rand = carriage.boundary
if ohne:
rand = rand.difference(unary_union(ohne))
teile = getattr(rand, "geoms", [rand])
return [g for g in teile if g.geom_type == "LineString" and g.length >= KERB_MIN]
def _lots(carriage, corridor) -> list[Polygon]:
"""Was weder Fahrbahn noch Gehweg ist: Parkplätze, Vorplätze, Rampen.
Der Gehwegstreifen muss mit abgezogen werden, nicht nur die Fahrbahn. Weil Fahrbahn plus
zweimal Gehweg je Klasse genau den Korridor ergibt, wäre "Korridor minus Fahrbahn" sonst
wörtlich der Gehweg — und läge auf jeder Straße ab 80 m ein zweites Mal als Restfläche
darüber.
"""
rest = corridor.difference(carriage.buffer(WALK_MAX, join_style="round"))
return [g for g in getattr(rest, "geoms", [rest])
if g.geom_type == "Polygon" and g.area >= LOT_MIN]
def punch(polygons, carriage) -> list[Polygon]:
"""Die Fahrbahn aus Bodenplatte oder Belag herausschneiden."""
out = []
for poly in polygons:
rest = poly.difference(carriage)
out += [g for g in getattr(rest, "geoms", [rest])
if g.geom_type == "Polygon" and g.area > 0.5]
return out