test_predicates.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. from functools import partial
  2. import numpy as np
  3. import pytest
  4. import shapely
  5. from shapely import LinearRing, LineString, Point
  6. from shapely.tests.common import (
  7. all_types,
  8. all_types_m,
  9. all_types_z,
  10. all_types_zm,
  11. empty,
  12. geometry_collection,
  13. ignore_invalid,
  14. line_string,
  15. linear_ring,
  16. point,
  17. polygon,
  18. )
  19. UNARY_PREDICATES = (
  20. shapely.has_z,
  21. pytest.param(
  22. shapely.has_m,
  23. marks=pytest.mark.skipif(
  24. shapely.geos_version < (3, 12, 0), reason="GEOS < 3.12"
  25. ),
  26. ),
  27. shapely.is_empty,
  28. shapely.is_simple,
  29. shapely.is_ring,
  30. shapely.is_closed,
  31. shapely.is_valid,
  32. shapely.is_missing,
  33. shapely.is_geometry,
  34. shapely.is_valid_input,
  35. shapely.is_prepared,
  36. shapely.is_ccw,
  37. )
  38. BINARY_PREDICATES = (
  39. shapely.disjoint,
  40. shapely.touches,
  41. shapely.intersects,
  42. shapely.crosses,
  43. shapely.within,
  44. shapely.contains,
  45. shapely.contains_properly,
  46. shapely.overlaps,
  47. shapely.covers,
  48. shapely.covered_by,
  49. pytest.param(
  50. partial(shapely.dwithin, distance=1.0),
  51. marks=pytest.mark.skipif(
  52. shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10"
  53. ),
  54. ),
  55. shapely.equals,
  56. shapely.equals_exact,
  57. shapely.equals_identical,
  58. )
  59. BINARY_PREPARED_PREDICATES = BINARY_PREDICATES[:-2]
  60. XY_PREDICATES = (
  61. (shapely.contains_xy, shapely.contains),
  62. (shapely.intersects_xy, shapely.intersects),
  63. )
  64. @pytest.mark.parametrize("geometry", all_types + all_types_z)
  65. @pytest.mark.parametrize("func", UNARY_PREDICATES)
  66. def test_unary_array(geometry, func):
  67. actual = func([geometry, geometry])
  68. assert actual.shape == (2,)
  69. assert actual.dtype == np.bool_
  70. @pytest.mark.parametrize("func", UNARY_PREDICATES)
  71. def test_unary_with_kwargs(func):
  72. out = np.empty((), dtype=np.uint8)
  73. actual = func(point, out=out)
  74. assert actual is out
  75. assert actual.dtype == np.uint8
  76. @pytest.mark.parametrize("func", UNARY_PREDICATES)
  77. def test_unary_missing(func):
  78. if func in (shapely.is_valid_input, shapely.is_missing):
  79. assert func(None)
  80. else:
  81. assert not func(None)
  82. @pytest.mark.parametrize("a", all_types)
  83. @pytest.mark.parametrize("func", BINARY_PREDICATES)
  84. def test_binary_array(a, func):
  85. with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)):
  86. # Empty geometries give 'invalid value encountered' in all predicates
  87. # (see https://github.com/libgeos/geos/issues/515)
  88. actual = func([a, a], point)
  89. assert actual.shape == (2,)
  90. assert actual.dtype == np.bool_
  91. @pytest.mark.parametrize("func", BINARY_PREDICATES)
  92. def test_binary_with_kwargs(func):
  93. out = np.empty((), dtype=np.uint8)
  94. actual = func(point, point, out=out)
  95. assert actual is out
  96. assert actual.dtype == np.uint8
  97. @pytest.mark.parametrize("func", BINARY_PREDICATES)
  98. def test_binary_missing(func):
  99. actual = func(np.array([point, None, None]), np.array([None, point, None]))
  100. assert (~actual).all()
  101. def test_binary_empty_result():
  102. a = LineString([(0, 0), (3, 0), (3, 3), (0, 3)])
  103. b = LineString([(5, 1), (6, 1)])
  104. with ignore_invalid(shapely.geos_version < (3, 12, 0)):
  105. # Intersection resulting in empty geometries give 'invalid value encountered'
  106. # (https://github.com/shapely/shapely/issues/1345)
  107. assert shapely.intersection(a, b).is_empty
  108. @pytest.mark.parametrize("a", all_types)
  109. @pytest.mark.parametrize("func, func_bin", XY_PREDICATES)
  110. def test_xy_array(a, func, func_bin):
  111. with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)):
  112. # Empty geometries give 'invalid value encountered' in all predicates
  113. # (see https://github.com/libgeos/geos/issues/515)
  114. actual = func([a, a], 2, 3)
  115. expected = func_bin([a, a], Point(2, 3))
  116. assert actual.shape == (2,)
  117. assert actual.dtype == np.bool_
  118. np.testing.assert_allclose(actual, expected)
  119. @pytest.mark.parametrize("a", all_types)
  120. @pytest.mark.parametrize("func, func_bin", XY_PREDICATES)
  121. def test_xy_array_broadcast(a, func, func_bin):
  122. a2 = shapely.transform(a, lambda x: x) # makes a copy
  123. with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)):
  124. # Empty geometries give 'invalid value encountered' in all predicates
  125. # (see https://github.com/libgeos/geos/issues/515)
  126. actual = func(a2, [0, 1, 2], [1, 2, 3])
  127. expected = func_bin(a, [Point(0, 1), Point(1, 2), Point(2, 3)])
  128. np.testing.assert_allclose(actual, expected)
  129. @pytest.mark.parametrize("func", [funcs[0] for funcs in XY_PREDICATES])
  130. def test_xy_array_2D(func):
  131. polygon2 = shapely.transform(polygon, lambda x: x) # makes a copy
  132. actual = func(polygon2, [0, 1, 2], [1, 2, 3])
  133. expected = func(polygon2, [[0, 1], [1, 2], [2, 3]])
  134. np.testing.assert_allclose(actual, expected)
  135. @pytest.mark.parametrize("func, func_bin", XY_PREDICATES)
  136. def test_xy_prepared(func, func_bin):
  137. actual = func(_prepare_with_copy([polygon, line_string]), 2, 3)
  138. expected = func_bin([polygon, line_string], Point(2, 3))
  139. np.testing.assert_allclose(actual, expected)
  140. @pytest.mark.parametrize("func", [funcs[0] for funcs in XY_PREDICATES])
  141. def test_xy_with_kwargs(func):
  142. out = np.empty((), dtype=np.uint8)
  143. point2 = shapely.transform(point, lambda x: x) # makes a copy
  144. actual = func(point2, point2.x, point2.y, out=out)
  145. assert actual is out
  146. assert actual.dtype == np.uint8
  147. @pytest.mark.parametrize("func", [funcs[0] for funcs in XY_PREDICATES])
  148. def test_xy_missing(func):
  149. actual = func(
  150. np.array([point, point, point, None]),
  151. np.array([point.x, np.nan, point.x, point.x]),
  152. np.array([point.y, point.y, np.nan, point.y]),
  153. )
  154. np.testing.assert_allclose(actual, [True, False, False, False])
  155. def test_equals_exact_tolerance():
  156. # specifying tolerance
  157. p1 = shapely.points(50, 4)
  158. p2 = shapely.points(50.1, 4.1)
  159. actual = shapely.equals_exact([p1, p2, None], p1, tolerance=0.05)
  160. np.testing.assert_allclose(actual, [True, False, False])
  161. assert actual.dtype == np.bool_
  162. actual = shapely.equals_exact([p1, p2, None], p1, tolerance=0.2)
  163. np.testing.assert_allclose(actual, [True, True, False])
  164. assert actual.dtype == np.bool_
  165. # default value for tolerance
  166. assert shapely.equals_exact(p1, p1).item() is True
  167. assert shapely.equals_exact(p1, p2).item() is False
  168. # an array of tolerances
  169. actual = shapely.equals_exact(p1, p2, tolerance=[0.05, 0.2, np.nan])
  170. np.testing.assert_allclose(actual, [False, True, False])
  171. def test_equals_exact_normalize():
  172. l1 = LineString([(0, 0), (1, 1)])
  173. l2 = LineString([(1, 1), (0, 0)])
  174. # default requires same order of coordinates
  175. assert not shapely.equals_exact(l1, l2)
  176. assert shapely.equals_exact(l1, l2, normalize=True)
  177. def test_equals_identical():
  178. # more elaborate tests are done at the Geometry.__eq__ level
  179. # requires same order of coordinates
  180. l1 = LineString([(0, 0), (1, 1)])
  181. l2 = LineString([(1, 1), (0, 0)])
  182. assert not shapely.equals_identical(l1, l2)
  183. # checks z-dimension (in contrast to equals_exact)
  184. l1 = LineString([(0, 0, 0), (1, 1, 0)])
  185. l2 = LineString([(0, 0, 1), (1, 1, 1)])
  186. assert not shapely.equals_identical(l1, l2)
  187. assert shapely.equals_exact(l1, l2)
  188. # NaNs in same place are equal (in contrast to equals_exact)
  189. with ignore_invalid():
  190. l1 = LineString([(0, np.nan), (1, 1)])
  191. l2 = LineString([(0, np.nan), (1, 1)])
  192. assert shapely.equals_identical(l1, l2)
  193. assert not shapely.equals_exact(l1, l2)
  194. @pytest.mark.skipif(shapely.geos_version < (3, 10, 0), reason="GEOS < 3.10")
  195. def test_dwithin():
  196. p1 = shapely.points(50, 4)
  197. p2 = shapely.points(50.1, 4.1)
  198. actual = shapely.dwithin([p1, p2, None], p1, distance=0.05)
  199. np.testing.assert_equal(actual, [True, False, False])
  200. assert actual.dtype == np.bool_
  201. actual = shapely.dwithin([p1, p2, None], p1, distance=0.2)
  202. np.testing.assert_allclose(actual, [True, True, False])
  203. assert actual.dtype == np.bool_
  204. # an array of distances
  205. actual = shapely.dwithin(p1, p2, distance=[0.05, 0.2, np.nan])
  206. np.testing.assert_allclose(actual, [False, True, False])
  207. @pytest.mark.parametrize("geometry", all_types)
  208. def test_has_z_has_m_all_types(geometry):
  209. assert not shapely.has_z(geometry)
  210. if shapely.geos_version >= (3, 12, 0):
  211. assert not shapely.has_m(geometry)
  212. # The next few tests skip has_z/has_m with empty geometries
  213. # See https://github.com/libgeos/geos/issues/888
  214. @pytest.mark.parametrize("geometry", all_types_z)
  215. def test_has_z_has_m_all_types_z(geometry):
  216. if shapely.is_empty(geometry):
  217. pytest.skip("GEOSHasZ with EMPTY geometries is inconsistent")
  218. assert shapely.has_z(geometry)
  219. if shapely.geos_version >= (3, 12, 0):
  220. assert not shapely.has_m(geometry)
  221. @pytest.mark.skipif(
  222. shapely.geos_version < (3, 12, 0),
  223. reason="M coordinates not supported with GEOS < 3.12",
  224. )
  225. @pytest.mark.parametrize("geometry", all_types_m)
  226. def test_has_m_all_types_m(geometry):
  227. if shapely.is_empty(geometry):
  228. pytest.skip("GEOSHasM with EMPTY geometries is inconsistent")
  229. assert not shapely.has_z(geometry)
  230. assert shapely.has_m(geometry)
  231. @pytest.mark.skipif(
  232. shapely.geos_version < (3, 12, 0),
  233. reason="M coordinates not supported with GEOS < 3.12",
  234. )
  235. @pytest.mark.parametrize("geometry", all_types_zm)
  236. def test_has_z_has_m_all_types_zm(geometry):
  237. if shapely.is_empty(geometry):
  238. pytest.skip("GEOSHasZ with EMPTY geometries is inconsistent")
  239. assert shapely.has_z(geometry)
  240. assert shapely.has_m(geometry)
  241. @pytest.mark.parametrize(
  242. "geometry,expected",
  243. [
  244. (point, False),
  245. (line_string, False),
  246. (linear_ring, True),
  247. (empty, False),
  248. ],
  249. )
  250. def test_is_closed(geometry, expected):
  251. assert shapely.is_closed(geometry) == expected
  252. def test_relate():
  253. p1 = shapely.points(0, 0)
  254. p2 = shapely.points(1, 1)
  255. actual = shapely.relate(p1, p2)
  256. assert isinstance(actual, str)
  257. assert actual == "FF0FFF0F2"
  258. @pytest.mark.parametrize("g1, g2", [(point, None), (None, point), (None, None)])
  259. def test_relate_none(g1, g2):
  260. assert shapely.relate(g1, g2) is None
  261. def test_relate_pattern():
  262. g = shapely.linestrings([(0, 0), (1, 0), (1, 1)])
  263. polygon = shapely.box(0, 0, 2, 2)
  264. assert shapely.relate(g, polygon) == "11F00F212"
  265. assert shapely.relate_pattern(g, polygon, "11F00F212")
  266. assert shapely.relate_pattern(g, polygon, "*********")
  267. assert not shapely.relate_pattern(g, polygon, "F********")
  268. def test_relate_pattern_empty():
  269. with ignore_invalid(shapely.geos_version < (3, 12, 0)):
  270. # Empty geometries give 'invalid value encountered' in all predicates
  271. # (see https://github.com/libgeos/geos/issues/515)
  272. assert shapely.relate_pattern(empty, empty, "*" * 9).item() is True
  273. @pytest.mark.parametrize("g1, g2", [(point, None), (None, point), (None, None)])
  274. def test_relate_pattern_none(g1, g2):
  275. assert shapely.relate_pattern(g1, g2, "*" * 9).item() is False
  276. def test_relate_pattern_incorrect_length():
  277. with pytest.raises(shapely.GEOSException, match="Should be length 9"):
  278. shapely.relate_pattern(point, polygon, "**")
  279. with pytest.raises(shapely.GEOSException, match="Should be length 9"):
  280. shapely.relate_pattern(point, polygon, "**********")
  281. @pytest.mark.parametrize("pattern", [b"*********", 10, None])
  282. def test_relate_pattern_non_string(pattern):
  283. with pytest.raises(TypeError, match="expected string"):
  284. shapely.relate_pattern(point, polygon, pattern)
  285. def test_relate_pattern_non_scalar():
  286. with pytest.raises(ValueError, match="only supports scalar"):
  287. shapely.relate_pattern([point] * 2, polygon, ["*********"] * 2)
  288. @pytest.mark.parametrize(
  289. "geom, expected",
  290. [
  291. (LinearRing([(0, 0), (0, 1), (1, 1), (0, 0)]), False),
  292. (LinearRing([(0, 0), (1, 1), (0, 1), (0, 0)]), True),
  293. (LineString([(0, 0), (0, 1), (1, 1), (0, 0)]), False),
  294. (LineString([(0, 0), (1, 1), (0, 1), (0, 0)]), True),
  295. (LineString([(0, 0), (1, 1), (0, 1)]), False),
  296. (LineString([(0, 0), (0, 1), (1, 1)]), False),
  297. (point, False),
  298. (polygon, False),
  299. (geometry_collection, False),
  300. (None, False),
  301. ],
  302. )
  303. def test_is_ccw(geom, expected):
  304. assert shapely.is_ccw(geom) == expected
  305. def _prepare_with_copy(geometry):
  306. """Prepare without modifying in-place"""
  307. geometry = shapely.transform(geometry, lambda x: x) # makes a copy
  308. shapely.prepare(geometry)
  309. return geometry
  310. @pytest.mark.parametrize("a", all_types)
  311. @pytest.mark.parametrize("func", BINARY_PREPARED_PREDICATES)
  312. def test_binary_prepared(a, func):
  313. with ignore_invalid(shapely.is_empty(a) and shapely.geos_version < (3, 12, 0)):
  314. # Empty geometries give 'invalid value encountered' in all predicates
  315. # (see https://github.com/libgeos/geos/issues/515)
  316. actual = func(a, point)
  317. result = func(_prepare_with_copy(a), point)
  318. assert actual == result
  319. @pytest.mark.parametrize("geometry", all_types)
  320. def test_is_prepared_true(geometry):
  321. assert shapely.is_prepared(_prepare_with_copy(geometry))
  322. @pytest.mark.parametrize("geometry", all_types + (None,))
  323. def test_is_prepared_false(geometry):
  324. assert not shapely.is_prepared(geometry)
  325. def test_contains_properly():
  326. # polygon contains itself, but does not properly contains itself
  327. assert shapely.contains(polygon, polygon).item() is True
  328. assert shapely.contains_properly(polygon, polygon).item() is False