Probe 22 points with the USGS 3DEP point service: walkable surfaces span 115.65-119.59 m, a 3.94 m relief, so elevation data is required by the AGENTS rule. Grade falls ~3.3 m eastward over the 279 m block (1.2%) and the north sidewalk sits 0.4-0.7 m above the south. 3DEP 1 m is public domain and becomes the chosen source; the tile itself is still to be downloaded. Factored the shared OSM helpers into Geo/scripts/osm_common.py so the street profile and the elevation probe read the same geometry.
120 lines
5.0 KiB
Python
Executable File
120 lines
5.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Street profile for a block: frontage setbacks, carriageway width, crossings.
|
|
|
|
Usage:
|
|
Geo/scripts/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()
|