Como calcular distância e ordenar os resultados usando GeoDjango

Tutorial demonstrativo em API com Django Rest Framework

Marcus Beckenkamp
Beck Blog
Published in
3 min readFeb 1, 2016

--

No final do ano passado eu comecei a desenvolver a API que serve de backend para um aplicativo mobile que ordena registros usando a distância dos mesmos para o usuário do app. Um dos primeiros desafios que enfrentei foi usar a geolocalização. Eu nunca havia trabalhado com ferramentas desse tipo antes e foi muito interessante ter a oportunidade de aprender.

Nesta publicação vou abordar como usar a localização do usuário para calcular a distância e ordenar os resultados de registros em um endpoint de API Rest, levando em conta que cada registro tenha um ponto de geolocalização, ou seja, latitude e longitude.

Para fazer isso é preciso utilizar um conjunto de ferramentas específicas para geolocalização que o Django possui, que são conhecidas como GeoDjango.

Nesse texto não irei cobrir as instalações e configurações específicas do banco de dados. Mas elas podem ser vistas aqui.

INSTALLED_APPS = ( 

‘django.contrib.gis’,

)

Começamos adicionando django.contrib.gis no nosso arquivo settings.py e partimos para os nossos models.

from django.contrib.gis.db import models  
class AddressBaseModel(models.Model):
"""
Abstract class to be extended on models that have address information
"""
address = models.CharField(max_length=200, blank=True, null=True)
address_extra = models.CharField(max_length=200, blank=True, null=True)
neighborhood = models.CharField(max_length=200, blank=True, null=True)
city = models.ForeignKey('City', blank=True, null=True)
zipcode = models.CharField(max_length=200, blank=True, null=True)
location = models.PointField(blank=True, null=True)
class Meta:
abstract = True

Ao invés de importar os models comuns do Django, importamos os models do django.contrib.gis.db. Nesse caso eu criei um abstract model que vai ser utilizado em todos os outros que necessitarem de endereço e localização.

O campo location vem com um tipo especial, o PointField, que irá guardar os dados de geolocalização do endereço.

No model que extende nosso AddressBaseModel utilizamos o GeoManager para substituir o manager dos models comuns do Django.

class Stuff(AddressBaseModel):
user = models.ForeignKey(User)
# overriding the default manager with a GeoManager instance
objects = models.GeoManager()

Agora podemos ordenar o resultado pela distância de um ponto específico.

Na view vamos utilizar o método distance do queryset para “criar” um campo com o mesmo nome e utilizar o order para ordenar por esse novo campo distance.

Para o método distance, precisamos passar uma origem. Origem será o local que o usuário está, nesse caso. Para transformar as strings com a latitude e longitude em um ponto de geolocalização, utilizamos a classe Point do django.contrib.gis.geos.

from django.contrib.gis.geos import Point
from rest_framework import viewsets, mixins
from .models import Stuff
class ListView(mixins.ListModelMixin, viewsets.GenericViewSet):
"""
API endpoint that allows stuff to be viewed ordered by distance. Requires ?lat={}&lon={}
"""
def get_queryset(self):
lat = self.request.query_params.get('lat', None)
lon = self.request.query_params.get('lon', None)
if not lat or not lon:
raise ValueError('Faltam parametros para completar esta acao. lat = {} | lon = {}.'.format(repr(lat), repr(lon)))
origin = Point(float(lat), float(lon)) queryset = Stuff.objects \
.exclude(user=self.request.user) \
.all() \
.distance(origin, field_name="location") \
.order_by('distance')
return queryset

Para formatar o resultado eu criei um método get_distance no StuffSerializer. O objetivo dele é mostrar a distância em metros ou quilômetros.

from rest_framework import serializers
from .models import Stuff

class StuffSerializer(serializers.HyperlinkedModelSerializer):
distance = serializers.SerializerMethodField()

class Meta:
model = Stuff
fields = ('url', 'user', 'distance')
def get_distance(self, obj):
if hasattr(obj, 'distance'):
if hasattr(obj.distance, 'm'):
if obj.distance.m < 1000:
return "{0:.0f} m".format(obj.distance.m)
else:
return "{0:.0f} km".format(obj.distance.km)
return None

O resultado será algo como o exemplo abaixo.

{
"count": 3,
"next": null,
"previous": null,
"results": [
{
"url": "http://127.0.0.1:8000/stuffs/1/",
"user": "http://127.0.0.1:8000/users/1/",
"distance": "150 m"
},
{
"url": "http://127.0.0.1:8000/stuffs/4/",
"user": "http://127.0.0.1:8000/users/5/",
"distance": "2 km"
},
{
"url": "http://127.0.0.1:8000/stuffs/3/",
"user": "http://127.0.0.1:8000/users/18/",
"distance": "28 km"
}
]
}

Este é apenas um exemplo de como podemos utilizar o GeoDjango para calcular a distância e ordenar os resultados através dela, mas existem diversas outras formas de fazer isso também.

Texto adaptado do original publicado no meu antigo blog em 1º de fevereiro de 2016.

Estou usando esse espaço para colocar em prática minha vontade de escrever sobre qualquer coisa (incluindo tutoriais!). Se você gostou e quer me incentivar a escrever mais, deixe seu “recomendar” clicando no coração abaixo do texto. ;)

--

--

I write code and stories - Senior Software Engineer @ Press Hook | Founder @ Fliptru