diff --git a/cadasta/organization/tests/test_downloads.py b/cadasta/organization/tests/test_downloads.py index 4119f9c28..1669e4d2c 100644 --- a/cadasta/organization/tests/test_downloads.py +++ b/cadasta/organization/tests/test_downloads.py @@ -475,15 +475,14 @@ def test_write_features(self): if i == 2: assert row == [su2.id, su2.type, '', 'linestring'] if i == 3: - area = format(su3.area, '.2f') - assert row == [su3.id, su3.type, area, 'polygon'] + assert row == [su3.id, su3.type, str(su3.area), 'polygon'] if i == 4: assert row == [su4.id, su4.type, '', 'multipoint'] if i == 5: - area = su5.area assert row == [su5.id, su5.type, '', 'multilinestring'] if i == 6: - assert row == [su6.id, su6.type, '', 'multipolygon'] + assert row == [su6.id, su6.type, str(su6.area), + 'multipolygon'] if i == 7: assert row == [su7.id, su7.type, '', 'empty'] if i == 8: diff --git a/cadasta/spatial/managers.py b/cadasta/spatial/managers.py index bbd91de90..cf97c1d8d 100644 --- a/cadasta/spatial/managers.py +++ b/cadasta/spatial/managers.py @@ -1,4 +1,3 @@ -from . import models as spatial_models from .exceptions import SpatialRelationshipError from party.managers import BaseRelationshipManager @@ -11,25 +10,17 @@ def create(self, *args, **kwargs): su1 = kwargs['su1'] su2 = kwargs['su2'] project = kwargs['project'] - if (su1.geometry is not None and - su2.geometry is not None): - - if (kwargs['type'] == 'C' and - su1.geometry.geom_type == 'Polygon'): - result = spatial_models.SpatialUnit.objects.filter( - id=su1.id - ).filter( - geometry__contains=su2.geometry - ) - - if len(result) != 0: - self.check_project_constraints( - project=project, left=su1, right=su2) - return super().create(**kwargs) - else: - raise SpatialRelationshipError( - """That selected location is not geographically - contained within the parent location""") + rel_error = ( + kwargs['type'] == 'C' and + su1.geometry is not None and + su2.geometry is not None and + su1.geometry.geom_type == 'Polygon' and + not su1.geometry.contains(su2.geometry) + ) + if rel_error: + raise SpatialRelationshipError( + "That selected location is not geographically " + "contained within the parent location") self.check_project_constraints( project=project, left=su1, right=su2) return super().create(**kwargs) diff --git a/cadasta/spatial/migrations/0005_recalculate_area.py b/cadasta/spatial/migrations/0005_recalculate_area.py new file mode 100644 index 000000000..86848e36d --- /dev/null +++ b/cadasta/spatial/migrations/0005_recalculate_area.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-08-03 17:18 +from __future__ import unicode_literals + +import django.contrib.gis.db.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + TABLE_NAME = 'spatial_spatialunit' + FUNC_NAME = 'calculate_area' + TRIGGER_NAME = '{}_trigger'.format(FUNC_NAME) + + dependencies = [ + ('spatial', '0004_area_location_field'), + ] + + operations = [ + migrations.AlterField( + model_name='historicalspatialunit', + name='geometry', + field=django.contrib.gis.db.models.fields.GeometryField(geography=True, null=True, srid=4326), + ), + migrations.AlterField( + model_name='spatialunit', + name='geometry', + field=django.contrib.gis.db.models.fields.GeometryField(geography=True, null=True, srid=4326), + ), + migrations.RunSQL( + ( + "UPDATE {table} SET area = ST_Area(geography(geometry)) " + "WHERE geometrytype(geometry) LIKE '%POLYGON';" + ).format(table=TABLE_NAME), + reverse_sql=migrations.RunSQL.noop + ), + migrations.RunSQL( + """ + CREATE FUNCTION {func}() RETURNS trigger AS $$ + BEGIN + IF geometrytype(NEW.geometry) LIKE '%POLYGON' + THEN + NEW.area := (SELECT ST_Area(geography(NEW.geometry))); + ELSE + NEW.area := null; + END IF; + RETURN NEW; + END; + $$ language plpgsql; + + CREATE TRIGGER {trigger} + BEFORE INSERT OR UPDATE + ON {table} + FOR EACH ROW + EXECUTE PROCEDURE {func}(); + """.format(func=FUNC_NAME, trigger=TRIGGER_NAME, table=TABLE_NAME), + reverse_sql=""" + DROP TRIGGER {trigger} ON {table}; + DROP FUNCTION {func}(); + """.format(func=FUNC_NAME, trigger=TRIGGER_NAME, table=TABLE_NAME) + ) + ] diff --git a/cadasta/spatial/models.py b/cadasta/spatial/models.py index e07c146fe..1a5f14031 100644 --- a/cadasta/spatial/models.py +++ b/cadasta/spatial/models.py @@ -39,8 +39,9 @@ class SpatialUnit(ResourceModelMixin, RandomIDModel): # Spatial unit geometry is optional: some spatial units may only # have a textual description of their location. - geometry = GeometryField(null=True) + geometry = GeometryField(null=True, geography=True) + # Area, auto-calculated via trigger (see spatial/migrations/#0005) area = models.FloatField(null=True) # JSON attributes field with management of allowed members. @@ -170,20 +171,22 @@ def check_extent(sender, instance, **kwargs): # (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 + from django.contrib.gis.geos import Polygon if isinstance(geom, Polygon) and geom.empty: instance.geometry = None - if geom and not geom.empty: reassign_spatial_geometry(instance) -@receiver(models.signals.pre_save, sender=SpatialUnit) -def calculate_area(sender, instance, **kwargs): +@receiver(models.signals.post_save, sender=SpatialUnit) +def refresh_area(sender, instance, **kwargs): + """ Ensure DB-generated area is set on instance """ + from django.contrib.gis.geos import MultiPolygon, Polygon geom = instance.geometry - from django.contrib.gis.geos.polygon import Polygon - if geom and isinstance(geom, Polygon) and geom.valid: - instance.area = geom.transform(3857, clone=True).area + if not isinstance(geom, (MultiPolygon, Polygon)): + return + qs = type(instance)._default_manager.filter(id=instance.id) + instance.area = qs.values_list('area', flat=True)[0] @fix_model_for_attributes diff --git a/cadasta/spatial/tests/test_models.py b/cadasta/spatial/tests/test_models.py index e48877aea..ffb94d3b4 100644 --- a/cadasta/spatial/tests/test_models.py +++ b/cadasta/spatial/tests/test_models.py @@ -184,7 +184,7 @@ def test_ui_class_name(self): def test_area(self): su = SpatialUnitFactory.create(geometry='SRID=4326;POLYGON \ ((30 10, 20 20, 20 20, 10 20, 30 10))') - assert su.area == 642391915473.7279 + assert su.area == 554923434497.9 def test_area_no_geometry(self): su = SpatialUnitFactory.create()