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>
This commit is contained in:
1 parent
ea72cb934a
commit
ca5710730e
2 files changed
+84
-24
No files matched your search
@@ -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
|
||||
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
|
||||
@@ -35,51 +35,66 @@ class Street:
|
||||
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(carriage, corridor, _kerbless_zone(graph, WALK_MAX))
|
||||
sidewalk = _sidewalk(_walk_source(graph), carriage, corridor)
|
||||
return Street(
|
||||
carriageway=carriage,
|
||||
kerb=_kerb_lines(carriage, _kerbless_zone(graph, 0.2), bridges),
|
||||
kerb=_kerb_lines(carriage, _kerbless_zone(graph), bridges),
|
||||
sidewalk=sidewalk,
|
||||
lots=_lots(carriage, corridor, sidewalk),
|
||||
)
|
||||
|
||||
|
||||
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.
|
||||
def _walk_source(graph: Graph):
|
||||
"""Die Fahrbahnteile, an denen ein Gehweg entstehen darf.
|
||||
|
||||
Zweimal gebraucht, mit unterschiedlicher Reichweite: Der Bordstein hört direkt an der
|
||||
Fahrbahnkante auf (extra=0,2, nur eine Fuge für einen sauberen Schnitt), aber der Gehwegpuffer
|
||||
in _sidewalk() reicht bis WALK_MAX über dieselbe Kante hinaus - mit der schmalen Fuge bliebe
|
||||
dort ein bis zu 2,8 m breiter Streifen übrig, den eine Klasse ohne Gehweg trotzdem als Gehweg
|
||||
bekäme, sobald neben ihr mehr gemessene Fläche liegt als ihre eigene Breite (siehe Bericht).
|
||||
Die volle Mittellinie statt ribbon(), damit auch der Anteil der Kreuzungsfläche abgedeckt ist,
|
||||
an dem die bordsteinlose Klasse einmündet.
|
||||
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.
|
||||
"""
|
||||
zonen = [LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + extra,
|
||||
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(carriage, corridor, kerbless):
|
||||
def _sidewalk(walk_source, carriage, corridor):
|
||||
"""Der Streifen zwischen Fahrbahnkante und Korridorrand.
|
||||
|
||||
Ein einziger Puffer für alle Klassen reicht: 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. `kerbless` schneidet zusätzlich heraus, wo eine Klasse
|
||||
laut SECTION gar keinen Gehweg vorsieht (die Gasse) - der Korridor allein reicht dafür nicht:
|
||||
er füllt zwar meist genau die Gassenbreite, aber sobald daneben mehr gemessene Fläche liegt,
|
||||
würde der WALK_MAX-Puffer sie sonst fälschlich zu Gehweg machen.
|
||||
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.
|
||||
"""
|
||||
result = carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
|
||||
if kerbless is not None:
|
||||
result = result.difference(kerbless)
|
||||
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])
|
||||
|
||||
@@ -130,3 +130,48 @@ def test_fahrbahn_gehweg_und_rest_ergeben_wieder_den_korridor():
|
||||
assert street.sidewalk.intersection(roh).area < 1e-6
|
||||
bilanz = street.carriageway.area + street.sidewalk.area + roh.area
|
||||
assert abs(bilanz - weit.area) < 1e-6
|
||||
|
||||
|
||||
def test_aufteilung_geht_fuer_jede_klasse_auf():
|
||||
# Fixrunde 3, Punkt 4: keine Klasse ausgenommen, auch nicht die Gasse (dort ist der Gehweg
|
||||
# leer, aber die Bilanz muss trotzdem exakt aufgehen: Fahrbahn plus roher Rest ergibt dann
|
||||
# den ganzen Korridor).
|
||||
for klass in SECTION:
|
||||
street = build_street(strasse(klass), korridor(klass), bridges=[])
|
||||
roh = korridor(klass).difference(street.carriageway).difference(street.sidewalk)
|
||||
bilanz = street.carriageway.area + street.sidewalk.area + roh.area
|
||||
assert abs(bilanz - korridor(klass).area) < 1e-6, klass
|
||||
|
||||
|
||||
def test_wohnstrasse_behaelt_ihren_gehweg_trotz_naher_paralleler_gasse():
|
||||
# Fixrunde 3: Der alte Ansatz ließ den Gehweg aus der *ganzen* Fahrbahn wachsen und zog
|
||||
# danach eine Reichweitenzone um jede bordsteinlose Klasse wieder ab. Diese Zone kennt ihre
|
||||
# Nachbarschaft nicht - jeder Punkt näher als WALK_MAX=3 m an einer Gassen-Mittellinie fiel
|
||||
# weg, auch wenn dort tatsächlich eine Wohnstraße liegt. Wohnstraße bei y=0 (Korridor bis
|
||||
# y=6), Gasse parallel bei y=9,5 (Korridor ab y=6,5) - 0,5 m Lücke zwischen den Korridoren,
|
||||
# die Gassen-Mittellinie liegt trotzdem nur 9,5 m entfernt, deutlich innerhalb der alten,
|
||||
# 6 m weiten Reichweitenzone (carriageway/2 + WALK_MAX = 3+3).
|
||||
graph = Graph(nodes=[Node((0.0, 0.0)), Node((100.0, 0.0)), Node((0.0, 9.5)), Node((100.0, 9.5))],
|
||||
edges=[Edge(0, 1, [(0.0, 0.0), (100.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
|
||||
Edge(2, 3, [(0.0, 9.5), (100.0, 9.5)], SECTION["gasse"].corridor, "gasse")])
|
||||
corridor = unary_union([box(0, -6, 100, 6), box(0, 6.5, 100, 12.5)])
|
||||
street = build_street(graph, corridor, bridges=[])
|
||||
assert round(street.sidewalk.area) == 500, "die volle Gehwegfläche der Wohnstraße bleibt"
|
||||
|
||||
|
||||
def test_wohnstrassen_gehweg_bleibt_vollstaendig_an_gasseneinmuendung():
|
||||
# Fixrunde 3: an der T-Einmündung reichte dieselbe Reichweitenzone der Gasse (jetzt um ihre
|
||||
# volle Mittellinie, wegen Fixrunde 1) symmetrisch in beide Wohnstraßen-Gehwege an der Ecke
|
||||
# hinein - unabhängig von der Gassenlänge, solange die Gasse nicht selbst in der Nähe endet
|
||||
# (deshalb hier 10 m lang, nicht die 5 m aus dem Bordstein-Test oben). Zwei Punkte, die genau
|
||||
# in den vorher fehlenden, je 3,79 m² großen Flächen an den gerundeten Kreuzungsecken liegen.
|
||||
graph = Graph(nodes=[Node((0.0, 0.0)), Node((50.0, 0.0)), Node((100.0, 0.0)), Node((50.0, -10.0))],
|
||||
edges=[Edge(0, 1, [(0.0, 0.0), (50.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
|
||||
Edge(1, 2, [(50.0, 0.0), (100.0, 0.0)], SECTION["wohnstrasse"].corridor, "wohnstrasse"),
|
||||
Edge(1, 3, [(50.0, 0.0), (50.0, -10.0)], SECTION["gasse"].corridor, "gasse")])
|
||||
wohn_korridor = box(0, -6, 100, 6)
|
||||
gassen_korridor = LineString([(50.0, 0.0), (50.0, -10.0)]).buffer(SECTION["gasse"].corridor / 2,
|
||||
cap_style="flat")
|
||||
street = build_street(graph, unary_union([wohn_korridor, gassen_korridor]), bridges=[])
|
||||
assert street.sidewalk.contains(Point(45.0, -5.0)), "Wohnstraßen-Gehweg links der Einmündung"
|
||||
assert street.sidewalk.contains(Point(55.0, -5.0)), "Wohnstraßen-Gehweg rechts der Einmündung"
|
||||
Reference in new issue
Block a user