_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>
93 lines
4.8 KiB
Python
93 lines
4.8 KiB
Python
"""Querschnitte und Fahrbahnbänder."""
|
|
from __future__ import annotations
|
|
|
|
from shapely.geometry import GeometryCollection, LineString, box
|
|
|
|
from leonida.roads import CLASS_WIDTH, Edge, Graph, Node
|
|
from leonida.section import LANE, SECTION, ribbon, setback, to_multipolygon, trimmed_line
|
|
|
|
|
|
def strecke(klass: str, length: float = 100.0) -> Graph:
|
|
return Graph(nodes=[Node((0.0, 0.0)), Node((length, 0.0))],
|
|
edges=[Edge(0, 1, [(0.0, 0.0), (length, 0.0)], CLASS_WIDTH[klass], klass)])
|
|
|
|
|
|
def test_fahrbahn_und_zwei_gehwege_ergeben_den_korridor():
|
|
for name, section in SECTION.items():
|
|
assert section.carriageway + 2 * section.walk == section.corridor, name
|
|
assert section.corridor == CLASS_WIDTH[name], name
|
|
|
|
|
|
def test_spuren_und_parkspuren_fuellen_die_fahrbahn():
|
|
for name, section in SECTION.items():
|
|
if name == "gasse":
|
|
continue # eine Fahrgasse ohne Spureinteilung, breiter als eine Regelspur
|
|
assert section.lanes * LANE + 2 * section.parking == section.carriageway, name
|
|
|
|
|
|
def test_nur_die_gasse_hat_keinen_bordstein():
|
|
assert [name for name, s in SECTION.items() if not s.kerb] == ["gasse"]
|
|
|
|
|
|
def test_band_ist_so_breit_wie_die_fahrbahn():
|
|
band = ribbon(strecke("hauptstrasse"), 0)
|
|
x0, y0, x1, y1 = band.bounds
|
|
assert round(y1 - y0, 6) == SECTION["hauptstrasse"].carriageway
|
|
assert round(x1 - x0, 6) == 100.0, "freie Enden werden nicht zurückgeschnitten"
|
|
|
|
|
|
def test_an_einer_kreuzung_wird_zurueckgeschnitten():
|
|
graph = Graph(
|
|
nodes=[Node((0.0, 0.0)), Node((100.0, 0.0)), Node((100.0, 60.0)), Node((160.0, 0.0))],
|
|
edges=[Edge(0, 1, [(0.0, 0.0), (100.0, 0.0)], 12.0, "wohnstrasse"),
|
|
Edge(1, 2, [(100.0, 0.0), (100.0, 60.0)], 12.0, "wohnstrasse"),
|
|
Edge(1, 3, [(100.0, 0.0), (160.0, 0.0)], 12.0, "wohnstrasse")])
|
|
assert graph.degree(1) == 3
|
|
r = setback(graph, 1)
|
|
assert r == SECTION["wohnstrasse"].carriageway / 2 + 2.0
|
|
line = trimmed_line(graph, 0)
|
|
assert round(line.length, 6) == round(100.0 - r, 6)
|
|
assert line.coords[0] == (0.0, 0.0), "das freie Ende bleibt, nur das Kreuzungsende weicht"
|
|
|
|
|
|
def test_kante_ganz_innerhalb_der_kreuzung_liefert_kein_band():
|
|
# Zwei Kreuzungen (Grad 3) nur 3 m auseinander - genau der Fall, den CONTRACT_MAX in roads.py
|
|
# sonst zusammenzieht, hier aber synthetisch stehen gelassen, um trimmed_line() an diesem
|
|
# Rand zu prüfen. Der Auftrag hatte hier nur ein Kreuz aus vier Knoten mit je einem Arm an
|
|
# den Knoten 0 und 1 - das ergibt Grad 2, keine Kreuzung, und die Kante würde nicht
|
|
# zurückgeschnitten. Erst der zweite Arm an jedem der beiden Knoten macht sie zu echten
|
|
# Kreuzungen (Grad 3), deren Rückschnitt die 3 m kurze Verbindungskante ganz auffrisst.
|
|
graph = Graph(nodes=[Node((0.0, 0.0)), Node((3.0, 0.0)),
|
|
Node((0.0, 50.0)), Node((0.0, -50.0)),
|
|
Node((3.0, 50.0)), Node((3.0, -50.0))],
|
|
edges=[Edge(0, 1, [(0.0, 0.0), (3.0, 0.0)], 18.0, "hauptstrasse"),
|
|
Edge(0, 2, [(0.0, 0.0), (0.0, 50.0)], 18.0, "hauptstrasse"),
|
|
Edge(0, 3, [(0.0, 0.0), (0.0, -50.0)], 18.0, "hauptstrasse"),
|
|
Edge(1, 4, [(3.0, 0.0), (3.0, 50.0)], 18.0, "hauptstrasse"),
|
|
Edge(1, 5, [(3.0, 0.0), (3.0, -50.0)], 18.0, "hauptstrasse")])
|
|
assert graph.degree(0) == 3 and graph.degree(1) == 3
|
|
assert trimmed_line(graph, 0) is None
|
|
assert ribbon(graph, 0) is None
|
|
|
|
|
|
def test_geometriesammlung_wird_zu_reinem_multipolygon():
|
|
# Fixrunde 4 zu Task 7: intersection()/difference() liefern nicht immer sauber ein
|
|
# (Multi)Polygon zurück - an einer Berührstelle kann eine GeometryCollection mit einer
|
|
# zusätzlichen entarteten Linie oder einem Punkt herauskommen (am Gehweg gemeldet).
|
|
# MultiPolygon(geoms) nimmt aber nur Polygone entgegen und wirft eine TypeError, sobald ein
|
|
# Bestandteil davon abweicht - carriageway_area() und _sidewalk() (street.py) stürzten daran
|
|
# ab. Hier direkt erzwungen (zwei Polygone plus eine entartete Nulllinie), weil sich die
|
|
# Geometriesammlung über echte Fahrbahnrechnungen trotz mehrerer Versuche nicht zuverlässig
|
|
# nachbauen ließ - GEOS bereinigt Berührstellen bei Polygon-Operationen erstaunlich robust.
|
|
entartet = GeometryCollection([box(0, 0, 10, 10), box(20, 0, 30, 10), LineString([(15, 5), (15, 5)])])
|
|
assert entartet.geom_type == "GeometryCollection"
|
|
ergebnis = to_multipolygon(entartet)
|
|
assert ergebnis.geom_type == "MultiPolygon"
|
|
assert len(ergebnis.geoms) == 2, "nur die zwei echten Flächen, die Nulllinie fällt weg"
|
|
assert round(ergebnis.area) == 200
|
|
|
|
|
|
def test_leere_geometrie_wird_zu_leerem_multipolygon():
|
|
assert to_multipolygon(GeometryCollection()).geom_type == "MultiPolygon"
|
|
assert to_multipolygon(GeometryCollection()).is_empty
|