fix(extract): Chunk-Ausgabe lauf-stabil, Eckenprüfung in beide Richtungen

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>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-16 13:43:01 +02:00
1 parent a3504df43d
commit 3c74202b5e
10 files changed
+333 -102

No files matched your search

+52 -1
View File
@@ -1,7 +1,8 @@
from shapely.geometry import Polygon
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)
@@ -48,6 +49,56 @@ def test_polygon_holes_survive_into_the_payload():
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