From 60929b687a5b32bfe882ab49c1c00ba70205bbd3 Mon Sep 17 00:00:00 2001 From: soheil Date: Mon, 22 Feb 2021 09:14:52 +0330 Subject: [PATCH 1/2] polygon field added --- README.md | 58 ++++++++++++ drf_extra_fields/geo_fields.py | 75 +++++++++++++++ tests/test_fields.py | 165 ++++++++++++++++++++++++++++++++- 3 files changed, 296 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e32c2e4..39ec56c 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,64 @@ 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: + + [ + [ + 51.778564453125, + 35.59925232772949 + ], + [ + 50.1470947265625, + 34.80929324176267 + ], + [ + 52.6080322265625, + 34.492975402501536 + ], + [ + 51.778564453125, + 35.59925232772949 + ] + ] + +**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}) +``` ## IntegerRangeField diff --git a/drf_extra_fields/geo_fields.py b/drf_extra_fields/geo_fields.py index f45e5cc..7d1278f 100644 --- a/drf_extra_fields/geo_fields.py +++ b/drf_extra_fields/geo_fields.py @@ -7,6 +7,7 @@ from rest_framework import serializers EMPTY_VALUES = (None, '', [], (), {}) +from django.contrib.gis.geos import Polygon class PointField(serializers.Field): @@ -76,3 +77,77 @@ 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 + + try: + new_value = [] + for item in value: + item = list(map(float, item)) + new_value.append(item) + except ValueError: + self.fail('invalid') + + try: + return Polygon(new_value) + except (GEOSException, ValueError, TypeError): + self.fail('invalid') + 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 = value.boundary.array + + + return value + diff --git a/tests/test_fields.py b/tests/test_fields.py index d0191e0..25dfdf2 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,174 @@ 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(self): + """ + Test for creating Polygon field in the server side + """ + 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_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'], + [ + [ + 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() From b70b35258ed5da3a08a2f003855271020ed6bfec Mon Sep 17 00:00:00 2001 From: soheil Date: Sun, 15 Aug 2021 21:16:36 +0430 Subject: [PATCH 2/2] added suport of polygon with interior ring --- README.md | 36 ++++---- drf_extra_fields/geo_fields.py | 62 +++++++++++--- tests/test_fields.py | 150 ++++++++++++++++++++++----------- 3 files changed, 171 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 2c64860..212c76a 100644 --- a/README.md +++ b/README.md @@ -133,25 +133,31 @@ 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 + [ + [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 diff --git a/drf_extra_fields/geo_fields.py b/drf_extra_fields/geo_fields.py index 7d1278f..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 _ @@ -15,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 } """ @@ -123,19 +123,53 @@ def to_internal_value(self, value): if value in EMPTY_VALUES and not self.required: return None + + polygon_type = None + try: - new_value = [] - for item in value: - item = list(map(float, item)) - new_value.append(item) - except ValueError: + 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: - return Polygon(new_value) - except (GEOSException, ValueError, TypeError): + 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') - self.fail('invalid') + def to_representation(self, value): @@ -146,8 +180,10 @@ def to_representation(self, value): return value if isinstance(value, GEOSGeometry): - value = value.boundary.array + value = json.loads(value.geojson)['coordinates'] - return value + return { + 'coordinates': value + } diff --git a/tests/test_fields.py b/tests/test_fields.py index d616376..21412ba 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -291,12 +291,46 @@ class StringPolygonSerializer(PolygonSerializer): class PolygonSerializerTest(TestCase): - def test_create(self): + 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 = [ + 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 @@ -314,6 +348,23 @@ def test_create(self): 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()) @@ -326,23 +377,23 @@ def test_remove_with_empty_string(self): """ now = datetime.datetime.now() polygon = [ - [ - 51.778564453125, - 35.59925232772949 - ], - [ - 50.1470947265625, - 34.80929324176267 - ], - [ - 52.6080322265625, - 34.492975402501536 - ], - [ - 51.778564453125, - 35.59925232772949 - ] + [ + 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()) @@ -378,39 +429,39 @@ def test_validation_error_with_none_closing_polygon(self): 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( + + 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 - ] + 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'], + saved_polygon = SavePolygon(polygon=polygon, created=now) + serializer = PolygonSerializer(saved_polygon) + self.assertEqual( + serializer.data['polygon']['coordinates'], + [ [ [ 51.778564453125, @@ -429,7 +480,8 @@ def test_serialization(self): 35.59925232772949 ] ] - ) + ] + ) class SavePoint(object):