问题
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