Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add distance from point as an annotation in GeoDjango

I have a Geographic Model with a single PointField, I'm looking to add an annotation for the distance of each model from a given point, which I can later filter on and do additional jiggery pokery.

There's the obvious queryset.distance(to_point) function, but this doesn't actually annotate the queryset, it just adds a distance attribute to each model in the queryset, meaning I can't then apply .filter(distance__lte=some_distance) to it later on.

I'm also aware of filtering by the field and distance itself like so:

queryset.filter(point__distance_lte=(to_point, D(mi=radius)))

but since I will want to do multiple filters (to get counts of models within different distance ranges), I don't really want to make the DB calculate the distance from the given point every time, since that could be expensive.

Any ideas? Specifically, is there a way to add this as a regular annotation rather than an inserted attribute of each model?

like image 909
Tom Manterfield Avatar asked Jun 12 '14 10:06

Tom Manterfield


2 Answers

I couldn't find any baked in way of doing this, so in the end I just created my own Aggregation class:

This only works with post_gis, but making one for another geo db shouldn't be too tricky.

from django.db.models import Aggregate, FloatField
from django.db.models.sql.aggregates import Aggregate as SQLAggregate


class Dist(Aggregate):
    def add_to_query(self, query, alias, col, source, is_summary):
        source = FloatField()
        aggregate = SQLDist(
            col, source=source, is_summary=is_summary, **self.extra)
        query.aggregates[alias] = aggregate


class SQLDist(SQLAggregate):
    sql_function = 'ST_Distance_Sphere'
    sql_template = "%(function)s(ST_GeomFromText('%(point)s'), %(field)s)"

This can be used as follows:

queryset.annotate(distance=Dist('longlat', point="POINT(1.022 -42.029)"))

Anyone knows a better way of doing this, please let me know (or tell me why mine is stupid)

like image 95
Tom Manterfield Avatar answered Sep 30 '22 18:09

Tom Manterfield


One of the modern approaches is the set "output_field" arg to avoid «Improper geometry input type: ». Withour output_field django trying to convert ST_Distance_Sphere float result to GEOField and can not.

    queryset = self.objects.annotate(
        distance=Func(
            Func(
                F('addresses__location'),
                Func(
                    Value('POINT(1.022 -42.029)'),
                    function='ST_GeomFromText'
                ),
                function='ST_Distance_Sphere',
                output_field=models.FloatField()
            ),
            function='round'
        )
    )
like image 27
Eugene Osiptsov Avatar answered Sep 30 '22 18:09

Eugene Osiptsov