mirror of
https://github.com/wassname/geopandas.git
synced 2026-09-09 11:22:50 +08:00
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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user