fix(extract): Kreuzungsstummel schmaeler wachsen lassen, GeometryCollection-Absturz beheben
_walk_source() nahm bisher die ganze junction_area() eines Knotens auf, sobald irgendein anliegender Arm Bordstein hat - auch den Anteil, der zu einer dort einmuendenden Gasse gehoert. Der Gehweg wuchs dadurch die volle Laenge des Kreuzungsstummels die Gasse hinauf (bis zu setback+1, beim Boulevard 11 m) statt nur um die Ecke. _kerbless_zone() bekommt den extra-Parameter zurueck (_kerb_lines() und _walk_source() rufen sie beide mit 0.2 auf) und beschneidet jetzt auch den Kreuzungsanteil der Gehwegquelle. Gemessen: an einer T-Kreuzung (2 Gassen + 1 Wohnstrasse) verschwindet der fehlerhafte Gassenstummel-Gehweg vollstaendig (53,11 -> 0,0 m2). An einer 4-Wege-Kreuzung mit 3 Gassen + 1 Boulevard reicht der Fix nicht aus: der Rest in der Nische zwischen zwei Gassenstummeln schrumpft nur auf 27,14 statt der erwarteten rund 2 m2, und seine maximale Ausdehnung von der Kreuzungsmitte (11,14 m) liegt weit ueber der als Grenze genannten 3-4 m - dokumentiert als bekannter, nicht ausreichend geloester Punkt, siehe Fixbericht. Unabhaengig davon: MultiPolygon([result]) wirft TypeError, wenn result eine GeometryCollection ist (intersection()/difference() liefern das gelegentlich an Beruehrstellen, mit einer entarteten Nulllinie neben echten Polygonen). Neuer Helfer to_multipolygon() in section.py filtert auf echte Polygone, statt daran abzustuerzen; carriageway_area() und _sidewalk() nutzen ihn beide. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca5710730e
commit
685ac666e6
4 files changed
+127
-28
No files matched your search
@@ -110,15 +110,26 @@ def junction_area(graph: Graph, node: int):
|
||||
return star.union(rounded.intersection(near))
|
||||
|
||||
|
||||
def to_multipolygon(geom) -> MultiPolygon:
|
||||
"""Nur die Flächenanteile einer Geometrie, als MultiPolygon.
|
||||
|
||||
intersection()/difference() liefern nicht immer sauber ein (Multi)Polygon: An einer
|
||||
Berührstelle kann eine GeometryCollection mit zusätzlichen entarteten Linien oder Punkten
|
||||
herauskommen (Fixrunde 4 zu Task 7, am Gehweg reproduziert). MultiPolygon(geoms) nimmt aber
|
||||
nur Polygone entgegen und wirft eine TypeError, sobald ein Element davon abweicht - hier
|
||||
werden die Nicht-Flächen-Teile deshalb verworfen, statt dass die Rechnung daran abstürzt.
|
||||
"""
|
||||
if geom.is_empty:
|
||||
return MultiPolygon()
|
||||
return MultiPolygon([g for g in getattr(geom, "geoms", [geom]) if g.geom_type == "Polygon"])
|
||||
|
||||
|
||||
def carriageway_area(graph: Graph):
|
||||
"""Alle Bänder und alle Kreuzungsflächen als eine Geometrie."""
|
||||
parts = [band for k in range(len(graph.edges)) if (band := ribbon(graph, k)) is not None]
|
||||
parts += [area for node in range(len(graph.nodes)) if graph.degree(node) >= 3
|
||||
and (area := junction_area(graph, node)) is not None]
|
||||
vereint = unary_union(parts)
|
||||
if vereint.is_empty:
|
||||
return MultiPolygon()
|
||||
# Zusage der Schnittstelle: immer MultiPolygon. unary_union() liefert bei zusammenhängender
|
||||
# Eingabe ein einfaches Polygon, und Task 7 iteriert über .geoms — das bricht dann an kleinen
|
||||
# Prüfkörpern, aber nicht am echten Netz, also genau da, wo niemand damit rechnet.
|
||||
return vereint if vereint.geom_type == "MultiPolygon" else MultiPolygon([vereint])
|
||||
return to_multipolygon(unary_union(parts))
|
||||
@@ -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_area, ribbon
|
||||
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
|
||||
@@ -38,7 +38,7 @@ def build_street(graph: Graph, corridor, bridges) -> Street:
|
||||
sidewalk = _sidewalk(_walk_source(graph), carriage, corridor)
|
||||
return Street(
|
||||
carriageway=carriage,
|
||||
kerb=_kerb_lines(carriage, _kerbless_zone(graph), bridges),
|
||||
kerb=_kerb_lines(carriage, _kerbless_zone(graph, 0.2), bridges),
|
||||
sidewalk=sidewalk,
|
||||
lots=_lots(carriage, corridor, sidewalk),
|
||||
)
|
||||
@@ -47,31 +47,39 @@ 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 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.
|
||||
Alle Kanten mit Bordstein, dazu der Kreuzungsanteil jedes Knotens, an dem mindestens ein
|
||||
solcher Arm hängt - aber ohne den Gassenstummel darin (Fixrunde 4): junction_area() liefert
|
||||
die ganze Kreuzungsfläche als ein Stück, auch den Anteil der Gasse, die dort einmündet. Ohne
|
||||
den Abzug wüchse der Gehweg die volle Länge dieses Stummels (bis zu setback+1, beim Boulevard
|
||||
11 m) die Gasse hinauf, statt nur um die Ecke herum. Dieselbe schmale Fuge wie beim Bordstein
|
||||
(_kerbless_zone, extra=0,2) schneidet nur den Gassenanteil heraus; die Kreuzung selbst bleibt
|
||||
Quelle, ein kurzer Rest an der Einmündungsecke bleibt und soll bleiben - das ist die Ecke,
|
||||
nicht die Gasse. 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 diesen zweiten Abzug nicht.
|
||||
"""
|
||||
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))
|
||||
ohne = _kerbless_zone(graph, 0.2)
|
||||
teile += [flaeche if ohne is None else flaeche.difference(ohne)
|
||||
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.
|
||||
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 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.
|
||||
Zweimal gebraucht, mit derselben Reichweite (Fixrunde 4): Der Bordstein hört hier auf
|
||||
(_kerb_lines()), und der Kreuzungsanteil der Gehwegquelle hört hier ebenfalls auf
|
||||
(_walk_source()) - eine Gasse hat laut SECTION weder Bordstein noch Gehweg, in beiden Fällen
|
||||
ist es dieselbe Fuge von 0,2 m über die eigene Fahrbahnbreite hinaus.
|
||||
"""
|
||||
zonen = [LineString(edge.points).buffer(SECTION[edge.klass].carriageway / 2 + 0.2,
|
||||
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]
|
||||
@@ -88,16 +96,17 @@ def _sidewalk(walk_source, carriage, corridor):
|
||||
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.
|
||||
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)
|
||||
if result.is_empty:
|
||||
return MultiPolygon()
|
||||
return result if result.geom_type == "MultiPolygon" else MultiPolygon([result])
|
||||
return to_multipolygon(result)
|
||||
|
||||
|
||||
def _kerb_lines(carriage, kerbless, bridges) -> list[LineString]:
|
||||
|
||||
Reference in new issue
Block a user