GeoDjango distance filter with distance value stored within model - query

django, geodjango, python

Solution

I think you're going to have to drop some SQL in to do what you want, as the GeoDjango helpers don't have a way of you making a suitable `Distance` object that is simultaneously a Django `F` object (ie field lookup). You don't say which database you're using, but here's how you do it with PostGIS:

Order.objects.all().extra(
    where=['ST_Distance(origin, ST_PointFromText(%s, 4326)) <= CAST(range AS double precision) / 1000'],
    params=[user.profile.geo_location.wkt]
)

Let's explain what's happening here, because it's pretty thorny. Going from the left:

- `.extra()` allows you to add extra fields and restrictions into a query; here we're adding a restriction

- `ST_Distance()` is the PostGIS function that the GeoDjango `distance_lte` operator converts into (from the Django documentation)

- `ST_PointFromText()` converts from WKT syntax into a PostGIS Geometry object

- `4326` is the default SRID for Django, but not for PostGIS so we have to specify it

- we have to `CAST` your field to double precision because you're using an integer

- then we have to divide by 1000 to convert from metres into kilometres, which I believe is what we need

- GeoDjango `Point` fields have an accessor `.wkt` which gives their WKT representation, which we needed earlier in our `ST_PointFromText()` call

Note that according to the PostGIS documentation, `ST_Distance()` doesn't use indexes, so you may want to investigate using `ST_DWithin()` instead (it's documented right after `ST_Distance()`).

Problem

I have an `Order` model, that has an `origin` PointField and a `range` IntegerField. Furthermore, there is an `UserProfile` model, which has a `geo_location` PointField. Now, I have an `User` instance, `user`. I want to select all `Orders`, whose distance between `Order.origin` and `user.userprofile.geo_location` is less then the value (meters) in the `Order.range` model field. So again, simplified models: ``` class Order(models.Model): origin = models.PointField() range = models.IntegerField(blank=True, default=10000) class UserProfile(models.Model): geo_location = models.PointField() ``` I've got this working (passing the distance statically): ``` >>> Order.objects.filter(origin__distance_lte=(user.profile.geo_location, D(m=3000))) ``` My next (unsuccessful) try was to use an F() expression to use the value from the `Order.range` field: ``` >>> Order.objects.filter(origin__distance_lte=(user.profile.geo_location, D(m=F('range')))) Traceback (most recent call last): File "<console>", line 1, in <module> File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/measure.py", line 165, in __init__ self.m, self._default_unit = self.default_units(kwargs) File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/measure.py", line 49, in default_units if not isinstance(value, float): value = float(value) TypeError: float() argument must be a string or a number ``` I think the problem is that the D() isn't ran lazily - I can understand that. So I tried to just take the raw value from the `range` field (integer) which I supposed to work, but: ``` >>> Order.objects.filter(origin__distance_lte=(user.profile.geo_location, F('range'))) Traceback (most recent call last): File "<console>", line 1, in <module> File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/query.py", line 69, in __repr__ data = list(self[:REPR_OUTPUT_SIZE + 1]) File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/query.py", line 84, in __len__ self._result_cache.extend(self._iter) File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/query.py", line 273, in iterator for row in compiler.results_iter(): File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 680, in results_iter for rows in self.execute_sql(MULTI): File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 725, in execute_sql sql, params = self.as_sql() File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/compiler.py", line 68, in as_sql where, w_params = self.query.where.as_sql(qn=qn, connection=self.connection) File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/where.py", line 92, in as_sql sql, params = child.as_sql(qn=qn, connection=connection) File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/db/models/sql/where.py", line 95, in as_sql sql, params = self.make_atom(child, qn, connection) File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/db/models/sql/where.py", line 47, in make_atom spatial_sql = connection.ops.spatial_lookup_sql(data, lookup_type, params_or_value, lvalue.field, qn) File "/Users/danger/devel/.virtualenvs/proj/lib/python2.7/site-packages/django/contrib/gis/db/backends/postgis/operations.py", line 531, in spatial_lookup_sql raise ValueError('Argument type should be %s, got %s instead.' % (arg_type, type(value[1]))) ValueError: Argument type should be (<class 'decimal.Decimal'>, <class 'django.contrib.gis.measure.Distance'>, <type 'float'>, <type 'int'>, <type 'long'>), got <class 'django.db.models.expressions.F'> instead. ``` So how can I accomplish what I am trying to? Any help is appreciated!

Original source

Related problems