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:
1 parent
77d53fb170
commit
2605f95951
3 files changed
+89
-15
No files matched your search
@@ -104,17 +104,23 @@ def junction_arms(graph: Graph, node: int, kerb_only: bool = False) -> list:
|
|||||||
return arme
|
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.
|
"""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
|
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
|
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.
|
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
|
return None
|
||||||
reach = setback(graph, node)
|
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)
|
near = Point(graph.nodes[node].point).buffer(reach + FILLET + 2.0)
|
||||||
rounded = star.buffer(FILLET, join_style="round").buffer(-FILLET, join_style="round")
|
rounded = star.buffer(FILLET, join_style="round").buffer(-FILLET, join_style="round")
|
||||||
return star.union(rounded.intersection(near))
|
return star.union(rounded.intersection(near))
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from shapely.geometry import LineString, MultiPolygon, Polygon
|
|||||||
from shapely.ops import unary_union
|
from shapely.ops import unary_union
|
||||||
|
|
||||||
from .roads import Graph
|
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).
|
# Die Tiefe steht hier als dokumentierter Wert; gebaut wird sie im Browser (geometry.js).
|
||||||
DEPTH = 0.15
|
DEPTH = 0.15
|
||||||
@@ -47,19 +47,27 @@ def build_street(graph: Graph, corridor, bridges) -> Street:
|
|||||||
def _walk_source(graph: Graph):
|
def _walk_source(graph: Graph):
|
||||||
"""Die Fahrbahnteile, an denen ein Gehweg entstehen darf.
|
"""Die Fahrbahnteile, an denen ein Gehweg entstehen darf.
|
||||||
|
|
||||||
Alle Kanten mit Bordstein, dazu an jeder Kreuzung nur die Armstummel mit Bordstein - roh,
|
Alle Kanten mit Bordstein, dazu an jeder Kreuzung der Kreuzungsanteil nur der Armstummel mit
|
||||||
ohne die Eckrundung von junction_area() (Fixrunde 5): Die volle, gerundete Kreuzungsfläche
|
Bordstein, mit derselben Eckrundung wie junction_area() (Befund 7 der Fixwelle). Fixrunde 5
|
||||||
brachte in Fixrunde 4 immer noch zu viel Gasse mit. Der Grund war nicht die Zuschnittbreite,
|
hatte die Rundung noch ganz weggelassen: Die volle, auf ALLE Arme angewandte Rundung brachte
|
||||||
sondern die Rundung selbst - sie füllt die einspringende Ecke zwischen zwei benachbarten
|
in Fixrunde 4 zu viel Gasse mit, weil sie die einspringende Ecke zwischen zwei benachbarten
|
||||||
Gassenstummeln zu einer Nische auf, die abseits der Gassenachse liegt und die ein linearer
|
Gassenstummeln zu einer Nische auffüllte. Der eigentliche Fehler war aber, Gassenarme
|
||||||
Abzug entlang dieser Achse deshalb nie erreicht. Ohne die Rundung entsteht die Nische gar
|
überhaupt in die zu rundende Fläche aufzunehmen - nicht die Rundung selbst. Ohne sie blieb
|
||||||
nicht erst: Die Quelle ist exakt die Fahrbahn mit Bordstein, ungerundet, und der Gehweg
|
stattdessen an jeder gewöhnlichen Kreuzung (mehrere Arme mit Bordstein, keine Gasse
|
||||||
wächst von dort WALK_MAX weit - um die Ecke herum, aber nie eine Gasse hinauf.
|
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)
|
teile = [band for k, edge in enumerate(graph.edges)
|
||||||
if SECTION[edge.klass].kerb and (band := ribbon(graph, k)) is not None]
|
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
|
teile += [area for node in range(len(graph.nodes)) if graph.degree(node) >= 3
|
||||||
for arm in junction_arms(graph, node, kerb_only=True)]
|
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
|
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
|
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
|
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,
|
zonen = [LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + extra,
|
||||||
cap_style="flat", join_style="mitre", mitre_limit=1.5)
|
cap_style="flat", join_style="mitre", mitre_limit=1.5)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"""Die Mulde: Löcher in der Bodenplatte, Bordsteinlinien, Gehwegflächen, Restflächen."""
|
"""Die Mulde: Löcher in der Bodenplatte, Bordsteinlinien, Gehwegflächen, Restflächen."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
from shapely.geometry import LineString, Point, box
|
from shapely.geometry import LineString, Point, box
|
||||||
from shapely.ops import unary_union
|
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
|
# 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.
|
# 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"
|
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"
|
||||||
Reference in new issue
Block a user