Skip to content

Commit

Permalink
Correctly calculate area (fixes #1689)
Browse files Browse the repository at this point in the history
  • Loading branch information
Anthony Lukach committed Aug 3, 2017
1 parent 60a42c9 commit 219365c
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 33 deletions.
7 changes: 3 additions & 4 deletions cadasta/organization/tests/test_downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
31 changes: 11 additions & 20 deletions cadasta/spatial/managers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from . import models as spatial_models
from .exceptions import SpatialRelationshipError
from party.managers import BaseRelationshipManager

Expand All @@ -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)
62 changes: 62 additions & 0 deletions cadasta/spatial/migrations/0005_recalculate_area.py
Original file line number Diff line number Diff line change
@@ -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)
)
]
19 changes: 11 additions & 8 deletions cadasta/spatial/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]

This comment has been minimized.

Copy link
@alukach

alukach Aug 8, 2017

Contributor

To be honest, I think this was overkill. Running instance.refresh_from_db() would likely be clearer.



@fix_model_for_attributes
Expand Down
2 changes: 1 addition & 1 deletion cadasta/spatial/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 219365c

Please sign in to comment.