import hashlib import math from shapely.geometry import Point, Polygon DISTRICT_RANGES = { "Downtown": (20.0, 60.0), "Ocean Beach": (8.0, 25.0), "default": (6.0, 15.0), } LANDMARK_SNAP = 20.0 # Meter: so weit darf ein Wahrzeichenpunkt vom Grundriss entfernt liegen def _seeded_unit(seed: int, key: str) -> float: digest = hashlib.sha256(f"{seed}:{key}".encode()).digest() return int.from_bytes(digest[:8], "big") / 2 ** 64 def assign_heights(polys: list[Polygon], landmarks: list[dict], districts: list[dict], seed: int) -> list[dict]: out = [] for poly in polys: centroid = poly.centroid key = f"{centroid.x:.1f},{centroid.y:.1f}" hit = None for lm in landmarks: p = Point(lm["xyz"][0], lm["xyz"][1]) if poly.contains(p) or poly.distance(p) <= LANDMARK_SNAP: if hit is None or lm["height"] > hit["height"]: hit = lm if hit is not None: out.append({"height": float(hit["height"]), "source": "skyline"}) continue name = "default" best = math.inf for d in districts: dist = math.dist((centroid.x, centroid.y), (d["x"], d["y"])) if dist < best: best, name = dist, d["name"] if d["name"] in DISTRICT_RANGES else "default" lo, hi = DISTRICT_RANGES[name] # Kleine Grundrisse bleiben niedriger: Fläche skaliert den oberen Rand. area_factor = min(1.0, poly.area / 400.0) height = lo + (hi - lo) * _seeded_unit(seed, key) * (0.4 + 0.6 * area_factor) out.append({"height": round(height, 1), "source": "district"}) return out PALM_SPACING = 12.0 # Meter entlang der Mittellinie PALM_BEACH_RANGE = 40.0 # Meter Abstand zur nächsten Sandfläche 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, land): """Erster Versatz von innen nach außen, der Fahrbahn und Gebäude freihält und auf Land bleibt (Ruling 26: sonst schiebt der Versatz Küstenpalmen ins Wasser).""" 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 if land is not None and not land.contains(q): continue return [(round(q.x, 2), round(q.y, 2))] return [] def palm_points(sand_polys, park_polys, road_lines, building_polys, road_polys, land_polys, seed: int): """Palmen entlang strandnaher Mittellinien und verstreut in Parks.""" from shapely.ops import unary_union sand = unary_union(sand_polys) if sand_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 land = unary_union(land_polys) if land_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 = [] for line in road_lines: if sand is None or line.distance(sand) > PALM_BEACH_RANGE: continue # Start bei halber Teilung: die Mittellinien sind an Kreuzungen getrennt, ein # Start bei 0 setzt je Ast eine Palme in denselben Punkt. pos = min(PALM_SPACING / 2, line.length / 2) while pos <= line.length: p = line.interpolate(pos) for side in ("left", "right"): points.extend(_verge_point(line, p, side, roads, buildings, land)) pos += PALM_SPACING for park in park_polys: minx, miny, maxx, maxy = park.bounds n = int(park.area // 250) for k in range(n): u = _seeded_unit(seed, f"park:{minx:.0f}:{k}:x") v = _seeded_unit(seed, f"park:{miny:.0f}:{k}:y") q = Point(minx + u * (maxx - minx), miny + v * (maxy - miny)) if park.contains(q) and (blocked is None or q.distance(blocked) > PALM_CLEARANCE): points.append((round(q.x, 2), round(q.y, 2))) return list(dict.fromkeys(points))