3

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'>
MattRowbum
  • 2,162
  • 1
  • 15
  • 20
  • Can you please share the `Model` and more specifically the `MultiPolygonField` creation? Do you use `geography=True` option in there? – John Moutafis Jul 21 '20 at 12:57
  • @JohnMoutafis - I've updated the question with the model declaration and the process. I've since looked into the source of the [LayerMapping](https://docs.djangoproject.com/en/3.0/ref/contrib/gis/layermapping/#django.contrib.gis.utils.LayerMapping) utility, and in these circumstances it constructs a multi-geometry if a single geometry is provided. – MattRowbum Jul 22 '20 at 01:06
  • Yes I have seen this before, the issue is with the `geography=True` setting and the Polygon you are passing in the field: https://stackoverflow.com/questions/55451675/cannot-set-model-name-spatialproxy-polygon-with-value-of-type-class-django – John Moutafis Jul 22 '20 at 07:24

1 Answers1

3

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.

John Moutafis
  • 22,254
  • 11
  • 68
  • 112
  • Thanks for your answer John, but I'm still a bit confused. All I'm doing is using the [simplify](https://docs.djangoproject.com/en/3.0/ref/contrib/gis/geos/#django.contrib.gis.geos.GEOSGeometry.simplify) method to create a new `GEOSGeometry` from the original field value. The SRID of the created Polygon is still 4326. Is there anything else I can check that would indicate a non-geography Polygon? – MattRowbum Jul 22 '20 at 09:06
  • I've updated the question with a quick example showing that if I convert the new `Polygon` into a `MultiPolygon`, it is accepted. – MattRowbum Jul 22 '20 at 09:23
  • @MattRowbum can you check if the failing geometry is still a `Polygon` and that it is `valid` after the `simplify`? – John Moutafis Jul 23 '20 at 07:18
  • Thanks @JohnMoutafis - yes, I can confirm that it is both a `Polygon` and valid. I've added more test results to the end of the question. It will even accept a blank `MultiPolygon`, but raises an exception with a blank `Polygon`. – MattRowbum Jul 23 '20 at 09:43
  • @MattRowbum I tried your issue and couldn't find a satisfying solution :/. Maybe this is a bug in Django and an issue should be probably raised. I made an update with a workaround and my opinion. – John Moutafis Jul 30 '20 at 13:27