Files
gta6/tools/extract/leonida/street.py
T
knsoandClaude Sonnet 5 ca5710730e fix(extract): Gehweg waechst nur aus Fahrbahn mit Bordstein, nicht mehr aus allem
Die Reichweitenzone aus der letzten Runde (Gehweg aus der ganzen Fahrbahn wachsen
lassen, dann eine Zone um jede bordsteinlose Klasse wieder abziehen) fragte die
falsche Richtung: "liegt dieser Punkt nahe einer Gasse?" statt "gehoert die
naechste Fahrbahn zu einer Klasse mit Bordstein?". Die Zone kennt ihre
Nachbarschaft nicht und frass dabei echten Wohnstrassen-Gehweg mit, sobald der
zufaellig nah an einer Gasse lag - 250 von 500 m2 bei einer parallelen Gasse mit
nur 0,5 m Korridorabstand, 7,58 m2 systematisch an jeder Gasse-Wohnstrasse-
Einmuendung, unabhaengig von der Gassenlaenge.

_walk_source(graph) baut die Fahrbahnquelle jetzt nur aus Kanten und Kreuzungen
mit mindestens einem bordsteintragenden Arm (ribbon()/junction_area(), gefiltert
auf SECTION[...].kerb). _sidewalk() waechst aus dieser Quelle statt aus der
ganzen Fahrbahn und braucht keinen nachtraeglichen Abzug mehr. _kerbless_zone()
bleibt nur noch fuer den Bordstein-Ausschluss in _kerb_lines(), mit dem
urspruenglichen festen Zuschlag von 0,2 m.

Rechenzeit am echten Netz gemessen: _walk_source() kostet zusaetzlich 1,8 s
(carriageway_area() braucht 2,45 s), build_street() insgesamt rund 5,1-5,2 s -
weit unter der 30-s-Schranke, kein Anlass, die Kreuzungsflaechen nur einmal
statt zweimal zu berechnen.

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

134 lines
6.1 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_area, ribbon
# 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), 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 jede Kreuzung, an der mindestens ein solcher Arm hängt.
Vorher wuchs der Gehweg aus der gesamten Fahrbahn, und die Klassen ohne Bordstein wurden
hinterher über eine Reichweitenzone wieder abgezogen — die kennt aber ihre Nachbarschaft
nicht und nahm dabei auch den Gehweg einer Wohnstraße mit, die zufällig nah an einer Gasse
lag. Wer von der richtigen Seite her wächst, braucht den Abzug gar nicht.
"""
teile = [band for k, edge in enumerate(graph.edges)
if SECTION[edge.klass].kerb and (band := ribbon(graph, k)) is not None]
teile += [flaeche for node in range(len(graph.nodes))
if graph.degree(node) >= 3
and any(SECTION[graph.edges[k].klass].kerb for k in graph.incident(node))
and (flaeche := junction_area(graph, node)) is not None]
return unary_union(teile) if teile else None
def _kerbless_zone(graph: Graph):
"""Der Korridor aller Klassen ohne Bordstein, aus den vollen Mittellinien, um 0,2 m über die
halbe Fahrbahnbreite hinaus gepuffert - eine Fuge für einen sauberen Schnitt.
Nur noch für _kerb_lines() gebraucht: Der Bordstein hört hier auf. Der Gehweg braucht diesen
Abzug seit Fixrunde 3 nicht mehr - er wächst aus _walk_source() und damit von vornherein nur
dort, wo eine Klasse mit Bordstein liegt, statt aus der ganzen Fahrbahn mit nachträglichem
Abzug einer Zone, die ihre Nachbarschaft nicht kennt.
"""
zonen = [LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + 0.2,
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): 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.
"""
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)
if result.is_empty:
return MultiPolygon()
return result if result.geom_type == "MultiPolygon" else 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