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:
1 parent
d756eccd77
commit
cfc9fc0690
2 files changed
+125
-2
No files matched your search
@@ -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)
|
||||||
@@ -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)]
|
||||||
Reference in new issue
Block a user