3

This is an unexpected behavior that came up on the road to solve this problem with @Nargiza: 3d distance calculations with GeoDjango.

Following the Django docs on the Distance function:

Accepts two geographic fields or expressions and returns the distance between them, as a Distance object.

And from a Distance object, we can get the distance in each and every one of the supported units.

But:

Let model be:

class MyModel(models.Model):
    ...
    coordinates = models.PointField()

then the following:

p1 = MyModel.objects.get(id=1).coordinates
p2 = MyModel.objects.get(id=2).coordinates
d = Distance(p1, p2) # This is the function call, 
                     # so d should be a Distance object
print d.m

should print the distance between p1 and p2 in meters.

Instead, we got the following error:

AttributeError: 'Distance' object has no attribute 'm'

We found a workaround eventually (d = Distance(m=p1.distance(p2))) but the question remains:

Why the Distance function didn't return a Distance object?
Is this a bug, or we missed something?

Thanks in advance for your time.

John Moutafis
  • 22,254
  • 11
  • 68
  • 112

1 Answers1

5

Those two aren't that closely related to each other. Docs do indeed say that it "returns" Distance object, but there is an extra step:

django.contrib.gis.db.models.functions.Distance is a database function, it takes two expressions (that may include database field names) and returns a Func object that can be used as a part of a query.

So simply put, it needs to be executed in a database. It will calculate distance using database function (e.g. postgis ST_Distance) and then bring it back as a django.contrib.gis.measure.Distance object.

So, unless one wants to mess with SQL compilers and db connections, easiest way to get Distance between two points is Distance(m=p1.distance(p2))

EDIT: Some code to illustrate the point:

You can check out code for Distance (measurement) class in django/contrib/gis/measure.py. It's fairly small and easy to understand. All it's doing is giving you a convenient way to do conversion, comparison and arithmetic operations with distances:

In [1]: from django.contrib.gis.measure import Distance

In [2]: d1 = Distance(mi=10)

In [3]: d2 = Distance(km=15)

In [4]: d1 > d2
Out[4]: True

In [5]: d1 + d2
Out[5]: Distance(mi=19.32056788356001)

In [6]: _.km
Out[6]: 31.09344

Now, let's take a look at the Distance function:

Add __str__ method to the model so we can see distance value when it returned by the queryset api and short db_table name so we can look in the queries:

class MyModel(models.Model):
    coordinates = models.PointField()

    class Meta:
        db_table = 'mymodel'

    def __str__(self):
        return f"{self.coordinates} {getattr(self, 'distance', '')}"

Create some objects and do a simple select * from query:

In [7]: from gisexperiments.models import MyModel

In [8]: from django.contrib.gis.geos import Point

In [10]: some_places = MyModel.objects.bulk_create(
    ...:     MyModel(coordinates=Point(i, i, srid=4326)) for i in range(1, 5)
    ...: )

In [11]: MyModel.objects.all()
Out[11]: <QuerySet [<MyModel: SRID=4326;POINT (1 1) >, <MyModel: SRID=4326;POINT (2 2) >, <MyModel: SRID=4326;POINT (3 3) >, <MyModel: SRID=4326;POINT (4 4) >]>

In [12]: str(MyModel.objects.all().query)
Out[12]: 'SELECT "mymodel"."id", "mymodel"."coordinates" FROM "mymodel"'

Boring. Let's use Distance function to add distance value to the result:

In [14]: from django.contrib.gis.db.models.functions import Distance

In [15]: from django.contrib.gis.measure import D  # an alias

In [16]: q = MyModel.objects.annotate(dist=Distance('coordinates', origin))

In [17]: list(q)
Out[17]:
[<MyModel: SRID=4326;POINT (1 1) 157249.597768505 m>,
 <MyModel: SRID=4326;POINT (2 2) 314475.238061007 m>,
 <MyModel: SRID=4326;POINT (3 3) 471652.937856715 m>,
 <MyModel: SRID=4326;POINT (4 4) 628758.663018087 m>]

In [18]: str(q.query)
Out[18]: 'SELECT "mymodel"."id", "mymodel"."coordinates", ST_distance_sphere("mymodel"."coordinates", ST_GeomFromEWKB(\'\\001\\001\\000\\000 \\346\\020\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\\000\'::bytea)) AS "distance" FROM "mymodel"'

You can see it uses ST_distance_sphere sql function do calculate distance using values from "mymodel"."coordinates" and byte representation of our origin point.

We can now use it for filtering and ordering and lots of other things, all inside the database management system (fast):

In [19]: q = q.filter(distance__lt=D(km=400).m)

In [20]: list(q)
Out[20]:
[<MyModel: SRID=4326;POINT (1 1) 157249.597768505 m>,
 <MyModel: SRID=4326;POINT (2 2) 314475.238061007 m>]

Notice .m you need to pass float number to the filter, it won't be able to recognize Distance object.

Igonato
  • 10,175
  • 3
  • 35
  • 64
  • Let me know if I need to add some code to the answer. I wanted to but struggled to get something minimal. I'll try again later – Igonato May 12 '17 at 10:25
  • Thank you for the response @Igonato , if you can provide a code sample that would be nice! I will keep the bounty up in case of other responses :) – John Moutafis May 12 '17 at 10:59
  • @JohnMoutafis added some code. I'm quite new to GeoDjango, maybe there is a better way to explain it, but this is my best. Also, fixed an error. Function Distance returns Func, not a Lookup object – Igonato May 13 '17 at 11:10