from pathlib import Path from types import SimpleNamespace import pytest from shapely.geometry import LineString, Point, Polygon, box from leonida.heights import PALM_CLEARANCE from leonida.park import ParkError, ParkLayout, World, apply_flamingo_park, apply_park, check_corners, load_park DATA = Path(__file__).resolve().parents[1] / "data" QUAD = box(0, 0, 100, 200) SERVICE = box(60, 0, 66, 30) def layout(): return ParkLayout(lawn=QUAD, surfaces=[(box(10, 10, 40, 40), "field")], paths=[box(0, 148, 100, 152)], service=[SERVICE], buildings=[(box(70, 60, 80, 70), 5.0)], keepouts=[box(60, 160, 90, 190)]) def world(): return World( land=[box(-100, -100, 200, 300)], surfaces=[(box(-30, -30, 130, 230), "urban")], roads=[(box(-50, 95, 150, 105), False), (box(-20, -20, 0, 220), False)], centerlines=[LineString([(-50, 100), (150, 100)]), LineString([(-5, 150), (10, 150)])], buildings=[(box(20, 60, 30, 70), {"height": 9.0, "source": "district"}, False), # ganz im Park (box(-6, 80, 4, 90), {"height": 9.0, "source": "district"}, False), # 40 % im Park (box(-4, 120, 6, 130), {"height": 9.0, "source": "district"}, False)], # 60 % im Park palms=[(50.0, 50.0), (-10.0, 50.0)], ) def test_old_roads_and_centerlines_end_at_the_park_edge(): result = apply_park(world(), layout(), seed=1) assert all(poly.intersection(QUAD).area < 1e-6 for poly, _ in result.roads if not poly.equals(SERVICE)) assert any(poly.contains(Point(-30, 100)) for poly, _ in result.roads) assert any(poly.contains(Point(120, 100)) for poly, _ in result.roads) assert sorted(line.bounds for line in result.centerlines) == [(-50.0, 100.0, 0.0, 100.0), (100.0, 100.0, 150.0, 100.0)] def test_old_surfaces_buildings_and_palms_in_the_park_go(): result = apply_park(world(), layout(), seed=1) assert all(poly.intersection(QUAD).area < 1e-6 for poly, kind in result.surfaces if kind == "urban") assert sorted((b[0].bounds, b[1]["source"]) for b in result.buildings) == [ ((-6.0, 80.0, 4.0, 90.0), "district"), ((70.0, 60.0, 80.0, 70.0), "osm")] assert (-10.0, 50.0) in result.palms and (50.0, 50.0) not in result.palms def test_layout_goes_in_with_the_lawn_before_the_fields(): result = apply_park(world(), layout(), seed=1) kinds = [kind for _, kind in result.surfaces] assert kinds.index("park") < kinds.index("field") assert any(kind == "park" and poly.equals(QUAD) for poly, kind in result.surfaces) assert result.paths == layout().paths assert any(poly.equals(SERVICE) and not bridge for poly, bridge in result.roads) def test_new_palms_stand_on_lawn_away_from_roads_paths_buildings_and_keepouts(): result = apply_park(world(), layout(), seed=1) new = [Point(p) for p in result.palms if p != (-10.0, 50.0)] assert len(new) > 5 for palm in new: assert QUAD.contains(palm) and not box(10, 10, 40, 40).contains(palm) assert palm.distance(box(0, 148, 100, 152)) > PALM_CLEARANCE assert palm.distance(box(60, 160, 90, 190)) > PALM_CLEARANCE assert all(palm.distance(poly) > PALM_CLEARANCE for poly, _ in result.roads) assert all(palm.distance(b[0]) > PALM_CLEARANCE for b in result.buildings) def test_same_seed_gives_the_same_palms(): assert apply_park(world(), layout(), seed=7).palms == apply_park(world(), layout(), seed=7).palms def corners(**moved): game = {"sw": (0.0, 0.0), "se": (100.0, 0.0), "ne": (100.0, 200.0), "nw": (0.0, 200.0)} return SimpleNamespace(game={**game, **moved}) def test_corners_must_lie_at_a_road_when_it_pulls_back(): """Die eine Richtung: die Straße bleibt, wo sie ist, die Ecke steht daneben.""" roads = [(box(-20, -20, 0, 220), False), (box(100, -20, 120, 220), False)] check_corners(corners(), roads) with pytest.raises(ParkError, match="Spielecke nw"): check_corners(corners(nw=(30.0, 200.0)), roads) def test_a_road_that_grew_inward_over_the_corner_is_caught(): """Die andere Richtung, und der Grund für die Messung gegen den Rand statt gegen die Fläche. Die Westkante der linken Straße ist von x = 0 auf x = 6 nach innen gewandert; die SW- und die NW-Ecke liegen jetzt 6 m tief in der Fahrbahn. apply_park() schnitte die verbreiterte Straße wortlos wieder auf das alte Viereck zurück — deshalb muss die Prüfung hier abbrechen. Mit dem Flächenabstand tut sie das nicht: er ist für eine Ecke mitten in der Straße 0 (die Zusicherung unten misst genau das), und die alte Bedingung poly.distance(corner) <= 2.0 ließ den Fall durchlaufen. """ grown = [(box(-20, -20, 6, 220), False), (box(100, -20, 120, 220), False)] assert min(poly.distance(Point(0, 0)) for poly, _ in grown) == 0 with pytest.raises(ParkError, match="Spielecke sw"): check_corners(corners(), grown) def test_flamingo_park_goes_into_a_world_with_roads_around_the_park(): config = load_park(DATA / "flamingo-park.toml") park = Polygon([config.game[key] for key in ("sw", "se", "ne", "nw")]) ring = park.buffer(15, join_style="mitre").difference(park) start = World(land=[park.buffer(400)], surfaces=[], roads=[(ring, False)], centerlines=[], buildings=[], palms=[]) result, messages = apply_flamingo_park(start, DATA / "flamingo-park.toml", seed=20260912) assert {"park", "field", "court", "pool", "parking", "sand"} <= {kind for _, kind in result.surfaces} assert len([b for b in result.buildings if b[1]["source"] == "osm"]) == 11 assert result.paths and result.palms assert LineString([config.street.start, config.street.end]) in result.centerlines # Bekannter Zustand: die Zufahrt zum Parkplatz quert die Sperrfläche der Tribüne und wird dort # ausgeschnitten (Task 5). Jede weitere oder andere Warnung ist ein echter Befund. assert [m for m in messages if m.startswith("Warnung")] == ["Warnung: Zufahrt schneidet die Sperrfläche tribuene"] # Die Flächenzahl gehört mit in den Vergleich: _surface_kind() kennt nur die Sportarten aus # FIELD_SPORTS und COURT_SPORTS und lässt jede andere wortlos weg. Taggt OpenStreetMap beim # nächsten Abruf ein Feld um — aus "tennis" wird "pickleball", aus "soccer" ein "multi" —, # verschwindet es aus dem Park, ohne dass eine Warnung oder ein anderer Test das bemerkt; # hier sinkt dann die 14, und der Abzug wird geprüft statt der Park stillschweigend kleiner. assert [m for m in messages if m.startswith("Flamingo Park:")] == [ "Flamingo Park: 2 Wegflächen, 14 Flächen, 11 Gebäude, Jefferson Avenue 12 m breit"]