ENH: Add fast rectangle clipping option to tools.clip (#2366)

* Add rectangle clipping to `clip.py`

* Add minor changes requested in PR

* Allow for list-like masks

* Update docstrings for GeoSeries.clip() and GeoDataFrame.clip()
This commit is contained in:
bstadlbauer
2022-04-29 08:20:30 +10:00
committed by GitHub
parent be647ab7e5
commit 3abc6a79ca
4 changed files with 313 additions and 218 deletions
+9 -5
View File
@@ -2106,17 +2106,21 @@ countries_w_city_data[countries_w_city_data["name_left"] == "Italy"]
"""Clip points, lines, or polygon geometries to the mask extent.
Both layers must be in the same Coordinate Reference System (CRS).
The GeoDataFrame will be clipped to the full extent of the `mask` object.
The GeoDataFrame will be clipped to the full extent of the ``mask`` object.
If there are multiple polygons in mask, data from the GeoDataFrame will be
clipped to the total boundary of all polygons in mask.
Parameters
----------
mask : GeoDataFrame, GeoSeries, (Multi)Polygon
Polygon vector layer used to clip `gdf`.
mask : GeoDataFrame, GeoSeries, (Multi)Polygon, list-like
Polygon vector layer used to clip the GeoDataFrame.
The mask's geometry is dissolved into one geometric feature
and intersected with `gdf`.
and intersected with GeoDataFrame.
If the mask is list-like with four elements ``(minx, miny, maxx, maxy)``,
``clip`` will use a faster rectangle clipping
(:meth:`~GeoSeries.clip_by_rect`), possibly leading to slightly different
results.
keep_geom_type : boolean, default False
If True, return only geometries of original type in case of intersection
resulting in multiple geometry types or GeometryCollections.
@@ -2125,7 +2129,7 @@ countries_w_city_data[countries_w_city_data["name_left"] == "Italy"]
Returns
-------
GeoDataFrame
Vector data (points, lines, polygons) from `gdf` clipped to
Vector data (points, lines, polygons) from the GeoDataFrame clipped to
polygon boundary from mask.
See also
+6 -2
View File
@@ -1288,10 +1288,14 @@ e": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": [3
Parameters
----------
mask : GeoDataFrame, GeoSeries, (Multi)Polygon
mask : GeoDataFrame, GeoSeries, (Multi)Polygon, list-like
Polygon vector layer used to clip `gdf`.
The mask's geometry is dissolved into one geometric feature
and intersected with `gdf`.
and intersected with GeoSeries.
If the mask is list-like with four elements ``(minx, miny, maxx, maxy)``,
``clip`` will use a faster rectangle clipping
(:meth:`~GeoSeries.clip_by_rect`), possibly leading to slightly different
results.
keep_geom_type : boolean, default False
If True, return only geometries of original type in case of intersection
resulting in multiple geometry types or GeometryCollections.
+69 -24
View File
@@ -7,16 +7,23 @@ A module to clip vector data using GeoPandas.
"""
import warnings
from shapely.geometry import Polygon, MultiPolygon
import pandas.api.types
from shapely.geometry import Polygon, MultiPolygon, box
from geopandas import GeoDataFrame, GeoSeries
from geopandas.array import _check_crs, _crs_mismatch_warn
def _clip_gdf_with_polygon(gdf, poly):
"""Clip geometry to the polygon extent.
def _mask_is_list_like_rectangle(mask):
return pandas.api.types.is_list_like(mask) and not isinstance(
mask, (GeoDataFrame, GeoSeries, Polygon, MultiPolygon)
)
Clip an input GeoDataFrame to the polygon extent of the poly
def _clip_gdf_with_mask(gdf, mask):
"""Clip geometry to the polygon/rectangle extent.
Clip an input GeoDataFrame to the polygon extent of the polygon
parameter.
Parameters
@@ -24,16 +31,22 @@ def _clip_gdf_with_polygon(gdf, poly):
gdf : GeoDataFrame, GeoSeries
Dataframe to clip.
poly : (Multi)Polygon
Reference polygon for clipping.
mask : (Multi)Polygon, list-like
Reference polygon/rectangle for clipping.
Returns
-------
GeoDataFrame
The returned GeoDataFrame is a clipped subset of gdf
that intersects with poly.
that intersects with polygon/rectangle.
"""
gdf_sub = gdf.iloc[gdf.sindex.query(poly, predicate="intersects")]
clipping_by_rectangle = _mask_is_list_like_rectangle(mask)
if clipping_by_rectangle:
intersection_polygon = box(*mask)
else:
intersection_polygon = mask
gdf_sub = gdf.iloc[gdf.sindex.query(intersection_polygon, predicate="intersects")]
# For performance reasons points don't need to be intersected with poly
non_point_mask = gdf_sub.geom_type != "Point"
@@ -45,14 +58,25 @@ def _clip_gdf_with_polygon(gdf, poly):
# Clip the data with the polygon
if isinstance(gdf_sub, GeoDataFrame):
clipped = gdf_sub.copy()
clipped.loc[
non_point_mask, clipped._geometry_column_name
] = gdf_sub.geometry.values[non_point_mask].intersection(poly)
if clipping_by_rectangle:
clipped.loc[
non_point_mask, clipped._geometry_column_name
] = gdf_sub.geometry.values[non_point_mask].clip_by_rect(*mask)
else:
clipped.loc[
non_point_mask, clipped._geometry_column_name
] = gdf_sub.geometry.values[non_point_mask].intersection(mask)
else:
# GeoSeries
clipped = gdf_sub.copy()
clipped[non_point_mask] = gdf_sub.values[non_point_mask].intersection(poly)
if clipping_by_rectangle:
clipped[non_point_mask] = gdf_sub.values[non_point_mask].clip_by_rect(*mask)
else:
clipped[non_point_mask] = gdf_sub.values[non_point_mask].intersection(mask)
if clipping_by_rectangle:
# clip_by_rect might return empty geometry collections in edge cases
clipped = clipped[~clipped.is_empty]
return clipped
@@ -60,19 +84,29 @@ def clip(gdf, mask, keep_geom_type=False):
"""Clip points, lines, or polygon geometries to the mask extent.
Both layers must be in the same Coordinate Reference System (CRS).
The `gdf` will be clipped to the full extent of the clip object.
The ``gdf`` will be clipped to the full extent of the clip object.
If there are multiple polygons in mask, data from `gdf` will be
If there are multiple polygons in mask, data from ``gdf`` will be
clipped to the total boundary of all polygons in mask.
If the ``mask`` is list-like with four elements ``(minx, miny, maxx, maxy)``, a
faster rectangle clipping algorithm will be used. Note that this can lead to
slightly different results in edge cases, e.g. if a line would be reduced to a
point, this point might not be returned.
The geometry is clipped in a fast but possibly dirty way. The output is not
guaranteed to be valid. No exceptions will be raised for topological errors.
Parameters
----------
gdf : GeoDataFrame or GeoSeries
Vector layer (point, line, polygon) to be clipped to mask.
mask : GeoDataFrame, GeoSeries, (Multi)Polygon
Polygon vector layer used to clip `gdf`.
mask : GeoDataFrame, GeoSeries, (Multi)Polygon, list-like
Polygon vector layer used to clip ``gdf``.
The mask's geometry is dissolved into one geometric feature
and intersected with `gdf`.
and intersected with ``gdf``.
If the mask is list-like with four elements ``(minx, miny, maxx, maxy)``,
``clip`` will use a faster rectangle clipping (:meth:`~GeoSeries.clip_by_rect`),
possibly leading to slightly different results.
keep_geom_type : boolean, default False
If True, return only geometries of original type in case of intersection
resulting in multiple geometry types or GeometryCollections.
@@ -81,7 +115,7 @@ def clip(gdf, mask, keep_geom_type=False):
Returns
-------
GeoDataFrame or GeoSeries
Vector data (points, lines, polygons) from `gdf` clipped to
Vector data (points, lines, polygons) from ``gdf`` clipped to
polygon boundary from mask.
See also
@@ -110,10 +144,19 @@ def clip(gdf, mask, keep_geom_type=False):
"'gdf' should be GeoDataFrame or GeoSeries, got {}".format(type(gdf))
)
if not isinstance(mask, (GeoDataFrame, GeoSeries, Polygon, MultiPolygon)):
mask_is_list_like = _mask_is_list_like_rectangle(mask)
if (
not isinstance(mask, (GeoDataFrame, GeoSeries, Polygon, MultiPolygon))
and not mask_is_list_like
):
raise TypeError(
"'mask' should be GeoDataFrame, GeoSeries or"
"(Multi)Polygon, got {}".format(type(mask))
"'mask' should be GeoDataFrame, GeoSeries,"
f"(Multi)Polygon or list-like, got {type(mask)}"
)
if mask_is_list_like and len(mask) != 4:
raise TypeError(
"If 'mask' is list-like, it must have four values (minx, miny, maxx, maxy)"
)
if isinstance(mask, (GeoDataFrame, GeoSeries)):
@@ -122,6 +165,8 @@ def clip(gdf, mask, keep_geom_type=False):
if isinstance(mask, (GeoDataFrame, GeoSeries)):
box_mask = mask.total_bounds
elif mask_is_list_like:
box_mask = mask
else:
box_mask = mask.bounds
box_gdf = gdf.total_bounds
@@ -132,11 +177,11 @@ def clip(gdf, mask, keep_geom_type=False):
return gdf.iloc[:0]
if isinstance(mask, (GeoDataFrame, GeoSeries)):
poly = mask.geometry.unary_union
combined_mask = mask.geometry.unary_union
else:
poly = mask
combined_mask = mask
clipped = _clip_gdf_with_polygon(gdf, poly)
clipped = _clip_gdf_with_mask(gdf, combined_mask)
if keep_geom_type:
geomcoll_concat = (clipped.geom_type == "GeometryCollection").any()
+229 -187
View File
@@ -14,6 +14,7 @@ from shapely.geometry import (
LinearRing,
GeometryCollection,
MultiPoint,
box,
)
import geopandas
@@ -22,9 +23,20 @@ from geopandas import GeoDataFrame, GeoSeries, clip
from geopandas.testing import assert_geodataframe_equal, assert_geoseries_equal
import pytest
from geopandas.tools.clip import _mask_is_list_like_rectangle
pytestmark = pytest.mark.skip_no_sindex
pandas_133 = Version(pd.__version__) == Version("1.3.3")
mask_variants_single_rectangle = [
"single_rectangle_gdf",
"single_rectangle_gdf_list_bounds",
"single_rectangle_gdf_tuple_bounds",
"single_rectangle_gdf_array_bounds",
]
mask_variants_large_rectangle = [
"larger_single_rectangle_gdf",
"larger_single_rectangle_gdf_bounds",
]
@pytest.fixture
@@ -62,6 +74,24 @@ def single_rectangle_gdf():
return gdf
@pytest.fixture
def single_rectangle_gdf_tuple_bounds(single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return tuple(single_rectangle_gdf.total_bounds)
@pytest.fixture
def single_rectangle_gdf_list_bounds(single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return list(single_rectangle_gdf.total_bounds)
@pytest.fixture
def single_rectangle_gdf_array_bounds(single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return single_rectangle_gdf.total_bounds
@pytest.fixture
def larger_single_rectangle_gdf():
"""Create a slightly larger rectangle for clipping.
@@ -75,6 +105,12 @@ def larger_single_rectangle_gdf():
return gdf
@pytest.fixture
def larger_single_rectangle_gdf_bounds(larger_single_rectangle_gdf):
"""Bounds of the created single rectangle"""
return tuple(larger_single_rectangle_gdf.total_bounds)
@pytest.fixture
def buffered_locations(point_gdf):
"""Buffer points to create a multi-polygon."""
@@ -174,19 +210,11 @@ def test_not_gdf(single_rectangle_gdf):
with pytest.raises(TypeError):
clip((2, 3), single_rectangle_gdf)
with pytest.raises(TypeError):
clip(single_rectangle_gdf, (2, 3))
def test_returns_gdf(point_gdf, single_rectangle_gdf):
"""Test that function returns a GeoDataFrame (or GDF-like) object."""
out = clip(point_gdf, single_rectangle_gdf)
assert isinstance(out, GeoDataFrame)
def test_returns_series(point_gdf, single_rectangle_gdf):
"""Test that function returns a GeoSeries if GeoSeries is passed."""
out = clip(point_gdf.geometry, single_rectangle_gdf)
assert isinstance(out, GeoSeries)
clip(single_rectangle_gdf, "foobar")
with pytest.raises(TypeError):
clip(single_rectangle_gdf, (1, 2, 3))
with pytest.raises(TypeError):
clip(single_rectangle_gdf, (1, 2, 3, 4, 5))
def test_non_overlapping_geoms():
@@ -203,50 +231,175 @@ def test_non_overlapping_geoms():
assert_geoseries_equal(out2, GeoSeries(crs=unit_gdf.crs))
def test_clip_points(point_gdf, single_rectangle_gdf):
"""Test clipping a points GDF with a generic polygon geometry."""
clip_pts = clip(point_gdf, single_rectangle_gdf)
pts = np.array([[2, 2], [3, 4], [9, 8]])
exp = GeoDataFrame([Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857")
assert_geodataframe_equal(clip_pts, exp)
@pytest.mark.parametrize("mask_fixture_name", mask_variants_single_rectangle)
class TestClipWithSingleRectangleGdf:
@pytest.fixture
def mask(self, mask_fixture_name, request):
return request.getfixturevalue(mask_fixture_name)
def test_returns_gdf(self, point_gdf, mask):
"""Test that function returns a GeoDataFrame (or GDF-like) object."""
out = clip(point_gdf, mask)
assert isinstance(out, GeoDataFrame)
def test_returns_series(self, point_gdf, mask):
"""Test that function returns a GeoSeries if GeoSeries is passed."""
out = clip(point_gdf.geometry, mask)
assert isinstance(out, GeoSeries)
def test_clip_points(self, point_gdf, mask):
"""Test clipping a points GDF with a generic polygon geometry."""
clip_pts = clip(point_gdf, mask)
pts = np.array([[2, 2], [3, 4], [9, 8]])
exp = GeoDataFrame(
[Point(xy) for xy in pts], columns=["geometry"], crs="EPSG:3857"
)
assert_geodataframe_equal(clip_pts, exp)
def test_clip_points_geom_col_rename(self, point_gdf, mask):
"""Test clipping a points GDF with a generic polygon geometry."""
point_gdf_geom_col_rename = point_gdf.rename_geometry("geometry2")
clip_pts = clip(point_gdf_geom_col_rename, mask)
pts = np.array([[2, 2], [3, 4], [9, 8]])
exp = GeoDataFrame(
[Point(xy) for xy in pts],
columns=["geometry2"],
crs="EPSG:3857",
geometry="geometry2",
)
assert_geodataframe_equal(clip_pts, exp)
def test_clip_poly(self, buffered_locations, mask):
"""Test clipping a polygon GDF with a generic polygon geometry."""
clipped_poly = clip(buffered_locations, mask)
assert len(clipped_poly.geometry) == 3
assert all(clipped_poly.geom_type == "Polygon")
def test_clip_poly_geom_col_rename(self, buffered_locations, mask):
"""Test clipping a polygon GDF with a generic polygon geometry."""
poly_gdf_geom_col_rename = buffered_locations.rename_geometry("geometry2")
clipped_poly = clip(poly_gdf_geom_col_rename, mask)
assert len(clipped_poly.geometry) == 3
assert "geometry" not in clipped_poly.keys()
assert "geometry2" in clipped_poly.keys()
def test_clip_poly_series(self, buffered_locations, mask):
"""Test clipping a polygon GDF with a generic polygon geometry."""
clipped_poly = clip(buffered_locations.geometry, mask)
assert len(clipped_poly) == 3
assert all(clipped_poly.geom_type == "Polygon")
@pytest.mark.xfail(pandas_133, reason="Regression in pandas 1.3.3 (GH #2101)")
def test_clip_multipoly_keep_geom_type(self, multi_poly_gdf, mask):
"""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)."""
clipped = clip(multi_poly_gdf, mask, keep_geom_type=True)
expected_bounds = (
mask if _mask_is_list_like_rectangle(mask) else mask.total_bounds
)
assert np.array_equal(clipped.total_bounds, expected_bounds)
# Assert returned data is a not geometry collection
assert (clipped.geom_type.isin(["Polygon", "MultiPolygon"])).all()
def test_clip_multiline(self, multi_line, mask):
"""Test that clipping a multiline feature with a poly returns expected
output."""
clipped = clip(multi_line, mask)
assert clipped.geom_type[0] == "MultiLineString"
def test_clip_multipoint(self, multi_point, mask):
"""Clipping a multipoint feature with a polygon works as expected.
should return a geodataframe with a single multi point feature"""
clipped = clip(multi_point, mask)
assert clipped.geom_type[0] == "MultiPoint"
assert hasattr(clipped, "attr")
# All points should intersect the clip geom
assert len(clipped) == 2
clipped_mutltipoint = MultiPoint(
[
Point(2, 2),
Point(3, 4),
Point(9, 8),
]
)
assert clipped.iloc[0].geometry.wkt == clipped_mutltipoint.wkt
shape_for_points = (
box(*mask) if _mask_is_list_like_rectangle(mask) else mask.unary_union
)
assert all(clipped.intersects(shape_for_points))
def test_clip_lines(self, two_line_gdf, mask):
"""Test what happens when you give the clip_extent a line GDF."""
clip_line = clip(two_line_gdf, mask)
assert len(clip_line.geometry) == 2
def test_mixed_geom(self, mixed_gdf, mask):
"""Test clipping a mixed GeoDataFrame"""
clipped = clip(mixed_gdf, mask)
assert (
clipped.geom_type[0] == "Point"
and clipped.geom_type[1] == "Polygon"
and clipped.geom_type[2] == "LineString"
)
def test_mixed_series(self, mixed_gdf, mask):
"""Test clipping a mixed GeoSeries"""
clipped = clip(mixed_gdf.geometry, mask)
assert (
clipped.geom_type[0] == "Point"
and clipped.geom_type[1] == "Polygon"
and clipped.geom_type[2] == "LineString"
)
def test_clip_warning_no_extra_geoms(self, buffered_locations, mask):
"""Test a user warning is provided if no new geometry types are found."""
with pytest.warns(UserWarning):
clip(buffered_locations, mask, True)
warnings.warn(
"keep_geom_type was called when no extra geometry types existed.",
UserWarning,
)
def test_clip_with_line_extra_geom(self, sliver_line, mask):
"""When the output of a clipped line returns a geom collection,
and keep_geom_type is True, no geometry collections should be returned."""
clipped = clip(sliver_line, mask, keep_geom_type=True)
assert len(clipped.geometry) == 1
# Assert returned data is a not geometry collection
assert not (clipped.geom_type == "GeometryCollection").any()
def test_clip_no_box_overlap(self, pointsoutside_nooverlap_gdf, mask):
"""Test clip when intersection is empty and boxes do not overlap."""
clipped = clip(pointsoutside_nooverlap_gdf, mask)
assert len(clipped) == 0
def test_clip_box_overlap(self, pointsoutside_overlap_gdf, mask):
"""Test clip when intersection is empty and boxes do overlap."""
clipped = clip(pointsoutside_overlap_gdf, mask)
assert len(clipped) == 0
def test_warning_extra_geoms_mixed(self, mixed_gdf, mask):
"""Test the correct warnings are raised if keep_geom_type is
called on a mixed GDF"""
with pytest.warns(UserWarning):
clip(mixed_gdf, mask, keep_geom_type=True)
def test_warning_geomcoll(self, geomcol_gdf, mask):
"""Test the correct warnings are raised if keep_geom_type is
called on a GDF with GeometryCollection"""
with pytest.warns(UserWarning):
clip(geomcol_gdf, mask, keep_geom_type=True)
def test_clip_points_geom_col_rename(point_gdf, single_rectangle_gdf):
"""Test clipping a points GDF with a generic polygon geometry."""
point_gdf_geom_col_rename = point_gdf.rename_geometry("geometry2")
clip_pts = clip(point_gdf_geom_col_rename, single_rectangle_gdf)
pts = np.array([[2, 2], [3, 4], [9, 8]])
exp = GeoDataFrame(
[Point(xy) for xy in pts],
columns=["geometry2"],
crs="EPSG:3857",
geometry="geometry2",
)
assert_geodataframe_equal(clip_pts, exp)
def test_clip_poly(buffered_locations, single_rectangle_gdf):
"""Test clipping a polygon GDF with a generic polygon geometry."""
clipped_poly = clip(buffered_locations, single_rectangle_gdf)
assert len(clipped_poly.geometry) == 3
assert all(clipped_poly.geom_type == "Polygon")
def test_clip_poly_geom_col_rename(buffered_locations, single_rectangle_gdf):
"""Test clipping a polygon GDF with a generic polygon geometry."""
poly_gdf_geom_col_rename = buffered_locations.rename_geometry("geometry2")
clipped_poly = clip(poly_gdf_geom_col_rename, single_rectangle_gdf)
assert len(clipped_poly.geometry) == 3
assert "geometry" not in clipped_poly.keys()
assert "geometry2" in clipped_poly.keys()
def test_clip_poly_series(buffered_locations, single_rectangle_gdf):
"""Test clipping a polygon GDF with a generic polygon geometry."""
clipped_poly = clip(buffered_locations.geometry, single_rectangle_gdf)
assert len(clipped_poly) == 3
assert all(clipped_poly.geom_type == "Polygon")
def test_clip_line_keep_slivers(sliver_line, single_rectangle_gdf):
"""Test the correct output if a point is returned
from a line only geometry type."""
clipped = clip(sliver_line, single_rectangle_gdf)
# Assert returned data is a geometry collection given sliver geoms
assert "Point" == clipped.geom_type[0]
assert "LineString" == clipped.geom_type[1]
@pytest.mark.xfail(pandas_133, reason="Regression in pandas 1.3.3 (GH #2101)")
@@ -260,93 +413,9 @@ def test_clip_multipoly_keep_slivers(multi_poly_gdf, single_rectangle_gdf):
assert "GeometryCollection" in clipped.geom_type[0]
@pytest.mark.xfail(pandas_133, reason="Regression in pandas 1.3.3 (GH #2101)")
def test_clip_multipoly_keep_geom_type(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)."""
clipped = clip(multi_poly_gdf, single_rectangle_gdf, keep_geom_type=True)
assert np.array_equal(clipped.total_bounds, single_rectangle_gdf.total_bounds)
# Assert returned data is a not geometry collection
assert (clipped.geom_type == "Polygon").any()
def test_clip_single_multipoly_no_extra_geoms(
buffered_locations, larger_single_rectangle_gdf
):
"""When clipping a multi-polygon feature, no additional geom types
should be returned."""
multi = buffered_locations.dissolve(by="type").reset_index()
clipped = clip(multi, larger_single_rectangle_gdf)
assert clipped.geom_type[0] == "Polygon"
def test_clip_multiline(multi_line, single_rectangle_gdf):
"""Test that clipping a multiline feature with a poly returns expected output."""
clipped = clip(multi_line, single_rectangle_gdf)
assert clipped.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"""
clipped = clip(multi_point, single_rectangle_gdf)
assert clipped.geom_type[0] == "MultiPoint"
assert hasattr(clipped, "attr")
# All points should intersect the clip geom
assert len(clipped) == 2
clipped_mutltipoint = MultiPoint(
[
Point(2, 2),
Point(3, 4),
Point(9, 8),
]
)
assert clipped.iloc[0].geometry.wkt == clipped_mutltipoint.wkt
assert all(clipped.intersects(single_rectangle_gdf.unary_union))
def test_clip_lines(two_line_gdf, single_rectangle_gdf):
"""Test what happens when you give the clip_extent a line GDF."""
clip_line = clip(two_line_gdf, single_rectangle_gdf)
assert len(clip_line.geometry) == 2
def test_clip_with_multipolygon(buffered_locations, single_rectangle_gdf):
"""Test clipping a polygon with a multipolygon."""
multi = buffered_locations.dissolve(by="type").reset_index()
clipped = clip(single_rectangle_gdf, multi)
assert clipped.geom_type[0] == "Polygon"
def test_mixed_geom(mixed_gdf, single_rectangle_gdf):
"""Test clipping a mixed GeoDataFrame"""
clipped = clip(mixed_gdf, single_rectangle_gdf)
assert (
clipped.geom_type[0] == "Point"
and clipped.geom_type[1] == "Polygon"
and clipped.geom_type[2] == "LineString"
)
def test_mixed_series(mixed_gdf, single_rectangle_gdf):
"""Test clipping a mixed GeoSeries"""
clipped = clip(mixed_gdf.geometry, single_rectangle_gdf)
assert (
clipped.geom_type[0] == "Point"
and clipped.geom_type[1] == "Polygon"
and clipped.geom_type[2] == "LineString"
)
def test_clip_warning_no_extra_geoms(buffered_locations, single_rectangle_gdf):
"""Test a user warning is provided if no new geometry types are found."""
with pytest.warns(UserWarning):
clip(buffered_locations, single_rectangle_gdf, True)
warnings.warn(
"keep_geom_type was called when no extra geometry types existed.",
UserWarning,
)
def test_warning_crs_mismatch(point_gdf, single_rectangle_gdf):
with pytest.warns(UserWarning, match="CRS mismatch between the CRS"):
clip(point_gdf, single_rectangle_gdf.to_crs(4326))
def test_clip_with_polygon(single_rectangle_gdf):
@@ -361,50 +430,23 @@ def test_clip_with_polygon(single_rectangle_gdf):
assert_geodataframe_equal(clipped, exp)
def test_clip_with_line_extra_geom(single_rectangle_gdf, sliver_line):
"""When the output of a clipped line returns a geom collection,
and keep_geom_type is True, no geometry collections should be returned."""
clipped = clip(sliver_line, single_rectangle_gdf, keep_geom_type=True)
assert len(clipped.geometry) == 1
# Assert returned data is a not geometry collection
assert not (clipped.geom_type == "GeometryCollection").any()
def test_clip_with_multipolygon(buffered_locations, single_rectangle_gdf):
"""Test clipping a polygon with a multipolygon."""
multi = buffered_locations.dissolve(by="type").reset_index()
clipped = clip(single_rectangle_gdf, multi)
assert clipped.geom_type[0] == "Polygon"
def test_clip_line_keep_slivers(single_rectangle_gdf, sliver_line):
"""Test the correct output if a point is returned
from a line only geometry type."""
clipped = clip(sliver_line, single_rectangle_gdf)
# Assert returned data is a geometry collection given sliver geoms
assert "Point" == clipped.geom_type[0]
assert "LineString" == clipped.geom_type[1]
def test_clip_no_box_overlap(pointsoutside_nooverlap_gdf, single_rectangle_gdf):
"""Test clip when intersection is empty and boxes do not overlap."""
clipped = clip(pointsoutside_nooverlap_gdf, single_rectangle_gdf)
assert len(clipped) == 0
def test_clip_box_overlap(pointsoutside_overlap_gdf, single_rectangle_gdf):
"""Test clip when intersection is empty and boxes do overlap."""
clipped = clip(pointsoutside_overlap_gdf, single_rectangle_gdf)
assert len(clipped) == 0
def test_warning_extra_geoms_mixed(single_rectangle_gdf, mixed_gdf):
"""Test the correct warnings are raised if keep_geom_type is
called on a mixed GDF"""
with pytest.warns(UserWarning):
clip(mixed_gdf, single_rectangle_gdf, keep_geom_type=True)
def test_warning_geomcoll(single_rectangle_gdf, geomcol_gdf):
"""Test the correct warnings are raised if keep_geom_type is
called on a GDF with GeometryCollection"""
with pytest.warns(UserWarning):
clip(geomcol_gdf, single_rectangle_gdf, keep_geom_type=True)
def test_warning_crs_mismatch(point_gdf, single_rectangle_gdf):
with pytest.warns(UserWarning, match="CRS mismatch between the CRS"):
clip(point_gdf, single_rectangle_gdf.to_crs(4326))
@pytest.mark.parametrize(
"mask_fixture_name",
mask_variants_large_rectangle,
)
def test_clip_single_multipoly_no_extra_geoms(
buffered_locations, mask_fixture_name, request
):
"""When clipping a multi-polygon feature, no additional geom types
should be returned."""
masks = request.getfixturevalue(mask_fixture_name)
multi = buffered_locations.dissolve(by="type").reset_index()
clipped = clip(multi, masks)
assert clipped.geom_type[0] == "Polygon"