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>
115 lines
6.7 KiB
Python
115 lines
6.7 KiB
Python
from shapely.geometry import LineString, Polygon
|
|
from leonida.emit import chunk_payload, split_by_chunk, split_by_chunk_all
|
|
|
|
BIG = Polygon([(-1000, 2500), (-700, 2500), (-700, 2200), (-1000, 2200)]) # Chunk (0,0) und Nachbarn
|
|
HEIGHT = {"height": 9.0, "source": "district"}
|
|
|
|
def test_polygon_is_clipped_into_its_chunks():
|
|
parts = split_by_chunk(BIG)
|
|
assert set(parts) >= {(0, 0), (1, 0), (0, 1), (1, 1)}
|
|
assert abs(sum(p.area for p in parts.values()) - BIG.area) < 1.0
|
|
|
|
def test_coordinates_are_chunk_local_three_js():
|
|
# chunk_payload() nimmt (wie in run.py) bereits chunk-geschnittene Geometrie entgegen;
|
|
# BIG selbst überspannt vier Chunks (siehe Test oben), darum hier zuerst zuschneiden.
|
|
clipped = split_by_chunk(BIG)[(0, 0)]
|
|
payload = chunk_payload(0, 0, land=[clipped], surfaces=[], roads=[], centerlines=[], buildings=[], palms=[])
|
|
ring = payload["land"][0][0] # Ruling 34: [0] ist jetzt der Aussenring
|
|
assert payload["origin"] == [-1000, -2500] # three-z = -Y
|
|
assert min(x for x, _ in ring) >= -0.01
|
|
assert max(x for x, _ in ring) <= 250.01
|
|
|
|
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", "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."""
|
|
outer = Polygon([(-1000, 2250), (-800, 2250), (-800, 2450), (-1000, 2450)],
|
|
[[(-950, 2300), (-870, 2300), (-870, 2400), (-950, 2400)]])
|
|
payload = chunk_payload(0, 0, land=[outer], surfaces=[], roads=[], centerlines=[],
|
|
buildings=[], palms=[])
|
|
assert len(payload["land"]) == 1
|
|
rings = payload["land"][0]
|
|
assert len(rings) == 2 # Aussenring und Loch
|
|
exterior, hole = rings
|
|
# Das Loch liegt innerhalb des Aussenrings, nicht daneben.
|
|
assert min(x for x, _ in exterior) < min(x for x, _ in hole)
|
|
assert max(x for x, _ in exterior) > max(x for x, _ in hole)
|
|
assert all(0 <= x <= 250 and 0 <= z <= 250 for x, z in hole)
|
|
|
|
def test_the_same_geometry_in_another_order_gives_the_same_output():
|
|
"""Die Ausgabe darf nicht am Rechenweg haengen, nur an der Geometrie.
|
|
|
|
unary_union() in park.py fasst das kartenweite Strassenpolygon neu zusammen und gibt dieselbe
|
|
Flaeche danach an anderer Listenstelle und mit anderem Ringstartpunkt zurueck. Das aenderte
|
|
88 Chunkdateien, in denen sich geometrisch nichts bewegt hatte (symmetrische Differenz der
|
|
Strassenvereinigung 0,00 m2 in allen 88). Hier steht dieselbe Welt zweimal: einmal in der
|
|
einen, einmal in der anderen Reihenfolge, und `zweite` beschreibt `a` ausserdem mit anderem
|
|
Startpunkt und anderer Wicklung."""
|
|
a = Polygon([(-990, 2490), (-980, 2490), (-980, 2480), (-990, 2480)])
|
|
b = Polygon([(-960, 2460), (-950, 2460), (-950, 2450), (-960, 2450)])
|
|
a_gedreht = Polygon([(-980, 2480), (-980, 2490), (-990, 2490), (-990, 2480)])
|
|
assert a.equals(a_gedreht) and list(a.exterior.coords) != list(a_gedreht.exterior.coords)
|
|
erste = chunk_payload(0, 0, land=[a, b], surfaces=[(a, "urban"), (b, "urban")],
|
|
roads=[(a, False), (b, True)], centerlines=[],
|
|
buildings=[(a, HEIGHT, False), (b, HEIGHT, True)],
|
|
palms=[(-990.0, 2490.0), (-960.0, 2460.0)], paths=[a, b])
|
|
zweite = chunk_payload(0, 0, land=[b, a_gedreht], surfaces=[(b, "urban"), (a_gedreht, "urban")],
|
|
roads=[(b, True), (a_gedreht, False)], centerlines=[],
|
|
buildings=[(b, HEIGHT, True), (a_gedreht, HEIGHT, False)],
|
|
palms=[(-960.0, 2460.0), (-990.0, 2490.0)], paths=[b, a_gedreht])
|
|
assert erste == zweite
|
|
|
|
|
|
def test_surfaces_keep_the_order_of_their_kinds():
|
|
"""Gegenprobe zum Sortieren: overview.py malt die Belaege in Listenreihenfolge uebereinander.
|
|
|
|
Der Parkrasen liegt unter den Feldern; sortierte man ueber die Arten hinweg, deckte der grosse
|
|
Rasen sie auf map.png wieder zu (19.573 m2 Feld allein im Flamingo Park)."""
|
|
rasen = Polygon([(-1000, 2500), (-900, 2500), (-900, 2400), (-1000, 2400)])
|
|
feld = Polygon([(-980, 2480), (-940, 2480), (-940, 2440), (-980, 2440)])
|
|
payload = chunk_payload(0, 0, land=[], surfaces=[(rasen, "park"), (feld, "field")], roads=[],
|
|
centerlines=[], buildings=[], palms=[])
|
|
assert [s["kind"] for s in payload["surfaces"]] == ["park", "field"]
|
|
|
|
|
|
def test_centerlines_keep_their_order_and_direction():
|
|
"""Die zweite Gegenprobe: Mittellinien werden bewusst nicht sortiert und nicht normalisiert.
|
|
|
|
vergeLampPoints() in src/world/props.js setzt jede Laterne abwechselnd links und rechts der
|
|
Fahrbahn (gerader Index links, ungerader rechts) und dreht sie in Laufrichtung der Linie.
|
|
Sortieren verschob an den ausgelieferten Chunks 1.848 von 3.921 Laternen auf die andere
|
|
Strassenseite; normalize() wuerde zusaetzlich jede zweite Linie umdrehen."""
|
|
spaet = LineString([(-900, 2400), (-950, 2400)])
|
|
frueh = LineString([(-990, 2490), (-960, 2490)])
|
|
payload = chunk_payload(0, 0, land=[], surfaces=[], roads=[], centerlines=[spaet, frueh],
|
|
buildings=[], palms=[])
|
|
assert payload["centerlines"] == [[[100, 100], [50, 100]], [[10, 10], [40, 10]]]
|
|
|
|
|
|
def test_split_by_chunk_all_yields_polygons_for_a_multi_part_intersection():
|
|
"""split_by_chunk_all() ist die Variante, die run.py tatsaechlich benutzt (nicht
|
|
split_by_chunk), und hat einen MultiPolygon-Zweig, den kein anderer Test trifft: ein
|
|
Polygon, dessen Schnitt mit der Chunk-Zelle in mehrere Teile zerfaellt. Hier eine
|
|
Klammerform (Steg ausserhalb der Zelle, zwei Zinken, die je einmal hineinreichen)."""
|
|
staple = Polygon([
|
|
(-1100, 2495), (-850, 2495), (-850, 2470), (-1010, 2470),
|
|
(-1010, 2280), (-850, 2280), (-850, 2255), (-1010, 2255),
|
|
(-1100, 2255),
|
|
])
|
|
parts = list(split_by_chunk_all([staple], 0, 0))
|
|
assert len(parts) == 2
|
|
assert all(p.geom_type == "Polygon" for p in parts)
|