Spaces:
Running
Running
File size: 10,385 Bytes
10fe5f1 3c22a41 10fe5f1 3c22a41 10fe5f1 b4bc313 10fe5f1 3c22a41 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 | """Right-of-way estimation geometry β the street-aware extension and its fallbacks.
`build_estimation_geometry` is pure; we drive it with synthetic street/neighbor
callbacks over a real Omaha UTM square so the WGS84<->UTM math is faithful. These
lock in: exact extension area, corner-lot dual frontage, phantom removal via
neighbor subtraction (no-street fallback), and no bleed into an attached unit.
"""
from __future__ import annotations
import pytest
from pyproj import Transformer
from shapely.geometry import LineString
from shapely.ops import transform as shp_tf
from conftest import (
CENTER_LAT,
CENTER_LON,
OMAHA_CX,
OMAHA_CY,
SQFT_PER_SQM,
rect_utm,
square_utm,
utm_to_wgs,
)
from lawn_estimator.geometry import (
area_per_pixel_sqft,
build_estimation_geometry,
geometry_area_sqft,
geometry_exceeds_frame,
)
CRS = "EPSG:26914"
BUFFER_FT = 12.0
DIST_M = BUFFER_FT * 0.3048 # 3.6576 m
SIZE_M = 30.0
def _street(name, line_utm):
return (name, utm_to_wgs(line_utm))
def _streets_fn(*named_lines):
return lambda bounds: list(named_lines)
def _neighbors_fn(*polys_wgs):
return lambda bounds, exclude_id: list(polys_wgs)
# A horizontal centerline 10 m south of the parcel's south edge (within the 22 m
# street-detection window), and a vertical one 10 m east of the east edge.
SOUTH_STREET = _street("South St", LineString([(OMAHA_CX - 50, OMAHA_CY - 25),
(OMAHA_CX + 50, OMAHA_CY - 25)]))
EAST_STREET = _street("East St", LineString([(OMAHA_CX + 25, OMAHA_CY - 50),
(OMAHA_CX + 25, OMAHA_CY + 50)]))
def test_single_frontage_extension_exact_area():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
_, est_utm, area_sqft, extensions = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(), # no neighbors
fetch_streets=_streets_fn(SOUTH_STREET),
)
assert len(extensions) == 1
ext = extensions[0]
assert ext["street"] == "South St"
assert ext["street_facing"] is True
# One edge (30 m) extended by 12 ft (3.6576 m).
expected_ext = SIZE_M * DIST_M * SQFT_PER_SQM # ~1181.1 sqft
assert ext["area_sqft"] == pytest.approx(expected_ext, rel=2e-3)
expected_total = (SIZE_M * SIZE_M + SIZE_M * DIST_M) * SQFT_PER_SQM
assert area_sqft == pytest.approx(expected_total, rel=2e-3)
def test_corner_lot_gets_two_named_frontages_only():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
_, _, area_sqft, extensions = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(),
fetch_streets=_streets_fn(SOUTH_STREET, EAST_STREET),
)
# Two street-facing edges β two frontages; the north/west edges get nothing.
assert len(extensions) == 2
assert {e["street"] for e in extensions} == {"South St", "East St"}
per_edge = SIZE_M * DIST_M * SQFT_PER_SQM
assert sum(e["area_sqft"] for e in extensions) == pytest.approx(2 * per_edge, rel=2e-3)
def test_no_street_data_extends_all_then_neighbor_subtraction_removes_phantom():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
north_neighbor = utm_to_wgs(rect_utm(OMAHA_CX, OMAHA_CY + SIZE_M, SIZE_M, SIZE_M))
# Fallback path: no street source β extend every edge (~4 strips).
_, _, area_no, ext_no = build_estimation_geometry(
parcel, BUFFER_FT, CRS, fetch_neighbors=_neighbors_fn(), fetch_streets=None)
_, est_with, area_with, ext_with = build_estimation_geometry(
parcel, BUFFER_FT, CRS, fetch_neighbors=_neighbors_fn(north_neighbor), fetch_streets=None)
one_strip = SIZE_M * DIST_M * SQFT_PER_SQM # ~1181 sqft
# The north neighbor carves one phantom strip out of both the area and the report.
assert area_no - area_with == pytest.approx(one_strip, rel=5e-2)
assert (sum(e["area_sqft"] for e in ext_no) - sum(e["area_sqft"] for e in ext_with)
== pytest.approx(one_strip, rel=5e-2))
# And the estimate never bleeds into the neighbor lot.
to_utm = Transformer.from_crs("EPSG:4326", CRS, always_xy=True).transform
assert est_with.intersection(shp_tf(to_utm, north_neighbor)).area == pytest.approx(0.0, abs=1.0)
# No-street extensions are reported unverified (street_facing is None).
assert all(e["street"] is None and e["street_facing"] is None for e in ext_with)
def test_attached_unit_no_bleed_into_neighbor_parcel():
# A duplex is one parcel per unit; a narrow lot with an adjoining unit to the
# east must not extend into that unit even in the extend-all fallback.
parcel = utm_to_wgs(rect_utm(OMAHA_CX, OMAHA_CY, 10.0, 30.0))
east_unit_utm = rect_utm(OMAHA_CX + 10.0, OMAHA_CY, 10.0, 30.0) # shares the east edge
east_unit = utm_to_wgs(east_unit_utm)
_, est_utm, _, _ = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(east_unit), fetch_streets=None)
to_utm = Transformer.from_crs("EPSG:4326", CRS, always_xy=True).transform
neighbor_utm = shp_tf(to_utm, east_unit)
overlap = est_utm.intersection(neighbor_utm).area
assert overlap == pytest.approx(0.0, abs=1.0) # no measurable bleed
# ---------------------------------------------------------------------------
# Adaptive ROW-to-curb (row_to_curb flag): extend a deep-setback frontage toward
# the actual centerline (β the curb) instead of the flat 12 ft, floored at 12 ft
# (never shrinks a frontage) and capped at 25 ft (wide arterials can't blow up).
# ---------------------------------------------------------------------------
HALF_ROAD_FT = 15.0
CAP_FT = 25.0
HALF_ROAD_M = HALF_ROAD_FT * 0.3048 # 4.572 m β half a typical residential road
CAP_M = CAP_FT * 0.3048 # 7.62 m
def _south_street_at(offset_m, name="South St"):
"""Horizontal centerline `offset_m` south of the 30 m parcel's south edge, so
the south frontage's measured centerline distance is exactly `offset_m`. The
line spans only Β±12 m (< the 15 m half-width) so the side edges' nearest point
is an inward endpoint β cleanly rejected by the outward-side test, isolating a
single south frontage regardless of how close the street sits."""
y = OMAHA_CY - SIZE_M / 2 - offset_m
return _street(name, LineString([(OMAHA_CX - 12, y), (OMAHA_CX + 12, y)]))
def _front_ext(offset_m, *, row_to_curb):
"""(front extension sqft, total estimation sqft) for a lone south frontage
whose centerline sits `offset_m` out."""
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
_, _, area_sqft, extensions = build_estimation_geometry(
parcel, BUFFER_FT, CRS,
fetch_neighbors=_neighbors_fn(),
fetch_streets=_streets_fn(_south_street_at(offset_m)),
row_to_curb=row_to_curb,
road_half_width_ft=HALF_ROAD_FT,
row_curb_cap_ft=CAP_FT,
)
assert len(extensions) == 1 # only the south edge faces the street
return extensions[0]["area_sqft"], area_sqft
def test_row_to_curb_reaches_curb_on_deep_setback():
# Centerline 10 m out β curb β 10 β 4.572 = 5.428 m past the property line,
# well beyond the flat 12 ft (3.6576 m) strip.
ext, _ = _front_ext(10.0, row_to_curb=True)
expected = SIZE_M * (10.0 - HALF_ROAD_M) * SQFT_PER_SQM # ~1752.8 sqft
assert ext == pytest.approx(expected, rel=3e-3)
flat, _ = _front_ext(10.0, row_to_curb=False)
assert ext > flat # adaptive reaches farther than the flat strip
def test_row_to_curb_capped_on_very_deep_setback():
# Centerline 16 m out β 16 β 4.572 = 11.428 m would overshoot; capped at 25 ft.
ext, _ = _front_ext(16.0, row_to_curb=True)
expected = SIZE_M * CAP_M * SQFT_PER_SQM # ~2460.7 sqft
assert ext == pytest.approx(expected, rel=3e-3)
def test_row_to_curb_floors_at_flat_strip_on_shallow_setback():
# Centerline 5 m out β 5 β 4.572 = 0.428 m would shrink the strip; floored at
# 12 ft, so a shallow lot is unchanged from the flat behavior.
ext_on, _ = _front_ext(5.0, row_to_curb=True)
ext_off, _ = _front_ext(5.0, row_to_curb=False)
assert ext_on == pytest.approx(ext_off, rel=1e-6)
def test_row_to_curb_off_leaves_deep_setback_at_flat_strip():
# The flag gates the change: with it off, a deep-setback frontage stays the
# flat 12 ft strip (the estimation path must not move when off).
ext_off, area_off = _front_ext(10.0, row_to_curb=False)
flat = SIZE_M * DIST_M * SQFT_PER_SQM # ~1181.1 sqft
assert ext_off == pytest.approx(flat, rel=2e-3)
_, area_on = _front_ext(10.0, row_to_curb=True)
assert area_on > area_off # the flag, and only the flag, moves the number
# ---------------------------------------------------------------------------
# Pure projection/area helpers
# ---------------------------------------------------------------------------
def test_area_per_pixel_scales_with_scale_factor():
# A scale-2 image has 4x the pixels, so each pixel covers 1/4 the ground area.
at_scale_1 = area_per_pixel_sqft(41.26, 20, 1)
at_scale_2 = area_per_pixel_sqft(41.26, 20, 2)
assert at_scale_1 == pytest.approx(4 * at_scale_2, rel=1e-9)
def test_geometry_area_of_known_square():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, SIZE_M))
assert geometry_area_sqft(parcel, CRS) == pytest.approx(SIZE_M * SIZE_M * SQFT_PER_SQM, rel=2e-3)
# ---------------------------------------------------------------------------
# Frame guard (H-3): a parcel larger than the fixed imagery window is measured on
# truncated masks; the pipeline flags it rather than return a confident wrong number.
# ---------------------------------------------------------------------------
_FRAME_KW = dict(center_latitude=CENTER_LAT, center_longitude=CENTER_LON,
zoom=20, image_width_px=1280, image_height_px=1280, scale=2)
def test_normal_lot_fits_the_frame():
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, 30.0)) # ~30 m, well inside
assert geometry_exceeds_frame(parcel, **_FRAME_KW) is False
def test_oversized_parcel_exceeds_frame():
# The frame is ~71.8 m across at zoom 20; a 120 m lot overflows it.
parcel = utm_to_wgs(square_utm(OMAHA_CX, OMAHA_CY, 120.0))
assert geometry_exceeds_frame(parcel, **_FRAME_KW) is True
|