Skip to content

Commit

Permalink
ENH: Get and set coordinates as a array of floats (#44)
Browse files Browse the repository at this point in the history
  • Loading branch information
caspervdw authored and jorisvandenbossche committed Oct 6, 2019
1 parent 808539d commit b757abb
Show file tree
Hide file tree
Showing 10 changed files with 756 additions and 5 deletions.
9 changes: 9 additions & 0 deletions docs/coordinates.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
:mod:`pygeos.coordinates`
=========================

.. automodule:: pygeos.coordinates
:members:
:undoc-members:
:special-members:
:inherited-members:
:show-inheritance:
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ API Reference
measurement
predicates
set_operations
coordinates


Indices and tables
Expand Down
4 changes: 3 additions & 1 deletion pygeos/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
from .measurement import *
from .set_operations import *
from .linear import *
from .coordinates import *

from ._version import get_versions
__version__ = get_versions()['version']

__version__ = get_versions()["version"]
del get_versions
127 changes: 127 additions & 0 deletions pygeos/coordinates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from . import lib, Geometry
import numpy as np

__all__ = ["apply", "count_coordinates", "get_coordinates", "set_coordinates"]


def apply(geometry, transformation):
"""Returns a copy of a geometry array with a function applied to its
coordinates.
All returned geometries will be two-dimensional; the third dimension will
be discarded, if present.
Parameters
----------
geometry : Geometry or array_like
transformation : function
A function that transforms a (N, 2) ndarray of float64 to another
(N, 2) ndarray of float64.
Examples
--------
>>> apply(Geometry("POINT (0 0)"), lambda x: x + 1)
<pygeos.Geometry POINT (1 1)>
>>> apply(Geometry("LINESTRING (2 2, 4 4)"), lambda x: x * [2, 3])
<pygeos.Geometry LINESTRING (4 6, 8 12)>
>>> apply(None, lambda x: x) is None
True
>>> apply([Geometry("POINT (0 0)"), None], lambda x: x).tolist()
[<pygeos.Geometry POINT (0 0)>, None]
"""
geometry_arr = np.array(geometry, dtype=np.object) # makes a copy
coordinates = lib.get_coordinates(geometry_arr)
new_coordinates = transformation(coordinates)
# check the array to yield understandable error messages
if not isinstance(new_coordinates, np.ndarray):
raise ValueError("The provided transformation did not return a numpy array")
if new_coordinates.dtype != np.float64:
raise ValueError(
"The provided transformation returned an array with an unexpected "
"dtype ({})".format(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 "
"shape ({})".format(new_coordinates.shape)
)
geometry_arr = lib.set_coordinates(geometry_arr, new_coordinates)
if geometry_arr.ndim == 0 and not isinstance(geometry, np.ndarray):
return geometry_arr.item()
return geometry_arr


def count_coordinates(geometry):
"""Counts the number of coordinate pairs in a geometry array.
Parameters
----------
geometry : Geometry or array_like
Examples
--------
>>> count_coordinates(Geometry("POINT (0 0)"))
1
>>> count_coordinates(Geometry("LINESTRING (2 2, 4 4)"))
2
>>> count_coordinates(None)
0
>>> count_coordinates([Geometry("POINT (0 0)"), None])
1
"""
return lib.count_coordinates(np.asarray(geometry, dtype=np.object))


def get_coordinates(geometry):
"""Gets 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. Three-dimensional data is ignored.
Parameters
----------
geometry : Geometry or array_like
Examples
--------
>>> get_coordinates(Geometry("POINT (0 0)")).tolist()
[[0.0, 0.0]]
>>> get_coordinates(Geometry("LINESTRING (2 2, 4 4)")).tolist()
[[2.0, 2.0], [4.0, 4.0]]
>>> get_coordinates(None)
array([], shape=(0, 2), dtype=float64)
"""
return lib.get_coordinates(np.asarray(geometry, dtype=np.object))


def set_coordinates(geometry, coordinates):
"""Returns a copy of a geometry array with different coordinates.
All returned geometries will be two-dimensional; the third dimension will
be discarded, if present.
Parameters
----------
geometry : Geometry or array_like
coordinates: array_like
Examples
--------
>>> set_coordinates(Geometry("POINT (0 0)"), [[1, 1]])
<pygeos.Geometry POINT (1 1)>
>>> set_coordinates([Geometry("POINT (0 0)"), Geometry("LINESTRING (0 0, 0 0)")], [[1, 2], [3, 4], [5, 6]]).tolist()
[<pygeos.Geometry POINT (1 2)>, <pygeos.Geometry LINESTRING (3 4, 5 6)>]
>>> set_coordinates([None, Geometry("POINT (0 0)")], [[1, 2]]).tolist()
[None, <pygeos.Geometry POINT (1 2)>]
"""
geometry_arr = np.asarray(geometry, dtype=np.object)
coordinates = np.atleast_2d(np.asarray(coordinates)).astype(np.float64)
if coordinates.shape != (lib.count_coordinates(geometry_arr), 2):
raise ValueError(
"The coordinate array has an invalid shape {}".format(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
2 changes: 1 addition & 1 deletion pygeos/test/test_constructive.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from pygeos import Geometry, GEOSException

from .common import point, all_types, geometry_collection
from .common import point, all_types

CONSTRUCTIVE_NO_ARGS = (
pygeos.boundary,
Expand Down
159 changes: 159 additions & 0 deletions pygeos/test/test_coordinates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import pytest
import pygeos
from pygeos import apply, count_coordinates, get_coordinates, set_coordinates
import numpy as np
from numpy.testing import assert_equal

from .common import empty
from .common import point
from .common import point_z
from .common import line_string
from .common import linear_ring
from .common import polygon
from .common import polygon_with_hole
from .common import multi_point
from .common import multi_line_string
from .common import multi_polygon
from .common import geometry_collection

nested_2 = pygeos.geometrycollections([geometry_collection, point])
nested_3 = pygeos.geometrycollections([nested_2, point])


@pytest.mark.parametrize(
"geoms,count",
[
([], 0),
([empty], 0),
([point, empty], 1),
([empty, point, empty], 1),
([point, None], 1),
([None, point, None], 1),
([point, point], 2),
([point, point_z], 2),
([line_string, linear_ring], 8),
([polygon], 5),
([polygon_with_hole], 10),
([multi_point, multi_line_string], 4),
([multi_polygon], 10),
([geometry_collection], 3),
([nested_2], 4),
([nested_3], 5),
],
)
def test_count_coords(geoms, count):
actual = count_coordinates(np.array(geoms, np.object))
assert actual == count


# fmt: off
@pytest.mark.parametrize(
"geoms,x,y",
[
([], [], []),
([empty], [], []),
([point, empty], [2], [3]),
([empty, point, empty], [2], [3]),
([point, None], [2], [3]),
([None, point, None], [2], [3]),
([point, point], [2, 2], [3, 3]),
([point, point_z], [2, 1], [3, 1]),
([line_string, linear_ring], [0, 1, 1, 0, 1, 1, 0, 0], [0, 0, 1, 0, 0, 1, 1, 0]),
([polygon], [0, 2, 2, 0, 0], [0, 0, 2, 2, 0]),
([polygon_with_hole], [0, 0, 10, 10, 0, 2, 2, 4, 4, 2], [0, 10, 10, 0, 0, 2, 4, 4, 2, 2]),
([multi_point, multi_line_string], [0, 1, 0, 1], [0, 2, 0, 2]),
([multi_polygon], [0, 1, 1, 0, 0, 2.1, 2.2, 2.2, 2.1, 2.1], [0, 0, 1, 1, 0, 2.1, 2.1, 2.2, 2.2, 2.1]),
([geometry_collection], [51, 52, 49], [-1, -1, 2]),
([nested_2], [51, 52, 49, 2], [-1, -1, 2, 3]),
([nested_3], [51, 52, 49, 2, 2], [-1, -1, 2, 3, 3]),
],
) # fmt: on
def test_get_coords(geoms, x, y):
actual = get_coordinates(np.array(geoms, np.object))
expected = np.array([x, y], np.float64).T
assert_equal(actual, expected)


@pytest.mark.parametrize(
"geoms,count,has_ring",
[
([], 0, False),
([empty], 0, False),
([point, empty], 1, False),
([empty, point, empty], 1, False),
([point, None], 1, False),
([None, point, None], 1, False),
([point, point], 2, False),
([point, point_z], 2, False),
([line_string, linear_ring], 8, True),
([polygon], 5, True),
([polygon_with_hole], 10, True),
([multi_point, multi_line_string], 4, False),
([multi_polygon], 10, True),
([geometry_collection], 3, False),
([nested_2], 4, False),
([nested_3], 5, False),
],
)
def test_set_coords(geoms, count, has_ring):
geoms = np.array(geoms, np.object)
if has_ring:
# do not randomize; linearrings / polygons should stay closed
coords = get_coordinates(geoms) + np.random.random((1, 2))
else:
coords = np.random.random((count, 2))
new_geoms = set_coordinates(geoms, coords)
assert_equal(coords, get_coordinates(new_geoms))


def test_set_coords_nan():
geoms = np.array([point])
coords = np.array([[np.nan, np.inf]])
new_geoms = set_coordinates(geoms, coords)
assert_equal(coords, get_coordinates(new_geoms))


def test_set_coords_breaks_ring():
with pytest.raises(pygeos.GEOSException):
set_coordinates(linear_ring, np.random.random((5, 2)))


def test_set_coords_0dim():
# a geometry input returns a geometry
actual = set_coordinates(point, [[1, 1]])
assert isinstance(actual, pygeos.Geometry)
# a 0-dim array input returns a 0-dim array
actual = set_coordinates(np.asarray(point), [[1, 1]])
assert isinstance(actual, np.ndarray)
assert actual.ndim == 0


@pytest.mark.parametrize(
"geoms",
[[], [empty], [None, point, None], [nested_3]],
)
def test_apply(geoms):
geoms = np.array(geoms, np.object)
coordinates_before = get_coordinates(geoms)
new_geoms = apply(geoms, lambda x: x + 1)
assert new_geoms is not geoms
coordinates_after = get_coordinates(new_geoms)
assert_equal(coordinates_before + 1, coordinates_after)


def test_apply_0dim():
# a geometry input returns a geometry
actual = apply(point, lambda x: x + 1)
assert isinstance(actual, pygeos.Geometry)
# a 0-dim array input returns a 0-dim array
actual = apply(np.asarray(point), lambda x: x + 1)
assert isinstance(actual, np.ndarray)
assert actual.ndim == 0


def test_apply_check_shape():
def remove_coord(arr):
return arr[:-1]

with pytest.raises(ValueError):
apply(linear_ring, remove_coord)
7 changes: 5 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def finalize_options(self):

module_lib = Extension(
"pygeos.lib",
sources=["src/lib.c", "src/geos.c", "src/pygeom.c", "src/ufuncs.c"],
sources=["src/lib.c", "src/geos.c", "src/pygeom.c", "src/ufuncs.c", "src/coords.c"],
**get_geos_paths()
)

Expand All @@ -135,7 +135,10 @@ def finalize_options(self):
packages=["pygeos"],
setup_requires=["numpy"],
install_requires=["numpy>=1.10"],
extras_require={"test": ["pytest"], "docs": ["sphinx", "numpydoc"]},
extras_require={
"test": ["pytest"],
"docs": ["sphinx", "numpydoc"],
},
python_requires=">=3",
include_package_data=True,
ext_modules=[module_lib],
Expand Down

0 comments on commit b757abb

Please sign in to comment.