Repository: geopandas/geopandas

Issue:
BUG: sjoin fails for empty dataframe
```
import geopandas as gpd

gdf = gpd.read_file(gpd.datasets.get_path('nybb'))
gpd.sjoin(gdf, gdf.head(0))

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-23-a647daaec265> in <module>
----> 1 gpd.sjoin(gdf.head(0), gdf)

~/model/.venv/lib/python3.7/site-packages/geopandas/tools/sjoin.py in sjoin(left_df, right_df, how, op, lsuffix, rsuffix)
    126             lambda x: list(tree_idx.intersection(x)) if not x == () else []
    127         )
--> 128         idxmatch = idxmatch[idxmatch.apply(len) > 0]
    129         # indexes of overlapping boundaries
    130         if idxmatch.shape[0] > 0:

~/model/.venv/lib/python3.7/site-packages/pandas/core/ops/common.py in new_method(self, other)
     62         other = item_from_zerodim(other)
     63
---> 64         return method(self, other)
     65
     66     return new_method

~/model/.venv/lib/python3.7/site-packages/pandas/core/ops/__init__.py in wrapper(self, other)
    524         rvalues = extract_array(other, extract_numpy=True)
    525
--> 526         res_values = comparison_op(lvalues, rvalues, op)
    527
    528         return _construct_result(self, res_values, index=self.index, name=res_name)

~/model/.venv/lib/python3.7/site-packages/pandas/core/ops/array_ops.py in comparison_op(left, right, op)
    235
    236     if should_extension_dispatch(lvalues, rvalues):
--> 237         res_values = dispatch_to_extension_op(op, lvalues, rvalues)
    238
    239     elif is_scalar(rvalues) and isna(rvalues):

~/model/.venv/lib/python3.7/site-packages/pandas/core/ops/dispatch.py in dispatch_to_extension_op(op, left, right)
    123     # The op calls will raise TypeError if the op is not defined
    124     # on the ExtensionArray
--> 125     res_values = op(left, right)
    126     return res_values

TypeError: '>' not supported between instances of 'GeometryArray' and 'int'
```
Looks like the existing checks handle an empty geometry but not a totally empty row; would just wrapping this line in `if not idxmatch.empty:` be sufficient...?

Discussion:
martinfleis: Good job! Changes looking good, tests cover both options being fixed, CI is happy. Merging. Thanks @ian-r-rose!

Patch:
diff --git a/geopandas/tools/sjoin.py b/geopandas/tools/sjoin.py
index d7157405..0473f099 100644
--- a/geopandas/tools/sjoin.py
+++ b/geopandas/tools/sjoin.py
@@ -75,14 +75,15 @@ def sjoin(
         )
 
     # Attempt to re-use spatial indexes, otherwise generate the spatial index
-    # for the longer dataframe
+    # for the longer dataframe. If we are joining to an empty dataframe,
+    # don't bother generating the index.
     if right_df._sindex_generated or (
         not left_df._sindex_generated and right_df.shape[0] > left_df.shape[0]
     ):
-        tree_idx = right_df.sindex
+        tree_idx = right_df.sindex if len(left_df) > 0 else None
         tree_idx_right = True
     else:
-        tree_idx = left_df.sindex
+        tree_idx = left_df.sindex if len(right_df) > 0 else None
         tree_idx_right = False
 
     # the rtree spatial index only allows limited (numeric) index types, but an
@@ -120,8 +121,10 @@ def sjoin(
 
     r_idx = np.empty((0, 0))
     l_idx = np.empty((0, 0))
-    # get rtree spatial index
-    if tree_idx_right:
+    # get rtree spatial index. If tree_idx does not exist, it is due to either a
+    # failure to generate the index (e.g., if the column is empty), or the
+    # other dataframe is empty so it wasn't necessary to generate it.
+    if tree_idx_right and tree_idx:
         idxmatch = left_df.geometry.apply(lambda x: x.bounds).apply(
             lambda x: list(tree_idx.intersection(x)) if not x == () else []
         )
@@ -130,7 +133,7 @@ def sjoin(
         if idxmatch.shape[0] > 0:
             r_idx = np.concatenate(idxmatch.values)
             l_idx = np.concatenate([[i] * len(v) for i, v in idxmatch.iteritems()])
-    else:
+    elif not tree_idx_right and tree_idx:
         # tree_idx_df == 'left'
         idxmatch = right_df.geometry.apply(lambda x: x.bounds).apply(
             lambda x: list(tree_idx.intersection(x)) if not x == () else []


Test patch:
diff --git a/geopandas/tools/tests/test_sjoin.py b/geopandas/tools/tests/test_sjoin.py
index 46309452..168f8983 100644
--- a/geopandas/tools/tests/test_sjoin.py
+++ b/geopandas/tools/tests/test_sjoin.py
@@ -186,7 +186,7 @@ class TestSpatialJoin:
         assert_frame_equal(res, exp)
 
     def test_empty_join(self):
-        # Check empty joins
+        # Check joins resulting in empty gdfs.
         polygons = geopandas.GeoDataFrame(
             {
                 "col2": [1, 2],
@@ -204,6 +204,32 @@ class TestSpatialJoin:
         empty = sjoin(not_in, polygons, how="inner", op="intersects")
         assert empty.empty
 
+    @pytest.mark.parametrize("op", ["intersects", "contains", "within"])
+    @pytest.mark.parametrize(
+        "empty",
+        [
+            GeoDataFrame(geometry=[GeometryCollection(), GeometryCollection()]),
+            GeoDataFrame(geometry=GeoSeries()),
+        ],
+    )
+    def test_join_with_empty(self, op, empty):
+        # Check joins with empty geometry columns/dataframes.
+        polygons = geopandas.GeoDataFrame(
+            {
+                "col2": [1, 2],
+                "geometry": [
+                    Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
+                    Polygon([(1, 0), (2, 0), (2, 1), (1, 1)]),
+                ],
+            }
+        )
+        result = sjoin(empty, polygons, how="left", op=op)
+        assert result.index_right.isnull().all()
+        result = sjoin(empty, polygons, how="right", op=op)
+        assert result.index_left.isnull().all()
+        result = sjoin(empty, polygons, how="inner", op=op)
+        assert result.empty
+
     @pytest.mark.parametrize("dfs", ["default-index", "string-index"], indirect=True)
     def test_sjoin_invalid_args(self, dfs):
         index, df1, df2, expected = dfs