Is a GeoDjango MultiPolygonField supposed to accept a Polygon geometry?

Viewed 2045

When I try to set a Polygon on a MultiPolygonField the following exception is raised:

Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/mattrowbum/.virtualenvs/my_env/lib/python3.7/site-packages/django/contrib/gis/db/models/proxy.py", line 75, in __set__
    instance.__class__.__name__, gtype, type(value)))
TypeError: Cannot set Location SpatialProxy (MULTIPOLYGON) with value of type: <class 'django.contrib.gis.geos.polygon.Polygon'>

That would be understandable, except that a note in the GeoDjango tutorial states:

...a GeoDjango MultiPolygonField will accept a Polygon geometry.

I've had a look at the source of proxy.py, and it is checking whether the value (a Polygon) is an instance of the relevant geometry class (a MultiPolygon). I've tried doing this check manually which confirms that a Polygon does not inherit from MultiPolygon:

>>> from django.contrib.gis.geos import MultiPolygon, Polygon
>>> ext_coords = ((0, 0), (0, 1), (1, 1), (1, 0), (0, 0))
>>> int_coords = ((0.4, 0.4), (0.4, 0.6), (0.6, 0.6), (0.6, 0.4), (0.4, 0.4))
>>> poly = Polygon(ext_coords, int_coords)
>>> isinstance(poly, Polygon)
True
>>> isinstance(poly, MultiPolygon)
False

This came to my attention when attempting to simplify an existing stored MultiPolygon value. Below is my model:

from django.contrib.gis.db import models

class Location(models.Model):
    name = models.CharField(max_length=180)
    mpoly = models.MultiPolygonField(geography=True, blank=True, null=True)

The process I used to simplify the MultiPolygon is below. The last line causes the exception to be raised:

>>> from my_app.models import Location
>>> location = Location.objects.get(pk=1)
>>> geom = location.mpoly
>>> simplified_geom = geom.simplify(0.0002)
>>> location.mpoly = simplified_geom

If I use the Polygon to create a MultiPolygon, it works fine:

>>> multi = MultiPolygon([simplified_geom,])
>>> location.mpoly = multi

Is the note in the tutorial misleading or am I doing something wrong?


EDIT: Further tests on the geometries.

The original MultiPolygon straight from the model field:

>>> geom = Location.mpoly
>>> type(geom)
<class 'django.contrib.gis.geos.collections.MultiPolygon'>
>>> geom.geom_type
'MultiPolygon'
>>> geom.valid
True
>>> geom.srid
4326

Applying the simplify() method:

>>> simplified_geom = geom.simplify(0.0002)
>>> type(simplified_geom)
<class 'django.contrib.gis.geos.polygon.Polygon'>
>>> simplified_geom.geom_type
'Polygon'
>>> simplified_geom.valid
True
>>> simplified_geom.srid
4326

Creating a MultiPolygon from the simplified geometry:

>>> multi = MultiPolygon([simplified_geom,])
>>> type(multi)
<class 'django.contrib.gis.geos.collections.MultiPolygon'>
>>> multi.geom_type
'MultiPolygon'
>>> multi.valid
True
>>> multi.srid
>>> print(multi.srs)
None

Note that the above MultiPolygon has no SRID. I thought that might have been the reason is was being accepted. I created one with the srid=4326 argument but it too was accepted by the field.

Here's a really basic example of the problem:

>>> # This works
>>> location.mpoly = MultiPolygon()
>>> # This doesn't
>>> location.mpoly = Polygon()
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/Users/mattrowbum/.virtualenvs/musicteacher/lib/python3.7/site-packages/django/contrib/gis/db/models/proxy.py", line 75, in __set__
    instance.__class__.__name__, gtype, type(value)))
TypeError: Cannot set Location SpatialProxy (MULTIPOLYGON) with value of type: <class 'django.contrib.gis.geos.polygon.Polygon'>
1 Answers

EDIT:

Although the MultiPolygon code seems to allow a Polygon to b stored as MultiPolygon object:

class MultiPolygon(GeometryCollection):
    _allowed = Polygon
    _typeid = 6

the issue you are presenting rises as you describe it.

I have tried some workarounds and the only one that I am somewhat satisfied with is to refactor your geom field into a generic GeometryField that can store any type of geometry.

Another option without touching your model would be to convert each Polygon to a MultiPolygon before inserting it to the field:

p = Polygon()
location.mpoly = MultiPolygon(p)

Seems to me that this is worthy of an issue with either a Tutorial update request, or a code fix.


Leaving the previous state of the answer here for comment continuity:

The issue is with the geography=True setting and not with the field, because the MultiPolygonField does accept a Polygon as well as a MultiPolygon:

The geography type provides native support for spatial features represented with geographic coordinates (e.g., WGS84 longitude/latitude). Unlike the plane used by a geometry type, the geography type uses a spherical representation of its data. Distance and measurement operations performed on a geography column automatically employ great circle arc calculations and return linear units. In other words, when ST_Distance is called on two geographies, a value in meters is returned (as opposed to degrees if called on a geometry column in WGS84).

Since you set the field to expect a geography type object, then if you try to pass a non-geography representation of a Polygon, you will get the error in question.

Related