Fixrunde 1 zu Task 10 (Rückmeldung des Koordinators). Zwei Punkte: Die edges-Korrektur aus dem Hauptcommit (Sortierschlüssel deckte nur (line, klasse) ab, nicht breite/fahrbahn) hatte keinen committeten Regressionstest - nur eine Ad-hoc-Probe, die nicht mitkam. Drei Tests ergänzt: zwei edges-Einträge mit gleicher Linie/Klasse aber unterschiedlicher Breite in beiden Reihenfolgen (der eigentliche Regressionsschutz - mit dem alten zweiteiligen Schlüssel probeweise reproduziert, um die Fehlermeldung zu belegen, dann zurückgestellt), zwei bridges-Einträge in beiden Reihenfolgen, und eine Linie gegen ihre exakte Umkehrung einmal als curb, einmal als edges - das ist die Zusicherung, auf der _line()s ganze Normalisierung beruht, und stand bisher nirgends als Test. _line()-Docstring behauptete im Präsens, Laternen wählten ihre Seite schon aus der Kantenrichtung statt aus der Listenposition. Das stimmt nicht: src/world/props.js:107 wählt weiterhin über den geraden/ungeraden Index (vergeLampPoints(), liest chunk.centerlines/chunk.roads). Docstring jetzt als Bedingung formuliert - was vorausgesetzt wird und wo es eingelöst wird (eine geometriebasierte Nachfolgefunktion in props.js, im Plan als sidewalkLampPoints() vorgesehen) - statt als Tatsachenbehauptung, und ohne Aufgabennummer im Kommentar. overview.py, geometry.js und props.js bewusst nicht angefasst (Koordinator: das ist Sache der Aufgaben 13-15). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
194 lines
11 KiB
Python
194 lines
11 KiB
Python
from shapely.geometry import 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=[], carriageway=[], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[], edges=[], 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=[], carriageway=[], curb=[], sidewalk=[], markings=[],
|
|
lots=[], bridges=[], edges=[], buildings=[], palms=[])
|
|
assert set(payload) == {"chunk", "origin", "land", "surfaces", "carriageway", "curb", "sidewalk",
|
|
"markings", "lots", "bridges", "edges", "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=[], carriageway=[], curb=[], sidewalk=[], markings=[],
|
|
lots=[], bridges=[], edges=[], 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=[], carriageway=[], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[], edges=[], 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")],
|
|
carriageway=[(a, "wohnstrasse"), (b, "wohnstrasse")], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[b], edges=[],
|
|
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")],
|
|
carriageway=[(b, "wohnstrasse"), (a_gedreht, "wohnstrasse")], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[b], edges=[],
|
|
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")],
|
|
carriageway=[], curb=[], sidewalk=[], markings=[], lots=[], bridges=[],
|
|
edges=[], buildings=[], palms=[])
|
|
assert [s["kind"] for s in payload["surfaces"]] == ["park", "field"]
|
|
|
|
|
|
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)
|
|
|
|
|
|
def test_neue_schichten_stehen_im_chunk():
|
|
from shapely.geometry import LineString, box
|
|
payload = chunk_payload(
|
|
0, 0,
|
|
land=[box(0, 2400, 100, 2500)], surfaces=[],
|
|
carriageway=[(box(10, 2400, 20, 2500), "wohnstrasse")],
|
|
curb=[LineString([(10, 2400), (10, 2500)])],
|
|
sidewalk=[box(20, 2400, 23, 2500)],
|
|
markings=[(box(14, 2440, 16, 2443), "gelb_gestrichelt")],
|
|
lots=[box(40, 2400, 60, 2420)],
|
|
bridges=[], edges=[(LineString([(15, 2400), (15, 2500)]), "wohnstrasse", 12.0, 7.0)],
|
|
buildings=[], palms=[])
|
|
assert payload["carriageway"][0]["klasse"] == "wohnstrasse"
|
|
assert payload["markings"][0]["art"] == "gelb_gestrichelt"
|
|
assert payload["edges"][0]["klasse"] == "wohnstrasse"
|
|
assert payload["edges"][0]["fahrbahn"] == 7.0
|
|
assert len(payload["curb"][0]) == 2, "der Bordstein ist ein Linienzug, keine Fläche"
|
|
assert "roads" not in payload and "centerlines" not in payload
|
|
|
|
|
|
def test_neue_schichten_sind_lauf_stabil():
|
|
"""Zwei Aufrufe mit vertauschter Eingabe liefern dieselbe Ausgabe (Fixrunde 3 aus Teil 1)."""
|
|
from shapely.geometry import LineString, box
|
|
a, b = box(10, 2400, 20, 2450), box(30, 2400, 40, 2450)
|
|
l1, l2 = LineString([(0, 2400), (0, 2450)]), LineString([(5, 2400), (5, 2450)])
|
|
eins = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[(a, "gasse"), (b, "gasse")],
|
|
curb=[l1, l2], sidewalk=[a], markings=[(a, "zebra"), (b, "zebra")],
|
|
lots=[a, b], bridges=[], edges=[], buildings=[], palms=[])
|
|
zwei = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[(b, "gasse"), (a, "gasse")],
|
|
curb=[l2, l1], sidewalk=[a], markings=[(b, "zebra"), (a, "zebra")],
|
|
lots=[b, a], bridges=[], edges=[], buildings=[], palms=[])
|
|
assert eins == zwei
|
|
|
|
|
|
def test_edges_sortierschluessel_deckt_alle_felder_ab():
|
|
"""Regressionsschutz fuer den Fund aus Fixrunde 1: Zwei Kanten mit gleicher Linie und
|
|
gleicher Klasse, aber unterschiedlicher Breite, duerfen nicht an der Eingabereihenfolge
|
|
haengen bleiben. Mit dem Schluessel (line, klasse) allein - wie im urspruenglichen
|
|
Auftragscode - loest keiner der beiden Faelle den Tie auf, sorted() bleibt dann stabil und
|
|
reicht die Eingabereihenfolge unveraendert durch; erst breite/fahrbahn im Schluessel machen
|
|
die zwei Eintraege wieder unterscheidbar und damit total geordnet."""
|
|
from shapely.geometry import LineString
|
|
linie = LineString([(70, 2400), (70, 2450)])
|
|
schmal = (linie, "gasse", 6.0, 6.0)
|
|
breit = (linie, "gasse", 99.0, 99.0)
|
|
eins = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[], edges=[schmal, breit],
|
|
buildings=[], palms=[])
|
|
zwei = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[], edges=[breit, schmal],
|
|
buildings=[], palms=[])
|
|
assert eins == zwei
|
|
|
|
|
|
def test_bridges_sind_lauf_stabil():
|
|
"""Gegenprobe zu test_neue_schichten_sind_lauf_stabil, die bridges leer laesst: zwei
|
|
Bruecken in vertauschter Reihenfolge muessen dieselbe Ausgabe ergeben."""
|
|
a = Polygon([(10, 2400), (20, 2400), (20, 2450), (10, 2450)])
|
|
b = Polygon([(30, 2400), (40, 2400), (40, 2450), (30, 2450)])
|
|
eins = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[a, b], edges=[], buildings=[], palms=[])
|
|
zwei = chunk_payload(0, 0, land=[], surfaces=[], carriageway=[], curb=[], sidewalk=[],
|
|
markings=[], lots=[], bridges=[b, a], edges=[], buildings=[], palms=[])
|
|
assert eins == zwei
|
|
|
|
|
|
def test_curb_und_edges_sind_richtungsunabhaengig():
|
|
"""Die Zusicherung, auf der die ganze Normalisierung in _line() beruht (siehe dessen
|
|
Docstring): eine Linie und ihre exakte Umkehrung muessen fuer curb wie fuer edges dieselbe
|
|
Ausgabe ergeben. Einzeln geprueft (nur curb bzw. nur edges besetzt), damit ein kuenftiger
|
|
Bruch das betroffene Feld benennt statt nur "irgendwas stimmt nicht"."""
|
|
from shapely.geometry import LineString
|
|
hin = LineString([(10, 2400), (18, 2470), (22, 2500)])
|
|
zurueck = LineString(list(reversed(list(hin.coords))))
|
|
rest = dict(land=[], surfaces=[], carriageway=[], sidewalk=[], markings=[], lots=[],
|
|
bridges=[], buildings=[], palms=[])
|
|
|
|
eins_curb = chunk_payload(0, 0, curb=[hin], edges=[], **rest)
|
|
zwei_curb = chunk_payload(0, 0, curb=[zurueck], edges=[], **rest)
|
|
assert eins_curb["curb"] == zwei_curb["curb"]
|
|
|
|
eins_edges = chunk_payload(0, 0, curb=[], edges=[(hin, "wohnstrasse", 12.0, 7.0)], **rest)
|
|
zwei_edges = chunk_payload(0, 0, curb=[], edges=[(zurueck, "wohnstrasse", 12.0, 7.0)], **rest)
|
|
assert eins_edges["edges"] == zwei_edges["edges"]
|