diff --git a/README.rst b/README.rst index 8716994c..e56b226d 100644 --- a/README.rst +++ b/README.rst @@ -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 ----------------- diff --git a/rest_framework_gis/filters.py b/rest_framework_gis/filters.py index 1bff95cf..1253514c 100644 --- a/rest_framework_gis/filters.py +++ b/rest_framework_gis/filters.py @@ -38,7 +38,8 @@ 'GeometryFilter', 'GeoFilterSet', 'TMSTileFilter', - 'DistanceToPointFilter' + 'DistanceToPointFilter', + 'DistanceToPointOrderingFilter' ] @@ -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 @@ -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): @@ -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)) diff --git a/tests/django_restframework_gis_tests/test_filters.py b/tests/django_restframework_gis_tests/test_filters.py index 431e7f12..d9086bdf 100644 --- a/tests/django_restframework_gis_tests/test_filters.py +++ b/tests/django_restframework_gis_tests/test_filters.py @@ -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): """ @@ -269,6 +270,37 @@ 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)) + 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. """ @@ -443,3 +475,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 \ No newline at end of file diff --git a/tests/django_restframework_gis_tests/urls.py b/tests/django_restframework_gis_tests/urls.py index 1503963a..dc54e482 100644 --- a/tests/django_restframework_gis_tests/urls.py +++ b/tests/django_restframework_gis_tests/urls.py @@ -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'), ] diff --git a/tests/django_restframework_gis_tests/views.py b/tests/django_restframework_gis_tests/views.py index 6157d67d..97f21f0d 100644 --- a/tests/django_restframework_gis_tests/views.py +++ b/tests/django_restframework_gis_tests/views.py @@ -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