test_split.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import unittest
  2. import pytest
  3. from shapely.errors import GeometryTypeError
  4. from shapely.geometry import (
  5. LineString,
  6. MultiLineString,
  7. MultiPoint,
  8. MultiPolygon,
  9. Point,
  10. Polygon,
  11. )
  12. from shapely.ops import linemerge, split, unary_union
  13. class TestSplitGeometry(unittest.TestCase):
  14. # helper class for testing below
  15. def helper(self, geom, splitter, expected_chunks):
  16. s = split(geom, splitter)
  17. assert s.geom_type == "GeometryCollection"
  18. assert len(s.geoms) == expected_chunks
  19. if expected_chunks > 1:
  20. # split --> expected collection that when merged is again equal to original
  21. # geometry
  22. if s.geoms[0].geom_type == "LineString":
  23. self.assertTrue(linemerge(s).simplify(0.000001).equals(geom))
  24. elif s.geoms[0].geom_type == "Polygon":
  25. union = unary_union(s).simplify(0.000001)
  26. assert union.equals(geom)
  27. assert union.area == geom.area
  28. else:
  29. raise ValueError
  30. elif expected_chunks == 1:
  31. # not split --> expected equal to line
  32. assert s.geoms[0].equals(geom)
  33. def test_split_closed_line_with_point(self):
  34. # point at start/end of closed ring -> return equal
  35. # see GH #524
  36. ls = LineString([(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)])
  37. splitter = Point(0, 0)
  38. self.helper(ls, splitter, 1)
  39. class TestSplitPolygon(TestSplitGeometry):
  40. poly_simple = Polygon([(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)])
  41. poly_hole = Polygon(
  42. [(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)],
  43. [[(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)]],
  44. )
  45. def test_split_poly_with_line(self):
  46. # crossing at 2 points --> return 2 polygons
  47. splitter = LineString([(1, 3), (1, -3)])
  48. self.helper(self.poly_simple, splitter, 2)
  49. self.helper(self.poly_hole, splitter, 2)
  50. # crossing twice with one linestring --> return 3 polygons
  51. splitter = LineString([(1, 3), (1, -3), (1.7, -3), (1.7, 3)])
  52. self.helper(self.poly_simple, splitter, 3)
  53. self.helper(self.poly_hole, splitter, 3)
  54. # touching the boundary --> return equal
  55. splitter = LineString([(0, 2), (5, 2)])
  56. self.helper(self.poly_simple, splitter, 1)
  57. self.helper(self.poly_hole, splitter, 1)
  58. # inside the polygon --> return equal
  59. splitter = LineString([(0.2, 0.2), (1.7, 1.7), (3, 2)])
  60. self.helper(self.poly_simple, splitter, 1)
  61. self.helper(self.poly_hole, splitter, 1)
  62. # outside the polygon --> return equal
  63. splitter = LineString([(0, 3), (3, 3), (3, 0)])
  64. self.helper(self.poly_simple, splitter, 1)
  65. self.helper(self.poly_hole, splitter, 1)
  66. def test_split_poly_with_multiline(self):
  67. # crossing twice with a multilinestring --> return 3 polygons
  68. splitter = MultiLineString([[(0.2, 3), (0.2, -3)], [(1.7, -3), (1.7, 3)]])
  69. self.helper(self.poly_simple, splitter, 3)
  70. self.helper(self.poly_hole, splitter, 3)
  71. # crossing twice with a cross multilinestring --> return 4 polygons
  72. splitter = MultiLineString([[(0.2, 3), (0.2, -3)], [(-3, 1), (3, 1)]])
  73. self.helper(self.poly_simple, splitter, 4)
  74. self.helper(self.poly_hole, splitter, 4)
  75. # cross once, touch the boundary once --> return 2 polygons
  76. splitter = MultiLineString([[(0.2, 3), (0.2, -3)], [(0, 2), (5, 2)]])
  77. self.helper(self.poly_simple, splitter, 2)
  78. self.helper(self.poly_hole, splitter, 2)
  79. # cross once, inside the polygon once --> return 2 polygons
  80. splitter = MultiLineString(
  81. [[(0.2, 3), (0.2, -3)], [(1.2, 1.2), (1.7, 1.7), (3, 2)]]
  82. )
  83. self.helper(self.poly_simple, splitter, 2)
  84. self.helper(self.poly_hole, splitter, 2)
  85. # cross once, outside the polygon once --> return 2 polygons
  86. splitter = MultiLineString([[(0.2, 3), (0.2, -3)], [(0, 3), (3, 3), (3, 0)]])
  87. self.helper(self.poly_simple, splitter, 2)
  88. self.helper(self.poly_hole, splitter, 2)
  89. def test_split_poly_with_other(self):
  90. with pytest.raises(GeometryTypeError):
  91. split(self.poly_simple, Point(1, 1))
  92. with pytest.raises(GeometryTypeError):
  93. split(self.poly_simple, MultiPoint([(1, 1), (3, 4)]))
  94. with pytest.raises(GeometryTypeError):
  95. split(self.poly_simple, self.poly_hole)
  96. class TestSplitLine(TestSplitGeometry):
  97. ls = LineString([(0, 0), (1.5, 1.5), (3.0, 4.0)])
  98. def test_split_line_with_point(self):
  99. # point on line interior --> return 2 segments
  100. splitter = Point(1, 1)
  101. self.helper(self.ls, splitter, 2)
  102. # point on line point --> return 2 segments
  103. splitter = Point(1.5, 1.5)
  104. self.helper(self.ls, splitter, 2)
  105. # point on boundary --> return equal
  106. splitter = Point(3, 4)
  107. self.helper(self.ls, splitter, 1)
  108. # point on exterior of line --> return equal
  109. splitter = Point(2, 2)
  110. self.helper(self.ls, splitter, 1)
  111. def test_split_line_with_multipoint(self):
  112. # points on line interior --> return 4 segments
  113. splitter = MultiPoint([(1, 1), (1.5, 1.5), (0.5, 0.5)])
  114. self.helper(self.ls, splitter, 4)
  115. # points on line interior and boundary -> return 2 segments
  116. splitter = MultiPoint([(1, 1), (3, 4)])
  117. self.helper(self.ls, splitter, 2)
  118. # point on linear interior but twice --> return 2 segments
  119. splitter = MultiPoint([(1, 1), (1.5, 1.5), (1, 1)])
  120. self.helper(self.ls, splitter, 3)
  121. def test_split_line_with_line(self):
  122. # crosses at one point --> return 2 segments
  123. splitter = LineString([(0, 1), (1, 0)])
  124. self.helper(self.ls, splitter, 2)
  125. # crosses at two points --> return 3 segments
  126. splitter = LineString([(0, 1), (1, 0), (1, 2)])
  127. self.helper(self.ls, splitter, 3)
  128. # overlaps --> raise
  129. splitter = LineString([(0, 0), (15, 15)])
  130. with pytest.raises(ValueError):
  131. self.helper(self.ls, splitter, 1)
  132. # does not cross --> return equal
  133. splitter = LineString([(0, 1), (0, 2)])
  134. self.helper(self.ls, splitter, 1)
  135. # is touching the boundary --> return equal
  136. splitter = LineString([(-1, 1), (1, -1)])
  137. assert splitter.touches(self.ls)
  138. self.helper(self.ls, splitter, 1)
  139. # splitter boundary touches interior of line --> return 2 segments
  140. splitter = LineString([(0, 1), (1, 1)]) # touches at (1, 1)
  141. assert splitter.touches(self.ls)
  142. self.helper(self.ls, splitter, 2)
  143. def test_split_line_with_multiline(self):
  144. # crosses at one point --> return 2 segments
  145. splitter = MultiLineString([[(0, 1), (1, 0)], [(0, 0), (2, -2)]])
  146. self.helper(self.ls, splitter, 2)
  147. # crosses at two points --> return 3 segments
  148. splitter = MultiLineString([[(0, 1), (1, 0)], [(0, 2), (2, 0)]])
  149. self.helper(self.ls, splitter, 3)
  150. # crosses at three points --> return 4 segments
  151. splitter = MultiLineString([[(0, 1), (1, 0)], [(0, 2), (2, 0), (2.2, 3.2)]])
  152. self.helper(self.ls, splitter, 4)
  153. # overlaps --> raise
  154. splitter = MultiLineString([[(0, 0), (1.5, 1.5)], [(1.5, 1.5), (3, 4)]])
  155. with pytest.raises(ValueError):
  156. self.helper(self.ls, splitter, 1)
  157. # does not cross --> return equal
  158. splitter = MultiLineString([[(0, 1), (0, 2)], [(1, 0), (2, 0)]])
  159. self.helper(self.ls, splitter, 1)
  160. def test_split_line_with_polygon(self):
  161. # crosses at two points --> return 3 segments
  162. splitter = Polygon([(1, 0), (1, 2), (2, 2), (2, 0), (1, 0)])
  163. self.helper(self.ls, splitter, 3)
  164. # crosses at one point and touches boundary --> return 2 segments
  165. splitter = Polygon([(0, 0), (1, 2), (2, 2), (1, 0), (0, 0)])
  166. self.helper(self.ls, splitter, 2)
  167. # exterior crosses at one point and touches at (0, 0)
  168. # interior crosses at two points
  169. splitter = Polygon(
  170. [(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)],
  171. [[(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)]],
  172. )
  173. self.helper(self.ls, splitter, 4)
  174. def test_split_line_with_multipolygon(self):
  175. poly1 = Polygon(
  176. [(0, 0), (2, 0), (2, 2), (0, 2), (0, 0)]
  177. ) # crosses at one point and touches at (0, 0)
  178. poly2 = Polygon(
  179. [(0.5, 0.5), (0.5, 1.5), (1.5, 1.5), (1.5, 0.5), (0.5, 0.5)]
  180. ) # crosses at two points
  181. poly3 = Polygon([(0, 0), (0, -2), (-2, -2), (-2, 0), (0, 0)]) # not crossing
  182. splitter = MultiPolygon([poly1, poly2, poly3])
  183. self.helper(self.ls, splitter, 4)
  184. class TestSplitClosedRing(TestSplitGeometry):
  185. ls = LineString([[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]])
  186. def test_split_closed_ring_with_point(self):
  187. splitter = Point([0.0, 0.0])
  188. self.helper(self.ls, splitter, 1)
  189. splitter = Point([0.0, 0.5])
  190. self.helper(self.ls, splitter, 2)
  191. result = split(self.ls, splitter)
  192. assert result.geoms[0].coords[:] == [(0, 0), (0.0, 0.5)]
  193. assert result.geoms[1].coords[:] == [(0.0, 0.5), (0, 1), (1, 1), (1, 0), (0, 0)]
  194. # previously failed, see GH#585
  195. splitter = Point([0.5, 0.0])
  196. self.helper(self.ls, splitter, 2)
  197. result = split(self.ls, splitter)
  198. assert result.geoms[0].coords[:] == [(0, 0), (0, 1), (1, 1), (1, 0), (0.5, 0)]
  199. assert result.geoms[1].coords[:] == [(0.5, 0), (0, 0)]
  200. splitter = Point([2.0, 2.0])
  201. self.helper(self.ls, splitter, 1)
  202. class TestSplitMulti(TestSplitGeometry):
  203. def test_split_multiline_with_point(self):
  204. # a cross-like multilinestring with a point in the middle --> return 4 line
  205. # segments
  206. l1 = LineString([(0, 1), (2, 1)])
  207. l2 = LineString([(1, 0), (1, 2)])
  208. ml = MultiLineString([l1, l2])
  209. splitter = Point((1, 1))
  210. self.helper(ml, splitter, 4)
  211. def test_split_multiline_with_multipoint(self):
  212. # a cross-like multilinestring with a point in middle, a point on one of the
  213. # lines and a point in the exterior
  214. # --> return 4+1 line segments
  215. l1 = LineString([(0, 1), (3, 1)])
  216. l2 = LineString([(1, 0), (1, 2)])
  217. ml = MultiLineString([l1, l2])
  218. splitter = MultiPoint([(1, 1), (2, 1), (4, 2)])
  219. self.helper(ml, splitter, 5)
  220. def test_split_multipolygon_with_line(self):
  221. # two polygons with a crossing line --> return 4 triangles
  222. poly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])
  223. poly2 = Polygon([(1, 1), (1, 2), (2, 2), (2, 1), (1, 1)])
  224. mpoly = MultiPolygon([poly1, poly2])
  225. ls = LineString([(-1, -1), (3, 3)])
  226. self.helper(mpoly, ls, 4)
  227. # two polygons away from the crossing line --> return identity
  228. poly1 = Polygon([(10, 10), (10, 11), (11, 11), (11, 10), (10, 10)])
  229. poly2 = Polygon([(-10, -10), (-10, -11), (-11, -11), (-11, -10), (-10, -10)])
  230. mpoly = MultiPolygon([poly1, poly2])
  231. ls = LineString([(-1, -1), (3, 3)])
  232. self.helper(mpoly, ls, 2)