Files
gig-line/Geo/tools/block_report.py
T
zwitschi 386d4bee78 geo: one tool directory, one unit boundary, per-asset provenance
Geo/tools/ is now every geographic script, with Geo/README.md documenting each
one; the Blender-side builders moved beside their outputs in Art/blender/*/ and
import their helpers from Geo/tools/osm_common.py.

That module gains to_unreal_local() and to_blender_local(): the only place in the
repository that multiplies metres by 100, so a second silent scale cannot be
introduced. block_height_report.py adds height provenance for the block — 3 of
15 buildings have no height data and render at a placeholder 6.4 m.
asset_metadata.py writes and audits <asset>.geo.json sidecars; four now exist
(hero-facade, terrain, street, blockout) and the audit recomputes each centre from
the origin, failing anything that disagrees.
2026-11-07 12:28:26 +01:00

104 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Report streets, boundary intersections and the demo building set for an OSM extract.
Usage:
Geo/tools/block_report.py Geo/raw/osm-api-map-2026-11-06.osm \
--blvd "Hollywood Boulevard" --west "North Orange Drive" --east "North Highland Avenue"
Reads a plain .osm XML file (the output of the OSM API `map` endpoint or an
Overpass export). No network access, no dependencies beyond the standard library.
Output: the boulevard's block span in metres, the two boundary intersections, the
buildings on the boulevard frontage split by side (even/odd is not trusted —
latitude relative to the centreline is), and each building's distance from the
centreline. Buildings behind the frontage are listed but marked as off-buffer.
"""
import argparse
import math
import xml.etree.ElementTree as ET
EARTH_R = 6378137.0
def local_metres(lat, lon, lat0, lon0):
x = math.radians(lon - lon0) * EARTH_R * math.cos(math.radians(lat0))
y = math.radians(lat - lat0) * EARTH_R
return x, y
def load(path):
root = ET.parse(path).getroot()
nodes = {n.get("id"): (float(n.get("lat")), float(n.get("lon"))) for n in root.findall("node")}
ways = []
for w in root.findall("way"):
tags = {c.get("k"): c.get("v") for c in w.findall("tag")}
ways.append((w.get("id"), tags, [nd.get("ref") for nd in w.findall("nd")]))
return nodes, ways
def refs_by_name(ways, name):
return {ref for _, tags, refs in ways if tags.get("name") == name for ref in refs}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("osm")
ap.add_argument("--blvd", default="Hollywood Boulevard")
ap.add_argument("--west", default="North Orange Drive")
ap.add_argument("--east", default="North Highland Avenue")
ap.add_argument("--buffer", type=float, default=50.0, help="north-side buffer in metres, from the centreline")
args = ap.parse_args()
nodes, ways = load(args.osm)
blvd = refs_by_name(ways, args.blvd)
if not blvd:
raise SystemExit(f"no highway named {args.blvd!r} in {args.osm}")
ends = {}
for label, street in (("west", args.west), ("east", args.east)):
inter = refs_by_name(ways, street) & blvd
pts = sorted(nodes[i][1] for i in inter if i in nodes)
if not pts:
raise SystemExit(f"no intersection between {args.blvd!r} and {street!r}")
ends[label] = pts[0] if label == "west" else pts[-1]
print(f"{street} x {args.blvd}: {len(pts)} shared node(s), lon {pts[0]:.6f}..{pts[-1]:.6f}")
lo_min, lo_max = ends["west"], ends["east"]
lat0 = sum(nodes[i][0] for i in blvd if lo_min <= nodes[i][1] <= lo_max) / max(
1, len([i for i in blvd if lo_min <= nodes[i][1] <= lo_max])
)
span = math.radians(lo_max - lo_min) * EARTH_R * math.cos(math.radians(lat0))
print(f"block span: {span:.1f} m of frontage, centreline lat {lat0:.6f}\n")
rows = []
for wid, tags, refs in ways:
if "building" not in tags:
continue
pts = [nodes[r] for r in refs if r in nodes]
if not pts:
continue
lat = sum(p[0] for p in pts) / len(pts)
lon = sum(p[1] for p in pts) / len(pts)
if not (lo_min - 0.00005 <= lon <= lo_max + 0.00005):
continue
side = "S" if lat < lat0 else "N"
_, y = local_metres(lat, lon, lat0, lon)
rows.append((side, abs(y), lon, wid, tags))
rows.sort(key=lambda r: r[2])
for side, dist, lon, wid, tags in rows:
if side == "S":
flag = ""
else:
flag = "" if dist <= args.buffer else " (outside buffer)"
print(
f"{side} way {wid:>10} {tags.get('addr:housenumber', '-'):>11} "
f"{tags.get('addr:street', '')[:22]:22} h={tags.get('height', '-'):>5} "
f"lv={tags.get('building:levels', '-'):>3} d={dist:5.1f}m {tags.get('name', '')[:30]}{flag}"
)
if __name__ == "__main__":
main()