Spaces:
Running
Running
| """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 | |