Merged PR #23: block height classification + hero proportions recorded Co-authored-by: Zwitschi <zwitschi82@gmail.com>
84 lines
3.3 KiB
Python
Executable File
84 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 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
|
|
tag_height = tags.get("height")
|
|
levels = tags.get("building:levels")
|
|
# Non-building volumes must not be extruded as two-storey blocks: a
|
|
# `building=roof` is an elevated slab and a public-transport shelter is a
|
|
# small street structure. Treating them as "2 levels x 3.2 m" produced a
|
|
# 6.4 m block for a bus shelter and a canopy.
|
|
if tags.get("building") == "roof" or tags.get("amenity") == "shelter":
|
|
kind = "roof" if tags.get("building") == "roof" else "shelter"
|
|
height, source = 0.0, f"not a building volume ({kind})"
|
|
elif tag_height:
|
|
height, source = float(tag_height), "height tag"
|
|
elif levels:
|
|
height, source = float(levels) * LEVEL_HEIGHT, f"levels x {LEVEL_HEIGHT} m"
|
|
else:
|
|
height, source = 2 * LEVEL_HEIGHT, f"GUESS: default 2 levels x {LEVEL_HEIGHT} m"
|
|
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())
|