Der Bordstein ist keine Ausstattung wie ein Gehweg, sondern die Kante, an der der Asphalt endet — und die hat auch eine Hinterhofgasse. _kerb_lines() schneidet darum nur noch die Brücken aus dem Fahrbahnrand: Dort steckt die Fahrbahn im Deck, eine Kante stünde in der Luft. Gehweg und Markierung bleiben der Gasse weiterhin fern. Das Feld kerb im Querschnitt entfällt. Es steuerte zuletzt nur noch Gehweg und Markierung und war aus walk > 0 ableitbar; kerb_only heißt deshalb jetzt walk_only. Co-Authored-By: Claude Opus 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.walk] == ["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
|