Skip to content

Commit

Permalink
Handle empty SpatialUnit geometries, work-around for "POLYGON EMPTY" …
Browse files Browse the repository at this point in the history
…geometries
  • Loading branch information
Anthony Lukach committed Mar 24, 2017
1 parent 912ca9b commit 29a32ec
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 50 deletions.
10 changes: 6 additions & 4 deletions cadasta/organization/importers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ def _create_models(self, type, headers, row, content_types, tenure_type):
if spatial_ct:
try:
spatial_unit_id = row[headers.index(s_id)]
except ValueError:
su = SpatialUnit.objects.create(**spatial_ct)
else:
if spatial_unit_id:
created_su_id = self._locations_created.get(
spatial_unit_id, None
Expand All @@ -233,12 +236,13 @@ def _create_models(self, type, headers, row, content_types, tenure_type):
else:
su = SpatialUnit.objects.create(**spatial_ct)
self._locations_created[spatial_unit_id] = su.pk
except ValueError:
su = SpatialUnit.objects.create(**spatial_ct)

if party_ct:
try:
party_id = row[headers.index(p_id)]
except ValueError:
party = Party.objects.create(**party_ct)
else:
if party_id:
created_party_id = self._parties_created.get(
party_id, None)
Expand All @@ -250,8 +254,6 @@ def _create_models(self, type, headers, row, content_types, tenure_type):
else:
party = Party.objects.create(**party_ct)
self._parties_created[party_id] = party.pk
except ValueError:
party = Party.objects.create(**party_ct)

if party_ct and spatial_ct:
tt = TenureRelationshipType.objects.get(id=tenure_type)
Expand Down
83 changes: 38 additions & 45 deletions cadasta/organization/importers/validators.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
from functools import partial

from django.contrib.gis.geos import GEOSGeometry
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _

from party.models import TenureRelationshipType
from spatial.choices import TYPE_CHOICES
from xforms.utils import InvalidODKGeometryError, odk_geom_to_wkt


def get_field_value(headers, row, header, field_name):
try:
return row[headers.index(header)]
except ValueError:
raise ValidationError(
_("No '{}' column found.".format(field_name))
)


def validate_row(headers, row, config):
party_name, party_type, geometry, tenure_type, location_type = (
None, None, None, None, None)
Expand All @@ -17,59 +29,40 @@ def validate_row(headers, row, config):
raise ValidationError(
_("Number of headers and columns do not match.")
)

_get_field_value = partial(get_field_value, headers, row)

if party_name_field and party_type_field:
try:
party_name = row[headers.index(party_name_field)]
except ValueError:
raise ValidationError(
_("No 'party_name' column found.")
)
try:
party_type = row[headers.index(party_type_field)]
except ValueError:
raise ValidationError(
_("No 'party_type' column found.")
)
party_name = _get_field_value(party_name_field, "party_name")
party_type = _get_field_value(party_type_field, "party_type")

if geometry_field:
try:
coords = row[headers.index(geometry_field)]
except ValueError:
raise ValidationError(
_("No 'geometry_field' column found.")
)
try:
geometry = GEOSGeometry(coords)
except:
pass # try ODK geom parser
if not geometry:
coords = _get_field_value(geometry_field, "geometry_field")
if coords == '':
geometry = None
else:
try:
geometry = odk_geom_to_wkt(coords)
except InvalidODKGeometryError:
raise ValidationError(_("Invalid geometry."))
geometry = GEOSGeometry(coords)
except:
try:
geometry = GEOSGeometry(odk_geom_to_wkt(coords))
except InvalidODKGeometryError:
raise ValidationError(_("Invalid geometry."))

if location_type_field:
try:
location_type = row[headers.index(location_type_field)]
type_choices = [choice[0] for choice in TYPE_CHOICES]
if location_type and location_type not in type_choices:
raise ValidationError(
_("Invalid location_type: '%s'.") % location_type
)
except ValueError:
location_type = _get_field_value(location_type_field, "location_type")
type_choices = [choice[0] for choice in TYPE_CHOICES]
if location_type and location_type not in type_choices:
raise ValidationError(
_("No 'location_type' column found.")
_("Invalid location_type: '%s'.") % location_type
)

if party_name_field and geometry_field:
try:
tenure_type = row[
headers.index(tenure_type_field)]
if tenure_type and not TenureRelationshipType.objects.filter(
id=tenure_type).exists():
raise ValidationError(
_("Invalid tenure_type: '%s'.") % tenure_type
)
except ValueError:
tenure_type = _get_field_value(tenure_type_field, 'tenure_type')
if tenure_type and not TenureRelationshipType.objects.filter(
id=tenure_type).exists():
raise ValidationError(
_("No 'tenure_type' column found.")
_("Invalid tenure_type: '%s'.") % tenure_type
)

return (party_name, party_type, geometry, location_type, tenure_type)
Expand Down
12 changes: 12 additions & 0 deletions cadasta/organization/tests/test_importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,18 @@ def test_validate_geometry(self):
headers, row, config)
assert e.value.message == "Invalid geometry."

def test_validate_empty_geometry(self):
config = {
'party_type_field': 'party_type',
'geometry_field': 'location_geometry',
'type': 'csv'
}
geometry = 'POLYGON EMPTY'
headers = ['party_type', 'location_geometry']
row = ['IN', geometry]
_, _, geo, _, _ = validators.validate_row(headers, row, config)
assert geo.empty is True

def test_validate_location_type_choice(self):
config = {
'party_name_field': 'party_name',
Expand Down
11 changes: 10 additions & 1 deletion cadasta/spatial/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,16 @@ def reassign_spatial_geometry(instance):

@receiver(models.signals.pre_save, sender=SpatialUnit)
def check_extent(sender, instance, **kwargs):
if instance.geometry:
geom = instance.geometry
# Store 'POLYGON EMPTY' data as null to avoid libgeos bug
# (https://trac.osgeo.org/geos/ticket/680)
# TODO: Rm this check when we're using Django 1.11+ or libgeos 3.6.1+
# https://github.com/django/django/commit/b90d72facf1e4294df1c2e6b51b26f6879bf2992#diff-181a3ea304dfaf57f1e1d680b32d2b76R248
from django.contrib.gis.geos.polygon import Polygon
if isinstance(geom, Polygon) and geom.empty:
instance.geometry = None

if geom and not geom.empty:
reassign_spatial_geometry(instance)


Expand Down
23 changes: 23 additions & 0 deletions cadasta/spatial/tests/test_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest

from django.contrib.gis.geos import GEOSGeometry
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
from jsonattrs.models import Attribute, AttributeType, Schema
Expand Down Expand Up @@ -44,6 +45,28 @@ def test_geometry(self):
'11.36667 47.25000))')
assert spatial_unit.geometry is not None

def test_empty_geometries(self):
geoms = (
"POINT EMPTY",
# "POLYGON EMPTY", # Uncomment when using Django 1.11+ or libgeos 3.6.1+
"LINESTRING EMPTY",
"MULTIPOINT EMPTY",
"MULTILINESTRING EMPTY",
"MULTIPOLYGON EMPTY",
"GEOMETRYCOLLECTION EMPTY",
)
for geom in geoms:
spatial_unit = SpatialUnitFactory.create(
geometry=GEOSGeometry(geom))
assert spatial_unit.geometry.wkt == geom

def test_empty_geometry(self):
# Temp workaround where 'POLYGON EMPTY' is cast to None. Should
# be removed when Django is 1.11+ or libgeos is 3.6.1+
spatial_unit = SpatialUnitFactory.create(
geometry=GEOSGeometry('POLYGON EMPTY'))
assert spatial_unit.geometry is None

def test_reassign_extent(self):
spatial_unit = SpatialUnitFactory.create(
geometry='SRID=4326;POLYGON(('
Expand Down

0 comments on commit 29a32ec

Please sign in to comment.