ShitboxInfinity/tools/osm_xml_to_json.py
type-two 7fd11f3806 Four more Brisbane levels + FLUX textures: these scenes pop
southbank, albert_parklands, story_bridge, kangaroo_point + queen_st_mall
rebuilt textured. Level builder now does: FLUX facade/road/grass/water/rock/
steel materials with box-projected UVs (roofs split to plain grey), parks,
multipolygon river water (solid, cars skim it), tree nodes as low-poly cones,
natural=cliff walls, and a procedural steel cantilever truss along a named
bridge way (Story Bridge / Bradfield Highway, 338m). osm_xml_to_json.py
assembles water relations; osm_level_args.py gains trace: mode for race paths
that follow one iconic road. 11 textures via Cloudflare Workers AI
flux-1-schnell (tools/flux_texture.sh, creds from .env never printed).

Data (c) OpenStreetMap contributors, ODbL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 23:48:55 +10:00

83 lines
3.3 KiB
Python

#!/usr/bin/env python3
"""Convert an OSM API XML dump to overpass-style JSON for build_level_osm.py.
Usage: osm_xml_to_json.py <in.xml> <out.json>
Emits ways (tags+nodes+geometry), tree nodes, and assembles water multipolygon
relations (the Brisbane River is a relation, not a way) into synthetic closed
ways tagged natural=water.
"""
import json
import sys
import xml.etree.ElementTree as ET
root = ET.parse(sys.argv[1]).getroot()
nodes = {}
elements = []
for n in root.iter("node"):
nid = int(n.get("id"))
lat, lon = float(n.get("lat")), float(n.get("lon"))
nodes[nid] = (lat, lon)
tags = {t.get("k"): t.get("v") for t in n.findall("tag")}
if tags.get("natural") == "tree":
elements.append({"type": "node", "id": nid, "tags": tags, "lat": lat, "lon": lon})
ways = {}
for w in root.iter("way"):
wid = int(w.get("id"))
tags = {t.get("k"): t.get("v") for t in w.findall("tag")}
nds = [int(nd.get("ref")) for nd in w.findall("nd")]
if not all(r in nodes for r in nds) or len(nds) < 2:
continue
ways[wid] = nds
if any(tags.get(k) for k in ("building", "highway", "leisure", "landuse", "natural", "water", "waterway")):
elements.append({"type": "way", "id": wid, "tags": tags, "nodes": nds,
"geometry": [{"lat": nodes[r][0], "lon": nodes[r][1]} for r in nds]})
# water multipolygons: join outer member ways into rings
def assemble(rings_nodes):
segs = [list(s) for s in rings_nodes if len(s) >= 2]
rings = []
while segs:
ring = segs.pop()
grew = True
while grew and ring[0] != ring[-1]:
grew = False
for i, s in enumerate(segs):
if s[0] == ring[-1]:
ring += s[1:]
elif s[-1] == ring[-1]:
ring += list(reversed(s))[1:]
elif s[-1] == ring[0]:
ring = s + ring[1:]
elif s[0] == ring[0]:
ring = list(reversed(s)) + ring[1:]
else:
continue
segs.pop(i)
grew = True
break
if ring[0] == ring[-1] and len(ring) > 3:
rings.append(ring)
return rings
synth = 1
for r in root.iter("relation"):
tags = {t.get("k"): t.get("v") for t in r.findall("tag")}
if not (tags.get("natural") == "water" or tags.get("water") or tags.get("waterway") == "riverbank"):
continue
outers = [ways[int(m.get("ref"))] for m in r.findall("member")
if m.get("type") == "way" and m.get("role") in ("outer", "") and int(m.get("ref")) in ways]
for ring in assemble(outers):
elements.append({"type": "way", "id": 10**10 + synth, "tags": {"natural": "water"},
"nodes": ring, "geometry": [{"lat": nodes[n][0], "lon": nodes[n][1]} for n in ring]})
synth += 1
json.dump({"elements": elements}, open(sys.argv[2], "w"))
b = sum(1 for e in elements if e.get("tags", {}).get("building"))
h = sum(1 for e in elements if e.get("tags", {}).get("highway"))
w = sum(1 for e in elements if e.get("tags", {}).get("natural") == "water" or e.get("tags", {}).get("water"))
t = sum(1 for e in elements if e["type"] == "node")
print("converted: %d buildings, %d highways, %d water polys, %d trees" % (b, h, w, t))