feat(extract): Mulde, Bordsteinlinien, Gehweg und Restflächen

Ein Prüfkörper korrigiert: Der Splitter in
test_kleine_reststuecke_fallen_weg_grosse_bleiben lag direkt an derselben
Korridorkante wie die Ausbuchtung und verschmolz dort schon bei unary_union()
mit ihr, bevor der LOT_MIN-Filter überhaupt etwas zu tun bekam. Mit einer
kleinen Lücke zum Rand bleibt er ein eigenständiges 2-m²-Polygon und fällt
durch den Filter - der Test prüft jetzt tatsächlich das Wegfallen kleiner
Reststücke, nicht nur, dass große bleiben.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-17 04:09:43 +02:00
1 parent ee07a6bf2a
commit 45a1a62103
2 files changed
+151

No files matched your search

+82
View File
@@ -0,0 +1,82 @@
"""Die Straße als Mulde: Loch in der Bodenplatte, Fahrbahn darunter, Bordstein dazwischen.
Warum die Fahrbahn nach unten und nicht der Gehweg nach oben (Entwurf, Abschnitt "Die Mulde"):
Läge der Gehweg 15 cm hoch, hätte er überall dort eine Kante ins Nichts, wo kein Haus
anschließt - an jedem Park, jeder Baulücke, jedem Strandzugang. So bleibt die ganze Welt auf
y = 0, und der Bordstein ist genau das, was er in Wirklichkeit ist: die Wand zwischen Gehweg
und Fahrbahn.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from shapely.geometry import LineString, Polygon
from shapely.ops import unary_union
from .roads import Graph
from .section import SECTION, WALK_MAX, carriageway_area, ribbon
# Die Tiefe steht hier als dokumentierter Wert; gebaut wird sie im Browser (geometry.js).
DEPTH = 0.15
# Kürzere Bordsteinstücke sind Rechenreste an Kreuzungsecken, keine Kante, die jemand sieht.
KERB_MIN = 2.0
# Ab hier ist ein Reststück eine Fläche (Parkplatz, Vorplatz, Rampe) und keine Zacke.
LOT_MIN = 200.0
@dataclass
class Street:
carriageway: object
kerb: list = field(default_factory=list)
sidewalk: object = None
lots: list = field(default_factory=list)
def build_street(graph: Graph, corridor, bridges) -> Street:
"""Fahrbahn, Bordsteinlinien, Gehweg und Restflächen aus Graph und gemessenem Korridor."""
carriage = carriageway_area(graph)
return Street(
carriageway=carriage,
kerb=_kerb_lines(graph, carriage, bridges),
sidewalk=_sidewalk(carriage, corridor),
lots=_lots(corridor, carriage),
)
def _sidewalk(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. Die Gasse füllt ihren Korridor ganz aus und bekommt
dadurch von selbst keinen Gehweg.
"""
return carriage.buffer(WALK_MAX, join_style="round").intersection(corridor).difference(carriage)
def _kerb_lines(graph: Graph, carriage, bridges) -> list[LineString]:
"""Die Fahrbahnkante, ohne die Stücke an Gassen und auf Brücken."""
ohne = [band.buffer(0.2) for k, edge in enumerate(graph.edges)
if not SECTION[edge.klass].kerb and (band := ribbon(graph, k)) is not None]
ohne += [b.buffer(0.2) for b in bridges]
rand = carriage.boundary
if ohne:
rand = rand.difference(unary_union(ohne))
teile = getattr(rand, "geoms", [rand])
return [g for g in teile if g.geom_type == "LineString" and g.length >= KERB_MIN]
def _lots(corridor, carriage) -> list[Polygon]:
rest = corridor.difference(carriage)
return [g for g in getattr(rest, "geoms", [rest])
if g.geom_type == "Polygon" and g.area >= LOT_MIN]
def punch(polygons, carriage) -> list[Polygon]:
"""Die Fahrbahn aus Bodenplatte oder Belag herausschneiden."""
out = []
for poly in polygons:
rest = poly.difference(carriage)
out += [g for g in getattr(rest, "geoms", [rest])
if g.geom_type == "Polygon" and g.area > 0.5]
return out
+69
View File
@@ -0,0 +1,69 @@
"""Die Mulde: Löcher in der Bodenplatte, Bordsteinlinien, Gehwegflächen, Restflächen."""
from __future__ import annotations
from shapely.geometry import Point, box
from shapely.ops import unary_union
from leonida.roads import Edge, Graph, Node
from leonida.section import SECTION
from leonida.street import LOT_MIN, build_street, punch
def strasse(klass: str = "wohnstrasse") -> Graph:
return Graph(nodes=[Node((0.0, 0.0)), Node((100.0, 0.0))],
edges=[Edge(0, 1, [(0.0, 0.0), (100.0, 0.0)], SECTION[klass].corridor, klass)])
def korridor(klass: str = "wohnstrasse"):
halb = SECTION[klass].corridor / 2
return box(0, -halb, 100, halb)
def test_gehweg_fuellt_genau_den_rest_des_korridors():
street = build_street(strasse(), korridor(), bridges=[])
section = SECTION["wohnstrasse"]
# 100 m lang, zwei Streifen von 2,5 m. Die Enden sind offen, deshalb genau die Fläche.
assert round(street.sidewalk.area) == round(100 * 2 * section.walk)
assert street.sidewalk.intersection(street.carriageway).area < 0.01
def test_gasse_bekommt_weder_gehweg_noch_bordstein():
street = build_street(strasse("gasse"), korridor("gasse"), bridges=[])
assert street.sidewalk.area < 0.01
assert street.kerb == []
def test_bordstein_laeuft_entlang_der_fahrbahnkante():
street = build_street(strasse(), korridor(), bridges=[])
gesamt = sum(line.length for line in street.kerb)
# Zwei Längsseiten von 100 m; die kurzen Stirnseiten sind mit 7 m kaum der Rede wert.
assert 190 < gesamt < 220
def test_bruecken_bekommen_keinen_bordstein():
bridge = box(40, -10, 60, 10)
street = build_street(strasse(), korridor(), bridges=[bridge])
for line in street.kerb:
assert not bridge.buffer(-0.5).intersects(line), "über der Brücke bleibt die Kante frei"
def test_loch_hat_genau_die_form_der_fahrbahn():
platte = box(-50, -50, 150, 50)
street = build_street(strasse(), korridor(), bridges=[])
rest = punch([platte], street.carriageway)
assert len(rest) == 1
assert not rest[0].contains(Point(50, 0)), "die Fahrbahnmitte ist ausgeschnitten"
assert rest[0].contains(Point(50, 20)), "daneben bleibt die Platte"
assert abs(unary_union(rest).area + street.carriageway.area - platte.area) < 1.0
def test_kleine_reststuecke_fallen_weg_grosse_bleiben():
# Ein Korridor mit einer 30 x 30 m großen Ausbuchtung (ein Parkplatz) und einem Splitter.
# Der Splitter braucht eine Lücke zum Korridorrand: Läge er wie die Ausbuchtung direkt an
# y = 6 an, verschmölze schon unary_union() ihn mit ihr, weil beide dieselbe durchgehende
# Kante berühren - dann bliebe für LOT_MIN unten gar nichts mehr zu filtern übrig.
weit = unary_union([korridor(), box(40, 6, 70, 36), box(10, 8, 12, 9)])
street = build_street(strasse(), weit, bridges=[])
assert all(lot.area >= LOT_MIN for lot in street.lots)
assert any(lot.area > 500 for lot in street.lots), "der Parkplatz bleibt"
assert len(street.lots) == 2, "der 2 m² große Splitter ist tatsächlich herausgefallen"