Files
gig-line/Geo/tools/street_profile.py
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

120 lines
4.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Street profile for a block: frontage setbacks, carriageway width, crossings.
Usage:
Geo/tools/street_profile.py Geo/raw/osm-api-map-2026-11-06.osm \
--blvd "Hollywood Boulevard" --west "North Orange Drive" --east "North Highland Avenue"
Measures, in metres, from a plain .osm extract (standard library only):
* the boulevard centreline, stitched and clipped between the two boundary
intersections;
* each building's frontage setback: the distance from its nearest vertex to the
centreline, and which side of the road it is on;
* each mapped crossing (`footway=crossing`) across the boulevard: its length,
which is the carriageway width at that point, and its position along the block;
* the offset of the mapped sidewalk centrelines from the road centreline.
What it cannot measure: carriageway width where no crossing or kerb is mapped,
and sidewalk width, because OSM models sidewalks here as centre lines with no
width tag. Derive those from the numbers printed here and record them as
assumptions.
"""
import argparse
import osm_common as oc
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")
args = ap.parse_args()
nodes, ways = oc.load(args.osm)
line, lat0, lon0, lengths, block_len = oc.block_extent(
nodes, ways, args.blvd, args.west, args.east
)
print(f"centreline: {len(line)} vertices, {block_len:.1f} m between the boundary intersections\n")
print("crossings across the boulevard (length = carriageway width there):")
for wid, tags, refs in ways:
if tags.get("footway") != "crossing":
continue
pts = oc.way_points(nodes, refs)
if not pts:
continue
if min(p[1] for p in pts) > line[-1][1] or max(p[1] for p in pts) < line[0][1]:
continue
a = oc.to_xy(pts[0][0], pts[0][1], lat0, lon0)
b = oc.to_xy(pts[-1][0], pts[-1][1], lat0, lon0)
length = ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5
mid = (sum(p[0] for p in pts) / len(pts), sum(p[1] for p in pts) / len(pts))
dist, s, _ = oc.project(mid, line, lat0, lon0, lengths)
if not (0.0 <= s <= block_len) or dist > 4.0:
continue
print(
f" way {wid:>10} {length:6.1f} m across, at {s:6.1f} m along the block "
f"(offset {dist:4.1f} m) markings={tags.get('crossing:markings', '-'):6} "
f"signals={tags.get('crossing')}"
)
print("\nsidewalk centrelines near the block (offset from road centreline):")
south, north = [], []
for wid, tags, refs in ways:
if tags.get("footway") != "sidewalk":
continue
pts = oc.way_points(nodes, refs)
if not pts:
continue
if min(p[1] for p in pts) > line[-1][1] or max(p[1] for p in pts) < line[0][1]:
continue
offsets = [oc.project(p, line, lat0, lon0, lengths) for p in pts]
mean = sum(o[0] for o in offsets) / len(offsets)
along = max(o[1] for o in offsets) - min(o[1] for o in offsets)
if not (6.0 <= mean <= 20.0) or along < 30.0:
continue
(south if offsets[0][2] < 0 else north).append((mean, along, wid, tags.get("name", "")))
for label, rows in (("south side", south), ("north side", north)):
if not rows:
print(f" {label}: none mapped")
continue
rows.sort()
mean = sum(r[0] for r in rows) / len(rows)
print(f" {label}: {len(rows)} line(s), mean offset {mean:.1f} m, range {rows[0][0]:.1f}..{rows[-1][0]:.1f} m")
print("\nbuilding frontage setbacks (nearest vertex to centreline):")
rows = []
for wid, tags, refs in ways:
if "building" not in tags:
continue
pts = oc.way_points(nodes, refs)
if not pts:
continue
if min(p[1] for p in pts) > line[-1][1] or max(p[1] for p in pts) < line[0][1]:
continue
proj = [oc.project(p, line, lat0, lon0, lengths) for p in pts]
nearest = min(proj, key=lambda o: o[0])
along = [o[1] for o in proj]
rows.append((nearest[0], nearest[2], wid, tags, max(along) - min(along)))
rows.sort()
for dist, side, wid, tags, frontage in rows:
side_name = "S" if side < 0 else "N"
print(
f" {side_name} way {wid:>10} {tags.get('addr:housenumber', '-'):>11} "
f"setback {dist:5.1f} m, extent {frontage:5.1f} m {tags.get('name', '')[:34]}"
)
print("\nassumptions to record, not measurements:")
print(" - lane width is not tagged; the crossing lengths above are the best carriageway")
print(" proxy, and crossing endpoints sit within about 1 m of the kerb line")
print(" - sidewalk width is not tagged; it is setback minus half carriageway")
if __name__ == "__main__":
main()