Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic calculation of a polygon area #1534

Merged
merged 18 commits into from
Jul 20, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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