Skip to content

Commit

Permalink
Merge pull request #2255 from onaio/add-geojson-simplestyle-spec-support
Browse files Browse the repository at this point in the history
Add geojson simplestyle-spec support
  • Loading branch information
KipSigei authored Jun 9, 2022
2 parents ac0a86a + 488dd5f commit 796a7a5
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 38 deletions.
54 changes: 54 additions & 0 deletions docs/data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,8 @@ Get a valid geojson value from the submissions

- ``geo_field`` - valid field that can be converted to a geojson (Point, LineString, Polygon).
- ``fields`` - additional comma separated values that are to be added to the properties section
- ``simple_style`` - boolean to enable or disable Mapbox Geojson simplestyle spec
- ``title`` - adds a title field and value to geojson properties section

**List all the geojson values for a submission**

Expand Down Expand Up @@ -1348,6 +1350,58 @@ Response
}
}

**List all the geojson values for a given form with simplestyle-spec enabled and title prop set**

.. raw:: html

<pre class="prettyprint">
<b>GET</b> /api/v1/data/<code>{form_pk}</code>.geojson?geo_field=<code>{name_of_field_on_form}</code>&simple_style=true&title=<code>{name_of_title_field_on_form}</code>
</pre>

Example
^^^^^^^^^
::

curl -X GET https://api.ona.io/api/v1/data/28058.geojson?geo_field=my_geoshape&style_spec=true

Response
^^^^^^^^^

**HTTP 200 OK**

Response
^^^^^^^^^
::

{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [36.787219, -1.294197]
},
"properties": {
"id": 6448,
"xform": 65,
"title": "my_field"
}
},
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [36.7872606, -1.2942131]
},
"properties": {
"id": 6447,
"xform": 65,
"title": "my_field"
}
}]
}

OSM
----

Expand Down
59 changes: 59 additions & 0 deletions onadata/apps/api/tests/viewsets/test_data_viewset.py
Original file line number Diff line number Diff line change
Expand Up @@ -1988,6 +1988,65 @@ def test_retry_on_operational_error(self, mock_paginate_queryset):
self.assertEqual(response.status_code, 200)
self.assertEqual(mock_paginate_queryset.call_count, 2)

def test_geojson_simple_style(self):
self._publish_submit_geojson()

dataid = self.xform.instances.all().order_by("id")[0].pk

data_get = {"geo_field": "location", "simple_style": "true"}

view = DataViewSet.as_view({"get": "retrieve"})

request = self.factory.get("/", data=data_get, **self.extra)

response = view(
request, pk=self.xform.pk, dataid=dataid, format="geojson")

self.assertEqual(response.status_code, 200)
# build out geojson simple style spec
test_loc = geojson.Feature(
geometry=geojson.Point((36.787219, -1.294197)),
properties={"xform": self.xform.pk, "id": dataid},
)
if "id" in test_loc:
test_loc.pop("id")

self.assertEqual(response.data, test_loc)

view = DataViewSet.as_view({"get": "list"})
request = self.factory.get("/", data=data_get, **self.extra)
response = view(request, pk=self.xform.pk, format="geojson")

self.assertEqual(response.status_code, 200)
self.assertEqual(response.data["type"], "FeatureCollection")
self.assertEqual(len(response.data["features"]), 4)
self.assertEqual(response.data["features"][0]["type"], "Feature")
self.assertEqual(
response.data["features"][0]["geometry"]["type"], "Point"
)

def test_geojson_simple_style_title_prop(self):
self._publish_submit_geojson()

dataid = self.xform.instances.all().order_by("id")[0].pk

data_get = {
"geo_field": "location",
"simple_style": "true",
"title": "shape"
}

view = DataViewSet.as_view({"get": "retrieve"})

request = self.factory.get("/", data=data_get, **self.extra)

response = view(
request, pk=self.xform.pk, dataid=dataid, format="geojson")

self.assertEqual(response.status_code, 200)
self.assertIn('title', response.data["properties"])
self.assertEqual('shape', response.data["properties"]["title"])

def test_geojson_geofield(self):
self._publish_submit_geojson()

Expand Down
91 changes: 53 additions & 38 deletions onadata/libs/serializers/geojson_serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from rest_framework_gis import serializers

from onadata.apps.logger.models.instance import Instance
from onadata.libs.utils.common_tools import str_to_bool


def create_feature(instance, geo_field, fields):
Expand All @@ -16,7 +17,7 @@ def create_feature(instance, geo_field, fields):
# Return an empty feature
return geojson.Feature()

points = data.get(geo_field).split(';')
points = data.get(geo_field).split(";")

if len(points) == 1:
# only one set of coordinates -> Point
Expand All @@ -29,7 +30,7 @@ def create_feature(instance, geo_field, fields):
point = pnt.split()
pnt_list.append((float(point[1]), float(point[0])))

if pnt_list[0] == pnt_list[len(pnt_list)-1]:
if pnt_list[0] == pnt_list[len(pnt_list) - 1]:
# First and last point are same -> Polygon
geometry = geojson.Polygon([pnt_list])
else:
Expand All @@ -46,27 +47,30 @@ def create_feature(instance, geo_field, fields):
else:
properties.update(data)

return geojson.Feature(geometry=geometry,
id=instance.pk,
properties=properties)
return geojson.Feature(geometry=geometry, id=instance.pk, properties=properties)


def is_polygon(point_list):
"""Takes a list of tuples and determines if it is a polygon"""
return (len(point_list) > 1 and
point_list[0] == point_list[len(point_list)-1])
return len(point_list) > 1 and point_list[0] == point_list[len(point_list) - 1]


def geometry_from_string(points):
"""Takes a string, returns a geometry object"""
def geometry_from_string(points, simple_style):
"""
Takes a string, returns a geometry object.
`simple_style` param allows building geojson
that adheres to the simplestyle-spec
"""

points = points.split(';')
pnt_list = [tuple(map(float, reversed(point.split()[:2])))
for point in points]
points = points.split(";")
pnt_list = [tuple(map(float, reversed(point.split()[:2]))) for point in points]

if len(pnt_list) == 1:
geometry = geojson.GeometryCollection(
[geojson.Point(pnt_list[0])])
geometry = (
geojson.Point(pnt_list[0])
if str_to_bool(simple_style)
else geojson.GeometryCollection([geojson.Point(pnt_list[0])])
)
elif is_polygon(pnt_list):
# First and last point are same -> Polygon
geometry = geojson.Polygon([pnt_list])
Expand All @@ -92,28 +96,35 @@ class GeoJsonSerializer(serializers.GeoFeatureModelSerializer):
class Meta:
model = Instance
geo_field = "geom"
lookup_field = 'pk'
lookup_field = "pk"
id_field = False
fields = ('id', 'xform')
fields = ("id", "xform")

def to_representation(self, obj):
ret = super(GeoJsonSerializer, self).to_representation(obj)
request = self.context.get('request')
ret = super().to_representation(obj)
request = self.context.get("request")

if obj and ret and 'properties' in ret and request is not None:
fields = request.query_params.get('fields')
if obj and ret and "properties" in ret and request is not None:
fields = request.query_params.get("fields")
if fields:
for field in fields.split(','):
ret['properties'][field] = obj.json.get(field)
for field in fields.split(","):
ret["properties"][field] = obj.json.get(field)

if obj and ret and request:
geo_field = request.query_params.get('geo_field')
geo_field = request.query_params.get("geo_field")
simple_style = request.query_params.get("simple_style")
title = request.query_params.get("title")
if geo_field:
if "properties" in ret and title:
ret["properties"]["title"] = title
points = obj.json.get(geo_field)
geometry = geometry_from_string(points) \
if points else geojson.Feature()
geometry = (
geometry_from_string(points, simple_style)
if points
else geojson.Feature()
)

ret['geometry'] = geometry
ret["geometry"] = geometry

return ret

Expand All @@ -126,29 +137,33 @@ class GeoJsonListSerializer(GeoJsonSerializer):
def to_representation(self, obj):

if obj is None:
return super(GeoJsonSerializer, self).to_representation(obj)

return super().to_representation(obj)
geo_field = None
fields = None

if 'fields' in obj and obj.get('fields'):
fields = obj.get('fields').split(',')
if "fields" in obj and obj.get("fields"):
fields = obj.get("fields").split(",")

if 'instances' in obj and obj.get('instances'):
insts = obj.get('instances')
if "instances" in obj and obj.get("instances"):
insts = obj.get("instances")

if 'geo_field' in obj and obj.get('geo_field'):
geo_field = obj.get('geo_field')
if "geo_field" in obj and obj.get("geo_field"):
geo_field = obj.get("geo_field")

# Get the instances from the form
instances = [inst for inst in insts[0].instances.all()]

if not geo_field:
return geojson.FeatureCollection(
[super(GeoJsonListSerializer, self).to_representation(
{'instance': ret, 'fields': obj.get('fields')})
for ret in instances])
[
super().to_representation(
{"instance": ret, "fields": obj.get("fields")}
)
for ret in instances
]
)

# Use the default serializer
return geojson.FeatureCollection(
[create_feature(ret, geo_field, fields)for ret in instances])
[create_feature(ret, geo_field, fields) for ret in instances]
)
23 changes: 23 additions & 0 deletions onadata/libs/tests/serializers/test_geojson_serializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from rest_framework.test import APIRequestFactory
from django.contrib.gis.geos import GeometryCollection, Point

from onadata.libs.serializers.geojson_serializer import GeoJsonSerializer
from onadata.apps.api.tests.viewsets.test_abstract_viewset import TestAbstractViewSet


class TestGeoJsonSerializer(TestAbstractViewSet):
def setUp(self):
self._login_user_and_profile()
self.factory = APIRequestFactory()

def test_geojson_serializer(self):
self._publish_xls_form_to_project()
data = {
"xform": self.xform.id,
"geom": GeometryCollection(Point([6.707548, 6.423264])),
}
request = self.factory.get("/", **self.extra)
serializer = GeoJsonSerializer(data=data, context={"request": request})
self.assertTrue(serializer.is_valid())
geometry = serializer.validated_data["geom"]
self.assertEqual(geometry.geojson, data.get("geom").geojson)

0 comments on commit 796a7a5

Please sign in to comment.