Merged PR #24: tooling + architecture-upkeep rules, tool tests Co-authored-by: Zwitschi <zwitschi82@gmail.com>
88 lines
3.3 KiB
Python
Executable File
88 lines
3.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Report which block buildings have real height data and which are guesses.
|
|
|
|
Usage:
|
|
Geo/tools/block_height_report.py Geo/raw/osm-api-map-2026-11-06.osm
|
|
|
|
`height` and `building:levels` are the only height evidence OSM carries. Where
|
|
neither exists, Blosm falls back to `defaultLevels * levelHeight`, which is a
|
|
placeholder: the demo renders a constant-height block instead of a real skyline.
|
|
This tool lists every building in the demo bounds with its height source, so the
|
|
substitutions are visible and measurable rather than buried in the model.
|
|
|
|
Exit code is 0 either way; the count of guessed buildings is the deliverable.
|
|
"""
|
|
|
|
import argparse
|
|
import math
|
|
import sys
|
|
|
|
import osm_common as oc
|
|
|
|
LEVEL_HEIGHT = 3.2
|
|
|
|
|
|
def classify_height(tags, level_height=LEVEL_HEIGHT):
|
|
"""Return (height_m, source) for one OSM way's tags.
|
|
|
|
Extracted from `main()` so the classification is testable: the roof/shelter
|
|
rule and the "height tag beats levels beats guess" order are the parts that
|
|
actually decide what the blockout should render.
|
|
"""
|
|
if tags.get("building") == "roof" or tags.get("amenity") == "shelter":
|
|
kind = "roof" if tags.get("building") == "roof" else "shelter"
|
|
return 0.0, f"not a building volume ({kind})"
|
|
if tags.get("height"):
|
|
return float(tags["height"]), "height tag"
|
|
if tags.get("building:levels"):
|
|
return (float(tags["building:levels"]) * level_height,
|
|
f"levels x {level_height} m")
|
|
return 2 * level_height, f"GUESS: default 2 levels x {level_height} m"
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("osm")
|
|
args = ap.parse_args()
|
|
nodes, ways = oc.load(args.osm)
|
|
|
|
line, lat0, lon0, lengths, block_len = oc.block_extent(
|
|
nodes, ways, "Hollywood Boulevard", "North Orange Drive", "North Highland Avenue")
|
|
|
|
def in_block(pts):
|
|
if min(p[1] for p in pts) > line[-1][1] or max(p[1] for p in pts) < line[0][1]:
|
|
return False
|
|
return True
|
|
|
|
rows = []
|
|
for wid, tags, refs in ways:
|
|
if "building" not in tags:
|
|
continue
|
|
pts = oc.way_points(nodes, refs)
|
|
if not pts or not in_block(pts):
|
|
continue
|
|
height, source = classify_height(tags)
|
|
nearest = min(oc.project(p, line, lat0, lon0, lengths)[0] for p in pts)
|
|
side = "S" if oc.project(pts[0], line, lat0, lon0, lengths)[2] < 0 else "N"
|
|
rows.append((side, nearest, wid, tags.get("name", "") or tags.get("addr:housenumber", ""),
|
|
height, source))
|
|
|
|
rows.sort(key=lambda r: (r[0], r[1]))
|
|
guessed = [r for r in rows if r[5].startswith("GUESS")]
|
|
print(f"{len(rows)} buildings in the block, {len(guessed)} with a guessed height\n")
|
|
for side, setback, wid, name, height, source in rows:
|
|
flag = (" <-- guessed" if source.startswith("GUESS")
|
|
else (" <-- not a building volume" if source.startswith("not a building") else ""))
|
|
print(f"{side} way {wid:>10} setback {setback:5.1f} m height {height:5.1f} m "
|
|
f"({source:26}) {name[:34]}{flag}")
|
|
|
|
if guessed:
|
|
print("\nGuessed heights to replace from the reference photography:")
|
|
for side, setback, wid, name, height, source in guessed:
|
|
print(f" {side} way {wid} {name}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|