Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: add minimum_bounding_circle and minimum_bounding_radius #315

Merged
merged 9 commits into from
Apr 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Version 0.10 (unreleased)
* Addition of a ``polygonize`` function (#275)
* Addition of a ``polygonize_full`` function (#298)
* Addition of a ``segmentize`` function for GEOS >= 3.10 (#299)
* Addition of ``minimum_bounding_circle`` and ``minimum_bounding_radius`` functions for GEOS >= 3.8 (#315)

**Bug fixes**

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


Expand Down
33 changes: 33 additions & 0 deletions pygeos/constructive.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"simplify",
"snap",
"voronoi_polygons",
"minimum_bounding_circle",
]


Expand Down Expand Up @@ -821,3 +822,35 @@ def voronoi_polygons(
<pygeos.Geometry GEOMETRYCOLLECTION EMPTY>
"""
return lib.voronoi_polygons(geometry, tolerance, extend_to, only_edges, **kwargs)


@requires_geos("3.8.0")
@multithreading_enabled
def minimum_bounding_circle(geometry, **kwargs):
"""Computes the minimum bounding circle that encloses an input geometry.

Parameters
----------
geometry : Geometry or array_like
**kwargs
For other keyword-only arguments, see the
`NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.

martinfleis marked this conversation as resolved.
Show resolved Hide resolved
Examples
--------
>>> minimum_bounding_circle(Geometry("POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))"))
<pygeos.Geometry POLYGON ((12.1 5, 11.9 3.62, 11.5 2.29, 10.9 1.07, 10 4.44e...>
>>> minimum_bounding_circle(Geometry("LINESTRING (1 1, 10 10)"))
<pygeos.Geometry POLYGON ((11.9 5.5, 11.7 4.26, 11.4 3.06, 10.8 1.96, 10 1, ...>
>>> minimum_bounding_circle(Geometry("MULTIPOINT (2 2, 4 2)"))
<pygeos.Geometry POLYGON ((4 2, 3.98 1.8, 3.92 1.62, 3.83 1.44, 3.71 1.29, 3...>
>>> minimum_bounding_circle(Geometry("POINT (0 1)"))
<pygeos.Geometry POINT (0 1)>
>>> minimum_bounding_circle(Geometry("GEOMETRYCOLLECTION EMPTY"))
<pygeos.Geometry POLYGON EMPTY>
martinfleis marked this conversation as resolved.
Show resolved Hide resolved

See also
--------
minimum_bounding_radius
"""
return lib.minimum_bounding_circle(geometry, **kwargs)
34 changes: 34 additions & 0 deletions pygeos/measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"hausdorff_distance",
"frechet_distance",
"minimum_clearance",
"minimum_bounding_radius",
]


Expand Down Expand Up @@ -277,3 +278,36 @@ def minimum_clearance(geometry, **kwargs):
nan
"""
return lib.minimum_clearance(geometry, **kwargs)


@requires_geos("3.8.0")
@multithreading_enabled
def minimum_bounding_radius(geometry, **kwargs):
"""Computes the radius of the minimum bounding circle that encloses an input geometry.

Parameters
----------
geometry : Geometry or array_like
**kwargs
For other keyword-only arguments, see the
`NumPy ufunc docs <https://numpy.org/doc/stable/reference/ufuncs.html#ufuncs-kwargs>`_.


martinfleis marked this conversation as resolved.
Show resolved Hide resolved
Examples
--------
>>> minimum_bounding_radius(Geometry("POLYGON ((0 5, 5 10, 10 5, 5 0, 0 5))"))
5.0
>>> minimum_bounding_radius(Geometry("LINESTRING (1 1, 1 10)"))
4.5
>>> minimum_bounding_radius(Geometry("MULTIPOINT (2 2, 4 2)"))
1.0
>>> minimum_bounding_radius(Geometry("POINT (0 1)"))
0.0
>>> minimum_bounding_radius(Geometry("GEOMETRYCOLLECTION EMPTY"))
0.0

See also
--------
minimum_bounding_circle
"""
return lib.minimum_bounding_radius(geometry, **kwargs)
42 changes: 42 additions & 0 deletions pygeos/test/test_constructive.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,3 +687,45 @@ def test_segmentize_none():
def test_segmentize(geometry, tolerance, expected):
actual = pygeos.segmentize(geometry, tolerance)
assert pygeos.equals(actual, geometry).all()


@pytest.mark.skipif(pygeos.geos_version < (3, 8, 0), reason="GEOS < 3.8")
@pytest.mark.parametrize("geometry", all_types)
def test_minimum_bounding_circle_all_types(geometry):
actual = pygeos.minimum_bounding_circle([geometry, geometry])
assert actual.shape == (2,)
assert actual[0] is None or isinstance(actual[0], Geometry)

actual = pygeos.minimum_bounding_circle(None)
assert actual is None


@pytest.mark.skipif(pygeos.geos_version < (3, 8, 0), reason="GEOS < 3.8")
@pytest.mark.parametrize(
"geometry, expected",
[
(
pygeos.Geometry("POLYGON ((0 5, 5 10, 10 5, 5 0, 0 5))"),
pygeos.buffer(pygeos.Geometry("POINT (5 5)"), 5),
),
(
pygeos.Geometry("LINESTRING (1 0, 1 10)"),
pygeos.buffer(pygeos.Geometry("POINT (1 5)"), 5),
),
(
pygeos.Geometry("MULTIPOINT (2 2, 4 2)"),
pygeos.buffer(pygeos.Geometry("POINT (3 2)"), 1),
),
(
pygeos.Geometry("POINT (2 2)"),
pygeos.Geometry("POINT (2 2)"),
),
(
pygeos.Geometry("GEOMETRYCOLLECTION EMPTY"),
pygeos.Geometry("POLYGON EMPTY"),
),
],
)
def test_minimum_bounding_circle(geometry, expected):
actual = pygeos.minimum_bounding_circle(geometry)
assert pygeos.equals(actual, expected).all()
31 changes: 31 additions & 0 deletions pygeos/test/test_measurement.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,34 @@ def test_minimum_clearance_nonexistent():
def test_minimum_clearance_missing():
actual = pygeos.minimum_clearance(None)
assert np.isnan(actual)


@pytest.mark.skipif(pygeos.geos_version < (3, 8, 0), reason="GEOS < 3.8")
@pytest.mark.parametrize(
"geometry, expected",
[
(
pygeos.Geometry("POLYGON ((0 5, 5 10, 10 5, 5 0, 0 5))"),
5,
),
(
pygeos.Geometry("LINESTRING (1 0, 1 10)"),
5,
),
(
pygeos.Geometry("MULTIPOINT (2 2, 4 2)"),
1,
),
(
pygeos.Geometry("POINT (2 2)"),
0,
),
(
pygeos.Geometry("GEOMETRYCOLLECTION EMPTY"),
0,
),
],
)
def test_minimum_bounding_radius(geometry, expected):
actual = pygeos.minimum_bounding_radius(geometry)
assert actual == pytest.approx(expected, abs=1e-12)
26 changes: 26 additions & 0 deletions src/ufuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,17 @@ static void* polygons_without_holes_data[1] = {GEOSLinearRingToPolygon};
static void* build_area_data[1] = {GEOSBuildArea_r};
static void* make_valid_data[1] = {GEOSMakeValid_r};
static void* coverage_union_data[1] = {GEOSCoverageUnion_r};
static void* GEOSMinimumBoundingCircleWithReturn(void* context, void* geom) {
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
GEOSGeometry* center = NULL;
double radius;
GEOSGeometry* ret = GEOSMinimumBoundingCircle_r(context, geom, &radius, &center);
if (ret == NULL) {
return NULL;
}
GEOSGeom_destroy_r(context, center);
return ret;
}
static void* minimum_bounding_circle_data[1] = {GEOSMinimumBoundingCircleWithReturn};
#endif
#if GEOS_SINCE_3_7_0
static void* reverse_data[1] = {GEOSReverse_r};
Expand Down Expand Up @@ -930,6 +941,19 @@ static int MinimumClearance(void* context, void* a, double* b) {
}
static void* minimum_clearance_data[1] = {MinimumClearance};
#endif
#if GEOS_SINCE_3_8_0
static int GEOSMinimumBoundingRadius(void* context, GEOSGeometry* geom, double* radius) {
GEOSGeometry* center = NULL;
GEOSGeometry* ret = GEOSMinimumBoundingCircle_r(context, geom, radius, &center);
if (ret == NULL) {
return 0; // exception code
}
GEOSGeom_destroy_r(context, center);
GEOSGeom_destroy_r(context, ret);
return 1; // success code
}
static void* minimum_bounding_radius_data[1] = {GEOSMinimumBoundingRadius};
#endif
typedef int FuncGEOS_Y_d(void* context, void* a, double* b);
static char Y_d_dtypes[2] = {NPY_OBJECT, NPY_DOUBLE};
static void Y_d_func(char** args, npy_intp* dimensions, npy_intp* steps, void* data) {
Expand Down Expand Up @@ -3012,6 +3036,8 @@ int init_ufuncs(PyObject* m, PyObject* d) {
DEFINE_Y_Y(make_valid);
DEFINE_Y_Y(build_area);
DEFINE_Y_Y(coverage_union);
DEFINE_Y_Y(minimum_bounding_circle);
DEFINE_Y_d(minimum_bounding_radius);
#endif

#if GEOS_SINCE_3_9_0
Expand Down