diff --git a/README.md b/README.md index 7529058..212c76a 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,70 @@ point = { } serializer = PointFieldSerializer(data={'created': now, 'point': point}) ``` +## PolygonField + +Polygon field for GeoDjango + +**Signature:** `PolygonField()` + - It takes a list of orderly pair arrays, representing points which all together are supposed to make a polygon. example: + + A polygon without inner ring represented in 2d array format + + [ + [51.778564453125, 35.59925232772949], [50.1470947265625, 34.80929324176267], + [52.6080322265625, 34.492975402501536],[51.778564453125, 35.59925232772949] + ] + + The same polygon represented in 3d array format + + [ + [ + [51.778564453125, 35.59925232772949], [50.1470947265625, 34.80929324176267], + [52.6080322265625, 34.492975402501536],[51.778564453125, 35.59925232772949] + ] + ] + + A polygon with inner ring (first element representing exterior and the second one interior ring) + + + [ + [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ], + [ [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ] ] + ] + + +**Example:** + +```python +# serializer + +from drf_extra_fields.geo_fields import PolygonField + +class PolygonFieldSerializer(serializers.Serializer): + polygon = PolygonField(required=False) + created = serializers.DateTimeField() + +# use the serializer +polygon = [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] +serializer = PolygonFieldSerializer(data={'created': now, 'polygon': polygon}) +``` # RangeField diff --git a/drf_extra_fields/geo_fields.py b/drf_extra_fields/geo_fields.py index f45e5cc..eea7b45 100644 --- a/drf_extra_fields/geo_fields.py +++ b/drf_extra_fields/geo_fields.py @@ -1,5 +1,5 @@ import json -from django.contrib.gis.geos import GEOSGeometry +from django.contrib.gis.geos import GEOSGeometry, polygon from django.contrib.gis.geos.error import GEOSException from django.utils.encoding import smart_str from django.utils.translation import gettext_lazy as _ @@ -7,6 +7,7 @@ from rest_framework import serializers EMPTY_VALUES = (None, '', [], (), {}) +from django.contrib.gis.geos import Polygon class PointField(serializers.Field): @@ -14,8 +15,8 @@ class PointField(serializers.Field): A field for handling GeoDjango Point fields as a json format. Expected input format: { - "latitude": 49.8782482189424, - "longitude": 24.452545489 + "latitude": 49.8782482189424, + "longitude": 24.452545489 } """ @@ -76,3 +77,113 @@ def to_representation(self, value): value['latitude'] = smart_str(value.pop('latitude')) return value + + + +class PolygonField(serializers.Field): + """ + A field for handling GeoDjango PolyGone fields as a array format. + Expected input format: + { + [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] + } + + """ + type_name = 'PolygonField' + type_label = 'polygon' + + default_error_messages = { + 'invalid': _('Enter a valid polygon.'), + } + + def __init__(self, *args, **kwargs): + super(PolygonField, self).__init__(*args, **kwargs) + + def to_internal_value(self, value): + """ + Parse array data and return a polygon object + """ + if value in EMPTY_VALUES and not self.required: + return None + + + polygon_type = None + + try: + new_value = [] + + if len(value)>2: + # a polygon without the ring in a 2-d array + for item in value: + item = list(map(float, item)) + new_value.append(item) + + elif len(value)==2: + # a polygon with inner ring + polygon_type = 'with_inner_ring' + + for i in range(2): + # a loop of 2 iterations. one per each ring + ring_array = [] + for item in value[i]: + item = list(map(float, item)) + ring_array.append(item) + new_value.append(ring_array) + + elif len(value)==1: + # a polygon without the ring in a 3-d array. not supported by django, should be converted to 2-d + for item in value[0]: + item = list(map(float, item)) + new_value.append(item) + + except ValueError as e: + print(e) + self.fail('invalid') + + try: + if polygon_type=='with_inner_ring': + # for polygons with inner ring you should pass the exterior and interior ring seperated with comma to Polygon + return Polygon(new_value[0], new_value[1]) + + else: + return Polygon(new_value) + + except (GEOSException, ValueError, TypeError) as e: + print(e) + print(new_value) + self.fail('invalid') + + + + def to_representation(self, value): + """ + Transform Polygon object to array of arrays. + """ + if value is None: + return value + + if isinstance(value, GEOSGeometry): + value = json.loads(value.geojson)['coordinates'] + + + return { + 'coordinates': value + } + diff --git a/tests/test_fields.py b/tests/test_fields.py index 86b0591..21412ba 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -26,7 +26,7 @@ LowercaseEmailField, DecimalRangeField, ) -from drf_extra_fields.geo_fields import PointField +from drf_extra_fields.geo_fields import PointField, PolygonField from drf_extra_fields import compat UNDETECTABLE_BY_IMGHDR_SAMPLE = """data:image/jpeg;base64, @@ -269,13 +269,226 @@ def test_download(self): finally: os.remove('im.jpg') +class SavePolygon(object): + def __init__(self, polygon=None, created=None): + self.polygon = polygon + self.created = created or datetime.datetime.now() + +class PolygonSerializer(serializers.Serializer): + polygon = PolygonField(required=False) + created = serializers.DateTimeField() + + def update(self, instance, validated_data): + instance.polygon = validated_data['polygon'] + return instance + + def create(self, validated_data): + return SavePolygon(**validated_data) + + +class StringPolygonSerializer(PolygonSerializer): + polygon = PolygonField(required=False) + + +class PolygonSerializerTest(TestCase): + def test_create_simple_polygon_2d_array(self): + """ + Test for creating Polygon field in the server side + The feeded array is 2-d + The polygon does not have inner ring + """ + now = datetime.datetime.now() + polygon = [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] + serializer = PolygonSerializer(data={'created': now, 'polygon': polygon}) + saved_polygon = SavePolygon(polygon=polygon, created=now) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data['created'], saved_polygon.created) + self.assertIsNone(serializer.validated_data['polygon'].srid) + + def test_create_simple_polygon_3d_array(self): + """ + Test for creating Polygon field in the server side + The feeded array is 3-d + The polygon does not have inner ring + """ + now = datetime.datetime.now() + polygon = [ + [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] + ] + serializer = PolygonSerializer(data={'created': now, 'polygon': polygon}) + saved_polygon = SavePolygon(polygon=polygon, created=now) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data['created'], saved_polygon.created) + self.assertIsNone(serializer.validated_data['polygon'].srid) + + def test_create_polygon_with_inner_ring(self): + """ + Test for creating Polygon field in the server side + The polygon does have inner ring + """ + now = datetime.datetime.now() + polygon = [ + [ [ 0 , 0 ] , [ 3 , 6 ] , [ 6 , 1 ] , [ 0 , 0 ] ], + [ [ 2 , 2 ] , [ 3 , 3 ] , [ 4 , 2 ] , [ 2 , 2 ] ] + ] + serializer = PolygonSerializer(data={'created': now, 'polygon': polygon}) + saved_polygon = SavePolygon(polygon=polygon, created=now) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data['created'], saved_polygon.created) + self.assertIsNone(serializer.validated_data['polygon'].srid) + + def test_remove_with_empty_string(self): + """ + Passing empty string as data should cause polygon to be removed + """ + now = datetime.datetime.now() + polygon = [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] + saved_polygon = SavePolygon(polygon=polygon, created=now) + serializer = PolygonSerializer(data={'created': now, 'polygon': ''}) + self.assertTrue(serializer.is_valid()) + self.assertEqual(serializer.validated_data['created'], saved_polygon.created) + self.assertIsNone(serializer.validated_data['polygon']) + + def test_validation_error_with_wrong_format(self): + """ + Passing data with the wrong format should cause validation error + """ + now = datetime.datetime.now() + serializer = PolygonSerializer(data={'created': now, 'polygon': '{[22,30], [23,31]}'}) + self.assertFalse(serializer.is_valid()) + + def test_validation_error_with_none_closing_polygon(self): + """ + Passing data which the last point is not equal to the first point (causing an open polygon) should cause validation error + """ + polygon = [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ] + ] + now = datetime.datetime.now() + serializer = PolygonSerializer(data={'created': now, 'polygon': polygon}) + self.assertFalse(serializer.is_valid()) + + def test_serialization(self): + """ + Regular JSON serialization should output float values + """ + from django.contrib.gis.geos import Polygon + now = datetime.datetime.now() + polygon = Polygon( + [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] + ) + + saved_polygon = SavePolygon(polygon=polygon, created=now) + serializer = PolygonSerializer(saved_polygon) + self.assertEqual( + serializer.data['polygon']['coordinates'], + [ + [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] + ] + ) + class SavePoint(object): def __init__(self, point=None, created=None): self.point = point self.created = created or datetime.datetime.now() - class PointSerializer(serializers.Serializer): point = PointField(required=False) created = serializers.DateTimeField()