Джанго сортировать по расстоянию
У меня есть следующая модель:
class Vacancy(models.Model):
lat = models.FloatField('Latitude', blank = True)
lng = models.FloatField('Longitude', blank = True)
Как сделать запрос на сортировку по расстоянию (расстояние равно бесконечности)?
Работаем над PosgreSQL, GeoDjango, если это требуется.
Спасибо.
8 ответов
Прежде всего, лучше сделать точечное поле, а не разделять lat и lnt:
from django.contrib.gis.db import models
location = models.PointField(null=False, blank=False, srid=4326, verbose_name="Location")
Затем вы можете отфильтровать это так:
from django.contrib.gis.geos import *
from django.contrib.gis.measure import D
distance = 2000
ref_location = Point(1.232433, 1.2323232)
res = yourmodel.objects.filter(location__distance_lte=(ref_location, D(m=distance))).distance(ref_location).order_by('distance')
.distance(ref_location)
удаляется в django >=1.9, вместо этого вы должны использовать аннотацию.
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.measure import D
from django.contrib.gis.geos import Point
ref_location = Point(1.232433, 1.2323232, srid=4326)
yourmodel.objects.filter(location__distance_lte=(ref_location, D(m=2000)))
.annotate(distance=Distance("location", ref_location))
.order_by("distance")
также вы должны сузить область поиска с помощью dwithin
Оператор, который использует пространственный индекс, расстояние не использует индекс, который замедляет ваш запрос:
yourmodel.objects.filter(location__dwithin=(ref_location, 0.02))
.filter(location__distance_lte=(ref_location, D(m=2000)))
.annotate(distance=Distance('location', ref_location))
.order_by('distance')
см. этот пост для объяснения location__dwithin=(ref_location, 0.02)
Вот решение, которое не требует GeoDjango с использованием собственного менеджера.
class LocationManager(models.Manager):
def nearby(self, latitude, longitude, proximity):
"""
Return all object which distance to specified coordinates
is less than proximity given in kilometers
"""
# Great circle distance formula
gcd = """
6371 * acos(
cos(radians(%s)) * cos(radians(latitude))
* cos(radians(longitude) - radians(%s)) +
sin(radians(%s)) * sin(radians(latitude))
)
"""
return self.get_queryset()\
.exclude(latitude=None)\
.exclude(longitude=None)\
.annotate(distance=RawSQL(gcd, (latitude,
longitude,
latitude)))\
.filter(distance__lt=proximity)\
.order_by('distance')
class Location(models.Model):
objects = LocationManager()
latitude = models.FloatField()
longitude = models.FloatField()
...
Используйте как следовать:
eiffel_tower_5k = Location.objects.nearby(48.8582, 2.2945, 5)
Если вы используете sqlite, вам нужно добавить куда-нибудь
from django.db.backends.signals import connection_created
from django.dispatch import receiver
@receiver(connection_created)
def extend_sqlite(connection=None, **kwargs):
if connection.vendor == "sqlite":
# sqlite doesn't natively support math functions, so add them
cf = connection.connection.create_function
cf('acos', 1, math.acos)
cf('cos', 1, math.cos)
cf('radians', 1, math.radians)
cf('sin', 1, math.sin)
Большая часть информации устарела, поэтому я отвечу, как мне кажется, самой актуальной информацией.
С помощью geography=True
с GeoDjango делает это намного проще. Это означает, что все хранится в lng / lat, но расчеты расстояния выполняются в метрах на поверхности сферы. Посмотреть документы
from django.db import models
from django.contrib.gis.db.models import PointField
class Vacancy(models.Model):
location = PointField(srid=4326, geography=True, blank=True, null=True)
Вы можете отсортировать всю таблицу, используя следующий запрос, но он использует ST_Distance, которая может быть медленной, если она выполняется для каждой записи и имеется много записей. Обратите внимание, что "сортировка по расстоянию" неявно требует расстояния от чего-либо. Первый аргумент Point
это долгота, а вторая широта (противоположность обычного соглашения).
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
ref_location = Point(140.0, 40.0, srid=4326)
Vacancy.objects.annotate(distance=Distance("location", ref_location))\
.order_by("distance")
Вы можете оптимизировать свой запрос, если существует максимальное расстояние, для которого вы хотите результаты. dwithin
Запрос django использует ST_DWithin, что означает, что он очень быстрый. Установка географии =True означает, что этот расчет выполняется в метрах, а не в градусах. Это означает, что вам никогда не нужно использовать distance_lte
, который использует ST_Distance и будет медленным. Окончательный запрос для всего в пределах 50 км будет:
from django.contrib.gis.db.models.functions import Distance
from django.contrib.gis.geos import Point
ref_location = Point(140.0, 40.0, srid=4326)
Vacancy.objects.filter(location__dwithin=(ref_location, 50000))\
.annotate(distance=Distance("location", ref_location))\
.order_by("distance")
Второй аргумент dwithin
также принимает django.contrib.gis.measure.D
объекты, которые он превращает в метры, поэтому вместо 50000
метров, вы могли бы просто использовать D(km=50)
,
На Django 3.0 будет GeometryDistance
функция, которая работает так же, как Distance
, но использует <->
вместо этого оператор, который использует пространственные индексы на ORDER BY
запросы, устраняя необходимость в dwithin
фильтр:
from django.contrib.gis.db.models.functions import GeometryDistance
from django.contrib.gis.geos import Point
ref_location = Point(140.0, 40.0, srid=4326)
Vacancy.objects.annotate(
distance=GeometryDistance('location', ref_location)
).order_by('distance')
Если вы хотите использовать его до выпуска Django 3.0, вы можете использовать что-то вроде этого:
from django.contrib.gis.db.models.functions import GeoFunc
from django.db.models import FloatField
from django.db.models.expressions import Func
class GeometryDistance(GeoFunc):
output_field = FloatField()
arity = 2
function = ''
arg_joiner = ' <-> '
geom_param_pos = (0, 1)
def as_sql(self, *args, **kwargs):
return Func.as_sql(self, *args, **kwargs)
Если вы не хотите / не имеете возможности использовать gis, вот решение (автор писем о расстояниях от haversine в django orm sql):
lat = 52.100
lng = 21.021
earth_radius=Value(6371.0, output_field=FloatField())
f1=Func(F('latitude'), function='RADIANS')
latitude2=Value(lat, output_field=FloatField())
f2=Func(latitude2, function='RADIANS')
l1=Func(F('longitude'), function='RADIANS')
longitude2=Value(lng, output_field=FloatField())
l2=Func(longitude2, function='RADIANS')
d_lat=Func(F('latitude'), function='RADIANS') - f2
d_lng=Func(F('longitude'), function='RADIANS') - l2
sin_lat = Func(d_lat/2, function='SIN')
cos_lat1 = Func(f1, function='COS')
cos_lat2 = Func(f2, function='COS')
sin_lng = Func(d_lng/2, function='SIN')
a = Func(sin_lat, 2, function='POW') + cos_lat1 * cos_lat2 * Func(sin_lng, 2, function='POW')
c = 2 * Func(Func(a, function='SQRT'), Func(1 - a, function='SQRT'), function='ATAN2')
d = earth_radius * c
Shop.objects.annotate(d=d).filter(d__lte=10.0)
PS измените модели, измените фильтр на order_by, измените ключевое слово и настройте параметры
PS2 для sqlite3, вы должны убедиться, что есть доступные функции SIN, COS, RADIANS, ATAN2, SQRT
Если вы не хотите изменять свои модели, то есть сохраняете lat и lng как отдельные поля и даже не хотите использовать слишком много Geodjango и хотите решить эту проблему с помощью некоторого базового кода, то вот решение;
origin = (some_latitude, some_longitude) #coordinates from where you want to measure distance
distance = {} #creating a dict which will store the distance of users.I am using usernames as keys and the distance as values.
for m in models.objects.all():
dest = (m.latitude, m.longitude)
distance[m.username] = round(geodesic(origin, dest).kilometers, 2) #here i am using geodesic function which takes two arguments, origin(coordinates from where the distance is to be calculated) and dest(to which distance is to be calculated) and round function rounds off the float to two decimal places
#Here i sort the distance dict as per value.So minimum distant users will be first.
s_d = sorted(distance.items(), key=lambda x: x[1]) #note that sorted function returns a list of tuples as a result not a dict.Those tuples have keys as their first elements and vaues as 2nd.
new_model_list = []
for i in range(len(s_d)):
new_model_list.append(models.objects.get(username=s_d[i][0]))
Теперь new_model_list будет содержать всех пользователей, упорядоченных по расстоянию. Путем итерации по нему вы получите их упорядоченные на основе расстояния.
в views.py используйте CustomHaystackGEOSpatialFilter для filter_backends:
class LocationGeoSearchViewSet(HaystackViewSet):
index_models = [yourModel]
serializer_class = LocationSerializer
filter_backends = [CustomHaystackGEOSpatialFilter]
в Filters.py определите CustomHaystackGEOSpatialFilter и переопределите метод apply_filters, чтобы вы могли упорядочить расстояние и ограничить количество результатов, например:
class CustomHaystackGEOSpatialFilter(HaystackGEOSpatialFilter):
# point_field = 'location'
def apply_filters(self, queryset, applicable_filters=None, applicable_exclusions=None):
if applicable_filters:
queryset = queryset.dwithin(**applicable_filters["dwithin"]).distance(
**applicable_filters["distance"]).order_by("distance")[:100]
return queryset