feat(extract): Jefferson Avenue als gerade Straße statt der Gasse

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Sonnet 5 committed 2026-09-16 07:28:27 +02:00
1 parent d756eccd77
commit cfc9fc0690
2 files changed
+125 -2

No files matched your search

+63 -2
View File
@@ -10,12 +10,14 @@ from __future__ import annotations
import json import json
import math import math
import tomllib import tomllib
from dataclasses import dataclass, field from dataclasses import dataclass, field, replace
from pathlib import Path from pathlib import Path
from shapely.geometry import LineString, Polygon from shapely.geometry import LineString, Point, Polygon
from shapely.ops import unary_union from shapely.ops import unary_union
from .heights import PALM_KERB
class ParkError(ValueError): class ParkError(ValueError):
"""Formfehler in der Parkdatei oder eine Welt, zu der sie nicht mehr passt: der Lauf bricht ab.""" """Formfehler in der Parkdatei oder eine Welt, zu der sie nicht mehr passt: der Lauf bricht ab."""
@@ -424,3 +426,62 @@ def _points(geometry) -> list[tuple[float, float]]:
elif g.geom_type in ("LineString", "LinearRing"): elif g.geom_type in ("LineString", "LinearRing"):
out.extend(g.coords) out.extend(g.coords)
return out return out
# --- Welt verändern ----------------------------------------------------------------------------
MIN_CENTERLINE = 15.0 # m, wie centerlines() in vectors.py: kürzere Stummel setzten Laternen auf die Kreuzung
MIN_ROAD_AREA = 20.0 # m², wie road_polygons() in vectors.py
@dataclass
class World:
"""Weltgeometrie zwischen Extraktion und Chunk-Ausgabe, in Welt-x/Welt-y."""
land: list[Polygon]
surfaces: list[tuple[Polygon, str]]
roads: list[tuple[Polygon, bool]]
centerlines: list[LineString]
buildings: list[tuple[Polygon, dict, bool]] # Grundriss, {"height", "source"}, spekulativ
palms: list[tuple[float, float]]
paths: list[Polygon] = field(default_factory=list)
def _lines(geometry, min_length: float = MIN_CENTERLINE) -> list[LineString]:
return [g for g in _pieces(geometry) if g.geom_type == "LineString" and g.length >= min_length]
def apply_street(world: World, street: Street) -> World:
"""Gebäude und Gasse weg, gerade Straße mit Mittellinie, frei gewordene Fläche wird Stadtfläche."""
cut = Polygon(street.alley_cut)
def hit(building) -> bool:
return any(building[0].contains(Point(p)) for p in street.remove_buildings)
removed = [b[0] for b in world.buildings if hit(b)]
buildings = [b for b in world.buildings if not hit(b)]
line = LineString([street.start, street.end])
corridor = line.buffer(street.width / 2, cap_style="flat")
kept, joined = [], [corridor]
for poly, bridge in world.roads:
rest = poly.difference(cut) if poly.intersects(cut) else poly
for piece in _polygons(rest, MIN_ROAD_AREA):
if not bridge and piece.intersects(corridor):
joined.append(piece)
else:
kept.append((piece, bridge))
roads = kept + [(piece, False) for piece in _polygons(unary_union(joined), MIN_ROAD_AREA)]
centerlines = [piece for l in world.centerlines
for piece in (_lines(l.difference(cut)) if l.intersects(cut) else [l])] + [line]
freed = unary_union(removed + [cut]).difference(corridor)
covered = [b[0] for b in buildings if b[0].intersects(freed)] + \
[poly for poly, kind in world.surfaces if kind == "urban" and poly.intersects(freed)]
if covered:
freed = freed.difference(unary_union(covered))
surfaces = world.surfaces + [(poly, "urban") for poly in _polygons(freed)]
palms = [p for p in world.palms if corridor.distance(Point(p)) >= PALM_KERB]
return replace(world, roads=roads, centerlines=centerlines, buildings=buildings,
surfaces=surfaces, palms=palms)
+62
View File
@@ -0,0 +1,62 @@
from shapely.geometry import LineString, Point, box
from shapely.ops import unary_union
from leonida.park import Street, World, apply_street
STREET = Street(start=(150.0, 660.0), end=(150.0, 320.0), width=12.0,
remove_buildings=((155.0, 615.0),),
alley_cut=((90.0, 639.0), (110.0, 639.0), (110.0, 341.0), (90.0, 341.0)))
def world():
"""Querstraßen bei y 640–660 und 320–340, dazwischen eine Gasse bei x 95–105."""
removed = box(140, 600, 170, 630)
return World(
land=[box(-100, 200, 300, 800)],
surfaces=[(box(0, 340, 95, 640), "urban"), (box(105, 340, 200, 640).difference(removed), "urban")],
roads=[(unary_union([box(0, 640, 200, 660), box(0, 320, 200, 340), box(95, 340, 105, 640)]), False)],
centerlines=[LineString([(0, 650), (200, 650)]), LineString([(0, 330), (200, 330)]),
LineString([(100, 650), (100, 330)])],
buildings=[(removed, {"height": 20.9, "source": "district"}, False),
(box(20, 400, 60, 450), {"height": 10.8, "source": "district"}, False),
(box(84, 450, 93, 470), {"height": 8.0, "source": "district"}, False)],
palms=[(150.0, 500.0), (60.0, 500.0), (140.0, 500.0)],
)
def on_road(result, x, y):
return any(poly.contains(Point(x, y)) for poly, _ in result.roads)
def test_buildings_under_the_street_go_and_the_others_stay():
result = apply_street(world(), STREET)
assert [b[0].bounds for b in result.buildings] == [(20.0, 400.0, 60.0, 450.0), (84.0, 450.0, 93.0, 470.0)]
def test_alley_is_cut_and_the_straight_street_joins_both_cross_streets():
result = apply_street(world(), STREET)
assert not on_road(result, 100, 500)
assert on_road(result, 150, 500) and not on_road(result, 143, 500)
assert on_road(result, 100, 650) and on_road(result, 100, 330)
assert on_road(result, 150, 650) and on_road(result, 150, 330)
def test_alley_centerline_and_short_stubs_go_and_the_street_gets_its_own():
result = apply_street(world(), STREET)
assert all(line.distance(Point(100, 500)) > 40 for line in result.centerlines)
assert LineString([(150, 660), (150, 320)]) in result.centerlines
assert all(line.length >= 15 for line in result.centerlines)
def test_freed_ground_becomes_urban_but_not_under_the_street_buildings_or_old_urban():
result = apply_street(world(), STREET)
new = [poly for poly, kind in result.surfaces[2:] if kind == "urban"]
assert any(poly.contains(Point(100, 500)) for poly in new) # alte Gasse
assert any(poly.contains(Point(165, 615)) for poly in new) # alter Grundriss neben der Straße
assert not any(poly.contains(Point(150, 615)) for poly in new) # unter der neuen Straße
assert not any(poly.contains(Point(91, 460)) for poly in new) # stehendes Gebäude
assert not any(poly.contains(Point(92, 500)) for poly in new) # war schon Stadtfläche
def test_palms_on_the_new_street_go():
assert apply_street(world(), STREET).palms == [(60.0, 500.0), (140.0, 500.0)]