Skip to content

Commit

Permalink
Implement DistanceToPointOrderingFilter
Browse files Browse the repository at this point in the history
  • Loading branch information
franciscouzo committed Aug 19, 2020
1 parent a31ffa1 commit dfaf92e
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 3 deletions.
28 changes: 28 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,34 @@ 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``, **available on Django >= 3.0**, which is a subclass of ``DistanceToPointFilter``.
Orders a queryset by distance to a given point, from the nearest to the most distant 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).

We can also reverse the order of the results by passing `order=desc`:
``/location/?point=-122.4862,37.769&order=desc4&format=json``

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

Expand Down
37 changes: 34 additions & 3 deletions rest_framework_gis/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,21 @@
else:
gis_lookups = BaseSpatialField.get_lookups()

try:
# Django >= 3.0
from django.contrib.gis.db.models.functions import GeometryDistance
except ImportError:
GeometryDistance = None


__all__ = [
'InBBoxFilter',
'InBBOXFilter',
'GeometryFilter',
'GeoFilterSet',
'TMSTileFilter',
'DistanceToPointFilter'
'DistanceToPointFilter',
'DistanceToPointOrderingFilter'
]


Expand Down Expand Up @@ -123,7 +130,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 +140,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 +195,27 @@ 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
order_param = 'order'

def filter_queryset(self, request, queryset, view):
if not GeometryDistance:
raise ValueError('GeometryDistance not available on this version of django')

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

order = request.query_params.get(self.order_param)
if order == 'desc':
return queryset.order_by(-GeometryDistance(filter_field, point))
else:
return queryset.order_by(GeometryDistance(filter_field, point))
68 changes: 68 additions & 0 deletions tests/django_restframework_gis_tests/test_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,15 @@
from django.core.urlresolvers import reverse

from .models import Location
from .views import GeojsonLocationOrderDistanceToPointList

has_spatialite = settings.DATABASES['default']['ENGINE'] == 'django.contrib.gis.db.backends.spatialite'

try:
from django.contrib.gis.db.models.functions import GeometryDistance
has_geometry_distance = True
except ImportError:
has_geometry_distance = False

class TestRestFrameworkGisFilters(TestCase):
"""
Expand All @@ -28,6 +34,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 +276,46 @@ 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"')
@skipIf(not has_geometry_distance, 'Skipped test for Django < 3.0: 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))
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']
)

url_params = '?point=%i,%i&order=desc&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']],
['Wellington', 'Victoria', 'Pueblo', 'Houston', 'Dallas', 'Oklahoma City', 'Lawrence', 'Chicago']
)

@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 +490,24 @@ 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')

@skipIf(not has_geometry_distance, 'Skipped test for Django < 3.0: missing feature "GeometryDistance"')
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': []})

@skipIf(not has_geometry_distance, 'Skipped test for Django < 3.0: missing feature "GeometryDistance"')
def test_DistanceToPointOrderingFilter_ordering_filter_field_none(self):
original_value = GeojsonLocationOrderDistanceToPointList.distance_ordering_filter_field
GeojsonLocationOrderDistanceToPointList.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': []})
GeojsonLocationOrderDistanceToPointList.distance_ordering_filter_field = original_value

@skipIf(has_geometry_distance, 'Skipped test for Django >= 3.0')
def test_DistanceToPointOrderingFilter_not_available(self):
url_params = '?point=12,42&format=json'
with self.assertRaises(ValueError):
self.client.get('%s%s' % (self.location_order_distance_to_point, url_params))
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 dfaf92e

Please sign in to comment.