| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335 |
- """Methods that operate on the coordinates of geometries."""
- import numpy as np
- import shapely
- from shapely import lib
- from shapely.decorators import deprecate_positional
- __all__ = ["count_coordinates", "get_coordinates", "set_coordinates", "transform"]
- # Note: future plan is to change this signature over a few releases:
- # shapely 2.0: only supported XY and XYZ geometries
- # transform(geometry, transformation, include_z=False)
- # shapely 2.1: shows deprecation warning about positional 'include_z' arg
- # transform(geometry, transformation, include_z=False, *, interleaved=True)
- # shapely 2.2(?): enforce keyword-only arguments after 'transformation'
- # transform(geometry, transformation, *, include_z=False, interleaved=True)
- @deprecate_positional(["include_z"], category=DeprecationWarning)
- def transform(
- geometry,
- transformation,
- include_z: bool | None = False,
- *,
- interleaved: bool = True,
- ):
- """Apply a function to the coordinates of a geometry.
- With the default of ``include_z=False``, all returned geometries will be
- two-dimensional; the third dimension will be discarded, if present.
- When specifying ``include_z=True``, the returned geometries preserve
- the dimensionality of the respective input geometries.
- Parameters
- ----------
- geometry : Geometry or array_like
- Geometry or geometries to transform.
- transformation : function
- A function that transforms a (N, 2) or (N, 3) ndarray of float64 to
- another (N, 2) or (N, 3) ndarray of float64.
- The function may not change N.
- include_z : bool, optional, default False
- If False, always return 2D geometries.
- If True, the data being passed to the
- transformation function will include the third dimension
- (if a geometry has no third dimension, the z-coordinates
- will be NaN). If None, will infer the dimensionality per
- input geometry using ``has_z``, which may result in 2 calls to
- the transformation function. Note that this inference
- can be unreliable with empty geometries or NaN coordinates: for a
- guaranteed result, it is recommended to specify ``include_z`` explicitly.
- interleaved : bool, default True
- If set to False, the transformation function should accept 2 or 3 separate
- one-dimensional arrays (x, y and optional z) instead of a single
- two-dimensional array.
- .. versionadded:: 2.1.0
- Notes
- -----
- .. deprecated:: 2.1.0
- A deprecation warning is shown if ``include_z`` is specified as a
- positional argument. This will need to be specified as a keyword
- argument in a future release.
- See Also
- --------
- has_z
- Examples
- --------
- >>> import shapely
- >>> from shapely import LineString, Point
- >>> shapely.transform(Point(0, 0), lambda x: x + 1)
- <POINT (1 1)>
- >>> shapely.transform(LineString([(2, 2), (4, 4)]), lambda x: x * [2, 3])
- <LINESTRING (4 6, 8 12)>
- >>> shapely.transform(None, lambda x: x) is None
- True
- >>> shapely.transform([Point(0, 0), None], lambda x: x).tolist()
- [<POINT (0 0)>, None]
- The presence of a third dimension can be automatically detected, or
- controlled explicitly:
- >>> shapely.transform(Point(0, 0, 0), lambda x: x + 1)
- <POINT (1 1)>
- >>> shapely.transform(Point(0, 0, 0), lambda x: x + 1, include_z=True)
- <POINT Z (1 1 1)>
- >>> shapely.transform(Point(0, 0, 0), lambda x: x + 1, include_z=None)
- <POINT Z (1 1 1)>
- With interleaved=False, the call signature of the transformation is different:
- >>> shapely.transform(LineString([(1, 2), (3, 4)]), lambda x, y: (x + 1, y), \
- interleaved=False)
- <LINESTRING (2 2, 4 4)>
- Or with a z coordinate:
- >>> shapely.transform(Point(0, 0, 0), lambda x, y, z: (x + 1, y, z + 2), \
- interleaved=False, include_z=True)
- <POINT Z (1 0 2)>
- Using pyproj >= 2.1, the following example will reproject Shapely geometries
- from EPSG 4326 to EPSG 32618:
- >>> from pyproj import Transformer
- >>> transformer = Transformer.from_crs(4326, 32618, always_xy=True)
- >>> shapely.transform(Point(-75, 50), transformer.transform, interleaved=False)
- <POINT (500000 5538630.703)>
- """
- geometry_arr = np.array(geometry, dtype=np.object_) # makes a copy
- if include_z is None:
- has_z = shapely.has_z(geometry_arr)
- result = np.empty_like(geometry_arr)
- result[has_z] = transform(
- geometry_arr[has_z], transformation, include_z=True, interleaved=interleaved
- )
- result[~has_z] = transform(
- geometry_arr[~has_z],
- transformation,
- include_z=False,
- interleaved=interleaved,
- )
- else:
- # TODO: expose include_m
- include_m = False
- coordinates = lib.get_coordinates(geometry_arr, include_z, include_m, False)
- if interleaved:
- new_coordinates = transformation(coordinates)
- else:
- new_coordinates = np.asarray(
- transformation(*coordinates.T), dtype=np.float64
- ).T
- # check the array to yield understandable error messages
- if not isinstance(new_coordinates, np.ndarray) or new_coordinates.ndim != 2:
- raise ValueError(
- "The provided transformation did not return a two-dimensional numpy "
- "array"
- )
- if new_coordinates.dtype != np.float64:
- raise ValueError(
- "The provided transformation returned an array with an unexpected "
- f"dtype ({new_coordinates.dtype})"
- )
- if new_coordinates.shape != coordinates.shape:
- # if the shape is too small we will get a segfault
- raise ValueError(
- "The provided transformation returned an array with an unexpected "
- f"shape ({new_coordinates.shape})"
- )
- result = lib.set_coordinates(geometry_arr, new_coordinates)
- if result.ndim == 0 and not isinstance(geometry, np.ndarray):
- return result.item()
- return result
- def count_coordinates(geometry):
- """Count the number of coordinate pairs in a geometry array.
- Parameters
- ----------
- geometry : Geometry or array_like
- Geometry or geometries to count the coordinates of.
- Examples
- --------
- >>> import shapely
- >>> from shapely import LineString, Point
- >>> shapely.count_coordinates(Point(0, 0))
- 1
- >>> shapely.count_coordinates(LineString([(2, 2), (4, 2)]))
- 2
- >>> shapely.count_coordinates(None)
- 0
- >>> shapely.count_coordinates([Point(0, 0), None])
- 1
- """
- return lib.count_coordinates(np.asarray(geometry, dtype=np.object_))
- # Note: future plan is to change this signature over a few releases:
- # shapely 2.0: only supported XY and XYZ geometries
- # get_coordinates(geometry, include_z=False, return_index=False)
- # shapely 2.1: shows deprecation warning about positional 'include_z' and 'return_index'
- # get_coordinates(geometry, include_z=False, return_index=False, *, include_m=False)
- # shapely 2.2(?): enforce keyword-only arguments after 'geometry'
- # get_coordinates(geometry, *, include_z=False, include_m=False, return_index=False)
- @deprecate_positional(["include_z", "return_index"], category=DeprecationWarning)
- def get_coordinates(geometry, include_z=False, return_index=False, *, include_m=False):
- """Get coordinates from a geometry array as an array of floats.
- The shape of the returned array is (N, 2), with N being the number of
- coordinate pairs. The shape of the data may also be (N, 3) or (N, 4),
- depending on ``include_z`` and ``include_m`` options.
- Parameters
- ----------
- geometry : Geometry or array_like
- Geometry or geometries to get the coordinates of.
- include_z, include_m : bool, default False
- If both are False, return XY (2D) geometries.
- If both are True, return XYZM (4D) geometries.
- If either are True, return XYZ or XYM (3D) geometries.
- If a geometry has no Z or M dimension, extra coordinate data will be NaN.
- .. versionadded:: 2.1.0
- The ``include_m`` parameter was added to support XYM (3D) and
- XYZM (4D) geometries available with GEOS 3.12.0 or later.
- With older GEOS versions, M dimension coordinates will be NaN.
- return_index : bool, default False
- If True, also return the index of each returned geometry as a separate
- ndarray of integers. For multidimensional arrays, this indexes into the
- flattened array (in C contiguous order).
- Notes
- -----
- .. deprecated:: 2.1.0
- A deprecation warning is shown if ``include_z`` or ``return_index`` are
- specified as positional arguments. In a future release, these will
- need to be specified as keyword arguments.
- Examples
- --------
- >>> import shapely
- >>> from shapely import LineString, Point
- >>> shapely.get_coordinates(Point(1, 2)).tolist()
- [[1.0, 2.0]]
- >>> shapely.get_coordinates(LineString([(2, 2), (4, 4)])).tolist()
- [[2.0, 2.0], [4.0, 4.0]]
- >>> shapely.get_coordinates(None)
- array([], shape=(0, 2), dtype=float64)
- By default the third dimension is ignored:
- >>> shapely.get_coordinates(Point(1, 2, 3)).tolist()
- [[1.0, 2.0]]
- >>> shapely.get_coordinates(Point(1, 2, 3), include_z=True).tolist()
- [[1.0, 2.0, 3.0]]
- If geometries don't have Z or M dimension, these values will be NaN:
- >>> pt = Point(1, 2)
- >>> shapely.get_coordinates(pt, include_z=True).tolist()
- [[1.0, 2.0, nan]]
- >>> shapely.get_coordinates(pt, include_z=True, include_m=True).tolist()
- [[1.0, 2.0, nan, nan]]
- When ``return_index=True``, indexes are returned also:
- >>> geometries = [LineString([(2, 2), (4, 4)]), Point(0, 0)]
- >>> coordinates, index = shapely.get_coordinates(geometries, return_index=True)
- >>> coordinates.tolist(), index.tolist()
- ([[2.0, 2.0], [4.0, 4.0], [0.0, 0.0]], [0, 0, 1])
- """
- return lib.get_coordinates(
- np.asarray(geometry, dtype=np.object_), include_z, include_m, return_index
- )
- def set_coordinates(geometry, coordinates):
- """Adapts the coordinates of a geometry array in-place.
- If the coordinates array has shape (N, 2), all returned geometries
- will be two-dimensional, and the third dimension will be discarded,
- if present. If the coordinates array has shape (N, 3), the returned
- geometries preserve the dimensionality of the input geometries.
- .. warning::
- The geometry array is modified in-place! If you do not want to
- modify the original array, you can do
- ``set_coordinates(arr.copy(), newcoords)``.
- Parameters
- ----------
- geometry : Geometry or array_like
- Geometry or geometries to set the coordinates of.
- coordinates: array_like
- An array of coordinates to set.
- See Also
- --------
- transform : Returns a copy of a geometry array with a function applied to its
- coordinates.
- Examples
- --------
- >>> import shapely
- >>> from shapely import LineString, Point
- >>> shapely.set_coordinates(Point(0, 0), [[1, 1]])
- <POINT (1 1)>
- >>> shapely.set_coordinates(
- ... [Point(0, 0), LineString([(0, 0), (0, 0)])],
- ... [[1, 2], [3, 4], [5, 6]]
- ... ).tolist()
- [<POINT (1 2)>, <LINESTRING (3 4, 5 6)>]
- >>> shapely.set_coordinates([None, Point(0, 0)], [[1, 2]]).tolist()
- [None, <POINT (1 2)>]
- Third dimension of input geometry is discarded if coordinates array does
- not include one:
- >>> shapely.set_coordinates(Point(0, 0, 0), [[1, 1]])
- <POINT (1 1)>
- >>> shapely.set_coordinates(Point(0, 0, 0), [[1, 1, 1]])
- <POINT Z (1 1 1)>
- """
- geometry_arr = np.asarray(geometry, dtype=np.object_)
- coordinates = np.atleast_2d(np.asarray(coordinates)).astype(np.float64)
- if coordinates.ndim != 2:
- raise ValueError(
- f"The coordinate array should have dimension of 2 (has {coordinates.ndim})"
- )
- n_coords = lib.count_coordinates(geometry_arr)
- if (coordinates.shape[0] != n_coords) or (coordinates.shape[1] not in {2, 3}):
- raise ValueError(
- f"The coordinate array has an invalid shape {coordinates.shape}"
- )
- lib.set_coordinates(geometry_arr, coordinates)
- if geometry_arr.ndim == 0 and not isinstance(geometry, np.ndarray):
- return geometry_arr.item()
- return geometry_arr
|