diff --git a/CHANGELOG.md b/CHANGELOG.md index d0235b8..8bd3c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Development version +New features and improvements: + +- Added ``minimum_bounding_circle()`` method from shapely to GeoSeries/GeoDataframe (#2621). + Deprecations and compatibility notes: - Added warning that ``unary_union`` will return ``'GEOMETRYCOLLECTION EMPTY'`` instead diff --git a/doc/source/docs/reference/geoseries.rst b/doc/source/docs/reference/geoseries.rst index bf7f4ec..7d7317b 100644 --- a/doc/source/docs/reference/geoseries.rst +++ b/doc/source/docs/reference/geoseries.rst @@ -86,6 +86,7 @@ Constructive methods and attributes GeoSeries.centroid GeoSeries.convex_hull GeoSeries.envelope + GeoSeries.minimum_bounding_circle GeoSeries.simplify GeoSeries.normalize diff --git a/geopandas/_vectorized.py b/geopandas/_vectorized.py index b67cf7e..3640880 100644 --- a/geopandas/_vectorized.py +++ b/geopandas/_vectorized.py @@ -677,6 +677,18 @@ def representative_point(data): return out +def minimum_bounding_circle(data): + if compat.USE_SHAPELY_20: + return shapely.minimum_bounding_circle(data) + elif compat.USE_PYGEOS: + return pygeos.minimum_bounding_circle(data) + else: + raise NotImplementedError( + f"shapely >= 2.0 or PyGEOS is required, " + f"version {shapely.__version__} is installed" + ) + + # # Binary predicates # diff --git a/geopandas/array.py b/geopandas/array.py index 5f47233..a80c021 100644 --- a/geopandas/array.py +++ b/geopandas/array.py @@ -511,6 +511,11 @@ class GeometryArray(ExtensionArray): def representative_point(self): return GeometryArray(vectorized.representative_point(self.data), crs=self.crs) + def minimum_bounding_circle(self): + return GeometryArray( + vectorized.minimum_bounding_circle(self.data), crs=self.crs + ) + def normalize(self): return GeometryArray(vectorized.normalize(self.data), crs=self.crs) diff --git a/geopandas/base.py b/geopandas/base.py index ada4c79..b4c6a90 100644 --- a/geopandas/base.py +++ b/geopandas/base.py @@ -692,6 +692,39 @@ GeometryCollection """ return _delegate_geo_method("representative_point", self) + def minimum_bounding_circle(self): + """Returns a ``GeoSeries`` of geometries representing the minimum bounding + circle that encloses each geometry. + + Examples + -------- + + >>> from shapely.geometry import Polygon, LineString, Point + >>> s = geopandas.GeoSeries( + ... [ + ... Polygon([(0, 0), (1, 1), (0, 1), (0, 0)]), + ... LineString([(0, 0), (1, 1), (1, 0)]), + ... Point(0, 0), + ... ] + ... ) + >>> s + 0 POLYGON ((0.00000 0.00000, 1.00000 1.00000, 0.... + 1 LINESTRING (0.00000 0.00000, 1.00000 1.00000, ... + 2 POINT (0.00000 0.00000) + dtype: geometry + + >>> s.minimum_bounding_circle() + 0 POLYGON ((1.20711 0.50000, 1.19352 0.36205, 1.... + 1 POLYGON ((1.20711 0.50000, 1.19352 0.36205, 1.... + 2 POINT (0.00000 0.00000) + dtype: geometry + + See also + -------- + GeoSeries.convex_hull : convex hull geometry + """ + return _delegate_geo_method("minimum_bounding_circle", self) + def normalize(self): """Returns a ``GeoSeries`` of normalized geometries to normal form (or canonical form). diff --git a/geopandas/tests/test_geom_methods.py b/geopandas/tests/test_geom_methods.py index 97c1420..eccd5c0 100644 --- a/geopandas/tests/test_geom_methods.py +++ b/geopandas/tests/test_geom_methods.py @@ -928,6 +928,21 @@ class TestGeomMethods: assert isinstance(e, GeoSeries) assert self.g3.crs == e.crs + @pytest.mark.skipif( + not (compat.USE_PYGEOS or compat.USE_SHAPELY_20), + reason="minimum_bounding_circle is only implemented for pygeos, not shapely", + ) + def test_minimum_bounding_circle(self): + mbc = self.g1.minimum_bounding_circle() + centers = GeoSeries([Point(0.5, 0.5)] * 2) + assert np.all(mbc.centroid.geom_equals_exact(centers, 0.001)) + assert_series_equal( + mbc.area, + Series([1.560723, 1.560723]), + ) + assert isinstance(mbc, GeoSeries) + assert self.g1.crs == mbc.crs + def test_total_bounds(self): bbox = self.sol.x, self.sol.y, self.esb.x, self.esb.y assert isinstance(self.landmarks.total_bounds, np.ndarray)