from types import SimpleNamespace import pytest from shapely.geometry import Point from leonida.park import (M_PER_DEG_LAT, M_PER_DEG_LON, REF_LAT, REF_LON, Keepout, ParkFit, build_layout, building_height, keepout_polygon) def latlon(x, y): return REF_LAT + y / M_PER_DEG_LAT, REF_LON + x / M_PER_DEG_LON def config(keepouts=()): """Echter Park 100 × 200 m, Spielpark 200 × 400 m ab (1000 | 1000), Anlage 50 m vom Westrand: Welt-x = 1050 + x, Welt-y = 1000 + 2 · y.""" return SimpleNamespace( real={"sw": latlon(0, 0), "se": latlon(100, 0), "nw": latlon(0, 200)}, game={"sw": (1000.0, 1000.0), "se": (1200.0, 1000.0), "ne": (1200.0, 1400.0), "nw": (1000.0, 1400.0)}, scale_u=1.0, offset_u=50.0, keepouts=tuple(keepouts), main_width=6.0, path_width=3.0, service_width=6.0, default_height=4.0, ) def way(tags, *points, id=1): return {"id": id, "tags": tags, "geometry": [list(latlon(x, y)) for x, y in points]} def square(tags, x, y, size=10, id=1): return way(tags, (x, y), (x + size, y), (x + size, y + size), (x, y + size), (x, y), id=id) def layout(elements, keepouts=()): cfg = config(keepouts) return build_layout(elements, cfg, ParkFit(cfg)) def test_footway_is_three_metres_wide_and_a_named_main_path_six(): (plain,) = layout([way({"highway": "footway"}, (20, 100), (80, 100))]).paths assert plain.contains(Point(1100, 1201.4)) and not plain.contains(Point(1100, 1201.6)) (main,) = layout([way({"highway": "footway", "name": "Flamingo Park Path (13th St)"}, (20, 100), (80, 100))]).paths assert main.contains(Point(1100, 1202.9)) and not main.contains(Point(1100, 1203.1)) def test_crossing_paths_become_one_surface(): result = layout([way({"highway": "footway"}, (20, 100), (80, 100), id=1), way({"highway": "path"}, (50, 60), (50, 140), id=2)]) assert len(result.paths) == 1 def test_path_is_cut_at_the_real_park_edge(): (path,) = layout([way({"highway": "footway"}, (50, 150), (50, 260))]).paths assert path.bounds[3] <= 1400 + 1.5 + 1e-6 def test_path_ending_near_the_west_edge_runs_on_to_the_road(): (path,) = layout([way({"highway": "footway"}, (40, 100), (3, 100))]).paths assert path.contains(Point(1001, 1200)) def test_path_along_the_edge_is_not_stretched(): (path,) = layout([way({"highway": "footway"}, (3, 50), (3, 150))]).paths assert not path.contains(Point(1010, 1200)) assert path.bounds[1] == pytest.approx(1100 - 1.5) def test_path_ending_far_from_the_edge_stays_as_it_is(): (path,) = layout([way({"highway": "footway"}, (40, 100), (20, 100))]).paths assert path.bounds[0] == pytest.approx(1070 - 1.5) def test_surfaces_are_sorted_by_kind_and_dog_parks_stay_lawn(): elements = [ square({"leisure": "playground"}, 10, 10, id=1), square({"leisure": "pitch", "sport": "tennis"}, 30, 10, id=2), square({"leisure": "pitch", "sport": "baseball"}, 50, 10, id=3), square({"amenity": "parking"}, 70, 10, id=4), square({"leisure": "swimming_pool"}, 10, 40, id=5), square({"leisure": "sports_centre"}, 30, 40, id=6), square({"leisure": "pitch", "sport": "soccer;american_football"}, 50, 40, id=7), square({"leisure": "dog_park"}, 70, 40, id=8), ] assert [kind for _, kind in layout(elements).surfaces] == ["field", "field", "court", "court", "pool", "parking", "sand"] def test_features_centred_outside_the_real_park_are_left_out(): assert layout([square({"leisure": "pitch", "sport": "soccer"}, 40, 195, size=20)]).surfaces == [] def test_lawn_is_the_whole_game_park(): assert layout([]).lawn.area == pytest.approx(200 * 400) def test_buildings_take_their_height_or_the_default_and_roofs_without_walls_are_dropped(): result = layout([ square({"building": "yes", "height": "8.5"}, 10, 10, id=1), square({"building": "yes"}, 30, 10, id=2), square({"building": "yes", "height": "0.0"}, 50, 10, id=3), square({"building": "yes", "leisure": "sports_centre"}, 70, 10, id=4), ]) assert sorted(height for _, height in result.buildings) == [4.0, 4.0, 8.5] assert result.surfaces == [] def test_building_height_reads_plain_numbers_and_units(): assert building_height({"height": "10.4"}, 4.0) == 10.4 assert building_height({"height": "12 m"}, 4.0) == 12.0 assert building_height({"height": "hoch"}, 4.0) == 4.0 assert building_height({}, 4.0) == 4.0 assert building_height({"height": "0"}, 4.0) is None def test_keepout_polygon_turns_like_places_js(): plain = keepout_polygon(Keepout("ort", (100.0, 200.0), 0.0, (-10.0, 20.0, -5.0, 8.0))) assert plain.bounds == pytest.approx((90.0, 192.0, 120.0, 205.0)) turned = keepout_polygon(Keepout("ort", (0.0, 0.0), 90.0, (0.0, 10.0, -1.0, 1.0))) assert turned.bounds == pytest.approx((-1.0, 0.0, 1.0, 10.0)) # +x zeigt nach 90° nach Norden def test_keepouts_cut_fields_and_paths_keep_buildings_out_and_warn(): keepout = Keepout("ort", (1100.0, 1100.0), 0.0, (-10.0, 10.0, -10.0, 10.0)) # Welt x 1090–1110, y 1090–1110 result = layout([ square({"leisure": "pitch", "sport": "baseball"}, 40, 40, size=20, id=1), # Welt x 1090–1110, y 1080–1120 way({"highway": "footway"}, (50, 30), (50, 70), id=2), # Welt x 1100, y 1060–1140 square({"building": "yes"}, 55, 48, size=4, id=3), # Welt x 1105–1109, y 1096–1104 ], keepouts=[keepout]) hole = keepout_polygon(keepout) assert all(poly.intersection(hole).area < 1e-6 for poly, _ in result.surfaces) assert sum(poly.area for poly, _ in result.surfaces) == pytest.approx(20 * 40 - 20 * 20) assert all(poly.intersection(hole).area < 1e-6 for poly in result.paths) assert result.buildings == [] assert result.warnings == ["Parkweg schneidet die Sperrfläche ort"] def test_service_ways_become_six_metre_service_surfaces(): result = layout([way({"highway": "service"}, (20, 20), (80, 20))]) (service,) = result.service assert service.contains(Point(1100, 1042.9)) and not service.contains(Point(1100, 1043.1)) assert result.paths == [] def test_keepouts_cut_service_surfaces_and_warn(): keepout = Keepout("ort", (1100.0, 1100.0), 0.0, (-10.0, 10.0, -10.0, 10.0)) # Welt x 1090–1110, y 1090–1110 result = layout([way({"highway": "service"}, (30, 50), (70, 50))], # Welt x 1080–1120, y 1100 keepouts=[keepout]) hole = keepout_polygon(keepout) assert result.service and all(poly.intersection(hole).area < 1e-6 for poly in result.service) assert result.warnings == ["Zufahrt schneidet die Sperrfläche ort"]