Skip to content

Limit sliver uses #23

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

Merged
merged 4 commits into from
Dec 6, 2019
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
82 changes: 54 additions & 28 deletions geopandas/tests/test_clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@

@pytest.fixture
def point_gdf():
""" Create a point GeoDataFrame. """
"""Create a point GeoDataFrame."""
pts = np.array([[2, 2], [3, 4], [9, 8], [-12, -15]])
gdf = GeoDataFrame(
[Point(xy) for xy in pts], columns=["geometry"], crs={"init": "epsg:4326"}
[Point(xy) for xy in pts], columns=["geometry"], crs={"init": "epsg:4326"},
)
return gdf


@pytest.fixture
def single_rectangle_gdf():
"""Create a single rectangle for clipping. """
"""Create a single rectangle for clipping."""
poly_inters = Polygon([(0, 0), (0, 10), (10, 10), (10, 0), (0, 0)])
gdf = GeoDataFrame([1], geometry=[poly_inters], crs={"init": "epsg:4326"})
gdf["attr2"] = "site-boundary"
Expand All @@ -32,7 +32,6 @@ def single_rectangle_gdf():
@pytest.fixture
def larger_single_rectangle_gdf():
"""Create a slightly larger rectangle for clipping.

The smaller single rectangle is used to test the edge case where slivers
are returned when you clip polygons. This fixture is larger which
eliminates the slivers in the clip return."""
Expand All @@ -44,7 +43,7 @@ def larger_single_rectangle_gdf():

@pytest.fixture
def buffered_locations(point_gdf):
"""Buffer points to create a multi-polygon. """
"""Buffer points to create a multi-polygon."""
buffered_locs = point_gdf
buffered_locs["geometry"] = buffered_locs.buffer(4)
buffered_locs["type"] = "plot"
Expand All @@ -53,7 +52,7 @@ def buffered_locations(point_gdf):

@pytest.fixture
def donut_geometry(buffered_locations, single_rectangle_gdf):
""" Make a geometry with a hole in the middle (a donut). """
"""Make a geometry with a hole in the middle (a donut)."""
donut = gpd.overlay(
buffered_locations, single_rectangle_gdf, how="symmetric_difference"
)
Expand All @@ -62,7 +61,7 @@ def donut_geometry(buffered_locations, single_rectangle_gdf):

@pytest.fixture
def two_line_gdf():
""" Create Line Objects For Testing """
"""Create Line Objects For Testing"""
linea = LineString([(1, 1), (2, 2), (3, 2), (5, 3)])
lineb = LineString([(3, 4), (5, 7), (12, 2), (10, 5), (9, 7.5)])
gdf = GeoDataFrame([1, 2], geometry=[linea, lineb], crs={"init": "epsg:4326"})
Expand All @@ -71,7 +70,7 @@ def two_line_gdf():

@pytest.fixture
def multi_poly_gdf(donut_geometry):
""" Create a multi-polygon GeoDataFrame. """
"""Create a multi-polygon GeoDataFrame."""
multi_poly = donut_geometry.unary_union
out_df = GeoDataFrame(geometry=gpd.GeoSeries(multi_poly), crs={"init": "epsg:4326"})
out_df["attr"] = ["pool"]
Expand All @@ -80,23 +79,21 @@ def multi_poly_gdf(donut_geometry):

@pytest.fixture
def multi_line(two_line_gdf):
""" Create a multi-line GeoDataFrame.

This GDF has one multiline and one regular line.
"""
"""Create a multi-line GeoDataFrame.
This GDF has one multiline and one regular line."""
# Create a single and multi line object
multiline_feat = two_line_gdf.unary_union
linec = LineString([(2, 1), (3, 1), (4, 1), (5, 2)])
out_df = GeoDataFrame(
geometry=gpd.GeoSeries([multiline_feat, linec]), crs={"init": "epsg:4326"}
geometry=gpd.GeoSeries([multiline_feat, linec]), crs={"init": "epsg:4326"},
)
out_df["attr"] = ["road", "stream"]
return out_df


@pytest.fixture
def multi_point(point_gdf):
""" Create a multi-point GeoDataFrame. """
"""Create a multi-point GeoDataFrame."""
multi_point = point_gdf.unary_union
out_df = GeoDataFrame(
geometry=gpd.GeoSeries(
Expand All @@ -110,7 +107,7 @@ def multi_point(point_gdf):

@pytest.fixture
def mixed_gdf():
""" Create a Mixed Polygon and LineString For Testing """
"""Create a Mixed Polygon and LineString For Testing"""
point = Point([(2, 3), (11, 4), (7, 2), (8, 9), (1, 13)])
line = LineString([(1, 1), (2, 2), (3, 2), (5, 3), (12, 1)])
poly = Polygon([(3, 4), (5, 2), (12, 2), (10, 5), (9, 7.5)])
Expand All @@ -120,6 +117,15 @@ def mixed_gdf():
return gdf


@pytest.fixture
def sliver_line():
"""Create a line that will create a point when clipped."""
linea = LineString([(10, 5), (13, 5), (15, 5)])
lineb = LineString([(1, 1), (2, 2), (3, 2), (5, 3), (12, 1)])
gdf = GeoDataFrame([1, 2], geometry=[linea, lineb], crs={"init": "epsg:4326"})
return gdf


def test_not_gdf(single_rectangle_gdf):
"""Non-GeoDataFrame inputs raise attribute errors."""
with pytest.raises(TypeError):
Expand Down Expand Up @@ -169,9 +175,8 @@ def test_clip_poly(buffered_locations, single_rectangle_gdf):

def test_clip_multipoly_keep_slivers(multi_poly_gdf, single_rectangle_gdf):
"""Test a multi poly object where the return includes a sliver.

Also the bounds of the object should == the bounds of the clip object
if they fully overlap (as they do in these fixtures). """
if they fully overlap (as they do in these fixtures)."""
with pytest.warns(UserWarning):
clip = gpd.clip(multi_poly_gdf, single_rectangle_gdf)
warnings.warn("A geometry collection has been returned. ", UserWarning)
Expand All @@ -183,9 +188,8 @@ def test_clip_multipoly_keep_slivers(multi_poly_gdf, single_rectangle_gdf):

def test_clip_multipoly_drop_slivers(multi_poly_gdf, single_rectangle_gdf):
"""Test a multi poly object where the return includes a sliver.

Also the bounds of the object should == the bounds of the clip object
if they fully overlap (as they do in these fixtures). """
if they fully overlap (as they do in these fixtures)."""
clip = gpd.clip(multi_poly_gdf, single_rectangle_gdf, drop_slivers=True)
assert hasattr(clip, "geometry")
assert np.array_equal(clip.total_bounds, single_rectangle_gdf.total_bounds)
Expand All @@ -195,7 +199,6 @@ def test_clip_multipoly_drop_slivers(multi_poly_gdf, single_rectangle_gdf):

def test_clip_multipoly_warning(multi_poly_gdf, single_rectangle_gdf):
"""Test that a user warning is provided when clip outputs a sliver."""

with pytest.warns(UserWarning):
gpd.clip(multi_poly_gdf, single_rectangle_gdf)
warnings.warn("A geometry collection has been returned. ", UserWarning)
Expand All @@ -204,10 +207,8 @@ def test_clip_multipoly_warning(multi_poly_gdf, single_rectangle_gdf):
def test_clip_single_multipoly_no_slivers(
buffered_locations, larger_single_rectangle_gdf
):
"""Test clipping a multi poly with another poly

No sliver shapes are returned in this clip operation. """

"""Test clipping a multi poly with another poly no sliver
shapes are returned in this clip operation."""
multi = buffered_locations.dissolve(by="type").reset_index()
clip = gpd.clip(multi, larger_single_rectangle_gdf)

Expand All @@ -216,18 +217,14 @@ def test_clip_single_multipoly_no_slivers(

def test_clip_multiline(multi_line, single_rectangle_gdf):
"""Test that clipping a multiline feature with a poly returns expected output."""

clip = gpd.clip(multi_line, single_rectangle_gdf)
assert hasattr(clip, "geometry") and clip.geom_type[0] == "MultiLineString"


def test_clip_multipoint(single_rectangle_gdf, multi_point):
"""Clipping a multipoint feature with a polygon works as expected.

should return a geodataframe with a single multi point feature"""

clip = gpd.clip(multi_point, single_rectangle_gdf)

assert hasattr(clip, "geometry") and clip.geom_type[0] == "MultiPoint"
assert hasattr(clip, "attr")
# All points should intersect the clip geom
Expand Down Expand Up @@ -270,3 +267,32 @@ def test_clip_with_polygon(single_rectangle_gdf):
polygon = Polygon([(0, 0), (5, 12), (10, 0), (0, 0)])
clip = gpd.clip(single_rectangle_gdf, polygon)
assert hasattr(clip, "geometry") and clip.geom_type[0] == "Polygon"


def test_clip_with_line_sliver(single_rectangle_gdf, sliver_line):
"""Test clipping a line so that there is a sliver drops the sliver."""
clip = gpd.clip(sliver_line, single_rectangle_gdf, drop_slivers=True)
assert hasattr(clip, "geometry")
assert len(clip.geometry) == 1
# Assert returned data is a not geometry collection
assert (clip.geom_type == "LineString").any()


def test_clip_line_keep_slivers(single_rectangle_gdf, sliver_line):
"""Test the correct warnings are raised if a point sliver is returned"""
with pytest.warns(UserWarning):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docstring please :)

clip = gpd.clip(sliver_line, single_rectangle_gdf)
warnings.warn(
"More geometry types were returned than were in the original ", UserWarning,
)
assert hasattr(clip, "geometry")
# Assert returned data is a geometry collection given sliver geoms
assert "Point" == clip.geom_type[0]
assert "LineString" == clip.geom_type[1]


def test_warning_slivers_mixed(single_rectangle_gdf, mixed_gdf):
"""Test the correct warnings are raised if drop_slivers is called on a mixed GDF"""
with pytest.warns(UserWarning):
gpd.clip(mixed_gdf, single_rectangle_gdf)
warnings.warn("Drop slivers was called on a mixed type GeoDataFrame.")
66 changes: 51 additions & 15 deletions geopandas/tools/clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,25 +180,61 @@ def clip(gdf, clip_obj, drop_slivers=False):
order = pd.Series(range(len(gdf)), index=gdf.index)
concat = pd.concat([point_gdf, line_gdf, poly_gdf])

if (concat.geom_type == "GeometryCollection").any() and drop_slivers:
concat = concat.explode()
if not polys.empty:
concat = concat.loc[concat.geom_type.isin(["Polygon", "MultiPolygon"])]
elif not lines.empty:
concat = concat.loc[
concat.geom_type.isin(["LineString", "MultiLineString", "LinearRing"])
]
elif not points.empty:
concat = concat.loc[concat.geom_type.isin(["Point", "MultiPoint"])]
else:
raise TypeError("`drop_sliver` does not support {}.".format(type))
elif (concat.geom_type == "GeometryCollection").any() and not drop_slivers:
polys = ["Polygon", "MultiPolygon"]
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can add a few tactful comments here.

lines = ["LineString", "MultiLineString", "LinearRing"]
points = ["Point", "MultiPoint"]

# Check that the gdf submitted is not a multi type GeoDataFrame
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Check that the gdf submitted is not a multi type GeoDataFrame
# Check that the gdf does not contain multiple feature types (points, lines and / or polygons)

orig_types_total = sum(
[
gdf.geom_type.isin(polys).any(),
gdf.geom_type.isin(lines).any(),
gdf.geom_type.isin(points).any(),
]
)

# Check how many geometry types are in the clipped GeoDataFrame
clip_types_total = sum(
[
concat.geom_type.isin(polys).any(),
concat.geom_type.isin(lines).any(),
concat.geom_type.isin(points).any(),
]
)

# Check if the clipped geometry is a geometry collection
geometry_collection = (concat.geom_type == "GeometryCollection").any()

# Check there aren't any additional geometries in the clipped GeoDataFrame
more_types = orig_types_total < clip_types_total

if orig_types_total > 1 and drop_slivers:
warnings.warn(
"Drop slivers was called on a mixed type GeoDataFrame. "
"Slivers cannot be dropped on mixed type GeoDataFrames. "
"The data will be return with slivers present."
)
elif drop_slivers and not geometry_collection and not more_types:
warnings.warn("Drop slivers was called when no slivers existed.")
elif drop_slivers and (geometry_collection or more_types):
orig_type = gdf.geom_type.iloc[0]
if geometry_collection:
concat = concat.explode()
if orig_type in polys:
concat = concat.loc[concat.geom_type.isin(polys)]
elif orig_type in lines:
concat = concat.loc[concat.geom_type.isin(lines)]
elif geometry_collection and not drop_slivers:
warnings.warn(
"A geometry collection has been returned. Use .explode() to "
"remove the collection object or drop_slivers=True to remove "
"sliver geometries."
)
elif drop_slivers:
warnings.warn("Drop slivers was called when no slivers existed.")
elif more_types and not drop_slivers:
warnings.warn(
"More geometry types were returned than were in the original "
"GeoDataFrame. This is likely due to a sliver being created. "
"To remove the slivers set drop_slivers=True. "
)
concat["_order"] = order
return concat.sort_values(by="_order").drop(columns="_order")