Opened 14 months ago

Last modified 3 weeks ago

#34406 assigned New feature

Add support for curved geometries in GeoDjango

Reported by: Fabien Le Frapper Owned by: David Smith
Component: GIS Version: dev
Severity: Normal Keywords: geodjango gdal
Cc: Anthony Ricaud Triage Stage: Accepted
Has patch: yes Needs documentation: yes
Needs tests: no Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description (last modified by Fabien Le Frapper)

I tried ingesting curved geometries in a GeometryField in GeoDjango.

At first I encountered some errors as these are not officially supported in GeoDjango, but I noticed that gdal is able to go from a geometry to another using converters :

I successfully ingested the following geometries for a project :

  • 9: "CompoundCurve"
  • 10: "CurvePolygon"
  • 11: "MultiCurve"
  • 12: "MultiSurface"

Below is a code snippet I used, subclassing OGRGeometry and OGRGeomType in order to bypass existing GeoDjango validation to add support for more geometries :

  • It mainly adds keys for new geometries and classes inheriting for OGRGeometry to handle these in GeoDjango
  • It shows how we could make these geometries backward compatible (from curved geometries to more standard geometries) using a simple call to existing gdal methods

Is there a reason why it is not supported at the moment in GeoDjango ?
Would you consider a pull request adding support for these geometries ?

I could suggest a patch based on the snippet above, but I am not sure how to treat the transform part of it.
Should we keep this part ?
It seems that Postgis can handle these polygons https://postgis.net/docs/using_postgis_dbmanagement.html#CircularString


Here is the full snippet

from django.contrib.gis.gdal.geometries import GEO_CLASSES, OGRGeometry
from django.contrib.gis.gdal.geomtype import OGRGeomType
from django.contrib.gis.gdal.libgdal import lgdal
from django.contrib.gis.gdal.prototypes import ds as capi
from django.contrib.gis.gdal.prototypes import geom as geom_api


class ExtendedOGRGeometry(OGRGeometry):
    def __init__(self, geom_input, srs=None):
        try:
            super().__init__(geom_input, srs)
        except KeyError:
            if (
                not isinstance(geom_input, self.ptr_type)
                and self.geom_type.num not in gdal_transform.keys()
            ):
                raise
                
             self.__class__ = EXTENDED_GEO_CLASSES[self.geom_type.num]
            
    @property
    def geom_type(self):
        "Return the Type for this Geometry."
        return ExtendedOGRGeomType(geom_api.get_geom_type(self.ptr))

class CurvePolygon(ExtendedOGRGeometry):
    pass

class CompoundCurve(ExtendedOGRGeometry):
    pass

class MultiSurface(ExtendedOGRGeometry):
    pass

class MultiCurve(ExtendedOGRGeometry):
    pass

EXTENDED_GEO_CLASSES = {
    **GEO_CLASSES,
    9: CompoundCurve,
    10: CurvePolygon,
    11: MultiCurve,
    12: MultiSurface,
}

class ExtendedOGRGeomType(OGRGeomType):
    # Copy paste of original types dictionnary from GeoDjango implementation
    # https://github.com/django/django/blob/main/django/contrib/gis/gdal/geomtype.py#L9
    _types = {
        0: "Unknown",
        1: "Point",
        2: "LineString",
        3: "Polygon",
        4: "MultiPoint",
        5: "MultiLineString",
        6: "MultiPolygon",
        7: "GeometryCollection",
        100: "None",
        101: "LinearRing",
        102: "PointZ",
        1 + OGRGeomType.wkb25bit: "Point25D",
        2 + OGRGeomType.wkb25bit: "LineString25D",
        3 + OGRGeomType.wkb25bit: "Polygon25D",
        4 + OGRGeomType.wkb25bit: "MultiPoint25D",
        5 + OGRGeomType.wkb25bit: "MultiLineString25D",
        6 + OGRGeomType.wkb25bit: "MultiPolygon25D",
        7 + OGRGeomType.wkb25bit: "GeometryCollection25D",
        # Extended geometry types
        9: "CompoundCurve",
        10: "CurvePolygon",
        11: "MultiCurve",
        12: "MultiSurface",
    }
 

# New bindings to existing GDAL methods
force_to_polygon = geom_output(lgdal.OGR_G_ForceToPolygon, [c_void_p])
force_to_multi_polygon = geom_output(lgdal.OGR_G_ForceToMultiPolygon, [c_void_p])
force_to_line = geom_output(lgdal.OGR_G_ForceToLineString, [c_void_p])
force_to_multi_line = geom_output(lgdal.OGR_G_ForceToMultiLineString, [c_void_p])

# The functions below need to be called on the corresponding geometry type 
# before saving it in the database to prevent errors in geodjango. 
gdal_transform = {
    9: force_to_line,
    10: force_to_polygon,
    11: force_to_multi_line,
    12: force_to_multi_polygon,
}

def ingest_curved_geometry(geom_ptr):
    transform = gdal_transform[geom.geom_type.num]
    geom = ExtendedOGRGeometry(transform(geom_api.clone_geom(geom_ptr)))

    # FIXME: for a yet unknown reason, the initial SRID is not kept when using ExtendedOGRGeometry 
    geom.srid = 3857
    geom.transform(4326)

Change History (11)

comment:1 by Claude Paroz, 14 months ago

At first, I'd say that we are open to extend OGR recognition of those types. However, I wonder if the missing GEOS type counterpart may prevent proper usage of those new types in GeoDjango. At the very least, we can recognize these new types and error out with a proper error message if we don't support them in the framework. Accepting on that base.

comment:2 by Claude Paroz, 14 months ago

Triage Stage: UnreviewedAccepted

in reply to:  1 comment:3 by Fabien Le Frapper, 14 months ago

Thanks for your reply.
Do we have existing geodjango features that are gdal-only ?
Would a gdal-only contribution erroring in case of GEOS be accepted ?
Edit : sorry this is my first time in the Django bug tracker, I just noticed that you updated the ticket status. I will work on a PR then.

Last edited 14 months ago by Fabien Le Frapper (previous) (diff)

comment:4 by Fabien Le Frapper, 14 months ago

Owner: changed from nobody to Fabien Le Frapper
Status: newassigned

comment:5 by Fabien Le Frapper, 14 months ago

Description: modified (diff)

comment:6 by Fabien Le Frapper, 14 months ago

Description: modified (diff)

comment:7 by Anthony Ricaud, 14 months ago

Cc: Anthony Ricaud added

comment:8 by David Smith, 4 months ago

Here is a link to GDAL's RFC for "Curve geometries" RFC 49

This required the following C API changes. I think we should make a decision on each of these (should Django implement them) as part of this ticket.

  • OGR_GT_xxxx (for Geometry Type)
  • OGRErr OGR_G_ExportToIsoWkb( OGRGeometryH, OGRwkbByteOrder, unsigned char*) -- Merged in 3f6d939c62efd967f548c27a265748cc2cc47ca5
  • OGRErr OGR_G_ExportToIsoWkt( OGRGeometryH, char ) -- Merged in 3f6d939c62efd967f548c27a265748cc2cc47ca5
  • OGRGeometryH OGR_G_Value( OGRGeometryH, double dfDistance )
  • int OGR_G_HasCurveGeometry( OGRGeometryH, int bLookForNonLinear )
  • OGRGeometryH OGR_G_GetLinearGeometry( OGRGeometryH hGeom, double dfMaxAngleStepSizeDegrees, char papszOptions)
  • OGRGeometryH OGR_G_GetCurveGeometry( OGRGeometryH hGeom, char papszOptions )
  • void OGRSetNonLinearGeometriesEnabledFlag(int bFlag)
  • int OGRGetNonLinearGeometriesEnabledFlag()
Last edited 3 months ago by David Smith (previous) (diff)

comment:9 by David Smith, 4 months ago

Owner: changed from Fabien Le Frapper to David Smith

comment:10 by David Smith, 4 weeks ago

Has patch: set

comment:11 by Natalia Bidart, 3 weeks ago

Needs documentation: set
Patch needs improvement: set
Version: 4.1dev

Setting patch flags per latest reviews from Claude and myself.

Note: See TracTickets for help on using tickets.
Back to Top