From 442f5b2ada18a91b9154cb130f5bd4123762ff98 Mon Sep 17 00:00:00 2001 From: Kenso Grimm Date: Wed, 16 Sep 2026 08:00:07 +0200 Subject: [PATCH] feat(extract): Flamingo Park in run.py, Parkwege in der Chunk-Ausgabe Co-Authored-By: Claude Sonnet 5 --- tools/extract/leonida/emit.py | 4 +++- tools/extract/leonida/park.py | 14 ++++++++++++++ tools/extract/run.py | 26 ++++++++++++++++++-------- tools/extract/tests/test_emit.py | 11 ++++++++++- tools/extract/tests/test_park_apply.py | 21 +++++++++++++++++++-- 5 files changed, 64 insertions(+), 12 deletions(-) diff --git a/tools/extract/leonida/emit.py b/tools/extract/leonida/emit.py index d3fb6b5..d20fe6c 100644 --- a/tools/extract/leonida/emit.py +++ b/tools/extract/leonida/emit.py @@ -39,7 +39,7 @@ def _rings(poly: Polygon, i: int, j: int) -> list[list[list[float]]]: for ring in (poly.exterior, *poly.interiors)] -def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms) -> dict: +def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms, paths=()) -> dict: ox = SLICE["x0"] + i * CHUNK_SIZE oy = SLICE["y1"] - j * CHUNK_SIZE return { @@ -52,6 +52,8 @@ def chunk_payload(i, j, *, land, surfaces, roads, centerlines, buildings, palms) "buildings": [{"poly": _rings(p, i, j), "height": h["height"], "source": h["source"], "speculative": s} for p, h, s in buildings], "palms": [[round(x - ox, 2), round(-y + oy, 2)] for x, y in palms], + # Parkwege (Flamingo Park, docs/superpowers/specs/2026-09-15-flamingo-park-design.md), aufgebaut wie roads. + "paths": [{"poly": _rings(p, i, j)} for p in paths], } diff --git a/tools/extract/leonida/park.py b/tools/extract/leonida/park.py index d2375c8..d4e927d 100644 --- a/tools/extract/leonida/park.py +++ b/tools/extract/leonida/park.py @@ -522,3 +522,17 @@ def apply_park(world: World, layout: ParkLayout, seed: int) -> World: palms += palm_points([], _polygons(lawn), [], [] if blocked.is_empty else [blocked], [], [], seed) return replace(world, roads=roads, centerlines=centerlines, surfaces=surfaces, buildings=buildings, palms=palms, paths=world.paths + layout.paths) + + +def apply_flamingo_park(world: World, config_path: Path, seed: int) -> tuple[World, list[str]]: + """Alles für Flamingo Park in der Reihenfolge der Spec: Ecken prüfen, Jefferson Avenue, Park.""" + config = load_park(config_path) + check_corners(config, world.roads) + world = apply_street(world, config.street) + fit = ParkFit(config) + layout = build_layout(load_elements(config.osm_path), config, fit) + world = apply_park(world, layout, seed) + messages = [f"Warnung: {warning}" for warning in layout.warnings] + messages.append(f"Flamingo Park: {len(layout.paths)} Wegflächen, {len(layout.surfaces)} Flächen, " + f"{len(layout.buildings)} Gebäude, Jefferson Avenue {config.street.width:g} m breit") + return world, messages diff --git a/tools/extract/run.py b/tools/extract/run.py index 73dc2cb..41cbdb5 100644 --- a/tools/extract/run.py +++ b/tools/extract/run.py @@ -8,12 +8,14 @@ from leonida.classify import CLASSES, class_shares, classify, drop_map_labels from leonida.emit import (chunk_box_of, chunk_payload, in_chunk, split_by_chunk_all, write_chunks, write_debug_png) from leonida.heights import assign_heights, palm_points +from leonida.park import World, apply_flamingo_park from leonida.tiles import COMMIT, download_tiles, load_mosaic from leonida.vectors import centerlines, land_polygons, mask_polygons, road_polygons from overview import render_map SEED = 20260912 OUT = Path(__file__).resolve().parents[2] / "public" / "data" / "vice-a" +PARK = Path("data/flamingo-park.toml") TOLERANCE = {"wasser": (0.30, 0.36), "strasse": (0.10, 0.14), "gebaeude": (0.09, 0.13)} @@ -73,25 +75,33 @@ def main() -> None: palms = palm_points(sand, parks, lines, [p for p, _ in buildings], road_shapes, land, SEED) + world = World(land=land, surfaces=surfaces, roads=roads, centerlines=lines, + buildings=[(p, h, s) for (p, s), h in zip(buildings, heights)], palms=palms) + world, messages = apply_flamingo_park(world, PARK, SEED) + for message in messages: + print(message) + payloads = [] for j in range(12): for i in range(12): payloads.append(chunk_payload( i, j, - land=list(split_by_chunk_all(land, i, j)), - surfaces=[(part, kind) for (p, kind) in surfaces for part in split_by_chunk_all([p], i, j)], - roads=[(part, b) for (p, b) in roads for part in split_by_chunk_all([p], i, j)], - centerlines=[ln for ln in lines if ln.intersects(chunk_box_of(i, j))], - buildings=[(p, h, s) for (p, s), h in zip(buildings, heights) - if p.intersects(chunk_box_of(i, j))], - palms=[pt for pt in palms if in_chunk(pt, i, j)], + land=list(split_by_chunk_all(world.land, i, j)), + surfaces=[(part, kind) for (p, kind) in world.surfaces for part in split_by_chunk_all([p], i, j)], + roads=[(part, b) for (p, b) in world.roads for part in split_by_chunk_all([p], i, j)], + centerlines=[ln for ln in world.centerlines if ln.intersects(chunk_box_of(i, j))], + buildings=[(p, h, s) for (p, h, s) in world.buildings if p.intersects(chunk_box_of(i, j))], + palms=[pt for pt in world.palms if in_chunk(pt, i, j)], + paths=[part for p in world.paths for part in split_by_chunk_all([p], i, j)], )) write_debug_png(Path("debug"), mosaic, payloads) write_chunks(OUT, payloads, { "slice": "vice-a", "source_commit": COMMIT, "seed": SEED, "chunk_size": 250, "chunks": [p["chunk"] for p in payloads], "class_shares": shares, - "attribution": "Kartenkacheln: GTA VI Map (CC BY 4.0) — https://map.gtadb.org, YANIS v15. Wahrzeichen: rolux/gtamaplib-vc (Unlicense).", + "attribution": "Kartenkacheln: GTA VI Map (CC BY 4.0) — https://map.gtadb.org, YANIS v15. " + "Wahrzeichen: rolux/gtamaplib-vc (Unlicense). " + "Flamingo Park: © OpenStreetMap-Mitwirkende (ODbL).", }) print(render_map(OUT, OUT), "->", OUT) diff --git a/tools/extract/tests/test_emit.py b/tools/extract/tests/test_emit.py index 6e363a0..496a7fb 100644 --- a/tools/extract/tests/test_emit.py +++ b/tools/extract/tests/test_emit.py @@ -20,9 +20,18 @@ def test_coordinates_are_chunk_local_three_js(): def test_payload_matches_schema(): payload = chunk_payload(2, 3, land=[], surfaces=[], roads=[], centerlines=[], buildings=[], palms=[]) - assert set(payload) == {"chunk", "origin", "land", "surfaces", "roads", "centerlines", "buildings", "palms"} + assert set(payload) == {"chunk", "origin", "land", "surfaces", "roads", "centerlines", "buildings", "palms", "paths"} assert payload["chunk"] == [2, 3] +def test_paths_are_chunk_local_rings_like_roads(): + path = Polygon([(-990, 2490), (-980, 2490), (-980, 2480), (-990, 2480)]) + payload = chunk_payload(0, 0, land=[], surfaces=[], roads=[], centerlines=[], buildings=[], palms=[], paths=[path]) + (entry,) = payload["paths"] + assert set(entry) == {"poly"} + ring = entry["poly"][0] + assert (min(x for x, _ in ring), max(x for x, _ in ring)) == (10, 20) + assert (min(z for _, z in ring), max(z for _, z in ring)) == (10, 20) + def test_polygon_holes_survive_into_the_payload(): """Ein von Strassen umschlossener Block darf nicht zu Asphalt werden. An echten Daten gehen sonst 449.876 m2 Strassenloch und 208.657 m2 Stadtloch verloren.""" diff --git a/tools/extract/tests/test_park_apply.py b/tools/extract/tests/test_park_apply.py index 3d1eea7..bfb5fbb 100644 --- a/tools/extract/tests/test_park_apply.py +++ b/tools/extract/tests/test_park_apply.py @@ -1,10 +1,13 @@ +from pathlib import Path from types import SimpleNamespace import pytest -from shapely.geometry import LineString, Point, box +from shapely.geometry import LineString, Point, Polygon, box from leonida.heights import PALM_CLEARANCE -from leonida.park import ParkError, ParkLayout, World, apply_park, check_corners +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) @@ -75,3 +78,17 @@ def test_corners_must_lie_at_a_road(): far = SimpleNamespace(game={"sw": (0.0, 0.0), "se": (100.0, 0.0), "ne": (100.0, 200.0), "nw": (30.0, 200.0)}) with pytest.raises(ParkError, match="Spielecke nw"): check_corners(far, roads) + + +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 + assert not any(message.startswith("Warnung") for message in messages) + assert any(message.startswith("Flamingo Park:") for message in messages)