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.
This commit is contained in:
knso committed 2026-09-18 07:04:30 +02:00
1 parent 77d53fb170
commit 2605f95951
3 files changed
+89 -15

No files matched your search

+9 -3
View File
@@ -104,17 +104,23 @@ def junction_arms(graph: Graph, node: int, kerb_only: bool = False) -> list:
return arme
def junction_area(graph: Graph, node: int):
def junction_area(graph: Graph, node: int, kerb_only: bool = False):
"""Die Fläche einer Kreuzung: die Stummel aller Arme, mit gerundeten einspringenden Ecken.
Gerundet wird durch Schließen (aufblasen, dann wieder schrumpfen) - und ausdrücklich nur
innerhalb einer Scheibe um den Knoten. Auf das ganze Netz angewandt verklebte dieselbe
Operation zwei parallele Straßen, deren Fahrbahnen weniger als 2 * FILLET auseinanderliegen.
kerb_only rundet nur den Kreuzungsanteil der bordsteintragenden Arme (Befund 7 der Fixwelle,
für die Gehwegquelle in street.py): Eine Gasse liefert dabei gar keinen Arm in die zu
schließende Fläche, kann also auch keine Nische mit einem Nachbararm bilden - anders als
Fixrunde 4, die dieselbe Rundung auf ALLE Arme anwandte und damit Gassen mit rundete.
"""
if not graph.incident(node):
incident = graph.incident(node)
if not incident or (kerb_only and not any(SECTION[graph.edges[k].klass].kerb for k in incident)):
return None
reach = setback(graph, node)
star = unary_union(junction_arms(graph, node))
star = unary_union(junction_arms(graph, node, kerb_only=kerb_only))
near = Point(graph.nodes[node].point).buffer(reach + FILLET + 2.0)
rounded = star.buffer(FILLET, join_style="round").buffer(-FILLET, join_style="round")
return star.union(rounded.intersection(near))
+21 -12
View File
@@ -14,7 +14,7 @@ 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
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
@@ -47,19 +47,27 @@ def build_street(graph: Graph, corridor, bridges) -> Street:
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.
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 += [arm for node in range(len(graph.nodes)) if graph.degree(node) >= 3
for arm in junction_arms(graph, node, kerb_only=True)]
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
@@ -73,7 +81,8 @@ def _kerbless_zone(graph: Graph, extra: float):
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_arms(kerb_only=True)) und braucht deshalb keinen nachträglichen Abzug mehr.
(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)
+59
View File
@@ -1,6 +1,8 @@
"""Die Mulde: Löcher in der Bodenplatte, Bordsteinlinien, Gehwegflächen, Restflächen."""
from __future__ import annotations
import math
from shapely.geometry import LineString, Point, box
from shapely.ops import unary_union
@@ -227,3 +229,60 @@ def test_gehwegrest_an_kreuzung_mit_mehreren_gassen_bleibt_aus():
# von rund WALK_MAX + halbe Gassenbreite (~6 m Ausdehnung); 1,0 m² lässt Spielraum für
# künftige, geometrisch bedingte Rundungsreste, ohne die Kernaussage zu verwässern.
assert rest.area < 1.0, "die Nische zwischen zwei Gassenstummeln bleibt frei von Gehweg"
def test_gehweg_schliesst_die_luecke_an_gewoehnlicher_kreuzungsecke():
"""Befund 7 der Fixwelle: An einer gewöhnlichen Kreuzung (mehrere Arme mit Bordstein, keine
Gasse beteiligt) klaffte zwischen der gerundeten Fahrbahnecke (carriageway_area() rundet mit
FILLET=6,0) und der ungerundeten Gehwegquelle eine kleine Lücke - eine reine Dilatation der
Quelle um WALK_MAX=3,0 zeichnet eine Reflexecke nicht nach. Am echten Netz 0,26-2,2 m² je
Ecke bei 690 Kreuzungen; hier reproduziert an einer T-Kreuzung aus drei Wohnstraßen-Armen,
mit demselben großzügigen Korridor wie in den Gassen-Tests oben (carriageway_area() + 3 m).
Vor der Korrektur maß jede der beiden Ecken exakt 0,256 m² Lücke (52 gerundete Doppelpunkte
davon zusammen 0,5114 m²) - innerhalb des berichteten Bereichs.
_walk_source() rundet den Kreuzungsanteil jetzt mit derselben Schließung wie carriageway_area()
(junction_area(kerb_only=True)), sodass Fahrbahn- und Gehwegquelle an der Ecke wieder
zueinander passen.
"""
graph = Graph(
nodes=[Node((0.0, 0.0)), Node((-50.0, 0.0)), Node((50.0, 0.0)), Node((0.0, -50.0))],
edges=[Edge(0, 1, [(0.0, 0.0), (-50.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
Edge(0, 2, [(0.0, 0.0), (50.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
Edge(0, 3, [(0.0, 0.0), (0.0, -50.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse")])
corridor = carriageway_area(graph).buffer(3.0, join_style="round")
street = build_street(graph, corridor, bridges=[])
# Der rohe Rest (vor dem LOT_MIN-Filter aus _lots()): An einer gewöhnlichen Ecke darf davon
# nichts übrig bleiben, sonst wird die Lücke später als zu kleines Reststück verworfen und
# bleibt ein unsichtbares Loch im Boden.
roh_rest = corridor.difference(street.carriageway).difference(street.sidewalk)
ecken = Point(0, 0).buffer(15.0)
luecke = sum(g.area for g in getattr(roh_rest, "geoms", [roh_rest])
if not g.is_empty and g.intersects(ecken) and g.area < LOT_MIN)
assert luecke < 0.05, f"{luecke:.4f} m² Lücke an den Kreuzungsecken, sollte geschlossen sein"
def test_gehweg_waechst_auch_bei_engem_winkel_nicht_in_die_gasse():
"""Gegenprobe zu obigem Befund-7-Fix, am schärfsten denkbaren Fall: Die Rundung des
Kreuzungsanteils darf nicht wieder (wie in Fixrunde 4) über eine benachbarte Gasse
hinwegreichen, auch wenn die Gasse in einem enges Winkel zwischen zwei Armen mit Bordstein
liegt - der Fall, in dem eine Schließung am ehesten eine Nische neben der Gasse mitfüllen
könnte. Zwei Wohnstraßen-Arme bei 0° und 40°, eine Gasse dazwischen, bei 10° bis 30° in 5°-
Schritten durchprobiert: In jedem Fall bleibt der Gehweg außerhalb des Gassenkorridors, weil
die Gasse selbst nie einen Arm in die zu schließende Fläche liefert (kerb_only=True).
"""
for grad in (10.0, 15.0, 20.0, 25.0, 30.0):
rad = math.radians(grad)
a = (60.0, 0.0)
b = (60.0 * math.cos(math.radians(40.0)), 60.0 * math.sin(math.radians(40.0)))
g = (60.0 * math.cos(rad), 60.0 * math.sin(rad))
graph = Graph(
nodes=[Node((0.0, 0.0)), Node(a), Node(b), Node(g)],
edges=[Edge(0, 1, [(0.0, 0.0), a], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
Edge(0, 2, [(0.0, 0.0), b], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
Edge(0, 3, [(0.0, 0.0), g], SECTION["gasse"].corridor, "gasse")])
corridor = carriageway_area(graph).buffer(3.0, join_style="round")
street = build_street(graph, corridor, bridges=[])
gassen_korridor = LineString([(0.0, 0.0), g]).buffer(SECTION["gasse"].corridor / 2, cap_style="flat")
leck = street.sidewalk.intersection(gassen_korridor).area
assert leck < 0.01, f"{leck:.4f} m² Gehweg im Gassenkorridor bei {grad}° Gassenwinkel"