feat(extract): Flamingo Park hängt seine Straße an den Graphen

World.centerlines wird zu World.graph: apply_street() und apply_park() schneiden und
verlängern jetzt den Straßengraphen statt einer losen Liste von Mittellinien, mit den neuen
roads.cut_edges()/attach() als Gegenstück zum bestehenden Schneiden der Straßenflächen.
apply_park() schneidet dabei einmal am Parkviereck (nicht je Sperrfläche - das schneidet
tatsächlich nichts, was der Parkumriss nicht schon geschnitten hätte). check_corners() bleibt
unverändert gegen world.roads (den Korridor) messen, mit einem erklärenden Docstring-Absatz und
einer Regressionswache dafür in den Tests.

MIN_CENTERLINE (15 m) entfällt zugunsten von MIN_EDGE (5 m): Der alte Grund - Laternen wählten
ihre Straßenseite über den Listenindex - fällt weg, sobald eine spätere Aufgabe die Seite aus
der Geometrie bestimmt. test_park_street.py dokumentiert das jetzt: die beiden 11-m-Enden der
geschnittenen Gasse bleiben deshalb stehen, statt wie vorher zu verschwinden.

Beim Nachrechnen der Prüfkörper aus dem Auftrag fielen zwei echte Fehler auf: Der Reste-unter-
Mindestlänge-Test schnitt ursprünglich bis übers Streckenende hinaus, sodass der Rest schon
geometrisch verschwand und der MIN_EDGE-Filter nie geprüft wurde - jetzt schneidet er innerhalb
der Strecke, sodass tatsächlich ein zu kurzes Stück verworfen wird. Und die neue
Korridor-Regressionswache verglich (12-7)/2 > 2.5, was mit den echten Maßen immer falsch ist
(2.5 > 2.5); korrigiert auf >=, denn die Halbdifferenz trifft die Toleranz exakt.

Ausserdem musste eine Test-Fixture in test_park_apply.py von einem 5-m-Reststück (exakt an der
neuen MIN_EDGE-Grenze, hätte also entgegen der ursprünglichen Testabsicht überlebt) auf 3 m
verkürzt werden, damit der Test weiterhin prüft, dass echte Kurzreste wegfallen.

run.py bleibt unangetastet: Es ist bereits durch eine frühere Aufgabe gebrochen und wird von der
nächsten Aufgabe repariert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-17 10:41:08 +02:00
1 parent 8283109ceb
commit 8bc8ca19fc
5 files changed
+162 -23

No files matched your search

+45 -1
View File
@@ -18,7 +18,7 @@ from dataclasses import dataclass, field
import networkx as nx
import numpy as np
from scipy import ndimage as ndi
from shapely.geometry import LineString
from shapely.geometry import LineString, Polygon
from skimage.morphology import disk
from .coords import SLICE
@@ -280,6 +280,50 @@ def measure_widths(graph: Graph, distance: np.ndarray) -> Graph:
return graph
# Kürzere Reste nach einem Schnitt sind Stummel, die nur eine Kreuzung vortäuschen würden.
MIN_EDGE = 5.0
def cut_edges(graph: Graph, cut: Polygon) -> Graph:
"""Alles, was in der Fläche liegt, aus den Kanten herausschneiden.
Gegenstück zum Schneiden der Straßenflächen: Wo Flamingo Park die alte Gasse entfernt, darf
im Graphen an derselben Stelle keine Kante mehr stehen - sonst führte dort weiterhin eine
Fahrbahn durch den Park, nur ohne die Straßenfläche, die sie einmal begründet hat.
"""
out = Graph(nodes=list(graph.nodes), edges=[])
for edge in graph.edges:
line = LineString(edge.points)
if not line.intersects(cut):
out.edges.append(edge)
continue
rest = line.difference(cut)
for piece in getattr(rest, "geoms", [rest]):
if piece.geom_type != "LineString" or piece.length < MIN_EDGE:
continue
points = [(round(x, 2), round(y, 2)) for x, y in piece.coords]
out.edges.append(Edge(a=attach(out, points[0]), b=attach(out, points[-1]),
points=points, width=edge.width, klass=edge.klass))
return _renumber(out)
def attach(graph: Graph, point: tuple[float, float], radius: float = JUNCTION_RADIUS) -> int:
"""Knoten-ID an dieser Stelle: der nächste innerhalb von radius, sonst ein neuer.
Damit hängt sich eine von Hand ergänzte Straße (die Jefferson Avenue) an die Querstraße an,
statt als freies Ende danebenzuliegen und ohne Kreuzung zu enden.
"""
best, best_distance = None, radius
for k, node in enumerate(graph.nodes):
distance = ((node.point[0] - point[0]) ** 2 + (node.point[1] - point[1]) ** 2) ** 0.5
if distance <= best_distance:
best, best_distance = k, distance
if best is not None:
return best
graph.nodes.append(Node((float(point[0]), float(point[1]))))
return len(graph.nodes) - 1
def _median_width(line: LineString, distance: np.ndarray) -> float:
start, end = WIDTH_MARGIN, line.length - WIDTH_MARGIN
if end <= start: