Repository: geopandas/geopandas

Issue:
BUG: Can't write empty GeoJSON
- [x] I have checked that this issue has not already been reported.

- [x] I have confirmed this bug exists on the latest version of geopandas.

- [x] (optional) I have confirmed this bug exists on the master branch of geopandas.

---

**Note**: Please read [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) detailing how to provide the necessary information for us to reproduce your bug.

#### Code Sample, a copy-pastable example

```python
import geopandas as gpd
gpd.GeoDataFrame(geometry=[]).to_file("test", driver="GeoJSON")
```
raises
```python
Traceback (most recent call last):

  File "/tmp/ipykernel_2102262/1921918865.py", line 1, in <module>
    gpd.GeoDataFrame(geometry=[]).to_file("test", driver="GeoJSON")

  File "/home/lukas/anaconda3/envs/tf_gpu/lib/python3.9/site-packages/geopandas/geodataframe.py", line 1086, in to_file
    _to_file(self, filename, driver, schema, index, **kwargs)

  File "/home/lukas/anaconda3/envs/tf_gpu/lib/python3.9/site-packages/geopandas/io/file.py", line 304, in _to_file
    schema = infer_schema(df)

  File "/home/lukas/anaconda3/envs/tf_gpu/lib/python3.9/site-packages/geopandas/io/file.py", line 362, in infer_schema
    raise ValueError("Cannot write empty DataFrame to file.")

ValueError: Cannot write empty DataFrame to file.
```

#### Problem description

It's perfectly valid to write an empty GeoJSON. While this may not be true for other filetypes that Geopandas supports, Geopandas shouldn't prevent you from writing an empty GeoJSON.

It was possible, until this PR: https://github.com/geopandas/geopandas/pull/663

Why is this important? When dealing with large amounts of data, it would be a lot of effort to special-case the edge case of an empty collection everywhere that it occurs.

#### Expected Output

By commenting out [these lines](https://github.com/geopandas/geopandas/blob/master/geopandas/io/file.py#L427-L428), Geopandas writes a valid GeoJSON file that looks like this:

```json
{
"type": "FeatureCollection",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [

]
}
```
This is the expected output.

#### Output of ``geopandas.show_versions()``

<details>

SYSTEM INFO
-----------
python     : 3.9.7 (default, Sep 16 2021, 13:09:58)  [GCC 7.5.0]
executable : /home/lukas/anaconda3/envs/tf_gpu/bin/python
machine    : Linux-5.4.0-80-generic-x86_64-with-glibc2.31

GEOS, GDAL, PROJ INFO
---------------------
GEOS       : None
GEOS lib   : None
GDAL       : 3.3.0
GDAL data dir: /home/lukas/anaconda3/envs/tf_gpu/lib/python3.9/site-packages/fiona/gdal_data
PROJ       : 8.1.1
PROJ data dir: /home/lukas/anaconda3/envs/tf_gpu/lib/python3.9/site-packages/pyproj/proj_dir/share/proj

PYTHON DEPENDENCIES
-------------------
geopandas  : 0.9.0
pandas     : 1.3.4
fiona      : 1.8.20
numpy      : 1.19.5
shapely    : 1.8.0
rtree      : 0.9.7
pyproj     : 3.2.1
matplotlib : 3.4.3
mapclassify: None
geopy      : None
psycopg2   : None
geoalchemy2: None
pyarrow    : None
pygeos     : 0.10.2

</details>


Discussion:

Patch:
diff --git a/geopandas/io/file.py b/geopandas/io/file.py
index 836dc025..d524a80b 100644
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -256,8 +256,6 @@ def _read_file(filename, bbox=None, mask=None, rows=None, **kwargs):
 
 
 def read_file(*args, **kwargs):
-    import warnings
-
     warnings.warn(
         "geopandas.io.file.read_file() is intended for internal "
         "use only, and will be deprecated. Use geopandas.read_file() instead.",
@@ -269,8 +267,6 @@ def read_file(*args, **kwargs):
 
 
 def to_file(*args, **kwargs):
-    import warnings
-
     warnings.warn(
         "geopandas.io.file.to_file() is intended for internal "
         "use only, and will be deprecated. Use GeoDataFrame.to_file() "
@@ -432,7 +428,12 @@ def infer_schema(df):
     )
 
     if df.empty:
-        raise ValueError("Cannot write empty DataFrame to file.")
+        warnings.warn(
+            "You are attempting to write an empty DataFrame to file. "
+            "For some drivers, this operation may fail.",
+            UserWarning,
+            stacklevel=3,
+        )
 
     # Since https://github.com/Toblerity/Fiona/issues/446 resolution,
     # Fiona allows a list of geometry types
diff --git a/geopandas/sindex.py b/geopandas/sindex.py
index 736059b4..effa2082 100644
--- a/geopandas/sindex.py
+++ b/geopandas/sindex.py
@@ -633,7 +633,7 @@ if compat.HAS_PYGEOS:
 
     _PYGEOS_PREDICATES = {p.name for p in pygeos.strtree.BinaryPredicate} | set([None])
 
-    class PyGEOSSTRTreeIndex(pygeos.STRtree):
+    class PyGEOSSTRTreeIndex(BaseSpatialIndex):
         """A simple wrapper around pygeos's STRTree.
 
 
@@ -651,7 +651,7 @@ if compat.HAS_PYGEOS:
             non_empty = geometry.copy()
             non_empty[pygeos.is_empty(non_empty)] = None
             # set empty geometries to None to maintain indexing
-            super().__init__(non_empty)
+            self._tree = pygeos.STRtree(non_empty)
             # store geometries, including empty geometries for user access
             self.geometries = geometry.copy()
 
@@ -687,7 +687,7 @@ if compat.HAS_PYGEOS:
             if isinstance(geometry, BaseGeometry):
                 geometry = array._shapely_to_geom(geometry)
 
-            matches = super().query(geometry=geometry, predicate=predicate)
+            matches = self._tree.query(geometry=geometry, predicate=predicate)
 
             if sort:
                 return np.sort(matches)
@@ -740,7 +740,7 @@ if compat.HAS_PYGEOS:
 
             geometry = self._as_geometry_array(geometry)
 
-            res = super().query_bulk(geometry, predicate)
+            res = self._tree.query_bulk(geometry, predicate)
 
             if sort:
                 # sort by first array (geometry) and then second (tree)
@@ -760,9 +760,9 @@ if compat.HAS_PYGEOS:
             geometry = self._as_geometry_array(geometry)
 
             if not return_all and max_distance is None and not return_distance:
-                return super().nearest(geometry)
+                return self._tree.nearest(geometry)
 
-            result = super().nearest_all(
+            result = self._tree.nearest_all(
                 geometry, max_distance=max_distance, return_distance=return_distance
             )
             if return_distance:
@@ -804,9 +804,9 @@ if compat.HAS_PYGEOS:
 
             # need to convert tuple of bounds to a geometry object
             if len(coordinates) == 4:
-                indexes = super().query(pygeos.box(*coordinates))
+                indexes = self._tree.query(pygeos.box(*coordinates))
             elif len(coordinates) == 2:
-                indexes = super().query(pygeos.points(*coordinates))
+                indexes = self._tree.query(pygeos.points(*coordinates))
             else:
                 raise TypeError(
                     "Invalid coordinates, must be iterable in format "
@@ -819,9 +819,12 @@ if compat.HAS_PYGEOS:
         @property
         @doc(BaseSpatialIndex.size)
         def size(self):
-            return len(self)
+            return len(self._tree)
 
         @property
         @doc(BaseSpatialIndex.is_empty)
         def is_empty(self):
-            return len(self) == 0
+            return len(self._tree) == 0
+
+        def __len__(self):
+            return len(self._tree)


Test patch:
diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py
index 16f26787..bd145fbb 100644
--- a/geopandas/io/tests/test_file.py
+++ b/geopandas/io/tests/test_file.py
@@ -255,7 +255,7 @@ def test_to_file_int64(tmpdir, df_points):
 def test_to_file_empty(tmpdir):
     input_empty_df = GeoDataFrame(columns=["geometry"])
     tempfilename = os.path.join(str(tmpdir), "test.shp")
-    with pytest.raises(ValueError, match="Cannot write empty DataFrame to file."):
+    with pytest.warns(UserWarning):
         input_empty_df.to_file(tempfilename)
 
 
diff --git a/geopandas/tests/test_pandas_methods.py b/geopandas/tests/test_pandas_methods.py
index 42c79de7..801e88a9 100644
--- a/geopandas/tests/test_pandas_methods.py
+++ b/geopandas/tests/test_pandas_methods.py
@@ -384,7 +384,7 @@ def test_fillna(s, df):
     df2["geometry"] = s2
     res = df2.fillna(Point(1, 1))
     assert_geodataframe_equal(res, df)
-    with pytest.raises(NotImplementedError):
+    with pytest.raises((NotImplementedError, TypeError)):  # GH2351
         df2.fillna(0)
 
     # allow non-geometry fill value if there are no missing values