"""Unit tests for obstacle polygon building and swath clipping (shapely only, no fields2cover)."""
# pylint: disable=protected-access
import unittest
from shapely.geometry import Polygon
from devkit_f2c_planner.f2c_test_helpers import LAT0, LON0, planner, to_ll
[docs]
def square(x0: float, y0: float, size: float) -> list:
return [(x0, y0), (x0 + size, y0), (x0 + size, y0 + size), (x0, y0 + size)]
[docs]
def build(rings_xy: list, pad_m: float = 0.0) -> list:
"""Run _build_obstacle_polys() on rings given in local xy metres."""
return planner._build_obstacle_polys([to_ll(ring) for ring in rings_xy], LAT0, LON0, pad_m, lambda _msg: None)
[docs]
def clip(rows: list, obstacles: list, min_fragment_len_m: float = 0.5) -> list:
"""Run _clip_lines_against_obstacles() with obstacles given as xy rectangles."""
return planner._clip_lines_against_obstacles(
rows, [Polygon(ring) for ring in obstacles], min_fragment_len_m, lambda _msg: None)
[docs]
class BuildObstaclePolysTest(unittest.TestCase):
[docs]
def test_valid_ring_becomes_polygon_with_matching_area(self):
polys = build([square(0, 0, 10)])
self.assertEqual(len(polys), 1)
self.assertEqual(polys[0].geom_type, 'Polygon')
self.assertAlmostEqual(polys[0].area, 100.0, places=3)
[docs]
def test_rings_with_fewer_than_three_points_are_skipped(self):
polys = build([[(0, 0), (5, 5)], square(20, 20, 10)])
self.assertEqual(len(polys), 1)
self.assertAlmostEqual(polys[0].bounds[0], 20.0, places=3)
[docs]
def test_zero_area_ring_is_dropped(self):
self.assertEqual(build([[(0, 0), (5, 0), (10, 0)]]), [])
[docs]
def test_self_intersecting_ring_is_repaired_or_dropped_never_returned_invalid(self):
polys = build([[(0, 0), (10, 10), (10, 0), (0, 10)]])
for poly in polys:
self.assertTrue(poly.is_valid)
self.assertEqual(poly.geom_type, 'Polygon')
[docs]
def test_ring_with_a_zero_width_spike_is_repaired_not_dropped(self):
"""A dropped obstacle means swaths run straight through it."""
spiked_square = [(0, 0), (10, 0), (10, 10), (5, 10), (5, 15), (5, 10), (0, 10)]
(poly,) = build([spiked_square])
self.assertTrue(poly.is_valid)
self.assertAlmostEqual(poly.area, 100.0, places=3)
[docs]
def test_bad_ring_does_not_affect_the_others(self):
polys = build([square(0, 0, 10), [(0, 0), (5, 0), (10, 0)], square(50, 50, 10)])
self.assertEqual(len(polys), 2)
self.assertAlmostEqual(polys[0].bounds[0], 0.0, places=3)
self.assertAlmostEqual(polys[1].bounds[0], 50.0, places=3)
[docs]
def test_padding_grows_the_polygon_by_the_pad_on_every_side(self):
"""Mitre joins: a 10 m square padded by 1 m is exactly 12 m x 12 m."""
(poly,) = build([square(0, 0, 10)], pad_m=1.0)
self.assertAlmostEqual(poly.area, 144.0, places=3)
for actual, expected in zip(poly.bounds, (-1.0, -1.0, 11.0, 11.0), strict=True):
self.assertAlmostEqual(actual, expected, places=3)
[docs]
def test_no_padding_leaves_the_polygon_unchanged(self):
(poly,) = build([square(0, 0, 10)], pad_m=0.0)
self.assertAlmostEqual(poly.area, 100.0, places=3)
[docs]
class ClipLinesAgainstObstaclesTest(unittest.TestCase):
OBSTACLE = square(4, -1, 2) # spans x 4..6, y -1..1
[docs]
def test_row_missing_the_obstacle_is_unchanged(self):
row = [(0.0, 5.0), (10.0, 5.0)]
self.assertEqual(clip([row], [self.OBSTACLE]), [row])
[docs]
def test_row_through_the_obstacle_is_split_at_its_edges(self):
result = clip([[(0.0, 0.0), (10.0, 0.0)]], [self.OBSTACLE])
self.assertEqual(result, [[(0.0, 0.0), (4.0, 0.0)], [(6.0, 0.0), (10.0, 0.0)]])
[docs]
def test_row_fully_inside_the_obstacle_is_dropped(self):
self.assertEqual(clip([[(4.2, 0.0), (5.8, 0.0)]], [self.OBSTACLE]), [])
[docs]
def test_fragments_shorter_than_the_minimum_are_dropped(self):
obstacle = [(1, -1), (9.5, -1), (9.5, 1), (1, 1)] # leaves 1.0 m and 0.5 m stubs
result = clip([[(0.0, 0.0), (10.0, 0.0)]], [obstacle], min_fragment_len_m=0.8)
self.assertEqual(result, [[(0.0, 0.0), (1.0, 0.0)]])
[docs]
def test_fragment_exactly_at_the_minimum_length_is_dropped(self):
obstacle = [(1, -1), (9.5, -1), (9.5, 1), (1, 1)]
self.assertEqual(clip([[(0.0, 0.0), (10.0, 0.0)]], [obstacle], min_fragment_len_m=1.0), [])
[docs]
def test_overlapping_obstacles_act_as_their_union(self):
obstacles = [square(4, -1, 2), [(5, -1), (8, -1), (8, 1), (5, 1)]]
result = clip([[(0.0, 0.0), (10.0, 0.0)]], obstacles)
self.assertEqual(result, [[(0.0, 0.0), (4.0, 0.0)], [(8.0, 0.0), (10.0, 0.0)]])
[docs]
def test_each_row_is_clipped_independently(self):
rows = [[(0.0, 0.0), (10.0, 0.0)], [(0.0, 5.0), (10.0, 5.0)], [(10.0, 0.5), (0.0, 0.5)]]
result = clip(rows, [self.OBSTACLE])
self.assertEqual(len(result), 5)
self.assertIn([(0.0, 5.0), (10.0, 5.0)], result)
if __name__ == '__main__':
unittest.main()