Files
gta6/tools/extract/leonida/street.py
T
knso 2605f95951 fix(extract): Gehweglücke an gewöhnlichen Kreuzungsecken geschlossen
Befund 7 (seit Task 7 geparkt): carriageway_area() rundet die einspringenden
Ecken einer Kreuzung mit FILLET=6,0 (Schließen: aufblasen, dann wieder
schrumpfen), die Gehwegquelle in _walk_source() blieb seit Fixrunde 5
dagegen ungerundet. Eine reine Dilatation der Quelle um WALK_MAX=3,0
zeichnet eine Reflexecke nicht nach - zwischen der gerundeten Fahrbahn und
dem ungerundeten Gehweg blieb an jeder gewöhnlichen Kreuzung (mehrere Arme
mit Bordstein, keine Gasse beteiligt) eine kleine Lücke, zu klein für
LOT_MIN und damit ein unsichtbares Loch im Boden statt einer Restfläche.
Gemessen 0,26-2,2 m² je Ecke bei 690 Kreuzungen.

junction_area() (section.py) bekommt einen kerb_only-Schalter und rundet
damit wahlweise nur den Kreuzungsanteil der bordsteintragenden Arme;
_walk_source() ruft sie jetzt statt des rohen junction_arms(kerb_only=True)
auf. Der Unterschied zu Fixrunde 4 (die dieselbe Rundung auf ALLE Arme
anwandte und damit Gassen mit rundete): Eine Gasse liefert gar keinen Arm
in die zu schließende Fläche, kann also strukturell keine Nische mit einem
Nachbararm bilden - unabhängig vom Winkel.

Nachgemessen an einer T-Kreuzung aus drei Wohnstraßen (derselbe großzügige
Korridor wie in den bestehenden Gassen-Tests, carriageway_area()+3 m): vor
der Korrektur 0,256 m² Lücke je der beiden Ecken (0,5114 m² zusammen), exakt
im berichteten Bereich; danach 0,0 m². Eine zweite, an sich schon bei
Fixrunde 5 bestehende Kontrolle: Gasse in einem engen Winkel (10-30°)
zwischen zwei Wohnstraßen-Armen - der Gehweg bleibt für jeden geprüften
Winkel bei 0,0 m² im Gassenkorridor. Alle bestehenden Gassen-Sicherungen in
test_street.py (fünf Fixrunden Vorarbeit) bleiben grün.
2026-09-18 07:04:30 +02:00

148 lines
7.3 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, 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 der Kreuzungsanteil nur der Armstummel mit
Bordstein, mit derselben Eckrundung wie junction_area() (Befund 7 der Fixwelle). Fixrunde 5
hatte die Rundung noch ganz weggelassen: Die volle, auf ALLE Arme angewandte Rundung brachte
in Fixrunde 4 zu viel Gasse mit, weil sie die einspringende Ecke zwischen zwei benachbarten
Gassenstummeln zu einer Nische auffüllte. Der eigentliche Fehler war aber, Gassenarme
überhaupt in die zu rundende Fläche aufzunehmen - nicht die Rundung selbst. Ohne sie blieb
stattdessen an jeder gewöhnlichen Kreuzung (mehrere Arme mit Bordstein, keine Gasse
beteiligt) eine Lücke zwischen der ungerundeten Gehwegquelle und der gerundeten Fahrbahn:
0,26-2,2 m² je Ecke bei 690 Kreuzungen, weil `carriage` (carriageway_area(), gerundet mit
FILLET=6) und `walk_source` (ungerundet) an der Kreuzungsecke verschieden geformt waren und
eine reine Dilatation um WALK_MAX=3 eine Reflexecke nicht nachzeichnet.
junction_area(kerb_only=True) rundet jetzt nur den Kreuzungsanteil der bordsteintragenden
Arme: Eine Gasse liefert dabei keinen Arm in die zu schließende Fläche und kann deshalb auch
keine Nische mit einem Nachbararm bilden, gewöhnliche Kreuzungsecken bekommen aber dieselbe
Rundung wie die Fahrbahn und schließen die Lücke.
"""
teile = [band for k, edge in enumerate(graph.edges)
if SECTION[edge.klass].kerb and (band := ribbon(graph, k)) is not None]
teile += [area for node in range(len(graph.nodes)) if graph.degree(node) >= 3
and (area := junction_area(graph, node, kerb_only=True)) is not None and not area.is_empty]
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.
Nur noch einmal gebraucht: Der Bordstein hört hier auf (_kerb_lines()) - eine Gasse hat laut
SECTION weder Bordstein noch Gehweg, und die Fuge dafür ist 0,2 m über die eigene
Fahrbahnbreite hinaus.
Bis Fixrunde 4 schnitt dieselbe Zone auch den Kreuzungsanteil der Gehwegquelle zu; seit
Fixrunde 5 nimmt _walk_source() stattdessen von vornherein nur die Arme mit Bordstein
(junction_area(kerb_only=True), Befund 7 der Fixwelle) und braucht deshalb keinen
nachträglichen Abzug mehr.
"""
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