diff --git a/.gitignore b/.gitignore index 1de3e5e..2e7b2fe 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ # Distribution / packaging .Python env/ +venv*/ build/ dist/ eggs/ diff --git a/geopandas/io/file.py b/geopandas/io/file.py index 4298244..4efe2d8 100644 --- a/geopandas/io/file.py +++ b/geopandas/io/file.py @@ -149,27 +149,66 @@ def infer_schema(df): if df.empty: raise ValueError("Cannot write empty DataFrame to file.") - geom_type = _common_geom_type(df) + # Since https://github.com/Toblerity/Fiona/issues/446 resolution, + # Fiona allows a list of geometry types + geom_types = _geometry_types(df) - schema = {'geometry': geom_type, 'properties': properties} + schema = {'geometry': geom_types, 'properties': properties} return schema -def _common_geom_type(df): - # Need to check geom_types before we write to file... - # Some (most?) providers expect a single geometry type: - # Point, LineString, or Polygon - geom_types = df.geometry.geom_type.unique() +def _geometry_types(df): + """ + Determine the geometry types in the GeoDataFrame for the schema. + """ + if _FIONA18: + # Starting from Fiona 1.8, schema submitted to fiona to write a gdf + # can have mixed geometries: + # - 3D and 2D shapes can coexist in inferred schema + # - Shape and MultiShape types can (and must) coexist in inferred + # schema + geom_types_2D = df[~df.geometry.has_z].geometry.geom_type.unique() + geom_types_2D = [gtype for gtype in geom_types_2D if gtype is not None] + geom_types_3D = df[df.geometry.has_z].geometry.geom_type.unique() + geom_types_3D = ["3D " + gtype for gtype in geom_types_3D + if gtype is not None] + geom_types = geom_types_3D + geom_types_2D - from os.path import commonprefix - # use reversed geom types and commonprefix to find the common suffix, - # then reverse the result to get back to a geom type - geom_type = commonprefix([g[::-1] for g in geom_types if g])[::-1] - if not geom_type: + else: + # Before Fiona 1.8, schema submitted to write a gdf should have + # one single geometry type whenever possible: + # - 3D and 2D shapes cannot coexist in inferred schema + # - Shape and MultiShape can not coexist in inferred schema + geom_types = _geometry_types_back_compat(df) + + if len(geom_types) == 0: + # Default geometry type supported by Fiona + # (Since https://github.com/Toblerity/Fiona/issues/446 resolution) return 'Unknown' - if df.geometry.has_z.any(): - geom_type = "3D " + geom_type + if len(geom_types) == 1: + geom_types = geom_types[0] - return geom_type + return geom_types + + +def _geometry_types_back_compat(df): + """ + for backward compatibility with Fiona<1.8 only + """ + unique_geom_types = df.geometry.geom_type.unique() + unique_geom_types = [ + gtype for gtype in unique_geom_types if gtype is not None] + + # merge single and Multi types (eg Polygon and MultiPolygon) + unique_geom_types = [ + gtype for gtype in unique_geom_types + if not gtype.startswith('Multi') or gtype[5:] not in unique_geom_types] + + if df.geometry.has_z.any(): + # declare all geometries as 3D geometries + unique_geom_types = ["3D " + type for type in unique_geom_types] + # by default, all geometries are 2D geometries + + return unique_geom_types diff --git a/geopandas/io/tests/test_file.py b/geopandas/io/tests/test_file.py index 1bf80c0..508e937 100644 --- a/geopandas/io/tests/test_file.py +++ b/geopandas/io/tests/test_file.py @@ -5,8 +5,9 @@ from distutils.version import LooseVersion import os import numpy as np -from shapely.geometry import Point, Polygon, box + import fiona +from shapely.geometry import Point, Polygon, box import geopandas from geopandas import GeoDataFrame, read_file @@ -96,26 +97,32 @@ def test_to_file_bool(tmpdir, driver, ext): assert_geodataframe_equal(result, df, check_column_type=False) -def test_to_file_with_point_z(tmpdir): +@pytest.mark.parametrize( + 'ext, driver', [('shp', 'ESRI Shapefile'), ('geojson', 'GeoJSON')]) +def test_to_file_with_point_z(tmpdir, ext, driver): """Test that 3D geometries are retained in writes (GH #612).""" - tempfilename = os.path.join(str(tmpdir), 'test_3Dpoint.shp') + tempfilename = os.path.join(str(tmpdir), 'test_3Dpoint.' + ext) point3d = Point(0, 0, 500) point2d = Point(1, 1) - df = GeoDataFrame({'a': [1, 2]}, geometry=[point3d, point2d], crs={}) - df.to_file(tempfilename) + df = GeoDataFrame({'a': [1, 2]}, geometry=[point3d, point2d], + crs={'init': 'epsg:4326'}) + df.to_file(tempfilename, driver=driver) df_read = GeoDataFrame.from_file(tempfilename) assert_geoseries_equal(df.geometry, df_read.geometry) -def test_to_file_with_poly_z(tmpdir): +@pytest.mark.parametrize( + 'ext, driver', [('shp', 'ESRI Shapefile'), ('geojson', 'GeoJSON')]) +def test_to_file_with_poly_z(tmpdir, ext, driver): """Test that 3D geometries are retained in writes (GH #612).""" - tempfilename = os.path.join(str(tmpdir), 'test_3Dpoly.shp') + tempfilename = os.path.join(str(tmpdir), 'test_3Dpoly.' + ext) poly3d = Polygon([[0, 0, 5], [0, 1, 5], [1, 1, 5], [1, 0, 5]]) poly2d = Polygon([[0, 0], [0, 1], [1, 1], [1, 0]]) - df = GeoDataFrame({'a': [1, 2]}, geometry=[poly3d, poly2d], crs={}) - df.to_file(tempfilename) + df = GeoDataFrame({'a': [1, 2]}, geometry=[poly3d, poly2d], + crs={'init': 'epsg:4326'}) + df.to_file(tempfilename, driver=driver) df_read = GeoDataFrame.from_file(tempfilename) assert_geoseries_equal(df.geometry, df_read.geometry) @@ -132,24 +139,6 @@ def test_to_file_types(tmpdir, df_points): df.to_file(tempfilename) -def test_to_file_mixed_types(tmpdir): - """ Test that mixed geometry types raise error when writing to file """ - tempfilename = os.path.join(str(tmpdir), 'test.shp') - s = GeoDataFrame({'geometry': [Point(0, 0), - Polygon([(0, 0), (1, 0), (1, 1)])]}) - # Exception type is different for different `fiona` versions - with pytest.raises((ValueError, RuntimeError)): - s.to_file(tempfilename) - - -def test_to_file_geojson_mixed_types(tmpdir): - """ Test that mixed geometry types can be saved as GeoJSON (GH #827) """ - tempfilename = os.path.join(str(tmpdir), 'test.geojson') - s = GeoDataFrame({'geometry': [Point(0, 0), - Polygon([(0, 0), (1, 0), (1, 1)])]}) - s.to_file(tempfilename, driver='GeoJSON') - - def test_to_file_empty(tmpdir): input_empty_df = GeoDataFrame() tempfilename = os.path.join(str(tmpdir), 'test.shp') diff --git a/geopandas/io/tests/test_file_geom_types_drivers.py b/geopandas/io/tests/test_file_geom_types_drivers.py new file mode 100644 index 0000000..fff5d1b --- /dev/null +++ b/geopandas/io/tests/test_file_geom_types_drivers.py @@ -0,0 +1,337 @@ +import os +import shutil +import sys +import tempfile +from enum import Enum + +from shapely.geometry import Point, Polygon, MultiPolygon, MultiPoint, \ + LineString, MultiLineString + +import geopandas +from geopandas import GeoDataFrame +from geopandas.io.file import _FIONA18 + +import pytest +from geopandas.testing import assert_geodataframe_equal + + +# Credit: Polygons below come from Montreal city Open Data portal +# http://donnees.ville.montreal.qc.ca/dataset/unites-evaluation-fonciere +city_hall_boundaries = Polygon(( + (-73.5541107525234, 45.5091983609661), + (-73.5546126200639, 45.5086813829106), + (-73.5540185061397, 45.5084409343852), + (-73.5539986525799, 45.5084323044531), + (-73.5535801792994, 45.5089539203786), + (-73.5541107525234, 45.5091983609661) +)) +vauquelin_place = Polygon(( + (-73.5542465586147, 45.5081555487952), + (-73.5540185061397, 45.5084409343852), + (-73.5546126200639, 45.5086813829106), + (-73.5548825850032, 45.5084033554357), + (-73.5542465586147, 45.5081555487952) +)) + +city_hall_walls = [ + LineString(( + (-73.5541107525234, 45.5091983609661), + (-73.5546126200639, 45.5086813829106), + (-73.5540185061397, 45.5084409343852) + )), + LineString(( + (-73.5539986525799, 45.5084323044531), + (-73.5535801792994, 45.5089539203786), + (-73.5541107525234, 45.5091983609661) + )) +] + +city_hall_entrance = Point(-73.553785, 45.508722) +city_hall_balcony = Point(-73.554138, 45.509080) +city_hall_council_chamber = Point(-73.554246, 45.508931) + +point_3D = Point(-73.553785, 45.508722, 300) + + +# ***************************************** +# TEST TOOLING + +class _Fiona(Enum): + below_1_8 = 'fiona_below_1_8' + above_1_8 = 'fiona_above_1_8' + + +class _ExpectedError: + def __init__(self, error_type, error_message_match): + self.type = error_type + self.match = error_message_match + + +class _ExpectedErrorBuilder: + def __init__(self, composite_key): + self.composite_key = composite_key + + def to_raise(self, error_type, error_match): + _expected_exceptions[self.composite_key] = _ExpectedError(error_type, + error_match) + + +def _expect_writing(gdf, ogr_driver, fiona_version): + return _ExpectedErrorBuilder( + _composite_key(gdf, ogr_driver, fiona_version) + ) + + +def _composite_key(gdf, ogr_driver, fiona_version): + return frozenset([id(gdf), ogr_driver, fiona_version.value]) + + +def _expected_error_on(gdf, ogr_driver, is_fiona_above_1_8): + if is_fiona_above_1_8: + composite_key = _composite_key(gdf, ogr_driver, _Fiona.above_1_8) + else: + composite_key = _composite_key(gdf, ogr_driver, _Fiona.below_1_8) + return _expected_exceptions.get(composite_key, None) + + +# ***************************************** +# TEST CASES +_geodataframes_to_write = [] +_expected_exceptions = {} + +# ------------------ +# gdf with Points +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[city_hall_entrance, city_hall_balcony] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with MultiPoints +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[ + MultiPoint([ + city_hall_balcony, + city_hall_council_chamber]), + MultiPoint([ + city_hall_entrance, + city_hall_balcony, + city_hall_council_chamber] + )] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with Points and MultiPoints +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[ + MultiPoint([city_hall_entrance, city_hall_balcony]), + city_hall_balcony + ] +) +_geodataframes_to_write.append(gdf) +# 'ESRI Shapefile' driver supports writing LineString/MultiLinestring and +# Polygon/MultiPolygon but does not mention Point/MultiPoint +# see https://www.gdal.org/drv_shapefile.html +for driver in ('ESRI Shapefile', 'GPKG'): + _expect_writing(gdf, driver, _Fiona.below_1_8).to_raise( + ValueError, + "Record's geometry type does not match collection schema's geometry " + "type: 'MultiPoint' != 'Point'" + ) +_expect_writing(gdf, 'ESRI Shapefile', _Fiona.above_1_8).to_raise( + RuntimeError, + "Failed to write record" +) + +# ------------------ +# gdf with LineStrings +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=city_hall_walls +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with MultiLineStrings +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[ + MultiLineString(city_hall_walls), + MultiLineString(city_hall_walls) + ] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with LineStrings and MultiLineStrings +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[MultiLineString(city_hall_walls), city_hall_walls[0]] +) +_geodataframes_to_write.append(gdf) +_expect_writing(gdf, 'GPKG', _Fiona.below_1_8).to_raise( + ValueError, + "Record's geometry type does not match collection schema's geometry " + "type: 'MultiLineString' != 'LineString'" +) + +# ------------------ +# gdf with Polygons +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[city_hall_boundaries, vauquelin_place] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with MultiPolygon +gdf = GeoDataFrame( + {'a': [1]}, + crs={'init': 'epsg:4326'}, + geometry=[MultiPolygon((city_hall_boundaries, vauquelin_place))] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with Polygon and MultiPolygon +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[ + MultiPolygon((city_hall_boundaries, vauquelin_place)), + city_hall_boundaries + ] +) +_geodataframes_to_write.append(gdf) +_expect_writing(gdf, 'GPKG', _Fiona.below_1_8).to_raise( + ValueError, + "Record's geometry type does not match collection schema's geometry " + "type: 'MultiPolygon' != 'Polygon'" +) + +# ------------------ +# gdf with null geometry and Point +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[None, city_hall_entrance] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with null geometry and 3D Point +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[None, point_3D] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with null geometries only +gdf = GeoDataFrame( + {'a': [1, 2]}, + crs={'init': 'epsg:4326'}, + geometry=[None, None] +) +_geodataframes_to_write.append(gdf) + +# ------------------ +# gdf with all shape types mixed together +gdf = GeoDataFrame( + {'a': [1, 2, 3, 4, 5, 6]}, + crs={'init': 'epsg:4326'}, + geometry=[ + MultiPolygon((city_hall_boundaries, vauquelin_place)), + city_hall_entrance, + MultiLineString(city_hall_walls), + city_hall_walls[0], + MultiPoint([city_hall_entrance, city_hall_balcony]), + city_hall_balcony + ] +) +_geodataframes_to_write.append(gdf) +# Not supported by 'ESRI Shapefile' driver +for driver in ('ESRI Shapefile', 'GPKG'): + _expect_writing(gdf, driver, _Fiona.below_1_8).to_raise( + AttributeError, + "'list' object has no attribute 'lstrip'" + ) +_expect_writing(gdf, 'ESRI Shapefile', _Fiona.above_1_8).to_raise( + RuntimeError, + "Failed to write record" +) + +# ------------------ +# gdf with all 2D shape types and 3D Point mixed together +gdf = GeoDataFrame( + {'a': [1, 2, 3, 4, 5, 6, 7]}, + crs={'init': 'epsg:4326'}, + geometry=[ + MultiPolygon((city_hall_boundaries, vauquelin_place)), + city_hall_entrance, + MultiLineString(city_hall_walls), + city_hall_walls[0], + MultiPoint([city_hall_entrance, city_hall_balcony]), + city_hall_balcony, + point_3D + ] +) +_geodataframes_to_write.append(gdf) +# Not supported by 'ESRI Shapefile' driver +for driver in ('ESRI Shapefile', 'GPKG'): + _expect_writing(gdf, driver, _Fiona.below_1_8).to_raise( + AttributeError, + "'list' object has no attribute 'lstrip'" + ) +_expect_writing(gdf, 'ESRI Shapefile', _Fiona.above_1_8).to_raise( + RuntimeError, + "Failed to write record" +) + + +@pytest.fixture(params=_geodataframes_to_write) +def geodataframe(request): + return request.param + + +@pytest.fixture(params=[ + 'GeoJSON', 'ESRI Shapefile', + pytest.param('GPKG', marks=pytest.mark.skipif( + (sys.version_info < (3, 0)) and sys.platform.startswith('win'), + reason="GPKG tests failing on AppVeyor for Python 2.7")) + ]) +def ogr_driver(request): + return request.param + + +def test_to_file_roundtrip(tmpdir, geodataframe, ogr_driver): + output_file = os.path.join(str(tmpdir), 'output_file') + + expected_error = _expected_error_on(geodataframe, ogr_driver, _FIONA18) + if expected_error: + with pytest.raises(expected_error.type, match=expected_error.match): + geodataframe.to_file(output_file, driver=ogr_driver) + else: + geodataframe.to_file(output_file, driver=ogr_driver) + + reloaded = geopandas.read_file(output_file) + + check_column_type = 'equiv' + if sys.version_info[0] < 3: + # do not check column types in python 2 (mixed string/unicode) + check_column_type = False + + assert_geodataframe_equal(geodataframe, reloaded, + check_column_type=check_column_type) diff --git a/geopandas/io/tests/test_infer_schema.py b/geopandas/io/tests/test_infer_schema.py new file mode 100644 index 0000000..f340e03 --- /dev/null +++ b/geopandas/io/tests/test_infer_schema.py @@ -0,0 +1,350 @@ +from collections import OrderedDict + +from shapely.geometry import Point, Polygon, MultiPolygon, MultiPoint, \ + LineString, MultiLineString + +from geopandas import GeoDataFrame +from geopandas.io.file import infer_schema, _FIONA18 + + +# Credit: Polygons below come from Montreal city Open Data portal +# http://donnees.ville.montreal.qc.ca/dataset/unites-evaluation-fonciere +city_hall_boundaries = Polygon(( + (-73.5541107525234, 45.5091983609661), + (-73.5546126200639, 45.5086813829106), + (-73.5540185061397, 45.5084409343852), + (-73.5539986525799, 45.5084323044531), + (-73.5535801792994, 45.5089539203786), + (-73.5541107525234, 45.5091983609661) +)) +vauquelin_place = Polygon(( + (-73.5542465586147, 45.5081555487952), + (-73.5540185061397, 45.5084409343852), + (-73.5546126200639, 45.5086813829106), + (-73.5548825850032, 45.5084033554357), + (-73.5542465586147, 45.5081555487952) +)) + +city_hall_walls = [ + LineString(( + (-73.5541107525234, 45.5091983609661), + (-73.5546126200639, 45.5086813829106), + (-73.5540185061397, 45.5084409343852) + )), + LineString(( + (-73.5539986525799, 45.5084323044531), + (-73.5535801792994, 45.5089539203786), + (-73.5541107525234, 45.5091983609661) + )) +] + +city_hall_entrance = Point(-73.553785, 45.508722) +city_hall_balcony = Point(-73.554138, 45.509080) +city_hall_council_chamber = Point(-73.554246, 45.508931) + +point_3D = Point(-73.553785, 45.508722, 300) +linestring_3D = LineString(( + (-73.5541107525234, 45.5091983609661, 300), + (-73.5546126200639, 45.5086813829106, 300), + (-73.5540185061397, 45.5084409343852, 300) +)) +polygon_3D = Polygon(( + (-73.5541107525234, 45.5091983609661, 300), + (-73.5535801792994, 45.5089539203786, 300), + (-73.5541107525234, 45.5091983609661, 300) +)) + + +def test_infer_schema_only_points(): + df = GeoDataFrame( + geometry=[city_hall_entrance, city_hall_balcony] + ) + + assert infer_schema(df) == { + 'geometry': 'Point', + 'properties': OrderedDict() + } + + +def test_infer_schema_points_and_multipoints(): + df = GeoDataFrame( + geometry=[ + MultiPoint([city_hall_entrance, city_hall_balcony]), + city_hall_balcony + ] + ) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': ['MultiPoint', 'Point'], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': 'Point', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_multipoints(): + df = GeoDataFrame( + geometry=[MultiPoint([ + city_hall_entrance, + city_hall_balcony, + city_hall_council_chamber + ])] + ) + + assert infer_schema(df) == { + 'geometry': 'MultiPoint', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_linestrings(): + df = GeoDataFrame(geometry=city_hall_walls) + + assert infer_schema(df) == { + 'geometry': 'LineString', + 'properties': OrderedDict() + } + + +def test_infer_schema_linestrings_and_multilinestrings(): + df = GeoDataFrame( + geometry=[ + MultiLineString(city_hall_walls), + city_hall_walls[0] + ] + ) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': ['MultiLineString', 'LineString'], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': 'LineString', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_multilinestrings(): + df = GeoDataFrame(geometry=[MultiLineString(city_hall_walls)]) + + assert infer_schema(df) == { + 'geometry': 'MultiLineString', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_polygons(): + df = GeoDataFrame( + geometry=[city_hall_boundaries, vauquelin_place] + ) + + assert infer_schema(df) == { + 'geometry': 'Polygon', + 'properties': OrderedDict() + } + + +def test_infer_schema_polygons_and_multipolygons(): + df = GeoDataFrame( + geometry=[ + MultiPolygon((city_hall_boundaries, vauquelin_place)), + city_hall_boundaries + ] + ) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': ['MultiPolygon', 'Polygon'], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': 'Polygon', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_multipolygons(): + df = GeoDataFrame( + geometry=[ + MultiPolygon((city_hall_boundaries, vauquelin_place)) + ] + ) + + assert infer_schema(df) == { + 'geometry': 'MultiPolygon', + 'properties': OrderedDict() + } + + +def test_infer_schema_multiple_shape_types(): + df = GeoDataFrame( + geometry=[ + MultiPolygon((city_hall_boundaries, vauquelin_place)), + city_hall_boundaries, + MultiLineString(city_hall_walls), + city_hall_walls[0], + MultiPoint([city_hall_entrance, city_hall_balcony]), + city_hall_balcony + ] + ) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': [ + 'MultiPolygon', 'Polygon', + 'MultiLineString', 'LineString', + 'MultiPoint', 'Point' + ], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': [ + 'Polygon', + 'LineString', + 'Point' + ], + 'properties': OrderedDict() + } + + +def test_infer_schema_mixed_3D_shape_type(): + df = GeoDataFrame( + geometry=[ + MultiPolygon((city_hall_boundaries, vauquelin_place)), + city_hall_boundaries, + MultiLineString(city_hall_walls), + city_hall_walls[0], + MultiPoint([city_hall_entrance, city_hall_balcony]), + city_hall_balcony, + point_3D + ] + ) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': [ + '3D Point', + 'MultiPolygon', 'Polygon', + 'MultiLineString', 'LineString', + 'MultiPoint', 'Point' + ], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': ['3D Polygon', '3D LineString', '3D Point'], + 'properties': OrderedDict() + } + + +def test_infer_schema_mixed_3D_Point(): + df = GeoDataFrame(geometry=[city_hall_balcony, point_3D]) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': ['3D Point', 'Point'], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': '3D Point', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_3D_Points(): + df = GeoDataFrame(geometry=[point_3D, point_3D]) + + assert infer_schema(df) == { + 'geometry': '3D Point', + 'properties': OrderedDict() + } + + +def test_infer_schema_mixed_3D_linestring(): + df = GeoDataFrame( + geometry=[city_hall_walls[0], linestring_3D] + ) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': ['3D LineString', 'LineString'], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': '3D LineString', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_3D_linestrings(): + df = GeoDataFrame(geometry=[linestring_3D, linestring_3D]) + + assert infer_schema(df) == { + 'geometry': '3D LineString', + 'properties': OrderedDict() + } + + +def test_infer_schema_mixed_3D_Polygon(): + df = GeoDataFrame(geometry=[city_hall_boundaries, polygon_3D]) + + if _FIONA18: + assert infer_schema(df) == { + 'geometry': ['3D Polygon', 'Polygon'], + 'properties': OrderedDict() + } + else: + assert infer_schema(df) == { + 'geometry': '3D Polygon', + 'properties': OrderedDict() + } + + +def test_infer_schema_only_3D_Polygons(): + df = GeoDataFrame(geometry=[polygon_3D, polygon_3D]) + + assert infer_schema(df) == { + 'geometry': '3D Polygon', + 'properties': OrderedDict() + } + + +def test_infer_schema_null_geometry_and_2D_point(): + df = GeoDataFrame(geometry=[None, city_hall_entrance]) + + # None geometry type is then omitted + assert infer_schema(df) == { + 'geometry': 'Point', + 'properties': OrderedDict() + } + + +def test_infer_schema_null_geometry_and_3D_point(): + df = GeoDataFrame(geometry=[None, point_3D]) + + # None geometry type is then omitted + assert infer_schema(df) == { + 'geometry': '3D Point', + 'properties': OrderedDict() + } + + +def test_infer_schema_null_geometry_all(): + df = GeoDataFrame(geometry=[None, None]) + + # None geometry type in then replaced by 'Unknown' + # (default geometry type supported by Fiona) + assert infer_schema(df) == { + 'geometry': 'Unknown', + 'properties': OrderedDict() + }