13

I have the following model.

class Location(models.Model):
    name = models.CharField(max_length = 128, blank = True)
    address =models.CharField(max_length = 200, blank= True)
    latitude = models.DecimalField(max_digits=6, decimal_places=3)
    longitude = models.DecimalField(max_digits=6, decimal_places=3)

    def __unicode__(self):
        return self.name

If my current latitude & longitude is:

current_lat = 43.648
current_long = 79.404

I did some research and came across the Haversine Equation which calculates the distance between two location coordinates. Below is the equation I found:

import math

def distance(origin, destination):
    lat1, lon1 = origin
    lat2, lon2 = destination
    radius = 6371 # km

    dlat = math.radians(lat2-lat1)
    dlon = math.radians(lon2-lon1)
    a = math.sin(dlat/2) * math.sin(dlat/2) + math.cos(math.radians(lat1)) \
        * math.cos(math.radians(lat2)) * math.sin(dlon/2) * math.sin(dlon/2)
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
    d = radius * c

    return d

I would like to return all the Location objects that fall within a 10 km radius, how can I filter it in such a way that it will only return all the Location objects that fall within this 10 km radius?

LocationsNearMe = Location.objects.filter(#This is where I am stuck)

Is there anyway I can implement the Haversine equation into the filtering so that it only returns the location objects that fall within a 10 km radius?

I'm looking for a well detailed answer. Appreciate the help.

cezar
  • 11,616
  • 6
  • 48
  • 84
deadlock
  • 7,048
  • 14
  • 67
  • 115
  • 1
    you should try and use geodjango https://docs.djangoproject.com/en/dev/ref/contrib/gis/ – user710907 Jul 16 '13 at 16:39
  • Definitely look into geodjango, I have a solution for this problem but it requires geodjango: https://gist.github.com/omouse/5623772 –  Jul 16 '13 at 17:32
  • 1
    @omouse thanks for leading me in the right direction, in your solution you have point, but here in this model, it's separated into two fields: longitude and latitude. Could you put your answer for this case below in the answer section. – deadlock Jul 16 '13 at 17:47

3 Answers3

13

You can do range queries with filter.

LocationsNearMe = Location.objects.filter(latitude__gte=(the minimal lat from distance()),
                                          latitude__lte=(the minimal lat from distance()),
                                          (repeat for longitude))

Unfortunately, this returns results in the form of a geometric square (instead of a circle)

Brian
  • 7,394
  • 3
  • 25
  • 46
12

But you can always make proposed by Brian approach better by filtering the results from previous step (which hoepfully should be smaller subset) and for each you check either they are within the radius.

Your user is in black point. Square approximation given by Brian return green but also orange points. The divernce in distance can be significant in worst case user have to go sqrt(2) times further than expected (extra 40% of distance). So for all orange and green points it is worth to check if their distance from black point (e.g euclidian one if this are really short distances e.g navigation in city) is not greater than assumed radius.

enter image description here

UPDATE:

If you would like to use Haversine distance or (better) mentioned GeoDjango hava a look on this snippet comparing two django views dealing with nearby search:

https://gist.github.com/andilabs/4232b463e5ad2f19c155

andilabs
  • 22,159
  • 14
  • 114
  • 151
5

If you don't want to use GeoDjango, then you could consider writing it out with Django's Database functions. In contrast to raw SQL, this also gives you the advantage of being able to easily append/prepend other ORM filters.

from django.db.models.functions import Radians, Power, Sin, Cos, ATan2, Sqrt, Radians
from django.db.models import F

dlat = Radians(F('latitude') - current_lat)
dlong = Radians(F('longitude') - current_long)

a = (Power(Sin(dlat/2), 2) + Cos(Radians(current_lat)) 
    * Cos(Radians(F('latitude'))) * Power(Sin(dlong/2), 2)
)

c = 2 * ATan2(Sqrt(a), Sqrt(1-a))
d = 6371 * c

LocationsNearMe = Location.objects.annotate(distance=d).order_by('distance').filter(distance__lt=10)
Beolap
  • 768
  • 9
  • 15
  • Where does all this fit into your `models.py`? – cjm Nov 25 '19 at 01:00
  • 1
    Create a class LocationQuerySet, which inherits from models.QuerySet and add `objects = LocationQuerySet.as_manager()` to your Location class. Then create a custom function in LocationQuerySet `def locations_near_x_within_y_km(self, current_lat, current_long, y_km)`, which does everything mentioned above and returns `self.annotate(distance=d).order_by('distance').filter(distance__lt=y_km)`. Now you can use `LocationsNearMe = Location.objects.locations_near_x_within_y_km(50,50,10)`. – Beolap Nov 25 '19 at 10:33
  • What is 10 here? Is that a mile or km? – Mohammad Ashraful Islam Apr 21 '21 at 04:35
  • It’s the parameter “y_km”, so km. – Beolap Apr 21 '21 at 07:03
  • what is `distance` in last line? – Hamza Lachi Jun 03 '21 at 19:48
  • @HamzaLachi It's an annotated field. You can name it anything. It basically stays with your query set, so that you can e.g. filter or order after it, like I do in the example. – Beolap Jun 03 '21 at 21:16