coordinates.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. """Methods that operate on the coordinates of geometries."""
  2. import numpy as np
  3. import shapely
  4. from shapely import lib
  5. from shapely.decorators import deprecate_positional
  6. __all__ = ["count_coordinates", "get_coordinates", "set_coordinates", "transform"]
  7. # Note: future plan is to change this signature over a few releases:
  8. # shapely 2.0: only supported XY and XYZ geometries
  9. # transform(geometry, transformation, include_z=False)
  10. # shapely 2.1: shows deprecation warning about positional 'include_z' arg
  11. # transform(geometry, transformation, include_z=False, *, interleaved=True)
  12. # shapely 2.2(?): enforce keyword-only arguments after 'transformation'
  13. # transform(geometry, transformation, *, include_z=False, interleaved=True)
  14. @deprecate_positional(["include_z"], category=DeprecationWarning)
  15. def transform(
  16. geometry,
  17. transformation,
  18. include_z: bool | None = False,
  19. *,
  20. interleaved: bool = True,
  21. ):
  22. """Apply a function to the coordinates of a geometry.
  23. With the default of ``include_z=False``, all returned geometries will be
  24. two-dimensional; the third dimension will be discarded, if present.
  25. When specifying ``include_z=True``, the returned geometries preserve
  26. the dimensionality of the respective input geometries.
  27. Parameters
  28. ----------
  29. geometry : Geometry or array_like
  30. Geometry or geometries to transform.
  31. transformation : function
  32. A function that transforms a (N, 2) or (N, 3) ndarray of float64 to
  33. another (N, 2) or (N, 3) ndarray of float64.
  34. The function may not change N.
  35. include_z : bool, optional, default False
  36. If False, always return 2D geometries.
  37. If True, the data being passed to the
  38. transformation function will include the third dimension
  39. (if a geometry has no third dimension, the z-coordinates
  40. will be NaN). If None, will infer the dimensionality per
  41. input geometry using ``has_z``, which may result in 2 calls to
  42. the transformation function. Note that this inference
  43. can be unreliable with empty geometries or NaN coordinates: for a
  44. guaranteed result, it is recommended to specify ``include_z`` explicitly.
  45. interleaved : bool, default True
  46. If set to False, the transformation function should accept 2 or 3 separate
  47. one-dimensional arrays (x, y and optional z) instead of a single
  48. two-dimensional array.
  49. .. versionadded:: 2.1.0
  50. Notes
  51. -----
  52. .. deprecated:: 2.1.0
  53. A deprecation warning is shown if ``include_z`` is specified as a
  54. positional argument. This will need to be specified as a keyword
  55. argument in a future release.
  56. See Also
  57. --------
  58. has_z
  59. Examples
  60. --------
  61. >>> import shapely
  62. >>> from shapely import LineString, Point
  63. >>> shapely.transform(Point(0, 0), lambda x: x + 1)
  64. <POINT (1 1)>
  65. >>> shapely.transform(LineString([(2, 2), (4, 4)]), lambda x: x * [2, 3])
  66. <LINESTRING (4 6, 8 12)>
  67. >>> shapely.transform(None, lambda x: x) is None
  68. True
  69. >>> shapely.transform([Point(0, 0), None], lambda x: x).tolist()
  70. [<POINT (0 0)>, None]
  71. The presence of a third dimension can be automatically detected, or
  72. controlled explicitly:
  73. >>> shapely.transform(Point(0, 0, 0), lambda x: x + 1)
  74. <POINT (1 1)>
  75. >>> shapely.transform(Point(0, 0, 0), lambda x: x + 1, include_z=True)
  76. <POINT Z (1 1 1)>
  77. >>> shapely.transform(Point(0, 0, 0), lambda x: x + 1, include_z=None)
  78. <POINT Z (1 1 1)>
  79. With interleaved=False, the call signature of the transformation is different:
  80. >>> shapely.transform(LineString([(1, 2), (3, 4)]), lambda x, y: (x + 1, y), \
  81. interleaved=False)
  82. <LINESTRING (2 2, 4 4)>
  83. Or with a z coordinate:
  84. >>> shapely.transform(Point(0, 0, 0), lambda x, y, z: (x + 1, y, z + 2), \
  85. interleaved=False, include_z=True)
  86. <POINT Z (1 0 2)>
  87. Using pyproj >= 2.1, the following example will reproject Shapely geometries
  88. from EPSG 4326 to EPSG 32618:
  89. >>> from pyproj import Transformer
  90. >>> transformer = Transformer.from_crs(4326, 32618, always_xy=True)
  91. >>> shapely.transform(Point(-75, 50), transformer.transform, interleaved=False)
  92. <POINT (500000 5538630.703)>
  93. """
  94. geometry_arr = np.array(geometry, dtype=np.object_) # makes a copy
  95. if include_z is None:
  96. has_z = shapely.has_z(geometry_arr)
  97. result = np.empty_like(geometry_arr)
  98. result[has_z] = transform(
  99. geometry_arr[has_z], transformation, include_z=True, interleaved=interleaved
  100. )
  101. result[~has_z] = transform(
  102. geometry_arr[~has_z],
  103. transformation,
  104. include_z=False,
  105. interleaved=interleaved,
  106. )
  107. else:
  108. # TODO: expose include_m
  109. include_m = False
  110. coordinates = lib.get_coordinates(geometry_arr, include_z, include_m, False)
  111. if interleaved:
  112. new_coordinates = transformation(coordinates)
  113. else:
  114. new_coordinates = np.asarray(
  115. transformation(*coordinates.T), dtype=np.float64
  116. ).T
  117. # check the array to yield understandable error messages
  118. if not isinstance(new_coordinates, np.ndarray) or new_coordinates.ndim != 2:
  119. raise ValueError(
  120. "The provided transformation did not return a two-dimensional numpy "
  121. "array"
  122. )
  123. if new_coordinates.dtype != np.float64:
  124. raise ValueError(
  125. "The provided transformation returned an array with an unexpected "
  126. f"dtype ({new_coordinates.dtype})"
  127. )
  128. if new_coordinates.shape != coordinates.shape:
  129. # if the shape is too small we will get a segfault
  130. raise ValueError(
  131. "The provided transformation returned an array with an unexpected "
  132. f"shape ({new_coordinates.shape})"
  133. )
  134. result = lib.set_coordinates(geometry_arr, new_coordinates)
  135. if result.ndim == 0 and not isinstance(geometry, np.ndarray):
  136. return result.item()
  137. return result
  138. def count_coordinates(geometry):
  139. """Count the number of coordinate pairs in a geometry array.
  140. Parameters
  141. ----------
  142. geometry : Geometry or array_like
  143. Geometry or geometries to count the coordinates of.
  144. Examples
  145. --------
  146. >>> import shapely
  147. >>> from shapely import LineString, Point
  148. >>> shapely.count_coordinates(Point(0, 0))
  149. 1
  150. >>> shapely.count_coordinates(LineString([(2, 2), (4, 2)]))
  151. 2
  152. >>> shapely.count_coordinates(None)
  153. 0
  154. >>> shapely.count_coordinates([Point(0, 0), None])
  155. 1
  156. """
  157. return lib.count_coordinates(np.asarray(geometry, dtype=np.object_))
  158. # Note: future plan is to change this signature over a few releases:
  159. # shapely 2.0: only supported XY and XYZ geometries
  160. # get_coordinates(geometry, include_z=False, return_index=False)
  161. # shapely 2.1: shows deprecation warning about positional 'include_z' and 'return_index'
  162. # get_coordinates(geometry, include_z=False, return_index=False, *, include_m=False)
  163. # shapely 2.2(?): enforce keyword-only arguments after 'geometry'
  164. # get_coordinates(geometry, *, include_z=False, include_m=False, return_index=False)
  165. @deprecate_positional(["include_z", "return_index"], category=DeprecationWarning)
  166. def get_coordinates(geometry, include_z=False, return_index=False, *, include_m=False):
  167. """Get coordinates from a geometry array as an array of floats.
  168. The shape of the returned array is (N, 2), with N being the number of
  169. coordinate pairs. The shape of the data may also be (N, 3) or (N, 4),
  170. depending on ``include_z`` and ``include_m`` options.
  171. Parameters
  172. ----------
  173. geometry : Geometry or array_like
  174. Geometry or geometries to get the coordinates of.
  175. include_z, include_m : bool, default False
  176. If both are False, return XY (2D) geometries.
  177. If both are True, return XYZM (4D) geometries.
  178. If either are True, return XYZ or XYM (3D) geometries.
  179. If a geometry has no Z or M dimension, extra coordinate data will be NaN.
  180. .. versionadded:: 2.1.0
  181. The ``include_m`` parameter was added to support XYM (3D) and
  182. XYZM (4D) geometries available with GEOS 3.12.0 or later.
  183. With older GEOS versions, M dimension coordinates will be NaN.
  184. return_index : bool, default False
  185. If True, also return the index of each returned geometry as a separate
  186. ndarray of integers. For multidimensional arrays, this indexes into the
  187. flattened array (in C contiguous order).
  188. Notes
  189. -----
  190. .. deprecated:: 2.1.0
  191. A deprecation warning is shown if ``include_z`` or ``return_index`` are
  192. specified as positional arguments. In a future release, these will
  193. need to be specified as keyword arguments.
  194. Examples
  195. --------
  196. >>> import shapely
  197. >>> from shapely import LineString, Point
  198. >>> shapely.get_coordinates(Point(1, 2)).tolist()
  199. [[1.0, 2.0]]
  200. >>> shapely.get_coordinates(LineString([(2, 2), (4, 4)])).tolist()
  201. [[2.0, 2.0], [4.0, 4.0]]
  202. >>> shapely.get_coordinates(None)
  203. array([], shape=(0, 2), dtype=float64)
  204. By default the third dimension is ignored:
  205. >>> shapely.get_coordinates(Point(1, 2, 3)).tolist()
  206. [[1.0, 2.0]]
  207. >>> shapely.get_coordinates(Point(1, 2, 3), include_z=True).tolist()
  208. [[1.0, 2.0, 3.0]]
  209. If geometries don't have Z or M dimension, these values will be NaN:
  210. >>> pt = Point(1, 2)
  211. >>> shapely.get_coordinates(pt, include_z=True).tolist()
  212. [[1.0, 2.0, nan]]
  213. >>> shapely.get_coordinates(pt, include_z=True, include_m=True).tolist()
  214. [[1.0, 2.0, nan, nan]]
  215. When ``return_index=True``, indexes are returned also:
  216. >>> geometries = [LineString([(2, 2), (4, 4)]), Point(0, 0)]
  217. >>> coordinates, index = shapely.get_coordinates(geometries, return_index=True)
  218. >>> coordinates.tolist(), index.tolist()
  219. ([[2.0, 2.0], [4.0, 4.0], [0.0, 0.0]], [0, 0, 1])
  220. """
  221. return lib.get_coordinates(
  222. np.asarray(geometry, dtype=np.object_), include_z, include_m, return_index
  223. )
  224. def set_coordinates(geometry, coordinates):
  225. """Adapts the coordinates of a geometry array in-place.
  226. If the coordinates array has shape (N, 2), all returned geometries
  227. will be two-dimensional, and the third dimension will be discarded,
  228. if present. If the coordinates array has shape (N, 3), the returned
  229. geometries preserve the dimensionality of the input geometries.
  230. .. warning::
  231. The geometry array is modified in-place! If you do not want to
  232. modify the original array, you can do
  233. ``set_coordinates(arr.copy(), newcoords)``.
  234. Parameters
  235. ----------
  236. geometry : Geometry or array_like
  237. Geometry or geometries to set the coordinates of.
  238. coordinates: array_like
  239. An array of coordinates to set.
  240. See Also
  241. --------
  242. transform : Returns a copy of a geometry array with a function applied to its
  243. coordinates.
  244. Examples
  245. --------
  246. >>> import shapely
  247. >>> from shapely import LineString, Point
  248. >>> shapely.set_coordinates(Point(0, 0), [[1, 1]])
  249. <POINT (1 1)>
  250. >>> shapely.set_coordinates(
  251. ... [Point(0, 0), LineString([(0, 0), (0, 0)])],
  252. ... [[1, 2], [3, 4], [5, 6]]
  253. ... ).tolist()
  254. [<POINT (1 2)>, <LINESTRING (3 4, 5 6)>]
  255. >>> shapely.set_coordinates([None, Point(0, 0)], [[1, 2]]).tolist()
  256. [None, <POINT (1 2)>]
  257. Third dimension of input geometry is discarded if coordinates array does
  258. not include one:
  259. >>> shapely.set_coordinates(Point(0, 0, 0), [[1, 1]])
  260. <POINT (1 1)>
  261. >>> shapely.set_coordinates(Point(0, 0, 0), [[1, 1, 1]])
  262. <POINT Z (1 1 1)>
  263. """
  264. geometry_arr = np.asarray(geometry, dtype=np.object_)
  265. coordinates = np.atleast_2d(np.asarray(coordinates)).astype(np.float64)
  266. if coordinates.ndim != 2:
  267. raise ValueError(
  268. f"The coordinate array should have dimension of 2 (has {coordinates.ndim})"
  269. )
  270. n_coords = lib.count_coordinates(geometry_arr)
  271. if (coordinates.shape[0] != n_coords) or (coordinates.shape[1] not in {2, 3}):
  272. raise ValueError(
  273. f"The coordinate array has an invalid shape {coordinates.shape}"
  274. )
  275. lib.set_coordinates(geometry_arr, coordinates)
  276. if geometry_arr.ndim == 0 and not isinstance(geometry, np.ndarray):
  277. return geometry_arr.item()
  278. return geometry_arr