Inventory the sibling security-game project and copy only the evidence: nine licensed photos with their sidecar provenance into Art/reference/hollywood/, now LFS-tracked. Add the block-wide OSM extract with a manifest, resolve the demo bounds (Orange Dr -118.341434 .. Highland Ave -118.338718, 279 m) and the south/north building sets, and add Geo/scripts/block_report.py to reproduce it. The sibling docs place the El Capitan Theatre west of 6840; OSM geometry and the house numbering put it east. Recorded with the two unresolved measurement conflicts rather than averaged.
104 lines
3.9 KiB
Python
Executable File
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/scripts/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()
|