Files
gig-line/Geo/tools/block_height_report.py
T
zwitschi b205a90db0 fix(geo): classify roof/shelter volumes so no block height is a guess
The three ways the height report flagged as guessed-height buildings are not
buildings: two are building=roof at layer=1 (elevated canopies) and one is an
amenity=shelter (public_transport). The report's '2 levels x 3.2 m' default
invented the 6.4 m figure. block_height_report.py now classifies them explicitly;
the block now reports 0 guessed heights, all 15 volumes having a real height tag,
a levels derivation, or a non-building classification.

Also records the measured hero proportions (colonnade 6.90 m = 47.6% of the
14.50 m envelope) for the open proportion task, which needs photo review.
2026-11-08 16:20:37 +01:00

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())