Disable PA on/off ramps; restore mainline to pre-ramp state

Three iterations of the Daikoku PA ramps never read as a real
interchange. Measured why, rather than tuning further:

  * the entry and exit decks INTERSECTED -- 2547 sample pairs within
    30 studs horizontally at <12 studs dY, worst case 0.5 studs apart
    at dY=0.0. On screen that is the bowtie tangle of white rails.
  * both real Daikoku ramps turn the SAME way (clockwise; the guide
    video says "right, right"). This build mirrored them into each
    other.
  * the real arc wraps OUTSIDE the lot and forms its north/east
    boundary. This build cut across the apron interior.

Gated behind BUILD_PA_RAMPS=false rather than deleted -- the radius
was already correct (470 studs vs an OSM-measured mean of 469) and
the merge lands tangent at 0.1-0.2 deg divergence. Both worth keeping.

Also removed two leftovers that would have outlived the ramps:

  * rampOpen punched holes in the mainline's outer parapet, railing
    and kill wall for ramps that no longer exist -- a barrier gap with
    a drop behind it. Sealed via RAMP_EDGE_OPENINGS=false. Verified:
    Barrier/EdgeRail/KillWall = 1646 each = 823 samples x 2 edges,
    0 road samples without a guard within 90 studs.
  * gore zebra hatching painted 1024 bars + 64 boundary lines marking
    a junction that is not there. Sealed via RAMP_GORE_MARKINGS=false.

Verified after a cache-busting fresh rebuild (Studio's require() cache
otherwise serves the previous map and reports a false pass):
  parts 22628 -> 15988
  all ramp part classes = 0
  Road = 823 (unchanged), spawn pad + PASpawn intact
  clip probe TRUSTWORTHY (control +11/-0), CLIPPING=0

Research and rebuild spec kept for when this is revisited:
reference/ramps/BUILD_SPEC.md, INDEX.md (10 CC-licensed Commons
photos), docs/daikoku-reference-spec.md (OSM circle fits).
This commit is contained in:
2026-08-03 21:15:25 -04:00
parent 1d23c187ad
commit c297b20aed
34 changed files with 2206 additions and 46 deletions
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""Pull real Daikoku JCT / PA geometry from OpenStreetMap via Overpass.
Outputs measured, real-world numbers:
- PA service-area polygon (extent, area)
- Each ramp/link way: length, turn direction, fitted circle radius
- Bridge levels present
All distances in metres. Local ENU projection about the PA centre so we can
fit circles in a flat plane without map-projection distortion.
"""
import json, math, urllib.request, urllib.parse, sys
LAT0, LON0 = 35.46246657, 139.68149001 # user's Google Earth point
R_EARTH = 6378137.0
# Generous bbox around the whole interchange
S, W, N, E = 35.4545, 139.6700, 35.4705, 139.6935
Q = f"""
[out:json][timeout:120];
(
way["highway"]({S},{W},{N},{E});
way["amenity"="parking"]({S},{W},{N},{E});
way["highway"="services"]({S},{W},{N},{E});
way["highway"="rest_area"]({S},{W},{N},{E});
);
out body geom;
"""
ENDPOINTS = [
"https://overpass-api.de/api/interpreter",
"https://overpass.kumi.systems/api/interpreter",
]
def fetch():
last = None
for ep in ENDPOINTS:
try:
data = urllib.parse.urlencode({"data": Q}).encode()
req = urllib.request.Request(ep, data=data,
headers={"User-Agent": "daikoku-ref/1.0"})
with urllib.request.urlopen(req, timeout=150) as r:
return json.loads(r.read().decode())
except Exception as e:
last = f"{ep}: {e}"
print(f" endpoint failed {last}", file=sys.stderr)
raise SystemExit(f"all overpass endpoints failed: {last}")
def enu(lat, lon):
"""Local east/north metres relative to (LAT0, LON0)."""
x = math.radians(lon - LON0) * R_EARTH * math.cos(math.radians(LAT0))
y = math.radians(lat - LAT0) * R_EARTH
return x, y
def polyline_len(pts):
return sum(math.dist(pts[i], pts[i + 1]) for i in range(len(pts) - 1))
def fit_circle(pts):
"""Algebraic (Kasa) circle fit. Returns (cx, cy, R, rms_residual)."""
n = len(pts)
if n < 3:
return None
sx = sum(p[0] for p in pts) / n
sy = sum(p[1] for p in pts) / n
u = [p[0] - sx for p in pts]
v = [p[1] - sy for p in pts]
Suu = sum(a * a for a in u); Svv = sum(b * b for b in v)
Suv = sum(u[i] * v[i] for i in range(n))
Suuu = sum(a ** 3 for a in u); Svvv = sum(b ** 3 for b in v)
Suvv = sum(u[i] * v[i] * v[i] for i in range(n))
Svuu = sum(v[i] * u[i] * u[i] for i in range(n))
d = 2 * (Suu * Svv - Suv * Suv)
if abs(d) < 1e-9:
return None
uc = (Svv * (Suuu + Suvv) - Suv * (Svvv + Svuu)) / d
vc = (Suu * (Svvv + Svuu) - Suv * (Suuu + Suvv)) / d
cx, cy = uc + sx, vc + sy
rs = [math.dist(p, (cx, cy)) for p in pts]
R = sum(rs) / len(rs)
rms = math.sqrt(sum((r - R) ** 2 for r in rs) / len(rs))
return cx, cy, R, rms
def total_turn(pts):
"""Signed cumulative heading change in degrees. +ccw / -cw."""
tot = 0.0
for i in range(1, len(pts) - 1):
a1 = math.atan2(pts[i][1] - pts[i - 1][1], pts[i][0] - pts[i - 1][0])
a2 = math.atan2(pts[i + 1][1] - pts[i][1], pts[i + 1][0] - pts[i][0])
d = math.degrees(a2 - a1)
while d > 180: d -= 360
while d < -180: d += 360
tot += d
return tot
def main():
data = fetch()
els = data.get("elements", [])
print(f"overpass returned {len(els)} ways\n")
ramps, areas = [], []
for el in els:
if el.get("type") != "way" or "geometry" not in el:
continue
t = el.get("tags", {})
pts = [enu(g["lat"], g["lon"]) for g in el["geometry"]]
if len(pts) < 2:
continue
rec = {"id": el["id"], "tags": t, "pts": pts,
"len": polyline_len(pts)}
hw = t.get("highway", "")
if hw in ("services", "rest_area") or t.get("amenity") == "parking":
areas.append(rec)
elif hw.endswith("_link") or t.get("oneway") == "yes":
ramps.append(rec)
# ---- PA area ----
print("=" * 70)
print("PARKING / SERVICE AREA POLYGONS")
print("=" * 70)
for a in sorted(areas, key=lambda r: -r["len"])[:6]:
xs = [p[0] for p in a["pts"]]; ys = [p[1] for p in a["pts"]]
w = max(xs) - min(xs); h = max(ys) - min(ys)
nm = a["tags"].get("name", a["tags"].get("name:en", "-"))
print(f" way {a['id']:>12} {a['tags'].get('highway') or a['tags'].get('amenity'):<10}"
f" extent {w:6.0f} x {h:6.0f} m perim {a['len']:7.0f} m {nm}")
print(f" E {min(xs):7.0f}..{max(xs):7.0f} N {min(ys):7.0f}..{max(ys):7.0f}")
# ---- curved ramps: the loops ----
print()
print("=" * 70)
print("CURVED RAMPS (|turn| >= 120 deg) -- the helix loops")
print("=" * 70)
print(f" {'way id':>12} {'len(m)':>7} {'turn':>8} {'radius(m)':>10} {'rms':>6} layer/bridge")
loops = []
for r in ramps:
turn = total_turn(r["pts"])
if abs(turn) < 120 or r["len"] < 60:
continue
fc = fit_circle(r["pts"])
if not fc:
continue
cx, cy, R, rms = fc
if R > 400: # not a tight loop
continue
loops.append((r, turn, cx, cy, R, rms))
for r, turn, cx, cy, R, rms in sorted(loops, key=lambda z: -abs(z[1])):
t = r["tags"]
lb = f"layer={t.get('layer','-')} bridge={t.get('bridge','-')}"
print(f" {r['id']:>12} {r['len']:7.0f} {turn:7.0f}\u00b0 {R:10.1f} {rms:6.1f} {lb}")
print(f" centre E {cx:7.0f} N {cy:7.0f} "
f"{'CCW/left' if turn>0 else 'CW/right'}")
if loops:
rr = [z[4] for z in loops]
print(f"\n loop radius: min {min(rr):.0f} m max {max(rr):.0f} m "
f"mean {sum(rr)/len(rr):.0f} m")
print(f" in studs (1 stud = 0.28 m): mean {sum(rr)/len(rr)/0.28:.0f} studs")
# ---- layers present ----
print()
print("=" * 70)
print("DECK LAYERS (bridge ways)")
print("=" * 70)
lay = {}
for r in ramps:
if r["tags"].get("bridge"):
L = r["tags"].get("layer", "0")
lay.setdefault(L, []).append(r["len"])
for L in sorted(lay, key=lambda s: int(s) if s.lstrip("-").isdigit() else 0):
v = lay[L]
print(f" layer {L:>3}: {len(v):3d} bridge ways, total {sum(v):8.0f} m")
if __name__ == "__main__":
main()