diff --git a/fiona/_geometry.pxd b/fiona/_geometry.pxd index f33368311..82ba5e9c9 100644 --- a/fiona/_geometry.pxd +++ b/fiona/_geometry.pxd @@ -108,21 +108,19 @@ cdef extern from "ogr_api.h": cdef class GeomBuilder: - cdef void *geom - cdef object code - cdef object geomtypename cdef object ndims - cdef _buildCoords(self, void *geom) - cpdef _buildPoint(self) - cpdef _buildLineString(self) - cpdef _buildLinearRing(self) - cdef _buildParts(self, void *geom) - cpdef _buildPolygon(self) - cpdef _buildMultiPoint(self) - cpdef _buildMultiLineString(self) - cpdef _buildMultiPolygon(self) - cpdef _buildGeometryCollection(self) - cdef build(self, void *geom) + cdef list _buildCoords(self, void *geom) + cdef dict _buildPoint(self, void *geom) + cdef dict _buildLineString(self, void *geom) + cdef dict _buildLinearRing(self, void *geom) + cdef list _buildParts(self, void *geom) + cdef dict _buildPolygon(self, void *geom) + cdef dict _buildMultiPoint(self, void *geom) + cdef dict _buildMultiLineString(self, void *geom) + cdef dict _buildMultiPolygon(self, void *geom) + cdef dict _buildGeometryCollection(self, void *geom) + cdef object build_from_feature(self, void *feature) + cdef object build(self, void *geom) cpdef build_wkb(self, object wkb) diff --git a/fiona/_geometry.pyx b/fiona/_geometry.pyx index 1b0fb8ca8..0f72f1c36 100644 --- a/fiona/_geometry.pyx +++ b/fiona/_geometry.pyx @@ -2,11 +2,12 @@ from __future__ import absolute_import +include "gdal.pxi" + import logging from fiona.errors import UnsupportedGeometryTypeError -from fiona.model import _guard_model_object, GEOMETRY_TYPES, Geometry - +from fiona.model import _guard_model_object, GEOMETRY_TYPES, Geometry, OGRGeometryType from fiona._err cimport exc_wrap_int @@ -87,7 +88,15 @@ cdef _deleteOgrGeom(void *cogr_geometry): cdef class GeomBuilder: """Builds Fiona (GeoJSON) geometries from an OGR geometry handle. """ - cdef _buildCoords(self, void *geom): + + # Note: The geometry passed to OGR_G_ForceToPolygon and + # OGR_G_ForceToMultiPolygon must be removed from the container / + # feature beforehand and the returned geometry needs to be cleaned up + # afterwards. + # OGR_G_GetLinearGeometry returns a copy of the geometry that needs + # to be cleaned up afterwards. + + cdef list _buildCoords(self, void *geom): # Build a coordinate sequence cdef int i if geom == NULL: @@ -101,66 +110,150 @@ cdef class GeomBuilder: coords.append(tuple(values)) return coords - cpdef _buildPoint(self): - return {'type': 'Point', 'coordinates': self._buildCoords(self.geom)[0]} + cdef dict _buildPoint(self, void *geom): + return {'type': 'Point', 'coordinates': self._buildCoords(geom)[0]} - cpdef _buildLineString(self): - return {'type': 'LineString', 'coordinates': self._buildCoords(self.geom)} + cdef dict _buildLineString(self, void *geom): + return {'type': 'LineString', 'coordinates': self._buildCoords(geom)} - cpdef _buildLinearRing(self): - return {'type': 'LinearRing', 'coordinates': self._buildCoords(self.geom)} + cdef dict _buildLinearRing(self, void *geom): + return {'type': 'LinearRing', 'coordinates': self._buildCoords(geom)} - cdef _buildParts(self, void *geom): + cdef list _buildParts(self, void *geom): cdef int j + cdef int code + cdef int count cdef void *part if geom == NULL: raise ValueError("Null geom") parts = [] - for j in range(OGR_G_GetGeometryCount(geom)): + j = 0 + count = OGR_G_GetGeometryCount(geom) + while j < count: part = OGR_G_GetGeometryRef(geom, j) - parts.append(GeomBuilder().build(part)) + code = base_geometry_type_code(OGR_G_GetGeometryType(part)) + if code in ( + OGRGeometryType.PolyhedralSurface.value, + OGRGeometryType.TIN.value, + OGRGeometryType.Triangle.value, + ): + OGR_G_RemoveGeometry(geom, j, False) + # Removing a geometry will cause the geometry count to drop by one, + # and all “higher” geometries will shuffle down one in index. + count -= 1 + parts.append(GeomBuilder().build(part)) + else: + parts.append(GeomBuilder().build(part)) + j += 1 return parts - cpdef _buildPolygon(self): - coordinates = [p['coordinates'] for p in self._buildParts(self.geom)] + cdef dict _buildPolygon(self, void *geom): + coordinates = [p['coordinates'] for p in self._buildParts(geom)] return {'type': 'Polygon', 'coordinates': coordinates} - cpdef _buildMultiPoint(self): - coordinates = [p['coordinates'] for p in self._buildParts(self.geom)] + cdef dict _buildMultiPoint(self, void *geom): + coordinates = [p['coordinates'] for p in self._buildParts(geom)] return {'type': 'MultiPoint', 'coordinates': coordinates} - cpdef _buildMultiLineString(self): - coordinates = [p['coordinates'] for p in self._buildParts(self.geom)] + cdef dict _buildMultiLineString(self, void *geom): + coordinates = [p['coordinates'] for p in self._buildParts(geom)] return {'type': 'MultiLineString', 'coordinates': coordinates} - cpdef _buildMultiPolygon(self): - coordinates = [p['coordinates'] for p in self._buildParts(self.geom)] + cdef dict _buildMultiPolygon(self, void *geom): + coordinates = [p['coordinates'] for p in self._buildParts(geom)] return {'type': 'MultiPolygon', 'coordinates': coordinates} - cpdef _buildGeometryCollection(self): - parts = self._buildParts(self.geom) + cdef dict _buildGeometryCollection(self, void *geom): + parts = self._buildParts(geom) return {'type': 'GeometryCollection', 'geometries': parts} - cdef build(self, void *geom): - # The only method anyone needs to call - if geom == NULL: - raise ValueError("Null geom") - cdef unsigned int etype = OGR_G_GetGeometryType(geom) + cdef object build_from_feature(self, void *feature): + # Build Geometry from *OGRFeatureH + cdef void *cogr_geometry = NULL + cdef int code + + cogr_geometry = OGR_F_GetGeometryRef(feature) + code = base_geometry_type_code(OGR_G_GetGeometryType(cogr_geometry)) - self.code = base_geometry_type_code(etype) + # We need to take ownership of the geometry before we can call + # OGR_G_ForceToPolygon or OGR_G_ForceToMultiPolygon + if code in ( + OGRGeometryType.PolyhedralSurface.value, + OGRGeometryType.TIN.value, + OGRGeometryType.Triangle.value, + ): + cogr_geometry = OGR_F_StealGeometry(feature) + return self.build(cogr_geometry) - if self.code not in GEOMETRY_TYPES: - raise UnsupportedGeometryTypeError(self.code) + cdef object build(self, void *geom): + # Build Geometry from *OGRGeometryH - self.geomtypename = GEOMETRY_TYPES[self.code] + cdef void *geometry_to_dealloc = NULL + + if geom == NULL: + return None + + code = base_geometry_type_code(OGR_G_GetGeometryType(geom)) + + # We convert special geometries (Curves, TIN, Triangle, ...) + # to GeoJSON compatible geometries (LineStrings, Polygons, MultiPolygon, ...) + if code in ( + OGRGeometryType.CircularString.value, + OGRGeometryType.CompoundCurve.value, + OGRGeometryType.CurvePolygon.value, + OGRGeometryType.MultiCurve.value, + OGRGeometryType.MultiSurface.value, + # OGRGeometryType.Curve.value, # Abstract type + # OGRGeometryType.Surface.value, # Abstract type + ): + geometry_to_dealloc = OGR_G_GetLinearGeometry(geom, 0.0, NULL) + code = base_geometry_type_code(OGR_G_GetGeometryType(geometry_to_dealloc)) + geom = geometry_to_dealloc + elif code in ( + OGRGeometryType.PolyhedralSurface.value, + OGRGeometryType.TIN.value, + OGRGeometryType.Triangle.value, + ): + if code in (OGRGeometryType.PolyhedralSurface.value, OGRGeometryType.TIN.value): + geometry_to_dealloc = OGR_G_ForceToMultiPolygon(geom) + elif code == OGRGeometryType.Triangle.value: + geometry_to_dealloc = OGR_G_ForceToPolygon(geom) + code = base_geometry_type_code(OGR_G_GetGeometryType(geometry_to_dealloc)) + geom = geometry_to_dealloc self.ndims = OGR_G_GetCoordinateDimension(geom) - self.geom = geom - built = getattr(self, '_build' + self.geomtypename)() + + if code not in GEOMETRY_TYPES: + raise UnsupportedGeometryTypeError(code) + + geomtypename = GEOMETRY_TYPES[code] + if geomtypename == "Point": + built = self._buildPoint(geom) + elif geomtypename == "LineString": + built = self._buildLineString(geom) + elif geomtypename == "LinearRing": + built = self._buildLinearRing(geom) + elif geomtypename == "Polygon": + built = self._buildPolygon(geom) + elif geomtypename == "MultiPoint": + built = self._buildMultiPoint(geom) + elif geomtypename == "MultiLineString": + built = self._buildMultiLineString(geom) + elif geomtypename == "MultiPolygon": + built = self._buildMultiPolygon(geom) + elif geomtypename == "GeometryCollection": + built = self._buildGeometryCollection(geom) + else: + raise UnsupportedGeometryTypeError(code) + + # Cleanup geometries we have ownership over + if geometry_to_dealloc is not NULL: + OGR_G_DestroyGeometry(geometry_to_dealloc) + return Geometry.from_dict(**built) cpdef build_wkb(self, object wkb): - # The only other method anyone needs to call + # Build geometry from wkb cdef object data = wkb cdef void *cogr_geometry = _createOgrGeomFromWKB(data) result = self.build(cogr_geometry) @@ -247,6 +340,7 @@ cdef class OGRGeomBuilder: cdef object typename = geometry.type cdef object coordinates = geometry.coordinates cdef object geometries = geometry.geometries + if typename == 'Point': return self._buildPoint(coordinates) elif typename == 'LineString': diff --git a/fiona/gdal.pxi b/fiona/gdal.pxi index d15c84cf1..3c3270d3b 100644 --- a/fiona/gdal.pxi +++ b/fiona/gdal.pxi @@ -521,6 +521,7 @@ cdef extern from "ogr_api.h" nogil: void OGR_Fld_SetPrecision(OGRFieldDefnH, int n) void OGR_Fld_SetWidth(OGRFieldDefnH, int n) OGRErr OGR_G_AddGeometryDirectly(OGRGeometryH geometry, OGRGeometryH part) + OGRErr OGR_G_RemoveGeometry(OGRGeometryH geometry, int i, int delete) void OGR_G_AddPoint(OGRGeometryH geometry, double x, double y, double z) void OGR_G_AddPoint_2D(OGRGeometryH geometry, double x, double y) void OGR_G_CloseRings(OGRGeometryH geometry) @@ -538,7 +539,7 @@ cdef extern from "ogr_api.h" nogil: double OGR_G_GetX(OGRGeometryH geometry, int n) double OGR_G_GetY(OGRGeometryH geometry, int n) double OGR_G_GetZ(OGRGeometryH geometry, int n) - void OGR_G_ImportFromWkb(OGRGeometryH geometry, unsigned char *bytes, + OGRErr OGR_G_ImportFromWkb(OGRGeometryH geometry, unsigned char *bytes, int nbytes) int OGR_G_WkbSize(OGRGeometryH geometry) OGRErr OGR_L_CreateFeature(OGRLayerH layer, OGRFeatureH feature) diff --git a/fiona/model.py b/fiona/model.py index 1c68ffb54..5a8eea487 100644 --- a/fiona/model.py +++ b/fiona/model.py @@ -1,78 +1,108 @@ """Fiona data model""" -from collections.abc import MutableMapping -from collections import OrderedDict import itertools +from collections import OrderedDict +from collections.abc import MutableMapping +from enum import Enum from json import JSONEncoder from warnings import warn -from collections.abc import MutableMapping from fiona.errors import FionaDeprecationWarning -# Mapping of OGR integer geometry types to GeoJSON type names. -GEOMETRY_TYPES = { - 0: "Unknown", - 1: "Point", - 2: "LineString", - 3: "Polygon", - 4: "MultiPoint", - 5: "MultiLineString", - 6: "MultiPolygon", - 7: "GeometryCollection", - # Unsupported types. - # 8: 'CircularString', - # 9: 'CompoundCurve', - # 10: 'CurvePolygon', - # 11: 'MultiCurve', - # 12: 'MultiSurface', - # 13: 'Curve', - # 14: 'Surface', - # 15: 'PolyhedralSurface', - # 16: 'TIN', - # 17: 'Triangle', - 100: "None", - 101: "LinearRing", - 0x80000001: "3D Point", - 0x80000002: "3D LineString", - 0x80000003: "3D Polygon", - 0x80000004: "3D MultiPoint", - 0x80000005: "3D MultiLineString", - 0x80000006: "3D MultiPolygon", - 0x80000007: "3D GeometryCollection", -} +class OGRGeometryType(Enum): + Unknown = 0 + Point = 1 + LineString = 2 + Polygon = 3 + MultiPoint = 4 + MultiLineString = 5 + MultiPolygon = 6 + GeometryCollection = 7 + CircularString = 8 + CompoundCurve = 9 + CurvePolygon = 10 + MultiCurve = 11 + MultiSurface = 12 + Curve = 13 + Surface = 14 + PolyhedralSurface = 15 + TIN = 16 + Triangle = 17 + NONE = 100 + LinearRing = 101 + CircularStringZ = 1008 + CompoundCurveZ = 1009 + CurvePolygonZ = 1010 + MultiCurveZ = 1011 + MultiSurfaceZ = 1012 + CurveZ = 1013 + SurfaceZ = 1014 + PolyhedralSurfaceZ = 1015 + TINZ = 1016 + TriangleZ = 1017 + PointM = 2001 + LineStringM = 2002 + PolygonM = 2003 + MultiPointM = 2004 + MultiLineStringM = 2005 + MultiPolygonM = 2006 + GeometryCollectionM = 2007 + CircularStringM = 2008 + CompoundCurveM = 2009 + CurvePolygonM = 2010 + MultiCurveM = 2011 + MultiSurfaceM = 2012 + CurveM = 2013 + SurfaceM = 2014 + PolyhedralSurfaceM = 2015 + TINM = 2016 + TriangleM = 2017 + PointZM = 3001 + LineStringZM = 3002 + PolygonZM = 3003 + MultiPointZM = 3004 + MultiLineStringZM = 3005 + MultiPolygonZM = 3006 + GeometryCollectionZM = 3007 + CircularStringZM = 3008 + CompoundCurveZM = 3009 + CurvePolygonZM = 3010 + MultiCurveZM = 3011 + MultiSurfaceZM = 3012 + CurveZM = 3013 + SurfaceZM = 3014 + PolyhedralSurfaceZM = 3015 + TINZM = 3016 + TriangleZM = 3017 + Point25D = 0x80000001 + LineString25D = 0x80000002 + Polygon25D = 0x80000003 + MultiPoint25D = 0x80000004 + MultiLineString25D = 0x80000005 + MultiPolygon25D = 0x80000006 + GeometryCollection25D = 0x80000007 # Mapping of OGR integer geometry types to GeoJSON type names. GEOMETRY_TYPES = { - 0: "Unknown", - 1: "Point", - 2: "LineString", - 3: "Polygon", - 4: "MultiPoint", - 5: "MultiLineString", - 6: "MultiPolygon", - 7: "GeometryCollection", - # Unsupported types. - # 8: 'CircularString', - # 9: 'CompoundCurve', - # 10: 'CurvePolygon', - # 11: 'MultiCurve', - # 12: 'MultiSurface', - # 13: 'Curve', - # 14: 'Surface', - # 15: 'PolyhedralSurface', - # 16: 'TIN', - # 17: 'Triangle', - 100: "None", - 101: "LinearRing", - 0x80000001: "3D Point", - 0x80000002: "3D LineString", - 0x80000003: "3D Polygon", - 0x80000004: "3D MultiPoint", - 0x80000005: "3D MultiLineString", - 0x80000006: "3D MultiPolygon", - 0x80000007: "3D GeometryCollection", + OGRGeometryType.Unknown.value: "Unknown", + OGRGeometryType.Point.value: "Point", + OGRGeometryType.LineString.value: "LineString", + OGRGeometryType.Polygon.value: "Polygon", + OGRGeometryType.MultiPoint.value: "MultiPoint", + OGRGeometryType.MultiLineString.value: "MultiLineString", + OGRGeometryType.MultiPolygon.value: "MultiPolygon", + OGRGeometryType.GeometryCollection.value: "GeometryCollection", + OGRGeometryType.NONE.value: "None", + OGRGeometryType.LinearRing.value: "LinearRing", + OGRGeometryType.Point25D.value: "3D Point", + OGRGeometryType.LineString25D.value: "3D LineString", + OGRGeometryType.Polygon25D.value: "3D Polygon", + OGRGeometryType.MultiPoint25D.value: "3D MultiPoint", + OGRGeometryType.MultiLineString25D.value: "3D MultiLineString", + OGRGeometryType.MultiPolygon25D.value: "3D MultiPolygon", + OGRGeometryType.GeometryCollection25D.value: "3D GeometryCollection", } diff --git a/fiona/ogrext.pyx b/fiona/ogrext.pyx index e6fb4cbb0..7dad492ae 100644 --- a/fiona/ogrext.pyx +++ b/fiona/ogrext.pyx @@ -351,37 +351,10 @@ cdef class FeatureBuilder: props[key] = None cdef void *cogr_geometry = NULL - cdef void *org_geometry = NULL - geom = None - if not ignore_geometry: cogr_geometry = OGR_F_GetGeometryRef(feature) - - if cogr_geometry is not NULL: - - code = base_geometry_type_code(OGR_G_GetGeometryType(cogr_geometry)) - - if 8 <= code <= 14: # Curves. - cogr_geometry = OGR_G_GetLinearGeometry(cogr_geometry, 0.0, NULL) - geom = GeomBuilder().build(cogr_geometry) - OGR_G_DestroyGeometry(cogr_geometry) - - elif 15 <= code <= 17: - # We steal the geometry: the geometry of the in-memory feature is now null - # and we are responsible for cogr_geometry. - org_geometry = OGR_F_StealGeometry(feature) - - if code in (15, 16): - cogr_geometry = OGR_G_ForceToMultiPolygon(org_geometry) - elif code == 17: - cogr_geometry = OGR_G_ForceToPolygon(org_geometry) - - geom = GeomBuilder().build(cogr_geometry) - OGR_G_DestroyGeometry(cogr_geometry) - - else: - geom = GeomBuilder().build(cogr_geometry) + geom = GeomBuilder().build_from_feature(feature) return Feature(id=str(fid), properties=Properties(**props), geometry=geom) diff --git a/tests/data/test_tin.csv b/tests/data/test_tin.csv index a1470260d..229bbdbd8 100644 --- a/tests/data/test_tin.csv +++ b/tests/data/test_tin.csv @@ -1,3 +1,4 @@ WKT,id "TIN (((0 0 0, 0 0 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 0 0 0)))",1 "TRIANGLE((0 0 0,0 1 0,1 1 0,0 0 0))",2 +"GEOMETRYCOLLECTION (TIN (((0 0 0, 0 0 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 0 0 0))), TRIANGLE((0 0 0,0 1 0,1 1 0,0 0 0)))",3 diff --git a/tests/test_rfc64_tin.py b/tests/test_rfc64_tin.py index c902a6855..730a62708 100644 --- a/tests/test_rfc64_tin.py +++ b/tests/test_rfc64_tin.py @@ -4,46 +4,52 @@ """ import fiona +from fiona.model import Geometry -from .conftest import requires_gdal22 + +def _test_tin(geometry: Geometry) -> None: + """Test if TIN (((0 0 0, 0 0 1, 0 1 0, 0 0 0)), ((0 0 0, 0 1 0, 1 1 0, 0 0 0))) + is correctly converted to MultiPolygon. + """ + assert geometry["type"] == "MultiPolygon" + assert geometry["coordinates"] == [ + [[(0.0, 0.0, 0.0), (0.0, 0.0, 1.0), (0.0, 1.0, 0.0), (0.0, 0.0, 0.0)]], + [[(0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (0.0, 0.0, 0.0)]], + ] + + +def _test_triangle(geometry: Geometry) -> None: + """Test if TRIANGLE((0 0 0,0 1 0,1 1 0,0 0 0)) + is correctly converted to MultiPolygon.""" + assert geometry["type"] == "Polygon" + assert geometry["coordinates"] == [ + [(0.0, 0.0, 0.0), (0.0, 1.0, 0.0), (1.0, 1.0, 0.0), (0.0, 0.0, 0.0)] + ] def test_tin_shp(path_test_tin_shp): """Convert TIN to MultiPolygon""" with fiona.open(path_test_tin_shp) as col: - assert col.schema['geometry'] == 'Unknown' + assert col.schema["geometry"] == "Unknown" features = list(col) assert len(features) == 1 - assert features[0]['geometry']['type'] == 'MultiPolygon' - assert features[0]['geometry']['coordinates'] == [[[(0.0, 0.0, 0.0), - (0.0, 0.0, 1.0), - (0.0, 1.0, 0.0), - (0.0, 0.0, 0.0)]], - [[(0.0, 0.0, 0.0), - (0.0, 1.0, 0.0), - (1.0, 1.0, 0.0), - (0.0, 0.0, 0.0)]]] - - -@requires_gdal22 + _test_tin(features[0]["geometry"]) + + def test_tin_csv(path_test_tin_csv): """Convert TIN to MultiPolygon and Triangle to Polygon""" with fiona.open(path_test_tin_csv) as col: - assert col.schema['geometry'] == 'Unknown' - features = list(col) - assert len(features) == 2 - assert features[0]['geometry']['type'] == 'MultiPolygon' - assert features[0]['geometry']['coordinates'] == [[[(0.0, 0.0, 0.0), - (0.0, 0.0, 1.0), - (0.0, 1.0, 0.0), - (0.0, 0.0, 0.0)]], - [[(0.0, 0.0, 0.0), - (0.0, 1.0, 0.0), - (1.0, 1.0, 0.0), - (0.0, 0.0, 0.0)]]] - - assert features[1]['geometry']['type'] == 'Polygon' - assert features[1]['geometry']['coordinates'] == [[(0.0, 0.0, 0.0), - (0.0, 1.0, 0.0), - (1.0, 1.0, 0.0), - (0.0, 0.0, 0.0)]] + assert col.schema["geometry"] == "Unknown" + + feature1 = next(col) + _test_tin(feature1["geometry"]) + + feature2 = next(col) + _test_triangle(feature2["geometry"]) + + feature3 = next(col) + assert feature3["geometry"]["type"] == "GeometryCollection" + assert len(feature3["geometry"]["geometries"]) == 2 + + _test_tin(feature3["geometry"]["geometries"][0]) + _test_triangle(feature3["geometry"]["geometries"][1])