More
This commit is contained in:
parent
1f68ca0512
commit
3599803589
43 changed files with 3578 additions and 262 deletions
|
|
@ -9,7 +9,8 @@ import pytest
|
|||
from shapely.geometry import MultiPolygon, Polygon, box
|
||||
|
||||
from .oa_boundaries import parse_gpkg_geometry
|
||||
from .output import merge_fragments, to_wgs84_geojson
|
||||
from .greenspace import subtract_greenspace
|
||||
from .output import _fill_holes, merge_fragments, to_wgs84_geojson
|
||||
from .process_oa import _extract_polygonal, process_oa
|
||||
from .uprn import get_oa_uprns, load_uprns
|
||||
from .voronoi import _equal_split_fallback, compute_voronoi_regions
|
||||
|
|
@ -426,3 +427,143 @@ class TestParseGpkgGeometry:
|
|||
blob = bytes([0x47, 0x50, 0x00, 0b00001010]) + b"\x00" * 100
|
||||
with pytest.raises(ValueError, match="Unknown GeoPackage envelope type 5"):
|
||||
parse_gpkg_geometry(blob)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _fill_holes removes interior rings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFillHoles:
|
||||
"""_fill_holes must remove all interior holes from polygons."""
|
||||
|
||||
def test_polygon_with_hole(self):
|
||||
"""A polygon with an interior ring should become a solid polygon."""
|
||||
outer = [(0, 0), (100, 0), (100, 100), (0, 100), (0, 0)]
|
||||
hole = [(30, 30), (70, 30), (70, 70), (30, 70), (30, 30)]
|
||||
poly_with_hole = Polygon(outer, [hole])
|
||||
assert len(list(poly_with_hole.interiors)) == 1
|
||||
result = _fill_holes(poly_with_hole)
|
||||
assert result.geom_type == "Polygon"
|
||||
assert len(list(result.interiors)) == 0
|
||||
assert result.area == pytest.approx(Polygon(outer).area)
|
||||
|
||||
def test_multipolygon_with_holes(self):
|
||||
"""A MultiPolygon where each part has holes should have all holes removed."""
|
||||
outer1 = [(0, 0), (50, 0), (50, 50), (0, 50), (0, 0)]
|
||||
hole1 = [(10, 10), (20, 10), (20, 20), (10, 20), (10, 10)]
|
||||
outer2 = [(60, 60), (110, 60), (110, 110), (60, 110), (60, 60)]
|
||||
hole2 = [(70, 70), (80, 70), (80, 80), (70, 80), (70, 70)]
|
||||
mp = MultiPolygon(
|
||||
[Polygon(outer1, [hole1]), Polygon(outer2, [hole2])]
|
||||
)
|
||||
result = _fill_holes(mp)
|
||||
assert result.geom_type == "MultiPolygon"
|
||||
for p in result.geoms:
|
||||
assert len(list(p.interiors)) == 0
|
||||
|
||||
def test_polygon_without_hole_unchanged(self):
|
||||
"""A polygon with no holes should pass through unchanged."""
|
||||
poly = box(0, 0, 100, 100)
|
||||
result = _fill_holes(poly)
|
||||
assert result.area == pytest.approx(poly.area)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Improved merge with 5m buffer closes 3m gaps
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestMergeImprovedBuffer:
|
||||
"""The 5m buffer should close gaps that the old 1m buffer could not."""
|
||||
|
||||
def test_3m_gap_merged(self):
|
||||
"""Two fragments with a 3m gap should merge into a single polygon."""
|
||||
left = box(0, 0, 50, 100)
|
||||
right = box(53, 0, 100, 100) # 3m gap at x=50..53
|
||||
result = merge_fragments([("AA1 1AA", left), ("AA1 1AA", right)])
|
||||
assert "AA1 1AA" in result
|
||||
geom = result["AA1 1AA"]
|
||||
assert geom.geom_type == "Polygon", (
|
||||
f"Expected single Polygon after merging 3m gap, got {geom.geom_type}"
|
||||
)
|
||||
|
||||
def test_holes_removed_after_merge(self):
|
||||
"""Interior holes created by merging should be filled."""
|
||||
# Create a donut-like shape from fragments
|
||||
outer = box(0, 0, 100, 100)
|
||||
inner = box(30, 30, 70, 70)
|
||||
ring = outer.difference(inner)
|
||||
# Add the inner piece as a separate fragment
|
||||
result = merge_fragments([("AA1 1AA", ring), ("AA1 1AA", inner)])
|
||||
assert "AA1 1AA" in result
|
||||
geom = result["AA1 1AA"]
|
||||
assert len(list(geom.interiors)) == 0, "Merged polygon should have no holes"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# subtract_greenspace
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSubtractGreenspace:
|
||||
"""subtract_greenspace must remove park/water area from postcode polygons."""
|
||||
|
||||
def test_park_subtracted(self):
|
||||
"""A park overlapping a postcode should reduce its area."""
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
postcode = box(0, 0, 100, 100) # 10000 sqm
|
||||
park = box(60, 0, 100, 100) # 4000 sqm overlap on the right
|
||||
tree = STRtree([park])
|
||||
geoms = [park]
|
||||
result = subtract_greenspace(postcode, tree, geoms)
|
||||
# Should have lost ~4000 sqm
|
||||
assert result.area == pytest.approx(6000, rel=0.01)
|
||||
|
||||
def test_no_greenspace_unchanged(self):
|
||||
"""With no overlapping greenspace, the geometry should be unchanged."""
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
postcode = box(0, 0, 100, 100)
|
||||
park = box(200, 200, 300, 300) # far away
|
||||
tree = STRtree([park])
|
||||
geoms = [park]
|
||||
result = subtract_greenspace(postcode, tree, geoms)
|
||||
assert result.area == pytest.approx(postcode.area)
|
||||
|
||||
def test_full_overlap_preserves_postcode(self):
|
||||
"""If greenspace covers the entire postcode, keep the original."""
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
postcode = box(0, 0, 100, 100)
|
||||
park = box(-10, -10, 110, 110) # completely covers postcode
|
||||
tree = STRtree([park])
|
||||
geoms = [park]
|
||||
result = subtract_greenspace(postcode, tree, geoms)
|
||||
# Should keep original since subtraction would erase entirely
|
||||
assert result.area == pytest.approx(postcode.area)
|
||||
|
||||
def test_over_90pct_removal_preserves_postcode(self):
|
||||
"""If greenspace would remove >90% of area, keep the original."""
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
postcode = box(0, 0, 100, 100) # 10000 sqm
|
||||
park = box(5, 0, 100, 100) # 9500 sqm overlap = 95% removal
|
||||
tree = STRtree([park])
|
||||
geoms = [park]
|
||||
result = subtract_greenspace(postcode, tree, geoms)
|
||||
# Should keep original since >90% would be removed
|
||||
assert result.area == pytest.approx(postcode.area)
|
||||
|
||||
def test_under_90pct_removal_subtracts(self):
|
||||
"""If greenspace removes <90%, subtraction should proceed."""
|
||||
from shapely.strtree import STRtree
|
||||
|
||||
postcode = box(0, 0, 100, 100) # 10000 sqm
|
||||
park = box(20, 0, 100, 100) # 8000 sqm overlap = 80% removal
|
||||
tree = STRtree([park])
|
||||
geoms = [park]
|
||||
result = subtract_greenspace(postcode, tree, geoms)
|
||||
# 80% < 90% cap, so subtraction should happen
|
||||
assert result.area == pytest.approx(2000, rel=0.01)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue