Как эффективно отфильтровать местоположения в базе данных, не перебирая их вручную?
В моем проекте DRF есть модель, структурированная следующим образом
class ServiceLocation(models.Model):
'''
Represents a location where an internet service is offered
'''
SERVICE_TYPES = [
("wifi", 'wifi'),
("fibre", "fibre"),
("p2p/ptmp", "p2p/ptmp")
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4,
editable=False, null=False, blank=False)
description = models.TextField()
# Location
address = models.CharField(max_length=150, null=False, blank=False)
latitude = models.DecimalField(max_digits=18, decimal_places=15)
longitude = models.DecimalField(max_digits=18, decimal_places=15)
# Service
service = models.CharField(
max_length=10, choices=SERVICE_TYPES, null=False, blank=False)
speed = models.IntegerField()
def __str__(self):
return f"{self.service} by {self.operator}"
и я пытаюсь отфильтровать экземпляры этой модели по их относительной близости к заданной координате.
Мое представление структурировано следующим образом
class CloseServiceLocations(View):
def get(self, request):
lat = request.GET.get('lat', 6.748134)
lng = request.GET.get('lng', 3.633301)
distance = request.GET.get('distance', 10) # Default distance to 10 if not provided
# if lat is None or lng is None:
# # return JsonResponse({'error': 'Latitude and Longitude are required parameters.'}, status=400)
try:
lat = float(lat)
lng = float(lng)
distance = float(distance)
except ValueError:
return JsonResponse({'error': 'Invalid latitude, longitude, or distance provided.'}, status=400)
# Create a Point object representing the provided latitude and longitude
user_location = Point(lng, lat, srid=4326)
# Calculate the distance in meters (Django's Distance function uses meters)
distance_in_meters = distance * 1000
close_service_locations = ServiceLocation.objects.annotate(
# Convert longitude and latitude fields to floats
longitude_float=Cast('longitude', FloatField()),
latitude_float=Cast('latitude', FloatField())
).annotate(
# Create Point object using converted longitude and latitude
location=Point(F('longitude_float'), F('latitude_float'), srid=4326)
).annotate(
# Calculate distance
distance=Distance('location', user_location)
).filter(distance__lte=distance_in_meters)
# Serialize the queryset to JSON
serialized_data = [{'id': location.id,
'description': location.description,
'operator': location.operator.name,
'address': location.address,
'latitude': location.latitude,
'longitude': location.longitude,
'service': location.service,
'speed': location.speed} for location in close_service_locations]
return JsonResponse(serialized_data, safe=False)
def post(self, request):
return JsonResponse({'error': 'Method not allowed'}, status=405)
где я пытаюсь аннотировать новый атрибут "location", чтобы я мог воспользоваться методом Distance из from django.contrib.gis.db.models.functions
для вычисления расстояния вместо того, чтобы зацикливаться и вычислять расстояние по Гаверсину вручную, что было моим первоначальным подходом.
Когда я запускаю это, я получаю сообщение об ошибке сервера, которое, я почти уверен, не из моих представлений.
В попытке исправить это, я разбил свои представления на секции и добавил операторы печати, чтобы увидеть, какая часть вызывает поломку
print("Annotating QS with lon/lat float... ")
close_service_locations = ServiceLocation.objects.annotate(
# Convert longitude and latitude fields to floats
longitude_float=Cast('longitude', FloatField()),
latitude_float=Cast('latitude', FloatField())
)
print("LON/LAT float annotation complete")
print("Annotating QS with location point... ")
print("Size: ", len(close_service_locations))
close_service_locations = close_service_locations.annotate(
# Create Point object using converted longitude and latitude
location=Point('longitude_float', 'latitude_float', srid=4326)
)
print("Location point annotation complete")
print("Annotating QS with relative distance... ")
close_service_locations = close_service_locations.annotate(
# Calculate distance
distance=Distance('location', user_location)
)
print("Distance annotation complete")
Я заметил, что блок print("Annotating QS with lon/lat float... ")
выполняется успешно и быстро, но он обрывается в блоке print("Annotating QS with location point... ")
, где я пытаюсь аннотировать QS с помощью атрибута location
В какой-то момент я получил ошибку "Invalid parameters given for Point initialization.", которая заставила меня добавить блок print("Annotating QS with lon/lat float... ")
, чтобы заставить все объекты Decimalfiled превратиться в floats.
Я также попробовал вручную просмотреть close_service_locations, чтобы узнать, есть ли у меня местоположение службы с недопустимой широтой или долготой, с помощью этого
for i in range(len(close_service_locations)):
print(i)
location=Point(close_service_locations[i].longitude_float, close_service_locations[i].latitude_float, srid=4326)
На удивление, все прошло успешно.
Но я все еще не знаю, как заставить мой вид успешно работать после этой точки.
Это ошибка, которую я продолжаю получать
Annotating QS with lon/lat float...
LON/LAT float annotation complete
Annotating QS with location point...
[13/Apr/2024 04:49:30] "GET /directory/close-service-locations/ HTTP/1.1" 500 145
В браузере нет страницы ошибки Django с подробным описанием причины, только большая ошибка сервера
Я также попробовал вручную пройтись по сервисам-локациям, добавляя атрибут location и выводя индекс, возможно, мне удастся понять, что именно нарушает мой код или есть ли сервис-локация, чья пара долготы и высоты не может быть преобразована в объект Point, но я все равно получил ту же ошибку
В следующем коде вы предоставляете строки (longitude_float
latitude_float и latitude_float
) классу Point
.
close_service_locations = close_service_locations.annotate(
# Create Point object using converted longitude and latitude
location=Point('longitude_float', 'latitude_float', srid=4326)
)
Вы можете подумать, что вы создаете это значение выше, но это в строке longitude_float=Cast('longitude', FloatField()),
, а в следующей аннотации оно рассматривается как просто строка.
Вы можете попробовать следующее, чтобы заставить его работать.
from django.contrib.gis.db.models import F
close_service_locations = ServiceLocation.objects.annotate(
# Convert longitude and latitude fields to floats
longitude_float=Cast('longitude', FloatField()),
latitude_float=Cast('latitude', FloatField()),
location=Point(F('longitude_float'), F('latitude_float'), srid=4326)
# in the same code.
)
EDIT: Другой способ сделать это - использовать Python вместо Django.
close_service_locations = ServiceLocation.objects.annotate(
# Convert longitude and latitude fields to floats
longitude_float=Cast('longitude', FloatField()),
latitude_float=Cast('latitude', FloatField())
)
service_location_mapping = {}
for service in close_service_locations:
location = Point(service.longitude_float, service.latitude_float, srid=4326)
distance = Distance(location, user_location)
service_location_mapping[service] = {
'distance': distance,
'service': service
}
Я придумал другой подход, основанный на том, насколько точны десятичные цифры в координатах на google maps, и он сработал.
def get(self, request):
lat = request.GET.get('lat', 8.464134)
lng = request.GET.get('lng', 4.454301)
distance = request.GET.get('distance', 10) # Default distance to 10 km if not provided
try:
lat = float(lat)
lng = float(lng)
distance = float(distance)
except ValueError:
return JsonResponse({'error': 'Invalid latitude, longitude, or distance provided.'}, status=400)
# Define the approximate distance difference in degrees (assuming 1 degree is approximately 111 km)
diff = distance / 111
print("Getting SLs .....")
close_locations = ServiceLocation.objects.filter(latitude__gte=(lat-diff), latitude__lte=(lat+diff), longitude__gte=(lng-diff), longitude__lte=(lng+diff), )
print("Close locations length ", len(close_locations))
serialized = ServiceLocationSerializer(close_locations, many=True)
print(serialized.data)
return Response(serialized.data, status=status.HTTP_200_OK)