docs: Palmenpunkte in Task 5 korrigieren (Rulings 20-23)

Die erste Palme startet bei halber Teilung, weil die Mittellinien seit
Task 4 an Kreuzungen getrennt sind und zwei gegenüberliegende Äste sonst
zwei Palmen in denselben Punkt setzen. Der feste Versatz von 4 m weicht
einer Suche vom Fahrbahnrand: am echten Mosaik stünden damit 60 % der
Palmen auf der Fahrbahn. Dazu zwei Tests, die es bisher nicht gab, und
die korrigierte Signatur im Interfaces-Block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5 committed 2026-09-12 15:27:37 +02:00
1 parent 830e9f87e0
commit 11f6aa76fe
1 file changed
+74 -14
@@ -893,7 +893,7 @@ Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>"
**Interfaces:** **Interfaces:**
- Consumes: `mask_polygons`, `centerlines` aus Task 4, `CLASSES` aus Task 3 - Consumes: `mask_polygons`, `centerlines` aus Task 4, `CLASSES` aus Task 3
- Produces: `regularize(poly) -> Polygon | None`, `building_polygons(labels) -> list[tuple[Polygon, bool]]` (Polygon, `spekulativ`), `assign_heights(polys, landmarks, districts, seed) -> list[float]`, `height_sources(...) -> list[str]`, `palm_points(labels, lines, seed) -> list[tuple[float, float]]` - Produces: `regularize(poly) -> Polygon | None`, `building_polygons(labels) -> list[tuple[Polygon, bool]]` (Polygon, `spekulativ`), `assign_heights(polys, landmarks, districts, seed) -> list[float]`, `height_sources(...) -> list[str]`, `palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed) -> list[tuple[float, float]]`
- [ ] **Step 1: Den fehlschlagenden Test für Gebäude schreiben** - [ ] **Step 1: Den fehlschlagenden Test für Gebäude schreiben**
@@ -1068,34 +1068,89 @@ def assign_heights(polys: list[Polygon], landmarks: list[dict],
Run: `cd tools/extract && uv run pytest -v` Run: `cd tools/extract && uv run pytest -v`
Expected: PASS, alle Tests Expected: PASS, alle Tests
- [ ] **Step 9: Palmenpunkte ergänzen** - [ ] **Step 9: Den fehlschlagenden Test für Palmenpunkte schreiben**
An `tools/extract/tests/test_heights.py` anfügen:
```python
from shapely.geometry import LineString, Point
from leonida.heights import PALM_KERB, palm_points
def test_no_palm_stands_on_the_carriageway():
"""Der Versatz muss vom Fahrbahnrand her gedacht werden, nicht von einem festen Wert.
Am echten Mosaik hat die Fahrbahn an den Mittellinienpunkten p50 5,4 m, p90 11,7 m
und p95 14,0 m Halbbreite. Ein fester Versatz von 4 m setzt 60 % der Palmen auf die
Fahrbahn, einer von 11 m immer noch 12,5 % – und auf der Hälfte der Straßen stünde
die Palme dann im Nachbargebäude.
"""
road = Polygon([(0, -9), (200, -9), (200, 9), (0, 9)]) # 18 m breite Fahrbahn
line = LineString([(0, 0), (200, 0)])
sand = Polygon([(0, 20), (200, 20), (200, 40), (0, 40)])
pts = palm_points([sand], [], [line], [], [road], seed=1)
assert pts
assert all(road.distance(Point(x, y)) >= PALM_KERB for x, y in pts)
def test_palms_do_not_pile_up_where_centerlines_meet():
"""Mittellinien sind an Kreuzungen getrennt (Task 4). Ein Start bei Position 0 setzt
auf zwei gegenüberliegenden Ästen zwei Palmen in genau denselben Punkt."""
sand = Polygon([(-80, 20), (80, 20), (80, 40), (-80, 40)])
arms = [LineString([(0, 0), (60, 0)]), LineString([(0, 0), (-60, 0)])]
pts = palm_points([sand], [], arms, [], [], seed=1)
assert pts
assert len(set(pts)) == len(pts)
```
- [ ] **Step 10: Palmenpunkte ergänzen**
An `leonida/heights.py` anfügen: An `leonida/heights.py` anfügen:
```python ```python
PALM_SPACING = 12.0 # Meter entlang der Mittellinie PALM_SPACING = 12.0 # Meter entlang der Mittellinie
PALM_BEACH_RANGE = 40.0 # Meter Abstand zur nächsten Sandfläche PALM_BEACH_RANGE = 40.0 # Meter Abstand zur nächsten Sandfläche
PALM_CLEARANCE = 6.0 # Meter Abstand zu Gebäuden und Straßen im Park PALM_CLEARANCE = 6.0 # Meter Abstand zu Gebäuden
PALM_KERB = 1.5 # Meter Abstand zum Fahrbahnrand
# Versatzkandidaten von innen nach außen; genommen wird der erste, der Fahrbahn und
# Gebäude freihält. Ein fester Wert geht nicht: die Fahrbahn ist an den Mittellinien-
# punkten p50 5,4 m, p90 11,7 m, p95 14,0 m halbbreit (am echten Mosaik gemessen).
PALM_VERGE = (6.0, 8.0, 10.0, 12.0, 14.0, 16.0)
def _verge_point(line, p, side: str, roads, buildings):
"""Erster Versatz von innen nach außen, der Fahrbahn und Gebäude freihält."""
for dist in PALM_VERGE:
off = line.parallel_offset(dist, side)
if off.is_empty:
continue
q = off.interpolate(off.project(p))
if roads is not None and q.distance(roads) < PALM_KERB:
continue
if buildings is not None and q.distance(buildings) < PALM_CLEARANCE:
continue
return [(round(q.x, 2), round(q.y, 2))]
return []
def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed: int): def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, seed: int):
"""Palmen entlang strandnaher Mittellinien und verstreut in Parks.""" """Palmen entlang strandnaher Mittellinien und verstreut in Parks."""
from shapely.ops import unary_union from shapely.ops import unary_union
sand = unary_union(sand_polys) if sand_polys else None sand = unary_union(sand_polys) if sand_polys else None
blocked = unary_union(list(building_polys) + list(road_polys)) if (building_polys or road_polys) else None roads = unary_union(list(road_polys)) if road_polys else None
buildings = unary_union(list(building_polys)) if building_polys else None
blocked = unary_union([g for g in (roads, buildings) if g is not None]) \
if (roads is not None or buildings is not None) else None
points = [] points = []
for line in road_lines: for line in road_lines:
if sand is None or line.distance(sand) > PALM_BEACH_RANGE: if sand is None or line.distance(sand) > PALM_BEACH_RANGE:
continue continue
steps = max(1, int(line.length // PALM_SPACING)) # Start bei halber Teilung: die Mittellinien sind an Kreuzungen getrennt, ein
for k in range(steps + 1): # Start bei 0 setzt je Ast eine Palme in denselben Punkt.
p = line.interpolate(k * PALM_SPACING) pos = min(PALM_SPACING / 2, line.length / 2)
for side in (-1, 1): while pos <= line.length:
off = line.parallel_offset(4.0 * side, "left" if side > 0 else "right") p = line.interpolate(pos)
if off.is_empty: for side in ("left", "right"):
continue points.extend(_verge_point(line, p, side, roads, buildings))
q = off.interpolate(off.project(p)) pos += PALM_SPACING
points.append((round(q.x, 2), round(q.y, 2)))
for park in park_polys: for park in park_polys:
minx, miny, maxx, maxy = park.bounds minx, miny, maxx, maxy = park.bounds
n = int(park.area // 250) n = int(park.area // 250)
@@ -1108,7 +1163,12 @@ def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys,
return points return points
``` ```
- [ ] **Step 10: Commit** - [ ] **Step 11: Test laufen lassen, er muss bestehen**
Run: `cd tools/extract && uv run pytest -v`
Expected: PASS, alle Tests
- [ ] **Step 12: Commit**
```bash ```bash
git add tools/extract/leonida/buildings.py tools/extract/leonida/heights.py tools/extract/tests git add tools/extract/leonida/buildings.py tools/extract/leonida/heights.py tools/extract/tests