that's too much!
This commit is contained in:
@@ -0,0 +1,958 @@
|
||||
from math import sqrt
|
||||
|
||||
from shapely.geometry import (
|
||||
Point,
|
||||
Polygon,
|
||||
MultiPolygon,
|
||||
box,
|
||||
GeometryCollection,
|
||||
LineString,
|
||||
)
|
||||
from numpy.testing import assert_array_equal
|
||||
|
||||
import geopandas
|
||||
from geopandas import _compat as compat
|
||||
from geopandas import GeoDataFrame, GeoSeries, read_file, datasets
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
if compat.USE_SHAPELY_20:
|
||||
import shapely as mod
|
||||
elif compat.USE_PYGEOS:
|
||||
import pygeos as mod
|
||||
|
||||
|
||||
@pytest.mark.skip_no_sindex
|
||||
class TestSeriesSindex:
|
||||
def test_has_sindex(self):
|
||||
"""Test the has_sindex method."""
|
||||
t1 = Polygon([(0, 0), (1, 0), (1, 1)])
|
||||
t2 = Polygon([(0, 0), (1, 1), (0, 1)])
|
||||
|
||||
d = GeoDataFrame({"geom": [t1, t2]}, geometry="geom")
|
||||
assert not d.has_sindex
|
||||
d.sindex
|
||||
assert d.has_sindex
|
||||
d.geometry.values._sindex = None
|
||||
assert not d.has_sindex
|
||||
d.sindex
|
||||
assert d.has_sindex
|
||||
|
||||
s = GeoSeries([t1, t2])
|
||||
assert not s.has_sindex
|
||||
s.sindex
|
||||
assert s.has_sindex
|
||||
s.values._sindex = None
|
||||
assert not s.has_sindex
|
||||
s.sindex
|
||||
assert s.has_sindex
|
||||
|
||||
def test_empty_geoseries(self):
|
||||
"""Tests creating a spatial index from an empty GeoSeries."""
|
||||
s = GeoSeries(dtype=object)
|
||||
assert not s.sindex
|
||||
assert len(s.sindex) == 0
|
||||
|
||||
def test_point(self):
|
||||
s = GeoSeries([Point(0, 0)])
|
||||
assert s.sindex.size == 1
|
||||
hits = s.sindex.intersection((-1, -1, 1, 1))
|
||||
assert len(list(hits)) == 1
|
||||
hits = s.sindex.intersection((-2, -2, -1, -1))
|
||||
assert len(list(hits)) == 0
|
||||
|
||||
def test_empty_point(self):
|
||||
"""Tests that a single empty Point results in an empty tree."""
|
||||
s = GeoSeries([Point()])
|
||||
assert not s.sindex
|
||||
assert len(s.sindex) == 0
|
||||
|
||||
def test_polygons(self):
|
||||
t1 = Polygon([(0, 0), (1, 0), (1, 1)])
|
||||
t2 = Polygon([(0, 0), (1, 1), (0, 1)])
|
||||
sq = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
|
||||
s = GeoSeries([t1, t2, sq])
|
||||
assert s.sindex.size == 3
|
||||
|
||||
@pytest.mark.filterwarnings("ignore:The series.append method is deprecated")
|
||||
@pytest.mark.skipif(compat.PANDAS_GE_20, reason="append removed in pandas 2.0")
|
||||
def test_polygons_append(self):
|
||||
t1 = Polygon([(0, 0), (1, 0), (1, 1)])
|
||||
t2 = Polygon([(0, 0), (1, 1), (0, 1)])
|
||||
sq = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
|
||||
s = GeoSeries([t1, t2, sq])
|
||||
t = GeoSeries([t1, t2, sq], [3, 4, 5])
|
||||
s = s.append(t)
|
||||
assert len(s) == 6
|
||||
assert s.sindex.size == 6
|
||||
|
||||
def test_lazy_build(self):
|
||||
s = GeoSeries([Point(0, 0)])
|
||||
assert s.values._sindex is None
|
||||
assert s.sindex.size == 1
|
||||
assert s.values._sindex is not None
|
||||
|
||||
def test_rebuild_on_item_change(self):
|
||||
s = GeoSeries([Point(0, 0)])
|
||||
original_index = s.sindex
|
||||
s.iloc[0] = Point(0, 0)
|
||||
assert s.sindex is not original_index
|
||||
|
||||
def test_rebuild_on_slice(self):
|
||||
s = GeoSeries([Point(0, 0), Point(0, 0)])
|
||||
original_index = s.sindex
|
||||
# Select a couple of rows
|
||||
sliced = s.iloc[:1]
|
||||
assert sliced.sindex is not original_index
|
||||
# Select all rows
|
||||
sliced = s.iloc[:]
|
||||
assert sliced.sindex is original_index
|
||||
# Select all rows and flip
|
||||
sliced = s.iloc[::-1]
|
||||
assert sliced.sindex is not original_index
|
||||
|
||||
|
||||
@pytest.mark.skip_no_sindex
|
||||
class TestFrameSindex:
|
||||
def setup_method(self):
|
||||
data = {
|
||||
"A": range(5),
|
||||
"B": range(-5, 0),
|
||||
"geom": [Point(x, y) for x, y in zip(range(5), range(5))],
|
||||
}
|
||||
self.df = GeoDataFrame(data, geometry="geom")
|
||||
|
||||
def test_sindex(self):
|
||||
self.df.crs = "epsg:4326"
|
||||
assert self.df.sindex.size == 5
|
||||
hits = list(self.df.sindex.intersection((2.5, 2.5, 4, 4)))
|
||||
assert len(hits) == 2
|
||||
assert hits[0] == 3
|
||||
|
||||
def test_lazy_build(self):
|
||||
assert self.df.geometry.values._sindex is None
|
||||
assert self.df.sindex.size == 5
|
||||
assert self.df.geometry.values._sindex is not None
|
||||
|
||||
def test_sindex_rebuild_on_set_geometry(self):
|
||||
# First build the sindex
|
||||
assert self.df.sindex is not None
|
||||
original_index = self.df.sindex
|
||||
self.df.set_geometry(
|
||||
[Point(x, y) for x, y in zip(range(5, 10), range(5, 10))], inplace=True
|
||||
)
|
||||
assert self.df.sindex is not original_index
|
||||
|
||||
def test_rebuild_on_row_slice(self):
|
||||
# Select a subset of rows rebuilds
|
||||
original_index = self.df.sindex
|
||||
sliced = self.df.iloc[:1]
|
||||
assert sliced.sindex is not original_index
|
||||
# Slicing all does not rebuild
|
||||
original_index = self.df.sindex
|
||||
sliced = self.df.iloc[:]
|
||||
assert sliced.sindex is original_index
|
||||
# Re-ordering rebuilds
|
||||
sliced = self.df.iloc[::-1]
|
||||
assert sliced.sindex is not original_index
|
||||
|
||||
def test_rebuild_on_single_col_selection(self):
|
||||
"""Selecting a single column should not rebuild the spatial index."""
|
||||
# Selecting geometry column preserves the index
|
||||
original_index = self.df.sindex
|
||||
geometry_col = self.df["geom"]
|
||||
assert geometry_col.sindex is original_index
|
||||
geometry_col = self.df.geometry
|
||||
assert geometry_col.sindex is original_index
|
||||
|
||||
def test_rebuild_on_multiple_col_selection(self):
|
||||
"""Selecting a subset of columns preserves the index."""
|
||||
original_index = self.df.sindex
|
||||
# Selecting a subset of columns preserves the index for pandas < 2.0
|
||||
# with pandas 2.0, the column is now copied, losing the index (although
|
||||
# with Copy-on-Write, this will again be preserved)
|
||||
subset1 = self.df[["geom", "A"]]
|
||||
if compat.PANDAS_GE_20 and not pd.options.mode.copy_on_write:
|
||||
assert subset1.sindex is not original_index
|
||||
else:
|
||||
assert subset1.sindex is original_index
|
||||
subset2 = self.df[["A", "geom"]]
|
||||
if compat.PANDAS_GE_20 and not pd.options.mode.copy_on_write:
|
||||
assert subset2.sindex is not original_index
|
||||
else:
|
||||
assert subset2.sindex is original_index
|
||||
|
||||
def test_rebuild_on_update_inplace(self):
|
||||
gdf = self.df.copy()
|
||||
old_sindex = gdf.sindex
|
||||
# sorting in place
|
||||
gdf.sort_values("A", ascending=False, inplace=True)
|
||||
# spatial index should be invalidated
|
||||
assert not gdf.has_sindex
|
||||
new_sindex = gdf.sindex
|
||||
# and should be different
|
||||
assert new_sindex is not old_sindex
|
||||
|
||||
# sorting should still have happened though
|
||||
assert gdf.index.tolist() == [4, 3, 2, 1, 0]
|
||||
|
||||
def test_update_inplace_no_rebuild(self):
|
||||
gdf = self.df.copy()
|
||||
old_sindex = gdf.sindex
|
||||
gdf.rename(columns={"A": "AA"}, inplace=True)
|
||||
# a rename shouldn't invalidate the index
|
||||
assert gdf.has_sindex
|
||||
# and the "new" should be the same
|
||||
new_sindex = gdf.sindex
|
||||
assert old_sindex is new_sindex
|
||||
|
||||
|
||||
# Skip to accommodate Shapely geometries being unhashable
|
||||
@pytest.mark.skip
|
||||
class TestJoinSindex:
|
||||
def setup_method(self):
|
||||
nybb_filename = geopandas.datasets.get_path("nybb")
|
||||
self.boros = read_file(nybb_filename)
|
||||
|
||||
def test_merge_geo(self):
|
||||
# First check that we gets hits from the boros frame.
|
||||
tree = self.boros.sindex
|
||||
hits = tree.intersection((1012821.80, 229228.26))
|
||||
res = [self.boros.iloc[hit]["BoroName"] for hit in hits]
|
||||
assert res == ["Bronx", "Queens"]
|
||||
|
||||
# Check that we only get the Bronx from this view.
|
||||
first = self.boros[self.boros["BoroCode"] < 3]
|
||||
tree = first.sindex
|
||||
hits = tree.intersection((1012821.80, 229228.26))
|
||||
res = [first.iloc[hit]["BoroName"] for hit in hits]
|
||||
assert res == ["Bronx"]
|
||||
|
||||
# Check that we only get Queens from this view.
|
||||
second = self.boros[self.boros["BoroCode"] >= 3]
|
||||
tree = second.sindex
|
||||
hits = tree.intersection((1012821.80, 229228.26))
|
||||
res = ([second.iloc[hit]["BoroName"] for hit in hits],)
|
||||
assert res == ["Queens"]
|
||||
|
||||
# Get both the Bronx and Queens again.
|
||||
merged = first.merge(second, how="outer")
|
||||
assert len(merged) == 5
|
||||
assert merged.sindex.size == 5
|
||||
tree = merged.sindex
|
||||
hits = tree.intersection((1012821.80, 229228.26))
|
||||
res = [merged.iloc[hit]["BoroName"] for hit in hits]
|
||||
assert res == ["Bronx", "Queens"]
|
||||
|
||||
|
||||
@pytest.mark.skip_no_sindex
|
||||
class TestPygeosInterface:
|
||||
def setup_method(self):
|
||||
data = {
|
||||
"geom": [Point(x, y) for x, y in zip(range(5), range(5))]
|
||||
+ [box(10, 10, 20, 20)] # include a box geometry
|
||||
}
|
||||
self.df = GeoDataFrame(data, geometry="geom")
|
||||
self.expected_size = len(data["geom"])
|
||||
|
||||
# --------------------------- `intersection` tests -------------------------- #
|
||||
@pytest.mark.parametrize(
|
||||
"test_geom, expected",
|
||||
(
|
||||
((-1, -1, -0.5, -0.5), []),
|
||||
((-0.5, -0.5, 0.5, 0.5), [0]),
|
||||
((0, 0, 1, 1), [0, 1]),
|
||||
((0, 0), [0]),
|
||||
),
|
||||
)
|
||||
def test_intersection_bounds_tuple(self, test_geom, expected):
|
||||
"""Tests the `intersection` method with valid inputs."""
|
||||
res = list(self.df.sindex.intersection(test_geom))
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
@pytest.mark.parametrize("test_geom", ((-1, -1, -0.5), -0.5, None, Point(0, 0)))
|
||||
def test_intersection_invalid_bounds_tuple(self, test_geom):
|
||||
"""Tests the `intersection` method with invalid inputs."""
|
||||
if compat.USE_PYGEOS:
|
||||
with pytest.raises(TypeError):
|
||||
# we raise a useful TypeError
|
||||
self.df.sindex.intersection(test_geom)
|
||||
else:
|
||||
with pytest.raises((TypeError, Exception)):
|
||||
# catch a general exception
|
||||
# rtree raises an RTreeError which we need to catch
|
||||
self.df.sindex.intersection(test_geom)
|
||||
|
||||
# ------------------------------ `query` tests ------------------------------ #
|
||||
@pytest.mark.parametrize(
|
||||
"predicate, test_geom, expected",
|
||||
(
|
||||
(None, box(-1, -1, -0.5, -0.5), []), # bbox does not intersect
|
||||
(None, box(-0.5, -0.5, 0.5, 0.5), [0]), # bbox intersects
|
||||
(None, box(0, 0, 1, 1), [0, 1]), # bbox intersects multiple
|
||||
(
|
||||
None,
|
||||
LineString([(0, 1), (1, 0)]),
|
||||
[0, 1],
|
||||
), # bbox intersects but not geometry
|
||||
("intersects", box(-1, -1, -0.5, -0.5), []), # bbox does not intersect
|
||||
(
|
||||
"intersects",
|
||||
box(-0.5, -0.5, 0.5, 0.5),
|
||||
[0],
|
||||
), # bbox and geometry intersect
|
||||
(
|
||||
"intersects",
|
||||
box(0, 0, 1, 1),
|
||||
[0, 1],
|
||||
), # bbox and geometry intersect multiple
|
||||
(
|
||||
"intersects",
|
||||
LineString([(0, 1), (1, 0)]),
|
||||
[],
|
||||
), # bbox intersects but not geometry
|
||||
("within", box(0.25, 0.28, 0.75, 0.75), []), # does not intersect
|
||||
("within", box(0, 0, 10, 10), []), # intersects but is not within
|
||||
("within", box(11, 11, 12, 12), [5]), # intersects and is within
|
||||
("within", LineString([(0, 1), (1, 0)]), []), # intersects but not within
|
||||
("contains", box(0, 0, 1, 1), []), # intersects but does not contain
|
||||
("contains", box(0, 0, 1.001, 1.001), [1]), # intersects and contains
|
||||
("contains", box(0.5, 0.5, 1.5, 1.5), [1]), # intersects and contains
|
||||
("contains", box(-1, -1, 2, 2), [0, 1]), # intersects and contains multiple
|
||||
(
|
||||
"contains",
|
||||
LineString([(0, 1), (1, 0)]),
|
||||
[],
|
||||
), # intersects but not contains
|
||||
("touches", box(-1, -1, 0, 0), [0]), # bbox intersects and touches
|
||||
(
|
||||
"touches",
|
||||
box(-0.5, -0.5, 1.5, 1.5),
|
||||
[],
|
||||
), # bbox intersects but geom does not touch
|
||||
(
|
||||
"contains",
|
||||
box(10, 10, 20, 20),
|
||||
[5],
|
||||
), # contains but does not contains_properly
|
||||
(
|
||||
"covers",
|
||||
box(-0.5, -0.5, 1, 1),
|
||||
[0, 1],
|
||||
), # covers (0, 0) and (1, 1)
|
||||
(
|
||||
"covers",
|
||||
box(0.001, 0.001, 0.99, 0.99),
|
||||
[],
|
||||
), # does not cover any
|
||||
(
|
||||
"covers",
|
||||
box(0, 0, 1, 1),
|
||||
[0, 1],
|
||||
), # covers but does not contain
|
||||
(
|
||||
"contains_properly",
|
||||
box(0, 0, 1, 1),
|
||||
[],
|
||||
), # intersects but does not contain
|
||||
(
|
||||
"contains_properly",
|
||||
box(0, 0, 1.001, 1.001),
|
||||
[1],
|
||||
), # intersects 2 and contains 1
|
||||
(
|
||||
"contains_properly",
|
||||
box(0.5, 0.5, 1.001, 1.001),
|
||||
[1],
|
||||
), # intersects 1 and contains 1
|
||||
(
|
||||
"contains_properly",
|
||||
box(0.5, 0.5, 1.5, 1.5),
|
||||
[1],
|
||||
), # intersects and contains
|
||||
(
|
||||
"contains_properly",
|
||||
box(-1, -1, 2, 2),
|
||||
[0, 1],
|
||||
), # intersects and contains multiple
|
||||
(
|
||||
"contains_properly",
|
||||
box(10, 10, 20, 20),
|
||||
[],
|
||||
), # contains but does not contains_properly
|
||||
),
|
||||
)
|
||||
def test_query(self, predicate, test_geom, expected):
|
||||
"""Tests the `query` method with valid inputs and valid predicates."""
|
||||
res = self.df.sindex.query(test_geom, predicate=predicate)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
def test_query_invalid_geometry(self):
|
||||
"""Tests the `query` method with invalid geometry."""
|
||||
with pytest.raises(TypeError):
|
||||
self.df.sindex.query("notavalidgeom")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_geom, expected_value",
|
||||
[
|
||||
(None, []),
|
||||
(GeometryCollection(), []),
|
||||
(Point(), []),
|
||||
(MultiPolygon(), []),
|
||||
(Polygon(), []),
|
||||
],
|
||||
)
|
||||
def test_query_empty_geometry(self, test_geom, expected_value):
|
||||
"""Tests the `query` method with empty geometry."""
|
||||
res = self.df.sindex.query(test_geom)
|
||||
assert_array_equal(res, expected_value)
|
||||
|
||||
def test_query_invalid_predicate(self):
|
||||
"""Tests the `query` method with invalid predicates."""
|
||||
test_geom = box(-1, -1, -0.5, -0.5)
|
||||
with pytest.raises(ValueError):
|
||||
self.df.sindex.query(test_geom, predicate="test")
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sort, expected",
|
||||
(
|
||||
(True, [[0, 0, 0], [0, 1, 2]]),
|
||||
# False could be anything, at least we'll know if it changes
|
||||
(False, [[0, 0, 0], [0, 1, 2]]),
|
||||
),
|
||||
)
|
||||
def test_query_sorting(self, sort, expected):
|
||||
"""Check that results from `query` don't depend on the
|
||||
order of geometries.
|
||||
"""
|
||||
# these geometries come from a reported issue:
|
||||
# https://github.com/geopandas/geopandas/issues/1337
|
||||
# there is no theoretical reason they were chosen
|
||||
test_polys = GeoSeries([Polygon([(1, 1), (3, 1), (3, 3), (1, 3)])])
|
||||
tree_polys = GeoSeries(
|
||||
[
|
||||
Polygon([(1, 1), (3, 1), (3, 3), (1, 3)]),
|
||||
Polygon([(-1, 1), (1, 1), (1, 3), (-1, 3)]),
|
||||
Polygon([(3, 3), (5, 3), (5, 5), (3, 5)]),
|
||||
]
|
||||
)
|
||||
expected = [0, 1, 2]
|
||||
|
||||
# pass through GeoSeries to have GeoPandas
|
||||
# determine if it should use shapely or pygeos geometry objects
|
||||
tree_df = geopandas.GeoDataFrame(geometry=tree_polys)
|
||||
test_df = geopandas.GeoDataFrame(geometry=test_polys)
|
||||
|
||||
test_geo = test_df.geometry.values[0]
|
||||
res = tree_df.sindex.query(test_geo, sort=sort)
|
||||
|
||||
# asserting the same elements
|
||||
assert sorted(res) == sorted(expected)
|
||||
# asserting the exact array can fail if sort=False
|
||||
try:
|
||||
assert_array_equal(res, expected)
|
||||
except AssertionError as e:
|
||||
if sort is False:
|
||||
pytest.xfail(
|
||||
"rtree results are known to be unordered, see "
|
||||
"https://github.com/geopandas/geopandas/issues/1337\n"
|
||||
"Expected:\n {}\n".format(expected)
|
||||
+ "Got:\n {}\n".format(res.tolist())
|
||||
)
|
||||
raise e
|
||||
|
||||
# ------------------------- `query_bulk` tests -------------------------- #
|
||||
@pytest.mark.parametrize(
|
||||
"predicate, test_geom, expected",
|
||||
(
|
||||
(None, [(-1, -1, -0.5, -0.5)], [[], []]),
|
||||
(None, [(-0.5, -0.5, 0.5, 0.5)], [[0], [0]]),
|
||||
(None, [(0, 0, 1, 1)], [[0, 0], [0, 1]]),
|
||||
("intersects", [(-1, -1, -0.5, -0.5)], [[], []]),
|
||||
("intersects", [(-0.5, -0.5, 0.5, 0.5)], [[0], [0]]),
|
||||
("intersects", [(0, 0, 1, 1)], [[0, 0], [0, 1]]),
|
||||
# only second geom intersects
|
||||
("intersects", [(-1, -1, -0.5, -0.5), (-0.5, -0.5, 0.5, 0.5)], [[1], [0]]),
|
||||
# both geoms intersect
|
||||
(
|
||||
"intersects",
|
||||
[(-1, -1, 1, 1), (-0.5, -0.5, 0.5, 0.5)],
|
||||
[[0, 0, 1], [0, 1, 0]],
|
||||
),
|
||||
("within", [(0.25, 0.28, 0.75, 0.75)], [[], []]), # does not intersect
|
||||
("within", [(0, 0, 10, 10)], [[], []]), # intersects but is not within
|
||||
("within", [(11, 11, 12, 12)], [[0], [5]]), # intersects and is within
|
||||
(
|
||||
"contains",
|
||||
[(0, 0, 1, 1)],
|
||||
[[], []],
|
||||
), # intersects and covers, but does not contain
|
||||
(
|
||||
"contains",
|
||||
[(0, 0, 1.001, 1.001)],
|
||||
[[0], [1]],
|
||||
), # intersects 2 and contains 1
|
||||
(
|
||||
"contains",
|
||||
[(0.5, 0.5, 1.001, 1.001)],
|
||||
[[0], [1]],
|
||||
), # intersects 1 and contains 1
|
||||
("contains", [(0.5, 0.5, 1.5, 1.5)], [[0], [1]]), # intersects and contains
|
||||
(
|
||||
"contains",
|
||||
[(-1, -1, 2, 2)],
|
||||
[[0, 0], [0, 1]],
|
||||
), # intersects and contains multiple
|
||||
(
|
||||
"contains",
|
||||
[(10, 10, 20, 20)],
|
||||
[[0], [5]],
|
||||
), # contains but does not contains_properly
|
||||
("touches", [(-1, -1, 0, 0)], [[0], [0]]), # bbox intersects and touches
|
||||
(
|
||||
"touches",
|
||||
[(-0.5, -0.5, 1.5, 1.5)],
|
||||
[[], []],
|
||||
), # bbox intersects but geom does not touch
|
||||
(
|
||||
"covers",
|
||||
[(-0.5, -0.5, 1, 1)],
|
||||
[[0, 0], [0, 1]],
|
||||
), # covers (0, 0) and (1, 1)
|
||||
(
|
||||
"covers",
|
||||
[(0.001, 0.001, 0.99, 0.99)],
|
||||
[[], []],
|
||||
), # does not cover any
|
||||
(
|
||||
"covers",
|
||||
[(0, 0, 1, 1)],
|
||||
[[0, 0], [0, 1]],
|
||||
), # covers but does not contain
|
||||
(
|
||||
"contains_properly",
|
||||
[(0, 0, 1, 1)],
|
||||
[[], []],
|
||||
), # intersects but does not contain
|
||||
(
|
||||
"contains_properly",
|
||||
[(0, 0, 1.001, 1.001)],
|
||||
[[0], [1]],
|
||||
), # intersects 2 and contains 1
|
||||
(
|
||||
"contains_properly",
|
||||
[(0.5, 0.5, 1.001, 1.001)],
|
||||
[[0], [1]],
|
||||
), # intersects 1 and contains 1
|
||||
(
|
||||
"contains_properly",
|
||||
[(0.5, 0.5, 1.5, 1.5)],
|
||||
[[0], [1]],
|
||||
), # intersects and contains
|
||||
(
|
||||
"contains_properly",
|
||||
[(-1, -1, 2, 2)],
|
||||
[[0, 0], [0, 1]],
|
||||
), # intersects and contains multiple
|
||||
(
|
||||
"contains_properly",
|
||||
[(10, 10, 20, 20)],
|
||||
[[], []],
|
||||
), # contains but does not contains_properly
|
||||
),
|
||||
)
|
||||
def test_query_bulk(self, predicate, test_geom, expected):
|
||||
"""Tests the `query_bulk` method with valid
|
||||
inputs and valid predicates.
|
||||
"""
|
||||
# pass through GeoSeries to have GeoPandas
|
||||
# determine if it should use shapely or pygeos geometry objects
|
||||
test_geom = geopandas.GeoSeries(
|
||||
[box(*geom) for geom in test_geom], index=range(len(test_geom))
|
||||
)
|
||||
res = self.df.sindex.query(test_geom, predicate=predicate)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"test_geoms, expected_value",
|
||||
[
|
||||
# single empty geometry
|
||||
([GeometryCollection()], [[], []]),
|
||||
# None should be skipped
|
||||
([GeometryCollection(), None], [[], []]),
|
||||
([None], [[], []]),
|
||||
([None, box(-0.5, -0.5, 0.5, 0.5), None], [[1], [0]]),
|
||||
],
|
||||
)
|
||||
def test_query_bulk_empty_geometry(self, test_geoms, expected_value):
|
||||
"""Tests the `query_bulk` method with an empty geometry."""
|
||||
# pass through GeoSeries to have GeoPandas
|
||||
# determine if it should use shapely or pygeos geometry objects
|
||||
# note: for this test, test_geoms (note plural) is a list already
|
||||
test_geoms = geopandas.GeoSeries(test_geoms, index=range(len(test_geoms)))
|
||||
res = self.df.sindex.query(test_geoms)
|
||||
assert_array_equal(res, expected_value)
|
||||
|
||||
def test_query_bulk_empty_input_array(self):
|
||||
"""Tests the `query_bulk` method with an empty input array."""
|
||||
test_array = np.array([], dtype=object)
|
||||
expected_value = [[], []]
|
||||
res = self.df.sindex.query(test_array)
|
||||
assert_array_equal(res, expected_value)
|
||||
|
||||
def test_query_bulk_invalid_input_geometry(self):
|
||||
"""
|
||||
Tests the `query_bulk` method with invalid input for the `geometry` parameter.
|
||||
"""
|
||||
test_array = "notanarray"
|
||||
with pytest.raises(TypeError):
|
||||
self.df.sindex.query(test_array)
|
||||
|
||||
def test_query_bulk_invalid_predicate(self):
|
||||
"""Tests the `query_bulk` method with invalid predicates."""
|
||||
test_geom_bounds = (-1, -1, -0.5, -0.5)
|
||||
test_predicate = "test"
|
||||
|
||||
# pass through GeoSeries to have GeoPandas
|
||||
# determine if it should use shapely or pygeos geometry objects
|
||||
test_geom = geopandas.GeoSeries([box(*test_geom_bounds)], index=["0"])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
self.df.sindex.query(test_geom.geometry, predicate=test_predicate)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"predicate, test_geom, expected",
|
||||
(
|
||||
(None, (-1, -1, -0.5, -0.5), [[], []]),
|
||||
("intersects", (-1, -1, -0.5, -0.5), [[], []]),
|
||||
("contains", (-1, -1, 1, 1), [[0], [0]]),
|
||||
),
|
||||
)
|
||||
def test_query_bulk_input_type(self, predicate, test_geom, expected):
|
||||
"""Tests that query_bulk can accept a GeoSeries, GeometryArray or
|
||||
numpy array.
|
||||
"""
|
||||
# pass through GeoSeries to have GeoPandas
|
||||
# determine if it should use shapely or pygeos geometry objects
|
||||
test_geom = geopandas.GeoSeries([box(*test_geom)], index=["0"])
|
||||
|
||||
# test GeoSeries
|
||||
res = self.df.sindex.query(test_geom, predicate=predicate)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
# test GeometryArray
|
||||
res = self.df.sindex.query(test_geom.geometry, predicate=predicate)
|
||||
assert_array_equal(res, expected)
|
||||
res = self.df.sindex.query(test_geom.geometry.values, predicate=predicate)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
# test numpy array
|
||||
res = self.df.sindex.query(
|
||||
test_geom.geometry.values.to_numpy(), predicate=predicate
|
||||
)
|
||||
assert_array_equal(res, expected)
|
||||
res = self.df.sindex.query(
|
||||
test_geom.geometry.values.to_numpy(), predicate=predicate
|
||||
)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"sort, expected",
|
||||
(
|
||||
(True, [[0, 0, 0], [0, 1, 2]]),
|
||||
# False could be anything, at least we'll know if it changes
|
||||
(False, [[0, 0, 0], [0, 1, 2]]),
|
||||
),
|
||||
)
|
||||
def test_query_bulk_sorting(self, sort, expected):
|
||||
"""Check that results from `query_bulk` don't depend
|
||||
on the order of geometries.
|
||||
"""
|
||||
# these geometries come from a reported issue:
|
||||
# https://github.com/geopandas/geopandas/issues/1337
|
||||
# there is no theoretical reason they were chosen
|
||||
test_polys = GeoSeries([Polygon([(1, 1), (3, 1), (3, 3), (1, 3)])])
|
||||
tree_polys = GeoSeries(
|
||||
[
|
||||
Polygon([(1, 1), (3, 1), (3, 3), (1, 3)]),
|
||||
Polygon([(-1, 1), (1, 1), (1, 3), (-1, 3)]),
|
||||
Polygon([(3, 3), (5, 3), (5, 5), (3, 5)]),
|
||||
]
|
||||
)
|
||||
|
||||
# pass through GeoSeries to have GeoPandas
|
||||
# determine if it should use shapely or pygeos geometry objects
|
||||
tree_df = geopandas.GeoDataFrame(geometry=tree_polys)
|
||||
test_df = geopandas.GeoDataFrame(geometry=test_polys)
|
||||
|
||||
res = tree_df.sindex.query(test_df.geometry, sort=sort)
|
||||
|
||||
# asserting the same elements
|
||||
assert sorted(res[0]) == sorted(expected[0])
|
||||
assert sorted(res[1]) == sorted(expected[1])
|
||||
# asserting the exact array can fail if sort=False
|
||||
try:
|
||||
assert_array_equal(res, expected)
|
||||
except AssertionError as e:
|
||||
if sort is False:
|
||||
pytest.xfail(
|
||||
"rtree results are known to be unordered, see "
|
||||
"https://github.com/geopandas/geopandas/issues/1337\n"
|
||||
"Expected:\n {}\n".format(expected)
|
||||
+ "Got:\n {}\n".format(res.tolist())
|
||||
)
|
||||
raise e
|
||||
|
||||
# ------------------------- `nearest` tests ------------------------- #
|
||||
@pytest.mark.skipif(
|
||||
compat.USE_PYGEOS or compat.USE_SHAPELY_20,
|
||||
reason=("RTree supports sindex.nearest with different behaviour"),
|
||||
)
|
||||
def test_rtree_nearest_warns(self):
|
||||
df = geopandas.GeoDataFrame({"geometry": []})
|
||||
with pytest.warns(
|
||||
FutureWarning, match="sindex.nearest using the rtree backend"
|
||||
):
|
||||
df.sindex.nearest((0, 0, 1, 1), num_results=2)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
compat.USE_SHAPELY_20 or not (compat.USE_PYGEOS and not compat.PYGEOS_GE_010),
|
||||
reason=("PyGEOS < 0.10 does not support sindex.nearest"),
|
||||
)
|
||||
def test_pygeos_error(self):
|
||||
df = geopandas.GeoDataFrame({"geometry": []})
|
||||
with pytest.raises(NotImplementedError, match="requires pygeos >= 0.10"):
|
||||
df.sindex.nearest(None)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not (compat.USE_SHAPELY_20 or (compat.USE_PYGEOS and compat.PYGEOS_GE_010)),
|
||||
reason=("PyGEOS >= 0.10 is required to test sindex.nearest"),
|
||||
)
|
||||
@pytest.mark.parametrize("return_all", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"geometry,expected",
|
||||
[
|
||||
([0.25, 0.25], [[0], [0]]),
|
||||
([0.75, 0.75], [[0], [1]]),
|
||||
],
|
||||
)
|
||||
def test_nearest_single(self, geometry, expected, return_all):
|
||||
geoms = mod.points(np.arange(10), np.arange(10))
|
||||
df = geopandas.GeoDataFrame({"geometry": geoms})
|
||||
|
||||
p = Point(geometry)
|
||||
res = df.sindex.nearest(p, return_all=return_all)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
p = mod.points(geometry)
|
||||
res = df.sindex.nearest(p, return_all=return_all)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not (compat.USE_SHAPELY_20 or (compat.USE_PYGEOS and compat.PYGEOS_GE_010)),
|
||||
reason=("PyGEOS >= 0.10 is required to test sindex.nearest"),
|
||||
)
|
||||
@pytest.mark.parametrize("return_all", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"geometry,expected",
|
||||
[
|
||||
([(1, 1), (0, 0)], [[0, 1], [1, 0]]),
|
||||
([(1, 1), (0.25, 1)], [[0, 1], [1, 1]]),
|
||||
],
|
||||
)
|
||||
def test_nearest_multi(self, geometry, expected, return_all):
|
||||
geoms = mod.points(np.arange(10), np.arange(10))
|
||||
df = geopandas.GeoDataFrame({"geometry": geoms})
|
||||
|
||||
ps = [Point(p) for p in geometry]
|
||||
res = df.sindex.nearest(ps, return_all=return_all)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
ps = mod.points(geometry)
|
||||
res = df.sindex.nearest(ps, return_all=return_all)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
s = geopandas.GeoSeries(ps)
|
||||
res = df.sindex.nearest(s, return_all=return_all)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
x, y = zip(*geometry)
|
||||
ga = geopandas.points_from_xy(x, y)
|
||||
res = df.sindex.nearest(ga, return_all=return_all)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not (compat.USE_SHAPELY_20 or (compat.USE_PYGEOS and compat.PYGEOS_GE_010)),
|
||||
reason=("PyGEOS >= 0.10 is required to test sindex.nearest"),
|
||||
)
|
||||
@pytest.mark.parametrize("return_all", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"geometry,expected",
|
||||
[
|
||||
(None, [[], []]),
|
||||
([None], [[], []]),
|
||||
],
|
||||
)
|
||||
def test_nearest_none(self, geometry, expected, return_all):
|
||||
geoms = mod.points(np.arange(10), np.arange(10))
|
||||
df = geopandas.GeoDataFrame({"geometry": geoms})
|
||||
|
||||
res = df.sindex.nearest(geometry, return_all=return_all)
|
||||
assert_array_equal(res, expected)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not (compat.USE_SHAPELY_20 or (compat.USE_PYGEOS and compat.PYGEOS_GE_010)),
|
||||
reason=("PyGEOS >= 0.10 is required to test sindex.nearest"),
|
||||
)
|
||||
@pytest.mark.parametrize("return_distance", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"return_all,max_distance,expected",
|
||||
[
|
||||
(True, None, ([[0, 0, 1], [0, 1, 5]], [sqrt(0.5), sqrt(0.5), sqrt(50)])),
|
||||
(False, None, ([[0, 1], [0, 5]], [sqrt(0.5), sqrt(50)])),
|
||||
(True, 1, ([[0, 0], [0, 1]], [sqrt(0.5), sqrt(0.5)])),
|
||||
(False, 1, ([[0], [0]], [sqrt(0.5)])),
|
||||
],
|
||||
)
|
||||
def test_nearest_max_distance(
|
||||
self, expected, max_distance, return_all, return_distance
|
||||
):
|
||||
geoms = mod.points(np.arange(10), np.arange(10))
|
||||
df = geopandas.GeoDataFrame({"geometry": geoms})
|
||||
|
||||
ps = [Point(0.5, 0.5), Point(0, 10)]
|
||||
res = df.sindex.nearest(
|
||||
ps,
|
||||
return_all=return_all,
|
||||
max_distance=max_distance,
|
||||
return_distance=return_distance,
|
||||
)
|
||||
if return_distance:
|
||||
assert_array_equal(res[0], expected[0])
|
||||
assert_array_equal(res[1], expected[1])
|
||||
else:
|
||||
assert_array_equal(res, expected[0])
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not (compat.USE_SHAPELY_20),
|
||||
reason=(
|
||||
"shapely >= 2.0 is required to test sindex.nearest with parameter exclusive"
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize("return_distance", [True, False])
|
||||
@pytest.mark.parametrize(
|
||||
"return_all,max_distance,exclusive,expected",
|
||||
[
|
||||
(False, None, False, ([[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], 5 * [0])),
|
||||
(False, None, True, ([[0, 1, 2, 3, 4], [1, 0, 1, 2, 3]], 5 * [sqrt(2)])),
|
||||
(True, None, False, ([[0, 1, 2, 3, 4], [0, 1, 2, 3, 4]], 5 * [0])),
|
||||
(
|
||||
True,
|
||||
None,
|
||||
True,
|
||||
([[0, 1, 1, 2, 2, 3, 3, 4], [1, 0, 2, 1, 3, 2, 4, 3]], 8 * [sqrt(2)]),
|
||||
),
|
||||
(False, 1.1, True, ([[1, 2, 5], [5, 5, 1]], 3 * [1])),
|
||||
(True, 1.1, True, ([[1, 2, 5, 5], [5, 5, 1, 2]], 4 * [1])),
|
||||
],
|
||||
)
|
||||
def test_nearest_exclusive(
|
||||
self, expected, max_distance, return_all, return_distance, exclusive
|
||||
):
|
||||
geoms = mod.points(np.arange(5), np.arange(5))
|
||||
if max_distance:
|
||||
# add a non grid point
|
||||
geoms = np.append(geoms, [Point(1, 2)])
|
||||
|
||||
df = geopandas.GeoDataFrame({"geometry": geoms})
|
||||
|
||||
ps = geoms
|
||||
res = df.sindex.nearest(
|
||||
ps,
|
||||
return_all=return_all,
|
||||
max_distance=max_distance,
|
||||
return_distance=return_distance,
|
||||
exclusive=exclusive,
|
||||
)
|
||||
if return_distance:
|
||||
assert_array_equal(res[0], expected[0])
|
||||
assert_array_equal(res[1], expected[1])
|
||||
else:
|
||||
assert_array_equal(res, expected[0])
|
||||
|
||||
@pytest.mark.skipif(
|
||||
compat.USE_SHAPELY_20 or not (compat.USE_PYGEOS and not compat.PYGEOS_GE_010),
|
||||
reason="sindex.nearest exclusive parameter requires shapely >= 2.0",
|
||||
)
|
||||
def test_nearest_exclusive_unavailable(self):
|
||||
from shapely.geometry import Point
|
||||
|
||||
geoms = [Point((x, y)) for (x, y) in zip(np.arange(5), np.arange(5))]
|
||||
df = geopandas.GeoDataFrame(geometry=geoms)
|
||||
|
||||
with pytest.raises(NotImplementedError, match="requires shapely >= 2.0"):
|
||||
df.sindex.nearest(geoms, exclusive=True)
|
||||
|
||||
# --------------------------- misc tests ---------------------------- #
|
||||
|
||||
def test_empty_tree_geometries(self):
|
||||
"""Tests building sindex with interleaved empty geometries."""
|
||||
geoms = [Point(0, 0), None, Point(), Point(1, 1), Point()]
|
||||
df = geopandas.GeoDataFrame(geometry=geoms)
|
||||
assert df.sindex.query(Point(1, 1))[0] == 3
|
||||
|
||||
def test_size(self):
|
||||
"""Tests the `size` property."""
|
||||
assert self.df.sindex.size == self.expected_size
|
||||
|
||||
def test_len(self):
|
||||
"""Tests the `__len__` method of spatial indexes."""
|
||||
assert len(self.df.sindex) == self.expected_size
|
||||
|
||||
def test_is_empty(self):
|
||||
"""Tests the `is_empty` property."""
|
||||
# create empty tree
|
||||
empty = geopandas.GeoSeries([], dtype=object)
|
||||
assert empty.sindex.is_empty
|
||||
empty = geopandas.GeoSeries([None])
|
||||
assert empty.sindex.is_empty
|
||||
empty = geopandas.GeoSeries([Point()])
|
||||
assert empty.sindex.is_empty
|
||||
# create a non-empty tree
|
||||
non_empty = geopandas.GeoSeries([Point(0, 0)])
|
||||
assert not non_empty.sindex.is_empty
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"predicate, expected_shape",
|
||||
[
|
||||
(None, (2, 471)),
|
||||
("intersects", (2, 213)),
|
||||
("within", (2, 213)),
|
||||
("contains", (2, 0)),
|
||||
("overlaps", (2, 0)),
|
||||
("crosses", (2, 0)),
|
||||
("touches", (2, 0)),
|
||||
],
|
||||
)
|
||||
def test_integration_natural_earth(self, predicate, expected_shape):
|
||||
"""Tests output sizes for the naturalearth datasets."""
|
||||
world = read_file(datasets.get_path("naturalearth_lowres"))
|
||||
capitals = read_file(datasets.get_path("naturalearth_cities"))
|
||||
|
||||
res = world.sindex.query(capitals.geometry, predicate)
|
||||
assert res.shape == expected_shape
|
||||
|
||||
|
||||
@pytest.mark.skipif(not compat.HAS_RTREE, reason="no rtree installed")
|
||||
def test_old_spatial_index_deprecated():
|
||||
t1 = Polygon([(0, 0), (1, 0), (1, 1)])
|
||||
t2 = Polygon([(0, 0), (1, 1), (0, 1)])
|
||||
|
||||
stream = ((i, item.bounds, None) for i, item in enumerate([t1, t2]))
|
||||
|
||||
with pytest.warns(FutureWarning):
|
||||
idx = geopandas.sindex.SpatialIndex(stream)
|
||||
|
||||
assert list(idx.intersection((0, 0, 1, 1))) == [0, 1]
|
||||
Reference in New Issue
Block a user