Django's Distance function not returning a Distance object

对着背影说爱祢 提交于 2019-12-01 06:52:02

问题


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.


回答1:


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.



来源:https://stackoverflow.com/questions/43864292/djangos-distance-function-not-returning-a-distance-object

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!