Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions python/sedona/spark/geopandas/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,19 +712,25 @@ def centroid(self):
return _delegate_to_geometry_column("centroid", self)

def concave_hull(self, ratio=0.0, allow_holes=False):
"""Return the concave hull of each geometry.
"""Return a ``GeoSeries`` of geometries representing the concave hull
of vertices of each geometry.

The concave hull of a geometry is a possibly concave geometry that
encloses the input geometry.
A concave hull is a concave or convex `Polygon` containing all the
vertices in each geometry. For two vertices, the hull collapses to a
`LineString`; for one vertex, it collapses to a `Point`.

The hull is constructed by removing border triangles of the Delaunay
triangulation. The ratio determines the maximum edge-length threshold
by interpolating between the shortest and longest edges in the
triangulation.

Parameters
----------
ratio : float, default 0.0
A value between 0 and 1 controlling the concaveness of the hull.
1 produces the convex hull; 0 produces a hull with maximum
concaveness.
Number in the range [0, 1]. Higher numbers will include fewer vertices
in the hull.
allow_holes : bool, default False
If True, the concave hull may contain holes.
If set to True, the concave hull may have holes.

Returns
-------
Expand All @@ -744,6 +750,16 @@ def concave_hull(self, ratio=0.0, allow_holes=False):
See Also
--------
GeoSeries.convex_hull : convex hull geometry

Notes
-----
The algorithm considers only the vertices of each geometry. As a result,
the hull may not fully enclose the edges of the input geometry. Increasing
``ratio`` should resolve that case.

Sedona and GeoPandas use the JTS and GEOS implementations, respectively.
When candidate border triangles have the same edge length and area, the
implementations may choose different, equally valid concave hulls.
"""
return _delegate_to_geometry_column("concave_hull", self, ratio, allow_holes)

Expand Down
2 changes: 1 addition & 1 deletion python/sedona/spark/geopandas/geoseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -988,7 +988,7 @@ def centroid(self) -> "GeoSeries":
returns_geom=True,
)

def concave_hull(self, ratio=0.0, allow_holes=False):
def concave_hull(self, ratio=0.0, allow_holes=False) -> "GeoSeries":
spark_expr = stf.ST_ConcaveHull(self.spark.column, ratio, allow_holes)
return self._query_geometry_column(
spark_expr,
Expand Down
43 changes: 43 additions & 0 deletions python/tests/geopandas/test_geoseries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1370,6 +1370,49 @@ def test_concave_hull(self):
df_result = s.to_geoframe().concave_hull(ratio=0.5)
self.check_sgpd_equals_gpd(df_result, expected)

# Use an asymmetric point set so that the Delaunay triangulation has no
# ambiguous ties, and verify that allow_holes=True can produce a hole.
points_around_hole = MultiPoint(
[
(11.1, 0.1),
(6.2, 8.1),
(0.1, 10.3),
(-8.2, 7.1),
(-10.1, -0.2),
(-7.2, -8.3),
(0.2, -11.1),
(7.3, -7.2),
(5.1, 0.2),
(-0.1, 5.2),
(-4.2, -0.9),
(0.1, -4.1),
]
)
result_with_holes = GeoSeries([points_around_hole], crs=3857).concave_hull(
ratio=0.9, allow_holes=True
)
expected_with_holes = gpd.GeoSeries(
[
Polygon(
[
(-8.2, 7.1),
(0.1, 10.3),
(6.2, 8.1),
(5.1, 0.2),
(11.1, 0.1),
(7.3, -7.2),
(0.2, -11.1),
(-7.2, -8.3),
(-10.1, -0.2),
],
[[(-4.2, -0.9), (0.1, -4.1), (-0.1, 5.2)]],
)
],
crs=3857,
)
self.check_sgpd_equals_gpd(result_with_holes, expected_with_holes)
assert result_with_holes.crs == expected_with_holes.crs

def test_convex_hull(self):
s = GeoSeries(
[
Expand Down
47 changes: 43 additions & 4 deletions python/tests/geopandas/test_match_geopandas_series.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,11 +758,50 @@ def test_centroid(self):
gpd_result = gpd.GeoSeries(geom).centroid
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)

@pytest.mark.skipif(
parse_version(gpd.__version__) < parse_version("0.14.0"),
reason="geopandas concave_hull requires version 0.14.0 or higher",
)
def test_concave_hull(self):
for geom in self.geoms:
sgpd_result = GeoSeries(geom).concave_hull(ratio=0.5)
gpd_result = gpd.GeoSeries(geom).concave_hull(ratio=0.5)
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
# Cover the valid ratio range without tripling this already broad set
# of geometry-type fixtures.
ratio_groups = (
(0.0, self.geoms[:3]),
(0.5, self.geoms[3:6]),
(1.0, self.geoms[6:]),
)
for ratio, geometry_groups in ratio_groups:
for geometries in geometry_groups:
sgpd_result = GeoSeries(geometries).concave_hull(ratio=ratio)
gpd_result = gpd.GeoSeries(geometries).concave_hull(ratio=ratio)
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)

# Avoid symmetric fixtures here: equal-priority Delaunay triangles can
# be resolved differently by JTS and GEOS while producing valid hulls.
points_around_hole = MultiPoint(
[
(11.1, 0.1),
(6.2, 8.1),
(0.1, 10.3),
(-8.2, 7.1),
(-10.1, -0.2),
(-7.2, -8.3),
(0.2, -11.1),
(7.3, -7.2),
(5.1, 0.2),
(-0.1, 5.2),
(-4.2, -0.9),
(0.1, -4.1),
]
)
sgpd_result = GeoSeries([points_around_hole]).concave_hull(
ratio=0.9, allow_holes=True
)
gpd_result = gpd.GeoSeries([points_around_hole]).concave_hull(
ratio=0.9, allow_holes=True
)
assert len(gpd_result.iloc[0].interiors) == 1
self.check_sgpd_equals_gpd(sgpd_result, gpd_result)
Comment thread
jiayuasu marked this conversation as resolved.
Resolved

def test_convex_hull(self):
for geom in self.geoms:
Expand Down
Loading