We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Writing to file doesn't respect fiona vsi schemes
Fiona supports writing to different OGR virtual filesystem schemes via the file path, e.g., `zip://`, `s3://`, etc. However, these schemes get overwritten in `gpd.to_file()`, which attempts to take the path and construct an absolute path:
https://github.com/geopandas/geopandas/blob/29add0a735b00dc20c79e0fccc8e6a775c4997b0/geopandas/io/file.py#L127
so you get bad paths like `/home/username/s3:/bucket/object`.

Is this absolute file path important? Is there a reason to not allow VSI-based paths in `gpd.to_file()`? Can we check for a url-like scheme before trying to abs-path it? If so, I'd be happy to submit a PR.

(N.b., the corresponding `gpd.read_file()` handles these URLs correctly).

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `geopandas/io/file.py`
Content:
```
1 from distutils.version import LooseVersion
2 import os
3 
4 import numpy as np
5 import six
6 
7 import fiona
8 
9 from geopandas import GeoDataFrame, GeoSeries
10 
11 try:
12     from fiona import Env as fiona_env
13 except ImportError:
14     from fiona import drivers as fiona_env
15 
16 
17 _FIONA18 = LooseVersion(fiona.__version__) >= LooseVersion("1.8")
18 
19 
20 # Adapted from pandas.io.common
21 if six.PY3:
22     from urllib.request import urlopen as _urlopen
23     from urllib.parse import urlparse as parse_url
24     from urllib.parse import uses_relative, uses_netloc, uses_params
25 else:
26     from urllib2 import urlopen as _urlopen
27     from urlparse import urlparse as parse_url
28     from urlparse import uses_relative, uses_netloc, uses_params
29 
30 _VALID_URLS = set(uses_relative + uses_netloc + uses_params)
31 _VALID_URLS.discard("")
32 
33 
34 def _is_url(url):
35     """Check to see if *url* has a valid protocol."""
36     try:
37         return parse_url(url).scheme in _VALID_URLS
38     except Exception:
39         return False
40 
41 
42 def read_file(filename, bbox=None, **kwargs):
43     """
44     Returns a GeoDataFrame from a file or URL.
45 
46     Parameters
47     ----------
48     filename: str
49         Either the absolute or relative path to the file or URL to
50         be opened.
51     bbox : tuple | GeoDataFrame or GeoSeries, default None
52         Filter features by given bounding box, GeoSeries, or GeoDataFrame.
53         CRS mis-matches are resolved if given a GeoSeries or GeoDataFrame.
54     **kwargs:
55         Keyword args to be passed to the `open` or `BytesCollection` method
56         in the fiona library when opening the file. For more information on
57         possible keywords, type:
58         ``import fiona; help(fiona.open)``
59 
60     Examples
61     --------
62     >>> df = geopandas.read_file("nybb.shp")
63 
64     Returns
65     -------
66     geodataframe : GeoDataFrame
67     """
68     if _is_url(filename):
69         req = _urlopen(filename)
70         path_or_bytes = req.read()
71         reader = fiona.BytesCollection
72     else:
73         path_or_bytes = filename
74         reader = fiona.open
75 
76     with fiona_env():
77         with reader(path_or_bytes, **kwargs) as features:
78 
79             # In a future Fiona release the crs attribute of features will
80             # no longer be a dict. The following code will be both forward
81             # and backward compatible.
82             if hasattr(features.crs, "to_dict"):
83                 crs = features.crs.to_dict()
84             else:
85                 crs = features.crs
86 
87             if bbox is not None:
88                 if isinstance(bbox, GeoDataFrame) or isinstance(bbox, GeoSeries):
89                     bbox = tuple(bbox.to_crs(crs).total_bounds)
90                 assert len(bbox) == 4
91                 f_filt = features.filter(bbox=bbox)
92             else:
93                 f_filt = features
94 
95             columns = list(features.meta["schema"]["properties"]) + ["geometry"]
96             gdf = GeoDataFrame.from_features(f_filt, crs=crs, columns=columns)
97 
98     return gdf
99 
100 
101 def to_file(df, filename, driver="ESRI Shapefile", schema=None, **kwargs):
102     """
103     Write this GeoDataFrame to an OGR data source
104 
105     A dictionary of supported OGR providers is available via:
106     >>> import fiona
107     >>> fiona.supported_drivers
108 
109     Parameters
110     ----------
111     df : GeoDataFrame to be written
112     filename : string
113         File path or file handle to write to.
114     driver : string, default 'ESRI Shapefile'
115         The OGR format driver used to write the vector file.
116     schema : dict, default None
117         If specified, the schema dictionary is passed to Fiona to
118         better control how the file is written. If None, GeoPandas
119         will determine the schema based on each column's dtype
120 
121     The *kwargs* are passed to fiona.open and can be used to write
122     to multi-layer data, store data within archives (zip files), etc.
123     """
124     if schema is None:
125         schema = infer_schema(df)
126     filename = os.path.abspath(os.path.expanduser(filename))
127     with fiona_env():
128         with fiona.open(
129             filename, "w", driver=driver, crs=df.crs, schema=schema, **kwargs
130         ) as colxn:
131             colxn.writerecords(df.iterfeatures())
132 
133 
134 def infer_schema(df):
135     try:
136         from collections import OrderedDict
137     except ImportError:
138         from ordereddict import OrderedDict
139 
140     def convert_type(column, in_type):
141         if in_type == object:
142             return "str"
143         if in_type.name.startswith("datetime64"):
144             # numpy datetime type regardless of frequency
145             return "datetime"
146         out_type = type(np.zeros(1, in_type).item()).__name__
147         if out_type == "long":
148             out_type = "int"
149         if not _FIONA18 and out_type == "bool":
150             raise ValueError(
151                 'column "{}" is boolean type, '.format(column)
152                 + "which is unsupported in file writing with fiona "
153                 "< 1.8. Consider casting the column to int type."
154             )
155         return out_type
156 
157     properties = OrderedDict(
158         [
159             (col, convert_type(col, _type))
160             for col, _type in zip(df.columns, df.dtypes)
161             if col != df._geometry_column_name
162         ]
163     )
164 
165     if df.empty:
166         raise ValueError("Cannot write empty DataFrame to file.")
167 
168     # Since https://github.com/Toblerity/Fiona/issues/446 resolution,
169     # Fiona allows a list of geometry types
170     geom_types = _geometry_types(df)
171 
172     schema = {"geometry": geom_types, "properties": properties}
173 
174     return schema
175 
176 
177 def _geometry_types(df):
178     """
179     Determine the geometry types in the GeoDataFrame for the schema.
180     """
181     if _FIONA18:
182         # Starting from Fiona 1.8, schema submitted to fiona to write a gdf
183         # can have mixed geometries:
184         # - 3D and 2D shapes can coexist in inferred schema
185         # - Shape and MultiShape types can (and must) coexist in inferred
186         #   schema
187         geom_types_2D = df[~df.geometry.has_z].geometry.geom_type.unique()
188         geom_types_2D = [gtype for gtype in geom_types_2D if gtype is not None]
189         geom_types_3D = df[df.geometry.has_z].geometry.geom_type.unique()
190         geom_types_3D = ["3D " + gtype for gtype in geom_types_3D if gtype is not None]
191         geom_types = geom_types_3D + geom_types_2D
192 
193     else:
194         # Before Fiona 1.8, schema submitted to write a gdf should have
195         # one single geometry type whenever possible:
196         # - 3D and 2D shapes cannot coexist in inferred schema
197         # - Shape and MultiShape can not coexist in inferred schema
198         geom_types = _geometry_types_back_compat(df)
199 
200     if len(geom_types) == 0:
201         # Default geometry type supported by Fiona
202         # (Since https://github.com/Toblerity/Fiona/issues/446 resolution)
203         return "Unknown"
204 
205     if len(geom_types) == 1:
206         geom_types = geom_types[0]
207 
208     return geom_types
209 
210 
211 def _geometry_types_back_compat(df):
212     """
213     for backward compatibility with Fiona<1.8 only
214     """
215     unique_geom_types = df.geometry.geom_type.unique()
216     unique_geom_types = [gtype for gtype in unique_geom_types if gtype is not None]
217 
218     # merge single and Multi types (eg Polygon and MultiPolygon)
219     unique_geom_types = [
220         gtype
221         for gtype in unique_geom_types
222         if not gtype.startswith("Multi") or gtype[5:] not in unique_geom_types
223     ]
224 
225     if df.geometry.has_z.any():
226         # declare all geometries as 3D geometries
227         unique_geom_types = ["3D " + type for type in unique_geom_types]
228     # by default, all geometries are 2D geometries
229 
230     return unique_geom_types
231
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/geopandas/io/file.py b/geopandas/io/file.py
--- a/geopandas/io/file.py
+++ b/geopandas/io/file.py
@@ -1,5 +1,4 @@
 from distutils.version import LooseVersion
-import os
 
 import numpy as np
 import six
@@ -120,10 +119,10 @@
 
     The *kwargs* are passed to fiona.open and can be used to write
     to multi-layer data, store data within archives (zip files), etc.
+    The path may specify a fiona VSI scheme.
     """
     if schema is None:
         schema = infer_schema(df)
-    filename = os.path.abspath(os.path.expanduser(filename))
     with fiona_env():
         with fiona.open(
             filename, "w", driver=driver, crs=df.crs, schema=schema, **kwargs

{"golden_diff": "diff --git a/geopandas/io/file.py b/geopandas/io/file.py\n--- a/geopandas/io/file.py\n+++ b/geopandas/io/file.py\n@@ -1,5 +1,4 @@\n from distutils.version import LooseVersion\n-import os\n \n import numpy as np\n import six\n@@ -120,10 +119,10 @@\n \n     The *kwargs* are passed to fiona.open and can be used to write\n     to multi-layer data, store data within archives (zip files), etc.\n+    The path may specify a fiona VSI scheme.\n     \"\"\"\n     if schema is None:\n         schema = infer_schema(df)\n-    filename = os.path.abspath(os.path.expanduser(filename))\n     with fiona_env():\n         with fiona.open(\n             filename, \"w\", driver=driver, crs=df.crs, schema=schema, **kwargs\n", "issue": "Writing to file doesn't respect fiona vsi schemes\nFiona supports writing to different OGR virtual filesystem schemes via the file path, e.g., `zip://`, `s3://`, etc. However, these schemes get overwritten in `gpd.to_file()`, which attempts to take the path and construct an absolute path:\r\nhttps://github.com/geopandas/geopandas/blob/29add0a735b00dc20c79e0fccc8e6a775c4997b0/geopandas/io/file.py#L127\r\nso you get bad paths like `/home/username/s3:/bucket/object`.\r\n\r\nIs this absolute file path important? Is there a reason to not allow VSI-based paths in `gpd.to_file()`? Can we check for a url-like scheme before trying to abs-path it? If so, I'd be happy to submit a PR.\r\n\r\n(N.b., the corresponding `gpd.read_file()` handles these URLs correctly).\n", "before_files": [{"content": "from distutils.version import LooseVersion\nimport os\n\nimport numpy as np\nimport six\n\nimport fiona\n\nfrom geopandas import GeoDataFrame, GeoSeries\n\ntry:\n    from fiona import Env as fiona_env\nexcept ImportError:\n    from fiona import drivers as fiona_env\n\n\n_FIONA18 = LooseVersion(fiona.__version__) >= LooseVersion(\"1.8\")\n\n\n# Adapted from pandas.io.common\nif six.PY3:\n    from urllib.request import urlopen as _urlopen\n    from urllib.parse import urlparse as parse_url\n    from urllib.parse import uses_relative, uses_netloc, uses_params\nelse:\n    from urllib2 import urlopen as _urlopen\n    from urlparse import urlparse as parse_url\n    from urlparse import uses_relative, uses_netloc, uses_params\n\n_VALID_URLS = set(uses_relative + uses_netloc + uses_params)\n_VALID_URLS.discard(\"\")\n\n\ndef _is_url(url):\n    \"\"\"Check to see if *url* has a valid protocol.\"\"\"\n    try:\n        return parse_url(url).scheme in _VALID_URLS\n    except Exception:\n        return False\n\n\ndef read_file(filename, bbox=None, **kwargs):\n    \"\"\"\n    Returns a GeoDataFrame from a file or URL.\n\n    Parameters\n    ----------\n    filename: str\n        Either the absolute or relative path to the file or URL to\n        be opened.\n    bbox : tuple | GeoDataFrame or GeoSeries, default None\n        Filter features by given bounding box, GeoSeries, or GeoDataFrame.\n        CRS mis-matches are resolved if given a GeoSeries or GeoDataFrame.\n    **kwargs:\n        Keyword args to be passed to the `open` or `BytesCollection` method\n        in the fiona library when opening the file. For more information on\n        possible keywords, type:\n        ``import fiona; help(fiona.open)``\n\n    Examples\n    --------\n    >>> df = geopandas.read_file(\"nybb.shp\")\n\n    Returns\n    -------\n    geodataframe : GeoDataFrame\n    \"\"\"\n    if _is_url(filename):\n        req = _urlopen(filename)\n        path_or_bytes = req.read()\n        reader = fiona.BytesCollection\n    else:\n        path_or_bytes = filename\n        reader = fiona.open\n\n    with fiona_env():\n        with reader(path_or_bytes, **kwargs) as features:\n\n            # In a future Fiona release the crs attribute of features will\n            # no longer be a dict. The following code will be both forward\n            # and backward compatible.\n            if hasattr(features.crs, \"to_dict\"):\n                crs = features.crs.to_dict()\n            else:\n                crs = features.crs\n\n            if bbox is not None:\n                if isinstance(bbox, GeoDataFrame) or isinstance(bbox, GeoSeries):\n                    bbox = tuple(bbox.to_crs(crs).total_bounds)\n                assert len(bbox) == 4\n                f_filt = features.filter(bbox=bbox)\n            else:\n                f_filt = features\n\n            columns = list(features.meta[\"schema\"][\"properties\"]) + [\"geometry\"]\n            gdf = GeoDataFrame.from_features(f_filt, crs=crs, columns=columns)\n\n    return gdf\n\n\ndef to_file(df, filename, driver=\"ESRI Shapefile\", schema=None, **kwargs):\n    \"\"\"\n    Write this GeoDataFrame to an OGR data source\n\n    A dictionary of supported OGR providers is available via:\n    >>> import fiona\n    >>> fiona.supported_drivers\n\n    Parameters\n    ----------\n    df : GeoDataFrame to be written\n    filename : string\n        File path or file handle to write to.\n    driver : string, default 'ESRI Shapefile'\n        The OGR format driver used to write the vector file.\n    schema : dict, default None\n        If specified, the schema dictionary is passed to Fiona to\n        better control how the file is written. If None, GeoPandas\n        will determine the schema based on each column's dtype\n\n    The *kwargs* are passed to fiona.open and can be used to write\n    to multi-layer data, store data within archives (zip files), etc.\n    \"\"\"\n    if schema is None:\n        schema = infer_schema(df)\n    filename = os.path.abspath(os.path.expanduser(filename))\n    with fiona_env():\n        with fiona.open(\n            filename, \"w\", driver=driver, crs=df.crs, schema=schema, **kwargs\n        ) as colxn:\n            colxn.writerecords(df.iterfeatures())\n\n\ndef infer_schema(df):\n    try:\n        from collections import OrderedDict\n    except ImportError:\n        from ordereddict import OrderedDict\n\n    def convert_type(column, in_type):\n        if in_type == object:\n            return \"str\"\n        if in_type.name.startswith(\"datetime64\"):\n            # numpy datetime type regardless of frequency\n            return \"datetime\"\n        out_type = type(np.zeros(1, in_type).item()).__name__\n        if out_type == \"long\":\n            out_type = \"int\"\n        if not _FIONA18 and out_type == \"bool\":\n            raise ValueError(\n                'column \"{}\" is boolean type, '.format(column)\n                + \"which is unsupported in file writing with fiona \"\n                \"< 1.8. Consider casting the column to int type.\"\n            )\n        return out_type\n\n    properties = OrderedDict(\n        [\n            (col, convert_type(col, _type))\n            for col, _type in zip(df.columns, df.dtypes)\n            if col != df._geometry_column_name\n        ]\n    )\n\n    if df.empty:\n        raise ValueError(\"Cannot write empty DataFrame to file.\")\n\n    # Since https://github.com/Toblerity/Fiona/issues/446 resolution,\n    # Fiona allows a list of geometry types\n    geom_types = _geometry_types(df)\n\n    schema = {\"geometry\": geom_types, \"properties\": properties}\n\n    return schema\n\n\ndef _geometry_types(df):\n    \"\"\"\n    Determine the geometry types in the GeoDataFrame for the schema.\n    \"\"\"\n    if _FIONA18:\n        # Starting from Fiona 1.8, schema submitted to fiona to write a gdf\n        # can have mixed geometries:\n        # - 3D and 2D shapes can coexist in inferred schema\n        # - Shape and MultiShape types can (and must) coexist in inferred\n        #   schema\n        geom_types_2D = df[~df.geometry.has_z].geometry.geom_type.unique()\n        geom_types_2D = [gtype for gtype in geom_types_2D if gtype is not None]\n        geom_types_3D = df[df.geometry.has_z].geometry.geom_type.unique()\n        geom_types_3D = [\"3D \" + gtype for gtype in geom_types_3D if gtype is not None]\n        geom_types = geom_types_3D + geom_types_2D\n\n    else:\n        # Before Fiona 1.8, schema submitted to write a gdf should have\n        # one single geometry type whenever possible:\n        # - 3D and 2D shapes cannot coexist in inferred schema\n        # - Shape and MultiShape can not coexist in inferred schema\n        geom_types = _geometry_types_back_compat(df)\n\n    if len(geom_types) == 0:\n        # Default geometry type supported by Fiona\n        # (Since https://github.com/Toblerity/Fiona/issues/446 resolution)\n        return \"Unknown\"\n\n    if len(geom_types) == 1:\n        geom_types = geom_types[0]\n\n    return geom_types\n\n\ndef _geometry_types_back_compat(df):\n    \"\"\"\n    for backward compatibility with Fiona<1.8 only\n    \"\"\"\n    unique_geom_types = df.geometry.geom_type.unique()\n    unique_geom_types = [gtype for gtype in unique_geom_types if gtype is not None]\n\n    # merge single and Multi types (eg Polygon and MultiPolygon)\n    unique_geom_types = [\n        gtype\n        for gtype in unique_geom_types\n        if not gtype.startswith(\"Multi\") or gtype[5:] not in unique_geom_types\n    ]\n\n    if df.geometry.has_z.any():\n        # declare all geometries as 3D geometries\n        unique_geom_types = [\"3D \" + type for type in unique_geom_types]\n    # by default, all geometries are 2D geometries\n\n    return unique_geom_types\n", "path": "geopandas/io/file.py"}], "after_files": [{"content": "from distutils.version import LooseVersion\n\nimport numpy as np\nimport six\n\nimport fiona\n\nfrom geopandas import GeoDataFrame, GeoSeries\n\ntry:\n    from fiona import Env as fiona_env\nexcept ImportError:\n    from fiona import drivers as fiona_env\n\n\n_FIONA18 = LooseVersion(fiona.__version__) >= LooseVersion(\"1.8\")\n\n\n# Adapted from pandas.io.common\nif six.PY3:\n    from urllib.request import urlopen as _urlopen\n    from urllib.parse import urlparse as parse_url\n    from urllib.parse import uses_relative, uses_netloc, uses_params\nelse:\n    from urllib2 import urlopen as _urlopen\n    from urlparse import urlparse as parse_url\n    from urlparse import uses_relative, uses_netloc, uses_params\n\n_VALID_URLS = set(uses_relative + uses_netloc + uses_params)\n_VALID_URLS.discard(\"\")\n\n\ndef _is_url(url):\n    \"\"\"Check to see if *url* has a valid protocol.\"\"\"\n    try:\n        return parse_url(url).scheme in _VALID_URLS\n    except Exception:\n        return False\n\n\ndef read_file(filename, bbox=None, **kwargs):\n    \"\"\"\n    Returns a GeoDataFrame from a file or URL.\n\n    Parameters\n    ----------\n    filename: str\n        Either the absolute or relative path to the file or URL to\n        be opened.\n    bbox : tuple | GeoDataFrame or GeoSeries, default None\n        Filter features by given bounding box, GeoSeries, or GeoDataFrame.\n        CRS mis-matches are resolved if given a GeoSeries or GeoDataFrame.\n    **kwargs:\n        Keyword args to be passed to the `open` or `BytesCollection` method\n        in the fiona library when opening the file. For more information on\n        possible keywords, type:\n        ``import fiona; help(fiona.open)``\n\n    Examples\n    --------\n    >>> df = geopandas.read_file(\"nybb.shp\")\n\n    Returns\n    -------\n    geodataframe : GeoDataFrame\n    \"\"\"\n    if _is_url(filename):\n        req = _urlopen(filename)\n        path_or_bytes = req.read()\n        reader = fiona.BytesCollection\n    else:\n        path_or_bytes = filename\n        reader = fiona.open\n\n    with fiona_env():\n        with reader(path_or_bytes, **kwargs) as features:\n\n            # In a future Fiona release the crs attribute of features will\n            # no longer be a dict. The following code will be both forward\n            # and backward compatible.\n            if hasattr(features.crs, \"to_dict\"):\n                crs = features.crs.to_dict()\n            else:\n                crs = features.crs\n\n            if bbox is not None:\n                if isinstance(bbox, GeoDataFrame) or isinstance(bbox, GeoSeries):\n                    bbox = tuple(bbox.to_crs(crs).total_bounds)\n                assert len(bbox) == 4\n                f_filt = features.filter(bbox=bbox)\n            else:\n                f_filt = features\n\n            columns = list(features.meta[\"schema\"][\"properties\"]) + [\"geometry\"]\n            gdf = GeoDataFrame.from_features(f_filt, crs=crs, columns=columns)\n\n    return gdf\n\n\ndef to_file(df, filename, driver=\"ESRI Shapefile\", schema=None, **kwargs):\n    \"\"\"\n    Write this GeoDataFrame to an OGR data source\n\n    A dictionary of supported OGR providers is available via:\n    >>> import fiona\n    >>> fiona.supported_drivers\n\n    Parameters\n    ----------\n    df : GeoDataFrame to be written\n    filename : string\n        File path or file handle to write to.\n    driver : string, default 'ESRI Shapefile'\n        The OGR format driver used to write the vector file.\n    schema : dict, default None\n        If specified, the schema dictionary is passed to Fiona to\n        better control how the file is written. If None, GeoPandas\n        will determine the schema based on each column's dtype\n\n    The *kwargs* are passed to fiona.open and can be used to write\n    to multi-layer data, store data within archives (zip files), etc.\n    The path may specify a fiona VSI scheme.\n    \"\"\"\n    if schema is None:\n        schema = infer_schema(df)\n    with fiona_env():\n        with fiona.open(\n            filename, \"w\", driver=driver, crs=df.crs, schema=schema, **kwargs\n        ) as colxn:\n            colxn.writerecords(df.iterfeatures())\n\n\ndef infer_schema(df):\n    try:\n        from collections import OrderedDict\n    except ImportError:\n        from ordereddict import OrderedDict\n\n    def convert_type(column, in_type):\n        if in_type == object:\n            return \"str\"\n        if in_type.name.startswith(\"datetime64\"):\n            # numpy datetime type regardless of frequency\n            return \"datetime\"\n        out_type = type(np.zeros(1, in_type).item()).__name__\n        if out_type == \"long\":\n            out_type = \"int\"\n        if not _FIONA18 and out_type == \"bool\":\n            raise ValueError(\n                'column \"{}\" is boolean type, '.format(column)\n                + \"which is unsupported in file writing with fiona \"\n                \"< 1.8. Consider casting the column to int type.\"\n            )\n        return out_type\n\n    properties = OrderedDict(\n        [\n            (col, convert_type(col, _type))\n            for col, _type in zip(df.columns, df.dtypes)\n            if col != df._geometry_column_name\n        ]\n    )\n\n    if df.empty:\n        raise ValueError(\"Cannot write empty DataFrame to file.\")\n\n    # Since https://github.com/Toblerity/Fiona/issues/446 resolution,\n    # Fiona allows a list of geometry types\n    geom_types = _geometry_types(df)\n\n    schema = {\"geometry\": geom_types, \"properties\": properties}\n\n    return schema\n\n\ndef _geometry_types(df):\n    \"\"\"\n    Determine the geometry types in the GeoDataFrame for the schema.\n    \"\"\"\n    if _FIONA18:\n        # Starting from Fiona 1.8, schema submitted to fiona to write a gdf\n        # can have mixed geometries:\n        # - 3D and 2D shapes can coexist in inferred schema\n        # - Shape and MultiShape types can (and must) coexist in inferred\n        #   schema\n        geom_types_2D = df[~df.geometry.has_z].geometry.geom_type.unique()\n        geom_types_2D = [gtype for gtype in geom_types_2D if gtype is not None]\n        geom_types_3D = df[df.geometry.has_z].geometry.geom_type.unique()\n        geom_types_3D = [\"3D \" + gtype for gtype in geom_types_3D if gtype is not None]\n        geom_types = geom_types_3D + geom_types_2D\n\n    else:\n        # Before Fiona 1.8, schema submitted to write a gdf should have\n        # one single geometry type whenever possible:\n        # - 3D and 2D shapes cannot coexist in inferred schema\n        # - Shape and MultiShape can not coexist in inferred schema\n        geom_types = _geometry_types_back_compat(df)\n\n    if len(geom_types) == 0:\n        # Default geometry type supported by Fiona\n        # (Since https://github.com/Toblerity/Fiona/issues/446 resolution)\n        return \"Unknown\"\n\n    if len(geom_types) == 1:\n        geom_types = geom_types[0]\n\n    return geom_types\n\n\ndef _geometry_types_back_compat(df):\n    \"\"\"\n    for backward compatibility with Fiona<1.8 only\n    \"\"\"\n    unique_geom_types = df.geometry.geom_type.unique()\n    unique_geom_types = [gtype for gtype in unique_geom_types if gtype is not None]\n\n    # merge single and Multi types (eg Polygon and MultiPolygon)\n    unique_geom_types = [\n        gtype\n        for gtype in unique_geom_types\n        if not gtype.startswith(\"Multi\") or gtype[5:] not in unique_geom_types\n    ]\n\n    if df.geometry.has_z.any():\n        # declare all geometries as 3D geometries\n        unique_geom_types = [\"3D \" + type for type in unique_geom_types]\n    # by default, all geometries are 2D geometries\n\n    return unique_geom_types\n", "path": "geopandas/io/file.py"}]}