Skip to content

Commit

Permalink
to_shapely and from_shapely updates (#312)
Browse files Browse the repository at this point in the history
  • Loading branch information
0phoff committed Apr 2, 2021
1 parent 85cb021 commit dcfcf9e
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 42 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Version 0.10 (unreleased)
* Updated ``box`` ufunc to use internal C function for creating polygon
(about 2x faster) and added ``ccw`` parameter to create polygon in
counterclockwise (default) or clockwise direction (#308).
* Added ``to_shapely`` and improved performance of ``from_shapely`` in the case
GEOS versions are different (#312).

**API Changes**

Expand Down Expand Up @@ -54,6 +56,7 @@ People with a "+" by their names contributed a patch for the first time.
* Brendan Ward
* Casper van der Wel
* Joris Van den Bossche
* 0phoff +


Version 0.9 (2021-01-23)
Expand Down
178 changes: 146 additions & 32 deletions pygeos/io.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from collections.abc import Sized

import numpy as np
Expand All @@ -13,27 +14,75 @@
)


shapely_geos_version = None
ShapelyGeometry = None
ShapelyPreparedGeometry = None
shapely_lgeos = None
shapely_geom_factory = None
shapely_wkb_loads = None
shapely_compatible = None
_shapely_checked = False


def check_shapely_version():
global shapely_geos_version
"""
This function will try to import shapely and extracts some necessary classes and functions from the package.
It also looks if Shapely and PyGEOS use the same GEOS version, as this means the conversion can be faster.
This function sets a few global variables:
- ShapelyGeometry:
shapely.geometry.base.BaseGeometry
- ShapelyPreparedGeometry:
shapely.prepared.PreparedGeometry
- shapely_lgeos:
shapely.geos.lgeos
- shapely_geom_factory:
shapely.geometry.base.geom_factory
- shapely_wkb_loads:
shapely.wkb.loads
- shapely_compatible:
``None`` if shapely is not installed,
``True`` if shapely and PyGEOS use the same GEOS version,
``False`` otherwise
- _shapely_checked:
Mostly internal variable to mark that we already tried to import shapely
"""
global ShapelyGeometry
global ShapelyPreparedGeometry
global shapely_lgeos
global shapely_geom_factory
global shapely_wkb_loads
global shapely_compatible
global _shapely_checked

if not _shapely_checked:
try:
from shapely.geometry.base import BaseGeometry as ShapelyGeometry
from shapely.geos import geos_version_string as shapely_geos_version
from shapely.geometry.base import geom_factory as shapely_geom_factory
from shapely.geos import geos_version_string
from shapely.geos import lgeos as shapely_lgeos
from shapely.prepared import PreparedGeometry as ShapelyPreparedGeometry
from shapely.wkb import loads as shapely_wkb_loads

# shapely has something like: "3.6.2-CAPI-1.10.2 4d2925d6"
# pygeos has something like: "3.6.2-CAPI-1.10.2"
shapely_compatible = True
if not geos_version_string.startswith(geos_capi_version_string):
shapely_compatible = False
warnings.warn(
"The shapely GEOS version ({}) is incompatible "
"with the PyGEOS GEOS version ({}). "
"Conversions between both will be slow".format(
geos_version_string, geos_capi_version_string
)
)
except ImportError:
pass

_shapely_checked = True


__all__ = ["from_shapely", "from_wkb", "from_wkt", "to_wkb", "to_wkt"]
__all__ = ["from_shapely", "from_wkb", "from_wkt", "to_shapely", "to_wkb", "to_wkt"]


def to_wkt(
Expand Down Expand Up @@ -174,6 +223,51 @@ def to_wkb(
)


def to_shapely(geometry):
"""
Converts PyGEOS geometries to Shapely.
Parameters
----------
geometry : shapely Geometry object or array_like
Examples
--------
>>> to_shapely(Geometry("POINT (1 1)")) # doctest: +SKIP
<shapely.geometry.point.Point at 0x7f0c3d737908>
Notes
-----
If PyGEOS and Shapely do not use the same GEOS version,
the conversion happens through the WKB format and will thus be slower.
"""
check_shapely_version()
if shapely_compatible is None:
raise ImportError("This function requires shapely")

unpack = geometry is None or isinstance(geometry, Geometry)
if unpack:
geometry = (geometry,)

if shapely_compatible:
geometry = [
None
if g is None
else shapely_geom_factory(shapely_lgeos.GEOSGeom_clone(g._ptr))
for g in geometry
]
else:
geometry = to_wkb(geometry)
geometry = [None if g is None else shapely_wkb_loads(g) for g in geometry]

if unpack:
return geometry[0]
else:
arr = np.empty(len(geometry), dtype=object)
arr[:] = geometry
return arr


def from_wkt(geometry, on_invalid="raise", **kwargs):
"""
Creates geometries from the Well-Known Text (WKT) representation.
Expand Down Expand Up @@ -247,9 +341,8 @@ def from_wkb(geometry, on_invalid="raise", **kwargs):


def from_shapely(geometry, **kwargs):
"""Creates geometries from shapely Geometry objects.
This function requires the GEOS version of PyGEOS and shapely to be equal.
"""
Creates geometries from shapely Geometry objects.
Parameters
----------
Expand All @@ -263,33 +356,54 @@ def from_shapely(geometry, **kwargs):
>>> from shapely.geometry import Point # doctest: +SKIP
>>> from_shapely(Point(1, 2)) # doctest: +SKIP
<pygeos.Geometry POINT (1 2)>
Notes
-----
If PyGEOS and Shapely do not use the same GEOS version,
the conversion happens through the WKB format and will thus be slower.
"""
check_shapely_version()

if shapely_geos_version is None:
if shapely_compatible is None:
raise ImportError("This function requires shapely")

# shapely has something like: "3.6.2-CAPI-1.10.2 4d2925d6"
# pygeos has something like: "3.6.2-CAPI-1.10.2"
if not shapely_geos_version.startswith(geos_capi_version_string):
raise ImportError(
"The shapely GEOS version ({}) is incompatible with the GEOS "
"version PyGEOS was compiled with ({})".format(
shapely_geos_version, geos_capi_version_string
)
)

if isinstance(geometry, ShapelyGeometry):
# this so that the __array_interface__ of the shapely geometry is not
# used, converting the Geometry to its coordinates
arr = np.empty(1, dtype=object)
arr[0] = geometry
arr.shape = ()
elif not isinstance(geometry, np.ndarray) and isinstance(geometry, Sized):
# geometry is a list/array-like
arr = np.empty(len(geometry), dtype=object)
arr[:] = geometry
if shapely_compatible:
if isinstance(geometry, (ShapelyGeometry, ShapelyPreparedGeometry)):
# this so that the __array_interface__ of the shapely geometry is not
# used, converting the Geometry to its coordinates
arr = np.empty(1, dtype=object)
arr[0] = geometry
arr.shape = ()
elif not isinstance(geometry, np.ndarray) and isinstance(geometry, Sized):
# geometry is a list/array-like
arr = np.empty(len(geometry), dtype=object)
arr[:] = geometry
else:
# we already have a numpy array or we are None
arr = geometry

return lib.from_shapely(arr, **kwargs)
else:
# we already have a numpy array or we are None
arr = geometry
return lib.from_shapely(arr, **kwargs)
unpack = geometry is None or isinstance(
geometry, (ShapelyGeometry, ShapelyPreparedGeometry)
)
if unpack:
geometry = (geometry,)

arr = []
for g in geometry:
if isinstance(g, ShapelyPreparedGeometry):
g = g.context

if g is None:
arr.append(None)
elif g.is_empty and g.geom_type == "Point":
arr.append(
b"\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf8\x7f\x00\x00\x00\x00\x00\x00\xf8\x7f"
)
else:
arr.append(g.wkb)

if unpack:
arr = arr[0]

return from_wkb(arr)

0 comments on commit dcfcf9e

Please sign in to comment.