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)) # Genau Grad 4, nicht bloß "mindestens 3": an jeder der beiden Kreuzungen treffen sich # zwei gespaltene Kanten (Querstraße und Jefferson Avenue), macht 2 + 2. assert result.graph.degree(knoten) == 4, 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)]