Skip to content

Commit

Permalink
Implement DistanceToPointOrderingFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
franciscouzo committed Aug 12, 2020
1 parent a31ffa1 commit f2d987f
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 3 deletions.
25 changes: 25 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,31 @@ which is indexed in degrees, the 'distance' will be interpreted as degrees. Set
to 'True' in order to convert an input distance in meters to degrees. This conversion is approximate, and the errors
at latitudes > 60 degrees are > 25%.

DistanceToPointOrderingFilter
~~~~~~~~~~~~~~~~~~~~~

Provides a ``DistanceToPointOrderingFilter``, which is a subclass of DRF
``DistanceToPointFilter``. Orders a queryset by distance to a given point.

``views.py:``

.. code-block:: python
from rest_framework_gis.filters import DistanceToPointOrderingFilter
class LocationList(ListAPIView):
queryset = models.Location.objects.all()
serializer_class = serializers.LocationSerializer
distance_ordering_filter_field = 'geometry'
filter_backends = (DistanceToPointOrderingFilter, )
We can then order the results by passing a point in (lon, lat) format in the URL.

eg:.
``/location/?point=-122.4862,37.7694&format=json``
will order the results by the distance to the point (-122.4862, 37.7694).

Running the tests
-----------------

Expand Down
25 changes: 22 additions & 3 deletions rest_framework_gis/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
'GeometryFilter',
'GeoFilterSet',
'TMSTileFilter',
'DistanceToPointFilter'
'DistanceToPointFilter',
'DistanceToPointOrderingFilter'
]


Expand Down Expand Up @@ -123,7 +124,7 @@ class DistanceToPointFilter(BaseFilterBackend):
dist_param = 'dist'
point_param = 'point' # The URL query parameter which contains the

def get_filter_point(self, request):
def get_filter_point(self, request, **kwargs):
point_string = request.query_params.get(self.point_param, None)
if not point_string:
return None
Expand All @@ -133,7 +134,7 @@ def get_filter_point(self, request):
except ValueError:
raise ParseError('Invalid geometry string supplied for parameter {0}'.format(self.point_param))

p = Point(x, y)
p = Point(x, y, **kwargs)
return p

def dist_to_deg(self, distance, latitude):
Expand Down Expand Up @@ -188,3 +189,21 @@ def filter_queryset(self, request, queryset, view):
dist = self.dist_to_deg(dist, point[1])

return queryset.filter(Q(**{'%s__%s' % (filter_field, geoDjango_filter): (point, dist)}))


class DistanceToPointOrderingFilter(DistanceToPointFilter):
srid = 4326

def filter_queryset(self, request, queryset, view):
from django.contrib.gis.db.models.functions import GeometryDistance

filter_field = getattr(view, 'distance_ordering_filter_field', None)

if not filter_field:
return queryset

point = self.get_filter_point(request, srid=self.srid)
if not point:
return queryset

return queryset.order_by(GeometryDistance(filter_field, point))
47 changes: 47 additions & 0 deletions tests/django_restframework_gis_tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def setUp(self):
self.location_within_distance_of_point_list_url = reverse('api_geojson_location_list_within_distance_of_point_filter')
self.location_within_degrees_of_point_list_url = reverse('api_geojson_location_list_within_degrees_of_point_filter')
self.geojson_contained_in_geometry = reverse('api_geojson_contained_in_geometry')
self.location_order_distance_to_point = reverse('api_geojson_location_order_distance_to_point_list_filter')

def test_inBBOXFilter_filtering(self):
"""
Expand Down Expand Up @@ -269,6 +270,38 @@ def test_DistanceToPointFilter_filtering(self):
for result in response.data['features']:
self.assertEqual(result['properties']['name'] in (treasure_island.name), True)

@skipIf(has_spatialite, 'Skipped test for spatialite backend: missing feature "GeometryDistance"')
def test_DistanceToPointOrderingFilter_filtering(self):
"""
Checks that the DistanceOrderingFilter returns the objects in the correct order
given the geometry defined by the URL parameters
"""
self.assertEqual(Location.objects.count(), 0)

url_params = '?point=hello&format=json'
response = self.client.get('%s%s' % (self.location_order_distance_to_point, url_params))
print(response.data)
self.assertEqual(response.status_code, 400)

Location.objects.create(name='Houston', geometry='SRID=4326;POINT (-95.363151 29.763374)')
Location.objects.create(name='Dallas', geometry='SRID=4326;POINT (-96.801611 32.782057)')
Location.objects.create(name='Oklahoma City', geometry='SRID=4326;POINT (-97.521157 34.464642)')
Location.objects.create(name='Wellington', geometry='SRID=4326;POINT (174.783117 -41.315268)')
Location.objects.create(name='Pueblo', geometry='SRID=4326;POINT (-104.609252 38.255001)')
Location.objects.create(name='Lawrence', geometry='SRID=4326;POINT (-95.235060 38.971823)')
Location.objects.create(name='Chicago', geometry='SRID=4326;POINT (-87.650175 41.850385)')
Location.objects.create(name='Victoria', geometry='SRID=4326;POINT (-123.305196 48.462611)')

point = [-90, 40]
url_params = '?point=%i,%i&format=json' % (point[0], point[1])

response = self.client.get('%s%s' % (self.location_order_distance_to_point, url_params))
self.assertEqual(len(response.data['features']), 8)
self.assertEqual(
[city['properties']['name'] for city in response.data['features']],
['Chicago', 'Lawrence', 'Oklahoma City', 'Dallas', 'Houston', 'Pueblo', 'Victoria', 'Wellington']
)

@skipIf(has_spatialite, 'Skipped test for spatialite backend: missing feature "contains_properly"')
def test_GeometryField_filtering(self):
""" Checks that the GeometryField allows sane filtering. """
Expand Down Expand Up @@ -443,3 +476,17 @@ def test_DistanceToPointFilter_ValueError_distance(self):
url_params = '?dist=wrong&point=12.0,42.0&format=json'
response = self.client.get('%s%s' % (self.location_within_distance_of_point_list_url, url_params))
self.assertEqual(response.data['detail'], 'Invalid distance string supplied for parameter dist')

def test_DistanceToPointOrderingFilter_filtering_none(self):
url_params = '?point=&format=json'
response = self.client.get('%s%s' % (self.location_order_distance_to_point, url_params))
self.assertDictEqual(response.data, {'type': 'FeatureCollection', 'features': []})

def test_DistanceToPointOrderingFilter_ordering_filter_field_none(self):
from .views import GeojsonLocationOrderDistanceToPointList as view
original_value = view.distance_ordering_filter_field
view.distance_ordering_filter_field = None
url_params = '?point=&format=json'
response = self.client.get('%s%s' % (self.location_order_distance_to_point, url_params))
self.assertDictEqual(response.data, {'type': 'FeatureCollection', 'features': []})
view.distance_ordering_filter_field = original_value
4 changes: 4 additions & 0 deletions tests/django_restframework_gis_tests/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,8 @@
r'^filters/within_degrees_of_point$',
views.geojson_location_within_degrees_of_point_list,
name='api_geojson_location_list_within_degrees_of_point_filter'),
url(
r'^filters/order_distance_to_point$',
views.geojson_location_order_distance_to_point_list,
name='api_geojson_location_order_distance_to_point_list_filter'),
]
10 changes: 10 additions & 0 deletions tests/django_restframework_gis_tests/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ class GeojsonLocationWithinDegreesOfPointList(GeojsonLocationWithinDistanceOfPoi
geojson_location_within_degrees_of_point_list = GeojsonLocationWithinDegreesOfPointList.as_view()


class GeojsonLocationOrderDistanceToPointList(generics.ListAPIView):
model = Location
serializer_class = LocationGeoFeatureSerializer
queryset = Location.objects.all()
distance_ordering_filter_field = 'geometry'
filter_backends = (DistanceToPointOrderingFilter,)

geojson_location_order_distance_to_point_list = GeojsonLocationOrderDistanceToPointList.as_view()


class GeojsonLocationDetails(generics.RetrieveUpdateDestroyAPIView):
model = Location
serializer_class = LocationGeoFeatureSerializer
Expand Down

0 comments on commit f2d987f

Please sign in to comment.