The OSM pipeline pointed at actual mountains. Both levels ride the real terrarium DEM (Coot-tha's range tops at 287 m, Gravatt's at 196 -- matching the survey numbers) with the real roads draped and carved over it: Sir Samuel Griffith Drive winding up one side of town, Mount Gravatt Outlook Drive zigzagging up the other. Race mode is 3 laps of the mountain; the drag finder even discovered a 1080 m strip through Mount Gravatt's suburbs (Logan Road, of course it is). Pipeline upgrades earned along the way, all reusable: - trace: RacePath mode now STITCHES every OSM way sharing the name into one chain by shared endpoints (SSG Drive is 19 segments; tracing only the longest produced a fraction of the route) and samples densely -- 16 points cut hairpins cross-country and gridded race starts in a paddock. - 9th builder arg selects the raw-terrain texture. Bushland levels pass "grass" (tiled at 3 m -- 18 m tiling reads as macro-photography lawn blades under the car) which also enables forest fill: ~2600 extra gums, 65% biased to within ~150 m of roads, where the driver's eyes actually are. - amenity=parking polygons are now PAVED (draped asphalt) as well as stocked with parked shitboxes -- a summit lookout carpark is a burnout pad, not a dirt patch. Both summits come ready. - man_made mast/tower ways become tapered lattice towers with a cage and whip antenna, built from beams into the steel bucket. Mount Gravatt's telecom tower stands over its carpark. - Levels can carry their own sky: levels/<id>/sky_<time>.jpg overrides the global panorama for any time of day, including day (previously always procedural). Both mountains ship FLUX skies with the Brisbane skyline on the horizon; Gravatt also has a sunset one, because John's reference photo demanded it. Suite green at 11 levels. Next burnout spot: the Gateway Bridge, when the details arrive. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
4.4 KiB
Python
121 lines
4.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Compute Spawn + RacePath args for build_level_osm.py from Overpass JSON.
|
|
|
|
Usage: osm_level_args.py <overpass.json> <spawn_way_name> <loop_street1,street2,...>
|
|
|
|
Spawn: a point on the named way that is NOT inside any building footprint
|
|
(CBD malls have awnings/buildings mapped over them), bearing taken from that
|
|
point's own segment. RacePath: intersection nodes of consecutive loop streets.
|
|
Prints two shell-ready args: "lat,lon,bearing" and "lat,lon;lat,lon;..."
|
|
"""
|
|
import json
|
|
import math
|
|
import sys
|
|
|
|
data = json.load(open(sys.argv[1]))
|
|
spawn_name = sys.argv[2]
|
|
loop = sys.argv[3].split(",")
|
|
|
|
M_LAT = 110574.0
|
|
|
|
def xy(lat, lon, lat0):
|
|
return (lon * 111320.0 * math.cos(math.radians(lat0)), lat * M_LAT)
|
|
|
|
ways = {}
|
|
polys = []
|
|
for e in data["elements"]:
|
|
tags = e.get("tags", {})
|
|
name = tags.get("name")
|
|
if name and e.get("nodes") and e.get("geometry"):
|
|
ways.setdefault(name, []).append(e)
|
|
if tags.get("building") and e.get("geometry"):
|
|
polys.append([(g["lat"], g["lon"]) for g in e["geometry"]])
|
|
|
|
def inside(lat, lon, poly):
|
|
n = len(poly)
|
|
hit = False
|
|
for i in range(n - 1):
|
|
(y1, x1), (y2, x2) = poly[i], poly[i + 1]
|
|
if (y1 > lat) != (y2 > lat) and lon < (x2 - x1) * (lat - y1) / (y2 - y1) + x1:
|
|
hit = not hit
|
|
return hit
|
|
|
|
def in_any_building(lat, lon):
|
|
return any(inside(lat, lon, p) for p in polys)
|
|
|
|
sw = ways.get(spawn_name)
|
|
assert sw, "no way named %r" % spawn_name
|
|
candidates = []
|
|
for w in sw:
|
|
geo = w["geometry"]
|
|
for i in range(1, len(geo) - 1):
|
|
g = geo[i]
|
|
if in_any_building(g["lat"], g["lon"]):
|
|
continue
|
|
a, b = geo[i - 1], geo[i + 1]
|
|
bearing = math.degrees(math.atan2(
|
|
(b["lon"] - a["lon"]) * math.cos(math.radians(g["lat"])),
|
|
b["lat"] - a["lat"])) % 360
|
|
candidates.append((g["lat"], g["lon"], bearing))
|
|
assert candidates, "every point of %r is inside a building" % spawn_name
|
|
clat = sum(c[0] for c in candidates) / len(candidates)
|
|
clon = sum(c[1] for c in candidates) / len(candidates)
|
|
lat, lon, bearing = min(candidates, key=lambda c: (c[0] - clat) ** 2 + (c[1] - clon) ** 2)
|
|
print("%.6f,%.6f,%.1f" % (lat, lon, bearing))
|
|
|
|
def nodes_of(name):
|
|
out = {}
|
|
for w in ways.get(name, []):
|
|
for nid, g in zip(w["nodes"], w["geometry"]):
|
|
out[nid] = (g["lat"], g["lon"])
|
|
return out
|
|
|
|
if loop[0].startswith("trace:"):
|
|
# Stitch EVERY way of this name into one chain by shared endpoint nodes,
|
|
# then sample it; the curve closes itself back. A named road is usually
|
|
# 10-20 OSM way segments (Sir Samuel Griffith Drive is 19) and tracing
|
|
# only the longest one produced a fraction of the route.
|
|
tname = loop[0][6:]
|
|
assert tname in ways, "no way named %r" % tname
|
|
segs = [{"nodes": list(w["nodes"]), "geo": list(w["geometry"])} for w in ways[tname]]
|
|
chain = segs.pop(0)
|
|
grew = True
|
|
while grew and segs:
|
|
grew = False
|
|
for i, s in enumerate(segs):
|
|
if s["nodes"][0] == chain["nodes"][-1]:
|
|
chain["nodes"] += s["nodes"][1:]
|
|
chain["geo"] += s["geo"][1:]
|
|
elif s["nodes"][-1] == chain["nodes"][-1]:
|
|
chain["nodes"] += list(reversed(s["nodes"]))[1:]
|
|
chain["geo"] += list(reversed(s["geo"]))[1:]
|
|
elif s["nodes"][-1] == chain["nodes"][0]:
|
|
chain["nodes"] = s["nodes"][:-1] + chain["nodes"]
|
|
chain["geo"] = s["geo"][:-1] + chain["geo"]
|
|
elif s["nodes"][0] == chain["nodes"][0]:
|
|
chain["nodes"] = list(reversed(s["nodes"]))[:-1] + chain["nodes"]
|
|
chain["geo"] = list(reversed(s["geo"]))[:-1] + chain["geo"]
|
|
else:
|
|
continue
|
|
segs.pop(i)
|
|
grew = True
|
|
break
|
|
geo = chain["geo"]
|
|
# dense sampling: a mountain road sampled every 16th point cuts hairpins
|
|
# cross-country and grids the race start in a paddock
|
|
step = max(1, len(geo) // 48)
|
|
picked = geo[::step]
|
|
if picked[-1] != geo[-1]:
|
|
picked.append(geo[-1])
|
|
print(";".join("%.6f,%.6f" % (g["lat"], g["lon"]) for g in picked))
|
|
sys.exit(0)
|
|
|
|
corners = []
|
|
for i, street in enumerate(loop):
|
|
nxt = loop[(i + 1) % len(loop)]
|
|
shared = set(nodes_of(street)) & set(nodes_of(nxt))
|
|
assert shared, "no intersection %s x %s" % (street, nxt)
|
|
lat, lon = nodes_of(street)[sorted(shared)[0]]
|
|
corners.append("%.6f,%.6f" % (lat, lon))
|
|
print(";".join(corners))
|