postgres + django: filter_in_range that allows quick filtering for locations in N range from X/Y (lat/lng) (earth_box, ll_to_earth, cube, earthdistance)
Python function that will annonate your queryset using Postgres earth_box and ll_to_earth functions that allow filtering in range N kilometers from given latitude/longitude point.
IMPORTANT:
https://www.postgresql.org/docs/devel/static/earthdistance.html
In this module, the Earth is assumed to be perfectly spherical. (If that's too inaccurate for you, you might want to look at the PostGIS project).
pip install git+https://github.com/bukowa/django-georangefilter
Results for filtering in 15km radius, with second circle 30km radius, as you can see its pretty inaccurate.
If you want to test in your custom radius, take a look at tests.py file.
For example, given model:
class City(models.Model):
longitude = models.FloatField(db_index=True)
latitude = models.FloatField(db_index=True)
def in_range(self, range_in_meters, **kwargs):
return filter_in_range(
queryset=self.__class__.objects,
latitude=self.latitude,
longitude=self.longitude,
range_in_meters=range_in_meters,
latitude_column_name="latitude",
longitude_column_name="longitude",
**kwargs,
)
Get all cities in range 10km from city:
city = City.objects.all()[0]
cities_in_range = city.in_range(10000)
How the sql query really looks like:
{'sql': 'SELECT "app_city"."longitude", "app_city"."latitude", earth_box(ll_to_earth(51.03923, 16.97184), 10000) AS "earthbox" FROM "X" WHERE earth_box(ll_to_earth(51.03923, 16.97184), 10000) @> (ll_to_earth("X"."latitude", "X"."longitude")) LIMIT 21', 'time': '0.004'}
Requires cube and earthdistance extensions. To enable them you can:
operations = [
CreateExtension("cube"),
CreateExtension("earthdistance"),
# you can also create an index like that:
migrations.RunSQL(
"CREATE INDEX indexname ON yourapp_yourmodel USING gist(ll_to_earth(latitude_column_name, longitude_column_name));"
),
]
or
from django.db import connection
cursor = connection.cursor()
cursor.execute("CREATE EXTENSION cube;")
cursor.execute("CREATE EXTENSION earthdistance;")
How the functions looks like:
def filter_in_range(
queryset: QuerySet,
latitude: float,
longitude: float,
range_in_meters: int,
latitude_column_name: str = "latitude",
longitude_column_name: str = "longitude",
field_name: str = "earthbox",
lookup_exp: str = "in_georange",
):
earthbox = {field_name: EarthBox(LLToEarth(latitude, longitude), range_in_meters)}
lookup = "%s__%s" % (field_name, lookup_exp)
in_range = {lookup: LLToEarth(latitude_column_name, longitude_column_name)}
return queryset.annotate(**earthbox).filter(**in_range)
data for map generation used from geonames.org