Skip to content

Commit

Permalink
Automatic calculation of a polygon area (#1534)
Browse files Browse the repository at this point in the history
* Adding geometry_details field to SpatialUnit Model, add receiver signal to calculate geometry details, m2,ft2,ha,ac

* Update changes for test and lint

* Working on fixing tests

* Adding Tests for both spatial model and xforms model. Also made sure that areas are only calculated for polygons

* Added area_metric_units, area_imperial_units to the spatial/view/default context for display in location_details

* Added the total area in ha to the project stats

* Removing model calculation, only calculting in m2.. Will replace for view conversion

* Adding test for location_details context view

* Fixing linting issues

* Added proper filter function for area conversion, added tests and UI for both project dashboard and location details

* Added to geometry_details area to the export data, shp, excel with tests

* Removing unwanted print statment

* PR changes, removed model migration, model test, change location for postgis query and project sum, updated tests and linting

* PR Changes, added SpatialUnit property for area to eliminate redundant code, made code style changes and added updated test based on new property

* Adding area as a float to the spatial unit

* Removed unnecessary comments

* Removed unnessesary code and fixed migration for efficiency

* Clean
  • Loading branch information
jnordling authored and amplifi committed Jul 20, 2017
1 parent f843b0a commit 76495ef
Show file tree
Hide file tree
Showing 15 changed files with 125 additions and 15 deletions.
21 changes: 21 additions & 0 deletions cadasta/core/templatetags/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,24 @@ def set_parsley_sanitize(field):
if type(field.field) == CharField:
field.field.widget.attrs['data-parsley-sanitize'] = '1'
return field


@register.filter(name='format_area_metric_units')
def set_format_area_metric_units(area):
area = float(area)
if area < 1000:
return format(area, '.2f') + ' m<sup>2</sup>'
else:
ha = area/10000
return format(ha, '.2f') + ' ha'


@register.filter(name='format_area_imperial_units')
def set_format_area_imperial_units(area):
area = float(area)
area_ft2 = area * 10.764
if area_ft2 < 4356:
return format(area_ft2, '.2f') + ' ft<sup>2</sup>'
else:
ac = area * 0.00024711
return format(ac, '.2f') + ' ac'
20 changes: 20 additions & 0 deletions cadasta/core/tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,23 @@ def test_set_parsley_sanitize(self):
bf = forms.BoundField(form, field, 'name')
filters.set_parsley_sanitize(bf)
assert bf.field.widget.attrs == {'data-parsley-sanitize': '1'}

def test_set_format_area_imperial_units(self):
area1 = '10004.494'
area2 = '10'
expected_value1 = '2.47 ac'
expected_value2 = '107.64 ft<sup>2</sup>'
imperial_units1 = filters.set_format_area_imperial_units(area1)
imperial_units2 = filters.set_format_area_imperial_units(area2)
assert imperial_units1 == expected_value1
assert imperial_units2 == expected_value2

def test_set_format_area_metric_units(self):
area1 = '10004.494'
area2 = '999'
expected_value1 = '1.00 ha'
expected_value2 = '999.00 m<sup>2</sup>'
metric_units1 = filters.set_format_area_metric_units(area1)
metric_units2 = filters.set_format_area_metric_units(area2)
assert metric_units1 == expected_value1
assert metric_units2 == expected_value2
2 changes: 1 addition & 1 deletion cadasta/organization/download/shape.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def write_features(self, ds, filename):

content_type = ContentType.objects.get(app_label='spatial',
model='spatialunit')
model_attrs = ('id', 'type')
model_attrs = ('id', 'type', 'area')

self.write_items(
filename, spatial_units, content_type, model_attrs)
Expand Down
2 changes: 1 addition & 1 deletion cadasta/organization/download/xls.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def write_locations(self):
content_type = ContentType.objects.get(app_label='spatial',
model='spatialunit')
self.write_items(worksheet, locations, content_type,
['id', 'geometry.ewkt', 'type'])
['id', 'geometry.ewkt', 'type', 'area'])

def write_parties(self):
parties = self.project.parties.all()
Expand Down
21 changes: 12 additions & 9 deletions cadasta/organization/tests/test_downloads.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,23 +468,26 @@ def test_write_features(self):
csvreader = csv.reader(csvfile)
for i, row in enumerate(csvreader):
if i == 0:
assert row == ['id', 'type', 'geom_type']
head = ['id', 'type', 'area', 'geom_type']
assert row == head
if i == 1:
assert row == [su1.id, su1.type, 'point']
assert row == [su1.id, su1.type, '', 'point']
if i == 2:
assert row == [su2.id, su2.type, 'linestring']
assert row == [su2.id, su2.type, '', 'linestring']
if i == 3:
assert row == [su3.id, su3.type, 'polygon']
area = format(su3.area, '.2f')
assert row == [su3.id, su3.type, area, 'polygon']
if i == 4:
assert row == [su4.id, su4.type, 'multipoint']
assert row == [su4.id, su4.type, '', 'multipoint']
if i == 5:
assert row == [su5.id, su5.type, 'multilinestring']
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, '', 'multipolygon']
if i == 7:
assert row == [su7.id, su7.type, 'empty']
assert row == [su7.id, su7.type, '', 'empty']
if i == 8:
assert row == [su8.id, su8.type, 'none']
assert row == [su8.id, su8.type, '', 'none']

# remove this so other tests pass
os.remove(filename)
Expand Down
6 changes: 4 additions & 2 deletions cadasta/organization/tests/test_views_default_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,7 +422,8 @@ def test_get_archived_project_with_org_admin(self):
assert response.content == expected

def test_get_with_overview_stats(self):
SpatialUnitFactory.create(project=self.project)
SpatialUnitFactory.create(project=self.project, geometry='SRID=4326;POLYGON \
((30 10, 20 20, 20 20, 10 20, 30 10))')
PartyFactory.create(project=self.project)
ResourceFactory.create(project=self.project)
ResourceFactory.create(project=self.project, archived=True)
Expand All @@ -431,7 +432,8 @@ def test_get_with_overview_stats(self):
assert response.content == self.render_content(has_content=True,
num_locations=1,
num_parties=1,
num_resources=1)
num_resources=1,
total_area=642391915500)

def test_get_with_labels(self):
file = self.get_file(
Expand Down
4 changes: 4 additions & 0 deletions cadasta/organization/views/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@ def get_context_data(self, **kwargs):
members = OrderedDict(sorted(m.items(), key=lambda t: t[0]))

num_locations = self.object.spatial_units.count()
total_area = self.object.spatial_units.aggregate(
Sum('area'))['area__sum']
if total_area:
context['total_area'] = total_area
num_parties = self.object.parties.count()
num_resources = self.object.resource_set.filter(archived=False).count()
context['has_content'] = (
Expand Down
37 changes: 37 additions & 0 deletions cadasta/spatial/migrations/0004_area_location_field.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-05-31 21:21
from __future__ import unicode_literals

from django.db import migrations, models
from django.contrib.gis.db.models.functions import Area, Transform


def calculate_area_field(apps, schema_editor):
SpatialUnit = apps.get_model('spatial', 'SpatialUnit')
SpatialUnit.objects.exclude(geometry=None).extra(
where=["geometrytype(geometry) LIKE 'POLYGON'"]).update(
area=Area(Transform('geometry', 3857)))


class Migration(migrations.Migration):

dependencies = [
('spatial', '0003_custom_location_types'),
]

operations = [
migrations.AddField(
model_name='historicalspatialunit',
name='area',
field=models.FloatField(null=True),
),
migrations.AddField(
model_name='spatialunit',
name='area',
field=models.FloatField(null=True),
),
migrations.RunPython(
calculate_area_field,
reverse_code=migrations.RunPython.noop
),
]
10 changes: 10 additions & 0 deletions cadasta/spatial/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ class SpatialUnit(ResourceModelMixin, RandomIDModel):
# have a textual description of their location.
geometry = GeometryField(null=True)

area = models.FloatField(null=True)

# JSON attributes field with management of allowed members.
attributes = JSONAttributeField(default={})

Expand Down Expand Up @@ -176,6 +178,14 @@ def check_extent(sender, instance, **kwargs):
reassign_spatial_geometry(instance)


@receiver(models.signals.pre_save, sender=SpatialUnit)
def calculate_area(sender, instance, **kwargs):
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


@fix_model_for_attributes
@permissioned_model
class SpatialRelationship(RandomIDModel):
Expand Down
9 changes: 9 additions & 0 deletions cadasta/spatial/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,15 @@ def test_ui_class_name(self):
su = SpatialUnitFactory.create()
assert su.ui_class_name == "Location"

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

def test_area_no_geometry(self):
su = SpatialUnitFactory.create()
assert su.area is None

def test_get_absolute_url(self):
su = SpatialUnitFactory.create()
assert su.get_absolute_url() == (
Expand Down
4 changes: 3 additions & 1 deletion cadasta/spatial/tests/test_views_default.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ def setup_models(self):
'fname': 'test',
'fname_2': 'two',
'fname_3': ['one', 'three']
})
},
geometry='SRID=4326;POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))')

def setup_template_context(self):
return {
Expand All @@ -353,6 +354,7 @@ def setup_template_context(self):
'is_allowed_add_location': True,
'is_allowed_edit_location': True,
'is_allowed_delete_location': True,
'area': '7700007175103.63'
}

def setup_url_kwargs(self):
Expand Down
1 change: 1 addition & 0 deletions cadasta/spatial/views/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ def get_context_data(self, *args, **kwargs):
pass

location = context['location']
context['area'] = location.area
user = self.request.user
context['is_allowed_edit_location'] = user.has_perm('spatial.update',
location)
Expand Down
1 change: 1 addition & 0 deletions cadasta/templates/organization/project_dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{% load i18n %}
{% load leaflet_tags %}
{% load staticfiles %}
{% load filters %}

{% block extra_head %}
{% leaflet_css plugins="groupedlayercontrol"%}
Expand Down
1 change: 1 addition & 0 deletions cadasta/templates/spatial/location_detail.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{% extends "spatial/location_wrapper.html" %}
{% load i18n %}
{% load widget_tweaks %}
{% load filters %}

{% block page_title %}{% trans "Location detail" %} | {% endblock %}

Expand Down
1 change: 0 additions & 1 deletion cadasta/xforms/tests/test_views_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,6 @@ def test_point_upload(self):
data = self._submission(form='submission_missing_semi')
response = self.request(method='POST', user=self.user, post_data=data,
content_type='multipart/form-data')

geom = SpatialUnit.objects.get(attributes={'name': 'Missing Semi'})
assert response.status_code == 201
assert geom.geometry.geom_type == 'Point'
Expand Down

0 comments on commit 76495ef

Please sign in to comment.