"""Erzeugt aus den Kacheln die Chunk-Dateien. Aufruf: uv run python run.py""" import json from pathlib import Path from leonida.annotations import load_annotations, mark_generated, remove_annotations from leonida.buildings import building_polygons 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)} def main() -> None: cache = Path("cache") download_tiles(cache) mosaic = load_mosaic(cache) labels, unknown_share = classify(mosaic, report_unknown=True) labels = drop_map_labels(labels) removal = remove_annotations(mosaic, labels, load_annotations(Path("data/annotations.toml"))) labels = removal.labels labels = drop_map_labels(labels) # Inselreste, die erst das Entfernen im Wasser hinterlässt print(removal.report()) if unknown_share > 0.05: raise SystemExit(f"{unknown_share:.1%} der Pixel unbekannt. Palette kalibrieren, nicht die Grenze anheben.") shares = class_shares(labels) for name, (lo, hi) in TOLERANCE.items(): if not lo <= shares[name] <= hi: raise SystemExit(f"Klassenanteil {name}={shares[name]:.3f} außerhalb {lo}..{hi}. Palette prüfen.") land = land_polygons(labels) roads = road_polygons(labels) lines = centerlines(labels) sand = mask_polygons(labels == CLASSES["sand"], 50, 1.0) parks = mask_polygons(labels == CLASSES["gruen"], 50, 1.0) urban = mask_polygons(labels == CLASSES["stadt"], 80, 1.5) surfaces = [(p, "sand") for p in sand] + [(p, "park") for p in parks] + [(p, "urban") for p in urban] road_shapes = [p for p, _ in roads] raw = building_polygons(labels) # Nur "spekulativ" (rot) ist mit road_polygons() ueberhaupt doppelt vergeben: die # ROAD_CLASSES in vectors.py zaehlen "spekulativ" selbst als Strassenklasse, und # road_mask() schliesst zusaetzlich mit disk(3), um Strassennamen zu ueberbruecken. # Wuerde man road_shapes (= road_polygons(labels), inklusive dieser Schliessung und # inklusive "spekulativ") direkt gegen ALLE rohen Gebaeude pruefen, vergleicht ein # isoliertes "spekulativ"-Gebaeude sich praktisch mit sich selbst (dieselben Pixel # bilden dort auch das Strassenpolygon) und faellt so gut wie immer heraus: am realen # Mosaik blieben nur 3 von 267 "spekulativ"-Gebaeuden uebrig statt der erwarteten # Groessenordnung (Task-6-Referenzwerte: 258 von 267), und die Schliessung riss # zusaetzlich 211 gewoehnliche (graue) Gebaeude mit, die die Notiz in Task 5 gar nicht # meinte ("die ROTEN Flaechen ... werden abgezogen"). Deshalb hier ein eigenes, # unzweideutiges Strassenmass nur aus "strasse"/"weg" (ohne "spekulativ", ohne die # Namens-Schliessung) und die Pruefung nur auf spekulative Kandidaten begrenzt; graue # Gebaeude durchlaufen ungeprueft. Ergebnis am realen Mosaik: 2.348 Gebaeude, davon 263 # spekulativ (Referenz: 2.352 bzw. 258) — Wahrzeichenhoehen (68, 31-248 m) und # Palmenzahl (2.133) treffen die Referenz exakt. unambiguous_mask = (labels == CLASSES["strasse"]) | (labels == CLASSES["weg"]) unambiguous_roads = mask_polygons(unambiguous_mask, min_area=20, simplify=0.8) buildings = [(p, s) for p, s in raw if not s or not any(p.intersection(r).area > 0.5 * p.area for r in unambiguous_roads)] landmarks = json.loads(Path("data/skyline.json").read_text())["landmarks"] districts = json.loads(Path("data/districts.json").read_text()) heights = assign_heights([p for p, _ in buildings], landmarks, districts, SEED) marked = mark_generated([p for p, _ in buildings], heights, removal.generated) print(f"Gebäude: {len(buildings)}, davon {marked} unter entfernten Beschriftungen ergänzt") 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(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). " "Flamingo Park: © OpenStreetMap-Mitwirkende (ODbL).", }) print(render_map(OUT, OUT), "->", OUT) if __name__ == "__main__": main()