feat(extract): Flamingo Park in run.py, Parkwege in der Chunk-Ausgabe

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-16 08:00:07 +02:00
1 parent 2ef9e6774a
commit 442f5b2ada
5 files changed
+64 -12

No files matched your search

+3 -1
View File
@@ -39,7 +39,7 @@ def _rings(poly: Polygon, i: int, j: int) -> list[list[list[float]]]:
for ring in (poly.exterior, *poly.interiors)] 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 ox = SLICE["x0"] + i * CHUNK_SIZE
oy = SLICE["y1"] - j * CHUNK_SIZE oy = SLICE["y1"] - j * CHUNK_SIZE
return { 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"], "buildings": [{"poly": _rings(p, i, j), "height": h["height"],
"source": h["source"], "speculative": s} for p, h, s in buildings], "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], "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],
} }
+14
View File
@@ -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) palms += palm_points([], _polygons(lawn), [], [] if blocked.is_empty else [blocked], [], [], seed)
return replace(world, roads=roads, centerlines=centerlines, surfaces=surfaces, return replace(world, roads=roads, centerlines=centerlines, surfaces=surfaces,
buildings=buildings, palms=palms, paths=world.paths + layout.paths) 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
+18 -8
View File
@@ -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, from leonida.emit import (chunk_box_of, chunk_payload, in_chunk, split_by_chunk_all,
write_chunks, write_debug_png) write_chunks, write_debug_png)
from leonida.heights import assign_heights, palm_points 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.tiles import COMMIT, download_tiles, load_mosaic
from leonida.vectors import centerlines, land_polygons, mask_polygons, road_polygons from leonida.vectors import centerlines, land_polygons, mask_polygons, road_polygons
from overview import render_map from overview import render_map
SEED = 20260912 SEED = 20260912
OUT = Path(__file__).resolve().parents[2] / "public" / "data" / "vice-a" 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)} 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, palms = palm_points(sand, parks, lines, [p for p, _ in buildings], road_shapes,
land, SEED) 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 = [] payloads = []
for j in range(12): for j in range(12):
for i in range(12): for i in range(12):
payloads.append(chunk_payload( payloads.append(chunk_payload(
i, j, i, j,
land=list(split_by_chunk_all(land, i, j)), land=list(split_by_chunk_all(world.land, i, j)),
surfaces=[(part, kind) for (p, kind) in surfaces for part in split_by_chunk_all([p], 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 roads 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 lines if ln.intersects(chunk_box_of(i, j))], centerlines=[ln for ln in world.centerlines if ln.intersects(chunk_box_of(i, j))],
buildings=[(p, h, s) for (p, s), h in zip(buildings, heights) buildings=[(p, h, s) for (p, h, s) in world.buildings if p.intersects(chunk_box_of(i, j))],
if p.intersects(chunk_box_of(i, j))], palms=[pt for pt in world.palms if in_chunk(pt, i, j)],
palms=[pt for pt in 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_debug_png(Path("debug"), mosaic, payloads)
write_chunks(OUT, payloads, { write_chunks(OUT, payloads, {
"slice": "vice-a", "source_commit": COMMIT, "seed": SEED, "slice": "vice-a", "source_commit": COMMIT, "seed": SEED,
"chunk_size": 250, "chunks": [p["chunk"] for p in payloads], "chunk_size": 250, "chunks": [p["chunk"] for p in payloads],
"class_shares": shares, "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) print(render_map(OUT, OUT), "->", OUT)
+10 -1
View File
@@ -20,9 +20,18 @@ def test_coordinates_are_chunk_local_three_js():
def test_payload_matches_schema(): def test_payload_matches_schema():
payload = chunk_payload(2, 3, land=[], surfaces=[], roads=[], centerlines=[], buildings=[], palms=[]) 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] 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(): def test_polygon_holes_survive_into_the_payload():
"""Ein von Strassen umschlossener Block darf nicht zu Asphalt werden. An echten Daten """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.""" gehen sonst 449.876 m2 Strassenloch und 208.657 m2 Stadtloch verloren."""
+19 -2
View File
@@ -1,10 +1,13 @@
from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import pytest 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.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) QUAD = box(0, 0, 100, 200)
SERVICE = box(60, 0, 66, 30) 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)}) 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"): with pytest.raises(ParkError, match="Spielecke nw"):
check_corners(far, roads) 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)