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 shapely.ops import unary_union
|
||||||
|
|
||||||
from .roads import Graph
|
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).
|
# Die Tiefe steht hier als dokumentierter Wert; gebaut wird sie im Browser (geometry.js).
|
||||||
DEPTH = 0.15
|
DEPTH = 0.15
|
||||||
@@ -35,51 +35,66 @@ class Street:
|
|||||||
def build_street(graph: Graph, corridor, bridges) -> Street:
|
def build_street(graph: Graph, corridor, bridges) -> Street:
|
||||||
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
|
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
|
||||||
carriage = carriageway_area(graph)
|
carriage = carriageway_area(graph)
|
||||||
sidewalk = _sidewalk(carriage, corridor, _kerbless_zone(graph, WALK_MAX))
|
sidewalk = _sidewalk(_walk_source(graph), carriage, corridor)
|
||||||
return Street(
|
return Street(
|
||||||
carriageway=carriage,
|
carriageway=carriage,
|
||||||
kerb=_kerb_lines(carriage, _kerbless_zone(graph, 0.2), bridges),
|
kerb=_kerb_lines(carriage, _kerbless_zone(graph), bridges),
|
||||||
sidewalk=sidewalk,
|
sidewalk=sidewalk,
|
||||||
lots=_lots(carriage, corridor, sidewalk),
|
lots=_lots(carriage, corridor, sidewalk),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _kerbless_zone(graph: Graph, extra: float):
|
def _walk_source(graph: Graph):
|
||||||
"""Der Korridor aller Klassen ohne Bordstein, aus den vollen Mittellinien, um `extra` über die
|
"""Die Fahrbahnteile, an denen ein Gehweg entstehen darf.
|
||||||
halbe Fahrbahnbreite hinaus gepuffert.
|
|
||||||
|
|
||||||
Zweimal gebraucht, mit unterschiedlicher Reichweite: Der Bordstein hört direkt an der
|
Alle Kanten mit Bordstein, dazu jede Kreuzung, an der mindestens ein solcher Arm hängt.
|
||||||
Fahrbahnkante auf (extra=0,2, nur eine Fuge für einen sauberen Schnitt), aber der Gehwegpuffer
|
Vorher wuchs der Gehweg aus der gesamten Fahrbahn, und die Klassen ohne Bordstein wurden
|
||||||
in _sidewalk() reicht bis WALK_MAX über dieselbe Kante hinaus - mit der schmalen Fuge bliebe
|
hinterher über eine Reichweitenzone wieder abgezogen — die kennt aber ihre Nachbarschaft
|
||||||
dort ein bis zu 2,8 m breiter Streifen übrig, den eine Klasse ohne Gehweg trotzdem als Gehweg
|
nicht und nahm dabei auch den Gehweg einer Wohnstraße mit, die zufällig nah an einer Gasse
|
||||||
bekäme, sobald neben ihr mehr gemessene Fläche liegt als ihre eigene Breite (siehe Bericht).
|
lag. Wer von der richtigen Seite her wächst, braucht den Abzug gar nicht.
|
||||||
Die volle Mittellinie statt ribbon(), damit auch der Anteil der Kreuzungsfläche abgedeckt ist,
|
|
||||||
an dem die bordsteinlose Klasse einmündet.
|
|
||||||
"""
|
"""
|
||||||
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)
|
cap_style="flat", join_style="mitre", mitre_limit=1.5)
|
||||||
for edge in graph.edges
|
for edge in graph.edges
|
||||||
if not SECTION[edge.klass].kerb and len(edge.points) >= 2]
|
if not SECTION[edge.klass].kerb and len(edge.points) >= 2]
|
||||||
return unary_union(zonen) if zonen else None
|
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.
|
"""Der Streifen zwischen Fahrbahnkante und Korridorrand.
|
||||||
|
|
||||||
Ein einziger Puffer für alle Klassen reicht: Weil am Korridor beschnitten wird, bleibt bei
|
Wächst aus walk_source (nur die Fahrbahnteile mit Bordstein) statt aus der ganzen Fahrbahn:
|
||||||
12 m Korridor und 7 m Fahrbahn genau der 2,5-m-Gehweg der Wohnstraße übrig, bei 18 m und
|
Weil am Korridor beschnitten wird, bleibt bei 12 m Korridor und 7 m Fahrbahn genau der
|
||||||
12 m genau die 3,0 m der Hauptstraße. `kerbless` schneidet zusätzlich heraus, wo eine Klasse
|
2,5-m-Gehweg der Wohnstraße übrig, bei 18 m und 12 m genau die 3,0 m der Hauptstraße - und
|
||||||
laut SECTION gar keinen Gehweg vorsieht (die Gasse) - der Korridor allein reicht dafür nicht:
|
weil der Puffer erst gar nicht aus einer Gasse heraus wächst, kann er auch keinen fremden
|
||||||
er füllt zwar meist genau die Gassenbreite, aber sobald daneben mehr gemessene Fläche liegt,
|
Gehweg verschlucken, der zufällig nah an einer liegt. `.difference(carriage)` schneidet die
|
||||||
würde der WALK_MAX-Puffer sie sonst fälschlich zu Gehweg machen.
|
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
|
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
|
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.
|
Task 10/12 verlassen sich auf die MultiPolygon-Zusage.
|
||||||
"""
|
"""
|
||||||
result = carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
|
if walk_source is None or walk_source.is_empty:
|
||||||
if kerbless is not None:
|
return MultiPolygon()
|
||||||
result = result.difference(kerbless)
|
result = walk_source.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
|
||||||
if result.is_empty:
|
if result.is_empty:
|
||||||
return MultiPolygon()
|
return MultiPolygon()
|
||||||
return result if result.geom_type == "MultiPolygon" else MultiPolygon([result])
|
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
|
assert street.sidewalk.intersection(roh).area < 1e-6
|
||||||
bilanz = street.carriageway.area + street.sidewalk.area + roh.area
|
bilanz = street.carriageway.area + street.sidewalk.area + roh.area
|
||||||
assert abs(bilanz - weit.area) < 1e-6
|
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