Files
gta6/tools/extract/leonida/street.py
T
knsoandClaude Sonnet 5 f2f5db94fe fix(extract): Gehwegquelle nur noch aus bordsteintragenden Armstummeln, ungerundet
Zuschneiden (Fixrunde 4) war der falsche Hebel: Egal wie breit die abgezogene
Zone, die Nische zwischen zwei Gassenstummeln entsteht erst durch die
FILLET-Rundung in junction_area() und liegt abseits der Gassenachse - ein
linearer Abzug entlang dieser Achse kann sie grundsaetzlich nicht erreichen.

Die Stummelbildung aus junction_area() in einen eigenen Helfer gezogen,
junction_arms(graph, node, kerb_only=False), mit einem Filter fuer Kanten ohne
Bordstein. junction_area() nutzt ihn ungefiltert und rundet wie bisher -
verhaelt sich dadurch identisch (913.566,7 m² in 213 Teilen am echten Netz,
bit-genau nachgemessen). _walk_source() in street.py nimmt jetzt nur noch die
rohen, ungerundeten Armstummel mit Bordstein (kerb_only=True) als Quelle,
ohne den kerbless_zone-Abzug aus Fixrunde 4 - die Nische entsteht dadurch gar
nicht erst, weil die Kreuzung ohne Rundung nur noch ueber ihre eigenen,
bordsteintragenden Arme in die Quelle eingeht.

Gemessen: an der Kreuzung mit drei Gassen und einem Boulevard verschwindet
der Rest in der Nische vollstaendig (0,0 statt der 27,14 m² aus Fixrunde 4)
- weiter als die fuer diese Runde erwartete Obergrenze von rund 6 m
Ausdehnung. Kriterium 4 damit erfuellt, keine weitere Variante noetig.

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

136 lines
6.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, junction_arms, ribbon, to_multipolygon
# 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)
sidewalk = _sidewalk(_walk_source(graph), carriage, corridor)
return Street(
carriageway=carriage,
kerb=_kerb_lines(carriage, _kerbless_zone(graph, 0.2), bridges),
sidewalk=sidewalk,
lots=_lots(carriage, corridor, sidewalk),
)
def _walk_source(graph: Graph):
"""Die Fahrbahnteile, an denen ein Gehweg entstehen darf.
Alle Kanten mit Bordstein, dazu an jeder Kreuzung nur die Armstummel mit Bordstein - roh,
ohne die Eckrundung von junction_area() (Fixrunde 5): Die volle, gerundete Kreuzungsfläche
brachte in Fixrunde 4 immer noch zu viel Gasse mit. Der Grund war nicht die Zuschnittbreite,
sondern die Rundung selbst - sie füllt die einspringende Ecke zwischen zwei benachbarten
Gassenstummeln zu einer Nische auf, die abseits der Gassenachse liegt und die ein linearer
Abzug entlang dieser Achse deshalb nie erreicht. Ohne die Rundung entsteht die Nische gar
nicht erst: Die Quelle ist exakt die Fahrbahn mit Bordstein, ungerundet, und der Gehweg
wächst von dort WALK_MAX weit - um die Ecke herum, aber nie eine Gasse hinauf.
"""
teile = [band for k, edge in enumerate(graph.edges)
if SECTION[edge.klass].kerb and (band := ribbon(graph, k)) is not None]
teile += [arm for node in range(len(graph.nodes)) if graph.degree(node) >= 3
for arm in junction_arms(graph, node, kerb_only=True)]
return unary_union(teile) if teile else None
def _kerbless_zone(graph: Graph, extra: float):
"""Der Korridor aller Klassen ohne Bordstein, aus den vollen Mittellinien, um `extra` über die
halbe Fahrbahnbreite hinaus gepuffert.
Zweimal gebraucht, mit derselben Reichweite (Fixrunde 4): Der Bordstein hört hier auf
(_kerb_lines()), und der Kreuzungsanteil der Gehwegquelle hört hier ebenfalls auf
(_walk_source()) - eine Gasse hat laut SECTION weder Bordstein noch Gehweg, in beiden Fällen
ist es dieselbe Fuge von 0,2 m über die eigene Fahrbahnbreite hinaus.
"""
zonen = [LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + extra,
cap_style="flat", join_style="mitre", mitre_limit=1.5)
for edge in graph.edges
if not SECTION[edge.klass].kerb and len(edge.points) >= 2]
return unary_union(zonen) if zonen else None
def _sidewalk(walk_source, carriage, corridor):
"""Der Streifen zwischen Fahrbahnkante und Korridorrand.
Wächst aus walk_source (nur die Fahrbahnteile mit Bordstein) statt aus der ganzen Fahrbahn:
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 - und
weil der Puffer erst gar nicht aus einer Gasse heraus wächst, kann er auch keinen fremden
Gehweg verschlucken, der zufällig nah an einer liegt. `.difference(carriage)` schneidet die
tatsächliche Fahrbahn heraus (auch die einer benachbarten Gasse), nicht nur die Quelle.
Normalisiert wie carriageway_area() auf MultiPolygon (Task 6, Fixrunde 4): Ein einseitiger
Gehweg oder ein leeres Ergebnis kämen sonst als einfaches Polygon bzw. als leere Geometrie
zurück; an einer Berührstelle kann intersection()/difference() sogar eine GeometryCollection
mit zusätzlichen entarteten Linien liefern, an der MultiPolygon([result]) mit TypeError
abstürzt (der Mechanismus ist in test_section.py mit einer erzwungenen GeometryCollection
reproduziert). to_multipolygon() fängt alle drei Fälle einheitlich ab.
"""
if walk_source is None or walk_source.is_empty:
return MultiPolygon()
result = walk_source.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
return to_multipolygon(result)
def _kerb_lines(carriage, kerbless, bridges) -> list[LineString]:
"""Die Fahrbahnkante, ohne die Stücke an Gassen und auf Brücken."""
ohne = [] if kerbless is None else [kerbless]
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, sidewalk) -> list[Polygon]:
"""Was weder Fahrbahn noch Gehweg ist: Parkplätze, Vorplätze, Rampen.
Exakt gerechnet statt mit einem eigenen Puffer: Ein klassenunabhängiger 3-m-Abzug fräße an
einer Gasse (kein Gehweg) und an einer Wohnstraße (2,5 m) mehr weg, als dort Gehweg ist, und
ließe echte Pflasterflächen verschwinden.
"""
rest = corridor.difference(carriage).difference(sidewalk)
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