Files
gta6/tools/extract/tests/test_park_layout.py
T
knsoandClaude Opus 5 3c74202b5e fix(extract): Chunk-Ausgabe lauf-stabil, Eckenprüfung in beide Richtungen
emit.py normalisiert jede ausgegebene Fläche und sortiert die Listen nach der
Ausgabestruktur selbst. unary_union() in apply_street() gab das kartenweite
Straßenpolygon mit anderem Ringstartpunkt und an anderer Listenstelle zurück und
änderte damit 88 Chunkdateien, in denen sich geometrisch nichts bewegt hatte
(symmetrische Differenz 0,00 m²). Belagsarten und Mittellinien behalten ihre
Reihenfolge: overview.py malt die Beläge in Listenreihenfolge übereinander, und
vergeLampPoints() hängt an Reihenfolge und Laufrichtung der Mittellinien —
Sortieren verschöbe dort 1.848 von 3.921 Laternen auf die andere Straßenseite.

check_corners() misst gegen den Straßenrand statt gegen die Straßenfläche. Alle
vier Ecken liegen in der Fahrbahn, ihr Flächenabstand ist 0,000 m; die alte
Prüfung schlug deshalb nur an, wenn eine Straße sich zurückzieht. Eine nach
innen verbreiterte Straße hätte apply_park() still auf das veraltete Viereck
zurückgeschnitten.

Die Parkwege werden am Parkviereck abgeschnitten — die runden Kappen von
LineString.buffer() lagen mit 95,96 m² darüber hinaus, rund 41 m² davon sichtbar
als Betonzungen auf den umgebenden Belägen. load_elements() prüft den OSM-Abzug
so streng wie die Parkdatei, mit Dateiname statt nacktem KeyError.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 13:43:01 +02:00

163 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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_paths_stop_at_the_park_edge_instead_of_bulging_over_it():
"""LineString.buffer() setzt an jedes Wegende eine runde Kappe von einer halben Wegbreite.
Der letzte Stützpunkt liegt schon auf der Parkkante (_extend zielt dorthin), die Kappe ragte
also bis zu 1,5 m (Fußweg) bzw. 3 m (Hauptweg) darüber hinaus und lag als Betonzunge auf dem
Belag daneben — an den echten Daten 95,96 m² außerhalb des Vierecks.
"""
cfg = config()
quad = ParkFit(cfg).game_quad
(path,) = build_layout([way({"highway": "footway"}, (50, 150), (50, 260))], cfg, ParkFit(cfg)).paths
assert path.difference(quad).area == 0
assert path.bounds[3] == pytest.approx(1400.0)
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"]