1import warnings
2from collections.abc import Sized
3
4import numpy as np
5
6from . import Geometry  # noqa
7from . import geos_capi_version_string, lib
8from .enum import ParamEnum
9
10# Allowed options for handling WKB/WKT decoding errors
11# Note: cannot use standard constructor since "raise" is a keyword
12DecodingErrorOptions = ParamEnum(
13    "DecodingErrorOptions", {"ignore": 0, "warn": 1, "raise": 2}
14)
15
16
17ShapelyGeometry = None
18ShapelyPreparedGeometry = None
19shapely_lgeos = None
20shapely_geom_factory = None
21shapely_wkb_loads = None
22shapely_compatible = None
23_shapely_checked = False
24
25
26def check_shapely_version():
27    """
28    This function will try to import shapely and extracts some necessary classes and functions from the package.
29    It also looks if Shapely and PyGEOS use the same GEOS version, as this means the conversion can be faster.
30
31    This function sets a few global variables:
32
33    - ShapelyGeometry:
34        shapely.geometry.base.BaseGeometry
35    - ShapelyPreparedGeometry:
36        shapely.prepared.PreparedGeometry
37    - shapely_lgeos:
38        shapely.geos.lgeos
39    - shapely_geom_factory:
40        shapely.geometry.base.geom_factory
41    - shapely_wkb_loads:
42        shapely.wkb.loads
43    - shapely_compatible:
44        ``None`` if shapely is not installed,
45        ``True`` if shapely and PyGEOS use the same GEOS version,
46        ``False`` otherwise
47    - _shapely_checked:
48        Mostly internal variable to mark that we already tried to import shapely
49    """
50    global ShapelyGeometry
51    global ShapelyPreparedGeometry
52    global shapely_lgeos
53    global shapely_geom_factory
54    global shapely_wkb_loads
55    global shapely_compatible
56    global _shapely_checked
57
58    if not _shapely_checked:
59        try:
60            from shapely.geometry.base import BaseGeometry as ShapelyGeometry
61            from shapely.geometry.base import geom_factory as shapely_geom_factory
62            from shapely.geos import geos_version_string
63            from shapely.geos import lgeos as shapely_lgeos
64            from shapely.prepared import PreparedGeometry as ShapelyPreparedGeometry
65            from shapely.wkb import loads as shapely_wkb_loads
66
67            # shapely has something like: "3.6.2-CAPI-1.10.2 4d2925d6"
68            # pygeos has something like: "3.6.2-CAPI-1.10.2"
69            shapely_compatible = True
70            if not geos_version_string.startswith(geos_capi_version_string):
71                shapely_compatible = False
72                warnings.warn(
73                    "The shapely GEOS version ({}) is incompatible "
74                    "with the PyGEOS GEOS version ({}). "
75                    "Conversions between both will be slow".format(
76                        geos_version_string, geos_capi_version_string
77                    )
78                )
79        except ImportError:
80            pass
81
82        _shapely_checked = True
83
84
85__all__ = ["from_shapely", "from_wkb", "from_wkt", "to_shapely", "to_wkb", "to_wkt"]
86
87
88def to_wkt(
89    geometry,
90    rounding_precision=6,
91    trim=True,
92    output_dimension=3,
93    old_3d=False,
94    **kwargs
95):
96    """
97    Converts to the Well-Known Text (WKT) representation of a Geometry.
98
99    The Well-known Text format is defined in the `OGC Simple Features
100    Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
101
102    Parameters
103    ----------
104    geometry : Geometry or array_like
105    rounding_precision : int, default 6
106        The rounding precision when writing the WKT string. Set to a value of
107        -1 to indicate the full precision.
108    trim : bool, default True
109        If True, trim unnecessary decimals (trailing zeros).
110    output_dimension : int, default 3
111        The output dimension for the WKT string. Supported values are 2 and 3.
112        Specifying 3 means that up to 3 dimensions will be written but 2D
113        geometries will still be represented as 2D in the WKT string.
114    old_3d : bool, default False
115        Enable old style 3D/4D WKT generation. By default, new style 3D/4D WKT
116        (ie. "POINT Z (10 20 30)") is returned, but with ``old_3d=True``
117        the WKT will be formatted in the style "POINT (10 20 30)".
118    **kwargs
119        For other keyword-only arguments, see the
120        `NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
121
122    Examples
123    --------
124    >>> to_wkt(Geometry("POINT (0 0)"))
125    'POINT (0 0)'
126    >>> to_wkt(Geometry("POINT (0 0)"), rounding_precision=3, trim=False)
127    'POINT (0.000 0.000)'
128    >>> to_wkt(Geometry("POINT (0 0)"), rounding_precision=-1, trim=False)
129    'POINT (0.0000000000000000 0.0000000000000000)'
130    >>> to_wkt(Geometry("POINT (1 2 3)"), trim=True)
131    'POINT Z (1 2 3)'
132    >>> to_wkt(Geometry("POINT (1 2 3)"), trim=True, output_dimension=2)
133    'POINT (1 2)'
134    >>> to_wkt(Geometry("POINT (1 2 3)"), trim=True, old_3d=True)
135    'POINT (1 2 3)'
136
137    Notes
138    -----
139    The defaults differ from the default of the GEOS library. To mimic this,
140    use::
141
142        to_wkt(geometry, rounding_precision=-1, trim=False, output_dimension=2)
143
144    """
145    if not np.isscalar(rounding_precision):
146        raise TypeError("rounding_precision only accepts scalar values")
147    if not np.isscalar(trim):
148        raise TypeError("trim only accepts scalar values")
149    if not np.isscalar(output_dimension):
150        raise TypeError("output_dimension only accepts scalar values")
151    if not np.isscalar(old_3d):
152        raise TypeError("old_3d only accepts scalar values")
153
154    return lib.to_wkt(
155        geometry,
156        np.intc(rounding_precision),
157        np.bool_(trim),
158        np.intc(output_dimension),
159        np.bool_(old_3d),
160        **kwargs,
161    )
162
163
164def to_wkb(
165    geometry, hex=False, output_dimension=3, byte_order=-1, include_srid=False, **kwargs
166):
167    r"""
168    Converts to the Well-Known Binary (WKB) representation of a Geometry.
169
170    The Well-Known Binary format is defined in the `OGC Simple Features
171    Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
172
173    The following limitations apply to WKB serialization:
174
175    - linearrings will be converted to linestrings
176    - a point with only NaN coordinates is converted to an empty point
177    - empty points are transformed to 3D in GEOS < 3.8
178    - empty points are transformed to 2D in GEOS 3.8
179
180    Parameters
181    ----------
182    geometry : Geometry or array_like
183    hex : bool, default False
184        If true, export the WKB as a hexidecimal string. The default is to
185        return a binary bytes object.
186    output_dimension : int, default 3
187        The output dimension for the WKB. Supported values are 2 and 3.
188        Specifying 3 means that up to 3 dimensions will be written but 2D
189        geometries will still be represented as 2D in the WKB represenation.
190    byte_order : int, default -1
191        Defaults to native machine byte order (-1). Use 0 to force big endian
192        and 1 for little endian.
193    include_srid : bool, default False
194        If True, the SRID is be included in WKB (this is an extension
195        to the OGC WKB specification).
196    **kwargs
197        For other keyword-only arguments, see the
198        `NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
199
200    Examples
201    --------
202    >>> to_wkb(Geometry("POINT (1 1)"), byte_order=1)
203    b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?'
204    >>> to_wkb(Geometry("POINT (1 1)"), hex=True, byte_order=1)
205    '0101000000000000000000F03F000000000000F03F'
206    """
207    if not np.isscalar(hex):
208        raise TypeError("hex only accepts scalar values")
209    if not np.isscalar(output_dimension):
210        raise TypeError("output_dimension only accepts scalar values")
211    if not np.isscalar(byte_order):
212        raise TypeError("byte_order only accepts scalar values")
213    if not np.isscalar(include_srid):
214        raise TypeError("include_srid only accepts scalar values")
215
216    return lib.to_wkb(
217        geometry,
218        np.bool_(hex),
219        np.intc(output_dimension),
220        np.intc(byte_order),
221        np.bool_(include_srid),
222        **kwargs,
223    )
224
225
226def to_shapely(geometry):
227    """
228    Converts PyGEOS geometries to Shapely.
229
230    Parameters
231    ----------
232    geometry : shapely Geometry object or array_like
233
234    Examples
235    --------
236    >>> to_shapely(Geometry("POINT (1 1)"))   # doctest: +SKIP
237    <shapely.geometry.point.Point at 0x7f0c3d737908>
238
239    Notes
240    -----
241    If PyGEOS and Shapely do not use the same GEOS version,
242    the conversion happens through the WKB format and will thus be slower.
243    """
244    check_shapely_version()
245    if shapely_compatible is None:
246        raise ImportError("This function requires shapely")
247
248    unpack = geometry is None or isinstance(geometry, Geometry)
249    if unpack:
250        geometry = (geometry,)
251
252    if shapely_compatible:
253        geometry = [
254            None
255            if g is None
256            else shapely_geom_factory(shapely_lgeos.GEOSGeom_clone(g._ptr))
257            for g in geometry
258        ]
259    else:
260        geometry = to_wkb(geometry)
261        geometry = [None if g is None else shapely_wkb_loads(g) for g in geometry]
262
263    if unpack:
264        return geometry[0]
265    else:
266        arr = np.empty(len(geometry), dtype=object)
267        arr[:] = geometry
268        return arr
269
270
271def from_wkt(geometry, on_invalid="raise", **kwargs):
272    """
273    Creates geometries from the Well-Known Text (WKT) representation.
274
275    The Well-known Text format is defined in the `OGC Simple Features
276    Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
277
278    Parameters
279    ----------
280    geometry : str or array_like
281        The WKT string(s) to convert.
282    on_invalid : {"raise", "warn", "ignore"}, default "raise"
283        - raise: an exception will be raised if WKT input geometries are invalid.
284        - warn: a warning will be raised and invalid WKT geometries will be
285          returned as ``None``.
286        - ignore: invalid WKT geometries will be returned as ``None`` without a warning.
287    **kwargs
288        For other keyword-only arguments, see the
289        `NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
290
291    Examples
292    --------
293    >>> from_wkt('POINT (0 0)')
294    <pygeos.Geometry POINT (0 0)>
295    """
296    if not np.isscalar(on_invalid):
297        raise TypeError("on_invalid only accepts scalar values")
298
299    invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid))
300
301    return lib.from_wkt(geometry, invalid_handler, **kwargs)
302
303
304def from_wkb(geometry, on_invalid="raise", **kwargs):
305    r"""
306    Creates geometries from the Well-Known Binary (WKB) representation.
307
308    The Well-Known Binary format is defined in the `OGC Simple Features
309    Specification for SQL <https://www.opengeospatial.org/standards/sfs>`__.
310
311
312    Parameters
313    ----------
314    geometry : str or array_like
315        The WKB byte object(s) to convert.
316    on_invalid : {"raise", "warn", "ignore"}, default "raise"
317        - raise: an exception will be raised if WKB input geometries are invalid.
318        - warn: a warning will be raised and invalid WKB geometries will be
319          returned as ``None``.
320        - ignore: invalid WKB geometries will be returned as ``None`` without a warning.
321    **kwargs
322        For other keyword-only arguments, see the
323        `NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
324
325    Examples
326    --------
327    >>> from_wkb(b'\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\xf0?')
328    <pygeos.Geometry POINT (1 1)>
329    """
330
331    if not np.isscalar(on_invalid):
332        raise TypeError("on_invalid only accepts scalar values")
333
334    invalid_handler = np.uint8(DecodingErrorOptions.get_value(on_invalid))
335
336    # ensure the input has object dtype, to avoid numpy inferring it as a
337    # fixed-length string dtype (which removes trailing null bytes upon access
338    # of array elements)
339    geometry = np.asarray(geometry, dtype=object)
340    return lib.from_wkb(geometry, invalid_handler, **kwargs)
341
342
343def from_shapely(geometry, **kwargs):
344    """
345    Creates geometries from shapely Geometry objects.
346
347    Parameters
348    ----------
349    geometry : shapely Geometry object or array_like
350    **kwargs
351        For other keyword-only arguments, see the
352        `NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.
353
354    Examples
355    --------
356    >>> from shapely.geometry import Point   # doctest: +SKIP
357    >>> from_shapely(Point(1, 2))   # doctest: +SKIP
358    <pygeos.Geometry POINT (1 2)>
359
360    Notes
361    -----
362    If PyGEOS and Shapely do not use the same GEOS version,
363    the conversion happens through the WKB format and will thus be slower.
364    """
365    check_shapely_version()
366    if shapely_compatible is None:
367        raise ImportError("This function requires shapely")
368
369    if shapely_compatible:
370        if isinstance(geometry, (ShapelyGeometry, ShapelyPreparedGeometry)):
371            # this so that the __array_interface__ of the shapely geometry is not
372            # used, converting the Geometry to its coordinates
373            arr = np.empty(1, dtype=object)
374            arr[0] = geometry
375            arr.shape = ()
376        elif not isinstance(geometry, np.ndarray) and isinstance(geometry, Sized):
377            # geometry is a list/array-like
378            arr = np.empty(len(geometry), dtype=object)
379            arr[:] = geometry
380        else:
381            # we already have a numpy array or we are None
382            arr = geometry
383
384        return lib.from_shapely(arr, **kwargs)
385    else:
386        unpack = geometry is None or isinstance(
387            geometry, (ShapelyGeometry, ShapelyPreparedGeometry)
388        )
389        if unpack:
390            geometry = (geometry,)
391
392        arr = []
393        for g in geometry:
394            if isinstance(g, ShapelyPreparedGeometry):
395                g = g.context
396
397            if g is None:
398                arr.append(None)
399            elif g.is_empty and g.geom_type == "Point":
400                arr.append(
401                    b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x7f\x00\x00\x00\x00\x00\x00\xf8\x7f"
402                )
403            else:
404                arr.append(g.wkb)
405
406        if unpack:
407            arr = arr[0]
408
409        return from_wkb(arr)
410