#!/usr/bin/env python3 """Compute Spawn + RacePath args for build_level_osm.py from Overpass JSON. Usage: osm_level_args.py 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 # second pass: divided carriageways never share nodes, so exact-endpoint # stitching leaves the twin deck behind. Join leftovers whose end lies # within 60 m of the chain's ends -- a motorway loop closes up one # carriageway and back down the other. def _gd(a, b): return math.hypot((a["lon"] - b["lon"]) * 111320 * math.cos(math.radians(a["lat"])), (a["lat"] - b["lat"]) * M_LAT) grew = True while grew and segs: grew = False for i, s in enumerate(segs): for rev in (False, True): g = list(reversed(s["geo"])) if rev else list(s["geo"]) if _gd(g[0], chain["geo"][-1]) < 60.0: chain["geo"] = chain["geo"] + g elif _gd(g[-1], chain["geo"][0]) < 60.0: chain["geo"] = g + chain["geo"] else: continue segs.pop(i) grew = True break if grew: 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))