Django сортировать по расстоянию
У меня есть следующая модель:
class Vacancy(models.Model):
lat = models.FloatField('Latitude', blank=True)
lng = models.FloatField('Longitude', blank=True)
Как создать запрос для сортировки по расстоянию (расстояние равно бесконечности)?
Работаем над PosgreSQL, GeoDjango, если это требуется.
Ответы
Ответ 1
Прежде всего, лучше сделать точечное поле, а не разделять 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')
Ответ 2
.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
оператора 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)
Ответ 3
Вот решение, которое не требует GeoDjango.
from django.db import models
from django.db.models.expressions import RawSQL
class Location(models.Model):
latitude = models.FloatField()
longitude = models.FloatField()
...
def get_locations_nearby_coords(latitude, longitude, max_distance=None):
"""
Return objects sorted by distance to specified coordinates
which distance is less than max_distance given in kilometers
"""
# Great circle distance formula
gcd_formula = "6371 * acos(least(greatest(\
cos(radians(%s)) * cos(radians(latitude)) \
* cos(radians(longitude) - radians(%s)) + \
sin(radians(%s)) * sin(radians(latitude)) \
, -1), 1))"
distance_raw_sql = RawSQL(
gcd_formula,
(latitude, longitude, latitude)
)
qs = Location.objects.all() \
.annotate(distance=distance_raw_sql))\
.order_by('distance')
if max_distance is not None:
qs = qs.filter(distance__lt=max_distance)
return qs
Используйте следующим образом:
nearby_locations = get_locations_nearby_coords(48.8582, 2.2945, 5)
Если вы используете sqlite, вам нужно добавить куда-нибудь
import math
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 does not 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)
cf('least', 2, max)
cf('greatest', 2, min)
Ответ 4
Большая часть информации устарела, поэтому я отвечу самой актуальной информацией.
Использование 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)
.
Ответ 5
Если вы не хотите/не имеете возможности использовать gis, вот разрешение (haversine distance fomula writer в 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
Ответ 6
В 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)