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 6 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
30 changes: 30 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,32 @@ 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

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)
30 changes: 30 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,32 @@ 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 tje minimum bounding circle that encloses an input geometry.
martinfleis marked this conversation as resolved.
Show resolved Hide resolved

Parameters
----------
geometry : Geometry or array_like

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_mminimum_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)
25 changes: 25 additions & 0 deletions src/ufuncs.c
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,29 @@ 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};
static int GEOSMinimumBoundingRadius(void* context, GEOSGeometry* geom, double* radius) {
martinfleis marked this conversation as resolved.
Show resolved Hide resolved
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
#if GEOS_SINCE_3_7_0
static void* reverse_data[1] = {GEOSReverse_r};
Expand Down Expand Up @@ -3012,6 +3035,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