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

+32 -5
View File
@@ -72,12 +72,33 @@ def test_same_seed_gives_the_same_palms():
assert apply_park(world(), layout(), seed=7).palms == apply_park(world(), layout(), seed=7).palms
def test_corners_must_lie_at_a_road():
def corners(**moved):
game = {"sw": (0.0, 0.0), "se": (100.0, 0.0), "ne": (100.0, 200.0), "nw": (0.0, 200.0)}
return SimpleNamespace(game={**game, **moved})
def test_corners_must_lie_at_a_road_when_it_pulls_back():
"""Die eine Richtung: die Straße bleibt, wo sie ist, die Ecke steht daneben."""
roads = [(box(-20, -20, 0, 220), False), (box(100, -20, 120, 220), False)]
check_corners(SimpleNamespace(game={"sw": (0.0, 0.0), "se": (100.0, 0.0), "ne": (100.0, 200.0), "nw": (0.0, 200.0)}), roads)
far = SimpleNamespace(game={"sw": (0.0, 0.0), "se": (100.0, 0.0), "ne": (100.0, 200.0), "nw": (30.0, 200.0)})
check_corners(corners(), roads)
with pytest.raises(ParkError, match="Spielecke nw"):
check_corners(far, roads)
check_corners(corners(nw=(30.0, 200.0)), roads)
def test_a_road_that_grew_inward_over_the_corner_is_caught():
"""Die andere Richtung, und der Grund für die Messung gegen den Rand statt gegen die Fläche.
Die Westkante der linken Straße ist von x = 0 auf x = 6 nach innen gewandert; die SW- und die
NW-Ecke liegen jetzt 6 m tief in der Fahrbahn. apply_park() schnitte die verbreiterte Straße
wortlos wieder auf das alte Viereck zurück — deshalb muss die Prüfung hier abbrechen. Mit dem
Flächenabstand tut sie das nicht: er ist für eine Ecke mitten in der Straße 0 (die Zusicherung
unten misst genau das), und die alte Bedingung poly.distance(corner) <= 2.0 ließ den Fall
durchlaufen.
"""
grown = [(box(-20, -20, 6, 220), False), (box(100, -20, 120, 220), False)]
assert min(poly.distance(Point(0, 0)) for poly, _ in grown) == 0
with pytest.raises(ParkError, match="Spielecke sw"):
check_corners(corners(), grown)
def test_flamingo_park_goes_into_a_world_with_roads_around_the_park():
@@ -93,4 +114,10 @@ def test_flamingo_park_goes_into_a_world_with_roads_around_the_park():
# Bekannter Zustand: die Zufahrt zum Parkplatz quert die Sperrfläche der Tribüne und wird dort
# ausgeschnitten (Task 5). Jede weitere oder andere Warnung ist ein echter Befund.
assert [m for m in messages if m.startswith("Warnung")] == ["Warnung: Zufahrt schneidet die Sperrfläche tribuene"]
assert any(message.startswith("Flamingo Park:") for message in messages)
# Die Flächenzahl gehört mit in den Vergleich: _surface_kind() kennt nur die Sportarten aus
# FIELD_SPORTS und COURT_SPORTS und lässt jede andere wortlos weg. Taggt OpenStreetMap beim
# nächsten Abruf ein Feld um — aus "tennis" wird "pickleball", aus "soccer" ein "multi" —,
# verschwindet es aus dem Park, ohne dass eine Warnung oder ein anderer Test das bemerkt;
# hier sinkt dann die 14, und der Abzug wird geprüft statt der Park stillschweigend kleiner.
assert [m for m in messages if m.startswith("Flamingo Park:")] == [
"Flamingo Park: 2 Wegflächen, 14 Flächen, 11 Gebäude, Jefferson Avenue 12 m breit"]