Fixrunde 1 zu Task 11, zwei von der Koordination gefundene und am echten Netz nachgemessene Fehler, beide durch die Senkung von MIN_CENTERLINE (15 m) auf MIN_EDGE (5 m) erst möglich: 1. cut_edges() hängte beide Enden eines Schnittstücks über attach() an - dessen 8-m-Umkreis fand bei einem Reststück zwischen 5 und 8 m Länge für beide Enden denselben ursprünglichen Knoten und machte aus dem geraden Stummel eine Schleife. section.py hält jeden Knoten ab Grad 3 für eine Kreuzung; an einem Sackgassenende wären dort Kreuzungsfläche, Zebrastreifen und Haltelinie entstanden. Die neue _cut_end() sucht nicht mehr im Umkreis, sondern nur noch nach exakter Übereinstimmung mit einem der beiden ursprünglichen Kantenenden. 2. apply_street() hängte die Jefferson Avenue nur mit einem attach()-Paar an ihre beiden Enden. Am echten Netz liegt ihr Nordende 23,2 m von jedem Knoten entfernt, aber die Straße kreuzt auf dem Weg zu ihrem Südende (6,6 m von Knoten 561) die 11th Street mittendrin, ohne dass dort ein Knoten entsteht - zwei Fahrbahnen hätten sich unverbunden überlagert. Die neue insert_edge() bricht jede gekreuzte Kante am Kreuzungspunkt auf und teilt sich dort einen Knoten mit der neuen Straße (mit attach() für die Knotenwahl, wie gehabt); apply_street() benutzt sie jetzt statt des attach()-Paars. Am echten Netz (1.537 Knoten, 1.790 Kanten vor Flamingo Park) bestätigt: Jefferson Avenue bricht jetzt korrekt an der 11th Street auf (neuer Knoten, Grad 4) und hängt am Südende weiterhin an Knoten 561 (Grad 3 -> 4); die Meldezeile und die Sperrflächen-Warnung blieben wörtlich. Die Schleifenzahl im Gesamtnetz sank von 196 auf 193 (die Park-Anlage schneidet alte Schleifen im Parkgebiet weg), keine der verbliebenen 193 entstand neu - jede hat ein Vor-Park-Gegenstück im 5-m-Umkreis. Keine Kante unter 1 m, weder vorher noch nachher. Neue Tests: test_roads_cut.py (Schleifenregression), test_roads_insert.py (drei Szenarien: Kreuzung in der Mitte, Anhängen an einen Grad-3-Knoten, Kreuzungspunkt nahe einem vorhandenen Knoten), test_park_street.py (Knotengrad an beiden Kreuzungen der Park-Zufahrtsstraße). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
88 lines
4.9 KiB
Python
88 lines
4.9 KiB
Python
from shapely.geometry import LineString, Point, box
|
||
from shapely.ops import unary_union
|
||
|
||
from leonida.park import Street, World, apply_street
|
||
from leonida.roads import MIN_EDGE, Edge, Graph, Node
|
||
|
||
STREET = Street(start=(150.0, 660.0), end=(150.0, 320.0), width=12.0,
|
||
remove_buildings=((155.0, 615.0),),
|
||
alley_cut=((90.0, 639.0), (110.0, 639.0), (110.0, 341.0), (90.0, 341.0)))
|
||
|
||
|
||
def world():
|
||
"""Querstraßen bei y 640–660 und 320–340, dazwischen eine Gasse bei x 95–105."""
|
||
removed = box(140, 600, 170, 630)
|
||
return World(
|
||
land=[box(-100, 200, 300, 800)],
|
||
surfaces=[(box(0, 340, 95, 640), "urban"), (box(105, 340, 200, 640).difference(removed), "urban")],
|
||
roads=[(unary_union([box(0, 640, 200, 660), box(0, 320, 200, 340), box(95, 340, 105, 640)]), False)],
|
||
graph=Graph(nodes=[Node((0.0, 650.0)), Node((200.0, 650.0)), Node((0.0, 330.0)), Node((200.0, 330.0)),
|
||
Node((100.0, 650.0)), Node((100.0, 330.0))],
|
||
edges=[Edge(0, 1, [(0.0, 650.0), (200.0, 650.0)], 12.0, "wohnstrasse"),
|
||
Edge(2, 3, [(0.0, 330.0), (200.0, 330.0)], 12.0, "wohnstrasse"),
|
||
Edge(4, 5, [(100.0, 650.0), (100.0, 330.0)], 6.0, "gasse")]),
|
||
buildings=[(removed, {"height": 20.9, "source": "district"}, False),
|
||
(box(20, 400, 60, 450), {"height": 10.8, "source": "district"}, False),
|
||
(box(84, 450, 93, 470), {"height": 8.0, "source": "district"}, False)],
|
||
palms=[(150.0, 500.0), (60.0, 500.0), (140.0, 500.0)],
|
||
)
|
||
|
||
|
||
def on_road(result, x, y):
|
||
return any(poly.contains(Point(x, y)) for poly, _ in result.roads)
|
||
|
||
|
||
def test_buildings_under_the_street_go_and_the_others_stay():
|
||
result = apply_street(world(), STREET)
|
||
assert [b[0].bounds for b in result.buildings] == [(20.0, 400.0, 60.0, 450.0), (84.0, 450.0, 93.0, 470.0)]
|
||
|
||
|
||
def test_alley_is_cut_and_the_straight_street_joins_both_cross_streets():
|
||
result = apply_street(world(), STREET)
|
||
assert not on_road(result, 100, 500)
|
||
assert on_road(result, 150, 500) and not on_road(result, 143, 500)
|
||
assert on_road(result, 100, 650) and on_road(result, 100, 330)
|
||
assert on_road(result, 150, 650) and on_road(result, 150, 330)
|
||
|
||
|
||
def test_alley_centerline_shrinks_to_its_stubs_and_the_street_gets_its_own():
|
||
"""Früher (MIN_CENTERLINE=15) verschwand die geschnittene Gasse ganz, weil ihre beiden Enden
|
||
nur 11 m lang sind. cut_edges() kennt nur noch MIN_EDGE (5 m): der alte Grund - Laternen
|
||
wählten ihre Straßenseite über den Listenindex und liefen an der Kreuzung sonst ins Leere -
|
||
entfällt, sobald eine spätere Aufgabe die Seite aus der Geometrie bestimmt statt aus der
|
||
Liste. Die beiden 11-m-Enden bleiben deshalb jetzt als eigene, kurze Kanten stehen.
|
||
|
||
Die neue Straße kreuzt dabei selbst beide Querstraßen bei y=650 und y=330 (insert_edge(),
|
||
Fixrunde 1): an beiden Stellen muss ein echter Knoten mit Grad >= 3 entstehen, sonst läge die
|
||
neue Fahrbahn nur unverbunden über der alten - genau der Fehler, den ein reines attach() an
|
||
den beiden Enden nicht gesehen hätte, weil die Kreuzung nicht an einem Ende liegt, sondern
|
||
mittendrin. Die Straße selbst zerfällt deshalb jetzt in drei Stücke statt eines.
|
||
"""
|
||
result = apply_street(world(), STREET)
|
||
lines = [LineString(e.points) for e in result.graph.edges]
|
||
assert sorted(round(line.length) for line in lines if line.length < 15) == [10, 10, 11, 11]
|
||
assert all(line.distance(Point(100, 500)) > 40 for line in lines)
|
||
assert all(line.length >= MIN_EDGE for line in lines)
|
||
|
||
for x, y in ((150.0, 650.0), (150.0, 330.0)):
|
||
knoten = next(k for k, n in enumerate(result.graph.nodes) if n.point == (x, y))
|
||
assert result.graph.degree(knoten) >= 3, f"Kreuzung bei ({x:g}, {y:g}) ohne echten Knoten"
|
||
|
||
assert any(e.points == [(150.0, 660.0), (150.0, 650.0)] for e in result.graph.edges)
|
||
assert any(e.points == [(150.0, 650.0), (150.0, 330.0)] for e in result.graph.edges)
|
||
assert any(e.points == [(150.0, 330.0), (150.0, 320.0)] for e in result.graph.edges)
|
||
|
||
|
||
def test_freed_ground_becomes_urban_but_not_under_the_street_buildings_or_old_urban():
|
||
result = apply_street(world(), STREET)
|
||
new = [poly for poly, kind in result.surfaces[2:] if kind == "urban"]
|
||
assert any(poly.contains(Point(100, 500)) for poly in new) # alte Gasse
|
||
assert any(poly.contains(Point(165, 615)) for poly in new) # alter Grundriss neben der Straße
|
||
assert not any(poly.contains(Point(150, 615)) for poly in new) # unter der neuen Straße
|
||
assert not any(poly.contains(Point(91, 460)) for poly in new) # stehendes Gebäude
|
||
assert not any(poly.contains(Point(92, 500)) for poly in new) # war schon Stadtfläche
|
||
|
||
|
||
def test_palms_on_the_new_street_go():
|
||
assert apply_street(world(), STREET).palms == [(60.0, 500.0), (140.0, 500.0)]
|