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)]
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],
}
+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)
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
+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,
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)
+10 -1
View File
@@ -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."""
+19 -2
View File
@@ -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)